extends Node

## The Global autoload of Pixelorama.
##
## This Autoload contains signals, enums, constants, variables and
## references to many UI elements used within Pixelorama.

signal pixelorama_opened  ## Emitted as soon as Pixelorama fully opens up.
signal pixelorama_about_to_close  ## Emitted just before Pixelorama is about to close.
signal project_created(Project)  ## Emitted when a new project class is initialized.
signal project_changed  ## Emitted whenever you switch to some other project tab.
signal cel_changed  ## Emitted whenever you select a different cel.

enum LayerTypes { PIXEL, GROUP, THREE_D }
enum GridTypes { CARTESIAN, ISOMETRIC, ALL }
## ## Used to tell whether a color is being taken from the current theme,
## or if it is a custom color.
enum ColorFrom { THEME, CUSTOM }
enum ButtonSize { SMALL, BIG }
enum MeasurementMode { NONE, MOVE }

##  Enumeration of items present in the File Menu.
enum FileMenu { NEW, OPEN, OPEN_LAST_PROJECT, RECENT, SAVE, SAVE_AS, EXPORT, EXPORT_AS, QUIT }
##  Enumeration of items present in the Edit Menu.
enum EditMenu { UNDO, REDO, COPY, CUT, PASTE, PASTE_IN_PLACE, DELETE, NEW_BRUSH, PREFERENCES }
##  Enumeration of items present in the View Menu.
enum ViewMenu {
	TILE_MODE,
	TILE_MODE_OFFSETS,
	GREYSCALE_VIEW,
	MIRROR_VIEW,
	SHOW_GRID,
	SHOW_PIXEL_GRID,
	SHOW_RULERS,
	SHOW_GUIDES,
	SHOW_MOUSE_GUIDES,
	DISPLAY_LAYER_EFFECTS,
	SNAP_TO,
}
##  Enumeration of items present in the Window Menu.
enum WindowMenu { WINDOW_OPACITY, PANELS, LAYOUTS, MOVABLE_PANELS, ZEN_MODE, FULLSCREEN_MODE }
##  Enumeration of items present in the Image Menu.
enum ImageMenu {
	RESIZE_CANVAS,
	OFFSET_IMAGE,
	SCALE_IMAGE,
	CROP_IMAGE,
	FLIP,
	ROTATE,
	OUTLINE,
	DROP_SHADOW,
	INVERT_COLORS,
	DESATURATION,
	HSV,
	POSTERIZE,
	GRADIENT,
	GRADIENT_MAP,
	SHADER
}
##  Enumeration of items present in the Select Menu.
enum SelectMenu { SELECT_ALL, CLEAR_SELECTION, INVERT, TILE_MODE }
##  Enumeration of items present in the Help Menu.
enum HelpMenu {
	VIEW_SPLASH_SCREEN, ONLINE_DOCS, ISSUE_TRACKER, OPEN_LOGS_FOLDER, CHANGELOG, ABOUT_PIXELORAMA
}

## The file used to save preferences that use [code]ProjectSettings.save_custom()[/code].
const OVERRIDE_FILE := "override.cfg"
## The name of folder containing Pixelorama preferences.
const HOME_SUBDIR_NAME := "pixelorama"
## The name of folder that contains subdirectories for users to place brushes, palettes, patterns.
const CONFIG_SUBDIR_NAME := "pixelorama_data"
const VALUE_SLIDER_V2_TSCN := preload("res://src/UI/Nodes/ValueSliderV2.tscn")
const GRADIENT_EDIT_TSCN := preload("res://src/UI/Nodes/GradientEdit.tscn")

## It is path to the executable's base drectory.
var root_directory := "."
## The path where preferences and other subdirectories for stuff like layouts, extensions, logs etc.
## will get stored by Pixelorama.
var home_data_directory := OS.get_data_dir().path_join(HOME_SUBDIR_NAME)
## Only read from these directories. This is an [Array] of directories potentially containing
## stuff such as Brushes, Palettes and Patterns in sub-directories.[br]
## ([member home_data_directory] and [member root_directory] are also included in this array).
var data_directories: PackedStringArray = [home_data_directory]
## The config file used to get/set preferences, tool settings etc.
var config_cache := ConfigFile.new()

var projects: Array[Project] = []  ## Array of currently open projects.
var current_project: Project  ## The project that currently in focus.
## The index of project that is currently in focus.
var current_project_index := 0:
	set(value):
		if value >= projects.size():
			return
		canvas.selection.transform_content_confirm()
		current_project_index = value
		current_project = projects[value]
		project_changed.connect(current_project.change_project)
		project_changed.emit()
		project_changed.disconnect(current_project.change_project)
		cel_changed.emit()

# Canvas related stuff
## Tells if the user allowed to draw on the canvas. Usually it is temporarily set to
## [code]false[/code] when we are moving some gizmo and don't want the current tool to accidentally
## start drawing.[br](This does not depend on layer invisibility or lock/unlock status).
var can_draw := false
## (Intended to be used as getter only) Tells if the user allowed to move the guide while on canvas.
var move_guides_on_canvas := true
## Tells if the canvas in currently in focus.
var has_focus := false

var play_only_tags := true  ## If [code]true[/code], animation plays only on frames of the same tag.
## (Intended to be used as getter only) Tells if the x-symmetry guide ( -- ) is visible.
var show_x_symmetry_axis := false
## (Intended to be used as getter only) Tells if the y-symmetry guide ( | ) is visible.
var show_y_symmetry_axis := false

# Preferences
## Found in Preferences. If [code]true[/code], the last saved project will open on startup.
var open_last_project := false
## Found in Preferences. If [code]true[/code], asks for permission to quit on exit.
var quit_confirmation := false
## Found in Preferences. If [code]true[/code], the zoom is smooth.
var smooth_zoom := true
## Found in Preferences. If [code]true[/code], the zoom is restricted to integral multiples of 100%.
var integer_zoom := false:
	set(value):
		integer_zoom = value
		var zoom_slider: ValueSlider = top_menu_container.get_node("%ZoomSlider")
		if value:
			zoom_slider.min_value = 100
			zoom_slider.snap_step = 100
			zoom_slider.step = 100
		else:
			zoom_slider.min_value = 1
			zoom_slider.snap_step = 1
			zoom_slider.step = 1
		zoom_slider.value = zoom_slider.value  # to trigger signal emission

