add toast notifications (#148)

closes #138

Reviewed-on: https://codeberg.org/StreamGraph/StreamGraph/pulls/148
Co-authored-by: Lera Elvoé <yagich@poto.cafe>
Co-committed-by: Lera Elvoé <yagich@poto.cafe>
This commit is contained in:
Lera Elvoé 2024-04-17 02:06:48 +00:00 committed by yagich
parent f24906715d
commit a80af97377
15 changed files with 301 additions and 42 deletions

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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")

View file

@ -0,0 +1 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M8 1a7 7 0 0 0 0 14A7 7 0 0 0 8 1zM5.172 3.758 8 6.586l2.828-2.828 1.414 1.414L9.414 8l2.828 2.828-1.414 1.414L8 9.414l-2.828 2.828-1.414-1.414L6.586 8 3.758 5.172l1.414-1.414z" fill="#ff5f5f"/></svg>

After

Width:  |  Height:  |  Size: 293 B

View file

@ -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

View file

@ -0,0 +1 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M8 1a7 7 0 0 0 0 14A7 7 0 0 0 8 1zM7 4h2v2H7zm0 3h2v5H7z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 170 B

View file

@ -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

View file

@ -0,0 +1 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M8 2a1 1 0 0 0-.85.484l-6 10A1 1 0 0 0 2 14h12a1 1 0 0 0 .857-1.514l-6-10a1 1 0 0 0-.85-.484zM7 5h2v5H7zm0 6h2v2H7z" fill="#ffdd65"/></svg>

After

Width:  |  Height:  |  Size: 232 B

View file

@ -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

View file

@ -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

View file

@ -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