diff --git a/Translations/Translations.pot b/Translations/Translations.pot index f62db50c5..00a40b52a 100644 --- a/Translations/Translations.pot +++ b/Translations/Translations.pot @@ -1881,6 +1881,9 @@ msgstr "" msgid "Frame properties" msgstr "" +msgid "Layer properties" +msgstr "" + msgid "Cel properties" msgstr "" diff --git a/src/Classes/Layers/BaseLayer.gd b/src/Classes/Layers/BaseLayer.gd index 13f334009..b5431e58f 100644 --- a/src/Classes/Layers/BaseLayer.gd +++ b/src/Classes/Layers/BaseLayer.gd @@ -2,6 +2,8 @@ class_name BaseLayer extends RefCounted ## Base class for layer properties. Different layer types extend from this class. +signal name_changed ## Emits when [member name] is changed. + ## All currently supported layer blend modes between two layers. The upper layer ## is the blend layer, and the bottom layer is the base layer. ## For more information, refer to: [url]https://en.wikipedia.org/wiki/Blend_modes[/url] @@ -28,7 +30,10 @@ enum BlendModes { LUMINOSITY ## Uses the blend luminosity while preserving the base hue and saturation. } -var name := "" ## Name of the layer. +var name := "": ## Name of the layer. + set(value): + name = value + name_changed.emit() var project: Project ## The project the layer belongs to. var index: int ## Index of layer in the timeline. var parent: BaseLayer ## Parent of the layer. @@ -41,6 +46,7 @@ var opacity := 1.0 ## The opacity of the layer, affects all frames that belong var cel_link_sets: Array[Dictionary] = [] ## Each Dictionary represents a cel's "link set" var effects: Array[LayerEffect] ## An array for non-destructive effects of the layer. var effects_enabled := true ## If [code]true[/code], the effects are being applied. +var user_data := "" ## User defined data, set in the layer properties. ## Returns true if this is a direct or indirect parent of layer @@ -221,6 +227,8 @@ func serialize() -> Dictionary: "parent": parent.index if is_instance_valid(parent) else -1, "effects": effect_data } + if not user_data.is_empty(): + dict["user_data"] = user_data if not cel_link_sets.is_empty(): var cels := [] # Cels array for easy finding of the frame index for link_set saving for frame in project.frames: @@ -241,6 +249,7 @@ func deserialize(dict: Dictionary) -> void: blend_mode = dict.get("blend_mode", BlendModes.NORMAL) clipping_mask = dict.get("clipping_mask", false) opacity = dict.get("opacity", 1.0) + user_data = dict.get("user_data", user_data) if dict.get("parent", -1) != -1: parent = project.layers[dict.parent] if dict.has("linked_cels") and not dict["linked_cels"].is_empty(): # Backwards compatibility diff --git a/src/Main.tscn b/src/Main.tscn index 27dacfe11..4c81b55e4 100644 --- a/src/Main.tscn +++ b/src/Main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=15 format=3 uid="uid://dbylw5k04ulp8"] +[gd_scene load_steps=16 format=3 uid="uid://dbylw5k04ulp8"] [ext_resource type="Theme" uid="uid://cngbvfpwjoimv" path="res://assets/themes/dark/theme.tres" id="1"] [ext_resource type="Script" path="res://src/Main.gd" id="2"] @@ -10,6 +10,7 @@ [ext_resource type="PackedScene" uid="uid://bs3dnnvnxyp68" path="res://src/UI/Timeline/FrameProperties.tscn" id="9"] [ext_resource type="PackedScene" uid="uid://d4euwo633u33b" path="res://src/UI/Dialogs/SaveSprite.tscn" id="11"] [ext_resource type="PackedScene" uid="uid://b3aeqj2k58wdk" path="res://src/UI/Dialogs/OpenSprite.tscn" id="12"] +[ext_resource type="PackedScene" uid="uid://d3dt1gdlf7hox" path="res://src/UI/Timeline/LayerProperties.tscn" id="13_4dhva"] [ext_resource type="PackedScene" uid="uid://c0nuukjakmai2" path="res://src/UI/Dialogs/TileModeOffsetsDialog.tscn" id="14"] [ext_resource type="Script" path="res://src/HandleExtensions.gd" id="15_v0k2h"] [ext_resource type="PackedScene" uid="uid://clbjfkdupw52l" path="res://src/UI/Timeline/CelProperties.tscn" id="17_ucs64"] @@ -84,6 +85,8 @@ dialog_autowrap = true [node name="FrameProperties" parent="Dialogs" instance=ExtResource("9")] +[node name="LayerProperties" parent="Dialogs" instance=ExtResource("13_4dhva")] + [node name="TileModeOffsetsDialog" parent="Dialogs" instance=ExtResource("14")] [node name="Extensions" type="Control" parent="."] diff --git a/src/UI/Timeline/AnimationTimeline.gd b/src/UI/Timeline/AnimationTimeline.gd index db026451d..ccc2466be 100644 --- a/src/UI/Timeline/AnimationTimeline.gd +++ b/src/UI/Timeline/AnimationTimeline.gd @@ -42,7 +42,7 @@ var frame_tag_dialog: AcceptDialog: @onready var move_down_layer := %MoveDownLayer as Button @onready var merge_down_layer := %MergeDownLayer as Button @onready var blend_modes_button := %BlendModes as OptionButton -@onready var opacity_slider: ValueSlider = %OpacitySlider +@onready var opacity_slider := %OpacitySlider as ValueSlider @onready var frame_scroll_container := %FrameScrollContainer as Control @onready var frame_scroll_bar := %FrameScrollBar as HScrollBar @onready var tag_scroll_container := %TagScroll as ScrollContainer @@ -60,6 +60,7 @@ var frame_tag_dialog: AcceptDialog: func _ready() -> void: + Global.control.find_child("LayerProperties").visibility_changed.connect(_update_layer_ui) min_cel_size = get_tree().current_scene.theme.default_font_size + 24 layer_container.custom_minimum_size.x = layer_settings_container.size.x + 12 cel_size = min_cel_size @@ -1023,6 +1024,10 @@ func _on_onion_skinning_settings_visibility_changed() -> void: func _cel_switched() -> void: _toggle_frame_buttons() _toggle_layer_buttons() + _update_layer_ui() + + +func _update_layer_ui() -> void: var project := Global.current_project var layer_opacity := project.layers[project.current_layer].opacity opacity_slider.value = layer_opacity * 100 diff --git a/src/UI/Timeline/LayerButton.gd b/src/UI/Timeline/LayerButton.gd index 812ea95cc..3fd616860 100644 --- a/src/UI/Timeline/LayerButton.gd +++ b/src/UI/Timeline/LayerButton.gd @@ -15,6 +15,7 @@ var button_pressed := false: get: return main_button.button_pressed +@onready var properties: AcceptDialog = Global.control.find_child("LayerProperties") @onready var main_button := %LayerMainButton as Button @onready var expand_button := %ExpandButton as BaseButton @onready var visibility_button := %VisibilityButton as BaseButton @@ -32,6 +33,7 @@ func _ready() -> void: main_button.hierarchy_depth_pixel_shift = HIERARCHY_DEPTH_PIXEL_SHIFT Global.cel_switched.connect(func(): z_index = 1 if button_pressed else 0) var layer := Global.current_project.layers[layer_index] + layer.name_changed.connect(func(): label.text = layer.name) if layer is PixelLayer: linked_button.visible = true elif layer is GroupLayer: @@ -153,7 +155,6 @@ func _save_layer_name(new_name: String) -> void: label.visible = true line_edit.visible = false line_edit.editable = false - label.text = new_name if layer_index < Global.current_project.layers.size(): Global.current_project.layers[layer_index].name = new_name @@ -207,7 +208,22 @@ func _select_current_layer() -> void: func _on_popup_menu_id_pressed(id: int) -> void: var layer := Global.current_project.layers[layer_index] if id == 0: + properties.layer_indices = _get_layer_indices() + properties.popup_centered() + if id == 1: layer.clipping_mask = not layer.clipping_mask popup_menu.set_item_checked(0, layer.clipping_mask) clipping_mask_icon.visible = layer.clipping_mask Global.canvas.draw_layers() + + +func _get_layer_indices() -> Array: + var indices := [] + for cel in Global.current_project.selected_cels: + var l: int = cel[1] + if not l in indices: + indices.append(l) + indices.sort() + if not layer_index in indices: + indices = [layer_index] + return indices diff --git a/src/UI/Timeline/LayerButton.tscn b/src/UI/Timeline/LayerButton.tscn index 2f8200893..302c3d67d 100644 --- a/src/UI/Timeline/LayerButton.tscn +++ b/src/UI/Timeline/LayerButton.tscn @@ -163,10 +163,12 @@ caret_blink_interval = 0.5 [node name="PopupMenu" type="PopupMenu" parent="."] disable_3d = true -item_count = 1 -item_0/text = "Clipping mask" -item_0/checkable = 1 +item_count = 2 +item_0/text = "Properties" item_0/id = 0 +item_1/text = "Clipping mask" +item_1/checkable = 1 +item_1/id = 1 [connection signal="pressed" from="VisibilityButton" to="." method="_on_visibility_button_pressed"] [connection signal="pressed" from="LockButton" to="." method="_on_lock_button_pressed"] diff --git a/src/UI/Timeline/LayerProperties.gd b/src/UI/Timeline/LayerProperties.gd new file mode 100644 index 000000000..8fcd7767a --- /dev/null +++ b/src/UI/Timeline/LayerProperties.gd @@ -0,0 +1,78 @@ +extends AcceptDialog + +var layer_indices: Array + +@onready var name_line_edit := $GridContainer/NameLineEdit as LineEdit +@onready var opacity_slider := $GridContainer/OpacitySlider as ValueSlider +@onready var blend_modes_button := $GridContainer/BlendModeOptionButton as OptionButton +@onready var user_data_text_edit := $GridContainer/UserDataTextEdit as TextEdit + + +func _ready() -> void: + # Fill the blend modes OptionButton with items + blend_modes_button.add_item("Normal", BaseLayer.BlendModes.NORMAL) + blend_modes_button.add_item("Darken", BaseLayer.BlendModes.DARKEN) + blend_modes_button.add_item("Multiply", BaseLayer.BlendModes.MULTIPLY) + blend_modes_button.add_item("Color burn", BaseLayer.BlendModes.COLOR_BURN) + blend_modes_button.add_item("Linear burn", BaseLayer.BlendModes.LINEAR_BURN) + blend_modes_button.add_item("Lighten", BaseLayer.BlendModes.LIGHTEN) + blend_modes_button.add_item("Screen", BaseLayer.BlendModes.SCREEN) + blend_modes_button.add_item("Color dodge", BaseLayer.BlendModes.COLOR_DODGE) + blend_modes_button.add_item("Add", BaseLayer.BlendModes.ADD) + blend_modes_button.add_item("Overlay", BaseLayer.BlendModes.OVERLAY) + blend_modes_button.add_item("Soft light", BaseLayer.BlendModes.SOFT_LIGHT) + blend_modes_button.add_item("Hard light", BaseLayer.BlendModes.HARD_LIGHT) + blend_modes_button.add_item("Difference", BaseLayer.BlendModes.DIFFERENCE) + blend_modes_button.add_item("Exclusion", BaseLayer.BlendModes.EXCLUSION) + blend_modes_button.add_item("Subtract", BaseLayer.BlendModes.SUBTRACT) + blend_modes_button.add_item("Divide", BaseLayer.BlendModes.DIVIDE) + blend_modes_button.add_item("Hue", BaseLayer.BlendModes.HUE) + blend_modes_button.add_item("Saturation", BaseLayer.BlendModes.SATURATION) + blend_modes_button.add_item("Color", BaseLayer.BlendModes.COLOR) + blend_modes_button.add_item("Luminosity", BaseLayer.BlendModes.LUMINOSITY) + + +func _on_visibility_changed() -> void: + if layer_indices.size() == 0: + return + Global.dialog_open(visible) + var first_layer := Global.current_project.layers[layer_indices[0]] + if visible: + name_line_edit.text = first_layer.name + opacity_slider.value = first_layer.opacity * 100.0 + blend_modes_button.selected = first_layer.blend_mode + user_data_text_edit.text = first_layer.user_data + else: + layer_indices = [] + + +func _on_name_line_edit_text_changed(new_text: String) -> void: + if layer_indices.size() == 0: + return + for layer_index in layer_indices: + var layer := Global.current_project.layers[layer_index] + layer.name = new_text + + +func _on_opacity_slider_value_changed(value: float) -> void: + if layer_indices.size() == 0: + return + for layer_index in layer_indices: + var layer := Global.current_project.layers[layer_index] + layer.opacity = value / 100.0 + Global.canvas.queue_redraw() + + +func _on_blend_mode_option_button_item_selected(index: BaseLayer.BlendModes) -> void: + if layer_indices.size() == 0: + return + for layer_index in layer_indices: + var layer := Global.current_project.layers[layer_index] + layer.blend_mode = index + Global.canvas.queue_redraw() + + +func _on_user_data_text_edit_text_changed() -> void: + for layer_index in layer_indices: + var layer := Global.current_project.layers[layer_index] + layer.user_data = user_data_text_edit.text diff --git a/src/UI/Timeline/LayerProperties.tscn b/src/UI/Timeline/LayerProperties.tscn new file mode 100644 index 000000000..d833783a2 --- /dev/null +++ b/src/UI/Timeline/LayerProperties.tscn @@ -0,0 +1,69 @@ +[gd_scene load_steps=3 format=3 uid="uid://d3dt1gdlf7hox"] + +[ext_resource type="Script" path="res://src/UI/Timeline/LayerProperties.gd" id="1_54q1t"] +[ext_resource type="Script" path="res://src/UI/Nodes/ValueSlider.gd" id="2_bwpwc"] + +[node name="LayerProperties" type="AcceptDialog"] +title = "Layer properties" +exclusive = false +popup_window = true +script = ExtResource("1_54q1t") + +[node name="GridContainer" type="GridContainer" parent="."] +offset_right = 40.0 +offset_bottom = 40.0 +columns = 2 + +[node name="NameLabel" type="Label" parent="GridContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Name:" + +[node name="NameLineEdit" type="LineEdit" parent="GridContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="OpacityLabel" type="Label" parent="GridContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Opacity:" + +[node name="OpacitySlider" type="TextureProgressBar" parent="GridContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +focus_mode = 2 +mouse_default_cursor_shape = 2 +theme_type_variation = &"ValueSlider" +nine_patch_stretch = true +stretch_margin_left = 3 +stretch_margin_top = 3 +stretch_margin_right = 3 +stretch_margin_bottom = 3 +script = ExtResource("2_bwpwc") + +[node name="BlendModeLabel" type="Label" parent="GridContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Blend mode:" + +[node name="BlendModeOptionButton" type="OptionButton" parent="GridContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +mouse_default_cursor_shape = 2 + +[node name="UserDataLabel" type="Label" parent="GridContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 0 +text = "User data:" + +[node name="UserDataTextEdit" type="TextEdit" parent="GridContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +scroll_fit_content_height = true + +[connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"] +[connection signal="text_changed" from="GridContainer/NameLineEdit" to="." method="_on_name_line_edit_text_changed"] +[connection signal="value_changed" from="GridContainer/OpacitySlider" to="." method="_on_opacity_slider_value_changed"] +[connection signal="item_selected" from="GridContainer/BlendModeOptionButton" to="." method="_on_blend_mode_option_button_item_selected"] +[connection signal="text_changed" from="GridContainer/UserDataTextEdit" to="." method="_on_user_data_text_edit_text_changed"]