Meta - Replace WPF projects with Avalonia projects

This commit is contained in:
Robert committed 2022-03-27 11:55:26 +02:00
1 parent fcf0376a9a
commit d7f3ee1190
1110 files changed
+10872 -52240

No files matched your search

@@ -0,0 +1,29 @@
<UserControl xmlns="https://github.com/avaloniaui"
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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="1050"
x:Class="Artemis.UI.Screens.Device.DeviceDetectInputView">
<StackPanel Width="500">
<!-- TODO: Replace with Text.Run stuff once available -->
<TextBlock TextWrapping="Wrap">
Press a button/key on your device that is currently showing a yellow color.
</TextBlock>
<avalonia:MaterialIcon Kind="Keyboard"
Width="300"
Height="300"
HorizontalAlignment="Center"
IsVisible="{Binding !IsMouse}" />
<avalonia:MaterialIcon Kind="Mouse"
Width="300"
Height="300"
HorizontalAlignment="Center"
IsVisible="{Binding IsMouse}" />
<TextBlock TextWrapping="Wrap" Margin="0 10">
This will teach Artemis to associate button/key presses with this specific device.
</TextBlock>
</StackPanel>
</UserControl>
@@ -0,0 +1,18 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Device
{
public class DeviceDetectInputView : ReactiveUserControl<DeviceDetectInputViewModel>
{
public DeviceDetectInputView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}
@@ -0,0 +1,60 @@
using System;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.Builders;
using Artemis.UI.Shared.Services.Interfaces;
using ReactiveUI;
using RGB.NET.Core;
namespace Artemis.UI.Screens.Device
{
public class DeviceDetectInputViewModel : ContentDialogViewModelBase
{
private readonly IInputService _inputService;
private readonly INotificationService _notificationService;
private readonly IRgbService _rgbService;
private readonly ListLedGroup _ledGroup;
public DeviceDetectInputViewModel(ArtemisDevice device, IInputService inputService, INotificationService notificationService, IRgbService rgbService)
{
_inputService = inputService;
_notificationService = notificationService;
_rgbService = rgbService;
Device = device;
// Create a LED group way at the top
_ledGroup = new ListLedGroup(_rgbService.Surface, Device.Leds.Select(l => l.RgbLed))
{
Brush = new SolidColorBrush(new Color(255, 255, 0)),
ZIndex = 999
};
this.WhenActivated(disposables =>
{
Observable.FromEventPattern(x => _inputService.DeviceIdentified += x, x => _inputService.DeviceIdentified -= x)
.Subscribe(_ => InputServiceOnDeviceIdentified())
.DisposeWith(disposables);
Disposable.Create(() => _ledGroup.Detach()).DisposeWith(disposables);
});
}
public ArtemisDevice Device { get; }
public bool IsMouse => Device.RgbDevice.DeviceInfo.DeviceType == RGBDeviceType.Mouse;
public bool MadeChanges { get; set; }
private void InputServiceOnDeviceIdentified()
{
_notificationService.CreateNotification()
.WithMessage($"{Device.RgbDevice.DeviceInfo.DeviceName} identified 😁")
.WithSeverity(NotificationSeverity.Success)
.Show();
}
}
}
@@ -0,0 +1,68 @@
<controls1:CoreWindow xmlns="https://github.com/avaloniaui"
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:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared"
xmlns:controls1="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800"
x:Class="Artemis.UI.Screens.Device.DevicePropertiesView"
Icon="/Assets/Images/Logo/application.ico"
Title="Artemis | Device Properties"
Width="1250"
Height="900">
<Grid ColumnDefinitions="*,0,1.5*">
<Grid.Background>
<VisualBrush TileMode="Tile" Stretch="Uniform" DestinationRect="0,0,25,25">
<VisualBrush.Visual>
<Grid Width="25" Height="25" RowDefinitions="*,*" ColumnDefinitions="*,*">
<Rectangle Grid.Row="0" Grid.Column="0" Fill="Black" Opacity="0.15" />
<Rectangle Grid.Row="0" Grid.Column="1" />
<Rectangle Grid.Row="1" Grid.Column="0" />
<Rectangle Grid.Row="1" Grid.Column="1" Fill="Black" Opacity="0.15" />
</Grid>
</VisualBrush.Visual>
</VisualBrush>
</Grid.Background>
<Grid Grid.Column="0" Name="DeviceDisplayGrid">
<!-- No need to provide LEDs to highlight as LEDs are already physically highlighted -->
<controls:DeviceVisualizer Device="{Binding Device}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
ShowColors="True"
Margin="20" />
<StackPanel Orientation="Horizontal"
VerticalAlignment="Bottom"
HorizontalAlignment="Left"
Margin="15"
IsVisible="{Binding Device.Layout.RgbLayout.Author, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<TextBlock Classes="h5" Text="Device layout by " />
<TextBlock Classes="h5" FontWeight="Bold" Text="{Binding Device.Layout.RgbLayout.Author}" />
</StackPanel>
</Grid>
<GridSplitter Grid.Column="1" Width="15" Margin="-15 0 0 0" Background="Transparent" HorizontalAlignment="Stretch" />
<Border Grid.Column="2" Classes="card" CornerRadius="10 0 0 0" Margin="0 10 0 0" Background="#ff323232">
<TabControl Items="{Binding Tabs}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayName}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<ContentControl Content="{Binding}" Margin="0 10 0 0" />
</ScrollViewer>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Border>
<StackPanel Grid.Column="0" Grid.ColumnSpan="3" Classes="notification-container" Name="NotificationContainer" VerticalAlignment="Bottom" HorizontalAlignment="Right"/>
</Grid>
</controls1:CoreWindow>
@@ -0,0 +1,22 @@
using Avalonia;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Device
{
public partial class DevicePropertiesView : ReactiveCoreWindow<DevicePropertiesViewModel>
{
public DevicePropertiesView()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}
@@ -0,0 +1,29 @@
using System.Collections.ObjectModel;
using Artemis.Core;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Shared;
using RGB.NET.Core;
using ArtemisLed = Artemis.Core.ArtemisLed;
namespace Artemis.UI.Screens.Device
{
public class DevicePropertiesViewModel : DialogViewModelBase<object>
{
public DevicePropertiesViewModel(ArtemisDevice device, IDeviceVmFactory deviceVmFactory)
{
Device = device;
SelectedLeds = new ObservableCollection<ArtemisLed>();
Tabs = new ObservableCollection<ActivatableViewModelBase>();
Tabs.Add(deviceVmFactory.DevicePropertiesTabViewModel(device));
Tabs.Add(deviceVmFactory.DeviceInfoTabViewModel(device));
if (Device .DeviceType == RGBDeviceType.Keyboard)
Tabs.Add(deviceVmFactory.InputMappingsTabViewModel(device, SelectedLeds));
Tabs.Add(deviceVmFactory.DeviceLedsTabViewModel(device, SelectedLeds));
}
public ArtemisDevice Device { get; }
public ObservableCollection<ArtemisLed> SelectedLeds { get; }
public ObservableCollection<ActivatableViewModelBase> Tabs { get; }
}
}
@@ -0,0 +1,58 @@
<UserControl xmlns="https://github.com/avaloniaui"
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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:controls1="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Device.DeviceSettingsView">
<Border Classes="card" Padding="0" Width="200" ClipToBounds="True" Margin="5">
<Grid RowDefinitions="140,*,Auto">
<Rectangle Grid.Row="0" Fill="{DynamicResource CheckerboardBrush}"/>
<controls1:DeviceVisualizer VerticalAlignment="Center"
HorizontalAlignment="Center"
Margin="5"
ShowColors="False"
Device="{Binding Device}"
Grid.Row="0" />
<Button Grid.Row="0"
Classes="icon-button icon-button-large"
VerticalAlignment="Bottom"
HorizontalAlignment="Right"
Margin="5"
ToolTip.Tip="Identify"
Command="{Binding IdentifyDevice}">
<avalonia:MaterialIcon Kind="AlarmLight" />
</Button>
<StackPanel Grid.Row="1" Margin="8 24 8 0">
<TextBlock Text="{Binding Device.RgbDevice.DeviceInfo.Model}" />
<StackPanel>
<TextBlock TextWrapping="Wrap" Classes="subtitle" Text="{Binding Device.RgbDevice.DeviceInfo.Manufacturer, Mode=OneWay}" />
<TextBlock TextWrapping="Wrap" Classes="subtitle" Text="{Binding Device.DeviceType, Mode=OneWay}" />
</StackPanel>
</StackPanel>
<Grid Grid.Row="2" Margin="10" ColumnDefinitions="25,*">
<CheckBox IsChecked="{Binding IsDeviceEnabled}" />
<controls:SplitButton Grid.Column="1" Content="Properties" Command="{Binding ViewProperties}" HorizontalAlignment="Right">
<controls:SplitButton.Flyout>
<MenuFlyout Placement="Bottom">
<MenuItem Header="Open plugin directory" Command="{Binding OpenPluginDirectory}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="FolderOpen" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Identity input" Command="{Binding DetectInput}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="GestureDoubleTap" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</controls:SplitButton.Flyout>
</controls:SplitButton>
</Grid>
</Grid>
</Border>
</UserControl>
@@ -0,0 +1,18 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.Device
{
public partial class DeviceSettingsView : UserControl
{
public DeviceSettingsView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}
@@ -0,0 +1,124 @@
using System.Reactive;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.Settings;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.Interfaces;
using Avalonia.Threading;
using Humanizer;
using ReactiveUI;
using RGB.NET.Core;
namespace Artemis.UI.Screens.Device
{
public class DeviceSettingsViewModel : ActivatableViewModelBase
{
private readonly IDeviceService _deviceService;
private readonly DevicesTabViewModel _devicesTabViewModel;
private readonly IDeviceVmFactory _deviceVmFactory;
private readonly IRgbService _rgbService;
private readonly IWindowService _windowService;
private bool _togglingDevice;
public DeviceSettingsViewModel(ArtemisDevice device, DevicesTabViewModel devicesTabViewModel, IDeviceService deviceService, IWindowService windowService, IDeviceVmFactory deviceVmFactory,
IRgbService rgbService)
{
_devicesTabViewModel = devicesTabViewModel;
_deviceService = deviceService;
_windowService = windowService;
_deviceVmFactory = deviceVmFactory;
_rgbService = rgbService;
Device = device;
Type = Device.DeviceType.ToString().Humanize();
Name = Device.RgbDevice.DeviceInfo.Model;
Manufacturer = Device.RgbDevice.DeviceInfo.Manufacturer;
DetectInput = ReactiveCommand.CreateFromTask(ExecuteDetectInput, this.WhenAnyValue(vm => vm.CanDetectInput));
}
public ArtemisDevice Device { get; }
public string Type { get; }
public string Name { get; }
public string Manufacturer { get; }
public bool CanDetectInput => Device.DeviceType is RGBDeviceType.Keyboard or RGBDeviceType.Mouse;
public ReactiveCommand<Unit, Unit> DetectInput { get; }
public bool IsDeviceEnabled
{
get => Device.IsEnabled;
set => Dispatcher.UIThread.InvokeAsync(async () => await UpdateIsDeviceEnabled(value));
}
public bool TogglingDevice
{
get => _togglingDevice;
set => RaiseAndSetIfChanged(ref _togglingDevice, value);
}
public void IdentifyDevice()
{
_deviceService.IdentifyDevice(Device);
}
public void OpenPluginDirectory()
{
Utilities.OpenFolder(Device.DeviceProvider.Plugin.Directory.FullName);
}
private async Task ExecuteDetectInput()
{
if (!CanDetectInput)
return;
await _windowService.CreateContentDialog()
.WithTitle($"{Device.RgbDevice.DeviceInfo.DeviceName} - Detect input")
.WithViewModel<DeviceDetectInputViewModel>(out var viewModel, ("device", Device))
.WithCloseButtonText("Cancel")
.ShowAsync();
if (viewModel.MadeChanges)
_rgbService.SaveDevice(Device);
}
public async Task ViewProperties()
{
await _windowService.ShowDialogAsync(_deviceVmFactory.DevicePropertiesViewModel(Device));
}
private async Task UpdateIsDeviceEnabled(bool value)
{
if (TogglingDevice)
return;
try
{
TogglingDevice = true;
if (!value)
value = !await _devicesTabViewModel.ShowDeviceDisableDialog();
if (value)
_rgbService.EnableDevice(Device);
else
_rgbService.DisableDevice(Device);
this.RaisePropertyChanged(nameof(IsDeviceEnabled));
SaveDevice();
}
finally
{
TogglingDevice = false;
}
}
private void SaveDevice()
{
_rgbService.SaveDevice(Device);
}
}
}
@@ -0,0 +1,85 @@
<UserControl xmlns="https://github.com/avaloniaui"
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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Device.DeviceInfoTabView">
<Grid RowDefinitions="Auto,*" ColumnDefinitions="*,*" Margin="-5">
<!-- First row -->
<Border Classes="card" Grid.Column="0" Grid.Row="0" Margin="5">
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Top">
<TextBlock FontWeight="Bold">Device name</TextBlock>
<TextBlock TextWrapping="Wrap" Text="{Binding Device.RgbDevice.DeviceInfo.DeviceName}" />
<Separator Classes="card-separator" />
<TextBlock FontWeight="Bold">Manufacturer</TextBlock>
<TextBlock TextWrapping="Wrap" Text="{Binding Device.RgbDevice.DeviceInfo.Manufacturer}" />
<Separator Classes="card-separator" />
<TextBlock FontWeight="Bold">Device type</TextBlock>
<TextBlock TextWrapping="Wrap" Text="{Binding Device.DeviceType}" />
<Separator Classes="card-separator" IsVisible="{Binding IsKeyboard}" />
<StackPanel IsVisible="{Binding IsKeyboard}">
<TextBlock FontWeight="Bold">Physical layout</TextBlock>
<TextBlock TextWrapping="Wrap" Text="{Binding Device.PhysicalLayout}" />
</StackPanel>
</StackPanel>
</Border>
<Border Classes="card" Grid.Row="0" Grid.Column="1" Margin="5">
<StackPanel VerticalAlignment="Top">
<TextBlock FontWeight="Bold">Size (1px = 1mm)</TextBlock>
<TextBlock TextWrapping="Wrap" Text="{Binding Device.RgbDevice.Size}" />
<Separator Classes="card-separator" />
<TextBlock FontWeight="Bold">Location (1px = 1mm)</TextBlock>
<TextBlock TextWrapping="Wrap" Text="{Binding Device.RgbDevice.Location}" />
<Separator Classes="card-separator" />
<TextBlock FontWeight="Bold">Rotation (degrees)</TextBlock>
<TextBlock TextWrapping="Wrap" Text="{Binding Device.RgbDevice.Rotation.Degrees}" />
<Separator Classes="card-separator" IsVisible="{Binding IsKeyboard}" />
<StackPanel IsVisible="{Binding IsKeyboard}">
<TextBlock FontWeight="Bold">Logical layout</TextBlock>
<TextBlock TextWrapping="Wrap" Text="{Binding Device.LogicalLayout}" />
</StackPanel>
</StackPanel>
</Border>
<!-- Second row -->
<Border Classes="card" Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="2" Margin="5">
<StackPanel>
<Grid ColumnDefinitions="*,Auto">
<TextBlock FontWeight="Bold">Default layout file path</TextBlock>
<Button Grid.Column="1"
Classes="icon-button"
ToolTip.Tip="Copy path to clipboard"
Command="{Binding CopyToClipboard}"
CommandParameter="{Binding DefaultLayoutPath}">
<avalonia:MaterialIcon Kind="ContentCopy" Width="18" Height="18" />
</Button>
</Grid>
<TextBlock
TextWrapping="Wrap"
Text="{Binding DefaultLayoutPath}" />
<Separator Classes="card-separator" />
<Grid ColumnDefinitions="*,Auto">
<TextBlock FontWeight="Bold">Image file path</TextBlock>
<Button Grid.Column="1"
Classes="icon-button"
ToolTip.Tip="Copy path to clipboard"
Command="{Binding CopyToClipboard}"
CommandParameter="{Binding Device.Layout.Image.LocalPath}">
<avalonia:MaterialIcon Kind="ContentCopy" Width="18" Height="18" />
</Button>
</Grid>
<TextBlock TextWrapping="Wrap" Text="{Binding Device.Layout.Image.LocalPath}" />
</StackPanel>
</Border>
</Grid>
</UserControl>
@@ -0,0 +1,18 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.Device
{
public partial class DeviceInfoTabView : UserControl
{
public DeviceInfoTabView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}
@@ -0,0 +1,38 @@
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.Interfaces;
using Avalonia;
using RGB.NET.Core;
namespace Artemis.UI.Screens.Device
{
public class DeviceInfoTabViewModel : ActivatableViewModelBase
{
private readonly INotificationService _notificationService;
public DeviceInfoTabViewModel(ArtemisDevice device, INotificationService notificationService)
{
_notificationService = notificationService;
Device = device;
DisplayName = "Info";
DefaultLayoutPath = Device.DeviceProvider.LoadLayout(Device).FilePath;
}
public bool IsKeyboard => Device.DeviceType == RGBDeviceType.Keyboard;
public ArtemisDevice Device { get; }
public string DefaultLayoutPath { get; }
public async Task CopyToClipboard(string content)
{
if (Application.Current?.Clipboard == null)
return;
await Application.Current.Clipboard.SetTextAsync(content);
_notificationService.CreateNotification().WithMessage("Copied path to clipboard.").Show();
}
}
}
@@ -0,0 +1,8 @@
<UserControl xmlns="https://github.com/avaloniaui"
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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Device.DeviceLedsTabView">
Welcome to Avalonia!
</UserControl>
@@ -0,0 +1,18 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.Device
{
public partial class DeviceLedsTabView : UserControl
{
public DeviceLedsTabView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}
@@ -0,0 +1,82 @@
using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Reactive.Disposables;
using Artemis.Core;
using Artemis.UI.Shared;
using DynamicData.Binding;
using ReactiveUI;
namespace Artemis.UI.Screens.Device
{
public class DeviceLedsTabViewModel : ActivatableViewModelBase
{
private readonly ObservableCollection<ArtemisLed> _selectedLeds;
public DeviceLedsTabViewModel(ArtemisDevice device, ObservableCollection<ArtemisLed> selectedLeds)
{
_selectedLeds = selectedLeds;
Device = device;
DisplayName = "LEDs";
LedViewModels = new ObservableCollection<DeviceLedsTabLedViewModel>(Device.Leds.Select(l => new DeviceLedsTabLedViewModel(l, _selectedLeds)));
this.WhenActivated(disposables => _selectedLeds.ToObservableChangeSet().Subscribe(_ => UpdateSelectedLeds()).DisposeWith(disposables));
}
public ArtemisDevice Device { get; }
public ObservableCollection<DeviceLedsTabLedViewModel> LedViewModels { get; }
private void SelectedLedsOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
UpdateSelectedLeds();
}
private void UpdateSelectedLeds()
{
foreach (DeviceLedsTabLedViewModel deviceLedsTabLedViewModel in LedViewModels)
deviceLedsTabLedViewModel.Update();
}
}
public class DeviceLedsTabLedViewModel : ViewModelBase
{
private readonly ObservableCollection<ArtemisLed> _selectedLeds;
private bool _isSelected;
public DeviceLedsTabLedViewModel(ArtemisLed artemisLed, ObservableCollection<ArtemisLed> selectedLeds)
{
_selectedLeds = selectedLeds;
ArtemisLed = artemisLed;
Update();
}
public ArtemisLed ArtemisLed { get; }
public bool IsSelected
{
get => _isSelected;
set
{
if (!RaiseAndSetIfChanged(ref _isSelected, value))
return;
Apply();
}
}
public void Update()
{
IsSelected = _selectedLeds.Contains(ArtemisLed);
}
public void Apply()
{
if (IsSelected && !_selectedLeds.Contains(ArtemisLed))
_selectedLeds.Add(ArtemisLed);
else if (!IsSelected && _selectedLeds.Contains(ArtemisLed))
_selectedLeds.Remove(ArtemisLed);
}
}
}
@@ -0,0 +1,197 @@
<UserControl xmlns="https://github.com/avaloniaui"
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:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
xmlns:device="clr-namespace:Artemis.UI.Screens.Device"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="1200"
x:Class="Artemis.UI.Screens.Device.DevicePropertiesTabView">
<UserControl.Resources>
<converters:SKColorToColorConverter x:Key="SKColorToColorConverter" />
</UserControl.Resources>
<Design.DataContext>
<device:DevicePropertiesTabViewModel />
</Design.DataContext>
<!-- Body -->
<Grid RowDefinitions="*,Auto">
<StackPanel Grid.Row="0">
<!-- Layout -->
<TextBlock Classes="h4">
Categories
</TextBlock>
<TextBlock TextWrapping="Wrap">
Artemis uses categories to determine where the layers of imported profiles are applied to.
</TextBlock>
<TextBlock TextWrapping="Wrap">
You can hover over a category for a more detailed description.
</TextBlock>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding HasDeskCategory}"
ToolTip.Tip="A device acting as desk ornamentation such as a LED strip"
Content="Desk" />
<CheckBox IsChecked="{Binding HasMonitorCategory}"
ToolTip.Tip="A device attached to the monitor such as ambilight LEDs"
Content="Monitor" />
<CheckBox ToolTip.Tip="A device inside your computer case"
IsChecked="{Binding HasCaseCategory}"
Content="Case" />
<CheckBox IsChecked="{Binding HasRoomCategory}"
ToolTip.Tip="A device elsewhere in the room"
Content="Room" />
<CheckBox IsChecked="{Binding HasPeripheralsCategory}"
ToolTip.Tip="A peripheral such as a mouse or keyboard"
Content="Peripheral" />
</StackPanel>
<Grid Margin="0 25" ColumnDefinitions="*,40,*">
<Grid Grid.Column="0" ColumnDefinitions="*,Auto,Auto" RowDefinitions="*,*,*,*,*">
<TextBlock Grid.Row="0" Grid.ColumnSpan="3" Classes="h4">
Surface properties
</TextBlock>
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center">X-coordinate</TextBlock>
<NumericUpDown Grid.Row="1"
Grid.Column="1"
Margin="10 0"
VerticalAlignment="Center"
Value="{Binding X}" />
<TextBlock Grid.Row="1" Grid.Column="2" VerticalAlignment="Center">mm</TextBlock>
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center">Y-coordinate</TextBlock>
<NumericUpDown Grid.Row="2"
Grid.Column="1"
Margin="10 0"
VerticalAlignment="Center"
Value="{Binding Y}" />
<TextBlock Grid.Row="2" Grid.Column="2" VerticalAlignment="Center">mm</TextBlock>
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center">Scale</TextBlock>
<NumericUpDown Grid.Row="3"
Grid.Column="1"
Margin="10 0"
VerticalAlignment="Center"
Value="{Binding Scale}" />
<TextBlock Grid.Row="3" Grid.Column="2" VerticalAlignment="Center">times</TextBlock>
<TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center">Rotation</TextBlock>
<NumericUpDown Grid.Row="4"
Grid.Column="1"
Margin="10 0"
VerticalAlignment="Center"
Value="{Binding Rotation}" />
<TextBlock Grid.Row="4" Grid.Column="2" VerticalAlignment="Center">deg</TextBlock>
</Grid>
<Rectangle Grid.Column="1" VerticalAlignment="Stretch" Width="1" Fill="{DynamicResource ButtonBorderBrush}" Margin="0 0 0 5" />
<StackPanel Grid.Column="2">
<TextBlock Classes="h4">
Color calibration
</TextBlock>
<TextBlock TextWrapping="Wrap">
Use the sliders below to adjust the colors of your device so that it matches your other devices.
</TextBlock>
<Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="*,*,*,*">
<Label Grid.Row="0" Grid.Column="0" Content="R" VerticalAlignment="Center" />
<Slider Grid.Row="0" Grid.Column="1" Minimum="0" Maximum="200" Value="{Binding RedScale}" Margin="10 0" VerticalAlignment="Center" />
<NumericUpDown Grid.Row="0"
Grid.Column="2"
VerticalAlignment="Center"
Width="65"
Value="{Binding RedScale}"
ShowButtonSpinner="False"
FormatString="{}{0:0.0}"
Minimum="0"
Maximum="200"
ClipValueToMinMax="True" />
<Label Grid.Row="1" Grid.Column="0" Content="G" VerticalAlignment="Center" />
<Slider Grid.Row="1" Grid.Column="1" Minimum="0" Maximum="200" Value="{Binding GreenScale}" Margin="10 0" VerticalAlignment="Center" />
<NumericUpDown Grid.Row="1"
Grid.Column="2"
VerticalAlignment="Center"
Width="65"
Value="{Binding GreenScale}"
ShowButtonSpinner="False"
FormatString="{}{0:0.0}"
Minimum="0"
Maximum="200"
ClipValueToMinMax="True" />
<Label Grid.Row="2" Grid.Column="0" Content="B" VerticalAlignment="Center" />
<Slider Grid.Row="2" Grid.Column="1" Minimum="0" Maximum="200" Value="{Binding BlueScale}" Margin="10 0" Ticks="100" VerticalAlignment="Center" />
<NumericUpDown Grid.Row="2"
Grid.Column="2"
VerticalAlignment="Center"
Width="65"
Value="{Binding BlueScale}"
ShowButtonSpinner="False"
FormatString="{}{0:0.0}"
Minimum="0"
Maximum="200"
ClipValueToMinMax="True" />
<CheckBox Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2" IsChecked="{Binding DisplayOnDevices}" Content="Show preview" VerticalAlignment="Center" />
<controls:ColorPickerButton Grid.Row="3" Grid.Column="2" VerticalAlignment="Center" HorizontalAlignment="Right" Color="{Binding CurrentColor, Converter={StaticResource SKColorToColorConverter}}" />
</Grid>
</StackPanel>
</Grid>
<!-- Layout -->
<TextBlock Classes="h4">
Layout
</TextBlock>
<TextBlock TextWrapping="Wrap">
The device layout is used to determine the position of LEDs and to create the visual representation of the device you see on the left side of this window.
</TextBlock>
<CheckBox Margin="0 0 0 5" IsChecked="{Binding Device.DisableDefaultLayout}">
<CheckBox.Content>
<StackPanel Orientation="Horizontal">
<TextBlock>Don't load default layout</TextBlock>
<avalonia:MaterialIcon Kind="HelpCircle"
Margin="5 0"
ToolTip.Tip="With this enabled Artemis will not load a layout for this device unless you specifically provide one." />
</StackPanel>
</CheckBox.Content>
</CheckBox>
<Grid>
<TextBox Text="{Binding Device.CustomLayoutPath}"
Watermark="Custom layout path"
UseFloatingWatermark="True"
IsReadOnly="True"
PointerReleased="InputElement_OnPointerReleased"
Padding="10 5 36 6"/>
<Button Classes="AppBarButton" HorizontalAlignment="Right" Command="{Binding ClearCustomLayout}" Padding="6" Margin="5 0">
<avalonia:MaterialIcon Kind="CloseCircle" />
</Button>
</Grid>
<controls:HyperlinkButton NavigateUri="https://wiki.artemis-rgb.com/en/guides/developer/layouts" Margin="0 20" HorizontalAlignment="Right">
Learn more about layouts on the wiki
</controls:HyperlinkButton>
</StackPanel>
<!-- Buttons -->
<Grid Grid.Row="1" ColumnDefinitions="Auto,*">
<Button Grid.Column="0"
Classes="AppBarButton"
Command="{Binding SelectPhysicalLayout}"
ToolTip.Tip="Restart device setup, allowing you to select a new physical and logical layout">
Restart setup
</Button>
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Classes="AppBarButton" IsCancel="True" Command="{Binding Reset}" Margin="0 0 5 0">Reset</Button>
<Button IsDefault="True" Command="{Binding Apply}">Apply</Button>
</StackPanel>
</Grid>
</Grid>
</UserControl>
@@ -0,0 +1,24 @@
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Device
{
public partial class DevicePropertiesTabView : ReactiveUserControl<DevicePropertiesTabViewModel>
{
public DevicePropertiesTabView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
ViewModel?.BrowseCustomLayout();
}
}
}
@@ -0,0 +1,278 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reactive.Disposables;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.Builders;
using Artemis.UI.Shared.Services.Interfaces;
using ReactiveUI;
using SkiaSharp;
namespace Artemis.UI.Screens.Device
{
public class DevicePropertiesTabViewModel : ActivatableViewModelBase
{
private readonly List<DeviceCategory> _categories;
private readonly ICoreService _coreService;
private readonly float _initialBlueScale;
private readonly float _initialGreenScale;
private readonly float _initialRedScale;
private readonly INotificationService _notificationService;
private readonly IRgbService _rgbService;
private readonly IWindowService _windowService;
private float _blueScale;
private SKColor _currentColor;
private bool _displayOnDevices;
private float _greenScale;
private float _redScale;
private int _rotation;
private float _scale;
private int _x;
private int _y;
#pragma warning disable CS8618 // Design-time constructor
public DevicePropertiesTabViewModel()
{
}
#pragma warning restore CS8618
public DevicePropertiesTabViewModel(ArtemisDevice device, ICoreService coreService, IRgbService rgbService, IWindowService windowService, INotificationService notificationService)
{
_coreService = coreService;
_rgbService = rgbService;
_windowService = windowService;
_notificationService = notificationService;
_categories = new List<DeviceCategory>(device.Categories);
Device = device;
DisplayName = "Properties";
X = (int) Device.X;
Y = (int) Device.Y;
Scale = Device.Scale;
Rotation = (int) Device.Rotation;
RedScale = Device.RedScale * 100f;
GreenScale = Device.GreenScale * 100f;
BlueScale = Device.BlueScale * 100f;
CurrentColor = SKColors.White;
// We need to store the initial values to be able to restore them when the user clicks "Cancel"
_initialRedScale = Device.RedScale;
_initialGreenScale = Device.GreenScale;
_initialBlueScale = Device.BlueScale;
this.WhenAnyValue(x => x.RedScale, x => x.GreenScale, x => x.BlueScale).Subscribe(_ => ApplyScaling());
this.WhenActivated(d =>
{
Device.PropertyChanged += DeviceOnPropertyChanged;
_coreService.FrameRendering += OnFrameRendering;
Disposable.Create(() =>
{
_coreService.FrameRendering -= OnFrameRendering;
Device.PropertyChanged -= DeviceOnPropertyChanged;
}).DisposeWith(d);
});
}
public ArtemisDevice Device { get; }
public int X
{
get => _x;
set => RaiseAndSetIfChanged(ref _x, value);
}
public int Y
{
get => _y;
set => RaiseAndSetIfChanged(ref _y, value);
}
public float Scale
{
get => _scale;
set => RaiseAndSetIfChanged(ref _scale, value);
}
public int Rotation
{
get => _rotation;
set => RaiseAndSetIfChanged(ref _rotation, value);
}
public float RedScale
{
get => _redScale;
set => RaiseAndSetIfChanged(ref _redScale, value);
}
public float GreenScale
{
get => _greenScale;
set => RaiseAndSetIfChanged(ref _greenScale, value);
}
public float BlueScale
{
get => _blueScale;
set => RaiseAndSetIfChanged(ref _blueScale, value);
}
public SKColor CurrentColor
{
get => _currentColor;
set => RaiseAndSetIfChanged(ref _currentColor, value);
}
public bool DisplayOnDevices
{
get => _displayOnDevices;
set => RaiseAndSetIfChanged(ref _displayOnDevices, value);
}
// This solution won't scale well but I don't expect there to be many more categories.
// If for some reason there will be, dynamically creating a view model per category may be more appropriate
public bool HasDeskCategory
{
get => GetCategory(DeviceCategory.Desk);
set => SetCategory(DeviceCategory.Desk, value);
}
public bool HasMonitorCategory
{
get => GetCategory(DeviceCategory.Monitor);
set => SetCategory(DeviceCategory.Monitor, value);
}
public bool HasCaseCategory
{
get => GetCategory(DeviceCategory.Case);
set => SetCategory(DeviceCategory.Case, value);
}
public bool HasRoomCategory
{
get => GetCategory(DeviceCategory.Room);
set => SetCategory(DeviceCategory.Room, value);
}
public bool HasPeripheralsCategory
{
get => GetCategory(DeviceCategory.Peripherals);
set => SetCategory(DeviceCategory.Peripherals, value);
}
public void ApplyScaling()
{
Device.RedScale = RedScale / 100f;
Device.GreenScale = GreenScale / 100f;
Device.BlueScale = BlueScale / 100f;
_rgbService.FlushLeds = true;
}
public void ClearCustomLayout()
{
Device.CustomLayoutPath = null;
_notificationService.CreateNotification()
.WithMessage("Cleared imported layout.")
.WithSeverity(NotificationSeverity.Informational);
}
public async Task BrowseCustomLayout()
{
string[]? files = await _windowService.CreateOpenFileDialog()
.WithTitle("Select device layout file")
.HavingFilter(f => f.WithName("Layout files").WithExtension("xml"))
.ShowAsync();
if (files?.Length > 0)
{
Device.CustomLayoutPath = files[0];
_notificationService.CreateNotification()
.WithTitle("Imported layout")
.WithMessage($"File loaded from {files[0]}")
.WithSeverity(NotificationSeverity.Informational);
}
}
public async Task SelectPhysicalLayout()
{
// await _windowService.CreateContentDialog()
// .WithTitle("Select layout")
// .WithViewModel<DeviceLayoutDialogViewModel>(("device", Device))
// .ShowAsync();
}
public async Task Apply()
{
// TODO: Validation
_coreService.ProfileRenderingDisabled = true;
await Task.Delay(100);
Device.X = X;
Device.Y = Y;
Device.Scale = Scale;
Device.Rotation = Rotation;
Device.RedScale = RedScale / 100f;
Device.GreenScale = GreenScale / 100f;
Device.BlueScale = BlueScale / 100f;
Device.Categories.Clear();
foreach (DeviceCategory deviceCategory in _categories)
Device.Categories.Add(deviceCategory);
_rgbService.SaveDevice(Device);
_coreService.ProfileRenderingDisabled = false;
}
public void Reset()
{
HasDeskCategory = Device.Categories.Contains(DeviceCategory.Desk);
HasMonitorCategory = Device.Categories.Contains(DeviceCategory.Monitor);
HasCaseCategory = Device.Categories.Contains(DeviceCategory.Case);
HasRoomCategory = Device.Categories.Contains(DeviceCategory.Room);
HasPeripheralsCategory = Device.Categories.Contains(DeviceCategory.Peripherals);
Device.RedScale = _initialRedScale;
Device.GreenScale = _initialGreenScale;
Device.BlueScale = _initialBlueScale;
}
private bool GetCategory(DeviceCategory category)
{
return _categories.Contains(category);
}
private void SetCategory(DeviceCategory category, bool value)
{
if (value && !_categories.Contains(category))
_categories.Add(category);
else
_categories.Remove(category);
this.RaisePropertyChanged($"Has{category}Category");
}
private void OnFrameRendering(object? sender, FrameRenderingEventArgs e)
{
if (!_displayOnDevices)
return;
using SKPaint overlayPaint = new() {Color = CurrentColor};
e.Canvas.DrawRect(0, 0, e.Canvas.LocalClipBounds.Width, e.Canvas.LocalClipBounds.Height, overlayPaint);
}
private void DeviceOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Device.CustomLayoutPath) || e.PropertyName == nameof(Device.DisableDefaultLayout))
Task.Run(() => _rgbService.ApplyBestDeviceLayout(Device));
}
}
}
@@ -0,0 +1,8 @@
<UserControl xmlns="https://github.com/avaloniaui"
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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Device.InputMappingsTabView">
Welcome to Avalonia!
</UserControl>
@@ -0,0 +1,18 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.Device
{
public partial class InputMappingsTabView : UserControl
{
public InputMappingsTabView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}
@@ -0,0 +1,108 @@
using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Exceptions;
using Artemis.UI.Shared;
using ReactiveUI;
using RGB.NET.Core;
namespace Artemis.UI.Screens.Device
{
public class InputMappingsTabViewModel : ActivatableViewModelBase
{
private readonly IRgbService _rgbService;
private readonly IInputService _inputService;
private readonly ObservableCollection<ArtemisLed> _selectedLeds;
private ArtemisLed? _selectedLed;
public InputMappingsTabViewModel(ArtemisDevice device, ObservableCollection<ArtemisLed> selectedLeds, IRgbService rgbService, IInputService inputService)
{
if (device.DeviceType != RGBDeviceType.Keyboard)
throw new ArtemisUIException("The input mappings tab only supports keyboards");
_rgbService = rgbService;
_inputService = inputService;
_selectedLeds = selectedLeds;
Device = device;
DisplayName = "Input Mappings";
InputMappings = new ObservableCollection<(ArtemisLed, ArtemisLed)>();
}
public ArtemisDevice Device { get; }
public ArtemisLed? SelectedLed
{
get => _selectedLed;
set => RaiseAndSetIfChanged(ref _selectedLed, value);
}
public ObservableCollection<(ArtemisLed, ArtemisLed)> InputMappings { get; }
public void DeleteMapping(Tuple<ArtemisLed, ArtemisLed> inputMapping)
{
Device.InputMappings.Remove(inputMapping.Item1);
UpdateInputMappings();
}
private void InputServiceOnKeyboardKeyUp(object sender, ArtemisKeyboardKeyEventArgs e)
{
if (SelectedLed == null || e.Led == null)
return;
// Locate the original LED the same way the InputService did it, but supply false to Device.GetLed
bool foundLedId = InputKeyUtilities.KeyboardKeyLedIdMap.TryGetValue(e.Key, out LedId ledId);
if (!foundLedId)
return;
ArtemisLed? artemisLed = Device.GetLed(ledId, false);
if (artemisLed == null)
return;
// Apply the new LED mapping
Device.InputMappings[SelectedLed] = artemisLed;
_rgbService.SaveDevice(Device);
// ((DeviceDialogViewModel) Parent).SelectedLeds.Clear();
UpdateInputMappings();
}
private void UpdateInputMappings()
{
if (InputMappings.Any())
InputMappings.Clear();
// InputMappings.AddRange(Device.InputMappings.Select(m => new Tuple<ArtemisLed, ArtemisLed>(m.Key, m.Value)));
}
private void SelectedLedsOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// SelectedLed = ((DeviceDialogViewModel) Parent).SelectedLeds.FirstOrDefault();
}
//
// #region Overrides of Screen
//
// /// <inheritdoc />
// protected override void OnActivate()
// {
// UpdateInputMappings();
// _inputService.KeyboardKeyUp += InputServiceOnKeyboardKeyUp;
// ((DeviceDialogViewModel) Parent).SelectedLeds.CollectionChanged += SelectedLedsOnCollectionChanged;
//
// base.OnActivate();
// }
//
// /// <inheritdoc />
// protected override void OnDeactivate()
// {
// InputMappings.Clear();
// _inputService.KeyboardKeyUp -= InputServiceOnKeyboardKeyUp;
// ((DeviceDialogViewModel) Parent).SelectedLeds.CollectionChanged -= SelectedLedsOnCollectionChanged;
// base.OnDeactivate();
// }
//
// #endregion
}
}