1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Added notifications and dialogs

This commit is contained in:
Robert 2021-11-03 21:07:03 +01:00
parent f98e398bc5
commit 4c836fb505
19 changed files with 1052 additions and 12 deletions

View File

@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Avalonia.Controls;
using FluentAvalonia.UI.Controls;
using Ninject;
using Ninject.Parameters;
namespace Artemis.UI.Avalonia.Shared.Services.Builders
{
public class ContentDialogBuilder
{
private readonly ContentDialog _contentDialog;
private readonly IKernel _kernel;
private readonly Window _parent;
internal ContentDialogBuilder(IKernel kernel, Window parent)
{
_kernel = kernel;
_parent = parent;
_contentDialog = new ContentDialog
{
CloseButtonText = "CLose"
};
}
public ContentDialogBuilder WithTitle(string? title)
{
_contentDialog.Title = title;
return this;
}
public ContentDialogBuilder WithContent(string? content)
{
_contentDialog.Content = content;
return this;
}
public ContentDialogBuilder WithDefaultButton(ContentDialogButton defaultButton)
{
_contentDialog.DefaultButton = defaultButton;
return this;
}
public ContentDialogBuilder HavingPrimaryButton(Action<ContentDialogButtonBuilder> configure)
{
ContentDialogButtonBuilder builder = new();
configure(builder);
_contentDialog.IsPrimaryButtonEnabled = true;
_contentDialog.PrimaryButtonText = builder.Text;
_contentDialog.PrimaryButtonCommand = builder.Command;
_contentDialog.PrimaryButtonCommandParameter = builder.CommandParameter;
return this;
}
public ContentDialogBuilder HavingSecondaryButton(Action<ContentDialogButtonBuilder> configure)
{
ContentDialogButtonBuilder builder = new();
configure(builder);
_contentDialog.IsSecondaryButtonEnabled = true;
_contentDialog.SecondaryButtonText = builder.Text;
_contentDialog.SecondaryButtonCommand = builder.Command;
_contentDialog.SecondaryButtonCommandParameter = builder.CommandParameter;
return this;
}
public ContentDialogBuilder WithCloseButtonText(string? text)
{
_contentDialog.CloseButtonText = text;
return this;
}
public ContentDialogBuilder WithViewModel<T>(params (string name, object value)[] parameters)
{
if (parameters.Length != 0)
{
IParameter[] paramsArray = parameters.Select(kv => new ConstructorArgument(kv.name, kv.value)).Cast<IParameter>().ToArray();
_contentDialog.Content = _kernel.Get<T>(paramsArray);
}
else
_contentDialog.Content = _kernel.Get<T>();
return this;
}
public async Task<ContentDialogResult> ShowAsync()
{
if (_parent.Content is not Panel panel)
return ContentDialogResult.None;
try
{
panel.Children.Add(_contentDialog);
return await _contentDialog.ShowAsync();
}
finally
{
panel.Children.Remove(_contentDialog);
}
}
}
public class ContentDialogButtonBuilder
{
internal ContentDialogButtonBuilder()
{
}
internal string? Text { get; set; }
internal ICommand? Command { get; set; }
internal object? CommandParameter { get; set; }
public ContentDialogButtonBuilder WithText(string? text)
{
Text = text;
return this;
}
public ContentDialogButtonBuilder WithCommand(ICommand? command)
{
Command = command;
return this;
}
public ContentDialogButtonBuilder WithCommandParameter(object? commandParameter)
{
CommandParameter = commandParameter;
return this;
}
}
}

View File

@ -0,0 +1,40 @@
using Avalonia.Controls;
namespace Artemis.UI.Avalonia.Shared.Services.Builders
{
/// <summary>
/// Represents a builder that can create a <see cref="FileDialogFilter" />.
/// </summary>
public class FileDialogFilterBuilder
{
private readonly FileDialogFilter _filter;
internal FileDialogFilterBuilder()
{
_filter = new FileDialogFilter();
}
/// <summary>
/// Sets the name of the filter
/// </summary>
public FileDialogFilterBuilder WithName(string name)
{
_filter.Name = name;
return this;
}
/// <summary>
/// Adds the provided extension to the filter
/// </summary>
public FileDialogFilterBuilder WithExtension(string extension)
{
_filter.Extensions.Add(extension);
return this;
}
internal FileDialogFilter Build()
{
return _filter;
}
}
}

