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

Merge branch 'Orama-Interactive:master' into live-fixes

This commit is contained in:
Variable 2024-01-25 23:21:32 +05:00 committed by GitHub
commit 4c316c2ff5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 1440 additions and 518 deletions

View file

@ -743,6 +743,22 @@ msgstr ""
msgid "Dim interface on dialog popup"
msgstr ""
#. Found in the preferences, under the interface section. When this setting is enabled, the native file dialogs of the operating system are being used, instead of Pixelorama's custom ones.
msgid "Use native file dialogs"
msgstr ""
#. Found in the preferences, tooltip of the "Use native file dialogs" option.
msgid "When this setting is enabled, the native file dialogs of the operating system are being used, instead of Pixelorama's custom ones."
msgstr ""
#. Found in the preferences, under the interface section. When this setting is enabled, Pixelorama's subwindows will be embedded in the main window, otherwise each dialog will be its own separate window.
msgid "Single window mode"
msgstr ""
#. Found in the preferences, tooltip of the "Single window mode" option.
msgid "When this setting is enabled, Pixelorama's subwindows will be embedded in the main window, otherwise each dialog will be its own separate window."
msgstr ""
msgid "Dark"
msgstr ""
@ -2305,6 +2321,10 @@ msgstr ""
msgid "Quit confirmation"
msgstr ""
#. Found in the preferences, under the startup section. Path is a noun and it refers to the location in the device where FFMPEG is located at. FFMPEG is a software name and it should not be translated. See https://en.wikipedia.org/wiki/Path_(computing)
msgid "FFMPEG path"
msgstr ""
msgid "Enable autosave"
msgstr ""

View file

