Selaa lähdekoodia

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

frankvandenbos 4 kuukautta sitten
vanhempi
commit
80c5e5d30b

+ 11 - 8
InABox.Avalonia/Components/MenuPanel/AvaloniaMenuItem.cs

@@ -66,15 +66,18 @@ public partial class AvaloniaMenuItem : ObservableObject
 
             else if (sourceItem is CoreMenuItem<IImage> item)
             {
-                var targetItem = new MenuItem
+                if(item.IsVisible is null || item.IsVisible())
                 {
-                    Header = item.Header,
-                    Icon = new Image { Source = item.Image },
-                    Command = item.Action != null
-                        ? new RelayCommand(() => item.Action())
-                        : null
-                };
-                targetItems.Add(targetItem);
+                    var targetItem = new MenuItem
+                    {
+                        Header = item.Header,
+                        Icon = new Image { Source = item.Image },
+                        Command = item.Action != null
+                            ? new RelayCommand(() => item.Action())
+                            : null
+                    };
+                    targetItems.Add(targetItem);
+                }
             }
             else if (sourceItem is CoreMenuHeader<IImage> header)
             {

+ 39 - 0
InABox.Avalonia/Components/SearchBar/SearchBar.axaml

@@ -0,0 +1,39 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+			 xmlns:components="clr-namespace:InABox.Avalonia.Components"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="100"
+             x:Class="InABox.Avalonia.Components.SearchBar"
+			 x:DataType="components:SearchBar">
+	<Border Background="{StaticResource PrsTileBackground}"
+			BorderThickness="{StaticResource PrsBorderThickness}"
+			BorderBrush="{StaticResource PrsTileBorder}"
+			CornerRadius="{StaticResource PrsCornerRadius}"
+			Height="40"
+			Padding="3">
+		<Grid>
+			<Grid.ColumnDefinitions>
+				<ColumnDefinition Width="Auto"/>
+				<ColumnDefinition Width="*"/>
+			</Grid.ColumnDefinitions>
+			<Image Classes="Small" Source="{SvgImage /Images/search.svg}"
+				   Grid.Column="0"/>
+			<TextBox Name="TextBox"
+					 Grid.Column="1"
+					 Watermark="{Binding $parent[components:SearchBar].PlaceholderText}"
+					 Text="{Binding $parent[components:SearchBar].Text}"
+					 TextChanged="TextBox_TextChanged"
+					 VerticalAlignment="Center"
+					 VerticalContentAlignment="Center"
+					 Background="Transparent"
+					 Margin="0"
+					 BorderBrush="Transparent"
+					 BorderThickness="0">
+				<TextBox.KeyBindings>
+					<KeyBinding Command="{Binding $parent[components:SearchBar].Command}" Gesture="Enter"/>
+				</TextBox.KeyBindings>
+			</TextBox>
+		</Grid>
+	</Border>
+</UserControl>

+ 59 - 0
InABox.Avalonia/Components/SearchBar/SearchBar.axaml.cs

@@ -0,0 +1,59 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Markup.Xaml;
+using System.Windows.Input;
+
+namespace InABox.Avalonia.Components;
+
+public partial class SearchBar : UserControl
+{
+    public static readonly StyledProperty<string> PlaceholderTextProperty =
+        AvaloniaProperty.Register<SearchBar, string>(nameof(PlaceholderText), "Search...");
+
+    public string PlaceholderText
+    {
+        get => GetValue(PlaceholderTextProperty);
+        set => SetValue(PlaceholderTextProperty, value);
+    }
+
+    public static readonly StyledProperty<ICommand?> CommandProperty =
+        AvaloniaProperty.Register<SearchBar, ICommand?>(nameof(Command), null);
+   
+    public ICommand? Command
+    {
+        get => GetValue(CommandProperty);
+        set => SetValue(CommandProperty, value);
+    }
+
+    public static readonly StyledProperty<string> TextProperty =
+        AvaloniaProperty.Register<SearchBar, string>(nameof(Text), "", defaultBindingMode: BindingMode.TwoWay);
+
+    public string Text
+    {
+        get => GetValue(TextProperty);
+        set => SetValue(TextProperty, value);
+    }
+
+    public static readonly StyledProperty<bool> SearchOnChangedProperty =
+        AvaloniaProperty.Register<SearchBar, bool>(nameof(SearchOnChanged), true);
+
+    public bool SearchOnChanged
+    {
+        get => GetValue(SearchOnChangedProperty);
+        set => SetValue(SearchOnChangedProperty, value);
+    }
+
+    public SearchBar()
+    {
+        InitializeComponent();
+    }
+
+    private void TextBox_TextChanged(object? sender, TextChangedEventArgs e)
+    {
+        if (SearchOnChanged && Command is not null && Command.CanExecute(null))
+        {
+            Command.Execute(null);
+        }
+    }
+}

+ 1 - 0
InABox.Avalonia/DataModels/CoreRepository.cs

@@ -477,6 +477,7 @@ namespace InABox.Avalonia
                     Dispatcher.UIThread.Post(() => OnPropertyChanged(nameof(FiltersVisible)));
                 }
                 
