Bläddra i källkod

DFLayout label and header styling; DFLayout import from spreadsheet
Option to convert labels to headers and back again.

Kenric Nugteren 2 år sedan
förälder
incheckning
bf2aa5599f

+ 30 - 5
InABox.Core/DigitalForms/Layouts/Controls/DFLayoutHeader/DFLayoutHeader.cs

@@ -1,5 +1,7 @@
-using System;
+using Newtonsoft.Json;
+using System;
 using System.Collections.Generic;
+using System.Drawing;
 using System.Text;
 
 namespace InABox.Core
@@ -7,12 +9,23 @@ namespace InABox.Core
     public class DFLayoutHeader : DFLayoutControl
     {
         [TextBoxEditor]
-        [EditorSequence(0)]
-        public string Header { get; set; } = "";
+        [EditorSequence(-1)]
+        public string Header { get; set; }
 
         [CheckBoxEditor]
-        [EditorSequence(1)]
-        public bool Collapsed { get; set; } = false;
+        [EditorSequence(0)]
+        public bool Collapsed { get; set; }
+
+        [EditorSequence("Style", 0)]
+        public DFLayoutTextStyle Style { get; set; }
+
+        protected override void Init()
+        {
+            base.Init();
+            Header = "";
+            Collapsed = false;
+            Style = new DFLayoutTextStyle { IsBold = true };
+        }
 
         protected override string GetDescription()
         {
@@ -25,6 +38,7 @@ namespace InABox.Core
 
             Header = GetProperty("Header", "");
             Collapsed = GetProperty("Collapsed", false);
+            Style = Serialization.Deserialize<DFLayoutTextStyle>(GetProperty<string?>("Style", null)) ?? new DFLayoutTextStyle { IsBold = true };
         }
 
         protected override void SaveProperties()
@@ -33,6 +47,17 @@ namespace InABox.Core
 
             SetProperty("Header", Header);
             SetProperty("Collapsed", Collapsed);
+            SetProperty("Style", Serialization.Serialize(Style));
+        }
+
+        public T GetStyleProperty<T>(string name, T defaultValue)
+        {
+            return GetProperty($"Style.{name}", defaultValue);
+        }
+
+        public void SetStyleProperty(string name, object? value)
+        {
+            SetProperty($"Style.{name}", value);
         }
     }
 }

+ 27 - 3
InABox.Core/DigitalForms/Layouts/Controls/DFLayoutLabel/DFLayoutLabel.cs

@@ -1,14 +1,25 @@
-namespace InABox.Core
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using System;
+using System.Drawing;
+
+namespace InABox.Core
 {
+
     public class DFLayoutLabel : DFLayoutControl
     {
-        [MemoEditor]
         [EditorSequence(0)]
+        [MemoEditor]
         public string Caption { get; set; }
 
-        public DFLayoutLabel()
+        [EditorSequence("Style", 0)]
+        public DFLayoutTextStyle Style { get; set; }
+
+        protected override void Init()
         {
+            base.Init();
             Caption = "";
+            Style = new DFLayoutTextStyle();
         }
 
         protected override string GetDescription()
@@ -20,12 +31,25 @@
         {
             base.LoadProperties();
             Caption = GetProperty("Caption", "");
+
+            Style = Serialization.Deserialize<DFLayoutTextStyle>(GetProperty<string?>("Style", null)) ?? new DFLayoutTextStyle();
         }
 
         protected override void SaveProperties()
         {
             base.SaveProperties();
             SetProperty("Caption", Caption);
+            SetProperty("Style", Serialization.Serialize(Style));
+        }
+
+        public T GetStyleProperty<T>(string name, T defaultValue)
+        {
+            return GetProperty($"Style.{name}", defaultValue);
+        }
+
+        public void SetStyleProperty(string name, object? value)
+        {
+            SetProperty($"Style.{name}", value);
         }
     }
 }

+ 75 - 0
InABox.Core/DigitalForms/Layouts/Controls/DFLayoutLabel/DFLayoutTextStyle.cs

@@ -0,0 +1,75 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Text;
+
+namespace InABox.Core
+{
+    public enum UnderlineType
+    {
+        None,
+        Single,
+        Double
+    }
+
+    public class DFLayoutTextStyle : EnclosedEntity
+    {
+        [EditorSequence(0)]
+        [CheckBoxEditor]
+        public bool IsItalic { get; set; }
+
+        [EditorSequence(1)]
+        [CheckBoxEditor]
+        public bool IsBold { get; set; }
+
+        [EditorSequence(2)]
+        [EnumLookupEditor(typeof(UnderlineType))]
+        public UnderlineType Underline { get; set; }
+
+        [EditorSequence(3)]
+        [DoubleEditor(Caption = "Font size in points. Set to 0 for the default size.")]
+        public double FontSize { get; set; }
+
+        [EditorSequence(4)]
+        [ColorEditor]
+        public string Foreground { get; set; }
+
+        [EditorSequence(5)]
+        [ColorEditor]
+        public string Background { get; set; }
+
+        [JsonIgnore]
+        [NullEditor]
+        public Color ForegroundColour { get => GetForegroundColour(); set => SetForegroundColour(value); }
+
+        [JsonIgnore]
+        [NullEditor]
+        public Color BackgroundColour { get => GetBackgroundColour(); set => SetBackgroundColour(value); }
+
+        protected override void Init()
+        {
+            base.Init();
+            IsItalic = false;
+            IsBold = false;
+            Underline = UnderlineType.None;
+            Foreground = "";
+            Background = "";
+            FontSize = 0;
+        }
+
+        public Color GetForegroundColour() => ColourFromString(Foreground);
+        public Color GetBackgroundColour() => ColourFromString(Background);
+
+        public string SetForegroundColour(Color colour) => Foreground = ColourToString(colour);
+        public string SetBackgroundColour(Color colour) => Background = ColourToString(colour);
+
+        public static Color ColourFromString(string colour) => string.IsNullOrWhiteSpace(colour)
+            ? Color.Empty
+            : DFLayoutUtils.ConvertObjectToColour(colour) ?? Color.Empty;
+
+        public static string ColourToString(Color colour) => colour == Color.Empty
+            ? ""
+            : colour.IsKnownColor ? colour.ToString() : $"#{colour.ToArgb():X4}";
+    }
+}

