mirror of
https://github.com/drwhut/tabletop-club.git
synced 2025-05-05 15:32:56 +00:00
432 lines
14 KiB
GDScript
432 lines
14 KiB
GDScript
# tabletop-club
|
|
# Copyright (c) 2020-2024 Benjamin 'drwhut' Beddows.
|
|
# Copyright (c) 2021-2024 Tabletop Club contributors (see game/CREDITS.tres).
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in all
|
|
# copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
# SOFTWARE.
|
|
|
|
tool
|
|
extends WindowDialog
|
|
|
|
class_name SaveDialog
|
|
|
|
signal load_file(path)
|
|
signal save_file(path)
|
|
|
|
enum {
|
|
CONTEXT_RENAME,
|
|
CONTEXT_DUPLICATE,
|
|
CONTEXT_DELETE
|
|
}
|
|
|
|
enum {
|
|
SORT_LAST_MODIFIED,
|
|
SORT_NAME
|
|
}
|
|
|
|
export(String) var save_dir: String = "user://" setget set_save_dir, get_save_dir
|
|
export(String) var save_ext: String = "tc"
|
|
export(bool) var save_mode: bool = false setget set_save_mode, get_save_mode
|
|
|
|
var _confirm_delete_dialog: ConfirmationDialog
|
|
var _confirm_overwrite_dialog: ConfirmationDialog
|
|
var _context_menu: PopupMenu
|
|
var _file_name_edit: LineEdit
|
|
var _invalid_name_dialog: AcceptDialog
|
|
var _load_save_button: Button
|
|
var _rename_line_edit: LineEdit
|
|
var _rename_window: WindowDialog
|
|
var _save_container: VBoxContainer
|
|
var _sort_by_button: OptionButton
|
|
|
|
var _context_file_entry: Dictionary = {}
|
|
|
|
var _save_preview = preload("res://Scenes/Game/UI/Previews/GenericPreview.tscn")
|
|
|
|
func _ready():
|
|
connect("gui_input", self, "_on_gui_input")
|
|
|
|
func _on_gui_input(event):
|
|
if event is InputEventMouseButton and event.pressed and event.doubleclick and event.button_index == BUTTON_LEFT:
|
|
_on_load_save_button_pressed()
|
|
|
|
# Clear the list of saves.
|
|
func clear() -> void:
|
|
for preview in _save_container.get_children():
|
|
_save_container.remove_child(preview)
|
|
preview.queue_free()
|
|
|
|
# Get the save directory.
|
|
# Returns: The save directory.
|
|
func get_save_dir() -> String:
|
|
return save_dir
|
|
|
|
# Get if the dialog is in save mode rather than load mode.
|
|
# Returns: If the dialog is in safe mode.
|
|
func get_save_mode() -> bool:
|
|
return save_mode
|
|
|
|
# Refresh the save list.
|
|
func refresh() -> void:
|
|
if Engine.is_editor_hint():
|
|
return
|
|
|
|
var dir = Directory.new()
|
|
if dir.open(save_dir) != OK:
|
|
push_error("Could not open the save directory at '%s'!" % save_dir)
|
|
return
|
|
|
|
var file_entry_list = []
|
|
dir.list_dir_begin(true, true)
|
|
|
|
var file_name = dir.get_next()
|
|
while file_name != "":
|
|
if not dir.current_is_dir():
|
|
if file_name.get_extension() == save_ext:
|
|
var file_path = save_dir + "/" + file_name
|
|
var file = File.new()
|
|
var modified_time = file.get_modified_time(file_path)
|
|
|
|
var modified_datetime = OS.get_datetime_from_unix_time(modified_time)
|
|
var modified_year = modified_datetime["year"]
|
|
var modified_month = modified_datetime["month"]
|
|
var modified_day = modified_datetime["day"]
|
|
var modified_hour = modified_datetime["hour"]
|
|
var modified_minute = modified_datetime["minute"]
|
|
var modified_second = modified_datetime["second"]
|
|
|
|
var modified_datetime_str = "%02d/%02d/%02d %02d:%02d:%02d" % [
|
|
modified_year, modified_month, modified_day, modified_hour,
|
|
modified_minute, modified_second]
|
|
|
|
# Don't display the file extension in the save list.
|
|
var display_name = file_name.get_basename()
|
|
|
|
var file_entry = {
|
|
"desc": tr("Modified: %s") % modified_datetime_str,
|
|
"modified_time": modified_time,
|
|
"name": display_name
|
|
}
|
|
|
|
# Check if there is an image file that goes with the save.
|
|
var image_path = save_dir + "/" + display_name + ".png"
|
|
if file.file_exists(image_path):
|
|
file_entry["texture_path"] = image_path
|
|
|
|
file_entry_list.append(file_entry)
|
|
|
|
file_name = dir.get_next()
|
|
|
|
match _sort_by_button.get_selected_id():
|
|
SORT_LAST_MODIFIED:
|
|
file_entry_list.sort_custom(self, "_sort_modified_time")
|
|
SORT_NAME:
|
|
file_entry_list.sort_custom(self, "_sort_name")
|
|
|
|
clear()
|
|
|
|
for file_entry in file_entry_list:
|
|
var preview = _save_preview.instance()
|
|
preview.imported_texture = false
|
|
preview.connect("clicked", self, "_on_preview_clicked")
|
|
_save_container.add_child(preview)
|
|
|
|
preview.set_entry(file_entry)
|
|
|
|
# Set the save directory.
|
|
# new_save_dir: The new save directory.
|
|
func set_save_dir(new_save_dir: String) -> void:
|
|
save_dir = new_save_dir
|
|
|
|
if visible:
|
|
refresh()
|
|
|
|
# Set if the dialog is in safe mode rather than load mode.
|
|
# new_save_mode: If the dialog shoulb be in save mode.
|
|
func set_save_mode(new_save_mode: bool) -> void:
|
|
save_mode = new_save_mode
|
|
|
|
window_title = tr("Save a file") if save_mode else tr("Load a file")
|
|
|
|
_file_name_edit.editable = save_mode
|
|
_load_save_button.text = tr("Save") if save_mode else tr("Load")
|
|
|
|
func _init():
|
|
var top_container = VBoxContainer.new()
|
|
top_container.anchor_bottom = ANCHOR_END
|
|
top_container.anchor_right = ANCHOR_END
|
|
add_child(top_container)
|
|
|
|
var filter_container = HBoxContainer.new()
|
|
filter_container.alignment = HALIGN_RIGHT
|
|
top_container.add_child(filter_container)
|
|
|
|
var sort_by_label = Label.new()
|
|
sort_by_label.text = tr("Sort:")
|
|
filter_container.add_child(sort_by_label)
|
|
|
|
_sort_by_button = OptionButton.new()
|
|
_sort_by_button.add_item(tr("Last Modified"), SORT_LAST_MODIFIED)
|
|
_sort_by_button.add_item(tr("Name"), SORT_NAME)
|
|
_sort_by_button.connect("item_selected", self, "_on_sort_by_selected")
|
|
filter_container.add_child(_sort_by_button)
|
|
|
|
var scroll_container = ScrollContainer.new()
|
|
scroll_container.size_flags_vertical = SIZE_EXPAND_FILL
|
|
scroll_container.mouse_filter = Control.MOUSE_FILTER_PASS
|
|
top_container.add_child(scroll_container)
|
|
|
|
_save_container = VBoxContainer.new()
|
|
_save_container.size_flags_horizontal = SIZE_EXPAND_FILL
|
|
scroll_container.add_child(_save_container)
|
|
|
|
var options_container = HBoxContainer.new()
|
|
top_container.add_child(options_container)
|
|
|
|
_file_name_edit = LineEdit.new()
|
|
_file_name_edit.size_flags_horizontal = SIZE_EXPAND_FILL
|
|
options_container.add_child(_file_name_edit)
|
|
|
|
_load_save_button = Button.new()
|
|
_load_save_button.connect("pressed", self, "_on_load_save_button_pressed")
|
|
options_container.add_child(_load_save_button)
|
|
|
|
_confirm_delete_dialog = ConfirmationDialog.new()
|
|
_confirm_delete_dialog.window_title = tr("Delete file?")
|
|
# The dialog text is set on popup.
|
|
_confirm_delete_dialog.dialog_autowrap = true
|
|
_confirm_delete_dialog.rect_size = Vector2(250, 100)
|
|
_confirm_delete_dialog.connect("confirmed", self, "_on_delete_confirmed")
|
|
add_child(_confirm_delete_dialog)
|
|
|
|
_confirm_overwrite_dialog = ConfirmationDialog.new()
|
|
_confirm_overwrite_dialog.window_title = tr("Overwrite file?")
|
|
# The dialog text is set on popup.
|
|
_confirm_overwrite_dialog.dialog_autowrap = true
|
|
_confirm_overwrite_dialog.rect_size = Vector2(250, 100)
|
|
_confirm_overwrite_dialog.connect("confirmed", self, "_on_save_confirmed")
|
|
add_child(_confirm_overwrite_dialog)
|
|
|
|
_context_menu = PopupMenu.new()
|
|
_context_menu.add_item(tr("Rename"), CONTEXT_RENAME)
|
|
_context_menu.add_item(tr("Duplicate"), CONTEXT_DUPLICATE)
|
|
_context_menu.add_item(tr("Delete"), CONTEXT_DELETE)
|
|
_context_menu.connect("id_pressed", self, "_on_context_menu_id_pressed")
|
|
add_child(_context_menu)
|
|
|
|
_invalid_name_dialog = AcceptDialog.new()
|
|
_invalid_name_dialog.window_title = tr("Invalid name!")
|
|
# We use String.is_valid_filename() to determine invalid characters.
|
|
_invalid_name_dialog.dialog_text = tr("File names cannot contain any of the following characters:\n%s") % ": / \\ ? * \" | % < >"
|
|
_invalid_name_dialog.dialog_autowrap = true
|
|
_invalid_name_dialog.rect_size = Vector2(250, 100)
|
|
add_child(_invalid_name_dialog)
|
|
|
|
_rename_window = WindowDialog.new()
|
|
_rename_window.window_title = tr("Rename file...")
|
|
_rename_window.rect_size = Vector2(300, 50)
|
|
add_child(_rename_window)
|
|
|
|
var rename_vbox_container = VBoxContainer.new()
|
|
rename_vbox_container.anchor_bottom = ANCHOR_END
|
|
rename_vbox_container.anchor_right = ANCHOR_END
|
|
_rename_window.add_child(rename_vbox_container)
|
|
|
|
var rename_label = Label.new()
|
|
rename_label.text = tr("Please enter a new name for the file:")
|
|
rename_vbox_container.add_child(rename_label)
|
|
|
|
var rename_hbox_container = HBoxContainer.new()
|
|
rename_vbox_container.add_child(rename_hbox_container)
|
|
|
|
_rename_line_edit = LineEdit.new()
|
|
_rename_line_edit.size_flags_horizontal = SIZE_EXPAND_FILL
|
|
rename_hbox_container.add_child(_rename_line_edit)
|
|
|
|
var rename_button = Button.new()
|
|
rename_button.text = tr("Rename")
|
|
rename_button.connect("pressed", self, "_on_rename_pressed")
|
|
rename_hbox_container.add_child(rename_button)
|
|
|
|
if not is_connected("about_to_show", self, "_on_about_to_show"):
|
|
connect("about_to_show", self, "_on_about_to_show")
|
|
if not is_connected("popup_hide", self, "_on_popup_hide"):
|
|
connect("popup_hide", self, "_on_popup_hide")
|
|
|
|
# Get the current file path.
|
|
# Returns: The current file path.
|
|
func _get_file_path() -> String:
|
|
var file_name = _file_name_edit.text.strip_edges().strip_escapes()
|
|
return save_dir + "/" + file_name + "." + save_ext
|
|
|
|
# Sort an array of file entries by modified time, descending.
|
|
# a: The first element.
|
|
# b: The second element.
|
|
func _sort_modified_time(a: Dictionary, b: Dictionary) -> bool:
|
|
return a["modified_time"] > b["modified_time"]
|
|
|
|
# Sort an array of file entries by name, ascending.
|
|
# a: The first element.
|
|
# b: The second element.
|
|
func _sort_name(a: Dictionary, b: Dictionary) -> bool:
|
|
return a["name"] < b["name"]
|
|
|
|
func _on_about_to_show():
|
|
refresh()
|
|
|
|
if save_mode:
|
|
# Give the user a default name for the save file that doesn't already
|
|
# exist.
|
|
var file_exists = true
|
|
var file_path = ""
|
|
var index = 0
|
|
|
|
while file_exists:
|
|
file_path = save_dir + "/New Game"
|
|
if index > 0:
|
|
file_path += " (%d)" % index
|
|
file_path += "." + save_ext
|
|
|
|
var file = File.new()
|
|
file_exists = file.file_exists(file_path)
|
|
index += 1
|
|
|
|
# Just use the file name without the extension.
|
|
var file_name = file_path.get_basename().get_file()
|
|
_file_name_edit.text = file_name
|
|
else:
|
|
# Let the player pick a save file to load.
|
|
_file_name_edit.text = ""
|
|
|
|
func _on_delete_confirmed():
|
|
if _context_file_entry.empty():
|
|
return
|
|
|
|
var save_path = save_dir + "/" + _context_file_entry["name"] + "." + save_ext
|
|
|
|
var paths_to_delete = [save_path]
|
|
if _context_file_entry.has("texture_path"):
|
|
paths_to_delete.append(_context_file_entry["texture_path"])
|
|
|
|
var dir = Directory.new()
|
|
for path in paths_to_delete:
|
|
if dir.file_exists(path):
|
|
if dir.remove(path) != OK:
|
|
push_error("Failed to delete the file '%s'!" % path)
|
|
else:
|
|
push_warning("File '%s' cannot be deleted, as it doesn't exist!" % path)
|
|
|
|
refresh()
|
|
|
|
func _on_save_confirmed():
|
|
var path = _get_file_path()
|
|
if save_mode:
|
|
emit_signal("save_file", path)
|
|
else:
|
|
emit_signal("load_file", path)
|
|
|
|
visible = false
|
|
|
|
func _on_context_menu_id_pressed(id: int):
|
|
if _context_file_entry.empty():
|
|
return
|
|
|
|
var file_name = _context_file_entry["name"]
|
|
|
|
match id:
|
|
CONTEXT_RENAME:
|
|
_rename_line_edit.text = file_name
|
|
_rename_window.popup_centered()
|
|
|
|
CONTEXT_DUPLICATE:
|
|
var old_base = save_dir + "/" + file_name
|
|
var new_base = old_base + " (Copy)"
|
|
|
|
var dir = Directory.new()
|
|
for ext in [save_ext, "png"]:
|
|
var old_path = old_base + "." + ext
|
|
var new_path = new_base + "." + ext
|
|
if dir.file_exists(old_path):
|
|
if dir.copy(old_path, new_path) != OK:
|
|
push_error("Failed to copy '%s' to '%s'!" % [old_path, new_path])
|
|
|
|
refresh()
|
|
|
|
CONTEXT_DELETE:
|
|
_confirm_delete_dialog.dialog_text = tr("Are you sure you want to delete '%s'?") % file_name
|
|
_confirm_delete_dialog.popup_centered()
|
|
|
|
func _on_load_save_button_pressed():
|
|
var file_name = _file_name_edit.text + "." + save_ext
|
|
if not file_name.is_valid_filename():
|
|
_invalid_name_dialog.popup_centered()
|
|
return
|
|
|
|
var path = _get_file_path()
|
|
var file = File.new()
|
|
if save_mode and file.file_exists(path):
|
|
var file_name_no_ext = path.get_basename().get_file()
|
|
_confirm_overwrite_dialog.dialog_text = tr("The file '%s' already exists. Are you sure you want to overwrite it?") % file_name_no_ext
|
|
_confirm_overwrite_dialog.popup_centered()
|
|
else:
|
|
_on_save_confirmed()
|
|
|
|
func _on_popup_hide():
|
|
clear()
|
|
|
|
func _on_preview_clicked(preview: GenericPreview, event: InputEventMouseButton):
|
|
if event.button_index == BUTTON_LEFT:
|
|
var file_entry = preview.get_entry()
|
|
_file_name_edit.text = file_entry["name"]
|
|
elif event.button_index == BUTTON_RIGHT:
|
|
_context_file_entry = preview.get_entry()
|
|
_context_menu.rect_position = get_viewport().get_mouse_position()
|
|
_context_menu.popup()
|
|
|
|
func _on_rename_pressed():
|
|
var file_name = _rename_line_edit.text
|
|
if not file_name.is_valid_filename():
|
|
_invalid_name_dialog.popup_centered()
|
|
return
|
|
|
|
_rename_window.visible = false
|
|
|
|
if _context_file_entry.empty():
|
|
return
|
|
|
|
var old_name = _context_file_entry["name"]
|
|
var new_name = _rename_line_edit.text.strip_edges().strip_escapes()
|
|
if new_name.empty():
|
|
return
|
|
|
|
var old_base = save_dir + "/" + old_name
|
|
var new_base = save_dir + "/" + new_name
|
|
|
|
var dir = Directory.new()
|
|
for ext in [save_ext, "png"]:
|
|
var old_path = old_base + "." + ext
|
|
var new_path = new_base + "." + ext
|
|
if dir.file_exists(old_path):
|
|
if dir.rename(old_path, new_path) != OK:
|
|
push_error("Failed to move '%s' to '%s'!" % [old_path, new_path])
|
|
|
|
refresh()
|
|
|
|
func _on_sort_by_selected(_index: int):
|
|
refresh()
|