Merge pull request #97894 from paulloz/dotnet/export-tool-button

Implement `[ExportToolButton]`
This commit is contained in:
Thaddeus Crews 2024-11-19 15:20:07 -06:00
commit ec7fd4f6f1
No known key found for this signature in database
GPG Key ID: 62181B86FE9E5D84
18 changed files with 406 additions and 3 deletions

View File

@ -74,4 +74,31 @@ public class ExportDiagnosticsTests
} }
); );
} }
[Fact]
public async void ExportToolButtonInNonToolClass()
{
await CSharpSourceGeneratorVerifier<ScriptPropertiesGenerator>.Verify(
new string[] { "ExportDiagnostics_GD0108.cs" },
new string[] { "ExportDiagnostics_GD0108_ScriptProperties.generated.cs" }
);
}
[Fact]
public async void ExportAndExportToolButtonOnSameMember()
{
await CSharpSourceGeneratorVerifier<ScriptPropertiesGenerator>.Verify(
new string[] { "ExportDiagnostics_GD0109.cs" },
new string[] { "ExportDiagnostics_GD0109_ScriptProperties.generated.cs" }
);
}
[Fact]
public async void ExportToolButtonOnNonCallable()
{
await CSharpSourceGeneratorVerifier<ScriptPropertiesGenerator>.Verify(
new string[] { "ExportDiagnostics_GD0110.cs" },
new string[] { "ExportDiagnostics_GD0110_ScriptProperties.generated.cs" }
);
}
} }

View File

@ -66,4 +66,13 @@ public class ScriptPropertiesGeneratorTests
"AbstractGenericNode(Of T)_ScriptProperties.generated.cs" "AbstractGenericNode(Of T)_ScriptProperties.generated.cs"
); );
} }
[Fact]
public async void ExportedButtons()
{
await CSharpSourceGeneratorVerifier<ScriptPropertiesGenerator>.Verify(
"ExportedToolButtons.cs",
"ExportedToolButtons_ScriptProperties.generated.cs"
);
}
} }

View File

@ -0,0 +1,48 @@
using Godot;
using Godot.NativeInterop;
partial class ExportDiagnostics_GD0108
{
#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword
/// <summary>
/// Cached StringNames for the properties and fields contained in this class, for fast lookup.
/// </summary>
public new class PropertyName : global::Godot.Node.PropertyName {
/// <summary>
/// Cached name for the 'MyButton' field.
/// </summary>
public new static readonly global::Godot.StringName @MyButton = "MyButton";
}
/// <inheritdoc/>
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
protected override bool SetGodotClassPropertyValue(in godot_string_name name, in godot_variant value)
{
if (name == PropertyName.@MyButton) {
this.@MyButton = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Callable>(value);
return true;
}
return base.SetGodotClassPropertyValue(name, value);
}
/// <inheritdoc/>
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
protected override bool GetGodotClassPropertyValue(in godot_string_name name, out godot_variant value)
{
if (name == PropertyName.@MyButton) {
value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Callable>(this.@MyButton);
return true;
}
return base.GetGodotClassPropertyValue(name, out value);
}
/// <summary>
/// Get the property information for all the properties declared in this class.
/// This method is used by Godot to register the available properties in the editor.
/// Do not call this method.
/// </summary>
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
internal new static global::System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo> GetGodotPropertyList()
{
var properties = new global::System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo>();
return properties;
}
#pragma warning restore CS0109
}

View File

@ -0,0 +1,48 @@
using Godot;
using Godot.NativeInterop;
partial class ExportDiagnostics_GD0109
{
#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword
/// <summary>
/// Cached StringNames for the properties and fields contained in this class, for fast lookup.
/// </summary>
public new class PropertyName : global::Godot.Node.PropertyName {
/// <summary>
/// Cached name for the 'MyButton' field.
/// </summary>
public new static readonly global::Godot.StringName @MyButton = "MyButton";
}
/// <inheritdoc/>
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
protected override bool SetGodotClassPropertyValue(in godot_string_name name, in godot_variant value)
{
if (name == PropertyName.@MyButton) {
this.@MyButton = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Callable>(value);
return true;
}
return base.SetGodotClassPropertyValue(name, value);
}
/// <inheritdoc/>
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
protected override bool GetGodotClassPropertyValue(in godot_string_name name, out godot_variant value)
{
if (name == PropertyName.@MyButton) {
value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Callable>(this.@MyButton);
return true;
}
return base.GetGodotClassPropertyValue(name, out value);
}
/// <summary>
/// Get the property information for all the properties declared in this class.
/// This method is used by Godot to register the available properties in the editor.
/// Do not call this method.
/// </summary>
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
internal new static global::System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo> GetGodotPropertyList()
{
var properties = new global::System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo>();
return properties;
}
#pragma warning restore CS0109
}