View File

@ -0,0 +1,81 @@
using System;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
namespace Artemis.UI.Avalonia.Shared.Services.Builders
{
public class NotificationBuilder
{
private readonly InfoBar _infoBar;
private readonly Window _parent;
private TimeSpan _timeout = TimeSpan.FromSeconds(3);
public NotificationBuilder(Window parent)
{
_parent = parent;
_infoBar = new InfoBar {Classes = Classes.Parse("notification-info-bar")};
}
public NotificationBuilder WithTitle(string? title)
{
_infoBar.Title = title;
return this;
}
public NotificationBuilder WithMessage(string? content)
{
_infoBar.Message = content;
return this;
}
public NotificationBuilder WithTimeout(TimeSpan timeout)
{
_timeout = timeout;
return this;
}
public NotificationBuilder WithSeverity(NotificationSeverity severity)
{
_infoBar.Severity = (InfoBarSeverity) severity;
return this;
}
public void Show()
{
if (_parent.Content is not Panel panel)
return;
Dispatcher.UIThread.Post(() =>
{
panel.Children.Add(_infoBar);
_infoBar.Closed += InfoBarOnClosed;
_infoBar.IsOpen = true;
});
Task.Run(async () =>
{
await Task.Delay(_timeout);
Dispatcher.UIThread.Post(() => _infoBar.IsOpen = false);
});
}
private void InfoBarOnClosed(InfoBar sender, InfoBarClosedEventArgs args)
{
_infoBar.Closed -= InfoBarOnClosed;
if (_parent.Content is not Panel panel)
return;
panel.Children.Remove(_infoBar);
}
}
public enum NotificationSeverity
{
Informational,
Success,
Warning,
Error
}
}

View File

@ -0,0 +1,81 @@
using System;
using System.Threading.Tasks;
using Avalonia.Controls;
namespace Artemis.UI.Avalonia.Shared.Services.Builders
{
/// <summary>
/// Represents a builder that can create a <see cref="OpenFileDialog" />.
/// </summary>
public class OpenFileDialogBuilder
{
private readonly OpenFileDialog _openFileDialog;
private readonly Window _parent;
public OpenFileDialogBuilder(Window parent)
{
_parent = parent;
_openFileDialog = new OpenFileDialog();
}
/// <summary>
/// Indicate that the user can select multiple files.
/// </summary>
public OpenFileDialogBuilder WithAllowMultiple()
{
_openFileDialog.AllowMultiple = true;
return this;
}
/// <summary>
/// Set the title of the dialog
/// </summary>
public OpenFileDialogBuilder WithTitle(string? title)
{
_openFileDialog.Title = title;
return this;
}
/// <summary>
/// Set the initial directory of the dialog
/// </summary>
public OpenFileDialogBuilder WithDirectory(string? directory)
{
_openFileDialog.Directory = directory;
return this;
}
/// <summary>
/// Set the initial file name of the dialog
/// </summary>
public OpenFileDialogBuilder WithInitialFileName(string? initialFileName)
{
_openFileDialog.InitialFileName = initialFileName;
return this;
}
/// <summary>
/// Add a filter to the dialog
/// </summary>
public OpenFileDialogBuilder HavingFilter(Action<FileDialogFilterBuilder> configure)
{
FileDialogFilterBuilder builder = new();
configure(builder);
_openFileDialog.Filters.Add(builder.Build());
return this;
}
/// <summary>
/// Shows the file dialog
/// </summary>
/// <returns>
/// A task that on completion returns an array containing the full path to the selected
/// files, or null if the dialog was canceled.
/// </returns>
public async Task<string[]?> ShowAsync()
{
return await _openFileDialog.ShowAsync(_parent);
}
}
}

