Code style - Use file scoped namespaces

Code style - Ran code cleanup
This commit is contained in:
Robert committed 2022-08-21 11:36:15 +02:00
1 parent e68e16df4d
commit f6090dc296
691 files changed
+37301 -38119

No files matched your search

@@ -28,10 +28,10 @@ public class TextBoxAssist : AvaloniaObject
{
element.SetValue(SuffixTextProperty, value);
if (!string.IsNullOrWhiteSpace(value) && !((TextBox)element).Classes.Contains("suffixed"))
((TextBox)element).Classes.Add("suffixed");
if (!string.IsNullOrWhiteSpace(value) && !((TextBox) element).Classes.Contains("suffixed"))
((TextBox) element).Classes.Add("suffixed");
else
((TextBox)element).Classes.Remove("suffixed");
((TextBox) element).Classes.Remove("suffixed");
}
/// <summary>
@@ -49,10 +49,10 @@ public class TextBoxAssist : AvaloniaObject
{
element.SetValue(PrefixTextProperty, value);
if (!string.IsNullOrWhiteSpace(value) && !((TextBox)element).Classes.Contains("prefixed"))
((TextBox)element).Classes.Add("prefixed");
if (!string.IsNullOrWhiteSpace(value) && !((TextBox) element).Classes.Contains("prefixed"))
((TextBox) element).Classes.Add("prefixed");
else
((TextBox)element).Classes.Remove("prefixed");
((TextBox) element).Classes.Remove("prefixed");
}
/// <summary>
@@ -13,7 +13,7 @@ namespace Artemis.UI.Shared.Behaviors;
public class LostFocusNumberBoxBindingBehavior : Behavior<NumberBox>
{
/// <summary>
/// Gets or sets the value of the binding.
/// Gets or sets the value of the binding.
/// </summary>
public static readonly StyledProperty<double> ValueProperty = AvaloniaProperty.Register<LostFocusTextBoxBindingBehavior, double>(
nameof(Value), defaultBindingMode: BindingMode.TwoWay);
@@ -24,7 +24,7 @@ public class LostFocusNumberBoxBindingBehavior : Behavior<NumberBox>
}
/// <summary>
/// Gets or sets the value of the binding.
/// Gets or sets the value of the binding.
/// </summary>
public double Value
{
@@ -13,7 +13,7 @@ namespace Artemis.UI.Shared.Behaviors;
public class LostFocusTextBoxBindingBehavior : Behavior<TextBox>
{
/// <summary>
/// Gets or sets the value of the binding.
/// Gets or sets the value of the binding.
/// </summary>
public static readonly StyledProperty<string> TextProperty = AvaloniaProperty.Register<LostFocusTextBoxBindingBehavior, string>(
"Text", defaultBindingMode: BindingMode.TwoWay);
@@ -24,7 +24,7 @@ public class LostFocusTextBoxBindingBehavior : Behavior<TextBox>
}
/// <summary>
/// Gets or sets the value of the binding.
/// Gets or sets the value of the binding.
/// </summary>
public string Text
{
@@ -4,7 +4,6 @@ using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Interactivity;
using Avalonia.Xaml.Interactivity;
using FluentAvalonia.UI.Controls;
namespace Artemis.UI.Shared.Behaviors;
@@ -14,7 +13,7 @@ namespace Artemis.UI.Shared.Behaviors;
public class SliderPointerReleasedBindingBehavior : Behavior<Slider>
{
/// <summary>
/// Gets or sets the value of the binding.
/// Gets or sets the value of the binding.
/// </summary>
public static readonly StyledProperty<double> ValueProperty = AvaloniaProperty.Register<LostFocusTextBoxBindingBehavior, double>(
nameof(Value), defaultBindingMode: BindingMode.TwoWay);
@@ -25,7 +24,7 @@ public class SliderPointerReleasedBindingBehavior : Behavior<Slider>
}
/// <summary>
/// Gets or sets the value of the binding.
/// Gets or sets the value of the binding.
/// </summary>
public double Value
{
@@ -11,133 +11,135 @@ using Avalonia.Visuals.Media.Imaging;
using Material.Icons;
using Material.Icons.Avalonia;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared;
/// <summary>
/// Represents a control that can display an arbitrary kind of icon.
/// </summary>
public class ArtemisIcon : UserControl
{
private static readonly Regex _imageRegex = new(@"[\/.](gif|jpg|jpeg|tiff|png)$", RegexOptions.Compiled);
/// <summary>
/// Represents a control that can display an arbitrary kind of icon.
/// Creates a new instance of the <see cref="ArtemisIcon" /> class.
/// </summary>
public class ArtemisIcon : UserControl
public ArtemisIcon()
{
private static Regex _imageRegex = new(@"[\/.](gif|jpg|jpeg|tiff|png)$", RegexOptions.Compiled);
/// <summary>
/// Creates a new instance of the <see cref="ArtemisIcon" /> class.
/// </summary>
public ArtemisIcon()
{
InitializeComponent();
DetachedFromLogicalTree += OnDetachedFromLogicalTree;
LayoutUpdated += OnLayoutUpdated;
}
InitializeComponent();
DetachedFromLogicalTree += OnDetachedFromLogicalTree;
LayoutUpdated += OnLayoutUpdated;
}
private static void IconChanging(IAvaloniaObject sender, bool before)
{
if (before)
((ArtemisIcon) sender).Update();
}
private static void IconChanging(IAvaloniaObject sender, bool before)
{
if (before)
((ArtemisIcon) sender).Update();
}
private void Update()
private void Update()
{
try
{
try
// First look for an enum value instead of a string
if (Icon is MaterialIconKind materialIcon)
{
// First look for an enum value instead of a string
if (Icon is MaterialIconKind materialIcon)
Content = new MaterialIcon {Kind = materialIcon, Width = Bounds.Width, Height = Bounds.Height};
// If it's a string there are several options
else if (Icon is string iconString)
Content = new MaterialIcon {Kind = materialIcon, Width = Bounds.Width, Height = Bounds.Height};
}
// If it's a string there are several options
else if (Icon is string iconString)
{
// An enum defined as a string
if (Enum.TryParse(iconString, true, out MaterialIconKind parsedIcon))
{
// An enum defined as a string
if (Enum.TryParse(iconString, true, out MaterialIconKind parsedIcon))
Content = new MaterialIcon {Kind = parsedIcon, Width = Bounds.Width, Height = Bounds.Height};
// An URI pointing to an image
else if (_imageRegex.IsMatch(iconString))
{
if (!Fill)
Content = new MaterialIcon {Kind = parsedIcon, Width = Bounds.Width, Height = Bounds.Height};
}
// An URI pointing to an image
else if (_imageRegex.IsMatch(iconString))
{
if (!Fill)
Content = new Image
{
Content = new Image
{
Source = new Bitmap(iconString),
VerticalAlignment = VerticalAlignment.Stretch,
HorizontalAlignment = HorizontalAlignment.Stretch
};
}
else
{
Content = new Border
{
Background = TextBlock.GetForeground(this),
VerticalAlignment = VerticalAlignment.Stretch,
HorizontalAlignment = HorizontalAlignment.Stretch,
OpacityMask = new ImageBrush(new Bitmap(iconString)) {BitmapInterpolationMode = BitmapInterpolationMode.MediumQuality}
};
}
}
Source = new Bitmap(iconString),
VerticalAlignment = VerticalAlignment.Stretch,
HorizontalAlignment = HorizontalAlignment.Stretch
};
else
Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark, Width = Bounds.Width, Height = Bounds.Height};
Content = new Border
{
Background = TextBlock.GetForeground(this),
VerticalAlignment = VerticalAlignment.Stretch,
HorizontalAlignment = HorizontalAlignment.Stretch,
OpacityMask = new ImageBrush(new Bitmap(iconString)) {BitmapInterpolationMode = BitmapInterpolationMode.MediumQuality}
};
}
else
{
Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark, Width = Bounds.Width, Height = Bounds.Height};
}
}
catch
{
Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark, Width = Bounds.Width, Height = Bounds.Height};
}
}
private void OnLayoutUpdated(object? sender, EventArgs e)
catch
{
if (Content is Control contentControl)
{
contentControl.Width = Bounds.Width;
contentControl.Height = Bounds.Height;
}
Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark, Width = Bounds.Width, Height = Bounds.Height};
}
private void OnDetachedFromLogicalTree(object? sender, LogicalTreeAttachmentEventArgs e)
{
if (Content is Image image && image.Source is IDisposable disposable)
disposable.Dispose();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
#region Properties
/// <summary>
/// Gets or sets the currently displayed icon as either a <see cref="MaterialIconKind" /> or an <see cref="Uri" />
/// pointing to an SVG
/// </summary>
public static readonly StyledProperty<object?> IconProperty =
AvaloniaProperty.Register<ArtemisIcon, object?>(nameof(Icon), notifying: IconChanging);
/// <summary>
/// Gets or sets the currently displayed icon as either a <see cref="MaterialIconKind" /> or an <see cref="Uri" />
/// pointing to an SVG
/// </summary>
public object? Icon
{
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether or not the icon should be filled in with the primary text color of the
/// theme
/// </summary>
public static readonly StyledProperty<bool> FillProperty =
AvaloniaProperty.Register<ArtemisIcon, bool>(nameof(Icon), defaultValue: true, notifying: IconChanging);
/// <summary>
/// Gets or sets a boolean indicating whether or not the icon should be filled in with the primary text color of the
/// theme
/// </summary>
public bool Fill
{
get => GetValue(FillProperty);
set => SetValue(FillProperty, value);
}
#endregion
}
private void OnLayoutUpdated(object? sender, EventArgs e)
{
if (Content is Control contentControl)
{
contentControl.Width = Bounds.Width;
contentControl.Height = Bounds.Height;
}
}
private void OnDetachedFromLogicalTree(object? sender, LogicalTreeAttachmentEventArgs e)
{
if (Content is Image image && image.Source is IDisposable disposable)
disposable.Dispose();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
#region Properties
/// <summary>
/// Gets or sets the currently displayed icon as either a <see cref="MaterialIconKind" /> or an <see cref="Uri" />
/// pointing to an SVG
/// </summary>
public static readonly StyledProperty<object?> IconProperty =
AvaloniaProperty.Register<ArtemisIcon, object?>(nameof(Icon), notifying: IconChanging);
/// <summary>
/// Gets or sets the currently displayed icon as either a <see cref="MaterialIconKind" /> or an <see cref="Uri" />
/// pointing to an SVG
/// </summary>
public object? Icon
{
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether or not the icon should be filled in with the primary text color of the
/// theme
/// </summary>
public static readonly StyledProperty<bool> FillProperty =
AvaloniaProperty.Register<ArtemisIcon, bool>(nameof(Icon), true, notifying: IconChanging);
/// <summary>
/// Gets or sets a boolean indicating whether or not the icon should be filled in with the primary text color of the
/// theme
/// </summary>
public bool Fill
{
get => GetValue(FillProperty);
set => SetValue(FillProperty, value);
}
#endregion
}
@@ -44,7 +44,7 @@ public class DataModelPickerButton : TemplatedControl
AvaloniaProperty.Register<DataModelPicker, bool>(nameof(HasValue));
/// <summary>
/// Gets or sets the desired flyout placement.
/// Gets or sets the desired flyout placement.
/// </summary>
public static readonly StyledProperty<FlyoutPlacementMode> PlacementProperty =
AvaloniaProperty.Register<FlyoutBase, FlyoutPlacementMode>(nameof(Placement));
@@ -82,10 +82,10 @@ public class DataModelPickerButton : TemplatedControl
AvaloniaProperty.Register<DataModelPicker, bool>(nameof(IsEventPicker));
private bool _attached;
private bool _flyoutActive;
private Button? _button;
private TextBlock? _label;
private DataModelPickerFlyout? _flyout;
private bool _flyoutActive;
private TextBlock? _label;
static DataModelPickerButton()
{
@@ -131,7 +131,7 @@ public class DataModelPickerButton : TemplatedControl
}
/// <summary>
/// Gets or sets the desired flyout placement.
/// Gets or sets the desired flyout placement.
/// </summary>
public FlyoutPlacementMode Placement
{
+300 -301
View File
@@ -17,326 +17,325 @@ using Avalonia.Rendering;
using Avalonia.Threading;
using Avalonia.Visuals.Media.Imaging;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared;
/// <summary>
/// Visualizes an <see cref="ArtemisDevice" /> with optional per-LED colors
/// </summary>
public class DeviceVisualizer : Control
{
/// <summary>
/// Visualizes an <see cref="ArtemisDevice" /> with optional per-LED colors
/// </summary>
public class DeviceVisualizer : Control
private const double UpdateFrameRate = 25.0;
private readonly List<DeviceVisualizerLed> _deviceVisualizerLeds;
private readonly DispatcherTimer _timer;
private Rect _deviceBounds;
private RenderTargetBitmap? _deviceImage;
private List<DeviceVisualizerLed>? _dimmedLeds;
private List<DeviceVisualizerLed>? _highlightedLeds;
private ArtemisDevice? _oldDevice;
/// <inheritdoc />
public DeviceVisualizer()
{
private const double UpdateFrameRate = 25.0;
private readonly List<DeviceVisualizerLed> _deviceVisualizerLeds;
private readonly DispatcherTimer _timer;
_timer = new DispatcherTimer(DispatcherPriority.Render) {Interval = TimeSpan.FromMilliseconds(1000.0 / UpdateFrameRate)};
_deviceVisualizerLeds = new List<DeviceVisualizerLed>();
private Rect _deviceBounds;
private RenderTargetBitmap? _deviceImage;
private List<DeviceVisualizerLed>? _dimmedLeds;
private List<DeviceVisualizerLed>? _highlightedLeds;
private ArtemisDevice? _oldDevice;
PointerReleased += OnPointerReleased;
}
/// <inheritdoc />
public DeviceVisualizer()
/// <inheritdoc />
public override void Render(DrawingContext drawingContext)
{
if (Device == null)
return;
// Determine the scale required to fit the desired size of the control
double scale = Math.Min(Bounds.Width / _deviceBounds.Width, Bounds.Height / _deviceBounds.Height);
DrawingContext.PushedState? boundsPush = null;
try
{
_timer = new DispatcherTimer(DispatcherPriority.Render) {Interval = TimeSpan.FromMilliseconds(1000.0 / UpdateFrameRate)};
_deviceVisualizerLeds = new List<DeviceVisualizerLed>();
// Scale the visualization in the desired bounding box
if (Bounds.Width > 0 && Bounds.Height > 0)
boundsPush = drawingContext.PushPreTransform(Matrix.CreateScale(scale, scale));
PointerReleased += OnPointerReleased;
}
// Apply device rotation
using DrawingContext.PushedState translationPush = drawingContext.PushPreTransform(Matrix.CreateTranslation(0 - _deviceBounds.Left, 0 - _deviceBounds.Top));
using DrawingContext.PushedState rotationPush = drawingContext.PushPreTransform(Matrix.CreateRotation(Matrix.ToRadians(Device.Rotation)));
/// <inheritdoc />
public override void Render(DrawingContext drawingContext)
{
if (Device == null)
// Render device and LED images
if (_deviceImage != null)
drawingContext.DrawImage(
_deviceImage,
new Rect(_deviceImage.Size),
new Rect(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height),
RenderOptions.GetBitmapInterpolationMode(this)
);
if (!ShowColors)
return;
// Determine the scale required to fit the desired size of the control
double scale = Math.Min(Bounds.Width / _deviceBounds.Width, Bounds.Height / _deviceBounds.Height);
lock (_deviceVisualizerLeds)
{
// Apply device scale
using DrawingContext.PushedState scalePush = drawingContext.PushPreTransform(Matrix.CreateScale(Device.Scale, Device.Scale));
foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds)
deviceVisualizerLed.RenderGeometry(drawingContext, false);
}
}
finally
{
boundsPush?.Dispose();
}
}
/// <summary>
/// Occurs when a LED of the device has been clicked
/// </summary>
public event EventHandler<LedClickedEventArgs>? LedClicked;
/// <summary>
/// Occurs when the device was clicked but not on a LED.
/// </summary>
public event EventHandler<PointerReleasedEventArgs>? Clicked;
/// <summary>
/// Invokes the <see cref="LedClicked" /> event
/// </summary>
protected virtual void OnLedClicked(LedClickedEventArgs e)
{
LedClicked?.Invoke(this, e);
}
/// <summary>
/// Invokes the <see cref="Clicked" /> event
/// </summary>
protected virtual void OnClicked(PointerReleasedEventArgs e)
{
Clicked?.Invoke(this, e);
}
private void Update()
{
InvalidateVisual();
}
private Rect MeasureDevice()
{
if (Device == null)
return Rect.Empty;
Rect deviceRect = new(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height);
Geometry geometry = new RectangleGeometry(deviceRect);
geometry.Transform = new RotateTransform(Device.Rotation);
return geometry.Bounds;
}
private void TimerOnTick(object? sender, EventArgs e)
{
if (ShowColors && IsVisible && Opacity > 0)
Update();
}
private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (Device == null)
return;
Point position = e.GetPosition(this);
double x = position.X / Bounds.Width;
double y = position.Y / Bounds.Height;
Point scaledPosition = new(x * Device.Rectangle.Width, y * Device.Rectangle.Height);
DeviceVisualizerLed? deviceVisualizerLed = _deviceVisualizerLeds.FirstOrDefault(l => l.HitTest(scaledPosition));
if (deviceVisualizerLed != null)
OnLedClicked(new LedClickedEventArgs(deviceVisualizerLed.Led.Device, deviceVisualizerLed.Led, e));
else
OnClicked(e);
}
private void DevicePropertyChanged(object? sender, PropertyChangedEventArgs e)
{
Dispatcher.UIThread.Post(SetupForDevice, DispatcherPriority.Background);
}
private void DeviceUpdated(object? sender, EventArgs e)
{
Dispatcher.UIThread.Post(SetupForDevice, DispatcherPriority.Background);
}
#region Properties
/// <summary>
/// Gets or sets the <see cref="ArtemisDevice" /> to display
/// </summary>
public static readonly StyledProperty<ArtemisDevice?> DeviceProperty =
AvaloniaProperty.Register<DeviceVisualizer, ArtemisDevice?>(nameof(Device), notifying: DeviceUpdated);
private static void DeviceUpdated(IAvaloniaObject sender, bool before)
{
if (!before)
((DeviceVisualizer) sender).SetupForDevice();
}
/// <summary>
/// Gets or sets the <see cref="ArtemisDevice" /> to display
/// </summary>
public ArtemisDevice? Device
{
get => GetValue(DeviceProperty);
set => SetValue(DeviceProperty, value);
}
/// <summary>
/// Gets or sets boolean indicating whether or not to show per-LED colors
/// </summary>
public static readonly StyledProperty<bool> ShowColorsProperty =
AvaloniaProperty.Register<DeviceVisualizer, bool>(nameof(ShowColors));
/// <summary>
/// Gets or sets a boolean indicating whether or not to show per-LED colors
/// </summary>
public bool ShowColors
{
get => GetValue(ShowColorsProperty);
set => SetValue(ShowColorsProperty, value);
}
/// <summary>
/// Gets or sets a list of LEDs to highlight
/// </summary>
public static readonly StyledProperty<ObservableCollection<ArtemisLed>?> HighlightedLedsProperty =
AvaloniaProperty.Register<DeviceVisualizer, ObservableCollection<ArtemisLed>?>(nameof(HighlightedLeds));
/// <summary>
/// Gets or sets a list of LEDs to highlight
/// </summary>
public ObservableCollection<ArtemisLed>? HighlightedLeds
{
get => GetValue(HighlightedLedsProperty);
set => SetValue(HighlightedLedsProperty, value);
}
#endregion
#region Lifetime management
/// <inheritdoc />
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
_deviceImage?.Dispose();
_deviceImage = null;
if (Device != null)
{
Device.RgbDevice.PropertyChanged -= DevicePropertyChanged;
Device.DeviceUpdated -= DeviceUpdated;
}
base.OnDetachedFromVisualTree(e);
}
/// <inheritdoc />
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
_timer.Start();
_timer.Tick += TimerOnTick;
base.OnAttachedToLogicalTree(e);
}
/// <inheritdoc />
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
_timer.Stop();
_timer.Tick -= TimerOnTick;
base.OnDetachedFromLogicalTree(e);
}
private void SetupForDevice()
{
_highlightedLeds = new List<DeviceVisualizerLed>();
_dimmedLeds = new List<DeviceVisualizerLed>();
lock (_deviceVisualizerLeds)
{
_deviceVisualizerLeds.Clear();
}
if (_oldDevice != null)
{
_oldDevice.RgbDevice.PropertyChanged -= DevicePropertyChanged;
_oldDevice.DeviceUpdated -= DeviceUpdated;
}
_oldDevice = Device;
if (Device == null)
return;
_deviceBounds = MeasureDevice();
Device.RgbDevice.PropertyChanged += DevicePropertyChanged;
Device.DeviceUpdated += DeviceUpdated;
// Create all the LEDs
lock (_deviceVisualizerLeds)
{
foreach (ArtemisLed artemisLed in Device.Leds)
_deviceVisualizerLeds.Add(new DeviceVisualizerLed(artemisLed));
}
// Load the device main image on a background thread
ArtemisDevice? device = Device;
Dispatcher.UIThread.Post(() =>
{
if (device.Layout?.Image == null || !File.Exists(device.Layout.Image.LocalPath))
{
_deviceImage?.Dispose();
_deviceImage = null;
return;
}
DrawingContext.PushedState? boundsPush = null;
try
{
// Scale the visualization in the desired bounding box
if (Bounds.Width > 0 && Bounds.Height > 0)
boundsPush = drawingContext.PushPreTransform(Matrix.CreateScale(scale, scale));
// 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));
// Apply device rotation
using DrawingContext.PushedState translationPush = drawingContext.PushPreTransform(Matrix.CreateTranslation(0 - _deviceBounds.Left, 0 - _deviceBounds.Top));
using DrawingContext.PushedState rotationPush = drawingContext.PushPreTransform(Matrix.CreateRotation(Matrix.ToRadians(Device.Rotation)));
// Render device and LED images
if (_deviceImage != null)
drawingContext.DrawImage(
_deviceImage,
new Rect(_deviceImage.Size),
new Rect(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height),
RenderOptions.GetBitmapInterpolationMode(this)
);
if (!ShowColors)
return;
using IDrawingContextImpl context = renderTargetBitmap.CreateDrawingContext(new ImmediateRenderer(this));
using Bitmap bitmap = new(device.Layout.Image.LocalPath);
context.DrawBitmap(bitmap.PlatformImpl, 1, new Rect(bitmap.Size), new Rect(renderTargetBitmap.Size), BitmapInterpolationMode.HighQuality);
lock (_deviceVisualizerLeds)
{
// Apply device scale
using DrawingContext.PushedState scalePush = drawingContext.PushPreTransform(Matrix.CreateScale(Device.Scale, Device.Scale));
foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds)
deviceVisualizerLed.RenderGeometry(drawingContext, false);
}
}
finally
{
boundsPush?.Dispose();
}
}
/// <summary>
/// Occurs when a LED of the device has been clicked
/// </summary>
public event EventHandler<LedClickedEventArgs>? LedClicked;
/// <summary>
/// Occurs when the device was clicked but not on a LED.
/// </summary>
public event EventHandler<PointerReleasedEventArgs>? Clicked;
/// <summary>
/// Invokes the <see cref="LedClicked" /> event
/// </summary>
protected virtual void OnLedClicked(LedClickedEventArgs e)
{
LedClicked?.Invoke(this, e);
}
/// <summary>
/// Invokes the <see cref="Clicked" /> event
/// </summary>
protected virtual void OnClicked(PointerReleasedEventArgs e)
{
Clicked?.Invoke(this, e);
}
private void Update()
{
InvalidateVisual();
}
private Rect MeasureDevice()
{
if (Device == null)
return Rect.Empty;
Rect deviceRect = new(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height);
Geometry geometry = new RectangleGeometry(deviceRect);
geometry.Transform = new RotateTransform(Device.Rotation);
return geometry.Bounds;
}
private void TimerOnTick(object? sender, EventArgs e)
{
if (ShowColors && IsVisible && Opacity > 0)
Update();
}
private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (Device == null)
return;
Point position = e.GetPosition(this);
double x = position.X / Bounds.Width;
double y = position.Y / Bounds.Height;
Point scaledPosition = new(x * Device.Rectangle.Width, y * Device.Rectangle.Height);
DeviceVisualizerLed? deviceVisualizerLed = _deviceVisualizerLeds.FirstOrDefault(l => l.HitTest(scaledPosition));
if (deviceVisualizerLed != null)
OnLedClicked(new LedClickedEventArgs(deviceVisualizerLed.Led.Device, deviceVisualizerLed.Led, e));
else
OnClicked(e);
}
private void DevicePropertyChanged(object? sender, PropertyChangedEventArgs e)
{
Dispatcher.UIThread.Post(SetupForDevice, DispatcherPriority.Background);
}
private void DeviceUpdated(object? sender, EventArgs e)
{
Dispatcher.UIThread.Post(SetupForDevice, DispatcherPriority.Background);
}
#region Properties
/// <summary>
/// Gets or sets the <see cref="ArtemisDevice" /> to display
/// </summary>
public static readonly StyledProperty<ArtemisDevice?> DeviceProperty =
AvaloniaProperty.Register<DeviceVisualizer, ArtemisDevice?>(nameof(Device), notifying: DeviceUpdated);
private static void DeviceUpdated(IAvaloniaObject sender, bool before)
{
if (!before)
((DeviceVisualizer) sender).SetupForDevice();
}
/// <summary>
/// Gets or sets the <see cref="ArtemisDevice" /> to display
/// </summary>
public ArtemisDevice? Device
{
get => GetValue(DeviceProperty);
set => SetValue(DeviceProperty, value);
}
/// <summary>
/// Gets or sets boolean indicating whether or not to show per-LED colors
/// </summary>
public static readonly StyledProperty<bool> ShowColorsProperty =
AvaloniaProperty.Register<DeviceVisualizer, bool>(nameof(ShowColors));
/// <summary>
/// Gets or sets a boolean indicating whether or not to show per-LED colors
/// </summary>
public bool ShowColors
{
get => GetValue(ShowColorsProperty);
set => SetValue(ShowColorsProperty, value);
}
/// <summary>
/// Gets or sets a list of LEDs to highlight
/// </summary>
public static readonly StyledProperty<ObservableCollection<ArtemisLed>?> HighlightedLedsProperty =
AvaloniaProperty.Register<DeviceVisualizer, ObservableCollection<ArtemisLed>?>(nameof(HighlightedLeds));
/// <summary>
/// Gets or sets a list of LEDs to highlight
/// </summary>
public ObservableCollection<ArtemisLed>? HighlightedLeds
{
get => GetValue(HighlightedLedsProperty);
set => SetValue(HighlightedLedsProperty, value);
}
#endregion
#region Lifetime management
/// <inheritdoc />
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
_deviceImage?.Dispose();
_deviceImage = null;
if (Device != null)
{
Device.RgbDevice.PropertyChanged -= DevicePropertyChanged;
Device.DeviceUpdated -= DeviceUpdated;
}
base.OnDetachedFromVisualTree(e);
}
/// <inheritdoc />
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
_timer.Start();
_timer.Tick += TimerOnTick;
base.OnAttachedToLogicalTree(e);
}
/// <inheritdoc />
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
_timer.Stop();
_timer.Tick -= TimerOnTick;
base.OnDetachedFromLogicalTree(e);
}
private void SetupForDevice()
{
_highlightedLeds = new List<DeviceVisualizerLed>();
_dimmedLeds = new List<DeviceVisualizerLed>();
lock (_deviceVisualizerLeds)
{
_deviceVisualizerLeds.Clear();
}
if (_oldDevice != null)
{
_oldDevice.RgbDevice.PropertyChanged -= DevicePropertyChanged;
_oldDevice.DeviceUpdated -= DeviceUpdated;
}
_oldDevice = Device;
if (Device == null)
return;
_deviceBounds = MeasureDevice();
Device.RgbDevice.PropertyChanged += DevicePropertyChanged;
Device.DeviceUpdated += DeviceUpdated;
// Create all the LEDs
lock (_deviceVisualizerLeds)
{
foreach (ArtemisLed artemisLed in Device.Leds)
_deviceVisualizerLeds.Add(new DeviceVisualizerLed(artemisLed));
}
// Load the device main image on a background thread
ArtemisDevice? device = Device;
Dispatcher.UIThread.Post(() =>
{
if (device.Layout?.Image == null || !File.Exists(device.Layout.Image.LocalPath))
{
_deviceImage?.Dispose();
_deviceImage = null;
return;
deviceVisualizerLed.DrawBitmap(context, 2 * Device.Scale);
}
try
{
// 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));
_deviceImage?.Dispose();
_deviceImage = renderTargetBitmap;
using IDrawingContextImpl context = renderTargetBitmap.CreateDrawingContext(new ImmediateRenderer(this));
using Bitmap bitmap = new(device.Layout.Image.LocalPath);
context.DrawBitmap(bitmap.PlatformImpl, 1, new Rect(bitmap.Size), new Rect(renderTargetBitmap.Size), BitmapInterpolationMode.HighQuality);
lock (_deviceVisualizerLeds)
{
foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds)
deviceVisualizerLed.DrawBitmap(context, 2 * Device.Scale);
}
_deviceImage?.Dispose();
_deviceImage = renderTargetBitmap;
Dispatcher.UIThread.Post(InvalidateMeasure);
}
catch (Exception e)
{
// ignored
}
}, DispatcherPriority.Background);
}
/// <inheritdoc />
protected override Size MeasureOverride(Size availableSize)
{
if (_deviceBounds.Width <= 0 || _deviceBounds.Height <= 0)
return new Size(0, 0);
double availableWidth = double.IsInfinity(availableSize.Width) ? _deviceBounds.Width : availableSize.Width;
double availableHeight = double.IsInfinity(availableSize.Height) ? _deviceBounds.Height : availableSize.Height;
double bestRatio = Math.Min(availableWidth / _deviceBounds.Width, availableHeight / _deviceBounds.Height);
return new Size(_deviceBounds.Width * bestRatio, _deviceBounds.Height * bestRatio);
}
#endregion
Dispatcher.UIThread.Post(InvalidateMeasure);
}
catch (Exception e)
{
// ignored
}
}, DispatcherPriority.Background);
}
/// <inheritdoc />
protected override Size MeasureOverride(Size availableSize)
{
if (_deviceBounds.Width <= 0 || _deviceBounds.Height <= 0)
return new Size(0, 0);
double availableWidth = double.IsInfinity(availableSize.Width) ? _deviceBounds.Width : availableSize.Width;
double availableHeight = double.IsInfinity(availableSize.Height) ? _deviceBounds.Height : availableSize.Height;
double bestRatio = Math.Min(availableWidth / _deviceBounds.Width, availableHeight / _deviceBounds.Height);
return new Size(_deviceBounds.Width * bestRatio, _deviceBounds.Height * bestRatio);
}
#endregion
}
@@ -11,148 +11,147 @@ using Color = Avalonia.Media.Color;
using Point = Avalonia.Point;
using SolidColorBrush = Avalonia.Media.SolidColorBrush;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared;
internal class DeviceVisualizerLed
{
internal class DeviceVisualizerLed
private readonly SolidColorBrush _fillBrush;
private readonly Pen _pen;
private readonly SolidColorBrush _penBrush;
public DeviceVisualizerLed(ArtemisLed led)
{
private readonly SolidColorBrush _penBrush;
private readonly SolidColorBrush _fillBrush;
private readonly Pen _pen;
Led = led;
LedRect = new Rect(
Led.RgbLed.Location.X,
Led.RgbLed.Location.Y,
Led.RgbLed.Size.Width,
Led.RgbLed.Size.Height
);
public DeviceVisualizerLed(ArtemisLed led)
_fillBrush = new SolidColorBrush();
_penBrush = new SolidColorBrush();
_pen = new Pen(_penBrush) {LineJoin = PenLineJoin.Round};
CreateLedGeometry();
}
public ArtemisLed Led { get; }
public Rect LedRect { get; set; }
public Geometry? DisplayGeometry { get; private set; }
public void DrawBitmap(IDrawingContextImpl drawingContext, double scale)
{
if (Led.Layout?.Image == null || !File.Exists(Led.Layout.Image.LocalPath))
return;
try
{
Led = led;
LedRect = new Rect(
Led.RgbLed.Location.X,
Led.RgbLed.Location.Y,
Led.RgbLed.Size.Width,
Led.RgbLed.Size.Height
using Bitmap bitmap = new(Led.Layout.Image.LocalPath);
drawingContext.DrawBitmap(
bitmap.PlatformImpl,
1,
new Rect(bitmap.Size),
new Rect(Led.RgbLed.Location.X * scale, Led.RgbLed.Location.Y * scale, Led.RgbLed.Size.Width * scale, Led.RgbLed.Size.Height * scale),
BitmapInterpolationMode.HighQuality
);
}
catch
{
// ignored
}
}
_fillBrush = new SolidColorBrush();
_penBrush = new SolidColorBrush();
_pen = new Pen(_penBrush) {LineJoin = PenLineJoin.Round};
public void RenderGeometry(DrawingContext drawingContext, bool dimmed)
{
byte r = Led.RgbLed.Color.GetR();
byte g = Led.RgbLed.Color.GetG();
byte b = Led.RgbLed.Color.GetB();
CreateLedGeometry();
if (dimmed)
{
_fillBrush.Color = new Color(50, r, g, b);
_penBrush.Color = new Color(100, r, g, b);
}
else
{
_fillBrush.Color = new Color(100, r, g, b);
_penBrush.Color = new Color(255, r, g, b);
}
public ArtemisLed Led { get; }
public Rect LedRect { get; set; }
public Geometry? DisplayGeometry { get; private set; }
// Render the LED geometry
drawingContext.DrawGeometry(_fillBrush, _pen, DisplayGeometry);
}
public void DrawBitmap(IDrawingContextImpl drawingContext, double scale)
public bool HitTest(Point position)
{
return DisplayGeometry != null && DisplayGeometry.FillContains(position);
}
private void CreateLedGeometry()
{
// The minimum required size for geometry to be created
if (Led.RgbLed.Size.Width < 2 || Led.RgbLed.Size.Height < 2)
return;
switch (Led.RgbLed.Shape)
{
if (Led.Layout?.Image == null || !File.Exists(Led.Layout.Image.LocalPath))
return;
case Shape.Custom:
if (Led.RgbLed.Device.DeviceInfo.DeviceType == RGBDeviceType.Keyboard || Led.RgbLed.Device.DeviceInfo.DeviceType == RGBDeviceType.Keypad)
CreateCustomGeometry(2.0);
else
CreateCustomGeometry(1.0);
break;
case Shape.Rectangle:
if (Led.RgbLed.Device.DeviceInfo.DeviceType == RGBDeviceType.Keyboard || Led.RgbLed.Device.DeviceInfo.DeviceType == RGBDeviceType.Keypad)
CreateKeyCapGeometry();
else
CreateRectangleGeometry();
break;
case Shape.Circle:
CreateCircleGeometry();
break;
default:
throw new ArgumentOutOfRangeException();
}
}
try
private void CreateRectangleGeometry()
{
DisplayGeometry = new RectangleGeometry(new Rect(Led.RgbLed.Location.X + 0.5, Led.RgbLed.Location.Y + 0.5, Led.RgbLed.Size.Width - 1, Led.RgbLed.Size.Height - 1));
}
private void CreateCircleGeometry()
{
DisplayGeometry = new EllipseGeometry(new Rect(Led.RgbLed.Location.X + 0.5, Led.RgbLed.Location.Y + 0.5, Led.RgbLed.Size.Width - 1, Led.RgbLed.Size.Height - 1));
}
private void CreateKeyCapGeometry()
{
DisplayGeometry = new RectangleGeometry(new Rect(Led.RgbLed.Location.X + 1, Led.RgbLed.Location.Y + 1, Led.RgbLed.Size.Width - 2, Led.RgbLed.Size.Height - 2));
}
private void CreateCustomGeometry(double deflateAmount)
{
try
{
double width = Led.RgbLed.Size.Width - deflateAmount;
double height = Led.RgbLed.Size.Height - deflateAmount;
Geometry geometry = Geometry.Parse(Led.RgbLed.ShapeData);
geometry.Transform = new TransformGroup
{
using Bitmap bitmap = new(Led.Layout.Image.LocalPath);
drawingContext.DrawBitmap(
bitmap.PlatformImpl,
1,
new Rect(bitmap.Size),
new Rect(Led.RgbLed.Location.X * scale, Led.RgbLed.Location.Y * scale, Led.RgbLed.Size.Width * scale, Led.RgbLed.Size.Height * scale),
BitmapInterpolationMode.HighQuality
);
}
catch
{
// ignored
}
}
public void RenderGeometry(DrawingContext drawingContext, bool dimmed)
{
byte r = Led.RgbLed.Color.GetR();
byte g = Led.RgbLed.Color.GetG();
byte b = Led.RgbLed.Color.GetB();
if (dimmed)
{
_fillBrush.Color = new Color(50, r, g, b);
_penBrush.Color = new Color(100, r, g, b);
}
else
{
_fillBrush.Color = new Color(100, r, g, b);
_penBrush.Color = new Color(255, r, g, b);
}
// Render the LED geometry
drawingContext.DrawGeometry(_fillBrush, _pen, DisplayGeometry);
}
public bool HitTest(Point position)
{
return DisplayGeometry != null && DisplayGeometry.FillContains(position);
}
private void CreateLedGeometry()
{
// The minimum required size for geometry to be created
if (Led.RgbLed.Size.Width < 2 || Led.RgbLed.Size.Height < 2)
return;
switch (Led.RgbLed.Shape)
{
case Shape.Custom:
if (Led.RgbLed.Device.DeviceInfo.DeviceType == RGBDeviceType.Keyboard || Led.RgbLed.Device.DeviceInfo.DeviceType == RGBDeviceType.Keypad)
CreateCustomGeometry(2.0);
else
CreateCustomGeometry(1.0);
break;
case Shape.Rectangle:
if (Led.RgbLed.Device.DeviceInfo.DeviceType == RGBDeviceType.Keyboard || Led.RgbLed.Device.DeviceInfo.DeviceType == RGBDeviceType.Keypad)
CreateKeyCapGeometry();
else
CreateRectangleGeometry();
break;
case Shape.Circle:
CreateCircleGeometry();
break;
default:
throw new ArgumentOutOfRangeException();
}
}
private void CreateRectangleGeometry()
{
DisplayGeometry = new RectangleGeometry(new Rect(Led.RgbLed.Location.X + 0.5, Led.RgbLed.Location.Y + 0.5, Led.RgbLed.Size.Width - 1, Led.RgbLed.Size.Height - 1));
}
private void CreateCircleGeometry()
{
DisplayGeometry = new EllipseGeometry(new Rect(Led.RgbLed.Location.X + 0.5, Led.RgbLed.Location.Y + 0.5, Led.RgbLed.Size.Width - 1, Led.RgbLed.Size.Height - 1));
}
private void CreateKeyCapGeometry()
{
DisplayGeometry = new RectangleGeometry(new Rect(Led.RgbLed.Location.X + 1, Led.RgbLed.Location.Y + 1, Led.RgbLed.Size.Width - 2, Led.RgbLed.Size.Height - 2));
}
private void CreateCustomGeometry(double deflateAmount)
{
try
{
double width = Led.RgbLed.Size.Width - deflateAmount;
double height = Led.RgbLed.Size.Height - deflateAmount;
Geometry geometry = Geometry.Parse(Led.RgbLed.ShapeData);
geometry.Transform = new TransformGroup
Children = new Transforms
{
Children = new Transforms
{
new ScaleTransform(width, height),
new TranslateTransform(Led.RgbLed.Location.X + deflateAmount / 2, Led.RgbLed.Location.Y + deflateAmount / 2)
}
};
DisplayGeometry = geometry;
}
catch (Exception)
{
CreateRectangleGeometry();
}
new ScaleTransform(width, height),
new TranslateTransform(Led.RgbLed.Location.X + deflateAmount / 2, Led.RgbLed.Location.Y + deflateAmount / 2)
}
};
DisplayGeometry = geometry;
}
catch (Exception)
{
CreateRectangleGeometry();
}
}
}
@@ -21,20 +21,6 @@ public class DraggableNumberBox : UserControl
/// </summary>
public static readonly StyledProperty<double> ValueProperty = AvaloniaProperty.Register<DraggableNumberBox, double>(nameof(Value), defaultBindingMode: BindingMode.TwoWay, notifying: ValueChanged);
private static void ValueChanged(IAvaloniaObject sender, bool before)
{
if (before)
return;
DraggableNumberBox draggable = (DraggableNumberBox) sender;
if (!(Math.Abs(draggable._numberBox.Value - draggable.Value) > 0.00001))
return;
draggable._updating = true;
draggable._numberBox.Value = draggable.Value;
draggable._updating = false;
}
/// <summary>
/// Defines the <see cref="Minimum" /> property.
/// </summary>
@@ -175,6 +161,20 @@ public class DraggableNumberBox : UserControl
/// </summary>
public event TypedEventHandler<DraggableNumberBox, EventArgs>? DragFinished;
private static void ValueChanged(IAvaloniaObject sender, bool before)
{
if (before)
return;
DraggableNumberBox draggable = (DraggableNumberBox) sender;
if (!(Math.Abs(draggable._numberBox.Value - draggable.Value) > 0.00001))
return;
draggable._updating = true;
draggable._numberBox.Value = draggable.Value;
draggable._updating = false;
}
private void HandleKeyUp(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Enter || e.Key == Key.Escape)
@@ -8,112 +8,111 @@ using Avalonia.Data;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared;
/// <summary>
/// Represents a combobox that can display the values of an enum.
/// </summary>
public class EnumComboBox : UserControl
{
/// <summary>
/// Represents a combobox that can display the values of an enum.
/// Gets or sets the currently selected value
/// </summary>
public class EnumComboBox : UserControl
public static readonly StyledProperty<object?> ValueProperty =
AvaloniaProperty.Register<EnumComboBox, object?>(nameof(Value), defaultBindingMode: BindingMode.TwoWay, notifying: ValueChanged);
private readonly ObservableCollection<(Enum, string)> _currentValues = new();
private Type? _currentType;
private ComboBox? _enumComboBox;
/// <summary>
/// Creates a new instance of the <see cref="EnumComboBox" /> class.
/// </summary>
public EnumComboBox()
{
/// <summary>
/// Gets or sets the currently selected value
/// </summary>
public static readonly StyledProperty<object?> ValueProperty =
AvaloniaProperty.Register<EnumComboBox, object?>(nameof(Value), defaultBindingMode: BindingMode.TwoWay, notifying: ValueChanged);
private readonly ObservableCollection<(Enum, string)> _currentValues = new();
private ComboBox? _enumComboBox;
private Type? _currentType;
/// <summary>
/// Creates a new instance of the <see cref="EnumComboBox" /> class.
/// </summary>
public EnumComboBox()
{
InitializeComponent();
}
/// <summary>
/// Gets or sets the currently selected value
/// </summary>
public object? Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
private static void ValueChanged(IAvaloniaObject sender, bool before)
{
if (sender is EnumComboBox enumCombo && !before)
{
enumCombo.UpdateValues();
enumCombo.UpdateSelection();
}
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (_enumComboBox == null || _enumComboBox.SelectedIndex == -1)
return;
(Enum enumValue, _) = _currentValues[_enumComboBox.SelectedIndex];
if (!Equals(Value, enumValue))
Value = enumValue;
}
private void UpdateValues()
{
Type? newType = Value?.GetType();
if (_enumComboBox == null || newType == null || _currentType == newType)
return;
_currentValues.Clear();
foreach ((Enum, string) valueDesc in EnumUtilities.GetAllValuesAndDescriptions(newType))
_currentValues.Add(valueDesc);
_currentType = newType;
}
private void UpdateSelection()
{
if (_enumComboBox == null || Value is not Enum)
return;
(Enum, string) value = _currentValues.FirstOrDefault(v => v.Item1.Equals(Value));
if (!Equals(value.Item1, _enumComboBox.SelectedItem))
_enumComboBox.SelectedItem = value;
}
#region Overrides of TemplatedControl
/// <inheritdoc />
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
_enumComboBox = this.Get<ComboBox>("EnumComboBox");
_enumComboBox.Items = _currentValues;
UpdateValues();
UpdateSelection();
_enumComboBox.SelectionChanged += OnSelectionChanged;
base.OnAttachedToLogicalTree(e);
}
/// <inheritdoc />
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
if (_enumComboBox != null)
_enumComboBox.SelectionChanged -= OnSelectionChanged;
base.OnDetachedFromLogicalTree(e);
}
#endregion
InitializeComponent();
}
/// <summary>
/// Gets or sets the currently selected value
/// </summary>
public object? Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
private static void ValueChanged(IAvaloniaObject sender, bool before)
{
if (sender is EnumComboBox enumCombo && !before)
{
enumCombo.UpdateValues();
enumCombo.UpdateSelection();
}
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (_enumComboBox == null || _enumComboBox.SelectedIndex == -1)
return;
(Enum enumValue, _) = _currentValues[_enumComboBox.SelectedIndex];
if (!Equals(Value, enumValue))
Value = enumValue;
}
private void UpdateValues()
{
Type? newType = Value?.GetType();
if (_enumComboBox == null || newType == null || _currentType == newType)
return;
_currentValues.Clear();
foreach ((Enum, string) valueDesc in EnumUtilities.GetAllValuesAndDescriptions(newType))
_currentValues.Add(valueDesc);
_currentType = newType;
}
private void UpdateSelection()
{
if (_enumComboBox == null || Value is not Enum)
return;
(Enum, string) value = _currentValues.FirstOrDefault(v => v.Item1.Equals(Value));
if (!Equals(value.Item1, _enumComboBox.SelectedItem))
_enumComboBox.SelectedItem = value;
}
#region Overrides of TemplatedControl
/// <inheritdoc />
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
_enumComboBox = this.Get<ComboBox>("EnumComboBox");
_enumComboBox.Items = _currentValues;
UpdateValues();
UpdateSelection();
_enumComboBox.SelectionChanged += OnSelectionChanged;
base.OnAttachedToLogicalTree(e);
}
/// <inheritdoc />
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
if (_enumComboBox != null)
_enumComboBox.SelectionChanged -= OnSelectionChanged;
base.OnDetachedFromLogicalTree(e);
}
#endregion
}
@@ -20,12 +20,12 @@ public sealed class DataModelPickerFlyout : PickerFlyoutBase
public DataModelPicker.DataModelPicker DataModelPicker => _picker ??= new DataModelPicker.DataModelPicker();
/// <summary>
/// Raised when the Confirmed button is tapped indicating the new Color should be applied
/// Raised when the Confirmed button is tapped indicating the new Color should be applied
/// </summary>
public event TypedEventHandler<DataModelPickerFlyout, object>? Confirmed;
/// <summary>
/// Raised when the Dismiss button is tapped, indicating the new color should not be applied
/// Raised when the Dismiss button is tapped, indicating the new color should not be applied
/// </summary>
public event TypedEventHandler<DataModelPickerFlyout, object>? Dismissed;
@@ -19,7 +19,7 @@ using Button = Avalonia.Controls.Button;
namespace Artemis.UI.Shared.Controls.GradientPicker;
/// <summary>
/// Represents a gradient picker that can be used to edit a gradient.
/// Represents a gradient picker that can be used to edit a gradient.
/// </summary>
public class GradientPicker : TemplatedControl
{
@@ -60,18 +60,18 @@ public class GradientPicker : TemplatedControl
AvaloniaProperty.RegisterDirect<GradientPicker, ICommand>(nameof(DeleteStop), g => g.DeleteStop);
private readonly ICommand _deleteStop;
private bool _shiftDown;
private ColorPicker? _colorPicker;
private Button? _flipStops;
private Border? _gradient;
private ColorGradient? _lastColorGradient;
private Button? _randomize;
private Button? _rotateStops;
private bool _shiftDown;
private Button? _spreadStops;
private Button? _toggleSeamless;
private Button? _randomize;
private ColorGradient? _lastColorGradient;
private ColorPicker? _colorPicker;
/// <summary>
/// Creates a new instance of the <see cref="GradientPicker"/> class.
/// Creates a new instance of the <see cref="GradientPicker" /> class.
/// </summary>
public GradientPicker()
{
@@ -45,11 +45,12 @@ public class GradientPickerButton : TemplatedControl
public static readonly DirectProperty<GradientPickerButton, LinearGradientBrush> LinearGradientBrushProperty =
AvaloniaProperty.RegisterDirect<GradientPickerButton, LinearGradientBrush>(nameof(LinearGradientBrush), g => g.LinearGradientBrush);
private ColorGradient? _lastColorGradient;
private Button? _button;
private GradientPickerFlyout? _flyout;
private bool _flyoutActive;
private ColorGradient? _lastColorGradient;
/// <summary>
/// Gets or sets the color gradient.
/// </summary>
@@ -178,10 +179,8 @@ public class GradientPickerButton : TemplatedControl
// Update the display gradient
GradientStops collection = new();
if (ColorGradient != null)
{
foreach (ColorGradientStop c in ColorGradient.OrderBy(s => s.Position))
collection.Add(new GradientStop(Color.FromArgb(c.Color.Alpha, c.Color.Red, c.Color.Green, c.Color.Blue), c.Position));
}
LinearGradientBrush.GradientStops = collection;
}
@@ -18,19 +18,6 @@ internal class GradientPickerColorStop : TemplatedControl
public static readonly StyledProperty<GradientPicker?> GradientPickerProperty =
AvaloniaProperty.Register<GradientPickerColorStop, GradientPicker?>(nameof(GradientPicker), notifying: Notifying);
private static void Notifying(IAvaloniaObject sender, bool before)
{
if (sender is not GradientPickerColorStop self)
return;
if (before && self.GradientPicker != null)
self.GradientPicker.PropertyChanged -= self.GradientPickerOnPropertyChanged;
else if (self.GradientPicker != null)
self.GradientPicker.PropertyChanged += self.GradientPickerOnPropertyChanged;
self.IsSelected = ReferenceEquals(self.GradientPicker?.SelectedColorStop, self.ColorStop);
}
/// <summary>
/// Gets or sets the color stop.
/// </summary>
@@ -50,9 +37,10 @@ internal class GradientPickerColorStop : TemplatedControl
public static readonly DirectProperty<GradientPickerColorStop, bool> IsSelectedProperty =
AvaloniaProperty.RegisterDirect<GradientPickerColorStop, bool>(nameof(IsSelected), g => g.IsSelected);
private bool _isSelected;
private double _dragOffset;
private bool _isSelected;
/// <summary>
/// Gets or sets the gradient picker.
/// </summary>
@@ -97,12 +85,23 @@ internal class GradientPickerColorStop : TemplatedControl
}
}
private static void Notifying(IAvaloniaObject sender, bool before)
{
if (sender is not GradientPickerColorStop self)
return;
if (before && self.GradientPicker != null)
self.GradientPicker.PropertyChanged -= self.GradientPickerOnPropertyChanged;
else if (self.GradientPicker != null)
self.GradientPicker.PropertyChanged += self.GradientPickerOnPropertyChanged;
self.IsSelected = ReferenceEquals(self.GradientPicker?.SelectedColorStop, self.ColorStop);
}
private void GradientPickerOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (GradientPicker != null && e.Property == GradientPicker.SelectedColorStopProperty)
{
IsSelected = GradientPicker.SelectedColorStop == ColorStop;
}
}
private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
+110 -111
View File
@@ -11,128 +11,127 @@ using Avalonia.Markup.Xaml;
using Humanizer;
using Material.Icons;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared;
/// <summary>
/// Represents a control that can be used to display or edit <see cref="Core.Hotkey" /> instances.
/// </summary>
public class HotkeyBox : UserControl
{
private readonly TextBox _displayTextBox;
/// <summary>
/// Represents a control that can be used to display or edit <see cref="Core.Hotkey"/> instances.
/// Creates a new instance of the <see cref="HotkeyBox" /> class
/// </summary>
public class HotkeyBox : UserControl
public HotkeyBox()
{
private readonly TextBox _displayTextBox;
InitializeComponent();
/// <summary>
/// Creates a new instance of the <see cref="HotkeyBox"/> class
/// </summary>
public HotkeyBox()
{
InitializeComponent();
_displayTextBox = this.Find<TextBox>("DisplayTextBox");
_displayTextBox.KeyDown += DisplayTextBoxOnKeyDown;
_displayTextBox.KeyUp += DisplayTextBoxOnKeyUp;
UpdateDisplayTextBox();
}
_displayTextBox = this.Find<TextBox>("DisplayTextBox");
_displayTextBox.KeyDown += DisplayTextBoxOnKeyDown;
_displayTextBox.KeyUp += DisplayTextBoxOnKeyUp;
UpdateDisplayTextBox();
}
private static void HotkeyChanging(IAvaloniaObject sender, bool before)
{
((HotkeyBox) sender).UpdateDisplayTextBox();
}
private static void HotkeyChanging(IAvaloniaObject sender, bool before)
{
((HotkeyBox) sender).UpdateDisplayTextBox();
}
private void DisplayTextBoxOnKeyDown(object? sender, KeyEventArgs e)
{
if (e.Key >= Key.LeftShift && e.Key <= Key.RightAlt)
return;
private void DisplayTextBoxOnKeyDown(object? sender, KeyEventArgs e)
{
if (e.Key >= Key.LeftShift && e.Key <= Key.RightAlt)
return;
Hotkey ??= new Hotkey();
Hotkey.Key = (KeyboardKey?) e.Key;
Hotkey.Modifiers = (KeyboardModifierKey?) e.KeyModifiers;
UpdateDisplayTextBox();
Hotkey ??= new Hotkey();
Hotkey.Key = (KeyboardKey?) e.Key;
Hotkey.Modifiers = (KeyboardModifierKey?) e.KeyModifiers;
UpdateDisplayTextBox();
e.Handled = true;
}
e.Handled = true;
}
private void DisplayTextBoxOnKeyUp(object? sender, KeyEventArgs e)
{
if (e.KeyModifiers == KeyModifiers.None)
FocusManager.Instance?.Focus(null);
e.Handled = true;
}
private void UpdateDisplayTextBox()
{
string? display = null;
if (Hotkey?.Modifiers != null)
display = string.Join("+", Enum.GetValues<KeyboardModifierKey>().Skip(1).Where(m => Hotkey.Modifiers.Value.HasFlag(m)).Select(v => v.Humanize()));
if (Hotkey?.Key != null)
display = string.IsNullOrEmpty(display) ? Hotkey.Key.ToString() : $"{display}+{Hotkey.Key}";
_displayTextBox.Text = display;
_displayTextBox.CaretIndex = _displayTextBox.Text?.Length ?? 0;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void Button_OnClick(object? sender, RoutedEventArgs e)
{
Hotkey = null;
private void DisplayTextBoxOnKeyUp(object? sender, KeyEventArgs e)
{
if (e.KeyModifiers == KeyModifiers.None)
FocusManager.Instance?.Focus(null);
UpdateDisplayTextBox();
}
#region Properties
/// <summary>
/// Gets or sets the currently displayed icon as either a <see cref="MaterialIconKind" /> or an <see cref="Uri" />
/// pointing to an SVG
/// </summary>
public static readonly StyledProperty<Hotkey?> HotkeyProperty =
AvaloniaProperty.Register<HotkeyBox, Hotkey?>(nameof(Hotkey), defaultBindingMode: BindingMode.TwoWay, notifying: HotkeyChanging);
/// <summary>
/// Gets or sets the watermark of the hotkey box when it is empty.
/// </summary>
public static readonly StyledProperty<string?> WatermarkProperty =
AvaloniaProperty.Register<HotkeyBox, string?>(nameof(Watermark));
/// <summary>
/// Gets or sets a boolean indicating whether the watermark should float above the hotkey box when it is not empty.
/// </summary>
public static readonly StyledProperty<bool> UseFloatingWatermarkProperty =
AvaloniaProperty.Register<HotkeyBox, bool>(nameof(UseFloatingWatermark));
/// <summary>
/// Gets or sets the currently displayed icon as either a <see cref="MaterialIconKind" /> or an <see cref="Uri" />
/// pointing to an SVG
/// </summary>
public Hotkey? Hotkey
{
get => GetValue(HotkeyProperty);
set => SetValue(HotkeyProperty, value);
}
/// <summary>
/// Gets or sets the watermark of the hotkey box when it is empty.
/// </summary>
public string? Watermark
{
get => GetValue(WatermarkProperty);
set => SetValue(WatermarkProperty, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the watermark should float above the hotkey box when it is not empty.
/// </summary>
public bool UseFloatingWatermark
{
get => GetValue(UseFloatingWatermarkProperty);
set => SetValue(UseFloatingWatermarkProperty, value);
}
#endregion
e.Handled = true;
}
private void UpdateDisplayTextBox()
{
string? display = null;
if (Hotkey?.Modifiers != null)
display = string.Join("+", Enum.GetValues<KeyboardModifierKey>().Skip(1).Where(m => Hotkey.Modifiers.Value.HasFlag(m)).Select(v => v.Humanize()));
if (Hotkey?.Key != null)
display = string.IsNullOrEmpty(display) ? Hotkey.Key.ToString() : $"{display}+{Hotkey.Key}";
_displayTextBox.Text = display;
_displayTextBox.CaretIndex = _displayTextBox.Text?.Length ?? 0;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void Button_OnClick(object? sender, RoutedEventArgs e)
{
Hotkey = null;
FocusManager.Instance?.Focus(null);
UpdateDisplayTextBox();
}
#region Properties
/// <summary>
/// Gets or sets the currently displayed icon as either a <see cref="MaterialIconKind" /> or an <see cref="Uri" />
/// pointing to an SVG
/// </summary>
public static readonly StyledProperty<Hotkey?> HotkeyProperty =
AvaloniaProperty.Register<HotkeyBox, Hotkey?>(nameof(Hotkey), defaultBindingMode: BindingMode.TwoWay, notifying: HotkeyChanging);
/// <summary>
/// Gets or sets the watermark of the hotkey box when it is empty.
/// </summary>
public static readonly StyledProperty<string?> WatermarkProperty =
AvaloniaProperty.Register<HotkeyBox, string?>(nameof(Watermark));
/// <summary>
/// Gets or sets a boolean indicating whether the watermark should float above the hotkey box when it is not empty.
/// </summary>
public static readonly StyledProperty<bool> UseFloatingWatermarkProperty =
AvaloniaProperty.Register<HotkeyBox, bool>(nameof(UseFloatingWatermark));
/// <summary>
/// Gets or sets the currently displayed icon as either a <see cref="MaterialIconKind" /> or an <see cref="Uri" />
/// pointing to an SVG
/// </summary>
public Hotkey? Hotkey
{
get => GetValue(HotkeyProperty);
set => SetValue(HotkeyProperty, value);
}
/// <summary>
/// Gets or sets the watermark of the hotkey box when it is empty.
/// </summary>
public string? Watermark
{
get => GetValue(WatermarkProperty);
set => SetValue(WatermarkProperty, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the watermark should float above the hotkey box when it is not empty.
/// </summary>
public bool UseFloatingWatermark
{
get => GetValue(UseFloatingWatermarkProperty);
set => SetValue(UseFloatingWatermarkProperty, value);
}
#endregion
}
@@ -3,22 +3,21 @@ using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Styling;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared;
internal class NoInputTextBox : TextBox, IStyleable
{
internal class NoInputTextBox : TextBox, IStyleable
/// <inheritdoc />
protected override void OnKeyDown(KeyEventArgs e)
{
/// <inheritdoc />
protected override void OnKeyDown(KeyEventArgs e)
{
// Don't call the base method on purpose
}
/// <inheritdoc />
protected override void OnKeyUp(KeyEventArgs e)
{
// Don't call the base method on purpose
}
Type IStyleable.StyleKey => typeof(TextBox);
// Don't call the base method on purpose
}
/// <inheritdoc />
protected override void OnKeyUp(KeyEventArgs e)
{
// Don't call the base method on purpose
}
Type IStyleable.StyleKey => typeof(TextBox);
}
@@ -13,133 +13,132 @@ using Avalonia.Visuals.Media.Imaging;
using Material.Icons;
using Material.Icons.Avalonia;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared;
/// <summary>
/// Represents a control that can display the icon of a specific <see cref="ProfileConfiguration" />.
/// </summary>
public class ProfileConfigurationIcon : UserControl, IDisposable
{
private Stream? _stream;
/// <summary>
/// Represents a control that can display the icon of a specific <see cref="ProfileConfiguration"/>.
/// Creates a new instance of the <see cref="ProfileConfigurationIcon" /> class.
/// </summary>
public class ProfileConfigurationIcon : UserControl, IDisposable
public ProfileConfigurationIcon()
{
private Stream? _stream;
InitializeComponent();
DetachedFromLogicalTree += OnDetachedFromLogicalTree;
PropertyChanged += OnPropertyChanged;
}
/// <summary>
/// Creates a new instance of the <see cref="ProfileConfigurationIcon" /> class.
/// </summary>
public ProfileConfigurationIcon()
private void Update()
{
if (ConfigurationIcon == null)
return;
Dispose();
try
{
InitializeComponent();
DetachedFromLogicalTree += OnDetachedFromLogicalTree;
PropertyChanged += OnPropertyChanged;
}
private void Update()
{
if (ConfigurationIcon == null)
return;
Dispose();
try
if (ConfigurationIcon.IconType == ProfileConfigurationIconType.MaterialIcon)
{
if (ConfigurationIcon.IconType == ProfileConfigurationIconType.MaterialIcon)
{
Content = Enum.TryParse(ConfigurationIcon.IconName, true, out MaterialIconKind parsedIcon)
? new MaterialIcon {Kind = parsedIcon!}
: new MaterialIcon {Kind = MaterialIconKind.QuestionMark};
}
Content = Enum.TryParse(ConfigurationIcon.IconName, true, out MaterialIconKind parsedIcon)
? new MaterialIcon {Kind = parsedIcon!}
: new MaterialIcon {Kind = MaterialIconKind.QuestionMark};
}
else
{
Stream? stream = ConfigurationIcon.GetIconStream();
if (stream == null)
Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark};
else
{
Stream? stream = ConfigurationIcon.GetIconStream();
if (stream == null)
Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark};
else
LoadFromBitmap(ConfigurationIcon, stream);
}
}
catch (Exception)
{
Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark};
LoadFromBitmap(ConfigurationIcon, stream);
}
}
private void LoadFromBitmap(Core.ProfileConfigurationIcon configurationIcon, Stream stream)
catch (Exception)
{
_stream = stream;
if (!configurationIcon.Fill)
{
Content = new Image {Source = new Bitmap(stream)};
return;
}
Content = new Border
{
Background = TextBlock.GetForeground(this),
VerticalAlignment = VerticalAlignment.Stretch,
HorizontalAlignment = HorizontalAlignment.Stretch,
OpacityMask = new ImageBrush(new Bitmap(stream)) {BitmapInterpolationMode = BitmapInterpolationMode.MediumQuality}
};
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void OnDetachedFromLogicalTree(object? sender, LogicalTreeAttachmentEventArgs e)
{
if (ConfigurationIcon != null)
ConfigurationIcon.IconUpdated -= ConfigurationIconOnIconUpdated;
Dispose();
}
private void OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property != ConfigurationIconProperty)
return;
if (e.OldValue is Core.ProfileConfigurationIcon oldIcon)
oldIcon.IconUpdated -= ConfigurationIconOnIconUpdated;
if (e.NewValue is Core.ProfileConfigurationIcon newIcon)
newIcon.IconUpdated += ConfigurationIconOnIconUpdated;
Dispatcher.UIThread.Post(Update, DispatcherPriority.ApplicationIdle);
}
private void ConfigurationIconOnIconUpdated(object? sender, EventArgs e)
{
Dispatcher.UIThread.Post(Update, DispatcherPriority.ApplicationIdle);
}
#region Properties
/// <summary>
/// Gets or sets the <see cref="Core.ProfileConfigurationIcon" /> to display
/// </summary>
public static readonly StyledProperty<Core.ProfileConfigurationIcon?> ConfigurationIconProperty =
AvaloniaProperty.Register<ProfileConfigurationIcon, Core.ProfileConfigurationIcon?>(nameof(ConfigurationIcon));
/// <summary>
/// Gets or sets the <see cref="Core.ProfileConfigurationIcon" /> to display
/// </summary>
public Core.ProfileConfigurationIcon? ConfigurationIcon
{
get => GetValue(ConfigurationIconProperty);
set => SetValue(ConfigurationIconProperty, value);
}
#endregion
/// <inheritdoc />
public void Dispose()
{
if (Content is Image {Source: IDisposable d})
{
d.Dispose();
Content = null;
}
_stream?.Dispose();
Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark};
}
}
private void LoadFromBitmap(Core.ProfileConfigurationIcon configurationIcon, Stream stream)
{
_stream = stream;
if (!configurationIcon.Fill)
{
Content = new Image {Source = new Bitmap(stream)};
return;
}
Content = new Border
{
Background = TextBlock.GetForeground(this),
VerticalAlignment = VerticalAlignment.Stretch,
HorizontalAlignment = HorizontalAlignment.Stretch,
OpacityMask = new ImageBrush(new Bitmap(stream)) {BitmapInterpolationMode = BitmapInterpolationMode.MediumQuality}
};
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void OnDetachedFromLogicalTree(object? sender, LogicalTreeAttachmentEventArgs e)
{
if (ConfigurationIcon != null)
ConfigurationIcon.IconUpdated -= ConfigurationIconOnIconUpdated;
Dispose();
}
private void OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property != ConfigurationIconProperty)
return;
if (e.OldValue is Core.ProfileConfigurationIcon oldIcon)
oldIcon.IconUpdated -= ConfigurationIconOnIconUpdated;
if (e.NewValue is Core.ProfileConfigurationIcon newIcon)
newIcon.IconUpdated += ConfigurationIconOnIconUpdated;
Dispatcher.UIThread.Post(Update, DispatcherPriority.ApplicationIdle);
}
private void ConfigurationIconOnIconUpdated(object? sender, EventArgs e)
{
Dispatcher.UIThread.Post(Update, DispatcherPriority.ApplicationIdle);
}
/// <inheritdoc />
public void Dispose()
{
if (Content is Image {Source: IDisposable d})
{
d.Dispose();
Content = null;
}
_stream?.Dispose();
}
#region Properties
/// <summary>
/// Gets or sets the <see cref="Core.ProfileConfigurationIcon" /> to display
/// </summary>
public static readonly StyledProperty<Core.ProfileConfigurationIcon?> ConfigurationIconProperty =
AvaloniaProperty.Register<ProfileConfigurationIcon, Core.ProfileConfigurationIcon?>(nameof(ConfigurationIcon));
/// <summary>
/// Gets or sets the <see cref="Core.ProfileConfigurationIcon" /> to display
/// </summary>
public Core.ProfileConfigurationIcon? ConfigurationIcon
{
get => GetValue(ConfigurationIconProperty);
set => SetValue(ConfigurationIconProperty, value);
}
#endregion
}
@@ -60,9 +60,9 @@ public class SelectionRectangle : Control
private Rect? _displayRect;
private bool _isSelecting;
private Point _lastPosition;
private IControl? _oldInputElement;
private Point _startPosition;
private Point _lastPosition;
/// <inheritdoc />
public SelectionRectangle()
@@ -166,7 +166,7 @@ public class SelectionRectangle : Control
{
((SelectionRectangle) sender).SubscribeToInputElement();
}
private void ParentOnPointerMoved(object? sender, PointerEventArgs e)
{
// Point moved seems to trigger when the element under the mouse changes?
@@ -179,7 +179,7 @@ public class SelectionRectangle : Control
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
// Capture the pointer and initialize dragging the first time it moves
if (!ReferenceEquals(e.Pointer.Captured, this))
{
@@ -5,34 +5,33 @@ using Avalonia.Media;
using FluentAvalonia.UI.Media;
using SkiaSharp;
namespace Artemis.UI.Shared.Converters
namespace Artemis.UI.Shared.Converters;
/// <summary>
/// Converts <see cref="T:Avalonia.Media.Color" /> into <see cref="T:SkiaSharp.SKColor" />.
/// </summary>
public class ColorToSKColorConverter : IValueConverter
{
/// <summary>
/// Converts <see cref="T:Avalonia.Media.Color" /> into <see cref="T:SkiaSharp.SKColor" />.
/// </summary>
public class ColorToSKColorConverter : IValueConverter
/// <inheritdoc />
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
/// <inheritdoc />
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is Color avaloniaColor)
return new SKColor(avaloniaColor.R, avaloniaColor.G, avaloniaColor.B, avaloniaColor.A);
if (value is Color2 fluentAvaloniaColor)
return new SKColor(fluentAvaloniaColor.R, fluentAvaloniaColor.G, fluentAvaloniaColor.B, fluentAvaloniaColor.A);
if (value is Color avaloniaColor)
return new SKColor(avaloniaColor.R, avaloniaColor.G, avaloniaColor.B, avaloniaColor.A);
if (value is Color2 fluentAvaloniaColor)
return new SKColor(fluentAvaloniaColor.R, fluentAvaloniaColor.G, fluentAvaloniaColor.B, fluentAvaloniaColor.A);
return SKColor.Empty;
}
return SKColor.Empty;
}
/// <inheritdoc />
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
Color result = new(0, 0, 0, 0);
if (value is SKColor skColor)
result = new Color(skColor.Alpha, skColor.Red, skColor.Green, skColor.Blue);
/// <inheritdoc />
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
Color result = new(0, 0, 0, 0);
if (value is SKColor skColor)
result = new Color(skColor.Alpha, skColor.Red, skColor.Green, skColor.Blue);
if (targetType == typeof(Color2))
return (Color2) result;
return result;
}
if (targetType == typeof(Color2))
return (Color2) result;
return result;
}
}
@@ -3,23 +3,22 @@ using System.Globalization;
using Avalonia.Data;
using Avalonia.Data.Converters;
namespace Artemis.UI.Shared.Converters
{
/// <summary>
/// Converts an enum into a boolean.
/// </summary>
public class EnumToBooleanConverter : IValueConverter
{
/// <inheritdoc />
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return Equals(value, parameter);
}
namespace Artemis.UI.Shared.Converters;
/// <inheritdoc />
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return value?.Equals(true) == true ? parameter : BindingOperations.DoNothing;
}
/// <summary>
/// Converts an enum into a boolean.
/// </summary>
public class EnumToBooleanConverter : IValueConverter
{
/// <inheritdoc />
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return Equals(value, parameter);
}
/// <inheritdoc />
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return value?.Equals(true) == true ? parameter : BindingOperations.DoNothing;
}
}
@@ -5,34 +5,33 @@ using Avalonia.Media;
using FluentAvalonia.UI.Media;
using SkiaSharp;
namespace Artemis.UI.Shared.Converters
namespace Artemis.UI.Shared.Converters;
/// <summary>
/// Converts <see cref="T:SkiaSharp.SKColor" /> into <see cref="T:Avalonia.Media.Color" />.
/// </summary>
public class SKColorToColorConverter : IValueConverter
{
/// <summary>
/// Converts <see cref="T:SkiaSharp.SKColor" /> into <see cref="T:Avalonia.Media.Color" />.
/// </summary>
public class SKColorToColorConverter : IValueConverter
/// <inheritdoc />
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
/// <inheritdoc />
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
Color result = new(0, 0, 0, 0);
if (value is SKColor skColor)
result = new Color(skColor.Alpha, skColor.Red, skColor.Green, skColor.Blue);
Color result = new(0, 0, 0, 0);
if (value is SKColor skColor)
result = new Color(skColor.Alpha, skColor.Red, skColor.Green, skColor.Blue);
if (targetType == typeof(Color2))
return (Color2) result;
return result;
}
if (targetType == typeof(Color2))
return (Color2) result;
return result;
}
/// <inheritdoc />
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is Color avaloniaColor)
return new SKColor(avaloniaColor.R, avaloniaColor.G, avaloniaColor.B, avaloniaColor.A);
if (value is Color2 fluentAvaloniaColor)
return new SKColor(fluentAvaloniaColor.R, fluentAvaloniaColor.G, fluentAvaloniaColor.B, fluentAvaloniaColor.A);
/// <inheritdoc />
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is Color avaloniaColor)
return new SKColor(avaloniaColor.R, avaloniaColor.G, avaloniaColor.B, avaloniaColor.A);
if (value is Color2 fluentAvaloniaColor)
return new SKColor(fluentAvaloniaColor.R, fluentAvaloniaColor.G, fluentAvaloniaColor.B, fluentAvaloniaColor.A);
return SKColor.Empty;
}
return SKColor.Empty;
}
}
@@ -5,7 +5,7 @@ using Avalonia.Data.Converters;
namespace Artemis.UI.Shared.Converters;
/// <summary>
/// Converts any object to string by calling its ToString implementation, seems Avalonia doesn't do this
/// Converts any object to string by calling its ToString implementation, seems Avalonia doesn't do this
/// </summary>
public class ToStringConverter : IValueConverter
{
@@ -3,27 +3,26 @@ using System.Globalization;
using Artemis.Core;
using Avalonia.Data.Converters;
namespace Artemis.UI.Shared.Converters
namespace Artemis.UI.Shared.Converters;
/// <summary>
/// Converts <see cref="T:System.Type" /> into <see cref="T:System.String" />.
/// </summary>
public class TypeToStringConverter : IValueConverter
{
/// <summary>
/// Converts <see cref="T:System.Type" /> into <see cref="T:System.String" />.
/// </summary>
public class TypeToStringConverter : IValueConverter
/// <inheritdoc />
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
/// <inheritdoc />
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
bool humanizeProvided = bool.TryParse(parameter?.ToString(), out bool humanize);
if (value is Type type)
return type.GetDisplayName(humanizeProvided && humanize);
bool humanizeProvided = bool.TryParse(parameter?.ToString(), out bool humanize);
if (value is Type type)
return type.GetDisplayName(humanizeProvided && humanize);
return value?.ToString();
}
return value?.ToString();
}
/// <inheritdoc />
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Data.Converters;
namespace Artemis.UI.Shared.Converters;
@@ -20,7 +19,7 @@ public class WidthNormalizedConverter : IMultiValueConverter
object? first = values.FirstOrDefault();
object? second = values.Skip(1).FirstOrDefault();
if (first is float value && second is double totalWidth)
return (totalWidth / 1.0) * value;
return totalWidth / 1.0 * value;
return 0.0;
}
@@ -1,73 +1,71 @@
using System.Diagnostics.CodeAnalysis;
using Artemis.Core.Modules;
using ReactiveUI;
namespace Artemis.UI.Shared.DataModelVisualization
namespace Artemis.UI.Shared.DataModelVisualization;
/// <summary>
/// Represents a <see cref="DataModel" /> display view model
/// </summary>
/// <typeparam name="T">The type of the data model</typeparam>
public abstract class DataModelDisplayViewModel<T> : DataModelDisplayViewModel
{
[AllowNull] private T _displayValue;
/// <summary>
/// Represents a <see cref="DataModel" /> display view model
/// Gets or sets value that the view model must display
/// </summary>
/// <typeparam name="T">The type of the data model</typeparam>
public abstract class DataModelDisplayViewModel<T> : DataModelDisplayViewModel
[AllowNull]
public T DisplayValue
{
[AllowNull] private T _displayValue = default!;
/// <summary>
/// Gets or sets value that the view model must display
/// </summary>
[AllowNull]
public T DisplayValue
{
get => _displayValue;
set
{
if (Equals(value, _displayValue)) return;
RaiseAndSetIfChanged(ref _displayValue, value);
OnDisplayValueUpdated();
}
}
internal override object InternalGuard => new();
/// <inheritdoc />
public override void UpdateValue(object? model)
{
DisplayValue = model is T value ? value : default;
}
/// <summary>
/// Occurs when the display value is updated
/// </summary>
protected virtual void OnDisplayValueUpdated()
get => _displayValue;
set
{
if (Equals(value, _displayValue)) return;
RaiseAndSetIfChanged(ref _displayValue, value);
OnDisplayValueUpdated();
}
}
/// <summary>
/// For internal use only, implement <see cref="DataModelDisplayViewModel{T}" /> instead.
/// </summary>
public abstract class DataModelDisplayViewModel : ViewModelBase
internal override object InternalGuard => new();
/// <inheritdoc />
public override void UpdateValue(object? model)
{
private DataModelPropertyAttribute? _propertyDescription;
/// <summary>
/// Gets the property description of this value
/// </summary>
public DataModelPropertyAttribute? PropertyDescription
{
get => _propertyDescription;
internal set => RaiseAndSetIfChanged(ref _propertyDescription, value);
}
/// <summary>
/// Prevents this type being implemented directly, implement <see cref="DataModelDisplayViewModel{T}" /> instead.
/// </summary>
internal abstract object InternalGuard { get; }
/// <summary>
/// Updates the display value
/// </summary>
/// <param name="model">The value to set</param>
public abstract void UpdateValue(object? model);
DisplayValue = model is T value ? value : default;
}
/// <summary>
/// Occurs when the display value is updated
/// </summary>
protected virtual void OnDisplayValueUpdated()
{
}
}
/// <summary>
/// For internal use only, implement <see cref="DataModelDisplayViewModel{T}" /> instead.
/// </summary>
public abstract class DataModelDisplayViewModel : ViewModelBase
{
private DataModelPropertyAttribute? _propertyDescription;
/// <summary>
/// Gets the property description of this value
/// </summary>
public DataModelPropertyAttribute? PropertyDescription
{
get => _propertyDescription;
internal set => RaiseAndSetIfChanged(ref _propertyDescription, value);
}
/// <summary>
/// Prevents this type being implemented directly, implement <see cref="DataModelDisplayViewModel{T}" /> instead.
/// </summary>
internal abstract object InternalGuard { get; }
/// <summary>
/// Updates the display value
/// </summary>
/// <param name="model">The value to set</param>
public abstract void UpdateValue(object? model);
}
@@ -4,111 +4,110 @@ using System.Diagnostics.CodeAnalysis;
using Artemis.Core.Modules;
using ReactiveUI;
namespace Artemis.UI.Shared.DataModelVisualization
namespace Artemis.UI.Shared.DataModelVisualization;
/// <summary>
/// Represents a <see cref="DataModel" /> input view model
/// </summary>
/// <typeparam name="T">The type of the data model</typeparam>
public abstract class DataModelInputViewModel<T> : DataModelInputViewModel
{
private bool _closed;
[AllowNull] private T _inputValue;
/// <summary>
/// Creates a new instance of the <see cref="DataModelInputViewModel{T}" /> class
/// </summary>
/// <param name="targetDescription">The description of the property this input VM is representing</param>
/// <param name="initialValue">The initial value to set the input value to</param>
protected DataModelInputViewModel(DataModelPropertyAttribute targetDescription, T initialValue)
{
TargetDescription = targetDescription;
InputValue = initialValue;
}
/// <summary>
/// Gets or sets the value shown in the input
/// </summary>
[AllowNull]
public T InputValue
{
get => _inputValue;
set => this.RaiseAndSetIfChanged(ref _inputValue, value);
}
/// <summary>
/// Gets the description of the property this input VM is representing
/// </summary>
public DataModelPropertyAttribute TargetDescription { get; }
internal override object InternalGuard { get; } = new();
/// <inheritdoc />
public sealed override void Submit()
{
if (_closed)
return;
_closed = true;
OnSubmit();
UpdateCallback(InputValue, true);
}
/// <inheritdoc />
public sealed override void Cancel()
{
if (_closed)
return;
_closed = true;
OnCancel();
UpdateCallback(InputValue, false);
}
}
/// <summary>
/// For internal use only, implement <see cref="DataModelInputViewModel{T}" /> instead.
/// </summary>
public abstract class DataModelInputViewModel : ReactiveObject
{
/// <summary>
/// Represents a <see cref="DataModel" /> input view model
/// Prevents this type being implemented directly, implement <see cref="DataModelInputViewModel{T}" /> instead.
/// </summary>
/// <typeparam name="T">The type of the data model</typeparam>
public abstract class DataModelInputViewModel<T> : DataModelInputViewModel
// ReSharper disable once UnusedMember.Global
internal abstract object InternalGuard { get; }
internal Action<object?, bool> UpdateCallback { get; set; } = null!; // Set right after construction
/// <summary>
/// Gets the types this input view model can support through type conversion. This list is defined when registering the
/// view model.
/// </summary>
internal IReadOnlyCollection<Type>? CompatibleConversionTypes { get; set; }
/// <summary>
/// Submits the input value and removes this view model.
/// <para>This is called automatically when the user presses enter or clicks outside the view</para>
/// </summary>
public abstract void Submit();
/// <summary>
/// Discards changes to the input value and removes this view model.
/// <para>This is called automatically when the user presses escape</para>
/// </summary>
public abstract void Cancel();
/// <summary>
/// Called before the current value is submitted
/// </summary>
protected virtual void OnSubmit()
{
private bool _closed;
[AllowNull] private T _inputValue = default!;
/// <summary>
/// Creates a new instance of the <see cref="DataModelInputViewModel{T}" /> class
/// </summary>
/// <param name="targetDescription">The description of the property this input VM is representing</param>
/// <param name="initialValue">The initial value to set the input value to</param>
protected DataModelInputViewModel(DataModelPropertyAttribute targetDescription, T initialValue)
{
TargetDescription = targetDescription;
InputValue = initialValue;
}
/// <summary>
/// Gets or sets the value shown in the input
/// </summary>
[AllowNull]
public T InputValue
{
get => _inputValue;
set => this.RaiseAndSetIfChanged(ref _inputValue, value);
}
/// <summary>
/// Gets the description of the property this input VM is representing
/// </summary>
public DataModelPropertyAttribute TargetDescription { get; }
internal override object InternalGuard { get; } = new();
/// <inheritdoc />
public sealed override void Submit()
{
if (_closed)
return;
_closed = true;
OnSubmit();
UpdateCallback(InputValue, true);
}
/// <inheritdoc />
public sealed override void Cancel()
{
if (_closed)
return;
_closed = true;
OnCancel();
UpdateCallback(InputValue, false);
}
}
/// <summary>
/// For internal use only, implement <see cref="DataModelInputViewModel{T}" /> instead.
/// Called before the current value is discarded
/// </summary>
public abstract class DataModelInputViewModel : ReactiveObject
protected virtual void OnCancel()
{
/// <summary>
/// Prevents this type being implemented directly, implement <see cref="DataModelInputViewModel{T}" /> instead.
/// </summary>
// ReSharper disable once UnusedMember.Global
internal abstract object InternalGuard { get; }
internal Action<object?, bool> UpdateCallback { get; set; } = null!; // Set right after construction
/// <summary>
/// Gets the types this input view model can support through type conversion. This list is defined when registering the
/// view model.
/// </summary>
internal IReadOnlyCollection<Type>? CompatibleConversionTypes { get; set; }
/// <summary>
/// Submits the input value and removes this view model.
/// <para>This is called automatically when the user presses enter or clicks outside the view</para>
/// </summary>
public abstract void Submit();
/// <summary>
/// Discards changes to the input value and removes this view model.
/// <para>This is called automatically when the user presses escape</para>
/// </summary>
public abstract void Cancel();
/// <summary>
/// Called before the current value is submitted
/// </summary>
protected virtual void OnSubmit()
{
}
/// <summary>
/// Called before the current value is discarded
/// </summary>
protected virtual void OnCancel()
{
}
}
}
@@ -3,86 +3,85 @@ using System.Collections.Generic;
using Artemis.Core;
using Artemis.UI.Shared.Services;
namespace Artemis.UI.Shared.DataModelVisualization
namespace Artemis.UI.Shared.DataModelVisualization;
/// <summary>
/// Represents a layer brush registered through
/// <see cref="IDataModelUIService.RegisterDataModelInput{T}" /> or
/// <see cref="IDataModelUIService.RegisterDataModelDisplay{T}" />
/// </summary>
public class DataModelVisualizationRegistration
{
private readonly IDataModelUIService _dataModelUIService;
internal DataModelVisualizationRegistration(IDataModelUIService dataModelUIService,
RegistrationType registrationType,
Plugin plugin,
Type supportedType,
Type viewModelType)
{
_dataModelUIService = dataModelUIService;
RegistrationType = registrationType;
Plugin = plugin;
SupportedType = supportedType;
ViewModelType = viewModelType;
if (Plugin != Constants.CorePlugin)
Plugin.Disabled += InstanceOnDisabled;
}
/// <summary>
/// Gets the type of registration, either a display or an input
/// </summary>
public RegistrationType RegistrationType { get; }
/// <summary>
/// Gets the plugin that registered the visualization
/// </summary>
public Plugin Plugin { get; }
/// <summary>
/// Gets the type supported by the visualization
/// </summary>
public Type SupportedType { get; }
/// <summary>
/// Gets the view model type of the visualization
/// </summary>
public Type ViewModelType { get; }
/// <summary>
/// Gets a read only collection of types this visualization can convert to and from
/// </summary>
public IReadOnlyCollection<Type>? CompatibleConversionTypes { get; internal set; }
internal void Unsubscribe()
{
if (Plugin != Constants.CorePlugin)
Plugin.Disabled -= InstanceOnDisabled;
}
private void InstanceOnDisabled(object? sender, EventArgs e)
{
if (RegistrationType == RegistrationType.Input)
_dataModelUIService.RemoveDataModelInput(this);
else if (RegistrationType == RegistrationType.Display)
_dataModelUIService.RemoveDataModelDisplay(this);
}
}
/// <summary>
/// Represents a type of data model visualization registration
/// </summary>
public enum RegistrationType
{
/// <summary>
/// Represents a layer brush registered through
/// <see cref="IDataModelUIService.RegisterDataModelInput{T}" /> or
/// <see cref="IDataModelUIService.RegisterDataModelDisplay{T}" />
/// A visualization used for displaying values
/// </summary>
public class DataModelVisualizationRegistration
{
private readonly IDataModelUIService _dataModelUIService;
internal DataModelVisualizationRegistration(IDataModelUIService dataModelUIService,
RegistrationType registrationType,
Plugin plugin,
Type supportedType,
Type viewModelType)
{
_dataModelUIService = dataModelUIService;
RegistrationType = registrationType;
Plugin = plugin;
SupportedType = supportedType;
ViewModelType = viewModelType;
if (Plugin != Constants.CorePlugin)
Plugin.Disabled += InstanceOnDisabled;
}
/// <summary>
/// Gets the type of registration, either a display or an input
/// </summary>
public RegistrationType RegistrationType { get; }
/// <summary>
/// Gets the plugin that registered the visualization
/// </summary>
public Plugin Plugin { get; }
/// <summary>
/// Gets the type supported by the visualization
/// </summary>
public Type SupportedType { get; }
/// <summary>
/// Gets the view model type of the visualization
/// </summary>
public Type ViewModelType { get; }
/// <summary>
/// Gets a read only collection of types this visualization can convert to and from
/// </summary>
public IReadOnlyCollection<Type>? CompatibleConversionTypes { get; internal set; }
internal void Unsubscribe()
{
if (Plugin != Constants.CorePlugin)
Plugin.Disabled -= InstanceOnDisabled;
}
private void InstanceOnDisabled(object? sender, EventArgs e)
{
if (RegistrationType == RegistrationType.Input)
_dataModelUIService.RemoveDataModelInput(this);
else if (RegistrationType == RegistrationType.Display)
_dataModelUIService.RemoveDataModelDisplay(this);
}
}
Display,
/// <summary>
/// Represents a type of data model visualization registration
/// A visualization used for inputting values
/// </summary>
public enum RegistrationType
{
/// <summary>
/// A visualization used for displaying values
/// </summary>
Display,
/// <summary>
/// A visualization used for inputting values
/// </summary>
Input
}
Input
}
@@ -5,66 +5,65 @@ using Artemis.Core.Modules;
using Artemis.UI.Shared.Services;
using ReactiveUI;
namespace Artemis.UI.Shared.DataModelVisualization.Shared
namespace Artemis.UI.Shared.DataModelVisualization.Shared;
/// <summary>
/// Represents a view model that visualizes an event data model property
/// </summary>
public class DataModelEventViewModel : DataModelVisualizationViewModel
{
/// <summary>
/// Represents a view model that visualizes an event data model property
/// </summary>
public class DataModelEventViewModel : DataModelVisualizationViewModel
private Type? _displayValueType;
internal DataModelEventViewModel(DataModel dataModel, DataModelVisualizationViewModel parent, DataModelPath dataModelPath) : base(dataModel, parent, dataModelPath)
{
private Type? _displayValueType;
}
internal DataModelEventViewModel(DataModel dataModel, DataModelVisualizationViewModel parent, DataModelPath dataModelPath) : base(dataModel, parent, dataModelPath)
/// <summary>
/// Gets the type of event arguments this event triggers and that must be displayed as children
/// </summary>
public Type? DisplayValueType
{
get => _displayValueType;
set => this.RaiseAndSetIfChanged(ref _displayValueType, value);
}
/// <inheritdoc />
public override void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? configuration)
{
DisplayValueType = DataModelPath?.GetPropertyType();
if (configuration != null)
{
if (configuration.CreateEventChildren)
PopulateProperties(dataModelUIService, configuration);
else if (Children.Any())
Children.Clear();
}
/// <summary>
/// Gets the type of event arguments this event triggers and that must be displayed as children
/// </summary>
public Type? DisplayValueType
{
get => _displayValueType;
set => this.RaiseAndSetIfChanged(ref _displayValueType, value);
}
// Only update children if the parent is expanded
if (Parent != null && !Parent.IsRootViewModel && !Parent.IsVisualizationExpanded)
return;
/// <inheritdoc />
public override void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? configuration)
{
DisplayValueType = DataModelPath?.GetPropertyType();
foreach (DataModelVisualizationViewModel dataModelVisualizationViewModel in Children)
dataModelVisualizationViewModel.Update(dataModelUIService, configuration);
}
if (configuration != null)
{
if (configuration.CreateEventChildren)
PopulateProperties(dataModelUIService, configuration);
else if (Children.Any())
Children.Clear();
}
// Only update children if the parent is expanded
if (Parent != null && !Parent.IsRootViewModel && !Parent.IsVisualizationExpanded)
return;
/// <summary>
/// Always returns <see langword="null" /> for data model events
/// </summary>
public override object? GetCurrentValue()
{
return null;
}
foreach (DataModelVisualizationViewModel dataModelVisualizationViewModel in Children)
dataModelVisualizationViewModel.Update(dataModelUIService, configuration);
}
/// <inheritdoc />
public override string? ToString()
{
return DisplayPath ?? Path;
}
/// <summary>
/// Always returns <see langword="null"/> for data model events
/// </summary>
public override object? GetCurrentValue()
{
return null;
}
/// <inheritdoc />
public override string? ToString()
{
return DisplayPath ?? Path;
}
internal override int GetChildDepth()
{
return PropertyDescription != null && !PropertyDescription.ResetsDepth ? Depth + 1 : 1;
}
internal override int GetChildDepth()
{
return PropertyDescription != null && !PropertyDescription.ResetsDepth ? Depth + 1 : 1;
}
}
@@ -2,90 +2,89 @@
using Artemis.UI.Shared.Services;
using ReactiveUI;
namespace Artemis.UI.Shared.DataModelVisualization.Shared
namespace Artemis.UI.Shared.DataModelVisualization.Shared;
/// <summary>
/// Represents a view model that visualizes a single data model property contained in a
/// <see cref="DataModelListViewModel" />
/// </summary>
public class DataModelListItemViewModel : DataModelVisualizationViewModel
{
/// <summary>
/// Represents a view model that visualizes a single data model property contained in a
/// <see cref="DataModelListViewModel" />
/// </summary>
public class DataModelListItemViewModel : DataModelVisualizationViewModel
private object? _displayValue;
private DataModelDisplayViewModel? _displayViewModel;
private int _index;
private Type? _listType;
internal DataModelListItemViewModel(Type listType, DataModelDisplayViewModel displayViewModel, string? name) : base(null, null, null)
{
private int _index;
private Type? _listType;
private object? _displayValue;
private DataModelDisplayViewModel? _displayViewModel;
ListType = listType;
DisplayViewModel = displayViewModel;
}
internal DataModelListItemViewModel(Type listType, DataModelDisplayViewModel displayViewModel, string? name) : base(null, null, null)
{
ListType = listType;
DisplayViewModel = displayViewModel;
}
internal DataModelListItemViewModel(Type listType, string? name) : base(null, null, null)
{
ListType = listType;
}
internal DataModelListItemViewModel(Type listType, string? name) : base(null, null, null)
{
ListType = listType;
}
/// <summary>
/// Gets the view model used to display the display value
/// </summary>
public DataModelDisplayViewModel? DisplayViewModel
{
get => _displayViewModel;
internal set => this.RaiseAndSetIfChanged(ref _displayViewModel, value);
}
/// <summary>
/// Gets the view model used to display the display value
/// </summary>
public DataModelDisplayViewModel? DisplayViewModel
{
get => _displayViewModel;
internal set => this.RaiseAndSetIfChanged(ref _displayViewModel, value);
}
/// <summary>
/// Gets the index of the element within the list
/// </summary>
public int Index
{
get => _index;
internal set => this.RaiseAndSetIfChanged(ref _index, value);
}
/// <summary>
/// Gets the index of the element within the list
/// </summary>
public int Index
{
get => _index;
internal set => this.RaiseAndSetIfChanged(ref _index, value);
}
/// <summary>
/// Gets the type of elements contained in the list
/// </summary>
public Type? ListType
{
get => _listType;
private set => this.RaiseAndSetIfChanged(ref _listType, value);
}
/// <summary>
/// Gets the type of elements contained in the list
/// </summary>
public Type? ListType
{
get => _listType;
private set => this.RaiseAndSetIfChanged(ref _listType, value);
}
/// <summary>
/// Gets the value of the property that is being visualized
/// </summary>
public object? DisplayValue
{
get => _displayValue;
internal set => this.RaiseAndSetIfChanged(ref _displayValue, value);
}
/// <summary>
/// Gets the value of the property that is being visualized
/// </summary>
public object? DisplayValue
{
get => _displayValue;
internal set => this.RaiseAndSetIfChanged(ref _displayValue, value);
}
/// <inheritdoc />
public override object? GetCurrentValue()
{
return DisplayValue;
}
/// <inheritdoc />
public override object? GetCurrentValue()
{
return DisplayValue;
}
/// <inheritdoc />
public override void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? configuration)
{
// Display value gets updated by parent, don't do anything if it is null
if (DisplayValue == null)
return;
/// <inheritdoc />
public override void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? configuration)
{
// Display value gets updated by parent, don't do anything if it is null
if (DisplayValue == null)
return;
if (DisplayViewModel == null)
DisplayViewModel = dataModelUIService.GetDataModelDisplayViewModel(DisplayValue.GetType(), PropertyDescription, true);
if (DisplayViewModel == null)
DisplayViewModel = dataModelUIService.GetDataModelDisplayViewModel(DisplayValue.GetType(), PropertyDescription, true);
ListType = DisplayValue.GetType();
DisplayViewModel?.UpdateValue(DisplayValue);
}
ListType = DisplayValue.GetType();
DisplayViewModel?.UpdateValue(DisplayValue);
}
/// <inheritdoc />
public override string ToString()
{
return $"[List item {Index}]";
}
/// <inheritdoc />
public override string ToString()
{
return $"[List item {Index}]";
}
}
@@ -6,131 +6,132 @@ using Artemis.Core.Modules;
using Artemis.UI.Shared.Services;
using ReactiveUI;
namespace Artemis.UI.Shared.DataModelVisualization.Shared
namespace Artemis.UI.Shared.DataModelVisualization.Shared;
/// <summary>
/// Represents a view model that visualizes a list data model property
/// </summary>
public class DataModelListViewModel : DataModelVisualizationViewModel
{
/// <summary>
/// Represents a view model that visualizes a list data model property
/// </summary>
public class DataModelListViewModel : DataModelVisualizationViewModel
private string _countDisplay;
private Type? _displayValueType;
private IEnumerable? _list;
private ObservableCollection<DataModelVisualizationViewModel> _listChildren;
private int _listCount;
internal DataModelListViewModel(DataModel dataModel, DataModelVisualizationViewModel parent, DataModelPath dataModelPath)
: base(dataModel, parent, dataModelPath)
{
private string _countDisplay;
private Type? _displayValueType;
private IEnumerable? _list;
private ObservableCollection<DataModelVisualizationViewModel> _listChildren;
private int _listCount;
_countDisplay = "0 items";
_listChildren = new ObservableCollection<DataModelVisualizationViewModel>();
}
internal DataModelListViewModel(DataModel dataModel, DataModelVisualizationViewModel parent, DataModelPath dataModelPath)
: base(dataModel, parent, dataModelPath)
/// <summary>
/// Gets the instance of the list that is being visualized
/// </summary>
public IEnumerable? List
{
get => _list;
private set => this.RaiseAndSetIfChanged(ref _list, value);
}
/// <summary>
/// Gets amount of elements in the list that is being visualized
/// </summary>
public int ListCount
{
get => _listCount;
private set => this.RaiseAndSetIfChanged(ref _listCount, value);
}
/// <summary>
/// Gets the type of elements this list contains and that must be displayed as children
/// </summary>
public Type? DisplayValueType
{
get => _displayValueType;
set => this.RaiseAndSetIfChanged(ref _displayValueType, value);
}
/// <summary>
/// Gets a human readable display count
/// </summary>
public string CountDisplay
{
get => _countDisplay;
set => this.RaiseAndSetIfChanged(ref _countDisplay, value);
}
/// <summary>
/// Gets a list of child view models that visualize the elements in the list
/// </summary>
public ObservableCollection<DataModelVisualizationViewModel> ListChildren
{
get => _listChildren;
private set => this.RaiseAndSetIfChanged(ref _listChildren, value);
}
/// <inheritdoc />
public override void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? configuration)
{
if (Parent != null && !Parent.IsVisualizationExpanded)
return;
List = GetCurrentValue() as IEnumerable;
DisplayValueType = List?.GetType();
if (List == null)
return;
int index = 0;
foreach (object item in List)
{
_countDisplay = "0 items";
_listChildren = new ObservableCollection<DataModelVisualizationViewModel>();
}
if (item == null)
continue;
/// <summary>
/// Gets the instance of the list that is being visualized
/// </summary>
public IEnumerable? List
{
get => _list;
private set => this.RaiseAndSetIfChanged(ref _list, value);
}
/// <summary>
/// Gets amount of elements in the list that is being visualized
/// </summary>
public int ListCount
{
get => _listCount;
private set => this.RaiseAndSetIfChanged(ref _listCount, value);
}
/// <summary>
/// Gets the type of elements this list contains and that must be displayed as children
/// </summary>
public Type? DisplayValueType
{
get => _displayValueType;
set => this.RaiseAndSetIfChanged(ref _displayValueType, value);
}
/// <summary>
/// Gets a human readable display count
/// </summary>
public string CountDisplay
{
get => _countDisplay;
set => this.RaiseAndSetIfChanged(ref _countDisplay, value);
}
/// <summary>
/// Gets a list of child view models that visualize the elements in the list
/// </summary>
public ObservableCollection<DataModelVisualizationViewModel> ListChildren
{
get => _listChildren;
private set => this.RaiseAndSetIfChanged(ref _listChildren, value);
}
/// <inheritdoc />
public override void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? configuration)
{
if (Parent != null && !Parent.IsVisualizationExpanded)
return;
List = GetCurrentValue() as IEnumerable;
DisplayValueType = List?.GetType();
if (List == null)
return;
int index = 0;
foreach (object item in List)
DataModelVisualizationViewModel? child;
if (ListChildren.Count <= index)
{
if (item == null)
child = CreateListChild(dataModelUIService, item.GetType(), DataModelPath?.GetPropertyDescription()?.ListItemName);
if (child == null)
continue;
DataModelVisualizationViewModel? child;
if (ListChildren.Count <= index)
{
child = CreateListChild(dataModelUIService, item.GetType(), DataModelPath?.GetPropertyDescription()?.ListItemName);
if (child == null)
continue;
ListChildren.Add(child);
}
else
child = ListChildren[index];
if (child is DataModelListItemViewModel dataModelListPropertyViewModel)
{
dataModelListPropertyViewModel.DisplayValue = item;
dataModelListPropertyViewModel.Index = index;
dataModelListPropertyViewModel.Update(dataModelUIService, configuration);
}
index++;
ListChildren.Add(child);
}
else
{
child = ListChildren[index];
}
ListCount = index;
if (child is DataModelListItemViewModel dataModelListPropertyViewModel)
{
dataModelListPropertyViewModel.DisplayValue = item;
dataModelListPropertyViewModel.Index = index;
dataModelListPropertyViewModel.Update(dataModelUIService, configuration);
}
while (ListChildren.Count > ListCount)
ListChildren.RemoveAt(ListChildren.Count - 1);
CountDisplay = $"{ListChildren.Count} {(ListChildren.Count == 1 ? "item" : "items")}";
index++;
}
/// <inheritdoc />
public override string ToString()
{
return $"[List] {DisplayPath ?? Path} - {ListCount} item(s)";
}
ListCount = index;
private DataModelVisualizationViewModel? CreateListChild(IDataModelUIService dataModelUIService, Type listType, string? name)
{
// If a display VM was found, prefer to use that in any case
DataModelDisplayViewModel? typeViewModel = dataModelUIService.GetDataModelDisplayViewModel(listType, PropertyDescription);
while (ListChildren.Count > ListCount)
ListChildren.RemoveAt(ListChildren.Count - 1);
return typeViewModel != null
? new DataModelListItemViewModel(listType, typeViewModel, name)
: new DataModelListItemViewModel(listType, name);
}
CountDisplay = $"{ListChildren.Count} {(ListChildren.Count == 1 ? "item" : "items")}";
}
/// <inheritdoc />
public override string ToString()
{
return $"[List] {DisplayPath ?? Path} - {ListCount} item(s)";
}
private DataModelVisualizationViewModel? CreateListChild(IDataModelUIService dataModelUIService, Type listType, string? name)
{
// If a display VM was found, prefer to use that in any case
DataModelDisplayViewModel? typeViewModel = dataModelUIService.GetDataModelDisplayViewModel(listType, PropertyDescription);
return typeViewModel != null
? new DataModelListItemViewModel(listType, typeViewModel, name)
: new DataModelListItemViewModel(listType, name);
}
}
@@ -4,79 +4,78 @@ using Artemis.Core.Modules;
using Artemis.UI.Shared.Services;
using ReactiveUI;
namespace Artemis.UI.Shared.DataModelVisualization.Shared
namespace Artemis.UI.Shared.DataModelVisualization.Shared;
/// <summary>
/// Represents a view model that visualizes a class (POCO) data model property containing child properties
/// </summary>
public class DataModelPropertiesViewModel : DataModelVisualizationViewModel
{
/// <summary>
/// Represents a view model that visualizes a class (POCO) data model property containing child properties
/// </summary>
public class DataModelPropertiesViewModel : DataModelVisualizationViewModel
private object? _displayValue;
private Type? _displayValueType;
internal DataModelPropertiesViewModel(DataModel? dataModel, DataModelVisualizationViewModel? parent, DataModelPath? dataModelPath)
: base(dataModel, parent, dataModelPath)
{
private object? _displayValue;
private Type? _displayValueType;
}
internal DataModelPropertiesViewModel(DataModel? dataModel, DataModelVisualizationViewModel? parent, DataModelPath? dataModelPath)
: base(dataModel, parent, dataModelPath)
{
}
/// <summary>
/// Gets the type of the property that is being visualized
/// </summary>
public Type? DisplayValueType
{
get => _displayValueType;
private set => this.RaiseAndSetIfChanged(ref _displayValueType, value);
}
/// <summary>
/// Gets the type of the property that is being visualized
/// </summary>
public Type? DisplayValueType
{
get => _displayValueType;
private set => this.RaiseAndSetIfChanged(ref _displayValueType, value);
}
/// <summary>
/// Gets the value of the property that is being visualized
/// </summary>
public object? DisplayValue
{
get => _displayValue;
private set => this.RaiseAndSetIfChanged(ref _displayValue, value);
}
/// <summary>
/// Gets the value of the property that is being visualized
/// </summary>
public object? DisplayValue
{
get => _displayValue;
private set => this.RaiseAndSetIfChanged(ref _displayValue, value);
}
/// <inheritdoc />
public override void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? configuration)
{
DisplayValueType = DataModelPath?.GetPropertyType();
/// <inheritdoc />
public override void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? configuration)
{
DisplayValueType = DataModelPath?.GetPropertyType();
// Only set a display value if ToString returns useful information and not just the type name
object? currentValue = GetCurrentValue();
if (currentValue != null && currentValue.ToString() != currentValue.GetType().ToString())
DisplayValue = currentValue.ToString();
else
DisplayValue = null;
// Only set a display value if ToString returns useful information and not just the type name
object? currentValue = GetCurrentValue();
if (currentValue != null && currentValue.ToString() != currentValue.GetType().ToString())
DisplayValue = currentValue.ToString();
else
DisplayValue = null;
// Always populate properties
PopulateProperties(dataModelUIService, configuration);
// Always populate properties
PopulateProperties(dataModelUIService, configuration);
// Only update children if the parent is expanded
if (Parent != null && !Parent.IsRootViewModel && !Parent.IsVisualizationExpanded)
return;
// Only update children if the parent is expanded
if (Parent != null && !Parent.IsRootViewModel && !Parent.IsVisualizationExpanded)
return;
foreach (DataModelVisualizationViewModel dataModelVisualizationViewModel in Children)
dataModelVisualizationViewModel.Update(dataModelUIService, configuration);
}
foreach (DataModelVisualizationViewModel dataModelVisualizationViewModel in Children)
dataModelVisualizationViewModel.Update(dataModelUIService, configuration);
}
/// <inheritdoc />
public override object? GetCurrentValue()
{
if (Parent == null || Parent.IsRootViewModel || IsRootViewModel)
return DataModel;
return base.GetCurrentValue();
}
/// <inheritdoc />
public override object? GetCurrentValue()
{
if (Parent == null || Parent.IsRootViewModel || IsRootViewModel)
return DataModel;
return base.GetCurrentValue();
}
/// <inheritdoc />
public override string? ToString()
{
return DisplayPath ?? Path;
}
/// <inheritdoc />
public override string? ToString()
{
return DisplayPath ?? Path;
}
internal override int GetChildDepth()
{
return PropertyDescription != null && !PropertyDescription.ResetsDepth ? Depth + 1 : 1;
}
internal override int GetChildDepth()
{
return PropertyDescription != null && !PropertyDescription.ResetsDepth ? Depth + 1 : 1;
}
}
@@ -4,79 +4,78 @@ using Artemis.Core.Modules;
using Artemis.UI.Shared.Services;
using ReactiveUI;
namespace Artemis.UI.Shared.DataModelVisualization.Shared
namespace Artemis.UI.Shared.DataModelVisualization.Shared;
/// <summary>
/// Represents a view model that visualizes a single data model property contained in a
/// <see cref="DataModelPropertiesViewModel" />
/// </summary>
public class DataModelPropertyViewModel : DataModelVisualizationViewModel
{
/// <summary>
/// Represents a view model that visualizes a single data model property contained in a
/// <see cref="DataModelPropertiesViewModel" />
/// </summary>
public class DataModelPropertyViewModel : DataModelVisualizationViewModel
private object? _displayValue;
private Type? _displayValueType;
private DataModelDisplayViewModel? _displayViewModel;
internal DataModelPropertyViewModel(DataModel? dataModel, DataModelVisualizationViewModel? parent, DataModelPath? dataModelPath)
: base(dataModel, parent, dataModelPath)
{
private object? _displayValue;
private Type? _displayValueType;
private DataModelDisplayViewModel? _displayViewModel;
}
internal DataModelPropertyViewModel(DataModel? dataModel, DataModelVisualizationViewModel? parent, DataModelPath? dataModelPath)
: base(dataModel, parent, dataModelPath)
/// <summary>
/// Gets the value of the property that is being visualized
/// </summary>
public object? DisplayValue
{
get => _displayValue;
internal set => this.RaiseAndSetIfChanged(ref _displayValue, value);
}
/// <summary>
/// Gets the type of the property that is being visualized
/// </summary>
public Type? DisplayValueType
{
get => _displayValueType;
protected set => this.RaiseAndSetIfChanged(ref _displayValueType, value);
}
/// <summary>
/// Gets the view model used to display the display value
/// </summary>
public DataModelDisplayViewModel? DisplayViewModel
{
get => _displayViewModel;
internal set => this.RaiseAndSetIfChanged(ref _displayViewModel, value);
}
/// <inheritdoc />
public override void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? configuration)
{
if (Parent != null && !Parent.IsVisualizationExpanded && !Parent.IsRootViewModel)
return;
if (DisplayViewModel == null)
{
}
/// <summary>
/// Gets the value of the property that is being visualized
/// </summary>
public object? DisplayValue
{
get => _displayValue;
internal set => this.RaiseAndSetIfChanged(ref _displayValue, value);
}
/// <summary>
/// Gets the type of the property that is being visualized
/// </summary>
public Type? DisplayValueType
{
get => _displayValueType;
protected set => this.RaiseAndSetIfChanged(ref _displayValueType, value);
}
/// <summary>
/// Gets the view model used to display the display value
/// </summary>
public DataModelDisplayViewModel? DisplayViewModel
{
get => _displayViewModel;
internal set => this.RaiseAndSetIfChanged(ref _displayViewModel, value);
}
/// <inheritdoc />
public override void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? configuration)
{
if (Parent != null && !Parent.IsVisualizationExpanded && !Parent.IsRootViewModel)
return;
if (DisplayViewModel == null)
Type? propertyType = DataModelPath?.GetPropertyType();
if (propertyType != null)
{
Type? propertyType = DataModelPath?.GetPropertyType();
if (propertyType != null)
{
DisplayViewModel = dataModelUIService.GetDataModelDisplayViewModel(propertyType, PropertyDescription, true);
if (DisplayViewModel != null)
DisplayViewModel.PropertyDescription = DataModelPath?.GetPropertyDescription();
}
DisplayViewModel = dataModelUIService.GetDataModelDisplayViewModel(propertyType, PropertyDescription, true);
if (DisplayViewModel != null)
DisplayViewModel.PropertyDescription = DataModelPath?.GetPropertyDescription();
}
DisplayValue = GetCurrentValue();
DisplayValueType = DisplayValue != null ? DisplayValue.GetType() : DataModelPath?.GetPropertyType();
DisplayViewModel?.UpdateValue(DisplayValue);
}
/// <inheritdoc />
public override string ToString()
{
if (DisplayValueType != null)
return $"[{DisplayValueType.Name}] {DisplayPath ?? Path} - {DisplayValue}";
return $"{DisplayPath ?? Path} - {DisplayValue}";
}
DisplayValue = GetCurrentValue();
DisplayValueType = DisplayValue != null ? DisplayValue.GetType() : DataModelPath?.GetPropertyType();
DisplayViewModel?.UpdateValue(DisplayValue);
}
/// <inheritdoc />
public override string ToString()
{
if (DisplayValueType != null)
return $"[{DisplayValueType.Name}] {DisplayPath ?? Path} - {DisplayValue}";
return $"{DisplayPath ?? Path} - {DisplayValue}";
}
}
@@ -1,22 +1,21 @@
namespace Artemis.UI.Shared.DataModelVisualization.Shared
namespace Artemis.UI.Shared.DataModelVisualization.Shared;
/// <summary>
/// Represents a configuration to use while updating a <see cref="DataModelVisualizationViewModel" />
/// </summary>
public class DataModelUpdateConfiguration
{
/// <summary>
/// Represents a configuration to use while updating a <see cref="DataModelVisualizationViewModel" />
/// Creates a new instance of the <see cref="DataModelUpdateConfiguration" /> class
/// </summary>
public class DataModelUpdateConfiguration
/// <param name="createEventChildren">A boolean indicating whether or not event children should be created</param>
public DataModelUpdateConfiguration(bool createEventChildren)
{
/// <summary>
/// Creates a new instance of the <see cref="DataModelUpdateConfiguration" /> class
/// </summary>
/// <param name="createEventChildren">A boolean indicating whether or not event children should be created</param>
public DataModelUpdateConfiguration(bool createEventChildren)
{
CreateEventChildren = createEventChildren;
}
/// <summary>
/// Gets a boolean indicating whether or not event children should be created
/// </summary>
public bool CreateEventChildren { get; }
CreateEventChildren = createEventChildren;
}
/// <summary>
/// Gets a boolean indicating whether or not event children should be created
/// </summary>
public bool CreateEventChildren { get; }
}
@@ -5,10 +5,10 @@ using System.Linq;
using System.Reactive;
using System.Reflection;
using System.Text;
using System.Windows;
using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.UI.Shared.Services;
using Avalonia;
using ReactiveUI;
namespace Artemis.UI.Shared.DataModelVisualization.Shared;
@@ -37,8 +37,8 @@ public abstract class DataModelVisualizationViewModel : ReactiveObject, IDisposa
CopyPath = ReactiveCommand.CreateFromTask(async () =>
{
if (Avalonia.Application.Current?.Clipboard != null && Path != null)
await Avalonia.Application.Current.Clipboard.SetTextAsync(Path);
if (Application.Current?.Clipboard != null && Path != null)
await Application.Current.Clipboard.SetTextAsync(Path);
});
if (parent == null)
@@ -47,7 +47,7 @@ public abstract class DataModelVisualizationViewModel : ReactiveObject, IDisposa
PropertyDescription = DataModelPath?.GetPropertyDescription() ?? DataModel?.DataModelDescription;
}
public ReactiveCommand<Unit,Unit> CopyPath { get; }
public ReactiveCommand<Unit, Unit> CopyPath { get; }
/// <summary>
/// Gets a boolean indicating whether this view model is at the root of the data model
@@ -197,11 +197,11 @@ public abstract class DataModelVisualizationViewModel : ReactiveObject, IDisposa
if (looseMatch)
IsMatchingFilteredTypes = filteredTypes.Any(t => t.IsCastableFrom(type) ||
t == typeof(Enum) && type.IsEnum ||
t == typeof(IEnumerable<>) && type.IsGenericEnumerable() ||
type.IsGenericType && t == type.GetGenericTypeDefinition());
(t == typeof(Enum) && type.IsEnum) ||
(t == typeof(IEnumerable<>) && type.IsGenericEnumerable()) ||
(type.IsGenericType && t == type.GetGenericTypeDefinition()));
else
IsMatchingFilteredTypes = filteredTypes.Any(t => t == type || t == typeof(Enum) && type.IsEnum);
IsMatchingFilteredTypes = filteredTypes.Any(t => t == type || (t == typeof(Enum) && type.IsEnum));
}
/// <summary>
@@ -362,11 +362,11 @@ public abstract class DataModelVisualizationViewModel : ReactiveObject, IDisposa
if (typeViewModel != null)
return new DataModelPropertyViewModel(DataModel, this, dataModelPath) {DisplayViewModel = typeViewModel, Depth = depth};
// For primitives, create a property view model, it may be null that is fine
if (propertyType.IsPrimitive || propertyType.IsEnum || propertyType == typeof(string) || propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
if (propertyType.IsPrimitive || propertyType.IsEnum || propertyType == typeof(string) || (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>)))
return new DataModelPropertyViewModel(DataModel, this, dataModelPath) {Depth = depth};
if (propertyType.IsGenericEnumerable())
return new DataModelListViewModel(DataModel, this, dataModelPath) {Depth = depth};
if (propertyType == typeof(DataModelEvent) || propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(DataModelEvent<>))
if (propertyType == typeof(DataModelEvent) || (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(DataModelEvent<>)))
return new DataModelEventViewModel(DataModel, this, dataModelPath) {Depth = depth};
// For other value types create a child view model
if (propertyType.IsClass || propertyType.IsStruct())
@@ -1,24 +1,23 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Shared.DefaultTypes.DataModel.Display
namespace Artemis.UI.Shared.DefaultTypes.DataModel.Display;
/// <summary>
/// Represents a default data model display view.
/// </summary>
public class DefaultDataModelDisplayView : UserControl
{
/// <summary>
/// Represents a default data model display view.
/// Creates a new instance of the <see cref="DefaultDataModelDisplayView" /> class.
/// </summary>
public class DefaultDataModelDisplayView : UserControl
public DefaultDataModelDisplayView()
{
/// <summary>
/// Creates a new instance of the <see cref="DefaultDataModelDisplayView" /> class.
/// </summary>
public DefaultDataModelDisplayView()
{
InitializeComponent();
}
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
@@ -2,43 +2,41 @@
using Artemis.Core;
using Artemis.UI.Shared.DataModelVisualization;
using Newtonsoft.Json;
using ReactiveUI;
namespace Artemis.UI.Shared.DefaultTypes.DataModel.Display
namespace Artemis.UI.Shared.DefaultTypes.DataModel.Display;
/// <summary>
/// Represents the default data model display view model that is used when no display viewmodel specific for the type
/// is registered
/// </summary>
internal class DefaultDataModelDisplayViewModel : DataModelDisplayViewModel<object>
{
/// <summary>
/// Represents the default data model display view model that is used when no display viewmodel specific for the type
/// is registered
/// </summary>
internal class DefaultDataModelDisplayViewModel : DataModelDisplayViewModel<object>
private readonly JsonSerializerSettings _serializerSettings;
private string _display;
public DefaultDataModelDisplayViewModel()
{
private readonly JsonSerializerSettings _serializerSettings;
private string _display;
_serializerSettings = new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
PreserveReferencesHandling = PreserveReferencesHandling.None
};
_display = "null";
}
public DefaultDataModelDisplayViewModel()
{
_serializerSettings = new JsonSerializerSettings()
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
PreserveReferencesHandling = PreserveReferencesHandling.None
};
_display = "null";
}
public string Display
{
get => _display;
set => RaiseAndSetIfChanged(ref _display, value);
}
public string Display
{
get => _display;
set => RaiseAndSetIfChanged(ref _display, value);
}
protected override void OnDisplayValueUpdated()
{
if (DisplayValue is Enum enumDisplayValue)
Display = EnumUtilities.HumanizeValue(enumDisplayValue);
else if (DisplayValue is not string)
Display = JsonConvert.SerializeObject(DisplayValue, _serializerSettings);
else
Display = DisplayValue?.ToString() ?? "null";
}
protected override void OnDisplayValueUpdated()
{
if (DisplayValue is Enum enumDisplayValue)
Display = EnumUtilities.HumanizeValue(enumDisplayValue);
else if (DisplayValue is not string)
Display = JsonConvert.SerializeObject(DisplayValue, _serializerSettings);
else
Display = DisplayValue?.ToString() ?? "null";
}
}
@@ -1,26 +1,24 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using SkiaSharp;
namespace Artemis.UI.Shared.DefaultTypes.DataModel.Display
namespace Artemis.UI.Shared.DefaultTypes.DataModel.Display;
/// <summary>
/// Represents a data model display view used to display <see cref="SKColor" /> values.
/// </summary>
public class SKColorDataModelDisplayView : UserControl
{
/// <summary>
/// Represents a data model display view used to display <see cref="SKColor" /> values.
/// Creates a new instance of the <see cref="SKColorDataModelDisplayView" /> class.
/// </summary>
public partial class SKColorDataModelDisplayView : UserControl
public SKColorDataModelDisplayView()
{
/// <summary>
/// Creates a new instance of the <see cref="SKColorDataModelDisplayView"/> class.
/// </summary>
public SKColorDataModelDisplayView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
InitializeComponent();
}
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
@@ -1,21 +1,20 @@
using System;
using Artemis.Core;
namespace Artemis.UI.Shared.Events
{
/// <summary>
/// Provides data about selection events raised by <see cref="DataModelPicker" />
/// </summary>
public class DataModelSelectedEventArgs : EventArgs
{
/// <summary>
/// Gets the data model path that was selected
/// </summary>
public DataModelPath? Path { get; }
namespace Artemis.UI.Shared.Events;
internal DataModelSelectedEventArgs(DataModelPath? path)
{
Path = path;
}
/// <summary>
/// Provides data about selection events raised by <see cref="DataModelPicker" />
/// </summary>
public class DataModelSelectedEventArgs : EventArgs
{
internal DataModelSelectedEventArgs(DataModelPath? path)
{
Path = path;
}
/// <summary>
/// Gets the data model path that was selected
/// </summary>
public DataModelPath? Path { get; }
}
@@ -1,14 +1,13 @@
using System;
namespace Artemis.UI.Shared.Events
{
internal class DialogClosedEventArgs<TResult> : EventArgs
{
public TResult Result { get; }
namespace Artemis.UI.Shared.Events;
public DialogClosedEventArgs(TResult result)
{
Result = result;
}
internal class DialogClosedEventArgs<TResult> : EventArgs
{
public DialogClosedEventArgs(TResult result)
{
Result = result;
}
}
public TResult Result { get; }
}
@@ -1,32 +1,31 @@
using System;
using Artemis.Core;
namespace Artemis.UI.Shared.Events
namespace Artemis.UI.Shared.Events;
/// <summary>
/// Provides data on profile related events raised by the profile editor
/// </summary>
public class ProfileConfigurationEventArgs : EventArgs
{
/// <summary>
/// Provides data on profile related events raised by the profile editor
/// </summary>
public class ProfileConfigurationEventArgs : EventArgs
internal ProfileConfigurationEventArgs(ProfileConfiguration? profileConfiguration)
{
internal ProfileConfigurationEventArgs(ProfileConfiguration? profileConfiguration)
{
ProfileConfiguration = profileConfiguration;
}
internal ProfileConfigurationEventArgs(ProfileConfiguration? profileConfiguration, ProfileConfiguration? previousProfileConfiguration)
{
ProfileConfiguration = profileConfiguration;
PreviousProfileConfiguration = previousProfileConfiguration;
}
/// <summary>
/// Gets the profile the event was raised for
/// </summary>
public ProfileConfiguration? ProfileConfiguration { get; }
/// <summary>
/// If applicable, the previous active profile before the event was raised
/// </summary>
public ProfileConfiguration? PreviousProfileConfiguration { get; }
ProfileConfiguration = profileConfiguration;
}
internal ProfileConfigurationEventArgs(ProfileConfiguration? profileConfiguration, ProfileConfiguration? previousProfileConfiguration)
{
ProfileConfiguration = profileConfiguration;
PreviousProfileConfiguration = previousProfileConfiguration;
}
/// <summary>
/// Gets the profile the event was raised for
/// </summary>
public ProfileConfiguration? ProfileConfiguration { get; }
/// <summary>
/// If applicable, the previous active profile before the event was raised
/// </summary>
public ProfileConfiguration? PreviousProfileConfiguration { get; }
}
@@ -1,32 +1,31 @@
using System;
using Artemis.Core;
namespace Artemis.UI.Shared.Events
namespace Artemis.UI.Shared.Events;
/// <summary>
/// Provides data on profile element related events raised by the profile editor
/// </summary>
public class RenderProfileElementEventArgs : EventArgs
{
/// <summary>
/// Provides data on profile element related events raised by the profile editor
/// </summary>
public class RenderProfileElementEventArgs : EventArgs
internal RenderProfileElementEventArgs(RenderProfileElement? renderProfileElement)
{
internal RenderProfileElementEventArgs(RenderProfileElement? renderProfileElement)
{
RenderProfileElement = renderProfileElement;
}
internal RenderProfileElementEventArgs(RenderProfileElement? renderProfileElement, RenderProfileElement? previousRenderProfileElement)
{
RenderProfileElement = renderProfileElement;
PreviousRenderProfileElement = previousRenderProfileElement;
}
/// <summary>
/// Gets the profile element the event was raised for
/// </summary>
public RenderProfileElement? RenderProfileElement { get; }
/// <summary>
/// If applicable, the previous active profile element before the event was raised
/// </summary>
public RenderProfileElement? PreviousRenderProfileElement { get; }
RenderProfileElement = renderProfileElement;
}
internal RenderProfileElementEventArgs(RenderProfileElement? renderProfileElement, RenderProfileElement? previousRenderProfileElement)
{
RenderProfileElement = renderProfileElement;
PreviousRenderProfileElement = previousRenderProfileElement;
}
/// <summary>
/// Gets the profile element the event was raised for
/// </summary>
public RenderProfileElement? RenderProfileElement { get; }
/// <summary>
/// If applicable, the previous active profile element before the event was raised
/// </summary>
public RenderProfileElement? PreviousRenderProfileElement { get; }
}
@@ -2,36 +2,35 @@
using Avalonia;
using Avalonia.Input;
namespace Artemis.UI.Shared.Events
namespace Artemis.UI.Shared.Events;
/// <summary>
/// Provides data on selection events raised by the <see cref="SelectionRectangle" />.
/// </summary>
public class SelectionRectangleEventArgs : EventArgs
{
/// <summary>
/// Provides data on selection events raised by the <see cref="SelectionRectangle" />.
/// Creates a new instance of the <see cref="SelectionRectangleEventArgs" /> class.
/// </summary>
public class SelectionRectangleEventArgs : EventArgs
public SelectionRectangleEventArgs(Rect rectangle, Rect absoluteRectangle, KeyModifiers keyModifiers)
{
/// <summary>
/// Creates a new instance of the <see cref="SelectionRectangleEventArgs" /> class.
/// </summary>
public SelectionRectangleEventArgs(Rect rectangle, Rect absoluteRectangle, KeyModifiers keyModifiers)
{
KeyModifiers = keyModifiers;
Rectangle = rectangle;
AbsoluteRectangle = absoluteRectangle;
}
/// <summary>
/// Gets the rectangle relative to the parent that was selected when the event occurred.
/// </summary>
public Rect Rectangle { get; }
/// <summary>
/// Gets the rectangle relative to the window that was selected when the event occurred.
/// </summary>
public Rect AbsoluteRectangle { get; }
/// <summary>
/// Gets the key modifiers that where pressed when the event occurred.
/// </summary>
public KeyModifiers KeyModifiers { get; }
KeyModifiers = keyModifiers;
Rectangle = rectangle;
AbsoluteRectangle = absoluteRectangle;
}
/// <summary>
/// Gets the rectangle relative to the parent that was selected when the event occurred.
/// </summary>
public Rect Rectangle { get; }
/// <summary>
/// Gets the rectangle relative to the window that was selected when the event occurred.
/// </summary>
public Rect AbsoluteRectangle { get; }
/// <summary>
/// Gets the key modifiers that where pressed when the event occurred.
/// </summary>
public KeyModifiers KeyModifiers { get; }
}
@@ -1,22 +1,21 @@
using System;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared;
/// <summary>
/// Represents errors that occur within the Artemis Shared UI library
/// </summary>
public class ArtemisSharedUIException : Exception
{
/// <summary>
/// Represents errors that occur within the Artemis Shared UI library
/// </summary>
public class ArtemisSharedUIException : Exception
internal ArtemisSharedUIException()
{
internal ArtemisSharedUIException()
{
}
}
internal ArtemisSharedUIException(string message) : base(message)
{
}
internal ArtemisSharedUIException(string message) : base(message)
{
}
internal ArtemisSharedUIException(string message, Exception inner) : base(message, inner)
{
}
internal ArtemisSharedUIException(string message, Exception inner) : base(message, inner)
{
}
}
@@ -1,25 +1,24 @@
using Avalonia.Controls;
using Avalonia.LogicalTree;
namespace Artemis.UI.Shared.Extensions
namespace Artemis.UI.Shared.Extensions;
/// <summary>
/// Provides extension methods for Avalonia's <see cref="Control" /> type
/// </summary>
public static class ControlExtensions
{
/// <summary>
/// Provides extension methods for Avalonia's <see cref="Control" /> type
/// Clears all data validation errors on the given control and any of it's logical siblings
/// </summary>
public static class ControlExtensions
/// <param name="target">The target control</param>
public static void ClearAllDataValidationErrors(this Control target)
{
/// <summary>
/// Clears all data validation errors on the given control and any of it's logical siblings
/// </summary>
/// <param name="target">The target control</param>
public static void ClearAllDataValidationErrors(this Control target)
DataValidationErrors.ClearErrors(target);
foreach (ILogical logicalChild in target.GetLogicalChildren())
{
DataValidationErrors.ClearErrors(target);
foreach (ILogical logicalChild in target.GetLogicalChildren())
{
if (logicalChild is Control childControl)
childControl.ClearAllDataValidationErrors();
}
if (logicalChild is Control childControl)
childControl.ClearAllDataValidationErrors();
}
}
}
@@ -10,7 +10,7 @@ namespace Artemis.UI.Shared.Extensions;
public static class LayerPropertyExtensions
{
/// <summary>
/// Returns an observable sequence of layer property values starting with the current value.
/// Returns an observable sequence of layer property values starting with the current value.
/// </summary>
/// <param name="layerProperty">The layer property to create the sequence of.</param>
/// <typeparam name="T">The value type of the layer property.</typeparam>
@@ -4,12 +4,12 @@ using SkiaSharp;
namespace Artemis.UI.Shared.Extensions;
/// <summary>
/// Provides extension methods for the <see cref="SKColor"/> type.
/// Provides extension methods for the <see cref="SKColor" /> type.
/// </summary>
public static class SKColorExtensions
{
/// <summary>
/// Converts a SkiaSharp <see cref="SKColor"/> to an Avalonia <see cref="Color"/>.
/// Converts a SkiaSharp <see cref="SKColor" /> to an Avalonia <see cref="Color" />.
/// </summary>
/// <param name="color">The color to convert.</param>
/// <returns>The resulting color.</returns>
@@ -1,37 +1,36 @@
using Artemis.Core;
using System;
using Artemis.Core;
using Material.Icons;
using SkiaSharp;
using System;
namespace Artemis.UI.Shared.Extensions
namespace Artemis.UI.Shared.Extensions;
/// <summary>
/// Provides utilities when working with types in UI elements.
/// </summary>
public static class TypeExtensions
{
/// <summary>
/// Provides utilities when working with types in UI elements.
/// Finds an appropriate Material Design icon for the given <paramref name="type" />.
/// </summary>
public static class TypeExtensions
/// <param name="type">The type to retrieve an icon for.</param>
/// <returns>An appropriate Material Design icon for the given <paramref name="type" />.</returns>
public static MaterialIconKind GetTypeIcon(this Type? type)
{
/// <summary>
/// Finds an appropriate Material Design icon for the given <paramref name="type"/>.
/// </summary>
/// <param name="type">The type to retrieve an icon for.</param>
/// <returns>An appropriate Material Design icon for the given <paramref name="type"/>.</returns>
public static MaterialIconKind GetTypeIcon(this Type? type)
{
if (type == null)
return MaterialIconKind.QuestionMarkCircle;
if (type.TypeIsNumber())
return MaterialIconKind.CalculatorVariantOutline;
if (type.IsEnum)
return MaterialIconKind.FormatListBulletedSquare;
if (type == typeof(bool))
return MaterialIconKind.CircleHalfFull;
if (type == typeof(string))
return MaterialIconKind.Text;
if (type == typeof(SKColor))
return MaterialIconKind.Palette;
if (type.IsAssignableTo(typeof(IDataModelEvent)))
return MaterialIconKind.LightningBolt;
return MaterialIconKind.Matrix;
}
if (type == null)
return MaterialIconKind.QuestionMarkCircle;
if (type.TypeIsNumber())
return MaterialIconKind.CalculatorVariantOutline;
if (type.IsEnum)
return MaterialIconKind.FormatListBulletedSquare;
if (type == typeof(bool))
return MaterialIconKind.CircleHalfFull;
if (type == typeof(string))
return MaterialIconKind.Text;
if (type == typeof(SKColor))
return MaterialIconKind.Palette;
if (type.IsAssignableTo(typeof(IDataModelEvent)))
return MaterialIconKind.LightningBolt;
return MaterialIconKind.Matrix;
}
}
}
+22 -23
View File
@@ -3,29 +3,28 @@ using Artemis.UI.Shared.Services;
using Ninject.Extensions.Conventions;
using Ninject.Modules;
namespace Artemis.UI.Shared.Ninject
{
/// <summary>
/// The main <see cref="NinjectModule" /> of the Artemis Shared UI toolkit that binds all services
/// </summary>
public class SharedUIModule : NinjectModule
{
/// <inheritdoc />
public override void Load()
{
if (Kernel == null)
throw new ArgumentNullException("Kernel shouldn't be null here.");
namespace Artemis.UI.Shared.Ninject;
// Bind all shared UI services as singletons
Kernel.Bind(x =>
{
x.FromAssemblyContaining<IArtemisSharedUIService>()
.IncludingNonPublicTypes()
.SelectAllClasses()
.InheritedFrom<IArtemisSharedUIService>()
.BindAllInterfaces()
.Configure(c => c.InSingletonScope());
});
}
/// <summary>
/// The main <see cref="NinjectModule" /> of the Artemis Shared UI toolkit that binds all services
/// </summary>
public class SharedUIModule : NinjectModule
{
/// <inheritdoc />
public override void Load()
{
if (Kernel == null)
throw new ArgumentNullException("Kernel shouldn't be null here.");
// Bind all shared UI services as singletons
Kernel.Bind(x =>
{
x.FromAssemblyContaining<IArtemisSharedUIService>()
.IncludingNonPublicTypes()
.SelectAllClasses()
.InheritedFrom<IArtemisSharedUIService>()
.BindAllInterfaces()
.Configure(c => c.InSingletonScope());
});
}
}
@@ -2,56 +2,55 @@
using System.Threading.Tasks;
using Artemis.Core.LayerBrushes;
namespace Artemis.UI.Shared.LayerBrushes
namespace Artemis.UI.Shared.LayerBrushes;
/// <summary>
/// Represents a view model for a brush configuration window
/// </summary>
public abstract class BrushConfigurationViewModel : ValidatableViewModelBase
{
/// <summary>
/// Represents a view model for a brush configuration window
/// Creates a new instance of the <see cref="BrushConfigurationViewModel" /> class
/// </summary>
public abstract class BrushConfigurationViewModel : ValidatableViewModelBase
/// <param name="layerBrush"></param>
protected BrushConfigurationViewModel(BaseLayerBrush layerBrush)
{
/// <summary>
/// Creates a new instance of the <see cref="BrushConfigurationViewModel" /> class
/// </summary>
/// <param name="layerBrush"></param>
protected BrushConfigurationViewModel(BaseLayerBrush layerBrush)
{
LayerBrush = layerBrush;
}
/// <summary>
/// Gets the layer brush this view model is associated with
/// </summary>
public BaseLayerBrush LayerBrush { get; }
/// <summary>
/// Closes the dialog
/// </summary>
public void RequestClose()
{
CloseRequested?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Called when the window wants to close, returning <see langword="false" /> will cause the window to stay open.
/// </summary>
/// <returns><see langword="true" /> if the window may close; otherwise <see langword="false" />.</returns>
public virtual bool CanClose()
{
return true;
}
/// <summary>
/// Called when the window wants to close, returning <see langword="false" /> will cause the window to stay open.
/// </summary>
/// <returns>A task <see langword="true" /> if the window may close; otherwise <see langword="false" />.</returns>
public virtual Task<bool> CanCloseAsync()
{
return Task.FromResult(true);
}
/// <summary>
/// Occurs when a close was requested
/// </summary>
public event EventHandler? CloseRequested;
LayerBrush = layerBrush;
}
/// <summary>
/// Gets the layer brush this view model is associated with
/// </summary>
public BaseLayerBrush LayerBrush { get; }
/// <summary>
/// Closes the dialog
/// </summary>
public void RequestClose()
{
CloseRequested?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Called when the window wants to close, returning <see langword="false" /> will cause the window to stay open.
/// </summary>
/// <returns><see langword="true" /> if the window may close; otherwise <see langword="false" />.</returns>
public virtual bool CanClose()
{
return true;
}
/// <summary>
/// Called when the window wants to close, returning <see langword="false" /> will cause the window to stay open.
/// </summary>
/// <returns>A task <see langword="true" /> if the window may close; otherwise <see langword="false" />.</returns>
public virtual Task<bool> CanCloseAsync()
{
return Task.FromResult(true);
}
/// <summary>
/// Occurs when a close was requested
/// </summary>
public event EventHandler? CloseRequested;
}
@@ -1,45 +1,44 @@
using System;
using Artemis.Core.LayerBrushes;
namespace Artemis.UI.Shared.LayerBrushes
namespace Artemis.UI.Shared.LayerBrushes;
/// <inheritdoc />
public class LayerBrushConfigurationDialog<T> : LayerBrushConfigurationDialog where T : BrushConfigurationViewModel
{
/// <inheritdoc />
public class LayerBrushConfigurationDialog<T> : LayerBrushConfigurationDialog where T : BrushConfigurationViewModel
public LayerBrushConfigurationDialog()
{
/// <inheritdoc />
public LayerBrushConfigurationDialog()
{
}
/// <inheritdoc />
public LayerBrushConfigurationDialog(int dialogWidth, int dialogHeight)
{
DialogWidth = dialogWidth;
DialogHeight = dialogHeight;
}
/// <inheritdoc />
public override Type Type => typeof(T);
}
/// <inheritdoc />
public LayerBrushConfigurationDialog(int dialogWidth, int dialogHeight)
{
DialogWidth = dialogWidth;
DialogHeight = dialogHeight;
}
/// <inheritdoc />
public override Type Type => typeof(T);
}
/// <summary>
/// Describes a UI tab for a layer brush
/// </summary>
public abstract class LayerBrushConfigurationDialog : ILayerBrushConfigurationDialog
{
/// <summary>
/// The default width of the dialog
/// </summary>
public int DialogWidth { get; set; } = 800;
/// <summary>
/// Describes a UI tab for a layer brush
/// The default height of the dialog
/// </summary>
public abstract class LayerBrushConfigurationDialog : ILayerBrushConfigurationDialog
{
/// <summary>
/// The default width of the dialog
/// </summary>
public int DialogWidth { get; set; } = 800;
public int DialogHeight { get; set; } = 800;
/// <summary>
/// The default height of the dialog
/// </summary>
public int DialogHeight { get; set; } = 800;
/// <summary>
/// The type of view model the dialog contains
/// </summary>
public abstract Type Type { get; }
}
/// <summary>
/// The type of view model the dialog contains
/// </summary>
public abstract Type Type { get; }
}
@@ -1,7 +1,6 @@
using System;
using System.Threading.Tasks;
using Artemis.Core.LayerEffects;
using Avalonia.Threading;
namespace Artemis.UI.Shared.LayerEffects;
@@ -1,46 +1,44 @@
using System;
using Artemis.Core.LayerEffects;
namespace Artemis.UI.Shared.LayerEffects
namespace Artemis.UI.Shared.LayerEffects;
/// <inheritdoc />
public class LayerEffectConfigurationDialog<T> : LayerEffectConfigurationDialog where T : EffectConfigurationViewModel
{
/// <inheritdoc />
public class LayerEffectConfigurationDialog<T> : LayerEffectConfigurationDialog where T : EffectConfigurationViewModel
public LayerEffectConfigurationDialog()
{
/// <inheritdoc />
public LayerEffectConfigurationDialog()
{
}
/// <inheritdoc />
public LayerEffectConfigurationDialog(int dialogWidth, int dialogHeight)
{
DialogWidth = dialogWidth;
DialogHeight = dialogHeight;
}
/// <inheritdoc />
public override Type Type => typeof(T);
}
/// <inheritdoc />
public LayerEffectConfigurationDialog(int dialogWidth, int dialogHeight)
{
DialogWidth = dialogWidth;
DialogHeight = dialogHeight;
}
/// <inheritdoc />
public override Type Type => typeof(T);
}
/// <summary>
/// Describes a UI tab for a specific layer effect
/// </summary>
public abstract class LayerEffectConfigurationDialog : ILayerEffectConfigurationDialog
{
/// <summary>
/// The default width of the dialog
/// </summary>
public int DialogWidth { get; set; } = 800;
/// <summary>
/// Describes a UI tab for a specific layer effect
/// The default height of the dialog
/// </summary>
public abstract class LayerEffectConfigurationDialog : ILayerEffectConfigurationDialog
{
/// <summary>
/// The default width of the dialog
/// </summary>
public int DialogWidth { get; set; } = 800;
public int DialogHeight { get; set; } = 800;
/// <summary>
/// The default height of the dialog
/// </summary>
public int DialogHeight { get; set; } = 800;
/// <summary>
/// The type of view model the dialog contains
/// </summary>
public abstract Type Type { get; }
}
/// <summary>
/// The type of view model the dialog contains
/// </summary>
public abstract Type Type { get; }
}
@@ -1,21 +1,20 @@
using System;
using Artemis.Core;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared;
/// <inheritdoc />
public class PluginConfigurationDialog<T> : PluginConfigurationDialog where T : PluginConfigurationViewModel
{
/// <inheritdoc />
public class PluginConfigurationDialog<T> : PluginConfigurationDialog where T : PluginConfigurationViewModel
{
/// <inheritdoc />
public override Type Type => typeof(T);
}
public override Type Type => typeof(T);
}
/// <summary>
/// Describes a configuration dialog for a specific plugin
/// </summary>
public abstract class PluginConfigurationDialog : IPluginConfigurationDialog
{
/// <inheritdoc />
public abstract Type Type { get; }
}
/// <summary>
/// Describes a configuration dialog for a specific plugin
/// </summary>
public abstract class PluginConfigurationDialog : IPluginConfigurationDialog
{
/// <inheritdoc />
public abstract Type Type { get; }
}
@@ -1,46 +1,45 @@
using System;
using Artemis.Core;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared;
/// <summary>
/// Represents a view model for a plugin configuration window
/// </summary>
public abstract class PluginConfigurationViewModel : ValidatableViewModelBase, IPluginConfigurationViewModel
{
/// <summary>
/// Represents a view model for a plugin configuration window
/// Creates a new instance of the <see cref="PluginConfigurationViewModel" /> class
/// </summary>
public abstract class PluginConfigurationViewModel : ValidatableViewModelBase, IPluginConfigurationViewModel
/// <param name="plugin"></param>
protected PluginConfigurationViewModel(Plugin plugin)
{
/// <summary>
/// Creates a new instance of the <see cref="PluginConfigurationViewModel" /> class
/// </summary>
/// <param name="plugin"></param>
protected PluginConfigurationViewModel(Plugin plugin)
{
Plugin = plugin;
}
/// <summary>
/// Gets the plugin this configuration view model is associated with
/// </summary>
public Plugin Plugin { get; }
/// <summary>
/// Closes the window hosting the view model
/// </summary>
public void Close()
{
CloseRequested?.Invoke(this, EventArgs.Empty);
OnCloseRequested();
}
/// <summary>
/// Called when the the window hosting the view model should close
/// </summary>
public virtual void OnCloseRequested()
{
}
/// <summary>
/// Occurs when the the window hosting the view model should close
/// </summary>
public event EventHandler? CloseRequested;
Plugin = plugin;
}
/// <summary>
/// Gets the plugin this configuration view model is associated with
/// </summary>
public Plugin Plugin { get; }
/// <summary>
/// Closes the window hosting the view model
/// </summary>
public void Close()
{
CloseRequested?.Invoke(this, EventArgs.Empty);
OnCloseRequested();
}
/// <summary>
/// Called when the the window hosting the view model should close
/// </summary>
public virtual void OnCloseRequested()
{
}
/// <summary>
/// Occurs when the the window hosting the view model should close
/// </summary>
public event EventHandler? CloseRequested;
}
@@ -1,55 +1,54 @@
using Artemis.Core.ScriptingProviders;
namespace Artemis.UI.Shared.ScriptingProviders
namespace Artemis.UI.Shared.ScriptingProviders;
/// <summary>
/// Represents a Stylet view model containing a script editor
/// </summary>
public class ScriptEditorViewModel : ActivatableViewModelBase, IScriptEditorViewModel
{
private Script? _script;
/// <summary>
/// Represents a Stylet view model containing a script editor
/// Creates a new instance of <see cref="ScriptEditorViewModel" />
/// </summary>
public class ScriptEditorViewModel : ActivatableViewModelBase, IScriptEditorViewModel
/// <param name="scriptType">The script type this view model was created for</param>
public ScriptEditorViewModel(ScriptType scriptType)
{
private Script? _script;
ScriptType = scriptType;
}
/// <summary>
/// Creates a new instance of <see cref="ScriptEditorViewModel" />
/// </summary>
/// <param name="scriptType">The script type this view model was created for</param>
public ScriptEditorViewModel(ScriptType scriptType)
{
ScriptType = scriptType;
}
/// <summary>
/// Called just before the script is changed to a different one
/// </summary>
/// <param name="script">The script to display or <see langword="null" /> if no script is to be displayed</param>
protected virtual void OnScriptChanging(Script? script)
{
}
/// <summary>
/// Called just before the script is changed to a different one
/// </summary>
/// <param name="script">The script to display or <see langword="null" /> if no script is to be displayed</param>
protected virtual void OnScriptChanging(Script? script)
{
}
/// <summary>
/// Called after the script was changed to a different one
/// </summary>
/// <param name="script">The script to display or <see langword="null" /> if no script is to be displayed</param>
protected virtual void OnScriptChanged(Script? script)
{
}
/// <summary>
/// Called after the script was changed to a different one
/// </summary>
/// <param name="script">The script to display or <see langword="null" /> if no script is to be displayed</param>
protected virtual void OnScriptChanged(Script? script)
{
}
/// <inheritdoc />
public ScriptType ScriptType { get; }
/// <inheritdoc />
public ScriptType ScriptType { get; }
/// <inheritdoc />
public Script? Script
{
get => _script;
internal set => RaiseAndSetIfChanged(ref _script, value);
}
/// <inheritdoc />
public Script? Script
{
get => _script;
internal set => RaiseAndSetIfChanged(ref _script, value);
}
/// <inheritdoc />
public void ChangeScript(Script? script)
{
OnScriptChanging(script);
Script = script;
OnScriptChanged(script);
}
/// <inheritdoc />
public void ChangeScript(Script? script)
{
OnScriptChanging(script);
Script = script;
OnScriptChanged(script);
}
}
@@ -10,7 +10,10 @@ public interface IAutoRunProvider
/// <summary>
/// Asynchronously enables auto-run.
/// </summary>
/// <param name="recreate">A boolean indicating whether the auto-run configuration should be recreated (the auto run delay changed)</param>
/// <param name="recreate">
/// A boolean indicating whether the auto-run configuration should be recreated (the auto run delay
/// changed)
/// </param>
/// <param name="autoRunDelay">The delay in seconds before the application should start (if supported)</param>
Task EnableAutoRun(bool recreate, int autoRunDelay);
@@ -15,7 +15,7 @@ public interface IUpdateProvider
Task<bool> CheckForUpdate(string channel);
/// <summary>
/// Applies any available updates.
/// Applies any available updates.
/// </summary>
/// <param name="channel">The channel to use when checking updates (i.e. master or development)</param>
/// <param name="silent">Whether or not to update silently.</param>
+65 -66
View File
@@ -8,79 +8,78 @@ using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Media;
using ReactiveUI;
namespace Artemis.UI.Shared
namespace Artemis.UI.Shared;
/// <summary>
/// A ReactiveUI <see cref="Window" /> that implements the <see cref="IViewFor{TViewModel}" /> interface and will
/// activate your ViewModel automatically if the view model implements <see cref="IActivatableViewModel" />. When
/// the DataContext property changes, this class will update the ViewModel property with the new DataContext value,
/// and vice versa.
/// </summary>
/// <typeparam name="TViewModel">ViewModel type.</typeparam>
public class ReactiveCoreWindow<TViewModel> : CoreWindow, IViewFor<TViewModel> where TViewModel : class
{
public static readonly StyledProperty<TViewModel?> ViewModelProperty = AvaloniaProperty
.Register<ReactiveCoreWindow<TViewModel>, TViewModel?>(nameof(ViewModel));
/// <summary>
/// A ReactiveUI <see cref="Window" /> that implements the <see cref="IViewFor{TViewModel}" /> interface and will
/// activate your ViewModel automatically if the view model implements <see cref="IActivatableViewModel" />. When
/// the DataContext property changes, this class will update the ViewModel property with the new DataContext value,
/// and vice versa.
/// Initializes a new instance of the <see cref="ReactiveCoreWindow{TViewModel}" /> class.
/// </summary>
/// <typeparam name="TViewModel">ViewModel type.</typeparam>
public class ReactiveCoreWindow<TViewModel> : CoreWindow, IViewFor<TViewModel> where TViewModel : class
public ReactiveCoreWindow()
{
public static readonly StyledProperty<TViewModel?> ViewModelProperty = AvaloniaProperty
.Register<ReactiveCoreWindow<TViewModel>, TViewModel?>(nameof(ViewModel));
// This WhenActivated block calls ViewModel's WhenActivated
// block if the ViewModel implements IActivatableViewModel.
this.WhenActivated(disposables => { });
this.GetObservable(DataContextProperty).Subscribe(OnDataContextChanged);
this.GetObservable(ViewModelProperty).Subscribe(OnViewModelChanged);
}
/// <summary>
/// Initializes a new instance of the <see cref="ReactiveCoreWindow{TViewModel}" /> class.
/// </summary>
public ReactiveCoreWindow()
{
// This WhenActivated block calls ViewModel's WhenActivated
// block if the ViewModel implements IActivatableViewModel.
this.WhenActivated(disposables => { });
this.GetObservable(DataContextProperty).Subscribe(OnDataContextChanged);
this.GetObservable(ViewModelProperty).Subscribe(OnViewModelChanged);
}
/// <inheritdoc />
protected override void OnOpened(EventArgs e)
{
// TODO: Move to a style and remove opacity on focus loss
base.OnOpened(e);
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || !IsWindows11)
return;
// Enable Mica on Windows 11, based on the FluentAvalonia sample application
TransparencyBackgroundFallback = Brushes.Transparent;
TransparencyLevelHint = WindowTransparencyLevel.Mica;
Color2 color = this.TryFindResource("SolidBackgroundFillColorBase", out object? value) ? (Color) value! : new Color2(32, 32, 32);
color = color.LightenPercent(-0.5f);
Background = new ImmutableSolidColorBrush(color, 0.82);
}
private void OnDataContextChanged(object? value)
{
if (value is TViewModel viewModel)
ViewModel = viewModel;
else
ViewModel = null;
}
private void OnViewModelChanged(object? value)
{
if (value == null)
ClearValue(DataContextProperty);
else if (DataContext != value) DataContext = value;
}
/// <summary>
/// The ViewModel.
/// </summary>
public TViewModel? ViewModel
{
get => GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
/// <summary>
/// The ViewModel.
/// </summary>
public TViewModel? ViewModel
{
get => GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
object? IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = (TViewModel?) value;
}
/// <inheritdoc />
protected override void OnOpened(EventArgs e)
{
// TODO: Move to a style and remove opacity on focus loss
base.OnOpened(e);
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || !IsWindows11)
return;
// Enable Mica on Windows 11, based on the FluentAvalonia sample application
TransparencyBackgroundFallback = Brushes.Transparent;
TransparencyLevelHint = WindowTransparencyLevel.Mica;
Color2 color = this.TryFindResource("SolidBackgroundFillColorBase", out object? value) ? (Color) value! : new Color2(32, 32, 32);
color = color.LightenPercent(-0.5f);
Background = new ImmutableSolidColorBrush(color, 0.82);
}
private void OnDataContextChanged(object? value)
{
if (value is TViewModel viewModel)
ViewModel = viewModel;
else
ViewModel = null;
}
private void OnViewModelChanged(object? value)
{
if (value == null)
ClearValue(DataContextProperty);
else if (DataContext != value) DataContext = value;
}
object? IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = (TViewModel?) value;
}
}
@@ -8,261 +8,260 @@ using Ninject;
using Ninject.Parameters;
using ReactiveUI;
namespace Artemis.UI.Shared.Services.Builders
namespace Artemis.UI.Shared.Services.Builders;
/// <summary>
/// Represents a builder that can be used to create Fluent UI dialogs.
/// </summary>
public class ContentDialogBuilder
{
private readonly ContentDialog _contentDialog;
private readonly IKernel _kernel;
private readonly Window _parent;
private ContentDialogViewModelBase? _viewModel;
internal ContentDialogBuilder(IKernel kernel, Window parent)
{
_kernel = kernel;
_parent = parent;
_contentDialog = new ContentDialog
{
CloseButtonText = "Close"
};
}
/// <summary>
/// Changes the title of the dialog.
/// </summary>
/// <param name="title">The new title.</param>
/// <returns>The builder that can be used to further build the dialog.</returns>
public ContentDialogBuilder WithTitle(string? title)
{
_contentDialog.Title = title;
return this;
}
/// <summary>
/// Changes the content of the dialog.
/// </summary>
/// <param name="content">The new content.</param>
/// <returns>The builder that can be used to further build the dialog.</returns>
public ContentDialogBuilder WithContent(string? content)
{
_contentDialog.Content = content;
return this;
}
/// <summary>
/// Changes the default button of the dialog that is pressed on enter.
/// </summary>
/// <param name="defaultButton">The default button.</param>
/// <returns>The builder that can be used to further build the dialog.</returns>
public ContentDialogBuilder WithDefaultButton(ContentDialogButton defaultButton)
{
_contentDialog.DefaultButton = (FluentAvalonia.UI.Controls.ContentDialogButton) defaultButton;
return this;
}
/// <summary>
/// Changes the primary button of the dialog.
/// </summary>
/// <param name="configure">An action to configure the button.</param>
/// <returns>The builder that can be used to further build the dialog.</returns>
public ContentDialogBuilder HavingPrimaryButton(Action<ContentDialogButtonBuilder> configure)
{
ContentDialogButtonBuilder builder = new();
configure(builder);
_contentDialog.IsPrimaryButtonEnabled = true;
_contentDialog.PrimaryButtonText = builder.Text;
_contentDialog.PrimaryButtonCommand = builder.Command;
_contentDialog.PrimaryButtonCommandParameter = builder.CommandParameter;
// I feel like this isn't my responsibility...
if (builder.Command != null)
{
_contentDialog.IsPrimaryButtonEnabled = builder.Command.CanExecute(builder.CommandParameter);
builder.Command.CanExecuteChanged += (_, _) => _contentDialog.IsPrimaryButtonEnabled = builder.Command.CanExecute(builder.CommandParameter);
}
return this;
}
/// <summary>
/// Changes the secondary button of the dialog.
/// </summary>
/// <param name="configure">An action to configure the button.</param>
/// <returns>The builder that can be used to further build the dialog.</returns>
public ContentDialogBuilder HavingSecondaryButton(Action<ContentDialogButtonBuilder> configure)
{
ContentDialogButtonBuilder builder = new();
configure(builder);
_contentDialog.IsSecondaryButtonEnabled = true;
_contentDialog.SecondaryButtonText = builder.Text;
_contentDialog.SecondaryButtonCommand = builder.Command;
_contentDialog.SecondaryButtonCommandParameter = builder.CommandParameter;
// I feel like this isn't my responsibility...
if (builder.Command != null)
{
_contentDialog.IsSecondaryButtonEnabled = builder.Command.CanExecute(builder.CommandParameter);
builder.Command.CanExecuteChanged += (_, _) => _contentDialog.IsSecondaryButtonEnabled = builder.Command.CanExecute(builder.CommandParameter);
}
return this;
}
/// <summary>
/// Changes the text of the close button of the dialog.
/// </summary>
/// <param name="text">The new text.</param>
/// <returns>The builder that can be used to further build the dialog.</returns>
public ContentDialogBuilder WithCloseButtonText(string? text)
{
_contentDialog.CloseButtonText = text;
return this;
}
/// <summary>
/// Changes the view model of the content dialog, hosting it inside the dialog.
/// </summary>
/// <typeparam name="T">The type of the view model to host.</typeparam>
/// <param name="viewModel">The resulting view model.</param>
/// <param name="parameters">Optional parameters to pass to the constructor of the view model, case and order sensitive.</param>
/// <returns>The builder that can be used to further build the dialog.</returns>
public ContentDialogBuilder WithViewModel<T>(out T viewModel, params (string name, object? value)[] parameters) where T : ContentDialogViewModelBase
{
IParameter[] paramsArray = parameters.Select(kv => new ConstructorArgument(kv.name, kv.value)).Cast<IParameter>().ToArray();
viewModel = _kernel.Get<T>(paramsArray);
viewModel.ContentDialog = _contentDialog;
_contentDialog.Content = viewModel;
_viewModel = viewModel;
return this;
}
/// <summary>
/// Changes the dialog to take the full height of the window it's being hosted in.
/// </summary>
/// <returns>The builder that can be used to further build the dialog.</returns>
public ContentDialogBuilder WithFullSize()
{
_contentDialog.FullSizeDesired = true;
return this;
}
/// <summary>
/// Asynchronously shows the content dialog.
/// </summary>
/// <returns>A task containing the result of the content dialog.</returns>
/// <exception cref="ArtemisSharedUIException">Thrown when the parent window does not contain a panel at its root.</exception>
public async Task<ContentDialogResult> ShowAsync()
{
if (_parent.Content is not Panel panel)
throw new ArtemisSharedUIException($"The parent window {_parent.GetType().FullName} should contain a panel at its root");
try
{
panel.Children.Add(_contentDialog);
ContentDialogResult result = await _contentDialog.ShowAsync();
// Take the dialog away from the VM in case it's going to try to hide it again or whatever...
if (_viewModel != null)
_viewModel.ContentDialog = null;
return result;
}
finally
{
panel.Children.Remove(_contentDialog);
}
}
}
/// <summary>
/// Represents a content dialog button.
/// </summary>
public enum ContentDialogButton
{
/// <summary>
/// Represents a builder that can be used to create Fluent UI dialogs.
/// No button.
/// </summary>
public class ContentDialogBuilder
None,
/// <summary>
/// The primary button.
/// </summary>
Primary,
/// <summary>
/// The secondary button.
/// </summary>
Secondary,
/// <summary>
/// The close button.
/// </summary>
Close
}
/// <summary>
/// Represents a builder that can be used to create buttons inside content dialogs.
/// </summary>
public class ContentDialogButtonBuilder
{
internal ContentDialogButtonBuilder()
{
private readonly ContentDialog _contentDialog;
private readonly IKernel _kernel;
private readonly Window _parent;
private ContentDialogViewModelBase? _viewModel;
}
internal ContentDialogBuilder(IKernel kernel, Window parent)
{
_kernel = kernel;
_parent = parent;
_contentDialog = new ContentDialog
{
CloseButtonText = "Close"
};
}
internal string? Text { get; set; }
internal ICommand? Command { get; set; }
internal Action? Action { get; set; }
internal object? CommandParameter { get; set; }
/// <summary>
/// Changes the title of the dialog.
/// </summary>
/// <param name="title">The new title.</param>
/// <returns>The builder that can be used to further build the dialog.</returns>
public ContentDialogBuilder WithTitle(string? title)
{
_contentDialog.Title = title;
return this;
}
/// <summary>
/// Changes the content of the dialog.
/// </summary>
/// <param name="content">The new content.</param>
/// <returns>The builder that can be used to further build the dialog.</returns>
public ContentDialogBuilder WithContent(string? content)
{
_contentDialog.Content = content;
return this;
}
/// <summary>
/// Changes the default button of the dialog that is pressed on enter.
/// </summary>
/// <param name="defaultButton">The default button.</param>
/// <returns>The builder that can be used to further build the dialog.</returns>
public ContentDialogBuilder WithDefaultButton(ContentDialogButton defaultButton)
{
_contentDialog.DefaultButton = (FluentAvalonia.UI.Controls.ContentDialogButton) defaultButton;
return this;
}
/// <summary>
/// Changes the primary button of the dialog.
/// </summary>
/// <param name="configure">An action to configure the button.</param>
/// <returns>The builder that can be used to further build the dialog.</returns>
public ContentDialogBuilder HavingPrimaryButton(Action<ContentDialogButtonBuilder> configure)
{
ContentDialogButtonBuilder builder = new();
configure(builder);
_contentDialog.IsPrimaryButtonEnabled = true;
_contentDialog.PrimaryButtonText = builder.Text;
_contentDialog.PrimaryButtonCommand = builder.Command;
_contentDialog.PrimaryButtonCommandParameter = builder.CommandParameter;
// I feel like this isn't my responsibility...
if (builder.Command != null)
{
_contentDialog.IsPrimaryButtonEnabled = builder.Command.CanExecute(builder.CommandParameter);
builder.Command.CanExecuteChanged += (_, _) => _contentDialog.IsPrimaryButtonEnabled = builder.Command.CanExecute(builder.CommandParameter);
}
return this;
}
/// <summary>
/// Changes the secondary button of the dialog.
/// </summary>
/// <param name="configure">An action to configure the button.</param>
/// <returns>The builder that can be used to further build the dialog.</returns>
public ContentDialogBuilder HavingSecondaryButton(Action<ContentDialogButtonBuilder> configure)
{
ContentDialogButtonBuilder builder = new();
configure(builder);
_contentDialog.IsSecondaryButtonEnabled = true;
_contentDialog.SecondaryButtonText = builder.Text;
_contentDialog.SecondaryButtonCommand = builder.Command;
_contentDialog.SecondaryButtonCommandParameter = builder.CommandParameter;
// I feel like this isn't my responsibility...
if (builder.Command != null)
{
_contentDialog.IsSecondaryButtonEnabled = builder.Command.CanExecute(builder.CommandParameter);
builder.Command.CanExecuteChanged += (_, _) => _contentDialog.IsSecondaryButtonEnabled = builder.Command.CanExecute(builder.CommandParameter);
}
return this;
}
/// <summary>
/// Changes the text of the close button of the dialog.
/// </summary>
/// <param name="text">The new text.</param>
/// <returns>The builder that can be used to further build the dialog.</returns>
public ContentDialogBuilder WithCloseButtonText(string? text)
{
_contentDialog.CloseButtonText = text;
return this;
}
/// <summary>
/// Changes the view model of the content dialog, hosting it inside the dialog.
/// </summary>
/// <typeparam name="T">The type of the view model to host.</typeparam>
/// <param name="viewModel">The resulting view model.</param>
/// <param name="parameters">Optional parameters to pass to the constructor of the view model, case and order sensitive.</param>
/// <returns>The builder that can be used to further build the dialog.</returns>
public ContentDialogBuilder WithViewModel<T>(out T viewModel, params (string name, object? value)[] parameters) where T : ContentDialogViewModelBase
{
IParameter[] paramsArray = parameters.Select(kv => new ConstructorArgument(kv.name, kv.value)).Cast<IParameter>().ToArray();
viewModel = _kernel.Get<T>(paramsArray);
viewModel.ContentDialog = _contentDialog;
_contentDialog.Content = viewModel;
_viewModel = viewModel;
return this;
}
/// <summary>
/// Changes the dialog to take the full height of the window it's being hosted in.
/// </summary>
/// <returns>The builder that can be used to further build the dialog.</returns>
public ContentDialogBuilder WithFullSize()
{
_contentDialog.FullSizeDesired = true;
return this;
}
/// <summary>
/// Asynchronously shows the content dialog.
/// </summary>
/// <returns>A task containing the result of the content dialog.</returns>
/// <exception cref="ArtemisSharedUIException">Thrown when the parent window does not contain a panel at its root.</exception>
public async Task<ContentDialogResult> ShowAsync()
{
if (_parent.Content is not Panel panel)
throw new ArtemisSharedUIException($"The parent window {_parent.GetType().FullName} should contain a panel at its root");
try
{
panel.Children.Add(_contentDialog);
ContentDialogResult result = await _contentDialog.ShowAsync();
// Take the dialog away from the VM in case it's going to try to hide it again or whatever...
if (_viewModel != null)
_viewModel.ContentDialog = null;
return result;
}
finally
{
panel.Children.Remove(_contentDialog);
}
}
/// <summary>
/// Changes text message of the button.
/// </summary>
/// <param name="text">The new text.</param>
/// <returns>The notification builder that can be used to further build the button.</returns>
public ContentDialogButtonBuilder WithText(string? text)
{
Text = text;
return this;
}
/// <summary>
/// Represents a content dialog button.
/// Changes action that is called when the button is clicked.
/// </summary>
public enum ContentDialogButton
/// <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 ContentDialogButtonBuilder WithAction(Action action)
{
/// <summary>
/// No button.
/// </summary>
None,
/// <summary>
/// The primary button.
/// </summary>
Primary,
/// <summary>
/// The secondary button.
/// </summary>
Secondary,
/// <summary>
/// The close button.
/// </summary>
Close,
Action = action;
Command = ReactiveCommand.Create(() => Action());
return this;
}
/// <summary>
/// Represents a builder that can be used to create buttons inside content dialogs.
/// Changes command that is called when the button is clicked.
/// </summary>
public class ContentDialogButtonBuilder
/// <param name="command">The command to call when the button is clicked.</param>
/// <returns>The builder that can be used to further build the button.</returns>
public ContentDialogButtonBuilder WithCommand(ICommand? command)
{
internal ContentDialogButtonBuilder()
{
}
Command = command;
return this;
}
internal string? Text { get; set; }
internal ICommand? Command { get; set; }
internal Action? Action { get; set; }
internal object? CommandParameter { get; set; }
/// <summary>
/// Changes text message of the button.
/// </summary>
/// <param name="text">The new text.</param>
/// <returns>The notification builder that can be used to further build the button.</returns>
public ContentDialogButtonBuilder WithText(string? text)
{
Text = text;
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 ContentDialogButtonBuilder WithAction(Action action)
{
Action = action;
Command = ReactiveCommand.Create(() => Action());
return this;
}
/// <summary>
/// Changes command that is called when the button is clicked.
/// </summary>
/// <param name="command">The command to call when the button is clicked.</param>
/// <returns>The builder that can be used to further build the button.</returns>
public ContentDialogButtonBuilder WithCommand(ICommand? command)
{
Command = command;
return this;
}
/// <summary>
/// Changes parameter of the command that is called when the button is clicked.
/// </summary>
/// <param name="commandParameter">The parameter of the command to call when the button is clicked.</param>
/// <returns>The builder that can be used to further build the button.</returns>
public ContentDialogButtonBuilder WithCommandParameter(object? commandParameter)
{
CommandParameter = commandParameter;
return this;
}
/// <summary>
/// Changes parameter of the command that is called when the button is clicked.
/// </summary>
/// <param name="commandParameter">The parameter of the command to call when the button is clicked.</param>
/// <returns>The builder that can be used to further build the button.</returns>
public ContentDialogButtonBuilder WithCommandParameter(object? commandParameter)
{
CommandParameter = commandParameter;
return this;
}
}
@@ -1,40 +1,39 @@
using Avalonia.Controls;
namespace Artemis.UI.Shared.Services.Builders
namespace Artemis.UI.Shared.Services.Builders;
/// <summary>
/// Represents a builder that can create a <see cref="FileDialogFilter" />.
/// </summary>
public class FileDialogFilterBuilder
{
/// <summary>
/// Represents a builder that can create a <see cref="FileDialogFilter" />.
/// </summary>
public class FileDialogFilterBuilder
private readonly FileDialogFilter _filter;
internal FileDialogFilterBuilder()
{
private readonly FileDialogFilter _filter;
_filter = new FileDialogFilter();
}
internal FileDialogFilterBuilder()
{
_filter = new FileDialogFilter();
}
/// <summary>
/// Sets the name of the filter
/// </summary>
public FileDialogFilterBuilder WithName(string name)
{
_filter.Name = name;
return this;
}
/// <summary>
/// Sets the name of the filter
/// </summary>
public FileDialogFilterBuilder WithName(string name)
{
_filter.Name = name;
return this;
}
/// <summary>
/// Adds the provided extension to the filter
/// </summary>
public FileDialogFilterBuilder WithExtension(string extension)
{
_filter.Extensions.Add(extension);
return this;
}
/// <summary>
/// Adds the provided extension to the filter
/// </summary>
public FileDialogFilterBuilder WithExtension(string extension)
{
_filter.Extensions.Add(extension);
return this;
}
internal FileDialogFilter Build()
{
return _filter;
}
internal FileDialogFilter Build()
{
return _filter;
}
}
@@ -3,86 +3,85 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Controls;
namespace Artemis.UI.Shared.Services.Builders
namespace Artemis.UI.Shared.Services.Builders;
/// <summary>
/// Represents a builder that can create a <see cref="OpenFileDialog" />.
/// </summary>
public class OpenFileDialogBuilder
{
private readonly OpenFileDialog _openFileDialog;
private readonly Window _parent;
/// <summary>
/// Represents a builder that can create a <see cref="OpenFileDialog" />.
/// Creates a new instance of the <see cref="OpenFileDialogBuilder" /> class.
/// </summary>
public class OpenFileDialogBuilder
/// <param name="parent">The parent window that will host the dialog.</param>
internal OpenFileDialogBuilder(Window parent)
{
private readonly OpenFileDialog _openFileDialog;
private readonly Window _parent;
_parent = parent;
_openFileDialog = new OpenFileDialog();
}
/// <summary>
/// Creates a new instance of the <see cref="OpenFileDialogBuilder"/> class.
/// </summary>
/// <param name="parent">The parent window that will host the dialog.</param>
internal OpenFileDialogBuilder(Window parent)
{
_parent = parent;
_openFileDialog = new OpenFileDialog();
}
/// <summary>
/// Indicate that the user can select multiple files.
/// </summary>
public OpenFileDialogBuilder WithAllowMultiple()
{
_openFileDialog.AllowMultiple = true;
return this;
}
/// <summary>
/// Indicate that the user can select multiple files.
/// </summary>
public OpenFileDialogBuilder WithAllowMultiple()
{
_openFileDialog.AllowMultiple = true;
return this;
}
/// <summary>
/// Set the title of the dialog
/// </summary>
public OpenFileDialogBuilder WithTitle(string? title)
{
_openFileDialog.Title = title;
return this;
}
/// <summary>
/// Set the title of the dialog
/// </summary>
public OpenFileDialogBuilder WithTitle(string? title)
{
_openFileDialog.Title = title;
return this;
}
/// <summary>
/// Set the initial directory of the dialog
/// </summary>
public OpenFileDialogBuilder WithDirectory(string? directory)
{
_openFileDialog.Directory = directory;
return this;
}
/// <summary>
/// Set the initial directory of the dialog
/// </summary>
public OpenFileDialogBuilder WithDirectory(string? directory)
{
_openFileDialog.Directory = directory;
return this;
}
/// <summary>
/// Set the initial file name of the dialog
/// </summary>
public OpenFileDialogBuilder WithInitialFileName(string? initialFileName)
{
_openFileDialog.InitialFileName = initialFileName;
return this;
}
/// <summary>
/// Set the initial file name of the dialog
/// </summary>
public OpenFileDialogBuilder WithInitialFileName(string? initialFileName)
{
_openFileDialog.InitialFileName = initialFileName;
return this;
}
/// <summary>
/// Add a filter to the dialog
/// </summary>
public OpenFileDialogBuilder HavingFilter(Action<FileDialogFilterBuilder> configure)
{
FileDialogFilterBuilder builder = new();
configure(builder);
/// <summary>
/// Add a filter to the dialog
/// </summary>
public OpenFileDialogBuilder HavingFilter(Action<FileDialogFilterBuilder> configure)
{
FileDialogFilterBuilder builder = new();
configure(builder);
_openFileDialog.Filters ??= new List<FileDialogFilter>();
_openFileDialog.Filters.Add(builder.Build());
_openFileDialog.Filters ??= new List<FileDialogFilter>();
_openFileDialog.Filters.Add(builder.Build());
return this;
}
return this;
}
/// <summary>
/// Asynchronously shows the file dialog.
/// </summary>
/// <returns>
/// A task that on completion returns an array containing the full path to the selected
/// files, or null if the dialog was canceled.
/// </returns>
public async Task<string[]?> ShowAsync()
{
return await _openFileDialog.ShowAsync(_parent);
}
/// <summary>
/// Asynchronously shows the file dialog.
/// </summary>
/// <returns>
/// A task that on completion returns an array containing the full path to the selected
/// files, or null if the dialog was canceled.
/// </returns>
public async Task<string[]?> ShowAsync()
{
return await _openFileDialog.ShowAsync(_parent);
}
}
@@ -3,86 +3,85 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Controls;
namespace Artemis.UI.Shared.Services.Builders
namespace Artemis.UI.Shared.Services.Builders;
/// <summary>
/// Represents a builder that can create a <see cref="SaveFileDialog" />.
/// </summary>
public class SaveFileDialogBuilder
{
private readonly Window _parent;
private readonly SaveFileDialog _saveFileDialog;
/// <summary>
/// Represents a builder that can create a <see cref="SaveFileDialog" />.
/// Creates a new instance of the <see cref="SaveFileDialogBuilder" /> class.
/// </summary>
public class SaveFileDialogBuilder
/// <param name="parent">The parent window that will host the notification.</param>
internal SaveFileDialogBuilder(Window parent)
{
private readonly Window _parent;
private readonly SaveFileDialog _saveFileDialog;
_parent = parent;
_saveFileDialog = new SaveFileDialog();
}
/// <summary>
/// Creates a new instance of the <see cref="SaveFileDialogBuilder" /> class.
/// </summary>
/// <param name="parent">The parent window that will host the notification.</param>
internal SaveFileDialogBuilder(Window parent)
{
_parent = parent;
_saveFileDialog = new SaveFileDialog();
}
/// <summary>
/// Set the title of the dialog
/// </summary>
public SaveFileDialogBuilder WithTitle(string? title)
{
_saveFileDialog.Title = title;
return this;
}
/// <summary>
/// Set the title of the dialog
/// </summary>
public SaveFileDialogBuilder WithTitle(string? title)
{
_saveFileDialog.Title = title;
return this;
}
/// <summary>
/// Set the initial directory of the dialog
/// </summary>
public SaveFileDialogBuilder WithDirectory(string? directory)
{
_saveFileDialog.Directory = directory;
return this;
}
/// <summary>
/// Set the initial directory of the dialog
/// </summary>
public SaveFileDialogBuilder WithDirectory(string? directory)
{
_saveFileDialog.Directory = directory;
return this;
}
/// <summary>
/// Set the initial file name of the dialog
/// </summary>
public SaveFileDialogBuilder WithInitialFileName(string? initialFileName)
{
_saveFileDialog.InitialFileName = initialFileName;
return this;
}
/// <summary>
/// Set the initial file name of the dialog
/// </summary>
public SaveFileDialogBuilder WithInitialFileName(string? initialFileName)
{
_saveFileDialog.InitialFileName = initialFileName;
return this;
}
/// <summary>
/// Set the default extension of the dialog
/// </summary>
public SaveFileDialogBuilder WithDefaultExtension(string? defaultExtension)
{
_saveFileDialog.DefaultExtension = defaultExtension;
return this;
}
/// <summary>
/// Set the default extension of the dialog
/// </summary>
public SaveFileDialogBuilder WithDefaultExtension(string? defaultExtension)
{
_saveFileDialog.DefaultExtension = defaultExtension;
return this;
}
/// <summary>
/// Add a filter to the dialog
/// </summary>
public SaveFileDialogBuilder HavingFilter(Action<FileDialogFilterBuilder> configure)
{
FileDialogFilterBuilder builder = new();
configure(builder);
/// <summary>
/// Add a filter to the dialog
/// </summary>
public SaveFileDialogBuilder HavingFilter(Action<FileDialogFilterBuilder> configure)
{
FileDialogFilterBuilder builder = new();
configure(builder);
_saveFileDialog.Filters ??= new List<FileDialogFilter>();
_saveFileDialog.Filters.Add(builder.Build());
_saveFileDialog.Filters ??= new List<FileDialogFilter>();
_saveFileDialog.Filters.Add(builder.Build());
return this;
}
return this;
}
/// <summary>
/// Asynchronously shows the save file dialog.
/// </summary>
/// <returns>
/// A task that on completion contains the full path of the save location, or null if the
/// dialog was canceled.
/// </returns>
public async Task<string?> ShowAsync()
{
return await _saveFileDialog.ShowAsync(_parent);
}
/// <summary>
/// Asynchronously shows the save file dialog.
/// </summary>
/// <returns>
/// A task that on completion contains the full path of the save location, or null if the
/// dialog was canceled.
/// </returns>
public async Task<string?> ShowAsync()
{
return await _saveFileDialog.ShowAsync(_parent);
}
}
@@ -11,241 +11,244 @@ using Artemis.UI.Shared.DefaultTypes.DataModel.Display;
using Ninject;
using Ninject.Parameters;
namespace Artemis.UI.Shared.Services
namespace Artemis.UI.Shared.Services;
internal class DataModelUIService : IDataModelUIService
{
internal class DataModelUIService : IDataModelUIService
private readonly IDataModelService _dataModelService;
private readonly IKernel _kernel;
private readonly List<DataModelVisualizationRegistration> _registeredDataModelDisplays;
private readonly List<DataModelVisualizationRegistration> _registeredDataModelEditors;
public DataModelUIService(IDataModelService dataModelService, IKernel kernel)
{
private readonly IDataModelService _dataModelService;
private readonly IKernel _kernel;
private readonly List<DataModelVisualizationRegistration> _registeredDataModelDisplays;
private readonly List<DataModelVisualizationRegistration> _registeredDataModelEditors;
_dataModelService = dataModelService;
_kernel = kernel;
_registeredDataModelEditors = new List<DataModelVisualizationRegistration>();
_registeredDataModelDisplays = new List<DataModelVisualizationRegistration>();
public DataModelUIService(IDataModelService dataModelService, IKernel kernel)
RegisteredDataModelEditors = new ReadOnlyCollection<DataModelVisualizationRegistration>(_registeredDataModelEditors);
RegisteredDataModelDisplays = new ReadOnlyCollection<DataModelVisualizationRegistration>(_registeredDataModelDisplays);
}
private DataModelInputViewModel InstantiateDataModelInputViewModel(DataModelVisualizationRegistration registration, DataModelPropertyAttribute? description, object? initialValue)
{
// This assumes the type can be converted, that has been checked when the VM was created
if (initialValue != null && initialValue.GetType() != registration.SupportedType)
initialValue = Convert.ChangeType(initialValue, registration.SupportedType);
IParameter[] parameters =
{
_dataModelService = dataModelService;
_kernel = kernel;
_registeredDataModelEditors = new List<DataModelVisualizationRegistration>();
_registeredDataModelDisplays = new List<DataModelVisualizationRegistration>();
new ConstructorArgument("targetDescription", description),
new ConstructorArgument("initialValue", initialValue)
};
RegisteredDataModelEditors = new ReadOnlyCollection<DataModelVisualizationRegistration>(_registeredDataModelEditors);
RegisteredDataModelDisplays = new ReadOnlyCollection<DataModelVisualizationRegistration>(_registeredDataModelDisplays);
// If this ever happens something is likely wrong with the plugin unload detection
if (registration.Plugin.Kernel == null)
throw new ArtemisSharedUIException("Cannot InstantiateDataModelInputViewModel for a registration by an uninitialized plugin");
DataModelInputViewModel viewModel = (DataModelInputViewModel) registration.Plugin.Kernel.Get(registration.ViewModelType, parameters);
viewModel.CompatibleConversionTypes = registration.CompatibleConversionTypes;
return viewModel;
}
public IReadOnlyCollection<DataModelVisualizationRegistration> RegisteredDataModelEditors { get; }
public IReadOnlyCollection<DataModelVisualizationRegistration> RegisteredDataModelDisplays { get; }
public DataModelPropertiesViewModel GetMainDataModelVisualization()
{
DataModelPropertiesViewModel viewModel = new(null, null, null);
foreach (DataModel dataModelExpansion in _dataModelService.GetDataModels().Where(d => d.IsExpansion || d.Module.IsActivated).OrderBy(d => d.DataModelDescription.Name))
viewModel.Children.Add(new DataModelPropertiesViewModel(dataModelExpansion, viewModel, new DataModelPath(dataModelExpansion)));
// Update to populate children
viewModel.Update(this, null);
viewModel.UpdateRequested += (sender, args) => viewModel.Update(this, null);
return viewModel;
}
public void UpdateModules(DataModelPropertiesViewModel mainDataModelVisualization)
{
List<DataModelVisualizationViewModel> disabledChildren = mainDataModelVisualization.Children
.Where(d => d.DataModel != null && !d.DataModel.Module.IsEnabled)
.ToList();
foreach (DataModelVisualizationViewModel child in disabledChildren)
mainDataModelVisualization.Children.Remove(child);
foreach (DataModel dataModelExpansion in _dataModelService.GetDataModels().OrderBy(d => d.DataModelDescription.Name))
{
if (mainDataModelVisualization.Children.All(c => c.DataModel != dataModelExpansion))
mainDataModelVisualization.Children.Add(
new DataModelPropertiesViewModel(dataModelExpansion, mainDataModelVisualization, new DataModelPath(dataModelExpansion))
);
}
public IReadOnlyCollection<DataModelVisualizationRegistration> RegisteredDataModelEditors { get; }
public IReadOnlyCollection<DataModelVisualizationRegistration> RegisteredDataModelDisplays { get; }
mainDataModelVisualization.Update(this, null);
}
public DataModelPropertiesViewModel GetMainDataModelVisualization()
public DataModelPropertiesViewModel? GetPluginDataModelVisualization(List<Module> modules, bool includeMainDataModel)
{
DataModelPropertiesViewModel root;
// This will contain any modules that are always available
if (includeMainDataModel)
{
DataModelPropertiesViewModel viewModel = new(null, null, null);
foreach (DataModel dataModelExpansion in _dataModelService.GetDataModels().Where(d => d.IsExpansion || d.Module.IsActivated).OrderBy(d => d.DataModelDescription.Name))
viewModel.Children.Add(new DataModelPropertiesViewModel(dataModelExpansion, viewModel, new DataModelPath(dataModelExpansion)));
// Update to populate children
viewModel.Update(this, null);
viewModel.UpdateRequested += (sender, args) => viewModel.Update(this, null);
return viewModel;
root = GetMainDataModelVisualization();
}
else
{
root = new DataModelPropertiesViewModel(null, null, null);
root.UpdateRequested += (sender, args) => root.Update(this, null);
}
public void UpdateModules(DataModelPropertiesViewModel mainDataModelVisualization)
foreach (Module module in modules)
{
List<DataModelVisualizationViewModel> disabledChildren = mainDataModelVisualization.Children
.Where(d => d.DataModel != null && !d.DataModel.Module.IsEnabled)
.ToList();
foreach (DataModelVisualizationViewModel child in disabledChildren)
mainDataModelVisualization.Children.Remove(child);
DataModel? dataModel = _dataModelService.GetPluginDataModel(module);
if (dataModel == null)
continue;
foreach (DataModel dataModelExpansion in _dataModelService.GetDataModels().OrderBy(d => d.DataModelDescription.Name))
root.Children.Add(new DataModelPropertiesViewModel(dataModel, root, new DataModelPath(dataModel)));
}
if (!root.Children.Any())
return null;
// Update to populate children
root.Update(this, null);
return root;
}
public DataModelVisualizationRegistration RegisterDataModelInput<T>(Plugin plugin, IReadOnlyCollection<Type>? compatibleConversionTypes = null) where T : DataModelInputViewModel
{
compatibleConversionTypes ??= new List<Type>();
Type viewModelType = typeof(T);
lock (_registeredDataModelEditors)
{
Type supportedType = viewModelType.BaseType!.GetGenericArguments()[0];
DataModelVisualizationRegistration? existing = _registeredDataModelEditors.FirstOrDefault(r => r.SupportedType == supportedType);
if (existing != null)
{
if (mainDataModelVisualization.Children.All(c => c.DataModel != dataModelExpansion))
{
mainDataModelVisualization.Children.Add(
new DataModelPropertiesViewModel(dataModelExpansion, mainDataModelVisualization, new DataModelPath(dataModelExpansion))
);
}
if (existing.Plugin != plugin)
throw new ArtemisSharedUIException($"Cannot register data model input for type {supportedType.Name} because an editor was already" +
$" registered by {existing.Plugin}");
return existing;
}
mainDataModelVisualization.Update(this, null);
}
_kernel.Bind(viewModelType).ToSelf();
public DataModelPropertiesViewModel? GetPluginDataModelVisualization(List<Module> modules, bool includeMainDataModel)
{
DataModelPropertiesViewModel root;
// This will contain any modules that are always available
if (includeMainDataModel)
root = GetMainDataModelVisualization();
else
// Create the registration
DataModelVisualizationRegistration registration = new(this, RegistrationType.Input, plugin, supportedType, viewModelType)
{
root = new DataModelPropertiesViewModel(null, null, null);
root.UpdateRequested += (sender, args) => root.Update(this, null);
}
foreach (Module module in modules)
{
DataModel? dataModel = _dataModelService.GetPluginDataModel(module);
if (dataModel == null)
continue;
root.Children.Add(new DataModelPropertiesViewModel(dataModel, root, new DataModelPath(dataModel)));
}
if (!root.Children.Any())
return null;
// Update to populate children
root.Update(this, null);
return root;
}
public DataModelVisualizationRegistration RegisterDataModelInput<T>(Plugin plugin, IReadOnlyCollection<Type>? compatibleConversionTypes = null) where T : DataModelInputViewModel
{
compatibleConversionTypes ??= new List<Type>();
Type viewModelType = typeof(T);
lock (_registeredDataModelEditors)
{
Type supportedType = viewModelType.BaseType!.GetGenericArguments()[0];
DataModelVisualizationRegistration? existing = _registeredDataModelEditors.FirstOrDefault(r => r.SupportedType == supportedType);
if (existing != null)
{
if (existing.Plugin != plugin)
throw new ArtemisSharedUIException($"Cannot register data model input for type {supportedType.Name} because an editor was already" +
$" registered by {existing.Plugin}");
return existing;
}
_kernel.Bind(viewModelType).ToSelf();
// Create the registration
DataModelVisualizationRegistration registration = new(this, RegistrationType.Input, plugin, supportedType, viewModelType)
{
// Apply the compatible conversion types to the registration
CompatibleConversionTypes = compatibleConversionTypes
};
_registeredDataModelEditors.Add(registration);
return registration;
}
}
public DataModelVisualizationRegistration RegisterDataModelDisplay<T>(Plugin plugin) where T : DataModelDisplayViewModel
{
Type viewModelType = typeof(T);
lock (_registeredDataModelDisplays)
{
Type supportedType = viewModelType.BaseType!.GetGenericArguments()[0];
DataModelVisualizationRegistration? existing = _registeredDataModelDisplays.FirstOrDefault(r => r.SupportedType == supportedType);
if (existing != null)
{
if (existing.Plugin != plugin)
throw new ArtemisSharedUIException($"Cannot register data model display for type {supportedType.Name} because an editor was already" +
$" registered by {existing.Plugin}");
return existing;
}
_kernel.Bind(viewModelType).ToSelf();
DataModelVisualizationRegistration registration = new(this, RegistrationType.Display, plugin, supportedType, viewModelType);
_registeredDataModelDisplays.Add(registration);
return registration;
}
}
public void RemoveDataModelInput(DataModelVisualizationRegistration registration)
{
lock (_registeredDataModelEditors)
{
if (_registeredDataModelEditors.Contains(registration))
{
registration.Unsubscribe();
_registeredDataModelEditors.Remove(registration);
_kernel.Unbind(registration.ViewModelType);
}
}
}
public void RemoveDataModelDisplay(DataModelVisualizationRegistration registration)
{
lock (_registeredDataModelDisplays)
{
if (_registeredDataModelDisplays.Contains(registration))
{
registration.Unsubscribe();
_registeredDataModelDisplays.Remove(registration);
_kernel.Unbind(registration.ViewModelType);
}
}
}
public DataModelDisplayViewModel? GetDataModelDisplayViewModel(Type propertyType, DataModelPropertyAttribute? description, bool fallBackToDefault)
{
lock (_registeredDataModelDisplays)
{
DataModelDisplayViewModel? result;
DataModelVisualizationRegistration? match = _registeredDataModelDisplays.FirstOrDefault(d => d.SupportedType == propertyType);
if (match != null)
{
// If this ever happens something is likely wrong with the plugin unload detection
if (match.Plugin.Kernel == null)
throw new ArtemisSharedUIException("Cannot GetDataModelDisplayViewModel for a registration by an uninitialized plugin");
result = (DataModelDisplayViewModel) match.Plugin.Kernel.Get(match.ViewModelType);
}
else if (!fallBackToDefault)
result = null;
else
result = _kernel.Get<DefaultDataModelDisplayViewModel>();
if (result != null)
result.PropertyDescription = description;
return result;
}
}
public DataModelInputViewModel? GetDataModelInputViewModel(Type propertyType, DataModelPropertyAttribute? description, object? initialValue, Action<object?, bool> updateCallback)
{
lock (_registeredDataModelEditors)
{
// Prefer a VM that natively supports the type
DataModelVisualizationRegistration? match = _registeredDataModelEditors.FirstOrDefault(d => d.SupportedType == propertyType);
// Fall back on a VM that supports the type through conversion
if (match == null)
match = _registeredDataModelEditors.FirstOrDefault(d => d.CompatibleConversionTypes != null && d.CompatibleConversionTypes.Contains(propertyType));
// Lastly try getting an enum VM if the provided type is an enum
if (match == null && propertyType.IsEnum)
match = _registeredDataModelEditors.FirstOrDefault(d => d.SupportedType == typeof(Enum));
if (match != null)
{
DataModelInputViewModel viewModel = InstantiateDataModelInputViewModel(match, description, initialValue);
viewModel.UpdateCallback = updateCallback;
return viewModel;
}
return null;
}
}
private DataModelInputViewModel InstantiateDataModelInputViewModel(DataModelVisualizationRegistration registration, DataModelPropertyAttribute? description, object? initialValue)
{
// This assumes the type can be converted, that has been checked when the VM was created
if (initialValue != null && initialValue.GetType() != registration.SupportedType)
initialValue = Convert.ChangeType(initialValue, registration.SupportedType);
IParameter[] parameters =
{
new ConstructorArgument("targetDescription", description),
new ConstructorArgument("initialValue", initialValue)
// Apply the compatible conversion types to the registration
CompatibleConversionTypes = compatibleConversionTypes
};
// If this ever happens something is likely wrong with the plugin unload detection
if (registration.Plugin.Kernel == null)
throw new ArtemisSharedUIException("Cannot InstantiateDataModelInputViewModel for a registration by an uninitialized plugin");
_registeredDataModelEditors.Add(registration);
return registration;
}
}
DataModelInputViewModel viewModel = (DataModelInputViewModel) registration.Plugin.Kernel.Get(registration.ViewModelType, parameters);
viewModel.CompatibleConversionTypes = registration.CompatibleConversionTypes;
return viewModel;
public DataModelVisualizationRegistration RegisterDataModelDisplay<T>(Plugin plugin) where T : DataModelDisplayViewModel
{
Type viewModelType = typeof(T);
lock (_registeredDataModelDisplays)
{
Type supportedType = viewModelType.BaseType!.GetGenericArguments()[0];
DataModelVisualizationRegistration? existing = _registeredDataModelDisplays.FirstOrDefault(r => r.SupportedType == supportedType);
if (existing != null)
{
if (existing.Plugin != plugin)
throw new ArtemisSharedUIException($"Cannot register data model display for type {supportedType.Name} because an editor was already" +
$" registered by {existing.Plugin}");
return existing;
}
_kernel.Bind(viewModelType).ToSelf();
DataModelVisualizationRegistration registration = new(this, RegistrationType.Display, plugin, supportedType, viewModelType);
_registeredDataModelDisplays.Add(registration);
return registration;
}
}
public void RemoveDataModelInput(DataModelVisualizationRegistration registration)
{
lock (_registeredDataModelEditors)
{
if (_registeredDataModelEditors.Contains(registration))
{
registration.Unsubscribe();
_registeredDataModelEditors.Remove(registration);
_kernel.Unbind(registration.ViewModelType);
}
}
}
public void RemoveDataModelDisplay(DataModelVisualizationRegistration registration)
{
lock (_registeredDataModelDisplays)
{
if (_registeredDataModelDisplays.Contains(registration))
{
registration.Unsubscribe();
_registeredDataModelDisplays.Remove(registration);
_kernel.Unbind(registration.ViewModelType);
}
}
}
public DataModelDisplayViewModel? GetDataModelDisplayViewModel(Type propertyType, DataModelPropertyAttribute? description, bool fallBackToDefault)
{
lock (_registeredDataModelDisplays)
{
DataModelDisplayViewModel? result;
DataModelVisualizationRegistration? match = _registeredDataModelDisplays.FirstOrDefault(d => d.SupportedType == propertyType);
if (match != null)
{
// If this ever happens something is likely wrong with the plugin unload detection
if (match.Plugin.Kernel == null)
throw new ArtemisSharedUIException("Cannot GetDataModelDisplayViewModel for a registration by an uninitialized plugin");
result = (DataModelDisplayViewModel) match.Plugin.Kernel.Get(match.ViewModelType);
}
else if (!fallBackToDefault)
{
result = null;
}
else
{
result = _kernel.Get<DefaultDataModelDisplayViewModel>();
}
if (result != null)
result.PropertyDescription = description;
return result;
}
}
public DataModelInputViewModel? GetDataModelInputViewModel(Type propertyType, DataModelPropertyAttribute? description, object? initialValue, Action<object?, bool> updateCallback)
{
lock (_registeredDataModelEditors)
{
// Prefer a VM that natively supports the type
DataModelVisualizationRegistration? match = _registeredDataModelEditors.FirstOrDefault(d => d.SupportedType == propertyType);
// Fall back on a VM that supports the type through conversion
if (match == null)
match = _registeredDataModelEditors.FirstOrDefault(d => d.CompatibleConversionTypes != null && d.CompatibleConversionTypes.Contains(propertyType));
// Lastly try getting an enum VM if the provided type is an enum
if (match == null && propertyType.IsEnum)
match = _registeredDataModelEditors.FirstOrDefault(d => d.SupportedType == typeof(Enum));
if (match != null)
{
DataModelInputViewModel viewModel = InstantiateDataModelInputViewModel(match, description, initialValue);
viewModel.UpdateCallback = updateCallback;
return viewModel;
}
return null;
}
}
}
@@ -1,9 +1,8 @@
namespace Artemis.UI.Shared.Services
namespace Artemis.UI.Shared.Services;
/// <summary>
/// Represents a service provided by the Artemis Shared UI library
/// </summary>
public interface IArtemisSharedUIService
{
/// <summary>
/// Represents a service provided by the Artemis Shared UI library
/// </summary>
public interface IArtemisSharedUIService
{
}
}
@@ -5,104 +5,103 @@ using Artemis.Core.Modules;
using Artemis.UI.Shared.DataModelVisualization;
using Artemis.UI.Shared.DataModelVisualization.Shared;
namespace Artemis.UI.Shared.Services
namespace Artemis.UI.Shared.Services;
/// <summary>
/// A service for UI related data model tasks
/// </summary>
public interface IDataModelUIService : IArtemisSharedUIService
{
/// <summary>
/// A service for UI related data model tasks
/// Gets a read-only list of all registered data model editors
/// </summary>
public interface IDataModelUIService : IArtemisSharedUIService
{
/// <summary>
/// Gets a read-only list of all registered data model editors
/// </summary>
IReadOnlyCollection<DataModelVisualizationRegistration> RegisteredDataModelEditors { get; }
IReadOnlyCollection<DataModelVisualizationRegistration> RegisteredDataModelEditors { get; }
/// <summary>
/// Gets a read-only list of all registered data model displays
/// </summary>
IReadOnlyCollection<DataModelVisualizationRegistration> RegisteredDataModelDisplays { get; }
/// <summary>
/// Gets a read-only list of all registered data model displays
/// </summary>
IReadOnlyCollection<DataModelVisualizationRegistration> RegisteredDataModelDisplays { get; }
/// <summary>
/// Creates a data model visualization view model for the main data model
/// </summary>
/// <returns>
/// A data model visualization view model containing all data model expansions and modules that expand the main
/// data model
/// </returns>
DataModelPropertiesViewModel GetMainDataModelVisualization();
/// <summary>
/// Creates a data model visualization view model for the main data model
/// </summary>
/// <returns>
/// A data model visualization view model containing all data model expansions and modules that expand the main
/// data model
/// </returns>
DataModelPropertiesViewModel GetMainDataModelVisualization();
/// <summary>
/// Creates a data model visualization view model for the data model of the provided plugin feature
/// </summary>
/// <param name="modules">The modules to create the data model visualization view model for</param>
/// <param name="includeMainDataModel">
/// Whether or not also to include the main data model (and therefore any modules marked
/// as <see cref="Module.IsAlwaysAvailable" />)
/// </param>
/// <returns>A data model visualization view model containing the data model of the provided feature</returns>
DataModelPropertiesViewModel? GetPluginDataModelVisualization(List<Module> modules, bool includeMainDataModel);
/// <summary>
/// Creates a data model visualization view model for the data model of the provided plugin feature
/// </summary>
/// <param name="modules">The modules to create the data model visualization view model for</param>
/// <param name="includeMainDataModel">
/// Whether or not also to include the main data model (and therefore any modules marked
/// as <see cref="Module.IsAlwaysAvailable" />)
/// </param>
/// <returns>A data model visualization view model containing the data model of the provided feature</returns>
DataModelPropertiesViewModel? GetPluginDataModelVisualization(List<Module> modules, bool includeMainDataModel);
/// <summary>
/// Updates the children of the provided main data model visualization, removing disabled children and adding newly
/// enabled children
/// </summary>
void UpdateModules(DataModelPropertiesViewModel mainDataModelVisualization);
/// <summary>
/// Updates the children of the provided main data model visualization, removing disabled children and adding newly
/// enabled children
/// </summary>
void UpdateModules(DataModelPropertiesViewModel mainDataModelVisualization);
/// <summary>
/// Registers a new data model editor
/// </summary>
/// <typeparam name="T">The type of the editor</typeparam>
/// <param name="plugin">The plugin this editor belongs to</param>
/// <param name="compatibleConversionTypes">A collection of extra types this editor supports</param>
/// <returns>A registration that can be used to remove the editor</returns>
DataModelVisualizationRegistration RegisterDataModelInput<T>(Plugin plugin, IReadOnlyCollection<Type> compatibleConversionTypes) where T : DataModelInputViewModel;
/// <summary>
/// Registers a new data model editor
/// </summary>
/// <typeparam name="T">The type of the editor</typeparam>
/// <param name="plugin">The plugin this editor belongs to</param>
/// <param name="compatibleConversionTypes">A collection of extra types this editor supports</param>
/// <returns>A registration that can be used to remove the editor</returns>
DataModelVisualizationRegistration RegisterDataModelInput<T>(Plugin plugin, IReadOnlyCollection<Type> compatibleConversionTypes) where T : DataModelInputViewModel;
/// <summary>
/// Registers a new data model display
/// </summary>
/// <typeparam name="T">The type of the display</typeparam>
/// <param name="plugin">The plugin this display belongs to</param>
/// <returns>A registration that can be used to remove the display</returns>
DataModelVisualizationRegistration RegisterDataModelDisplay<T>(Plugin plugin) where T : DataModelDisplayViewModel;
/// <summary>
/// Registers a new data model display
/// </summary>
/// <typeparam name="T">The type of the display</typeparam>
/// <param name="plugin">The plugin this display belongs to</param>
/// <returns>A registration that can be used to remove the display</returns>
DataModelVisualizationRegistration RegisterDataModelDisplay<T>(Plugin plugin) where T : DataModelDisplayViewModel;
/// <summary>
/// Removes a data model editor
/// </summary>
/// <param name="registration">
/// The registration of the editor as returned by <see cref="RegisterDataModelInput{T}" />
/// </param>
void RemoveDataModelInput(DataModelVisualizationRegistration registration);
/// <summary>
/// Removes a data model editor
/// </summary>
/// <param name="registration">
/// The registration of the editor as returned by <see cref="RegisterDataModelInput{T}" />
/// </param>
void RemoveDataModelInput(DataModelVisualizationRegistration registration);
/// <summary>
/// Removes a data model display
/// </summary>
/// <param name="registration">
/// The registration of the display as returned by <see cref="RegisterDataModelDisplay{T}" />
/// </param>
void RemoveDataModelDisplay(DataModelVisualizationRegistration registration);
/// <summary>
/// Removes a data model display
/// </summary>
/// <param name="registration">
/// The registration of the display as returned by <see cref="RegisterDataModelDisplay{T}" />
/// </param>
void RemoveDataModelDisplay(DataModelVisualizationRegistration registration);
/// <summary>
/// Creates the most appropriate display view model for the provided <paramref name="propertyType" /> that can display
/// a value
/// </summary>
/// <param name="propertyType">The type of data model property to find a display view model for</param>
/// <param name="description">The description of the data model property</param>
/// <param name="fallBackToDefault">
/// If <see langword="true"></see>, a simple .ToString() display view model will be
/// returned if nothing else is found
/// </param>
/// <returns>The most appropriate display view model for the provided <paramref name="propertyType"></paramref></returns>
DataModelDisplayViewModel? GetDataModelDisplayViewModel(Type propertyType, DataModelPropertyAttribute? description, bool fallBackToDefault = false);
/// <summary>
/// Creates the most appropriate display view model for the provided <paramref name="propertyType" /> that can display
/// a value
/// </summary>
/// <param name="propertyType">The type of data model property to find a display view model for</param>
/// <param name="description">The description of the data model property</param>
/// <param name="fallBackToDefault">
/// If <see langword="true"></see>, a simple .ToString() display view model will be
/// returned if nothing else is found
/// </param>
/// <returns>The most appropriate display view model for the provided <paramref name="propertyType"></paramref></returns>
DataModelDisplayViewModel? GetDataModelDisplayViewModel(Type propertyType, DataModelPropertyAttribute? description, bool fallBackToDefault = false);
/// <summary>
/// Creates the most appropriate input view model for the provided <paramref name="propertyType" /> that allows
/// inputting a value
/// </summary>
/// <param name="propertyType">The type of data model property to find a display view model for</param>
/// <param name="description">The description of the data model property</param>
/// <param name="initialValue">The initial value to show in the input</param>
/// <param name="updateCallback">A function to call whenever the input was updated (submitted or not)</param>
/// <returns>The most appropriate input view model for the provided <paramref name="propertyType" /></returns>
DataModelInputViewModel? GetDataModelInputViewModel(Type propertyType, DataModelPropertyAttribute? description, object? initialValue, Action<object?, bool> updateCallback);
}
/// <summary>
/// Creates the most appropriate input view model for the provided <paramref name="propertyType" /> that allows
/// inputting a value
/// </summary>
/// <param name="propertyType">The type of data model property to find a display view model for</param>
/// <param name="description">The description of the data model property</param>
/// <param name="initialValue">The initial value to show in the input</param>
/// <param name="updateCallback">A function to call whenever the input was updated (submitted or not)</param>
/// <returns>The most appropriate input view model for the provided <paramref name="propertyType" /></returns>
DataModelInputViewModel? GetDataModelInputViewModel(Type propertyType, DataModelPropertyAttribute? description, object? initialValue, Action<object?, bool> updateCallback);
}
@@ -1,16 +1,15 @@
using Artemis.UI.Shared.Services.Builders;
namespace Artemis.UI.Shared.Services
namespace Artemis.UI.Shared.Services;
/// <summary>
/// A service that can be used to create notifications in either the application or on the desktop.
/// </summary>
public interface INotificationService : IArtemisSharedUIService
{
/// <summary>
/// A service that can be used to create notifications in either the application or on the desktop.
/// Creates an in-app notification using a builder.
/// </summary>
public interface INotificationService : IArtemisSharedUIService
{
/// <summary>
/// Creates an in-app notification using a builder.
/// </summary>
/// <returns>A builder used to configure and show the notification.</returns>
NotificationBuilder CreateNotification();
}
/// <returns>A builder used to configure and show the notification.</returns>
NotificationBuilder CreateNotification();
}
@@ -3,106 +3,105 @@ using System.Threading.Tasks;
using Artemis.UI.Shared.Services.Builders;
using Avalonia.Controls;
namespace Artemis.UI.Shared.Services
namespace Artemis.UI.Shared.Services;
/// <summary>
/// A service that can be used to show windows and dialogs.
/// </summary>
public interface IWindowService : IArtemisSharedUIService
{
/// <summary>
/// A service that can be used to show windows and dialogs.
/// Creates a view model instance of type <typeparamref name="TViewModel" /> and shows its corresponding View as a
/// window
/// </summary>
public interface IWindowService : IArtemisSharedUIService
{
/// <summary>
/// Creates a view model instance of type <typeparamref name="TViewModel" /> and shows its corresponding View as a
/// window
/// </summary>
/// <typeparam name="TViewModel">The type of view model to create</typeparam>
/// <returns>The created view model</returns>
TViewModel ShowWindow<TViewModel>(params (string name, object value)[] parameters);
/// <typeparam name="TViewModel">The type of view model to create</typeparam>
/// <returns>The created view model</returns>
TViewModel ShowWindow<TViewModel>(params (string name, object value)[] parameters);
/// <summary>
/// Given a ViewModel, show its corresponding View as a window
/// </summary>
/// <param name="viewModel">ViewModel to show the View for</param>
Window ShowWindow(object viewModel);
/// <summary>
/// Given a ViewModel, show its corresponding View as a window
/// </summary>
/// <param name="viewModel">ViewModel to show the View for</param>
Window ShowWindow(object viewModel);
/// <summary>
/// Shows a dialog displaying the given exception
/// </summary>
/// <param name="title">The title of the dialog</param>
/// <param name="exception">The exception to display</param>
void ShowExceptionDialog(string title, Exception exception);
/// <summary>
/// Shows a dialog displaying the given exception
/// </summary>
/// <param name="title">The title of the dialog</param>
/// <param name="exception">The exception to display</param>
void ShowExceptionDialog(string title, Exception exception);
/// <summary>
/// Creates a view model instance of type <typeparamref name="TViewModel" /> and shows its corresponding View as a
/// dialog
/// </summary>
/// <typeparam name="TViewModel">The type of view model to create</typeparam>
/// <returns>The created view model</returns>
Task<TViewModel> ShowDialogAsync<TViewModel>(params (string name, object value)[] parameters);
/// <summary>
/// Creates a view model instance of type <typeparamref name="TViewModel" /> and shows its corresponding View as a
/// dialog
/// </summary>
/// <typeparam name="TViewModel">The type of view model to create</typeparam>
/// <returns>The created view model</returns>
Task<TViewModel> ShowDialogAsync<TViewModel>(params (string name, object value)[] parameters);
/// <summary>
/// Given a ViewModel, show its corresponding View as a dialog
/// </summary>
/// <param name="viewModel">ViewModel to show the View for</param>
Task ShowDialogAsync(object viewModel);
/// <summary>
/// Given an existing ViewModel, show its corresponding View as a Dialog
/// </summary>
/// <typeparam name="TResult">The return type</typeparam>
/// <param name="viewModel">ViewModel to show the View for</param>
/// <returns>A task containing the return value of type <typeparamref name="TResult" /></returns>
Task<TResult> ShowDialogAsync<TResult>(DialogViewModelBase<TResult> viewModel);
/// <summary>
/// Given a ViewModel, show its corresponding View as a dialog
/// </summary>
/// <param name="viewModel">ViewModel to show the View for</param>
Task ShowDialogAsync(object viewModel);
/// <summary>
/// Creates a view model instance of type <typeparamref name="TViewModel" /> and shows its corresponding View as a
/// Dialog
/// </summary>
/// <typeparam name="TViewModel">The view model type</typeparam>
/// <typeparam name="TResult">The return type</typeparam>
/// <returns>A task containing the return value of type <typeparamref name="TResult" /></returns>
Task<TResult> ShowDialogAsync<TViewModel, TResult>(params (string name, object? value)[] parameters) where TViewModel : DialogViewModelBase<TResult>;
/// <summary>
/// Given an existing ViewModel, show its corresponding View as a Dialog
/// </summary>
/// <typeparam name="TResult">The return type</typeparam>
/// <param name="viewModel">ViewModel to show the View for</param>
/// <returns>A task containing the return value of type <typeparamref name="TResult" /></returns>
Task<TResult> ShowDialogAsync<TResult>(DialogViewModelBase<TResult> viewModel);
/// <summary>
/// Shows a content dialog asking the user to confirm an action
/// </summary>
/// <param name="title">The title of the dialog</param>
/// <param name="message">The message of the dialog</param>
/// <param name="confirm">The text of the confirm button</param>
/// <param name="cancel">The text of the cancel button, if <see langword="null" /> the cancel button will not be shown</param>
/// <returns>
/// A task containing the result of the dialog, <see langword="true" /> if confirmed; otherwise
/// <see langword="false" />
/// </returns>
Task<bool> ShowConfirmContentDialog(string title, string message, string confirm = "Confirm", string? cancel = "Cancel");
/// <summary>
/// Creates a view model instance of type <typeparamref name="TViewModel" /> and shows its corresponding View as a
/// Dialog
/// </summary>
/// <typeparam name="TViewModel">The view model type</typeparam>
/// <typeparam name="TResult">The return type</typeparam>
/// <returns>A task containing the return value of type <typeparamref name="TResult" /></returns>
Task<TResult> ShowDialogAsync<TViewModel, TResult>(params (string name, object? value)[] parameters) where TViewModel : DialogViewModelBase<TResult>;
/// <summary>
/// Creates an open folder dialog, use the fluent API to configure it
/// </summary>
/// <returns>The builder that can be used to configure the dialog</returns>
OpenFolderDialogBuilder CreateOpenFolderDialog();
/// <summary>
/// Creates an open file dialog, use the fluent API to configure it
/// </summary>
/// <returns>The builder that can be used to configure the dialog</returns>
OpenFileDialogBuilder CreateOpenFileDialog();
/// <summary>
/// Shows a content dialog asking the user to confirm an action
/// </summary>
/// <param name="title">The title of the dialog</param>
/// <param name="message">The message of the dialog</param>
/// <param name="confirm">The text of the confirm button</param>
/// <param name="cancel">The text of the cancel button, if <see langword="null" /> the cancel button will not be shown</param>
/// <returns>
/// A task containing the result of the dialog, <see langword="true" /> if confirmed; otherwise
/// <see langword="false" />
/// </returns>
Task<bool> ShowConfirmContentDialog(string title, string message, string confirm = "Confirm", string? cancel = "Cancel");
/// <summary>
/// Creates a save file dialog, use the fluent API to configure it
/// </summary>
/// <returns>The builder that can be used to configure the dialog</returns>
SaveFileDialogBuilder CreateSaveFileDialog();
/// <summary>
/// Creates an open folder dialog, use the fluent API to configure it
/// </summary>
/// <returns>The builder that can be used to configure the dialog</returns>
OpenFolderDialogBuilder CreateOpenFolderDialog();
/// <summary>
/// Creates a content dialog, use the fluent API to configure it
/// </summary>
/// <returns>The builder that can be used to configure the dialog</returns>
ContentDialogBuilder CreateContentDialog();
/// <summary>
/// Creates an open file dialog, use the fluent API to configure it
/// </summary>
/// <returns>The builder that can be used to configure the dialog</returns>
OpenFileDialogBuilder CreateOpenFileDialog();
/// <summary>
/// Gets the current window of the application
/// </summary>
/// <returns>The current window of the application</returns>
Window? GetCurrentWindow();
}
/// <summary>
/// Creates a save file dialog, use the fluent API to configure it
/// </summary>
/// <returns>The builder that can be used to configure the dialog</returns>
SaveFileDialogBuilder CreateSaveFileDialog();
/// <summary>
/// Creates a content dialog, use the fluent API to configure it
/// </summary>
/// <returns>The builder that can be used to configure the dialog</returns>
ContentDialogBuilder CreateContentDialog();
/// <summary>
/// Gets the current window of the application
/// </summary>
/// <returns>The current window of the application</returns>
Window? GetCurrentWindow();
}
@@ -1,36 +1,36 @@
using System;
namespace Artemis.UI.Shared.Services.MainWindow
namespace Artemis.UI.Shared.Services.MainWindow;
/// <summary>
/// Represents a class that provides the main window, so that <see cref="IMainWindowService" /> can control the state
/// of
/// the main window.
/// </summary>
public interface IMainWindowProvider
{
/// <summary>
/// Represents a class that provides the main window, so that <see cref="IMainWindowService" /> can control the state of
/// the main window.
/// Gets a boolean indicating whether the main window is currently open
/// </summary>
public interface IMainWindowProvider
{
/// <summary>
/// Gets a boolean indicating whether the main window is currently open
/// </summary>
bool IsMainWindowOpen { get; }
bool IsMainWindowOpen { get; }
/// <summary>
/// Opens the main window
/// </summary>
void OpenMainWindow();
/// <summary>
/// Opens the main window
/// </summary>
void OpenMainWindow();
/// <summary>
/// Closes the main window
/// </summary>
void CloseMainWindow();
/// <summary>
/// Closes the main window
/// </summary>
void CloseMainWindow();
/// <summary>
/// Occurs when the main window has been opened
/// </summary>
public event EventHandler? MainWindowOpened;
/// <summary>
/// Occurs when the main window has been opened
/// </summary>
public event EventHandler? MainWindowOpened;
/// <summary>
/// Occurs when the main window has been closed
/// </summary>
public event EventHandler? MainWindowClosed;
}
/// <summary>
/// Occurs when the main window has been closed
/// </summary>
public event EventHandler? MainWindowClosed;
}
@@ -1,41 +1,40 @@
using System;
namespace Artemis.UI.Shared.Services.MainWindow
namespace Artemis.UI.Shared.Services.MainWindow;
/// <summary>
/// A service that can be used to manage the state of the main window.
/// </summary>
public interface IMainWindowService : IArtemisSharedUIService
{
/// <summary>
/// A service that can be used to manage the state of the main window.
/// Gets a boolean indicating whether the main window is currently open
/// </summary>
public interface IMainWindowService : IArtemisSharedUIService
{
/// <summary>
/// Gets a boolean indicating whether the main window is currently open
/// </summary>
bool IsMainWindowOpen { get; }
bool IsMainWindowOpen { get; }
/// <summary>
/// Sets up the main window provider that controls the state of the main window
/// </summary>
/// <param name="mainWindowProvider">The main window provider to use to control the state of the main window</param>
void ConfigureMainWindowProvider(IMainWindowProvider mainWindowProvider);
/// <summary>
/// Sets up the main window provider that controls the state of the main window
/// </summary>
/// <param name="mainWindowProvider">The main window provider to use to control the state of the main window</param>
void ConfigureMainWindowProvider(IMainWindowProvider mainWindowProvider);
/// <summary>
/// Opens the main window if it is not already open
/// </summary>
void OpenMainWindow();
/// <summary>
/// Opens the main window if it is not already open
/// </summary>
void OpenMainWindow();
/// <summary>
/// Closes the main window if it is not already closed
/// </summary>
void CloseMainWindow();
/// <summary>
/// Closes the main window if it is not already closed
/// </summary>
void CloseMainWindow();
/// <summary>
/// Occurs when the main window has been opened
/// </summary>
public event EventHandler? MainWindowOpened;
/// <summary>
/// Occurs when the main window has been opened
/// </summary>
public event EventHandler? MainWindowOpened;
/// <summary>
/// Occurs when the main window has been closed
/// </summary>
public event EventHandler? MainWindowClosed;
}
/// <summary>
/// Occurs when the main window has been closed
/// </summary>
public event EventHandler? MainWindowClosed;
}
@@ -1,80 +1,79 @@
using System;
namespace Artemis.UI.Shared.Services.MainWindow
namespace Artemis.UI.Shared.Services.MainWindow;
internal class MainWindowService : IMainWindowService
{
internal class MainWindowService : IMainWindowService
private IMainWindowProvider? _mainWindowManager;
protected virtual void OnMainWindowOpened()
{
private IMainWindowProvider? _mainWindowManager;
protected virtual void OnMainWindowOpened()
{
MainWindowOpened?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnMainWindowClosed()
{
MainWindowClosed?.Invoke(this, EventArgs.Empty);
}
private void SyncWithManager()
{
if (_mainWindowManager == null)
return;
if (IsMainWindowOpen && !_mainWindowManager.IsMainWindowOpen)
{
IsMainWindowOpen = false;
OnMainWindowClosed();
}
if (!IsMainWindowOpen && _mainWindowManager.IsMainWindowOpen)
{
IsMainWindowOpen = true;
OnMainWindowOpened();
}
}
private void HandleMainWindowOpened(object? sender, EventArgs e)
{
SyncWithManager();
}
private void HandleMainWindowClosed(object? sender, EventArgs e)
{
SyncWithManager();
}
public bool IsMainWindowOpen { get; private set; }
public void ConfigureMainWindowProvider(IMainWindowProvider mainWindowProvider)
{
if (mainWindowProvider == null) throw new ArgumentNullException(nameof(mainWindowProvider));
if (_mainWindowManager != null)
{
_mainWindowManager.MainWindowOpened -= HandleMainWindowOpened;
_mainWindowManager.MainWindowClosed -= HandleMainWindowClosed;
}
_mainWindowManager = mainWindowProvider;
_mainWindowManager.MainWindowOpened += HandleMainWindowOpened;
_mainWindowManager.MainWindowClosed += HandleMainWindowClosed;
// Sync up with the new manager's state
SyncWithManager();
}
public void OpenMainWindow()
{
_mainWindowManager?.OpenMainWindow();
}
public void CloseMainWindow()
{
_mainWindowManager?.CloseMainWindow();
}
public event EventHandler? MainWindowOpened;
public event EventHandler? MainWindowClosed;
MainWindowOpened?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnMainWindowClosed()
{
MainWindowClosed?.Invoke(this, EventArgs.Empty);
}
private void SyncWithManager()
{
if (_mainWindowManager == null)
return;
if (IsMainWindowOpen && !_mainWindowManager.IsMainWindowOpen)
{
IsMainWindowOpen = false;
OnMainWindowClosed();
}
if (!IsMainWindowOpen && _mainWindowManager.IsMainWindowOpen)
{
IsMainWindowOpen = true;
OnMainWindowOpened();
}
}
private void HandleMainWindowOpened(object? sender, EventArgs e)
{
SyncWithManager();
}
private void HandleMainWindowClosed(object? sender, EventArgs e)
{
SyncWithManager();
}
public bool IsMainWindowOpen { get; private set; }
public void ConfigureMainWindowProvider(IMainWindowProvider mainWindowProvider)
{
if (mainWindowProvider == null) throw new ArgumentNullException(nameof(mainWindowProvider));
if (_mainWindowManager != null)
{
_mainWindowManager.MainWindowOpened -= HandleMainWindowOpened;
_mainWindowManager.MainWindowClosed -= HandleMainWindowClosed;
}
_mainWindowManager = mainWindowProvider;
_mainWindowManager.MainWindowOpened += HandleMainWindowOpened;
_mainWindowManager.MainWindowClosed += HandleMainWindowClosed;
// Sync up with the new manager's state
SyncWithManager();
}
public void OpenMainWindow()
{
_mainWindowManager?.OpenMainWindow();
}
public void CloseMainWindow()
{
_mainWindowManager?.CloseMainWindow();
}
public event EventHandler? MainWindowOpened;
public event EventHandler? MainWindowClosed;
}
@@ -8,8 +8,8 @@ namespace Artemis.UI.Shared.Services.NodeEditor.Commands;
/// </summary>
public class AddNode : INodeEditorCommand, IDisposable
{
private readonly INodeScript _nodeScript;
private readonly INode _node;
private readonly INodeScript _nodeScript;
private bool _isRemoved;
/// <summary>
@@ -23,6 +23,13 @@ public class AddNode : INodeEditorCommand, IDisposable
_node = node;
}
/// <inheritdoc />
public void Dispose()
{
if (_isRemoved && _node is IDisposable disposableNode)
disposableNode.Dispose();
}
/// <inheritdoc />
public string DisplayName => $"Add '{_node.Name}' node";
@@ -30,7 +37,7 @@ public class AddNode : INodeEditorCommand, IDisposable
public void Execute()
{
_nodeScript.AddNode(_node);
_isRemoved = false;
_isRemoved = false;
}
/// <inheritdoc />
@@ -39,15 +46,4 @@ public class AddNode : INodeEditorCommand, IDisposable
_nodeScript.RemoveNode(_node);
_isRemoved = true;
}
#region IDisposable
/// <inheritdoc />
public void Dispose()
{
if (_isRemoved && _node is IDisposable disposableNode)
disposableNode.Dispose();
}
#endregion
}
@@ -9,8 +9,8 @@ namespace Artemis.UI.Shared.Services.NodeEditor.Commands;
/// </summary>
public class CompositeCommand : INodeEditorCommand, IDisposable
{
private bool _ignoreNextExecute;
private readonly List<INodeEditorCommand> _commands;
private bool _ignoreNextExecute;
/// <summary>
/// Creates a new instance of the <see cref="CompositeCommand" /> class.
@@ -45,8 +45,10 @@ public class CompositeCommand : INodeEditorCommand, IDisposable
public void Dispose()
{
foreach (INodeEditorCommand NodeEditorCommand in _commands)
{
if (NodeEditorCommand is IDisposable disposable)
disposable.Dispose();
}
}
#region Implementation of INodeEditorCommand
@@ -8,9 +8,9 @@ namespace Artemis.UI.Shared.Services.NodeEditor.Commands;
/// </summary>
public class ConnectPins : INodeEditorCommand
{
private readonly List<IPin>? _originalConnections;
private readonly IPin _source;
private readonly IPin _target;
private readonly List<IPin>? _originalConnections;
/// <summary>
/// Creates a new instance of the <see cref="ConnectPins" /> class.
@@ -42,7 +42,7 @@ public class ConnectPins : INodeEditorCommand
public void Undo()
{
_target.DisconnectFrom(_source);
if (_originalConnections == null)
return;
foreach (IPin pin in _originalConnections)
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using Artemis.Core;
namespace Artemis.UI.Shared.Services.NodeEditor.Commands;
@@ -9,9 +8,9 @@ namespace Artemis.UI.Shared.Services.NodeEditor.Commands;
/// </summary>
public class DeleteNode : INodeEditorCommand, IDisposable
{
private readonly NodeConnectionStore _connections;
private readonly INode _node;
private readonly INodeScript _nodeScript;
private readonly NodeConnectionStore _connections;
private bool _isRemoved;
/// <summary>
@@ -8,8 +8,8 @@ namespace Artemis.UI.Shared.Services.NodeEditor.Commands;
/// </summary>
public class DisconnectPins : INodeEditorCommand
{
private readonly IPin _pin;
private readonly List<IPin> _originalConnections;
private readonly IPin _pin;
/// <summary>
/// Creates a new instance of the <see cref="DisconnectPins" /> class.
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Linq;
using Artemis.Core;
using Artemis.Core.Services;
using FluentAvalonia.Core;
namespace Artemis.UI.Shared.Services.NodeEditor.Commands;
@@ -12,12 +11,12 @@ namespace Artemis.UI.Shared.Services.NodeEditor.Commands;
/// </summary>
public class DuplicateNode : INodeEditorCommand, IDisposable
{
private readonly INode _node;
private readonly bool _copyIncomingConnections;
private readonly INodeService _nodeService;
private readonly INode _node;
private readonly INodeScript _nodeScript;
private INode? _copy;
private readonly INodeService _nodeService;
private NodeConnectionStore? _connections;
private INode? _copy;
private bool _executed;
@@ -36,45 +35,6 @@ public class DuplicateNode : INodeEditorCommand, IDisposable
_nodeService = nodeService;
}
/// <inheritdoc />
public void Dispose()
{
if (!_executed && _copy is IDisposable disposableNode)
disposableNode.Dispose();
}
/// <inheritdoc />
public string DisplayName => $"Duplicate '{_node.Name}' node";
/// <inheritdoc />
public void Execute()
{
if (_copy == null)
{
_copy = CreateCopy();
_nodeScript.AddNode(_copy);
}
else if (_connections != null)
{
_nodeScript.AddNode(_copy);
_connections.Restore();
}
_executed = true;
}
/// <inheritdoc />
public void Undo()
{
if (_copy != null && _connections != null)
{
_connections.Store();
_nodeScript.RemoveNode(_copy);
}
_executed = false;
}
private INode CreateCopy()
{
NodeData? nodeData = _nodeService.AvailableNodes.FirstOrDefault(d => d.Type == _node.GetType());
@@ -123,4 +83,43 @@ public class DuplicateNode : INodeEditorCommand, IDisposable
_connections = new NodeConnectionStore(node);
return node;
}
/// <inheritdoc />
public void Dispose()
{
if (!_executed && _copy is IDisposable disposableNode)
disposableNode.Dispose();
}
/// <inheritdoc />
public string DisplayName => $"Duplicate '{_node.Name}' node";
/// <inheritdoc />
public void Execute()
{
if (_copy == null)
{
_copy = CreateCopy();
_nodeScript.AddNode(_copy);
}
else if (_connections != null)
{
_nodeScript.AddNode(_copy);
_connections.Restore();
}
_executed = true;
}
/// <inheritdoc />
public void Undo()
{
if (_copy != null && _connections != null)
{
_connections.Store();
_nodeScript.RemoveNode(_copy);
}
_executed = false;
}
}
@@ -9,9 +9,9 @@ namespace Artemis.UI.Shared.Services.NodeEditor.Commands;
/// </summary>
public class RemovePin : INodeEditorCommand
{
private readonly IPinCollection _pinCollection;
private readonly IPin _pin;
private readonly List<IPin> _originalConnections;
private readonly IPin _pin;
private readonly IPinCollection _pinCollection;
/// <summary>
/// Creates a new instance of the <see cref="RemovePin" /> class.
@@ -30,6 +30,21 @@ public class UpdateStorage<TStorage> : INodeEditorCommand, IDisposable
_originalValue = _node.Storage;
}
/// <inheritdoc />
public void Dispose()
{
if (_updatedValue)
{
if (_originalValue is IDisposable disposable)
disposable.Dispose();
}
else
{
if (_value is IDisposable disposable)
disposable.Dispose();
}
}
#region Implementation of INodeEditorCommand
/// <inheritdoc />
@@ -50,23 +65,4 @@ public class UpdateStorage<TStorage> : INodeEditorCommand, IDisposable
}
#endregion
#region IDisposable
/// <inheritdoc />
public void Dispose()
{
if (_updatedValue)
{
if (_originalValue is IDisposable disposable)
disposable.Dispose();
}
else
{
if (_value is IDisposable disposable)
disposable.Dispose();
}
}
#endregion
}
@@ -1,23 +1,22 @@
namespace Artemis.UI.Shared.Services.NodeEditor
namespace Artemis.UI.Shared.Services.NodeEditor;
/// <summary>
/// Represents a command that can be executed and if needed, undone
/// </summary>
public interface INodeEditorCommand
{
/// <summary>
/// Represents a command that can be executed and if needed, undone
/// Gets the name of the command
/// </summary>
public interface INodeEditorCommand
{
/// <summary>
/// Gets the name of the command
/// </summary>
string DisplayName { get; }
string DisplayName { get; }
/// <summary>
/// Executes the command
/// </summary>
void Execute();
/// <summary>
/// Executes the command
/// </summary>
void Execute();
/// <summary>
/// Undoes the command
/// </summary>
void Undo();
}
/// <summary>
/// Undoes the command
/// </summary>
void Undo();
}
@@ -1,6 +1,4 @@
using System;
using Artemis.Core;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.Core;
namespace Artemis.UI.Shared.Services.NodeEditor;
@@ -33,30 +33,31 @@ public class NodeEditorHistory
}
/// <summary>
/// Gets the node script the history relates to.
/// Gets the node script the history relates to.
/// </summary>
public INodeScript NodeScript { get; }
/// <summary>
/// Gets an observable sequence containing a boolean value indicating whether history can be undone.
/// </summary>
public IObservable<bool> CanUndo => _canUndo.AsObservable().DistinctUntilChanged();
/// <summary>
/// Gets an observable sequence containing a boolean value indicating whether history can be redone.
/// </summary>
public IObservable<bool> CanRedo => _canRedo.AsObservable().DistinctUntilChanged();
/// <summary>
/// Gets a reactive command that can be executed to execute an instance of a <see cref="INodeEditorCommand" /> and puts it in history.
/// Gets a reactive command that can be executed to execute an instance of a <see cref="INodeEditorCommand" /> and puts
/// it in history.
/// </summary>
public ReactiveCommand<INodeEditorCommand, Unit> Execute { get; }
/// <summary>
/// Gets a reactive command that can be executed to undo history.
/// </summary>
public ReactiveCommand<Unit, INodeEditorCommand?> Undo { get; }
/// <summary>
/// Gets a reactive command that can be executed to redo history.
/// </summary>
@@ -88,8 +89,10 @@ public class NodeEditorHistory
private void ClearRedo()
{
foreach (INodeEditorCommand nodeEditorCommand in _redoCommands)
{
if (nodeEditorCommand is IDisposable disposable)
disposable.Dispose();
}
_redoCommands.Clear();
}
@@ -97,8 +100,10 @@ public class NodeEditorHistory
private void ClearUndo()
{
foreach (INodeEditorCommand nodeEditorCommand in _undoCommands)
{
if (nodeEditorCommand is IDisposable disposable)
disposable.Dispose();
}
_undoCommands.Clear();
}
@@ -5,21 +5,34 @@ using Artemis.UI.Shared.Services.NodeEditor.Commands;
namespace Artemis.UI.Shared.Services.NodeEditor;
/// <inheritdoc cref="INodeEditorService"/>
/// <inheritdoc cref="INodeEditorService" />
public class NodeEditorService : INodeEditorService
{
private readonly Dictionary<INodeScript, NodeEditorCommandScope> _nodeEditorCommandScopes = new();
private readonly Dictionary<INodeScript, NodeEditorHistory> _nodeEditorHistories = new();
private readonly IWindowService _windowService;
/// <summary>
/// Creates a new instance of the <see cref="NodeEditorService"/> class.
/// Creates a new instance of the <see cref="NodeEditorService" /> class.
/// </summary>
public NodeEditorService(IWindowService windowService)
{
_windowService = windowService;
}
private readonly Dictionary<INodeScript, NodeEditorHistory> _nodeEditorHistories = new();
private readonly Dictionary<INodeScript, NodeEditorCommandScope> _nodeEditorCommandScopes = new();
internal void StopCommandScope(INodeScript nodeScript)
{
// This might happen if the scope is disposed twice, it's no biggie
if (!_nodeEditorCommandScopes.TryGetValue(nodeScript, out NodeEditorCommandScope? scope))
return;
_nodeEditorCommandScopes.Remove(nodeScript);
// Executing the composite command won't do anything the first time (see last ctor variable)
// commands were already executed each time they were added to the scope
ExecuteCommand(nodeScript, new CompositeCommand(scope.NodeEditorCommands, scope.Name, true));
}
/// <inheritdoc />
public NodeEditorHistory GetHistory(INodeScript nodeScript)
@@ -38,7 +51,7 @@ public class NodeEditorService : INodeEditorService
try
{
NodeEditorHistory history = GetHistory(nodeScript);
// If a scope is active add the command to it, the scope will execute it immediately
_nodeEditorCommandScopes.TryGetValue(nodeScript, out NodeEditorCommandScope? scope);
if (scope != null)
@@ -66,17 +79,4 @@ public class NodeEditorService : INodeEditorService
_nodeEditorCommandScopes.Add(nodeScript, newScope);
return newScope;
}
internal void StopCommandScope(INodeScript nodeScript)
{
// This might happen if the scope is disposed twice, it's no biggie
if (!_nodeEditorCommandScopes.TryGetValue(nodeScript, out NodeEditorCommandScope? scope))
return;
_nodeEditorCommandScopes.Remove(nodeScript);
// Executing the composite command won't do anything the first time (see last ctor variable)
// commands were already executed each time they were added to the scope
ExecuteCommand(nodeScript, new CompositeCommand(scope.NodeEditorCommands, scope.Name, true));
}
}
@@ -1,24 +1,23 @@
using Artemis.UI.Shared.Services.Builders;
using Avalonia.Controls;
namespace Artemis.UI.Shared.Services
namespace Artemis.UI.Shared.Services;
internal class NotificationService : INotificationService
{
internal class NotificationService : INotificationService
private readonly IWindowService _windowService;
public NotificationService(IWindowService windowService)
{
private readonly IWindowService _windowService;
_windowService = windowService;
}
public NotificationService(IWindowService windowService)
{
_windowService = windowService;
}
public NotificationBuilder CreateNotification()
{
Window? currentWindow = _windowService.GetCurrentWindow();
if (currentWindow == null)
throw new ArtemisSharedUIException("Can't show an in-app notification without any windows being shown.");
public NotificationBuilder CreateNotification()
{
Window? currentWindow = _windowService.GetCurrentWindow();
if (currentWindow == null)
throw new ArtemisSharedUIException("Can't show an in-app notification without any windows being shown.");
return new NotificationBuilder(currentWindow);
}
return new NotificationBuilder(currentWindow);
}
}
@@ -5,16 +5,16 @@ using Artemis.Core.LayerEffects;
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
/// <summary>
/// Represents a profile editor command that can be used to add a layer effect to a profile element.
/// Represents a profile editor command that can be used to add a layer effect to a profile element.
/// </summary>
public class AddLayerEffect : IProfileEditorCommand, IDisposable
{
private readonly RenderProfileElement _renderProfileElement;
private readonly BaseLayerEffect _layerEffect;
private readonly RenderProfileElement _renderProfileElement;
private bool _executed;
/// <summary>
/// Creates a new instance of the <see cref="AddLayerEffect"/> class.
/// Creates a new instance of the <see cref="AddLayerEffect" /> class.
/// </summary>
public AddLayerEffect(RenderProfileElement renderProfileElement, BaseLayerEffect layerEffect)
{
@@ -22,6 +22,13 @@ public class AddLayerEffect : IProfileEditorCommand, IDisposable
_layerEffect = layerEffect;
}
/// <inheritdoc />
public void Dispose()
{
if (!_executed)
_layerEffect.Dispose();
}
/// <inheritdoc />
public string DisplayName => "Add layer effect";
@@ -38,11 +45,4 @@ public class AddLayerEffect : IProfileEditorCommand, IDisposable
_renderProfileElement.RemoveLayerEffect(_layerEffect);
_executed = false;
}
/// <inheritdoc />
public void Dispose()
{
if (!_executed)
_layerEffect.Dispose();
}
}
@@ -4,7 +4,7 @@ using Artemis.Core;
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
/// <summary>
/// Represents a profile editor command that can be used to add a profile element.
/// Represents a profile editor command that can be used to add a profile element.
/// </summary>
public class AddProfileElement : IProfileEditorCommand, IDisposable
{
@@ -14,7 +14,7 @@ public class AddProfileElement : IProfileEditorCommand, IDisposable
private bool _isAdded;
/// <summary>
/// Creates a new instance of the <see cref="AddProfileElement"/> class.
/// Creates a new instance of the <see cref="AddProfileElement" /> class.
/// </summary>
public AddProfileElement(RenderProfileElement subject, ProfileElement target, int index)
{
@@ -8,9 +8,9 @@ namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
/// </summary>
public class ChangeConditionType : IProfileEditorCommand, IDisposable
{
private readonly RenderProfileElement _profileElement;
private readonly ICondition _condition;
private readonly ICondition _oldCondition;
private readonly RenderProfileElement _profileElement;
private bool _executed;
/// <summary>
@@ -24,7 +24,16 @@ public class ChangeConditionType : IProfileEditorCommand, IDisposable
_condition = condition;
_oldCondition = _profileElement.DisplayCondition;
}
/// <inheritdoc />
public void Dispose()
{
if (_executed)
_oldCondition?.Dispose();
else
_condition?.Dispose();
}
/// <inheritdoc />
public string DisplayName => "Change element display condition type";
@@ -41,13 +50,4 @@ public class ChangeConditionType : IProfileEditorCommand, IDisposable
_profileElement.DisplayCondition = _oldCondition;
_executed = false;
}
/// <inheritdoc />
public void Dispose()
{
if (_executed)
_oldCondition?.Dispose();
else
_condition?.Dispose();
}
}
@@ -8,12 +8,12 @@ namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
/// </summary>
public class ChangeKeyframeEasing : IProfileEditorCommand
{
private readonly ILayerPropertyKeyframe _keyframe;
private readonly Easings.Functions _easingFunction;
private readonly ILayerPropertyKeyframe _keyframe;
private readonly Easings.Functions _originalEasingFunction;
/// <summary>
/// Creates a new instance of the <see cref="ChangeKeyframeEasing"/> class.
/// Creates a new instance of the <see cref="ChangeKeyframeEasing" /> class.
/// </summary>
public ChangeKeyframeEasing(ILayerPropertyKeyframe keyframe, Easings.Functions easingFunction)
{
@@ -12,9 +12,9 @@ public class ChangeLayerBrush : IProfileEditorCommand, IDisposable
private readonly Layer _layer;
private readonly LayerBrushDescriptor _layerBrushDescriptor;
private readonly BaseLayerBrush? _previousBrush;
private bool _executed;
private BaseLayerBrush? _newBrush;
private bool _executed;
/// <summary>
/// Creates a new instance of the <see cref="ChangeLayerBrush" /> class.
@@ -26,6 +26,15 @@ public class ChangeLayerBrush : IProfileEditorCommand, IDisposable
_previousBrush = _layer.LayerBrush;
}
/// <inheritdoc />
public void Dispose()
{
if (_executed)
_previousBrush?.Dispose();
else
_newBrush?.Dispose();
}
#region Implementation of IProfileEditorCommand
/// <inheritdoc />
@@ -38,7 +47,7 @@ public class ChangeLayerBrush : IProfileEditorCommand, IDisposable
_newBrush ??= _layerBrushDescriptor.CreateInstance(_layer, null);
// Change the brush to the new brush
_layer.ChangeLayerBrush(_newBrush);
_executed = true;
}
@@ -50,17 +59,4 @@ public class ChangeLayerBrush : IProfileEditorCommand, IDisposable
}
#endregion
#region IDisposable
/// <inheritdoc />
public void Dispose()
{
if (_executed)
_previousBrush?.Dispose();
else
_newBrush?.Dispose();
}
#endregion
}
@@ -9,8 +9,8 @@ namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
/// </summary>
public class CompositeCommand : IProfileEditorCommand, IDisposable
{
private bool _ignoreNextExecute;
private readonly List<IProfileEditorCommand> _commands;
private bool _ignoreNextExecute;
/// <summary>
/// Creates a new instance of the <see cref="CompositeCommand" /> class.
@@ -45,8 +45,10 @@ public class CompositeCommand : IProfileEditorCommand, IDisposable
public void Dispose()
{
foreach (IProfileEditorCommand profileEditorCommand in _commands)
{
if (profileEditorCommand is IDisposable disposable)
disposable.Dispose();
}
}
#region Implementation of IProfileEditorCommand
@@ -11,11 +11,6 @@ public class DuplicateKeyframe : IProfileEditorCommand
private readonly ILayerPropertyKeyframe _keyframe;
private readonly TimeSpan _position;
/// <summary>
/// Gets the duplicated keyframe, only available after the command has been executed.
/// </summary>
public ILayerPropertyKeyframe? Duplication { get; private set; }
/// <summary>
/// Creates a new instance of the <see cref="DeleteKeyframe" /> class.
/// </summary>
@@ -27,6 +22,11 @@ public class DuplicateKeyframe : IProfileEditorCommand
_position = position;
}
/// <summary>
/// Gets the duplicated keyframe, only available after the command has been executed.
/// </summary>
public ILayerPropertyKeyframe? Duplication { get; private set; }
#region Implementation of IProfileEditorCommand
/// <inheritdoc />
@@ -5,16 +5,16 @@ using Artemis.Core.LayerEffects;
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
/// <summary>
/// Represents a profile editor command that can be used to remove a layer effect from a profile element.
/// Represents a profile editor command that can be used to remove a layer effect from a profile element.
/// </summary>
public class RemoveLayerEffect : IProfileEditorCommand, IDisposable
{
private readonly RenderProfileElement _renderProfileElement;
private readonly BaseLayerEffect _layerEffect;
private readonly RenderProfileElement _renderProfileElement;
private bool _executed;
/// <summary>
/// Creates a new instance of the <see cref="RemoveLayerEffect"/> class.
/// Creates a new instance of the <see cref="RemoveLayerEffect" /> class.
/// </summary>
public RemoveLayerEffect(BaseLayerEffect layerEffect)
{
@@ -22,6 +22,13 @@ public class RemoveLayerEffect : IProfileEditorCommand, IDisposable
_layerEffect = layerEffect;
}
/// <inheritdoc />
public void Dispose()
{
if (_executed)
_layerEffect.Dispose();
}
/// <inheritdoc />
public string DisplayName => "Remove layer effect";
@@ -38,11 +45,4 @@ public class RemoveLayerEffect : IProfileEditorCommand, IDisposable
_renderProfileElement.AddLayerEffect(_layerEffect);
_executed = false;
}
/// <inheritdoc />
public void Dispose()
{
if (_executed)
_layerEffect.Dispose();
}
}
@@ -4,18 +4,18 @@ using Artemis.Core;
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
/// <summary>
/// Represents a profile editor command that can be used to remove a profile element.
/// Represents a profile editor command that can be used to remove a profile element.
/// </summary>
public class RemoveProfileElement : IProfileEditorCommand, IDisposable
{
private readonly int _index;
private readonly RenderProfileElement _subject;
private readonly ProfileElement _target;
private bool _isRemoved;
private readonly bool _wasEnabled;
private bool _isRemoved;
/// <summary>
/// Creates a new instance of the <see cref="RemoveProfileElement"/> class.
/// Creates a new instance of the <see cref="RemoveProfileElement" /> class.
/// </summary>
public RemoveProfileElement(RenderProfileElement subject)
{
@@ -1,11 +1,9 @@
using System;
using Artemis.Core;
using Artemis.Core.LayerEffects;
using Artemis.Core.LayerEffects;
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
/// <summary>
/// Represents a profile editor command that can be used to rename a layer effect
/// Represents a profile editor command that can be used to rename a layer effect
/// </summary>
public class RenameLayerEffect : IProfileEditorCommand
{
@@ -15,7 +13,7 @@ public class RenameLayerEffect : IProfileEditorCommand
private readonly bool _wasRenamed;
/// <summary>
/// Creates a new instance of the <see cref="RenameLayerEffect"/> class.
/// Creates a new instance of the <see cref="RenameLayerEffect" /> class.
/// </summary>
public RenameLayerEffect(BaseLayerEffect layerEffect, string name)
{
@@ -7,9 +7,9 @@ namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
/// </summary>
public class ResetLayerProperty<T> : IProfileEditorCommand
{
private readonly bool _keyframesEnabled;
private readonly LayerProperty<T> _layerProperty;
private readonly T _originalBaseValue;
private readonly bool _keyframesEnabled;
/// <summary>
/// Creates a new instance of the <see cref="ResetLayerProperty{T}" /> class.
@@ -37,13 +37,13 @@ public class ResetLayerProperty<T> : IProfileEditorCommand
if (_keyframesEnabled)
_layerProperty.KeyframesEnabled = false;
_layerProperty.SetCurrentValue(CoreJson.DeserializeObject<T>(json)!, null);
_layerProperty.SetCurrentValue(CoreJson.DeserializeObject<T>(json)!);
}
/// <inheritdoc />
public void Undo()
{
_layerProperty.SetCurrentValue(_originalBaseValue, null);
_layerProperty.SetCurrentValue(_originalBaseValue);
if (_keyframesEnabled)
_layerProperty.KeyframesEnabled = true;
}
@@ -5,18 +5,18 @@ using Artemis.Core;
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
/// <summary>
/// Represents a profile editor command that can be used to enable or disable keyframes on a layer property.
/// Represents a profile editor command that can be used to enable or disable keyframes on a layer property.
/// </summary>
/// <typeparam name="T"></typeparam>
public class ToggleLayerPropertyKeyframes<T> : IProfileEditorCommand
{
private readonly bool _enable;
private readonly TimeSpan _time;
private readonly LayerProperty<T> _layerProperty;
private readonly TimeSpan _time;
private LayerPropertyKeyframe<T>? _keyframe;
/// <summary>
/// Creates a new instance of the <see cref="ToggleLayerPropertyKeyframes{T}"/> class.
/// Creates a new instance of the <see cref="ToggleLayerPropertyKeyframes{T}" /> class.
/// </summary>
public ToggleLayerPropertyKeyframes(LayerProperty<T> layerProperty, bool enable, TimeSpan time)
{
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using Artemis.Core;
@@ -11,8 +10,8 @@ namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
public class UpdateColorGradient : IProfileEditorCommand
{
private readonly ColorGradient _colorGradient;
private readonly List<ColorGradientStop> _stops;
private readonly List<ColorGradientStop> _originalStops;
private readonly List<ColorGradientStop> _stops;
/// <summary>
/// Creates a new instance of the <see cref="UpdateColorGradient" /> class.
@@ -10,9 +10,9 @@ namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
public class UpdateEventConditionPath : IProfileEditorCommand, IDisposable
{
private readonly EventCondition _eventCondition;
private readonly DataModelPath? _value;
private readonly DataModelPath? _oldValue;
private readonly NodeConnectionStore? _store;
private readonly DataModelPath? _value;
private bool _executed;
/// <summary>
@@ -29,6 +29,15 @@ public class UpdateEventConditionPath : IProfileEditorCommand, IDisposable
_store = new NodeConnectionStore(startNode);
}
/// <inheritdoc />
public void Dispose()
{
if (_executed)
_oldValue?.Dispose();
else
_value?.Dispose();
}
/// <inheritdoc />
public string DisplayName => "Update event path";
@@ -57,17 +66,4 @@ public class UpdateEventConditionPath : IProfileEditorCommand, IDisposable
_executed = false;
}
#region IDisposable
/// <inheritdoc />
public void Dispose()
{
if (_executed)
_oldValue?.Dispose();
else
_value?.Dispose();
}
#endregion
}
@@ -8,8 +8,8 @@ namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
public class UpdateEventOverlapMode : IProfileEditorCommand
{
private readonly EventCondition _eventCondition;
private readonly EventOverlapMode _value;
private readonly EventOverlapMode _oldValue;
private readonly EventOverlapMode _value;
/// <summary>
/// Creates a new instance of the <see cref="UpdateEventOverlapMode" /> class.
@@ -8,8 +8,8 @@ namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
public class UpdateEventToggleOffMode : IProfileEditorCommand
{
private readonly EventCondition _eventCondition;
private readonly EventToggleOffMode _value;
private readonly EventToggleOffMode _oldValue;
private readonly EventToggleOffMode _value;
/// <summary>
/// Creates a new instance of the <see cref="UpdateEventOverlapMode" /> class.
@@ -9,11 +9,11 @@ namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
/// </summary>
public class UpdateLayerProperty<T> : IProfileEditorCommand
{
private readonly bool _hasKeyframe;
private readonly LayerProperty<T> _layerProperty;
private readonly T _newValue;
private readonly T _originalValue;
private readonly TimeSpan? _time;
private readonly bool _hasKeyframe;
private LayerPropertyKeyframe<T>? _newKeyframe;
/// <summary>
@@ -65,7 +65,9 @@ public class UpdateLayerProperty<T> : IProfileEditorCommand
DisplayName = $"Add {_layerProperty.PropertyDescription.Name ?? "property"} keyframe";
}
else
{
_layerProperty.SetCurrentValue(_newValue, _time);
}
}
/// <inheritdoc />
Loaded 100 of 691 files, more files were not shown because too many files have changed in this diff. Show more