miggor-StreamGraph/graph_node_renderer/deck_holder_renderer.gd
Lera Elvoé f720efcc72 lib group storage rework (#162)
saving decks that use lib groups will no longer save the whole file path to that library (at the expense of the structure needing to be the same)

also some ui/ux improvements:
- more menus in sidebar remember their collapsed state between deck/node switches
- adding a lib group will name the group node appropriately
- save dialog properly remembers the most recent path when invoked via ctrl+s and the deck hasn't been saved before

Reviewed-on: https://codeberg.org/StreamGraph/StreamGraph/pulls/162
Co-authored-by: Lera Elvoé <yagich@poto.cafe>
Co-committed-by: Lera Elvoé <yagich@poto.cafe>
2024-05-26 14:13:42 +00:00

695 lines
25 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
@export var DEBUG_NODES_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
## 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,
RPC,
}
@onready var connections_popup_menu: PopupMenu = %Connections
enum DebugMenuId {
DECKS,
NODES,
EMBED_SUBWINDOWS,
}
@onready var debug_popup_menu: PopupMenu = %Debug
enum HelpMenuId {
DOCS,
ABOUT,
}
@onready var about_dialog: AcceptDialog = %AboutDialog
enum EditMenuId {
COPY,
PASTE,
DUPLICATE,
SETTINGS,
}
@onready var edit_popup_menu: PopupMenu = %Edit
@onready var settings_dialog: SettingsDialog = %SettingsDialog
## 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
@onready var sidebar_split: HSplitContainer = %SidebarSplit
@onready var sidebar: Sidebar = %Sidebar as Sidebar
@onready var compat_dialog: ConfirmationDialog = %CompatDialog
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)
)
RendererShortcuts.load_overrides()
reset_popup_menu_shortcuts()
add_recents_to_menu()
tab_container.tab_close_requested.connect(request_tab_close)
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)
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)
)
sidebar_split.dragger_visibility = SplitContainer.DRAGGER_HIDDEN_COLLAPSED
sidebar.go_to_node_requested.connect(
func(node_id: String) -> void:
# we can reasonably assume its the current deck
var deck := get_active_deck()
var node := deck.get_node(node_id)
var deck_renderer := get_active_deck_renderer()
deck_renderer.focus_node(deck_renderer.get_node_renderer(node))
)
connections_popup_menu.set_item_tooltip(ConnectionsMenuId.RPC, "RPC support has been removed and will return in a future version.")
func reset_popup_menu_shortcuts() -> void:
file_popup_menu.set_item_shortcut(FileMenuId.NEW, RendererShortcuts.get_shortcut("new_deck"))
file_popup_menu.set_item_shortcut(FileMenuId.OPEN, RendererShortcuts.get_shortcut("open_deck"))
file_popup_menu.set_item_shortcut(FileMenuId.SAVE, RendererShortcuts.get_shortcut("save_deck"))
file_popup_menu.set_item_shortcut(FileMenuId.SAVE_AS, RendererShortcuts.get_shortcut("save_deck_as"))
file_popup_menu.set_item_shortcut(FileMenuId.CLOSE, RendererShortcuts.get_shortcut("close_deck"))
edit_popup_menu.set_item_shortcut(EditMenuId.COPY, RendererShortcuts.get_shortcut("copy_nodes"))
edit_popup_menu.set_item_shortcut(EditMenuId.PASTE, RendererShortcuts.get_shortcut("paste_nodes"))
edit_popup_menu.set_item_shortcut(EditMenuId.DUPLICATE, RendererShortcuts.get_shortcut("duplicate_nodes"))
edit_popup_menu.set_item_shortcut(EditMenuId.SETTINGS, RendererShortcuts.get_shortcut("settings"))
func reset_recents_shortcuts() -> void:
for i in recent_files.size():
var s := RendererShortcuts.get_shortcut("open_recent_deck_%s" % [i + 1])
file_popup_menu.set_item_shortcut(file_popup_menu.get_item_count() - recent_files.size() + i, s)
func _notification(what: int) -> void:
if what == RendererShortcuts.NOTIFICATION_SHORTCUTS_UPDATED:
reset_popup_menu_shortcuts()
func _on_tab_container_tab_about_to_change(previous_tab: int) -> void:
var deck := get_deck_at_tab(previous_tab)
if deck == null:
sidebar.set_edited_deck()
return
Util.safe_disconnect(deck.variables_updated, bottom_dock.rebuild_variable_tree)
var deck_renderer := get_deck_renderer_at_tab(previous_tab)
Util.safe_disconnect(deck_renderer.node_selected, set_sidebar_node)
Util.safe_disconnect(deck_renderer.node_deselected, set_sidebar_node)
Util.pop_batch(&"sidebar_signals")
func _on_tab_container_tab_changed(tab: int) -> void:
var deck := get_active_deck()
if deck == null:
sidebar.set_edited_deck()
return
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)
deck.variables_updated.connect(bottom_dock.rebuild_variable_tree.bind(deck.variable_stack))
sidebar.set_edited_deck(deck.id)
get_active_deck_renderer().node_selected.connect(set_sidebar_node)
get_active_deck_renderer().node_deselected.connect(set_sidebar_node)
#get_active_deck_renderer().nodes_about_to_delete.connect(sidebar.set_edited_node)
sidebar.refresh_node_list()
var batch := Util.batch_begin()
batch.add(deck.node_added, refresh_sidebar_node_list)
batch.add(deck.node_removed, refresh_sidebar_node_list)
#batch.add(get_active_deck_renderer().nodes_about_to_delete, sidebar.set_edited_node)
Util.push_batch(batch, &"sidebar_signals")
func set_sidebar_node(_node: Node) -> void:
var count = get_active_deck_renderer().get_selected_nodes().size()
if count != 1:
sidebar.set_edited_node()
return
var dnode := (get_active_deck_renderer().get_selected_nodes()[0] as DeckNodeRendererGraphNode).node
sidebar.set_edited_node(dnode._id)
func refresh_sidebar_node_list(_unused1 = null, _unused2 = null, _unused3 = null) -> void:
sidebar.refresh_node_list()
## 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:
request_tab_close(tab_container.get_current_tab())
_ when id in range(FileMenuId.RECENTS, FileMenuId.RECENTS + max_recents + 1):
open_deck_at_path(recent_files[id - FileMenuId.RECENTS - 1])
func _on_edit_id_pressed(id: int) -> void:
match id:
EditMenuId.SETTINGS:
settings_dialog.popup_centered()
EditMenuId.COPY:
var r := get_active_deck_renderer()
r._on_copy_nodes_request()
EditMenuId.PASTE:
var r := get_active_deck_renderer()
r._on_paste_nodes_request()
EditMenuId.DUPLICATE:
var r := get_active_deck_renderer()
r._on_duplicate_nodes_request()
func _on_edit_about_to_popup() -> void:
# enable/disable the copy/paste buttons depending on if there's a deck renderer and if it has nodes selected
if tab_container.get_tab_count() == 0:
edit_popup_menu.set_item_disabled(EditMenuId.COPY, true)
edit_popup_menu.set_item_disabled(EditMenuId.PASTE, true)
edit_popup_menu.set_item_disabled(EditMenuId.DUPLICATE, true)
return
edit_popup_menu.set_item_disabled(EditMenuId.PASTE, false)
var r := get_active_deck_renderer()
edit_popup_menu.set_item_disabled(EditMenuId.COPY, r.get_selected_nodes().size() == 0)
edit_popup_menu.set_item_disabled(EditMenuId.DUPLICATE, r.get_selected_nodes().size() == 0)
## 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 request_tab_close(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)
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)
var tc = tab_container.get_tab_count()
if tab_container.get_tab_count() == 0:
bottom_dock.variable_viewer.disable_new_button()
sidebar.set_edited_deck()
## 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 if not path.is_empty() else recent_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
# TODO: disabled opening multiple for now until better compat dialog method is found
file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
#file_dialog.title = "Open Deck(s)"
file_dialog.title = "Open Deck"
file_dialog.current_path = path + "/"
file_dialog.popup_centered()
#file_dialog.files_selected.connect(_on_file_dialog_open_files, CONNECT_ONE_SHOT)
file_dialog.file_selected.connect(open_deck_at_path, 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 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)
## 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 f := FileAccess.open(path, FileAccess.READ)
if f.get_error() != OK:
return
var deck_data: Dictionary = JSON.parse_string(f.get_as_text())
if DeckHolder.get_deck_compat(deck_data) != 0:
compat_dialog.set_meta(&"path", path)
file_dialog.hide()
compat_dialog.popup_centered()
return
open_deck_from_dict(deck_data, path)
func open_deck_from_dict(data: Dictionary, path: String) -> void:
var deck := DeckHolder.open_deck_from_dict(data, path)
if deck == null:
DeckHolder.logger.toast_error("Error loading deck at path: %s" % path)
return
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:
var r := get_deck_renderer_at_tab(tab)
if is_instance_valid(r.deck):
return get_deck_renderer_at_tab(tab).deck
else:
return null
## 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://") # what the fuck?
open_save_dialog(tab_container.get_tab_metadata(tab, "path", recent_path))
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)
if file_dialog.file_selected.is_connected(open_deck_at_path):
file_dialog.file_selected.disconnect(open_deck_at_path)
## 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 title := ""
if group_deck.is_library:
title = "(L) %s" % group_deck.id
else:
title = "(G) %s" % group_id.left(8)
var tab := tab_container.add_content(deck_renderer, title)
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.NODES:
var d := AcceptDialog.new()
var debug_nodes: DebugNodesList = DEBUG_NODES_LIST.instantiate()
d.add_child(debug_nodes)
d.canceled.connect(d.queue_free)
d.confirmed.connect(d.queue_free)
debug_nodes.build(get_active_deck())
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)
reset_recents_shortcuts()
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_key_input(event: InputEvent) -> void:
if RendererShortcuts.check_shortcut("toggle_bottom_dock", event):
bottom_dock.visible = !bottom_dock.visible
accept_event()
if RendererShortcuts.check_shortcut("toggle_sidebar", event):
sidebar.visible = !sidebar.visible
if sidebar.visible:
sidebar_split.dragger_visibility = SplitContainer.DRAGGER_VISIBLE
else:
sidebar_split.dragger_visibility = SplitContainer.DRAGGER_HIDDEN_COLLAPSED
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")
func _on_compat_dialog_confirmed() -> void:
var path: String = compat_dialog.get_meta(&"path", "")
var f := FileAccess.open(path, FileAccess.READ)
if f.get_error() != OK:
return
# save a backup
if path.split(".")[-2] != "old":
var backup_path := "%s.old.deck" % path.trim_suffix(".deck")
DirAccess.copy_absolute(path, backup_path)
var deck_data: Dictionary = JSON.parse_string(f.get_as_text())
open_deck_from_dict(deck_data, path)
# set it as dirty to make sure the user resaves
get_active_deck_renderer().dirty = true