added a talking rock

This commit is contained in:
2024-09-07 14:12:04 -04:00
parent 1573768619
commit 55a291e2ae
79 changed files with 14358 additions and 17 deletions

View File

@@ -0,0 +1,444 @@
@tool
extends CodeEdit
signal active_title_change(title: String)
signal error_clicked(line_number: int)
signal external_file_requested(path: String, title: String)
const DialogueManagerParser = preload("./parser.gd")
const DialogueSyntaxHighlighter = preload("./code_edit_syntax_highlighter.gd")
# A link back to the owner `MainView`
var main_view
# Theme overrides for syntax highlighting, etc
var theme_overrides: Dictionary:
set(value):
theme_overrides = value
syntax_highlighter = DialogueSyntaxHighlighter.new()
# General UI
add_theme_color_override("font_color", theme_overrides.text_color)
add_theme_color_override("background_color", theme_overrides.background_color)
add_theme_color_override("current_line_color", theme_overrides.current_line_color)
add_theme_font_override("font", get_theme_font("source", "EditorFonts"))
add_theme_font_size_override("font_size", theme_overrides.font_size * theme_overrides.scale)
font_size = round(theme_overrides.font_size)
get:
return theme_overrides
# Any parse errors
var errors: Array:
set(next_errors):
errors = next_errors
for i in range(0, get_line_count()):
var is_error: bool = false
for error in errors:
if error.line_number == i:
is_error = true
mark_line_as_error(i, is_error)
_on_code_edit_caret_changed()
get:
return errors
# The last selection (if there was one) so we can remember it for refocusing
var last_selected_text: String
var font_size: int:
set(value):
font_size = value
add_theme_font_size_override("font_size", font_size * theme_overrides.scale)
get:
return font_size
var WEIGHTED_RANDOM_PREFIX: RegEx = RegEx.create_from_string("^\\%[\\d.]+\\s")
func _ready() -> void:
# Add error gutter
add_gutter(0)
set_gutter_type(0, TextEdit.GUTTER_TYPE_ICON)
# Add comment delimiter
if not has_comment_delimiter("#"):
add_comment_delimiter("#", "", true)
syntax_highlighter = DialogueSyntaxHighlighter.new()
func _gui_input(event: InputEvent) -> void:
# Handle shortcuts that come from the editor
if event is InputEventKey and event.is_pressed():
var shortcut: String = Engine.get_meta("DialogueManagerPlugin").get_editor_shortcut(event)
match shortcut:
"toggle_comment":
toggle_comment()
get_viewport().set_input_as_handled()
"delete_line":
delete_current_line()
get_viewport().set_input_as_handled()
"move_up":
move_line(-1)
get_viewport().set_input_as_handled()
"move_down":
move_line(1)
get_viewport().set_input_as_handled()
"text_size_increase":
self.font_size += 1
get_viewport().set_input_as_handled()
"text_size_decrease":
self.font_size -= 1
get_viewport().set_input_as_handled()
"text_size_reset":
self.font_size = theme_overrides.font_size
get_viewport().set_input_as_handled()
elif event is InputEventMouse:
match event.as_text():
"Ctrl+Mouse Wheel Up", "Command+Mouse Wheel Up":
self.font_size += 1
get_viewport().set_input_as_handled()
"Ctrl+Mouse Wheel Down", "Command+Mouse Wheel Down":
self.font_size -= 1
get_viewport().set_input_as_handled()
func _can_drop_data(at_position: Vector2, data) -> bool:
if typeof(data) != TYPE_DICTIONARY: return false
if data.type != "files": return false
var files: PackedStringArray = Array(data.files).filter(func(f): return f.get_extension() == "dialogue")
return files.size() > 0
func _drop_data(at_position: Vector2, data) -> void:
var replace_regex: RegEx = RegEx.create_from_string("[^a-zA-Z_0-9]+")
var files: PackedStringArray = Array(data.files).filter(func(f): return f.get_extension() == "dialogue")
for file in files:
# Don't import the file into itself
if file == main_view.current_file_path: continue
var path = file.replace("res://", "").replace(".dialogue", "")
# Find the first non-import line in the file to add our import
var lines = text.split("\n")
for i in range(0, lines.size()):
if not lines[i].begins_with("import "):
insert_line_at(i, "import \"%s\" as %s\n" % [file, replace_regex.sub(path, "_", true)])
set_caret_line(i)
break
func _request_code_completion(force: bool) -> void:
var cursor: Vector2 = get_cursor()
var current_line: String = get_line(cursor.y)
if ("=> " in current_line or "=>< " in current_line) and (cursor.x > current_line.find("=>")):
var prompt: String = current_line.split("=>")[1]
if prompt.begins_with("< "):
prompt = prompt.substr(2)
else:
prompt = prompt.substr(1)
if "=> " in current_line:
if matches_prompt(prompt, "end"):
add_code_completion_option(CodeEdit.KIND_CLASS, "END", "END".substr(prompt.length()), theme_overrides.text_color, get_theme_icon("Stop", "EditorIcons"))
if matches_prompt(prompt, "end!"):
add_code_completion_option(CodeEdit.KIND_CLASS, "END!", "END!".substr(prompt.length()), theme_overrides.text_color, get_theme_icon("Stop", "EditorIcons"))
# Get all titles, including those in imports
var parser: DialogueManagerParser = DialogueManagerParser.new()
parser.prepare(text, main_view.current_file_path, false)
for title in parser.titles:
if "/" in title:
var bits = title.split("/")
if matches_prompt(prompt, bits[0]) or matches_prompt(prompt, bits[1]):
add_code_completion_option(CodeEdit.KIND_CLASS, title, title.substr(prompt.length()), theme_overrides.text_color, get_theme_icon("CombineLines", "EditorIcons"))
elif matches_prompt(prompt, title):
add_code_completion_option(CodeEdit.KIND_CLASS, title, title.substr(prompt.length()), theme_overrides.text_color, get_theme_icon("ArrowRight", "EditorIcons"))
update_code_completion_options(true)
parser.free()
return
var name_so_far: String = WEIGHTED_RANDOM_PREFIX.sub(current_line.strip_edges(), "")
if name_so_far != "" and name_so_far[0].to_upper() == name_so_far[0]:
# Only show names starting with that character
var names: PackedStringArray = get_character_names(name_so_far)
if names.size() > 0:
for name in names:
add_code_completion_option(CodeEdit.KIND_CLASS, name + ": ", name.substr(name_so_far.length()) + ": ", theme_overrides.text_color, get_theme_icon("Sprite2D", "EditorIcons"))
update_code_completion_options(true)
else:
cancel_code_completion()
func _filter_code_completion_candidates(candidates: Array) -> Array:
# Not sure why but if this method isn't overridden then all completions are wrapped in quotes.
return candidates
func _confirm_code_completion(replace: bool) -> void:
var completion = get_code_completion_option(get_code_completion_selected_index())
begin_complex_operation()
# Delete any part of the text that we've already typed
for i in range(0, completion.display_text.length() - completion.insert_text.length()):
backspace()
# Insert the whole match
insert_text_at_caret(completion.display_text)
end_complex_operation()
# Close the autocomplete menu on the next tick
call_deferred("cancel_code_completion")
### Helpers
# Get the current caret as a Vector2
func get_cursor() -> Vector2:
return Vector2(get_caret_column(), get_caret_line())
# Set the caret from a Vector2
func set_cursor(from_cursor: Vector2) -> void:
set_caret_line(from_cursor.y)
set_caret_column(from_cursor.x)
# Check if a prompt is the start of a string without actually being that string
func matches_prompt(prompt: String, matcher: String) -> bool:
return prompt.length() < matcher.length() and matcher.to_lower().begins_with(prompt.to_lower())
## Get a list of titles from the current text
func get_titles() -> PackedStringArray:
var titles = PackedStringArray([])
var lines = text.split("\n")
for line in lines:
if line.begins_with("~ "):
titles.append(line.substr(2).strip_edges())
return titles
## Work out what the next title above the current line is
func check_active_title() -> void:
var line_number = get_caret_line()
var lines = text.split("\n")
# Look at each line above this one to find the next title line
for i in range(line_number, -1, -1):
if lines[i].begins_with("~ "):
active_title_change.emit(lines[i].replace("~ ", ""))
return
active_title_change.emit("")
# Move the caret line to match a given title
func go_to_title(title: String) -> void:
var lines = text.split("\n")
for i in range(0, lines.size()):
if lines[i].strip_edges() == "~ " + title:
set_caret_line(i)
center_viewport_to_caret()
func get_character_names(beginning_with: String) -> PackedStringArray:
var names: PackedStringArray = []
var lines = text.split("\n")
for line in lines:
if ": " in line:
var name: String = WEIGHTED_RANDOM_PREFIX.sub(line.split(": ")[0].strip_edges(), "")
if not name in names and matches_prompt(beginning_with, name):
names.append(name)
return names
# Mark a line as an error or not
func mark_line_as_error(line_number: int, is_error: bool) -> void:
if is_error:
set_line_background_color(line_number, theme_overrides.error_line_color)
set_line_gutter_icon(line_number, 0, get_theme_icon("StatusError", "EditorIcons"))
else:
set_line_background_color(line_number, theme_overrides.background_color)
set_line_gutter_icon(line_number, 0, null)
# Insert or wrap some bbcode at the caret/selection
func insert_bbcode(open_tag: String, close_tag: String = "") -> void:
if close_tag == "":
insert_text_at_caret(open_tag)
grab_focus()
else:
var selected_text = get_selected_text()
insert_text_at_caret("%s%s%s" % [open_tag, selected_text, close_tag])
grab_focus()
set_caret_column(get_caret_column() - close_tag.length())
# Insert text at current caret position
# Move Caret down 1 line if not => END
func insert_text_at_cursor(text: String) -> void:
if text != "=> END":
insert_text_at_caret(text+"\n")
set_caret_line(get_caret_line()+1)
else:
insert_text_at_caret(text)
grab_focus()
# Toggle the selected lines as comments
func toggle_comment() -> void:
begin_complex_operation()
var comment_delimiter: String = delimiter_comments[0]
var is_first_line: bool = true
var will_comment: bool = true
var selections: Array = []
var line_offsets: Dictionary = {}
for caret_index in range(0, get_caret_count()):
var from_line: int = get_caret_line(caret_index)
var from_column: int = get_caret_column(caret_index)
var to_line: int = get_caret_line(caret_index)
var to_column: int = get_caret_column(caret_index)
if has_selection(caret_index):
from_line = get_selection_from_line(caret_index)
to_line = get_selection_to_line(caret_index)
from_column = get_selection_from_column(caret_index)
to_column = get_selection_to_column(caret_index)
selections.append({
from_line = from_line,
from_column = from_column,
to_line = to_line,
to_column = to_column
})
for line_number in range(from_line, to_line + 1):
if line_offsets.has(line_number): continue
var line_text: String = get_line(line_number)
# The first line determines if we are commenting or uncommentingg
if is_first_line:
is_first_line = false
will_comment = not line_text.strip_edges().begins_with(comment_delimiter)
# Only comment/uncomment if the current line needs to
if will_comment:
set_line(line_number, comment_delimiter + line_text)
line_offsets[line_number] = 1
elif line_text.begins_with(comment_delimiter):
set_line(line_number, line_text.substr(comment_delimiter.length()))
line_offsets[line_number] = -1
else:
line_offsets[line_number] = 0
for caret_index in range(0, get_caret_count()):
var selection: Dictionary = selections[caret_index]
select(
selection.from_line,
selection.from_column + line_offsets[selection.from_line],
selection.to_line,
selection.to_column + line_offsets[selection.to_line],
caret_index
)
set_caret_column(selection.from_column + line_offsets[selection.from_line], false, caret_index)
end_complex_operation()
text_set.emit()
text_changed.emit()
# Remove the current line
func delete_current_line() -> void:
var cursor = get_cursor()
if get_line_count() == 1:
select_all()
elif cursor.y == 0:
select(0, 0, 1, 0)
else:
select(cursor.y - 1, get_line_width(cursor.y - 1), cursor.y, get_line_width(cursor.y))
delete_selection()
text_changed.emit()
# Move the selected lines up or down
func move_line(offset: int) -> void:
offset = clamp(offset, -1, 1)
var cursor = get_cursor()
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")
# Prevent the lines from being out of bounds
if from + offset < 0 or to + offset >= lines.size(): return
var target_from_index = from - 1 if offset == -1 else to + 1
var target_to_index = to if offset == -1 else from
var line_to_move = 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_cursor(cursor)
text_changed.emit()
### Signals
func _on_code_edit_symbol_validate(symbol: String) -> void:
if symbol.begins_with("res://") and symbol.ends_with(".dialogue"):
set_symbol_lookup_word_as_valid(true)
return
for title in get_titles():
if symbol == title:
set_symbol_lookup_word_as_valid(true)
return
set_symbol_lookup_word_as_valid(false)
func _on_code_edit_symbol_lookup(symbol: String, line: int, column: int) -> void:
if symbol.begins_with("res://") and symbol.ends_with(".dialogue"):
external_file_requested.emit(symbol, "")
else:
go_to_title(symbol)
func _on_code_edit_text_changed() -> void:
request_code_completion(true)
func _on_code_edit_text_set() -> void:
queue_redraw()
func _on_code_edit_caret_changed() -> void:
check_active_title()
last_selected_text = get_selected_text()
func _on_code_edit_gutter_clicked(line: int, gutter: int) -> void:
var line_errors = errors.filter(func(error): return error.line_number == line)
if line_errors.size() > 0:
error_clicked.emit(line)

