Complete C# rewrite with working game in Editor (#6)

* Refactor collectable components to C# and update resource scripts for consistency

* Update resource paths and refactor properties for consistency

* Refactor UI components to inherit from Control and update node paths for consistency

* Update node paths and group assignments for consistency across scenes

* Refactor GameManager and PlayerDeathComponent for improved state management and logging; update scene connections for player death handling

* Add PhantomCamera components and UI elements for improved scene management; refactor existing components for better integration

* Refactor skill components and update resource paths for consistency; enhance skill management in scenes

* Add new UID files and update scene configurations for dialogue components; refactor skill management and input handling

* Add next level command and refactor player retrieval in GameManager; update scene files for consistency

* Add skill upgrade system and refactor skill components for enhanced functionality; update resource paths and configurations

* Enhance ChargeProgressBar and Marketplace functionality; add owner exit handling and update skill button states

* Refactor ChargeProgressBar and SkillManager; update skill handling and improve component interactions

* Refactor player and level configurations; streamline FlipPlayerComponent and reposition Spaceship Enter
This commit is contained in:
2025-08-27 01:12:26 +02:00
committed by GitHub
parent d84f7d1740
commit d786ef4c22
532 changed files with 22009 additions and 6630 deletions

View File

@@ -0,0 +1,610 @@
@tool
class_name DMCodeEdit extends CodeEdit
signal active_title_change(title: String)
signal error_clicked(line_number: int)
signal external_file_requested(path: String, title: String)
# 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 = DMSyntaxHighlighter.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")
var compiler_regex: DMCompilerRegEx = DMCompilerRegEx.new()
var _autoloads: Dictionary[String, String] = {}
var _autoload_member_cache: Dictionary[String, Dictionary] = {}
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 = DMSyntaxHighlighter.new()
# Keep track of any autoloads
ProjectSettings.settings_changed.connect(_on_project_settings_changed)
_on_project_settings_changed()
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)
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)
for file in files:
# Don't import the file into itself
if file == main_view.current_file_path: continue
if file.get_extension() == "dialogue":
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
else:
var cursor: Vector2 = get_line_column_at_pos(at_position)
if cursor.x > -1 and cursor.y > -1:
set_cursor(cursor)
remove_secondary_carets()
insert_text("\"%s\"" % file, cursor.y, cursor.x)
grab_focus()
func _request_code_completion(force: bool) -> void:
var cursor: Vector2 = get_cursor()
var current_line: String = get_line(cursor.y)
# Match jumps
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
for title: String in DMCompiler.get_titles_in_text(text, main_view.current_file_path):
# Ignore any imported titles that aren't resolved to human readable.
if title.to_int() > 0:
continue
elif "/" 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"))
# Match character names
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"))
# Match autoloads on mutation lines
for prefix in ["do ", "do! ", "set ", "if ", "elif ", "else if ", "match ", "when ", "using "]:
if (current_line.strip_edges().begins_with(prefix) and (cursor.x > current_line.find(prefix))):
var expression: String = current_line.substr(0, cursor.x).strip_edges().substr(3)
# Find the last couple of tokens
var possible_prompt: String = expression.reverse()
possible_prompt = possible_prompt.substr(0, possible_prompt.find(" "))
possible_prompt = possible_prompt.substr(0, possible_prompt.find("("))
possible_prompt = possible_prompt.reverse()
var segments: PackedStringArray = possible_prompt.split(".").slice(-2)
var auto_completes: Array[Dictionary] = []
# Autoloads and state shortcuts
if segments.size() == 1:
var prompt: String = segments[0]
for autoload in _autoloads.keys():
if matches_prompt(prompt, autoload):
auto_completes.append({
prompt = prompt,
text = autoload,
type = "script"
})
for autoload in get_state_shortcuts():
for member: Dictionary in get_members_for_autoload(autoload):
if matches_prompt(prompt, member.name):
auto_completes.append({
prompt = prompt,
text = member.name,
type = member.type
})
# Members of an autoload
elif segments[0] in _autoloads.keys() and not current_line.strip_edges().begins_with("using "):
var prompt: String = segments[1]
for member: Dictionary in get_members_for_autoload(segments[0]):
if matches_prompt(prompt, member.name):
auto_completes.append({
prompt = prompt,
text = member.name,
type = member.type
})
auto_completes.sort_custom(func(a, b): return a.text < b.text)
for auto_complete in auto_completes:
var icon: Texture2D
var text: String = auto_complete.text
match auto_complete.type:
"script":
icon = get_theme_icon("Script", "EditorIcons")
"property":
icon = get_theme_icon("MemberProperty", "EditorIcons")
"method":
icon = get_theme_icon("MemberMethod", "EditorIcons")
text += "()"
"signal":
icon = get_theme_icon("MemberSignal", "EditorIcons")
"constant":
icon = get_theme_icon("MemberConstant", "EditorIcons")
var insert: String = text.substr(auto_complete.prompt.length())
add_code_completion_option(CodeEdit.KIND_CLASS, text, insert, theme_overrides.text_color, icon)
update_code_completion_options(true)
if get_code_completion_options().size() == 0:
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
if completion.insert_text.length() > 0:
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()
if completion.display_text.ends_with("()"):
set_cursor(get_cursor() - Vector2.RIGHT)
# Close the autocomplete menu on the next tick
call_deferred("cancel_code_completion")
#region 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, false)
set_caret_column(from_cursor.x, false)
# 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())
func get_state_shortcuts() -> PackedStringArray:
# Get any shortcuts defined in settings
var shortcuts: PackedStringArray = DMSettings.get_setting(DMSettings.STATE_AUTOLOAD_SHORTCUTS, [])
# Check for "using" clauses
for line: String in text.split("\n"):
var found: RegExMatch = compiler_regex.USING_REGEX.search(line)
if found:
shortcuts.append(found.strings[found.names.state])
# Check for any other script sources
for extra_script_source in DMSettings.get_setting(DMSettings.EXTRA_AUTO_COMPLETE_SCRIPT_SOURCES, []):
shortcuts.append(extra_script_source)
return shortcuts
func get_members_for_autoload(autoload_name: String) -> Array[Dictionary]:
# Debounce method list lookups
if _autoload_member_cache.has(autoload_name) and _autoload_member_cache.get(autoload_name).get("at") > Time.get_ticks_msec() - 5000:
return _autoload_member_cache.get(autoload_name).get("members")
if not _autoloads.has(autoload_name) and not autoload_name.begins_with("res://") and not autoload_name.begins_with("uid://"): return []
var autoload = load(_autoloads.get(autoload_name, autoload_name))
var script: Script = autoload if autoload is Script else autoload.get_script()
if not is_instance_valid(script): return []
var members: Array[Dictionary] = []
if script.resource_path.ends_with(".gd"):
for m: Dictionary in script.get_script_method_list():
if not m.name.begins_with("@"):
members.append({
name = m.name,
type = "method"
})
for m: Dictionary in script.get_script_property_list():
members.append({
name = m.name,
type = "property"
})
for m: Dictionary in script.get_script_signal_list():
members.append({
name = m.name,
type = "signal"
})
for c: String in script.get_script_constant_map():
members.append({
name = c,
type = "constant"
})
elif script.resource_path.ends_with(".cs"):
var dotnet = load(Engine.get_meta("DialogueManagerPlugin").get_plugin_path() + "/DialogueManager.cs").new()
for m: Dictionary in dotnet.GetMembersForAutoload(script):
members.append(m)
_autoload_member_cache[autoload_name] = {
at = Time.get_ticks_msec(),
members = members
}
return members
## 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.strip_edges().begins_with("~ "):
titles.append(line.strip_edges().substr(2))
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:
# Lines display counting from 1 but are actually indexed from 0
line_number -= 1
if line_number < 0: return
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 starting_scroll := scroll_vertical
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
set_cursor(cursor)
from += offset
to += offset
if reselect:
select(from, 0, to, get_line_width(to))
text_changed.emit()
scroll_vertical = starting_scroll + offset
#endregion
#region Signals
func _on_project_settings_changed() -> void:
_autoloads = {}
var project = ConfigFile.new()
project.load("res://project.godot")
for autoload in project.get_section_keys("autoload"):
if autoload != "DialogueManager":
_autoloads[autoload] = project.get_value("autoload", autoload).substr(1)
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)
#endregion

