Procházet zdrojové kódy

Merge remote-tracking branch 'origin/kenric' into frank

frankvandenbos před 2 měsíci
rodič
revize
44fc6a1957

+ 197 - 0
InABox.Avalonia/Components/ButtonStrip/ButtonStrip.cs

@@ -0,0 +1,197 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Metadata;
+using Avalonia.Controls.Primitives;
+using Avalonia.LogicalTree;
+using Avalonia.Media;
+using Avalonia.Metadata;
+using Avalonia.Threading;
+using CommunityToolkit.Mvvm.Input;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Input;
+
+namespace InABox.Avalonia.Components;
+
+public partial class ButtonStrip : TemplatedControl
+{
+    public static StyledProperty<IBrush?> SelectedBackgroundProperty = AvaloniaProperty.Register<ButtonStripItem, IBrush?>(
+        nameof(SelectedBackground));
+    public static StyledProperty<IBrush?> SelectedForegroundProperty = AvaloniaProperty.Register<ButtonStripItem, IBrush?>(
+        nameof(SelectedForeground));
+    public static StyledProperty<double> ItemSpacingProperty = AvaloniaProperty.Register<ButtonStripItem, double>(
+        nameof(ItemSpacing));
+    public static StyledProperty<ObservableCollection<ButtonStripItem>> ItemsProperty =
+        AvaloniaProperty.Register<ButtonStripItem, ObservableCollection<ButtonStripItem>>(nameof(Items));
+
+    public IBrush? SelectedBackground
+    {
+        get => GetValue(SelectedBackgroundProperty);
+        set => SetValue(SelectedBackgroundProperty, value);
+    }
+    public IBrush? SelectedForeground
+    {
+        get => GetValue(SelectedForegroundProperty);
+        set => SetValue(SelectedForegroundProperty, value);
+    }
+    public double ItemSpacing
+    {
+        get => GetValue(ItemSpacingProperty);
+        set => SetValue(ItemSpacingProperty, value);
+    }
+
+    [Content]
+    public ObservableCollection<ButtonStripItem> Items
+    {
+        get => GetValue(ItemsProperty);
+        set => SetValue(ItemsProperty, value);
+    }
+
+    public ButtonStripItem? SelectedItem
+    {
+        get => Items.FirstOrDefault(x => x.Selected);
+        set
+        {
+            foreach(var item in Items)
+            {
+                item.Selected = item == value;
+            }
+        }
+    }
+
+    public event EventHandler SelectionChanged;
+
+    static ButtonStrip()
+    {
+        ItemsProperty.Changed.AddClassHandler<ButtonStrip>(Items_Changed);
+    }
+
+    private static void Items_Changed(ButtonStrip strip, AvaloniaPropertyChangedEventArgs args)
+    {
+        strip.LogicalChildren.Clear();
+        if (strip.Items is not null)
+        {
+            strip.SelectedItem = strip.Items.FirstOrDefault();
+
+            strip.LogicalChildren.AddRange(strip.Items);
+            foreach(var item in strip.Items)
+            {
+                item.Command = strip.ItemSelectedCommand;
+            }
+        }
+    }
+
+    public ButtonStrip()
+    {
+        Items = new();
+        Items.CollectionChanged += Items_CollectionChanged;
+    }
+
+    private void Items_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
+    {
+        SelectedItem ??= Items.FirstOrDefault();
+        switch (e.Action)
+        {
+            case NotifyCollectionChangedAction.Add:
+                AddControlItemsToLogicalChildren(e.NewItems);
+                break;
+            case NotifyCollectionChangedAction.Remove:
+                RemoveControlItemsFromLogicalChildren(e.OldItems);
+                break;
+        }
+        foreach(var item in Items)
+        {
+            item.Command = ItemSelectedCommand;
+        }
+    }
+
+    private void AddControlItemsToLogicalChildren(IEnumerable? items)
+    {
+        if (items is null) return;
+
+        List<ILogical>? toAdd = null;
+        foreach(var i in items)
+        {
+            if(i is Control control && !LogicalChildren.Contains(control))
+            {
+                toAdd ??= new();
+                toAdd.Add(control);
+            }
+        }
+        if(toAdd is not null)
+        {
+            LogicalChildren.AddRange(toAdd);
+        }
+    }
+    private void RemoveControlItemsFromLogicalChildren(IEnumerable? items)
+    {
+        if (items is null) return;
+
+        List<ILogical>? toRemove = null;
+        foreach(var i in items)
+        {
+            if(i is Control control)
+            {
+                toRemove ??= new();
+                toRemove.Add(control);
+            }
+        }
+        if(toRemove is not null)
+        {
+            LogicalChildren.RemoveAll(toRemove);
+        }
+    }
+
+    [RelayCommand]
+    private void ItemSelected(ButtonStripItem item)
+    {
+        var children = this.GetLogicalChildren().ToArray();
+        SelectedItem = item;
+    }
+}
+
+[PseudoClasses(":selected")]
+public class ButtonStripItem : TemplatedControl
+{
+    public static StyledProperty<string> TextProperty = AvaloniaProperty.Register<ButtonStripItem, string>(nameof(Text));
+    public static StyledProperty<bool> SelectedProperty = AvaloniaProperty.Register<ButtonStripItem, bool>(nameof(Selected));
+    public static StyledProperty<int> IndexProperty = AvaloniaProperty.Register<ButtonStripItem, int>(nameof(Index));
+    public static StyledProperty<ICommand> CommandProperty = AvaloniaProperty.Register<ButtonStripItem, ICommand>(nameof(Command));
+
+    public string Text
+    {
+        get => GetValue(TextProperty);
+        set => SetValue(TextProperty, value);
+    }
+    public bool Selected
+    {
+        get => GetValue(SelectedProperty);
+        set => SetValue(SelectedProperty, value);
+    }
+    public int Index
+    {
+        get => GetValue(IndexProperty);
+        set => SetValue(IndexProperty, value);
+    }
+    public ICommand Command
+    {
+        get => GetValue(CommandProperty);
+        set => SetValue(CommandProperty, value);
+    }
+
+    static ButtonStripItem()
+    {
+        SelectedProperty.Changed.AddClassHandler<ButtonStripItem>(Selected_Changed);
+    }
+
+    private static void Selected_Changed(ButtonStripItem item, AvaloniaPropertyChangedEventArgs args)
+    {
+        item.PseudoClasses.Set(":selected", item.Selected);
+    }
+}