View File

@@ -0,0 +1,56 @@
[gd_scene load_steps=4 format=3 uid="uid://civ6shmka5e8u"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/code_edit_syntax_highlighter.gd" id="1_58cfo"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/code_edit.gd" id="1_g324i"]
[sub_resource type="SyntaxHighlighter" id="SyntaxHighlighter_cobxx"]
script = ExtResource("1_58cfo")
[node name="CodeEdit" type="CodeEdit"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
text = "~ title_thing
if this = \"that\" or 'this'
Nathan: Something
- Then [if test.thing() == 2.0] => somewhere
- Other => END!
~ somewhere
set has_something = true
=> END"
highlight_all_occurrences = true
highlight_current_line = true
draw_tabs = true
syntax_highlighter = SubResource("SyntaxHighlighter_cobxx")
scroll_past_end_of_file = true
minimap_draw = true
symbol_lookup_on_click = true
line_folding = true
gutters_draw_line_numbers = true
gutters_draw_fold_gutter = true
delimiter_strings = Array[String](["\" \""])
delimiter_comments = Array[String](["#"])
code_completion_enabled = true
code_completion_prefixes = Array[String]([">", "<"])
indent_automatic = true
auto_brace_completion_enabled = true
auto_brace_completion_highlight_matching = true
auto_brace_completion_pairs = {
"\"": "\"",
"(": ")",
"[": "]",
"{": "}"
}
script = ExtResource("1_g324i")
[connection signal="caret_changed" from="." to="." method="_on_code_edit_caret_changed"]
[connection signal="gutter_clicked" from="." to="." method="_on_code_edit_gutter_clicked"]
[connection signal="symbol_lookup" from="." to="." method="_on_code_edit_symbol_lookup"]
[connection signal="symbol_validate" from="." to="." method="_on_code_edit_symbol_validate"]
[connection signal="text_changed" from="." to="." method="_on_code_edit_text_changed"]
[connection signal="text_set" from="." to="." method="_on_code_edit_text_set"]

View File

@@ -0,0 +1,385 @@
@tool
extends SyntaxHighlighter
const DialogueManagerParser = preload("./parser.gd")
enum ExpressionType {DO, SET, IF}
var dialogue_manager_parser: DialogueManagerParser = DialogueManagerParser.new()
var regex_titles: RegEx = RegEx.create_from_string("^\\s*(?<title>~\\s+[^\\!\\@\\#\\$\\%\\^\\&\\*\\(\\)\\-\\=\\+\\{\\}\\[\\]\\;\\:\\\"\\'\\,\\.\\<\\>\\?\\/\\s]+)")
var regex_comments: RegEx = RegEx.create_from_string("(?:(?>\"(?:\\\\\"|[^\"\\n])*\")[^\"\\n]*?\\s*(?<comment>#[^\\n]*)$|^[^\"#\\n]*?\\s*(?<comment2>#[^\\n]*))")
var regex_mutation: RegEx = RegEx.create_from_string("^\\s*(do|do!|set) (?<mutation>.*)")
var regex_condition: RegEx = RegEx.create_from_string("^\\s*(if|elif|while|else if) (?<condition>.*)")
var regex_wcondition: RegEx = RegEx.create_from_string("\\[if (?<condition>((?:[^\\[\\]]*)|(?:\\[(?1)\\]))*?)\\]")
var regex_wendif: RegEx = RegEx.create_from_string("\\[(\\/if|else)\\]")
var regex_rgroup: RegEx = RegEx.create_from_string("\\[\\[(?<options>.*?)\\]\\]")
var regex_endconditions: RegEx = RegEx.create_from_string("^\\s*(endif|else):?\\s*$")
var regex_tags: RegEx = RegEx.create_from_string("\\[(?<tag>(?!(?:ID:.*)|if)[a-zA-Z_][a-zA-Z0-9_]*!?)(?:[= ](?<val>[^\\[\\]]+))?\\](?:(?<text>(?!\\[\\/\\k<tag>\\]).*?)?(?<end>\\[\\/\\k<tag>\\]))?")
var regex_dialogue: RegEx = RegEx.create_from_string("^\\s*(?:(?<random>\\%[\\d.]* )|(?<response>- ))?(?:(?<character>[^#:]*): )?(?<dialogue>.*)$")
var regex_goto: RegEx = RegEx.create_from_string("=><? (?:(?<file>[^\\/]+)\\/)?(?<title>[^\\/]*)")
var regex_string: RegEx = RegEx.create_from_string("^&?(?<delimiter>[\"'])(?<content>(?:\\\\{2})*|(?:.*?[^\\\\](?:\\\\{2})*))\\1$")
var regex_escape: RegEx = RegEx.create_from_string("\\\\.")
var regex_number: RegEx = RegEx.create_from_string("^-?(?:(?:0x(?:[0-9A-Fa-f]{2})+)|(?:0b[01]+)|(?:\\d+(?:(?:[\\.]\\d*)?(?:e\\d+)?)|(?:_\\d+)+)?)$")
var regex_array: RegEx = RegEx.create_from_string("\\[((?>[^\\[\\]]+|(?R))*)\\]")
var regex_dict: RegEx = RegEx.create_from_string("^\\{((?>[^\\{\\}]+|(?R))*)\\}$")
var regex_kvdict: RegEx = RegEx.create_from_string("^\\s*(?<left>.*?)\\s*(?<colon>:|=)\\s*(?<right>[^\\/]+)$")
var regex_commas: RegEx = RegEx.create_from_string("([^,]+)(?:\\s*,\\s*)?")
var regex_assignment: RegEx = RegEx.create_from_string("^\\s*(?<var>[a-zA-Z_][a-zA-Z_0-9]*)(?:(?<attr>(?:\\.[a-zA-Z_][a-zA-Z_0-9]*)+)|(?:\\[(?<key>[^\\]]+)\\]))?\\s*(?<op>(?:\\/|\\*|-|\\+)?=)\\s*(?<val>.*)$")
var regex_varname: RegEx = RegEx.create_from_string("^\\s*(?!true|false|and|or|&&|\\|\\|not|in|null)(?<var>[a-zA-Z_][a-zA-Z_0-9]*)(?:(?<attr>(?:\\.[a-zA-Z_][a-zA-Z_0-9]*)+)|(?:\\[(?<key>[^\\]]+)\\]))?\\s*$")
var regex_keyword: RegEx = RegEx.create_from_string("^\\s*(true|false|null)\\s*$")
var regex_function: RegEx = RegEx.create_from_string("^\\s*([a-zA-Z_][a-zA-Z_0-9]*\\s*)\\(")
var regex_comparison: RegEx = RegEx.create_from_string("^(?<left>.*?)\\s*(?<op>==|>=|<=|<|>|!=)\\s*(?<right>.*)$")
var regex_blogical: RegEx = RegEx.create_from_string("^(?<left>.*?)\\s+(?<op>and|or|in|&&|\\|\\|)\\s+(?<right>.*)$")
var regex_ulogical: RegEx = RegEx.create_from_string("^\\s*(?<op>not)\\s+(?<right>.*)$")
var regex_paren: RegEx = RegEx.create_from_string("\\((?<paren>((?:[^\\(\\)]*)|(?:\\((?1)\\)))*?)\\)")
var cache: Dictionary = {}
func _notification(what: int) -> void:
if what == NOTIFICATION_PREDELETE:
dialogue_manager_parser.free()
func _clear_highlighting_cache() -> void:
cache = {}
## Returns the syntax coloring for a dialogue file line
func _get_line_syntax_highlighting(line: int) -> Dictionary:
var colors: Dictionary = {}
var text_edit: TextEdit = get_text_edit()
var text: String = text_edit.get_line(line)
# Prevent an error from popping up while developing
if not is_instance_valid(text_edit) or text_edit.theme_overrides.is_empty():
return colors
# Disable this, as well as the line at the bottom of this function to remove the cache.
if text in cache:
return cache[text]
# Comments have to be removed to make the remaining processing easier.
# Count both end-of-line and single-line comments
# Comments are not allowed within dialogue lines or response lines, so we ask the parser what it thinks the current line is
if not (dialogue_manager_parser.is_dialogue_line(text) or dialogue_manager_parser.is_response_line(text)) or dialogue_manager_parser.is_line_empty(text) or dialogue_manager_parser.is_import_line(text):
var comment_matches: Array[RegExMatch] = regex_comments.search_all(text)
for comment_match in comment_matches:
for i in ["comment", "comment2"]:
if i in comment_match.names:
colors[comment_match.get_start(i)] = {"color": text_edit.theme_overrides.comments_color}
text = text.substr(0, comment_match.get_start(i))
# Dialogues
var dialogue_matches: Array[RegExMatch] = regex_dialogue.search_all(text)
for dialogue_match in dialogue_matches:
if "random" in dialogue_match.names:
colors[dialogue_match.get_start("random")] = {"color": text_edit.theme_overrides.symbols_color}
colors[dialogue_match.get_end("random")] = {"color": text_edit.theme_overrides.text_color}
if "response" in dialogue_match.names:
colors[dialogue_match.get_start("response")] = {"color": text_edit.theme_overrides.symbols_color}
colors[dialogue_match.get_end("response")] = {"color": text_edit.theme_overrides.text_color}
if "character" in dialogue_match.names:
colors[dialogue_match.get_start("character")] = {"color": text_edit.theme_overrides.members_color}
colors[dialogue_match.get_end("character")] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_dialogue_syntax_highlighting(dialogue_match.get_start("dialogue"), dialogue_match.get_string("dialogue")), true)
# Title lines
if dialogue_manager_parser.is_title_line(text):
var title_matches: Array[RegExMatch] = regex_titles.search_all(text)
for title_match in title_matches:
colors[title_match.get_start("title")] = {"color": text_edit.theme_overrides.titles_color}
# Import lines
var import_matches: Array[RegExMatch] = dialogue_manager_parser.IMPORT_REGEX.search_all(text)
for import_match in import_matches:
colors[import_match.get_start(0)] = {"color": text_edit.theme_overrides.conditions_color}
colors[import_match.get_start("path") - 1] = {"color": text_edit.theme_overrides.strings_color}
colors[import_match.get_end("path") + 1] = {"color": text_edit.theme_overrides.conditions_color}
colors[import_match.get_start("prefix")] = {"color": text_edit.theme_overrides.members_color}
colors[import_match.get_end("prefix")] = {"color": text_edit.theme_overrides.conditions_color}
# Using clauses
var using_matches: Array[RegExMatch] = dialogue_manager_parser.USING_REGEX.search_all(text)
for using_match in using_matches:
colors[using_match.get_start(0)] = {"color": text_edit.theme_overrides.conditions_color}
colors[using_match.get_start("state") - 1] = {"color": text_edit.theme_overrides.text_color}
# Condition keywords and expressions
var condition_matches: Array[RegExMatch] = regex_condition.search_all(text)
for condition_match in condition_matches:
colors[condition_match.get_start(0)] = {"color": text_edit.theme_overrides.conditions_color}
colors[condition_match.get_end(1)] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_expression_syntax_highlighting(condition_match.get_start("condition"), ExpressionType.IF, condition_match.get_string("condition")), true)
# endif/else
var endcondition_matches: Array[RegExMatch] = regex_endconditions.search_all(text)
for endcondition_match in endcondition_matches:
colors[endcondition_match.get_start(1)] = {"color": text_edit.theme_overrides.conditions_color}
colors[endcondition_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color}
# Mutations
var mutation_matches: Array[RegExMatch] = regex_mutation.search_all(text)
for mutation_match in mutation_matches:
colors[mutation_match.get_start(0)] = {"color": text_edit.theme_overrides.mutations_color}
colors.merge(_get_expression_syntax_highlighting(mutation_match.get_start("mutation"), ExpressionType.DO if mutation_match.strings[1] == "do" else ExpressionType.SET, mutation_match.get_string("mutation")), true)
# Order the dictionary keys to prevent CodeEdit from having issues
var new_colors: Dictionary = {}
var ordered_keys: Array = colors.keys()
ordered_keys.sort()
for index in ordered_keys:
new_colors[index] = colors[index]
cache[text] = new_colors
return new_colors
## Return the syntax highlighting for a dialogue line
func _get_dialogue_syntax_highlighting(start_index: int, text: String) -> Dictionary:
var text_edit: TextEdit = get_text_edit()
var colors: Dictionary = {}
# #tag style tags
var hashtag_matches: Array[RegExMatch] = dialogue_manager_parser.TAGS_REGEX.search_all(text)
for hashtag_match in hashtag_matches:
colors[start_index + hashtag_match.get_start(0)] = { "color": text_edit.theme_overrides.comments_color }
colors[start_index + hashtag_match.get_end(0)] = { "color": text_edit.theme_overrides.text_color }
# bbcode-like global tags
var tag_matches: Array[RegExMatch] = regex_tags.search_all(text)
for tag_match in tag_matches:
colors[start_index + tag_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
if "val" in tag_match.names:
colors.merge(_get_literal_syntax_highlighting(start_index + tag_match.get_start("val"), tag_match.get_string("val")), true)
colors[start_index + tag_match.get_end("val")] = {"color": text_edit.theme_overrides.symbols_color}
# Show the text color straight in the editor for better ease-of-use
if tag_match.get_string("tag") == "color":
colors[start_index + tag_match.get_start("val")] = {"color": Color.from_string(tag_match.get_string("val"), text_edit.theme_overrides.text_color)}
if "text" in tag_match.names:
colors[start_index + tag_match.get_start("text")] = {"color": text_edit.theme_overrides.text_color}
# Text can still contain tags if several effects are applied ([center][b]Something[/b][/center], so recursing
colors.merge(_get_dialogue_syntax_highlighting(start_index + tag_match.get_start("text"), tag_match.get_string("text")), true)
colors[start_index + tag_match.get_end("text")] = {"color": text_edit.theme_overrides.symbols_color}
if "end" in tag_match.names:
colors[start_index + tag_match.get_start("end")] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + tag_match.get_end("end")] = {"color": text_edit.theme_overrides.text_color}
colors[start_index + tag_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
# ID tag
var translation_matches: Array[RegExMatch] = dialogue_manager_parser.TRANSLATION_REGEX.search_all(text)
for translation_match in translation_matches:
colors[start_index + translation_match.get_start(0)] = {"color": text_edit.theme_overrides.comments_color}
colors[start_index + translation_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
# Replacements
var replacement_matches: Array[RegExMatch] = dialogue_manager_parser.REPLACEMENTS_REGEX.search_all(text)
for replacement_match in replacement_matches:
colors[start_index + replacement_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + replacement_match.get_start(1)] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_literal_syntax_highlighting(start_index + replacement_match.get_start(1), replacement_match.strings[1]), true)
colors[start_index + replacement_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + replacement_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
# Jump at the end of a response
var goto_matches: Array[RegExMatch] = regex_goto.search_all(text)
for goto_match in goto_matches:
colors[start_index + goto_match.get_start(0)] = {"color": text_edit.theme_overrides.jumps_color}
if "file" in goto_match.names:
colors[start_index + goto_match.get_start("file")] = {"color": text_edit.theme_overrides.jumps_color}
colors[start_index + goto_match.get_end("file")] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + goto_match.get_start("title")] = {"color": text_edit.theme_overrides.jumps_color}
colors[start_index + goto_match.get_end("title")] = {"color": text_edit.theme_overrides.jumps_color}
colors[start_index + goto_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
# Wrapped condition
var wcondition_matches: Array[RegExMatch] = regex_wcondition.search_all(text)
for wcondition_match in wcondition_matches:
colors[start_index + wcondition_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + wcondition_match.get_start(0) + 1] = {"color": text_edit.theme_overrides.conditions_color}
colors[start_index + wcondition_match.get_start(0) + 3] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_literal_syntax_highlighting(start_index + wcondition_match.get_start("condition"), wcondition_match.get_string("condition")), true)
colors[start_index + wcondition_match.get_end("condition")] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + wcondition_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
# [/if] tag for color matching with the opening tag
var wendif_matches: Array[RegExMatch] = regex_wendif.search_all(text)
for wendif_match in wendif_matches:
colors[start_index + wendif_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + wendif_match.get_start(1)] = {"color": text_edit.theme_overrides.conditions_color}
colors[start_index + wendif_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + wendif_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
# Random groups
var rgroup_matches: Array[RegExMatch] = regex_rgroup.search_all(text)
for rgroup_match in rgroup_matches:
colors[start_index + rgroup_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + rgroup_match.get_start("options")] = {"color": text_edit.theme_overrides.text_color}
var separator_matches: Array[RegExMatch] = RegEx.create_from_string("\\|").search_all(rgroup_match.get_string("options"))
for separator_match in separator_matches:
colors[start_index + rgroup_match.get_start("options") + separator_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + rgroup_match.get_start("options") + separator_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
colors[start_index + rgroup_match.get_end("options")] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + rgroup_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
return colors
## Returns the syntax highlighting for an expression (mutation set/do, or condition)
func _get_expression_syntax_highlighting(start_index: int, type: ExpressionType, text: String) -> Dictionary:
var text_edit: TextEdit = get_text_edit()
var colors: Dictionary = {}
if type == ExpressionType.SET:
var assignment_matches: Array[RegExMatch] = regex_assignment.search_all(text)
for assignment_match in assignment_matches:
colors[start_index + assignment_match.get_start("var")] = {"color": text_edit.theme_overrides.text_color}
if "attr" in assignment_match.names:
colors[start_index + assignment_match.get_start("attr")] = {"color": text_edit.theme_overrides.members_color}
colors[start_index + assignment_match.get_end("attr")] = {"color": text_edit.theme_overrides.text_color}
if "key" in assignment_match.names:
# Braces are outside of the key, so coloring them symbols_color
colors[start_index + assignment_match.get_start("key") - 1] = {"color": text_edit.theme_overrides.symbols_color}
colors.merge(_get_literal_syntax_highlighting(start_index + assignment_match.get_start("key"), assignment_match.get_string("key")), true)
colors[start_index + assignment_match.get_end("key")] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + assignment_match.get_end("key") + 1] = {"color": text_edit.theme_overrides.text_color}
colors[start_index + assignment_match.get_start("op")] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + assignment_match.get_end("op")] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_literal_syntax_highlighting(start_index + assignment_match.get_start("val"), assignment_match.get_string("val")), true)
else:
colors.merge(_get_literal_syntax_highlighting(start_index, text), true)
return colors
## Return the syntax highlighting for a literal
## For this purpose, "literal" refers to a regular code line that could be used to get a value out of:
## - function calls
## - real literals (bool, string, int, float, etc.)
## - logical operators (>, <, >=, or, and, not, etc.)
func _get_literal_syntax_highlighting(start_index: int, text: String) -> Dictionary:
var text_edit: TextEdit = get_text_edit()
var colors: Dictionary = {}
# Remove spaces at start/end of the literal
var text_length: int = text.length()
text = text.lstrip(" ")
start_index += text_length - text.length()
text = text.rstrip(" ")
# Parenthesis expression.
var paren_matches: Array[RegExMatch] = regex_paren.search_all(text)
for paren_match in paren_matches:
colors[start_index + paren_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + paren_match.get_start(0) + 1] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_literal_syntax_highlighting(start_index + paren_match.get_start("paren"), paren_match.get_string("paren")), true)
colors[start_index + paren_match.get_end(0) - 1] = {"color": text_edit.theme_overrides.symbols_color}
# Strings
var string_matches: Array[RegExMatch] = regex_string.search_all(text)
for string_match in string_matches:
colors[start_index + string_match.get_start(0)] = {"color": text_edit.theme_overrides.strings_color}
if "content" in string_match.names:
var escape_matches: Array[RegExMatch] = regex_escape.search_all(string_match.get_string("content"))
for escape_match in escape_matches:
colors[start_index + string_match.get_start("content") + escape_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + string_match.get_start("content") + escape_match.get_end(0)] = {"color": text_edit.theme_overrides.strings_color}
# Numbers
var number_matches: Array[RegExMatch] = regex_number.search_all(text)
for number_match in number_matches:
colors[start_index + number_match.get_start(0)] = {"color": text_edit.theme_overrides.numbers_color}
# Arrays
var array_matches: Array[RegExMatch] = regex_array.search_all(text)
for array_match in array_matches:
colors[start_index + array_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors.merge(_get_list_syntax_highlighting(start_index + array_match.get_start(1), array_match.strings[1]), true)
colors[start_index + array_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color}
# Dictionaries
var dict_matches: Array[RegExMatch] = regex_dict.search_all(text)
for dict_match in dict_matches:
colors[start_index + dict_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors.merge(_get_list_syntax_highlighting(start_index + dict_match.get_start(1), dict_match.strings[1]), true)
colors[start_index + dict_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color}
# Dictionary key: value pairs
var kvdict_matches: Array[RegExMatch] = regex_kvdict.search_all(text)
for kvdict_match in kvdict_matches:
colors.merge(_get_literal_syntax_highlighting(start_index + kvdict_match.get_start("left"), kvdict_match.get_string("left")), true)
colors[start_index + kvdict_match.get_start("colon")] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + kvdict_match.get_end("colon")] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_literal_syntax_highlighting(start_index + kvdict_match.get_start("right"), kvdict_match.get_string("right")), true)
# Booleans
var bool_matches: Array[RegExMatch] = regex_keyword.search_all(text)
for bool_match in bool_matches:
colors[start_index + bool_match.get_start(0)] = {"color": text_edit.theme_overrides.conditions_color}
# Functions
var function_matches: Array[RegExMatch] = regex_function.search_all(text)
for function_match in function_matches:
var last_brace_index: int = text.rfind(")")
colors[start_index + function_match.get_start(1)] = {"color": text_edit.theme_overrides.mutations_color}
colors[start_index + function_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color}
colors.merge(_get_list_syntax_highlighting(start_index + function_match.get_end(0), text.substr(function_match.get_end(0), last_brace_index - function_match.get_end(0))), true)
colors[start_index + last_brace_index] = {"color": text_edit.theme_overrides.symbols_color}
# Variables
var varname_matches: Array[RegExMatch] = regex_varname.search_all(text)
for varname_match in varname_matches:
colors[start_index + varname_match.get_start("var")] = {"color": text_edit.theme_overrides.text_color}
if "attr" in varname_match.names:
colors[start_index + varname_match.get_start("attr")] = {"color": text_edit.theme_overrides.members_color}
colors[start_index + varname_match.get_end("attr")] = {"color": text_edit.theme_overrides.text_color}
if "key" in varname_match.names:
# Braces are outside of the key, so coloring them symbols_color
colors[start_index + varname_match.get_start("key") - 1] = {"color": text_edit.theme_overrides.symbols_color}
colors.merge(_get_literal_syntax_highlighting(start_index + varname_match.get_start("key"), varname_match.get_string("key")), true)
colors[start_index + varname_match.get_end("key")] = {"color": text_edit.theme_overrides.symbols_color}
# Comparison operators
var comparison_matches: Array[RegExMatch] = regex_comparison.search_all(text)
for comparison_match in comparison_matches:
colors.merge(_get_literal_syntax_highlighting(start_index + comparison_match.get_start("left"), comparison_match.get_string("left")), true)
colors[start_index + comparison_match.get_start("op")] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + comparison_match.get_end("op")] = {"color": text_edit.theme_overrides.text_color}
var right = comparison_match.get_string("right")
if right.ends_with(":"):
right = right.substr(0, right.length() - 1)
colors.merge(_get_literal_syntax_highlighting(start_index + comparison_match.get_start("right"), right), true)
colors[start_index + comparison_match.get_start("right") + right.length()] = { "color": text_edit.theme_overrides.symbols_color }
# Logical binary operators
var blogical_matches: Array[RegExMatch] = regex_blogical.search_all(text)
for blogical_match in blogical_matches:
colors.merge(_get_literal_syntax_highlighting(start_index + blogical_match.get_start("left"), blogical_match.get_string("left")), true)
colors[start_index + blogical_match.get_start("op")] = {"color": text_edit.theme_overrides.conditions_color}
colors[start_index + blogical_match.get_end("op")] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_literal_syntax_highlighting(start_index + blogical_match.get_start("right"), blogical_match.get_string("right")), true)
# Logical unary operators
var ulogical_matches: Array[RegExMatch] = regex_ulogical.search_all(text)
for ulogical_match in ulogical_matches:
colors[start_index + ulogical_match.get_start("op")] = {"color": text_edit.theme_overrides.conditions_color}
colors[start_index + ulogical_match.get_end("op")] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_literal_syntax_highlighting(start_index + ulogical_match.get_start("right"), ulogical_match.get_string("right")), true)
return colors
## Returns the syntax coloring for a list of literals separated by commas
func _get_list_syntax_highlighting(start_index: int, text: String) -> Dictionary:
var text_edit: TextEdit = get_text_edit()
var colors: Dictionary = {}
# Comma-separated list of literals (for arrays and function arguments)
var element_matches: Array[RegExMatch] = regex_commas.search_all(text)
for element_match in element_matches:
colors.merge(_get_literal_syntax_highlighting(start_index + element_match.get_start(1), element_match.strings[1]), true)
return colors

