Meta - Replace WPF projects with Avalonia projects

This commit is contained in:
Robert committed 2022-03-27 11:55:26 +02:00
1 parent fcf0376a9a
commit d7f3ee1190
1110 files changed
+10872 -52240

No files matched your search

@@ -1,42 +0,0 @@
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.Dialogs.LayerBrushPresetView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:layerBrushes="clr-namespace:Artemis.Core.LayerBrushes;assembly=Artemis.Core"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<StackPanel Margin="16">
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}">
Select a brush preset
</TextBlock>
<ListBox ItemsSource="{Binding Presets}" SelectedItem="{Binding SelectedPreset}" HorizontalContentAlignment="Stretch" Margin="0 8 0 16">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type layerBrushes:ILayerBrushPreset}">
<Border x:Name="Border" Padding="8" BorderThickness="0 0 0 1" BorderBrush="{DynamicResource MaterialDesignDivider}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<shared:ArtemisIcon Icon="{Binding Icon}" VerticalAlignment="Center" Width="30" Height="30"/>
<StackPanel Margin="8 0 0 0" Grid.Column="1">
<TextBlock FontWeight="Bold" Text="{Binding Name}" />
<TextBlock Text="{Binding Description}" />
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Style="{StaticResource MaterialDesignFlatButton}" IsDefault="True" Margin="0 8 8 0" Command="{s:Action Cancel}">
USE DEFAULT SETTINGS
</Button>
</StackPanel>
</StackPanel>
</UserControl>
@@ -1,43 +0,0 @@
using System.Threading.Tasks;
using Artemis.Core.LayerBrushes;
using Artemis.UI.Shared.Services;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.Dialogs
{
public class LayerBrushPresetViewModel : DialogViewModelBase
{
private readonly BaseLayerBrush _layerBrush;
private ILayerBrushPreset _selectedPreset;
public LayerBrushPresetViewModel(BaseLayerBrush layerBrush)
{
_layerBrush = layerBrush;
Presets = new BindableCollection<ILayerBrushPreset>();
Presets.AddRange(layerBrush.Presets);
}
public BindableCollection<ILayerBrushPreset> Presets { get; }
public ILayerBrushPreset SelectedPreset
{
get => _selectedPreset;
set
{
SetAndNotify(ref _selectedPreset, value);
SelectPreset(value);
}
}
public void SelectPreset(ILayerBrushPreset preset)
{
_layerBrush.BaseProperties?.ResetAllLayerProperties();
preset.Apply();
Execute.OnUIThreadAsync(async () =>
{
await Task.Delay(250);
Session?.Close(true);
});
}
}
}
@@ -1,32 +0,0 @@
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.Dialogs.RenameView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="213.053" d:DesignWidth="254.425">
<StackPanel Margin="16">
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}">
<Run Text="Rename" />
<Run Text="{Binding Subject, Mode=OneWay}" />
</TextBlock>
<TextBox materialDesign:HintAssist.Hint="Element name"
Margin="0 8 0 16"
Width="300"
Style="{StaticResource MaterialDesignFilledTextBox}"
Text="{Binding ElementName, UpdateSourceTrigger=PropertyChanged}"
Loaded="FrameworkElement_OnLoaded" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0 8 0 0">
<Button Style="{StaticResource MaterialDesignFlatButton}" IsCancel="True" Margin="0 0 8 0" Command="{s:Action Cancel}">
CANCEL
</Button>
<Button Style="{StaticResource MaterialDesignFlatButton}" IsDefault="True" Margin="0 0 0 0" Command="{s:Action Accept}">
ACCEPT
</Button>
</StackPanel>
</StackPanel>
</UserControl>
@@ -1,24 +0,0 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace Artemis.UI.Screens.ProfileEditor.Dialogs
{
/// <summary>
/// Interaction logic for RenameView.xaml
/// </summary>
public partial class RenameView : UserControl
{
public RenameView()
{
InitializeComponent();
}
private void FrameworkElement_OnLoaded(object sender, RoutedEventArgs e)
{
TextBox textBox = (TextBox) sender;
Keyboard.Focus(textBox);
textBox.SelectAll();
}
}
}
@@ -1,44 +0,0 @@
using System.Threading.Tasks;
using Artemis.UI.Shared.Services;
using FluentValidation;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.Dialogs
{
public class RenameViewModel : DialogViewModelBase
{
private string _elementName;
public RenameViewModel(IModelValidator<RenameViewModel> validator, string subject, string currentName) : base(validator)
{
Subject = subject;
ElementName = currentName;
}
public string Subject { get; }
public string ElementName
{
get => _elementName;
set => SetAndNotify(ref _elementName, value);
}
public async Task Accept()
{
await ValidateAsync();
if (HasErrors)
return;
Session.Close(ElementName);
}
}
public class ProfileElementRenameViewModelValidator : AbstractValidator<RenameViewModel>
{
public ProfileElementRenameViewModelValidator()
{
RuleFor(m => m.ElementName).NotEmpty().WithMessage("Element name may not be empty");
}
}
}
@@ -1,118 +0,0 @@
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:displayConditions="clr-namespace:Artemis.UI.Screens.ProfileEditor.DisplayConditions"
xmlns:core="clr-namespace:Artemis.Core;assembly=Artemis.Core"
xmlns:controls="clr-namespace:Artemis.VisualScripting.Editor.Controls;assembly=Artemis.VisualScripting"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
x:Class="Artemis.UI.Screens.ProfileEditor.DisplayConditions.DisplayConditionsView"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance {x:Type displayConditions:DisplayConditionsViewModel}}">
<UserControl.Resources>
<converters:ComparisonConverter x:Key="ComparisonConverter" />
<shared:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
</UserControl.Resources>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<DockPanel Grid.Row="0" Margin="0 -2">
<TextBlock Style="{StaticResource MaterialDesignSubtitle1TextBlock}" VerticalAlignment="Center">
Display conditions
</TextBlock>
<materialDesign:ColorZone Mode="Standard" CornerRadius="3" Margin="0 -2" HorizontalAlignment="Right">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<RadioButton Grid.Column="0"
Style="{StaticResource MaterialDesignTabRadioButton}"
IsChecked="{Binding Path=DisplayConditionType, Converter={StaticResource ComparisonConverter}, ConverterParameter={x:Static displayConditions:DisplayConditionType.None}}"
MinWidth="0"
Padding="10 0">
<RadioButton.ToolTip>
<ToolTip Placement="Center" VerticalOffset="-40">
<TextBlock>
No condition, the element is always displayed
</TextBlock>
</ToolTip>
</RadioButton.ToolTip>
<TextBlock VerticalAlignment="Center" FontSize="12">
<materialDesign:PackIcon Kind="No" VerticalAlignment="Center" Margin="0 0 2 -3" />
NONE
</TextBlock>
</RadioButton>
<RadioButton Grid.Column="1"
Style="{StaticResource MaterialDesignTabRadioButton}"
IsChecked="{Binding Path=DisplayConditionType, Converter={StaticResource ComparisonConverter}, ConverterParameter={x:Static displayConditions:DisplayConditionType.Static}}"
MinWidth="0"
Padding="10 0">
<RadioButton.ToolTip>
<ToolTip Placement="Center" VerticalOffset="-40">
<TextBlock>
A condition that constantly checks and is either 'on' or 'off'
</TextBlock>
</ToolTip>
</RadioButton.ToolTip>
<TextBlock VerticalAlignment="Center" FontSize="12">
<materialDesign:PackIcon Kind="Check" VerticalAlignment="Center" Margin="0 0 2 -3" />
CONSTANT
</TextBlock>
</RadioButton>
<RadioButton Grid.Column="2"
Style="{StaticResource MaterialDesignTabRadioButton}"
IsChecked="{Binding Path=DisplayConditionType, Converter={StaticResource ComparisonConverter}, ConverterParameter={x:Static displayConditions:DisplayConditionType.Events}}"
MinWidth="0"
Padding="10 0">
<RadioButton.ToolTip>
<ToolTip Placement="Center" VerticalOffset="-40">
<TextBlock>
A condition triggers on a selectable event
</TextBlock>
</ToolTip>
</RadioButton.ToolTip>
<TextBlock VerticalAlignment="Center" FontSize="12">
<materialDesign:PackIcon Kind="LightningBolt" VerticalAlignment="Center" Margin="0 0 2 -3" />
EVENT
</TextBlock>
</RadioButton>
</Grid>
</materialDesign:ColorZone>
</DockPanel>
<Separator Grid.Row="1" Grid.Column="0" Style="{StaticResource MaterialDesignDarkSeparator}" Margin="-2 0" />
<ContentControl Grid.Row="2"
s:View.Model="{Binding ActiveItem}"
VerticalContentAlignment="Stretch"
HorizontalContentAlignment="Stretch"
IsTabStop="False"
Visibility="{Binding ActiveItem, Converter={StaticResource NullToVisibilityConverter}}"/>
<StackPanel Grid.Row="2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Visibility="{Binding ActiveItem, ConverterParameter=Inverted, Converter={StaticResource NullToVisibilityConverter}}">
<TextBlock Style="{StaticResource MaterialDesignHeadline5TextBlock}" TextWrapping="Wrap" TextAlignment="Center" Margin="0 10">
Display conditions disabled
<materialDesign:PackIcon Kind="No" Margin="0 0 0 -6" Width="30" Height="30" VerticalAlignment="Center" />
</TextBlock>
<TextBlock Style="{StaticResource MaterialDesignSubtitle1TextBlock}" TextWrapping="Wrap" TextAlignment="Center">
This element will always display.
</TextBlock>
</StackPanel>
</Grid>
</UserControl>
@@ -1,114 +0,0 @@
using Artemis.Core;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions
{
public class DisplayConditionsViewModel : Conductor<Screen>, IProfileEditorPanelViewModel
{
private readonly IConditionVmFactory _conditionVmFactory;
private readonly IProfileEditorService _profileEditorService;
private DisplayConditionType _displayConditionType;
private StaticCondition _staticCondition;
private EventCondition _eventCondition;
public DisplayConditionsViewModel(IProfileEditorService profileEditorService, IConditionVmFactory conditionVmFactory)
{
_profileEditorService = profileEditorService;
_conditionVmFactory = conditionVmFactory;
}
public DisplayConditionType DisplayConditionType
{
get => _displayConditionType;
set
{
if (!SetAndNotify(ref _displayConditionType, value)) return;
ChangeConditionType();
}
}
protected override void OnInitialActivate()
{
_profileEditorService.SelectedProfileElementChanged += ProfileEditorServiceOnSelectedProfileElementChanged;
Update(_profileEditorService.SelectedProfileElement);
base.OnInitialActivate();
}
protected override void OnClose()
{
_profileEditorService.SelectedProfileElementChanged -= ProfileEditorServiceOnSelectedProfileElementChanged;
base.OnClose();
}
private void ChangeConditionType()
{
if (_profileEditorService.SelectedProfileElement == null)
return;
// Keep the old condition around in case the user changes their mind
if (_profileEditorService.SelectedProfileElement.DisplayCondition is StaticCondition staticCondition)
_staticCondition = staticCondition;
else if (_profileEditorService.SelectedProfileElement.DisplayCondition is EventCondition eventCondition)
_eventCondition = eventCondition;
// If we have the old condition around put it back
if (DisplayConditionType == DisplayConditionType.Static)
_profileEditorService.SelectedProfileElement.DisplayCondition = _staticCondition ?? new StaticCondition(_profileEditorService.SelectedProfileElement);
else if (DisplayConditionType == DisplayConditionType.Events)
_profileEditorService.SelectedProfileElement.DisplayCondition = _eventCondition ?? new EventCondition(_profileEditorService.SelectedProfileElement);
else
_profileEditorService.SelectedProfileElement.DisplayCondition = null;
_profileEditorService.SaveSelectedProfileElement();
Update(_profileEditorService.SelectedProfileElement);
}
private void Update(RenderProfileElement renderProfileElement)
{
if (renderProfileElement == null)
{
ActiveItem = null;
return;
}
if (renderProfileElement.DisplayCondition is StaticCondition staticCondition)
{
ActiveItem = _conditionVmFactory.StaticConditionViewModel(staticCondition);
_displayConditionType = DisplayConditionType.Static;
}
else if (renderProfileElement.DisplayCondition is EventCondition eventsCondition)
{
ActiveItem = _conditionVmFactory.EventConditionViewModel(eventsCondition);
_displayConditionType = DisplayConditionType.Events;
}
else
{
ActiveItem = null;
_displayConditionType = DisplayConditionType.None;
}
NotifyOfPropertyChange(nameof(DisplayConditionType));
}
private void ProfileEditorServiceOnSelectedProfileElementChanged(object sender, RenderProfileElementEventArgs e)
{
if (_staticCondition != null && e.PreviousRenderProfileElement?.DisplayCondition != _staticCondition)
_staticCondition.Dispose();
if (_eventCondition != null && e.PreviousRenderProfileElement?.DisplayCondition != _eventCondition)
_eventCondition.Dispose();
Update(e.RenderProfileElement);
}
}
public enum DisplayConditionType
{
None,
Static,
Events
}
}
@@ -1,204 +0,0 @@
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.DisplayConditions.Event"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:controls="clr-namespace:Artemis.VisualScripting.Editor.Controls;assembly=Artemis.VisualScripting"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:core="clr-namespace:Artemis.Core;assembly=Artemis.Core"
xmlns:shared="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared"
xmlns:Shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" x:Class="Artemis.UI.Screens.ProfileEditor.DisplayConditions.Event.EventConditionView"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance {x:Type local:EventConditionViewModel}}">
<UserControl.Resources>
<Shared:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
<converters:ComparisonConverter x:Key="ComparisonConverter" />
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="22" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" VerticalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<shared:DataModelPicker Grid.Row="0"
Grid.Column="0"
Margin="0 0 0 5"
ButtonBrush="DarkGoldenrod"
DataModelPath="{Binding EventCondition.EventPath}"
DataModelPathSelected="{s:Action DataModelPathSelected}"
FilterTypes="{Binding FilterTypes}"
ShowFullPath="True"
Modules="{Binding Modules}" />
<CheckBox Grid.Row="0"
Grid.Column="1"
IsChecked="{Binding TriggerConditionally}"
Content="Conditional trigger"
VerticalAlignment="Center"
ToolTip="When enabled, the element will only trigger if the script evaluates to true"
Style="{StaticResource MaterialDesignDarkCheckBox}" />
<Grid Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
MouseUp="{s:Action ScriptGridMouseUp}"
Cursor="Hand"
Visibility="{Binding EventCondition.Script, Converter={StaticResource NullToVisibilityConverter}, ConverterParameter=Normal, Mode=OneWay}">
<controls:VisualScriptPresenter Script="{Binding EventCondition.Script}" AutoFitScript="True" />
<Border VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Opacity="0">
<Border.Background>
<SolidColorBrush Color="{Binding Color, Source={StaticResource MaterialDesignCardBackground}}" Opacity="0.75" />
</Border.Background>
<Border.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding IsMouseOver, RelativeSource={RelativeSource AncestorType={x:Type Grid}, Mode=FindAncestor}}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="1" Duration="0:0:0.25" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:0.25" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock Style="{StaticResource MaterialDesignHeadline5TextBlock}" VerticalAlignment="Center" TextAlignment="Center">
Click to edit script
<materialDesign:PackIcon Kind="OpenInNew" Margin="0 0 0 -6 " Width="30" Height="30" VerticalAlignment="Center" />
</TextBlock>
</Border>
</Grid>
<Grid Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
Visibility="{Binding EventCondition.Script, Converter={StaticResource NullToVisibilityConverter}, ConverterParameter=Inverted, Mode=OneWay}">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Style="{StaticResource MaterialDesignHeadline5TextBlock}" TextWrapping="Wrap" TextAlignment="Center" Margin="0 10">
Conditional trigger disabled
<materialDesign:PackIcon Kind="No" Margin="0 0 0 -6" Width="30" Height="30" VerticalAlignment="Center" />
</TextBlock>
<TextBlock Style="{StaticResource MaterialDesignSubtitle1TextBlock}" TextWrapping="Wrap" TextAlignment="Center">
This element will trigger any time the event fires.
</TextBlock>
</StackPanel>
</Grid>
</Grid>
<!-- Trigger mode -->
<TextBlock Grid.Row="1" Grid.Column="0" Text="Trigger mode" VerticalAlignment="Center">
<TextBlock.ToolTip>
<ToolTip Placement="Center" VerticalOffset="-30">
<TextBlock>
<Run Text="Configure how the layer should act when the event(s) trigger" />
</TextBlock>
</ToolTip>
</TextBlock.ToolTip>
</TextBlock>
<materialDesign:ColorZone Grid.Row="2" Grid.Column="0" Mode="Standard" CornerRadius="3">
<Grid HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<RadioButton Grid.Column="0"
Style="{StaticResource MaterialDesignTabRadioButton}"
IsChecked="{Binding EventOverlapMode, ConverterParameter={x:Static core:TimeLineEventOverlapMode.Restart}, Converter={StaticResource ComparisonConverter}}">
<RadioButton.ToolTip>
<ToolTip Placement="Center" VerticalOffset="-40">
<TextBlock>
<Run Text="Stop the current run and restart the timeline" />
</TextBlock>
</ToolTip>
</RadioButton.ToolTip>
<TextBlock VerticalAlignment="Center" FontSize="12">
<InlineUIContainer>
<materialDesign:PackIcon Kind="Repeat" VerticalAlignment="Center" Margin="-3 0 0 -3" />
</InlineUIContainer>
<Run Text=" RESTART" />
</TextBlock>
</RadioButton>
<RadioButton Grid.Column="1"
Style="{StaticResource MaterialDesignTabRadioButton}"
IsChecked="{Binding EventOverlapMode, ConverterParameter={x:Static core:TimeLineEventOverlapMode.Toggle}, Converter={StaticResource ComparisonConverter}}">
<RadioButton.ToolTip>
<ToolTip Placement="Center" VerticalOffset="-40">
<TextBlock>
<Run Text="Repeat the timeline until the event fires again" />
</TextBlock>
</ToolTip>
</RadioButton.ToolTip>
<TextBlock VerticalAlignment="Center" FontSize="12">
<InlineUIContainer>
<materialDesign:PackIcon Kind="TrafficLight" VerticalAlignment="Center" Margin="-3 0 0 -3" />
</InlineUIContainer>
<Run Text=" TOGGLE" />
</TextBlock>
</RadioButton>
<RadioButton Grid.Column="2"
Style="{StaticResource MaterialDesignTabRadioButton}"
IsChecked="{Binding EventOverlapMode, ConverterParameter={x:Static core:TimeLineEventOverlapMode.Ignore}, Converter={StaticResource ComparisonConverter}}">
<RadioButton.ToolTip>
<ToolTip Placement="Center" VerticalOffset="-40">
<TextBlock>
<Run Text="Ignore subsequent event fires until the timeline finishes" />
</TextBlock>
</ToolTip>
</RadioButton.ToolTip>
<TextBlock VerticalAlignment="Center" FontSize="12">
<InlineUIContainer>
<materialDesign:PackIcon Kind="EarHearingOff" VerticalAlignment="Center" Margin="-3 0 0 -3" />
</InlineUIContainer>
<Run Text=" IGNORE" />
</TextBlock>
</RadioButton>
<RadioButton Grid.Column="3"
Style="{StaticResource MaterialDesignTabRadioButton}"
IsChecked="{Binding EventOverlapMode, ConverterParameter={x:Static core:TimeLineEventOverlapMode.Copy}, Converter={StaticResource ComparisonConverter}}">
<RadioButton.ToolTip>
<ToolTip Placement="Center" VerticalOffset="-40">
<TextBlock>
<Run Text="Play another copy of the timeline on top of the current run" />
</TextBlock>
</ToolTip>
</RadioButton.ToolTip>
<TextBlock VerticalAlignment="Center" FontSize="12">
<InlineUIContainer>
<materialDesign:PackIcon Kind="ContentCopy" VerticalAlignment="Center" Margin="-3 0 0 -3" />
</InlineUIContainer>
<Run Text=" COPY" />
</TextBlock>
</RadioButton>
</Grid>
</materialDesign:ColorZone>
</Grid>
</UserControl>
@@ -1,99 +0,0 @@
using System;
using System.Linq;
using System.Windows.Input;
using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions.Event
{
public class EventConditionViewModel : Screen
{
private readonly INodeVmFactory _nodeVmFactory;
private readonly IProfileEditorService _profileEditorService;
private readonly IWindowManager _windowManager;
private NodeScript<bool> _oldScript;
public EventConditionViewModel(EventCondition eventCondition, IProfileEditorService profileEditorService, IWindowManager windowManager, INodeVmFactory nodeVmFactory)
{
_profileEditorService = profileEditorService;
_windowManager = windowManager;
_nodeVmFactory = nodeVmFactory;
EventCondition = eventCondition;
FilterTypes = new BindableCollection<Type> {typeof(IDataModelEvent)};
if (_profileEditorService.SelectedProfileConfiguration?.Module != null)
Modules = new BindableCollection<Module> {_profileEditorService.SelectedProfileConfiguration.Module};
}
public EventCondition EventCondition { get; }
public BindableCollection<Type> FilterTypes { get; }
public BindableCollection<Module> Modules { get; }
public TimeLineEventOverlapMode EventOverlapMode
{
get => EventCondition.EventOverlapMode;
set
{
if (EventCondition.EventOverlapMode == value) return;
EventCondition.EventOverlapMode = value;
_profileEditorService.SaveSelectedProfileElement();
}
}
public bool TriggerConditionally
{
get => EventCondition.Script != null;
set
{
if (EventCondition.Script != null)
{
_oldScript = EventCondition.Script;
EventCondition.Script = null;
}
else
{
if (_oldScript != null)
{
EventCondition.Script = _oldScript;
_oldScript = null;
}
else
{
EventCondition.CreateEmptyNodeScript();
}
}
}
}
#region Overrides of Screen
/// <inheritdoc />
protected override void OnClose()
{
_oldScript?.Dispose();
base.OnClose();
}
#endregion
public void DataModelPathSelected(object sender, DataModelSelectedEventArgs e)
{
EventCondition.UpdateEventNode();
DisplayName = EventCondition.EventPath?.Segments.LastOrDefault()?.GetPropertyDescription()?.Name ?? "Invalid event";
_profileEditorService.SaveSelectedProfileElement();
}
public void ScriptGridMouseUp(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton != MouseButton.Left)
return;
_windowManager.ShowDialog(_nodeVmFactory.NodeScriptWindowViewModel(EventCondition.Script));
_profileEditorService.SaveSelectedProfileElement();
}
}
}
@@ -1,175 +0,0 @@
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.DisplayConditions.Static.StaticConditionView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.DisplayConditions.Static"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:controls="clr-namespace:Artemis.VisualScripting.Editor.Controls;assembly=Artemis.VisualScripting"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance {x:Type local:StaticConditionViewModel}}">
<UserControl.Resources>
<converters:InverseBooleanConverter x:Key="InverseBooleanConverter" />
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" MouseUp="{s:Action ScriptGridMouseUp}" Cursor="Hand">
<controls:VisualScriptPresenter Script="{Binding StaticCondition.Script}" AutoFitScript="True"/>
<Border Opacity="0">
<Border.Background>
<SolidColorBrush Color="{Binding Color, Source={StaticResource MaterialDesignCardBackground}}" Opacity="0.75" />
</Border.Background>
<Border.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding IsMouseOver, RelativeSource={RelativeSource AncestorType={x:Type Grid}, Mode=FindAncestor}}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="1" Duration="0:0:0.25" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:0.25" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock Style="{StaticResource MaterialDesignHeadline5TextBlock}" VerticalAlignment="Center" TextAlignment="Center">
Click to edit script
<materialDesign:PackIcon Kind="OpenInNew" Margin="0 0 0 -6 " Width="30" Height="30" VerticalAlignment="Center" />
</TextBlock>
</Border>
</Grid>
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="22" />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="140" />
<ColumnDefinition Width="*" MinWidth="170" />
</Grid.ColumnDefinitions>
<!-- Play mode -->
<TextBlock Grid.Column="0" Text="Play mode" VerticalAlignment="Center">
<TextBlock.ToolTip>
<ToolTip Placement="Center" VerticalOffset="-30">
<TextBlock>
Configure how the layer should act while the conditions above are met
</TextBlock>
</ToolTip>
</TextBlock.ToolTip>
</TextBlock>
<materialDesign:ColorZone Grid.Row="1" Grid.Column="0" Mode="Standard" CornerRadius="3" Margin="0 0 2 0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<RadioButton Grid.Column="0"
Style="{StaticResource MaterialDesignTabRadioButton}"
IsChecked="{Binding DisplayContinuously}"
MinWidth="0"
Padding="5 0">
<RadioButton.ToolTip>
<ToolTip Placement="Center" VerticalOffset="-40">
<TextBlock>
Continue repeating the main segment of the timeline while the condition is met
</TextBlock>
</ToolTip>
</RadioButton.ToolTip>
<TextBlock VerticalAlignment="Center" FontSize="12">
<materialDesign:PackIcon Kind="Repeat" VerticalAlignment="Center" Margin="-3 0 0 -3" />
REPEAT
</TextBlock>
</RadioButton>
<RadioButton Grid.Column="1"
Style="{StaticResource MaterialDesignTabRadioButton}"
IsChecked="{Binding DisplayContinuously, Converter={StaticResource InverseBooleanConverter}}"
MinWidth="0"
Padding="5 0">
<RadioButton.ToolTip>
<ToolTip Placement="Center" VerticalOffset="-40">
<TextBlock>
Only play the timeline once when the condition is met
</TextBlock>
</ToolTip>
</RadioButton.ToolTip>
<TextBlock VerticalAlignment="Center" FontSize="12">
<materialDesign:PackIcon Kind="StopwatchOutline" VerticalAlignment="Center" Margin="-3 0 0 -3" />
ONCE
</TextBlock>
</RadioButton>
</Grid>
</materialDesign:ColorZone>
<!-- Stop mode -->
<TextBlock Grid.Row="0" Grid.Column="1" Text="Stop mode">
<TextBlock.ToolTip>
<ToolTip Placement="Center" VerticalOffset="-30">
<TextBlock>
Configure how the layer should act when the conditions above are no longer met
</TextBlock>
</ToolTip>
</TextBlock.ToolTip>
</TextBlock>
<materialDesign:ColorZone Grid.Row="1" Grid.Column="1" Mode="Standard" CornerRadius="3" Margin="2 0 0 0" HorizontalAlignment="Stretch">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition MinWidth="100" />
</Grid.ColumnDefinitions>
<RadioButton Grid.Column="0"
Style="{StaticResource MaterialDesignTabRadioButton}"
IsChecked="{Binding AlwaysFinishTimeline}"
MinWidth="0"
Padding="5 0">
<RadioButton.ToolTip>
<ToolTip Placement="Center" VerticalOffset="-40">
<TextBlock>
When conditions are no longer met, finish the the current run of the main timeline
</TextBlock>
</ToolTip>
</RadioButton.ToolTip>
<TextBlock VerticalAlignment="Center" FontSize="12">
<materialDesign:PackIcon Kind="PlayOutline" VerticalAlignment="Center" Margin="-3 0 0 -3" />
FINISH
</TextBlock>
</RadioButton>
<RadioButton Grid.Column="1"
Style="{StaticResource MaterialDesignTabRadioButton}"
IsChecked="{Binding AlwaysFinishTimeline, Converter={StaticResource InverseBooleanConverter}}"
MinWidth="0"
Padding="5 0">
<RadioButton.ToolTip>
<ToolTip Placement="Center" VerticalOffset="-40">
<TextBlock>
When conditions are no longer met, skip to the end segment of the timeline
</TextBlock>
</ToolTip>
</RadioButton.ToolTip>
<TextBlock VerticalAlignment="Center" FontSize="12">
<materialDesign:PackIcon Kind="SkipNextOutline" VerticalAlignment="Center" Margin="-3 0 0 -3" />
SKIP TO END
</TextBlock>
</RadioButton>
</Grid>
</materialDesign:ColorZone>
</Grid>
</Grid>
</UserControl>
@@ -1,60 +0,0 @@
using System.Windows.Input;
using Artemis.Core;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Shared.Services;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions.Static
{
public class StaticConditionViewModel : Screen
{
private readonly IWindowManager _windowManager;
private readonly INodeVmFactory _nodeVmFactory;
private readonly IProfileEditorService _profileEditorService;
private readonly RenderProfileElement _renderProfileElement;
public StaticConditionViewModel(StaticCondition staticCondition, IWindowManager windowManager, INodeVmFactory nodeVmFactory, IProfileEditorService profileEditorService)
{
_windowManager = windowManager;
_nodeVmFactory = nodeVmFactory;
_profileEditorService = profileEditorService;
_renderProfileElement = (RenderProfileElement) staticCondition.ProfileElement;
StaticCondition = staticCondition;
}
public StaticCondition StaticCondition { get; }
public bool DisplayContinuously
{
get => _renderProfileElement.Timeline.PlayMode == TimelinePlayMode.Repeat;
set
{
TimelinePlayMode playMode = value ? TimelinePlayMode.Repeat : TimelinePlayMode.Once;
if (_renderProfileElement.Timeline.PlayMode == playMode) return;
_renderProfileElement.Timeline.PlayMode = playMode;
_profileEditorService.SaveSelectedProfileElement();
}
}
public bool AlwaysFinishTimeline
{
get => _renderProfileElement?.Timeline.StopMode == TimelineStopMode.Finish;
set
{
TimelineStopMode stopMode = value ? TimelineStopMode.Finish : TimelineStopMode.SkipToEnd;
if (_renderProfileElement.Timeline.StopMode == stopMode) return;
_renderProfileElement.Timeline.StopMode = stopMode;
_profileEditorService.SaveSelectedProfileElement();
}
}
public void ScriptGridMouseUp(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton != MouseButton.Left)
return;
_windowManager.ShowDialog(_nodeVmFactory.NodeScriptWindowViewModel(StaticCondition.Script));
_profileEditorService.SaveSelectedProfileElement();
}
}
}
@@ -1,8 +0,0 @@
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor
{
public interface IProfileEditorPanelViewModel : IScreen
{
}
}
@@ -1,54 +0,0 @@
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.DataBindingsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:controls="clr-namespace:Artemis.VisualScripting.Editor.Controls;assembly=Artemis.VisualScripting"
xmlns:layerProperties="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance {x:Type layerProperties:DataBindingsViewModel}}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="48" />
<RowDefinition />
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}"
VerticalAlignment="Center"
Margin="15 0"
Text="{Binding DataBinding.BaseLayerProperty.PropertyDescription.Name}" />
<CheckBox Grid.Column="1"
VerticalAlignment="Center"
IsChecked="{Binding DataBindingEnabled}">
Enable data binding
</CheckBox>
</Grid>
<controls:VisualScriptPresenter Grid.Row="1"
AvailableNodes="{Binding AvailableNodes}"
Script="{Binding DataBinding.Script}"
AlwaysShowValues="{Binding AlwaysShowValues.Value}"/>
<Border Grid.Row="1" Background="{StaticResource MaterialDesignToolBarBackground}"
Visibility="{Binding DataBindingEnabled, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Margin="16">
<materialDesign:PackIcon Kind="TransitConnectionVariant" Width="100" Height="100" HorizontalAlignment="Center" />
<TextBlock Style="{StaticResource MaterialDesignHeadline4TextBlock}" TextWrapping="Wrap" HorizontalAlignment="Center" Margin="0 25">
Enable this data binding to use nodes!
</TextBlock>
<TextBlock Style="{StaticResource MaterialDesignBody1TextBlock}" TextWrapping="Wrap" HorizontalAlignment="Center" TextAlignment="Center">
When you enable data bindings you can no longer use keyframes or normal values for this property. <LineBreak />
Instead you'll be using our node scripting system to set a value based on the data model or values of other layers/folders.
</TextBlock>
</StackPanel>
</Border>
</Grid>
</UserControl>
@@ -1,88 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Shared.Services;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings
{
public class DataBindingsViewModel : Screen
{
private readonly IProfileEditorService _profileEditorService;
private IDataBinding _dataBinding;
public DataBindingsViewModel(IProfileEditorService profileEditorService, INodeService nodeService, ISettingsService settingsService)
{
_profileEditorService = profileEditorService;
AvailableNodes = nodeService.AvailableNodes.ToList();
AlwaysShowValues = settingsService.GetSetting("ProfileEditor.AlwaysShowValues", true);
}
public List<NodeData> AvailableNodes { get; }
public PluginSetting<bool> AlwaysShowValues { get; }
public IDataBinding DataBinding
{
get => _dataBinding;
set => SetAndNotify(ref _dataBinding, value);
}
public bool DataBindingEnabled
{
get => _dataBinding?.IsEnabled ?? false;
set
{
if (_dataBinding != null)
_dataBinding.IsEnabled = value;
}
}
private void ProfileEditorServiceOnSelectedDataBindingChanged(object sender, EventArgs e)
{
SubscribeToSelectedDataBinding();
}
private void SubscribeToSelectedDataBinding()
{
if (DataBinding != null)
{
DataBinding.DataBindingEnabled -= DataBindingOnDataBindingToggled;
DataBinding.DataBindingDisabled -= DataBindingOnDataBindingToggled;
}
DataBinding = _profileEditorService.SelectedDataBinding;
if (DataBinding != null)
{
DataBinding.DataBindingEnabled += DataBindingOnDataBindingToggled;
DataBinding.DataBindingDisabled += DataBindingOnDataBindingToggled;
OnPropertyChanged(nameof(DataBindingEnabled));
}
}
private void DataBindingOnDataBindingToggled(object sender, DataBindingEventArgs e)
{
OnPropertyChanged(nameof(DataBindingEnabled));
}
#region Overrides of Screen
protected override void OnInitialActivate()
{
_profileEditorService.SelectedDataBindingChanged += ProfileEditorServiceOnSelectedDataBindingChanged;
SubscribeToSelectedDataBinding();
base.OnInitialActivate();
}
protected override void OnClose()
{
_profileEditorService.SelectedDataBindingChanged -= ProfileEditorServiceOnSelectedDataBindingChanged;
base.OnClose();
}
#endregion
}
}
@@ -1,53 +0,0 @@
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.LayerProperties.LayerEffects.EffectsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.LayerEffects"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:behaviors="clr-namespace:Artemis.UI.Behaviors"
xmlns:layerEffect="clr-namespace:Artemis.Core.LayerEffects;assembly=Artemis.Core"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:EffectsViewModel}">
<Grid Background="{DynamicResource MaterialDesignCardBackground}">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Top" Margin="16"
Visibility="{Binding HasLayerEffectDescriptors, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}}">
<materialDesign:PackIcon Kind="AutoAwesome" Width="80" Height="80" HorizontalAlignment="Center" />
<TextBlock Style="{StaticResource MaterialDesignBody1TextBlock}" TextWrapping="Wrap" HorizontalAlignment="Center" Margin="0 15">
Looks like you have no effects installed or enabled!
</TextBlock>
<TextBlock Style="{StaticResource MaterialDesignCaptionTextBlock}" TextWrapping="Wrap" HorizontalAlignment="Center">
Effects will apply some sort of post- or pre-processing to layers or even entire folders. <LineBreak />
Think of things like blur, black &amp; white but also audio visualization etc.
</TextBlock>
</StackPanel>
<ListBox ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedLayerEffectDescriptor}" HorizontalContentAlignment="Stretch"
Visibility="{Binding HasLayerEffectDescriptors, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="behaviors:MouseBehavior.MouseUpCommand" Value="{x:Static materialDesign:Transitioner.MoveFirstCommand}" />
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type layerEffect:LayerEffectDescriptor}">
<Border Padding="8" BorderThickness="0 0 0 1" BorderBrush="{DynamicResource MaterialDesignDivider}" VerticalAlignment="Stretch">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<shared:ArtemisIcon Icon="{Binding Icon}" Width="20" Height="20" VerticalAlignment="Center" />
<StackPanel Margin="8 0 0 0" Grid.Column="1" VerticalAlignment="Stretch">
<TextBlock FontWeight="Bold" Text="{Binding DisplayName}" TextWrapping="Wrap" />
<TextBlock Text="{Binding Description}" TextWrapping="Wrap" />
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</UserControl>
@@ -1,78 +0,0 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.LayerEffects;
using Artemis.Core.Services;
using Artemis.UI.Shared.Services;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.LayerEffects
{
public class EffectsViewModel : Conductor<LayerEffectDescriptor>.Collection.AllActive
{
private readonly IPluginManagementService _pluginManagementService;
private readonly IProfileEditorService _profileEditorService;
private LayerEffectDescriptor _selectedLayerEffectDescriptor;
public EffectsViewModel(LayerPropertiesViewModel layerPropertiesViewModel, IPluginManagementService pluginManagementService, IProfileEditorService profileEditorService)
{
_pluginManagementService = pluginManagementService;
_profileEditorService = profileEditorService;
LayerPropertiesViewModel = layerPropertiesViewModel;
PropertyChanged += HandleSelectedLayerEffectChanged;
}
public LayerPropertiesViewModel LayerPropertiesViewModel { get; }
public bool HasLayerEffectDescriptors => Items.Any();
public LayerEffectDescriptor SelectedLayerEffectDescriptor
{
get => _selectedLayerEffectDescriptor;
set => SetAndNotify(ref _selectedLayerEffectDescriptor, value);
}
public void PopulateDescriptors()
{
List<LayerEffectProvider> layerBrushProviders = _pluginManagementService.GetFeaturesOfType<LayerEffectProvider>();
List<LayerEffectDescriptor> descriptors = layerBrushProviders.SelectMany(l => l.LayerEffectDescriptors).ToList();
Items.AddRange(descriptors.Except(Items));
Items.RemoveRange(Items.Except(descriptors));
// Sort by display name
int index = 0;
foreach (LayerEffectDescriptor layerEffectDescriptor in Items.OrderBy(d => d.DisplayName).ToList())
{
if (Items.IndexOf(layerEffectDescriptor) != index)
((BindableCollection<LayerEffectDescriptor>) Items).Move(Items.IndexOf(layerEffectDescriptor), index);
index++;
}
SelectedLayerEffectDescriptor = null;
NotifyOfPropertyChange(nameof(HasLayerEffectDescriptors));
}
private void HandleSelectedLayerEffectChanged(object sender, PropertyChangedEventArgs e)
{
RenderProfileElement renderElement;
if (LayerPropertiesViewModel.SelectedLayer != null)
renderElement = LayerPropertiesViewModel.SelectedLayer;
else if (LayerPropertiesViewModel.SelectedFolder != null)
renderElement = LayerPropertiesViewModel.SelectedFolder;
else
return;
if (e.PropertyName == nameof(SelectedLayerEffectDescriptor) && SelectedLayerEffectDescriptor != null)
{
// Let the fancy animation run
Execute.PostToUIThread(async () =>
{
await Task.Delay(500);
renderElement.AddLayerEffect(SelectedLayerEffectDescriptor);
_profileEditorService.SaveSelectedProfileElement();
});
}
}
}
}
@@ -1,470 +0,0 @@
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:behaviors="clr-namespace:Artemis.UI.Behaviors"
xmlns:Converters="clr-namespace:Artemis.UI.Converters"
xmlns:utilities="clr-namespace:Artemis.UI.Utilities"
xmlns:layerProperties="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties"
xmlns:controls="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.Controls"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
x:Class="Artemis.UI.Screens.ProfileEditor.LayerProperties.LayerPropertiesView"
mc:Ignorable="d"
d:DesignHeight="450"
d:DesignWidth="800"
d:DataContext="{d:DesignInstance {x:Type layerProperties:LayerPropertiesViewModel}}"
behaviors:InputBindingBehavior.PropagateInputBindingsToWindow="True">
<UserControl.Resources>
<shared:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
<Converters:NullToBooleanConverter x:Key="NullToBooleanConverter" />
<utilities:BindingProxy x:Key="DataContextProxy" Data="{Binding}" />
<Style x:Key="SvStyle" TargetType="{x:Type ScrollViewer}">
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ScrollViewer}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ScrollContentPresenter Grid.ColumnSpan="2" Grid.RowSpan="2" />
<ScrollBar x:Name="PART_VerticalScrollBar"
HorizontalAlignment="Right"
Opacity="0.5"
Grid.Row="0"
Grid.Column="1"
Value="{TemplateBinding VerticalOffset}"
Maximum="{TemplateBinding ScrollableHeight}"
ViewportSize="{TemplateBinding ViewportHeight}"
Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" />
<ScrollBar x:Name="PART_HorizontalScrollBar"
VerticalAlignment="Bottom"
Orientation="Horizontal"
Opacity="0.5"
Grid.Row="1"
Grid.Column="0"
Value="{TemplateBinding HorizontalOffset}"
Maximum="{TemplateBinding ScrollableWidth}"
ViewportSize="{TemplateBinding ViewportWidth}"
Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<UserControl.InputBindings>
<KeyBinding Command="{s:Action Play}" Key="Space" />
<KeyBinding Command="{s:Action PlayFromStart}" Modifiers="Shift" Key="Space" />
</UserControl.InputBindings>
<materialDesign:DialogHost Identifier="LayerProperties" DialogTheme="Inherit">
<Grid x:Name="ContainerGrid">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="28" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1.5*" />
</Grid.ColumnDefinitions>
<!-- Left side -->
<Grid Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="48" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Misc controls & time display -->
<Border BorderThickness="0,0,1,0" BorderBrush="{DynamicResource MaterialDesignDivider}">
<DockPanel VerticalAlignment="Center">
<StackPanel Orientation="Horizontal">
<Button Style="{StaticResource MaterialDesignIconForegroundButton}"
ToolTip="Play from start (Shift+Space)" Command="{s:Action PlayFromStart}"
Focusable="False">
<materialDesign:PackIcon Kind="StepForward" />
</Button>
<Button Style="{StaticResource MaterialDesignIconForegroundButton}"
ToolTip="Toggle play/pause (Space)" Command="{s:Action Play}" Focusable="False">
<StackPanel>
<materialDesign:PackIcon Kind="Play"
Visibility="{Binding Playing, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}" />
<materialDesign:PackIcon Kind="Pause"
Visibility="{Binding Playing, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
</StackPanel>
</Button>
<Button Style="{StaticResource MaterialDesignIconForegroundButton}" ToolTip="Go to start" Command="{s:Action GoToStart}" Focusable="False">
<materialDesign:PackIcon Kind="SkipBackward" />
</Button>
<Button Style="{StaticResource MaterialDesignIconForegroundButton}" ToolTip="Go to end" Command="{s:Action GoToEnd}" Focusable="False">
<materialDesign:PackIcon Kind="SkipForward" />
</Button>
<Button Style="{StaticResource MaterialDesignIconForegroundButton}" ToolTip="Previous frame" Command="{s:Action GoToPreviousFrame}" Focusable="False">
<materialDesign:PackIcon Kind="SkipPrevious" />
</Button>
<Button Style="{StaticResource MaterialDesignIconForegroundButton}" ToolTip="Next frame" Command="{s:Action GoToNextFrame}" Focusable="False">
<materialDesign:PackIcon Kind="SkipNext" />
</Button>
<shared:LockableToggleButton Style="{StaticResource MaterialDesignFlatToggleButton}"
IsChecked="{Binding Repeating}"
IsLocked="True"
Focusable="False"
Click="{s:Action CycleRepeating}">
<shared:LockableToggleButton.ToolTip>
<StackPanel>
<StackPanel Visibility="{Binding Repeating, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
<TextBlock Text="Repeat entire timeline"
Visibility="{Binding RepeatTimeline, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
<TextBlock Text="Repeat segment"
Visibility="{Binding RepeatSegment, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
</StackPanel>
<TextBlock Visibility="{Binding Repeating, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}">
Don't repeat the timeline
</TextBlock>
<TextBlock TextWrapping="Wrap">
This setting only applies to the editor and <Run Text="does not" FontWeight="Bold" /> affect the repeat mode during normal profile playback
</TextBlock>
</StackPanel>
</shared:LockableToggleButton.ToolTip>
<Grid>
<materialDesign:PackIcon Kind="Repeat"
Height="24"
Width="24"
Visibility="{Binding RepeatTimeline, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
<materialDesign:PackIcon Kind="RepeatOne"
Height="24"
Width="24"
Visibility="{Binding RepeatSegment, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
</Grid>
</shared:LockableToggleButton>
</StackPanel>
<StackPanel VerticalAlignment="Center">
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}"
Text="{Binding FormattedCurrentTime}" HorizontalAlignment="Right" Margin="0 0 20 0" />
</StackPanel>
</DockPanel>
</Border>
<!-- Properties tree -->
<materialDesign:DialogHost Identifier="PropertyTreeDialogHost" DialogTheme="Inherit" CloseOnClickAway="True" Grid.Row="1">
<materialDesign:Transitioner x:Name="PropertyTreeTransitioner" SelectedIndex="{Binding PropertyTreeIndex}" DefaultTransitionOrigin="0.9, 0.9" AutoApplyTransitionOrigins="True">
<ScrollViewer x:Name="PropertyTreeScrollViewer"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Hidden"
ScrollChanged="TimelineScrollChanged">
<Border BorderThickness="0,0,1,0" BorderBrush="{DynamicResource MaterialDesignDivider}">
<ContentControl s:View.Model="{Binding TreeViewModel}"
shared:SizeObserver.Observe="True"
shared:SizeObserver.ObservedHeight="{Binding TreeViewModelHeight, Mode=OneWayToSource}" />
</Border>
</ScrollViewer>
<materialDesign:TransitionerSlide>
<materialDesign:TransitionerSlide.BackwardWipe>
<materialDesign:CircleWipe />
</materialDesign:TransitionerSlide.BackwardWipe>
<Grid>
<ContentControl s:View.Model="{Binding EffectsViewModel}" />
<!-- Transitions only work when the command comes from inside the transitioner but we want the button outside,
by setting the command target to this hidden button we circumvent that -->
<Button x:Name="TransitionCommandAnchor"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Width="110"
Height="1"
Visibility="Hidden" />
</Grid>
</materialDesign:TransitionerSlide>
</materialDesign:Transitioner>
</materialDesign:DialogHost>
</Grid>
<!-- Resize -->
<GridSplitter Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Width="15" HorizontalAlignment="Stretch" Cursor="SizeWE" Margin="-15 0" Background="Transparent" />
<!-- Right side -->
<materialDesign:Transitioner Grid.Row="0"
Grid.Column="2"
x:Name="RightSideTransitioner"
SelectedIndex="{Binding RightSideIndex}"
DefaultTransitionOrigin="0, 0.5">
<materialDesign:TransitionerSlide>
<materialDesign:TransitionerSlide.BackwardWipe>
<materialDesign:CircleWipe />
</materialDesign:TransitionerSlide.BackwardWipe>
<Grid>
<Grid.InputBindings>
<KeyBinding Key="Delete" Command="{s:Action DeleteKeyframes}" s:View.ActionTarget="{Binding TimelineViewModel}" />
<KeyBinding Key="D" Modifiers="Control" Command="{s:Action DuplicateKeyframes}" s:View.ActionTarget="{Binding TimelineViewModel}" />
<KeyBinding Key="C" Modifiers="Control" Command="{s:Action CopyKeyframes}" s:View.ActionTarget="{Binding TimelineViewModel}" />
<KeyBinding Key="V" Modifiers="Control" Command="{s:Action PasteKeyframes}" s:View.ActionTarget="{Binding TimelineViewModel}" />
</Grid.InputBindings>
<Grid.RowDefinitions>
<RowDefinition Height="48" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Timeline headers -->
<ScrollViewer Grid.Row="0"
x:Name="TimelineHeaderScrollViewer"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Hidden"
ScrollChanged="TimelineScrollChanged">
<Canvas Background="{DynamicResource MaterialDesignCardBackground}" Width="{Binding ActualWidth, ElementName=PropertyTimeLine}">
<!-- Timeline segments -->
<ContentControl Canvas.Left="{Binding EndTimelineSegmentViewModel.SegmentStartPosition}" s:View.Model="{Binding EndTimelineSegmentViewModel}" />
<ContentControl Canvas.Left="{Binding MainTimelineSegmentViewModel.SegmentStartPosition}" s:View.Model="{Binding MainTimelineSegmentViewModel}" />
<ContentControl Canvas.Left="{Binding StartTimelineSegmentViewModel.SegmentStartPosition}" s:View.Model="{Binding StartTimelineSegmentViewModel}" />
<!-- Timeline header body -->
<controls:PropertyTimelineHeader Margin="0 18 0 0"
Foreground="{DynamicResource MaterialDesignBody}"
Background="{DynamicResource MaterialDesignCardBackground}"
PixelsPerSecond="{Binding ProfileEditorService.PixelsPerSecond}"
HorizontalOffset="{Binding ContentHorizontalOffset, ElementName=TimelineHeaderScrollViewer}"
VisibleWidth="{Binding ActualWidth, ElementName=TimelineHeaderScrollViewer}"
OffsetFirstValue="True"
MouseLeftButtonUp="{s:Action TimelineJump}"
Width="{Binding ActualWidth, ElementName=PropertyTimeLine}"
Cursor="Hand" />
<!-- Timeline caret -->
<Polygon Canvas.Left="{Binding TimeCaretPosition}"
Cursor="SizeWE"
MouseDown="{s:Action TimelineMouseDown}"
MouseUp="{s:Action TimelineMouseUp}"
MouseMove="{s:Action TimelineMouseMove}"
Points="-8,0 -8,8 0,20, 8,8 8,0"
Fill="{StaticResource SecondaryHueMidBrush}" />
<Line Canvas.Left="{Binding TimeCaretPosition}"
Cursor="SizeWE"
MouseDown="{s:Action TimelineMouseDown}"
MouseUp="{s:Action TimelineMouseUp}"
MouseMove="{s:Action TimelineMouseMove}"
X1="0"
X2="0"
Y1="0"
Y2="{Binding ActualHeight, ElementName=TimelineHeaderScrollViewer}"
StrokeThickness="2"
Stroke="{StaticResource SecondaryHueMidBrush}" />
</Canvas>
</ScrollViewer>
<!-- Timeline rails -->
<ScrollViewer Grid.Row="1"
x:Name="TimelineRailsScrollViewer"
Style="{StaticResource SvStyle}"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
ScrollChanged="TimelineScrollChanged">
<Grid Background="{DynamicResource MaterialDesignToolBarBackground}" x:Name="RailsGrid">
<Canvas Grid.Column="0" Panel.ZIndex="1">
<Line Canvas.Left="{Binding TimeCaretPosition}"
Cursor="SizeWE"
MouseDown="{s:Action TimelineMouseDown}"
MouseUp="{s:Action TimelineMouseUp}"
MouseMove="{s:Action TimelineMouseMove}"
X1="0"
X2="0"
Y1="0"
Y2="{Binding ActualHeight, ElementName=RailsGrid}"
StrokeThickness="2"
Stroke="{StaticResource SecondaryHueMidBrush}" />
</Canvas>
<ContentControl Grid.Column="0" s:View.Model="{Binding TimelineViewModel}" x:Name="PropertyTimeLine" />
</Grid>
</ScrollViewer>
</Grid>
</materialDesign:TransitionerSlide>
<materialDesign:TransitionerSlide>
<materialDesign:TransitionerSlide.BackwardWipe>
<materialDesign:CircleWipe />
</materialDesign:TransitionerSlide.BackwardWipe>
<Grid Background="{StaticResource MaterialDesignCardBackground}">
<ContentControl s:View.Model="{Binding DataBindingsViewModel}" />
</Grid>
</materialDesign:TransitionerSlide>
</materialDesign:Transitioner>
<!-- Bottom row, a bit hacky but has a ZIndex of 2 to cut off the time caret that overlaps the entire timeline -->
<Grid Grid.Row="1"
Grid.Column="0"
Panel.ZIndex="2"
HorizontalAlignment="Stretch"
Background="{DynamicResource MaterialDesignCardBackground}">
<!-- Selected layer controls -->
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0"
Orientation="Horizontal"
Margin="6"
Visibility="{Binding SelectedLayer, Converter={StaticResource NullToVisibilityConverter}}">
<materialDesign:PackIcon Kind="Layers" Width="16" />
<shared:ArtemisIcon Icon="{Binding SelectedLayer.LayerBrush.Descriptor.Icon}"
Width="16"
Margin="5 0 0 0"
ToolTip="{Binding SelectedLayer.LayerBrush.Descriptor.DisplayName, Mode=OneWay}"
Background="Transparent"
Visibility="{Binding SelectedLayer.LayerBrush, Converter={StaticResource NullToVisibilityConverter}}" />
<TextBlock Text="{Binding SelectedLayer.Name}" Margin="5 0 0 0" />
</StackPanel>
<StackPanel Grid.Column="0"
Orientation="Horizontal"
Margin="6"
Visibility="{Binding SelectedFolder, Converter={StaticResource NullToVisibilityConverter}}">
<materialDesign:PackIcon Kind="Folder" Width="16" />
<TextBlock Text="{Binding SelectedFolder.Name}" Margin="5 0 0 0" />
</StackPanel>
<Button Grid.Column="1"
Style="{StaticResource MaterialDesignFlatMidBgButton}"
Margin="0 -2 5 -2"
Padding="10 0"
Height="20"
Width="115"
ToolTip="Select an effect to add"
VerticalAlignment="Center"
Visibility="{Binding PropertyTreeVisible, Converter={x:Static s:BoolToVisibilityConverter.Instance}}"
Command="{x:Static materialDesign:Transitioner.MoveLastCommand}"
CommandTarget="{Binding ElementName=TransitionCommandAnchor}"
Click="{s:Action ToggleEffectsViewModel}"
IsEnabled="{Binding CanToggleEffectsViewModel}">
<TextBlock FontSize="11">
ADD EFFECT
</TextBlock>
</Button>
<Button Grid.Column="1"
Style="{StaticResource MaterialDesignFlatMidBgButton}"
Margin="0 -2 5 -2"
Padding="10 0"
Height="20"
Width="115"
ToolTip="Show the layer/folder properties"
VerticalAlignment="Center"
Visibility="{Binding PropertyTreeVisible, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}}"
Command="{x:Static materialDesign:Transitioner.MoveFirstCommand}"
CommandTarget="{Binding ElementName=TransitionCommandAnchor}"
Click="{s:Action ToggleEffectsViewModel}"
IsEnabled="{Binding CanToggleEffectsViewModel}">
<TextBlock FontSize="10">
SHOW PROPERTIES
</TextBlock>
</Button>
</Grid>
<Grid Grid.Row="1"
Grid.Column="2"
HorizontalAlignment="Stretch"
Panel.ZIndex="2"
ClipToBounds="False"
Background="{DynamicResource MaterialDesignCardBackground}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Grid.Column="0"
Margin="5 -2 0 -2"
Padding="10 0"
Height="20"
Width="110"
ToolTip="Add a new segment to the timeline"
VerticalAlignment="Center"
IsEnabled="{Binding SelectedProfileElement, Converter={StaticResource NullToBooleanConverter}}"
Visibility="{Binding TimelineVisible, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
<Button.Style>
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource MaterialDesignFlatAccentBgButton}">
<Style.Triggers>
<EventTrigger RoutedEvent="Click">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="ContextMenu.IsOpen">
<DiscreteBooleanKeyFrame KeyTime="0:0:0" Value="True" />
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Style.Triggers>
</Style>
</Button.Style>
<Button.ContextMenu>
<ContextMenu>
<ContextMenu.Resources>
<Converters:InverseBooleanConverter x:Key="InverseBooleanConverter" />
</ContextMenu.Resources>
<MenuItem Header="Start"
Command="{s:Action EnableSegment}"
CommandParameter="Start"
IsEnabled="{Binding Data.StartTimelineSegmentViewModel.SegmentEnabled, Converter={StaticResource InverseBooleanConverter}, Source={StaticResource DataContextProxy}}" />
<MenuItem Header="Main"
Command="{s:Action EnableSegment}"
CommandParameter="Main"
IsEnabled="{Binding Data.MainTimelineSegmentViewModel.SegmentEnabled, Converter={StaticResource InverseBooleanConverter}, Source={StaticResource DataContextProxy}}" />
<MenuItem Header="End"
Command="{s:Action EnableSegment}"
CommandParameter="End"
IsEnabled="{Binding Data.EndTimelineSegmentViewModel.SegmentEnabled, Converter={StaticResource InverseBooleanConverter}, Source={StaticResource DataContextProxy}}" />
</ContextMenu>
</Button.ContextMenu>
<TextBlock FontSize="11">
ADD SEGMENT
</TextBlock>
</Button>
<!-- Zoom control -->
<Slider Grid.Column="1"
Orientation="Horizontal"
HorizontalAlignment="Right"
VerticalAlignment="Center"
materialDesign:SliderAssist.OnlyShowFocusVisualWhileDragging="True"
Minimum="31"
Maximum="350"
Margin="10 0"
Value="{Binding ProfileEditorService.PixelsPerSecond}"
Foreground="{StaticResource SecondaryHueMidBrush}"
Width="319" />
</Grid>
<!-- Suspended overlay -->
<Border Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="3"
Grid.RowSpan="2"
Panel.ZIndex="3"
Background="#CD353535"
Visibility="{Binding SuspendedEditing, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Margin="16">
<materialDesign:PackIcon Kind="TimerOffOutline" Width="150" Height="150" HorizontalAlignment="Center" />
<TextBlock Style="{StaticResource MaterialDesignHeadline4TextBlock}" TextWrapping="Wrap" HorizontalAlignment="Center" Margin="0 10">
Timeline suspended
</TextBlock>
<TextBlock Style="{StaticResource MaterialDesignBody1TextBlock}" TextWrapping="Wrap" HorizontalAlignment="Center" TextAlignment="Center">
The profile is currently running in normal mode and the timeline cannot be edited.
</TextBlock>
<TextBlock Style="{StaticResource MaterialDesignBody2TextBlock}" TextWrapping="Wrap" HorizontalAlignment="Center" TextAlignment="Center">
Press <Run Text="F5" FontWeight="Bold"/> to switch between editor mode and normal mode. Auto-switching can be disabled in the run menu.
</TextBlock>
</StackPanel>
</Border>
</Grid>
</materialDesign:DialogHost>
</UserControl>
@@ -1,29 +0,0 @@
using System.Windows.Controls;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties
{
/// <summary>
/// Interaction logic for LayerPropertiesView.xaml
/// </summary>
public partial class LayerPropertiesView : UserControl
{
public LayerPropertiesView()
{
InitializeComponent();
}
// Keeping the scroll viewers in sync is up to the view, not a viewmodel concern
private void TimelineScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (e.OriginalSource == TimelineHeaderScrollViewer)
TimelineRailsScrollViewer.ScrollToHorizontalOffset(e.HorizontalOffset);
else if (e.OriginalSource == PropertyTreeScrollViewer)
TimelineRailsScrollViewer.ScrollToVerticalOffset(e.VerticalOffset);
else if (e.OriginalSource == TimelineRailsScrollViewer)
{
TimelineHeaderScrollViewer.ScrollToHorizontalOffset(e.HorizontalOffset);
PropertyTreeScrollViewer.ScrollToVerticalOffset(e.VerticalOffset);
}
}
}
}
@@ -1,682 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using Artemis.Core;
using Artemis.Core.LayerEffects;
using Artemis.Core.Services;
using Artemis.UI.Extensions;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings;
using Artemis.UI.Screens.ProfileEditor.LayerProperties.LayerEffects;
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline;
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using GongSolutions.Wpf.DragDrop;
using Stylet;
using static Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree.TreeGroupViewModel.LayerPropertyGroupType;
using MouseButtonEventArgs = System.Windows.Input.MouseButtonEventArgs;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties
{
public class LayerPropertiesViewModel : Conductor<LayerPropertyGroupViewModel>.Collection.AllActive, IProfileEditorPanelViewModel, IDropTarget
{
private readonly ILayerPropertyVmFactory _layerPropertyVmFactory;
private LayerPropertyGroupViewModel _brushPropertyGroup;
private bool _repeating;
private bool _repeatSegment;
private bool _repeatTimeline = true;
private int _propertyTreeIndex;
private int _rightSideIndex;
private RenderProfileElement _selectedProfileElement;
private DateTime _lastEffectsViewModelToggle;
private double _treeViewModelHeight;
public LayerPropertiesViewModel(IProfileEditorService profileEditorService,
ICoreService coreService,
ISettingsService settingsService,
ILayerPropertyVmFactory layerPropertyVmFactory,
DataBindingsViewModel dataBindingsViewModel)
{
_layerPropertyVmFactory = layerPropertyVmFactory;
ProfileEditorService = profileEditorService;
CoreService = coreService;
SettingsService = settingsService;
PropertyChanged += HandlePropertyTreeIndexChanged;
// Left side
TreeViewModel = _layerPropertyVmFactory.TreeViewModel(this, Items);
TreeViewModel.ConductWith(this);
EffectsViewModel = _layerPropertyVmFactory.EffectsViewModel(this);
EffectsViewModel.ConductWith(this);
// Right side
StartTimelineSegmentViewModel = _layerPropertyVmFactory.TimelineSegmentViewModel(SegmentViewModelType.Start, Items);
StartTimelineSegmentViewModel.ConductWith(this);
MainTimelineSegmentViewModel = _layerPropertyVmFactory.TimelineSegmentViewModel(SegmentViewModelType.Main, Items);
MainTimelineSegmentViewModel.ConductWith(this);
EndTimelineSegmentViewModel = _layerPropertyVmFactory.TimelineSegmentViewModel(SegmentViewModelType.End, Items);
EndTimelineSegmentViewModel.ConductWith(this);
TimelineViewModel = _layerPropertyVmFactory.TimelineViewModel(this, Items);
TimelineViewModel.ConductWith(this);
DataBindingsViewModel = dataBindingsViewModel;
DataBindingsViewModel.ConductWith(this);
}
#region Child VMs
public TreeViewModel TreeViewModel { get; }
public EffectsViewModel EffectsViewModel { get; }
public TimelineSegmentViewModel StartTimelineSegmentViewModel { get; }
public TimelineSegmentViewModel MainTimelineSegmentViewModel { get; }
public TimelineSegmentViewModel EndTimelineSegmentViewModel { get; }
public TimelineViewModel TimelineViewModel { get; }
public DataBindingsViewModel DataBindingsViewModel { get; }
#endregion
public IProfileEditorService ProfileEditorService { get; }
public ICoreService CoreService { get; }
public ISettingsService SettingsService { get; }
public bool Playing
{
get => ProfileEditorService.Playing;
set
{
ProfileEditorService.Playing = value;
NotifyOfPropertyChange(nameof(Playing));
}
}
public bool Repeating
{
get => _repeating;
set => SetAndNotify(ref _repeating, value);
}
public bool RepeatSegment
{
get => _repeatSegment;
set => SetAndNotify(ref _repeatSegment, value);
}
public bool RepeatTimeline
{
get => _repeatTimeline;
set => SetAndNotify(ref _repeatTimeline, value);
}
public string FormattedCurrentTime => $"{Math.Floor(ProfileEditorService.CurrentTime.TotalSeconds):00}.{ProfileEditorService.CurrentTime.Milliseconds:000}";
public double TimeCaretPosition
{
get => ProfileEditorService.CurrentTime.TotalSeconds * ProfileEditorService.PixelsPerSecond;
set => ProfileEditorService.CurrentTime = TimeSpan.FromSeconds(value / ProfileEditorService.PixelsPerSecond);
}
public bool SuspendedEditing => ProfileEditorService.SuspendEditing;
public int PropertyTreeIndex
{
get => _propertyTreeIndex;
set
{
if (!SetAndNotify(ref _propertyTreeIndex, value)) return;
NotifyOfPropertyChange(nameof(PropertyTreeVisible));
}
}
public int RightSideIndex
{
get => _rightSideIndex;
set
{
if (!SetAndNotify(ref _rightSideIndex, value)) return;
NotifyOfPropertyChange(nameof(TimelineVisible));
}
}
public bool CanToggleEffectsViewModel => SelectedProfileElement != null && DateTime.Now - _lastEffectsViewModelToggle > TimeSpan.FromMilliseconds(250);
public bool PropertyTreeVisible => PropertyTreeIndex == 0;
public bool TimelineVisible => RightSideIndex == 0;
public RenderProfileElement SelectedProfileElement
{
get => _selectedProfileElement;
set
{
if (!SetAndNotify(ref _selectedProfileElement, value)) return;
NotifyOfPropertyChange(nameof(SelectedLayer));
NotifyOfPropertyChange(nameof(SelectedFolder));
NotifyOfPropertyChange(nameof(SelectedFolder));
NotifyOfPropertyChange(nameof(CanToggleEffectsViewModel));
}
}
public Layer SelectedLayer => SelectedProfileElement as Layer;
public Folder SelectedFolder => SelectedProfileElement as Folder;
public double TreeViewModelHeight
{
get => _treeViewModelHeight;
set => SetAndNotify(ref _treeViewModelHeight, value);
}
#region Segments
public void EnableSegment(string segment)
{
if (segment == "Start")
StartTimelineSegmentViewModel.EnableSegment();
else if (segment == "Main")
MainTimelineSegmentViewModel.EnableSegment();
else if (segment == "End")
EndTimelineSegmentViewModel.EnableSegment();
}
#endregion
protected override void OnInitialActivate()
{
PopulateProperties(ProfileEditorService.SelectedProfileElement);
ProfileEditorService.SelectedProfileElementChanged += SelectedProfileEditorServiceOnSelectedProfileElementChanged;
ProfileEditorService.CurrentTimeChanged += ProfileEditorServiceOnCurrentTimeChanged;
ProfileEditorService.SuspendEditingChanged += ProfileEditorServiceOnSuspendEditingChanged;
ProfileEditorService.SelectedDataBindingChanged += ProfileEditorServiceOnSelectedDataBindingChanged;
ProfileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged;
base.OnInitialActivate();
}
protected override void OnClose()
{
ProfileEditorService.SelectedProfileElementChanged -= SelectedProfileEditorServiceOnSelectedProfileElementChanged;
ProfileEditorService.CurrentTimeChanged -= ProfileEditorServiceOnCurrentTimeChanged;
ProfileEditorService.SuspendEditingChanged -= ProfileEditorServiceOnSuspendEditingChanged;
ProfileEditorService.SelectedDataBindingChanged -= ProfileEditorServiceOnSelectedDataBindingChanged;
ProfileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged;
PopulateProperties(null);
base.OnClose();
}
protected override void OnDeactivate()
{
Pause();
base.OnDeactivate();
}
private void HandlePropertyTreeIndexChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(PropertyTreeIndex) && PropertyTreeIndex == 1)
EffectsViewModel.PopulateDescriptors();
}
private void SelectedProfileEditorServiceOnSelectedProfileElementChanged(object sender, RenderProfileElementEventArgs e)
{
Execute.PostToUIThread(() => PopulateProperties(e.RenderProfileElement));
}
private void ProfileEditorServiceOnCurrentTimeChanged(object sender, EventArgs e)
{
NotifyOfPropertyChange(nameof(FormattedCurrentTime));
NotifyOfPropertyChange(nameof(TimeCaretPosition));
}
private void ProfileEditorServiceOnSuspendEditingChanged(object sender, EventArgs e)
{
NotifyOfPropertyChange(nameof(SuspendedEditing));
}
private void ProfileEditorServiceOnPixelsPerSecondChanged(object sender, EventArgs e)
{
NotifyOfPropertyChange(nameof(TimeCaretPosition));
}
private void ProfileEditorServiceOnSelectedDataBindingChanged(object sender, EventArgs e)
{
RightSideIndex = ProfileEditorService.SelectedDataBinding != null ? 1 : 0;
}
#region View model managament
public List<LayerPropertyGroupViewModel> GetAllLayerPropertyGroupViewModels()
{
List<LayerPropertyGroupViewModel> groups = Items.ToList();
List<LayerPropertyGroupViewModel> toAdd = groups.SelectMany(g => g.Items).Where(g => g is LayerPropertyGroupViewModel).Cast<LayerPropertyGroupViewModel>().ToList();
groups.AddRange(toAdd);
return groups;
}
private void PopulateProperties(RenderProfileElement profileElement)
{
// Unsubscribe from old selected element
if (SelectedProfileElement != null)
SelectedProfileElement.LayerEffectsUpdated -= SelectedElementOnLayerEffectsUpdated;
if (SelectedLayer != null)
SelectedLayer.LayerBrushUpdated -= SelectedLayerOnLayerBrushUpdated;
// Clear old properties
Items.Clear();
_brushPropertyGroup = null;
if (profileElement == null)
return;
// Subscribe to new element
SelectedProfileElement = profileElement;
SelectedProfileElement.LayerEffectsUpdated += SelectedElementOnLayerEffectsUpdated;
if (SelectedLayer != null)
{
SelectedLayer.LayerBrushUpdated += SelectedLayerOnLayerBrushUpdated;
// Add the built-in root groups of the layer
Items.Add(_layerPropertyVmFactory.LayerPropertyGroupViewModel(SelectedLayer.General));
Items.Add(_layerPropertyVmFactory.LayerPropertyGroupViewModel(SelectedLayer.Transform));
}
ApplyLayerBrush();
ApplyEffects();
}
private void SelectedLayerOnLayerBrushUpdated(object sender, EventArgs e)
{
ApplyLayerBrush();
}
private void SelectedElementOnLayerEffectsUpdated(object sender, EventArgs e)
{
ApplyEffects();
}
public void ApplyLayerBrush()
{
if (SelectedLayer == null)
return;
bool hideRenderRelatedProperties = SelectedLayer?.LayerBrush != null && !SelectedLayer.LayerBrush.SupportsTransformation;
SelectedLayer.General.ShapeType.IsHidden = hideRenderRelatedProperties;
SelectedLayer.General.BlendMode.IsHidden = hideRenderRelatedProperties;
SelectedLayer.Transform.IsHidden = hideRenderRelatedProperties;
if (_brushPropertyGroup != null)
{
Items.Remove(_brushPropertyGroup);
_brushPropertyGroup = null;
}
if (SelectedLayer.LayerBrush != null)
{
_brushPropertyGroup = _layerPropertyVmFactory.LayerPropertyGroupViewModel(SelectedLayer.LayerBrush.BaseProperties);
Items.Add(_brushPropertyGroup);
}
SortProperties();
}
private void ApplyEffects()
{
if (SelectedProfileElement == null)
return;
// // Remove VMs of effects no longer applied on the layer
// List<LayerPropertyGroupViewModel> toRemove = Items
// .Where(l => l.LayerPropertyGroup.LayerEffect != null && !SelectedProfileElement.LayerEffects.Contains(l.LayerPropertyGroup.LayerEffect))
// .ToList();
// Items.RemoveRange(toRemove);
//
// foreach (BaseLayerEffect layerEffect in SelectedProfileElement.LayerEffects)
// {
// if (Items.Any(l => l.LayerPropertyGroup.LayerEffect == layerEffect) || layerEffect.BaseProperties == null)
// continue;
//
// Items.Add(_layerPropertyVmFactory.LayerPropertyGroupViewModel(layerEffect.BaseProperties));
// }
SortProperties();
}
private void SortProperties()
{
// Get all non-effect properties
List<LayerPropertyGroupViewModel> nonEffectProperties = Items
.Where(l => l.TreeGroupViewModel.GroupType != LayerEffectRoot)
.ToList();
// // Order the effects
// List<LayerPropertyGroupViewModel> effectProperties = Items
// .Where(l => l.TreeGroupViewModel.GroupType == LayerEffectRoot && l.LayerPropertyGroup.LayerEffect != null)
// .OrderBy(l => l.LayerPropertyGroup.LayerEffect.Order)
// .ToList();
// Put the non-effect properties in front
for (int index = 0; index < nonEffectProperties.Count; index++)
{
LayerPropertyGroupViewModel layerPropertyGroupViewModel = nonEffectProperties[index];
if (Items.IndexOf(layerPropertyGroupViewModel) != index)
((BindableCollection<LayerPropertyGroupViewModel>) Items).Move(Items.IndexOf(layerPropertyGroupViewModel), index);
}
// // Put the effect properties after, sorted by their order
// for (int index = 0; index < effectProperties.Count; index++)
// {
// LayerPropertyGroupViewModel layerPropertyGroupViewModel = effectProperties[index];
// if (Items.IndexOf(layerPropertyGroupViewModel) != index + nonEffectProperties.Count)
// ((BindableCollection<LayerPropertyGroupViewModel>) Items).Move(Items.IndexOf(layerPropertyGroupViewModel), index + nonEffectProperties.Count);
// }
}
public async void ToggleEffectsViewModel()
{
_lastEffectsViewModelToggle = DateTime.Now;
NotifyOfPropertyChange(nameof(CanToggleEffectsViewModel));
await Task.Delay(300);
NotifyOfPropertyChange(nameof(CanToggleEffectsViewModel));
}
#endregion
#region Drag and drop
public void DragOver(IDropInfo dropInfo)
{
// Workaround for https://github.com/punker76/gong-wpf-dragdrop/issues/344
// Luckily we know the index can never be 1 so it's an easy enough fix
if (dropInfo.InsertIndex == 1)
return;
LayerPropertyGroupViewModel source = dropInfo.Data as LayerPropertyGroupViewModel;
LayerPropertyGroupViewModel target = dropInfo.TargetItem as LayerPropertyGroupViewModel;
if (source == target ||
target?.TreeGroupViewModel.GroupType != LayerEffectRoot ||
source?.TreeGroupViewModel.GroupType != LayerEffectRoot)
return;
dropInfo.DropTargetAdorner = DropTargetAdorners.Insert;
dropInfo.Effects = DragDropEffects.Move;
}
public void Drop(IDropInfo dropInfo)
{
// Workaround for https://github.com/punker76/gong-wpf-dragdrop/issues/344
// Luckily we know the index can never be 1 so it's an easy enough fix
if (dropInfo.InsertIndex == 1)
return;
LayerPropertyGroupViewModel source = dropInfo.Data as LayerPropertyGroupViewModel;
LayerPropertyGroupViewModel target = dropInfo.TargetItem as LayerPropertyGroupViewModel;
if (source == target ||
target?.TreeGroupViewModel.GroupType != LayerEffectRoot ||
source?.TreeGroupViewModel.GroupType != LayerEffectRoot)
return;
if (dropInfo.InsertPosition == RelativeInsertPosition.BeforeTargetItem)
MoveBefore(source, target);
else if (dropInfo.InsertPosition == RelativeInsertPosition.AfterTargetItem)
MoveAfter(source, target);
ApplyCurrentEffectsOrder();
ProfileEditorService.SaveSelectedProfileConfiguration();
}
private void MoveBefore(LayerPropertyGroupViewModel source, LayerPropertyGroupViewModel target)
{
if (Items.IndexOf(target) == Items.IndexOf(source) + 1)
return;
((BindableCollection<LayerPropertyGroupViewModel>) Items).Move(Items.IndexOf(source), Items.IndexOf(target));
}
private void MoveAfter(LayerPropertyGroupViewModel source, LayerPropertyGroupViewModel target)
{
Items.Remove(source);
Items.Insert(Items.IndexOf(target) + 1, source);
}
private void ApplyCurrentEffectsOrder()
{
int order = 1;
foreach (LayerPropertyGroupViewModel groupViewModel in Items.Where(p => p.TreeGroupViewModel.GroupType == LayerEffectRoot))
{
groupViewModel.UpdateOrder(order);
order++;
}
}
#endregion
#region Controls
public void PlayFromStart()
{
if (!Playing)
ProfileEditorService.CurrentTime = TimeSpan.Zero;
Play();
}
public void Play()
{
if (!IsActive)
return;
if (Playing)
{
Pause();
return;
}
CoreService.FrameRendering += CoreServiceOnFrameRendering;
Playing = true;
}
public void Pause()
{
if (!Playing)
return;
CoreService.FrameRendering -= CoreServiceOnFrameRendering;
Playing = false;
}
public void GoToStart()
{
ProfileEditorService.CurrentTime = TimeSpan.Zero;
}
public void GoToEnd()
{
if (SelectedProfileElement == null)
return;
ProfileEditorService.CurrentTime = SelectedProfileElement.Timeline.EndSegmentEndPosition;
}
public void GoToPreviousFrame()
{
if (SelectedProfileElement == null)
return;
double frameTime = 1000.0 / SettingsService.GetSetting("Core.TargetFrameRate", 30).Value;
double newTime = Math.Max(0, Math.Round((ProfileEditorService.CurrentTime.TotalMilliseconds - frameTime) / frameTime) * frameTime);
ProfileEditorService.CurrentTime = TimeSpan.FromMilliseconds(newTime);
}
public void GoToNextFrame()
{
if (SelectedProfileElement == null)
return;
double frameTime = 1000.0 / SettingsService.GetSetting("Core.TargetFrameRate", 30).Value;
double newTime = Math.Round((ProfileEditorService.CurrentTime.TotalMilliseconds + frameTime) / frameTime) * frameTime;
newTime = Math.Min(newTime, SelectedProfileElement.Timeline.EndSegmentEndPosition.TotalMilliseconds);
ProfileEditorService.CurrentTime = TimeSpan.FromMilliseconds(newTime);
}
public void CycleRepeating()
{
if (!Repeating)
{
RepeatTimeline = true;
RepeatSegment = false;
Repeating = true;
}
else if (RepeatTimeline)
{
RepeatTimeline = false;
RepeatSegment = true;
}
else if (RepeatSegment)
{
RepeatTimeline = true;
RepeatSegment = false;
Repeating = false;
}
}
private TimeSpan GetCurrentSegmentStart()
{
TimeSpan current = ProfileEditorService.CurrentTime;
if (current < StartTimelineSegmentViewModel.SegmentEnd)
return StartTimelineSegmentViewModel.SegmentStart;
if (current < MainTimelineSegmentViewModel.SegmentEnd)
return MainTimelineSegmentViewModel.SegmentStart;
if (current < EndTimelineSegmentViewModel.SegmentEnd)
return EndTimelineSegmentViewModel.SegmentStart;
return TimeSpan.Zero;
}
private TimeSpan GetCurrentSegmentEnd()
{
TimeSpan current = ProfileEditorService.CurrentTime;
if (current < StartTimelineSegmentViewModel.SegmentEnd)
return StartTimelineSegmentViewModel.SegmentEnd;
if (current < MainTimelineSegmentViewModel.SegmentEnd)
return MainTimelineSegmentViewModel.SegmentEnd;
if (current < EndTimelineSegmentViewModel.SegmentEnd)
return EndTimelineSegmentViewModel.SegmentEnd;
return TimeSpan.Zero;
}
private void CoreServiceOnFrameRendering(object sender, FrameRenderingEventArgs e)
{
if (!ProfileEditorService.Playing)
{
CoreService.FrameRendering -= CoreServiceOnFrameRendering;
return;
}
TimeSpan newTime = ProfileEditorService.CurrentTime.Add(TimeSpan.FromSeconds(e.DeltaTime));
if (SelectedProfileElement != null)
{
if (Repeating && RepeatTimeline)
{
if (newTime > SelectedProfileElement.Timeline.Length)
newTime = TimeSpan.Zero;
}
else if (Repeating && RepeatSegment)
{
if (newTime > GetCurrentSegmentEnd())
newTime = GetCurrentSegmentStart();
}
else if (newTime > SelectedProfileElement.Timeline.Length)
{
newTime = SelectedProfileElement.Timeline.Length;
Pause();
}
}
// Update current time on high priority to keep things buttery smooth as if you're using the mouse ༼ つ ◕_◕ ༽つ
Execute.OnUIThreadSync(() => ProfileEditorService.CurrentTime = newTime);
}
#endregion
#region Caret movement
public void TimelineMouseDown(object sender, MouseButtonEventArgs e)
{
((IInputElement) sender).CaptureMouse();
}
public void TimelineMouseUp(object sender, MouseButtonEventArgs e)
{
((IInputElement) sender).ReleaseMouseCapture();
}
public void TimelineMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
// Get the parent grid, need that for our position
IInputElement parent = (IInputElement) VisualTreeHelper.GetParent((DependencyObject) sender);
double x = Math.Max(0, e.GetPosition(parent).X);
TimeSpan newTime = TimeSpan.FromSeconds(x / ProfileEditorService.PixelsPerSecond);
// Round the time to something that fits the current zoom level
if (ProfileEditorService.PixelsPerSecond < 200)
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 5.0) * 5.0);
else if (ProfileEditorService.PixelsPerSecond < 500)
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 2.0) * 2.0);
else
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds));
// If holding down shift, snap to the closest segment or keyframe
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
{
List<TimeSpan> snapTimes = Items.SelectMany(g => g.GetAllKeyframeViewModels(true)).Select(k => k.Position).ToList();
TimeSpan snappedTime = ProfileEditorService.SnapToTimeline(newTime, TimeSpan.FromMilliseconds(1000f / ProfileEditorService.PixelsPerSecond * 5), true, false, snapTimes);
ProfileEditorService.CurrentTime = snappedTime;
return;
}
// If holding down control, round to the closest 50ms
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
{
TimeSpan roundedTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 50.0) * 50.0);
ProfileEditorService.CurrentTime = roundedTime;
return;
}
ProfileEditorService.CurrentTime = newTime;
}
}
public void TimelineJump(object sender, MouseButtonEventArgs e)
{
// Get the parent grid, need that for our position
IInputElement parent = (IInputElement) VisualTreeHelper.GetParent((DependencyObject) sender);
double x = Math.Max(0, e.GetPosition(parent).X);
TimeSpan newTime = TimeSpan.FromSeconds(x / ProfileEditorService.PixelsPerSecond);
// Round the time to something that fits the current zoom level
if (ProfileEditorService.PixelsPerSecond < 200)
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 5.0) * 5.0);
else if (ProfileEditorService.PixelsPerSecond < 500)
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 2.0) * 2.0);
else
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds));
ProfileEditorService.CurrentTime = newTime;
}
#endregion
}
}
@@ -1,183 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Artemis.Core;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline;
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree;
using Artemis.UI.Shared.Services;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties
{
public sealed class LayerPropertyGroupViewModel : Conductor<Screen>.Collection.AllActive
{
private readonly IProfileEditorService _profileEditorService;
private readonly ILayerPropertyVmFactory _layerPropertyVmFactory;
private bool _isVisible;
private TreeGroupViewModel _treeGroupViewModel;
private TimelineGroupViewModel _timelineGroupViewModel;
private bool _hasChildren;
public LayerPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, IProfileEditorService profileEditorService, ILayerPropertyVmFactory layerPropertyVmFactory)
{
_profileEditorService = profileEditorService;
_layerPropertyVmFactory = layerPropertyVmFactory;
LayerPropertyGroup = layerPropertyGroup;
IsVisible = !LayerPropertyGroup.IsHidden;
TreeGroupViewModel = _layerPropertyVmFactory.TreeGroupViewModel(this);
TreeGroupViewModel.ConductWith(this);
TimelineGroupViewModel = _layerPropertyVmFactory.TimelineGroupViewModel(this);
TimelineGroupViewModel.ConductWith(this);
}
public LayerPropertyGroup LayerPropertyGroup { get; }
public TreeGroupViewModel TreeGroupViewModel
{
get => _treeGroupViewModel;
set => SetAndNotify(ref _treeGroupViewModel, value);
}
public TimelineGroupViewModel TimelineGroupViewModel
{
get => _timelineGroupViewModel;
set => SetAndNotify(ref _timelineGroupViewModel, value);
}
public bool IsVisible
{
get => _isVisible;
set => SetAndNotify(ref _isVisible, value);
}
public bool IsExpanded
{
get => LayerPropertyGroup.ProfileElement.IsPropertyGroupExpanded(LayerPropertyGroup);
set
{
LayerPropertyGroup.ProfileElement.SetPropertyGroupExpanded(LayerPropertyGroup, value);
NotifyOfPropertyChange(nameof(IsExpanded));
}
}
public bool HasChildren
{
get => _hasChildren;
set => SetAndNotify(ref _hasChildren, value);
}
protected override void OnInitialActivate()
{
LayerPropertyGroup.VisibilityChanged += LayerPropertyGroupOnVisibilityChanged;
PopulateChildren();
base.OnInitialActivate();
}
protected override void OnClose()
{
LayerPropertyGroup.VisibilityChanged -= LayerPropertyGroupOnVisibilityChanged;
base.OnClose();
}
public void UpdateOrder(int order)
{
// if (LayerPropertyGroup.LayerEffect != null)
// LayerPropertyGroup.LayerEffect.Order = order;
NotifyOfPropertyChange(nameof(IsExpanded));
}
public List<ITimelineKeyframeViewModel> GetAllKeyframeViewModels(bool expandedOnly)
{
List<ITimelineKeyframeViewModel> result = new();
if (expandedOnly && !IsExpanded)
return result;
foreach (Screen child in Items)
{
if (child is LayerPropertyViewModel layerPropertyViewModel)
result.AddRange(layerPropertyViewModel.TimelinePropertyViewModel.GetAllKeyframeViewModels());
else if (child is LayerPropertyGroupViewModel layerPropertyGroupViewModel)
result.AddRange(layerPropertyGroupViewModel.GetAllKeyframeViewModels(expandedOnly));
}
return result;
}
/// <summary>
/// Removes the keyframes between the <paramref name="start" /> and <paramref name="end" /> position from this property
/// group
/// </summary>
/// <param name="start">The position at which to start removing keyframes, if null this will start at the first keyframe</param>
/// <param name="end">The position at which to start removing keyframes, if null this will end at the last keyframe</param>
public void WipeKeyframes(TimeSpan? start, TimeSpan? end)
{
foreach (Screen item in Items)
{
if (item is LayerPropertyViewModel layerPropertyViewModel)
layerPropertyViewModel.TimelinePropertyViewModel.WipeKeyframes(start, end);
else if (item is LayerPropertyGroupViewModel layerPropertyGroupViewModel)
layerPropertyGroupViewModel.WipeKeyframes(start, end);
}
TimelineGroupViewModel.UpdateKeyframePositions();
}
/// <summary>
/// Shifts the keyframes between the <paramref name="start" /> and <paramref name="end" /> position by the provided
/// <paramref name="amount" />
/// </summary>
/// <param name="start">The position at which to start shifting keyframes, if null this will start at the first keyframe</param>
/// <param name="end">The position at which to start shifting keyframes, if null this will end at the last keyframe</param>
/// <param name="amount">The amount to shift the keyframes for</param>
public void ShiftKeyframes(TimeSpan? start, TimeSpan? end, TimeSpan amount)
{
foreach (Screen item in Items)
{
if (item is LayerPropertyViewModel layerPropertyViewModel)
layerPropertyViewModel.TimelinePropertyViewModel.ShiftKeyframes(start, end, amount);
else if (item is LayerPropertyGroupViewModel layerPropertyGroupViewModel)
layerPropertyGroupViewModel.ShiftKeyframes(start, end, amount);
}
TimelineGroupViewModel.UpdateKeyframePositions();
}
private void LayerPropertyGroupOnVisibilityChanged(object sender, EventArgs e)
{
IsVisible = !LayerPropertyGroup.IsHidden;
}
private void PopulateChildren()
{
// 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
foreach (PropertyInfo propertyInfo in LayerPropertyGroup.GetType().GetProperties())
{
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 && _profileEditorService.CanCreatePropertyInputViewModel(value))
Items.Add(_layerPropertyVmFactory.LayerPropertyViewModel(value));
}
else if (typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType))
{
LayerPropertyGroup value = (LayerPropertyGroup) propertyInfo.GetValue(LayerPropertyGroup);
if (value != null)
Items.Add(_layerPropertyVmFactory.LayerPropertyGroupViewModel(value));
}
}
HasChildren = Items.Any(i => i is LayerPropertyViewModel {IsVisible: true} || i is LayerPropertyGroupViewModel {IsVisible: true});
}
}
}
@@ -1,60 +0,0 @@
using Artemis.Core;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline;
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties
{
public sealed class LayerPropertyViewModel : Screen
{
private bool _isExpanded;
private bool _isHighlighted;
private bool _isVisible;
private ITimelinePropertyViewModel _timelinePropertyViewModel;
private ITreePropertyViewModel _treePropertyViewModel;
public LayerPropertyViewModel(ILayerProperty layerProperty, IPropertyVmFactory propertyVmFactory)
{
LayerProperty = layerProperty;
TreePropertyViewModel = propertyVmFactory.TreePropertyViewModel(LayerProperty, this);
TreePropertyViewModel.ConductWith(this);
TimelinePropertyViewModel = propertyVmFactory.TimelinePropertyViewModel(LayerProperty, this);
TimelinePropertyViewModel.ConductWith(this);
}
public ILayerProperty LayerProperty { get; }
public ITreePropertyViewModel TreePropertyViewModel
{
get => _treePropertyViewModel;
set => SetAndNotify(ref _treePropertyViewModel, value);
}
public ITimelinePropertyViewModel TimelinePropertyViewModel
{
get => _timelinePropertyViewModel;
set => SetAndNotify(ref _timelinePropertyViewModel, value);
}
public bool IsVisible
{
get => _isVisible;
set => SetAndNotify(ref _isVisible, value);
}
public bool IsHighlighted
{
get => _isHighlighted;
set => SetAndNotify(ref _isHighlighted, value);
}
public bool IsExpanded
{
get => _isExpanded;
set => SetAndNotify(ref _isExpanded, value);
}
}
}
@@ -1,191 +0,0 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.Controls
{
public class PropertyTimelineHeader : FrameworkElement
{
public static readonly DependencyProperty ForegroundProperty = DependencyProperty.Register(nameof(Foreground), typeof(Brush), typeof(PropertyTimelineHeader),
new FrameworkPropertyMetadata(default(Brush), FrameworkPropertyMetadataOptions.AffectsRender));
public static readonly DependencyProperty BackgroundProperty = DependencyProperty.Register(nameof(Background), typeof(Brush), typeof(PropertyTimelineHeader),
new FrameworkPropertyMetadata(default(Brush), FrameworkPropertyMetadataOptions.AffectsRender));
public static readonly DependencyProperty FontFamilyProperty = DependencyProperty.Register(nameof(FontFamily), typeof(FontFamily), typeof(PropertyTimelineHeader),
new FrameworkPropertyMetadata(TextBlock.FontFamilyProperty.DefaultMetadata.DefaultValue, FrameworkPropertyMetadataOptions.AffectsRender));
public static readonly DependencyProperty PixelsPerSecondProperty = DependencyProperty.Register(nameof(PixelsPerSecond), typeof(int), typeof(PropertyTimelineHeader),
new FrameworkPropertyMetadata(default(int), FrameworkPropertyMetadataOptions.AffectsRender));
public static readonly DependencyProperty HorizontalOffsetProperty = DependencyProperty.Register(nameof(HorizontalOffset), typeof(double), typeof(PropertyTimelineHeader),
new FrameworkPropertyMetadata(default(double), FrameworkPropertyMetadataOptions.AffectsRender));
public static readonly DependencyProperty VisibleWidthProperty = DependencyProperty.Register(nameof(VisibleWidth), typeof(double), typeof(PropertyTimelineHeader),
new FrameworkPropertyMetadata(default(double), FrameworkPropertyMetadataOptions.AffectsRender));
public static readonly DependencyProperty OffsetFirstValueProperty = DependencyProperty.Register(nameof(OffsetFirstValue), typeof(bool), typeof(PropertyTimelineHeader),
new FrameworkPropertyMetadata(default(bool), FrameworkPropertyMetadataOptions.AffectsRender));
private double _subd1;
private double _subd2;
private double _subd3;
public Brush Foreground
{
get => (Brush) GetValue(ForegroundProperty);
set => SetValue(ForegroundProperty, value);
}
public Brush Background
{
get => (Brush) GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
public FontFamily FontFamily
{
get => (FontFamily) GetValue(FontFamilyProperty);
set => SetValue(FontFamilyProperty, value);
}
public int PixelsPerSecond
{
get => (int) GetValue(PixelsPerSecondProperty);
set => SetValue(PixelsPerSecondProperty, value);
}
public double HorizontalOffset
{
get => (double) GetValue(HorizontalOffsetProperty);
set => SetValue(HorizontalOffsetProperty, value);
}
public double VisibleWidth
{
get => (double) GetValue(VisibleWidthProperty);
set => SetValue(VisibleWidthProperty, value);
}
public bool OffsetFirstValue
{
get => (bool) GetValue(OffsetFirstValueProperty);
set => SetValue(OffsetFirstValueProperty, value);
}
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
UpdateTimeScale();
if (Background != null)
drawingContext.DrawRectangle(Background, null, new Rect(0,0, ActualWidth, 30));
Pen linePen = new(Foreground, 1);
double width = HorizontalOffset + VisibleWidth;
int frameStart = 0;
double units = PixelsPerSecond / _subd1;
double offsetUnits = frameStart * PixelsPerSecond % units;
// Labels
double count = (width + offsetUnits) / units;
for (int i = 0; i < count; i++)
{
double x = i * units - offsetUnits;
// Add a 100px margin to allow the text to partially render when needed
if (x < HorizontalOffset - 100 || x > HorizontalOffset + width)
continue;
TimeSpan t = TimeSpan.FromSeconds((i * units - offsetUnits) / PixelsPerSecond + frameStart);
// 0.00 is always formatted as 0.00
if (t == TimeSpan.Zero)
RenderLabel(drawingContext, "0.00", x);
else if (PixelsPerSecond > 200)
RenderLabel(drawingContext, $"{Math.Floor(t.TotalSeconds):00}.{t.Milliseconds:000}", x);
else if (PixelsPerSecond > 60)
RenderLabel(drawingContext, $"{Math.Floor(t.TotalSeconds):00}.{t.Milliseconds:000}", x);
else
RenderLabel(drawingContext, $"{Math.Floor(t.TotalMinutes):0}:{t.Seconds:00}", x);
}
// Large ticks
units = PixelsPerSecond / _subd2;
count = (width + offsetUnits) / units;
for (int i = 0; i < count; i++)
{
double x = i * units - offsetUnits;
if (x == 0 && OffsetFirstValue)
drawingContext.DrawLine(linePen, new Point(1, 20), new Point(1, 30));
else if (x > HorizontalOffset && x < HorizontalOffset + width)
drawingContext.DrawLine(linePen, new Point(x, 20), new Point(x, 30));
}
// Small ticks
double mul = _subd3 / _subd2;
units = PixelsPerSecond / _subd3;
count = (width + offsetUnits) / units;
for (int i = 0; i < count; i++)
{
if (Math.Abs(i % mul) < 0.001) continue;
double x = i * units - offsetUnits;
if (x > HorizontalOffset && x < HorizontalOffset + width)
drawingContext.DrawLine(linePen, new Point(x, 25), new Point(x, 30));
}
}
private void RenderLabel(DrawingContext drawingContext, string text, double x)
{
Typeface typeFace = new(FontFamily, new FontStyle(), new FontWeight(), new FontStretch());
FormattedText formattedText = new(text, CultureInfo.CurrentUICulture, FlowDirection.LeftToRight, typeFace, 9, Foreground, null, VisualTreeHelper.GetDpi(this).PixelsPerDip);
if (x == 0 && OffsetFirstValue)
drawingContext.DrawText(formattedText, new Point(2, 5));
else
drawingContext.DrawText(formattedText, new Point(x - formattedText.Width / 2, 5));
}
private void UpdateTimeScale()
{
object[] subds;
if (PixelsPerSecond > 350)
subds = new object[] {12d, 12d, 60d};
else if (PixelsPerSecond > 250)
subds = new object[] {6d, 12d, 60d};
else if (PixelsPerSecond > 200)
subds = new object[] {6d, 6d, 30d};
else if (PixelsPerSecond > 150)
subds = new object[] {4d, 4d, 20d};
else if (PixelsPerSecond > 140)
subds = new object[] {4d, 4d, 20d};
else if (PixelsPerSecond > 90)
subds = new object[] {2d, 4d, 20d};
else if (PixelsPerSecond > 60)
subds = new object[] {2d, 4d, 8d};
else if (PixelsPerSecond > 40)
subds = new object[] {1d, 2d, 10d};
else if (PixelsPerSecond > 30)
subds = new object[] {1d, 2d, 10d};
else if (PixelsPerSecond > 10)
subds = new object[] {1d / 2d, 1d / 2d, 1d / 2d};
else if (PixelsPerSecond > 4)
subds = new object[] {1d / 5d, 1d / 5d, 1d / 5d};
else if (PixelsPerSecond > 3)
subds = new object[] {1d / 10d, 1d / 10d, 1d / 5d};
else if (PixelsPerSecond > 1)
subds = new object[] {1d / 20d, 1d / 20d, 1d / 10d};
else if (PixelsPerSecond >= 1)
subds = new object[] {1d / 30d, 1d / 30d, 1d / 15d};
else
{
// 1s per pixel
subds = new object[] {1d / 60d, 1d / 60d, 1d / 15d};
}
_subd1 = (double) subds[0]; // big ticks / labels
_subd2 = (double) subds[1]; // medium ticks
_subd3 = (double) subds[2]; // small ticks
}
}
}
@@ -1,42 +0,0 @@
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.Dialogs.TimelineSegmentDialogView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.Dialogs"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:TimelineSegmentDialogViewModel}">
<StackPanel Margin="16" Width="240">
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}">
<Run Text="Update " /><Run Text="{Binding Segment.Segment, Mode=OneWay}" /><Run Text=" segment" />
</TextBlock>
<TextBox materialDesign:HintAssist.Hint="Segment length"
materialDesign:TextFieldAssist.SuffixText="seconds"
Margin="0 16"
Text="{Binding InputValue, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource MaterialDesignFilledTextBox}" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Style="{StaticResource MaterialDesignFlatButton}" IsCancel="True" Margin="0 8 8 0" Command="{s:Action Cancel}">
<Button.CommandParameter>
<system:Boolean xmlns:system="clr-namespace:System;assembly=mscorlib">
False
</system:Boolean>
</Button.CommandParameter>
CANCEL
</Button>
<Button Style="{StaticResource MaterialDesignFlatButton}" IsDefault="True" Margin="0 8 0 0" Command="{s:Action Accept}">
<Button.CommandParameter>
<system:Boolean xmlns:system="clr-namespace:System;assembly=mscorlib">
True
</system:Boolean>
</Button.CommandParameter>
ACCEPT
</Button>
</StackPanel>
</StackPanel>
</UserControl>
@@ -1,90 +0,0 @@
using System;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Artemis.UI.Shared.Services;
using FluentValidation;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.Dialogs
{
public class TimelineSegmentDialogViewModel : DialogViewModelBase
{
private string _inputValue;
public TimelineSegmentDialogViewModel(IModelValidator<TimelineSegmentDialogViewModel> validator, TimelineSegmentViewModel segment)
: base(validator)
{
Segment = segment;
InputValue = $"{Math.Floor(Segment.SegmentLength.TotalSeconds):00}.{Segment.SegmentLength.Milliseconds:000}";
}
public TimelineSegmentViewModel Segment { get; }
public string InputValue
{
get => _inputValue;
set => SetAndNotify(ref _inputValue, value);
}
public async Task Accept()
{
await ValidateAsync();
if (HasErrors)
return;
Segment.UpdateLength(TimelineSegmentDialogViewModelValidator.CreateTime(InputValue));
Session.Close();
}
}
public class TimelineSegmentDialogViewModelValidator : AbstractValidator<TimelineSegmentDialogViewModel>
{
private readonly Regex _inputRegex = new("^[.][-|0-9]+$|^-?[0-9]*[.]{0,1}[0-9]*$");
public TimelineSegmentDialogViewModelValidator()
{
CascadeMode = CascadeMode.Stop;
RuleFor(m => m.InputValue)
.NotNull()
.WithMessage("A timeline length is required");
RuleFor(m => m.InputValue)
.Must(ValidateTime)
.WithMessage("Input cannot be converted to a time");
Transform(m => m.InputValue, CreateTime)
.GreaterThanOrEqualTo(TimeSpan.FromMilliseconds(100))
.WithMessage("Minimum timeline length is 100ms");
Transform(m => m.InputValue, CreateTime)
.LessThanOrEqualTo(TimeSpan.FromHours(24))
.WithMessage("Maximum timeline length is 24 hours");
}
public static TimeSpan CreateTime(string s)
{
string[] parts = s.Split(".");
// Only seconds provided
if (parts.Length == 1)
return TimeSpan.FromSeconds(double.Parse(parts[0]));
// Only milliseconds provided with a leading .
if (string.IsNullOrEmpty(parts[0]))
{
// Add trailing zeros so 2.5 becomes 2.500, can't seem to make double.Parse do that
while (parts[0].Length < 3) parts[0] += "0";
return TimeSpan.FromMilliseconds(double.Parse(parts[1], CultureInfo.InvariantCulture));
}
// Seconds and milliseconds provided
// Add trailing zeros so 2.5 becomes 2.500, can't seem to make double.Parse do that
while (parts[1].Length < 3) parts[1] += "0";
return TimeSpan.FromSeconds(double.Parse(parts[0])).Add(TimeSpan.FromMilliseconds(double.Parse(parts[1])));
}
private bool ValidateTime(string arg)
{
return _inputRegex.IsMatch(arg);
}
}
}
@@ -1,86 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Artemis.Core;
using Artemis.Storage.Entities.Profile;
using Artemis.UI.Exceptions;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.Models
{
public class KeyframesClipboardModel
{
// ReSharper disable once UnusedMember.Global - For JSON.NET
public KeyframesClipboardModel()
{
ClipboardModels = new List<KeyframeClipboardModel>();
}
public KeyframesClipboardModel(IEnumerable<ILayerPropertyKeyframe> keyframes)
{
ClipboardModels = new List<KeyframeClipboardModel>();
foreach (ILayerPropertyKeyframe keyframe in keyframes.OrderBy(k => k.Position))
ClipboardModels.Add(new KeyframeClipboardModel(keyframe));
}
public List<KeyframeClipboardModel> ClipboardModels { get; set; }
public bool HasBeenPasted { get; set; }
public List<ILayerPropertyKeyframe> Paste(RenderProfileElement target, TimeSpan pastePosition)
{
if (target == null) throw new ArgumentNullException(nameof(target));
if (HasBeenPasted)
throw new ArtemisUIException("Clipboard model can only be pasted once");
List<ILayerPropertyKeyframe> results = new();
if (!ClipboardModels.Any())
return results;
// Determine the offset by looking at the position of the first keyframe, start pasting from there
TimeSpan offset = pastePosition - ClipboardModels.First().KeyframeEntity.Position;
List<ILayerProperty> properties = target.GetAllLayerProperties();
foreach (KeyframeClipboardModel clipboardModel in ClipboardModels)
{
ILayerPropertyKeyframe layerPropertyKeyframe = clipboardModel.Paste(properties, offset);
if (layerPropertyKeyframe != null)
results.Add(layerPropertyKeyframe);
}
HasBeenPasted = true;
return results;
}
}
public class KeyframeClipboardModel
{
// ReSharper disable once UnusedMember.Global - For JSON.NET
public KeyframeClipboardModel()
{
}
public KeyframeClipboardModel(ILayerPropertyKeyframe layerPropertyKeyframe)
{
// FeatureId = layerPropertyKeyframe.UntypedLayerProperty.LayerPropertyGroup.Feature.Id;
Path = layerPropertyKeyframe.UntypedLayerProperty.Path;
KeyframeEntity = layerPropertyKeyframe.GetKeyframeEntity();
}
public string FeatureId { get; set; }
public string Path { get; set; }
public KeyframeEntity KeyframeEntity { get; set; }
public ILayerPropertyKeyframe Paste(List<ILayerProperty> properties, TimeSpan offset)
{
// ILayerProperty property = properties.FirstOrDefault(p => p.LayerPropertyGroup.Feature.Id == FeatureId && p.Path == Path);
// if (property != null)
// {
// KeyframeEntity.Position += offset;
// ILayerPropertyKeyframe keyframe = property.AddKeyframeEntity(KeyframeEntity);
// KeyframeEntity.Position -= offset;
//
// return keyframe;
// }
return null;
}
}
}
@@ -1,51 +0,0 @@
using System;
using System.Windows;
using System.Windows.Media;
using Artemis.Core;
using Humanizer;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
{
public class TimelineEasingViewModel : PropertyChangedBase
{
private bool _isEasingModeSelected;
public TimelineEasingViewModel(Easings.Functions easingFunction, bool isSelected)
{
_isEasingModeSelected = isSelected;
EasingFunction = easingFunction;
Description = easingFunction.Humanize();
EasingPoints = new PointCollection();
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 PointCollection EasingPoints { get; }
public string Description { get; }
public bool IsEasingModeSelected
{
get => _isEasingModeSelected;
set
{
_isEasingModeSelected = value;
if (value) OnEasingModeSelected();
}
}
public event EventHandler EasingModeSelected;
protected virtual void OnEasingModeSelected()
{
EasingModeSelected?.Invoke(this, EventArgs.Empty);
}
}
}
@@ -1,77 +0,0 @@
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.TimelineGroupView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:layerProperties="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:TimelineGroupViewModel}"
Visibility="{Binding LayerPropertyGroupViewModel.IsVisible, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="24" />
<RowDefinition Height="1" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ItemsControl Grid.Row="0"
Height="24"
Visibility="{Binding LayerPropertyGroupViewModel.IsExpanded, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}"
ItemsSource="{Binding KeyframePositions}"
Background="{DynamicResource MaterialDesignToolBarBackground}"
HorizontalAlignment="Stretch">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="{x:Type ContentPresenter}">
<Setter Property="Canvas.Left" Value="{Binding}" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Fill="{StaticResource MaterialDesignCheckBoxDisabled}"
Stroke="White"
StrokeThickness="0"
Width="10"
Height="10"
Margin="-5,6,0,0" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Rectangle Grid.Row="1" HorizontalAlignment="Stretch" Fill="{DynamicResource MaterialDesignDivider}" Height="1" />
<ItemsControl Grid.Row="2"
ItemsSource="{Binding LayerPropertyGroupViewModel.Items}"
Visibility="{Binding LayerPropertyGroupViewModel.IsExpanded, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch">
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type layerProperties:LayerPropertyGroupViewModel}">
<ContentControl s:View.Model="{Binding TimelineGroupViewModel}"
IsTabStop="False"
HorizontalAlignment="Stretch"
Visibility="{Binding IsVisible, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type layerProperties:LayerPropertyViewModel}">
<ContentControl s:View.Model="{Binding TimelinePropertyViewModel}"
IsTabStop="False"
HorizontalAlignment="Stretch"
Visibility="{Binding IsVisible, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}" />
</DataTemplate>
</ItemsControl.Resources>
</ItemsControl>
</Grid>
</UserControl>
@@ -1,73 +0,0 @@
using System;
using System.ComponentModel;
using System.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Services;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
{
public sealed class TimelineGroupViewModel : Screen
{
private readonly IProfileEditorService _profileEditorService;
public TimelineGroupViewModel(LayerPropertyGroupViewModel layerPropertyGroupViewModel, IProfileEditorService profileEditorService)
{
_profileEditorService = profileEditorService;
LayerPropertyGroupViewModel = layerPropertyGroupViewModel;
LayerPropertyGroup = LayerPropertyGroupViewModel.LayerPropertyGroup;
KeyframePositions = new BindableCollection<double>();
UpdateKeyframePositions();
}
public LayerPropertyGroupViewModel LayerPropertyGroupViewModel { get; }
public LayerPropertyGroup LayerPropertyGroup { get; }
public BindableCollection<double> KeyframePositions { get; }
public void UpdateKeyframePositions()
{
KeyframePositions.Clear();
KeyframePositions.AddRange(LayerPropertyGroupViewModel
.GetAllKeyframeViewModels(false)
.Select(p => p.Position.TotalSeconds * _profileEditorService.PixelsPerSecond));
}
#region Overrides of Screen
/// <inheritdoc />
protected override void OnInitialActivate()
{
_profileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged;
LayerPropertyGroupViewModel.PropertyChanged += LayerPropertyGroupViewModelOnPropertyChanged;
base.OnInitialActivate();
}
/// <inheritdoc />
protected override void OnClose()
{
_profileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged;
LayerPropertyGroupViewModel.PropertyChanged -= LayerPropertyGroupViewModelOnPropertyChanged;
base.OnClose();
}
#endregion
#region Event handlers
private void LayerPropertyGroupViewModelOnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(LayerPropertyGroupViewModel.IsExpanded))
UpdateKeyframePositions();
}
private void ProfileEditorServiceOnPixelsPerSecondChanged(object sender, EventArgs e)
{
UpdateKeyframePositions();
}
#endregion
}
}
@@ -1,206 +0,0 @@
using System;
using System.ComponentModel;
using System.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Services;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
{
public sealed class TimelineKeyframeViewModel<T> : Screen, ITimelineKeyframeViewModel
{
private readonly IProfileEditorService _profileEditorService;
private bool _isSelected;
private string _timestamp;
private double _x;
public TimelineKeyframeViewModel(LayerPropertyKeyframe<T> layerPropertyKeyframe, IProfileEditorService profileEditorService)
{
_profileEditorService = profileEditorService;
LayerPropertyKeyframe = layerPropertyKeyframe;
EasingViewModels = new BindableCollection<TimelineEasingViewModel>();
}
public LayerPropertyKeyframe<T> LayerPropertyKeyframe { get; }
public BindableCollection<TimelineEasingViewModel> EasingViewModels { get; }
public double X
{
get => _x;
set => SetAndNotify(ref _x, value);
}
public string Timestamp
{
get => _timestamp;
set => SetAndNotify(ref _timestamp, value);
}
public bool IsSelected
{
get => _isSelected;
set => SetAndNotify(ref _isSelected, value);
}
public TimeSpan Position => LayerPropertyKeyframe.Position;
public ILayerPropertyKeyframe Keyframe => LayerPropertyKeyframe;
#region Overrides of Screen
protected override void OnInitialActivate()
{
_profileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged;
LayerPropertyKeyframe.PropertyChanged += LayerPropertyKeyframeOnPropertyChanged;
base.OnInitialActivate();
}
protected override void OnClose()
{
_profileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged;
LayerPropertyKeyframe.PropertyChanged -= LayerPropertyKeyframeOnPropertyChanged;
foreach (TimelineEasingViewModel timelineEasingViewModel in EasingViewModels)
timelineEasingViewModel.EasingModeSelected -= TimelineEasingViewModelOnEasingModeSelected;
base.OnClose();
}
#endregion
public void Update()
{
X = _profileEditorService.PixelsPerSecond * LayerPropertyKeyframe.Position.TotalSeconds;
Timestamp = $"{Math.Floor(LayerPropertyKeyframe.Position.TotalSeconds):00}.{LayerPropertyKeyframe.Position.Milliseconds:000}";
}
private void ProfileEditorServiceOnPixelsPerSecondChanged(object sender, EventArgs e)
{
Update();
}
private void LayerPropertyKeyframeOnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(LayerPropertyKeyframe.Position))
Update();
}
#region Easing
public void PopulateEasingViewModels()
{
if (EasingViewModels.Any())
return;
EasingViewModels.AddRange(Enum.GetValues(typeof(Easings.Functions))
.Cast<Easings.Functions>()
.Select(e => new TimelineEasingViewModel(e, e == LayerPropertyKeyframe.EasingFunction)));
foreach (TimelineEasingViewModel timelineEasingViewModel in EasingViewModels)
timelineEasingViewModel.EasingModeSelected += TimelineEasingViewModelOnEasingModeSelected;
}
public void ClearEasingViewModels()
{
EasingViewModels.Clear();
}
private void TimelineEasingViewModelOnEasingModeSelected(object sender, EventArgs e)
{
SelectEasingMode((TimelineEasingViewModel) sender);
}
public void SelectEasingMode(TimelineEasingViewModel easingViewModel)
{
LayerPropertyKeyframe.EasingFunction = easingViewModel.EasingFunction;
// Set every selection to false except on the VM that made the change
foreach (TimelineEasingViewModel propertyTrackEasingViewModel in EasingViewModels.Where(vm => vm != easingViewModel))
propertyTrackEasingViewModel.IsEasingModeSelected = false;
_profileEditorService.SaveSelectedProfileElement();
}
#endregion
#region Movement
private TimeSpan? _offset;
public void ReleaseMovement()
{
_offset = null;
}
public void SaveOffsetToKeyframe(ITimelineKeyframeViewModel source)
{
if (source == this)
{
_offset = null;
return;
}
if (_offset != null)
return;
_offset = LayerPropertyKeyframe.Position - source.Position;
}
public void ApplyOffsetToKeyframe(ITimelineKeyframeViewModel source)
{
if (source == this || _offset == null)
return;
UpdatePosition(source.Position + _offset.Value);
}
public void UpdatePosition(TimeSpan position)
{
if (position < TimeSpan.Zero)
LayerPropertyKeyframe.Position = TimeSpan.Zero;
else if (position > _profileEditorService.SelectedProfileElement.Timeline.Length)
LayerPropertyKeyframe.Position = _profileEditorService.SelectedProfileElement.Timeline.Length;
else
LayerPropertyKeyframe.Position = position;
Update();
}
#endregion
#region Context menu actions
public void Delete(bool save = true)
{
LayerPropertyKeyframe.LayerProperty.RemoveKeyframe(LayerPropertyKeyframe);
if (save)
_profileEditorService.SaveSelectedProfileElement();
}
#endregion
}
public interface ITimelineKeyframeViewModel : IScreen
{
bool IsSelected { get; set; }
TimeSpan Position { get; }
ILayerPropertyKeyframe Keyframe { get; }
#region Movement
void SaveOffsetToKeyframe(ITimelineKeyframeViewModel source);
void ApplyOffsetToKeyframe(ITimelineKeyframeViewModel source);
void UpdatePosition(TimeSpan position);
void ReleaseMovement();
#endregion
#region Context menu actions
void PopulateEasingViewModels();
void ClearEasingViewModels();
void Delete(bool save = true);
#endregion
}
}
@@ -1,117 +0,0 @@
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.TimelinePropertyView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:timeline="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
HorizontalAlignment="Stretch">
<Border Height="25" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource MaterialDesignDivider}">
<ItemsControl ItemsSource="{Binding Items}" Background="{DynamicResource MaterialDesignToolBarBackground}" HorizontalAlignment="Left">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="{x:Type ContentPresenter}">
<Setter Property="Canvas.Left" Value="{Binding X}" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Fill="{StaticResource PrimaryHueMidBrush}"
Stroke="White"
StrokeThickness="0"
Width="10"
Height="10"
Margin="-5,6,0,0"
ToolTip="{Binding Timestamp}"
s:View.ActionTarget="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type timeline:TimelineView}}, Path=DataContext}"
MouseDown="{s:Action KeyframeMouseDown}"
MouseUp="{s:Action KeyframeMouseUp}"
MouseMove="{s:Action KeyframeMouseMove}"
ContextMenuOpening="{s:Action KeyframeContextMenuOpening}"
ContextMenuClosing="{s:Action KeyframeContextMenuClosing}">
<Ellipse.Style>
<Style TargetType="{x:Type Ellipse}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="StrokeThickness" To="1" Duration="0:0:0.25" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="StrokeThickness" To="0" Duration="0:0:0.25" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
<Ellipse.ContextMenu>
<ContextMenu>
<MenuItem Header="Easing" ItemsSource="{Binding EasingViewModels}">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="Creation" />
</MenuItem.Icon>
<MenuItem.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}" BasedOn="{StaticResource MaterialDesignMenuItem}">
<Setter Property="IsCheckable" Value="True" />
<Setter Property="IsChecked" Value="{Binding Path=IsEasingModeSelected, Mode=TwoWay}" />
</Style>
</MenuItem.ItemContainerStyle>
<MenuItem.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Polyline Stroke="{DynamicResource MaterialDesignBody}"
StrokeThickness="1"
Points="{Binding EasingPoints}"
Stretch="Uniform"
Width="20"
Height="20"
Margin="0 0 10 0" />
<TextBlock Text="{Binding Description}" />
</StackPanel>
</DataTemplate>
</MenuItem.ItemTemplate>
</MenuItem>
<Separator />
<MenuItem Header="Duplicate" Command="{s:Action DuplicateKeyframes}" CommandParameter="{Binding}" InputGestureText="Ctrl+D">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="ContentDuplicate" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Copy" Command="{s:Action CopyKeyframes}" CommandParameter="{Binding}" InputGestureText="Ctrl+C">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="ContentCopy" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Paste" Command="{s:Action PasteKeyframes}" CommandParameter="{Binding}" InputGestureText="Ctrl+V">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="ContentPaste" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Delete" Command="{s:Action DeleteKeyframes}" InputGestureText="Del">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="Delete" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</Ellipse.ContextMenu>
</Ellipse>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
</UserControl>
@@ -1,122 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Services;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
{
public sealed class TimelinePropertyViewModel<T> : Conductor<TimelineKeyframeViewModel<T>>.Collection.AllActive, ITimelinePropertyViewModel
{
private readonly IProfileEditorService _profileEditorService;
private double _width;
public TimelinePropertyViewModel(LayerProperty<T> layerProperty, LayerPropertyViewModel layerPropertyViewModel, IProfileEditorService profileEditorService)
{
_profileEditorService = profileEditorService;
LayerProperty = layerProperty;
LayerPropertyViewModel = layerPropertyViewModel;
}
public LayerProperty<T> LayerProperty { get; }
public LayerPropertyViewModel LayerPropertyViewModel { get; }
public double Width
{
get => _width;
set => SetAndNotify(ref _width, value);
}
public List<ITimelineKeyframeViewModel> GetAllKeyframeViewModels()
{
return Items.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();
}
protected override void OnClose()
{
LayerProperty.KeyframesToggled -= LayerPropertyOnKeyframesToggled;
LayerProperty.KeyframeAdded -= LayerPropertyOnKeyframeAdded;
LayerProperty.KeyframeRemoved -= LayerPropertyOnKeyframeRemoved;
base.OnClose();
}
protected override void OnInitialActivate()
{
LayerProperty.KeyframesToggled += LayerPropertyOnKeyframesToggled;
LayerProperty.KeyframeAdded += LayerPropertyOnKeyframeAdded;
LayerProperty.KeyframeRemoved += LayerPropertyOnKeyframeRemoved;
UpdateKeyframes();
base.OnInitialActivate();
}
private void LayerPropertyOnKeyframesToggled(object sender, LayerPropertyEventArgs e)
{
UpdateKeyframes();
}
private void LayerPropertyOnKeyframeRemoved(object sender, LayerPropertyEventArgs e)
{
UpdateKeyframes();
}
private void LayerPropertyOnKeyframeAdded(object sender, LayerPropertyEventArgs e)
{
UpdateKeyframes();
}
private void UpdateKeyframes()
{
NotifyOfPropertyChange(nameof(LayerProperty.KeyframesEnabled));
// Only show keyframes if they are enabled
if (LayerProperty.KeyframesEnabled)
{
List<LayerPropertyKeyframe<T>> keyframes = LayerProperty.Keyframes.ToList();
Items.RemoveRange(Items.Where(t => !keyframes.Contains(t.LayerPropertyKeyframe)).ToList());
Items.AddRange(
keyframes.Where(k => Items.All(t => t.LayerPropertyKeyframe != k)).Select(k => new TimelineKeyframeViewModel<T>(k, _profileEditorService))
);
}
else
Items.Clear();
foreach (TimelineKeyframeViewModel<T> timelineKeyframeViewModel in Items)
timelineKeyframeViewModel.Update();
}
}
public interface ITimelinePropertyViewModel : IScreen
{
List<ITimelineKeyframeViewModel> GetAllKeyframeViewModels();
void WipeKeyframes(TimeSpan? start, TimeSpan? end);
void ShiftKeyframes(TimeSpan? start, TimeSpan? end, TimeSpan amount);
}
}
@@ -1,169 +0,0 @@
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.TimelineSegmentView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:utilities="clr-namespace:Artemis.UI.Utilities"
xmlns:timeline="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance {x:Type timeline:TimelineSegmentViewModel}}">
<UserControl.Resources>
<utilities:BindingProxy x:Key="DataContextProxy" Data="{Binding}" />
</UserControl.Resources>
<StackPanel Orientation="Horizontal" Visibility="{Binding SegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
<StackPanel.ContextMenu>
<ContextMenu>
<MenuItem Header="Open segment settings" Command="{s:Action OpenSettingsDialog}">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="Cog" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Repeat main segment"
IsCheckable="True"
IsChecked="{Binding Data.RepeatSegment, Source={StaticResource DataContextProxy}}"
Visibility="{Binding Data.IsMainSegment, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Source={StaticResource DataContextProxy}}" />
<MenuItem Header="Disable segment" Command="{s:Action DisableSegment}">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="Close" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</StackPanel.ContextMenu>
<!-- Segment content -->
<Grid VerticalAlignment="Top" Background="{StaticResource MaterialDesignPaper}" Width="{Binding SegmentWidth}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock TextAlignment="Center"
Padding="0 3"
Margin="5 0 0 0"
FontSize="12"
ToolTip="{Binding ToolTip}"
Text="{Binding Segment}"
ClipToBounds="False">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding ShowSegmentName}" Value="False">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="1.0" To="0.0" Duration="0:0:0.25">
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode="EaseInOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="0.0" To="1.0" Duration="0:0:0.25" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<ToggleButton Grid.Column="1"
ToolTip="Toggle main segment repeat"
Width="16"
Height="16"
IsChecked="{Binding RepeatSegment}"
VerticalAlignment="Center"
Visibility="{Binding IsMainSegment, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
<ToggleButton.Style>
<Style TargetType="ToggleButton" BasedOn="{StaticResource MaterialDesignFlatToggleButton}">
<Style.Triggers>
<DataTrigger Binding="{Binding ShowRepeatButton}" Value="False">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="1.0" To="0.0" Duration="0:0:0.25">
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode="EaseInOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="00:00:0.25" Value="{x:Static Visibility.Collapsed}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="00:00:00" Value="{x:Static Visibility.Visible}" />
</ObjectAnimationUsingKeyFrames>
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="0.0" To="1.0" Duration="0:0:0.25" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</ToggleButton.Style>
<materialDesign:PackIcon Kind="Repeat" Height="12" Width="12" />
</ToggleButton>
<Button Grid.Column="2" ToolTip="Disable segment" Width="20" Height="20" Margin="0 0 5 0" Command="{s:Action DisableSegment}">
<Button.Style>
<Style TargetType="Button" BasedOn="{StaticResource MaterialDesignIconButton}">
<Style.Triggers>
<DataTrigger Binding="{Binding ShowDisableButton}" Value="False">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="1.0" To="0.0" Duration="0:0:0.25">
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode="EaseInOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="0.0" To="1.0" Duration="0:0:0.25" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
<materialDesign:PackIcon Kind="Close" Height="18" Width="18" />
</Button>
</Grid>
<!-- Segment movement display -->
<Rectangle RadiusX="2" RadiusY="2" Fill="{DynamicResource PrimaryHueDarkBrush}" Margin="-2 -2 0 0" Width="5" Height="24" VerticalAlignment="Top" HorizontalAlignment="Right" />
<!-- Segment movement handles -->
<Rectangle RadiusX="2"
RadiusY="2"
Fill="Transparent"
Margin="-9 -2 0 0"
Width="12"
Height="25"
VerticalAlignment="Top"
HorizontalAlignment="Right"
Cursor="SizeWE"
MouseDown="{s:Action SegmentMouseDown}"
MouseUp="{s:Action SegmentMouseUp}"
MouseMove="{s:Action SegmentMouseMove}" />
</StackPanel>
</UserControl>
@@ -1,404 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Artemis.Core;
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.Dialogs;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
{
public sealed class TimelineSegmentViewModel : Screen
{
private readonly IDialogService _dialogService;
private bool _draggingSegment;
private bool _segmentEnabled;
private TimeSpan _segmentEnd;
private TimeSpan _segmentLength;
private TimeSpan _segmentStart;
private double _segmentStartPosition;
private double _segmentWidth;
private bool _showDisableButton;
private bool _showRepeatButton;
private bool _showSegmentName;
public TimelineSegmentViewModel(SegmentViewModelType segment, IObservableCollection<LayerPropertyGroupViewModel> layerPropertyGroups,
IProfileEditorService profileEditorService, IDialogService dialogService)
{
_dialogService = dialogService;
ProfileEditorService = profileEditorService;
Segment = segment;
LayerPropertyGroups = layerPropertyGroups;
if (Segment == SegmentViewModelType.Start)
ToolTip = "This segment is played when a layer starts displaying because it's conditions are met";
else if (Segment == SegmentViewModelType.Main)
ToolTip = "This segment is played while a condition is met, either once or on a repeating loop";
else if (Segment == SegmentViewModelType.End)
ToolTip = "This segment is played once a condition is no longer met";
IsMainSegment = Segment == SegmentViewModelType.Main;
}
public RenderProfileElement SelectedProfileElement => ProfileEditorService.SelectedProfileElement;
public SegmentViewModelType Segment { get; }
public IObservableCollection<LayerPropertyGroupViewModel> LayerPropertyGroups { get; }
public IProfileEditorService ProfileEditorService { get; }
public string ToolTip { get; }
public bool IsMainSegment { get; }
public TimeSpan SegmentLength
{
get => _segmentLength;
set => SetAndNotify(ref _segmentLength, value);
}
public TimeSpan SegmentStart
{
get => _segmentStart;
set => SetAndNotify(ref _segmentStart, value);
}
public TimeSpan SegmentEnd
{
get => _segmentEnd;
set => SetAndNotify(ref _segmentEnd, value);
}
public double SegmentWidth
{
get => _segmentWidth;
set => SetAndNotify(ref _segmentWidth, value);
}
public double SegmentStartPosition
{
get => _segmentStartPosition;
set => SetAndNotify(ref _segmentStartPosition, value);
}
public bool SegmentEnabled
{
get => _segmentEnabled;
set => SetAndNotify(ref _segmentEnabled, value);
}
// Only the main segment supports this, for any other segment the getter always returns false and the setter does nothing
public bool RepeatSegment
{
get
{
if (Segment != SegmentViewModelType.Main)
return false;
return SelectedProfileElement?.Timeline.PlayMode == TimelinePlayMode.Repeat;
}
set
{
if (Segment != SegmentViewModelType.Main || SelectedProfileElement == null)
return;
SelectedProfileElement.Timeline.PlayMode = value ? TimelinePlayMode.Repeat : TimelinePlayMode.Once;
ProfileEditorService.SaveSelectedProfileElement();
NotifyOfPropertyChange(nameof(RepeatSegment));
}
}
public bool ShowSegmentName
{
get => _showSegmentName;
set => SetAndNotify(ref _showSegmentName, value);
}
public bool ShowRepeatButton
{
get => _showRepeatButton;
set => SetAndNotify(ref _showRepeatButton, value);
}
public bool ShowDisableButton
{
get => _showDisableButton;
set => SetAndNotify(ref _showDisableButton, value);
}
public async Task OpenSettingsDialog()
{
await _dialogService.ShowDialog<TimelineSegmentDialogViewModel>(new Dictionary<string, object> {{"segment", this}});
}
public void ShiftNextSegment(TimeSpan amount)
{
if (Segment == SegmentViewModelType.Start)
ShiftKeyframes(SelectedProfileElement.Timeline.StartSegmentEndPosition, null, amount);
else if (Segment == SegmentViewModelType.Main)
ShiftKeyframes(SelectedProfileElement.Timeline.MainSegmentEndPosition, null, amount);
else if (Segment == SegmentViewModelType.End)
ShiftKeyframes(SelectedProfileElement.Timeline.EndSegmentEndPosition, null, amount);
}
private void WipeKeyframes(TimeSpan? start, TimeSpan? end)
{
foreach (LayerPropertyGroupViewModel layerPropertyGroupViewModel in LayerPropertyGroups)
layerPropertyGroupViewModel.WipeKeyframes(start, end);
}
private void ShiftKeyframes(TimeSpan? start, TimeSpan? end, TimeSpan amount)
{
foreach (LayerPropertyGroupViewModel layerPropertyGroupViewModel in LayerPropertyGroups)
layerPropertyGroupViewModel.ShiftKeyframes(start, end, amount);
}
#region Overrides of Screen
/// <inheritdoc />
protected override void OnInitialActivate()
{
ProfileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged;
ProfileEditorService.SelectedProfileElementChanged += SelectedProfileEditorServiceOnSelectedProfileElementChanged;
if (ProfileEditorService.SelectedProfileElement != null)
ProfileEditorService.SelectedProfileElement.Timeline.PropertyChanged += TimelineOnPropertyChanged;
Update();
base.OnInitialActivate();
}
/// <inheritdoc />
protected override void OnClose()
{
ProfileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged;
ProfileEditorService.SelectedProfileElementChanged -= SelectedProfileEditorServiceOnSelectedProfileElementChanged;
if (SelectedProfileElement != null)
SelectedProfileElement.Timeline.PropertyChanged -= TimelineOnPropertyChanged;
base.OnClose();
}
#endregion
#region Updating
private void UpdateHeader()
{
if (!IsMainSegment)
ShowSegmentName = SegmentWidth > 60;
else
ShowSegmentName = SegmentWidth > 80;
ShowRepeatButton = SegmentWidth > 45 && IsMainSegment;
ShowDisableButton = SegmentWidth > 25;
if (Segment == SegmentViewModelType.Main)
NotifyOfPropertyChange(nameof(RepeatSegment));
}
private void Update()
{
if (SelectedProfileElement == null)
{
SegmentStart = TimeSpan.Zero;
SegmentEnd = TimeSpan.Zero;
SegmentLength = TimeSpan.Zero;
SegmentStartPosition = 0;
SegmentWidth = 0;
SegmentEnabled = false;
return;
}
// It would be nice to do this differently if this patterns end up appearing in more places
if (Segment == SegmentViewModelType.Start)
{
SegmentStart = TimeSpan.Zero;
SegmentEnd = SelectedProfileElement.Timeline.StartSegmentEndPosition;
SegmentLength = SelectedProfileElement.Timeline.StartSegmentLength;
}
else if (Segment == SegmentViewModelType.Main)
{
SegmentStart = SelectedProfileElement.Timeline.MainSegmentStartPosition;
SegmentEnd = SelectedProfileElement.Timeline.MainSegmentEndPosition;
SegmentLength = SelectedProfileElement.Timeline.MainSegmentLength;
}
else if (Segment == SegmentViewModelType.End)
{
SegmentStart = SelectedProfileElement.Timeline.EndSegmentStartPosition;
SegmentEnd = SelectedProfileElement.Timeline.EndSegmentEndPosition;
SegmentLength = SelectedProfileElement.Timeline.EndSegmentLength;
}
SegmentStartPosition = SegmentStart.TotalSeconds * ProfileEditorService.PixelsPerSecond;
SegmentWidth = SegmentLength.TotalSeconds * ProfileEditorService.PixelsPerSecond;
SegmentEnabled = SegmentLength != TimeSpan.Zero;
UpdateHeader();
}
#endregion
#region Controls
public void DisableSegment()
{
TimeSpan oldSegmentLength = SegmentLength;
if (Segment == SegmentViewModelType.Start)
{
// Remove keyframes that fall in this segment
WipeKeyframes(null, SelectedProfileElement.Timeline.StartSegmentEndPosition);
SelectedProfileElement.Timeline.StartSegmentLength = TimeSpan.Zero;
}
else if (Segment == SegmentViewModelType.Main)
{
// Remove keyframes that fall in this segment
WipeKeyframes(SelectedProfileElement.Timeline.MainSegmentStartPosition, SelectedProfileElement.Timeline.MainSegmentEndPosition);
SelectedProfileElement.Timeline.MainSegmentLength = TimeSpan.Zero;
}
else if (Segment == SegmentViewModelType.End)
{
// Remove keyframes that fall in this segment
WipeKeyframes(SelectedProfileElement.Timeline.EndSegmentStartPosition, SelectedProfileElement.Timeline.EndSegmentEndPosition);
SelectedProfileElement.Timeline.EndSegmentLength = TimeSpan.Zero;
}
ShiftNextSegment(SegmentLength - oldSegmentLength);
ProfileEditorService.SaveSelectedProfileElement();
Update();
}
public void EnableSegment()
{
ShiftNextSegment(TimeSpan.FromSeconds(1));
if (Segment == SegmentViewModelType.Start)
SelectedProfileElement.Timeline.StartSegmentLength = TimeSpan.FromSeconds(1);
else if (Segment == SegmentViewModelType.Main)
SelectedProfileElement.Timeline.MainSegmentLength = TimeSpan.FromSeconds(1);
else if (Segment == SegmentViewModelType.End)
SelectedProfileElement.Timeline.EndSegmentLength = TimeSpan.FromSeconds(1);
ProfileEditorService.SaveSelectedProfileElement();
Update();
}
#endregion
#region Mouse events
public void SegmentMouseDown(object sender, MouseButtonEventArgs e)
{
((IInputElement) sender).CaptureMouse();
_draggingSegment = true;
}
public void SegmentMouseUp(object sender, MouseButtonEventArgs e)
{
((IInputElement) sender).ReleaseMouseCapture();
_draggingSegment = false;
ProfileEditorService.SaveSelectedProfileElement();
}
public void SegmentMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed || !_draggingSegment)
return;
// Get the parent scroll viewer, need that for our position
ScrollViewer parent = VisualTreeUtilities.FindParent<ScrollViewer>((DependencyObject) sender, "TimelineHeaderScrollViewer");
double x = Math.Max(0, e.GetPosition(parent).X);
TimeSpan newTime = TimeSpan.FromSeconds(x / ProfileEditorService.PixelsPerSecond);
// Round the time to something that fits the current zoom level
if (ProfileEditorService.PixelsPerSecond < 200)
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 5.0) * 5.0);
else if (ProfileEditorService.PixelsPerSecond < 500)
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 2.0) * 2.0);
else
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds));
// If holding down shift, snap to the closest element on the timeline
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
{
List<TimeSpan> keyframeTimes = LayerPropertyGroups.SelectMany(g => g.GetAllKeyframeViewModels(true)).Select(k => k.Position).ToList();
newTime = ProfileEditorService.SnapToTimeline(newTime, TimeSpan.FromMilliseconds(1000f / ProfileEditorService.PixelsPerSecond * 5), false, true, keyframeTimes);
}
// If holding down control, round to the closest 50ms
else if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
{
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 50.0) * 50.0);
}
UpdateEndPosition(newTime);
}
public void UpdateEndPosition(TimeSpan end)
{
if (end < TimeSpan.FromMilliseconds(100))
end = TimeSpan.FromMilliseconds(100);
if (Segment == SegmentViewModelType.Start)
SelectedProfileElement.Timeline.StartSegmentEndPosition = end;
else if (Segment == SegmentViewModelType.Main)
SelectedProfileElement.Timeline.MainSegmentEndPosition = end;
else if (Segment == SegmentViewModelType.End)
SelectedProfileElement.Timeline.EndSegmentEndPosition = end;
Update();
}
public void UpdateLength(TimeSpan length)
{
if (length < TimeSpan.FromMilliseconds(100))
length = TimeSpan.FromMilliseconds(100);
if (Segment == SegmentViewModelType.Start)
SelectedProfileElement.Timeline.StartSegmentLength = length;
else if (Segment == SegmentViewModelType.Main)
SelectedProfileElement.Timeline.MainSegmentLength = length;
else if (Segment == SegmentViewModelType.End)
SelectedProfileElement.Timeline.EndSegmentLength = length;
Update();
}
#endregion
#region Event handlers
private void SelectedProfileEditorServiceOnSelectedProfileElementChanged(object sender, RenderProfileElementEventArgs e)
{
if (e.PreviousRenderProfileElement != null)
e.PreviousRenderProfileElement.Timeline.PropertyChanged -= TimelineOnPropertyChanged;
if (e.RenderProfileElement != null)
e.RenderProfileElement.Timeline.PropertyChanged += TimelineOnPropertyChanged;
Update();
}
private void TimelineOnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Core.Timeline.StartSegmentLength) ||
e.PropertyName == nameof(Core.Timeline.MainSegmentLength) ||
e.PropertyName == nameof(Core.Timeline.EndSegmentLength))
Update();
else if (e.PropertyName == nameof(Core.Timeline.PlayMode))
NotifyOfPropertyChange(nameof(RepeatSegment));
}
private void ProfileEditorServiceOnPixelsPerSecondChanged(object sender, EventArgs e)
{
Update();
}
#endregion
}
public enum SegmentViewModelType
{
Start,
Main,
End
}
}
@@ -1,110 +0,0 @@
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.TimelineView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="25"
d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:TimelineViewModel}">
<Grid Background="{DynamicResource MaterialDesignToolBarBackground}"
MouseDown="{s:Action TimelineCanvasMouseDown}"
MouseUp="{s:Action TimelineCanvasMouseUp}"
MouseMove="{s:Action TimelineCanvasMouseMove}"
ContextMenuOpening="{s:Action ContextMenuOpening}"
ContextMenuClosing="{s:Action ContextMenuClosing}"
Height="{Binding LayerPropertiesViewModel.TreeViewModelHeight}"
VerticalAlignment="Top"
Focusable="True">
<Grid.Triggers>
<EventTrigger RoutedEvent="UIElement.MouseLeftButtonDown">
<BeginStoryboard>
<Storyboard Storyboard.TargetName="MultiSelectionPath" Storyboard.TargetProperty="Opacity">
<DoubleAnimation To="1" Duration="0:0:0.1" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="UIElement.MouseLeftButtonUp">
<BeginStoryboard>
<Storyboard Storyboard.TargetName="MultiSelectionPath" Storyboard.TargetProperty="Opacity">
<DoubleAnimation To="0" Duration="0:0:0.2" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Grid.Triggers>
<Grid.ContextMenu>
<ContextMenu>
<MenuItem Header="Duplicate" Command="{s:Action DuplicateKeyframes}" CommandParameter="{Binding}" InputGestureText="Ctrl+D">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="ContentDuplicate" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Copy" Command="{s:Action CopyKeyframes}" CommandParameter="{Binding}" InputGestureText="Ctrl+C">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="ContentCopy" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Paste" Command="{s:Action PasteKeyframes}" CommandParameter="{Binding}" InputGestureText="Ctrl+V" >
<MenuItem.Icon>
<materialDesign:PackIcon Kind="ContentPaste" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Delete" Command="{s:Action DeleteKeyframes}" InputGestureText="Del">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="Delete" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</Grid.ContextMenu>
<ItemsControl ItemsSource="{Binding LayerPropertyGroups}"
MinWidth="{Binding TotalTimelineWidth}"
HorizontalAlignment="Left">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl s:View.Model="{Binding TimelineGroupViewModel}" HorizontalContentAlignment="Left" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Line Stroke="{StaticResource PrimaryHueDarkBrush}"
Opacity="0.5"
StrokeDashArray="4 2"
X1="{Binding StartSegmentEndPosition}"
X2="{Binding StartSegmentEndPosition}"
Y1="0"
Y2="{Binding LayerPropertiesViewModel.TreeViewModelHeight}"
HorizontalAlignment="Left"
Visibility="{Binding LayerPropertiesViewModel.StartTimelineSegmentViewModel.SegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}" />
<Line Stroke="{StaticResource PrimaryHueDarkBrush}"
Opacity="0.5"
StrokeDashArray="4 2"
X1="{Binding MainSegmentEndPosition}"
X2="{Binding MainSegmentEndPosition}"
Y1="0"
Y2="{Binding LayerPropertiesViewModel.TreeViewModelHeight}" />
<Line Stroke="{StaticResource PrimaryHueDarkBrush}"
Opacity="0.5"
StrokeDashArray="4 2"
X1="{Binding EndSegmentEndPosition}"
X2="{Binding EndSegmentEndPosition}"
Y1="0"
Y2="{Binding LayerPropertiesViewModel.TreeViewModelHeight}"
Visibility="{Binding LayerPropertiesViewModel.EndTimelineSegmentViewModel.SegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}" />
<!-- Multi-selection rectangle -->
<Path x:Name="MultiSelectionPath"
Data="{Binding SelectionRectangle}"
Opacity="0"
Stroke="{DynamicResource PrimaryHueLightBrush}"
StrokeThickness="1"
IsHitTestVisible="False">
<Path.Fill>
<SolidColorBrush Color="{DynamicResource Primary400}" Opacity="0.25" />
</Path.Fill>
</Path>
</Grid>
</UserControl>
@@ -1,453 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using Artemis.Core;
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.Models;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
{
public sealed class TimelineViewModel : Screen
{
private readonly IProfileEditorService _profileEditorService;
private RectangleGeometry _selectionRectangle;
public TimelineViewModel(LayerPropertiesViewModel layerPropertiesViewModel, IObservableCollection<LayerPropertyGroupViewModel> layerPropertyGroups, IProfileEditorService profileEditorService)
{
_profileEditorService = profileEditorService;
LayerPropertiesViewModel = layerPropertiesViewModel;
LayerPropertyGroups = layerPropertyGroups;
SelectionRectangle = new RectangleGeometry();
}
public LayerPropertiesViewModel LayerPropertiesViewModel { get; }
public IObservableCollection<LayerPropertyGroupViewModel> LayerPropertyGroups { get; }
public RectangleGeometry SelectionRectangle
{
get => _selectionRectangle;
set => SetAndNotify(ref _selectionRectangle, value);
}
public double StartSegmentEndPosition
{
get => _startSegmentEndPosition;
set => SetAndNotify(ref _startSegmentEndPosition, value);
}
public double MainSegmentEndPosition
{
get => _mainSegmentEndPosition;
set => SetAndNotify(ref _mainSegmentEndPosition, value);
}
public double EndSegmentEndPosition
{
get => _endSegmentEndPosition;
set => SetAndNotify(ref _endSegmentEndPosition, value);
}
public double TotalTimelineWidth
{
get => _totalTimelineWidth;
set => SetAndNotify(ref _totalTimelineWidth, value);
}
private void Update()
{
StartSegmentEndPosition = LayerPropertiesViewModel.StartTimelineSegmentViewModel.SegmentEnd.TotalSeconds * _profileEditorService.PixelsPerSecond;
MainSegmentEndPosition = LayerPropertiesViewModel.MainTimelineSegmentViewModel.SegmentEnd.TotalSeconds * _profileEditorService.PixelsPerSecond;
EndSegmentEndPosition = LayerPropertiesViewModel.EndTimelineSegmentViewModel.SegmentEnd.TotalSeconds * _profileEditorService.PixelsPerSecond;
TotalTimelineWidth = EndSegmentEndPosition;
}
private void TimelineOnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Core.Timeline.StartSegmentLength) ||
e.PropertyName == nameof(Core.Timeline.MainSegmentLength) ||
e.PropertyName == nameof(Core.Timeline.EndSegmentLength))
Update();
}
private void ProfileEditorServiceOnPixelsPerSecondChanged(object sender, EventArgs e)
{
Update();
}
private void SelectedProfileEditorServiceOnSelectedProfileElementChanged(object sender, RenderProfileElementEventArgs e)
{
if (e.PreviousRenderProfileElement != null)
e.PreviousRenderProfileElement.Timeline.PropertyChanged -= TimelineOnPropertyChanged;
if (e.RenderProfileElement != null)
e.RenderProfileElement.Timeline.PropertyChanged += TimelineOnPropertyChanged;
Update();
}
#region Overrides of Screen
/// <inheritdoc />
protected override void OnInitialActivate()
{
_profileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged;
_profileEditorService.SelectedProfileElementChanged += SelectedProfileEditorServiceOnSelectedProfileElementChanged;
if (_profileEditorService.SelectedProfileElement != null)
_profileEditorService.SelectedProfileElement.Timeline.PropertyChanged += TimelineOnPropertyChanged;
Update();
base.OnInitialActivate();
}
/// <inheritdoc />
protected override void OnClose()
{
_profileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged;
_profileEditorService.SelectedProfileElementChanged -= SelectedProfileEditorServiceOnSelectedProfileElementChanged;
if (_profileEditorService.SelectedProfileElement != null)
_profileEditorService.SelectedProfileElement.Timeline.PropertyChanged -= TimelineOnPropertyChanged;
base.OnClose();
}
#endregion
#region Command handlers
public void KeyframeMouseDown(object sender, MouseButtonEventArgs e)
{
// if (e.LeftButton == MouseButtonState.Released)
// return;
if (!((sender as Ellipse)?.DataContext is ITimelineKeyframeViewModel viewModel))
return;
((IInputElement) sender).CaptureMouse();
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift) && !viewModel.IsSelected)
SelectKeyframe(viewModel, true, false);
else if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
SelectKeyframe(viewModel, false, true);
else if (!viewModel.IsSelected)
SelectKeyframe(viewModel, false, false);
e.Handled = true;
}
public void KeyframeMouseUp(object sender, MouseButtonEventArgs e)
{
_profileEditorService.SaveSelectedProfileElement();
ReleaseSelectedKeyframes();
((IInputElement) sender).ReleaseMouseCapture();
}
public void KeyframeMouseMove(object sender, MouseEventArgs e)
{
if (sender is Ellipse ellipse && ellipse.DataContext is ITimelineKeyframeViewModel viewModel)
{
if (e.LeftButton == MouseButtonState.Pressed)
MoveSelectedKeyframes(GetCursorTime(e.GetPosition(View)), viewModel);
e.Handled = true;
}
}
#region Context menu actions
public bool CanDuplicateKeyframes => GetAllKeyframeViewModels().Any(k => k.IsSelected);
public bool CanCopyKeyframes => GetAllKeyframeViewModels().Any(k => k.IsSelected);
public bool CanDeleteKeyframes => GetAllKeyframeViewModels().Any(k => k.IsSelected);
public bool CanPasteKeyframes => JsonClipboard.GetData() is KeyframesClipboardModel;
private TimeSpan? _contextMenuOpenPosition;
public void ContextMenuOpening(object sender, ContextMenuEventArgs e)
{
_contextMenuOpenPosition = GetCursorTime(new Point(e.CursorLeft, e.CursorTop));
NotifyOfPropertyChange(nameof(CanDuplicateKeyframes));
NotifyOfPropertyChange(nameof(CanCopyKeyframes));
NotifyOfPropertyChange(nameof(CanDeleteKeyframes));
NotifyOfPropertyChange(nameof(CanPasteKeyframes));
}
public void ContextMenuClosing(object sender, ContextMenuEventArgs e)
{
_contextMenuOpenPosition = null;
}
public void KeyframeContextMenuOpening(object sender, ContextMenuEventArgs e)
{
if (sender is FrameworkElement fe && fe.DataContext is ITimelineKeyframeViewModel viewModel)
viewModel.PopulateEasingViewModels();
}
public void KeyframeContextMenuClosing(object sender, ContextMenuEventArgs e)
{
if (sender is Ellipse ellipse && ellipse.DataContext is ITimelineKeyframeViewModel viewModel)
viewModel.ClearEasingViewModels();
}
public void DeleteKeyframes()
{
List<ITimelineKeyframeViewModel> keyframeViewModels = GetAllKeyframeViewModels().Where(k => k.IsSelected).ToList();
foreach (ITimelineKeyframeViewModel keyframeViewModel in keyframeViewModels)
keyframeViewModel.Delete(false);
_profileEditorService.SaveSelectedProfileElement();
}
public void DuplicateKeyframes(object sender)
{
TimeSpan pastePosition = GetPastePosition(sender as ITimelineKeyframeViewModel);
List<ILayerPropertyKeyframe> keyframes = GetAllKeyframeViewModels().Where(k => k.IsSelected).Select(k => k.Keyframe).ToList();
List<ILayerPropertyKeyframe> newKeyframes = DuplicateKeyframes(keyframes, pastePosition);
// Select only the newly duplicated keyframes
foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in GetAllKeyframeViewModels())
timelineKeyframeViewModel.IsSelected = newKeyframes.Contains(timelineKeyframeViewModel.Keyframe);
_profileEditorService.SaveSelectedProfileElement();
}
public void CopyKeyframes()
{
List<ILayerPropertyKeyframe> keyframes = GetAllKeyframeViewModels().Where(k => k.IsSelected).Select(k => k.Keyframe).ToList();
CopyKeyframes(keyframes);
}
public void PasteKeyframes(object sender)
{
TimeSpan pastePosition = GetPastePosition(sender as ITimelineKeyframeViewModel);
List<ILayerPropertyKeyframe> newKeyframes = PasteKeyframes(pastePosition);
// Select only the newly pasted keyframes
foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in GetAllKeyframeViewModels())
timelineKeyframeViewModel.IsSelected = newKeyframes.Contains(timelineKeyframeViewModel.Keyframe);
_profileEditorService.SaveSelectedProfileElement();
}
private TimeSpan GetPastePosition(ITimelineKeyframeViewModel viewModel)
{
TimeSpan pastePosition = _profileEditorService.CurrentTime;
// If a keyframe VM is provided, paste onto there
if (viewModel != null)
pastePosition = viewModel.Position;
// Paste at the position the context menu was opened
else if (_contextMenuOpenPosition != null)
pastePosition = _contextMenuOpenPosition.Value;
return pastePosition;
}
private List<ILayerPropertyKeyframe> DuplicateKeyframes(List<ILayerPropertyKeyframe> keyframes, TimeSpan pastePosition)
{
KeyframesClipboardModel clipboardModel = CoreJson.DeserializeObject<KeyframesClipboardModel>(CoreJson.SerializeObject(new KeyframesClipboardModel(keyframes), true), true);
return PasteClipboardData(clipboardModel, pastePosition);
}
private void CopyKeyframes(List<ILayerPropertyKeyframe> keyframes)
{
KeyframesClipboardModel clipboardModel = new(keyframes);
JsonClipboard.SetObject(clipboardModel);
}
private List<ILayerPropertyKeyframe> PasteKeyframes(TimeSpan pastePosition)
{
KeyframesClipboardModel clipboardObject = JsonClipboard.GetData<KeyframesClipboardModel>();
return PasteClipboardData(clipboardObject, pastePosition);
}
private List<ILayerPropertyKeyframe> PasteClipboardData(KeyframesClipboardModel clipboardModel, TimeSpan pastePosition)
{
List<ILayerPropertyKeyframe> pasted = new();
if (clipboardModel == null)
return pasted;
RenderProfileElement target = _profileEditorService.SelectedProfileElement;
if (target == null)
return pasted;
return clipboardModel.Paste(target, pastePosition);
}
#endregion
private TimeSpan GetCursorTime(Point position)
{
// Get the parent grid, need that for our position
double x = Math.Max(0, position.X);
TimeSpan time = TimeSpan.FromSeconds(x / _profileEditorService.PixelsPerSecond);
// Round the time to something that fits the current zoom level
if (_profileEditorService.PixelsPerSecond < 200)
time = TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds / 5.0) * 5.0);
else if (_profileEditorService.PixelsPerSecond < 500)
time = TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds / 2.0) * 2.0);
else
time = TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds));
// If shift is held, snap to the current time
// Take a tolerance of 5 pixels (half a keyframe width)
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
time = _profileEditorService.SnapToTimeline(time, TimeSpan.FromMilliseconds(1000f / _profileEditorService.PixelsPerSecond * 5), false, true);
return time;
}
#endregion
#region Keyframe movement
public void MoveSelectedKeyframes(TimeSpan cursorTime, ITimelineKeyframeViewModel sourceKeyframeViewModel)
{
// Ensure the selection rectangle doesn't show, the view isn't aware of different types of dragging
SelectionRectangle.Rect = new Rect();
List<ITimelineKeyframeViewModel> keyframeViewModels = GetAllKeyframeViewModels();
foreach (ITimelineKeyframeViewModel keyframeViewModel in keyframeViewModels.Where(k => k.IsSelected))
keyframeViewModel.SaveOffsetToKeyframe(sourceKeyframeViewModel);
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
cursorTime = _profileEditorService.SnapToTimeline(
cursorTime,
TimeSpan.FromMilliseconds(1000f / _profileEditorService.PixelsPerSecond * 5),
true,
false,
keyframeViewModels.Where(k => k != sourceKeyframeViewModel).Select(k => k.Position).ToList()
);
// If holding down control, round to the closest 50ms
else if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
cursorTime = TimeSpan.FromMilliseconds(Math.Round(cursorTime.TotalMilliseconds / 50.0) * 50.0);
sourceKeyframeViewModel.UpdatePosition(cursorTime);
foreach (ITimelineKeyframeViewModel keyframeViewModel in keyframeViewModels.Where(k => k.IsSelected))
keyframeViewModel.ApplyOffsetToKeyframe(sourceKeyframeViewModel);
_profileEditorService.UpdateProfilePreview();
}
public void ReleaseSelectedKeyframes()
{
List<ITimelineKeyframeViewModel> keyframeViewModels = GetAllKeyframeViewModels();
foreach (ITimelineKeyframeViewModel keyframeViewModel in keyframeViewModels.Where(k => k.IsSelected))
keyframeViewModel.ReleaseMovement();
}
#endregion
#region Keyframe selection
private Point _mouseDragStartPoint;
private bool _mouseDragging;
private double _startSegmentEndPosition;
private double _mainSegmentEndPosition;
private double _endSegmentEndPosition;
private double _totalTimelineWidth;
// ReSharper disable once UnusedMember.Global - Called from view
public void TimelineCanvasMouseDown(object sender, MouseButtonEventArgs e)
{
// Workaround for focus not being applied to the grid causing keybinds not to function
((IInputElement) sender).Focus();
if (e.LeftButton == MouseButtonState.Released)
return;
((IInputElement) sender).CaptureMouse();
SelectionRectangle.Rect = new Rect();
_mouseDragStartPoint = e.GetPosition((IInputElement) sender);
_mouseDragging = true;
e.Handled = true;
}
// ReSharper disable once UnusedMember.Global - Called from view
public void TimelineCanvasMouseUp(object sender, MouseEventArgs e)
{
if (!_mouseDragging)
return;
Point position = e.GetPosition((IInputElement) sender);
Rect selectedRect = new(_mouseDragStartPoint, position);
SelectionRectangle.Rect = selectedRect;
List<ITimelineKeyframeViewModel> keyframeViewModels = GetAllKeyframeViewModels();
List<ITimelineKeyframeViewModel> selectedKeyframes = HitTestUtilities.GetHitViewModels<ITimelineKeyframeViewModel>((Visual) sender, SelectionRectangle);
foreach (ITimelineKeyframeViewModel keyframeViewModel in keyframeViewModels)
keyframeViewModel.IsSelected = selectedKeyframes.Contains(keyframeViewModel);
_mouseDragging = false;
e.Handled = true;
((IInputElement) sender).ReleaseMouseCapture();
}
public void TimelineCanvasMouseMove(object sender, MouseEventArgs e)
{
if (_mouseDragging && e.LeftButton == MouseButtonState.Pressed)
{
Point position = e.GetPosition((IInputElement) sender);
Rect selectedRect = new(_mouseDragStartPoint, position);
SelectionRectangle.Rect = selectedRect;
e.Handled = true;
}
}
public void SelectKeyframe(ITimelineKeyframeViewModel clicked, bool selectBetween, bool toggle)
{
List<ITimelineKeyframeViewModel> keyframeViewModels = GetAllKeyframeViewModels();
if (selectBetween)
{
int selectedIndex = keyframeViewModels.FindIndex(k => k.IsSelected);
// If nothing is selected, select only the clicked
if (selectedIndex == -1)
{
clicked.IsSelected = true;
return;
}
foreach (ITimelineKeyframeViewModel keyframeViewModel in keyframeViewModels)
keyframeViewModel.IsSelected = false;
int clickedIndex = keyframeViewModels.IndexOf(clicked);
if (clickedIndex < selectedIndex)
foreach (ITimelineKeyframeViewModel keyframeViewModel in keyframeViewModels.Skip(clickedIndex).Take(selectedIndex - clickedIndex + 1))
keyframeViewModel.IsSelected = true;
else
foreach (ITimelineKeyframeViewModel keyframeViewModel in keyframeViewModels.Skip(selectedIndex).Take(clickedIndex - selectedIndex + 1))
keyframeViewModel.IsSelected = true;
}
else if (toggle)
{
// Toggle only the clicked keyframe, leave others alone
clicked.IsSelected = !clicked.IsSelected;
}
else
{
// Only select the clicked keyframe
foreach (ITimelineKeyframeViewModel keyframeViewModel in keyframeViewModels)
keyframeViewModel.IsSelected = false;
clicked.IsSelected = true;
}
}
private List<ITimelineKeyframeViewModel> GetAllKeyframeViewModels()
{
List<ITimelineKeyframeViewModel> viewModels = new();
foreach (LayerPropertyGroupViewModel layerPropertyGroupViewModel in LayerPropertyGroups)
viewModels.AddRange(layerPropertyGroupViewModel.GetAllKeyframeViewModels(false));
return viewModels;
}
#endregion
}
}
@@ -1,283 +0,0 @@
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree.TreeGroupView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:dd="urn:gong-wpf-dragdrop"
xmlns:layerProperties="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance local:TreeGroupViewModel}"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<shared:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
<converters:InverseBooleanConverter x:Key="InverseBooleanConverter" />
<converters:PropertyTreeMarginConverter Length="20" x:Key="PropertyTreeMarginConverter" />
</UserControl.Resources>
<StackPanel>
<Border Name="Bd"
BorderBrush="{DynamicResource MaterialDesignDivider}"
BorderThickness="0,0,0,1"
Height="25">
<Grid Margin="{Binding Converter={StaticResource PropertyTreeMarginConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="19" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ToggleButton x:Name="Expander"
Foreground="{DynamicResource MaterialDesignBody}"
Style="{StaticResource MaterialDesignExpandCollapseToggleStyle}"
IsChecked="{Binding Path=LayerPropertyGroupViewModel.IsExpanded}"
Visibility="{Binding Path=LayerPropertyGroupViewModel.HasChildren, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}"
ClickMode="Press" />
<StackPanel Grid.Column="1">
<!-- Type: None -->
<TextBlock
dd:DragDrop.DragSourceIgnore="True"
Text="{Binding LayerPropertyGroup.GroupDescription.Name}"
ToolTip="{Binding LayerPropertyGroup.GroupDescription.Description}"
VerticalAlignment="Center"
HorizontalAlignment="Left"
Margin="3 5 0 5">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding GroupType}" Value="None">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<!-- Type: General -->
<StackPanel Orientation="Horizontal" Margin="0 5" dd:DragDrop.DragSourceIgnore="True">
<StackPanel.Style>
<Style TargetType="{x:Type StackPanel}">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding GroupType}" Value="General">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<materialDesign:PackIcon Kind="HammerWrench" Margin="0 -1 5 0" />
<TextBlock ToolTip="{Binding LayerPropertyGroup.GroupDescription.Description}">General</TextBlock>
</StackPanel>
<!-- Type: Transform -->
<StackPanel Orientation="Horizontal" Margin="0 5" dd:DragDrop.DragSourceIgnore="True">
<StackPanel.Style>
<Style TargetType="{x:Type StackPanel}">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding GroupType}" Value="Transform">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<materialDesign:PackIcon Kind="TransitConnectionVariant" Margin="0 -1 5 0" />
<TextBlock ToolTip="{Binding LayerPropertyGroup.GroupDescription.Description}">Transform</TextBlock>
</StackPanel>
<!-- Type: LayerBrushRoot -->
<Grid dd:DragDrop.DragSourceIgnore="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.Style>
<Style TargetType="{x:Type Grid}">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding GroupType}" Value="LayerBrushRoot">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<shared:ArtemisIcon Grid.Column="0"
Icon="{Binding LayerPropertyGroup.LayerBrush.Descriptor.Icon}"
Width="16"
Height="16"
Margin="0 5 5 0" />
<TextBlock Grid.Column="1"
ToolTip="{Binding LayerPropertyGroup.LayerBrush.Descriptor.Description}"
Margin="0 5 0 0">
Brush -&#160;
</TextBlock>
<TextBlock Grid.Column="2"
Text="{Binding LayerPropertyGroup.LayerBrush.Descriptor.DisplayName}"
ToolTip="{Binding LayerPropertyGroup.LayerBrush.Descriptor.Description}"
Margin="0 5 0 0" />
<StackPanel Grid.Column="3"
Orientation="Horizontal"
HorizontalAlignment="Right"
Visibility="{Binding LayerPropertyGroup.LayerBrush.ConfigurationDialog, Converter={StaticResource NullToVisibilityConverter}}">
<TextBlock VerticalAlignment="Center">Extra options available!</TextBlock>
<materialDesign:PackIcon Kind="ChevronRight" VerticalAlignment="Center">
<materialDesign:PackIcon.RenderTransform>
<TranslateTransform X="0" />
</materialDesign:PackIcon.RenderTransform>
<materialDesign:PackIcon.Triggers>
<EventTrigger RoutedEvent="materialDesign:PackIcon.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(materialDesign:PackIcon.RenderTransform).(TranslateTransform.X)"
From="0"
To="2"
Duration="0:0:0.5"
AutoReverse="True"
RepeatBehavior="Forever">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseInOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</materialDesign:PackIcon.Triggers>
</materialDesign:PackIcon>
<Button Style="{StaticResource MaterialDesignIconButton}" ToolTip="Open brush settings" Width="24" Height="24" HorizontalAlignment="Right" Command="{s:Action OpenBrushSettings}">
<materialDesign:PackIcon Kind="Settings" Height="16" Width="16" />
</Button>
</StackPanel>
</Grid>
<!-- Type: LayerEffectRoot -->
<Grid Height="24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.Style>
<Style TargetType="{x:Type Grid}">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding GroupType}" Value="LayerEffectRoot">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<shared:ArtemisIcon
Grid.Column="0"
Cursor="SizeNS"
Icon="{Binding LayerPropertyGroup.LayerEffect.Descriptor.Icon}"
Width="16"
Height="16"
Margin="0 5 5 0"
Background="Transparent" />
<TextBlock Grid.Column="1" ToolTip="{Binding LayerPropertyGroup.LayerEffect.Descriptor.Description}" Margin="0 5 0 0">
Effect
</TextBlock>
<TextBlock Grid.Column="2"
ToolTip="{Binding LayerPropertyGroup.LayerEffect.Descriptor.Description}"
Margin="3 5">
-
</TextBlock>
<!-- Show either the descriptors display name or, if set, the effect name -->
<TextBlock Grid.Column="3"
Text="{Binding LayerPropertyGroup.LayerEffect.Descriptor.DisplayName}"
ToolTip="{Binding LayerPropertyGroup.LayerEffect.Descriptor.Description}"
Margin="0 5"
Visibility="{Binding LayerPropertyGroup.LayerEffect.Name, Converter={StaticResource NullToVisibilityConverter}, ConverterParameter=Inverted}" />
<TextBlock Grid.Column="4"
Text="{Binding LayerPropertyGroup.LayerEffect.Name}"
ToolTip="{Binding LayerPropertyGroup.LayerEffect.Descriptor.Description}"
Margin="0 5"
Visibility="{Binding LayerPropertyGroup.LayerEffect.Name, Converter={StaticResource NullToVisibilityConverter}}" />
<StackPanel Grid.Column="5" Orientation="Horizontal">
<ToggleButton
Style="{StaticResource MaterialDesignFlatToggleButton}"
ToolTip="Toggle suspended state"
Width="18"
Height="18"
IsChecked="{Binding LayerPropertyGroup.LayerEffect.Suspended, Converter={StaticResource InverseBooleanConverter}}"
VerticalAlignment="Center" Padding="-25"
Margin="5 0"
Command="{s:Action SuspendedToggled}">
<materialDesign:PackIcon Kind="Eye" Height="13" Width="13" />
</ToggleButton>
<Button Style="{StaticResource MaterialDesignIconButton}"
ToolTip="Rename"
Width="24"
Height="24"
VerticalAlignment="Center"
Command="{s:Action RenameEffect}">
<materialDesign:PackIcon Kind="RenameBox" Height="16" Width="16" />
</Button>
<Button Style="{StaticResource MaterialDesignIconButton}"
ToolTip="Open effect settings"
Width="24"
Height="24"
VerticalAlignment="Center"
Command="{s:Action OpenEffectSettings}"
Visibility="{Binding LayerPropertyGroup.LayerEffect.ConfigurationDialog, Converter={StaticResource NullToVisibilityConverter}}">
<materialDesign:PackIcon Kind="Settings" Height="16" Width="16" />
</Button>
<Button Style="{StaticResource MaterialDesignIconButton}"
ToolTip="Remove"
Width="24"
Height="24"
VerticalAlignment="Center"
Command="{s:Action DeleteEffect}">
<materialDesign:PackIcon Kind="TrashCan" Height="16" Width="16" />
</Button>
</StackPanel>
</Grid>
</StackPanel>
</Grid>
</Border>
<!--
Do not bind directly to the LayerPropertyGroupViewModel.Children collection
Instead use a reference provided by the VM that is null when collapsed, virtualization for noobs
-->
<ItemsControl ItemsSource="{Binding Items}"
Visibility="{Binding LayerPropertyGroupViewModel.IsExpanded, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
dd:DragDrop.DragSourceIgnore="True">
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type layerProperties:LayerPropertyGroupViewModel}">
<ContentControl s:View.Model="{Binding TreeGroupViewModel}"
IsTabStop="False"
HorizontalAlignment="Stretch"
Visibility="{Binding IsVisible, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}" />
</DataTemplate>
<DataTemplate DataType="{x:Type layerProperties:LayerPropertyViewModel}">
<ContentControl s:View.Model="{Binding TreePropertyViewModel}"
IsTabStop="False"
HorizontalAlignment="Stretch"
Visibility="{Binding IsVisible, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}" />
</DataTemplate>
</ItemsControl.Resources>
</ItemsControl>
</StackPanel>
</UserControl>
@@ -1,209 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using Artemis.Core;
using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects;
using Artemis.UI.Exceptions;
using Artemis.UI.Screens.ProfileEditor.Dialogs;
using Artemis.UI.Screens.ProfileEditor.Windows;
using Artemis.UI.Shared.LayerBrushes;
using Artemis.UI.Shared.LayerEffects;
using Artemis.UI.Shared.Services;
using Ninject;
using Ninject.Parameters;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree
{
public class TreeGroupViewModel : Screen
{
public enum LayerPropertyGroupType
{
General,
Transform,
LayerBrushRoot,
LayerEffectRoot,
None
}
private readonly IDialogService _dialogService;
private readonly IProfileEditorService _profileEditorService;
private readonly IWindowManager _windowManager;
private LayerBrushSettingsWindowViewModel _layerBrushSettingsWindowVm;
private LayerEffectSettingsWindowViewModel _layerEffectSettingsWindowVm;
public TreeGroupViewModel(LayerPropertyGroupViewModel layerPropertyGroupViewModel, IProfileEditorService profileEditorService, IDialogService dialogService, IWindowManager windowManager)
{
_profileEditorService = profileEditorService;
_dialogService = dialogService;
_windowManager = windowManager;
LayerPropertyGroupViewModel = layerPropertyGroupViewModel;
LayerPropertyGroup = LayerPropertyGroupViewModel.LayerPropertyGroup;
DetermineGroupType();
}
public LayerPropertyGroupViewModel LayerPropertyGroupViewModel { get; }
public LayerPropertyGroup LayerPropertyGroup { get; }
public LayerPropertyGroupType GroupType { get; set; }
public IObservableCollection<Screen> Items => LayerPropertyGroupViewModel.IsExpanded &&
LayerPropertyGroupViewModel.IsVisible
? LayerPropertyGroupViewModel.Items
: null;
// public void OpenBrushSettings()
// {
// BaseLayerBrush layerBrush = LayerPropertyGroup.LayerBrush;
// LayerBrushConfigurationDialog configurationViewModel = (LayerBrushConfigurationDialog) layerBrush.ConfigurationDialog;
// if (configurationViewModel == null)
// return;
//
// try
// {
// // Limit to one constructor, there's no need to have more and it complicates things anyway
// ConstructorInfo[] constructors = configurationViewModel.Type.GetConstructors();
// if (constructors.Length != 1)
// throw new ArtemisUIException("Brush configuration dialogs must have exactly one constructor");
//
// // Find the BaseLayerBrush parameter, it is required by the base constructor so its there for sure
// ParameterInfo brushParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerBrush).IsAssignableFrom(p.ParameterType));
// ConstructorArgument argument = new(brushParameter.Name, layerBrush);
// BrushConfigurationViewModel viewModel = (BrushConfigurationViewModel) layerBrush.Descriptor.Provider.Plugin.Kernel.Get(configurationViewModel.Type, argument);
//
// _layerBrushSettingsWindowVm = new LayerBrushSettingsWindowViewModel(viewModel, configurationViewModel);
// _windowManager.ShowDialog(_layerBrushSettingsWindowVm);
//
// // Save changes after the dialog closes
// _profileEditorService.SaveSelectedProfileConfiguration();
// }
// catch (Exception e)
// {
// _dialogService.ShowExceptionDialog("An exception occured while trying to show the brush's settings window", e);
// }
// }
// public void OpenEffectSettings()
// {
// BaseLayerEffect layerEffect = LayerPropertyGroup.LayerEffect;
// LayerEffectConfigurationDialog configurationViewModel = (LayerEffectConfigurationDialog) layerEffect.ConfigurationDialog;
// if (configurationViewModel == null)
// return;
//
// try
// {
// // Limit to one constructor, there's no need to have more and it complicates things anyway
// ConstructorInfo[] constructors = configurationViewModel.Type.GetConstructors();
// if (constructors.Length != 1)
// throw new ArtemisUIException("Effect configuration dialogs must have exactly one constructor");
//
// ParameterInfo effectParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerEffect).IsAssignableFrom(p.ParameterType));
// ConstructorArgument argument = new(effectParameter.Name, layerEffect);
// EffectConfigurationViewModel viewModel = (EffectConfigurationViewModel) layerEffect.Descriptor.Provider.Plugin.Kernel.Get(configurationViewModel.Type, argument);
//
// _layerEffectSettingsWindowVm = new LayerEffectSettingsWindowViewModel(viewModel, configurationViewModel);
// _windowManager.ShowDialog(_layerEffectSettingsWindowVm);
//
// // Save changes after the dialog closes
// _profileEditorService.SaveSelectedProfileConfiguration();
// }
// catch (Exception e)
// {
// _dialogService.ShowExceptionDialog("An exception occured while trying to show the effect's settings window", e);
// throw;
// }
// }
// public async void RenameEffect()
// {
// object result = await _dialogService.ShowDialogAt<RenameViewModel>(
// "PropertyTreeDialogHost",
// new Dictionary<string, object>
// {
// {"subject", "effect"},
// {"currentName", LayerPropertyGroup.LayerEffect.Name}
// }
// );
// if (result is string newName)
// {
// LayerPropertyGroup.LayerEffect.Name = newName;
// LayerPropertyGroup.LayerEffect.HasBeenRenamed = true;
// _profileEditorService.SaveSelectedProfileConfiguration();
// }
// }
//
// public void DeleteEffect()
// {
// if (LayerPropertyGroup.LayerEffect == null)
// return;
//
// LayerPropertyGroup.ProfileElement.RemoveLayerEffect(LayerPropertyGroup.LayerEffect);
// _profileEditorService.SaveSelectedProfileConfiguration();
// }
public void SuspendedToggled()
{
_profileEditorService.SaveSelectedProfileConfiguration();
}
public double GetDepth()
{
int depth = 0;
LayerPropertyGroup current = LayerPropertyGroup.Parent;
while (current != null)
{
depth++;
current = current.Parent;
}
return depth;
}
private void LayerPropertyGroupViewModelOnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(LayerPropertyGroupViewModel.IsExpanded) || e.PropertyName == nameof(LayerPropertyGroupViewModel.IsVisible))
NotifyOfPropertyChange(nameof(Items));
}
private void DetermineGroupType()
{
if (LayerPropertyGroup is LayerGeneralProperties)
GroupType = LayerPropertyGroupType.General;
else if (LayerPropertyGroup is LayerTransformProperties)
GroupType = LayerPropertyGroupType.Transform;
// else if (LayerPropertyGroup.Parent == null && LayerPropertyGroup.LayerBrush != null)
// GroupType = LayerPropertyGroupType.LayerBrushRoot;
// else if (LayerPropertyGroup.Parent == null && LayerPropertyGroup.LayerEffect != null)
// GroupType = LayerPropertyGroupType.LayerEffectRoot;
else
GroupType = LayerPropertyGroupType.None;
}
#region Overrides of Screen
/// <inheritdoc />
protected override void OnInitialActivate()
{
LayerPropertyGroupViewModel.PropertyChanged += LayerPropertyGroupViewModelOnPropertyChanged;
base.OnInitialActivate();
}
/// <inheritdoc />
protected override void OnClose()
{
LayerPropertyGroupViewModel.PropertyChanged -= LayerPropertyGroupViewModelOnPropertyChanged;
// Clean up windows that may be open, sorry but your brush/effect is closed!
_layerBrushSettingsWindowVm?.RequestClose();
_layerEffectSettingsWindowVm?.RequestClose();
base.OnClose();
}
#endregion
}
}
@@ -1,102 +0,0 @@
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree.TreePropertyView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<converters:PropertyTreeMarginConverter Length="20" x:Key="PropertyTreeMarginConverter" />
<Style x:Key="DataBindingsButton" TargetType="shared:LockableToggleButton" BasedOn="{StaticResource MaterialDesignFlatToggleButton}">
<Style.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="IsEnabled">
<DiscreteBooleanKeyFrame KeyTime="00:00:00" Value="False" />
<DiscreteBooleanKeyFrame KeyTime="00:00:00.3" Value="True" />
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<Border Name="Bd"
BorderBrush="{DynamicResource MaterialDesignDivider}"
BorderThickness="0,0,0,1"
Height="25">
<Border.Style>
<Style TargetType="Border">
<Style.Triggers>
<DataTrigger Binding="{Binding LayerPropertyViewModel.IsHighlighted, Mode=OneWay}" Value="True">
<Setter Property="Background" Value="{DynamicResource MaterialDesignPaper}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<Grid Margin="{Binding Converter={StaticResource PropertyTreeMarginConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ToggleButton Grid.Column="0"
Style="{StaticResource MaterialDesignFlatToggleButton}"
ToolTip="Toggle key-framing"
Width="24"
Height="24"
IsChecked="{Binding KeyframesEnabled}"
IsEnabled="{Binding LayerProperty.KeyframesSupported}"
VerticalAlignment="Center" Padding="-25">
<materialDesign:PackIcon Kind="Stopwatch" Height="16" Width="16" />
</ToggleButton>
<TextBlock Grid.Column="1"
Margin="5,0,0,0"
Padding="0,0,5,0"
VerticalAlignment="Center"
TextTrimming="CharacterEllipsis"
Text="{Binding LayerProperty.PropertyDescription.Name}"
ToolTip="{Binding LayerProperty.PropertyDescription.Description}"
HorizontalAlignment="Left" />
<ContentControl Grid.Column="2"
Margin="5 0"
s:View.Model="{Binding PropertyInputViewModel, IsAsync=True}"
ToolTip="{Binding LayerProperty.PropertyDescription.Description}"/>
<Button Grid.Column="3"
Command="{s:Action ResetToDefault}"
Style="{StaticResource MaterialDesignIconButton}"
ToolTip="Reset the property to its default value."
Width="24"
Height="24">
<materialDesign:PackIcon Kind="BackupRestore" Height="16" Width="16" />
</Button>
<shared:LockableToggleButton Grid.Column="4"
Style="{StaticResource DataBindingsButton}"
ToolTip="Change the property's data binding"
Width="24"
Height="24"
VerticalAlignment="Center"
IsLocked="True"
IsEnabled="{Binding LayerProperty.DataBindingsSupported}"
IsChecked="{Binding HasDataBinding, Mode=OneWay}"
Click="{s:Action ActivateDataBindingViewModel}">
<materialDesign:PackIcon Kind="VectorLink" Height="16" Width="16" />
</shared:LockableToggleButton>
</Grid>
</Border>
</UserControl>
@@ -1,145 +0,0 @@
using System;
using System.Linq;
using Artemis.Core;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree
{
public sealed class TreePropertyViewModel<T> : Screen, ITreePropertyViewModel
{
private readonly IProfileEditorService _profileEditorService;
private PropertyInputViewModel<T> _propertyInputViewModel;
public TreePropertyViewModel(LayerProperty<T> layerProperty, LayerPropertyViewModel layerPropertyViewModel, IProfileEditorService profileEditorService)
{
_profileEditorService = profileEditorService;
LayerProperty = layerProperty;
LayerPropertyViewModel = layerPropertyViewModel;
PropertyInputViewModel = _profileEditorService.CreatePropertyInputViewModel(LayerProperty);
PropertyInputViewModel.ConductWith(this);
}
public LayerProperty<T> LayerProperty { get; }
public LayerPropertyViewModel LayerPropertyViewModel { get; }
public PropertyInputViewModel<T> PropertyInputViewModel
{
get => _propertyInputViewModel;
set => SetAndNotify(ref _propertyInputViewModel, value);
}
public bool KeyframesEnabled
{
get => LayerProperty.KeyframesEnabled;
set => ApplyKeyframesEnabled(value);
}
public void ActivateDataBindingViewModel()
{
if (_profileEditorService.SelectedDataBinding == LayerProperty.DataBinding)
_profileEditorService.ChangeSelectedDataBinding(null);
else
_profileEditorService.ChangeSelectedDataBinding(LayerProperty.DataBinding);
}
public void ResetToDefault()
{
LayerProperty.ApplyDefaultValue();
_profileEditorService.SaveSelectedProfileElement();
}
private void ApplyKeyframesEnabled(bool enable)
{
// If enabling keyframes for the first time, add a keyframe with the current value at the current position
if (enable && !LayerProperty.Keyframes.Any())
LayerProperty.AddKeyframe(new LayerPropertyKeyframe<T>(
LayerProperty.CurrentValue,
_profileEditorService.CurrentTime,
Easings.Functions.Linear,
LayerProperty
));
// If disabling keyframes, set the base value to the current value
else if (!enable && LayerProperty.Keyframes.Any())
LayerProperty.BaseValue = LayerProperty.CurrentValue;
LayerProperty.KeyframesEnabled = enable;
_profileEditorService.SaveSelectedProfileElement();
}
public bool HasDataBinding => LayerProperty.HasDataBinding;
public double GetDepth()
{
int depth = 0;
LayerPropertyGroup current = LayerProperty.LayerPropertyGroup;
while (current != null)
{
depth++;
current = current.Parent;
}
return depth;
}
#region Overrides of Screen
/// <inheritdoc />
protected override void OnInitialActivate()
{
_profileEditorService.SelectedDataBindingChanged += ProfileEditorServiceOnSelectedDataBindingChanged;
LayerProperty.VisibilityChanged += LayerPropertyOnVisibilityChanged;
LayerProperty.DataBinding.DataBindingEnabled += LayerPropertyOnDataBindingChange;
LayerProperty.DataBinding.DataBindingDisabled += LayerPropertyOnDataBindingChange;
LayerProperty.KeyframesToggled += LayerPropertyOnKeyframesToggled;
LayerPropertyViewModel.IsVisible = !LayerProperty.IsHidden;
base.OnInitialActivate();
}
/// <inheritdoc />
protected override void OnClose()
{
_profileEditorService.SelectedDataBindingChanged -= ProfileEditorServiceOnSelectedDataBindingChanged;
LayerProperty.VisibilityChanged -= LayerPropertyOnVisibilityChanged;
LayerProperty.DataBinding.DataBindingEnabled -= LayerPropertyOnDataBindingChange;
LayerProperty.DataBinding.DataBindingDisabled -= LayerPropertyOnDataBindingChange;
LayerProperty.KeyframesToggled -= LayerPropertyOnKeyframesToggled;
base.OnClose();
}
#endregion
#region Event handlers
private void ProfileEditorServiceOnSelectedDataBindingChanged(object sender, EventArgs e)
{
LayerPropertyViewModel.IsHighlighted = _profileEditorService.SelectedDataBinding == LayerProperty.DataBinding;
}
private void LayerPropertyOnVisibilityChanged(object sender, LayerPropertyEventArgs e)
{
LayerPropertyViewModel.IsVisible = !LayerProperty.IsHidden;
}
private void LayerPropertyOnDataBindingChange(object sender, DataBindingEventArgs e)
{
NotifyOfPropertyChange(nameof(HasDataBinding));
}
private void LayerPropertyOnKeyframesToggled(object sender, LayerPropertyEventArgs e)
{
NotifyOfPropertyChange(nameof(KeyframesEnabled));
}
#endregion
}
public interface ITreePropertyViewModel : IScreen
{
bool HasDataBinding { get; }
double GetDepth();
}
}
@@ -1,29 +0,0 @@
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree.TreeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:dd="urn:gong-wpf-dragdrop"
xmlns:tree="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree"
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance {x:Type tree:TreeViewModel}}">
<ItemsControl ItemsSource="{Binding LayerPropertyGroups}"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"
HorizontalContentAlignment="Stretch"
HorizontalAlignment="Stretch"
Background="{DynamicResource MaterialDesignToolBarBackground}"
dd:DragDrop.IsDragSource="True"
dd:DragDrop.IsDropTarget="True"
dd:DragDrop.DropHandler="{Binding LayerPropertiesViewModel}">
<ItemsControl.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:LayerPropertyGroupViewModel}" ItemsSource="{Binding Children}">
<ContentControl s:View.Model="{Binding TreeGroupViewModel}" />
</HierarchicalDataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</UserControl>
@@ -1,36 +0,0 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree
{
public class TreeViewModel : Screen
{
public TreeViewModel(LayerPropertiesViewModel layerPropertiesViewModel, IObservableCollection<LayerPropertyGroupViewModel> layerPropertyGroups)
{
LayerPropertiesViewModel = layerPropertiesViewModel;
// Not using the Items collection because the list should persist even after this VM gets closed
LayerPropertyGroups = layerPropertyGroups;
}
public LayerPropertiesViewModel LayerPropertiesViewModel { get; }
public IObservableCollection<LayerPropertyGroupViewModel> LayerPropertyGroups { get; }
public void PropertyTreePreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
if (e.Handled || !(sender is System.Windows.Controls.TreeView))
return;
e.Handled = true;
MouseWheelEventArgs eventArg = new(e.MouseDevice, e.Timestamp, e.Delta)
{
RoutedEvent = UIElement.MouseWheelEvent,
Source = sender
};
UIElement parent = ((Control)sender).Parent as UIElement;
parent?.RaiseEvent(eventArg);
}
}
}
@@ -0,0 +1,217 @@
<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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.MenuBar.MenuBarView"
x:DataType="menuBar:MenuBarViewModel">
<Menu VerticalAlignment="Top">
<MenuItem Header="_File">
<MenuItem Header="New">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Plus" />
</MenuItem.Icon>
<MenuItem Header="Folder" Command="{Binding AddFolder}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Folder" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Layer" Command="{Binding AddLayer}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Layers" />
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<Separator />
<MenuItem Header="View Properties" Command="{Binding ViewProperties}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Settings" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="_View Scripts" Command="{Binding ViewScripts}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="BookEdit" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Adapt Profile" Command="{Binding AdaptProfile}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Magic" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Suspend Profile" IsSelected="{CompiledBinding IsSuspended}">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsHitTestVisible="False"
IsChecked="{CompiledBinding IsSuspended}"/>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Export Profile" Command="{Binding ExportProfile}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Export" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Duplicate Profile" Command="{Binding DuplicateProfile}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ContentDuplicate" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Delete Profile" Command="{Binding DeleteProfile}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Trash" />
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<MenuItem Header="_Edit" SubmenuOpened="MenuItem_OnSubmenuOpened">
<MenuItem Header="_Undo"
Command="{Binding History.Undo}"
HotKey="Ctrl+Z"
InputGesture="Ctrl+Z">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Undo" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="_Redo"
Command="{Binding History.Redo}"
HotKey="Ctrl+Y"
InputGesture="Ctrl+Y">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Redo" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="_Duplicate"
Command="{Binding Duplicate}"
HotKey="Ctrl+D"
IsEnabled="{Binding HasSelectedElement}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ContentDuplicate" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="_Copy"
Command="{Binding Copy}"
HotKey="Ctrl+C"
IsEnabled="{Binding HasSelectedElement}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ContentCopy" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="_Paste"
Command="{Binding Paste}"
HotKey="Ctrl+V">
<MenuItem.Icon>
<avalonia:MaterialIconExt Kind="ContentPaste" />
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<MenuItem Header="_Run">
<MenuItem Header="_Switch run mode"
Command="{Binding ToggleSuspend}"
HotKey="F5">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="SwapHorizontal" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Run Profile on Focus Loss"
ToolTip.Tip="If enabled, run mode is set to normal on focus loss"
HotKey="Shift+F5">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsHitTestVisible="False"
IsChecked="{Binding StopOnFocusLoss.Value}" />
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<MenuItem Header="_Options">
<MenuItem Header="Focus Selected Layer"
ToolTip.Tip="If enabled, displays only the layer you currently have selected"
IsEnabled="False">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsHitTestVisible="False"
IsChecked="{Binding FocusSelectedLayer.Value}" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Display Data Model Values">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsHitTestVisible="False"
IsChecked="{Binding ShowDataModelValues.Value}" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Display Full Condition Paths">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsHitTestVisible="False"
IsChecked="{Binding 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">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsHitTestVisible="False"
IsChecked="{Binding 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">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsHitTestVisible="False"
IsChecked="{Binding AlwaysApplyDataBindings.Value}" />
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<MenuItem Header="_Help">
<MenuItem Header="Artemis Wiki" Command="{Binding OpenUrl}" CommandParameter="https://wiki.artemis-rgb.com/">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="BookEdit" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Editor" Command="{Binding OpenUrl}" CommandParameter="https://wiki.artemis-rgb.com/en/guides/user/profiles">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Edit" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Layers" Command="{Binding OpenUrl}" CommandParameter="https://wiki.artemis-rgb.com/guides/user/profiles/layers">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Layers" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Display Conditions" Command="{Binding OpenUrl}" CommandParameter="https://wiki.artemis-rgb.com/guides/user/profiles/conditions">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="NotEqual" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Timeline" Command="{Binding OpenUrl}" CommandParameter="https://wiki.artemis-rgb.com/guides/user/profiles/timeline">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Stopwatch" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Data Bindings" Command="{Binding OpenUrl}" CommandParameter="https://wiki.artemis-rgb.com/guides/user/profiles/data-bindings">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="VectorLink" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Scripting" Command="{Binding OpenUrl}" CommandParameter="https://wiki.artemis-rgb.com/guides/user/profiles/scripting">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="CodeJson" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Report a Bug" Command="{Binding OpenUrl}" CommandParameter="https://github.com/Artemis-RGB/Artemis/issues">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Github" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Get Help on Discord" Command="{Binding OpenUrl}" CommandParameter="https://discord.gg/S3MVaC9">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Discord" />
</MenuItem.Icon>
</MenuItem>
</MenuItem>
</Menu>
</UserControl>
@@ -0,0 +1,24 @@
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.MenuBar
{
public partial class MenuBarView : ReactiveUserControl<MenuBarViewModel>
{
public MenuBarView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void MenuItem_OnSubmenuOpened(object? sender, RoutedEventArgs e)
{
}
}
}
@@ -0,0 +1,54 @@
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.MenuBar;
public class MenuBarViewModel : ActivatableViewModelBase
{
private readonly IProfileService _profileService;
private ProfileEditorHistory? _history;
private ObservableAsPropertyHelper<ProfileConfiguration?>? _profileConfiguration;
private ObservableAsPropertyHelper<bool>? _isSuspended;
public MenuBarViewModel(IProfileEditorService profileEditorService, IProfileService profileService)
{
_profileService = profileService;
this.WhenActivated(d =>
{
profileEditorService.History.Subscribe(history => History = history).DisposeWith(d);
_profileConfiguration = profileEditorService.ProfileConfiguration.ToProperty(this, vm => vm.ProfileConfiguration);
_isSuspended = profileEditorService.ProfileConfiguration
.Select(p => p?.WhenAnyValue(c => c.IsSuspended) ?? Observable.Never<bool>())
.Switch()
.ToProperty(this, vm => vm.IsSuspended)
.DisposeWith(d);
});
}
public ProfileEditorHistory? History
{
get => _history;
set => RaiseAndSetIfChanged(ref _history, value);
}
public ProfileConfiguration? ProfileConfiguration => _profileConfiguration?.Value;
public bool IsSuspended
{
get => _isSuspended?.Value ?? false;
set
{
if (ProfileConfiguration == null)
return;
ProfileConfiguration.IsSuspended = value;
_profileService.SaveProfileCategory(ProfileConfiguration.Category);
}
}
}
@@ -0,0 +1,65 @@
<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">
<DockPanel Margin="8 0">
<StackPanel Orientation="Horizontal" Spacing="5">
<Button Classes="icon-button icon-button-large"
ToolTip.Tip="Play from start (Shift+Space)" Command="{Binding PlayFromStart}"
Focusable="False">
<avalonia:MaterialIcon Kind="StepForward" />
</Button>
<Button Classes="icon-button icon-button-large"
ToolTip.Tip="Toggle play/pause (Space)" Command="{Binding 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="{Binding GoToStart}" Focusable="False">
<avalonia:MaterialIcon Kind="SkipBackward" />
</Button>
<Button Classes="icon-button icon-button-large" ToolTip.Tip="Go to end" Command="{Binding GoToEnd}" Focusable="False">
<avalonia:MaterialIcon Kind="SkipForward" />
</Button>
<Button Classes="icon-button icon-button-large" ToolTip.Tip="Previous frame" Command="{Binding GoToPreviousFrame}" Focusable="False">
<avalonia:MaterialIcon Kind="SkipPrevious" />
</Button>
<Button Classes="icon-button icon-button-large" ToolTip.Tip="Next frame" Command="{Binding GoToNextFrame}" Focusable="False">
<avalonia:MaterialIcon Kind="SkipNext" />
</Button>
<ToggleButton Classes="icon-button icon-button-large"
IsChecked="{CompiledBinding Repeating}"
Focusable="False"
Command="{Binding 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 Classes="h4" Text="{CompiledBinding FormattedCurrentTime, FallbackValue=00.000}" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0"/>
</DockPanel>
</UserControl>
@@ -0,0 +1,18 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Playback
{
public partial class PlaybackView : ReactiveUserControl<PlaybackViewModel>
{
public PlaybackView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}
@@ -0,0 +1,205 @@
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Avalonia.Threading;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Playback;
public class PlaybackViewModel : ActivatableViewModelBase
{
private readonly IProfileEditorService _profileEditorService;
private readonly ISettingsService _settingsService;
private RenderProfileElement? _profileElement;
private ObservableAsPropertyHelper<TimeSpan>? _currentTime;
private ObservableAsPropertyHelper<string?>? _formattedCurrentTime;
private ObservableAsPropertyHelper<bool>? _playing;
private bool _repeating;
private bool _repeatTimeline;
private bool _repeatSegment;
private DateTime _lastUpdate;
public PlaybackViewModel(IProfileEditorService profileEditorService, ISettingsService settingsService)
{
_profileEditorService = profileEditorService;
_settingsService = settingsService;
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);
_lastUpdate = DateTime.MinValue;
DispatcherTimer updateTimer = new(TimeSpan.FromMilliseconds(60.0 / 1000), DispatcherPriority.Render, Update);
updateTimer.Start();
Disposable.Create(() => updateTimer.Stop());
});
}
public TimeSpan CurrentTime => _currentTime?.Value ?? TimeSpan.Zero;
public string? FormattedCurrentTime => _formattedCurrentTime?.Value;
public bool Playing => _playing?.Value ?? false;
public bool Repeating
{
get => _repeating;
set => RaiseAndSetIfChanged(ref _repeating, value);
}
public bool RepeatTimeline
{
get => _repeatTimeline;
set => RaiseAndSetIfChanged(ref _repeatTimeline, value);
}
public bool RepeatSegment
{
get => _repeatSegment;
set => RaiseAndSetIfChanged(ref _repeatSegment, value);
}
public void PlayFromStart()
{
GoToStart();
if (!Playing)
_profileEditorService.Play();
}
public void TogglePlay()
{
if (!Playing)
_profileEditorService.Play();
else
_profileEditorService.Pause();
}
public void GoToStart()
{
_profileEditorService.ChangeTime(TimeSpan.Zero);
}
public void GoToEnd()
{
if (_profileElement == null)
return;
_profileEditorService.ChangeTime(_profileElement.Timeline.EndSegmentEndPosition);
}
public void GoToPreviousFrame()
{
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));
}
public void GoToNextFrame()
{
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));
}
public void CycleRepeating()
{
if (!Repeating)
{
RepeatTimeline = true;
RepeatSegment = false;
Repeating = true;
}
else if (RepeatTimeline)
{
RepeatTimeline = false;
RepeatSegment = true;
}
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(object? sender, EventArgs e)
{
try
{
if (!Playing)
return;
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();
}
}
_profileEditorService.ChangeTime(newTime);
}
finally
{
_lastUpdate = DateTime.Now;
}
}
}
@@ -0,0 +1,186 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Generators;
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 IControl && 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 IControl && 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);
IVisual? targetVisual = treeView.GetVisualAt(position).FindAncestorOfType<TreeViewItem>();
if (sourceContext is not T sourceNode || targetContext is not ProfileTreeViewModel vm || targetVisual is not IControl {DataContext: T targetNode})
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)
{
if (position.Y > targetVisual.Bounds.Top + targetVisual.Bounds.Height / 2)
dropType = TreeDropType.After;
}
else
{
IVisual? header = targetVisual.GetVisualDescendants().FirstOrDefault(d => d is Border b && b.Name == "PART_LayoutRoot");
if (header != null)
{
double segments = header.Bounds.Height / 3.0;
if (position.Y > targetVisual.Bounds.Top + segments * 2)
dropType = TreeDropType.After;
else if (position.Y > targetVisual.Bounds.Top + 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((IControl) targetVisual, dropType);
}
return true;
}
private List<TreeViewItem> GetFlattenedTreeView(ItemsControl currentNode)
{
List<TreeViewItem> result = new();
foreach (ItemContainerInfo containerInfo in currentNode.ItemContainerGenerator.Containers)
{
if (containerInfo.ContainerControl is TreeViewItem treeViewItem && containerInfo.Item is TreeItemViewModel)
{
result.Add(treeViewItem);
if (treeViewItem.ItemContainerGenerator.Containers.Any())
result.AddRange(GetFlattenedTreeView(treeViewItem));
}
}
return result;
}
private void SetDraggingPseudoClasses(IControl 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
}
}
@@ -0,0 +1,48 @@
<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"
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">
<Button Grid.Column="0"
ToolTip.Tip="{CompiledBinding ProfileElement.BrokenState}"
IsVisible="{CompiledBinding ProfileElement.BrokenState, Converter={x:Static ObjectConverters.IsNotNull}}"
Classes="icon-button icon-button-small"
Foreground="White"
Background="#E74C4C"
BorderBrush="#E74C4C"
Margin="0 0 5 0"
Command="{Binding ShowBrokenStateExceptions}">
<avalonia:MaterialIcon Kind="AlertCircle" />
</Button>
<avalonia:MaterialIcon Grid.Column="1"
Kind="Folder"
Margin="0 0 5 0"
IsVisible="{Binding !IsExpanded, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TreeViewItem}}}" />
<avalonia:MaterialIcon Grid.Column="1"
Kind="FolderOpen"
Margin="0 0 5 0"
IsVisible="{Binding IsExpanded, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TreeViewItem}}}" />
<TextBox Grid.Column="2"
Margin="-5 0 0 0"
Classes="condensed"
IsVisible="{CompiledBinding Renaming}"
Text="{CompiledBinding RenameValue}"
x:Name="Input"
KeyUp="InputElement_OnKeyUp"
LostFocus="InputElement_OnLostFocus"/>
<TextBlock Grid.Column="2" IsVisible="{CompiledBinding !Renaming}" Text="{Binding Folder.Name}" VerticalAlignment="Center" />
<ToggleButton Grid.Column="3"
Classes="icon-button icon-button-small"
ToolTip.Tip="Toggle suspended state"
IsChecked="{CompiledBinding Folder.Suspended}"
VerticalAlignment="Center"
Margin="4 0">
<avalonia:MaterialIcon Kind="Pause" />
</ToggleButton>
</Grid>
</UserControl>
@@ -0,0 +1,40 @@
using System;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree;
public class FolderTreeItemView : ReactiveUserControl<FolderTreeItemViewModel>
{
public FolderTreeItemView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
this.WhenActivated(_ => ViewModel?.Rename.Subscribe(_ =>
{
this.Get<TextBox>("Input").Focus();
this.Get<TextBox>("Input").SelectAll();
}));
}
private void InputElement_OnKeyUp(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
ViewModel?.SubmitRename();
else if (e.Key == Key.Escape)
ViewModel?.CancelRename();
}
private void InputElement_OnLostFocus(object? sender, RoutedEventArgs e)
{
ViewModel?.CancelRename();
}
}
@@ -0,0 +1,33 @@
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Shared.Services.Interfaces;
using Artemis.UI.Shared.Services.ProfileEditor;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree
{
public class FolderTreeItemViewModel : TreeItemViewModel
{
public FolderTreeItemViewModel(TreeItemViewModel? parent,
Folder folder,
IWindowService windowService,
IProfileEditorService profileEditorService,
ILayerBrushService layerBrushService,
IProfileEditorVmFactory profileEditorVmFactory,
IRgbService rgbService)
: base(parent, folder, windowService, profileEditorService, rgbService, layerBrushService, profileEditorVmFactory)
{
Folder = folder;
}
public Folder Folder { get; }
#region Overrides of TreeItemViewModel
/// <inheritdoc />
public override bool SupportsChildren => true;
#endregion
}
}
@@ -0,0 +1,39 @@
<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"
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">
<Button Grid.Column="0"
ToolTip.Tip="{CompiledBinding ProfileElement.BrokenState}"
IsVisible="{CompiledBinding ProfileElement.BrokenState, Converter={x:Static ObjectConverters.IsNotNull}}"
Classes="icon-button icon-button-small"
Foreground="#E74C4C"
Margin="0 0 5 0"
Command="{Binding ShowBrokenStateExceptions}">
<avalonia:MaterialIcon Kind="AlertCircle" />
</Button>
<avalonia:MaterialIcon Grid.Column="1" Kind="{CompiledBinding Layer.LayerBrush.Descriptor.Icon, FallbackValue=Layers}" Margin="0 0 5 0" />
<TextBox Grid.Column="2"
Margin="-5 0 0 0"
Classes="condensed"
x:Name="Input"
IsVisible="{CompiledBinding Renaming}"
Text="{CompiledBinding RenameValue}"
KeyUp="InputElement_OnKeyUp"
LostFocus="InputElement_OnLostFocus"></TextBox>
<TextBlock Grid.Column="2" IsVisible="{CompiledBinding !Renaming}" Text="{CompiledBinding Layer.Name}" VerticalAlignment="Center" />
<ToggleButton Grid.Column="3"
Classes="icon-button icon-button-small"
ToolTip.Tip="Toggle suspended state"
IsChecked="{CompiledBinding Layer.Suspended}"
VerticalAlignment="Center"
Margin="4 0">
<avalonia:MaterialIcon Kind="Pause" />
</ToggleButton>
</Grid>
</UserControl>
@@ -0,0 +1,40 @@
using System;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree;
public class LayerTreeItemView : ReactiveUserControl<LayerTreeItemViewModel>
{
public LayerTreeItemView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
this.WhenActivated(_ => ViewModel?.Rename.Subscribe(_ =>
{
this.Get<TextBox>("Input").Focus();
this.Get<TextBox>("Input").SelectAll();
}));
}
private void InputElement_OnKeyUp(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
ViewModel?.SubmitRename();
else if (e.Key == Key.Escape)
ViewModel?.CancelRename();
}
private void InputElement_OnLostFocus(object? sender, RoutedEventArgs e)
{
ViewModel?.CancelRename();
}
}
@@ -0,0 +1,32 @@
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Shared.Services.Interfaces;
using Artemis.UI.Shared.Services.ProfileEditor;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree
{
public class LayerTreeItemViewModel : TreeItemViewModel
{
public LayerTreeItemViewModel(TreeItemViewModel? parent,
Layer layer,
IWindowService windowService,
IProfileEditorService profileEditorService,
IRgbService rgbService,
ILayerBrushService layerBrushService,
IProfileEditorVmFactory profileEditorVmFactory)
: base(parent, layer, windowService, profileEditorService, rgbService, layerBrushService, profileEditorVmFactory)
{
Layer = layer;
}
public Layer Layer { get; }
#region Overrides of TreeItemViewModel
/// <inheritdoc />
public override bool SupportsChildren => false;
#endregion
}
}
@@ -0,0 +1,157 @@
<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">
<UserControl.Resources>
<converters:ColorOpacityConverter x:Key="ColorOpacityConverter" />
</UserControl.Resources>
<UserControl.Styles>
<StyleInclude Source="avares://Avalonia.Xaml.Interactions/Draggable/Styles.axaml" />
<Style Selector="TreeView#ProfileTreeView">
<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#ProfileTreeView TreeViewItem">
<Setter Property="IsExpanded" Value="True" />
<Setter Property="(Interaction.Behaviors)">
<BehaviorCollectionTemplate>
<BehaviorCollection>
<behaviors:SimpleContextDragBehavior />
</BehaviorCollection>
</BehaviorCollectionTemplate>
</Setter>
</Style>
<Style Selector="TreeView#ProfileTreeView 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="{Binding Source={StaticResource SystemAccentColorLight3}, Converter={StaticResource ColorOpacityConverter}, ConverterParameter=0.25}" Offset="0.05" />
<GradientStop Color="{Binding Source={StaticResource SystemAccentColorLight3}, Converter={StaticResource ColorOpacityConverter}, ConverterParameter=0}" Offset="0.25" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Style>
<Style Selector="TreeView#ProfileTreeView 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="{Binding Source={StaticResource SystemAccentColorLight3}, Converter={StaticResource ColorOpacityConverter}, ConverterParameter=0.25}" Offset="0.05" />
<GradientStop Color="{Binding Source={StaticResource SystemAccentColorLight3}, Converter={StaticResource ColorOpacityConverter}, ConverterParameter=0}" Offset="0.25" />
<GradientStop Color="{Binding Source={StaticResource SystemAccentColorLight3}, Converter={StaticResource ColorOpacityConverter}, ConverterParameter=0}" Offset="0.75" />
<GradientStop Color="{Binding 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#ProfileTreeView TreeViewItem:dragging-after">
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,28">
<GradientStop Color="{Binding Source={StaticResource SystemAccentColorLight3}, Converter={StaticResource ColorOpacityConverter}, ConverterParameter=0}" Offset="0.75" />
<GradientStop Color="{Binding 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="ProfileTreeView" Classes="no-right-margin draggable" Items="{CompiledBinding Children}" SelectedItem="{CompiledBinding SelectedChild}"
SelectionChanged="ProfileTreeView_OnSelectionChanged">
<TreeView.Styles>
<Style Selector="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.Styles>
<TreeView.KeyBindings>
<KeyBinding Gesture="Escape" Command="{CompiledBinding ClearSelection}" />
<KeyBinding Gesture="F2" Command="{CompiledBinding SelectedChild.Rename}" />
<KeyBinding Gesture="Delete" Command="{CompiledBinding SelectedChild.Delete}" />
<KeyBinding Gesture="Ctrl+D" Command="{CompiledBinding SelectedChild.Duplicate}" />
<KeyBinding Gesture="Ctrl+C" Command="{CompiledBinding SelectedChild.Copy}" />
<KeyBinding Gesture="Ctrl+V" Command="{CompiledBinding SelectedChild.Paste}" />
</TreeView.KeyBindings>
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Children}">
<ContentControl Content="{Binding}" x:DataType="profileTree:TreeItemViewModel">
<ContentControl.ContextFlyout>
<MenuFlyout>
<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>
<Separator />
<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>
<Separator />
<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>
<StackPanel Grid.Row="1" Orientation="Horizontal" 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>
</UserControl>
@@ -0,0 +1,128 @@
using System;
using System.Diagnostics;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.ReactiveUI;
using Avalonia.VisualTree;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree;
public class ProfileTreeView : ReactiveUserControl<ProfileTreeViewModel>
{
private TreeView _treeView;
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(PointerEnterEvent, 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)
{
if (_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(this.FindAncestorOfType<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)
{
if (_dragAdorner == null)
return;
Point position = e.GetPosition(this.FindAncestorOfType<Window>());
_dragAdorner.RenderTransform = new TranslateTransform(_dragStartPosition.X - _elementDragOffset.X, position.Y - _elementDragOffset.Y);
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
_treeView = this.Get<TreeView>("ProfileTreeView");
}
private void ProfileTreeView_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
_treeView.Focus();
}
}
@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Shared.Services.Interfaces;
using Artemis.UI.Shared.Services.ProfileEditor;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree
{
public class ProfileTreeViewModel : TreeItemViewModel
{
private TreeItemViewModel? _selectedChild;
public ProfileTreeViewModel(IWindowService windowService,
IProfileEditorService profileEditorService,
ILayerBrushService layerBrushService,
IProfileEditorVmFactory profileEditorVmFactory,
IRgbService rgbService)
: base(null, null, windowService, profileEditorService, rgbService, layerBrushService, 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);
});
this.WhenAnyValue(vm => vm.SelectedChild).Subscribe(model =>
{
if (model?.ProfileElement is RenderProfileElement renderProfileElement)
profileEditorService.ChangeCurrentProfileElement(renderProfileElement);
});
ClearSelection = ReactiveCommand.Create(() => profileEditorService.ChangeCurrentProfileElement(null));
}
public ReactiveCommand<Unit, Unit> ClearSelection { get; }
public TreeItemViewModel? SelectedChild
{
get => _selectedChild;
set => RaiseAndSetIfChanged(ref _selectedChild, value);
}
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;
}
public override bool SupportsChildren => true;
}
}
@@ -0,0 +1,239 @@
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.Ninject.Factories;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.Interfaces;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree;
public abstract class TreeItemViewModel : ActivatableViewModelBase
{
private readonly IProfileEditorService _profileEditorService;
private readonly IProfileEditorVmFactory _profileEditorVmFactory;
private readonly IWindowService _windowService;
private RenderProfileElement? _currentProfileElement;
private bool _isExpanded;
private ProfileElement? _profileElement;
private string? _renameValue;
private bool _renaming;
protected TreeItemViewModel(TreeItemViewModel? parent, ProfileElement? profileElement,
IWindowService windowService,
IProfileEditorService profileEditorService,
IRgbService rgbService,
ILayerBrushService layerBrushService,
IProfileEditorVmFactory profileEditorVmFactory)
{
_windowService = windowService;
_profileEditorService = profileEditorService;
_profileEditorVmFactory = profileEditorVmFactory;
Parent = parent;
ProfileElement = profileElement;
AddLayer = ReactiveCommand.Create(() =>
{
if (ProfileElement is Layer targetLayer)
{
Layer layer = new(targetLayer.Parent, targetLayer.GetNewLayerName());
layerBrushService.ApplyDefaultBrush(layer);
layer.AddLeds(rgbService.EnabledDevices.SelectMany(d => d.Leds));
profileEditorService.ExecuteCommand(new AddProfileElement(layer, targetLayer.Parent, targetLayer.Order));
}
else if (ProfileElement != null)
{
Layer layer = new(ProfileElement, ProfileElement.GetNewLayerName());
layerBrushService.ApplyDefaultBrush(layer);
layer.AddLeds(rgbService.EnabledDevices.SelectMany(d => d.Leds));
profileEditorService.ExecuteCommand(new AddProfileElement(layer, ProfileElement, 0));
}
});
AddFolder = ReactiveCommand.Create(() =>
{
if (ProfileElement is Layer targetLayer)
profileEditorService.ExecuteCommand(new AddProfileElement(new Folder(targetLayer.Parent, targetLayer.Parent.GetNewFolderName()), targetLayer.Parent, targetLayer.Order));
else if (ProfileElement != null)
profileEditorService.ExecuteCommand(new AddProfileElement(new Folder(ProfileElement, ProfileElement.GetNewFolderName()), ProfileElement, 0));
});
Rename = ReactiveCommand.Create(() =>
{
Renaming = true;
RenameValue = ProfileElement?.Name;
});
Duplicate = ReactiveCommand.Create(() => throw new NotImplementedException());
Copy = ReactiveCommand.Create(() => throw new NotImplementedException());
Paste = ReactiveCommand.Create(() => throw new NotImplementedException());
Delete = ReactiveCommand.Create(() =>
{
if (ProfileElement is RenderProfileElement renderProfileElement)
profileEditorService.ExecuteCommand(new RemoveProfileElement(renderProfileElement));
});
this.WhenActivated(d =>
{
_profileEditorService.ProfileElement.Subscribe(element => _currentProfileElement = element).DisposeWith(d);
SubscribeToProfileElement(d);
CreateTreeItems();
});
}
public ProfileElement? ProfileElement
{
get => _profileElement;
set => RaiseAndSetIfChanged(ref _profileElement, value);
}
public bool IsExpanded
{
get => _isExpanded;
set => RaiseAndSetIfChanged(ref _isExpanded, value);
}
public bool Renaming
{
get => _renaming;
set => RaiseAndSetIfChanged(ref _renaming, value);
}
public TreeItemViewModel? Parent { get; set; }
public ObservableCollection<TreeItemViewModel> Children { get; } = new();
public ReactiveCommand<Unit, Unit> AddLayer { get; }
public ReactiveCommand<Unit, Unit> AddFolder { get; }
public ReactiveCommand<Unit, Unit> 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 string? RenameValue
{
get => _renameValue;
set => RaiseAndSetIfChanged(ref _renameValue, value);
}
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)
if (!await _windowService.ShowConfirmContentDialog("Broken state", "Do you want to view the next exception?"))
return;
}
}
public void SubmitRename()
{
if (ProfileElement == null)
{
Renaming = false;
return;
}
_profileEditorService.ExecuteCommand(new RenameProfileElement(ProfileElement, RenameValue));
Renaming = false;
}
public void CancelRename()
{
Renaming = false;
}
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 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);
}
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));
}
}
}
@@ -0,0 +1,147 @@
<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"></converters: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}" />
<ScrollViewer Grid.Row="1"
Grid.Column="0"
Name="TreeScrollViewer"
Offset="{CompiledBinding #TimelineScrollViewer.Offset, Mode=OneWay}"
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}">
<ItemsControl Items="{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>
</ScrollViewer>
</Grid>
<GridSplitter Grid.Column="1"
Cursor="SizeWestEast"
Foreground="Transparent"
Background="Transparent"
Margin="0 0 -5 0" />
<!-- Horizontal scrolling -->
<ScrollViewer Grid.Column="2" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
<Grid RowDefinitions="48,*">
<!-- Timeline header body -->
<controls:TimelineHeader Grid.Row="0"
Name="TimelineHeader"
Margin="0 18 0 0"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
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>
<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>
<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>
<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">
</Line>
<!-- 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}">
</Polygon>
</Canvas>
<ScrollViewer Grid.Row="1"
Name="TimelineScrollViewer"
Offset="{CompiledBinding #TreeScrollViewer.Offset, Mode=OneWay}"
VerticalScrollBarVisibility="Hidden">
<ContentControl Content="{CompiledBinding TimelineViewModel}"
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}" />
</ScrollViewer>
<!-- TODO: Databindings here -->
</Grid>
</ScrollViewer>
</Grid>
</UserControl>
@@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using Avalonia.VisualTree;
namespace Artemis.UI.Screens.ProfileEditor.Properties;
public class PropertiesView : ReactiveUserControl<PropertiesViewModel>
{
private readonly Polygon _timelineCaret;
private readonly Line _timelineLine;
public PropertiesView()
{
InitializeComponent();
_timelineCaret = this.Get<Polygon>("TimelineCaret");
_timelineLine = this.Get<Line>("TimelineLine");
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
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;
IInputElement? senderElement = (IInputElement?) sender;
if (senderElement == null)
return;
// Get the parent grid, need that for our position
IVisual? parent = senderElement.VisualParent;
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 IInputElement senderElement)
return;
// Get the parent grid, need that for our position
double x = Math.Max(0, e.GetPosition(senderElement.VisualParent).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));
}
}
@@ -0,0 +1,131 @@
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 Artemis.Core;
using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects;
using Artemis.Core.Services;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.ProfileEditor.Playback;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties;
public class PropertiesViewModel : ActivatableViewModelBase
{
private readonly Dictionary<LayerPropertyGroup, PropertyGroupViewModel> _cachedViewModels;
private readonly ILayerPropertyVmFactory _layerPropertyVmFactory;
private readonly IProfileEditorService _profileEditorService;
private readonly ISettingsService _settingsService;
private ObservableAsPropertyHelper<int>? _pixelsPerSecond;
private ObservableAsPropertyHelper<RenderProfileElement?>? _profileElement;
/// <inheritdoc />
public PropertiesViewModel(IProfileEditorService profileEditorService, ISettingsService settingsService, ILayerPropertyVmFactory layerPropertyVmFactory, PlaybackViewModel playbackViewModel)
{
_profileEditorService = profileEditorService;
_settingsService = settingsService;
_layerPropertyVmFactory = layerPropertyVmFactory;
_cachedViewModels = new Dictionary<LayerPropertyGroup, PropertyGroupViewModel>();
PropertyGroupViewModels = new ObservableCollection<PropertyGroupViewModel>();
PlaybackViewModel = playbackViewModel;
TimelineViewModel = layerPropertyVmFactory.TimelineViewModel(PropertyGroupViewModels);
// Subscribe to events of the latest selected profile element - borrowed from https://stackoverflow.com/a/63950940
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()
.Subscribe(_ => UpdateGroups());
this.WhenAnyValue(vm => vm.ProfileElement)
.Select(p => p != null
? Observable.FromEventPattern(x => p.LayerEffectsUpdated += x, x => p.LayerEffectsUpdated -= x)
: Observable.Never<EventPattern<object>>())
.Switch()
.Subscribe(_ => UpdateGroups());
// 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);
Disposable.Create(() => _settingsService.SaveAllSettings()).DisposeWith(d);
});
this.WhenAnyValue(vm => vm.ProfileElement).Subscribe(_ => UpdateGroups());
}
public ObservableCollection<PropertyGroupViewModel> PropertyGroupViewModels { get; }
public PlaybackViewModel PlaybackViewModel { get; }
public TimelineViewModel TimelineViewModel { get; }
public RenderProfileElement? ProfileElement => _profileElement?.Value;
public Layer? Layer => _profileElement?.Value as Layer;
public int PixelsPerSecond => _pixelsPerSecond?.Value ?? 0;
public IObservable<bool> Playing => _profileEditorService.Playing;
public PluginSetting<double> PropertiesTreeWidth => _settingsService.GetSetting("ProfileEditor.PropertiesTreeWidth", 500.0);
private void UpdateGroups()
{
if (ProfileElement == null)
{
PropertyGroupViewModels.Clear();
return;
}
ObservableCollection<PropertyGroupViewModel> viewModels = new();
if (Layer != null)
{
// Add base VMs
viewModels.Add(GetOrCreateViewModel(Layer.General, null, null));
viewModels.Add(GetOrCreateViewModel(Layer.Transform, null, null));
// Add brush VM if the brush has properties
if (Layer.LayerBrush?.BaseProperties != null)
viewModels.Add(GetOrCreateViewModel(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(GetOrCreateViewModel(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 GetOrCreateViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerBrush? layerBrush, BaseLayerEffect? layerEffect)
{
if (_cachedViewModels.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);
_cachedViewModels[layerPropertyGroup] = createdVm;
return createdVm;
}
}
@@ -0,0 +1,140 @@
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.Ninject.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;
namespace Artemis.UI.Screens.ProfileEditor.Properties;
public class PropertyGroupViewModel : ViewModelBase, IDisposable
{
private readonly ILayerPropertyVmFactory _layerPropertyVmFactory;
private readonly IPropertyInputService _propertyInputService;
private bool _hasChildren;
private bool _isExpanded;
private bool _isVisible;
public PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService)
{
_layerPropertyVmFactory = layerPropertyVmFactory;
_propertyInputService = propertyInputService;
Children = new ObservableCollection<ViewModelBase>();
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)
: this(layerPropertyGroup, layerPropertyVmFactory, propertyInputService)
{
LayerBrush = layerBrush;
}
public PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService,
BaseLayerEffect layerEffect)
: this(layerPropertyGroup, layerPropertyVmFactory, propertyInputService)
{
LayerEffect = layerEffect;
}
public ObservableCollection<ViewModelBase> Children { get; }
public LayerPropertyGroup LayerPropertyGroup { get; }
public BaseLayerBrush? LayerBrush { get; }
public BaseLayerEffect? LayerEffect { get; }
public TreeGroupViewModel TreeGroupViewModel { get; }
public TimelineGroupViewModel TimelineGroupViewModel { get; }
public bool IsVisible
{
get => _isVisible;
set => RaiseAndSetIfChanged(ref _isVisible, value);
}
public bool IsExpanded
{
get => _isExpanded;
set => RaiseAndSetIfChanged(ref _isExpanded, value);
}
public bool HasChildren
{
get => _hasChildren;
set => RaiseAndSetIfChanged(ref _hasChildren, value);
}
public List<ITimelineKeyframeViewModel> GetAllKeyframeViewModels(bool expandedOnly)
{
List<ITimelineKeyframeViewModel> result = new();
if (expandedOnly && !IsExpanded)
return result;
foreach (ViewModelBase 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()
{
// 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
foreach (PropertyInfo propertyInfo in LayerPropertyGroup.GetType().GetProperties())
{
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} || i is PropertyGroupViewModel {IsVisible: true});
}
private void LayerPropertyGroupOnVisibilityChanged(object? sender, EventArgs e)
{
IsVisible = !LayerPropertyGroup.IsHidden;
}
/// <inheritdoc />
public void Dispose()
{
LayerPropertyGroup.VisibilityChanged -= LayerPropertyGroupOnVisibilityChanged;
foreach (ViewModelBase viewModelBase in Children)
{
if (viewModelBase is IDisposable disposable)
disposable.Dispose();
}
}
}
@@ -0,0 +1,58 @@
using System;
using Artemis.Core;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
using Artemis.UI.Screens.ProfileEditor.Properties.Tree;
using Artemis.UI.Shared;
namespace Artemis.UI.Screens.ProfileEditor.Properties;
public class PropertyViewModel : ViewModelBase, IDisposable
{
private bool _isExpanded;
private bool _isHighlighted;
private bool _isVisible;
public PropertyViewModel(ILayerProperty layerProperty, IPropertyVmFactory propertyVmFactory)
{
LayerProperty = layerProperty;
TreePropertyViewModel = propertyVmFactory.TreePropertyViewModel(LayerProperty, this);
TimelinePropertyViewModel = propertyVmFactory.TimelinePropertyViewModel(LayerProperty, this);
LayerProperty.VisibilityChanged += LayerPropertyOnVisibilityChanged;
_isVisible = !LayerProperty.IsHidden;
}
public ILayerProperty LayerProperty { get; }
public ITreePropertyViewModel TreePropertyViewModel { get; }
public ITimelinePropertyViewModel TimelinePropertyViewModel { get; }
public bool IsVisible
{
get => _isVisible;
set => RaiseAndSetIfChanged(ref _isVisible, value);
}
public bool IsHighlighted
{
get => _isHighlighted;
set => RaiseAndSetIfChanged(ref _isHighlighted, value);
}
public bool IsExpanded
{
get => _isExpanded;
set => RaiseAndSetIfChanged(ref _isExpanded, value);
}
private void LayerPropertyOnVisibilityChanged(object? sender, LayerPropertyEventArgs e)
{
IsVisible = !LayerProperty.IsHidden;
}
/// <inheritdoc />
public void Dispose()
{
LayerProperty.VisibilityChanged -= LayerPropertyOnVisibilityChanged;
}
}
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public interface ITimelinePropertyViewModel : IReactiveObject
{
List<ITimelineKeyframeViewModel> GetAllKeyframeViewModels();
void WipeKeyframes(TimeSpan? start, TimeSpan? end);
void ShiftKeyframes(TimeSpan? start, TimeSpan? end, TimeSpan amount);
}
@@ -0,0 +1,31 @@
using System;
using Artemis.Core;
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();
void Duplicate();
void Copy();
void Paste();
void Delete();
#endregion
}
@@ -0,0 +1,19 @@
<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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes.TimelineEasingView"
x:DataType="keyframes:TimelineEasingViewModel">
<StackPanel Orientation="Horizontal">
<Polyline Stroke="{DynamicResource TextFillColorPrimaryBrush}"
StrokeThickness="1"
Points="{CompiledBinding EasingPoints}"
Stretch="Uniform"
Width="20"
Height="20"
Margin="0 0 10 0" />
<TextBlock Text="{CompiledBinding Description}" />
</StackPanel>
</UserControl>
@@ -0,0 +1,18 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes
{
public partial class TimelineEasingView : UserControl
{
public TimelineEasingView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}
@@ -0,0 +1,33 @@
using System.Collections.Generic;
using Artemis.Core;
using Artemis.UI.Shared;
using Avalonia;
using Humanizer;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
public class TimelineEasingViewModel : ViewModelBase
{
private readonly ILayerPropertyKeyframe _keyframe;
public TimelineEasingViewModel(Easings.Functions easingFunction, ILayerPropertyKeyframe keyframe)
{
_keyframe = keyframe;
EasingFunction = easingFunction;
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 List<Point> EasingPoints { get; }
public string Description { get; }
public bool IsEasingModeSelected => _keyframe.EasingFunction == EasingFunction;
}
@@ -0,0 +1,80 @@
<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:timeline="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes.TimelineKeyframeView"
ClipToBounds="False"
Height="{DynamicResource RailsHeight}">
<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" Items="{Binding EasingViewModels}">
<MenuItem.Styles>
<Style Selector="MenuItem > MenuItem">
<Setter Property="Icon">
<Setter.Value>
<Template>
<CheckBox IsHitTestVisible="False" IsChecked="{Binding IsEasingModeSelected}" />
</Template>
</Setter.Value>
</Setter>
<Setter Property="Command" Value="{Binding $parent[UserControl].DataContext.SelectEasingFunction}"></Setter>
<Setter Property="CommandParameter" Value="{Binding EasingFunction}"></Setter>
</Style>
</MenuItem.Styles>
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Creation" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Duplicate" Command="{Binding $parent[timeline:TimelineView].DataContext.DuplicateKeyframes}" CommandParameter="{Binding}" InputGesture="Ctrl+D">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ContentDuplicate" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Copy" Command="{Binding $parent[timeline:TimelineView].DataContext.CopyKeyframes}" CommandParameter="{Binding}" InputGesture="Ctrl+C">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ContentCopy" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Paste" Command="{Binding $parent[timeline:TimelineView].DataContext.PasteKeyframes}" CommandParameter="{Binding}" InputGesture="Ctrl+V">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ContentPaste" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Delete" Command="{Binding $parent[timeline:TimelineView].DataContext.DeleteKeyframes}" CommandParameter="{Binding}" InputGesture="Delete">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Delete" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Ellipse.ContextFlyout>
</Ellipse>
</UserControl>
@@ -0,0 +1,85 @@
using System;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
public class TimelineKeyframeView : ReactiveUserControl<ITimelineKeyframeViewModel>
{
private TimelinePropertyView? _timelinePropertyView;
private TimelineView? _timelineView;
private bool _moved;
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 InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
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();
}
}
@@ -0,0 +1,168 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia.Controls.Mixins;
using DynamicData;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineKeyframeViewModel
{
private readonly IProfileEditorService _profileEditorService;
private double _x;
private string _timestamp;
private ObservableAsPropertyHelper<bool>? _isSelected;
public TimelineKeyframeViewModel(LayerPropertyKeyframe<T> layerPropertyKeyframe, IProfileEditorService profileEditorService)
{
_profileEditorService = profileEditorService;
_timestamp = "0.000";
LayerPropertyKeyframe = layerPropertyKeyframe;
EasingViewModels = new ObservableCollection<TimelineEasingViewModel>();
this.WhenActivated(d =>
{
_isSelected = profileEditorService.ConnectToKeyframes().ToCollection().Select(keyframes => keyframes.Contains(LayerPropertyKeyframe)).ToProperty(this, vm => vm.IsSelected).DisposeWith(d);
profileEditorService.ConnectToKeyframes();
profileEditorService.PixelsPerSecond.Subscribe(p =>
{
_pixelsPerSecond = p;
profileEditorService.PixelsPerSecond.Subscribe(_ => Update()).DisposeWith(d);
}).DisposeWith(d);
this.WhenAnyValue(vm => vm.LayerPropertyKeyframe.Position).Subscribe(_ => Update()).DisposeWith(d);
});
}
public LayerPropertyKeyframe<T> LayerPropertyKeyframe { get; }
public ObservableCollection<TimelineEasingViewModel> EasingViewModels { get; }
public double X
{
get => _x;
set => RaiseAndSetIfChanged(ref _x, value);
}
public string Timestamp
{
get => _timestamp;
set => RaiseAndSetIfChanged(ref _timestamp, value);
}
public bool IsSelected => _isSelected?.Value ?? false;
public TimeSpan Position => LayerPropertyKeyframe.Position;
public ILayerPropertyKeyframe Keyframe => LayerPropertyKeyframe;
public void Update()
{
X = _pixelsPerSecond * LayerPropertyKeyframe.Position.TotalSeconds;
Timestamp = $"{Math.Floor(LayerPropertyKeyframe.Position.TotalSeconds):00}.{LayerPropertyKeyframe.Position.Milliseconds:000}";
}
/// <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
public void Duplicate()
{
throw new NotImplementedException();
}
public void Copy()
{
throw new NotImplementedException();
}
public void Paste()
{
throw new NotImplementedException();
}
public void Delete()
{
_profileEditorService.ExecuteCommand(new DeleteKeyframe(LayerPropertyKeyframe));
}
#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)));
}
public void SelectEasingFunction(Easings.Functions easingFunction)
{
_profileEditorService.ExecuteCommand(new ChangeKeyframeEasing(Keyframe, easingFunction));
}
#endregion
}
@@ -0,0 +1,58 @@
<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 CardStrokeColorDefaultSolidBrush}"
Width="{CompiledBinding Width}"
ColumnDefinitions="Auto, Auto,*,Auto">
<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="{Binding 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="{Binding 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>
@@ -0,0 +1,50 @@
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
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 readonly Rectangle _keyframeDragAnchor;
private double _dragOffset;
public EndSegmentView()
{
InitializeComponent();
_keyframeDragAnchor = this.Get<Rectangle>("KeyframeDragAnchor");
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
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);
}
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);
}
}
}
@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia.Controls.Mixins;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
public class EndSegmentViewModel : TimelineSegmentViewModel
{
private readonly IProfileEditorService _profileEditorService;
private RenderProfileElement? _profileElement;
private int _pixelsPerSecond;
private TimeSpan _time;
private ObservableAsPropertyHelper<double>? _start;
private ObservableAsPropertyHelper<double>? _end;
private ObservableAsPropertyHelper<string?>? _endTimestamp;
private readonly ObservableAsPropertyHelper<double> _width;
private TimeSpan _initialLength;
public EndSegmentViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
{
_profileEditorService = profileEditorService;
this.WhenActivated(d =>
{
profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d);
profileEditorService.Time.Subscribe(t => _time = t).DisposeWith(d);
profileEditorService.PixelsPerSecond.Subscribe(p => _pixelsPerSecond = 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;
}
}
public override double Width => _width.Value;
public override string? EndTimestamp => _endTimestamp?.Value;
public override ResizeTimelineSegment.SegmentType Type => ResizeTimelineSegment.SegmentType.End;
}
@@ -0,0 +1,80 @@
<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 CardStrokeColorDefaultSolidBrush}"
Width="{CompiledBinding Width}"
ColumnDefinitions="Auto,Auto,*,Auto,Auto">
<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="{Binding 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="{Binding DisableSegment}">
<avalonia:MaterialIcon Kind="CloseCircle" />
</Button>
<ToggleButton Name="SegmentRepeat"
Classes="icon-button icon-button-small"
ToolTip.Tip="Repeat this segment"
IsChecked="{CompiledBinding RepeatSegment}"
Padding="0">
<avalonia:MaterialIcon Kind="Repeat" />
</ToggleButton>
</StackPanel>
<Button Name="AddEndSegment"
Grid.Column="3"
Classes="AppBarButton icon-button icon-button-small"
VerticalAlignment="Center"
ToolTip.Tip="Add an end segment"
Command="{Binding 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>
@@ -0,0 +1,62 @@
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
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 readonly Rectangle _keyframeDragAnchor;
private double _dragOffset;
public MainSegmentView()
{
InitializeComponent();
_keyframeDragAnchor = this.Get<Rectangle>("KeyframeDragAnchor");
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
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);
}
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);
}
private void KeyframeDragStartAnchor_OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
}
private void KeyframeDragStartAnchor_OnPointerMoved(object? sender, PointerEventArgs e)
{
}
private void KeyframeDragStartAnchor_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
}
}
}
@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia.Controls.Mixins;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
public class MainSegmentViewModel : TimelineSegmentViewModel
{
private RenderProfileElement? _profileElement;
private ObservableAsPropertyHelper<double>? _start;
private ObservableAsPropertyHelper<double>? _end;
private ObservableAsPropertyHelper<string?>? _endTimestamp;
private ObservableAsPropertyHelper<bool>? _repeatSegment;
private readonly ObservableAsPropertyHelper<double> _width;
public MainSegmentViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
{
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);
_repeatSegment = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.PlayMode) ?? Observable.Never<TimelinePlayMode>())
.Switch()
.Select(p => p == TimelinePlayMode.Repeat)
.ToProperty(this, vm => vm.RepeatSegment)
.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;
}
}
public override double Width => _width.Value;
public override string? EndTimestamp => _endTimestamp?.Value;
public override ResizeTimelineSegment.SegmentType Type => ResizeTimelineSegment.SegmentType.Main;
public bool RepeatSegment
{
get => _repeatSegment?.Value ?? false;
set => throw new NotImplementedException();
}
}
@@ -0,0 +1,28 @@
<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>
<Style Selector="Border.segment-container">
<Setter Property="Height" Value="18"></Setter>
</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>
@@ -0,0 +1,56 @@
<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:system="clr-namespace:System;assembly=System.Runtime"
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 CardStrokeColorDefaultSolidBrush}"
Width="{CompiledBinding Width}"
ColumnDefinitions="*,Auto,Auto">
<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="{Binding 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="{Binding 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>
@@ -0,0 +1,50 @@
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
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 readonly Rectangle _keyframeDragAnchor;
private double _dragOffset;
public StartSegmentView()
{
InitializeComponent();
_keyframeDragAnchor = this.Get<Rectangle>("KeyframeDragAnchor");
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
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);
}
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);
}
}
}
@@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia.Controls.Mixins;
using Castle.DynamicProxy.Generators;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
public class StartSegmentViewModel : TimelineSegmentViewModel
{
private readonly IProfileEditorService _profileEditorService;
private RenderProfileElement? _profileElement;
private int _pixelsPerSecond;
private TimeSpan _time;
private ObservableAsPropertyHelper<double>? _end;
private ObservableAsPropertyHelper<string?>? _endTimestamp;
private readonly ObservableAsPropertyHelper<double> _width;
private TimeSpan _initialLength;
public StartSegmentViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
{
_profileEditorService = profileEditorService;
this.WhenActivated(d =>
{
profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d);
profileEditorService.Time.Subscribe(t => _time = t).DisposeWith(d);
profileEditorService.PixelsPerSecond.Subscribe(p => _pixelsPerSecond = 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;
}
}
public override double Width => _width.Value;
public override string? EndTimestamp => _endTimestamp?.Value;
public override ResizeTimelineSegment.SegmentType Type => ResizeTimelineSegment.SegmentType.Start;
}
@@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia.Controls.Mixins;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
public abstract class TimelineSegmentViewModel : ActivatableViewModelBase
{
private static readonly TimeSpan NewSegmentLength = TimeSpan.FromSeconds(2);
private readonly IProfileEditorService _profileEditorService;
private RenderProfileElement? _profileElement;
private int _pixelsPerSecond;
private Dictionary<ILayerPropertyKeyframe, TimeSpan> _originalKeyframePositions = new();
private ObservableAsPropertyHelper<bool>? _showAddStart;
private ObservableAsPropertyHelper<bool>? _showAddMain;
private ObservableAsPropertyHelper<bool>? _showAddEnd;
private TimeSpan _initialLength;
protected TimelineSegmentViewModel(IProfileEditorService profileEditorService)
{
_profileEditorService = profileEditorService;
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);
});
}
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 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)
{
if (_profileElement == null)
return;
TimeSpan difference = GetTimeFromX(x) - Length;
List<ILayerPropertyKeyframe> keyframes = _profileElement.GetAllLayerProperties().SelectMany(p => p.UntypedKeyframes).ToList();
ShiftKeyframes(keyframes.Where(k => k.Position > End.Add(difference)), difference);
Length = GetTimeFromX(x);
}
public void FinishResize(double x)
{
if (_profileElement == null)
return;
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Resize segment");
ApplyPendingKeyframeMovement();
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(Type, _profileElement, GetTimeFromX(x), _initialLength));
}
public void RemoveSegment()
{
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));
}
protected TimeSpan GetTimeFromX(double x)
{
TimeSpan length = TimeSpan.FromSeconds(x / _pixelsPerSecond);
if (length < TimeSpan.Zero)
length = TimeSpan.Zero;
return length;
}
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();
}
}
@@ -0,0 +1,55 @@
<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"
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="{DynamicResource RailsHeight}"
IsVisible="{CompiledBinding !PropertyGroupViewModel.IsExpanded}"
Items="{CompiledBinding KeyframePositions}"
HorizontalAlignment="Stretch">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Styles>
<Style Selector="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding}" />
</Style>
</ItemsControl.Styles>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Fill="{DynamicResource ControlFillColorDisabledBrush}"
Stroke="White"
StrokeThickness="0"
Width="10"
Height="10"
Margin="-5,6,0,0" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Rectangle Grid.Row="1" HorizontalAlignment="Stretch" Fill="{DynamicResource ButtonBorderBrush}" Height="1" />
<ItemsControl Grid.Row="2"
Items="{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>
@@ -0,0 +1,17 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public class TimelineGroupView : ReactiveUserControl<TimelineGroupViewModel>
{
public TimelineGroupView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
@@ -0,0 +1,48 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Avalonia.Controls.Mixins;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public class TimelineGroupViewModel : ActivatableViewModelBase
{
private ObservableCollection<double>? _keyframePositions;
private int _pixelsPerSecond;
public TimelineGroupViewModel(PropertyGroupViewModel propertyGroupViewModel, IProfileEditorService profileEditorService)
{
PropertyGroupViewModel = propertyGroupViewModel;
this.WhenActivated(d =>
{
profileEditorService.PixelsPerSecond.Subscribe(p =>
{
_pixelsPerSecond = p;
UpdateKeyframePositions();
});
this.WhenAnyValue(vm => vm.PropertyGroupViewModel.IsExpanded).Subscribe(_ => UpdateKeyframePositions()).DisposeWith(d);
PropertyGroupViewModel.WhenAnyValue(vm => vm.IsExpanded).Subscribe(_ => this.RaisePropertyChanged(nameof(Children))).DisposeWith(d);
});
}
public PropertyGroupViewModel PropertyGroupViewModel { get; }
public ObservableCollection<ViewModelBase>? Children => PropertyGroupViewModel.IsExpanded ? PropertyGroupViewModel.Children : null;
public ObservableCollection<double>? KeyframePositions
{
get => _keyframePositions;
set => RaiseAndSetIfChanged(ref _keyframePositions, value);
}
private void UpdateKeyframePositions()
{
KeyframePositions = new ObservableCollection<double>(PropertyGroupViewModel
.GetAllKeyframeViewModels(false)
.Select(p => p.Position.TotalSeconds * _pixelsPerSecond));
}
}
@@ -0,0 +1,21 @@
<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="{DynamicResource RailsBorderHeight}" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource ButtonBorderBrush}">
<ItemsControl Items="{Binding KeyframeViewModels}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Styles>
<Style Selector="ItemsControl > ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X, TargetNullValue=0}" />
</Style>
</ItemsControl.Styles>
</ItemsControl>
</Border>
</UserControl>
@@ -0,0 +1,17 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public class TimelinePropertyView : ReactiveUserControl<ITimelinePropertyViewModel>
{
public TimelinePropertyView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
@@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
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 Avalonia.Controls.Mixins;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public class TimelinePropertyViewModel<T> : ActivatableViewModelBase, ITimelinePropertyViewModel
{
private readonly IProfileEditorService _profileEditorService;
public TimelinePropertyViewModel(LayerProperty<T> layerProperty, PropertyViewModel propertyViewModel, IProfileEditorService profileEditorService)
{
_profileEditorService = profileEditorService;
LayerProperty = layerProperty;
PropertyViewModel = propertyViewModel;
KeyframeViewModels = new ObservableCollection<TimelineKeyframeViewModel<T>>();
this.WhenActivated(d =>
{
Observable.FromEventPattern<LayerPropertyEventArgs>(x => LayerProperty.KeyframesToggled += x, x => LayerProperty.KeyframesToggled -= x)
.Subscribe(_ => UpdateKeyframes())
.DisposeWith(d);
Observable.FromEventPattern<LayerPropertyEventArgs>(x => LayerProperty.KeyframeAdded += x, x => LayerProperty.KeyframeAdded -= x)
.Subscribe(_ => UpdateKeyframes())
.DisposeWith(d);
Observable.FromEventPattern<LayerPropertyEventArgs>(x => LayerProperty.KeyframeRemoved += x, x => LayerProperty.KeyframeRemoved -= x)
.Subscribe(_ => UpdateKeyframes())
.DisposeWith(d);
UpdateKeyframes();
});
}
public LayerProperty<T> LayerProperty { get; }
public PropertyViewModel PropertyViewModel { get; }
public ObservableCollection<TimelineKeyframeViewModel<T>> KeyframeViewModels { get; }
private void UpdateKeyframes()
{
// Only show keyframes if they are enabled
if (LayerProperty.KeyframesEnabled)
{
List<LayerPropertyKeyframe<T>> keyframes = LayerProperty.Keyframes.ToList();
List<TimelineKeyframeViewModel<T>> toRemove = KeyframeViewModels.Where(t => !keyframes.Contains(t.LayerPropertyKeyframe)).ToList();
foreach (TimelineKeyframeViewModel<T> timelineKeyframeViewModel in toRemove)
KeyframeViewModels.Remove(timelineKeyframeViewModel);
List<TimelineKeyframeViewModel<T>> toAdd = keyframes.Where(k => KeyframeViewModels.All(t => t.LayerPropertyKeyframe != k))
.Select(k => new TimelineKeyframeViewModel<T>(k, _profileEditorService)).ToList();
foreach (TimelineKeyframeViewModel<T> timelineKeyframeViewModel in toAdd)
KeyframeViewModels.Add(timelineKeyframeViewModel);
}
else
{
KeyframeViewModels.Clear();
}
foreach (TimelineKeyframeViewModel<T> timelineKeyframeViewModel in KeyframeViewModels)
timelineKeyframeViewModel.Update();
}
#region Implementation of ITimelinePropertyViewModel
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
}
@@ -0,0 +1,30 @@
<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:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared"
xmlns:timeline="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.TimelineView"
x:DataType="timeline:TimelineViewModel">
<UserControl.Resources>
<x:Double x:Key="RailsHeight">28</x:Double>
<x:Double x:Key="RailsBorderHeight">29</x:Double>
</UserControl.Resources>
<Grid Background="Transparent" PointerReleased="InputElement_OnPointerReleased" Focusable="True" MinWidth="{Binding MinWidth}">
<Grid.KeyBindings>
<KeyBinding Command="{Binding DeleteKeyframes}" Gesture="Delete"/>
</Grid.KeyBindings>
<ItemsControl Items="{CompiledBinding PropertyGroupViewModels}">
<ItemsControl.ItemTemplate>
<TreeDataTemplate DataType="{x:Type local:PropertyGroupViewModel}" ItemsSource="{CompiledBinding Children}">
<ContentControl Content="{CompiledBinding TimelineGroupViewModel}" />
</TreeDataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<controls:SelectionRectangle Name="SelectionRectangle" InputElement="{CompiledBinding $parent}" SelectionFinished="SelectionRectangle_OnSelectionFinished"></controls:SelectionRectangle>
</Grid>
</UserControl>
@@ -0,0 +1,51 @@
using System.Collections.Generic;
using System.Linq;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
using Artemis.UI.Shared.Controls;
using Artemis.UI.Shared.Events;
using Artemis.UI.Shared.Extensions;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public class TimelineView : ReactiveUserControl<TimelineViewModel>
{
private readonly SelectionRectangle _selectionRectangle;
public TimelineView()
{
InitializeComponent();
_selectionRectangle = this.Get<SelectionRectangle>("SelectionRectangle");
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void SelectionRectangle_OnSelectionFinished(object? sender, SelectionRectangleEventArgs e)
{
if (ViewModel == null)
return;
List<TimelineKeyframeView> keyframeViews = this.GetVisualChildrenOfType<TimelineKeyframeView>().Where(k =>
{
Rect hitTestRect = k.TransformedBounds != null ? k.TransformedBounds.Value.Bounds.TransformToAABB(k.TransformedBounds.Value.Transform) : Rect.Empty;
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);
}
}
@@ -0,0 +1,195 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia.Controls.Mixins;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public class TimelineViewModel : ActivatableViewModelBase
{
private readonly IProfileEditorService _profileEditorService;
private ObservableAsPropertyHelper<double>? _caretPosition;
private ObservableAsPropertyHelper<int>? _pixelsPerSecond;
private List<ITimelineKeyframeViewModel>? _moveKeyframes;
private ObservableAsPropertyHelper<double> _minWidth;
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 =>
{
_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);
});
}
public ObservableCollection<PropertyGroupViewModel> PropertyGroupViewModels { get; }
public StartSegmentViewModel StartSegmentViewModel { get; }
public MainSegmentViewModel MainSegmentViewModel { get; }
public EndSegmentViewModel EndSegmentViewModel { 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
public void DuplicateKeyframes(ITimelineKeyframeViewModel? source = null)
{
if (source is { IsSelected: false })
source.Delete();
else
{
List<ITimelineKeyframeViewModel> keyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Where(k => k.IsSelected).ToList();
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope($"Duplicate {keyframes.Count} keyframes.");
foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in keyframes)
timelineKeyframeViewModel.Duplicate();
}
}
public void CopyKeyframes(ITimelineKeyframeViewModel? source = null)
{
if (source is { IsSelected: false })
source.Copy();
else
{
List<ITimelineKeyframeViewModel> keyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Where(k => k.IsSelected).ToList();
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope($"Copy {keyframes.Count} keyframes.");
foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in keyframes)
timelineKeyframeViewModel.Copy();
}
}
public void PasteKeyframes(ITimelineKeyframeViewModel? source = null)
{
if (source is { IsSelected: false })
source.Paste();
else
{
List<ITimelineKeyframeViewModel> keyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Where(k => k.IsSelected).ToList();
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope($"Paste {keyframes.Count} keyframes.");
foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in keyframes)
timelineKeyframeViewModel.Paste();
}
}
public void DeleteKeyframes(ITimelineKeyframeViewModel? source = null)
{
if (source is {IsSelected: false})
source.Delete();
else
{
List<ITimelineKeyframeViewModel> keyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Where(k => k.IsSelected).ToList();
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope($"Delete {keyframes.Count} keyframes.");
foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in keyframes)
timelineKeyframeViewModel.Delete();
}
}
#endregion
}
@@ -0,0 +1,14 @@
using Artemis.Core.LayerBrushes;
using Artemis.UI.Shared;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Tree.Dialogs;
public class LayerBrushPresetViewModel : ContentDialogViewModelBase
{
public LayerBrushPresetViewModel(BaseLayerBrush layerBrush)
{
LayerBrush = layerBrush;
}
public BaseLayerBrush LayerBrush { get; }
}
@@ -0,0 +1,11 @@
using Artemis.Core;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Tree;
public interface ITreePropertyViewModel : IReactiveObject
{
ILayerProperty BaseLayerProperty { get; }
bool HasDataBinding { get; }
double GetDepth();
}
@@ -0,0 +1,196 @@
<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:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared"
xmlns:sharedConverters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:viewModel="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Tree"
xmlns:properties="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Tree.TreeGroupView">
<UserControl.Resources>
<converters:PropertyTreeMarginConverter x:Key="PropertyTreeMarginConverter" Length="20" />
<sharedConverters:EnumToBooleanConverter x:Key="EnumBoolConverter" />
</UserControl.Resources>
<UserControl.Styles>
<Style Selector="avalonia|MaterialIcon.chevron-collapsed">
<Setter Property="RenderTransform" Value="rotate(-90deg)" />
</Style>
</UserControl.Styles>
<StackPanel>
<Border Name="Bd"
BorderBrush="{DynamicResource ButtonBorderBrush}"
BorderThickness="0,0,0,1"
Height="29">
<Grid Margin="{Binding Converter={StaticResource PropertyTreeMarginConverter}}" ColumnDefinitions="19,*">
<avalonia:MaterialIcon Classes.chevron-collapsed="{Binding !PropertyGroupViewModel.IsExpanded}"
IsVisible="{Binding PropertyGroupViewModel.HasChildren}"
Kind="ChevronDown"
Grid.Column="0"
Margin="5 0"
PointerPressed="InputElement_OnPointerPressed"
Background="Transparent">
<avalonia:MaterialIcon.Transitions>
<Transitions>
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.2" />
</Transitions>
</avalonia:MaterialIcon.Transitions>
</avalonia:MaterialIcon>
<StackPanel Grid.Column="1">
<!-- Type: None -->
<TextBlock Text="{Binding LayerPropertyGroup.GroupDescription.Name}"
ToolTip.Tip="{Binding LayerPropertyGroup.GroupDescription.Description}"
VerticalAlignment="Center"
HorizontalAlignment="Left"
Margin="3 5 0 5"
IsVisible="{Binding GroupType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static viewModel:LayerPropertyGroupType.None}}" />
<!-- Type: General -->
<StackPanel Orientation="Horizontal"
Margin="0 5"
IsVisible="{Binding GroupType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static viewModel:LayerPropertyGroupType.General}}">
<avalonia:MaterialIcon Kind="HammerWrench" Margin="0 -1 5 0" />
<TextBlock ToolTip.Tip="{Binding LayerPropertyGroup.GroupDescription.Description}">General</TextBlock>
</StackPanel>
<!-- Type: Transform -->
<StackPanel Orientation="Horizontal"
Margin="0 5"
IsVisible="{Binding GroupType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static viewModel:LayerPropertyGroupType.Transform}}">
<avalonia:MaterialIcon Kind="TransitConnectionVariant" Margin="0 -1 5 0" />
<TextBlock ToolTip.Tip="{Binding LayerPropertyGroup.GroupDescription.Description}">Transform</TextBlock>
</StackPanel>
<!-- Type: LayerBrushRoot -->
<Grid IsVisible="{Binding GroupType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static viewModel:LayerPropertyGroupType.LayerBrushRoot}}"
ColumnDefinitions="Auto,Auto,Auto,*">
<controls:ArtemisIcon Grid.Column="0"
Icon="{Binding LayerBrush.Descriptor.Icon}"
Width="16"
Height="16"
Margin="0 5 5 0" />
<TextBlock Grid.Column="1"
ToolTip.Tip="{Binding LayerBrush.Descriptor.Description}"
Margin="0 5 0 0">
Brush -&#160;
</TextBlock>
<TextBlock Grid.Column="2"
Text="{Binding LayerBrush.Descriptor.DisplayName}"
ToolTip.Tip="{Binding LayerBrush.Descriptor.Description}"
Margin="0 5 0 0" />
<StackPanel Grid.Column="3"
Orientation="Horizontal"
HorizontalAlignment="Right"
IsVisible="{Binding LayerBrush.ConfigurationDialog, Converter={x:Static ObjectConverters.IsNotNull}}">
<TextBlock VerticalAlignment="Center">Extra options available!</TextBlock>
<avalonia:MaterialIcon Kind="ChevronRight" VerticalAlignment="Center">
<avalonia:MaterialIcon.RenderTransform>
<TranslateTransform X="0" />
</avalonia:MaterialIcon.RenderTransform>
</avalonia:MaterialIcon>
<Button Classes="icon-button" ToolTip.Tip="Open brush settings" Width="24" Height="24" HorizontalAlignment="Right" Command="{Binding OpenBrushSettings}">
<avalonia:MaterialIcon Kind="Settings" Height="16" Width="16" />
</Button>
</StackPanel>
</Grid>
<!-- Type: LayerEffectRoot -->
<Grid Height="24"
IsVisible="{Binding GroupType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static viewModel:LayerPropertyGroupType.LayerEffectRoot}}"
ColumnDefinitions="Auto,Auto,Auto,Auto,*,Auto">
<controls:ArtemisIcon
Grid.Column="0"
Cursor="SizeNorthSouth"
Icon="{Binding LayerEffect.Descriptor.Icon}"
Width="16"
Height="16"
Margin="0 5 5 0"
Background="Transparent" />
<TextBlock Grid.Column="1" ToolTip.Tip="{Binding LayerEffect.Descriptor.Description}" Margin="0 5 0 0">
Effect
</TextBlock>
<TextBlock Grid.Column="2"
ToolTip.Tip="{Binding LayerEffect.Descriptor.Description}"
Margin="3 5">
-
</TextBlock>
<!-- Show either the descriptors display name or, if set, the effect name -->
<TextBlock Grid.Column="3"
Text="{Binding LayerEffect.Descriptor.DisplayName}"
ToolTip.Tip="{Binding LayerEffect.Descriptor.Description}"
Margin="0 5"
IsVisible="{Binding !LayerEffect.Name, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
<TextBlock Grid.Column="4"
Text="{Binding LayerEffect.Name}"
ToolTip.Tip="{Binding LayerEffect.Descriptor.Description}"
Margin="0 5"
IsVisible="{Binding LayerEffect.Name, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
<StackPanel Grid.Column="5" Orientation="Horizontal">
<ToggleButton
Classes="icon-button"
ToolTip.Tip="Toggle suspended state"
Width="18"
Height="18"
IsChecked="{Binding !LayerEffect.Suspended}"
VerticalAlignment="Center" Padding="-25"
Margin="5 0"
Command="{Binding SuspendedToggled}">
<avalonia:MaterialIcon Kind="Eye" Height="13" Width="13" />
</ToggleButton>
<Button Classes="icon-button"
ToolTip.Tip="Rename"
Width="24"
Height="24"
VerticalAlignment="Center"
Command="{Binding RenameEffect}">
<avalonia:MaterialIcon Kind="RenameBox" Height="16" Width="16" />
</Button>
<Button Classes="icon-button"
ToolTip.Tip="Open effect settings"
Width="24"
Height="24"
VerticalAlignment="Center"
Command="{Binding OpenEffectSettings}"
IsVisible="{Binding LayerEffect.ConfigurationDialog, Converter={x:Static ObjectConverters.IsNotNull}}">
<avalonia:MaterialIcon Kind="Settings" Height="16" Width="16" />
</Button>
<Button Classes="icon-button"
ToolTip.Tip="Remove"
Width="24"
Height="24"
VerticalAlignment="Center"
Command="{Binding DeleteEffect}">
<avalonia:MaterialIcon Kind="TrashCan" Height="16" Width="16" />
</Button>
</StackPanel>
</Grid>
</StackPanel>
</Grid>
</Border>
<!--
Do not bind directly to the PropertyGroupViewModel.Children collection
Instead use a reference provided by the VM that is null when collapsed, virtualization for noobs
-->
<ItemsControl Items="{Binding Children}"
IsVisible="{Binding PropertyGroupViewModel.IsExpanded}"
HorizontalAlignment="Stretch">
<ItemsControl.DataTemplates>
<DataTemplate DataType="properties:PropertyGroupViewModel">
<ContentControl Content="{Binding TreeGroupViewModel}" IsVisible="{Binding IsVisible}" />
</DataTemplate>
<DataTemplate DataType="properties:PropertyViewModel">
<ContentControl Content="{Binding TreePropertyViewModel}" IsVisible="{Binding IsVisible}" />
</DataTemplate>
</ItemsControl.DataTemplates>
</ItemsControl>
</StackPanel>
</UserControl>
@@ -0,0 +1,24 @@
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Tree;
public class TreeGroupView : ReactiveUserControl<TreeGroupViewModel>
{
public TreeGroupView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void InputElement_OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (ViewModel != null)
ViewModel.PropertyGroupViewModel.IsExpanded = !ViewModel.PropertyGroupViewModel.IsExpanded;
}
}
@@ -0,0 +1,167 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Disposables;
using System.Reflection;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects;
using Artemis.UI.Exceptions;
using Artemis.UI.Screens.ProfileEditor.Properties.Windows;
using Artemis.UI.Shared;
using Artemis.UI.Shared.LayerBrushes;
using Artemis.UI.Shared.LayerEffects;
using Artemis.UI.Shared.Services.Interfaces;
using Artemis.UI.Shared.Services.ProfileEditor;
using Ninject;
using Ninject.Parameters;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Tree;
public class TreeGroupViewModel : ActivatableViewModelBase
{
private readonly IProfileEditorService _profileEditorService;
private readonly IWindowService _windowService;
private BrushConfigurationWindowViewModel? _brushConfigurationWindowViewModel;
private EffectConfigurationWindowViewModel? _effectConfigurationWindowViewModel;
public TreeGroupViewModel(PropertyGroupViewModel propertyGroupViewModel, IWindowService windowService, IProfileEditorService profileEditorService)
{
_windowService = windowService;
_profileEditorService = profileEditorService;
PropertyGroupViewModel = propertyGroupViewModel;
DetermineGroupType();
this.WhenActivated(d =>
{
PropertyGroupViewModel.WhenAnyValue(vm => vm.IsExpanded).Subscribe(_ => this.RaisePropertyChanged(nameof(Children))).DisposeWith(d);
Disposable.Create(CloseViewModels).DisposeWith(d);
});
// TODO: Update ProfileElementPropertyGroupViewModel visibility on change (can remove the sub on line 41 as well then)
}
public PropertyGroupViewModel PropertyGroupViewModel { get; }
public LayerPropertyGroup LayerPropertyGroup => PropertyGroupViewModel.LayerPropertyGroup;
public BaseLayerBrush? LayerBrush => PropertyGroupViewModel.LayerBrush;
public BaseLayerEffect? LayerEffect => PropertyGroupViewModel.LayerEffect;
public ObservableCollection<ViewModelBase>? Children => PropertyGroupViewModel.IsExpanded ? PropertyGroupViewModel.Children : null;
public LayerPropertyGroupType GroupType { get; private set; }
public async Task OpenBrushSettings()
{
if (LayerBrush?.ConfigurationDialog is not LayerBrushConfigurationDialog configurationViewModel)
return;
try
{
// Limit to one constructor, there's no need to have more and it complicates things anyway
ConstructorInfo[] constructors = configurationViewModel.Type.GetConstructors();
if (constructors.Length != 1)
throw new ArtemisUIException("Brush configuration dialogs must have exactly one constructor");
// Find the BaseLayerBrush parameter, it is required by the base constructor so its there for sure
ParameterInfo brushParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerBrush).IsAssignableFrom(p.ParameterType));
ConstructorArgument argument = new(brushParameter.Name!, LayerBrush);
BrushConfigurationViewModel viewModel =
(BrushConfigurationViewModel) LayerBrush.Descriptor.Provider.Plugin.Kernel!.Get(configurationViewModel.Type, argument);
_brushConfigurationWindowViewModel = new BrushConfigurationWindowViewModel(viewModel, configurationViewModel);
await _windowService.ShowDialogAsync(_brushConfigurationWindowViewModel);
// Save changes after the dialog closes
await _profileEditorService.SaveProfileAsync();
}
catch (Exception e)
{
_windowService.ShowExceptionDialog("An exception occurred while trying to show the brush's settings window", e);
}
}
public async Task OpenEffectSettings()
{
if (LayerEffect?.ConfigurationDialog is not LayerEffectConfigurationDialog configurationViewModel)
return;
try
{
// Limit to one constructor, there's no need to have more and it complicates things anyway
ConstructorInfo[] constructors = configurationViewModel.Type.GetConstructors();
if (constructors.Length != 1)
throw new ArtemisUIException("Effect configuration dialogs must have exactly one constructor");
// Find the BaseLayerEffect parameter, it is required by the base constructor so its there for sure
ParameterInfo effectParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerEffect).IsAssignableFrom(p.ParameterType));
ConstructorArgument argument = new(effectParameter.Name!, LayerEffect);
EffectConfigurationViewModel viewModel =
(EffectConfigurationViewModel) LayerEffect.Descriptor.Provider.Plugin.Kernel!.Get(configurationViewModel.Type, argument);
_effectConfigurationWindowViewModel = new EffectConfigurationWindowViewModel(viewModel, configurationViewModel);
await _windowService.ShowDialogAsync(_effectConfigurationWindowViewModel);
// Save changes after the dialog closes
await _profileEditorService.SaveProfileAsync();
}
catch (Exception e)
{
_windowService.ShowExceptionDialog("An exception occurred while trying to show the effect's settings window", e);
}
}
public async Task RenameEffect()
{
await _windowService.ShowConfirmContentDialog("Not yet implemented", "Try again later :p");
}
public async Task DeleteEffect()
{
await _windowService.ShowConfirmContentDialog("Not yet implemented", "Try again later :p");
}
public double GetDepth()
{
int depth = 0;
LayerPropertyGroup? current = LayerPropertyGroup.Parent;
while (current != null)
{
depth++;
current = current.Parent;
}
return depth;
}
private void CloseViewModels()
{
_effectConfigurationWindowViewModel?.Close(null);
_brushConfigurationWindowViewModel?.Close(null);
}
private void DetermineGroupType()
{
if (LayerPropertyGroup is LayerGeneralProperties)
GroupType = LayerPropertyGroupType.General;
else if (LayerPropertyGroup is LayerTransformProperties)
GroupType = LayerPropertyGroupType.Transform;
else if (LayerPropertyGroup.Parent == null && PropertyGroupViewModel.LayerBrush != null)
GroupType = LayerPropertyGroupType.LayerBrushRoot;
else if (LayerPropertyGroup.Parent == null && PropertyGroupViewModel.LayerEffect != null)
GroupType = LayerPropertyGroupType.LayerEffectRoot;
else
GroupType = LayerPropertyGroupType.None;
}
}
public enum LayerPropertyGroupType
{
General,
Transform,
LayerBrushRoot,
LayerEffectRoot,
None
}
@@ -0,0 +1,65 @@
<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:converters="clr-namespace:Artemis.UI.Converters"
xmlns:sharedConverters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Tree.TreePropertyView">
<UserControl.Resources>
<converters:PropertyTreeMarginConverter x:Key="PropertyTreeMarginConverter" Length="20" />
</UserControl.Resources>
<Border Name="Bd"
BorderBrush="{DynamicResource ButtonBorderBrush}"
BorderThickness="0,0,0,1"
Height="29">
<Grid Margin="{Binding Converter={StaticResource PropertyTreeMarginConverter}}" ColumnDefinitions="Auto,*,Auto,Auto,Auto">
<ToggleButton Grid.Column="0"
Classes="icon-button"
ToolTip.Tip="Toggle key-framing"
Width="24"
Height="24"
IsChecked="{Binding KeyframesEnabled}"
IsEnabled="{Binding LayerProperty.KeyframesSupported}"
VerticalAlignment="Center" Padding="-25">
<avalonia:MaterialIcon Kind="Stopwatch" />
</ToggleButton>
<TextBlock Grid.Column="1"
Margin="5,0,0,0"
Padding="0,0,5,0"
VerticalAlignment="Center"
TextTrimming="CharacterEllipsis"
Text="{Binding LayerProperty.PropertyDescription.Name}"
ToolTip.Tip="{Binding LayerProperty.PropertyDescription.Description}"
HorizontalAlignment="Left" />
<ContentControl Grid.Column="2"
Margin="5 0"
Content="{Binding PropertyInputViewModel}"/>
<Button Grid.Column="3"
Margin="0 0 2 0"
Command="{Binding ResetToDefault}"
Classes="icon-button"
ToolTip.Tip="Reset the property to its default value."
Width="24"
Height="24">
<avalonia:MaterialIcon Kind="BackupRestore" />
</Button>
<ToggleButton Grid.Column="4" Classes="icon-button"
Margin="2 0 0 0"
ToolTip.Tip="Change the property's data binding"
Width="24"
Height="24"
VerticalAlignment="Center"
IsEnabled="{Binding LayerProperty.DataBindingsSupported}"
IsChecked="{Binding HasDataBinding, Mode=OneWay}">
<avalonia:MaterialIcon Kind="VectorLink" />
</ToggleButton>
</Grid>
</Border>
</UserControl>
@@ -0,0 +1,30 @@
using System;
using System.Reactive.Linq;
using Artemis.Core;
using Avalonia.Controls;
using Avalonia.Controls.Mixins;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Tree;
public class TreePropertyView : ReactiveUserControl<ITreePropertyViewModel>
{
public TreePropertyView()
{
this.WhenActivated(d =>
{
if (ViewModel != null)
Observable.FromEventPattern<LayerPropertyEventArgs>(e => ViewModel.BaseLayerProperty.CurrentValueSet += e, e => ViewModel.BaseLayerProperty.CurrentValueSet -= e)
.Subscribe(_ => this.BringIntoView())
.DisposeWith(d);
});
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
@@ -0,0 +1,76 @@
using System;
using System.Reactive;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Artemis.UI.Shared.Services.PropertyInput;
using Avalonia.Controls.Mixins;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Tree;
internal class TreePropertyViewModel<T> : ActivatableViewModelBase, ITreePropertyViewModel
{
private readonly IProfileEditorService _profileEditorService;
private TimeSpan _time;
public TreePropertyViewModel(LayerProperty<T> layerProperty, PropertyViewModel propertyViewModel, IProfileEditorService profileEditorService, IPropertyInputService propertyInputService)
{
_profileEditorService = profileEditorService;
LayerProperty = layerProperty;
PropertyViewModel = propertyViewModel;
PropertyInputViewModel = propertyInputService.CreatePropertyInputViewModel(LayerProperty);
this.WhenActivated(d =>
{
_profileEditorService.Time.Subscribe(t => _time = t).DisposeWith(d);
this.WhenAnyValue(vm => vm.LayerProperty.KeyframesEnabled).Subscribe(_ => this.RaisePropertyChanged(nameof(KeyframesEnabled))).DisposeWith(d);
});
ResetToDefault = ReactiveCommand.Create(ExecuteResetToDefault, Observable.Return(LayerProperty.DefaultValue != null));
}
private void ExecuteResetToDefault()
{
_profileEditorService.ExecuteCommand(new ResetLayerProperty<T>(LayerProperty));
}
public LayerProperty<T> LayerProperty { get; }
public PropertyViewModel PropertyViewModel { get; }
public PropertyInputViewModel<T>? PropertyInputViewModel { get; }
public ReactiveCommand<Unit, Unit> ResetToDefault { get; }
public bool KeyframesEnabled
{
get => LayerProperty.KeyframesEnabled;
set => UpdateKeyframesEnabled(value);
}
private void UpdateKeyframesEnabled(bool value)
{
if (value == LayerProperty.KeyframesEnabled)
return;
_profileEditorService.ExecuteCommand(new ToggleLayerPropertyKeyframes<T>(LayerProperty, value, _time));
}
public ILayerProperty BaseLayerProperty => LayerProperty;
public bool HasDataBinding => LayerProperty.HasDataBinding;
public double GetDepth()
{
int depth = 0;
LayerPropertyGroup? current = LayerProperty.LayerPropertyGroup;
while (current != null)
{
depth++;
current = current.Parent;
}
return depth;
}
}
@@ -0,0 +1,16 @@
<controls:CoreWindow 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"
mc:Ignorable="d"
d:DesignWidth="800"
d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Windows.BrushConfigurationWindowView"
Title="Artemis | Brush configuration"
Width="{Binding Configuration.DialogWidth}"
Height="{Binding Configuration.DialogHeight}">
<Panel>
<ContentControl Content="{Binding ConfigurationViewModel}" />
</Panel>
</controls:CoreWindow>
Loaded 100 of 1110 files, more files were not shown because too many files have changed in this diff. Show more