Add addons
This commit is contained in:
parent
2c862b2235
commit
3367bad24f
527 changed files with 36197 additions and 0 deletions
|
|
@ -0,0 +1,183 @@
|
|||
@tool
|
||||
extends SyntaxHighlighter
|
||||
|
||||
## Syntax highlighter for the dialogic text timeline editor.
|
||||
|
||||
## RegEx's
|
||||
var word_regex := RegEx.new()
|
||||
var region_regex := RegEx.new()
|
||||
var text_event_regex := RegEx.new()
|
||||
var character_event_regex := RegEx.new()
|
||||
var shortcode_regex := RegEx.new()
|
||||
var shortcode_param_regex := RegEx.new()
|
||||
var text_effects_regex := RegEx.new()
|
||||
|
||||
## Colors
|
||||
var normal_color : Color
|
||||
var comment_color : Color
|
||||
var text_effect_color : Color
|
||||
var choice_color : Color
|
||||
|
||||
var code_flow_color : Color
|
||||
var boolean_operator_color : Color
|
||||
var variable_color : Color
|
||||
var string_color : Color
|
||||
|
||||
var keyword_VAR_color : Color
|
||||
|
||||
var character_event_color : Color
|
||||
var character_name_color : Color
|
||||
var character_portrait_color : Color
|
||||
|
||||
var shortcode_color : Color
|
||||
var shortcode_param_color : Color
|
||||
var shortcode_value_color : Color
|
||||
|
||||
func _init():
|
||||
# Load colors from editor settings
|
||||
if DialogicUtil.get_dialogic_plugin():
|
||||
var editor_settings = DialogicUtil.get_dialogic_plugin().editor_interface.get_editor_settings()
|
||||
normal_color = editor_settings.get('text_editor/theme/highlighting/text_color')
|
||||
comment_color = editor_settings.get('text_editor/theme/highlighting/comment_color')
|
||||
text_effect_color = normal_color.darkened(0.5)
|
||||
choice_color = editor_settings.get('text_editor/theme/highlighting/function_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')
|
||||
|
||||
|
||||
shortcode_color = editor_settings.get('text_editor/theme/highlighting/gdscript/annotation_color')
|
||||
shortcode_param_color = editor_settings.get('text_editor/theme/highlighting/gdscript/node_path_color')
|
||||
shortcode_value_color = editor_settings.get('text_editor/theme/highlighting/gdscript/node_reference_color')
|
||||
|
||||
keyword_VAR_color = editor_settings.get('text_editor/theme/highlighting/keyword_color')
|
||||
|
||||
character_event_color = editor_settings.get('text_editor/theme/highlighting/symbol_color')
|
||||
character_name_color = editor_settings.get('text_editor/theme/highlighting/executing_line_color')
|
||||
character_portrait_color = character_name_color.lightened(0.6)
|
||||
|
||||
shortcode_regex.compile("\\W*\\[(?<id>\\w*)(?<args>[^\\]]*)?")
|
||||
shortcode_param_regex.compile('((?<parameter>[^\\s=]*)\\s*=\\s*"(?<value>([^=]|\\\\=)*)(?<!\\\\)")')
|
||||
text_event_regex.compile("\\W*((\")?(?<name>(?(2)[^\"\\n]*|[^(: \\n]*))(?(2)\"|)(\\W*\\((?<portrait>.*)\\))?\\s*(?<!\\\\):)?(?<text>.*)")
|
||||
text_effects_regex.compile("(?<!\\\\)\\[\\s*(?<command>mood|portrait|speed|signal|pause|br)\\s*(=\\s*(?<value>.+?)\\s*)?\\]")
|
||||
character_event_regex.compile("(?<type>Join|Update|Leave)\\s*(\")?(?<name>(?(2)[^\"\\n]*|[^(: \\n]*))(?(2)\"|)(\\W*\\((?<portrait>.*)\\))?(\\s*(?<position>\\d))?(\\s*\\[(?<shortcode>.*)\\])?")
|
||||
|
||||
func _get_line_syntax_highlighting(line:int) -> Dictionary:
|
||||
var str_line := get_text_edit().get_line(line)
|
||||
|
||||
var dict := {}
|
||||
dict[0] = {'color':normal_color}
|
||||
|
||||
if str_line.strip_edges().begins_with('#'):
|
||||
dict[0] = {'color':comment_color}
|
||||
return dict
|
||||
|
||||
if str_line.strip_edges().begins_with("["):
|
||||
var result:= shortcode_regex.search(str_line)
|
||||
if result:
|
||||
dict[result.get_start('id')] = {"color":shortcode_color}
|
||||
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'))
|
||||
return dict
|
||||
|
||||
if str_line.strip_edges().begins_with('-'):
|
||||
dict[0] = {'color':choice_color}
|
||||
if '[' in str_line:
|
||||
dict[str_line.find('[')] = {"color":normal_color}
|
||||
dict = color_word(dict, code_flow_color, str_line, 'if', str_line.find('['), str_line.find(']'))
|
||||
dict = color_condition(dict, str_line, str_line.find('['), str_line.find(']'))
|
||||
return dict
|
||||
|
||||
for word in ['if', 'elif', 'else']:
|
||||
if str_line.strip_edges().begins_with(word):
|
||||
dict[str_line.find(word)] = {"color":code_flow_color}
|
||||
dict[str_line.find(word)+len(word)] = {"color":normal_color}
|
||||
dict = color_condition(dict, str_line)
|
||||
return dict
|
||||
|
||||
for word in ['Join', 'Update', 'Leave']:
|
||||
if str_line.strip_edges().begins_with(word):
|
||||
dict[str_line.find(word)] = {"color":character_event_color}
|
||||
dict[str_line.find(word)+len(word)] = {"color":normal_color}
|
||||
var result := character_event_regex.search(str_line)
|
||||
if result.get_string('name'):
|
||||
dict[result.get_start('name')] = {"color":character_name_color}
|
||||
dict[result.get_end('name')] = {"color":normal_color}
|
||||
if result.get_string('portrait'):
|
||||
dict[result.get_start('portrait')] = {"color":character_portrait_color}
|
||||
dict[result.get_end('portrait')] = {"color":normal_color}
|
||||
if result.get_string('shortcode'):
|
||||
dict = color_shortcode_content(dict, str_line, result.get_start('shortcode'), result.get_end('shortcode'))
|
||||
return dict
|
||||
|
||||
if str_line.strip_edges().begins_with('VAR'):
|
||||
dict[str_line.find('VAR')] = {"color":keyword_VAR_color}
|
||||
dict[str_line.find('VAR')+3] = {"color":normal_color}
|
||||
dict = color_region(dict, string_color, str_line, '"', '"', str_line.find('VAR'))
|
||||
dict = color_region(dict, variable_color, str_line, '{', '}', str_line.find('VAR'))
|
||||
return dict
|
||||
|
||||
var result := text_event_regex.search(str_line)
|
||||
if !result:
|
||||
return dict
|
||||
if result.get_string('name'):
|
||||
dict[result.get_start('name')] = {"color":character_name_color}
|
||||
dict[result.get_end('name')] = {"color":normal_color}
|
||||
if result.get_string('portrait'):
|
||||
dict[result.get_start('portrait')] = {"color":character_portrait_color}
|
||||
dict[result.get_end('portrait')] = {"color":normal_color}
|
||||
if result.get_string('text'):
|
||||
var effects_result = text_effects_regex.search_all(str_line)
|
||||
for eff in effects_result:
|
||||
dict[eff.get_start()] = {"color":text_effect_color}
|
||||
dict[eff.get_end()] = {"color":normal_color}
|
||||
dict = color_region(dict, variable_color, str_line, '{', '}', result.get_start('text'))
|
||||
|
||||
return dict
|
||||
|
||||
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)
|
||||
dict = color_word(dict, code_flow_color, line, '>', from, to)
|
||||
dict = color_word(dict, code_flow_color, line, '<', from, to)
|
||||
dict = color_word(dict, code_flow_color, line, '>=', from, to)
|
||||
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_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) -> Dictionary:
|
||||
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':normal_color}
|
||||
return dict
|
||||
|
||||
func color_shortcode_content(dict:Dictionary, line:String, from:int = 0, to:int = 0) -> 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":shortcode_param_color}
|
||||
dict[x.get_start('value')+from-1] = {"color":shortcode_value_color}
|
||||
dict[x.get_end()+from] = {"color":normal_color}
|
||||
return dict
|
||||
|
|
@ -0,0 +1,408 @@
|
|||
@tool
|
||||
extends CodeEdit
|
||||
|
||||
## Sub-Editor that allows editing timelines in a text format.
|
||||
|
||||
# These RegEx's are used to deduce information from the current line for auto-completion
|
||||
# E.g. the character for portrait suggestions or the event type for parameter suggestions
|
||||
|
||||
# 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 character from a character event (for portrait suggestions)
|
||||
var completion_character_getter_regex := RegEx.new()
|
||||
# To find the character from a text event (for portrait suggestions)
|
||||
var completion_text_character_getter_regex := RegEx.new()
|
||||
|
||||
# Stores references to all shortcode events for parameter and value suggestions
|
||||
var completion_shortcodes := {}
|
||||
|
||||
|
||||
func _ready():
|
||||
syntax_highlighter = load("res://addons/dialogic/Editor/TimelineEditor/TextEditor/syntax_highlighter.gd").new()
|
||||
|
||||
# Compile RegEx's
|
||||
completion_word_regex.compile("(?<s>(\\W)|^)(?<word>\\w*)\\x{FFFF}")
|
||||
completion_shortcode_getter_regex.compile("\\[(?<code>\\w*)")
|
||||
completion_character_getter_regex.compile("(?<type>Join|Update|Leave)\\s*(\")?(?<name>(?(2)[^\"\\n]*|[^(: \\n]*))(?(2)\"|)(\\W*\\((?<portrait>.*)\\))?(\\s*(?<position>\\d))?(\\s*\\[(?<shortcode>.*)\\])?")
|
||||
completion_text_character_getter_regex.compile("\\W*(\")?(?<name>(?(2)[^\"\\n]*|[^(: \\n]*))(?(1)\"|)")
|
||||
completion_shortcode_param_getter_regex.compile("(?<param>\\w*)\\W*=\\s*\"?"+String.chr(0xFFFF))
|
||||
|
||||
|
||||
func _on_text_editor_text_changed():
|
||||
get_parent().current_resource_state = DialogicEditor.ResourceStates.Unsaved
|
||||
request_code_completion(true)
|
||||
|
||||
|
||||
|
||||
func clear_timeline():
|
||||
text = ''
|
||||
|
||||
|
||||
func load_timeline(object:DialogicTimeline) -> void:
|
||||
clear_timeline()
|
||||
if get_parent().current_resource.events.size() == 0:
|
||||
pass
|
||||
else:
|
||||
if typeof(get_parent().current_resource.events[0]) == TYPE_STRING:
|
||||
get_parent().current_resource.events_processed = false
|
||||
get_parent().current_resource = get_parent().editors_manager.resource_helper.process_timeline(get_parent().current_resource)
|
||||
|
||||
var result:String = ""
|
||||
var indent := 0
|
||||
for idx in range(0, len(object.events)):
|
||||
var event = object.events[idx]
|
||||
|
||||
if event['event_name'] == 'End Branch':
|
||||
indent -= 1
|
||||
continue
|
||||
|
||||
if event != null:
|
||||
for i in event.empty_lines_above:
|
||||
result += "\t".repeat(indent)+"\n"
|
||||
result += "\t".repeat(indent)+event['event_node_as_text'].replace('\n', "\n"+"\t".repeat(indent)) + "\n"
|
||||
if event.can_contain_events:
|
||||
indent += 1
|
||||
if indent < 0:
|
||||
indent = 0
|
||||
|
||||
text = result
|
||||
get_parent().current_resource.set_meta("timeline_not_saved", false)
|
||||
|
||||
|
||||
func save_timeline():
|
||||
if get_parent().current_resource:
|
||||
var text_array:Array = text_timeline_to_array(text)
|
||||
|
||||
get_parent().current_resource.events = text_array
|
||||
get_parent().current_resource.events_processed = false
|
||||
ResourceSaver.save(get_parent().current_resource, get_parent().current_resource.resource_path)
|
||||
|
||||
get_parent().current_resource.set_meta("timeline_not_saved", false)
|
||||
get_parent().current_resource_state = DialogicEditor.ResourceStates.Saved
|
||||
get_parent().editors_manager.resource_helper.rebuild_timeline_directory()
|
||||
|
||||
|
||||
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)
|
||||
_:
|
||||
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 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 = not lines[from].begins_with("# ")
|
||||
for i in range(from, to + 1):
|
||||
lines[i] = "# " + lines[i] if will_comment else lines[i].substr(2)
|
||||
|
||||
text = "\n".join(lines)
|
||||
select(from, 0, to, get_line_width(to))
|
||||
set_caret_line(cursor.y)
|
||||
set_caret_column(cursor.x)
|
||||
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()
|
||||
|
||||
|
||||
# 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)
|
||||
insert_text_at_caret('"'+data.files[0]+'"')
|
||||
|
||||
|
||||
################################################################################
|
||||
## AUTO COMPLETION
|
||||
################################################################################
|
||||
|
||||
# Helper that gets the current line with a special character where the caret is
|
||||
func get_code_completion_line() -> String:
|
||||
return get_line(get_caret_line()).insert(get_caret_column(), String.chr(0xFFFF)).strip_edges()
|
||||
|
||||
|
||||
# Helper that gets the currently typed word
|
||||
func get_code_completion_word() -> String:
|
||||
var result := completion_word_regex.search(get_code_completion_line())
|
||||
return result.get_string('word') if result else ""
|
||||
|
||||
|
||||
# Helper that gets the symbol before the current word
|
||||
func get_code_completion_prev_symbol() -> String:
|
||||
var result := completion_word_regex.search(get_code_completion_line())
|
||||
return result.get_string('s') if result else ""
|
||||
|
||||
|
||||
# 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):
|
||||
# make sure shortcode event references are loaded
|
||||
if completion_shortcodes.is_empty():
|
||||
for event in get_parent().editors_manager.resource_helper.event_script_cache:
|
||||
if event.get_shortcode() != 'default_shortcode':
|
||||
completion_shortcodes[event.get_shortcode()] = event
|
||||
|
||||
# fill helpers
|
||||
var line := get_code_completion_line()
|
||||
var word := get_code_completion_word()
|
||||
var symbol := get_code_completion_prev_symbol()
|
||||
|
||||
|
||||
## 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 letter for the completion.
|
||||
# The completion will check if the letter is already present and add it otherwise.
|
||||
|
||||
|
||||
# Shortcode event suggestions
|
||||
if line.begins_with('['):
|
||||
if symbol == '[':
|
||||
# suggest shortcodes if a shortcode event has just begun
|
||||
for shortcode in completion_shortcodes.keys():
|
||||
add_code_completion_option(CodeEdit.KIND_MEMBER, shortcode, shortcode, completion_shortcodes[shortcode].event_color, completion_shortcodes[shortcode]._get_icon())
|
||||
|
||||
else:
|
||||
# suggest either parameters or values
|
||||
var current_shortcode := completion_shortcode_getter_regex.search(line)
|
||||
if current_shortcode:
|
||||
var code := current_shortcode.get_string('code')
|
||||
if code in completion_shortcodes.keys():
|
||||
if symbol == ' ':
|
||||
for param in completion_shortcodes[code].get_shortcode_parameters().keys():
|
||||
add_code_completion_option(CodeEdit.KIND_MEMBER, param, param+'="' , syntax_highlighter.shortcode_param_color)
|
||||
elif symbol == '=' or symbol == '"':
|
||||
var current_parameter_gex := completion_shortcode_param_getter_regex.search(line)
|
||||
if current_parameter_gex:
|
||||
var current_parameter := current_parameter_gex.get_string('param')
|
||||
if completion_shortcodes[code].get_shortcode_parameters().has(current_parameter):
|
||||
if completion_shortcodes[code].get_shortcode_parameters()[current_parameter].has('suggestions'):
|
||||
var suggestions : Dictionary= completion_shortcodes[code].get_shortcode_parameters()[current_parameter]['suggestions'].call()
|
||||
for key in suggestions.keys():
|
||||
add_code_completion_option(CodeEdit.KIND_MEMBER, key, suggestions[key].value, syntax_highlighter.shortcode_value_color, suggestions[key].get('icon', null), '"')
|
||||
|
||||
# Condition event suggestions
|
||||
elif line.begins_with('if') or line.begins_with('elif'):
|
||||
if symbol == '{':
|
||||
suggest_variables()
|
||||
|
||||
# Choice Event suggestions
|
||||
elif line.begins_with('-') and '[' in line:
|
||||
if symbol == '[':
|
||||
add_code_completion_option(CodeEdit.KIND_MEMBER, 'if', 'if ', syntax_highlighter.code_flow_color)
|
||||
elif symbol == '{':
|
||||
suggest_variables()
|
||||
|
||||
# Character Event suggestions
|
||||
elif line.begins_with('Join') or line.begins_with('Leave') or line.begins_with('Update'):
|
||||
if symbol == ' ' and line.count(' ') <= max(line.count('"'), 1):
|
||||
suggest_characters()
|
||||
if line.begins_with('Leave'):
|
||||
add_code_completion_option(CodeEdit.KIND_MEMBER, 'All', '--All-- ', syntax_highlighter.character_name_color, get_theme_icon("GuiEllipsis", "EditorIcons"))
|
||||
|
||||
if symbol == '(':
|
||||
var character:= completion_character_getter_regex.search(line).get_string('name')
|
||||
suggest_portraits(character)
|
||||
|
||||
# Start of line suggestions
|
||||
# These are all as KIND_PLAIN_TEXT, because that means they won't
|
||||
# be suggested unless at least the first letter is typed in.
|
||||
elif not ' ' in line:
|
||||
add_code_completion_option(CodeEdit.KIND_PLAIN_TEXT, 'Join', 'Join ', syntax_highlighter.character_event_color, load('res://addons/dialogic/Editor/Images/Dropdown/join.svg'))
|
||||
add_code_completion_option(CodeEdit.KIND_PLAIN_TEXT, 'Leave', 'Leave ', syntax_highlighter.character_event_color, load('res://addons/dialogic/Editor/Images/Dropdown/leave.svg'))
|
||||
add_code_completion_option(CodeEdit.KIND_PLAIN_TEXT, 'Update', 'Update ', syntax_highlighter.character_event_color, load('res://addons/dialogic/Editor/Images/Dropdown/update.svg'))
|
||||
|
||||
add_code_completion_option(CodeEdit.KIND_PLAIN_TEXT, 'if', 'if ', syntax_highlighter.code_flow_color)
|
||||
add_code_completion_option(CodeEdit.KIND_PLAIN_TEXT, 'elif', 'elif ', syntax_highlighter.code_flow_color)
|
||||
add_code_completion_option(CodeEdit.KIND_PLAIN_TEXT, 'else', 'else:', syntax_highlighter.code_flow_color)
|
||||
|
||||
add_code_completion_option(CodeEdit.KIND_PLAIN_TEXT, 'VAR', 'VAR ', syntax_highlighter.keyword_VAR_color)
|
||||
|
||||
suggest_characters(CodeEdit.KIND_CLASS)
|
||||
|
||||
# Text Event Suggestions
|
||||
else:
|
||||
if not ':' in line and symbol == '(':
|
||||
var character := completion_text_character_getter_regex.search(line).get_string('name')
|
||||
suggest_portraits(character)
|
||||
if symbol == '[':
|
||||
add_code_completion_option(CodeEdit.KIND_MEMBER, 'speed (custom)', 'speed=', syntax_highlighter.normal_color, get_theme_icon("RichTextEffect", "EditorIcons"))
|
||||
add_code_completion_option(CodeEdit.KIND_MEMBER, 'speed (default)', 'speed', syntax_highlighter.normal_color, get_theme_icon("RichTextEffect", "EditorIcons"), ']')
|
||||
add_code_completion_option(CodeEdit.KIND_MEMBER, 'pause (custom)', 'pause=', syntax_highlighter.normal_color, get_theme_icon("RichTextEffect", "EditorIcons"))
|
||||
add_code_completion_option(CodeEdit.KIND_MEMBER, 'pause (default)', 'pause', syntax_highlighter.normal_color, get_theme_icon("RichTextEffect", "EditorIcons"), ']')
|
||||
add_code_completion_option(CodeEdit.KIND_MEMBER, 'portrait', 'portrait=', syntax_highlighter.normal_color, get_theme_icon("RichTextEffect", "EditorIcons"))
|
||||
add_code_completion_option(CodeEdit.KIND_MEMBER, 'br', 'br', syntax_highlighter.normal_color, get_theme_icon("RichTextEffect", "EditorIcons"), ']')
|
||||
add_code_completion_option(CodeEdit.KIND_MEMBER, 'signal', 'signal=', syntax_highlighter.normal_color, get_theme_icon("RichTextEffect", "EditorIcons"))
|
||||
if symbol == '{':
|
||||
suggest_variables()
|
||||
|
||||
# Force update and showing of the popup
|
||||
update_code_completion_options(true)
|
||||
|
||||
|
||||
# Helper that adds all characters as options
|
||||
func suggest_characters(type := CodeEdit.KIND_MEMBER) -> void:
|
||||
for character in get_parent().editors_manager.resource_helper.character_directory:
|
||||
add_code_completion_option(type, character, character, syntax_highlighter.character_name_color, load("res://addons/dialogic/Editor/Images/Resources/character.svg"))
|
||||
|
||||
|
||||
# Helper that adds all portraits of a given character as options
|
||||
func suggest_portraits(character_name:String) -> void:
|
||||
var character_resource :DialogicCharacter= get_parent().editors_manager.resource_helper.character_directory[character_name]['resource']
|
||||
for portrait in character_resource.portraits:
|
||||
add_code_completion_option(CodeEdit.KIND_MEMBER, portrait, portrait, syntax_highlighter.character_portrait_color, load("res://addons/dialogic/Editor/Images/Resources/character.svg"), ')')
|
||||
if character_resource.portraits.is_empty():
|
||||
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():
|
||||
for variable in DialogicUtil.list_variables(DialogicUtil.get_project_setting('dialogic/variables')):
|
||||
add_code_completion_option(CodeEdit.KIND_MEMBER, variable, variable, syntax_highlighter.variable_color, get_theme_icon("MemberProperty", "EditorIcons"), '}')
|
||||
|
||||
|
||||
# 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):
|
||||
var valid_candidates := []
|
||||
var current_word := get_code_completion_word()
|
||||
for candidate in candidates:
|
||||
if candidate.kind == KIND_PLAIN_TEXT:
|
||||
if !current_word.is_empty() and candidate.insert_text.begins_with(current_word):
|
||||
valid_candidates.append(candidate)
|
||||
elif candidate.kind == 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 == KIND_CONSTANT:
|
||||
if current_word.is_empty() or candidate.insert_text.begins_with(current_word):
|
||||
valid_candidates.append(candidate)
|
||||
elif candidate.kind == KIND_CLASS:
|
||||
if !current_word.is_empty() and current_word.to_lower() in candidate.insert_text.to_lower():
|
||||
valid_candidates.append(candidate)
|
||||
valid_candidates.reverse()
|
||||
return valid_candidates
|
||||
|
||||
|
||||
# Called when code completion was activated
|
||||
# Inserts the selected item
|
||||
func _confirm_code_completion(replace):
|
||||
# Note: I decided to ALWAYS use replace mode, as dialogic is supposed to be beginner friendly
|
||||
var word := get_code_completion_word()
|
||||
var code_completion := get_code_completion_option(get_code_completion_selected_index())
|
||||
remove_text(get_caret_line(), get_caret_column()-len(word), get_caret_line(), get_caret_column())
|
||||
set_caret_column(get_caret_column()-len(word))
|
||||
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 := get_line(get_caret_line()).substr(get_caret_column(), 1)
|
||||
if next_letter != code_completion['default_value']:
|
||||
insert_text_at_caret(code_completion['default_value'])
|
||||
else:
|
||||
set_caret_column(get_caret_column()+1)
|
||||
|
||||
|
||||
################################################################################
|
||||
## SYMBOL CLICKING
|
||||
################################################################################
|
||||
|
||||
# Performs an action (like opening a link) when a valid symbol was clicked
|
||||
func _on_symbol_lookup(symbol, line, column):
|
||||
if symbol in completion_shortcodes.keys():
|
||||
if !completion_shortcodes[symbol].help_page_path.is_empty():
|
||||
OS.shell_open(completion_shortcodes[symbol].help_page_path)
|
||||
|
||||
|
||||
# Called to test if a symbol can be clicked
|
||||
func _on_symbol_validate(symbol:String) -> void:
|
||||
if symbol in completion_shortcodes.keys():
|
||||
if !completion_shortcodes[symbol].help_page_path.is_empty():
|
||||
set_symbol_lookup_word_as_valid(true)
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
[gd_scene load_steps=4 format=3 uid="uid://defdeav8rli6o"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/dialogic/Editor/TimelineEditor/TextEditor/timeline_editor_text.gd" id="1_1kbx2"]
|
||||
[ext_resource type="Script" path="res://addons/dialogic/Editor/TimelineEditor/TextEditor/syntax_highlighter.gd" id="1_5qfro"]
|
||||
|
||||
[sub_resource type="SyntaxHighlighter" id="SyntaxHighlighter_q86qr"]
|
||||
script = ExtResource("1_5qfro")
|
||||
|
||||
[node name="TimelineTextEditor" type="CodeEdit"]
|
||||
offset_top = 592.0
|
||||
offset_right = 1024.0
|
||||
offset_bottom = 600.0
|
||||
theme_override_constants/line_spacing = 10
|
||||
highlight_current_line = true
|
||||
draw_tabs = true
|
||||
syntax_highlighter = SubResource("SyntaxHighlighter_q86qr")
|
||||
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 = ["[", "{", "f"]
|
||||
indent_automatic = true
|
||||
auto_brace_completion_enabled = true
|
||||
auto_brace_completion_highlight_matching = true
|
||||
script = ExtResource("1_1kbx2")
|
||||
|
||||
[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"]
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
@tool
|
||||
extends Button
|
||||
|
||||
@export var visible_name:String = ""
|
||||
@export var event_id:String = ''
|
||||
@export var event_icon:Texture :
|
||||
get:
|
||||
return event_icon
|
||||
set(texture):
|
||||
event_icon = texture
|
||||
%Icon.texture = texture
|
||||
@export var event_category:int = 0
|
||||
@export var event_sorting_index:int = 0
|
||||
@export var resource:Resource
|
||||
@export var dialogic_color_name:String = ''
|
||||
|
||||
func _ready():
|
||||
self_modulate = Color(1,1,1)
|
||||
if visible_name != '':
|
||||
text = ' ' + visible_name
|
||||
#tooltip_text = DTS.translate(tooltip_text)
|
||||
|
||||
var _scale = DialogicUtil.get_editor_scale()
|
||||
custom_minimum_size = Vector2(30,30)* _scale
|
||||
icon = null
|
||||
|
||||
%ColorBorder.custom_minimum_size.x = 5 * _scale
|
||||
%IconContainer.custom_minimum_size = custom_minimum_size
|
||||
|
||||
add_theme_color_override("font_color", get_theme_color("font_color", "Editor"))
|
||||
add_theme_color_override("font_color_hover", get_theme_color("accent_color", "Editor"))
|
||||
%Icon.modulate = get_theme_color("font_color", "Button")
|
||||
|
||||
# TODO godot4 signal was removed. find a way to react to color changes
|
||||
# ProjectSettings.project_settings_changed.connect(_update_color)
|
||||
|
||||
func set_color(color):
|
||||
%ColorBorder.self_modulate = color
|
||||
|
||||
#func _update_color():
|
||||
# if dialogic_color_name != '':
|
||||
# var new_color = DialogicUtil.get_color(dialogic_color_name)
|
||||
# resource.event_color = new_color
|
||||
# %ColorBorder.self_modulate = DialogicUtil.get_color(dialogic_color_name)
|
||||
|
||||
func _get_drag_data(position):
|
||||
var preview_label = Label.new()
|
||||
|
||||
preview_label.text = 'Add Event %s' % [ tooltip_text ]
|
||||
if self.text != '':
|
||||
preview_label.text = text
|
||||
|
||||
set_drag_preview(preview_label)
|
||||
|
||||
return { "source": "EventButton", "resource": resource }
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
[gd_scene load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/dialogic/Editor/TimelineEditor/VisualEditor/AddEventButton.gd" id="1_s43sc"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ba8bn"]
|
||||
bg_color = Color(1, 1, 1, 1)
|
||||
corner_radius_top_left = 5
|
||||
corner_radius_bottom_left = 5
|
||||
|
||||
[node name="AddEventButton" type="Button"]
|
||||
custom_minimum_size = Vector2(60, 60)
|
||||
offset_right = 103.0
|
||||
offset_bottom = 31.0
|
||||
theme_override_colors/font_color = Color(0, 0, 0, 1)
|
||||
text = " S"
|
||||
flat = true
|
||||
script = ExtResource("1_s43sc")
|
||||
visible_name = "S"
|
||||
|
||||
[node name="Border" type="Panel" parent="."]
|
||||
show_behind_parent = true
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Hbox" type="HBoxContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/separation = 0
|
||||
metadata/_edit_layout_mode = 1
|
||||
|
||||
[node name="ColorBorder" type="Panel" parent="Hbox"]
|
||||
unique_name_in_owner = true
|
||||
self_modulate = Color(0.635294, 0.635294, 0.635294, 0.282353)
|
||||
show_behind_parent = true
|
||||
custom_minimum_size = Vector2(10, 0)
|
||||
layout_mode = 2
|
||||
offset_right = 10.0
|
||||
offset_bottom = 60.0
|
||||
mouse_filter = 2
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_ba8bn")
|
||||
|
||||
[node name="IconContainer" type="MarginContainer" parent="Hbox"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(60, 60)
|
||||
layout_mode = 2
|
||||
offset_left = 10.0
|
||||
offset_right = 70.0
|
||||
offset_bottom = 60.0
|
||||
theme_override_constants/margin_left = 0
|
||||
theme_override_constants/margin_top = 0
|
||||
theme_override_constants/margin_right = 0
|
||||
theme_override_constants/margin_bottom = 0
|
||||
|
||||
[node name="Icon" type="TextureRect" parent="Hbox/IconContainer"]
|
||||
unique_name_in_owner = true
|
||||
modulate = Color(0, 0, 0, 1)
|
||||
layout_mode = 2
|
||||
offset_right = 60.0
|
||||
offset_bottom = 60.0
|
||||
grow_vertical = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
ignore_texture_size = true
|
||||
metadata/_edit_layout_mode = 1
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
@tool
|
||||
extends Button
|
||||
|
||||
@export var EventName:String = ''
|
||||
|
||||
func get_drag_data(position):
|
||||
var preview_label = Label.new()
|
||||
|
||||
if (self.text != ''):
|
||||
preview_label.text = text
|
||||
else:
|
||||
preview_label.text = 'Add Event %s' % [ EventName ]
|
||||
|
||||
set_drag_preview(preview_label)
|
||||
|
||||
return { "source": "EventButton", "event_name": EventName }
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
@tool
|
||||
extends ScrollContainer
|
||||
|
||||
# store last attempts since godot sometimes misses drop events
|
||||
var _is_drag_receiving = false
|
||||
var _last_event_button_drop_attempt = ''
|
||||
var _mouse_exited = false
|
||||
|
||||
@onready var timeline_editor = find_parent('TimelineVisualEditor')
|
||||
|
||||
|
||||
func _ready():
|
||||
mouse_entered.connect(_on_mouse_entered)
|
||||
mouse_exited.connect(_on_mouse_exited)
|
||||
gui_input.connect(_on_gui_input)
|
||||
|
||||
|
||||
set('theme_override_styles/panel', get_theme_stylebox("Background", "EditorStyles"))
|
||||
|
||||
|
||||
func _can_drop_data(position, data):
|
||||
if data != null and data is Dictionary and data.has("source"):
|
||||
if data["source"] == "EventButton":
|
||||
if _last_event_button_drop_attempt is Resource == false:
|
||||
timeline_editor.create_drag_and_drop_event(data["resource"])
|
||||
_is_drag_receiving = true
|
||||
_last_event_button_drop_attempt = data["resource"]
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func cancel_drop():
|
||||
_is_drag_receiving = false
|
||||
_last_event_button_drop_attempt = ''
|
||||
timeline_editor.cancel_drop_event()
|
||||
|
||||
|
||||
func _drop_data(position, data):
|
||||
# add event
|
||||
if (data["source"] == "EventButton"):
|
||||
timeline_editor.drop_event()
|
||||
_is_drag_receiving = false
|
||||
_last_event_button_drop_attempt = ''
|
||||
|
||||
|
||||
func _on_mouse_exited():
|
||||
if _is_drag_receiving and not _mouse_exited:
|
||||
var preview_label = Label.new()
|
||||
preview_label.text = "Cancel"
|
||||
set_drag_preview(preview_label)
|
||||
_mouse_exited = true
|
||||
|
||||
|
||||
func _on_mouse_entered():
|
||||
if _is_drag_receiving and _mouse_exited:
|
||||
var preview_label = Label.new()
|
||||
preview_label.text = "Insert Event"
|
||||
set_drag_preview(preview_label)
|
||||
_mouse_exited = false
|
||||
|
||||
|
||||
func _input(event):
|
||||
if (event is InputEventMouseButton and is_visible_in_tree() and event.button_index == MOUSE_BUTTON_LEFT):
|
||||
if (_mouse_exited and _is_drag_receiving):
|
||||
cancel_drop()
|
||||
|
||||
|
||||
func _on_gui_input(event):
|
||||
# godot sometimes misses drop events
|
||||
if (event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT):
|
||||
if (_is_drag_receiving):
|
||||
if (_last_event_button_drop_attempt != ''):
|
||||
_drop_data(Vector2.ZERO, { "source": "EventButton", "event_id": _last_event_button_drop_attempt} )
|
||||
_is_drag_receiving = false
|
||||
|
||||
func _draw():
|
||||
var _scale = DialogicUtil.get_editor_scale()
|
||||
var line_width = 5 * _scale
|
||||
var horizontal_line_length = 100*_scale
|
||||
var color_multiplier = Color(1,1,1,0.5)
|
||||
var selected_color_multiplier = Color(1,1,1,1)
|
||||
for idx in range($Timeline.get_child_count()):
|
||||
var event = $Timeline.get_child(idx)
|
||||
|
||||
if not "resource" in event:
|
||||
continue
|
||||
|
||||
if not event.visible:
|
||||
continue
|
||||
|
||||
if !event.resource is DialogicEndBranchEvent:
|
||||
if event.has_body_content or event.resource.can_contain_events:
|
||||
var icon_panel_height = 32*_scale
|
||||
var rect_position = event.get_node('%IconPanel').global_position+Vector2(0,1)*event.get_node('%IconPanel').size+Vector2(0,-4)
|
||||
var color = event.resource.event_color
|
||||
if event.is_selected():
|
||||
color *= selected_color_multiplier
|
||||
else:
|
||||
color *= color_multiplier
|
||||
if idx < $Timeline.get_child_count()-1 and event.current_indent_level < $Timeline.get_child(idx+1).current_indent_level:
|
||||
var end_node = event.end_node
|
||||
var sub_idx = idx
|
||||
if !end_node:
|
||||
while sub_idx < $Timeline.get_child_count()-1:
|
||||
sub_idx += 1
|
||||
if $Timeline.get_child(sub_idx).current_indent_level == event.current_indent_level:
|
||||
end_node = $Timeline.get_child(sub_idx-1)
|
||||
break
|
||||
var rect_size := Vector2()
|
||||
if end_node != null:
|
||||
rect_size = Vector2(line_width, end_node.global_position.y+end_node.size.y-rect_position.y)
|
||||
if end_node.resource is DialogicEndBranchEvent and event.resource.can_contain_events:
|
||||
rect_size = Vector2(line_width, end_node.global_position.y+end_node.size.y/2-rect_position.y)
|
||||
else:
|
||||
rect_size = Vector2(line_width, $Timeline.get_child(-2).position.y+$Timeline.get_child(-2).size.y)
|
||||
|
||||
draw_rect(Rect2(rect_position-global_position, rect_size), color)
|
||||
draw_rect(Rect2(Vector2(event.get_node('%IconPanel').global_position.x+line_width, rect_position.y+rect_size.y-line_width)-global_position, Vector2(horizontal_line_length, line_width)), color)
|
||||
|
||||
elif event.expanded:
|
||||
draw_rect(Rect2(rect_position-global_position, Vector2(line_width, event.size.y-event.get_node('%IconPanel').size.y-8*_scale)), color)
|
||||
|
|
@ -0,0 +1,904 @@
|
|||
@tool
|
||||
extends Container
|
||||
|
||||
## Visual mode of the timeline editor.
|
||||
|
||||
|
||||
################################################################################
|
||||
## EDITOR NODES
|
||||
################################################################################
|
||||
var TimelineUndoRedo := UndoRedo.new()
|
||||
var event_node
|
||||
|
||||
################################################################################
|
||||
## SIGNALS
|
||||
################################################################################
|
||||
signal selection_updated
|
||||
signal batch_loaded
|
||||
signal timeline_loaded
|
||||
|
||||
|
||||
################################################################################
|
||||
## TIMELINE LOADING
|
||||
################################################################################
|
||||
var _batches = []
|
||||
var _building_timeline = true
|
||||
|
||||
|
||||
################################################################################
|
||||
## TIMELINE EVENT MANAGEMENT
|
||||
################################################################################
|
||||
var selected_items : Array = []
|
||||
|
||||
var move_start_position = null
|
||||
var moving_piece = null
|
||||
var piece_was_dragged = false
|
||||
|
||||
|
||||
################################################################################
|
||||
## CREATE/SAVE/LOAD
|
||||
################################################################################
|
||||
|
||||
func something_changed():
|
||||
get_parent().current_resource_state = DialogicEditor.ResourceStates.Unsaved
|
||||
|
||||
|
||||
func save_timeline() -> void:
|
||||
# return if resource is unchanged
|
||||
if get_parent().current_resource_state != DialogicEditor.ResourceStates.Unsaved:
|
||||
return
|
||||
|
||||
# create a list of text versions of all the events with the right indent
|
||||
var new_events := []
|
||||
var indent := 0
|
||||
|
||||
for event in %Timeline.get_children():
|
||||
|
||||
if 'event_name' in event.resource:
|
||||
event.resource.update_text_version()
|
||||
new_events.append(event.resource)
|
||||
|
||||
if !get_parent().current_resource:
|
||||
return
|
||||
|
||||
get_parent().current_resource.set_events(new_events)
|
||||
get_parent().current_resource.events_processed = true
|
||||
var error :int = ResourceSaver.save(get_parent().current_resource, get_parent().current_resource.resource_path)
|
||||
if error != OK:
|
||||
print('[Dialogic] Saving error: ', error)
|
||||
|
||||
get_parent().current_resource.set_meta("unsaved", false)
|
||||
get_parent().current_resource_state = DialogicEditor.ResourceStates.Saved
|
||||
get_parent().editors_manager.resource_helper.rebuild_timeline_directory()
|
||||
|
||||
|
||||
func load_timeline(object) -> void:
|
||||
clear_timeline_nodes()
|
||||
|
||||
_building_timeline = true
|
||||
|
||||
if get_parent().current_resource.events.size() == 0:
|
||||
pass
|
||||
else:
|
||||
if typeof(get_parent().current_resource.events[0]) == TYPE_STRING:
|
||||
get_parent().current_resource.events_processed = false
|
||||
get_parent().current_resource = get_parent().editors_manager.resource_helper.process_timeline(get_parent().current_resource)
|
||||
|
||||
var data = object.get_events()
|
||||
var page = 1
|
||||
var batch_size = 12
|
||||
while batch_events(data, batch_size, page).size() != 0:
|
||||
_batches.append(batch_events(data, batch_size, page))
|
||||
page += 1
|
||||
load_batch(_batches)
|
||||
# Reset the scroll position
|
||||
%TimelineArea.scroll_vertical = 0
|
||||
|
||||
|
||||
func batch_events(array, size, batch_number):
|
||||
|
||||
return array.slice((batch_number - 1) * size, batch_number * size)
|
||||
|
||||
|
||||
# a list of all events like choice and condition events (so they get connected to their end events)
|
||||
var opener_events_stack = []
|
||||
|
||||
func load_batch(data):
|
||||
var current_batch = _batches.pop_front()
|
||||
if current_batch:
|
||||
for i in current_batch:
|
||||
if i is DialogicEndBranchEvent:
|
||||
create_end_branch_event(%Timeline.get_child_count(), opener_events_stack.pop_back())
|
||||
else:
|
||||
var piece = add_event_node(i, %Timeline.get_child_count())
|
||||
if i.can_contain_events:
|
||||
opener_events_stack.push_back(piece)
|
||||
emit_signal("batch_loaded")
|
||||
|
||||
|
||||
func _on_batch_loaded():
|
||||
if _batches.size() > 0:
|
||||
await get_tree().create_timer(0.01).timeout
|
||||
load_batch(_batches)
|
||||
else:
|
||||
if opener_events_stack:
|
||||
for ev in opener_events_stack:
|
||||
create_end_branch_event(%Timeline.get_child_count(), ev)
|
||||
opener_events_stack = []
|
||||
indent_events()
|
||||
_building_timeline = false
|
||||
|
||||
add_extra_scroll_area_to_timeline()
|
||||
|
||||
|
||||
func clear_timeline_nodes():
|
||||
deselect_all_items()
|
||||
for event in %Timeline.get_children():
|
||||
event.free()
|
||||
|
||||
################################################################################
|
||||
## SETUP
|
||||
################################################################################
|
||||
|
||||
func _ready():
|
||||
DialogicUtil.get_dialogic_plugin().dialogic_save.connect(save_timeline)
|
||||
event_node = load("res://addons/dialogic/Editor/Events/EventNode/EventNode.tscn")
|
||||
|
||||
batch_loaded.connect(_on_batch_loaded)
|
||||
|
||||
# Margins
|
||||
var _scale = DialogicUtil.get_editor_scale()
|
||||
var scroll_container = $View/ScrollContainer
|
||||
scroll_container.custom_minimum_size.x = 200 * _scale
|
||||
|
||||
|
||||
if find_parent('EditorView'): # This prevents the view to turn black if you are editing this scene in Godot
|
||||
%TimelineArea.get_theme_color("background_color", "CodeEdit")
|
||||
|
||||
%TimelineArea.resized.connect(add_extra_scroll_area_to_timeline)
|
||||
|
||||
|
||||
func load_event_buttons() -> void:
|
||||
var scripts: Array = get_parent().editors_manager.resource_helper.get_event_scripts()
|
||||
|
||||
# Event buttons
|
||||
var buttonScene = load("res://addons/dialogic/Editor/TimelineEditor/VisualEditor/AddEventButton.tscn")
|
||||
|
||||
for event_script in scripts:
|
||||
var event_resource: Variant
|
||||
|
||||
if typeof(event_script) == TYPE_STRING:
|
||||
event_resource = load(event_script).new()
|
||||
else:
|
||||
event_resource = event_script
|
||||
|
||||
if event_resource.disable_editor_button == true: continue
|
||||
var button = buttonScene.instantiate()
|
||||
button.resource = event_resource
|
||||
button.visible_name = ' ' + event_resource.event_name
|
||||
button.event_icon = event_resource._get_icon()
|
||||
button.set_color(event_resource.event_color)
|
||||
button.dialogic_color_name = event_resource.dialogic_color_name
|
||||
button.event_category = event_resource.event_category
|
||||
button.event_sorting_index = event_resource.event_sorting_index
|
||||
|
||||
button.button_up.connect(_add_event_button_pressed.bind(event_resource))
|
||||
|
||||
get_node("View/ScrollContainer/EventContainer/FlexContainer" + str(button.event_category)).add_child(button)
|
||||
while event_resource.event_sorting_index < get_node("View/ScrollContainer/EventContainer/FlexContainer" + str(button.event_category)).get_child(max(0, button.get_index()-1)).resource.event_sorting_index:
|
||||
get_node("View/ScrollContainer/EventContainer/FlexContainer" + str(button.event_category)).move_child(button, button.get_index()-1)
|
||||
|
||||
|
||||
################################################################################
|
||||
## CLEANUP
|
||||
################################################################################
|
||||
|
||||
func _exit_tree() -> void:
|
||||
# Explicitly free any open cache resources on close, so we don't get leaked resource errors on shutdown
|
||||
clear_timeline_nodes()
|
||||
|
||||
|
||||
################################################################################
|
||||
## DRAG&DROP + DRAGGING EVENTS
|
||||
################################################################################
|
||||
# handles dragging/moving of events
|
||||
func _process(delta:float) -> void:
|
||||
if moving_piece != null:
|
||||
var current_position = get_global_mouse_position()
|
||||
var node_position = moving_piece.global_position.y
|
||||
var height = get_block_height(moving_piece)
|
||||
var up_offset = get_block_height(get_block_above(moving_piece))
|
||||
var down_offset = get_block_height(get_block_below(moving_piece))
|
||||
if up_offset != null:
|
||||
up_offset = (up_offset / 2) + 5
|
||||
if current_position.y < node_position - up_offset:
|
||||
if moving_piece.resource is DialogicEndBranchEvent:
|
||||
if moving_piece.parent_node != get_block_above(moving_piece):
|
||||
move_block_up(moving_piece)
|
||||
piece_was_dragged = true
|
||||
else:
|
||||
move_block_up(moving_piece)
|
||||
piece_was_dragged = true
|
||||
if down_offset != null:
|
||||
down_offset = height + (down_offset / 2) + 5
|
||||
if current_position.y > node_position + down_offset:
|
||||
move_block_down(moving_piece)
|
||||
piece_was_dragged = true
|
||||
|
||||
|
||||
## INFO: These methods are mainly used by the TimelineArea
|
||||
# Creates a ghost event for drag and drop
|
||||
func create_drag_and_drop_event(resource) -> void:
|
||||
var index = get_index_under_cursor()
|
||||
var piece = add_event_node(resource)
|
||||
%Timeline.move_child(piece, index)
|
||||
moving_piece = piece
|
||||
piece_was_dragged = true
|
||||
select_item(piece)
|
||||
return piece
|
||||
|
||||
|
||||
func drop_event() -> void:
|
||||
if moving_piece != null:
|
||||
var at_index = moving_piece.get_index()
|
||||
var resource = moving_piece.resource
|
||||
moving_piece.queue_free()
|
||||
|
||||
_add_event_button_pressed(resource)
|
||||
moving_piece = null
|
||||
piece_was_dragged = false
|
||||
something_changed()
|
||||
|
||||
|
||||
func cancel_drop_event() -> void:
|
||||
if moving_piece != null:
|
||||
moving_piece = null
|
||||
piece_was_dragged = false
|
||||
delete_selected_events()
|
||||
deselect_all_items()
|
||||
|
||||
|
||||
# SIGNAL handles input on the events mainly for selection and moving events
|
||||
func _on_event_block_gui_input(event, item: Node):
|
||||
if event is InputEventMouseButton and event.button_index == 1:
|
||||
if (not event.is_pressed()):
|
||||
if (piece_was_dragged and moving_piece != null and move_start_position):
|
||||
var to_position = moving_piece.get_index()
|
||||
if move_start_position != to_position:
|
||||
TimelineUndoRedo.create_action("[D] Moved event (type '"+moving_piece.resource.to_string()+"').")
|
||||
TimelineUndoRedo.add_do_method(move_block_to_index.bind(move_start_position, to_position))
|
||||
TimelineUndoRedo.add_undo_method(move_block_to_index.bind(to_position, move_start_position))
|
||||
|
||||
# in case a something like a choice or condition was moved BELOW it's end node the end_node is moved as well!!!
|
||||
if moving_piece.resource.can_contain_events:
|
||||
if moving_piece.end_node.get_index() < to_position:
|
||||
TimelineUndoRedo.add_do_method(move_block_to_index.bind(moving_piece.end_node.get_index(), to_position))
|
||||
TimelineUndoRedo.add_undo_method(move_block_to_index.bind(to_position+1, moving_piece.end_node.get_index()))
|
||||
|
||||
# move it back so the DO action works. (Kinda stupid but whatever)
|
||||
move_block_to_index(to_position, move_start_position)
|
||||
TimelineUndoRedo.commit_action()
|
||||
|
||||
move_start_position = null
|
||||
else:
|
||||
select_item(item)
|
||||
if (moving_piece != null):
|
||||
|
||||
indent_events()
|
||||
piece_was_dragged = false
|
||||
moving_piece = null
|
||||
elif event.is_pressed():
|
||||
moving_piece = item
|
||||
move_start_position = moving_piece.get_index()
|
||||
if not _is_item_selected(item):
|
||||
pass#piece_was_dragged = true
|
||||
else:
|
||||
piece_was_dragged = false
|
||||
|
||||
|
||||
## *****************************************************************************
|
||||
## SHORTCUTS
|
||||
## *****************************************************************************
|
||||
func _input(event):
|
||||
# some shortcuts need to get handled in the common input event
|
||||
# especially CTRL-based
|
||||
# because certain godot controls swallow events (like textedit)
|
||||
# we protect this with is_visible_in_tree to not
|
||||
# invoke a shortcut by accident
|
||||
|
||||
if get_viewport().gui_get_focus_owner() is TextEdit || get_viewport().gui_get_focus_owner() is LineEdit:
|
||||
return
|
||||
|
||||
if (event is InputEventKey and event is InputEventWithModifiers and is_visible_in_tree()):
|
||||
# CTRL Z -> UNDO
|
||||
if is_event_pressed(event, KEY_Z, false, false, true):
|
||||
TimelineUndoRedo.undo()
|
||||
indent_events()
|
||||
get_viewport().set_input_as_handled()
|
||||
if (event is InputEventKey and event is InputEventWithModifiers and is_visible_in_tree()):
|
||||
# CTRL +SHIFT+ Z // CTRL + Y -> REDO
|
||||
if is_event_pressed(event, KEY_Z, false, true, true) or is_event_pressed(event, KEY_Y, false, false, true):
|
||||
TimelineUndoRedo.redo()
|
||||
indent_events()
|
||||
get_viewport().set_input_as_handled()
|
||||
if (event is InputEventKey and event is InputEventWithModifiers and is_visible_in_tree()):
|
||||
# UP -> select previous
|
||||
if is_event_pressed(event, KEY_UP, false, false, false):
|
||||
if (len(selected_items) == 1):
|
||||
var prev = max(0, selected_items[0].get_index() - 1)
|
||||
var prev_node = %Timeline.get_child(prev)
|
||||
if (prev_node != selected_items[0]):
|
||||
selected_items = []
|
||||
select_item(prev_node)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
# DOWN -> select next
|
||||
if is_event_pressed(event, KEY_DOWN, false, false, false):
|
||||
if (len(selected_items) == 1):
|
||||
var next = min(%Timeline.get_child_count() - 1, selected_items[0].get_index() + 1)
|
||||
var next_node = %Timeline.get_child(next)
|
||||
if (next_node != selected_items[0]):
|
||||
selected_items = []
|
||||
select_item(next_node)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
# DELETE
|
||||
if is_event_pressed(event, KEY_DELETE, false, false, false):
|
||||
if (len(selected_items) != 0):
|
||||
var events_indexed = get_events_indexed(selected_items)
|
||||
TimelineUndoRedo.create_action("[D] Deleting "+str(len(selected_items))+" event(s).")
|
||||
TimelineUndoRedo.add_do_method(delete_events_indexed.bind(events_indexed))
|
||||
TimelineUndoRedo.add_undo_method(add_events_indexed.bind(events_indexed))
|
||||
TimelineUndoRedo.commit_action()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
# CTRL T -> Add text event
|
||||
if is_event_pressed(event, KEY_T, false, false, true):
|
||||
_add_event_button_pressed(DialogicTextEvent.new())
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
# CTRL A -> select all
|
||||
if is_event_pressed(event, KEY_A, false, false, true):
|
||||
if (len(selected_items) != 0):
|
||||
select_all_items()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
# CTRL SHIFT A -> deselect all
|
||||
if is_event_pressed(event, KEY_A, false, true, true):
|
||||
if (len(selected_items) != 0):
|
||||
deselect_all_items()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
# CTRL C
|
||||
if is_event_pressed(event, KEY_C, false, false, true):
|
||||
copy_selected_events()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
# CTRL V
|
||||
if is_event_pressed(event, KEY_V, false, false, true):
|
||||
var events_list = paste_check()
|
||||
var paste_position = -1
|
||||
if selected_items:
|
||||
paste_position = selected_items[-1].get_index()
|
||||
else:
|
||||
paste_position = %Timeline.get_child_count()-1
|
||||
if events_list:
|
||||
TimelineUndoRedo.create_action("[D] Pasting "+str(len(events_list))+" event(s).")
|
||||
TimelineUndoRedo.add_do_method(add_events_at_index.bind(events_list, paste_position))
|
||||
TimelineUndoRedo.add_undo_method(remove_events_at_index.bind(paste_position+1, len(events_list)))
|
||||
TimelineUndoRedo.commit_action()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
# CTRL X
|
||||
if is_event_pressed(event, KEY_X, false, false, true):
|
||||
var events_indexed = get_events_indexed(selected_items)
|
||||
TimelineUndoRedo.create_action("[D] Cut "+str(len(selected_items))+" event(s).")
|
||||
TimelineUndoRedo.add_do_method(cut_events_indexed.bind(events_indexed))
|
||||
TimelineUndoRedo.add_undo_method(add_events_indexed.bind(events_indexed))
|
||||
TimelineUndoRedo.commit_action()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
# CTRL D
|
||||
if is_event_pressed(event, KEY_D, false, false, true):
|
||||
if len(selected_items) > 0:
|
||||
var events = get_events_indexed(selected_items).values()
|
||||
var at_index = selected_items[-1].get_index()
|
||||
TimelineUndoRedo.create_action("[D] Duplicate "+str(len(events))+" event(s).")
|
||||
TimelineUndoRedo.add_do_method(add_events_at_index.bind(events, at_index))
|
||||
TimelineUndoRedo.add_undo_method(remove_events_at_index.bind(at_index, len(events)))
|
||||
TimelineUndoRedo.commit_action()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
# ALT UP -> move selected up
|
||||
if is_event_pressed(event, KEY_UP, true, false, false):
|
||||
if (len(selected_items) == 1):
|
||||
move_block_up(selected_items[0])
|
||||
indent_events()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
# ALT DOWN -> move selected down
|
||||
if is_event_pressed(event, KEY_DOWN, true, false, false):
|
||||
if (len(selected_items) == 1):
|
||||
move_block_down(selected_items[0])
|
||||
indent_events()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
# helper to make the above methods shorter
|
||||
func is_event_pressed(event, keycode, alt:bool, shift:bool, ctrl_or_meta:bool):
|
||||
return (event.pressed and event.alt_pressed == alt
|
||||
and event.shift_pressed == shift
|
||||
and (event.ctrl_pressed or event.meta_pressed ) == ctrl_or_meta
|
||||
and event.keycode == keycode
|
||||
and event.echo == false)
|
||||
|
||||
## *****************************************************************************
|
||||
## DELETING, COPY, PASTE
|
||||
## *****************************************************************************
|
||||
|
||||
func get_events_indexed(events:Array) -> Dictionary:
|
||||
var indexed_dict = {}
|
||||
for event in events:
|
||||
if not event.resource is DialogicEndBranchEvent:
|
||||
indexed_dict[event.get_index()] = event.resource.to_text()
|
||||
if 'end_node' in event and event.end_node:
|
||||
indexed_dict[event.end_node.get_index()] = event.end_node.resource.to_text()+str(event.get_index())
|
||||
return indexed_dict
|
||||
|
||||
|
||||
func select_indexed_events(indexed_events:Dictionary) -> void:
|
||||
selected_items = []
|
||||
for event_index in indexed_events.keys():
|
||||
selected_items.append(%Timeline.get_child(event_index))
|
||||
|
||||
|
||||
func add_events_indexed(indexed_events:Dictionary) -> void:
|
||||
var indexes = indexed_events.keys()
|
||||
indexes.sort()
|
||||
|
||||
var events = []
|
||||
for event_idx in indexes:
|
||||
deselect_all_items()
|
||||
|
||||
var event_resource :Variant
|
||||
if get_parent().editors_manager.resource_helper:
|
||||
for i in get_parent().editors_manager.resource_helper.get_event_scripts():
|
||||
if i._test_event_string(indexed_events[event_idx]):
|
||||
event_resource = i.duplicate()
|
||||
break
|
||||
else:
|
||||
printerr("[Dialogic] Unable to access resource_helper!")
|
||||
continue
|
||||
event_resource.from_text(indexed_events[event_idx])
|
||||
if event_resource is DialogicEndBranchEvent:
|
||||
events.append(create_end_branch_event(%Timeline.get_child_count(), %Timeline.get_child(indexed_events[event_idx].trim_prefix('<<END BRANCH>>').to_int())))
|
||||
%Timeline.move_child(events[-1], event_idx)
|
||||
else:
|
||||
events.append(add_event_node(event_resource))
|
||||
%Timeline.move_child(events[-1], event_idx)
|
||||
|
||||
selected_items = events
|
||||
visual_update_selection()
|
||||
|
||||
|
||||
func delete_events_indexed(indexed_events:Dictionary) -> void:
|
||||
select_indexed_events(indexed_events)
|
||||
delete_selected_events()
|
||||
|
||||
|
||||
func delete_selected_events():
|
||||
if len(selected_items) == 0:
|
||||
return
|
||||
|
||||
# get next element
|
||||
var next = min(%Timeline.get_child_count() - 1, selected_items[-1].get_index() + 1)
|
||||
var next_node = %Timeline.get_child(next)
|
||||
if _is_item_selected(next_node):
|
||||
next_node = null
|
||||
|
||||
for event in selected_items:
|
||||
if event.resource is DialogicEndBranchEvent:
|
||||
continue
|
||||
if 'end_node' in event and event.end_node != null:
|
||||
event.end_node.get_parent().remove_child(event.end_node)
|
||||
event.end_node.queue_free()
|
||||
event.get_parent().remove_child(event)
|
||||
event.queue_free()
|
||||
|
||||
# select next
|
||||
if (next_node != null):
|
||||
select_item(next_node, false)
|
||||
else:
|
||||
if (%Timeline.get_child_count() > 0):
|
||||
next_node = %Timeline.get_child(max(0, %Timeline.get_child_count() - 1))
|
||||
if (next_node != null):
|
||||
select_item(next_node, false)
|
||||
else:
|
||||
deselect_all_items()
|
||||
|
||||
indent_events()
|
||||
|
||||
|
||||
func cut_selected_events():
|
||||
copy_selected_events()
|
||||
delete_selected_events()
|
||||
|
||||
|
||||
func cut_events_indexed(indexed_events:Dictionary) -> void:
|
||||
select_indexed_events(indexed_events)
|
||||
cut_selected_events()
|
||||
|
||||
|
||||
func copy_selected_events():
|
||||
if len(selected_items) == 0:
|
||||
return
|
||||
var event_copy_array = []
|
||||
for item in selected_items:
|
||||
event_copy_array.append(item.resource.to_text())
|
||||
var _json = JSON.new()
|
||||
DisplayServer.clipboard_set(_json.stringify(
|
||||
{
|
||||
"events":event_copy_array,
|
||||
"project_name": ProjectSettings.get_setting("application/config/name")
|
||||
}))
|
||||
|
||||
|
||||
func paste_check():
|
||||
var _json = JSON.new()
|
||||
var clipboard_parse = _json.parse(DisplayServer.clipboard_get())
|
||||
if clipboard_parse == OK:
|
||||
clipboard_parse = _json.get_data()
|
||||
if clipboard_parse.has("project_name"):
|
||||
if clipboard_parse.project_name != ProjectSettings.get_setting("application/config/name"):
|
||||
print("[D] Be careful when copying from another project!")
|
||||
if clipboard_parse.has('events'):
|
||||
return clipboard_parse.events
|
||||
|
||||
|
||||
func remove_events_at_index(at_index:int, amount:int = 1)-> void:
|
||||
selected_items = []
|
||||
something_changed()
|
||||
for i in range(0, amount):
|
||||
selected_items.append(%Timeline.get_child(at_index + i))
|
||||
delete_selected_events()
|
||||
|
||||
|
||||
func add_events_at_index(event_list:Array, at_index:int) -> void:
|
||||
if at_index != -1:
|
||||
event_list.reverse()
|
||||
selected_items = [%Timeline.get_child(at_index)]
|
||||
else:
|
||||
selected_items = []
|
||||
|
||||
var new_items := []
|
||||
for item in event_list:
|
||||
if typeof(item) == TYPE_STRING:
|
||||
var resource: Variant
|
||||
if get_parent().editors_manager.resource_helper:
|
||||
for i in get_parent().editors_manager.resource_helper.get_event_scripts():
|
||||
if i._test_event_string(item):
|
||||
resource = i.duplicate()
|
||||
break
|
||||
else:
|
||||
printerr("[Dialogic] Unable to access resource_helper!")
|
||||
continue
|
||||
if resource['event_name'] == 'Character' or resource['event_name'] == 'Text':
|
||||
resource.set_meta('editor_character_directory', find_parent('EditorView').character_directory)
|
||||
resource.from_text(item)
|
||||
if item:
|
||||
new_items.append(add_event_node(resource))
|
||||
selected_items = new_items
|
||||
something_changed()
|
||||
sort_selection()
|
||||
visual_update_selection()
|
||||
indent_events()
|
||||
|
||||
|
||||
## *****************************************************************************
|
||||
## BLOCK SELECTION
|
||||
## *****************************************************************************
|
||||
|
||||
func _is_item_selected(item: Node):
|
||||
return item in selected_items
|
||||
|
||||
func select_item(item: Node, multi_possible:bool = true):
|
||||
if item == null:
|
||||
return
|
||||
|
||||
if Input.is_key_pressed(KEY_CTRL) and multi_possible:
|
||||
# deselect the item if it is selected
|
||||
if _is_item_selected(item):
|
||||
selected_items.erase(item)
|
||||
else:
|
||||
selected_items.append(item)
|
||||
elif Input.is_key_pressed(KEY_SHIFT) and multi_possible:
|
||||
|
||||
if len(selected_items) == 0:
|
||||
selected_items = [item]
|
||||
else:
|
||||
var index = selected_items[-1].get_index()
|
||||
var goal_idx = item.get_index()
|
||||
while true:
|
||||
if index < goal_idx: index += 1
|
||||
else: index -= 1
|
||||
if not %Timeline.get_child(index) in selected_items:
|
||||
selected_items.append(%Timeline.get_child(index))
|
||||
|
||||
if index == goal_idx:
|
||||
break
|
||||
else:
|
||||
if len(selected_items) == 1:
|
||||
if _is_item_selected(item):
|
||||
selected_items.erase(item)
|
||||
else:
|
||||
selected_items = [item]
|
||||
else:
|
||||
selected_items = [item]
|
||||
|
||||
sort_selection()
|
||||
visual_update_selection()
|
||||
|
||||
# checks all the events and sets their styles (selected/deselected)
|
||||
func visual_update_selection():
|
||||
for item in %Timeline.get_children():
|
||||
item.visual_deselect()
|
||||
if 'end_node' in item and item.end_node != null:
|
||||
item.end_node.unhighlight()
|
||||
for item in selected_items:
|
||||
item.visual_select()
|
||||
if 'end_node' in item and item.end_node != null:
|
||||
item.end_node.highlight()
|
||||
|
||||
## Sorts the selection using 'custom_sort_selection'
|
||||
func sort_selection():
|
||||
selected_items.sort_custom(custom_sort_selection)
|
||||
|
||||
## Compares two event blocks based on their position in the timeline
|
||||
func custom_sort_selection(item1, item2):
|
||||
return item1.get_index() < item2.get_index()
|
||||
|
||||
## Helpers
|
||||
func select_all_items():
|
||||
|
||||
selected_items = []
|
||||
for event in %Timeline.get_children():
|
||||
selected_items.append(event)
|
||||
visual_update_selection()
|
||||
|
||||
func deselect_all_items():
|
||||
selected_items = []
|
||||
visual_update_selection()
|
||||
|
||||
|
||||
## *****************************************************************************
|
||||
## CREATING NEW EVENTS USING THE BUTTONS
|
||||
## *****************************************************************************
|
||||
# Event Creation signal for buttons
|
||||
func _add_event_button_pressed(event_resource:DialogicEvent):
|
||||
var at_index := -1
|
||||
if selected_items:
|
||||
at_index = selected_items[-1].get_index()+1
|
||||
else:
|
||||
at_index = %Timeline.get_child_count()
|
||||
|
||||
var remove_event_index := 1
|
||||
|
||||
TimelineUndoRedo.create_action("[D] Add "+event_resource.event_name+" event.")
|
||||
if event_resource.can_contain_events:
|
||||
TimelineUndoRedo.add_do_method(add_event_with_end_branch.bind(event_resource.duplicate(), at_index, true, true))
|
||||
TimelineUndoRedo.add_undo_method(remove_events_at_index.bind(at_index, 2))
|
||||
else:
|
||||
TimelineUndoRedo.add_do_method(add_event_node.bind(event_resource.duplicate(), at_index, true, true))
|
||||
TimelineUndoRedo.add_undo_method(remove_events_at_index.bind(at_index, 1))
|
||||
TimelineUndoRedo.commit_action()
|
||||
|
||||
something_changed()
|
||||
scroll_to_piece(at_index)
|
||||
indent_events()
|
||||
|
||||
## *****************************************************************************
|
||||
## CREATING THE TIMELINE
|
||||
## *****************************************************************************
|
||||
# Adding an event to the timeline
|
||||
func add_event_node(event_resource:DialogicEvent, at_index:int = -1, auto_select: bool = false, indent: bool = false):
|
||||
if event_resource is DialogicEndBranchEvent:
|
||||
return create_end_branch_event(at_index, %Timeline.get_child(0))
|
||||
|
||||
if event_resource['event_node_ready'] == false:
|
||||
if event_resource['event_node_as_text'] != "":
|
||||
event_resource._load_from_string(event_resource['event_node_as_text'])
|
||||
|
||||
var piece = event_node.instantiate()
|
||||
var resource = event_resource
|
||||
piece.resource = event_resource
|
||||
event_resource._editor_node = piece
|
||||
piece.content_changed.connect(something_changed)
|
||||
if at_index == -1:
|
||||
if len(selected_items) != 0:
|
||||
selected_items[0].add_sibling(piece)
|
||||
else:
|
||||
%Timeline.add_child(piece)
|
||||
else:
|
||||
%Timeline.add_child(piece)
|
||||
%Timeline.move_child(piece, at_index)
|
||||
|
||||
piece.option_action.connect(_on_event_options_action.bind(piece))
|
||||
piece.gui_input.connect(_on_event_block_gui_input.bind(piece))
|
||||
|
||||
# Buidling editing part
|
||||
piece.build_editor()
|
||||
|
||||
if auto_select:
|
||||
select_item(piece, false)
|
||||
|
||||
# Spacing
|
||||
add_extra_scroll_area_to_timeline()
|
||||
|
||||
# Indent on create
|
||||
if indent:
|
||||
indent_events()
|
||||
|
||||
if not _building_timeline:
|
||||
piece.focus()
|
||||
|
||||
return piece
|
||||
|
||||
func create_end_branch_event(at_index:int, parent_node:Node) -> Node:
|
||||
var end_branch_event = load("res://addons/dialogic/Editor/Events/BranchEnd.tscn").instantiate()
|
||||
end_branch_event.resource = DialogicEndBranchEvent.new()
|
||||
end_branch_event.gui_input.connect(_on_event_block_gui_input.bind(end_branch_event))
|
||||
parent_node.end_node = end_branch_event
|
||||
end_branch_event.parent_node = parent_node
|
||||
end_branch_event.add_end_control(parent_node.resource.get_end_branch_control())
|
||||
%Timeline.add_child(end_branch_event)
|
||||
%Timeline.move_child(end_branch_event, at_index)
|
||||
return end_branch_event
|
||||
|
||||
# combination of the above that establishes the correct connection between the event and it's end branch
|
||||
func add_event_with_end_branch(resource, at_index:int=-1, auto_select:bool = false, indent:bool = false):
|
||||
var event = add_event_node(resource, at_index, auto_select, indent)
|
||||
create_end_branch_event(at_index+1, event)
|
||||
|
||||
## *****************************************************************************
|
||||
## BLOCK GETTERS
|
||||
## *****************************************************************************
|
||||
func get_block_above(block):
|
||||
var block_index = block.get_index()
|
||||
var item = null
|
||||
if block_index > 0:
|
||||
item = %Timeline.get_child(block_index - 1)
|
||||
return item
|
||||
|
||||
|
||||
func get_block_below(block):
|
||||
var block_index = block.get_index()
|
||||
var item = null
|
||||
if block_index < %Timeline.get_child_count() - 1:
|
||||
item = %Timeline.get_child(block_index + 1)
|
||||
return item
|
||||
|
||||
|
||||
func get_block_height(block):
|
||||
if block != null:
|
||||
return block.size.y
|
||||
else:
|
||||
return null
|
||||
|
||||
|
||||
func get_index_under_cursor():
|
||||
var current_position = get_global_mouse_position()
|
||||
var top_pos = 0
|
||||
for i in range(%Timeline.get_child_count()):
|
||||
var c = %Timeline.get_child(i)
|
||||
if c.global_position.y < current_position.y:
|
||||
top_pos = i
|
||||
return top_pos
|
||||
|
||||
## *****************************************************************************
|
||||
## BLOCK MOVEMENT
|
||||
## *****************************************************************************
|
||||
func move_block_up(block):
|
||||
if block.get_index() < 1: return false
|
||||
%Timeline.move_child(block, block.get_index() - 1)
|
||||
%TimelineArea.queue_redraw()
|
||||
something_changed()
|
||||
return true
|
||||
|
||||
|
||||
func move_block_down(block):
|
||||
%Timeline.move_child(block, block.get_index() + 1)
|
||||
%TimelineArea.queue_redraw()
|
||||
something_changed()
|
||||
return true
|
||||
|
||||
func move_block_to_index(block_index, index):
|
||||
something_changed()
|
||||
%Timeline.move_child(%Timeline.get_child(block_index), index)
|
||||
|
||||
## *****************************************************************************
|
||||
## VISIBILITY/VISUALS
|
||||
## *****************************************************************************
|
||||
func scroll_to_piece(piece_index) -> void:
|
||||
var height = 0
|
||||
for i in range(0, piece_index):
|
||||
height += %Timeline.get_child(i).size.y
|
||||
if height < %TimelineArea.scroll_vertical or height > %TimelineArea.scroll_vertical+%TimelineArea.size.y-(200*DialogicUtil.get_editor_scale()):
|
||||
%TimelineArea.scroll_vertical = height
|
||||
|
||||
|
||||
func indent_events() -> void:
|
||||
var indent: int = 0
|
||||
var event_list: Array = %Timeline.get_children()
|
||||
|
||||
if event_list.size() < 2:
|
||||
return
|
||||
|
||||
var currently_hidden = false
|
||||
var hidden_until = null
|
||||
|
||||
# will be applied to the indent after the current event
|
||||
var delayed_indent: int = 0
|
||||
|
||||
for event in event_list:
|
||||
if (not "resource" in event):
|
||||
continue
|
||||
|
||||
if (not currently_hidden) and event.resource.can_contain_events and event.end_node and event.collapsed:
|
||||
currently_hidden = true
|
||||
hidden_until = event.end_node
|
||||
elif currently_hidden and event == hidden_until:
|
||||
currently_hidden = false
|
||||
hidden_until = null
|
||||
elif currently_hidden:
|
||||
event.hide()
|
||||
else:
|
||||
event.show()
|
||||
|
||||
delayed_indent = 0
|
||||
|
||||
if event.resource.can_contain_events:
|
||||
delayed_indent = 1
|
||||
|
||||
if event.resource.needs_parent_event:
|
||||
var current_block_above = get_block_above(event)
|
||||
while current_block_above != null and current_block_above.resource is DialogicEndBranchEvent:
|
||||
current_block_above = get_block_above(current_block_above.parent_node)
|
||||
|
||||
if current_block_above != null and event.resource.is_expected_parent_event(current_block_above.resource):
|
||||
indent += 1
|
||||
|
||||
if event.resource is DialogicEndBranchEvent:
|
||||
event.parent_node_changed()
|
||||
delayed_indent -= 1
|
||||
if event.parent_node.resource.needs_parent_event:
|
||||
delayed_indent -= 1
|
||||
|
||||
if indent >= 0:
|
||||
event.set_indent(indent)
|
||||
else:
|
||||
event.set_indent(0)
|
||||
indent += delayed_indent
|
||||
|
||||
%TimelineArea.queue_redraw()
|
||||
|
||||
|
||||
func add_extra_scroll_area_to_timeline():
|
||||
if %Timeline.get_children().size() > 4:
|
||||
%Timeline.custom_minimum_size.y = 0
|
||||
%Timeline.size.y = 0
|
||||
if %Timeline.size.y + 200 > %TimelineArea.size.y:
|
||||
%Timeline.custom_minimum_size = Vector2(0, %Timeline.size.y + 200)
|
||||
|
||||
|
||||
## *****************************************************************************
|
||||
## SPECIAL BLOCK OPERATIONS
|
||||
## *****************************************************************************
|
||||
# SIGNAL handles the actions of the small menu on the right (that was removed wasn't it?)
|
||||
func _on_event_options_action(action: String, item: Node):
|
||||
### WORK TODO
|
||||
if action == "remove":
|
||||
delete_selected_events()
|
||||
elif action == "up":
|
||||
move_block_up(item)
|
||||
elif action == "down":
|
||||
move_block_down(item)
|
||||
indent_events()
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
[gd_scene load_steps=5 format=3 uid="uid://b3fd6xpit60pb"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/dialogic/Editor/TimelineEditor/VisualEditor/timeline_editor_visual.gd" id="1_8smxc"]
|
||||
[ext_resource type="Theme" uid="uid://cqst728xxipcw" path="res://addons/dialogic/Editor/Theme/MainTheme.tres" id="2_x0fhp"]
|
||||
[ext_resource type="Script" path="res://addons/dialogic/Editor/TimelineEditor/VisualEditor/TimelineArea.gd" id="3_sap1x"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_p7s2v"]
|
||||
content_margin_left = 4.0
|
||||
content_margin_top = 4.0
|
||||
content_margin_right = 4.0
|
||||
content_margin_bottom = 4.0
|
||||
bg_color = Color(1, 0.365, 0.365, 1)
|
||||
draw_center = false
|
||||
border_width_left = 2
|
||||
border_width_top = 2
|
||||
border_width_right = 2
|
||||
border_width_bottom = 2
|
||||
corner_detail = 1
|
||||
|
||||
[node name="TimelineVisualEditor" type="MarginContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/margin_left = 5
|
||||
theme_override_constants/margin_right = 5
|
||||
theme_override_constants/margin_bottom = 5
|
||||
script = ExtResource("1_8smxc")
|
||||
|
||||
[node name="View" type="HSplitContainer" parent="."]
|
||||
layout_mode = 2
|
||||
offset_left = 5.0
|
||||
offset_right = 1147.0
|
||||
offset_bottom = 643.0
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme = ExtResource("2_x0fhp")
|
||||
split_offset = 7
|
||||
|
||||
[node name="TimelineArea" type="ScrollContainer" parent="View"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
offset_right = 980.0
|
||||
offset_bottom = 643.0
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_p7s2v")
|
||||
script = ExtResource("3_sap1x")
|
||||
|
||||
[node name="Timeline" type="VBoxContainer" parent="View/TimelineArea"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
offset_left = 4.0
|
||||
offset_top = 4.0
|
||||
offset_right = 976.0
|
||||
offset_bottom = 639.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="View"]
|
||||
custom_minimum_size = Vector2(150, 0)
|
||||
layout_mode = 2
|
||||
offset_left = 992.0
|
||||
offset_right = 1142.0
|
||||
offset_bottom = 643.0
|
||||
|
||||
[node name="EventContainer" type="VBoxContainer" parent="View/ScrollContainer"]
|
||||
layout_mode = 2
|
||||
offset_right = 150.0
|
||||
offset_bottom = 643.0
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="HBoxContainer6" type="HBoxContainer" parent="View/ScrollContainer/EventContainer"]
|
||||
layout_mode = 2
|
||||
offset_right = 150.0
|
||||
offset_bottom = 26.0
|
||||
|
||||
[node name="Label" type="Label" parent="View/ScrollContainer/EventContainer/HBoxContainer6"]
|
||||
layout_mode = 2
|
||||
offset_right = 39.0
|
||||
offset_bottom = 26.0
|
||||
text = "Main"
|
||||
|
||||
[node name="HSeparator2" type="HSeparator" parent="View/ScrollContainer/EventContainer/HBoxContainer6"]
|
||||
layout_mode = 2
|
||||
offset_left = 43.0
|
||||
offset_right = 150.0
|
||||
offset_bottom = 26.0
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="FlexContainer0" type="HFlowContainer" parent="View/ScrollContainer/EventContainer"]
|
||||
layout_mode = 2
|
||||
offset_top = 30.0
|
||||
offset_right = 150.0
|
||||
offset_bottom = 30.0
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="HBoxContainer5" type="HBoxContainer" parent="View/ScrollContainer/EventContainer"]
|
||||
layout_mode = 2
|
||||
offset_top = 34.0
|
||||
offset_right = 150.0
|
||||
offset_bottom = 60.0
|
||||
|
||||
[node name="Label2" type="Label" parent="View/ScrollContainer/EventContainer/HBoxContainer5"]
|
||||
layout_mode = 2
|
||||
offset_right = 40.0
|
||||
offset_bottom = 26.0
|
||||
text = "Logic"
|
||||
|
||||
[node name="HSeparator2" type="HSeparator" parent="View/ScrollContainer/EventContainer/HBoxContainer5"]
|
||||
layout_mode = 2
|
||||
offset_left = 44.0
|
||||
offset_right = 150.0
|
||||
offset_bottom = 26.0
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="FlexContainer1" type="HFlowContainer" parent="View/ScrollContainer/EventContainer"]
|
||||
layout_mode = 2
|
||||
offset_top = 64.0
|
||||
offset_right = 150.0
|
||||
offset_bottom = 64.0
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="HBoxContainer3" type="HBoxContainer" parent="View/ScrollContainer/EventContainer"]
|
||||
layout_mode = 2
|
||||
offset_top = 68.0
|
||||
offset_right = 150.0
|
||||
offset_bottom = 94.0
|
||||
|
||||
[node name="Label3" type="Label" parent="View/ScrollContainer/EventContainer/HBoxContainer3"]
|
||||
layout_mode = 2
|
||||
offset_right = 67.0
|
||||
offset_bottom = 26.0
|
||||
text = "Timeline"
|
||||
|
||||
[node name="HSeparator2" type="HSeparator" parent="View/ScrollContainer/EventContainer/HBoxContainer3"]
|
||||
layout_mode = 2
|
||||
offset_left = 71.0
|
||||
offset_right = 150.0
|
||||
offset_bottom = 26.0
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="FlexContainer2" type="HFlowContainer" parent="View/ScrollContainer/EventContainer"]
|
||||
layout_mode = 2
|
||||
offset_top = 98.0
|
||||
offset_right = 150.0
|
||||
offset_bottom = 98.0
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="HBoxContainer4" type="HBoxContainer" parent="View/ScrollContainer/EventContainer"]
|
||||
layout_mode = 2
|
||||
offset_top = 102.0
|
||||
offset_right = 150.0
|
||||
offset_bottom = 128.0
|
||||
|
||||
[node name="Label4" type="Label" parent="View/ScrollContainer/EventContainer/HBoxContainer4"]
|
||||
layout_mode = 2
|
||||
offset_right = 98.0
|
||||
offset_bottom = 26.0
|
||||
text = "Audio/Visual"
|
||||
|
||||
[node name="HSeparator2" type="HSeparator" parent="View/ScrollContainer/EventContainer/HBoxContainer4"]
|
||||
layout_mode = 2
|
||||
offset_left = 102.0
|
||||
offset_right = 150.0
|
||||
offset_bottom = 26.0
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="FlexContainer3" type="HFlowContainer" parent="View/ScrollContainer/EventContainer"]
|
||||
layout_mode = 2
|
||||
offset_top = 132.0
|
||||
offset_right = 150.0
|
||||
offset_bottom = 132.0
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="View/ScrollContainer/EventContainer"]
|
||||
layout_mode = 2
|
||||
offset_top = 136.0
|
||||
offset_right = 150.0
|
||||
offset_bottom = 162.0
|
||||
|
||||
[node name="Label5" type="Label" parent="View/ScrollContainer/EventContainer/HBoxContainer"]
|
||||
layout_mode = 2
|
||||
offset_right = 48.0
|
||||
offset_bottom = 26.0
|
||||
text = "Godot"
|
||||
|
||||
[node name="HSeparator2" type="HSeparator" parent="View/ScrollContainer/EventContainer/HBoxContainer"]
|
||||
layout_mode = 2
|
||||
offset_left = 52.0
|
||||
offset_right = 150.0
|
||||
offset_bottom = 26.0
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="FlexContainer4" type="HFlowContainer" parent="View/ScrollContainer/EventContainer"]
|
||||
layout_mode = 2
|
||||
offset_top = 166.0
|
||||
offset_right = 150.0
|
||||
offset_bottom = 166.0
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="OtherContainer" type="HBoxContainer" parent="View/ScrollContainer/EventContainer"]
|
||||
layout_mode = 2
|
||||
offset_top = 170.0
|
||||
offset_right = 150.0
|
||||
offset_bottom = 196.0
|
||||
|
||||
[node name="Label6" type="Label" parent="View/ScrollContainer/EventContainer/OtherContainer"]
|
||||
layout_mode = 2
|
||||
offset_right = 46.0
|
||||
offset_bottom = 26.0
|
||||
text = "Other"
|
||||
|
||||
[node name="HSeparator2" type="HSeparator" parent="View/ScrollContainer/EventContainer/OtherContainer"]
|
||||
layout_mode = 2
|
||||
offset_left = 50.0
|
||||
offset_right = 150.0
|
||||
offset_bottom = 26.0
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="FlexContainer5" type="HFlowContainer" parent="View/ScrollContainer/EventContainer"]
|
||||
layout_mode = 2
|
||||
offset_top = 200.0
|
||||
offset_right = 150.0
|
||||
offset_bottom = 200.0
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="Spacer" type="Control" parent="View/ScrollContainer/EventContainer"]
|
||||
layout_mode = 2
|
||||
anchors_preset = 0
|
||||
offset_top = 204.0
|
||||
offset_right = 150.0
|
||||
offset_bottom = 204.0
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
extends Control
|
||||
|
||||
func _ready() -> void:
|
||||
$PauseIndictator.hide()
|
||||
var dialog_scene_path: String = DialogicUtil.get_project_setting(
|
||||
'dialogic/editor/test_dialog_scene', "res://addons/dialogic/Example Assets/example-scenes/DialogicDefaultScene.tscn")
|
||||
var scene: Node = load(dialog_scene_path).instantiate()
|
||||
add_child(scene)
|
||||
if scene is CanvasLayer:
|
||||
if scene is Control:
|
||||
scene.rect_position = get_viewport_rect().size/2.0
|
||||
if scene is Node2D:
|
||||
scene.position = get_viewport_rect().size/2.0
|
||||
|
||||
randomize()
|
||||
var current_timeline: String = ProjectSettings.get_setting('dialogic/editor/current_timeline_path')
|
||||
Dialogic.start_timeline(current_timeline)
|
||||
Dialogic.timeline_ended.connect(get_tree().quit)
|
||||
Dialogic.signal_event.connect(recieve_event_signal)
|
||||
Dialogic.text_signal.connect(recieve_text_signal)
|
||||
|
||||
func recieve_event_signal(argument:String) -> void:
|
||||
print("[Dialogic] Encountered a signal event: ", argument)
|
||||
|
||||
func recieve_text_signal(argument:String) -> void:
|
||||
print("[Dialogic] Encountered a signal in text: ", argument)
|
||||
|
||||
func _input(event:InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
||||
Dialogic.paused = !Dialogic.paused
|
||||
$PauseIndictator.visible = Dialogic.paused
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
[gd_scene load_steps=2 format=3 uid="uid://ud18ke1g2nw4"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/dialogic/Editor/TimelineEditor/test_timeline_scene.gd" id="1_bamud"]
|
||||
|
||||
[node name="TestTimelineScene" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
script = ExtResource("1_bamud")
|
||||
|
||||
[node name="PauseIndictator" type="Label" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 1
|
||||
anchor_left = 1.0
|
||||
anchor_right = 1.0
|
||||
offset_left = -65.0
|
||||
offset_top = 7.0
|
||||
offset_right = -8.0
|
||||
offset_bottom = 33.0
|
||||
grow_horizontal = 0
|
||||
text = "Paused"
|
||||
metadata/_edit_layout_mode = 1
|
||||
140
Sprout/addons/dialogic/Editor/TimelineEditor/timeline_editor.gd
Normal file
140
Sprout/addons/dialogic/Editor/TimelineEditor/timeline_editor.gd
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
@tool
|
||||
extends DialogicEditor
|
||||
|
||||
## Editor that holds both the visual and the text timeline editors.
|
||||
|
||||
# references
|
||||
var editor_mode_toggle_button : Button
|
||||
var current_editor_mode: int = 0 # 0 = visal, 1 = text
|
||||
|
||||
|
||||
## Overwrite. Register to the editor manager in here.
|
||||
func _register() -> void:
|
||||
resource_unsaved.connect(_on_resource_unsaved)
|
||||
resource_saved.connect(_on_resource_saved)
|
||||
|
||||
# register editor
|
||||
editors_manager.register_resource_editor('dtl', self)
|
||||
# add timeline button
|
||||
var add_timeline_button: Button = editors_manager.add_icon_button(
|
||||
load("res://addons/dialogic/Editor/Images/Toolbar/add-timeline.svg"),
|
||||
"Add Timeline",
|
||||
self)
|
||||
add_timeline_button.pressed.connect(editors_manager.show_add_resource_dialog.bind(
|
||||
new_timeline,
|
||||
'*.dtl; DialogicTimeline',
|
||||
'Create new timeline',
|
||||
'timeline',
|
||||
))
|
||||
# play timeline button
|
||||
var play_timeline_button: Button = editors_manager.add_custom_button(
|
||||
"Play Timeline",
|
||||
get_theme_icon("PlayScene", "EditorIcons"),
|
||||
self)
|
||||
play_timeline_button.pressed.connect(play_timeline)
|
||||
# switch editor mode button
|
||||
editor_mode_toggle_button = editors_manager.add_custom_button(
|
||||
"Text editor",
|
||||
get_theme_icon("ArrowRight", "EditorIcons"),
|
||||
self)
|
||||
editor_mode_toggle_button.pressed.connect(toggle_editor_mode)
|
||||
|
||||
$VisualEditor.load_event_buttons()
|
||||
|
||||
current_editor_mode = DialogicUtil.get_project_setting('dialogic/editor/timeline_editor_mode', 0)
|
||||
|
||||
match current_editor_mode:
|
||||
0:
|
||||
$VisualEditor.show()
|
||||
$TextEditor.hide()
|
||||
editor_mode_toggle_button.text = "Text Editor"
|
||||
1:
|
||||
$VisualEditor.hide()
|
||||
$TextEditor.show()
|
||||
editor_mode_toggle_button.text = "Visual Editor"
|
||||
$NoTimelineScreen.show()
|
||||
|
||||
|
||||
## If this editor supports editing resources, load them here (overwrite in subclass)
|
||||
func _open_resource(resource:Resource) -> void:
|
||||
current_resource = resource
|
||||
current_resource_state = ResourceStates.Saved
|
||||
match current_editor_mode:
|
||||
0:
|
||||
$VisualEditor.load_timeline(current_resource)
|
||||
1:
|
||||
$TextEditor.load_timeline(current_resource)
|
||||
$NoTimelineScreen.hide()
|
||||
|
||||
|
||||
## If this editor supports editing resources, save them here (overwrite in subclass)
|
||||
func _save_resource() -> void:
|
||||
match current_editor_mode:
|
||||
0:
|
||||
$VisualEditor.save_timeline()
|
||||
1:
|
||||
$TextEditor.save_timeline()
|
||||
|
||||
|
||||
|
||||
## Method to play the current timeline. Connected to the button in the sidebar.
|
||||
func play_timeline():
|
||||
_save_resource()
|
||||
|
||||
var dialogic_plugin = DialogicUtil.get_dialogic_plugin()
|
||||
|
||||
# Save the current opened timeline
|
||||
ProjectSettings.set_setting('dialogic/editor/current_timeline_path', current_resource.resource_path)
|
||||
ProjectSettings.save()
|
||||
|
||||
DialogicUtil.get_dialogic_plugin().editor_interface.play_custom_scene("res://addons/dialogic/Editor/TimelineEditor/test_timeline_scene.tscn")
|
||||
|
||||
|
||||
## Method to switch from visual to text editor (and vice versa). Connected to the button in the sidebar.
|
||||
func toggle_editor_mode():
|
||||
match current_editor_mode:
|
||||
0:
|
||||
current_editor_mode = 1
|
||||
$VisualEditor.save_timeline()
|
||||
$VisualEditor.hide()
|
||||
$TextEditor.show()
|
||||
$TextEditor.load_timeline(current_resource)
|
||||
editor_mode_toggle_button.text = "Visual Editor"
|
||||
1:
|
||||
current_editor_mode = 0
|
||||
$TextEditor.save_timeline()
|
||||
$TextEditor.hide()
|
||||
$VisualEditor.load_timeline(current_resource)
|
||||
$VisualEditor.show()
|
||||
editor_mode_toggle_button.text = "Text Editor"
|
||||
|
||||
ProjectSettings.set_setting('dialogic/editor/timeline_editor_mode', current_editor_mode)
|
||||
ProjectSettings.save()
|
||||
|
||||
|
||||
func _on_resource_unsaved():
|
||||
current_resource.set_meta("timeline_not_saved", true)
|
||||
|
||||
|
||||
func _on_resource_saved():
|
||||
current_resource.set_meta("timeline_not_saved", false)
|
||||
|
||||
|
||||
func new_timeline(path:String) -> void:
|
||||
_save_resource()
|
||||
var new_timeline := DialogicTimeline.new()
|
||||
new_timeline.resource_path = path
|
||||
_open_resource(new_timeline)
|
||||
|
||||
|
||||
func _ready():
|
||||
$NoTimelineScreen.add_theme_stylebox_override("panel", get_theme_stylebox("Background", "EditorStyles"))
|
||||
|
||||
|
||||
func _on_create_timeline_button_pressed():
|
||||
editors_manager.show_add_resource_dialog(
|
||||
new_timeline,
|
||||
'*.dtl; DialogicTimeline',
|
||||
'Create new timeline',
|
||||
'timeline',
|
||||
)
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
[gd_scene load_steps=7 format=3 uid="uid://crce0na84rhfd"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/dialogic/Editor/TimelineEditor/timeline_editor.gd" id="1_4aceh"]
|
||||
[ext_resource type="PackedScene" uid="uid://b3fd6xpit60pb" path="res://addons/dialogic/Editor/TimelineEditor/VisualEditor/timeline_editor_visual.tscn" id="2_qs7vc"]
|
||||
[ext_resource type="PackedScene" uid="uid://defdeav8rli6o" path="res://addons/dialogic/Editor/TimelineEditor/TextEditor/timeline_editor_text.tscn" id="3_up2bn"]
|
||||
[ext_resource type="Script" path="res://addons/dialogic/Editor/TimelineEditor/TextEditor/syntax_highlighter.gd" id="4_1t6bf"]
|
||||
|
||||
[sub_resource type="SyntaxHighlighter" id="SyntaxHighlighter_wn64x"]
|
||||
script = ExtResource("4_1t6bf")
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_gyf34"]
|
||||
content_margin_left = 4.0
|
||||
content_margin_top = 4.0
|
||||
content_margin_right = 4.0
|
||||
content_margin_bottom = 4.0
|
||||
bg_color = Color(1, 0.365, 0.365, 1)
|
||||
draw_center = false
|
||||
border_width_left = 2
|
||||
border_width_top = 2
|
||||
border_width_right = 2
|
||||
border_width_bottom = 2
|
||||
corner_detail = 1
|
||||
|
||||
[node name="Timeline" type="MarginContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_4aceh")
|
||||
|
||||
[node name="VisualEditor" parent="." instance=ExtResource("2_qs7vc")]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 0
|
||||
theme_override_constants/margin_top = 0
|
||||
theme_override_constants/margin_right = 0
|
||||
theme_override_constants/margin_bottom = 0
|
||||
|
||||
[node name="TextEditor" parent="." instance=ExtResource("3_up2bn")]
|
||||
layout_mode = 2
|
||||
syntax_highlighter = SubResource("SyntaxHighlighter_wn64x")
|
||||
symbol_lookup_on_click = true
|
||||
line_folding = false
|
||||
gutters_draw_fold_gutter = false
|
||||
|
||||
[node name="NoTimelineScreen" type="PanelContainer" parent="."]
|
||||
layout_mode = 2
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_gyf34")
|
||||
|
||||
[node name="CenterContainer" type="CenterContainer" parent="NoTimelineScreen"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="NoTimelineScreen/CenterContainer"]
|
||||
custom_minimum_size = Vector2(250, 0)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="NoTimelineScreen/CenterContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "No timeline opened.
|
||||
Create a timeline or double-click one in the file system dock."
|
||||
horizontal_alignment = 1
|
||||
autowrap_mode = 3
|
||||
|
||||
[node name="CreateTimelineButton" type="Button" parent="NoTimelineScreen/CenterContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "Create New Timeline"
|
||||
|
||||
[connection signal="pressed" from="NoTimelineScreen/CenterContainer/VBoxContainer/CreateTimelineButton" to="." method="_on_create_timeline_button_pressed"]
|
||||
Loading…
Add table
Add a link
Reference in a new issue