miggor-StreamGraph/graph_node_renderer/deck_holder_renderer.gd

512 lines
18 KiB
GDScript3
Raw Normal View History

# (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
signal quit_completed
func _ready() -> void:
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 = not 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") and not 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
no_obsws.event_received.connect(
func(m: NoOBSWS.Message):
Connections._obs_event_received(m.get_data())
)
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))
2023-11-25 12:01:17 +01:00
## 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 not 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()
2023-11-25 12:01:17 +01:00
## 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)
2023-11-25 12:01:17 +01:00
## 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)
2023-11-25 12:01:17 +01:00
## 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 not 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)
2023-11-25 12:01:17 +01:00
## 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)
2023-11-25 12:01:17 +01:00
## 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.subscriptions = obs_setup_dialog.get_subscriptions()
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)
OBSWebsocketSetupDialog.ConnectionState.CONNECTED:
no_obsws.disconnect_from_obsws()
obs_setup_dialog.set_button_state(OBSWebsocketSetupDialog.ConnectionState.DISCONNECTED)
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, not c)
get_tree().get_root().gui_embed_subwindows = not c
RendererPersistence.set_value(PERSISTENCE_NAMESPACE, "config", "embed_subwindows", not 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()
@warning_ignore("int_as_enum_without_cast")
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 request_quit() -> void:
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 range(tab_container.get_tab_count() - 1, -1, -1):
await close_tab(i)
#get_tree().quit()
quit_completed.emit()
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()
quit_completed.emit()
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")