1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-01-19 17:49:47 +00:00
Pixelorama/src/Main.gd
Emmanouil Papadeas 785701b093 Add an option to export a JSON file with the project data
Probably temporary, it might be a good idea to add a third "Data" tab in the export dialog, if we add stuff like exporting to Godot resources, such as AnimatedSprite, AnimationPlayer etc.
2024-04-06 03:30:50 +03:00

578 lines
20 KiB
GDScript

extends Control
const SPLASH_DIALOG_SCENE_PATH := "res://src/UI/Dialogs/SplashDialog.tscn"
var opensprite_file_selected := false
var redone := false
var is_quitting_on_save := false
var changed_projects_on_quit: Array[Project]
var cursor_image := preload("res://assets/graphics/cursor.png")
var splash_dialog: AcceptDialog:
get:
if not is_instance_valid(splash_dialog):
splash_dialog = load(SPLASH_DIALOG_SCENE_PATH).instantiate()
add_child(splash_dialog)
return splash_dialog
@onready var main_ui := $MenuAndUI/UI/DockableContainer as DockableContainer
@onready var backup_confirmation: ConfirmationDialog = $Dialogs/BackupConfirmation
@onready var save_sprite_html5: ConfirmationDialog = $Dialogs/SaveSpriteHTML5
@onready var quit_dialog: ConfirmationDialog = $Dialogs/QuitDialog
@onready var quit_and_save_dialog: ConfirmationDialog = $Dialogs/QuitAndSaveDialog
@onready var left_cursor: Sprite2D = $LeftCursor
@onready var right_cursor: Sprite2D = $RightCursor
class CLI:
static var args_list := {
["--version", "--pixelorama-version"]:
[CLI.print_version, "Prints current Pixelorama version"],
["--size"]: [CLI.print_project_size, "Prints size of the given project"],
["--framecount"]: [CLI.print_frame_count, "Prints total frames in the current project"],
["--export", "-e"]: [CLI.enable_export, "Indicates given project should be exported"],
["--spritesheet", "-s"]:
[CLI.enable_spritesheet, "Indicates given project should be exported as spritesheet"],
["--output", "-o"]: [CLI.set_output, "[path] Name of output file (with extension)"],
["--scale"]: [CLI.set_export_scale, "[integer] Scales up the export image by a number"],
["--frames", "-f"]: [CLI.set_frames, "[integer-integer] Used to specify frame range"],
["--direction", "-d"]: [CLI.set_direction, "[0, 1, 2] Specifies direction"],
["--json"]: [CLI.set_json, "Export the JSON data of the project"],
["--split-layers"]: [CLI.set_split_layers, "Each layer exports separately"],
["--help", "-h", "-?"]: [CLI.generate_help, "Displays this help page"]
}
static func generate_help(_project: Project, _next_arg: String):
var help := str(
(
"""
=========================================================================\n
Help for Pixelorama's CLI.
Usage:
\t%s [SYSTEM OPTIONS] -- [USER OPTIONS] [FILES]...
Use -h in place of [SYSTEM OPTIONS] to see [SYSTEM OPTIONS].
Or use -h in place of [USER OPTIONS] to see [USER OPTIONS].
some useful [SYSTEM OPTIONS] are:
--headless Run in headless mode.
--quit Close pixelorama after current command.
[USER OPTIONS]:\n
(The terms in [ ] reflect the valid type for corresponding argument).
"""
% OS.get_executable_path().get_file()
)
)
for command_group: Array in args_list.keys():
help += str(
var_to_str(command_group).replace("[", "").replace("]", "").replace('"', ""),
"\t\t".c_unescape(),
args_list[command_group][1],
"\n".c_unescape()
)
help += "========================================================================="
print(help)
## Dedicated place for command line args callables
static func print_version(_project: Project, _next_arg: String) -> void:
print(Global.current_version)
static func print_project_size(project: Project, _next_arg: String) -> void:
print(project.size)
static func print_frame_count(project: Project, _next_arg: String) -> void:
print(project.frames.size())
static func enable_export(_project: Project, _next_arg: String):
return true
static func enable_spritesheet(_project: Project, _next_arg: String):
Export.current_tab = Export.ExportTab.SPRITESHEET
return true
static func set_output(project: Project, next_arg: String) -> void:
if not next_arg.is_empty():
project.file_name = next_arg.get_basename()
var extension := next_arg.get_extension()
project.file_format = Export.get_file_format_from_extension(extension)
static func set_export_scale(_project: Project, next_arg: String) -> void:
if not next_arg.is_empty():
if next_arg.is_valid_float():
Export.resize = next_arg.to_float() * 100
static func set_frames(project: Project, next_arg: String) -> void:
if not next_arg.is_empty():
if next_arg.contains("-"):
var frame_numbers := next_arg.split("-")
if frame_numbers.size() > 1:
project.selected_cels.clear()
var frame_number_1 := 0
if frame_numbers[0].is_valid_int():
frame_number_1 = frame_numbers[0].to_int() - 1
frame_number_1 = clampi(frame_number_1, 0, project.frames.size() - 1)
var frame_number_2 := project.frames.size() - 1
if frame_numbers[1].is_valid_int():
frame_number_2 = frame_numbers[1].to_int() - 1
frame_number_2 = clampi(frame_number_2, 0, project.frames.size() - 1)
for frame in range(frame_number_1, frame_number_2 + 1):
project.selected_cels.append([frame, project.current_layer])
project.change_cel(frame)
Export.frame_current_tag = Export.ExportFrames.SELECTED_FRAMES
elif next_arg.is_valid_int():
var frame_number := next_arg.to_int() - 1
frame_number = clampi(frame_number, 0, project.frames.size() - 1)
project.selected_cels = [[frame_number, project.current_layer]]
project.change_cel(frame_number)
Export.frame_current_tag = Export.ExportFrames.SELECTED_FRAMES
static func set_direction(_project: Project, next_arg: String) -> void:
if not next_arg.is_empty():
next_arg = next_arg.to_lower()
if next_arg == "0" or next_arg.contains("forward"):
Export.direction = Export.AnimationDirection.FORWARD
elif next_arg == "1" or next_arg.contains("backward"):
Export.direction = Export.AnimationDirection.BACKWARDS
elif next_arg == "2" or next_arg.contains("ping"):
Export.direction = Export.AnimationDirection.PING_PONG
else:
print(Export.AnimationDirection.keys()[Export.direction])
else:
print(Export.AnimationDirection.keys()[Export.direction])
static func set_json(_project: Project, _next_arg: String) -> void:
Export.export_json = true
static func set_split_layers(_project: Project, _next_arg: String) -> void:
Export.split_layers = true
func _init() -> void:
if not DirAccess.dir_exists_absolute("user://backups"):
DirAccess.make_dir_recursive_absolute("user://backups")
Global.shrink = _get_auto_display_scale()
_handle_layout_files()
func _ready() -> void:
get_tree().set_auto_accept_quit(false)
Global.main_window.title = tr("untitled") + " - Pixelorama " + Global.current_version
Global.current_project.layers.append(PixelLayer.new(Global.current_project))
Global.current_project.frames.append(Global.current_project.new_empty_frame())
Global.animation_timeline.project_changed()
Import.import_brushes(Global.path_join_array(Global.data_directories, "Brushes"))
Import.import_patterns(Global.path_join_array(Global.data_directories, "Patterns"))
quit_and_save_dialog.add_button("Exit without saving", false, "ExitWithoutSaving")
Global.open_sprites_dialog.current_dir = Global.config_cache.get_value(
"data", "current_dir", OS.get_system_dir(OS.SYSTEM_DIR_DESKTOP)
)
Global.save_sprites_dialog.current_dir = Global.config_cache.get_value(
"data", "current_dir", OS.get_system_dir(OS.SYSTEM_DIR_DESKTOP)
)
var include_blended := CheckBox.new()
include_blended.name = "IncludeBlended"
include_blended.text = "Include blended images"
include_blended.tooltip_text = """
If enabled, the final blended images are also being stored in the pxo, for each frame.
This makes the pxo file larger and is useful for importing by third-party software
or CLI exporting. Loading pxo files in Pixelorama does not need this option to be enabled.
"""
include_blended.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
Global.save_sprites_dialog.get_vbox().add_child(include_blended)
_handle_cmdline_arguments()
get_tree().root.files_dropped.connect(_on_files_dropped)
if OS.get_name() == "Android":
OS.request_permissions()
_handle_backup()
await get_tree().process_frame
_setup_application_window_size()
_show_splash_screen()
Global.pixelorama_opened.emit()
func _input(event: InputEvent) -> void:
left_cursor.position = get_global_mouse_position() + Vector2(-32, 32)
right_cursor.position = get_global_mouse_position() + Vector2(32, 32)
if event is InputEventKey and (event.keycode == KEY_ENTER or event.keycode == KEY_KP_ENTER):
if get_viewport().gui_get_focus_owner() is LineEdit:
get_viewport().gui_get_focus_owner().release_focus()
# Taken from https://github.com/godotengine/godot/blob/3.x/editor/editor_settings.cpp#L1474
func _get_auto_display_scale() -> float:
if OS.get_name() == "macOS":
return DisplayServer.screen_get_max_scale()
var dpi := DisplayServer.screen_get_dpi()
var smallest_dimension := mini(
DisplayServer.screen_get_size().x, DisplayServer.screen_get_size().y
)
if dpi >= 192 && smallest_dimension >= 1400:
return 2.0 # hiDPI display.
elif smallest_dimension >= 1700:
return 1.5 # Likely a hiDPI display, but we aren't certain due to the returned DPI.
return 1.0
func _handle_layout_files() -> void:
if not DirAccess.dir_exists_absolute(Global.LAYOUT_DIR):
DirAccess.make_dir_absolute(Global.LAYOUT_DIR)
var dir := DirAccess.open(Global.LAYOUT_DIR)
var files := dir.get_files()
if files.size() == 0:
for layout in Global.default_layouts:
var file_name := layout.resource_path.get_basename().get_file() + ".tres"
ResourceSaver.save(layout, Global.LAYOUT_DIR.path_join(file_name))
files = dir.get_files()
for file in files:
var layout := ResourceLoader.load(Global.LAYOUT_DIR.path_join(file))
if layout is DockableLayout:
Global.layouts.append(layout)
# Save the layout every time it changes
layout.save_on_change = true
func _setup_application_window_size() -> void:
if DisplayServer.get_name() == "headless":
return
var root := get_tree().root
root.content_scale_aspect = Window.CONTENT_SCALE_ASPECT_IGNORE
root.content_scale_mode = Window.CONTENT_SCALE_MODE_DISABLED
# Set a minimum window size to prevent UI elements from collapsing on each other.
root.min_size = Vector2(1024, 576)
root.content_scale_factor = Global.shrink
if Global.font_size != theme.default_font_size:
theme.default_font_size = Global.font_size
theme.set_font_size("font_size", "HeaderSmall", Global.font_size + 2)
set_custom_cursor()
if OS.get_name() == "Web":
return
# Restore the window position/size if values are present in the configuration cache
if Global.config_cache.has_section_key("window", "screen"):
get_window().current_screen = Global.config_cache.get_value("window", "screen")
if Global.config_cache.has_section_key("window", "maximized"):
get_window().mode = (
Window.MODE_MAXIMIZED
if (Global.config_cache.get_value("window", "maximized"))
else Window.MODE_WINDOWED
)
if !(get_window().mode == Window.MODE_MAXIMIZED):
if Global.config_cache.has_section_key("window", "position"):
get_window().position = Global.config_cache.get_value("window", "position")
if Global.config_cache.has_section_key("window", "size"):
get_window().size = Global.config_cache.get_value("window", "size")
func set_custom_cursor() -> void:
if Global.native_cursors:
return
if Global.shrink == 1.0:
Input.set_custom_mouse_cursor(cursor_image, Input.CURSOR_CROSS, Vector2(15, 15))
else:
var cursor_data := cursor_image.get_image()
var cursor_size := cursor_data.get_size() * Global.shrink
cursor_data.resize(cursor_size.x, cursor_size.y, Image.INTERPOLATE_NEAREST)
var cursor_tex := ImageTexture.create_from_image(cursor_data)
Input.set_custom_mouse_cursor(
cursor_tex, Input.CURSOR_CROSS, Vector2(15, 15) * Global.shrink
)
func _show_splash_screen() -> void:
if not Global.config_cache.has_section_key("preferences", "startup"):
Global.config_cache.set_value("preferences", "startup", true)
if Global.config_cache.get_value("preferences", "startup"):
# Wait for the window to adjust itself, so the popup is correctly centered
await get_tree().process_frame
splash_dialog.popup_centered() # Splash screen
modulate = Color(0.5, 0.5, 0.5)
func _handle_backup() -> void:
# If backup file exists, Pixelorama was not closed properly (probably crashed) - reopen backup
backup_confirmation.add_button("Discard All", false, "discard")
var backup_dir := DirAccess.open("user://backups")
if backup_dir.get_files().size() > 0:
# Temporatily stop autosave until user confirms backup
OpenSave.autosave_timer.stop()
backup_confirmation.confirmed.connect(_on_BackupConfirmation_confirmed)
backup_confirmation.custom_action.connect(_on_BackupConfirmation_custom_action)
backup_confirmation.popup_centered()
modulate = Color(0.5, 0.5, 0.5)
else:
if Global.open_last_project:
load_last_project()
func _handle_cmdline_arguments() -> void:
var args := OS.get_cmdline_args()
args.append_array(OS.get_cmdline_user_args())
if args.is_empty():
return
# Load the files first
for arg in args:
var file_path := arg
if file_path.is_relative_path():
file_path = OS.get_executable_path().get_base_dir().path_join(arg)
OpenSave.handle_loading_file(file_path)
var project := Global.current_project
# True when exporting from the CLI.
# Exporting should be done last, this variable helps with that
var should_export := false
var parse_dic := {}
for command_group: Array in CLI.args_list.keys():
for command: String in command_group:
parse_dic[command] = CLI.args_list[command_group][0]
for i in args.size(): # Handle the rest of the CLI arguments
var arg := args[i]
var next_argument := ""
if i + 1 < args.size():
next_argument = args[i + 1]
if arg.begins_with("-") or arg.begins_with("--"):
if arg in parse_dic.keys():
var callable: Callable = parse_dic[arg]
var output = callable.call(project, next_argument)
if typeof(output) == TYPE_BOOL:
should_export = output
else:
print("==========")
print("Unknown option: %s" % arg)
for compare_arg in parse_dic.keys():
if arg.similarity(compare_arg) >= 0.4:
print("Similar option: %s" % compare_arg)
print("==========")
should_export = false
get_tree().quit()
break
if should_export:
Export.external_export(project)
func _notification(what: int) -> void:
match what:
NOTIFICATION_WM_CLOSE_REQUEST:
show_quit_dialog()
# If the mouse exits the window and another application has the focus,
# pause the application
NOTIFICATION_APPLICATION_FOCUS_OUT:
if Global.pause_when_unfocused:
get_tree().paused = true
NOTIFICATION_WM_MOUSE_EXIT:
if !get_window().has_focus() and Global.pause_when_unfocused:
get_tree().paused = true
# Unpause it when the mouse enters the window or when it gains focus
NOTIFICATION_WM_MOUSE_ENTER:
get_tree().paused = false
NOTIFICATION_APPLICATION_FOCUS_IN:
get_tree().paused = false
func _on_files_dropped(files: PackedStringArray) -> void:
for file in files:
OpenSave.handle_loading_file(file)
if splash_dialog.visible:
splash_dialog.hide()
func load_last_project() -> void:
if OS.get_name() == "Web":
return
# Check if any project was saved or opened last time
if Global.config_cache.has_section_key("data", "last_project_path"):
# Check if file still exists on disk
var file_path = Global.config_cache.get_value("data", "last_project_path")
load_recent_project_file(file_path)
func load_recent_project_file(path: String) -> void:
if OS.get_name() == "Web":
return
# Check if file still exists on disk
if FileAccess.file_exists(path): # If yes then load the file
OpenSave.handle_loading_file(path)
else:
# If file doesn't exist on disk then warn user about this
Global.popup_error("Cannot find project file.")
func _on_OpenSprite_files_selected(paths: PackedStringArray) -> void:
for path in paths:
OpenSave.handle_loading_file(path)
Global.save_sprites_dialog.current_dir = paths[0].get_base_dir()
func show_save_dialog(project := Global.current_project) -> void:
Global.dialog_open(true, true)
if OS.get_name() == "Web":
var save_filename := save_sprite_html5.get_node("%FileNameLineEdit")
save_sprite_html5.popup_centered()
save_filename.text = project.name
else:
Global.save_sprites_dialog.popup_centered()
Global.save_sprites_dialog.get_line_edit().text = project.name
func _on_SaveSprite_file_selected(path: String) -> void:
save_project(path)
func save_project(path: String) -> void:
var project_to_save := Global.current_project
if is_quitting_on_save:
project_to_save = changed_projects_on_quit[0]
var include_blended := false
if OS.get_name() == "Web":
var file_name: String = save_sprite_html5.get_node("%FileNameLineEdit").text
file_name += ".pxo"
path = "user://".path_join(file_name)
include_blended = save_sprite_html5.get_node("%IncludeBlended").button_pressed
else:
include_blended = (
Global.save_sprites_dialog.get_vbox().get_node("IncludeBlended").button_pressed
)
var success := OpenSave.save_pxo_file(path, false, include_blended, project_to_save)
if success:
Global.open_sprites_dialog.current_dir = path.get_base_dir()
if is_quitting_on_save:
changed_projects_on_quit.pop_front()
_save_on_quit_confirmation()
is_quitting_on_save = false
func _on_open_sprite_visibility_changed() -> void:
if !opensprite_file_selected:
_can_draw_true()
func _can_draw_true() -> void:
Global.dialog_open(false)
func show_quit_dialog() -> void:
changed_projects_on_quit = []
for project in Global.projects:
if project.has_changed:
changed_projects_on_quit.append(project)
if not quit_dialog.visible:
if changed_projects_on_quit.size() == 0:
if Global.quit_confirmation:
quit_dialog.popup_centered()
else:
_quit()
else:
quit_and_save_dialog.dialog_text = (
tr("Project %s has unsaved progress. How do you wish to proceed?")
% changed_projects_on_quit[0].name
)
quit_and_save_dialog.popup_centered()
Global.dialog_open(true)
func _save_on_quit_confirmation() -> void:
if changed_projects_on_quit.size() == 0:
_quit()
else:
quit_and_save_dialog.dialog_text = (
tr("Project %s has unsaved progress. How do you wish to proceed?")
% changed_projects_on_quit[0].name
)
quit_and_save_dialog.popup_centered()
Global.dialog_open(true)
func _on_QuitDialog_confirmed() -> void:
_quit()
func _on_QuitAndSaveDialog_custom_action(action: String) -> void:
if action == "ExitWithoutSaving":
changed_projects_on_quit.pop_front()
_save_on_quit_confirmation()
func _on_QuitAndSaveDialog_confirmed() -> void:
is_quitting_on_save = true
show_save_dialog(changed_projects_on_quit[0])
func _quit() -> void:
Global.pixelorama_about_to_close.emit()
# Darken the UI to denote that the application is currently exiting
# (it won't respond to user input in this state).
modulate = Color(0.5, 0.5, 0.5)
get_tree().quit()
func _on_BackupConfirmation_confirmed() -> void:
OpenSave.reload_backup_file()
func _on_BackupConfirmation_custom_action(action: String) -> void:
backup_confirmation.hide()
if action != "discard":
return
_clear_backup_files()
# Reopen last project
if Global.open_last_project:
load_last_project()
func _on_backup_confirmation_visibility_changed() -> void:
if backup_confirmation.visible:
return
if Global.enable_autosave:
OpenSave.autosave_timer.start()
Global.dialog_open(false)
func _clear_backup_files() -> void:
for file in DirAccess.get_files_at("user://backups"):
DirAccess.remove_absolute("user://backups".path_join(file))
func _exit_tree() -> void:
for project in Global.projects:
project.remove()
# For some reason, the above is not enough to remove all backup files
_clear_backup_files()
if DisplayServer.get_name() == "headless":
return
Global.config_cache.set_value("window", "layout", Global.layouts.find(main_ui.layout))
Global.config_cache.set_value("window", "screen", get_window().current_screen)
Global.config_cache.set_value(
"window",
"maximized",
(
(get_window().mode == Window.MODE_MAXIMIZED)
|| (
(get_window().mode == Window.MODE_EXCLUSIVE_FULLSCREEN)
or (get_window().mode == Window.MODE_FULLSCREEN)
)
)
)
Global.config_cache.set_value("window", "position", get_window().position)
Global.config_cache.set_value("window", "size", get_window().size)
Global.config_cache.set_value("view_menu", "draw_grid", Global.draw_grid)
Global.config_cache.set_value("view_menu", "draw_pixel_grid", Global.draw_pixel_grid)
Global.config_cache.set_value("view_menu", "show_rulers", Global.show_rulers)
Global.config_cache.set_value("view_menu", "show_guides", Global.show_guides)
Global.config_cache.set_value("view_menu", "show_mouse_guides", Global.show_mouse_guides)
Global.config_cache.save("user://cache.ini")