@ -21,7 +21,7 @@ func blend_layers(
# the second are the opacities and the third are the origins
var metadata_image := Image.create(project.layers.size(), 3, false, Image.FORMAT_R8)
var frame_index := project.frames.find(frame)
var previous_ordered_layers: Array[int] = Array(project.ordered_layers)
var previous_ordered_layers: Array[int] = project.ordered_layers
project.order_layers(frame_index)
for i in project.layers.size():
var ordered_index := project.ordered_layers[i]
@ -53,7 +53,7 @@ func blend_layers(
gen.generate_image(blended, blend_layers_shader, params, project.size)
image.blend_rect(blended, Rect2i(Vector2i.ZERO, project.size), origin)
# Re-order the layers again to ensure correct canvas drawing
project.ordered_layers = Array(previous_ordered_layers)
project.ordered_layers = previous_ordered_layers
## Algorithm based on http://members.chello.at/easyfilter/bresenham.html
@ -425,6 +425,8 @@ func fake_rotsprite(sprite: Image, angle: float, pivot: Vector2) -> void:
func nn_rotate(sprite: Image, angle: float, pivot: Vector2) -> void:
if is_zero_approx(angle):
return
var aux := Image.new()
aux.copy_from(sprite)
var ox: int
@ -456,37 +458,6 @@ func color_distance(c1: Color, c2: Color) -> float:
# Image effects
func scale_image(width: int, height: int, interpolation: int) -> void:
general_do_scale(width, height)
for f in Global.current_project.frames:
for i in range(f.cels.size() - 1, -1, -1):
var cel := f.cels[i]
if not cel is PixelCel:
continue
var sprite := Image.new()
sprite.copy_from(cel.get_image())
if interpolation == Interpolation.SCALE3X:
var times := Vector2i(
ceili(width / (3.0 * sprite.get_width())),
ceili(height / (3.0 * sprite.get_height()))
)
for _j in range(maxi(times.x, times.y)):
sprite.copy_from(scale_3x(sprite))
sprite.resize(width, height, Image.INTERPOLATE_NEAREST)
elif interpolation == Interpolation.CLEANEDGE:
var gen := ShaderImageEffect.new()
gen.generate_image(sprite, clean_edge_shader, {}, Vector2i(width, height))
elif interpolation == Interpolation.OMNISCALE and omniscale_shader:
var gen := ShaderImageEffect.new()
gen.generate_image(sprite, omniscale_shader, {}, Vector2i(width, height))
else:
sprite.resize(width, height, interpolation)
Global.undo_redo_compress_images({cel.image: sprite.data}, {cel.image: cel.image.data})
general_undo_scale()
func center(indices: Array) -> void:
var project := Global.current_project
Global.canvas.selection.transform_content_confirm()
@ -517,22 +488,56 @@ func center(indices: Array) -> void:
project.undo_redo.commit_action()
func scale_image(width: int, height: int, interpolation: int) -> void:
var redo_data := {}
var undo_data := {}
for f in Global.current_project.frames:
for i in range(f.cels.size() - 1, -1, -1):
var cel := f.cels[i]
if not cel is PixelCel:
continue
var sprite := Image.new()
sprite.copy_from(cel.get_image())
if interpolation == Interpolation.SCALE3X:
var times := Vector2i(
ceili(width / (3.0 * sprite.get_width())),
ceili(height / (3.0 * sprite.get_height()))
)
for _j in range(maxi(times.x, times.y)):
sprite.copy_from(scale_3x(sprite))
sprite.resize(width, height, Image.INTERPOLATE_NEAREST)
elif interpolation == Interpolation.CLEANEDGE:
var gen := ShaderImageEffect.new()
gen.generate_image(sprite, clean_edge_shader, {}, Vector2i(width, height))
elif interpolation == Interpolation.OMNISCALE and omniscale_shader:
var gen := ShaderImageEffect.new()
gen.generate_image(sprite, omniscale_shader, {}, Vector2i(width, height))
else:
sprite.resize(width, height, interpolation)
redo_data[cel.image] = sprite.data
undo_data[cel.image] = cel.image.data
general_do_and_undo_scale(width, height, redo_data, undo_data)
## Sets the size of the project to be the same as the size of the active selection.
func crop_to_selection() -> void:
if not Global.current_project.has_selection:
return
var redo_data := {}
var undo_data := {}
Global.canvas.selection.transform_content_confirm()
var rect: Rect2i = Global.canvas.selection.big_bounding_rectangle
general_do_scale(rect.size.x, rect.size.y)
# Loop through all the cels to crop them
for f in Global.current_project.frames:
for cel in f.cels:
if not cel is PixelCel:
continue
var sprite := cel.get_image().get_region(rect)
Global.undo_redo_compress_images({cel.image: sprite.data}, {cel.image: cel.image.data})
redo_data[cel.image] = sprite.data
undo_data[cel.image] = cel.image.data
general_undo_scale()
general_do_and_undo_scale(rect.size.x, rect.size.y, redo_data, undo_data)
## Automatically makes the project smaller by looping through all of the cels and
@ -559,20 +564,23 @@ func crop_to_content() -> void:
var width := used_rect.size.x
var height := used_rect.size.y
general_do_scale(width, height)
var redo_data := {}
var undo_data := {}
# Loop through all the cels to trim them
for f in Global.current_project.frames:
for cel in f.cels:
if not cel is PixelCel:
continue
var sprite := cel.get_image().get_region(used_rect)
Global.undo_redo_compress_images({cel.image: sprite.data}, {cel.image: cel.image.data})
redo_data[cel.image] = sprite.data
undo_data[cel.image] = cel.image.data
general_undo_scale()
general_do_and_undo_scale(width, height, redo_data, undo_data)
func resize_canvas(width: int, height: int, offset_x: int, offset_y: int) -> void:
general_do_scale(width, height)
var redo_data := {}
var undo_data := {}
for f in Global.current_project.frames:
for cel in f.cels:
if not cel is PixelCel:
@ -583,17 +591,26 @@ func resize_canvas(width: int, height: int, offset_x: int, offset_y: int) -> voi
Rect2i(Vector2i.ZERO, Global.current_project.size),
Vector2i(offset_x, offset_y)
)
Global.undo_redo_compress_images({cel.image: sprite.data}, {cel.image: cel.image.data})
redo_data[cel.image] = sprite.data
undo_data[cel.image] = cel.image.data
general_undo_scale()
general_do_and_undo_scale(width, height, redo_data, undo_data)
func general_do_scale(width: int, height: int) -> void:
func general_do_and_undo_scale(
width: int, height: int, redo_data: Dictionary, undo_data: Dictionary
) -> void:
var project := Global.current_project
var size := Vector2i(width, height)
var x_ratio := float(project.size.x) / width
var y_ratio := float(project.size.y) / height
var selection_map_copy := SelectionMap.new()
selection_map_copy.copy_from(project.selection_map)
selection_map_copy.crop(size.x, size.y)
redo_data[project.selection_map] = selection_map_copy.data
undo_data[project.selection_map] = project.selection_map.data
var new_x_symmetry_point := project.x_symmetry_point / x_ratio
var new_y_symmetry_point := project.y_symmetry_point / y_ratio
var new_x_symmetry_axis_points := project.x_symmetry_axis.points
@ -606,19 +623,12 @@ func general_do_scale(width: int, height: int) -> void:
project.undos += 1
project.undo_redo.create_action("Scale")
project.undo_redo.add_do_property(project, "size", size)
project.undo_redo.add_do_method(project.selection_map.crop.bind(size.x, size.y))
project.undo_redo.add_do_property(project, "x_symmetry_point", new_x_symmetry_point)
project.undo_redo.add_do_property(project, "y_symmetry_point", new_y_symmetry_point)
project.undo_redo.add_do_property(project.x_symmetry_axis, "points", new_x_symmetry_axis_points)
project.undo_redo.add_do_property(project.y_symmetry_axis, "points", new_y_symmetry_axis_points)
func general_undo_scale() -> void:
var project := Global.current_project
Global.undo_redo_compress_images(redo_data, undo_data)
project.undo_redo.add_undo_property(project, "size", project.size)
project.undo_redo.add_undo_method(
project.selection_map.crop.bind(project.size.x, project.size.y)
)
project.undo_redo.add_undo_property(project, "x_symmetry_point", project.x_symmetry_point)
project.undo_redo.add_undo_property(project, "y_symmetry_point", project.y_symmetry_point)
project.undo_redo.add_undo_property(

View file

@ -4,10 +4,24 @@ enum ExportTab { IMAGE = 0, SPRITESHEET = 1 }
enum Orientation { ROWS = 0, COLUMNS = 1 }
enum AnimationDirection { FORWARD = 0, BACKWARDS = 1, PING_PONG = 2 }
## See file_format_string, file_format_description, and ExportDialog.gd
enum FileFormat { PNG, WEBP, JPEG, GIF, APNG }
enum FileFormat { PNG, WEBP, JPEG, GIF, APNG, MP4, AVI, OGV, MKV, WEBM }
const TEMP_PATH := "user://tmp"
## List of animated formats
var animated_formats := [FileFormat.GIF, FileFormat.APNG]
var animated_formats := [
FileFormat.GIF,
FileFormat.APNG,
FileFormat.MP4,
FileFormat.AVI,
FileFormat.OGV,
FileFormat.MKV,
FileFormat.WEBM
]
var ffmpeg_formats := [
FileFormat.MP4, FileFormat.AVI, FileFormat.OGV, FileFormat.MKV, FileFormat.WEBM
]
## A dictionary of custom exporter generators (received from extensions)
var custom_file_formats := {}
@ -262,23 +276,28 @@ func export_processed_images(
return result
if is_single_file_format(project):
var exporter: AImgIOBaseExporter
if project.file_format == FileFormat.APNG:
exporter = AImgIOAPNGExporter.new()
if is_using_ffmpeg(project.file_format):
var video_exported := export_video(export_paths)
if not video_exported:
return false
else:
exporter = GIFAnimationExporter.new()
var details := {
"exporter": exporter,
"export_dialog": export_dialog,
"export_paths": export_paths,
"project": project
}
if not _multithreading_enabled():
export_animated(details)
else:
if gif_export_thread.is_started():
gif_export_thread.wait_to_finish()
gif_export_thread.start(export_animated.bind(details))
var exporter: AImgIOBaseExporter
if project.file_format == FileFormat.APNG:
exporter = AImgIOAPNGExporter.new()
else:
exporter = GIFAnimationExporter.new()
var details := {
"exporter": exporter,
"export_dialog": export_dialog,
"export_paths": export_paths,
"project": project
}
if not _multithreading_enabled():
export_animated(details)
else:
if gif_export_thread.is_started():
gif_export_thread.wait_to_finish()
gif_export_thread.start(export_animated.bind(details))
else:
var succeeded := true
for i in range(processed_images.size()):
@ -311,11 +330,9 @@ func export_processed_images(
elif project.file_format == FileFormat.JPEG:
err = processed_images[i].save_jpg(export_paths[i])
if err != OK:
Global.error_dialog.set_text(
Global.popup_error(
tr("File failed to save. Error code %s (%s)") % [err, error_string(err)]
)
Global.error_dialog.popup_centered()
Global.dialog_open(true)
succeeded = false
if succeeded:
Global.notification_label("File(s) exported")
@ -334,6 +351,37 @@ func export_processed_images(
return true
## Uses FFMPEG to export a video
func export_video(export_paths: PackedStringArray) -> bool:
DirAccess.make_dir_absolute(TEMP_PATH)
var temp_path_real := ProjectSettings.globalize_path(TEMP_PATH)
var input_file_path := temp_path_real.path_join("input.txt")
var input_file := FileAccess.open(input_file_path, FileAccess.WRITE)
for i in range(processed_images.size()):
var temp_file_name := str(i + 1).pad_zeros(number_of_digits) + ".png"
var temp_file_path := temp_path_real.path_join(temp_file_name)
processed_images[i].save_png(temp_file_path)
input_file.store_line("file '" + temp_file_name + "'")
input_file.store_line("duration %s" % durations[i])
input_file.close()
var ffmpeg_execute: PackedStringArray = [
"-y", "-f", "concat", "-i", input_file_path, export_paths[0]
]
var output := []
var success := OS.execute(Global.ffmpeg_path, ffmpeg_execute, output, true)
print(output)
var temp_dir := DirAccess.open(TEMP_PATH)
for file in temp_dir.get_files():
temp_dir.remove(file)
DirAccess.remove_absolute(TEMP_PATH)
if success < 0 or success > 1:
var fail_text := """Video failed to export. Make sure you have FFMPEG installed
and have set the correct path in the preferences."""
Global.popup_error(tr(fail_text))
return false
return true
func export_animated(args: Dictionary) -> void:
var project: Project = args["project"]
var exporter: AImgIOBaseExporter = args["exporter"]
@ -397,6 +445,16 @@ func file_format_string(format_enum: int) -> String:
return ".gif"
FileFormat.APNG:
return ".apng"
FileFormat.MP4:
return ".mp4"
FileFormat.AVI:
return ".avi"
FileFormat.OGV:
return ".ogv"
FileFormat.MKV:
return ".mkv"
FileFormat.WEBM:
return ".webm"
_:
# If a file format description is not found, try generating one
if custom_exporter_generators.has(format_enum):
@ -418,6 +476,16 @@ func file_format_description(format_enum: int) -> String:
return "GIF Image"
FileFormat.APNG:
return "APNG Image"
FileFormat.MP4:
return "MPEG-4 Video"
FileFormat.AVI:
return "AVI Video"
FileFormat.OGV:
return "OGV Video"
FileFormat.MKV:
return "Matroska Video"
FileFormat.WEBM:
return "WebM Video"
_:
# If a file format description is not found, try generating one
for key in custom_file_formats.keys():
@ -426,12 +494,25 @@ func file_format_description(format_enum: int) -> String:
return ""
## True when exporting to .gif and .apng (and potentially video formats in the future)
## False when exporting to .png, and other non-animated formats in the future
## True when exporting to .gif, .apng and video
## False when exporting to .png, .jpg and static .webp
func is_single_file_format(project := Global.current_project) -> bool:
return animated_formats.has(project.file_format)
func is_using_ffmpeg(format: FileFormat) -> bool:
return ffmpeg_formats.has(format)
func is_ffmpeg_installed() -> bool:
if Global.ffmpeg_path.is_empty():
return false
var ffmpeg_executed := OS.execute(Global.ffmpeg_path, [])
if ffmpeg_executed == 0 or ffmpeg_executed == 1:
return true
return false
func _create_export_path(multifile: bool, project: Project, frame := 0) -> String:
var path := project.file_name
# Only append frame number when there are multiple files exported

View file

@ -218,9 +218,7 @@ class DialogAPI:
## Shows an alert dialog with the given [param text].
## Useful for displaying messages like "Incompatible API" etc...
func show_error(text: String) -> void:
Global.error_dialog.set_text(text)
Global.error_dialog.popup_centered()
Global.dialog_open(true)
Global.popup_error(text)
## Returns the node that is the parent of dialogs used in pixelorama.
func get_dialogs_parent_node() -> Node:

View file

@ -117,8 +117,6 @@ var current_project_index := 0:
var can_draw := false
## (Intended to be used as getter only) Tells if the user allowed to move the guide while on canvas.
var move_guides_on_canvas := true
## Tells if the canvas in currently in focus.
var has_focus := false
var play_only_tags := true ## If [code]true[/code], animation plays only on frames of the same tag.
## (Intended to be used as getter only) Tells if the x-symmetry guide ( -- ) is visible.
@ -131,6 +129,8 @@ var show_y_symmetry_axis := false
var open_last_project := false
## Found in Preferences. If [code]true[/code], asks for permission to quit on exit.
var quit_confirmation := false
## Found in Preferences. Refers to the ffmpeg location path.
var ffmpeg_path := ""
## Found in Preferences. If [code]true[/code], the zoom is smooth.
var smooth_zoom := true
## Found in Preferences. If [code]true[/code], the zoom is restricted to integral multiples of 100%.
@ -148,16 +148,33 @@ var integer_zoom := false:
zoom_slider.step = 1
zoom_slider.value = zoom_slider.value # to trigger signal emission
## Found in Preferences. The scale of the Interface.
## Found in Preferences. The scale of the interface.
var shrink := 1.0
## Found in Preferences. The font size used by the Interface.
## Found in Preferences. The font size used by the interface.
var font_size := 16:
set(value):
font_size = value
control.theme.default_font_size = value
control.theme.set_font_size("font_size", "HeaderSmall", value + 2)
## Found in Preferences. If [code]true[/code], the Interface dims on popups.
## Found in Preferences. If [code]true[/code], the interface dims on popups.
var dim_on_popup := true
## Found in Preferences. If [code]true[/code], the native file dialogs of the
## operating system are being used, instead of Godot's FileDialog node.
var use_native_file_dialogs := false:
set(value):
use_native_file_dialogs = value
if not is_inside_tree():
await tree_entered
await get_tree().process_frame
get_tree().set_group(&"FileDialogs", "use_native_dialog", value)
## Found in Preferences. If [code]true[/code], subwindows are embedded in the main window.
var single_window_mode := true:
set(value):
single_window_mode = value
if OS.has_feature("editor"):
return
ProjectSettings.set_setting("display/window/subwindows/embed_subwindows", value)
ProjectSettings.save_custom(OVERRIDE_FILE)
## Found in Preferences. The modulation color (or simply color) of icons.
var modulate_icon_color := Color.GRAY
## Found in Preferences. Determines if [member modulate_icon_color] uses custom or theme color.
@ -535,6 +552,7 @@ func _init() -> void:
data_directories.append(default_loc.path_join(HOME_SUBDIR_NAME))
if ProjectSettings.get_setting("display/window/tablet_driver") == "winink":
tablet_driver = 1
single_window_mode = ProjectSettings.get_setting("display/window/subwindows/embed_subwindows")
func _ready() -> void:
@ -769,9 +787,7 @@ func undo_or_redo(
if current_cel is Cel3D:
current_cel.size_changed(project.size)
else:
current_cel.image_texture = ImageTexture.create_from_image(
current_cel.get_image()
)
current_cel.image_texture.set_image(current_cel.get_image())
canvas.camera_zoom()
canvas.grid.queue_redraw()
canvas.pixel_grid.queue_redraw()
@ -807,16 +823,19 @@ func _renderer_changed(value: int) -> void:
func dialog_open(open: bool) -> void:
var dim_color := Color.WHITE
if open:
can_draw = false
if dim_on_popup:
dim_color = Color(0.5, 0.5, 0.5)
else:
can_draw = true
var tween := create_tween().set_trans(Tween.TRANS_LINEAR).set_ease(Tween.EASE_OUT)
tween.tween_property(control, "modulate", dim_color, 0.1)
func popup_error(text: String) -> void:
error_dialog.set_text(text)
error_dialog.popup_centered()
dialog_open(true)
## sets the [member BaseButton.disabled] property of the [param button] to [param disable],
## changes the cursor shape for it accordingly, and dims/brightens any textures it may have.
func disable_button(button: BaseButton, disable: bool) -> void:
@ -1054,6 +1073,7 @@ func create_ui_for_shader_uniforms(
file_dialog.access = FileDialog.ACCESS_FILESYSTEM
file_dialog.size = Vector2(384, 281)
file_dialog.file_selected.connect(file_selected.bind(u_name))
file_dialog.use_native_dialog = use_native_file_dialogs
var button := Button.new()
button.text = "Load texture"
button.pressed.connect(file_dialog.popup_centered)

View file

@ -62,9 +62,7 @@ func handle_loading_file(file: String) -> void:
var image := Image.load_from_file(file)
if not is_instance_valid(image): # An error occurred
var file_name: String = file.get_file()
Global.error_dialog.set_text(tr("Can't load file '%s'.") % [file_name])
Global.error_dialog.popup_centered()
Global.dialog_open(true)
Global.popup_error(tr("Can't load file '%s'.") % [file_name])
return
handle_loading_image(file, image)
@ -159,11 +157,7 @@ func open_pxo_file(path: String, untitled_backup := false, replace_empty := true
if not success:
return
elif err != OK:
Global.error_dialog.set_text(
tr("File failed to open. Error code %s (%s)") % [err, error_string(err)]
)
Global.error_dialog.popup_centered()
Global.dialog_open(true)
Global.popup_error(tr("File failed to open. Error code %s (%s)") % [err, error_string(err)])
return
else:
var data_json := zip_reader.read_file("data.json").get_string_from_utf8()
@ -253,11 +247,7 @@ func open_v0_pxo_file(path: String, new_project: Project) -> bool:
file = FileAccess.open(path, FileAccess.READ)
var err := FileAccess.get_open_error()
if err != OK:
Global.error_dialog.set_text(
tr("File failed to open. Error code %s (%s)") % [err, error_string(err)]
)
Global.error_dialog.popup_centered()
Global.dialog_open(true)
Global.popup_error(tr("File failed to open. Error code %s (%s)") % [err, error_string(err)])
return false
var first_line := file.get_line()
@ -320,19 +310,11 @@ func save_pxo_file(
project.name = path.get_file().trim_suffix(".pxo")
var serialized_data := project.serialize()
if !serialized_data:
Global.error_dialog.set_text(
tr("File failed to save. Converting project data to dictionary failed.")
)
Global.error_dialog.popup_centered()
Global.dialog_open(true)
Global.popup_error(tr("File failed to save. Converting project data to dictionary failed."))
return false
var to_save := JSON.stringify(serialized_data)
if !to_save:
Global.error_dialog.set_text(
tr("File failed to save. Converting dictionary to JSON failed.")
)
Global.error_dialog.popup_centered()
Global.dialog_open(true)
Global.popup_error(tr("File failed to save. Converting dictionary to JSON failed."))
return false
# Check if a file with the same name exists. If it does, rename the new file temporarily.
@ -346,11 +328,7 @@ func save_pxo_file(
if err != OK:
if temp_path.is_valid_filename():
return false
Global.error_dialog.set_text(
tr("File failed to save. Error code %s (%s)") % [err, error_string(err)]
)
Global.error_dialog.popup_centered()
Global.dialog_open(true)
Global.popup_error(tr("File failed to save. Error code %s (%s)") % [err, error_string(err)])
if zip_packer: # this would be null if we attempt to save filenames such as "//\\||.pxo"
zip_packer.close()
return false

View file

@ -441,11 +441,7 @@ func import_palette_from_path(path: String, make_copy := false, is_initialising
new_palette_imported.emit()
select_palette(palette.name)
else:
Global.error_dialog.set_text(
tr("Can't load file '%s'.\nThis is not a valid palette file.") % [path]
)
Global.error_dialog.popup_centered()
Global.dialog_open(true)
Global.popup_error(tr("Can't load file '%s'.\nThis is not a valid palette file.") % [path])
## Refer to app/core/gimppalette-load.c of the GNU Image Manipulation Program for the "living spec"

View file

@ -541,8 +541,7 @@ func handle_draw(position: Vector2i, event: InputEvent) -> void:
var project := Global.current_project
var text := "[%s×%s]" % [project.size.x, project.size.y]
if Global.has_focus:
text += " %s, %s" % [position.x, position.y]
text += " %s, %s" % [position.x, position.y]
if not _slots[MOUSE_BUTTON_LEFT].tool_node.cursor_text.is_empty():
text += " %s" % _slots[MOUSE_BUTTON_LEFT].tool_node.cursor_text
if not _slots[MOUSE_BUTTON_RIGHT].tool_node.cursor_text.is_empty():

View file

@ -206,7 +206,6 @@ func _notification(what: int) -> void:
# If the mouse exits the window and another application has the focus,
# pause the application
NOTIFICATION_APPLICATION_FOCUS_OUT:
Global.has_focus = false
if Global.pause_when_unfocused:
get_tree().paused = true
NOTIFICATION_WM_MOUSE_EXIT:
@ -217,12 +216,6 @@ func _notification(what: int) -> void:
get_tree().paused = false
NOTIFICATION_APPLICATION_FOCUS_IN:
get_tree().paused = false
var mouse_pos := get_global_mouse_position()
var viewport_rect := Rect2(
Global.main_viewport.global_position, Global.main_viewport.size
)
if viewport_rect.has_point(mouse_pos):
Global.has_focus = true
func _on_files_dropped(files: PackedStringArray) -> void:
@ -248,9 +241,7 @@ func load_last_project() -> void:
Global.config_cache.set_value("data", "current_dir", file_path.get_base_dir())
else:
# If file doesn't exist on disk then warn user about this
Global.error_dialog.set_text("Cannot find last project file.")
Global.error_dialog.popup_centered()
Global.dialog_open(true)
Global.popup_error("Cannot find last project file.")
func load_recent_project_file(path: String) -> void:
@ -266,9 +257,7 @@ func load_recent_project_file(path: String) -> void:
Global.config_cache.set_value("data", "current_dir", path.get_base_dir())
else:
# If file doesn't exist on disk then warn user about this
Global.error_dialog.set_text("Cannot find project file.")
Global.error_dialog.popup_centered()
Global.dialog_open(true)
Global.popup_error("Cannot find project file.")
func _on_OpenSprite_files_selected(paths: PackedStringArray) -> void:

View file

@ -115,7 +115,7 @@ text = "Delete Palette?"
horizontal_alignment = 1
vertical_alignment = 1
[node name="ExportFileDialog" type="FileDialog" parent="."]
[node name="ExportFileDialog" type="FileDialog" parent="." groups=["FileDialogs"]]
size = Vector2i(677, 400)
access = 2
filters = PackedStringArray("*.png ; PNG Image", "*.jpg,*.jpeg ; JPEG Image", "*.webp ; WebP Image")

View file

@ -212,9 +212,7 @@ func read_extension(extension_file_or_folder_name: StringName, internal := false
"\n",
"But Pixelorama's API version is: %s" % ExtensionsApi.get_api_version()
)
Global.error_dialog.set_text(str(err_text, required_text))
Global.error_dialog.popup_centered()
Global.dialog_open(true)
Global.popup_error(str(err_text, required_text))
print("Incompatible API")
if !internal: # the file isn't created for internal extensions, no need for removal
# Don't put it in faulty, (it's merely incompatible)

View file

@ -7,9 +7,20 @@ var preferences: Array[Preference] = [
Preference.new(
"quit_confirmation", "Startup/StartupContainer/QuitConfirmation", "button_pressed"
),
Preference.new("ffmpeg_path", "Startup/StartupContainer/FFMPEGPath", "text"),
Preference.new("shrink", "%ShrinkSlider", "value"),
Preference.new("font_size", "Interface/InterfaceOptions/FontSizeSlider", "value"),
Preference.new("dim_on_popup", "Interface/InterfaceOptions/DimCheckBox", "button_pressed"),
Preference.new(
"use_native_file_dialogs", "Interface/InterfaceOptions/NativeFileDialogs", "button_pressed"
),
Preference.new(
"single_window_mode",
"Interface/InterfaceOptions/SingleWindowMode",
"button_pressed",
true,
true
),
Preference.new("icon_color_from", "Interface/ButtonOptions/IconColorOptionButton", "selected"),
Preference.new("custom_icon_color", "Interface/ButtonOptions/IconColorButton", "color"),
Preference.new("left_tool_color", "Interface/ButtonOptions/LeftToolColorButton", "color"),
@ -202,6 +213,10 @@ func _ready() -> void:
node.item_selected.connect(
_on_Preference_value_changed.bind(pref, restore_default_button)
)
"text":
node.text_changed.connect(
_on_Preference_value_changed.bind(pref, restore_default_button)
)
var global_value = Global.get(pref.prop_name)
if Global.config_cache.has_section_key("preferences", pref.prop_name):

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/HandleExtensions.gd" id="2"]
@ -7,11 +7,13 @@
[ext_resource type="Script" path="res://src/Preferences/HandleThemes.gd" id="5"]
[ext_resource type="PackedScene" path="res://src/UI/Nodes/ValueSliderV2.tscn" id="7"]
[ext_resource type="Script" path="res://src/UI/Nodes/ValueSlider.gd" id="8"]
[ext_resource type="PackedScene" uid="uid://chy5d42l72crk" path="res://src/UI/ExtensionExplorer/Store.tscn" id="8_jmnx8"]
[sub_resource type="ButtonGroup" id="ButtonGroup_8vsfb"]
[node name="PreferencesDialog" type="AcceptDialog"]
title = "Preferences"
position = Vector2i(0, 36)
size = Vector2i(800, 500)
exclusive = false
popup_window = true
@ -28,7 +30,7 @@ offset_bottom = -49.0
size_flags_horizontal = 3
theme_override_constants/separation = 20
theme_override_constants/autohide = 0
split_offset = 150
split_offset = 125
[node name="List" type="ItemList" parent="HSplitContainer"]
custom_minimum_size = Vector2(85, 0)
@ -90,6 +92,13 @@ layout_mode = 2
mouse_default_cursor_shape = 2
text = "On"
[node name="FFMPEGPathLabel" type="Label" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Startup/StartupContainer"]
layout_mode = 2
text = "FFMPEG path"
[node name="FFMPEGPath" type="LineEdit" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Startup/StartupContainer"]
layout_mode = 2
[node name="Language" type="VBoxContainer" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide"]
visible = false
layout_mode = 2
@ -200,6 +209,32 @@ mouse_default_cursor_shape = 2
button_pressed = true
text = "On"
[node name="NativeFileDialogsLabel" type="Label" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Interface/InterfaceOptions"]
layout_mode = 2
size_flags_horizontal = 3
text = "Use native file dialogs"
[node name="NativeFileDialogs" type="CheckBox" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Interface/InterfaceOptions"]
layout_mode = 2
size_flags_horizontal = 3
tooltip_text = "When this setting is enabled, the native file dialogs of the operating system are being used, instead of Pixelorama's custom ones."
mouse_default_cursor_shape = 2
button_pressed = true
text = "On"
[node name="SingleWindowModeLabel" type="Label" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Interface/InterfaceOptions"]
layout_mode = 2
size_flags_horizontal = 3
text = "Single window mode"
[node name="SingleWindowMode" type="CheckBox" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Interface/InterfaceOptions"]
layout_mode = 2
size_flags_horizontal = 3
tooltip_text = "When this setting is enabled, Pixelorama's subwindows will be embedded in the main window, otherwise each dialog will be its own separate window."
mouse_default_cursor_shape = 2
button_pressed = true
text = "On"
[node name="ThemesHeader" type="HBoxContainer" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Interface"]
layout_mode = 2
theme_override_constants/separation = 0
@ -1076,6 +1111,7 @@ tooltip_text = "Specifies the tablet driver being used on Windows. If you have W
mouse_default_cursor_shape = 2
[node name="Extensions" type="VBoxContainer" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide"]
unique_name_in_owner = true
visible = false
layout_mode = 2
script = ExtResource("2")
@ -1093,6 +1129,10 @@ text = "Extensions"
layout_mode = 2
size_flags_horizontal = 3
[node name="Explore" type="Button" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions/ExtensionsHeader"]
layout_mode = 2
text = "Explore Online"
[node name="InstalledExtensions" type="ItemList" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions"]
layout_mode = 2
auto_height = true
@ -1293,9 +1333,20 @@ layout_mode = 2
layout_mode = 2
text = "Pixelorama must be restarted for changes to take effect."
[node name="Popups" type="Node" parent="."]
[node name="Popups" type="Control" parent="."]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 8.0
offset_top = 8.0
offset_right = -8.0
offset_bottom = -49.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
[node name="AddExtensionFileDialog" type="FileDialog" parent="Popups"]
[node name="AddExtensionFileDialog" type="FileDialog" parent="Popups" groups=["FileDialogs"]]
mode = 1
title = "Open File(s)"
size = Vector2i(560, 400)
@ -1307,6 +1358,9 @@ access = 2
filters = PackedStringArray("*.pck ; Godot Resource Pack File", "*.zip ;")
show_hidden_files = true
[node name="Store" parent="Popups" instance=ExtResource("8_jmnx8")]
transient = true
[node name="DeleteConfirmation" type="ConfirmationDialog" parent="."]
unique_name_in_owner = true
position = Vector2i(0, 36)
@ -1329,6 +1383,7 @@ vertical_alignment = 1
[connection signal="item_selected" from="HSplitContainer/List" to="." method="_on_List_item_selected"]
[connection signal="pressed" from="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Language/System Language" to="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Language" method="_on_Language_pressed" binds= [1]]
[connection signal="pressed" from="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Interface/InterfaceOptions/ShrinkContainer/ShrinkApplyButton" to="." method="_on_ShrinkApplyButton_pressed"]
[connection signal="pressed" from="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions/ExtensionsHeader/Explore" to="Popups/Store" method="_on_explore_pressed"]
[connection signal="empty_clicked" from="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions/InstalledExtensions" to="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions" method="_on_InstalledExtensions_empty_clicked"]
[connection signal="item_selected" from="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions/InstalledExtensions" to="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions" method="_on_InstalledExtensions_item_selected"]
[connection signal="pressed" from="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions/HBoxContainer/AddExtensionButton" to="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions" method="_on_AddExtensionButton_pressed"]

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;
render_mode unshaded;
uniform float angle;
uniform sampler2D selection_tex;
uniform vec2 pivot_pixel;
vec2 rotate(vec2 uv, vec2 pivot, float ratio) {
// Scale and center image
uv.x -= pivot.x;
uv.x *= ratio;
uv.x += pivot.x;
// Rotate image
uv -= pivot;
uv = vec2(cos(angle) * uv.x + sin(angle) * uv.y,
-sin(angle) * uv.x + cos(angle) * uv.y);
uv.x /= ratio;
uv += pivot;
return uv;
}
#include "res://src/Shaders/Effects/Rotation/CommonRotation.gdshaderinc"
void fragment() {
vec4 original = texture(TEXTURE, UV);
float selection = texture(selection_tex, UV).a;
vec2 tex_size = 1.0 / TEXTURE_PIXEL_SIZE; // Texture size in real pixel coordinates
vec2 pixelated_uv = floor(UV * tex_size) / (tex_size - 1.0); // Pixelate UV to fit resolution
vec2 pivot = pivot_pixel / tex_size; // Normalize pivot position
float ratio = tex_size.x / tex_size.y; // Resolution ratio
// Make a border to prevent stretching pixels on the edge
vec2 border_uv = rotate(pixelated_uv, pivot, ratio);
// Center the border
border_uv -= 0.5;
border_uv *= 2.0;
border_uv = abs(border_uv);
float border = max(border_uv.x, border_uv.y); // This is a rectangular gradient
border = floor(border - TEXTURE_PIXEL_SIZE.x); // Turn the grad into a rectangle shape
border = 1.0 - clamp(border, 0.0, 1.0); // Invert the rectangle
// Mixing
vec4 rotated = texture(TEXTURE, rotate(pixelated_uv, pivot, ratio)); // Rotated image
rotated.a *= texture(selection_tex, rotate(pixelated_uv, pivot, ratio)).a; // Combine with selection mask
float mask = mix(selection, 1.0, 1.0 - ceil(original.a)); // Combine selection mask with area outside original
// Combine original and rotated image only when intersecting, otherwise just pure rotated image.
COLOR.rgb = mix(mix(original.rgb, rotated.rgb, rotated.a * border), rotated.rgb, mask);
COLOR.a = mix(original.a, 0.0, selection); // Remove alpha on the selected area
COLOR.a = mix(COLOR.a, 1.0, rotated.a * border); // Combine alpha of original image and rotated
vec2 rotated_uv = rotate(pixelated_uv, pivot, ratio);
vec4 rotated_color = texture(TEXTURE, rotated_uv); // Rotated image
COLOR = mix_rotated_and_original(rotated_color, original, UV, rotated_uv, TEXTURE_PIXEL_SIZE);
}

View file

@ -33,6 +33,8 @@ shader_type canvas_item;
// SOFTWARE.
//
#include "res://src/Shaders/Effects/Rotation/CommonRotation.gdshaderinc"
uniform int ScaleMultiplier : hint_range(0, 100) = 4;
// vertex compatibility #defines
@ -40,9 +42,6 @@ uniform int ScaleMultiplier : hint_range(0, 100) = 4;
// #define outsize vec4(OutputSize, 1.0 / OutputSize)
// Pixelorama-specific uniforms
uniform float angle;
uniform sampler2D selection_tex;
uniform vec2 pivot_pixel;
uniform bool preview = false;
@ -289,27 +288,9 @@ vec4 scale(sampler2D image, vec2 coord, vec2 pxSize) {
}
vec2 rotate(vec2 uv, vec2 pivot, float ratio) { // Taken from NearestNeighbour shader
// Scale and center image
uv.x -= pivot.x;
uv.x *= ratio;
uv.x += pivot.x;
// Rotate image
uv -= pivot;
uv = vec2(cos(angle) * uv.x + sin(angle) * uv.y,
-sin(angle) * uv.x + cos(angle) * uv.y);
uv.x /= ratio;
uv += pivot;
return uv;
}
void fragment()
{
vec4 original = texture(TEXTURE, UV);
float selection = texture(selection_tex, UV).a;
vec2 size = 1.0 / TEXTURE_PIXEL_SIZE;
vec2 pivot = pivot_pixel / size; // Normalize pivot position
float ratio = size.x / size.y; // Resolution ratio
@ -321,29 +302,8 @@ void fragment()
else {
rotated_uv = rotate(UV, pivot, ratio);
}
vec4 c;
c = scale(TEXTURE, rotated_uv, TEXTURE_PIXEL_SIZE);
vec4 c = scale(TEXTURE, rotated_uv, TEXTURE_PIXEL_SIZE);
// Taken from NearestNeighbour shader
c.a *= texture(selection_tex, rotated_uv).a; // Combine with selection mask
// Make a border to prevent stretching pixels on the edge
vec2 border_uv = rotated_uv;
// Center the border
border_uv -= 0.5;
border_uv *= 2.0;
border_uv = abs(border_uv);
float border = max(border_uv.x, border_uv.y); // This is a rectangular gradient
border = floor(border - TEXTURE_PIXEL_SIZE.x); // Turn the grad into a rectangle shape
border = 1.0 - clamp(border, 0.0, 1.0); // Invert the rectangle
float mask = mix(selection, 1.0, 1.0 - ceil(original.a)); // Combine selection mask with area outside original
// Combine original and rotated image only when intersecting, otherwise just pure rotated image.
COLOR.rgb = mix(mix(original.rgb, c.rgb, c.a * border), c.rgb, mask);
COLOR.a = mix(original.a, 0.0, selection); // Remove alpha on the selected area
COLOR.a = mix(COLOR.a, 1.0, c.a * border); // Combine alpha of original image and rotated
//c.a = step(0.5,c.a);
//COLOR = c;
// Pixelorama edit
COLOR = mix_rotated_and_original(c, original, UV, rotated_uv, TEXTURE_PIXEL_SIZE);
}

View file

@ -25,7 +25,7 @@ OTHER DEALINGS IN THE SOFTWARE.
shader_type canvas_item;
#include "res://src/Shaders/Effects/Rotation/CommonRotation.gdshaderinc"
//enables 2:1 slopes. otherwise only uses 45 degree slopes
#define SLOPE
//cleans up small detail slope transitions (if SLOPE is enabled)
@ -45,9 +45,6 @@ uniform float similarThreshold = 0.0;
uniform float lineWidth = 1.0;
// Edited for Pixelorama
uniform float angle;
uniform sampler2D selection_tex;
uniform vec2 pivot_pixel;
uniform bool preview = false;
bool similar(vec4 col1, vec4 col2){
@ -274,28 +271,10 @@ vec4 sliceDist(vec2 point, vec2 mainDir, vec2 pointDir, vec4 u, vec4 uf, vec4 uf
return vec4(-1.0);
}
// Pixelorama edit, taken from NearestNeighbour shader
vec2 rotate(vec2 uv, vec2 pivot, float ratio) {
// Scale and center image
uv.x -= pivot.x;
uv.x *= ratio;
uv.x += pivot.x;
// Rotate image
uv -= pivot;
uv = vec2(cos(angle) * uv.x + sin(angle) * uv.y,
-sin(angle) * uv.x + cos(angle) * uv.y);
uv.x /= ratio;
uv += pivot;
return uv;
}
void fragment() {
vec2 size = 1.0/TEXTURE_PIXEL_SIZE+0.0001; //fix for some sort of rounding error
// Pixelorama edit
vec4 original = texture(TEXTURE, UV);
float selection = texture(selection_tex, UV).a;
vec2 pivot = pivot_pixel / size; // Normalize pivot position
float ratio = size.x / size.y; // Resolution ratio
vec2 pixelated_uv = floor(UV * size) / (size - 1.0); // Pixelate UV to fit resolutio
@ -360,24 +339,6 @@ void fragment() {
col = u_col;
}
// Pixelorama edit, taken from NearestNeighbour shader
col.a *= texture(selection_tex, rotated_uv).a; // Combine with selection mask
// Make a border to prevent stretching pixels on the edge
vec2 border_uv = rotated_uv;
// Center the border
border_uv -= 0.5;
border_uv *= 2.0;
border_uv = abs(border_uv);
float border = max(border_uv.x, border_uv.y); // This is a rectangular gradient
border = floor(border - TEXTURE_PIXEL_SIZE.x); // Turn the grad into a rectangle shape
border = 1.0 - clamp(border, 0.0, 1.0); // Invert the rectangle
float mask = mix(selection, 1.0, 1.0 - ceil(original.a)); // Combine selection mask with area outside original
// Combine original and rotated image only when intersecting, otherwise just pure rotated image.
COLOR.rgb = mix(mix(original.rgb, col.rgb, col.a * border), col.rgb, mask);
COLOR.a = mix(original.a, 0.0, selection); // Remove alpha on the selected area
COLOR.a = mix(COLOR.a, 1.0, col.a * border); // Combine alpha of original image and rotated
// Pixelorama edit
COLOR = mix_rotated_and_original(col, original, UV, rotated_uv, TEXTURE_PIXEL_SIZE);
}

View file

@ -773,7 +773,7 @@ script = ExtResource("5")
wait_time = 0.2
one_shot = true
[node name="LoadModelDialog" type="FileDialog" parent="." index="6"]
[node name="LoadModelDialog" type="FileDialog" parent="." index="6" groups=["FileDialogs"]]
mode = 1
title = "Open File(s)"
size = Vector2i(558, 300)

View file

@ -252,6 +252,8 @@ func _on_Size_value_changed(value: Vector2i) -> void:
if timer.is_stopped():
undo_data = selection_node.get_undo_data(false)
if not selection_node.is_moving_content:
selection_node.original_bitmap.copy_from(Global.current_project.selection_map)
timer.start()
selection_node.big_bounding_rectangle.size = value
selection_node.resize_selection()
@ -262,5 +264,5 @@ func _on_Size_ratio_toggled(button_pressed: bool) -> void:
func _on_Timer_timeout() -> void:
if !selection_node.is_moving_content:
if not selection_node.is_moving_content:
selection_node.commit_undo("Move Selection", undo_data)

View file

@ -91,8 +91,7 @@ func _input(event: InputEvent) -> void:
Tools.handle_draw(Vector2i(current_pixel.floor()), event)
if sprite_changed_this_frame:
if Global.has_focus:
queue_redraw()
queue_redraw()
update_selected_cels_textures()

View file

@ -2,12 +2,11 @@ extends Node2D
func _input(event: InputEvent) -> void:
if Global.has_focus:
if event is InputEventMouse or event is InputEventKey:
queue_redraw()
if event is InputEventMouse or event is InputEventKey:
queue_redraw()
func _draw() -> void:
# Draw rectangle to indicate the pixel currently being hovered on
if Global.has_focus and Global.can_draw:
if Global.can_draw:
Tools.draw_indicator()

View file

@ -29,7 +29,7 @@ func draw_guide_line():
func _input(event: InputEvent) -> void:
if !Global.show_mouse_guides or !Global.can_draw or !Global.has_focus:
if !Global.show_mouse_guides or !Global.can_draw:
visible = false
return
visible = true

View file

@ -2,11 +2,10 @@ extends Node2D
func _input(event: InputEvent) -> void:
if Global.has_focus:
if event is InputEventMouse or event is InputEventKey:
queue_redraw()
if event is InputEventMouse or event is InputEventKey:
queue_redraw()
func _draw() -> void:
if Global.has_focus and Global.can_draw:
if Global.can_draw:
Tools.draw_preview()

View file

@ -55,7 +55,6 @@ func _input(event: InputEvent) -> void:
var ri: ReferenceImage = Global.current_project.get_current_reference_image()
if !ri:
Global.can_draw = true
return
# Check if want to cancelthe reference transform

View file

@ -39,7 +39,6 @@ func _input(_event: InputEvent) -> void:
if (
Input.is_action_just_pressed(&"left_mouse")
and Global.can_draw
and Global.has_focus
and rect.has_point(mouse_pos)
):
if (

View file

@ -13,6 +13,11 @@ var is_pasting := false
var big_bounding_rectangle := Rect2i():
set(value):
big_bounding_rectangle = value
if value.size == Vector2i(0, 0):
set_process_input(false)
Global.can_draw = true
else:
set_process_input(true)
for slot in Tools._slots.values():
if slot.tool_node is BaseSelectionTool:
slot.tool_node.set_spinbox_values()
@ -34,7 +39,8 @@ var preview_image_texture := ImageTexture.new()
var undo_data: Dictionary
var gizmos: Array[Gizmo] = []
var dragged_gizmo: Gizmo = null
var prev_angle := 0
var angle := 0.0
var content_pivot := Vector2.ZERO
var mouse_pos_on_gizmo_drag := Vector2.ZERO
var resize_keep_ratio := false
@ -53,24 +59,24 @@ class Gizmo:
type = _type
direction = _direction
func get_cursor() -> Control.CursorShape:
var cursor := Control.CURSOR_MOVE
func get_cursor() -> DisplayServer.CursorShape:
var cursor := DisplayServer.CURSOR_MOVE
if direction == Vector2i.ZERO:
return Control.CURSOR_POINTING_HAND
return DisplayServer.CURSOR_POINTING_HAND
elif direction == Vector2i(-1, -1) or direction == Vector2i(1, 1): # Top left or bottom right
if Global.mirror_view:
cursor = Control.CURSOR_BDIAGSIZE
cursor = DisplayServer.CURSOR_BDIAGSIZE
else:
cursor = Control.CURSOR_FDIAGSIZE
cursor = DisplayServer.CURSOR_FDIAGSIZE
elif direction == Vector2i(1, -1) or direction == Vector2i(-1, 1): # Top right or bottom left
if Global.mirror_view:
cursor = Control.CURSOR_FDIAGSIZE
cursor = DisplayServer.CURSOR_FDIAGSIZE
else:
cursor = Control.CURSOR_BDIAGSIZE
cursor = DisplayServer.CURSOR_BDIAGSIZE
elif direction == Vector2i(0, -1) or direction == Vector2i(0, 1): # Center top or center bottom
cursor = Control.CURSOR_VSIZE
cursor = DisplayServer.CURSOR_VSIZE
elif direction == Vector2i(-1, 0) or direction == Vector2i(1, 0): # Center left or center right
cursor = Control.CURSOR_HSIZE
cursor = DisplayServer.CURSOR_HSIZE
return cursor
@ -84,12 +90,11 @@ func _ready() -> void:
gizmos.append(Gizmo.new(Gizmo.Type.SCALE, Vector2i(0, 1))) # Center bottom
gizmos.append(Gizmo.new(Gizmo.Type.SCALE, Vector2i(-1, 1))) # Bottom left
gizmos.append(Gizmo.new(Gizmo.Type.SCALE, Vector2i(-1, 0))) # Center left
# gizmos.append(Gizmo.new(Gizmo.Type.ROTATE)) # Rotation gizmo (temp)
#gizmos.append(Gizmo.new(Gizmo.Type.ROTATE)) # Rotation gizmo (temp)
func _input(event: InputEvent) -> void:
var project := Global.current_project
image_current_pixel = canvas.current_pixel
if Global.mirror_view:
image_current_pixel.x = Global.current_project.size.x - image_current_pixel.x
@ -99,7 +104,6 @@ func _input(event: InputEvent) -> void:
elif Input.is_action_just_pressed("transformation_cancel"):
transform_content_cancel()
var project := Global.current_project
if not project.layers[project.current_layer].can_layer_get_drawn():
return
if event is InputEventKey:
@ -123,17 +127,14 @@ func _input(event: InputEvent) -> void:
if Input.is_action_pressed("transform_move_selection_only"):
transform_content_confirm()
if not is_moving_content:
original_bitmap.copy_from(Global.current_project.selection_map)
original_big_bounding_rectangle = big_bounding_rectangle
if Input.is_action_pressed("transform_move_selection_only"):
undo_data = get_undo_data(false)
temp_rect = big_bounding_rectangle
else:
transform_content_start()
project.selection_offset = Vector2.ZERO
if dragged_gizmo.type == Gizmo.Type.ROTATE:
var img_size := maxi(
original_preview_image.get_width(), original_preview_image.get_height()
)
original_preview_image.crop(img_size, img_size)
else:
var prev_temp_rect := temp_rect
dragged_gizmo.direction.x *= signi(temp_rect.size.x)
@ -157,7 +158,9 @@ func _input(event: InputEvent) -> void:
Global.can_draw = true
dragged_gizmo = null
if not is_moving_content:
original_bitmap = SelectionMap.new()
commit_undo("Select", undo_data)
angle = 0.0
if dragged_gizmo:
if dragged_gizmo.type == Gizmo.Type.SCALE:
@ -166,17 +169,17 @@ func _input(event: InputEvent) -> void:
_gizmo_rotate()
else: # Set the appropriate cursor
if gizmo_hover:
Global.main_viewport.mouse_default_cursor_shape = gizmo_hover.get_cursor()
DisplayServer.cursor_set_shape(gizmo_hover.get_cursor())
else:
var cursor := Control.CURSOR_ARROW
var cursor := DisplayServer.CURSOR_ARROW
if Global.cross_cursor:
cursor = Control.CURSOR_CROSS
cursor = DisplayServer.CURSOR_CROSS
var layer: BaseLayer = project.layers[project.current_layer]
if not layer.can_layer_get_drawn():
cursor = Control.CURSOR_FORBIDDEN
cursor = DisplayServer.CURSOR_FORBIDDEN
if Global.main_viewport.mouse_default_cursor_shape != cursor:
Global.main_viewport.mouse_default_cursor_shape = cursor
if DisplayServer.cursor_get_shape() != cursor:
DisplayServer.cursor_set_shape(cursor)
func _move_with_arrow_keys(event: InputEvent) -> void:
@ -210,14 +213,14 @@ func _move_with_arrow_keys(event: InputEvent) -> void:
var move := input.rotated(snappedf(Global.camera.rotation, PI / 2))
# These checks are needed to fix a bug where the selection got stuck
# to the canvas boundaries when they were 1px away from them
if is_equal_approx(absf(move.x), 0.0):
if is_zero_approx(absf(move.x)):
move.x = 0
if is_equal_approx(absf(move.y), 0.0):
if is_zero_approx(absf(move.y)):
move.y = 0
move_content(move * step)
# Check if an event is a ui_up/down/left/right event-press
## Check if an event is a ui_up/down/left/right event pressed
func _is_action_direction_pressed(event: InputEvent) -> bool:
for action in KEY_MOVE_ACTION_NAMES:
if event.is_action_pressed(action, false, true):
@ -225,7 +228,7 @@ func _is_action_direction_pressed(event: InputEvent) -> bool:
return false
# Check if an event is a ui_up/down/left/right event release
## Check if an event is a ui_up/down/left/right event
func _is_action_direction(event: InputEvent) -> bool:
for action in KEY_MOVE_ACTION_NAMES:
if event.is_action(action, true):
@ -233,7 +236,7 @@ func _is_action_direction(event: InputEvent) -> bool:
return false
# Check if an event is a ui_up/down/left/right event release
## Check if an event is a ui_up/down/left/right event release
func _is_action_direction_released(event: InputEvent) -> bool:
for action in KEY_MOVE_ACTION_NAMES:
if event.is_action_released(action, true):
@ -282,9 +285,9 @@ func _update_gizmos() -> void:
)
# Rotation gizmo (temp)
# gizmos[8].rect = Rect2(
# Vector2((rect_end.x + rect_pos.x - size.x) / 2, rect_pos.y - size.y - (size.y * 2)), size
# )
#gizmos[8].rect = Rect2(
#Vector2((rect_end.x + rect_pos.x - size.x) / 2, rect_pos.y - size.y - (size.y * 2)), size
#)
queue_redraw()
@ -337,8 +340,6 @@ func _gizmo_resize() -> void:
temp_rect.position.y = end_y - temp_rect.size.y
big_bounding_rectangle = temp_rect.abs()
# big_bounding_rectangle.position = Vector2(big_bounding_rectangle.position).ceil()
# big_bounding_rectangle.size = big_bounding_rectangle.size.floor()
if big_bounding_rectangle.size.x == 0:
big_bounding_rectangle.size.x = 1
if big_bounding_rectangle.size.y == 0:
@ -370,9 +371,14 @@ func _resize_rect(pos: Vector2, dir: Vector2) -> void:
func resize_selection() -> void:
var size := big_bounding_rectangle.size.abs()
if is_moving_content:
if original_bitmap.is_empty():
print("original_bitmap is empty, this shouldn't happen.")
else:
Global.current_project.selection_map.copy_from(original_bitmap)
if is_moving_content:
content_pivot = original_big_bounding_rectangle.size / 2.0
preview_image.copy_from(original_preview_image)
DrawingAlgos.nn_rotate(preview_image, angle, content_pivot)
preview_image.resize(size.x, size.y, Image.INTERPOLATE_NEAREST)
if temp_rect.size.x < 0:
preview_image.flip_x()
@ -380,6 +386,12 @@ func resize_selection() -> void:
preview_image.flip_y()
preview_image_texture = ImageTexture.create_from_image(preview_image)
Global.current_project.selection_map.copy_from(original_bitmap)
var bitmap_pivot := (
original_big_bounding_rectangle.position
+ ((original_big_bounding_rectangle.end - original_big_bounding_rectangle.position) / 2)
)
DrawingAlgos.nn_rotate(Global.current_project.selection_map, angle, bitmap_pivot)
Global.current_project.selection_map.resize_bitmap_values(
Global.current_project, size, temp_rect.size.x < 0, temp_rect.size.y < 0
)
@ -388,42 +400,12 @@ func resize_selection() -> void:
Global.canvas.queue_redraw()
func _gizmo_rotate() -> void: # Does not work properly yet
var angle := image_current_pixel.angle_to_point(mouse_pos_on_gizmo_drag)
angle = deg_to_rad(floorf(rad_to_deg(angle)))
if angle == prev_angle:
return
prev_angle = angle
# var img_size := max(original_preview_image.get_width(), original_preview_image.get_height())
# var pivot = Vector2(original_preview_image.get_width()/2, original_preview_image.get_height()/2)
var pivot := Vector2(big_bounding_rectangle.size.x / 2.0, big_bounding_rectangle.size.y / 2.0)
preview_image.copy_from(original_preview_image)
if original_big_bounding_rectangle.position != big_bounding_rectangle.position:
preview_image.fill(Color(0, 0, 0, 0))
var pos_diff := (
(original_big_bounding_rectangle.position - big_bounding_rectangle.position).abs()
)
# pos_diff.y = 0
preview_image.blit_rect(
original_preview_image, Rect2(Vector2.ZERO, preview_image.get_size()), pos_diff
)
DrawingAlgos.nn_rotate(preview_image, angle, pivot)
preview_image_texture = ImageTexture.create_from_image(preview_image)
var bitmap_image := original_bitmap
var bitmap_pivot := (
original_big_bounding_rectangle.position
+ ((original_big_bounding_rectangle.end - original_big_bounding_rectangle.position) / 2)
)
DrawingAlgos.nn_rotate(bitmap_image, angle, bitmap_pivot)
Global.current_project.selection_map = bitmap_image
Global.current_project.selection_map_changed()
big_bounding_rectangle = bitmap_image.get_used_rect()
queue_redraw()
Global.canvas.queue_redraw()
func _gizmo_rotate() -> void:
angle = image_current_pixel.angle_to_point(mouse_pos_on_gizmo_drag)
resize_selection()
func select_rect(rect: Rect2i, operation: int = SelectionOperation.ADD) -> void:
func select_rect(rect: Rect2i, operation := SelectionOperation.ADD) -> void:
var project := Global.current_project
# Used only if the selection is outside of the canvas boundaries,
# on the left and/or above (negative coords)
@ -510,15 +492,15 @@ func transform_content_confirm() -> void:
return
var project := Global.current_project
for cel in _get_selected_draw_cels():
var cel_image: Image = cel.get_image()
var src: Image = preview_image
var cel_image := cel.get_image()
var src := Image.new()
src.copy_from(preview_image)
if not is_pasting:
src.copy_from(cel.transformed_content)
cel.transformed_content = null
DrawingAlgos.nn_rotate(src, angle, content_pivot)
src.resize(
big_bounding_rectangle.size.x,
big_bounding_rectangle.size.y,
Image.INTERPOLATE_NEAREST
preview_image.get_width(), preview_image.get_height(), Image.INTERPOLATE_NEAREST
)
if temp_rect.size.x < 0:
src.flip_x()
@ -539,6 +521,8 @@ func transform_content_confirm() -> void:
original_bitmap = SelectionMap.new()
is_moving_content = false
is_pasting = false
angle = 0.0
content_pivot = Vector2.ZERO
queue_redraw()
Global.canvas.queue_redraw()
@ -570,6 +554,8 @@ func transform_content_cancel() -> void:
preview_image = Image.new()
original_bitmap = SelectionMap.new()
is_pasting = false
angle = 0.0
content_pivot = Vector2.ZERO
queue_redraw()
Global.canvas.queue_redraw()

View file

@ -1,5 +1,7 @@
extends Container
## The swatches button of the [ColorPicker] node. Used to ensure that swatches are always invisible
var swatches_button: Button
@onready var color_picker := %ColorPicker as ColorPicker
@onready var color_buttons := %ColorButtons as HBoxContainer
@onready var left_color_rect := %LeftColorRect as ColorRect
@ -55,6 +57,14 @@ func _ready() -> void:
color_buttons.get_parent().remove_child(color_buttons)
sampler_cont.add_child(color_buttons)
sampler_cont.move_child(color_buttons, 0)
swatches_button = picker_vbox_container.get_child(5, true) as Button
swatches_button.visible = false
# The GridContainer that contains the swatch buttons. These are not visible in our case
# but for some reason its h_separation needs to be set to a value larger than 4,
# otherwise a weird bug occurs with the Recent Colors where, adding new colors
# increases the size of the color buttons.
var presets_container := picker_vbox_container.get_child(6, true) as GridContainer
presets_container.add_theme_constant_override("h_separation", 5)
func _on_color_picker_color_changed(color: Color) -> void:
@ -98,6 +108,9 @@ func _on_ColorDefaults_pressed() -> void:
func _on_expand_button_toggled(toggled_on: bool) -> void:
color_picker.color_modes_visible = toggled_on
color_picker.sliders_visible = toggled_on
color_picker.presets_visible = toggled_on
if is_instance_valid(swatches_button):
swatches_button.visible = false
Global.config_cache.set_value("color_picker", "is_expanded", toggled_on)

View file

@ -13,7 +13,12 @@ var image_exports: Array[Export.FileFormat] = [
Export.FileFormat.WEBP,
Export.FileFormat.JPEG,
Export.FileFormat.GIF,
Export.FileFormat.APNG
Export.FileFormat.APNG,
Export.FileFormat.MP4,
Export.FileFormat.AVI,
Export.FileFormat.OGV,
Export.FileFormat.MKV,
Export.FileFormat.WEBM,
]
var spritesheet_exports: Array[Export.FileFormat] = [
Export.FileFormat.PNG, Export.FileFormat.WEBP, Export.FileFormat.JPEG
@ -188,6 +193,12 @@ func set_file_format_selector() -> void:
match Export.current_tab:
Export.ExportTab.IMAGE:
_set_file_format_selector_suitable_file_formats(image_exports)
if Export.is_ffmpeg_installed():
for format in Export.ffmpeg_formats:
file_format_options.set_item_disabled(format, false)
else:
for format in Export.ffmpeg_formats:
file_format_options.set_item_disabled(format, true)
Export.ExportTab.SPRITESHEET:
_set_file_format_selector_suitable_file_formats(spritesheet_exports)
@ -246,9 +257,9 @@ func update_dimensions_label() -> void:
func open_path_validation_alert_popup(path_or_name: int = -1) -> void:
# 0 is invalid path, 1 is invalid name
var error_text := "DirAccess path and file name are not valid!"
var error_text := "Directory path and file name are not valid!"
if path_or_name == 0:
error_text = "DirAccess path is not valid!"
error_text = "Directory path is not valid!"
elif path_or_name == 1:
error_text = "File name is not valid!"

View file

@ -306,7 +306,7 @@ offset_right = 692.0
offset_bottom = 551.0
mouse_filter = 2
[node name="PathDialog" type="FileDialog" parent="Popups"]
[node name="PathDialog" type="FileDialog" parent="Popups" groups=["FileDialogs"]]
mode = 2
title = "Open a Directory"
size = Vector2i(675, 500)

View file

@ -222,13 +222,13 @@ func _on_Indicator_draw() -> void:
else:
conversion_scale = ratio.y
var pivot_position := pivot * conversion_scale
pivot_indicator.draw_arc(pivot_position, 2, 0, 360, 360, Color.YELLOW, 0.5)
pivot_indicator.draw_arc(pivot_position, 6, 0, 360, 360, Color.WHITE, 0.5)
pivot_indicator.draw_arc(pivot_position, 2, 0, 360, 360, Color.YELLOW)
pivot_indicator.draw_arc(pivot_position, 6, 0, 360, 360, Color.WHITE)
pivot_indicator.draw_line(
pivot_position - Vector2.UP * 10, pivot_position - Vector2.DOWN * 10, Color.WHITE, 0.5
pivot_position - Vector2.UP * 10, pivot_position - Vector2.DOWN * 10, Color.WHITE
)
pivot_indicator.draw_line(
pivot_position - Vector2.RIGHT * 10, pivot_position - Vector2.LEFT * 10, Color.WHITE, 0.5
pivot_position - Vector2.RIGHT * 10, pivot_position - Vector2.LEFT * 10, Color.WHITE
)
@ -239,8 +239,7 @@ func _on_Indicator_gui_input(event: InputEvent) -> void:
drag_pivot = false
if drag_pivot:
var img_size := preview_image.get_size()
# var mouse_pos := get_local_mouse_position() - pivot_indicator.position
var mouse_pos := pivot_indicator.position
var mouse_pos := pivot_indicator.get_local_mouse_position()
var ratio := Vector2(img_size) / pivot_indicator.size
# we need to set the scale according to the larger side
var conversion_scale: float

View file

@ -53,7 +53,7 @@ text = "No shader loaded!"
[node name="ShaderParams" type="VBoxContainer" parent="VBoxContainer"]
layout_mode = 2
[node name="FileDialog" type="FileDialog" parent="."]
[node name="FileDialog" type="FileDialog" parent="." groups=["FileDialogs"]]
access = 2
filters = PackedStringArray("*gdshader; Godot Shader File")
show_hidden_files = true

View file

@ -1,6 +1,6 @@
[gd_scene format=3 uid="uid://b3aeqj2k58wdk"]
[node name="OpenSprite" type="FileDialog"]
[node name="OpenSprite" type="FileDialog" groups=["FileDialogs"]]
title = "Open File(s)"
size = Vector2i(558, 400)
exclusive = false

View file

@ -1,6 +1,6 @@
[gd_scene format=3 uid="uid://d4euwo633u33b"]
[node name="SaveSprite" type="FileDialog"]
[node name="SaveSprite" type="FileDialog" groups=["FileDialogs"]]
size = Vector2i(675, 400)
exclusive = false
popup_window = true

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):
_vanishing_point = vanishing_point
width = LINE_WIDTH / Global.camera.zoom.x
Global.canvas.add_child(self)
deserialize(data)
# a small delay is needed for Global.camera.zoom to have correct value
await get_tree().process_frame
width = LINE_WIDTH / Global.camera.zoom.x
refresh()
@ -63,7 +65,7 @@ func _input(event: InputEvent) -> void:
var project_size := Global.current_project.size
if track_mouse:
if !Global.can_draw or !Global.has_focus or Global.perspective_editor.tracker_disabled:
if !Global.can_draw or Global.perspective_editor.tracker_disabled:
hide_perspective_line()
return
default_color.a = 0.5
@ -92,7 +94,7 @@ func try_rotate_scale():
var test_line := (points[1] - points[0]).rotated(deg_to_rad(90)).normalized()
var from_a := mouse_point - test_line * CIRCLE_RAD * 2 / Global.camera.zoom.x
var from_b := mouse_point + test_line * CIRCLE_RAD * 2 / Global.camera.zoom.x
if Input.is_action_just_pressed("left_mouse") and Global.can_draw and Global.has_focus:
if Input.is_action_just_pressed("left_mouse") and Global.can_draw:
if (
Geometry2D.segment_intersects_segment(from_a, from_b, points[0], points[1])
or mouse_point.distance_to(points[1]) < CIRCLE_RAD * 2 / Global.camera.zoom.x
@ -126,6 +128,7 @@ func try_rotate_scale():
func _draw() -> void:
width = LINE_WIDTH / Global.camera.zoom.x
var mouse_point := Global.canvas.current_pixel
var arc_points := PackedVector2Array()
draw_circle(points[0], CIRCLE_RAD / Global.camera.zoom.x, default_color) # Starting circle
@ -150,8 +153,9 @@ func _draw() -> void:
arc_points.append(points[1])
for point in arc_points:
draw_arc(point, CIRCLE_RAD * 2 / Global.camera.zoom.x, 0, 360, 360, default_color, 0.5)
# if we put width <= -1, then the arc line will automatically adjust itself to remain thin
# in 0.x this behavior was achieved at width <= 1
draw_arc(point, CIRCLE_RAD * 2 / Global.camera.zoom.x, 0, 360, 360, default_color)
width = LINE_WIDTH / Global.camera.zoom.x
if is_hidden: # Hidden line
return

View file

@ -75,8 +75,7 @@ func _input(_event: InputEvent):
if (
Input.is_action_just_pressed("left_mouse")
and Global.can_draw
and Global.has_focus
and mouse_point.distance_to(start) < Global.camera.zoom.x * 8
and mouse_point.distance_to(start) < 8 / Global.camera.zoom.x
):
if (
!Rect2(Vector2.ZERO, project_size).has_point(Global.canvas.current_pixel)

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 current_frame_no := 0 ## Used to compare with skip_amount to see if it can be captured
var resize := 100
var resize_percent := 100
@onready var captured_label := %CapturedLabel as Label
@onready var project_list := $"%TargetProjectOption" as OptionButton
@onready var folder_button := $"%Folder" as Button
@onready var start_button := $"%Start" as Button
@onready var size_label := $"%Size" as Label
@onready var path_field := $"%Path" as LineEdit
@onready var options_dialog := $Dialogs/OptionsDialog as AcceptDialog
@onready var options_container := %OptionsContainer as VBoxContainer
func _ready() -> void:
@ -40,10 +42,9 @@ func initialize_recording() -> void:
current_frame_no = skip_amount - 1
# disable some options that are not required during recording
folder_button.visible = true
project_list.visible = false
$ScrollContainer/CenterContainer/GridContainer/Captured.visible = true
for child in $Dialogs/Options/PanelContainer/VBoxContainer.get_children():
captured_label.visible = true
for child in options_container.get_children():
if !child.is_in_group("visible during recording"):
child.visible = false
@ -77,11 +78,10 @@ func capture_frame() -> void:
DrawingAlgos.blend_layers(image, frame, Vector2i.ZERO, project)
if mode == Mode.CANVAS:
if resize != 100:
if resize_percent != 100:
var resize := resize_percent / 100
image.resize(
image.get_size().x * resize / 100,
image.get_size().y * resize / 100,
Image.INTERPOLATE_NEAREST
image.get_width() * resize, image.get_height() * resize, Image.INTERPOLATE_NEAREST
)
cache.append(image)
@ -102,7 +102,7 @@ func save_frame(img: Image) -> void:
func _on_frame_saved() -> void:
frame_captured += 1
$ScrollContainer/CenterContainer/GridContainer/Captured.text = str("Saved: ", frame_captured)
captured_label.text = str("Saved: ", frame_captured)
func finalize_recording() -> void:
@ -111,10 +111,9 @@ func finalize_recording() -> void:
save_frame(img)
cache.clear()
disconnect_undo()
folder_button.visible = false
project_list.visible = true
$ScrollContainer/CenterContainer/GridContainer/Captured.visible = false
for child in $Dialogs/Options/PanelContainer/VBoxContainer.get_children():
captured_label.visible = false
for child in options_container.get_children():
child.visible = true
if mode == Mode.PIXELORAMA:
size_label.get_parent().visible = false
@ -152,9 +151,7 @@ func _on_Start_toggled(button_pressed: bool) -> void:
func _on_Settings_pressed() -> void:
var settings := $Dialogs/Options as Window
var pos := position
settings.popup(Rect2(pos, settings.size))
options_dialog.popup(Rect2(position, options_dialog.size))
func _on_SkipAmount_value_changed(value: float) -> void:
@ -171,8 +168,8 @@ func _on_Mode_toggled(button_pressed: bool) -> void:
func _on_SpinBox_value_changed(value: float) -> void:
resize = value
var new_size: Vector2 = project.size * (resize / 100.0)
resize_percent = value
var new_size: Vector2 = project.size * (resize_percent / 100.0)
size_label.text = str("(", new_size.x, "×", new_size.y, ")")
@ -181,7 +178,7 @@ func _on_Choose_pressed() -> void:
$Dialogs/Path.current_dir = chosen_dir
func _on_Open_pressed() -> void:
func _on_open_folder_pressed() -> void:
OS.shell_open(path_field.text)
@ -189,13 +186,3 @@ func _on_Path_dir_selected(dir: String) -> void:
chosen_dir = dir
path_field.text = chosen_dir
start_button.disabled = false
func _on_Fps_value_changed(value: float) -> void:
var dur_label := $Dialogs/Options/PanelContainer/VBoxContainer/Fps/Duration as Label
var duration := snappedf(1.0 / value, 0.0001)
dur_label.text = str("= ", duration, " sec")
func _on_options_close_requested() -> void:
$Dialogs/Options.hide()

View file

@ -26,7 +26,8 @@ layout_mode = 2
size_flags_vertical = 0
columns = 4
[node name="Captured" type="Label" parent="ScrollContainer/CenterContainer/GridContainer"]
[node name="CapturedLabel" type="Label" parent="ScrollContainer/CenterContainer/GridContainer"]
unique_name_in_owner = true
visible = false
layout_mode = 2
@ -80,16 +81,14 @@ offset_bottom = 10.5
texture = ExtResource("3")
stretch_mode = 6
[node name="Folder" type="Button" parent="ScrollContainer/CenterContainer/GridContainer"]
unique_name_in_owner = true
visible = false
[node name="OpenFolder" type="Button" parent="ScrollContainer/CenterContainer/GridContainer"]
custom_minimum_size = Vector2(32, 32)
layout_mode = 2
tooltip_text = "Open Folder"
mouse_default_cursor_shape = 2
toggle_mode = true
[node name="TextureRect" type="TextureRect" parent="ScrollContainer/CenterContainer/GridContainer/Folder"]
[node name="TextureRect" type="TextureRect" parent="ScrollContainer/CenterContainer/GridContainer/OpenFolder"]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
@ -104,110 +103,95 @@ stretch_mode = 6
layout_mode = 2
mouse_filter = 2
[node name="Options" type="Window" parent="Dialogs"]
[node name="OptionsDialog" type="AcceptDialog" parent="Dialogs"]
position = Vector2i(0, 36)
size = Vector2i(400, 300)
visible = false
exclusive = false
popup_window = true
[node name="PanelContainer" type="PanelContainer" parent="Dialogs/Options"]
[node name="PanelContainer" type="MarginContainer" parent="Dialogs/OptionsDialog"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 9.0
offset_top = 9.0
offset_right = -9.0
offset_bottom = -9.0
offset_left = 8.0
offset_top = 8.0
offset_right = -8.0
offset_bottom = -49.0
[node name="VBoxContainer" type="VBoxContainer" parent="Dialogs/Options/PanelContainer"]
[node name="OptionsContainer" type="VBoxContainer" parent="Dialogs/OptionsDialog/PanelContainer"]
unique_name_in_owner = true
layout_mode = 2
[node name="IntervalHeader" type="HBoxContainer" parent="Dialogs/Options/PanelContainer/VBoxContainer"]
[node name="IntervalHeader" type="HBoxContainer" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer"]
layout_mode = 2
theme_override_constants/separation = 0
[node name="Label" type="Label" parent="Dialogs/Options/PanelContainer/VBoxContainer/IntervalHeader"]
[node name="Label" type="Label" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/IntervalHeader"]
layout_mode = 2
theme_type_variation = &"HeaderSmall"
text = "Interval"
[node name="HSeparator" type="HSeparator" parent="Dialogs/Options/PanelContainer/VBoxContainer/IntervalHeader"]
[node name="HSeparator" type="HSeparator" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/IntervalHeader"]
layout_mode = 2
size_flags_horizontal = 3
[node name="ActionGap" type="HBoxContainer" parent="Dialogs/Options/PanelContainer/VBoxContainer"]
[node name="ActionGap" type="HBoxContainer" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer"]
layout_mode = 2
alignment = 1
[node name="Label" type="Label" parent="Dialogs/Options/PanelContainer/VBoxContainer/ActionGap"]
[node name="Label" type="Label" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/ActionGap"]
layout_mode = 2
size_flags_horizontal = 3
text = "Capture frame every"
[node name="SkipAmount" type="SpinBox" parent="Dialogs/Options/PanelContainer/VBoxContainer/ActionGap"]
[node name="SkipAmount" type="SpinBox" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/ActionGap"]
layout_mode = 2
size_flags_horizontal = 3
suffix = "actions"
[node name="Label2" type="Label" parent="Dialogs/Options/PanelContainer/VBoxContainer/ActionGap"]
layout_mode = 2
text = "Actions"
[node name="Fps" type="HBoxContainer" parent="Dialogs/Options/PanelContainer/VBoxContainer"]
layout_mode = 2
alignment = 1
[node name="Label" type="Label" parent="Dialogs/Options/PanelContainer/VBoxContainer/Fps"]
layout_mode = 2
text = "Fps:"
[node name="Fps" type="SpinBox" parent="Dialogs/Options/PanelContainer/VBoxContainer/Fps"]
layout_mode = 2
min_value = 1.0
value = 30.0
allow_greater = true
[node name="Duration" type="Label" parent="Dialogs/Options/PanelContainer/VBoxContainer/Fps"]
layout_mode = 2
text = "= 0.0333 sec"
[node name="ModeHeader" type="HBoxContainer" parent="Dialogs/Options/PanelContainer/VBoxContainer" groups=["visible during recording"]]
[node name="ModeHeader" type="HBoxContainer" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer" groups=["visible during recording"]]
layout_mode = 2
theme_override_constants/separation = 0
[node name="Label" type="Label" parent="Dialogs/Options/PanelContainer/VBoxContainer/ModeHeader"]
[node name="Label" type="Label" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/ModeHeader"]
layout_mode = 2
theme_type_variation = &"HeaderSmall"
text = "Mode"
[node name="HSeparator" type="HSeparator" parent="Dialogs/Options/PanelContainer/VBoxContainer/ModeHeader"]
[node name="HSeparator" type="HSeparator" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/ModeHeader"]
layout_mode = 2
size_flags_horizontal = 3
[node name="ModeType" type="HBoxContainer" parent="Dialogs/Options/PanelContainer/VBoxContainer" groups=["visible during recording"]]
[node name="ModeType" type="HBoxContainer" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer" groups=["visible during recording"]]
layout_mode = 2
alignment = 1
[node name="Label" type="Label" parent="Dialogs/Options/PanelContainer/VBoxContainer/ModeType"]
[node name="Label" type="Label" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/ModeType"]
layout_mode = 2
text = "Canvas Only"
size_flags_horizontal = 3
text = "Record canvas only"
[node name="Mode" type="CheckButton" parent="Dialogs/Options/PanelContainer/VBoxContainer/ModeType"]
[node name="Mode" type="CheckButton" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/ModeType"]
layout_mode = 2
size_flags_horizontal = 3
mouse_default_cursor_shape = 2
[node name="Label2" type="Label" parent="Dialogs/Options/PanelContainer/VBoxContainer/ModeType"]
layout_mode = 2
text = "Pixelorama"
[node name="OutputScale" type="HBoxContainer" parent="Dialogs/Options/PanelContainer/VBoxContainer"]
[node name="OutputScale" type="HBoxContainer" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer"]
layout_mode = 2
alignment = 1
[node name="Label" type="Label" parent="Dialogs/Options/PanelContainer/VBoxContainer/OutputScale"]
[node name="Label" type="Label" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/OutputScale"]
layout_mode = 2
size_flags_horizontal = 3
text = "Output Scale:"
[node name="Size" type="Label" parent="Dialogs/Options/PanelContainer/VBoxContainer/OutputScale"]
[node name="Size" type="Label" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/OutputScale"]
unique_name_in_owner = true
layout_mode = 2
[node name="Resize" type="SpinBox" parent="Dialogs/Options/PanelContainer/VBoxContainer/OutputScale"]
[node name="Resize" type="SpinBox" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/OutputScale"]
layout_mode = 2
size_flags_horizontal = 3
mouse_default_cursor_shape = 2
min_value = 50.0
max_value = 1000.0
@ -216,49 +200,34 @@ value = 100.0
allow_greater = true
suffix = "%"
[node name="PathHeader" type="HBoxContainer" parent="Dialogs/Options/PanelContainer/VBoxContainer"]
[node name="PathHeader" type="HBoxContainer" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer"]
layout_mode = 2
theme_override_constants/separation = 0
[node name="Label" type="Label" parent="Dialogs/Options/PanelContainer/VBoxContainer/PathHeader"]
[node name="Label" type="Label" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/PathHeader"]
layout_mode = 2
theme_type_variation = &"HeaderSmall"
text = "Path"
[node name="HSeparator" type="HSeparator" parent="Dialogs/Options/PanelContainer/VBoxContainer/PathHeader"]
[node name="HSeparator" type="HSeparator" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/PathHeader"]
layout_mode = 2
size_flags_horizontal = 3
[node name="PathContainer" type="HBoxContainer" parent="Dialogs/Options/PanelContainer/VBoxContainer"]
[node name="PathContainer" type="HBoxContainer" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer"]
layout_mode = 2
[node name="Path" type="LineEdit" parent="Dialogs/Options/PanelContainer/VBoxContainer/PathContainer"]
[node name="Path" type="LineEdit" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/PathContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Choose Destination --->"
placeholder_text = "Choose destination"
editable = false
[node name="Open" type="Button" parent="Dialogs/Options/PanelContainer/VBoxContainer/PathContainer"]
custom_minimum_size = Vector2(25, 25)
layout_mode = 2
[node name="TextureRect" type="TextureRect" parent="Dialogs/Options/PanelContainer/VBoxContainer/PathContainer/Open"]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 2.0
offset_top = 2.0
offset_right = -2.0
offset_bottom = -2.0
texture = ExtResource("4")
stretch_mode = 6
[node name="Choose" type="Button" parent="Dialogs/Options/PanelContainer/VBoxContainer/PathContainer"]
[node name="Choose" type="Button" parent="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/PathContainer"]
layout_mode = 2
text = "Choose"
[node name="Path" type="FileDialog" parent="Dialogs"]
[node name="Path" type="FileDialog" parent="Dialogs" groups=["FileDialogs"]]
mode = 2
exclusive = false
popup_window = true
@ -270,13 +239,10 @@ access = 2
[connection signal="pressed" from="ScrollContainer/CenterContainer/GridContainer/TargetProjectOption" to="." method="_on_TargetProjectOption_pressed"]
[connection signal="toggled" from="ScrollContainer/CenterContainer/GridContainer/Start" to="." method="_on_Start_toggled"]
[connection signal="pressed" from="ScrollContainer/CenterContainer/GridContainer/Settings" to="." method="_on_Settings_pressed"]
[connection signal="pressed" from="ScrollContainer/CenterContainer/GridContainer/Folder" to="." method="_on_Open_pressed"]
[connection signal="close_requested" from="Dialogs/Options" to="." method="_on_options_close_requested"]
[connection signal="value_changed" from="Dialogs/Options/PanelContainer/VBoxContainer/ActionGap/SkipAmount" to="." method="_on_SkipAmount_value_changed"]
[connection signal="value_changed" from="Dialogs/Options/PanelContainer/VBoxContainer/Fps/Fps" to="." method="_on_Fps_value_changed"]
[connection signal="toggled" from="Dialogs/Options/PanelContainer/VBoxContainer/ModeType/Mode" to="." method="_on_Mode_toggled"]
[connection signal="value_changed" from="Dialogs/Options/PanelContainer/VBoxContainer/OutputScale/Resize" to="." method="_on_SpinBox_value_changed"]
[connection signal="pressed" from="Dialogs/Options/PanelContainer/VBoxContainer/PathContainer/Open" to="." method="_on_Open_pressed"]
[connection signal="pressed" from="Dialogs/Options/PanelContainer/VBoxContainer/PathContainer/Choose" to="." method="_on_Choose_pressed"]
[connection signal="pressed" from="ScrollContainer/CenterContainer/GridContainer/OpenFolder" to="." method="_on_open_folder_pressed"]
[connection signal="value_changed" from="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/ActionGap/SkipAmount" to="." method="_on_SkipAmount_value_changed"]
[connection signal="toggled" from="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/ModeType/Mode" to="." method="_on_Mode_toggled"]
[connection signal="value_changed" from="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/OutputScale/Resize" to="." method="_on_SpinBox_value_changed"]
[connection signal="pressed" from="Dialogs/OptionsDialog/PanelContainer/OptionsContainer/PathContainer/Choose" to="." method="_on_Choose_pressed"]
[connection signal="dir_selected" from="Dialogs/Path" to="." method="_on_Path_dir_selected"]
[connection signal="timeout" from="Timer" to="." method="_on_Timer_timeout"]

View file

@ -953,7 +953,7 @@ mouse_filter = 2
color = Color(0, 0.741176, 1, 0.501961)
[node name="PasteTagPopup" type="Popup" parent="."]
size = Vector2i(250, 574)
size = Vector2i(250, 335)
min_size = Vector2i(250, 0)
script = ExtResource("12")
@ -992,6 +992,7 @@ layout_mode = 2
text = "Create new tags"
[node name="Instructions" type="Label" parent="PasteTagPopup/PanelContainer/VBoxContainer"]
custom_minimum_size = Vector2(250, 23)
layout_mode = 2
text = "Available tags:"
autowrap_mode = 3
@ -1004,6 +1005,7 @@ size_flags_vertical = 3
[node name="StartFrame" type="Label" parent="PasteTagPopup/PanelContainer/VBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(250, 23)
layout_mode = 2
horizontal_alignment = 1
autowrap_mode = 3

View file

@ -3,11 +3,17 @@ extends FlowContainer
var pen_inverted := false
func _ready() -> void:
# Ensure to only call _input() if the cursor is inside the main canvas viewport
Global.main_viewport.mouse_entered.connect(set_process_input.bind(true))
Global.main_viewport.mouse_exited.connect(set_process_input.bind(false))
func _input(event: InputEvent) -> void:
if event is InputEventMouseMotion:
pen_inverted = event.pen_inverted
return
if not Global.has_focus or not Global.can_draw:
if not Global.can_draw:
return
for action in ["undo", "redo"]:
if event.is_action_pressed(action):

View file

@ -398,8 +398,6 @@ func _popup_dialog(dialog: Window, dialog_size := Vector2i.ZERO) -> void:
func file_menu_id_pressed(id: int) -> void:
if not Global.can_draw:
return
match id:
Global.FileMenu.NEW:
_on_new_project_file_menu_option_pressed()
@ -464,8 +462,6 @@ func _on_recent_projects_submenu_id_pressed(id: int) -> void:
func edit_menu_id_pressed(id: int) -> void:
if not Global.can_draw:
return
match id:
Global.EditMenu.UNDO:
Global.current_project.commit_undo()
@ -490,8 +486,6 @@ func edit_menu_id_pressed(id: int) -> void:
func view_menu_id_pressed(id: int) -> void:
if not Global.can_draw:
return
match id:
Global.ViewMenu.TILE_MODE_OFFSETS:
_popup_dialog(Global.control.get_node("Dialogs/TileModeOffsetsDialog"))
@ -518,8 +512,6 @@ func view_menu_id_pressed(id: int) -> void:
func window_menu_id_pressed(id: int) -> void:
if not Global.can_draw:
return
match id:
Global.WindowMenu.WINDOW_OPACITY:
_popup_dialog(window_opacity_dialog)
@ -703,8 +695,6 @@ func _toggle_fullscreen() -> void:
func image_menu_id_pressed(id: int) -> void:
if not Global.can_draw:
return
match id:
Global.ImageMenu.SCALE_IMAGE:
_popup_dialog(Global.control.get_node("Dialogs/ImageEffects/ScaleImage"))
@ -744,8 +734,6 @@ func image_menu_id_pressed(id: int) -> void:
func select_menu_id_pressed(id: int) -> void:
if not Global.can_draw:
return
match id:
Global.SelectMenu.SELECT_ALL:
Global.canvas.selection.select_all()
@ -762,8 +750,6 @@ func select_menu_id_pressed(id: int) -> void:
func help_menu_id_pressed(id: int) -> void:
if not Global.can_draw:
return
match id:
Global.HelpMenu.VIEW_SPLASH_SCREEN:
_popup_dialog(Global.control.get_node("Dialogs/SplashDialog"))

View file

@ -12,13 +12,11 @@ func _ready() -> void:
func _on_ViewportContainer_mouse_entered() -> void:
camera.set_process_input(true)
Global.has_focus = true
Global.control.left_cursor.visible = Global.show_left_tool_icon
Global.control.right_cursor.visible = Global.show_right_tool_icon
func _on_ViewportContainer_mouse_exited() -> void:
camera.set_process_input(false)
Global.has_focus = false
Global.control.left_cursor.visible = false
Global.control.right_cursor.visible = false