View File

@ -0,0 +1,48 @@
using Godot;
using Godot.NativeInterop;
partial class ExportDiagnostics_GD0110
{
#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword
/// <summary>
/// Cached StringNames for the properties and fields contained in this class, for fast lookup.
/// </summary>
public new class PropertyName : global::Godot.Node.PropertyName {
/// <summary>
/// Cached name for the 'MyButton' field.
/// </summary>
public new static readonly global::Godot.StringName @MyButton = "MyButton";
}
/// <inheritdoc/>
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
protected override bool SetGodotClassPropertyValue(in godot_string_name name, in godot_variant value)
{
if (name == PropertyName.@MyButton) {
this.@MyButton = global::Godot.NativeInterop.VariantUtils.ConvertTo<string>(value);
return true;
}
return base.SetGodotClassPropertyValue(name, value);
}
/// <inheritdoc/>
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
protected override bool GetGodotClassPropertyValue(in godot_string_name name, out godot_variant value)
{
if (name == PropertyName.@MyButton) {
value = global::Godot.NativeInterop.VariantUtils.CreateFrom<string>(this.@MyButton);
return true;
}
return base.GetGodotClassPropertyValue(name, out value);
}
/// <summary>
/// Get the property information for all the properties declared in this class.
/// This method is used by Godot to register the available properties in the editor.
/// Do not call this method.
/// </summary>
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
internal new static global::System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo> GetGodotPropertyList()
{
var properties = new global::System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo>();
return properties;
}
#pragma warning restore CS0109
}

View File

@ -0,0 +1,48 @@
using Godot;
using Godot.NativeInterop;
partial class ExportedToolButtons
{
#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword
/// <summary>
/// Cached StringNames for the properties and fields contained in this class, for fast lookup.
/// </summary>
public new class PropertyName : global::Godot.GodotObject.PropertyName {
/// <summary>
/// Cached name for the 'MyButton1' property.
/// </summary>
public new static readonly global::Godot.StringName @MyButton1 = "MyButton1";
/// <summary>
/// Cached name for the 'MyButton2' property.
/// </summary>
public new static readonly global::Godot.StringName @MyButton2 = "MyButton2";
}
/// <inheritdoc/>
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
protected override bool GetGodotClassPropertyValue(in godot_string_name name, out godot_variant value)
{
if (name == PropertyName.@MyButton1) {
value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Callable>(this.@MyButton1);
return true;
}
if (name == PropertyName.@MyButton2) {
value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Callable>(this.@MyButton2);
return true;
}
return base.GetGodotClassPropertyValue(name, out value);
}
/// <summary>
/// Get the property information for all the properties declared in this class.
/// This method is used by Godot to register the available properties in the editor.
/// Do not call this method.
/// </summary>
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
internal new static global::System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo> GetGodotPropertyList()
{
var properties = new global::System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo>();
properties.Add(new(type: (global::Godot.Variant.Type)25, name: PropertyName.@MyButton1, hint: (global::Godot.PropertyHint)39, hintString: "Click me!", usage: (global::Godot.PropertyUsageFlags)4, exported: true));
properties.Add(new(type: (global::Godot.Variant.Type)25, name: PropertyName.@MyButton2, hint: (global::Godot.PropertyHint)39, hintString: "Click me!,ColorRect", usage: (global::Godot.PropertyUsageFlags)4, exported: true));
return properties;
}
#pragma warning restore CS0109
}

View File

@ -0,0 +1,8 @@
using Godot;
using Godot.Collections;
public partial class ExportDiagnostics_GD0108 : Node
{
[ExportToolButton("")]
public Callable {|GD0108:MyButton|};
}

View File

@ -0,0 +1,9 @@
using Godot;
using Godot.Collections;
[Tool]
public partial class ExportDiagnostics_GD0109 : Node
{
[Export, ExportToolButton("")]
public Callable {|GD0109:MyButton|};
}

View File

@ -0,0 +1,9 @@
using Godot;
using Godot.Collections;
[Tool]
public partial class ExportDiagnostics_GD0110 : Node
{
[ExportToolButton("")]
public string {|GD0110:MyButton|};
}