View File

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

View File

@@ -0,0 +1,56 @@
[gd_scene load_steps=4 format=3 uid="uid://civ6shmka5e8u"]
[ext_resource type="Script" uid="uid://klpiq4tk3t7a" path="res://addons/dialogue_manager/components/code_edit_syntax_highlighter.gd" id="1_58cfo"]
[ext_resource type="Script" uid="uid://djeybvlb332mp" 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,231 @@
@tool
class_name DMSyntaxHighlighter extends SyntaxHighlighter
var regex: DMCompilerRegEx = DMCompilerRegEx.new()
var compilation: DMCompilation = DMCompilation.new()
var expression_parser = DMExpressionParser.new()
var cache: Dictionary = {}
func _clear_highlighting_cache() -> void:
cache.clear()
func _get_line_syntax_highlighting(line: int) -> Dictionary:
expression_parser.include_comments = true
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]
var theme: Dictionary = text_edit.theme_overrides
var index: int = 0
match DMCompiler.get_line_type(text):
DMConstants.TYPE_USING:
colors[index] = { color = theme.conditions_color }
colors[index + "using ".length()] = { color = theme.text_color }
DMConstants.TYPE_IMPORT:
colors[index] = { color = theme.conditions_color }
var import: RegExMatch = regex.IMPORT_REGEX.search(text)
if import:
colors[index + import.get_start("path") - 1] = { color = theme.strings_color }
colors[index + import.get_end("path") + 1] = { color = theme.conditions_color }
colors[index + import.get_start("prefix")] = { color = theme.text_color }
DMConstants.TYPE_COMMENT:
colors[index] = { color = theme.comments_color }
DMConstants.TYPE_TITLE:
colors[index] = { color = theme.titles_color }
DMConstants.TYPE_CONDITION, DMConstants.TYPE_WHILE, DMConstants.TYPE_MATCH, DMConstants.TYPE_WHEN:
colors[0] = { color = theme.conditions_color }
index = text.find(" ")
if index > -1:
var expression: Array = expression_parser.tokenise(text.substr(index), DMConstants.TYPE_CONDITION, 0)
if expression.size() == 0:
colors[index] = { color = theme.critical_color }
else:
_highlight_expression(expression, colors, index)
DMConstants.TYPE_MUTATION:
colors[0] = { color = theme.mutations_color }
index = text.find(" ")
var expression: Array = expression_parser.tokenise(text.substr(index), DMConstants.TYPE_MUTATION, 0)
if expression.size() == 0:
colors[index] = { color = theme.critical_color }
else:
_highlight_expression(expression, colors, index)
DMConstants.TYPE_GOTO:
if text.strip_edges().begins_with("%"):
colors[index] = { color = theme.symbols_color }
index = text.find(" ")
_highlight_goto(text, colors, index)
DMConstants.TYPE_RANDOM:
colors[index] = { color = theme.symbols_color }
DMConstants.TYPE_DIALOGUE, DMConstants.TYPE_RESPONSE:
if text.strip_edges().begins_with("%"):
colors[index] = { color = theme.symbols_color }
index = text.find(" ", text.find("%"))
colors[index] = { color = theme.text_color.lerp(theme.symbols_color, 0.5) }
var dialogue_text: String = text.substr(index, text.find("=>"))
# Highlight character name (but ignore ":" within line ID reference)
var split_index: int = dialogue_text.replace("\\:", "??").find(":")
if text.substr(split_index - 3, 3) != "[ID":
colors[index + split_index + 1] = { color = theme.text_color }
else:
# If there's no character name then just highlight the text as dialogue.
colors[index] = { color = theme.text_color }
# Interpolation
var replacements: Array[RegExMatch] = regex.REPLACEMENTS_REGEX.search_all(dialogue_text)
for replacement: RegExMatch in replacements:
var expression_text: String = replacement.get_string().substr(0, replacement.get_string().length() - 2).substr(2)
var expression: Array = expression_parser.tokenise(expression_text, DMConstants.TYPE_MUTATION, replacement.get_start())
var expression_index: int = index + replacement.get_start()
colors[expression_index] = { color = theme.symbols_color }
if expression.size() == 0 or expression[0].type == DMConstants.TYPE_ERROR:
colors[expression_index] = { color = theme.critical_color }
else:
_highlight_expression(expression, colors, index + 2)
colors[expression_index + expression_text.length() + 2] = { color = theme.symbols_color }
colors[expression_index + expression_text.length() + 4] = { color = theme.text_color }
# Tags (and inline mutations)
var resolved_line_data: DMResolvedLineData = DMResolvedLineData.new("")
var bbcodes: Array[Dictionary] = resolved_line_data.find_bbcode_positions_in_string(dialogue_text, true, true)
for bbcode: Dictionary in bbcodes:
var tag: String = bbcode.code
var code: String = bbcode.raw_args
if code.begins_with("["):
colors[index + bbcode.start] = { color = theme.symbols_color }
colors[index + bbcode.start + 2] = { color = theme.text_color }
var pipe_cursor: int = code.find("|")
while pipe_cursor > -1:
colors[index + bbcode.start + pipe_cursor + 1] = { color = theme.symbols_color }
colors[index + bbcode.start + pipe_cursor + 2] = { color = theme.text_color }
pipe_cursor = code.find("|", pipe_cursor + 1)
colors[index + bbcode.end - 1] = { color = theme.symbols_color }
colors[index + bbcode.end + 1] = { color = theme.text_color }
else:
colors[index + bbcode.start] = { color = theme.symbols_color }
if tag.begins_with("do") or tag.begins_with("set") or tag.begins_with("if"):
if tag.begins_with("if"):
colors[index + bbcode.start + 1] = { color = theme.conditions_color }
else:
colors[index + bbcode.start + 1] = { color = theme.mutations_color }
var expression: Array = expression_parser.tokenise(code, DMConstants.TYPE_MUTATION, bbcode.start + bbcode.code.length())
if expression.size() == 0 or expression[0].type == DMConstants.TYPE_ERROR:
colors[index + bbcode.start + tag.length() + 1] = { color = theme.critical_color }
else:
_highlight_expression(expression, colors, index + 2)
# else and closing if have no expression
elif tag.begins_with("else") or tag.begins_with("/if"):
colors[index + bbcode.start + 1] = { color = theme.conditions_color }
colors[index + bbcode.end] = { color = theme.symbols_color }
colors[index + bbcode.end + 1] = { color = theme.text_color }
# Jumps
if "=> " in text or "=>< " in text:
_highlight_goto(text, colors, index)
# Order the dictionary keys to prevent CodeEdit from having issues
var ordered_colors: Dictionary = {}
var ordered_keys: Array = colors.keys()
ordered_keys.sort()
for key_index: int in ordered_keys:
ordered_colors[key_index] = colors[key_index]
cache[text] = ordered_colors
return ordered_colors
func _highlight_expression(tokens: Array, colors: Dictionary, index: int) -> int:
var theme: Dictionary = get_text_edit().theme_overrides
var last_index: int = index
for token: Dictionary in tokens:
last_index = token.i
match token.type:
DMConstants.TOKEN_COMMENT:
colors[index + token.i] = { color = theme.comments_color }
DMConstants.TOKEN_CONDITION, DMConstants.TOKEN_AND_OR:
colors[index + token.i] = { color = theme.conditions_color }
DMConstants.TOKEN_VARIABLE:
if token.value in ["true", "false"]:
colors[index + token.i] = { color = theme.conditions_color }
else:
colors[index + token.i] = { color = theme.members_color }
DMConstants.TOKEN_OPERATOR, DMConstants.TOKEN_COLON, \
DMConstants.TOKEN_COMMA, DMConstants.TOKEN_DOT, DMConstants.TOKEN_NULL_COALESCE, \
DMConstants.TOKEN_NUMBER, DMConstants.TOKEN_ASSIGNMENT:
colors[index + token.i] = { color = theme.symbols_color }
DMConstants.TOKEN_STRING:
colors[index + token.i] = { color = theme.strings_color }
DMConstants.TOKEN_FUNCTION:
colors[index + token.i] = { color = theme.mutations_color }
colors[index + token.i + token.function.length()] = { color = theme.symbols_color }
for parameter: Array in token.value:
last_index = _highlight_expression(parameter, colors, index)
DMConstants.TOKEN_PARENS_CLOSE:
colors[index + token.i] = { color = theme.symbols_color }
DMConstants.TOKEN_DICTIONARY_REFERENCE:
colors[index + token.i] = { color = theme.members_color }
colors[index + token.i + token.variable.length()] = { color = theme.symbols_color }
last_index = _highlight_expression(token.value, colors, index)
DMConstants.TOKEN_ARRAY:
colors[index + token.i] = { color = theme.symbols_color }
for item: Array in token.value:
last_index = _highlight_expression(item, colors, index)
DMConstants.TOKEN_BRACKET_CLOSE:
colors[index + token.i] = { color = theme.symbols_color }
DMConstants.TOKEN_DICTIONARY:
colors[index + token.i] = { color = theme.symbols_color }
last_index = _highlight_expression(token.value.keys() + token.value.values(), colors, index)
DMConstants.TOKEN_BRACE_CLOSE:
colors[index + token.i] = { color = theme.symbols_color }
last_index += 1
DMConstants.TOKEN_GROUP:
last_index = _highlight_expression(token.value, colors, index)
return last_index
func _highlight_goto(text: String, colors: Dictionary, index: int) -> int:
var theme: Dictionary = get_text_edit().theme_overrides
var goto_data: DMResolvedGotoData = DMResolvedGotoData.new(text, {})
colors[goto_data.index] = { color = theme.jumps_color }
if "{{" in text:
index = text.find("{{", goto_data.index)
var last_index: int = 0
if goto_data.error:
colors[index + 2] = { color = theme.critical_color }
else:
last_index = _highlight_expression(goto_data.expression, colors, index)
index = text.find("}}", index + last_index)
colors[index] = { color = theme.jumps_color }
return index