## Found in Preferences. The scale of the Interface.
var shrink := 1.0
## Found in Preferences. The font size used by the Interface.
var font_size := 16:
	set(value):
		font_size = value
		control.theme.default_font_size = value
		control.theme.set_font_size("font_size", "HeaderSmall", value + 2)
## Found in Preferences. If [code]true[/code], the Interface dims on popups.
var dim_on_popup := true
## Found in Preferences. The modulation color (or simply color) of icons.
var modulate_icon_color := Color.GRAY
## Found in Preferences. Determines if [member modulate_icon_color] uses custom or theme color.
var icon_color_from := ColorFrom.THEME:
	set(value):
		icon_color_from = value
		var themes = preferences_dialog.themes
		if icon_color_from == ColorFrom.THEME:
			var current_theme: Theme = themes.themes[themes.theme_index]
			modulate_icon_color = current_theme.get_color("modulate_color", "Icons")
		else:
			modulate_icon_color = custom_icon_color
		themes.change_icon_colors()
## Found in Preferences. Color of icons when [member icon_color_from] is set to use custom colors.
var custom_icon_color := Color.GRAY:
	set(value):
		custom_icon_color = value
		if icon_color_from == ColorFrom.CUSTOM:
			modulate_icon_color = custom_icon_color
			preferences_dialog.themes.change_icon_colors()
## Found in Preferences. The modulation color (or simply color) of canvas background
## (aside from checker background).
var modulate_clear_color := Color.GRAY:
	set(value):
		modulate_clear_color = value
		preferences_dialog.themes.change_clear_color()
## Found in Preferences. Determines if [member modulate_clear_color] uses custom or theme color.
var clear_color_from := ColorFrom.THEME:
	set(value):
		clear_color_from = value
		preferences_dialog.themes.change_clear_color()
## Found in Preferences. The selected size mode of tool buttons using [enum ButtonSize] enum.
var tool_button_size := ButtonSize.SMALL:
	set(value):
		tool_button_size = value
		Tools.set_button_size(tool_button_size)
## Found in Preferences. The left tool color.
var left_tool_color := Color("0086cf"):
	set(value):
		left_tool_color = value
		for child in Tools._tool_buttons.get_children():
			var background: NinePatchRect = child.get_node("BackgroundLeft")
			background.modulate = value
		Tools._slots[MOUSE_BUTTON_LEFT].tool_node.color_rect.color = value
## Found in Preferences. The right tool color.
var right_tool_color := Color("fd6d14"):
	set(value):
		right_tool_color = value
		for child in Tools._tool_buttons.get_children():
			var background: NinePatchRect = child.get_node("BackgroundRight")
			background.modulate = value
		Tools._slots[MOUSE_BUTTON_RIGHT].tool_node.color_rect.color = value

var default_width := 64  ## Found in Preferences. The default width of startup project.
var default_height := 64  ## Found in Preferences. The default height of startup project.
## Found in Preferences. The fill color of startup project.
var default_fill_color := Color(0, 0, 0, 0)
## Found in Preferences. The distance to the guide or grig below which cursor snapping activates.
var snapping_distance := 32.0
## Found in Preferences. The grid type defined by [enum GridTypes] enum.
var grid_type := GridTypes.CARTESIAN:
	set(value):
		grid_type = value
		canvas.grid.queue_redraw()
## Found in Preferences. The size of rectangular grid.
var grid_size := Vector2i(2, 2):
	set(value):
		grid_size = value
		canvas.grid.queue_redraw()
## Found in Preferences. The size of isometric grid.
var isometric_grid_size := Vector2i(16, 8):
	set(value):
		isometric_grid_size = value
		canvas.grid.queue_redraw()
## Found in Preferences. The grid offset from top-left corner of the canvas.
var grid_offset := Vector2i.ZERO:
	set(value):
		grid_offset = value
		canvas.grid.queue_redraw()
## Found in Preferences. If [code]true[/code], The grid draws over the area extended by
## tile-mode as well.
var grid_draw_over_tile_mode := false:
	set(value):
		grid_draw_over_tile_mode = value
		canvas.grid.queue_redraw()
## Found in Preferences. The color of grid.
var grid_color := Color.BLACK:
	set(value):
		grid_color = value
		canvas.grid.queue_redraw()
## Found in Preferences. The minimum zoom after which pixel grid gets drawn if enabled.
var pixel_grid_show_at_zoom := 1500.0:  # percentage
	set(value):
		pixel_grid_show_at_zoom = value
		canvas.pixel_grid.queue_redraw()
## Found in Preferences. The color of pixel grid.
var pixel_grid_color := Color("21212191"):
	set(value):
		pixel_grid_color = value
		canvas.pixel_grid.queue_redraw()
## Found in Preferences. The color of guides.
var guide_color := Color.PURPLE:
	set(value):
		guide_color = value
		for guide in canvas.get_children():
			if guide is Guide:
				guide.set_color(guide_color)
## Found in Preferences. The size of checkers in the checker background.
var checker_size := 10:
	set(value):
		checker_size = value
		transparent_checker.update_rect()
## Found in Preferences. The color of first checker.
var checker_color_1 := Color(0.47, 0.47, 0.47, 1):
	set(value):
		checker_color_1 = value
		transparent_checker.update_rect()
## Found in Preferences. The color of second checker.
var checker_color_2 := Color(0.34, 0.35, 0.34, 1):
	set(value):
		checker_color_2 = value
		transparent_checker.update_rect()
## Found in Preferences. The color of second checker.
var checker_follow_movement := false:
	set(value):
		checker_follow_movement = value
		transparent_checker.update_rect()
## Found in Preferences. If [code]true[/code], the checker follows zoom.
var checker_follow_scale := false:
	set(value):
		checker_follow_scale = value
		transparent_checker.update_rect()
## Found in Preferences. Opacity of the sprites rendered on the extended area of tile-mode.
var tilemode_opacity := 1.0:
	set(value):
		tilemode_opacity = value
		canvas.tile_mode.queue_redraw()

