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