add reactive binding
This commit is contained in:
@@ -1,13 +1,25 @@
|
|||||||
using Godot;
|
using Godot;
|
||||||
|
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
|
||||||
|
using SharpIDE.Godot.Features.Problems;
|
||||||
|
|
||||||
namespace SharpIDE.Godot.Features.BottomPanel;
|
namespace SharpIDE.Godot.Features.BottomPanel;
|
||||||
|
|
||||||
public partial class BottomPanelManager : Panel
|
public partial class BottomPanelManager : Panel
|
||||||
{
|
{
|
||||||
|
public SharpIdeSolutionModel? Solution
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
field = value;
|
||||||
|
_problemsPanel.Solution = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Control _runPanel = null!;
|
private Control _runPanel = null!;
|
||||||
private Control _debugPanel = null!;
|
private Control _debugPanel = null!;
|
||||||
private Control _buildPanel = null!;
|
private Control _buildPanel = null!;
|
||||||
private Control _problemsPanel = null!;
|
private ProblemsPanel _problemsPanel = null!;
|
||||||
|
|
||||||
private Dictionary<BottomPanelType, Control> _panelTypeMap = [];
|
private Dictionary<BottomPanelType, Control> _panelTypeMap = [];
|
||||||
|
|
||||||
@@ -16,7 +28,7 @@ public partial class BottomPanelManager : Panel
|
|||||||
_runPanel = GetNode<Control>("%RunPanel");
|
_runPanel = GetNode<Control>("%RunPanel");
|
||||||
_debugPanel = GetNode<Control>("%DebugPanel");
|
_debugPanel = GetNode<Control>("%DebugPanel");
|
||||||
_buildPanel = GetNode<Control>("%BuildPanel");
|
_buildPanel = GetNode<Control>("%BuildPanel");
|
||||||
_problemsPanel = GetNode<Control>("%ProblemsPanel");
|
_problemsPanel = GetNode<ProblemsPanel>("%ProblemsPanel");
|
||||||
|
|
||||||
_panelTypeMap = new Dictionary<BottomPanelType, Control>
|
_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"]
|
[node name="ProblemsPanel" type="Control"]
|
||||||
layout_mode = 3
|
layout_mode = 3
|
||||||
@@ -7,3 +10,23 @@ anchor_right = 1.0
|
|||||||
anchor_bottom = 1.0
|
anchor_bottom = 1.0
|
||||||
grow_horizontal = 2
|
grow_horizontal = 2
|
||||||
grow_vertical = 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 Microsoft.Build.Locator;
|
||||||
using SharpIDE.Application.Features.Analysis;
|
using SharpIDE.Application.Features.Analysis;
|
||||||
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
|
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
|
||||||
|
using SharpIDE.Godot.Features.BottomPanel;
|
||||||
using SharpIDE.Godot.Features.CustomControls;
|
using SharpIDE.Godot.Features.CustomControls;
|
||||||
using SharpIDE.Godot.Features.Run;
|
using SharpIDE.Godot.Features.Run;
|
||||||
using SharpIDE.Godot.Features.SolutionExplorer;
|
using SharpIDE.Godot.Features.SolutionExplorer;
|
||||||
@@ -19,6 +20,7 @@ public partial class IdeRoot : Control
|
|||||||
private RunPanel _runPanel = null!;
|
private RunPanel _runPanel = null!;
|
||||||
private Button _runMenuButton = null!;
|
private Button _runMenuButton = null!;
|
||||||
private Popup _runMenuPopup = null!;
|
private Popup _runMenuPopup = null!;
|
||||||
|
private BottomPanelManager _bottomPanelManager = null!;
|
||||||
|
|
||||||
private readonly PackedScene _runMenuItemScene = ResourceLoader.Load<PackedScene>("res://Features/Run/RunMenuItem.tscn");
|
private readonly PackedScene _runMenuItemScene = ResourceLoader.Load<PackedScene>("res://Features/Run/RunMenuItem.tscn");
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
@@ -34,6 +36,7 @@ public partial class IdeRoot : Control
|
|||||||
_solutionExplorerPanel = GetNode<SolutionExplorerPanel>("%SolutionExplorerPanel");
|
_solutionExplorerPanel = GetNode<SolutionExplorerPanel>("%SolutionExplorerPanel");
|
||||||
_runPanel = GetNode<RunPanel>("%RunPanel");
|
_runPanel = GetNode<RunPanel>("%RunPanel");
|
||||||
_invertedVSplitContainer = GetNode<InvertedVSplitContainer>("%InvertedVSplitContainer");
|
_invertedVSplitContainer = GetNode<InvertedVSplitContainer>("%InvertedVSplitContainer");
|
||||||
|
_bottomPanelManager = GetNode<BottomPanelManager>("%BottomPanel");
|
||||||
|
|
||||||
_runMenuButton.Pressed += OnRunMenuButtonPressed;
|
_runMenuButton.Pressed += OnRunMenuButtonPressed;
|
||||||
_solutionExplorerPanel.FileSelected += OnSolutionExplorerPanelOnFileSelected;
|
_solutionExplorerPanel.FileSelected += OnSolutionExplorerPanelOnFileSelected;
|
||||||
@@ -71,6 +74,7 @@ public partial class IdeRoot : Control
|
|||||||
var solutionModel = await VsPersistenceMapper.GetSolutionModel(path);
|
var solutionModel = await VsPersistenceMapper.GetSolutionModel(path);
|
||||||
_solutionExplorerPanel.SolutionModel = solutionModel;
|
_solutionExplorerPanel.SolutionModel = solutionModel;
|
||||||
_sharpIdeCodeEdit.Solution = solutionModel;
|
_sharpIdeCodeEdit.Solution = solutionModel;
|
||||||
|
_bottomPanelManager.Solution = solutionModel;
|
||||||
Callable.From(_solutionExplorerPanel.RepopulateTree).CallDeferred();
|
Callable.From(_solutionExplorerPanel.RepopulateTree).CallDeferred();
|
||||||
RoslynAnalysis.StartSolutionAnalysis(path);
|
RoslynAnalysis.StartSolutionAnalysis(path);
|
||||||
|
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ item_0/text = "Getting Context Actions..."
|
|||||||
item_0/id = 0
|
item_0/id = 0
|
||||||
|
|
||||||
[node name="BottomPanel" type="Panel" parent="VBoxContainer/HBoxContainer/InvertedVSplitContainer"]
|
[node name="BottomPanel" type="Panel" parent="VBoxContainer/HBoxContainer/InvertedVSplitContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
script = ExtResource("7_i62lx")
|
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;
|
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
|
public static class NodeExtensions
|
||||||
{
|
{
|
||||||
extension(Node node)
|
extension(Node node)
|
||||||
|
|||||||
@@ -8,4 +8,8 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\SharpIDE.Application\SharpIDE.Application.csproj" />
|
<ProjectReference Include="..\SharpIDE.Application\SharpIDE.Application.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="ObservableCollections" Version="3.3.4" />
|
||||||
|
<PackageReference Include="R3" Version="1.3.0" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</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
|
run/low_processor_mode=true
|
||||||
config/icon="res://icon.svg"
|
config/icon="res://icon.svg"
|
||||||
|
|
||||||
|
[autoload]
|
||||||
|
|
||||||
|
FrameProviderDispatcher="*res://addons/R3.Godot/FrameProviderDispatcher.cs"
|
||||||
|
ObservableTrackerRuntimeHook="*res://addons/R3.Godot/ObservableTrackerRuntimeHook.cs"
|
||||||
|
|
||||||
[display]
|
[display]
|
||||||
|
|
||||||
window/energy_saving/keep_screen_on=false
|
window/energy_saving/keep_screen_on=false
|
||||||
@@ -25,6 +30,10 @@ window/energy_saving/keep_screen_on=false
|
|||||||
|
|
||||||
project/assembly_name="SharpIDE.Godot"
|
project/assembly_name="SharpIDE.Godot"
|
||||||
|
|
||||||
|
[editor_plugins]
|
||||||
|
|
||||||
|
enabled=PackedStringArray("res://addons/R3.Godot/plugin.cfg", "res://addons/csharp_gdextension_bindgen/plugin.cfg")
|
||||||
|
|
||||||
[input]
|
[input]
|
||||||
|
|
||||||
CodeFixes={
|
CodeFixes={
|
||||||
|
|||||||
Reference in New Issue
Block a user