View File

@ -0,0 +1,81 @@
using System;
using System.Threading.Tasks;
using Avalonia.Controls;
namespace Artemis.UI.Avalonia.Shared.Services.Builders
{
/// <summary>
/// Represents a builder that can create a <see cref="SaveFileDialog" />.
/// </summary>
public class SaveFileDialogBuilder
{
private readonly Window _parent;
private readonly SaveFileDialog _saveFileDialog;
public SaveFileDialogBuilder(Window parent)
{
_parent = parent;
_saveFileDialog = new SaveFileDialog();
}
/// <summary>
/// Set the title of the dialog
/// </summary>
public SaveFileDialogBuilder WithTitle(string? title)
{
_saveFileDialog.Title = title;
return this;
}
/// <summary>
/// Set the initial directory of the dialog
/// </summary>
public SaveFileDialogBuilder WithDirectory(string? directory)
{
_saveFileDialog.Directory = directory;
return this;
}
/// <summary>
/// Set the initial file name of the dialog
/// </summary>
public SaveFileDialogBuilder WithInitialFileName(string? initialFileName)
{
_saveFileDialog.InitialFileName = initialFileName;
return this;
}
/// <summary>
/// Set the default extension of the dialog
/// </summary>
public SaveFileDialogBuilder WithDefaultExtension(string? defaultExtension)
{
_saveFileDialog.DefaultExtension = defaultExtension;
return this;
}
/// <summary>
/// Add a filter to the dialog
/// </summary>
public SaveFileDialogBuilder HavingFilter(Action<FileDialogFilterBuilder> configure)
{
FileDialogFilterBuilder builder = new();
configure(builder);
_saveFileDialog.Filters.Add(builder.Build());
return this;
}
/// <summary>
/// Shows the save file dialog.
/// </summary>
/// <returns>
/// A task that on completion contains the full path of the save location, or null if the
/// dialog was canceled.
/// </returns>
public async Task<string?> ShowAsync()
{
return await _saveFileDialog.ShowAsync(_parent);
}
}
}

View File

@ -0,0 +1,9 @@
using Artemis.UI.Avalonia.Shared.Services.Builders;
namespace Artemis.UI.Avalonia.Shared.Services.Interfaces
{
public interface INotificationService : IArtemisSharedUIService
{
NotificationBuilder CreateNotification();
}
}

View File

@ -1,9 +1,16 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Artemis.UI.Avalonia.Shared.Services.Builders;
using Avalonia.Controls;
namespace Artemis.UI.Avalonia.Shared.Services.Interfaces namespace Artemis.UI.Avalonia.Shared.Services.Interfaces
{ {
public interface IWindowService : IArtemisSharedUIService public interface IWindowService : IArtemisSharedUIService
{ {
/// <summary>
/// Creates a view model instance of type <typeparamref name="T" /> and shows its corresponding View as a window
/// </summary>
/// <typeparam name="T">The type of view model to create</typeparam>
/// <returns>The created view model</returns>
T ShowWindow<T>(); T ShowWindow<T>();
/// <summary> /// <summary>
@ -13,11 +20,27 @@ namespace Artemis.UI.Avalonia.Shared.Services.Interfaces
void ShowWindow(object viewModel); void ShowWindow(object viewModel);
/// <summary> /// <summary>
/// Given a ViewModel, show its corresponding View as a Dialog /// Given a ViewModel, show its corresponding View as a Dialog
/// </summary> /// </summary>
/// <typeparam name="T">The return type</typeparam> /// <typeparam name="T">The return type</typeparam>
/// <param name="viewModel">ViewModel to show the View for</param> /// <param name="viewModel">ViewModel to show the View for</param>
/// <returns>A task containing the return value of type <typeparamref name="T"/></returns> /// <returns>A task containing the return value of type <typeparamref name="T" /></returns>
Task<T> ShowDialog<T>(object viewModel); Task<T> ShowDialogAsync<T>(object viewModel);
/// <summary>
/// Creates an open file dialog, use the fluent API to configure it
/// </summary>
/// <returns>The builder that can be used to configure the dialog</returns>
OpenFileDialogBuilder CreateOpenFileDialog();
/// <summary>
/// Creates a save file dialog, use the fluent API to configure it
/// </summary>
/// <returns>The builder that can be used to configure the dialog</returns>
SaveFileDialogBuilder CreateSaveFileDialog();
ContentDialogBuilder CreateContentDialog();
Window GetCurrentWindow();
} }
} }

