improved Twitch connection menu experience

This commit is contained in:
2024-04-23 05:46:39 -05:00
parent d3beca8014
commit 8e01e9cb9b
10 changed files with 220 additions and 80 deletions

17
ChatMarginContainer.cs Normal file
View File

@@ -0,0 +1,17 @@
using Godot;
using System;
public partial class ChatMarginContainer : MarginContainer
{
public readonly string Title = "Twitch";
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
GetParentOrNull<TabContainer>().SetTabTitle(GetIndex(), Title);
}
// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
}
}

17
ConfigMarginContainer.cs Normal file
View File

@@ -0,0 +1,17 @@
using Godot;
using System;
public partial class ConfigMarginContainer : MarginContainer
{
public readonly string Title = "Config";
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
GetParentOrNull<TabContainer>().SetTabTitle(GetIndex(), Title);
}
// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
}
}

View File

@@ -47,7 +47,7 @@ public partial class ConfigStretchContainer : VBoxContainer
return StretchMode.Stretch;
throw new Exception($"No {nameof(StretchMode)} buttons pressed");
}
public void OkClicked()
public void Apply()
{
Settings.StretchMode = GetStretchMode();
}

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Godot;
@@ -86,4 +87,8 @@ public static class ExtensionHelper
else
return par.GetParentOfType<TNode>();
}
public static Vector2 Union(this Vector2 vect, Vector2 other)
=> new(Math.Max(vect.X, other.X), Math.Max(vect.Y, other.Y));
public static Vector2I Union(this Vector2I vect, Vector2 other)
=> new((int)Math.Max(vect.X, other.X), (int)Math.Max(vect.Y, other.Y));
}

View File