+
                 DoBeforeLoad();
                 BeforeLoad(_query);
                 

+ 203 - 0
InABox.Avalonia/Geolocation.cs

@@ -0,0 +1,203 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace InABox.Avalonia
+{
+    public class GPSLocation
+    {
+        // public delegate void LocationEvent(GPSLocation sender);
+        // public event LocationEvent? OnLocationFound;
+
+        // public delegate void LocationError(GPSLocation sender, Exception error);
+        // public event LocationError? OnLocationError;
+
+        public TimeSpan ScanDelay { get; set; }
+
+        public double Latitude { get; private set; }
+        public double Longitude { get; private set; }
+        public String Address { get; private set; }
+
+        // private bool bLocating = false;
+        public DateTime TimeStamp { get; private set; }
+
+        public GPSLocation() : base()
+        {
+            TimeStamp = DateTime.MinValue;
+            ScanDelay = new TimeSpan(0, 0, 0);
+            Address = "Searching for GPS";
+        }
+
+        public bool RecentlyLocated
+        {
+            get
+            {
+                return (DateTime.Now.Subtract(TimeStamp).Ticks < ScanDelay.Ticks);
+            }
+        }
+
+        private double DistanceBetween(double sLatitude, double sLongitude, double eLatitude,
+                               double eLongitude)
+        {
+            var radiansOverDegrees = (Math.PI / 180.0);
+
+            var sLatitudeRadians = sLatitude * radiansOverDegrees;
+            var sLongitudeRadians = sLongitude * radiansOverDegrees;
+            var eLatitudeRadians = eLatitude * radiansOverDegrees;
+            var eLongitudeRadians = eLongitude * radiansOverDegrees;
+
+            var dLongitude = eLongitudeRadians - sLongitudeRadians;
+            var dLatitude = eLatitudeRadians - sLatitudeRadians;
+
+            var result1 = Math.Pow(Math.Sin(dLatitude / 2.0), 2.0) +
+                          Math.Cos(sLatitudeRadians) * Math.Cos(eLatitudeRadians) *
+                          Math.Pow(Math.Sin(dLongitude / 2.0), 2.0);
+
+            // Using 3956 as the number of miles around the earth
+            var result2 = 3956.0 * 2.0 *
+                          Math.Atan2(Math.Sqrt(result1), Math.Sqrt(1.0 - result1));
+
+            return result2;
+        }
+
+        // public void GetLocation(bool skiprecentlylocated = false)
+        // {
+        //     if (bLocating || RecentlyLocated)
+        //     {
+        //         if (!skiprecentlylocated)
+        //             return;
+        //     }
+
+        //     bLocating = true;
+
+        //     // Don't reset this on every refresh, otherwise the final UI will randomly get "Searching for GPS" as the address
+        //     //Address = "Searching for GPS";
+
+        //     bool bOK = MobileUtils.IsPermitted<Permissions.LocationWhenInUse>().Result;
+        //     
+        //     Task.Run(async () =>
+        //     {
+
+        //         try
+        //         {
+        //             
+        //             if (!bOK)
+        //             {
+        //                 Latitude = 0.0F;
+        //                 Longitude = 0.0F;
+        //                 TimeStamp = DateTime.MinValue;
+        //                 Address = "GPS Services Disabled";
+        //                 Device.BeginInvokeOnMainThread(() =>
+        //                 {
+        //                     OnLocationError?.Invoke(this, new Exception("Please enable GPS Services to continue"));
+        //                 });
+        //                 bOK = false;
+        //                 bLocating = false;
+        //             }
+        //             else
+        //             {
+
+        //                 try
+        //                 {
+        //                     var request = new GeolocationRequest(GeolocationAccuracy.Best, new TimeSpan(0, 0, 20));
+        //                     var location = await Geolocation.GetLocationAsync(request);
+
+        //                     if (location != null)
+        //                     {
+
+        //                         //if (location.IsFromMockProvider)
+        //                         //{
+        //                         //    Device.BeginInvokeOnMainThread(() =>
+        //                         //    {
+        //                         //        OnLocationError?.Invoke(this, new Exception("Mock GPS Location Detected!\nPlease correct and restart TimeBench."));
+        //                         //    });
+        //                         //}
+        //                         //else
+        //                         {
+
+        //                             Latitude = location.Latitude;
+        //                             Longitude = location.Longitude;
+        //                             TimeStamp = DateTime.Now;
+
+
+        //                             String sErr = "";
+        //                             Placemark address = null;
+        //                             try
+        //                             {
+        //                                 var addresses = await Geocoding.GetPlacemarksAsync(Latitude, Longitude);
+        //                                 double maxdist = double.MaxValue;
+        //                                 foreach (var cur in addresses.Where(x => !String.IsNullOrEmpty(x.Thoroughfare)))
+        //                                 {
+        //                                     var delta = Location.CalculateDistance(location, cur.Location, DistanceUnits.Kilometers);
+        //                                     if (delta < maxdist)
+        //                                     {
+        //                                         address = cur;
+        //                                         maxdist = delta;
+        //                                     }
+        //                                 }
+
+        //                             }
+        //                     
+        //                             catch (Exception ee2)
+        //                             {
+        //                                 sErr = ee2.Message;
+        //                                 //address = null;
+        //                             }
+        //                             if (address != null)
+        //                                 Address = String.Format("{0} {1} {2}", address.SubThoroughfare, address.Thoroughfare, address.Locality);
+        //                             else
+        //                                 Address = String.Format("Lat: {0}, Lng: {1}", Latitude, Longitude);
+
+        //                             if (location.IsFromMockProvider)
+        //                                 Address = "** " + Address;
+
+        //                             if (!String.IsNullOrEmpty(sErr))
+        //                                 Address = String.Format("{0} (ERROR: {1})", Address, sErr);
+
+        //                             Device.BeginInvokeOnMainThread(() =>
+        //                             {
+        //                                 OnLocationFound?.Invoke(this);
+        //                             });
+        //                             bLocating = false;
+        //                         }
+
+        //                     }
+        //                     else
+        //                     {
+        //                         Latitude = 0.00;
+        //                         Longitude = 0.00;
+        //                         TimeStamp = DateTime.MinValue;
+        //                         bLocating = false;
+        //                         Device.BeginInvokeOnMainThread(() =>
+        //                         {
+        //                             OnLocationError?.Invoke(this, new Exception("Unable to get GPS Location"));
+        //                         });
+
+        //                     }
+
+        //                 }
+        //                 catch (Exception e)
+        //                 {
+        //                     bLocating = false;
+        //                     Device.BeginInvokeOnMainThread(() =>
+        //                     {
+        //                         OnLocationError?.Invoke(this, e);
+        //                     });
+        //                 }
+        //             }
+        //         }
+        //         catch (Exception e)
+        //         {
+        //             
+        //         }
+
+        //         bLocating = false;
+        //     });
+
+
+        // }
+
+    }
+}

