Profiles - Added module activation requirements

Editor - Refactored tools and selected keyframes management
Timeline - Added keyframe duplicating, copying and pasting
Windows UI - Added logging of fatal exceptions
This commit is contained in:
Robert committed 2022-06-05 18:57:42 +02:00
1 parent d563d17270
commit dd40bdd544
31 files changed
+719 -178

No files matched your search

@@ -1,5 +1,7 @@
using System;
using System.Reactive;
using Artemis.Core;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
@@ -22,10 +24,11 @@ public interface ITimelineKeyframeViewModel
#region Context menu actions
void PopulateEasingViewModels();
void Duplicate();
void Copy();
void Paste();
void Delete();
ReactiveCommand<Unit, Unit> Duplicate { get; }
ReactiveCommand<Unit, Unit> Copy { get; }
ReactiveCommand<Unit, Unit> Paste { get; }
ReactiveCommand<Unit, Unit> Delete { get; }
#endregion
}
@@ -52,23 +52,23 @@
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="-" />
<MenuItem Header="Duplicate" Command="{Binding $parent[timeline:TimelineView].DataContext.DuplicateKeyframes}" CommandParameter="{Binding}" InputGesture="Ctrl+D">
<MenuItem Header="Duplicate" Command="{Binding Duplicate}" InputGesture="Ctrl+D">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ContentDuplicate" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Copy" Command="{Binding $parent[timeline:TimelineView].DataContext.CopyKeyframes}" CommandParameter="{Binding}" InputGesture="Ctrl+C">
<MenuItem Header="Copy" Command="{Binding Copy}" InputGesture="Ctrl+C">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ContentCopy" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Paste" Command="{Binding $parent[timeline:TimelineView].DataContext.PasteKeyframes}" CommandParameter="{Binding}" InputGesture="Ctrl+V">
<MenuItem Header="Paste" Command="{Binding Paste}" InputGesture="Ctrl+V">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ContentPaste" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="-" />
<MenuItem Header="Delete" Command="{Binding $parent[timeline:TimelineView].DataContext.DeleteKeyframes}" CommandParameter="{Binding}" InputGesture="Delete">
<MenuItem Header="Delete" Command="{Binding Delete}" InputGesture="Delete">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Delete" />
</MenuItem.Icon>
@@ -1,13 +1,22 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Models;
using Artemis.UI.Services.ProfileEditor.Commands;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Extensions;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia;
using Avalonia.Controls.Mixins;
using Avalonia.Input;
using DynamicData;
using DynamicData.Binding;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
@@ -17,8 +26,9 @@ public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineK
private readonly IProfileEditorService _profileEditorService;
private ObservableAsPropertyHelper<bool>? _isSelected;
private string _timestamp;
private double _x;
private bool _canPaste;
private bool _isFlyoutOpen;
public TimelineKeyframeViewModel(LayerPropertyKeyframe<T> layerPropertyKeyframe, IProfileEditorService profileEditorService)
{
@@ -29,20 +39,29 @@ public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineK
this.WhenActivated(d =>
{
_isSelected = profileEditorService.ConnectToKeyframes()
.ToCollection()
.Select(keyframes => keyframes.Contains(LayerPropertyKeyframe))
_isSelected = profileEditorService.SelectedKeyframes
.ToObservableChangeSet()
.Select(_ => profileEditorService.SelectedKeyframes.Contains(LayerPropertyKeyframe))
.ToProperty(this, vm => vm.IsSelected)
.DisposeWith(d);
profileEditorService.ConnectToKeyframes();
profileEditorService.PixelsPerSecond.Subscribe(p => _pixelsPerSecond = p).DisposeWith(d);
profileEditorService.PixelsPerSecond.Subscribe(_ => Update()).DisposeWith(d);
this.WhenAnyValue(vm => vm.LayerPropertyKeyframe.Position).Subscribe(_ => Update()).DisposeWith(d);
});
this.WhenAnyValue(vm => vm.IsFlyoutOpen).Subscribe(UpdateCanPaste);
Duplicate = ReactiveCommand.Create(ExecuteDuplicate);
Copy = ReactiveCommand.CreateFromTask(ExecuteCopy);
Paste = ReactiveCommand.CreateFromTask(ExecutePaste);
Delete = ReactiveCommand.Create(ExecuteDelete);
}
public LayerPropertyKeyframe<T> LayerPropertyKeyframe { get; }
public ObservableCollection<TimelineEasingViewModel> EasingViewModels { 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 double X
{
@@ -56,6 +75,18 @@ public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineK
set => RaiseAndSetIfChanged(ref _timestamp, value);
}
public bool IsFlyoutOpen
{
get => _isFlyoutOpen;
set => RaiseAndSetIfChanged(ref _isFlyoutOpen, value);
}
public bool CanPaste
{
get => _canPaste;
set => RaiseAndSetIfChanged(ref _canPaste, value);
}
public void Update()
{
X = _pixelsPerSecond * LayerPropertyKeyframe.Position.TotalSeconds;
@@ -79,24 +110,104 @@ public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineK
#region Context menu actions
public void Duplicate()
private void ExecuteDelete()
{
throw new NotImplementedException();
if (!IsSelected)
{
_profileEditorService.ExecuteCommand(new DeleteKeyframe(Keyframe));
}
else
{
List<ILayerPropertyKeyframe> keyframes = _profileEditorService.SelectedKeyframes.ToList();
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Delete keyframes");
foreach (ILayerPropertyKeyframe keyframe in keyframes)
_profileEditorService.ExecuteCommand(new DeleteKeyframe(keyframe));
}
}
public void Copy()
private void ExecuteDuplicate()
{
throw new NotImplementedException();
if (!IsSelected)
{
DuplicateKeyframe command = new(Keyframe, FindKeyframeDuplicationPosition(Keyframe));
_profileEditorService.ExecuteCommand(command);
_profileEditorService.SelectKeyframe(command.Duplication, false, false);
}
else
{
List<ILayerPropertyKeyframe> keyframes = _profileEditorService.SelectedKeyframes.ToList();
_profileEditorService.SelectKeyframe(null, false, false);
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Duplicate keyframes");
foreach (ILayerPropertyKeyframe keyframe in keyframes)
{
DuplicateKeyframe command = new(keyframe, FindKeyframeDuplicationPosition(keyframe));
_profileEditorService.ExecuteCommand(command);
_profileEditorService.SelectKeyframe(command.Duplication, true, false);
}
}
}
public void Paste()
private async Task ExecuteCopy()
{
throw new NotImplementedException();
if (Application.Current?.Clipboard == null)
return;
List<KeyframeClipboardModel> keyframes = new();
if (!IsSelected)
keyframes.Add(new KeyframeClipboardModel(Keyframe));
else
keyframes.AddRange(_profileEditorService.SelectedKeyframes.Select(k => new KeyframeClipboardModel(k)));
string copy = CoreJson.SerializeObject(keyframes, true);
DataObject dataObject = new();
dataObject.Set(KeyframeClipboardModel.ClipboardDataFormat, copy);
await Application.Current.Clipboard.SetDataObjectAsync(dataObject);
}
public void Delete()
private async Task ExecutePaste()
{
_profileEditorService.ExecuteCommand(new DeleteKeyframe(LayerPropertyKeyframe));
if (Application.Current?.Clipboard == null)
return;
List<KeyframeClipboardModel>? keyframes = await Application.Current.Clipboard.GetJsonAsync<List<KeyframeClipboardModel>>(KeyframeClipboardModel.ClipboardDataFormat);
if (keyframes == null)
return;
PasteKeyframes command = new(Keyframe.UntypedLayerProperty.ProfileElement, keyframes, FindKeyframeDuplicationPosition(Keyframe));
_profileEditorService.ExecuteCommand(command);
if (command.PastedKeyframes != null && command.PastedKeyframes.Any())
_profileEditorService.SelectKeyframes(command.PastedKeyframes, false);
}
private TimeSpan FindKeyframeDuplicationPosition(ILayerPropertyKeyframe keyframe)
{
TimeSpan position;
TimeSpan distance = TimeSpan.FromSeconds(15 / _pixelsPerSecond);
TimeSpan maxRight = keyframe.UntypedLayerProperty.ProfileElement.Timeline.Length;
// Pick the side that has the most available space
// Prefer right side
if (keyframe.Position + distance <= maxRight)
// Put the keyframe as far to the right as possible within the max
position = TimeSpan.FromSeconds(Math.Min(maxRight.TotalSeconds, (keyframe.Position + distance).TotalSeconds));
// Fall back to left side
else
// Put the keyframe as far to the left as possible withing the max
position = TimeSpan.FromSeconds(Math.Max(0, (keyframe.Position - distance).TotalSeconds));
return position;
}
private async void UpdateCanPaste(bool isFlyoutOpen)
{
if (Application.Current?.Clipboard == null)
{
CanPaste = false;
return;
}
string[] formats = await Application.Current.Clipboard.GetFormatsAsync();
CanPaste = formats.Contains("Artemis.Keyframes");
}
#endregion
@@ -12,7 +12,10 @@
</UserControl.Resources>
<Grid Background="Transparent" PointerReleased="InputElement_OnPointerReleased" Focusable="True" MinWidth="{Binding MinWidth}">
<Grid.KeyBindings>
<KeyBinding Command="{Binding DeleteKeyframes}" Gesture="Delete" />
<KeyBinding Command="{CompiledBinding CopySelectedKeyframes}" Gesture="Ctrl+C" />
<KeyBinding Command="{CompiledBinding DuplicateSelectedKeyframes}" Gesture="Ctrl+D" />
<KeyBinding Command="{CompiledBinding PasteKeyframes}" Gesture="Ctrl+V" />
<KeyBinding Command="{CompiledBinding DeleteSelectedKeyframes}" Gesture="Delete" />
</Grid.KeyBindings>
<ItemsControl Items="{CompiledBinding PropertyGroupViewModels}">
<ItemsControl.ItemTemplate>
@@ -2,11 +2,18 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Models;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
using Artemis.UI.Services.ProfileEditor.Commands;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Extensions;
using Artemis.UI.Shared.Services.ProfileEditor;
using Avalonia;
using Avalonia.Controls.Mixins;
using ReactiveUI;
@@ -19,6 +26,8 @@ public class TimelineViewModel : ActivatableViewModelBase
private ObservableAsPropertyHelper<double> _minWidth;
private List<ITimelineKeyframeViewModel>? _moveKeyframes;
private ObservableAsPropertyHelper<int>? _pixelsPerSecond;
private RenderProfileElement? _profileElement;
private TimeSpan _time;
public TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels,
StartSegmentViewModel startSegmentViewModel,
@@ -34,6 +43,8 @@ public class TimelineViewModel : ActivatableViewModelBase
_profileEditorService = profileEditorService;
this.WhenActivated(d =>
{
_profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d);
_profileEditorService.Time.Subscribe(t => _time = t).DisposeWith(d);
_caretPosition = _profileEditorService.Time
.CombineLatest(_profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
.ToProperty(this, vm => vm.CaretPosition)
@@ -46,12 +57,21 @@ public class TimelineViewModel : ActivatableViewModelBase
.ToProperty(this, vm => vm.MinWidth)
.DisposeWith(d);
});
DuplicateSelectedKeyframes = ReactiveCommand.Create(ExecuteDuplicateSelectedKeyframes);
CopySelectedKeyframes = ReactiveCommand.Create(ExecuteCopySelectedKeyframes);
PasteKeyframes = ReactiveCommand.CreateFromTask(ExecutePasteKeyframes);
DeleteSelectedKeyframes = ReactiveCommand.Create(ExecuteDeleteSelectedKeyframes);
}
public ObservableCollection<PropertyGroupViewModel> PropertyGroupViewModels { get; }
public StartSegmentViewModel StartSegmentViewModel { get; }
public MainSegmentViewModel MainSegmentViewModel { get; }
public EndSegmentViewModel EndSegmentViewModel { get; }
public ReactiveCommand<Unit, Unit> DuplicateSelectedKeyframes { get; }
public ReactiveCommand<Unit, Unit> CopySelectedKeyframes { get; }
public ReactiveCommand<Unit, Unit> PasteKeyframes { get; }
public ReactiveCommand<Unit, Unit> DeleteSelectedKeyframes { get; }
public double CaretPosition => _caretPosition?.Value ?? 0.0;
public int PixelsPerSecond => _pixelsPerSecond?.Value ?? 0;
@@ -137,64 +157,34 @@ public class TimelineViewModel : ActivatableViewModelBase
#region Keyframe actions
public void DuplicateKeyframes(ITimelineKeyframeViewModel? source = null)
private void ExecuteDuplicateSelectedKeyframes()
{
if (source is {IsSelected: false})
{
source.Delete();
}
else
{
List<ITimelineKeyframeViewModel> keyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Where(k => k.IsSelected).ToList();
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope($"Duplicate {keyframes.Count} keyframes.");
foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in keyframes)
timelineKeyframeViewModel.Duplicate();
}
PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).FirstOrDefault(k => k.IsSelected)?.Duplicate.Execute().Subscribe();
}
public void CopyKeyframes(ITimelineKeyframeViewModel? source = null)
private void ExecuteCopySelectedKeyframes()
{
if (source is {IsSelected: false})
{
source.Copy();
}
else
{
List<ITimelineKeyframeViewModel> keyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Where(k => k.IsSelected).ToList();
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope($"Copy {keyframes.Count} keyframes.");
foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in keyframes)
timelineKeyframeViewModel.Copy();
}
PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).FirstOrDefault(k => k.IsSelected)?.Copy.Execute().Subscribe();
}
public void PasteKeyframes(ITimelineKeyframeViewModel? source = null)
private async Task ExecutePasteKeyframes()
{
if (source is {IsSelected: false})
{
source.Paste();
}
else
{
List<ITimelineKeyframeViewModel> keyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Where(k => k.IsSelected).ToList();
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope($"Paste {keyframes.Count} keyframes.");
foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in keyframes)
timelineKeyframeViewModel.Paste();
}
if (_profileElement == null || Application.Current?.Clipboard == null)
return;
List<KeyframeClipboardModel>? keyframes = await Application.Current.Clipboard.GetJsonAsync<List<KeyframeClipboardModel>>(KeyframeClipboardModel.ClipboardDataFormat);
if (keyframes == null)
return;
PasteKeyframes command = new(_profileElement, keyframes, _time);
_profileEditorService.ExecuteCommand(command);
if (command.PastedKeyframes != null && command.PastedKeyframes.Any())
_profileEditorService.SelectKeyframes(command.PastedKeyframes, false);
}
public void DeleteKeyframes(ITimelineKeyframeViewModel? source = null)
private void ExecuteDeleteSelectedKeyframes()
{
if (source is {IsSelected: false})
{
source.Delete();
}
else
{
List<ITimelineKeyframeViewModel> keyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Where(k => k.IsSelected).ToList();
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope($"Delete {keyframes.Count} keyframes.");
foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in keyframes)
timelineKeyframeViewModel.Delete();
}
PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).FirstOrDefault(k => k.IsSelected)?.Delete.Execute().Subscribe();
}
#endregion
@@ -44,8 +44,10 @@ public class VisualEditorViewModel : ActivatableViewModelBase
profileEditorService.ProfileConfiguration.Subscribe(CreateVisualizers).DisposeWith(d);
profileEditorService.Tools
.Connect()
.AutoRefreshOnObservable(t => t.WhenAnyValue(vm => vm.IsSelected)).Filter(t => t.IsSelected).Bind(out ReadOnlyObservableCollection<IToolViewModel> tools)
.ToObservableChangeSet()
.AutoRefreshOnObservable(t => t.WhenAnyValue(vm => vm.IsSelected))
.Filter(t => t.IsSelected)
.Bind(out ReadOnlyObservableCollection<IToolViewModel> tools)
.Subscribe()
.DisposeWith(d);
Tools = tools;
@@ -51,7 +51,8 @@ public class ProfileEditorViewModel : MainScreenViewModel
_profileConfiguration = profileEditorService.ProfileConfiguration.ToProperty(this, vm => vm.ProfileConfiguration).DisposeWith(d);
_history = profileEditorService.History.ToProperty(this, vm => vm.History).DisposeWith(d);
_suspendedEditing = profileEditorService.SuspendedEditing.ToProperty(this, vm => vm.SuspendedEditing).DisposeWith(d);
profileEditorService.Tools.Connect()
profileEditorService.Tools
.ToObservableChangeSet()
.Filter(t => t.ShowInToolbar)
.Sort(SortExpressionComparer<IToolViewModel>.Ascending(vm => vm.Order))
.Bind(out ReadOnlyObservableCollection<IToolViewModel> tools)
@@ -0,0 +1,42 @@
<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:sidebar="clr-namespace:Artemis.UI.Screens.Sidebar"
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.Sidebar.ModuleActivationRequirementView"
x:DataType="sidebar:ModuleActivationRequirementViewModel">
<UserControl.Styles>
<Styles>
<Style Selector="Border.status-border">
<Setter Property="Width" Value="32" />
<Setter Property="Height" Value="32" />
<Setter Property="CornerRadius" Value="16" />
</Style>
<Style Selector="Border.status-border avalonia|MaterialIcon">
<Setter Property="Width" Value="16" />
<Setter Property="Height" Value="16" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalAlignment" Value="Center" />
</Style>
</Styles>
</UserControl.Styles>
<StackPanel>
<Separator Classes="card-separator" />
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0">
<TextBlock TextWrapping="Wrap" Text="{CompiledBinding RequirementName}" />
<TextBlock Classes="subtitle" TextWrapping="Wrap" Text="{CompiledBinding RequirementDescription}" />
</StackPanel>
<Border Grid.Row="0" Grid.Column="1" Classes="status-border" IsVisible="{CompiledBinding !RequirementMet}" Background="#ff3838">
<avalonia:MaterialIcon Kind="Close" />
</Border>
<Border Grid.Row="0" Grid.Column="1" Classes="status-border" IsVisible="{CompiledBinding RequirementMet}" Background="#32a852">
<avalonia:MaterialIcon Kind="Check" />
</Border>
</Grid>
</StackPanel>
</UserControl>
@@ -0,0 +1,17 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Sidebar;
public class ModuleActivationRequirementView : ReactiveUserControl<ModuleActivationRequirementViewModel>
{
public ModuleActivationRequirementView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
@@ -0,0 +1,56 @@
using System;
using System.Reactive.Disposables;
using Artemis.Core.Modules;
using Artemis.UI.Shared;
using Avalonia.Threading;
using Humanizer;
using ReactiveUI;
namespace Artemis.UI.Screens.Sidebar;
public class ModuleActivationRequirementViewModel : ActivatableViewModelBase
{
private readonly IModuleActivationRequirement _activationRequirement;
private string _requirementDescription;
private bool _requirementMet;
private DispatcherTimer? _updateTimer;
public ModuleActivationRequirementViewModel(IModuleActivationRequirement activationRequirement)
{
RequirementName = activationRequirement.GetType().Name.Humanize();
_requirementDescription = activationRequirement.GetUserFriendlyDescription();
_activationRequirement = activationRequirement;
this.WhenActivated(d =>
{
_updateTimer = new DispatcherTimer(TimeSpan.FromMilliseconds(500), DispatcherPriority.Normal, Update);
_updateTimer.Start();
Disposable.Create(() =>
{
_updateTimer?.Stop();
_updateTimer = null;
}).DisposeWith(d);
});
}
public string RequirementName { get; }
public string RequirementDescription
{
get => _requirementDescription;
set => RaiseAndSetIfChanged(ref _requirementDescription, value);
}
public bool RequirementMet
{
get => _requirementMet;
set => RaiseAndSetIfChanged(ref _requirementMet, value);
}
private void Update(object? sender, EventArgs e)
{
RequirementDescription = _activationRequirement.GetUserFriendlyDescription();
RequirementMet = _activationRequirement.Evaluate();
}
}
@@ -0,0 +1,28 @@
<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:sidebar="clr-namespace:Artemis.UI.Screens.Sidebar"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Sidebar.ModuleActivationRequirementsView"
x:DataType="sidebar:ModuleActivationRequirementsViewModel">
<StackPanel>
<Grid ColumnDefinitions="*,Auto,*">
<Separator Grid.Column="0" Height="1" Background="{DynamicResource TextFillColorTertiaryBrush}" />
<TextBlock Grid.Column="1" Margin="16 0">AND</TextBlock>
<Separator Grid.Column="2" Height="1" Background="{DynamicResource TextFillColorTertiaryBrush}" />
</Grid>
<Border Classes="card" Margin="0 5">
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<TextBlock Text="This module has built-in activation requirements and your profile won't activate until " TextWrapping="Wrap" />
<TextBlock Text="{CompiledBinding ActivationType}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" FontWeight="Medium" TextWrapping="Wrap" />
<TextBlock Text="." />
</StackPanel>
<TextBlock TextWrapping="Wrap">These requirements allow the module creator to decide when the data is available to your profile you cannot override them.</TextBlock>
<ItemsControl Items="{CompiledBinding ActivationRequirements}" />
</StackPanel>
</Border>
</StackPanel>
</UserControl>
@@ -0,0 +1,17 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.Sidebar;
public class ModuleActivationRequirementsView : UserControl
{
public ModuleActivationRequirementsView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
@@ -0,0 +1,20 @@
using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Core.Modules;
using Artemis.UI.Shared;
namespace Artemis.UI.Screens.Sidebar;
public class ModuleActivationRequirementsViewModel : ViewModelBase
{
public ModuleActivationRequirementsViewModel(Module module)
{
ActivationType = module.ActivationRequirementMode == ActivationRequirementType.All ? "all requirements are met" : "any requirements are met";
ActivationRequirements = module.ActivationRequirements != null
? new ObservableCollection<ModuleActivationRequirementViewModel>(module.ActivationRequirements.Select(r => new ModuleActivationRequirementViewModel(r)))
: new ObservableCollection<ModuleActivationRequirementViewModel>();
}
public string ActivationType { get; }
public ObservableCollection<ModuleActivationRequirementViewModel> ActivationRequirements { get; }
}
@@ -53,10 +53,10 @@
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
<ComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type local:ProfileModuleViewModel}">
<DataTemplate>
<StackPanel Orientation="Horizontal">
<shared:ArtemisIcon Icon="{CompiledBinding Icon}" Width="16" Height="16" Margin="0 0 5 0" />
<TextBlock Text="{CompiledBinding Name}" />
<shared:ArtemisIcon Icon="{CompiledBinding Icon, FallbackValue='Close'}" Width="16" Height="16" Margin="0 0 5 0" />
<TextBlock Text="{CompiledBinding Name, FallbackValue='None'}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
@@ -167,7 +167,7 @@
<TextBlock Classes="h5 section-header">Activation conditions</TextBlock>
<TextBlock Classes="subtitle section-subtitle">Set up certain conditions under which the profile should be active</TextBlock>
</StackPanel>
<Border Classes="card" Margin="0 5 0 15">
<Border Classes="card" Margin="0 5">
<Grid>
<Border CornerRadius="5" ClipToBounds="True">
<ContentControl Content="{CompiledBinding VisualEditorViewModel}" Height="150" />
@@ -182,8 +182,9 @@
Edit condition script
</Button>
</Grid>
</Border>
<ContentControl Content="{CompiledBinding ModuleActivationRequirementsViewModel}" />
</StackPanel>
</ScrollViewer>
@@ -3,6 +3,7 @@ using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Modules;
@@ -39,6 +40,7 @@ namespace Artemis.UI.Screens.Sidebar
private ProfileIconViewModel? _selectedMaterialIcon;
private ProfileModuleViewModel? _selectedModule;
private SvgImage? _selectedSvgSource;
private readonly ObservableAsPropertyHelper<ModuleActivationRequirementsViewModel?> _moduleActivationRequirementsViewModel;
public ProfileConfigurationEditViewModel(
ProfileCategory profileCategory,
@@ -64,11 +66,12 @@ namespace Artemis.UI.Screens.Sidebar
IsNew = profileConfiguration == null;
DisplayName = IsNew ? "Artemis | Add profile" : "Artemis | Edit profile";
Modules = new ObservableCollection<ProfileModuleViewModel>(
Modules = new ObservableCollection<ProfileModuleViewModel?>(
pluginManagementService.GetFeaturesOfType<Module>().Where(m => !m.IsAlwaysAvailable).Select(m => new ProfileModuleViewModel(m))
);
Modules.Insert(0, null);
VisualEditorViewModel = nodeVmFactory.NodeScriptViewModel(_profileConfiguration.ActivationCondition, true);
Dispatcher.UIThread.Post(LoadIcon, DispatcherPriority.Background);
BrowseBitmapFile = ReactiveCommand.CreateFromTask(ExecuteBrowseBitmapFile);
BrowseSvgFile = ReactiveCommand.CreateFromTask(ExecuteBrowseSvgFile);
@@ -77,7 +80,12 @@ namespace Artemis.UI.Screens.Sidebar
Import = ReactiveCommand.CreateFromTask(ExecuteImport);
Delete = ReactiveCommand.CreateFromTask(ExecuteDelete);
Cancel = ReactiveCommand.Create(ExecuteCancel);
_moduleActivationRequirementsViewModel = this.WhenAnyValue(vm => vm.SelectedModule)
.Select(m => m != null ? new ModuleActivationRequirementsViewModel(m.Module) : null)
.ToProperty(this, vm => vm.ModuleActivationRequirementsViewModel);
Dispatcher.UIThread.Post(LoadIcon, DispatcherPriority.Background);
}
public bool IsNew { get; }
@@ -112,7 +120,7 @@ namespace Artemis.UI.Screens.Sidebar
set => RaiseAndSetIfChanged(ref _disableHotkey, value);
}
public ObservableCollection<ProfileModuleViewModel> Modules { get; }
public ObservableCollection<ProfileModuleViewModel?> Modules { get; }
public ProfileModuleViewModel? SelectedModule
{
@@ -121,6 +129,7 @@ namespace Artemis.UI.Screens.Sidebar
}
public NodeScriptViewModel VisualEditorViewModel { get; }
public ModuleActivationRequirementsViewModel? ModuleActivationRequirementsViewModel => _moduleActivationRequirementsViewModel.Value;
public ReactiveCommand<Unit, Unit> OpenConditionEditor { get; }
public ReactiveCommand<Unit, Unit> BrowseBitmapFile { get; }
@@ -129,7 +138,7 @@ namespace Artemis.UI.Screens.Sidebar
public ReactiveCommand<Unit, Unit> Import { get; }
public ReactiveCommand<Unit, Unit> Delete { get; }
public new ReactiveCommand<Unit, Unit> Cancel { get; }
private async Task ExecuteImport()
{
if (!IsNew)
@@ -306,7 +315,7 @@ namespace Artemis.UI.Screens.Sidebar
SelectedSvgSource = new SvgImage {Source = newSource};
_selectedIconPath = result[0];
}
private async Task ExecuteOpenConditionEditor()
{
await _windowService.ShowDialogAsync<NodeScriptWindowViewModel, bool>(("nodeScript", ProfileConfiguration.ActivationCondition));