View File

@@ -0,0 +1,168 @@
extends Node
const DialogueConstants = preload("../constants.gd")
const DialogueSettings = preload("../settings.gd")
const DialogueManagerParseResult = preload("./parse_result.gd")
signal file_content_changed(path: String, new_content: String)
# Keep track of errors and dependencies
# {
# <dialogue file path> = {
# path = <dialogue file path>,
# dependencies = [<dialogue file path>, <dialogue file path>],
# errors = [<error>, <error>]
# }
# }
var _cache: Dictionary = {}
var _update_dependency_timer: Timer = Timer.new()
var _update_dependency_paths: PackedStringArray = []
func _ready() -> void:
add_child(_update_dependency_timer)
_update_dependency_timer.timeout.connect(_on_update_dependency_timeout)
_build_cache()
func reimport_files(files: PackedStringArray = []) -> void:
if files.is_empty(): files = get_files()
var file_system: EditorFileSystem = Engine.get_meta("DialogueManagerPlugin") \
.get_editor_interface() \
.get_resource_filesystem()
# NOTE: Godot 4.2rc1 has an issue with reimporting more than one
# file at a time so we do them one by one
for file in files:
file_system.reimport_files([file])
await get_tree().create_timer(0.2)
## Add a dialogue file to the cache.
func add_file(path: String, parse_results: DialogueManagerParseResult = null) -> void:
_cache[path] = {
path = path,
dependencies = [],
errors = []
}
if parse_results != null:
_cache[path].dependencies = Array(parse_results.imported_paths).filter(func(d): return d != path)
_cache[path].parsed_at = Time.get_ticks_msec()
# If this is a fresh cache entry, check for dependencies
if parse_results == null and not _update_dependency_paths.has(path):
queue_updating_dependencies(path)
## Get the file paths in the cache
func get_files() -> PackedStringArray:
return _cache.keys()
## Check if a file is known to the cache
func has_file(path: String) -> bool:
return _cache.has(path)
## Remember any errors in a dialogue file
func add_errors_to_file(path: String, errors: Array[Dictionary]) -> void:
if _cache.has(path):
_cache[path].errors = errors
else:
_cache[path] = {
path = path,
resource_path = "",
dependencies = [],
errors = errors
}
## Get a list of files that have errors
func get_files_with_errors() -> Array[Dictionary]:
var files_with_errors: Array[Dictionary] = []
for dialogue_file in _cache.values():
if dialogue_file and dialogue_file.errors.size() > 0:
files_with_errors.append(dialogue_file)
return files_with_errors
## Queue a file to have its dependencies checked
func queue_updating_dependencies(of_path: String) -> void:
_update_dependency_timer.stop()
if not _update_dependency_paths.has(of_path):
_update_dependency_paths.append(of_path)
_update_dependency_timer.start(0.5)
## Update any references to a file path that has moved
func move_file_path(from_path: String, to_path: String) -> void:
if not _cache.has(from_path): return
if to_path != "":
_cache[to_path] = _cache[from_path].duplicate()
_cache.erase(from_path)
## Get every dialogue file that imports on a file of a given path
func get_files_with_dependency(imported_path: String) -> Array:
return _cache.values().filter(func(d): return d.dependencies.has(imported_path))
## Get any paths that are dependent on a given path
func get_dependent_paths_for_reimport(on_path: String) -> PackedStringArray:
return get_files_with_dependency(on_path) \
.filter(func(d): return Time.get_ticks_msec() - d.get("parsed_at", 0) > 3000) \
.map(func(d): return d.path)
# Build the initial cache for dialogue files
func _build_cache() -> void:
var current_files: PackedStringArray = _get_dialogue_files_in_filesystem()
for file in current_files:
add_file(file)
# Recursively find any dialogue files in a directory
func _get_dialogue_files_in_filesystem(path: String = "res://") -> PackedStringArray:
var files: PackedStringArray = []
if DirAccess.dir_exists_absolute(path):
var dir = DirAccess.open(path)
dir.list_dir_begin()
var file_name = dir.get_next()
while file_name != "":
var file_path: String = (path + "/" + file_name).simplify_path()
if dir.current_is_dir():
if not file_name in [".godot", ".tmp"]:
files.append_array(_get_dialogue_files_in_filesystem(file_path))
elif file_name.get_extension() == "dialogue":
files.append(file_path)
file_name = dir.get_next()
return files
### Signals
func _on_update_dependency_timeout() -> void:
_update_dependency_timer.stop()
var import_regex: RegEx = RegEx.create_from_string("import \"(?<path>.*?)\"")
var file: FileAccess
var found_imports: Array[RegExMatch]
for path in _update_dependency_paths:
# Open the file and check for any "import" lines
file = FileAccess.open(path, FileAccess.READ)
found_imports = import_regex.search_all(file.get_as_text())
var dependencies: PackedStringArray = []
for found in found_imports:
dependencies.append(found.strings[found.names.path])
_cache[path].dependencies = dependencies
_update_dependency_paths.clear()

