lakewood/addons/dialogue_manager/views/main_view.gd

1135 lines
39 KiB
GDScript3
Raw Normal View History

2024-09-07 14:12:04 -04:00
@tool
extends Control
const DialogueConstants = preload("../constants.gd")
const DialogueSettings = preload("../settings.gd")
const DialogueResource = preload("../dialogue_resource.gd")
const DialogueManagerParser = preload("../components/parser.gd")
const OPEN_OPEN = 100
const OPEN_CLEAR = 101
const TRANSLATIONS_GENERATE_LINE_IDS = 100
const TRANSLATIONS_SAVE_CHARACTERS_TO_CSV = 201
const TRANSLATIONS_SAVE_TO_CSV = 202
const TRANSLATIONS_IMPORT_FROM_CSV = 203
const ITEM_SAVE = 100
const ITEM_SAVE_AS = 101
const ITEM_CLOSE = 102
const ITEM_CLOSE_ALL = 103
const ITEM_CLOSE_OTHERS = 104
const ITEM_COPY_PATH = 200
const ITEM_SHOW_IN_FILESYSTEM = 201
enum TranslationSource {
CharacterNames,
Lines
}
signal confirmation_closed()
@onready var parse_timer := $ParseTimer
# Dialogs
@onready var new_dialog: FileDialog = $NewDialog
@onready var save_dialog: FileDialog = $SaveDialog
@onready var open_dialog: FileDialog = $OpenDialog
@onready var export_dialog: FileDialog = $ExportDialog
@onready var import_dialog: FileDialog = $ImportDialog
@onready var errors_dialog: AcceptDialog = $ErrorsDialog
@onready var settings_dialog: AcceptDialog = $SettingsDialog
@onready var settings_view := $SettingsDialog/SettingsView
@onready var build_error_dialog: AcceptDialog = $BuildErrorDialog
@onready var close_confirmation_dialog: ConfirmationDialog = $CloseConfirmationDialog
@onready var updated_dialog: AcceptDialog = $UpdatedDialog
@onready var find_in_files_dialog: AcceptDialog = $FindInFilesDialog
@onready var find_in_files: Control = $FindInFilesDialog/FindInFiles
# Toolbar
@onready var new_button: Button = %NewButton
@onready var open_button: MenuButton = %OpenButton
@onready var save_all_button: Button = %SaveAllButton
@onready var find_in_files_button: Button = %FindInFilesButton
@onready var test_button: Button = %TestButton
@onready var search_button: Button = %SearchButton
@onready var insert_button: MenuButton = %InsertButton
@onready var translations_button: MenuButton = %TranslationsButton
@onready var settings_button: Button = %SettingsButton
@onready var support_button: Button = %SupportButton
@onready var docs_button: Button = %DocsButton
@onready var version_label: Label = %VersionLabel
@onready var update_button: Button = %UpdateButton
@onready var search_and_replace := %SearchAndReplace
# Code editor
@onready var content: HSplitContainer = %Content
@onready var files_list := %FilesList
@onready var files_popup_menu: PopupMenu = %FilesPopupMenu
@onready var title_list := %TitleList
@onready var code_edit := %CodeEdit
@onready var errors_panel := %ErrorsPanel
# The currently open file
var current_file_path: String = "":
set(next_current_file_path):
current_file_path = next_current_file_path
files_list.current_file_path = current_file_path
if current_file_path == "":
save_all_button.disabled = true
test_button.disabled = true
search_button.disabled = true
insert_button.disabled = true
translations_button.disabled = true
content.dragger_visibility = SplitContainer.DRAGGER_HIDDEN
files_list.hide()
title_list.hide()
code_edit.hide()
errors_panel.hide()
else:
test_button.disabled = false
search_button.disabled = false
insert_button.disabled = false
translations_button.disabled = false
content.dragger_visibility = SplitContainer.DRAGGER_VISIBLE
files_list.show()
title_list.show()
code_edit.show()
code_edit.text = open_buffers[current_file_path].text
code_edit.errors = []
code_edit.clear_undo_history()
code_edit.set_cursor(DialogueSettings.get_caret(current_file_path))
code_edit.grab_focus()
_on_code_edit_text_changed()
errors_panel.errors = []
code_edit.errors = []
get:
return current_file_path
# A reference to the currently open files and their last saved text
var open_buffers: Dictionary = {}
# Which thing are we exporting translations for?
var translation_source: TranslationSource = TranslationSource.Lines
var plugin: EditorPlugin
func _ready() -> void:
plugin = Engine.get_meta("DialogueManagerPlugin")
apply_theme()
# Start with nothing open
self.current_file_path = ""
# Set up the update checker
version_label.text = "v%s" % plugin.get_version()
update_button.on_before_refresh = func on_before_refresh():
# Save everything
DialogueSettings.set_user_value("just_refreshed", {
current_file_path = current_file_path,
open_buffers = open_buffers
})
return true
# Did we just load from an addon version refresh?
var just_refreshed = DialogueSettings.get_user_value("just_refreshed", null)
if just_refreshed != null:
DialogueSettings.set_user_value("just_refreshed", null)
call_deferred("load_from_version_refresh", just_refreshed)
# Hook up the search toolbar
search_and_replace.code_edit = code_edit
# Connect menu buttons
insert_button.get_popup().id_pressed.connect(_on_insert_button_menu_id_pressed)
translations_button.get_popup().id_pressed.connect(_on_translations_button_menu_id_pressed)
code_edit.main_view = self
code_edit.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY if DialogueSettings.get_setting("wrap_lines", false) else TextEdit.LINE_WRAPPING_NONE
var editor_settings: EditorSettings = plugin.get_editor_interface().get_editor_settings()
editor_settings.settings_changed.connect(_on_editor_settings_changed)
_on_editor_settings_changed()
# Reopen any files that were open when Godot was closed
if editor_settings.get_setting("text_editor/behavior/files/restore_scripts_on_load"):
var reopen_files: Array = DialogueSettings.get_user_value("reopen_files", [])
for reopen_file in reopen_files:
open_file(reopen_file)
self.current_file_path = DialogueSettings.get_user_value("most_recent_reopen_file", "")
save_all_button.disabled = true
close_confirmation_dialog.ok_button_text = DialogueConstants.translate(&"confirm_close.save")
close_confirmation_dialog.add_button(DialogueConstants.translate(&"confirm_close.discard"), true, "discard")
errors_dialog.dialog_text = DialogueConstants.translate(&"errors_in_script")
# Update the buffer if a file was modified externally (retains undo step)
Engine.get_meta("DialogueCache").file_content_changed.connect(_on_cache_file_content_changed)
plugin.get_editor_interface().get_file_system_dock().files_moved.connect(_on_files_moved)
func _exit_tree() -> void:
DialogueSettings.set_user_value("reopen_files", open_buffers.keys())
DialogueSettings.set_user_value("most_recent_reopen_file", self.current_file_path)
func _unhandled_input(event: InputEvent) -> void:
if not visible: return
if event is InputEventKey and event.is_pressed():
var shortcut: String = plugin.get_editor_shortcut(event)
match shortcut:
"close_file":
get_viewport().set_input_as_handled()
close_file(current_file_path)
"save":
get_viewport().set_input_as_handled()
save_file(current_file_path)
"find_in_files":
get_viewport().set_input_as_handled()
_on_find_in_files_button_pressed()
"run_test_scene":
get_viewport().set_input_as_handled()
_on_test_button_pressed()
func apply_changes() -> void:
save_files()
# Load back to the previous buffer regardless of if it was actually saved
func load_from_version_refresh(just_refreshed: Dictionary) -> void:
if just_refreshed.has("current_file_content"):
# We just loaded from a version before multiple buffers
var file: FileAccess = FileAccess.open(just_refreshed.current_file_path, FileAccess.READ)
var file_text: String = file.get_as_text()
open_buffers[just_refreshed.current_file_path] = {
pristine_text = file_text,
text = just_refreshed.current_file_content
}
else:
open_buffers = just_refreshed.open_buffers
var interface: EditorInterface = plugin.get_editor_interface()
if just_refreshed.current_file_path != "":
interface.edit_resource(load(just_refreshed.current_file_path))
else:
interface.set_main_screen_editor("Dialogue")
updated_dialog.dialog_text = DialogueConstants.translate(&"update.success").format({ version = update_button.get_version() })
updated_dialog.popup_centered()
func new_file(path: String, content: String = "") -> void:
if open_buffers.has(path):
remove_file_from_open_buffers(path)
var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
if content == "":
if DialogueSettings.get_setting("new_with_template", true):
file.store_string("\n".join([
"~ this_is_a_node_title",
"",
"Nathan: [[Hi|Hello|Howdy]], this is some dialogue.",
"Nathan: Here are some choices.",
"- First one",
"\tNathan: You picked the first one.",
"- Second one",
"\tNathan: You picked the second one.",
"- Start again => this_is_a_node_title",
"- End the conversation => END",
"Nathan: For more information see the online documentation.",
"",
"=> END"
]))
else:
file.store_string(content)
plugin.get_editor_interface().get_resource_filesystem().scan()
# Open a dialogue resource for editing
func open_resource(resource: DialogueResource) -> void:
open_file(resource.resource_path)
func open_file(path: String) -> void:
if not open_buffers.has(path):
var file: FileAccess = FileAccess.open(path, FileAccess.READ)
var text = file.get_as_text()
open_buffers[path] = {
cursor = Vector2.ZERO,
text = text,
pristine_text = text
}
DialogueSettings.add_recent_file(path)
build_open_menu()
files_list.files = open_buffers.keys()
files_list.select_file(path)
self.current_file_path = path
func show_file_in_filesystem(path: String) -> void:
var file_system_dock: FileSystemDock = plugin \
.get_editor_interface() \
.get_file_system_dock()
file_system_dock.navigate_to_path(path)
# Save any open files
func save_files() -> void:
save_all_button.disabled = true
var saved_files: PackedStringArray = []
for path in open_buffers:
if open_buffers[path].text != open_buffers[path].pristine_text:
saved_files.append(path)
save_file(path, false)
if saved_files.size() > 0:
Engine.get_meta("DialogueCache").reimport_files(saved_files)
# Save a file
func save_file(path: String, rescan_file_system: bool = true) -> void:
var buffer = open_buffers[path]
files_list.mark_file_as_unsaved(path, false)
save_all_button.disabled = files_list.unsaved_files.size() == 0
# Don't bother saving if there is nothing to save
if buffer.text == buffer.pristine_text:
return
buffer.pristine_text = buffer.text
# Save the current text
var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
file.store_string(buffer.text)
file.close()
if rescan_file_system:
plugin \
.get_editor_interface() \
.get_resource_filesystem()\
.scan()
func close_file(path: String) -> void:
if not path in open_buffers.keys(): return
var buffer = open_buffers[path]
if buffer.text == buffer.pristine_text:
remove_file_from_open_buffers(path)
await get_tree().process_frame
else:
close_confirmation_dialog.dialog_text = DialogueConstants.translate(&"confirm_close").format({ path = path.get_file() })
close_confirmation_dialog.popup_centered()
await confirmation_closed
func remove_file_from_open_buffers(path: String) -> void:
if not path in open_buffers.keys(): return
var current_index = open_buffers.keys().find(current_file_path)
open_buffers.erase(path)
if open_buffers.size() == 0:
self.current_file_path = ""
else:
current_index = clamp(current_index, 0, open_buffers.size() - 1)
self.current_file_path = open_buffers.keys()[current_index]
files_list.files = open_buffers.keys()
# Apply theme colors and icons to the UI
func apply_theme() -> void:
if is_instance_valid(plugin) and is_instance_valid(code_edit):
var scale: float = plugin.get_editor_interface().get_editor_scale()
var editor_settings = plugin.get_editor_interface().get_editor_settings()
code_edit.theme_overrides = {
scale = scale,
background_color = editor_settings.get_setting("text_editor/theme/highlighting/background_color"),
current_line_color = editor_settings.get_setting("text_editor/theme/highlighting/current_line_color"),
error_line_color = editor_settings.get_setting("text_editor/theme/highlighting/mark_color"),
critical_color = editor_settings.get_setting("text_editor/theme/highlighting/comment_markers/critical_color"),
notice_color = editor_settings.get_setting("text_editor/theme/highlighting/comment_markers/notice_color"),
titles_color = editor_settings.get_setting("text_editor/theme/highlighting/control_flow_keyword_color"),
text_color = editor_settings.get_setting("text_editor/theme/highlighting/text_color"),
conditions_color = editor_settings.get_setting("text_editor/theme/highlighting/keyword_color"),
mutations_color = editor_settings.get_setting("text_editor/theme/highlighting/function_color"),
members_color = editor_settings.get_setting("text_editor/theme/highlighting/member_variable_color"),
strings_color = editor_settings.get_setting("text_editor/theme/highlighting/string_color"),
numbers_color = editor_settings.get_setting("text_editor/theme/highlighting/number_color"),
symbols_color = editor_settings.get_setting("text_editor/theme/highlighting/symbol_color"),
comments_color = editor_settings.get_setting("text_editor/theme/highlighting/comment_color"),
jumps_color = Color(editor_settings.get_setting("text_editor/theme/highlighting/control_flow_keyword_color"), 0.7),
font_size = editor_settings.get_setting("interface/editor/code_font_size")
}
new_button.icon = get_theme_icon("New", "EditorIcons")
new_button.tooltip_text = DialogueConstants.translate(&"start_a_new_file")
open_button.icon = get_theme_icon("Load", "EditorIcons")
open_button.tooltip_text = DialogueConstants.translate(&"open_a_file")
save_all_button.icon = get_theme_icon("Save", "EditorIcons")
save_all_button.tooltip_text = DialogueConstants.translate(&"start_all_files")
find_in_files_button.icon = get_theme_icon("ViewportZoom", "EditorIcons")
find_in_files_button.tooltip_text = DialogueConstants.translate(&"find_in_files")
test_button.icon = get_theme_icon("PlayScene", "EditorIcons")
test_button.tooltip_text = DialogueConstants.translate(&"test_dialogue")
search_button.icon = get_theme_icon("Search", "EditorIcons")
search_button.tooltip_text = DialogueConstants.translate(&"search_for_text")
insert_button.icon = get_theme_icon("RichTextEffect", "EditorIcons")
insert_button.text = DialogueConstants.translate(&"insert")
translations_button.icon = get_theme_icon("Translation", "EditorIcons")
translations_button.text = DialogueConstants.translate(&"translations")
settings_button.icon = get_theme_icon("Tools", "EditorIcons")
settings_button.tooltip_text = DialogueConstants.translate(&"settings")
support_button.icon = get_theme_icon("Heart", "EditorIcons")
support_button.text = DialogueConstants.translate(&"sponsor")
support_button.tooltip_text = DialogueConstants.translate(&"show_support")
docs_button.icon = get_theme_icon("Help", "EditorIcons")
docs_button.text = DialogueConstants.translate(&"docs")
update_button.apply_theme()
# Set up the effect menu
var popup: PopupMenu = insert_button.get_popup()
popup.clear()
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.wave_bbcode"), 0)
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.shake_bbcode"), 1)
popup.add_separator()
popup.add_icon_item(get_theme_icon("Time", "EditorIcons"), DialogueConstants.translate(&"insert.typing_pause"), 3)
popup.add_icon_item(get_theme_icon("ViewportSpeed", "EditorIcons"), DialogueConstants.translate(&"insert.typing_speed_change"), 4)
popup.add_icon_item(get_theme_icon("DebugNext", "EditorIcons"), DialogueConstants.translate(&"insert.auto_advance"), 5)
popup.add_separator(DialogueConstants.translate(&"insert.templates"))
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.title"), 6)
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.dialogue"), 7)
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.response"), 8)
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.random_lines"), 9)
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.random_text"), 10)
popup.add_separator(DialogueConstants.translate(&"insert.actions"))
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.jump"), 11)
popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.end_dialogue"), 12)
# Set up the translations menu
popup = translations_button.get_popup()
popup.clear()
popup.add_icon_item(get_theme_icon("Translation", "EditorIcons"), DialogueConstants.translate(&"generate_line_ids"), TRANSLATIONS_GENERATE_LINE_IDS)
popup.add_separator()
popup.add_icon_item(get_theme_icon("FileList", "EditorIcons"), DialogueConstants.translate(&"save_characters_to_csv"), TRANSLATIONS_SAVE_CHARACTERS_TO_CSV)
popup.add_icon_item(get_theme_icon("FileList", "EditorIcons"), DialogueConstants.translate(&"save_to_csv"), TRANSLATIONS_SAVE_TO_CSV)
popup.add_icon_item(get_theme_icon("AssetLib", "EditorIcons"), DialogueConstants.translate(&"import_from_csv"), TRANSLATIONS_IMPORT_FROM_CSV)
# Dialog sizes
new_dialog.min_size = Vector2(600, 500) * scale
save_dialog.min_size = Vector2(600, 500) * scale
open_dialog.min_size = Vector2(600, 500) * scale
export_dialog.min_size = Vector2(600, 500) * scale
import_dialog.min_size = Vector2(600, 500) * scale
settings_dialog.min_size = Vector2(1000, 600) * scale
settings_dialog.max_size = Vector2(1000, 600) * scale
find_in_files_dialog.min_size = Vector2(800, 600) * scale
### Helpers
# Refresh the open menu with the latest files
func build_open_menu() -> void:
var menu = open_button.get_popup()
menu.clear()
menu.add_icon_item(get_theme_icon("Load", "EditorIcons"), DialogueConstants.translate(&"open.open"), OPEN_OPEN)
menu.add_separator()
var recent_files = DialogueSettings.get_recent_files()
if recent_files.size() == 0:
menu.add_item(DialogueConstants.translate(&"open.no_recent_files"))
menu.set_item_disabled(2, true)
else:
for path in recent_files:
if FileAccess.file_exists(path):
menu.add_icon_item(get_theme_icon("File", "EditorIcons"), path)
menu.add_separator()
menu.add_item(DialogueConstants.translate(&"open.clear_recent_files"), OPEN_CLEAR)
if menu.id_pressed.is_connected(_on_open_menu_id_pressed):
menu.id_pressed.disconnect(_on_open_menu_id_pressed)
menu.id_pressed.connect(_on_open_menu_id_pressed)
# Get the last place a CSV, etc was exported
func get_last_export_path(extension: String) -> String:
var filename = current_file_path.get_file().replace(".dialogue", "." + extension)
return DialogueSettings.get_user_value("last_export_path", current_file_path.get_base_dir()) + "/" + filename
# Check the current text for errors
func parse() -> void:
# Skip if nothing to parse
if current_file_path == "": return
var parser = DialogueManagerParser.new()
var errors: Array[Dictionary] = []
if parser.parse(code_edit.text, current_file_path) != OK:
errors = parser.get_errors()
code_edit.errors = errors
errors_panel.errors = errors
parser.free()
func show_build_error_dialog() -> void:
build_error_dialog.dialog_text = DialogueConstants.translate(&"errors_with_build")
build_error_dialog.popup_centered()
# Generate translation line IDs for any line that doesn't already have one
func generate_translations_keys() -> void:
randomize()
seed(Time.get_unix_time_from_system())
var parser = DialogueManagerParser.new()
var cursor: Vector2 = code_edit.get_cursor()
var lines: PackedStringArray = code_edit.text.split("\n")
var key_regex = RegEx.new()
key_regex.compile("\\[ID:(?<key>.*?)\\]")
# Make list of known keys
var known_keys = {}
for i in range(0, lines.size()):
var line = lines[i]
var found = key_regex.search(line)
if found:
var text = ""
var l = line.replace(found.strings[0], "").strip_edges().strip_edges()
if l.begins_with("- "):
text = parser.extract_response_prompt(l)
elif ":" in l:
text = l.split(":")[1]
else:
text = l
known_keys[found.strings[found.names.get("key")]] = text
# Add in any that are missing
for i in lines.size():
var line = lines[i]
var l = line.strip_edges()
if parser.is_line_empty(l): continue
if parser.is_condition_line(l, true): continue
if parser.is_title_line(l): continue
if parser.is_mutation_line(l): continue
if parser.is_goto_line(l): continue
if parser.is_import_line(l): continue
if "[ID:" in line: continue
var key = "t" + str(randi() % 1000000).sha1_text().substr(0, 10)
while key in known_keys:
key = "t" + str(randi() % 1000000).sha1_text().substr(0, 10)
var text = ""
if l.begins_with("- "):
text = parser.extract_response_prompt(l)
else:
text = l.substr(l.find(":") + 1)
lines[i] = line.replace(text, text + " [ID:%s]" % key)
known_keys[key] = text
code_edit.text = "\n".join(lines)
code_edit.set_cursor(cursor)
_on_code_edit_text_changed()
parser.free()
# Add a translation file to the project settings
func add_path_to_project_translations(path: String) -> void:
var translations: PackedStringArray = ProjectSettings.get_setting("internationalization/locale/translations")
if not path in translations:
translations.append(path)
ProjectSettings.save()
# Export dialogue and responses to CSV
func export_translations_to_csv(path: String) -> void:
var default_locale: String = DialogueSettings.get_setting("default_csv_locale", "en")
var file: FileAccess
# If the file exists, open it first and work out which keys are already in it
var existing_csv: Dictionary = {}
var column_count: int = 2
var default_locale_column: int = 1
var character_column: int = -1
var notes_column: int = -1
if FileAccess.file_exists(path):
file = FileAccess.open(path, FileAccess.READ)
var is_first_line = true
var line: Array
while !file.eof_reached():
line = file.get_csv_line()
if is_first_line:
is_first_line = false
column_count = line.size()
for i in range(1, line.size()):
if line[i] == default_locale:
default_locale_column = i
elif line[i] == "_character":
character_column = i
elif line[i] == "_notes":
notes_column = i
# Make sure the line isn't empty before adding it
if line.size() > 0 and line[0].strip_edges() != "":
existing_csv[line[0]] = line
# The character column wasn't found in the existing file but the setting is turned on
if character_column == -1 and DialogueSettings.get_setting("include_character_in_translation_exports", false):
character_column = column_count
column_count += 1
existing_csv["keys"].append("_character")
# The notes column wasn't found in the existing file but the setting is turned on
if notes_column == -1 and DialogueSettings.get_setting("include_notes_in_translation_exports", false):
notes_column = column_count
column_count += 1
existing_csv["keys"].append("_notes")
# Start a new file
file = FileAccess.open(path, FileAccess.WRITE)
if not FileAccess.file_exists(path):
var headings: PackedStringArray = ["keys", default_locale]
if DialogueSettings.get_setting("include_character_in_translation_exports", false):
character_column = headings.size()
headings.append("_character")
if DialogueSettings.get_setting("include_notes_in_translation_exports", false):
notes_column = headings.size()
headings.append("_notes")
file.store_csv_line(headings)
column_count = headings.size()
# Write our translations to file
var known_keys: PackedStringArray = []
var dialogue: Dictionary = DialogueManagerParser.parse_string(code_edit.text, current_file_path).lines
# Make a list of stuff that needs to go into the file
var lines_to_save = []
for key in dialogue.keys():
var line: Dictionary = dialogue.get(key)
if not line.type in [DialogueConstants.TYPE_DIALOGUE, DialogueConstants.TYPE_RESPONSE]: continue
if line.translation_key in known_keys: continue
known_keys.append(line.translation_key)
var line_to_save: PackedStringArray = []
if existing_csv.has(line.translation_key):
line_to_save = existing_csv.get(line.translation_key)
line_to_save.resize(column_count)
existing_csv.erase(line.translation_key)
else:
line_to_save.resize(column_count)
line_to_save[0] = line.translation_key
line_to_save[default_locale_column] = line.text
if character_column > -1:
line_to_save[character_column] = "(response)" if line.type == DialogueConstants.TYPE_RESPONSE else line.character
if notes_column > -1:
line_to_save[notes_column] = line.notes
lines_to_save.append(line_to_save)
# Store lines in the file, starting with anything that already exists that hasn't been touched
for line in existing_csv.values():
file.store_csv_line(line)
for line in lines_to_save:
file.store_csv_line(line)
file.close()
plugin.get_editor_interface().get_resource_filesystem().scan()
plugin.get_editor_interface().get_file_system_dock().call_deferred("navigate_to_path", path)
# Add it to the project l10n settings if it's not already there
var language_code: RegExMatch = RegEx.create_from_string("^[a-z]{2,3}").search(default_locale)
var translation_path: String = path.replace(".csv", ".%s.translation" % language_code.get_string())
call_deferred("add_path_to_project_translations", translation_path)
func export_character_names_to_csv(path: String) -> void:
var file: FileAccess
# If the file exists, open it first and work out which keys are already in it
var existing_csv = {}
var commas = []
if FileAccess.file_exists(path):
file = FileAccess.open(path, FileAccess.READ)
var is_first_line = true
var line: Array
while !file.eof_reached():
line = file.get_csv_line()
if is_first_line:
is_first_line = false
for i in range(2, line.size()):
commas.append("")
# Make sure the line isn't empty before adding it
if line.size() > 0 and line[0].strip_edges() != "":
existing_csv[line[0]] = line
# Start a new file
file = FileAccess.open(path, FileAccess.WRITE)
if not file.file_exists(path):
file.store_csv_line(["keys", DialogueSettings.get_setting("default_csv_locale", "en")])
# Write our translations to file
var known_keys: PackedStringArray = []
var character_names: PackedStringArray = DialogueManagerParser.parse_string(code_edit.text, current_file_path).character_names
# Make a list of stuff that needs to go into the file
var lines_to_save = []
for character_name in character_names:
if character_name in known_keys: continue
known_keys.append(character_name)
if existing_csv.has(character_name):
var existing_line = existing_csv.get(character_name)
existing_line[1] = character_name
lines_to_save.append(existing_line)
existing_csv.erase(character_name)
else:
lines_to_save.append(PackedStringArray([character_name, character_name] + commas))
# Store lines in the file, starting with anything that already exists that hasn't been touched
for line in existing_csv.values():
file.store_csv_line(line)
for line in lines_to_save:
file.store_csv_line(line)
file.close()
plugin.get_editor_interface().get_resource_filesystem().scan()
plugin.get_editor_interface().get_file_system_dock().call_deferred("navigate_to_path", path)
# Add it to the project l10n settings if it's not already there
var translation_path: String = path.replace(".csv", ".en.translation")
call_deferred("add_path_to_project_translations", translation_path)
# Import changes back from an exported CSV by matching translation keys
func import_translations_from_csv(path: String) -> void:
var cursor: Vector2 = code_edit.get_cursor()
if not FileAccess.file_exists(path): return
# Open the CSV file and build a dictionary of the known keys
var keys: Dictionary = {}
var file: FileAccess = FileAccess.open(path, FileAccess.READ)
var csv_line: Array
while !file.eof_reached():
csv_line = file.get_csv_line()
if csv_line.size() > 1:
keys[csv_line[0]] = csv_line[1]
var parser: DialogueManagerParser = DialogueManagerParser.new()
# Now look over each line in the dialogue and replace the content for matched keys
var lines: PackedStringArray = code_edit.text.split("\n")
var start_index: int = 0
var end_index: int = 0
for i in range(0, lines.size()):
var line: String = lines[i]
var translation_key: String = parser.extract_translation(line)
if keys.has(translation_key):
if parser.is_dialogue_line(line):
start_index = 0
# See if we need to skip over a character name
line = line.replace("\\:", "!ESCAPED_COLON!")
if ": " in line:
start_index = line.find(": ") + 2
lines[i] = (line.substr(0, start_index) + keys.get(translation_key) + " [ID:" + translation_key + "]").replace("!ESCAPED_COLON!", ":")
elif parser.is_response_line(line):
start_index = line.find("- ") + 2
# See if we need to skip over a character name
line = line.replace("\\:", "!ESCAPED_COLON!")
if ": " in line:
start_index = line.find(": ") + 2
end_index = line.length()
if " =>" in line:
end_index = line.find(" =>")
if " [if " in line:
end_index = line.find(" [if ")
lines[i] = (line.substr(0, start_index) + keys.get(translation_key) + " [ID:" + translation_key + "]" + line.substr(end_index)).replace("!ESCAPED_COLON!", ":")
code_edit.text = "\n".join(lines)
code_edit.set_cursor(cursor)
parser.free()
func show_search_form(is_enabled: bool) -> void:
if code_edit.last_selected_text:
search_and_replace.input.text = code_edit.last_selected_text
search_and_replace.visible = is_enabled
search_button.set_pressed_no_signal(is_enabled)
search_and_replace.focus_line_edit()
### Signals
func _on_files_moved(old_file: String, new_file: String) -> void:
if open_buffers.has(old_file):
open_buffers[new_file] = open_buffers[old_file]
open_buffers.erase(old_file)
open_buffers[new_file]
func _on_cache_file_content_changed(path: String, new_content: String) -> void:
if open_buffers.has(path):
var buffer = open_buffers[path]
if buffer.text != new_content:
buffer.text = new_content
buffer.pristine_text = new_content
code_edit.text = new_content
func _on_editor_settings_changed() -> void:
var editor_settings: EditorSettings = plugin.get_editor_interface().get_editor_settings()
code_edit.minimap_draw = editor_settings.get_setting("text_editor/appearance/minimap/show_minimap")
code_edit.minimap_width = editor_settings.get_setting("text_editor/appearance/minimap/minimap_width")
code_edit.scroll_smooth = editor_settings.get_setting("text_editor/behavior/navigation/smooth_scrolling")
func _on_open_menu_id_pressed(id: int) -> void:
match id:
OPEN_OPEN:
open_dialog.popup_centered()
OPEN_CLEAR:
DialogueSettings.clear_recent_files()
build_open_menu()
_:
var menu = open_button.get_popup()
var item = menu.get_item_text(menu.get_item_index(id))
open_file(item)
func _on_files_list_file_selected(file_path: String) -> void:
self.current_file_path = file_path
func _on_insert_button_menu_id_pressed(id: int) -> void:
match id:
0:
code_edit.insert_bbcode("[wave amp=25 freq=5]", "[/wave]")
1:
code_edit.insert_bbcode("[shake rate=20 level=10]", "[/shake]")
3:
code_edit.insert_bbcode("[wait=1]")
4:
code_edit.insert_bbcode("[speed=0.2]")
5:
code_edit.insert_bbcode("[next=auto]")
6:
code_edit.insert_text_at_cursor("~ title")
7:
code_edit.insert_text_at_cursor("Nathan: This is Some Dialogue")
8:
code_edit.insert_text_at_cursor("Nathan: Choose a Response...\n- Option 1\n\tNathan: You chose option 1\n- Option 2\n\tNathan: You chose option 2")
9:
code_edit.insert_text_at_cursor("% Nathan: This is random line 1.\n% Nathan: This is random line 2.\n%1 Nathan: This is weighted random line 3.")
10:
code_edit.insert_text_at_cursor("Nathan: [[Hi|Hello|Howdy]]")
11:
code_edit.insert_text_at_cursor("=> title")
12:
code_edit.insert_text_at_cursor("=> END")
func _on_translations_button_menu_id_pressed(id: int) -> void:
match id:
TRANSLATIONS_GENERATE_LINE_IDS:
generate_translations_keys()
TRANSLATIONS_SAVE_CHARACTERS_TO_CSV:
translation_source = TranslationSource.CharacterNames
export_dialog.filters = PackedStringArray(["*.csv ; Translation CSV"])
export_dialog.current_path = get_last_export_path("csv")
export_dialog.popup_centered()
TRANSLATIONS_SAVE_TO_CSV:
translation_source = TranslationSource.Lines
export_dialog.filters = PackedStringArray(["*.csv ; Translation CSV"])
export_dialog.current_path = get_last_export_path("csv")
export_dialog.popup_centered()
TRANSLATIONS_IMPORT_FROM_CSV:
import_dialog.current_path = get_last_export_path("csv")
import_dialog.popup_centered()
func _on_export_dialog_file_selected(path: String) -> void:
DialogueSettings.set_user_value("last_export_path", path.get_base_dir())
match path.get_extension():
"csv":
match translation_source:
TranslationSource.CharacterNames:
export_character_names_to_csv(path)
TranslationSource.Lines:
export_translations_to_csv(path)
func _on_import_dialog_file_selected(path: String) -> void:
DialogueSettings.set_user_value("last_export_path", path.get_base_dir())
import_translations_from_csv(path)
func _on_main_view_theme_changed():
apply_theme()
func _on_main_view_visibility_changed() -> void:
if visible and is_instance_valid(code_edit):
code_edit.grab_focus()
func _on_new_button_pressed() -> void:
new_dialog.current_file = ""
new_dialog.popup_centered()
func _on_new_dialog_file_selected(path: String) -> void:
new_file(path)
open_file(path)
func _on_save_dialog_file_selected(path: String) -> void:
new_file(path, code_edit.text)
open_file(path)
func _on_open_button_about_to_popup() -> void:
build_open_menu()
func _on_open_dialog_file_selected(path: String) -> void:
open_file(path)
func _on_save_all_button_pressed() -> void:
save_files()
func _on_find_in_files_button_pressed() -> void:
find_in_files_dialog.popup_centered()
find_in_files.prepare()
func _on_code_edit_text_changed() -> void:
title_list.titles = code_edit.get_titles()
var buffer = open_buffers[current_file_path]
buffer.text = code_edit.text
files_list.mark_file_as_unsaved(current_file_path, buffer.text != buffer.pristine_text)
save_all_button.disabled = open_buffers.values().filter(func(d): return d.text != d.pristine_text).size() == 0
parse_timer.start(1)
func _on_code_edit_active_title_change(title: String) -> void:
title_list.select_title(title)
DialogueSettings.set_user_value("run_title", title)
func _on_code_edit_caret_changed() -> void:
DialogueSettings.set_caret(current_file_path, code_edit.get_cursor())
func _on_code_edit_error_clicked(line_number: int) -> void:
errors_panel.show_error_for_line_number(line_number)
func _on_title_list_title_selected(title: String) -> void:
code_edit.go_to_title(title)
code_edit.grab_focus()
func _on_parse_timer_timeout() -> void:
parse_timer.stop()
parse()
func _on_errors_panel_error_pressed(line_number: int, column_number: int) -> void:
code_edit.set_caret_line(line_number)
code_edit.set_caret_column(column_number)
code_edit.grab_focus()
func _on_search_button_toggled(button_pressed: bool) -> void:
show_search_form(button_pressed)
func _on_search_and_replace_open_requested() -> void:
show_search_form(true)
func _on_search_and_replace_close_requested() -> void:
search_button.set_pressed_no_signal(false)
search_and_replace.visible = false
code_edit.grab_focus()
func _on_settings_button_pressed() -> void:
settings_view.prepare()
settings_dialog.popup_centered()
func _on_settings_view_script_button_pressed(path: String) -> void:
settings_dialog.hide()
plugin.get_editor_interface().edit_resource(load(path))
func _on_test_button_pressed() -> void:
save_file(current_file_path)
if errors_panel.errors.size() > 0:
errors_dialog.popup_centered()
return
DialogueSettings.set_user_value("is_running_test_scene", true)
DialogueSettings.set_user_value("run_resource_path", current_file_path)
var test_scene_path: String = DialogueSettings.get_setting("custom_test_scene_path", "res://addons/dialogue_manager/test_scene.tscn")
plugin.get_editor_interface().play_custom_scene(test_scene_path)
func _on_settings_dialog_confirmed() -> void:
settings_view.apply_settings_changes()
parse()
code_edit.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY if DialogueSettings.get_setting("wrap_lines", false) else TextEdit.LINE_WRAPPING_NONE
code_edit.grab_focus()
func _on_support_button_pressed() -> void:
OS.shell_open("https://patreon.com/nathanhoad")
func _on_docs_button_pressed() -> void:
OS.shell_open("https://github.com/nathanhoad/godot_dialogue_manager")
func _on_files_list_file_popup_menu_requested(at_position: Vector2) -> void:
files_popup_menu.position = Vector2(get_viewport().position) + files_list.global_position + at_position
files_popup_menu.popup()
func _on_files_list_file_middle_clicked(path: String):
close_file(path)
func _on_files_popup_menu_about_to_popup() -> void:
files_popup_menu.clear()
var shortcuts: Dictionary = plugin.get_editor_shortcuts()
files_popup_menu.add_item(DialogueConstants.translate(&"buffer.save"), ITEM_SAVE, OS.find_keycode_from_string(shortcuts.get("save")[0].as_text_keycode()))
files_popup_menu.add_item(DialogueConstants.translate(&"buffer.save_as"), ITEM_SAVE_AS)
files_popup_menu.add_item(DialogueConstants.translate(&"buffer.close"), ITEM_CLOSE, OS.find_keycode_from_string(shortcuts.get("close_file")[0].as_text_keycode()))
files_popup_menu.add_item(DialogueConstants.translate(&"buffer.close_all"), ITEM_CLOSE_ALL)
files_popup_menu.add_item(DialogueConstants.translate(&"buffer.close_other_files"), ITEM_CLOSE_OTHERS)
files_popup_menu.add_separator()
files_popup_menu.add_item(DialogueConstants.translate(&"buffer.copy_file_path"), ITEM_COPY_PATH)
files_popup_menu.add_item(DialogueConstants.translate(&"buffer.show_in_filesystem"), ITEM_SHOW_IN_FILESYSTEM)
func _on_files_popup_menu_id_pressed(id: int) -> void:
match id:
ITEM_SAVE:
save_file(current_file_path)
ITEM_SAVE_AS:
save_dialog.popup_centered()
ITEM_CLOSE:
close_file(current_file_path)
ITEM_CLOSE_ALL:
for path in open_buffers.keys():
close_file(path)
ITEM_CLOSE_OTHERS:
var current_current_file_path: String = current_file_path
for path in open_buffers.keys():
if path != current_current_file_path:
await close_file(path)
ITEM_COPY_PATH:
DisplayServer.clipboard_set(current_file_path)
ITEM_SHOW_IN_FILESYSTEM:
show_file_in_filesystem(current_file_path)
func _on_code_edit_external_file_requested(path: String, title: String) -> void:
open_file(path)
if title != "":
code_edit.go_to_title(title)
else:
code_edit.set_caret_line(0)
func _on_close_confirmation_dialog_confirmed() -> void:
save_file(current_file_path)
remove_file_from_open_buffers(current_file_path)
confirmation_closed.emit()
func _on_close_confirmation_dialog_custom_action(action: StringName) -> void:
if action == "discard":
remove_file_from_open_buffers(current_file_path)
close_confirmation_dialog.hide()
confirmation_closed.emit()
func _on_find_in_files_result_selected(path: String, cursor: Vector2, length: int) -> void:
open_file(path)
code_edit.select(cursor.y, cursor.x, cursor.y, cursor.x + length)