mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-11-11 11:26:27 +00:00
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:
1 parent
565647f1ed
commit
3c8d8b2387
191 files changed
+449
-416
No files matched your search
-18
@@ -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; }
|
||||
}
|
||||
-16
@@ -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>
|
||||
-13
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
-14
@@ -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;
|
||||
}
|
||||
}
|
||||
-70
@@ -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>
|
||||
-13
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
-84
@@ -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();
|
||||
}
|
||||
}
|
||||
-15
@@ -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>
|
||||
-13
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
-14
@@ -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;
|
||||
}
|
||||
}
|
||||
-45
@@ -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>
|
||||
-13
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
-57
@@ -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();
|
||||
}
|
||||
}
|
||||
-49
@@ -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>
|
||||
-13
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
-91
@@ -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&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&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&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&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&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&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&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&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;
|
||||
}
|
||||
}
|
||||
}
|
||||
-184
@@ -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
|
||||
}
|
||||
}
|
||||
-12
@@ -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>
|
||||
-31
@@ -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();
|
||||
}
|
||||
}
|
||||
-39
@@ -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);
|
||||
}
|
||||
}
|
||||
-25
@@ -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);
|
||||
}
|
||||
}
|
||||
-52
@@ -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>
|
||||
-13
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
-13
@@ -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; }
|
||||
}
|
||||
-52
@@ -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>
|
||||
-13
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
-13
@@ -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; }
|
||||
}
|
||||
-31
@@ -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>
|
||||
-13
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
-13
@@ -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; }
|
||||
}
|
||||
-57
@@ -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>
|
||||
-13
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
-42
@@ -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; }
|
||||
}
|
||||
-101
@@ -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&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>
|
||||
-17
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
-92
@@ -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;
|
||||
}
|
||||
}
|
||||
-52
@@ -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&mtm_kwd=profile-editor"
|
||||
Margin="0 10">
|
||||
Learn more
|
||||
</HyperlinkButton>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
-13
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
-110
@@ -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);
|
||||
}
|
||||
}
|
||||
-18
@@ -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>
|
||||
-13
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
-14
@@ -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; }
|
||||
}
|
||||
-15
@@ -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);
|
||||
}
|
||||
-34
@@ -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
|
||||
}
|
||||
-23
@@ -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>
|
||||
-13
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
-37
@@ -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;
|
||||
}
|
||||
-72
@@ -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>
|
||||
-83
@@ -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();
|
||||
}
|
||||
}
|
||||
-249
@@ -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
|
||||
}
|
||||
-72
@@ -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>
|
||||
-41
@@ -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));
|
||||
}
|
||||
}
|
||||
-69
@@ -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;
|
||||
}
|
||||
-86
@@ -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>
|
||||
-53
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
-68
@@ -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;
|
||||
}
|
||||
-27
@@ -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>
|
||||
-69
@@ -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>
|
||||
-41
@@ -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));
|
||||
}
|
||||
}
|
||||
-62
@@ -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;
|
||||
}
|
||||
-239
@@ -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);
|
||||
}
|
||||
}
|
||||
-57
@@ -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>
|
||||
-13
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
-43
@@ -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;
|
||||
}
|
||||
-22
@@ -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>
|
||||
-13
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
-104
@@ -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
|
||||
}
|
||||
-12
@@ -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>
|
||||
-29
@@ -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();
|
||||
}
|
||||
}
|
||||
-39
@@ -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);
|
||||
}
|
||||
}
|
||||
-64
@@ -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>
|
||||
-24
@@ -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
Reference in new issue
Block a user