+ 21 - 0
InABox.Avalonia/Components/DateSelector/DateSelectorButton.axaml.cs

@@ -12,6 +12,13 @@ using System.Windows.Input;
 
 namespace InABox.Avalonia.Components;
 
+public class DateSelectorDateChangedEventArgs(DateTime? oldDate, DateTime? newDate)
+{
+    public DateTime? OldDate { get; set; } = oldDate;
+
+    public DateTime? NewDate { get; set; } = newDate;
+}
+
 public partial class DateSelectorButton : UserControl
 {
     public static readonly StyledProperty<string> PromptProperty =
@@ -47,6 +54,20 @@ public partial class DateSelectorButton : UserControl
         set => SetValue(DateProperty, value);
     }
 
+    public event EventHandler<DateSelectorDateChangedEventArgs>? DateChanged;
+
+    static DateSelectorButton()
+    {
+        DateProperty.Changed.AddClassHandler<DateSelectorButton>(DateProperty_Changed);
+    }
+
+    private static void DateProperty_Changed(DateSelectorButton button, AvaloniaPropertyChangedEventArgs args)
+    {
+        button.DateChanged?.Invoke(button, new(
+            args.OldValue is DateTime oldDate ? oldDate : null,
+            args.NewValue is DateTime newDate ? newDate : null));
+    }
+
     public DateSelectorButton()
     {
         InitializeComponent();

+ 32 - 0
InABox.Avalonia/Components/DoubleBox/DoubleBox.cs

@@ -0,0 +1,32 @@
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace InABox.Avalonia.Components;
+
+public class DoubleBox : NumericUpDown
+{
+    protected override Type StyleKeyOverride => typeof(NumericUpDown);
+
+    public DoubleBox()
+    {
+        ParsingNumberStyle = NumberStyles.Number;
+        ShowButtonSpinner = false;
+        AddHandler(TextInputEvent, TunnelTextEvent, RoutingStrategies.Direct | RoutingStrategies.Tunnel);
+    }
+
+    private void TunnelTextEvent(object? sender, TextInputEventArgs e)
+    {
+        e.Text = new string(e.Text?.Where(c => Char.IsDigit(c) || c == '.').ToArray());
+        if(e.Text == "")
+        {
+            e.Handled = true;
+        }
+    }
+}

+ 33 - 0
InABox.Avalonia/Components/IntegerBox/IntegerBox.cs

@@ -0,0 +1,33 @@
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace InABox.Avalonia.Components;
+
+public class IntegerBox : NumericUpDown
+{
+    protected override Type StyleKeyOverride => typeof(NumericUpDown);
+
+    public IntegerBox()
+    {
+        ParsingNumberStyle = NumberStyles.Integer;
+        FormatString = "N0";
+        ShowButtonSpinner = false;
+        AddHandler(TextInputEvent, TunnelTextEvent, RoutingStrategies.Direct | RoutingStrategies.Tunnel);
+    }
+
+    private void TunnelTextEvent(object? sender, TextInputEventArgs e)
+    {
+        e.Text = new string(e.Text?.Where(c => Char.IsDigit(c)).ToArray());
+        if(e.Text == "")
+        {
+            e.Handled = true;
+        }
+    }
+}

+ 6 - 0
InABox.Avalonia/Theme/Classes/Button.axaml

@@ -26,6 +26,12 @@
         <Setter Property="Background" Value="{Binding $parent[Button].Background}" />
         <Setter Property="Foreground" Value="{Binding $parent[Button].Foreground}" />
     </Style>
+    <Style Selector="Button.NoHover:pointerover /template/ ContentPresenter#PART_ContentPresenter">
+        <Setter Property="BorderBrush" Value="{Binding $parent[Button].BorderBrush}" />
+        <Setter Property="BorderThickness" Value="{Binding $parent[Button].BorderThickness}" />
+        <Setter Property="Background" Value="{Binding $parent[Button].Background}" />
+        <Setter Property="Foreground" Value="{Binding $parent[Button].Foreground}" />
+    </Style>
 
     <Style Selector="Button.Transparent">
         <Setter Property="HorizontalAlignment" Value="Stretch" />

+ 47 - 0
InABox.Avalonia/Theme/Classes/ButtonStrip.axaml

@@ -0,0 +1,47 @@
+<Styles xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+		xmlns:components="using:InABox.Avalonia.Components">
+	<Style Selector="components|ButtonStrip">
+		<Setter Property="Template">
+			<ControlTemplate>
+				<ScrollViewer VerticalScrollBarVisibility="Disabled"
+							  HorizontalScrollBarVisibility="Auto"
+							  BorderBrush="{TemplateBinding BorderBrush}"
+							  CornerRadius="{TemplateBinding CornerRadius}">
+					<ItemsControl ItemsSource="{TemplateBinding Items}">
+						<ItemsControl.ItemsPanel>
+							<ItemsPanelTemplate>
+								<StackPanel Orientation="Horizontal" Spacing="{Binding $parent[components:ButtonStrip].ItemSpacing}"/>
+							</ItemsPanelTemplate>
+						</ItemsControl.ItemsPanel>
+					</ItemsControl>
+				</ScrollViewer>
+			</ControlTemplate>
+		</Setter>
+		<Setter Property="Background" Value="{DynamicResource PrsButtonBackground}"/>
+		<Setter Property="Foreground" Value="{DynamicResource PrsButtonForeground}"/>
+		<Setter Property="SelectedBackground" Value="{DynamicResource PrsTileBackground}"/>
+		<Setter Property="SelectedForeground" Value="{DynamicResource PrsTileForeground}"/>
+	</Style>
+	<Style Selector="components|ButtonStrip components|ButtonStripItem">
+		<Setter Property="Template">
+			<ControlTemplate>
+				<Button Classes="NoHover"
+						CommandParameter="{TemplateBinding}"
+						Command="{TemplateBinding Command}"
+						Background="{TemplateBinding Background}"
+						Foreground="{TemplateBinding Foreground}"
+						Content="{TemplateBinding Text}">
+				</Button>
+			</ControlTemplate>
+		</Setter>
+	</Style>
+	<Style Selector="components|ButtonStrip > components|ButtonStripItem">
+		<Setter Property="Background" Value="{Binding $parent[components:ButtonStrip].Background}"/>
+		<Setter Property="Foreground" Value="{Binding $parent[components:ButtonStrip].Foreground}"/>
+	</Style>
+	<Style Selector="components|ButtonStrip > components|ButtonStripItem:selected">
+		<Setter Property="Background" Value="{Binding $parent[components:ButtonStrip].SelectedBackground}"/>
+		<Setter Property="Foreground" Value="{Binding $parent[components:ButtonStrip].SelectedForeground}"/>
+	</Style>
+</Styles>

+ 0 - 1
InABox.Avalonia/Theme/Classes/DateSelectorButton.axaml

@@ -2,7 +2,6 @@
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 		xmlns:components="using:InABox.Avalonia.Components">
 	<Style Selector="components|DateSelectorButton">
-		<Setter Property="Margin" Value="{DynamicResource PrsControlSpacing}" />
 		<Setter Property="BorderBrush" Value="{DynamicResource PrsButtonBorder}" />
 		<Setter Property="BorderThickness">
 			<Setter.Value>

+ 29 - 3
InABox.Avalonia/Theme/Classes/TabStrip.axaml

@@ -3,7 +3,7 @@
 	<Style Selector="TabStrip">
 		<Setter Property="Background" Value="{DynamicResource PrsMenuBackground}"/>
 		<Setter Property="BorderBrush" Value="{DynamicResource PrsMenuBackground}"/>
-		<Setter Property="BorderThickness" Value="2"/>
+		<Setter Property="BorderThickness" Value="1"/>
 		<Setter Property="CornerRadius" Value="{DynamicResource PrsCornerRadius}"/>
 		<Setter Property="ItemsPanel">
 			<Setter.Value>
@@ -14,15 +14,16 @@
 		</Setter>
 	</Style>
 	<Style Selector="TabStripItem">
-		<Setter Property="Height" Value="30"/>
-		<Setter Property="MinHeight" Value="0"/>
+		<Setter Property="MinHeight" Value="30"/>
 		<Setter Property="FontSize" Value="{DynamicResource PrsFontSizeSmall}"/>
+		<Setter Property="FontWeight" Value="{DynamicResource PrsFontWeightBold}"/>
 		
 		<Setter Property="HorizontalContentAlignment" Value="Center"/>
 		
 		<Setter Property="Background" Value="{DynamicResource PrsMenuBackground}"/>
 		<Setter Property="Foreground" Value="White"/>
 		<Setter Property="CornerRadius" Value="{DynamicResource PrsCornerRadius}"/>
+		<Setter Property="Padding" Value="0,10,0,10"/>
 	</Style>
 	
 	<Style Selector="TabStripItem:pointerover /template/ Border#PART_LayoutRoot">
@@ -41,4 +42,29 @@
 	<Style Selector="TabStripItem:selected /template/ Border#PART_SelectedPipe">
 		<Setter Property="IsVisible" Value="False"/>
 	</Style>
+
+	<!-- ButtonsList styling -->
+	
+	<Style Selector="TabStrip.ButtonsList">
+		<Setter Property="Background" Value="{DynamicResource PrsButtonBackground}"/>
+		<Setter Property="BorderBrush" Value="{DynamicResource PrsButtonBackground}"/>
+	</Style>
+	<Style Selector="TabStrip.ButtonsList TabStripItem">
+		<Setter Property="Background" Value="{DynamicResource PrsButtonBackground}"/>
+		<Setter Property="Foreground" Value="{DynamicResource PrsButtonForeground}"/>
+	</Style>
+	
+	<Style Selector="TabStrip.ButtonsList TabStripItem:pointerover /template/ Border#PART_LayoutRoot">
+		<Setter Property="Background" Value="{DynamicResource PrsButtonBackground}"/>
+		<Setter Property="TextElement.Foreground" Value="{DynamicResource PrsButtonForeground}"/>
+	</Style>
+	
+	<Style Selector="TabStrip.ButtonsList TabStripItem:selected">
+		<Setter Property="Background" Value="{DynamicResource PrsTileBackground}"/>
+		<Setter Property="Foreground" Value="{DynamicResource PrsTileForeground}"/>
+	</Style>
+	<Style Selector="TabStrip.ButtonsList TabStripItem:selected:pointerover /template/ Border#PART_LayoutRoot">
+		<Setter Property="Background" Value="{DynamicResource PrsTileBackground}"/>
+		<Setter Property="TextElement.Foreground" Value="{DynamicResource PrsTileForeground}"/>
+	</Style>
 </Styles>

+ 5 - 4
InABox.Avalonia/Theme/Styles.axaml

@@ -4,13 +4,14 @@
 
 	<StyleInclude Source="/Theme/Classes/Border.axaml" />
 	<StyleInclude Source="/Theme/Classes/Button.axaml" />
+	<StyleInclude Source="/Theme/Classes/ButtonStrip.axaml" />
 	<StyleInclude Source="/Theme/Classes/CheckBox.axaml" />
+	<StyleInclude Source="/Theme/Classes/DateSelectorButton.axaml" />
 	<StyleInclude Source="/Theme/Classes/Image.axaml" />
 	<StyleInclude Source="/Theme/Classes/Label.axaml" />
-	<StyleInclude Source="/Theme/Classes/TextBox.axaml" />
-	<StyleInclude Source="/Theme/Classes/Separator.axaml" />
 	<StyleInclude Source="/Theme/Classes/ListBox.axaml" />
-	<StyleInclude Source="/Theme/Classes/TabStrip.axaml" />
+	<StyleInclude Source="/Theme/Classes/Separator.axaml" />
 	<StyleInclude Source="/Theme/Classes/TabItem.axaml" />
-	<StyleInclude Source="/Theme/Classes/DateSelectorButton.axaml" />
+	<StyleInclude Source="/Theme/Classes/TabStrip.axaml" />
+	<StyleInclude Source="/Theme/Classes/TextBox.axaml" />
 </Styles>

+ 31 - 32
inabox.wpf/DigitalForms/Designer/Controls/Fields/DFDoubleControl.cs

@@ -7,43 +7,42 @@ using System.Text;
 using System.Threading.Tasks;
 using System.Windows;
 
-namespace InABox.DynamicGrid
+namespace InABox.DynamicGrid;
+
+public class DFDoubleControl : DynamicFormFieldControl<DFLayoutDoubleField, DFLayoutDoubleFieldProperties, double, double?>
 {
-    public class DFDoubleControl : DynamicFormFieldControl<DFLayoutDoubleField, DFLayoutDoubleFieldProperties, double, double?>
-    {
-        private DoubleTextBox Double = null!; // late-initialising
+    private DoubleTextBox Double = null!; // late-initialising
 
-        protected override FrameworkElement Create()
-        {
-            Double = new DoubleTextBox();
-            Double.Value = Field.Properties.Default;
-            Double.HorizontalContentAlignment = HorizontalAlignment.Center;
-            Double.VerticalContentAlignment = VerticalAlignment.Center;
-            Double.VerticalAlignment = VerticalAlignment.Stretch;
-            Double.ValueChanged += (sender, e) => ChangeField();
-            Double.IsScrollingOnCircle = false;
-            return Double;
-        }
+    protected override FrameworkElement Create()
+    {
+        Double = new DoubleTextBox();
+        Double.Value = Field.Properties.Default;
+        Double.HorizontalContentAlignment = HorizontalAlignment.Center;
+        Double.VerticalContentAlignment = VerticalAlignment.Center;
+        Double.VerticalAlignment = VerticalAlignment.Stretch;
+        Double.ValueChanged += (sender, e) => ChangeField();
+        Double.IsScrollingOnCircle = false;
+        return Double;
+    }
 
-        public override void SetSerializedValue(double? value)
-        {
-            Double.Value = value;
-        }
+    public override void SetSerializedValue(double? value)
+    {
+        Double.Value = value;
+    }
 
-        public override double? GetSerializedValue()
-        {
-            return Double.Value ?? 0.0;
-        }
+    public override double? GetSerializedValue()
+    {
+        return Double.Value ?? 0.0;
+    }
 
-        public override double GetValue()
-        {
-            return Double.Value ?? 0.0;
-        }
+    public override double GetValue()
+    {
+        return Double.Value ?? 0.0;
+    }
 
-        public override void SetValue(double value)
-        {
-            Double.Value = value;
-        }
-        protected override bool IsEmpty() => Double.Value == null;
+    public override void SetValue(double value)
+    {
+        Double.Value = value;
     }
+    protected override bool IsEmpty() => Double.Value == null;
 }

+ 42 - 43
inabox.wpf/DigitalForms/Designer/Controls/Fields/DFIntegerControl.cs

@@ -7,50 +7,49 @@ using System.Text;
 using System.Threading.Tasks;
 using System.Windows;
 
-namespace InABox.DynamicGrid
+namespace InABox.DynamicGrid;
+
+public class DFIntegerControl : DynamicFormFieldControl<DFLayoutIntegerField, DFLayoutIntegerFieldProperties, int, int?>
 {
-    public class DFIntegerControl : DynamicFormFieldControl<DFLayoutIntegerField, DFLayoutIntegerFieldProperties, int, int?>
+    private IntegerTextBox Integer = null!; // late-initialising
+
+    protected override FrameworkElement Create()
+    {
+        Integer = new IntegerTextBox();
+        Integer.Value = Field.Properties.Default;
+        Integer.HorizontalContentAlignment = HorizontalAlignment.Center;
+        Integer.VerticalContentAlignment = VerticalAlignment.Center;
+        Integer.IsScrollingOnCircle = false;
+        Integer.VerticalAlignment = VerticalAlignment.Stretch;
+        Integer.ValueChanged += (sender, e) => ChangeField();
+
+        return Integer;
+    }
+
+    public override int? GetSerializedValue()
+    {
+        var value = Integer.Value;
+        if (value != null)
+            return Convert.ToInt32((long)value);
+        return null;
+    }
+
+    public override void SetSerializedValue(int? value)
+    {
+        Integer.Value = value;
+    }
+
+    public override int GetValue()
+    {
+        var value = Integer.Value;
+        if (value != null)
+            return Convert.ToInt32((long)value);
+        return 0;
+    }
+
+    public override void SetValue(int value)
     {
-        private IntegerTextBox Integer = null!; // late-initialising
-
-        protected override FrameworkElement Create()
-        {
-            Integer = new IntegerTextBox();
-            Integer.Value = Field.Properties.Default;
-            Integer.HorizontalContentAlignment = HorizontalAlignment.Center;
-            Integer.VerticalContentAlignment = VerticalAlignment.Center;
-            Integer.IsScrollingOnCircle = false;
-            Integer.VerticalAlignment = VerticalAlignment.Stretch;
-            Integer.ValueChanged += (sender, e) => ChangeField();
-
-            return Integer;
-        }
-
-        public override int? GetSerializedValue()
-        {
-            var value = Integer.Value;
-            if (value != null)
-                return Convert.ToInt32((long)value);
-            return null;
-        }
-
-        public override void SetSerializedValue(int? value)
-        {
-            Integer.Value = value;
-        }
-
-        public override int GetValue()
-        {
-            var value = Integer.Value;
-            if (value != null)
-                return Convert.ToInt32((long)value);
-            return 0;
-        }
-
-        public override void SetValue(int value)
-        {
-            Integer.Value = value;
-        }
-        protected override bool IsEmpty() => Integer.Value == null;
+        Integer.Value = value;
     }
+    protected override bool IsEmpty() => Integer.Value == null;
 }

+ 218 - 219
inabox.wpf/DigitalForms/Designer/Controls/Fields/DFOptionControl.cs

@@ -8,308 +8,307 @@ using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Media;
 
-namespace InABox.DynamicGrid
+namespace InABox.DynamicGrid;
+
+public interface IOptionControl
 {
-    public interface IOptionControl
-    {
-        public string? GetValue();
-        public void SetValue(string value);
+    public string? GetValue();
+    public void SetValue(string value);
 
-        public bool IsEmpty();
-    }
+    public bool IsEmpty();
+}
 
-    public class ButtonsOptionControl : Grid, IOptionControl
-    {
-        private Action OnChanged;
+public class ButtonsOptionControl : Grid, IOptionControl
+{
+    private Action OnChanged;
 
-        private class OptionButton : Border
-        {
-            public string Option { get; set; }
+    private class OptionButton : Border
+    {
+        public string Option { get; set; }
 
-            private bool _selected { get; set; }
+        private bool _selected { get; set; }
 
-            public bool Selected
+        public bool Selected
+        {
+            get => _selected;
+            set
             {
-                get => _selected;
-                set
+                _selected = value;
+                if (value == true)
+                {
+                    Background = new SolidColorBrush(Colors.LightGray);
+                }
+                else if (value == false)
                 {
-                    _selected = value;
-                    if (value == true)
-                    {
-                        Background = new SolidColorBrush(Colors.LightGray);
-                    }
-                    else if (value == false)
-                    {
-                        Background = new SolidColorBrush(Colors.DarkGray);
-                    }
+                    Background = new SolidColorBrush(Colors.DarkGray);
                 }
             }
+        }
 
-            public OptionButton(string option, bool selected)
-            {
-                Option = option;
-                Selected = selected;
-            }
+        public OptionButton(string option, bool selected)
+        {
+            Option = option;
+            Selected = selected;
         }
+    }
+
+    public ButtonsOptionControl(string[] options, Action onChanged)
+    {
+        OnChanged = onChanged;
 
-        public ButtonsOptionControl(string[] options, Action onChanged)
+        foreach (var option in options)
         {
-            OnChanged = onChanged;
+            ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+            var button = new OptionButton(option, false);
 
-            foreach (var option in options)
+            var label = new Label()
             {
-                ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
-                var button = new OptionButton(option, false);
-
-                var label = new Label()
-                {
-                    Content = option.Replace("\r", "").Replace("\n", "")
-                };
-                label.HorizontalContentAlignment = HorizontalAlignment.Center;
-                label.VerticalContentAlignment = VerticalAlignment.Center;
-                button.Child = label;
-
-                button.Margin = new Thickness(
-                    Children.Count == 0 ? 0.0F : 2.5F,
-                    0.0F,
-                    Children.Count == options.Length - 1 ? 0.0F : 2.5F,
-                    0.0F
-                );
-                button.Padding = new Thickness(5.0F, 0.0F, 5.0F, 0.0F);
-
-                button.MouseUp += Button_MouseUp;
-
-                button.SetValue(ColumnProperty, Children.Count);
-                Children.Add(button);
-            }
+                Content = option.Replace("\r", "").Replace("\n", "")
+            };
+            label.HorizontalContentAlignment = HorizontalAlignment.Center;
+            label.VerticalContentAlignment = VerticalAlignment.Center;
+            button.Child = label;
+
+            button.Margin = new Thickness(
+                Children.Count == 0 ? 0.0F : 2.5F,
+                0.0F,
+                Children.Count == options.Length - 1 ? 0.0F : 2.5F,
+                0.0F
+            );
+            button.Padding = new Thickness(5.0F, 0.0F, 5.0F, 0.0F);
+
+            button.MouseUp += Button_MouseUp;
+
+            button.SetValue(ColumnProperty, Children.Count);
+            Children.Add(button);
         }
+    }
 
-        private void Button_MouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
-        {
-            SelectButton((OptionButton)sender);
-            OnChanged();
-        }
+    private void Button_MouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
+    {
+        SelectButton((OptionButton)sender);
+        OnChanged();
+    }
 
-        private void SelectButton(OptionButton button)
+    private void SelectButton(OptionButton button)
+    {
+        foreach (var child in Children)
         {
-            foreach (var child in Children)
+            if (child is OptionButton childButton && childButton != button)
             {
-                if (child is OptionButton childButton && childButton != button)
-                {
-                    childButton.Selected = false;
-                }
+                childButton.Selected = false;
             }
-            button.Selected = true;
         }
+        button.Selected = true;
+    }
 
-        public string? GetValue()
+    public string? GetValue()
+    {
+        foreach (var child in Children)
         {
-            foreach (var child in Children)
+            if (child is OptionButton button)
             {
-                if (child is OptionButton button)
+                if (button.Selected)
                 {
-                    if (button.Selected)
-                    {
-                        return button.Option;
-                    }
+                    return button.Option;
                 }
             }
-            return null;
         }
+        return null;
+    }
 
-        public void SetValue(string value)
+    public void SetValue(string value)
+    {
+        foreach (var child in Children)
         {
-            foreach (var child in Children)
+            if (child is OptionButton button)
             {
-                if (child is OptionButton button)
+                if (button.Option == value)
                 {
-                    if (button.Option == value)
-                    {
-                        SelectButton(button);
-                    }
+                    SelectButton(button);
                 }
             }
         }
-
-        public bool IsEmpty() => GetValue() == null;
     }
 
-    public class RadioOptionControl : Grid, IOptionControl
-    {
-        private Action OnChanged;
+    public bool IsEmpty() => GetValue() == null;
+}
 
-        public RadioOptionControl(string[] options, Action onChanged)
-        {
-            Margin = new Thickness(4,2,4,2);
-            
-            OnChanged = onChanged;
+public class RadioOptionControl : Grid, IOptionControl
+{
+    private Action OnChanged;
 
-            foreach (var option in options)
-            {
-                RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Auto) });
-                var button = new RadioButton();
-                button.Margin = new Thickness(0, 2, 0, 2);
-                button.Content = option.Replace("\r", "").Replace("\n", "");
-                button.VerticalContentAlignment = VerticalAlignment.Center;
-                button.Padding = new Thickness(5.0F, 0.0F, 5.0F, 0.0F);
-                button.SetValue(RowProperty, Children.Count);
-                button.Tag = option;
-                button.Checked += Button_Checked;
-                button.Unchecked += Button_Checked;
-                Children.Add(button);
-            }
-        }
+    public RadioOptionControl(string[] options, Action onChanged)
+    {
+        Margin = new Thickness(4,2,4,2);
+        
+        OnChanged = onChanged;
 
-        private void Button_Checked(object sender, RoutedEventArgs e)
+        foreach (var option in options)
         {
-            OnChanged();
+            RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Auto) });
+            var button = new RadioButton();
+            button.Margin = new Thickness(0, 2, 0, 2);
+            button.Content = option.Replace("\r", "").Replace("\n", "");
+            button.VerticalContentAlignment = VerticalAlignment.Center;
+            button.Padding = new Thickness(5.0F, 0.0F, 5.0F, 0.0F);
+            button.SetValue(RowProperty, Children.Count);
+            button.Tag = option;
+            button.Checked += Button_Checked;
+            button.Unchecked += Button_Checked;
+            Children.Add(button);
         }
