Files
Kitchen-Adventure/addons/dialogue_manager/plugin.gd
T

509 lines
17 KiB
GDScript

@tool
class_name DMPlugin extends EditorPlugin
@warning_ignore("unused_signal")
signal cache_file_content_changed(path: String, new_content: String)
const MainView: PackedScene = preload("./views/main_view.tscn")
const FindInDialogueView: PackedScene = preload("./views/find_in_dialogue_view.tscn")
static var instance: DMPlugin
var import_plugin: DMImportPlugin
var export_plugin: DMExportPlugin
var inspector_plugin: DMInspectorPlugin
var translation_parser_plugin: DMTranslationParserPlugin
var debugger_plugin: DMDebuggerPlugin
var main_view: Control
var find_in_dialogue_view: Control
var preview_generator: DMPreviewGenerator
func _init() -> void:
instance = self
func _enable_plugin() -> void:
add_autoload_singleton("DialogueManager", get_plugin_path() + "/dialogue_manager.gd")
func _disable_plugin() -> void:
remove_autoload_singleton("DialogueManager")
func _enter_tree() -> void:
if Engine.is_editor_hint():
DMSettings.prepare()
DMCache.prepare()
import_plugin = DMImportPlugin.new()
add_import_plugin(import_plugin)
export_plugin = DMExportPlugin.new()
add_export_plugin(export_plugin)
inspector_plugin = DMInspectorPlugin.new()
add_inspector_plugin(inspector_plugin)
translation_parser_plugin = DMTranslationParserPlugin.new()
add_translation_parser_plugin(translation_parser_plugin)
debugger_plugin = DMDebuggerPlugin.new()
add_debugger_plugin(debugger_plugin)
preview_generator = DMPreviewGenerator.new()
EditorInterface.get_resource_previewer().add_preview_generator(preview_generator)
main_view = MainView.instantiate()
EditorInterface.get_editor_main_screen().add_child(main_view)
_make_visible(false)
_update_localization()
EditorInterface.get_file_system_dock().files_moved.connect(_on_files_moved)
EditorInterface.get_file_system_dock().file_removed.connect(_on_file_removed)
add_tool_submenu_item("Dialogue", _create_translations_tool_menu_item())
func _exit_tree() -> void:
remove_import_plugin(import_plugin)
import_plugin = null
remove_export_plugin(export_plugin)
export_plugin = null
remove_inspector_plugin(inspector_plugin)
inspector_plugin = null
remove_translation_parser_plugin(translation_parser_plugin)
translation_parser_plugin = null
remove_debugger_plugin(debugger_plugin)
debugger_plugin = null
EditorInterface.get_resource_previewer().remove_preview_generator(preview_generator)
preview_generator = null
if is_instance_valid(main_view):
main_view.queue_free()
_hide_find_in_dialogue()
EditorInterface.get_file_system_dock().files_moved.disconnect(_on_files_moved)
EditorInterface.get_file_system_dock().file_removed.disconnect(_on_file_removed)
remove_tool_menu_item("Dialogue")
instance = null
func _has_main_screen() -> bool:
return true
func _make_visible(next_visible: bool) -> void:
if is_instance_valid(main_view):
main_view.visible = next_visible
func _get_plugin_name() -> String:
return "Dialogue"
func _get_plugin_icon() -> Texture2D:
return load(get_plugin_path() + "/assets/icon.svg")
func _handles(object: Object) -> bool:
var editor_settings: EditorSettings = EditorInterface.get_editor_settings()
var external_editor: String = editor_settings.get_setting("text_editor/external/exec_path")
var use_external_editor: bool = editor_settings.get_setting("text_editor/external/use_external_editor") and external_editor != ""
if object is DialogueResource and use_external_editor and DMSettings.get_user_value("open_in_external_editor", false):
var project_path: String = ProjectSettings.globalize_path("res://")
var file_path: String = ProjectSettings.globalize_path(object.resource_path)
OS.create_process(external_editor, [project_path, file_path])
return false
return object is DialogueResource
func _edit(object: Object) -> void:
if is_instance_valid(main_view) and is_instance_valid(object):
main_view.open_resource(object)
func _apply_changes() -> void:
if is_instance_valid(main_view):
main_view.apply_changes()
_update_localization()
func _get_unsaved_status(for_scene: String) -> String:
if not for_scene.is_empty(): return ""
if not is_instance_valid(main_view): return ""
var unsaved_count: int = main_view.count_unsaved_files()
if unsaved_count > 0:
return DMConstants.translate(&"confirm_n_unsaved_files").format({ count = unsaved_count })
else:
return ""
func _save_external_data() -> void:
if is_instance_valid(main_view) and EditorInterface.get_editor_settings().get_setting("run/auto_save/save_before_running"):
main_view.apply_changes()
_update_localization()
DMCache.reimport_files()
func _build() -> bool:
# If this is the dotnet Godot then we need to check if the solution file exists
DMSettings.check_for_dotnet_solution()
# Ignore errors in other files if we are just running the test scene
if DMSettings.get_user_value("is_running_test_scene", true): return true
DMCache.reimport_files()
var files_with_errors: Array[Dictionary] = DMCache.get_files_with_errors()
if files_with_errors.size() > 0:
for dialogue_file: Dictionary in files_with_errors:
push_error("You have %d error(s) in %s" % [dialogue_file.errors.size(), dialogue_file.path])
EditorInterface.edit_resource(load(files_with_errors[0].path))
main_view.show_build_error_dialog()
return false
return true
## Open a [DialogueResource] in the dialogue editor.
static func open_file(resource_or_path: Variant) -> void:
var resource: DialogueResource = resource_or_path if resource_or_path is DialogueResource else load(resource_or_path)
instance.main_view.open_file(resource.resource_path)
EditorInterface.set_main_screen_editor("Dialogue")
## Open a [DialogueResource] and jump to a given cue.
static func open_file_at_cue(resource_or_path: Variant, cue: String, create_if_none: bool = false) -> void:
open_file(resource_or_path)
instance.main_view.go_to_cue(cue, create_if_none)
## Open a [DialogueResource] and jump to a given line number.
static func open_file_at_line(resource_or_path: Variant, line_number: int) -> void:
open_file(resource_or_path)
instance.main_view.go_to_line_number(line_number)
## Show the panel for finding text in dialogue files.
static func show_find_in_dialogue() -> void:
instance._show_find_in_dialogue()
func _show_find_in_dialogue() -> void:
if not is_instance_valid(find_in_dialogue_view):
find_in_dialogue_view = FindInDialogueView.instantiate()
find_in_dialogue_view.main_view = main_view
find_in_dialogue_view.result_selected.connect(main_view._on_find_in_files_result_selected)
add_control_to_bottom_panel(find_in_dialogue_view, DMConstants.translate(&"search.find_in_dialogue"))
make_bottom_panel_item_visible(find_in_dialogue_view)
find_in_dialogue_view.prepare()
func _hide_find_in_dialogue() -> void:
if is_instance_valid(find_in_dialogue_view):
remove_control_from_bottom_panel(find_in_dialogue_view)
find_in_dialogue_view.queue_free()
## Get the shortcuts used by the plugin
static func get_editor_shortcuts() -> Dictionary:
var shortcuts: Dictionary = {
toggle_comment = [
_create_event("Ctrl+K"),
_create_event("Ctrl+Slash")
],
delete_line = [
_create_event("Ctrl+Shift+K")
],
move_up = [
_create_event("Alt+Up")
],
move_down = [
_create_event("Alt+Down")
],
save = [
_create_event("Ctrl+Alt+S")
],
close_file = [
_create_event("Ctrl+W")
],
find_in_files = [
_create_event("Ctrl+Shift+F")
],
run_test_scene = [
_create_event("Ctrl+F5")
],
text_size_increase = [
_create_event("Ctrl+Equal")
],
text_size_decrease = [
_create_event("Ctrl+Minus")
],
text_size_reset = [
_create_event("Ctrl+0")
],
make_bold = [
_create_event("Ctrl+Shift+b")
],
make_italic = [
_create_event("Ctrl+Shift+i")
]
}
var paths: EditorPaths = EditorInterface.get_editor_paths()
var settings: Resource = null
for version: String in ["4.5", "4.4", "4.3", "4"]:
var path: String = paths.get_config_dir() + "/editor_settings-" + version + ".tres"
if FileAccess.file_exists(path):
settings = load(path)
break
if settings == null: return shortcuts
for s: Dictionary in settings.get("shortcuts"):
for key: String in shortcuts:
if s.name == "script_text_editor/%s" % key or s.name == "script_editor/%s" % key:
shortcuts[key] = []
for event: InputEvent in s.shortcuts:
if event is InputEventKey:
shortcuts[key].append(event)
return shortcuts
static func _create_event(string: String) -> InputEventKey:
var event: InputEventKey = InputEventKey.new()
var bits: PackedStringArray = string.split("+")
event.keycode = OS.find_keycode_from_string(bits[bits.size() - 1])
event.shift_pressed = bits.has("Shift")
event.alt_pressed = bits.has("Alt")
if bits.has("Ctrl") or bits.has("Command"):
event.command_or_control_autoremap = true
return event
## Get the editor shortcut that matches an event
static func get_editor_shortcut(event: InputEventKey) -> String:
var shortcuts: Dictionary = get_editor_shortcuts()
for key: String in shortcuts:
for shortcut: InputEvent in shortcuts.get(key, []):
if event.as_text().split(" ")[0] == shortcut.as_text().split(" ")[0]:
return key
return ""
## Get the current version
static func get_version() -> String:
var config: ConfigFile = ConfigFile.new()
config.load(get_plugin_path() + "/plugin.cfg")
return config.get_value("plugin", "version")
## Get the current path of the plugin
static func get_plugin_path() -> String:
if not is_instance_valid(instance):
return ""
return instance.get_script().resource_path.get_base_dir()
## Update references to a moved file
func _update_import_paths(from_path: String, to_path: String) -> void:
DMCache.move_file_path(from_path, to_path)
# Reopen the file if it's already open
if main_view.current_file_path == from_path:
if to_path == "":
main_view.close_file(from_path)
else:
main_view.current_file_path = ""
main_view.open_file(to_path)
# Update any other files that import the moved file
var dependents: Array = DMCache.get_files_with_dependency(from_path)
for dependent: Dictionary in dependents:
dependent.dependencies.remove_at(dependent.dependencies.find(from_path))
dependent.dependencies.append(to_path)
# Update the live buffer
if main_view.current_file_path == dependent.path:
main_view.code_edit.text = main_view.code_edit.text.replace(from_path, to_path)
main_view.open_buffers[main_view.current_file_path].pristine_text = main_view.code_edit.text
# Open the file and update the path
var file: FileAccess = FileAccess.open(dependent.path, FileAccess.READ)
var text: String = file.get_as_text().replace(from_path, to_path)
file.close()
file = FileAccess.open(dependent.path, FileAccess.WRITE)
file.store_string(text)
file.close()
func _update_localization() -> void:
if not DMSettings.get_setting(DMSettings.UPDATE_TRANSLATION_TEMPLATES_AUTOMATICALLY, true):
return
var dialogue_files: PackedStringArray = DMCache.get_files()
# Add any new files to POT generation
var files_for_pot: PackedStringArray = ProjectSettings.get_setting("internationalization/locale/translations_pot_files", [])
var files_for_pot_changed: bool = false
for path: String in dialogue_files:
if not files_for_pot.has(path):
files_for_pot.append(path)
files_for_pot_changed = true
# Remove any POT references that don't exist any more
for i: int in range(files_for_pot.size() - 1, -1, -1):
var file_for_pot: String = files_for_pot[i]
if file_for_pot.get_extension() == "dialogue" and not dialogue_files.has(file_for_pot):
files_for_pot.remove_at(i)
files_for_pot_changed = true
# Update project settings if POT changed
if files_for_pot_changed:
ProjectSettings.set_setting("internationalization/locale/translations_pot_files", files_for_pot)
ProjectSettings.save()
func _create_translations_tool_menu_item() -> PopupMenu:
var tool_menu: PopupMenu = PopupMenu.new()
tool_menu.add_icon_item(_get_plugin_icon(), "Create balloon...")
tool_menu.add_icon_item(main_view.get_theme_icon("Translation", "EditorIcons"), DMConstants.translate("generate_line_ids"))
tool_menu.index_pressed.connect(func(index: int) -> void:
match index:
0: # create balloon
_create_dialogue_balloon()
1: # generate IDs
var confirm: ConfirmationDialog = ConfirmationDialog.new()
confirm.title = DMConstants.translate("generate_ids.warning_title")
confirm.dialog_text = DMConstants.translate("generate_ids.warning_text")
confirm.ok_button_text = DMConstants.translate("generate_ids.ok_button")
confirm.confirmed.connect(func() -> void:
confirm.queue_free()
DMTranslationUtilities.generate_static_line_ids_for_project()
)
confirm.canceled.connect(func() -> void:
confirm.queue_free()
)
add_child(confirm)
confirm.popup_centered()
)
return tool_menu
#region Callbacks
func _create_dialogue_balloon() -> void:
var scale: float = EditorInterface.get_editor_scale()
var directory_dialog: FileDialog = FileDialog.new()
var label: Label = Label.new()
label.text = "Dialogue balloon files will be copied into chosen directory."
directory_dialog.get_vbox().add_child(label)
directory_dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR
directory_dialog.min_size = Vector2(600, 500) * scale
directory_dialog.dir_selected.connect(func(path: String) -> void:
var plugin_path: String = get_plugin_path()
var is_dotnet: bool = DMSettings.check_for_dotnet_solution()
var balloon_path: String = path + ("/Balloon.tscn" if is_dotnet else "/balloon.tscn")
var balloon_script_path: String = path + ("/DialogueBalloon.cs" if is_dotnet else "/balloon.gd")
# Copy the balloon scene file and change the script reference
var is_small_window: bool = ProjectSettings.get_setting("display/window/size/viewport_width") < 400
var example_balloon_file_name: String
if is_dotnet:
example_balloon_file_name = "SmallExampleBalloon.tscn" if is_small_window else "ExampleBalloon.tscn"
else:
example_balloon_file_name = "small_example_balloon.tscn" if is_small_window else "example_balloon.tscn"
var example_balloon_path: String = plugin_path + "/example_balloon/" + example_balloon_file_name
var example_balloon_script_file_name: String = "ExampleBalloon.cs" if is_dotnet else "example_balloon.gd"
var example_balloon_script_uid: String = ResourceUID.id_to_text(ResourceLoader.get_resource_uid(plugin_path + "/example_balloon/" + example_balloon_script_file_name))
var example_balloon_uid: String = ResourceUID.id_to_text(ResourceLoader.get_resource_uid(example_balloon_path))
# Copy the script file
var file: FileAccess = FileAccess.open(plugin_path + "/example_balloon/" + example_balloon_script_file_name, FileAccess.READ)
var file_contents: String = file.get_as_text()
if is_dotnet:
file_contents = file_contents.replace("class ExampleBalloon", "class DialogueBalloon")
else:
file_contents = file_contents.replace("class_name DialogueManagerExampleBalloon ", "")
# Remove the example warning
var lines: PackedStringArray = file_contents.split("\n")
var updated_lines: PackedStringArray = []
var ignore_line: bool = false
for line: String in lines:
if line.contains("EXAMPLE MESSAGE"):
ignore_line = not ignore_line
elif not ignore_line:
updated_lines.append(line)
file_contents = "\n".join(updated_lines)
file = FileAccess.open(balloon_script_path, FileAccess.WRITE)
file.store_string(file_contents)
file.close()
var new_balloon_script_uid_raw: int = ResourceUID.create_id()
ResourceUID.add_id(new_balloon_script_uid_raw, balloon_script_path)
var new_balloon_script_uid: String = ResourceUID.id_to_text(new_balloon_script_uid_raw)
# Save the new balloon
file_contents = FileAccess.get_file_as_string(example_balloon_path)
if "example_balloon.gd" in file_contents:
file_contents = file_contents.replace(plugin_path + "/example_balloon/example_balloon.gd", balloon_script_path)
else:
file_contents = file_contents.replace(plugin_path + "/example_balloon/ExampleBalloon.cs", balloon_script_path)
var new_balloon_uid: String = ResourceUID.id_to_text(ResourceUID.create_id())
file_contents = file_contents.replace(example_balloon_uid, new_balloon_uid).replace(example_balloon_script_uid, new_balloon_script_uid)
file = FileAccess.open(balloon_path, FileAccess.WRITE)
file.store_string(file_contents)
file.close()
EditorInterface.get_resource_filesystem().scan()
EditorInterface.get_file_system_dock().call_deferred("navigate_to_path", balloon_path)
DMSettings.set_setting(DMSettings.BALLOON_PATH, balloon_path)
directory_dialog.queue_free()
)
EditorInterface.get_base_control().add_child(directory_dialog)
directory_dialog.popup_centered()
#endregion
#region Signals
func _on_files_moved(old_file: String, new_file: String) -> void:
_update_import_paths(old_file, new_file)
DMSettings.move_recent_file(old_file, new_file)
func _on_file_removed(file: String) -> void:
DMSettings.forget_path(file)
_update_import_paths(file, "")
if is_instance_valid(main_view):
main_view.close_file(file)
_update_localization()
#endregion