## Found in Preferences. If [code]true[/code], layers get selected when their buttons are pressed.
var select_layer_on_button_click := false
## Found in Preferences. The onion color of past frames.
var onion_skinning_past_color := Color.RED:
	set(value):
		onion_skinning_past_color = value
		canvas.onion_past.blue_red_color = value
		canvas.onion_past.queue_redraw()
## Found in Preferences. The onion color of future frames.
var onion_skinning_future_color := Color.BLUE:
	set(value):
		onion_skinning_future_color = value
		canvas.onion_future.blue_red_color = value
		canvas.onion_future.queue_redraw()

## Found in Preferences. If [code]true[/code], the selection rect has animated borders.
var selection_animated_borders := true:
	set(value):
		selection_animated_borders = value
		var marching_ants: Sprite2D = canvas.selection.marching_ants_outline
		marching_ants.material.set_shader_parameter("animated", selection_animated_borders)
## Found in Preferences. The first color of border.
var selection_border_color_1 := Color.WHITE:
	set(value):
		selection_border_color_1 = value
		var marching_ants: Sprite2D = canvas.selection.marching_ants_outline
		marching_ants.material.set_shader_parameter("first_color", selection_border_color_1)
		canvas.selection.queue_redraw()
## Found in Preferences. The second color of border.
var selection_border_color_2 := Color.BLACK:
	set(value):
		selection_border_color_2 = value
		var marching_ants: Sprite2D = canvas.selection.marching_ants_outline
		marching_ants.material.set_shader_parameter("second_color", selection_border_color_2)
		canvas.selection.queue_redraw()

## Found in Preferences. If [code]true[/code], Pixelorama pauses when unfocused to save cpu usage.
var pause_when_unfocused := true
## Found in Preferences. The max fps, Pixelorama is allowed to use (does not limit fps if it is 0).
var fps_limit := 0:
	set(value):
		fps_limit = value
		Engine.max_fps = fps_limit

## Found in Preferences. The time (in minutes) after which backup is created (if enabled).
var autosave_interval := 1.0:
	set(value):
		autosave_interval = value
		OpenSave.update_autosave()
## Found in Preferences. If [code]true[/code], generation of backups get enabled.
var enable_autosave := true:
	set(value):
		enable_autosave = value
		OpenSave.update_autosave()
		preferences_dialog.autosave_interval.editable = enable_autosave
## Found in Preferences. The index of graphics renderer used by Pixelorama.
var renderer := 0:
	set = _renderer_changed
## Found in Preferences. The index of tablet driver used by Pixelorama.
var tablet_driver := 0:
	set(value):
		tablet_driver = value
		if OS.has_feature("editor"):
			return
		var tablet_driver_name := DisplayServer.tablet_get_current_driver()
		ProjectSettings.set_setting("display/window/tablet_driver", tablet_driver_name)
		ProjectSettings.save_custom(OVERRIDE_FILE)

# Tools & options
## Found in Preferences. If [code]true[/code], the cursor's left tool icon is visible.
var show_left_tool_icon := true
## Found in Preferences. If [code]true[/code], the cursor's right tool icon is visible.
var show_right_tool_icon := true
## Found in Preferences. If [code]true[/code], the left tool's brush indicator is visible.
var left_square_indicator_visible := true
## Found in Preferences. If [code]true[/code], the right tool's brush indicator is visible.
var right_square_indicator_visible := true
## Found in Preferences. If [code]true[/code], native cursors are used instead of default cursors.
var native_cursors := false:
	set(value):
		native_cursors = value
		if native_cursors:
			Input.set_custom_mouse_cursor(null, Input.CURSOR_CROSS, Vector2(15, 15))
		else:
			control.set_custom_cursor()
## Found in Preferences. If [code]true[/code], cursor becomes cross shaped when hovering the canvas.
var cross_cursor := true:
	set(value):
		cross_cursor = value
		if cross_cursor:
			main_viewport.mouse_default_cursor_shape = Control.CURSOR_CROSS
		else:
			main_viewport.mouse_default_cursor_shape = Control.CURSOR_ARROW

# View menu options
## If [code]true[/code], the canvas is in greyscale.
var greyscale_view := false
## If [code]true[/code], the content of canvas is flipped.
var mirror_view := false
## If [code]true[/code], the grid is visible.
var draw_grid := false
## If [code]true[/code], the pixel grid is visible.
var draw_pixel_grid := false
## If [code]true[/code], the rulers are visible.
var show_rulers := true
## If [code]true[/code], the guides are visible.
var show_guides := true
## If [code]true[/code], the mouse guides are visible.
var show_mouse_guides := false
var display_layer_effects := true:
	set(value):
		display_layer_effects = value
		if is_instance_valid(top_menu_container):
			top_menu_container.view_menu.set_item_checked(ViewMenu.DISPLAY_LAYER_EFFECTS, value)
			canvas.queue_redraw()
## If [code]true[/code], cursor snaps to the boundary of rectangular grid boxes.
var snap_to_rectangular_grid_boundary := false
## If [code]true[/code], cursor snaps to the center of rectangular grid boxes.
var snap_to_rectangular_grid_center := false
## If [code]true[/code], cursor snaps to regular guides.
var snap_to_guides := false
## If [code]true[/code], cursor snaps to perspective guides.
var snap_to_perspective_guides := false

# Onion skinning options
var onion_skinning := false  ## If [code]true[/code], onion skinning is enabled.
var onion_skinning_past_rate := 1  ## Number of past frames shown when onion skinning is enabled.
## Number of future frames shown when onion skinning is enabled.
var onion_skinning_future_rate := 1
var onion_skinning_blue_red := false  ## If [code]true[/code], then blue-red mode is enabled.

## The current version of pixelorama
var current_version: String = ProjectSettings.get_setting("application/config/Version")

# Nodes
## The [PackedScene] of the button used by layers in the timeline.
var layer_button_node := preload("res://src/UI/Timeline/LayerButton.tscn")
## The [PackedScene] of the button used by cels in the timeline.
var cel_button_scene: PackedScene = load("res://src/UI/Timeline/CelButton.tscn")

