Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// 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
{
/// <summary>
/// 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.
/// </summary>
public class GuidInputMaskBehavior : Behavior<TextBox>
{
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;
private int previousLength;

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;
}

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;

// 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('{', '}', '(', ')', ' ');
var valid = raw.Length <= HexDigits && IsHex(raw);
SetInvalidHint(!valid);
if (!valid)
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;
}
}

/// <summary>
/// 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.
/// </summary>
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)
{
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,35 @@
</DataTemplate>
</edvw:TypeMatchTemplateProvider>

<!-- Provider for Guid editor -->
<edvw:TypeMatchTemplateProvider x:Key="GuidPropertyTemplateProvider" Type="{x:Type s:Guid}" edvw:PropertyViewHelper.TemplateCategory="PropertyEditor">
<DataTemplate DataType="qvm:NodeViewModel">
<Grid Margin="2">
<sd:TextBox x:Name="TextBox" SelectAllOnFocus="True"
Text="{Binding NodeValue, Converter={cvt:GuidToString}}"
WatermarkContentTemplate="{StaticResource DifferentValuesWatermarkTemplate}">
<i:Interaction.Behaviors>
<behaviors:GuidInputMaskBehavior/>
<behaviors:TextBoxPropertyValueValidationBehavior AdornerStoryboard="{StaticResource HighlightBorderAdornerValidationErrorStoryboard}"/>
</i:Interaction.Behaviors>
</sd:TextBox>
<!-- Ghost template: while editing, completes the typed prefix with the remaining
"0000...-" pattern so the missing length/dashes are visible at a glance. The typed
part is rendered transparent purely to align the remainder; hit-testing stays on
the TextBox. Once 36 chars are reached the remainder is empty and nothing shows. -->
<TextBlock IsHitTestVisible="False" VerticalAlignment="Center" Margin="3,0,0,0"
Visibility="{Binding IsKeyboardFocusWithin, ElementName=TextBox, Converter={sd:VisibleOrCollapsed}}"><Run
Text="{Binding Text, ElementName=TextBox, Mode=OneWay}" Foreground="Transparent"/><Run
Text="{Binding Text, ElementName=TextBox, Mode=OneWay, Converter={cvt:GuidTemplateRemainder}}" Foreground="#807F7F7F"/></TextBlock>
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding NodeValue}" Value="{x:Static qvm:NodeViewModel.DifferentValues}">
<Setter TargetName="TextBox" Property="WatermarkContent" Value="{sd:Localize (Different values)}"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</edvw:TypeMatchTemplateProvider>

<!-- Provider for vector2 editor -->
<edvw:TypeMatchTemplateProvider x:Key="Vector2PropertyTemplateProvider" Type="{x:Type math:Vector2}" edvw:PropertyViewHelper.TemplateCategory="PropertyEditor">
<DataTemplate DataType="qvm:NodeViewModel">
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
public class GuidTemplateRemainder : OneWayValueConverter<GuidTemplateRemainder>
{
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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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.Quantum.ViewModels;
using Stride.Core.Presentation.ValueConverters;

namespace Stride.Core.Assets.Editor.View.ValueConverters
{
public class GuidToString : ValueConverterBase<GuidToString>
{
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)
{
// 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;
}
}
}
Loading