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:
parent
f98e398bc5
commit
4c836fb505
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
using Artemis.UI.Avalonia.Shared.Services.Builders;
|
||||
|
||||
namespace Artemis.UI.Avalonia.Shared.Services.Interfaces
|
||||
{
|
||||
public interface INotificationService : IArtemisSharedUIService
|
||||
{
|
||||
NotificationBuilder CreateNotification();
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,16 @@
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.UI.Avalonia.Shared.Services.Builders;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Artemis.UI.Avalonia.Shared.Services.Interfaces
|
||||
{
|
||||
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>();
|
||||
|
||||
/// <summary>
|
||||
@ -17,7 +24,23 @@ namespace Artemis.UI.Avalonia.Shared.Services.Interfaces
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The return type</typeparam>
|
||||
/// <param name="viewModel">ViewModel to show the View for</param>
|
||||
/// <returns>A task containing the return value of type <typeparamref name="T"/></returns>
|
||||
Task<T> ShowDialog<T>(object viewModel);
|
||||
/// <returns>A task containing the return value of type <typeparamref name="T" /></returns>
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.UI.Avalonia.Shared.Exceptions;
|
||||
using Artemis.UI.Avalonia.Shared.Services.Builders;
|
||||
using Artemis.UI.Avalonia.Shared.Services.Interfaces;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
@ -19,9 +20,6 @@ namespace Artemis.UI.Avalonia.Shared.Services
|
||||
_kernel = kernel;
|
||||
}
|
||||
|
||||
#region Implementation of IWindowService
|
||||
|
||||
/// <inheritdoc />
|
||||
public T ShowWindow<T>()
|
||||
{
|
||||
T viewModel = _kernel.Get<T>()!;
|
||||
@ -29,7 +27,6 @@ namespace Artemis.UI.Avalonia.Shared.Services
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ShowWindow(object viewModel)
|
||||
{
|
||||
string name = viewModel.GetType().FullName!.Split('`')[0].Replace("ViewModel", "View");
|
||||
@ -45,8 +42,7 @@ namespace Artemis.UI.Avalonia.Shared.Services
|
||||
window.Show();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<T> ShowDialog<T>(object viewModel)
|
||||
public async Task<T> ShowDialogAsync<T>(object viewModel)
|
||||
{
|
||||
if (Application.Current.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime classic)
|
||||
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);
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/Artemis.UI.Avalonia.Shared/Styles/InfoBar.axaml
Normal file
19
src/Artemis.UI.Avalonia.Shared/Styles/InfoBar.axaml
Normal 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>
|
||||
@ -22,5 +22,9 @@
|
||||
<StyleInclude Source="/Styles/Button.axaml" />
|
||||
<StyleInclude Source="/Styles/TextBlock.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>
|
||||
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/Artemis.UI.Avalonia/Exceptions/ArtemisUIException.cs
Normal file
19
src/Artemis.UI.Avalonia/Exceptions/ArtemisUIException.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
using Artemis.Core;
|
||||
using System.Collections.ObjectModel;
|
||||
using Artemis.Core;
|
||||
|
||||
namespace Artemis.UI.Avalonia.Screens.Device.ViewModels
|
||||
{
|
||||
@ -10,5 +11,6 @@ namespace Artemis.UI.Avalonia.Screens.Device.ViewModels
|
||||
}
|
||||
|
||||
public ArtemisDevice Device { get; }
|
||||
public ObservableCollection<ArtemisLed> SelectedLeds { get; } = new();
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -105,7 +105,7 @@ namespace Artemis.UI.Avalonia.Screens.SurfaceEditor.ViewModels
|
||||
|
||||
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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user