@onready var main_window := get_window()  ## The main Pixelorama [Window].
## The control node (aka Main node). It has the [param Main.gd] script attached.
@onready var control := get_tree().current_scene

## The project tabs bar. It has the [param Tabs.gd] script attached.
@onready var tabs: TabBar = control.find_child("TabBar")
## Contains viewport of the main canvas. It has the [param ViewportContainer.gd] script attached.
@onready var main_viewport: SubViewportContainer = control.find_child("SubViewportContainer")
## The main canvas node. It has the [param Canvas.gd] script attached.
@onready var canvas: Canvas = main_viewport.find_child("Canvas")
## Contains viewport of the second canvas preview.
## It has the [param ViewportContainer.gd] script attached.
@onready var second_viewport: SubViewportContainer = control.find_child("Second Canvas")
## The panel container of the canvas preview.
## It has the [param CanvasPreviewContainer.gd] script attached.
@onready var canvas_preview_container: Container = control.find_child("Canvas Preview")
## The global tool options. It has the [param GlobalToolOptions.gd] script attached.
@onready var global_tool_options: PanelContainer = control.find_child("Global Tool Options")
## Contains viewport of the canvas preview.
@onready var small_preview_viewport: SubViewportContainer = canvas_preview_container.find_child(
	"PreviewViewportContainer"
)
## Camera of the main canvas. It has the [param CameraMovement.gd] script attached.
@onready var camera: Camera2D = main_viewport.find_child("Camera2D")
## Camera of the second canvas preview. It has the [param CameraMovement.gd] script attached.
@onready var camera2: Camera2D = second_viewport.find_child("Camera2D2")
## Camera of the canvas preview. It has the [param CameraMovement.gd] script attached.
@onready var camera_preview: Camera2D = control.find_child("CameraPreview")
## Array of cameras used in Pixelorama.
@onready var cameras := [camera, camera2, camera_preview]
## Horizontal ruler of the main canvas. It has the [param HorizontalRuler.gd] script attached.
@onready var horizontal_ruler: BaseButton = control.find_child("HorizontalRuler")
## Vertical ruler of the main canvas. It has the [param VerticalRuler.gd] script attached.
@onready var vertical_ruler: BaseButton = control.find_child("VerticalRuler")
## Transparent checker of the main canvas. It has the [param TransparentChecker.gd] script attached.
@onready var transparent_checker: ColorRect = control.find_child("TransparentChecker")

## The palettes panel. It has the [param PalettePanel.gd] script attached.
@onready var palette_panel: PalettePanel = control.find_child("Palettes")
## The perspectice editor. It has the [param PerspectiveEditor.gd] script attached.
@onready var perspective_editor := control.find_child("Perspective Editor")

## The top menu container. It has the [param TopMenuContainer.gd] script attached.
@onready var top_menu_container: Panel = control.find_child("TopMenuContainer")
## The label indicating cursor position.
@onready var cursor_position_label: Label = top_menu_container.find_child("CursorPosition")
## The label indicating current frame number.
@onready var current_frame_mark_label: Label = top_menu_container.find_child("CurrentFrameMark")

## The animation timeline. It has the [param AnimationTimeline.gd] script attached.
@onready var animation_timeline: Panel = control.find_child("Animation Timeline")
## The timer used by the animation timeline.
@onready var animation_timer: Timer = animation_timeline.find_child("AnimationTimer")
## The container of frame buttons
@onready var frame_hbox: HBoxContainer = animation_timeline.find_child("FrameHBox")
## The container of layer buttons
@onready var layer_vbox: VBoxContainer = animation_timeline.find_child("LayerVBox")
## At runtime HBoxContainers containing cel buttons get added to it.
@onready var cel_vbox: VBoxContainer = animation_timeline.find_child("CelVBox")
## The container of animation tags.
@onready var tag_container: Control = animation_timeline.find_child("TagContainer")

## The brushes popup dialog used to display brushes.
## It has the [param BrushesPopup.gd] script attached.
@onready var brushes_popup: Popup = control.find_child("BrushesPopup")
## The patterns popup dialog used to display patterns
## It has the [param PatternsPopup.gd] script attached.
@onready var patterns_popup: Popup = control.find_child("PatternsPopup")
@onready var tile_mode_offset_dialog: AcceptDialog = control.find_child("TileModeOffsetsDialog")
## Dialog used to navigate and open images and projects.
@onready var open_sprites_dialog: FileDialog = control.find_child("OpenSprite")
## Dialog used to save (.pxo) projects.
@onready var save_sprites_dialog: FileDialog = control.find_child("SaveSprite")
## Html version of [member save_sprites_dialog] used to save (.pxo) projects.
@onready var save_sprites_html5_dialog: ConfirmationDialog = control.find_child("SaveSpriteHTML5")
## Dialog used to export images. It has the [param ExportDialog.gd] script attached.
@onready var export_dialog: AcceptDialog = control.find_child("ExportDialog")
## The preferences dialog. It has the [param PreferencesDialog.gd] script attached.
@onready var preferences_dialog: AcceptDialog = control.find_child("PreferencesDialog")
## An error dialog to show errors.
@onready var error_dialog: AcceptDialog = control.find_child("ErrorDialog")


func _init() -> void:
	if OS.has_feature("template"):
		root_directory = OS.get_executable_path().get_base_dir()
	data_directories.append(root_directory.path_join(CONFIG_SUBDIR_NAME))
	if OS.get_name() in ["Linux", "FreeBSD", "NetBSD", "OpenBSD", "BSD"]:
		# Checks the list of files var, and processes them.
		if OS.has_environment("XDG_DATA_DIRS"):
			var raw_env_var := OS.get_environment("XDG_DATA_DIRS")  # includes empties.
			var unappended_subdirs := raw_env_var.split(":", true)
			for unapp_subdir in unappended_subdirs:
				data_directories.append(unapp_subdir.path_join(HOME_SUBDIR_NAME))
		else:
			# Create defaults
			for default_loc in ["/usr/local/share", "/usr/share"]:
				data_directories.append(default_loc.path_join(HOME_SUBDIR_NAME))
	if ProjectSettings.get_setting("display/window/tablet_driver") == "winink":
		tablet_driver = 1