View File

@@ -0,0 +1,84 @@
@tool
extends Control
signal failed()
signal updated(updated_to_version: String)
const DialogueConstants = preload("../constants.gd")
const TEMP_FILE_NAME = "user://temp.zip"
@onready var logo: TextureRect = %Logo
@onready var label: Label = $VBox/Label
@onready var http_request: HTTPRequest = $HTTPRequest
@onready var download_button: Button = %DownloadButton
var next_version_release: Dictionary:
set(value):
next_version_release = value
label.text = DialogueConstants.translate(&"update.is_available_for_download") % value.tag_name.substr(1)
get:
return next_version_release
func _ready() -> void:
$VBox/Center/DownloadButton.text = DialogueConstants.translate(&"update.download_update")
$VBox/Center2/NotesButton.text = DialogueConstants.translate(&"update.release_notes")
### Signals
func _on_download_button_pressed() -> void:
# Safeguard the actual dialogue manager repo from accidentally updating itself
if FileAccess.file_exists("res://examples/test_scenes/test_scene.gd"):
prints("You can't update the addon from within itself.")
failed.emit()
return
http_request.request(next_version_release.zipball_url)
download_button.disabled = true
download_button.text = DialogueConstants.translate(&"update.downloading")
func _on_http_request_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
if result != HTTPRequest.RESULT_SUCCESS:
failed.emit()
return
# Save the downloaded zip
var zip_file: FileAccess = FileAccess.open(TEMP_FILE_NAME, FileAccess.WRITE)
zip_file.store_buffer(body)
zip_file.close()
OS.move_to_trash(ProjectSettings.globalize_path("res://addons/dialogue_manager"))
var zip_reader: ZIPReader = ZIPReader.new()
zip_reader.open(TEMP_FILE_NAME)
var files: PackedStringArray = zip_reader.get_files()
var base_path = files[1]
# Remove archive folder
files.remove_at(0)
# Remove assets folder
files.remove_at(0)
for path in files:
var new_file_path: String = path.replace(base_path, "")
if path.ends_with("/"):
DirAccess.make_dir_recursive_absolute("res://addons/%s" % new_file_path)
else:
var file: FileAccess = FileAccess.open("res://addons/%s" % new_file_path, FileAccess.WRITE)
file.store_buffer(zip_reader.read_file(path))
zip_reader.close()
DirAccess.remove_absolute(TEMP_FILE_NAME)
updated.emit(next_version_release.tag_name.substr(1))
func _on_notes_button_pressed() -> void:
OS.shell_open(next_version_release.html_url)

View File

