483 lines
16 KiB
GDScript
483 lines
16 KiB
GDScript
@tool
|
|
extends EditorPlugin
|
|
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Handles the inception of all editor-specific processes:
|
|
# Plant creation, painting, UI
|
|
# Controls the editing lifecycle of a Gardener
|
|
#-------------------------------------------------------------------------------
|
|
|
|
|
|
const Logger = preload("utility/logger.gd")
|
|
const Globals = preload("utility/globals.gd")
|
|
const FunLib = preload("utility/fun_lib.gd")
|
|
const ProjectSettingsManager = preload("utility/project_settings_manager.gd")
|
|
const Gardener = preload("gardener/gardener.gd")
|
|
const DebugViewer = preload("gardener/debug_viewer.gd")
|
|
const UI_SidePanel_SCN = preload("controls/side_panel/ui_side_panel.tscn")
|
|
const UI_SidePanel = preload("controls/side_panel/ui_side_panel.gd")
|
|
const ThemeAdapter = preload("controls/theme_adapter.gd")
|
|
const SceneConverter = preload("scene_converter/scene_converter.gd")
|
|
|
|
const Greenhouse = preload("greenhouse/greenhouse.gd")
|
|
const Greenhouse_Plant = preload("greenhouse/greenhouse_plant.gd")
|
|
const Greenhouse_PlantState = preload("greenhouse/greenhouse_plant_state.gd")
|
|
const Greenhouse_LODVariant = preload("greenhouse/greenhouse_LOD_variant.gd")
|
|
const Toolshed = preload("toolshed/toolshed.gd")
|
|
const Toolshed_Brush = preload("toolshed/toolshed_brush.gd")
|
|
|
|
const Console_SCN = preload("utility/console/console.tscn")
|
|
const Console = preload("utility/console/console.gd")
|
|
|
|
const gardener_icon:Texture2D = preload("icons/gardener_icon.svg")
|
|
|
|
|
|
var _side_panel:UI_SidePanel = null
|
|
var _base_control:Control = null
|
|
var _resource_previewer = null
|
|
var control_theme:Theme = null
|
|
|
|
var toolbar:HBoxContainer = null
|
|
var debug_view_menu:MenuButton
|
|
|
|
var active_gardener = null
|
|
var gardeners_in_tree:Array = []
|
|
var folding_states: Dictionary = {}
|
|
var scene_converter: SceneConverter = null
|
|
var _editor_camera_cache: Camera3D = null
|
|
|
|
var logger = null
|
|
var undo_redo = null
|
|
|
|
|
|
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Lifecycle
|
|
#-------------------------------------------------------------------------------
|
|
|
|
|
|
func _init():
|
|
DPON_FM.setup()
|
|
|
|
|
|
# Most lifecycle functions here and later on are restricted as editor-only
|
|
# Editing plants without an editor is not currently supported
|
|
func _ready():
|
|
# Is calling it from _ready() the correct way to use it?
|
|
# See https://github.com/godotengine/godot/pull/9099
|
|
# And https://github.com/godotengine/godot/issues/6869
|
|
set_input_event_forwarding_always_enabled()
|
|
|
|
if !Engine.is_editor_hint(): return
|
|
|
|
logger = Logger.get_for(self)
|
|
|
|
if Engine.is_editor_hint():
|
|
undo_redo = get_undo_redo()
|
|
else:
|
|
undo_redo = UndoRedo.new()
|
|
|
|
# Using selection to start/stop editing of chosen Gardener
|
|
get_editor_interface().get_selection().selection_changed.connect(selection_changed)
|
|
get_tree().node_added.connect(on_tree_node_added)
|
|
get_tree().node_removed.connect(on_tree_node_removed)
|
|
|
|
|
|
|
|
func _enter_tree():
|
|
# We need settings without editor too
|
|
ProjectSettingsManager.add_plugin_project_settings()
|
|
|
|
if !Engine.is_editor_hint(): return
|
|
|
|
_base_control = get_editor_interface().get_base_control()
|
|
_resource_previewer = get_editor_interface().get_resource_previewer()
|
|
|
|
adapt_editor_theme()
|
|
# TODO: reimplement once functionality is merged in Godot 4.1
|
|
# https://github.com/godotengine/godot/pull/62038
|
|
# ProjectSettings.project_settings_changed.connect(_on_project_settings_changed)
|
|
|
|
scene_converter = SceneConverter.new()
|
|
scene_converter.setup(_base_control)
|
|
_side_panel = UI_SidePanel_SCN.instantiate()
|
|
_side_panel.theme = control_theme
|
|
|
|
make_debug_view_menu()
|
|
|
|
toolbar = HBoxContainer.new()
|
|
toolbar.add_child(VSeparator.new())
|
|
toolbar.add_child(debug_view_menu)
|
|
toolbar.visible = false
|
|
|
|
add_custom_types()
|
|
add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_SIDE_RIGHT, _side_panel)
|
|
add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, toolbar)
|
|
selection_changed()
|
|
|
|
|
|
func _exit_tree():
|
|
if !Engine.is_editor_hint(): return
|
|
|
|
if is_instance_valid(scene_converter):
|
|
scene_converter.destroy()
|
|
scene_converter.queue_free()
|
|
|
|
set_gardener_edit_state(null)
|
|
remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_SIDE_RIGHT, _side_panel)
|
|
remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, toolbar)
|
|
remove_custom_types()
|
|
|
|
if is_instance_valid(_side_panel):
|
|
_side_panel.queue_free()
|
|
if is_instance_valid(toolbar):
|
|
toolbar.queue_free()
|
|
|
|
|
|
# Previously here was '_apply_changes', but it fired even when scene was closed without saving
|
|
# '_save_external_data' respects saving/not saving choice
|
|
func _save_external_data():
|
|
if !Engine.is_editor_hint(): return
|
|
|
|
apply_changes_to_gardeners()
|
|
|
|
|
|
func add_custom_types():
|
|
add_custom_type("Gardener", "Node3D", Gardener, gardener_icon)
|
|
add_custom_type("Greenhouse", "Resource", Greenhouse, null)
|
|
add_custom_type("Greenhouse_Plant", "Resource", Greenhouse_Plant, null)
|
|
add_custom_type("Greenhouse_PlantState", "Resource", Greenhouse_PlantState, null)
|
|
add_custom_type("Greenhouse_LODVariant", "Resource", Greenhouse_LODVariant, null)
|
|
add_custom_type("Toolshed", "Resource", Toolshed, null)
|
|
add_custom_type("Toolshed_Brush", "Resource", Toolshed_Brush, null)
|
|
|
|
|
|
func remove_custom_types():
|
|
remove_custom_type("Gardener")
|
|
remove_custom_type("Greenhouse")
|
|
remove_custom_type("Greenhouse_Plant")
|
|
remove_custom_type("Greenhouse_PlantState")
|
|
remove_custom_type("Greenhouse_LODVariant")
|
|
remove_custom_type("Toolshed")
|
|
remove_custom_type("Toolshed_Brush")
|
|
|
|
|
|
func on_tree_node_added(node:Node):
|
|
if FunLib.obj_is_script(node, Gardener):
|
|
gardeners_in_tree.append(node)
|
|
|
|
if node.has_method("dpon_testing_set_undo_redo"):
|
|
node.dpon_testing_set_undo_redo(undo_redo)
|
|
if node.has_method("dpon_testing_set_editor_selection"):
|
|
node.dpon_testing_set_editor_selection(get_editor_interface().get_selection())
|
|
|
|
|
|
func on_tree_node_removed(node:Node):
|
|
if FunLib.obj_is_script(node, Gardener):
|
|
gardeners_in_tree.erase(node)
|
|
|
|
|
|
# Call _apply_changes on all Gardeners in the scene
|
|
func apply_changes_to_gardeners():
|
|
for gardener in gardeners_in_tree:
|
|
if is_instance_valid(gardener) && is_instance_of(gardener, Gardener):
|
|
gardener._apply_changes()
|
|
|
|
|
|
func _get_plugin_name() -> String:
|
|
return 'SpatialGardener'
|
|
|
|
|
|
func _on_project_settings_changed():
|
|
if scene_converter:
|
|
scene_converter._on_project_settings_changed()
|
|
|
|
|
|
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Input
|
|
#-------------------------------------------------------------------------------
|
|
|
|
|
|
# Allows editor to forward us the spatial GUI input for any Gardener
|
|
func handles(object):
|
|
return is_instance_of(object, Gardener)
|
|
|
|
|
|
# Handle events
|
|
# Propagate editor camera
|
|
# Forward input to Gardener if selected
|
|
func _forward_3d_gui_input(camera, event):
|
|
_editor_camera_cache = camera
|
|
propagate_camera()
|
|
|
|
var handled = false
|
|
|
|
if is_instance_valid(active_gardener):
|
|
handled = active_gardener.forwarded_input(camera, event)
|
|
|
|
if !handled:
|
|
plugin_input(event)
|
|
|
|
return handled
|
|
|
|
|
|
func plugin_input(event):
|
|
if is_instance_of(event, InputEventKey) && !event.pressed:
|
|
if event.keycode == debug_get_dump_editor_tree_key():
|
|
debug_dump_editor_tree()
|
|
elif (event.keycode == get_focus_painter_key()
|
|
&& !Input.is_key_pressed(KEY_SHIFT) && !Input.is_key_pressed(KEY_CTRL) && !Input.is_key_pressed(KEY_ALT) && !Input.is_key_pressed(KEY_SYSREQ)):
|
|
focus_painter()
|
|
|
|
|
|
# A hack to propagate editor camera using _forward_3d_gui_input
|
|
func propagate_camera():
|
|
for gardener in gardeners_in_tree:
|
|
if is_instance_valid(gardener):
|
|
gardener.propagate_camera(_editor_camera_cache)
|
|
|
|
|
|
func on_debug_view_menu_id_pressed(id):
|
|
if is_instance_valid(active_gardener):
|
|
active_gardener.debug_view_flag_checked(debug_view_menu, id)
|
|
|
|
|
|
# A somewhat hacky way to focus editor camera on the painter
|
|
func focus_painter():
|
|
if !Engine.is_editor_hint(): return
|
|
if !active_gardener: return
|
|
|
|
var editor_selection:EditorSelection = get_editor_interface().get_selection()
|
|
if get_editor_interface().get_selection().selection_changed.is_connected(selection_changed):
|
|
get_editor_interface().get_selection().selection_changed.disconnect(selection_changed)
|
|
|
|
editor_selection.clear()
|
|
editor_selection.add_node(active_gardener.painter.paint_brush_node)
|
|
|
|
simulate_key(KEY_F)
|
|
# Have to delay that so input has time to process
|
|
call_deferred("restore_gardener_selection")
|
|
|
|
|
|
func simulate_key(keycode):
|
|
var event = InputEventKey.new()
|
|
event.keycode = keycode
|
|
event.pressed = true
|
|
Input.parse_input_event(event)
|
|
|
|
|
|
# Restore selection to seamlessly continue gardener editing
|
|
func restore_gardener_selection():
|
|
if !Engine.is_editor_hint(): return
|
|
|
|
if !get_editor_interface().get_selection().selection_changed.is_connected(selection_changed):
|
|
get_editor_interface().get_selection().selection_changed.connect(selection_changed)
|
|
|
|
if !active_gardener: return
|
|
|
|
var editor_selection:EditorSelection = get_editor_interface().get_selection()
|
|
editor_selection.clear()
|
|
editor_selection.add_node(active_gardener)
|
|
|
|
|
|
func get_focus_painter_key():
|
|
var key = FunLib.get_setting_safe("dreadpons_spatial_gardener/input_and_ui/focus_painter_key", KEY_Q)
|
|
return Globals.index_to_enum(key, Globals.KeyboardKey)
|
|
|
|
|
|
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# UI
|
|
#-------------------------------------------------------------------------------
|
|
|
|
|
|
func make_debug_view_menu():
|
|
debug_view_menu = DebugViewer.make_debug_view_menu()
|
|
debug_view_menu.get_popup().id_pressed.connect(on_debug_view_menu_id_pressed)
|
|
|
|
|
|
# Modify editor theme to use proper colors, margins, etc.
|
|
func adapt_editor_theme():
|
|
if !Engine.is_editor_hint(): return
|
|
|
|
var editorTheme = get_editor_interface().get_editor_theme()
|
|
control_theme = ThemeAdapter.adapt_theme(editorTheme)
|
|
|
|
|
|
# Gather folding states from side panel
|
|
func _get_state() -> Dictionary:
|
|
_side_panel.cleanup_folding_states(folding_states)
|
|
return {'folding_states': folding_states}
|
|
|
|
|
|
# Restore folding states for side panel
|
|
func _set_state(state: Dictionary):
|
|
folding_states = state.folding_states
|
|
|
|
|
|
func on_greenhouse_prop_action_executed(prop_action, final_val):
|
|
_side_panel.on_greenhouse_prop_action_executed(folding_states, active_gardener.greenhouse, prop_action, final_val)
|
|
|
|
|
|
func refresh_folding_state_for_greenhouse(greenhouse):
|
|
if greenhouse:
|
|
_side_panel.refresh_folding_states_for_greenhouse(folding_states, greenhouse)
|
|
|
|
|
|
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Gardener editing lifecycle
|
|
#-------------------------------------------------------------------------------
|
|
|
|
|
|
# Selection changed. Check if we should start/stop editing a Gardener
|
|
func selection_changed():
|
|
assert(get_editor_interface() && get_editor_interface().get_selection())
|
|
|
|
var selection = get_editor_interface().get_selection().get_selected_nodes()
|
|
handle_selected_gardener(selection)
|
|
|
|
|
|
func handle_selected_gardener(selection:Array):
|
|
var gardener = null
|
|
|
|
if selection.size() == 1:
|
|
# Find a Gardener in selection. If found more than one - abort because of ambiguity
|
|
for selected in selection:
|
|
if is_instance_of(selected, Gardener):
|
|
if gardener:
|
|
gardener = null
|
|
logger.warn("Cannot edit multiple Gardeners at once!")
|
|
if !gardener:
|
|
gardener = selected
|
|
|
|
if gardener:
|
|
if gardener == active_gardener: return
|
|
set_gardener_edit_state(selection[0])
|
|
else:
|
|
set_gardener_edit_state(null)
|
|
|
|
|
|
# Start/stop editing an active Gardener
|
|
func set_gardener_edit_state(gardener):
|
|
if (is_instance_valid(active_gardener) && active_gardener != gardener) || !gardener:
|
|
stop_gardener_edit()
|
|
|
|
if gardener:
|
|
start_gardener_edit(gardener)
|
|
|
|
|
|
func start_gardener_edit(gardener):
|
|
active_gardener = gardener
|
|
# TODO: figure out this weird bug :/
|
|
# basically, when having 2 scenes open, one with gardener and another NOT SAVED PREVIOUSLY (new empty scene)
|
|
# if you switch to an empty scene and save it, gardener loses all references (this doesnt happen is was saved at least once)
|
|
# To prevent that we call _ready each time we start gardener editing
|
|
# But this leads to some nodes being instanced again, even though they already exist
|
|
# This is a workaround that I haven't tested extensively, so it might backfire in the future
|
|
#
|
|
# There's more.
|
|
# Foldable states are reset two (maybe even all resource incuding gardeners and toolsheds, etc.?)
|
|
# Doesn't seem so, but still weird
|
|
#
|
|
# Worth noting, that this leads to a severe slowdown when clicking through gardeners in a scene
|
|
# Since stuff like "restoring after load" has to run again
|
|
# active_gardener._ready()
|
|
|
|
# I am testing a workaround of just restoring references, to avoid the unneccesary operations caused be previous solution
|
|
# UPD: Actually seems to work even without calling the method below. I'm confused
|
|
# I'll keep it here *just in case* the bug still persists but hides well
|
|
#
|
|
# UPD: when converting to Godot 4.0, this method resulted in enormous delay when selecting a Gardener for edit (3 seconds for empty Gardener)
|
|
# UPD: it seems most of the lag came from UI nodes, and method below is actually more-or-less fine
|
|
# it's still extra work, so wouldn't hurt to actually find a solution without it
|
|
active_gardener.restore_references()
|
|
|
|
active_gardener.tree_exited.connect(set_gardener_edit_state.bind(null))
|
|
active_gardener.greenhouse_prop_action_executed.connect(on_greenhouse_prop_action_executed)
|
|
active_gardener.start_editing(_base_control, _resource_previewer, undo_redo, _side_panel)
|
|
_side_panel.visible = true
|
|
toolbar.visible = true
|
|
active_gardener.up_to_date_debug_view_menu(debug_view_menu)
|
|
refresh_folding_state_for_greenhouse(active_gardener.greenhouse)
|
|
active_gardener.propagate_camera(_editor_camera_cache)
|
|
|
|
|
|
func stop_gardener_edit():
|
|
_get_state()
|
|
|
|
_side_panel.visible = false
|
|
toolbar.visible = false
|
|
|
|
if active_gardener:
|
|
active_gardener.stop_editing()
|
|
if active_gardener.tree_exited.is_connected(set_gardener_edit_state):
|
|
active_gardener.tree_exited.disconnect(set_gardener_edit_state)
|
|
if active_gardener.greenhouse_prop_action_executed.is_connected(on_greenhouse_prop_action_executed):
|
|
active_gardener.greenhouse_prop_action_executed.disconnect(on_greenhouse_prop_action_executed)
|
|
|
|
active_gardener = null
|
|
|
|
|
|
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Debug
|
|
#-------------------------------------------------------------------------------
|
|
|
|
|
|
# Dump the whole editor tree to console
|
|
func debug_dump_editor_tree():
|
|
debug_dump_node_descendants(get_editor_interface().get_editor_main_screen())
|
|
|
|
|
|
func debug_dump_node_descendants(node:Node, intendation:int = 0):
|
|
var intend_str = ""
|
|
for i in range(0, intendation):
|
|
intend_str += " "
|
|
var string = ""
|
|
|
|
if is_instance_of(node, Control):
|
|
string = "%s%s %s" % [intend_str, str(node), str(node.size)]
|
|
else:
|
|
string = "%s%s" % [intend_str, str(node)]
|
|
|
|
logger.info(string)
|
|
|
|
intendation += 1
|
|
for child in node.get_children():
|
|
debug_dump_node_descendants(child, intendation)
|
|
|
|
|
|
func debug_save_node_descendants(node:Node, owner_node: Node):
|
|
print("Adding %s" % [str(node)])
|
|
for child in node.get_children():
|
|
child.owner = owner_node
|
|
debug_save_node_descendants(child, owner_node)
|
|
|
|
if node == owner_node:
|
|
print("Saving dump...")
|
|
var packed_editor := PackedScene.new()
|
|
packed_editor.pack(node)
|
|
ResourceSaver.save(packed_editor, "res://packed_editor.tscn")
|
|
|
|
|
|
func debug_get_dump_editor_tree_key():
|
|
var key = FunLib.get_setting_safe("dreadpons_spatial_gardener/debug/dump_editor_tree_key", 0)
|
|
return Globals.index_to_enum(key, Globals.KeyboardKey)
|
|
|
|
|
|
func debug_toggle_console():
|
|
var current_scene := get_tree().get_current_scene()
|
|
if current_scene.has_node("Console") && is_instance_of(current_scene.get_node("Console"), Console):
|
|
current_scene.get_node("Console").queue_free()
|
|
else:
|
|
var console = Console_SCN.instantiate()
|
|
current_scene.add_child(console)
|