miggor-StreamGraph/graph_node_renderer/deck_holder_renderer.gd
Lera Elvoé 759f6eff73 attempt to fix scene preload hell (#70)
every now and then, opening the project in Godot would throw an error saying that `res://graph_node_renderer/graph_node_renderer.tscn` scene was corrupted and could not be opened. the scene would refuse to open in the editor, throwing the same error. however, the app ran, instantiating the scene like nothing was wrong. the error would sometimes go away after a few restarts.

after many hours of searching up about it, i think i identified the problem. that specific scene file has nothing actually corrupted about it, and deleting and re-adding it solved the problem until some other random next time it popped up. see the relevant issues on godot's github:

https://github.com/godotengine/godot/issues/85907
https://github.com/godotengine/godot/issues/79545
https://github.com/godotengine/godot/issues/70985

for the time being, i've replaced most relevant scene preloads with exports. hopefully it doesn't happen now.

Reviewed-on: https://codeberg.org/StreamGraph/StreamGraph/pulls/70
Co-authored-by: Lera Elvoé <yagich@poto.cafe>
Co-committed-by: Lera Elvoé <yagich@poto.cafe>
2024-02-21 01:11:26 +00:00

501 lines
18 KiB
GDScript

# (c) 2023-present Eroax
# (c) 2023-present Yagich
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
extends Control
class_name DeckHolderRenderer
## Renderer class for [DeckHolder]
##
## Entry point for the [GraphEdit] based Renderer
## Reference to the base scene for [DeckRendererGraphEdit]
@export var DECK_SCENE: PackedScene
@export var DEBUG_DECKS_LIST: PackedScene
const PERSISTENCE_NAMESPACE := "default"
## Reference to the main windows [TabContainerCustom]
@onready var tab_container := %TabContainerCustom as TabContainerCustom
## Reference to the [FileDialog] used for File operations through the program.
@onready var file_dialog: FileDialog = $FileDialog
@export var new_deck_shortcut: Shortcut
@export var open_deck_shortcut: Shortcut
@export var save_deck_shortcut: Shortcut
@export var save_deck_as_shortcut: Shortcut
@export var close_deck_shortcut: Shortcut
## Enum for storing the Options in the "File" PopupMenu.
enum FileMenuId {
NEW,
OPEN,
SAVE = 3,
SAVE_AS,
CLOSE = 6,
RECENTS,
}
@onready var file_popup_menu: PopupMenu = %File as PopupMenu
var max_recents := 4
var recent_files := []
var recent_path: String
@onready var unsaved_changes_dialog_single_deck := $UnsavedChangesDialogSingleDeck as UnsavedChangesDialogSingleDeck
@onready var unsaved_changes_dialog: ConfirmationDialog = $UnsavedChangesDialog
enum ConnectionsMenuId {
OBS,
TWITCH,
}
enum DebugMenuId {
DECKS,
EMBED_SUBWINDOWS,
}
@onready var debug_popup_menu: PopupMenu = %Debug
enum HelpMenuId {
DOCS,
ABOUT,
}
@onready var about_dialog: AcceptDialog = %AboutDialog
## Weak Reference to the Deck that is currently going to be saved.
var _deck_to_save: WeakRef
@onready var no_obsws := %NoOBSWS as NoOBSWS
@onready var obs_setup_dialog := $OBSWebsocketSetupDialog as OBSWebsocketSetupDialog
@onready var twitch_setup_dialog := $Twitch_Setup_Dialog as TwitchSetupDialog
@onready var bottom_dock: BottomDock = %BottomDock
func _ready() -> void:
get_tree().auto_accept_quit = false
tab_container.add_button_pressed.connect(add_empty_deck)
tab_container.tab_changed.connect(_on_tab_container_tab_changed)
tab_container.tab_about_to_change.connect(_on_tab_container_tab_about_to_change)
RendererPersistence.init_namespace(PERSISTENCE_NAMESPACE)
var embed_subwindows: bool = RendererPersistence.get_or_create(PERSISTENCE_NAMESPACE, "config", "embed_subwindows", true)
debug_popup_menu.set_item_checked(
DebugMenuId.EMBED_SUBWINDOWS,
embed_subwindows
)
get_tree().get_root().gui_embed_subwindows = embed_subwindows
file_dialog.use_native_dialog = !embed_subwindows
recent_files = RendererPersistence.get_or_create(
PERSISTENCE_NAMESPACE, "config",
"recent_files", []
)
recent_path = RendererPersistence.get_or_create(
PERSISTENCE_NAMESPACE, "config",
"recent_path", OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS)
)
add_recents_to_menu()
tab_container.tab_close_requested.connect(
func(tab: int):
if tab_container.get_tab_metadata(tab, "dirty") && !tab_container.get_tab_metadata(tab, "group"):
unsaved_changes_dialog_single_deck.set_meta("tab", tab)
unsaved_changes_dialog_single_deck.show()
return
await close_tab(tab)
)
file_dialog.canceled.connect(disconnect_file_dialog_signals)
Connections.obs_websocket = no_obsws
Connections.twitch = %Twitch_Connection
Connections.twitch.chat_received_rich.connect(Connections._twitch_chat_received)
file_popup_menu.set_item_shortcut(FileMenuId.NEW, new_deck_shortcut)
file_popup_menu.set_item_shortcut(FileMenuId.OPEN, open_deck_shortcut)
file_popup_menu.set_item_shortcut(FileMenuId.SAVE, save_deck_shortcut)
file_popup_menu.set_item_shortcut(FileMenuId.SAVE_AS, save_deck_as_shortcut)
file_popup_menu.set_item_shortcut(FileMenuId.CLOSE, close_deck_shortcut)
bottom_dock.variable_viewer.top_field_edited.connect(
func(old_name: String, new_name: String, new_value: Variant) -> void:
get_active_deck_renderer().dirty = true
get_active_deck().update_variable(old_name, new_name, new_value)
)
bottom_dock.variable_viewer.top_field_removed.connect(
func(field_name: String) -> void:
get_active_deck_renderer().dirty = true
get_active_deck().remove_variable(field_name)
)
func _on_tab_container_tab_about_to_change(previous_tab: int) -> void:
var deck := get_deck_at_tab(previous_tab)
if deck.variables_updated.is_connected(bottom_dock.rebuild_variable_tree):
deck.variables_updated.disconnect(bottom_dock.rebuild_variable_tree)
func _on_tab_container_tab_changed(tab: int) -> void:
var is_group = tab_container.get_tab_metadata(tab, "group", false)
file_popup_menu.set_item_disabled(FileMenuId.SAVE, is_group)
file_popup_menu.set_item_disabled(FileMenuId.SAVE_AS, is_group)
bottom_dock.rebuild_variable_tree(get_active_deck().variable_stack)
var deck := get_active_deck()
deck.variables_updated.connect(bottom_dock.rebuild_variable_tree.bind(deck.variable_stack))
## Called when the File button in the [MenuBar] is pressed with the [param id]
## of the button within it that was pressed.
func _on_file_id_pressed(id: int) -> void:
match id:
FileMenuId.NEW:
add_empty_deck()
FileMenuId.OPEN:
open_open_dialog(recent_path)
FileMenuId.SAVE when tab_container.get_tab_count() > 0:
save_active_deck()
FileMenuId.SAVE_AS when tab_container.get_tab_count() > 0:
open_save_dialog(tab_container.get_tab_metadata(tab_container.get_current_tab(), "path"))
FileMenuId.CLOSE:
close_current_tab()
_ when id in range(FileMenuId.RECENTS, FileMenuId.RECENTS + max_recents + 1):
open_deck_at_path(recent_files[id - FileMenuId.RECENTS - 1])
## Adds an empty [DeckRendererGraphEdit] with a corresponding [Deck] for it's data.
func add_empty_deck() -> void:
var deck := DeckHolder.add_empty_deck()
var inst: DeckRendererGraphEdit = DECK_SCENE.instantiate()
inst.deck = deck
var tab := tab_container.add_content(inst, "<unsaved deck>")
tab_container.set_tab_metadata(tab, "id", deck.id)
tab_container.set_tab_metadata(tab, "group", false)
tab_container.set_tab_metadata(tab, "path", recent_path.path_join(""))
inst.group_enter_requested.connect(_on_deck_renderer_group_enter_requested)
inst.dirty_state_changed.connect(_on_deck_renderer_dirty_state_changed.bind(inst))
tab_container.set_current_tab(tab)
bottom_dock.variable_viewer.enable_new_button()
## Closes the current tab in [member tab_container]
func close_current_tab() -> void:
tab_container.close_tab(tab_container.get_current_tab())
func close_tab(tab: int) -> void:
if !tab_container.get_tab_metadata(tab, "group"):
var groups := DeckHolder.close_deck(tab_container.get_tab_metadata(tab, "id"))
# close tabs associated with this deck's groups
for group in groups:
for c_tab in range(tab_container.get_tab_count() - 1, -1, -1):
if tab_container.get_tab_metadata(c_tab, "id") == group:
tab_container.close_tab(c_tab)
await get_tree().process_frame
tab_container.close_tab(tab)
if tab_container.get_tab_count() == 0:
bottom_dock.variable_viewer.disable_new_button()
## Opens [member file_dialog] with the mode [member FileDialog.FILE_MODE_SAVE_FILE]
## as well as getting a weakref to the active [Deck]
func open_save_dialog(path: String) -> void:
file_dialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE
file_dialog.title = "Save a Deck"
file_dialog.current_path = path
_deck_to_save = weakref(get_active_deck())
file_dialog.popup_centered()
file_dialog.file_selected.connect(_on_file_dialog_save_file, CONNECT_ONE_SHOT)
## Opens [member file_dialog] with the mode [FileDialog.FILE_MODE_OPEN_FILES]
## with the supplied [param path]
func open_open_dialog(path: String) -> void:
file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILES
file_dialog.title = "Open Deck(s)"
file_dialog.current_path = path + "/"
file_dialog.popup_centered()
file_dialog.files_selected.connect(_on_file_dialog_open_files, CONNECT_ONE_SHOT)
## Connected to [signal FileDialog.save_file] on [member file_dialog].
## Saves the selected [Deck] if it still exists.
func _on_file_dialog_save_file(path: String) -> void:
var deck: Deck = _deck_to_save.get_ref() as Deck
if !deck:
return
deck.save_path = path
tab_container.set_tab_title(tab_container.get_current_tab(), path.get_file())
var renderer: DeckRendererGraphEdit = tab_container.get_content(tab_container.get_current_tab()) as DeckRendererGraphEdit
renderer.dirty = false
# TODO: put this into DeckHolder instead
var json := JSON.stringify(deck.to_dict(), "\t")
var f := FileAccess.open(path, FileAccess.WRITE)
f.store_string(json)
add_recent_file(get_active_deck().save_path)
recent_path = path.get_base_dir()
RendererPersistence.set_value(PERSISTENCE_NAMESPACE, "config", "recent_path", recent_path)
## Connected to [signal FileDialog.open_files] on [member file_dialog]. Opens
## the selected paths, instantiating [DeckRenderGraphEdit]s and [Deck]s for each.
func _on_file_dialog_open_files(paths: PackedStringArray) -> void:
for path in paths:
open_deck_at_path(path)
func open_deck_at_path(path: String) -> void:
for tab in tab_container.get_tab_count():
if tab_container.get_tab_metadata(tab, "path") == path:
tab_container.set_current_tab(tab)
return
var deck := DeckHolder.open_deck_from_file(path)
var inst: DeckRendererGraphEdit = DECK_SCENE.instantiate()
inst.deck = deck
var tab := tab_container.add_content(inst, path.get_file())
tab_container.set_tab_metadata(tab, "id", deck.id)
tab_container.set_tab_metadata(tab, "group", false)
tab_container.set_tab_metadata(tab, "path", path)
inst.initialize_from_deck()
inst.group_enter_requested.connect(_on_deck_renderer_group_enter_requested)
inst.dirty_state_changed.connect(_on_deck_renderer_dirty_state_changed.bind(inst))
add_recent_file(path)
recent_path = path.get_base_dir()
RendererPersistence.set_value(PERSISTENCE_NAMESPACE, "config", "recent_path", recent_path)
tab_container.set_current_tab(tab)
bottom_dock.variable_viewer.enable_new_button()
## Returns the current deck in the [member tab_container].
func get_active_deck() -> Deck:
return get_deck_at_tab(tab_container.get_current_tab())
## Returns the deck at [param tab] in the [member tab_container].
func get_deck_at_tab(tab: int) -> Deck:
return get_deck_renderer_at_tab(tab).deck
## Returns the current deck renderer in the [member tab_container].
func get_active_deck_renderer() -> DeckRendererGraphEdit:
return get_deck_renderer_at_tab(tab_container.get_current_tab())
## Returns the deck renderer at [param tab] in the [member tab_container].
func get_deck_renderer_at_tab(tab: int) -> DeckRendererGraphEdit:
if tab_container.is_empty():
return null
return tab_container.get_content(tab) as DeckRendererGraphEdit
## Saves the active [Deck] in [member tab_container]
func save_active_deck() -> void:
save_tab(tab_container.get_current_tab())
func save_tab(tab: int) -> void:
if tab_container.get_tab_metadata(tab, "group"):
return
var renderer := tab_container.get_content(tab) as DeckRendererGraphEdit
var deck := renderer.deck
if deck.save_path.is_empty():
open_save_dialog("res://")
else:
var json := JSON.stringify(deck.to_dict(), "\t")
var f := FileAccess.open(deck.save_path, FileAccess.WRITE)
f.store_string(json)
add_recent_file(deck.save_path)
renderer.dirty = false
## Disconnects the [FileDialog] signals if they are already connected.
func disconnect_file_dialog_signals() -> void:
if file_dialog.file_selected.is_connected(_on_file_dialog_save_file):
file_dialog.file_selected.disconnect(_on_file_dialog_save_file)
if file_dialog.files_selected.is_connected(_on_file_dialog_open_files):
file_dialog.files_selected.disconnect(_on_file_dialog_open_files)
## Connected to [signal DeckRenderGraphEdit.group_entered_request] to allow entering
## groups based off the given [param group_id] and [param deck]. As well as adding
## a corresponding tab to [member tab_container]
func _on_deck_renderer_group_enter_requested(group_id: String) -> void:
#var group_deck := deck.get_group(group_id)
for tab in tab_container.get_tab_count():
if tab_container.get_tab_metadata(tab, "id") == group_id:
tab_container.set_current_tab(tab)
return
var group_deck := DeckHolder.get_deck(group_id)
var deck_renderer: DeckRendererGraphEdit = DECK_SCENE.instantiate()
deck_renderer.deck = group_deck
deck_renderer.initialize_from_deck()
var tab := tab_container.add_content(deck_renderer, "(g) %s" % group_id.left(8))
tab_container.set_tab_metadata(tab, "id", group_id)
tab_container.set_tab_metadata(tab, "group", true)
deck_renderer.group_enter_requested.connect(_on_deck_renderer_group_enter_requested)
tab_container.set_current_tab(tab)
func _on_connections_id_pressed(id: int) -> void:
match id:
ConnectionsMenuId.OBS:
obs_setup_dialog.popup_centered()
ConnectionsMenuId.TWITCH:
twitch_setup_dialog.popup_centered()
func _on_obs_websocket_setup_dialog_connect_button_pressed(state: OBSWebsocketSetupDialog.ConnectionState) -> void:
match state:
OBSWebsocketSetupDialog.ConnectionState.DISCONNECTED:
obs_setup_dialog.set_button_state(OBSWebsocketSetupDialog.ConnectionState.CONNECTING)
no_obsws.connect_to_obsws(obs_setup_dialog.get_port(), obs_setup_dialog.get_password())
await no_obsws.connection_ready
obs_setup_dialog.set_button_state(OBSWebsocketSetupDialog.ConnectionState.CONNECTED)
func _process(delta: float) -> void:
DeckHolder.send_event(&"process", {"delta": delta})
func _on_debug_id_pressed(id: int) -> void:
match id:
DebugMenuId.DECKS:
var d := AcceptDialog.new()
var debug_decks: DebugDecksList = DEBUG_DECKS_LIST.instantiate()
d.add_child(debug_decks)
d.canceled.connect(d.queue_free)
d.confirmed.connect(d.queue_free)
debug_decks.item_pressed.connect(_on_debug_decks_viewer_item_pressed)
add_child(d)
d.popup_centered()
DebugMenuId.EMBED_SUBWINDOWS:
var c := debug_popup_menu.is_item_checked(id)
debug_popup_menu.set_item_checked(id, !c)
get_tree().get_root().gui_embed_subwindows = !c
RendererPersistence.set_value(PERSISTENCE_NAMESPACE, "config", "embed_subwindows", !c)
file_dialog.use_native_dialog = c
func _on_debug_decks_viewer_item_pressed(deck_id: String, instance_id: String) -> void:
if instance_id == "":
var deck := DeckHolder.get_deck(deck_id)
var inst: DeckRendererGraphEdit = DECK_SCENE.instantiate()
inst.deck = deck
var tab := tab_container.add_content(inst, "<Deck %s>" % [deck_id.left(8)])
tab_container.set_tab_metadata(tab, "id", deck.id)
inst.initialize_from_deck()
inst.group_enter_requested.connect(_on_deck_renderer_group_enter_requested)
tab_container.set_current_tab(tab)
else:
var deck := DeckHolder.get_group_instance(deck_id, instance_id)
var inst: DeckRendererGraphEdit = DECK_SCENE.instantiate()
inst.deck = deck
var tab := tab_container.add_content(inst, "<Group %s::%s>" % [deck_id.left(8), instance_id.left(8)])
tab_container.set_tab_metadata(tab, "id", deck.id)
inst.initialize_from_deck()
inst.group_enter_requested.connect(_on_deck_renderer_group_enter_requested)
tab_container.set_current_tab(tab)
func _on_deck_renderer_dirty_state_changed(renderer: DeckRendererGraphEdit) -> void:
var idx: int = range(tab_container.get_tab_count()).filter(
func(x: int):
return tab_container.get_content(x) == renderer
)[0]
var title := tab_container.get_tab_title(idx).trim_suffix("(*)")
if renderer.dirty:
tab_container.set_tab_title(idx, "%s(*)" % title)
else:
tab_container.set_tab_title(idx, title.trim_suffix("(*)"))
tab_container.set_tab_metadata(idx, "dirty", renderer.dirty)
func add_recent_file(path: String) -> void:
var item := recent_files.find(path)
if item == -1:
recent_files.push_front(path)
else:
recent_files.push_front(recent_files.pop_at(item))
recent_files = recent_files.slice(0, max_recents)
add_recents_to_menu()
func add_recents_to_menu() -> void:
if recent_files.is_empty():
return
if file_popup_menu.get_item_count() > FileMenuId.RECENTS + 1:
var end := file_popup_menu.get_item_count() - FileMenuId.RECENTS - 1
for i in end:
file_popup_menu.remove_item(file_popup_menu.get_item_count() - 1)
var reduce_length := recent_files.any(func(x: String): return x.length() > 35)
for i in recent_files.size():
var file = recent_files[i] as String
if reduce_length:
# shorten the basepath to be the first letter of all folders
var base: String = Array(
file.get_base_dir().split("/", false)
).reduce(
func(a: String, s: String):
return a + s[0] + "/",
"/")
var filename := file.get_file()
file = base.path_join(filename)
file_popup_menu.add_item(file)
var s := Shortcut.new()
var k := InputEventKey.new()
k.keycode = KEY_1 + i
k.ctrl_pressed = true
s.events.append(k)
file_popup_menu.set_item_shortcut(file_popup_menu.get_item_count() - 1, s)
RendererPersistence.set_value(PERSISTENCE_NAMESPACE, "config", "recent_files", recent_files)
func _notification(what: int) -> void:
if what == NOTIFICATION_WM_CLOSE_REQUEST:
RendererPersistence.commit(PERSISTENCE_NAMESPACE)
if range(tab_container.get_tab_count()).any(func(x: int): return tab_container.get_content(x).dirty):
unsaved_changes_dialog.show()
else:
#for i in tab_container.get_tab_count():
#close_tab(i)
for i in range(tab_container.get_tab_count() - 1, -1, -1):
await close_tab(i)
get_tree().quit()
func _on_unsaved_changes_dialog_single_deck_confirmed() -> void:
save_tab(unsaved_changes_dialog_single_deck.get_meta("tab"))
await close_tab(unsaved_changes_dialog_single_deck.get_meta("tab"))
func _on_unsaved_changes_dialog_single_deck_custom_action(action: StringName) -> void:
if action == &"force_close":
await close_tab(unsaved_changes_dialog_single_deck.get_meta("tab"))
unsaved_changes_dialog_single_deck.hide()
func _on_unsaved_changes_dialog_confirmed() -> void:
for i in range(tab_container.get_tab_count() - 1, -1, -1):
await close_tab(i)
get_tree().quit()
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("toggle_bottom_dock"):
bottom_dock.visible = !bottom_dock.visible
accept_event()
func _on_help_id_pressed(id: int) -> void:
match id:
HelpMenuId.ABOUT:
about_dialog.show()
HelpMenuId.DOCS:
OS.shell_open("https://codeberg.org/Eroax/StreamGraph/wiki")