func _ready() -> void:
	_initialize_keychain()
	# Load settings from the config file
	config_cache.load("user://cache.ini")

	default_width = config_cache.get_value("preferences", "default_width", default_width)
	default_height = config_cache.get_value("preferences", "default_height", default_height)
	default_fill_color = config_cache.get_value(
		"preferences", "default_fill_color", default_fill_color
	)
	var proj_size := Vector2i(default_width, default_height)
	projects.append(Project.new([], tr("untitled"), proj_size))
	current_project = projects[0]
	current_project.fill_color = default_fill_color

	await get_tree().process_frame
	project_changed.emit()


func _initialize_keychain() -> void:
	Keychain.config_file = config_cache
	Keychain.actions = {
		"new_file": Keychain.InputAction.new("", "File menu", true),
		"open_file": Keychain.InputAction.new("", "File menu", true),
		"open_last_project": Keychain.InputAction.new("", "File menu", true),
		"save_file": Keychain.InputAction.new("", "File menu", true),
		"save_file_as": Keychain.InputAction.new("", "File menu", true),
		"export_file": Keychain.InputAction.new("", "File menu", true),
		"export_file_as": Keychain.InputAction.new("", "File menu", true),
		"quit": Keychain.InputAction.new("", "File menu", true),
		"redo": Keychain.InputAction.new("", "Edit menu", true),
		"undo": Keychain.InputAction.new("", "Edit menu", true),
		"cut": Keychain.InputAction.new("", "Edit menu", true),
		"copy": Keychain.InputAction.new("", "Edit menu", true),
		"paste": Keychain.InputAction.new("", "Edit menu", true),
		"paste_in_place": Keychain.InputAction.new("", "Edit menu", true),
		"delete": Keychain.InputAction.new("", "Edit menu", true),
		"new_brush": Keychain.InputAction.new("", "Edit menu", true),
		"preferences": Keychain.InputAction.new("", "Edit menu", true),
		"scale_image": Keychain.InputAction.new("", "Image menu", true),
		"crop_image": Keychain.InputAction.new("", "Image menu", true),
		"resize_canvas": Keychain.InputAction.new("", "Image menu", true),
		"offset_image": Keychain.InputAction.new("", "Image menu", true),
		"mirror_image": Keychain.InputAction.new("", "Image menu", true),
		"rotate_image": Keychain.InputAction.new("", "Image menu", true),
		"invert_colors": Keychain.InputAction.new("", "Image menu", true),
		"desaturation": Keychain.InputAction.new("", "Image menu", true),
		"outline": Keychain.InputAction.new("", "Image menu", true),
		"drop_shadow": Keychain.InputAction.new("", "Image menu", true),
		"adjust_hsv": Keychain.InputAction.new("", "Image menu", true),
		"gradient": Keychain.InputAction.new("", "Image menu", true),
		"gradient_map": Keychain.InputAction.new("", "Image menu", true),
		"posterize": Keychain.InputAction.new("", "Image menu", true),
		"mirror_view": Keychain.InputAction.new("", "View menu", true),
		"show_grid": Keychain.InputAction.new("", "View menu", true),
		"show_pixel_grid": Keychain.InputAction.new("", "View menu", true),
		"show_guides": Keychain.InputAction.new("", "View menu", true),
		"show_rulers": Keychain.InputAction.new("", "View menu", true),
		&"display_layer_effects": Keychain.InputAction.new("", "View menu", true),
		"moveable_panels": Keychain.InputAction.new("", "Window menu", true),
		"zen_mode": Keychain.InputAction.new("", "Window menu", true),
		"toggle_fullscreen": Keychain.InputAction.new("", "Window menu", true),
		"clear_selection": Keychain.InputAction.new("", "Select menu", true),
		"select_all": Keychain.InputAction.new("", "Select menu", true),
		"invert_selection": Keychain.InputAction.new("", "Select menu", true),
		"view_splash_screen": Keychain.InputAction.new("", "Help menu", true),
		"open_docs": Keychain.InputAction.new("", "Help menu", true),
		"issue_tracker": Keychain.InputAction.new("", "Help menu", true),
		"open_logs_folder": Keychain.InputAction.new("", "Help menu", true),
		"changelog": Keychain.InputAction.new("", "Help menu", true),
		"about_pixelorama": Keychain.InputAction.new("", "Help menu", true),
		"zoom_in": Keychain.InputAction.new("", "Canvas"),
		"zoom_out": Keychain.InputAction.new("", "Canvas"),
		"camera_left": Keychain.InputAction.new("", "Canvas"),
		"camera_right": Keychain.InputAction.new("", "Canvas"),
		"camera_up": Keychain.InputAction.new("", "Canvas"),
		"camera_down": Keychain.InputAction.new("", "Canvas"),
		"pan": Keychain.InputAction.new("", "Canvas"),
		"activate_left_tool": Keychain.InputAction.new("", "Canvas"),
		"activate_right_tool": Keychain.InputAction.new("", "Canvas"),
		"move_mouse_left": Keychain.InputAction.new("", "Cursor movement"),
		"move_mouse_right": Keychain.InputAction.new("", "Cursor movement"),
		"move_mouse_up": Keychain.InputAction.new("", "Cursor movement"),
		"move_mouse_down": Keychain.InputAction.new("", "Cursor movement"),
		"reset_colors_default": Keychain.InputAction.new("", "Buttons"),
		"switch_colors": Keychain.InputAction.new("", "Buttons"),
		"horizontal_mirror": Keychain.InputAction.new("", "Buttons"),
		"vertical_mirror": Keychain.InputAction.new("", "Buttons"),
		"pixel_perfect": Keychain.InputAction.new("", "Buttons"),
		"new_layer": Keychain.InputAction.new("", "Buttons"),
		"remove_layer": Keychain.InputAction.new("", "Buttons"),
		"move_layer_up": Keychain.InputAction.new("", "Buttons"),
		"move_layer_down": Keychain.InputAction.new("", "Buttons"),
		"clone_layer": Keychain.InputAction.new("", "Buttons"),
		"merge_down_layer": Keychain.InputAction.new("", "Buttons"),
		"add_frame": Keychain.InputAction.new("", "Buttons"),
		"remove_frame": Keychain.InputAction.new("", "Buttons"),
		"clone_frame": Keychain.InputAction.new("", "Buttons"),
		"manage_frame_tags": Keychain.InputAction.new("", "Buttons"),
		"move_frame_left": Keychain.InputAction.new("", "Buttons"),
		"move_frame_right": Keychain.InputAction.new("", "Buttons"),
		"go_to_first_frame": Keychain.InputAction.new("", "Buttons"),
		"go_to_last_frame": Keychain.InputAction.new("", "Buttons"),
		"go_to_previous_frame": Keychain.InputAction.new("", "Buttons"),
		"go_to_next_frame": Keychain.InputAction.new("", "Buttons"),
		"play_backwards": Keychain.InputAction.new("", "Buttons"),
		"play_forward": Keychain.InputAction.new("", "Buttons"),
		"onion_skinning_toggle": Keychain.InputAction.new("", "Buttons"),
		"loop_toggle": Keychain.InputAction.new("", "Buttons"),
		"onion_skinning_settings": Keychain.InputAction.new("", "Buttons"),
		"new_palette": Keychain.InputAction.new("", "Buttons"),
		"edit_palette": Keychain.InputAction.new("", "Buttons"),
		"brush_size_increment": Keychain.InputAction.new("", "Buttons"),
		"brush_size_decrement": Keychain.InputAction.new("", "Buttons"),
		"change_tool_mode": Keychain.InputAction.new("", "Tool modifiers", false),
		"draw_create_line": Keychain.InputAction.new("", "Draw tools", false),
		"draw_snap_angle": Keychain.InputAction.new("", "Draw tools", false),
		"draw_color_picker": Keychain.InputAction.new("Quick color picker", "Draw tools", false),
		"shape_perfect": Keychain.InputAction.new("", "Shape tools", false),
		"shape_center": Keychain.InputAction.new("", "Shape tools", false),
		"shape_displace": Keychain.InputAction.new("", "Shape tools", false),
		"selection_add": Keychain.InputAction.new("", "Selection tools", false),
		"selection_subtract": Keychain.InputAction.new("", "Selection tools", false),
		"selection_intersect": Keychain.InputAction.new("", "Selection tools", false),
		"transformation_confirm": Keychain.InputAction.new("", "Transformation tools", false),
		"transformation_cancel": Keychain.InputAction.new("", "Transformation tools", false),
		"transform_snap_axis": Keychain.InputAction.new("", "Transformation tools", false),
		"transform_snap_grid": Keychain.InputAction.new("", "Transformation tools", false),
		"transform_move_selection_only":
		Keychain.InputAction.new("", "Transformation tools", false),
		"transform_copy_selection_content":
		Keychain.InputAction.new("", "Transformation tools", false),
	}

	Keychain.groups = {
		"Canvas": Keychain.InputGroup.new("", false),
		"Cursor movement": Keychain.InputGroup.new("Canvas"),
		"Buttons": Keychain.InputGroup.new(),
		"Tools": Keychain.InputGroup.new(),
		"Left": Keychain.InputGroup.new("Tools"),
		"Right": Keychain.InputGroup.new("Tools"),
		"Menu": Keychain.InputGroup.new(),
		"File menu": Keychain.InputGroup.new("Menu"),
		"Edit menu": Keychain.InputGroup.new("Menu"),
		"View menu": Keychain.InputGroup.new("Menu"),
		"Select menu": Keychain.InputGroup.new("Menu"),
		"Image menu": Keychain.InputGroup.new("Menu"),
		"Window menu": Keychain.InputGroup.new("Menu"),
		"Help menu": Keychain.InputGroup.new("Menu"),
		"Tool modifiers": Keychain.InputGroup.new(),
		"Draw tools": Keychain.InputGroup.new("Tool modifiers"),
		"Shape tools": Keychain.InputGroup.new("Tool modifiers"),
		"Selection tools": Keychain.InputGroup.new("Tool modifiers"),
		"Transformation tools": Keychain.InputGroup.new("Tool modifiers"),
	}
	Keychain.ignore_actions = ["left_mouse", "right_mouse", "middle_mouse", "shift", "ctrl"]