+ 1 - 41
InABox.Core/DigitalForms/Layouts/DFLayout.cs

@@ -228,46 +228,6 @@ namespace InABox.Core
             }
         }
 
-        private Color? ConvertObjectToColour(object? colour)
-        {
-            if(colour is string str)
-            {
-                if (str.StartsWith('#'))
-                {
-                    var trimmed = str.TrimStart('#');
-                    try
-                    {
-                        if (trimmed.Length == 6)
-                        {
-                            return Color.FromArgb(
-                                Int32.Parse(trimmed[..2], NumberStyles.HexNumber),
-                                Int32.Parse(trimmed.Substring(2, 2), NumberStyles.HexNumber),
-                                Int32.Parse(trimmed.Substring(4, 2), NumberStyles.HexNumber));
-                        }
-                        else if (trimmed.Length == 8)
-                        {
-                            return Color.FromArgb(Int32.Parse(trimmed, NumberStyles.HexNumber));
-                        }
-                        else
-                        {
-                            return null;
-                        }
-                    }
-                    catch (Exception e)
-                    {
-                        Logger.Send(LogType.Error, "", $"Error parsing Colour Expression colour '{str}': {e.Message}");
-                        return null;
-                    }
-                }
-                else if(Enum.TryParse<KnownColor>(str, out var result))
-                {
-                    return Color.FromKnownColor(result);
-                }
-                return null;
-            }
-            return null;
-        }
-
         private void EvaluateColourExpression(string name)
         {
             var expression = ColourExpressions[name];
@@ -280,7 +240,7 @@ namespace InABox.Core
             try
             {
                 var colour = expression?.Evaluate(values);
-                Renderer?.SetFieldColour(name, ConvertObjectToColour(colour));
+                Renderer?.SetFieldColour(name, DFLayoutUtils.ConvertObjectToColour(colour));
             }
             catch (Exception e)
             {

+ 52 - 0
InABox.Core/DigitalForms/Layouts/DFLayoutUtils.cs

@@ -0,0 +1,52 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Globalization;
+using System.Text;
+
+namespace InABox.Core
+{
+    public static class DFLayoutUtils
+    {
+        public static Color? ConvertObjectToColour(object? colour)
+        {
+            if (colour is string str)
+            {
+                if (str.StartsWith('#'))
+                {
+                    var trimmed = str.TrimStart('#');
+                    try
+                    {
+                        if (trimmed.Length == 6)
+                        {
+                            return Color.FromArgb(
+                                Int32.Parse(trimmed[..2], NumberStyles.HexNumber),
+                                Int32.Parse(trimmed.Substring(2, 2), NumberStyles.HexNumber),
+                                Int32.Parse(trimmed.Substring(4, 2), NumberStyles.HexNumber));
+                        }
+                        else if (trimmed.Length == 8)
+                        {
+                            return Color.FromArgb(Int32.Parse(trimmed, NumberStyles.HexNumber));
+                        }
+                        else
+                        {
+                            return null;
+                        }
+                    }
+                    catch (Exception e)
+                    {
+                        Logger.Send(LogType.Error, "", $"Error parsing colour '{str}': {e.Message}");
+                        return null;
+                    }
+                }
+                else if (Enum.TryParse<KnownColor>(str, out var result))
+                {
+                    return Color.FromKnownColor(result);
+                }
+                return null;
+            }
+            return null;
+        }
+
+    }
+}

+ 6 - 1
InABox.DynamicGrid/DynamicGrid.cs

@@ -2953,7 +2953,7 @@ namespace InABox.DynamicGrid
             return filename;
         }
 
-        private void Import_Click(object sender, RoutedEventArgs e)
+        protected virtual void DoImport()
         {
             var list = new DynamicImportList(
                 typeof(T),
@@ -2965,6 +2965,11 @@ namespace InABox.DynamicGrid
             Refresh(false, true);
         }
 
+        private void Import_Click(object sender, RoutedEventArgs e)
+        {
+            DoImport();
+        }
+
         protected virtual void CustomiseExportColumns(List<string> columnnames)
         {
         }

+ 52 - 1
InABox.DynamicGrid/FormDesigner/Controls/DFHeaderControl.cs

@@ -5,6 +5,8 @@ using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
 using static System.Windows.Forms.VisualStyles.VisualStyleElement;
 
 namespace InABox.DynamicGrid
@@ -15,10 +17,15 @@ namespace InABox.DynamicGrid
 
         protected override FrameworkElement Create()
         {
+
+            var style = Control.Style;
+
             Header = new FormHeader
             {
                 Collapsed = Control.Collapsed,
-                HeaderText = Control.Header
+                HeaderText = Control.Header,
+                FontWeight = style.IsBold ? FontWeights.Bold : FontWeights.Normal,
+                FontStyle = style.IsItalic ? FontStyles.Italic : FontStyles.Normal,
             };
             Header.CollapsedChanged += (o, c) =>
             {
@@ -28,7 +35,51 @@ namespace InABox.DynamicGrid
             {
                 Header.IsEnabled = false;
             }
+
+            if (style.FontSize > 0)
+            {
+                Header.FontSize = style.FontSize;
+            }
+            if (style.BackgroundColour != System.Drawing.Color.Empty)
+            {
+                Header.Background = new SolidColorBrush(ConvertColour(style.BackgroundColour));
+            }
+            if (style.ForegroundColour != System.Drawing.Color.Empty)
+            {
+                Header.Foreground = new SolidColorBrush(ConvertColour(style.ForegroundColour));
+            }
+
+            if (style.Underline == UnderlineType.Single)
+            {
+                Header.TextDecorations.Add(TextDecorations.Underline);
+            }
+            else if (style.Underline == UnderlineType.Double)
+            {
+                var underline1 = new TextDecoration
+                {
+                    Pen = new Pen
+                    {
+                        Brush = Header.Foreground,
+                    }
+                };
+                var underline2 = new TextDecoration
+                {
+                    Pen = new Pen
+                    {
+                        Brush = Header.Foreground,
+                    },
+                    PenOffset = 2
+                };
+                Header.TextDecorations.Add(underline1);
+                Header.TextDecorations.Add(underline2);
+            }
+
             return Header;
         }
+
+        private static Color ConvertColour(System.Drawing.Color colour)
+        {
+            return new Color { R = colour.R, G = colour.G, B = colour.B, A = colour.A };
+        }
     }
 }

+ 64 - 5
InABox.DynamicGrid/FormDesigner/Controls/DFLabelControl.cs

@@ -6,6 +6,7 @@ using System.Text;
 using System.Threading.Tasks;
 using System.Windows;
 using System.Windows.Controls;
+using System.Windows.Media;
 
 namespace InABox.DynamicGrid
 {
@@ -13,11 +14,69 @@ namespace InABox.DynamicGrid
     {
         protected override FrameworkElement Create()
         {
-            var label = new Label();
-            label.Content = Control.Caption;
-            label.HorizontalContentAlignment = HorizontalAlignment.Left;
-            label.VerticalContentAlignment = VerticalAlignment.Center;
-            return label;
+            var border = new Border
+            {
+                HorizontalAlignment = HorizontalAlignment.Stretch,
+                VerticalAlignment = VerticalAlignment.Stretch
+            };
+
+            var style = Control.Style;
+
+            var textBlock = new TextBlock
+            {
+                Text = Control.Caption,
+                FontWeight = style.IsBold ? FontWeights.Bold : FontWeights.Normal,
+                FontStyle = style.IsItalic ? FontStyles.Italic : FontStyles.Normal,
+                VerticalAlignment = VerticalAlignment.Center,
+                Margin = new Thickness(5)
+            };
+
+            if(style.FontSize > 0)
+            {
+                textBlock.FontSize = style.FontSize;
+            }
+            if (style.BackgroundColour != System.Drawing.Color.Empty)
+            {
+                border.Background = new SolidColorBrush(ConvertColour(style.BackgroundColour));
+            }
+            if (style.ForegroundColour != System.Drawing.Color.Empty)
+            {
+                textBlock.Foreground = new SolidColorBrush(ConvertColour(style.ForegroundColour));
+            }
+
+            if (style.Underline == UnderlineType.Single)
+            {
+                textBlock.TextDecorations.Add(TextDecorations.Underline);
+            }
+            else if(style.Underline == UnderlineType.Double)
+            {
+                var underline1 = new TextDecoration
+                {
+                    Pen = new Pen
+                    {
+                        Brush = textBlock.Foreground,
+                    }
+                };
+                var underline2 = new TextDecoration
+                {
+                    Pen = new Pen
+                    {
+                        Brush = textBlock.Foreground,
+                    },
+                    PenOffset = 2
+                };
+                textBlock.TextDecorations.Add(underline1);
+                textBlock.TextDecorations.Add(underline2);
+            }
+
+            border.Child = textBlock;
+
+            return border;
+        }
+
+        private static Color ConvertColour(System.Drawing.Color colour)
+        {
+            return new Color { R = colour.R, G = colour.G, B = colour.B, A = colour.A };
         }
     }
 }

+ 8 - 4
InABox.DynamicGrid/FormDesigner/Controls/FormHeader.xaml

@@ -22,13 +22,17 @@
     </UserControl.Resources>
 
     <Button Click="Button_Click" HorizontalContentAlignment="Left" Cursor="Hand"
-            Background="Transparent" BorderBrush="Transparent">
+            Background="{Binding Path=Background, ElementName=UserControl}" BorderBrush="Transparent">
         <StackPanel Orientation="Horizontal">
             <Image Width="32" Height="32"
                    RenderTransformOrigin="0.5,0.5" Source="/InABox.DynamicGrid;component/Resources/header_closed.png"/>
-            <Label Content="{Binding Path=HeaderText, ElementName=UserControl}" FontWeight="Bold"
-                   HorizontalContentAlignment="Left"
-                   VerticalContentAlignment="Center"/>
+            <TextBlock x:Name="TextBlock" 
+                       Text="{Binding Path=HeaderText, ElementName=UserControl}" 
+                       FontWeight="{Binding Path=FontWeight, ElementName=UserControl}"
+                       FontStyle="{Binding Path=FontStyle, ElementName=UserControl}"
+                       Foreground="{Binding Path=Foreground,ElementName=UserControl}"
+                       TextAlignment="Left"
+                       VerticalAlignment="Center"/>
         </StackPanel>
     </Button>
 </UserControl>

+ 8 - 0
InABox.DynamicGrid/FormDesigner/Controls/FormHeader.xaml.cs

@@ -27,6 +27,8 @@ namespace InABox.DynamicGrid
             "Collapsed", typeof(bool), typeof(FormHeader),
             new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.None, new PropertyChangedCallback(OnCollapsedChanged)));
 
