add reactive binding
This commit is contained in:
@@ -1,13 +1,25 @@
|
||||
using Godot;
|
||||
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
|
||||
using SharpIDE.Godot.Features.Problems;
|
||||
|
||||
namespace SharpIDE.Godot.Features.BottomPanel;
|
||||
|
||||
public partial class BottomPanelManager : Panel
|
||||
{
|
||||
public SharpIdeSolutionModel? Solution
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
_problemsPanel.Solution = value;
|
||||
}
|
||||
}
|
||||
|
||||
private Control _runPanel = null!;
|
||||
private Control _debugPanel = null!;
|
||||
private Control _buildPanel = null!;
|
||||
private Control _problemsPanel = null!;
|
||||
private ProblemsPanel _problemsPanel = null!;
|
||||
|
||||
private Dictionary<BottomPanelType, Control> _panelTypeMap = [];
|
||||
|
||||
@@ -16,7 +28,7 @@ public partial class BottomPanelManager : Panel
|
||||
_runPanel = GetNode<Control>("%RunPanel");
|
||||
_debugPanel = GetNode<Control>("%DebugPanel");
|
||||
_buildPanel = GetNode<Control>("%BuildPanel");
|
||||
_problemsPanel = GetNode<Control>("%ProblemsPanel");
|
||||
_problemsPanel = GetNode<ProblemsPanel>("%ProblemsPanel");
|
||||
|
||||
_panelTypeMap = new Dictionary<BottomPanelType, Control>
|
||||
{
|
||||
|
||||
8
src/SharpIDE.Godot/Features/Problems/ProblemEntry.cs
Normal file
8
src/SharpIDE.Godot/Features/Problems/ProblemEntry.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Godot;
|
||||
|
||||
namespace SharpIDE.Godot.Features.Problems;
|
||||
|
||||
public partial class ProblemEntry : Control
|
||||
{
|
||||
|
||||
}
|
||||
1
src/SharpIDE.Godot/Features/Problems/ProblemEntry.cs.uid
Normal file
1
src/SharpIDE.Godot/Features/Problems/ProblemEntry.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dkgiknux4rqit
|
||||
30
src/SharpIDE.Godot/Features/Problems/ProblemsPanel.cs
Normal file
30
src/SharpIDE.Godot/Features/Problems/ProblemsPanel.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.Collections.Specialized;
|
||||
using Godot;
|
||||
using ObservableCollections;
|
||||
using R3;
|
||||
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
|
||||
|
||||
namespace SharpIDE.Godot.Features.Problems;
|
||||
|
||||
public partial class ProblemsPanel : Control
|
||||
{
|
||||
private PackedScene _problemsPanelProjectEntryScene = GD.Load<PackedScene>("uid://do72lghjvfdp3");
|
||||
private VBoxContainer _vBoxContainer = null!;
|
||||
// TODO: Use observable collections in the solution model and downwards
|
||||
public SharpIdeSolutionModel? Solution { get; set; }
|
||||
private readonly ObservableHashSet<SharpIdeProjectModel> _projects = [];
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
Observable.EveryValueChanged(this, manager => manager.Solution)
|
||||
.Where(s => s is not null)
|
||||
.Subscribe(s =>
|
||||
{
|
||||
GD.Print($"ProblemsPanel: Solution changed to {s?.Name ?? "null"}");
|
||||
_projects.Clear();
|
||||
_projects.AddRange(s!.AllProjects);
|
||||
});
|
||||
_vBoxContainer = GetNode<VBoxContainer>("ScrollContainer/VBoxContainer");
|
||||
_vBoxContainer.BindChildren(_projects, _problemsPanelProjectEntryScene);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://b1r3no4u3khik
|
||||
@@ -1,4 +1,7 @@
|
||||
[gd_scene format=3 uid="uid://tqpmww430cor"]
|
||||
[gd_scene load_steps=3 format=3 uid="uid://tqpmww430cor"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://b1r3no4u3khik" path="res://Features/Problems/ProblemsPanel.cs" id="1_bnenc"]
|
||||
[ext_resource type="PackedScene" uid="uid://do72lghjvfdp3" path="res://Features/Problems/ProblemsPanelProjectEntry.tscn" id="2_xj8le"]
|
||||
|
||||
[node name="ProblemsPanel" type="Control"]
|
||||
layout_mode = 3
|
||||
@@ -7,3 +10,23 @@ anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_bnenc")
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="ScrollContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="ProblemsPanelProjectEntry" parent="ScrollContainer/VBoxContainer" instance=ExtResource("2_xj8le")]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ProblemsPanelProjectEntry2" parent="ScrollContainer/VBoxContainer" instance=ExtResource("2_xj8le")]
|
||||
layout_mode = 2
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
using Godot;
|
||||
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
|
||||
|
||||
namespace SharpIDE.Godot.Features.Problems;
|
||||
|
||||
public partial class ProblemsPanelProjectEntry : MarginContainer
|
||||
{
|
||||
public SharpIdeProjectModel Project { get; set; } = null!;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
GetNode<Label>("Label").Text = Project?.Name ?? "NULL";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bx5v8rr87343o
|
||||
@@ -0,0 +1,12 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://do72lghjvfdp3"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bx5v8rr87343o" path="res://Features/Problems/ProblemsPanelProjectEntry.cs" id="1_2yh8u"]
|
||||
|
||||
[node name="ProblemsPanelProjectEntry" type="MarginContainer"]
|
||||
offset_right = 40.0
|
||||
offset_bottom = 40.0
|
||||
script = ExtResource("1_2yh8u")
|
||||
|
||||
[node name="Label" type="Label" parent="."]
|
||||
layout_mode = 2
|
||||
text = "Project"
|
||||
@@ -2,6 +2,7 @@ using Godot;
|
||||
using Microsoft.Build.Locator;
|
||||
using SharpIDE.Application.Features.Analysis;
|
||||
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
|
||||
using SharpIDE.Godot.Features.BottomPanel;
|
||||
using SharpIDE.Godot.Features.CustomControls;
|
||||
using SharpIDE.Godot.Features.Run;
|
||||
using SharpIDE.Godot.Features.SolutionExplorer;
|
||||
@@ -19,6 +20,7 @@ public partial class IdeRoot : Control
|
||||
private RunPanel _runPanel = null!;
|
||||
private Button _runMenuButton = null!;
|
||||
private Popup _runMenuPopup = null!;
|
||||
private BottomPanelManager _bottomPanelManager = null!;
|
||||
|
||||
private readonly PackedScene _runMenuItemScene = ResourceLoader.Load<PackedScene>("res://Features/Run/RunMenuItem.tscn");
|
||||
public override void _Ready()
|
||||
@@ -34,6 +36,7 @@ public partial class IdeRoot : Control
|
||||
_solutionExplorerPanel = GetNode<SolutionExplorerPanel>("%SolutionExplorerPanel");
|
||||
_runPanel = GetNode<RunPanel>("%RunPanel");
|
||||
_invertedVSplitContainer = GetNode<InvertedVSplitContainer>("%InvertedVSplitContainer");
|
||||
_bottomPanelManager = GetNode<BottomPanelManager>("%BottomPanel");
|
||||
|
||||
_runMenuButton.Pressed += OnRunMenuButtonPressed;
|
||||
_solutionExplorerPanel.FileSelected += OnSolutionExplorerPanelOnFileSelected;
|
||||
@@ -71,6 +74,7 @@ public partial class IdeRoot : Control
|
||||
var solutionModel = await VsPersistenceMapper.GetSolutionModel(path);
|
||||
_solutionExplorerPanel.SolutionModel = solutionModel;
|
||||
_sharpIdeCodeEdit.Solution = solutionModel;
|
||||
_bottomPanelManager.Solution = solutionModel;
|
||||
Callable.From(_solutionExplorerPanel.RepopulateTree).CallDeferred();
|
||||
RoslynAnalysis.StartSolutionAnalysis(path);
|
||||
|
||||
|
||||
@@ -153,6 +153,7 @@ item_0/text = "Getting Context Actions..."
|
||||
item_0/id = 0
|
||||
|
||||
[node name="BottomPanel" type="Panel" parent="VBoxContainer/HBoxContainer/InvertedVSplitContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
script = ExtResource("7_i62lx")
|
||||
|
||||
|
||||
@@ -1,7 +1,37 @@
|
||||
using Godot;
|
||||
using System.Collections.Specialized;
|
||||
using Godot;
|
||||
using ObservableCollections;
|
||||
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
|
||||
using SharpIDE.Godot.Features.Problems;
|
||||
|
||||
namespace SharpIDE.Godot;
|
||||
|
||||
public static class ControlExtensions
|
||||
{
|
||||
extension(Control control)
|
||||
{
|
||||
public void BindChildren(ObservableHashSet<SharpIdeProjectModel> list, PackedScene scene)
|
||||
{
|
||||
var view = list.CreateView(x =>
|
||||
{
|
||||
var node = scene.Instantiate<ProblemsPanelProjectEntry>();
|
||||
node.Project = x;
|
||||
Callable.From(() => control.AddChild(node)).CallDeferred();
|
||||
return node;
|
||||
});
|
||||
view.ViewChanged += OnViewChanged;
|
||||
}
|
||||
private static void OnViewChanged(in SynchronizedViewChangedEventArgs<SharpIdeProjectModel, ProblemsPanelProjectEntry> eventArgs)
|
||||
{
|
||||
GD.Print("View changed: " + eventArgs.Action);
|
||||
if (eventArgs.Action == NotifyCollectionChangedAction.Remove)
|
||||
{
|
||||
eventArgs.OldItem.View.QueueFree();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class NodeExtensions
|
||||
{
|
||||
extension(Node node)
|
||||
|
||||
@@ -8,4 +8,8 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SharpIDE.Application\SharpIDE.Application.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ObservableCollections" Version="3.3.4" />
|
||||
<PackageReference Include="R3" Version="1.3.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,33 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace R3;
|
||||
|
||||
public partial class FrameProviderDispatcher : global::Godot.Node
|
||||
{
|
||||
StrongBox<double> processDelta = new StrongBox<double>();
|
||||
StrongBox<double> physicsProcessDelta = new StrongBox<double>();
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
GodotProviderInitializer.SetDefaultObservableSystem();
|
||||
|
||||
((GodotFrameProvider)GodotFrameProvider.Process).Delta = processDelta;
|
||||
((GodotFrameProvider)GodotFrameProvider.PhysicsProcess).Delta = physicsProcessDelta;
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
processDelta.Value = delta;
|
||||
((GodotTimeProvider)GodotTimeProvider.Process).time += delta;
|
||||
((GodotFrameProvider)GodotFrameProvider.Process).Run(delta);
|
||||
}
|
||||
|
||||
public override void _PhysicsProcess(double delta)
|
||||
{
|
||||
physicsProcessDelta.Value = delta;
|
||||
((GodotTimeProvider)GodotTimeProvider.PhysicsProcess).time += delta;
|
||||
((GodotFrameProvider)GodotFrameProvider.PhysicsProcess).Run(delta);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://ngclnv5y220f
|
||||
80
src/SharpIDE.Godot/addons/R3.Godot/GodotFrameProvider.cs
Normal file
80
src/SharpIDE.Godot/addons/R3.Godot/GodotFrameProvider.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
#nullable enable
|
||||
|
||||
using Godot;
|
||||
using R3.Collections;
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace R3;
|
||||
|
||||
internal enum PlayerLoopTiming
|
||||
{
|
||||
Process,
|
||||
PhysicsProcess
|
||||
}
|
||||
|
||||
public class GodotFrameProvider : FrameProvider
|
||||
{
|
||||
public static readonly GodotFrameProvider Process = new GodotFrameProvider(PlayerLoopTiming.Process);
|
||||
public static readonly GodotFrameProvider PhysicsProcess = new GodotFrameProvider(PlayerLoopTiming.PhysicsProcess);
|
||||
|
||||
FreeListCore<IFrameRunnerWorkItem> list;
|
||||
readonly object gate = new object();
|
||||
|
||||
PlayerLoopTiming PlayerLoopTiming { get; }
|
||||
|
||||
internal StrongBox<double> Delta = default!; // set from Node before running process.
|
||||
|
||||
internal GodotFrameProvider(PlayerLoopTiming playerLoopTiming)
|
||||
{
|
||||
this.PlayerLoopTiming = playerLoopTiming;
|
||||
this.list = new FreeListCore<IFrameRunnerWorkItem>(gate);
|
||||
}
|
||||
|
||||
public override long GetFrameCount()
|
||||
{
|
||||
if (PlayerLoopTiming == PlayerLoopTiming.Process)
|
||||
{
|
||||
return (long)Engine.GetProcessFrames();
|
||||
}
|
||||
else
|
||||
{
|
||||
return (long)Engine.GetPhysicsFrames();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Register(IFrameRunnerWorkItem callback)
|
||||
{
|
||||
list.Add(callback, out _);
|
||||
}
|
||||
|
||||
internal void Run(double _)
|
||||
{
|
||||
long frameCount = GetFrameCount();
|
||||
|
||||
var span = list.AsSpan();
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
ref readonly var item = ref span[i];
|
||||
if (item != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!item.MoveNext(frameCount))
|
||||
{
|
||||
list.Remove(i);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
list.Remove(i);
|
||||
try
|
||||
{
|
||||
ObservableSystem.GetUnhandledExceptionHandler().Invoke(ex);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bus13cr6ba71l
|
||||
32
src/SharpIDE.Godot/addons/R3.Godot/GodotNodeExtensions.cs
Normal file
32
src/SharpIDE.Godot/addons/R3.Godot/GodotNodeExtensions.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using Godot;
|
||||
|
||||
namespace R3;
|
||||
|
||||
public static class GodotNodeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Dispose self on target node has bee tree exited.
|
||||
/// </summary>
|
||||
/// <param name="disposable"></param>
|
||||
/// <param name="node"></param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns>Self disposable</returns>
|
||||
public static T AddTo<T>(this T disposable, Node node) where T : IDisposable
|
||||
{
|
||||
// Note: Dispose when tree exited, so if node is not inside tree, dispose immediately.
|
||||
if (!node.IsInsideTree())
|
||||
{
|
||||
if (!node.IsNodeReady()) // Before enter tree
|
||||
{
|
||||
GD.PrintErr("AddTo does not support to use before enter tree.");
|
||||
}
|
||||
|
||||
disposable.Dispose();
|
||||
return disposable;
|
||||
}
|
||||
|
||||
node.TreeExited += () => disposable.Dispose();
|
||||
return disposable;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://cob4ct214hg4v
|
||||
@@ -0,0 +1,78 @@
|
||||
using System;
|
||||
|
||||
namespace R3;
|
||||
|
||||
public static class GodotObservableExtensions
|
||||
{
|
||||
public static Observable<double> Delta(this Observable<Unit> source)
|
||||
{
|
||||
return Delta(source, GodotFrameProvider.Process);
|
||||
}
|
||||
|
||||
public static Observable<double> Delta(this Observable<Unit> source, GodotFrameProvider frameProvider)
|
||||
{
|
||||
return new Delta(source, frameProvider);
|
||||
}
|
||||
|
||||
public static Observable<(double Delta, T Item)> Delta<T>(this Observable<T> source)
|
||||
{
|
||||
return Delta(source, GodotFrameProvider.Process);
|
||||
}
|
||||
|
||||
public static Observable<(double Delta, T Item)> Delta<T>(this Observable<T> source, GodotFrameProvider frameProvider)
|
||||
{
|
||||
return new Delta<T>(source, frameProvider);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class Delta(Observable<Unit> source, GodotFrameProvider frameProvider) : Observable<double>
|
||||
{
|
||||
protected override IDisposable SubscribeCore(Observer<double> observer)
|
||||
{
|
||||
return source.Subscribe(new _Delta(observer, frameProvider));
|
||||
}
|
||||
|
||||
sealed class _Delta(Observer<double> observer, GodotFrameProvider frameProvider) : Observer<Unit>
|
||||
{
|
||||
protected override void OnNextCore(Unit value)
|
||||
{
|
||||
observer.OnNext(frameProvider.Delta.Value);
|
||||
}
|
||||
|
||||
protected override void OnErrorResumeCore(Exception error)
|
||||
{
|
||||
observer.OnErrorResume(error);
|
||||
}
|
||||
|
||||
protected override void OnCompletedCore(Result result)
|
||||
{
|
||||
observer.OnCompleted(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class Delta<T>(Observable<T> source, GodotFrameProvider frameProvider) : Observable<(double Delta, T Item)>
|
||||
{
|
||||
protected override IDisposable SubscribeCore(Observer<(double Delta, T Item)> observer)
|
||||
{
|
||||
return source.Subscribe(new _Delta(observer, frameProvider));
|
||||
}
|
||||
|
||||
sealed class _Delta(Observer<(double Delta, T Item)> observer, GodotFrameProvider frameProvider) : Observer<T>
|
||||
{
|
||||
protected override void OnNextCore(T value)
|
||||
{
|
||||
observer.OnNext((frameProvider.Delta.Value, value));
|
||||
}
|
||||
|
||||
protected override void OnErrorResumeCore(Exception error)
|
||||
{
|
||||
observer.OnErrorResume(error);
|
||||
}
|
||||
|
||||
protected override void OnCompletedCore(Result result)
|
||||
{
|
||||
observer.OnCompleted(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://8lnifa1x4o6e
|
||||
@@ -0,0 +1,21 @@
|
||||
#nullable enable
|
||||
|
||||
using Godot;
|
||||
using System;
|
||||
|
||||
namespace R3;
|
||||
|
||||
public static class GodotProviderInitializer
|
||||
{
|
||||
public static void SetDefaultObservableSystem()
|
||||
{
|
||||
SetDefaultObservableSystem(ex => GD.PrintErr(ex));
|
||||
}
|
||||
|
||||
public static void SetDefaultObservableSystem(Action<Exception> unhandledExceptionHandler)
|
||||
{
|
||||
ObservableSystem.RegisterUnhandledExceptionHandler(unhandledExceptionHandler);
|
||||
ObservableSystem.DefaultTimeProvider = GodotTimeProvider.Process;
|
||||
ObservableSystem.DefaultFrameProvider = GodotFrameProvider.Process;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://cfm1x8aucvw47
|
||||
32
src/SharpIDE.Godot/addons/R3.Godot/GodotR3Plugin.cs
Normal file
32
src/SharpIDE.Godot/addons/R3.Godot/GodotR3Plugin.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
#if TOOLS
|
||||
#nullable enable
|
||||
|
||||
using Godot;
|
||||
|
||||
namespace R3;
|
||||
|
||||
[Tool]
|
||||
public partial class GodotR3Plugin : EditorPlugin
|
||||
{
|
||||
static ObservableTrackerDebuggerPlugin? observableTrackerDebugger;
|
||||
public override void _EnterTree()
|
||||
{
|
||||
observableTrackerDebugger ??= new ObservableTrackerDebuggerPlugin();
|
||||
AddDebuggerPlugin(observableTrackerDebugger);
|
||||
// Automatically install autoloads here for ease of use.
|
||||
AddAutoloadSingleton(nameof(FrameProviderDispatcher), "res://addons/R3.Godot/FrameProviderDispatcher.cs");
|
||||
AddAutoloadSingleton(nameof(ObservableTrackerRuntimeHook), "res://addons/R3.Godot/ObservableTrackerRuntimeHook.cs");
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
if (observableTrackerDebugger != null)
|
||||
{
|
||||
RemoveDebuggerPlugin(observableTrackerDebugger);
|
||||
observableTrackerDebugger = null;
|
||||
}
|
||||
RemoveAutoloadSingleton(nameof(FrameProviderDispatcher));
|
||||
RemoveAutoloadSingleton(nameof(ObservableTrackerRuntimeHook));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
1
src/SharpIDE.Godot/addons/R3.Godot/GodotR3Plugin.cs.uid
Normal file
1
src/SharpIDE.Godot/addons/R3.Godot/GodotR3Plugin.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cdglfkymcq38j
|
||||
261
src/SharpIDE.Godot/addons/R3.Godot/GodotSignalMapper.cs
Normal file
261
src/SharpIDE.Godot/addons/R3.Godot/GodotSignalMapper.cs
Normal file
@@ -0,0 +1,261 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Godot;
|
||||
|
||||
namespace R3;
|
||||
|
||||
public static class GodotObjectExtensions
|
||||
{
|
||||
/// <summary>Returns a <see cref="CancellationToken"/> that cancels when <paramref name="obj"/> emits the specified signal, this cancellation is one-shot unless <paramref name="oneShot"/> is false.</summary>
|
||||
public static CancellationToken CancelOnSignal(this GodotObject obj, StringName signalName, bool oneShot = true)
|
||||
{
|
||||
CancellationTokenSource cts = new();
|
||||
obj.Connect(signalName, Callable.From(cts.Cancel), oneShot ? (uint) GodotObject.ConnectFlags.OneShot : 0);
|
||||
return cts.Token;
|
||||
}
|
||||
|
||||
/// <summary>Returns an observable that: publishes a <see cref="Unit"/> when <paramref name="node"/> emits the specified signal (except for <see cref="Node.SignalName.TreeExited"/>); finishes when <paramref name="node"/> emits <see cref="Node.SignalName.TreeExited"/>.</summary>
|
||||
public static Observable<Unit> SignalAsObservable(this Node node, StringName signalName)
|
||||
{
|
||||
return node.SignalAsObservable(signalName, node.CancelOnSignal(Node.SignalName.TreeExited));
|
||||
}
|
||||
|
||||
/// <summary>Returns an observable that: publishes data of type <typeparamref name="T"/> when <paramref name="node"/> emits the specified signal (except for <see cref="Node.SignalName.TreeExited"/>); finishes when <paramref name="node"/> emits <see cref="Node.SignalName.TreeExited"/>.</summary>
|
||||
public static Observable<T> SignalAsObservable<[MustBeVariant] T>(this Node node, StringName signalName)
|
||||
{
|
||||
return node.SignalAsObservable<T>(signalName, node.CancelOnSignal(Node.SignalName.TreeExited));
|
||||
}
|
||||
|
||||
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>) when <paramref name="node"/> emits the specified signal (except for <see cref="Node.SignalName.TreeExited"/>); finishes when <paramref name="node"/> emits <see cref="Node.SignalName.TreeExited"/>.</summary>
|
||||
public static Observable<(T0, T1)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1>(this Node node, StringName signalName)
|
||||
{
|
||||
return node.SignalAsObservable<T0, T1>(signalName, node.CancelOnSignal(Node.SignalName.TreeExited));
|
||||
}
|
||||
|
||||
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>) when <paramref name="node"/> emits the specified signal (except for <see cref="Node.SignalName.TreeExited"/>); finishes when <paramref name="node"/> emits <see cref="Node.SignalName.TreeExited"/>.</summary>
|
||||
public static Observable<(T0, T1, T2)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2>(this Node node, StringName signalName)
|
||||
{
|
||||
return node.SignalAsObservable<T0, T1, T2>(signalName, node.CancelOnSignal(Node.SignalName.TreeExited));
|
||||
}
|
||||
|
||||
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>, <typeparamref name="T3"/>) when <paramref name="node"/> emits the specified signal (except for <see cref="Node.SignalName.TreeExited"/>); finishes when <paramref name="node"/> emits <see cref="Node.SignalName.TreeExited"/>.</summary>
|
||||
public static Observable<(T0, T1, T2, T3)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3>(this Node node, StringName signalName)
|
||||
{
|
||||
return node.SignalAsObservable<T0, T1, T2, T3>(signalName, node.CancelOnSignal(Node.SignalName.TreeExited));
|
||||
}
|
||||
|
||||
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>, <typeparamref name="T3"/>, <typeparamref name="T4"/>) when <paramref name="node"/> emits the specified signal (except for <see cref="Node.SignalName.TreeExited"/>); finishes when <paramref name="node"/> emits <see cref="Node.SignalName.TreeExited"/>.</summary>
|
||||
public static Observable<(T0, T1, T2, T3, T4)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4>(this Node node, StringName signalName)
|
||||
{
|
||||
return node.SignalAsObservable<T0, T1, T2, T3, T4>(signalName, node.CancelOnSignal(Node.SignalName.TreeExited));
|
||||
}
|
||||
|
||||
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>, <typeparamref name="T3"/>, <typeparamref name="T4"/>, <typeparamref name="T5"/>) when <paramref name="node"/> emits the specified signal (except for <see cref="Node.SignalName.TreeExited"/>); finishes when <paramref name="node"/> emits <see cref="Node.SignalName.TreeExited"/>.</summary>
|
||||
public static Observable<(T0, T1, T2, T3, T4, T5)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4, [MustBeVariant] T5>(this Node node, StringName signalName)
|
||||
{
|
||||
return node.SignalAsObservable<T0, T1, T2, T3, T4, T5>(signalName, node.CancelOnSignal(Node.SignalName.TreeExited));
|
||||
}
|
||||
|
||||
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>, <typeparamref name="T3"/>, <typeparamref name="T4"/>, <typeparamref name="T5"/>, <typeparamref name="T6"/>) when <paramref name="node"/> emits the specified signal (except for <see cref="Node.SignalName.TreeExited"/>); finishes when <paramref name="node"/> emits <see cref="Node.SignalName.TreeExited"/>.</summary>
|
||||
public static Observable<(T0, T1, T2, T3, T4, T5, T6)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4, [MustBeVariant] T5, [MustBeVariant] T6>(this Node node, StringName signalName)
|
||||
{
|
||||
return node.SignalAsObservable<T0, T1, T2, T3, T4, T5, T6>(signalName, node.CancelOnSignal(Node.SignalName.TreeExited));
|
||||
}
|
||||
|
||||
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>, <typeparamref name="T3"/>, <typeparamref name="T4"/>, <typeparamref name="T5"/>, <typeparamref name="T6"/>, <typeparamref name="T7"/>) when <paramref name="node"/> emits the specified signal (except for <see cref="Node.SignalName.TreeExited"/>); finishes when <paramref name="node"/> emits <see cref="Node.SignalName.TreeExited"/>.</summary>
|
||||
public static Observable<(T0, T1, T2, T3, T4, T5, T6, T7)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4, [MustBeVariant] T5, [MustBeVariant] T6, [MustBeVariant] T7>(this Node node, StringName signalName)
|
||||
{
|
||||
return node.SignalAsObservable<T0, T1, T2, T3, T4, T5, T6, T7>(signalName, node.CancelOnSignal(Node.SignalName.TreeExited));
|
||||
}
|
||||
|
||||
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>, <typeparamref name="T3"/>, <typeparamref name="T4"/>, <typeparamref name="T5"/>, <typeparamref name="T6"/>, <typeparamref name="T7"/>, <typeparamref name="T8"/>) when <paramref name="node"/> emits the specified signal (except for <see cref="Node.SignalName.TreeExited"/>); finishes when <paramref name="node"/> emits <see cref="Node.SignalName.TreeExited"/>.</summary>
|
||||
public static Observable<(T0, T1, T2, T3, T4, T5, T6, T7, T8)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4, [MustBeVariant] T5, [MustBeVariant] T6, [MustBeVariant] T7, [MustBeVariant] T8>(this Node node, StringName signalName)
|
||||
{
|
||||
return node.SignalAsObservable<T0, T1, T2, T3, T4, T5, T6, T7, T8>(signalName, node.CancelOnSignal(Node.SignalName.TreeExited));
|
||||
}
|
||||
|
||||
/// <summary>Returns an observable that: publishes a <see cref="Unit"/> when <paramref name="obj"/> emits the specified signal; finishes when <paramref name="cancellationToken"/> cancels, or when all subscriptions are disposed, if only <paramref name="cancellationToken"/> is default (= <see cref="CancellationToken.None"/>).</summary>
|
||||
public static Observable<Unit> SignalAsObservable(this GodotObject obj, StringName signalName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new GodotSignalMapper(obj, signalName, cancellationToken).RefCount();
|
||||
}
|
||||
|
||||
/// <summary>Returns an observable that: publishes data of type <typeparamref name="T"/> when <paramref name="obj"/> emits the specified signal; finishes when <paramref name="cancellationToken"/> cancels, or when all subscriptions are disposed, if only <paramref name="cancellationToken"/> is default (= <see cref="CancellationToken.None"/>).</summary>
|
||||
public static Observable<T> SignalAsObservable<[MustBeVariant] T>(this GodotObject obj, StringName signalName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new GodotSignalMapper<T>(obj, signalName, cancellationToken).RefCount();
|
||||
}
|
||||
|
||||
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>) when <paramref name="obj"/> emits the specified signal; finishes when <paramref name="cancellationToken"/> cancels, or when all subscriptions are disposed, if only <paramref name="cancellationToken"/> is default (= <see cref="CancellationToken.None"/>).</summary>
|
||||
public static Observable<(T0, T1)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1>(this GodotObject obj, StringName signalName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new GodotSignalMapper<T0, T1>(obj, signalName, cancellationToken).RefCount();
|
||||
}
|
||||
|
||||
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>) when <paramref name="obj"/> emits the specified signal; finishes when <paramref name="cancellationToken"/> cancels, or when all subscriptions are disposed, if only <paramref name="cancellationToken"/> is default (= <see cref="CancellationToken.None"/>).</summary>
|
||||
public static Observable<(T0, T1, T2)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2>(this GodotObject obj, StringName signalName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new GodotSignalMapper<T0, T1, T2>(obj, signalName, cancellationToken).RefCount();
|
||||
}
|
||||
|
||||
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>, <typeparamref name="T3"/>) when <paramref name="obj"/> emits the specified signal; finishes when <paramref name="cancellationToken"/> cancels, or when all subscriptions are disposed, if only <paramref name="cancellationToken"/> is default (= <see cref="CancellationToken.None"/>).</summary>
|
||||
public static Observable<(T0, T1, T2, T3)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3>(this GodotObject obj, StringName signalName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new GodotSignalMapper<T0, T1, T2, T3>(obj, signalName, cancellationToken).RefCount();
|
||||
}
|
||||
|
||||
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>, <typeparamref name="T3"/>, <typeparamref name="T4"/>) when <paramref name="obj"/> emits the specified signal; finishes when <paramref name="cancellationToken"/> cancels, or when all subscriptions are disposed, if only <paramref name="cancellationToken"/> is default (= <see cref="CancellationToken.None"/>).</summary>
|
||||
public static Observable<(T0, T1, T2, T3, T4)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4>(this GodotObject obj, StringName signalName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new GodotSignalMapper<T0, T1, T2, T3, T4>(obj, signalName, cancellationToken).RefCount();
|
||||
}
|
||||
|
||||
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>, <typeparamref name="T3"/>, <typeparamref name="T4"/>, <typeparamref name="T5"/>) when <paramref name="obj"/> emits the specified signal; finishes when <paramref name="cancellationToken"/> cancels, or when all subscriptions are disposed, if only <paramref name="cancellationToken"/> is default (= <see cref="CancellationToken.None"/>).</summary>
|
||||
public static Observable<(T0, T1, T2, T3, T4, T5)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4, [MustBeVariant] T5>(this GodotObject obj, StringName signalName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new GodotSignalMapper<T0, T1, T2, T3, T4, T5>(obj, signalName, cancellationToken).RefCount();
|
||||
}
|
||||
|
||||
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>, <typeparamref name="T3"/>, <typeparamref name="T4"/>, <typeparamref name="T5"/>, <typeparamref name="T6"/>) when <paramref name="obj"/> emits the specified signal; finishes when <paramref name="cancellationToken"/> cancels, or when all subscriptions are disposed, if only <paramref name="cancellationToken"/> is default (= <see cref="CancellationToken.None"/>).</summary>
|
||||
public static Observable<(T0, T1, T2, T3, T4, T5, T6)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4, [MustBeVariant] T5, [MustBeVariant] T6>(this GodotObject obj, StringName signalName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new GodotSignalMapper<T0, T1, T2, T3, T4, T5, T6>(obj, signalName, cancellationToken).RefCount();
|
||||
}
|
||||
|
||||
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>, <typeparamref name="T3"/>, <typeparamref name="T4"/>, <typeparamref name="T5"/>, <typeparamref name="T6"/>, <typeparamref name="T7"/>) when <paramref name="obj"/> emits the specified signal; finishes when <paramref name="cancellationToken"/> cancels, or when all subscriptions are disposed, if only <paramref name="cancellationToken"/> is default (= <see cref="CancellationToken.None"/>).</summary>
|
||||
public static Observable<(T0, T1, T2, T3, T4, T5, T6, T7)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4, [MustBeVariant] T5, [MustBeVariant] T6, [MustBeVariant] T7>(this GodotObject obj, StringName signalName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new GodotSignalMapper<T0, T1, T2, T3, T4, T5, T6, T7>(obj, signalName, cancellationToken).RefCount();
|
||||
}
|
||||
|
||||
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>, <typeparamref name="T3"/>, <typeparamref name="T4"/>, <typeparamref name="T5"/>, <typeparamref name="T6"/>, <typeparamref name="T7"/>, <typeparamref name="T8"/>) when <paramref name="obj"/> emits the specified signal; finishes when <paramref name="cancellationToken"/> cancels, or when all subscriptions are disposed, if only <paramref name="cancellationToken"/> is default (= <see cref="CancellationToken.None"/>).</summary>
|
||||
public static Observable<(T0, T1, T2, T3, T4, T5, T6, T7, T8)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4, [MustBeVariant] T5, [MustBeVariant] T6, [MustBeVariant] T7, [MustBeVariant] T8>(this GodotObject obj, StringName signalName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new GodotSignalMapper<T0, T1, T2, T3, T4, T5, T6, T7, T8>(obj, signalName, cancellationToken).RefCount();
|
||||
}
|
||||
}
|
||||
|
||||
internal abstract class GodotSignalMapperBase<T> : ConnectableObservable<T>, IDisposable
|
||||
{
|
||||
protected readonly Subject<T> subject = new();
|
||||
|
||||
private readonly GodotObject godotObject;
|
||||
private readonly StringName godotSignalName;
|
||||
private readonly CancellationTokenRegistration? cancellationTokenRegistration;
|
||||
private Callable? godotCallableOnNext;
|
||||
private int connected;
|
||||
private int disposed;
|
||||
|
||||
private bool ShouldDisposeOnDisconnect => !cancellationTokenRegistration.HasValue;
|
||||
|
||||
protected GodotSignalMapperBase(GodotObject obj, StringName signalName, CancellationToken cancellationToken)
|
||||
{
|
||||
godotObject = obj;
|
||||
godotSignalName = signalName;
|
||||
if (cancellationToken.CanBeCanceled)
|
||||
{
|
||||
cancellationTokenRegistration = cancellationToken.UnsafeRegister((state) => ((GodotSignalMapperBase<T>) state!).Dispose(), this);
|
||||
}
|
||||
}
|
||||
|
||||
protected override IDisposable SubscribeCore(Observer<T> observer)
|
||||
{
|
||||
return subject.Subscribe(observer.Wrap());
|
||||
}
|
||||
|
||||
public override IDisposable Connect()
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref connected, 1, 0) == 0)
|
||||
{
|
||||
godotCallableOnNext ??= OnNextAsGodotCallable();
|
||||
godotObject.Connect(godotSignalName, godotCallableOnNext.Value);
|
||||
}
|
||||
return new Connection(this);
|
||||
}
|
||||
|
||||
private void Disconnect()
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref connected, 0, 1) == 1)
|
||||
{
|
||||
godotObject.Disconnect(godotSignalName, godotCallableOnNext!.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private class Connection(GodotSignalMapperBase<T> parent) : IDisposable
|
||||
{
|
||||
public void Dispose()
|
||||
{
|
||||
parent.Disconnect();
|
||||
if (parent.ShouldDisposeOnDisconnect) { parent.Dispose(); }
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref disposed, 1, 0) == 1) { return; }
|
||||
Disconnect();
|
||||
subject.Dispose(true);
|
||||
cancellationTokenRegistration?.Dispose();
|
||||
}
|
||||
|
||||
protected abstract Callable OnNextAsGodotCallable();
|
||||
}
|
||||
|
||||
internal class GodotSignalMapper(GodotObject obj, StringName signalName, CancellationToken cancellationToken) : GodotSignalMapperBase<Unit>(obj, signalName, cancellationToken) {
|
||||
protected override Callable OnNextAsGodotCallable() => Callable.From((Action) OnNextWithUnit);
|
||||
private void OnNextWithUnit() => subject.OnNext(Unit.Default);
|
||||
}
|
||||
|
||||
internal class GodotSignalMapper<[MustBeVariant] T>(GodotObject obj, StringName signalName, CancellationToken cancellationToken) : GodotSignalMapperBase<T>(obj, signalName, cancellationToken)
|
||||
{
|
||||
protected override Callable OnNextAsGodotCallable() => Callable.From((Action<T>) OnNextWithArgs);
|
||||
private void OnNextWithArgs(T _a) => subject.OnNext(_a);
|
||||
}
|
||||
|
||||
internal class GodotSignalMapper<[MustBeVariant] T0, [MustBeVariant] T1>(GodotObject obj, StringName signalName, CancellationToken cancellationToken) : GodotSignalMapperBase<(T0, T1)>(obj, signalName, cancellationToken)
|
||||
{
|
||||
protected override Callable OnNextAsGodotCallable() => Callable.From((Action<T0, T1>) OnNextWithArgs);
|
||||
private void OnNextWithArgs(T0 _0, T1 _1) => subject.OnNext((_0, _1));
|
||||
}
|
||||
|
||||
internal class GodotSignalMapper<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2>(GodotObject obj, StringName signalName, CancellationToken cancellationToken) : GodotSignalMapperBase<(T0, T1, T2)>(obj, signalName, cancellationToken)
|
||||
{
|
||||
protected override Callable OnNextAsGodotCallable() => Callable.From((Action<T0, T1, T2>) OnNextWithArgs);
|
||||
private void OnNextWithArgs(T0 _0, T1 _1, T2 _2) => subject.OnNext((_0, _1, _2));
|
||||
}
|
||||
|
||||
internal class GodotSignalMapper<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3>(GodotObject obj, StringName signalName, CancellationToken cancellationToken) : GodotSignalMapperBase<(T0, T1, T2, T3)>(obj, signalName, cancellationToken)
|
||||
{
|
||||
protected override Callable OnNextAsGodotCallable() => Callable.From((Action<T0, T1, T2, T3>) OnNextWithArgs);
|
||||
private void OnNextWithArgs(T0 _0, T1 _1, T2 _2, T3 _3) => subject.OnNext((_0, _1, _2, _3));
|
||||
}
|
||||
|
||||
internal class GodotSignalMapper<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4>(GodotObject obj, StringName signalName, CancellationToken cancellationToken) : GodotSignalMapperBase<(T0, T1, T2, T3, T4)>(obj, signalName, cancellationToken)
|
||||
{
|
||||
protected override Callable OnNextAsGodotCallable() => Callable.From((Action<T0, T1, T2, T3, T4>) OnNextWithArgs);
|
||||
private void OnNextWithArgs(T0 _0, T1 _1, T2 _2, T3 _3, T4 _4) => subject.OnNext((_0, _1, _2, _3, _4));
|
||||
}
|
||||
|
||||
internal class GodotSignalMapper<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4, [MustBeVariant] T5>(GodotObject obj, StringName signalName, CancellationToken cancellationToken) : GodotSignalMapperBase<(T0, T1, T2, T3, T4, T5)>(obj, signalName, cancellationToken)
|
||||
{
|
||||
protected override Callable OnNextAsGodotCallable() => Callable.From((Action<T0, T1, T2, T3, T4, T5>) OnNextWithArgs);
|
||||
private void OnNextWithArgs(T0 _0, T1 _1, T2 _2, T3 _3, T4 _4, T5 _5) => subject.OnNext((_0, _1, _2, _3, _4, _5));
|
||||
}
|
||||
|
||||
internal class GodotSignalMapper<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4, [MustBeVariant] T5, [MustBeVariant] T6>(GodotObject obj, StringName signalName, CancellationToken cancellationToken) : GodotSignalMapperBase<(T0, T1, T2, T3, T4, T5, T6)>(obj, signalName, cancellationToken)
|
||||
{
|
||||
protected override Callable OnNextAsGodotCallable() => Callable.From((Action<T0, T1, T2, T3, T4, T5, T6>) OnNextWithArgs);
|
||||
private void OnNextWithArgs(T0 _0, T1 _1, T2 _2, T3 _3, T4 _4, T5 _5, T6 _6) => subject.OnNext((_0, _1, _2, _3, _4, _5, _6));
|
||||
}
|
||||
|
||||
internal class GodotSignalMapper<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4, [MustBeVariant] T5, [MustBeVariant] T6, [MustBeVariant] T7>(GodotObject obj, StringName signalName, CancellationToken cancellationToken) : GodotSignalMapperBase<(T0, T1, T2, T3, T4, T5, T6, T7)>(obj, signalName, cancellationToken)
|
||||
{
|
||||
protected override Callable OnNextAsGodotCallable() => Callable.From((Action<T0, T1, T2, T3, T4, T5, T6, T7>) OnNextWithArgs);
|
||||
private void OnNextWithArgs(T0 _0, T1 _1, T2 _2, T3 _3, T4 _4, T5 _5, T6 _6, T7 _7) => subject.OnNext((_0, _1, _2, _3, _4, _5, _6, _7));
|
||||
}
|
||||
|
||||
internal class GodotSignalMapper<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4, [MustBeVariant] T5, [MustBeVariant] T6, [MustBeVariant] T7, [MustBeVariant] T8>(GodotObject obj, StringName signalName, CancellationToken cancellationToken) : GodotSignalMapperBase<(T0, T1, T2, T3, T4, T5, T6, T7, T8)>(obj, signalName, cancellationToken)
|
||||
{
|
||||
protected override Callable OnNextAsGodotCallable() => Callable.From((Action<T0, T1, T2, T3, T4, T5, T6, T7, T8>) OnNextWithArgs);
|
||||
private void OnNextWithArgs(T0 _0, T1 _1, T2 _2, T3 _3, T4 _4, T5 _5, T6 _6, T7 _7, T8 _8) => subject.OnNext((_0, _1, _2, _3, _4, _5, _6, _7, _8));
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://cwsypls11xo1t
|
||||
196
src/SharpIDE.Godot/addons/R3.Godot/GodotTimeProvider.cs
Normal file
196
src/SharpIDE.Godot/addons/R3.Godot/GodotTimeProvider.cs
Normal file
@@ -0,0 +1,196 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace R3;
|
||||
|
||||
public class GodotTimeProvider : TimeProvider
|
||||
{
|
||||
public static readonly GodotTimeProvider Process = new GodotTimeProvider(GodotFrameProvider.Process);
|
||||
public static readonly GodotTimeProvider PhysicsProcess = new GodotTimeProvider(GodotFrameProvider.PhysicsProcess);
|
||||
|
||||
readonly GodotFrameProvider frameProvider;
|
||||
|
||||
internal double time;
|
||||
|
||||
GodotTimeProvider(FrameProvider frameProvider)
|
||||
{
|
||||
this.frameProvider = (GodotFrameProvider)frameProvider;
|
||||
}
|
||||
|
||||
public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
|
||||
{
|
||||
return new FrameTimer(callback, state, dueTime, period, frameProvider);
|
||||
}
|
||||
|
||||
public override long GetTimestamp()
|
||||
{
|
||||
return TimeSpan.FromSeconds(time).Ticks;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameTimer : ITimer, IFrameRunnerWorkItem
|
||||
{
|
||||
enum RunningState
|
||||
{
|
||||
Stop,
|
||||
RunningDueTime,
|
||||
RunningPeriod,
|
||||
ChangeRequested
|
||||
}
|
||||
|
||||
readonly TimerCallback callback;
|
||||
readonly object? state;
|
||||
readonly GodotFrameProvider frameProvider;
|
||||
readonly object gate = new object();
|
||||
|
||||
TimeSpan dueTime;
|
||||
TimeSpan period;
|
||||
RunningState runningState;
|
||||
double elapsed;
|
||||
bool isDisposed;
|
||||
|
||||
public FrameTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period, GodotFrameProvider frameProvider)
|
||||
{
|
||||
this.callback = callback;
|
||||
this.state = state;
|
||||
this.dueTime = dueTime;
|
||||
this.period = period;
|
||||
this.frameProvider = frameProvider;
|
||||
Change(dueTime, period);
|
||||
}
|
||||
|
||||
public bool Change(TimeSpan dueTime, TimeSpan period)
|
||||
{
|
||||
if (isDisposed) return false;
|
||||
|
||||
lock (gate)
|
||||
{
|
||||
this.dueTime = dueTime;
|
||||
this.period = period;
|
||||
|
||||
if (dueTime == Timeout.InfiniteTimeSpan)
|
||||
{
|
||||
if (runningState == RunningState.Stop)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (runningState == RunningState.Stop)
|
||||
{
|
||||
frameProvider.Register(this);
|
||||
}
|
||||
|
||||
runningState = RunningState.ChangeRequested;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IFrameRunnerWorkItem.MoveNext(long frameCount)
|
||||
{
|
||||
if (isDisposed) return false;
|
||||
|
||||
RunningState runState;
|
||||
TimeSpan p; // period
|
||||
TimeSpan d; // dueTime
|
||||
lock (gate)
|
||||
{
|
||||
runState = runningState;
|
||||
|
||||
if (runState == RunningState.ChangeRequested)
|
||||
{
|
||||
elapsed = 0;
|
||||
if (dueTime == Timeout.InfiniteTimeSpan)
|
||||
{
|
||||
runningState = RunningState.Stop;
|
||||
return false;
|
||||
}
|
||||
|
||||
runState = runningState = RunningState.RunningDueTime;
|
||||
}
|
||||
p = period;
|
||||
d = dueTime;
|
||||
}
|
||||
|
||||
elapsed += frameProvider.Delta.Value;
|
||||
|
||||
try
|
||||
{
|
||||
if (runState == RunningState.RunningDueTime)
|
||||
{
|
||||
var dt = (double)d.TotalSeconds;
|
||||
if (elapsed >= dt)
|
||||
{
|
||||
callback(state);
|
||||
|
||||
elapsed = 0;
|
||||
if (period == Timeout.InfiniteTimeSpan)
|
||||
{
|
||||
return ChangeState(RunningState.Stop);
|
||||
}
|
||||
else
|
||||
{
|
||||
return ChangeState(RunningState.RunningPeriod);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var dt = (double)p.TotalSeconds;
|
||||
if (elapsed >= dt)
|
||||
{
|
||||
callback(state);
|
||||
elapsed = 0;
|
||||
}
|
||||
|
||||
return ChangeState(RunningState.RunningPeriod);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ObservableSystem.GetUnhandledExceptionHandler().Invoke(ex);
|
||||
return ChangeState(RunningState.Stop);
|
||||
}
|
||||
}
|
||||
|
||||
bool ChangeState(RunningState state)
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
// change requested is high priority
|
||||
if (runningState == RunningState.ChangeRequested)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case RunningState.RunningPeriod:
|
||||
runningState = state;
|
||||
return true;
|
||||
default: // otherwise(Stop)
|
||||
runningState = state;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
|
||||
isDisposed = true;
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
Dispose();
|
||||
return default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dvccr1jhyokui
|
||||
82
src/SharpIDE.Godot/addons/R3.Godot/GodotUINodeExtensions.cs
Normal file
82
src/SharpIDE.Godot/addons/R3.Godot/GodotUINodeExtensions.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Godot;
|
||||
|
||||
namespace R3;
|
||||
|
||||
public static class GodotUINodeExtensions
|
||||
{
|
||||
public static IDisposable SubscribeToLabel(this Observable<string> source, Label label)
|
||||
{
|
||||
return source.Subscribe(label, static (x, l) => l.Text = x);
|
||||
}
|
||||
|
||||
public static IDisposable SubscribeToLabel<T>(this Observable<T> source, Label label)
|
||||
{
|
||||
return source.Subscribe(label, static (x, l) => l.Text = x?.ToString());
|
||||
}
|
||||
|
||||
public static IDisposable SubscribeToLabel<T>(this Observable<T> source, Label label, Func<T, string> selector)
|
||||
{
|
||||
return source.Subscribe((label, selector), static (x, state) => state.label.Text = state.selector(x));
|
||||
}
|
||||
|
||||
/// <summary>Observe Pressed event.</summary>
|
||||
public static Observable<Unit> OnPressedAsObservable(this BaseButton button, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Observable.FromEvent(h => button.Pressed += h, h => button.Pressed -= h, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>Observe Toggled with current `ButtonPressed` value on subscribe.</summary>
|
||||
public static Observable<bool> OnToggledAsObservable(this BaseButton button, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!button.ToggleMode) return Observable.Empty<bool>();
|
||||
|
||||
return Observable.Create<bool, (BaseButton, CancellationToken)>((button, cancellationToken), static (observer, state) =>
|
||||
{
|
||||
var (b, cancellationToken) = state;
|
||||
observer.OnNext(b.ButtonPressed);
|
||||
return Observable.FromEvent<BaseButton.ToggledEventHandler, bool>(h => new BaseButton.ToggledEventHandler(h), h => b.Toggled += h, h => b.Toggled -= h, cancellationToken).Subscribe(observer);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Observe ValueChanged with current `Value` on subscribe.</summary>
|
||||
public static Observable<double> OnValueChangedAsObservable(this Godot.Range range, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Observable.Create<double, (Godot.Range, CancellationToken)>((range, cancellationToken), static (observer, state) =>
|
||||
{
|
||||
var (s, cancellationToken) = state;
|
||||
observer.OnNext(s.Value);
|
||||
return Observable.FromEvent<Godot.Range.ValueChangedEventHandler, double>(h => new Godot.Range.ValueChangedEventHandler(h), h => s.ValueChanged += h, h => s.ValueChanged -= h, cancellationToken).Subscribe(observer);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Observe TextSubmitted event.</summary>
|
||||
public static Observable<string> OnTextSubmittedAsObservable(this LineEdit lineEdit, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Observable.FromEvent<LineEdit.TextSubmittedEventHandler, string>(h => new LineEdit.TextSubmittedEventHandler(h), h => lineEdit.TextSubmitted += h, h => lineEdit.TextSubmitted -= h, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>Observe TextChanged event.</summary>
|
||||
public static Observable<string> OnTextChangedAsObservable(this LineEdit lineEdit, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Observable.FromEvent<LineEdit.TextChangedEventHandler, string>(h => new LineEdit.TextChangedEventHandler(h), h => lineEdit.TextChanged += h, h => lineEdit.TextChanged -= h, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>Observe TextChanged event.</summary>
|
||||
public static Observable<Unit> OnTextChangedAsObservable(this TextEdit textEdit, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Observable.FromEvent(h => textEdit.TextChanged += h, h => textEdit.TextChanged -= h, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>Observe ItemSelected with current `Selected` on subscribe.</summary>
|
||||
public static Observable<long> OnItemSelectedAsObservable(this OptionButton optionButton, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Observable.Create<long, (OptionButton, CancellationToken)>((optionButton, cancellationToken), static (observer, state) =>
|
||||
{
|
||||
var (b, cancellationToken) = state;
|
||||
observer.OnNext(b.Selected);
|
||||
return Observable.FromEvent<OptionButton.ItemSelectedEventHandler, long>(h => new OptionButton.ItemSelectedEventHandler(h), h => b.ItemSelected += h, h => b.ItemSelected -= h, cancellationToken).Subscribe(observer);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://syarh3o2acgh
|
||||
@@ -0,0 +1,150 @@
|
||||
#if TOOLS
|
||||
#nullable enable
|
||||
|
||||
using Godot;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GDArray = Godot.Collections.Array;
|
||||
|
||||
namespace R3;
|
||||
|
||||
// ObservableTrackerDebuggerPlugin creates the Observable Tracker tab in the debugger, and communicates with ObservableTrackerRuntimeHook via EditorDebuggerSessions.
|
||||
[Tool]
|
||||
public partial class ObservableTrackerDebuggerPlugin : EditorDebuggerPlugin
|
||||
{
|
||||
// Shared header used in IPC by ObservableTracker classes.
|
||||
public const string MessageHeader = "ObservableTracker";
|
||||
|
||||
// Implemented by ObservableTrackerRuntimeHook.
|
||||
public const string Message_RequestActiveTasks = "RequestActiveTasks";
|
||||
public const string Message_SetEnableStates = "SetEnableStates";
|
||||
public const string Message_InvokeGCCollect = "InvokeGCCollect";
|
||||
|
||||
// Implemented by ObservableTrackerDebuggerPlugin.
|
||||
public const string Message_ReceiveActiveTasks = "ReceiveActiveTasks";
|
||||
|
||||
// A TrackerSession isolates each debugger session's states.
|
||||
// There's no way to know if a session has been disposed for good, so we will never remove anything from this dictionary.
|
||||
// This is similar to how it is handled in the Godot core (see: https://github.com/godotengine/godot/blob/master/modules/multiplayer/editor/multiplayer_editor_plugin.cpp)
|
||||
readonly Dictionary<int, TrackerSession> sessions = new();
|
||||
|
||||
private class TrackerSession
|
||||
{
|
||||
public readonly EditorDebuggerSession debuggerSession;
|
||||
public readonly List<TrackingState> states = new();
|
||||
public event Action<IEnumerable<TrackingState>>? ReceivedActiveTasks;
|
||||
|
||||
public TrackerSession(EditorDebuggerSession debuggerSession)
|
||||
{
|
||||
this.debuggerSession = debuggerSession;
|
||||
}
|
||||
|
||||
public void InvokeReceivedActiveTasks()
|
||||
{
|
||||
ReceivedActiveTasks?.Invoke(states);
|
||||
}
|
||||
}
|
||||
|
||||
public override void _SetupSession(int sessionId)
|
||||
{
|
||||
var currentSession = GetSession(sessionId);
|
||||
sessions[sessionId] = new TrackerSession(currentSession);
|
||||
|
||||
// NotifyOnSessionSetup gives the tab a reference to the debugger plugin, as well as the sessionId which is needed for messages.
|
||||
var tab = new ObservableTrackerTab();
|
||||
tab.NotifyOnSessionSetup(this, sessionId);
|
||||
currentSession.AddSessionTab(tab);
|
||||
|
||||
// As sessions don't seem to be ever disposed, we don't need to unregister these callbacks either.
|
||||
currentSession.Started += () =>
|
||||
{
|
||||
if (IsInstanceValid(tab))
|
||||
{
|
||||
tab.SetProcess(true);
|
||||
// Important! We need to tell the tab the session has started, so it can initialize the enabled states of the runtime ObservableTracker.
|
||||
tab.NotifyOnSessionStart();
|
||||
}
|
||||
};
|
||||
currentSession.Stopped += () =>
|
||||
{
|
||||
if (IsInstanceValid(tab))
|
||||
{
|
||||
tab.SetProcess(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public override bool _HasCapture(string capture)
|
||||
{
|
||||
return capture == MessageHeader;
|
||||
}
|
||||
|
||||
public override bool _Capture(string message, GDArray data, int sessionId)
|
||||
{
|
||||
// When EditorDebuggerPlugin._Capture receives messages, the header isn't trimmed (unlike how it is in EngineDebugger),
|
||||
// so we need to trim it here.
|
||||
string messageWithoutHeader = message.Substring(message.IndexOf(':') + 1);
|
||||
//GD.Print(nameof(ObservableTrackerDebuggerPlugin) + " received " + messageWithoutHeader);
|
||||
switch(messageWithoutHeader)
|
||||
{
|
||||
case Message_ReceiveActiveTasks:
|
||||
// Only invoke event if updated.
|
||||
if (data[0].AsBool())
|
||||
{
|
||||
var session = sessions[sessionId];
|
||||
session.states.Clear();
|
||||
foreach (GDArray item in data[1].AsGodotArray())
|
||||
{
|
||||
var state = new TrackingState()
|
||||
{
|
||||
TrackingId = item[0].AsInt32(),
|
||||
FormattedType = item[1].AsString(),
|
||||
AddTime = new DateTime(item[2].AsInt64()),
|
||||
StackTrace = item[3].AsString(),
|
||||
};;
|
||||
session.states.Add(state);
|
||||
}
|
||||
session.InvokeReceivedActiveTasks();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return base._Capture(message, data, sessionId);
|
||||
}
|
||||
|
||||
public void RegisterReceivedActiveTasks(int sessionId, Action<IEnumerable<TrackingState>> action)
|
||||
{
|
||||
if (sessions.Count > 0)
|
||||
sessions[sessionId].ReceivedActiveTasks += action;
|
||||
}
|
||||
|
||||
public void UnregisterReceivedActiveTasks(int sessionId, Action<IEnumerable<TrackingState>> action)
|
||||
{
|
||||
if (sessions.Count > 0)
|
||||
sessions[sessionId].ReceivedActiveTasks -= action;
|
||||
}
|
||||
|
||||
public void UpdateTrackingStates(int sessionId, bool forceUpdate = false)
|
||||
{
|
||||
if (sessions.Count > 0 && sessions[sessionId].debuggerSession.IsActive())
|
||||
{
|
||||
sessions[sessionId].debuggerSession.SendMessage(MessageHeader + ":" + Message_RequestActiveTasks, new () { forceUpdate });
|
||||
}
|
||||
}
|
||||
|
||||
public void SetEnableStates(int sessionId, bool enableTracking, bool enableStackTrace)
|
||||
{
|
||||
if (sessions.Count > 0 && sessions[sessionId].debuggerSession.IsActive())
|
||||
{
|
||||
sessions[sessionId].debuggerSession.SendMessage(MessageHeader + ":" + Message_SetEnableStates, new () { enableTracking, enableStackTrace});
|
||||
}
|
||||
}
|
||||
|
||||
public void InvokeGCCollect(int sessionId)
|
||||
{
|
||||
if (sessions.Count > 0 && sessions[sessionId].debuggerSession.IsActive())
|
||||
{
|
||||
sessions[sessionId].debuggerSession.SendMessage(MessageHeader + ":" + Message_InvokeGCCollect);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://bhe85bu3rgxja
|
||||
@@ -0,0 +1,58 @@
|
||||
|
||||
#nullable enable
|
||||
|
||||
using Godot;
|
||||
using System;
|
||||
using GDArray = Godot.Collections.Array;
|
||||
|
||||
namespace R3;
|
||||
|
||||
// Sends runtime ObservableTracker information to ObservableTrackerDebuggerPlugin.
|
||||
// Needs to be an Autoload. Should not be instantiated manually.
|
||||
public partial class ObservableTrackerRuntimeHook : Node
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
#if TOOLS
|
||||
EngineDebugger.RegisterMessageCapture(ObservableTrackerDebuggerPlugin.MessageHeader, Callable.From((string message, GDArray data) =>
|
||||
{
|
||||
//GD.Print(nameof(ObservableTrackerRuntimeHook) + " received " + message);
|
||||
switch (message)
|
||||
{
|
||||
case ObservableTrackerDebuggerPlugin.Message_RequestActiveTasks:
|
||||
// data[0]: If true, force an update anyway.
|
||||
if (ObservableTracker.CheckAndResetDirty() || data[0].AsBool())
|
||||
{
|
||||
GDArray states = new();
|
||||
ObservableTracker.ForEachActiveTask(state =>
|
||||
{
|
||||
// DateTime is not a Variant type, so we serialize it using Ticks instead.
|
||||
states.Add(new GDArray { state.TrackingId, state.FormattedType, state.AddTime.Ticks, state.StackTrace });
|
||||
});
|
||||
EngineDebugger.SendMessage(ObservableTrackerDebuggerPlugin.MessageHeader + ":" + ObservableTrackerDebuggerPlugin.Message_ReceiveActiveTasks, new () { true, states });
|
||||
}
|
||||
else
|
||||
{
|
||||
EngineDebugger.SendMessage(ObservableTrackerDebuggerPlugin.MessageHeader + ":" + ObservableTrackerDebuggerPlugin.Message_ReceiveActiveTasks, new () { false, });
|
||||
}
|
||||
break;
|
||||
case ObservableTrackerDebuggerPlugin.Message_SetEnableStates:
|
||||
ObservableTracker.EnableTracking = data[0].AsBool();
|
||||
ObservableTracker.EnableStackTrace = data[1].AsBool();
|
||||
break;
|
||||
case ObservableTrackerDebuggerPlugin.Message_InvokeGCCollect:
|
||||
GC.Collect(0);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}));
|
||||
#endif
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
#if TOOLS
|
||||
EngineDebugger.UnregisterMessageCapture(ObservableTrackerDebuggerPlugin.MessageHeader);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dwd2efc46qc0v
|
||||
146
src/SharpIDE.Godot/addons/R3.Godot/ObservableTrackerTab.cs
Normal file
146
src/SharpIDE.Godot/addons/R3.Godot/ObservableTrackerTab.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
#if TOOLS
|
||||
#nullable enable
|
||||
|
||||
using Godot;
|
||||
using System;
|
||||
|
||||
namespace R3;
|
||||
|
||||
[Tool]
|
||||
public partial class ObservableTrackerTab : VBoxContainer
|
||||
{
|
||||
public const string EnableAutoReloadKey = "ObservableTracker_EnableAutoReloadKey";
|
||||
public const string EnableTrackingKey = "ObservableTracker_EnableTrackingKey";
|
||||
public const string EnableStackTraceKey = "ObservableTracker_EnableStackTraceKey";
|
||||
bool enableAutoReload, enableTracking, enableStackTrace;
|
||||
ObservableTrackerTree? tree;
|
||||
ObservableTrackerDebuggerPlugin? debuggerPlugin;
|
||||
int interval = 0;
|
||||
int sessionId = 0;
|
||||
|
||||
public void NotifyOnSessionSetup(ObservableTrackerDebuggerPlugin debuggerPlugin, int sessionId)
|
||||
{
|
||||
this.debuggerPlugin = debuggerPlugin;
|
||||
this.sessionId = sessionId;
|
||||
tree ??= new ObservableTrackerTree();
|
||||
tree.NotifyOnSessionSetup(debuggerPlugin!, sessionId);
|
||||
}
|
||||
|
||||
public void NotifyOnSessionStart()
|
||||
{
|
||||
debuggerPlugin!.SetEnableStates(sessionId, enableTracking, enableStackTrace);
|
||||
}
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
Name = "Observable Tracker";
|
||||
|
||||
tree ??= new ObservableTrackerTree();
|
||||
|
||||
// Head panel
|
||||
var headPanelLayout = new HBoxContainer();
|
||||
headPanelLayout.SetAnchor(Side.Left, 0);
|
||||
headPanelLayout.SetAnchor(Side.Right, 0);
|
||||
AddChild(headPanelLayout);
|
||||
|
||||
// Toggle buttons (top left)
|
||||
var enableAutoReloadToggle = new CheckButton
|
||||
{
|
||||
Text = "Enable AutoReload",
|
||||
TooltipText = "Reload automatically."
|
||||
};
|
||||
var enableTrackingToggle = new CheckButton
|
||||
{
|
||||
Text = "Enable Tracking",
|
||||
TooltipText = "Start to track Observable subscription. Performance impact: low"
|
||||
};
|
||||
var enableStackTraceToggle = new CheckButton
|
||||
{
|
||||
Text = "Enable StackTrace",
|
||||
TooltipText = "Capture StackTrace when subscribed. Performance impact: high"
|
||||
};
|
||||
|
||||
// For every button: Initialize pressed state and subscribe to Toggled event.
|
||||
EditorSettings settings = EditorInterface.Singleton.GetEditorSettings();
|
||||
enableAutoReloadToggle.ButtonPressed = enableAutoReload = GetSettingOrDefault(settings, EnableAutoReloadKey, false).AsBool();
|
||||
enableAutoReloadToggle.Toggled += toggledOn =>
|
||||
{
|
||||
settings.SetSetting(EnableAutoReloadKey, toggledOn);
|
||||
enableAutoReload = toggledOn;
|
||||
};
|
||||
enableTrackingToggle.ButtonPressed = enableTracking = GetSettingOrDefault(settings, EnableTrackingKey, false).AsBool();
|
||||
enableTrackingToggle.Toggled += toggledOn =>
|
||||
{
|
||||
settings.SetSetting(EnableTrackingKey, toggledOn);
|
||||
enableTracking = toggledOn;
|
||||
debuggerPlugin!.SetEnableStates(sessionId, enableTracking, enableStackTrace);
|
||||
};
|
||||
enableStackTraceToggle.ButtonPressed = enableStackTrace = GetSettingOrDefault(settings, EnableStackTraceKey, false).AsBool();
|
||||
enableStackTraceToggle.Toggled += toggledOn =>
|
||||
{
|
||||
settings.SetSetting(EnableStackTraceKey, toggledOn);
|
||||
enableStackTrace = toggledOn;
|
||||
debuggerPlugin!.SetEnableStates(sessionId, enableTracking, enableStackTrace);
|
||||
};
|
||||
|
||||
// Regular buttons (top right)
|
||||
var reloadButton = new Button
|
||||
{
|
||||
Text = "Reload",
|
||||
TooltipText = "Reload View."
|
||||
};
|
||||
var GCButton = new Button
|
||||
{
|
||||
Text = "GC.Collect",
|
||||
TooltipText = "Invoke GC.Collect."
|
||||
};
|
||||
|
||||
reloadButton.Pressed += () =>
|
||||
{
|
||||
debuggerPlugin!.UpdateTrackingStates(sessionId, true);
|
||||
};
|
||||
GCButton.Pressed += () =>
|
||||
{
|
||||
debuggerPlugin!.InvokeGCCollect(sessionId);
|
||||
};
|
||||
|
||||
// Button layout.
|
||||
headPanelLayout.AddChild(enableAutoReloadToggle);
|
||||
headPanelLayout.AddChild(enableTrackingToggle);
|
||||
headPanelLayout.AddChild(enableStackTraceToggle);
|
||||
// Kind of like Unity's FlexibleSpace. Pushes the first three buttons to the left, and the remaining buttons to the right.
|
||||
headPanelLayout.AddChild(new Control()
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.Expand,
|
||||
});
|
||||
headPanelLayout.AddChild(reloadButton);
|
||||
headPanelLayout.AddChild(GCButton);
|
||||
|
||||
// Tree goes last.
|
||||
AddChild(tree);
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
if (enableAutoReload)
|
||||
{
|
||||
if (interval++ % 120 == 0)
|
||||
{
|
||||
debuggerPlugin!.UpdateTrackingStates(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Variant GetSettingOrDefault(EditorSettings settings, string key, Variant @default)
|
||||
{
|
||||
if (settings.HasSetting(key))
|
||||
{
|
||||
return settings.GetSetting(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
return @default;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://c2u5u8xkqvh1c
|
||||
67
src/SharpIDE.Godot/addons/R3.Godot/ObservableTrackerTree.cs
Normal file
67
src/SharpIDE.Godot/addons/R3.Godot/ObservableTrackerTree.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
#if TOOLS
|
||||
#nullable enable
|
||||
|
||||
using Godot;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace R3;
|
||||
|
||||
[Tool]
|
||||
public partial class ObservableTrackerTree : Tree
|
||||
{
|
||||
ObservableTrackerDebuggerPlugin? debuggerPlugin;
|
||||
int sessionId;
|
||||
public void NotifyOnSessionSetup(ObservableTrackerDebuggerPlugin debuggerPlugin, int sessionId)
|
||||
{
|
||||
this.debuggerPlugin = debuggerPlugin;
|
||||
this.sessionId = sessionId;
|
||||
debuggerPlugin!.RegisterReceivedActiveTasks(sessionId, Reload);
|
||||
Clear();
|
||||
}
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
AllowReselect = false;
|
||||
Columns = 3;
|
||||
ColumnTitlesVisible = true;
|
||||
SetColumnTitle(0, "Type");
|
||||
SetColumnTitle(1, "Elapsed");
|
||||
SetColumnTitle(2, "StackTrace");
|
||||
SetColumnExpand(0, true);
|
||||
SetColumnExpand(1, true);
|
||||
SetColumnExpand(2, true);
|
||||
SetColumnExpandRatio(0, 3);
|
||||
SetColumnExpandRatio(1, 1);
|
||||
SetColumnExpandRatio(2, 6);
|
||||
SetColumnClipContent(0, true);
|
||||
SetColumnClipContent(1, true);
|
||||
SetColumnClipContent(2, true);
|
||||
HideRoot = true;
|
||||
SizeFlagsVertical = SizeFlags.ExpandFill;
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
debuggerPlugin!.UnregisterReceivedActiveTasks(sessionId, Reload);
|
||||
}
|
||||
|
||||
public void Reload(IEnumerable<TrackingState> states)
|
||||
{
|
||||
Clear();
|
||||
TreeItem root = CreateItem();
|
||||
foreach(TrackingState state in states)
|
||||
{
|
||||
TreeItem row = CreateItem(root);
|
||||
var now = DateTime.Now;
|
||||
// Type
|
||||
row.SetText(0, state.FormattedType);
|
||||
// Elapsed
|
||||
row.SetText(1, (now - state.AddTime).TotalSeconds.ToString("00.00"));
|
||||
// StackTrace
|
||||
row.SetText(2, state.StackTrace);
|
||||
};
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://crct8t2w3hcce
|
||||
8
src/SharpIDE.Godot/addons/R3.Godot/plugin.cfg
Normal file
8
src/SharpIDE.Godot/addons/R3.Godot/plugin.cfg
Normal file
@@ -0,0 +1,8 @@
|
||||
[plugin]
|
||||
|
||||
name="R3.Godot"
|
||||
description="The new future of dotnet/reactive and UniRx."
|
||||
author="Cysharp"
|
||||
version="1.3.0"
|
||||
language="C-sharp"
|
||||
script="GodotR3Plugin.cs"
|
||||
@@ -17,6 +17,11 @@ run/max_fps=157
|
||||
run/low_processor_mode=true
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
[autoload]
|
||||
|
||||
FrameProviderDispatcher="*res://addons/R3.Godot/FrameProviderDispatcher.cs"
|
||||
ObservableTrackerRuntimeHook="*res://addons/R3.Godot/ObservableTrackerRuntimeHook.cs"
|
||||
|
||||
[display]
|
||||
|
||||
window/energy_saving/keep_screen_on=false
|
||||
@@ -25,6 +30,10 @@ window/energy_saving/keep_screen_on=false
|
||||
|
||||
project/assembly_name="SharpIDE.Godot"
|
||||
|
||||
[editor_plugins]
|
||||
|
||||
enabled=PackedStringArray("res://addons/R3.Godot/plugin.cfg", "res://addons/csharp_gdextension_bindgen/plugin.cfg")
|
||||
|
||||
[input]
|
||||
|
||||
CodeFixes={
|
||||
|
||||
Reference in New Issue
Block a user