mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Core - Removed rendering from CoreService Core - Store and restore original device LEDs when applying/clearing layouts
298 lines
12 KiB
C#
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;
|
|
}
|
|
} |