+    }
 
-        public string? GetValue()
+    private void Button_Checked(object sender, RoutedEventArgs e)
+    {
+        OnChanged();
+    }
+
+    public string? GetValue()
+    {
+        foreach (var child in Children)
         {
-            foreach (var child in Children)
+            if (child is RadioButton radio)
             {
-                if (child is RadioButton radio)
+                if (radio.IsChecked == true)
                 {
-                    if (radio.IsChecked == true)
-                    {
-                        return radio.Tag as string;
-                    }
+                    return radio.Tag as string;
                 }
             }
-            return null;
         }
+        return null;
+    }
 
-        public void SetValue(string value)
+    public void SetValue(string value)
+    {
+        foreach (var child in Children)
         {
-            foreach (var child in Children)
+            if (child is RadioButton radio)
             {
-                if (child is RadioButton radio)
+                if ((string)radio.Tag == value)
                 {
-                    if ((string)radio.Tag == value)
-                    {
-                        radio.IsChecked = true;
-                    }
+                    radio.IsChecked = true;
                 }
             }
         }
-        public bool IsEmpty() => GetValue() == null;
     }
+    public bool IsEmpty() => GetValue() == null;
+}
 
-    public class ComboBoxOptionControl : ComboBox, IOptionControl
-    {
-        private readonly Action OnChanged;
-
-        public ComboBoxOptionControl(string[] options, Action onChanged)
-        {
-            SetResourceReference(StyleProperty, typeof(ComboBox));
-
-            OnChanged = onChanged;
-
-            foreach (var option in options)
-                Items.Add(option);
-            VerticalContentAlignment = VerticalAlignment.Center;
-            SelectionChanged += ComboBoxOptionControl_SelectionChanged;
-        }
+public class ComboBoxOptionControl : ComboBox, IOptionControl
+{
+    private readonly Action OnChanged;
 
-        private void ComboBoxOptionControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
-        {
-            OnChanged();
-        }
+    public ComboBoxOptionControl(string[] options, Action onChanged)
+    {
+        SetResourceReference(StyleProperty, typeof(ComboBox));
 
-        public string? GetValue()
-        {
-            return SelectedValue as string;
-        }
+        OnChanged = onChanged;
 
-        public void SetValue(string value)
-        {
-            SelectedValue = value;
-        }
-        public bool IsEmpty() => GetValue() == null;
+        foreach (var option in options)
+            Items.Add(option);
+        VerticalContentAlignment = VerticalAlignment.Center;
+        SelectionChanged += ComboBoxOptionControl_SelectionChanged;
     }
-    
-    public class CheckBoxOptionControl : CheckBox, IOptionControl
+
+    private void ComboBoxOptionControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
     {
-        private readonly Action OnChanged;
+        OnChanged();
+    }
 
-        private readonly string TrueValue;
-        private readonly string FalseValue;
+    public string? GetValue()
+    {
+        return SelectedValue as string;
+    }
 
-        public CheckBoxOptionControl(string trueValue, string falseValue, Action onChanged)
-        {
-            OnChanged = onChanged;
-            TrueValue = trueValue;
-            FalseValue = falseValue;
-
-            IsThreeState = false;
-            HorizontalContentAlignment = HorizontalAlignment.Center;
-            VerticalContentAlignment = VerticalAlignment.Center;
-            Checked += CheckBoxOptionControl_Checked;
-            Unchecked += CheckBoxOptionControl_Checked;
-        }
+    public void SetValue(string value)
+    {
+        SelectedValue = value;
+    }
+    public bool IsEmpty() => GetValue() == null;
+}
 
-        private void CheckBoxOptionControl_Checked(object sender, RoutedEventArgs e)
-        {
-            OnChanged();
-        }
+public class CheckBoxOptionControl : CheckBox, IOptionControl
+{
+    private readonly Action OnChanged;
 
-        public string? GetValue()
-        {
-            return IsChecked == true
-                ? TrueValue
-                : FalseValue;
-        }
+    private readonly string TrueValue;
+    private readonly string FalseValue;
 
-        public void SetValue(string value)
-        {
-            IsChecked = value == TrueValue;
-        }
+    public CheckBoxOptionControl(string trueValue, string falseValue, Action onChanged)
+    {
+        OnChanged = onChanged;
+        TrueValue = trueValue;
+        FalseValue = falseValue;
+
+        IsThreeState = false;
+        HorizontalContentAlignment = HorizontalAlignment.Center;
+        VerticalContentAlignment = VerticalAlignment.Center;
+        Checked += CheckBoxOptionControl_Checked;
+        Unchecked += CheckBoxOptionControl_Checked;
+    }
 
-        public bool IsEmpty() => false;
+    private void CheckBoxOptionControl_Checked(object sender, RoutedEventArgs e)
+    {
+        OnChanged();
     }
 
-    public class DFOptionControl : DynamicFormFieldControl<DFLayoutOptionField, DFLayoutOptionFieldProperties, string, string?>
+    public string? GetValue()
     {
-        private IOptionControl OptionControl = null!; // Late-initialised
+        return IsChecked == true
+            ? TrueValue
+            : FalseValue;
+    }
 
-        protected override FrameworkElement Create()
-        {
-            var options = string.IsNullOrWhiteSpace(Field.Properties.Options) 
-                ? new string[] { } 
-                : Field.Properties.Options.Replace(",","\n").Split('\n').Select(x=>x.Trim()).ToArray();
+    public void SetValue(string value)
+    {
+        IsChecked = value == TrueValue;
+    }
 
-            switch (Field.Properties.OptionType)
-            {
-                case DFLayoutOptionType.Buttons:
-                    return SetControl(new ButtonsOptionControl(options, ChangeField));
-                case DFLayoutOptionType.Radio:
-                    return SetControl(new RadioOptionControl(options, ChangeField));
-            }
-            return SetControl(new ComboBoxOptionControl(options, ChangeField));
-        }
+    public bool IsEmpty() => false;
+}
 
-        private T SetControl<T>(T value)
-            where T : FrameworkElement, IOptionControl
-        {
-            OptionControl = value;
-            return value;
-        }
+public class DFOptionControl : DynamicFormFieldControl<DFLayoutOptionField, DFLayoutOptionFieldProperties, string, string?>
+{
+    private IOptionControl OptionControl = null!; // Late-initialised
 
-        public override void SetSerializedValue(string? value)
-        {
-            SetValue(value);
-        }
+    protected override FrameworkElement Create()
+    {
+        var options = string.IsNullOrWhiteSpace(Field.Properties.Options) 
+            ? new string[] { } 
+            : Field.Properties.Options.Replace(",","\n").Split('\n').Select(x=>x.Trim()).ToArray();
 
-        public override string? GetSerializedValue()
+        switch (Field.Properties.OptionType)
         {
-            return OptionControl.GetValue();
+            case DFLayoutOptionType.Buttons:
+                return SetControl(new ButtonsOptionControl(options, ChangeField));
+            case DFLayoutOptionType.Radio:
+                return SetControl(new RadioOptionControl(options, ChangeField));
         }
+        return SetControl(new ComboBoxOptionControl(options, ChangeField));
+    }
 
-        public override string GetValue() => OptionControl.GetValue() ?? "";
+    private T SetControl<T>(T value)
+        where T : FrameworkElement, IOptionControl
+    {
+        OptionControl = value;
+        return value;
+    }
 
-        public override void SetValue(string? value) => OptionControl.SetValue(value ?? "");
+    public override void SetSerializedValue(string? value)
+    {
+        SetValue(value);
+    }
 
-        protected override bool IsEmpty() => OptionControl.IsEmpty();
+    public override string? GetSerializedValue()
+    {
+        return OptionControl.GetValue();
     }
+
+    public override string GetValue() => OptionControl.GetValue() ?? "";
+
+    public override void SetValue(string? value) => OptionControl.SetValue(value ?? "");
+
+    protected override bool IsEmpty() => OptionControl.IsEmpty();
 }

+ 1 - 0
inabox.wpf/InABox.Wpf.csproj

@@ -153,6 +153,7 @@
         <PackageReference Include="Syncfusion.SfGrid.WPF" Version="25.2.6" />
         <PackageReference Include="Syncfusion.SfImageEditor.WPF" Version="25.2.6" />
         <PackageReference Include="Syncfusion.SfInput.WPF" Version="25.2.6" />
+        <PackageReference Include="Syncfusion.SfMaps.WPF" Version="25.2.6" />
         <PackageReference Include="Syncfusion.SfRichTextBoxAdv.WPF" Version="25.2.6" />
         <PackageReference Include="Syncfusion.SfSpreadsheet.WPF" Version="25.2.6" />
         <PackageReference Include="Syncfusion.Tools.WPF.Classic" Version="19.4.0.56" />