View File

@ -0,0 +1,20 @@
using Artemis.UI.Avalonia.Shared.Services.Builders;
using Artemis.UI.Avalonia.Shared.Services.Interfaces;
namespace Artemis.UI.Avalonia.Shared.Services
{
public class NotificationService : INotificationService
{
private readonly IWindowService _windowService;
public NotificationService(IWindowService windowService)
{
_windowService = windowService;
}
public NotificationBuilder CreateNotification()
{
return new NotificationBuilder(_windowService.GetCurrentWindow());
}
}
}

View File

@ -2,6 +2,7 @@
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Artemis.UI.Avalonia.Shared.Exceptions; using Artemis.UI.Avalonia.Shared.Exceptions;
using Artemis.UI.Avalonia.Shared.Services.Builders;
using Artemis.UI.Avalonia.Shared.Services.Interfaces; using Artemis.UI.Avalonia.Shared.Services.Interfaces;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
@ -19,9 +20,6 @@ namespace Artemis.UI.Avalonia.Shared.Services
_kernel = kernel; _kernel = kernel;
} }
#region Implementation of IWindowService
/// <inheritdoc />
public T ShowWindow<T>() public T ShowWindow<T>()
{ {
T viewModel = _kernel.Get<T>()!; T viewModel = _kernel.Get<T>()!;
@ -29,7 +27,6 @@ namespace Artemis.UI.Avalonia.Shared.Services
return viewModel; return viewModel;
} }
/// <inheritdoc />
public void ShowWindow(object viewModel) public void ShowWindow(object viewModel)
{ {
string name = viewModel.GetType().FullName!.Split('`')[0].Replace("ViewModel", "View"); string name = viewModel.GetType().FullName!.Split('`')[0].Replace("ViewModel", "View");
@ -45,8 +42,7 @@ namespace Artemis.UI.Avalonia.Shared.Services
window.Show(); window.Show();
} }
/// <inheritdoc /> public async Task<T> ShowDialogAsync<T>(object viewModel)
public async Task<T> ShowDialog<T>(object viewModel)
{ {
if (Application.Current.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime classic) if (Application.Current.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime classic)
throw new ArtemisSharedUIException($"Can't show a dialog when application lifetime is not IClassicDesktopStyleApplicationLifetime."); throw new ArtemisSharedUIException($"Can't show a dialog when application lifetime is not IClassicDesktopStyleApplicationLifetime.");
@ -65,6 +61,28 @@ namespace Artemis.UI.Avalonia.Shared.Services
return await window.ShowDialog<T>(parent); return await window.ShowDialog<T>(parent);
} }
#endregion public ContentDialogBuilder CreateContentDialog()
{
return new ContentDialogBuilder(_kernel, GetCurrentWindow());
}
public OpenFileDialogBuilder CreateOpenFileDialog()
{
return new OpenFileDialogBuilder(GetCurrentWindow());
}
public SaveFileDialogBuilder CreateSaveFileDialog()
{
return new SaveFileDialogBuilder(GetCurrentWindow());
}
public Window GetCurrentWindow()
{
if (Application.Current.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime classic)
throw new ArtemisSharedUIException("Can't show a dialog when application lifetime is not IClassicDesktopStyleApplicationLifetime.");
Window parent = classic.Windows.FirstOrDefault(w => w.IsActive) ?? classic.MainWindow;
return parent;
}
} }
} }

View File

