592 lines
19 KiB
GDScript
592 lines
19 KiB
GDScript
@tool
|
|
extends Control
|
|
|
|
@export var dialog_data: Resource = preload("res://addons/madtalk/runtime/madtalk_data.tres")
|
|
var current_sheet = null
|
|
|
|
# Scene templates
|
|
var DialogNode_template = preload("res://addons/madtalk/components/DialogNode.tscn")
|
|
var SideBar_SheetItem_template = preload("res://addons/madtalk/components/SideBar_SheetItem.tscn")
|
|
|
|
# Scene nodes
|
|
@onready var graph_area = get_node("GraphArea")
|
|
|
|
|
|
@onready var sidebar_sheetlist = get_node("SideBar/Content/SheetsScroll/VBox")
|
|
@onready var sidebar_current_panel = get_node("SideBar/Content/CurrentPanel")
|
|
@onready var SideBar_sheet_id = get_node("SideBar/Content/CurrentPanel/SheetIDLabel")
|
|
@onready var SideBar_sheet_desc = get_node("SideBar/Content/CurrentPanel/DescEdit")
|
|
@onready var SideBar_search = get_node("SideBar/Content/SearchEdit")
|
|
|
|
@onready var graph_area_popup = get_node("PopupMenu")
|
|
|
|
@onready var popup_delete_node = get_node("DialogDeleteNodePopup")
|
|
|
|
@onready var dialog_sheet_edit = get_node("DialogSheetEdit")
|
|
@onready var dialog_sheet_rename_error_popup = get_node("DialogSheetRenameError")
|
|
@onready var dialog_sheet_create_popup = get_node("DialogSheetCreated")
|
|
@onready var dialog_export = get_node("DialogExport")
|
|
@onready var dialog_import = get_node("DialogImport")
|
|
|
|
# Maps sequence ids to graph nodes
|
|
var sequence_map: Dictionary = {}
|
|
|
|
# Holds the node being deleted when user presses X
|
|
var deleting_node = null
|
|
|
|
# Holds the item object being dragged
|
|
var dragging_object = null
|
|
var hovering_object = null
|
|
|
|
|
|
func _ready() -> void:
|
|
pass
|
|
call_deferred("setup")
|
|
|
|
func setup():
|
|
if dialog_data.sheets.size() == 0:
|
|
create_new_sheet()
|
|
|
|
else:
|
|
open_sheet(dialog_data.sheets.keys()[0])
|
|
|
|
dialog_export.setup(dialog_data, current_sheet)
|
|
dialog_import.setup(dialog_data, current_sheet)
|
|
|
|
# Opens a sheet for the first time, or reopens (updates area content)
|
|
func open_sheet(sheet_id: String) -> void:
|
|
# FAILSAFE: We ignore this call if the sheet id is invalid
|
|
if not sheet_id in dialog_data.sheets:
|
|
print("Invalid sheet id \"%s\"" % sheet_id)
|
|
return
|
|
|
|
# Clear all current content in the graph area
|
|
for dialog_node in graph_area.get_children():
|
|
if dialog_node is DialogGraphNode:
|
|
graph_area.remove_child(dialog_node)
|
|
dialog_node.queue_free()
|
|
|
|
sequence_map.clear()
|
|
|
|
# Prepare new sheet
|
|
current_sheet = sheet_id
|
|
var sheet_data = dialog_data.sheets[sheet_id]
|
|
|
|
# First we build all nodes *without* updating them
|
|
for node_data in sheet_data.nodes:
|
|
var new_node = create_node_instance(node_data, false)
|
|
sequence_map[node_data.sequence_id] = new_node
|
|
|
|
# After we have all node instances available for connections,
|
|
# we update them
|
|
for sequence_id in sequence_map:
|
|
sequence_map[sequence_id].update_from_data()
|
|
|
|
graph_area.scroll_offset.y -= 1
|
|
update_sidebar()
|
|
rebuild_connections()
|
|
|
|
await get_tree().process_frame
|
|
graph_area.scroll_offset.y += 1
|
|
graph_area.queue_redraw()
|
|
|
|
func reopen_current_sheet():
|
|
open_sheet(current_sheet)
|
|
|
|
# Creates the visual representation of a node
|
|
# Does not modify the data structure
|
|
func create_node_instance(node_data: Resource, update_now: bool = true) -> DialogGraphNode:
|
|
var new_node: GraphNode = DialogNode_template.instantiate()
|
|
new_node.name = "DialogNode_ID%d" % node_data.sequence_id
|
|
new_node.main_editor = self
|
|
graph_area.add_child(new_node)
|
|
new_node.position_offset = node_data.position
|
|
new_node.connections_changed.connect(_on_node_connections_changed)
|
|
new_node.mouse_entered.connect(_on_sequence_mouse_entered.bind(new_node))
|
|
new_node.mouse_exited.connect(_on_sequence_mouse_exited.bind(new_node))
|
|
#new_node.connect("close_request", Callable(self, "_on_node_close_request").bind(new_node))
|
|
new_node.data = node_data # Assign the reference, not a copy
|
|
# Any changes to this node will reflect back in
|
|
# the main Resource
|
|
#new_node.show_close = (node_data.sequence_id != 0)
|
|
if (node_data.sequence_id != 0):
|
|
var new_close_btn = Button.new()
|
|
new_close_btn.text = " X "
|
|
new_close_btn.focus_mode = Control.FOCUS_NONE
|
|
new_node.get_titlebar_hbox().add_child(new_close_btn)
|
|
new_close_btn.pressed.connect(_on_node_close_request.bind(new_node))
|
|
|
|
# During sheet building not all nodes are ready so updating connections
|
|
# will fail. In such a case we skip this task and update all nodes at once
|
|
# later
|
|
if (update_now):
|
|
new_node.update_from_data()
|
|
|
|
return new_node
|
|
|
|
# Creates a new node, optionally creating the visual GraphNode
|
|
func create_new_node(graph_position: Vector2 = Vector2(0,0), create_visual_instance = false) -> DialogNodeData:
|
|
if not current_sheet:
|
|
return null
|
|
|
|
var sheet_data = dialog_data.sheets[current_sheet]
|
|
|
|
# Find next available sequence id
|
|
var next_available_id = sheet_data.next_sequence_id
|
|
for this_node in sheet_data.nodes:
|
|
if this_node.sequence_id >= next_available_id:
|
|
next_available_id = this_node.sequence_id+1
|
|
|
|
var new_data = DialogNodeData.new()
|
|
new_data.resource_scene_unique_id = Resource.generate_scene_unique_id()
|
|
new_data.position = graph_position
|
|
new_data.sequence_id = next_available_id
|
|
new_data.items = [] # New Array to avoid sharing references
|
|
new_data.options = [] # New Array to avoid sharing references
|
|
|
|
sheet_data.nodes.append(new_data)
|
|
sheet_data.next_sequence_id = next_available_id+1
|
|
|
|
# create_visual_instance is true when the node is created from user
|
|
# interaction ("New sequence" button). It is false when the data is being
|
|
# created procedurally and instances will be created later by open_sheet()
|
|
# DEPRECATED: now all methods call here with create_visual_instance=false
|
|
# and call open_sheet() afterwards
|
|
if create_visual_instance:
|
|
create_node_instance(new_data, true)
|
|
rebuild_connections() # Should not be needed but reduntant calls are harmless
|
|
|
|
return new_data
|
|
|
|
# Creates a new sheet, set as current, and returns the name of the sheet
|
|
func create_new_sheet() -> String:
|
|
# Find a suitable available name
|
|
var sheet_num = 1
|
|
var new_sheet_name = "new_sheet_1"
|
|
while new_sheet_name in dialog_data.sheets:
|
|
sheet_num += 1
|
|
new_sheet_name = "new_sheet_%d" % sheet_num
|
|
|
|
# Create the new sheet
|
|
var new_sheet_data = DialogSheetData.new() # default next_sequence_id=0
|
|
new_sheet_data.resource_scene_unique_id = Resource.generate_scene_unique_id()
|
|
new_sheet_data.sheet_id = new_sheet_name
|
|
new_sheet_data.nodes = [] # Forces a new array to avoid reference sharing
|
|
dialog_data.sheets[new_sheet_name] = new_sheet_data
|
|
current_sheet = new_sheet_name
|
|
|
|
# All sheets need at least one node with ID=0
|
|
# Create a node data item without creating the GraphNode instance, as
|
|
# it will be created later by open_sheet()
|
|
create_new_node(Vector2(0,0), false)
|
|
|
|
# Update sidebar and open sheet
|
|
update_sidebar()
|
|
open_sheet(new_sheet_name)
|
|
#rebuild_connections()
|
|
|
|
return new_sheet_name
|
|
|
|
|
|
# Connections are not build directly from UI
|
|
# Instead they are rebuilt from the Resource data objects every time
|
|
# This is the safest way to make sure there is never any difference between
|
|
# the visual representation and the underlying data
|
|
func rebuild_connections() -> void:
|
|
|
|
graph_area.clear_connections()
|
|
|
|
for sequence_id in sequence_map:
|
|
var dialog_node = sequence_map[sequence_id]
|
|
|
|
|
|
var sequence_data = dialog_node.data
|
|
|
|
# For each item in this sequence
|
|
for item_data in sequence_data.items:
|
|
# Do we have a connection?
|
|
if (item_data.connected_to_id > -1) and (item_data.port_index > -1):
|
|
var target_node = get_dialognode_by_id(item_data.connected_to_id)
|
|
if target_node:
|
|
graph_area.connect_node(dialog_node.name, item_data.port_index, target_node.name, 0)
|
|
|
|
# For each option in this sequence
|
|
for opt_data in sequence_data.options:
|
|
# Do we have a connection?
|
|
if (opt_data.connected_to_id > -1) and (opt_data.port_index > -1):
|
|
var target_node = get_dialognode_by_id(opt_data.connected_to_id)
|
|
if target_node:
|
|
graph_area.connect_node(dialog_node.name, opt_data.port_index, target_node.name, 0)
|
|
|
|
# If we have a continue option at the end
|
|
if sequence_data.continue_sequence_id > -1:
|
|
var target_node = get_dialognode_by_id(sequence_data.continue_sequence_id)
|
|
if target_node:
|
|
graph_area.connect_node(dialog_node.name, sequence_data.continue_port_index, target_node.name, 0)
|
|
|
|
|
|
|
|
|
|
# Given a sequence id, returns the corresponding GraphNode object
|
|
func get_dialognode_by_id(id: int) -> DialogGraphNode:
|
|
if not id in sequence_map:
|
|
print("Error: node ID %s not found in sequence map" % id)
|
|
return null
|
|
|
|
return sequence_map[id]
|
|
|
|
func update_sidebar():
|
|
# === Update current sheet
|
|
if current_sheet:
|
|
var sheet_data = dialog_data.sheets[current_sheet]
|
|
SideBar_sheet_id.text = sheet_data.sheet_id
|
|
SideBar_sheet_desc.text = sheet_data.sheet_description
|
|
sidebar_current_panel.show()
|
|
|
|
else:
|
|
sidebar_current_panel.hide()
|
|
|
|
# === Update list
|
|
|
|
# Remove old items
|
|
for old_item in sidebar_sheetlist.get_children():
|
|
sidebar_sheetlist.remove_child(old_item)
|
|
old_item.queue_free()
|
|
|
|
# Add new items
|
|
var search_term = SideBar_search.text
|
|
for this_sheet_id in dialog_data.sheets:
|
|
var new_item_data = dialog_data.sheets[this_sheet_id]
|
|
# If there is no search, or search shows up in either id or description:
|
|
if (search_term == "") or (search_term in this_sheet_id) or (search_term in new_item_data.sheet_description):
|
|
var new_item = SideBar_SheetItem_template.instantiate()
|
|
sidebar_sheetlist.add_child(new_item)
|
|
new_item.get_node("Panel/SheetLabel").text = new_item_data.sheet_id
|
|
new_item.get_node("Panel/DescriptionLabel").text = new_item_data.sheet_description
|
|
new_item.get_node("Panel/BtnOpen").connect("pressed", Callable(self, "_on_SideBar_Item_open").bind(new_item_data.sheet_id))
|
|
|
|
|
|
func _save_external_data():
|
|
var res_path = dialog_data.resource_path
|
|
ResourceSaver.save(dialog_data, res_path, 0)
|
|
|
|
# ==============================================================================
|
|
# UI CALLBACKS
|
|
|
|
|
|
## Distributes the input event to the appropriate method
|
|
func _on_GraphEdit_gui_input(event: InputEvent) -> void:
|
|
if (event is InputEventMouseButton) and (event.pressed):
|
|
match event.button_index:
|
|
MOUSE_BUTTON_LEFT:
|
|
_on_GraphEdit_left_click(event)
|
|
|
|
MOUSE_BUTTON_RIGHT:
|
|
_on_GraphEdit_right_click(event)
|
|
#if (event is InputEventMouseMotion):
|
|
# print( (graph_area.get_local_mouse_position() + graph_area.scroll_offset)/graph_area.zoom )
|
|
|
|
|
|
## Handles left clicks
|
|
func _on_GraphEdit_left_click(event: InputEvent) -> void:
|
|
# event.position is screen coordinate, not taking scroll into account
|
|
# graph_position is in node local coordinates
|
|
var graph_position = event.position + graph_area.scroll_offset
|
|
|
|
#print("LEFT CLICK: " + str(graph_position))
|
|
#print(graph_area.scroll_offset)
|
|
|
|
## Handles right clicks
|
|
func _on_GraphEdit_right_click(event: InputEvent) -> void:
|
|
# event.position is screen coordinate, not taking scroll into account
|
|
# graph_position is in node local coordinates
|
|
var graph_position = event.position + graph_area.scroll_offset
|
|
|
|
var cursor_position = Vector2(get_viewport().get_mouse_position() if get_viewport().gui_embed_subwindows else DisplayServer.mouse_get_position())
|
|
graph_area_popup.popup(Rect2(cursor_position, Vector2(10,10)))
|
|
|
|
|
|
# When a node item (message, condition, effect) is mouse-hovered
|
|
# Also happens if dragging started on another object
|
|
func _on_item_mouse_entered(obj: Control) -> void:
|
|
hovering_object = obj
|
|
if (dragging_object != null) and (dragging_object != obj):
|
|
obj.modulate.a = 0.7
|
|
obj.dragdrop_line.show()
|
|
|
|
# When a node item (message, condition, effect) loses mouse hover
|
|
# Also happens if dragging started on another object
|
|
func _on_item_mouse_exited(obj: Control) -> void:
|
|
if hovering_object == obj:
|
|
hovering_object = null
|
|
if dragging_object != null:
|
|
obj.modulate.a = 1.0
|
|
obj.dragdrop_line.hide()
|
|
|
|
|
|
func _on_sequence_mouse_entered(obj: Control):
|
|
hovering_object = obj
|
|
if dragging_object != null:
|
|
obj.modulate.a = 0.7
|
|
|
|
func _on_sequence_mouse_exited(obj: Control):
|
|
if hovering_object == obj:
|
|
hovering_object = null
|
|
if dragging_object != null:
|
|
obj.modulate.a = 1.0
|
|
|
|
|
|
# When the mouse is pressed down on a node item, which counts as
|
|
# start dragging it.
|
|
func _on_item_drag_started(obj: Control) -> void:
|
|
dragging_object = obj
|
|
|
|
|
|
# When the mouse is released after dragging an item. The obj argument
|
|
# contains the object being dragged, not the one under the cursor
|
|
func _on_item_drag_ended(obj: Control) -> void:
|
|
if (dragging_object != hovering_object):
|
|
move_item_by_instance(dragging_object, hovering_object)
|
|
|
|
if hovering_object and is_instance_valid(hovering_object):
|
|
hovering_object.modulate.a = 1.0
|
|
if not hovering_object is DialogGraphNode:
|
|
hovering_object.dragdrop_line.hide()
|
|
|
|
hovering_object = null
|
|
dragging_object = null
|
|
|
|
|
|
func move_item_by_instance(source_inst: Control, dest_inst):
|
|
if (not source_inst) or (not is_instance_valid(source_inst)):
|
|
return
|
|
if (not dest_inst) or (not is_instance_valid(dest_inst)):
|
|
return
|
|
|
|
var source_seq = source_inst.sequence_node
|
|
var data_seq_origin = source_seq.data
|
|
var source_index = data_seq_origin.items.find(source_inst.data)
|
|
var source_item_data = data_seq_origin.items[source_index]
|
|
|
|
var dest_seq
|
|
var data_seq_dest
|
|
var dest_index
|
|
|
|
if dest_inst is DialogGraphNode:
|
|
# Dragging onto a sequence header
|
|
dest_seq = dest_inst
|
|
data_seq_dest = dest_seq.data
|
|
dest_index = data_seq_dest.items.size()
|
|
|
|
else:
|
|
# Dragging onto another item
|
|
dest_seq = dest_inst.sequence_node
|
|
data_seq_dest = dest_seq.data
|
|
dest_index = data_seq_dest.items.find(dest_inst.data)
|
|
|
|
# There are special cases if the node is being reordered inside the same sequence
|
|
if (data_seq_origin == data_seq_dest):
|
|
if (dest_index == (source_index+1)):
|
|
# If the user dropped on the item immediately below, no operation is needed
|
|
return
|
|
|
|
elif (dest_index > source_index):
|
|
# If item is being moved below, removing it first will shift indices
|
|
# below that point, causing the reinsert to have an unintended
|
|
# extra offset of 1. So counteract is needed
|
|
dest_index -= 1
|
|
|
|
data_seq_origin.items.remove_at(source_index)
|
|
data_seq_dest.items.insert(dest_index, source_item_data)
|
|
|
|
source_seq.update_from_data()
|
|
dest_seq.update_from_data()
|
|
|
|
await get_tree().create_timer(0.02).timeout
|
|
call_deferred("rebuild_connections")
|
|
|
|
|
|
|
|
func _on_GraphArea_connection_request(from, from_slot, to, to_slot):
|
|
# Get the required data
|
|
var from_node = graph_area.get_node(NodePath(from))
|
|
var from_data = from_node.get_data_by_port(from_slot)
|
|
var to_node = graph_area.get_node(NodePath(to))
|
|
# to_slot is always 0 in this application
|
|
var to_sequence_id = to_node.data.sequence_id
|
|
|
|
# Make the connection in the underlying data resources
|
|
if from_data is DialogNodeData:
|
|
# This is a simple continue
|
|
from_data.continue_sequence_id = to_sequence_id
|
|
|
|
else:
|
|
# This is a branching
|
|
from_data.connected_to_id = to_sequence_id
|
|
|
|
rebuild_connections()
|
|
|
|
|
|
func _on_GraphArea_disconnection_request(from, from_slot, to, to_slot):
|
|
# Get the required data
|
|
var from_node = graph_area.get_node(NodePath(from))
|
|
var from_data = from_node.get_data_by_port(from_slot)
|
|
|
|
# Make the connection in the underlying data resources
|
|
if from_data is DialogNodeData:
|
|
# This is a simple continue
|
|
from_data.continue_sequence_id = -1
|
|
|
|
else:
|
|
# This is a branching
|
|
from_data.connected_to_id = -1
|
|
|
|
rebuild_connections()
|
|
|
|
|
|
func _on_node_connections_changed() -> void:
|
|
rebuild_connections()
|
|
|
|
func _on_node_close_request(node_object) -> void:
|
|
deleting_node = node_object
|
|
popup_delete_node.popup_centered()
|
|
|
|
|
|
func _on_SideBar_SearchEdit_text_changed(new_text) -> void:
|
|
update_sidebar()
|
|
|
|
|
|
func _on_GraphArea_PopupMenu_id_pressed(id) -> void:
|
|
match id:
|
|
0:
|
|
# Create new node
|
|
|
|
# graph_area_popup.rect_position is screen coordinate, not taking scroll into account
|
|
# graph_position is in node local coordinates
|
|
#var cursor_position = Vector2(graph_area_popup.position)
|
|
#var graph_position = Vector2(cursor_position) + Vector2(graph_area.scroll_offset)
|
|
#var graph_position = Vector2(graph_area_popup.position) + Vector2(graph_area.scroll_offset)
|
|
var graph_position = Vector2((graph_area.get_local_mouse_position() + graph_area.scroll_offset)/graph_area.zoom)
|
|
|
|
create_new_node(graph_position - Vector2(100,10), false)
|
|
open_sheet(current_sheet)
|
|
|
|
|
|
func _on_DialogDeleteNodePopup_confirmed() -> void:
|
|
if (not current_sheet) or (not deleting_node):
|
|
return
|
|
|
|
var sheet_data = dialog_data.sheets[current_sheet]
|
|
var node_data = deleting_node.data
|
|
|
|
sheet_data.nodes.erase(node_data)
|
|
|
|
# Reopens the sheet to update area
|
|
open_sheet(current_sheet)
|
|
|
|
|
|
func _on_BtnEditSheet_pressed() -> void:
|
|
if not current_sheet:
|
|
return
|
|
|
|
var sheet_data = dialog_data.sheets[current_sheet]
|
|
dialog_sheet_edit.open(sheet_data)
|
|
|
|
func _on_DialogSheetEdit_sheet_saved(sheet_id, sheet_desc, delete_word) -> void:
|
|
if not current_sheet:
|
|
return
|
|
|
|
# Is the user requesting to delete the sheet?
|
|
if delete_word == "delete":
|
|
dialog_data.sheets[current_sheet].nodes = [] # Discards references to node data
|
|
dialog_data.sheets[current_sheet] = null
|
|
dialog_data.sheets.erase(current_sheet)
|
|
|
|
# If this was the last sheet, we create a new one
|
|
if dialog_data.sheets.size() == 0:
|
|
# This method creates a new sheet, sets as current and returns the
|
|
# new sheet name
|
|
var _new_sheet = create_new_sheet()
|
|
|
|
# Otherwise select some other sheet
|
|
else:
|
|
current_sheet = dialog_data.sheets.keys()[0]
|
|
open_sheet(current_sheet)
|
|
|
|
# Hide the window
|
|
dialog_sheet_edit.hide()
|
|
|
|
update_sidebar()
|
|
# Stop here since the old sheet is no longer valid
|
|
return
|
|
|
|
# Otherwise the user is editting the sheet
|
|
|
|
var sheet_data = dialog_data.sheets[current_sheet]
|
|
|
|
# Check if the sheet is being renamed and if the name does not collide
|
|
if sheet_id != sheet_data.sheet_id:
|
|
# User wants to rename
|
|
# Check if this id is invalid or being used
|
|
if (sheet_id == "") or (sheet_id in dialog_data.sheets):
|
|
# Show an error message instead
|
|
dialog_sheet_rename_error_popup.popup_centered()
|
|
# Stop here and don't make any changes
|
|
return
|
|
|
|
else:
|
|
# Rename the sheet
|
|
dialog_data.sheets.erase(current_sheet)
|
|
sheet_data.sheet_id = sheet_id
|
|
dialog_data.sheets[sheet_id] = sheet_data
|
|
current_sheet = sheet_id
|
|
|
|
# Change description
|
|
sheet_data.sheet_description = sheet_desc
|
|
|
|
# Editting sheet details (ID, description) does not change node content
|
|
# so calling open_sheet() is not required
|
|
|
|
# Update current sheet panel and listing
|
|
update_sidebar()
|
|
# Hide window
|
|
dialog_sheet_edit.hide()
|
|
|
|
#var mtdefs = MTDefs.new()
|
|
#mtdefs.debug_resource(dialog_data)
|
|
|
|
func _on_SideBar_Item_open(sheet_id) -> void:
|
|
open_sheet(sheet_id)
|
|
|
|
func _on_BtnNewSheet_pressed() -> void:
|
|
create_new_sheet()
|
|
|
|
# Open sheet edit window
|
|
_on_BtnEditSheet_pressed()
|
|
|
|
# Inform user about successful sheet creation and edit window
|
|
dialog_sheet_create_popup.popup_centered()
|
|
|
|
|
|
func _on_BtnSaveDB_pressed():
|
|
var res_path = dialog_data.resource_path
|
|
ResourceSaver.save(dialog_data, res_path, 0)
|
|
|
|
|
|
func _on_ImportExport_BtnExport_pressed() -> void:
|
|
dialog_export.set_current_sheet(current_sheet, true)
|
|
dialog_export.refresh_export_sheet_list()
|
|
dialog_export.popup_centered()
|
|
|
|
|
|
func _on_ImportExport_BtnImportSheet_pressed() -> void:
|
|
dialog_import.set_current_sheet(current_sheet)
|
|
dialog_import.reset_and_show()
|
|
|
|
|
|
func _on_dialog_import_import_executed(destination_sheet: String) -> void:
|
|
if destination_sheet != "":
|
|
open_sheet(destination_sheet)
|
|
else:
|
|
reopen_current_sheet()
|