View File

@ -0,0 +1,12 @@
using Godot;
using System;
[Tool]
public partial class ExportedToolButtons : GodotObject
{
[ExportToolButton("Click me!")]
public Callable MyButton1 => Callable.From(() => { GD.Print("Clicked MyButton1!"); });
[ExportToolButton("Click me!", Icon = "ColorRect")]
public Callable MyButton2 => Callable.From(() => { GD.Print("Clicked MyButton2!"); });
}

View File

@ -3,3 +3,6 @@
Rule ID | Category | Severity | Notes Rule ID | Category | Severity | Notes
--------|----------|----------|-------------------- --------|----------|----------|--------------------
GD0003 | Usage | Error | ScriptPathAttributeGenerator, [Documentation](https://docs.godotengine.org/en/latest/tutorials/scripting/c_sharp/diagnostics/GD0003.html) GD0003 | Usage | Error | ScriptPathAttributeGenerator, [Documentation](https://docs.godotengine.org/en/latest/tutorials/scripting/c_sharp/diagnostics/GD0003.html)
GD0108 | Usage | Error | ScriptPropertiesGenerator, [Documentation](https://docs.godotengine.org/en/latest/tutorials/scripting/c_sharp/diagnostics/GD0108.html)
GD0109 | Usage | Error | ScriptPropertiesGenerator, [Documentation](https://docs.godotengine.org/en/latest/tutorials/scripting/c_sharp/diagnostics/GD0109.html)
GD0110 | Usage | Error | ScriptPropertiesGenerator, [Documentation](https://docs.godotengine.org/en/latest/tutorials/scripting/c_sharp/diagnostics/GD0110.html)

View File

@ -107,6 +107,36 @@ namespace Godot.SourceGenerators
"Types not derived from Node should not export Node members. Node export is only supported in Node-derived classes.", "Types not derived from Node should not export Node members. Node export is only supported in Node-derived classes.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0107")); helpLinkUri: string.Format(_helpLinkFormat, "GD0107"));
public static readonly DiagnosticDescriptor OnlyToolClassesShouldUseExportToolButtonRule =
new DiagnosticDescriptor(id: "GD0108",
title: "The exported tool button is not in a tool class",
messageFormat: "The exported tool button '{0}' is not in a tool class",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
"The exported tool button is not in a tool class. Annotate the class with the '[Tool]' attribute, or remove the '[ExportToolButton]' attribute.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0108"));
public static readonly DiagnosticDescriptor ExportToolButtonShouldNotBeUsedWithExportRule =
new DiagnosticDescriptor(id: "GD0109",
title: "The '[ExportToolButton]' attribute cannot be used with another '[Export]' attribute",
messageFormat: "The '[ExportToolButton]' attribute cannot be used with another '[Export]' attribute on '{0}'",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
"The '[ExportToolButton]' attribute cannot be used with the '[Export]' attribute. Remove one of the attributes.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0109"));
public static readonly DiagnosticDescriptor ExportToolButtonIsNotCallableRule =
new DiagnosticDescriptor(id: "GD0110",
title: "The exported tool button is not a Callable",
messageFormat: "The exported tool button '{0}' is not a Callable",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
"The exported tool button is not a Callable. The '[ExportToolButton]' attribute is only supported on members of type Callable.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0110"));
public static readonly DiagnosticDescriptor SignalDelegateMissingSuffixRule = public static readonly DiagnosticDescriptor SignalDelegateMissingSuffixRule =
new DiagnosticDescriptor(id: "GD0201", new DiagnosticDescriptor(id: "GD0201",
title: "The name of the delegate must end with 'EventHandler'", title: "The name of the delegate must end with 'EventHandler'",

View File

@ -287,6 +287,12 @@ namespace Godot.SourceGenerators
public static bool IsGodotGlobalClassAttribute(this INamedTypeSymbol symbol) public static bool IsGodotGlobalClassAttribute(this INamedTypeSymbol symbol)
=> symbol.FullQualifiedNameOmitGlobal() == GodotClasses.GlobalClassAttr; => symbol.FullQualifiedNameOmitGlobal() == GodotClasses.GlobalClassAttr;
public static bool IsGodotExportToolButtonAttribute(this INamedTypeSymbol symbol)
=> symbol.FullQualifiedNameOmitGlobal() == GodotClasses.ExportToolButtonAttr;
public static bool IsGodotToolAttribute(this INamedTypeSymbol symbol)
=> symbol.FullQualifiedNameOmitGlobal() == GodotClasses.ToolAttr;
public static bool IsSystemFlagsAttribute(this INamedTypeSymbol symbol) public static bool IsSystemFlagsAttribute(this INamedTypeSymbol symbol)
=> symbol.FullQualifiedNameOmitGlobal() == GodotClasses.SystemFlagsAttr; => symbol.FullQualifiedNameOmitGlobal() == GodotClasses.SystemFlagsAttr;

View File

@ -9,10 +9,12 @@ namespace Godot.SourceGenerators
public const string ExportCategoryAttr = "Godot.ExportCategoryAttribute"; public const string ExportCategoryAttr = "Godot.ExportCategoryAttribute";
public const string ExportGroupAttr = "Godot.ExportGroupAttribute"; public const string ExportGroupAttr = "Godot.ExportGroupAttribute";
public const string ExportSubgroupAttr = "Godot.ExportSubgroupAttribute"; public const string ExportSubgroupAttr = "Godot.ExportSubgroupAttribute";
public const string ExportToolButtonAttr = "Godot.ExportToolButtonAttribute";
public const string SignalAttr = "Godot.SignalAttribute"; public const string SignalAttr = "Godot.SignalAttribute";
public const string MustBeVariantAttr = "Godot.MustBeVariantAttribute"; public const string MustBeVariantAttr = "Godot.MustBeVariantAttribute";
public const string GodotClassNameAttr = "Godot.GodotClassNameAttribute"; public const string GodotClassNameAttr = "Godot.GodotClassNameAttribute";
public const string GlobalClassAttr = "Godot.GlobalClassAttribute"; public const string GlobalClassAttr = "Godot.GlobalClassAttribute";
public const string ToolAttr = "Godot.ToolAttribute";
public const string SystemFlagsAttr = "System.FlagsAttribute"; public const string SystemFlagsAttr = "System.FlagsAttribute";
} }
} }