@@ -12,7 +12,7 @@ public partial class TwitchChatWatcher : Node
{
private readonly ClientWebSocket Socket = new();
public readonly ConcurrentQueue<TwitchChatMessage> Queue = new();
private readonly CancellationTokenSource TokenSource = new();
private CancellationTokenSource TokenSource = new();
public CancellationToken Token => TokenSource.Token;
private CommandHandler CommandHandler { get; set; }
public WebSocketState State => Socket.State;
@@ -22,6 +22,11 @@ public partial class TwitchChatWatcher : Node
[Signal]
public delegate void IncomingCommandEventHandler(Command command);
[Signal]
public delegate void SocketConnectedEventHandler();
[Signal]
public delegate void SocketDisconnectedEventHandler();
public string Channel { get; private set ;}
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
@@ -30,16 +35,11 @@ public partial class TwitchChatWatcher : Node
CommandHandler = GetNode<CommandHandler>("/root/CommandHandler")
?? throw new Exception($"{nameof(Command)} not found");
}
private readonly ConcurrentQueue<string> PrintQueue = new();
private readonly ConcurrentQueue<string> ErrorQueue = new();
// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
if (PrintQueue.TryDequeue(out string s))
GD.Print(s);
if (ErrorQueue.TryDequeue(out string e))
GD.PrintErr(e);
}
public async Task ConnectAsync()
{
@@ -47,9 +47,16 @@ public partial class TwitchChatWatcher : Node
if (Socket.State == WebSocketState.Open)
return;
await Socket.ConnectAsync(new Uri("wss://irc-ws.chat.twitch.tv:443"), Token);
if (Socket.State == WebSocketState.Open)
{
_ = Task.Run(GetPacketsTask, Token);
_ = Task.Run(HandleMessages, Token);
CallDeferred("emit_signal", nameof(SocketConnected));
}
else
{
throw new Exception("Failed to connect to Twitch");
}
}
public async Task Authenticate(string user = null, string pass = null)
{
@@ -65,10 +72,15 @@ public partial class TwitchChatWatcher : Node
}
public async Task JoinChannel(string channel)
{
GD.Print("Joining channel");
channel = channel.TrimStart('#');
if (Channel is not null)
{
await SendMessageAsync(TwitchChatMessageType.PART,
parameters: new string[] {"#" + Channel});
}
await SendMessageAsync(TwitchChatMessageType.JOIN,
parameters: new string[] {"#" + channel});
Channel = channel;
}
public async Task SendMessageAsync(string message)
{
@@ -109,7 +121,6 @@ public partial class TwitchChatWatcher : Node
}
await SendMessageAsync(message);
}
private static ulong PacketCount;
private async Task GetPacketsTask()
{
try
@@ -123,37 +134,29 @@ public partial class TwitchChatWatcher : Node
return;
if (Socket.State != WebSocketState.Open)
{
ErrorQueue.Enqueue("Socket closed");
GD.PrintErr("Socket closed");
CallDeferred("emit_signal", nameof(SocketDisconnected));
return;
}
if (res.Count == 0)
{
ErrorQueue.Enqueue("Empty packet received");
GD.PrintErr("Empty packet received");
continue;
}
PacketCount++;
PrintQueue.Enqueue($"Packet count: {PacketCount}");
stringData += Encoding.UTF8.GetString(arr, 0, res.Count);
//PrintQueue.Enqueue(stringData);
var lines = stringData.Split("\r\n", StringSplitOptions.TrimEntries);
if (!lines.Any())
continue;
stringData = lines.Last();
PrintQueue.Enqueue($"Line count: {lines.SkipLast(1).Count()}");
foreach (var line in lines.SkipLast(1))
MessageStrings.Enqueue(line);
}
}
catch (Exception e)
{
ErrorQueue.Enqueue(e.ToString());
}
finally
{
if (!Token.IsCancellationRequested)
ErrorQueue.Enqueue($"{nameof(GetPacketsTask)} exited without cancellation");
else
PrintQueue.Enqueue($"{nameof(GetPacketsTask)} cancelled and exited");
GD.PushError($"{nameof(GetPacketsTask)} exited without cancellation");
}
}
private readonly ConcurrentQueue<string> MessageStrings = new();
@@ -167,7 +170,7 @@ public partial class TwitchChatWatcher : Node
{
if (string.IsNullOrWhiteSpace(message))
continue;
PrintQueue.Enqueue(message);
GD.Print(message);
// if (PrintAllIncoming)
// PrintQueue.Enqueue(message);
var tcm = TwitchChatMessage.Parse(message);
@@ -180,6 +183,7 @@ public partial class TwitchChatWatcher : Node
continue;
var chat = p.ChatMessage;
chat = chat[com.Length..].TrimStart();
//TODO make better
CallDeferred("emit_signal", SignalName.IncomingCommand,
new Command(p.DisplayName,
false, p.Moderator, chat));
@@ -188,17 +192,10 @@ public partial class TwitchChatWatcher : Node
await Task.Delay(50);
}
}
catch (Exception e)
{
ErrorQueue.Enqueue(e.ToString() + System.Environment.NewLine
+ e.StackTrace);
}
finally
{
if (!Token.IsCancellationRequested)
ErrorQueue.Enqueue($"{nameof(HandleMessages)} exited without cancellation");
else
ErrorQueue.Enqueue($"{nameof(HandleMessages)} cancelled and exited");
GD.PushError($"{nameof(HandleMessages)} exited without cancellation");
}
}
private async Task SendPong(TwitchChatMessage ping)
@@ -206,6 +203,6 @@ public partial class TwitchChatWatcher : Node
var pong = TwitchChatMessage.MakePong(ping);
await SendMessageAsync(TwitchChatMessageType.PONG, ping.Parameters,
ping.MessageTags, ping.Prefix);
PrintQueue.Enqueue("Sent Pong");
GD.Print("Sent Pong");
}
}

17
game.cs
View File