+ 28 - 0
InABox.Avalonia/Images/Images.cs

@@ -0,0 +1,28 @@
+using System;
+using System.Reflection;
+using Avalonia.Svg.Skia;
+using InABox.Avalonia.Components;
+using Syncfusion.Pdf.Parsing;
+
+namespace PRS.Avalonia;
+
+public static class Images
+{
+    
+    public static SvgImage? LoadSVG(string image, Assembly? assembly = null)
+    {
+        SvgImage? result = null;
+        if (!string.IsNullOrWhiteSpace(image))
+        {
+            SvgSource.EnableThrowOnMissingResource = false;
+            var source = assembly != null 
+                ? SvgSource.Load($"avares://{assembly.GetName().Name}{image}")
+                : SvgSource.Load($"avares://{Assembly.GetCallingAssembly().GetName().Name}{image}"); 
+            result = new SvgImage { Source = source };
+        }
+
+        return result;
+    }
+    
+    public static SvgImage? search => LoadSVG("/Images/search.svg");
+}

+ 11 - 0
InABox.Avalonia/Images/search.svg

@@ -0,0 +1,11 @@
+<svg height="512" viewBox="0 0 32 32" width="512" xmlns="http://www.w3.org/2000/svg">
+    <g id="Ikon">
+        <path d="m22 19.81-2.1 2.1-2.62-2.62a8.9081 8.9081 0 0 0 2.08-2.12z" fill="#757575"/>
+        <path d="m28.1 28.1a2.5107 2.5107 0 0 1 -3.54 0l-5.42-5.43 3.53-3.53 5.43 5.42a2.5169 2.5169 0 0 1 0 3.54z"
+              fill="#f9a825"/>
+        <circle cx="12" cy="12" fill="#eee" r="9"/>
+    </g>
+    <g id="Line">
+        <path d="m28.8066 23.8521-5.43-5.42a.9944.9944 0 0 0 -1.369-.0286l-1.3484-1.3484c5.2893-8.5045-5.0935-19.9918-14.7392-12.9946-10.0739 8.1359.6986 22.8215 11.2162 16.5l1.35 1.35a1.0066 1.0066 0 0 0 -.0538 1.4664l5.4917 5.4951a3.5025 3.5025 0 0 0 4.8825-5.0199zm-21.6707-18.2035c8.5533-6.3148 18.0541 6.0889 9.7285 12.7027-8.5535 6.315-18.0544-6.0888-9.7285-12.7027zm11.6307 13.7137q.3582-.33.68-.6909l1.1393 1.1386-.686.686zm8.6553 8a1.4842 1.4842 0 0 1 -2.1538.0308l-4.7144-4.7231 2.1163-2.1163 4.7188 4.71a1.5211 1.5211 0 0 1 .0331 2.0991z"/>
+    </g>
+</svg>