## Generates an animated notification label showing [param text].
func notification_label(text: String) -> void:
	var notif := NotificationLabel.new()
	notif.text = tr(text)
	notif.position = main_viewport.global_position
	notif.position.y += main_viewport.size.y
	control.add_child(notif)


## Performs the general, bare minimum stuff needed after an undo is done.
func general_undo(project := current_project) -> void:
	project.undos -= 1
	var action_name := project.undo_redo.get_current_action_name()
	notification_label("Undo: %s" % action_name)


## Performs the general, bare minimum stuff needed after a redo is done.
func general_redo(project := current_project) -> void:
	if project.undos < project.undo_redo.get_version():  # If we did undo and then redo
		project.undos = project.undo_redo.get_version()
	if control.redone:
		var action_name := project.undo_redo.get_current_action_name()
		notification_label("Redo: %s" % action_name)


## Performs actions done after an undo or redo is done. this takes [member general_undo] and
## [member general_redo] a step further. Does further work if the current action requires it
## like refreshing textures, redraw UI elements etc...[br]
## [param frame_index] and [param layer_index] are there for optimizzation. if the undo or redo
## happens only in one cel then the cel's frame and layer should be passed to [param frame_index]
## and [param layer_index] respectively, otherwise the entire timeline will be refreshed.
func undo_or_redo(
	undo: bool, frame_index := -1, layer_index := -1, project := current_project
) -> void:
	if undo:
		general_undo(project)
	else:
		general_redo(project)
	var action_name := project.undo_redo.get_current_action_name()
	if (
		action_name
		in [
			"Draw",
			"Draw Shape",
			"Select",
			"Move Selection",
			"Scale",
			"Center Frames",
			"Merge Layer",
			"Link Cel",
			"Unlink Cel"
		]
	):
		if layer_index > -1 and frame_index > -1:
			canvas.update_texture(layer_index, frame_index, project)
		else:
			for i in project.frames.size():
				for j in project.layers.size():
					canvas.update_texture(j, i, project)

		canvas.selection.queue_redraw()
		if action_name == "Scale":
			for i in project.frames.size():
				for j in project.layers.size():
					var current_cel := project.frames[i].cels[j]
					if current_cel is Cel3D:
						current_cel.size_changed(project.size)
					else:
						current_cel.image_texture = ImageTexture.create_from_image(
							current_cel.get_image()
						)
			canvas.camera_zoom()
			canvas.grid.queue_redraw()
			canvas.pixel_grid.queue_redraw()
			project.selection_map_changed()
			cursor_position_label.text = "[%s×%s]" % [project.size.x, project.size.y]

	await RenderingServer.frame_post_draw
	canvas.queue_redraw()
	second_viewport.get_child(0).get_node("CanvasPreview").queue_redraw()
	canvas_preview_container.canvas_preview.queue_redraw()
	if !project.has_changed:
		project.has_changed = true
		if project == current_project:
			main_window.title = main_window.title + "(*)"


