@tool # ############################################################################ # # Copyright © 2018-2022 Paul Joannon # Copyright © 2019-2022 Frédéric Maquin # Licensed under the MIT License. # See LICENSE in the project root for license information. # ############################################################################ # extends Control # Hiding this type to prevent registration of "private" nodes. # See https://github.com/godotengine/godot-proposals/issues/1047 # class_name InkPreviewPanel # ############################################################################ # # Imports # ############################################################################ # var InkPlayerFactory := preload("res://addons/inkgd/ink_player_factory.gd") as GDScript # ############################################################################ # # Enums # ############################################################################ # enum StoryOrigin { CONFIGURATION, FILE } # ############################################################################ # # Constants # ############################################################################ # const NAME = "name" const STORY_ORIGIN = "story_origin" const FILE_PATH = "file_path" # ############################################################################ # # Public Properties # ############################################################################ # var editor_interface: InkEditorInterface var configuration: InkConfiguration var progress_texture: AnimatedTexture # ############################################################################ # # Private Properties # ############################################################################ # var _scrollbar_max_value = -1 var _current_story_index = -1 var _custom_stories: Array = [] var _available_stories: Array = [] var _file_dialog = EditorFileDialog.new() var _ink_player = InkPlayerFactory.create() # ############################################################################ # # On Ready | Private Properties # ############################################################################ # @onready var _play_icon = get_theme_icon("Play", "EditorIcons") # ############################################################################ # # On Ready | Private Nodes # ############################################################################ @onready var _command_strip = find_child("CommandStripHBoxContainer") @onready var _pick_story_button = _command_strip.get_node("PickStoryOptionButton") @onready var _load_story_button = _command_strip.get_node("LoadStoryButton") @onready var _start_button = _command_strip.get_node("StartButton") @onready var _stop_button = _command_strip.get_node("StopButton") @onready var _clear_button = _command_strip.get_node("ClearButton") @onready var _scroll_container = find_child("ScrollContainer") @onready var _story_container = _scroll_container.get_node("MarginContainer/StoryVBoxContainer") @onready var _choice_area_container = find_child("ChoicesAreaVBoxContainer") @onready var _choices_container = _choice_area_container.get_node("ChoicesVBoxContainer") # ############################################################################ # # Overrides # ############################################################################ # func _ready(): # FIXME: This needs investigating. # Sanity check. It seems the editor instantiates tools script on their # own, probably to add them to its tree. In that case, they won't have # their dependencies injected, so we're not doing anything. if editor_interface == null || configuration == null || progress_texture == null: print("[inkgd] [INFO] Ink Preview Tab: dependencies not met, ignoring.") return add_child(_ink_player) _connect_signals() _apply_configuration() _update_story_picker() var load_icon = get_theme_icon("Load", "EditorIcons") var stop_icon = get_theme_icon("Stop", "EditorIcons") var clear_icon = get_theme_icon("Clear", "EditorIcons") _start_button.icon = _play_icon _load_story_button.icon = load_icon _stop_button.icon = stop_icon _clear_button.icon = clear_icon _stop_button.visible = false _choice_area_container.custom_minimum_size = Vector2(200, 0) * editor_interface.scale _choice_area_container.visible = false _file_dialog.connect("file_selected", Callable(self, "_on_file_selected")) add_child(_file_dialog) # ############################################################################ # # Signal Receivers # ############################################################################ # func _start_button_pressed(): var file_path = _get_current_story_file_path() if file_path == null: return print("[inkgd] [INFO] Previewing %s" % file_path) _clear_content() _ink_player.destroy() _ink_player.ink_file = load(file_path) _start_button.icon = progress_texture _disable_command_strip(true) _ink_player.create_story() func _stop_button_pressed(): _start_button.visible = true _stop_button.visible = false _choice_area_container.visible = false _ink_player.destroy() _clear_choices() _clear_content() func _story_loaded(successfully: bool): _disable_command_strip(false) _start_button.icon = _play_icon if !successfully: return _start_button.visible = false _stop_button.visible = true _ink_player.allow_external_function_fallbacks = true _continue_story() func _pick_story_button_selected(index): if _current_story_index != index: _stop_button_pressed() _current_story_index = index func _load_story_button_pressed(): _file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE _file_dialog.access = FileDialog.ACCESS_RESOURCES _file_dialog.add_filter("*.json;Compiled Ink story") _file_dialog.popup_centered(Vector2(1280, 800) * editor_interface.scale) func _choice_button_pressed(index): _clear_choices() _ink_player.choose_choice_index(index) _continue_story() func _on_file_selected(path: String): if _custom_stories.has(path): return for story_configuration in self.configuration.stories: var target_file_path = self.configuration.get_target_file_path(story_configuration) if target_file_path == path: return _custom_stories.append(path) _apply_configuration() _update_story_picker(path) func _scrollbar_changed(): var max_value = _scroll_container.get_v_scroll_bar().max_value if _scrollbar_max_value == max_value && _scrollbar_max_value != -1: return _scrollbar_max_value = max_value _scroll_container.scroll_vertical = max_value func _configuration_changed(): # Cleaning everything on configuration change may end up being frustrating. # But for now, it's going to make everybody's life easier. _stop_button_pressed() _apply_configuration() _update_story_picker() # ############################################################################ # # Private Methods # ############################################################################ # func _apply_configuration(): _available_stories.clear() _current_story_index = -1 _pick_story_button.selected = _current_story_index var i = 0 for story_configuration in self.configuration.stories: var target_file_path = self.configuration.get_target_file_path(story_configuration) if target_file_path != null && !target_file_path.is_empty(): _available_stories.append({ NAME: "Story %d - %s" % [i + 1, target_file_path.get_file()], FILE_PATH: target_file_path, STORY_ORIGIN: StoryOrigin.CONFIGURATION }) i += 1 var j = 0 for custom_story_path in _custom_stories: if custom_story_path != null && !custom_story_path.is_empty(): _available_stories.append({ NAME: custom_story_path.get_file(), FILE_PATH: ProjectSettings.localize_path(custom_story_path), STORY_ORIGIN: StoryOrigin.FILE }) j += 1 func _update_story_picker(selected_path = null): _pick_story_button.clear() var i = 0 for story in _available_stories: _pick_story_button.add_item(story[NAME], i) if selected_path != null && story[FILE_PATH] == selected_path: _current_story_index = i _pick_story_button.selected = _current_story_index i += 1 if _available_stories.size() > 0: if _current_story_index == -1: _current_story_index = 0 _pick_story_button.selected = _current_story_index _pick_story_button.visible = true else: _current_story_index = -1 _pick_story_button.selected = _current_story_index _pick_story_button.visible = false func _continue_story(): while _ink_player.can_continue: var text = _ink_player.continue_story() if text.right(text.length() - 1) == "\n": text.erase(text.length() - 1, 1) var text_label = Label.new() text_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART text_label.text = text _story_container.add_child(text_label) var tags = _ink_player.current_tags if !tags.is_empty(): var tag_label = Label.new() tag_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART tag_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER tag_label.text = "# " + ", ".join(PackedStringArray(tags)) tag_label.add_theme_color_override("font_color", Color(1, 1, 1, 0.4)) _story_container.add_child(tag_label) var separator = HSeparator.new() _story_container.add_child(separator) if _ink_player.current_choices.size() > 0: var i = 0 for choice in _ink_player.current_choices: var button = Button.new() button.text = choice.text button.connect("pressed", Callable(self, "_choice_button_pressed").bind(i)) _choices_container.add_child(button) i += 1 _choice_area_container.visible = true else: var label = Label.new() label.text = "End of the story." label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT _story_container.add_child(label) _choice_area_container.visible = false func _get_current_story_file_path(): if _current_story_index >= 0 && _current_story_index < _available_stories.size(): return _available_stories[_current_story_index][FILE_PATH] else: return null func _clear_content(): for child in _story_container.get_children(): _story_container.remove_child(child) child.queue_free() func _clear_choices(): for child in _choices_container.get_children(): _choices_container.remove_child(child) child.queue_free() func _disable_command_strip(disabled: bool): _pick_story_button.disabled = disabled _load_story_button.disabled = disabled _start_button.disabled = disabled _stop_button.disabled = disabled _clear_button.disabled = disabled func _connect_signals(): if configuration != null: var is_signal_connected := configuration.is_connected( "story_configuration_changed", Callable(self, "_configuration_changed") ) if !is_signal_connected: configuration.connect( "story_configuration_changed", Callable(self, "_configuration_changed") ) _ink_player.connect("loaded", Callable(self, "_story_loaded")) _pick_story_button.connect("item_selected", Callable(self, "_pick_story_button_selected")) _load_story_button.connect("pressed", Callable(self, "_load_story_button_pressed")) _start_button.connect("pressed", Callable(self, "_start_button_pressed")) _stop_button.connect("pressed", Callable(self, "_stop_button_pressed")) _clear_button.connect("pressed", Callable(self, "_clear_content")) _scroll_container.get_v_scroll_bar().connect("changed", Callable(self, "_scrollbar_changed"))