@@ -0,0 +1,60 @@
[gd_scene load_steps=3 format=3 uid="uid://qdxrxv3c3hxk"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/download_update_panel.gd" id="1_4tm1k"]
[ext_resource type="Texture2D" uid="uid://d3baj6rygkb3f" path="res://addons/dialogue_manager/assets/update.svg" id="2_4o2m6"]
[node name="DownloadUpdatePanel" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_4tm1k")
[node name="HTTPRequest" type="HTTPRequest" parent="."]
[node name="VBox" type="VBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = -1.0
offset_top = 9.0
offset_right = -1.0
offset_bottom = 9.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/separation = 10
[node name="Logo" type="TextureRect" parent="VBox"]
unique_name_in_owner = true
clip_contents = true
custom_minimum_size = Vector2(300, 80)
layout_mode = 2
texture = ExtResource("2_4o2m6")
stretch_mode = 5
[node name="Label" type="Label" parent="VBox"]
layout_mode = 2
text = "v1.2.3 is available for download."
horizontal_alignment = 1
[node name="Center" type="CenterContainer" parent="VBox"]
layout_mode = 2
[node name="DownloadButton" type="Button" parent="VBox/Center"]
unique_name_in_owner = true
layout_mode = 2
text = "Download update"
[node name="Center2" type="CenterContainer" parent="VBox"]
layout_mode = 2
[node name="NotesButton" type="LinkButton" parent="VBox/Center2"]
layout_mode = 2
text = "Read release notes"
[connection signal="request_completed" from="HTTPRequest" to="." method="_on_http_request_request_completed"]
[connection signal="pressed" from="VBox/Center/DownloadButton" to="." method="_on_download_button_pressed"]
[connection signal="pressed" from="VBox/Center2/NotesButton" to="." method="_on_notes_button_pressed"]

View File

@@ -0,0 +1,48 @@
@tool
extends EditorProperty
const DialoguePropertyEditorControl = preload("./editor_property_control.tscn")
var editor_plugin: EditorPlugin
var control = DialoguePropertyEditorControl.instantiate()
var current_value: Resource
var is_updating: bool = false
func _init() -> void:
add_child(control)
control.resource = current_value
control.pressed.connect(_on_button_pressed)
control.resource_changed.connect(_on_resource_changed)
func _update_property() -> void:
var next_value = get_edited_object()[get_edited_property()]
# The resource might have been deleted elsewhere so check that it's not in a weird state
if is_instance_valid(next_value) and not next_value.resource_path.ends_with(".dialogue"):
emit_changed(get_edited_property(), null)
return
if next_value == current_value: return
is_updating = true
current_value = next_value
control.resource = current_value
is_updating = false
### Signals
func _on_button_pressed() -> void:
editor_plugin.edit(current_value)
func _on_resource_changed(next_resource: Resource) -> void:
emit_changed(get_edited_property(), next_resource)

View File

@@ -0,0 +1,147 @@
@tool
extends HBoxContainer
signal pressed()
signal resource_changed(next_resource: Resource)
const ITEM_NEW = 100
const ITEM_QUICK_LOAD = 200
const ITEM_LOAD = 201
const ITEM_EDIT = 300
const ITEM_CLEAR = 301
const ITEM_FILESYSTEM = 400
@onready var button: Button = $ResourceButton
@onready var menu_button: Button = $MenuButton
@onready var menu: PopupMenu = $Menu
@onready var quick_open_dialog: ConfirmationDialog = $QuickOpenDialog
@onready var files_list = $QuickOpenDialog/FilesList
@onready var new_dialog: FileDialog = $NewDialog
@onready var open_dialog: FileDialog = $OpenDialog
var editor_plugin: EditorPlugin
var resource: Resource:
set(next_resource):
resource = next_resource
if button:
button.resource = resource
get:
return resource
var is_waiting_for_file: bool = false
var quick_selected_file: String = ""
func _ready() -> void:
menu_button.icon = get_theme_icon("GuiDropdown", "EditorIcons")
editor_plugin = Engine.get_meta("DialogueManagerPlugin")
func build_menu() -> void:
menu.clear()
menu.add_icon_item(editor_plugin._get_plugin_icon(), "New Dialogue", ITEM_NEW)
menu.add_separator()
menu.add_icon_item(get_theme_icon("Load", "EditorIcons"), "Quick Load", ITEM_QUICK_LOAD)
menu.add_icon_item(get_theme_icon("Load", "EditorIcons"), "Load", ITEM_LOAD)
if resource:
menu.add_icon_item(get_theme_icon("Edit", "EditorIcons"), "Edit", ITEM_EDIT)
menu.add_icon_item(get_theme_icon("Clear", "EditorIcons"), "Clear", ITEM_CLEAR)
menu.add_separator()
menu.add_item("Show in FileSystem", ITEM_FILESYSTEM)
menu.size = Vector2.ZERO
### Signals
func _on_new_dialog_file_selected(path: String) -> void:
editor_plugin.main_view.new_file(path)
is_waiting_for_file = false
if Engine.get_meta("DialogueCache").has_file(path):
resource_changed.emit(load(path))
else:
var next_resource: Resource = await editor_plugin.import_plugin.compiled_resource
next_resource.resource_path = path
resource_changed.emit(next_resource)
func _on_open_dialog_file_selected(file: String) -> void:
resource_changed.emit(load(file))
func _on_file_dialog_canceled() -> void:
is_waiting_for_file = false
func _on_resource_button_pressed() -> void:
if is_instance_valid(resource):
editor_plugin.get_editor_interface().call_deferred("edit_resource", resource)
else:
build_menu()
menu.position = get_viewport().position + Vector2i(
button.global_position.x + button.size.x - menu.size.x,
2 + menu_button.global_position.y + button.size.y
)
menu.popup()
func _on_resource_button_resource_dropped(next_resource: Resource) -> void:
resource_changed.emit(next_resource)
func _on_menu_button_pressed() -> void:
build_menu()
menu.position = get_viewport().position + Vector2i(
menu_button.global_position.x + menu_button.size.x - menu.size.x,
2 + menu_button.global_position.y + menu_button.size.y
)
menu.popup()
func _on_menu_id_pressed(id: int) -> void:
match id:
ITEM_NEW:
is_waiting_for_file = true
new_dialog.popup_centered()
ITEM_QUICK_LOAD:
quick_selected_file = ""
files_list.files = Engine.get_meta("DialogueCache").get_files()
if resource:
files_list.select_file(resource.resource_path)
quick_open_dialog.popup_centered()
files_list.focus_filter()
ITEM_LOAD:
is_waiting_for_file = true
open_dialog.popup_centered()
ITEM_EDIT:
editor_plugin.get_editor_interface().call_deferred("edit_resource", resource)
ITEM_CLEAR:
resource_changed.emit(null)
ITEM_FILESYSTEM:
var file_system = editor_plugin.get_editor_interface().get_file_system_dock()
file_system.navigate_to_path(resource.resource_path)
func _on_files_list_file_double_clicked(file_path: String) -> void:
resource_changed.emit(load(file_path))
quick_open_dialog.hide()
func _on_files_list_file_selected(file_path: String) -> void:
quick_selected_file = file_path
func _on_quick_open_dialog_confirmed() -> void:
if quick_selected_file != "":
resource_changed.emit(load(quick_selected_file))

View File

@@ -0,0 +1,58 @@
[gd_scene load_steps=4 format=3 uid="uid://ycn6uaj7dsrh"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/editor_property/editor_property_control.gd" id="1_het12"]
[ext_resource type="PackedScene" uid="uid://b16uuqjuof3n5" path="res://addons/dialogue_manager/components/editor_property/resource_button.tscn" id="2_hh3d4"]
[ext_resource type="PackedScene" uid="uid://dnufpcdrreva3" path="res://addons/dialogue_manager/components/files_list.tscn" id="3_l8fp6"]
[node name="PropertyEditorButton" type="HBoxContainer"]
offset_right = 40.0
offset_bottom = 40.0
size_flags_horizontal = 3
theme_override_constants/separation = 0
script = ExtResource("1_het12")
[node name="ResourceButton" parent="." instance=ExtResource("2_hh3d4")]
layout_mode = 2
text = "<empty>"
text_overrun_behavior = 3
clip_text = true
[node name="MenuButton" type="Button" parent="."]
layout_mode = 2
[node name="Menu" type="PopupMenu" parent="."]
[node name="QuickOpenDialog" type="ConfirmationDialog" parent="."]
title = "Find Dialogue Resource"
size = Vector2i(400, 600)
min_size = Vector2i(400, 600)
ok_button_text = "Open"
[node name="FilesList" parent="QuickOpenDialog" instance=ExtResource("3_l8fp6")]
[node name="NewDialog" type="FileDialog" parent="."]
size = Vector2i(900, 750)
min_size = Vector2i(900, 750)
dialog_hide_on_ok = true
filters = PackedStringArray("*.dialogue ; Dialogue")
[node name="OpenDialog" type="FileDialog" parent="."]
title = "Open a File"
size = Vector2i(900, 750)
min_size = Vector2i(900, 750)
ok_button_text = "Open"
dialog_hide_on_ok = true
file_mode = 0
filters = PackedStringArray("*.dialogue ; Dialogue")
[connection signal="pressed" from="ResourceButton" to="." method="_on_resource_button_pressed"]
[connection signal="resource_dropped" from="ResourceButton" to="." method="_on_resource_button_resource_dropped"]
[connection signal="pressed" from="MenuButton" to="." method="_on_menu_button_pressed"]
[connection signal="id_pressed" from="Menu" to="." method="_on_menu_id_pressed"]
[connection signal="confirmed" from="QuickOpenDialog" to="." method="_on_quick_open_dialog_confirmed"]
[connection signal="file_double_clicked" from="QuickOpenDialog/FilesList" to="." method="_on_files_list_file_double_clicked"]
[connection signal="file_selected" from="QuickOpenDialog/FilesList" to="." method="_on_files_list_file_selected"]
[connection signal="canceled" from="NewDialog" to="." method="_on_file_dialog_canceled"]
[connection signal="file_selected" from="NewDialog" to="." method="_on_new_dialog_file_selected"]
[connection signal="canceled" from="OpenDialog" to="." method="_on_file_dialog_canceled"]
[connection signal="file_selected" from="OpenDialog" to="." method="_on_open_dialog_file_selected"]

View File

@@ -0,0 +1,48 @@
@tool
extends Button
signal resource_dropped(next_resource: Resource)
var resource: Resource:
set(next_resource):
resource = next_resource
if resource:
icon = Engine.get_meta("DialogueManagerPlugin")._get_plugin_icon()
text = resource.resource_path.get_file().replace(".dialogue", "")
else:
icon = null
text = "<empty>"
get:
return resource
func _notification(what: int) -> void:
match what:
NOTIFICATION_DRAG_BEGIN:
var data = get_viewport().gui_get_drag_data()
if typeof(data) == TYPE_DICTIONARY and data.type == "files" and data.files.size() > 0 and data.files[0].ends_with(".dialogue"):
add_theme_stylebox_override("normal", get_theme_stylebox("focus", "LineEdit"))
add_theme_stylebox_override("hover", get_theme_stylebox("focus", "LineEdit"))
NOTIFICATION_DRAG_END:
self.resource = resource
remove_theme_stylebox_override("normal")
remove_theme_stylebox_override("hover")
func _can_drop_data(at_position: Vector2, data) -> bool:
if typeof(data) != TYPE_DICTIONARY: return false
if data.type != "files": return false
var files: PackedStringArray = Array(data.files).filter(func(f): return f.get_extension() == "dialogue")
return files.size() > 0
func _drop_data(at_position: Vector2, data) -> void:
var files: PackedStringArray = Array(data.files).filter(func(f): return f.get_extension() == "dialogue")
if files.size() == 0: return
resource_dropped.emit(load(files[0]))

View File

@@ -0,0 +1,9 @@
[gd_scene load_steps=2 format=3 uid="uid://b16uuqjuof3n5"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/editor_property/resource_button.gd" id="1_7u2i7"]
[node name="ResourceButton" type="Button"]
offset_right = 8.0
offset_bottom = 8.0
size_flags_horizontal = 3
script = ExtResource("1_7u2i7")

View File

@@ -0,0 +1,85 @@
@tool
extends HBoxContainer
signal error_pressed(line_number)
const DialogueConstants = preload("../constants.gd")
@onready var error_button: Button = $ErrorButton
@onready var next_button: Button = $NextButton
@onready var count_label: Label = $CountLabel
@onready var previous_button: Button = $PreviousButton
## The index of the current error being shown
var error_index: int = 0:
set(next_error_index):
error_index = wrap(next_error_index, 0, errors.size())
show_error()
get:
return error_index
## The list of all errors
var errors: Array = []:
set(next_errors):
errors = next_errors
self.error_index = 0
get:
return errors
func _ready() -> void:
apply_theme()
hide()
## Set up colors and icons
func apply_theme() -> void:
error_button.add_theme_color_override("font_color", get_theme_color("error_color", "Editor"))
error_button.add_theme_color_override("font_hover_color", get_theme_color("error_color", "Editor"))
error_button.icon = get_theme_icon("StatusError", "EditorIcons")
previous_button.icon = get_theme_icon("ArrowLeft", "EditorIcons")
next_button.icon = get_theme_icon("ArrowRight", "EditorIcons")
## Move the error index to match a given line
func show_error_for_line_number(line_number: int) -> void:
for i in range(0, errors.size()):
if errors[i].line_number == line_number:
self.error_index = i
## Show the current error
func show_error() -> void:
if errors.size() == 0:
hide()
else:
show()
count_label.text = DialogueConstants.translate(&"n_of_n").format({ index = error_index + 1, total = errors.size() })
var error = errors[error_index]
error_button.text = DialogueConstants.translate(&"errors.line_and_message").format({ line = error.line_number + 1, column = error.column_number, message = DialogueConstants.get_error_message(error.error) })
if error.has("external_error"):
error_button.text += " " + DialogueConstants.get_error_message(error.external_error)
### Signals
func _on_errors_panel_theme_changed() -> void:
apply_theme()
func _on_error_button_pressed() -> void:
emit_signal("error_pressed", errors[error_index].line_number, errors[error_index].column_number)
func _on_previous_button_pressed() -> void:
self.error_index -= 1
_on_error_button_pressed()
func _on_next_button_pressed() -> void:
self.error_index += 1
_on_error_button_pressed()

View File

@@ -0,0 +1,56 @@
[gd_scene load_steps=4 format=3 uid="uid://cs8pwrxr5vxix"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/errors_panel.gd" id="1_nfm3c"]
[sub_resource type="Image" id="Image_wy5pj"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="ImageTexture_s6fxl"]
image = SubResource("Image_wy5pj")
[node name="ErrorsPanel" type="HBoxContainer"]
visible = false
offset_right = 1024.0
offset_bottom = 600.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_nfm3c")
metadata/_edit_layout_mode = 1
[node name="ErrorButton" type="Button" parent="."]
layout_mode = 2
size_flags_horizontal = 3
theme_override_colors/font_color = Color(0, 0, 0, 1)
theme_override_colors/font_hover_color = Color(0, 0, 0, 1)
theme_override_constants/h_separation = 3
icon = SubResource("ImageTexture_s6fxl")
flat = true
alignment = 0
text_overrun_behavior = 4
[node name="Spacer" type="Control" parent="."]
custom_minimum_size = Vector2(40, 0)
layout_mode = 2
[node name="PreviousButton" type="Button" parent="."]
layout_mode = 2
icon = SubResource("ImageTexture_s6fxl")
flat = true
[node name="CountLabel" type="Label" parent="."]
layout_mode = 2
[node name="NextButton" type="Button" parent="."]
layout_mode = 2
icon = SubResource("ImageTexture_s6fxl")
flat = true
[connection signal="pressed" from="ErrorButton" to="." method="_on_error_button_pressed"]
[connection signal="pressed" from="PreviousButton" to="." method="_on_previous_button_pressed"]
[connection signal="pressed" from="NextButton" to="." method="_on_next_button_pressed"]

View File

@@ -0,0 +1,146 @@
@tool
extends VBoxContainer
signal file_selected(file_path: String)
signal file_popup_menu_requested(at_position: Vector2)
signal file_double_clicked(file_path: String)
signal file_middle_clicked(file_path: String)
const DialogueConstants = preload("../constants.gd")
const MODIFIED_SUFFIX = "(*)"
@export var icon: Texture2D
@onready var filter_edit: LineEdit = $FilterEdit
@onready var list: ItemList = $List
var file_map: Dictionary = {}
var current_file_path: String = ""
var files: PackedStringArray = []:
set(next_files):
files = next_files
files.sort()
update_file_map()
apply_filter()
get:
return files
var unsaved_files: Array[String] = []
var filter: String:
set(next_filter):
filter = next_filter
apply_filter()
get:
return filter
func _ready() -> void:
apply_theme()
filter_edit.placeholder_text = DialogueConstants.translate(&"files_list.filter")
func focus_filter() -> void:
filter_edit.grab_focus()
func select_file(file: String) -> void:
list.deselect_all()
for i in range(0, list.get_item_count()):
var item_text = list.get_item_text(i).replace(MODIFIED_SUFFIX, "")
if item_text == get_nice_file(file, item_text.count("/") + 1):
list.select(i)
func mark_file_as_unsaved(file: String, is_unsaved: bool) -> void:
if not file in unsaved_files and is_unsaved:
unsaved_files.append(file)
elif file in unsaved_files and not is_unsaved:
unsaved_files.erase(file)
apply_filter()
func update_file_map() -> void:
file_map = {}
for file in files:
var nice_file: String = get_nice_file(file)
# See if a value with just the file name is already in the map
for key in file_map.keys():
if file_map[key] == nice_file:
var bit_count = nice_file.count("/") + 2
var existing_nice_file = get_nice_file(key, bit_count)
nice_file = get_nice_file(file, bit_count)
while nice_file == existing_nice_file:
bit_count += 1
existing_nice_file = get_nice_file(key, bit_count)
nice_file = get_nice_file(file, bit_count)
file_map[key] = existing_nice_file
file_map[file] = nice_file
func get_nice_file(file_path: String, path_bit_count: int = 1) -> String:
var bits = file_path.replace("res://", "").replace(".dialogue", "").split("/")
bits = bits.slice(-path_bit_count)
return "/".join(bits)
func apply_filter() -> void:
list.clear()
for file in file_map.keys():
if filter == "" or filter.to_lower() in file.to_lower():
var nice_file = file_map[file]
if file in unsaved_files:
nice_file += MODIFIED_SUFFIX
var new_id := list.add_item(nice_file)
list.set_item_icon(new_id, icon)
select_file(current_file_path)
func apply_theme() -> void:
if is_instance_valid(filter_edit):
filter_edit.right_icon = get_theme_icon("Search", "EditorIcons")
### Signals
func _on_theme_changed() -> void:
apply_theme()
func _on_filter_edit_text_changed(new_text: String) -> void:
self.filter = new_text
func _on_list_item_clicked(index: int, at_position: Vector2, mouse_button_index: int) -> void:
var item_text = list.get_item_text(index).replace(MODIFIED_SUFFIX, "")
var file = file_map.find_key(item_text)
if mouse_button_index == MOUSE_BUTTON_LEFT or mouse_button_index == MOUSE_BUTTON_RIGHT:
select_file(file)
file_selected.emit(file)
if mouse_button_index == MOUSE_BUTTON_RIGHT:
file_popup_menu_requested.emit(at_position)
if mouse_button_index == MOUSE_BUTTON_MIDDLE:
file_middle_clicked.emit(file)
func _on_list_item_activated(index: int) -> void:
var item_text = list.get_item_text(index).replace(MODIFIED_SUFFIX, "")
var file = file_map.find_key(item_text)
select_file(file)
file_double_clicked.emit(file)

View File

@@ -0,0 +1,28 @@
[gd_scene load_steps=3 format=3 uid="uid://dnufpcdrreva3"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/files_list.gd" id="1_cytii"]
[ext_resource type="Texture2D" uid="uid://d3lr2uas6ax8v" path="res://addons/dialogue_manager/assets/icon.svg" id="2_3ijx1"]
[node name="FilesList" type="VBoxContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_cytii")
icon = ExtResource("2_3ijx1")
[node name="FilterEdit" type="LineEdit" parent="."]
layout_mode = 2
placeholder_text = "Filter files"
clear_button_enabled = true
[node name="List" type="ItemList" parent="."]
layout_mode = 2
size_flags_vertical = 3
allow_rmb_select = true
[connection signal="theme_changed" from="." to="." method="_on_theme_changed"]
[connection signal="text_changed" from="FilterEdit" to="." method="_on_filter_edit_text_changed"]
[connection signal="item_activated" from="List" to="." method="_on_list_item_activated"]
[connection signal="item_clicked" from="List" to="." method="_on_list_item_clicked"]

View File

@@ -0,0 +1,229 @@
@tool
extends Control
signal result_selected(path: String, cursor: Vector2, length: int)
const DialogueConstants = preload("../constants.gd")
@export var main_view: Control
@export var code_edit: CodeEdit
@onready var input: LineEdit = %Input
@onready var search_button: Button = %SearchButton
@onready var match_case_button: CheckBox = %MatchCaseButton
@onready var replace_toggle: CheckButton = %ReplaceToggle
@onready var replace_container: VBoxContainer = %ReplaceContainer
@onready var replace_input: LineEdit = %ReplaceInput
@onready var replace_selected_button: Button = %ReplaceSelectedButton
@onready var replace_all_button: Button = %ReplaceAllButton
@onready var results_container: VBoxContainer = %ResultsContainer
@onready var result_template: HBoxContainer = %ResultTemplate
var current_results: Dictionary = {}:
set(value):
current_results = value
update_results_view()
if current_results.size() == 0:
replace_selected_button.disabled = true
replace_all_button.disabled = true
else:
replace_selected_button.disabled = false
replace_all_button.disabled = false
get:
return current_results
var selections: PackedStringArray = []
func prepare() -> void:
input.grab_focus()
var template_label = result_template.get_node("Label")
template_label.get_theme_stylebox(&"focus").bg_color = code_edit.theme_overrides.current_line_color
template_label.add_theme_font_override(&"normal_font", code_edit.get_theme_font(&"font"))
replace_toggle.set_pressed_no_signal(false)
replace_container.hide()
$VBoxContainer/HBoxContainer/FindContainer/Label.text = DialogueConstants.translate(&"search.find")
input.placeholder_text = DialogueConstants.translate(&"search.placeholder")
input.text = ""
search_button.text = DialogueConstants.translate(&"search.find_all")
match_case_button.text = DialogueConstants.translate(&"search.match_case")
replace_toggle.text = DialogueConstants.translate(&"search.toggle_replace")
$VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceLabel.text = DialogueConstants.translate(&"search.replace_with")
replace_input.placeholder_text = DialogueConstants.translate(&"search.replace_placeholder")
replace_input.text = ""
replace_all_button.text = DialogueConstants.translate(&"search.replace_all")
replace_selected_button.text = DialogueConstants.translate(&"search.replace_selected")
selections.clear()
self.current_results = {}
#region helpers
func update_results_view() -> void:
for child in results_container.get_children():
child.queue_free()
for path in current_results.keys():
var path_label: Label = Label.new()
path_label.text = path
# Show open files
if main_view.open_buffers.has(path):
path_label.text += "(*)"
results_container.add_child(path_label)
for path_result in current_results.get(path):
var result_item: HBoxContainer = result_template.duplicate()
var checkbox: CheckBox = result_item.get_node("CheckBox") as CheckBox
var key: String = get_selection_key(path, path_result)
checkbox.toggled.connect(func(is_pressed):
if is_pressed:
if not selections.has(key):
selections.append(key)
else:
if selections.has(key):
selections.remove_at(selections.find(key))
)
checkbox.set_pressed_no_signal(selections.has(key))
checkbox.visible = replace_toggle.button_pressed
var result_label: RichTextLabel = result_item.get_node("Label") as RichTextLabel
var colors: Dictionary = code_edit.theme_overrides
var highlight: String = ""
if replace_toggle.button_pressed:
var matched_word: String = "[bgcolor=" + colors.critical_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + path_result.matched_text + "[/color][/bgcolor]"
highlight = "[s]" + matched_word + "[/s][bgcolor=" + colors.notice_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + replace_input.text + "[/color][/bgcolor]"
else:
highlight = "[bgcolor=" + colors.symbols_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + path_result.matched_text + "[/color][/bgcolor]"
var text: String = path_result.text.substr(0, path_result.index) + highlight + path_result.text.substr(path_result.index + path_result.query.length())
result_label.text = "%s: %s" % [str(path_result.line).lpad(4), text]
result_label.gui_input.connect(func(event):
if event is InputEventMouseButton and (event as InputEventMouseButton).button_index == MOUSE_BUTTON_LEFT and (event as InputEventMouseButton).double_click:
result_selected.emit(path, Vector2(path_result.index, path_result.line), path_result.query.length())
)
results_container.add_child(result_item)
func find_in_files() -> Dictionary:
var results: Dictionary = {}
var q: String = input.text
var cache = Engine.get_meta("DialogueCache")
var file: FileAccess
for path in cache.get_files():
var path_results: Array = []
var lines: PackedStringArray = []
if main_view.open_buffers.has(path):
lines = main_view.open_buffers.get(path).text.split("\n")
else:
file = FileAccess.open(path, FileAccess.READ)
lines = file.get_as_text().split("\n")
for i in range(0, lines.size()):
var index: int = find_in_line(lines[i], q)
while index > -1:
path_results.append({
line = i,
index = index,
text = lines[i],
matched_text = lines[i].substr(index, q.length()),
query = q
})
index = find_in_line(lines[i], q, index + q.length())
if file != null and file.is_open():
file.close()
if path_results.size() > 0:
results[path] = path_results
return results
func get_selection_key(path: String, path_result: Dictionary) -> String:
return "%s-%d-%d" % [path, path_result.line, path_result.index]
func find_in_line(line: String, query: String, from_index: int = 0) -> int:
if match_case_button.button_pressed:
return line.find(query, from_index)
else:
return line.findn(query, from_index)
func replace_results(only_selected: bool) -> void:
var file: FileAccess
var lines: PackedStringArray = []
for path in current_results:
if main_view.open_buffers.has(path):
lines = main_view.open_buffers.get(path).text.split("\n")
else:
file = FileAccess.open(path, FileAccess.READ_WRITE)
lines = file.get_as_text().split("\n")
# Read the results in reverse because we're going to be modifying them as we go
var path_results: Array = current_results.get(path).duplicate()
path_results.reverse()
for path_result in path_results:
var key: String = get_selection_key(path, path_result)
if not only_selected or (only_selected and selections.has(key)):
lines[path_result.line] = lines[path_result.line].substr(0, path_result.index) + replace_input.text + lines[path_result.line].substr(path_result.index + path_result.matched_text.length())
var replaced_text: String = "\n".join(lines)
if file != null and file.is_open():
file.seek(0)
file.store_string(replaced_text)
file.close()
else:
main_view.open_buffers.get(path).text = replaced_text
if main_view.current_file_path == path:
code_edit.text = replaced_text
current_results = find_in_files()
#endregion
#region signals
func _on_search_button_pressed() -> void:
selections.clear()
self.current_results = find_in_files()
func _on_input_text_submitted(new_text: String) -> void:
_on_search_button_pressed()
func _on_replace_toggle_toggled(toggled_on: bool) -> void:
replace_container.visible = toggled_on
if toggled_on:
replace_input.grab_focus()
update_results_view()
func _on_replace_input_text_changed(new_text: String) -> void:
update_results_view()
func _on_replace_selected_button_pressed() -> void:
replace_results(true)
func _on_replace_all_button_pressed() -> void:
replace_results(false)
func _on_match_case_button_toggled(toggled_on: bool) -> void:
_on_search_button_pressed()
#endregion

View File

@@ -0,0 +1,139 @@
[gd_scene load_steps=3 format=3 uid="uid://0n7hwviyyly4"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/find_in_files.gd" id="1_3xicy"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_owohg"]
bg_color = Color(0.266667, 0.278431, 0.352941, 0.243137)
corner_detail = 1
[node name="FindInFiles" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
script = ExtResource("1_3xicy")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
[node name="FindContainer" type="VBoxContainer" parent="VBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer/FindContainer"]
layout_mode = 2
text = "Find:"
[node name="Input" type="LineEdit" parent="VBoxContainer/HBoxContainer/FindContainer"]
unique_name_in_owner = true
layout_mode = 2
clear_button_enabled = true
[node name="FindToolbar" type="HBoxContainer" parent="VBoxContainer/HBoxContainer/FindContainer"]
layout_mode = 2
[node name="SearchButton" type="Button" parent="VBoxContainer/HBoxContainer/FindContainer/FindToolbar"]
unique_name_in_owner = true
layout_mode = 2
text = "Find all..."
[node name="MatchCaseButton" type="CheckBox" parent="VBoxContainer/HBoxContainer/FindContainer/FindToolbar"]
unique_name_in_owner = true
layout_mode = 2
text = "Match case"
[node name="Control" type="Control" parent="VBoxContainer/HBoxContainer/FindContainer/FindToolbar"]
layout_mode = 2
size_flags_horizontal = 3
[node name="ReplaceToggle" type="CheckButton" parent="VBoxContainer/HBoxContainer/FindContainer/FindToolbar"]
unique_name_in_owner = true
layout_mode = 2
text = "Replace"
[node name="ReplaceContainer" type="VBoxContainer" parent="VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
[node name="ReplaceLabel" type="Label" parent="VBoxContainer/HBoxContainer/ReplaceContainer"]
layout_mode = 2
text = "Replace with:"
[node name="ReplaceInput" type="LineEdit" parent="VBoxContainer/HBoxContainer/ReplaceContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
clear_button_enabled = true
[node name="ReplaceToolbar" type="HBoxContainer" parent="VBoxContainer/HBoxContainer/ReplaceContainer"]
layout_mode = 2
[node name="ReplaceSelectedButton" type="Button" parent="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceToolbar"]
unique_name_in_owner = true
layout_mode = 2
text = "Replace selected"
[node name="ReplaceAllButton" type="Button" parent="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceToolbar"]
unique_name_in_owner = true
layout_mode = 2
text = "Replace all"
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer"]
layout_mode = 2
[node name="ReplaceToolbar" type="HBoxContainer" parent="VBoxContainer/VBoxContainer"]
layout_mode = 2
[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
follow_focus = true
[node name="ResultsContainer" type="VBoxContainer" parent="VBoxContainer/ScrollContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_constants/separation = 0
[node name="ResultTemplate" type="HBoxContainer" parent="."]
unique_name_in_owner = true
layout_mode = 0
offset_left = 155.0
offset_top = -74.0
offset_right = 838.0
offset_bottom = -51.0
[node name="CheckBox" type="CheckBox" parent="ResultTemplate"]
layout_mode = 2
[node name="Label" type="RichTextLabel" parent="ResultTemplate"]
layout_mode = 2
size_flags_horizontal = 3
focus_mode = 2
theme_override_styles/focus = SubResource("StyleBoxFlat_owohg")
bbcode_enabled = true
text = "Result"
fit_content = true
scroll_active = false
[connection signal="text_submitted" from="VBoxContainer/HBoxContainer/FindContainer/Input" to="." method="_on_input_text_submitted"]
[connection signal="pressed" from="VBoxContainer/HBoxContainer/FindContainer/FindToolbar/SearchButton" to="." method="_on_search_button_pressed"]
[connection signal="toggled" from="VBoxContainer/HBoxContainer/FindContainer/FindToolbar/MatchCaseButton" to="." method="_on_match_case_button_toggled"]
[connection signal="toggled" from="VBoxContainer/HBoxContainer/FindContainer/FindToolbar/ReplaceToggle" to="." method="_on_replace_toggle_toggled"]
[connection signal="text_changed" from="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceInput" to="." method="_on_replace_input_text_changed"]
[connection signal="pressed" from="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceToolbar/ReplaceSelectedButton" to="." method="_on_replace_selected_button_pressed"]
[connection signal="pressed" from="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceToolbar/ReplaceAllButton" to="." method="_on_replace_all_button_pressed"]

View File

@@ -0,0 +1,10 @@
class_name DialogueManagerParseResult extends RefCounted
var imported_paths: PackedStringArray = []
var using_states: PackedStringArray = []
var titles: Dictionary = {}
var character_names: PackedStringArray = []
var first_title: String = ""
var lines: Dictionary = {}
var errors: Array[Dictionary] = []
var raw_text: String = ""

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
extends RefCounted
var text: String = ""
var pauses: Dictionary = {}
var speeds: Dictionary = {}
var mutations: Array[Array] = []
var time: String = ""
func _init(data: Dictionary) -> void:
text = data.text
pauses = data.pauses
speeds = data.speeds
mutations = data.mutations
time = data.time

View File

@@ -0,0 +1,10 @@
extends RefCounted
var tags: PackedStringArray = []
var line_without_tags: String = ""
func _init(data: Dictionary) -> void:
tags = data.tags
line_without_tags = data.line_without_tags

View File

@@ -0,0 +1,212 @@
@tool
extends VBoxContainer
signal open_requested()
signal close_requested()
const DialogueConstants = preload("../constants.gd")
@onready var input: LineEdit = $Search/Input
@onready var result_label: Label = $Search/ResultLabel
@onready var previous_button: Button = $Search/PreviousButton
@onready var next_button: Button = $Search/NextButton
@onready var match_case_button: CheckBox = $Search/MatchCaseCheckBox
@onready var replace_check_button: CheckButton = $Search/ReplaceCheckButton
@onready var replace_panel: HBoxContainer = $Replace
@onready var replace_input: LineEdit = $Replace/Input
@onready var replace_button: Button = $Replace/ReplaceButton
@onready var replace_all_button: Button = $Replace/ReplaceAllButton
# The code edit we will be affecting (for some reason exporting this didn't work)
var code_edit: CodeEdit:
set(next_code_edit):
code_edit = next_code_edit
code_edit.gui_input.connect(_on_text_edit_gui_input)
code_edit.text_changed.connect(_on_text_edit_text_changed)
get:
return code_edit
var results: Array = []
var result_index: int = -1:
set(next_result_index):
result_index = next_result_index
if results.size() > 0:
var r = results[result_index]
code_edit.set_caret_line(r[0])
code_edit.select(r[0], r[1], r[0], r[1] + r[2])
else:
result_index = -1
if is_instance_valid(code_edit):
code_edit.deselect()
result_label.text = DialogueConstants.translate(&"n_of_n").format({ index = result_index + 1, total = results.size() })
get:
return result_index
func _ready() -> void:
apply_theme()
input.placeholder_text = DialogueConstants.translate(&"search.placeholder")
previous_button.tooltip_text = DialogueConstants.translate(&"search.previous")
next_button.tooltip_text = DialogueConstants.translate(&"search.next")
match_case_button.text = DialogueConstants.translate(&"search.match_case")
$Search/ReplaceCheckButton.text = DialogueConstants.translate(&"search.toggle_replace")
replace_button.text = DialogueConstants.translate(&"search.replace")
replace_all_button.text = DialogueConstants.translate(&"search.replace_all")
$Replace/ReplaceLabel.text = DialogueConstants.translate(&"search.replace_with")
self.result_index = -1
replace_panel.hide()
replace_button.disabled = true
replace_all_button.disabled = true
hide()
func focus_line_edit() -> void:
input.grab_focus()
input.select_all()
func apply_theme() -> void:
if is_instance_valid(previous_button):
previous_button.icon = get_theme_icon("ArrowLeft", "EditorIcons")
if is_instance_valid(next_button):
next_button.icon = get_theme_icon("ArrowRight", "EditorIcons")
# Find text in the code
func search(text: String = "", default_result_index: int = 0) -> void:
results.clear()
if text == "":
text = input.text
var lines = code_edit.text.split("\n")
for line_number in range(0, lines.size()):
var line = lines[line_number]
var column = find_in_line(line, text, 0)
while column > -1:
results.append([line_number, column, text.length()])
column = find_in_line(line, text, column + 1)
if results.size() > 0:
replace_button.disabled = false
replace_all_button.disabled = false
else:
replace_button.disabled = true
replace_all_button.disabled = true
self.result_index = clamp(default_result_index, 0, results.size() - 1)
# Find text in a string and match case if requested
func find_in_line(line: String, text: String, from_index: int = 0) -> int:
if match_case_button.button_pressed:
return line.find(text, from_index)
else:
return line.findn(text, from_index)
### Signals
func _on_text_edit_gui_input(event: InputEvent) -> void:
if event is InputEventKey and event.is_pressed():
match event.as_text():
"Ctrl+F", "Command+F":
open_requested.emit()
get_viewport().set_input_as_handled()
"Ctrl+Shift+R", "Command+Shift+R":
replace_check_button.set_pressed(true)
open_requested.emit()
get_viewport().set_input_as_handled()
func _on_text_edit_text_changed() -> void:
results.clear()
func _on_search_and_replace_theme_changed() -> void:
apply_theme()
func _on_input_text_changed(new_text: String) -> void:
search(new_text)
func _on_previous_button_pressed() -> void:
self.result_index = wrapi(result_index - 1, 0, results.size())
func _on_next_button_pressed() -> void:
self.result_index = wrapi(result_index + 1, 0, results.size())
func _on_search_and_replace_visibility_changed() -> void:
if is_instance_valid(input):
if visible:
input.grab_focus()
var selection = code_edit.get_selected_text()
if input.text == "" and selection != "":
input.text = selection
search(selection)
else:
search()
else:
input.text = ""
func _on_input_gui_input(event: InputEvent) -> void:
if event is InputEventKey and event.is_pressed():
match event.as_text():
"Enter":
search(input.text)
"Escape":
emit_signal("close_requested")
func _on_replace_button_pressed() -> void:
if result_index == -1: return
# Replace the selection at result index
var r: Array = results[result_index]
var lines: PackedStringArray = code_edit.text.split("\n")
var line: String = lines[r[0]]
line = line.substr(0, r[1]) + replace_input.text + line.substr(r[1] + r[2])
lines[r[0]] = line
code_edit.text = "\n".join(lines)
search(input.text, result_index)
code_edit.text_changed.emit()
func _on_replace_all_button_pressed() -> void:
if match_case_button.button_pressed:
code_edit.text = code_edit.text.replace(input.text, replace_input.text)
else:
code_edit.text = code_edit.text.replacen(input.text, replace_input.text)
search()
code_edit.text_changed.emit()
func _on_replace_check_button_toggled(button_pressed: bool) -> void:
replace_panel.visible = button_pressed
if button_pressed:
replace_input.grab_focus()
func _on_input_focus_entered() -> void:
if results.size() == 0:
search()
else:
self.result_index = result_index
func _on_match_case_check_box_toggled(button_pressed: bool) -> void:
search()

View File

@@ -0,0 +1,87 @@
[gd_scene load_steps=2 format=3 uid="uid://gr8nakpbrhby"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/search_and_replace.gd" id="1_8oj1f"]
[node name="SearchAndReplace" type="VBoxContainer"]
visible = false
anchors_preset = 10
anchor_right = 1.0
offset_bottom = 31.0
grow_horizontal = 2
size_flags_horizontal = 3
script = ExtResource("1_8oj1f")
[node name="Search" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="Input" type="LineEdit" parent="Search"]
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Text to search for"
metadata/_edit_use_custom_anchors = true
[node name="MatchCaseCheckBox" type="CheckBox" parent="Search"]
layout_mode = 2
text = "Match case"
[node name="VSeparator" type="VSeparator" parent="Search"]
layout_mode = 2
[node name="PreviousButton" type="Button" parent="Search"]
layout_mode = 2
tooltip_text = "Previous"
flat = true
[node name="ResultLabel" type="Label" parent="Search"]
layout_mode = 2
text = "0 of 0"
[node name="NextButton" type="Button" parent="Search"]
layout_mode = 2
tooltip_text = "Next"
flat = true
[node name="VSeparator2" type="VSeparator" parent="Search"]
layout_mode = 2
[node name="ReplaceCheckButton" type="CheckButton" parent="Search"]
layout_mode = 2
text = "Replace"
[node name="Replace" type="HBoxContainer" parent="."]
visible = false
layout_mode = 2
[node name="ReplaceLabel" type="Label" parent="Replace"]
layout_mode = 2
text = "Replace with:"
[node name="Input" type="LineEdit" parent="Replace"]
layout_mode = 2
size_flags_horizontal = 3
[node name="ReplaceButton" type="Button" parent="Replace"]
layout_mode = 2
disabled = true
text = "Replace"
flat = true
[node name="ReplaceAllButton" type="Button" parent="Replace"]
layout_mode = 2
disabled = true
text = "Replace all"
flat = true
[connection signal="theme_changed" from="." to="." method="_on_search_and_replace_theme_changed"]
[connection signal="visibility_changed" from="." to="." method="_on_search_and_replace_visibility_changed"]
[connection signal="focus_entered" from="Search/Input" to="." method="_on_input_focus_entered"]
[connection signal="gui_input" from="Search/Input" to="." method="_on_input_gui_input"]
[connection signal="text_changed" from="Search/Input" to="." method="_on_input_text_changed"]
[connection signal="toggled" from="Search/MatchCaseCheckBox" to="." method="_on_match_case_check_box_toggled"]
[connection signal="pressed" from="Search/PreviousButton" to="." method="_on_previous_button_pressed"]
[connection signal="pressed" from="Search/NextButton" to="." method="_on_next_button_pressed"]
[connection signal="toggled" from="Search/ReplaceCheckButton" to="." method="_on_replace_check_button_toggled"]
[connection signal="focus_entered" from="Replace/Input" to="." method="_on_input_focus_entered"]
[connection signal="gui_input" from="Replace/Input" to="." method="_on_input_gui_input"]
[connection signal="pressed" from="Replace/ReplaceButton" to="." method="_on_replace_button_pressed"]
[connection signal="pressed" from="Replace/ReplaceAllButton" to="." method="_on_replace_all_button_pressed"]

View File

@@ -0,0 +1,67 @@
@tool
extends VBoxContainer
signal title_selected(title: String)
const DialogueConstants = preload("../constants.gd")
@onready var filter_edit: LineEdit = $FilterEdit
@onready var list: ItemList = $List
var titles: PackedStringArray:
set(next_titles):
titles = next_titles
apply_filter()
get:
return titles
var filter: String:
set(next_filter):
filter = next_filter
apply_filter()
get:
return filter
func _ready() -> void:
apply_theme()
filter_edit.placeholder_text = DialogueConstants.translate(&"titles_list.filter")
func select_title(title: String) -> void:
list.deselect_all()
for i in range(0, list.get_item_count()):
if list.get_item_text(i) == title.strip_edges():
list.select(i)
func apply_filter() -> void:
list.clear()
for title in titles:
if filter == "" or filter.to_lower() in title.to_lower():
list.add_item(title.strip_edges())
func apply_theme() -> void:
if is_instance_valid(filter_edit):
filter_edit.right_icon = get_theme_icon("Search", "EditorIcons")
### Signals
func _on_theme_changed() -> void:
apply_theme()
func _on_filter_edit_text_changed(new_text: String) -> void:
self.filter = new_text
func _on_list_item_clicked(index: int, at_position: Vector2, mouse_button_index: int) -> void:
if mouse_button_index == MOUSE_BUTTON_LEFT:
var title = list.get_item_text(index)
title_selected.emit(title)

View File

@@ -0,0 +1,27 @@
[gd_scene load_steps=2 format=3 uid="uid://ctns6ouwwd68i"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/title_list.gd" id="1_5qqmd"]
[node name="TitleList" type="VBoxContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
script = ExtResource("1_5qqmd")
[node name="FilterEdit" type="LineEdit" parent="."]
layout_mode = 2
placeholder_text = "Filter titles"
clear_button_enabled = true
[node name="List" type="ItemList" parent="."]
layout_mode = 2
size_flags_vertical = 3
allow_reselect = true
[connection signal="theme_changed" from="." to="." method="_on_theme_changed"]
[connection signal="text_changed" from="FilterEdit" to="." method="_on_filter_edit_text_changed"]
[connection signal="item_clicked" from="List" to="." method="_on_list_item_clicked"]

View File

@@ -0,0 +1,125 @@
@tool
extends Button
const DialogueConstants = preload("../constants.gd")
const DialogueSettings = preload("../settings.gd")
const REMOTE_RELEASES_URL = "https://api.github.com/repos/nathanhoad/godot_dialogue_manager/releases"
@onready var http_request: HTTPRequest = $HTTPRequest
@onready var download_dialog: AcceptDialog = $DownloadDialog
@onready var download_update_panel = $DownloadDialog/DownloadUpdatePanel
@onready var needs_reload_dialog: AcceptDialog = $NeedsReloadDialog
@onready var update_failed_dialog: AcceptDialog = $UpdateFailedDialog
@onready var timer: Timer = $Timer
var needs_reload: bool = false
# A lambda that gets called just before refreshing the plugin. Return false to stop the reload.
var on_before_refresh: Callable = func(): return true
func _ready() -> void:
hide()
apply_theme()
# Check for updates on GitHub
check_for_update()
# Check again every few hours
timer.start(60 * 60 * 12)
# Convert a version number to an actually comparable number
func version_to_number(version: String) -> int:
var bits = version.split(".")
return bits[0].to_int() * 1000000 + bits[1].to_int() * 1000 + bits[2].to_int()
func apply_theme() -> void:
var color: Color = get_theme_color("success_color", "Editor")
if needs_reload:
color = get_theme_color("error_color", "Editor")
icon = get_theme_icon("Reload", "EditorIcons")
add_theme_color_override("icon_normal_color", color)
add_theme_color_override("icon_focus_color", color)
add_theme_color_override("icon_hover_color", color)
add_theme_color_override("font_color", color)
add_theme_color_override("font_focus_color", color)
add_theme_color_override("font_hover_color", color)
func check_for_update() -> void:
if DialogueSettings.get_user_value("check_for_updates", true):
http_request.request(REMOTE_RELEASES_URL)
### Signals
func _on_http_request_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
if result != HTTPRequest.RESULT_SUCCESS: return
var current_version: String = Engine.get_meta("DialogueManagerPlugin").get_version()
# Work out the next version from the releases information on GitHub
var response = JSON.parse_string(body.get_string_from_utf8())
if typeof(response) != TYPE_ARRAY: return
# GitHub releases are in order of creation, not order of version
var versions = (response as Array).filter(func(release):
var version: String = release.tag_name.substr(1)
var major_version: int = version.split(".")[0].to_int()
var current_major_version: int = current_version.split(".")[0].to_int()
return major_version == current_major_version and version_to_number(version) > version_to_number(current_version)
)
if versions.size() > 0:
download_update_panel.next_version_release = versions[0]
text = DialogueConstants.translate(&"update.available").format({ version = versions[0].tag_name.substr(1) })
show()
func _on_update_button_pressed() -> void:
if needs_reload:
var will_refresh = on_before_refresh.call()
if will_refresh:
Engine.get_meta("DialogueManagerPlugin").get_editor_interface().restart_editor(true)
else:
var scale: float = Engine.get_meta("DialogueManagerPlugin").get_editor_interface().get_editor_scale()
download_dialog.min_size = Vector2(300, 250) * scale
download_dialog.popup_centered()
func _on_download_dialog_close_requested() -> void:
download_dialog.hide()
func _on_download_update_panel_updated(updated_to_version: String) -> void:
download_dialog.hide()
needs_reload_dialog.dialog_text = DialogueConstants.translate(&"update.needs_reload")
needs_reload_dialog.ok_button_text = DialogueConstants.translate(&"update.reload_ok_button")
needs_reload_dialog.cancel_button_text = DialogueConstants.translate(&"update.reload_cancel_button")
needs_reload_dialog.popup_centered()
needs_reload = true
text = DialogueConstants.translate(&"update.reload_project")
apply_theme()
func _on_download_update_panel_failed() -> void:
download_dialog.hide()
update_failed_dialog.dialog_text = DialogueConstants.translate(&"update.failed")
update_failed_dialog.popup_centered()
func _on_needs_reload_dialog_confirmed() -> void:
Engine.get_meta("DialogueManagerPlugin").get_editor_interface().restart_editor(true)
func _on_timer_timeout() -> void:
if not needs_reload:
check_for_update()

View File

@@ -0,0 +1,42 @@
[gd_scene load_steps=3 format=3 uid="uid://co8yl23idiwbi"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/update_button.gd" id="1_d2tpb"]
[ext_resource type="PackedScene" uid="uid://qdxrxv3c3hxk" path="res://addons/dialogue_manager/components/download_update_panel.tscn" id="2_iwm7r"]
[node name="UpdateButton" type="Button"]
visible = false
offset_right = 8.0
offset_bottom = 8.0
theme_override_colors/font_color = Color(0, 0, 0, 1)
theme_override_colors/font_hover_color = Color(0, 0, 0, 1)
theme_override_colors/font_focus_color = Color(0, 0, 0, 1)
text = "v2.9.0 available"
flat = true
script = ExtResource("1_d2tpb")
[node name="HTTPRequest" type="HTTPRequest" parent="."]
[node name="DownloadDialog" type="AcceptDialog" parent="."]
title = "Download update"
size = Vector2i(400, 300)
unresizable = true
min_size = Vector2i(300, 250)
ok_button_text = "Close"
[node name="DownloadUpdatePanel" parent="DownloadDialog" instance=ExtResource("2_iwm7r")]
[node name="UpdateFailedDialog" type="AcceptDialog" parent="."]
dialog_text = "You have been updated to version 2.4.3"
[node name="NeedsReloadDialog" type="ConfirmationDialog" parent="."]
[node name="Timer" type="Timer" parent="."]
wait_time = 14400.0
[connection signal="pressed" from="." to="." method="_on_update_button_pressed"]
[connection signal="request_completed" from="HTTPRequest" to="." method="_on_http_request_request_completed"]
[connection signal="close_requested" from="DownloadDialog" to="." method="_on_download_dialog_close_requested"]
[connection signal="failed" from="DownloadDialog/DownloadUpdatePanel" to="." method="_on_download_update_panel_failed"]
[connection signal="updated" from="DownloadDialog/DownloadUpdatePanel" to="." method="_on_download_update_panel_updated"]
[connection signal="confirmed" from="NeedsReloadDialog" to="." method="_on_needs_reload_dialog_confirmed"]
[connection signal="timeout" from="Timer" to="." method="_on_timer_timeout"]