View File

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

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://tests/test_basic_dialogue.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 @@
uid://kpwo418lb2t2

View File

@@ -0,0 +1,60 @@
[gd_scene load_steps=3 format=3 uid="uid://qdxrxv3c3hxk"]
[ext_resource type="Script" uid="uid://kpwo418lb2t2" 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 @@
uid://nyypeje1a036

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("DMCache").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):
EditorInterface.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("DMCache").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:
EditorInterface.call_deferred("edit_resource", resource)
ITEM_CLEAR:
resource_changed.emit(null)
ITEM_FILESYSTEM:
var file_system = EditorInterface.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 @@
uid://dooe2pflnqtve

View File

@@ -0,0 +1,58 @@
[gd_scene load_steps=4 format=3 uid="uid://ycn6uaj7dsrh"]
[ext_resource type="Script" uid="uid://dooe2pflnqtve" 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 @@
uid://damhqta55t67c

View File

@@ -0,0 +1,9 @@
[gd_scene load_steps=2 format=3 uid="uid://b16uuqjuof3n5"]
[ext_resource type="Script" uid="uid://damhqta55t67c" 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, 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:
error_pressed.emit(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 @@
uid://d2l8nlb6hhrfp

View File

@@ -0,0 +1,56 @@
[gd_scene load_steps=4 format=3 uid="uid://cs8pwrxr5vxix"]
[ext_resource type="Script" uid="uid://d2l8nlb6hhrfp" path="res://addons/dialogue_manager/components/errors_panel.gd" id="1_nfm3c"]
[sub_resource type="Image" id="Image_w0gko"]
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_w0gko")
[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,150 @@
@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 last_selected_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)
last_selected_file_path = file
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")
if is_instance_valid(list):
list.add_theme_stylebox_override("panel", get_theme_stylebox("panel", "Panel"))
### 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 @@
uid://dqa4a4wwoo0aa

View File

@@ -0,0 +1,29 @@
[gd_scene load_steps=3 format=3 uid="uid://dnufpcdrreva3"]
[ext_resource type="Script" uid="uid://dqa4a4wwoo0aa" 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
size_flags_vertical = 3
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.notice_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("DMCache")
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 @@
uid://q368fmxxa8sd

View File

@@ -0,0 +1,139 @@
[gd_scene load_steps=3 format=3 uid="uid://0n7hwviyyly4"]
[ext_resource type="Script" uid="uid://q368fmxxa8sd" 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,218 @@
@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)
#region 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]
code_edit.begin_complex_operation()
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)
code_edit.end_complex_operation()
code_edit.text_changed.emit()
search(input.text, result_index)
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()
#endregion

View File

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

View File

@@ -0,0 +1,87 @@
[gd_scene load_steps=2 format=3 uid="uid://gr8nakpbrhby"]
[ext_resource type="Script" uid="uid://cijsmjkq21cdq" 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,69 @@
@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")
if is_instance_valid(list):
list.add_theme_stylebox_override("panel", get_theme_stylebox("panel", "Panel"))
### 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 @@
uid://d0k2wndjj0ifm

View File

@@ -0,0 +1,27 @@
[gd_scene load_steps=2 format=3 uid="uid://ctns6ouwwd68i"]
[ext_resource type="Script" uid="uid://d0k2wndjj0ifm" 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:
EditorInterface.restart_editor(true)
else:
var scale: float = EditorInterface.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:
EditorInterface.restart_editor(true)
func _on_timer_timeout() -> void:
if not needs_reload:
check_for_update()

View File

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

View File

@@ -0,0 +1,42 @@
[gd_scene load_steps=3 format=3 uid="uid://co8yl23idiwbi"]
[ext_resource type="Script" uid="uid://cr1tt12dh5ecr" 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"]