From 5d8a541624852545aa64471137c7057867bda586 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 10 Oct 2021 22:37:37 +0200 Subject: [PATCH] UI - Further sidebar improvements and fixed titlebar --- .../Artemis.UI.Avalonia.Shared.csproj | 3 + .../Controls/DeviceVisualizer.cs | 179 +++++++++++++++--- .../Controls/DeviceVisualizerLed.cs | 159 ++++++++++++++++ .../Events/DataModelInputDynamicEventArgs.cs | 21 ++ .../Events/DataModelInputStaticEventArgs.cs | 20 ++ .../Events/LedClickedEventArgs.cs | 27 +++ .../Events/ProfileConfigurationEventArgs.cs | 32 ++++ .../Events/RenderProfileElementEventArgs.cs | 32 ++++ src/Artemis.UI.Avalonia/App.axaml | 9 +- src/Artemis.UI.Avalonia/App.axaml.cs | 5 +- src/Artemis.UI.Avalonia/MainWindow.axaml | 4 +- src/Artemis.UI.Avalonia/MainWindow.axaml.cs | 2 +- src/Artemis.UI.Avalonia/Ninject/UIModule.cs | 2 + .../Screens/Home/ViewModels/HomeViewModel.cs | 6 +- .../Screens/MainScreenViewModel.cs | 18 +- .../Screens/Root/ViewModels/RootViewModel.cs | 9 +- .../Root/ViewModels/SidebarScreenViewModel.cs | 10 +- .../Root/ViewModels/SidebarViewModel.cs | 48 ++++- .../Screens/Root/Views/RootView.axaml | 19 +- .../Screens/Root/Views/SidebarView.axaml | 55 +++--- .../ViewModels/SurfaceEditorViewModel.cs | 6 +- .../ViewModels/SurfaceEditorViewModel.cs | 6 +- .../Workshop/ViewModels/WorkshopViewModel.cs | 6 +- 23 files changed, 583 insertions(+), 95 deletions(-) create mode 100644 src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizerLed.cs create mode 100644 src/Artemis.UI.Avalonia.Shared/Events/DataModelInputDynamicEventArgs.cs create mode 100644 src/Artemis.UI.Avalonia.Shared/Events/DataModelInputStaticEventArgs.cs create mode 100644 src/Artemis.UI.Avalonia.Shared/Events/LedClickedEventArgs.cs create mode 100644 src/Artemis.UI.Avalonia.Shared/Events/ProfileConfigurationEventArgs.cs create mode 100644 src/Artemis.UI.Avalonia.Shared/Events/RenderProfileElementEventArgs.cs diff --git a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj index 2ddd5386b..a52cde147 100644 --- a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj +++ b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj @@ -21,5 +21,8 @@ ..\..\..\..\Users\Robert\.nuget\packages\material.icons.avalonia\1.0.2\lib\netstandard2.0\Material.Icons.Avalonia.dll + + ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Core.dll + diff --git a/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs b/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs index 841069aed..76c83efc8 100644 --- a/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs +++ b/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs @@ -1,14 +1,17 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.ComponentModel; +using System.IO; using System.Linq; -using System.Text; -using System.Threading.Tasks; 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.Threading; namespace Artemis.UI.Avalonia.Shared.Controls @@ -18,6 +21,121 @@ namespace Artemis.UI.Avalonia.Shared.Controls /// public class DeviceVisualizer : Control { + private readonly DispatcherTimer _timer; + private readonly List _deviceVisualizerLeds; + + private Bitmap? _deviceImage; + private List? _dimmedLeds; + private List? _highlightedLeds; + private ArtemisDevice? _oldDevice; + + /// + public DeviceVisualizer() + { + // Run an update timer at 25 fps + _timer = new DispatcherTimer(DispatcherPriority.Render) {Interval = TimeSpan.FromMilliseconds(40)}; + _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 + Rect measureSize = MeasureDevice(); + double scale = Math.Min(Bounds.Width / measureSize.Width, Bounds.Height / measureSize.Height); + + // Scale the visualization in the desired bounding box + if (Bounds.Width > 0 && Bounds.Height > 0) + drawingContext.PushPostTransform(Matrix.CreateScale(scale, scale)); + + // Apply device rotation + drawingContext.PushPostTransform(Matrix.CreateTranslation(0 - measureSize.Left, 0 - measureSize.Top)); + drawingContext.PushPostTransform(Matrix.CreateRotation(Device.Rotation)); + + // Apply device scale + 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.Size.Width, Device.RgbDevice.Size.Height)); + + foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds) + deviceVisualizerLed.RenderImage(drawingContext); + } + + /// + /// 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 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 /// @@ -67,26 +185,6 @@ namespace Artemis.UI.Avalonia.Shared.Controls #endregion - private readonly DispatcherTimer _timer; - - /// - public DeviceVisualizer() - { - // Run an update timer at 25 fps - _timer = new DispatcherTimer(DispatcherPriority.Render) {Interval = TimeSpan.FromMilliseconds(40)}; - } - - /// - public override void Render(DrawingContext context) - { - base.Render(context); - } - - private void Update() - { - throw new NotImplementedException(); - } - #region Lifetime management /// @@ -105,14 +203,37 @@ namespace Artemis.UI.Avalonia.Shared.Controls base.OnDetachedFromLogicalTree(e); } - #endregion - - #region Event handlers - - private void TimerOnTick(object? sender, EventArgs e) + private void SetupForDevice() { - if (ShowColors && IsVisible && Opacity > 0) - Update(); + _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; + + 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 Bitmap(Device.Layout.Image.AbsolutePath); + + // Create all the LEDs + foreach (ArtemisLed artemisLed in Device.Leds) + _deviceVisualizerLeds.Add(new DeviceVisualizerLed(artemisLed)); + + InvalidateMeasure(); } #endregion diff --git a/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizerLed.cs b/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizerLed.cs new file mode 100644 index 000000000..ae6c6ff5e --- /dev/null +++ b/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizerLed.cs @@ -0,0 +1,159 @@ +using System; +using System.IO; +using Artemis.Core; +using Avalonia; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using RGB.NET.Core; +using Color = Avalonia.Media.Color; +using Point = Avalonia.Point; +using SolidColorBrush = Avalonia.Media.SolidColorBrush; + +namespace Artemis.UI.Avalonia.Shared.Controls +{ + internal class DeviceVisualizerLed + { + private const byte Dimmed = 100; + private const byte NonDimmed = 255; + + public DeviceVisualizerLed(ArtemisLed led) + { + Led = led; + LedRect = new Rect( + Led.RgbLed.Location.X, + Led.RgbLed.Location.Y, + Led.RgbLed.Size.Width, + Led.RgbLed.Size.Height + ); + + if (Led.Layout?.Image != null && File.Exists(Led.Layout.Image.LocalPath)) + LedImage = new Bitmap(Led.Layout.Image.AbsolutePath); + + CreateLedGeometry(); + } + + public ArtemisLed Led { get; } + public Rect LedRect { get; set; } + public Bitmap? LedImage { get; set; } + public Geometry? DisplayGeometry { get; private set; } + + public void RenderImage(DrawingContext drawingContext) + { + if (LedImage == null) + return; + + drawingContext.DrawImage(LedImage, LedRect); + + byte r = Led.RgbLed.Color.GetR(); + byte g = Led.RgbLed.Color.GetG(); + byte b = Led.RgbLed.Color.GetB(); + SolidColorBrush fillBrush = new(new Color(100, r, g, b)); + SolidColorBrush penBrush = new(new Color(255, r, g, b)); + + // Create transparent pixels covering the entire LedRect so the image size matched the LedRect size + // drawingContext.DrawRectangle(new SolidColorBrush(Colors.Transparent), new Pen(new SolidColorBrush(Colors.Transparent), 1), LedRect); + // Translate to the top-left of the LedRect + using DrawingContext.PushedState push = drawingContext.PushPostTransform(Matrix.CreateTranslation(LedRect.X, LedRect.Y)); + // Render the LED geometry + drawingContext.DrawGeometry(fillBrush, new Pen(penBrush) {LineJoin = PenLineJoin.Round}, DisplayGeometry); + } + + public bool HitTest(Point position) + { + if (DisplayGeometry == null) + return false; + + Geometry translatedGeometry = DisplayGeometry.Clone(); + translatedGeometry.Transform = new TranslateTransform(Led.RgbLed.Location.X, Led.RgbLed.Location.Y); + return translatedGeometry.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(0.5, 0.5, Led.RgbLed.Size.Width - 1, Led.RgbLed.Size.Height - 1)); + } + + private void CreateCircleGeometry() + { + DisplayGeometry = new EllipseGeometry(new Rect(0.5, 0.5, Led.RgbLed.Size.Width - 1, Led.RgbLed.Size.Height - 1)); + } + + private void CreateKeyCapGeometry() + { + PathGeometry path = PathGeometry.Parse($"M1,1" + + $"h{Led.RgbLed.Size.Width - 2} a10," + + $"10 0 0 1 10," + + $"10 v{Led.RgbLed.Size.Height - 2} a10," + + $"10 0 0 1 -10," + + $"10 h-{Led.RgbLed.Size.Width - 2} a10," + + $"10 0 0 1 -10," + + $"-10 v-{Led.RgbLed.Size.Height - 2} a10," + + $"10 0 0 1 10,-10 z"); + DisplayGeometry = path; + } + + 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 ScaleTransform(width, height); + geometry = geometry.Clone(); + geometry.Transform = new TranslateTransform(deflateAmount / 2, deflateAmount / 2); + DisplayGeometry = geometry.Clone(); + + // TODO: Figure out wtf was going on here + // if (DisplayGeometry.Bounds.Width > width) + // { + // DisplayGeometry = Geometry.Combine(Geometry.Empty, DisplayGeometry, GeometryCombineMode.Union, new TransformGroup + // { + // Children = new TransformCollection {new ScaleTransform(width / DisplayGeometry.Bounds.Width, 1)} + // }); + // } + // + // if (DisplayGeometry.Bounds.Height > height) + // { + // DisplayGeometry = Geometry.Combine(Geometry.Empty, DisplayGeometry, GeometryCombineMode.Union, new TransformGroup + // { + // Children = new TransformCollection {new ScaleTransform(1, height / DisplayGeometry.Bounds.Height)} + // }); + // } + } + catch (Exception) + { + CreateRectangleGeometry(); + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/Events/DataModelInputDynamicEventArgs.cs b/src/Artemis.UI.Avalonia.Shared/Events/DataModelInputDynamicEventArgs.cs new file mode 100644 index 000000000..596f7a3c2 --- /dev/null +++ b/src/Artemis.UI.Avalonia.Shared/Events/DataModelInputDynamicEventArgs.cs @@ -0,0 +1,21 @@ +using System; +using Artemis.Core; + +namespace Artemis.UI.Avalonia.Shared.Events +{ + /// + /// Provides data about selection events raised by + /// + public class DataModelInputDynamicEventArgs : EventArgs + { + internal DataModelInputDynamicEventArgs(DataModelPath? dataModelPath) + { + DataModelPath = dataModelPath; + } + + /// + /// Gets the data model path that was selected + /// + public DataModelPath? DataModelPath { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/Events/DataModelInputStaticEventArgs.cs b/src/Artemis.UI.Avalonia.Shared/Events/DataModelInputStaticEventArgs.cs new file mode 100644 index 000000000..2224d5021 --- /dev/null +++ b/src/Artemis.UI.Avalonia.Shared/Events/DataModelInputStaticEventArgs.cs @@ -0,0 +1,20 @@ +using System; + +namespace Artemis.UI.Avalonia.Shared.Events +{ + /// + /// Provides data about submit events raised by + /// + public class DataModelInputStaticEventArgs : EventArgs + { + internal DataModelInputStaticEventArgs(object? value) + { + Value = value; + } + + /// + /// The value that was submitted + /// + public object? Value { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/Events/LedClickedEventArgs.cs b/src/Artemis.UI.Avalonia.Shared/Events/LedClickedEventArgs.cs new file mode 100644 index 000000000..e6f763a25 --- /dev/null +++ b/src/Artemis.UI.Avalonia.Shared/Events/LedClickedEventArgs.cs @@ -0,0 +1,27 @@ +using System; +using Artemis.Core; + +namespace Artemis.UI.Avalonia.Shared.Events +{ + /// + /// Provides data on LED click events raised by the device visualizer + /// + public class LedClickedEventArgs : EventArgs + { + internal LedClickedEventArgs(ArtemisDevice device, ArtemisLed led) + { + Device = device; + Led = led; + } + + /// + /// The device that was clicked + /// + public ArtemisDevice Device { get; set; } + + /// + /// The LED that was clicked + /// + public ArtemisLed Led { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/Events/ProfileConfigurationEventArgs.cs b/src/Artemis.UI.Avalonia.Shared/Events/ProfileConfigurationEventArgs.cs new file mode 100644 index 000000000..ce9e24a5d --- /dev/null +++ b/src/Artemis.UI.Avalonia.Shared/Events/ProfileConfigurationEventArgs.cs @@ -0,0 +1,32 @@ +using System; +using Artemis.Core; + +namespace Artemis.UI.Avalonia.Shared.Events +{ + /// + /// Provides data on profile related events raised by the profile editor + /// + public class ProfileConfigurationEventArgs : EventArgs + { + internal ProfileConfigurationEventArgs(ProfileConfiguration? profileConfiguration) + { + ProfileConfiguration = profileConfiguration; + } + + internal ProfileConfigurationEventArgs(ProfileConfiguration? profileConfiguration, ProfileConfiguration? previousProfileConfiguration) + { + ProfileConfiguration = profileConfiguration; + PreviousProfileConfiguration = previousProfileConfiguration; + } + + /// + /// Gets the profile the event was raised for + /// + public ProfileConfiguration? ProfileConfiguration { get; } + + /// + /// If applicable, the previous active profile before the event was raised + /// + public ProfileConfiguration? PreviousProfileConfiguration { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/Events/RenderProfileElementEventArgs.cs b/src/Artemis.UI.Avalonia.Shared/Events/RenderProfileElementEventArgs.cs new file mode 100644 index 000000000..95455d818 --- /dev/null +++ b/src/Artemis.UI.Avalonia.Shared/Events/RenderProfileElementEventArgs.cs @@ -0,0 +1,32 @@ +using System; +using Artemis.Core; + +namespace Artemis.UI.Avalonia.Shared.Events +{ + /// + /// Provides data on profile element related events raised by the profile editor + /// + public class RenderProfileElementEventArgs : EventArgs + { + internal RenderProfileElementEventArgs(RenderProfileElement? renderProfileElement) + { + RenderProfileElement = renderProfileElement; + } + + internal RenderProfileElementEventArgs(RenderProfileElement? renderProfileElement, RenderProfileElement? previousRenderProfileElement) + { + RenderProfileElement = renderProfileElement; + PreviousRenderProfileElement = previousRenderProfileElement; + } + + /// + /// Gets the profile element the event was raised for + /// + public RenderProfileElement? RenderProfileElement { get; } + + /// + /// If applicable, the previous active profile element before the event was raised + /// + public RenderProfileElement? PreviousRenderProfileElement { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/App.axaml b/src/Artemis.UI.Avalonia/App.axaml index bc3f412c4..0daed3338 100644 --- a/src/Artemis.UI.Avalonia/App.axaml +++ b/src/Artemis.UI.Avalonia/App.axaml @@ -11,8 +11,13 @@ - - + + + + + + + diff --git a/src/Artemis.UI.Avalonia/App.axaml.cs b/src/Artemis.UI.Avalonia/App.axaml.cs index de78f841c..345424826 100644 --- a/src/Artemis.UI.Avalonia/App.axaml.cs +++ b/src/Artemis.UI.Avalonia/App.axaml.cs @@ -1,10 +1,10 @@ using Artemis.Core.Ninject; using Artemis.UI.Avalonia.Ninject; -using Artemis.UI.Avalonia.Screens.Main.Views; using Artemis.UI.Avalonia.Screens.Root.ViewModels; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; +using FluentAvalonia.Styling; using Ninject; using Splat.Ninject; @@ -23,10 +23,13 @@ namespace Artemis.UI.Avalonia public override void OnFrameworkInitializationCompleted() { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { desktop.MainWindow = new MainWindow { DataContext = _kernel.Get() }; + AvaloniaLocator.Current.GetService().ForceNativeTitleBarToTheme(desktop.MainWindow, "Dark"); + } base.OnFrameworkInitializationCompleted(); } diff --git a/src/Artemis.UI.Avalonia/MainWindow.axaml b/src/Artemis.UI.Avalonia/MainWindow.axaml index ee8375848..2b5967a0c 100644 --- a/src/Artemis.UI.Avalonia/MainWindow.axaml +++ b/src/Artemis.UI.Avalonia/MainWindow.axaml @@ -4,11 +4,11 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:reactiveUi="http://reactiveui.net" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Avalonia.Screens.Main.Views.MainWindow" + x:Class="Artemis.UI.Avalonia.MainWindow" Icon="/Assets/avalonia-logo.ico" Title="Artemis.UI.Avalonia" + ExtendClientAreaToDecorationsHint="True" TransparencyLevelHint="AcrylicBlur" Background="Transparent" - ExtendClientAreaToDecorationsHint="True" Content="{Binding}"> \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/MainWindow.axaml.cs b/src/Artemis.UI.Avalonia/MainWindow.axaml.cs index be13f759e..d57321c31 100644 --- a/src/Artemis.UI.Avalonia/MainWindow.axaml.cs +++ b/src/Artemis.UI.Avalonia/MainWindow.axaml.cs @@ -3,7 +3,7 @@ using Avalonia; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Artemis.UI.Avalonia.Screens.Main.Views +namespace Artemis.UI.Avalonia { public partial class MainWindow : ReactiveWindow { diff --git a/src/Artemis.UI.Avalonia/Ninject/UIModule.cs b/src/Artemis.UI.Avalonia/Ninject/UIModule.cs index 74f829d80..a934f3b91 100644 --- a/src/Artemis.UI.Avalonia/Ninject/UIModule.cs +++ b/src/Artemis.UI.Avalonia/Ninject/UIModule.cs @@ -3,6 +3,7 @@ using Artemis.UI.Avalonia.Ninject.Factories; using Artemis.UI.Avalonia.Screens; using Ninject.Extensions.Conventions; using Ninject.Modules; +using Ninject.Planning.Bindings.Resolvers; namespace Artemis.UI.Avalonia.Ninject { @@ -13,6 +14,7 @@ namespace Artemis.UI.Avalonia.Ninject if (Kernel == null) throw new ArgumentNullException("Kernel shouldn't be null here."); + Kernel.Components.Add(); Kernel.Bind(x => { diff --git a/src/Artemis.UI.Avalonia/Screens/Home/ViewModels/HomeViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Home/ViewModels/HomeViewModel.cs index 53783ae7b..c91071b45 100644 --- a/src/Artemis.UI.Avalonia/Screens/Home/ViewModels/HomeViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Home/ViewModels/HomeViewModel.cs @@ -1,8 +1,10 @@ -namespace Artemis.UI.Avalonia.Screens.Home.ViewModels +using ReactiveUI; + +namespace Artemis.UI.Avalonia.Screens.Home.ViewModels { public class HomeViewModel : MainScreenViewModel { - public HomeViewModel() + public HomeViewModel(IScreen hostScreens) : base(hostScreens, "home") { DisplayName = "Home"; } diff --git a/src/Artemis.UI.Avalonia/Screens/MainScreenViewModel.cs b/src/Artemis.UI.Avalonia/Screens/MainScreenViewModel.cs index 7ad2e7bcd..466d7b0c0 100644 --- a/src/Artemis.UI.Avalonia/Screens/MainScreenViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/MainScreenViewModel.cs @@ -1,7 +1,19 @@ -namespace Artemis.UI.Avalonia.Screens +using ReactiveUI; + +namespace Artemis.UI.Avalonia.Screens { - public class MainScreenViewModel : ViewModelBase + public abstract class MainScreenViewModel : ViewModelBase, IRoutableViewModel { - + protected MainScreenViewModel(IScreen hostScreen, string urlPathSegment) + { + HostScreen = hostScreen; + UrlPathSegment = urlPathSegment; + } + + /// + public string UrlPathSegment { get; } + + /// + public IScreen HostScreen { get; } } } \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/RootViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/RootViewModel.cs index 2645dc999..376906861 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/RootViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/RootViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Reactive; using Artemis.Core.Services; using ReactiveUI; @@ -10,18 +11,20 @@ namespace Artemis.UI.Avalonia.Screens.Root.ViewModels public RootViewModel(ICoreService coreService, SidebarViewModel sidebarViewModel) { + Router = new RoutingState(); SidebarViewModel = sidebarViewModel; + SidebarViewModel.Router = Router; + _coreService = coreService; _coreService.Initialize(); - Console.WriteLine("test"); } public SidebarViewModel SidebarViewModel { get; } - + /// public ViewModelActivator Activator { get; } = new(); /// - public RoutingState Router { get; } = new(); + public RoutingState Router { get; } } } \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarScreenViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarScreenViewModel.cs index 5d8d7e11a..2b2769587 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarScreenViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarScreenViewModel.cs @@ -1,5 +1,8 @@ -using Material.Icons; +using System; +using System.Reactive.Linq; +using Material.Icons; using Ninject; +using ReactiveUI; namespace Artemis.UI.Avalonia.Screens.Root.ViewModels { @@ -27,5 +30,10 @@ namespace Artemis.UI.Avalonia.Screens.Root.ViewModels public string DisplayName { get; } public abstract MainScreenViewModel CreateInstance(IKernel kernel); + + public bool IsActive(IObservable routerCurrentViewModel) + { + return false; + } } } \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarViewModel.cs index 80a0340f0..a8b63697a 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarViewModel.cs @@ -10,6 +10,7 @@ using Artemis.UI.Avalonia.Screens.Workshop.ViewModels; using Material.Icons; using Ninject; using ReactiveUI; +using RGB.NET.Core; namespace Artemis.UI.Avalonia.Screens.Root.ViewModels { @@ -17,13 +18,18 @@ namespace Artemis.UI.Avalonia.Screens.Root.ViewModels { private readonly IKernel _kernel; private readonly IProfileService _profileService; + private readonly IRgbService _rgbService; private readonly ISidebarVmFactory _sidebarVmFactory; - private SidebarScreenViewModel _selectedSidebarScreen; + private ArtemisDevice? _headerDevice; - public SidebarViewModel(IKernel kernel, IProfileService profileService, ISidebarVmFactory sidebarVmFactory) + private SidebarScreenViewModel _selectedSidebarScreen; + private RoutingState _router; + + public SidebarViewModel(IKernel kernel, IProfileService profileService, IRgbService rgbService, ISidebarVmFactory sidebarVmFactory) { _kernel = kernel; _profileService = profileService; + _rgbService = rgbService; _sidebarVmFactory = sidebarVmFactory; SidebarScreens = new ObservableCollection @@ -33,17 +39,43 @@ namespace Artemis.UI.Avalonia.Screens.Root.ViewModels new SidebarScreenViewModel(MaterialIconKind.Devices, "Surface Editor"), new SidebarScreenViewModel(MaterialIconKind.Cog, "Settings") }; - SelectedSidebarScreen = SidebarScreens.First(); + _selectedSidebarScreen = SidebarScreens.First(); + UpdateProfileCategories(); + UpdateHeaderDevice(); } public ObservableCollection SidebarScreens { get; } public ObservableCollection SidebarCategories { get; } = new(); - + + public ArtemisDevice? HeaderDevice + { + get => _headerDevice; + set => this.RaiseAndSetIfChanged(ref _headerDevice, value); + } + public SidebarScreenViewModel SelectedSidebarScreen { get => _selectedSidebarScreen; - set => this.RaiseAndSetIfChanged(ref _selectedSidebarScreen, value); + set + { + this.RaiseAndSetIfChanged(ref _selectedSidebarScreen, value); + // if (!SelectedSidebarScreen.IsActive(Router.CurrentViewModel)) + // Router.Navigate.Execute(SelectedSidebarScreen.CreateInstance(_kernel)).Subscribe(); + } + } + + public RoutingState Router + { + get => _router; + set => this.RaiseAndSetIfChanged(ref _router, value); + } + + public SidebarCategoryViewModel AddProfileCategoryViewModel(ProfileCategory profileCategory) + { + SidebarCategoryViewModel viewModel = _sidebarVmFactory.SidebarCategoryViewModel(profileCategory); + SidebarCategories.Add(viewModel); + return viewModel; } private void UpdateProfileCategories() @@ -53,11 +85,9 @@ namespace Artemis.UI.Avalonia.Screens.Root.ViewModels AddProfileCategoryViewModel(profileCategory); } - public SidebarCategoryViewModel AddProfileCategoryViewModel(ProfileCategory profileCategory) + private void UpdateHeaderDevice() { - SidebarCategoryViewModel viewModel = _sidebarVmFactory.SidebarCategoryViewModel(profileCategory); - SidebarCategories.Add(viewModel); - return viewModel; + HeaderDevice = _rgbService.Devices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Keyboard && d.Layout is {IsValid: true}); } } } \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml b/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml index 0d4df056b..472555f77 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml @@ -1,10 +1,10 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:reactiveUi="http://reactiveui.net" + mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + x:Class="Artemis.UI.Avalonia.Screens.Root.Views.RootView"> @@ -19,11 +19,10 @@ TintOpacity="1" MaterialOpacity="0.65" /> - + - - - + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarView.axaml b/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarView.axaml index 44e0b35c1..5dc95cbc8 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarView.axaml @@ -4,7 +4,9 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" - mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + xmlns:svg="clr-namespace:Avalonia.Svg.Skia;assembly=Avalonia.Svg.Skia" + xmlns:shared="clr-namespace:Artemis.UI.Avalonia.Shared.Controls;assembly=Artemis.UI.Avalonia.Shared" + mc:Ignorable="d" d:DesignWidth="240" d:DesignHeight="450" x:Class="Artemis.UI.Avalonia.Screens.Root.Views.SidebarView"> @@ -16,40 +18,41 @@ - + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + - - + + - + + + + + Artemis 2 diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/SurfaceEditorViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/SurfaceEditorViewModel.cs index 116b88016..30c9fde1a 100644 --- a/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/SurfaceEditorViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/SurfaceEditorViewModel.cs @@ -1,8 +1,10 @@ -namespace Artemis.UI.Avalonia.Screens.Settings.ViewModels +using ReactiveUI; + +namespace Artemis.UI.Avalonia.Screens.Settings.ViewModels { public class SettingsViewModel : MainScreenViewModel { - public SettingsViewModel() + public SettingsViewModel(IScreen hostScreens) : base(hostScreens, "settings") { DisplayName = "Settings"; } diff --git a/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/ViewModels/SurfaceEditorViewModel.cs b/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/ViewModels/SurfaceEditorViewModel.cs index dfbf164bd..c97d12ed8 100644 --- a/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/ViewModels/SurfaceEditorViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/ViewModels/SurfaceEditorViewModel.cs @@ -1,8 +1,10 @@ -namespace Artemis.UI.Avalonia.Screens.SurfaceEditor.ViewModels +using ReactiveUI; + +namespace Artemis.UI.Avalonia.Screens.SurfaceEditor.ViewModels { public class SurfaceEditorViewModel : MainScreenViewModel { - public SurfaceEditorViewModel() + public SurfaceEditorViewModel(IScreen hostScreens) : base(hostScreens, "surface-editor") { DisplayName = "Surface Editor"; } diff --git a/src/Artemis.UI.Avalonia/Screens/Workshop/ViewModels/WorkshopViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Workshop/ViewModels/WorkshopViewModel.cs index f645047c9..28eb2caef 100644 --- a/src/Artemis.UI.Avalonia/Screens/Workshop/ViewModels/WorkshopViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Workshop/ViewModels/WorkshopViewModel.cs @@ -1,8 +1,10 @@ -namespace Artemis.UI.Avalonia.Screens.Workshop.ViewModels +using ReactiveUI; + +namespace Artemis.UI.Avalonia.Screens.Workshop.ViewModels { public class WorkshopViewModel : MainScreenViewModel { - public WorkshopViewModel() + public WorkshopViewModel(IScreen hostScreens) : base(hostScreens, "workshop") { DisplayName = "Workshop"; }