+        public static readonly DependencyProperty TextDecorationsProperty = DependencyProperty.Register("TextDecorations", typeof(TextDecorationCollection), typeof(FormHeader));
+
         public string HeaderText
         {
             get => GetValue(HeaderTextProperty).ToString() ?? "";
@@ -39,6 +41,12 @@ namespace InABox.DynamicGrid
             set => SetValue(CollapsedProperty, value);
         }
 
+        public TextDecorationCollection TextDecorations
+        {
+            get => TextBlock.TextDecorations;
+            set => TextBlock.TextDecorations = value;
+        }
+
         public event CollapsedChangedEvent? CollapsedChanged;
 
         public FormHeader()

+ 46 - 0
InABox.DynamicGrid/FormDesigner/DynamicFormDesignGrid.cs

@@ -796,6 +796,44 @@ namespace InABox.DynamicGrid
             }
         }
 
+        private void ConvertToHeaderClick(DFLayoutLabel label)
+        {
+            var header = new DFLayoutHeader
+            {
+                Row = label.Row,
+                RowSpan = label.RowSpan,
+                Column = label.Column,
+                ColumnSpan = label.ColumnSpan,
+                Collapsed = false,
+                Header = label.Caption,
+
+                Style = label.Style
+            };
+            header.Style.IsBold = true;
+            form.Elements.Remove(label);
+            form.Elements.Add(header);
+            Render();
+        }
+
+        private void ConvertToLabelClick(DFLayoutHeader header)
+        {
+            var label = new DFLayoutLabel
+            {
+                Row = header.Row,
+                RowSpan = header.RowSpan,
+                Column = header.Column,
+                ColumnSpan = header.ColumnSpan,
+                Caption = header.Header,
+
+                Style = header.Style
+            };
+            label.Style.IsBold = false;
+
+            form.Elements.Remove(header);
+            form.Elements.Add(label);
+            Render();
+        }
+
         private ContextMenu CreateElementContextMenu(FrameworkElement element, DFLayoutControl control)
         {
             var result = new ContextMenu();
@@ -804,6 +842,14 @@ namespace InABox.DynamicGrid
             {
                 result.Items.Add(CreateMenuItem("Edit Variable", field, EditVariableClick));
             }
+            if(control is DFLayoutLabel label)
+            {
+                result.Items.Add(CreateMenuItem("Convert to Header", label, ConvertToHeaderClick));
+            }
+            if(control is DFLayoutHeader header)
+            {
+                result.Items.Add(CreateMenuItem("Convert to Label", header, ConvertToLabelClick));
+            }
             result.Items.Add(new Separator());
             result.Items.Add(CreateMenuItem("Delete Item", control, DeleteElementClick));
             element.SetValue(ContextMenuProperty, result);

+ 244 - 1
InABox.DynamicGrid/FormDesigner/DynamicFormLayoutGrid.cs

@@ -1,21 +1,222 @@
 using System;
 using System.Collections.Generic;
+using System.Drawing;
+using System.IO;
 using System.Linq;
+using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Media.Imaging;
 using InABox.Core;
 using InABox.DynamicGrid;
+using InABox.Scripting;
 using InABox.WPF;
+using Microsoft.Win32;
+using Org.BouncyCastle.Asn1.Mozilla;
+using Syncfusion.UI.Xaml.Spreadsheet;
+using Syncfusion.Windows.Shared;
+using UnderlineType = InABox.Core.UnderlineType;
 
 namespace InABox.DynamicGrid
 {
+
+    public static class FormLayoutImporter
+    {
+        private class Cell
+        {
+            public string Content { get; set; }
+
+            public int Row { get; set; }
+
+            public int Column { get; set; }
+
+            public int RowSpan { get; set; } = 1;
+
+            public int ColumnSpan { get; set; } = 1;
+
+            public ICell InnerCell { get; set; }
+
+            public Cell(int row, int column, string content, ICell cell)
+            {
+                Row = row;
+                Column = column;
+                Content = content;
+                InnerCell = cell;
+            }
+        }
+
+        private static void DeleteColumn(List<Cell> cells, int column)
+        {
+            foreach(var cell in cells)
+            {
+                if(cell.Column <= column && cell.Column + cell.ColumnSpan - 1 >= column)
+                {
+                    --cell.ColumnSpan;
+                }
+                else if(cell.Column > column)
+                {
+                    --cell.Column;
+                }
+            }
+            cells.RemoveAll(x => x.ColumnSpan < 0);
+        }
+
+        private static List<Cell> GetCells(ISheet sheet)
+        {
+            var grid = new Dictionary<int, Dictionary<int, Cell>>();
+
+            for (int rowIdx = sheet.FirstRow; rowIdx <= sheet.LastRow; ++rowIdx)
+            {
+                var row = sheet.GetRow(rowIdx);
+                if (row is not null)
+                {
+                    var rowCells = new Dictionary<int, Cell>();
+                    for (int colIdx = row.FirstColumn; colIdx <= row.LastColumn; ++colIdx)
+                    {
+                        var cell = row.GetCell(colIdx);
+                        if (cell is not null)
+                        {
+                            rowCells.Add(colIdx, new Cell(rowIdx, colIdx, cell.GetValue(), cell));
+                        }
+                    }
+                    grid.Add(rowIdx, rowCells);
+                }
+            }
+
+            foreach (var region in sheet.GetMergedCells())
+            {
+                for (int r = region.FirstRow; r <= region.LastRow; ++r)
+                {
+                    if (!grid.TryGetValue(r, out var row)) continue;
+
+                    for (int c = region.FirstColumn; c <= region.LastColumn; ++c)
+                    {
+                        if ((r - region.FirstRow) + (c - region.FirstColumn) != 0)
+                        {
+                            row.Remove(c);
+                        }
+                    }
+                    if (row.Count == 0)
+                    {
+                        grid.Remove(r);
+                    }
+                }
+
+                if (grid.TryGetValue(region.FirstRow, out var cRow) && cRow.TryGetValue(region.FirstColumn, out var cCell))
+                {
+                    cCell.RowSpan = region.LastRow - region.FirstRow + 1;
+                    cCell.ColumnSpan = region.LastColumn - region.FirstColumn + 1;
+                }
+            }
+
+            var cells = new List<Cell>();
+            foreach (var row in grid.Values)
+            {
+                foreach (var cell in row.Values)
+                {
+                    cells.Add(cell);
+                }
+            }
+            return cells;
+        }
+
+        public static DFLayout LoadLayout(ISpreadsheet spreadsheet)
+        {
+            var sheet = spreadsheet.GetSheet(0);
+
+            var cells = GetCells(sheet);
+
+            int firstRow = int.MaxValue;
+            int lastRow = 0;
+            int firstCol = int.MaxValue;
+            int lastCol = 0;
+
+            foreach (var cell in cells)
+            {
+                firstCol = Math.Min(cell.Column, firstCol);
+                lastCol = Math.Max(cell.Column + cell.ColumnSpan - 1, lastCol);
+
+                firstRow = Math.Min(cell.Row, firstRow);
+                lastRow = Math.Max(cell.Row + cell.RowSpan - 1, lastRow);
+            }
+
+            var layout = new DFLayout();
+
+            var columnWidths = new Dictionary<int, float>();
+            var colOffset = 0;
+            for (int col = firstCol; col <= lastCol; ++col)
+            {
+                var width = sheet.GetColumnWidth(col);
+                if(width == float.MinValue)
+                {
+                    layout.ColumnWidths.Add("10*");
+                }
+                else if(width <= 0f)
+                {
+                    DeleteColumn(cells, col);
+                }
+                else
+                {
+                    layout.ColumnWidths.Add($"{width}*");
+                }
+            }
+
+            /*var rowHeights = new Dictionary<int, float>();
+            for (int row = firstRow; row <= lastRow; ++row)
+            {
+                rowHeights[row] = sheet.GetRowHeight(row);
+            }
+            var totalHeight = rowHeights.Values.Sum();*/
+
+            for(int row = firstRow; row <= lastRow; ++row)
+            {
+                layout.RowHeights.Add("Auto");
+            }
+
+            foreach(var cell in cells)
+            {
+                //if (string.IsNullOrWhiteSpace(cell.Content)) continue;
+
+                var style = cell.InnerCell.GetStyle();
+                var font = style.Font;
+
+                layout.Elements.Add(new DFLayoutLabel
+                {
+                    Caption = cell.Content,
+                    Row = cell.Row - firstRow + 1,
+                    Column = cell.Column - firstCol + 1 - colOffset,
+                    RowSpan = cell.RowSpan,
+                    ColumnSpan = cell.ColumnSpan,
+
+                    Style = new DFLayoutTextStyle
+                    {
+                        FontSize = font.FontSize,
+
+                        IsItalic = font.Italic,
+                        IsBold = font.Bold,
+                        Underline = font.Underline switch
+                        {
+                            Scripting.UnderlineType.None => UnderlineType.None,
+                            Scripting.UnderlineType.Single or Scripting.UnderlineType.SingleAccounting => UnderlineType.Single,
+                            Scripting.UnderlineType.Double or Scripting.UnderlineType.DoubleAccounting => UnderlineType.Double,
+                            _ => UnderlineType.None
+                        },
+                        BackgroundColour = style.Foreground,
+                        ForegroundColour = font.Colour
+                    }
+                });
+            }
+
+            return layout;
+        }
+    }
+
     public abstract class DynamicFormLayoutGrid<T> : DynamicOneToManyGrid<DigitalForm, DigitalFormLayout> where T : Entity, IRemotable, IPersistent, new()
     {
         private readonly BitmapImage design = Properties.Resources.design.AsBitmapImage();
 
         public DynamicFormLayoutGrid()
         {
-            Options.AddRange(DynamicGridOption.RecordCount);
+            Options.AddRange(DynamicGridOption.RecordCount, DynamicGridOption.ImportData);
             ActionColumns.Add(new DynamicImageColumn(DesignImage, DesignClick));
             //AddButton("Design", PRSDesktop.Resources.design.AsBitmapImage(), DesignClick);
             HiddenColumns.Add(x => x.Layout);
@@ -24,6 +225,38 @@ namespace InABox.DynamicGrid
             AddButton("Duplicate", null, Duplicate_Click);
         }
 
+        private DFLayout LoadLayoutFromSpreadsheet(ISpreadsheet spreadsheet)
+        {
+            return FormLayoutImporter.LoadLayout(spreadsheet);
+        }
+
+        protected override void DoImport()
+        {
+            var dialog = new OpenFileDialog();
+            dialog.Filter = "Excel Spreadsheet (.xlsx)|*.xlsx";
+            if (dialog.ShowDialog() == true)
+            {
+                try
+                {
+                    DFLayout layout;
+                    using (var fs = new FileStream(dialog.FileName, FileMode.Open, FileAccess.Read, FileShare.Read))
+                    {
+                        layout = LoadLayoutFromSpreadsheet(new Spreadsheet(fs));
+                    }
+
+                    var dfLayout = CreateItem();
+                    dfLayout.Layout = layout.SaveLayout();
+                    SaveItem(dfLayout);
+                    Refresh(false, true);
+                }
+                catch(Exception e)
+                {
+                    Logger.Send(LogType.Error, "", CoreUtils.FormatException(e));
+                    MessageBox.Show($"Error: {e.Message}");
+                }
+            }
+        }
+
         private bool Duplicate_Click(Button btn, CoreRow[] rows)
         {
             if (!rows.Any()) return false;
@@ -110,6 +343,16 @@ namespace InABox.DynamicGrid
                 }
                 return null;
             };
+            /*form.OnEditVariable += (variable) =>
+            {
+                var properties = variable.CreateProperties();
+                if (DynamicVariableUtils.EditProperties(Item, GetVariables(), properties.GetType(), properties))
+                {
+                    variable.SaveProperties(properties);
+                    return true;
+                }
+                return false;
+            };*/
             form.LoadLayout(layout, variables);
             form.Initialize();
 

+ 1 - 1
inabox.scripting/InABox.Scripting.csproj

@@ -2,7 +2,7 @@
 
     <PropertyGroup>
         <TargetFramework>net6.0</TargetFramework>
-        <Nullable>disable</Nullable>
+        <Nullable>enable</Nullable>
     </PropertyGroup>
 
     <ItemGroup>

+ 85 - 3
inabox.scripting/Spreadsheet/ISheet.cs

@@ -1,5 +1,7 @@
-using System;
+using Org.BouncyCastle.Asn1.Mozilla;
+using System;
 using System.Collections.Generic;
+using System.Drawing;
 using System.IO;
 using System.Linq;
 using System.Text;
@@ -8,30 +10,99 @@ using System.Threading.Tasks;
 namespace InABox.Scripting
 {
 
+    public class CellRange
+    {
+        public int FirstRow { get; set; }
+        public int LastRow { get; set; }
+        public int FirstColumn { get; set; }
+        public int LastColumn { get; set; }
+
+        public CellRange(int firstRow, int lastRow, int firstColumn, int lastColumn)
+        {
+            FirstRow = firstRow;
+            LastRow = lastRow;
+            FirstColumn = firstColumn;
+            LastColumn = lastColumn;
+        }
+    }
+
     public interface IDataFormat {
         public short FormatIndex { get; }
     }
 
+    public enum UnderlineType
+    {
+        None,
+        Single,
+        Double,
+        SingleAccounting,
+        DoubleAccounting
+    }
+
+    public interface IFont
+    {
+        public bool Bold { get; set; }
+
+        public bool Italic { get; set; }
+
+        public UnderlineType Underline { get; set; }
+
+        public double FontSize { get; set; }
+
+        public Color Colour { get; }
+    }
+
     public interface ICellStyle
     {
+        public ISpreadsheet Spreadsheet { get; }
+
         public IDataFormat DataFormat { get; set; }
+
+        public IFont Font { get; }
+
+        public Color Background { get; }
+
+        public Color Foreground { get; }
     }
 
     public interface ISheet
     {
         public string Name { get; }
 
+        int FirstRow { get; }
+
+        int LastRow { get; }
+
+        ISpreadsheet Spreadsheet { get; }
+
         IRow NewRow();
 
+        IRow? GetRow(int row);
+
         /// <summary>
-        /// Sets the column width with a certain number of characters
+        /// Sets the column width with a certain number of characters.
         /// </summary>
         /// <param name="column"></param>
         /// <param name="charWidth"></param>
         /// <returns></returns>
         ISheet SetColumnWidth(int column, float charWidth);
 
+        /// <summary>
+        /// Gets the width of a column in characters. Returns float.MinValue if no width has been set.
+        /// </summary>
+        /// <param name="column"></param>
+        /// <returns></returns>
+        float GetColumnWidth(int column);
+
+        /// <summary>
+        /// Gets the height of a row in points.
+        /// </summary>
+        /// <param name="row"></param>
+        /// <returns></returns>
+        float GetRowHeight(int row);
+
         ISheet MergeCells(int firstRow, int lastRow, int firstColumn, int lastColumn);
+        IEnumerable<CellRange> GetMergedCells();
 
         IEnumerable<IRow> Rows();
 
@@ -42,6 +113,12 @@ namespace InABox.Scripting
     {
         public int RowNumber { get; }
 
+        int FirstColumn { get; }
+
+        int LastColumn { get; }
+
+        ISheet Sheet { get; }
+
         public ICell this[int column]
         {
             get { return GetCell(column); }
@@ -55,13 +132,17 @@ namespace InABox.Scripting
 
         public int GetColumn(string name, bool throwException = true);
 
-        public ICell GetCell(int column);
+        public ICell? GetCell(int column);
 
         public ICell NewCell(int column);
+
+        public IEnumerable<ICell> Cells();
     }
 
     public interface ICell
     {
+        IRow Row { get; }
+
         string GetValue();
         bool? GetBoolValue();
         double? GetDoubleValue();
@@ -76,6 +157,7 @@ namespace InABox.Scripting
 
         ICell SetBlank();
 
+        ICellStyle GetStyle();
         ICell SetStyle(ICellStyle style);
     }
 

+ 219 - 24
inabox.scripting/Spreadsheet/NPOISpreadsheet.cs

@@ -9,25 +9,34 @@ using NPOI.SS.UserModel;
 using NPOI.XSSF.UserModel;
 using NCell = NPOI.SS.UserModel.ICell;
 using NRow = NPOI.SS.UserModel.IRow;
+using NFont = NPOI.SS.UserModel.IFont;
 using NSheet = NPOI.SS.UserModel.ISheet;
 using NDataFormat = NPOI.SS.UserModel.IDataFormat;
 using NCellStyle = NPOI.SS.UserModel.ICellStyle;
 using NPOI.SS.Util;
+using NPOI.OpenXmlFormats.Spreadsheet;
+using System.Security.Policy;
+using System.Drawing;
+using NPOI.HSSF.Util;
+using NPOI.HSSF.UserModel;
 
 namespace InABox.Scripting
 {
 
     public class RowEnumerator : IEnumerator<Row>
     {
+        public Sheet Sheet { get; }
+
         private IEnumerator _enumerator { get; set; }
 
-        public Row Current => new Row(_enumerator.Current as NRow);
+        public Row Current => new Row((_enumerator.Current as NRow)!, Sheet);
 
-        object IEnumerator.Current => new Row(_enumerator.Current as NRow);
+        object IEnumerator.Current => new Row((_enumerator.Current as NRow)!, Sheet);
 
-        internal RowEnumerator(IEnumerator enumerator)
+        internal RowEnumerator(IEnumerator enumerator, Sheet sheet)
         {
             _enumerator = enumerator;
+            Sheet = sheet;
         }
 
         public bool MoveNext()
@@ -51,9 +60,18 @@ namespace InABox.Scripting
 
         public string Name => _sheet.SheetName;
 
-        internal Sheet(NSheet sheet)
+        public int FirstRow => _sheet.FirstRowNum;
+
+        public int LastRow => _sheet.LastRowNum;
+
+        public Spreadsheet Spreadsheet { get; }
+
+        ISpreadsheet ISheet.Spreadsheet => Spreadsheet;
+
+        internal Sheet(NSheet sheet, Spreadsheet spreadsheet)
         {
             _sheet = sheet;
+            Spreadsheet = spreadsheet;
         }
 
         public IEnumerable<IRow> Rows()
@@ -62,22 +80,41 @@ namespace InABox.Scripting
             var row = 0;
             while (enumerator.MoveNext() && row <= int.MaxValue)
             {
-                yield return new Row((NRow)enumerator.Current);
+                yield return new Row((NRow)enumerator.Current, this);
                 row++;
             }
         }
 
         public IEnumerator<IRow> RowEnumerator()
         {
-            return new RowEnumerator(_sheet.GetRowEnumerator());
+            return new RowEnumerator(_sheet.GetRowEnumerator(), this);
         }
 
         public IRow NewRow()
         {
             var row = _sheet.CreateRow(_sheet.LastRowNum + 1);
-            return new Row(row);
+            return new Row(row, this);
+        }
+
+        public IRow? GetRow(int row)
+        {
+            var nRow = _sheet.GetRow(row);
+            if (nRow is null) return null;
+            return new Row(nRow, this);
+        }
+
+        public float GetRowHeight(int row)
+        {
+            return _sheet.GetRow(row)?.HeightInPoints ?? _sheet.DefaultRowHeightInPoints;
         }
 
+        public float GetColumnWidth(int column)
+        {
+            if (_sheet.IsColumnHidden(column)) return 0f;
+            var width = _sheet.GetColumnWidth(column) / 256f;
+            if (width <= 0f) return float.MinValue;
+            return width;
+        }
         public ISheet SetColumnWidth(int column, float charWidth)
         {
             _sheet.SetColumnWidth(column, (int)Math.Round(charWidth * 256));
@@ -90,6 +127,14 @@ namespace InABox.Scripting
             _sheet.AddMergedRegion(range);
             return this;
         }
+
+        public IEnumerable<CellRange> GetMergedCells()
+        {
+            foreach(var region in _sheet.MergedRegions)
+            {
+                yield return new CellRange(region.FirstRow, region.LastRow, region.FirstColumn, region.LastColumn);
+            }
+        }
     }
 
     public class Row : IRow
@@ -98,9 +143,18 @@ namespace InABox.Scripting
 
         public int RowNumber => _row.RowNum;
 
-        internal Row(NRow row)
+        public int FirstColumn => _row.FirstCellNum;
+
+        public int LastColumn => _row.LastCellNum;
+
+        public Sheet Sheet { get; }
+
+        ISheet IRow.Sheet => Sheet;
+
+        internal Row(NRow row, Sheet sheet)
         {
             _row = row;
+            Sheet = sheet;
         }
         public string ExtractString(int column, bool uppercase = false)
         {
@@ -186,12 +240,25 @@ namespace InABox.Scripting
             throw new Exception("Unable to find Column: " + name);
         }
 
-        public ICell GetCell(int column) => new Cell(_row.GetCell(column));
+        public ICell? GetCell(int column)
+        {
+            var nCell = _row.GetCell(column);
+            if (nCell is null) return null;
+            return new Cell(nCell, this);
+        }
 
         public ICell NewCell(int column)
         {
             var cell = _row.CreateCell(column);
-            return new Cell(cell);
+            return new Cell(cell, this);
+        }
+
+        public IEnumerable<ICell> Cells()
+        {
+            foreach(var cell in _row)
+            {
+                yield return new Cell(cell, this);
+            }
         }
     }
 
@@ -200,9 +267,14 @@ namespace InABox.Scripting
 
         private NCell _cell;
 
-        internal Cell(NCell cell)
+        IRow ICell.Row => Row;
+
+        public Row Row { get; }
+
+        internal Cell(NCell cell, Row row)
         {
             _cell = cell;
+            Row = row;
         }
 
         public string GetValue()
@@ -214,7 +286,7 @@ namespace InABox.Scripting
                 return _cell.StringCellValue;
             }
 
-            return _cell.ToString();
+            return _cell.ToString() ?? "";
         }
 
         public bool? GetBoolValue()
@@ -313,24 +385,31 @@ namespace InABox.Scripting
             return this;
         }
 
+        public ICellStyle GetStyle()
+        {
+            return new CellStyle(_cell.CellStyle, Row.Sheet.Spreadsheet);
+        }
         public ICell SetStyle(ICellStyle style)
         {
-            _cell.CellStyle = (style as CellStyle)._style;
+            _cell.CellStyle = (style as CellStyle)!._style;
             return this;
         }
     }
 
     public class SheetEnumerator : IEnumerator<Sheet>
     {
-        public Sheet Current => new(_enumerator.Current);
+        public Spreadsheet Spreadsheet { get; }
+
+        public Sheet Current => new(_enumerator.Current, Spreadsheet);
 
         private IEnumerator<NSheet> _enumerator { get; }
 
-        object IEnumerator.Current => new Sheet(_enumerator.Current);
+        object IEnumerator.Current => new Sheet(_enumerator.Current, Spreadsheet);
 
-        internal SheetEnumerator(IEnumerator<NSheet> enumerator)
+        internal SheetEnumerator(IEnumerator<NSheet> enumerator, Spreadsheet spreadsheet)
         {
             _enumerator = enumerator;
+            Spreadsheet = spreadsheet;
         }
 
         public void Dispose()
@@ -359,6 +438,64 @@ namespace InABox.Scripting
         }
     }
 