+ 14 - 0
InABox.Avalonia/InABox.Avalonia.csproj

@@ -7,8 +7,13 @@
         <RootNamespace>InABox.Avalonia</RootNamespace>
         <LangVersion>default</LangVersion>
         <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
+        <Configurations>Debug;Release;DebugDev</Configurations>
     </PropertyGroup>
 
+    <ItemGroup>
+      <None Remove="Images\search.svg" />
+    </ItemGroup>
+
     <ItemGroup>
       <ProjectReference Include="..\InABox.Avalonia.Platform\InABox.Avalonia.Platform.csproj" />
       <ProjectReference Include="..\InABox.Core\InABox.Core.csproj" />
@@ -43,11 +48,20 @@
       <AdditionalFiles Include="Components\Modules\ModuleList\AvaloniaModuleList.axaml" />
     </ItemGroup>
 
+    <ItemGroup>
+      <AvaloniaResource Include="Images\search.svg">
+        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+      </AvaloniaResource>
+    </ItemGroup>
+
     <ItemGroup>
       <Compile Update="Components\ModuleGrid\PrsModuleGrid.axaml.cs">
         <DependentUpon>PrsModuleGrid.axaml</DependentUpon>
         <SubType>Code</SubType>
       </Compile>
+      <Compile Update="Components\SearchBar\SearchBar.axaml.cs">
+        <DependentUpon>SearchBar.axaml</DependentUpon>
+      </Compile>
     </ItemGroup>
 
     <ItemGroup>

+ 4 - 0
InABox.Avalonia/Navigation/Navigation.cs

@@ -57,6 +57,7 @@ public static class Navigation
     public static async Task<object?> Popup<T>(T viewModel, bool canTapAway = true)
         where T : IViewModelBase, IPopupViewModel
     {
+        await viewModel.Activate();
         var _result = await DialogHostAvalonia.DialogHost.Show(viewModel, (object sender, DialogClosingEventArgs eventArgs) =>
         {
             if(!canTapAway && !viewModel.IsClosed)
@@ -64,6 +65,7 @@ public static class Navigation
                 eventArgs.Cancel();
             }
         });
