From e1b73d0954ab799d9c7132ac25652867528c4d89 Mon Sep 17 00:00:00 2001 From: Bamboy Date: Sat, 31 Jan 2026 07:02:51 -0800 Subject: [PATCH 01/28] WIP refactor of PropertyGrid to be type user-extendable. --- .../UI/Properties/AttachedPropertyRecord.cs | 4 +- .../Graphics2D/UI/Properties/FieldRecord.cs | 20 +-- .../Graphics2D/UI/Properties/IInspector.cs | 9 + src/Myra/Graphics2D/UI/Properties/IRecord.cs | 18 ++ .../UI/Properties/PropertyEditor.cs | 128 ++++++++++++++ .../UI/Properties/PropertyGrid.SubGrid.cs | 130 +++++++++++++++ .../Graphics2D/UI/Properties/PropertyGrid.cs | 157 ++---------------- .../UI/Properties/PropertyRecord.cs | 20 +-- src/Myra/Graphics2D/UI/Properties/Record.cs | 14 +- src/Myra/Myra.MonoGame.csproj | 12 ++ 10 files changed, 329 insertions(+), 183 deletions(-) create mode 100644 src/Myra/Graphics2D/UI/Properties/IInspector.cs create mode 100644 src/Myra/Graphics2D/UI/Properties/IRecord.cs create mode 100644 src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs create mode 100644 src/Myra/Graphics2D/UI/Properties/PropertyGrid.SubGrid.cs diff --git a/src/Myra/Graphics2D/UI/Properties/AttachedPropertyRecord.cs b/src/Myra/Graphics2D/UI/Properties/AttachedPropertyRecord.cs index 73beb3bf..56a99a31 100644 --- a/src/Myra/Graphics2D/UI/Properties/AttachedPropertyRecord.cs +++ b/src/Myra/Graphics2D/UI/Properties/AttachedPropertyRecord.cs @@ -19,8 +19,8 @@ public AttachedPropertyRecord(BaseAttachedPropertyInfo property) public override string Name => _property.Name; public override Type Type => _property.PropertyType; - public override object GetValue(object obj) => _property.GetValueObject((Widget)obj); + public override object GetValue(object field) => _property.GetValueObject((Widget)field); - public override void SetValue(object obj, object value) => _property.SetValueObject((Widget)obj, value); + public override void SetValue(object field, object value) => _property.SetValueObject((Widget)field, value); } } diff --git a/src/Myra/Graphics2D/UI/Properties/FieldRecord.cs b/src/Myra/Graphics2D/UI/Properties/FieldRecord.cs index a021ec24..d9194d6c 100644 --- a/src/Myra/Graphics2D/UI/Properties/FieldRecord.cs +++ b/src/Myra/Graphics2D/UI/Properties/FieldRecord.cs @@ -7,16 +7,8 @@ internal class FieldRecord : Record { private readonly FieldInfo _fieldInfo; - public override string Name - { - get { return _fieldInfo.Name; } - } - - public override Type Type - { - get { return _fieldInfo.FieldType; } - } - + public override string Name => _fieldInfo.Name; + public override Type Type => _fieldInfo.FieldType; public override MemberInfo MemberInfo => _fieldInfo; public FieldRecord(FieldInfo fieldInfo) @@ -24,14 +16,14 @@ public FieldRecord(FieldInfo fieldInfo) _fieldInfo = fieldInfo; } - public override object GetValue(object obj) + public override object GetValue(object field) { - return _fieldInfo.GetValue(obj); + return _fieldInfo.GetValue(field); } - public override void SetValue(object obj, object value) + public override void SetValue(object field, object value) { - _fieldInfo.SetValue(obj, value); + _fieldInfo.SetValue(field, value); } } } diff --git a/src/Myra/Graphics2D/UI/Properties/IInspector.cs b/src/Myra/Graphics2D/UI/Properties/IInspector.cs new file mode 100644 index 00000000..f0c12460 --- /dev/null +++ b/src/Myra/Graphics2D/UI/Properties/IInspector.cs @@ -0,0 +1,9 @@ +namespace Myra.Graphics2D.UI.Properties +{ + public interface IInspector + { + //object? SelectionRoot { get; set; } + object SelectedField { get; } + void FireChanged(string propertyName); + } +} \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/IRecord.cs b/src/Myra/Graphics2D/UI/Properties/IRecord.cs new file mode 100644 index 00000000..def08309 --- /dev/null +++ b/src/Myra/Graphics2D/UI/Properties/IRecord.cs @@ -0,0 +1,18 @@ +using System; +using System.Reflection; + +namespace Myra.Graphics2D.UI.Properties +{ + public interface IRecord + { + Type Type { get; } + object GetValue(object field); + void SetValue(object field, object value); + } + + public interface IRecord : IRecord + { + new T GetValue(object field); + void SetValue(object field, T value); + } +} \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs new file mode 100644 index 00000000..2d49dd8c --- /dev/null +++ b/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Myra.Graphics2D.UI.Properties +{ + /// + /// Encapsulates a widget and .Net property or field, for the purposes of display or editing by the user. + /// + public abstract class PropertyEditor : IRecord + { + //public static readonly Dictionary<> + static PropertyEditor() + { + + } + + protected readonly IInspector Owner; + public readonly Record Record; + //TODO cache attributes ? + + public Widget Widget { get; protected set; } + + protected PropertyEditor(IInspector owner, Record record) + { + Owner = owner; + Record = record; + if (TryCreateWidget(out Widget editor)) + { + Widget = editor; + } + else + { + throw new Exception(); + } + } + + protected bool TryCreateWidget(out Widget widget) + { + var atts = Record.FindAttributes(); + return TryCreateEditorWidget(Record, out widget, atts); + } + protected abstract bool TryCreateEditorWidget(Record record, out Widget widget, params Attribute[] attributes); + + Type IRecord.Type => Record.Type; + object IRecord.GetValue(object field) => Record.GetValue(field); + void IRecord.SetValue(object field, object value) => Record.SetValue(field, value); + } + + /// + public abstract class PropertyEditor : PropertyEditor, IRecord + { + protected delegate bool WidgetCreatorDelegate(Record record, out Widget widget); + protected abstract bool CreatorPicker(in Attribute[] attributes, out WidgetCreatorDelegate creatorDelegate); + + protected override bool TryCreateEditorWidget(Record record, out Widget widget, params Attribute[] attributes) + { + if (CreatorPicker(attributes, out var func)) + return func.Invoke(record, out widget); + widget = null; + return false; + } + + protected PropertyEditor(IInspector owner, Record record) : base(owner, record) + { + + } + + /// + /// Gets the value from the field + /// + public virtual T GetValue(object field) + { + object o = Record.GetValue(field); + if (o is T ot) + return ot; + return default; + } + /// + /// Tries to set the value to to the field + /// + public virtual void SetValue(object field, T value) + { + Record.SetValue(field, value); + } + } + + public sealed class BooleanEditor : PropertyEditor + { + public BooleanEditor(IInspector owner, Record record) : base(owner, record) + { + + } + + protected override bool CreatorPicker(in Attribute[] attributes, out WidgetCreatorDelegate creatorDelegate) + { + creatorDelegate = CreateCheckBox; + return true; + } + + private bool CreateCheckBox(Record record, out Widget widget) + { + var propertyType = record.Type; + bool value = GetValue(Owner.SelectedField); + + var cb = new CheckButton + { + IsChecked = value + }; + + if (Record.HasSetter) + { + cb.Click += (sender, args) => + { + SetValue(Owner.SelectedField, cb.IsChecked); + Owner.FireChanged(propertyType.Name); + }; + } + else + { + cb.Enabled = false; + } + + widget = cb; + return true; + } + } +} \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/PropertyGrid.SubGrid.cs b/src/Myra/Graphics2D/UI/Properties/PropertyGrid.SubGrid.cs new file mode 100644 index 00000000..39543ef8 --- /dev/null +++ b/src/Myra/Graphics2D/UI/Properties/PropertyGrid.SubGrid.cs @@ -0,0 +1,130 @@ +using System.ComponentModel; +using System.Xml.Serialization; +using Microsoft.Xna.Framework; +using Myra.Attributes; + +namespace Myra.Graphics2D.UI.Properties +{ + public partial class PropertyGrid + { + private class SubGrid : Widget + { + private readonly GridLayout _layout = new GridLayout(); + + private readonly ToggleButton _mark; + private readonly PropertyGrid _propertyGrid; + + public ToggleButton Mark => _mark; + public PropertyGrid PropertyGrid => _propertyGrid; + + public Rectangle HeaderBounds => new Rectangle(0, 0, ActualBounds.Width, _layout.GetRowHeight(0)); + + [Browsable(false)] + [XmlIgnore] + public bool IsEmpty => _propertyGrid.IsEmpty; + + public SubGrid(PropertyGrid parent, object value, string header, string category, string filter, Record parentProperty) + { + ChildrenLayout = _layout; + + _layout.ColumnSpacing = 4; + _layout.RowSpacing = 4; + + _layout.ColumnsProportions.Add(new Proportion(ProportionType.Auto)); + _layout.ColumnsProportions.Add(new Proportion(ProportionType.Fill)); + _layout.RowsProportions.Add(new Proportion(ProportionType.Auto)); + _layout.RowsProportions.Add(new Proportion(ProportionType.Auto)); + + _propertyGrid = new PropertyGrid(parent.PropertyGridStyle, category, parentProperty, parent) + { + Object = value, + Filter = filter, + HorizontalAlignment = HorizontalAlignment.Stretch, + }; + Grid.SetColumn(_propertyGrid, 1); + Grid.SetRow(_propertyGrid, 1); + + // Mark + var markImage = new Image(); + var imageStyle = parent.PropertyGridStyle.MarkStyle.ImageStyle; + if (imageStyle != null) + { + markImage.ApplyPressableImageStyle(imageStyle); + } + + _mark = new ToggleButton(null) + { + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Content = markImage + }; + + Children.Add(_mark); + + _mark.PressedChanged += (sender, args) => + { + if (_mark.IsPressed) + { + Children.Add(_propertyGrid); + parent._expandedCategories.Add(category); + } + else + { + Children.Remove(_propertyGrid); + parent._expandedCategories.Remove(category); + } + }; + + var expanded = true; + if (parentProperty != null && parentProperty.FindAttribute() != null) + { + expanded = false; + } + + if (expanded) + { + _mark.IsPressed = true; + } + + var label = new Label(null) + { + Text = header, + }; + Grid.SetColumn(label, 1); + label.ApplyLabelStyle(parent.PropertyGridStyle.LabelStyle); + + Children.Add(label); + + HorizontalAlignment = HorizontalAlignment.Stretch; + VerticalAlignment = VerticalAlignment.Stretch; + } + + public override void OnTouchDoubleClick() + { + base.OnTouchDoubleClick(); + + var mousePosition = ToLocal(Desktop.MousePosition); + if (!HeaderBounds.Contains(mousePosition) || _mark.Bounds.Contains(mousePosition)) + { + return; + } + + _mark.IsPressed = !_mark.IsPressed; + } + + public override void InternalRender(RenderContext context) + { + if (_propertyGrid.PropertyGridStyle.SelectionHoverBackground != null && IsMouseInside) + { + var headerBounds = HeaderBounds; + if (headerBounds.Contains(ToLocal(Desktop.MousePosition))) + { + _propertyGrid.PropertyGridStyle.SelectionHoverBackground.Draw(context, headerBounds); + } + } + + base.InternalRender(context); + } + } + } +} \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/PropertyGrid.cs b/src/Myra/Graphics2D/UI/Properties/PropertyGrid.cs index 270e9590..800183bc 100644 --- a/src/Myra/Graphics2D/UI/Properties/PropertyGrid.cs +++ b/src/Myra/Graphics2D/UI/Properties/PropertyGrid.cs @@ -31,151 +31,10 @@ namespace Myra.Graphics2D.UI.Properties { - public class PropertyGrid : Widget + public partial class PropertyGrid : Widget, IInspector { private const string DefaultCategoryName = "Miscellaneous"; - private class SubGrid : Widget - { - private readonly GridLayout _layout = new GridLayout(); - - private readonly ToggleButton _mark; - private readonly PropertyGrid _propertyGrid; - - public ToggleButton Mark - { - get { return _mark; } - } - - public PropertyGrid PropertyGrid - { - get { return _propertyGrid; } - } - - public Rectangle HeaderBounds - { - get - { - var headerBounds = new Rectangle(0, 0, ActualBounds.Width, _layout.GetRowHeight(0)); - - return headerBounds; - } - } - - [Browsable(false)] - [XmlIgnore] - public bool IsEmpty - { - get - { - return _propertyGrid.IsEmpty; - } - } - - public SubGrid(PropertyGrid parent, object value, string header, string category, string filter, Record parentProperty) - { - ChildrenLayout = _layout; - - _layout.ColumnSpacing = 4; - _layout.RowSpacing = 4; - - _layout.ColumnsProportions.Add(new Proportion(ProportionType.Auto)); - _layout.ColumnsProportions.Add(new Proportion(ProportionType.Fill)); - _layout.RowsProportions.Add(new Proportion(ProportionType.Auto)); - _layout.RowsProportions.Add(new Proportion(ProportionType.Auto)); - - _propertyGrid = new PropertyGrid(parent.PropertyGridStyle, category, parentProperty, parent) - { - Object = value, - Filter = filter, - HorizontalAlignment = HorizontalAlignment.Stretch, - }; - Grid.SetColumn(_propertyGrid, 1); - Grid.SetRow(_propertyGrid, 1); - - // Mark - var markImage = new Image(); - var imageStyle = parent.PropertyGridStyle.MarkStyle.ImageStyle; - if (imageStyle != null) - { - markImage.ApplyPressableImageStyle(imageStyle); - } - - _mark = new ToggleButton(null) - { - HorizontalAlignment = HorizontalAlignment.Left, - VerticalAlignment = VerticalAlignment.Center, - Content = markImage - }; - - Children.Add(_mark); - - _mark.PressedChanged += (sender, args) => - { - if (_mark.IsPressed) - { - Children.Add(_propertyGrid); - parent._expandedCategories.Add(category); - } - else - { - Children.Remove(_propertyGrid); - parent._expandedCategories.Remove(category); - } - }; - - var expanded = true; - if (parentProperty != null && parentProperty.FindAttribute() != null) - { - expanded = false; - } - - if (expanded) - { - _mark.IsPressed = true; - } - - var label = new Label(null) - { - Text = header, - }; - Grid.SetColumn(label, 1); - label.ApplyLabelStyle(parent.PropertyGridStyle.LabelStyle); - - Children.Add(label); - - HorizontalAlignment = HorizontalAlignment.Stretch; - VerticalAlignment = VerticalAlignment.Stretch; - } - - public override void OnTouchDoubleClick() - { - base.OnTouchDoubleClick(); - - var mousePosition = ToLocal(Desktop.MousePosition); - if (!HeaderBounds.Contains(mousePosition) || _mark.Bounds.Contains(mousePosition)) - { - return; - } - - _mark.IsPressed = !_mark.IsPressed; - } - - public override void InternalRender(RenderContext context) - { - if (_propertyGrid.PropertyGridStyle.SelectionHoverBackground != null && IsMouseInside) - { - var headerBounds = HeaderBounds; - if (headerBounds.Contains(ToLocal(Desktop.MousePosition))) - { - _propertyGrid.PropertyGridStyle.SelectionHoverBackground.Draw(context, headerBounds); - } - } - - base.InternalRender(context); - } - } - private readonly GridLayout _layout = new GridLayout(); private readonly PropertyGrid _parentGrid; private Record _parentProperty; @@ -341,6 +200,8 @@ public string Filter [XmlIgnore] public Func CustomWidgetProvider; + private object _selected; + public event EventHandler> PropertyChanged; public event EventHandler ObjectChanged; @@ -383,8 +244,13 @@ public PropertyGrid(string category) : this(Stylesheet.Current.TreeStyle, catego public PropertyGrid() : this(DefaultCategoryName) { } + + object IInspector.SelectedField + { + get => _selected; + } - private void FireChanged(string name) + void IInspector.FireChanged(string name) { var ev = PropertyChanged; @@ -409,9 +275,7 @@ private static void UpdateLabelCount(Label textBlock, int count) private void SetValue(Record record, object obj, object value) { if (CustomSetter != null && CustomSetter(record, obj, value)) - { return; - } record.SetValue(obj, value); } @@ -1462,8 +1326,7 @@ public void Rebuild() var subGrid = new SubGrid(this, Object, category.Key, category.Key, Filter, null); Grid.SetColumnSpan(subGrid, 2); - Grid.SetRow(subGrid, y); ; - + Grid.SetRow(subGrid, y); if (subGrid.IsEmpty) { diff --git a/src/Myra/Graphics2D/UI/Properties/PropertyRecord.cs b/src/Myra/Graphics2D/UI/Properties/PropertyRecord.cs index 74f618a8..7496b596 100644 --- a/src/Myra/Graphics2D/UI/Properties/PropertyRecord.cs +++ b/src/Myra/Graphics2D/UI/Properties/PropertyRecord.cs @@ -7,16 +7,8 @@ internal class PropertyRecord : Record { private readonly PropertyInfo _propertyInfo; - public override string Name - { - get { return _propertyInfo.Name; } - } - - public override Type Type - { - get { return _propertyInfo.PropertyType; } - } - + public override string Name => _propertyInfo.Name; + public override Type Type => _propertyInfo.PropertyType; public override MemberInfo MemberInfo => _propertyInfo; public PropertyRecord(PropertyInfo propertyInfo) @@ -24,14 +16,14 @@ public PropertyRecord(PropertyInfo propertyInfo) _propertyInfo = propertyInfo; } - public override object GetValue(object obj) + public override object GetValue(object field) { - return _propertyInfo.GetValue(obj, new object[0]); + return _propertyInfo.GetValue(field, Array.Empty()); } - public override void SetValue(object obj, object value) + public override void SetValue(object field, object value) { - _propertyInfo.SetValue(obj, value); + _propertyInfo.SetValue(field, value); } } } diff --git a/src/Myra/Graphics2D/UI/Properties/Record.cs b/src/Myra/Graphics2D/UI/Properties/Record.cs index 26902711..54ef4a20 100644 --- a/src/Myra/Graphics2D/UI/Properties/Record.cs +++ b/src/Myra/Graphics2D/UI/Properties/Record.cs @@ -4,17 +4,19 @@ namespace Myra.Graphics2D.UI.Properties { - public abstract class Record + /// + /// Base for encapsulating reflective information using . + /// + public abstract class Record : IRecord { public bool HasSetter { get; set; } - + public string Category { get; set; } public abstract string Name { get; } public abstract Type Type { get; } - public string Category { get; set; } public abstract MemberInfo MemberInfo { get; } - public abstract object GetValue(object obj); - public abstract void SetValue(object obj, object value); + public abstract object GetValue(object field); + public abstract void SetValue(object field, object value); public T FindAttribute() where T : Attribute { @@ -26,7 +28,7 @@ public T FindAttribute() where T : Attribute return MemberInfo.FindAttribute(); } - public T[] FindAttributes() where T: Attribute + public T[] FindAttributes() where T : Attribute { if (MemberInfo == null) { diff --git a/src/Myra/Myra.MonoGame.csproj b/src/Myra/Myra.MonoGame.csproj index d9fe5ea9..c11e3910 100644 --- a/src/Myra/Myra.MonoGame.csproj +++ b/src/Myra/Myra.MonoGame.csproj @@ -23,6 +23,18 @@ FileDialog.cs + + PropertyGrid.cs + + + Record.cs + + + Record.cs + + + Record.cs + From b2138fcb6caf2845675ced7a01e4574f43051d2c Mon Sep 17 00:00:00 2001 From: Bamboy Date: Sun, 8 Feb 2026 11:38:05 -0800 Subject: [PATCH 02/28] Widespread refactors in PropertyGrid to enable anonymous C# property inspection. --- .../UI/Properties/BooleanPropertyEditor.cs | 50 +++++ .../UI/Properties/BrushPropertyEditor.cs | 106 +++++++++ .../UI/Properties/CollectionPropertyEditor.cs | 84 ++++++++ .../UI/Properties/ColorPropertyEditor.cs | 112 ++++++++++ .../UI/Properties/EditorTypeRegistry.cs | 42 ++++ src/Myra/Graphics2D/UI/Properties/Editors.cs | 107 ++++++++++ .../UI/Properties/EnumPropertyEditor.cs | 81 +++++++ .../Graphics2D/UI/Properties/IInspector.cs | 15 +- .../UI/Properties/NumericPropertyEditor.cs | 115 ++++++++++ .../UI/Properties/PropertyEditor.cs | 118 ++++------ .../Graphics2D/UI/Properties/PropertyGrid.cs | 33 +-- .../UI/Properties/PropertyGridSettings.cs | 1 + .../UI/Properties/StringPropertyEditor.cs | 202 ++++++++++++++++++ src/Myra/Myra.MonoGame.csproj | 21 ++ 14 files changed, 996 insertions(+), 91 deletions(-) create mode 100644 src/Myra/Graphics2D/UI/Properties/BooleanPropertyEditor.cs create mode 100644 src/Myra/Graphics2D/UI/Properties/BrushPropertyEditor.cs create mode 100644 src/Myra/Graphics2D/UI/Properties/CollectionPropertyEditor.cs create mode 100644 src/Myra/Graphics2D/UI/Properties/ColorPropertyEditor.cs create mode 100644 src/Myra/Graphics2D/UI/Properties/EditorTypeRegistry.cs create mode 100644 src/Myra/Graphics2D/UI/Properties/Editors.cs create mode 100644 src/Myra/Graphics2D/UI/Properties/EnumPropertyEditor.cs create mode 100644 src/Myra/Graphics2D/UI/Properties/NumericPropertyEditor.cs create mode 100644 src/Myra/Graphics2D/UI/Properties/StringPropertyEditor.cs diff --git a/src/Myra/Graphics2D/UI/Properties/BooleanPropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/BooleanPropertyEditor.cs new file mode 100644 index 00000000..2f555cc5 --- /dev/null +++ b/src/Myra/Graphics2D/UI/Properties/BooleanPropertyEditor.cs @@ -0,0 +1,50 @@ +namespace Myra.Graphics2D.UI.Properties +{ + [PropertyEditor(typeof(BooleanPropertyEditor), typeof(bool))] + public sealed class BooleanPropertyEditor : PropertyEditor + { + public BooleanPropertyEditor(IInspector owner, Record methodInfo) : base(owner, methodInfo) + { + + } + + protected override bool CreatorPicker(out WidgetCreatorDelegate creatorDelegate) + { + creatorDelegate = CreateCheckBox; + return true; + } + + private bool CreateCheckBox(out Widget widget) + { + if (_owner.SelectedField == null) + { + widget = null; + return false; + } + + var propertyType = _record.Type; + bool value = GetValue(_owner.SelectedField); + + var cb = new CheckButton + { + IsChecked = value + }; + + if (_record.HasSetter) + { + cb.Click += (sender, args) => + { + SetValue(_owner.SelectedField, cb.IsChecked); + _owner.FireChanged(propertyType.Name); + }; + } + else + { + cb.Enabled = false; + } + + widget = cb; + return true; + } + } +} \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/BrushPropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/BrushPropertyEditor.cs new file mode 100644 index 00000000..327a2979 --- /dev/null +++ b/src/Myra/Graphics2D/UI/Properties/BrushPropertyEditor.cs @@ -0,0 +1,106 @@ +using Microsoft.Xna.Framework; +using FontStashSharp.RichText; +using Myra.Graphics2D.Brushes; +using Myra.Graphics2D.UI.ColorPicker; +using Myra.Graphics2D.UI.Styles; +using Myra.MML; + +namespace Myra.Graphics2D.UI.Properties +{ + [PropertyEditor(typeof(BrushPropertyEditor), typeof(IBrush))] + public class BrushPropertyEditor : PropertyEditor + { + public BrushPropertyEditor(IInspector owner, Record methodInfo) : base(owner, methodInfo) + { + + } + + protected override bool CreatorPicker(out WidgetCreatorDelegate creatorDelegate) + { + creatorDelegate = CreateBrushEditor; + return true; + } + + private bool CreateBrushEditor(out Widget widget) + { + var value = GetValue(_owner.SelectedField) as SolidBrush; + + var subGrid = new Grid + { + ColumnSpacing = 8, + HorizontalAlignment = HorizontalAlignment.Stretch + }; + + subGrid.ColumnsProportions.Add(new Proportion()); + subGrid.ColumnsProportions.Add(new Proportion(ProportionType.Fill)); + + var color = Color.Transparent; + if (value != null) + { + color = value.Color; + } + + var image = new Image + { + Renderable = Stylesheet.Current.WhiteRegion, + VerticalAlignment = VerticalAlignment.Center, + Width = 32, + Height = 16, + Color = color + }; + + subGrid.Widgets.Add(image); + + var button = new Button + { + Tag = value, + HorizontalAlignment = HorizontalAlignment.Stretch, + Content = new Label + { + Text = "Change...", + HorizontalAlignment = HorizontalAlignment.Center, + } + }; + Grid.SetColumn(button, 1); + + subGrid.Widgets.Add(button); + + if (_record.HasSetter) + { + button.Click += (sender, args) => + { + var dlg = new ColorPickerDialog() + { + Color = image.Color + }; + + dlg.Closed += (s, a) => + { + if (!dlg.Result) + { + return; + } + + image.Color = dlg.Color; + SetValue(_owner.SelectedField, new SolidBrush(dlg.Color)); + var baseObject = _owner.SelectedField as BaseObject; + if (baseObject != null) + { + baseObject.Resources[_record.Name] = dlg.Color.ToHexString(); + } + _owner.FireChanged(_record.Type.Name); + }; + + dlg.ShowModal(_owner.Desktop); + }; + } + else + { + button.Enabled = false; + } + + widget = subGrid; + return true; + } + } +} \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/CollectionPropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/CollectionPropertyEditor.cs new file mode 100644 index 00000000..0670aaa6 --- /dev/null +++ b/src/Myra/Graphics2D/UI/Properties/CollectionPropertyEditor.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Myra.Graphics2D.UI.Properties +{ + [PropertyEditor(typeof(CollectionPropertyEditor), typeof(IList))] + public class CollectionPropertyEditor : PropertyEditor + { + private readonly Type collectionKind; + + public CollectionPropertyEditor(IInspector owner, Record methodInfo) : base(owner, methodInfo) + { + //if(methodInfo.Type.isg) + collectionKind = methodInfo.Type.GenericTypeArguments[0]; + } + + protected override bool CreatorPicker(out WidgetCreatorDelegate creatorDelegate) + { + creatorDelegate = CreateCollectionPropEditor; + return true; + } + + private bool CreateCollectionPropEditor(out Widget widget) + { + object value = _record.GetValue(_owner.SelectedField); + IList items = (IList)value; + + var subGrid = new Grid + { + ColumnSpacing = 8, + HorizontalAlignment = HorizontalAlignment.Stretch + }; + + subGrid.ColumnsProportions.Add(new Proportion()); + subGrid.ColumnsProportions.Add(new Proportion(ProportionType.Fill)); + + var label = new Label + { + VerticalAlignment = VerticalAlignment.Center, + }; + UpdateLabelCount(label, items.Count); + + subGrid.Widgets.Add(label); + + var button = new Button + { + Tag = value, + HorizontalAlignment = HorizontalAlignment.Stretch, + Content = new Label + { + Text = "Change...", + HorizontalAlignment = HorizontalAlignment.Center, + } + }; + Grid.SetColumn(button, 1); + + button.Click += (sender, args) => + { + var collectionEditor = new CollectionEditor(items, collectionKind); + + var dialog = Dialog.CreateMessageBox("Edit", collectionEditor); + + dialog.ButtonOk.Click += (o, eventArgs) => + { + collectionEditor.SaveChanges(); + UpdateLabelCount(label, items.Count); + }; + + dialog.ShowModal(_owner.Desktop); + }; + + subGrid.Widgets.Add(button); + + widget = subGrid; + return true; + } + + private static void UpdateLabelCount(Label textBlock, int count) + { + textBlock.Text = string.Format("{0} Items", count); + } + } +} \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/ColorPropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/ColorPropertyEditor.cs new file mode 100644 index 00000000..bcecd8cb --- /dev/null +++ b/src/Myra/Graphics2D/UI/Properties/ColorPropertyEditor.cs @@ -0,0 +1,112 @@ +using Microsoft.Xna.Framework; +using Myra.Graphics2D.UI.ColorPicker; +using Myra.Graphics2D.UI.Styles; + +namespace Myra.Graphics2D.UI.Properties +{ + [PropertyEditor(typeof(ColorPropertyEditor), typeof(Color), typeof(Color?))] + public sealed class ColorPropertyEditor : PropertyEditor + { + public ColorPropertyEditor(IInspector owner, Record methodInfo) : base(owner, methodInfo) + { + + } + + protected override bool CreatorPicker(out WidgetCreatorDelegate creatorDelegate) + { + creatorDelegate = CreateEditor; + return true; + } + + private bool CreateEditor(out Widget widget) + { + if (_owner.SelectedField == null) + { + widget = null; + return false; + } + + var propertyType = _record.Type; + var value = _record.GetValue(_owner.SelectedField); + + var subGrid = new Grid + { + ColumnSpacing = 8, + HorizontalAlignment = HorizontalAlignment.Stretch + }; + + var isColor = propertyType == typeof(Color); + + subGrid.ColumnsProportions.Add(new Proportion()); + subGrid.ColumnsProportions.Add(new Proportion(ProportionType.Fill)); + + var color = Color.Transparent; + if (isColor) + { + color = (Color)value; + } + else if (value != null) + { + color = ((Color?)value).Value; + } + + var image = new Image + { + Renderable = Stylesheet.Current.WhiteRegion, + VerticalAlignment = VerticalAlignment.Center, + Width = 32, + Height = 16, + Color = color + }; + + subGrid.Widgets.Add(image); + + var button = new Button + { + Tag = value, + HorizontalAlignment = HorizontalAlignment.Stretch, + Content = new Label + { + HorizontalAlignment = HorizontalAlignment.Center, + Text = "Change..." + } + }; + Grid.SetColumn(button, 1); + + subGrid.Widgets.Add(button); + widget = subGrid; + + if (_record.HasSetter) + { + button.Click += (sender, args) => + { + var dlg = new ColorPickerDialog() + { + Color = image.Color + }; + + dlg.Closed += (s, a) => + { + if (!dlg.Result) + { + return; + } + + image.Color = dlg.Color; + SetValue(_owner.SelectedField, dlg.Color); + + _owner.FireChanged(propertyType.Name); + }; + + dlg.ShowModal(_owner.Desktop); + }; + } + else + { + button.Enabled = false; + } + + return true; + } + } +} \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/EditorTypeRegistry.cs b/src/Myra/Graphics2D/UI/Properties/EditorTypeRegistry.cs new file mode 100644 index 00000000..08343c39 --- /dev/null +++ b/src/Myra/Graphics2D/UI/Properties/EditorTypeRegistry.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Myra.Utility; + +namespace Myra.Graphics2D.UI.Properties +{ + public sealed class EditorTypeRegistry + { + public readonly Type EditorType; + public readonly string[] PropertyTypes; + + public EditorTypeRegistry(Type editorType, params Type[] propertyTypes) + { + EditorType = editorType; + PropertyTypes = TypeToString(propertyTypes); + } + + public bool CanEditType(string value) + { + foreach (string other in PropertyTypes) + { + if(StringComparer.InvariantCultureIgnoreCase.Equals(other, value)) + return true; + } + return false; + } + + private static string TypeToString(Type value) => value.GetFriendlyName(); + private static string[] TypeToString(params Type[] args) + { + if (args == null || args.Length == 0) + return null; + HashSet result = new HashSet(); + for (int i = 0; i < args.Length; i++) + { + result.Add(TypeToString(args[i])); + } + return result.ToArray(); + } + } +} \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/Editors.cs b/src/Myra/Graphics2D/UI/Properties/Editors.cs new file mode 100644 index 00000000..37afec74 --- /dev/null +++ b/src/Myra/Graphics2D/UI/Properties/Editors.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; +using Myra.Utility; + +namespace Myra.Graphics2D.UI.Properties +{ + internal static class Editors + { + private static readonly Type[] ActivatorTypeArgs = { typeof(IInspector), typeof(Record) }; + private static ReadOnlyCollection EditorRegistry; + private static bool _init; + public static PropertyEditor Create(IInspector inspector, Record methodInfo) + { + if (TryGetEditorTypeForType(methodInfo.Type, out Type editorType)) + { + var ctor = editorType.GetConstructor(ActivatorTypeArgs); + if (ctor != null) + { + try + { + //This also creates the widget + object obj = Activator.CreateInstance(editorType, inspector, methodInfo); + return obj as PropertyEditor; + } + catch (Exception e) + { + throw e; + } + } + } + else + { + Console.WriteLine("Could not find property editor for type: "+methodInfo.Type); + } + return null; + } + + internal static bool TryGetEditorTypeForType(Type propertyKind, out Type editorType) + { + if (!_init) + InitializeRegistry(); + + string nice = propertyKind.GetFriendlyName(); + for (int i = 0; i < EditorRegistry.Count; i++) + { + if (EditorRegistry[i].CanEditType(nice)) + { + editorType = EditorRegistry[i].EditorType; + return true; + } + } + editorType = null; + return false; + } + + public static void InitializeRegistry(params Assembly[] fromAssemblies) + { + List scanAsm; + if (fromAssemblies == null || fromAssemblies.Length <= 0) + scanAsm = new List { typeof(Editors).Assembly, Assembly.GetEntryAssembly() }; + else + { + scanAsm = new List(fromAssemblies); + scanAsm.Add(typeof(Editors).Assembly); + scanAsm.Add(Assembly.GetEntryAssembly()); + } + + Reflective_LoadTypeRegistry( + 16, + scanAsm, + out List registry + ); + + EditorRegistry = registry.AsReadOnly(); + _init = true; + } + + /// + /// Creates all concrete classes that inherit the type, from the specified assemblies. Only creates types with no-argument constructors. + /// + /// The base abstract type + /// The assemblies to scan for inheritors + private static void Reflective_LoadTypeRegistry(int predictedCount, IEnumerable assemblies, out List registry) where T : class + { + registry = new List(predictedCount); + foreach (Assembly asm in assemblies) + { + // LoadAttributesFromConcreteSubTypes(asm, results); + foreach (Type type in asm.GetTypes() + .Where(myType => myType.IsClass && !myType.IsAbstract && myType.IsSubclassOf(typeof(T)))) + { + PropertyEditorAttribute att = type.FindAttribute(); + if (att == null) + continue; + EditorTypeRegistry reg = att.GetRegistry(); + if(reg == null) + continue; + registry.Add(reg); + } + } + } + + } +} \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/EnumPropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/EnumPropertyEditor.cs new file mode 100644 index 00000000..eb5c0350 --- /dev/null +++ b/src/Myra/Graphics2D/UI/Properties/EnumPropertyEditor.cs @@ -0,0 +1,81 @@ +using System; +using Myra.Utility; + +namespace Myra.Graphics2D.UI.Properties +{ + [PropertyEditor(typeof(EnumPropertyEditor), typeof(Enum))] + public sealed class EnumPropertyEditor : PropertyEditor + { + public EnumPropertyEditor(IInspector owner, Record methodInfo) : base(owner, methodInfo) + { + + } + + protected override bool CreatorPicker(out WidgetCreatorDelegate creatorDelegate) + { + //TODO - support flags enums + creatorDelegate = CreateComboView; + return true; + } + + private bool CreateComboView(out Widget widget) + { + if (_owner.SelectedField == null) + { + widget = null; + return false; + } + + var propertyType = _record.Type; + var value = _record.GetValue(_owner.SelectedField); + + var isNullable = propertyType.IsNullableEnum(); + var enumType = isNullable ? propertyType.GetNullableType() : propertyType; + var values = Enum.GetValues(enumType); + + var cv = new ComboView(); + + if (isNullable) + { + cv.Widgets.Add(new Label + { + Text = string.Empty + }); + } + + foreach (var v in values) + { + cv.Widgets.Add(new Label + { + Text = v.ToString(), + Tag = v + }); + } + + var selectedIndex = Array.IndexOf(values, value); + if (isNullable) + { + ++selectedIndex; + } + cv.SelectedIndex = selectedIndex; + + if (_record.HasSetter) + { + cv.SelectedIndexChanged += (sender, args) => + { + if (cv.SelectedIndex != -1) + { + SetValue(_owner.SelectedField, cv.SelectedItem.Tag); + } + }; + } + else + { + cv.Enabled = false; + } + + widget = cv; + return true; + } + } +} \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/IInspector.cs b/src/Myra/Graphics2D/UI/Properties/IInspector.cs index f0c12460..f0bdfb82 100644 --- a/src/Myra/Graphics2D/UI/Properties/IInspector.cs +++ b/src/Myra/Graphics2D/UI/Properties/IInspector.cs @@ -1,9 +1,22 @@ +using AssetManagementBase; + namespace Myra.Graphics2D.UI.Properties { public interface IInspector { - //object? SelectionRoot { get; set; } + Desktop Desktop { get; } + + /// + /// The selected field object. + /// object SelectedField { get; } + + /// + /// Base file path content is chosen from + /// + string BasePath { get; } + AssetManager AssetManager { get; } + void FireChanged(string propertyName); } } \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/NumericPropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/NumericPropertyEditor.cs new file mode 100644 index 00000000..875ba5ef --- /dev/null +++ b/src/Myra/Graphics2D/UI/Properties/NumericPropertyEditor.cs @@ -0,0 +1,115 @@ +using System; +using Myra.Utility; + +namespace Myra.Graphics2D.UI.Properties +{ + [PropertyEditor(typeof(NumericPropertyEditor), + typeof(byte), typeof(sbyte), typeof(byte?), typeof(sbyte?), + typeof(short), typeof(ushort), typeof(short?), typeof(ushort?), + typeof(int), typeof(uint), typeof(int?), typeof(uint?), + typeof(long), typeof(ulong), typeof(long?), typeof(ulong?), + typeof(float), typeof(float?), typeof(double), typeof(double?))] + public sealed class NumericPropertyEditor : PropertyEditor + { + public NumericPropertyEditor(IInspector owner, Record methodInfo) : base(owner, methodInfo) + { + + } + + protected override bool TryCreateEditorWidget(out Widget widget) + { + return CreateNumericEditor(_record, out widget); + //if (CreatorPicker(attributes, out var func)) + // return func.Invoke(methodInfo, out widget); + widget = null; + return false; + } + + private bool CreateNumericEditor(Record record, out Widget widget) + { + if (_owner.SelectedField == null) + { + widget = null; + return false; + } + + var propertyType = record.Type; + object value = record.GetValue(_owner.SelectedField); + + var numericType = propertyType; + if (propertyType.IsNullablePrimitive()) + { + numericType = propertyType.GetNullableType(); + } + + var spinButton = new SpinButton + { + Integer = numericType.IsNumericInteger(), + Nullable = propertyType.IsNullablePrimitive(), + Value = value != null ? (float)Convert.ChangeType(value, typeof(float)) : default(float?) + }; + + if (_record.HasSetter) + { + spinButton.ValueChanged += (sender, args) => + { + try + { + object result; + + if (spinButton.Value != null) + { + result = Convert.ChangeType(spinButton.Value.Value, numericType); + } + else + { + result = null; + } + + SetValue(_owner.SelectedField, result); + + if (record.Type.IsValueType) + { + // Handle structs + /* + var tg = this; + var pg = tg._parentGrid; + while (pg != null && tg._parentProperty != null && tg._parentProperty.Type.IsValueType) + { + tg._parentProperty.SetValue(pg._object, tg._object); + + if (!tg._parentProperty.Type.IsValueType) + { + break; + } + + tg = pg; + pg = tg._parentGrid; + }*/ + } + + _owner.FireChanged(record.Name); + } + catch (InvalidCastException) + { + // TODO: Rework this ugly type conversion solution + } + catch (Exception ex) + { + spinButton.Value = args.OldValue; + var dialog = Dialog.CreateMessageBox("Error", ex.ToString()); + dialog.ShowModal(_owner.Desktop); + } + }; + } + else + { + spinButton.Enabled = false; + } + + widget = spinButton; + return true; + } + + } +} \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs index 2d49dd8c..497a7315 100644 --- a/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs +++ b/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs @@ -1,67 +1,73 @@ using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; namespace Myra.Graphics2D.UI.Properties { /// - /// Encapsulates a widget and .Net property or field, for the purposes of display or editing by the user. + /// Attribute that ties a concrete to one or more property types. /// - public abstract class PropertyEditor : IRecord + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class PropertyEditorAttribute : Attribute { - //public static readonly Dictionary<> - static PropertyEditor() + private readonly Type attached; + private readonly Type[] editTypes; + + public PropertyEditorAttribute(Type attached, params Type[] editTypes) { - + this.attached = attached; + this.editTypes = editTypes; } + public EditorTypeRegistry GetRegistry() => new EditorTypeRegistry(attached, editTypes); + } + + /// + /// Encapsulates a widget and .Net property or field, for the purposes of display or editing by the user. + /// + public abstract class PropertyEditor : IRecord + { + protected delegate bool WidgetCreatorDelegate(out Widget widget); - protected readonly IInspector Owner; - public readonly Record Record; - //TODO cache attributes ? + protected readonly IInspector _owner; + protected readonly Record _record; public Widget Widget { get; protected set; } - protected PropertyEditor(IInspector owner, Record record) + /// + /// Creates a new widget attached to the given Record + /// + protected PropertyEditor(IInspector owner, Record methodInfo) { - Owner = owner; - Record = record; + _owner = owner; + _record = methodInfo; if (TryCreateWidget(out Widget editor)) - { Widget = editor; - } - else - { - throw new Exception(); - } } - protected bool TryCreateWidget(out Widget widget) + private bool TryCreateWidget(out Widget widget) => TryCreateEditorWidget(out widget); + protected abstract bool TryCreateEditorWidget(out Widget widget); + + public Type Type => _record.Type; + object IRecord.GetValue(object field) => _record.GetValue(field); + public void SetValue(object field, object value) { - var atts = Record.FindAttributes(); - return TryCreateEditorWidget(Record, out widget, atts); + _record.SetValue(field, value); + _owner.FireChanged(_record.Name); } - protected abstract bool TryCreateEditorWidget(Record record, out Widget widget, params Attribute[] attributes); - - Type IRecord.Type => Record.Type; - object IRecord.GetValue(object field) => Record.GetValue(field); - void IRecord.SetValue(object field, object value) => Record.SetValue(field, value); } /// public abstract class PropertyEditor : PropertyEditor, IRecord { - protected delegate bool WidgetCreatorDelegate(Record record, out Widget widget); - protected abstract bool CreatorPicker(in Attribute[] attributes, out WidgetCreatorDelegate creatorDelegate); + protected abstract bool CreatorPicker(out WidgetCreatorDelegate creatorDelegate); - protected override bool TryCreateEditorWidget(Record record, out Widget widget, params Attribute[] attributes) + protected override bool TryCreateEditorWidget(out Widget widget) { - if (CreatorPicker(attributes, out var func)) - return func.Invoke(record, out widget); + if (CreatorPicker(out var func)) + return func.Invoke(out widget); widget = null; return false; } - protected PropertyEditor(IInspector owner, Record record) : base(owner, record) + protected PropertyEditor(IInspector owner, Record methodInfo) : base(owner, methodInfo) { } @@ -71,7 +77,7 @@ protected PropertyEditor(IInspector owner, Record record) : base(owner, record) /// public virtual T GetValue(object field) { - object o = Record.GetValue(field); + object o = _record.GetValue(field); if (o is T ot) return ot; return default; @@ -81,48 +87,8 @@ public virtual T GetValue(object field) /// public virtual void SetValue(object field, T value) { - Record.SetValue(field, value); - } - } - - public sealed class BooleanEditor : PropertyEditor - { - public BooleanEditor(IInspector owner, Record record) : base(owner, record) - { - - } - - protected override bool CreatorPicker(in Attribute[] attributes, out WidgetCreatorDelegate creatorDelegate) - { - creatorDelegate = CreateCheckBox; - return true; - } - - private bool CreateCheckBox(Record record, out Widget widget) - { - var propertyType = record.Type; - bool value = GetValue(Owner.SelectedField); - - var cb = new CheckButton - { - IsChecked = value - }; - - if (Record.HasSetter) - { - cb.Click += (sender, args) => - { - SetValue(Owner.SelectedField, cb.IsChecked); - Owner.FireChanged(propertyType.Name); - }; - } - else - { - cb.Enabled = false; - } - - widget = cb; - return true; + _record.SetValue(field, value); + _owner.FireChanged(_record.Name); } } } \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/PropertyGrid.cs b/src/Myra/Graphics2D/UI/Properties/PropertyGrid.cs index 800183bc..2ee4be89 100644 --- a/src/Myra/Graphics2D/UI/Properties/PropertyGrid.cs +++ b/src/Myra/Graphics2D/UI/Properties/PropertyGrid.cs @@ -245,10 +245,11 @@ public PropertyGrid() : this(DefaultCategoryName) { } - object IInspector.SelectedField - { - get => _selected; - } + object IInspector.SelectedField => _object; + /// + string IInspector.BasePath => Settings.BasePath; + + AssetManager IInspector.AssetManager => Settings.AssetManager; void IInspector.FireChanged(string name) { @@ -266,12 +267,7 @@ void IInspector.FireChanged(string name) ev(this, new GenericEventArgs(name)); } } - - private static void UpdateLabelCount(Label textBlock, int count) - { - textBlock.Text = string.Format("{0} Items", count); - } - + private void SetValue(Record record, object obj, object value) { if (CustomSetter != null && CustomSetter(record, obj, value)) @@ -279,7 +275,7 @@ private void SetValue(Record record, object obj, object value) record.SetValue(obj, value); } - +/* private ComboView CreateCustomValuesEditor(Record record, CustomValues customValues, bool hasSetter) { var propertyType = record.Type; @@ -990,7 +986,7 @@ private Widget CreateAttributeFileEditor(Record record, bool hasSetter, FilePath return result; } - +*/ private void FillSubGrid(ref int y, IReadOnlyList records) { for (var i = 0; i < records.Count; ++i) @@ -1014,6 +1010,14 @@ private void FillSubGrid(ref int y, IReadOnlyList records) CustomValues customValues = null; var needsSubGrid = false; + + var editor = Editors.Create(this, record); + if (editor != null) + { + valueWidget = editor.Widget; + } + + /* if ((valueWidget = CustomWidgetProvider?.Invoke(record, _object)) != null) { @@ -1110,8 +1114,9 @@ private void FillSubGrid(ref int y, IReadOnlyList records) needsSubGrid = true; } } - - if (valueWidget != null) + */ + + if (valueWidget != null) //Add single value display { var name = record.Name; var dn = record.FindAttribute(); diff --git a/src/Myra/Graphics2D/UI/Properties/PropertyGridSettings.cs b/src/Myra/Graphics2D/UI/Properties/PropertyGridSettings.cs index 2abe9111..195bd33d 100644 --- a/src/Myra/Graphics2D/UI/Properties/PropertyGridSettings.cs +++ b/src/Myra/Graphics2D/UI/Properties/PropertyGridSettings.cs @@ -5,6 +5,7 @@ namespace Myra.Graphics2D.UI.Properties { + [Obsolete("Properties moved to IInspector")] public class PropertyGridSettings { [Browsable(false)] diff --git a/src/Myra/Graphics2D/UI/Properties/StringPropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/StringPropertyEditor.cs new file mode 100644 index 00000000..dd0e6d08 --- /dev/null +++ b/src/Myra/Graphics2D/UI/Properties/StringPropertyEditor.cs @@ -0,0 +1,202 @@ +using System; +using Myra.Utility; + +namespace Myra.Graphics2D.UI.Properties +{ + [PropertyEditor(typeof(StringPropertyEditor), typeof(string))] + public sealed class StringPropertyEditor : PropertyEditor + { + public StringPropertyEditor(IInspector owner, Record methodInfo) : base(owner, methodInfo) + { + + } + + protected override bool CreatorPicker(out WidgetCreatorDelegate creatorDelegate) + { + creatorDelegate = CreateTextBox; + // CreateTextBoxAsFilePath + return true; + } + + private bool CreateTextBox(out Widget widget) + { + if (_owner.SelectedField == null) + { + widget = null; + return false; + } + + var propertyType = _record.Type; + var value = GetValue(_owner.SelectedField); + + var tf = new TextBox + { + Text = value != null ? value : string.Empty + }; + + if (_record.HasSetter) + { + tf.TextChanged += (sender, args) => + { + try + { + object result; + + if (propertyType.IsNullablePrimitive()) + { + if (string.IsNullOrEmpty(tf.Text)) + { + result = null; + } + else + { + result = Convert.ChangeType(tf.Text, _record.Type.GetNullableType()); + } + } + else + { + result = Convert.ChangeType(tf.Text, _record.Type); + } + + SetValue(_owner.SelectedField, result); + /* + if (record.Type.IsValueType) + { + var tg = this; + var pg = tg._parentGrid; + while (pg != null && tg._parentProperty != null) + { + tg._parentProperty.SetValue(pg._object, tg._object); + + if (!tg._parentProperty.Type.IsValueType) + { + break; + } + + tg = pg; + pg = tg._parentGrid; + } + }*/ + } + catch (Exception) + { + // TODO: Rework this ugly type conversion solution + } + }; + } + else + { + tf.Enabled = false; + } + + widget = tf; + return true; + } + + /* + private bool CreateTextBoxAsFilePath(Record record, out Widget widget) + { + var propertyType = record.Type; + var value = record.GetValue(Owner.SelectedField); + + var result = new HorizontalStackPanel + { + Spacing = 8 + }; + + TextBox path = null; + if (attribute.ShowPath) + { + path = new TextBox + { + Readonly = true, + HorizontalAlignment = HorizontalAlignment.Stretch + }; + + if (value != null) + { + path.Text = value.ToString(); + } + + StackPanel.SetProportionType(path, ProportionType.Fill); + result.Widgets.Add(path); + } + + var button = new Button + { + Tag = value, + HorizontalAlignment = HorizontalAlignment.Stretch, + Content = new Label + { + Text = "Change...", + HorizontalAlignment = HorizontalAlignment.Center, + } + }; + Grid.SetColumn(button, 1); + + if (Record.HasSetter) + { + button.Click += (sender, args) => + { + var dlg = new FileDialog(attribute.DialogMode) + { + Filter = attribute.Filter + }; + + if (value != null) + { + var filePath = value.ToString(); + if (!Path.IsPathRooted(filePath) && !string.IsNullOrEmpty(Owner.BasePath)) + { + filePath = Path.Combine(Owner.BasePath, filePath); + } + dlg.FilePath = filePath; + } + else if (!string.IsNullOrEmpty(Owner.BasePath)) + { + dlg.Folder = Owner.BasePath; + } + + dlg.Closed += (s, a) => + { + if (!dlg.Result) + { + return; + } + + try + { + var filePath = dlg.FilePath; + if (!string.IsNullOrEmpty(Owner.BasePath)) + { + filePath = PathUtils.TryToMakePathRelativeTo(filePath, Owner.BasePath); + } + + if (path != null) + { + path.Text = filePath; + } + + SetValue(record, _object, filePath); + + Owner.FireChanged(propertyType.Name); + } + catch (Exception) + { + } + }; + + dlg.ShowModal(Owner.Desktop); + }; + } + else + { + button.Enabled = false; + } + + result.Widgets.Add(button); + widget = result; + return true; + }*/ + } +} \ No newline at end of file diff --git a/src/Myra/Myra.MonoGame.csproj b/src/Myra/Myra.MonoGame.csproj index c11e3910..3ed0e895 100644 --- a/src/Myra/Myra.MonoGame.csproj +++ b/src/Myra/Myra.MonoGame.csproj @@ -35,6 +35,27 @@ Record.cs + + PropertyEditor.cs + + + PropertyEditor.cs + + + PropertyEditor.cs + + + PropertyEditor.cs + + + PropertyEditor.cs + + + PropertyEditor.cs + + + PropertyEditor.cs + From f25a60543853e4ec926819cb3ef2fc20aaca9d0e Mon Sep 17 00:00:00 2001 From: Bamboy Date: Sun, 8 Feb 2026 11:38:47 -0800 Subject: [PATCH 03/28] Inspector sample --- .../.config/dotnet-tools.json | 36 ++++ .../.vscode/launch.json | 14 ++ samples/Myra.Samples.Inspector/InspectGame.cs | 160 ++++++++++++++++++ .../Myra.Samples.Inspector.csproj | 16 ++ .../Myra.Samples.Inspector.sln | 16 ++ samples/Myra.Samples.Inspector/Program.cs | 2 + samples/Myra.Samples.Inspector/RootWidgets.cs | 49 ++++++ .../SomeTypesInAClass.cs | 27 +++ 8 files changed, 320 insertions(+) create mode 100644 samples/Myra.Samples.Inspector/.config/dotnet-tools.json create mode 100644 samples/Myra.Samples.Inspector/.vscode/launch.json create mode 100644 samples/Myra.Samples.Inspector/InspectGame.cs create mode 100644 samples/Myra.Samples.Inspector/Myra.Samples.Inspector.csproj create mode 100644 samples/Myra.Samples.Inspector/Myra.Samples.Inspector.sln create mode 100644 samples/Myra.Samples.Inspector/Program.cs create mode 100644 samples/Myra.Samples.Inspector/RootWidgets.cs create mode 100644 samples/Myra.Samples.Inspector/SomeTypesInAClass.cs diff --git a/samples/Myra.Samples.Inspector/.config/dotnet-tools.json b/samples/Myra.Samples.Inspector/.config/dotnet-tools.json new file mode 100644 index 00000000..afd4e2c4 --- /dev/null +++ b/samples/Myra.Samples.Inspector/.config/dotnet-tools.json @@ -0,0 +1,36 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-mgcb": { + "version": "3.8.3", + "commands": [ + "mgcb" + ] + }, + "dotnet-mgcb-editor": { + "version": "3.8.3", + "commands": [ + "mgcb-editor" + ] + }, + "dotnet-mgcb-editor-linux": { + "version": "3.8.3", + "commands": [ + "mgcb-editor-linux" + ] + }, + "dotnet-mgcb-editor-windows": { + "version": "3.8.3", + "commands": [ + "mgcb-editor-windows" + ] + }, + "dotnet-mgcb-editor-mac": { + "version": "3.8.3", + "commands": [ + "mgcb-editor-mac" + ] + } + } +} \ No newline at end of file diff --git a/samples/Myra.Samples.Inspector/.vscode/launch.json b/samples/Myra.Samples.Inspector/.vscode/launch.json new file mode 100644 index 00000000..99933d02 --- /dev/null +++ b/samples/Myra.Samples.Inspector/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "C#: Myra.Samples.Inspector Debug", + "type": "dotnet", + "request": "launch", + "projectPath": "${workspaceFolder}/Myra.Samples.Inspector.csproj" + } + ], + } \ No newline at end of file diff --git a/samples/Myra.Samples.Inspector/InspectGame.cs b/samples/Myra.Samples.Inspector/InspectGame.cs new file mode 100644 index 00000000..582ff376 --- /dev/null +++ b/samples/Myra.Samples.Inspector/InspectGame.cs @@ -0,0 +1,160 @@ +using Myra.Graphics2D.UI; +using System; +using System.Linq; +using Myra.Graphics2D.UI.Styles; + + +#if !STRIDE +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +#if ANDROID +#endif +#else +using System.Threading.Tasks; +using Stride.Engine; +using Stride.Games; +using Stride.Graphics; +using Stride.Core.Mathematics; +#endif + +namespace Myra.Samples.Inspector +{ + public class InspectGame : Game + { +#if !STRIDE + private readonly GraphicsDeviceManager _graphics; +#endif + + private RootWidgets widgets; + private Desktop _desktop; + + public static InspectGame Instance { get; private set; } + + public InspectGame() + { + Instance = this; + +#if !STRIDE + _graphics = new GraphicsDeviceManager(this) + { + PreferredBackBufferWidth = 1200, + PreferredBackBufferHeight = 800 + }; + Window.AllowUserResizing = true; +#else +#endif + + IsMouseVisible = true; + } + +#if STRIDE + protected override Task LoadContent() + { + MyraEnvironment.Game = this; + + _allWidgets = new Widgets(); + + _desktop = new Desktop(); + _desktop.Widgets.Add(_allWidgets); + + return base.LoadContent(); + } +#else + protected override void LoadContent() + { + base.LoadContent(); + + MyraEnvironment.Game = this; + MyraEnvironment.EnableModalDarkening = true; + +// Stylesheet.Current = DefaultAssets.DefaultStylesheet2X; + + widgets = new RootWidgets(); + + _desktop = new Desktop(); + _desktop.Root = widgets; + +#if MONOGAME && !ANDROID + // Inform Myra that external text input is available + // So it stops translating Keys to chars + _desktop.HasExternalTextInput = true; + + // Provide that text input + Window.TextInput += (s, a) => + { + _desktop.OnChar(a.Character); + }; +#endif + } +#endif + + private int selectedIndex = 0; + private bool lastDown, lastUp; + protected override void Update(GameTime gameTime) + { + base.Update(gameTime); + var state = Keyboard.GetState(); + + // prevent spam cycling by only triggering an input change up or down once + bool thisDown = state[Keys.Down] == KeyState.Down; + bool thisUp = state[Keys.Up] == KeyState.Down; + if (thisDown & !lastDown) + CycleSelection(false); + else if(thisUp & !lastUp) + CycleSelection(true); + + lastDown = thisDown; + lastUp = thisUp; + } + + private void CycleSelection(bool forward) + { + if (forward) + { + selectedIndex++; + if (selectedIndex >= widgets.inspectables.Count) + selectedIndex = 0; + } + else + { + selectedIndex--; + if (selectedIndex < 0) + selectedIndex = widgets.inspectables.Count - 1; + } + widgets.Inspect( widgets.inspectables[selectedIndex] ); + } + + private string TextDisplay + { + get + { + Type inspectedType = widgets.inspectables[selectedIndex].GetType(); + string baseType = string.Empty; + if (inspectedType.BaseType != null) + { + baseType = $" BaseType: {inspectedType.BaseType.Name}"; + } + + return $"\nInspecting object [{selectedIndex+1}] of [{widgets.inspectables.Count}]:\n\n Type: {inspectedType.Name}\n in: {inspectedType.Namespace}\n{baseType}\n\n Assembly:\n{inspectedType.Assembly.GetName().Name}\n\n\n\n\n\n\nIs mouse over GUI: {_desktop.IsMouseOverGUI}"; + } + } + + protected override void Draw(GameTime gameTime) + { + base.Draw(gameTime); + +#if !STRIDE + GraphicsDevice.Clear(Color.Black); +#else + // Clear screen + GraphicsContext.CommandList.Clear(GraphicsDevice.Presenter.BackBuffer, Color.Black); + GraphicsContext.CommandList.Clear(GraphicsDevice.Presenter.DepthStencilBuffer, DepthStencilClearOptions.DepthBuffer | DepthStencilClearOptions.Stencil); + + // Set render target + GraphicsContext.CommandList.SetRenderTargetAndViewport(GraphicsDevice.Presenter.DepthStencilBuffer, GraphicsDevice.Presenter.BackBuffer); +#endif + widgets.labelOverGui.Text = TextDisplay; + _desktop.Render(); + } + } +} \ No newline at end of file diff --git a/samples/Myra.Samples.Inspector/Myra.Samples.Inspector.csproj b/samples/Myra.Samples.Inspector/Myra.Samples.Inspector.csproj new file mode 100644 index 00000000..47abce29 --- /dev/null +++ b/samples/Myra.Samples.Inspector/Myra.Samples.Inspector.csproj @@ -0,0 +1,16 @@ + + + Exe + $(AppTargetFramework) + bin\MonoGame\$(Configuration) + 9 + + + + + + + + + + \ No newline at end of file diff --git a/samples/Myra.Samples.Inspector/Myra.Samples.Inspector.sln b/samples/Myra.Samples.Inspector/Myra.Samples.Inspector.sln new file mode 100644 index 00000000..d4b5d063 --- /dev/null +++ b/samples/Myra.Samples.Inspector/Myra.Samples.Inspector.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Myra.Samples.Inspector", "Myra.Samples.Inspector.csproj", "{916D1C19-828B-4324-926F-6409D096DBA8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {916D1C19-828B-4324-926F-6409D096DBA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {916D1C19-828B-4324-926F-6409D096DBA8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {916D1C19-828B-4324-926F-6409D096DBA8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {916D1C19-828B-4324-926F-6409D096DBA8}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/samples/Myra.Samples.Inspector/Program.cs b/samples/Myra.Samples.Inspector/Program.cs new file mode 100644 index 00000000..8cdf01ee --- /dev/null +++ b/samples/Myra.Samples.Inspector/Program.cs @@ -0,0 +1,2 @@ +using var game = new Myra.Samples.Inspector.InspectGame(); +game.Run(); \ No newline at end of file diff --git a/samples/Myra.Samples.Inspector/RootWidgets.cs b/samples/Myra.Samples.Inspector/RootWidgets.cs new file mode 100644 index 00000000..ed3e0f25 --- /dev/null +++ b/samples/Myra.Samples.Inspector/RootWidgets.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using Myra.Graphics2D; +using Myra.Graphics2D.UI; +using Myra.Graphics2D.UI.Properties; + +namespace Myra.Samples.Inspector +{ + public class RootWidgets : HorizontalStackPanel + { + public Label labelOverGui; + private PropertyGrid propertyGrid; + + public readonly List inspectables; + + public RootWidgets() + { + inspectables = BuildInspectables(); + BuildUI(); + Inspect(inspectables[0]); + } + + private List BuildInspectables() + { + return new List() + { + new SomeTypesInAClass(), + this, + InspectGame.Instance + }; + } + + private void BuildUI() + { + labelOverGui = new Label(); + labelOverGui.Width = 400; + labelOverGui.Padding = new Thickness(4); + this.Widgets.Add(labelOverGui); + + propertyGrid = new PropertyGrid(); + propertyGrid.Width = 500; + this.Widgets.Add(propertyGrid); + } + + public void Inspect(object obj) + { + propertyGrid.Object = obj; + } + } +} \ No newline at end of file diff --git a/samples/Myra.Samples.Inspector/SomeTypesInAClass.cs b/samples/Myra.Samples.Inspector/SomeTypesInAClass.cs new file mode 100644 index 00000000..821c5c3c --- /dev/null +++ b/samples/Myra.Samples.Inspector/SomeTypesInAClass.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework; + +namespace Myra.Samples.Inspector +{ + public enum MyEnum + { + One = 1, + Five = 5, + Twenty = 20 + } + + public class SomeTypesInAClass + { + public MyEnum enumValue = MyEnum.Five; + + public bool checkbox = true; + + public string stringValue = "Hotdog"; + + public Color colorValue = Color.Honeydew; + + public List stringCollection = new List(); + + public byte @byte = 250; + } +} \ No newline at end of file From 8a16ca134cc1eaa2896daef0c5b5c96b2b032032 Mon Sep 17 00:00:00 2001 From: Bamboy Date: Sun, 8 Feb 2026 17:03:33 -0800 Subject: [PATCH 04/28] Improve inspector sample for better testing --- samples/Myra.Samples.Inspector/Input.cs | 86 +++++++++++++ samples/Myra.Samples.Inspector/InspectGame.cs | 74 +++-------- samples/Myra.Samples.Inspector/RootWidgets.cs | 120 ++++++++++++++++-- .../Myra.Samples.Inspector/StringGenerator.cs | 34 +++++ 4 files changed, 246 insertions(+), 68 deletions(-) create mode 100644 samples/Myra.Samples.Inspector/Input.cs create mode 100644 samples/Myra.Samples.Inspector/StringGenerator.cs diff --git a/samples/Myra.Samples.Inspector/Input.cs b/samples/Myra.Samples.Inspector/Input.cs new file mode 100644 index 00000000..9a09f21c --- /dev/null +++ b/samples/Myra.Samples.Inspector/Input.cs @@ -0,0 +1,86 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace Myra.Samples.Inspector +{ + public class Input + { + public int SelectedIndex => selectedIndex; + public int TextInfoIndex => infoIndex; + + private int selectedIndex, infoIndex; + private bool lastDown, lastUp, lastLeft, lastRight; + + private readonly Func _maxSelectionsGetter; + private readonly Func _maxInfosGetter; + + public Action OnSelectionChanged; + public Action OnTextInfoChanged; + + public Input(Func maxSelectionsGetter, Func maxInfosGetter) + { + _maxSelectionsGetter = maxSelectionsGetter; + _maxInfosGetter = maxInfosGetter; + } + + public void Update(KeyboardState state) + { + // Prevent spam cycling by only triggering an input change once. + bool thisDown = state[Keys.Down] == KeyState.Down; + bool thisUp = state[Keys.Up] == KeyState.Down; + bool thisLeft = state[Keys.Left] == KeyState.Down; + bool thisRight = state[Keys.Right] == KeyState.Down; + + if (thisDown & !lastDown) + CycleSelection(false); + else if(thisUp & !lastUp) + CycleSelection(true); + + if (thisLeft & !lastLeft) + CycleInfo(false); + else if(thisRight & !lastRight) + CycleInfo(true); + + lastDown = thisDown; + lastUp = thisUp; + lastLeft = thisLeft; + lastRight = thisRight; + } + + private void CycleSelection(bool forward) + { + if (forward) + { + selectedIndex++; + if (selectedIndex >= _maxSelectionsGetter.Invoke()) + selectedIndex = 0; + } + else + { + selectedIndex--; + if (selectedIndex < 0) + selectedIndex = _maxSelectionsGetter.Invoke() - 1; + } + OnSelectionChanged?.Invoke(selectedIndex); //widgets.Inspect( widgets.inspectables[selectedIndex] ); + } + + private void CycleInfo(bool forward) + { + if (forward) + { + infoIndex++; + if (infoIndex >= _maxInfosGetter.Invoke()) + infoIndex = 0; + } + else + { + infoIndex--; + if (infoIndex < 0) + infoIndex = _maxInfosGetter.Invoke() - 1; + } + OnTextInfoChanged?.Invoke(infoIndex); + } + } + +} \ No newline at end of file diff --git a/samples/Myra.Samples.Inspector/InspectGame.cs b/samples/Myra.Samples.Inspector/InspectGame.cs index 582ff376..2bdd3662 100644 --- a/samples/Myra.Samples.Inspector/InspectGame.cs +++ b/samples/Myra.Samples.Inspector/InspectGame.cs @@ -1,10 +1,7 @@ using Myra.Graphics2D.UI; -using System; -using System.Linq; -using Myra.Graphics2D.UI.Styles; - #if !STRIDE +using System.Text; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; #if ANDROID @@ -25,7 +22,8 @@ public class InspectGame : Game private readonly GraphicsDeviceManager _graphics; #endif - private RootWidgets widgets; + private Input _input; + private RootWidgets _widgets; private Desktop _desktop; public static InspectGame Instance { get; private set; } @@ -40,7 +38,7 @@ public InspectGame() PreferredBackBufferWidth = 1200, PreferredBackBufferHeight = 800 }; - Window.AllowUserResizing = true; + Window.AllowUserResizing = false; #else #endif @@ -52,7 +50,7 @@ protected override Task LoadContent() { MyraEnvironment.Game = this; - _allWidgets = new Widgets(); + _widgets = new RootWidgets(); _desktop = new Desktop(); _desktop.Widgets.Add(_allWidgets); @@ -69,11 +67,17 @@ protected override void LoadContent() // Stylesheet.Current = DefaultAssets.DefaultStylesheet2X; - widgets = new RootWidgets(); + _widgets = new RootWidgets(); _desktop = new Desktop(); - _desktop.Root = widgets; - + _desktop.Root = _widgets; + + _input = new Input(() => _widgets.inspectables.Count, () => _widgets.infos.Count) + { + OnSelectionChanged = (int i) => { _widgets.SetSelectedIndex(i); }, + OnTextInfoChanged = (int i) => { _widgets.SetInfoIndex(i); }, + }; + #if MONOGAME && !ANDROID // Inform Myra that external text input is available // So it stops translating Keys to chars @@ -87,58 +91,12 @@ protected override void LoadContent() #endif } #endif - - private int selectedIndex = 0; - private bool lastDown, lastUp; protected override void Update(GameTime gameTime) { base.Update(gameTime); - var state = Keyboard.GetState(); - - // prevent spam cycling by only triggering an input change up or down once - bool thisDown = state[Keys.Down] == KeyState.Down; - bool thisUp = state[Keys.Up] == KeyState.Down; - if (thisDown & !lastDown) - CycleSelection(false); - else if(thisUp & !lastUp) - CycleSelection(true); - - lastDown = thisDown; - lastUp = thisUp; - } - - private void CycleSelection(bool forward) - { - if (forward) - { - selectedIndex++; - if (selectedIndex >= widgets.inspectables.Count) - selectedIndex = 0; - } - else - { - selectedIndex--; - if (selectedIndex < 0) - selectedIndex = widgets.inspectables.Count - 1; - } - widgets.Inspect( widgets.inspectables[selectedIndex] ); + _input.Update(Keyboard.GetState()); } - private string TextDisplay - { - get - { - Type inspectedType = widgets.inspectables[selectedIndex].GetType(); - string baseType = string.Empty; - if (inspectedType.BaseType != null) - { - baseType = $" BaseType: {inspectedType.BaseType.Name}"; - } - - return $"\nInspecting object [{selectedIndex+1}] of [{widgets.inspectables.Count}]:\n\n Type: {inspectedType.Name}\n in: {inspectedType.Namespace}\n{baseType}\n\n Assembly:\n{inspectedType.Assembly.GetName().Name}\n\n\n\n\n\n\nIs mouse over GUI: {_desktop.IsMouseOverGUI}"; - } - } - protected override void Draw(GameTime gameTime) { base.Draw(gameTime); @@ -153,7 +111,7 @@ protected override void Draw(GameTime gameTime) // Set render target GraphicsContext.CommandList.SetRenderTargetAndViewport(GraphicsDevice.Presenter.DepthStencilBuffer, GraphicsDevice.Presenter.BackBuffer); #endif - widgets.labelOverGui.Text = TextDisplay; + _widgets.OnPreRender(); _desktop.Render(); } } diff --git a/samples/Myra.Samples.Inspector/RootWidgets.cs b/samples/Myra.Samples.Inspector/RootWidgets.cs index ed3e0f25..dc36eea8 100644 --- a/samples/Myra.Samples.Inspector/RootWidgets.cs +++ b/samples/Myra.Samples.Inspector/RootWidgets.cs @@ -1,4 +1,7 @@ +using System; using System.Collections.Generic; +using System.Reflection; +using System.Text; using Myra.Graphics2D; using Myra.Graphics2D.UI; using Myra.Graphics2D.UI.Properties; @@ -7,15 +10,28 @@ namespace Myra.Samples.Inspector { public class RootWidgets : HorizontalStackPanel { - public Label labelOverGui; + private Label headerLabel; + private Label infoLabel; private PropertyGrid propertyGrid; + private bool _init; + private int sel_i, inf_i; public readonly List inspectables; + public readonly List infos; + private readonly StringGenerator headerInfo; public RootWidgets() { - inspectables = BuildInspectables(); BuildUI(); + + inspectables = BuildInspectables(); + infos = BuildInfoGenerators(); + headerInfo = new StringGenerator(() => + { + Type inspectedType = inspectables[sel_i].GetType(); + return $"\nInspecting object [{sel_i+1}] of [{inspectables.Count}]:\n\nType: {inspectedType.Name}\nin: {inspectedType.Namespace}\nBaseType: {inspectedType.BaseType?.Name}\n\nAssembly:\n{inspectedType.Assembly.GetName().Name}\n"; + }); + Inspect(inspectables[0]); } @@ -25,25 +41,109 @@ private List BuildInspectables() { new SomeTypesInAClass(), this, + propertyGrid, InspectGame.Instance }; } + private List BuildInfoGenerators() + { + return new List() + { + new StringGenerator(() => + { + return "\n=== Controls ===\nArrowUp/ArrowDown:\n Cycle inspected object\n\nArrowLeft/ArrowRight:\n Cycle this info display"; + }), + new StringGenerator(() => + { + // List loaded assemblies + List assemblies = new List(AppDomain.CurrentDomain.GetAssemblies()); + assemblies.Sort( + (a, b) => Comparer.Default.Compare(a.GetName().Name, b.GetName().Name) + ); + StringBuilder builder = new StringBuilder(); + int i = 0; + foreach (Assembly asm in assemblies) + { + builder.AppendLine($" [{i}]: {asm.GetName().Name}"); + i++; + } + return $"\n=== Loaded Assemblies ===\n{builder}"; + }), + }; + } + private void BuildUI() { - labelOverGui = new Label(); - labelOverGui.Width = 400; - labelOverGui.Padding = new Thickness(4); - this.Widgets.Add(labelOverGui); - - propertyGrid = new PropertyGrid(); - propertyGrid.Width = 500; - this.Widgets.Add(propertyGrid); + ScrollViewer scroll; + int scrollBarWidth = 30; + + int uiWidth = 500; + headerLabel = new Label() + { + Width = uiWidth - scrollBarWidth, + SingleLine = false, + }; + infoLabel = new Label() + { + Width = uiWidth - scrollBarWidth, + SingleLine = false, + }; + + var vert = new VerticalStackPanel() + { + Width = uiWidth - scrollBarWidth, + }; + vert.Widgets.Add(headerLabel); + vert.Widgets.Add(new HorizontalSeparator()); + vert.Widgets.Add(infoLabel); + + scroll = new ScrollViewer() + { + ShowHorizontalScrollBar = false, + Content = vert, + Padding = new Thickness(8), + Width = uiWidth, + }; + Widgets.Add(scroll); + + uiWidth = 700; + propertyGrid = new PropertyGrid() + { + Width = uiWidth - scrollBarWidth, + }; + scroll = new ScrollViewer() + { + ShowHorizontalScrollBar = false, + Content = propertyGrid, + Padding = new Thickness(8), + Width = uiWidth, + }; + Widgets.Add(scroll); } public void Inspect(object obj) { propertyGrid.Object = obj; } + + public void SetSelectedIndex(int index) + { + sel_i = index; + propertyGrid.Object = inspectables[index]; + headerInfo.MarkDirty(); + } + public void SetInfoIndex(int index) + { + inf_i = index; + infos[inf_i].MarkDirty(); + } + + public void OnPreRender() + { + headerLabel.Text = headerInfo.Text; + infoLabel.Text = infos[inf_i].Text; + } + } } \ No newline at end of file diff --git a/samples/Myra.Samples.Inspector/StringGenerator.cs b/samples/Myra.Samples.Inspector/StringGenerator.cs new file mode 100644 index 00000000..37680a7d --- /dev/null +++ b/samples/Myra.Samples.Inspector/StringGenerator.cs @@ -0,0 +1,34 @@ +using System; + +namespace Myra.Samples.Inspector +{ + // Caches complicated strings and only regenerates them on a dirty-pattern basis. + public class StringGenerator + { + private readonly Func _textGenerator; + private string _textCache; + private bool _isDirty = true; + + public string Text + { + get + { + if(_isDirty) + Clean(); + return _textCache; + } + } + + public StringGenerator(Func generator) + { + _textGenerator = generator; + } + + public bool MarkDirty() => _isDirty = true; + private void Clean() + { + _isDirty = false; + _textCache = _textGenerator.Invoke(); + } + } +} \ No newline at end of file From f5cc40eaa82ee2b760da11f9ff06a85c5d7dadcd Mon Sep 17 00:00:00 2001 From: Bamboy Date: Mon, 9 Feb 2026 12:21:52 -0800 Subject: [PATCH 05/28] Code cleanup --- .../UI/Properties/EditorTypeRegistry.cs | 23 +- src/Myra/Graphics2D/UI/Properties/Editors.cs | 79 +++---- .../UI/Properties/PropertyEditor.cs | 38 +++ .../Graphics2D/UI/Properties/PropertyGrid.cs | 219 ++++++++++-------- 4 files changed, 205 insertions(+), 154 deletions(-) diff --git a/src/Myra/Graphics2D/UI/Properties/EditorTypeRegistry.cs b/src/Myra/Graphics2D/UI/Properties/EditorTypeRegistry.cs index 08343c39..62917dea 100644 --- a/src/Myra/Graphics2D/UI/Properties/EditorTypeRegistry.cs +++ b/src/Myra/Graphics2D/UI/Properties/EditorTypeRegistry.cs @@ -8,25 +8,38 @@ namespace Myra.Graphics2D.UI.Properties public sealed class EditorTypeRegistry { public readonly Type EditorType; - public readonly string[] PropertyTypes; + private readonly Type[] Types; + private readonly string[] TypeNames; public EditorTypeRegistry(Type editorType, params Type[] propertyTypes) { EditorType = editorType; - PropertyTypes = TypeToString(propertyTypes); + Types = propertyTypes; + TypeNames = TypeToString(propertyTypes); } + public bool CanEditType(Type value) + { + for (int i = 0; i < Types.Length; i++) + { + if (Types[i].IsInterface && Types[i].IsAssignableFrom(value)) + return true; + } + return CanEditType( TypeToString(value) ); + } public bool CanEditType(string value) { - foreach (string other in PropertyTypes) + foreach (string other in TypeNames) { if(StringComparer.InvariantCultureIgnoreCase.Equals(other, value)) return true; } return false; } - - private static string TypeToString(Type value) => value.GetFriendlyName(); + + //TODO use a less expensive conversion here. + //We only use string for the purpose of testing type/type equality across assembly + internal static string TypeToString(Type value) => value.GetFriendlyName(); private static string[] TypeToString(params Type[] args) { if (args == null || args.Length == 0) diff --git a/src/Myra/Graphics2D/UI/Properties/Editors.cs b/src/Myra/Graphics2D/UI/Properties/Editors.cs index 37afec74..1ffe2781 100644 --- a/src/Myra/Graphics2D/UI/Properties/Editors.cs +++ b/src/Myra/Graphics2D/UI/Properties/Editors.cs @@ -9,88 +9,69 @@ namespace Myra.Graphics2D.UI.Properties { internal static class Editors { - private static readonly Type[] ActivatorTypeArgs = { typeof(IInspector), typeof(Record) }; - private static ReadOnlyCollection EditorRegistry; + private static ReadOnlyCollection _registry; private static bool _init; - public static PropertyEditor Create(IInspector inspector, Record methodInfo) - { - if (TryGetEditorTypeForType(methodInfo.Type, out Type editorType)) - { - var ctor = editorType.GetConstructor(ActivatorTypeArgs); - if (ctor != null) - { - try - { - //This also creates the widget - object obj = Activator.CreateInstance(editorType, inspector, methodInfo); - return obj as PropertyEditor; - } - catch (Exception e) - { - throw e; - } - } - } - else - { - Console.WriteLine("Could not find property editor for type: "+methodInfo.Type); - } - return null; - } internal static bool TryGetEditorTypeForType(Type propertyKind, out Type editorType) { if (!_init) InitializeRegistry(); - string nice = propertyKind.GetFriendlyName(); - for (int i = 0; i < EditorRegistry.Count; i++) + string str = EditorTypeRegistry.TypeToString(propertyKind); + for (int i = 0; i < _registry.Count; i++) + { + if (_registry[i].CanEditType(str)) + { + editorType = _registry[i].EditorType; + return true; + } + } + + for (int i = 0; i < _registry.Count; i++) { - if (EditorRegistry[i].CanEditType(nice)) + if (_registry[i].CanEditType(propertyKind)) { - editorType = EditorRegistry[i].EditorType; + editorType = _registry[i].EditorType; return true; } } + //typeof(IList).IsAssignableFrom(propertyKind) editorType = null; return false; } - public static void InitializeRegistry(params Assembly[] fromAssemblies) + /// + internal static void InitializeRegistry(int predictedCount = 16, params Assembly[] fromAssemblies) { - List scanAsm; + Assembly[] scanAsm; if (fromAssemblies == null || fromAssemblies.Length <= 0) - scanAsm = new List { typeof(Editors).Assembly, Assembly.GetEntryAssembly() }; + scanAsm = AppDomain.CurrentDomain.GetAssemblies(); //this scans way more assemblies than needed, but works else { - scanAsm = new List(fromAssemblies); - scanAsm.Add(typeof(Editors).Assembly); - scanAsm.Add(Assembly.GetEntryAssembly()); + scanAsm = fromAssemblies; + if(!fromAssemblies.Contains(typeof(Editors).Assembly)) + Console.WriteLine("PropertyEditor.InitializeRegistry() warning: Myra's Assembly was not included."); + //maybe ensure fromAssemblies contains Myra (this) assembly? } - Reflective_LoadTypeRegistry( - 16, - scanAsm, - out List registry - ); + Reflective_LoadTypeRegistry(predictedCount, scanAsm, out List registry); - EditorRegistry = registry.AsReadOnly(); + _registry = registry.AsReadOnly(); _init = true; } /// - /// Creates all concrete classes that inherit the type, from the specified assemblies. Only creates types with no-argument constructors. + /// Generate objects for each concrete implementation found. + /// That implementation must also have a . /// - /// The base abstract type - /// The assemblies to scan for inheritors - private static void Reflective_LoadTypeRegistry(int predictedCount, IEnumerable assemblies, out List registry) where T : class + private static void Reflective_LoadTypeRegistry(int predictedCount, IEnumerable assemblies, out List registry) { registry = new List(predictedCount); foreach (Assembly asm in assemblies) { - // LoadAttributesFromConcreteSubTypes(asm, results); + foreach (Type type in asm.GetTypes() - .Where(myType => myType.IsClass && !myType.IsAbstract && myType.IsSubclassOf(typeof(T)))) + .Where(myType => myType.IsClass && !myType.IsAbstract && myType.IsSubclassOf(typeof(PropertyEditor)) )) { PropertyEditorAttribute att = type.FindAttribute(); if (att == null) diff --git a/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs index 497a7315..747b934b 100644 --- a/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs +++ b/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs @@ -24,6 +24,44 @@ public PropertyEditorAttribute(Type attached, params Type[] editTypes) /// public abstract class PropertyEditor : IRecord { +#region Statics + private static readonly Type[] ActivatorTypeArgs = { typeof(IInspector), typeof(Record) }; + /// + /// Initialize the - relationship registry. + /// + /// Internal editor-array alloc size. + /// The assemblies which are scanned for concrete inheritors of . Null will scan all assemblies in the current . + public static void InitializeRegistry(int predictedCount = 16, params System.Reflection.Assembly[] fromAssemblies) + => Editors.InitializeRegistry(predictedCount, fromAssemblies); + public static bool TryCreate(IInspector inspector, Record bindProperty, out PropertyEditor result) + { + if (Editors.TryGetEditorTypeForType(bindProperty.Type, out Type editorType)) + { + var ctor = editorType.GetConstructor(ActivatorTypeArgs); + if (ctor != null) + { + try + { + //This also creates the widget + object obj = Activator.CreateInstance(editorType, inspector, bindProperty); + result = obj as PropertyEditor; + return result != null; + } + catch (Exception e) + { + Console.WriteLine($"Activator Reflection Error: {e}"); + } + } + } + else + { + Console.WriteLine("Could not find property editor for type: "+bindProperty.Type); + } + result = null; + return false; + } +#endregion + protected delegate bool WidgetCreatorDelegate(out Widget widget); protected readonly IInspector _owner; diff --git a/src/Myra/Graphics2D/UI/Properties/PropertyGrid.cs b/src/Myra/Graphics2D/UI/Properties/PropertyGrid.cs index 2ee4be89..51a963a0 100644 --- a/src/Myra/Graphics2D/UI/Properties/PropertyGrid.cs +++ b/src/Myra/Graphics2D/UI/Properties/PropertyGrid.cs @@ -39,13 +39,15 @@ public partial class PropertyGrid : Widget, IInspector private readonly PropertyGrid _parentGrid; private Record _parentProperty; private readonly Dictionary> _records = new Dictionary>(); + private List _recMemory = new List(32); private readonly HashSet _expandedCategories = new HashSet(); private object _object; private bool _ignoreCollections; + private bool _doFancyLayout = true; private readonly PropertyGridSettings _settings = new PropertyGridSettings(); private string _filter; private Type _parentType; - + [Browsable(false)] [XmlIgnore] public TreeStyle PropertyGridStyle { get; private set; } @@ -80,17 +82,10 @@ public Type ParentType get { if (_parentGrid != null) - { return _parentGrid.ParentType; - } - return _parentType; } - - set - { - _parentType = value; - } + set => _parentType = value; } [Browsable(false)] @@ -104,28 +99,37 @@ public bool IgnoreCollections get { if (_parentGrid != null) - { return _parentGrid.IgnoreCollections; - } - return _ignoreCollections; } + set => _ignoreCollections = value; + } + [Category("Behavior")] + [DefaultValue(true)] + public bool DoFancyLayout + { + get + { + if (_parentGrid != null) + return _parentGrid.DoFancyLayout; + return _doFancyLayout; + } set { - _ignoreCollections = value; + if (_parentGrid != null) + return; + if (_doFancyLayout != value) + { + _doFancyLayout = value; + Rebuild(); + } } } [Browsable(false)] [XmlIgnore] - public bool IsEmpty - { - get - { - return Children.Count == 0; - } - } + public bool IsEmpty => Children.Count == 0; [Browsable(false)] [XmlIgnore] @@ -270,7 +274,7 @@ void IInspector.FireChanged(string name) private void SetValue(Record record, object obj, object value) { - if (CustomSetter != null && CustomSetter(record, obj, value)) + if (CustomSetter != null && CustomSetter(record, obj, value)) //TODO reimplement custom setter return; record.SetValue(obj, value); @@ -1010,9 +1014,8 @@ private void FillSubGrid(ref int y, IReadOnlyList records) CustomValues customValues = null; var needsSubGrid = false; - - var editor = Editors.Create(this, record); - if (editor != null) + + if (PropertyEditor.TryCreate(this, record, out PropertyEditor editor)) { valueWidget = editor.Widget; } @@ -1177,7 +1180,7 @@ private void FillSubGrid(ref int y, IReadOnlyList records) } } } - + public bool PassesFilter(string name) { if (string.IsNullOrEmpty(Filter) || string.IsNullOrEmpty(name)) @@ -1195,14 +1198,87 @@ public void Rebuild() _records.Clear(); _expandedCategories.Clear(); - if (_object == null) + if(!RecordAggregator(in _object, in _parentType, _recMemory, _doFancyLayout)) + return; + + // Sort by categories + for (var i = 0; i < _recMemory.Count; ++i) + { + var record = _recMemory[i]; + + List categoryRecords; + if (!_records.TryGetValue(record.Category, out categoryRecords)) + { + categoryRecords = new List(); + _records[record.Category] = categoryRecords; + } + + categoryRecords.Add(record); + } + + if (_doFancyLayout) + { + // Sort by names within categories + foreach (var category in _records) + { + category.Value.Sort((a, b) => Comparer.Default.Compare(a.Name, b.Name)); + } + } + + var ordered = _records.OrderBy(key => key.Key); + + var y = 0; + List defaultCategoryRecords; + if (_records.TryGetValue(Category, out defaultCategoryRecords)) + { + FillSubGrid(ref y, defaultCategoryRecords); + } + + if (Category != DefaultCategoryName) { return; } + foreach (var category in ordered) + { + if (category.Key == DefaultCategoryName) + { + continue; + } + + var subGrid = new SubGrid(this, Object, category.Key, category.Key, Filter, null); + Grid.SetColumnSpan(subGrid, 2); + Grid.SetRow(subGrid, y); + + if (subGrid.IsEmpty) + { + continue; + } + + Children.Add(subGrid); + + if (_expandedCategories.Contains(category.Key)) + { + subGrid.Mark.IsPressed = true; + } + + var rp = new Proportion(ProportionType.Auto); + _layout.RowsProportions.Add(rp); + + y++; + } + } + + private static bool RecordAggregator(in object target, in Type parentType, List result, bool categorize = true) + { + result.Clear(); + if (target == null) + return false; + + Type targetType = target.GetType(); + // Properties - var properties = from p in _object.GetType().GetProperties() select p; - var records = new List(); + var properties = from p in targetType.GetProperties() select p; foreach (var property in properties) { if (property.GetGetMethod() == null || @@ -1231,15 +1307,15 @@ public void Rebuild() { HasSetter = hasSetter }; - + var categoryAttr = property.FindAttribute(); - record.Category = categoryAttr != null ? categoryAttr.Category : DefaultCategoryName; + record.Category = (categoryAttr != null & categorize) ? categoryAttr.Category : DefaultCategoryName; - records.Add(record); + result.Add(record); } // Fields - var fields = from f in _object.GetType().GetFields() select f; + var fields = from f in targetType.GetFields() select f; foreach (var field in fields) { if (!field.IsPublic || field.IsStatic) @@ -1265,17 +1341,17 @@ public void Rebuild() var record = new FieldRecord(field) { HasSetter = hasSetter, - Category = categoryAttr != null ? categoryAttr.Category : DefaultCategoryName + Category = (categoryAttr != null & categorize) ? categoryAttr.Category : DefaultCategoryName }; - records.Add(record); + result.Add(record); } // Attached properties - var asWidget = _object as Widget; - if (asWidget != null && ParentType != null) + var asWidget = target as Widget; + if (asWidget != null && parentType != null) { - var attachedProperties = AttachedPropertiesRegistry.GetPropertiesOfType(ParentType); + var attachedProperties = AttachedPropertiesRegistry.GetPropertiesOfType(parentType); foreach (var attachedProperty in attachedProperties) { var record = new AttachedPropertyRecord(attachedProperty) @@ -1283,73 +1359,16 @@ public void Rebuild() Category = attachedProperty.OwnerType.Name }; - records.Add(record); + result.Add(record); } } - // Sort by categories - for (var i = 0; i < records.Count; ++i) - { - var record = records[i]; - - List categoryRecords; - if (!_records.TryGetValue(record.Category, out categoryRecords)) - { - categoryRecords = new List(); - _records[record.Category] = categoryRecords; - } - - categoryRecords.Add(record); - } - - // Sort by names within categories - foreach (var category in _records) - { - category.Value.Sort((a, b) => Comparer.Default.Compare(a.Name, b.Name)); - } - - var ordered = _records.OrderBy(key => key.Key); - - var y = 0; - List defaultCategoryRecords; - if (_records.TryGetValue(Category, out defaultCategoryRecords)) - { - FillSubGrid(ref y, defaultCategoryRecords); - } - - if (Category != DefaultCategoryName) - { - return; - } - - foreach (var category in ordered) - { - if (category.Key == DefaultCategoryName) - { - continue; - } - - var subGrid = new SubGrid(this, Object, category.Key, category.Key, Filter, null); - Grid.SetColumnSpan(subGrid, 2); - Grid.SetRow(subGrid, y); - - if (subGrid.IsEmpty) - { - continue; - } - - Children.Add(subGrid); - - if (_expandedCategories.Contains(category.Key)) - { - subGrid.Mark.IsPressed = true; - } - - var rp = new Proportion(ProportionType.Auto); - _layout.RowsProportions.Add(rp); + return true; + } - y++; - } + private void RecordSorter(ref List collection) + { + } public void ApplyPropertyGridStyle(TreeStyle style) From 2341ccdce93c236cde01cdca2f86c20b5d0b7f12 Mon Sep 17 00:00:00 2001 From: Bamboy Date: Mon, 9 Feb 2026 16:00:59 -0800 Subject: [PATCH 06/28] Merge into local fork. Fix version dependency for AssetManagementBase --- src/Myra/Myra.MonoGame.csproj | 3 +- ...AssetManagerExtensions.PlatformAgnostic.cs | 2 +- src/Myra/MyraAssetManagerExtensions.cs | 146 ++++++++++++++++-- 3 files changed, 137 insertions(+), 14 deletions(-) diff --git a/src/Myra/Myra.MonoGame.csproj b/src/Myra/Myra.MonoGame.csproj index 73ba2460..67444448 100644 --- a/src/Myra/Myra.MonoGame.csproj +++ b/src/Myra/Myra.MonoGame.csproj @@ -26,9 +26,10 @@ + - \ No newline at end of file + diff --git a/src/Myra/MyraAssetManagerExtensions.PlatformAgnostic.cs b/src/Myra/MyraAssetManagerExtensions.PlatformAgnostic.cs index fa2b5bf1..fa277cd0 100644 --- a/src/Myra/MyraAssetManagerExtensions.PlatformAgnostic.cs +++ b/src/Myra/MyraAssetManagerExtensions.PlatformAgnostic.cs @@ -10,7 +10,7 @@ namespace AssetManagementBase { - partial class MyraAssetManagerExtensions + public static partial class MyraAssetManagerExtensions { internal class Texture2DWrapper { diff --git a/src/Myra/MyraAssetManagerExtensions.cs b/src/Myra/MyraAssetManagerExtensions.cs index df3e2bad..3d01bfc1 100644 --- a/src/Myra/MyraAssetManagerExtensions.cs +++ b/src/Myra/MyraAssetManagerExtensions.cs @@ -16,13 +16,82 @@ using Texture2D = Stride.Graphics.Texture; #else using System.Drawing; +using SolidBrush = Myra.Graphics2D.Brushes.SolidBrush; +using Color = FontStashSharp.FSColor; using Texture2D = System.Object; +using StbImageSharp; +using System.IO; #endif namespace AssetManagementBase { public static partial class MyraAssetManagerExtensions { +#if PLATFORM_AGNOSTIC + internal class Texture2DWrapper + { + public int Width { get; private set; } + public int Height { get; private set; } + public object Texture { get; private set; } + + public Texture2DWrapper(int width, int height, object texture) + { + Width = width; + Height = height; + Texture = texture; + } + } + + private static AssetLoader _textureLoader = (manager, assetName, settings, tag) => + { + ImageResult result = null; + using (var stream = manager.Open(assetName)) + { + if (stream.CanSeek) + { + result = ImageResult.FromStream(stream, ColorComponents.RedGreenBlueAlpha); + } + else + { + // If stream doesnt provide seek functionaly, use MemoryStream instead + using (var ms = new MemoryStream()) + { + stream.CopyTo(ms); + ms.Seek(0, SeekOrigin.Begin); + result = ImageResult.FromStream(ms, ColorComponents.RedGreenBlueAlpha); + } + } + } + + // Premultiply Alpha + var b = result.Data; + for (var i = 0; i < result.Data.Length; i += 4) + { + var falpha = b[i + 3] / 255.0f; + b[i] = (byte)(b[i] * falpha); + b[i + 1] = (byte)(b[i + 1] * falpha); + b[i + 2] = (byte)(b[i + 2] * falpha); + } + + var textureManager = MyraEnvironment.Platform.Renderer.TextureManager; + var texture = textureManager.CreateTexture(result.Width, result.Height); + textureManager.SetTextureData(texture, new Rectangle(0, 0, result.Width, result.Height), result.Data); + return new Texture2DWrapper(result.Width, result.Height, texture); + }; + + internal static Texture2DWrapper LoadTexture2D(this AssetManager assetManager, string assetName) => + assetManager.UseLoader(_textureLoader, assetName); +#endif + + private class FontSystemLoadingSettings : IAssetSettings + { + public Texture2D ExistingTexture { get; set; } + public Rectangle ExistingTextureUsedSpace { get; set; } + public string[] AdditionalFonts { get; set; } + + public string BuildKey() => string.Empty; + } + private static AssetLoader _atlasLoader = (manager, assetName, settings, tag) => { var data = manager.ReadAsString(assetName); @@ -34,6 +103,32 @@ public static partial class MyraAssetManagerExtensions #endif }; + private static AssetLoader _fontSystemLoader = (manager, assetName, settings, tag) => + { + var fontSystemSettings = new FontSystemSettings(); + + var fontSystemLoadingSettings = (FontSystemLoadingSettings)settings; + if (fontSystemLoadingSettings != null) + { + fontSystemSettings.ExistingTexture = fontSystemLoadingSettings.ExistingTexture; + fontSystemSettings.ExistingTextureUsedSpace = fontSystemLoadingSettings.ExistingTextureUsedSpace; + }; + + var fontSystem = new FontSystem(fontSystemSettings); + var data = manager.ReadAsByteArray(assetName); + fontSystem.AddFont(data); + if (fontSystemLoadingSettings != null && fontSystemLoadingSettings.AdditionalFonts != null) + { + foreach (var file in fontSystemLoadingSettings.AdditionalFonts) + { + data = manager.ReadAsByteArray(file); + fontSystem.AddFont(data); + } + } + + return fontSystem; + }; + private static AssetLoader _staticFontLoader = (manager, assetName, settings, tag) => { var fontData = manager.ReadAsString(assetName); @@ -82,9 +177,9 @@ public static partial class MyraAssetManagerExtensions if (fontFile.EndsWith(".ttf") || fontFile.EndsWith(".otf")) { var parts = new List() - { - fontFile - }; + { + fontFile + }; var typeAttribute = el.Attribute("Effect"); if (typeAttribute != null) @@ -101,12 +196,12 @@ public static partial class MyraAssetManagerExtensions } parts.Add(el.Attribute("Size").Value); - var fontSystem = manager.LoadFontSystem(fontFile, existingTexture: existingTexture, existingTextureUsedSpace: existingTextureUsedSpace); + var fontSystem = LoadFontSystem(manager, fontFile, existingTexture: existingTexture, existingTextureUsedSpace: existingTextureUsedSpace); font = fontSystem.GetFont(float.Parse(el.Attribute("Size").Value)); } else if (fontFile.EndsWith(".fnt")) { - font = manager.MyraLoadStaticSpriteFont(fontFile); + font = manager.LoadStaticSpriteFont(fontFile); } else { @@ -148,7 +243,23 @@ public static TextureRegion LoadTextureRegion(this AssetManager assetManager, st #endif } - internal static StaticSpriteFont MyraLoadStaticSpriteFont(this AssetManager assetManager, string assetName) => assetManager.UseLoader(_staticFontLoader, assetName); + public static FontSystem LoadFontSystem(this AssetManager assetManager, string assetName, string[] additionalFonts = null, Texture2D existingTexture = null, Rectangle existingTextureUsedSpace = default(Rectangle)) + { + FontSystemLoadingSettings settings = null; + if (additionalFonts != null || existingTexture != null) + { + settings = new FontSystemLoadingSettings + { + AdditionalFonts = additionalFonts, + ExistingTexture = existingTexture, + ExistingTextureUsedSpace = existingTextureUsedSpace + }; + } + + return assetManager.UseLoader(_fontSystemLoader, assetName, settings); + } + + public static StaticSpriteFont LoadStaticSpriteFont(this AssetManager assetManager, string assetName) => assetManager.UseLoader(_staticFontLoader, assetName); /// /// Loads a font by either ttf name/size(i.e. 'font.ttf:32') or by fnt name(i.e. 'font.fnt') @@ -160,24 +271,35 @@ public static SpriteFontBase LoadFont(this AssetManager assetManager, string ass { if (assetName.Contains(".fnt")) { - return assetManager.MyraLoadStaticSpriteFont(assetName); + return assetManager.LoadStaticSpriteFont(assetName); } else if (assetName.Contains(".ttf")) { - var parts = assetName.Split(':'); if (parts.Length < 2) { - throw new Exception("Missing font size"); + throw new ArgumentException("Missing font size"); } - var fontSize = int.Parse(parts[1].Trim()); - var fontSystem = assetManager.LoadFontSystem(parts[0].Trim()); + string fontPath = parts[0].Trim(); + if (!int.TryParse(parts[1].Trim(), out int fontSize)) + { + throw new ArgumentException($"Font size is not a number: '{parts[1]}'"); + } + if (fontSize <= 0) + fontSize = 1; + + FontSystem fontSystem; +#if PLATFORM_AGNOSTIC + fontSystem = LoadFontSystem(assetManager, fontPath); +#else + fontSystem = XNAssetsExtFontStashSharp.LoadFontSystem(assetManager, fontPath); +#endif return fontSystem.GetFont(fontSize); } - throw new Exception(string.Format("Can't load font '{0}'", assetName)); + throw new ArgumentException(string.Format("Can't load font '{0}'", assetName)); } public static Stylesheet LoadStylesheet(this AssetManager assetManager, string assetName) => assetManager.UseLoader(_stylesheetLoader, assetName); From 1bc981cf37902db0277ee2f2ba9195d2a5e34904 Mon Sep 17 00:00:00 2001 From: Bamboy Date: Mon, 9 Feb 2026 16:39:56 -0800 Subject: [PATCH 07/28] Fix build when PLATFORM_AGNOSTIC is on --- src/Myra/MyraAssetManagerExtensions.cs | 142 ++----------------------- 1 file changed, 10 insertions(+), 132 deletions(-) diff --git a/src/Myra/MyraAssetManagerExtensions.cs b/src/Myra/MyraAssetManagerExtensions.cs index 3d01bfc1..80ebcd3f 100644 --- a/src/Myra/MyraAssetManagerExtensions.cs +++ b/src/Myra/MyraAssetManagerExtensions.cs @@ -16,82 +16,13 @@ using Texture2D = Stride.Graphics.Texture; #else using System.Drawing; -using SolidBrush = Myra.Graphics2D.Brushes.SolidBrush; -using Color = FontStashSharp.FSColor; using Texture2D = System.Object; -using StbImageSharp; -using System.IO; #endif namespace AssetManagementBase { public static partial class MyraAssetManagerExtensions { -#if PLATFORM_AGNOSTIC - internal class Texture2DWrapper - { - public int Width { get; private set; } - public int Height { get; private set; } - public object Texture { get; private set; } - - public Texture2DWrapper(int width, int height, object texture) - { - Width = width; - Height = height; - Texture = texture; - } - } - - private static AssetLoader _textureLoader = (manager, assetName, settings, tag) => - { - ImageResult result = null; - using (var stream = manager.Open(assetName)) - { - if (stream.CanSeek) - { - result = ImageResult.FromStream(stream, ColorComponents.RedGreenBlueAlpha); - } - else - { - // If stream doesnt provide seek functionaly, use MemoryStream instead - using (var ms = new MemoryStream()) - { - stream.CopyTo(ms); - ms.Seek(0, SeekOrigin.Begin); - result = ImageResult.FromStream(ms, ColorComponents.RedGreenBlueAlpha); - } - } - } - - // Premultiply Alpha - var b = result.Data; - for (var i = 0; i < result.Data.Length; i += 4) - { - var falpha = b[i + 3] / 255.0f; - b[i] = (byte)(b[i] * falpha); - b[i + 1] = (byte)(b[i + 1] * falpha); - b[i + 2] = (byte)(b[i + 2] * falpha); - } - - var textureManager = MyraEnvironment.Platform.Renderer.TextureManager; - var texture = textureManager.CreateTexture(result.Width, result.Height); - textureManager.SetTextureData(texture, new Rectangle(0, 0, result.Width, result.Height), result.Data); - return new Texture2DWrapper(result.Width, result.Height, texture); - }; - - internal static Texture2DWrapper LoadTexture2D(this AssetManager assetManager, string assetName) => - assetManager.UseLoader(_textureLoader, assetName); -#endif - - private class FontSystemLoadingSettings : IAssetSettings - { - public Texture2D ExistingTexture { get; set; } - public Rectangle ExistingTextureUsedSpace { get; set; } - public string[] AdditionalFonts { get; set; } - - public string BuildKey() => string.Empty; - } - private static AssetLoader _atlasLoader = (manager, assetName, settings, tag) => { var data = manager.ReadAsString(assetName); @@ -103,32 +34,6 @@ private class FontSystemLoadingSettings : IAssetSettings #endif }; - private static AssetLoader _fontSystemLoader = (manager, assetName, settings, tag) => - { - var fontSystemSettings = new FontSystemSettings(); - - var fontSystemLoadingSettings = (FontSystemLoadingSettings)settings; - if (fontSystemLoadingSettings != null) - { - fontSystemSettings.ExistingTexture = fontSystemLoadingSettings.ExistingTexture; - fontSystemSettings.ExistingTextureUsedSpace = fontSystemLoadingSettings.ExistingTextureUsedSpace; - }; - - var fontSystem = new FontSystem(fontSystemSettings); - var data = manager.ReadAsByteArray(assetName); - fontSystem.AddFont(data); - if (fontSystemLoadingSettings != null && fontSystemLoadingSettings.AdditionalFonts != null) - { - foreach (var file in fontSystemLoadingSettings.AdditionalFonts) - { - data = manager.ReadAsByteArray(file); - fontSystem.AddFont(data); - } - } - - return fontSystem; - }; - private static AssetLoader _staticFontLoader = (manager, assetName, settings, tag) => { var fontData = manager.ReadAsString(assetName); @@ -177,9 +82,9 @@ private class FontSystemLoadingSettings : IAssetSettings if (fontFile.EndsWith(".ttf") || fontFile.EndsWith(".otf")) { var parts = new List() - { - fontFile - }; + { + fontFile + }; var typeAttribute = el.Attribute("Effect"); if (typeAttribute != null) @@ -196,12 +101,12 @@ private class FontSystemLoadingSettings : IAssetSettings } parts.Add(el.Attribute("Size").Value); - var fontSystem = LoadFontSystem(manager, fontFile, existingTexture: existingTexture, existingTextureUsedSpace: existingTextureUsedSpace); + var fontSystem = manager.LoadFontSystem(fontFile, existingTexture: existingTexture, existingTextureUsedSpace: existingTextureUsedSpace); font = fontSystem.GetFont(float.Parse(el.Attribute("Size").Value)); } else if (fontFile.EndsWith(".fnt")) { - font = manager.LoadStaticSpriteFont(fontFile); + font = manager.MyraLoadStaticSpriteFont(fontFile); } else { @@ -243,23 +148,7 @@ public static TextureRegion LoadTextureRegion(this AssetManager assetManager, st #endif } - public static FontSystem LoadFontSystem(this AssetManager assetManager, string assetName, string[] additionalFonts = null, Texture2D existingTexture = null, Rectangle existingTextureUsedSpace = default(Rectangle)) - { - FontSystemLoadingSettings settings = null; - if (additionalFonts != null || existingTexture != null) - { - settings = new FontSystemLoadingSettings - { - AdditionalFonts = additionalFonts, - ExistingTexture = existingTexture, - ExistingTextureUsedSpace = existingTextureUsedSpace - }; - } - - return assetManager.UseLoader(_fontSystemLoader, assetName, settings); - } - - public static StaticSpriteFont LoadStaticSpriteFont(this AssetManager assetManager, string assetName) => assetManager.UseLoader(_staticFontLoader, assetName); + internal static StaticSpriteFont MyraLoadStaticSpriteFont(this AssetManager assetManager, string assetName) => assetManager.UseLoader(_staticFontLoader, assetName); /// /// Loads a font by either ttf name/size(i.e. 'font.ttf:32') or by fnt name(i.e. 'font.fnt') @@ -271,31 +160,20 @@ public static SpriteFontBase LoadFont(this AssetManager assetManager, string ass { if (assetName.Contains(".fnt")) { - return assetManager.LoadStaticSpriteFont(assetName); + return assetManager.MyraLoadStaticSpriteFont(assetName); } else if (assetName.Contains(".ttf")) { + var parts = assetName.Split(':'); if (parts.Length < 2) { throw new ArgumentException("Missing font size"); } - string fontPath = parts[0].Trim(); - if (!int.TryParse(parts[1].Trim(), out int fontSize)) - { - throw new ArgumentException($"Font size is not a number: '{parts[1]}'"); - } + var fontSize = int.Parse(parts[1].Trim()); + var fontSystem = assetManager.LoadFontSystem(parts[0].Trim()); - if (fontSize <= 0) - fontSize = 1; - - FontSystem fontSystem; -#if PLATFORM_AGNOSTIC - fontSystem = LoadFontSystem(assetManager, fontPath); -#else - fontSystem = XNAssetsExtFontStashSharp.LoadFontSystem(assetManager, fontPath); -#endif return fontSystem.GetFont(fontSize); } From 22e26664d1b32b557d99295651e308c92f36cedd Mon Sep 17 00:00:00 2001 From: Bamboy Date: Wed, 11 Feb 2026 20:01:04 -0800 Subject: [PATCH 08/28] Structures to support generic numbers through Record, misc --- .../Myra.Samples.Inspector.csproj | 3 +- .../UI/Properties/EditorTypeRegistry.cs | 24 +++- src/Myra/Graphics2D/UI/Properties/Editors.cs | 7 +- src/Myra/Graphics2D/UI/Properties/IRecord.cs | 18 --- .../UI/Properties/PropertyEditor.cs | 9 +- .../Graphics2D/UI/Properties/PropertyGrid.cs | 4 +- src/Myra/Graphics2D/UI/Properties/Record.cs | 2 +- src/Myra/Myra.MonoGame.csproj | 1 + src/Myra/Utility/Reflection.cs | 36 ++++-- src/Myra/Utility/Types/IRecordReference.cs | 53 +++++++++ src/Myra/Utility/Types/Range.cs | 59 ++++++++++ src/Myra/Utility/Types/RecordReference.cs | 108 ++++++++++++++++++ src/Myra/Utility/Types/TypeHelper.cs | 66 +++++++++++ src/Myra/Utility/Types/TypeInfo.cs | 81 +++++++++++++ 14 files changed, 427 insertions(+), 44 deletions(-) delete mode 100644 src/Myra/Graphics2D/UI/Properties/IRecord.cs create mode 100644 src/Myra/Utility/Types/IRecordReference.cs create mode 100644 src/Myra/Utility/Types/Range.cs create mode 100644 src/Myra/Utility/Types/RecordReference.cs create mode 100644 src/Myra/Utility/Types/TypeHelper.cs create mode 100644 src/Myra/Utility/Types/TypeInfo.cs diff --git a/samples/Myra.Samples.Inspector/Myra.Samples.Inspector.csproj b/samples/Myra.Samples.Inspector/Myra.Samples.Inspector.csproj index 47abce29..30259a76 100644 --- a/samples/Myra.Samples.Inspector/Myra.Samples.Inspector.csproj +++ b/samples/Myra.Samples.Inspector/Myra.Samples.Inspector.csproj @@ -7,10 +7,11 @@ + - \ No newline at end of file + diff --git a/src/Myra/Graphics2D/UI/Properties/EditorTypeRegistry.cs b/src/Myra/Graphics2D/UI/Properties/EditorTypeRegistry.cs index 62917dea..dbf6daa5 100644 --- a/src/Myra/Graphics2D/UI/Properties/EditorTypeRegistry.cs +++ b/src/Myra/Graphics2D/UI/Properties/EditorTypeRegistry.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Myra.Utility; +using Myra.Utility.Types; namespace Myra.Graphics2D.UI.Properties { @@ -18,20 +19,33 @@ public EditorTypeRegistry(Type editorType, params Type[] propertyTypes) TypeNames = TypeToString(propertyTypes); } - public bool CanEditType(Type value) + public bool CanEditType(Type type, bool allowCasts = true) { + if (!allowCasts) + { + for (int i = 0; i < Types.Length; i++) + { + if (object.ReferenceEquals(Types[i], type)) + return true; + } + return CanEditType( TypeToString(type) ); + } + for (int i = 0; i < Types.Length; i++) { - if (Types[i].IsInterface && Types[i].IsAssignableFrom(value)) + Type supported = Types[i]; + if (object.ReferenceEquals(supported, type)) + return true; + if (supported.IsInterface && supported.IsAssignableFrom(type)) return true; } - return CanEditType( TypeToString(value) ); + return CanEditType( TypeToString(type) ); } public bool CanEditType(string value) { - foreach (string other in TypeNames) + foreach (string supported in TypeNames) { - if(StringComparer.InvariantCultureIgnoreCase.Equals(other, value)) + if(StringComparer.InvariantCultureIgnoreCase.Equals(supported, value)) return true; } return false; diff --git a/src/Myra/Graphics2D/UI/Properties/Editors.cs b/src/Myra/Graphics2D/UI/Properties/Editors.cs index 1ffe2781..940f56b9 100644 --- a/src/Myra/Graphics2D/UI/Properties/Editors.cs +++ b/src/Myra/Graphics2D/UI/Properties/Editors.cs @@ -17,24 +17,25 @@ internal static bool TryGetEditorTypeForType(Type propertyKind, out Type editorT if (!_init) InitializeRegistry(); - string str = EditorTypeRegistry.TypeToString(propertyKind); for (int i = 0; i < _registry.Count; i++) { - if (_registry[i].CanEditType(str)) + if (_registry[i].CanEditType(propertyKind)) { editorType = _registry[i].EditorType; return true; } } + string str = EditorTypeRegistry.TypeToString(propertyKind); for (int i = 0; i < _registry.Count; i++) { - if (_registry[i].CanEditType(propertyKind)) + if (_registry[i].CanEditType(str)) { editorType = _registry[i].EditorType; return true; } } + //typeof(IList).IsAssignableFrom(propertyKind) editorType = null; return false; diff --git a/src/Myra/Graphics2D/UI/Properties/IRecord.cs b/src/Myra/Graphics2D/UI/Properties/IRecord.cs deleted file mode 100644 index def08309..00000000 --- a/src/Myra/Graphics2D/UI/Properties/IRecord.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Reflection; - -namespace Myra.Graphics2D.UI.Properties -{ - public interface IRecord - { - Type Type { get; } - object GetValue(object field); - void SetValue(object field, object value); - } - - public interface IRecord : IRecord - { - new T GetValue(object field); - void SetValue(object field, T value); - } -} \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs index 747b934b..e6af86c8 100644 --- a/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs +++ b/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs @@ -1,4 +1,5 @@ using System; +using Myra.Utility.Types; namespace Myra.Graphics2D.UI.Properties { @@ -22,7 +23,7 @@ public PropertyEditorAttribute(Type attached, params Type[] editTypes) /// /// Encapsulates a widget and .Net property or field, for the purposes of display or editing by the user. /// - public abstract class PropertyEditor : IRecord + public abstract class PropertyEditor : IRecordReference { #region Statics private static readonly Type[] ActivatorTypeArgs = { typeof(IInspector), typeof(Record) }; @@ -84,7 +85,9 @@ protected PropertyEditor(IInspector owner, Record methodInfo) protected abstract bool TryCreateEditorWidget(out Widget widget); public Type Type => _record.Type; - object IRecord.GetValue(object field) => _record.GetValue(field); + object IRecordReference.GetValue(object field) => _record.GetValue(field); + Record IRecordReference.Record => _record; + bool IRecordReference.IsReadOnly => !_record.HasSetter; public void SetValue(object field, object value) { _record.SetValue(field, value); @@ -93,7 +96,7 @@ public void SetValue(object field, object value) } /// - public abstract class PropertyEditor : PropertyEditor, IRecord + public abstract class PropertyEditor : PropertyEditor, IRecordReference { protected abstract bool CreatorPicker(out WidgetCreatorDelegate creatorDelegate); diff --git a/src/Myra/Graphics2D/UI/Properties/PropertyGrid.cs b/src/Myra/Graphics2D/UI/Properties/PropertyGrid.cs index f9a967e9..801f7570 100644 --- a/src/Myra/Graphics2D/UI/Properties/PropertyGrid.cs +++ b/src/Myra/Graphics2D/UI/Properties/PropertyGrid.cs @@ -203,9 +203,7 @@ public string Filter [Browsable(false)] [XmlIgnore] public Func CustomWidgetProvider; - - private object _selected; - + public event EventHandler> PropertyChanged; public event EventHandler ObjectChanged; diff --git a/src/Myra/Graphics2D/UI/Properties/Record.cs b/src/Myra/Graphics2D/UI/Properties/Record.cs index 09b4c40d..10f0cfa9 100644 --- a/src/Myra/Graphics2D/UI/Properties/Record.cs +++ b/src/Myra/Graphics2D/UI/Properties/Record.cs @@ -7,7 +7,7 @@ namespace Myra.Graphics2D.UI.Properties /// /// Base for encapsulating reflective information using . /// - public abstract class Record : IRecord + public abstract class Record { public bool HasSetter { get; set; } public string Category { get; set; } diff --git a/src/Myra/Myra.MonoGame.csproj b/src/Myra/Myra.MonoGame.csproj index ffce1ac4..4d1fd827 100644 --- a/src/Myra/Myra.MonoGame.csproj +++ b/src/Myra/Myra.MonoGame.csproj @@ -59,6 +59,7 @@ + diff --git a/src/Myra/Utility/Reflection.cs b/src/Myra/Utility/Reflection.cs index 1aada931..d14250cd 100644 --- a/src/Myra/Utility/Reflection.cs +++ b/src/Myra/Utility/Reflection.cs @@ -120,9 +120,14 @@ public static Type FindGenericType(this Type givenType, Type genericType) return FindGenericType(baseType, genericType); } - public static bool IsNumericInteger(this Type t) + public static bool IsNumericType(this Type t) => IsNumericType(Type.GetTypeCode(t)); + public static bool IsNumericType(this TypeCode t) => IsNumericInteger(t) || IsNumericFractional(t); + + public static bool IsNumericInteger(this Type t) => IsNumericInteger(Type.GetTypeCode(t)); + public static bool IsNumericFractional(this Type t) => IsNumericFractional(Type.GetTypeCode(t)); + public static bool IsNumericInteger(this TypeCode t) { - switch (Type.GetTypeCode(t)) + switch (t) { case TypeCode.Byte: case TypeCode.SByte: @@ -137,15 +142,9 @@ public static bool IsNumericInteger(this Type t) return false; } } - - public static bool IsNumericType(this Type t) + public static bool IsNumericFractional(this TypeCode t) { - if (IsNumericInteger(t)) - { - return true; - } - - switch (Type.GetTypeCode(t)) + switch (t) { case TypeCode.Decimal: case TypeCode.Double: @@ -155,5 +154,22 @@ public static bool IsNumericType(this Type t) return false; } } + public static bool IsSigned(this TypeCode t) + { + // https://learn.microsoft.com/en-us/dotnet/api/system.typecode + int val = (int)t; + if (val < 5) + return false; + + if (val > 12) + { + if (val <= 15) // 13-15 are floating point + return true; + return false; + } + + val -= 5; + return val % 2 == 0; // Test even/odd + } } } diff --git a/src/Myra/Utility/Types/IRecordReference.cs b/src/Myra/Utility/Types/IRecordReference.cs new file mode 100644 index 00000000..4a8fe419 --- /dev/null +++ b/src/Myra/Utility/Types/IRecordReference.cs @@ -0,0 +1,53 @@ +using System; +using Myra.Graphics2D.UI.Properties; + +namespace Myra.Utility.Types +{ + public interface IRecordReference + { + Record Record { get; } + Type Type { get; } + bool IsReadOnly { get; } + object GetValue(object field); + void SetValue(object field, object value); + } + + public interface IRecordReference : IRecordReference + { + new T GetValue(object field); + void SetValue(object field, T value); + } + + public interface IStructTypeRef : IRecordReference where T : struct + { + bool IsNullable { get; } + } + + public interface INumberTypeRef : IStructTypeRef where T : struct + { + // todo include min/max T limiters + } + + public interface IWholeNumberTypeRef : INumberTypeRef where T : struct + { + + } + + public interface IFracNumberTypeRef : INumberTypeRef where T : struct + { + + } + + internal static class TypeInterfaceExtensions + { + public static TypeInfo TypeInfo(this IRecordReference obj) + => TypeHelper.Info; + public static bool IsNullable(this IStructTypeRef obj) where T : struct + => TypeHelper.Info.IsNullable; + public static bool IsSignedNumber(this INumberTypeRef obj) where T : struct + => TypeHelper.Info.IsSignedNumber; + + public static Type GetNullableTypePassThrough(this IStructTypeRef obj) where T : struct + => TypeHelper.GetNullableTypeOrPassThrough(); + } +} \ No newline at end of file diff --git a/src/Myra/Utility/Types/Range.cs b/src/Myra/Utility/Types/Range.cs new file mode 100644 index 00000000..102c34c0 --- /dev/null +++ b/src/Myra/Utility/Types/Range.cs @@ -0,0 +1,59 @@ +using System; + +namespace Myra.Utility.Types +{ + public struct Range where TNumber : struct + { + static Range() + { + Type arg = typeof(TNumber); + TypeInfo info = TypeHelper.Info; + + if(info.IsNullable) + throw new ArgumentException($"Invalid Generic-Type Argument: '{arg}', Nullable types are unsupported"); + if(!info.IsNumber) + throw new ArgumentException($"Invalid Generic-Type Argument: '{arg}', Only numeric types are supported"); + + } + // TODO import https://github.com/HelloKitty/Generic.Math + + public Range(TNumber min, TNumber max) + { + _applyMin = true; + _applyMax = true; + _min = min; + _max = max; + } + public Range(TNumber? min = null, TNumber? max = null) + { + _applyMin = min.HasValue; + _applyMax = max.HasValue; + _min = _applyMin ? min.Value : default; + _max = _applyMax ? max.Value : default; + } + + private bool _applyMin, _applyMax; + private TNumber _min, _max; + + public TNumber? Min + { + get => _min; + set + { + _applyMin = value.HasValue; + _min = value ?? default; + } + } + public TNumber? Max + { + get => _max; + set + { + _applyMax = value.HasValue; + _max = value ?? default; + } + } + + //Math.Max(_min, value); + } +} \ No newline at end of file diff --git a/src/Myra/Utility/Types/RecordReference.cs b/src/Myra/Utility/Types/RecordReference.cs new file mode 100644 index 00000000..c24fa4ec --- /dev/null +++ b/src/Myra/Utility/Types/RecordReference.cs @@ -0,0 +1,108 @@ +using System; +using Myra.Graphics2D.UI.Properties; + +namespace Myra.Utility.Types +{ + public abstract class RecordReference : IRecordReference + { + protected RecordReference(Record backer) + { + Record = backer; + } + + public Record Record { get; } + public virtual Type Type => Record?.Type; + public virtual bool IsReadOnly + { + get + { + if (Record == null) + return true; + return Record.HasSetter; + } + } + + public void SetValue(object field, object value) => Internal_Set(field, value); + protected virtual void Internal_Set(object field, object value) + { + if (IsReadOnly) + return; + Record?.SetValue(field, value); + } + protected virtual object Internal_Get(object field) => Record?.GetValue(field); + object IRecordReference.GetValue(object field) => Internal_Get(field); + } + public abstract class RecordReference : RecordReference, IRecordReference + { + protected RecordReference(Record backer) : base(backer) + { + + } + + public T GetValue(object field) + { + object obj = Internal_Get(field); + return (T)obj; + } + + public void SetValue(object field, T value) + { + Internal_Set(field, value); + } + } + + public abstract class StructRecordReference : RecordReference, IStructTypeRef where T : struct + { + static StructRecordReference() + { + isNullable = TypeHelper.Info.IsNullable; + realType = TypeHelper.GetNullableTypeOrPassThrough(); + } + + // ReSharper disable once StaticMemberInGenericType + private static readonly bool isNullable; + public bool IsNullable => isNullable; + // ReSharper disable once StaticMemberInGenericType + private static readonly Type realType; + public override Type Type => realType; + + protected StructRecordReference(Record backer) : base(backer) + { + + } + + } + + public abstract class NumberReference : StructRecordReference, INumberTypeRef where T : struct + { + protected NumberReference(Record backer) : base(backer) + { + + } + } + + public sealed class WholeNumberRef : NumberReference, IWholeNumberTypeRef where T : struct + { + static WholeNumberRef() + { + if (TypeHelper.Info.IsWholeNumber == false) + throw new ArgumentException($"Invalid Generic Argument: {typeof(T)}"); + } + public WholeNumberRef(Record backer) : base(backer) + { + + } + } + public sealed class FracNumberRef : NumberReference, IFracNumberTypeRef where T : struct + { + static FracNumberRef() + { + if (TypeHelper.Info.IsFractionalNumber == false) + throw new ArgumentException($"Invalid Generic Argument: {typeof(T)}"); + } + public FracNumberRef(Record backer) : base(backer) + { + + } + } +} \ No newline at end of file diff --git a/src/Myra/Utility/Types/TypeHelper.cs b/src/Myra/Utility/Types/TypeHelper.cs new file mode 100644 index 00000000..c844888c --- /dev/null +++ b/src/Myra/Utility/Types/TypeHelper.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Myra.Utility.Types +{ + public static class TypeHelper + { + private static Dictionary> _lookup; + internal static void RegisterHelperForType() + { + if(_lookup == null) + _lookup = new Dictionary>(16); + + Type helpingType = typeof(T); + _lookup.Add(helpingType, () => TypeHelper.Info); + } + public static bool TryGetInfoForType(Type type, out TypeInfo result) + { + if (_lookup != null) + { + if (_lookup.TryGetValue(type, out var getterFunc)) + { + result = getterFunc.Invoke(); + return (int)result.Code > 0; + } + } + result = default; + return false; + } + } + + /// + /// Provides static helpers and cached info about a generic type . + /// Accessing a type via this class will load a static + /// + internal static class TypeHelper + { + static TypeHelper() + { + _type = typeof(T); + _info = new TypeInfo(_type); + TypeHelper.RegisterHelperForType(); + } + + private static readonly Type _type; + private static readonly TypeInfo _info; + + /// + /// Cached information about generic type . + /// + public static TypeInfo Info => _info; + + /// + /// If is , return the generic type the nullable holds, else return type . + /// + public static Type GetNullableTypeOrPassThrough() + { + if (!_info.IsNullable) + return _type; + return _type.GenericTypeArguments[0]; + } + + public static bool CanAssign(Type other) => _type.IsAssignableFrom(other); + } +} \ No newline at end of file diff --git a/src/Myra/Utility/Types/TypeInfo.cs b/src/Myra/Utility/Types/TypeInfo.cs new file mode 100644 index 00000000..e11d6541 --- /dev/null +++ b/src/Myra/Utility/Types/TypeInfo.cs @@ -0,0 +1,81 @@ +using System; + +namespace Myra.Utility.Types +{ + public readonly struct TypeInfo + { + public readonly TypeCode Code; + + /// + public readonly bool IsClass; + /// + public readonly bool IsInterface; + /// + public readonly bool IsValue; + /// + /// If true, the type is a basic .Net primitive type. + /// + public readonly bool IsPrimitive; + /// + /// True if: + /// The type contained inside a . + /// The type is any other complex class or object. + /// + public readonly bool IsNullable; + + /// + /// If true, the type represents a numerical .Net value. This value is based off . + /// + public readonly bool IsNumber; + /// + /// If true, the type represents a numerical .Net value that can be negative. This value is based off . + /// + public readonly bool IsSignedNumber; + /// + /// If true, the type represents an integer-based numerical .Net value. This value is based off . + /// + public readonly bool IsWholeNumber; + /// + /// If true, the type represents a floating-point numerical .Net value. This value is based off . + /// + public readonly bool IsFractionalNumber; + + internal TypeInfo(Type type) + { + Code = Type.GetTypeCode(type); + + IsNumber = false; + IsSignedNumber = false; + IsWholeNumber = false; + IsFractionalNumber = false; + + IsPrimitive = type.IsPrimitive; + IsValue = type.IsValueType; + IsInterface = type.IsInterface; + IsClass = type.IsClass; + + if (IsClass) + { + // The type is a class or delegate + IsNullable = true; + } + else + { + // The type is a value or interface + if (IsInterface) + { + IsNullable = true; + } + else + { + IsNullable = type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + + IsNumber = Code.IsNumericType(); + IsSignedNumber = Code.IsSigned(); + IsWholeNumber = Code.IsNumericInteger(); + IsFractionalNumber = Code.IsNumericFractional(); + } + } + } + } +} \ No newline at end of file From 3bf13024ba7cb3f64b83757dba944a43b737fda0 Mon Sep 17 00:00:00 2001 From: Bamboy Date: Wed, 11 Feb 2026 21:14:13 -0800 Subject: [PATCH 09/28] Add Generic.Math library --- src/Myra/Myra.MonoGame.csproj | 1 + src/Myra/Myra.PlatformAgnostic.csproj | 1 + src/Myra/MyraAssetManagerExtensions.cs | 2 +- src/Myra/Utility/Types/GenericMathExtra.cs | 56 ++++++++++++++++++++++ src/Myra/Utility/Types/Range.cs | 49 ++++++++----------- src/Myra/Utility/Types/TypeHelper.cs | 2 +- 6 files changed, 79 insertions(+), 32 deletions(-) create mode 100644 src/Myra/Utility/Types/GenericMathExtra.cs diff --git a/src/Myra/Myra.MonoGame.csproj b/src/Myra/Myra.MonoGame.csproj index 4d1fd827..93481749 100644 --- a/src/Myra/Myra.MonoGame.csproj +++ b/src/Myra/Myra.MonoGame.csproj @@ -61,6 +61,7 @@ + diff --git a/src/Myra/Myra.PlatformAgnostic.csproj b/src/Myra/Myra.PlatformAgnostic.csproj index 0a18ed3e..ad21a377 100644 --- a/src/Myra/Myra.PlatformAgnostic.csproj +++ b/src/Myra/Myra.PlatformAgnostic.csproj @@ -18,6 +18,7 @@ + \ No newline at end of file diff --git a/src/Myra/MyraAssetManagerExtensions.cs b/src/Myra/MyraAssetManagerExtensions.cs index 20a33e90..80ebcd3f 100644 --- a/src/Myra/MyraAssetManagerExtensions.cs +++ b/src/Myra/MyraAssetManagerExtensions.cs @@ -21,7 +21,7 @@ namespace AssetManagementBase { - public static class MyraAssetManagerExtensions + public static partial class MyraAssetManagerExtensions { private static AssetLoader _atlasLoader = (manager, assetName, settings, tag) => { diff --git a/src/Myra/Utility/Types/GenericMathExtra.cs b/src/Myra/Utility/Types/GenericMathExtra.cs new file mode 100644 index 00000000..024e3273 --- /dev/null +++ b/src/Myra/Utility/Types/GenericMathExtra.cs @@ -0,0 +1,56 @@ +using System; +using Generic.Math; + +namespace Myra.Utility.Types +{ + /// + /// Supplemental generic math methods for . + /// + public static class GenericMathExtra where TNum : struct + { + static GenericMathExtra() + { + Type arg = typeof(TNum); + TypeInfo info = TypeHelper.Info; + + if(info.IsNullable) + throw new ArgumentException($"Invalid Generic-Type Argument: '{arg}', Nullable types are unsupported"); + if(!info.IsNumber) + throw new ArgumentException($"Invalid Generic-Type Argument: '{arg}', Only numeric types are supported"); + } + + public static TNum Min(TNum lhs, TNum rhs) + => GenericMath.LessThan(lhs, rhs) ? lhs : rhs; + public static TNum Max(TNum lhs, TNum rhs) + => GenericMath.GreaterThan(lhs, rhs) ? lhs : rhs; + public static TNum Clamp(TNum value, TNum minValue, TNum maxValue) + { + if (GenericMath.Equal(minValue, maxValue)) + return minValue; + + TNum min = Min(minValue, maxValue); + TNum max = Max(minValue, maxValue); + + if (GenericMath.LessThanOrEqual(value, min)) + return min; + if (GenericMath.GreaterThanOrEqual(value, max)) + return max; + return value; + } + public static TNum Clamp(TNum value, TNum? minValue, TNum? maxValue) + { + bool limitMin = minValue.HasValue, limitMax = maxValue.HasValue; + if (limitMin & limitMax) + return Clamp(value, minValue.Value, maxValue.Value); + if (!limitMin & !limitMax) + return value; + + // limitMin != limitMax... + if (limitMin && GenericMath.LessThanOrEqual(value, minValue.Value)) + return minValue.Value; + if (limitMax && GenericMath.GreaterThanOrEqual(value, maxValue.Value)) + return maxValue.Value; + return value; + } + } +} \ No newline at end of file diff --git a/src/Myra/Utility/Types/Range.cs b/src/Myra/Utility/Types/Range.cs index 102c34c0..5e817dbd 100644 --- a/src/Myra/Utility/Types/Range.cs +++ b/src/Myra/Utility/Types/Range.cs @@ -1,59 +1,48 @@ using System; +using Generic.Math; namespace Myra.Utility.Types { - public struct Range where TNumber : struct + /// + /// Represents a generic value range with optional minimum and maximum clamp. + /// + public struct Range where TNum : struct { static Range() { - Type arg = typeof(TNumber); - TypeInfo info = TypeHelper.Info; + Type arg = typeof(TNum); + TypeInfo info = TypeHelper.Info; if(info.IsNullable) throw new ArgumentException($"Invalid Generic-Type Argument: '{arg}', Nullable types are unsupported"); if(!info.IsNumber) throw new ArgumentException($"Invalid Generic-Type Argument: '{arg}', Only numeric types are supported"); - } - // TODO import https://github.com/HelloKitty/Generic.Math - public Range(TNumber min, TNumber max) + public Range(TNum min, TNum max) { - _applyMin = true; - _applyMax = true; _min = min; _max = max; } - public Range(TNumber? min = null, TNumber? max = null) + public Range(TNum? min = null, TNum? max = null) { - _applyMin = min.HasValue; - _applyMax = max.HasValue; - _min = _applyMin ? min.Value : default; - _max = _applyMax ? max.Value : default; + _min = min; + _max = max; } + + private TNum? _min, _max; - private bool _applyMin, _applyMax; - private TNumber _min, _max; - - public TNumber? Min + public TNum? Min { get => _min; - set - { - _applyMin = value.HasValue; - _min = value ?? default; - } + set => _min = value; } - public TNumber? Max + public TNum? Max { get => _max; - set - { - _applyMax = value.HasValue; - _max = value ?? default; - } + set => _max = value; } - - //Math.Max(_min, value); + + public TNum Clamp(TNum value) => GenericMathExtra.Clamp(value, _min, _max); } } \ No newline at end of file diff --git a/src/Myra/Utility/Types/TypeHelper.cs b/src/Myra/Utility/Types/TypeHelper.cs index c844888c..aee535ca 100644 --- a/src/Myra/Utility/Types/TypeHelper.cs +++ b/src/Myra/Utility/Types/TypeHelper.cs @@ -32,7 +32,7 @@ public static bool TryGetInfoForType(Type type, out TypeInfo result) /// /// Provides static helpers and cached info about a generic type . - /// Accessing a type via this class will load a static + /// Accessing a type via this class will load a associated with that type. /// internal static class TypeHelper { From 69bb02a7ea80db320d55980b9d5b05479bd3b518 Mon Sep 17 00:00:00 2001 From: Bamboy Date: Thu, 12 Feb 2026 07:49:17 -0800 Subject: [PATCH 10/28] SpinButton now uses a generic data type. --- .../Myra.Samples.Inspector.csproj | 4 + samples/Myra.Samples.Inspector/RootWidgets.cs | 1 + .../SomeTypesInAClass.cs | 15 + .../UI/Properties/BooleanPropertyEditor.cs | 6 +- .../UI/Properties/CollectionPropertyEditor.cs | 2 +- .../UI/Properties/EditorTypeRegistry.cs | 28 +- src/Myra/Graphics2D/UI/Properties/Editors.cs | 28 +- .../UI/Properties/NumericPropertyEditor.cs | 100 +++--- .../UI/Properties/PropertyEditor.cs | 13 +- src/Myra/Graphics2D/UI/Range/SpinButton.cs | 318 +++++++----------- src/Myra/Utility/Types/GenericMathExtra.cs | 17 +- src/Myra/Utility/Types/IRecordReference.cs | 7 + src/Myra/Utility/Types/Range.cs | 8 + src/Myra/Utility/Types/TypeHelper.cs | 59 ++++ 14 files changed, 329 insertions(+), 277 deletions(-) diff --git a/samples/Myra.Samples.Inspector/Myra.Samples.Inspector.csproj b/samples/Myra.Samples.Inspector/Myra.Samples.Inspector.csproj index 30259a76..eceba115 100644 --- a/samples/Myra.Samples.Inspector/Myra.Samples.Inspector.csproj +++ b/samples/Myra.Samples.Inspector/Myra.Samples.Inspector.csproj @@ -14,4 +14,8 @@ + + + + diff --git a/samples/Myra.Samples.Inspector/RootWidgets.cs b/samples/Myra.Samples.Inspector/RootWidgets.cs index dc36eea8..b550b8fa 100644 --- a/samples/Myra.Samples.Inspector/RootWidgets.cs +++ b/samples/Myra.Samples.Inspector/RootWidgets.cs @@ -40,6 +40,7 @@ private List BuildInspectables() return new List() { new SomeTypesInAClass(), + new SomeNumerics(), this, propertyGrid, InspectGame.Instance diff --git a/samples/Myra.Samples.Inspector/SomeTypesInAClass.cs b/samples/Myra.Samples.Inspector/SomeTypesInAClass.cs index 821c5c3c..268a2a58 100644 --- a/samples/Myra.Samples.Inspector/SomeTypesInAClass.cs +++ b/samples/Myra.Samples.Inspector/SomeTypesInAClass.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using Microsoft.Xna.Framework; @@ -24,4 +25,18 @@ public class SomeTypesInAClass public byte @byte = 250; } + public class SomeNumerics + { + public byte @byte = 250; + public sbyte @sbyte = -43; + public short @short = short.MaxValue - 4; + public ushort @ushort = 333; + public int @int = -30; + public uint @uint = 30; + public long @long = 0; + public ulong @ulong = ulong.MaxValue - 4; + public float @float = 1.0f; + public double @double = Math.PI; + public decimal @decimal = decimal.MinusOne; + } } \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/BooleanPropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/BooleanPropertyEditor.cs index 2f555cc5..34d08827 100644 --- a/src/Myra/Graphics2D/UI/Properties/BooleanPropertyEditor.cs +++ b/src/Myra/Graphics2D/UI/Properties/BooleanPropertyEditor.cs @@ -1,7 +1,9 @@ +using Myra.Utility.Types; + namespace Myra.Graphics2D.UI.Properties { [PropertyEditor(typeof(BooleanPropertyEditor), typeof(bool))] - public sealed class BooleanPropertyEditor : PropertyEditor + public sealed class BooleanPropertyEditor : PropertyEditor, IStructTypeRef { public BooleanPropertyEditor(IInspector owner, Record methodInfo) : base(owner, methodInfo) { @@ -46,5 +48,7 @@ private bool CreateCheckBox(out Widget widget) widget = cb; return true; } + + bool IStructTypeRef.IsNullable => false; } } \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/CollectionPropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/CollectionPropertyEditor.cs index 0670aaa6..f44ed39b 100644 --- a/src/Myra/Graphics2D/UI/Properties/CollectionPropertyEditor.cs +++ b/src/Myra/Graphics2D/UI/Properties/CollectionPropertyEditor.cs @@ -4,7 +4,7 @@ namespace Myra.Graphics2D.UI.Properties { - [PropertyEditor(typeof(CollectionPropertyEditor), typeof(IList))] + //[PropertyEditor(typeof(CollectionPropertyEditor), typeof(IList))] public class CollectionPropertyEditor : PropertyEditor { private readonly Type collectionKind; diff --git a/src/Myra/Graphics2D/UI/Properties/EditorTypeRegistry.cs b/src/Myra/Graphics2D/UI/Properties/EditorTypeRegistry.cs index dbf6daa5..aaad34d7 100644 --- a/src/Myra/Graphics2D/UI/Properties/EditorTypeRegistry.cs +++ b/src/Myra/Graphics2D/UI/Properties/EditorTypeRegistry.cs @@ -6,34 +6,36 @@ namespace Myra.Graphics2D.UI.Properties { - public sealed class EditorTypeRegistry + internal sealed class EditorTypeRegistry { - public readonly Type EditorType; - private readonly Type[] Types; - private readonly string[] TypeNames; - + private readonly Type _editorType; + private readonly Type[] _types; + private readonly string[] _typeNames; + public Type EditorType => _editorType; + public bool IsOpenGenericType => _editorType.IsGenericTypeDefinition; + public EditorTypeRegistry(Type editorType, params Type[] propertyTypes) { - EditorType = editorType; - Types = propertyTypes; - TypeNames = TypeToString(propertyTypes); + _editorType = editorType; + _types = propertyTypes; + _typeNames = TypeToString(propertyTypes); } public bool CanEditType(Type type, bool allowCasts = true) { if (!allowCasts) { - for (int i = 0; i < Types.Length; i++) + for (int i = 0; i < _types.Length; i++) { - if (object.ReferenceEquals(Types[i], type)) + if (object.ReferenceEquals(_types[i], type)) return true; } return CanEditType( TypeToString(type) ); } - for (int i = 0; i < Types.Length; i++) + for (int i = 0; i < _types.Length; i++) { - Type supported = Types[i]; + Type supported = _types[i]; if (object.ReferenceEquals(supported, type)) return true; if (supported.IsInterface && supported.IsAssignableFrom(type)) @@ -43,7 +45,7 @@ public bool CanEditType(Type type, bool allowCasts = true) } public bool CanEditType(string value) { - foreach (string supported in TypeNames) + foreach (string supported in _typeNames) { if(StringComparer.InvariantCultureIgnoreCase.Equals(supported, value)) return true; diff --git a/src/Myra/Graphics2D/UI/Properties/Editors.cs b/src/Myra/Graphics2D/UI/Properties/Editors.cs index 940f56b9..f1462413 100644 --- a/src/Myra/Graphics2D/UI/Properties/Editors.cs +++ b/src/Myra/Graphics2D/UI/Properties/Editors.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reflection; using Myra.Utility; +using Myra.Utility.Types; namespace Myra.Graphics2D.UI.Properties { @@ -17,21 +18,36 @@ internal static bool TryGetEditorTypeForType(Type propertyKind, out Type editorT if (!_init) InitializeRegistry(); - for (int i = 0; i < _registry.Count; i++) + foreach (EditorTypeRegistry reg in _registry) { - if (_registry[i].CanEditType(propertyKind)) + if (reg.CanEditType(propertyKind)) { - editorType = _registry[i].EditorType; + if (reg.IsOpenGenericType) + { + TypeHelper.GetNullableTypeOrPassThrough(ref propertyKind); + editorType = reg.EditorType.MakeGenericType(propertyKind); + } + else + { + editorType = reg.EditorType; + } return true; } } string str = EditorTypeRegistry.TypeToString(propertyKind); - for (int i = 0; i < _registry.Count; i++) + foreach (EditorTypeRegistry reg in _registry) { - if (_registry[i].CanEditType(str)) + if (reg.CanEditType(str)) { - editorType = _registry[i].EditorType; + if (reg.IsOpenGenericType) + { + editorType = reg.EditorType.MakeGenericType(propertyKind); + } + else + { + editorType = reg.EditorType; + } return true; } } diff --git a/src/Myra/Graphics2D/UI/Properties/NumericPropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/NumericPropertyEditor.cs index 875ba5ef..a7eae939 100644 --- a/src/Myra/Graphics2D/UI/Properties/NumericPropertyEditor.cs +++ b/src/Myra/Graphics2D/UI/Properties/NumericPropertyEditor.cs @@ -1,28 +1,35 @@ using System; +using Generic.Math; +using Myra.Graphics2D.UI.Styles; using Myra.Utility; +using Myra.Utility.Types; namespace Myra.Graphics2D.UI.Properties { - [PropertyEditor(typeof(NumericPropertyEditor), + /// + /// + /// + /// + [PropertyEditor(typeof(NumericPropertyEditor<>), typeof(byte), typeof(sbyte), typeof(byte?), typeof(sbyte?), typeof(short), typeof(ushort), typeof(short?), typeof(ushort?), typeof(int), typeof(uint), typeof(int?), typeof(uint?), typeof(long), typeof(ulong), typeof(long?), typeof(ulong?), - typeof(float), typeof(float?), typeof(double), typeof(double?))] - public sealed class NumericPropertyEditor : PropertyEditor + typeof(float), typeof(float?), typeof(double), typeof(double?), + typeof(decimal), typeof(decimal?))] + public sealed class NumericPropertyEditor : PropertyEditor, INumberTypeRef where TNum : struct { public NumericPropertyEditor(IInspector owner, Record methodInfo) : base(owner, methodInfo) { - + if(methodInfo == null) + throw new NullReferenceException(nameof(methodInfo)); + //if (typeof(TNum) != methodInfo.Type) + // throw new ArgumentException($"Type mismatch: Record '{methodInfo.Type}', Generic '{typeof(TNum)}'"); } protected override bool TryCreateEditorWidget(out Widget widget) { return CreateNumericEditor(_record, out widget); - //if (CreatorPicker(attributes, out var func)) - // return func.Invoke(methodInfo, out widget); - widget = null; - return false; } private bool CreateNumericEditor(Record record, out Widget widget) @@ -33,20 +40,26 @@ private bool CreateNumericEditor(Record record, out Widget widget) return false; } - var propertyType = record.Type; - object value = record.GetValue(_owner.SelectedField); - - var numericType = propertyType; - if (propertyType.IsNullablePrimitive()) - { - numericType = propertyType.GetNullableType(); - } - - var spinButton = new SpinButton + Type type = record.Type; + bool isNullable = type.IsNullablePrimitive(); + + object obj = record.GetValue(_owner.SelectedField); + //TypeHelper.GetNullableTypeOrPassThrough(ref type); + TNum? convert; + if (isNullable) + { + convert = (TNum?)obj; + } + else + { + convert = (TNum)obj; + } + //obj = Convert.ChangeType(obj, type); + + var spinButton = new SpinButton() { - Integer = numericType.IsNumericInteger(), - Nullable = propertyType.IsNullablePrimitive(), - Value = value != null ? (float)Convert.ChangeType(value, typeof(float)) : default(float?) + Nullable = isNullable, + Value = convert, }; if (_record.HasSetter) @@ -55,45 +68,13 @@ private bool CreateNumericEditor(Record record, out Widget widget) { try { - object result; - - if (spinButton.Value != null) - { - result = Convert.ChangeType(spinButton.Value.Value, numericType); - } - else - { - result = null; - } - - SetValue(_owner.SelectedField, result); - - if (record.Type.IsValueType) - { - // Handle structs - /* - var tg = this; - var pg = tg._parentGrid; - while (pg != null && tg._parentProperty != null && tg._parentProperty.Type.IsValueType) - { - tg._parentProperty.SetValue(pg._object, tg._object); - - if (!tg._parentProperty.Type.IsValueType) - { - break; - } - - tg = pg; - pg = tg._parentGrid; - }*/ - } + if(IsNullable) + SetValue(_owner.SelectedField, args.NewValue); + else + SetValue(_owner.SelectedField, args.NewValue.GetValueOrDefault()); _owner.FireChanged(record.Name); } - catch (InvalidCastException) - { - // TODO: Rework this ugly type conversion solution - } catch (Exception ex) { spinButton.Value = args.OldValue; @@ -110,6 +91,9 @@ private bool CreateNumericEditor(Record record, out Widget widget) widget = spinButton; return true; } - + + public TNum GetValue(object field) => (TNum)_record.GetValue(field); + public void SetValue(object field, TNum value) => base.SetValue(field, value); + public bool IsNullable => TypeHelper.Info.IsNullable; } } \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs index e6af86c8..3ecfbe73 100644 --- a/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs +++ b/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs @@ -17,11 +17,11 @@ public PropertyEditorAttribute(Type attached, params Type[] editTypes) this.attached = attached; this.editTypes = editTypes; } - public EditorTypeRegistry GetRegistry() => new EditorTypeRegistry(attached, editTypes); + internal EditorTypeRegistry GetRegistry() => new EditorTypeRegistry(attached, editTypes); } /// - /// Encapsulates a widget and .Net property or field, for the purposes of display or editing by the user. + /// Encapsulates a (UI element) and (.Net property or field), for the purposes of display or editing by the user. /// public abstract class PropertyEditor : IRecordReference { @@ -83,16 +83,15 @@ protected PropertyEditor(IInspector owner, Record methodInfo) private bool TryCreateWidget(out Widget widget) => TryCreateEditorWidget(out widget); protected abstract bool TryCreateEditorWidget(out Widget widget); - - public Type Type => _record.Type; - object IRecordReference.GetValue(object field) => _record.GetValue(field); - Record IRecordReference.Record => _record; - bool IRecordReference.IsReadOnly => !_record.HasSetter; public void SetValue(object field, object value) { _record.SetValue(field, value); _owner.FireChanged(_record.Name); } + public Type Type => _record.Type; + object IRecordReference.GetValue(object field) => _record.GetValue(field); + Record IRecordReference.Record => _record; + bool IRecordReference.IsReadOnly => !_record.HasSetter; } /// diff --git a/src/Myra/Graphics2D/UI/Range/SpinButton.cs b/src/Myra/Graphics2D/UI/Range/SpinButton.cs index e865e6aa..6c13cba3 100644 --- a/src/Myra/Graphics2D/UI/Range/SpinButton.cs +++ b/src/Myra/Graphics2D/UI/Range/SpinButton.cs @@ -1,9 +1,12 @@ using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; using System.ComponentModel; -using System.Linq; using Myra.Graphics2D.UI.Styles; using System.Xml.Serialization; +using Generic.Math; using Myra.Events; +using Myra.Utility.Types; #if MONOGAME || FNA using Microsoft.Xna.Framework.Input; @@ -15,28 +18,20 @@ namespace Myra.Graphics2D.UI { - public class SpinButton : Widget + public class SpinButton : Widget where TNum : struct { private readonly GridLayout _layout = new GridLayout(); private readonly TextBox _textField; private readonly Button _upButton; private readonly Button _downButton; - private bool _integer = false; - private int _decimalPlaces = 0; - private float _increment = 1f; + private int _decimalPlaces; + private TNum _increment; + private Range _range; [Category("Behavior")] [DefaultValue(false)] public bool Nullable { get; set; } - [Category("Behavior")] - [DefaultValue(null)] - public float? Maximum { get; set; } - - [Category("Behavior")] - [DefaultValue(null)] - public float? Minimum { get; set; } - [DefaultValue(HorizontalAlignment.Left)] public override HorizontalAlignment HorizontalAlignment { @@ -62,20 +57,27 @@ public override VerticalAlignment VerticalAlignment base.VerticalAlignment = value; } } - + [Category("Behavior")] - [DefaultValue(0.0f)] - public float? Value + [DefaultValue(null)] + public TNum? Minimum { get => _range.Min; set => _range.Min = value; } + + [Category("Behavior")] + [DefaultValue(null)] + public TNum? Maximum { get => _range.Max; set => _range.Max = value; } + + [Category("Behavior")] + [DefaultValue(null)] + public TNum? Value { get { if (string.IsNullOrEmpty(_textField.Text)) { - return Nullable ? default(float?) : 0.0f; + return Nullable ? default(TNum?) : GenericMathExtra.Zero; } - float result; - if (float.TryParse(_textField.Text, out result)) + if(TypeHelper.TryParse(_textField.Text, out TNum result)) { return result; } @@ -90,35 +92,31 @@ public float? Value throw new Exception("value can't be null when Nullable is false"); } - if (value.HasValue && Minimum.HasValue && value.Value < Minimum.Value) + if (value.HasValue) { - throw new Exception("Value can't be lower than Minimum"); - } - - if (value.HasValue && Maximum.HasValue && value.Value > Maximum.Value) - { - throw new Exception("Value can't be higher than Maximum"); + value = _range.Clamp(value.Value); } if (FixedNumberSize) { + throw new NotImplementedException(); string MajorString = ""; int k = 0; int k2 = 0; if (Maximum.HasValue) { - k = Math.Abs(Maximum.Value).ToString().Count(); + k = GenericMathExtra.Abs(Maximum.Value).ToString().Length; } if (Minimum.HasValue) { - k2 = Math.Abs(Minimum.Value).ToString().Count(); + k2 = GenericMathExtra.Abs(Minimum.Value).ToString().Length; } k = k > k2 ? k : k2; for (int i = 0; i < k; i++) { MajorString += "0"; } - if (value.HasValue && value.Value >= 0) + if (value.HasValue && GenericMath.GreaterThanOrEqual(value.Value, GenericMathExtra.Zero)) { MajorString = " " + MajorString; } @@ -127,7 +125,7 @@ public float? Value { MinorString += "0"; } - _textField.Text = value.HasValue ? value.Value.ToString(MajorString + MinorString) : string.Empty; + //_textField.Text = value.HasValue ? value.Value.ToString(MajorString + MinorString) : string.Empty; } else { @@ -142,26 +140,22 @@ public float? Value } [Category("Behavior")] - [DefaultValue(1f)] - public float Increment + [DefaultValue(1)] + public TNum Increment { get { return _increment; } - set { - if (Integer) - { - _increment = (int)value; - } - else - { - _increment = value; - } + _increment = value; } } + + [Category("Behavior")] + [DefaultValue(1)] + public TNum Mul_Increment { get; set; } [Category("Behavior")] [DefaultValue(0)] @@ -174,7 +168,7 @@ public int DecimalPlaces set { - if (Integer) + if (TypeHelper.Info.IsWholeNumber) { _decimalPlaces = 0; } @@ -189,30 +183,6 @@ public int DecimalPlaces [DefaultValue(false)] public bool FixedNumberSize { get; set; } - [Category("Behavior")] - [DefaultValue(false)] - public bool Integer - { - get - { - return _integer; - } - - set - { - _integer = value; - if (Integer) - { - _increment = (int)_increment; - Value = (int)Value; - } - } - } - - [Category("Behavior")] - [DefaultValue(1f)] - public float Mul_Increment { get; set; } = 1f; - [XmlIgnore] [Browsable(false)] public TextBox TextBox => _textField; @@ -223,17 +193,17 @@ public bool Integer /// Fires when the value is about to be changed /// Set Cancel to true if you want to cancel the change /// - public event EventHandler> ValueChanging; + public event EventHandler> ValueChanging; /// /// Fires when the value had been changed /// - public event EventHandler> ValueChanged; + public event EventHandler> ValueChanged; /// /// Fires only when the value had been changed by user(doesnt fire if it had been assigned through code) /// - public event EventHandler> ValueChangedByUser; + public event EventHandler> ValueChangedByUser; public SpinButton(string styleName = Stylesheet.DefaultStyleName) { @@ -293,64 +263,68 @@ public SpinButton(string styleName = Stylesheet.DefaultStyleName) Children.Add(_downButton); SetStyle(styleName); - - Value = 0; + + Value = GenericMathExtra.Zero; + Increment = GenericMathExtra.One; + Mul_Increment = GenericMathExtra.One; } - private static float? StringToFloat(string s) + private static TNum? StringToNumber(string str) { - if (string.IsNullOrEmpty(s)) + if (string.IsNullOrEmpty(str)) { return null; } - float f; - if (!float.TryParse(s, out f)) + if (TypeHelper.TryParse(str, out TNum value)) { - return null; + return value; } - return f; + return null; } - private string NumberToString(float? v) + private string NumberToString(TNum? value) { - if (v == null) + if (value.HasValue) { - if (Nullable) - { - return string.Empty; - } - - // Default value - return "0"; + return value.Value.ToString(); } - if (Integer) + if (Nullable) { - return ((int)v.Value).ToString(); + return string.Empty; } - - return v.Value.ToString(); + // Default value + return "0"; } private void _textField_ValueChanging(object sender, ValueChangingEventArgs e) { - var s = e.NewValue; - if (string.IsNullOrEmpty(s)) + var str = e.NewValue; + if (string.IsNullOrEmpty(str)) { } - else if (s == "-") + else if (str == "-") { // Allow prefix 'minus' only if Minimum lower than zero - if (Minimum != null && Minimum.Value >= 0) + if (Minimum.HasValue && GenericMath.GreaterThanOrEqual(Minimum.Value, GenericMathExtra.Zero)) { e.Cancel = true; } } else { - float? newValue = null; + TNum? newValue = null; + if (TypeHelper.TryParse(str, out TNum num) && _range.IsInRange(num)) + { + newValue = num; + } + else + { + e.Cancel = true; + } + /* if (Integer) { int i; @@ -390,17 +364,13 @@ private void _textField_ValueChanging(object sender, ValueChangingEventArgs(Value, newValue); - ValueChanging(this, args); + var args = new ValueChangingEventArgs(Value, newValue); + ValueChanging.Invoke(this, args); if (args.Cancel) { e.Cancel = true; @@ -415,27 +385,12 @@ private void _textField_ValueChanging(object sender, ValueChangingEventArgs eventArgs) { - ValueChanged?.Invoke(this, new ValueChangedEventArgs(StringToFloat(eventArgs.OldValue), StringToFloat(eventArgs.NewValue))); + ValueChanged?.Invoke(this, new ValueChangedEventArgs(StringToNumber(eventArgs.OldValue), StringToNumber(eventArgs.NewValue))); } private void TextBoxOnTextChangedByUser(object sender, ValueChangedEventArgs eventArgs) { - ValueChangedByUser?.Invoke(this, new ValueChangedEventArgs(StringToFloat(eventArgs.OldValue), StringToFloat(eventArgs.NewValue))); - } - - private bool InRange(float value) - { - if (Minimum.HasValue && value < Minimum.Value) - { - return false; - } - - if (Maximum.HasValue && value > Maximum.Value) - { - return false; - } - - return true; + ValueChangedByUser?.Invoke(this, new ValueChangedEventArgs(StringToNumber(eventArgs.OldValue), StringToNumber(eventArgs.NewValue))); } public void ApplySpinButtonStyle(SpinButtonStyle style) @@ -463,94 +418,52 @@ protected override void InternalSetStyle(Stylesheet stylesheet, string name) ApplySpinButtonStyle(stylesheet.SpinButtonStyles.SafelyGetStyle(name)); } - private void UpButtonOnUp(object sender, EventArgs eventArgs) + private void SpinValue(bool spinUpward, bool isMouseWheel) { - float value; - if (!float.TryParse(_textField.Text, out value)) + TNum newValue, delta; + if (!TypeHelper.TryParse(_textField.Text, out newValue)) { - value = 0; + newValue = GenericMathExtra.Zero; } - value += _increment; - if (InRange(value)) - { - var changed = Value != value; - var oldValue = Value; - Value = value; - if (changed) - { - var ev = ValueChangedByUser; - if (ev != null) - { - ev(this, new ValueChangedEventArgs(oldValue, value)); - } - } - } - } - private void DownButtonOnUp(object sender, EventArgs eventArgs) - { - float value; - if (!float.TryParse(_textField.Text, out value)) - { - value = 0; - } + if (isMouseWheel) + delta = GenericMath.Multiply(_increment, Mul_Increment); + else + delta = _increment; - value -= _increment; - if (InRange(value)) + if (spinUpward) + newValue = GenericMath.Add(newValue, delta); + else + newValue = GenericMath.Subtract(newValue, delta); + + if (_range.IsInRange(newValue)) { - var changed = Value != value; - var oldValue = Value; - Value = value; + bool changed = GenericMath.NotEqual(Value.GetValueOrDefault(), newValue); + TNum? oldValue = Value; + Value = newValue; if (changed) { - var ev = ValueChangedByUser; - if (ev != null) - { - ev(this, new ValueChangedEventArgs(oldValue, value)); - } + ValueChangedByUser?.Invoke(this, new ValueChangedEventArgs(oldValue, newValue)); } } } + private void UpButtonOnUp(object sender, EventArgs eventArgs) + => SpinValue(true, false); + private void DownButtonOnUp(object sender, EventArgs eventArgs) + => SpinValue(false, false); public override void OnMouseWheel(float delta) { base.OnMouseWheel(delta); - float value; - if (!float.TryParse(_textField.Text, out value)) - { - value = 0; - } if (delta < 0 && _downButton.Visible && _downButton.Enabled) { - value -= _increment * Mul_Increment; - if (InRange(value)) - { - var changed = Value != value; - var oldValue = Value; - Value = value; - - if (changed) - { - ValueChangedByUser?.Invoke(this, new ValueChangedEventArgs(oldValue, value)); - } - } + SpinValue(false, true); } - else if (delta > 0 && _upButton.Visible && _upButton.Enabled) + else if(delta > 0 && _upButton.Visible && _upButton.Enabled) { - value += _increment * Mul_Increment; - if (InRange(value)) - { - var changed = Value != value; - var oldValue = Value; - Value = value; - - if (changed) - { - ValueChangedByUser?.Invoke(this, new ValueChangedEventArgs(oldValue, value)); - } - } + SpinValue(true, true); } } @@ -568,11 +481,11 @@ public override void OnLostKeyboardFocus() if (string.IsNullOrEmpty(_textField.Text) && !Nullable) { var defaultValue = "0"; - if (Minimum != null && Minimum.Value > 0) + if (Minimum.HasValue && GenericMath.GreaterThan(Minimum.Value, GenericMathExtra.Zero)) { defaultValue = NumberToString(Minimum.Value); } - else if (Maximum != null && Maximum.Value < 0) + else if (Maximum.HasValue && GenericMath.LessThan(Maximum.Value, GenericMathExtra.Zero)) { defaultValue = NumberToString(Maximum.Value); } @@ -601,7 +514,7 @@ protected internal override void CopyFrom(Widget w) { base.CopyFrom(w); - var spinButton = (SpinButton)w; + var spinButton = (SpinButton)w; Nullable = spinButton.Nullable; Minimum = spinButton.Minimum; @@ -610,8 +523,33 @@ protected internal override void CopyFrom(Widget w) Increment = spinButton.Increment; DecimalPlaces = spinButton.DecimalPlaces; FixedNumberSize = spinButton.FixedNumberSize; - Integer = spinButton.Integer; Mul_Increment = spinButton.Mul_Increment; } } + + // Helper static class for types + internal static class SpinButton + { + private static ReadOnlyDictionary> _typeCtors = new ReadOnlyDictionary>(new Dictionary> + { + { typeof(byte), () => new SpinButton() }, + { typeof(sbyte), () => new SpinButton() }, + { typeof(short), () => new SpinButton() }, + { typeof(ushort), () => new SpinButton() }, + { typeof(int), () => new SpinButton() }, + { typeof(uint), () => new SpinButton() }, + { typeof(long), () => new SpinButton() }, + { typeof(ulong), () => new SpinButton() }, + + { typeof(float), () => new SpinButton() }, + { typeof(double), () => new SpinButton() }, + { typeof(decimal), () => new SpinButton() }, + }); + + public static bool TryCreate(Type numberType, out Widget spinButton) + { + spinButton = default; + return default; + } + } } \ No newline at end of file diff --git a/src/Myra/Utility/Types/GenericMathExtra.cs b/src/Myra/Utility/Types/GenericMathExtra.cs index 024e3273..f00cf8ad 100644 --- a/src/Myra/Utility/Types/GenericMathExtra.cs +++ b/src/Myra/Utility/Types/GenericMathExtra.cs @@ -6,7 +6,7 @@ namespace Myra.Utility.Types /// /// Supplemental generic math methods for . /// - public static class GenericMathExtra where TNum : struct + internal static class GenericMathExtra where TNum : struct { static GenericMathExtra() { @@ -17,8 +17,23 @@ static GenericMathExtra() throw new ArgumentException($"Invalid Generic-Type Argument: '{arg}', Nullable types are unsupported"); if(!info.IsNumber) throw new ArgumentException($"Invalid Generic-Type Argument: '{arg}', Only numeric types are supported"); + + Zero = GenericMath.Zero; + One = GenericMath.Convert( 1 ); } + + public static readonly TNum Zero; + public static readonly TNum One; + public static TNum Abs(TNum value) + { + if (TypeHelper.Info.IsSignedNumber) + { + if (GenericMath.LessThan(value, GenericMath.Zero)) + value = GenericMath.Negate(value); + } + return value; + } public static TNum Min(TNum lhs, TNum rhs) => GenericMath.LessThan(lhs, rhs) ? lhs : rhs; public static TNum Max(TNum lhs, TNum rhs) diff --git a/src/Myra/Utility/Types/IRecordReference.cs b/src/Myra/Utility/Types/IRecordReference.cs index 4a8fe419..da38a7f3 100644 --- a/src/Myra/Utility/Types/IRecordReference.cs +++ b/src/Myra/Utility/Types/IRecordReference.cs @@ -3,6 +3,7 @@ namespace Myra.Utility.Types { + public interface IRecordReference { Record Record { get; } @@ -40,6 +41,12 @@ public interface IFracNumberTypeRef : INumberTypeRef where T : struct internal static class TypeInterfaceExtensions { + /// + /// Checks if the is null + /// + public static bool IsNull(this IRecordReference obj) + => obj == null || obj.Record == null; + public static TypeInfo TypeInfo(this IRecordReference obj) => TypeHelper.Info; public static bool IsNullable(this IStructTypeRef obj) where T : struct diff --git a/src/Myra/Utility/Types/Range.cs b/src/Myra/Utility/Types/Range.cs index 5e817dbd..687afe4f 100644 --- a/src/Myra/Utility/Types/Range.cs +++ b/src/Myra/Utility/Types/Range.cs @@ -43,6 +43,14 @@ public TNum? Max set => _max = value; } + public bool IsInRange(TNum value) + { + if (_min.HasValue && GenericMath.LessThan(value, _min.Value)) + return false; + if (_max.HasValue && GenericMath.GreaterThan(value, _max.Value)) + return false; + return true; + } public TNum Clamp(TNum value) => GenericMathExtra.Clamp(value, _min, _max); } } \ No newline at end of file diff --git a/src/Myra/Utility/Types/TypeHelper.cs b/src/Myra/Utility/Types/TypeHelper.cs index aee535ca..9d57a415 100644 --- a/src/Myra/Utility/Types/TypeHelper.cs +++ b/src/Myra/Utility/Types/TypeHelper.cs @@ -28,6 +28,15 @@ public static bool TryGetInfoForType(Type type, out TypeInfo result) result = default; return false; } + + /// + /// If is , return the generic type the nullable holds, else return type . + /// + public static void GetNullableTypeOrPassThrough(ref Type type) + { + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + type = type.GenericTypeArguments[0]; + } } /// @@ -41,6 +50,8 @@ static TypeHelper() _type = typeof(T); _info = new TypeInfo(_type); TypeHelper.RegisterHelperForType(); + + FindMethodTryParse(); } private static readonly Type _type; @@ -62,5 +73,53 @@ public static Type GetNullableTypeOrPassThrough() } public static bool CanAssign(Type other) => _type.IsAssignableFrom(other); + + private static MethodInfo _tryParse; + private static void FindMethodTryParse() + { + const string METHOD_NAME = "TryParse"; + Type type = typeof(T); + try + { + // public static bool TryParse(string str, out TData value) + _tryParse = type.GetMethod(METHOD_NAME, + BindingFlags.Static | BindingFlags.Public, null, + new[] { typeof(string), type.MakeByRefType() }, null); + + if (_tryParse == null) + { + throw new Exception($"Reflection Error: '{type.Name}' does not contain a public static method '{METHOD_NAME}'"); + } + } + catch (Exception e) + { + throw new Exception($"Unhandled Reflection Error: {e}"); + } + } + + /// + /// Attempts find and invoke 's static TryParse() method using Reflection. + /// + public static bool TryParse(string str, out T data) + { + if (_tryParse == null) + throw new NotSupportedException($"Not supported: {typeof(T)}.TryParse()"); + + // public static bool TryParse(string str, out TData value) + // The method's "out TData" gets written to object[] array. + object[] param = new object[] { str, default(T) }; + object result = _tryParse.Invoke(null, param); + + if (result is bool didConvert) + { + // Read what the method wrote as "out T" + data = didConvert ? (T)param[1] : default; + return didConvert; + } + + // We really might want to throw an exception here instead + data = default; + return false; + } } } \ No newline at end of file From 062aa2ea1e1742776c567349c6bb0e1a26eeca12 Mon Sep 17 00:00:00 2001 From: Bamboy Date: Fri, 13 Feb 2026 12:09:20 -0800 Subject: [PATCH 11/28] Refactor SpinButton ToString method to fit generic numbers, new MathHelper intermediate class, PropertyGrid bypass of byte and sbyte types due to lacking math ops --- samples/Myra.Samples.Inspector/InspectGame.cs | 1 - .../Myra.Samples.Inspector.csproj | 5 +- src/Myra/Graphics2D/UI/Properties/Editors.cs | 4 +- .../UI/Properties/NumericPropertyEditor.cs | 166 +++++--- .../UI/Properties/PropertyEditor.cs | 6 + src/Myra/Graphics2D/UI/Range/SpinButton.cs | 362 +++++++++++------- src/Myra/Utility/Types/GenericMathExtra.cs | 71 ---- src/Myra/Utility/Types/MathHelper.cs | 219 +++++++++++ src/Myra/Utility/Types/Range.cs | 20 +- 9 files changed, 592 insertions(+), 262 deletions(-) delete mode 100644 src/Myra/Utility/Types/GenericMathExtra.cs create mode 100644 src/Myra/Utility/Types/MathHelper.cs diff --git a/samples/Myra.Samples.Inspector/InspectGame.cs b/samples/Myra.Samples.Inspector/InspectGame.cs index 2bdd3662..6ff52b9f 100644 --- a/samples/Myra.Samples.Inspector/InspectGame.cs +++ b/samples/Myra.Samples.Inspector/InspectGame.cs @@ -31,7 +31,6 @@ public class InspectGame : Game public InspectGame() { Instance = this; - #if !STRIDE _graphics = new GraphicsDeviceManager(this) { diff --git a/samples/Myra.Samples.Inspector/Myra.Samples.Inspector.csproj b/samples/Myra.Samples.Inspector/Myra.Samples.Inspector.csproj index eceba115..b83e5de2 100644 --- a/samples/Myra.Samples.Inspector/Myra.Samples.Inspector.csproj +++ b/samples/Myra.Samples.Inspector/Myra.Samples.Inspector.csproj @@ -13,9 +13,12 @@ + - + + ..\..\src\Myra\bin\MonoGame\Debug\netstandard2.0\Myra.dll + diff --git a/src/Myra/Graphics2D/UI/Properties/Editors.cs b/src/Myra/Graphics2D/UI/Properties/Editors.cs index f1462413..785c53eb 100644 --- a/src/Myra/Graphics2D/UI/Properties/Editors.cs +++ b/src/Myra/Graphics2D/UI/Properties/Editors.cs @@ -34,7 +34,7 @@ internal static bool TryGetEditorTypeForType(Type propertyKind, out Type editorT return true; } } - + /* string str = EditorTypeRegistry.TypeToString(propertyKind); foreach (EditorTypeRegistry reg in _registry) { @@ -50,7 +50,7 @@ internal static bool TryGetEditorTypeForType(Type propertyKind, out Type editorT } return true; } - } + }*/ //typeof(IList).IsAssignableFrom(propertyKind) editorType = null; diff --git a/src/Myra/Graphics2D/UI/Properties/NumericPropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/NumericPropertyEditor.cs index a7eae939..25b8cfcd 100644 --- a/src/Myra/Graphics2D/UI/Properties/NumericPropertyEditor.cs +++ b/src/Myra/Graphics2D/UI/Properties/NumericPropertyEditor.cs @@ -1,5 +1,6 @@ using System; using Generic.Math; +using Myra.Events; using Myra.Graphics2D.UI.Styles; using Myra.Utility; using Myra.Utility.Types; @@ -18,21 +19,30 @@ namespace Myra.Graphics2D.UI.Properties typeof(float), typeof(float?), typeof(double), typeof(double?), typeof(decimal), typeof(decimal?))] public sealed class NumericPropertyEditor : PropertyEditor, INumberTypeRef where TNum : struct - { + { + private Type _type; + private bool _nullable; + private bool _valueConvert; + public NumericPropertyEditor(IInspector owner, Record methodInfo) : base(owner, methodInfo) { - if(methodInfo == null) - throw new NullReferenceException(nameof(methodInfo)); - //if (typeof(TNum) != methodInfo.Type) - // throw new ArgumentException($"Type mismatch: Record '{methodInfo.Type}', Generic '{typeof(TNum)}'"); } - + protected override void Initialize() + { + _type = _record.Type; + _nullable = _type.IsNullablePrimitive(); + + Type selfType = typeof(TNum); + _valueConvert = _type == typeof(byte) | _type == typeof(sbyte) | _type == typeof(byte?) | _type == typeof(sbyte?); + _valueConvert |= selfType == typeof(byte) | selfType == typeof(sbyte) | selfType == typeof(byte?) | selfType == typeof(sbyte?); + } + protected override bool TryCreateEditorWidget(out Widget widget) { - return CreateNumericEditor(_record, out widget); + return CreateNumericEditor(out widget); } - private bool CreateNumericEditor(Record record, out Widget widget) + private bool CreateNumericEditor(out Widget widget) { if (_owner.SelectedField == null) { @@ -40,60 +50,122 @@ private bool CreateNumericEditor(Record record, out Widget widget) return false; } - Type type = record.Type; - bool isNullable = type.IsNullablePrimitive(); - - object obj = record.GetValue(_owner.SelectedField); - //TypeHelper.GetNullableTypeOrPassThrough(ref type); + object obj = _record.GetValue(_owner.SelectedField); TNum? convert; - if (isNullable) + if (_nullable) { convert = (TNum?)obj; } else { - convert = (TNum)obj; + if (obj == null) + convert = MathHelper.Zero; + else + convert = (TNum)obj; } - //obj = Convert.ChangeType(obj, type); - + + if (_valueConvert) + { + widget = CreateByteDodge(convert); + } + else + { + widget = CreateNativeType(convert); + } + return true; + } + + private Widget CreateNativeType(TNum? val) + { var spinButton = new SpinButton() - { - Nullable = isNullable, - Value = convert, - }; + { + Nullable = _nullable, + Value = val, + }; - if (_record.HasSetter) - { - spinButton.ValueChanged += (sender, args) => - { - try - { - if(IsNullable) - SetValue(_owner.SelectedField, args.NewValue); + if (_record.HasSetter) + { + spinButton.ValueChanged += (sender, args) => + { + try + { + if (IsNullable) + SetValue(_owner.SelectedField, args.NewValue); else SetValue(_owner.SelectedField, args.NewValue.GetValueOrDefault()); - _owner.FireChanged(record.Name); - } - catch (Exception ex) - { - spinButton.Value = args.OldValue; - var dialog = Dialog.CreateMessageBox("Error", ex.ToString()); - dialog.ShowModal(_owner.Desktop); - } - }; - } - else - { - spinButton.Enabled = false; - } + _owner.FireChanged(_record.Name); + } + catch (Exception ex) + { + spinButton.Value = args.OldValue; + var dialog = Dialog.CreateMessageBox("Error", ex.ToString()); + dialog.ShowModal(_owner.Desktop); + } + }; + } + else + { + spinButton.Enabled = false; + } + return spinButton; + } - widget = spinButton; - return true; + private Widget CreateByteDodge(TNum? val) + { + // val is byte or sbyte which doesn't have math ops + var spinButton = new SpinButton() + { + Nullable = _nullable, + Value = GenericMath.Convert(val), + Minimum = 0, + Maximum = 255, + }; + + if (_record.HasSetter) + { + spinButton.ValueChanged += (sender, args) => + { + try + { + short? newShort = args.NewValue; + if (!newShort.HasValue) + { + if(IsNullable) + { + SetValue(_owner.SelectedField, null); + _owner.FireChanged(_record.Name); + return; + } + else + { + SetValue(_owner.SelectedField, GenericMath.Zero); + _owner.FireChanged(_record.Name); + return; + } + } + + newShort = MathHelper.Clamp(newShort.Value, 0, 255); + SetValue(_owner.SelectedField, GenericMath.Convert(newShort.Value)); + _owner.FireChanged(_record.Name); + } + catch (Exception ex) + { + spinButton.Value = args.OldValue; + var dialog = Dialog.CreateMessageBox("Error", ex.ToString()); + dialog.ShowModal(_owner.Desktop); + } + }; + } + else + { + spinButton.Enabled = false; + } + return spinButton; } - + public TNum GetValue(object field) => (TNum)_record.GetValue(field); public void SetValue(object field, TNum value) => base.SetValue(field, value); - public bool IsNullable => TypeHelper.Info.IsNullable; + public bool IsNullable => _nullable; } } \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs index 3ecfbe73..04538622 100644 --- a/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs +++ b/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs @@ -38,6 +38,7 @@ public static bool TryCreate(IInspector inspector, Record bindProperty, out Prop { if (Editors.TryGetEditorTypeForType(bindProperty.Type, out Type editorType)) { + // TODO mayble cache a lookup of known constructor-info objects? var ctor = editorType.GetConstructor(ActivatorTypeArgs); if (ctor != null) { @@ -75,11 +76,16 @@ public static bool TryCreate(IInspector inspector, Record bindProperty, out Prop /// protected PropertyEditor(IInspector owner, Record methodInfo) { + if(methodInfo == null) + throw new NullReferenceException(nameof(methodInfo)); _owner = owner; _record = methodInfo; + DoInit(); if (TryCreateWidget(out Widget editor)) Widget = editor; } + private void DoInit() => Initialize(); + protected virtual void Initialize() { } private bool TryCreateWidget(out Widget widget) => TryCreateEditorWidget(out widget); protected abstract bool TryCreateEditorWidget(out Widget widget); diff --git a/src/Myra/Graphics2D/UI/Range/SpinButton.cs b/src/Myra/Graphics2D/UI/Range/SpinButton.cs index 6c13cba3..8ba4d8f3 100644 --- a/src/Myra/Graphics2D/UI/Range/SpinButton.cs +++ b/src/Myra/Graphics2D/UI/Range/SpinButton.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; -using Myra.Graphics2D.UI.Styles; +using System.Globalization; using System.Xml.Serialization; -using Generic.Math; +using Myra.Graphics2D.UI.Styles; using Myra.Events; using Myra.Utility.Types; @@ -20,52 +20,155 @@ namespace Myra.Graphics2D.UI { public class SpinButton : Widget where TNum : struct { + private const string DefaultStringWhole = "0"; + private const string DefaultStringFloat = "0.0"; + private static readonly bool _numTypeHasSign; + private static readonly bool _numTypeIsFloating; + private static readonly string _defaultString; + private static readonly Func _strConverter; + + static SpinButton() //Static ctor on generics is invoked once per type-instance + { + Type arg = typeof(TNum); //Validate generic arg + if (arg == typeof(byte) || arg == typeof(sbyte)) + { + throw new ArgumentException($"Invalid Generic-Type Argument: '{arg}' does not have full math support. Convert to another type first"); + } + + TypeInfo info = TypeHelper.Info; + _numTypeHasSign = info.IsSignedNumber; + _numTypeIsFloating = info.IsFractionalNumber; + _defaultString = _numTypeIsFloating ? DefaultStringFloat : DefaultStringWhole; + + // Assign strConverter depending on TNum type. + // object.ToString() can't accept IFormatProvider... + switch (info.Code) + { + // Handle floating point types + case TypeCode.Single: + _strConverter = (TNum num, NumberFormatInfo formatter) => + { + if (num is float value) + return value.ToString(formatter); + throw new TypeAccessException(); + }; + break; + case TypeCode.Double: + _strConverter = (TNum num, NumberFormatInfo formatter) => + { + if (num is double value) + return value.ToString(formatter); + throw new TypeAccessException(); + }; + break; + case TypeCode.Decimal: + _strConverter = (TNum num, NumberFormatInfo formatter) => + { + if (num is decimal value) + return value.ToString(formatter); + throw new TypeAccessException(); + }; + break; + + // Handle signed whole number types + case TypeCode.Int16: + _strConverter = (TNum num, NumberFormatInfo formatter) => + { + if (num is short value) + return value.ToString(formatter); + throw new TypeAccessException(); + }; + break; + case TypeCode.Int32: + _strConverter = (TNum num, NumberFormatInfo formatter) => + { + if (num is int value) + return value.ToString(formatter); + throw new TypeAccessException(); + }; + break; + case TypeCode.Int64: + _strConverter = (TNum num, NumberFormatInfo formatter) => + { + if (num is long value) + return value.ToString(formatter); + throw new TypeAccessException(); + }; + break; + + // Handle unsigned whole number types + case TypeCode.UInt16: + _strConverter = (TNum num, NumberFormatInfo formatter) => + { + if (num is ushort value) + return value.ToString(formatter); + throw new TypeAccessException(); + }; + break; + case TypeCode.UInt32: + _strConverter = (TNum num, NumberFormatInfo formatter) => + { + if (num is uint value) + return value.ToString(formatter); + throw new TypeAccessException(); + }; + break; + case TypeCode.UInt64: + _strConverter = (TNum num, NumberFormatInfo formatter) => + { + if (num is ulong value) + return value.ToString(formatter); + throw new TypeAccessException(); + }; + break; + + default: + throw new TypeAccessException(); + } + } + private readonly GridLayout _layout = new GridLayout(); private readonly TextBox _textField; private readonly Button _upButton; private readonly Button _downButton; + private readonly NumberFormatInfo _formatter; private int _decimalPlaces; + private bool _fixedDigits; private TNum _increment; private Range _range; - [Category("Behavior")] - [DefaultValue(false)] - public bool Nullable { get; set; } - [DefaultValue(HorizontalAlignment.Left)] public override HorizontalAlignment HorizontalAlignment { - get - { - return base.HorizontalAlignment; - } - set - { - base.HorizontalAlignment = value; - } + get => base.HorizontalAlignment; + set => base.HorizontalAlignment = value; } [DefaultValue(VerticalAlignment.Top)] public override VerticalAlignment VerticalAlignment { - get - { - return base.VerticalAlignment; - } - set - { - base.VerticalAlignment = value; - } + get => base.VerticalAlignment; + set => base.VerticalAlignment = value; } + [Category("Behavior")] + [DefaultValue(false)] + public bool Nullable { get; set; } + [Category("Behavior")] [DefaultValue(null)] - public TNum? Minimum { get => _range.Min; set => _range.Min = value; } - + public TNum? Minimum + { + get => _range.Min; + set => _range.Min = value; + } [Category("Behavior")] [DefaultValue(null)] - public TNum? Maximum { get => _range.Max; set => _range.Max = value; } - + public TNum? Maximum + { + get => _range.Max; + set => _range.Max = value; + } [Category("Behavior")] [DefaultValue(null)] public TNum? Value @@ -74,7 +177,7 @@ public TNum? Value { if (string.IsNullOrEmpty(_textField.Text)) { - return Nullable ? default(TNum?) : GenericMathExtra.Zero; + return Nullable ? default(TNum?) : MathHelper.Zero; } if(TypeHelper.TryParse(_textField.Text, out TNum result)) @@ -87,50 +190,55 @@ public TNum? Value set { - if (value == null && !Nullable) + if (!value.HasValue && !Nullable) { - throw new Exception("value can't be null when Nullable is false"); + throw new Exception("Value can't be set null when Nullable is false"); } if (value.HasValue) { value = _range.Clamp(value.Value); } - + + _textField.Text = value.HasValue ? + _strConverter.Invoke(value.Value, _formatter) : + string.Empty; + /* if (FixedNumberSize) { throw new NotImplementedException(); - string MajorString = ""; + string majorString = ""; int k = 0; int k2 = 0; if (Maximum.HasValue) { - k = GenericMathExtra.Abs(Maximum.Value).ToString().Length; + k = MathHelper.Abs(Maximum.Value).ToString().Length; } if (Minimum.HasValue) { - k2 = GenericMathExtra.Abs(Minimum.Value).ToString().Length; + k2 = MathHelper.Abs(Minimum.Value).ToString().Length; } k = k > k2 ? k : k2; for (int i = 0; i < k; i++) { - MajorString += "0"; + majorString += ZeroChar; } - if (value.HasValue && GenericMath.GreaterThanOrEqual(value.Value, GenericMathExtra.Zero)) + if (value.HasValue && MathHelper.GreaterThanOrEqual(value.Value, MathHelper.Zero)) { - MajorString = " " + MajorString; + majorString = SpaceChar + majorString; } - string MinorString = "."; + + string minorString = DecimalChar.ToString(); for (int i = 0; i < _decimalPlaces; i++) { - MinorString += "0"; + minorString += ZeroChar; } //_textField.Text = value.HasValue ? value.Value.ToString(MajorString + MinorString) : string.Empty; } else { _textField.Text = value.HasValue ? value.Value.ToString() : string.Empty; - } + }*/ if (_textField.Text != null) { @@ -143,14 +251,8 @@ public TNum? Value [DefaultValue(1)] public TNum Increment { - get - { - return _increment; - } - set - { - _increment = value; - } + get => _increment; + set => _increment = value; } [Category("Behavior")] @@ -158,30 +260,32 @@ public TNum Increment public TNum Mul_Increment { get; set; } [Category("Behavior")] - [DefaultValue(0)] + [DefaultValue(2)] public int DecimalPlaces { - get - { - return _decimalPlaces; - } - + get => _fixedDigits ? _decimalPlaces : 0; set { - if (TypeHelper.Info.IsWholeNumber) - { - _decimalPlaces = 0; - } + if (_fixedDigits & _numTypeIsFloating) + value = Math.Abs(value); else - { - _decimalPlaces = value; - } + value = 0; + _decimalPlaces = value; + _formatter.NumberDecimalDigits = value; } } [Category("Behavior")] [DefaultValue(false)] - public bool FixedNumberSize { get; set; } + public bool FixedNumberSize + { + get => _fixedDigits; + set + { + _fixedDigits = value; + DecimalPlaces = _decimalPlaces; + } + } [XmlIgnore] [Browsable(false)] @@ -263,10 +367,11 @@ public SpinButton(string styleName = Stylesheet.DefaultStyleName) Children.Add(_downButton); SetStyle(styleName); - - Value = GenericMathExtra.Zero; - Increment = GenericMathExtra.One; - Mul_Increment = GenericMathExtra.One; + + _formatter = new NumberFormatInfo(); + Value = MathHelper.Zero; + Increment = MathHelper.One; + Mul_Increment = MathHelper.One; } private static TNum? StringToNumber(string str) @@ -284,31 +389,36 @@ public SpinButton(string styleName = Stylesheet.DefaultStyleName) return null; } - private string NumberToString(TNum? value) + private static string NumberToString(TNum? value, bool isNullable, NumberFormatInfo formatter) { + if(formatter == null) + formatter = NumberFormatInfo.CurrentInfo; + if (value.HasValue) { - return value.Value.ToString(); + TNum num = value.Value; + return _strConverter.Invoke(num, formatter); } - if (Nullable) + if (isNullable) { return string.Empty; } - // Default value - return "0"; + return _defaultString; // Default value } private void _textField_ValueChanging(object sender, ValueChangingEventArgs e) { - var str = e.NewValue; + string str = e.NewValue; if (string.IsNullOrEmpty(str)) { + return; } - else if (str == "-") + + if (str == "-") { // Allow prefix 'minus' only if Minimum lower than zero - if (Minimum.HasValue && GenericMath.GreaterThanOrEqual(Minimum.Value, GenericMathExtra.Zero)) + if (Minimum.HasValue && MathHelper.GreaterThanOrEqual(Minimum.Value, MathHelper.Zero)) { e.Cancel = true; } @@ -324,61 +434,19 @@ private void _textField_ValueChanging(object sender, ValueChangingEventArgs(Value, newValue); + ValueChanging.Invoke(this, args); + if (args.Cancel) { - int i; - if (!int.TryParse(s, out i)) - { - e.Cancel = true; - } - else - { - if ((Minimum != null && i < (int)Minimum) || - (Maximum != null && i > (int)Maximum)) - { - e.Cancel = true; - } - else - { - newValue = i; - } - } + e.Cancel = true; } else { - float f; - if (!float.TryParse(s, out f)) - { - e.Cancel = true; - } - else - { - if ((Minimum != null && f < Minimum) || - (Maximum != null && f > Maximum)) - { - e.Cancel = true; - } - else - { - newValue = f; - } - } - }*/ - - // Now SpinButton's - if (ValueChanging != null) - { - var args = new ValueChangingEventArgs(Value, newValue); - ValueChanging.Invoke(this, args); - if (args.Cancel) - { - e.Cancel = true; - } - else - { - e.NewValue = args.NewValue.HasValue ? NumberToString(args.NewValue) : null; - } + e.NewValue = args.NewValue.HasValue ? NumberToString(args.NewValue, Nullable, _formatter) : null; } } } @@ -423,22 +491,22 @@ private void SpinValue(bool spinUpward, bool isMouseWheel) TNum newValue, delta; if (!TypeHelper.TryParse(_textField.Text, out newValue)) { - newValue = GenericMathExtra.Zero; + newValue = MathHelper.Zero; } if (isMouseWheel) - delta = GenericMath.Multiply(_increment, Mul_Increment); + delta = MathHelper.Multiply(_increment, Mul_Increment); else delta = _increment; if (spinUpward) - newValue = GenericMath.Add(newValue, delta); + MathHelper.Add(ref newValue, delta); else - newValue = GenericMath.Subtract(newValue, delta); + MathHelper.Subtract(ref newValue, delta); if (_range.IsInRange(newValue)) { - bool changed = GenericMath.NotEqual(Value.GetValueOrDefault(), newValue); + bool changed = MathHelper.NotEqual(Value.GetValueOrDefault(), newValue); TNum? oldValue = Value; Value = newValue; @@ -480,17 +548,17 @@ public override void OnLostKeyboardFocus() if (string.IsNullOrEmpty(_textField.Text) && !Nullable) { - var defaultValue = "0"; - if (Minimum.HasValue && GenericMath.GreaterThan(Minimum.Value, GenericMathExtra.Zero)) + string textValue = _defaultString; + if (Minimum.HasValue && MathHelper.GreaterThan(Minimum.Value, MathHelper.Zero)) { - defaultValue = NumberToString(Minimum.Value); + textValue = NumberToString(Minimum.Value, Nullable, _formatter); } - else if (Maximum.HasValue && GenericMath.LessThan(Maximum.Value, GenericMathExtra.Zero)) + else if (Maximum.HasValue && MathHelper.LessThan(Maximum.Value, MathHelper.Zero)) { - defaultValue = NumberToString(Maximum.Value); + textValue = NumberToString(Maximum.Value, Nullable, _formatter); } - _textField.Text = defaultValue; + _textField.Text = textValue; } _textField.OnLostKeyboardFocus(); @@ -506,8 +574,28 @@ public override void OnKeyDown(Keys k) public override void OnChar(char c) { base.OnChar(c); - - _textField.OnChar(c); + + const char sign = '-'; + bool isNegSign = c == sign; + bool doPrint = false; + if (_numTypeHasSign) + { + if (isNegSign) + { + if (_textField.Text.Length == 0 || (_textField.CursorPosition == 0 && _textField.Text[0] != sign)) + doPrint = true; + } + else + doPrint = true; + } + else + { + if (!isNegSign) + doPrint = true; + } + + if(doPrint) + _textField.OnChar(c); } protected internal override void CopyFrom(Widget w) diff --git a/src/Myra/Utility/Types/GenericMathExtra.cs b/src/Myra/Utility/Types/GenericMathExtra.cs deleted file mode 100644 index f00cf8ad..00000000 --- a/src/Myra/Utility/Types/GenericMathExtra.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using Generic.Math; - -namespace Myra.Utility.Types -{ - /// - /// Supplemental generic math methods for . - /// - internal static class GenericMathExtra where TNum : struct - { - static GenericMathExtra() - { - Type arg = typeof(TNum); - TypeInfo info = TypeHelper.Info; - - if(info.IsNullable) - throw new ArgumentException($"Invalid Generic-Type Argument: '{arg}', Nullable types are unsupported"); - if(!info.IsNumber) - throw new ArgumentException($"Invalid Generic-Type Argument: '{arg}', Only numeric types are supported"); - - Zero = GenericMath.Zero; - One = GenericMath.Convert( 1 ); - } - - public static readonly TNum Zero; - public static readonly TNum One; - - public static TNum Abs(TNum value) - { - if (TypeHelper.Info.IsSignedNumber) - { - if (GenericMath.LessThan(value, GenericMath.Zero)) - value = GenericMath.Negate(value); - } - return value; - } - public static TNum Min(TNum lhs, TNum rhs) - => GenericMath.LessThan(lhs, rhs) ? lhs : rhs; - public static TNum Max(TNum lhs, TNum rhs) - => GenericMath.GreaterThan(lhs, rhs) ? lhs : rhs; - public static TNum Clamp(TNum value, TNum minValue, TNum maxValue) - { - if (GenericMath.Equal(minValue, maxValue)) - return minValue; - - TNum min = Min(minValue, maxValue); - TNum max = Max(minValue, maxValue); - - if (GenericMath.LessThanOrEqual(value, min)) - return min; - if (GenericMath.GreaterThanOrEqual(value, max)) - return max; - return value; - } - public static TNum Clamp(TNum value, TNum? minValue, TNum? maxValue) - { - bool limitMin = minValue.HasValue, limitMax = maxValue.HasValue; - if (limitMin & limitMax) - return Clamp(value, minValue.Value, maxValue.Value); - if (!limitMin & !limitMax) - return value; - - // limitMin != limitMax... - if (limitMin && GenericMath.LessThanOrEqual(value, minValue.Value)) - return minValue.Value; - if (limitMax && GenericMath.GreaterThanOrEqual(value, maxValue.Value)) - return maxValue.Value; - return value; - } - } -} \ No newline at end of file diff --git a/src/Myra/Utility/Types/MathHelper.cs b/src/Myra/Utility/Types/MathHelper.cs new file mode 100644 index 00000000..0875c343 --- /dev/null +++ b/src/Myra/Utility/Types/MathHelper.cs @@ -0,0 +1,219 @@ +using System; +using Generic.Math; + +namespace Myra.Utility.Types +{ + /// + /// Generic math methods for . + /// Uses which relies on Reflection.Emit and codegen. + /// + internal static class MathHelper where TNum : struct + { + // TODO Use generic math interfaces instead of GenericMath if the dotnet version supports it. + // The Generic.Math lib requires runtime codegen! + + private static readonly bool NumIsSigned; + + /// Value that represents 0 for + public static readonly TNum Zero; + /// Value that represents 1 for + public static readonly TNum One; + + static MathHelper() + { + Type arg = typeof(TNum); + TypeInfo info = TypeHelper.Info; + + if(info.IsNullable) + throw new ArgumentException($"Invalid Generic-Type Argument: '{arg}', Nullable types are not supported"); + if(!info.IsNumber) + throw new ArgumentException($"Invalid Generic-Type Argument: '{arg}', Only numeric types are supported"); + if(arg == typeof(byte) || arg == typeof(sbyte)) + throw new ArgumentException($"Invalid Generic-Type Argument: '{arg}' does not have full math support. Convert to another type first"); + + NumIsSigned = info.IsSignedNumber; + Zero = GenericMath.Zero; + One = GenericMath.Convert( 1 ); + } + + /// + /// Equivalent to operator: + + /// + public static TNum Add(TNum lhs, TNum rhs) => Add_Internal(lhs, rhs); + /// + /// Equivalent to operator: += + /// + public static void Add(ref TNum lhs, TNum rhs) => lhs = Add_Internal(lhs, rhs); + /// + /// Equivalent to operator: - + /// + public static TNum Subtract(TNum lhs, TNum rhs) => Subtract_Internal(lhs, rhs); + /// + /// Equivalent to operator: -= + /// + public static void Subtract(ref TNum lhs, TNum rhs) => lhs = Subtract_Internal(lhs, rhs); + /// + /// Equivalent to operator: * + /// + public static TNum Multiply(TNum lhs, TNum rhs) => Multiply_Internal(lhs, rhs); + /// + /// Equivalent to operator: *= + /// + public static void Multiply(ref TNum lhs, TNum rhs) => lhs = Multiply_Internal(lhs, rhs); + /// + /// Equivalent to operator: / + /// + public static TNum Divide(TNum lhs, TNum rhs) => Divide_Internal(lhs, rhs); + /// + /// Equivalent to operator: /= + /// + public static void Divide(ref TNum lhs, TNum rhs) => lhs = Divide_Internal(lhs, rhs); + + private static TNum Add_Internal(TNum lhs, TNum rhs) + { + return GenericMath.Add(lhs, rhs); + } + private static TNum Subtract_Internal(TNum lhs, TNum rhs) + { + return GenericMath.Subtract(lhs, rhs); + } + private static TNum Multiply_Internal(TNum lhs, TNum rhs) + { + return GenericMath.Multiply(lhs, rhs); + } + private static TNum Divide_Internal(TNum lhs, TNum rhs) + { + return GenericMath.Divide(lhs, rhs); + } + +#region Compare + /// + /// Equivalent to operator: == + /// + public static bool Equal(TNum lhs, TNum rhs) + { + return GenericMath.Equal(lhs, rhs); + } + /// + /// Equivalent to operator: != + /// + public static bool NotEqual(TNum lhs, TNum rhs) + { + return GenericMath.NotEqual(lhs, rhs); + } + /// + /// Equivalent to operator: < + /// + public static bool LessThan(TNum lhs, TNum rhs) + { + return GenericMath.LessThan(lhs, rhs); + } + /// + /// Equivalent to operator: <= + /// + public static bool LessThanOrEqual(TNum lhs, TNum rhs) + { + return GenericMath.LessThanOrEqual(lhs, rhs); + } + /// + /// Equivalent to operator: > + /// + public static bool GreaterThan(TNum lhs, TNum rhs) + { + return GenericMath.GreaterThan(lhs, rhs); + } + /// + /// Equivalent to operator: >= + /// + public static bool GreaterThanOrEqual(TNum lhs, TNum rhs) + { + return GenericMath.GreaterThanOrEqual(lhs, rhs); + } +#endregion + + /// + /// Returns the absolute positive value of . + /// Returns unchanged if does not support negatives. + /// + public static TNum Abs(TNum value) + { + if (NumIsSigned && LessThan(value, Zero)) + value = Negate_Internal(value); + return value; + } + /// + /// Equivalent to operator: - + /// Returns unchanged if does not support negatives. + /// + public static TNum Negate(TNum value) + { + if(NumIsSigned) + return Negate_Internal(value); + return value; + } + private static TNum Negate_Internal(TNum value) + { + return GenericMath.Negate(value); + } + + /// + /// Returns the smallest of two values. + /// + public static TNum Min(TNum lhs, TNum rhs) + => LessThan(lhs, rhs) ? lhs : rhs; + /// + /// Returns the largest of two values. + /// + public static TNum Max(TNum lhs, TNum rhs) + => GreaterThan(lhs, rhs) ? lhs : rhs; + + /// + /// Clamp between and . + /// + /// The value to limit. + /// The minimum range. (inclusive) If null, there will be no lower limit applied. + /// The maximum range. (inclusive) If null, there will be no upper limit applied. + public static TNum Clamp(TNum value, TNum? minValue, TNum? maxValue) + { + bool limitMin = minValue.HasValue, limitMax = maxValue.HasValue; + if (limitMin & limitMax) + return Clamp(value, minValue.Value, maxValue.Value); + if (!limitMin & !limitMax) + return value; + + // limitMin != limitMax... + if (limitMin && LessThanOrEqual(value, minValue.Value)) + return minValue.Value; + if (limitMax && GreaterThanOrEqual(value, maxValue.Value)) + return maxValue.Value; + return value; + } + /// + /// Clamp between and . + /// + /// The value to limit. + /// The minimum range. (inclusive) + /// The maximum range. (inclusive) + public static TNum Clamp(TNum value, TNum minValue, TNum maxValue) + { + if (Equal(minValue, maxValue)) + return minValue; + + if(GreaterThan(minValue, maxValue)) + SwapValues(ref minValue, ref maxValue); + + if (LessThanOrEqual(value, minValue)) + return minValue; + if (GreaterThanOrEqual(value, maxValue)) + return maxValue; + return value; + } + private static void SwapValues(ref TNum a, ref TNum b) + { + TNum c = a; + TNum d = b; + b = c; + a = d; + } + } +} \ No newline at end of file diff --git a/src/Myra/Utility/Types/Range.cs b/src/Myra/Utility/Types/Range.cs index 687afe4f..9f3fc7ef 100644 --- a/src/Myra/Utility/Types/Range.cs +++ b/src/Myra/Utility/Types/Range.cs @@ -43,14 +43,28 @@ public TNum? Max set => _max = value; } + public bool InsideMinBound(TNum value) + { + if (_min.HasValue && MathHelper.LessThan(value, _min.Value)) + return false; + return true; + } + public bool InsideMaxBound(TNum value) + { + if (_max.HasValue && MathHelper.GreaterThan(value, _max.Value)) + return false; + return true; + } + public bool IsInRange(TNum value) { - if (_min.HasValue && GenericMath.LessThan(value, _min.Value)) + if (_min.HasValue && MathHelper.LessThan(value, _min.Value)) return false; - if (_max.HasValue && GenericMath.GreaterThan(value, _max.Value)) + if (_max.HasValue && MathHelper.GreaterThan(value, _max.Value)) return false; return true; } - public TNum Clamp(TNum value) => GenericMathExtra.Clamp(value, _min, _max); + /// + public TNum Clamp(TNum value) => MathHelper.Clamp(value, _min, _max); } } \ No newline at end of file From f2128b661c65aeeaeeb6678e060a81ba211eedd2 Mon Sep 17 00:00:00 2001 From: Bamboy Date: Tue, 17 Feb 2026 17:28:33 -0800 Subject: [PATCH 12/28] Misc comments --- src/Myra/Graphics2D/UI/Range/SpinButton.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Myra/Graphics2D/UI/Range/SpinButton.cs b/src/Myra/Graphics2D/UI/Range/SpinButton.cs index 8ba4d8f3..ec258631 100644 --- a/src/Myra/Graphics2D/UI/Range/SpinButton.cs +++ b/src/Myra/Graphics2D/UI/Range/SpinButton.cs @@ -18,8 +18,14 @@ namespace Myra.Graphics2D.UI { + /// + /// An UI element that edits a numeric data type , with up and down buttons. + /// + /// The standard number-based value type to hold. + /// Generic cannot be or as they lack math operators and cannot be used.OR an unknown type was provided. public class SpinButton : Widget where TNum : struct { +#region Statics private const string DefaultStringWhole = "0"; private const string DefaultStringFloat = "0.0"; private static readonly bool _numTypeHasSign; @@ -32,7 +38,7 @@ static SpinButton() //Static ctor on generics is invoked once per type-instance Type arg = typeof(TNum); //Validate generic arg if (arg == typeof(byte) || arg == typeof(sbyte)) { - throw new ArgumentException($"Invalid Generic-Type Argument: '{arg}' does not have full math support. Convert to another type first"); + throw new TypeLoadException($"Invalid Generic-Type Argument: '{arg}' does not have full math support. Convert to another type first"); } TypeInfo info = TypeHelper.Info; @@ -123,10 +129,10 @@ static SpinButton() //Static ctor on generics is invoked once per type-instance break; default: - throw new TypeAccessException(); + throw new TypeLoadException($"Unknown generic numerical type: {arg}"); } } - +#endregion private readonly GridLayout _layout = new GridLayout(); private readonly TextBox _textField; private readonly Button _upButton; From d47171e2905d89ae1bedec0295b2c986042e220d Mon Sep 17 00:00:00 2001 From: Bamboy Date: Thu, 19 Feb 2026 00:46:35 -0800 Subject: [PATCH 13/28] MyraPad can now load legacy non-generic SpinButton type, and when loading XML now parses "SpinButton" "GenericTypeArg" into a constructed generic type. --- .../AllWidgets.Generated.cs | 2 +- samples/Myra.Samples.AllWidgets/AllWidgets.cs | 2 +- src/Myra/Graphics2D/UI/Project.cs | 34 ++++--- src/Myra/MML/LoadContext.cs | 90 ++++++++++++++----- src/MyraPad/AsyncTasksQueue.cs | 2 + src/MyraPad/UI/MainForm.cs | 2 +- 6 files changed, 92 insertions(+), 40 deletions(-) diff --git a/samples/Myra.Samples.AllWidgets/AllWidgets.Generated.cs b/samples/Myra.Samples.AllWidgets/AllWidgets.Generated.cs index 4ab42fe7..9dc056b9 100644 --- a/samples/Myra.Samples.AllWidgets/AllWidgets.Generated.cs +++ b/samples/Myra.Samples.AllWidgets/AllWidgets.Generated.cs @@ -272,7 +272,7 @@ private void BuildUI() label16.Text = "Spin Button:"; Grid.SetRow(label16, 9); - var spinButton1 = new SpinButton(); + var spinButton1 = new SpinButton(); spinButton1.Value = 1; spinButton1.Width = 100; Grid.SetColumn(spinButton1, 1); diff --git a/samples/Myra.Samples.AllWidgets/AllWidgets.cs b/samples/Myra.Samples.AllWidgets/AllWidgets.cs index 1f1817b7..34c799c7 100644 --- a/samples/Myra.Samples.AllWidgets/AllWidgets.cs +++ b/samples/Myra.Samples.AllWidgets/AllWidgets.cs @@ -133,7 +133,7 @@ public AllWidgets() Content = imageButtonContent }); node3.AddSubNode(new HorizontalSlider()); - node3.AddSubNode(new SpinButton()); + node3.AddSubNode(new SpinButton()); var imageButtonContent2 = new HorizontalStackPanel { diff --git a/src/Myra/Graphics2D/UI/Project.cs b/src/Myra/Graphics2D/UI/Project.cs index 1ce17b71..aacc5bc0 100644 --- a/src/Myra/Graphics2D/UI/Project.cs +++ b/src/Myra/Graphics2D/UI/Project.cs @@ -7,6 +7,7 @@ using System; using Myra.MML; using System.Collections.Generic; +using System.Collections.ObjectModel; using Myra.Attributes; using System.Linq; using Myra.Graphics2D.TextureAtlases; @@ -44,7 +45,7 @@ public ObjectPosition(object obj, int start, int end) public class Project { - private struct StylesheetChanger: IDisposable + private struct StylesheetChanger : IDisposable { private readonly Stylesheet _oldStylesheet; @@ -64,8 +65,21 @@ public void Dispose() public const string DefaultProportionName = "DefaultProportion"; public const string DefaultColumnProportionName = "DefaultColumnProportion"; public const string DefaultRowProportionName = "DefaultRowProportion"; - - private static readonly Dictionary LegacyClassNames = new Dictionary(); + + internal const string GenericTypeArgName = "GenericTypeArg"; + internal const char LegacySeparator = ':'; + internal const string LegacyClassToGeneric = "TO_GENERIC"; + internal const string LegacyToGenericDefaultType = "DEFAULT_TYPE"; + private static readonly ReadOnlyDictionary LegacyClassNames = new ReadOnlyDictionary(new Dictionary + { + {"VerticalBox", "VerticalStackPanel"}, + {"HorizontalBox", "HorizontalStackPanel"}, + {"TextField", "TextBox"}, + {"TextBlock", "Label"}, + {"ScrollPane", "ScrollViewer"}, + // SpinButton was made generic, default to SpinButton if using legacy format: + {"SpinButton", $"{LegacyClassToGeneric}{LegacySeparator}SpinButton<>{LegacySeparator}{LegacyToGenericDefaultType}{LegacySeparator}Single"}, + }); private readonly ExportOptions _exportOptions = new ExportOptions(); @@ -95,16 +109,7 @@ public ExportOptions ExportOptions [Browsable(false)] [XmlIgnore] public List> ObjectsNodes { get; internal set; } - - static Project() - { - LegacyClassNames["VerticalBox"] = "VerticalStackPanel"; - LegacyClassNames["HorizontalBox"] = "HorizontalStackPanel"; - LegacyClassNames["TextField"] = "TextBox"; - LegacyClassNames["TextBlock"] = "Label"; - LegacyClassNames["ScrollPane"] = "ScrollViewer"; - } - + public Project() { Stylesheet = Stylesheet.Current; @@ -208,11 +213,12 @@ internal static LoadContext CreateLoadContext(AssetManager assetManager) }; Dictionary assemblies = new Dictionary(ExtraWidgetAssembliesAndNamespaces); + assemblies.Add(typeof(float).Assembly, new string[] { typeof(float).Namespace }); assemblies.Add(typeof(Widget).Assembly, new string[] { typeof(Widget).Namespace, typeof(PropertyGrid).Namespace }); return new LoadContext { Assemblies = assemblies, - LegacyClassNames = LegacyClassNames, + LegacyClassNames = new Dictionary(LegacyClassNames), ObjectCreator = (t, el) => CreateItem(t, el), ResourceGetter = resourceGetter }; diff --git a/src/Myra/MML/LoadContext.cs b/src/Myra/MML/LoadContext.cs index fdd3d1fa..586e43b2 100644 --- a/src/Myra/MML/LoadContext.cs +++ b/src/Myra/MML/LoadContext.cs @@ -22,7 +22,7 @@ namespace Myra.MML { - internal class LoadContext: BaseContext + internal class LoadContext : BaseContext { struct SimplePropertyInfo { @@ -83,9 +83,9 @@ public void Load(object obj, XElement el, T handler) where T : class List complexProperties, simpleProperties; ParseProperties(type, false, out complexProperties, out simpleProperties); - + string newName; - foreach (var attr in el.Attributes()) + foreach (XAttribute attr in el.Attributes()) { var propertyName = attr.Name.ToString(); if (LegacyPropertyNames != null && LegacyPropertyNames.TryGetValue(propertyName, out newName)) @@ -217,14 +217,13 @@ public void Load(object obj, XElement el, T handler) where T : class } } - var contentProperty = (from p in complexProperties - where p.FindAttribute() - != null select p).FirstOrDefault(); + where p.FindAttribute() != null + select p).FirstOrDefault(); - foreach (var child in el.Elements()) + foreach (XElement child in el.Elements()) { - var childName = child.Name.ToString(); + string childName = child.Name.ToString(); if (NodesToIgnore != null && NodesToIgnore.Contains(childName)) { continue; @@ -309,27 +308,54 @@ where p.FindAttribute() } // Should be widget class name then - var widgetName = childName; - if (LegacyClassNames != null && LegacyClassNames.TryGetValue(widgetName, out newName)) - { - widgetName = newName; - } - - Type itemType = null; - foreach (var pair in Assemblies) + string widgetName = childName; + bool isGeneric = childName.EndsWith("<>"); + string genericTypeArg = null; + if (LegacyClassNames != null && LegacyClassNames.TryGetValue(widgetName, out string replace)) { - foreach (var ns in pair.Value) + if (replace.StartsWith(Project.LegacyClassToGeneric)) { - var widgetType = pair.Key.GetType(ns + "." + widgetName); - if (widgetType != null) + string[] split = replace.Split(new char[]{ Project.LegacySeparator }, StringSplitOptions.RemoveEmptyEntries); + isGeneric = split[1].EndsWith("<>"); + if (isGeneric) { - itemType = widgetType; - break; + XAttribute attr = child.Attribute(Project.GenericTypeArgName); + if (attr != null) + genericTypeArg = attr.Value; + else + { + for (int i = 0; i < split.Length - 1; i++) + { + if (split[i] == Project.LegacyToGenericDefaultType) + genericTypeArg = split[i + 1]; //Set a default fallback type arg (LEGACY) + } + } + widgetName = split[1].Replace("<>", "`1"); } } + else + { + widgetName = replace; + } + } + + Type itemType; + if (isGeneric) + { + if (!TryResolveType(widgetName, out itemType)) + { + throw new Exception($"Could not resolve open-generic type name: {widgetName}"); + } + if (!TryResolveType(genericTypeArg, out Type genericArg)) + { + throw new Exception($"Could not resolve generic argument type: {genericTypeArg} of {widgetName}"); + } - if (itemType != null) - break; + itemType = itemType.MakeGenericType(genericArg); + } + else + { + TryResolveType(widgetName, out itemType); } if (itemType != null) @@ -361,5 +387,23 @@ where p.FindAttribute() } } } + + private bool TryResolveType(string typeName, out Type itemType) + { + itemType = null; + foreach (var pair in Assemblies) + { + foreach (var ns in pair.Value) + { + var type = pair.Key.GetType(ns + "." + typeName); + if (type != null) + { + itemType = type; + return true; + } + } + } + return false; + } } } diff --git a/src/MyraPad/AsyncTasksQueue.cs b/src/MyraPad/AsyncTasksQueue.cs index 9b65cf5d..367a9e1c 100644 --- a/src/MyraPad/AsyncTasksQueue.cs +++ b/src/MyraPad/AsyncTasksQueue.cs @@ -62,6 +62,7 @@ private void RefreshProc(object state) { Studio.MainForm.QueueClearExplorer(); Studio.MainForm.QueueSetStatusText(ex.Message); + Console.WriteLine(ex); } } @@ -84,6 +85,7 @@ private void RefreshProc(object state) { Studio.MainForm.QueueClearExplorer(); Studio.MainForm.QueueSetStatusText(ex.Message); + Console.WriteLine(ex); } } } diff --git a/src/MyraPad/UI/MainForm.cs b/src/MyraPad/UI/MainForm.cs index a4750bee..cccdb769 100644 --- a/src/MyraPad/UI/MainForm.cs +++ b/src/MyraPad/UI/MainForm.cs @@ -69,7 +69,7 @@ public partial class MainForm }; private static readonly string[] SpecialContainers = new[] -{ + { "HorizontalMenu", "VerticalMenu", "TabControl", From 6c20bb42b37702ef6cf1a26fe139ab2728cb3514 Mon Sep 17 00:00:00 2001 From: Bamboy Date: Thu, 19 Feb 2026 02:15:31 -0800 Subject: [PATCH 14/28] MyraPad now auto-fills the generic type arg for SpinButton --- src/Myra/Graphics2D/UI/Project.cs | 2 +- src/MyraPad/UI/MainForm.cs | 148 +++++++++++++++++------------- 2 files changed, 86 insertions(+), 64 deletions(-) diff --git a/src/Myra/Graphics2D/UI/Project.cs b/src/Myra/Graphics2D/UI/Project.cs index aacc5bc0..e5430591 100644 --- a/src/Myra/Graphics2D/UI/Project.cs +++ b/src/Myra/Graphics2D/UI/Project.cs @@ -66,7 +66,7 @@ public void Dispose() public const string DefaultColumnProportionName = "DefaultColumnProportion"; public const string DefaultRowProportionName = "DefaultRowProportion"; - internal const string GenericTypeArgName = "GenericTypeArg"; + public const string GenericTypeArgName = "GenericTypeArg"; internal const char LegacySeparator = ':'; internal const string LegacyClassToGeneric = "TO_GENERIC"; internal const string LegacyToGenericDefaultType = "DEFAULT_TYPE"; diff --git a/src/MyraPad/UI/MainForm.cs b/src/MyraPad/UI/MainForm.cs index cccdb769..3443aacc 100644 --- a/src/MyraPad/UI/MainForm.cs +++ b/src/MyraPad/UI/MainForm.cs @@ -21,6 +21,7 @@ using System.Reflection; using Myra.Attributes; using System.Collections; +using System.Collections.ObjectModel; using System.Xml.Linq; using System.Xml; @@ -37,7 +38,7 @@ public partial class MainForm private static readonly string[] SimpleWidgets = new[] { "ImageTextButton", - "SpinButton", + "SpinButton<>", "HorizontalProgressBar", "VerticalProgressBar", "HorizontalSeparator", @@ -75,6 +76,12 @@ public partial class MainForm "TabControl", }; + private static readonly ReadOnlyDictionary GenericTypesLookup = //TODO use this for type lookup in auto-predict + new ReadOnlyDictionary(new Dictionary + { // The first entry is always used as a default. + {"SpinButton<>", new []{"Single", "Double", "Decimal", "Int16", "Int32", "Int64", "UInt16", "UInt32", "UInt64"}} + }); + private static readonly Regex TagResolver = new Regex("<([A-Za-z0-9\\.]+)"); private readonly AsyncTasksQueue _queue = new AsyncTasksQueue(); @@ -1000,81 +1007,96 @@ private void HandleAutoComplete() all = (from a in all where a.StartsWith(typed, StringComparison.OrdinalIgnoreCase) select a).ToList(); } - if (all.Count > 0) - { - var lastStartPos = _currentTagStart.Value; - var lastEndPos = cursorPos; - // Build context menu - _autoCompleteMenu = new VerticalMenu(); - foreach (var a in all) + if (all.Count <= 0) + return; + + var lastStartPos = _currentTagStart.Value; + var lastEndPos = cursorPos; + // Build context menu + _autoCompleteMenu = new VerticalMenu(); + foreach (string item in all) + { + var menuItem = new MenuItem { - var menuItem = new MenuItem - { - Text = a - }; + Text = item, + }; - menuItem.Selected += (s, args) => + menuItem.Selected += (s, args) => + { + int skip; + bool needsClose = false; + string result = "<"; + if (menuItem.Text.EndsWith("<>")) // Is generic? { - var result = "<" + menuItem.Text; - var skip = result.Length; - var needsClose = false; - - if (SimpleWidgets.Contains(menuItem.Text) || - Project.IsProportionName(menuItem.Text) || - menuItem.Text == MenuItemName || - menuItem.Text == ListItemName) - { - result += "/>"; - skip += 2; - } - else + const char quote = '"'; + // Remove the <> from the item name, while also appending GenericTypeArg="Type" + if (GenericTypesLookup.TryGetValue(menuItem.Text, out var strs)) { - result += ">"; - ++skip; - - if (Options.AutoIndent && Options.IndentSpacesSize > 0) - { - // Indent before cursor pos - result += "\n"; - var indentSize = Options.IndentSpacesSize * (_indentLevel + 1); - result += new string(' ', indentSize); - skip += indentSize; - - // Indent before closing tag - result += "\n"; - indentSize = Options.IndentSpacesSize * _indentLevel; - result += new string(' ', indentSize); - } - result += ""; - ++skip; - needsClose = true; + result += menuItem.Text.Replace("<>", $" {Project.GenericTypeArgName}={quote}{strs[0]}{quote}"); } + skip = result.Length; + } + else + { + result += menuItem.Text; + skip = result.Length; + } + + if (SimpleWidgets.Contains(menuItem.Text) || + Project.IsProportionName(menuItem.Text) || + menuItem.Text == MenuItemName || + menuItem.Text == ListItemName) + { + result += "/>"; + skip += 2; + } + else + { + result += ">"; + ++skip; - _textSource.Replace(lastStartPos, lastEndPos - lastStartPos, result); - _textSource.CursorPosition = lastStartPos + skip; - if (needsClose) + if (Options.AutoIndent && Options.IndentSpacesSize > 0) { - // _textSource.OnKeyDown(Keys.Enter); + // Indent before cursor pos + result += "\n"; + var indentSize = Options.IndentSpacesSize * (_indentLevel + 1); + result += new string(' ', indentSize); + skip += indentSize; + + // Indent before closing tag + result += "\n"; + indentSize = Options.IndentSpacesSize * _indentLevel; + result += new string(' ', indentSize); } - }; - - _autoCompleteMenu.Items.Add(menuItem); - } + result += ""; + ++skip; + needsClose = true; + } - var screen = _textSource.ToGlobal(_textSource.CursorCoords); - screen.Y += _textSource.Font.LineHeight; + _textSource.Replace(lastStartPos, lastEndPos - lastStartPos, result); + _textSource.CursorPosition = lastStartPos + skip; + if (needsClose) + { + // _textSource.OnKeyDown(Keys.Enter); + } + }; - if (_autoCompleteMenu.Items.Count > 0) - { - _autoCompleteMenu.HoverIndex = 0; - } + _autoCompleteMenu.Items.Add(menuItem); + } - Desktop.ShowContextMenu(_autoCompleteMenu, screen); - // Keep focus at text field - Desktop.FocusedKeyboardWidget = _textSource; + var screen = _textSource.ToGlobal(_textSource.CursorCoords); + screen.Y += _textSource.Font.LineHeight; - _refreshInitiated = null; + if (_autoCompleteMenu.Items.Count > 0) + { + _autoCompleteMenu.HoverIndex = 0; } + + Desktop.ShowContextMenu(_autoCompleteMenu, screen); + // Keep focus at text field + Desktop.FocusedKeyboardWidget = _textSource; + + _refreshInitiated = null; } } From 9a5aea6ad927e7ca38d9416177cad6abf8bf21e9 Mon Sep 17 00:00:00 2001 From: Bamboy Date: Fri, 20 Feb 2026 06:17:25 -0800 Subject: [PATCH 15/28] NumericPropertyEditor now handles nullables properly, misc --- .../Myra.Samples.Inspector.csproj | 2 - samples/Myra.Samples.Inspector/RootWidgets.cs | 22 ++++++--- .../Myra.Samples.Inspector/StringGenerator.cs | 6 ++- .../TestObjects/SomeNullableNumerics.cs | 19 ++++++++ .../TestObjects/SomeNumerics.cs | 19 ++++++++ .../{ => TestObjects}/SomeTypesInAClass.cs | 16 +------ .../UI/Properties/EditorTypeRegistry.cs | 8 ++++ src/Myra/Graphics2D/UI/Properties/Editors.cs | 21 +-------- .../UI/Properties/NumericPropertyEditor.cs | 45 +++++++++---------- .../UI/Properties/PropertyEditor.cs | 25 +++++------ src/Myra/Utility/Types/TypeHelper.cs | 17 +++++-- 11 files changed, 114 insertions(+), 86 deletions(-) create mode 100644 samples/Myra.Samples.Inspector/TestObjects/SomeNullableNumerics.cs create mode 100644 samples/Myra.Samples.Inspector/TestObjects/SomeNumerics.cs rename samples/Myra.Samples.Inspector/{ => TestObjects}/SomeTypesInAClass.cs (52%) diff --git a/samples/Myra.Samples.Inspector/Myra.Samples.Inspector.csproj b/samples/Myra.Samples.Inspector/Myra.Samples.Inspector.csproj index b83e5de2..72e10f60 100644 --- a/samples/Myra.Samples.Inspector/Myra.Samples.Inspector.csproj +++ b/samples/Myra.Samples.Inspector/Myra.Samples.Inspector.csproj @@ -7,13 +7,11 @@ - - diff --git a/samples/Myra.Samples.Inspector/RootWidgets.cs b/samples/Myra.Samples.Inspector/RootWidgets.cs index b550b8fa..b65b5c42 100644 --- a/samples/Myra.Samples.Inspector/RootWidgets.cs +++ b/samples/Myra.Samples.Inspector/RootWidgets.cs @@ -8,6 +8,7 @@ namespace Myra.Samples.Inspector { + // All the widgets for this sample project are children of this Widget. public class RootWidgets : HorizontalStackPanel { private Label headerLabel; @@ -15,7 +16,7 @@ public class RootWidgets : HorizontalStackPanel private PropertyGrid propertyGrid; private bool _init; - private int sel_i, inf_i; + private int select_index, info_index; public readonly List inspectables; public readonly List infos; private readonly StringGenerator headerInfo; @@ -28,25 +29,32 @@ public RootWidgets() infos = BuildInfoGenerators(); headerInfo = new StringGenerator(() => { - Type inspectedType = inspectables[sel_i].GetType(); - return $"\nInspecting object [{sel_i+1}] of [{inspectables.Count}]:\n\nType: {inspectedType.Name}\nin: {inspectedType.Namespace}\nBaseType: {inspectedType.BaseType?.Name}\n\nAssembly:\n{inspectedType.Assembly.GetName().Name}\n"; + Type inspectedType = inspectables[select_index].GetType(); + return $"\nInspecting object [{select_index+1}] of [{inspectables.Count}]:\n\nType: {inspectedType.Name}\nin: {inspectedType.Namespace}\nBaseType: {inspectedType.BaseType?.Name}\n\nAssembly:\n{inspectedType.Assembly.GetName().Name}\n"; }); Inspect(inspectables[0]); } + /// + /// Build the list of objects whose properties can be viewed + /// private List BuildInspectables() { return new List() { new SomeTypesInAClass(), new SomeNumerics(), + new SomeNullableNumerics(), this, propertyGrid, InspectGame.Instance }; } + /// + /// Build the list of info-outputs which generate strings + /// private List BuildInfoGenerators() { return new List() @@ -130,20 +138,20 @@ public void Inspect(object obj) public void SetSelectedIndex(int index) { - sel_i = index; + select_index = index; propertyGrid.Object = inspectables[index]; headerInfo.MarkDirty(); } public void SetInfoIndex(int index) { - inf_i = index; - infos[inf_i].MarkDirty(); + info_index = index; + infos[info_index].MarkDirty(); } public void OnPreRender() { headerLabel.Text = headerInfo.Text; - infoLabel.Text = infos[inf_i].Text; + infoLabel.Text = infos[info_index].Text; } } diff --git a/samples/Myra.Samples.Inspector/StringGenerator.cs b/samples/Myra.Samples.Inspector/StringGenerator.cs index 37680a7d..8cb47b30 100644 --- a/samples/Myra.Samples.Inspector/StringGenerator.cs +++ b/samples/Myra.Samples.Inspector/StringGenerator.cs @@ -2,7 +2,9 @@ namespace Myra.Samples.Inspector { - // Caches complicated strings and only regenerates them on a dirty-pattern basis. + /// + /// Caches generated strings and only regenerates them on a dirty-pattern basis. + /// public class StringGenerator { private readonly Func _textGenerator; @@ -24,7 +26,7 @@ public StringGenerator(Func generator) _textGenerator = generator; } - public bool MarkDirty() => _isDirty = true; + public void MarkDirty() => _isDirty = true; private void Clean() { _isDirty = false; diff --git a/samples/Myra.Samples.Inspector/TestObjects/SomeNullableNumerics.cs b/samples/Myra.Samples.Inspector/TestObjects/SomeNullableNumerics.cs new file mode 100644 index 00000000..c639a183 --- /dev/null +++ b/samples/Myra.Samples.Inspector/TestObjects/SomeNullableNumerics.cs @@ -0,0 +1,19 @@ +using System; + +namespace Myra.Samples.Inspector +{ + public class SomeNullableNumerics + { + public byte? @byte = null; + public sbyte? @sbyte = 100; + public short? @short = short.MaxValue; + public ushort? @ushort = 13; + public int? @int = null; + public uint? @uint = 353; + public long? @long = null; + public ulong? @ulong = ulong.MaxValue; + public float? @float = 1.14f; + public double? @double = Math.PI * 40; + public decimal? @decimal = decimal.MinusOne; + } +} \ No newline at end of file diff --git a/samples/Myra.Samples.Inspector/TestObjects/SomeNumerics.cs b/samples/Myra.Samples.Inspector/TestObjects/SomeNumerics.cs new file mode 100644 index 00000000..b2a1ca97 --- /dev/null +++ b/samples/Myra.Samples.Inspector/TestObjects/SomeNumerics.cs @@ -0,0 +1,19 @@ +using System; + +namespace Myra.Samples.Inspector +{ + public class SomeNumerics + { + public byte @byte = 250; + public sbyte @sbyte = -43; + public short @short = short.MaxValue - 4; + public ushort @ushort = 333; + public int @int = -30; + public uint @uint = 30; + public long @long = 0; + public ulong @ulong = ulong.MaxValue - 4; + public float @float = 1.0f; + public double @double = Math.PI; + public decimal @decimal = decimal.MinusOne; + } +} \ No newline at end of file diff --git a/samples/Myra.Samples.Inspector/SomeTypesInAClass.cs b/samples/Myra.Samples.Inspector/TestObjects/SomeTypesInAClass.cs similarity index 52% rename from samples/Myra.Samples.Inspector/SomeTypesInAClass.cs rename to samples/Myra.Samples.Inspector/TestObjects/SomeTypesInAClass.cs index 268a2a58..d0232c99 100644 --- a/samples/Myra.Samples.Inspector/SomeTypesInAClass.cs +++ b/samples/Myra.Samples.Inspector/TestObjects/SomeTypesInAClass.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using Microsoft.Xna.Framework; @@ -16,6 +15,7 @@ public class SomeTypesInAClass public MyEnum enumValue = MyEnum.Five; public bool checkbox = true; + public bool? nullableBool = null; public string stringValue = "Hotdog"; @@ -25,18 +25,4 @@ public class SomeTypesInAClass public byte @byte = 250; } - public class SomeNumerics - { - public byte @byte = 250; - public sbyte @sbyte = -43; - public short @short = short.MaxValue - 4; - public ushort @ushort = 333; - public int @int = -30; - public uint @uint = 30; - public long @long = 0; - public ulong @ulong = ulong.MaxValue - 4; - public float @float = 1.0f; - public double @double = Math.PI; - public decimal @decimal = decimal.MinusOne; - } } \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/EditorTypeRegistry.cs b/src/Myra/Graphics2D/UI/Properties/EditorTypeRegistry.cs index aaad34d7..7ae03787 100644 --- a/src/Myra/Graphics2D/UI/Properties/EditorTypeRegistry.cs +++ b/src/Myra/Graphics2D/UI/Properties/EditorTypeRegistry.cs @@ -12,6 +12,10 @@ internal sealed class EditorTypeRegistry private readonly Type[] _types; private readonly string[] _typeNames; public Type EditorType => _editorType; + + /// + /// Return true if the editor type is Generic and not yet complete. + /// public bool IsOpenGenericType => _editorType.IsGenericTypeDefinition; public EditorTypeRegistry(Type editorType, params Type[] propertyTypes) @@ -21,6 +25,10 @@ public EditorTypeRegistry(Type editorType, params Type[] propertyTypes) _typeNames = TypeToString(propertyTypes); } + /// + /// Returns true if this editor type can support . + /// + /// Allow casting to an intermediate type? (Like interfaces) public bool CanEditType(Type type, bool allowCasts = true) { if (!allowCasts) diff --git a/src/Myra/Graphics2D/UI/Properties/Editors.cs b/src/Myra/Graphics2D/UI/Properties/Editors.cs index 785c53eb..1383aedc 100644 --- a/src/Myra/Graphics2D/UI/Properties/Editors.cs +++ b/src/Myra/Graphics2D/UI/Properties/Editors.cs @@ -34,25 +34,7 @@ internal static bool TryGetEditorTypeForType(Type propertyKind, out Type editorT return true; } } - /* - string str = EditorTypeRegistry.TypeToString(propertyKind); - foreach (EditorTypeRegistry reg in _registry) - { - if (reg.CanEditType(str)) - { - if (reg.IsOpenGenericType) - { - editorType = reg.EditorType.MakeGenericType(propertyKind); - } - else - { - editorType = reg.EditorType; - } - return true; - } - }*/ - - //typeof(IList).IsAssignableFrom(propertyKind) + editorType = null; return false; } @@ -100,6 +82,5 @@ private static void Reflective_LoadTypeRegistry(int predictedCount, IEnumerable< } } } - } } \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/NumericPropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/NumericPropertyEditor.cs index 25b8cfcd..94db806f 100644 --- a/src/Myra/Graphics2D/UI/Properties/NumericPropertyEditor.cs +++ b/src/Myra/Graphics2D/UI/Properties/NumericPropertyEditor.cs @@ -7,10 +7,6 @@ namespace Myra.Graphics2D.UI.Properties { - /// - /// - /// - /// [PropertyEditor(typeof(NumericPropertyEditor<>), typeof(byte), typeof(sbyte), typeof(byte?), typeof(sbyte?), typeof(short), typeof(ushort), typeof(short?), typeof(ushort?), @@ -18,30 +14,37 @@ namespace Myra.Graphics2D.UI.Properties typeof(long), typeof(ulong), typeof(long?), typeof(ulong?), typeof(float), typeof(float?), typeof(double), typeof(double?), typeof(decimal), typeof(decimal?))] - public sealed class NumericPropertyEditor : PropertyEditor, INumberTypeRef where TNum : struct + public sealed class NumericPropertyEditor : PropertyEditor, INumberTypeRef where TNum : struct { - private Type _type; + private Type _underlyingType; private bool _nullable; - private bool _valueConvert; - + private bool _doByteDodge; + public bool IsNullable => _nullable; public NumericPropertyEditor(IInspector owner, Record methodInfo) : base(owner, methodInfo) { } protected override void Initialize() { - _type = _record.Type; - _nullable = _type.IsNullablePrimitive(); - + _underlyingType = Type; + _nullable = TypeHelper.GetNullableTypeOrPassThrough(ref _underlyingType); + _doByteDodge = _underlyingType == typeof(byte) | _underlyingType == typeof(sbyte); + Type selfType = typeof(TNum); - _valueConvert = _type == typeof(byte) | _type == typeof(sbyte) | _type == typeof(byte?) | _type == typeof(sbyte?); - _valueConvert |= selfType == typeof(byte) | selfType == typeof(sbyte) | selfType == typeof(byte?) | selfType == typeof(sbyte?); + Type copy = selfType; + TypeHelper.GetNullableTypeOrPassThrough(ref copy); + if (_underlyingType != copy) + throw new TypeLoadException($"Type mismatch: Generic type arg '{selfType}' is unequatable to assigned Record type '{Type}'"); + if (selfType.IsGenericType) + throw new TypeLoadException($"Generic types are not a valid generic argument for this type: {selfType}"); + if (Type.IsGenericType && Type.GetGenericTypeDefinition() != typeof(Nullable<>)) + throw new TypeLoadException($"Record provided is a generic but not Nullable: {Type}"); } - - protected override bool TryCreateEditorWidget(out Widget widget) + + protected override bool CreatorPicker(out WidgetCreatorDelegate creatorDelegate) { - return CreateNumericEditor(out widget); + creatorDelegate = CreateNumericEditor; + return true; } - private bool CreateNumericEditor(out Widget widget) { if (_owner.SelectedField == null) @@ -64,7 +67,7 @@ private bool CreateNumericEditor(out Widget widget) convert = (TNum)obj; } - if (_valueConvert) + if (_doByteDodge) { widget = CreateByteDodge(convert); } @@ -117,7 +120,7 @@ private Widget CreateByteDodge(TNum? val) var spinButton = new SpinButton() { Nullable = _nullable, - Value = GenericMath.Convert(val), + Value = GenericMath.Convert(val), Minimum = 0, Maximum = 255, }; @@ -163,9 +166,5 @@ private Widget CreateByteDodge(TNum? val) } return spinButton; } - - public TNum GetValue(object field) => (TNum)_record.GetValue(field); - public void SetValue(object field, TNum value) => base.SetValue(field, value); - public bool IsNullable => _nullable; } } \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs index 04538622..91eed932 100644 --- a/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs +++ b/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs @@ -31,7 +31,7 @@ public abstract class PropertyEditor : IRecordReference /// Initialize the - relationship registry. /// /// Internal editor-array alloc size. - /// The assemblies which are scanned for concrete inheritors of . Null will scan all assemblies in the current . + /// The assemblies which are scanned for concrete inheritors of . Any without are ignored. Null will scan all assemblies in the current . public static void InitializeRegistry(int predictedCount = 16, params System.Reflection.Assembly[] fromAssemblies) => Editors.InitializeRegistry(predictedCount, fromAssemblies); public static bool TryCreate(IInspector inspector, Record bindProperty, out PropertyEditor result) @@ -63,38 +63,38 @@ public static bool TryCreate(IInspector inspector, Record bindProperty, out Prop return false; } #endregion - protected delegate bool WidgetCreatorDelegate(out Widget widget); protected readonly IInspector _owner; protected readonly Record _record; - public Widget Widget { get; protected set; } + public Type Type => _record.Type; + public Widget Widget { get; protected set; } //TODO this is never going to be needed with how this class is used /// /// Creates a new widget attached to the given Record /// protected PropertyEditor(IInspector owner, Record methodInfo) { - if(methodInfo == null) - throw new NullReferenceException(nameof(methodInfo)); _owner = owner; - _record = methodInfo; + _record = methodInfo ?? throw new NullReferenceException(nameof(methodInfo)); DoInit(); if (TryCreateWidget(out Widget editor)) Widget = editor; } + private void DoInit() => Initialize(); protected virtual void Initialize() { } private bool TryCreateWidget(out Widget widget) => TryCreateEditorWidget(out widget); protected abstract bool TryCreateEditorWidget(out Widget widget); + public void SetValue(object field, object value) { _record.SetValue(field, value); _owner.FireChanged(_record.Name); } - public Type Type => _record.Type; + object IRecordReference.GetValue(object field) => _record.GetValue(field); Record IRecordReference.Record => _record; bool IRecordReference.IsReadOnly => !_record.HasSetter; @@ -103,9 +103,11 @@ public void SetValue(object field, object value) /// public abstract class PropertyEditor : PropertyEditor, IRecordReference { - protected abstract bool CreatorPicker(out WidgetCreatorDelegate creatorDelegate); + protected PropertyEditor(IInspector owner, Record methodInfo) : base(owner, methodInfo) + { + } - protected override bool TryCreateEditorWidget(out Widget widget) + protected sealed override bool TryCreateEditorWidget(out Widget widget) { if (CreatorPicker(out var func)) return func.Invoke(out widget); @@ -113,10 +115,7 @@ protected override bool TryCreateEditorWidget(out Widget widget) return false; } - protected PropertyEditor(IInspector owner, Record methodInfo) : base(owner, methodInfo) - { - - } + protected abstract bool CreatorPicker(out WidgetCreatorDelegate creatorDelegate); /// /// Gets the value from the field diff --git a/src/Myra/Utility/Types/TypeHelper.cs b/src/Myra/Utility/Types/TypeHelper.cs index 9d57a415..346c6b29 100644 --- a/src/Myra/Utility/Types/TypeHelper.cs +++ b/src/Myra/Utility/Types/TypeHelper.cs @@ -30,12 +30,21 @@ public static bool TryGetInfoForType(Type type, out TypeInfo result) } /// - /// If is , return the generic type the nullable holds, else return type . + /// If type is , return the generic type the nullable holds (T), else return type unchanged. Also returns boolean for if type was changed /// - public static void GetNullableTypeOrPassThrough(ref Type type) + public static bool GetNullableTypeOrPassThrough(ref Type type) { - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) - type = type.GenericTypeArguments[0]; + bool changed = false; + if (type.IsGenericType) + { + Type genericType = type.GetGenericTypeDefinition(); + if (genericType == typeof(Nullable<>)) + { + type = type.GenericTypeArguments[0]; + changed = true; + } + } + return changed; } } From 6887c5eeb2bef17b44b1bfdcae959ed7b3de098c Mon Sep 17 00:00:00 2001 From: Bamboy Date: Fri, 20 Feb 2026 09:16:02 -0800 Subject: [PATCH 16/28] EnumPropertyEditor is now working, more misc --- samples/Myra.Samples.Inspector/InspectGame.cs | 1 + samples/Myra.Samples.Inspector/RootWidgets.cs | 13 +++++- .../TestObjects/SomeChangingValues.cs | 18 +++++++++ .../UI/Properties/BooleanPropertyEditor.cs | 11 +++-- .../UI/Properties/EditorTypeRegistry.cs | 39 +++++++++++++----- .../UI/Properties/EnumPropertyEditor.cs | 5 ++- .../Graphics2D/UI/Properties/PropertyGrid.cs | 1 + src/Myra/Graphics2D/UI/Range/SpinButton.cs | 40 ++++++------------- 8 files changed, 83 insertions(+), 45 deletions(-) create mode 100644 samples/Myra.Samples.Inspector/TestObjects/SomeChangingValues.cs diff --git a/samples/Myra.Samples.Inspector/InspectGame.cs b/samples/Myra.Samples.Inspector/InspectGame.cs index 6ff52b9f..ce781b79 100644 --- a/samples/Myra.Samples.Inspector/InspectGame.cs +++ b/samples/Myra.Samples.Inspector/InspectGame.cs @@ -94,6 +94,7 @@ protected override void Update(GameTime gameTime) { base.Update(gameTime); _input.Update(Keyboard.GetState()); + _widgets.Update(gameTime); } protected override void Draw(GameTime gameTime) diff --git a/samples/Myra.Samples.Inspector/RootWidgets.cs b/samples/Myra.Samples.Inspector/RootWidgets.cs index b65b5c42..f7c55b46 100644 --- a/samples/Myra.Samples.Inspector/RootWidgets.cs +++ b/samples/Myra.Samples.Inspector/RootWidgets.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Reflection; using System.Text; +using Microsoft.Xna.Framework; using Myra.Graphics2D; using Myra.Graphics2D.UI; using Myra.Graphics2D.UI.Properties; @@ -20,11 +21,14 @@ public class RootWidgets : HorizontalStackPanel public readonly List inspectables; public readonly List infos; private readonly StringGenerator headerInfo; + + private readonly SomeChangingValues _changingValues; public RootWidgets() { BuildUI(); - + + _changingValues = new SomeChangingValues(); inspectables = BuildInspectables(); infos = BuildInfoGenerators(); headerInfo = new StringGenerator(() => @@ -46,6 +50,7 @@ private List BuildInspectables() new SomeTypesInAClass(), new SomeNumerics(), new SomeNullableNumerics(), + _changingValues, this, propertyGrid, InspectGame.Instance @@ -153,6 +158,10 @@ public void OnPreRender() headerLabel.Text = headerInfo.Text; infoLabel.Text = infos[info_index].Text; } - + + public void Update(GameTime time) + { + _changingValues.Update(time); + } } } \ No newline at end of file diff --git a/samples/Myra.Samples.Inspector/TestObjects/SomeChangingValues.cs b/samples/Myra.Samples.Inspector/TestObjects/SomeChangingValues.cs new file mode 100644 index 00000000..8aeb793a --- /dev/null +++ b/samples/Myra.Samples.Inspector/TestObjects/SomeChangingValues.cs @@ -0,0 +1,18 @@ +using Microsoft.Xna.Framework; + +namespace Myra.Samples.Inspector +{ + public class SomeChangingValues + { + public int tick; + public double Time { get; private set; } + public string EvenOrOdd { get; private set; } + + public void Update(GameTime time) + { + tick++; + Time = time.ElapsedGameTime.TotalSeconds; + EvenOrOdd = time.ElapsedGameTime.Seconds % 2 == 0 ? "Even" : "Odd"; + } + } +} \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/BooleanPropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/BooleanPropertyEditor.cs index 34d08827..8c230e25 100644 --- a/src/Myra/Graphics2D/UI/Properties/BooleanPropertyEditor.cs +++ b/src/Myra/Graphics2D/UI/Properties/BooleanPropertyEditor.cs @@ -1,3 +1,4 @@ +using System; using Myra.Utility.Types; namespace Myra.Graphics2D.UI.Properties @@ -5,9 +6,15 @@ namespace Myra.Graphics2D.UI.Properties [PropertyEditor(typeof(BooleanPropertyEditor), typeof(bool))] public sealed class BooleanPropertyEditor : PropertyEditor, IStructTypeRef { + private readonly bool _hasSetter; + private readonly bool _nullable; + public bool IsNullable => _nullable; + public BooleanPropertyEditor(IInspector owner, Record methodInfo) : base(owner, methodInfo) { - + Type type = methodInfo.Type; + _nullable = TypeHelper.GetNullableTypeOrPassThrough(ref type); + _hasSetter = methodInfo.HasSetter; } protected override bool CreatorPicker(out WidgetCreatorDelegate creatorDelegate) @@ -48,7 +55,5 @@ private bool CreateCheckBox(out Widget widget) widget = cb; return true; } - - bool IStructTypeRef.IsNullable => false; } } \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/EditorTypeRegistry.cs b/src/Myra/Graphics2D/UI/Properties/EditorTypeRegistry.cs index 7ae03787..f85e19a5 100644 --- a/src/Myra/Graphics2D/UI/Properties/EditorTypeRegistry.cs +++ b/src/Myra/Graphics2D/UI/Properties/EditorTypeRegistry.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using Myra.Utility; -using Myra.Utility.Types; namespace Myra.Graphics2D.UI.Properties { @@ -11,6 +10,7 @@ internal sealed class EditorTypeRegistry private readonly Type _editorType; private readonly Type[] _types; private readonly string[] _typeNames; + private readonly Func _extraTypeChecks; public Type EditorType => _editorType; /// @@ -23,8 +23,17 @@ public EditorTypeRegistry(Type editorType, params Type[] propertyTypes) _editorType = editorType; _types = propertyTypes; _typeNames = TypeToString(propertyTypes); - } + if (propertyTypes.Length > 0 && propertyTypes[0] == typeof(Enum)) + { + _extraTypeChecks = _enumCheck; + } + else + { + _extraTypeChecks = _falseCheck; + } + } + /// /// Returns true if this editor type can support . /// @@ -35,21 +44,26 @@ public bool CanEditType(Type type, bool allowCasts = true) { for (int i = 0; i < _types.Length; i++) { - if (object.ReferenceEquals(_types[i], type)) + Type supported = _types[i]; + if (object.ReferenceEquals(supported, type)) return true; } return CanEditType( TypeToString(type) ); } - - for (int i = 0; i < _types.Length; i++) + else { - Type supported = _types[i]; - if (object.ReferenceEquals(supported, type)) - return true; - if (supported.IsInterface && supported.IsAssignableFrom(type)) - return true; + for (int i = 0; i < _types.Length; i++) + { + Type supported = _types[i]; + if (object.ReferenceEquals(supported, type)) + return true; + if (supported.IsInterface && supported.IsAssignableFrom(type)) + return true; + if (_extraTypeChecks.Invoke(type)) + return true; + } + return CanEditType( TypeToString(type) ); } - return CanEditType( TypeToString(type) ); } public bool CanEditType(string value) { @@ -75,5 +89,8 @@ private static string[] TypeToString(params Type[] args) } return result.ToArray(); } + + private static bool _falseCheck(Type t) => false; + private static bool _enumCheck(Type t) => t.IsEnum; } } \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/EnumPropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/EnumPropertyEditor.cs index eb5c0350..15457934 100644 --- a/src/Myra/Graphics2D/UI/Properties/EnumPropertyEditor.cs +++ b/src/Myra/Graphics2D/UI/Properties/EnumPropertyEditor.cs @@ -8,9 +8,10 @@ public sealed class EnumPropertyEditor : PropertyEditor { public EnumPropertyEditor(IInspector owner, Record methodInfo) : base(owner, methodInfo) { - + if (!methodInfo.Type.IsEnum) + throw new TypeLoadException($"Record is not an enum: {methodInfo.Type}"); } - + protected override bool CreatorPicker(out WidgetCreatorDelegate creatorDelegate) { //TODO - support flags enums diff --git a/src/Myra/Graphics2D/UI/Properties/PropertyGrid.cs b/src/Myra/Graphics2D/UI/Properties/PropertyGrid.cs index 801f7570..b92572e1 100644 --- a/src/Myra/Graphics2D/UI/Properties/PropertyGrid.cs +++ b/src/Myra/Graphics2D/UI/Properties/PropertyGrid.cs @@ -60,6 +60,7 @@ public object Object set { + // TODO send notification to active editors before if (value == _object) { return; diff --git a/src/Myra/Graphics2D/UI/Range/SpinButton.cs b/src/Myra/Graphics2D/UI/Range/SpinButton.cs index ec258631..703bc5d3 100644 --- a/src/Myra/Graphics2D/UI/Range/SpinButton.cs +++ b/src/Myra/Graphics2D/UI/Range/SpinButton.cs @@ -181,34 +181,23 @@ public TNum? Value { get { - if (string.IsNullOrEmpty(_textField.Text)) - { - return Nullable ? default(TNum?) : MathHelper.Zero; - } - - if(TypeHelper.TryParse(_textField.Text, out TNum result)) - { - return result; - } - - return null; + return StringToNumber(_textField.Text, Nullable); } set { if (!value.HasValue && !Nullable) { - throw new Exception("Value can't be set null when Nullable is false"); + value = default(TNum); } if (value.HasValue) { value = _range.Clamp(value.Value); } + + _textField.Text = NumberToString(value, Nullable, _formatter); - _textField.Text = value.HasValue ? - _strConverter.Invoke(value.Value, _formatter) : - string.Empty; /* if (FixedNumberSize) { @@ -380,19 +369,16 @@ public SpinButton(string styleName = Stylesheet.DefaultStyleName) Mul_Increment = MathHelper.One; } - private static TNum? StringToNumber(string str) + private static TNum? StringToNumber(string str, bool isNullable) { - if (string.IsNullOrEmpty(str)) - { - return null; - } - - if (TypeHelper.TryParse(str, out TNum value)) + if (!string.IsNullOrEmpty(str)) { - return value; + if (TypeHelper.TryParse(str, out TNum value)) + { + return value; + } } - - return null; + return isNullable ? default(TNum?) : MathHelper.Zero; } private static string NumberToString(TNum? value, bool isNullable, NumberFormatInfo formatter) @@ -459,12 +445,12 @@ private void _textField_ValueChanging(object sender, ValueChangingEventArgs eventArgs) { - ValueChanged?.Invoke(this, new ValueChangedEventArgs(StringToNumber(eventArgs.OldValue), StringToNumber(eventArgs.NewValue))); + ValueChanged?.Invoke(this, new ValueChangedEventArgs(StringToNumber(eventArgs.OldValue, Nullable), StringToNumber(eventArgs.NewValue, Nullable))); } private void TextBoxOnTextChangedByUser(object sender, ValueChangedEventArgs eventArgs) { - ValueChangedByUser?.Invoke(this, new ValueChangedEventArgs(StringToNumber(eventArgs.OldValue), StringToNumber(eventArgs.NewValue))); + ValueChangedByUser?.Invoke(this, new ValueChangedEventArgs(StringToNumber(eventArgs.OldValue, Nullable), StringToNumber(eventArgs.NewValue, Nullable))); } public void ApplySpinButtonStyle(SpinButtonStyle style) From bee3e97ae32a257ee0a6cb387acfcbfae5aeba14 Mon Sep 17 00:00:00 2001 From: Bamboy Date: Fri, 20 Feb 2026 11:47:24 -0800 Subject: [PATCH 17/28] Real-time updating of property values --- .../UI/Properties/BooleanPropertyEditor.cs | 20 +++++---- .../UI/Properties/BrushPropertyEditor.cs | 6 +++ .../UI/Properties/CollectionPropertyEditor.cs | 7 ++- .../UI/Properties/ColorPropertyEditor.cs | 14 ++++-- .../UI/Properties/EnumPropertyEditor.cs | 44 ++++++++++++------- .../UI/Properties/NumericPropertyEditor.cs | 25 ++++++++++- .../UI/Properties/PropertyEditor.cs | 29 +++++++++++- .../Graphics2D/UI/Properties/PropertyGrid.cs | 17 ++++++- .../UI/Properties/StringPropertyEditor.cs | 9 ++++ 9 files changed, 138 insertions(+), 33 deletions(-) diff --git a/src/Myra/Graphics2D/UI/Properties/BooleanPropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/BooleanPropertyEditor.cs index 8c230e25..6594bd7f 100644 --- a/src/Myra/Graphics2D/UI/Properties/BooleanPropertyEditor.cs +++ b/src/Myra/Graphics2D/UI/Properties/BooleanPropertyEditor.cs @@ -4,7 +4,7 @@ namespace Myra.Graphics2D.UI.Properties { [PropertyEditor(typeof(BooleanPropertyEditor), typeof(bool))] - public sealed class BooleanPropertyEditor : PropertyEditor, IStructTypeRef + public sealed class BooleanPropertyEditor : StructPropertyEditor, IStructTypeRef { private readonly bool _hasSetter; private readonly bool _nullable; @@ -30,21 +30,17 @@ private bool CreateCheckBox(out Widget widget) widget = null; return false; } - - var propertyType = _record.Type; - bool value = GetValue(_owner.SelectedField); var cb = new CheckButton { - IsChecked = value + IsChecked = GetValue(_owner.SelectedField) }; if (_record.HasSetter) { cb.Click += (sender, args) => { - SetValue(_owner.SelectedField, cb.IsChecked); - _owner.FireChanged(propertyType.Name); + SetValue(_owner.SelectedField, !cb.IsChecked); }; } else @@ -52,8 +48,16 @@ private bool CreateCheckBox(out Widget widget) cb.Enabled = false; } - widget = cb; + Widget = widget = cb; return true; } + + public override void SetWidgetValue(bool? value) + { + var cb = Widget as CheckButton; + if (!_nullable && !value.HasValue) + value = false; + cb.IsChecked = value.Value; + } } } \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/BrushPropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/BrushPropertyEditor.cs index 327a2979..55bc97f6 100644 --- a/src/Myra/Graphics2D/UI/Properties/BrushPropertyEditor.cs +++ b/src/Myra/Graphics2D/UI/Properties/BrushPropertyEditor.cs @@ -1,3 +1,4 @@ +using System; using Microsoft.Xna.Framework; using FontStashSharp.RichText; using Myra.Graphics2D.Brushes; @@ -102,5 +103,10 @@ private bool CreateBrushEditor(out Widget widget) widget = subGrid; return true; } + + public override void SetWidgetValue(object value) + { + Console.WriteLine("BrushPropertyEditor SetWidgetValue Not implemented"); + } } } \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/CollectionPropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/CollectionPropertyEditor.cs index f44ed39b..a26e0947 100644 --- a/src/Myra/Graphics2D/UI/Properties/CollectionPropertyEditor.cs +++ b/src/Myra/Graphics2D/UI/Properties/CollectionPropertyEditor.cs @@ -4,7 +4,7 @@ namespace Myra.Graphics2D.UI.Properties { - //[PropertyEditor(typeof(CollectionPropertyEditor), typeof(IList))] + [PropertyEditor(typeof(CollectionPropertyEditor), typeof(IList))] public class CollectionPropertyEditor : PropertyEditor { private readonly Type collectionKind; @@ -80,5 +80,10 @@ private static void UpdateLabelCount(Label textBlock, int count) { textBlock.Text = string.Format("{0} Items", count); } + + public override void SetWidgetValue(object value) + { + Console.WriteLine("CollectionPropertyEditor SetWidgetValue Not implemented"); + } } } \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/ColorPropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/ColorPropertyEditor.cs index bcecd8cb..e37b7367 100644 --- a/src/Myra/Graphics2D/UI/Properties/ColorPropertyEditor.cs +++ b/src/Myra/Graphics2D/UI/Properties/ColorPropertyEditor.cs @@ -4,9 +4,10 @@ namespace Myra.Graphics2D.UI.Properties { - [PropertyEditor(typeof(ColorPropertyEditor), typeof(Color), typeof(Color?))] - public sealed class ColorPropertyEditor : PropertyEditor + [PropertyEditor(typeof(ColorPropertyEditor), typeof(Color))] + public sealed class ColorPropertyEditor : StructPropertyEditor { + private Image _colorDisplay; public ColorPropertyEditor(IInspector owner, Record methodInfo) : base(owner, methodInfo) { @@ -58,7 +59,7 @@ private bool CreateEditor(out Widget widget) Height = 16, Color = color }; - + _colorDisplay = image; subGrid.Widgets.Add(image); var button = new Button @@ -108,5 +109,12 @@ private bool CreateEditor(out Widget widget) return true; } + + public override void SetWidgetValue(Color? value) + { + if (!value.HasValue) + value = Color.Magenta; + _colorDisplay.Color = value.Value; + } } } \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/EnumPropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/EnumPropertyEditor.cs index 15457934..300ba526 100644 --- a/src/Myra/Graphics2D/UI/Properties/EnumPropertyEditor.cs +++ b/src/Myra/Graphics2D/UI/Properties/EnumPropertyEditor.cs @@ -1,17 +1,28 @@ using System; using Myra.Utility; +using Myra.Utility.Types; namespace Myra.Graphics2D.UI.Properties { - [PropertyEditor(typeof(EnumPropertyEditor), typeof(Enum))] - public sealed class EnumPropertyEditor : PropertyEditor + [PropertyEditor(typeof(EnumPropertyEditor<>), typeof(Enum))] + public sealed class EnumPropertyEditor : StructPropertyEditor where TEnum : struct, Enum { + private Array _values; + private bool _nullable; + public EnumPropertyEditor(IInspector owner, Record methodInfo) : base(owner, methodInfo) { if (!methodInfo.Type.IsEnum) throw new TypeLoadException($"Record is not an enum: {methodInfo.Type}"); } + protected override void Initialize() + { + Type type = typeof(TEnum); + _nullable = TypeHelper.GetNullableTypeOrPassThrough(ref type); + _values = Enum.GetValues(type); + } + protected override bool CreatorPicker(out WidgetCreatorDelegate creatorDelegate) { //TODO - support flags enums @@ -27,24 +38,18 @@ private bool CreateComboView(out Widget widget) return false; } - var propertyType = _record.Type; - var value = _record.GetValue(_owner.SelectedField); - - var isNullable = propertyType.IsNullableEnum(); - var enumType = isNullable ? propertyType.GetNullableType() : propertyType; - var values = Enum.GetValues(enumType); - var cv = new ComboView(); - - if (isNullable) + + if (_nullable) { cv.Widgets.Add(new Label { - Text = string.Empty + Text = string.Empty, + Tag = null }); } - foreach (var v in values) + foreach (var v in _values) { cv.Widgets.Add(new Label { @@ -53,8 +58,10 @@ private bool CreateComboView(out Widget widget) }); } - var selectedIndex = Array.IndexOf(values, value); - if (isNullable) + object obj = _record.GetValue(_owner.SelectedField); + + int selectedIndex = Array.IndexOf(_values, obj); + if (_nullable) { ++selectedIndex; } @@ -78,5 +85,12 @@ private bool CreateComboView(out Widget widget) widget = cv; return true; } + + public override void SetWidgetValue(TEnum? value) + { + var cb = Widget as ComboView; + int i = Array.IndexOf(_values, value); + cb.SelectedIndex = i; + } } } \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/NumericPropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/NumericPropertyEditor.cs index 94db806f..ceffada4 100644 --- a/src/Myra/Graphics2D/UI/Properties/NumericPropertyEditor.cs +++ b/src/Myra/Graphics2D/UI/Properties/NumericPropertyEditor.cs @@ -14,7 +14,7 @@ namespace Myra.Graphics2D.UI.Properties typeof(long), typeof(ulong), typeof(long?), typeof(ulong?), typeof(float), typeof(float?), typeof(double), typeof(double?), typeof(decimal), typeof(decimal?))] - public sealed class NumericPropertyEditor : PropertyEditor, INumberTypeRef where TNum : struct + public sealed class NumericPropertyEditor : StructPropertyEditor, INumberTypeRef where TNum : struct { private Type _underlyingType; private bool _nullable; @@ -75,6 +75,8 @@ private bool CreateNumericEditor(out Widget widget) { widget = CreateNativeType(convert); } + + Widget = widget; return true; } @@ -166,5 +168,24 @@ private Widget CreateByteDodge(TNum? val) } return spinButton; } - } + + public override void SetWidgetValue(TNum? value) + { + if (Widget is SpinButton native) + native.Value = value; + else if(Widget is SpinButton dodge) + { + short? val; + if (!IsNullable && !value.HasValue) + { + val = 0; + } + else + { + val = GenericMath.Convert(value); + } + dodge.Value = val; + } + } + } } \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs index 91eed932..d33baf94 100644 --- a/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs +++ b/src/Myra/Graphics2D/UI/Properties/PropertyEditor.cs @@ -69,7 +69,7 @@ public static bool TryCreate(IInspector inspector, Record bindProperty, out Prop protected readonly Record _record; public Type Type => _record.Type; - public Widget Widget { get; protected set; } //TODO this is never going to be needed with how this class is used + public Widget Widget { get; protected set; } /// /// Creates a new widget attached to the given Record @@ -94,7 +94,14 @@ public void SetValue(object field, object value) _record.SetValue(field, value); _owner.FireChanged(_record.Name); } - + public abstract void SetWidgetValue(object value); + public void UpdateDisplay() + { + if(Widget.Desktop.FocusedKeyboardWidget == Widget) + return; + SetWidgetValue(_record.GetValue(_owner.SelectedField)); + } + object IRecordReference.GetValue(object field) => _record.GetValue(field); Record IRecordReference.Record => _record; bool IRecordReference.IsReadOnly => !_record.HasSetter; @@ -136,4 +143,22 @@ public virtual void SetValue(object field, T value) _owner.FireChanged(_record.Name); } } + + public abstract class StructPropertyEditor : PropertyEditor, IStructTypeRef where T : struct + { + private bool _nullable; + public bool IsNullable => _nullable; + + protected StructPropertyEditor(IInspector owner, Record methodInfo) : base(owner, methodInfo) + { + } + + public sealed override void SetWidgetValue(object value) + { + if(value is T kind) + SetWidgetValue(kind); + } + + public abstract void SetWidgetValue(T? value); + } } \ No newline at end of file diff --git a/src/Myra/Graphics2D/UI/Properties/PropertyGrid.cs b/src/Myra/Graphics2D/UI/Properties/PropertyGrid.cs index b92572e1..9c0af40d 100644 --- a/src/Myra/Graphics2D/UI/Properties/PropertyGrid.cs +++ b/src/Myra/Graphics2D/UI/Properties/PropertyGrid.cs @@ -34,12 +34,14 @@ namespace Myra.Graphics2D.UI.Properties public partial class PropertyGrid : Widget, IInspector { private const string DefaultCategoryName = "Miscellaneous"; + private const int ALLOC = 32; private readonly GridLayout _layout = new GridLayout(); private readonly PropertyGrid _parentGrid; - private Record _parentProperty; + private readonly Record _parentProperty; private readonly Dictionary> _records = new Dictionary>(); - private List _recMemory = new List(32); + private readonly List _recMemory = new List(ALLOC); + private readonly List _editors = new List(ALLOC); private readonly HashSet _expandedCategories = new HashSet(); private object _object; private bool _ignoreCollections; @@ -1023,6 +1025,7 @@ private void FillSubGrid(ref int y, IReadOnlyList records) if (PropertyEditor.TryCreate(this, record, out PropertyEditor editor)) { + _editors.Add(editor); valueWidget = editor.Widget; } @@ -1202,6 +1205,7 @@ public void Rebuild() _layout.RowsProportions.Clear(); Children.Clear(); _records.Clear(); + _editors.Clear(); _expandedCategories.Clear(); if(!RecordAggregator(in _object, in _parentType, _recMemory, _doFancyLayout)) @@ -1383,5 +1387,14 @@ public void ApplyPropertyGridStyle(TreeStyle style) PropertyGridStyle = style; } + + public override void InternalRender(RenderContext context) + { + foreach (var editor in _editors) + { + editor.UpdateDisplay(); + } + base.InternalRender(context); + } } } diff --git a/src/Myra/Graphics2D/UI/Properties/StringPropertyEditor.cs b/src/Myra/Graphics2D/UI/Properties/StringPropertyEditor.cs index dd0e6d08..a11baf43 100644 --- a/src/Myra/Graphics2D/UI/Properties/StringPropertyEditor.cs +++ b/src/Myra/Graphics2D/UI/Properties/StringPropertyEditor.cs @@ -92,6 +92,15 @@ private bool CreateTextBox(out Widget widget) widget = tf; return true; } + + public override void SetWidgetValue(object value) + { + var tb = Widget as TextBox; + if (value == null) + tb.Text = string.Empty; + else if (value is string str) + tb.Text = str; + } /* private bool CreateTextBoxAsFilePath(Record record, out Widget widget) From 477dc2bce7f852a00f30e73d510dbc41fe5823a7 Mon Sep 17 00:00:00 2001 From: Bamboy Date: Sat, 21 Feb 2026 07:26:12 -0800 Subject: [PATCH 18/28] Added compatibility for .net/keyword string-type conversion in TypeHelper, and MyraPad now accepts either type format. Fixed MyraPad codegen, saving, and loading for generic types. MyraPad now displays the generic type arg on the left pane. --- .../AllWidgets.Generated.cs | 11 +- .../Myra.Samples.AllWidgets/allControls.xmmp | 4 +- .../Myra.Samples.GridContainer/GridGame.cs | 4 +- src/Myra/MML/BaseContext.cs | 16 --- src/Myra/MML/LoadContext.cs | 109 +++++++++------ src/Myra/MML/SaveContext.cs | 32 ++++- src/Myra/Utility/Reflection.cs | 2 + src/Myra/Utility/Serialization.cs | 15 ++- src/Myra/Utility/Types/TypeHelper.cs | 124 ++++++++++++++++++ src/MyraPad/UI/MainForm.cs | 22 +++- 10 files changed, 256 insertions(+), 83 deletions(-) diff --git a/samples/Myra.Samples.AllWidgets/AllWidgets.Generated.cs b/samples/Myra.Samples.AllWidgets/AllWidgets.Generated.cs index 9dc056b9..daff3477 100644 --- a/samples/Myra.Samples.AllWidgets/AllWidgets.Generated.cs +++ b/samples/Myra.Samples.AllWidgets/AllWidgets.Generated.cs @@ -1,4 +1,4 @@ -/* Generated by MyraPad at 12/6/2023 3:29:08 AM */ +/* Generated by MyraPad at 2/21/2026 7:18:26 AM */ using Myra; using Myra.Graphics2D; using Myra.Graphics2D.TextureAtlases; @@ -129,9 +129,8 @@ private void BuildUI() _buttonSaveFile.Id = "_buttonSaveFile"; Grid.SetColumn(_buttonSaveFile, 1); _buttonSaveFile.Content = horizontalStackPanel1; - _buttonSaveFile.MouseCursor = MouseCursorType.Hand; - _textSaveFile = new TextBox(); + _textSaveFile = new TextBox(); _textSaveFile.Id = "_textSaveFile"; Grid.SetColumn(_textSaveFile, 2); @@ -272,8 +271,9 @@ private void BuildUI() label16.Text = "Spin Button:"; Grid.SetRow(label16, 9); - var spinButton1 = new SpinButton(); - spinButton1.Value = 1; + var spinButton1 = new SpinButton(); + spinButton1.Value = 5; + spinButton1.DecimalPlaces = 0; spinButton1.Width = 100; Grid.SetColumn(spinButton1, 1); Grid.SetRow(spinButton1, 9); @@ -376,6 +376,7 @@ private void BuildUI() _gridRight.Widgets.Add(label22); var scrollViewer1 = new ScrollViewer(); + scrollViewer1.ScrollMultiplier = 10; scrollViewer1.ShowHorizontalScrollBar = false; scrollViewer1.Content = _gridRight; diff --git a/samples/Myra.Samples.AllWidgets/allControls.xmmp b/samples/Myra.Samples.AllWidgets/allControls.xmmp index f439c335..1684c66b 100644 --- a/samples/Myra.Samples.AllWidgets/allControls.xmmp +++ b/samples/Myra.Samples.AllWidgets/allControls.xmmp @@ -23,7 +23,7 @@ - + @@ -77,7 +77,7 @@