mirror of
https://github.com/Orama-Interactive/Pixelorama.git
synced 2025-01-18 09:09:47 +00:00
Compare commits
30 commits
cf6bf9ecb1
...
4c316c2ff5
Author | SHA1 | Date | |
---|---|---|---|
4c316c2ff5 | |||
b126e95b64 | |||
3d04a8d276 | |||
964e9fbd26 | |||
de5db85345 | |||
f8b32762a1 | |||
56fe1840e0 | |||
3a0977ce21 | |||
ce7a5e77ba | |||
f0a5637d8a | |||
5297fe6a80 | |||
d640b6a979 | |||
4bc0fba941 | |||
b08420d09d | |||
4e9b657077 | |||
42de5ccb29 | |||
d9c0cd7546 | |||
3a852e44ff | |||
43d241a5c2 | |||
204eff8184 | |||
e7a469fa4d | |||
6448b7ee7c | |||
519fa77791 | |||
f43f80cee0 | |||
ddce0393dd | |||
bec7ceb974 | |||
561a374cc0 | |||
bc8fadb67c | |||
73e40ed127 | |||
aa77dcb61f |
|
@ -743,6 +743,22 @@ msgstr ""
|
||||||
msgid "Dim interface on dialog popup"
|
msgid "Dim interface on dialog popup"
|
||||||
msgstr ""
|
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"
|
msgid "Dark"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -2305,6 +2321,10 @@ msgstr ""
|
||||||
msgid "Quit confirmation"
|
msgid "Quit confirmation"
|
||||||
msgstr ""
|
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"
|
msgid "Enable autosave"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ func blend_layers(
|
||||||
# the second are the opacities and the third are the origins
|
# 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 metadata_image := Image.create(project.layers.size(), 3, false, Image.FORMAT_R8)
|
||||||
var frame_index := project.frames.find(frame)
|
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)
|
project.order_layers(frame_index)
|
||||||
for i in project.layers.size():
|
for i in project.layers.size():
|
||||||
var ordered_index := project.ordered_layers[i]
|
var ordered_index := project.ordered_layers[i]
|
||||||
|
@ -53,7 +53,7 @@ func blend_layers(
|
||||||
gen.generate_image(blended, blend_layers_shader, params, project.size)
|
gen.generate_image(blended, blend_layers_shader, params, project.size)
|
||||||
image.blend_rect(blended, Rect2i(Vector2i.ZERO, project.size), origin)
|
image.blend_rect(blended, Rect2i(Vector2i.ZERO, project.size), origin)
|
||||||
# Re-order the layers again to ensure correct canvas drawing
|
# 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
|
## 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:
|
func nn_rotate(sprite: Image, angle: float, pivot: Vector2) -> void:
|
||||||
|
if is_zero_approx(angle):
|
||||||
|
return
|
||||||
var aux := Image.new()
|
var aux := Image.new()
|
||||||
aux.copy_from(sprite)
|
aux.copy_from(sprite)
|
||||||
var ox: int
|
var ox: int
|
||||||
|
@ -456,37 +458,6 @@ func color_distance(c1: Color, c2: Color) -> float:
|
||||||
|
|
||||||
|
|
||||||
# Image effects
|
# 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:
|
func center(indices: Array) -> void:
|
||||||
var project := Global.current_project
|
var project := Global.current_project
|
||||||
Global.canvas.selection.transform_content_confirm()
|
Global.canvas.selection.transform_content_confirm()
|
||||||
|
@ -517,22 +488,56 @@ func center(indices: Array) -> void:
|
||||||
project.undo_redo.commit_action()
|
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.
|
## Sets the size of the project to be the same as the size of the active selection.
|
||||||
func crop_to_selection() -> void:
|
func crop_to_selection() -> void:
|
||||||
if not Global.current_project.has_selection:
|
if not Global.current_project.has_selection:
|
||||||
return
|
return
|
||||||
|
var redo_data := {}
|
||||||
|
var undo_data := {}
|
||||||
Global.canvas.selection.transform_content_confirm()
|
Global.canvas.selection.transform_content_confirm()
|
||||||
var rect: Rect2i = Global.canvas.selection.big_bounding_rectangle
|
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
|
# Loop through all the cels to crop them
|
||||||
for f in Global.current_project.frames:
|
for f in Global.current_project.frames:
|
||||||
for cel in f.cels:
|
for cel in f.cels:
|
||||||
if not cel is PixelCel:
|
if not cel is PixelCel:
|
||||||
continue
|
continue
|
||||||
var sprite := cel.get_image().get_region(rect)
|
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
|
## 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 width := used_rect.size.x
|
||||||
var height := used_rect.size.y
|
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
|
# Loop through all the cels to trim them
|
||||||
for f in Global.current_project.frames:
|
for f in Global.current_project.frames:
|
||||||
for cel in f.cels:
|
for cel in f.cels:
|
||||||
if not cel is PixelCel:
|
if not cel is PixelCel:
|
||||||
continue
|
continue
|
||||||
var sprite := cel.get_image().get_region(used_rect)
|
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:
|
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 f in Global.current_project.frames:
|
||||||
for cel in f.cels:
|
for cel in f.cels:
|
||||||
if not cel is PixelCel:
|
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),
|
Rect2i(Vector2i.ZERO, Global.current_project.size),
|
||||||
Vector2i(offset_x, offset_y)
|
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 project := Global.current_project
|
||||||
var size := Vector2i(width, height)
|
var size := Vector2i(width, height)
|
||||||
var x_ratio := float(project.size.x) / width
|
var x_ratio := float(project.size.x) / width
|
||||||
var y_ratio := float(project.size.y) / height
|
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_x_symmetry_point := project.x_symmetry_point / x_ratio
|
||||||
var new_y_symmetry_point := project.y_symmetry_point / y_ratio
|
var new_y_symmetry_point := project.y_symmetry_point / y_ratio
|
||||||
var new_x_symmetry_axis_points := project.x_symmetry_axis.points
|
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.undos += 1
|
||||||
project.undo_redo.create_action("Scale")
|
project.undo_redo.create_action("Scale")
|
||||||
project.undo_redo.add_do_property(project, "size", size)
|
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, "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, "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.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)
|
project.undo_redo.add_do_property(project.y_symmetry_axis, "points", new_y_symmetry_axis_points)
|
||||||
|
Global.undo_redo_compress_images(redo_data, undo_data)
|
||||||
|
|
||||||
func general_undo_scale() -> void:
|
|
||||||
var project := Global.current_project
|
|
||||||
project.undo_redo.add_undo_property(project, "size", project.size)
|
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, "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(project, "y_symmetry_point", project.y_symmetry_point)
|
||||||
project.undo_redo.add_undo_property(
|
project.undo_redo.add_undo_property(
|
||||||
|
|
|
@ -4,10 +4,24 @@ enum ExportTab { IMAGE = 0, SPRITESHEET = 1 }
|
||||||
enum Orientation { ROWS = 0, COLUMNS = 1 }
|
enum Orientation { ROWS = 0, COLUMNS = 1 }
|
||||||
enum AnimationDirection { FORWARD = 0, BACKWARDS = 1, PING_PONG = 2 }
|
enum AnimationDirection { FORWARD = 0, BACKWARDS = 1, PING_PONG = 2 }
|
||||||
## See file_format_string, file_format_description, and ExportDialog.gd
|
## 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
|
## 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)
|
## A dictionary of custom exporter generators (received from extensions)
|
||||||
var custom_file_formats := {}
|
var custom_file_formats := {}
|
||||||
|
@ -262,23 +276,28 @@ func export_processed_images(
|
||||||
return result
|
return result
|
||||||
|
|
||||||
if is_single_file_format(project):
|
if is_single_file_format(project):
|
||||||
var exporter: AImgIOBaseExporter
|
if is_using_ffmpeg(project.file_format):
|
||||||
if project.file_format == FileFormat.APNG:
|
var video_exported := export_video(export_paths)
|
||||||
exporter = AImgIOAPNGExporter.new()
|
if not video_exported:
|
||||||
|
return false
|
||||||
else:
|
else:
|
||||||
exporter = GIFAnimationExporter.new()
|
var exporter: AImgIOBaseExporter
|
||||||
var details := {
|
if project.file_format == FileFormat.APNG:
|
||||||
"exporter": exporter,
|
exporter = AImgIOAPNGExporter.new()
|
||||||
"export_dialog": export_dialog,
|
else:
|
||||||
"export_paths": export_paths,
|
exporter = GIFAnimationExporter.new()
|
||||||
"project": project
|
var details := {
|
||||||
}
|
"exporter": exporter,
|
||||||
if not _multithreading_enabled():
|
"export_dialog": export_dialog,
|
||||||
export_animated(details)
|
"export_paths": export_paths,
|
||||||
else:
|
"project": project
|
||||||
if gif_export_thread.is_started():
|
}
|
||||||
gif_export_thread.wait_to_finish()
|
if not _multithreading_enabled():
|
||||||
gif_export_thread.start(export_animated.bind(details))
|
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:
|
else:
|
||||||
var succeeded := true
|
var succeeded := true
|
||||||
for i in range(processed_images.size()):
|
for i in range(processed_images.size()):
|
||||||
|
@ -311,11 +330,9 @@ func export_processed_images(
|
||||||
elif project.file_format == FileFormat.JPEG:
|
elif project.file_format == FileFormat.JPEG:
|
||||||
err = processed_images[i].save_jpg(export_paths[i])
|
err = processed_images[i].save_jpg(export_paths[i])
|
||||||
if err != OK:
|
if err != OK:
|
||||||
Global.error_dialog.set_text(
|
Global.popup_error(
|
||||||
tr("File failed to save. Error code %s (%s)") % [err, error_string(err)]
|
tr("File failed to save. Error code %s (%s)") % [err, error_string(err)]
|
||||||
)
|
)
|
||||||
Global.error_dialog.popup_centered()
|
|
||||||
Global.dialog_open(true)
|
|
||||||
succeeded = false
|
succeeded = false
|
||||||
if succeeded:
|
if succeeded:
|
||||||
Global.notification_label("File(s) exported")
|
Global.notification_label("File(s) exported")
|
||||||
|
@ -334,6 +351,37 @@ func export_processed_images(
|
||||||
return true
|
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:
|
func export_animated(args: Dictionary) -> void:
|
||||||
var project: Project = args["project"]
|
var project: Project = args["project"]
|
||||||
var exporter: AImgIOBaseExporter = args["exporter"]
|
var exporter: AImgIOBaseExporter = args["exporter"]
|
||||||
|
@ -397,6 +445,16 @@ func file_format_string(format_enum: int) -> String:
|
||||||
return ".gif"
|
return ".gif"
|
||||||
FileFormat.APNG:
|
FileFormat.APNG:
|
||||||
return ".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 a file format description is not found, try generating one
|
||||||
if custom_exporter_generators.has(format_enum):
|
if custom_exporter_generators.has(format_enum):
|
||||||
|
@ -418,6 +476,16 @@ func file_format_description(format_enum: int) -> String:
|
||||||
return "GIF Image"
|
return "GIF Image"
|
||||||
FileFormat.APNG:
|
FileFormat.APNG:
|
||||||
return "APNG Image"
|
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
|
# If a file format description is not found, try generating one
|
||||||
for key in custom_file_formats.keys():
|
for key in custom_file_formats.keys():
|
||||||
|
@ -426,12 +494,25 @@ func file_format_description(format_enum: int) -> String:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
## True when exporting to .gif and .apng (and potentially video formats in the future)
|
## True when exporting to .gif, .apng and video
|
||||||
## False when exporting to .png, and other non-animated formats in the future
|
## False when exporting to .png, .jpg and static .webp
|
||||||
func is_single_file_format(project := Global.current_project) -> bool:
|
func is_single_file_format(project := Global.current_project) -> bool:
|
||||||
return animated_formats.has(project.file_format)
|
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:
|
func _create_export_path(multifile: bool, project: Project, frame := 0) -> String:
|
||||||
var path := project.file_name
|
var path := project.file_name
|
||||||
# Only append frame number when there are multiple files exported
|
# Only append frame number when there are multiple files exported
|
||||||
|
|
|
@ -218,9 +218,7 @@ class DialogAPI:
|
||||||
## Shows an alert dialog with the given [param text].
|
## Shows an alert dialog with the given [param text].
|
||||||
## Useful for displaying messages like "Incompatible API" etc...
|
## Useful for displaying messages like "Incompatible API" etc...
|
||||||
func show_error(text: String) -> void:
|
func show_error(text: String) -> void:
|
||||||
Global.error_dialog.set_text(text)
|
Global.popup_error(text)
|
||||||
Global.error_dialog.popup_centered()
|
|
||||||
Global.dialog_open(true)
|
|
||||||
|
|
||||||
## Returns the node that is the parent of dialogs used in pixelorama.
|
## Returns the node that is the parent of dialogs used in pixelorama.
|
||||||
func get_dialogs_parent_node() -> Node:
|
func get_dialogs_parent_node() -> Node:
|
||||||
|
|
|
@ -117,8 +117,6 @@ var current_project_index := 0:
|
||||||
var can_draw := false
|
var can_draw := false
|
||||||
## (Intended to be used as getter only) Tells if the user allowed to move the guide while on canvas.
|
## (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
|
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.
|
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.
|
## (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
|
var open_last_project := false
|
||||||
## Found in Preferences. If [code]true[/code], asks for permission to quit on exit.
|
## Found in Preferences. If [code]true[/code], asks for permission to quit on exit.
|
||||||
var quit_confirmation := false
|
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.
|
## Found in Preferences. If [code]true[/code], the zoom is smooth.
|
||||||
var smooth_zoom := true
|
var smooth_zoom := true
|
||||||
## Found in Preferences. If [code]true[/code], the zoom is restricted to integral multiples of 100%.
|
## 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.step = 1
|
||||||
zoom_slider.value = zoom_slider.value # to trigger signal emission
|
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
|
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:
|
var font_size := 16:
|
||||||
set(value):
|
set(value):
|
||||||
font_size = value
|
font_size = value
|
||||||
control.theme.default_font_size = value
|
control.theme.default_font_size = value
|
||||||
control.theme.set_font_size("font_size", "HeaderSmall", value + 2)
|
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
|
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.
|
## Found in Preferences. The modulation color (or simply color) of icons.
|
||||||
var modulate_icon_color := Color.GRAY
|
var modulate_icon_color := Color.GRAY
|
||||||
## Found in Preferences. Determines if [member modulate_icon_color] uses custom or theme color.
|
## 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))
|
data_directories.append(default_loc.path_join(HOME_SUBDIR_NAME))
|
||||||
if ProjectSettings.get_setting("display/window/tablet_driver") == "winink":
|
if ProjectSettings.get_setting("display/window/tablet_driver") == "winink":
|
||||||
tablet_driver = 1
|
tablet_driver = 1
|
||||||
|
single_window_mode = ProjectSettings.get_setting("display/window/subwindows/embed_subwindows")
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
@ -769,9 +787,7 @@ func undo_or_redo(
|
||||||
if current_cel is Cel3D:
|
if current_cel is Cel3D:
|
||||||
current_cel.size_changed(project.size)
|
current_cel.size_changed(project.size)
|
||||||
else:
|
else:
|
||||||
current_cel.image_texture = ImageTexture.create_from_image(
|
current_cel.image_texture.set_image(current_cel.get_image())
|
||||||
current_cel.get_image()
|
|
||||||
)
|
|
||||||
canvas.camera_zoom()
|
canvas.camera_zoom()
|
||||||
canvas.grid.queue_redraw()
|
canvas.grid.queue_redraw()
|
||||||
canvas.pixel_grid.queue_redraw()
|
canvas.pixel_grid.queue_redraw()
|
||||||
|
@ -807,16 +823,19 @@ func _renderer_changed(value: int) -> void:
|
||||||
func dialog_open(open: bool) -> void:
|
func dialog_open(open: bool) -> void:
|
||||||
var dim_color := Color.WHITE
|
var dim_color := Color.WHITE
|
||||||
if open:
|
if open:
|
||||||
can_draw = false
|
|
||||||
if dim_on_popup:
|
if dim_on_popup:
|
||||||
dim_color = Color(0.5, 0.5, 0.5)
|
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)
|
var tween := create_tween().set_trans(Tween.TRANS_LINEAR).set_ease(Tween.EASE_OUT)
|
||||||
tween.tween_property(control, "modulate", dim_color, 0.1)
|
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],
|
## 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.
|
## changes the cursor shape for it accordingly, and dims/brightens any textures it may have.
|
||||||
func disable_button(button: BaseButton, disable: bool) -> void:
|
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.access = FileDialog.ACCESS_FILESYSTEM
|
||||||
file_dialog.size = Vector2(384, 281)
|
file_dialog.size = Vector2(384, 281)
|
||||||
file_dialog.file_selected.connect(file_selected.bind(u_name))
|
file_dialog.file_selected.connect(file_selected.bind(u_name))
|
||||||
|
file_dialog.use_native_dialog = use_native_file_dialogs
|
||||||
var button := Button.new()
|
var button := Button.new()
|
||||||
button.text = "Load texture"
|
button.text = "Load texture"
|
||||||
button.pressed.connect(file_dialog.popup_centered)
|
button.pressed.connect(file_dialog.popup_centered)
|
||||||
|
|
|
@ -62,9 +62,7 @@ func handle_loading_file(file: String) -> void:
|
||||||
var image := Image.load_from_file(file)
|
var image := Image.load_from_file(file)
|
||||||
if not is_instance_valid(image): # An error occurred
|
if not is_instance_valid(image): # An error occurred
|
||||||
var file_name: String = file.get_file()
|
var file_name: String = file.get_file()
|
||||||
Global.error_dialog.set_text(tr("Can't load file '%s'.") % [file_name])
|
Global.popup_error(tr("Can't load file '%s'.") % [file_name])
|
||||||
Global.error_dialog.popup_centered()
|
|
||||||
Global.dialog_open(true)
|
|
||||||
return
|
return
|
||||||
handle_loading_image(file, image)
|
handle_loading_image(file, image)
|
||||||
|
|
||||||
|
@ -159,11 +157,7 @@ func open_pxo_file(path: String, untitled_backup := false, replace_empty := true
|
||||||
if not success:
|
if not success:
|
||||||
return
|
return
|
||||||
elif err != OK:
|
elif err != OK:
|
||||||
Global.error_dialog.set_text(
|
Global.popup_error(tr("File failed to open. Error code %s (%s)") % [err, error_string(err)])
|
||||||
tr("File failed to open. Error code %s (%s)") % [err, error_string(err)]
|
|
||||||
)
|
|
||||||
Global.error_dialog.popup_centered()
|
|
||||||
Global.dialog_open(true)
|
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
var data_json := zip_reader.read_file("data.json").get_string_from_utf8()
|
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)
|
file = FileAccess.open(path, FileAccess.READ)
|
||||||
var err := FileAccess.get_open_error()
|
var err := FileAccess.get_open_error()
|
||||||
if err != OK:
|
if err != OK:
|
||||||
Global.error_dialog.set_text(
|
Global.popup_error(tr("File failed to open. Error code %s (%s)") % [err, error_string(err)])
|
||||||
tr("File failed to open. Error code %s (%s)") % [err, error_string(err)]
|
|
||||||
)
|
|
||||||
Global.error_dialog.popup_centered()
|
|
||||||
Global.dialog_open(true)
|
|
||||||
return false
|
return false
|
||||||
|
|
||||||
var first_line := file.get_line()
|
var first_line := file.get_line()
|
||||||
|
@ -320,19 +310,11 @@ func save_pxo_file(
|
||||||
project.name = path.get_file().trim_suffix(".pxo")
|
project.name = path.get_file().trim_suffix(".pxo")
|
||||||
var serialized_data := project.serialize()
|
var serialized_data := project.serialize()
|
||||||
if !serialized_data:
|
if !serialized_data:
|
||||||
Global.error_dialog.set_text(
|
Global.popup_error(tr("File failed to save. Converting project data to dictionary failed."))
|
||||||
tr("File failed to save. Converting project data to dictionary failed.")
|
|
||||||
)
|
|
||||||
Global.error_dialog.popup_centered()
|
|
||||||
Global.dialog_open(true)
|
|
||||||
return false
|
return false
|
||||||
var to_save := JSON.stringify(serialized_data)
|
var to_save := JSON.stringify(serialized_data)
|
||||||
if !to_save:
|
if !to_save:
|
||||||
Global.error_dialog.set_text(
|
Global.popup_error(tr("File failed to save. Converting dictionary to JSON failed."))
|
||||||
tr("File failed to save. Converting dictionary to JSON failed.")
|
|
||||||
)
|
|
||||||
Global.error_dialog.popup_centered()
|
|
||||||
Global.dialog_open(true)
|
|
||||||
return false
|
return false
|
||||||
|
|
||||||
# Check if a file with the same name exists. If it does, rename the new file temporarily.
|
# 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 err != OK:
|
||||||
if temp_path.is_valid_filename():
|
if temp_path.is_valid_filename():
|
||||||
return false
|
return false
|
||||||
Global.error_dialog.set_text(
|
Global.popup_error(tr("File failed to save. Error code %s (%s)") % [err, error_string(err)])
|
||||||
tr("File failed to save. Error code %s (%s)") % [err, error_string(err)]
|
|
||||||
)
|
|
||||||
Global.error_dialog.popup_centered()
|
|
||||||
Global.dialog_open(true)
|
|
||||||
if zip_packer: # this would be null if we attempt to save filenames such as "//\\||.pxo"
|
if zip_packer: # this would be null if we attempt to save filenames such as "//\\||.pxo"
|
||||||
zip_packer.close()
|
zip_packer.close()
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -441,11 +441,7 @@ func import_palette_from_path(path: String, make_copy := false, is_initialising
|
||||||
new_palette_imported.emit()
|
new_palette_imported.emit()
|
||||||
select_palette(palette.name)
|
select_palette(palette.name)
|
||||||
else:
|
else:
|
||||||
Global.error_dialog.set_text(
|
Global.popup_error(tr("Can't load file '%s'.\nThis is not a valid palette file.") % [path])
|
||||||
tr("Can't load file '%s'.\nThis is not a valid palette file.") % [path]
|
|
||||||
)
|
|
||||||
Global.error_dialog.popup_centered()
|
|
||||||
Global.dialog_open(true)
|
|
||||||
|
|
||||||
|
|
||||||
## Refer to app/core/gimppalette-load.c of the GNU Image Manipulation Program for the "living spec"
|
## Refer to app/core/gimppalette-load.c of the GNU Image Manipulation Program for the "living spec"
|
||||||
|
|
|
@ -541,8 +541,7 @@ func handle_draw(position: Vector2i, event: InputEvent) -> void:
|
||||||
|
|
||||||
var project := Global.current_project
|
var project := Global.current_project
|
||||||
var text := "[%s×%s]" % [project.size.x, project.size.y]
|
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():
|
if not _slots[MOUSE_BUTTON_LEFT].tool_node.cursor_text.is_empty():
|
||||||
text += " %s" % _slots[MOUSE_BUTTON_LEFT].tool_node.cursor_text
|
text += " %s" % _slots[MOUSE_BUTTON_LEFT].tool_node.cursor_text
|
||||||
if not _slots[MOUSE_BUTTON_RIGHT].tool_node.cursor_text.is_empty():
|
if not _slots[MOUSE_BUTTON_RIGHT].tool_node.cursor_text.is_empty():
|
||||||
|
|
15
src/Main.gd
15
src/Main.gd
|
@ -206,7 +206,6 @@ func _notification(what: int) -> void:
|
||||||
# If the mouse exits the window and another application has the focus,
|
# If the mouse exits the window and another application has the focus,
|
||||||
# pause the application
|
# pause the application
|
||||||
NOTIFICATION_APPLICATION_FOCUS_OUT:
|
NOTIFICATION_APPLICATION_FOCUS_OUT:
|
||||||
Global.has_focus = false
|
|
||||||
if Global.pause_when_unfocused:
|
if Global.pause_when_unfocused:
|
||||||
get_tree().paused = true
|
get_tree().paused = true
|
||||||
NOTIFICATION_WM_MOUSE_EXIT:
|
NOTIFICATION_WM_MOUSE_EXIT:
|
||||||
|
@ -217,12 +216,6 @@ func _notification(what: int) -> void:
|
||||||
get_tree().paused = false
|
get_tree().paused = false
|
||||||
NOTIFICATION_APPLICATION_FOCUS_IN:
|
NOTIFICATION_APPLICATION_FOCUS_IN:
|
||||||
get_tree().paused = false
|
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:
|
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())
|
Global.config_cache.set_value("data", "current_dir", file_path.get_base_dir())
|
||||||
else:
|
else:
|
||||||
# If file doesn't exist on disk then warn user about this
|
# If file doesn't exist on disk then warn user about this
|
||||||
Global.error_dialog.set_text("Cannot find last project file.")
|
Global.popup_error("Cannot find last project file.")
|
||||||
Global.error_dialog.popup_centered()
|
|
||||||
Global.dialog_open(true)
|
|
||||||
|
|
||||||
|
|
||||||
func load_recent_project_file(path: String) -> void:
|
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())
|
Global.config_cache.set_value("data", "current_dir", path.get_base_dir())
|
||||||
else:
|
else:
|
||||||
# If file doesn't exist on disk then warn user about this
|
# If file doesn't exist on disk then warn user about this
|
||||||
Global.error_dialog.set_text("Cannot find project file.")
|
Global.popup_error("Cannot find project file.")
|
||||||
Global.error_dialog.popup_centered()
|
|
||||||
Global.dialog_open(true)
|
|
||||||
|
|
||||||
|
|
||||||
func _on_OpenSprite_files_selected(paths: PackedStringArray) -> void:
|
func _on_OpenSprite_files_selected(paths: PackedStringArray) -> void:
|
||||||
|
|
|
@ -115,7 +115,7 @@ text = "Delete Palette?"
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
vertical_alignment = 1
|
vertical_alignment = 1
|
||||||
|
|
||||||
[node name="ExportFileDialog" type="FileDialog" parent="."]
|
[node name="ExportFileDialog" type="FileDialog" parent="." groups=["FileDialogs"]]
|
||||||
size = Vector2i(677, 400)
|
size = Vector2i(677, 400)
|
||||||
access = 2
|
access = 2
|
||||||
filters = PackedStringArray("*.png ; PNG Image", "*.jpg,*.jpeg ; JPEG Image", "*.webp ; WebP Image")
|
filters = PackedStringArray("*.png ; PNG Image", "*.jpg,*.jpeg ; JPEG Image", "*.webp ; WebP Image")
|
||||||
|
|
|
@ -212,9 +212,7 @@ func read_extension(extension_file_or_folder_name: StringName, internal := false
|
||||||
"\n",
|
"\n",
|
||||||
"But Pixelorama's API version is: %s" % ExtensionsApi.get_api_version()
|
"But Pixelorama's API version is: %s" % ExtensionsApi.get_api_version()
|
||||||
)
|
)
|
||||||
Global.error_dialog.set_text(str(err_text, required_text))
|
Global.popup_error(str(err_text, required_text))
|
||||||
Global.error_dialog.popup_centered()
|
|
||||||
Global.dialog_open(true)
|
|
||||||
print("Incompatible API")
|
print("Incompatible API")
|
||||||
if !internal: # the file isn't created for internal extensions, no need for removal
|
if !internal: # the file isn't created for internal extensions, no need for removal
|
||||||
# Don't put it in faulty, (it's merely incompatible)
|
# Don't put it in faulty, (it's merely incompatible)
|
||||||
|
|
|
@ -7,9 +7,20 @@ var preferences: Array[Preference] = [
|
||||||
Preference.new(
|
Preference.new(
|
||||||
"quit_confirmation", "Startup/StartupContainer/QuitConfirmation", "button_pressed"
|
"quit_confirmation", "Startup/StartupContainer/QuitConfirmation", "button_pressed"
|
||||||
),
|
),
|
||||||
|
Preference.new("ffmpeg_path", "Startup/StartupContainer/FFMPEGPath", "text"),
|
||||||
Preference.new("shrink", "%ShrinkSlider", "value"),
|
Preference.new("shrink", "%ShrinkSlider", "value"),
|
||||||
Preference.new("font_size", "Interface/InterfaceOptions/FontSizeSlider", "value"),
|
Preference.new("font_size", "Interface/InterfaceOptions/FontSizeSlider", "value"),
|
||||||
Preference.new("dim_on_popup", "Interface/InterfaceOptions/DimCheckBox", "button_pressed"),
|
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("icon_color_from", "Interface/ButtonOptions/IconColorOptionButton", "selected"),
|
||||||
Preference.new("custom_icon_color", "Interface/ButtonOptions/IconColorButton", "color"),
|
Preference.new("custom_icon_color", "Interface/ButtonOptions/IconColorButton", "color"),
|
||||||
Preference.new("left_tool_color", "Interface/ButtonOptions/LeftToolColorButton", "color"),
|
Preference.new("left_tool_color", "Interface/ButtonOptions/LeftToolColorButton", "color"),
|
||||||
|
@ -202,6 +213,10 @@ func _ready() -> void:
|
||||||
node.item_selected.connect(
|
node.item_selected.connect(
|
||||||
_on_Preference_value_changed.bind(pref, restore_default_button)
|
_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)
|
var global_value = Global.get(pref.prop_name)
|
||||||
if Global.config_cache.has_section_key("preferences", pref.prop_name):
|
if Global.config_cache.has_section_key("preferences", pref.prop_name):
|
||||||
|
|
|
@ -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/PreferencesDialog.gd" id="1"]
|
||||||
[ext_resource type="Script" path="res://src/Preferences/HandleExtensions.gd" id="2"]
|
[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="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="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="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"]
|
[sub_resource type="ButtonGroup" id="ButtonGroup_8vsfb"]
|
||||||
|
|
||||||
[node name="PreferencesDialog" type="AcceptDialog"]
|
[node name="PreferencesDialog" type="AcceptDialog"]
|
||||||
title = "Preferences"
|
title = "Preferences"
|
||||||
|
position = Vector2i(0, 36)
|
||||||
size = Vector2i(800, 500)
|
size = Vector2i(800, 500)
|
||||||
exclusive = false
|
exclusive = false
|
||||||
popup_window = true
|
popup_window = true
|
||||||
|
@ -28,7 +30,7 @@ offset_bottom = -49.0
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
theme_override_constants/separation = 20
|
theme_override_constants/separation = 20
|
||||||
theme_override_constants/autohide = 0
|
theme_override_constants/autohide = 0
|
||||||
split_offset = 150
|
split_offset = 125
|
||||||
|
|
||||||
[node name="List" type="ItemList" parent="HSplitContainer"]
|
[node name="List" type="ItemList" parent="HSplitContainer"]
|
||||||
custom_minimum_size = Vector2(85, 0)
|
custom_minimum_size = Vector2(85, 0)
|
||||||
|
@ -90,6 +92,13 @@ layout_mode = 2
|
||||||
mouse_default_cursor_shape = 2
|
mouse_default_cursor_shape = 2
|
||||||
text = "On"
|
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"]
|
[node name="Language" type="VBoxContainer" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide"]
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
@ -200,6 +209,32 @@ mouse_default_cursor_shape = 2
|
||||||
button_pressed = true
|
button_pressed = true
|
||||||
text = "On"
|
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"]
|
[node name="ThemesHeader" type="HBoxContainer" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Interface"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_constants/separation = 0
|
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
|
mouse_default_cursor_shape = 2
|
||||||
|
|
||||||
[node name="Extensions" type="VBoxContainer" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide"]
|
[node name="Extensions" type="VBoxContainer" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide"]
|
||||||
|
unique_name_in_owner = true
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
script = ExtResource("2")
|
script = ExtResource("2")
|
||||||
|
@ -1093,6 +1129,10 @@ text = "Extensions"
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
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"]
|
[node name="InstalledExtensions" type="ItemList" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
auto_height = true
|
auto_height = true
|
||||||
|
@ -1293,9 +1333,20 @@ layout_mode = 2
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
text = "Pixelorama must be restarted for changes to take effect."
|
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
|
mode = 1
|
||||||
title = "Open File(s)"
|
title = "Open File(s)"
|
||||||
size = Vector2i(560, 400)
|
size = Vector2i(560, 400)
|
||||||
|
@ -1307,6 +1358,9 @@ access = 2
|
||||||
filters = PackedStringArray("*.pck ; Godot Resource Pack File", "*.zip ;")
|
filters = PackedStringArray("*.pck ; Godot Resource Pack File", "*.zip ;")
|
||||||
show_hidden_files = true
|
show_hidden_files = true
|
||||||
|
|
||||||
|
[node name="Store" parent="Popups" instance=ExtResource("8_jmnx8")]
|
||||||
|
transient = true
|
||||||
|
|
||||||
[node name="DeleteConfirmation" type="ConfirmationDialog" parent="."]
|
[node name="DeleteConfirmation" type="ConfirmationDialog" parent="."]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
position = Vector2i(0, 36)
|
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="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/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/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="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="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"]
|
[connection signal="pressed" from="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions/HBoxContainer/AddExtensionButton" to="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions" method="_on_AddExtensionButton_pressed"]
|
||||||
|
|
49
src/Shaders/Effects/Rotation/CommonRotation.gdshaderinc
Normal file
49
src/Shaders/Effects/Rotation/CommonRotation.gdshaderinc
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -1,56 +1,17 @@
|
||||||
shader_type canvas_item;
|
shader_type canvas_item;
|
||||||
render_mode unshaded;
|
render_mode unshaded;
|
||||||
|
|
||||||
uniform float angle;
|
#include "res://src/Shaders/Effects/Rotation/CommonRotation.gdshaderinc"
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void fragment() {
|
void fragment() {
|
||||||
vec4 original = texture(TEXTURE, UV);
|
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 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 pixelated_uv = floor(UV * tex_size) / (tex_size - 1.0); // Pixelate UV to fit resolution
|
||||||
vec2 pivot = pivot_pixel / tex_size; // Normalize pivot position
|
vec2 pivot = pivot_pixel / tex_size; // Normalize pivot position
|
||||||
float ratio = tex_size.x / tex_size.y; // Resolution ratio
|
float ratio = tex_size.x / tex_size.y; // Resolution ratio
|
||||||
|
|
||||||
// Make a border to prevent stretching pixels on the edge
|
vec2 rotated_uv = rotate(pixelated_uv, pivot, ratio);
|
||||||
vec2 border_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);
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,8 @@ shader_type canvas_item;
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
#include "res://src/Shaders/Effects/Rotation/CommonRotation.gdshaderinc"
|
||||||
|
|
||||||
uniform int ScaleMultiplier : hint_range(0, 100) = 4;
|
uniform int ScaleMultiplier : hint_range(0, 100) = 4;
|
||||||
|
|
||||||
// vertex compatibility #defines
|
// vertex compatibility #defines
|
||||||
|
@ -40,9 +42,6 @@ uniform int ScaleMultiplier : hint_range(0, 100) = 4;
|
||||||
// #define outsize vec4(OutputSize, 1.0 / OutputSize)
|
// #define outsize vec4(OutputSize, 1.0 / OutputSize)
|
||||||
|
|
||||||
// Pixelorama-specific uniforms
|
// Pixelorama-specific uniforms
|
||||||
uniform float angle;
|
|
||||||
uniform sampler2D selection_tex;
|
|
||||||
uniform vec2 pivot_pixel;
|
|
||||||
uniform bool preview = false;
|
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()
|
void fragment()
|
||||||
{
|
{
|
||||||
vec4 original = texture(TEXTURE, UV);
|
vec4 original = texture(TEXTURE, UV);
|
||||||
float selection = texture(selection_tex, UV).a;
|
|
||||||
vec2 size = 1.0 / TEXTURE_PIXEL_SIZE;
|
vec2 size = 1.0 / TEXTURE_PIXEL_SIZE;
|
||||||
vec2 pivot = pivot_pixel / size; // Normalize pivot position
|
vec2 pivot = pivot_pixel / size; // Normalize pivot position
|
||||||
float ratio = size.x / size.y; // Resolution ratio
|
float ratio = size.x / size.y; // Resolution ratio
|
||||||
|
@ -321,29 +302,8 @@ void fragment()
|
||||||
else {
|
else {
|
||||||
rotated_uv = rotate(UV, pivot, ratio);
|
rotated_uv = rotate(UV, pivot, ratio);
|
||||||
}
|
}
|
||||||
vec4 c;
|
vec4 c = scale(TEXTURE, rotated_uv, TEXTURE_PIXEL_SIZE);
|
||||||
c = scale(TEXTURE, rotated_uv, TEXTURE_PIXEL_SIZE);
|
|
||||||
|
|
||||||
// Taken from NearestNeighbour shader
|
// Pixelorama edit
|
||||||
c.a *= texture(selection_tex, rotated_uv).a; // Combine with selection mask
|
COLOR = mix_rotated_and_original(c, original, UV, rotated_uv, TEXTURE_PIXEL_SIZE);
|
||||||
// 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;
|
|
||||||
}
|
}
|
|
@ -25,7 +25,7 @@ OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
shader_type canvas_item;
|
shader_type canvas_item;
|
||||||
|
#include "res://src/Shaders/Effects/Rotation/CommonRotation.gdshaderinc"
|
||||||
//enables 2:1 slopes. otherwise only uses 45 degree slopes
|
//enables 2:1 slopes. otherwise only uses 45 degree slopes
|
||||||
#define SLOPE
|
#define SLOPE
|
||||||
//cleans up small detail slope transitions (if SLOPE is enabled)
|
//cleans up small detail slope transitions (if SLOPE is enabled)
|
||||||
|
@ -45,9 +45,6 @@ uniform float similarThreshold = 0.0;
|
||||||
uniform float lineWidth = 1.0;
|
uniform float lineWidth = 1.0;
|
||||||
|
|
||||||
// Edited for Pixelorama
|
// Edited for Pixelorama
|
||||||
uniform float angle;
|
|
||||||
uniform sampler2D selection_tex;
|
|
||||||
uniform vec2 pivot_pixel;
|
|
||||||
uniform bool preview = false;
|
uniform bool preview = false;
|
||||||
|
|
||||||
bool similar(vec4 col1, vec4 col2){
|
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);
|
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() {
|
void fragment() {
|
||||||
vec2 size = 1.0/TEXTURE_PIXEL_SIZE+0.0001; //fix for some sort of rounding error
|
vec2 size = 1.0/TEXTURE_PIXEL_SIZE+0.0001; //fix for some sort of rounding error
|
||||||
// Pixelorama edit
|
// Pixelorama edit
|
||||||
vec4 original = texture(TEXTURE, UV);
|
vec4 original = texture(TEXTURE, UV);
|
||||||
float selection = texture(selection_tex, UV).a;
|
|
||||||
vec2 pivot = pivot_pixel / size; // Normalize pivot position
|
vec2 pivot = pivot_pixel / size; // Normalize pivot position
|
||||||
float ratio = size.x / size.y; // Resolution ratio
|
float ratio = size.x / size.y; // Resolution ratio
|
||||||
vec2 pixelated_uv = floor(UV * size) / (size - 1.0); // Pixelate UV to fit resolutio
|
vec2 pixelated_uv = floor(UV * size) / (size - 1.0); // Pixelate UV to fit resolutio
|
||||||
|
@ -360,24 +339,6 @@ void fragment() {
|
||||||
col = u_col;
|
col = u_col;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pixelorama edit, taken from NearestNeighbour shader
|
// Pixelorama edit
|
||||||
col.a *= texture(selection_tex, rotated_uv).a; // Combine with selection mask
|
COLOR = mix_rotated_and_original(col, original, UV, rotated_uv, TEXTURE_PIXEL_SIZE);
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -773,7 +773,7 @@ script = ExtResource("5")
|
||||||
wait_time = 0.2
|
wait_time = 0.2
|
||||||
one_shot = true
|
one_shot = true
|
||||||
|
|
||||||
[node name="LoadModelDialog" type="FileDialog" parent="." index="6"]
|
[node name="LoadModelDialog" type="FileDialog" parent="." index="6" groups=["FileDialogs"]]
|
||||||
mode = 1
|
mode = 1
|
||||||
title = "Open File(s)"
|
title = "Open File(s)"
|
||||||
size = Vector2i(558, 300)
|
size = Vector2i(558, 300)
|
||||||
|
|
|
@ -252,6 +252,8 @@ func _on_Size_value_changed(value: Vector2i) -> void:
|
||||||
|
|
||||||
if timer.is_stopped():
|
if timer.is_stopped():
|
||||||
undo_data = selection_node.get_undo_data(false)
|
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()
|
timer.start()
|
||||||
selection_node.big_bounding_rectangle.size = value
|
selection_node.big_bounding_rectangle.size = value
|
||||||
selection_node.resize_selection()
|
selection_node.resize_selection()
|
||||||
|
@ -262,5 +264,5 @@ func _on_Size_ratio_toggled(button_pressed: bool) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _on_Timer_timeout() -> 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)
|
selection_node.commit_undo("Move Selection", undo_data)
|
||||||
|
|
|
@ -91,8 +91,7 @@ func _input(event: InputEvent) -> void:
|
||||||
Tools.handle_draw(Vector2i(current_pixel.floor()), event)
|
Tools.handle_draw(Vector2i(current_pixel.floor()), event)
|
||||||
|
|
||||||
if sprite_changed_this_frame:
|
if sprite_changed_this_frame:
|
||||||
if Global.has_focus:
|
queue_redraw()
|
||||||
queue_redraw()
|
|
||||||
update_selected_cels_textures()
|
update_selected_cels_textures()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,11 @@ extends Node2D
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
func _input(event: InputEvent) -> void:
|
||||||
if Global.has_focus:
|
if event is InputEventMouse or event is InputEventKey:
|
||||||
if event is InputEventMouse or event is InputEventKey:
|
queue_redraw()
|
||||||
queue_redraw()
|
|
||||||
|
|
||||||
|
|
||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
# Draw rectangle to indicate the pixel currently being hovered on
|
# 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()
|
Tools.draw_indicator()
|
||||||
|
|
|
@ -29,7 +29,7 @@ func draw_guide_line():
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
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
|
visible = false
|
||||||
return
|
return
|
||||||
visible = true
|
visible = true
|
||||||
|
|
|
@ -2,11 +2,10 @@ extends Node2D
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
func _input(event: InputEvent) -> void:
|
||||||
if Global.has_focus:
|
if event is InputEventMouse or event is InputEventKey:
|
||||||
if event is InputEventMouse or event is InputEventKey:
|
queue_redraw()
|
||||||
queue_redraw()
|
|
||||||
|
|
||||||
|
|
||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
if Global.has_focus and Global.can_draw:
|
if Global.can_draw:
|
||||||
Tools.draw_preview()
|
Tools.draw_preview()
|
||||||
|
|
|
@ -55,7 +55,6 @@ func _input(event: InputEvent) -> void:
|
||||||
var ri: ReferenceImage = Global.current_project.get_current_reference_image()
|
var ri: ReferenceImage = Global.current_project.get_current_reference_image()
|
||||||
|
|
||||||
if !ri:
|
if !ri:
|
||||||
Global.can_draw = true
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if want to cancelthe reference transform
|
# Check if want to cancelthe reference transform
|
||||||
|
|
|
@ -39,7 +39,6 @@ func _input(_event: InputEvent) -> void:
|
||||||
if (
|
if (
|
||||||
Input.is_action_just_pressed(&"left_mouse")
|
Input.is_action_just_pressed(&"left_mouse")
|
||||||
and Global.can_draw
|
and Global.can_draw
|
||||||
and Global.has_focus
|
|
||||||
and rect.has_point(mouse_pos)
|
and rect.has_point(mouse_pos)
|
||||||
):
|
):
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -13,6 +13,11 @@ var is_pasting := false
|
||||||
var big_bounding_rectangle := Rect2i():
|
var big_bounding_rectangle := Rect2i():
|
||||||
set(value):
|
set(value):
|
||||||
big_bounding_rectangle = 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():
|
for slot in Tools._slots.values():
|
||||||
if slot.tool_node is BaseSelectionTool:
|
if slot.tool_node is BaseSelectionTool:
|
||||||
slot.tool_node.set_spinbox_values()
|
slot.tool_node.set_spinbox_values()
|
||||||
|
@ -34,7 +39,8 @@ var preview_image_texture := ImageTexture.new()
|
||||||
var undo_data: Dictionary
|
var undo_data: Dictionary
|
||||||
var gizmos: Array[Gizmo] = []
|
var gizmos: Array[Gizmo] = []
|
||||||
var dragged_gizmo: Gizmo = null
|
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 mouse_pos_on_gizmo_drag := Vector2.ZERO
|
||||||
var resize_keep_ratio := false
|
var resize_keep_ratio := false
|
||||||
|
|
||||||
|
@ -53,24 +59,24 @@ class Gizmo:
|
||||||
type = _type
|
type = _type
|
||||||
direction = _direction
|
direction = _direction
|
||||||
|
|
||||||
func get_cursor() -> Control.CursorShape:
|
func get_cursor() -> DisplayServer.CursorShape:
|
||||||
var cursor := Control.CURSOR_MOVE
|
var cursor := DisplayServer.CURSOR_MOVE
|
||||||
if direction == Vector2i.ZERO:
|
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
|
elif direction == Vector2i(-1, -1) or direction == Vector2i(1, 1): # Top left or bottom right
|
||||||
if Global.mirror_view:
|
if Global.mirror_view:
|
||||||
cursor = Control.CURSOR_BDIAGSIZE
|
cursor = DisplayServer.CURSOR_BDIAGSIZE
|
||||||
else:
|
else:
|
||||||
cursor = Control.CURSOR_FDIAGSIZE
|
cursor = DisplayServer.CURSOR_FDIAGSIZE
|
||||||
elif direction == Vector2i(1, -1) or direction == Vector2i(-1, 1): # Top right or bottom left
|
elif direction == Vector2i(1, -1) or direction == Vector2i(-1, 1): # Top right or bottom left
|
||||||
if Global.mirror_view:
|
if Global.mirror_view:
|
||||||
cursor = Control.CURSOR_FDIAGSIZE
|
cursor = DisplayServer.CURSOR_FDIAGSIZE
|
||||||
else:
|
else:
|
||||||
cursor = Control.CURSOR_BDIAGSIZE
|
cursor = DisplayServer.CURSOR_BDIAGSIZE
|
||||||
elif direction == Vector2i(0, -1) or direction == Vector2i(0, 1): # Center top or center bottom
|
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
|
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
|
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(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, 1))) # Bottom left
|
||||||
gizmos.append(Gizmo.new(Gizmo.Type.SCALE, Vector2i(-1, 0))) # Center 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:
|
func _input(event: InputEvent) -> void:
|
||||||
|
var project := Global.current_project
|
||||||
image_current_pixel = canvas.current_pixel
|
image_current_pixel = canvas.current_pixel
|
||||||
if Global.mirror_view:
|
if Global.mirror_view:
|
||||||
image_current_pixel.x = Global.current_project.size.x - image_current_pixel.x
|
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"):
|
elif Input.is_action_just_pressed("transformation_cancel"):
|
||||||
transform_content_cancel()
|
transform_content_cancel()
|
||||||
|
|
||||||
var project := Global.current_project
|
|
||||||
if not project.layers[project.current_layer].can_layer_get_drawn():
|
if not project.layers[project.current_layer].can_layer_get_drawn():
|
||||||
return
|
return
|
||||||
if event is InputEventKey:
|
if event is InputEventKey:
|
||||||
|
@ -123,17 +127,14 @@ func _input(event: InputEvent) -> void:
|
||||||
if Input.is_action_pressed("transform_move_selection_only"):
|
if Input.is_action_pressed("transform_move_selection_only"):
|
||||||
transform_content_confirm()
|
transform_content_confirm()
|
||||||
if not is_moving_content:
|
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"):
|
if Input.is_action_pressed("transform_move_selection_only"):
|
||||||
undo_data = get_undo_data(false)
|
undo_data = get_undo_data(false)
|
||||||
temp_rect = big_bounding_rectangle
|
temp_rect = big_bounding_rectangle
|
||||||
else:
|
else:
|
||||||
transform_content_start()
|
transform_content_start()
|
||||||
project.selection_offset = Vector2.ZERO
|
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:
|
else:
|
||||||
var prev_temp_rect := temp_rect
|
var prev_temp_rect := temp_rect
|
||||||
dragged_gizmo.direction.x *= signi(temp_rect.size.x)
|
dragged_gizmo.direction.x *= signi(temp_rect.size.x)
|
||||||
|
@ -157,7 +158,9 @@ func _input(event: InputEvent) -> void:
|
||||||
Global.can_draw = true
|
Global.can_draw = true
|
||||||
dragged_gizmo = null
|
dragged_gizmo = null
|
||||||
if not is_moving_content:
|
if not is_moving_content:
|
||||||
|
original_bitmap = SelectionMap.new()
|
||||||
commit_undo("Select", undo_data)
|
commit_undo("Select", undo_data)
|
||||||
|
angle = 0.0
|
||||||
|
|
||||||
if dragged_gizmo:
|
if dragged_gizmo:
|
||||||
if dragged_gizmo.type == Gizmo.Type.SCALE:
|
if dragged_gizmo.type == Gizmo.Type.SCALE:
|
||||||
|
@ -166,17 +169,17 @@ func _input(event: InputEvent) -> void:
|
||||||
_gizmo_rotate()
|
_gizmo_rotate()
|
||||||
else: # Set the appropriate cursor
|
else: # Set the appropriate cursor
|
||||||
if gizmo_hover:
|
if gizmo_hover:
|
||||||
Global.main_viewport.mouse_default_cursor_shape = gizmo_hover.get_cursor()
|
DisplayServer.cursor_set_shape(gizmo_hover.get_cursor())
|
||||||
else:
|
else:
|
||||||
var cursor := Control.CURSOR_ARROW
|
var cursor := DisplayServer.CURSOR_ARROW
|
||||||
if Global.cross_cursor:
|
if Global.cross_cursor:
|
||||||
cursor = Control.CURSOR_CROSS
|
cursor = DisplayServer.CURSOR_CROSS
|
||||||
var layer: BaseLayer = project.layers[project.current_layer]
|
var layer: BaseLayer = project.layers[project.current_layer]
|
||||||
if not layer.can_layer_get_drawn():
|
if not layer.can_layer_get_drawn():
|
||||||
cursor = Control.CURSOR_FORBIDDEN
|
cursor = DisplayServer.CURSOR_FORBIDDEN
|
||||||
|
|
||||||
if Global.main_viewport.mouse_default_cursor_shape != cursor:
|
if DisplayServer.cursor_get_shape() != cursor:
|
||||||
Global.main_viewport.mouse_default_cursor_shape = cursor
|
DisplayServer.cursor_set_shape(cursor)
|
||||||
|
|
||||||
|
|
||||||
func _move_with_arrow_keys(event: InputEvent) -> void:
|
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))
|
var move := input.rotated(snappedf(Global.camera.rotation, PI / 2))
|
||||||
# These checks are needed to fix a bug where the selection got stuck
|
# These checks are needed to fix a bug where the selection got stuck
|
||||||
# to the canvas boundaries when they were 1px away from them
|
# 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
|
move.x = 0
|
||||||
if is_equal_approx(absf(move.y), 0.0):
|
if is_zero_approx(absf(move.y)):
|
||||||
move.y = 0
|
move.y = 0
|
||||||
move_content(move * step)
|
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:
|
func _is_action_direction_pressed(event: InputEvent) -> bool:
|
||||||
for action in KEY_MOVE_ACTION_NAMES:
|
for action in KEY_MOVE_ACTION_NAMES:
|
||||||
if event.is_action_pressed(action, false, true):
|
if event.is_action_pressed(action, false, true):
|
||||||
|
@ -225,7 +228,7 @@ func _is_action_direction_pressed(event: InputEvent) -> bool:
|
||||||
return false
|
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:
|
func _is_action_direction(event: InputEvent) -> bool:
|
||||||
for action in KEY_MOVE_ACTION_NAMES:
|
for action in KEY_MOVE_ACTION_NAMES:
|
||||||
if event.is_action(action, true):
|
if event.is_action(action, true):
|
||||||
|
@ -233,7 +236,7 @@ func _is_action_direction(event: InputEvent) -> bool:
|
||||||
return false
|
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:
|
func _is_action_direction_released(event: InputEvent) -> bool:
|
||||||
for action in KEY_MOVE_ACTION_NAMES:
|
for action in KEY_MOVE_ACTION_NAMES:
|
||||||
if event.is_action_released(action, true):
|
if event.is_action_released(action, true):
|
||||||
|
@ -282,9 +285,9 @@ func _update_gizmos() -> void:
|
||||||
)
|
)
|
||||||
|
|
||||||
# Rotation gizmo (temp)
|
# Rotation gizmo (temp)
|
||||||
# gizmos[8].rect = Rect2(
|
#gizmos[8].rect = Rect2(
|
||||||
# Vector2((rect_end.x + rect_pos.x - size.x) / 2, rect_pos.y - size.y - (size.y * 2)), size
|
#Vector2((rect_end.x + rect_pos.x - size.x) / 2, rect_pos.y - size.y - (size.y * 2)), size
|
||||||
# )
|
#)
|
||||||
queue_redraw()
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
@ -337,8 +340,6 @@ func _gizmo_resize() -> void:
|
||||||
temp_rect.position.y = end_y - temp_rect.size.y
|
temp_rect.position.y = end_y - temp_rect.size.y
|
||||||
|
|
||||||
big_bounding_rectangle = temp_rect.abs()
|
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:
|
if big_bounding_rectangle.size.x == 0:
|
||||||
big_bounding_rectangle.size.x = 1
|
big_bounding_rectangle.size.x = 1
|
||||||
if big_bounding_rectangle.size.y == 0:
|
if big_bounding_rectangle.size.y == 0:
|
||||||
|
@ -370,9 +371,14 @@ func _resize_rect(pos: Vector2, dir: Vector2) -> void:
|
||||||
|
|
||||||
func resize_selection() -> void:
|
func resize_selection() -> void:
|
||||||
var size := big_bounding_rectangle.size.abs()
|
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)
|
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)
|
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)
|
preview_image.resize(size.x, size.y, Image.INTERPOLATE_NEAREST)
|
||||||
if temp_rect.size.x < 0:
|
if temp_rect.size.x < 0:
|
||||||
preview_image.flip_x()
|
preview_image.flip_x()
|
||||||
|
@ -380,6 +386,12 @@ func resize_selection() -> void:
|
||||||
preview_image.flip_y()
|
preview_image.flip_y()
|
||||||
preview_image_texture = ImageTexture.create_from_image(preview_image)
|
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.selection_map.resize_bitmap_values(
|
||||||
Global.current_project, size, temp_rect.size.x < 0, temp_rect.size.y < 0
|
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()
|
Global.canvas.queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
func _gizmo_rotate() -> void: # Does not work properly yet
|
func _gizmo_rotate() -> void:
|
||||||
var angle := image_current_pixel.angle_to_point(mouse_pos_on_gizmo_drag)
|
angle = image_current_pixel.angle_to_point(mouse_pos_on_gizmo_drag)
|
||||||
angle = deg_to_rad(floorf(rad_to_deg(angle)))
|
resize_selection()
|
||||||
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 select_rect(rect: Rect2i, operation: int = SelectionOperation.ADD) -> void:
|
func select_rect(rect: Rect2i, operation := SelectionOperation.ADD) -> void:
|
||||||
var project := Global.current_project
|
var project := Global.current_project
|
||||||
# Used only if the selection is outside of the canvas boundaries,
|
# Used only if the selection is outside of the canvas boundaries,
|
||||||
# on the left and/or above (negative coords)
|
# on the left and/or above (negative coords)
|
||||||
|
@ -510,15 +492,15 @@ func transform_content_confirm() -> void:
|
||||||
return
|
return
|
||||||
var project := Global.current_project
|
var project := Global.current_project
|
||||||
for cel in _get_selected_draw_cels():
|
for cel in _get_selected_draw_cels():
|
||||||
var cel_image: Image = cel.get_image()
|
var cel_image := cel.get_image()
|
||||||
var src: Image = preview_image
|
var src := Image.new()
|
||||||
|
src.copy_from(preview_image)
|
||||||
if not is_pasting:
|
if not is_pasting:
|
||||||
src.copy_from(cel.transformed_content)
|
src.copy_from(cel.transformed_content)
|
||||||
cel.transformed_content = null
|
cel.transformed_content = null
|
||||||
|
DrawingAlgos.nn_rotate(src, angle, content_pivot)
|
||||||
src.resize(
|
src.resize(
|
||||||
big_bounding_rectangle.size.x,
|
preview_image.get_width(), preview_image.get_height(), Image.INTERPOLATE_NEAREST
|
||||||
big_bounding_rectangle.size.y,
|
|
||||||
Image.INTERPOLATE_NEAREST
|
|
||||||
)
|
)
|
||||||
if temp_rect.size.x < 0:
|
if temp_rect.size.x < 0:
|
||||||
src.flip_x()
|
src.flip_x()
|
||||||
|
@ -539,6 +521,8 @@ func transform_content_confirm() -> void:
|
||||||
original_bitmap = SelectionMap.new()
|
original_bitmap = SelectionMap.new()
|
||||||
is_moving_content = false
|
is_moving_content = false
|
||||||
is_pasting = false
|
is_pasting = false
|
||||||
|
angle = 0.0
|
||||||
|
content_pivot = Vector2.ZERO
|
||||||
queue_redraw()
|
queue_redraw()
|
||||||
Global.canvas.queue_redraw()
|
Global.canvas.queue_redraw()
|
||||||
|
|
||||||
|
@ -570,6 +554,8 @@ func transform_content_cancel() -> void:
|
||||||
preview_image = Image.new()
|
preview_image = Image.new()
|
||||||
original_bitmap = SelectionMap.new()
|
original_bitmap = SelectionMap.new()
|
||||||
is_pasting = false
|
is_pasting = false
|
||||||
|
angle = 0.0
|
||||||
|
content_pivot = Vector2.ZERO
|
||||||
queue_redraw()
|
queue_redraw()
|
||||||
Global.canvas.queue_redraw()
|
Global.canvas.queue_redraw()
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
extends Container
|
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_picker := %ColorPicker as ColorPicker
|
||||||
@onready var color_buttons := %ColorButtons as HBoxContainer
|
@onready var color_buttons := %ColorButtons as HBoxContainer
|
||||||
@onready var left_color_rect := %LeftColorRect as ColorRect
|
@onready var left_color_rect := %LeftColorRect as ColorRect
|
||||||
|
@ -55,6 +57,14 @@ func _ready() -> void:
|
||||||
color_buttons.get_parent().remove_child(color_buttons)
|
color_buttons.get_parent().remove_child(color_buttons)
|
||||||
sampler_cont.add_child(color_buttons)
|
sampler_cont.add_child(color_buttons)
|
||||||
sampler_cont.move_child(color_buttons, 0)
|
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:
|
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:
|
func _on_expand_button_toggled(toggled_on: bool) -> void:
|
||||||
color_picker.color_modes_visible = toggled_on
|
color_picker.color_modes_visible = toggled_on
|
||||||
color_picker.sliders_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)
|
Global.config_cache.set_value("color_picker", "is_expanded", toggled_on)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,12 @@ var image_exports: Array[Export.FileFormat] = [
|
||||||
Export.FileFormat.WEBP,
|
Export.FileFormat.WEBP,
|
||||||
Export.FileFormat.JPEG,
|
Export.FileFormat.JPEG,
|
||||||
Export.FileFormat.GIF,
|
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] = [
|
var spritesheet_exports: Array[Export.FileFormat] = [
|
||||||
Export.FileFormat.PNG, Export.FileFormat.WEBP, Export.FileFormat.JPEG
|
Export.FileFormat.PNG, Export.FileFormat.WEBP, Export.FileFormat.JPEG
|
||||||
|
@ -188,6 +193,12 @@ func set_file_format_selector() -> void:
|
||||||
match Export.current_tab:
|
match Export.current_tab:
|
||||||
Export.ExportTab.IMAGE:
|
Export.ExportTab.IMAGE:
|
||||||
_set_file_format_selector_suitable_file_formats(image_exports)
|
_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:
|
Export.ExportTab.SPRITESHEET:
|
||||||
_set_file_format_selector_suitable_file_formats(spritesheet_exports)
|
_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:
|
func open_path_validation_alert_popup(path_or_name: int = -1) -> void:
|
||||||
# 0 is invalid path, 1 is invalid name
|
# 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:
|
if path_or_name == 0:
|
||||||
error_text = "DirAccess path is not valid!"
|
error_text = "Directory path is not valid!"
|
||||||
elif path_or_name == 1:
|
elif path_or_name == 1:
|
||||||
error_text = "File name is not valid!"
|
error_text = "File name is not valid!"
|
||||||
|
|
||||||
|
|
|
@ -306,7 +306,7 @@ offset_right = 692.0
|
||||||
offset_bottom = 551.0
|
offset_bottom = 551.0
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
|
|
||||||
[node name="PathDialog" type="FileDialog" parent="Popups"]
|
[node name="PathDialog" type="FileDialog" parent="Popups" groups=["FileDialogs"]]
|
||||||
mode = 2
|
mode = 2
|
||||||
title = "Open a Directory"
|
title = "Open a Directory"
|
||||||
size = Vector2i(675, 500)
|
size = Vector2i(675, 500)
|
||||||
|
|
|
@ -222,13 +222,13 @@ func _on_Indicator_draw() -> void:
|
||||||
else:
|
else:
|
||||||
conversion_scale = ratio.y
|
conversion_scale = ratio.y
|
||||||
var pivot_position := pivot * conversion_scale
|
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, 2, 0, 360, 360, Color.YELLOW)
|
||||||
pivot_indicator.draw_arc(pivot_position, 6, 0, 360, 360, Color.WHITE, 0.5)
|
pivot_indicator.draw_arc(pivot_position, 6, 0, 360, 360, Color.WHITE)
|
||||||
pivot_indicator.draw_line(
|
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_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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -239,8 +239,7 @@ func _on_Indicator_gui_input(event: InputEvent) -> void:
|
||||||
drag_pivot = false
|
drag_pivot = false
|
||||||
if drag_pivot:
|
if drag_pivot:
|
||||||
var img_size := preview_image.get_size()
|
var img_size := preview_image.get_size()
|
||||||
# var mouse_pos := get_local_mouse_position() - pivot_indicator.position
|
var mouse_pos := pivot_indicator.get_local_mouse_position()
|
||||||
var mouse_pos := pivot_indicator.position
|
|
||||||
var ratio := Vector2(img_size) / pivot_indicator.size
|
var ratio := Vector2(img_size) / pivot_indicator.size
|
||||||
# we need to set the scale according to the larger side
|
# we need to set the scale according to the larger side
|
||||||
var conversion_scale: float
|
var conversion_scale: float
|
||||||
|
|
|
@ -53,7 +53,7 @@ text = "No shader loaded!"
|
||||||
[node name="ShaderParams" type="VBoxContainer" parent="VBoxContainer"]
|
[node name="ShaderParams" type="VBoxContainer" parent="VBoxContainer"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
|
||||||
[node name="FileDialog" type="FileDialog" parent="."]
|
[node name="FileDialog" type="FileDialog" parent="." groups=["FileDialogs"]]
|
||||||
access = 2
|
access = 2
|
||||||
filters = PackedStringArray("*gdshader; Godot Shader File")
|
filters = PackedStringArray("*gdshader; Godot Shader File")
|
||||||
show_hidden_files = true
|
show_hidden_files = true
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[gd_scene format=3 uid="uid://b3aeqj2k58wdk"]
|
[gd_scene format=3 uid="uid://b3aeqj2k58wdk"]
|
||||||
|
|
||||||
[node name="OpenSprite" type="FileDialog"]
|
[node name="OpenSprite" type="FileDialog" groups=["FileDialogs"]]
|
||||||
title = "Open File(s)"
|
title = "Open File(s)"
|
||||||
size = Vector2i(558, 400)
|
size = Vector2i(558, 400)
|
||||||
exclusive = false
|
exclusive = false
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[gd_scene format=3 uid="uid://d4euwo633u33b"]
|
[gd_scene format=3 uid="uid://d4euwo633u33b"]
|
||||||
|
|
||||||
[node name="SaveSprite" type="FileDialog"]
|
[node name="SaveSprite" type="FileDialog" groups=["FileDialogs"]]
|
||||||
size = Vector2i(675, 400)
|
size = Vector2i(675, 400)
|
||||||
exclusive = false
|
exclusive = false
|
||||||
popup_window = true
|
popup_window = true
|
||||||
|
|
185
src/UI/ExtensionExplorer/Entry/ExtensionEntry.gd
Normal file
185
src/UI/ExtensionExplorer/Entry/ExtensionEntry.gd
Normal file
|
@ -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()
|
125
src/UI/ExtensionExplorer/Entry/ExtensionEntry.tscn
Normal file
125
src/UI/ExtensionExplorer/Entry/ExtensionEntry.tscn
Normal file
|
@ -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"]
|
214
src/UI/ExtensionExplorer/Store.gd
Normal file
214
src/UI/ExtensionExplorer/Store.gd
Normal file
|
@ -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
|
230
src/UI/ExtensionExplorer/Store.tscn
Normal file
230
src/UI/ExtensionExplorer/Store.tscn
Normal file
|
@ -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"]
|
47
src/UI/ExtensionExplorer/Subscripts/CustomStoreLinks.gd
Normal file
47
src/UI/ExtensionExplorer/Subscripts/CustomStoreLinks.gd
Normal file
|
@ -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")
|
50
src/UI/ExtensionExplorer/Subscripts/SearchManager.gd
Normal file
50
src/UI/ExtensionExplorer/Subscripts/SearchManager.gd
Normal file
|
@ -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()
|
27
src/UI/ExtensionExplorer/store_info.md
Normal file
27
src/UI/ExtensionExplorer/store_info.md
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
// This file is for online use.<br>
|
||||||
|
|
||||||
|
## 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")<br>
|
||||||
|
// e.g `[[keyword, ....], [keyword, ....], [keyword, ....], .......]`<br>
|
||||||
|
// 2. Each data must have a keyword of type `String` at it's first index which helps in identifying what the data represents.<br>
|
||||||
|
// e.g, ["name", "name of extension"] is the data giving information about "name".<br>
|
||||||
|
// Valid keywords are `name`, `version`, `description`, `tags`, `thumbnail`, `download_link`<br>
|
||||||
|
// Put quotation marks ("") to make it a string, otherwise error will occur.<br>
|
||||||
|
// 3. One store entry must occupy only one line (and vice-versa).<br>
|
||||||
|
// 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).<br>
|
||||||
|
// 5. links to another store_info file can be placed inside another store_info file (it will get detected as a custom store file).<br>
|
||||||
|
|
||||||
|
## TIPS:
|
||||||
|
// - `thumbnail` is the link you get by right clicking an image (uploaded somewhere on the internet) and selecting Copy Image Link.<br>
|
||||||
|
// - `download_link` is ususlly od the form `{repo}/raw/{Path of extension within repo}`<br>
|
||||||
|
// 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"`<br>
|
||||||
|
|
||||||
|
// 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
|
|
@ -29,9 +29,11 @@ func deserialize(data: Dictionary):
|
||||||
|
|
||||||
func initiate(data: Dictionary, vanishing_point: Node):
|
func initiate(data: Dictionary, vanishing_point: Node):
|
||||||
_vanishing_point = vanishing_point
|
_vanishing_point = vanishing_point
|
||||||
width = LINE_WIDTH / Global.camera.zoom.x
|
|
||||||
Global.canvas.add_child(self)
|
Global.canvas.add_child(self)
|
||||||
deserialize(data)
|
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()
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
|
@ -63,7 +65,7 @@ func _input(event: InputEvent) -> void:
|
||||||
var project_size := Global.current_project.size
|
var project_size := Global.current_project.size
|
||||||
|
|
||||||
if track_mouse:
|
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()
|
hide_perspective_line()
|
||||||
return
|
return
|
||||||
default_color.a = 0.5
|
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 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_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
|
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 (
|
if (
|
||||||
Geometry2D.segment_intersects_segment(from_a, from_b, points[0], points[1])
|
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
|
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:
|
func _draw() -> void:
|
||||||
|
width = LINE_WIDTH / Global.camera.zoom.x
|
||||||
var mouse_point := Global.canvas.current_pixel
|
var mouse_point := Global.canvas.current_pixel
|
||||||
var arc_points := PackedVector2Array()
|
var arc_points := PackedVector2Array()
|
||||||
draw_circle(points[0], CIRCLE_RAD / Global.camera.zoom.x, default_color) # Starting circle
|
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])
|
arc_points.append(points[1])
|
||||||
|
|
||||||
for point in arc_points:
|
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
|
if is_hidden: # Hidden line
|
||||||
return
|
return
|
||||||
|
|
|
@ -75,8 +75,7 @@ func _input(_event: InputEvent):
|
||||||
if (
|
if (
|
||||||
Input.is_action_just_pressed("left_mouse")
|
Input.is_action_just_pressed("left_mouse")
|
||||||
and Global.can_draw
|
and Global.can_draw
|
||||||
and Global.has_focus
|
and mouse_point.distance_to(start) < 8 / Global.camera.zoom.x
|
||||||
and mouse_point.distance_to(start) < Global.camera.zoom.x * 8
|
|
||||||
):
|
):
|
||||||
if (
|
if (
|
||||||
!Rect2(Vector2.ZERO, project_size).has_point(Global.canvas.current_pixel)
|
!Rect2(Vector2.ZERO, project_size).has_point(Global.canvas.current_pixel)
|
||||||
|
|
|
@ -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 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 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 project_list := $"%TargetProjectOption" as OptionButton
|
||||||
@onready var folder_button := $"%Folder" as Button
|
|
||||||
@onready var start_button := $"%Start" as Button
|
@onready var start_button := $"%Start" as Button
|
||||||
@onready var size_label := $"%Size" as Label
|
@onready var size_label := $"%Size" as Label
|
||||||
@onready var path_field := $"%Path" as LineEdit
|
@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:
|
func _ready() -> void:
|
||||||
|
@ -40,10 +42,9 @@ func initialize_recording() -> void:
|
||||||
current_frame_no = skip_amount - 1
|
current_frame_no = skip_amount - 1
|
||||||
|
|
||||||
# disable some options that are not required during recording
|
# disable some options that are not required during recording
|
||||||
folder_button.visible = true
|
|
||||||
project_list.visible = false
|
project_list.visible = false
|
||||||
$ScrollContainer/CenterContainer/GridContainer/Captured.visible = true
|
captured_label.visible = true
|
||||||
for child in $Dialogs/Options/PanelContainer/VBoxContainer.get_children():
|
for child in options_container.get_children():
|
||||||
if !child.is_in_group("visible during recording"):
|
if !child.is_in_group("visible during recording"):
|
||||||
child.visible = false
|
child.visible = false
|
||||||
|
|
||||||
|
@ -77,11 +78,10 @@ func capture_frame() -> void:
|
||||||
DrawingAlgos.blend_layers(image, frame, Vector2i.ZERO, project)
|
DrawingAlgos.blend_layers(image, frame, Vector2i.ZERO, project)
|
||||||
|
|
||||||
if mode == Mode.CANVAS:
|
if mode == Mode.CANVAS:
|
||||||
if resize != 100:
|
if resize_percent != 100:
|
||||||
|
var resize := resize_percent / 100
|
||||||
image.resize(
|
image.resize(
|
||||||
image.get_size().x * resize / 100,
|
image.get_width() * resize, image.get_height() * resize, Image.INTERPOLATE_NEAREST
|
||||||
image.get_size().y * resize / 100,
|
|
||||||
Image.INTERPOLATE_NEAREST
|
|
||||||
)
|
)
|
||||||
|
|
||||||
cache.append(image)
|
cache.append(image)
|
||||||
|
@ -102,7 +102,7 @@ func save_frame(img: Image) -> void:
|
||||||
|
|
||||||
func _on_frame_saved() -> void:
|
func _on_frame_saved() -> void:
|
||||||
frame_captured += 1
|
frame_captured += 1
|
||||||
$ScrollContainer/CenterContainer/GridContainer/Captured.text = str("Saved: ", frame_captured)
|
captured_label.text = str("Saved: ", frame_captured)
|
||||||
|
|
||||||
|
|
||||||
func finalize_recording() -> void:
|
func finalize_recording() -> void:
|
||||||
|
@ -111,10 +111,9 @@ func finalize_recording() -> void:
|
||||||
save_frame(img)
|
save_frame(img)
|
||||||
cache.clear()
|
cache.clear()
|
||||||
disconnect_undo()
|
disconnect_undo()
|
||||||
folder_button.visible = false
|
|
||||||
project_list.visible = true
|
project_list.visible = true
|
||||||
$ScrollContainer/CenterContainer/GridContainer/Captured.visible = false
|
captured_label.visible = false
|
||||||
for child in $Dialogs/Options/PanelContainer/VBoxContainer.get_children():
|
for child in options_container.get_children():
|
||||||
child.visible = true
|
child.visible = true
|
||||||
if mode == Mode.PIXELORAMA:
|
if mode == Mode.PIXELORAMA:
|
||||||
size_label.get_parent().visible = false
|
size_label.get_parent().visible = false
|
||||||
|
@ -152,9 +151,7 @@ func _on_Start_toggled(button_pressed: bool) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _on_Settings_pressed() -> void:
|
func _on_Settings_pressed() -> void:
|
||||||
var settings := $Dialogs/Options as Window
|
options_dialog.popup(Rect2(position, options_dialog.size))
|
||||||
var pos := position
|
|
||||||
settings.popup(Rect2(pos, settings.size))
|
|
||||||
|
|
||||||
|
|
||||||
func _on_SkipAmount_value_changed(value: float) -> void:
|
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:
|
func _on_SpinBox_value_changed(value: float) -> void:
|
||||||
resize = value
|
resize_percent = value
|
||||||
var new_size: Vector2 = project.size * (resize / 100.0)
|
var new_size: Vector2 = project.size * (resize_percent / 100.0)
|
||||||
size_label.text = str("(", new_size.x, "×", new_size.y, ")")
|
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
|
$Dialogs/Path.current_dir = chosen_dir
|
||||||
|
|
||||||
|
|
||||||
func _on_Open_pressed() -> void:
|
func _on_open_folder_pressed() -> void:
|
||||||
OS.shell_open(path_field.text)
|
OS.shell_open(path_field.text)
|
||||||
|
|
||||||
|
|
||||||
|
@ -189,13 +186,3 @@ func _on_Path_dir_selected(dir: String) -> void:
|
||||||
chosen_dir = dir
|
chosen_dir = dir
|
||||||
path_field.text = chosen_dir
|
path_field.text = chosen_dir
|
||||||
start_button.disabled = false
|
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()
|
|
||||||
|
|
|
@ -26,7 +26,8 @@ layout_mode = 2
|
||||||
size_flags_vertical = 0
|
size_flags_vertical = 0
|
||||||
columns = 4
|
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
|
visible = false
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
|
||||||
|
@ -80,16 +81,14 @@ offset_bottom = 10.5
|
||||||
texture = ExtResource("3")
|
texture = ExtResource("3")
|
||||||
stretch_mode = 6
|
stretch_mode = 6
|
||||||
|
|
||||||
[node name="Folder" type="Button" parent="ScrollContainer/CenterContainer/GridContainer"]
|
[node name="OpenFolder" type="Button" parent="ScrollContainer/CenterContainer/GridContainer"]
|
||||||
unique_name_in_owner = true
|
|
||||||
visible = false
|
|
||||||
custom_minimum_size = Vector2(32, 32)
|
custom_minimum_size = Vector2(32, 32)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
tooltip_text = "Open Folder"
|
tooltip_text = "Open Folder"
|
||||||
mouse_default_cursor_shape = 2
|
mouse_default_cursor_shape = 2
|
||||||
toggle_mode = true
|
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
|
layout_mode = 0
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
anchor_bottom = 1.0
|
anchor_bottom = 1.0
|
||||||
|
@ -104,110 +103,95 @@ stretch_mode = 6
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
mouse_filter = 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)
|
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
|
anchors_preset = 15
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
anchor_bottom = 1.0
|
anchor_bottom = 1.0
|
||||||
offset_left = 9.0
|
offset_left = 8.0
|
||||||
offset_top = 9.0
|
offset_top = 8.0
|
||||||
offset_right = -9.0
|
offset_right = -8.0
|
||||||
offset_bottom = -9.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
|
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
|
layout_mode = 2
|
||||||
theme_override_constants/separation = 0
|
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
|
layout_mode = 2
|
||||||
theme_type_variation = &"HeaderSmall"
|
theme_type_variation = &"HeaderSmall"
|
||||||
text = "Interval"
|
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
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
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
|
layout_mode = 2
|
||||||
alignment = 1
|
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
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
text = "Capture frame every"
|
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
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
suffix = "actions"
|
||||||
|
|
||||||
[node name="Label2" type="Label" parent="Dialogs/Options/PanelContainer/VBoxContainer/ActionGap"]
|
[node name="ModeHeader" type="HBoxContainer" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer" groups=["visible during recording"]]
|
||||||
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"]]
|
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_constants/separation = 0
|
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
|
layout_mode = 2
|
||||||
theme_type_variation = &"HeaderSmall"
|
theme_type_variation = &"HeaderSmall"
|
||||||
text = "Mode"
|
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
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
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
|
layout_mode = 2
|
||||||
alignment = 1
|
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
|
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
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
mouse_default_cursor_shape = 2
|
||||||
|
|
||||||
[node name="Label2" type="Label" parent="Dialogs/Options/PanelContainer/VBoxContainer/ModeType"]
|
[node name="OutputScale" type="HBoxContainer" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer"]
|
||||||
layout_mode = 2
|
|
||||||
text = "Pixelorama"
|
|
||||||
|
|
||||||
[node name="OutputScale" type="HBoxContainer" parent="Dialogs/Options/PanelContainer/VBoxContainer"]
|
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
alignment = 1
|
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
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
text = "Output Scale:"
|
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
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
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
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
mouse_default_cursor_shape = 2
|
mouse_default_cursor_shape = 2
|
||||||
min_value = 50.0
|
min_value = 50.0
|
||||||
max_value = 1000.0
|
max_value = 1000.0
|
||||||
|
@ -216,49 +200,34 @@ value = 100.0
|
||||||
allow_greater = true
|
allow_greater = true
|
||||||
suffix = "%"
|
suffix = "%"
|
||||||
|
|
||||||
[node name="PathHeader" type="HBoxContainer" parent="Dialogs/Options/PanelContainer/VBoxContainer"]
|
[node name="PathHeader" type="HBoxContainer" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_constants/separation = 0
|
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
|
layout_mode = 2
|
||||||
theme_type_variation = &"HeaderSmall"
|
theme_type_variation = &"HeaderSmall"
|
||||||
text = "Path"
|
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
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
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
|
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
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
placeholder_text = "Choose Destination --->"
|
placeholder_text = "Choose destination"
|
||||||
editable = false
|
editable = false
|
||||||
|
|
||||||
[node name="Open" type="Button" parent="Dialogs/Options/PanelContainer/VBoxContainer/PathContainer"]
|
[node name="Choose" type="Button" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/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"]
|
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
text = "Choose"
|
text = "Choose"
|
||||||
|
|
||||||
[node name="Path" type="FileDialog" parent="Dialogs"]
|
[node name="Path" type="FileDialog" parent="Dialogs" groups=["FileDialogs"]]
|
||||||
mode = 2
|
mode = 2
|
||||||
exclusive = false
|
exclusive = false
|
||||||
popup_window = true
|
popup_window = true
|
||||||
|
@ -270,13 +239,10 @@ access = 2
|
||||||
[connection signal="pressed" from="ScrollContainer/CenterContainer/GridContainer/TargetProjectOption" to="." method="_on_TargetProjectOption_pressed"]
|
[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="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/Settings" to="." method="_on_Settings_pressed"]
|
||||||
[connection signal="pressed" from="ScrollContainer/CenterContainer/GridContainer/Folder" to="." method="_on_Open_pressed"]
|
[connection signal="pressed" from="ScrollContainer/CenterContainer/GridContainer/OpenFolder" to="." method="_on_open_folder_pressed"]
|
||||||
[connection signal="close_requested" from="Dialogs/Options" to="." method="_on_options_close_requested"]
|
[connection signal="value_changed" from="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/ActionGap/SkipAmount" to="." method="_on_SkipAmount_value_changed"]
|
||||||
[connection signal="value_changed" from="Dialogs/Options/PanelContainer/VBoxContainer/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/Options/PanelContainer/VBoxContainer/Fps/Fps" to="." method="_on_Fps_value_changed"]
|
[connection signal="value_changed" from="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/OutputScale/Resize" to="." method="_on_SpinBox_value_changed"]
|
||||||
[connection signal="toggled" from="Dialogs/Options/PanelContainer/VBoxContainer/ModeType/Mode" to="." method="_on_Mode_toggled"]
|
[connection signal="pressed" from="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/PathContainer/Choose" to="." method="_on_Choose_pressed"]
|
||||||
[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="dir_selected" from="Dialogs/Path" to="." method="_on_Path_dir_selected"]
|
[connection signal="dir_selected" from="Dialogs/Path" to="." method="_on_Path_dir_selected"]
|
||||||
[connection signal="timeout" from="Timer" to="." method="_on_Timer_timeout"]
|
[connection signal="timeout" from="Timer" to="." method="_on_Timer_timeout"]
|
||||||
|
|
|
@ -953,7 +953,7 @@ mouse_filter = 2
|
||||||
color = Color(0, 0.741176, 1, 0.501961)
|
color = Color(0, 0.741176, 1, 0.501961)
|
||||||
|
|
||||||
[node name="PasteTagPopup" type="Popup" parent="."]
|
[node name="PasteTagPopup" type="Popup" parent="."]
|
||||||
size = Vector2i(250, 574)
|
size = Vector2i(250, 335)
|
||||||
min_size = Vector2i(250, 0)
|
min_size = Vector2i(250, 0)
|
||||||
script = ExtResource("12")
|
script = ExtResource("12")
|
||||||
|
|
||||||
|
@ -992,6 +992,7 @@ layout_mode = 2
|
||||||
text = "Create new tags"
|
text = "Create new tags"
|
||||||
|
|
||||||
[node name="Instructions" type="Label" parent="PasteTagPopup/PanelContainer/VBoxContainer"]
|
[node name="Instructions" type="Label" parent="PasteTagPopup/PanelContainer/VBoxContainer"]
|
||||||
|
custom_minimum_size = Vector2(250, 23)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
text = "Available tags:"
|
text = "Available tags:"
|
||||||
autowrap_mode = 3
|
autowrap_mode = 3
|
||||||
|
@ -1004,6 +1005,7 @@ size_flags_vertical = 3
|
||||||
|
|
||||||
[node name="StartFrame" type="Label" parent="PasteTagPopup/PanelContainer/VBoxContainer"]
|
[node name="StartFrame" type="Label" parent="PasteTagPopup/PanelContainer/VBoxContainer"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(250, 23)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
autowrap_mode = 3
|
autowrap_mode = 3
|
||||||
|
|
|
@ -3,11 +3,17 @@ extends FlowContainer
|
||||||
var pen_inverted := false
|
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:
|
func _input(event: InputEvent) -> void:
|
||||||
if event is InputEventMouseMotion:
|
if event is InputEventMouseMotion:
|
||||||
pen_inverted = event.pen_inverted
|
pen_inverted = event.pen_inverted
|
||||||
return
|
return
|
||||||
if not Global.has_focus or not Global.can_draw:
|
if not Global.can_draw:
|
||||||
return
|
return
|
||||||
for action in ["undo", "redo"]:
|
for action in ["undo", "redo"]:
|
||||||
if event.is_action_pressed(action):
|
if event.is_action_pressed(action):
|
||||||
|
|
|
@ -398,8 +398,6 @@ func _popup_dialog(dialog: Window, dialog_size := Vector2i.ZERO) -> void:
|
||||||
|
|
||||||
|
|
||||||
func file_menu_id_pressed(id: int) -> void:
|
func file_menu_id_pressed(id: int) -> void:
|
||||||
if not Global.can_draw:
|
|
||||||
return
|
|
||||||
match id:
|
match id:
|
||||||
Global.FileMenu.NEW:
|
Global.FileMenu.NEW:
|
||||||
_on_new_project_file_menu_option_pressed()
|
_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:
|
func edit_menu_id_pressed(id: int) -> void:
|
||||||
if not Global.can_draw:
|
|
||||||
return
|
|
||||||
match id:
|
match id:
|
||||||
Global.EditMenu.UNDO:
|
Global.EditMenu.UNDO:
|
||||||
Global.current_project.commit_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:
|
func view_menu_id_pressed(id: int) -> void:
|
||||||
if not Global.can_draw:
|
|
||||||
return
|
|
||||||
match id:
|
match id:
|
||||||
Global.ViewMenu.TILE_MODE_OFFSETS:
|
Global.ViewMenu.TILE_MODE_OFFSETS:
|
||||||
_popup_dialog(Global.control.get_node("Dialogs/TileModeOffsetsDialog"))
|
_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:
|
func window_menu_id_pressed(id: int) -> void:
|
||||||
if not Global.can_draw:
|
|
||||||
return
|
|
||||||
match id:
|
match id:
|
||||||
Global.WindowMenu.WINDOW_OPACITY:
|
Global.WindowMenu.WINDOW_OPACITY:
|
||||||
_popup_dialog(window_opacity_dialog)
|
_popup_dialog(window_opacity_dialog)
|
||||||
|
@ -703,8 +695,6 @@ func _toggle_fullscreen() -> void:
|
||||||
|
|
||||||
|
|
||||||
func image_menu_id_pressed(id: int) -> void:
|
func image_menu_id_pressed(id: int) -> void:
|
||||||
if not Global.can_draw:
|
|
||||||
return
|
|
||||||
match id:
|
match id:
|
||||||
Global.ImageMenu.SCALE_IMAGE:
|
Global.ImageMenu.SCALE_IMAGE:
|
||||||
_popup_dialog(Global.control.get_node("Dialogs/ImageEffects/ScaleImage"))
|
_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:
|
func select_menu_id_pressed(id: int) -> void:
|
||||||
if not Global.can_draw:
|
|
||||||
return
|
|
||||||
match id:
|
match id:
|
||||||
Global.SelectMenu.SELECT_ALL:
|
Global.SelectMenu.SELECT_ALL:
|
||||||
Global.canvas.selection.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:
|
func help_menu_id_pressed(id: int) -> void:
|
||||||
if not Global.can_draw:
|
|
||||||
return
|
|
||||||
match id:
|
match id:
|
||||||
Global.HelpMenu.VIEW_SPLASH_SCREEN:
|
Global.HelpMenu.VIEW_SPLASH_SCREEN:
|
||||||
_popup_dialog(Global.control.get_node("Dialogs/SplashDialog"))
|
_popup_dialog(Global.control.get_node("Dialogs/SplashDialog"))
|
||||||
|
|
|
@ -12,13 +12,11 @@ func _ready() -> void:
|
||||||
|
|
||||||
func _on_ViewportContainer_mouse_entered() -> void:
|
func _on_ViewportContainer_mouse_entered() -> void:
|
||||||
camera.set_process_input(true)
|
camera.set_process_input(true)
|
||||||
Global.has_focus = true
|
|
||||||
Global.control.left_cursor.visible = Global.show_left_tool_icon
|
Global.control.left_cursor.visible = Global.show_left_tool_icon
|
||||||
Global.control.right_cursor.visible = Global.show_right_tool_icon
|
Global.control.right_cursor.visible = Global.show_right_tool_icon
|
||||||
|
|
||||||
|
|
||||||
func _on_ViewportContainer_mouse_exited() -> void:
|
func _on_ViewportContainer_mouse_exited() -> void:
|
||||||
camera.set_process_input(false)
|
camera.set_process_input(false)
|
||||||
Global.has_focus = false
|
|
||||||
Global.control.left_cursor.visible = false
|
Global.control.left_cursor.visible = false
|
||||||
Global.control.right_cursor.visible = false
|
Global.control.right_cursor.visible = false
|
||||||
|
|
Loading…
Reference in a new issue