using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Timers;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Artemis.Core;
using Stylet;
namespace Artemis.UI.Shared
{
///
/// Visualizes an with optional per-LED colors
///
public class DeviceVisualizer : FrameworkElement, IDisposable
{
///
/// The device to visualize
///
public static readonly DependencyProperty DeviceProperty = DependencyProperty.Register(nameof(Device), typeof(ArtemisDevice), typeof(DeviceVisualizer),
new FrameworkPropertyMetadata(default(ArtemisDevice), FrameworkPropertyMetadataOptions.AffectsRender, DevicePropertyChangedCallback));
///
/// Whether or not to show per-LED colors
///
public static readonly DependencyProperty ShowColorsProperty = DependencyProperty.Register(nameof(ShowColors), typeof(bool), typeof(DeviceVisualizer),
new FrameworkPropertyMetadata(default(bool), FrameworkPropertyMetadataOptions.AffectsRender, ShowColorsPropertyChangedCallback));
///
/// A list of LEDs to highlight
///
public static readonly DependencyProperty HighlightedLedsProperty = DependencyProperty.Register(nameof(HighlightedLeds), typeof(IEnumerable), typeof(DeviceVisualizer),
new FrameworkPropertyMetadata(default(IEnumerable)));
private readonly DrawingGroup _backingStore;
private readonly List _deviceVisualizerLeds;
private readonly Timer _timer;
private BitmapImage? _deviceImage;
private ArtemisDevice? _oldDevice;
///
/// Creates a new instance of the class
///
public DeviceVisualizer()
{
_backingStore = new DrawingGroup();
_deviceVisualizerLeds = new List();
// Run an update timer at 25 fps
_timer = new Timer(40);
MouseLeftButtonUp += OnMouseLeftButtonUp;
Loaded += OnLoaded;
Unloaded += OnUnloaded;
}
///
/// Gets or sets the device to visualize
///
public ArtemisDevice? Device
{
get => (ArtemisDevice) GetValue(DeviceProperty);
set => SetValue(DeviceProperty, value);
}
///
/// Gets or sets whether or not to show per-LED colors
///
public bool ShowColors
{
get => (bool) GetValue(ShowColorsProperty);
set => SetValue(ShowColorsProperty, value);
}
///
/// Gets or sets a list of LEDs to highlight
///
public IEnumerable? HighlightedLeds
{
get => (IEnumerable) GetValue(HighlightedLedsProperty);
set => SetValue(HighlightedLedsProperty, value);
}
///
/// Occurs when a LED of the device has been clicked
///
public event EventHandler? LedClicked;
///
protected override void OnRender(DrawingContext drawingContext)
{
if (Device == null)
return;
// Determine the scale required to fit the desired size of the control
Size measureSize = MeasureDevice();
double scale = Math.Min(RenderSize.Width / measureSize.Width, RenderSize.Height / measureSize.Height);
// Scale the visualization in the desired bounding box
if (RenderSize.Width > 0 && RenderSize.Height > 0)
drawingContext.PushTransform(new ScaleTransform(scale, scale));
// Determine the offset required to rotate within bounds
Rect rotationRect = new(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height);
rotationRect.Transform(new RotateTransform(Device.Rotation).Value);
// Apply device rotation
drawingContext.PushTransform(new TranslateTransform(0 - rotationRect.Left, 0 - rotationRect.Top));
drawingContext.PushTransform(new RotateTransform(Device.Rotation));
// Apply device scale
drawingContext.PushTransform(new ScaleTransform(Device.Scale, Device.Scale));
// Render device and LED images
if (_deviceImage != null)
drawingContext.DrawImage(_deviceImage, new Rect(0, 0, Device.RgbDevice.Size.Width, Device.RgbDevice.Size.Height));
foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds)
deviceVisualizerLed.RenderImage(drawingContext);
drawingContext.DrawDrawing(_backingStore);
}
///
protected override Size MeasureOverride(Size availableSize)
{
if (Device == null)
return Size.Empty;
Size deviceSize = MeasureDevice();
if (deviceSize.Width <= 0 || deviceSize.Height <= 0)
return Size.Empty;
return ResizeKeepAspect(deviceSize, availableSize.Width, availableSize.Height);
}
///
/// Invokes the event
///
///
protected virtual void OnLedClicked(LedClickedEventArgs e)
{
LedClicked?.Invoke(this, e);
}
///
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
///
///
/// to release both managed and unmanaged resources;
/// to release only unmanaged resources.
///
protected virtual void Dispose(bool disposing)
{
if (disposing)
_timer.Dispose();
}
private static Size ResizeKeepAspect(Size src, double maxWidth, double maxHeight)
{
double scale;
if (double.IsPositiveInfinity(maxWidth) && !double.IsPositiveInfinity(maxHeight))
scale = maxHeight / src.Height;
else if (!double.IsPositiveInfinity(maxWidth) && double.IsPositiveInfinity(maxHeight))
scale = maxWidth / src.Width;
else if (double.IsPositiveInfinity(maxWidth) && double.IsPositiveInfinity(maxHeight))
return src;
else
scale = Math.Min(maxWidth / src.Width, maxHeight / src.Height);
return new Size(src.Width * scale, src.Height * scale);
}
private Size MeasureDevice()
{
if (Device == null)
return Size.Empty;
Rect rotationRect = new(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height);
rotationRect.Transform(new RotateTransform(Device.Rotation).Value);
return rotationRect.Size;
}
private void OnUnloaded(object? sender, RoutedEventArgs e)
{
_timer.Stop();
_timer.Elapsed -= TimerOnTick;
if (_oldDevice != null)
{
if (Device != null)
{
Device.RgbDevice.PropertyChanged -= DevicePropertyChanged;
Device.DeviceUpdated -= DeviceUpdated;
}
_oldDevice = null;
}
}
private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (Device == null)
return;
Point position = e.GetPosition(this);
double x = position.X / RenderSize.Width;
double y = position.Y / RenderSize.Height;
Point scaledPosition = new(x * Device.Rectangle.Width, y * Device.Rectangle.Height);
DeviceVisualizerLed? deviceVisualizerLed = _deviceVisualizerLeds.FirstOrDefault(l => l.DisplayGeometry != null && l.LedRect.Contains(scaledPosition));
if (deviceVisualizerLed != null)
OnLedClicked(new LedClickedEventArgs(deviceVisualizerLed.Led.Device, deviceVisualizerLed.Led));
}
private void OnLoaded(object? sender, RoutedEventArgs e)
{
_timer.Start();
_timer.Elapsed += TimerOnTick;
}
private void TimerOnTick(object? sender, EventArgs e)
{
Execute.PostToUIThread(() =>
{
if (ShowColors && Visibility == Visibility.Visible)
Render();
});
}
private void UpdateTransform()
{
InvalidateVisual();
InvalidateMeasure();
}
private static void DevicePropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DeviceVisualizer deviceVisualizer = (DeviceVisualizer) d;
deviceVisualizer.Dispatcher.Invoke(() => { deviceVisualizer.SetupForDevice(); });
}
private static void ShowColorsPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DeviceVisualizer deviceVisualizer = (DeviceVisualizer) d;
deviceVisualizer.Dispatcher.Invoke(() => { deviceVisualizer.SetupForDevice(); });
}
private void SetupForDevice()
{
_deviceImage = null;
_deviceVisualizerLeds.Clear();
if (Device == null)
return;
if (_oldDevice != null)
{
Device.RgbDevice.PropertyChanged -= DevicePropertyChanged;
Device.DeviceUpdated -= DeviceUpdated;
}
_oldDevice = Device;
Device.RgbDevice.PropertyChanged += DevicePropertyChanged;
Device.DeviceUpdated += DeviceUpdated;
UpdateTransform();
// Load the device main image
if (Device.Layout?.Image != null && File.Exists(Device.Layout.Image.LocalPath))
_deviceImage = new BitmapImage(Device.Layout.Image);
// Create all the LEDs
foreach (ArtemisLed artemisLed in Device.Leds)
_deviceVisualizerLeds.Add(new DeviceVisualizerLed(artemisLed));
if (!ShowColors)
{
InvalidateMeasure();
return;
}
// Create the opacity drawing group
DrawingGroup opacityDrawingGroup = new();
DrawingContext drawingContext = opacityDrawingGroup.Open();
foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds)
deviceVisualizerLed.RenderOpacityMask(drawingContext);
drawingContext.Close();
// Render the store as a bitmap
DrawingImage drawingImage = new(opacityDrawingGroup);
Image image = new() {Source = drawingImage};
RenderTargetBitmap bitmap = new(
Math.Max(1, (int) (opacityDrawingGroup.Bounds.Width * 2.5)),
Math.Max(1, (int) (opacityDrawingGroup.Bounds.Height * 2.5)),
96,
96,
PixelFormats.Pbgra32
);
image.Arrange(new Rect(0, 0, bitmap.Width, bitmap.Height));
bitmap.Render(image);
bitmap.Freeze();
// Set the bitmap as the opacity mask for the colors backing store
ImageBrush bitmapBrush = new(bitmap);
bitmapBrush.Freeze();
_backingStore.OpacityMask = bitmapBrush;
_backingStore.Children.Clear();
InvalidateMeasure();
}
private void DeviceUpdated(object? sender, EventArgs e)
{
Execute.PostToUIThread(SetupForDevice);
}
private void DevicePropertyChanged(object? sender, PropertyChangedEventArgs e)
{
Execute.PostToUIThread(SetupForDevice);
}
private void Render()
{
DrawingContext drawingContext = _backingStore.Append();
if (HighlightedLeds != null && HighlightedLeds.Any())
{
// Faster on large devices, maybe a bit slower on smaller ones but that's ok
ILookup toHighlight = HighlightedLeds.ToLookup(l => l);
foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds)
deviceVisualizerLed.RenderColor(_backingStore, drawingContext, !toHighlight.Contains(deviceVisualizerLed.Led));
}
else
{
foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds)
deviceVisualizerLed.RenderColor(_backingStore, drawingContext, false);
}
drawingContext.Close();
}
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}