@ -0,0 +1,19 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia">
<Design.PreviewWith>
<Border Padding="20">
<!-- Add Controls for Previewer Here -->
<controls:InfoBar Classes="notification-info-bar" Title="Test title" Message="Test message" IsOpen="True"/>
</Border>
</Design.PreviewWith>
<!-- Add Styles Here -->
<Style Selector="controls|InfoBar.notification-info-bar">
<Setter Property="MaxWidth" Value="600" />
<Setter Property="Margin" Value="15"/>
<Setter Property="Background" Value="#75000000"/>
<Setter Property="VerticalAlignment" Value="Bottom" />
<Setter Property="HorizontalAlignment" Value="Right" />
</Style>
</Styles>

View File

@ -22,5 +22,9 @@
<StyleInclude Source="/Styles/Button.axaml" /> <StyleInclude Source="/Styles/Button.axaml" />
<StyleInclude Source="/Styles/TextBlock.axaml" /> <StyleInclude Source="/Styles/TextBlock.axaml" />
<StyleInclude Source="/Styles/Sidebar.axaml" /> <StyleInclude Source="/Styles/Sidebar.axaml" />
<!-- Shared styles -->
<!-- TODO: Make a single file-->
<StyleInclude Source="avares://Artemis.UI.Avalonia.Shared/Styles/InfoBar.axaml" />
</Application.Styles> </Application.Styles>
</Application> </Application>

View File

@ -0,0 +1,22 @@
using System;
namespace Artemis.UI.Avalonia.Exceptions
{
public class ArtemisGraphicsContextException : Exception
{
/// <inheritdoc />
public ArtemisGraphicsContextException()
{
}
/// <inheritdoc />
public ArtemisGraphicsContextException(string message) : base(message)
{
}
/// <inheritdoc />
public ArtemisGraphicsContextException(string message, Exception innerException) : base(message, innerException)
{
}
}
}

View File

@ -0,0 +1,19 @@
using System;
namespace Artemis.UI.Avalonia.Exceptions
{
public class ArtemisUIException : Exception
{
public ArtemisUIException()
{
}
public ArtemisUIException(string message) : base(message)
{
}
public ArtemisUIException(string message, Exception inner) : base(message, inner)
{
}
}
}

View File

@ -0,0 +1,29 @@
using System.Threading.Tasks;
using Artemis.Core;
using Avalonia;
using RGB.NET.Core;
namespace Artemis.UI.Avalonia.Screens.Device.ViewModels
{
public class DeviceInfoTabViewModel : ActivatableViewModelBase
{
public DeviceInfoTabViewModel(ArtemisDevice device)
{
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)
{
await Application.Current.Clipboard.SetTextAsync(content);
// ((DeviceDialogViewModel) Parent).DeviceMessageQueue.Enqueue("Copied path to clipboard.");
}
}
}

View File

