1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00
Robert 2a0a0f3cfd Core - Split up RGBService into RenderService and DeviceService
Core - Removed rendering from CoreService
Core - Store and restore original device LEDs when applying/clearing layouts
2023-10-05 20:38:07 +02:00

298 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Extensions;
using Artemis.UI.Screens.ProfileEditor.ProfileTree.ContentDialogs;
using Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia;
using Avalonia.ReactiveUI;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree;
public abstract class TreeItemViewModel : ActivatableViewModelBase
{
private readonly IProfileEditorVmFactory _profileEditorVmFactory;
private readonly IWindowService _windowService;
private readonly IDeviceService _deviceService;
protected readonly IProfileEditorService ProfileEditorService;
private bool _canPaste;
private RenderProfileElement? _currentProfileElement;
private bool _isExpanded;
private bool _isFlyoutOpen;
private ObservableAsPropertyHelper<bool>? _isFocused;
private ProfileElement? _profileElement;
private TimeSpan _time;
protected TreeItemViewModel(TreeItemViewModel? parent,
ProfileElement? profileElement,
IWindowService windowService,
IDeviceService deviceService,
IProfileEditorService profileEditorService,
IProfileEditorVmFactory profileEditorVmFactory)
{
ProfileEditorService = profileEditorService;
_windowService = windowService;
_deviceService = deviceService;
_profileEditorVmFactory = profileEditorVmFactory;
Parent = parent;
ProfileElement = profileElement;
AddLayer = ReactiveCommand.Create(ExecuteAddLayer);
AddFolder = ReactiveCommand.Create(ExecuteAddFolder);
OpenAdaptionHints = ReactiveCommand.CreateFromTask(ExecuteOpenAdaptionHints, this.WhenAnyValue(vm => vm.ProfileElement).Select(p => p is Layer));
ApplyAdaptionHints = ReactiveCommand.Create(ExecuteApplyAdaptionHints, this.WhenAnyValue(vm => vm.ProfileElement).Select(p => p is Layer));
Rename = ReactiveCommand.CreateFromTask(ExecuteRename);
Delete = ReactiveCommand.Create(ExecuteDelete);
Duplicate = ReactiveCommand.CreateFromTask(ExecuteDuplicate);
Copy = ReactiveCommand.CreateFromTask(ExecuteCopy);
Paste = ReactiveCommand.CreateFromTask(ExecutePaste, this.WhenAnyValue(vm => vm.CanPaste));
AbsorbCommand = ReactiveCommand.Create(() => true);
this.WhenActivated(d =>
{
_isFocused = ProfileEditorService.FocusMode
.CombineLatest(ProfileEditorService.ProfileElement)
.Select(tuple => GetIsFocused(tuple.First, tuple.Second))
.ToProperty(this, vm => vm.IsFocused)
.DisposeWith(d);
ProfileEditorService.Time.Subscribe(t => _time = t).DisposeWith(d);
ProfileEditorService.ProfileElement.Subscribe(element => _currentProfileElement = element).DisposeWith(d);
SubscribeToProfileElement(d);
CreateTreeItems();
});
this.WhenAnyValue(vm => vm.IsFlyoutOpen).ObserveOn(AvaloniaScheduler.Instance).Subscribe(UpdateCanPaste);
}
public ReactiveCommand<Unit, bool> AbsorbCommand { get; }
public bool IsFocused => _isFocused?.Value ?? false;
public ProfileElement? ProfileElement
{
get => _profileElement;
set => RaiseAndSetIfChanged(ref _profileElement, value);
}
public bool IsExpanded
{
get => _isExpanded;
set => RaiseAndSetIfChanged(ref _isExpanded, value);
}
public bool IsFlyoutOpen
{
get => _isFlyoutOpen;
set => RaiseAndSetIfChanged(ref _isFlyoutOpen, value);
}
public bool CanPaste
{
get => _canPaste;
set => RaiseAndSetIfChanged(ref _canPaste, value);
}
public TreeItemViewModel? Parent { get; set; }
public ObservableCollection<TreeItemViewModel> Children { get; } = new();
public ReactiveCommand<Unit, Unit> AddLayer { get; }
public ReactiveCommand<Unit, Unit> AddFolder { get; }
public ReactiveCommand<Unit, Unit> OpenAdaptionHints { get; }
public ReactiveCommand<Unit, Unit> ApplyAdaptionHints { get; }
public ReactiveCommand<Unit, Unit> Rename { get; }
public ReactiveCommand<Unit, Unit> Duplicate { get; }
public ReactiveCommand<Unit, Unit> Copy { get; }
public ReactiveCommand<Unit, Unit> Paste { get; }
public ReactiveCommand<Unit, Unit> Delete { get; }
public abstract bool SupportsChildren { get; }
public async Task ShowBrokenStateExceptions()
{
if (ProfileElement == null)
return;
List<IBreakableModel> broken = ProfileElement.GetBrokenHierarchy().Where(b => b.BrokenStateException != null).ToList();
foreach (IBreakableModel current in broken)
{
_windowService.ShowExceptionDialog($"{current.BrokenDisplayName} - {current.BrokenState}", current.BrokenStateException!);
if (broken.Last() == current)
continue;
if (!await _windowService.ShowConfirmContentDialog("Broken state", "Do you want to view the next exception?"))
return;
}
}
public void InsertElement(TreeItemViewModel elementViewModel, int targetIndex)
{
if (elementViewModel.Parent == this && Children.IndexOf(elementViewModel) == targetIndex)
return;
if (ProfileElement != null && elementViewModel.ProfileElement != null)
ProfileEditorService.ExecuteCommand(new MoveProfileElement(ProfileElement, elementViewModel.ProfileElement, targetIndex));
}
protected abstract Task ExecuteDuplicate();
protected abstract Task ExecuteCopy();
protected abstract Task ExecutePaste();
protected void SubscribeToProfileElement(CompositeDisposable d)
{
if (ProfileElement == null)
return;
Observable.FromEventPattern<ProfileElementEventArgs>(x => ProfileElement.ChildAdded += x, x => ProfileElement.ChildAdded -= x)
.Subscribe(c => AddTreeItemIfMissing(c.EventArgs.ProfileElement))
.DisposeWith(d);
Observable.FromEventPattern<ProfileElementEventArgs>(x => ProfileElement.ChildRemoved += x, x => ProfileElement.ChildRemoved -= x)
.Subscribe(c => RemoveTreeItemsIfFound(c.Sender, c.EventArgs.ProfileElement))
.DisposeWith(d);
ProfileElement.WhenAnyValue(e => e.Suspended).Subscribe(_ => ProfileEditorService.ChangeTime(_time)).DisposeWith(d);
}
protected void RemoveTreeItemsIfFound(object? sender, ProfileElement profileElement)
{
List<TreeItemViewModel> toRemove = Children.Where(t => t.ProfileElement == profileElement).ToList();
foreach (TreeItemViewModel treeItemViewModel in toRemove)
Children.Remove(treeItemViewModel);
if (_currentProfileElement != profileElement)
return;
// Find a good candidate for a new selection, preferring the next sibling, falling back to the previous sibling and finally the parent
ProfileElement? parent = sender as ProfileElement;
ProfileElement? newSelection = null;
if (parent != null)
{
newSelection = parent.Children.FirstOrDefault(c => c.Order == profileElement.Order) ?? parent.Children.FirstOrDefault(c => c.Order == profileElement.Order - 1);
if (newSelection == null && parent is Folder {IsRootFolder: false})
newSelection = parent;
}
ProfileEditorService.ChangeCurrentProfileElement(newSelection as RenderProfileElement);
}
protected void AddTreeItemIfMissing(ProfileElement profileElement)
{
if (Children.Any(t => t.ProfileElement == profileElement))
return;
if (profileElement is Folder folder)
Children.Insert(folder.Parent.Children.IndexOf(folder), _profileEditorVmFactory.FolderTreeItemViewModel(this, folder));
else if (profileElement is Layer layer)
Children.Insert(layer.Parent.Children.IndexOf(layer), _profileEditorVmFactory.LayerTreeItemViewModel(this, layer));
// Select the newly added element
if (profileElement is RenderProfileElement renderProfileElement)
ProfileEditorService.ChangeCurrentProfileElement(renderProfileElement);
}
protected void CreateTreeItems()
{
if (Children.Any())
Children.Clear();
if (ProfileElement == null)
return;
foreach (ProfileElement profileElement in ProfileElement.Children)
{
if (profileElement is Folder folder)
Children.Add(_profileEditorVmFactory.FolderTreeItemViewModel(this, folder));
else if (profileElement is Layer layer)
Children.Add(_profileEditorVmFactory.LayerTreeItemViewModel(this, layer));
}
}
private void ExecuteDelete()
{
if (ProfileElement is RenderProfileElement renderProfileElement)
ProfileEditorService.ExecuteCommand(new RemoveProfileElement(renderProfileElement));
}
private async Task ExecuteRename()
{
if (ProfileElement == null)
return;
await _windowService.CreateContentDialog()
.WithTitle(ProfileElement is Folder ? "Rename folder" : "Rename layer")
.WithViewModel(out ProfileElementRenameViewModel vm, ProfileElement)
.HavingPrimaryButton(b => b.WithText("Confirm").WithCommand(vm.Confirm))
.WithCloseButtonText("Cancel")
.WithDefaultButton(ContentDialogButton.Primary)
.ShowAsync();
}
private void ExecuteAddFolder()
{
if (ProfileElement != null)
ProfileEditorService.CreateAndAddFolder(ProfileElement);
}
private void ExecuteAddLayer()
{
if (ProfileElement != null)
ProfileEditorService.CreateAndAddLayer(ProfileElement);
}
private async Task ExecuteOpenAdaptionHints()
{
if (ProfileElement is not Layer layer)
return;
await _windowService.ShowDialogAsync<LayerHintsDialogViewModel, bool>(layer);
await ProfileEditorService.SaveProfileAsync();
}
private void ExecuteApplyAdaptionHints()
{
if (ProfileElement is not Layer layer)
return;
ProfileEditorService.ExecuteCommand(new ApplyAdaptionHints(layer, _deviceService.EnabledDevices.ToList()));
}
private async void UpdateCanPaste(bool isFlyoutOpen)
{
string[] formats = await Shared.UI.Clipboard.GetFormatsAsync();
//diogotr7: This can be null on Linux sometimes. I'm not sure why.
if (formats == null!)
{
CanPaste = false;
return;
}
CanPaste = formats.Contains(ProfileElementExtensions.ClipboardDataFormat);
}
private bool GetIsFocused(ProfileEditorFocusMode focusMode, RenderProfileElement? currentProfileElement)
{
if (focusMode == ProfileEditorFocusMode.None || currentProfileElement == null)
return false;
if (focusMode == ProfileEditorFocusMode.Selection)
return currentProfileElement == ProfileElement;
if (focusMode == ProfileEditorFocusMode.Folder && currentProfileElement?.Parent != null)
// Any direct parent or direct siblings cause focus
return currentProfileElement.Parent == ProfileElement?.Parent || currentProfileElement.Parent.GetAllRenderElements().Any(e => e == ProfileElement);
return false;
}
}