using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.IO; using System.Linq; using System.Threading.Tasks; using Artemis.Core; using Artemis.UI.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.Shared { /// /// 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); 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)); // 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))); // Apply device scale using DrawingContext.PushedState scalePush = drawingContext.PushPreTransform(Matrix.CreateScale(Device.Scale, Device.Scale)); // 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; lock (_deviceVisualizerLeds) { foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds) deviceVisualizerLed.RenderGeometry(drawingContext, false); } } finally { boundsPush?.Dispose(); } } /// /// Occurs when a LED of the device has been clicked /// public event EventHandler? LedClicked; /// /// Invokes the event /// /// protected virtual void OnLedClicked(LedClickedEventArgs e) { LedClicked?.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)); } 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 /// /// 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 OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { _deviceImage?.Dispose(); _deviceImage = null; if (Device != null) { Device.RgbDevice.PropertyChanged -= DevicePropertyChanged; Device.DeviceUpdated -= DeviceUpdated; } base.OnDetachedFromVisualTree(e); } /// 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?.Dispose(); _deviceImage = null; _highlightedLeds = new List(); _dimmedLeds = new List(); 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; Task.Run(() => { if (device.Layout?.Image == null || !File.Exists(device.Layout.Image.LocalPath)) return; 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); lock (_deviceVisualizerLeds) { foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds) deviceVisualizerLed.DrawBitmap(context); } _deviceImage = renderTargetBitmap; Dispatcher.UIThread.Post(InvalidateMeasure); } catch { // ignored } }); } /// 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 } }