+        await viewModel.Deactivate();
         return _result;
     }
 
@@ -77,6 +79,7 @@ public static class Navigation
     public static async Task<TResult?> Popup<T, TResult>(T viewModel, bool canTapAway = true)
         where T : IViewModelBase, IPopupViewModel<TResult>
     {
+        await viewModel.Activate();
         var _result = await DialogHostAvalonia.DialogHost.Show(viewModel, (object sender, DialogClosingEventArgs eventArgs) =>
         {
             if(!canTapAway && !viewModel.IsClosed)
@@ -84,6 +87,7 @@ public static class Navigation
                 eventArgs.Cancel();
             }
         });
+        await viewModel.Deactivate();
         return viewModel.GetResult();
     }
 

+ 1 - 1
InABox.Avalonia/Theme/Classes/Border.axaml

@@ -1,7 +1,7 @@
 <Styles xmlns="https://github.com/avaloniaui"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
 
-    <Style Selector="Border">
+    <Style Selector="Border.Standard">
         <Setter Property="BorderBrush" Value="{DynamicResource PrsTileBorder}" />
         <Setter Property="BorderThickness">
             <Setter.Value>

+ 9 - 15
InABox.Avalonia/Theme/Classes/TextBox.axaml

@@ -1,14 +1,12 @@
 <Styles xmlns="https://github.com/avaloniaui"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
-
     <Style Selector="TextBox">
-        <Setter Property="BorderThickness" Value="0" />
-        <!-- <Setter Property="BorderBrush" Value="{DynamicResource PrsTileBorder}" /> -->
-        <!-- <Setter Property="BorderThickness"> -->
-        <!--     <Setter.Value> -->
-        <!--         <DynamicResource ResourceKey="PrsBorderThickness" /> -->
-        <!--     </Setter.Value> -->
-        <!-- </Setter> -->
+        <Setter Property="BorderBrush" Value="{DynamicResource PrsTileBorder}" />
+        <Setter Property="BorderThickness">
+            <Setter.Value>
+                <DynamicResource ResourceKey="PrsBorderThickness" />
+            </Setter.Value>
+        </Setter>
         <Setter Property="Background" Value="{DynamicResource PrsTileBackground}" />
         <Setter Property="Foreground" Value="{DynamicResource PrsTileForeground}" />
         <Setter Property="CornerRadius" Value="{DynamicResource PrsCornerRadius}" />
@@ -34,12 +32,8 @@
     </Style>
     
     <Style Selector="TextBox:focus /template/ Border#PART_BorderElement">
-        <Setter Property="BorderThickness" Value="0" />
-        <Setter Property="Background" Value="{DynamicResource PrsTileBackground}" />
+        <Setter Property="BorderBrush" Value="{TemplateBinding BorderBrush}" />
+		<Setter Property="BorderThickness" Value="{TemplateBinding BorderThickness}"/>
+        <Setter Property="Background" Value="{TemplateBinding Background}" />
     </Style>
-
-
-
-
-
 </Styles>

+ 4 - 4
InABox.Core/CoreMenu/CoreMenu.cs

@@ -9,16 +9,16 @@ namespace InABox.Core
     {
         public List<ICoreMenuItem> Items { get; } = new List<ICoreMenuItem>();
 
-        public CoreMenu<T> AddItem(string header, T? image, Func<Task<bool>> action)
+        public CoreMenu<T> AddItem(string header, T? image, Func<Task<bool>> action, Func<bool>? isVisible = null)
         {
-            var result = new CoreMenuItem<T>(header, image, action);
+            var result = new CoreMenuItem<T>(header, image, action, isVisible);
             Items.Add(result);
             return this;
         }
 
-        public CoreMenu<T> AddItem(string header, Func<Task<bool>> action)
+        public CoreMenu<T> AddItem(string header, Func<Task<bool>> action, Func<bool>? isVisible = null)
         {
-            var result = new CoreMenuItem<T>(header, null, action);
+            var result = new CoreMenuItem<T>(header, null, action, isVisible);
             Items.Add(result);
             return this;
         }

+ 4 - 1
InABox.Core/CoreMenu/CoreMenuItem.cs

@@ -9,11 +9,14 @@ namespace InABox.Core
         public T? Image { get; set; }
         public Func<Task<bool>>? Action { get; set; }
 
-        public CoreMenuItem(string header, T? image = null, Func<Task<bool>>? action = null)
+        public Func<bool>? IsVisible { get; set; }
+
+        public CoreMenuItem(string header, T? image = null, Func<Task<bool>>? action = null, Func<bool>? isVisible = null)
         {
             Header = header;
             Image = image;
             Action = action;
+            IsVisible = isVisible;
         }
     }
 }

