Plugins - Allow config window to be mandatory

UI - Move profile editor into Profile screen, profile page no longer always has to show the editor
This commit is contained in:
Robert authored and RobertBeekman committed 2025-12-09 20:42:13 +01:00
1 parent 565647f1ed
commit 3c8d8b2387
191 files changed
+449 -416

No files matched your search

@@ -1,18 +0,0 @@
using System;
using Artemis.UI.Shared;
namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition;
public class ConditionTypeViewModel : ViewModelBase
{
public ConditionTypeViewModel(string name, string description, Type conditionType)
{
Name = name;
Description = description;
ConditionType = conditionType;
}
public string Name { get; }
public string Description { get; }
public Type ConditionType { get; }
}
@@ -1,16 +0,0 @@
<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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="300" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes.AlwaysOnConditionView">
<DockPanel VerticalAlignment="Top" Margin="0 5">
<avalonia:MaterialIcon Kind="InfoCircle" Margin="5 0" />
<TextBlock Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap">
After playing the start segment, the main segment is endlessly repeated.
</TextBlock>
</DockPanel>
</UserControl>
@@ -1,13 +0,0 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes;
public partial class AlwaysOnConditionView : UserControl
{
public AlwaysOnConditionView()
{
InitializeComponent();
}
}
@@ -1,14 +0,0 @@
using Artemis.Core;
using Artemis.UI.Shared;
namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes;
public class AlwaysOnConditionViewModel : ViewModelBase
{
private readonly AlwaysOnCondition _alwaysOnCondition;
public AlwaysOnConditionViewModel(AlwaysOnCondition alwaysOnCondition)
{
_alwaysOnCondition = alwaysOnCondition;
}
}
@@ -1,70 +0,0 @@
<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:conditionTypes="clr-namespace:Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes"
xmlns:dataModelPicker="clr-namespace:Artemis.UI.Shared.DataModelPicker;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes.EventConditionView"
x:DataType="conditionTypes:EventConditionViewModel">
<DockPanel HorizontalAlignment="Stretch">
<DockPanel.Styles>
<Style Selector="DockPanel > TextBlock">
<Setter Property="Margin" Value="0 5" />
</Style>
</DockPanel.Styles>
<TextBlock DockPanel.Dock="Top">Triggered by event</TextBlock>
<dataModelPicker:DataModelPickerButton Placeholder="Select an event"
DockPanel.Dock="Top"
HorizontalAlignment="Stretch"
DataModelPath="{CompiledBinding EventPath}"
ShowFullPath="{CompiledBinding ShowFullPaths.Value}"
IsEventPicker="True" />
<TextBlock DockPanel.Dock="Top">When the event fires..</TextBlock>
<ComboBox PlaceholderText="Select a play mode" HorizontalAlignment="Stretch" DockPanel.Dock="Top" SelectedIndex="{CompiledBinding SelectedTriggerMode}">
<ComboBoxItem>
Play the timeline once
</ComboBoxItem>
<ComboBoxItem>
Toggle the element on or off
</ComboBoxItem>
</ComboBox>
<TextBlock DockPanel.Dock="Top" IsVisible="{CompiledBinding ShowOverlapOptions}">And if already playing..</TextBlock>
<ComboBox PlaceholderText="Select a play mode"
HorizontalAlignment="Stretch"
DockPanel.Dock="Top"
SelectedIndex="{CompiledBinding SelectedOverlapMode}"
IsVisible="{CompiledBinding ShowOverlapOptions}">
<ComboBoxItem>
Restart the timeline
</ComboBoxItem>
<ComboBoxItem IsEnabled="{CompiledBinding IsConditionForLayer}">
Play a second copy
</ComboBoxItem>
<ComboBoxItem>
Do nothing
</ComboBoxItem>
</ComboBox>
<TextBlock DockPanel.Dock="Top" IsVisible="{CompiledBinding ShowToggleOffOptions}">When toggling off..</TextBlock>
<ComboBox PlaceholderText="Select a play mode"
HorizontalAlignment="Stretch"
DockPanel.Dock="Top"
SelectedIndex="{CompiledBinding SelectedToggleOffMode}"
IsVisible="{CompiledBinding ShowToggleOffOptions}">
<ComboBoxItem>Finish the main segment</ComboBoxItem>
<ComboBoxItem>Skip forward to the end segment</ComboBoxItem>
</ComboBox>
<Button DockPanel.Dock="Bottom"
ToolTip.Tip="Open editor"
Margin="0 15 0 5"
Command="{CompiledBinding OpenEditor}"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom">
Edit condition script
</Button>
</DockPanel>
</UserControl>
@@ -1,13 +0,0 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes;
public partial class EventConditionView : ReactiveUserControl<EventConditionViewModel>
{
public EventConditionView()
{
InitializeComponent();
}
}
@@ -1,84 +0,0 @@
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Screens.VisualScripting;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes;
public class EventConditionViewModel : ActivatableViewModelBase
{
private readonly EventCondition _eventCondition;
private readonly IProfileEditorService _profileEditorService;
private readonly ISettingsService _settingsService;
private readonly ObservableAsPropertyHelper<bool> _showOverlapOptions;
private readonly ObservableAsPropertyHelper<bool> _showToggleOffOptions;
private readonly IWindowService _windowService;
private ObservableAsPropertyHelper<DataModelPath?>? _eventPath;
private ObservableAsPropertyHelper<int>? _selectedOverlapMode;
private ObservableAsPropertyHelper<int>? _selectedToggleOffMode;
private ObservableAsPropertyHelper<int>? _selectedTriggerMode;
public EventConditionViewModel(EventCondition eventCondition, IProfileEditorService profileEditorService, IWindowService windowService, ISettingsService settingsService)
{
_eventCondition = eventCondition;
_profileEditorService = profileEditorService;
_windowService = windowService;
_settingsService = settingsService;
_showOverlapOptions = this.WhenAnyValue(vm => vm.SelectedTriggerMode).Select(m => m == 0).ToProperty(this, vm => vm.ShowOverlapOptions);
_showToggleOffOptions = this.WhenAnyValue(vm => vm.SelectedTriggerMode).Select(m => m == 1).ToProperty(this, vm => vm.ShowToggleOffOptions);
this.WhenActivated(d =>
{
_eventPath = eventCondition.WhenAnyValue(c => c.EventPath).ToProperty(this, vm => vm.EventPath).DisposeWith(d);
_selectedTriggerMode = eventCondition.WhenAnyValue(c => c.TriggerMode).Select(m => (int) m).ToProperty(this, vm => vm.SelectedTriggerMode).DisposeWith(d);
_selectedOverlapMode = eventCondition.WhenAnyValue(c => c.OverlapMode).Select(m => (int) m).ToProperty(this, vm => vm.SelectedOverlapMode).DisposeWith(d);
_selectedToggleOffMode = eventCondition.WhenAnyValue(c => c.OverlapMode).Select(m => (int) m).ToProperty(this, vm => vm.SelectedOverlapMode).DisposeWith(d);
});
OpenEditor = ReactiveCommand.CreateFromTask(ExecuteOpenEditor, this.WhenAnyValue(vm => vm.EventPath).Select(p => p != null));
}
public ReactiveCommand<Unit, Unit> OpenEditor { get; }
public bool ShowOverlapOptions => _showOverlapOptions.Value;
public bool ShowToggleOffOptions => _showToggleOffOptions.Value;
public bool IsConditionForLayer => _eventCondition.ProfileElement is Layer;
public PluginSetting<bool> ShowFullPaths => _settingsService.GetSetting("ProfileEditor.ShowFullPaths", false);
public DataModelPath? EventPath
{
get => _eventPath?.Value != null ? new DataModelPath(_eventPath.Value) : null;
set => _profileEditorService.ExecuteCommand(new UpdateEventConditionPath(_eventCondition, value != null ? new DataModelPath(value) : null));
}
public int SelectedTriggerMode
{
get => _selectedTriggerMode?.Value ?? 0;
set => _profileEditorService.ExecuteCommand(new UpdateEventTriggerMode(_eventCondition, (EventTriggerMode) value));
}
public int SelectedOverlapMode
{
get => _selectedOverlapMode?.Value ?? 0;
set => _profileEditorService.ExecuteCommand(new UpdateEventOverlapMode(_eventCondition, (EventOverlapMode) value));
}
public int SelectedToggleOffMode
{
get => _selectedToggleOffMode?.Value ?? 0;
set => _profileEditorService.ExecuteCommand(new UpdateEventToggleOffMode(_eventCondition, (EventToggleOffMode) value));
}
private async Task ExecuteOpenEditor()
{
await _windowService.ShowDialogAsync<NodeScriptWindowViewModel, bool>(_eventCondition.Script);
await _profileEditorService.SaveProfileAsync();
}
}
@@ -1,15 +0,0 @@
<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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes.PlayOnceConditionView">
<DockPanel VerticalAlignment="Top" Margin="0 5">
<avalonia:MaterialIcon Kind="InfoCircle" Margin="5 0" />
<TextBlock Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap">
Every segment of the timeline is played once.
</TextBlock>
</DockPanel>
</UserControl>
@@ -1,13 +0,0 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes;
public partial class PlayOnceConditionView : UserControl
{
public PlayOnceConditionView()
{
InitializeComponent();
}
}
@@ -1,14 +0,0 @@
using Artemis.Core;
using Artemis.UI.Shared;
namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes;
public class PlayOnceConditionViewModel : ViewModelBase
{
private readonly PlayOnceCondition _playOnceCondition;
public PlayOnceConditionViewModel(PlayOnceCondition playOnceCondition)
{
_playOnceCondition = playOnceCondition;
}
}
@@ -1,45 +0,0 @@
<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:conditionTypes="clr-namespace:Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes"
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.ProfileEditor.DisplayCondition.ConditionTypes.StaticConditionView"
x:DataType="conditionTypes:StaticConditionViewModel">
<DockPanel HorizontalAlignment="Stretch">
<DockPanel.Styles>
<Style Selector="DockPanel > TextBlock">
<Setter Property="Margin" Value="0 5" />
</Style>
</DockPanel.Styles>
<TextBlock DockPanel.Dock="Top">While condition is met..</TextBlock>
<ComboBox DockPanel.Dock="Top" PlaceholderText="Select a play mode" HorizontalAlignment="Stretch" SelectedIndex="{CompiledBinding SelectedPlayMode}">
<ComboBoxItem>Repeat the main segment</ComboBoxItem>
<ComboBoxItem>Play the main segment once</ComboBoxItem>
</ComboBox>
<TextBlock DockPanel.Dock="Top">And when no longer met..</TextBlock>
<ComboBox DockPanel.Dock="Top" PlaceholderText="Select a stop mode" HorizontalAlignment="Stretch" SelectedIndex="{CompiledBinding SelectedStopMode}">
<ComboBoxItem>Finish the main segment</ComboBoxItem>
<ComboBoxItem>Skip forward to the end segment</ComboBoxItem>
</ComboBox>
<DockPanel DockPanel.Dock="Top" Margin="0 5">
<avalonia:MaterialIcon Kind="InfoCircle" Margin="5 0" />
<TextBlock Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap">
The start- and end-segments are played once each time the condition is met.
</TextBlock>
</DockPanel>
<Button DockPanel.Dock="Bottom"
ToolTip.Tip="Open editor"
Margin="0 15 0 5"
Command="{CompiledBinding OpenEditor}"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom">
Edit condition script
</Button>
</DockPanel>
</UserControl>
@@ -1,13 +0,0 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes;
public partial class StaticConditionView : ReactiveUserControl<StaticConditionViewModel>
{
public StaticConditionView()
{
InitializeComponent();
}
}
@@ -1,57 +0,0 @@
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Screens.VisualScripting;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes;
public class StaticConditionViewModel : ActivatableViewModelBase
{
private readonly IProfileEditorService _profileEditorService;
private readonly StaticCondition _staticCondition;
private readonly IWindowService _windowService;
private ObservableAsPropertyHelper<int>? _selectedPlayMode;
private ObservableAsPropertyHelper<int>? _selectedStopMode;
public StaticConditionViewModel(StaticCondition staticCondition, IProfileEditorService profileEditorService, IWindowService windowService)
{
_staticCondition = staticCondition;
_profileEditorService = profileEditorService;
_windowService = windowService;
this.WhenActivated(d =>
{
_selectedPlayMode = staticCondition.WhenAnyValue(c => c.PlayMode).Select(m => (int) m).ToProperty(this, vm => vm.SelectedPlayMode).DisposeWith(d);
_selectedStopMode = staticCondition.WhenAnyValue(c => c.StopMode).Select(m => (int) m).ToProperty(this, vm => vm.SelectedStopMode).DisposeWith(d);
});
OpenEditor = ReactiveCommand.CreateFromTask(ExecuteOpenEditor);
}
public ReactiveCommand<Unit, Unit> OpenEditor { get; }
public int SelectedPlayMode
{
get => _selectedPlayMode?.Value ?? 0;
set => _profileEditorService.ExecuteCommand(new UpdateStaticPlayMode(_staticCondition, (StaticPlayMode) value));
}
public int SelectedStopMode
{
get => _selectedStopMode?.Value ?? 0;
set => _profileEditorService.ExecuteCommand(new UpdateStaticStopMode(_staticCondition, (StaticStopMode) value));
}
private async Task ExecuteOpenEditor()
{
await _windowService.ShowDialogAsync<NodeScriptWindowViewModel, bool>(_staticCondition.Script);
await _profileEditorService.SaveProfileAsync();
}
}
@@ -1,49 +0,0 @@
<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:displayCondition="clr-namespace:Artemis.UI.Screens.ProfileEditor.DisplayCondition"
mc:Ignorable="d" d:DesignWidth="200" d:DesignHeight="650"
x:Class="Artemis.UI.Screens.ProfileEditor.DisplayCondition.DisplayConditionScriptView"
x:DataType="displayCondition:DisplayConditionScriptViewModel">
<UserControl.Styles>
<Style Selector="ComboBox.condition-type /template/ ContentPresenter#ContentPresenter">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate DataType="{x:Type displayCondition:ConditionTypeViewModel}">
<TextBlock Text="{CompiledBinding Name}" VerticalAlignment="Center" />
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Styles>
<ScrollViewer>
<Grid RowDefinitions="Auto,Auto,*">
<TextBlock Grid.Row="0" Margin="0 5">Activation type</TextBlock>
<ComboBox Name="ConditionType"
Grid.Row="1"
Classes="condition-type"
PlaceholderText="Select an activation type"
ItemsSource="{CompiledBinding ConditionTypeViewModels}"
IsEnabled="{CompiledBinding ProfileElement, Converter={x:Static ObjectConverters.IsNotNull}}"
SelectedItem="{CompiledBinding SelectedConditionTypeViewModel}"
HorizontalAlignment="Stretch">
<ComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type displayCondition:ConditionTypeViewModel}">
<StackPanel Spacing="5">
<TextBlock Text="{CompiledBinding Name}" TextWrapping="Wrap" MaxWidth="350" />
<TextBlock Text="{CompiledBinding Description}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" TextWrapping="Wrap" MaxWidth="350" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
<ComboBox.SelectionBoxItemTemplate>
<DataTemplate DataType="{x:Type displayCondition:ConditionTypeViewModel}">
<TextBlock Text="{CompiledBinding Name}" TextWrapping="Wrap" MaxWidth="350" />
</DataTemplate>
</ComboBox.SelectionBoxItemTemplate>
</ComboBox>
<ContentControl Grid.Row="2" Content="{CompiledBinding ConditionViewModel}" />
</Grid>
</ScrollViewer>
</UserControl>
@@ -1,13 +0,0 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition;
public partial class DisplayConditionScriptView : ReactiveUserControl<DisplayConditionScriptViewModel>
{
public DisplayConditionScriptView()
{
InitializeComponent();
}
}
@@ -1,91 +0,0 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition;
public class DisplayConditionScriptViewModel : ActivatableViewModelBase
{
private readonly ObservableAsPropertyHelper<ViewModelBase?> _conditionViewModel;
private readonly IConditionVmFactory _conditionVmFactory;
private readonly IProfileEditorService _profileEditorService;
private ObservableAsPropertyHelper<RenderProfileElement?>? _profileElement;
private ObservableAsPropertyHelper<ConditionTypeViewModel?>? _selectedConditionTypeViewModel;
public DisplayConditionScriptViewModel(IProfileEditorService profileEditorService, IConditionVmFactory conditionVmFactory)
{
_profileEditorService = profileEditorService;
_conditionVmFactory = conditionVmFactory;
ConditionTypeViewModels = new ObservableCollection<ConditionTypeViewModel>
{
new("Always", "The element is always active.", typeof(AlwaysOnCondition)),
new("Once", "The element is shown once until its timeline is finished.", typeof(PlayOnceCondition)),
new("Conditional", "The element is activated when the provided visual script ends in true.", typeof(StaticCondition)),
new("On event", "The element is activated when the selected event fires.\r\n" +
"Events that contain data can conditionally trigger the layer using a visual script.", typeof(EventCondition))
};
this.WhenActivated(d =>
{
_profileElement = profileEditorService.ProfileElement.ToProperty(this, vm => vm.ProfileElement).DisposeWith(d);
_selectedConditionTypeViewModel = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.DisplayCondition) ?? Observable.Return<ICondition?>(null))
.Switch()
.Select(c => c != null ? ConditionTypeViewModels.FirstOrDefault(vm => vm.ConditionType == c.GetType()) : null)
.ToProperty(this, vm => vm.SelectedConditionTypeViewModel)
.DisposeWith(d);
});
_conditionViewModel = this.WhenAnyValue(vm => vm.SelectedConditionTypeViewModel, vm => vm.ProfileElement).Select(_ => CreateConditionViewModel()).ToProperty(this, vm => vm.ConditionViewModel);
}
public RenderProfileElement? ProfileElement => _profileElement?.Value;
public ViewModelBase? ConditionViewModel => _conditionViewModel.Value;
public ObservableCollection<ConditionTypeViewModel> ConditionTypeViewModels { get; }
public ConditionTypeViewModel? SelectedConditionTypeViewModel
{
get => _selectedConditionTypeViewModel?.Value;
set => ApplyConditionType(value);
}
private ViewModelBase? CreateConditionViewModel()
{
if (ProfileElement == null)
return null;
if (ProfileElement.DisplayCondition is AlwaysOnCondition alwaysOnCondition)
return _conditionVmFactory.AlwaysOnConditionViewModel(alwaysOnCondition);
if (ProfileElement.DisplayCondition is PlayOnceCondition playOnceCondition)
return _conditionVmFactory.PlayOnceConditionViewModel(playOnceCondition);
if (ProfileElement.DisplayCondition is StaticCondition staticCondition)
return _conditionVmFactory.StaticConditionViewModel(staticCondition);
if (ProfileElement.DisplayCondition is EventCondition eventCondition)
return _conditionVmFactory.EventConditionViewModel(eventCondition);
return null;
}
private void ApplyConditionType(ConditionTypeViewModel? value)
{
if (ProfileElement == null || value == null || ProfileElement.DisplayCondition.GetType() == value.ConditionType)
return;
if (value.ConditionType == typeof(AlwaysOnCondition))
_profileEditorService.ExecuteCommand(new ChangeConditionType(ProfileElement, new AlwaysOnCondition(ProfileElement)));
if (value.ConditionType == typeof(PlayOnceCondition))
_profileEditorService.ExecuteCommand(new ChangeConditionType(ProfileElement, new PlayOnceCondition(ProfileElement)));
if (value.ConditionType == typeof(StaticCondition))
_profileEditorService.ExecuteCommand(new ChangeConditionType(ProfileElement, new StaticCondition(ProfileElement)));
if (value.ConditionType == typeof(EventCondition))
_profileEditorService.ExecuteCommand(new ChangeConditionType(ProfileElement, new EventCondition(ProfileElement)));
}
}
@@ -1,211 +0,0 @@
<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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:menuBar="clr-namespace:Artemis.UI.Screens.ProfileEditor.MenuBar"
xmlns:profileEditor="clr-namespace:Artemis.UI.Shared.Services.ProfileEditor;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.MenuBar.MenuBarView"
x:DataType="menuBar:MenuBarViewModel">
<Menu VerticalAlignment="Top" Closed="MenuBase_OnMenuClosed">
<MenuItem Header="_File">
<MenuItem Header="New">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Plus" />
</MenuItem.Icon>
<MenuItem Header="Folder" Command="{CompiledBinding AddFolder}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Folder" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Layer" Command="{CompiledBinding AddLayer}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Layers" />
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<Separator />
<MenuItem Header="View Properties" Command="{CompiledBinding ViewProperties}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Settings" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Adapt Profile" Command="{CompiledBinding AdaptProfile}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Magic" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Suspend Profile" Command="{CompiledBinding ToggleSuspended}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Check" IsVisible="{CompiledBinding IsSuspended}" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Export Profile" Command="{CompiledBinding ExportProfile}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Export" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Duplicate Profile" Command="{CompiledBinding DuplicateProfile}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ContentDuplicate" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Delete Profile" Command="{CompiledBinding DeleteProfile}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Trash" />
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<MenuItem Header="_Edit">
<MenuItem Header="_Undo" Command="{CompiledBinding History.Undo}" InputGesture="Ctrl+Z">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Undo" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="_Redo" Command="{CompiledBinding History.Redo}" InputGesture="Ctrl+Y">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Redo" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="_Duplicate" Command="{Binding Duplicate}" InputGesture="Ctrl+D">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ContentDuplicate" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="_Copy" Command="{Binding Copy}" InputGesture="Ctrl+C">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ContentCopy" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="_Paste" Command="{Binding Paste}" InputGesture="Ctrl+V">
<MenuItem.Icon>
<avalonia:MaterialIconExt Kind="ContentPaste" />
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<MenuItem Header="_Run">
<MenuItem Header="_Switch run mode" Command="{CompiledBinding ToggleSuspendedEditing}" InputGesture="F5">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="SwapHorizontal" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Switch Run Mode on Focus Loss"
ToolTip.Tip="If enabled, run mode is set to normal on focus loss"
InputGesture="Shift+F5"
Command="{CompiledBinding ToggleBooleanSetting}"
CommandParameter="{CompiledBinding AutoSuspend}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Check" IsVisible="{CompiledBinding AutoSuspend.Value}" />
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<MenuItem Header="_Options">
<MenuItem Header="Focus Mode" InputGesture="F">
<MenuItem Header="None"
Command="{CompiledBinding ChangeFocusMode}"
CommandParameter="{x:Static profileEditor:ProfileEditorFocusMode.None}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Check" IsVisible="{CompiledBinding FocusNone}" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Current Folder"
Command="{CompiledBinding ChangeFocusMode}"
CommandParameter="{x:Static profileEditor:ProfileEditorFocusMode.Folder}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Check" IsVisible="{CompiledBinding FocusFolder}" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Current Selection"
Command="{CompiledBinding ChangeFocusMode}"
CommandParameter="{x:Static profileEditor:ProfileEditorFocusMode.Selection}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Check" IsVisible="{CompiledBinding FocusSelection}" />
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<MenuItem Header="Display Data Model Values"
Command="{CompiledBinding ToggleBooleanSetting}"
CommandParameter="{CompiledBinding ShowDataModelValues}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Check" IsVisible="{CompiledBinding ShowDataModelValues.Value}" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Display Full Condition Paths"
Command="{CompiledBinding ToggleBooleanSetting}"
CommandParameter="{CompiledBinding ShowFullPaths}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Check" IsVisible="{CompiledBinding ShowFullPaths.Value}" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Always Display Cable Values"
ToolTip.Tip="If enabled, cable values are always shown instead of only on hover"
Command="{CompiledBinding ToggleBooleanSetting}"
CommandParameter="{CompiledBinding AlwaysShowValues}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Check" IsVisible="{CompiledBinding AlwaysShowValues.Value}" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Apply All Data Bindings During Edit"
ToolTip.Tip="If enabled, updates all data bindings instead of only the one you are editing"
Command="{CompiledBinding ToggleBooleanSetting}"
CommandParameter="{CompiledBinding AlwaysApplyDataBindings}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Check" IsVisible="{CompiledBinding AlwaysApplyDataBindings.Value}" />
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<MenuItem Header="_Help">
<MenuItem Header="Artemis Wiki" Command="{CompiledBinding OpenUri}" CommandParameter="https://wiki.artemis-rgb.com/?mtm_campaign=artemis&amp;mtm_kwd=profile-editor">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="BookEdit" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Editor" Command="{CompiledBinding OpenUri}" CommandParameter="https://wiki.artemis-rgb.com/en/guides/user/profiles?mtm_campaign=artemis&amp;mtm_kwd=profile-editor">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Edit" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Layers" Command="{CompiledBinding OpenUri}" CommandParameter="https://wiki.artemis-rgb.com/guides/user/profiles/layers?mtm_campaign=artemis&amp;mtm_kwd=profile-editor">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Layers" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Display Conditions" Command="{CompiledBinding OpenUri}" CommandParameter="https://wiki.artemis-rgb.com/guides/user/profiles/conditions?mtm_campaign=artemis&amp;mtm_kwd=profile-editor">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="NotEqual" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Timeline" Command="{CompiledBinding OpenUri}" CommandParameter="https://wiki.artemis-rgb.com/guides/user/profiles/timeline?mtm_campaign=artemis&amp;mtm_kwd=profile-editor">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Stopwatch" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Data Bindings" Command="{CompiledBinding OpenUri}" CommandParameter="https://wiki.artemis-rgb.com/guides/user/profiles/data-bindings?mtm_campaign=artemis&amp;mtm_kwd=profile-editor">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="VectorLink" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Scripting" Command="{CompiledBinding OpenUri}" CommandParameter="https://wiki.artemis-rgb.com/guides/user/profiles/scripting?mtm_campaign=artemis&amp;mtm_kwd=profile-editor">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="CodeJson" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Report a Bug" Command="{CompiledBinding OpenUri}" CommandParameter="https://github.com/Artemis-RGB/Artemis/issues?mtm_campaign=artemis&amp;mtm_kwd=profile-editor">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Github" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Get Help on Discord" Command="{CompiledBinding OpenUri}" CommandParameter="https://discord.gg/S3MVaC9">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Chat" />
</MenuItem.Icon>
</MenuItem>
</MenuItem>
</Menu>
</UserControl>
@@ -1,22 +0,0 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using Avalonia.VisualTree;
namespace Artemis.UI.Screens.ProfileEditor.MenuBar;
public partial class MenuBarView : ReactiveUserControl<MenuBarViewModel>
{
public MenuBarView()
{
InitializeComponent();
}
private void MenuBase_OnMenuClosed(object? sender, RoutedEventArgs e)
{
ProfileEditorView? profileEditorView = this.FindAncestorOfType<Window>().FindDescendantOfType<ProfileEditorView>();
profileEditorView?.Focus();
}
}
@@ -1,236 +0,0 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Screens.Sidebar;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.ProfileEditor;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.MenuBar;
public partial class MenuBarViewModel : ActivatableViewModelBase
{
private readonly IRouter _router;
private readonly IProfileEditorService _profileEditorService;
private readonly IProfileService _profileService;
private readonly ISettingsService _settingsService;
private readonly IWindowService _windowService;
private ObservableAsPropertyHelper<bool>? _focusFolder;
private ObservableAsPropertyHelper<bool>? _focusNone;
private ObservableAsPropertyHelper<bool>? _focusSelection;
private ObservableAsPropertyHelper<bool>? _isSuspended;
private ObservableAsPropertyHelper<bool>? _keyBindingsEnabled;
private ObservableAsPropertyHelper<ProfileConfiguration?>? _profileConfiguration;
private ObservableAsPropertyHelper<RenderProfileElement?>? _profileElement;
private ObservableAsPropertyHelper<bool>? _suspendedEditing;
[Notify] private ProfileEditorHistory? _history;
public MenuBarViewModel(IRouter router, IProfileEditorService profileEditorService, IProfileService profileService, ISettingsService settingsService, IWindowService windowService)
{
_router = router;
_profileEditorService = profileEditorService;
_profileService = profileService;
_settingsService = settingsService;
_windowService = windowService;
this.WhenActivated(d =>
{
profileEditorService.History.Subscribe(history => History = history).DisposeWith(d);
_profileConfiguration = profileEditorService.ProfileConfiguration.ToProperty(this, vm => vm.ProfileConfiguration).DisposeWith(d);
_profileElement = profileEditorService.ProfileElement.ToProperty(this, vm => vm.ProfileElement).DisposeWith(d);
_suspendedEditing = profileEditorService.SuspendedEditing.ToProperty(this, vm => vm.SuspendedEditing).DisposeWith(d);
_isSuspended = profileEditorService.ProfileConfiguration
.Select(p => p?.WhenAnyValue(c => c.IsSuspended) ?? Observable.Never<bool>())
.Switch()
.ToProperty(this, vm => vm.IsSuspended)
.DisposeWith(d);
_focusNone = profileEditorService.FocusMode.Select(f => f == ProfileEditorFocusMode.None).ToProperty(this, vm => vm.FocusNone).DisposeWith(d);
_focusFolder = profileEditorService.FocusMode.Select(f => f == ProfileEditorFocusMode.Folder).ToProperty(this, vm => vm.FocusFolder).DisposeWith(d);
_focusSelection = profileEditorService.FocusMode.Select(f => f == ProfileEditorFocusMode.Selection).ToProperty(this, vm => vm.FocusSelection).DisposeWith(d);
_keyBindingsEnabled = Shared.UI.CurrentKeyBindingsEnabled.ToProperty(this, vm => vm.KeyBindingsEnabled).DisposeWith(d);
});
AddFolder = ReactiveCommand.Create(ExecuteAddFolder);
AddLayer = ReactiveCommand.Create(ExecuteAddLayer);
ViewProperties = ReactiveCommand.CreateFromTask(ExecuteViewProperties, this.WhenAnyValue(vm => vm.ProfileConfiguration).Select(c => c != null));
AdaptProfile = ReactiveCommand.CreateFromTask(ExecuteAdaptProfile, this.WhenAnyValue(vm => vm.ProfileConfiguration).Select(c => c != null));
ToggleSuspended = ReactiveCommand.Create(ExecuteToggleSuspended, this.WhenAnyValue(vm => vm.ProfileConfiguration).Select(c => c != null));
DeleteProfile = ReactiveCommand.CreateFromTask(ExecuteDeleteProfile, this.WhenAnyValue(vm => vm.ProfileConfiguration).Select(c => c != null));
ExportProfile = ReactiveCommand.CreateFromTask(ExecuteExportProfile, this.WhenAnyValue(vm => vm.ProfileConfiguration).Select(c => c != null));
DuplicateProfile = ReactiveCommand.CreateFromTask(ExecuteDuplicateProfile, this.WhenAnyValue(vm => vm.ProfileConfiguration).Select(c => c != null));
ToggleSuspendedEditing = ReactiveCommand.Create(ExecuteToggleSuspendedEditing);
OpenUri = ReactiveCommand.Create<string>(s => Process.Start(new ProcessStartInfo(s) {UseShellExecute = true, Verb = "open"}));
ToggleBooleanSetting = ReactiveCommand.Create<PluginSetting<bool>>(ExecuteToggleBooleanSetting);
ChangeFocusMode = ReactiveCommand.Create<ProfileEditorFocusMode>(ExecuteChangeFocusMode);
CycleFocusMode = ReactiveCommand.Create(ExecuteCycleFocusMode, this.WhenAnyValue(vm => vm.KeyBindingsEnabled));
}
public ReactiveCommand<Unit, Unit> AddFolder { get; }
public ReactiveCommand<Unit, Unit> AddLayer { get; }
public ReactiveCommand<Unit, Unit> ToggleSuspended { get; }
public ReactiveCommand<Unit, Unit> ViewProperties { get; }
public ReactiveCommand<Unit, Unit> AdaptProfile { get; }
public ReactiveCommand<Unit, Unit> DeleteProfile { get; }
public ReactiveCommand<Unit, Unit> ExportProfile { get; }
public ReactiveCommand<Unit, Unit> DuplicateProfile { get; }
public ReactiveCommand<PluginSetting<bool>, Unit> ToggleBooleanSetting { get; }
public ReactiveCommand<string, Unit> OpenUri { get; }
public ReactiveCommand<Unit, Unit> ToggleSuspendedEditing { get; }
public ReactiveCommand<ProfileEditorFocusMode, Unit> ChangeFocusMode { get; }
public ReactiveCommand<Unit, Unit> CycleFocusMode { get; }
public ProfileConfiguration? ProfileConfiguration => _profileConfiguration?.Value;
public RenderProfileElement? ProfileElement => _profileElement?.Value;
public bool IsSuspended => _isSuspended?.Value ?? false;
public bool SuspendedEditing => _suspendedEditing?.Value ?? false;
public bool FocusNone => _focusNone?.Value ?? false;
public bool FocusFolder => _focusFolder?.Value ?? false;
public bool FocusSelection => _focusSelection?.Value ?? false;
public bool KeyBindingsEnabled => _keyBindingsEnabled?.Value ?? false;
public PluginSetting<bool> AutoSuspend => _settingsService.GetSetting("ProfileEditor.AutoSuspend", true);
public PluginSetting<bool> ShowDataModelValues => _settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false);
public PluginSetting<bool> ShowFullPaths => _settingsService.GetSetting("ProfileEditor.ShowFullPaths", false);
public PluginSetting<bool> AlwaysShowValues => _settingsService.GetSetting("ProfileEditor.AlwaysShowValues", true);
public PluginSetting<bool> AlwaysApplyDataBindings => _settingsService.GetSetting("ProfileEditor.AlwaysApplyDataBindings", false);
public PluginSetting<ProfileEditorFocusMode> FocusMode => _settingsService.GetSetting("ProfileEditor.FocusMode", ProfileEditorFocusMode.Folder);
private void ExecuteAddFolder()
{
if (ProfileConfiguration?.Profile == null)
return;
RenderProfileElement target = ProfileElement ?? ProfileConfiguration.Profile.GetRootFolder();
_profileEditorService.CreateAndAddFolder(target);
}
private void ExecuteAddLayer()
{
if (ProfileConfiguration?.Profile == null)
return;
RenderProfileElement target = ProfileElement ?? ProfileConfiguration.Profile.GetRootFolder();
_profileEditorService.CreateAndAddLayer(target);
}
private async Task ExecuteViewProperties()
{
if (ProfileConfiguration == null)
return;
await _windowService.ShowDialogAsync<ProfileConfigurationEditViewModel, ProfileConfiguration?>(ProfileConfiguration.Category, ProfileConfiguration);
}
private async Task ExecuteAdaptProfile()
{
if (ProfileConfiguration?.Profile == null)
return;
bool confirmed = await _windowService.ShowConfirmContentDialog(
"Adapt profile",
"Are you sure you want to adapt the profile to your current surface? Layer assignments may change and you will lose your undo/redo history."
);
if (!confirmed)
return;
_profileService.AdaptProfile(ProfileConfiguration.Profile);
_history?.Clear();
}
private void ExecuteToggleSuspended()
{
if (ProfileConfiguration == null)
return;
ProfileConfiguration.IsSuspended = !ProfileConfiguration.IsSuspended;
_profileService.SaveProfileCategory(ProfileConfiguration.Category);
}
private async Task ExecuteDeleteProfile()
{
if (ProfileConfiguration == null)
return;
if (!await _windowService.ShowConfirmContentDialog("Delete profile", "Are you sure you want to permanently delete this profile?"))
return;
if (_profileService.FocusProfile == ProfileConfiguration)
await _router.Navigate("home");
_profileService.RemoveProfileConfiguration(ProfileConfiguration);
}
private async Task ExecuteExportProfile()
{
if (ProfileConfiguration == null)
return;
// Might not cover everything but then the dialog will complain and that's good enough
string fileName = Path.GetInvalidFileNameChars().Aggregate(ProfileConfiguration.Name, (current, c) => current.Replace(c, '-'));
string? result = await _windowService.CreateSaveFileDialog()
.HavingFilter(f => f.WithExtension("zip").WithName("Artemis profile"))
.WithInitialFileName(fileName)
.ShowAsync();
if (result == null)
return;
try
{
await using Stream stream = await _profileService.ExportProfile(ProfileConfiguration);
await using FileStream fileStream = File.OpenWrite(result);
await stream.CopyToAsync(fileStream);
}
catch (Exception e)
{
_windowService.ShowExceptionDialog("Failed to export profile", e);
}
}
private async Task ExecuteDuplicateProfile()
{
if (ProfileConfiguration == null)
return;
await using Stream export = await _profileService.ExportProfile(ProfileConfiguration);
await _profileService.ImportProfile(export, ProfileConfiguration.Category, true, false, "copy");
}
private void ExecuteToggleSuspendedEditing()
{
_profileEditorService.ChangeSuspendedEditing(!SuspendedEditing);
}
private void ExecuteToggleBooleanSetting(PluginSetting<bool> setting)
{
setting.Value = !setting.Value;
setting.Save();
}
private void ExecuteCycleFocusMode()
{
if (FocusMode.Value == ProfileEditorFocusMode.None)
FocusMode.Value = ProfileEditorFocusMode.Folder;
else if (FocusMode.Value == ProfileEditorFocusMode.Folder)
FocusMode.Value = ProfileEditorFocusMode.Selection;
else if (FocusMode.Value == ProfileEditorFocusMode.Selection)
FocusMode.Value = ProfileEditorFocusMode.None;
FocusMode.Save();
_profileEditorService.ChangeFocusMode(FocusMode.Value);
}
private void ExecuteChangeFocusMode(ProfileEditorFocusMode focusMode)
{
FocusMode.Value = focusMode;
FocusMode.Save();
_profileEditorService.ChangeFocusMode(FocusMode.Value);
}
}
@@ -1,69 +0,0 @@
<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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:playback="clr-namespace:Artemis.UI.Screens.ProfileEditor.Playback"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="48"
x:Class="Artemis.UI.Screens.ProfileEditor.Playback.PlaybackView"
x:DataType="playback:PlaybackViewModel">
<Grid Margin="8 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="200"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="5">
<Button Classes="icon-button icon-button-large"
ToolTip.Tip="Play from start (Shift+Space)" Command="{CompiledBinding PlayFromStart}"
Focusable="False">
<avalonia:MaterialIcon Kind="StepForward" />
</Button>
<Button Classes="icon-button icon-button-large"
ToolTip.Tip="Toggle play/pause (Space)" Command="{CompiledBinding TogglePlay}" Focusable="False">
<StackPanel>
<avalonia:MaterialIcon Kind="Play" IsVisible="{CompiledBinding !Playing}" />
<avalonia:MaterialIcon Kind="Pause" IsVisible="{CompiledBinding Playing}" />
</StackPanel>
</Button>
<Button Classes="icon-button icon-button-large" ToolTip.Tip="Go to start" Command="{CompiledBinding GoToStart}" Focusable="False">
<avalonia:MaterialIcon Kind="SkipBackward" />
</Button>
<Button Classes="icon-button icon-button-large" ToolTip.Tip="Go to end" Command="{CompiledBinding GoToEnd}" Focusable="False">
<avalonia:MaterialIcon Kind="SkipForward" />
</Button>
<Button Classes="icon-button icon-button-large" ToolTip.Tip="Previous frame" Command="{CompiledBinding GoToPreviousFrame}" Focusable="False">
<avalonia:MaterialIcon Kind="SkipPrevious" />
</Button>
<Button Classes="icon-button icon-button-large" ToolTip.Tip="Next frame" Command="{CompiledBinding GoToNextFrame}" Focusable="False">
<avalonia:MaterialIcon Kind="SkipNext" />
</Button>
<ToggleButton Classes="icon-button icon-button-large"
IsChecked="{CompiledBinding Repeating, Mode=OneWay}"
Focusable="False"
Command="{CompiledBinding CycleRepeating}">
<ToolTip.Tip>
<StackPanel>
<StackPanel IsVisible="{CompiledBinding Repeating}">
<TextBlock Text="Repeat entire timeline"
IsVisible="{CompiledBinding RepeatTimeline}" />
<TextBlock Text="Repeat segment"
IsVisible="{CompiledBinding RepeatSegment}" />
</StackPanel>
<TextBlock IsVisible="{CompiledBinding !Repeating}">
Don't repeat the timeline
</TextBlock>
<TextBlock TextWrapping="Wrap">
This setting only applies to the editor and does not affect the repeat mode during normal profile playback
</TextBlock>
</StackPanel>
</ToolTip.Tip>
<Grid>
<avalonia:MaterialIcon Kind="Repeat" IsVisible="{CompiledBinding RepeatTimeline}" />
<avalonia:MaterialIcon Kind="RepeatOne" IsVisible="{CompiledBinding RepeatSegment}" />
</Grid>
</ToggleButton>
</StackPanel>
<TextBlock Grid.Column="1" Classes="h4" Height="32" Text="{CompiledBinding FormattedCurrentTime, FallbackValue=00.000}" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0" />
</Grid>
</UserControl>
@@ -1,13 +0,0 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Playback;
public partial class PlaybackView : ReactiveUserControl<PlaybackViewModel>
{
public PlaybackView()
{
InitializeComponent();
}
}
@@ -1,231 +0,0 @@
using System;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Timers;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Avalonia.Threading;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Playback;
public partial class PlaybackViewModel : ActivatableViewModelBase
{
private readonly IProfileEditorService _profileEditorService;
private readonly ISettingsService _settingsService;
private ObservableAsPropertyHelper<TimeSpan>? _currentTime;
private ObservableAsPropertyHelper<string?>? _formattedCurrentTime;
private ObservableAsPropertyHelper<bool>? _keyBindingsEnabled;
private DateTime _lastUpdate;
private ObservableAsPropertyHelper<bool>? _playing;
private RenderProfileElement? _profileElement;
[Notify] private bool _repeating;
[Notify] private bool _repeatSegment;
[Notify] private bool _repeatTimeline;
public PlaybackViewModel(IProfileEditorService profileEditorService, ISettingsService settingsService)
{
_profileEditorService = profileEditorService;
_settingsService = settingsService;
if (_settingsService.GetSetting("ProfileEditor.RepeatTimeline", true).Value)
{
_repeating = true;
_repeatTimeline = true;
}
else if (_settingsService.GetSetting("ProfileEditor.RepeatSegment", false).Value)
{
_repeating = true;
_repeatSegment = true;
}
else
{
_repeating = false;
_repeatTimeline = true;
}
this.WhenActivated(d =>
{
_profileEditorService.ProfileElement.Subscribe(e => _profileElement = e).DisposeWith(d);
_currentTime = _profileEditorService.Time.ToProperty(this, vm => vm.CurrentTime).DisposeWith(d);
_formattedCurrentTime = _profileEditorService.Time.Select(t => $"{Math.Floor(t.TotalSeconds):00}.{t.Milliseconds:000}").ToProperty(this, vm => vm.FormattedCurrentTime).DisposeWith(d);
_playing = _profileEditorService.Playing.ToProperty(this, vm => vm.Playing).DisposeWith(d);
_keyBindingsEnabled = Shared.UI.CurrentKeyBindingsEnabled.ToProperty(this, vm => vm.KeyBindingsEnabled).DisposeWith(d);
// Update timer
Timer updateTimer = new(TimeSpan.FromMilliseconds(16));
updateTimer.Elapsed += (_, _) => Update();
updateTimer.DisposeWith(d);
_profileEditorService.Playing.Subscribe(_ => _lastUpdate = DateTime.Now).DisposeWith(d);
_profileEditorService.Playing.Subscribe(p => updateTimer.Enabled = p).DisposeWith(d);
_lastUpdate = DateTime.MinValue;
Disposable.Create(() =>
{
_settingsService.GetSetting("ProfileEditor.RepeatTimeline", true).Value = _repeating && _repeatTimeline;
_settingsService.GetSetting("ProfileEditor.RepeatSegment", false).Value = _repeating && _repeatSegment;
}).DisposeWith(d);
});
PlayFromStart = ReactiveCommand.Create(ExecutePlayFromStart, this.WhenAnyValue(vm => vm.KeyBindingsEnabled));
TogglePlay = ReactiveCommand.Create(ExecuteTogglePlay, this.WhenAnyValue(vm => vm.KeyBindingsEnabled));
GoToStart = ReactiveCommand.Create(ExecuteGoToStart);
GoToEnd = ReactiveCommand.Create(ExecuteGoToEnd);
GoToPreviousFrame = ReactiveCommand.Create(ExecuteGoToPreviousFrame);
GoToNextFrame = ReactiveCommand.Create(ExecuteGoToNextFrame);
CycleRepeating = ReactiveCommand.Create(ExecuteCycleRepeating);
}
public TimeSpan CurrentTime => _currentTime?.Value ?? TimeSpan.Zero;
public string? FormattedCurrentTime => _formattedCurrentTime?.Value;
public bool Playing => _playing?.Value ?? false;
public bool KeyBindingsEnabled => _keyBindingsEnabled?.Value ?? false;
public ReactiveCommand<Unit, Unit> PlayFromStart { get; }
public ReactiveCommand<Unit, Unit> TogglePlay { get; }
public ReactiveCommand<Unit, Unit> GoToStart { get; }
public ReactiveCommand<Unit, Unit> GoToEnd { get; }
public ReactiveCommand<Unit, Unit> GoToPreviousFrame { get; }
public ReactiveCommand<Unit, Unit> GoToNextFrame { get; }
public ReactiveCommand<Unit, Unit> CycleRepeating { get; }
private void ExecutePlayFromStart()
{
ExecuteGoToStart();
if (!Playing)
_profileEditorService.Play();
}
private void ExecuteTogglePlay()
{
if (!Playing)
_profileEditorService.Play();
else
_profileEditorService.Pause();
}
private void ExecuteGoToStart()
{
_profileEditorService.ChangeTime(TimeSpan.Zero);
}
private void ExecuteGoToEnd()
{
if (_profileElement == null)
return;
_profileEditorService.ChangeTime(_profileElement.Timeline.EndSegmentEndPosition);
}
private void ExecuteGoToPreviousFrame()
{
if (_profileElement == null)
return;
double frameTime = 1000.0 / _settingsService.GetSetting("Core.TargetFrameRate", 30).Value;
double newTime = Math.Max(0, Math.Round((CurrentTime.TotalMilliseconds - frameTime) / frameTime) * frameTime);
_profileEditorService.ChangeTime(TimeSpan.FromMilliseconds(newTime));
}
private void ExecuteGoToNextFrame()
{
if (_profileElement == null)
return;
double frameTime = 1000.0 / _settingsService.GetSetting("Core.TargetFrameRate", 30).Value;
double newTime = Math.Round((CurrentTime.TotalMilliseconds + frameTime) / frameTime) * frameTime;
newTime = Math.Min(newTime, _profileElement.Timeline.EndSegmentEndPosition.TotalMilliseconds);
_profileEditorService.ChangeTime(TimeSpan.FromMilliseconds(newTime));
}
private void ExecuteCycleRepeating()
{
if (!Repeating)
{
RepeatTimeline = true;
RepeatSegment = false;
Repeating = true;
}
else if (RepeatTimeline)
{
RepeatTimeline = false;
RepeatSegment = true;
this.RaisePropertyChanged(nameof(Repeating));
}
else if (RepeatSegment)
{
RepeatTimeline = true;
RepeatSegment = false;
Repeating = false;
}
}
private TimeSpan GetCurrentSegmentStart()
{
if (_profileElement == null)
return TimeSpan.Zero;
if (CurrentTime < _profileElement.Timeline.StartSegmentEndPosition)
return TimeSpan.Zero;
if (CurrentTime < _profileElement.Timeline.MainSegmentEndPosition)
return _profileElement.Timeline.MainSegmentStartPosition;
if (CurrentTime < _profileElement.Timeline.EndSegmentEndPosition)
return _profileElement.Timeline.EndSegmentStartPosition;
return TimeSpan.Zero;
}
private TimeSpan GetCurrentSegmentEnd()
{
if (_profileElement == null)
return TimeSpan.Zero;
if (CurrentTime < _profileElement.Timeline.StartSegmentEndPosition)
return _profileElement.Timeline.StartSegmentEndPosition;
if (CurrentTime < _profileElement.Timeline.MainSegmentEndPosition)
return _profileElement.Timeline.MainSegmentEndPosition;
if (CurrentTime < _profileElement.Timeline.EndSegmentEndPosition)
return _profileElement.Timeline.EndSegmentEndPosition;
return TimeSpan.Zero;
}
private void Update()
{
try
{
if (_lastUpdate == DateTime.MinValue)
_lastUpdate = DateTime.Now;
TimeSpan newTime = CurrentTime.Add(DateTime.Now - _lastUpdate);
if (_profileElement != null)
{
if (Repeating && RepeatTimeline)
{
if (newTime > _profileElement.Timeline.Length)
newTime = TimeSpan.Zero;
}
else if (Repeating && RepeatSegment)
{
if (newTime > GetCurrentSegmentEnd())
newTime = GetCurrentSegmentStart();
}
else if (newTime > _profileElement.Timeline.Length)
{
newTime = _profileElement.Timeline.Length;
_profileEditorService.Pause();
}
}
Dispatcher.UIThread.Invoke(() => _profileEditorService.ChangeTime(newTime));
}
finally
{
_lastUpdate = DateTime.Now;
}
}
}
@@ -1,184 +0,0 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
using Avalonia.Xaml.Interactions.DragAndDrop;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.Behaviors;
public class ProfileTreeViewDropHandler : DropHandlerBase
{
public override bool Validate(object? sender, DragEventArgs e, object? sourceContext, object? targetContext, object? state)
{
if (e.Source is Control && sender is TreeView treeView)
return Validate<TreeItemViewModel>(treeView, e, sourceContext, targetContext, false);
return false;
}
public override bool Execute(object? sender, DragEventArgs e, object? sourceContext, object? targetContext, object? state)
{
bool result = false;
if (e.Source is Control && sender is TreeView treeView)
result = Validate<TreeItemViewModel>(treeView, e, sourceContext, targetContext, true);
if (sender is ItemsControl itemsControl)
foreach (TreeViewItem treeViewItem in GetFlattenedTreeView(itemsControl))
SetDraggingPseudoClasses(treeViewItem, TreeDropType.None);
return result;
}
public override void Cancel(object? sender, RoutedEventArgs e)
{
if (sender is ItemsControl itemsControl)
foreach (TreeViewItem treeViewItem in GetFlattenedTreeView(itemsControl))
SetDraggingPseudoClasses(treeViewItem, TreeDropType.None);
base.Cancel(sender, e);
}
private bool Validate<T>(TreeView treeView, DragEventArgs e, object? sourceContext, object? targetContext, bool bExecute) where T : TreeItemViewModel
{
Point position = e.GetPosition(treeView);
TreeViewItem? targetVisual = treeView.GetVisualAt(position).FindAncestorOfType<TreeViewItem>();
if (sourceContext is not T sourceNode || targetContext is not ProfileTreeViewModel vm || targetVisual is not Control {DataContext: T targetNode})
return false;
if (bExecute && targetNode == sourceNode)
return false;
TreeItemViewModel? sourceParent = sourceNode.Parent;
TreeItemViewModel? targetParent = targetNode.Parent;
ObservableCollection<TreeItemViewModel> sourceNodes = sourceParent is { } ? sourceParent.Children : vm.Children;
ObservableCollection<TreeItemViewModel> targetNodes = targetParent is { } ? targetParent.Children : vm.Children;
int sourceIndex = sourceNodes.IndexOf(sourceNode);
int targetIndex = targetNodes.IndexOf(targetNode);
// Update the target index according to the position
TreeDropType dropType = TreeDropType.Before;
if (!targetNode.SupportsChildren)
{
position = e.GetPosition(targetVisual);
if (position.Y > targetVisual.Bounds.Height / 2)
dropType = TreeDropType.After;
}
else
{
Visual? header = targetVisual.GetVisualDescendants().FirstOrDefault(d => d is Border b && b.Name == "PART_LayoutRoot");
if (header != null)
{
position = e.GetPosition(header);
double segments = header.Bounds.Height / 3.0;
if (position.Y > segments * 2)
dropType = TreeDropType.After;
else if (position.Y > segments)
dropType = TreeDropType.Into;
}
}
if (dropType == TreeDropType.After)
targetIndex += 1;
else if (dropType == TreeDropType.Into)
targetIndex = 0;
TreeItemViewModel? currentParent = targetNode.Parent;
while (currentParent != null)
{
if (currentParent == sourceNode)
return false;
currentParent = currentParent.Parent;
}
if (sourceIndex < 0 || targetIndex < 0)
return false;
if (e.DragEffects != DragDropEffects.Move)
return false;
foreach (TreeViewItem treeViewItem in GetFlattenedTreeView(treeView))
SetDraggingPseudoClasses(treeViewItem, TreeDropType.None);
if (bExecute)
{
if (dropType == TreeDropType.Into)
{
targetNode.InsertElement(sourceNode, 0);
}
else
{
if (sourceParent == targetParent && sourceIndex < targetIndex)
targetIndex--;
if (targetNode.Parent != null)
targetNode.Parent.InsertElement(sourceNode, targetIndex);
}
}
else
{
SetDraggingPseudoClasses(targetVisual, dropType);
}
return true;
}
private List<TreeViewItem> GetFlattenedTreeView(ItemsControl currentNode)
{
List<TreeViewItem> result = new();
foreach (Control containerControl in currentNode.GetRealizedContainers())
{
if (containerControl is TreeViewItem treeViewItem && containerControl.DataContext is TreeItemViewModel)
{
result.Add(treeViewItem);
if (treeViewItem.ItemCount > 0)
result.AddRange(GetFlattenedTreeView(treeViewItem));
}
}
return result;
}
private void SetDraggingPseudoClasses(TreeViewItem control, TreeDropType type)
{
if (type == TreeDropType.None)
{
((IPseudoClasses) control.Classes).Remove(":dragging");
((IPseudoClasses) control.Classes).Remove(":dragging-before");
((IPseudoClasses) control.Classes).Remove(":dragging-after");
((IPseudoClasses) control.Classes).Remove(":dragging-into");
}
else
{
((IPseudoClasses) control.Classes).Add(":dragging");
if (type == TreeDropType.Before)
{
((IPseudoClasses) control.Classes).Add(":dragging-before");
((IPseudoClasses) control.Classes).Remove(":dragging-after");
((IPseudoClasses) control.Classes).Remove(":dragging-into");
}
else if (type == TreeDropType.After)
{
((IPseudoClasses) control.Classes).Remove(":dragging-before");
((IPseudoClasses) control.Classes).Add(":dragging-after");
((IPseudoClasses) control.Classes).Remove(":dragging-into");
}
else if (type == TreeDropType.Into)
{
((IPseudoClasses) control.Classes).Remove(":dragging-before");
((IPseudoClasses) control.Classes).Remove(":dragging-after");
((IPseudoClasses) control.Classes).Add(":dragging-into");
}
}
}
private enum TreeDropType
{
Before,
After,
Into,
None
}
}
@@ -1,12 +0,0 @@
<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:contentDialogs="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileTree.ContentDialogs"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.ProfileTree.ContentDialogs.ProfileElementRenameView"
x:DataType="contentDialogs:ProfileElementRenameViewModel">
<StackPanel>
<TextBox Name="NameTextBox" Text="{CompiledBinding ProfileElementName}" Watermark="Profile element name" />
</StackPanel>
</UserControl>
@@ -1,31 +0,0 @@
using System.Threading.Tasks;
using Artemis.UI.Shared.Extensions;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using Avalonia.Threading;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.ContentDialogs;
public partial class ProfileElementRenameView : ReactiveUserControl<ProfileElementRenameViewModel>
{
public ProfileElementRenameView()
{
InitializeComponent();
this.WhenActivated(_ =>
{
this.ClearAllDataValidationErrors();
Dispatcher.UIThread.Post(DelayedAutoFocus);
});
}
private async void DelayedAutoFocus()
{
// Don't ask
await Task.Delay(200);
NameTextBox.SelectAll();
NameTextBox.Focus();
}
}
@@ -1,39 +0,0 @@
using System.Reactive;
using Artemis.Core;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using FluentAvalonia.UI.Controls;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using ReactiveUI.Validation.Extensions;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.ContentDialogs;
public partial class ProfileElementRenameViewModel : ContentDialogViewModelBase
{
private readonly IProfileEditorService _profileEditorService;
private readonly ProfileElement _profileElement;
[Notify] private string? _profileElementName;
public ProfileElementRenameViewModel(IProfileEditorService profileEditorService, ProfileElement profileElement)
{
_profileEditorService = profileEditorService;
_profileElement = profileElement;
_profileElementName = profileElement.Name;
Confirm = ReactiveCommand.Create(ExecuteConfirm, ValidationContext.Valid);
this.ValidationRule(vm => vm.ProfileElementName, name => !string.IsNullOrWhiteSpace(name), "You must specify a valid name");
}
public ReactiveCommand<Unit, Unit> Confirm { get; }
private void ExecuteConfirm()
{
if (ProfileElementName == null)
return;
_profileEditorService.ExecuteCommand(new RenameProfileElement(_profileElement, ProfileElementName));
ContentDialog?.Hide(ContentDialogResult.Primary);
}
}
@@ -1,25 +0,0 @@
using System.Reactive;
using Artemis.Core;
using Artemis.UI.Shared;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints;
public abstract class AdaptionHintViewModelBase : ViewModelBase
{
protected AdaptionHintViewModelBase(Layer layer, IAdaptionHint adaptionHint)
{
Layer = layer;
AdaptionHint = adaptionHint;
Remove = ReactiveCommand.Create(ExecuteRemove);
}
public Layer Layer { get; }
public IAdaptionHint AdaptionHint { get; }
public ReactiveCommand<Unit, Unit> Remove { get; }
private void ExecuteRemove()
{
Layer.Adapter.Remove(AdaptionHint);
}
}
@@ -1,52 +0,0 @@
<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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:adaptionHints="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints.CategoryAdaptionHintView"
x:DataType="adaptionHints:CategoryAdaptionHintViewModel">
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,Auto">
<StackPanel Grid.ColumnSpan="2" Margin="0 0 0 5">
<TextBlock Classes="h5">Category hint</TextBlock>
<TextBlock Classes="subtitle">Applies the layer to devices of a certain category</TextBlock>
</StackPanel>
<Button Grid.Row="0"
Grid.Column="1"
Classes="icon-button"
VerticalAlignment="Top"
HorizontalAlignment="Right"
ToolTip.Tip="Remove hint"
Command="{CompiledBinding Remove}">
<avalonia:MaterialIcon Kind="Delete" />
</Button>
<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Horizontal" Spacing="5">
<shared:EnumComboBox Value="{CompiledBinding CategoryAdaptionHint.Category}" Width="200" Margin="0 0 10 0" />
<TextBlock VerticalAlignment="Center">Skip</TextBlock>
<controls:NumberBox HorizontalAlignment="Stretch"
Value="{CompiledBinding CategoryAdaptionHint.Skip}"
Minimum="0" />
<TextBlock VerticalAlignment="Center">device(s)</TextBlock>
<TextBlock VerticalAlignment="Center">Take</TextBlock>
<controls:NumberBox IsEnabled="{CompiledBinding CategoryAdaptionHint.LimitAmount}"
HorizontalAlignment="Stretch"
Value="{CompiledBinding CategoryAdaptionHint.Amount}"
Minimum="1" />
<TextBlock VerticalAlignment="Center">device(s)</TextBlock>
</StackPanel>
<CheckBox Grid.Row="1"
Grid.Column="1"
Content="Take all devices"
VerticalAlignment="Center"
HorizontalAlignment="Right"
IsChecked="{CompiledBinding !CategoryAdaptionHint.LimitAmount}" />
</Grid>
</UserControl>
@@ -1,13 +0,0 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints;
public partial class CategoryAdaptionHintView : UserControl
{
public CategoryAdaptionHintView()
{
InitializeComponent();
}
}
@@ -1,13 +0,0 @@
using Artemis.Core;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints;
public class CategoryAdaptionHintViewModel : AdaptionHintViewModelBase
{
public CategoryAdaptionHintViewModel(Layer layer, CategoryAdaptionHint adaptionHint) : base(layer, adaptionHint)
{
CategoryAdaptionHint = adaptionHint;
}
public CategoryAdaptionHint CategoryAdaptionHint { get; }
}
@@ -1,52 +0,0 @@
<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:adaptionHints="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints.DeviceAdaptionHintView"
x:DataType="adaptionHints:DeviceAdaptionHintViewModel">
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,Auto">
<StackPanel Grid.ColumnSpan="2" Margin="0 0 0 5">
<TextBlock Classes="h5">Device type hint</TextBlock>
<TextBlock Classes="subtitle">Applies the layer to devices of a certain type</TextBlock>
</StackPanel>
<Button Grid.Row="0"
Grid.Column="1"
Classes="icon-button"
VerticalAlignment="Top"
HorizontalAlignment="Right"
ToolTip.Tip="Remove hint"
Command="{CompiledBinding Remove}">
<avalonia:MaterialIcon Kind="Delete" />
</Button>
<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Horizontal" Spacing="5">
<shared:EnumComboBox Value="{CompiledBinding DeviceAdaptionHint.DeviceType}" Width="200" Margin="0 0 10 0" />
<TextBlock VerticalAlignment="Center">Skip</TextBlock>
<controls:NumberBox HorizontalAlignment="Stretch"
Value="{CompiledBinding DeviceAdaptionHint.Skip}"
Minimum="0" />
<TextBlock VerticalAlignment="Center">device(s)</TextBlock>
<TextBlock VerticalAlignment="Center">Take</TextBlock>
<controls:NumberBox IsEnabled="{CompiledBinding DeviceAdaptionHint.LimitAmount}"
HorizontalAlignment="Stretch"
Value="{CompiledBinding DeviceAdaptionHint.Amount}"
Minimum="1" />
<TextBlock VerticalAlignment="Center">device(s)</TextBlock>
</StackPanel>
<CheckBox Grid.Row="1"
Grid.Column="1"
Content="Take all devices"
VerticalAlignment="Center"
HorizontalAlignment="Right"
IsChecked="{CompiledBinding !DeviceAdaptionHint.LimitAmount}" />
</Grid>
</UserControl>
@@ -1,13 +0,0 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints;
public partial class DeviceAdaptionHintView : UserControl
{
public DeviceAdaptionHintView()
{
InitializeComponent();
}
}
@@ -1,13 +0,0 @@
using Artemis.Core;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints;
public class DeviceAdaptionHintViewModel : AdaptionHintViewModelBase
{
public DeviceAdaptionHintViewModel(Layer layer, DeviceAdaptionHint adaptionHint) : base(layer, adaptionHint)
{
DeviceAdaptionHint = adaptionHint;
}
public DeviceAdaptionHint DeviceAdaptionHint { get; }
}
@@ -1,31 +0,0 @@
<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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
xmlns:adaptionHints="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints.KeyboardSectionAdaptionHintView"
x:DataType="adaptionHints:KeyboardSectionAdaptionHintViewModel">
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,Auto">
<StackPanel Grid.ColumnSpan="2" Margin="0 0 0 5">
<TextBlock Classes="h5">Keyboard section hint</TextBlock>
<TextBlock Classes="subtitle">Applies the layer to a section of all keyboards</TextBlock>
</StackPanel>
<Button Grid.Row="0"
Grid.Column="1"
Classes="icon-button"
VerticalAlignment="Top"
HorizontalAlignment="Right"
ToolTip.Tip="Remove hint"
Command="{CompiledBinding Remove}">
<avalonia:MaterialIcon Kind="Delete" />
</Button>
<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Horizontal" Spacing="5">
<shared:EnumComboBox Value="{CompiledBinding KeyboardSectionAdaptionHint.Section}" Width="200" Margin="0 0 10 0" />
</StackPanel>
</Grid>
</UserControl>
@@ -1,13 +0,0 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints;
public partial class KeyboardSectionAdaptionHintView : UserControl
{
public KeyboardSectionAdaptionHintView()
{
InitializeComponent();
}
}
@@ -1,13 +0,0 @@
using Artemis.Core;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints;
public class KeyboardSectionAdaptionHintViewModel : AdaptionHintViewModelBase
{
public KeyboardSectionAdaptionHintViewModel(Layer layer, KeyboardSectionAdaptionHint adaptionHint) : base(layer, adaptionHint)
{
KeyboardSectionAdaptionHint = adaptionHint;
}
public KeyboardSectionAdaptionHint KeyboardSectionAdaptionHint { get; }
}
@@ -1,57 +0,0 @@
<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:adaptionHints="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints.SingleLedAdaptionHintView"
x:DataType="adaptionHints:SingleLedAdaptionHintViewModel">
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,Auto">
<StackPanel Grid.ColumnSpan="2" Margin="0 0 0 5">
<TextBlock Classes="h5">Single LED hint</TextBlock>
<TextBlock Classes="subtitle">Applies the layer to a single LED of one or more devices</TextBlock>
</StackPanel>
<Button Grid.Row="0"
Grid.Column="1"
Classes="icon-button"
VerticalAlignment="Top"
HorizontalAlignment="Right"
ToolTip.Tip="Remove hint"
Command="{CompiledBinding Remove}">
<avalonia:MaterialIcon Kind="Delete" />
</Button>
<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Horizontal" Spacing="5">
<controls:FAComboBox Width="200"
Margin="0 0 10 0"
ItemsSource="{CompiledBinding AdaptionLeds}"
SelectedItem="{CompiledBinding SelectedLed}"
DisplayMemberBinding="{CompiledBinding Description}"
IsEditable="True"
IsTextSearchEnabled="True"></controls:FAComboBox>
<TextBlock VerticalAlignment="Center">Skip</TextBlock>
<controls:NumberBox HorizontalAlignment="Stretch"
Value="{CompiledBinding SingleLedAdaptionHint.Skip}"
Minimum="0" />
<TextBlock VerticalAlignment="Center">device(s)</TextBlock>
<TextBlock VerticalAlignment="Center">Take</TextBlock>
<controls:NumberBox IsEnabled="{CompiledBinding SingleLedAdaptionHint.LimitAmount}"
HorizontalAlignment="Stretch"
Value="{CompiledBinding SingleLedAdaptionHint.Amount}"
Minimum="1" />
<TextBlock VerticalAlignment="Center">device(s)</TextBlock>
</StackPanel>
<CheckBox Grid.Row="1"
Grid.Column="1"
Content="Take all devices"
VerticalAlignment="Center"
HorizontalAlignment="Right"
IsChecked="{CompiledBinding !SingleLedAdaptionHint.LimitAmount}" />
</Grid>
</UserControl>
@@ -1,13 +0,0 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints;
public partial class SingleLedAdaptionHintView : UserControl
{
public SingleLedAdaptionHintView()
{
InitializeComponent();
}
}
@@ -1,42 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Artemis.Core;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using RGB.NET.Core;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints;
public partial class SingleLedAdaptionHintViewModel : AdaptionHintViewModelBase
{
[Notify] private List<AdaptionLed> _adaptionLeds = new();
[Notify] private AdaptionLed? _selectedLed;
public SingleLedAdaptionHintViewModel(Layer layer, SingleLedAdaptionHint adaptionHint) : base(layer, adaptionHint)
{
SingleLedAdaptionHint = adaptionHint;
this.WhenAnyValue(vm => vm.SelectedLed).WhereNotNull().Subscribe(l => SingleLedAdaptionHint.LedId = l.Value);
Task.Run(() =>
{
AdaptionLeds = Enum.GetValues<LedId>().Select(l => new AdaptionLed(l)).ToList();
SelectedLed = AdaptionLeds.FirstOrDefault(l => l.Value == adaptionHint.LedId);
});
}
public SingleLedAdaptionHint SingleLedAdaptionHint { get; }
}
public class AdaptionLed
{
public AdaptionLed(LedId led)
{
Value = led;
Description = led.ToString();
}
public LedId Value { get; }
public string Description { get; }
}
@@ -1,101 +0,0 @@
<windowing:AppWindow 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:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:dialogs="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:windowing="clr-namespace:FluentAvalonia.UI.Windowing;assembly=FluentAvalonia"
mc:Ignorable="d"
x:Class="Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.LayerHintsDialogView"
x:DataType="dialogs:LayerHintsDialogViewModel"
WindowStartupLocation="CenterOwner"
Icon="/Assets/Images/Logo/application.ico"
Title="Artemis | Adaption hints"
Width="750"
Height="800">
<Grid Margin="15" RowDefinitions="Auto,*,Auto">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" RowDefinitions="Auto,Auto">
<StackPanel Grid.Row="0" Orientation="Horizontal">
<avalonia:MaterialIcon Kind="{CompiledBinding Layer.LayerBrush.Descriptor.Icon}" Width="24" Height="24" Margin="0 0 5 10" />
<TextBlock Classes="h4" Text="{CompiledBinding Layer.Name}" TextWrapping="Wrap" />
</StackPanel>
<HyperlinkButton Grid.Row="0"
Grid.Column="1"
VerticalAlignment="Top"
NavigateUri="https://wiki.artemis-rgb.com/guides/user/profiles/layers/adaption-hints?mtm_campaign=artemis&amp;mtm_kwd=profile-editor">
Learn more about adaption hints
</HyperlinkButton>
<TextBlock Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Classes="subtitle" TextWrapping="Wrap">
Add hints below to help decide where to place this layer when the profile is imported.
</TextBlock>
</Grid>
<Panel Grid.Row="1" Classes="card" Margin="0 15">
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Hidden" IsVisible="{CompiledBinding AdaptionHints.Count}">
<ItemsControl ItemsSource="{CompiledBinding AdaptionHints}" Classes="adaption-hints">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Spacing="5" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Classes="card-condensed">
<ContentControl Content="{CompiledBinding}"></ContentControl>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<StackPanel IsVisible="{CompiledBinding !AdaptionHints.Count}" VerticalAlignment="Center" HorizontalAlignment="Center">
<avalonia:MaterialIcon Kind="AlertCircleOutline" HorizontalAlignment="Center" Width="48" Height="48" />
<TextBlock Classes="h4" TextAlignment="Center" TextWrapping="Wrap">
You haven't set up any adaption hints
</TextBlock>
<TextBlock Classes="subtitle" TextAlignment="Center" TextWrapping="Wrap">
Artemis will attempt to directly map the LEDs of this layer to different surfaces but results may vary.
</TextBlock>
</StackPanel>
</Panel>
<Grid Grid.Row="2" ColumnDefinitions="*,Auto">
<Button Grid.Row="0" Grid.Column="0" Command="{CompiledBinding AutoDetermineHints}">Auto-determine hints</Button>
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" Spacing="5">
<DropDownButton>
<DropDownButton.Flyout>
<MenuFlyout Placement="Top">
<MenuItem Header="Category hint" Command="{CompiledBinding AddCategoryHint}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Desk" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Device type hint" Command="{CompiledBinding AddDeviceHint}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Devices" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Keyboard-section hint" Command="{CompiledBinding AddKeyboardSectionHint}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Keyboard" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Single LED hint" Command="{CompiledBinding AddSingleLedHint}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="LedVariantOn" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</DropDownButton.Flyout>
Add hint
</DropDownButton>
<Button Command="{CompiledBinding Finish}">Close</Button>
</StackPanel>
</Grid>
</Grid>
</windowing:AppWindow>
@@ -1,17 +0,0 @@
using Artemis.UI.Shared;
using Avalonia;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs;
public partial class LayerHintsDialogView : ReactiveAppWindow<LayerHintsDialogViewModel>
{
public LayerHintsDialogView()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
}
@@ -1,92 +0,0 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints;
using Artemis.UI.Shared;
using DynamicData;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs;
public class LayerHintsDialogViewModel : DialogViewModelBase<bool>
{
private readonly IDeviceService _deviceService;
private readonly ILayerHintVmFactory _vmFactory;
public LayerHintsDialogViewModel(Layer layer, IDeviceService deviceService, ILayerHintVmFactory vmFactory)
{
_deviceService = deviceService;
_vmFactory = vmFactory;
Layer = layer;
AdaptionHints = new ObservableCollection<AdaptionHintViewModelBase>();
this.WhenActivated(d =>
{
Observable.FromEventPattern<LayerAdapterHintEventArgs>(x => layer.Adapter.AdapterHintAdded += x, x => layer.Adapter.AdapterHintAdded -= x)
.Subscribe(c => AdaptionHints.Add(CreateHintViewModel(c.EventArgs.AdaptionHint)))
.DisposeWith(d);
Observable.FromEventPattern<LayerAdapterHintEventArgs>(x => layer.Adapter.AdapterHintRemoved += x, x => layer.Adapter.AdapterHintRemoved -= x)
.Subscribe(c => AdaptionHints.RemoveMany(AdaptionHints.Where(h => h.AdaptionHint == c.EventArgs.AdaptionHint)))
.DisposeWith(d);
AdaptionHints.AddRange(Layer.Adapter.AdaptionHints.Select(CreateHintViewModel));
});
}
public Layer Layer { get; }
public ObservableCollection<AdaptionHintViewModelBase> AdaptionHints { get; }
public void Finish()
{
Close(true);
}
public void AutoDetermineHints()
{
Layer.Adapter.DetermineHints(_deviceService.EnabledDevices);
}
public void AddCategoryHint()
{
Layer.Adapter.Add(new CategoryAdaptionHint());
}
public void AddDeviceHint()
{
Layer.Adapter.Add(new DeviceAdaptionHint());
}
public void AddKeyboardSectionHint()
{
Layer.Adapter.Add(new KeyboardSectionAdaptionHint());
}
public void AddSingleLedHint()
{
Layer.Adapter.Add(new SingleLedAdaptionHint());
}
public void RemoveAdaptionHint(IAdaptionHint hint)
{
Layer.Adapter.Remove(hint);
}
private AdaptionHintViewModelBase CreateHintViewModel(IAdaptionHint hint)
{
return hint switch
{
CategoryAdaptionHint categoryAdaptionHint => _vmFactory.CategoryAdaptionHintViewModel(Layer, categoryAdaptionHint),
DeviceAdaptionHint deviceAdaptionHint => _vmFactory.DeviceAdaptionHintViewModel(Layer, deviceAdaptionHint),
KeyboardSectionAdaptionHint keyboardSectionAdaptionHint => _vmFactory.KeyboardSectionAdaptionHintViewModel(Layer, keyboardSectionAdaptionHint),
SingleLedAdaptionHint singleLedAdaptionHint => _vmFactory.SingleLedAdaptionHintViewModel(Layer, singleLedAdaptionHint),
_ => throw new ArgumentOutOfRangeException(nameof(hint))
};
}
}
@@ -1,42 +0,0 @@
<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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:profileTree="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileTree"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.ProfileTree.FolderTreeItemView"
x:DataType="profileTree:FolderTreeItemViewModel">
<Grid ColumnDefinitions="Auto,Auto,*,Auto,Auto">
<HyperlinkButton Grid.Column="0"
Classes="icon-button icon-button-small broken-state-button"
Margin="0 0 5 0"
Command="{CompiledBinding ShowBrokenStateExceptions}"
IsVisible="{CompiledBinding ProfileElement.BrokenState, Converter={x:Static ObjectConverters.IsNotNull}}"
ToolTip.Tip="{CompiledBinding ProfileElement.BrokenState, FallbackValue=''}">
<avalonia:MaterialIcon Kind="AlertCircle" />
</HyperlinkButton>
<avalonia:MaterialIcon Grid.Column="1"
Kind="Folder"
Margin="0 0 5 0"
IsVisible="{CompiledBinding !IsExpanded, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TreeViewItem}}}" />
<avalonia:MaterialIcon Grid.Column="1"
Kind="FolderOpen"
Margin="0 0 5 0"
IsVisible="{CompiledBinding IsExpanded, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TreeViewItem}}}" />
<TextBlock Grid.Column="2" Text="{CompiledBinding Folder.Name}" VerticalAlignment="Center" />
<avalonia:MaterialIcon Grid.Column="3" Kind="Eye" IsVisible="{CompiledBinding IsFocused}" ToolTip.Tip="This element is visible because of the current focus mode" Margin="0 0 5 0"/>
<ToggleButton Grid.Column="4"
Classes="icon-button icon-button-small"
ToolTip.Tip="Toggle suspended state"
IsChecked="{CompiledBinding Folder.Suspended}"
Focusable="False"
VerticalAlignment="Center"
Margin="4 0">
<avalonia:MaterialIcon Kind="Pause" />
</ToggleButton>
</Grid>
</UserControl>
@@ -1,11 +0,0 @@
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree;
public partial class FolderTreeItemView : ReactiveUserControl<FolderTreeItemViewModel>
{
public FolderTreeItemView()
{
InitializeComponent();
}
}
@@ -1,85 +0,0 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.Storage.Entities.Profile;
using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Extensions;
using Artemis.UI.Models;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree;
public class FolderTreeItemViewModel : TreeItemViewModel
{
private readonly IDeviceService _deviceService;
public FolderTreeItemViewModel(TreeItemViewModel? parent,
Folder folder,
IWindowService windowService,
IDeviceService deviceService,
IProfileEditorService profileEditorService,
IProfileEditorVmFactory profileEditorVmFactory)
: base(parent, folder, windowService, deviceService, profileEditorService, profileEditorVmFactory)
{
_deviceService = deviceService;
Folder = folder;
}
public Folder Folder { get; }
#region Overrides of TreeItemViewModel
protected override async Task ExecuteDuplicate()
{
await ProfileEditorService.SaveProfileAsync();
FolderClipboardModel copy = CoreJson.Deserialize<FolderClipboardModel>(CoreJson.Serialize(new FolderClipboardModel(Folder)))!;
Folder copied = copy.Paste(Folder.Profile, Folder.Parent);
ProfileEditorService.ExecuteCommand(new AddProfileElement(copied, Folder.Parent, Folder.Order - 1));
}
protected override async Task ExecuteCopy()
{
await ProfileEditorService.SaveProfileAsync();
await Folder.CopyToClipboard();
}
protected override async Task ExecutePaste()
{
if (Folder.Parent is not Folder parent)
return;
RenderProfileElement? pasted = await parent.PasteChildFromClipboard();
if (pasted == null)
return;
// If the target contains an element with the same name, affix with " - copy"
if (parent.Children.Any(c => c.Name == pasted.Name))
pasted.Name = parent.GetNewFolderName(pasted.Name + " - copy");
ProfileEditorService.ExecuteCommand(new AddProfileElement(pasted, parent, Folder.Order - 1));
Folder.Profile.PopulateLeds(_deviceService.EnabledDevices);
}
private async Task ExecutePasteInto()
{
RenderProfileElement? pasted = await Folder.PasteChildFromClipboard();
if (pasted == null)
return;
// If the target contains an element with the same name, affix with " - copy"
if (Folder.Children.Any(c => c.Name == pasted.Name))
pasted.Name = Folder.GetNewFolderName(pasted.Name + " - copy");
ProfileEditorService.ExecuteCommand(new AddProfileElement(pasted, Folder, 0));
}
/// <inheritdoc />
public override bool SupportsChildren => true;
#endregion
}
@@ -1,35 +0,0 @@
<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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:profileTree="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileTree"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.ProfileTree.LayerTreeItemView"
x:DataType="profileTree:LayerTreeItemViewModel">
<Grid ColumnDefinitions="Auto,Auto,*,Auto,Auto">
<HyperlinkButton Grid.Column="0"
Classes="icon-button icon-button-small broken-state-button"
Margin="0 0 5 0"
Command="{CompiledBinding ShowBrokenStateExceptions}"
IsVisible="{CompiledBinding ProfileElement.BrokenState, Converter={x:Static ObjectConverters.IsNotNull}}"
ToolTip.Tip="{CompiledBinding ProfileElement.BrokenState, FallbackValue=''}">
<avalonia:MaterialIcon Kind="AlertCircle" />
</HyperlinkButton>
<avalonia:MaterialIcon Grid.Column="1" Kind="{CompiledBinding Layer.LayerBrush.Descriptor.Icon, FallbackValue=Layers}" Margin="0 0 5 0" />
<TextBlock Grid.Column="2" Text="{CompiledBinding Layer.Name}" VerticalAlignment="Center" />
<avalonia:MaterialIcon Grid.Column="3" Kind="Eye" IsVisible="{CompiledBinding IsFocused}" ToolTip.Tip="This element is visible because of the current focus mode" Margin="0 0 5 0"/>
<ToggleButton Grid.Column="4"
Classes="icon-button icon-button-small"
ToolTip.Tip="Toggle suspended state"
IsChecked="{CompiledBinding Layer.Suspended}"
Focusable="False"
VerticalAlignment="Center"
Margin="4 0">
<avalonia:MaterialIcon Kind="Pause" />
</ToggleButton>
</Grid>
</UserControl>
@@ -1,11 +0,0 @@
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree;
public partial class LayerTreeItemView : ReactiveUserControl<LayerTreeItemViewModel>
{
public LayerTreeItemView()
{
InitializeComponent();
}
}
@@ -1,75 +0,0 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.Storage.Entities.Profile;
using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Extensions;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree;
public class LayerTreeItemViewModel : TreeItemViewModel
{
private readonly IDeviceService _deviceService;
public LayerTreeItemViewModel(TreeItemViewModel? parent,
Layer layer,
IWindowService windowService,
IProfileEditorService profileEditorService,
IDeviceService deviceService,
IProfileEditorVmFactory profileEditorVmFactory)
: base(parent, layer, windowService, deviceService, profileEditorService, profileEditorVmFactory)
{
_deviceService = deviceService;
Layer = layer;
}
public Layer Layer { get; }
#region Overrides of TreeItemViewModel
/// <inheritdoc />
protected override async Task ExecuteDuplicate()
{
await ProfileEditorService.SaveProfileAsync();
LayerEntity copy = CoreJson.Deserialize<LayerEntity>(CoreJson.Serialize(Layer.LayerEntity))!;
copy.Id = Guid.NewGuid();
copy.Name = Layer.Parent.GetNewFolderName(copy.Name + " - copy");
Layer copied = new(Layer.Profile, Layer.Parent, copy, true);
ProfileEditorService.ExecuteCommand(new AddProfileElement(copied, Layer.Parent, Layer.Order - 1));
Layer.Profile.PopulateLeds(_deviceService.EnabledDevices);
}
protected override async Task ExecuteCopy()
{
await ProfileEditorService.SaveProfileAsync();
await Layer.CopyToClipboard();
}
protected override async Task ExecutePaste()
{
if (Layer.Parent is not Folder parent)
return;
RenderProfileElement? pasted = await parent.PasteChildFromClipboard();
if (pasted == null)
return;
// If the target contains an element with the same name, affix with " - copy"
if (parent.Children.Any(c => c.Name == pasted.Name))
pasted.Name = parent.GetNewLayerName(pasted.Name + " - copy");
ProfileEditorService.ExecuteCommand(new AddProfileElement(pasted, parent, Layer.Order - 1));
Layer.Profile.PopulateLeds(_deviceService.EnabledDevices);
}
/// <inheritdoc />
public override bool SupportsChildren => false;
#endregion
}
@@ -1,183 +0,0 @@
<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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:profileTree="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileTree"
xmlns:profileBehaviors="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileTree.Behaviors"
xmlns:behaviors="clr-namespace:Artemis.UI.Behaviors"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.ProfileTree.ProfileTreeView"
x:DataType="profileTree:ProfileTreeViewModel">
<!-- These cause binding errors, not my fault - https://github.com/AvaloniaUI/Avalonia/issues/5762 -->
<UserControl.KeyBindings>
<KeyBinding Gesture="Escape" Command="{CompiledBinding ClearSelection}" />
<KeyBinding Gesture="F2" Command="{CompiledBinding RenameSelected}" />
<KeyBinding Gesture="Delete" Command="{CompiledBinding DeleteSelected}" />
<KeyBinding Gesture="Ctrl+D" Command="{CompiledBinding DuplicateSelected}" />
<KeyBinding Gesture="Ctrl+C" Command="{CompiledBinding CopySelected}" />
<KeyBinding Gesture="Ctrl+V" Command="{CompiledBinding PasteSelected}" />
</UserControl.KeyBindings>
<UserControl.Resources>
<converters:ColorOpacityConverter x:Key="ColorOpacityConverter" />
</UserControl.Resources>
<UserControl.Styles>
<StyleInclude Source="avares://Avalonia.Xaml.Interactions.Draggable/Styles.axaml" />
<Style Selector="TreeView#Profile">
<Style.Resources>
<profileBehaviors:ProfileTreeViewDropHandler x:Key="ProfileTreeViewDropHandler" />
</Style.Resources>
<Setter Property="(Interaction.Behaviors)">
<BehaviorCollectionTemplate>
<BehaviorCollection>
<ContextDropBehavior Handler="{StaticResource ProfileTreeViewDropHandler}" />
</BehaviorCollection>
</BehaviorCollectionTemplate>
</Setter>
</Style>
<Style Selector="TreeView#Profile TreeViewItem">
<Setter Property="IsExpanded" Value="True" />
<Setter Property="(Interaction.Behaviors)">
<BehaviorCollectionTemplate>
<BehaviorCollection>
<behaviors:SimpleContextDragBehavior />
</BehaviorCollection>
</BehaviorCollectionTemplate>
</Setter>
</Style>
<Style Selector="TreeView#Profile TreeViewItem:dragging-before">
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,28">
<GradientStop Color="{StaticResource SystemAccentColorLight3}" Offset="0.0" />
<GradientStop Color="{StaticResource SystemAccentColorLight3}" Offset="0.05" />
<GradientStop Color="{CompiledBinding Source={StaticResource SystemAccentColorLight3}, Converter={StaticResource ColorOpacityConverter}, ConverterParameter=0.25}" Offset="0.05" />
<GradientStop Color="{CompiledBinding Source={StaticResource SystemAccentColorLight3}, Converter={StaticResource ColorOpacityConverter}, ConverterParameter=0}" Offset="0.25" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Style>
<Style Selector="TreeView#Profile TreeViewItem:dragging-into">
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,28">
<GradientStop Color="{StaticResource SystemAccentColorLight3}" Offset="0.0" />
<GradientStop Color="{StaticResource SystemAccentColorLight3}" Offset="0.05" />
<GradientStop Color="{CompiledBinding Source={StaticResource SystemAccentColorLight3}, Converter={StaticResource ColorOpacityConverter}, ConverterParameter=0.25}" Offset="0.05" />
<GradientStop Color="{CompiledBinding Source={StaticResource SystemAccentColorLight3}, Converter={StaticResource ColorOpacityConverter}, ConverterParameter=0}" Offset="0.25" />
<GradientStop Color="{CompiledBinding Source={StaticResource SystemAccentColorLight3}, Converter={StaticResource ColorOpacityConverter}, ConverterParameter=0}" Offset="0.75" />
<GradientStop Color="{CompiledBinding Source={StaticResource SystemAccentColorLight3}, Converter={StaticResource ColorOpacityConverter}, ConverterParameter=0.25}" Offset="0.95" />
<GradientStop Color="{StaticResource SystemAccentColorLight3}" Offset="0.95" />
<GradientStop Color="{StaticResource SystemAccentColorLight3}" Offset="1" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Style>
<Style Selector="TreeView#Profile TreeViewItem:dragging-after">
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,28">
<GradientStop Color="{CompiledBinding Source={StaticResource SystemAccentColorLight3}, Converter={StaticResource ColorOpacityConverter}, ConverterParameter=0}" Offset="0.75" />
<GradientStop Color="{CompiledBinding Source={StaticResource SystemAccentColorLight3}, Converter={StaticResource ColorOpacityConverter}, ConverterParameter=0.25}" Offset="0.95" />
<GradientStop Color="{StaticResource SystemAccentColorLight3}" Offset="0.95" />
<GradientStop Color="{StaticResource SystemAccentColorLight3}" Offset="1" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Style>
</UserControl.Styles>
<Grid RowDefinitions="*,Auto">
<TreeView Name="Profile" Classes="no-right-margin draggable"
ItemsSource="{CompiledBinding Children}"
SelectedItem="{CompiledBinding SelectedChild}"
SelectionChanged="ProfileTreeView_OnSelectionChanged">
<TreeView.Styles>
<Style Selector="TreeViewItem">
<Setter Property="IsExpanded" Value="{CompiledBinding IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.Styles>
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{CompiledBinding Children}">
<ContentControl Content="{CompiledBinding}" x:DataType="profileTree:TreeItemViewModel" Background="Transparent">
<ContentControl.ContextFlyout>
<MenuFlyout IsOpen="{CompiledBinding IsFlyoutOpen, Mode=OneWayToSource}">
<MenuItem Header="Add new folder" Command="{CompiledBinding AddFolder}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="CreateNewFolder" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Add new layer" Command="{CompiledBinding AddLayer}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="LayersPlus" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Open adaption hints" Command="{CompiledBinding OpenAdaptionHints}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Magic" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Apply adaption hints" Command="{CompiledBinding ApplyAdaptionHints}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="CogPlay" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="-" />
<MenuItem Header="Duplicate" Command="{CompiledBinding Duplicate}" InputGesture="Ctrl+D">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ContentDuplicate" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Copy" Command="{CompiledBinding Copy}" InputGesture="Ctrl+C">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ContentCopy" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Paste" Command="{CompiledBinding Paste}" InputGesture="Ctrl+V">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ContentPaste" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="-" />
<MenuItem Header="Rename" Command="{CompiledBinding Rename}" InputGesture="F2">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="RenameBox" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Delete" Command="{CompiledBinding Delete}" InputGesture="Delete">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="TrashCan" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</ContentControl.ContextFlyout>
</ContentControl>
</TreeDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<Grid Grid.Row="1" ColumnDefinitions="*,*">
<StackPanel Grid.Column="0"
Orientation="Horizontal"
VerticalAlignment="Center"
Spacing="5"
Background="Transparent"
ToolTip.Tip="Focus Mode - Use to disable parts of the profile you're not working on (F to change)">
<avalonia:MaterialIcon Kind="EyeOff" IsVisible="{CompiledBinding FocusNone}" />
<avalonia:MaterialIcon Kind="Eye" IsVisible="{CompiledBinding !FocusNone}" />
<TextBlock Text="Focus disabled" IsVisible="{CompiledBinding FocusNone}" />
<TextBlock Text="Focus on folder" IsVisible="{CompiledBinding FocusFolder}" />
<TextBlock Text="Focus on selection" IsVisible="{CompiledBinding FocusSelection}" />
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right">
<Button Classes="icon-button" ToolTip.Tip="Add new folder to root" Command="{CompiledBinding AddFolder}">
<avalonia:MaterialIcon Kind="FolderAdd" />
</Button>
<Button Classes="icon-button" ToolTip.Tip="Add new layer to root" Command="{CompiledBinding AddLayer}" Margin="2 0 0 0">
<avalonia:MaterialIcon Kind="LayersPlus" />
</Button>
</StackPanel>
</Grid>
</Grid>
</UserControl>
@@ -1,122 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.ReactiveUI;
using Avalonia.VisualTree;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree;
public partial class ProfileTreeView : ReactiveUserControl<ProfileTreeViewModel>
{
private Image? _dragAdorner;
private Point _dragStartPosition;
private Point _elementDragOffset;
public ProfileTreeView()
{
InitializeComponent();
AddHandler(DragDrop.DragEnterEvent, HandleDragEnterEvent, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble, true);
AddHandler(DragDrop.DragOverEvent, HandleDragOver, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble, true);
AddHandler(PointerEnteredEvent, HandlePointerEnter, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble, true);
}
private void HandlePointerEnter(object? sender, PointerEventArgs e)
{
DisposeDragAdorner();
}
private void HandleDragEnterEvent(object? sender, DragEventArgs e)
{
CreateDragAdorner(e);
}
private void CreateDragAdorner(DragEventArgs e)
{
Window? window = this.FindAncestorOfType<Window>();
if (window == null || _dragAdorner != null)
return;
if (e.Source is not Control c)
return;
// Get the tree view item that raised the event
TreeViewItem? container = c.FindLogicalAncestorOfType<TreeViewItem>();
if (container == null)
return;
// Take a snapshot of said tree view item and add it as an adorner
ITransform? originalTransform = container.RenderTransform;
try
{
_dragStartPosition = e.GetPosition(window);
_elementDragOffset = e.GetPosition(container);
RenderTargetBitmap renderTarget = new(new PixelSize((int) container.Bounds.Width, (int) container.Bounds.Height));
container.RenderTransform = new TranslateTransform(container.Bounds.X * -1, container.Bounds.Y * -1);
renderTarget.Render(container);
_dragAdorner = new Image
{
Source = renderTarget,
VerticalAlignment = VerticalAlignment.Top,
HorizontalAlignment = HorizontalAlignment.Left,
Stretch = Stretch.None,
IsHitTestVisible = false
};
AdornerLayer.GetAdornerLayer(this)!.Children.Add(_dragAdorner);
}
finally
{
container.RenderTransform = originalTransform;
}
}
private void HandleDragOver(object? sender, DragEventArgs e)
{
UpdateDragAdorner(e);
}
private void HandleLeaveEvent(object? sender, RoutedEventArgs e)
{
// If there is currently an adorner, dispose of it
DisposeDragAdorner();
}
private void HandleDrop(object? sender, DragEventArgs e)
{
// If there is currently an adorner, dispose of it
DisposeDragAdorner();
}
private void DisposeDragAdorner()
{
if (_dragAdorner == null)
return;
AdornerLayer.GetAdornerLayer(this)!.Children.Remove(_dragAdorner);
(_dragAdorner.Source as RenderTargetBitmap)?.Dispose();
_dragAdorner = null;
}
private void UpdateDragAdorner(DragEventArgs e)
{
Window? window = this.FindAncestorOfType<Window>();
if (window == null || _dragAdorner == null)
return;
Point position = e.GetPosition(window);
_dragAdorner.RenderTransform = new TranslateTransform(_dragStartPosition.X - _elementDragOffset.X, position.Y - _elementDragOffset.Y);
}
private void ProfileTreeView_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
Profile.Focus();
}
}
@@ -1,134 +0,0 @@
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.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.ProfileEditor;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree;
public partial class ProfileTreeViewModel : TreeItemViewModel
{
private ObservableAsPropertyHelper<bool>? _focusFolder;
private ObservableAsPropertyHelper<bool>? _focusNone;
private ObservableAsPropertyHelper<bool>? _focusSelection;
private ObservableAsPropertyHelper<bool>? _keyBindingsEnabled;
[Notify] private TreeItemViewModel? _selectedChild;
public ProfileTreeViewModel(IWindowService windowService, IDeviceService deviceService, IProfileEditorService profileEditorService, IProfileEditorVmFactory profileEditorVmFactory)
: base(null, null, windowService, deviceService, profileEditorService, profileEditorVmFactory)
{
this.WhenActivated(d =>
{
profileEditorService.ProfileConfiguration.WhereNotNull().Subscribe(configuration =>
{
if (configuration.Profile == null)
{
windowService.ShowConfirmContentDialog("Failed to load profile", "It appears that this profile is corrupt and cannot be loaded. Please check your logs.", "Confirm", null);
return;
}
ProfileElement = configuration.Profile.GetRootFolder();
SubscribeToProfileElement(d);
CreateTreeItems();
}).DisposeWith(d);
profileEditorService.ProfileElement.Subscribe(SelectCurrentProfileElement).DisposeWith(d);
_focusNone = profileEditorService.FocusMode.Select(f => f == ProfileEditorFocusMode.None).ToProperty(this, vm => vm.FocusNone).DisposeWith(d);
_focusFolder = profileEditorService.FocusMode.Select(f => f == ProfileEditorFocusMode.Folder).ToProperty(this, vm => vm.FocusFolder).DisposeWith(d);
_focusSelection = profileEditorService.FocusMode.Select(f => f == ProfileEditorFocusMode.Selection).ToProperty(this, vm => vm.FocusSelection).DisposeWith(d);
_keyBindingsEnabled = Shared.UI.CurrentKeyBindingsEnabled.ToProperty(this, vm => vm.KeyBindingsEnabled).DisposeWith(d);
});
ClearSelection = ReactiveCommand.Create(() => profileEditorService.ChangeCurrentProfileElement(null), this.WhenAnyValue(vm => vm.KeyBindingsEnabled));
RenameSelected = ReactiveCommand.Create(() => { SelectedChild?.Rename.Execute().Subscribe(); }, this.WhenAnyValue(vm => vm.KeyBindingsEnabled));
DeleteSelected = ReactiveCommand.Create(() => { SelectedChild?.Delete.Execute().Subscribe(); }, this.WhenAnyValue(vm => vm.KeyBindingsEnabled));
DuplicateSelected = ReactiveCommand.Create(() => { SelectedChild?.Duplicate.Execute().Subscribe(); }, this.WhenAnyValue(vm => vm.KeyBindingsEnabled));
CopySelected = ReactiveCommand.Create(() => { SelectedChild?.Copy.Execute().Subscribe(); }, this.WhenAnyValue(vm => vm.KeyBindingsEnabled));
PasteSelected = ReactiveCommand.Create(() => { SelectedChild?.Paste.Execute().Subscribe(); }, this.WhenAnyValue(vm => vm.KeyBindingsEnabled));
this.WhenAnyValue(vm => vm.SelectedChild).Subscribe(model =>
{
if (model?.ProfileElement is RenderProfileElement renderProfileElement)
profileEditorService.ChangeCurrentProfileElement(renderProfileElement);
});
}
public bool FocusNone => _focusNone?.Value ?? false;
public bool FocusFolder => _focusFolder?.Value ?? false;
public bool FocusSelection => _focusSelection?.Value ?? false;
public bool KeyBindingsEnabled => _keyBindingsEnabled?.Value ?? false;
public ReactiveCommand<Unit, Unit> ClearSelection { get; }
public ReactiveCommand<Unit, Unit> RenameSelected { get; }
public ReactiveCommand<Unit, Unit> DeleteSelected { get; }
public ReactiveCommand<Unit, Unit> DuplicateSelected { get; }
public ReactiveCommand<Unit, Unit> CopySelected { get; }
public ReactiveCommand<Unit, Unit> PasteSelected { get; }
public override bool SupportsChildren => true;
public void UpdateCanPaste()
{
throw new NotImplementedException();
}
protected override Task ExecuteDuplicate()
{
throw new NotSupportedException();
}
protected override Task ExecuteCopy()
{
throw new NotSupportedException();
}
protected override Task ExecutePaste()
{
throw new NotSupportedException();
}
private void SelectCurrentProfileElement(RenderProfileElement? element)
{
if (SelectedChild?.ProfileElement == element)
return;
// Find the tree item belonging to the selected element
List<TreeItemViewModel> treeItems = GetAllTreeItems(Children);
TreeItemViewModel? selected = treeItems.FirstOrDefault(e => e.ProfileElement == element);
// Walk up the tree, expanding parents
TreeItemViewModel? currentParent = selected?.Parent;
while (currentParent != null)
{
currentParent.IsExpanded = true;
currentParent = currentParent.Parent;
}
SelectedChild = selected;
}
private List<TreeItemViewModel> GetAllTreeItems(ObservableCollection<TreeItemViewModel> treeItems)
{
List<TreeItemViewModel> result = new();
foreach (TreeItemViewModel treeItemViewModel in treeItems)
{
result.Add(treeItemViewModel);
if (treeItemViewModel.Children.Any())
result.AddRange(GetAllTreeItems(treeItemViewModel.Children));
}
return result;
}
}
@@ -1,272 +0,0 @@
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 PropertyChanged.SourceGenerator;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree;
public abstract partial class TreeItemViewModel : ActivatableViewModelBase
{
private readonly IProfileEditorVmFactory _profileEditorVmFactory;
private readonly IWindowService _windowService;
private readonly IDeviceService _deviceService;
protected readonly IProfileEditorService ProfileEditorService;
private RenderProfileElement? _currentProfileElement;
private ObservableAsPropertyHelper<bool>? _isFocused;
private TimeSpan _time;
[Notify] private bool _canPaste;
[Notify] private bool _isExpanded;
[Notify] private bool _isFlyoutOpen;
[Notify] private ProfileElement? _profileElement;
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 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();
// Can be null on some platforms
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
CanPaste = formats != null && 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;
}
}
@@ -1,52 +0,0 @@
<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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:dataBinding="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.DataBinding"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.DataBinding.DataBindingView"
x:DataType="dataBinding:DataBindingViewModel">
<Grid RowDefinitions="48,*">
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*" Background="{DynamicResource ControlFillColorDefaultBrush}">
<TextBlock Grid.Column="0"
Classes="h5"
VerticalAlignment="Center"
Text="{CompiledBinding LayerProperty.PropertyDescription.Name}"
Margin="10 0" />
<ToggleSwitch Grid.Column="1"
OnContent="Data binding enabled"
OffContent="Data binding disabled"
VerticalAlignment="Center"
IsChecked="{CompiledBinding DataBindingEnabled}" />
<Button Grid.Column="2"
Classes="icon-button"
ToolTip.Tip="Open editor"
VerticalAlignment="Center"
HorizontalAlignment="Right"
Margin="10"
Command="{CompiledBinding OpenEditor}">
<avalonia:MaterialIcon Kind="OpenInNew" />
</Button>
</Grid>
<ContentControl Grid.Row="1"
IsVisible="{CompiledBinding NodeScriptViewModel, Converter={x:Static ObjectConverters.IsNotNull}}"
Content="{CompiledBinding NodeScriptViewModel}" />
<StackPanel Grid.Row="1"
VerticalAlignment="Center"
IsVisible="{CompiledBinding NodeScriptViewModel, Converter={x:Static ObjectConverters.IsNull}}">
<TextBlock TextAlignment="Center" Classes="h5">To use data bindings enable them in with the toggle above.</TextBlock>
<TextBlock TextWrapping="Wrap" HorizontalAlignment="Center" TextAlignment="Center" Foreground="{DynamicResource TextFillColorSecondary}">
When you enable data bindings you can no longer use keyframes or normal values for this property.
</TextBlock>
<HyperlinkButton HorizontalAlignment="Center"
NavigateUri="https://wiki.artemis-rgb.com/en/guides/user/profiles/data-bindings?mtm_campaign=artemis&amp;mtm_kwd=profile-editor"
Margin="0 10">
Learn more
</HyperlinkButton>
</StackPanel>
</Grid>
</UserControl>
@@ -1,13 +0,0 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.DataBinding;
public partial class DataBindingView : ReactiveUserControl<DataBindingViewModel>
{
public DataBindingView()
{
InitializeComponent();
}
}
@@ -1,110 +0,0 @@
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading.Tasks;
using System.Timers;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Extensions;
using Artemis.UI.Screens.VisualScripting;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.DataBinding;
public class DataBindingViewModel : ActivatableViewModelBase
{
private readonly PluginSetting<bool> _alwaysApplyDataBindings;
private readonly IProfileEditorService _profileEditorService;
private readonly IWindowService _windowService;
private ObservableAsPropertyHelper<bool>? _dataBindingEnabled;
private ObservableAsPropertyHelper<ILayerProperty?>? _layerProperty;
private ObservableAsPropertyHelper<NodeScriptViewModel?>? _nodeScriptViewModel;
private bool _editorOpen;
private bool _playing;
public DataBindingViewModel(IProfileEditorService profileEditorService, INodeVmFactory nodeVmFactory, IWindowService windowService, ISettingsService settingsService)
{
_profileEditorService = profileEditorService;
_windowService = windowService;
_alwaysApplyDataBindings = settingsService.GetSetting("ProfileEditor.AlwaysApplyDataBindings", false);
this.WhenActivated(d =>
{
_layerProperty = profileEditorService.LayerProperty.ToProperty(this, vm => vm.LayerProperty).DisposeWith(d);
_nodeScriptViewModel = profileEditorService.LayerProperty
.Select(p => p != null ? p.BaseDataBinding.AsObservable() : Observable.Never<IDataBinding>())
.Switch()
.Select(b => b.IsEnabled ? nodeVmFactory.NodeScriptViewModel((NodeScript) b.Script, false) : null)
.ToProperty(this, vm => vm.NodeScriptViewModel)
.DisposeWith(d);
_dataBindingEnabled = profileEditorService.LayerProperty
.Select(p => p != null ? p.BaseDataBinding.AsObservable() : Observable.Never<IDataBinding>())
.Switch()
.Select(b => b.IsEnabled)
.ToProperty(this, vm => vm.DataBindingEnabled)
.DisposeWith(d);
_profileEditorService.Playing.CombineLatest(_profileEditorService.SuspendedEditing).Subscribe(tuple => _playing = tuple.First || tuple.Second).DisposeWith(d);
Timer updateTimer = new(TimeSpan.FromMilliseconds(25));
Timer saveTimer = new(TimeSpan.FromMinutes(2));
updateTimer.Elapsed += (_, _) => Update();
saveTimer.Elapsed += (_, _) => Save();
updateTimer.Start();
saveTimer.Start();
updateTimer.DisposeWith(d);
saveTimer.DisposeWith(d);
});
}
public ILayerProperty? LayerProperty => _layerProperty?.Value;
public NodeScriptViewModel? NodeScriptViewModel => _nodeScriptViewModel?.Value;
public bool DataBindingEnabled
{
get => _dataBindingEnabled?.Value ?? false;
set
{
if (LayerProperty != null)
_profileEditorService.ExecuteCommand(new ChangeDataBindingEnabled(LayerProperty, value));
}
}
public async Task OpenEditor()
{
if (LayerProperty == null || !LayerProperty.BaseDataBinding.IsEnabled)
return;
try
{
_editorOpen = true;
await _windowService.ShowDialogAsync<NodeScriptWindowViewModel, bool>(LayerProperty.BaseDataBinding.Script);
await _profileEditorService.SaveProfileAsync();
}
finally
{
_editorOpen = false;
}
}
private void Update()
{
// If playing the data binding will already be updated, no need to do it here
if (_playing || !_alwaysApplyDataBindings.Value)
return;
LayerProperty?.UpdateDataBinding();
}
private void Save()
{
if (!_editorOpen)
_profileEditorService.SaveProfileAsync();
}
}
@@ -1,64 +0,0 @@
<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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:layerEffects="clr-namespace:Artemis.Core.LayerEffects;assembly=Artemis.Core"
xmlns:dialogs="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Dialogs"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Dialogs.AddEffectView"
x:DataType="dialogs:AddEffectViewModel"
Width="500">
<Grid RowDefinitions="Auto,*">
<TextBox Name="SearchBox" Text="{CompiledBinding SearchText}" Margin="0 0 0 15" Watermark="Search" />
<ListBox Name="EffectDescriptorsList"
Grid.Row="1"
ItemsSource="{CompiledBinding LayerEffectDescriptors}"
IsVisible="{CompiledBinding LayerEffectDescriptors.Count}"
Height="300">
<ListBox.DataTemplates>
<DataTemplate DataType="{x:Type layerEffects:LayerEffectDescriptor}">
<Grid RowDefinitions="Auto,*"
ColumnDefinitions="Auto,Auto"
Background="Transparent"
PointerReleased="InputElement_OnPointerReleased"
Margin="0 4"
VerticalAlignment="Center">
<shared:ArtemisIcon Grid.Column="0"
Grid.RowSpan="2"
Icon="{CompiledBinding Icon}"
Width="24"
Height="24"
VerticalAlignment="Center"
Margin="0 0 15 0" />
<TextBlock Grid.Column="1"
Grid.Row="0"
Classes="BodyStrongTextBlockStyle"
Text="{CompiledBinding DisplayName}"
VerticalAlignment="Bottom"
Width="450"
TextWrapping="Wrap" />
<TextBlock Grid.Column="1"
Grid.Row="1"
Foreground="{DynamicResource TextFillColorSecondary}"
Text="{CompiledBinding Description}"
VerticalAlignment="Top"
Width="450"
TextWrapping="Wrap" />
</Grid>
</DataTemplate>
</ListBox.DataTemplates>
</ListBox>
<Grid Grid.Row="1" Height="300">
<StackPanel VerticalAlignment="Center"
Spacing="20"
IsVisible="{CompiledBinding !LayerEffectDescriptors.Count}">
<avalonia:MaterialIcon Kind="CloseCircle" Width="32" Height="32" />
<TextBlock Classes="h5" TextAlignment="Center">None of the effects match your search</TextBlock>
</StackPanel>
</Grid>
</Grid>
</UserControl>
@@ -1,24 +0,0 @@
using Artemis.Core.LayerEffects;
using Avalonia;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Dialogs;
public partial class AddEffectView : ReactiveUserControl<AddEffectViewModel>
{
public AddEffectView()
{
InitializeComponent();
}
private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (sender is not IDataContextProvider {DataContext: LayerEffectDescriptor descriptor} || ViewModel == null)
return;
ViewModel?.AddLayerEffect(descriptor);
}
}
@@ -1,56 +0,0 @@
using System;
using System.Collections.ObjectModel;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.Core.LayerEffects;
using Artemis.Core.Services;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using DynamicData;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Dialogs;
public partial class AddEffectViewModel : ContentDialogViewModelBase
{
private readonly IProfileEditorService _profileEditorService;
private readonly RenderProfileElement _renderProfileElement;
[Notify] private string? _searchText;
public AddEffectViewModel(RenderProfileElement renderProfileElement, IProfileEditorService profileEditorService, ILayerEffectService layerEffectService)
{
_renderProfileElement = renderProfileElement;
_profileEditorService = profileEditorService;
SourceList<LayerEffectDescriptor> layerEffectSourceList = new();
layerEffectSourceList.AddRange(layerEffectService.GetLayerEffects());
IObservable<Func<LayerEffectDescriptor, bool>> layerEffectFilter = this.WhenAnyValue(vm => vm.SearchText).Select(CreatePredicate);
layerEffectSourceList.Connect()
.Filter(layerEffectFilter)
.Bind(out ReadOnlyObservableCollection<LayerEffectDescriptor> layerEffectDescriptors)
.Subscribe();
LayerEffectDescriptors = layerEffectDescriptors;
}
public ReadOnlyObservableCollection<LayerEffectDescriptor> LayerEffectDescriptors { get; }
public void AddLayerEffect(LayerEffectDescriptor descriptor)
{
BaseLayerEffect layerEffect = descriptor.CreateInstance(_renderProfileElement, null);
_profileEditorService.ExecuteCommand(new AddLayerEffect(_renderProfileElement, layerEffect));
ContentDialog?.Hide();
}
private Func<LayerEffectDescriptor, bool> CreatePredicate(string? search)
{
if (string.IsNullOrWhiteSpace(search))
return _ => true;
search = search.Trim();
return data => data.DisplayName.Contains(search, StringComparison.InvariantCultureIgnoreCase) ||
data.Description.Contains(search, StringComparison.InvariantCultureIgnoreCase);
}
}
@@ -1,18 +0,0 @@
<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:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:dialogs="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Dialogs"
xmlns:attachedProperties="clr-namespace:Artemis.UI.Shared.AttachedProperties;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Dialogs.TimelineSegmentEditView"
x:DataType="dialogs:TimelineSegmentEditViewModel">
<StackPanel>
<controls:NumberBox Name="LengthNumberBox"
Minimum="0.1"
Value="{CompiledBinding SegmentLength}"
HorizontalAlignment="Stretch"
attachedProperties:NumberBoxAssist.SuffixText="sec"/>
</StackPanel>
</UserControl>
@@ -1,13 +0,0 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Dialogs;
public partial class TimelineSegmentEditView : ReactiveUserControl<TimelineSegmentEditViewModel>
{
public TimelineSegmentEditView()
{
InitializeComponent();
}
}
@@ -1,14 +0,0 @@
using System;
using Artemis.UI.Shared;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Dialogs;
public class TimelineSegmentEditViewModel : ContentDialogViewModelBase
{
public TimelineSegmentEditViewModel(TimeSpan segmentLength)
{
SegmentLength = segmentLength.TotalSeconds;
}
public double SegmentLength { get; set; }
}
@@ -1,153 +0,0 @@
<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:controls="clr-namespace:Artemis.UI.Controls"
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="350"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.PropertiesView"
x:DataType="local:PropertiesViewModel">
<UserControl.Styles>
<StyleInclude Source="/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/Segment.axaml" />
</UserControl.Styles>
<UserControl.Resources>
<converters:DoubleToGridLengthConverter x:Key="DoubleToGridLengthConverter" />
</UserControl.Resources>
<Grid Name="ContainerGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{CompiledBinding PropertiesTreeWidth.Value, Mode=TwoWay, Converter={StaticResource DoubleToGridLengthConverter}}" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid RowDefinitions="48,*">
<ContentControl Grid.Row="0" Content="{CompiledBinding PlaybackViewModel}" Background="{DynamicResource ControlFillColorDefaultBrush}" />
<ScrollViewer Grid.Row="1"
Grid.Column="0"
Name="TreeScrollViewer"
Offset="{CompiledBinding #TimelineScrollViewer.Offset, Mode=OneWay}">
<Grid RowDefinitions="*,Auto">
<ItemsControl ItemsSource="{CompiledBinding PropertyGroupViewModels}" Padding="0 0 8 0">
<ItemsControl.ItemTemplate>
<TreeDataTemplate DataType="{x:Type local:PropertyGroupViewModel}" ItemsSource="{CompiledBinding Children}">
<ContentControl Content="{CompiledBinding TreeGroupViewModel}" />
</TreeDataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Grid.Row="1"
Command="{CompiledBinding AddEffect}"
Margin="4"
VerticalAlignment="Bottom"
HorizontalAlignment="Right">
Add new effect
</Button>
</Grid>
</ScrollViewer>
</Grid>
<GridSplitter Grid.Column="1"
Cursor="SizeWestEast"
Foreground="Transparent"
Background="Transparent"
Margin="0 0 -5 0" />
<ContentControl Grid.Column="2"
IsVisible="{CompiledBinding DataBindingViewModel, Converter={x:Static ObjectConverters.IsNotNull}}"
Content="{CompiledBinding DataBindingViewModel}" />
<!-- Horizontal scrolling -->
<ScrollViewer Grid.Column="2"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Disabled"
IsVisible="{CompiledBinding DataBindingViewModel, Converter={x:Static ObjectConverters.IsNull}}">
<Grid RowDefinitions="48,*">
<!-- Timeline header body -->
<controls:TimelineHeader Grid.Row="0"
Name="TimelineHeader"
Margin="0 18 0 0"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Background="{DynamicResource ControlFillColorDefaultBrush}"
HorizontalAlignment="Left"
PixelsPerSecond="{CompiledBinding PixelsPerSecond}"
HorizontalOffset="{CompiledBinding #TimelineScrollViewer.Offset.X, Mode=OneWay}"
VisibleWidth="{CompiledBinding #TimelineScrollViewer.Bounds.Width}"
OffsetFirstValue="True"
PointerReleased="TimelineHeader_OnPointerReleased"
Width="{CompiledBinding #TimelineScrollViewer.Viewport.Width}"
Cursor="Hand" />
<Canvas Grid.Row="0" ZIndex="2">
<!-- Segment dividers -->
<Line Name="TimelineLine"
Canvas.Left="{CompiledBinding TimelineViewModel.CaretPosition}"
Cursor="SizeWestEast"
PointerPressed="TimelineCaret_OnPointerPressed"
PointerReleased="TimelineCaret_OnPointerReleased"
PointerMoved="TimelineCaret_OnPointerMoved"
StartPoint="0,0"
EndPoint="{CompiledBinding #ContainerGrid.Bounds.BottomLeft}"
StrokeThickness="2"
Stroke="{DynamicResource SystemAccentColorLight1}" />
<Line Name="StartSegmentLine"
Canvas.Left="{CompiledBinding TimelineViewModel.StartSegmentViewModel.EndX}"
IsVisible="{CompiledBinding !TimelineViewModel.MainSegmentViewModel.ShowAddStart}"
StartPoint="0,0"
EndPoint="{CompiledBinding #ContainerGrid.Bounds.BottomLeft}"
StrokeThickness="2"
Stroke="{DynamicResource SystemAccentColorLight1}"
StrokeDashArray="6,2"
Opacity="0.5" />
<Line Name="MainSegmentLine"
Canvas.Left="{CompiledBinding TimelineViewModel.MainSegmentViewModel.EndX}"
IsVisible="{CompiledBinding !TimelineViewModel.MainSegmentViewModel.ShowAddMain}"
StartPoint="0,0"
EndPoint="{CompiledBinding #ContainerGrid.Bounds.BottomLeft}"
StrokeThickness="2"
Stroke="{DynamicResource SystemAccentColorLight1}"
StrokeDashArray="6,2"
Opacity="0.5" />
<Line Name="EndSegmentLine"
Canvas.Left="{CompiledBinding TimelineViewModel.EndSegmentViewModel.EndX}"
IsVisible="{CompiledBinding !TimelineViewModel.MainSegmentViewModel.ShowAddEnd}"
StartPoint="0,0"
EndPoint="{CompiledBinding #ContainerGrid.Bounds.BottomLeft}"
StrokeThickness="2"
Stroke="{DynamicResource SystemAccentColorLight1}"
StrokeDashArray="6,2"
Opacity="0.5" />
<!-- Timeline segments -->
<ContentControl Canvas.Left="{CompiledBinding TimelineViewModel.EndSegmentViewModel.StartX}"
Classes="segment-content-control"
Content="{CompiledBinding TimelineViewModel.EndSegmentViewModel}" />
<ContentControl Canvas.Left="{CompiledBinding TimelineViewModel.MainSegmentViewModel.StartX}"
Classes="segment-content-control"
Content="{CompiledBinding TimelineViewModel.MainSegmentViewModel}" />
<ContentControl Canvas.Left="{CompiledBinding TimelineViewModel.StartSegmentViewModel.StartX}"
Classes="segment-content-control"
Content="{CompiledBinding TimelineViewModel.StartSegmentViewModel}" />
<!-- Timeline caret -->
<Polygon Name="TimelineCaret"
Canvas.Left="{CompiledBinding TimelineViewModel.CaretPosition}"
Cursor="SizeWestEast"
PointerPressed="TimelineCaret_OnPointerPressed"
PointerReleased="TimelineCaret_OnPointerReleased"
PointerMoved="TimelineCaret_OnPointerMoved"
Points="-8,0 -8,8 0,20, 8,8 8,0"
Fill="{DynamicResource SystemAccentColorLight1}" />
</Canvas>
<ScrollViewer Grid.Row="1"
Name="TimelineScrollViewer"
Offset="{CompiledBinding #TreeScrollViewer.Offset, Mode=OneWay}"
VerticalScrollBarVisibility="Hidden">
<ContentControl Content="{CompiledBinding TimelineViewModel}" />
</ScrollViewer>
</Grid>
</ScrollViewer>
</Grid>
</UserControl>
@@ -1,96 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using Avalonia.VisualTree;
namespace Artemis.UI.Screens.ProfileEditor.Properties;
public partial class PropertiesView : ReactiveUserControl<PropertiesViewModel>
{
public PropertiesView()
{
InitializeComponent();
}
private void ApplyTransition(bool enable)
{
// if (enable)
// {
// ((DoubleTransition) _timelineCaret.Transitions![0]).Duration = TimeSpan.FromMilliseconds(50);
// ((DoubleTransition) _timelineLine.Transitions![0]).Duration = TimeSpan.FromMilliseconds(50);
// }
// else
// {
// ((DoubleTransition) _timelineCaret.Transitions![0]).Duration = TimeSpan.Zero;
// ((DoubleTransition) _timelineLine.Transitions![0]).Duration = TimeSpan.Zero;
// }
}
private void TimelineCaret_OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
e.Pointer.Capture((IInputElement?) sender);
ApplyTransition(false);
}
private void TimelineCaret_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
e.Pointer.Capture(null);
ApplyTransition(true);
}
private void TimelineCaret_OnPointerMoved(object? sender, PointerEventArgs e)
{
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel == null)
return;
Visual? senderElement = (Visual?) sender;
if (senderElement == null)
return;
// Get the parent grid, need that for our position
Visual? parent = senderElement.GetVisualParent();
double x = Math.Max(0, e.GetPosition(parent).X);
TimeSpan newTime = TimeSpan.FromSeconds(x / ViewModel.PixelsPerSecond);
newTime = RoundTime(newTime);
// If holding down shift, snap to the closest segment or keyframe
if (e.KeyModifiers.HasFlag(KeyModifiers.Shift))
{
List<TimeSpan> snapTimes = ViewModel.PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Select(k => k.Position).ToList();
newTime = ViewModel.TimelineViewModel.SnapToTimeline(newTime, true, false, snapTimes);
}
// If holding down control, round to the closest 50ms
if (e.KeyModifiers.HasFlag(KeyModifiers.Control))
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 50.0) * 50.0);
ViewModel.TimelineViewModel.ChangeTime(newTime);
}
private void TimelineHeader_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (ViewModel == null || sender is not Visual senderElement)
return;
// Get the parent grid, need that for our position
double x = Math.Max(0, e.GetPosition(senderElement.GetVisualParent()).X);
TimeSpan newTime = TimeSpan.FromSeconds(x / ViewModel.PixelsPerSecond);
ViewModel.TimelineViewModel.ChangeTime(RoundTime(newTime));
}
private TimeSpan RoundTime(TimeSpan time)
{
// Round the time to something that fits the current zoom level
if (ViewModel!.PixelsPerSecond < 200)
return TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds / 5.0) * 5.0);
if (ViewModel.PixelsPerSecond < 500)
return TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds / 2.0) * 2.0);
return TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds));
}
}
@@ -1,199 +0,0 @@
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.Tasks;
using Artemis.Core;
using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects;
using Artemis.Core.Services;
using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Screens.ProfileEditor.Playback;
using Artemis.UI.Screens.ProfileEditor.Properties.DataBinding;
using Artemis.UI.Screens.ProfileEditor.Properties.Dialogs;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.ProfileEditor;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties;
public partial class PropertiesViewModel : ActivatableViewModelBase
{
private readonly Dictionary<LayerPropertyGroup, PropertyGroupViewModel> _cachedPropertyViewModels;
private readonly IDataBindingVmFactory _dataBindingVmFactory;
private readonly ILayerEffectService _layerEffectService;
private readonly ILayerPropertyVmFactory _layerPropertyVmFactory;
private readonly IProfileEditorService _profileEditorService;
private readonly ISettingsService _settingsService;
private readonly IWindowService _windowService;
private DataBindingViewModel? _backgroundDataBindingViewModel;
private ObservableAsPropertyHelper<ILayerProperty?>? _layerProperty;
private ObservableAsPropertyHelper<int>? _pixelsPerSecond;
private ObservableAsPropertyHelper<RenderProfileElement?>? _profileElement;
private ObservableAsPropertyHelper<bool>? _suspendedEditing;
[Notify] private DataBindingViewModel? _dataBindingViewModel;
/// <inheritdoc />
public PropertiesViewModel(IProfileEditorService profileEditorService,
ISettingsService settingsService,
ILayerPropertyVmFactory layerPropertyVmFactory,
IDataBindingVmFactory dataBindingVmFactory,
IWindowService windowService,
ILayerEffectService layerEffectService,
PlaybackViewModel playbackViewModel)
{
_profileEditorService = profileEditorService;
_settingsService = settingsService;
_layerPropertyVmFactory = layerPropertyVmFactory;
_dataBindingVmFactory = dataBindingVmFactory;
_windowService = windowService;
_layerEffectService = layerEffectService;
_cachedPropertyViewModels = new Dictionary<LayerPropertyGroup, PropertyGroupViewModel>();
PropertyGroupViewModels = new ObservableCollection<PropertyGroupViewModel>();
PlaybackViewModel = playbackViewModel;
TimelineViewModel = layerPropertyVmFactory.TimelineViewModel(PropertyGroupViewModels);
AddEffect = ReactiveCommand.CreateFromTask(ExecuteAddEffect);
// React to service profile element changes as long as the VM is active
this.WhenActivated(d =>
{
_profileElement = profileEditorService.ProfileElement.ToProperty(this, vm => vm.ProfileElement).DisposeWith(d);
_pixelsPerSecond = profileEditorService.PixelsPerSecond.ToProperty(this, vm => vm.PixelsPerSecond).DisposeWith(d);
_layerProperty = profileEditorService.LayerProperty.ToProperty(this, vm => vm.LayerProperty).DisposeWith(d);
_suspendedEditing = profileEditorService.SuspendedEditing.ToProperty(this, vm => vm.SuspendedEditing).DisposeWith(d);
Disposable.Create(() =>
{
_settingsService.SaveAllSettings();
foreach ((LayerPropertyGroup _, PropertyGroupViewModel value) in _cachedPropertyViewModels)
value.Dispose();
_cachedPropertyViewModels.Clear();
}).DisposeWith(d);
});
// Subscribe to events of the latest selected profile element - borrowed from https://stackoverflow.com/a/63950940
this.WhenActivated(d =>
{
this.WhenAnyValue(vm => vm.ProfileElement)
.Select(p => p is Layer l
? Observable.FromEventPattern(x => l.LayerBrushUpdated += x, x => l.LayerBrushUpdated -= x)
: Observable.Never<EventPattern<object>>())
.Switch()
.ObserveOn(Shared.UI.BackgroundScheduler)
.Subscribe(_ => UpdatePropertyGroups())
.DisposeWith(d);
this.WhenAnyValue(vm => vm.ProfileElement)
.Select(p => p != null
? Observable.FromEventPattern(x => p.LayerEffectsUpdated += x, x => p.LayerEffectsUpdated -= x)
: Observable.Never<EventPattern<object>>())
.Switch()
.ObserveOn(Shared.UI.BackgroundScheduler)
.Subscribe(_ => UpdatePropertyGroups())
.DisposeWith(d);
});
this.WhenAnyValue(vm => vm.ProfileElement).ObserveOn(Shared.UI.BackgroundScheduler).Subscribe(_ => UpdatePropertyGroups());
this.WhenAnyValue(vm => vm.LayerProperty).Subscribe(_ => UpdateTimelineViewModel());
}
public ObservableCollection<PropertyGroupViewModel> PropertyGroupViewModels { get; }
public PlaybackViewModel PlaybackViewModel { get; }
public TimelineViewModel TimelineViewModel { get; }
public ReactiveCommand<Unit, Unit> AddEffect { get; }
public RenderProfileElement? ProfileElement => _profileElement?.Value;
public ILayerProperty? LayerProperty => _layerProperty?.Value;
public bool SuspendedEditing => _suspendedEditing?.Value ?? false;
public int PixelsPerSecond => _pixelsPerSecond?.Value ?? 0;
public IObservable<bool> Playing => _profileEditorService.Playing;
public PluginSetting<double> PropertiesTreeWidth => _settingsService.GetSetting("ProfileEditor.PropertiesTreeWidth", 500.0);
private async Task ExecuteAddEffect()
{
if (ProfileElement == null)
return;
await _windowService.CreateContentDialog()
.WithTitle("Add layer effect")
.WithViewModel(out AddEffectViewModel _, ProfileElement)
.WithCloseButtonText("Cancel")
.ShowAsync();
}
private void UpdatePropertyGroups()
{
RenderProfileElement? profileElement = ProfileElement;
if (profileElement == null)
{
PropertyGroupViewModels.Clear();
return;
}
ObservableCollection<PropertyGroupViewModel> viewModels = new();
if (profileElement is Layer layer)
{
// Add base VMs
viewModels.Add(GetOrCreatePropertyViewModel(layer.General, null, null));
viewModels.Add(GetOrCreatePropertyViewModel(layer.Transform, null, null));
// Add brush VM if the brush has properties
if (layer.LayerBrush?.BaseProperties != null)
viewModels.Add(GetOrCreatePropertyViewModel(layer.LayerBrush.BaseProperties, layer.LayerBrush, null));
}
// Add effect VMs
foreach (BaseLayerEffect layerEffect in profileElement.LayerEffects.OrderBy(e => e.Order))
{
if (layerEffect.BaseProperties != null)
viewModels.Add(GetOrCreatePropertyViewModel(layerEffect.BaseProperties, null, layerEffect));
}
// Map the most recent collection of VMs to the current list of VMs, making as little changes to the collection as possible
for (int index = 0; index < viewModels.Count; index++)
{
PropertyGroupViewModel propertyGroupViewModel = viewModels[index];
if (index > PropertyGroupViewModels.Count - 1)
PropertyGroupViewModels.Add(propertyGroupViewModel);
else if (!ReferenceEquals(PropertyGroupViewModels[index], propertyGroupViewModel))
PropertyGroupViewModels[index] = propertyGroupViewModel;
}
while (PropertyGroupViewModels.Count > viewModels.Count)
PropertyGroupViewModels.RemoveAt(PropertyGroupViewModels.Count - 1);
}
private PropertyGroupViewModel GetOrCreatePropertyViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerBrush? layerBrush, BaseLayerEffect? layerEffect)
{
if (_cachedPropertyViewModels.TryGetValue(layerPropertyGroup, out PropertyGroupViewModel? cachedVm))
return cachedVm;
PropertyGroupViewModel createdVm;
if (layerBrush != null)
createdVm = _layerPropertyVmFactory.PropertyGroupViewModel(layerPropertyGroup, layerBrush);
else if (layerEffect != null)
createdVm = _layerPropertyVmFactory.PropertyGroupViewModel(layerPropertyGroup, layerEffect);
else
createdVm = _layerPropertyVmFactory.PropertyGroupViewModel(layerPropertyGroup);
_cachedPropertyViewModels[layerPropertyGroup] = createdVm;
return createdVm;
}
private void UpdateTimelineViewModel()
{
if (LayerProperty == null)
{
DataBindingViewModel = null;
}
else
{
_backgroundDataBindingViewModel ??= _dataBindingVmFactory.DataBindingViewModel();
DataBindingViewModel = _backgroundDataBindingViewModel;
}
}
}
@@ -1,173 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using Artemis.Core;
using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects;
using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
using Artemis.UI.Screens.ProfileEditor.Properties.Tree;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.PropertyInput;
using DynamicData;
using DynamicData.Binding;
using PropertyChanged.SourceGenerator;
namespace Artemis.UI.Screens.ProfileEditor.Properties;
public partial class PropertyGroupViewModel : PropertyViewModelBase, IDisposable
{
private readonly ILayerPropertyVmFactory _layerPropertyVmFactory;
private readonly IPropertyInputService _propertyInputService;
private ReadOnlyObservableCollection<ILayerPropertyKeyframe> _keyframes = null!;
private IDisposable _keyframeSubscription = null!;
[Notify] private bool _hasChildren;
[Notify] private bool _isExpanded;
[Notify] private bool _isVisible;
public PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService)
{
_layerPropertyVmFactory = layerPropertyVmFactory;
_propertyInputService = propertyInputService;
LayerPropertyGroup = layerPropertyGroup;
TreeGroupViewModel = layerPropertyVmFactory.TreeGroupViewModel(this);
TimelineGroupViewModel = layerPropertyVmFactory.TimelineGroupViewModel(this);
LayerPropertyGroup.VisibilityChanged += LayerPropertyGroupOnVisibilityChanged;
_isVisible = !LayerPropertyGroup.IsHidden;
PopulateChildren();
}
public PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService, BaseLayerBrush layerBrush)
{
_layerPropertyVmFactory = layerPropertyVmFactory;
_propertyInputService = propertyInputService;
LayerBrush = layerBrush;
LayerPropertyGroup = layerPropertyGroup;
TreeGroupViewModel = layerPropertyVmFactory.TreeGroupViewModel(this);
TimelineGroupViewModel = layerPropertyVmFactory.TimelineGroupViewModel(this);
LayerPropertyGroup.VisibilityChanged += LayerPropertyGroupOnVisibilityChanged;
_isVisible = !LayerPropertyGroup.IsHidden;
PopulateChildren();
}
public PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService, BaseLayerEffect layerEffect)
{
_layerPropertyVmFactory = layerPropertyVmFactory;
_propertyInputService = propertyInputService;
LayerEffect = layerEffect;
LayerPropertyGroup = layerPropertyGroup;
TreeGroupViewModel = layerPropertyVmFactory.TreeGroupViewModel(this);
TimelineGroupViewModel = layerPropertyVmFactory.TimelineGroupViewModel(this);
LayerPropertyGroup.VisibilityChanged += LayerPropertyGroupOnVisibilityChanged;
_isVisible = !LayerPropertyGroup.IsHidden;
PopulateChildren();
}
public ObservableCollection<PropertyViewModelBase> Children { get; private set; } = null!;
public LayerPropertyGroup LayerPropertyGroup { get; }
public BaseLayerBrush? LayerBrush { get; }
public BaseLayerEffect? LayerEffect { get; }
public TreeGroupViewModel TreeGroupViewModel { get; }
public TimelineGroupViewModel TimelineGroupViewModel { get; }
public override ReadOnlyObservableCollection<ILayerPropertyKeyframe> Keyframes => _keyframes;
public List<ILayerPropertyKeyframe> GetAllKeyframes(bool expandedOnly)
{
List<ILayerPropertyKeyframe> result = new();
if (expandedOnly && !IsExpanded)
return result;
foreach (PropertyViewModelBase child in Children)
{
if (child is PropertyViewModel profileElementPropertyViewModel)
result.AddRange(profileElementPropertyViewModel.TimelinePropertyViewModel.GetAllKeyframes());
else if (child is PropertyGroupViewModel profileElementPropertyGroupViewModel)
result.AddRange(profileElementPropertyGroupViewModel.GetAllKeyframes(expandedOnly));
}
return result;
}
public List<ITimelineKeyframeViewModel> GetAllKeyframeViewModels(bool expandedOnly)
{
List<ITimelineKeyframeViewModel> result = new();
if (expandedOnly && !IsExpanded)
return result;
foreach (PropertyViewModelBase child in Children)
{
if (child is PropertyViewModel profileElementPropertyViewModel)
result.AddRange(profileElementPropertyViewModel.TimelinePropertyViewModel.GetAllKeyframeViewModels());
else if (child is PropertyGroupViewModel profileElementPropertyGroupViewModel)
result.AddRange(profileElementPropertyGroupViewModel.GetAllKeyframeViewModels(expandedOnly));
}
return result;
}
private void PopulateChildren()
{
Children = new ObservableCollection<PropertyViewModelBase>();
// Get all properties and property groups and create VMs for them
// The group has methods for getting this without reflection but then we lose the order of the properties as they are defined on the group
// Sorting is done to ensure properties defined by the Core (such as on layers) are always on top.
foreach (PropertyInfo propertyInfo in LayerPropertyGroup.GetType().GetProperties().OrderBy(p => p.DeclaringType?.Assembly != Constants.CoreAssembly))
{
if (Attribute.IsDefined(propertyInfo, typeof(LayerPropertyIgnoreAttribute)))
continue;
if (typeof(ILayerProperty).IsAssignableFrom(propertyInfo.PropertyType))
{
ILayerProperty? value = (ILayerProperty?) propertyInfo.GetValue(LayerPropertyGroup);
// Ensure a supported input VM was found, otherwise don't add it
if (value != null && _propertyInputService.CanCreatePropertyInputViewModel(value))
Children.Add(_layerPropertyVmFactory.PropertyViewModel(value));
}
else if (typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType))
{
LayerPropertyGroup? value = (LayerPropertyGroup?) propertyInfo.GetValue(LayerPropertyGroup);
if (value != null)
Children.Add(_layerPropertyVmFactory.PropertyGroupViewModel(value));
}
}
HasChildren = Children.Any(i => i is PropertyViewModel {IsVisible: true} or PropertyGroupViewModel {IsVisible: true});
_keyframeSubscription = Children
.ToObservableChangeSet()
.TransformMany(c => c.Keyframes)
.Bind(out ReadOnlyObservableCollection<ILayerPropertyKeyframe> keyframes)
.Subscribe();
_keyframes = keyframes;
}
private void LayerPropertyGroupOnVisibilityChanged(object? sender, EventArgs e)
{
IsVisible = !LayerPropertyGroup.IsHidden;
}
/// <inheritdoc />
public void Dispose()
{
LayerPropertyGroup.VisibilityChanged -= LayerPropertyGroupOnVisibilityChanged;
while (Children.Any())
{
if (Children[0] is IDisposable disposable)
disposable.Dispose();
Children.RemoveAt(0);
}
_keyframeSubscription.Dispose();
}
}
@@ -1,64 +0,0 @@
using System;
using System.Collections.ObjectModel;
using Artemis.Core;
using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
using Artemis.UI.Screens.ProfileEditor.Properties.Tree;
using DynamicData;
using PropertyChanged.SourceGenerator;
namespace Artemis.UI.Screens.ProfileEditor.Properties;
public partial class PropertyViewModel : PropertyViewModelBase, IDisposable
{
private readonly SourceList<ILayerPropertyKeyframe> _keyframes;
[Notify] private bool _isExpanded;
[Notify] private bool _isHighlighted;
[Notify] private bool _isVisible;
public PropertyViewModel(ILayerProperty layerProperty, IPropertyVmFactory propertyVmFactory)
{
LayerProperty = layerProperty;
TreePropertyViewModel = propertyVmFactory.TreePropertyViewModel(LayerProperty, this);
TimelinePropertyViewModel = propertyVmFactory.TimelinePropertyViewModel(LayerProperty, this);
_isVisible = !LayerProperty.IsHidden;
_keyframes = new SourceList<ILayerPropertyKeyframe>();
_keyframes.Edit(k => k.AddRange(LayerProperty.UntypedKeyframes));
_keyframes.Connect().Bind(out ReadOnlyObservableCollection<ILayerPropertyKeyframe> keyframes).Subscribe();
Keyframes = keyframes;
LayerProperty.VisibilityChanged += LayerPropertyOnVisibilityChanged;
LayerProperty.KeyframeAdded += LayerPropertyOnKeyframeAdded;
LayerProperty.KeyframeRemoved += LayerPropertyOnKeyframeRemoved;
}
public ILayerProperty LayerProperty { get; }
public ITreePropertyViewModel TreePropertyViewModel { get; }
public ITimelinePropertyViewModel TimelinePropertyViewModel { get; }
public override ReadOnlyObservableCollection<ILayerPropertyKeyframe> Keyframes { get; }
private void LayerPropertyOnVisibilityChanged(object? sender, LayerPropertyEventArgs e)
{
IsVisible = !LayerProperty.IsHidden;
}
private void LayerPropertyOnKeyframeAdded(object? sender, LayerPropertyKeyframeEventArgs e)
{
_keyframes.Add(e.Keyframe);
}
private void LayerPropertyOnKeyframeRemoved(object? sender, LayerPropertyKeyframeEventArgs e)
{
_keyframes.Remove(e.Keyframe);
}
/// <inheritdoc />
public void Dispose()
{
LayerProperty.VisibilityChanged -= LayerPropertyOnVisibilityChanged;
LayerProperty.KeyframeAdded -= LayerPropertyOnKeyframeAdded;
LayerProperty.KeyframeRemoved -= LayerPropertyOnKeyframeRemoved;
}
}
@@ -1,10 +0,0 @@
using System.Collections.ObjectModel;
using Artemis.Core;
using Artemis.UI.Shared;
namespace Artemis.UI.Screens.ProfileEditor.Properties;
public abstract class PropertyViewModelBase : ViewModelBase
{
public abstract ReadOnlyObservableCollection<ILayerPropertyKeyframe> Keyframes { get; }
}
@@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using Artemis.Core;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public interface ITimelinePropertyViewModel : IReactiveObject
{
List<ILayerPropertyKeyframe> GetAllKeyframes();
List<ITimelineKeyframeViewModel> GetAllKeyframeViewModels();
void WipeKeyframes(TimeSpan? start, TimeSpan? end);
void ShiftKeyframes(TimeSpan? start, TimeSpan? end, TimeSpan amount);
}
@@ -1,34 +0,0 @@
using System;
using System.Reactive;
using Artemis.Core;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
public interface ITimelineKeyframeViewModel
{
bool IsSelected { get; }
TimeSpan Position { get; }
ILayerPropertyKeyframe Keyframe { get; }
#region Movement
void Select(bool expand, bool toggle);
void StartMovement(ITimelineKeyframeViewModel source);
void UpdateMovement(TimeSpan position);
void FinishMovement();
TimeSpan GetTimeSpanAtPosition(double x);
#endregion
#region Context menu actions
void PopulateEasingViewModels();
ReactiveCommand<Unit, Unit> Duplicate { get; }
ReactiveCommand<Unit, Unit> Copy { get; }
ReactiveCommand<Unit, Unit> Paste { get; }
ReactiveCommand<Unit, Unit> Delete { get; }
#endregion
}
@@ -1,23 +0,0 @@
<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:keyframes="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes"
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.ProfileEditor.Properties.Timeline.Keyframes.TimelineEasingView"
x:DataType="keyframes:TimelineEasingViewModel">
<Grid ColumnDefinitions="25,30,*">
<avalonia:MaterialIcon Grid.Column="0" Kind="Check" IsVisible="{CompiledBinding IsEasingModeSelected}" HorizontalAlignment="Left"/>
<Polyline Grid.Column="1"
Stroke="{DynamicResource TextFillColorPrimaryBrush}"
StrokeThickness="1"
Points="{CompiledBinding EasingPoints}"
Stretch="Uniform"
Width="20"
Height="20"
HorizontalAlignment="Left"/>
<TextBlock Grid.Column="2" Text="{CompiledBinding Description}" HorizontalAlignment="Left"/>
</Grid>
</UserControl>
@@ -1,13 +0,0 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
public partial class TimelineEasingView : UserControl
{
public TimelineEasingView()
{
InitializeComponent();
}
}
@@ -1,37 +0,0 @@
using System.Collections.Generic;
using System.Reactive;
using Artemis.Core;
using Artemis.UI.Shared;
using Avalonia;
using Humanizer;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
public class TimelineEasingViewModel : ViewModelBase
{
private readonly ILayerPropertyKeyframe _keyframe;
public TimelineEasingViewModel(Easings.Functions easingFunction, ILayerPropertyKeyframe keyframe, ReactiveCommand<Easings.Functions, Unit> selectEasingFunction)
{
_keyframe = keyframe;
EasingFunction = easingFunction;
SelectEasingFunction = selectEasingFunction;
Description = easingFunction.Humanize();
EasingPoints = new List<Point>();
for (int i = 1; i <= 10; i++)
{
int x = i;
double y = Easings.Interpolate(i / 10.0, EasingFunction) * 10;
EasingPoints.Add(new Point(x, y));
}
}
public Easings.Functions EasingFunction { get; }
public ReactiveCommand<Easings.Functions, Unit> SelectEasingFunction { get; }
public List<Point> EasingPoints { get; }
public string Description { get; }
public bool IsEasingModeSelected => _keyframe.EasingFunction == EasingFunction;
}
@@ -1,72 +0,0 @@
<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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:keyframes="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes.TimelineKeyframeView"
ClipToBounds="False">
<Ellipse Fill="{DynamicResource SystemAccentColorLight2}"
Stroke="White"
Width="10"
Height="10"
Margin="-5,0,0,0"
ToolTip.Tip="{Binding Timestamp}"
Classes.selected="{Binding IsSelected}"
PointerPressed="InputElement_OnPointerPressed"
PointerReleased="InputElement_OnPointerReleased"
PointerMoved="InputElement_OnPointerMoved"
Cursor="Hand">
<Ellipse.Styles>
<Style Selector="Ellipse">
<Setter Property="StrokeThickness" Value="0" />
</Style>
<Style Selector="Ellipse.selected">
<Setter Property="StrokeThickness" Value="1" />
</Style>
</Ellipse.Styles>
<Ellipse.Transitions>
<Transitions>
<DoubleTransition Property="StrokeThickness" Duration="0:0:0.25" />
</Transitions>
</Ellipse.Transitions>
<Ellipse.ContextFlyout>
<MenuFlyout Opening="FlyoutBase_OnOpening">
<MenuItem Header="Easing" ItemsSource="{Binding EasingViewModels}">
<MenuItem.Styles>
<Style Selector="MenuItem > MenuItem" x:DataType="keyframes:TimelineEasingViewModel">
<Setter Property="Command" Value="{CompiledBinding SelectEasingFunction}" />
<Setter Property="CommandParameter" Value="{CompiledBinding EasingFunction}" />
</Style>
</MenuItem.Styles>
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Creation" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="-" />
<MenuItem Header="Duplicate" Command="{Binding Duplicate}" InputGesture="Ctrl+D">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ContentDuplicate" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Copy" Command="{Binding Copy}" InputGesture="Ctrl+C">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ContentCopy" />
</MenuItem.Icon>
</MenuItem>
<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 Delete}" InputGesture="Delete">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Delete" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Ellipse.ContextFlyout>
</Ellipse>
</UserControl>
@@ -1,83 +0,0 @@
using System;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
public partial class TimelineKeyframeView : ReactiveUserControl<ITimelineKeyframeViewModel>
{
private bool _moved;
private TimelinePropertyView? _timelinePropertyView;
private TimelineView? _timelineView;
public TimelineKeyframeView()
{
InitializeComponent();
}
#region Overrides of TemplatedControl
/// <inheritdoc />
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
_timelineView = this.FindLogicalAncestorOfType<TimelineView>();
_timelinePropertyView = this.FindLogicalAncestorOfType<TimelinePropertyView>();
base.OnAttachedToLogicalTree(e);
}
#endregion
private void InputElement_OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (ViewModel == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
e.Pointer.Capture((IInputElement?) sender);
e.Handled = true;
_moved = false;
}
private void InputElement_OnPointerMoved(object? sender, PointerEventArgs e)
{
if (ViewModel == null || _timelineView?.ViewModel == null || e.Pointer.Captured != sender || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
if (!_moved)
_timelineView.ViewModel.StartKeyframeMovement(ViewModel);
_moved = true;
TimeSpan time = ViewModel.GetTimeSpanAtPosition(e.GetPosition(_timelinePropertyView).X);
_timelineView.ViewModel.UpdateKeyframeMovement(ViewModel, time, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control));
e.Handled = true;
}
private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (ViewModel == null || _timelineView?.ViewModel == null || e.Pointer.Captured != sender || e.InitialPressMouseButton != MouseButton.Left)
return;
e.Pointer.Capture(null);
e.Handled = true;
// Select the keyframe if the user didn't move
if (!_moved)
{
ViewModel.Select(e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control));
}
else
{
TimeSpan time = ViewModel.GetTimeSpanAtPosition(e.GetPosition(_timelinePropertyView).X);
_timelineView.ViewModel.FinishKeyframeMovement(ViewModel, time, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control));
}
}
private void FlyoutBase_OnOpening(object? sender, EventArgs e)
{
ViewModel?.PopulateEasingViewModels();
}
}
@@ -1,249 +0,0 @@
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.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.Input;
using DynamicData;
using DynamicData.Binding;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
public partial class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineKeyframeViewModel
{
private readonly IProfileEditorService _profileEditorService;
private ObservableAsPropertyHelper<bool>? _isSelected;
[Notify] private bool _canPaste;
[Notify] private bool _isFlyoutOpen;
[Notify] private string _timestamp;
[Notify] private double _x;
public TimelineKeyframeViewModel(LayerPropertyKeyframe<T> layerPropertyKeyframe, IProfileEditorService profileEditorService)
{
_profileEditorService = profileEditorService;
_timestamp = "0.000";
LayerPropertyKeyframe = layerPropertyKeyframe;
EasingViewModels = new ObservableCollection<TimelineEasingViewModel>();
this.WhenActivated(d =>
{
_isSelected = profileEditorService.SelectedKeyframes
.ToObservableChangeSet()
.Select(_ => profileEditorService.SelectedKeyframes.Contains(LayerPropertyKeyframe))
.ToProperty(this, vm => vm.IsSelected)
.DisposeWith(d);
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);
SelectEasingFunction = ReactiveCommand.Create<Easings.Functions>(ExecuteSelectEasingFunction);
}
public LayerPropertyKeyframe<T> LayerPropertyKeyframe { get; }
public ObservableCollection<TimelineEasingViewModel> EasingViewModels { get; }
public void Update()
{
X = _pixelsPerSecond * LayerPropertyKeyframe.Position.TotalSeconds;
Timestamp = $"{Math.Floor(LayerPropertyKeyframe.Position.TotalSeconds):00}.{LayerPropertyKeyframe.Position.Milliseconds:000}";
}
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 ReactiveCommand<Easings.Functions, Unit> SelectEasingFunction { get; }
public bool IsSelected => _isSelected?.Value ?? false;
public TimeSpan Position => LayerPropertyKeyframe.Position;
public ILayerPropertyKeyframe Keyframe => LayerPropertyKeyframe;
/// <inheritdoc />
public void Select(bool expand, bool toggle)
{
_profileEditorService.SelectKeyframe(Keyframe, expand, toggle);
}
public TimeSpan GetTimeSpanAtPosition(double x)
{
return TimeSpan.FromSeconds(x / _pixelsPerSecond);
}
#region Context menu actions
private void ExecuteDelete()
{
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));
}
}
private void ExecuteDuplicate()
{
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);
}
}
}
private async Task ExecuteCopy()
{
List<KeyframeClipboardModel> keyframes = new();
if (!IsSelected)
keyframes.Add(new KeyframeClipboardModel(Keyframe));
else
keyframes.AddRange(_profileEditorService.SelectedKeyframes.Select(k => new KeyframeClipboardModel(k)));
string copy = CoreJson.Serialize(keyframes);
DataObject dataObject = new();
dataObject.Set(KeyframeClipboardModel.ClipboardDataFormat, copy);
await Shared.UI.Clipboard.SetDataObjectAsync(dataObject);
}
private async Task ExecutePaste()
{
List<KeyframeClipboardModel>? keyframes = await Shared.UI.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)
{
string[]? formats = await Shared.UI.Clipboard.GetFormatsAsync();
// Can be null on some platforms
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
CanPaste = formats != null && formats.Contains("Artemis.Keyframes");
}
#endregion
#region Movement
private TimeSpan? _offset;
private TimeSpan? _startPosition;
private double _pixelsPerSecond;
public void StartMovement(ITimelineKeyframeViewModel source)
{
_startPosition = LayerPropertyKeyframe.Position;
if (source != this)
_offset = LayerPropertyKeyframe.Position - source.Position;
}
public void UpdateMovement(TimeSpan position)
{
if (_offset == null)
UpdatePosition(position);
else
UpdatePosition(position + _offset.Value);
}
public void FinishMovement()
{
MoveKeyframe command = _startPosition != null
? new MoveKeyframe(Keyframe, Keyframe.Position, _startPosition.Value)
: new MoveKeyframe(Keyframe, Keyframe.Position);
_startPosition = null;
_offset = null;
_profileEditorService.ExecuteCommand(command);
}
private void UpdatePosition(TimeSpan position)
{
if (position < TimeSpan.Zero)
LayerPropertyKeyframe.Position = TimeSpan.Zero;
else if (position > LayerPropertyKeyframe.LayerProperty.ProfileElement.Timeline.Length)
LayerPropertyKeyframe.Position = LayerPropertyKeyframe.LayerProperty.ProfileElement.Timeline.Length;
else
LayerPropertyKeyframe.Position = position;
}
#endregion
#region Easing
public void PopulateEasingViewModels()
{
if (EasingViewModels.Any())
return;
EasingViewModels.AddRange(Enum.GetValues(typeof(Easings.Functions))
.Cast<Easings.Functions>()
.Select(e => new TimelineEasingViewModel(e, Keyframe, SelectEasingFunction)));
}
private void ExecuteSelectEasingFunction(Easings.Functions easingFunction)
{
_profileEditorService.ExecuteCommand(new ChangeKeyframeEasing(Keyframe, easingFunction));
}
#endregion
}
@@ -1,72 +0,0 @@
<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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:segments="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="18"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments.EndSegmentView"
x:DataType="segments:EndSegmentViewModel">
<UserControl.Styles>
<StyleInclude Source="/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/Segment.axaml" />
</UserControl.Styles>
<Border Classes="segment-container">
<Grid Name="SegmentGrid"
Background="{DynamicResource ControlFillColorDefaultBrush}"
Width="{CompiledBinding Width}"
ColumnDefinitions="Auto, Auto,*,Auto">
<Grid.ContextFlyout>
<MenuFlyout>
<MenuItem Header="Edit length" Command="{CompiledBinding EditTime}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Edit" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Remove segment" Command="{CompiledBinding RemoveSegment}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Remove" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Grid.ContextFlyout>
<Rectangle Name="KeyframeDragVisualLeft"
Grid.Column="0"
Classes="resize-visual" />
<Button Grid.Column="1"
Name="AddMainSegment"
Classes="AppBarButton icon-button icon-button-small"
ToolTip.Tip="Add main segment"
Command="{CompiledBinding AddMainSegment}"
IsVisible="{CompiledBinding ShowAddMain}">
<avalonia:MaterialIcon Kind="PlusCircle" />
</Button>
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center" ClipToBounds="True">
<TextBlock Name="SegmentTitle"
FontSize="13"
ToolTip.Tip="This segment is played once a condition is no longer met">
End
</TextBlock>
<Button Name="SegmentClose"
Classes="AppBarButton icon-button icon-button-small"
ToolTip.Tip="Remove this segment"
Command="{CompiledBinding RemoveSegment}">
<avalonia:MaterialIcon Kind="CloseCircle" />
</Button>
</StackPanel>
<Rectangle Name="KeyframeDragVisualRight"
Grid.Column="3"
Classes="resize-visual" />
<Rectangle Name="KeyframeDragAnchor"
Grid.Column="3"
Classes="resize-anchor"
PointerPressed="KeyframeDragAnchor_OnPointerPressed"
PointerMoved="KeyframeDragAnchor_OnPointerMoved"
PointerReleased="KeyframeDragAnchor_OnPointerReleased"
ToolTip.Tip="{CompiledBinding EndTimestamp}" />
</Grid>
</Border>
</UserControl>
@@ -1,41 +0,0 @@
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
public partial class EndSegmentView : ReactiveUserControl<EndSegmentViewModel>
{
private double _dragOffset;
public EndSegmentView()
{
InitializeComponent();
}
private void KeyframeDragAnchor_OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (ViewModel == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
e.Pointer.Capture(KeyframeDragAnchor);
_dragOffset = ViewModel.Width - e.GetCurrentPoint(this).Position.X;
ViewModel.StartResize();
}
private void KeyframeDragAnchor_OnPointerMoved(object? sender, PointerEventArgs e)
{
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, KeyframeDragAnchor))
return;
ViewModel.UpdateResize(e.GetCurrentPoint(this).Position.X + _dragOffset, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control));
}
private void KeyframeDragAnchor_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, KeyframeDragAnchor))
return;
e.Pointer.Capture(null);
ViewModel.FinishResize(e.GetCurrentPoint(this).Position.X + _dragOffset, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control));
}
}
@@ -1,69 +0,0 @@
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
public class EndSegmentViewModel : TimelineSegmentViewModel
{
private readonly ObservableAsPropertyHelper<double> _width;
private ObservableAsPropertyHelper<double>? _end;
private ObservableAsPropertyHelper<string?>? _endTimestamp;
private RenderProfileElement? _profileElement;
private ObservableAsPropertyHelper<double>? _start;
public EndSegmentViewModel(IProfileEditorService profileEditorService, IWindowService windowService) : base(profileEditorService, windowService)
{
this.WhenActivated(d =>
{
profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d);
_start = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.EndSegmentStartPosition) ?? Observable.Never<TimeSpan>())
.Switch()
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
.ToProperty(this, vm => vm.StartX)
.DisposeWith(d);
_end = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.EndSegmentEndPosition) ?? Observable.Never<TimeSpan>())
.Switch()
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
.ToProperty(this, vm => vm.EndX)
.DisposeWith(d);
_endTimestamp = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.EndSegmentEndPosition) ?? Observable.Never<TimeSpan>())
.Switch()
.Select(p => $"{Math.Floor(p.TotalSeconds):00}.{p.Milliseconds:000}")
.ToProperty(this, vm => vm.EndTimestamp)
.DisposeWith(d);
});
_width = this.WhenAnyValue(vm => vm.StartX, vm => vm.EndX).Select(t => t.Item2 - t.Item1).ToProperty(this, vm => vm.Width);
}
public override TimeSpan Start => _profileElement?.Timeline.EndSegmentStartPosition ?? TimeSpan.Zero;
public override double StartX => _start?.Value ?? 0;
public override TimeSpan End => _profileElement?.Timeline.EndSegmentEndPosition ?? TimeSpan.Zero;
public override double EndX => _end?.Value ?? 0;
public override TimeSpan Length
{
get => _profileElement?.Timeline.EndSegmentLength ?? TimeSpan.Zero;
set
{
if (_profileElement != null)
_profileElement.Timeline.EndSegmentLength = value;
this.RaisePropertyChanged(nameof(Length));
}
}
public override double Width => _width.Value;
public override string? EndTimestamp => _endTimestamp?.Value;
public override ResizeTimelineSegment.SegmentType Type => ResizeTimelineSegment.SegmentType.End;
}
@@ -1,86 +0,0 @@
<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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:segments="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="18"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments.MainSegmentView"
x:DataType="segments:MainSegmentViewModel">
<UserControl.Styles>
<StyleInclude Source="/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/Segment.axaml" />
</UserControl.Styles>
<Border Classes="segment-container">
<Grid Name="SegmentGrid"
Background="{DynamicResource ControlFillColorDefaultBrush}"
Width="{CompiledBinding Width}"
ColumnDefinitions="Auto,Auto,*,Auto,Auto">
<Grid.ContextFlyout>
<MenuFlyout>
<MenuItem Header="Edit length" Command="{CompiledBinding EditTime}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Edit" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Remove segment" Command="{CompiledBinding RemoveSegment}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Remove" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Grid.ContextFlyout>
<Rectangle Name="KeyframeDragVisualLeft"
Grid.Column="0"
IsVisible="{CompiledBinding !ShowAddStart}"
Classes="resize-visual" />
<Button Name="AddStartSegment"
Grid.Column="1"
Classes="AppBarButton icon-button icon-button-small"
VerticalAlignment="Center"
ToolTip.Tip="Add a start segment"
Command="{CompiledBinding AddStartSegment}"
IsVisible="{CompiledBinding ShowAddStart}">
<avalonia:MaterialIcon Kind="PlusCircle" />
</Button>
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Name="SegmentTitle"
VerticalAlignment="Center"
HorizontalAlignment="Center"
FontSize="13"
ToolTip.Tip="This segment is played while a condition is met, either once or on a repeating loop">
Main
</TextBlock>
<Button Name="SegmentClose"
Classes="AppBarButton icon-button icon-button-small"
ToolTip.Tip="Remove this segment"
Command="{CompiledBinding RemoveSegment}">
<avalonia:MaterialIcon Kind="CloseCircle" />
</Button>
</StackPanel>
<Button Name="AddEndSegment"
Grid.Column="3"
Classes="AppBarButton icon-button icon-button-small"
VerticalAlignment="Center"
ToolTip.Tip="Add an end segment"
Command="{CompiledBinding AddEndSegment}"
IsVisible="{CompiledBinding ShowAddEnd}">
<avalonia:MaterialIcon Kind="PlusCircle" />
</Button>
<Rectangle Name="KeyframeDragVisual"
Grid.Column="4"
Classes="resize-visual" />
<Rectangle Name="KeyframeDragAnchor"
Grid.Column="4"
Classes="resize-anchor"
PointerPressed="KeyframeDragAnchor_OnPointerPressed"
PointerMoved="KeyframeDragAnchor_OnPointerMoved"
PointerReleased="KeyframeDragAnchor_OnPointerReleased"
ToolTip.Tip="{CompiledBinding EndTimestamp}" />
</Grid>
</Border>
</UserControl>
@@ -1,53 +0,0 @@
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
public partial class MainSegmentView : ReactiveUserControl<MainSegmentViewModel>
{
private double _dragOffset;
public MainSegmentView()
{
InitializeComponent();
}
private void KeyframeDragAnchor_OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (ViewModel == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
e.Pointer.Capture(KeyframeDragAnchor);
_dragOffset = ViewModel.Width - e.GetCurrentPoint(this).Position.X;
ViewModel.StartResize();
}
private void KeyframeDragAnchor_OnPointerMoved(object? sender, PointerEventArgs e)
{
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, KeyframeDragAnchor))
return;
ViewModel.UpdateResize(e.GetCurrentPoint(this).Position.X + _dragOffset, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control));
}
private void KeyframeDragAnchor_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, KeyframeDragAnchor))
return;
e.Pointer.Capture(null);
ViewModel.FinishResize(e.GetCurrentPoint(this).Position.X + _dragOffset, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control));
}
private void KeyframeDragStartAnchor_OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
}
private void KeyframeDragStartAnchor_OnPointerMoved(object? sender, PointerEventArgs e)
{
}
private void KeyframeDragStartAnchor_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
}
}
@@ -1,68 +0,0 @@
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
public class MainSegmentViewModel : TimelineSegmentViewModel
{
private readonly ObservableAsPropertyHelper<double> _width;
private ObservableAsPropertyHelper<double>? _end;
private ObservableAsPropertyHelper<string?>? _endTimestamp;
private RenderProfileElement? _profileElement;
private ObservableAsPropertyHelper<double>? _start;
public MainSegmentViewModel(IProfileEditorService profileEditorService, IWindowService windowService) : base(profileEditorService, windowService)
{
this.WhenActivated(d =>
{
profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d);
_start = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.MainSegmentStartPosition) ?? Observable.Never<TimeSpan>())
.Switch()
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
.ToProperty(this, vm => vm.StartX)
.DisposeWith(d);
_end = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.MainSegmentEndPosition) ?? Observable.Never<TimeSpan>())
.Switch()
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
.ToProperty(this, vm => vm.EndX)
.DisposeWith(d);
_endTimestamp = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.MainSegmentEndPosition) ?? Observable.Never<TimeSpan>())
.Switch()
.Select(p => $"{Math.Floor(p.TotalSeconds):00}.{p.Milliseconds:000}")
.ToProperty(this, vm => vm.EndTimestamp)
.DisposeWith(d);
});
_width = this.WhenAnyValue(vm => vm.StartX, vm => vm.EndX).Select(t => t.Item2 - t.Item1).ToProperty(this, vm => vm.Width);
}
public override TimeSpan Start => _profileElement?.Timeline.MainSegmentStartPosition ?? TimeSpan.Zero;
public override double StartX => _start?.Value ?? 0;
public override TimeSpan End => _profileElement?.Timeline.MainSegmentEndPosition ?? TimeSpan.Zero;
public override double EndX => _end?.Value ?? 0;
public override TimeSpan Length
{
get => _profileElement?.Timeline.MainSegmentLength ?? TimeSpan.Zero;
set
{
if (_profileElement != null)
_profileElement.Timeline.MainSegmentLength = value;
this.RaisePropertyChanged(nameof(Length));
}
}
public override double Width => _width.Value;
public override string? EndTimestamp => _endTimestamp?.Value;
public override ResizeTimelineSegment.SegmentType Type => ResizeTimelineSegment.SegmentType.Main;
}
@@ -1,27 +0,0 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<Border Padding="20">
<!-- Add Controls for Previewer Here -->
</Border>
</Design.PreviewWith>
<Style Selector="ContentControl.segment-content-control" />
<Style Selector="Border.segment-container">
<Setter Property="Height" Value="17" />
</Style>
<Style Selector="Rectangle.resize-visual">
<Setter Property="Fill" Value="{DynamicResource SystemAccentColorLight2}" />
<Setter Property="Width" Value="4" />
<Setter Property="HorizontalAlignment" Value="Right" />
</Style>
<Style Selector="Rectangle.resize-anchor">
<Setter Property="Fill" Value="Transparent" />
<Setter Property="Width" Value="10" />
<Setter Property="HorizontalAlignment" Value="Right" />
<Setter Property="Cursor" Value="SizeWestEast" />
</Style>
</Styles>
@@ -1,69 +0,0 @@
<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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:segments="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="18"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments.StartSegmentView"
x:DataType="segments:StartSegmentViewModel">
<UserControl.Styles>
<StyleInclude Source="/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/Segment.axaml" />
</UserControl.Styles>
<Border Classes="segment-container">
<Grid Name="SegmentGrid"
Background="{DynamicResource ControlFillColorDefaultBrush}"
Width="{CompiledBinding Width}"
ColumnDefinitions="*,Auto,Auto">
<Grid.ContextFlyout>
<MenuFlyout>
<MenuItem Header="Edit length" Command="{CompiledBinding EditTime}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Edit" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Remove segment" Command="{CompiledBinding RemoveSegment}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Remove" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Grid.ContextFlyout>
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Name="SegmentTitle"
FontSize="13"
ToolTip.Tip="This segment is played when a layer starts displaying because it's conditions are met">
Start
</TextBlock>
<Button Name="SegmentClose"
Classes="AppBarButton icon-button icon-button-small"
ToolTip.Tip="Remove this segment"
Command="{CompiledBinding RemoveSegment}">
<avalonia:MaterialIcon Kind="CloseCircle" />
</Button>
</StackPanel>
<Button Grid.Column="1"
Name="AddMainSegment"
Classes="AppBarButton icon-button icon-button-small"
ToolTip.Tip="Add main segment"
Command="{CompiledBinding AddMainSegment}"
IsVisible="{CompiledBinding ShowAddMain}">
<avalonia:MaterialIcon Kind="PlusCircle" />
</Button>
<Rectangle Name="KeyframeDragVisual"
Grid.Column="2"
Classes="resize-visual" />
<Rectangle Name="KeyframeDragAnchor"
Grid.Column="2"
Classes="resize-anchor"
PointerPressed="KeyframeDragAnchor_OnPointerPressed"
PointerMoved="KeyframeDragAnchor_OnPointerMoved"
PointerReleased="KeyframeDragAnchor_OnPointerReleased"
ToolTip.Tip="{CompiledBinding EndTimestamp}" />
</Grid>
</Border>
</UserControl>
@@ -1,41 +0,0 @@
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
public partial class StartSegmentView : ReactiveUserControl<StartSegmentViewModel>
{
private double _dragOffset;
public StartSegmentView()
{
InitializeComponent();
}
private void KeyframeDragAnchor_OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (ViewModel == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
e.Pointer.Capture(KeyframeDragAnchor);
_dragOffset = ViewModel.Width - e.GetCurrentPoint(this).Position.X;
ViewModel.StartResize();
}
private void KeyframeDragAnchor_OnPointerMoved(object? sender, PointerEventArgs e)
{
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, KeyframeDragAnchor))
return;
ViewModel.UpdateResize(e.GetCurrentPoint(this).Position.X + _dragOffset, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control));
}
private void KeyframeDragAnchor_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, KeyframeDragAnchor))
return;
e.Pointer.Capture(null);
ViewModel.FinishResize(e.GetCurrentPoint(this).Position.X + _dragOffset, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control));
}
}
@@ -1,62 +0,0 @@
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
public class StartSegmentViewModel : TimelineSegmentViewModel
{
private readonly ObservableAsPropertyHelper<double> _width;
private ObservableAsPropertyHelper<double>? _end;
private ObservableAsPropertyHelper<string?>? _endTimestamp;
private RenderProfileElement? _profileElement;
public StartSegmentViewModel(IProfileEditorService profileEditorService, IWindowService windowService) : base(profileEditorService, windowService)
{
this.WhenActivated(d =>
{
profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d);
_end = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.StartSegmentEndPosition) ?? Observable.Never<TimeSpan>())
.Switch()
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
.ToProperty(this, vm => vm.EndX)
.DisposeWith(d);
_endTimestamp = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.StartSegmentEndPosition) ?? Observable.Never<TimeSpan>())
.Switch()
.Select(p => $"{Math.Floor(p.TotalSeconds):00}.{p.Milliseconds:000}")
.ToProperty(this, vm => vm.EndTimestamp)
.DisposeWith(d);
});
_width = this.WhenAnyValue(vm => vm.StartX, vm => vm.EndX).Select(t => t.Item2 - t.Item1).ToProperty(this, vm => vm.Width);
}
public override TimeSpan Start => TimeSpan.Zero;
public override double StartX => 0;
public override TimeSpan End => _profileElement?.Timeline.StartSegmentEndPosition ?? TimeSpan.Zero;
public override double EndX => _end?.Value ?? 0;
public override TimeSpan Length
{
get => _profileElement?.Timeline.StartSegmentLength ?? TimeSpan.Zero;
set
{
if (_profileElement != null)
_profileElement.Timeline.StartSegmentLength = value;
this.RaisePropertyChanged(nameof(Length));
}
}
public override double Width => _width.Value;
public override string? EndTimestamp => _endTimestamp?.Value;
public override ResizeTimelineSegment.SegmentType Type => ResizeTimelineSegment.SegmentType.Start;
}
@@ -1,239 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Screens.ProfileEditor.Properties.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 PropertyChanged.SourceGenerator;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
public abstract partial class TimelineSegmentViewModel : ActivatableViewModelBase
{
private static readonly TimeSpan NewSegmentLength = TimeSpan.FromSeconds(2);
private readonly Dictionary<ILayerPropertyKeyframe, TimeSpan> _originalKeyframePositions = new();
private readonly IProfileEditorService _profileEditorService;
private readonly IWindowService _windowService;
private TimeSpan _initialLength;
private int _pixelsPerSecond;
private RenderProfileElement? _profileElement;
private ObservableAsPropertyHelper<bool>? _showAddEnd;
private ObservableAsPropertyHelper<bool>? _showAddMain;
private ObservableAsPropertyHelper<bool>? _showAddStart;
[Notify] private ReactiveCommand<Unit, Unit>? _removeSegment;
protected TimelineSegmentViewModel(IProfileEditorService profileEditorService, IWindowService windowService)
{
_profileEditorService = profileEditorService;
_windowService = windowService;
EditTime = ReactiveCommand.CreateFromTask(ExecuteEditTime);
this.WhenActivated(d =>
{
profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d);
profileEditorService.PixelsPerSecond.Subscribe(p => _pixelsPerSecond = p).DisposeWith(d);
_showAddStart = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.StartSegmentLength) ?? Observable.Never<TimeSpan>())
.Switch()
.Select(t => t == TimeSpan.Zero)
.ToProperty(this, vm => vm.ShowAddStart)
.DisposeWith(d);
_showAddMain = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.MainSegmentLength) ?? Observable.Never<TimeSpan>())
.Switch()
.Select(t => t == TimeSpan.Zero)
.ToProperty(this, vm => vm.ShowAddMain)
.DisposeWith(d);
_showAddEnd = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.EndSegmentLength) ?? Observable.Never<TimeSpan>())
.Switch()
.Select(t => t == TimeSpan.Zero)
.ToProperty(this, vm => vm.ShowAddEnd)
.DisposeWith(d);
if (Type == ResizeTimelineSegment.SegmentType.Start)
RemoveSegment = ReactiveCommand.Create(
ExecuteRemoveSegment,
this.WhenAnyValue(vm => vm.ShowAddMain).CombineLatest(this.WhenAnyValue(vm => vm.ShowAddEnd)).Select(tuple => !tuple.First || !tuple.Second)
);
if (Type == ResizeTimelineSegment.SegmentType.Main)
RemoveSegment = ReactiveCommand.Create(
ExecuteRemoveSegment,
this.WhenAnyValue(vm => vm.ShowAddEnd).CombineLatest(this.WhenAnyValue(vm => vm.ShowAddStart)).Select(tuple => !tuple.First || !tuple.Second)
);
if (Type == ResizeTimelineSegment.SegmentType.End)
RemoveSegment = ReactiveCommand.Create(
ExecuteRemoveSegment,
this.WhenAnyValue(vm => vm.ShowAddStart).CombineLatest(this.WhenAnyValue(vm => vm.ShowAddMain)).Select(tuple => !tuple.First || !tuple.Second)
);
});
}
public bool ShowAddStart => _showAddStart?.Value ?? false;
public bool ShowAddMain => _showAddMain?.Value ?? false;
public bool ShowAddEnd => _showAddEnd?.Value ?? false;
public abstract TimeSpan Start { get; }
public abstract double StartX { get; }
public abstract TimeSpan End { get; }
public abstract double EndX { get; }
public abstract TimeSpan Length { get; set; }
public abstract double Width { get; }
public abstract string? EndTimestamp { get; }
public abstract ResizeTimelineSegment.SegmentType Type { get; }
public ReactiveCommand<Unit, Unit> EditTime { get; }
public void AddStartSegment()
{
if (_profileElement == null)
return;
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Add start segment");
ShiftKeyframes(_profileElement.GetAllLayerProperties().SelectMany(p => p.UntypedKeyframes), NewSegmentLength);
ApplyPendingKeyframeMovement();
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(ResizeTimelineSegment.SegmentType.Start, _profileElement, NewSegmentLength));
}
public void AddMainSegment()
{
if (_profileElement == null)
return;
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Add main segment");
ShiftKeyframes(_profileElement.GetAllLayerProperties().SelectMany(p => p.UntypedKeyframes).Where(s => s.Position > _profileElement.Timeline.StartSegmentEndPosition), NewSegmentLength);
ApplyPendingKeyframeMovement();
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(ResizeTimelineSegment.SegmentType.Main, _profileElement, NewSegmentLength));
}
public void AddEndSegment()
{
if (_profileElement == null)
return;
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Add end segment");
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(ResizeTimelineSegment.SegmentType.End, _profileElement, NewSegmentLength));
}
public void StartResize()
{
if (_profileElement == null)
return;
_initialLength = Length;
}
public void UpdateResize(double x, bool snap, bool round)
{
if (_profileElement == null)
return;
TimeSpan time = TimeSpan.FromMilliseconds(Math.Max(GetTimeFromX(x, snap, round).TotalMilliseconds, 100));
TimeSpan difference = time - Length;
List<ILayerPropertyKeyframe> keyframes = _profileElement.GetAllLayerProperties().SelectMany(p => p.UntypedKeyframes).ToList();
ShiftKeyframes(keyframes.Where(k => k.Position > End.Add(difference)), difference);
Length = time;
}
public void FinishResize(double x, bool snap, bool round)
{
if (_profileElement == null)
return;
TimeSpan time = TimeSpan.FromMilliseconds(Math.Max(GetTimeFromX(x, snap, round).TotalMilliseconds, 100));
if (_initialLength == time)
return;
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Resize segment");
ApplyPendingKeyframeMovement();
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(Type, _profileElement, time, _initialLength));
}
protected TimeSpan GetTimeFromX(double x, bool snap, bool round)
{
TimeSpan time = TimeSpan.FromSeconds(x / _pixelsPerSecond);
if (time < TimeSpan.Zero)
time = TimeSpan.Zero;
if (round)
time = _profileEditorService.RoundTime(time);
if (snap)
time = SnapToTimeline(time);
return time;
}
protected void ShiftKeyframes(IEnumerable<ILayerPropertyKeyframe> keyframes, TimeSpan amount)
{
foreach (ILayerPropertyKeyframe layerPropertyKeyframe in keyframes)
{
if (!_originalKeyframePositions.ContainsKey(layerPropertyKeyframe))
_originalKeyframePositions[layerPropertyKeyframe] = layerPropertyKeyframe.Position;
layerPropertyKeyframe.Position = layerPropertyKeyframe.Position.Add(amount);
}
}
protected void ApplyPendingKeyframeMovement()
{
foreach ((ILayerPropertyKeyframe keyframe, TimeSpan originalPosition) in _originalKeyframePositions)
_profileEditorService.ExecuteCommand(new MoveKeyframe(keyframe, keyframe.Position, originalPosition));
_originalKeyframePositions.Clear();
}
private void ExecuteRemoveSegment()
{
if (_profileElement == null)
return;
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Remove segment");
IEnumerable<ILayerPropertyKeyframe> keyframes = _profileElement.GetAllLayerProperties().SelectMany(p => p.UntypedKeyframes);
// Delete keyframes in the segment
foreach (ILayerPropertyKeyframe layerPropertyKeyframe in keyframes)
{
if (layerPropertyKeyframe.Position > Start && layerPropertyKeyframe.Position <= End)
_profileEditorService.ExecuteCommand(new DeleteKeyframe(layerPropertyKeyframe));
}
// Move keyframes after the segment forwards
ShiftKeyframes(keyframes.Where(s => s.Position > End), new TimeSpan(Length.Ticks * -1));
ApplyPendingKeyframeMovement();
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(Type, _profileElement, TimeSpan.Zero));
}
private async Task ExecuteEditTime()
{
await _windowService.CreateContentDialog()
.WithTitle("Edit segment length")
.WithViewModel(out TimelineSegmentEditViewModel vm, Length)
.HavingPrimaryButton(b => b.WithText("Save").WithAction(() =>
{
if (_profileElement != null)
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(Type, _profileElement, TimeSpan.FromSeconds(vm.SegmentLength)));
}))
.WithDefaultButton(ContentDialogButton.Primary)
.WithCloseButtonText("Cancel")
.ShowAsync();
}
private TimeSpan SnapToTimeline(TimeSpan time)
{
TimeSpan tolerance = TimeSpan.FromMilliseconds(1000f / _pixelsPerSecond * 5);
return _profileEditorService.SnapToTimeline(time, tolerance, false, true);
}
}
@@ -1,57 +0,0 @@
<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:properties="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties"
xmlns:timeline="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline"
xmlns:system="clr-namespace:System;assembly=netstandard"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.TimelineGroupView"
x:DataType="timeline:TimelineGroupViewModel">
<Grid RowDefinitions="28,1,Auto">
<ItemsControl Grid.Row="0"
Height="28"
IsVisible="{CompiledBinding !PropertyGroupViewModel.IsExpanded}"
ItemsSource="{CompiledBinding KeyframePositions}"
HorizontalAlignment="Stretch">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Styles>
<Style Selector="ContentPresenter" x:DataType="system:Double">
<Setter Property="Canvas.Left" Value="{CompiledBinding}" />
<Setter Property="Canvas.Top" Value="9" />
</Style>
</ItemsControl.Styles>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Fill="{DynamicResource ControlSolidFillColorDefaultBrush}"
Stroke="White"
StrokeThickness="0"
Width="10"
Height="10"
Margin="-5 0 0 0" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Rectangle Grid.Row="1" HorizontalAlignment="Stretch" Fill="{DynamicResource ButtonBorderBrush}" Height="1" />
<ItemsControl Grid.Row="2"
ItemsSource="{CompiledBinding Children}"
IsVisible="{CompiledBinding PropertyGroupViewModel.IsExpanded}"
HorizontalAlignment="Stretch">
<ItemsControl.DataTemplates>
<DataTemplate DataType="properties:PropertyGroupViewModel">
<ContentControl Content="{CompiledBinding TimelineGroupViewModel}" IsVisible="{CompiledBinding IsVisible}" />
</DataTemplate>
<DataTemplate DataType="properties:PropertyViewModel">
<ContentControl Content="{CompiledBinding TimelinePropertyViewModel}" IsVisible="{CompiledBinding IsVisible}" />
</DataTemplate>
</ItemsControl.DataTemplates>
</ItemsControl>
</Grid>
</UserControl>
@@ -1,13 +0,0 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public partial class TimelineGroupView : ReactiveUserControl<TimelineGroupViewModel>
{
public TimelineGroupView()
{
InitializeComponent();
}
}
@@ -1,43 +0,0 @@
using System;
using System.Collections.ObjectModel;
using System.Reactive.Disposables;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using DynamicData;
using DynamicData.Binding;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public partial class TimelineGroupViewModel : ActivatableViewModelBase
{
private int _pixelsPerSecond;
[Notify] private ReadOnlyObservableCollection<double> _keyframePositions;
public TimelineGroupViewModel(PropertyGroupViewModel propertyGroupViewModel, IProfileEditorService profileEditorService)
{
PropertyGroupViewModel = propertyGroupViewModel;
_keyframePositions = new ReadOnlyObservableCollection<double>(new ObservableCollection<double>());
this.WhenActivated(d =>
{
profileEditorService.PixelsPerSecond.Subscribe(p => _pixelsPerSecond = p).DisposeWith(d);
PropertyGroupViewModel.WhenAnyValue(vm => vm.IsExpanded).Subscribe(_ => this.RaisePropertyChanged(nameof(Children))).DisposeWith(d);
PropertyGroupViewModel.Keyframes
.ToObservableChangeSet()
.AutoRefreshOnObservable(_ => profileEditorService.PixelsPerSecond)
.AutoRefreshOnObservable(k => k.WhenAnyValue(kv => kv.Position))
.Transform(k => k.Position.TotalSeconds * _pixelsPerSecond, true)
.Bind(out ReadOnlyObservableCollection<double> keyframePositions)
.Subscribe()
.DisposeWith(d);
KeyframePositions = keyframePositions;
});
}
public PropertyGroupViewModel PropertyGroupViewModel { get; }
public ObservableCollection<PropertyViewModelBase>? Children => PropertyGroupViewModel.IsExpanded ? PropertyGroupViewModel.Children : null;
}
@@ -1,22 +0,0 @@
<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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.TimelinePropertyView">
<Border Height="29" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource ButtonBorderBrush}">
<ItemsControl ItemsSource="{Binding KeyframeViewModels}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Styles>
<Style Selector="ItemsControl > ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X, TargetNullValue=0}" />
<Setter Property="Canvas.Top" Value="9" />
</Style>
</ItemsControl.Styles>
</ItemsControl>
</Border>
</UserControl>
@@ -1,13 +0,0 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public partial class TimelinePropertyView : ReactiveUserControl<ITimelinePropertyViewModel>
{
public TimelinePropertyView()
{
InitializeComponent();
}
}
@@ -1,104 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using DynamicData;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public class TimelinePropertyViewModel<T> : ActivatableViewModelBase, ITimelinePropertyViewModel
{
private readonly SourceList<LayerPropertyKeyframe<T>> _keyframes;
private readonly IProfileEditorService _profileEditorService;
private ObservableAsPropertyHelper<bool>? _keyframesEnabled;
public TimelinePropertyViewModel(LayerProperty<T> layerProperty, PropertyViewModel propertyViewModel, IProfileEditorService profileEditorService)
{
_profileEditorService = profileEditorService;
LayerProperty = layerProperty;
PropertyViewModel = propertyViewModel;
_keyframes = new SourceList<LayerPropertyKeyframe<T>>();
_keyframes.Connect()
// Only show items when keyframes are enabled
.Filter(this.WhenAnyValue(vm => vm.KeyframesEnabled).Select(b => new Func<LayerPropertyKeyframe<T>, bool>(_ => b)))
.Transform(k => new TimelineKeyframeViewModel<T>(k, _profileEditorService))
.Bind(out ReadOnlyObservableCollection<TimelineKeyframeViewModel<T>> keyframeViewModels)
.Subscribe();
KeyframeViewModels = keyframeViewModels;
this.WhenActivated(d =>
{
_keyframesEnabled = LayerProperty.WhenAnyValue(p => p.KeyframesEnabled).ToProperty(this, vm => vm.KeyframesEnabled).DisposeWith(d);
Observable.FromEventPattern<LayerPropertyKeyframeEventArgs>(x => LayerProperty.KeyframeAdded += x, x => LayerProperty.KeyframeAdded -= x)
.Subscribe(e => _keyframes.Add((LayerPropertyKeyframe<T>) e.EventArgs.Keyframe))
.DisposeWith(d);
Observable.FromEventPattern<LayerPropertyKeyframeEventArgs>(x => LayerProperty.KeyframeRemoved += x, x => LayerProperty.KeyframeRemoved -= x)
.Subscribe(e => _keyframes.RemoveMany(_keyframes.Items.Where(k => k == e.EventArgs.Keyframe)))
.DisposeWith(d);
_keyframes.Edit(k =>
{
k.Clear();
k.AddRange(LayerProperty.Keyframes);
});
});
}
public LayerProperty<T> LayerProperty { get; }
public PropertyViewModel PropertyViewModel { get; }
public ReadOnlyObservableCollection<TimelineKeyframeViewModel<T>> KeyframeViewModels { get; }
public bool KeyframesEnabled => _keyframesEnabled?.Value ?? false;
private void UpdateKeyframes()
{
foreach (TimelineKeyframeViewModel<T> timelineKeyframeViewModel in KeyframeViewModels)
timelineKeyframeViewModel.Update();
}
#region Implementation of ITimelinePropertyViewModel
public List<ILayerPropertyKeyframe> GetAllKeyframes()
{
return LayerProperty.KeyframesEnabled ? new List<ILayerPropertyKeyframe>(LayerProperty.Keyframes) : new List<ILayerPropertyKeyframe>();
}
public List<ITimelineKeyframeViewModel> GetAllKeyframeViewModels()
{
return KeyframeViewModels.Cast<ITimelineKeyframeViewModel>().ToList();
}
public void WipeKeyframes(TimeSpan? start, TimeSpan? end)
{
start ??= TimeSpan.Zero;
end ??= TimeSpan.MaxValue;
List<LayerPropertyKeyframe<T>> toShift = LayerProperty.Keyframes.Where(k => k.Position >= start && k.Position < end).ToList();
foreach (LayerPropertyKeyframe<T> keyframe in toShift)
LayerProperty.RemoveKeyframe(keyframe);
UpdateKeyframes();
}
public void ShiftKeyframes(TimeSpan? start, TimeSpan? end, TimeSpan amount)
{
start ??= TimeSpan.Zero;
end ??= TimeSpan.MaxValue;
List<LayerPropertyKeyframe<T>> toShift = LayerProperty.Keyframes.Where(k => k.Position > start && k.Position < end).ToList();
foreach (LayerPropertyKeyframe<T> keyframe in toShift)
keyframe.Position += amount;
UpdateKeyframes();
}
#endregion
}
@@ -1,40 +0,0 @@
<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:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties"
xmlns:timeline="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.TimelineView"
x:DataType="timeline:TimelineViewModel">
<UserControl.Resources>
</UserControl.Resources>
<Grid Background="Transparent" PointerReleased="InputElement_OnPointerReleased" Focusable="True" MinWidth="{CompiledBinding MinWidth}">
<Grid.KeyBindings>
<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 ItemsSource="{CompiledBinding PropertyGroupViewModels}">
<ItemsControl.ItemTemplate>
<TreeDataTemplate DataType="{x:Type local:PropertyGroupViewModel}" ItemsSource="{CompiledBinding Children}">
<ContentControl Content="{CompiledBinding TimelineGroupViewModel}" />
</TreeDataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<shared:SelectionRectangle Name="SelectionRectangle"
InputElement="{CompiledBinding $parent}"
BorderBrush="{DynamicResource SystemAccentColor}"
BorderRadius="8"
BorderThickness="1"
SelectionFinished="SelectionRectangle_OnSelectionFinished">
<shared:SelectionRectangle.Background>
<SolidColorBrush Color="{DynamicResource SystemAccentColorLight1}" Opacity="0.2" />
</shared:SelectionRectangle.Background>
</shared:SelectionRectangle>
</Grid>
</UserControl>
@@ -1,44 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
using Artemis.UI.Shared.Events;
using Artemis.UI.Shared.Extensions;
using Avalonia;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using Avalonia.VisualTree;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public partial class TimelineView : ReactiveUserControl<TimelineViewModel>
{
public TimelineView()
{
InitializeComponent();
}
private void SelectionRectangle_OnSelectionFinished(object? sender, SelectionRectangleEventArgs e)
{
if (ViewModel == null)
return;
List<TimelineKeyframeView> keyframeViews = this.GetVisualChildrenOfType<TimelineKeyframeView>().Where(k =>
{
TransformedBounds? transformedBounds = k.GetTransformedBounds();
Rect hitTestRect = transformedBounds != null ? transformedBounds.Value.Bounds.TransformToAABB(transformedBounds.Value.Transform) : new Rect();
return e.AbsoluteRectangle.Intersects(hitTestRect);
}).ToList();
ViewModel.SelectKeyframes(keyframeViews.Where(kv => kv.ViewModel != null).Select(kv => kv.ViewModel!), e.KeyModifiers.HasFlag(KeyModifiers.Shift));
}
private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (SelectionRectangle.IsSelecting)
return;
ViewModel?.SelectKeyframes(new List<ITimelineKeyframeViewModel>(), false);
}
}
@@ -1,191 +0,0 @@
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.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 ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public class TimelineViewModel : ActivatableViewModelBase
{
private readonly IProfileEditorService _profileEditorService;
private ObservableAsPropertyHelper<double>? _caretPosition;
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,
MainSegmentViewModel mainSegmentViewModel,
EndSegmentViewModel endSegmentViewModel,
IProfileEditorService profileEditorService)
{
PropertyGroupViewModels = propertyGroupViewModels;
StartSegmentViewModel = startSegmentViewModel;
MainSegmentViewModel = mainSegmentViewModel;
EndSegmentViewModel = endSegmentViewModel;
_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)
.DisposeWith(d);
_pixelsPerSecond = _profileEditorService.PixelsPerSecond.ToProperty(this, vm => vm.PixelsPerSecond).DisposeWith(d);
_minWidth = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.Length) ?? Observable.Never<TimeSpan>())
.Switch()
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p + 100)
.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;
public double MinWidth => _minWidth?.Value ?? 0;
public void ChangeTime(TimeSpan newTime)
{
_profileEditorService.ChangeTime(newTime);
}
public TimeSpan SnapToTimeline(TimeSpan time, bool snapToSegments, bool snapToCurrentTime, List<TimeSpan>? snapTimes = null)
{
TimeSpan tolerance = TimeSpan.FromMilliseconds(1000f / PixelsPerSecond * 5);
return _profileEditorService.SnapToTimeline(time, tolerance, snapToSegments, snapToCurrentTime, snapTimes);
}
public void SelectKeyframes(IEnumerable<ITimelineKeyframeViewModel> keyframes, bool expand)
{
_profileEditorService.SelectKeyframes(keyframes.Select(k => k.Keyframe), expand);
}
#region Keyframe movement
public void StartKeyframeMovement(ITimelineKeyframeViewModel source)
{
if (!source.IsSelected)
SelectKeyframes(new List<ITimelineKeyframeViewModel> {source}, false);
_moveKeyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Where(k => k.IsSelected).ToList();
source.StartMovement(source);
foreach (ITimelineKeyframeViewModel keyframeViewModel in _moveKeyframes)
keyframeViewModel.StartMovement(source);
}
public void UpdateKeyframeMovement(ITimelineKeyframeViewModel source, TimeSpan time, bool snap, bool round)
{
if (_moveKeyframes == null)
return;
if (round)
time = _profileEditorService.RoundTime(time);
if (snap)
time = SnapToTimeline(time, true, true);
// Always update source first
source.UpdateMovement(time);
foreach (ITimelineKeyframeViewModel keyframeViewModel in _moveKeyframes)
keyframeViewModel.UpdateMovement(time);
}
public void FinishKeyframeMovement(ITimelineKeyframeViewModel source, TimeSpan time, bool snap, bool round)
{
if (_moveKeyframes == null)
return;
if (round)
time = _profileEditorService.RoundTime(time);
if (snap)
time = SnapToTimeline(time, true, true);
// If only one selected it's always the source, update it as a single command
if (_moveKeyframes.Count == 1)
{
source.UpdateMovement(time);
source.FinishMovement();
}
// Otherwise update in a scope
else
{
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope($"Move {_moveKeyframes.Count} keyframes.");
// Always update source first
source.UpdateMovement(time);
foreach (ITimelineKeyframeViewModel keyframeViewModel in _moveKeyframes)
{
keyframeViewModel.UpdateMovement(time);
keyframeViewModel.FinishMovement();
}
}
}
#endregion
#region Keyframe actions
private void ExecuteDuplicateSelectedKeyframes()
{
PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).FirstOrDefault(k => k.IsSelected)?.Duplicate.Execute().Subscribe();
}
private void ExecuteCopySelectedKeyframes()
{
PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).FirstOrDefault(k => k.IsSelected)?.Copy.Execute().Subscribe();
}
private async Task ExecutePasteKeyframes()
{
if (_profileElement == null)
return;
List<KeyframeClipboardModel>? keyframes = await Shared.UI.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);
}
private void ExecuteDeleteSelectedKeyframes()
{
PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).FirstOrDefault(k => k.IsSelected)?.Delete.Execute().Subscribe();
}
#endregion
}
@@ -1,12 +0,0 @@
<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:contentDialogs="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Tree.ContentDialogs"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Tree.ContentDialogs.LayerEffectRenameView"
x:DataType="contentDialogs:LayerEffectRenameViewModel">
<StackPanel>
<TextBox Name="NameTextBox" Text="{CompiledBinding LayerEffectName}" Watermark="Layer effect name" />
</StackPanel>
</UserControl>
@@ -1,29 +0,0 @@
using System.Threading.Tasks;
using Artemis.UI.Shared.Extensions;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using Avalonia.Threading;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Tree.ContentDialogs;
public partial class LayerEffectRenameView : ReactiveUserControl<LayerEffectRenameViewModel>
{
public LayerEffectRenameView()
{
InitializeComponent();
this.WhenActivated(_ =>
{
this.ClearAllDataValidationErrors();
Dispatcher.UIThread.Post(DelayedAutoFocus);
});
}
private async void DelayedAutoFocus()
{
// Don't ask
await Task.Delay(200);
NameTextBox.SelectAll();
NameTextBox.Focus();
}
}
@@ -1,39 +0,0 @@
using System.Reactive;
using Artemis.Core.LayerEffects;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using FluentAvalonia.UI.Controls;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using ReactiveUI.Validation.Extensions;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Tree.ContentDialogs;
public partial class LayerEffectRenameViewModel : ContentDialogViewModelBase
{
private readonly BaseLayerEffect _layerEffect;
private readonly IProfileEditorService _profileEditorService;
[Notify] private string? _layerEffectName;
public LayerEffectRenameViewModel(IProfileEditorService profileEditorService, BaseLayerEffect layerEffect)
{
_profileEditorService = profileEditorService;
_layerEffect = layerEffect;
_layerEffectName = layerEffect.Name;
Confirm = ReactiveCommand.Create(ExecuteConfirm, ValidationContext.Valid);
this.ValidationRule(vm => vm.LayerEffectName, categoryName => !string.IsNullOrWhiteSpace(categoryName), "You must specify a valid name");
}
public ReactiveCommand<Unit, Unit> Confirm { get; }
private void ExecuteConfirm()
{
if (LayerEffectName == null)
return;
_profileEditorService.ExecuteCommand(new RenameLayerEffect(_layerEffect, LayerEffectName));
ContentDialog?.Hide(ContentDialogResult.Primary);
}
}
@@ -1,64 +0,0 @@
<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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
xmlns:dialogs="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Tree.Dialogs"
xmlns:layerBrushes="clr-namespace:Artemis.Core.LayerBrushes;assembly=Artemis.Core"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Tree.Dialogs.LayerBrushPresetView"
x:DataType="dialogs:LayerBrushPresetViewModel"
Width="500">
<Grid RowDefinitions="Auto,*">
<TextBox Name="SearchBox" Text="{CompiledBinding SearchText}" Margin="0 0 0 15" Watermark="Search" />
<ListBox Name="EffectDescriptorsList"
Grid.Row="1"
ItemsSource="{CompiledBinding Presets}"
IsVisible="{CompiledBinding Presets.Count}"
Height="300">
<ListBox.DataTemplates>
<DataTemplate DataType="{x:Type layerBrushes:ILayerBrushPreset}">
<Grid RowDefinitions="Auto,*"
ColumnDefinitions="Auto,Auto"
Background="Transparent"
PointerReleased="InputElement_OnPointerReleased"
Margin="0 4"
VerticalAlignment="Center">
<shared:ArtemisIcon Grid.Column="0"
Grid.RowSpan="2"
Icon="{CompiledBinding Icon}"
Width="24"
Height="24"
VerticalAlignment="Center"
Margin="0 0 15 0" />
<TextBlock Grid.Column="1"
Grid.Row="0"
Classes="BodyStrongTextBlockStyle"
Text="{CompiledBinding Name}"
VerticalAlignment="Bottom"
Width="450"
TextWrapping="Wrap" />
<TextBlock Grid.Column="1"
Grid.Row="1"
Foreground="{DynamicResource TextFillColorSecondary}"
Text="{CompiledBinding Description}"
VerticalAlignment="Top"
Width="450"
TextWrapping="Wrap" />
</Grid>
</DataTemplate>
</ListBox.DataTemplates>
</ListBox>
<Grid Grid.Row="1" Height="300">
<StackPanel VerticalAlignment="Center"
Spacing="20"
IsVisible="{CompiledBinding !Presets.Count}">
<avalonia:MaterialIcon Kind="CloseCircle" Width="32" Height="32" />
<TextBlock Classes="h5" TextAlignment="Center">None of the presets match your search</TextBlock>
</StackPanel>
</Grid>
</Grid>
</UserControl>
@@ -1,24 +0,0 @@
using Artemis.Core.LayerBrushes;
using Avalonia;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Tree.Dialogs;
public partial class LayerBrushPresetView : ReactiveUserControl<LayerBrushPresetViewModel>
{
public LayerBrushPresetView()
{
InitializeComponent();
}
private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (sender is not IDataContextProvider {DataContext: ILayerBrushPreset preset} || ViewModel == null)
return;
ViewModel?.SelectPreset(preset);
}
}
Loaded 100 of 191 files, more files were not shown because too many files have changed in this diff. Show more