View File

@ -89,7 +89,8 @@ namespace Godot.SourceGenerators
Password = 36, Password = 36,
LayersAvoidance = 37, LayersAvoidance = 37,
DictionaryType = 38, DictionaryType = 38,
Max = 39 ToolButton = 39,
Max = 40
} }
[Flags] [Flags]

View File

@ -69,6 +69,7 @@ namespace Godot.SourceGenerators
bool hasNamespace = classNs.Length != 0; bool hasNamespace = classNs.Length != 0;
bool isInnerClass = symbol.ContainingType != null; bool isInnerClass = symbol.ContainingType != null;
bool isToolClass = symbol.GetAttributes().Any(a => a.AttributeClass?.IsGodotToolAttribute() ?? false);
string uniqueHint = symbol.FullQualifiedNameOmitGlobal().SanitizeQualifiedNameForUniqueHint() string uniqueHint = symbol.FullQualifiedNameOmitGlobal().SanitizeQualifiedNameForUniqueHint()
+ "_ScriptProperties.generated"; + "_ScriptProperties.generated";
@ -277,6 +278,16 @@ namespace Godot.SourceGenerators
if (propertyInfo == null) if (propertyInfo == null)
continue; continue;
if (propertyInfo.Value.Hint == PropertyHint.ToolButton && !isToolClass)
{
context.ReportDiagnostic(Diagnostic.Create(
Common.OnlyToolClassesShouldUseExportToolButtonRule,
member.Symbol.Locations.FirstLocationWithSourceTreeOrDefault(),
member.Symbol.ToDisplayString()
));
continue;
}
AppendPropertyInfo(source, propertyInfo.Value); AppendPropertyInfo(source, propertyInfo.Value);
} }
@ -418,6 +429,19 @@ namespace Godot.SourceGenerators
var exportAttr = memberSymbol.GetAttributes() var exportAttr = memberSymbol.GetAttributes()
.FirstOrDefault(a => a.AttributeClass?.IsGodotExportAttribute() ?? false); .FirstOrDefault(a => a.AttributeClass?.IsGodotExportAttribute() ?? false);
var exportToolButtonAttr = memberSymbol.GetAttributes()
.FirstOrDefault(a => a.AttributeClass?.IsGodotExportToolButtonAttribute() ?? false);
if (exportAttr != null && exportToolButtonAttr != null)
{
context.ReportDiagnostic(Diagnostic.Create(
Common.ExportToolButtonShouldNotBeUsedWithExportRule,
memberSymbol.Locations.FirstLocationWithSourceTreeOrDefault(),
memberSymbol.ToDisplayString()
));
return null;
}
var propertySymbol = memberSymbol as IPropertySymbol; var propertySymbol = memberSymbol as IPropertySymbol;
var fieldSymbol = memberSymbol as IFieldSymbol; var fieldSymbol = memberSymbol as IFieldSymbol;
@ -431,19 +455,56 @@ namespace Godot.SourceGenerators
} }
} }
if (exportToolButtonAttr != null && propertySymbol != null && propertySymbol.GetMethod == null)
{
context.ReportDiagnostic(Diagnostic.Create(
Common.ExportedPropertyIsWriteOnlyRule,
propertySymbol.Locations.FirstLocationWithSourceTreeOrDefault(),
propertySymbol.ToDisplayString()
));
return null;
}
var memberType = propertySymbol?.Type ?? fieldSymbol!.Type; var memberType = propertySymbol?.Type ?? fieldSymbol!.Type;
var memberVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(marshalType)!.Value; var memberVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(marshalType)!.Value;
string memberName = memberSymbol.Name; string memberName = memberSymbol.Name;
string? hintString = null;
if (exportToolButtonAttr != null)
{
if (memberVariantType != VariantType.Callable)
{
context.ReportDiagnostic(Diagnostic.Create(
Common.ExportToolButtonIsNotCallableRule,
memberSymbol.Locations.FirstLocationWithSourceTreeOrDefault(),
memberSymbol.ToDisplayString()
));
return null;
}
hintString = exportToolButtonAttr.ConstructorArguments[0].Value?.ToString() ?? "";
foreach (var namedArgument in exportToolButtonAttr.NamedArguments)
{
if (namedArgument is { Key: "Icon", Value.Value: string { Length: > 0 } })
{
hintString += $",{namedArgument.Value.Value}";
}
}
return new PropertyInfo(memberVariantType, memberName, PropertyHint.ToolButton,
hintString: hintString, PropertyUsageFlags.Editor, exported: true);
}
if (exportAttr == null) if (exportAttr == null)
{ {
return new PropertyInfo(memberVariantType, memberName, PropertyHint.None, return new PropertyInfo(memberVariantType, memberName, PropertyHint.None,
hintString: null, PropertyUsageFlags.ScriptVariable, exported: false); hintString: hintString, PropertyUsageFlags.ScriptVariable, exported: false);
} }
if (!TryGetMemberExportHint(typeCache, memberType, exportAttr, memberVariantType, if (!TryGetMemberExportHint(typeCache, memberType, exportAttr, memberVariantType,
isTypeArgument: false, out var hint, out var hintString)) isTypeArgument: false, out var hint, out hintString))
{ {
var constructorArguments = exportAttr.ConstructorArguments; var constructorArguments = exportAttr.ConstructorArguments;

View File

@ -0,0 +1,33 @@
using System;
#nullable enable
namespace Godot
{
/// <summary>
/// Exports the annotated <see cref="Callable"/> as a clickable button.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public sealed class ExportToolButtonAttribute : Attribute
{
/// <summary>
/// The label of the button.
/// </summary>
public string Text { get; }
/// <summary>
/// If defined, used to fetch an icon for the button via <see cref="Control.GetThemeIcon"/>,
/// from the <code>EditorIcons</code> theme type.
/// </summary>
public string? Icon { get; init; }
/// <summary>
/// Exports the annotated <see cref="Callable"/> as a clickable button.
/// </summary>
/// <param name="text">The label of the button.</param>
public ExportToolButtonAttribute(string text)
{
Text = text;
}
}
}

View File

@ -48,6 +48,7 @@
<!-- Sources --> <!-- Sources -->
<ItemGroup> <ItemGroup>
<Compile Include="Core\Aabb.cs" /> <Compile Include="Core\Aabb.cs" />
<Compile Include="Core\Attributes\ExportToolButtonAttribute.cs" />
<Compile Include="Core\Bridge\GodotSerializationInfo.cs" /> <Compile Include="Core\Bridge\GodotSerializationInfo.cs" />
<Compile Include="Core\Bridge\MethodInfo.cs" /> <Compile Include="Core\Bridge\MethodInfo.cs" />
<Compile Include="Core\Callable.generics.cs" /> <Compile Include="Core\Callable.generics.cs" />