+ 2 - 0
InABox.Core/CoreUtils.cs

@@ -2862,6 +2862,8 @@ namespace InABox.Core
             return thisList.All(x => list.Contains(x, comparer));
         }
 
+        public static bool Contains<T>(this T[] arr, T value) => Array.IndexOf(arr, value) != -1;
+
         #endregion
 
     }

+ 62 - 0
InABox.Core/Expression/ArrayFunctions.cs

@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Text;
+
+namespace InABox.Core
+{
+    internal static class ArrayFunctions
+    {
+        public const string GROUP = "Array";
+
+        public static void Register()
+        {
+            CoreExpression.RegisterFunction("Array", "Create a new array, with the given type and length.", GROUP, new[]
+            {
+                "string type", "int length"
+            }, (p, v, c) =>
+            {
+                var type = ConvertType(p[0].Evaluate<string>(v));
+                var length = p[1].Evaluate<int>(v);
+                return Array.CreateInstance(type, length);
+            });
+        }
+
+        private static readonly Dictionary<string, Type> _types = new Dictionary<string, Type>()
+        {
+            { "sbyte", typeof(sbyte) },
+            { "byte", typeof(byte) },
+            { "short", typeof(short) },
+            { "ushort", typeof(ushort) },
+            { "int", typeof(int) },
+            { "uint", typeof(uint) },
+            { "long", typeof(long) },
+            { "ulong", typeof(ulong) },
+
+            { "float", typeof(float) },
+            { "double", typeof(double) },
+            { "decimal", typeof(decimal) },
+
+            { "bool", typeof(bool) },
+            { "char", typeof(char) },
+            { "string", typeof(string) },
+
+            { "TimeSpan", typeof(TimeSpan) },
+            { "DateTime", typeof(DateTime) },
+        };
+
+        public static bool TryConvertType(string str, [NotNullWhen(true)] out Type? type)
+        {
+            if(_types.TryGetValue(str, out type)) return true;
+            type = Type.GetType(str);
+            if (type != null) return true;
+            return false;
+        }
+
+        public static Type ConvertType(string str)
+        {
+            if(TryConvertType(str, out var type)) return type;
+            throw new Exception($"Invalid type {str}");
+        }
+    }
+}

+ 67 - 3
InABox.Core/CoreExpression.cs → InABox.Core/Expression/CoreExpression.cs

@@ -9,6 +9,7 @@ using System.Linq;
 using System.Text;
 using System.Text.RegularExpressions;
 using Expressive.Exceptions;
+using Expressive.Operators;
 
 namespace InABox.Core
 {
@@ -21,14 +22,58 @@ namespace InABox.Core
         public abstract string Description { get; }
         public abstract string[] Parameters { get; }
     }