@@ -6,6 +6,14 @@ using System.Linq;
public partial class game : Control
{
private settings_popup _SettingsPopup;
private settings_popup SettingsPopup
{ get
{
_SettingsPopup ??= GetNode<settings_popup>("%SettingsPopup");
return _SettingsPopup;
}
}
[Export]
private Vector2 _CardSize = new(200, 200);
public Vector2 CardSize
@@ -59,7 +67,14 @@ public partial class game : Control
{
if (@event.IsActionPressed("OpenMenu"))
{
GetNode<settings_popup>("%SettingsPopup").Visible = true;
if (SettingsPopup.Visible)
{
SettingsPopup.ClosePopup();
}
else
{
SettingsPopup.ShowPopup();
}
GetViewport().SetInputAsHandled();
}
}

View File

@@ -94,6 +94,7 @@ item_0/id = 0
[node name="SettingsPopup" parent="." instance=ExtResource("6_e1cou")]
unique_name_in_owner = true
visible = false
layout_mode = 0
[node name="CardEditPopup" parent="." instance=ExtResource("6_eqvov")]
unique_name_in_owner = true

View File

@@ -23,6 +23,11 @@ Settings="*res://Settings.cs"
CommandHandler="*res://CommandHandler.cs"
TwitchChatWatcher="*res://TwitchChatWatcher.cs"
[display]
window/size/viewport_width=1280
window/size/viewport_height=720
[dotnet]
project/assembly_name="TierMakerControl"

View File

@@ -6,17 +6,33 @@ using System.Net.NetworkInformation;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
public partial class settings_popup : PopupPanel
public partial class settings_popup : PanelContainer
{
private defer_manager _Deferer = null;
private defer_manager Deferer
{ get
{
_Deferer ??= GetNode<defer_manager>("/root/DeferManager");
return _Deferer;
}
}
public string Channel => GetNode<LineEdit>("%ChannelNameEdit").Text;
[Signal]
public delegate void BeforeOkEventHandler();
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
Transient = true;
Exclusive = true;
DisplayServer.WindowSetMinSize(
DisplayServer.WindowGetMinSize().Union(Size));
GetNode<TwitchChatWatcher>("/root/TwitchChatWatcher")
.Connect(TwitchChatWatcher.SignalName.SocketConnected,
new Callable(this, nameof(SocketConnected)),
(uint)ConnectFlags.Deferred);
GetNode<TwitchChatWatcher>("/root/TwitchChatWatcher")
.Connect(TwitchChatWatcher.SignalName.SocketDisconnected,
new Callable(this, nameof(SocketDisconnected)),
(uint)ConnectFlags.Deferred);
}
private readonly ConcurrentQueue<Action> ActionQueue = new();
// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
@@ -27,25 +43,43 @@ public partial class settings_popup : PopupPanel
// a?.Invoke();
// }
}
public void ShowPopup()
{
Visible = true;
}
public void ClosePopup()
{
Hide();
}
public void _on_cancel_button_pressed()
{
GD.Print("Cancel pressed");
Hide();
ClosePopup();
}
public void _on_ok_button_pressed()
{
GD.Print("OK pressed");
GetNode<ConfigStretchContainer>("%ConfigStretchContainer").Apply();
ClosePopup();
}
public void _on_connect_button_pressed()
{
var tcw = GetNode<TwitchChatWatcher>("/root/TwitchChatWatcher");
string chName = GetNode<LineEdit>("%ChannelNameEdit").Text;
//string chName = GetNode<LineEdit>("%ChannelNameEdit").Text;
Task.Run(tcw.ConnectAsync).ContinueWith(t => tcw.Authenticate(null, null))
.ContinueWith(t => tcw.RequestTags())
.ContinueWith(t => tcw.JoinChannel(chName))
//.ContinueWith(t => tcw.JoinChannel(chName))
.ContinueWith(FinishConnection);
ProcessMode = ProcessModeEnum.Disabled;
}
public void _on_join_channel_button_pressed()
{
var channel = Channel;
if (string.IsNullOrWhiteSpace(channel))
return;
var tcw = GetNode<TwitchChatWatcher>("/root/TwitchChatWatcher");
_ = Task.Run(() => tcw.JoinChannel(channel));
}
private void FinishConnection(Task t)
{
GD.Print(t.Status);
if (t.IsCompletedSuccessfully)
{
CallDeferred(nameof(SuccessfulConnection));
@@ -53,12 +87,15 @@ public partial class settings_popup : PopupPanel
else
{
GD.PrintErr(t.Exception);
ActionQueue.Enqueue(() => ProcessMode = ProcessModeEnum.Inherit);
CallDeferred(nameof(UnlockPopup));
}
}
private void UnlockPopup()
{
ProcessMode = ProcessModeEnum.Inherit;
}
private void SuccessfulConnection()
{
GD.Print("Running completion task");
var tcw = GetNode<TwitchChatWatcher>("/root/TwitchChatWatcher");
if (tcw.State != System.Net.WebSockets.WebSocketState.Open)
throw new Exception("Websocket closed");
@@ -69,7 +106,14 @@ public partial class settings_popup : PopupPanel
StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries),
GetNode<TextEdit>("%BlackListEdit").Text.Split('\n',
StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries));
ProcessMode = ProcessModeEnum.Inherit;
Hide();
UnlockPopup();
}
private void SocketConnected()
{
GetNode<BaseButton>("%ConnectButton").Disabled = true;
}
private void SocketDisconnected()
{
GetNode<BaseButton>("%ConnectButton").Disabled = false;
}
}

