mirror of
https://github.com/Orama-Interactive/Pixelorama.git
synced 2025-01-31 15:39:49 +00:00
af01234470
Supports exporting images & spritesheets with frame, direction, scale and output file name specification. Also added some informative arguments that simply print out some data.
488 lines
17 KiB
GDScript
488 lines
17 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
|
|
|
|
|
|
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:
|
|
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()
|
|
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
|
|
for i in args.size(): # Handle the rest of the CLI arguments
|
|
var arg := args[i]
|
|
var arg_lower := arg.to_lower()
|
|
if arg.begins_with("-") or arg.begins_with("--"):
|
|
if arg_lower == "--pixelorama-version":
|
|
print(Global.current_version)
|
|
elif arg_lower == "--size":
|
|
print(project.size)
|
|
elif arg_lower == "--framecount":
|
|
print(project.frames.size())
|
|
elif arg_lower == "--export":
|
|
should_export = true
|
|
elif arg_lower == "--spritesheet":
|
|
Export.current_tab = Export.ExportTab.SPRITESHEET
|
|
should_export = true
|
|
elif arg_lower == "--output" or arg_lower == "-o":
|
|
if i + 1 < args.size():
|
|
var next_argument := args[i + 1]
|
|
project.file_name = next_argument.get_basename()
|
|
var extension := next_argument.get_extension()
|
|
project.file_format = Export.get_file_format_from_extension(extension)
|
|
elif arg_lower == "--scale":
|
|
if i + 1 < args.size():
|
|
var next_argument := args[i + 1]
|
|
if next_argument.is_valid_float():
|
|
Export.resize = next_argument.to_float() * 100
|
|
elif arg_lower == "--frames":
|
|
if i + 1 < args.size():
|
|
var next_argument := args[i + 1]
|
|
if next_argument.contains("-"):
|
|
var frame_numbers := next_argument.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_argument.is_valid_int():
|
|
var frame_number := next_argument.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
|
|
elif arg_lower == "--direction":
|
|
if i + 1 < args.size():
|
|
var next_argument := args[i + 1].to_lower()
|
|
if next_argument == "0" or next_argument.contains("forward"):
|
|
Export.direction = Export.AnimationDirection.FORWARD
|
|
elif next_argument == "1" or next_argument.contains("backward"):
|
|
Export.direction = Export.AnimationDirection.BACKWARDS
|
|
elif next_argument == "2" or next_argument.contains("ping"):
|
|
Export.direction = Export.AnimationDirection.PING_PONG
|
|
else:
|
|
print(Export.AnimationDirection.keys()[Export.direction])
|
|
else:
|
|
print(Export.AnimationDirection.keys()[Export.direction])
|
|
elif arg_lower == "--split-layers":
|
|
Export.split_layers = true
|
|
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:
|
|
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")
|
|
|
|
for project in Global.projects:
|
|
project.remove()
|
|
# For some reason, the above is not enough to remove all backup files
|
|
_clear_backup_files()
|