@ -0,0 +1,81 @@
using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Reactive.Disposables;
using Artemis.Core;
using DynamicData.Binding;
using ReactiveUI;
namespace Artemis.UI.Avalonia.Screens.Device.ViewModels
{
public class DeviceLedsTabViewModel : ActivatableViewModelBase
{
private readonly DevicePropertiesViewModel _devicePropertiesViewModel;
public DeviceLedsTabViewModel(ArtemisDevice device, DevicePropertiesViewModel devicePropertiesViewModel)
{
_devicePropertiesViewModel = devicePropertiesViewModel;
Device = device;
DisplayName = "LEDS";
LedViewModels = new ObservableCollection<DeviceLedsTabLedViewModel>(Device.Leds.Select(l => new DeviceLedsTabLedViewModel(l, _devicePropertiesViewModel.SelectedLeds)));
this.WhenActivated(disposables => _devicePropertiesViewModel.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 (!this.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);
}
}
}

View File

@ -0,0 +1,270 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Avalonia.Shared.Services.Builders;
using Artemis.UI.Avalonia.Shared.Services.Interfaces;
using FluentAvalonia.UI.Controls;
using ReactiveUI;
using SkiaSharp;
namespace Artemis.UI.Avalonia.Screens.Device.ViewModels
{
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 IRgbService _rgbService;
private readonly IWindowService _windowService;
private readonly INotificationService _notificationService;
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;
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;
}
public ArtemisDevice Device { get; }
public int X
{
get => _x;
set => this.RaiseAndSetIfChanged(ref _x, value);
}
public int Y
{
get => _y;
set => this.RaiseAndSetIfChanged(ref _y, value);
}
public float Scale
{
get => _scale;
set => this.RaiseAndSetIfChanged(ref _scale, value);
}
public int Rotation
{
get => _rotation;
set => this.RaiseAndSetIfChanged(ref _rotation, value);
}
public float RedScale
{
get => _redScale;
set => this.RaiseAndSetIfChanged(ref _redScale, value);
}
public float GreenScale
{
get => _greenScale;
set => this.RaiseAndSetIfChanged(ref _greenScale, value);
}
public float BlueScale
{
get => _blueScale;
set => this.RaiseAndSetIfChanged(ref _blueScale, value);
}
public SKColor CurrentColor
{
get => _currentColor;
set => this.RaiseAndSetIfChanged(ref _currentColor, value);
}
public bool DisplayOnDevices
{
get => _displayOnDevices;
set => this.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;
}
protected void OnActivate()
{
_coreService.FrameRendering += OnFrameRendering;
Device.PropertyChanged += DeviceOnPropertyChanged;
}
protected void OnDeactivate()
{
_coreService.FrameRendering -= OnFrameRendering;
Device.PropertyChanged -= DeviceOnPropertyChanged;
}
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 DeviceOnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Device.CustomLayoutPath) || e.PropertyName == nameof(Device.DisableDefaultLayout)) Task.Run(() => _rgbService.ApplyBestDeviceLayout(Device));
}
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);
}
}
}

View File

@ -1,4 +1,5 @@
using Artemis.Core; using System.Collections.ObjectModel;
using Artemis.Core;
namespace Artemis.UI.Avalonia.Screens.Device.ViewModels namespace Artemis.UI.Avalonia.Screens.Device.ViewModels
{ {
@ -10,5 +11,6 @@ namespace Artemis.UI.Avalonia.Screens.Device.ViewModels
} }
public ArtemisDevice Device { get; } public ArtemisDevice Device { get; }
public ObservableCollection<ArtemisLed> SelectedLeds { get; } = new();
} }
} }

View File

@ -0,0 +1,104 @@
using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Avalonia.Exceptions;
using ReactiveUI;
using RGB.NET.Core;
namespace Artemis.UI.Avalonia.Screens.Device.ViewModels
{
public class InputMappingsTabViewModel : ActivatableViewModelBase
{
private readonly IRgbService _rgbService;
private readonly IInputService _inputService;
private ArtemisLed _selectedLed;
public InputMappingsTabViewModel(ArtemisDevice device, IRgbService rgbService, IInputService inputService)
{
if (device.DeviceType != RGBDeviceType.Keyboard)
throw new ArtemisUIException("The input mappings tab only supports keyboards");
_rgbService = rgbService;
_inputService = inputService;
Device = device;
DisplayName = "INPUT MAPPINGS";
InputMappings = new ObservableCollection<(ArtemisLed, ArtemisLed)>();
}
public ArtemisDevice Device { get; }
public ArtemisLed SelectedLed
{
get => _selectedLed;
set => this.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
}
}

View File

@ -105,7 +105,7 @@ namespace Artemis.UI.Avalonia.Screens.SurfaceEditor.ViewModels
private async Task ExecuteViewProperties(ArtemisDevice device) private async Task ExecuteViewProperties(ArtemisDevice device)
{ {
await _windowService.ShowDialog<bool>(_deviceVmFactory.DevicePropertiesViewModel(device)); await _windowService.ShowDialogAsync<bool>(_deviceVmFactory.DevicePropertiesViewModel(device));
} }
private bool Fits(float x, float y) private bool Fits(float x, float y)