View File

@@ -1,118 +1,155 @@
[gd_scene load_steps=5 format=3 uid="uid://jm7tss267q8y"]
[gd_scene load_steps=7 format=3 uid="uid://jm7tss267q8y"]
[ext_resource type="Script" path="res://settings_popup.cs" id="1_blkox"]
[ext_resource type="Script" path="res://ConfigStretchContainer.cs" id="2_fhn7i"]
[ext_resource type="Script" path="res://ChatMarginContainer.cs" id="2_x6hlu"]
[ext_resource type="Script" path="res://ConfigMarginContainer.cs" id="3_pguul"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_jcc71"]
[sub_resource type="ButtonGroup" id="ButtonGroup_4itga"]
[node name="SettingsPopup" type="PopupPanel"]
title = "Settings"
size = Vector2i(728, 523)
visible = true
[node name="SettingsPopup" type="PanelContainer"]
offset_right = 728.0
offset_bottom = 523.0
theme_override_styles/panel = SubResource("StyleBoxFlat_jcc71")
script = ExtResource("1_blkox")
[node name="SettingsDivider" type="VSplitContainer" parent="."]
offset_right = 728.0
offset_bottom = 523.0
layout_mode = 2
[node name="SettingsPopupContainer" type="TabContainer" parent="SettingsDivider"]
custom_minimum_size = Vector2(720, 480)
layout_mode = 2
[node name="ChatContainer" type="VBoxContainer" parent="SettingsDivider/SettingsPopupContainer"]
[node name="ChatMarginContainer" type="MarginContainer" parent="SettingsDivider/SettingsPopupContainer"]
layout_mode = 2
theme_override_constants/margin_left = 5
theme_override_constants/margin_top = 5
theme_override_constants/margin_right = 5
theme_override_constants/margin_bottom = 5
script = ExtResource("2_x6hlu")
[node name="ChatContainer" type="VBoxContainer" parent="SettingsDivider/SettingsPopupContainer/ChatMarginContainer"]
layout_mode = 2
[node name="TopBoxContainer" type="GridContainer" parent="SettingsDivider/SettingsPopupContainer/ChatContainer"]
[node name="TopBoxContainer" type="GridContainer" parent="SettingsDivider/SettingsPopupContainer/ChatMarginContainer/ChatContainer"]
layout_mode = 2
columns = 2
[node name="ChannelNameLabel" type="Label" parent="SettingsDivider/SettingsPopupContainer/ChatContainer/TopBoxContainer"]
[node name="ChannelNameLabel" type="Label" parent="SettingsDivider/SettingsPopupContainer/ChatMarginContainer/ChatContainer/TopBoxContainer"]
layout_mode = 2
text = "Channel Name"
[node name="ChannelNameEdit" type="LineEdit" parent="SettingsDivider/SettingsPopupContainer/ChatContainer/TopBoxContainer"]
[node name="ChannelNameEdit" type="LineEdit" parent="SettingsDivider/SettingsPopupContainer/ChatMarginContainer/ChatContainer/TopBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(500, 0)
layout_mode = 2
[node name="CommandLabel" type="Label" parent="SettingsDivider/SettingsPopupContainer/ChatContainer/TopBoxContainer"]
[node name="CommandLabel" type="Label" parent="SettingsDivider/SettingsPopupContainer/ChatMarginContainer/ChatContainer/TopBoxContainer"]
layout_mode = 2
text = "Command"
text = "Trigger"
[node name="CommandEdit" type="LineEdit" parent="SettingsDivider/SettingsPopupContainer/ChatContainer/TopBoxContainer"]
[node name="CommandEdit" type="LineEdit" parent="SettingsDivider/SettingsPopupContainer/ChatMarginContainer/ChatContainer/TopBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(500, 0)
layout_mode = 2
[node name="CheckBoxModerator" type="CheckBox" parent="SettingsDivider/SettingsPopupContainer/ChatContainer"]
[node name="CheckBoxModerator" type="CheckBox" parent="SettingsDivider/SettingsPopupContainer/ChatMarginContainer/ChatContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Allow moderators"
[node name="UserListContainer" type="HBoxContainer" parent="SettingsDivider/SettingsPopupContainer/ChatContainer"]
[node name="UserListContainer" type="HBoxContainer" parent="SettingsDivider/SettingsPopupContainer/ChatMarginContainer/ChatContainer"]
layout_mode = 2
size_flags_vertical = 3
[node name="WhiteListContainer" type="VBoxContainer" parent="SettingsDivider/SettingsPopupContainer/ChatContainer/UserListContainer"]
[node name="WhiteListContainer" type="VBoxContainer" parent="SettingsDivider/SettingsPopupContainer/ChatMarginContainer/ChatContainer/UserListContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="WhiteListLabel" type="Label" parent="SettingsDivider/SettingsPopupContainer/ChatContainer/UserListContainer/WhiteListContainer"]
[node name="WhiteListLabel" type="Label" parent="SettingsDivider/SettingsPopupContainer/ChatMarginContainer/ChatContainer/UserListContainer/WhiteListContainer"]
layout_mode = 2
text = "Whitelist"
[node name="WhiteListEdit" type="TextEdit" parent="SettingsDivider/SettingsPopupContainer/ChatContainer/UserListContainer/WhiteListContainer"]
[node name="WhiteListEdit" type="TextEdit" parent="SettingsDivider/SettingsPopupContainer/ChatMarginContainer/ChatContainer/UserListContainer/WhiteListContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="BlackListContainer" type="VBoxContainer" parent="SettingsDivider/SettingsPopupContainer/ChatContainer/UserListContainer"]
[node name="BlackListContainer" type="VBoxContainer" parent="SettingsDivider/SettingsPopupContainer/ChatMarginContainer/ChatContainer/UserListContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="BlackListLabel" type="Label" parent="SettingsDivider/SettingsPopupContainer/ChatContainer/UserListContainer/BlackListContainer"]
[node name="BlackListLabel" type="Label" parent="SettingsDivider/SettingsPopupContainer/ChatMarginContainer/ChatContainer/UserListContainer/BlackListContainer"]
layout_mode = 2
text = "Blacklist
"
[node name="BlackListEdit" type="TextEdit" parent="SettingsDivider/SettingsPopupContainer/ChatContainer/UserListContainer/BlackListContainer"]
[node name="BlackListEdit" type="TextEdit" parent="SettingsDivider/SettingsPopupContainer/ChatMarginContainer/ChatContainer/UserListContainer/BlackListContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="ConfigContainer" type="VBoxContainer" parent="SettingsDivider/SettingsPopupContainer"]
[node name="ConnectButtonMarginContainer" type="MarginContainer" parent="SettingsDivider/SettingsPopupContainer/ChatMarginContainer/ChatContainer"]
layout_mode = 2
size_flags_horizontal = 8
theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 5
[node name="HBoxContainer" type="HBoxContainer" parent="SettingsDivider/SettingsPopupContainer/ChatMarginContainer/ChatContainer/ConnectButtonMarginContainer"]
layout_mode = 2
theme_override_constants/separation = 6
[node name="JoinChannelButton" type="Button" parent="SettingsDivider/SettingsPopupContainer/ChatMarginContainer/ChatContainer/ConnectButtonMarginContainer/HBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(125, 0)
layout_mode = 2
size_flags_horizontal = 8
text = "Join Channel"
[node name="ConnectButton" type="Button" parent="SettingsDivider/SettingsPopupContainer/ChatMarginContainer/ChatContainer/ConnectButtonMarginContainer/HBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(125, 0)
layout_mode = 2
size_flags_horizontal = 8
text = "Connect"
[node name="ConfigMarginContainer" type="MarginContainer" parent="SettingsDivider/SettingsPopupContainer"]
visible = false
layout_mode = 2
script = ExtResource("3_pguul")
[node name="ConfigStretchContainer" type="VBoxContainer" parent="SettingsDivider/SettingsPopupContainer/ConfigContainer"]
[node name="ConfigContainer" type="VBoxContainer" parent="SettingsDivider/SettingsPopupContainer/ConfigMarginContainer"]
layout_mode = 2
[node name="ConfigStretchContainer" type="VBoxContainer" parent="SettingsDivider/SettingsPopupContainer/ConfigMarginContainer/ConfigContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
alignment = 1
script = ExtResource("2_fhn7i")
[node name="ConfigStretchLabel" type="Label" parent="SettingsDivider/SettingsPopupContainer/ConfigContainer/ConfigStretchContainer"]
[node name="ConfigStretchLabel" type="Label" parent="SettingsDivider/SettingsPopupContainer/ConfigMarginContainer/ConfigContainer/ConfigStretchContainer"]
layout_mode = 2
text = " Default Stretch Mode"
[node name="ConfigStretchFitButton" type="CheckBox" parent="SettingsDivider/SettingsPopupContainer/ConfigContainer/ConfigStretchContainer"]
[node name="ConfigStretchFitButton" type="CheckBox" parent="SettingsDivider/SettingsPopupContainer/ConfigMarginContainer/ConfigContainer/ConfigStretchContainer"]
unique_name_in_owner = true
layout_mode = 2
button_pressed = true
button_group = SubResource("ButtonGroup_4itga")
text = "Fit"
[node name="ConfigStretchStretchButton" type="CheckBox" parent="SettingsDivider/SettingsPopupContainer/ConfigContainer/ConfigStretchContainer"]
[node name="ConfigStretchStretchButton" type="CheckBox" parent="SettingsDivider/SettingsPopupContainer/ConfigMarginContainer/ConfigContainer/ConfigStretchContainer"]
unique_name_in_owner = true
layout_mode = 2
button_group = SubResource("ButtonGroup_4itga")
text = "Stretch"
[node name="ConfigStretchCropButton" type="CheckBox" parent="SettingsDivider/SettingsPopupContainer/ConfigContainer/ConfigStretchContainer"]
[node name="ConfigStretchCropButton" type="CheckBox" parent="SettingsDivider/SettingsPopupContainer/ConfigMarginContainer/ConfigContainer/ConfigStretchContainer"]
unique_name_in_owner = true
layout_mode = 2
button_group = SubResource("ButtonGroup_4itga")
@@ -136,6 +173,8 @@ layout_mode = 2
size_flags_horizontal = 8
text = "OK"
[connection signal="BeforeOk" from="." to="SettingsDivider/SettingsPopupContainer/ConfigContainer/ConfigStretchContainer" method="OkClicked"]
[connection signal="BeforeOk" from="." to="SettingsDivider/SettingsPopupContainer/ConfigMarginContainer/ConfigContainer/ConfigStretchContainer" method="OkClicked"]
[connection signal="pressed" from="SettingsDivider/SettingsPopupContainer/ChatMarginContainer/ChatContainer/ConnectButtonMarginContainer/HBoxContainer/JoinChannelButton" to="." method="_on_join_channel_button_pressed"]
[connection signal="pressed" from="SettingsDivider/SettingsPopupContainer/ChatMarginContainer/ChatContainer/ConnectButtonMarginContainer/HBoxContainer/ConnectButton" to="." method="_on_connect_button_pressed"]
[connection signal="pressed" from="SettingsDivider/ButtonContainer/CancelButton" to="." method="_on_cancel_button_pressed"]
[connection signal="pressed" from="SettingsDivider/ButtonContainer/OkButton" to="." method="_on_ok_button_pressed"]