-    
+
+    public interface IExpressionConstant
+    {
+        string Name { get; set; }
+
+        object? GetValue();
+    }
+
+    public class CoreExpressionFunctor : CoreExpressionFunction
+    {
+        public delegate object? EvaluateDelegate(IExpression[] parameters, IDictionary<string, object?> variables, Context context);
+
+        public EvaluateDelegate Function { get; set; }
+
+        private string _name;
+        public override string Name => _name;
+
+        private string _description;
+        public override string Description => _description;
+
+        private string _group;
+        public override string Group => _group;
+
+        private string[] _parameters;
+        public override string[] Parameters => _parameters;
+
+        private int minParameters;
+
+        public CoreExpressionFunctor(EvaluateDelegate function, string name, string description, string group, string[] parameters, int? minParameters = null)
+        {
+            Function = function;
+            _name = name;
+            _description = description;
+            _group = group;
+            _parameters = parameters;
+            minParameters = minParameters ?? parameters.Length;
+        }
+
+        public override object? Evaluate(IExpression[] parameters, Context context)
+        {
+            ValidateParameterCount(parameters, _parameters.Length, minParameters);
+            return Function(parameters, Variables, context);
+        }
+    }
+
     internal class FormatFunction : CoreExpressionFunction
     {
         #region IFunction Members
 
         public override object Evaluate(IExpression[] parameters, Context context)
         {
-            ValidateParameterCount(parameters,-1, 2);
+            ValidateParameterCount(parameters, -1, 2);
 
             string fmt = parameters.First()?.Evaluate(Variables).ToString() ?? string.Empty;
             object[] objects = parameters.Skip(1).Select(x => x.Evaluate(Variables)).ToArray();
@@ -174,17 +219,27 @@ namespace InABox.Core
         
         private static Context _context = new Context(ExpressiveOptions.None);
         
-        static void RegisterFunction<T>() where T : CoreExpressionFunction, new ()
+        public static void RegisterFunction<T>() where T : CoreExpressionFunction, new ()
         {
             var function = new T();
             Functions.Add(function);
             _context.RegisterFunction(function);
         }
+
+        public static void RegisterFunction(string name, string description, string group, string[] parameters, CoreExpressionFunctor.EvaluateDelegate function)
+        {
+            var f = new CoreExpressionFunctor(function, name, description, group, parameters);
+            Functions.Add(f);
+            _context.RegisterFunction(f);
+        }
         
         static CoreExpression()
         {
             RegisterFunction<FormatFunction>();
             RegisterFunction<Client_LoadDocumentFunction>();
+
+            DateFunctions.Register();
+            ArrayFunctions.Register();
         }
 
         #endregion
@@ -242,4 +297,13 @@ namespace InABox.Core
             }
         }
     }
+
+    public static class CoreExpressionExtensions
+    {
+        public static T Evaluate<T>(this IExpression expression, IDictionary<string, object?>? variables)
+        {
+            var result = expression.Evaluate(variables);
+            return CoreUtils.ChangeType<T>(result);
+        }
+    }
 }

+ 17 - 0
InABox.Core/Expression/DateFunctions.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace InABox.Core
+{
+    internal static class DateFunctions
+    {
+        public const string GROUP = "Date";
+
+        public static void Register()
+        {
+            CoreExpression.RegisterFunction("Now", "Returns the current date and time.", GROUP, new string[] {}, (p, vars, c) => DateTime.Now);
+            CoreExpression.RegisterFunction("Today", "Returns the current date.", GROUP, new string[] {}, (p, vars, c) => DateTime.Today);
+        }
+    }
+}

+ 1 - 0
inabox.wpf/DynamicGrid/DynamicEditorForm/DynamicEditorForm.xaml

@@ -10,6 +10,7 @@
                              Title="Dynamic Editor"
                              WindowStartupLocation="CenterScreen"
                              Closing="Window_Closing"
+                             Loaded="ThemableChromelessWindow_Loaded"
                              syncfusion:SkinStorage.VisualStyle="Metro"
                              TitleTextAlignment="Center"
                              IconAlignment="Left"

+ 9 - 1
inabox.wpf/DynamicGrid/DynamicEditorForm/DynamicEditorForm.xaml.cs

@@ -216,8 +216,11 @@ public partial class DynamicEditorForm : ThemableChromelessWindow, IDynamicEdito
         var desiredheight = Form.ContentHeight + spareheight;
         var desiredwidth = Form.ContentWidth + sparewidth;
 
-        var maxheight = screen.WorkingArea.Height - 0;
+        var oldHeight = Height;
+
+        var maxheight = screen.WorkingArea.Height - 300;
         Height = desiredheight > maxheight ? maxheight : desiredheight;
+        Top += (oldHeight - Height) / 2;
 
         var maxwidth = screen.WorkingArea.Width - 0;
         Width = desiredwidth > maxwidth ? maxwidth : desiredwidth;
@@ -264,4 +267,9 @@ public partial class DynamicEditorForm : ThemableChromelessWindow, IDynamicEdito
     }
 
     #endregion
+
+    private void ThemableChromelessWindow_Loaded(object sender, RoutedEventArgs e)
+    {
+        SetSize();
+    }
 }