1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-01-18 17:19:50 +00:00

Compare commits

..

30 commits

Author SHA1 Message Date
Variable ed98b4d37b
Merge branch 'Orama-Interactive:master' into reference-UI 2024-01-25 23:22:44 +05:00
Emmanouil Papadeas b126e95b64 Almost made selection rotation with gizmos functional
Not exposed yet
2024-01-25 02:33:41 +02:00
Emmanouil Papadeas 3d04a8d276 Selection rotation with gizmos works on selections without content now
Still not ready and thus not exposed
2024-01-25 01:35:42 +02:00
Emmanouil Papadeas 964e9fbd26 Don't set the selection_map of the project to the original_bitmap, if the latter is empty
Shouldn't happen, but best to check in case it does. Setting empty data to the selection_map breaks selections.
2024-01-25 01:06:30 +02:00
Emmanouil Papadeas de5db85345 When resizing a selection with gizmos or from the tool options, only set the original_bitmap when we're not already transforming content 2024-01-25 00:59:53 +02:00
Emmanouil Papadeas f8b32762a1 Fix canceling selection content resizing breaking the selection 2024-01-25 00:45:49 +02:00
Emmanouil Papadeas 56fe1840e0 Make selections scale properly even if they don't transform any image content
Fixes #774.
2024-01-25 00:40:53 +02:00
Emmanouil Papadeas 3a0977ce21 Some code cleanup in Selection.gd 2024-01-25 00:11:19 +02:00
Emmanouil Papadeas ce7a5e77ba Add a single window mode setting in the preferences
True by default, when set to false the UI uses multiple windows
2024-01-24 18:31:22 +02:00
Emmanouil Papadeas f0a5637d8a Some recorder UI improvements
Removed the fps option completely as it doesn't have any effects to exported static images. Should be re-introduced once we add video exporting with ffmpeg though.
2024-01-24 14:41:15 +02:00
Emmanouil Papadeas 5297fe6a80 Forgot to format the previous commit :( 2024-01-24 04:22:45 +02:00
Emmanouil Papadeas d640b6a979 Add a setting to allow usage of native file dialogs in the preferences
Closes #274 and implements #568, at long last! Some issues remain:
- The native save pxo dialog doesn't have an "Include blended images" option. This will be fixed once https://github.com/godotengine/godot/pull/83480 is merged.
- When a native file dialog closes, the interface still remains dimmed.
- In the export dialog, the "Browse" file dialog will also close the export dialog itself when it closes, when it's native.
2024-01-24 04:20:46 +02:00
Emmanouil Papadeas 4bc0fba941 Add a variable in Global for setting file dialogs as native, and add a "FileDialogs" node group
This settings is not exposed in the preferences in this commit
2024-01-24 03:57:40 +02:00
Emmanouil Papadeas b08420d09d Don't change the value of can_draw in Global.dialog_open()
In Godot 4 dialogs seem to be blocking the input to the rest of the UI, so this may not be needed
2024-01-24 03:19:57 +02:00
Emmanouil Papadeas 4e9b657077 Remove Global.has_focus completely
Might be a risky change, but I haven't noticed any bugs so far
2024-01-24 03:14:11 +02:00
Emmanouil Papadeas 42de5ccb29 Remove unneeded has_focus checks 2024-01-24 03:06:09 +02:00
Emmanouil Papadeas d9c0cd7546 Remove Global.can_draw conditions from TopMenuContainer
These were needed with Godot 3 to ensure that you couldn't open any other dialog when a dialog is already open, by using keyboard shortcuts. This no longer seems to be required in Godot 4.
2024-01-24 02:37:28 +02:00
Emmanouil Papadeas 3a852e44ff Add a popup_error() method in Global
Just to reduce code duplication and make error appearing easier
2024-01-24 02:27:57 +02:00
Emmanouil Papadeas 43d241a5c2
Video exporting by calling FFMPEG externally (#980)
* Basic mp4 exporting, needs ffmpeg

* Add avi, ogv and mkv file exporting

* Add webm exporting

* Set ffmpeg path in the preferences

* Show an error message if the video fails to export

* Make sure to delete the temp files even if video exporting fails
2024-01-24 02:00:17 +02:00
Emmanouil Papadeas 204eff8184 Fix selection being incorrect when the image is being scaled (mostly when being made smaller). 2024-01-23 19:38:21 +02:00
Emmanouil Papadeas e7a469fa4d Refactor scale methods of DrawingAlgos 2024-01-23 19:25:32 +02:00
Emmanouil Papadeas 6448b7ee7c Remove unnecessary Array casting in DrawingAlgos 2024-01-23 19:07:35 +02:00
Emmanouil Papadeas 519fa77791 Add a recent colors section to the color picker
Sort-of implements #859
2024-01-23 03:57:31 +02:00
Variable f43f80cee0
Integrate Extension Explorer (#910)
* Integration of ExtensionExplorer

---------

Co-authored-by: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com>
Co-authored-by: Emmanouil Papadeas <manoschool@yahoo.gr>
2024-01-23 03:25:37 +02:00
Variable ddce0393dd
Fixed some issues with perspective lines and removed unneeded code from the rotate image dialog (#979)
* fix drag gizmo of perspectice lines

* fix more stuff
2024-01-22 00:09:57 +02:00
Emmanouil Papadeas bec7ceb974 Fix Global.can_draw being set to true all the time
Note for the future, perhaps using _unhandled_input() might remove the need of this variable.
2024-01-21 19:46:01 +02:00
Variable 561a374cc0
Fixed rotation gixmo (#976) 2024-01-20 02:06:44 +02:00
Emmanouil Papadeas bc8fadb67c Fix cel button image texture breaking when the image gets resized 2024-01-19 18:25:50 +02:00
Emmanouil Papadeas 73e40ed127 Add a ShaderInclude file for common shader rotation code 2024-01-18 00:19:30 +02:00
Emmanouil Papadeas aa77dcb61f Selection rotation with gizmo improvements, still not usable and not exposed 2024-01-17 20:04:19 +02:00
48 changed files with 1440 additions and 518 deletions

View file

@ -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 ""

View file

@ -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(

View file

@ -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

View file

@ -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:

View file

@ -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)

View file

@ -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

View file

@ -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"

View file

@ -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():

View file

@ -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:

View file

@ -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")

View file

@ -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)

View file

@ -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):

View file

@ -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"]

View 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;
}

View file

@ -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
} }

View file

@ -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;
} }

View file

@ -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
} }

View file

@ -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)

View file

@ -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)

View file

@ -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()

View file

@ -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()

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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 (

View file

@ -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()

View file

@ -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)

View file

@ -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!"

View file

@ -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)

View file

@ -250,13 +250,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
) )
@ -267,8 +267,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

View file

@ -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

View file

@ -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

View file

@ -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

View 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()

View 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"]

View 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

View 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"]

View 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")

View 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()

View 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

View file

@ -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

View file

@ -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)

View file

@ -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()

View file

@ -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"]

View file

@ -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

View file

@ -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):

View file

@ -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"))

View file

@ -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