You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

659 lines
24 KiB

6 months ago
@tool
extends DialogicEditor
## Editor for editing character resources.
signal character_loaded(resource_path:String)
signal portrait_selected()
# Current state
var loading := false
var current_previewed_scene = null
# References
var selected_item: TreeItem
var def_portrait_path :String= DialogicUtil.get_module_path('Character').path_join('default_portrait.tscn')
######### EDITOR STUFF and LOADING/SAVING ######################################
#region Resource Logic
## Method is called once editors manager is ready to accept registers.
func _register() -> void:
## Makes the editor open this when a .dch file is selected.
## Then _open_resource() is called.
editors_manager.register_resource_editor("dch", self)
## Add an "add character" button
var add_character_button = editors_manager.add_icon_button(
load("res://addons/dialogic/Editor/Images/Toolbar/add-character.svg"),
'Add Character',
self)
add_character_button.pressed.connect(_on_create_character_button_pressed)
add_character_button.shortcut = Shortcut.new()
add_character_button.shortcut.events.append(InputEventKey.new())
add_character_button.shortcut.events[0].keycode = KEY_2
add_character_button.shortcut.events[0].ctrl_pressed = true
## By default show the no character screen
$NoCharacterScreen.show()
func _get_title() -> String:
return "Character"
func _get_icon() -> Texture:
return load("res://addons/dialogic/Editor/Images/Resources/character.svg")
## Called when a character is opened somehow
func _open_resource(resource:Resource) -> void:
if resource == null:
$NoCharacterScreen.show()
return
## Update resource
current_resource = (resource as DialogicCharacter)
## Make sure changes in the ui won't trigger saving
loading = true
## Load other main tabs
for child in %MainSettingsSections.get_children():
if child is DialogicCharacterEditorMainSection:
child._load_character(current_resource)
## Clear and then load Portrait section
%PortraitSearch.text = ""
load_portrait_tree()
loading = false
character_loaded.emit(resource.resource_path)
%CharacterName.text = DialogicResourceUtil.get_unique_identifier(resource.resource_path)
$NoCharacterScreen.hide()
%PortraitChangeInfo.hide()
## Called when the character is opened.
func _open(extra_info:Variant="") -> void:
if !ProjectSettings.get_setting('dialogic/portraits/default_portrait', '').is_empty():
def_portrait_path = ProjectSettings.get_setting('dialogic/portraits/default_portrait', '')
else:
def_portrait_path = DialogicUtil.get_module_path('Character').path_join('default_portrait.tscn')
if current_resource == null:
$NoCharacterScreen.show()
return
update_preview(true)
%PortraitChangeInfo.hide()
func _clear() -> void:
current_resource = null
current_resource_state = ResourceStates.SAVED
$NoCharacterScreen.show()
func _save() -> void:
if ! visible or not current_resource:
return
## Portrait list
current_resource.portraits = get_updated_portrait_dict()
## Main tabs
for child in %MainSettingsSections.get_children():
if child is DialogicCharacterEditorMainSection:
current_resource = child._save_changes(current_resource)
ResourceSaver.save(current_resource, current_resource.resource_path)
current_resource_state = ResourceStates.SAVED
DialogicResourceUtil.update_directory('dch')
## Saves a new empty character to the given path
func new_character(path: String) -> void:
var resource := DialogicCharacter.new()
resource.resource_path = path
resource.display_name = path.get_file().trim_suffix("."+path.get_extension())
resource.color = Color(1,1,1,1)
resource.default_portrait = ""
resource.custom_info = {}
ResourceSaver.save(resource, path)
DialogicResourceUtil.update_directory('dch')
editors_manager.edit_resource(resource)
#endregion
######### INTERFACE ############################################################
#region Interface
func _ready() -> void:
if get_parent() is SubViewport:
return
DialogicUtil.get_dialogic_plugin().resource_saved.connect(_on_some_resource_saved)
# NOTE: This check is required because up to 4.2 this signal is not exposed.
if DialogicUtil.get_dialogic_plugin().has_signal("scene_saved"):
DialogicUtil.get_dialogic_plugin().scene_saved.connect(_on_some_resource_saved)
$NoCharacterScreen.color = get_theme_color("dark_color_2", "Editor")
$NoCharacterScreen.show()
setup_portrait_list_tab()
_on_fit_preview_toggle_toggled(DialogicUtil.get_editor_setting('character_preview_fit', true))
%PreviewLabel.add_theme_color_override("font_color", get_theme_color("readonly_color", "Editor"))
%PortraitChangeWarning.add_theme_color_override("font_color", get_theme_color("warning_color", "Editor"))
%RealPreviewPivot.texture = get_theme_icon("EditorPivot", "EditorIcons")
%MainSettingsCollapse.icon = get_theme_icon("GuiVisibilityVisible", "EditorIcons")
set_portrait_settings_position(DialogicUtil.get_editor_setting('portrait_settings_position', true))
await find_parent('EditorView').ready
## Add general tabs
add_settings_section(load("res://addons/dialogic/Editor/CharacterEditor/char_edit_section_general.tscn").instantiate(), %MainSettingsSections)
add_settings_section(load("res://addons/dialogic/Editor/CharacterEditor/char_edit_section_portraits.tscn").instantiate(), %MainSettingsSections)
add_settings_section(load("res://addons/dialogic/Editor/CharacterEditor/char_edit_p_section_main_exports.tscn").instantiate(), %PortraitSettingsSection)
add_settings_section(load("res://addons/dialogic/Editor/CharacterEditor/char_edit_p_section_exports.tscn").instantiate(), %PortraitSettingsSection)
add_settings_section(load("res://addons/dialogic/Editor/CharacterEditor/char_edit_p_section_main.tscn").instantiate(), %PortraitSettingsSection)
add_settings_section(load("res://addons/dialogic/Editor/CharacterEditor/char_edit_p_section_layout.tscn").instantiate(), %PortraitSettingsSection)
## Load custom sections from modules
for indexer in DialogicUtil.get_indexers():
for path in indexer._get_character_editor_sections():
var scene: Control = load(path).instantiate()
if scene is DialogicCharacterEditorMainSection:
add_settings_section(scene, %MainSettingsSections)
elif scene is DialogicCharacterEditorPortraitSection:
add_settings_section(scene, %PortraitSettingsSection)
## Add a section (a control) either to the given settings section (Main or Portraits)
## - sets up the title of the section
## - connects to various signals
func add_settings_section(edit:Control, parent:Node) -> void:
edit.changed.connect(something_changed)
edit.character_editor = self
if edit.has_signal('update_preview'):
edit.update_preview.connect(update_preview)
var button :Button
if edit._show_title():
var hbox := HBoxContainer.new()
hbox.name = edit._get_title()+"BOX"
button = Button.new()
button.flat = true
button.theme_type_variation = "DialogicSection"
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
button.text = edit._get_title()
button.icon_alignment = HORIZONTAL_ALIGNMENT_RIGHT
button.pressed.connect(_on_section_button_pressed.bind(button))
button.focus_mode = Control.FOCUS_NONE
button.icon = get_theme_icon("CodeFoldDownArrow", "EditorIcons")
button.add_theme_color_override('icon_normal_color', get_theme_color("font_color", "DialogicSection"))
hbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
hbox.add_child(button)
if !edit.hint_text.is_empty():
var hint :Node = load("res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn").instantiate()
hint.hint_text = edit.hint_text
hbox.add_child(hint)
parent.add_child(hbox)
parent.add_child(edit)
parent.add_child(HSeparator.new())
if button and !edit._start_opened():
_on_section_button_pressed(button)
func get_settings_section_by_name(name:String, main:=true) -> Node:
var parent := %MainSettingsSections
if not main:
parent = %PortraitSettingsSection
if parent.has_node(name):
return parent.get_node(name)
elif parent.has_node(name+"BOX/"+name):
return parent.get_node(name+"BOX/"+name)
else:
return null
func _on_section_button_pressed(button:Button) -> void:
var section_header := button.get_parent()
var section := section_header.get_parent().get_child(section_header.get_index()+1)
if section.visible:
button.icon = get_theme_icon("CodeFoldedRightArrow", "EditorIcons")
section.visible = false
else:
button.icon = get_theme_icon("CodeFoldDownArrow", "EditorIcons")
section.visible = true
if section_header.get_parent().get_child_count() > section_header.get_index()+2 and section_header.get_parent().get_child(section_header.get_index()+2) is Separator:
section_header.get_parent().get_child(section_header.get_index()+2).visible = section_header.get_parent().get_child(section_header.get_index()+1).visible
func something_changed(fake_argument = "", fake_arg2 = null) -> void:
if not loading:
current_resource_state = ResourceStates.UNSAVED
func _on_main_settings_collapse_toggled(button_pressed:bool) -> void:
%MainSettingsTitle.visible = !button_pressed
%MainSettingsScroll.visible = !button_pressed
if button_pressed:
%MainSettings.hide()
%MainSettingsCollapse.icon = get_theme_icon("GuiVisibilityHidden", "EditorIcons")
else:
%MainSettings.show()
%MainSettingsCollapse.icon = get_theme_icon("GuiVisibilityVisible", "EditorIcons")
func _on_switch_portrait_settings_position_pressed() -> void:
set_portrait_settings_position(!%RightSection.vertical)
func set_portrait_settings_position(is_below:bool) -> void:
%RightSection.vertical = is_below
DialogicUtil.set_editor_setting('portrait_settings_position', is_below)
if is_below:
%SwitchPortraitSettingsPosition.icon = get_theme_icon("ControlAlignRightWide", "EditorIcons")
else:
%SwitchPortraitSettingsPosition.icon = get_theme_icon("ControlAlignBottomWide", "EditorIcons")
#endregion
########## PORTRAIT SECTION ####################################################
#region Portrait Section
func setup_portrait_list_tab() -> void:
%PortraitTree.editor = self
## Portrait section styling/connections
%AddPortraitButton.icon = get_theme_icon("Add", "EditorIcons")
%AddPortraitButton.pressed.connect(add_portrait)
%AddPortraitGroupButton.icon = load("res://addons/dialogic/Editor/Images/Pieces/add-folder.svg")
%AddPortraitGroupButton.pressed.connect(add_portrait_group)
%ImportPortraitsButton.icon = get_theme_icon("Load", "EditorIcons")
%ImportPortraitsButton.pressed.connect(open_portrait_folder_select)
%PortraitSearch.right_icon = get_theme_icon("Search", "EditorIcons")
%PortraitSearch.text_changed.connect(filter_portrait_list)
%PortraitTree.item_selected.connect(load_selected_portrait)
%PortraitTree.item_edited.connect(_on_item_edited)
%PortraitTree.item_activated.connect(_on_item_activated)
func open_portrait_folder_select() -> void:
find_parent("EditorView").godot_file_dialog(
import_portraits_from_folder, "*.svg, *.png",
EditorFileDialog.FILE_MODE_OPEN_DIR)
func import_portraits_from_folder(path:String) -> void:
var parent: TreeItem = %PortraitTree.get_root()
if %PortraitTree.get_selected() and %PortraitTree.get_selected() != parent and %PortraitTree.get_selected().get_metadata(0).has('group'):
parent = %PortraitTree.get_selected()
var dir := DirAccess.open(path)
dir.list_dir_begin()
var file_name :String = dir.get_next()
while file_name != "":
if not dir.current_is_dir():
var file_lower = file_name.to_lower()
if '.svg' in file_lower or '.png' in file_lower:
if not '.import' in file_lower:
var final_name: String = path.path_join(file_name)
%PortraitTree.add_portrait_item(file_name.trim_suffix('.'+file_name.get_extension()),
{'scene':"",'export_overrides':{'image':var_to_str(final_name)}, 'scale':1, 'offset':Vector2(), 'mirror':false}, parent)
file_name = dir.get_next()
## Handle selection
if parent.get_child_count():
parent.get_first_child().select(0)
else:
# Call anyways to clear preview and hide portrait settings section
load_selected_portrait()
something_changed()
func add_portrait(portrait_name:String='New portrait', portrait_data:Dictionary={'scene':"", 'export_overrides':{'image':''}, 'scale':1, 'offset':Vector2(), 'mirror':false}) -> void:
var parent: TreeItem = %PortraitTree.get_root()
if %PortraitTree.get_selected():
if %PortraitTree.get_selected().get_metadata(0) and %PortraitTree.get_selected().get_metadata(0).has('group'):
parent = %PortraitTree.get_selected()
else:
parent = %PortraitTree.get_selected().get_parent()
var item: TreeItem = %PortraitTree.add_portrait_item(portrait_name, portrait_data, parent)
item.set_meta('new', true)
item.set_editable(0, true)
item.select(0)
%PortraitTree.call_deferred('edit_selected')
something_changed()
func add_portrait_group() -> void:
var parent_item: TreeItem = %PortraitTree.get_root()
if %PortraitTree.get_selected() and %PortraitTree.get_selected().get_metadata(0).has('group'):
parent_item = %PortraitTree.get_selected()
var item :TreeItem = %PortraitTree.add_portrait_group("Group", parent_item)
item.set_meta('new', true)
item.set_editable(0, true)
item.select(0)
%PortraitTree.call_deferred('edit_selected')
func load_portrait_tree() -> void:
%PortraitTree.clear_tree()
var root: TreeItem = %PortraitTree.create_item()
for portrait in current_resource.portraits.keys():
var portrait_label = portrait
var parent = %PortraitTree.get_root()
if '/' in portrait:
parent = %PortraitTree.create_necessary_group_items(portrait)
portrait_label = portrait.split('/')[-1]
%PortraitTree.add_portrait_item(portrait_label, current_resource.portraits[portrait], parent)
update_default_portrait_star(current_resource.default_portrait)
if root.get_child_count():
root.get_first_child().select(0)
while %PortraitTree.get_selected().get_child_count():
%PortraitTree.get_selected().get_child(0).select(0)
else:
# Call anyways to clear preview and hide portrait settings section
load_selected_portrait()
func filter_portrait_list(filter_term := "") -> void:
filter_branch(%PortraitTree.get_root(), filter_term)
func filter_branch(parent: TreeItem, filter_term: String) -> bool:
var anything_visible := false
for item in parent.get_children():
if item.get_metadata(0).has('group'):
item.visible = filter_branch(item, filter_term)
anything_visible = item.visible
elif filter_term.is_empty() or filter_term.to_lower() in item.get_text(0).to_lower():
item.visible = true
anything_visible = true
else:
item.visible = false
return anything_visible
## This is used to save the portrait data
func get_updated_portrait_dict() -> Dictionary:
return list_portraits(%PortraitTree.get_root().get_children())
func list_portraits(tree_items: Array[TreeItem], dict := {}, path_prefix := "") -> Dictionary:
for item in tree_items:
if item.get_metadata(0).has('group'):
dict = list_portraits(item.get_children(), dict, path_prefix+item.get_text(0)+"/")
else:
dict[path_prefix +item.get_text(0)] = item.get_metadata(0)
return dict
func load_selected_portrait() -> void:
if selected_item and is_instance_valid(selected_item):
selected_item.set_editable(0, false)
selected_item = %PortraitTree.get_selected()
if selected_item and selected_item.get_metadata(0) != null and !selected_item.get_metadata(0).has('group'):
%PortraitSettingsSection.show()
var current_portrait_data: Dictionary = selected_item.get_metadata(0)
portrait_selected.emit(%PortraitTree.get_full_item_name(selected_item), current_portrait_data)
update_preview()
for child in %PortraitSettingsSection.get_children():
if child is DialogicCharacterEditorPortraitSection:
child.selected_item = selected_item
child._load_portrait_data(current_portrait_data)
else:
%PortraitSettingsSection.hide()
update_preview()
func delete_portrait_item(item: TreeItem) -> void:
if item.get_next_visible(true) and item.get_next_visible(true) != item:
item.get_next_visible(true).select(0)
else:
selected_item = null
load_selected_portrait()
item.free()
something_changed()
func duplicate_item(item: TreeItem) -> void:
var new_item: TreeItem = %PortraitTree.add_portrait_item(item.get_text(0)+'_duplicated', item.get_metadata(0).duplicate(true), item.get_parent())
new_item.set_meta('new', true)
new_item.select(0)
func _input(event: InputEvent) -> void:
if !is_visible_in_tree() or (get_viewport().gui_get_focus_owner()!= null and !name+'/' in str(get_viewport().gui_get_focus_owner().get_path())):
return
if event is InputEventKey and event.pressed:
if event.keycode == KEY_F2 and %PortraitTree.get_selected():
%PortraitTree.get_selected().set_editable(0, true)
%PortraitTree.edit_selected()
get_viewport().set_input_as_handled()
elif event.keycode == KEY_DELETE and get_viewport().gui_get_focus_owner() is Tree and %PortraitTree.get_selected():
delete_portrait_item(%PortraitTree.get_selected())
get_viewport().set_input_as_handled()
func _on_portrait_right_click_menu_index_pressed(id: int) -> void:
# RENAME BUTTON
if id == 0:
_on_item_activated()
# DELETE BUTTON
if id == 2:
delete_portrait_item(%PortraitTree.get_selected())
# DUPLICATE ITEM
elif id == 1:
duplicate_item(%PortraitTree.get_selected())
elif id == 4:
get_settings_section_by_name("Portraits").set_default_portrait(%PortraitTree.get_full_item_name(%PortraitTree.get_selected()))
## This removes/and adds the DEFAULT star on the portrait list
func update_default_portrait_star(default_portrait_name: String) -> void:
var item_list: Array = %PortraitTree.get_root().get_children()
if item_list.is_empty() == false:
while true:
var item := item_list.pop_back()
if item.get_button_by_id(0, 2) != -1:
item.erase_button(0, item.get_button_by_id(0, 2))
if %PortraitTree.get_full_item_name(item) == default_portrait_name:
item.add_button(0, get_theme_icon("Favorites", "EditorIcons"), 2, true, "Default")
item_list.append_array(item.get_children())
if item_list.is_empty():
break
func _on_item_edited() -> void:
selected_item = %PortraitTree.get_selected()
something_changed()
if selected_item:
if %PreviewLabel.text.trim_prefix('Preview of "').trim_suffix('"') == current_resource.default_portrait:
current_resource.default_portrait = %PortraitTree.get_full_item_name(selected_item)
selected_item.set_editable(0, false)
if !selected_item.has_meta('new') and %PortraitTree.get_full_item_name(selected_item) != selected_item.get_meta('previous_name'):
report_name_change(selected_item)
%PortraitChangeInfo.show()
update_preview()
func _on_item_activated() -> void:
if %PortraitTree.get_selected() == null:
return
%PortraitTree.get_selected().set_editable(0, true)
%PortraitTree.edit_selected()
func report_name_change(item: TreeItem) -> void:
if item.get_metadata(0).has('group'):
for s_item in item.get_children():
if s_item.get_metadata(0).has('group') or !s_item.has_meta('new'):
report_name_change(s_item)
else:
if item.get_meta('previous_name') == %PortraitTree.get_full_item_name(item):
return
editors_manager.reference_manager.add_portrait_ref_change(
item.get_meta('previous_name'),
%PortraitTree.get_full_item_name(item),
[DialogicResourceUtil.get_unique_identifier(current_resource.resource_path)])
item.set_meta('previous_name', %PortraitTree.get_full_item_name(item))
%PortraitChangeInfo.show()
#endregion
########### PREVIEW ############################################################
#region Preview
func update_preview(force := false) -> void:
%ScenePreviewWarning.hide()
if selected_item and is_instance_valid(selected_item) and selected_item.get_metadata(0) != null and !selected_item.get_metadata(0).has('group'):
%PreviewLabel.text = 'Preview of "'+%PortraitTree.get_full_item_name(selected_item)+'"'
var current_portrait_data: Dictionary = selected_item.get_metadata(0)
if not force and current_previewed_scene != null \
and current_previewed_scene.get_meta('path', '') == current_portrait_data.get('scene') \
and current_previewed_scene.has_method('_should_do_portrait_update') \
and is_instance_valid(current_previewed_scene.get_script()) \
and current_previewed_scene._should_do_portrait_update(current_resource, selected_item.get_text(0)):
pass # we keep the same scene
else:
for node in %RealPreviewPivot.get_children():
node.queue_free()
current_previewed_scene = null
var scene_path := def_portrait_path
if not current_portrait_data.get('scene', '').is_empty():
scene_path = current_portrait_data.get('scene')
if ResourceLoader.exists(scene_path):
current_previewed_scene = load(scene_path).instantiate()
if current_previewed_scene:
%RealPreviewPivot.add_child(current_previewed_scene)
if current_previewed_scene != null:
var scene: Node = current_previewed_scene
scene.show_behind_parent = true
DialogicUtil.apply_scene_export_overrides(scene, current_portrait_data.get('export_overrides', {}))
var mirror: bool = current_portrait_data.get('mirror', false) != current_resource.mirror
var scale: float = current_portrait_data.get('scale', 1) * current_resource.scale
if current_portrait_data.get('ignore_char_scale', false):
scale = current_portrait_data.get('scale', 1)
var offset: Vector2 = current_portrait_data.get('offset', Vector2()) + current_resource.offset
if is_instance_valid(scene.get_script()) and scene.script.is_tool():
if scene.has_method('_update_portrait'):
## Create a fake duplicate resource that has all the portrait changes applied already
var preview_character := current_resource.duplicate()
preview_character.portraits = get_updated_portrait_dict()
scene._update_portrait(preview_character, %PortraitTree.get_full_item_name(selected_item))
if scene.has_method('_set_mirror'):
scene._set_mirror(mirror)
if !%FitPreview_Toggle.button_pressed:
scene.position = Vector2() + offset
scene.scale = Vector2(1,1)*scale
else:
if is_instance_valid(scene.get_script()) and scene.script.is_tool() and scene.has_method('_get_covered_rect'):
var rect: Rect2= scene._get_covered_rect()
var available_rect: Rect2 = %FullPreviewAvailableRect.get_rect()
scene.scale = Vector2(1,1) * min(available_rect.size.x/rect.size.x, available_rect.size.y/rect.size.y)
%RealPreviewPivot.position = (rect.position)*-1*scene.scale
%RealPreviewPivot.position.x = %FullPreviewAvailableRect.size.x/2
scene.position = Vector2()
else:
%ScenePreviewWarning.show()
else:
%PreviewLabel.text = 'Nothing to preview'
for child in %PortraitSettingsSection.get_children():
if child is DialogicCharacterEditorPortraitSection:
child._recheck(current_portrait_data)
else:
%PreviewLabel.text = 'No portrait to preview.'
for node in %RealPreviewPivot.get_children():
node.queue_free()
current_previewed_scene = null
func _on_some_resource_saved(file:Variant) -> void:
if current_previewed_scene == null:
return
if file is Resource and file == current_previewed_scene.script:
update_preview(true)
if typeof(file) == TYPE_STRING and file == current_previewed_scene.get_meta("path", ""):
update_preview(true)
func _on_full_preview_available_rect_resized():
if %FitPreview_Toggle.button_pressed:
update_preview()
func _on_create_character_button_pressed():
editors_manager.show_add_resource_dialog(
new_character,
'*.dch; DialogicCharacter',
'Create new character',
'character',
)
func _on_fit_preview_toggle_toggled(button_pressed):
%FitPreview_Toggle.set_pressed_no_signal(button_pressed)
if button_pressed:
%FitPreview_Toggle.icon = get_theme_icon("ScrollContainer", "EditorIcons")
%FitPreview_Toggle.tooltip_text = "Real scale"
else:
%FitPreview_Toggle.tooltip_text = "Fit into preview"
%FitPreview_Toggle.icon = get_theme_icon("CenterContainer", "EditorIcons")
DialogicUtil.set_editor_setting('character_preview_fit', button_pressed)
update_preview()
#endregion
## Open the reference manager
func _on_reference_manger_button_pressed():
editors_manager.reference_manager.open()
%PortraitChangeInfo.hide()