using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using Artemis.Core;
using Artemis.UI.Avalonia.Shared.Events;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Threading;
using Avalonia.Visuals.Media.Imaging;
namespace Artemis.UI.Avalonia.Shared.Controls
{
///
/// Visualizes an with optional per-LED colors
///
public class DeviceVisualizer : Control
{
private const double UpdateFrameRate = 25.0;
private readonly List _deviceVisualizerLeds;
private readonly DispatcherTimer _timer;
private Rect _deviceBounds;
private RenderTargetBitmap? _deviceImage;
private List? _dimmedLeds;
private List? _highlightedLeds;
private ArtemisDevice? _oldDevice;
///
public DeviceVisualizer()
{
_timer = new DispatcherTimer(DispatcherPriority.Render) {Interval = TimeSpan.FromMilliseconds(1000.0 / UpdateFrameRate)};
_deviceVisualizerLeds = new List();
PointerReleased += OnPointerReleased;
}
///
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);
// Scale the visualization in the desired bounding box
DrawingContext.PushedState? boundsPush = null;
if (Bounds.Width > 0 && Bounds.Height > 0)
boundsPush = drawingContext.PushPostTransform(Matrix.CreateScale(scale, scale));
// Apply device rotation
using DrawingContext.PushedState translationPush = drawingContext.PushPostTransform(Matrix.CreateTranslation(0 - _deviceBounds.Left, 0 - _deviceBounds.Top));
using DrawingContext.PushedState rotationPush = drawingContext.PushPostTransform(Matrix.CreateRotation(Device.Rotation));
// Apply device scale
using DrawingContext.PushedState scalePush = drawingContext.PushPostTransform(Matrix.CreateScale(Device.Scale, Device.Scale));
// Render device and LED images
if (_deviceImage != null)
drawingContext.DrawImage(_deviceImage, new Rect(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height));
foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds)
deviceVisualizerLed.RenderGeometry(drawingContext, false);
boundsPush?.Dispose();
}
///
/// Occurs when a LED of the device has been clicked
///
public event EventHandler? LedClicked;
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
_deviceImage?.Dispose();
_deviceImage = null;
base.OnDetachedFromVisualTree(e);
}
///
/// Invokes the event
///
///
protected virtual void OnLedClicked(LedClickedEventArgs e)
{
LedClicked?.Invoke(this, e);
}
private void Update()
{
InvalidateVisual();
}
private void UpdateTransform()
{
InvalidateVisual();
InvalidateMeasure();
}
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));
}
private void DevicePropertyChanged(object? sender, PropertyChangedEventArgs e)
{
Dispatcher.UIThread.Post(SetupForDevice);
}
private void DeviceUpdated(object? sender, EventArgs e)
{
Dispatcher.UIThread.Post(SetupForDevice);
}
#region Properties
///
/// Gets or sets the to display
///
public static readonly StyledProperty DeviceProperty =
AvaloniaProperty.Register(nameof(Device), notifying: DeviceUpdated);
private static void DeviceUpdated(IAvaloniaObject sender, bool before)
{
if (!before)
((DeviceVisualizer) sender).SetupForDevice();
}
///
/// Gets or sets the to display
///
public ArtemisDevice? Device
{
get => GetValue(DeviceProperty);
set => SetValue(DeviceProperty, value);
}
///
/// Gets or sets boolean indicating whether or not to show per-LED colors
///
public static readonly StyledProperty ShowColorsProperty =
AvaloniaProperty.Register(nameof(ShowColors));
///
/// Gets or sets a boolean indicating whether or not to show per-LED colors
///
public bool ShowColors
{
get => GetValue(ShowColorsProperty);
set => SetValue(ShowColorsProperty, value);
}
///
/// Gets or sets a list of LEDs to highlight
///
public static readonly StyledProperty?> HighlightedLedsProperty =
AvaloniaProperty.Register?>(nameof(HighlightedLeds));
///
/// Gets or sets a list of LEDs to highlight
///
public ObservableCollection? HighlightedLeds
{
get => GetValue(HighlightedLedsProperty);
set => SetValue(HighlightedLedsProperty, value);
}
#endregion
#region Lifetime management
///
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
_timer.Start();
_timer.Tick += TimerOnTick;
base.OnAttachedToLogicalTree(e);
}
///
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
_timer.Stop();
_timer.Tick -= TimerOnTick;
base.OnDetachedFromLogicalTree(e);
}
private void SetupForDevice()
{
_deviceImage = null;
_deviceVisualizerLeds.Clear();
_highlightedLeds = new List();
_dimmedLeds = new List();
if (Device == null)
return;
if (_oldDevice != null)
{
Device.RgbDevice.PropertyChanged -= DevicePropertyChanged;
Device.DeviceUpdated -= DeviceUpdated;
}
_oldDevice = Device;
_deviceBounds = MeasureDevice();
Device.RgbDevice.PropertyChanged += DevicePropertyChanged;
Device.DeviceUpdated += DeviceUpdated;
UpdateTransform();
// Create all the LEDs
foreach (ArtemisLed artemisLed in Device.Leds)
_deviceVisualizerLeds.Add(new DeviceVisualizerLed(artemisLed));
// Load the device main image
if (Device.Layout?.Image != null && File.Exists(Device.Layout.Image.LocalPath))
{
try
{
// Create a bitmap that'll be used to render the device and LED images just once
RenderTargetBitmap renderTargetBitmap = new(new PixelSize((int) Device.RgbDevice.Size.Width * 4, (int) Device.RgbDevice.Size.Height * 4));
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);
foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds)
deviceVisualizerLed.DrawBitmap(context);
_deviceImage = renderTargetBitmap;
}
catch
{
// ignored
}
}
InvalidateMeasure();
}
#endregion
}
}