+    public class Font : IFont
+    {
+        public Spreadsheet Spreadsheet { get; set; }
+
+        internal NFont _font { get; }
+        public bool Bold { get => _font.IsBold; set => _font.IsBold = value; }
+        public bool Italic { get => _font.IsItalic; set => throw new NotImplementedException(); }
+        public UnderlineType Underline
+        {
+            get => _font.Underline switch
+            {
+                FontUnderlineType.None => UnderlineType.None,
+                FontUnderlineType.Single => UnderlineType.Single,
+                FontUnderlineType.Double => UnderlineType.Double,
+                FontUnderlineType.SingleAccounting => UnderlineType.SingleAccounting,
+                FontUnderlineType.DoubleAccounting => UnderlineType.DoubleAccounting,
+                _ => UnderlineType.None,
+            };
+            set
+            {
+                _font.Underline = value switch
+                {
+                    UnderlineType.None => FontUnderlineType.None,
+                    UnderlineType.Single => FontUnderlineType.Single,
+                    UnderlineType.Double => FontUnderlineType.Double,
+                    UnderlineType.SingleAccounting => FontUnderlineType.SingleAccounting,
+                    UnderlineType.DoubleAccounting => FontUnderlineType.DoubleAccounting,
+                    _ => FontUnderlineType.None
+                };
+            }
+        }
+        public Color Colour {
+            get
+            {
+                if(_font is XSSFFont xFont)
+                {
+                    return CellStyle.ConvertColour(xFont.GetXSSFColor());
+                }
+                else if(_font is HSSFFont hFont && Spreadsheet.Workbook is HSSFWorkbook workbook)
+                {
+                    return CellStyle.ConvertColour(hFont.GetHSSFColor(workbook));
+                }
+                else
+                {
+                    return CellStyle.ColourFromIndex(_font.Color);
+                }
+            }
+        }
+
+        public double FontSize { get => _font.FontHeightInPoints; set => _font.FontHeightInPoints = value; }
+
+        public Font(NFont font, Spreadsheet spreadsheet)
+        {
+            _font = font;
+            Spreadsheet = spreadsheet;
+        }
+    }
+
     public class CellStyle : ICellStyle
     {
         internal NCellStyle _style { get; }
@@ -367,9 +504,68 @@ namespace InABox.Scripting
             set => _style.DataFormat = value.FormatIndex;
         }
 
