From a80af97377d788132316891ea0043bb2727bf825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lera=20Elvo=C3=A9?= Date: Wed, 17 Apr 2024 02:06:48 +0000 Subject: [PATCH] add toast notifications (#148) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes #138 Reviewed-on: https://codeberg.org/StreamGraph/StreamGraph/pulls/148 Co-authored-by: Lera ElvoƩ Co-committed-by: Lera ElvoƩ --- classes/deck/deck.gd | 71 ++++++++++-------- classes/deck/deck_node.gd | 6 +- classes/deck/logger.gd | 20 +++++ classes/deck/nodes/group/group_input_node.gd | 4 +- classes/deck/nodes/group/group_output_node.gd | 4 +- graph_node_renderer/deck_holder_renderer.tscn | 6 +- graph_node_renderer/default_theme.tres | 16 +++- .../textures/error_icon_color.svg | 1 + .../textures/error_icon_color.svg.import | 37 +++++++++ graph_node_renderer/textures/info_icon.svg | 1 + .../textures/info_icon.svg.import | 37 +++++++++ .../textures/warning_icon_color.svg | 1 + .../textures/warning_icon_color.svg.import | 37 +++++++++ graph_node_renderer/toast_renderer.gd | 75 +++++++++++++++++++ graph_node_renderer/toast_renderer.tscn | 27 +++++++ 15 files changed, 301 insertions(+), 42 deletions(-) create mode 100644 graph_node_renderer/textures/error_icon_color.svg create mode 100644 graph_node_renderer/textures/error_icon_color.svg.import create mode 100644 graph_node_renderer/textures/info_icon.svg create mode 100644 graph_node_renderer/textures/info_icon.svg.import create mode 100644 graph_node_renderer/textures/warning_icon_color.svg create mode 100644 graph_node_renderer/textures/warning_icon_color.svg.import create mode 100644 graph_node_renderer/toast_renderer.gd create mode 100644 graph_node_renderer/toast_renderer.tscn diff --git a/classes/deck/deck.gd b/classes/deck/deck.gd index e802616..9a322b2 100644 --- a/classes/deck/deck.gd +++ b/classes/deck/deck.gd @@ -367,37 +367,37 @@ func copy_nodes(nodes_to_copy: Array[String]) -> Dictionary: for node_id: String in nodes_to_copy: d.nodes[node_id] = get_node(node_id).to_dict() - for node: String in d.nodes: - var outgoing_connections: Dictionary = d.nodes[node].outgoing_connections.duplicate(true) - - for from_port: int in outgoing_connections: - for to_node: String in outgoing_connections[from_port]: - if to_node not in nodes_to_copy: - (d.nodes[node].outgoing_connections[from_port] as Dictionary).erase(to_node) - - var keys_to_erase := [] - for from_port: int in d.nodes[node].outgoing_connections: - if (d.nodes[node].outgoing_connections[from_port] as Dictionary).is_empty(): - keys_to_erase.append(from_port) - - for key in keys_to_erase: - (d.nodes[node].outgoing_connections as Dictionary).erase(key) - - var incoming_connections: Dictionary = d.nodes[node].incoming_connections.duplicate(true) - - for to_port: int in incoming_connections: - for from_node: String in incoming_connections[to_port]: - if from_node not in nodes_to_copy: - (d.nodes[node].incoming_connections[to_port] as Dictionary).erase(from_node) - - keys_to_erase.clear() - for to_port: int in d.nodes[node].incoming_connections: - if (d.nodes[node].incoming_connections[to_port] as Dictionary).is_empty(): - keys_to_erase.append(to_port) - - for key in keys_to_erase: - (d.nodes[node].incoming_connections as Dictionary).erase(key) - keys_to_erase.clear() + #for node: String in d.nodes: + #var outgoing_connections: Dictionary = d.nodes[node].outgoing_connections.duplicate(true) +# + #for from_port: int in outgoing_connections: + #for to_node: String in outgoing_connections[from_port]: + #if to_node not in nodes_to_copy: + #(d.nodes[node].outgoing_connections[from_port] as Dictionary).erase(to_node) +# + #var keys_to_erase := [] + #for from_port: int in d.nodes[node].outgoing_connections: + #if (d.nodes[node].outgoing_connections[from_port] as Dictionary).is_empty(): + #keys_to_erase.append(from_port) +# + #for key in keys_to_erase: + #(d.nodes[node].outgoing_connections as Dictionary).erase(key) +# + #var incoming_connections: Dictionary = d.nodes[node].incoming_connections.duplicate(true) +# + #for to_port: int in incoming_connections: + #for from_node: String in incoming_connections[to_port]: + #if from_node not in nodes_to_copy: + #(d.nodes[node].incoming_connections[to_port] as Dictionary).erase(from_node) + # + #keys_to_erase.clear() + #for to_port: int in d.nodes[node].incoming_connections: + #if (d.nodes[node].incoming_connections[to_port] as Dictionary).is_empty(): + #keys_to_erase.append(to_port) +# + #for key in keys_to_erase: + #(d.nodes[node].incoming_connections as Dictionary).erase(key) + #keys_to_erase.clear() for node: Dictionary in d.nodes.values().slice(1): node.position.x = node.position.x - d.nodes.values()[0].position.x @@ -468,10 +468,15 @@ func paste_nodes_from_dict(nodes_to_paste: Dictionary, position: Vector2 = Vecto add_node_inst(node, ids_map[node_id]) if group_needs_unique: node.make_unique() + DeckHolder.logger.toast_warn("Group was made unique.") func paste_nodes_from_json(json: String, position: Vector2 = Vector2()) -> void: - paste_nodes_from_dict(JSON.parse_string(json), position) + var d = JSON.parse_string(json) + if not d: + DeckHolder.logger.toast_error("Paste failed.") + return + paste_nodes_from_dict(d, position) func duplicate_nodes(nodes_to_copy: Array[String]) -> void: @@ -595,7 +600,7 @@ static func from_dict(data: Dictionary, path: String = "") -> Deck: var nodes_data: Dictionary = data.deck.nodes as Dictionary for node_id in nodes_data: - var node := DeckNode.from_dict(nodes_data[node_id]) + var node := DeckNode.from_dict(nodes_data[node_id], deck.connections) deck.add_node_inst(node, node_id) var groups_data: Dictionary = data.deck.groups as Dictionary diff --git a/classes/deck/deck_node.gd b/classes/deck/deck_node.gd index b234306..1070b6d 100644 --- a/classes/deck/deck_node.gd +++ b/classes/deck/deck_node.gd @@ -239,7 +239,7 @@ func _pre_port_load() -> void: ## Virtual function that's called after the node has been deserialized. -func _post_load() -> void: +func _post_load(connections: Deck.NodeConnections) -> void: pass @@ -289,7 +289,7 @@ func to_dict(with_meta: bool = true) -> Dictionary: return d -static func from_dict(data: Dictionary) -> DeckNode: +static func from_dict(data: Dictionary, connections: Deck.NodeConnections = null) -> DeckNode: var node := NodeDB.instance_node(data.node_type) #node._id = data._id node.name = data.name @@ -326,7 +326,7 @@ static func from_dict(data: Dictionary) -> DeckNode: for key in data.meta: node.set_meta(key, str_to_var(data.meta[key])) - node._post_load() + node._post_load(connections) return node diff --git a/classes/deck/logger.gd b/classes/deck/logger.gd index f71a287..c219650 100644 --- a/classes/deck/logger.gd +++ b/classes/deck/logger.gd @@ -17,6 +17,7 @@ enum LogCategory { } signal log_message(text: String, type: LogType, category: LogCategory) +signal log_toast(text: String, type: LogType) func log_node(text: Variant, type: LogType = LogType.INFO) -> void: @@ -40,3 +41,22 @@ func log(text: String, type: LogType, category: LogCategory) -> void: if OS.has_feature("editor"): prints(LogType.keys()[type].capitalize(), LogCategory.keys()[category].capitalize(), text) + + +func toast_info(text: String) -> void: + toast(text, LogType.INFO) + + +func toast_warn(text: String) -> void: + toast(text, LogType.WARN) + + +func toast_error(text: String) -> void: + toast(text, LogType.ERROR) + + +func toast(text: String, type: LogType) -> void: + log_toast.emit(text, type) + + if OS.has_feature("editor"): + prints("(t)", LogType.keys()[type].capitalize(), text) diff --git a/classes/deck/nodes/group/group_input_node.gd b/classes/deck/nodes/group/group_input_node.gd index 982631f..221e317 100644 --- a/classes/deck/nodes/group/group_input_node.gd +++ b/classes/deck/nodes/group/group_input_node.gd @@ -59,10 +59,10 @@ func _pre_port_load() -> void: ) -func _post_load() -> void: +func _post_load(connections: Deck.NodeConnections) -> void: # ensure we have enough ports after connections var last_connected_port := 0 - var outgoing_connections = _belonging_to.connections.get_all_outgoing_connections(_id) + var outgoing_connections = connections.get_all_outgoing_connections(_id) for port: int in outgoing_connections: last_connected_port = port if outgoing_connections.has(port) else last_connected_port diff --git a/classes/deck/nodes/group/group_output_node.gd b/classes/deck/nodes/group/group_output_node.gd index 10781a8..e04606d 100644 --- a/classes/deck/nodes/group/group_output_node.gd +++ b/classes/deck/nodes/group/group_output_node.gd @@ -60,10 +60,10 @@ func _pre_port_load() -> void: ) -func _post_load() -> void: +func _post_load(connections: Deck.NodeConnections) -> void: # ensure we have enough ports after connections var last_connected_port := 0 - var incoming_connections := _belonging_to.connections.get_all_incoming_connections(_id) + var incoming_connections := connections.get_all_incoming_connections(_id) for port: int in incoming_connections: last_connected_port = port if incoming_connections.has(port) else last_connected_port diff --git a/graph_node_renderer/deck_holder_renderer.tscn b/graph_node_renderer/deck_holder_renderer.tscn index 548e50f..c072686 100644 --- a/graph_node_renderer/deck_holder_renderer.tscn +++ b/graph_node_renderer/deck_holder_renderer.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=18 format=3 uid="uid://duaah5x0jhkn6"] +[gd_scene load_steps=19 format=3 uid="uid://duaah5x0jhkn6"] [ext_resource type="Script" path="res://graph_node_renderer/deck_holder_renderer.gd" id="1_67g2g"] [ext_resource type="PackedScene" uid="uid://b84f2ngtcm5b8" path="res://graph_node_renderer/tab_container_custom.tscn" id="1_s3ug2"] @@ -9,6 +9,7 @@ [ext_resource type="Script" path="res://addons/no_twitch/twitch_connection.gd" id="5_3n36q"] [ext_resource type="PackedScene" uid="uid://cddfyvpf5nqq7" path="res://graph_node_renderer/debug_nodes_list.tscn" id="5_pnfg8"] [ext_resource type="PackedScene" uid="uid://eioso6jb42jy" path="res://graph_node_renderer/obs_websocket_setup_dialog.tscn" id="5_uo2gj"] +[ext_resource type="PackedScene" uid="uid://234k3tn8ny01" path="res://graph_node_renderer/toast_renderer.tscn" id="6_5532r"] [ext_resource type="PackedScene" uid="uid://b56hjad0ih0gu" path="res://graph_node_renderer/sidebar/sidebar.tscn" id="7_0lv5b"] [ext_resource type="PackedScene" uid="uid://bq2lxmbnic4lc" path="res://graph_node_renderer/twitch_setup_dialog.tscn" id="7_7rhap"] [ext_resource type="PackedScene" uid="uid://cuwou2aa7qfc2" path="res://graph_node_renderer/unsaved_changes_dialog_single_deck.tscn" id="8_qf6ve"] @@ -128,6 +129,9 @@ item_1/id = 1 unique_name_in_owner = true layout_mode = 2 +[node name="ToastRenderer" parent="MarginContainer/SidebarSplit/BottomSplit/VBoxContainer" instance=ExtResource("6_5532r")] +layout_mode = 2 + [node name="BottomDock" parent="MarginContainer/SidebarSplit/BottomSplit" instance=ExtResource("4_gwnhy")] unique_name_in_owner = true visible = false diff --git a/graph_node_renderer/default_theme.tres b/graph_node_renderer/default_theme.tres index 55509cb..0a26704 100644 --- a/graph_node_renderer/default_theme.tres +++ b/graph_node_renderer/default_theme.tres @@ -1,4 +1,4 @@ -[gd_resource type="Theme" load_steps=5 format=3 uid="uid://dqqdqscid2iem"] +[gd_resource type="Theme" load_steps=6 format=3 uid="uid://dqqdqscid2iem"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dt61u"] content_margin_left = 4.0 @@ -36,6 +36,18 @@ corner_radius_bottom_left = 4 [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_q56mu"] bg_color = Color(0.623529, 0.564706, 0.909804, 0.0588235) +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_w8gqp"] +content_margin_left = 8.0 +content_margin_top = 8.0 +content_margin_right = 8.0 +content_margin_bottom = 8.0 +bg_color = Color(0.12549, 0.12549, 0.12549, 0.627451) +border_color = Color(0.545098, 0.545098, 0.545098, 0.627451) +corner_radius_top_left = 4 +corner_radius_top_right = 4 +corner_radius_bottom_right = 4 +corner_radius_bottom_left = 4 + [resource] default_font_size = 14 AccordionButton/base_type = &"Button" @@ -43,3 +55,5 @@ AccordionButton/styles/hover = SubResource("StyleBoxFlat_dt61u") AccordionButton/styles/normal = SubResource("StyleBoxFlat_fmm5o") AccordionButton/styles/pressed = SubResource("StyleBoxFlat_5q5t0") AccordionMenu/styles/background = SubResource("StyleBoxFlat_q56mu") +ToastPanel/base_type = &"PanelContainer" +ToastPanel/styles/panel = SubResource("StyleBoxFlat_w8gqp") diff --git a/graph_node_renderer/textures/error_icon_color.svg b/graph_node_renderer/textures/error_icon_color.svg new file mode 100644 index 0000000..d6e3ebe --- /dev/null +++ b/graph_node_renderer/textures/error_icon_color.svg @@ -0,0 +1 @@ + diff --git a/graph_node_renderer/textures/error_icon_color.svg.import b/graph_node_renderer/textures/error_icon_color.svg.import new file mode 100644 index 0000000..2a1aa75 --- /dev/null +++ b/graph_node_renderer/textures/error_icon_color.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://t3s83w2wkyqp" +path="res://.godot/imported/error_icon_color.svg-36e8290c5e271c0fa763289e90b46d3d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://graph_node_renderer/textures/error_icon_color.svg" +dest_files=["res://.godot/imported/error_icon_color.svg-36e8290c5e271c0fa763289e90b46d3d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/graph_node_renderer/textures/info_icon.svg b/graph_node_renderer/textures/info_icon.svg new file mode 100644 index 0000000..732bccf --- /dev/null +++ b/graph_node_renderer/textures/info_icon.svg @@ -0,0 +1 @@ + diff --git a/graph_node_renderer/textures/info_icon.svg.import b/graph_node_renderer/textures/info_icon.svg.import new file mode 100644 index 0000000..beb5920 --- /dev/null +++ b/graph_node_renderer/textures/info_icon.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bdv8ejbjaujvm" +path="res://.godot/imported/info_icon.svg-c3f8f29486a0a0cd1cfd29f9859897eb.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://graph_node_renderer/textures/info_icon.svg" +dest_files=["res://.godot/imported/info_icon.svg-c3f8f29486a0a0cd1cfd29f9859897eb.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/graph_node_renderer/textures/warning_icon_color.svg b/graph_node_renderer/textures/warning_icon_color.svg new file mode 100644 index 0000000..1f7b900 --- /dev/null +++ b/graph_node_renderer/textures/warning_icon_color.svg @@ -0,0 +1 @@ + diff --git a/graph_node_renderer/textures/warning_icon_color.svg.import b/graph_node_renderer/textures/warning_icon_color.svg.import new file mode 100644 index 0000000..8c6e94e --- /dev/null +++ b/graph_node_renderer/textures/warning_icon_color.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cmqrl3e53unmv" +path="res://.godot/imported/warning_icon_color.svg-944b7caf1a46684fe55272e6b2f36f61.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://graph_node_renderer/textures/warning_icon_color.svg" +dest_files=["res://.godot/imported/warning_icon_color.svg-944b7caf1a46684fe55272e6b2f36f61.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/graph_node_renderer/toast_renderer.gd b/graph_node_renderer/toast_renderer.gd new file mode 100644 index 0000000..3c1b8ef --- /dev/null +++ b/graph_node_renderer/toast_renderer.gd @@ -0,0 +1,75 @@ +# (c) 2023-present Eroax +# (c) 2023-present Yagich +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +extends Control +class_name ToastRenderer + + +const ERROR_ICON = preload("res://graph_node_renderer/textures/error_icon_color.svg") +const WARNING_ICON = preload("res://graph_node_renderer/textures/warning_icon_color.svg") +const INFO_ICON = preload("res://graph_node_renderer/textures/info_icon.svg") + +@export var hold_time: float = 1.5 +@export var limit: int = 5 + +@onready var vbox: VBoxContainer = %VBoxContainer + + +func _ready() -> void: + DeckHolder.logger.log_toast.connect(_on_logger_toast) + + +func _on_logger_toast(text: String, type: Logger.LogType) -> void: + var panel := PanelContainer.new() + panel.theme_type_variation = &"ToastPanel" + var hb := HBoxContainer.new() + var icon := TextureRect.new() + icon.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL + + match type: + Logger.LogType.INFO: + icon.texture = INFO_ICON + Logger.LogType.WARN: + icon.texture = WARNING_ICON + Logger.LogType.ERROR: + icon.texture = ERROR_ICON + + var label := Label.new() + label.text = text + + hb.add_child(icon) + hb.add_child(label) + + panel.add_child(hb) + + move_and_remove() + + panel.modulate.a = 0.0 + vbox.add_child(panel) + var t := panel.create_tween() + t.tween_property(panel, ^"modulate:a", 1.0, 0.1) + t.tween_interval(hold_time) + t.tween_property(panel, ^"modulate:a", 0.0, 1.0) + t.finished.connect( + func(): + panel.queue_free() + ) + + +func move_and_remove() -> void: + if not (vbox.get_child_count() + 1 > limit): + return + + for i: Node in vbox.get_children().slice(0, limit): + if i.get_meta(&"queued", false): + continue + + i.set_meta(&"queued", true) + var t := i.create_tween() + t.tween_property(i, ^"position:x", i.position.x + i.size.x + 32, 0.4).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_IN) + t.finished.connect( + func(): + i.queue_free() + ) + break + diff --git a/graph_node_renderer/toast_renderer.tscn b/graph_node_renderer/toast_renderer.tscn new file mode 100644 index 0000000..969d00e --- /dev/null +++ b/graph_node_renderer/toast_renderer.tscn @@ -0,0 +1,27 @@ +[gd_scene load_steps=2 format=3 uid="uid://234k3tn8ny01"] + +[ext_resource type="Script" path="res://graph_node_renderer/toast_renderer.gd" id="1_446pb"] + +[node name="ToastRenderer" type="Control"] +layout_mode = 3 +anchors_preset = 0 +script = ExtResource("1_446pb") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 3 +anchor_left = 1.0 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -490.0 +offset_top = -312.0 +offset_right = -32.0 +offset_bottom = -32.0 +grow_horizontal = 0 +grow_vertical = 0 +alignment = 2 + +[node name="PanelContainer" type="PanelContainer" parent="VBoxContainer"] +layout_mode = 2