func _renderer_changed(value: int) -> void:
	renderer = value


#	if OS.has_feature("editor"):
#		return
#
#	# Sets GLES2 as the default value in `override.cfg`.
#	# Without this, switching to GLES3 does not work, because it will default to GLES2.
#	ProjectSettings.set_initial_value("rendering/quality/driver/driver_name", "GLES2")
#	var renderer_name := OS.get_video_driver_name(renderer)
#	ProjectSettings.set_setting("rendering/quality/driver/driver_name", renderer_name)
#	ProjectSettings.save_custom(OVERRIDE_FILE)


## Use this to prepare Pixelorama before opening a dialog.
func dialog_open(open: bool) -> void:
	var dim_color := Color.WHITE
	if open:
		can_draw = false
		if dim_on_popup:
			dim_color = Color(0.5, 0.5, 0.5)
	else:
		can_draw = true

	var tween := create_tween().set_trans(Tween.TRANS_LINEAR).set_ease(Tween.EASE_OUT)
	tween.tween_property(control, "modulate", dim_color, 0.1)


## sets the [member BaseButton.disabled] property of the [param button] to [param disable],
## changes the cursor shape for it accordingly, and dims/brightens any textures it may have.
func disable_button(button: BaseButton, disable: bool) -> void:
	button.disabled = disable
	if disable:
		button.mouse_default_cursor_shape = Control.CURSOR_FORBIDDEN
	else:
		button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND

	if button is Button:
		for c in button.get_children():
			if c is TextureRect:
				c.modulate.a = 0.5 if disable else 1.0
				break


## Changes the texture of the [param texture_rect] to another texture of name [param new_file_name]
## present in the same directory as the old one.
func change_button_texturerect(texture_rect: TextureRect, new_file_name: String) -> void:
	if !texture_rect.texture:
		return
	var file_name := texture_rect.texture.resource_path.get_basename().get_file()
	var directory_path := texture_rect.texture.resource_path.get_basename().replace(file_name, "")
	texture_rect.texture = load(directory_path.path_join(new_file_name))


## Joins each [String] path in [param basepaths] with [param subpath] using
## [method String.path_join]
func path_join_array(basepaths: PackedStringArray, subpath: String) -> PackedStringArray:
	var res := PackedStringArray()
	for _path in basepaths:
		res.append(_path.path_join(subpath))
	return res


## Used by undo/redo operations to store compressed images in memory.
func undo_redo_compress_images(
	redo_data: Dictionary, undo_data: Dictionary, project := current_project
) -> void:
	for image in redo_data:
		if not image is Image:
			continue
		var new_image: Dictionary = redo_data[image]
		var new_size := Vector2i(new_image["width"], new_image["height"])
		var buffer_size: int = new_image["data"].size()
		var compressed_data: PackedByteArray = new_image["data"].compress()
		project.undo_redo.add_do_method(
			undo_redo_draw_op.bind(image, new_size, compressed_data, buffer_size)
		)
	for image in undo_data:
		if not image is Image:
			continue
		var new_image: Dictionary = undo_data[image]
		var new_size := Vector2i(new_image["width"], new_image["height"])
		var buffer_size: int = new_image["data"].size()
		var compressed_data: PackedByteArray = new_image["data"].compress()
		project.undo_redo.add_undo_method(
			undo_redo_draw_op.bind(image, new_size, compressed_data, buffer_size)
		)


## Decompresses the [param compressed_image_data] with [param buffer_size] to the [param image]
## This is an optimization method used while performing undo/redo drawing operations.
func undo_redo_draw_op(
	image: Image, new_size: Vector2i, compressed_image_data: PackedByteArray, buffer_size: int
) -> void:
	var decompressed := compressed_image_data.decompress(buffer_size)
	image.set_data(new_size.x, new_size.y, image.has_mipmaps(), image.get_format(), decompressed)


## Used by the Move tool for undo/redo, moves all of the [Image]s in [param images]
## by [param diff] pixels.
func undo_redo_move(diff: Vector2i, images: Array[Image]) -> void:
	for image in images:
		var image_copy := Image.new()
		image_copy.copy_from(image)
		image.fill(Color(0, 0, 0, 0))
		image.blit_rect(image_copy, Rect2i(Vector2i.ZERO, image.get_size()), diff)


