diff --git a/Translations/Translations.pot b/Translations/Translations.pot
index 8ef06e0e6..e4a696b6a 100644
--- a/Translations/Translations.pot
+++ b/Translations/Translations.pot
@@ -743,6 +743,22 @@ msgstr ""
msgid "Dim interface on dialog popup"
msgstr ""
+#. Found in the preferences, under the interface section. When this setting is enabled, the native file dialogs of the operating system are being used, instead of Pixelorama's custom ones.
+msgid "Use native file dialogs"
+msgstr ""
+
+#. Found in the preferences, tooltip of the "Use native file dialogs" option.
+msgid "When this setting is enabled, the native file dialogs of the operating system are being used, instead of Pixelorama's custom ones."
+msgstr ""
+
+#. Found in the preferences, under the interface section. When this setting is enabled, Pixelorama's subwindows will be embedded in the main window, otherwise each dialog will be its own separate window.
+msgid "Single window mode"
+msgstr ""
+
+#. Found in the preferences, tooltip of the "Single window mode" option.
+msgid "When this setting is enabled, Pixelorama's subwindows will be embedded in the main window, otherwise each dialog will be its own separate window."
+msgstr ""
+
msgid "Dark"
msgstr ""
@@ -2305,6 +2321,10 @@ msgstr ""
msgid "Quit confirmation"
msgstr ""
+#. Found in the preferences, under the startup section. Path is a noun and it refers to the location in the device where FFMPEG is located at. FFMPEG is a software name and it should not be translated. See https://en.wikipedia.org/wiki/Path_(computing)
+msgid "FFMPEG path"
+msgstr ""
+
msgid "Enable autosave"
msgstr ""
diff --git a/src/Autoload/DrawingAlgos.gd b/src/Autoload/DrawingAlgos.gd
index 7ceac387a..264b3a329 100644
--- a/src/Autoload/DrawingAlgos.gd
+++ b/src/Autoload/DrawingAlgos.gd
@@ -21,7 +21,7 @@ func blend_layers(
# the second are the opacities and the third are the origins
var metadata_image := Image.create(project.layers.size(), 3, false, Image.FORMAT_R8)
var frame_index := project.frames.find(frame)
- var previous_ordered_layers: Array[int] = Array(project.ordered_layers)
+ var previous_ordered_layers: Array[int] = project.ordered_layers
project.order_layers(frame_index)
for i in project.layers.size():
var ordered_index := project.ordered_layers[i]
@@ -53,7 +53,7 @@ func blend_layers(
gen.generate_image(blended, blend_layers_shader, params, project.size)
image.blend_rect(blended, Rect2i(Vector2i.ZERO, project.size), origin)
# Re-order the layers again to ensure correct canvas drawing
- project.ordered_layers = Array(previous_ordered_layers)
+ project.ordered_layers = previous_ordered_layers
## Algorithm based on http://members.chello.at/easyfilter/bresenham.html
@@ -425,6 +425,8 @@ func fake_rotsprite(sprite: Image, angle: float, pivot: Vector2) -> void:
func nn_rotate(sprite: Image, angle: float, pivot: Vector2) -> void:
+ if is_zero_approx(angle):
+ return
var aux := Image.new()
aux.copy_from(sprite)
var ox: int
@@ -456,37 +458,6 @@ func color_distance(c1: Color, c2: Color) -> float:
# Image effects
-func scale_image(width: int, height: int, interpolation: int) -> void:
- general_do_scale(width, height)
-
- for f in Global.current_project.frames:
- for i in range(f.cels.size() - 1, -1, -1):
- var cel := f.cels[i]
- if not cel is PixelCel:
- continue
- var sprite := Image.new()
- sprite.copy_from(cel.get_image())
- if interpolation == Interpolation.SCALE3X:
- var times := Vector2i(
- ceili(width / (3.0 * sprite.get_width())),
- ceili(height / (3.0 * sprite.get_height()))
- )
- for _j in range(maxi(times.x, times.y)):
- sprite.copy_from(scale_3x(sprite))
- sprite.resize(width, height, Image.INTERPOLATE_NEAREST)
- elif interpolation == Interpolation.CLEANEDGE:
- var gen := ShaderImageEffect.new()
- gen.generate_image(sprite, clean_edge_shader, {}, Vector2i(width, height))
- elif interpolation == Interpolation.OMNISCALE and omniscale_shader:
- var gen := ShaderImageEffect.new()
- gen.generate_image(sprite, omniscale_shader, {}, Vector2i(width, height))
- else:
- sprite.resize(width, height, interpolation)
- Global.undo_redo_compress_images({cel.image: sprite.data}, {cel.image: cel.image.data})
-
- general_undo_scale()
-
-
func center(indices: Array) -> void:
var project := Global.current_project
Global.canvas.selection.transform_content_confirm()
@@ -517,22 +488,56 @@ func center(indices: Array) -> void:
project.undo_redo.commit_action()
+func scale_image(width: int, height: int, interpolation: int) -> void:
+ var redo_data := {}
+ var undo_data := {}
+ for f in Global.current_project.frames:
+ for i in range(f.cels.size() - 1, -1, -1):
+ var cel := f.cels[i]
+ if not cel is PixelCel:
+ continue
+ var sprite := Image.new()
+ sprite.copy_from(cel.get_image())
+ if interpolation == Interpolation.SCALE3X:
+ var times := Vector2i(
+ ceili(width / (3.0 * sprite.get_width())),
+ ceili(height / (3.0 * sprite.get_height()))
+ )
+ for _j in range(maxi(times.x, times.y)):
+ sprite.copy_from(scale_3x(sprite))
+ sprite.resize(width, height, Image.INTERPOLATE_NEAREST)
+ elif interpolation == Interpolation.CLEANEDGE:
+ var gen := ShaderImageEffect.new()
+ gen.generate_image(sprite, clean_edge_shader, {}, Vector2i(width, height))
+ elif interpolation == Interpolation.OMNISCALE and omniscale_shader:
+ var gen := ShaderImageEffect.new()
+ gen.generate_image(sprite, omniscale_shader, {}, Vector2i(width, height))
+ else:
+ sprite.resize(width, height, interpolation)
+ redo_data[cel.image] = sprite.data
+ undo_data[cel.image] = cel.image.data
+
+ general_do_and_undo_scale(width, height, redo_data, undo_data)
+
+
## Sets the size of the project to be the same as the size of the active selection.
func crop_to_selection() -> void:
if not Global.current_project.has_selection:
return
+ var redo_data := {}
+ var undo_data := {}
Global.canvas.selection.transform_content_confirm()
var rect: Rect2i = Global.canvas.selection.big_bounding_rectangle
- general_do_scale(rect.size.x, rect.size.y)
# Loop through all the cels to crop them
for f in Global.current_project.frames:
for cel in f.cels:
if not cel is PixelCel:
continue
var sprite := cel.get_image().get_region(rect)
- Global.undo_redo_compress_images({cel.image: sprite.data}, {cel.image: cel.image.data})
+ redo_data[cel.image] = sprite.data
+ undo_data[cel.image] = cel.image.data
- general_undo_scale()
+ general_do_and_undo_scale(rect.size.x, rect.size.y, redo_data, undo_data)
## Automatically makes the project smaller by looping through all of the cels and
@@ -559,20 +564,23 @@ func crop_to_content() -> void:
var width := used_rect.size.x
var height := used_rect.size.y
- general_do_scale(width, height)
+ var redo_data := {}
+ var undo_data := {}
# Loop through all the cels to trim them
for f in Global.current_project.frames:
for cel in f.cels:
if not cel is PixelCel:
continue
var sprite := cel.get_image().get_region(used_rect)
- Global.undo_redo_compress_images({cel.image: sprite.data}, {cel.image: cel.image.data})
+ redo_data[cel.image] = sprite.data
+ undo_data[cel.image] = cel.image.data
- general_undo_scale()
+ general_do_and_undo_scale(width, height, redo_data, undo_data)
func resize_canvas(width: int, height: int, offset_x: int, offset_y: int) -> void:
- general_do_scale(width, height)
+ var redo_data := {}
+ var undo_data := {}
for f in Global.current_project.frames:
for cel in f.cels:
if not cel is PixelCel:
@@ -583,17 +591,26 @@ func resize_canvas(width: int, height: int, offset_x: int, offset_y: int) -> voi
Rect2i(Vector2i.ZERO, Global.current_project.size),
Vector2i(offset_x, offset_y)
)
- Global.undo_redo_compress_images({cel.image: sprite.data}, {cel.image: cel.image.data})
+ redo_data[cel.image] = sprite.data
+ undo_data[cel.image] = cel.image.data
- general_undo_scale()
+ general_do_and_undo_scale(width, height, redo_data, undo_data)
-func general_do_scale(width: int, height: int) -> void:
+func general_do_and_undo_scale(
+ width: int, height: int, redo_data: Dictionary, undo_data: Dictionary
+) -> void:
var project := Global.current_project
var size := Vector2i(width, height)
var x_ratio := float(project.size.x) / width
var y_ratio := float(project.size.y) / height
+ var selection_map_copy := SelectionMap.new()
+ selection_map_copy.copy_from(project.selection_map)
+ selection_map_copy.crop(size.x, size.y)
+ redo_data[project.selection_map] = selection_map_copy.data
+ undo_data[project.selection_map] = project.selection_map.data
+
var new_x_symmetry_point := project.x_symmetry_point / x_ratio
var new_y_symmetry_point := project.y_symmetry_point / y_ratio
var new_x_symmetry_axis_points := project.x_symmetry_axis.points
@@ -606,19 +623,12 @@ func general_do_scale(width: int, height: int) -> void:
project.undos += 1
project.undo_redo.create_action("Scale")
project.undo_redo.add_do_property(project, "size", size)
- project.undo_redo.add_do_method(project.selection_map.crop.bind(size.x, size.y))
project.undo_redo.add_do_property(project, "x_symmetry_point", new_x_symmetry_point)
project.undo_redo.add_do_property(project, "y_symmetry_point", new_y_symmetry_point)
project.undo_redo.add_do_property(project.x_symmetry_axis, "points", new_x_symmetry_axis_points)
project.undo_redo.add_do_property(project.y_symmetry_axis, "points", new_y_symmetry_axis_points)
-
-
-func general_undo_scale() -> void:
- var project := Global.current_project
+ Global.undo_redo_compress_images(redo_data, undo_data)
project.undo_redo.add_undo_property(project, "size", project.size)
- project.undo_redo.add_undo_method(
- project.selection_map.crop.bind(project.size.x, project.size.y)
- )
project.undo_redo.add_undo_property(project, "x_symmetry_point", project.x_symmetry_point)
project.undo_redo.add_undo_property(project, "y_symmetry_point", project.y_symmetry_point)
project.undo_redo.add_undo_property(
diff --git a/src/Autoload/Export.gd b/src/Autoload/Export.gd
index de28b3442..ebe46d711 100644
--- a/src/Autoload/Export.gd
+++ b/src/Autoload/Export.gd
@@ -4,10 +4,24 @@ enum ExportTab { IMAGE = 0, SPRITESHEET = 1 }
enum Orientation { ROWS = 0, COLUMNS = 1 }
enum AnimationDirection { FORWARD = 0, BACKWARDS = 1, PING_PONG = 2 }
## See file_format_string, file_format_description, and ExportDialog.gd
-enum FileFormat { PNG, WEBP, JPEG, GIF, APNG }
+enum FileFormat { PNG, WEBP, JPEG, GIF, APNG, MP4, AVI, OGV, MKV, WEBM }
+
+const TEMP_PATH := "user://tmp"
## List of animated formats
-var animated_formats := [FileFormat.GIF, FileFormat.APNG]
+var animated_formats := [
+ FileFormat.GIF,
+ FileFormat.APNG,
+ FileFormat.MP4,
+ FileFormat.AVI,
+ FileFormat.OGV,
+ FileFormat.MKV,
+ FileFormat.WEBM
+]
+
+var ffmpeg_formats := [
+ FileFormat.MP4, FileFormat.AVI, FileFormat.OGV, FileFormat.MKV, FileFormat.WEBM
+]
## A dictionary of custom exporter generators (received from extensions)
var custom_file_formats := {}
@@ -262,23 +276,28 @@ func export_processed_images(
return result
if is_single_file_format(project):
- var exporter: AImgIOBaseExporter
- if project.file_format == FileFormat.APNG:
- exporter = AImgIOAPNGExporter.new()
+ if is_using_ffmpeg(project.file_format):
+ var video_exported := export_video(export_paths)
+ if not video_exported:
+ return false
else:
- exporter = GIFAnimationExporter.new()
- var details := {
- "exporter": exporter,
- "export_dialog": export_dialog,
- "export_paths": export_paths,
- "project": project
- }
- if not _multithreading_enabled():
- export_animated(details)
- else:
- if gif_export_thread.is_started():
- gif_export_thread.wait_to_finish()
- gif_export_thread.start(export_animated.bind(details))
+ var exporter: AImgIOBaseExporter
+ if project.file_format == FileFormat.APNG:
+ exporter = AImgIOAPNGExporter.new()
+ else:
+ exporter = GIFAnimationExporter.new()
+ var details := {
+ "exporter": exporter,
+ "export_dialog": export_dialog,
+ "export_paths": export_paths,
+ "project": project
+ }
+ if not _multithreading_enabled():
+ export_animated(details)
+ else:
+ if gif_export_thread.is_started():
+ gif_export_thread.wait_to_finish()
+ gif_export_thread.start(export_animated.bind(details))
else:
var succeeded := true
for i in range(processed_images.size()):
@@ -311,11 +330,9 @@ func export_processed_images(
elif project.file_format == FileFormat.JPEG:
err = processed_images[i].save_jpg(export_paths[i])
if err != OK:
- Global.error_dialog.set_text(
+ Global.popup_error(
tr("File failed to save. Error code %s (%s)") % [err, error_string(err)]
)
- Global.error_dialog.popup_centered()
- Global.dialog_open(true)
succeeded = false
if succeeded:
Global.notification_label("File(s) exported")
@@ -334,6 +351,37 @@ func export_processed_images(
return true
+## Uses FFMPEG to export a video
+func export_video(export_paths: PackedStringArray) -> bool:
+ DirAccess.make_dir_absolute(TEMP_PATH)
+ var temp_path_real := ProjectSettings.globalize_path(TEMP_PATH)
+ var input_file_path := temp_path_real.path_join("input.txt")
+ var input_file := FileAccess.open(input_file_path, FileAccess.WRITE)
+ for i in range(processed_images.size()):
+ var temp_file_name := str(i + 1).pad_zeros(number_of_digits) + ".png"
+ var temp_file_path := temp_path_real.path_join(temp_file_name)
+ processed_images[i].save_png(temp_file_path)
+ input_file.store_line("file '" + temp_file_name + "'")
+ input_file.store_line("duration %s" % durations[i])
+ input_file.close()
+ var ffmpeg_execute: PackedStringArray = [
+ "-y", "-f", "concat", "-i", input_file_path, export_paths[0]
+ ]
+ var output := []
+ var success := OS.execute(Global.ffmpeg_path, ffmpeg_execute, output, true)
+ print(output)
+ var temp_dir := DirAccess.open(TEMP_PATH)
+ for file in temp_dir.get_files():
+ temp_dir.remove(file)
+ DirAccess.remove_absolute(TEMP_PATH)
+ if success < 0 or success > 1:
+ var fail_text := """Video failed to export. Make sure you have FFMPEG installed
+ and have set the correct path in the preferences."""
+ Global.popup_error(tr(fail_text))
+ return false
+ return true
+
+
func export_animated(args: Dictionary) -> void:
var project: Project = args["project"]
var exporter: AImgIOBaseExporter = args["exporter"]
@@ -397,6 +445,16 @@ func file_format_string(format_enum: int) -> String:
return ".gif"
FileFormat.APNG:
return ".apng"
+ FileFormat.MP4:
+ return ".mp4"
+ FileFormat.AVI:
+ return ".avi"
+ FileFormat.OGV:
+ return ".ogv"
+ FileFormat.MKV:
+ return ".mkv"
+ FileFormat.WEBM:
+ return ".webm"
_:
# If a file format description is not found, try generating one
if custom_exporter_generators.has(format_enum):
@@ -418,6 +476,16 @@ func file_format_description(format_enum: int) -> String:
return "GIF Image"
FileFormat.APNG:
return "APNG Image"
+ FileFormat.MP4:
+ return "MPEG-4 Video"
+ FileFormat.AVI:
+ return "AVI Video"
+ FileFormat.OGV:
+ return "OGV Video"
+ FileFormat.MKV:
+ return "Matroska Video"
+ FileFormat.WEBM:
+ return "WebM Video"
_:
# If a file format description is not found, try generating one
for key in custom_file_formats.keys():
@@ -426,12 +494,25 @@ func file_format_description(format_enum: int) -> String:
return ""
-## True when exporting to .gif and .apng (and potentially video formats in the future)
-## False when exporting to .png, and other non-animated formats in the future
+## True when exporting to .gif, .apng and video
+## False when exporting to .png, .jpg and static .webp
func is_single_file_format(project := Global.current_project) -> bool:
return animated_formats.has(project.file_format)
+func is_using_ffmpeg(format: FileFormat) -> bool:
+ return ffmpeg_formats.has(format)
+
+
+func is_ffmpeg_installed() -> bool:
+ if Global.ffmpeg_path.is_empty():
+ return false
+ var ffmpeg_executed := OS.execute(Global.ffmpeg_path, [])
+ if ffmpeg_executed == 0 or ffmpeg_executed == 1:
+ return true
+ return false
+
+
func _create_export_path(multifile: bool, project: Project, frame := 0) -> String:
var path := project.file_name
# Only append frame number when there are multiple files exported
diff --git a/src/Autoload/ExtensionsApi.gd b/src/Autoload/ExtensionsApi.gd
index 9ac67f9a5..4dcc45f2a 100644
--- a/src/Autoload/ExtensionsApi.gd
+++ b/src/Autoload/ExtensionsApi.gd
@@ -218,9 +218,7 @@ class DialogAPI:
## Shows an alert dialog with the given [param text].
## Useful for displaying messages like "Incompatible API" etc...
func show_error(text: String) -> void:
- Global.error_dialog.set_text(text)
- Global.error_dialog.popup_centered()
- Global.dialog_open(true)
+ Global.popup_error(text)
## Returns the node that is the parent of dialogs used in pixelorama.
func get_dialogs_parent_node() -> Node:
diff --git a/src/Autoload/Global.gd b/src/Autoload/Global.gd
index 1fbe945eb..b614452c7 100644
--- a/src/Autoload/Global.gd
+++ b/src/Autoload/Global.gd
@@ -117,8 +117,6 @@ var current_project_index := 0:
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.
@@ -131,6 +129,8 @@ var show_y_symmetry_axis := false
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. Refers to the ffmpeg location path.
+var ffmpeg_path := ""
## 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%.
@@ -148,16 +148,33 @@ var integer_zoom := false:
zoom_slider.step = 1
zoom_slider.value = zoom_slider.value # to trigger signal emission
-## Found in Preferences. The scale of the Interface.
+## Found in Preferences. The scale of the interface.
var shrink := 1.0
-## Found in Preferences. The font size used by the Interface.
+## 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.
+## Found in Preferences. If [code]true[/code], the interface dims on popups.
var dim_on_popup := true
+## Found in Preferences. If [code]true[/code], the native file dialogs of the
+## operating system are being used, instead of Godot's FileDialog node.
+var use_native_file_dialogs := false:
+ set(value):
+ use_native_file_dialogs = value
+ if not is_inside_tree():
+ await tree_entered
+ await get_tree().process_frame
+ get_tree().set_group(&"FileDialogs", "use_native_dialog", value)
+## Found in Preferences. If [code]true[/code], subwindows are embedded in the main window.
+var single_window_mode := true:
+ set(value):
+ single_window_mode = value
+ if OS.has_feature("editor"):
+ return
+ ProjectSettings.set_setting("display/window/subwindows/embed_subwindows", value)
+ ProjectSettings.save_custom(OVERRIDE_FILE)
## 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.
@@ -535,6 +552,7 @@ func _init() -> void:
data_directories.append(default_loc.path_join(HOME_SUBDIR_NAME))
if ProjectSettings.get_setting("display/window/tablet_driver") == "winink":
tablet_driver = 1
+ single_window_mode = ProjectSettings.get_setting("display/window/subwindows/embed_subwindows")
func _ready() -> void:
@@ -769,9 +787,7 @@ func undo_or_redo(
if current_cel is Cel3D:
current_cel.size_changed(project.size)
else:
- current_cel.image_texture = ImageTexture.create_from_image(
- current_cel.get_image()
- )
+ current_cel.image_texture.set_image(current_cel.get_image())
canvas.camera_zoom()
canvas.grid.queue_redraw()
canvas.pixel_grid.queue_redraw()
@@ -807,16 +823,19 @@ func _renderer_changed(value: int) -> void:
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)
+func popup_error(text: String) -> void:
+ error_dialog.set_text(text)
+ error_dialog.popup_centered()
+ dialog_open(true)
+
+
## 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:
@@ -1054,6 +1073,7 @@ func create_ui_for_shader_uniforms(
file_dialog.access = FileDialog.ACCESS_FILESYSTEM
file_dialog.size = Vector2(384, 281)
file_dialog.file_selected.connect(file_selected.bind(u_name))
+ file_dialog.use_native_dialog = use_native_file_dialogs
var button := Button.new()
button.text = "Load texture"
button.pressed.connect(file_dialog.popup_centered)
diff --git a/src/Autoload/OpenSave.gd b/src/Autoload/OpenSave.gd
index 5bf38ab64..478f97e1c 100644
--- a/src/Autoload/OpenSave.gd
+++ b/src/Autoload/OpenSave.gd
@@ -62,9 +62,7 @@ func handle_loading_file(file: String) -> void:
var image := Image.load_from_file(file)
if not is_instance_valid(image): # An error occurred
var file_name: String = file.get_file()
- Global.error_dialog.set_text(tr("Can't load file '%s'.") % [file_name])
- Global.error_dialog.popup_centered()
- Global.dialog_open(true)
+ Global.popup_error(tr("Can't load file '%s'.") % [file_name])
return
handle_loading_image(file, image)
@@ -159,11 +157,7 @@ func open_pxo_file(path: String, untitled_backup := false, replace_empty := true
if not success:
return
elif err != OK:
- Global.error_dialog.set_text(
- tr("File failed to open. Error code %s (%s)") % [err, error_string(err)]
- )
- Global.error_dialog.popup_centered()
- Global.dialog_open(true)
+ Global.popup_error(tr("File failed to open. Error code %s (%s)") % [err, error_string(err)])
return
else:
var data_json := zip_reader.read_file("data.json").get_string_from_utf8()
@@ -253,11 +247,7 @@ func open_v0_pxo_file(path: String, new_project: Project) -> bool:
file = FileAccess.open(path, FileAccess.READ)
var err := FileAccess.get_open_error()
if err != OK:
- Global.error_dialog.set_text(
- tr("File failed to open. Error code %s (%s)") % [err, error_string(err)]
- )
- Global.error_dialog.popup_centered()
- Global.dialog_open(true)
+ Global.popup_error(tr("File failed to open. Error code %s (%s)") % [err, error_string(err)])
return false
var first_line := file.get_line()
@@ -320,19 +310,11 @@ func save_pxo_file(
project.name = path.get_file().trim_suffix(".pxo")
var serialized_data := project.serialize()
if !serialized_data:
- Global.error_dialog.set_text(
- tr("File failed to save. Converting project data to dictionary failed.")
- )
- Global.error_dialog.popup_centered()
- Global.dialog_open(true)
+ Global.popup_error(tr("File failed to save. Converting project data to dictionary failed."))
return false
var to_save := JSON.stringify(serialized_data)
if !to_save:
- Global.error_dialog.set_text(
- tr("File failed to save. Converting dictionary to JSON failed.")
- )
- Global.error_dialog.popup_centered()
- Global.dialog_open(true)
+ Global.popup_error(tr("File failed to save. Converting dictionary to JSON failed."))
return false
# Check if a file with the same name exists. If it does, rename the new file temporarily.
@@ -346,11 +328,7 @@ func save_pxo_file(
if err != OK:
if temp_path.is_valid_filename():
return false
- Global.error_dialog.set_text(
- tr("File failed to save. Error code %s (%s)") % [err, error_string(err)]
- )
- Global.error_dialog.popup_centered()
- Global.dialog_open(true)
+ Global.popup_error(tr("File failed to save. Error code %s (%s)") % [err, error_string(err)])
if zip_packer: # this would be null if we attempt to save filenames such as "//\\||.pxo"
zip_packer.close()
return false
diff --git a/src/Autoload/Palettes.gd b/src/Autoload/Palettes.gd
index eb16353a2..c0280c282 100644
--- a/src/Autoload/Palettes.gd
+++ b/src/Autoload/Palettes.gd
@@ -441,11 +441,7 @@ func import_palette_from_path(path: String, make_copy := false, is_initialising
new_palette_imported.emit()
select_palette(palette.name)
else:
- Global.error_dialog.set_text(
- tr("Can't load file '%s'.\nThis is not a valid palette file.") % [path]
- )
- Global.error_dialog.popup_centered()
- Global.dialog_open(true)
+ Global.popup_error(tr("Can't load file '%s'.\nThis is not a valid palette file.") % [path])
## Refer to app/core/gimppalette-load.c of the GNU Image Manipulation Program for the "living spec"
diff --git a/src/Autoload/Tools.gd b/src/Autoload/Tools.gd
index 4ef87fdf8..1188a9586 100644
--- a/src/Autoload/Tools.gd
+++ b/src/Autoload/Tools.gd
@@ -541,8 +541,7 @@ func handle_draw(position: Vector2i, event: InputEvent) -> void:
var project := Global.current_project
var text := "[%s×%s]" % [project.size.x, project.size.y]
- if Global.has_focus:
- text += " %s, %s" % [position.x, position.y]
+ text += " %s, %s" % [position.x, position.y]
if not _slots[MOUSE_BUTTON_LEFT].tool_node.cursor_text.is_empty():
text += " %s" % _slots[MOUSE_BUTTON_LEFT].tool_node.cursor_text
if not _slots[MOUSE_BUTTON_RIGHT].tool_node.cursor_text.is_empty():
diff --git a/src/Main.gd b/src/Main.gd
index dddcc1b95..b1bf082a3 100644
--- a/src/Main.gd
+++ b/src/Main.gd
@@ -206,7 +206,6 @@ func _notification(what: int) -> void:
# If the mouse exits the window and another application has the focus,
# pause the application
NOTIFICATION_APPLICATION_FOCUS_OUT:
- Global.has_focus = false
if Global.pause_when_unfocused:
get_tree().paused = true
NOTIFICATION_WM_MOUSE_EXIT:
@@ -217,12 +216,6 @@ func _notification(what: int) -> void:
get_tree().paused = false
NOTIFICATION_APPLICATION_FOCUS_IN:
get_tree().paused = false
- var mouse_pos := get_global_mouse_position()
- var viewport_rect := Rect2(
- Global.main_viewport.global_position, Global.main_viewport.size
- )
- if viewport_rect.has_point(mouse_pos):
- Global.has_focus = true
func _on_files_dropped(files: PackedStringArray) -> void:
@@ -248,9 +241,7 @@ func load_last_project() -> void:
Global.config_cache.set_value("data", "current_dir", file_path.get_base_dir())
else:
# If file doesn't exist on disk then warn user about this
- Global.error_dialog.set_text("Cannot find last project file.")
- Global.error_dialog.popup_centered()
- Global.dialog_open(true)
+ Global.popup_error("Cannot find last project file.")
func load_recent_project_file(path: String) -> void:
@@ -266,9 +257,7 @@ func load_recent_project_file(path: String) -> void:
Global.config_cache.set_value("data", "current_dir", path.get_base_dir())
else:
# If file doesn't exist on disk then warn user about this
- Global.error_dialog.set_text("Cannot find project file.")
- Global.error_dialog.popup_centered()
- Global.dialog_open(true)
+ Global.popup_error("Cannot find project file.")
func _on_OpenSprite_files_selected(paths: PackedStringArray) -> void:
diff --git a/src/Palette/EditPaletteDialog.tscn b/src/Palette/EditPaletteDialog.tscn
index 815a7313a..1745e88bf 100644
--- a/src/Palette/EditPaletteDialog.tscn
+++ b/src/Palette/EditPaletteDialog.tscn
@@ -115,7 +115,7 @@ text = "Delete Palette?"
horizontal_alignment = 1
vertical_alignment = 1
-[node name="ExportFileDialog" type="FileDialog" parent="."]
+[node name="ExportFileDialog" type="FileDialog" parent="." groups=["FileDialogs"]]
size = Vector2i(677, 400)
access = 2
filters = PackedStringArray("*.png ; PNG Image", "*.jpg,*.jpeg ; JPEG Image", "*.webp ; WebP Image")
diff --git a/src/Preferences/HandleExtensions.gd b/src/Preferences/HandleExtensions.gd
index 0c0952e3d..ef3cd02de 100644
--- a/src/Preferences/HandleExtensions.gd
+++ b/src/Preferences/HandleExtensions.gd
@@ -212,9 +212,7 @@ func read_extension(extension_file_or_folder_name: StringName, internal := false
"\n",
"But Pixelorama's API version is: %s" % ExtensionsApi.get_api_version()
)
- Global.error_dialog.set_text(str(err_text, required_text))
- Global.error_dialog.popup_centered()
- Global.dialog_open(true)
+ Global.popup_error(str(err_text, required_text))
print("Incompatible API")
if !internal: # the file isn't created for internal extensions, no need for removal
# Don't put it in faulty, (it's merely incompatible)
diff --git a/src/Preferences/PreferencesDialog.gd b/src/Preferences/PreferencesDialog.gd
index 9720c3dbb..fc07a0c25 100644
--- a/src/Preferences/PreferencesDialog.gd
+++ b/src/Preferences/PreferencesDialog.gd
@@ -7,9 +7,20 @@ var preferences: Array[Preference] = [
Preference.new(
"quit_confirmation", "Startup/StartupContainer/QuitConfirmation", "button_pressed"
),
+ Preference.new("ffmpeg_path", "Startup/StartupContainer/FFMPEGPath", "text"),
Preference.new("shrink", "%ShrinkSlider", "value"),
Preference.new("font_size", "Interface/InterfaceOptions/FontSizeSlider", "value"),
Preference.new("dim_on_popup", "Interface/InterfaceOptions/DimCheckBox", "button_pressed"),
+ Preference.new(
+ "use_native_file_dialogs", "Interface/InterfaceOptions/NativeFileDialogs", "button_pressed"
+ ),
+ Preference.new(
+ "single_window_mode",
+ "Interface/InterfaceOptions/SingleWindowMode",
+ "button_pressed",
+ true,
+ true
+ ),
Preference.new("icon_color_from", "Interface/ButtonOptions/IconColorOptionButton", "selected"),
Preference.new("custom_icon_color", "Interface/ButtonOptions/IconColorButton", "color"),
Preference.new("left_tool_color", "Interface/ButtonOptions/LeftToolColorButton", "color"),
@@ -202,6 +213,10 @@ func _ready() -> void:
node.item_selected.connect(
_on_Preference_value_changed.bind(pref, restore_default_button)
)
+ "text":
+ node.text_changed.connect(
+ _on_Preference_value_changed.bind(pref, restore_default_button)
+ )
var global_value = Global.get(pref.prop_name)
if Global.config_cache.has_section_key("preferences", pref.prop_name):
diff --git a/src/Preferences/PreferencesDialog.tscn b/src/Preferences/PreferencesDialog.tscn
index d03e2d934..1db38cd78 100644
--- a/src/Preferences/PreferencesDialog.tscn
+++ b/src/Preferences/PreferencesDialog.tscn
@@ -1,4 +1,4 @@
-[gd_scene load_steps=9 format=3 uid="uid://b3hkjj3s6pe4x"]
+[gd_scene load_steps=10 format=3 uid="uid://b3hkjj3s6pe4x"]
[ext_resource type="Script" path="res://src/Preferences/PreferencesDialog.gd" id="1"]
[ext_resource type="Script" path="res://src/Preferences/HandleExtensions.gd" id="2"]
@@ -7,11 +7,13 @@
[ext_resource type="Script" path="res://src/Preferences/HandleThemes.gd" id="5"]
[ext_resource type="PackedScene" path="res://src/UI/Nodes/ValueSliderV2.tscn" id="7"]
[ext_resource type="Script" path="res://src/UI/Nodes/ValueSlider.gd" id="8"]
+[ext_resource type="PackedScene" uid="uid://chy5d42l72crk" path="res://src/UI/ExtensionExplorer/Store.tscn" id="8_jmnx8"]
[sub_resource type="ButtonGroup" id="ButtonGroup_8vsfb"]
[node name="PreferencesDialog" type="AcceptDialog"]
title = "Preferences"
+position = Vector2i(0, 36)
size = Vector2i(800, 500)
exclusive = false
popup_window = true
@@ -28,7 +30,7 @@ offset_bottom = -49.0
size_flags_horizontal = 3
theme_override_constants/separation = 20
theme_override_constants/autohide = 0
-split_offset = 150
+split_offset = 125
[node name="List" type="ItemList" parent="HSplitContainer"]
custom_minimum_size = Vector2(85, 0)
@@ -90,6 +92,13 @@ layout_mode = 2
mouse_default_cursor_shape = 2
text = "On"
+[node name="FFMPEGPathLabel" type="Label" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Startup/StartupContainer"]
+layout_mode = 2
+text = "FFMPEG path"
+
+[node name="FFMPEGPath" type="LineEdit" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Startup/StartupContainer"]
+layout_mode = 2
+
[node name="Language" type="VBoxContainer" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide"]
visible = false
layout_mode = 2
@@ -200,6 +209,32 @@ mouse_default_cursor_shape = 2
button_pressed = true
text = "On"
+[node name="NativeFileDialogsLabel" type="Label" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Interface/InterfaceOptions"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Use native file dialogs"
+
+[node name="NativeFileDialogs" type="CheckBox" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Interface/InterfaceOptions"]
+layout_mode = 2
+size_flags_horizontal = 3
+tooltip_text = "When this setting is enabled, the native file dialogs of the operating system are being used, instead of Pixelorama's custom ones."
+mouse_default_cursor_shape = 2
+button_pressed = true
+text = "On"
+
+[node name="SingleWindowModeLabel" type="Label" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Interface/InterfaceOptions"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Single window mode"
+
+[node name="SingleWindowMode" type="CheckBox" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Interface/InterfaceOptions"]
+layout_mode = 2
+size_flags_horizontal = 3
+tooltip_text = "When this setting is enabled, Pixelorama's subwindows will be embedded in the main window, otherwise each dialog will be its own separate window."
+mouse_default_cursor_shape = 2
+button_pressed = true
+text = "On"
+
[node name="ThemesHeader" type="HBoxContainer" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Interface"]
layout_mode = 2
theme_override_constants/separation = 0
@@ -1076,6 +1111,7 @@ tooltip_text = "Specifies the tablet driver being used on Windows. If you have W
mouse_default_cursor_shape = 2
[node name="Extensions" type="VBoxContainer" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide"]
+unique_name_in_owner = true
visible = false
layout_mode = 2
script = ExtResource("2")
@@ -1093,6 +1129,10 @@ text = "Extensions"
layout_mode = 2
size_flags_horizontal = 3
+[node name="Explore" type="Button" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions/ExtensionsHeader"]
+layout_mode = 2
+text = "Explore Online"
+
[node name="InstalledExtensions" type="ItemList" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions"]
layout_mode = 2
auto_height = true
@@ -1293,9 +1333,20 @@ layout_mode = 2
layout_mode = 2
text = "Pixelorama must be restarted for changes to take effect."
-[node name="Popups" type="Node" parent="."]
+[node name="Popups" type="Control" parent="."]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 8.0
+offset_top = 8.0
+offset_right = -8.0
+offset_bottom = -49.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
-[node name="AddExtensionFileDialog" type="FileDialog" parent="Popups"]
+[node name="AddExtensionFileDialog" type="FileDialog" parent="Popups" groups=["FileDialogs"]]
mode = 1
title = "Open File(s)"
size = Vector2i(560, 400)
@@ -1307,6 +1358,9 @@ access = 2
filters = PackedStringArray("*.pck ; Godot Resource Pack File", "*.zip ;")
show_hidden_files = true
+[node name="Store" parent="Popups" instance=ExtResource("8_jmnx8")]
+transient = true
+
[node name="DeleteConfirmation" type="ConfirmationDialog" parent="."]
unique_name_in_owner = true
position = Vector2i(0, 36)
@@ -1329,6 +1383,7 @@ vertical_alignment = 1
[connection signal="item_selected" from="HSplitContainer/List" to="." method="_on_List_item_selected"]
[connection signal="pressed" from="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Language/System Language" to="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Language" method="_on_Language_pressed" binds= [1]]
[connection signal="pressed" from="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Interface/InterfaceOptions/ShrinkContainer/ShrinkApplyButton" to="." method="_on_ShrinkApplyButton_pressed"]
+[connection signal="pressed" from="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions/ExtensionsHeader/Explore" to="Popups/Store" method="_on_explore_pressed"]
[connection signal="empty_clicked" from="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions/InstalledExtensions" to="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions" method="_on_InstalledExtensions_empty_clicked"]
[connection signal="item_selected" from="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions/InstalledExtensions" to="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions" method="_on_InstalledExtensions_item_selected"]
[connection signal="pressed" from="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions/HBoxContainer/AddExtensionButton" to="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions" method="_on_AddExtensionButton_pressed"]
diff --git a/src/Shaders/Effects/Rotation/CommonRotation.gdshaderinc b/src/Shaders/Effects/Rotation/CommonRotation.gdshaderinc
new file mode 100644
index 000000000..43d7dbd31
--- /dev/null
+++ b/src/Shaders/Effects/Rotation/CommonRotation.gdshaderinc
@@ -0,0 +1,49 @@
+uniform sampler2D selection_tex;
+uniform vec2 pivot_pixel;
+uniform float angle;
+
+vec2 rotate(vec2 uv, vec2 pivot, float ratio) {
+ // Scale and center image
+ uv.x -= pivot.x;
+ uv.x *= ratio;
+ uv.x += pivot.x;
+
+ // Rotate image
+ uv -= pivot;
+ mat3 transformation = mat3(
+ vec3(cos(angle), -sin(angle), 0.0),
+ vec3(sin(angle), cos(angle), 0.0),
+ vec3(0.0, 0.0, 1.0)
+ );
+
+ uv = (transformation * vec3(uv, 1.0)).xy;
+ uv.x /= ratio;
+ uv += pivot;
+
+ return uv;
+}
+
+vec4 mix_rotated_and_original(vec4 color, vec4 original_color, vec2 uv, vec2 rotated_uv, vec2 tex_pixel_size) {
+ color.a *= texture(selection_tex, rotated_uv).a; // Combine with selection mask
+ // Make a border to prevent stretching pixels on the edge
+ vec2 border_uv = rotated_uv;
+
+ // Center the border
+ border_uv -= 0.5;
+ border_uv *= 2.0;
+ border_uv = abs(border_uv);
+
+ float border = max(border_uv.x, border_uv.y); // This is a rectangular gradient
+ border = floor(border - tex_pixel_size.x); // Turn the grad into a rectangle shape
+ border = 1.0 - clamp(border, 0.0, 1.0); // Invert the rectangle
+
+ float selection = texture(selection_tex, uv).a;
+ float mask = mix(selection, 1.0, 1.0 - ceil(original_color.a)); // Combine selection mask with area outside original
+
+ vec4 final_color;
+ // Combine original and rotated image only when intersecting, otherwise just pure rotated image.
+ final_color.rgb = mix(mix(original_color.rgb, color.rgb, color.a * border), color.rgb, mask);
+ final_color.a = mix(original_color.a, 0.0, selection); // Remove alpha on the selected area
+ final_color.a = mix(final_color.a, 1.0, color.a * border); // Combine alpha of original image and rotated
+ return final_color;
+}
\ No newline at end of file
diff --git a/src/Shaders/Effects/Rotation/NearestNeighbour.gdshader b/src/Shaders/Effects/Rotation/NearestNeighbour.gdshader
index 7dfbc470c..9d8564b09 100644
--- a/src/Shaders/Effects/Rotation/NearestNeighbour.gdshader
+++ b/src/Shaders/Effects/Rotation/NearestNeighbour.gdshader
@@ -1,56 +1,17 @@
shader_type canvas_item;
render_mode unshaded;
-uniform float angle;
-uniform sampler2D selection_tex;
-uniform vec2 pivot_pixel;
-
-
-vec2 rotate(vec2 uv, vec2 pivot, float ratio) {
- // Scale and center image
- uv.x -= pivot.x;
- uv.x *= ratio;
- uv.x += pivot.x;
-
- // Rotate image
- uv -= pivot;
- uv = vec2(cos(angle) * uv.x + sin(angle) * uv.y,
- -sin(angle) * uv.x + cos(angle) * uv.y);
- uv.x /= ratio;
- uv += pivot;
-
- return uv;
-}
+#include "res://src/Shaders/Effects/Rotation/CommonRotation.gdshaderinc"
void fragment() {
vec4 original = texture(TEXTURE, UV);
- float selection = texture(selection_tex, UV).a;
-
vec2 tex_size = 1.0 / TEXTURE_PIXEL_SIZE; // Texture size in real pixel coordinates
vec2 pixelated_uv = floor(UV * tex_size) / (tex_size - 1.0); // Pixelate UV to fit resolution
vec2 pivot = pivot_pixel / tex_size; // Normalize pivot position
float ratio = tex_size.x / tex_size.y; // Resolution ratio
- // Make a border to prevent stretching pixels on the edge
- vec2 border_uv = rotate(pixelated_uv, pivot, ratio);
-
- // Center the border
- border_uv -= 0.5;
- border_uv *= 2.0;
- border_uv = abs(border_uv);
-
- float border = max(border_uv.x, border_uv.y); // This is a rectangular gradient
- border = floor(border - TEXTURE_PIXEL_SIZE.x); // Turn the grad into a rectangle shape
- border = 1.0 - clamp(border, 0.0, 1.0); // Invert the rectangle
-
- // Mixing
- vec4 rotated = texture(TEXTURE, rotate(pixelated_uv, pivot, ratio)); // Rotated image
- rotated.a *= texture(selection_tex, rotate(pixelated_uv, pivot, ratio)).a; // Combine with selection mask
- float mask = mix(selection, 1.0, 1.0 - ceil(original.a)); // Combine selection mask with area outside original
-
- // Combine original and rotated image only when intersecting, otherwise just pure rotated image.
- COLOR.rgb = mix(mix(original.rgb, rotated.rgb, rotated.a * border), rotated.rgb, mask);
- COLOR.a = mix(original.a, 0.0, selection); // Remove alpha on the selected area
- COLOR.a = mix(COLOR.a, 1.0, rotated.a * border); // Combine alpha of original image and rotated
+ vec2 rotated_uv = rotate(pixelated_uv, pivot, ratio);
+ vec4 rotated_color = texture(TEXTURE, rotated_uv); // Rotated image
+ COLOR = mix_rotated_and_original(rotated_color, original, UV, rotated_uv, TEXTURE_PIXEL_SIZE);
}
diff --git a/src/Shaders/Effects/Rotation/OmniScale.gdshader b/src/Shaders/Effects/Rotation/OmniScale.gdshader
index 3510ef97b..832fae4cb 100644
--- a/src/Shaders/Effects/Rotation/OmniScale.gdshader
+++ b/src/Shaders/Effects/Rotation/OmniScale.gdshader
@@ -33,6 +33,8 @@ shader_type canvas_item;
// SOFTWARE.
//
+#include "res://src/Shaders/Effects/Rotation/CommonRotation.gdshaderinc"
+
uniform int ScaleMultiplier : hint_range(0, 100) = 4;
// vertex compatibility #defines
@@ -40,9 +42,6 @@ uniform int ScaleMultiplier : hint_range(0, 100) = 4;
// #define outsize vec4(OutputSize, 1.0 / OutputSize)
// Pixelorama-specific uniforms
-uniform float angle;
-uniform sampler2D selection_tex;
-uniform vec2 pivot_pixel;
uniform bool preview = false;
@@ -289,27 +288,9 @@ vec4 scale(sampler2D image, vec2 coord, vec2 pxSize) {
}
-vec2 rotate(vec2 uv, vec2 pivot, float ratio) { // Taken from NearestNeighbour shader
- // Scale and center image
- uv.x -= pivot.x;
- uv.x *= ratio;
- uv.x += pivot.x;
-
- // Rotate image
- uv -= pivot;
- uv = vec2(cos(angle) * uv.x + sin(angle) * uv.y,
- -sin(angle) * uv.x + cos(angle) * uv.y);
- uv.x /= ratio;
- uv += pivot;
-
- return uv;
-}
-
-
void fragment()
{
vec4 original = texture(TEXTURE, UV);
- float selection = texture(selection_tex, UV).a;
vec2 size = 1.0 / TEXTURE_PIXEL_SIZE;
vec2 pivot = pivot_pixel / size; // Normalize pivot position
float ratio = size.x / size.y; // Resolution ratio
@@ -321,29 +302,8 @@ void fragment()
else {
rotated_uv = rotate(UV, pivot, ratio);
}
- vec4 c;
- c = scale(TEXTURE, rotated_uv, TEXTURE_PIXEL_SIZE);
+ vec4 c = scale(TEXTURE, rotated_uv, TEXTURE_PIXEL_SIZE);
- // Taken from NearestNeighbour shader
- c.a *= texture(selection_tex, rotated_uv).a; // Combine with selection mask
- // Make a border to prevent stretching pixels on the edge
- vec2 border_uv = rotated_uv;
-
- // Center the border
- border_uv -= 0.5;
- border_uv *= 2.0;
- border_uv = abs(border_uv);
-
- float border = max(border_uv.x, border_uv.y); // This is a rectangular gradient
- border = floor(border - TEXTURE_PIXEL_SIZE.x); // Turn the grad into a rectangle shape
- border = 1.0 - clamp(border, 0.0, 1.0); // Invert the rectangle
-
- float mask = mix(selection, 1.0, 1.0 - ceil(original.a)); // Combine selection mask with area outside original
-
- // Combine original and rotated image only when intersecting, otherwise just pure rotated image.
- COLOR.rgb = mix(mix(original.rgb, c.rgb, c.a * border), c.rgb, mask);
- COLOR.a = mix(original.a, 0.0, selection); // Remove alpha on the selected area
- COLOR.a = mix(COLOR.a, 1.0, c.a * border); // Combine alpha of original image and rotated
- //c.a = step(0.5,c.a);
- //COLOR = c;
+ // Pixelorama edit
+ COLOR = mix_rotated_and_original(c, original, UV, rotated_uv, TEXTURE_PIXEL_SIZE);
}
\ No newline at end of file
diff --git a/src/Shaders/Effects/Rotation/cleanEdge.gdshader b/src/Shaders/Effects/Rotation/cleanEdge.gdshader
index 9add3d6de..38c90775a 100644
--- a/src/Shaders/Effects/Rotation/cleanEdge.gdshader
+++ b/src/Shaders/Effects/Rotation/cleanEdge.gdshader
@@ -25,7 +25,7 @@ OTHER DEALINGS IN THE SOFTWARE.
shader_type canvas_item;
-
+#include "res://src/Shaders/Effects/Rotation/CommonRotation.gdshaderinc"
//enables 2:1 slopes. otherwise only uses 45 degree slopes
#define SLOPE
//cleans up small detail slope transitions (if SLOPE is enabled)
@@ -45,9 +45,6 @@ uniform float similarThreshold = 0.0;
uniform float lineWidth = 1.0;
// Edited for Pixelorama
-uniform float angle;
-uniform sampler2D selection_tex;
-uniform vec2 pivot_pixel;
uniform bool preview = false;
bool similar(vec4 col1, vec4 col2){
@@ -274,28 +271,10 @@ vec4 sliceDist(vec2 point, vec2 mainDir, vec2 pointDir, vec4 u, vec4 uf, vec4 uf
return vec4(-1.0);
}
-// Pixelorama edit, taken from NearestNeighbour shader
-vec2 rotate(vec2 uv, vec2 pivot, float ratio) {
- // Scale and center image
- uv.x -= pivot.x;
- uv.x *= ratio;
- uv.x += pivot.x;
-
- // Rotate image
- uv -= pivot;
- uv = vec2(cos(angle) * uv.x + sin(angle) * uv.y,
- -sin(angle) * uv.x + cos(angle) * uv.y);
- uv.x /= ratio;
- uv += pivot;
-
- return uv;
-}
-
void fragment() {
vec2 size = 1.0/TEXTURE_PIXEL_SIZE+0.0001; //fix for some sort of rounding error
// Pixelorama edit
vec4 original = texture(TEXTURE, UV);
- float selection = texture(selection_tex, UV).a;
vec2 pivot = pivot_pixel / size; // Normalize pivot position
float ratio = size.x / size.y; // Resolution ratio
vec2 pixelated_uv = floor(UV * size) / (size - 1.0); // Pixelate UV to fit resolutio
@@ -360,24 +339,6 @@ void fragment() {
col = u_col;
}
- // Pixelorama edit, taken from NearestNeighbour shader
- col.a *= texture(selection_tex, rotated_uv).a; // Combine with selection mask
- // Make a border to prevent stretching pixels on the edge
- vec2 border_uv = rotated_uv;
-
- // Center the border
- border_uv -= 0.5;
- border_uv *= 2.0;
- border_uv = abs(border_uv);
-
- float border = max(border_uv.x, border_uv.y); // This is a rectangular gradient
- border = floor(border - TEXTURE_PIXEL_SIZE.x); // Turn the grad into a rectangle shape
- border = 1.0 - clamp(border, 0.0, 1.0); // Invert the rectangle
-
- float mask = mix(selection, 1.0, 1.0 - ceil(original.a)); // Combine selection mask with area outside original
-
- // Combine original and rotated image only when intersecting, otherwise just pure rotated image.
- COLOR.rgb = mix(mix(original.rgb, col.rgb, col.a * border), col.rgb, mask);
- COLOR.a = mix(original.a, 0.0, selection); // Remove alpha on the selected area
- COLOR.a = mix(COLOR.a, 1.0, col.a * border); // Combine alpha of original image and rotated
+ // Pixelorama edit
+ COLOR = mix_rotated_and_original(col, original, UV, rotated_uv, TEXTURE_PIXEL_SIZE);
}
diff --git a/src/Tools/3DTools/3DShapeEdit.tscn b/src/Tools/3DTools/3DShapeEdit.tscn
index 2d34b83ed..f78845945 100644
--- a/src/Tools/3DTools/3DShapeEdit.tscn
+++ b/src/Tools/3DTools/3DShapeEdit.tscn
@@ -773,7 +773,7 @@ script = ExtResource("5")
wait_time = 0.2
one_shot = true
-[node name="LoadModelDialog" type="FileDialog" parent="." index="6"]
+[node name="LoadModelDialog" type="FileDialog" parent="." index="6" groups=["FileDialogs"]]
mode = 1
title = "Open File(s)"
size = Vector2i(558, 300)
diff --git a/src/Tools/BaseSelectionTool.gd b/src/Tools/BaseSelectionTool.gd
index 0195c8826..4374ca148 100644
--- a/src/Tools/BaseSelectionTool.gd
+++ b/src/Tools/BaseSelectionTool.gd
@@ -252,6 +252,8 @@ func _on_Size_value_changed(value: Vector2i) -> void:
if timer.is_stopped():
undo_data = selection_node.get_undo_data(false)
+ if not selection_node.is_moving_content:
+ selection_node.original_bitmap.copy_from(Global.current_project.selection_map)
timer.start()
selection_node.big_bounding_rectangle.size = value
selection_node.resize_selection()
@@ -262,5 +264,5 @@ func _on_Size_ratio_toggled(button_pressed: bool) -> void:
func _on_Timer_timeout() -> void:
- if !selection_node.is_moving_content:
+ if not selection_node.is_moving_content:
selection_node.commit_undo("Move Selection", undo_data)
diff --git a/src/UI/Canvas/Canvas.gd b/src/UI/Canvas/Canvas.gd
index 29de35cc2..ec1dd888e 100644
--- a/src/UI/Canvas/Canvas.gd
+++ b/src/UI/Canvas/Canvas.gd
@@ -91,8 +91,7 @@ func _input(event: InputEvent) -> void:
Tools.handle_draw(Vector2i(current_pixel.floor()), event)
if sprite_changed_this_frame:
- if Global.has_focus:
- queue_redraw()
+ queue_redraw()
update_selected_cels_textures()
diff --git a/src/UI/Canvas/Indicators.gd b/src/UI/Canvas/Indicators.gd
index 08ceb727a..e0c864fca 100644
--- a/src/UI/Canvas/Indicators.gd
+++ b/src/UI/Canvas/Indicators.gd
@@ -2,12 +2,11 @@ extends Node2D
func _input(event: InputEvent) -> void:
- if Global.has_focus:
- if event is InputEventMouse or event is InputEventKey:
- queue_redraw()
+ if event is InputEventMouse or event is InputEventKey:
+ queue_redraw()
func _draw() -> void:
# Draw rectangle to indicate the pixel currently being hovered on
- if Global.has_focus and Global.can_draw:
+ if Global.can_draw:
Tools.draw_indicator()
diff --git a/src/UI/Canvas/MouseGuide.gd b/src/UI/Canvas/MouseGuide.gd
index 65a2168c9..6019659b0 100644
--- a/src/UI/Canvas/MouseGuide.gd
+++ b/src/UI/Canvas/MouseGuide.gd
@@ -29,7 +29,7 @@ func draw_guide_line():
func _input(event: InputEvent) -> void:
- if !Global.show_mouse_guides or !Global.can_draw or !Global.has_focus:
+ if !Global.show_mouse_guides or !Global.can_draw:
visible = false
return
visible = true
diff --git a/src/UI/Canvas/Previews.gd b/src/UI/Canvas/Previews.gd
index 2f90d6cef..82340a881 100644
--- a/src/UI/Canvas/Previews.gd
+++ b/src/UI/Canvas/Previews.gd
@@ -2,11 +2,10 @@ extends Node2D
func _input(event: InputEvent) -> void:
- if Global.has_focus:
- if event is InputEventMouse or event is InputEventKey:
- queue_redraw()
+ if event is InputEventMouse or event is InputEventKey:
+ queue_redraw()
func _draw() -> void:
- if Global.has_focus and Global.can_draw:
+ if Global.can_draw:
Tools.draw_preview()
diff --git a/src/UI/Canvas/ReferenceImages.gd b/src/UI/Canvas/ReferenceImages.gd
index 5cd81d494..4963e6785 100644
--- a/src/UI/Canvas/ReferenceImages.gd
+++ b/src/UI/Canvas/ReferenceImages.gd
@@ -55,7 +55,6 @@ func _input(event: InputEvent) -> void:
var ri: ReferenceImage = Global.current_project.get_current_reference_image()
if !ri:
- Global.can_draw = true
return
# Check if want to cancelthe reference transform
diff --git a/src/UI/Canvas/Rulers/Guide.gd b/src/UI/Canvas/Rulers/Guide.gd
index 6733ad4dd..14e05b0e6 100644
--- a/src/UI/Canvas/Rulers/Guide.gd
+++ b/src/UI/Canvas/Rulers/Guide.gd
@@ -39,7 +39,6 @@ func _input(_event: InputEvent) -> void:
if (
Input.is_action_just_pressed(&"left_mouse")
and Global.can_draw
- and Global.has_focus
and rect.has_point(mouse_pos)
):
if (
diff --git a/src/UI/Canvas/Selection.gd b/src/UI/Canvas/Selection.gd
index 659b65f08..4b1636b89 100644
--- a/src/UI/Canvas/Selection.gd
+++ b/src/UI/Canvas/Selection.gd
@@ -13,6 +13,11 @@ var is_pasting := false
var big_bounding_rectangle := Rect2i():
set(value):
big_bounding_rectangle = value
+ if value.size == Vector2i(0, 0):
+ set_process_input(false)
+ Global.can_draw = true
+ else:
+ set_process_input(true)
for slot in Tools._slots.values():
if slot.tool_node is BaseSelectionTool:
slot.tool_node.set_spinbox_values()
@@ -34,7 +39,8 @@ var preview_image_texture := ImageTexture.new()
var undo_data: Dictionary
var gizmos: Array[Gizmo] = []
var dragged_gizmo: Gizmo = null
-var prev_angle := 0
+var angle := 0.0
+var content_pivot := Vector2.ZERO
var mouse_pos_on_gizmo_drag := Vector2.ZERO
var resize_keep_ratio := false
@@ -53,24 +59,24 @@ class Gizmo:
type = _type
direction = _direction
- func get_cursor() -> Control.CursorShape:
- var cursor := Control.CURSOR_MOVE
+ func get_cursor() -> DisplayServer.CursorShape:
+ var cursor := DisplayServer.CURSOR_MOVE
if direction == Vector2i.ZERO:
- return Control.CURSOR_POINTING_HAND
+ return DisplayServer.CURSOR_POINTING_HAND
elif direction == Vector2i(-1, -1) or direction == Vector2i(1, 1): # Top left or bottom right
if Global.mirror_view:
- cursor = Control.CURSOR_BDIAGSIZE
+ cursor = DisplayServer.CURSOR_BDIAGSIZE
else:
- cursor = Control.CURSOR_FDIAGSIZE
+ cursor = DisplayServer.CURSOR_FDIAGSIZE
elif direction == Vector2i(1, -1) or direction == Vector2i(-1, 1): # Top right or bottom left
if Global.mirror_view:
- cursor = Control.CURSOR_FDIAGSIZE
+ cursor = DisplayServer.CURSOR_FDIAGSIZE
else:
- cursor = Control.CURSOR_BDIAGSIZE
+ cursor = DisplayServer.CURSOR_BDIAGSIZE
elif direction == Vector2i(0, -1) or direction == Vector2i(0, 1): # Center top or center bottom
- cursor = Control.CURSOR_VSIZE
+ cursor = DisplayServer.CURSOR_VSIZE
elif direction == Vector2i(-1, 0) or direction == Vector2i(1, 0): # Center left or center right
- cursor = Control.CURSOR_HSIZE
+ cursor = DisplayServer.CURSOR_HSIZE
return cursor
@@ -84,12 +90,11 @@ func _ready() -> void:
gizmos.append(Gizmo.new(Gizmo.Type.SCALE, Vector2i(0, 1))) # Center bottom
gizmos.append(Gizmo.new(Gizmo.Type.SCALE, Vector2i(-1, 1))) # Bottom left
gizmos.append(Gizmo.new(Gizmo.Type.SCALE, Vector2i(-1, 0))) # Center left
-
-
-# gizmos.append(Gizmo.new(Gizmo.Type.ROTATE)) # Rotation gizmo (temp)
+ #gizmos.append(Gizmo.new(Gizmo.Type.ROTATE)) # Rotation gizmo (temp)
func _input(event: InputEvent) -> void:
+ var project := Global.current_project
image_current_pixel = canvas.current_pixel
if Global.mirror_view:
image_current_pixel.x = Global.current_project.size.x - image_current_pixel.x
@@ -99,7 +104,6 @@ func _input(event: InputEvent) -> void:
elif Input.is_action_just_pressed("transformation_cancel"):
transform_content_cancel()
- var project := Global.current_project
if not project.layers[project.current_layer].can_layer_get_drawn():
return
if event is InputEventKey:
@@ -123,17 +127,14 @@ func _input(event: InputEvent) -> void:
if Input.is_action_pressed("transform_move_selection_only"):
transform_content_confirm()
if not is_moving_content:
+ original_bitmap.copy_from(Global.current_project.selection_map)
+ original_big_bounding_rectangle = big_bounding_rectangle
if Input.is_action_pressed("transform_move_selection_only"):
undo_data = get_undo_data(false)
temp_rect = big_bounding_rectangle
else:
transform_content_start()
project.selection_offset = Vector2.ZERO
- if dragged_gizmo.type == Gizmo.Type.ROTATE:
- var img_size := maxi(
- original_preview_image.get_width(), original_preview_image.get_height()
- )
- original_preview_image.crop(img_size, img_size)
else:
var prev_temp_rect := temp_rect
dragged_gizmo.direction.x *= signi(temp_rect.size.x)
@@ -157,7 +158,9 @@ func _input(event: InputEvent) -> void:
Global.can_draw = true
dragged_gizmo = null
if not is_moving_content:
+ original_bitmap = SelectionMap.new()
commit_undo("Select", undo_data)
+ angle = 0.0
if dragged_gizmo:
if dragged_gizmo.type == Gizmo.Type.SCALE:
@@ -166,17 +169,17 @@ func _input(event: InputEvent) -> void:
_gizmo_rotate()
else: # Set the appropriate cursor
if gizmo_hover:
- Global.main_viewport.mouse_default_cursor_shape = gizmo_hover.get_cursor()
+ DisplayServer.cursor_set_shape(gizmo_hover.get_cursor())
else:
- var cursor := Control.CURSOR_ARROW
+ var cursor := DisplayServer.CURSOR_ARROW
if Global.cross_cursor:
- cursor = Control.CURSOR_CROSS
+ cursor = DisplayServer.CURSOR_CROSS
var layer: BaseLayer = project.layers[project.current_layer]
if not layer.can_layer_get_drawn():
- cursor = Control.CURSOR_FORBIDDEN
+ cursor = DisplayServer.CURSOR_FORBIDDEN
- if Global.main_viewport.mouse_default_cursor_shape != cursor:
- Global.main_viewport.mouse_default_cursor_shape = cursor
+ if DisplayServer.cursor_get_shape() != cursor:
+ DisplayServer.cursor_set_shape(cursor)
func _move_with_arrow_keys(event: InputEvent) -> void:
@@ -210,14 +213,14 @@ func _move_with_arrow_keys(event: InputEvent) -> void:
var move := input.rotated(snappedf(Global.camera.rotation, PI / 2))
# These checks are needed to fix a bug where the selection got stuck
# to the canvas boundaries when they were 1px away from them
- if is_equal_approx(absf(move.x), 0.0):
+ if is_zero_approx(absf(move.x)):
move.x = 0
- if is_equal_approx(absf(move.y), 0.0):
+ if is_zero_approx(absf(move.y)):
move.y = 0
move_content(move * step)
-# Check if an event is a ui_up/down/left/right event-press
+## Check if an event is a ui_up/down/left/right event pressed
func _is_action_direction_pressed(event: InputEvent) -> bool:
for action in KEY_MOVE_ACTION_NAMES:
if event.is_action_pressed(action, false, true):
@@ -225,7 +228,7 @@ func _is_action_direction_pressed(event: InputEvent) -> bool:
return false
-# Check if an event is a ui_up/down/left/right event release
+## Check if an event is a ui_up/down/left/right event
func _is_action_direction(event: InputEvent) -> bool:
for action in KEY_MOVE_ACTION_NAMES:
if event.is_action(action, true):
@@ -233,7 +236,7 @@ func _is_action_direction(event: InputEvent) -> bool:
return false
-# Check if an event is a ui_up/down/left/right event release
+## Check if an event is a ui_up/down/left/right event release
func _is_action_direction_released(event: InputEvent) -> bool:
for action in KEY_MOVE_ACTION_NAMES:
if event.is_action_released(action, true):
@@ -282,9 +285,9 @@ func _update_gizmos() -> void:
)
# Rotation gizmo (temp)
-# gizmos[8].rect = Rect2(
-# Vector2((rect_end.x + rect_pos.x - size.x) / 2, rect_pos.y - size.y - (size.y * 2)), size
-# )
+ #gizmos[8].rect = Rect2(
+ #Vector2((rect_end.x + rect_pos.x - size.x) / 2, rect_pos.y - size.y - (size.y * 2)), size
+ #)
queue_redraw()
@@ -337,8 +340,6 @@ func _gizmo_resize() -> void:
temp_rect.position.y = end_y - temp_rect.size.y
big_bounding_rectangle = temp_rect.abs()
-# big_bounding_rectangle.position = Vector2(big_bounding_rectangle.position).ceil()
-# big_bounding_rectangle.size = big_bounding_rectangle.size.floor()
if big_bounding_rectangle.size.x == 0:
big_bounding_rectangle.size.x = 1
if big_bounding_rectangle.size.y == 0:
@@ -370,9 +371,14 @@ func _resize_rect(pos: Vector2, dir: Vector2) -> void:
func resize_selection() -> void:
var size := big_bounding_rectangle.size.abs()
- if is_moving_content:
+ if original_bitmap.is_empty():
+ print("original_bitmap is empty, this shouldn't happen.")
+ else:
Global.current_project.selection_map.copy_from(original_bitmap)
+ if is_moving_content:
+ content_pivot = original_big_bounding_rectangle.size / 2.0
preview_image.copy_from(original_preview_image)
+ DrawingAlgos.nn_rotate(preview_image, angle, content_pivot)
preview_image.resize(size.x, size.y, Image.INTERPOLATE_NEAREST)
if temp_rect.size.x < 0:
preview_image.flip_x()
@@ -380,6 +386,12 @@ func resize_selection() -> void:
preview_image.flip_y()
preview_image_texture = ImageTexture.create_from_image(preview_image)
+ Global.current_project.selection_map.copy_from(original_bitmap)
+ var bitmap_pivot := (
+ original_big_bounding_rectangle.position
+ + ((original_big_bounding_rectangle.end - original_big_bounding_rectangle.position) / 2)
+ )
+ DrawingAlgos.nn_rotate(Global.current_project.selection_map, angle, bitmap_pivot)
Global.current_project.selection_map.resize_bitmap_values(
Global.current_project, size, temp_rect.size.x < 0, temp_rect.size.y < 0
)
@@ -388,42 +400,12 @@ func resize_selection() -> void:
Global.canvas.queue_redraw()
-func _gizmo_rotate() -> void: # Does not work properly yet
- var angle := image_current_pixel.angle_to_point(mouse_pos_on_gizmo_drag)
- angle = deg_to_rad(floorf(rad_to_deg(angle)))
- if angle == prev_angle:
- return
- prev_angle = angle
-# var img_size := max(original_preview_image.get_width(), original_preview_image.get_height())
-# var pivot = Vector2(original_preview_image.get_width()/2, original_preview_image.get_height()/2)
- var pivot := Vector2(big_bounding_rectangle.size.x / 2.0, big_bounding_rectangle.size.y / 2.0)
- preview_image.copy_from(original_preview_image)
- if original_big_bounding_rectangle.position != big_bounding_rectangle.position:
- preview_image.fill(Color(0, 0, 0, 0))
- var pos_diff := (
- (original_big_bounding_rectangle.position - big_bounding_rectangle.position).abs()
- )
-# pos_diff.y = 0
- preview_image.blit_rect(
- original_preview_image, Rect2(Vector2.ZERO, preview_image.get_size()), pos_diff
- )
- DrawingAlgos.nn_rotate(preview_image, angle, pivot)
- preview_image_texture = ImageTexture.create_from_image(preview_image)
-
- var bitmap_image := original_bitmap
- var bitmap_pivot := (
- original_big_bounding_rectangle.position
- + ((original_big_bounding_rectangle.end - original_big_bounding_rectangle.position) / 2)
- )
- DrawingAlgos.nn_rotate(bitmap_image, angle, bitmap_pivot)
- Global.current_project.selection_map = bitmap_image
- Global.current_project.selection_map_changed()
- big_bounding_rectangle = bitmap_image.get_used_rect()
- queue_redraw()
- Global.canvas.queue_redraw()
+func _gizmo_rotate() -> void:
+ angle = image_current_pixel.angle_to_point(mouse_pos_on_gizmo_drag)
+ resize_selection()
-func select_rect(rect: Rect2i, operation: int = SelectionOperation.ADD) -> void:
+func select_rect(rect: Rect2i, operation := SelectionOperation.ADD) -> void:
var project := Global.current_project
# Used only if the selection is outside of the canvas boundaries,
# on the left and/or above (negative coords)
@@ -510,15 +492,15 @@ func transform_content_confirm() -> void:
return
var project := Global.current_project
for cel in _get_selected_draw_cels():
- var cel_image: Image = cel.get_image()
- var src: Image = preview_image
+ var cel_image := cel.get_image()
+ var src := Image.new()
+ src.copy_from(preview_image)
if not is_pasting:
src.copy_from(cel.transformed_content)
cel.transformed_content = null
+ DrawingAlgos.nn_rotate(src, angle, content_pivot)
src.resize(
- big_bounding_rectangle.size.x,
- big_bounding_rectangle.size.y,
- Image.INTERPOLATE_NEAREST
+ preview_image.get_width(), preview_image.get_height(), Image.INTERPOLATE_NEAREST
)
if temp_rect.size.x < 0:
src.flip_x()
@@ -539,6 +521,8 @@ func transform_content_confirm() -> void:
original_bitmap = SelectionMap.new()
is_moving_content = false
is_pasting = false
+ angle = 0.0
+ content_pivot = Vector2.ZERO
queue_redraw()
Global.canvas.queue_redraw()
@@ -570,6 +554,8 @@ func transform_content_cancel() -> void:
preview_image = Image.new()
original_bitmap = SelectionMap.new()
is_pasting = false
+ angle = 0.0
+ content_pivot = Vector2.ZERO
queue_redraw()
Global.canvas.queue_redraw()
diff --git a/src/UI/ColorPickers/ColorPicker.gd b/src/UI/ColorPickers/ColorPicker.gd
index 1d45ec70d..f89ebdda5 100644
--- a/src/UI/ColorPickers/ColorPicker.gd
+++ b/src/UI/ColorPickers/ColorPicker.gd
@@ -1,5 +1,7 @@
extends Container
+## The swatches button of the [ColorPicker] node. Used to ensure that swatches are always invisible
+var swatches_button: Button
@onready var color_picker := %ColorPicker as ColorPicker
@onready var color_buttons := %ColorButtons as HBoxContainer
@onready var left_color_rect := %LeftColorRect as ColorRect
@@ -55,6 +57,14 @@ func _ready() -> void:
color_buttons.get_parent().remove_child(color_buttons)
sampler_cont.add_child(color_buttons)
sampler_cont.move_child(color_buttons, 0)
+ swatches_button = picker_vbox_container.get_child(5, true) as Button
+ swatches_button.visible = false
+ # The GridContainer that contains the swatch buttons. These are not visible in our case
+ # but for some reason its h_separation needs to be set to a value larger than 4,
+ # otherwise a weird bug occurs with the Recent Colors where, adding new colors
+ # increases the size of the color buttons.
+ var presets_container := picker_vbox_container.get_child(6, true) as GridContainer
+ presets_container.add_theme_constant_override("h_separation", 5)
func _on_color_picker_color_changed(color: Color) -> void:
@@ -98,6 +108,9 @@ func _on_ColorDefaults_pressed() -> void:
func _on_expand_button_toggled(toggled_on: bool) -> void:
color_picker.color_modes_visible = toggled_on
color_picker.sliders_visible = toggled_on
+ color_picker.presets_visible = toggled_on
+ if is_instance_valid(swatches_button):
+ swatches_button.visible = false
Global.config_cache.set_value("color_picker", "is_expanded", toggled_on)
diff --git a/src/UI/Dialogs/ExportDialog.gd b/src/UI/Dialogs/ExportDialog.gd
index 56edb5125..3ec69bdb1 100644
--- a/src/UI/Dialogs/ExportDialog.gd
+++ b/src/UI/Dialogs/ExportDialog.gd
@@ -13,7 +13,12 @@ var image_exports: Array[Export.FileFormat] = [
Export.FileFormat.WEBP,
Export.FileFormat.JPEG,
Export.FileFormat.GIF,
- Export.FileFormat.APNG
+ Export.FileFormat.APNG,
+ Export.FileFormat.MP4,
+ Export.FileFormat.AVI,
+ Export.FileFormat.OGV,
+ Export.FileFormat.MKV,
+ Export.FileFormat.WEBM,
]
var spritesheet_exports: Array[Export.FileFormat] = [
Export.FileFormat.PNG, Export.FileFormat.WEBP, Export.FileFormat.JPEG
@@ -188,6 +193,12 @@ func set_file_format_selector() -> void:
match Export.current_tab:
Export.ExportTab.IMAGE:
_set_file_format_selector_suitable_file_formats(image_exports)
+ if Export.is_ffmpeg_installed():
+ for format in Export.ffmpeg_formats:
+ file_format_options.set_item_disabled(format, false)
+ else:
+ for format in Export.ffmpeg_formats:
+ file_format_options.set_item_disabled(format, true)
Export.ExportTab.SPRITESHEET:
_set_file_format_selector_suitable_file_formats(spritesheet_exports)
@@ -246,9 +257,9 @@ func update_dimensions_label() -> void:
func open_path_validation_alert_popup(path_or_name: int = -1) -> void:
# 0 is invalid path, 1 is invalid name
- var error_text := "DirAccess path and file name are not valid!"
+ var error_text := "Directory path and file name are not valid!"
if path_or_name == 0:
- error_text = "DirAccess path is not valid!"
+ error_text = "Directory path is not valid!"
elif path_or_name == 1:
error_text = "File name is not valid!"
diff --git a/src/UI/Dialogs/ExportDialog.tscn b/src/UI/Dialogs/ExportDialog.tscn
index 029d1c67e..9534e604e 100644
--- a/src/UI/Dialogs/ExportDialog.tscn
+++ b/src/UI/Dialogs/ExportDialog.tscn
@@ -306,7 +306,7 @@ offset_right = 692.0
offset_bottom = 551.0
mouse_filter = 2
-[node name="PathDialog" type="FileDialog" parent="Popups"]
+[node name="PathDialog" type="FileDialog" parent="Popups" groups=["FileDialogs"]]
mode = 2
title = "Open a Directory"
size = Vector2i(675, 500)
diff --git a/src/UI/Dialogs/ImageEffects/RotateImage.gd b/src/UI/Dialogs/ImageEffects/RotateImage.gd
index 741639d3f..2d65b1f4d 100644
--- a/src/UI/Dialogs/ImageEffects/RotateImage.gd
+++ b/src/UI/Dialogs/ImageEffects/RotateImage.gd
@@ -250,13 +250,13 @@ func _on_Indicator_draw() -> void:
else:
conversion_scale = ratio.y
var pivot_position := pivot * conversion_scale
- pivot_indicator.draw_arc(pivot_position, 2, 0, 360, 360, Color.YELLOW, 0.5)
- pivot_indicator.draw_arc(pivot_position, 6, 0, 360, 360, Color.WHITE, 0.5)
+ pivot_indicator.draw_arc(pivot_position, 2, 0, 360, 360, Color.YELLOW)
+ pivot_indicator.draw_arc(pivot_position, 6, 0, 360, 360, Color.WHITE)
pivot_indicator.draw_line(
- pivot_position - Vector2.UP * 10, pivot_position - Vector2.DOWN * 10, Color.WHITE, 0.5
+ pivot_position - Vector2.UP * 10, pivot_position - Vector2.DOWN * 10, Color.WHITE
)
pivot_indicator.draw_line(
- pivot_position - Vector2.RIGHT * 10, pivot_position - Vector2.LEFT * 10, Color.WHITE, 0.5
+ pivot_position - Vector2.RIGHT * 10, pivot_position - Vector2.LEFT * 10, Color.WHITE
)
@@ -267,8 +267,7 @@ func _on_Indicator_gui_input(event: InputEvent) -> void:
drag_pivot = false
if drag_pivot:
var img_size := preview_image.get_size()
-# var mouse_pos := get_local_mouse_position() - pivot_indicator.position
- var mouse_pos := pivot_indicator.position
+ var mouse_pos := pivot_indicator.get_local_mouse_position()
var ratio := Vector2(img_size) / pivot_indicator.size
# we need to set the scale according to the larger side
var conversion_scale: float
diff --git a/src/UI/Dialogs/ImageEffects/ShaderEffect.tscn b/src/UI/Dialogs/ImageEffects/ShaderEffect.tscn
index ccecefc55..d86aa0464 100644
--- a/src/UI/Dialogs/ImageEffects/ShaderEffect.tscn
+++ b/src/UI/Dialogs/ImageEffects/ShaderEffect.tscn
@@ -53,7 +53,7 @@ text = "No shader loaded!"
[node name="ShaderParams" type="VBoxContainer" parent="VBoxContainer"]
layout_mode = 2
-[node name="FileDialog" type="FileDialog" parent="."]
+[node name="FileDialog" type="FileDialog" parent="." groups=["FileDialogs"]]
access = 2
filters = PackedStringArray("*gdshader; Godot Shader File")
show_hidden_files = true
diff --git a/src/UI/Dialogs/OpenSprite.tscn b/src/UI/Dialogs/OpenSprite.tscn
index c0e98ee2a..c7745d848 100644
--- a/src/UI/Dialogs/OpenSprite.tscn
+++ b/src/UI/Dialogs/OpenSprite.tscn
@@ -1,6 +1,6 @@
[gd_scene format=3 uid="uid://b3aeqj2k58wdk"]
-[node name="OpenSprite" type="FileDialog"]
+[node name="OpenSprite" type="FileDialog" groups=["FileDialogs"]]
title = "Open File(s)"
size = Vector2i(558, 400)
exclusive = false
diff --git a/src/UI/Dialogs/SaveSprite.tscn b/src/UI/Dialogs/SaveSprite.tscn
index 73a3288ae..50b177449 100644
--- a/src/UI/Dialogs/SaveSprite.tscn
+++ b/src/UI/Dialogs/SaveSprite.tscn
@@ -1,6 +1,6 @@
[gd_scene format=3 uid="uid://d4euwo633u33b"]
-[node name="SaveSprite" type="FileDialog"]
+[node name="SaveSprite" type="FileDialog" groups=["FileDialogs"]]
size = Vector2i(675, 400)
exclusive = false
popup_window = true
diff --git a/src/UI/ExtensionExplorer/Entry/ExtensionEntry.gd b/src/UI/ExtensionExplorer/Entry/ExtensionEntry.gd
new file mode 100644
index 000000000..33102ccc2
--- /dev/null
+++ b/src/UI/ExtensionExplorer/Entry/ExtensionEntry.gd
@@ -0,0 +1,185 @@
+class_name ExtensionEntry
+extends Panel
+
+var extension_container: VBoxContainer
+var thumbnail := ""
+var download_link := ""
+var download_path := ""
+var tags := PackedStringArray()
+var is_update := false ## An update instead of download
+
+# node references used in this script
+@onready var ext_name := %ExtensionName as Label
+@onready var ext_discription := %ExtensionDescription as TextEdit
+@onready var small_picture := %Picture as TextureButton
+@onready var enlarged_picture := %Enlarged as TextureRect
+@onready var request_delay := %RequestDelay as Timer
+@onready var thumbnail_request := %ImageRequest as HTTPRequest
+@onready var extension_downloader := %DownloadRequest as HTTPRequest
+@onready var down_button := %DownloadButton as Button
+@onready var progress_bar := %ProgressBar as ProgressBar
+@onready var done_label := %Done as Label
+@onready var alert_dialog := %Alert as AcceptDialog
+
+
+func set_info(info: Dictionary, extension_path: String) -> void:
+ if "name" in info.keys() and "version" in info.keys():
+ ext_name.text = str(info["name"], "-v", info["version"])
+ # check for updates
+ change_button_if_updatable(info["name"], info["version"])
+ # Setting path extension will be "temporarily" downloaded to before install
+ DirAccess.make_dir_recursive_absolute(str(extension_path, "Download/"))
+ download_path = str(extension_path, "Download/", info["name"], ".pck")
+ if "description" in info.keys():
+ ext_discription.text = info["description"]
+ ext_discription.tooltip_text = ext_discription.text
+ if "thumbnail" in info.keys():
+ thumbnail = info["thumbnail"]
+ if "download_link" in info.keys():
+ download_link = info["download_link"]
+ if "tags" in info.keys():
+ tags.append_array(info["tags"])
+
+ # Adding a tiny delay to prevent sending bulk requests
+ request_delay.wait_time = randf() * 2
+ request_delay.start()
+
+
+func _on_RequestDelay_timeout() -> void:
+ request_delay.queue_free() # node no longer needed
+ thumbnail_request.request(thumbnail) # image
+
+
+func _on_ImageRequest_request_completed(
+ _result, _response_code, _headers, body: PackedByteArray
+) -> void:
+ # Update the received image
+ thumbnail_request.queue_free()
+ var image := Image.new()
+ # for images on internet there is a hagh chance that extension is wrong
+ # so check all of them even if they give error
+ var err := image.load_png_from_buffer(body)
+ if err != OK:
+ var err_a := image.load_jpg_from_buffer(body)
+ if err_a != OK:
+ var err_b := image.load_webp_from_buffer(body)
+ if err_b != OK:
+ var err_c := image.load_tga_from_buffer(body)
+ if err_c != OK:
+ image.load_bmp_from_buffer(body)
+ var texture := ImageTexture.create_from_image(image)
+ small_picture.texture_normal = texture
+ small_picture.pressed.connect(enlarge_thumbnail.bind(texture))
+
+
+func _on_Download_pressed() -> void:
+ down_button.disabled = true
+ extension_downloader.download_file = download_path
+ extension_downloader.request(download_link)
+ prepare_progress()
+
+
+## Called after the extension downloader has finished its job
+func _on_DownloadRequest_request_completed(
+ result: int, _response_code: int, _headers: PackedStringArray, _body: PackedByteArray
+) -> void:
+ if result == HTTPRequest.RESULT_SUCCESS:
+ # Add extension
+ extension_container.install_extension(download_path)
+ if is_update:
+ is_update = false
+ announce_done(true)
+ else:
+ alert_dialog.get_node("Text").text = (
+ str(
+ "Unable to Download extension...\nHttp Code: ",
+ result,
+ " (",
+ error_string(result),
+ ")"
+ )
+ . c_unescape()
+ )
+ alert_dialog.popup_centered()
+ announce_done(false)
+ DirAccess.remove_absolute(download_path)
+
+
+## Updates the entry node's UI
+func announce_done(success: bool) -> void:
+ close_progress()
+ down_button.disabled = false
+ if success:
+ done_label.visible = true
+ down_button.text = "Re-Download"
+ done_label.get_node("DoneDelay").start()
+
+
+## Returns true if entry contains ALL tags in tag_array
+func tags_match(tag_array: PackedStringArray) -> bool:
+ if tags.size() > 0:
+ for tag in tag_array:
+ if !tag in tags:
+ return false
+ return true
+ else:
+ if tag_array.size() > 0:
+ return false
+ return true
+
+
+## Updates the entry node's UI if it has an update available
+func change_button_if_updatable(extension_name: String, new_version: float) -> void:
+ for extension in extension_container.extensions.keys():
+ if extension_container.extensions[extension].file_name == extension_name:
+ var old_version = str_to_var(extension_container.extensions[extension].version)
+ if typeof(old_version) == TYPE_FLOAT:
+ if new_version > old_version:
+ down_button.text = "Update"
+ is_update = true
+ elif new_version == old_version:
+ down_button.text = "Re-Download"
+
+
+## Show an enlarged version of the thumbnail
+func enlarge_thumbnail(texture: ImageTexture) -> void:
+ enlarged_picture.texture = texture
+ enlarged_picture.get_parent().popup_centered()
+
+
+## A beautification function that hides the "Done" label bar after some time
+func _on_DoneDelay_timeout() -> void:
+ done_label.visible = false
+
+
+## Progress bar method
+func prepare_progress() -> void:
+ progress_bar.visible = true
+ progress_bar.value = 0
+ progress_bar.get_node("ProgressTimer").start()
+
+
+## Progress bar method
+func update_progress() -> void:
+ var down := extension_downloader.get_downloaded_bytes()
+ var total := extension_downloader.get_body_size()
+ progress_bar.value = (float(down) / float(total)) * 100.0
+
+
+## Progress bar method
+func close_progress() -> void:
+ progress_bar.visible = false
+ progress_bar.get_node("ProgressTimer").stop()
+
+
+## Progress bar method
+func _on_ProgressTimer_timeout() -> void:
+ update_progress()
+
+
+func _manage_enlarded_thumbnail_close() -> void:
+ enlarged_picture.get_parent().hide()
+
+
+func _manage_alert_close() -> void:
+ alert_dialog.hide()
diff --git a/src/UI/ExtensionExplorer/Entry/ExtensionEntry.tscn b/src/UI/ExtensionExplorer/Entry/ExtensionEntry.tscn
new file mode 100644
index 000000000..23f0a387d
--- /dev/null
+++ b/src/UI/ExtensionExplorer/Entry/ExtensionEntry.tscn
@@ -0,0 +1,125 @@
+[gd_scene load_steps=3 format=3 uid="uid://dnjpemuehkxsn"]
+
+[ext_resource type="Script" path="res://src/UI/ExtensionExplorer/Entry/ExtensionEntry.gd" id="1_3no3v"]
+[ext_resource type="Texture2D" uid="uid://b47r0c6auaqk6" path="res://assets/graphics/icons/icon.png" id="2_qhsve"]
+
+[node name="ExtensionEntry" type="Panel"]
+self_modulate = Color(0.411765, 0.411765, 0.411765, 1)
+custom_minimum_size = Vector2(300, 150)
+offset_right = 284.0
+offset_bottom = 150.0
+size_flags_horizontal = 3
+script = ExtResource("1_3no3v")
+
+[node name="MarginContainer" type="MarginContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer"]
+layout_mode = 2
+
+[node name="Picture" type="TextureButton" parent="MarginContainer/HBoxContainer"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(120, 120)
+layout_mode = 2
+mouse_default_cursor_shape = 2
+texture_normal = ExtResource("2_qhsve")
+ignore_texture_size = true
+stretch_mode = 5
+
+[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="ExtensionName" type="Label" parent="MarginContainer/HBoxContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Extension Name..."
+
+[node name="ExtensionDescription" type="TextEdit" parent="MarginContainer/HBoxContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+placeholder_text = "Description"
+editable = false
+wrap_mode = 1
+
+[node name="Done" type="Label" parent="MarginContainer/HBoxContainer/VBoxContainer"]
+unique_name_in_owner = true
+visible = false
+self_modulate = Color(0.337255, 1, 0, 1)
+layout_mode = 2
+text = "Done!!!"
+
+[node name="DoneDelay" type="Timer" parent="MarginContainer/HBoxContainer/VBoxContainer/Done"]
+wait_time = 2.0
+
+[node name="ProgressBar" type="ProgressBar" parent="MarginContainer/HBoxContainer/VBoxContainer"]
+unique_name_in_owner = true
+visible = false
+custom_minimum_size = Vector2(0, 20)
+layout_mode = 2
+
+[node name="ProgressTimer" type="Timer" parent="MarginContainer/HBoxContainer/VBoxContainer/ProgressBar"]
+wait_time = 0.1
+
+[node name="DownloadButton" type="Button" parent="MarginContainer/HBoxContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Download"
+
+[node name="RequestDelay" type="Timer" parent="."]
+unique_name_in_owner = true
+one_shot = true
+autostart = true
+
+[node name="ImageRequest" type="HTTPRequest" parent="."]
+unique_name_in_owner = true
+
+[node name="DownloadRequest" type="HTTPRequest" parent="."]
+unique_name_in_owner = true
+
+[node name="Alert" type="AcceptDialog" parent="."]
+unique_name_in_owner = true
+size = Vector2i(421, 106)
+
+[node name="Text" type="Label" parent="Alert"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 8.0
+offset_top = 8.0
+offset_right = -8.0
+offset_bottom = -49.0
+horizontal_alignment = 1
+
+[node name="EnlardedThumbnail" type="Window" parent="."]
+position = Vector2i(0, 36)
+size = Vector2i(440, 360)
+visible = false
+transient = true
+exclusive = true
+
+[node name="Enlarged" type="TextureRect" parent="EnlardedThumbnail"]
+unique_name_in_owner = true
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+stretch_mode = 5
+
+[connection signal="timeout" from="MarginContainer/HBoxContainer/VBoxContainer/Done/DoneDelay" to="." method="_on_DoneDelay_timeout"]
+[connection signal="timeout" from="MarginContainer/HBoxContainer/VBoxContainer/ProgressBar/ProgressTimer" to="." method="_on_ProgressTimer_timeout"]
+[connection signal="pressed" from="MarginContainer/HBoxContainer/VBoxContainer/DownloadButton" to="." method="_on_Download_pressed"]
+[connection signal="timeout" from="RequestDelay" to="." method="_on_RequestDelay_timeout"]
+[connection signal="request_completed" from="ImageRequest" to="." method="_on_ImageRequest_request_completed"]
+[connection signal="request_completed" from="DownloadRequest" to="." method="_on_DownloadRequest_request_completed"]
+[connection signal="close_requested" from="Alert" to="." method="_manage_alert_close"]
+[connection signal="focus_exited" from="Alert" to="." method="_manage_alert_close"]
+[connection signal="close_requested" from="EnlardedThumbnail" to="." method="_manage_enlarded_thumbnail_close"]
+[connection signal="focus_exited" from="EnlardedThumbnail" to="." method="_manage_enlarded_thumbnail_close"]
diff --git a/src/UI/ExtensionExplorer/Store.gd b/src/UI/ExtensionExplorer/Store.gd
new file mode 100644
index 000000000..b0a6343ac
--- /dev/null
+++ b/src/UI/ExtensionExplorer/Store.gd
@@ -0,0 +1,214 @@
+extends Window
+
+## Usage:
+## Change the "STORE_NAME" and "STORE_LINK"
+## Don't touch anything else
+
+const STORE_NAME := "Extension Explorer"
+# gdlint: ignore=max-line-length
+const STORE_LINK := "https://raw.githubusercontent.com/Orama-Interactive/Pixelorama/master/src/UI/ExtensionExplorer/store_info.md"
+## File that will contain information about extensions available for download
+const STORE_INFORMATION_FILE := STORE_NAME + ".md"
+const EXTENSION_ENTRY_TSCN := preload("res://src/UI/ExtensionExplorer/Entry/ExtensionEntry.tscn")
+
+# Variables placed here due to their frequent use
+var extension_container: VBoxContainer
+var extension_path: String ## The path where extensions will be stored (obtained from pixelorama)
+var custom_links_remaining: int ## Remaining custom links to be processed
+var redirects: Array[String]
+var faulty_custom_links: Array[String]
+
+# node references used in this script
+@onready var content: VBoxContainer = $"%Content"
+@onready var store_info_downloader: HTTPRequest = %StoreInformationDownloader
+@onready var main_store_link: LineEdit = %MainStoreLink
+@onready var custom_store_links: VBoxContainer = %CustomStoreLinks
+@onready var search_manager: LineEdit = %SearchManager
+@onready var tab_container: TabContainer = %TabContainer
+@onready var progress_bar: ProgressBar = %ProgressBar
+@onready var update_timer: Timer = %UpdateTimer
+@onready var faulty_links_label: Label = %FaultyLinks
+@onready var custom_link_error: AcceptDialog = %ErrorCustom
+@onready var error_get_info: AcceptDialog = %Error
+
+
+func _ready() -> void:
+ # Basic setup
+ extension_container = Global.preferences_dialog.find_child("Extensions")
+ main_store_link.text = STORE_LINK
+ # Get the path that pixelorama uses to store extensions
+ extension_path = ProjectSettings.globalize_path(extension_container.EXTENSIONS_PATH)
+ # tell the downloader where to download the store information
+ store_info_downloader.download_file = extension_path.path_join(STORE_INFORMATION_FILE)
+
+
+func _on_Store_about_to_show() -> void:
+ # Clear old tags
+ search_manager.available_tags = PackedStringArray()
+ for tag in search_manager.tag_list.get_children():
+ tag.queue_free()
+ # Clear old entries
+ for entry in content.get_children():
+ entry.queue_free()
+ faulty_custom_links.clear()
+ custom_links_remaining = custom_store_links.custom_links.size()
+ fetch_info(STORE_LINK)
+
+
+func _on_close_requested() -> void:
+ hide()
+
+
+func fetch_info(link: String) -> void:
+ if extension_path != "": # Did everything went smoothly in _ready() function?
+ # everything is ready, now request the store information
+ # so that available extensions could be displayed
+ var error := store_info_downloader.request(link)
+ if error == OK:
+ prepare_progress()
+ else:
+ printerr("Unable to get info from remote repository.")
+ error_getting_info(error)
+
+
+## When downloading is finished
+func _on_StoreInformation_request_completed(
+ result: int, _response_code: int, _headers: PackedStringArray, _body: PackedByteArray
+) -> void:
+ if result == HTTPRequest.RESULT_SUCCESS:
+ # process the info contained in the file
+ var file := FileAccess.open(
+ extension_path.path_join(STORE_INFORMATION_FILE), FileAccess.READ
+ )
+ while not file.eof_reached():
+ process_line(file.get_line())
+ file.close()
+
+ DirAccess.remove_absolute(extension_path.path_join(STORE_INFORMATION_FILE))
+ # Hide the progress bar because it's no longer required
+ close_progress()
+ else:
+ printerr("Unable to get info from remote repository...")
+ error_getting_info(result)
+
+
+func close_progress() -> void:
+ progress_bar.get_parent().visible = false
+ tab_container.visible = true
+ update_timer.stop()
+ if redirects.size() > 0:
+ var next_link := redirects.pop_front() as String
+ fetch_info(next_link)
+ else:
+ # no more redirects, jump to the next store
+ custom_links_remaining -= 1
+ if custom_links_remaining >= 0:
+ var next_link: String = custom_store_links.custom_links[custom_links_remaining]
+ fetch_info(next_link)
+ else:
+ if faulty_custom_links.size() > 0: # manage custom faulty links
+ faulty_links_label.text = ""
+ for link in faulty_custom_links:
+ faulty_links_label.text += str(link, "\n")
+ custom_link_error.popup_centered()
+
+
+## Signal connected from StoreButton.tscn
+func _on_explore_pressed() -> void:
+ popup_centered()
+
+
+## Function related to error dialog
+func _on_CopyCommand_pressed() -> void:
+ DisplayServer.clipboard_set(
+ "sudo flatpak override com.orama_interactive.Pixelorama --share=network"
+ )
+
+
+## Adds a new extension entry to the "content"
+func add_entry(info: Dictionary) -> void:
+ var entry := EXTENSION_ENTRY_TSCN.instantiate()
+ entry.extension_container = extension_container
+ content.add_child(entry)
+ entry.set_info(info, extension_path)
+
+
+## Gets called when data couldn't be fetched from remote repository
+func error_getting_info(result: int) -> void:
+ # Shows a popup if error is from main link (i-e MainStore)
+ # Popups for errors in custom_links are handled in close_progress()
+ if custom_links_remaining == custom_store_links.custom_links.size():
+ error_get_info.popup_centered()
+ error_get_info.title = error_string(result)
+ else:
+ faulty_custom_links.append(custom_store_links.custom_links[custom_links_remaining])
+ close_progress()
+
+
+## Progress bar method
+func prepare_progress() -> void:
+ progress_bar.get_parent().visible = true
+ tab_container.visible = false
+ progress_bar.value = 0
+ update_timer.start()
+
+
+## Progress bar method
+func update_progress() -> void:
+ var down := store_info_downloader.get_downloaded_bytes()
+ var total := store_info_downloader.get_body_size()
+ progress_bar.value = (float(down) / float(total)) * 100.0
+
+
+## Progress bar method
+func _on_UpdateTimer_timeout() -> void:
+ update_progress()
+
+
+# DATA PROCESSORS
+func process_line(line: String):
+ # If the line isn't a comment, we will check data type
+ var raw_data
+ line = line.strip_edges()
+ if !line.begins_with("#") and !line.begins_with("//") and line != "":
+ # attempting to convert to a variable other than a string
+ raw_data = str_to_var(line)
+ if !raw_data: # attempt failed, using it as string
+ raw_data = line
+
+ # Determine action based on data type
+ match typeof(raw_data):
+ TYPE_ARRAY:
+ var extension_data: Dictionary = parse_extension_data(raw_data)
+ add_entry(extension_data)
+ TYPE_STRING:
+ # it's most probably a store link
+ var link: String = raw_data.strip_edges()
+ if !link in redirects:
+ redirects.append(link)
+
+
+func parse_extension_data(raw_data: Array) -> Dictionary:
+ DirAccess.make_dir_recursive_absolute(str(extension_path, "Download/"))
+ var result := {}
+ # Check for non-compulsory things if they exist
+ for item in raw_data:
+ if typeof(item) == TYPE_ARRAY:
+ # first array element should always be an identifier text type
+ var identifier = item.pop_front()
+ if typeof(identifier) == TYPE_STRING and item.size() > 0:
+ match identifier:
+ "name":
+ result["name"] = item[0]
+ "version":
+ result["version"] = item[0]
+ "description":
+ result["description"] = item[0]
+ "thumbnail":
+ result["thumbnail"] = item[0]
+ "download_link":
+ result["download_link"] = item[0]
+ "tags": # (this should remain as an array)
+ result["tags"] = item
+ search_manager.add_new_tags(item)
+ return result
diff --git a/src/UI/ExtensionExplorer/Store.tscn b/src/UI/ExtensionExplorer/Store.tscn
new file mode 100644
index 000000000..283a90b3a
--- /dev/null
+++ b/src/UI/ExtensionExplorer/Store.tscn
@@ -0,0 +1,230 @@
+[gd_scene load_steps=5 format=3 uid="uid://chy5d42l72crk"]
+
+[ext_resource type="Script" path="res://src/UI/ExtensionExplorer/Store.gd" id="1_pwcwi"]
+[ext_resource type="Script" path="res://src/UI/ExtensionExplorer/Subscripts/SearchManager.gd" id="2_uqsvm"]
+[ext_resource type="Script" path="res://src/UI/ExtensionExplorer/Subscripts/CustomStoreLinks.gd" id="3_dk1xf"]
+[ext_resource type="Texture2D" uid="uid://d1urikaf1lxwl" path="res://assets/graphics/timeline/new_frame.png" id="4_ntl7p"]
+
+[node name="Store" type="Window"]
+title = "Explore Online"
+position = Vector2i(0, 36)
+size = Vector2i(760, 470)
+visible = false
+wrap_controls = true
+exclusive = true
+script = ExtResource("1_pwcwi")
+
+[node name="TabContainer" type="TabContainer" parent="."]
+unique_name_in_owner = true
+clip_contents = true
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 7.0
+offset_top = 8.0
+offset_right = -7.0
+offset_bottom = -8.0
+
+[node name="Store" type="MarginContainer" parent="TabContainer"]
+layout_mode = 2
+
+[node name="StoreMainContainer" type="HSplitContainer" parent="TabContainer/Store"]
+layout_mode = 2
+
+[node name="Parameters" type="VBoxContainer" parent="TabContainer/Store/StoreMainContainer"]
+custom_minimum_size = Vector2(150, 0)
+layout_mode = 2
+
+[node name="SearchManager" type="LineEdit" parent="TabContainer/Store/StoreMainContainer/Parameters"]
+unique_name_in_owner = true
+layout_mode = 2
+placeholder_text = "Search..."
+script = ExtResource("2_uqsvm")
+
+[node name="Header" type="Label" parent="TabContainer/Store/StoreMainContainer/Parameters"]
+layout_mode = 2
+theme_type_variation = &"HeaderSmall"
+text = "Tags:"
+
+[node name="ScrollContainer" type="ScrollContainer" parent="TabContainer/Store/StoreMainContainer/Parameters"]
+layout_mode = 2
+size_flags_vertical = 3
+horizontal_scroll_mode = 0
+
+[node name="TagList" type="VBoxContainer" parent="TabContainer/Store/StoreMainContainer/Parameters/ScrollContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+alignment = 1
+
+[node name="ContentScrollContainer" type="ScrollContainer" parent="TabContainer/Store/StoreMainContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="Content" type="VBoxContainer" parent="TabContainer/Store/StoreMainContainer/ContentScrollContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="Options" type="MarginContainer" parent="TabContainer"]
+visible = false
+layout_mode = 2
+
+[node name="ScrollContainer" type="ScrollContainer" parent="TabContainer/Options"]
+layout_mode = 2
+
+[node name="CustomStoreLinks" type="VBoxContainer" parent="TabContainer/Options/ScrollContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+script = ExtResource("3_dk1xf")
+
+[node name="Header" type="Label" parent="TabContainer/Options/ScrollContainer/CustomStoreLinks"]
+layout_mode = 2
+theme_type_variation = &"HeaderSmall"
+text = "Store Links:"
+
+[node name="MainStoreLink" type="LineEdit" parent="TabContainer/Options/ScrollContainer/CustomStoreLinks"]
+unique_name_in_owner = true
+layout_mode = 2
+editable = false
+
+[node name="Links" type="VBoxContainer" parent="TabContainer/Options/ScrollContainer/CustomStoreLinks"]
+layout_mode = 2
+
+[node name="ButtonContainer" type="HBoxContainer" parent="TabContainer/Options/ScrollContainer/CustomStoreLinks"]
+layout_mode = 2
+
+[node name="Guide" type="Button" parent="TabContainer/Options/ScrollContainer/CustomStoreLinks/ButtonContainer"]
+visible = false
+layout_mode = 2
+text = "Guide to making a Store File"
+
+[node name="NewLink" type="Button" parent="TabContainer/Options/ScrollContainer/CustomStoreLinks/ButtonContainer"]
+custom_minimum_size = Vector2(24, 24)
+layout_mode = 2
+size_flags_horizontal = 8
+
+[node name="TextureRect" type="TextureRect" parent="TabContainer/Options/ScrollContainer/CustomStoreLinks/ButtonContainer/NewLink"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 5.0
+offset_top = 5.0
+offset_right = -5.0
+offset_bottom = -5.0
+grow_horizontal = 2
+grow_vertical = 2
+texture = ExtResource("4_ntl7p")
+stretch_mode = 5
+
+[node name="ProgressContainer" type="VBoxContainer" parent="."]
+unique_name_in_owner = true
+visible = false
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 51.0
+offset_top = 21.0
+offset_right = -37.0
+offset_bottom = -5.0
+alignment = 1
+
+[node name="ProgressBar" type="ProgressBar" parent="ProgressContainer"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(0, 20)
+layout_mode = 2
+
+[node name="Label" type="Label" parent="ProgressContainer"]
+layout_mode = 2
+text = "Fetching data from Remote Repository
+Please Wait"
+horizontal_alignment = 1
+
+[node name="UpdateTimer" type="Timer" parent="ProgressContainer"]
+unique_name_in_owner = true
+wait_time = 0.1
+
+[node name="StoreInformationDownloader" type="HTTPRequest" parent="."]
+unique_name_in_owner = true
+
+[node name="Error" type="AcceptDialog" parent="."]
+unique_name_in_owner = true
+position = Vector2i(0, 36)
+size = Vector2i(511, 326)
+unresizable = true
+popup_window = true
+
+[node name="Content" type="VBoxContainer" parent="Error"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 8.0
+offset_top = 8.0
+offset_right = -8.0
+offset_bottom = -49.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="Label" type="Label" parent="Error/Content"]
+custom_minimum_size = Vector2(495, 180)
+layout_mode = 2
+text = "Unable to get info from remote repository.
+
+Possible Solutions:
+- Make sure you are connected to the internet.
+- If you are using the Flatpak version of Pixelorama, you need to grant it permission to connect to the internet. To do that, you can run the following command on your terminal:"
+autowrap_mode = 2
+
+[node name="HBoxContainer" type="HBoxContainer" parent="Error/Content"]
+layout_mode = 2
+
+[node name="LineEdit" type="LineEdit" parent="Error/Content/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "sudo flatpak override com.orama_interactive.Pixelorama --share=network"
+editable = false
+
+[node name="CopyCommand" type="Button" parent="Error/Content/HBoxContainer"]
+layout_mode = 2
+text = "Copy"
+
+[node name="Label2" type="Label" parent="Error/Content"]
+custom_minimum_size = Vector2(495, 50)
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Alternatively, you download Flatseal and set permissions for Flatpak apps there."
+autowrap_mode = 3
+
+[node name="ErrorCustom" type="AcceptDialog" parent="."]
+unique_name_in_owner = true
+position = Vector2i(0, 36)
+size = Vector2i(357, 110)
+popup_window = true
+
+[node name="Content" type="VBoxContainer" parent="ErrorCustom"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 8.0
+offset_top = 8.0
+offset_right = -8.0
+offset_bottom = -49.0
+
+[node name="Label" type="Label" parent="ErrorCustom/Content"]
+layout_mode = 2
+text = "Unable to get info from remote repository."
+
+[node name="FaultyLinks" type="Label" parent="ErrorCustom/Content"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[connection signal="about_to_popup" from="." to="." method="_on_Store_about_to_show"]
+[connection signal="close_requested" from="." to="." method="_on_close_requested"]
+[connection signal="text_changed" from="TabContainer/Store/StoreMainContainer/Parameters/SearchManager" to="TabContainer/Store/StoreMainContainer/Parameters/SearchManager" method="_on_SearchManager_text_changed"]
+[connection signal="visibility_changed" from="TabContainer/Options" to="TabContainer/Options/ScrollContainer/CustomStoreLinks" method="_on_Options_visibility_changed"]
+[connection signal="pressed" from="TabContainer/Options/ScrollContainer/CustomStoreLinks/ButtonContainer/Guide" to="TabContainer/Options/ScrollContainer/CustomStoreLinks" method="_on_Guide_pressed"]
+[connection signal="pressed" from="TabContainer/Options/ScrollContainer/CustomStoreLinks/ButtonContainer/NewLink" to="TabContainer/Options/ScrollContainer/CustomStoreLinks" method="_on_NewLink_pressed"]
+[connection signal="timeout" from="ProgressContainer/UpdateTimer" to="." method="_on_UpdateTimer_timeout"]
+[connection signal="request_completed" from="StoreInformationDownloader" to="." method="_on_StoreInformation_request_completed"]
+[connection signal="pressed" from="Error/Content/HBoxContainer/CopyCommand" to="." method="_on_CopyCommand_pressed"]
diff --git a/src/UI/ExtensionExplorer/Subscripts/CustomStoreLinks.gd b/src/UI/ExtensionExplorer/Subscripts/CustomStoreLinks.gd
new file mode 100644
index 000000000..55f7c5749
--- /dev/null
+++ b/src/UI/ExtensionExplorer/Subscripts/CustomStoreLinks.gd
@@ -0,0 +1,47 @@
+extends VBoxContainer
+
+var custom_links := []
+
+
+func _ready() -> void:
+ custom_links = Global.config_cache.get_value("ExtensionExplorer", "custom_links", [])
+ for link in custom_links:
+ add_field(link)
+
+
+func update_links() -> void:
+ custom_links.clear()
+ for child in $Links.get_children():
+ if child.text != "":
+ custom_links.append(child.text)
+ Global.config_cache.set_value("ExtensionExplorer", "custom_links", custom_links)
+
+
+func _on_NewLink_pressed() -> void:
+ add_field()
+
+
+func add_field(link := "") -> void:
+ var link_field := LineEdit.new()
+ # gdlint: ignore=max-line-length
+ link_field.placeholder_text = "Paste Store link, given by the store owner (will automatically be removed if left empty)"
+ link_field.text = link
+ $Links.add_child(link_field)
+ link_field.text_changed.connect(field_text_changed)
+
+
+func field_text_changed(_text: String) -> void:
+ update_links()
+
+
+func _on_Options_visibility_changed() -> void:
+ for child in $Links.get_children():
+ if child.text == "":
+ child.queue_free()
+
+
+# Uncomment it when we have a proper guide for writing a store_info file
+func _on_Guide_pressed() -> void:
+ pass
+# gdlint: ignore=max-line-length
+# OS.shell_open("https://github.com/Variable-Interactive/Variable-Store/tree/master#rules-for-writing-a-store_info-file")
diff --git a/src/UI/ExtensionExplorer/Subscripts/SearchManager.gd b/src/UI/ExtensionExplorer/Subscripts/SearchManager.gd
new file mode 100644
index 000000000..ca7027829
--- /dev/null
+++ b/src/UI/ExtensionExplorer/Subscripts/SearchManager.gd
@@ -0,0 +1,50 @@
+extends LineEdit
+
+var available_tags := PackedStringArray()
+@onready var tag_list: VBoxContainer = $"%TagList"
+
+
+func _on_SearchManager_text_changed(_new_text: String) -> void:
+ tag_text_search()
+
+
+func tag_text_search() -> void:
+ var result := text_search(text)
+ var tags := PackedStringArray([])
+ for tag: Button in tag_list.get_children():
+ if tag.button_pressed:
+ tags.append(tag.text)
+
+ for entry in result:
+ if !entry.tags_match(tags):
+ entry.visible = false
+
+
+func text_search(text_to_search: String) -> Array[ExtensionEntry]:
+ var result: Array[ExtensionEntry] = []
+ for entry: ExtensionEntry in $"%Content".get_children():
+ var visibility := true
+ if text_to_search != "":
+ var extension_name := entry.ext_name.text.to_lower()
+ var extension_description := entry.ext_discription.text.to_lower()
+ if not text_to_search.to_lower() in extension_name:
+ if not text_to_search.to_lower() in extension_description:
+ visibility = false
+ if visibility == true:
+ result.append(entry)
+ entry.visible = visibility
+ return result
+
+
+func add_new_tags(tag_array: PackedStringArray) -> void:
+ for tag in tag_array:
+ if !tag in available_tags:
+ available_tags.append(tag)
+ var tag_checkbox := CheckBox.new()
+ tag_checkbox.text = tag
+ tag_list.add_child(tag_checkbox)
+ tag_checkbox.toggled.connect(start_tag_search)
+
+
+func start_tag_search(_button_pressed: bool) -> void:
+ tag_text_search()
diff --git a/src/UI/ExtensionExplorer/store_info.md b/src/UI/ExtensionExplorer/store_info.md
new file mode 100644
index 000000000..bb0354595
--- /dev/null
+++ b/src/UI/ExtensionExplorer/store_info.md
@@ -0,0 +1,27 @@
+// This file is for online use.
+
+## Rules for writing a (store_info) file:
+// 1. The Store Entry is one large Array (referred to as "entry") consisting of sub-arrays (referred to as "data")
+// e.g `[[keyword, ....], [keyword, ....], [keyword, ....], .......]`
+// 2. Each data must have a keyword of type `String` at it's first index which helps in identifying what the data represents.
+// e.g, ["name", "name of extension"] is the data giving information about "name".
+// Valid keywords are `name`, `version`, `description`, `tags`, `thumbnail`, `download_link`
+// Put quotation marks ("") to make it a string, otherwise error will occur.
+// 3. One store entry must occupy only one line (and vice-versa).
+// 4. Comments are supported. you can comment an entire line by placing `#` or `//` at the start of the line (comments between or at end of line are not allowed).
+// 5. links to another store_info file can be placed inside another store_info file (it will get detected as a custom store file).
+
+## TIPS:
+// - `thumbnail` is the link you get by right clicking an image (uploaded somewhere on the internet) and selecting Copy Image Link.
+// - `download_link` is ususlly od the form `{repo}/raw/{Path of extension within repo}`
+// e.g, if `https://github.com/Variable-ind/Pixelorama-Extensions/blob/master/Extensions/Example.pck` is the URL path to your extension then replace "blob" with "raw"
+// and the link becomes `"https://github.com/Variable-ind/Pixelorama-Extensions/raw/master/Extensions/Example.pck"`
+
+// For further help see the entries below for reference of how it's done
+## Entries:
+// Put Official Extensions Here
+
+
+## Other Store Links:
+### VariableStore
+https://raw.githubusercontent.com/Variable-ind/Pixelorama-Extensions/4.0/store_info.md
diff --git a/src/UI/PerspectiveEditor/PerspectiveLine.gd b/src/UI/PerspectiveEditor/PerspectiveLine.gd
index a77cd97da..6e3ff00dc 100644
--- a/src/UI/PerspectiveEditor/PerspectiveLine.gd
+++ b/src/UI/PerspectiveEditor/PerspectiveLine.gd
@@ -29,9 +29,11 @@ func deserialize(data: Dictionary):
func initiate(data: Dictionary, vanishing_point: Node):
_vanishing_point = vanishing_point
- width = LINE_WIDTH / Global.camera.zoom.x
Global.canvas.add_child(self)
deserialize(data)
+ # a small delay is needed for Global.camera.zoom to have correct value
+ await get_tree().process_frame
+ width = LINE_WIDTH / Global.camera.zoom.x
refresh()
@@ -63,7 +65,7 @@ func _input(event: InputEvent) -> void:
var project_size := Global.current_project.size
if track_mouse:
- if !Global.can_draw or !Global.has_focus or Global.perspective_editor.tracker_disabled:
+ if !Global.can_draw or Global.perspective_editor.tracker_disabled:
hide_perspective_line()
return
default_color.a = 0.5
@@ -92,7 +94,7 @@ func try_rotate_scale():
var test_line := (points[1] - points[0]).rotated(deg_to_rad(90)).normalized()
var from_a := mouse_point - test_line * CIRCLE_RAD * 2 / Global.camera.zoom.x
var from_b := mouse_point + test_line * CIRCLE_RAD * 2 / Global.camera.zoom.x
- if Input.is_action_just_pressed("left_mouse") and Global.can_draw and Global.has_focus:
+ if Input.is_action_just_pressed("left_mouse") and Global.can_draw:
if (
Geometry2D.segment_intersects_segment(from_a, from_b, points[0], points[1])
or mouse_point.distance_to(points[1]) < CIRCLE_RAD * 2 / Global.camera.zoom.x
@@ -126,6 +128,7 @@ func try_rotate_scale():
func _draw() -> void:
+ width = LINE_WIDTH / Global.camera.zoom.x
var mouse_point := Global.canvas.current_pixel
var arc_points := PackedVector2Array()
draw_circle(points[0], CIRCLE_RAD / Global.camera.zoom.x, default_color) # Starting circle
@@ -150,8 +153,9 @@ func _draw() -> void:
arc_points.append(points[1])
for point in arc_points:
- draw_arc(point, CIRCLE_RAD * 2 / Global.camera.zoom.x, 0, 360, 360, default_color, 0.5)
+ # if we put width <= -1, then the arc line will automatically adjust itself to remain thin
+ # in 0.x this behavior was achieved at width <= 1
+ draw_arc(point, CIRCLE_RAD * 2 / Global.camera.zoom.x, 0, 360, 360, default_color)
- width = LINE_WIDTH / Global.camera.zoom.x
if is_hidden: # Hidden line
return
diff --git a/src/UI/PerspectiveEditor/VanishingPoint.gd b/src/UI/PerspectiveEditor/VanishingPoint.gd
index 8d63e5fe3..15b9ba51a 100644
--- a/src/UI/PerspectiveEditor/VanishingPoint.gd
+++ b/src/UI/PerspectiveEditor/VanishingPoint.gd
@@ -75,8 +75,7 @@ func _input(_event: InputEvent):
if (
Input.is_action_just_pressed("left_mouse")
and Global.can_draw
- and Global.has_focus
- and mouse_point.distance_to(start) < Global.camera.zoom.x * 8
+ and mouse_point.distance_to(start) < 8 / Global.camera.zoom.x
):
if (
!Rect2(Vector2.ZERO, project_size).has_point(Global.canvas.current_pixel)
diff --git a/src/UI/Recorder/Recorder.gd b/src/UI/Recorder/Recorder.gd
index f1e555e01..34d9c996b 100644
--- a/src/UI/Recorder/Recorder.gd
+++ b/src/UI/Recorder/Recorder.gd
@@ -13,13 +13,15 @@ var frame_captured := 0 ## Used to visualize frames captured
var skip_amount := 1 ## Number of "do" actions after which a frame can be captured
var current_frame_no := 0 ## Used to compare with skip_amount to see if it can be captured
-var resize := 100
+var resize_percent := 100
+@onready var captured_label := %CapturedLabel as Label
@onready var project_list := $"%TargetProjectOption" as OptionButton
-@onready var folder_button := $"%Folder" as Button
@onready var start_button := $"%Start" as Button
@onready var size_label := $"%Size" as Label
@onready var path_field := $"%Path" as LineEdit
+@onready var options_dialog := $Dialogs/OptionsDialog as AcceptDialog
+@onready var options_container := %OptionsContainer as VBoxContainer
func _ready() -> void:
@@ -40,10 +42,9 @@ func initialize_recording() -> void:
current_frame_no = skip_amount - 1
# disable some options that are not required during recording
- folder_button.visible = true
project_list.visible = false
- $ScrollContainer/CenterContainer/GridContainer/Captured.visible = true
- for child in $Dialogs/Options/PanelContainer/VBoxContainer.get_children():
+ captured_label.visible = true
+ for child in options_container.get_children():
if !child.is_in_group("visible during recording"):
child.visible = false
@@ -77,11 +78,10 @@ func capture_frame() -> void:
DrawingAlgos.blend_layers(image, frame, Vector2i.ZERO, project)
if mode == Mode.CANVAS:
- if resize != 100:
+ if resize_percent != 100:
+ var resize := resize_percent / 100
image.resize(
- image.get_size().x * resize / 100,
- image.get_size().y * resize / 100,
- Image.INTERPOLATE_NEAREST
+ image.get_width() * resize, image.get_height() * resize, Image.INTERPOLATE_NEAREST
)
cache.append(image)
@@ -102,7 +102,7 @@ func save_frame(img: Image) -> void:
func _on_frame_saved() -> void:
frame_captured += 1
- $ScrollContainer/CenterContainer/GridContainer/Captured.text = str("Saved: ", frame_captured)
+ captured_label.text = str("Saved: ", frame_captured)
func finalize_recording() -> void:
@@ -111,10 +111,9 @@ func finalize_recording() -> void:
save_frame(img)
cache.clear()
disconnect_undo()
- folder_button.visible = false
project_list.visible = true
- $ScrollContainer/CenterContainer/GridContainer/Captured.visible = false
- for child in $Dialogs/Options/PanelContainer/VBoxContainer.get_children():
+ captured_label.visible = false
+ for child in options_container.get_children():
child.visible = true
if mode == Mode.PIXELORAMA:
size_label.get_parent().visible = false
@@ -152,9 +151,7 @@ func _on_Start_toggled(button_pressed: bool) -> void:
func _on_Settings_pressed() -> void:
- var settings := $Dialogs/Options as Window
- var pos := position
- settings.popup(Rect2(pos, settings.size))
+ options_dialog.popup(Rect2(position, options_dialog.size))
func _on_SkipAmount_value_changed(value: float) -> void:
@@ -171,8 +168,8 @@ func _on_Mode_toggled(button_pressed: bool) -> void:
func _on_SpinBox_value_changed(value: float) -> void:
- resize = value
- var new_size: Vector2 = project.size * (resize / 100.0)
+ resize_percent = value
+ var new_size: Vector2 = project.size * (resize_percent / 100.0)
size_label.text = str("(", new_size.x, "×", new_size.y, ")")
@@ -181,7 +178,7 @@ func _on_Choose_pressed() -> void:
$Dialogs/Path.current_dir = chosen_dir
-func _on_Open_pressed() -> void:
+func _on_open_folder_pressed() -> void:
OS.shell_open(path_field.text)
@@ -189,13 +186,3 @@ func _on_Path_dir_selected(dir: String) -> void:
chosen_dir = dir
path_field.text = chosen_dir
start_button.disabled = false
-
-
-func _on_Fps_value_changed(value: float) -> void:
- var dur_label := $Dialogs/Options/PanelContainer/VBoxContainer/Fps/Duration as Label
- var duration := snappedf(1.0 / value, 0.0001)
- dur_label.text = str("= ", duration, " sec")
-
-
-func _on_options_close_requested() -> void:
- $Dialogs/Options.hide()
diff --git a/src/UI/Recorder/Recorder.tscn b/src/UI/Recorder/Recorder.tscn
index fb5af105d..f75557fc8 100644
--- a/src/UI/Recorder/Recorder.tscn
+++ b/src/UI/Recorder/Recorder.tscn
@@ -26,7 +26,8 @@ layout_mode = 2
size_flags_vertical = 0
columns = 4
-[node name="Captured" type="Label" parent="ScrollContainer/CenterContainer/GridContainer"]
+[node name="CapturedLabel" type="Label" parent="ScrollContainer/CenterContainer/GridContainer"]
+unique_name_in_owner = true
visible = false
layout_mode = 2
@@ -80,16 +81,14 @@ offset_bottom = 10.5
texture = ExtResource("3")
stretch_mode = 6
-[node name="Folder" type="Button" parent="ScrollContainer/CenterContainer/GridContainer"]
-unique_name_in_owner = true
-visible = false
+[node name="OpenFolder" type="Button" parent="ScrollContainer/CenterContainer/GridContainer"]
custom_minimum_size = Vector2(32, 32)
layout_mode = 2
tooltip_text = "Open Folder"
mouse_default_cursor_shape = 2
toggle_mode = true
-[node name="TextureRect" type="TextureRect" parent="ScrollContainer/CenterContainer/GridContainer/Folder"]
+[node name="TextureRect" type="TextureRect" parent="ScrollContainer/CenterContainer/GridContainer/OpenFolder"]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
@@ -104,110 +103,95 @@ stretch_mode = 6
layout_mode = 2
mouse_filter = 2
-[node name="Options" type="Window" parent="Dialogs"]
+[node name="OptionsDialog" type="AcceptDialog" parent="Dialogs"]
+position = Vector2i(0, 36)
size = Vector2i(400, 300)
-visible = false
+exclusive = false
+popup_window = true
-[node name="PanelContainer" type="PanelContainer" parent="Dialogs/Options"]
+[node name="PanelContainer" type="MarginContainer" parent="Dialogs/OptionsDialog"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
-offset_left = 9.0
-offset_top = 9.0
-offset_right = -9.0
-offset_bottom = -9.0
+offset_left = 8.0
+offset_top = 8.0
+offset_right = -8.0
+offset_bottom = -49.0
-[node name="VBoxContainer" type="VBoxContainer" parent="Dialogs/Options/PanelContainer"]
+[node name="OptionsContainer" type="VBoxContainer" parent="Dialogs/OptionsDialog/PanelContainer"]
+unique_name_in_owner = true
layout_mode = 2
-[node name="IntervalHeader" type="HBoxContainer" parent="Dialogs/Options/PanelContainer/VBoxContainer"]
+[node name="IntervalHeader" type="HBoxContainer" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer"]
layout_mode = 2
theme_override_constants/separation = 0
-[node name="Label" type="Label" parent="Dialogs/Options/PanelContainer/VBoxContainer/IntervalHeader"]
+[node name="Label" type="Label" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/IntervalHeader"]
layout_mode = 2
theme_type_variation = &"HeaderSmall"
text = "Interval"
-[node name="HSeparator" type="HSeparator" parent="Dialogs/Options/PanelContainer/VBoxContainer/IntervalHeader"]
+[node name="HSeparator" type="HSeparator" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/IntervalHeader"]
layout_mode = 2
size_flags_horizontal = 3
-[node name="ActionGap" type="HBoxContainer" parent="Dialogs/Options/PanelContainer/VBoxContainer"]
+[node name="ActionGap" type="HBoxContainer" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer"]
layout_mode = 2
alignment = 1
-[node name="Label" type="Label" parent="Dialogs/Options/PanelContainer/VBoxContainer/ActionGap"]
+[node name="Label" type="Label" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/ActionGap"]
layout_mode = 2
+size_flags_horizontal = 3
text = "Capture frame every"
-[node name="SkipAmount" type="SpinBox" parent="Dialogs/Options/PanelContainer/VBoxContainer/ActionGap"]
+[node name="SkipAmount" type="SpinBox" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/ActionGap"]
layout_mode = 2
+size_flags_horizontal = 3
+suffix = "actions"
-[node name="Label2" type="Label" parent="Dialogs/Options/PanelContainer/VBoxContainer/ActionGap"]
-layout_mode = 2
-text = "Actions"
-
-[node name="Fps" type="HBoxContainer" parent="Dialogs/Options/PanelContainer/VBoxContainer"]
-layout_mode = 2
-alignment = 1
-
-[node name="Label" type="Label" parent="Dialogs/Options/PanelContainer/VBoxContainer/Fps"]
-layout_mode = 2
-text = "Fps:"
-
-[node name="Fps" type="SpinBox" parent="Dialogs/Options/PanelContainer/VBoxContainer/Fps"]
-layout_mode = 2
-min_value = 1.0
-value = 30.0
-allow_greater = true
-
-[node name="Duration" type="Label" parent="Dialogs/Options/PanelContainer/VBoxContainer/Fps"]
-layout_mode = 2
-text = "= 0.0333 sec"
-
-[node name="ModeHeader" type="HBoxContainer" parent="Dialogs/Options/PanelContainer/VBoxContainer" groups=["visible during recording"]]
+[node name="ModeHeader" type="HBoxContainer" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer" groups=["visible during recording"]]
layout_mode = 2
theme_override_constants/separation = 0
-[node name="Label" type="Label" parent="Dialogs/Options/PanelContainer/VBoxContainer/ModeHeader"]
+[node name="Label" type="Label" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/ModeHeader"]
layout_mode = 2
theme_type_variation = &"HeaderSmall"
text = "Mode"
-[node name="HSeparator" type="HSeparator" parent="Dialogs/Options/PanelContainer/VBoxContainer/ModeHeader"]
+[node name="HSeparator" type="HSeparator" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/ModeHeader"]
layout_mode = 2
size_flags_horizontal = 3
-[node name="ModeType" type="HBoxContainer" parent="Dialogs/Options/PanelContainer/VBoxContainer" groups=["visible during recording"]]
+[node name="ModeType" type="HBoxContainer" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer" groups=["visible during recording"]]
layout_mode = 2
alignment = 1
-[node name="Label" type="Label" parent="Dialogs/Options/PanelContainer/VBoxContainer/ModeType"]
+[node name="Label" type="Label" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/ModeType"]
layout_mode = 2
-text = "Canvas Only"
+size_flags_horizontal = 3
+text = "Record canvas only"
-[node name="Mode" type="CheckButton" parent="Dialogs/Options/PanelContainer/VBoxContainer/ModeType"]
+[node name="Mode" type="CheckButton" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/ModeType"]
layout_mode = 2
+size_flags_horizontal = 3
+mouse_default_cursor_shape = 2
-[node name="Label2" type="Label" parent="Dialogs/Options/PanelContainer/VBoxContainer/ModeType"]
-layout_mode = 2
-text = "Pixelorama"
-
-[node name="OutputScale" type="HBoxContainer" parent="Dialogs/Options/PanelContainer/VBoxContainer"]
+[node name="OutputScale" type="HBoxContainer" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer"]
layout_mode = 2
alignment = 1
-[node name="Label" type="Label" parent="Dialogs/Options/PanelContainer/VBoxContainer/OutputScale"]
+[node name="Label" type="Label" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/OutputScale"]
layout_mode = 2
+size_flags_horizontal = 3
text = "Output Scale:"
-[node name="Size" type="Label" parent="Dialogs/Options/PanelContainer/VBoxContainer/OutputScale"]
+[node name="Size" type="Label" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/OutputScale"]
unique_name_in_owner = true
layout_mode = 2
-[node name="Resize" type="SpinBox" parent="Dialogs/Options/PanelContainer/VBoxContainer/OutputScale"]
+[node name="Resize" type="SpinBox" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/OutputScale"]
layout_mode = 2
+size_flags_horizontal = 3
mouse_default_cursor_shape = 2
min_value = 50.0
max_value = 1000.0
@@ -216,49 +200,34 @@ value = 100.0
allow_greater = true
suffix = "%"
-[node name="PathHeader" type="HBoxContainer" parent="Dialogs/Options/PanelContainer/VBoxContainer"]
+[node name="PathHeader" type="HBoxContainer" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer"]
layout_mode = 2
theme_override_constants/separation = 0
-[node name="Label" type="Label" parent="Dialogs/Options/PanelContainer/VBoxContainer/PathHeader"]
+[node name="Label" type="Label" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/PathHeader"]
layout_mode = 2
theme_type_variation = &"HeaderSmall"
text = "Path"
-[node name="HSeparator" type="HSeparator" parent="Dialogs/Options/PanelContainer/VBoxContainer/PathHeader"]
+[node name="HSeparator" type="HSeparator" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/PathHeader"]
layout_mode = 2
size_flags_horizontal = 3
-[node name="PathContainer" type="HBoxContainer" parent="Dialogs/Options/PanelContainer/VBoxContainer"]
+[node name="PathContainer" type="HBoxContainer" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer"]
layout_mode = 2
-[node name="Path" type="LineEdit" parent="Dialogs/Options/PanelContainer/VBoxContainer/PathContainer"]
+[node name="Path" type="LineEdit" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/PathContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
-placeholder_text = "Choose Destination --->"
+placeholder_text = "Choose destination"
editable = false
-[node name="Open" type="Button" parent="Dialogs/Options/PanelContainer/VBoxContainer/PathContainer"]
-custom_minimum_size = Vector2(25, 25)
-layout_mode = 2
-
-[node name="TextureRect" type="TextureRect" parent="Dialogs/Options/PanelContainer/VBoxContainer/PathContainer/Open"]
-layout_mode = 0
-anchor_right = 1.0
-anchor_bottom = 1.0
-offset_left = 2.0
-offset_top = 2.0
-offset_right = -2.0
-offset_bottom = -2.0
-texture = ExtResource("4")
-stretch_mode = 6
-
-[node name="Choose" type="Button" parent="Dialogs/Options/PanelContainer/VBoxContainer/PathContainer"]
+[node name="Choose" type="Button" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/PathContainer"]
layout_mode = 2
text = "Choose"
-[node name="Path" type="FileDialog" parent="Dialogs"]
+[node name="Path" type="FileDialog" parent="Dialogs" groups=["FileDialogs"]]
mode = 2
exclusive = false
popup_window = true
@@ -270,13 +239,10 @@ access = 2
[connection signal="pressed" from="ScrollContainer/CenterContainer/GridContainer/TargetProjectOption" to="." method="_on_TargetProjectOption_pressed"]
[connection signal="toggled" from="ScrollContainer/CenterContainer/GridContainer/Start" to="." method="_on_Start_toggled"]
[connection signal="pressed" from="ScrollContainer/CenterContainer/GridContainer/Settings" to="." method="_on_Settings_pressed"]
-[connection signal="pressed" from="ScrollContainer/CenterContainer/GridContainer/Folder" to="." method="_on_Open_pressed"]
-[connection signal="close_requested" from="Dialogs/Options" to="." method="_on_options_close_requested"]
-[connection signal="value_changed" from="Dialogs/Options/PanelContainer/VBoxContainer/ActionGap/SkipAmount" to="." method="_on_SkipAmount_value_changed"]
-[connection signal="value_changed" from="Dialogs/Options/PanelContainer/VBoxContainer/Fps/Fps" to="." method="_on_Fps_value_changed"]
-[connection signal="toggled" from="Dialogs/Options/PanelContainer/VBoxContainer/ModeType/Mode" to="." method="_on_Mode_toggled"]
-[connection signal="value_changed" from="Dialogs/Options/PanelContainer/VBoxContainer/OutputScale/Resize" to="." method="_on_SpinBox_value_changed"]
-[connection signal="pressed" from="Dialogs/Options/PanelContainer/VBoxContainer/PathContainer/Open" to="." method="_on_Open_pressed"]
-[connection signal="pressed" from="Dialogs/Options/PanelContainer/VBoxContainer/PathContainer/Choose" to="." method="_on_Choose_pressed"]
+[connection signal="pressed" from="ScrollContainer/CenterContainer/GridContainer/OpenFolder" to="." method="_on_open_folder_pressed"]
+[connection signal="value_changed" from="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/ActionGap/SkipAmount" to="." method="_on_SkipAmount_value_changed"]
+[connection signal="toggled" from="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/ModeType/Mode" to="." method="_on_Mode_toggled"]
+[connection signal="value_changed" from="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/OutputScale/Resize" to="." method="_on_SpinBox_value_changed"]
+[connection signal="pressed" from="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/PathContainer/Choose" to="." method="_on_Choose_pressed"]
[connection signal="dir_selected" from="Dialogs/Path" to="." method="_on_Path_dir_selected"]
[connection signal="timeout" from="Timer" to="." method="_on_Timer_timeout"]
diff --git a/src/UI/Timeline/AnimationTimeline.tscn b/src/UI/Timeline/AnimationTimeline.tscn
index 2271ccc0f..6c0ecd273 100644
--- a/src/UI/Timeline/AnimationTimeline.tscn
+++ b/src/UI/Timeline/AnimationTimeline.tscn
@@ -953,7 +953,7 @@ mouse_filter = 2
color = Color(0, 0.741176, 1, 0.501961)
[node name="PasteTagPopup" type="Popup" parent="."]
-size = Vector2i(250, 574)
+size = Vector2i(250, 335)
min_size = Vector2i(250, 0)
script = ExtResource("12")
@@ -992,6 +992,7 @@ layout_mode = 2
text = "Create new tags"
[node name="Instructions" type="Label" parent="PasteTagPopup/PanelContainer/VBoxContainer"]
+custom_minimum_size = Vector2(250, 23)
layout_mode = 2
text = "Available tags:"
autowrap_mode = 3
@@ -1004,6 +1005,7 @@ size_flags_vertical = 3
[node name="StartFrame" type="Label" parent="PasteTagPopup/PanelContainer/VBoxContainer"]
unique_name_in_owner = true
+custom_minimum_size = Vector2(250, 23)
layout_mode = 2
horizontal_alignment = 1
autowrap_mode = 3
diff --git a/src/UI/ToolsPanel/ToolButtons.gd b/src/UI/ToolsPanel/ToolButtons.gd
index f1b794898..200e4e631 100644
--- a/src/UI/ToolsPanel/ToolButtons.gd
+++ b/src/UI/ToolsPanel/ToolButtons.gd
@@ -3,11 +3,17 @@ extends FlowContainer
var pen_inverted := false
+func _ready() -> void:
+ # Ensure to only call _input() if the cursor is inside the main canvas viewport
+ Global.main_viewport.mouse_entered.connect(set_process_input.bind(true))
+ Global.main_viewport.mouse_exited.connect(set_process_input.bind(false))
+
+
func _input(event: InputEvent) -> void:
if event is InputEventMouseMotion:
pen_inverted = event.pen_inverted
return
- if not Global.has_focus or not Global.can_draw:
+ if not Global.can_draw:
return
for action in ["undo", "redo"]:
if event.is_action_pressed(action):
diff --git a/src/UI/TopMenuContainer/TopMenuContainer.gd b/src/UI/TopMenuContainer/TopMenuContainer.gd
index dc1dbf81a..25687793a 100644
--- a/src/UI/TopMenuContainer/TopMenuContainer.gd
+++ b/src/UI/TopMenuContainer/TopMenuContainer.gd
@@ -398,8 +398,6 @@ func _popup_dialog(dialog: Window, dialog_size := Vector2i.ZERO) -> void:
func file_menu_id_pressed(id: int) -> void:
- if not Global.can_draw:
- return
match id:
Global.FileMenu.NEW:
_on_new_project_file_menu_option_pressed()
@@ -464,8 +462,6 @@ func _on_recent_projects_submenu_id_pressed(id: int) -> void:
func edit_menu_id_pressed(id: int) -> void:
- if not Global.can_draw:
- return
match id:
Global.EditMenu.UNDO:
Global.current_project.commit_undo()
@@ -490,8 +486,6 @@ func edit_menu_id_pressed(id: int) -> void:
func view_menu_id_pressed(id: int) -> void:
- if not Global.can_draw:
- return
match id:
Global.ViewMenu.TILE_MODE_OFFSETS:
_popup_dialog(Global.control.get_node("Dialogs/TileModeOffsetsDialog"))
@@ -518,8 +512,6 @@ func view_menu_id_pressed(id: int) -> void:
func window_menu_id_pressed(id: int) -> void:
- if not Global.can_draw:
- return
match id:
Global.WindowMenu.WINDOW_OPACITY:
_popup_dialog(window_opacity_dialog)
@@ -703,8 +695,6 @@ func _toggle_fullscreen() -> void:
func image_menu_id_pressed(id: int) -> void:
- if not Global.can_draw:
- return
match id:
Global.ImageMenu.SCALE_IMAGE:
_popup_dialog(Global.control.get_node("Dialogs/ImageEffects/ScaleImage"))
@@ -744,8 +734,6 @@ func image_menu_id_pressed(id: int) -> void:
func select_menu_id_pressed(id: int) -> void:
- if not Global.can_draw:
- return
match id:
Global.SelectMenu.SELECT_ALL:
Global.canvas.selection.select_all()
@@ -762,8 +750,6 @@ func select_menu_id_pressed(id: int) -> void:
func help_menu_id_pressed(id: int) -> void:
- if not Global.can_draw:
- return
match id:
Global.HelpMenu.VIEW_SPLASH_SCREEN:
_popup_dialog(Global.control.get_node("Dialogs/SplashDialog"))
diff --git a/src/UI/ViewportContainer.gd b/src/UI/ViewportContainer.gd
index d1822f873..1110ef470 100644
--- a/src/UI/ViewportContainer.gd
+++ b/src/UI/ViewportContainer.gd
@@ -12,13 +12,11 @@ func _ready() -> void:
func _on_ViewportContainer_mouse_entered() -> void:
camera.set_process_input(true)
- Global.has_focus = true
Global.control.left_cursor.visible = Global.show_left_tool_icon
Global.control.right_cursor.visible = Global.show_right_tool_icon
func _on_ViewportContainer_mouse_exited() -> void:
camera.set_process_input(false)
- Global.has_focus = false
Global.control.left_cursor.visible = false
Global.control.right_cursor.visible = false