diff --git a/src/UI/Timeline/AnimationTimeline.gd b/src/UI/Timeline/AnimationTimeline.gd index bf324f358..03e124db8 100644 --- a/src/UI/Timeline/AnimationTimeline.gd +++ b/src/UI/Timeline/AnimationTimeline.gd @@ -349,9 +349,12 @@ func _on_CopyFrame_pressed() -> void: ## When [param destination] is -1, the new frames will be placed right next to the last frame in ## [param destination]. if [param select_all_cels] is [code]true[/code] then all of the new copied ## cels will be selected, otherwise only the cels corresponding to the original selected cels will -## get selected. +## get selected. if [param tag_name_from] holds an animation tag then a tag of it's name will be +## created over the new frames. ## [br]Note: [param indices] must be in ascending order -func copy_frames(indices := [], destination := -1, select_all_cels := true) -> void: +func copy_frames( + indices := [], destination := -1, select_all_cels := true, tag_name_from: AnimationTag = null +) -> void: var project := Global.current_project if indices.size() == 0: @@ -423,12 +426,22 @@ func copy_frames(indices := [], destination := -1, select_all_cels := true) -> v new_cel.selected = new_cel.get_object_from_id(selected_id) new_frame.cels.append(new_cel) - for tag in new_animation_tags: # Loop through the tags to see if the frame is in one + # After adding one frame, loop through the tags to see if the frame was in an animation tag + for tag in new_animation_tags: if copied_indices[0] >= tag.from && copied_indices[0] <= tag.to: tag.to += 1 elif copied_indices[0] < tag.from: tag.from += 1 tag.to += 1 + if tag_name_from: + new_animation_tags.append( + AnimationTag.new( + tag_name_from.name, + tag_name_from.color, + copied_indices[0] + 1, + copied_indices[-1] + 1 + ) + ) project.undo_redo.add_do_method(Global.undo_or_redo.bind(false)) project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true)) # Note: temporarily set the selected cels to an empty array (needed for undo/redo) diff --git a/src/UI/Timeline/AnimationTimeline.tscn b/src/UI/Timeline/AnimationTimeline.tscn index 3a709109a..cce4d28ca 100644 --- a/src/UI/Timeline/AnimationTimeline.tscn +++ b/src/UI/Timeline/AnimationTimeline.tscn @@ -952,9 +952,63 @@ offset_bottom = 40.0 mouse_filter = 2 color = Color(0, 0.741176, 1, 0.501961) -[node name="PasteTagPopup" type="PopupMenu" parent="."] +[node name="PasteTagPopup" type="Popup" parent="."] +size = Vector2i(250, 307) +visible = true +min_size = Vector2i(250, 0) script = ExtResource("12") +[node name="PanelContainer" type="PanelContainer" parent="PasteTagPopup"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="PasteTagPopup/PanelContainer"] +layout_mode = 2 + +[node name="Title" type="Label" parent="PasteTagPopup/PanelContainer/VBoxContainer"] +layout_mode = 2 +theme_type_variation = &"HeaderSmall" +text = "Import Tags" +horizontal_alignment = 1 + +[node name="HBoxContainer" type="HBoxContainer" parent="PasteTagPopup/PanelContainer/VBoxContainer"] +layout_mode = 2 + +[node name="Label" type="Label" parent="PasteTagPopup/PanelContainer/VBoxContainer/HBoxContainer"] +layout_mode = 2 +text = "From: " + +[node name="ProjectList" type="OptionButton" parent="PasteTagPopup/PanelContainer/VBoxContainer/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +alignment = 1 + +[node name="CreateTags" type="CheckButton" parent="PasteTagPopup/PanelContainer/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Create new tags" + +[node name="Instructions" type="Label" parent="PasteTagPopup/PanelContainer/VBoxContainer"] +layout_mode = 2 +text = "Available tags:" +autowrap_mode = 3 + +[node name="TagList" type="ItemList" parent="PasteTagPopup/PanelContainer/VBoxContainer"] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 150) +layout_mode = 2 +size_flags_vertical = 3 + +[node name="StartFrame" type="Label" parent="PasteTagPopup/PanelContainer/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +horizontal_alignment = 1 +autowrap_mode = 3 + [connection signal="pressed" from="TimelineContainer/TimelineButtons/LayerTools/LayerSettingsContainer/LayerButtons/AddLayer" to="." method="add_layer"] [connection signal="pressed" from="TimelineContainer/TimelineButtons/LayerTools/LayerSettingsContainer/LayerButtons/RemoveLayer" to="." method="_on_RemoveLayer_pressed"] [connection signal="pressed" from="TimelineContainer/TimelineButtons/LayerTools/LayerSettingsContainer/LayerButtons/MoveUpLayer" to="." method="change_layer_order" binds= [true]] diff --git a/src/UI/Timeline/PasteTagPopup.gd b/src/UI/Timeline/PasteTagPopup.gd index 7d1efa5dc..278412900 100644 --- a/src/UI/Timeline/PasteTagPopup.gd +++ b/src/UI/Timeline/PasteTagPopup.gd @@ -1,37 +1,237 @@ -extends PopupMenu +extends Popup -@onready var tag_container: Control = Global.animation_timeline.find_child("TagContainer") +var from_project: Project +var create_new_tags := false + +@onready var from_project_list: OptionButton = %ProjectList +@onready var create_tags: CheckButton = %CreateTags +@onready var animation_tags_list: ItemList = %TagList +@onready var start_frame: Label = %StartFrame func _ready() -> void: - id_pressed.connect(_on_TagList_id_pressed) - tag_container.gui_input.connect(_on_TagContainer_gui_input) + var tag_container: Control = Global.animation_timeline.find_child("TagContainer") + # connect signals + tag_container.connect("gui_input", _on_TagContainer_gui_input) + from_project_list.connect("item_selected", _on_FromProject_changed) + animation_tags_list.connect("item_selected", _on_TagList_id_pressed) + create_tags.connect("toggled", _on_CreateTags_toggled) + + +func refresh_list() -> void: + animation_tags_list.clear() + for tag in from_project.animation_tags: + var img = Image.create(5, 5, true, Image.FORMAT_RGBA8) + img.fill(tag.color) + var tex = ImageTexture.create_from_image(img) + var tag_title = tag.name + if tag_title == "": + tag_title = "(Untitled)" + animation_tags_list.add_item(tag_title, tex) + + +func _on_CreateTags_toggled(pressed: bool) -> void: + create_new_tags = pressed func _on_TagContainer_gui_input(event: InputEvent) -> void: if !event is InputEventMouseButton: return if Input.is_action_just_released("right_mouse"): - clear() - if Global.current_project.animation_tags.is_empty(): - return - add_separator("Paste content from tag:") - for tag in Global.current_project.animation_tags: - var img := Image.create(5, 5, true, Image.FORMAT_RGBA8) - img.fill(tag.color) - var tex := ImageTexture.create_from_image(img) - var tag_name := tag.name - if tag_name == "": - tag_name = "(Untitled)" - add_icon_item(tex, tag_name) + # Reset UI + from_project_list.clear() + if Global.projects.find(from_project) < 0: + from_project = Global.current_project + # Populate project list + for project in Global.projects: + from_project_list.add_item(project.name) + from_project_list.select(Global.projects.find(from_project)) + # Populate tag list + refresh_list() var frame_idx := Global.current_project.current_frame + 2 - add_separator(str("The pasted frames will start at (Frame ", frame_idx, ")")) - popup(Rect2i(tag_container.get_global_mouse_position(), Vector2.ONE)) + start_frame.text = str("The pasted frames will start at (Frame ", frame_idx, ")") + popup(Rect2i(Global.control.get_global_mouse_position(), Vector2i.ONE)) + + +func _on_FromProject_changed(id: int) -> void: + from_project = Global.projects[id] + refresh_list() func _on_TagList_id_pressed(id: int) -> void: - var tag: AnimationTag = Global.current_project.animation_tags[id - 1] + var tag: AnimationTag = from_project.animation_tags[id] var frames = [] for i in range(tag.from - 1, tag.to): frames.append(i) - Global.animation_timeline.copy_frames(frames, Global.current_project.current_frame) + if create_new_tags: + add_animation(frames, Global.current_project.current_frame, tag) + else: + add_animation(frames, Global.current_project.current_frame) + hide() + + +## Gets frame indices of [member from_project] and dumps it in the current project. +func add_animation(indices: Array, destination: int, from_tag: AnimationTag = null): + var project: Project = Global.current_project + if from_project == project: ## If we are copying tags within project + Global.animation_timeline.copy_frames(indices, destination, true, from_tag) + return + var new_animation_tags := project.animation_tags.duplicate() + # Loop through the tags to create new classes for them, so that they won't be the same + # as project.animation_tags's classes. Needed for undo/redo to work properly. + for i in new_animation_tags.size(): + new_animation_tags[i] = AnimationTag.new( + new_animation_tags[i].name, + new_animation_tags[i].color, + new_animation_tags[i].from, + new_animation_tags[i].to + ) + var imported_frames: Array[Frame] = [] # The copied frames + # the indices of newly copied frames + var copied_indices: PackedInt32Array = range( + destination + 1, (destination + 1) + indices.size() + ) + project.undos += 1 + project.undo_redo.create_action("Import Tag") + # Step 1: calculate layers to generate + var layer_to_names := PackedStringArray() # names of currently existing layers + for l in project.layers: + layer_to_names.append(l.name) + + # the goal of this section is to mark existing layers with their indices else with -1 + var layer_from_to := {} # indices of layers from and to + for from in from_project.layers.size(): + var to = layer_to_names.find(from_project.layers[from].name) + if project.layers[to].get_layer_type() != from_project.layers[from].get_layer_type(): + to = -1 + if to in layer_from_to.values(): # from_project has layers with duplicate frames + to = -1 + layer_from_to[from] = to + + # Step 2: generate required layers + var combined_copy := Array() # Makes calculations easy + combined_copy.append_array(project.layers) + var added_layers := Array() # Array of layers + var added_idx := Array() # Array of indices to add the respective layers (in added_layers) to + var added_cels := Array() # Array of an Array of cels (added in same order as their layer) + + if layer_from_to.values().count(-1) > 0: + # As it is extracted from a dictionary, so i assume the keys aren't sorted + var from_layers_size = layer_from_to.keys().duplicate(true) + from_layers_size.sort() # it's values should now be from (layer size - 1) to zero + for i in from_layers_size: + if layer_from_to[i] == -1: + var type = from_project.layers[i].get_layer_type() + var l: BaseLayer + match type: + Global.LayerTypes.PIXEL: + l = PixelLayer.new(project) + Global.LayerTypes.GROUP: + l = GroupLayer.new(project) + Global.LayerTypes.THREE_D: + l = Layer3D.new(project) + var cels := [] + for f in project.frames: + cels.append(l.new_empty_cel()) + l.name = from_project.layers[i].name # this will set it to the required layer name + + # Set an appropriate parent + var new_layer_idx = combined_copy.size() + layer_from_to[i] = new_layer_idx + var from_children = from_project.layers[i].get_children(false) + for from_child in from_children: # If this layer had children + var child_to_idx = layer_from_to[from_project.layers.find(from_child)] + var to_child = combined_copy[child_to_idx] + if to_child in added_layers: # if child was added recently + to_child.parent = l + + combined_copy.insert(new_layer_idx, l) + added_layers.append(l) # layer is now added + added_idx.append(new_layer_idx) # at index new_layer_idx + added_cels.append(cels) # with cels + + # Now initiate import + for f in indices: + var src_frame: Frame = from_project.frames[f] + var new_frame := Frame.new() + imported_frames.append(new_frame) + new_frame.duration = src_frame.duration + for to in combined_copy.size(): + var new_cel: BaseCel + if to in layer_from_to.values(): + var from = layer_from_to.find_key(to) + # Cel we're copying from, the source + var src_cel: BaseCel = from_project.frames[f].cels[from] + var selected_id := -1 + if src_cel is Cel3D: + new_cel = src_cel.get_script().new( + project.size, false, src_cel.object_properties, src_cel.scene_properties + ) + if src_cel.selected != null: + selected_id = src_cel.selected.id + else: + new_cel = src_cel.get_script().new() + + # add more types here if they have a copy_content() method + if src_cel is PixelCel: + var src_img = src_cel.copy_content() + var copy := Image.create( + project.size.x, project.size.y, false, Image.FORMAT_RGBA8 + ) + copy.blit_rect( + src_img, Rect2(Vector2.ZERO, src_img.get_size()), Vector2.ZERO + ) + new_cel.set_content(copy) + new_cel.opacity = src_cel.opacity + + if new_cel is Cel3D: + if selected_id in new_cel.object_properties.keys(): + if selected_id != -1: + new_cel.selected = new_cel.get_object_from_id(selected_id) + else: + new_cel = combined_copy[to].new_empty_cel() + new_frame.cels.append(new_cel) + + for tag in new_animation_tags: # Loop through the tags to see if the frame is in one + if copied_indices[0] >= tag.from && copied_indices[0] <= tag.to: + tag.to += 1 + elif copied_indices[0] < tag.from: + tag.from += 1 + tag.to += 1 + if from_tag: + new_animation_tags.append( + AnimationTag.new( + from_tag.name, from_tag.color, copied_indices[0] + 1, copied_indices[-1] + 1 + ) + ) + project.undo_redo.add_undo_method(project.remove_frames.bind(copied_indices)) + project.undo_redo.add_do_method(project.add_layers.bind(added_layers, added_idx, added_cels)) + project.undo_redo.add_do_method(project.add_frames.bind(imported_frames, copied_indices)) + project.undo_redo.add_undo_method(project.remove_layers.bind(added_idx)) + # Note: temporarily set the selected cels to an empty array (needed for undo/redo) + project.undo_redo.add_do_property(Global.current_project, "selected_cels", []) + project.undo_redo.add_undo_property(Global.current_project, "selected_cels", []) + + var all_new_cels = [] + # Select all the new frames so that it is easier to move/offset collectively if user wants + # To ease animation workflow, new current frame is the first copied frame instead of the last + var range_start: int = copied_indices[-1] + var range_end: int = copied_indices[0] + var frame_diff_sign := signi(range_end - range_start) + if frame_diff_sign == 0: + frame_diff_sign = 1 + for i in range(range_start, range_end + frame_diff_sign, frame_diff_sign): + for j in range(0, combined_copy.size()): + var frame_layer := [i, j] + if !all_new_cels.has(frame_layer): + all_new_cels.append(frame_layer) + project.undo_redo.add_do_property(Global.current_project, "selected_cels", all_new_cels) + project.undo_redo.add_undo_method( + project.change_cel.bind(project.current_frame, project.current_layer) + ) + project.undo_redo.add_do_method(project.change_cel.bind(range_end)) + project.undo_redo.add_do_property(project, "animation_tags", new_animation_tags) + project.undo_redo.add_undo_property(project, "animation_tags", project.animation_tags) + project.undo_redo.add_do_method(Global.undo_or_redo.bind(false)) + project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true)) + project.undo_redo.commit_action()