From fd5e3ca4a12344e1d88935a0b46aaeea4cd7789b Mon Sep 17 00:00:00 2001 From: "nicogo.eth" Date: Sun, 19 Jul 2026 15:08:43 +0200 Subject: [PATCH 1/5] GameStudio: edit System.Guid properties in the property grid Guid members on components/assets previously fell through to the read-only ObjectPropertyTemplateProvider fallback and rendered as a non-editable "Guid" row. Add a GuidPropertyTemplateProvider (TypeMatchTemplateProvider, also covers Nullable) mirroring the string editor: a TextBox bound through a new GuidToString converter whose ConvertBack keeps the current value (Binding.DoNothing) when the text does not parse as a Guid. Co-Authored-By: Claude Fable 5 --- .../DefaultPropertyTemplateProviders.xaml | 14 +++++++++++ .../View/ValueConverters/GuidToString.cs | 24 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 sources/editor/Stride.Core.Assets.Editor/View/ValueConverters/GuidToString.cs diff --git a/sources/editor/Stride.Core.Assets.Editor/View/DefaultPropertyTemplateProviders.xaml b/sources/editor/Stride.Core.Assets.Editor/View/DefaultPropertyTemplateProviders.xaml index ac4752ef2a..88ddef96f2 100644 --- a/sources/editor/Stride.Core.Assets.Editor/View/DefaultPropertyTemplateProviders.xaml +++ b/sources/editor/Stride.Core.Assets.Editor/View/DefaultPropertyTemplateProviders.xaml @@ -1009,6 +1009,20 @@ + + + + + + + + + + + + diff --git a/sources/editor/Stride.Core.Assets.Editor/View/ValueConverters/GuidToString.cs b/sources/editor/Stride.Core.Assets.Editor/View/ValueConverters/GuidToString.cs new file mode 100644 index 0000000000..b4f1cb576a --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Editor/View/ValueConverters/GuidToString.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using System; +using System.Globalization; +using System.Windows.Data; +using Stride.Core.Presentation.Quantum.ViewModels; +using Stride.Core.Presentation.ValueConverters; + +namespace Stride.Core.Assets.Editor.View.ValueConverters +{ + public class GuidToString : ValueConverterBase + { + public override object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value != NodeViewModel.DifferentValues ? value?.ToString() ?? string.Empty : null; + } + + public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + // Keep the current value when the text isn't a valid Guid, instead of resetting it. + return Guid.TryParse(value as string, out var guid) ? guid : Binding.DoNothing; + } + } +} From 743ce1da33bd8a8ed6a28b81296735fd11637685 Mon Sep 17 00:00:00 2001 From: "nicogo.eth" Date: Sun, 19 Jul 2026 15:40:57 +0200 Subject: [PATCH 2/5] GameStudio: red-adorner feedback on invalid Guid text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the Guid property editor: ConvertBack now passes invalid text through instead of swallowing it with Binding.DoNothing — the typed node setter throws InvalidCastException, TextBoxBase raises TextToSourceValueConversionFailed, and the attached TextBoxPropertyValueValidationBehavior (same pattern as the UFile editor) plays the error adorner while the text reverts to the source value. Co-Authored-By: Claude Fable 5 --- .../View/DefaultPropertyTemplateProviders.xaml | 6 +++++- .../View/ValueConverters/GuidToString.cs | 7 ++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/sources/editor/Stride.Core.Assets.Editor/View/DefaultPropertyTemplateProviders.xaml b/sources/editor/Stride.Core.Assets.Editor/View/DefaultPropertyTemplateProviders.xaml index 88ddef96f2..7044528026 100644 --- a/sources/editor/Stride.Core.Assets.Editor/View/DefaultPropertyTemplateProviders.xaml +++ b/sources/editor/Stride.Core.Assets.Editor/View/DefaultPropertyTemplateProviders.xaml @@ -1014,7 +1014,11 @@ + WatermarkContentTemplate="{StaticResource DifferentValuesWatermarkTemplate}"> + + + + diff --git a/sources/editor/Stride.Core.Assets.Editor/View/ValueConverters/GuidToString.cs b/sources/editor/Stride.Core.Assets.Editor/View/ValueConverters/GuidToString.cs index b4f1cb576a..11a9ba5510 100644 --- a/sources/editor/Stride.Core.Assets.Editor/View/ValueConverters/GuidToString.cs +++ b/sources/editor/Stride.Core.Assets.Editor/View/ValueConverters/GuidToString.cs @@ -2,7 +2,6 @@ // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. using System; using System.Globalization; -using System.Windows.Data; using Stride.Core.Presentation.Quantum.ViewModels; using Stride.Core.Presentation.ValueConverters; @@ -17,8 +16,10 @@ public override object Convert(object value, Type targetType, object parameter, public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - // Keep the current value when the text isn't a valid Guid, instead of resetting it. - return Guid.TryParse(value as string, out var guid) ? guid : Binding.DoNothing; + // Invalid text is passed through untouched: the strongly-typed node setter then throws + // InvalidCastException, which TextBoxBase turns into TextToSourceValueConversionFailed — + // the validation behavior shows its error adorner and the text reverts to the source value. + return Guid.TryParse(value as string, out var guid) ? guid : value; } } } From 9b287e553b5dfa3c7bfb6afbc0b2d10e2fb8c055 Mon Sep 17 00:00:00 2001 From: "nicogo.eth" Date: Sun, 19 Jul 2026 15:43:42 +0200 Subject: [PATCH 3/5] GameStudio: Guid editor ghost template + automatic dash insertion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two usability layers on the Guid property editor: - GuidTemplateRemainder + a ghost TextBlock overlay: while the editor has focus, the typed prefix is completed with the remaining "0000...-0000" pattern in faint text, so the missing length and dash positions are visible at a glance. The typed part is rendered transparent purely to keep the remainder aligned. - GuidInputMaskBehavior: hex input is regrouped live as 8-4-4-4-12 with dashes inserted automatically (caret hops past a freshly inserted dash). Deletions are not fought — backspacing a dash does not re-append it — and non-hex input is left for commit-time validation to flag. Co-Authored-By: Claude Fable 5 --- .../View/Behaviors/GuidInputMaskBehavior.cs | 131 ++++++++++++++++++ .../DefaultPropertyTemplateProviders.xaml | 25 +++- .../ValueConverters/GuidTemplateRemainder.cs | 25 ++++ 3 files changed, 174 insertions(+), 7 deletions(-) create mode 100644 sources/editor/Stride.Core.Assets.Editor/View/Behaviors/GuidInputMaskBehavior.cs create mode 100644 sources/editor/Stride.Core.Assets.Editor/View/ValueConverters/GuidTemplateRemainder.cs diff --git a/sources/editor/Stride.Core.Assets.Editor/View/Behaviors/GuidInputMaskBehavior.cs b/sources/editor/Stride.Core.Assets.Editor/View/Behaviors/GuidInputMaskBehavior.cs new file mode 100644 index 0000000000..7cd7b26bee --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Editor/View/Behaviors/GuidInputMaskBehavior.cs @@ -0,0 +1,131 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using System.Windows.Controls; +using Microsoft.Xaml.Behaviors; + +namespace Stride.Core.Assets.Editor.View.Behaviors +{ + /// + /// Live input mask for Guid text: re-groups hex input as 8-4-4-4-12 while typing, inserting + /// the dashes automatically (including right after the 8th/12th/16th/20th hex digit, so the + /// caret is already past the dash for the next group). Deletions are left alone — removing a + /// dash with backspace doesn't fight the user by re-appending it. Input that isn't plain + /// hex/dashes (braces, garbage) is left untouched; commit-time validation deals with it. + /// + public class GuidInputMaskBehavior : Behavior + { + private const int HexDigits = 32; + private static readonly int[] GroupSizes = [8, 4, 4, 4, 12]; + + private bool updating; + private int previousLength; + + protected override void OnAttached() + { + base.OnAttached(); + previousLength = AssociatedObject.Text?.Length ?? 0; + AssociatedObject.TextChanged += OnTextChanged; + } + + protected override void OnDetaching() + { + AssociatedObject.TextChanged -= OnTextChanged; + base.OnDetaching(); + } + + private void OnTextChanged(object sender, TextChangedEventArgs e) + { + if (updating) + return; + + var box = AssociatedObject; + var text = box.Text ?? string.Empty; + var grew = text.Length > previousLength; + previousLength = text.Length; + + var raw = text.Replace("-", ""); + if (raw.Length > HexDigits || !IsHex(raw)) + return; + + var formatted = Format(raw, appendTrailingDash: grew); + if (formatted == text) + return; + + var rawBeforeCaret = CountHexBefore(text, box.CaretIndex); + + updating = true; + try + { + // SetCurrentValue keeps the Text binding alive (a plain Text= would clear it). + box.SetCurrentValue(TextBox.TextProperty, formatted); + box.CaretIndex = CaretAfter(formatted, rawBeforeCaret, grew); + previousLength = formatted.Length; + } + finally + { + updating = false; + } + } + + private static bool IsHex(string s) + { + foreach (var c in s) + { + if (!char.IsAsciiHexDigit(c)) + return false; + } + return true; + } + + private static string Format(string raw, bool appendTrailingDash) + { + var result = new System.Text.StringBuilder(raw.Length + 4); + int taken = 0; + foreach (var size in GroupSizes) + { + if (taken >= raw.Length) + break; + + var count = System.Math.Min(size, raw.Length - taken); + result.Append(raw, taken, count); + taken += count; + + var groupFull = count == size && taken < HexDigits; + // Dash between groups when more digits follow; trailing dash only on growth, + // so backspacing over a dash doesn't immediately re-append it. + if (groupFull && (taken < raw.Length || appendTrailingDash)) + result.Append('-'); + } + return result.ToString(); + } + + private static int CountHexBefore(string text, int caret) + { + int count = 0; + for (int i = 0; i < caret && i < text.Length; i++) + { + if (text[i] != '-') + count++; + } + return count; + } + + private static int CaretAfter(string formatted, int rawBefore, bool grew) + { + int i = 0, seen = 0; + while (i < formatted.Length && seen < rawBefore) + { + if (formatted[i] != '-') + seen++; + i++; + } + // After typing, hop over the dash we just inserted so the next digit starts the new group. + if (grew) + { + while (i < formatted.Length && formatted[i] == '-') + i++; + } + return i; + } + } +} diff --git a/sources/editor/Stride.Core.Assets.Editor/View/DefaultPropertyTemplateProviders.xaml b/sources/editor/Stride.Core.Assets.Editor/View/DefaultPropertyTemplateProviders.xaml index 7044528026..492e181024 100644 --- a/sources/editor/Stride.Core.Assets.Editor/View/DefaultPropertyTemplateProviders.xaml +++ b/sources/editor/Stride.Core.Assets.Editor/View/DefaultPropertyTemplateProviders.xaml @@ -1012,13 +1012,24 @@ - - - - - + + + + + + + + + + diff --git a/sources/editor/Stride.Core.Assets.Editor/View/ValueConverters/GuidTemplateRemainder.cs b/sources/editor/Stride.Core.Assets.Editor/View/ValueConverters/GuidTemplateRemainder.cs new file mode 100644 index 0000000000..1ec40daa37 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Editor/View/ValueConverters/GuidTemplateRemainder.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using System; +using System.Globalization; +using Stride.Core.Presentation.ValueConverters; + +namespace Stride.Core.Assets.Editor.View.ValueConverters +{ + /// + /// Ghost-text helper for the Guid editor: given the text typed so far, returns the rest of the + /// canonical Guid template ("00000000-0000-0000-0000-000000000000"), so the editor can show + /// inline how many characters (and which dashes) are still missing. Empty once the input + /// reaches the full template length. + /// + public class GuidTemplateRemainder : OneWayValueConverter + { + private const string Template = "00000000-0000-0000-0000-000000000000"; + + public override object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + var text = value as string ?? string.Empty; + return text.Length < Template.Length ? Template[text.Length..] : string.Empty; + } + } +} From 16287a6ec7b9af1effce055a2bf19799d3dca8ec Mon Sep 17 00:00:00 2001 From: "nicogo.eth" Date: Sun, 19 Jul 2026 15:51:13 +0200 Subject: [PATCH 4/5] GameStudio: cap Guid editor input at the full 36-char template The mask behavior sets MaxLength = 36 (32 hex + 4 dashes) so typing past a complete Guid is rejected natively while selection replacement still works, and strips brace/paren wrappers from pasted Guid forms. Co-Authored-By: Claude Fable 5 --- .../View/Behaviors/GuidInputMaskBehavior.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sources/editor/Stride.Core.Assets.Editor/View/Behaviors/GuidInputMaskBehavior.cs b/sources/editor/Stride.Core.Assets.Editor/View/Behaviors/GuidInputMaskBehavior.cs index 7cd7b26bee..7b1d2dc69c 100644 --- a/sources/editor/Stride.Core.Assets.Editor/View/Behaviors/GuidInputMaskBehavior.cs +++ b/sources/editor/Stride.Core.Assets.Editor/View/Behaviors/GuidInputMaskBehavior.cs @@ -15,6 +15,7 @@ namespace Stride.Core.Assets.Editor.View.Behaviors public class GuidInputMaskBehavior : Behavior { private const int HexDigits = 32; + private const int FullLength = 36; // 32 hex digits + 4 dashes private static readonly int[] GroupSizes = [8, 4, 4, 4, 12]; private bool updating; @@ -23,6 +24,9 @@ public class GuidInputMaskBehavior : Behavior protected override void OnAttached() { base.OnAttached(); + // A formatted Guid is exactly 36 chars: once full, further typing is rejected natively + // (replacing a selection still works). Programmatic sets are unaffected. + AssociatedObject.MaxLength = FullLength; previousLength = AssociatedObject.Text?.Length ?? 0; AssociatedObject.TextChanged += OnTextChanged; } @@ -43,7 +47,9 @@ private void OnTextChanged(object sender, TextChangedEventArgs e) var grew = text.Length > previousLength; previousLength = text.Length; - var raw = text.Replace("-", ""); + // Also tolerate the brace/paren Guid forms on paste ("{...}", "(...)"): the wrapper + // characters are dropped so the content still fits the mask. + var raw = text.Replace("-", "").Trim('{', '}', '(', ')', ' '); if (raw.Length > HexDigits || !IsHex(raw)) return; From 9d96dcb66a3249880830b90ace72741d3bc5eacc Mon Sep 17 00:00:00 2001 From: "nicogo.eth" Date: Sun, 19 Jul 2026 15:52:19 +0200 Subject: [PATCH 5/5] GameStudio: live red hint on non-hex Guid input While typing, any non-hex content turns the editor text IndianRed immediately (SetCurrentValue keeps styling intact; InvalidateProperty restores it once the text is clean). Purely visual - input is neither blocked nor rewritten, commit-time validation still does the hard reject. Co-Authored-By: Claude Fable 5 --- .../View/Behaviors/GuidInputMaskBehavior.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/sources/editor/Stride.Core.Assets.Editor/View/Behaviors/GuidInputMaskBehavior.cs b/sources/editor/Stride.Core.Assets.Editor/View/Behaviors/GuidInputMaskBehavior.cs index 7b1d2dc69c..d0a9c97ff9 100644 --- a/sources/editor/Stride.Core.Assets.Editor/View/Behaviors/GuidInputMaskBehavior.cs +++ b/sources/editor/Stride.Core.Assets.Editor/View/Behaviors/GuidInputMaskBehavior.cs @@ -50,7 +50,9 @@ private void OnTextChanged(object sender, TextChangedEventArgs e) // Also tolerate the brace/paren Guid forms on paste ("{...}", "(...)"): the wrapper // characters are dropped so the content still fits the mask. var raw = text.Replace("-", "").Trim('{', '}', '(', ')', ' '); - if (raw.Length > HexDigits || !IsHex(raw)) + var valid = raw.Length <= HexDigits && IsHex(raw); + SetInvalidHint(!valid); + if (!valid) return; var formatted = Format(raw, appendTrailingDash: grew); @@ -73,6 +75,18 @@ private void OnTextChanged(object sender, TextChangedEventArgs e) } } + /// + /// Live feedback while typing: non-hex content turns the text red immediately, without + /// blocking or rewriting the input — commit-time validation still does the hard reject. + /// + private void SetInvalidHint(bool invalid) + { + if (invalid) + AssociatedObject.SetCurrentValue(System.Windows.Controls.Control.ForegroundProperty, System.Windows.Media.Brushes.IndianRed); + else + AssociatedObject.InvalidateProperty(System.Windows.Controls.Control.ForegroundProperty); + } + private static bool IsHex(string s) { foreach (var c in s)