-        public CellStyle(NCellStyle style)
+        ISpreadsheet ICellStyle.Spreadsheet => Spreadsheet;
+
+        public Spreadsheet Spreadsheet { get; }
+
+        public Color Background => ConvertColour(_style.FillBackgroundColorColor);
+
+        public Color Foreground => ConvertColour(_style.FillForegroundColorColor);
+
+        public IFont Font => new Font(_style.GetFont(Spreadsheet.Workbook), Spreadsheet);
+
+        public CellStyle(NCellStyle style, Spreadsheet spreadsheet)
         {
             _style = style;
+            Spreadsheet = spreadsheet;
+        }
+
+        public static Color ColourFromIndex(short index)
+        {
+            int indexNum = index;
+            var hashIndex = HSSFColor.GetIndexHash();
+            HSSFColor? indexed = null;
+            if (hashIndex.ContainsKey(indexNum))
+                indexed = hashIndex[indexNum];
+            if (indexed != null)
+            {
+                byte[] rgb = new byte[3];
+                rgb[0] = (byte)indexed.GetTriplet()[0];
+                rgb[1] = (byte)indexed.GetTriplet()[1];
+                rgb[2] = (byte)indexed.GetTriplet()[2];
+                return Color.FromArgb(255, rgb[0], rgb[1], rgb[2]);
+            }
+            return Color.Empty;
+        }
+
+        public static Color ConvertColour(IColor? colour)
+        {
+            if(colour is null)
+            {
+                return Color.Empty;
+            }
+            if(colour is ExtendedColor extendedColour)
+            {
+                if (extendedColour.IsIndexed)
+                {
+                    return ColourFromIndex(extendedColour.Index);
+                }
+                else
+                {
+                    var argb = extendedColour.ARGB;
+                    return Color.FromArgb(argb[0], argb[1], argb[2], argb[3]);
+                }
+            }
+            else if(colour is HSSFColor hssfColour)
+            {
+                var rgb = hssfColour.RGB;
+                return Color.FromArgb(255, rgb[0], rgb[1], rgb[2]);
+            }
+            else
+            {
+                Logger.Send(LogType.Error, "", $"Unknown NPOI Colour class {colour.GetType()}");
+                return Color.Empty;
+            }
         }
     }
 
