mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-11-12 03:48:56 +00:00
UI - Added routing (#802)
ColorGradient - Fixed GetColor returning the first stop if position was after the last stop
This commit is contained in:
1 parent
79309dcee7
commit
962b13c6ed
77 files changed
+2034
-854
No files matched your search
@@ -2,6 +2,9 @@
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=controls/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=exceptions/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=routing_005Croutable/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=routing_005Croute/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=routing_005Crouter/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cwindow/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cwindows/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cwindowservice/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Shared.Events;
|
||||
@@ -26,6 +27,7 @@ namespace Artemis.UI.Shared;
|
||||
/// </summary>
|
||||
public class DeviceVisualizer : Control
|
||||
{
|
||||
internal static readonly Dictionary<ArtemisDevice, RenderTargetBitmap?> BitmapCache = new();
|
||||
private readonly ICoreService _coreService;
|
||||
private readonly List<DeviceVisualizerLed> _deviceVisualizerLeds;
|
||||
|
||||
@@ -160,7 +162,7 @@ public class DeviceVisualizer : Control
|
||||
|
||||
return geometry.Bounds;
|
||||
}
|
||||
|
||||
|
||||
private void OnFrameRendered(object? sender, FrameRenderedEventArgs e)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
@@ -195,12 +197,14 @@ public class DeviceVisualizer : Control
|
||||
|
||||
private void DevicePropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
Dispatcher.UIThread.Post(SetupForDevice, DispatcherPriority.Background);
|
||||
if (Device != null)
|
||||
BitmapCache.Remove(Device);
|
||||
Dispatcher.UIThread.Invoke(SetupForDevice, DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
private void DeviceUpdated(object? sender, EventArgs e)
|
||||
{
|
||||
Dispatcher.UIThread.Post(SetupForDevice, DispatcherPriority.Background);
|
||||
Dispatcher.UIThread.Invoke(SetupForDevice, DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
#region Properties
|
||||
@@ -242,9 +246,6 @@ public class DeviceVisualizer : Control
|
||||
/// <inheritdoc />
|
||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
_deviceImage?.Dispose();
|
||||
_deviceImage = null;
|
||||
|
||||
if (Device != null)
|
||||
{
|
||||
Device.RgbDevice.PropertyChanged -= DevicePropertyChanged;
|
||||
@@ -270,7 +271,7 @@ public class DeviceVisualizer : Control
|
||||
base.OnDetachedFromLogicalTree(e);
|
||||
}
|
||||
|
||||
private void SetupForDevice()
|
||||
private async Task SetupForDevice()
|
||||
{
|
||||
lock (_deviceVisualizerLeds)
|
||||
{
|
||||
@@ -302,46 +303,47 @@ public class DeviceVisualizer : Control
|
||||
|
||||
// Load the device main image on a background thread
|
||||
ArtemisDevice? device = Device;
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
if (device.Layout?.Image == null || !File.Exists(device.Layout.Image.LocalPath))
|
||||
{
|
||||
_deviceImage?.Dispose();
|
||||
_deviceImage = null;
|
||||
return;
|
||||
}
|
||||
_deviceImage = await Task.Run(() => GetDeviceImage(device));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
// Create a bitmap that'll be used to render the device and LED images just once
|
||||
// Render 4 times the actual size of the device to make sure things look sharp when zoomed in
|
||||
RenderTargetBitmap renderTargetBitmap = new(new PixelSize((int) device.RgbDevice.ActualSize.Width * 2, (int) device.RgbDevice.ActualSize.Height * 2));
|
||||
InvalidateMeasure();
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
using DrawingContext context = renderTargetBitmap.CreateDrawingContext();
|
||||
using Bitmap bitmap = new(device.Layout.Image.LocalPath);
|
||||
using Bitmap scaledBitmap = bitmap.CreateScaledBitmap(renderTargetBitmap.PixelSize);
|
||||
private RenderTargetBitmap? GetDeviceImage(ArtemisDevice device)
|
||||
{
|
||||
if (BitmapCache.TryGetValue(device, out RenderTargetBitmap? existingBitmap))
|
||||
return existingBitmap;
|
||||
|
||||
context.DrawImage(scaledBitmap, new Rect(scaledBitmap.Size));
|
||||
lock (_deviceVisualizerLeds)
|
||||
{
|
||||
foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds)
|
||||
deviceVisualizerLed.DrawBitmap(context, 2 * Device.Scale);
|
||||
}
|
||||
if (device.Layout?.Image == null || !File.Exists(device.Layout.Image.LocalPath))
|
||||
{
|
||||
BitmapCache[device] = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
_deviceImage?.Dispose();
|
||||
_deviceImage = renderTargetBitmap;
|
||||
// Create a bitmap that'll be used to render the device and LED images just once
|
||||
// Render 4 times the actual size of the device to make sure things look sharp when zoomed in
|
||||
RenderTargetBitmap renderTargetBitmap = new(new PixelSize((int) device.RgbDevice.ActualSize.Width * 2, (int) device.RgbDevice.ActualSize.Height * 2));
|
||||
|
||||
InvalidateMeasure();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
});
|
||||
using DrawingContext context = renderTargetBitmap.CreateDrawingContext();
|
||||
using Bitmap bitmap = new(device.Layout.Image.LocalPath);
|
||||
using Bitmap scaledBitmap = bitmap.CreateScaledBitmap(renderTargetBitmap.PixelSize);
|
||||
|
||||
context.DrawImage(scaledBitmap, new Rect(scaledBitmap.Size));
|
||||
lock (_deviceVisualizerLeds)
|
||||
{
|
||||
foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds)
|
||||
deviceVisualizerLed.DrawBitmap(context, 2 * device.Scale);
|
||||
}
|
||||
|
||||
BitmapCache[device] = renderTargetBitmap;
|
||||
return renderTargetBitmap;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -48,11 +48,14 @@ public partial class ProfileConfigurationIcon : UserControl, IDisposable
|
||||
}
|
||||
else
|
||||
{
|
||||
Stream? stream = ConfigurationIcon.GetIconStream();
|
||||
if (stream == null)
|
||||
Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark};
|
||||
else
|
||||
LoadFromBitmap(ConfigurationIcon, stream);
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
Stream? stream = ConfigurationIcon.GetIconStream();
|
||||
if (stream == null)
|
||||
Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark};
|
||||
else
|
||||
LoadFromBitmap(ConfigurationIcon, stream);
|
||||
}, DispatcherPriority.ApplicationIdle);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
@@ -97,12 +100,12 @@ public partial class ProfileConfigurationIcon : UserControl, IDisposable
|
||||
if (e.NewValue is Core.ProfileConfigurationIcon newIcon)
|
||||
newIcon.IconUpdated += ConfigurationIconOnIconUpdated;
|
||||
|
||||
Dispatcher.UIThread.Post(Update, DispatcherPriority.ApplicationIdle);
|
||||
Update();
|
||||
}
|
||||
|
||||
private void ConfigurationIconOnIconUpdated(object? sender, EventArgs e)
|
||||
{
|
||||
Dispatcher.UIThread.Post(Update, DispatcherPriority.ApplicationIdle);
|
||||
Update();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Reflection;
|
||||
using Artemis.UI.Shared.Routing;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using DryIoc;
|
||||
|
||||
@@ -15,6 +16,7 @@ public static class ContainerExtensions
|
||||
/// <param name="container">The builder building the current container</param>
|
||||
public static void RegisterSharedUI(this IContainer container)
|
||||
{
|
||||
container.Register<IRouter, Router>(Reuse.Singleton);
|
||||
Assembly artemisShared = typeof(IArtemisSharedUIService).GetAssembly();
|
||||
container.RegisterMany(new[] {artemisShared}, type => type.IsAssignableTo<IArtemisSharedUIService>(), Reuse.Singleton);
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
|
||||
namespace Artemis.UI.Shared;
|
||||
|
||||
public class ArtemisRoutingException : Exception
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public ArtemisRoutingException()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ArtemisRoutingException(string? message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ArtemisRoutingException(string? message, Exception? innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Shared.Routing;
|
||||
|
||||
/// <summary>
|
||||
/// For internal use.
|
||||
/// </summary>
|
||||
/// <seealso cref="RoutableScreen{TScreen}"/>
|
||||
/// <seealso cref="RoutableScreen{TScreen, TParam}"/>
|
||||
internal interface IRoutableScreen : IActivatableViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not to reuse the child screen instance if the type has not changed.
|
||||
/// </summary>
|
||||
/// <remarks>Defaults to <see langword="true"/>.</remarks>
|
||||
bool RecycleScreen { get; }
|
||||
|
||||
object? InternalScreen { get; }
|
||||
void InternalChangeScreen(object? screen);
|
||||
Task InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken);
|
||||
Task InternalOnClosing(NavigationArguments args);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Artemis.UI.Shared.Routing;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a view model to which routing can take place and which in turn can host another view model.
|
||||
/// </summary>
|
||||
/// <typeparam name="TScreen">The type of view model the screen can host.</typeparam>
|
||||
public abstract class RoutableScreen<TScreen> : ActivatableViewModelBase, IRoutableScreen where TScreen : class
|
||||
{
|
||||
private TScreen? _screen;
|
||||
private bool _recycleScreen = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the currently active child screen.
|
||||
/// </summary>
|
||||
public TScreen? Screen
|
||||
{
|
||||
get => _screen;
|
||||
private set => RaiseAndSetIfChanged(ref _screen, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool RecycleScreen
|
||||
{
|
||||
get => _recycleScreen;
|
||||
protected set => RaiseAndSetIfChanged(ref _recycleScreen, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called before navigating to this screen.
|
||||
/// </summary>
|
||||
/// <param name="args">Navigation arguments containing information about the navigation action.</param>
|
||||
public virtual Task BeforeNavigating(NavigationArguments args)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called while navigating to this screen.
|
||||
/// </summary>
|
||||
/// <param name="args">Navigation arguments containing information about the navigation action.</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
||||
public virtual Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called before navigating away from this screen.
|
||||
/// </summary>
|
||||
/// <param name="args">Navigation arguments containing information about the navigation action.</param>
|
||||
public virtual Task OnClosing(NavigationArguments args)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
#region Overrides of RoutableScreen
|
||||
|
||||
object? IRoutableScreen.InternalScreen => Screen;
|
||||
|
||||
void IRoutableScreen.InternalChangeScreen(object? screen)
|
||||
{
|
||||
Screen = screen as TScreen;
|
||||
}
|
||||
|
||||
async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken)
|
||||
{
|
||||
await OnNavigating(args, cancellationToken);
|
||||
}
|
||||
|
||||
async Task IRoutableScreen.InternalOnClosing(NavigationArguments args)
|
||||
{
|
||||
await OnClosing(args);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Artemis.UI.Shared.Routing;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a view model to which routing with parameters can take place and which in turn can host another view
|
||||
/// model.
|
||||
/// </summary>
|
||||
/// <typeparam name="TScreen">The type of view model the screen can host.</typeparam>
|
||||
/// <typeparam name="TParam">The type of parameters the screen expects. It must have a parameterless constructor.</typeparam>
|
||||
public abstract class RoutableScreen<TScreen, TParam> : ActivatableViewModelBase, IRoutableScreen where TScreen : class where TParam : new()
|
||||
{
|
||||
private bool _recycleScreen = true;
|
||||
private TScreen? _screen;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the currently active child screen.
|
||||
/// </summary>
|
||||
public TScreen? Screen
|
||||
{
|
||||
get => _screen;
|
||||
private set => RaiseAndSetIfChanged(ref _screen, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called before navigating to this screen.
|
||||
/// </summary>
|
||||
/// <param name="args">Navigation arguments containing information about the navigation action.</param>
|
||||
public virtual Task BeforeNavigating(NavigationArguments args)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called while navigating to this screen.
|
||||
/// </summary>
|
||||
/// <param name="parameters">An object containing the parameters of the navigation action.</param>
|
||||
/// <param name="args">Navigation arguments containing information about the navigation action.</param>
|
||||
/// <param name="cancellationToken">
|
||||
/// A cancellation token that can be used by other objects or threads to receive notice of
|
||||
/// cancellation.
|
||||
/// </param>
|
||||
public virtual Task OnNavigating(TParam parameters, NavigationArguments args, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called before navigating away from this screen.
|
||||
/// </summary>
|
||||
/// <param name="args">Navigation arguments containing information about the navigation action.</param>
|
||||
public virtual Task OnClosing(NavigationArguments args)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool RecycleScreen
|
||||
{
|
||||
get => _recycleScreen;
|
||||
protected set => RaiseAndSetIfChanged(ref _recycleScreen, value);
|
||||
}
|
||||
|
||||
#region Overrides of RoutableScreen
|
||||
|
||||
object? IRoutableScreen.InternalScreen => Screen;
|
||||
|
||||
void IRoutableScreen.InternalChangeScreen(object? screen)
|
||||
{
|
||||
Screen = screen as TScreen;
|
||||
}
|
||||
|
||||
async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken)
|
||||
{
|
||||
Func<object[], TParam> activator = GetParameterActivator();
|
||||
|
||||
if (args.SegmentParameters.Length != _parameterPropertyCount)
|
||||
throw new ArtemisRoutingException($"Did not retrieve the required amount of parameters, expects {_parameterPropertyCount}, got {args.SegmentParameters.Length}.");
|
||||
|
||||
TParam parameters = activator(args.SegmentParameters);
|
||||
await OnNavigating(parameters, args, cancellationToken);
|
||||
}
|
||||
|
||||
async Task IRoutableScreen.InternalOnClosing(NavigationArguments args)
|
||||
{
|
||||
await OnClosing(args);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Parameter generation
|
||||
|
||||
// ReSharper disable once StaticMemberInGenericType - That's intentional, each kind of TParam should have its own property count
|
||||
private static int _parameterPropertyCount;
|
||||
private static Func<object[], TParam>? _parameterActivator;
|
||||
|
||||
private static Func<object[], TParam> GetParameterActivator()
|
||||
{
|
||||
if (_parameterActivator != null)
|
||||
return _parameterActivator;
|
||||
|
||||
// Generates a lambda that creates a new instance of TParam
|
||||
// - Each property of TParam with a public setter must be set using the source object[]
|
||||
// - Use the index of each property as the index on the source array
|
||||
// - Cast the object of the source array to the correct type for that property
|
||||
Type parameterType = typeof(TParam);
|
||||
ParameterExpression sourceExpression = Expression.Parameter(typeof(object[]), "source");
|
||||
ParameterExpression parameterExpression = Expression.Parameter(parameterType, "parameters");
|
||||
|
||||
List<BinaryExpression> propertyAssignments = parameterType.GetProperties()
|
||||
.Where(p => p.CanWrite)
|
||||
.Select((property, index) =>
|
||||
{
|
||||
UnaryExpression sourceValueExpression = Expression.Convert(
|
||||
Expression.ArrayIndex(sourceExpression, Expression.Constant(index)),
|
||||
property.PropertyType
|
||||
);
|
||||
BinaryExpression propertyAssignment = Expression.Assign(
|
||||
Expression.Property(parameterExpression, property),
|
||||
sourceValueExpression
|
||||
);
|
||||
|
||||
return propertyAssignment;
|
||||
})
|
||||
.ToList();
|
||||
|
||||
Expression<Func<object[], TParam>> lambda = Expression.Lambda<Func<object[], TParam>>(
|
||||
Expression.Block(
|
||||
new[] {parameterExpression},
|
||||
Expression.Assign(parameterExpression, Expression.New(parameterType)),
|
||||
Expression.Block(propertyAssignments),
|
||||
parameterExpression
|
||||
),
|
||||
sourceExpression
|
||||
);
|
||||
|
||||
_parameterActivator = lambda.Compile();
|
||||
_parameterPropertyCount = propertyAssignments.Count;
|
||||
|
||||
return _parameterActivator;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
|
||||
namespace Artemis.UI.Shared.Routing.ParameterParsers;
|
||||
|
||||
internal class GuidParameterParser : IRouteParameterParser
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool IsMatch(RouteSegment segment, string source)
|
||||
{
|
||||
return Guid.TryParse(source, out _);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public object GetValue(RouteSegment segment, string source)
|
||||
{
|
||||
return Guid.Parse(source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Artemis.UI.Shared.Routing.ParameterParsers;
|
||||
|
||||
public interface IRouteParameterParser
|
||||
{
|
||||
bool IsMatch(RouteSegment segment, string source);
|
||||
object GetValue(RouteSegment segment, string source);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Artemis.UI.Shared.Routing.ParameterParsers;
|
||||
|
||||
internal class StringParameterParser : IRouteParameterParser
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool IsMatch(RouteSegment segment, string source)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public object GetValue(RouteSegment segment, string source)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Artemis.UI.Shared.Routing;
|
||||
|
||||
public class Route
|
||||
{
|
||||
public Route(string path)
|
||||
{
|
||||
Path = path;
|
||||
Segments = path.Split('/').Select(s => new RouteSegment(s)).ToList();
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
public List<RouteSegment> Segments { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Artemis.UI.Shared.Routing;
|
||||
|
||||
public class RouteRegistration<TViewModel> : IRouterRegistration where TViewModel : ViewModelBase
|
||||
{
|
||||
public RouteRegistration(string path)
|
||||
{
|
||||
Route = new Route(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{nameof(Route)}: {Route}, {nameof(ViewModel)}: {ViewModel}";
|
||||
}
|
||||
|
||||
public Route Route { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Type ViewModel => typeof(TViewModel);
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<IRouterRegistration> Children { get; set; } = new();
|
||||
}
|
||||
|
||||
public interface IRouterRegistration
|
||||
{
|
||||
Route Route { get; }
|
||||
Type ViewModel { get; }
|
||||
List<IRouterRegistration> Children { get; set; }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using DryIoc;
|
||||
|
||||
namespace Artemis.UI.Shared.Routing;
|
||||
|
||||
internal class RouteResolution
|
||||
{
|
||||
private RouteResolution(string path)
|
||||
{
|
||||
Path = path;
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
public bool Success { get; private set; }
|
||||
public Type? ViewModel { get; private set; }
|
||||
public object[]? Parameters { get; private set; }
|
||||
public RouteResolution? Child { get; private set; }
|
||||
|
||||
public static RouteResolution Resolve(IRouterRegistration registration, string path)
|
||||
{
|
||||
List<string> segments = path.Split('/').ToList();
|
||||
if (registration.Route.Segments.Count > segments.Count)
|
||||
return AsFailure(path);
|
||||
|
||||
// Ensure self is a match
|
||||
List<object> parameters = new();
|
||||
int currentSegment = 0;
|
||||
foreach (RouteSegment routeSegment in registration.Route.Segments)
|
||||
{
|
||||
string segment = segments[currentSegment];
|
||||
if (!routeSegment.IsMatch(segment))
|
||||
return AsFailure(path);
|
||||
|
||||
object? parameter = routeSegment.GetParameter(segment);
|
||||
if (parameter != null)
|
||||
parameters.Add(parameter);
|
||||
|
||||
currentSegment++;
|
||||
}
|
||||
|
||||
if (currentSegment == segments.Count)
|
||||
return AsSuccess(registration.ViewModel, path, parameters.ToArray());
|
||||
|
||||
// If segments remain, a child should match it
|
||||
string childPath = string.Join('/', segments.Skip(currentSegment));
|
||||
foreach (IRouterRegistration routerRegistration in registration.Children)
|
||||
{
|
||||
RouteResolution result = Resolve(routerRegistration, childPath);
|
||||
if (result.Success)
|
||||
return AsSuccess(registration.ViewModel, path, parameters.ToArray(), result);
|
||||
}
|
||||
|
||||
return AsFailure(path);
|
||||
}
|
||||
|
||||
public static RouteResolution AsFailure(string path)
|
||||
{
|
||||
return new RouteResolution(path);
|
||||
}
|
||||
|
||||
public static RouteResolution AsSuccess(Type viewModel, string path, object[] parameters, RouteResolution? child = null)
|
||||
{
|
||||
if (child != null && !child.Success)
|
||||
throw new ArtemisRoutingException("Cannot create a success route resolution with a failed child");
|
||||
|
||||
return new RouteResolution(path)
|
||||
{
|
||||
Success = true,
|
||||
ViewModel = viewModel,
|
||||
Parameters = parameters,
|
||||
Child = child
|
||||
};
|
||||
}
|
||||
|
||||
public object GetViewModel(IContainer container)
|
||||
{
|
||||
if (ViewModel == null)
|
||||
throw new ArtemisRoutingException("Cannot get a view model of a non-success route resolution");
|
||||
|
||||
object? viewModel = container.Resolve(ViewModel);
|
||||
if (viewModel == null)
|
||||
throw new ArtemisRoutingException($"Could not resolve view model of type {ViewModel}");
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
public T GetViewModel<T>(IContainer container)
|
||||
{
|
||||
if (ViewModel == null)
|
||||
throw new ArtemisRoutingException("Cannot get a view model of a non-success route resolution");
|
||||
|
||||
object? viewModel = container.Resolve(ViewModel);
|
||||
if (viewModel == null)
|
||||
throw new ArtemisRoutingException($"Could not resolve view model of type {ViewModel}");
|
||||
if (viewModel is not T typedViewModel)
|
||||
throw new ArtemisRoutingException($"View model of type {ViewModel} is does mot implement {typeof(T)}.");
|
||||
|
||||
return typedViewModel;
|
||||
}
|
||||
|
||||
public object[] GetAllParameters()
|
||||
{
|
||||
List<object> result = new();
|
||||
if (Parameters != null)
|
||||
result.AddRange(Parameters);
|
||||
object[]? childParameters = Child?.GetAllParameters();
|
||||
if (childParameters != null)
|
||||
result.AddRange(childParameters);
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using Artemis.UI.Shared.Routing.ParameterParsers;
|
||||
|
||||
namespace Artemis.UI.Shared.Routing;
|
||||
|
||||
public partial class RouteSegment
|
||||
{
|
||||
private readonly IRouteParameterParser? _parameterParser;
|
||||
|
||||
public RouteSegment(string segment)
|
||||
{
|
||||
Segment = segment;
|
||||
|
||||
Match match = ParameterRegex().Match(segment);
|
||||
if (match.Success)
|
||||
{
|
||||
Parameter = match.Groups[1].Value;
|
||||
ParameterType = match.Groups[2].Value;
|
||||
_parameterParser = GetParameterParser(ParameterType);
|
||||
}
|
||||
}
|
||||
|
||||
public string Segment { get; }
|
||||
public string? Parameter { get; }
|
||||
public string? ParameterType { get; }
|
||||
|
||||
public bool IsMatch(string value)
|
||||
{
|
||||
if (_parameterParser == null)
|
||||
return Segment.Equals(value, StringComparison.InvariantCultureIgnoreCase);
|
||||
return _parameterParser.IsMatch(this, value);
|
||||
}
|
||||
|
||||
public object? GetParameter(string value)
|
||||
{
|
||||
if (_parameterParser == null)
|
||||
return null;
|
||||
|
||||
return _parameterParser.GetValue(this, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Segment} (param: {Parameter ?? "none"}, type: {ParameterType ?? "N/A"})";
|
||||
}
|
||||
|
||||
private IRouteParameterParser GetParameterParser(string parameterType)
|
||||
{
|
||||
if (parameterType == "guid")
|
||||
return new GuidParameterParser();
|
||||
|
||||
// Default to a string parser which just returns the segment as is
|
||||
return new StringParameterParser();
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"\{(\w+):(\w+)\}")]
|
||||
private static partial Regex ParameterRegex();
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Artemis.UI.Shared.Routing;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a router that can be used to navigate to different screens.
|
||||
/// </summary>
|
||||
public interface IRouter
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets an observable containing the current path.
|
||||
/// </summary>
|
||||
IObservable<string?> CurrentPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of router registrations, you can use this to register new routes.
|
||||
/// </summary>
|
||||
List<IRouterRegistration> Routes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously navigates to the provided path.
|
||||
/// </summary>
|
||||
/// <remarks>Navigating cancels any currently processing navigations.</remarks>
|
||||
/// <param name="path">The path to navigate to.</param>
|
||||
/// <param name="options">Optional navigation options used to control navigation behaviour.</param>
|
||||
/// <returns>A task representing the operation</returns>
|
||||
Task Navigate(string path, RouterNavigationOptions? options = null);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously navigates back to the previous active route.
|
||||
/// </summary>
|
||||
/// <returns>A task containing a boolean value which indicates whether there was a previous path to go back to.</returns>
|
||||
Task<bool> GoBack();
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously navigates forward to the previous active route.
|
||||
/// </summary>
|
||||
/// <returns>A task containing a boolean value which indicates whether there was a forward path to go back to.</returns>
|
||||
Task<bool> GoForward();
|
||||
|
||||
/// <summary>
|
||||
/// Clears the navigation history.
|
||||
/// </summary>
|
||||
void ClearHistory();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the root screen from which navigation takes place.
|
||||
/// </summary>
|
||||
/// <param name="root">The root screen to set.</param>
|
||||
/// <typeparam name="TScreen">The type of the root screen. It must be a class.</typeparam>
|
||||
void SetRoot<TScreen>(RoutableScreen<TScreen> root) where TScreen : class;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the root screen from which navigation takes place.
|
||||
/// </summary>
|
||||
/// <param name="root">The root screen to set.</param>
|
||||
/// <typeparam name="TScreen">The type of the root screen. It must be a class.</typeparam>
|
||||
/// <typeparam name="TParam">The type of the parameters for the root screen. It must have a parameterless constructor.</typeparam>
|
||||
void SetRoot<TScreen, TParam>(RoutableScreen<TScreen, TParam> root) where TScreen : class where TParam : new();
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
using System;
|
||||
using System.Reactive.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Threading;
|
||||
using DryIoc;
|
||||
using ReactiveUI;
|
||||
using Serilog;
|
||||
|
||||
namespace Artemis.UI.Shared.Routing;
|
||||
|
||||
internal class Navigation
|
||||
{
|
||||
private readonly IContainer _container;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private readonly IRoutableScreen _root;
|
||||
private readonly RouteResolution _resolution;
|
||||
private readonly RouterNavigationOptions _options;
|
||||
private CancellationTokenSource _cts;
|
||||
|
||||
public Navigation(IContainer container, ILogger logger, IRoutableScreen root, RouteResolution resolution, RouterNavigationOptions options)
|
||||
{
|
||||
_container = container;
|
||||
_logger = logger;
|
||||
|
||||
_root = root;
|
||||
_resolution = resolution;
|
||||
_options = options;
|
||||
_cts = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
public bool Cancelled => _cts.IsCancellationRequested;
|
||||
public bool Completed { get; private set; }
|
||||
|
||||
public async Task Navigate(NavigationArguments args)
|
||||
{
|
||||
_logger.Information("Navigating to {Path}", _resolution.Path);
|
||||
_cts = new CancellationTokenSource();
|
||||
await NavigateResolution(_resolution, args, _root);
|
||||
|
||||
if (!Cancelled)
|
||||
_logger.Information("Navigated to {Path}", _resolution.Path);
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
if (Cancelled || Completed)
|
||||
return;
|
||||
|
||||
_logger.Information("Cancelled navigation to {Path}", _resolution.Path);
|
||||
_cts.Cancel();
|
||||
}
|
||||
|
||||
private async Task NavigateResolution(RouteResolution resolution, NavigationArguments args, IRoutableScreen host)
|
||||
{
|
||||
if (Cancelled)
|
||||
return;
|
||||
|
||||
// Reuse the screen if its type has not changed, if a new one must be created, don't do so on the UI thread
|
||||
object screen;
|
||||
if (_options.RecycleScreens && host.RecycleScreen && host.InternalScreen != null && host.InternalScreen.GetType() == resolution.ViewModel)
|
||||
screen = host.InternalScreen;
|
||||
else
|
||||
screen = await Task.Run(() => resolution.GetViewModel(_container));
|
||||
|
||||
// If resolution has a child, ensure the screen can host it
|
||||
if (resolution.Child != null && screen is not IRoutableScreen)
|
||||
throw new ArtemisRoutingException($"Route resolved with a child but view model of type {resolution.ViewModel} is does mot implement {nameof(IRoutableScreen)}.");
|
||||
|
||||
// Only change the screen if it wasn't reused
|
||||
if (!ReferenceEquals(host.InternalScreen, screen))
|
||||
host.InternalChangeScreen(screen);
|
||||
|
||||
if (CancelIfRequested(args, "ChangeScreen", screen))
|
||||
return;
|
||||
|
||||
// If the screen implements some form of Navigable, activate it
|
||||
args.SegmentParameters = resolution.Parameters ?? Array.Empty<object>();
|
||||
if (screen is IRoutableScreen routableScreen)
|
||||
{
|
||||
try
|
||||
{
|
||||
await routableScreen.InternalOnNavigating(args, _cts.Token);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Cancel();
|
||||
_logger.Error(e, "Failed to navigate to {Path}", resolution.Path);
|
||||
}
|
||||
|
||||
if (CancelIfRequested(args, "OnNavigating", screen))
|
||||
return;
|
||||
}
|
||||
|
||||
if (resolution.Child != null && screen is IRoutableScreen childScreen)
|
||||
await NavigateResolution(resolution.Child, args, childScreen);
|
||||
|
||||
Completed = true;
|
||||
}
|
||||
|
||||
public bool PathEquals(string path, bool allowPartialMatch)
|
||||
{
|
||||
if (allowPartialMatch)
|
||||
return _resolution.Path.StartsWith(path, StringComparison.InvariantCultureIgnoreCase);
|
||||
return string.Equals(_resolution.Path, path, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
private bool CancelIfRequested(NavigationArguments args, string stage, object screen)
|
||||
{
|
||||
if (Cancelled)
|
||||
return true;
|
||||
|
||||
if (!args.Cancelled)
|
||||
return false;
|
||||
|
||||
_logger.Debug("Navigation to {Path} during {Stage} by {Screen}", args.Path, stage, screen.GetType().Name);
|
||||
Cancel();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Artemis.UI.Shared.Routing;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an object that contains information about the current navigation action.
|
||||
/// </summary>
|
||||
public class NavigationArguments
|
||||
{
|
||||
internal NavigationArguments(IRouter router, string path, object[] routeParameters)
|
||||
{
|
||||
Router = router;
|
||||
Path = path;
|
||||
RouteParameters = routeParameters;
|
||||
SegmentParameters = Array.Empty<object>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the router in which the navigation is taking place.
|
||||
/// </summary>
|
||||
public IRouter Router { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path of the route that is being navigated to.
|
||||
/// </summary>
|
||||
public string Path { get; }
|
||||
|
||||
/// <summary>
|
||||
/// GEts an array of all parameters provided to this route.
|
||||
/// </summary>
|
||||
public object[] RouteParameters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an array of parameters provided to this screen's segment of the route.
|
||||
/// </summary>
|
||||
public object[] SegmentParameters { get; internal set; }
|
||||
|
||||
internal bool Cancelled { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cancels further processing of the current navigation.
|
||||
/// </summary>
|
||||
/// <remarks>It not necessary to cancel the navigation in order to navigate to another route, the current navigation will be cancelled by the router.</remarks>
|
||||
public void Cancel()
|
||||
{
|
||||
Cancelled = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Avalonia.Threading;
|
||||
using Serilog;
|
||||
|
||||
namespace Artemis.UI.Shared.Routing;
|
||||
|
||||
internal class Router : CorePropertyChanged, IRouter
|
||||
{
|
||||
private readonly Stack<string> _backStack = new();
|
||||
private readonly BehaviorSubject<string?> _currentRouteSubject;
|
||||
private readonly Stack<string> _forwardStack = new();
|
||||
private readonly Func<IRoutableScreen, RouteResolution, RouterNavigationOptions, Navigation> _getNavigation;
|
||||
private readonly ILogger _logger;
|
||||
private Navigation? _currentNavigation;
|
||||
|
||||
private IRoutableScreen? _root;
|
||||
|
||||
public Router(ILogger logger, Func<IRoutableScreen, RouteResolution, RouterNavigationOptions, Navigation> getNavigation)
|
||||
{
|
||||
_logger = logger;
|
||||
_getNavigation = getNavigation;
|
||||
_currentRouteSubject = new BehaviorSubject<string?>(null);
|
||||
}
|
||||
|
||||
private RouteResolution Resolve(string path)
|
||||
{
|
||||
foreach (IRouterRegistration routerRegistration in Routes)
|
||||
{
|
||||
RouteResolution result = RouteResolution.Resolve(routerRegistration, path);
|
||||
if (result.Success)
|
||||
return result;
|
||||
}
|
||||
|
||||
return RouteResolution.AsFailure(path);
|
||||
}
|
||||
|
||||
private async Task<bool> RequestClose(object screen, NavigationArguments args)
|
||||
{
|
||||
if (screen is not IRoutableScreen routableScreen)
|
||||
return true;
|
||||
|
||||
await routableScreen.InternalOnClosing(args);
|
||||
if (args.Cancelled)
|
||||
{
|
||||
_logger.Debug("Navigation to {Path} cancelled during RequestClose by {Screen}", args.Path, screen.GetType().Name);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (routableScreen.InternalScreen == null)
|
||||
return true;
|
||||
return await RequestClose(routableScreen.InternalScreen, args);
|
||||
}
|
||||
|
||||
private bool PathEquals(string path, bool allowPartialMatch)
|
||||
{
|
||||
if (allowPartialMatch)
|
||||
return _currentRouteSubject.Value != null && _currentRouteSubject.Value.StartsWith(path, StringComparison.InvariantCultureIgnoreCase);
|
||||
return string.Equals(_currentRouteSubject.Value, path, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IObservable<string?> CurrentPath => _currentRouteSubject;
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<IRouterRegistration> Routes { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Navigate(string path, RouterNavigationOptions? options = null)
|
||||
{
|
||||
options ??= new RouterNavigationOptions();
|
||||
|
||||
// Routing takes place on the UI thread with processing heavy tasks offloaded by the router itself
|
||||
await Dispatcher.UIThread.InvokeAsync(() => InternalNavigate(path, options));
|
||||
}
|
||||
|
||||
private async Task InternalNavigate(string path, RouterNavigationOptions options)
|
||||
{
|
||||
if (_root == null)
|
||||
throw new ArtemisRoutingException("Cannot navigate without a root having been set");
|
||||
if (PathEquals(path, options.IgnoreOnPartialMatch) || (_currentNavigation != null && _currentNavigation.PathEquals(path, options.IgnoreOnPartialMatch)))
|
||||
return;
|
||||
|
||||
string? previousPath = _currentRouteSubject.Value;
|
||||
RouteResolution resolution = Resolve(path);
|
||||
if (!resolution.Success)
|
||||
{
|
||||
_logger.Warning("Failed to resolve path {Path}", path);
|
||||
return;
|
||||
}
|
||||
|
||||
NavigationArguments args = new(this, resolution.Path, resolution.GetAllParameters());
|
||||
|
||||
if (!await RequestClose(_root, args))
|
||||
return;
|
||||
|
||||
Navigation navigation = _getNavigation(_root, resolution, options);
|
||||
|
||||
_currentNavigation?.Cancel();
|
||||
_currentNavigation = navigation;
|
||||
|
||||
// Execute the navigation
|
||||
await navigation.Navigate(args);
|
||||
|
||||
// If it was cancelled before completion, don't add it to history or update the current path
|
||||
if (navigation.Cancelled)
|
||||
return;
|
||||
|
||||
if (options.AddToHistory && previousPath != null)
|
||||
{
|
||||
_backStack.Push(previousPath);
|
||||
_forwardStack.Clear();
|
||||
}
|
||||
|
||||
_currentRouteSubject.OnNext(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> GoBack()
|
||||
{
|
||||
if (!_backStack.TryPop(out string? path))
|
||||
return false;
|
||||
|
||||
string? previousPath = _currentRouteSubject.Value;
|
||||
await Navigate(path, new RouterNavigationOptions {AddToHistory = false});
|
||||
if (previousPath != null)
|
||||
_forwardStack.Push(previousPath);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> GoForward()
|
||||
{
|
||||
if (!_forwardStack.TryPop(out string? path))
|
||||
return false;
|
||||
|
||||
string? previousPath = _currentRouteSubject.Value;
|
||||
await Navigate(path, new RouterNavigationOptions {AddToHistory = false});
|
||||
if (previousPath != null)
|
||||
_backStack.Push(previousPath);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ClearHistory()
|
||||
{
|
||||
_backStack.Clear();
|
||||
_forwardStack.Clear();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetRoot<TScreen>(RoutableScreen<TScreen> root) where TScreen : class
|
||||
{
|
||||
_root = root;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetRoot<TScreen, TParam>(RoutableScreen<TScreen, TParam> root) where TScreen : class where TParam : new()
|
||||
{
|
||||
_root = root;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace Artemis.UI.Shared.Routing;
|
||||
|
||||
/// <summary>
|
||||
/// Represents navigation options used to control navigation behaviour.
|
||||
/// </summary>
|
||||
public class RouterNavigationOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a boolean indicating whether or not to add the navigation to the history.
|
||||
/// </summary>
|
||||
public bool AddToHistory { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a boolean indicating whether or not to recycle already active screens.
|
||||
/// </summary>
|
||||
public bool RecycleScreens { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a boolean indicating whether route changes should be ignored if they are a partial match.
|
||||
/// </summary>
|
||||
/// <example>If set to true, a route change from <c>page/subpage1/subpage2</c> to <c>page/subpage1</c> will be ignored.</example>
|
||||
public bool IgnoreOnPartialMatch { get; set; } = false;
|
||||
}
|
||||
@@ -153,6 +153,7 @@ public class NotificationBuilder
|
||||
public class NotificationButtonBuilder
|
||||
{
|
||||
private Action? _action;
|
||||
private Func<Task>? _asyncAction;
|
||||
private ICommand? _command;
|
||||
private object? _commandParameter;
|
||||
private string _text = "Text";
|
||||
@@ -179,6 +180,18 @@ public class NotificationButtonBuilder
|
||||
_action = action;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes action that is called when the button is clicked.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to call when the button is clicked.</param>
|
||||
/// <returns>The builder that can be used to further build the button.</returns>
|
||||
public NotificationButtonBuilder WithAction(Func<Task> action)
|
||||
{
|
||||
_command = null;
|
||||
_asyncAction = action;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes command that is called when the button is clicked.
|
||||
@@ -210,6 +223,8 @@ public class NotificationButtonBuilder
|
||||
|
||||
if (_action != null)
|
||||
button.Command = ReactiveCommand.Create(() => _action());
|
||||
else if (_asyncAction != null)
|
||||
button.Command = ReactiveCommand.CreateFromTask(() => _asyncAction());
|
||||
else if (_command != null)
|
||||
{
|
||||
button.Command = _command;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Shared.Services.MainWindow;
|
||||
|
||||
@@ -12,12 +11,7 @@ public interface IMainWindowService : IArtemisSharedUIService
|
||||
/// Gets a boolean indicating whether the main window is currently open
|
||||
/// </summary>
|
||||
bool IsMainWindowOpen { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the host screen contained in the main window
|
||||
/// </summary>
|
||||
IScreen? HostScreen { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the main window provider that controls the state of the main window
|
||||
/// </summary>
|
||||
|
||||
@@ -9,10 +9,7 @@ internal class MainWindowService : IMainWindowService
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsMainWindowOpen { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IScreen? HostScreen { get; set; }
|
||||
|
||||
|
||||
protected virtual void OnMainWindowOpened()
|
||||
{
|
||||
MainWindowOpened?.Invoke(this, EventArgs.Empty);
|
||||
@@ -20,6 +17,7 @@ internal class MainWindowService : IMainWindowService
|
||||
|
||||
protected virtual void OnMainWindowClosed()
|
||||
{
|
||||
UI.ClearCache();
|
||||
MainWindowClosed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ public interface IProfileEditorService : IArtemisSharedUIService
|
||||
/// Changes the selected profile by its <see cref="Core.ProfileConfiguration" />.
|
||||
/// </summary>
|
||||
/// <param name="profileConfiguration">The profile configuration of the profile to select.</param>
|
||||
void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration);
|
||||
Task ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration);
|
||||
|
||||
/// <summary>
|
||||
/// Changes the selected profile element.
|
||||
|
||||
@@ -10,6 +10,7 @@ using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Shared.Services.MainWindow;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||
using Avalonia.Threading;
|
||||
using DynamicData;
|
||||
using Serilog;
|
||||
|
||||
@@ -155,14 +156,13 @@ internal class ProfileEditorService : IProfileEditorService
|
||||
public IObservable<ILayerProperty?> LayerProperty { get; }
|
||||
public IObservable<ProfileEditorHistory?> History { get; }
|
||||
public IObservable<bool> SuspendedEditing { get; }
|
||||
public IObservable<bool> SuspendedKeybindings { get; }
|
||||
public IObservable<TimeSpan> Time { get; }
|
||||
public IObservable<bool> Playing { get; }
|
||||
public IObservable<int> PixelsPerSecond { get; }
|
||||
public IObservable<ProfileEditorFocusMode> FocusMode { get; }
|
||||
public ReadOnlyObservableCollection<ILayerPropertyKeyframe> SelectedKeyframes { get; }
|
||||
|
||||
public void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration)
|
||||
public async Task ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration)
|
||||
{
|
||||
if (ReferenceEquals(_profileConfigurationSubject.Value, profileConfiguration))
|
||||
return;
|
||||
@@ -177,7 +177,7 @@ internal class ProfileEditorService : IProfileEditorService
|
||||
_profileConfigurationSubject.Value.Profile.LastSelectedProfileElement = _profileElementSubject.Value;
|
||||
}
|
||||
|
||||
SaveProfile();
|
||||
await SaveProfileAsync();
|
||||
|
||||
// No need to deactivate the profile, if needed it will be deactivated next update
|
||||
if (_profileConfigurationSubject.Value != null)
|
||||
@@ -192,11 +192,13 @@ internal class ProfileEditorService : IProfileEditorService
|
||||
// The new profile may need activation
|
||||
if (profileConfiguration != null)
|
||||
{
|
||||
profileConfiguration.IsBeingEdited = true;
|
||||
_moduleService.SetActivationOverride(profileConfiguration.Module);
|
||||
_profileService.ActivateProfile(profileConfiguration);
|
||||
_profileService.RenderForEditor = true;
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
profileConfiguration.IsBeingEdited = true;
|
||||
_moduleService.SetActivationOverride(profileConfiguration.Module);
|
||||
_profileService.ActivateProfile(profileConfiguration);
|
||||
_profileService.RenderForEditor = true;
|
||||
});
|
||||
if (profileConfiguration.Profile?.LastSelectedProfileElement is RenderProfileElement renderProfileElement)
|
||||
ChangeCurrentProfileElement(renderProfileElement);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Reactive.Concurrency;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Input.Platform;
|
||||
@@ -16,6 +18,13 @@ public static class UI
|
||||
{
|
||||
KeyBindingsEnabled = InputElement.GotFocusEvent.Raised.Select(e => e.Item2.Source is not TextBox).StartWith(true);
|
||||
}
|
||||
|
||||
public static EventLoopScheduler BackgroundScheduler = new EventLoopScheduler(ts => new Thread(ts));
|
||||
|
||||
internal static void ClearCache()
|
||||
{
|
||||
DeviceVisualizer.BitmapCache.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current IoC locator.
|
||||
|
||||
Reference in new issue
Block a user