func create_ui_for_shader_uniforms(
	shader: Shader,
	params: Dictionary,
	parent_node: Control,
	value_changed: Callable,
	file_selected: Callable
) -> void:
	var code := shader.code.split("\n")
	var uniforms: PackedStringArray = []
	for line in code:
		if line.begins_with("uniform"):
			uniforms.append(line)

	for uniform in uniforms:
		# Example uniform:
		# uniform float parameter_name : hint_range(0, 255) = 100.0;
		var uniform_split := uniform.split("=")
		var u_value := ""
		if uniform_split.size() > 1:
			u_value = uniform_split[1].replace(";", "").strip_edges()
		else:
			uniform_split[0] = uniform_split[0].replace(";", "").strip_edges()

		var u_left_side := uniform_split[0].split(":")
		var u_hint := ""
		if u_left_side.size() > 1:
			u_hint = u_left_side[1].strip_edges()
			u_hint = u_hint.replace(";", "")

		var u_init := u_left_side[0].split(" ")
		var u_type := u_init[1]
		var u_name := u_init[2]
		var humanized_u_name := Keychain.humanize_snake_case(u_name) + ":"

		if u_type == "float" or u_type == "int":
			var label := Label.new()
			label.text = humanized_u_name
			label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
			var slider := ValueSlider.new()
			slider.allow_greater = true
			slider.allow_lesser = true
			slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
			var min_value := 0.0
			var max_value := 255.0
			var step := 1.0
			var range_values_array: PackedStringArray
			if "hint_range" in u_hint:
				var range_values: String = u_hint.replace("hint_range(", "")
				range_values = range_values.replace(")", "").strip_edges()
				range_values_array = range_values.split(",")

			if u_type == "float":
				if range_values_array.size() >= 1:
					min_value = float(range_values_array[0])
				else:
					min_value = 0.01

				if range_values_array.size() >= 2:
					max_value = float(range_values_array[1])

				if range_values_array.size() >= 3:
					step = float(range_values_array[2])
				else:
					step = 0.01

				if u_value != "":
					slider.value = float(u_value)
			else:
				if range_values_array.size() >= 1:
					min_value = int(range_values_array[0])

				if range_values_array.size() >= 2:
					max_value = int(range_values_array[1])

				if range_values_array.size() >= 3:
					step = int(range_values_array[2])

				if u_value != "":
					slider.value = int(u_value)
			if params.has(u_name):
				slider.value = params[u_name]
			else:
				params[u_name] = slider.value
			slider.min_value = min_value
			slider.max_value = max_value
			slider.step = step
			slider.value_changed.connect(value_changed.bind(u_name))
			slider.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
			var hbox := HBoxContainer.new()
			hbox.add_child(label)
			hbox.add_child(slider)
			parent_node.add_child(hbox)
		elif u_type == "vec2":
			var label := Label.new()
			label.text = humanized_u_name
			label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
			var vector2 := _vec2str_to_vector2(u_value)
			var slider := VALUE_SLIDER_V2_TSCN.instantiate() as ValueSliderV2
			slider.allow_greater = true
			slider.allow_lesser = true
			slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
			slider.value = vector2
			if params.has(u_name):
				slider.value = params[u_name]
			else:
				params[u_name] = slider.value
			slider.value_changed.connect(value_changed.bind(u_name))
			var hbox := HBoxContainer.new()
			hbox.add_child(label)
			hbox.add_child(slider)
			parent_node.add_child(hbox)
		elif u_type == "vec4":
			if "source_color" in u_hint:
				var label := Label.new()
				label.text = humanized_u_name
				label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
				var color := _vec4str_to_color(u_value)
				var color_button := ColorPickerButton.new()
				color_button.custom_minimum_size = Vector2(20, 20)
				color_button.color = color
				if params.has(u_name):
					color_button.color = params[u_name]
				else:
					params[u_name] = color_button.color
				color_button.color_changed.connect(value_changed.bind(u_name))
				color_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
				color_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
				var hbox := HBoxContainer.new()
				hbox.add_child(label)
				hbox.add_child(color_button)
				parent_node.add_child(hbox)
		elif u_type == "sampler2D":
			if u_name == "selection":
				continue
			var label := Label.new()
			label.text = humanized_u_name
			label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
			var hbox := HBoxContainer.new()
			hbox.add_child(label)
			if u_name.begins_with("gradient_"):
				var gradient_edit := GRADIENT_EDIT_TSCN.instantiate() as GradientEditNode
				gradient_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
				if params.has(u_name) and params[u_name] is GradientTexture2D:
					gradient_edit.set_gradient_texture(params[u_name])
				else:
					params[u_name] = gradient_edit.texture
				value_changed.call(gradient_edit.get_node("TextureRect").texture, u_name)
				gradient_edit.updated.connect(
					func(_gradient, _cc): value_changed.call(gradient_edit.texture, u_name)
				)
				hbox.add_child(gradient_edit)
			else:
				var file_dialog := FileDialog.new()
				file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
				file_dialog.access = FileDialog.ACCESS_FILESYSTEM
				file_dialog.size = Vector2(384, 281)
				file_dialog.file_selected.connect(file_selected.bind(u_name))
				var button := Button.new()
				button.text = "Load texture"
				button.pressed.connect(file_dialog.popup_centered)
				button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
				button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
				hbox.add_child(button)
				parent_node.add_child(file_dialog)
			parent_node.add_child(hbox)

		elif u_type == "bool":
			var label := Label.new()
			label.text = humanized_u_name
			label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
			var checkbox := CheckBox.new()
			checkbox.text = "On"
			if u_value == "true":
				checkbox.button_pressed = true
			if params.has(u_name):
				checkbox.button_pressed = params[u_name]
			else:
				params[u_name] = checkbox.button_pressed
			checkbox.toggled.connect(value_changed.bind(u_name))
			checkbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
			checkbox.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
			var hbox := HBoxContainer.new()
			hbox.add_child(label)
			hbox.add_child(checkbox)
			parent_node.add_child(hbox)


func _vec2str_to_vector2(vec2: String) -> Vector2:
	vec2 = vec2.replace("vec2(", "")
	vec2 = vec2.replace(")", "")
	var vec_values := vec2.split(",")
	if vec_values.size() == 0:
		return Vector2.ZERO
	var y := float(vec_values[0])
	if vec_values.size() == 2:
		y = float(vec_values[1])
	var vector2 := Vector2(float(vec_values[0]), y)
	return vector2


func _vec4str_to_color(vec4: String) -> Color:
	vec4 = vec4.replace("vec4(", "")
	vec4 = vec4.replace(")", "")
	var rgba_values := vec4.split(",")
	var red := float(rgba_values[0])

	var green := float(rgba_values[0])
	if rgba_values.size() >= 2:
		green = float(rgba_values[1])

	var blue := float(rgba_values[0])
	if rgba_values.size() >= 3:
		blue = float(rgba_values[2])

	var alpha := float(rgba_values[0])
	if rgba_values.size() == 4:
		alpha = float(rgba_values[3])
	var color := Color(red, green, blue, alpha)
	return color