@@ -378,7 +574,6 @@ namespace InABox.Scripting
 
         public IWorkbook Workbook;
 
-        private NCellStyle DateStyle;
         private NDataFormat DataFormat;
 
         private Spreadsheet(IWorkbook workbook)
@@ -395,18 +590,18 @@ namespace InABox.Scripting
 
         public ISheet GetSheet(int index)
         {
-            return new Sheet(Workbook.GetSheetAt(index));
+            return new Sheet(Workbook.GetSheetAt(index), this);
         }
 
         public ISheet GetSheet(string name)
         {
-            return new Sheet(Workbook.GetSheet(name));
+            return new Sheet(Workbook.GetSheet(name), this);
         }
 
         public IEnumerator<ISheet> SheetEnumerator()
         {
             var enumerator = Workbook.GetEnumerator();
-            return new SheetEnumerator(enumerator);
+            return new SheetEnumerator(enumerator, this);
         }
 
         public IEnumerable<ISheet> Sheets()
@@ -431,20 +626,20 @@ namespace InABox.Scripting
         public ISheet NewSheet(string name)
         {
             var sheet = Workbook.CreateSheet(name);
-            return new Sheet(sheet);
+            return new Sheet(sheet, this);
         }
 
         public ISheet NewSheet()
         {
             var sheet = Workbook.CreateSheet();
-            return new Sheet(sheet);
+            return new Sheet(sheet, this);
         }
 
         public ICellStyle NewStyle()
         {
             var style = Workbook.CreateCellStyle();
             var x = style.GetDataFormatString();
-            return new CellStyle(style);
+            return new CellStyle(style, this);
         }
 
         public IDataFormat GetDataFormat(string format)