mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-11-11 11:26:27 +00:00
Profile editor - Moved files
This commit is contained in:
1 parent
f359256ede
commit
75e2fb1689
86 files changed
+199
-215
No files matched your search
@@ -0,0 +1,29 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.Dialogs.ProfileCreateView"
|
||||
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}">
|
||||
Add a new profile
|
||||
</TextBlock>
|
||||
|
||||
<TextBox materialDesign:HintAssist.Hint="Profile name"
|
||||
Margin="0 8 0 16"
|
||||
Style="{StaticResource MaterialDesignFloatingHintTextBox}"
|
||||
Text="{Binding ProfileName, UpdateSourceTrigger=PropertyChanged}" />
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Style="{StaticResource MaterialDesignFlatButton}" IsCancel="True" Margin="0 8 8 0" Command="{s:Action Cancel}">
|
||||
CANCEL
|
||||
</Button>
|
||||
<Button Style="{StaticResource MaterialDesignFlatButton}" IsDefault="True" Margin="0 8 8 0" Command="{s:Action Accept}">
|
||||
ACCEPT
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.UI.Shared.Services.Dialog;
|
||||
using FluentValidation;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Dialogs
|
||||
{
|
||||
public class ProfileCreateViewModel : DialogViewModelBase
|
||||
{
|
||||
private string _profileName;
|
||||
|
||||
public ProfileCreateViewModel(IModelValidator<ProfileCreateViewModel> validator) : base(validator)
|
||||
{
|
||||
}
|
||||
|
||||
public string ProfileName
|
||||
{
|
||||
get => _profileName;
|
||||
set => SetAndNotify(ref _profileName, value);
|
||||
}
|
||||
|
||||
public async Task Accept()
|
||||
{
|
||||
await ValidateAsync();
|
||||
|
||||
if (HasErrors)
|
||||
return;
|
||||
|
||||
Session.Close(ProfileName);
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
Session.Close();
|
||||
}
|
||||
}
|
||||
|
||||
public class ProfileCreateViewModelValidator : AbstractValidator<ProfileCreateViewModel>
|
||||
{
|
||||
public ProfileCreateViewModelValidator()
|
||||
{
|
||||
RuleFor(m => m.ProfileName).NotEmpty().WithMessage("Profile name may not be empty");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.Dialogs.ProfileExportView"
|
||||
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.Dialogs"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
<Grid Margin="16" Width="500">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}" Grid.Row="0">
|
||||
Export current profile
|
||||
</TextBlock>
|
||||
|
||||
<TextBlock Margin="0 10" TextAlignment="Justify" TextWrapping="Wrap" Grid.Row="1">
|
||||
Please note that exporting profiles like this is placeholder functionality. The idea is that this will eventually happen via the workshop.
|
||||
</TextBlock>
|
||||
|
||||
<TextBlock Margin="0 10" TextAlignment="Justify" TextWrapping="Wrap" Grid.Row="2">
|
||||
The workshop will include tools to make profiles convert easily and look good on different layouts.
|
||||
That means right now when someone imports this export unless they have the exact same setup as
|
||||
you, they'll have to select LEDs for each layer in the profile.
|
||||
</TextBlock>
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Grid.Row="3">
|
||||
<Button Style="{StaticResource MaterialDesignFlatButton}" IsCancel="True" Margin="0 8 8 0" Command="{s:Action Cancel}">
|
||||
CANCEL
|
||||
</Button>
|
||||
<Button Style="{StaticResource MaterialDesignFlatButton}" IsDefault="True" Margin="0 8 0 0" Command="{s:Action Accept}">
|
||||
I UNDERSTAND, EXPORT
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Windows;
|
||||
using Artemis.Core.Models.Profile;
|
||||
using Artemis.Core.Services.Storage.Interfaces;
|
||||
using Artemis.UI.Shared.Services.Dialog;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Dialogs
|
||||
{
|
||||
public class ProfileExportViewModel : DialogViewModelBase
|
||||
{
|
||||
private readonly ISnackbarMessageQueue _mainMessageQueue;
|
||||
|
||||
private readonly IProfileService _profileService;
|
||||
|
||||
public ProfileExportViewModel(ProfileDescriptor profileDescriptor, IProfileService profileService, ISnackbarMessageQueue mainMessageQueue)
|
||||
{
|
||||
ProfileDescriptor = profileDescriptor;
|
||||
|
||||
_profileService = profileService;
|
||||
_mainMessageQueue = mainMessageQueue;
|
||||
}
|
||||
|
||||
public ProfileDescriptor ProfileDescriptor { get; }
|
||||
|
||||
public void Accept()
|
||||
{
|
||||
var encoded = _profileService.ExportProfile(ProfileDescriptor);
|
||||
Clipboard.SetText(encoded);
|
||||
_mainMessageQueue.Enqueue("Profile contents exported to clipboard.");
|
||||
|
||||
Session.Close();
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
Session.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.Dialogs.ProfileImportView"
|
||||
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.Dialogs"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
xmlns:avalonedit="http://icsharpcode.net/sharpdevelop/avalonedit"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
<Grid Margin="16" Width="800">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}" Grid.Row="0">
|
||||
Import profile to current module
|
||||
</TextBlock>
|
||||
|
||||
<TextBlock Margin="0 10" TextAlignment="Justify" TextWrapping="Wrap" Grid.Row="1">
|
||||
Please note that importing profiles like this is placeholder functionality. The idea is that this will eventually happen via the workshop.
|
||||
</TextBlock>
|
||||
|
||||
<TextBlock Margin="0 10" TextAlignment="Justify" TextWrapping="Wrap" Grid.Row="2">
|
||||
The workshop will include tools to make profiles convert easily and look good on different layouts.
|
||||
That means right now when you import this profile unless you have the exact same setup as
|
||||
the person who exported it, you'll have to select LEDs for each layer in the profile.
|
||||
</TextBlock>
|
||||
|
||||
<avalonedit:TextEditor Grid.Row="3" SyntaxHighlighting="JavaScript"
|
||||
FontFamily="pack://application:,,,/Resources/Fonts/#Roboto Mono"
|
||||
FontSize="10pt"
|
||||
Document="{Binding Document}"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
Height="500"
|
||||
Padding="10" />
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Grid.Row="4">
|
||||
<Button Style="{StaticResource MaterialDesignFlatButton}" IsCancel="True" Margin="0 8 8 0" Command="{s:Action Cancel}">
|
||||
CANCEL
|
||||
</Button>
|
||||
<Button Style="{StaticResource MaterialDesignFlatButton}" IsDefault="True" Margin="0 8 0 0" Command="{s:Action Accept}">
|
||||
I UNDERSTAND, IMPORT
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,45 @@
|
||||
using Artemis.Core.Plugins.Abstract;
|
||||
using Artemis.Core.Services.Storage.Interfaces;
|
||||
using Artemis.UI.Shared.Services.Dialog;
|
||||
using ICSharpCode.AvalonEdit.Document;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Dialogs
|
||||
{
|
||||
public class ProfileImportViewModel : DialogViewModelBase
|
||||
{
|
||||
private readonly ISnackbarMessageQueue _mainMessageQueue;
|
||||
private readonly IProfileService _profileService;
|
||||
private string _profileJson;
|
||||
|
||||
public ProfileImportViewModel(ProfileModule profileModule, IProfileService profileService, ISnackbarMessageQueue mainMessageQueue)
|
||||
{
|
||||
ProfileModule = profileModule;
|
||||
Document = new TextDocument();
|
||||
|
||||
_profileService = profileService;
|
||||
_mainMessageQueue = mainMessageQueue;
|
||||
}
|
||||
|
||||
public ProfileModule ProfileModule { get; }
|
||||
public TextDocument Document { get; set; }
|
||||
|
||||
public string ProfileJson
|
||||
{
|
||||
get => _profileJson;
|
||||
set => SetAndNotify(ref _profileJson, value);
|
||||
}
|
||||
|
||||
public void Accept()
|
||||
{
|
||||
_profileService.ImportProfile(Document.Text, ProfileModule);
|
||||
_mainMessageQueue.Enqueue("Profile imported.");
|
||||
Session.Close();
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
Session.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<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"
|
||||
Style="{StaticResource MaterialDesignFloatingHintTextBox}"
|
||||
Text="{Binding ElementName, UpdateSourceTrigger=PropertyChanged}"
|
||||
Loaded="FrameworkElement_OnLoaded" />
|
||||
|
||||
<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 8 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>
|
||||
@@ -0,0 +1,24 @@
|
||||
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)
|
||||
{
|
||||
var textBox = (TextBox) sender;
|
||||
Keyboard.Focus(textBox);
|
||||
textBox.SelectAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.UI.Shared.Services.Dialog;
|
||||
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 void Cancel()
|
||||
{
|
||||
Session.Close();
|
||||
}
|
||||
}
|
||||
|
||||
public class ProfileElementRenameViewModelValidator : AbstractValidator<RenameViewModel>
|
||||
{
|
||||
public ProfileElementRenameViewModelValidator()
|
||||
{
|
||||
RuleFor(m => m.ElementName).NotEmpty().WithMessage("Element name may not be empty");
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using Artemis.Core.Models.Profile.Conditions.Abstract;
|
||||
using Artemis.UI.Shared.DataModelVisualization.Shared;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions.Abstract
|
||||
{
|
||||
public abstract class DisplayConditionViewModel : PropertyChangedBase, IDisposable
|
||||
{
|
||||
protected DisplayConditionViewModel(DisplayConditionPart model, DisplayConditionViewModel parent)
|
||||
{
|
||||
Model = model;
|
||||
Parent = parent;
|
||||
Children = new BindableCollection<DisplayConditionViewModel>();
|
||||
}
|
||||
|
||||
public DisplayConditionPart Model { get; }
|
||||
public DisplayConditionViewModel Parent { get; set; }
|
||||
public BindableCollection<DisplayConditionViewModel> Children { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var child in Children)
|
||||
child.Dispose();
|
||||
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public abstract void Update();
|
||||
|
||||
public virtual DataModelPropertiesViewModel GetDataModelOverride()
|
||||
{
|
||||
return Parent?.GetDataModelOverride();
|
||||
}
|
||||
|
||||
public virtual void Delete()
|
||||
{
|
||||
Model.Parent.RemoveChild(Model);
|
||||
Parent.Update();
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.DisplayConditions.DisplayConditionGroupView"
|
||||
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"
|
||||
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 Type=local:DisplayConditionGroupViewModel, IsDesignTimeCreatable=False}">
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/Artemis.UI;component/ResourceDictionaries/DisplayConditions.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
ToolTip="Delete the group and all its children"
|
||||
Style="{StaticResource MaterialDesignIconForegroundButton}"
|
||||
HorizontalAlignment="Left"
|
||||
Visibility="{Binding IsRootGroup, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}"
|
||||
Width="25"
|
||||
Height="25"
|
||||
Foreground="#E74C4C"
|
||||
Command="{s:Action Delete}">
|
||||
<materialDesign:PackIcon Kind="Close" Width="18" Height="18" />
|
||||
</Button>
|
||||
<Button Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
ToolTip="Change the operator of the group"
|
||||
Style="{StaticResource DisplayConditionButtonLeftClickMenu}"
|
||||
Background="#E74C4C"
|
||||
BorderBrush="#E74C4C"
|
||||
Margin="3 1"
|
||||
Content="{Binding SelectedBooleanOperator}"
|
||||
Visibility="{Binding DisplayBooleanOperator, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
|
||||
<Button.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="And" Command="{s:Action SelectBooleanOperator}" CommandParameter="And" />
|
||||
<MenuItem Header="Or" Command="{s:Action SelectBooleanOperator}" CommandParameter="Or" />
|
||||
<MenuItem Header="And not" Command="{s:Action SelectBooleanOperator}" CommandParameter="AndNot" />
|
||||
<MenuItem Header="Or not" Command="{s:Action SelectBooleanOperator}" CommandParameter="OrNot" />
|
||||
</ContextMenu>
|
||||
</Button.ContextMenu>
|
||||
</Button>
|
||||
<Button x:Name="AddChildButton"
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
ToolTip="Add another condition to the current group"
|
||||
HorizontalAlignment="Left"
|
||||
Foreground="#4CE758"
|
||||
Width="25"
|
||||
Height="25">
|
||||
<Button.Style>
|
||||
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource MaterialDesignIconForegroundButton}">
|
||||
<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>
|
||||
<materialDesign:PackIcon Kind="Add" Width="18" Height="18" />
|
||||
<Button.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="Add static condition"
|
||||
ToolTip="A condition that compares a data model property to a static input"
|
||||
Command="{s:Action AddCondition}"
|
||||
CommandParameter="Static">
|
||||
<MenuItem.Icon>
|
||||
<materialDesign:PackIcon Kind="FormTextarea" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Add dynamic condition"
|
||||
ToolTip="A condition that compares two data model properties"
|
||||
Command="{s:Action AddCondition}"
|
||||
CommandParameter="Dynamic">
|
||||
<MenuItem.Icon>
|
||||
<materialDesign:PackIcon Kind="InsertLink" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Add list condition"
|
||||
ToolTip="A condition that evaluates on items in a list"
|
||||
Command="{s:Action AddCondition}"
|
||||
CommandParameter="List">
|
||||
<MenuItem.Icon>
|
||||
<materialDesign:PackIcon Kind="FormatListBulleted" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Add group" ToolTip="A group can contain conditions and other groups" Command="{s:Action AddGroup}">
|
||||
<MenuItem.Icon>
|
||||
<materialDesign:PackIcon Kind="CodeParentheses" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
</ContextMenu>
|
||||
</Button.ContextMenu>
|
||||
</Button>
|
||||
|
||||
<ItemsControl Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" ItemsSource="{Binding Children}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<materialDesign:TransitioningContent>
|
||||
<materialDesign:TransitioningContent.OpeningEffects>
|
||||
<materialDesign:TransitionEffect Kind="FadeIn" />
|
||||
<materialDesign:TransitionEffect Kind="SlideInFromLeft" />
|
||||
</materialDesign:TransitioningContent.OpeningEffects>
|
||||
<ContentControl s:View.Model="{Binding}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" IsTabStop="False" />
|
||||
</materialDesign:TransitioningContent>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using Artemis.Core.Models.Profile.Conditions;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Screens.ProfileEditor.DisplayConditions.Abstract;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Humanizer;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions
|
||||
{
|
||||
public class DisplayConditionGroupViewModel : DisplayConditionViewModel, IViewAware
|
||||
{
|
||||
private readonly IDisplayConditionsVmFactory _displayConditionsVmFactory;
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private bool _isInitialized;
|
||||
private bool _isRootGroup;
|
||||
|
||||
public DisplayConditionGroupViewModel(DisplayConditionGroup displayConditionGroup, DisplayConditionViewModel parent,
|
||||
IProfileEditorService profileEditorService, IDisplayConditionsVmFactory displayConditionsVmFactory) : base(displayConditionGroup, parent)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
_displayConditionsVmFactory = displayConditionsVmFactory;
|
||||
|
||||
Children.CollectionChanged += (sender, args) => NotifyOfPropertyChange(nameof(DisplayBooleanOperator));
|
||||
|
||||
Execute.PostToUIThread(async () =>
|
||||
{
|
||||
await Task.Delay(50);
|
||||
IsInitialized = true;
|
||||
});
|
||||
}
|
||||
|
||||
public DisplayConditionGroup DisplayConditionGroup => (DisplayConditionGroup) Model;
|
||||
|
||||
public bool IsRootGroup
|
||||
{
|
||||
get => _isRootGroup;
|
||||
set => SetAndNotify(ref _isRootGroup, value);
|
||||
}
|
||||
|
||||
public bool IsInitialized
|
||||
{
|
||||
get => _isInitialized;
|
||||
set => SetAndNotify(ref _isInitialized, value);
|
||||
}
|
||||
|
||||
public bool DisplayBooleanOperator => Children.Count > 1;
|
||||
public string SelectedBooleanOperator => DisplayConditionGroup.BooleanOperator.Humanize();
|
||||
|
||||
public void AttachView(UIElement view)
|
||||
{
|
||||
View = view;
|
||||
}
|
||||
|
||||
public UIElement View { get; set; }
|
||||
|
||||
public void SelectBooleanOperator(string type)
|
||||
{
|
||||
var enumValue = Enum.Parse<BooleanOperator>(type);
|
||||
DisplayConditionGroup.BooleanOperator = enumValue;
|
||||
NotifyOfPropertyChange(nameof(SelectedBooleanOperator));
|
||||
}
|
||||
|
||||
public void AddCondition(string type)
|
||||
{
|
||||
if (type == "Static")
|
||||
DisplayConditionGroup.AddChild(new DisplayConditionPredicate(DisplayConditionGroup, PredicateType.Static));
|
||||
else if (type == "Dynamic")
|
||||
DisplayConditionGroup.AddChild(new DisplayConditionPredicate(DisplayConditionGroup, PredicateType.Dynamic));
|
||||
else if (type == "List")
|
||||
DisplayConditionGroup.AddChild(new DisplayConditionList(DisplayConditionGroup));
|
||||
|
||||
Update();
|
||||
_profileEditorService.UpdateSelectedProfileElement();
|
||||
}
|
||||
|
||||
public void AddGroup()
|
||||
{
|
||||
DisplayConditionGroup.AddChild(new DisplayConditionGroup(DisplayConditionGroup));
|
||||
|
||||
Update();
|
||||
_profileEditorService.UpdateSelectedProfileElement();
|
||||
}
|
||||
|
||||
public override void Update()
|
||||
{
|
||||
NotifyOfPropertyChange(nameof(SelectedBooleanOperator));
|
||||
|
||||
// Remove VMs of effects no longer applied on the layer
|
||||
var toRemove = Children.Where(c => !DisplayConditionGroup.Children.Contains(c.Model)).ToList();
|
||||
// Using RemoveRange breaks our lovely animations
|
||||
foreach (var displayConditionViewModel in toRemove)
|
||||
{
|
||||
Children.Remove(displayConditionViewModel);
|
||||
displayConditionViewModel.Dispose();
|
||||
}
|
||||
|
||||
foreach (var childModel in Model.Children)
|
||||
{
|
||||
if (Children.Any(c => c.Model == childModel))
|
||||
continue;
|
||||
|
||||
switch (childModel)
|
||||
{
|
||||
case DisplayConditionGroup displayConditionGroup:
|
||||
Children.Add(_displayConditionsVmFactory.DisplayConditionGroupViewModel(displayConditionGroup, this));
|
||||
break;
|
||||
case DisplayConditionList displayConditionListPredicate:
|
||||
Children.Add(_displayConditionsVmFactory.DisplayConditionListViewModel(displayConditionListPredicate, this));
|
||||
break;
|
||||
case DisplayConditionPredicate displayConditionPredicate:
|
||||
Children.Add(_displayConditionsVmFactory.DisplayConditionPredicateViewModel(displayConditionPredicate, this));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var childViewModel in Children)
|
||||
childViewModel.Update();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.DisplayConditions.DisplayConditionListView"
|
||||
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"
|
||||
xmlns:converters="clr-namespace:Artemis.UI.Converters"
|
||||
xmlns:utilities="clr-namespace:Artemis.UI.Utilities"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance Type=local:DisplayConditionListViewModel, IsDesignTimeCreatable=False}">
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/Artemis.UI;component/ResourceDictionaries/DisplayConditions.xaml" />
|
||||
<ResourceDictionary>
|
||||
<converters:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
|
||||
<utilities:BindingProxy x:Key="DataContextProxy" Data="{Binding}" />
|
||||
<DataTemplate x:Key="DataModelDataTemplate">
|
||||
<Control x:Name="TemplateControl" Focusable="False" Template="{StaticResource DataModelSelectionTemplate}" />
|
||||
<DataTemplate.Triggers>
|
||||
<DataTrigger Binding="{Binding Data.ShowDataModelValues.Value, Source={StaticResource DataContextProxy}}" Value="True">
|
||||
<Setter TargetName="TemplateControl" Property="Template" Value="{StaticResource DataModelSelectionTemplateWithValues}" />
|
||||
</DataTrigger>
|
||||
</DataTemplate.Triggers>
|
||||
</DataTemplate>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
<Grid Margin="0 3" Visibility="{Binding IsInitialized, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
ToolTip="Delete the list predicate and all its children"
|
||||
Style="{StaticResource MaterialDesignIconForegroundButton}"
|
||||
HorizontalAlignment="Left"
|
||||
Foreground="#E74C4C"
|
||||
Width="25"
|
||||
Height="25"
|
||||
Command="{s:Action Delete}">
|
||||
<materialDesign:PackIcon Kind="Close" Width="18" Height="18" />
|
||||
</Button>
|
||||
|
||||
<!-- Left side, the list this predicate is targeting -->
|
||||
<Button Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Background="#ab47bc"
|
||||
BorderBrush="#ab47bc"
|
||||
Style="{StaticResource DisplayConditionButton}"
|
||||
ToolTip="{Binding SelectedListProperty.DisplayPropertyPath}"
|
||||
Click="PropertyButton_OnClick">
|
||||
<Button.ContextMenu>
|
||||
<ContextMenu ItemsSource="{Binding TargetDataModel.Children}" IsOpen="{Binding TargetDataModelOpen, Mode=OneWayToSource}">
|
||||
<ContextMenu.ItemContainerStyle>
|
||||
<Style TargetType="{x:Type MenuItem}" BasedOn="{StaticResource MaterialDesignMenuItem}">
|
||||
<Setter Property="ItemsSource" Value="{Binding Children}" />
|
||||
<Setter Property="Command" Value="{Binding Data.SelectListPropertyCommand, Source={StaticResource DataContextProxy}}" />
|
||||
<Setter Property="CommandParameter" Value="{Binding}" />
|
||||
<Setter Property="CommandTarget" Value="{Binding}" />
|
||||
<Setter Property="IsEnabled" Value="{Binding IsMatchingFilteredTypes}" />
|
||||
<Setter Property="IsSubmenuOpen" Value="{Binding IsVisualizationExpanded, Mode=TwoWay}" />
|
||||
<Setter Property="HeaderTemplate" Value="{StaticResource DataModelDataTemplate}" />
|
||||
</Style>
|
||||
</ContextMenu.ItemContainerStyle>
|
||||
</ContextMenu>
|
||||
</Button.ContextMenu>
|
||||
<Grid>
|
||||
<TextBlock Text="{Binding SelectedListProperty.PropertyDescription.Name}"
|
||||
Visibility="{Binding SelectedListProperty, Converter={StaticResource NullToVisibilityConverter}}" />
|
||||
<TextBlock Text="« Select a list »"
|
||||
FontStyle="Italic"
|
||||
Visibility="{Binding SelectedListProperty, Converter={StaticResource NullToVisibilityConverter}, ConverterParameter=Inverted}" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<!-- Operator -->
|
||||
<Button Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Style="{StaticResource DisplayConditionButtonLeftClickMenu}"
|
||||
Background="#7B7B7B"
|
||||
BorderBrush="#7B7B7B"
|
||||
Content="{Binding SelectedListOperator}"
|
||||
Click="PropertyButton_OnClick"
|
||||
HorizontalAlignment="Left">
|
||||
<Button.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="Any" Command="{s:Action SelectListOperator}" CommandParameter="Any" />
|
||||
<MenuItem Header="All" Command="{s:Action SelectListOperator}" CommandParameter="All" />
|
||||
<MenuItem Header="None" Command="{s:Action SelectListOperator}" CommandParameter="None" />
|
||||
<MenuItem Header="Count (NYI)" Command="{s:Action SelectListOperator}" CommandParameter="Count" IsEnabled="False"/>
|
||||
</ContextMenu>
|
||||
</Button.ContextMenu>
|
||||
</Button>
|
||||
|
||||
<ItemsControl Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" ItemsSource="{Binding Children}" Margin="0 4 0 0">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<materialDesign:TransitioningContent>
|
||||
<materialDesign:TransitioningContent.OpeningEffects>
|
||||
<materialDesign:TransitionEffect Kind="FadeIn" />
|
||||
<materialDesign:TransitionEffect Kind="SlideInFromLeft" />
|
||||
</materialDesign:TransitioningContent.OpeningEffects>
|
||||
<ContentControl s:View.Model="{Binding}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" IsTabStop="False" />
|
||||
</materialDesign:TransitioningContent>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for DisplayConditionListView.xaml
|
||||
/// </summary>
|
||||
public partial class DisplayConditionListView : UserControl
|
||||
{
|
||||
public DisplayConditionListView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void PropertyButton_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// DataContext is not set when using left button, I don't know why but there it is
|
||||
if (sender is Button button && button.ContextMenu != null)
|
||||
{
|
||||
button.ContextMenu.DataContext = button.DataContext;
|
||||
button.ContextMenu.IsOpen = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Timers;
|
||||
using Artemis.Core.Models.Profile.Conditions;
|
||||
using Artemis.Core.Plugins.Models;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Screens.ProfileEditor.DisplayConditions.Abstract;
|
||||
using Artemis.UI.Shared.DataModelVisualization.Shared;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Artemis.UI.Utilities;
|
||||
using Humanizer;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions
|
||||
{
|
||||
public class DisplayConditionListViewModel : DisplayConditionViewModel
|
||||
{
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private readonly IDataModelVisualizationService _dataModelVisualizationService;
|
||||
private readonly IDisplayConditionsVmFactory _displayConditionsVmFactory;
|
||||
private bool _isInitialized;
|
||||
private DataModelListViewModel _selectedListProperty;
|
||||
private DataModelPropertiesViewModel _targetDataModel;
|
||||
private readonly Timer _updateTimer;
|
||||
|
||||
public DisplayConditionListViewModel(
|
||||
DisplayConditionList displayConditionList,
|
||||
DisplayConditionViewModel parent,
|
||||
IProfileEditorService profileEditorService,
|
||||
IDataModelVisualizationService dataModelVisualizationService,
|
||||
IDisplayConditionsVmFactory displayConditionsVmFactory,
|
||||
ISettingsService settingsService) : base(displayConditionList, parent)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
_dataModelVisualizationService = dataModelVisualizationService;
|
||||
_displayConditionsVmFactory = displayConditionsVmFactory;
|
||||
_updateTimer = new Timer(500);
|
||||
|
||||
SelectListPropertyCommand = new DelegateCommand(ExecuteSelectListProperty);
|
||||
|
||||
ShowDataModelValues = settingsService.GetSetting<bool>("ProfileEditor.ShowDataModelValues");
|
||||
|
||||
// Initialize async, no need to wait for it
|
||||
Task.Run(Initialize);
|
||||
}
|
||||
|
||||
public DelegateCommand SelectListPropertyCommand { get; }
|
||||
public PluginSetting<bool> ShowDataModelValues { get; }
|
||||
|
||||
public DisplayConditionList DisplayConditionList => (DisplayConditionList) Model;
|
||||
|
||||
public bool IsInitialized
|
||||
{
|
||||
get => _isInitialized;
|
||||
set => SetAndNotify(ref _isInitialized, value);
|
||||
}
|
||||
|
||||
public bool TargetDataModelOpen { get; set; }
|
||||
|
||||
public DataModelPropertiesViewModel TargetDataModel
|
||||
{
|
||||
get => _targetDataModel;
|
||||
set => SetAndNotify(ref _targetDataModel, value);
|
||||
}
|
||||
|
||||
public DataModelListViewModel SelectedListProperty
|
||||
{
|
||||
get => _selectedListProperty;
|
||||
set => SetAndNotify(ref _selectedListProperty, value);
|
||||
}
|
||||
|
||||
public string SelectedListOperator => DisplayConditionList.ListOperator.Humanize();
|
||||
|
||||
public void SelectListOperator(string type)
|
||||
{
|
||||
var enumValue = Enum.Parse<ListOperator>(type);
|
||||
DisplayConditionList.ListOperator = enumValue;
|
||||
NotifyOfPropertyChange(nameof(SelectedListOperator));
|
||||
}
|
||||
|
||||
public void AddCondition(string type)
|
||||
{
|
||||
if (type == "Static")
|
||||
DisplayConditionList.AddChild(new DisplayConditionPredicate(DisplayConditionList, PredicateType.Static));
|
||||
else if (type == "Dynamic")
|
||||
DisplayConditionList.AddChild(new DisplayConditionPredicate(DisplayConditionList, PredicateType.Dynamic));
|
||||
|
||||
Update();
|
||||
_profileEditorService.UpdateSelectedProfileElement();
|
||||
}
|
||||
|
||||
public void AddGroup()
|
||||
{
|
||||
DisplayConditionList.AddChild(new DisplayConditionGroup(DisplayConditionList));
|
||||
|
||||
Update();
|
||||
_profileEditorService.UpdateSelectedProfileElement();
|
||||
}
|
||||
|
||||
public override void Delete()
|
||||
{
|
||||
base.Delete();
|
||||
_profileEditorService.UpdateSelectedProfileElement();
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
// Get the data models
|
||||
TargetDataModel = _dataModelVisualizationService.GetMainDataModelVisualization();
|
||||
if (!_dataModelVisualizationService.GetPluginExtendsDataModel(_profileEditorService.GetCurrentModule()))
|
||||
TargetDataModel.Children.Add(_dataModelVisualizationService.GetPluginDataModelVisualization(_profileEditorService.GetCurrentModule()));
|
||||
|
||||
TargetDataModel.UpdateRequested += TargetDataModelUpdateRequested;
|
||||
|
||||
Update();
|
||||
|
||||
_updateTimer.Start();
|
||||
_updateTimer.Elapsed += OnUpdateTimerOnElapsed;
|
||||
|
||||
IsInitialized = true;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
_updateTimer.Stop();
|
||||
_updateTimer.Elapsed -= OnUpdateTimerOnElapsed;
|
||||
}
|
||||
|
||||
private void OnUpdateTimerOnElapsed(object sender, ElapsedEventArgs e)
|
||||
{
|
||||
if (TargetDataModelOpen)
|
||||
{
|
||||
TargetDataModel?.Update(_dataModelVisualizationService);
|
||||
SelectedListProperty?.Update(_dataModelVisualizationService);
|
||||
}
|
||||
}
|
||||
|
||||
private void TargetDataModelUpdateRequested(object sender, EventArgs e)
|
||||
{
|
||||
TargetDataModel.ApplyTypeFilter(true, typeof(IList));
|
||||
}
|
||||
|
||||
public void ApplyList()
|
||||
{
|
||||
DisplayConditionList.UpdateList(SelectedListProperty.DataModel, SelectedListProperty.PropertyPath);
|
||||
_profileEditorService.UpdateSelectedProfileElement();
|
||||
|
||||
Update();
|
||||
}
|
||||
|
||||
public override DataModelPropertiesViewModel GetDataModelOverride()
|
||||
{
|
||||
if (SelectedListProperty != null)
|
||||
return SelectedListProperty.GetListTypeViewModel(_dataModelVisualizationService);
|
||||
|
||||
return base.GetDataModelOverride();
|
||||
}
|
||||
|
||||
public override void Update()
|
||||
{
|
||||
if (TargetDataModel == null)
|
||||
return;
|
||||
|
||||
NotifyOfPropertyChange(nameof(SelectedListOperator));
|
||||
|
||||
// Update the selected list property
|
||||
if (DisplayConditionList.ListDataModel != null && DisplayConditionList.ListPropertyPath != null)
|
||||
{
|
||||
var child = TargetDataModel.GetChildByPath(
|
||||
DisplayConditionList.ListDataModel.PluginInfo.Guid,
|
||||
DisplayConditionList.ListPropertyPath
|
||||
);
|
||||
SelectedListProperty = child as DataModelListViewModel;
|
||||
}
|
||||
|
||||
// Ensure filtering is applied to include Enumerables only
|
||||
TargetDataModel.ApplyTypeFilter(true, typeof(IList));
|
||||
|
||||
// Remove VMs of effects no longer applied on the layer
|
||||
var toRemove = Children.Where(c => !DisplayConditionList.Children.Contains(c.Model)).ToList();
|
||||
// Using RemoveRange breaks our lovely animations
|
||||
foreach (var displayConditionViewModel in toRemove)
|
||||
{
|
||||
Children.Remove(displayConditionViewModel);
|
||||
displayConditionViewModel.Dispose();
|
||||
}
|
||||
|
||||
foreach (var childModel in Model.Children)
|
||||
{
|
||||
if (Children.Any(c => c.Model == childModel))
|
||||
continue;
|
||||
if (!(childModel is DisplayConditionGroup displayConditionGroup))
|
||||
continue;
|
||||
|
||||
var viewModel = _displayConditionsVmFactory.DisplayConditionGroupViewModel(displayConditionGroup, this);
|
||||
viewModel.IsRootGroup = true;
|
||||
Children.Add(viewModel);
|
||||
}
|
||||
|
||||
foreach (var childViewModel in Children)
|
||||
childViewModel.Update();
|
||||
}
|
||||
|
||||
|
||||
private void ExecuteSelectListProperty(object context)
|
||||
{
|
||||
if (!(context is DataModelListViewModel dataModelListViewModel))
|
||||
return;
|
||||
|
||||
SelectedListProperty = dataModelListViewModel;
|
||||
ApplyList();
|
||||
}
|
||||
}
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
<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"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:converters="clr-namespace:Artemis.UI.Converters"
|
||||
xmlns:utilities="clr-namespace:Artemis.UI.Utilities"
|
||||
xmlns:displayConditions="clr-namespace:Artemis.UI.Screens.ProfileEditor.DisplayConditions"
|
||||
x:Class="Artemis.UI.Screens.ProfileEditor.DisplayConditions.DisplayConditionPredicateView"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance IsDesignTimeCreatable=False, Type={x:Type displayConditions:DisplayConditionPredicateViewModel}}">
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/Artemis.UI;component/ResourceDictionaries/DisplayConditions.xaml" />
|
||||
<ResourceDictionary>
|
||||
<converters:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
|
||||
<utilities:BindingProxy x:Key="DataContextProxy" Data="{Binding}" />
|
||||
<DataTemplate x:Key="DataModelDataTemplate">
|
||||
<Control x:Name="TemplateControl" Focusable="False" Template="{StaticResource DataModelSelectionTemplate}" />
|
||||
<DataTemplate.Triggers>
|
||||
<DataTrigger Binding="{Binding Data.ShowDataModelValues.Value, Source={StaticResource DataContextProxy}}" Value="True">
|
||||
<Setter TargetName="TemplateControl" Property="Template" Value="{StaticResource DataModelSelectionTemplateWithValues}" />
|
||||
</DataTrigger>
|
||||
</DataTemplate.Triggers>
|
||||
</DataTemplate>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
<Grid Margin="0 3" Visibility="{Binding IsInitialized, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
ToolTip="Delete the predicate"
|
||||
Style="{StaticResource MaterialDesignIconForegroundButton}"
|
||||
HorizontalAlignment="Left"
|
||||
Foreground="#E74C4C"
|
||||
Width="25"
|
||||
Height="25"
|
||||
Command="{s:Action Delete}">
|
||||
<materialDesign:PackIcon Kind="Close" Width="18" Height="18" />
|
||||
</Button>
|
||||
|
||||
<!-- Left side, always a property -->
|
||||
<Button Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Background="#ab47bc"
|
||||
BorderBrush="#ab47bc"
|
||||
Style="{StaticResource DisplayConditionButton}"
|
||||
ToolTip="{Binding SelectedLeftSideProperty.DisplayPropertyPath}"
|
||||
Click="PropertyButton_OnClick">
|
||||
<Button.ContextMenu>
|
||||
<ContextMenu ItemsSource="{Binding LeftSideDataModel.Children}" IsOpen="{Binding LeftSideDataModelOpen, Mode=OneWayToSource}">
|
||||
<ContextMenu.ItemContainerStyle>
|
||||
<Style TargetType="{x:Type MenuItem}" BasedOn="{StaticResource MaterialDesignMenuItem}">
|
||||
<Setter Property="ItemsSource" Value="{Binding Children}" />
|
||||
<Setter Property="Command" Value="{Binding Data.SelectLeftPropertyCommand, Source={StaticResource DataContextProxy}}" />
|
||||
<Setter Property="CommandParameter" Value="{Binding}" />
|
||||
<Setter Property="CommandTarget" Value="{Binding}" />
|
||||
<Setter Property="IsEnabled" Value="{Binding IsMatchingFilteredTypes}" />
|
||||
<Setter Property="IsSubmenuOpen" Value="{Binding IsVisualizationExpanded, Mode=TwoWay}" />
|
||||
<Setter Property="HeaderTemplate" Value="{StaticResource DataModelDataTemplate}" />
|
||||
</Style>
|
||||
</ContextMenu.ItemContainerStyle>
|
||||
</ContextMenu>
|
||||
</Button.ContextMenu>
|
||||
<Grid>
|
||||
<TextBlock Text="{Binding SelectedLeftSideProperty.PropertyDescription.Name}"
|
||||
Visibility="{Binding SelectedLeftSideProperty, Converter={StaticResource NullToVisibilityConverter}}" />
|
||||
<TextBlock Text="« Select a property »"
|
||||
FontStyle="Italic"
|
||||
Visibility="{Binding SelectedLeftSideProperty, Converter={StaticResource NullToVisibilityConverter}, ConverterParameter=Inverted}" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<!-- Operator -->
|
||||
<Button Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Style="{StaticResource DisplayConditionButtonLeftClickMenu}"
|
||||
Background="#7B7B7B"
|
||||
BorderBrush="#7B7B7B"
|
||||
Content="{Binding SelectedOperator.Description}"
|
||||
Click="PropertyButton_OnClick">
|
||||
<Button.ContextMenu>
|
||||
<ContextMenu ItemsSource="{Binding Operators}">
|
||||
<ContextMenu.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="{Binding Icon}" VerticalAlignment="Center" Margin="0 0 15 0" />
|
||||
<TextBlock Text="{Binding Description}" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ContextMenu.ItemTemplate>
|
||||
<ContextMenu.ItemContainerStyle>
|
||||
<Style TargetType="{x:Type MenuItem}" BasedOn="{StaticResource MaterialDesignMenuItem}">
|
||||
<Setter Property="Command" Value="{Binding Data.SelectOperatorCommand, Source={StaticResource DataContextProxy}}" />
|
||||
<Setter Property="CommandParameter" Value="{Binding}" />
|
||||
<Setter Property="CommandTarget" Value="{Binding}" />
|
||||
</Style>
|
||||
</ContextMenu.ItemContainerStyle>
|
||||
</ContextMenu>
|
||||
</Button.ContextMenu>
|
||||
</Button>
|
||||
|
||||
<Grid Grid.Row="0"
|
||||
Grid.Column="3"
|
||||
Visibility="{Binding SelectedOperator.SupportsRightSide, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
|
||||
<!-- Right side property if type is dynamic -->
|
||||
<Button Background="{DynamicResource PrimaryHueMidBrush}"
|
||||
BorderBrush="{DynamicResource PrimaryHueMidBrush}"
|
||||
Style="{StaticResource DisplayConditionButton}"
|
||||
ToolTip="{Binding SelectedRightSideProperty.DisplayPropertyPath}"
|
||||
Click="PropertyButton_OnClick"
|
||||
Visibility="{Binding ShowRightSidePropertySelection, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
|
||||
<Button.ContextMenu>
|
||||
<ContextMenu ItemsSource="{Binding RightSideDataModel.Children}">
|
||||
<ContextMenu.ItemContainerStyle>
|
||||
<Style TargetType="{x:Type MenuItem}" BasedOn="{StaticResource MaterialDesignMenuItem}">
|
||||
<Setter Property="ItemsSource" Value="{Binding Children}" />
|
||||
<Setter Property="Command" Value="{Binding Data.SelectRightPropertyCommand, Source={StaticResource DataContextProxy}}" />
|
||||
<Setter Property="CommandParameter" Value="{Binding}" />
|
||||
<Setter Property="CommandTarget" Value="{Binding}" />
|
||||
<Setter Property="IsEnabled" Value="{Binding IsMatchingFilteredTypes}" />
|
||||
<Setter Property="IsSubmenuOpen" Value="{Binding IsVisualizationExpanded, Mode=TwoWay}" />
|
||||
<Setter Property="HeaderTemplate" Value="{StaticResource DataModelDataTemplate}" />
|
||||
</Style>
|
||||
</ContextMenu.ItemContainerStyle>
|
||||
</ContextMenu>
|
||||
</Button.ContextMenu>
|
||||
<Grid>
|
||||
<TextBlock Text="{Binding SelectedRightSideProperty.PropertyDescription.Name}"
|
||||
Visibility="{Binding SelectedRightSideProperty, Converter={StaticResource NullToVisibilityConverter}}" />
|
||||
<TextBlock Text="« Select a property »"
|
||||
FontStyle="Italic"
|
||||
Visibility="{Binding SelectedRightSideProperty, Converter={StaticResource NullToVisibilityConverter}, ConverterParameter=Inverted}" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<!-- Right side property if type is static -->
|
||||
<materialDesign:Transitioner SelectedIndex="{Binding RightSideTransitionIndex}"
|
||||
DefaultTransitionOrigin="0.5, 0.5"
|
||||
Margin="3 -4"
|
||||
Visibility="{Binding ShowRightSidePropertySelection, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}">
|
||||
<Button Style="{StaticResource DisplayConditionButton}"
|
||||
Background="{DynamicResource PrimaryHueMidBrush}"
|
||||
BorderBrush="{DynamicResource PrimaryHueMidBrush}"
|
||||
Margin="0 4"
|
||||
Command="{s:Action ActivateRightSideInputViewModel}"
|
||||
HorizontalAlignment="Left">
|
||||
<Grid>
|
||||
<StackPanel Visibility="{Binding RightStaticValue, Converter={StaticResource NullToVisibilityConverter}}" Orientation="Horizontal">
|
||||
<TextBlock FontWeight="Light"
|
||||
Text="{Binding SelectedLeftSideProperty.PropertyDescription.Prefix}"
|
||||
Visibility="{Binding SelectedLeftSideProperty.PropertyDescription.Prefix, Converter={StaticResource NullToVisibilityConverter}}" />
|
||||
<TextBlock Text="{Binding RightStaticValue}" />
|
||||
<TextBlock FontWeight="Light"
|
||||
Text="{Binding SelectedLeftSideProperty.PropertyDescription.Affix}"
|
||||
Visibility="{Binding SelectedLeftSideProperty.PropertyDescription.Affix, Converter={StaticResource NullToVisibilityConverter}}" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="« Enter a value »"
|
||||
FontStyle="Italic"
|
||||
Visibility="{Binding RightStaticValue, Converter={StaticResource NullToVisibilityConverter}, ConverterParameter=Inverted}" />
|
||||
</Grid>
|
||||
</Button>
|
||||
<Border BorderBrush="{DynamicResource PrimaryHueMidBrush}"
|
||||
Background="{DynamicResource MaterialDesignPaper}"
|
||||
CornerRadius="3"
|
||||
Padding="3"
|
||||
HorizontalAlignment="Left"
|
||||
MinWidth="140">
|
||||
<ContentControl s:View.Model="{Binding RightSideInputViewModel}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" />
|
||||
</Border>
|
||||
</materialDesign:Transitioner>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for DisplayConditionPredicateView.xaml
|
||||
/// </summary>
|
||||
public partial class DisplayConditionPredicateView : UserControl
|
||||
{
|
||||
public DisplayConditionPredicateView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void PropertyButton_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// DataContext is not set when using left button, I don't know why but there it is
|
||||
if (sender is Button button && button.ContextMenu != null)
|
||||
{
|
||||
button.ContextMenu.DataContext = button.DataContext;
|
||||
button.ContextMenu.IsOpen = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+349
@@ -0,0 +1,349 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Timers;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using Artemis.Core.Models.Profile.Conditions;
|
||||
using Artemis.Core.Plugins.Models;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.Core.Services.Interfaces;
|
||||
using Artemis.UI.Events;
|
||||
using Artemis.UI.Screens.ProfileEditor.DisplayConditions.Abstract;
|
||||
using Artemis.UI.Shared.DataModelVisualization;
|
||||
using Artemis.UI.Shared.DataModelVisualization.Shared;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Artemis.UI.Utilities;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions
|
||||
{
|
||||
public class DisplayConditionPredicateViewModel : DisplayConditionViewModel, IHandle<MainWindowKeyEvent>, IHandle<MainWindowMouseEvent>
|
||||
{
|
||||
private readonly IDataModelService _dataModelService;
|
||||
private readonly IDataModelVisualizationService _dataModelVisualizationService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private bool _isInitialized;
|
||||
private DataModelPropertiesViewModel _leftSideDataModel;
|
||||
private BindableCollection<DisplayConditionOperator> _operators;
|
||||
private DataModelPropertiesViewModel _rightSideDataModel;
|
||||
private DataModelInputViewModel _rightSideInputViewModel;
|
||||
private int _rightSideTransitionIndex;
|
||||
private object _rightStaticValue;
|
||||
private DataModelVisualizationViewModel _selectedLeftSideProperty;
|
||||
private DisplayConditionOperator _selectedOperator;
|
||||
private DataModelVisualizationViewModel _selectedRightSideProperty;
|
||||
|
||||
private List<Type> _supportedInputTypes;
|
||||
private readonly Timer _updateTimer;
|
||||
|
||||
public DisplayConditionPredicateViewModel(
|
||||
DisplayConditionPredicate displayConditionPredicate,
|
||||
DisplayConditionViewModel parent,
|
||||
IProfileEditorService profileEditorService,
|
||||
IDataModelVisualizationService dataModelVisualizationService,
|
||||
IDataModelService dataModelService,
|
||||
ISettingsService settingsService,
|
||||
IEventAggregator eventAggregator) : base(displayConditionPredicate, parent)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
_dataModelVisualizationService = dataModelVisualizationService;
|
||||
_dataModelService = dataModelService;
|
||||
_eventAggregator = eventAggregator;
|
||||
_updateTimer = new Timer(500);
|
||||
_supportedInputTypes = new List<Type>();
|
||||
|
||||
SelectLeftPropertyCommand = new DelegateCommand(ExecuteSelectLeftProperty);
|
||||
SelectRightPropertyCommand = new DelegateCommand(ExecuteSelectRightProperty);
|
||||
SelectOperatorCommand = new DelegateCommand(ExecuteSelectOperatorCommand);
|
||||
Operators = new BindableCollection<DisplayConditionOperator>();
|
||||
|
||||
ShowDataModelValues = settingsService.GetSetting<bool>("ProfileEditor.ShowDataModelValues");
|
||||
|
||||
// Initialize async, no need to wait for it
|
||||
Task.Run(Initialize);
|
||||
}
|
||||
|
||||
public DisplayConditionPredicate DisplayConditionPredicate => (DisplayConditionPredicate) Model;
|
||||
public bool ShowRightSidePropertySelection => DisplayConditionPredicate.PredicateType == PredicateType.Dynamic;
|
||||
public bool CanActivateRightSideInputViewModel => SelectedLeftSideProperty?.PropertyInfo != null;
|
||||
public PluginSetting<bool> ShowDataModelValues { get; }
|
||||
|
||||
public bool IsInitialized
|
||||
{
|
||||
get => _isInitialized;
|
||||
private set => SetAndNotify(ref _isInitialized, value);
|
||||
}
|
||||
|
||||
public bool LeftSideDataModelOpen { get; set; }
|
||||
|
||||
public DataModelPropertiesViewModel LeftSideDataModel
|
||||
{
|
||||
get => _leftSideDataModel;
|
||||
set => SetAndNotify(ref _leftSideDataModel, value);
|
||||
}
|
||||
|
||||
public DataModelPropertiesViewModel RightSideDataModel
|
||||
{
|
||||
get => _rightSideDataModel;
|
||||
set => SetAndNotify(ref _rightSideDataModel, value);
|
||||
}
|
||||
|
||||
public bool RightSideDataModelOpen { get; set; }
|
||||
|
||||
public DataModelVisualizationViewModel SelectedLeftSideProperty
|
||||
{
|
||||
get => _selectedLeftSideProperty;
|
||||
set
|
||||
{
|
||||
if (!SetAndNotify(ref _selectedLeftSideProperty, value)) return;
|
||||
NotifyOfPropertyChange(nameof(CanActivateRightSideInputViewModel));
|
||||
}
|
||||
}
|
||||
|
||||
public DataModelVisualizationViewModel SelectedRightSideProperty
|
||||
{
|
||||
get => _selectedRightSideProperty;
|
||||
set => SetAndNotify(ref _selectedRightSideProperty, value);
|
||||
}
|
||||
|
||||
public object RightStaticValue
|
||||
{
|
||||
get => _rightStaticValue;
|
||||
set => SetAndNotify(ref _rightStaticValue, value);
|
||||
}
|
||||
|
||||
public int RightSideTransitionIndex
|
||||
{
|
||||
get => _rightSideTransitionIndex;
|
||||
set => SetAndNotify(ref _rightSideTransitionIndex, value);
|
||||
}
|
||||
|
||||
public DataModelInputViewModel RightSideInputViewModel
|
||||
{
|
||||
get => _rightSideInputViewModel;
|
||||
set => SetAndNotify(ref _rightSideInputViewModel, value);
|
||||
}
|
||||
|
||||
public BindableCollection<DisplayConditionOperator> Operators
|
||||
{
|
||||
get => _operators;
|
||||
set => SetAndNotify(ref _operators, value);
|
||||
}
|
||||
|
||||
public DisplayConditionOperator SelectedOperator
|
||||
{
|
||||
get => _selectedOperator;
|
||||
set => SetAndNotify(ref _selectedOperator, value);
|
||||
}
|
||||
|
||||
public DelegateCommand SelectLeftPropertyCommand { get; }
|
||||
public DelegateCommand SelectRightPropertyCommand { get; }
|
||||
public DelegateCommand SelectOperatorCommand { get; }
|
||||
|
||||
public void Handle(MainWindowKeyEvent message)
|
||||
{
|
||||
if (RightSideInputViewModel == null)
|
||||
return;
|
||||
|
||||
if (!message.KeyDown && message.EventArgs.Key == Key.Escape)
|
||||
RightSideInputViewModel.Cancel();
|
||||
if (!message.KeyDown && message.EventArgs.Key == Key.Enter)
|
||||
RightSideInputViewModel.Submit();
|
||||
}
|
||||
|
||||
public void Handle(MainWindowMouseEvent message)
|
||||
{
|
||||
if (RightSideInputViewModel == null)
|
||||
return;
|
||||
|
||||
if (message.Sender is FrameworkElement frameworkElement && !frameworkElement.IsDescendantOf(RightSideInputViewModel.View))
|
||||
RightSideInputViewModel.Submit();
|
||||
}
|
||||
|
||||
public override void Delete()
|
||||
{
|
||||
base.Delete();
|
||||
_profileEditorService.UpdateSelectedProfileElement();
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
// Get the data models
|
||||
LeftSideDataModel = _dataModelVisualizationService.GetMainDataModelVisualization();
|
||||
RightSideDataModel = _dataModelVisualizationService.GetMainDataModelVisualization();
|
||||
if (!_dataModelVisualizationService.GetPluginExtendsDataModel(_profileEditorService.GetCurrentModule()))
|
||||
{
|
||||
LeftSideDataModel.Children.Add(_dataModelVisualizationService.GetPluginDataModelVisualization(_profileEditorService.GetCurrentModule()));
|
||||
RightSideDataModel.Children.Add(_dataModelVisualizationService.GetPluginDataModelVisualization(_profileEditorService.GetCurrentModule()));
|
||||
}
|
||||
|
||||
// Determine which types are currently supported
|
||||
var editors = _dataModelVisualizationService.RegisteredDataModelEditors;
|
||||
_supportedInputTypes = editors.Select(e => e.SupportedType).ToList();
|
||||
_supportedInputTypes.AddRange(editors.Where(e => e.CompatibleConversionTypes != null).SelectMany(e => e.CompatibleConversionTypes));
|
||||
|
||||
LeftSideDataModel.UpdateRequested += LeftDataModelUpdateRequested;
|
||||
RightSideDataModel.UpdateRequested += RightDataModelUpdateRequested;
|
||||
|
||||
Update();
|
||||
|
||||
_updateTimer.Start();
|
||||
_updateTimer.Elapsed += OnUpdateTimerOnElapsed;
|
||||
|
||||
IsInitialized = true;
|
||||
}
|
||||
|
||||
public override void Update()
|
||||
{
|
||||
if (LeftSideDataModel == null || DisplayConditionPredicate.PredicateType == PredicateType.Dynamic && RightSideDataModel == null)
|
||||
return;
|
||||
|
||||
if (GetDataModelOverride() != null)
|
||||
LeftSideDataModel = GetDataModelOverride();
|
||||
|
||||
// If static, only allow selecting properties also supported by input
|
||||
if (DisplayConditionPredicate.PredicateType == PredicateType.Static)
|
||||
LeftSideDataModel.ApplyTypeFilter(false, _supportedInputTypes.ToArray());
|
||||
|
||||
// Determine the left side property first
|
||||
SelectedLeftSideProperty = LeftSideDataModel.GetChildForCondition(DisplayConditionPredicate, DisplayConditionSide.Left);
|
||||
var leftSideType = SelectedLeftSideProperty?.PropertyInfo?.PropertyType;
|
||||
|
||||
// Get the supported operators
|
||||
Operators.Clear();
|
||||
Operators.AddRange(_dataModelService.GetCompatibleConditionOperators(leftSideType));
|
||||
if (DisplayConditionPredicate.Operator == null)
|
||||
DisplayConditionPredicate.UpdateOperator(Operators.FirstOrDefault(o => o.SupportsType(leftSideType)));
|
||||
SelectedOperator = DisplayConditionPredicate.Operator;
|
||||
|
||||
// Determine the right side
|
||||
if (DisplayConditionPredicate.PredicateType == PredicateType.Dynamic)
|
||||
{
|
||||
SelectedRightSideProperty = LeftSideDataModel.GetChildForCondition(DisplayConditionPredicate, DisplayConditionSide.Right);
|
||||
if (GetDataModelOverride() != null)
|
||||
RightSideDataModel = GetDataModelOverride();
|
||||
|
||||
RightSideDataModel.ApplyTypeFilter(true, leftSideType);
|
||||
}
|
||||
else
|
||||
RightStaticValue = DisplayConditionPredicate.RightStaticValue;
|
||||
}
|
||||
|
||||
public void ApplyLeftSide()
|
||||
{
|
||||
DisplayConditionPredicate.UpdateLeftSide(SelectedLeftSideProperty.DataModel, SelectedLeftSideProperty.PropertyPath);
|
||||
_profileEditorService.UpdateSelectedProfileElement();
|
||||
|
||||
SelectedOperator = DisplayConditionPredicate.Operator;
|
||||
Update();
|
||||
}
|
||||
|
||||
public void ApplyRightSideDynamic()
|
||||
{
|
||||
DisplayConditionPredicate.UpdateRightSide(SelectedRightSideProperty.DataModel, SelectedRightSideProperty.PropertyPath);
|
||||
_profileEditorService.UpdateSelectedProfileElement();
|
||||
|
||||
Update();
|
||||
}
|
||||
|
||||
public void ApplyRightSideStatic(object value, bool isSubmitted)
|
||||
{
|
||||
if (isSubmitted)
|
||||
{
|
||||
DisplayConditionPredicate.UpdateRightSide(value);
|
||||
_profileEditorService.UpdateSelectedProfileElement();
|
||||
|
||||
Update();
|
||||
}
|
||||
|
||||
RightSideTransitionIndex = 0;
|
||||
RightSideInputViewModel = null;
|
||||
RightStaticValue = value;
|
||||
_eventAggregator.Unsubscribe(this);
|
||||
}
|
||||
|
||||
public void ApplyOperator()
|
||||
{
|
||||
DisplayConditionPredicate.UpdateOperator(SelectedOperator);
|
||||
_profileEditorService.UpdateSelectedProfileElement();
|
||||
|
||||
Update();
|
||||
}
|
||||
|
||||
public void ActivateRightSideInputViewModel()
|
||||
{
|
||||
if (SelectedLeftSideProperty?.PropertyInfo == null)
|
||||
return;
|
||||
|
||||
RightSideTransitionIndex = 1;
|
||||
RightSideInputViewModel = _dataModelVisualizationService.GetDataModelInputViewModel(
|
||||
SelectedLeftSideProperty.PropertyInfo.PropertyType,
|
||||
SelectedLeftSideProperty.PropertyDescription,
|
||||
DisplayConditionPredicate.RightStaticValue,
|
||||
ApplyRightSideStatic
|
||||
);
|
||||
_eventAggregator.Subscribe(this);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
_updateTimer.Stop();
|
||||
_updateTimer.Elapsed -= OnUpdateTimerOnElapsed;
|
||||
}
|
||||
|
||||
private void OnUpdateTimerOnElapsed(object sender, ElapsedEventArgs e)
|
||||
{
|
||||
if (LeftSideDataModelOpen)
|
||||
LeftSideDataModel.Update(_dataModelVisualizationService);
|
||||
else if (RightSideDataModelOpen)
|
||||
RightSideDataModel.Update(_dataModelVisualizationService);
|
||||
}
|
||||
|
||||
private void RightDataModelUpdateRequested(object sender, EventArgs e)
|
||||
{
|
||||
var leftSideType = SelectedLeftSideProperty?.PropertyInfo?.PropertyType;
|
||||
if (DisplayConditionPredicate.PredicateType == PredicateType.Dynamic)
|
||||
SelectedRightSideProperty = LeftSideDataModel.GetChildForCondition(DisplayConditionPredicate, DisplayConditionSide.Right);
|
||||
|
||||
// With the data model updated, also reapply the filter
|
||||
RightSideDataModel.ApplyTypeFilter(true, leftSideType);
|
||||
}
|
||||
|
||||
private void LeftDataModelUpdateRequested(object sender, EventArgs e)
|
||||
{
|
||||
if (DisplayConditionPredicate.PredicateType == PredicateType.Static)
|
||||
LeftSideDataModel.ApplyTypeFilter(false, _supportedInputTypes.ToArray());
|
||||
}
|
||||
|
||||
private void ExecuteSelectLeftProperty(object context)
|
||||
{
|
||||
if (!(context is DataModelVisualizationViewModel dataModelVisualizationViewModel))
|
||||
return;
|
||||
|
||||
SelectedLeftSideProperty = dataModelVisualizationViewModel;
|
||||
ApplyLeftSide();
|
||||
}
|
||||
|
||||
private void ExecuteSelectRightProperty(object context)
|
||||
{
|
||||
if (!(context is DataModelVisualizationViewModel dataModelVisualizationViewModel))
|
||||
return;
|
||||
|
||||
SelectedRightSideProperty = dataModelVisualizationViewModel;
|
||||
ApplyRightSideDynamic();
|
||||
}
|
||||
|
||||
private void ExecuteSelectOperatorCommand(object context)
|
||||
{
|
||||
if (!(context is DisplayConditionOperator displayConditionOperator))
|
||||
return;
|
||||
|
||||
SelectedOperator = displayConditionOperator;
|
||||
ApplyOperator();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
<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"
|
||||
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"
|
||||
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:InverseBooleanConverter x:Key="InverseBooleanConverter" />
|
||||
</UserControl.Resources>
|
||||
|
||||
<materialDesign:Transitioner AutoApplyTransitionOrigins="True" SelectedIndex="{Binding TransitionerIndex}">
|
||||
<!-- Conditions intro -->
|
||||
<materialDesign:ColorZone Mode="PrimaryDark" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" VerticalContentAlignment="Stretch">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Margin="32">
|
||||
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}" TextWrapping="Wrap" TextAlignment="Center" Margin="0 15">Unleash Artemis's true potential</TextBlock>
|
||||
<TextBlock TextWrapping="Wrap" TextAlignment="Center">Start using conditions to dynamically show and hide groups and layers</TextBlock>
|
||||
|
||||
<Button Style="{DynamicResource MaterialDesignFloatingActionAccentButton}" Command="{x:Static materialDesign:Transitioner.MoveNextCommand}" Margin="16">
|
||||
<materialDesign:PackIcon Kind="ArrowRight" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</materialDesign:ColorZone>
|
||||
|
||||
<!-- Conditions content -->
|
||||
<Grid Margin="10">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Style="{StaticResource MaterialDesignSubtitle1TextBlock}" Margin="0 0 0 -4">
|
||||
Display conditions
|
||||
</TextBlock>
|
||||
<Separator Grid.Row="1" Grid.Column="0" Style="{StaticResource MaterialDesignDarkSeparator}" Margin="-2 0" />
|
||||
|
||||
<Grid Grid.Row="2" Grid.Column="0">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Background="{StaticResource MaterialDesignCardBackground}">
|
||||
<ContentControl s:View.Model="{Binding RootGroup}" />
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="3">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="20" />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" MinWidth="140" />
|
||||
<ColumnDefinition Width="*" MinWidth="170" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Play mode -->
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="PlayOutline" VerticalAlignment="Center" />
|
||||
<TextBlock 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>
|
||||
</StackPanel>
|
||||
|
||||
<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>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="Repeat" VerticalAlignment="Center" />
|
||||
<TextBlock FontSize="12" VerticalAlignment="Center">REPEAT</TextBlock>
|
||||
</StackPanel>
|
||||
</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>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="StopwatchOutline" VerticalAlignment="Center" />
|
||||
<TextBlock FontSize="12" VerticalAlignment="Center">ONCE</TextBlock>
|
||||
</StackPanel>
|
||||
</RadioButton>
|
||||
</Grid>
|
||||
</materialDesign:ColorZone>
|
||||
|
||||
<!-- Stop mode -->
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="Stop" VerticalAlignment="Center" />
|
||||
<TextBlock Text="Stop mode" VerticalAlignment="Center">
|
||||
<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>
|
||||
</StackPanel>
|
||||
<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>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="PlayOutline" VerticalAlignment="Center" />
|
||||
<TextBlock FontSize="12" VerticalAlignment="Center">FINISH</TextBlock>
|
||||
</StackPanel>
|
||||
</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>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="SkipNextOutline" VerticalAlignment="Center" />
|
||||
<TextBlock FontSize="12" VerticalAlignment="Center">SKIP TO END</TextBlock>
|
||||
</StackPanel>
|
||||
</RadioButton>
|
||||
</Grid>
|
||||
</materialDesign:ColorZone>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
|
||||
</materialDesign:Transitioner>
|
||||
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,110 @@
|
||||
using System.Linq;
|
||||
using Artemis.Core.Models.Profile;
|
||||
using Artemis.Core.Models.Profile.Conditions;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Shared.Events;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions
|
||||
{
|
||||
public class DisplayConditionsViewModel : ProfileEditorPanelViewModel
|
||||
{
|
||||
private readonly IDisplayConditionsVmFactory _displayConditionsVmFactory;
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private RenderProfileElement _renderProfileElement;
|
||||
private DisplayConditionGroupViewModel _rootGroup;
|
||||
private int _transitionerIndex;
|
||||
private bool _displayContinuously;
|
||||
private bool _alwaysFinishTimeline;
|
||||
|
||||
public DisplayConditionsViewModel(IProfileEditorService profileEditorService, IDisplayConditionsVmFactory displayConditionsVmFactory)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
_displayConditionsVmFactory = displayConditionsVmFactory;
|
||||
}
|
||||
|
||||
public int TransitionerIndex
|
||||
{
|
||||
get => _transitionerIndex;
|
||||
set => SetAndNotify(ref _transitionerIndex, value);
|
||||
}
|
||||
|
||||
public DisplayConditionGroupViewModel RootGroup
|
||||
{
|
||||
get => _rootGroup;
|
||||
set => SetAndNotify(ref _rootGroup, value);
|
||||
}
|
||||
|
||||
public RenderProfileElement RenderProfileElement
|
||||
{
|
||||
get => _renderProfileElement;
|
||||
set => SetAndNotify(ref _renderProfileElement, value);
|
||||
}
|
||||
|
||||
public bool DisplayContinuously
|
||||
{
|
||||
get => _displayContinuously;
|
||||
set
|
||||
{
|
||||
if (!SetAndNotify(ref _displayContinuously, value)) return;
|
||||
_profileEditorService.UpdateSelectedProfileElement();
|
||||
}
|
||||
}
|
||||
|
||||
public bool AlwaysFinishTimeline
|
||||
{
|
||||
get => _alwaysFinishTimeline;
|
||||
set
|
||||
{
|
||||
if (!SetAndNotify(ref _alwaysFinishTimeline, value)) return;
|
||||
_profileEditorService.UpdateSelectedProfileElement();
|
||||
}
|
||||
}
|
||||
|
||||
public bool ConditionBehaviourEnabled => RenderProfileElement != null;
|
||||
|
||||
protected override void OnActivate()
|
||||
{
|
||||
_profileEditorService.ProfileElementSelected += ProfileEditorServiceOnProfileElementSelected;
|
||||
}
|
||||
|
||||
protected override void OnDeactivate()
|
||||
{
|
||||
_profileEditorService.ProfileElementSelected -= ProfileEditorServiceOnProfileElementSelected;
|
||||
|
||||
RootGroup?.Dispose();
|
||||
RootGroup = null;
|
||||
}
|
||||
|
||||
private void ProfileEditorServiceOnProfileElementSelected(object sender, RenderProfileElementEventArgs e)
|
||||
{
|
||||
RenderProfileElement = e.RenderProfileElement;
|
||||
NotifyOfPropertyChange(nameof(ConditionBehaviourEnabled));
|
||||
|
||||
_displayContinuously = RenderProfileElement?.DisplayContinuously ?? false;
|
||||
NotifyOfPropertyChange(nameof(DisplayContinuously));
|
||||
_alwaysFinishTimeline = RenderProfileElement?.AlwaysFinishTimeline ?? false;
|
||||
NotifyOfPropertyChange(nameof(AlwaysFinishTimeline));
|
||||
|
||||
if (e.RenderProfileElement == null)
|
||||
{
|
||||
RootGroup?.Dispose();
|
||||
RootGroup = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure the layer has a root display condition group
|
||||
if (e.RenderProfileElement.DisplayConditionGroup == null)
|
||||
e.RenderProfileElement.DisplayConditionGroup = new DisplayConditionGroup(null);
|
||||
|
||||
RootGroup?.Dispose();
|
||||
RootGroup = _displayConditionsVmFactory.DisplayConditionGroupViewModel(e.RenderProfileElement.DisplayConditionGroup, null);
|
||||
RootGroup.IsRootGroup = true;
|
||||
RootGroup.Update();
|
||||
|
||||
// Only show the intro to conditions once, and only if the layer has no conditions
|
||||
if (TransitionerIndex != 1)
|
||||
TransitionerIndex = RootGroup.Children.Any() ? 1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<controls:MaterialWindow x:Class="Artemis.UI.Screens.ProfileEditor.LayerBrushSettingsWindowView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
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"
|
||||
xmlns:controls="clr-namespace:MaterialDesignExtensions.Controls;assembly=MaterialDesignExtensions"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
mc:Ignorable="d"
|
||||
Title="Layer brush configuration"
|
||||
Background="{DynamicResource MaterialDesignPaper}"
|
||||
FontFamily="pack://application:,,,/MaterialDesignThemes.Wpf;component/Resources/Roboto/#Roboto"
|
||||
UseLayoutRounding="True"
|
||||
Width="800"
|
||||
Height="800"
|
||||
d:DesignHeight="800"
|
||||
d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance local:LayerBrushSettingsWindowViewModel}"
|
||||
Icon="/Resources/Images/Logo/logo-512.png">
|
||||
<DockPanel>
|
||||
<controls:AppBar Type="Dense" Title="{Binding ActiveItem.LayerBrush.Descriptor.DisplayName}" DockPanel.Dock="Top" Margin="-18 0 0 0" ShowShadow="False">
|
||||
<controls:AppBar.AppIcon>
|
||||
<materialDesign:PackIcon Kind="{Binding ActiveItem.LayerBrush.Descriptor.Icon}" Width="20" Height="28" />
|
||||
</controls:AppBar.AppIcon>
|
||||
</controls:AppBar>
|
||||
|
||||
<ContentControl s:View.Model="{Binding ActiveItem}" />
|
||||
</DockPanel>
|
||||
</controls:MaterialWindow>
|
||||
@@ -0,0 +1,13 @@
|
||||
using Artemis.Core.Plugins.Abstract.ViewModels;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor
|
||||
{
|
||||
public class LayerBrushSettingsWindowViewModel : Conductor<BrushConfigurationViewModel>
|
||||
{
|
||||
public LayerBrushSettingsWindowViewModel(BrushConfigurationViewModel configurationViewModel)
|
||||
{
|
||||
ActiveItem = configurationViewModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<controls:MaterialWindow x:Class="Artemis.UI.Screens.ProfileEditor.LayerEffectSettingsWindowView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
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"
|
||||
xmlns:controls="clr-namespace:MaterialDesignExtensions.Controls;assembly=MaterialDesignExtensions"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
mc:Ignorable="d"
|
||||
Title="Layer effect configuration"
|
||||
Background="{DynamicResource MaterialDesignPaper}"
|
||||
FontFamily="pack://application:,,,/MaterialDesignThemes.Wpf;component/Resources/Roboto/#Roboto"
|
||||
UseLayoutRounding="True"
|
||||
Width="800"
|
||||
Height="800"
|
||||
d:DesignHeight="800"
|
||||
d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance local:LayerEffectSettingsWindowViewModel}"
|
||||
Icon="/Resources/Images/Logo/logo-512.png">
|
||||
<DockPanel>
|
||||
<controls:AppBar Type="Dense" Title="{Binding ActiveItem.LayerEffect.Descriptor.DisplayName}" DockPanel.Dock="Top" Margin="-18 0 0 0" ShowShadow="False">
|
||||
<controls:AppBar.AppIcon>
|
||||
<materialDesign:PackIcon Kind="{Binding ActiveItem.LayerEffect.Descriptor.Icon}" Width="20" Height="28" />
|
||||
</controls:AppBar.AppIcon>
|
||||
</controls:AppBar>
|
||||
|
||||
<ContentControl s:View.Model="{Binding ActiveItem}" />
|
||||
</DockPanel>
|
||||
</controls:MaterialWindow>
|
||||
@@ -0,0 +1,13 @@
|
||||
using Artemis.Core.Plugins.Abstract.ViewModels;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor
|
||||
{
|
||||
public class LayerEffectSettingsWindowViewModel : Conductor<EffectConfigurationViewModel>
|
||||
{
|
||||
public LayerEffectSettingsWindowViewModel(EffectConfigurationViewModel configurationViewModel)
|
||||
{
|
||||
ActiveItem = configurationViewModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Artemis.Core.Models.Profile.LayerProperties;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Abstract
|
||||
{
|
||||
public abstract class LayerPropertyBaseViewModel : PropertyChangedBase, IDisposable
|
||||
{
|
||||
private bool _isExpanded;
|
||||
private BindableCollection<LayerPropertyBaseViewModel> _children;
|
||||
|
||||
protected LayerPropertyBaseViewModel()
|
||||
{
|
||||
Children = new BindableCollection<LayerPropertyBaseViewModel>();
|
||||
}
|
||||
|
||||
public abstract bool IsVisible { get; }
|
||||
|
||||
public virtual bool IsExpanded
|
||||
{
|
||||
get => _isExpanded;
|
||||
set => SetAndNotify(ref _isExpanded, value);
|
||||
}
|
||||
|
||||
public BindableCollection<LayerPropertyBaseViewModel> Children
|
||||
{
|
||||
get => _children;
|
||||
set => SetAndNotify(ref _children, value);
|
||||
}
|
||||
|
||||
public abstract void Dispose();
|
||||
|
||||
public abstract List<BaseLayerPropertyKeyframe> GetKeyframes(bool expandedOnly);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<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:layerEffect="clr-namespace:Artemis.Core.Plugins.LayerEffect;assembly=Artemis.Core"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
xmlns:behaviors="clr-namespace:Artemis.UI.Behaviors"
|
||||
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 & white but also audio visualization etc.
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
<ListBox ItemsSource="{Binding LayerEffectDescriptors}" 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>
|
||||
<materialDesign:PackIcon Kind="{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>
|
||||
@@ -0,0 +1,88 @@
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core.Models.Profile;
|
||||
using Artemis.Core.Plugins.Abstract;
|
||||
using Artemis.Core.Plugins.LayerEffect;
|
||||
using Artemis.Core.Services.Interfaces;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.LayerEffects
|
||||
{
|
||||
public class EffectsViewModel : PropertyChangedBase
|
||||
{
|
||||
private readonly IRenderElementService _renderElementService;
|
||||
private readonly IPluginService _pluginService;
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private BindableCollection<LayerEffectDescriptor> _layerEffectDescriptors;
|
||||
private LayerEffectDescriptor _selectedLayerEffectDescriptor;
|
||||
|
||||
public EffectsViewModel(LayerPropertiesViewModel layerPropertiesViewModel, IPluginService pluginService, IRenderElementService renderElementService, IProfileEditorService profileEditorService)
|
||||
{
|
||||
_pluginService = pluginService;
|
||||
_renderElementService = renderElementService;
|
||||
_profileEditorService = profileEditorService;
|
||||
LayerPropertiesViewModel = layerPropertiesViewModel;
|
||||
LayerEffectDescriptors = new BindableCollection<LayerEffectDescriptor>();
|
||||
PropertyChanged += HandleSelectedLayerEffectChanged;
|
||||
}
|
||||
|
||||
public LayerPropertiesViewModel LayerPropertiesViewModel { get; }
|
||||
public bool HasLayerEffectDescriptors => LayerEffectDescriptors.Any();
|
||||
|
||||
public BindableCollection<LayerEffectDescriptor> LayerEffectDescriptors
|
||||
{
|
||||
get => _layerEffectDescriptors;
|
||||
set => SetAndNotify(ref _layerEffectDescriptors, value);
|
||||
}
|
||||
|
||||
public LayerEffectDescriptor SelectedLayerEffectDescriptor
|
||||
{
|
||||
get => _selectedLayerEffectDescriptor;
|
||||
set => SetAndNotify(ref _selectedLayerEffectDescriptor, value);
|
||||
}
|
||||
|
||||
public void PopulateDescriptors()
|
||||
{
|
||||
var layerBrushProviders = _pluginService.GetPluginsOfType<LayerEffectProvider>();
|
||||
var descriptors = layerBrushProviders.SelectMany(l => l.LayerEffectDescriptors).ToList();
|
||||
LayerEffectDescriptors.AddRange(descriptors.Except(LayerEffectDescriptors));
|
||||
LayerEffectDescriptors.RemoveRange(LayerEffectDescriptors.Except(descriptors));
|
||||
|
||||
// Sort by display name
|
||||
var index = 0;
|
||||
foreach (var layerEffectDescriptor in LayerEffectDescriptors.OrderBy(d => d.DisplayName).ToList())
|
||||
{
|
||||
if (LayerEffectDescriptors.IndexOf(layerEffectDescriptor) != index)
|
||||
LayerEffectDescriptors.Move(LayerEffectDescriptors.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);
|
||||
_renderElementService.AddLayerEffect(renderElement, SelectedLayerEffectDescriptor);
|
||||
_profileEditorService.UpdateSelectedProfileElement();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
<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.LayerProperties"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
xmlns:timeline="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline"
|
||||
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"
|
||||
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>
|
||||
<Converters: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>
|
||||
<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="56" />
|
||||
<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>
|
||||
<ToggleButton Style="{StaticResource MaterialDesignFlatToggleButton}"
|
||||
ToolTip="Repeat after last keyframe"
|
||||
IsChecked="{Binding RepeatAfterLastKeyframe}"
|
||||
Focusable="False">
|
||||
<materialDesign:PackIcon Kind="Repeat" Height="24" Width="24" />
|
||||
</ToggleButton>
|
||||
</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}" />
|
||||
</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 cercumvent 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 -->
|
||||
<Grid Grid.Row="0" Grid.Column="2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="56" />
|
||||
<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 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 SecondaryAccentBrush}" />
|
||||
<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=ContainerGrid}"
|
||||
StrokeThickness="2"
|
||||
Stroke="{StaticResource SecondaryAccentBrush}" />
|
||||
|
||||
|
||||
<!-- Timeline header body -->
|
||||
<controls:PropertyTimelineHeader Margin="0 25 0 0"
|
||||
Fill="{DynamicResource MaterialDesignBody}"
|
||||
PixelsPerSecond="{Binding ProfileEditorService.PixelsPerSecond}"
|
||||
HorizontalOffset="{Binding ContentHorizontalOffset, ElementName=TimelineHeaderScrollViewer}"
|
||||
VisibleWidth="{Binding ActualWidth, ElementName=TimelineHeaderScrollViewer}"
|
||||
OffsetFirstValue="True"
|
||||
Width="{Binding ActualWidth, ElementName=PropertyTimeLine}" />
|
||||
</Canvas>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- Timeline rails -->
|
||||
<ScrollViewer x:Name="TimelineRailsScrollViewer" Grid.Row="1" Style="{StaticResource SvStyle}" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
|
||||
ScrollChanged="TimelineScrollChanged">
|
||||
<Grid Background="{DynamicResource MaterialDesignToolBarBackground}">
|
||||
<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=ContainerGrid}"
|
||||
StrokeThickness="2"
|
||||
Stroke="{StaticResource SecondaryAccentBrush}" />
|
||||
</Canvas>
|
||||
|
||||
<ContentControl Grid.Column="0" s:View.Model="{Binding TimelineViewModel}" x:Name="PropertyTimeLine" />
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
<!-- 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" />
|
||||
<materialDesign:PackIcon Kind="{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="110"
|
||||
ToolTip="Select an effect to add"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{Binding PropertyTreeVisible, Converter={x:Static s:BoolToVisibilityConverter.Instance}}"
|
||||
IsEnabled="{Binding SelectedProfileElement, Converter={StaticResource NullToBooleanConverter}}"
|
||||
Command="{x:Static materialDesign:Transitioner.MoveLastCommand}"
|
||||
CommandTarget="{Binding ElementName=TransitionCommandAnchor}">
|
||||
<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="110"
|
||||
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}">
|
||||
<TextBlock FontSize="11">
|
||||
SHOW PROPERTIES
|
||||
</TextBlock>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
Panel.ZIndex="2"
|
||||
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="Select an effect to add"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{Binding PropertyTreeVisible, Converter={x:Static s:BoolToVisibilityConverter.Instance}}"
|
||||
IsEnabled="{Binding SelectedProfileElement, Converter={StaticResource NullToBooleanConverter}}">
|
||||
<Button.Style>
|
||||
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource MaterialDesignFlatMidBgButton}">
|
||||
<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"
|
||||
Margin="10 5"
|
||||
Minimum="31"
|
||||
Maximum="350"
|
||||
TickFrequency="1"
|
||||
IsSnapToTickEnabled="True"
|
||||
AutoToolTipPlacement="TopLeft"
|
||||
Value="{Binding ProfileEditorService.PixelsPerSecond}"
|
||||
Width="319" />
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,29 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using Artemis.Core.Events;
|
||||
using Artemis.Core.Models.Profile;
|
||||
using Artemis.Core.Models.Profile.LayerProperties;
|
||||
using Artemis.Core.Models.Profile.LayerProperties.Attributes;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.Core.Services.Interfaces;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
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.Events;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using GongSolutions.Wpf.DragDrop;
|
||||
using Stylet;
|
||||
using static Artemis.UI.Screens.ProfileEditor.LayerProperties.LayerPropertyGroupViewModel.ViewModelType;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties
|
||||
{
|
||||
public class LayerPropertiesViewModel : ProfileEditorPanelViewModel, IDropTarget
|
||||
{
|
||||
private readonly ILayerPropertyVmFactory _layerPropertyVmFactory;
|
||||
private LayerPropertyGroupViewModel _brushPropertyGroup;
|
||||
private EffectsViewModel _effectsViewModel;
|
||||
private BindableCollection<LayerPropertyGroupViewModel> _layerPropertyGroups;
|
||||
private bool _playing;
|
||||
private int _propertyTreeIndex;
|
||||
private bool _repeatAfterLastKeyframe;
|
||||
private RenderProfileElement _selectedProfileElement;
|
||||
private TimelineViewModel _timelineViewModel;
|
||||
private TreeViewModel _treeViewModel;
|
||||
private TimelineSegmentViewModel _startTimelineSegmentViewModel;
|
||||
private TimelineSegmentViewModel _mainTimelineSegmentViewModel;
|
||||
private TimelineSegmentViewModel _endTimelineSegmentViewModel;
|
||||
|
||||
public LayerPropertiesViewModel(IProfileEditorService profileEditorService, ICoreService coreService, ISettingsService settingsService,
|
||||
ILayerPropertyVmFactory layerPropertyVmFactory)
|
||||
{
|
||||
_layerPropertyVmFactory = layerPropertyVmFactory;
|
||||
|
||||
ProfileEditorService = profileEditorService;
|
||||
CoreService = coreService;
|
||||
SettingsService = settingsService;
|
||||
|
||||
EffectsViewModel = _layerPropertyVmFactory.EffectsViewModel(this);
|
||||
LayerPropertyGroups = new BindableCollection<LayerPropertyGroupViewModel>();
|
||||
PropertyChanged += HandlePropertyTreeIndexChanged;
|
||||
}
|
||||
|
||||
public IProfileEditorService ProfileEditorService { get; }
|
||||
public ICoreService CoreService { get; }
|
||||
public ISettingsService SettingsService { get; }
|
||||
|
||||
public bool Playing
|
||||
{
|
||||
get => _playing;
|
||||
set => SetAndNotify(ref _playing, value);
|
||||
}
|
||||
|
||||
public bool RepeatAfterLastKeyframe
|
||||
{
|
||||
get => _repeatAfterLastKeyframe;
|
||||
set => SetAndNotify(ref _repeatAfterLastKeyframe, 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 int PropertyTreeIndex
|
||||
{
|
||||
get => _propertyTreeIndex;
|
||||
set
|
||||
{
|
||||
if (!SetAndNotify(ref _propertyTreeIndex, value)) return;
|
||||
NotifyOfPropertyChange(nameof(PropertyTreeVisible));
|
||||
}
|
||||
}
|
||||
|
||||
public bool PropertyTreeVisible => PropertyTreeIndex == 0;
|
||||
|
||||
public RenderProfileElement SelectedProfileElement
|
||||
{
|
||||
get => _selectedProfileElement;
|
||||
set
|
||||
{
|
||||
if (!SetAndNotify(ref _selectedProfileElement, value)) return;
|
||||
NotifyOfPropertyChange(nameof(SelectedLayer));
|
||||
NotifyOfPropertyChange(nameof(SelectedFolder));
|
||||
}
|
||||
}
|
||||
|
||||
public Layer SelectedLayer => SelectedProfileElement as Layer;
|
||||
public Folder SelectedFolder => SelectedProfileElement as Folder;
|
||||
|
||||
public BindableCollection<LayerPropertyGroupViewModel> LayerPropertyGroups
|
||||
{
|
||||
get => _layerPropertyGroups;
|
||||
set => SetAndNotify(ref _layerPropertyGroups, value);
|
||||
}
|
||||
|
||||
public TreeViewModel TreeViewModel
|
||||
{
|
||||
get => _treeViewModel;
|
||||
set => SetAndNotify(ref _treeViewModel, value);
|
||||
}
|
||||
|
||||
public EffectsViewModel EffectsViewModel
|
||||
{
|
||||
get => _effectsViewModel;
|
||||
set => SetAndNotify(ref _effectsViewModel, value);
|
||||
}
|
||||
|
||||
public TimelineViewModel TimelineViewModel
|
||||
{
|
||||
get => _timelineViewModel;
|
||||
set => SetAndNotify(ref _timelineViewModel, value);
|
||||
}
|
||||
|
||||
public TimelineSegmentViewModel StartTimelineSegmentViewModel
|
||||
{
|
||||
get => _startTimelineSegmentViewModel;
|
||||
set => SetAndNotify(ref _startTimelineSegmentViewModel, value);
|
||||
}
|
||||
|
||||
public TimelineSegmentViewModel MainTimelineSegmentViewModel
|
||||
{
|
||||
get => _mainTimelineSegmentViewModel;
|
||||
set => SetAndNotify(ref _mainTimelineSegmentViewModel, value);
|
||||
}
|
||||
|
||||
public TimelineSegmentViewModel EndTimelineSegmentViewModel
|
||||
{
|
||||
get => _endTimelineSegmentViewModel;
|
||||
set => SetAndNotify(ref _endTimelineSegmentViewModel, value);
|
||||
}
|
||||
|
||||
protected override void OnInitialActivate()
|
||||
{
|
||||
PopulateProperties(ProfileEditorService.SelectedProfileElement);
|
||||
|
||||
ProfileEditorService.ProfileElementSelected += ProfileEditorServiceOnProfileElementSelected;
|
||||
ProfileEditorService.CurrentTimeChanged += ProfileEditorServiceOnCurrentTimeChanged;
|
||||
ProfileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged;
|
||||
|
||||
base.OnInitialActivate();
|
||||
}
|
||||
|
||||
protected override void OnClose()
|
||||
{
|
||||
ProfileEditorService.ProfileElementSelected -= ProfileEditorServiceOnProfileElementSelected;
|
||||
ProfileEditorService.CurrentTimeChanged -= ProfileEditorServiceOnCurrentTimeChanged;
|
||||
ProfileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged;
|
||||
|
||||
PopulateProperties(null);
|
||||
|
||||
TimelineViewModel?.Dispose();
|
||||
TimelineViewModel = null;
|
||||
StartTimelineSegmentViewModel?.Dispose();
|
||||
StartTimelineSegmentViewModel = null;
|
||||
MainTimelineSegmentViewModel?.Dispose();
|
||||
MainTimelineSegmentViewModel = null;
|
||||
EndTimelineSegmentViewModel?.Dispose();
|
||||
EndTimelineSegmentViewModel = 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 ProfileEditorServiceOnProfileElementSelected(object sender, RenderProfileElementEventArgs e)
|
||||
{
|
||||
PopulateProperties(e.RenderProfileElement);
|
||||
}
|
||||
|
||||
private void ProfileEditorServiceOnCurrentTimeChanged(object sender, EventArgs e)
|
||||
{
|
||||
NotifyOfPropertyChange(nameof(FormattedCurrentTime));
|
||||
NotifyOfPropertyChange(nameof(TimeCaretPosition));
|
||||
}
|
||||
|
||||
private void ProfileEditorServiceOnPixelsPerSecondChanged(object sender, EventArgs e)
|
||||
{
|
||||
NotifyOfPropertyChange(nameof(TimeCaretPosition));
|
||||
}
|
||||
|
||||
#region View model managament
|
||||
|
||||
public List<LayerPropertyGroupViewModel> GetAllLayerPropertyGroupViewModels()
|
||||
{
|
||||
var groups = LayerPropertyGroups.ToList();
|
||||
var toAdd = groups.SelectMany(g => g.Children).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
|
||||
foreach (var layerPropertyGroupViewModel in LayerPropertyGroups)
|
||||
layerPropertyGroupViewModel.Dispose();
|
||||
LayerPropertyGroups.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
|
||||
var generalAttribute = Attribute.GetCustomAttribute(
|
||||
SelectedLayer.GetType().GetProperty(nameof(SelectedLayer.General)),
|
||||
typeof(PropertyGroupDescriptionAttribute)
|
||||
);
|
||||
var transformAttribute = Attribute.GetCustomAttribute(
|
||||
SelectedLayer.GetType().GetProperty(nameof(SelectedLayer.Transform)),
|
||||
typeof(PropertyGroupDescriptionAttribute)
|
||||
);
|
||||
LayerPropertyGroups.Add(_layerPropertyVmFactory.LayerPropertyGroupViewModel(SelectedLayer.General, (PropertyGroupDescriptionAttribute) generalAttribute));
|
||||
LayerPropertyGroups.Add(_layerPropertyVmFactory.LayerPropertyGroupViewModel(SelectedLayer.Transform, (PropertyGroupDescriptionAttribute) transformAttribute));
|
||||
}
|
||||
|
||||
TreeViewModel = _layerPropertyVmFactory.TreeViewModel(this, LayerPropertyGroups);
|
||||
|
||||
TimelineViewModel?.Dispose();
|
||||
TimelineViewModel = _layerPropertyVmFactory.TimelineViewModel(this, LayerPropertyGroups);
|
||||
StartTimelineSegmentViewModel?.Dispose();
|
||||
StartTimelineSegmentViewModel = new TimelineSegmentViewModel(ProfileEditorService, SegmentViewModelType.Start);
|
||||
MainTimelineSegmentViewModel?.Dispose();
|
||||
MainTimelineSegmentViewModel = new TimelineSegmentViewModel(ProfileEditorService, SegmentViewModelType.Main);
|
||||
EndTimelineSegmentViewModel?.Dispose();
|
||||
EndTimelineSegmentViewModel = new TimelineSegmentViewModel(ProfileEditorService, SegmentViewModelType.End);
|
||||
|
||||
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;
|
||||
|
||||
var hideRenderRelatedProperties = SelectedLayer?.LayerBrush != null && !SelectedLayer.LayerBrush.SupportsTransformation;
|
||||
|
||||
SelectedLayer.General.ShapeType.IsHidden = hideRenderRelatedProperties;
|
||||
SelectedLayer.General.ResizeMode.IsHidden = hideRenderRelatedProperties;
|
||||
SelectedLayer.General.BlendMode.IsHidden = hideRenderRelatedProperties;
|
||||
SelectedLayer.Transform.IsHidden = hideRenderRelatedProperties;
|
||||
|
||||
if (_brushPropertyGroup != null)
|
||||
{
|
||||
LayerPropertyGroups.Remove(_brushPropertyGroup);
|
||||
_brushPropertyGroup = null;
|
||||
}
|
||||
|
||||
if (SelectedLayer.LayerBrush != null)
|
||||
{
|
||||
// Add the rout group of the brush
|
||||
// The root group of the brush has no attribute so let's pull one out of our sleeve
|
||||
var brushDescription = new PropertyGroupDescriptionAttribute
|
||||
{
|
||||
Name = SelectedLayer.LayerBrush.Descriptor.DisplayName,
|
||||
Description = SelectedLayer.LayerBrush.Descriptor.Description
|
||||
};
|
||||
_brushPropertyGroup = _layerPropertyVmFactory.LayerPropertyGroupViewModel(SelectedLayer.LayerBrush.BaseProperties, brushDescription);
|
||||
LayerPropertyGroups.Add(_brushPropertyGroup);
|
||||
}
|
||||
|
||||
SortProperties();
|
||||
UpdateKeyframes();
|
||||
}
|
||||
|
||||
private void ApplyEffects()
|
||||
{
|
||||
RenderProfileElement renderElement;
|
||||
if (SelectedLayer != null)
|
||||
renderElement = SelectedLayer;
|
||||
else if (SelectedFolder != null)
|
||||
renderElement = SelectedFolder;
|
||||
else
|
||||
return;
|
||||
|
||||
// Remove VMs of effects no longer applied on the layer
|
||||
var toRemove = LayerPropertyGroups.Where(l => l.LayerPropertyGroup.LayerEffect != null && !renderElement.LayerEffects.Contains(l.LayerPropertyGroup.LayerEffect)).ToList();
|
||||
LayerPropertyGroups.RemoveRange(toRemove);
|
||||
foreach (var layerPropertyGroupViewModel in toRemove)
|
||||
layerPropertyGroupViewModel.Dispose();
|
||||
|
||||
foreach (var layerEffect in renderElement.LayerEffects)
|
||||
{
|
||||
if (LayerPropertyGroups.Any(l => l.LayerPropertyGroup.LayerEffect == layerEffect))
|
||||
continue;
|
||||
|
||||
// Add the rout group of the brush
|
||||
// The root group of the brush has no attribute so let's pull one out of our sleeve
|
||||
var brushDescription = new PropertyGroupDescriptionAttribute
|
||||
{
|
||||
Name = layerEffect.Descriptor.DisplayName,
|
||||
Description = layerEffect.Descriptor.Description
|
||||
};
|
||||
LayerPropertyGroups.Add(_layerPropertyVmFactory.LayerPropertyGroupViewModel(layerEffect.BaseProperties, brushDescription));
|
||||
}
|
||||
|
||||
SortProperties();
|
||||
UpdateKeyframes();
|
||||
}
|
||||
|
||||
private void SortProperties()
|
||||
{
|
||||
// Get all non-effect properties
|
||||
var nonEffectProperties = LayerPropertyGroups.Where(l => l.GroupType != LayerEffectRoot).ToList();
|
||||
// Order the effects
|
||||
var effectProperties = LayerPropertyGroups.Where(l => l.GroupType == LayerEffectRoot).OrderBy(l => l.LayerPropertyGroup.LayerEffect.Order).ToList();
|
||||
|
||||
// Put the non-effect properties in front
|
||||
for (var index = 0; index < nonEffectProperties.Count; index++)
|
||||
{
|
||||
var layerPropertyGroupViewModel = nonEffectProperties[index];
|
||||
LayerPropertyGroups.Move(LayerPropertyGroups.IndexOf(layerPropertyGroupViewModel), index);
|
||||
}
|
||||
|
||||
// Put the effect properties after, sorted by their order
|
||||
for (var index = 0; index < effectProperties.Count; index++)
|
||||
{
|
||||
var layerPropertyGroupViewModel = effectProperties[index];
|
||||
LayerPropertyGroups.Move(LayerPropertyGroups.IndexOf(layerPropertyGroupViewModel), index + nonEffectProperties.Count);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateKeyframes()
|
||||
{
|
||||
TimelineViewModel.Update();
|
||||
}
|
||||
|
||||
#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;
|
||||
|
||||
var source = dropInfo.Data as LayerPropertyGroupViewModel;
|
||||
var target = dropInfo.TargetItem as LayerPropertyGroupViewModel;
|
||||
|
||||
if (source == target || target?.GroupType != LayerEffectRoot || source?.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;
|
||||
|
||||
var source = dropInfo.Data as LayerPropertyGroupViewModel;
|
||||
var target = dropInfo.TargetItem as LayerPropertyGroupViewModel;
|
||||
|
||||
if (source == target || target?.GroupType != LayerEffectRoot || source?.GroupType != LayerEffectRoot)
|
||||
return;
|
||||
|
||||
if (dropInfo.InsertPosition == RelativeInsertPosition.BeforeTargetItem)
|
||||
MoveBefore(source, target);
|
||||
else if (dropInfo.InsertPosition == RelativeInsertPosition.AfterTargetItem)
|
||||
MoveAfter(source, target);
|
||||
|
||||
ApplyCurrentEffectsOrder();
|
||||
ProfileEditorService.UpdateSelectedProfile();
|
||||
}
|
||||
|
||||
private void MoveBefore(LayerPropertyGroupViewModel source, LayerPropertyGroupViewModel target)
|
||||
{
|
||||
if (LayerPropertyGroups.IndexOf(target) == LayerPropertyGroups.IndexOf(source) + 1)
|
||||
return;
|
||||
|
||||
LayerPropertyGroups.Move(LayerPropertyGroups.IndexOf(source), LayerPropertyGroups.IndexOf(target));
|
||||
}
|
||||
|
||||
private void MoveAfter(LayerPropertyGroupViewModel source, LayerPropertyGroupViewModel target)
|
||||
{
|
||||
LayerPropertyGroups.Remove(source);
|
||||
LayerPropertyGroups.Insert(LayerPropertyGroups.IndexOf(target) + 1, source);
|
||||
}
|
||||
|
||||
private void ApplyCurrentEffectsOrder()
|
||||
{
|
||||
var order = 1;
|
||||
foreach (var groupViewModel in LayerPropertyGroups.Where(p => p.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()
|
||||
{
|
||||
ProfileEditorService.CurrentTime = CalculateEndTime();
|
||||
}
|
||||
|
||||
public void GoToPreviousFrame()
|
||||
{
|
||||
var frameTime = 1000.0 / SettingsService.GetSetting("Core.TargetFrameRate", 25).Value;
|
||||
var newTime = Math.Max(0, Math.Round((ProfileEditorService.CurrentTime.TotalMilliseconds - frameTime) / frameTime) * frameTime);
|
||||
ProfileEditorService.CurrentTime = TimeSpan.FromMilliseconds(newTime);
|
||||
}
|
||||
|
||||
public void GoToNextFrame()
|
||||
{
|
||||
var frameTime = 1000.0 / SettingsService.GetSetting("Core.TargetFrameRate", 25).Value;
|
||||
var newTime = Math.Round((ProfileEditorService.CurrentTime.TotalMilliseconds + frameTime) / frameTime) * frameTime;
|
||||
newTime = Math.Min(newTime, CalculateEndTime().TotalMilliseconds);
|
||||
ProfileEditorService.CurrentTime = TimeSpan.FromMilliseconds(newTime);
|
||||
}
|
||||
|
||||
private TimeSpan CalculateEndTime()
|
||||
{
|
||||
if (!(ProfileEditorService.SelectedProfileElement is Layer layer))
|
||||
return TimeSpan.MaxValue;
|
||||
|
||||
var keyframes = GetKeyframes(false);
|
||||
|
||||
// If there are no keyframes, don't stop at all
|
||||
if (!keyframes.Any())
|
||||
return TimeSpan.MaxValue;
|
||||
// If there are keyframes, stop after the last keyframe + 10 sec
|
||||
return keyframes.Max(k => k.Position).Add(TimeSpan.FromSeconds(10));
|
||||
}
|
||||
|
||||
private void CoreServiceOnFrameRendering(object sender, FrameRenderingEventArgs e)
|
||||
{
|
||||
Execute.PostToUIThread(() =>
|
||||
{
|
||||
var newTime = ProfileEditorService.CurrentTime.Add(TimeSpan.FromSeconds(e.DeltaTime));
|
||||
if (RepeatAfterLastKeyframe)
|
||||
{
|
||||
if (newTime > CalculateEndTime().Subtract(TimeSpan.FromSeconds(10)))
|
||||
newTime = TimeSpan.Zero;
|
||||
}
|
||||
else if (newTime > CalculateEndTime())
|
||||
{
|
||||
newTime = CalculateEndTime();
|
||||
Pause();
|
||||
}
|
||||
|
||||
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
|
||||
var parent = (IInputElement) VisualTreeHelper.GetParent((DependencyObject) sender);
|
||||
var x = Math.Max(0, e.GetPosition(parent).X);
|
||||
var 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))
|
||||
{
|
||||
var snappedTime = ProfileEditorService.SnapToTimeline(newTime, TimeSpan.FromMilliseconds(1000f / ProfileEditorService.PixelsPerSecond * 5), true, false, true);
|
||||
ProfileEditorService.CurrentTime = snappedTime;
|
||||
return;
|
||||
}
|
||||
|
||||
// If holding down control, round to the closest 50ms
|
||||
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
|
||||
{
|
||||
var roundedTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 50.0) * 50.0);
|
||||
ProfileEditorService.CurrentTime = roundedTime;
|
||||
return;
|
||||
}
|
||||
|
||||
ProfileEditorService.CurrentTime = newTime;
|
||||
}
|
||||
}
|
||||
|
||||
private List<BaseLayerPropertyKeyframe> GetKeyframes(bool visibleOnly)
|
||||
{
|
||||
var result = new List<BaseLayerPropertyKeyframe>();
|
||||
foreach (var layerPropertyGroupViewModel in LayerPropertyGroups)
|
||||
result.AddRange(layerPropertyGroupViewModel.GetKeyframes(visibleOnly));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Artemis.Core.Models.Profile;
|
||||
using Artemis.Core.Models.Profile.LayerProperties;
|
||||
using Artemis.Core.Models.Profile.LayerProperties.Attributes;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Abstract;
|
||||
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline;
|
||||
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Humanizer;
|
||||
using Ninject;
|
||||
using Ninject.Parameters;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties
|
||||
{
|
||||
public class LayerPropertyGroupViewModel : LayerPropertyBaseViewModel
|
||||
{
|
||||
public enum ViewModelType
|
||||
{
|
||||
General,
|
||||
Transform,
|
||||
LayerBrushRoot,
|
||||
LayerEffectRoot,
|
||||
None
|
||||
}
|
||||
|
||||
private readonly ILayerPropertyVmFactory _layerPropertyVmFactory;
|
||||
private ViewModelType _groupType;
|
||||
private TreePropertyGroupViewModel _treePropertyGroupViewModel;
|
||||
private TimelinePropertyGroupViewModel _timelinePropertyGroupViewModel;
|
||||
|
||||
public LayerPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, PropertyGroupDescriptionAttribute propertyGroupDescription,
|
||||
IProfileEditorService profileEditorService, ILayerPropertyVmFactory layerPropertyVmFactory)
|
||||
{
|
||||
_layerPropertyVmFactory = layerPropertyVmFactory;
|
||||
ProfileEditorService = profileEditorService;
|
||||
|
||||
LayerPropertyGroup = layerPropertyGroup;
|
||||
PropertyGroupDescription = propertyGroupDescription;
|
||||
|
||||
TreePropertyGroupViewModel = _layerPropertyVmFactory.TreePropertyGroupViewModel(this);
|
||||
TimelinePropertyGroupViewModel = _layerPropertyVmFactory.TimelinePropertyGroupViewModel(this);
|
||||
|
||||
// Generate a fallback name if the description does not contain one
|
||||
if (PropertyGroupDescription.Name == null)
|
||||
{
|
||||
var propertyInfo = LayerPropertyGroup.Parent?.GetType().GetProperties().FirstOrDefault(p => ReferenceEquals(p.GetValue(LayerPropertyGroup.Parent), LayerPropertyGroup));
|
||||
if (propertyInfo != null)
|
||||
PropertyGroupDescription.Name = propertyInfo.Name.Humanize();
|
||||
else
|
||||
PropertyGroupDescription.Name = "Unknown group";
|
||||
}
|
||||
|
||||
LayerPropertyGroup.VisibilityChanged += LayerPropertyGroupOnVisibilityChanged;
|
||||
PopulateChildren();
|
||||
DetermineType();
|
||||
}
|
||||
|
||||
public override bool IsVisible => !LayerPropertyGroup.IsHidden;
|
||||
public IProfileEditorService ProfileEditorService { get; }
|
||||
public LayerPropertyGroup LayerPropertyGroup { get; }
|
||||
public PropertyGroupDescriptionAttribute PropertyGroupDescription { get; }
|
||||
|
||||
public override bool IsExpanded
|
||||
{
|
||||
get => LayerPropertyGroup.ProfileElement.IsPropertyGroupExpanded(LayerPropertyGroup);
|
||||
set
|
||||
{
|
||||
LayerPropertyGroup.ProfileElement.SetPropertyGroupExpanded(LayerPropertyGroup, value);
|
||||
NotifyOfPropertyChange(nameof(IsExpanded));
|
||||
}
|
||||
}
|
||||
|
||||
public ViewModelType GroupType
|
||||
{
|
||||
get => _groupType;
|
||||
set => SetAndNotify(ref _groupType, value);
|
||||
}
|
||||
|
||||
public TreePropertyGroupViewModel TreePropertyGroupViewModel
|
||||
{
|
||||
get => _treePropertyGroupViewModel;
|
||||
set => SetAndNotify(ref _treePropertyGroupViewModel, value);
|
||||
}
|
||||
|
||||
public TimelinePropertyGroupViewModel TimelinePropertyGroupViewModel
|
||||
{
|
||||
get => _timelinePropertyGroupViewModel;
|
||||
set => SetAndNotify(ref _timelinePropertyGroupViewModel, value);
|
||||
}
|
||||
|
||||
public override List<BaseLayerPropertyKeyframe> GetKeyframes(bool expandedOnly)
|
||||
{
|
||||
var result = new List<BaseLayerPropertyKeyframe>();
|
||||
if (expandedOnly && !IsExpanded || LayerPropertyGroup.IsHidden)
|
||||
return result;
|
||||
|
||||
foreach (var layerPropertyBaseViewModel in Children)
|
||||
result.AddRange(layerPropertyBaseViewModel.GetKeyframes(expandedOnly));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
foreach (var layerPropertyBaseViewModel in Children)
|
||||
layerPropertyBaseViewModel.Dispose();
|
||||
|
||||
LayerPropertyGroup.VisibilityChanged -= LayerPropertyGroupOnVisibilityChanged;
|
||||
TimelinePropertyGroupViewModel.Dispose();
|
||||
}
|
||||
|
||||
public List<LayerPropertyBaseViewModel> GetAllChildren()
|
||||
{
|
||||
var result = new List<LayerPropertyBaseViewModel>();
|
||||
foreach (var layerPropertyBaseViewModel in Children)
|
||||
{
|
||||
result.Add(layerPropertyBaseViewModel);
|
||||
if (layerPropertyBaseViewModel is LayerPropertyGroupViewModel layerPropertyGroupViewModel)
|
||||
result.AddRange(layerPropertyGroupViewModel.GetAllChildren());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void DetermineType()
|
||||
{
|
||||
if (LayerPropertyGroup is LayerGeneralProperties)
|
||||
GroupType = ViewModelType.General;
|
||||
else if (LayerPropertyGroup is LayerTransformProperties)
|
||||
GroupType = ViewModelType.Transform;
|
||||
else if (LayerPropertyGroup.Parent == null && LayerPropertyGroup.LayerBrush != null)
|
||||
GroupType = ViewModelType.LayerBrushRoot;
|
||||
else if (LayerPropertyGroup.Parent == null && LayerPropertyGroup.LayerEffect != null)
|
||||
GroupType = ViewModelType.LayerEffectRoot;
|
||||
else
|
||||
GroupType = ViewModelType.None;
|
||||
}
|
||||
|
||||
private void PopulateChildren()
|
||||
{
|
||||
// Get all properties and property groups and create VMs for them
|
||||
foreach (var propertyInfo in LayerPropertyGroup.GetType().GetProperties())
|
||||
{
|
||||
var propertyAttribute = (PropertyDescriptionAttribute) Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyDescriptionAttribute));
|
||||
var groupAttribute = (PropertyGroupDescriptionAttribute) Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyGroupDescriptionAttribute));
|
||||
var value = propertyInfo.GetValue(LayerPropertyGroup);
|
||||
|
||||
// Create VMs for properties on the group
|
||||
if (propertyAttribute != null && value is BaseLayerProperty baseLayerProperty)
|
||||
{
|
||||
var viewModel = CreateLayerPropertyViewModel(baseLayerProperty, propertyAttribute);
|
||||
if (viewModel != null)
|
||||
Children.Add(viewModel);
|
||||
}
|
||||
// Create VMs for child groups on this group, resulting in a nested structure
|
||||
else if (groupAttribute != null && value is LayerPropertyGroup layerPropertyGroup)
|
||||
Children.Add(_layerPropertyVmFactory.LayerPropertyGroupViewModel(layerPropertyGroup, groupAttribute));
|
||||
}
|
||||
}
|
||||
|
||||
private LayerPropertyBaseViewModel CreateLayerPropertyViewModel(BaseLayerProperty baseLayerProperty, PropertyDescriptionAttribute propertyDescription)
|
||||
{
|
||||
// Go through the pain of instantiating a generic type VM now via reflection to make things a lot simpler down the line
|
||||
var genericType = baseLayerProperty.GetType().Name == typeof(LayerProperty<>).Name
|
||||
? baseLayerProperty.GetType().GetGenericArguments()[0]
|
||||
: baseLayerProperty.GetType().BaseType.GetGenericArguments()[0];
|
||||
|
||||
// Only create entries for types supported by a tree input VM
|
||||
if (!genericType.IsEnum && ProfileEditorService.RegisteredPropertyEditors.All(r => r.SupportedType != genericType))
|
||||
return null;
|
||||
var genericViewModel = typeof(LayerPropertyViewModel<>).MakeGenericType(genericType);
|
||||
var parameters = new IParameter[]
|
||||
{
|
||||
new ConstructorArgument("layerProperty", baseLayerProperty),
|
||||
new ConstructorArgument("propertyDescription", propertyDescription)
|
||||
};
|
||||
|
||||
return (LayerPropertyBaseViewModel) ProfileEditorService.Kernel.Get(genericViewModel, parameters);
|
||||
}
|
||||
|
||||
private void LayerPropertyGroupOnVisibilityChanged(object sender, EventArgs e)
|
||||
{
|
||||
NotifyOfPropertyChange(nameof(IsVisible));
|
||||
}
|
||||
|
||||
public void UpdateOrder(int order)
|
||||
{
|
||||
LayerPropertyGroup.LayerEffect.Order = order;
|
||||
NotifyOfPropertyChange(nameof(IsExpanded));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Artemis.Core.Models.Profile.LayerProperties;
|
||||
using Artemis.UI.Exceptions;
|
||||
using Artemis.UI.PropertyInput;
|
||||
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Abstract;
|
||||
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline;
|
||||
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree;
|
||||
using Artemis.UI.Shared.PropertyInput;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Humanizer;
|
||||
using Ninject;
|
||||
using Ninject.Parameters;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties
|
||||
{
|
||||
public class LayerPropertyViewModel<T> : LayerPropertyViewModel
|
||||
{
|
||||
private TreePropertyViewModel<T> _treePropertyViewModel;
|
||||
private TimelinePropertyViewModel<T> _timelinePropertyViewModel;
|
||||
|
||||
public LayerPropertyViewModel(IProfileEditorService profileEditorService, LayerProperty<T> layerProperty) : base(profileEditorService, layerProperty)
|
||||
{
|
||||
LayerProperty = layerProperty;
|
||||
|
||||
TreePropertyViewModel = CreateTreePropertyViewModel();
|
||||
TimelinePropertyViewModel = new TimelinePropertyViewModel<T>(this, profileEditorService);
|
||||
|
||||
TreePropertyBaseViewModel = TreePropertyViewModel;
|
||||
TimelinePropertyBaseViewModel = TimelinePropertyViewModel;
|
||||
|
||||
// Generate a fallback name if the description does not contain one
|
||||
if (LayerProperty.PropertyDescription.Name == null)
|
||||
{
|
||||
var propertyInfo = LayerProperty.Parent?.GetType().GetProperties().FirstOrDefault(p => ReferenceEquals(p.GetValue(LayerProperty.Parent), LayerProperty));
|
||||
if (propertyInfo != null)
|
||||
LayerProperty.PropertyDescription.Name = propertyInfo.Name.Humanize();
|
||||
else
|
||||
LayerProperty.PropertyDescription.Name = $"Unknown {typeof(T).Name} property";
|
||||
}
|
||||
|
||||
LayerProperty.VisibilityChanged += LayerPropertyOnVisibilityChanged;
|
||||
}
|
||||
|
||||
public override bool IsVisible => !LayerProperty.IsHidden;
|
||||
|
||||
public LayerProperty<T> LayerProperty { get; }
|
||||
|
||||
public TreePropertyViewModel<T> TreePropertyViewModel
|
||||
{
|
||||
get => _treePropertyViewModel;
|
||||
set => SetAndNotify(ref _treePropertyViewModel, value);
|
||||
}
|
||||
|
||||
public TimelinePropertyViewModel<T> TimelinePropertyViewModel
|
||||
{
|
||||
get => _timelinePropertyViewModel;
|
||||
set => SetAndNotify(ref _timelinePropertyViewModel, value);
|
||||
}
|
||||
|
||||
public override List<BaseLayerPropertyKeyframe> GetKeyframes(bool expandedOnly)
|
||||
{
|
||||
if (LayerProperty.KeyframesEnabled && !LayerProperty.IsHidden)
|
||||
return LayerProperty.BaseKeyframes.ToList();
|
||||
return new List<BaseLayerPropertyKeyframe>();
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
TreePropertyViewModel.Dispose();
|
||||
TimelinePropertyViewModel.Dispose();
|
||||
|
||||
LayerProperty.VisibilityChanged -= LayerPropertyOnVisibilityChanged;
|
||||
}
|
||||
|
||||
public void SetCurrentValue(T value, bool saveChanges)
|
||||
{
|
||||
LayerProperty.SetCurrentValue(value, ProfileEditorService.CurrentTime);
|
||||
if (saveChanges)
|
||||
ProfileEditorService.UpdateSelectedProfileElement();
|
||||
else
|
||||
ProfileEditorService.UpdateProfilePreview();
|
||||
}
|
||||
|
||||
private TreePropertyViewModel<T> CreateTreePropertyViewModel()
|
||||
{
|
||||
// Make sure there is a supported property editor VM, unless the type is an enum, then we'll use the EnumPropertyInputViewModel
|
||||
Type vmType = null;
|
||||
if (typeof(T).IsEnum)
|
||||
vmType = typeof(EnumPropertyInputViewModel<>).MakeGenericType(typeof(T));
|
||||
else
|
||||
{
|
||||
var registration = ProfileEditorService.RegisteredPropertyEditors.FirstOrDefault(r => r.SupportedType == typeof(T));
|
||||
if (registration != null)
|
||||
vmType = registration.ViewModelType;
|
||||
}
|
||||
|
||||
if (vmType == null)
|
||||
throw new ArtemisUIException($"Cannot create a tree property view model for type {typeof(T)}, found no matching property editor");
|
||||
|
||||
var parameters = new IParameter[]
|
||||
{
|
||||
new ConstructorArgument("layerProperty", LayerProperty)
|
||||
};
|
||||
return new TreePropertyViewModel<T>(this, (PropertyInputViewModel<T>) ProfileEditorService.Kernel.Get(vmType, parameters), ProfileEditorService);
|
||||
}
|
||||
|
||||
private void LayerPropertyOnVisibilityChanged(object sender, EventArgs e)
|
||||
{
|
||||
NotifyOfPropertyChange(nameof(IsVisible));
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class LayerPropertyViewModel : LayerPropertyBaseViewModel
|
||||
{
|
||||
private TreePropertyViewModel _treePropertyBaseViewModel;
|
||||
private TimelinePropertyViewModel _timelinePropertyBaseViewModel;
|
||||
|
||||
protected LayerPropertyViewModel(IProfileEditorService profileEditorService, BaseLayerProperty baseLayerProperty)
|
||||
{
|
||||
ProfileEditorService = profileEditorService;
|
||||
BaseLayerProperty = baseLayerProperty;
|
||||
}
|
||||
|
||||
public IProfileEditorService ProfileEditorService { get; }
|
||||
public BaseLayerProperty BaseLayerProperty { get; }
|
||||
|
||||
public TreePropertyViewModel TreePropertyBaseViewModel
|
||||
{
|
||||
get => _treePropertyBaseViewModel;
|
||||
set => SetAndNotify(ref _treePropertyBaseViewModel, value);
|
||||
}
|
||||
|
||||
public TimelinePropertyViewModel TimelinePropertyBaseViewModel
|
||||
{
|
||||
get => _timelinePropertyBaseViewModel;
|
||||
set => SetAndNotify(ref _timelinePropertyBaseViewModel, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
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 FillProperty = DependencyProperty.Register(nameof(Fill), 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 Fill
|
||||
{
|
||||
get => (Brush) GetValue(FillProperty);
|
||||
set => SetValue(FillProperty, 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();
|
||||
|
||||
var linePen = new Pen(Fill, 1);
|
||||
var width = HorizontalOffset + VisibleWidth;
|
||||
var frameStart = 0;
|
||||
|
||||
var units = PixelsPerSecond / _subd1;
|
||||
var offsetUnits = frameStart * PixelsPerSecond % units;
|
||||
|
||||
// Labels
|
||||
var count = (width + offsetUnits) / units;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var 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;
|
||||
|
||||
var 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 (var i = 0; i < count; i++)
|
||||
{
|
||||
var 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
|
||||
var mul = _subd3 / _subd2;
|
||||
units = PixelsPerSecond / _subd3;
|
||||
count = (width + offsetUnits) / units;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
if (Math.Abs(i % mul) < 0.001) continue;
|
||||
var 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)
|
||||
{
|
||||
var typeFace = new Typeface(FontFamily, new FontStyle(), new FontWeight(), new FontStretch());
|
||||
var formattedText = new FormattedText(text, CultureInfo.CurrentUICulture, FlowDirection.LeftToRight, typeFace, 9, Fill, null, VisualTreeHelper.GetDpi(this).PixelsPerDip);
|
||||
if (x == 0 && OffsetFirstValue)
|
||||
drawingContext.DrawText(formattedText, new Point(2, 2));
|
||||
else
|
||||
drawingContext.DrawText(formattedText, new Point(x - formattedText.Width / 2, 2));
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using Artemis.Core.Utilities;
|
||||
using Humanizer;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
|
||||
{
|
||||
public class TimelineEasingViewModel
|
||||
{
|
||||
private readonly TimelineKeyframeViewModel _keyframeViewModel;
|
||||
private bool _isEasingModeSelected;
|
||||
|
||||
public TimelineEasingViewModel(TimelineKeyframeViewModel keyframeViewModel, Easings.Functions easingFunction)
|
||||
{
|
||||
_keyframeViewModel = keyframeViewModel;
|
||||
_isEasingModeSelected = keyframeViewModel.BaseLayerPropertyKeyframe.EasingFunction == easingFunction;
|
||||
|
||||
EasingFunction = easingFunction;
|
||||
Description = easingFunction.Humanize();
|
||||
|
||||
EasingPoints = new PointCollection();
|
||||
for (var i = 1; i <= 10; i++)
|
||||
{
|
||||
var x = i;
|
||||
var 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 (_isEasingModeSelected)
|
||||
_keyframeViewModel.SelectEasingMode(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using Artemis.Core.Models.Profile.LayerProperties;
|
||||
using Artemis.Core.Utilities;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
|
||||
{
|
||||
public class TimelineKeyframeViewModel<T> : TimelineKeyframeViewModel
|
||||
{
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
|
||||
public TimelineKeyframeViewModel(IProfileEditorService profileEditorService, LayerPropertyKeyframe<T> layerPropertyKeyframe)
|
||||
: base(profileEditorService, layerPropertyKeyframe)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
LayerPropertyKeyframe = layerPropertyKeyframe;
|
||||
}
|
||||
|
||||
public LayerPropertyKeyframe<T> LayerPropertyKeyframe { get; }
|
||||
|
||||
#region Context menu actions
|
||||
|
||||
public override void Copy()
|
||||
{
|
||||
var newKeyframe = new LayerPropertyKeyframe<T>(
|
||||
LayerPropertyKeyframe.Value,
|
||||
LayerPropertyKeyframe.Position,
|
||||
LayerPropertyKeyframe.EasingFunction,
|
||||
LayerPropertyKeyframe.LayerProperty
|
||||
);
|
||||
// If possible, shift the keyframe to the right by 11 pixels
|
||||
var desiredPosition = newKeyframe.Position + TimeSpan.FromMilliseconds(1000f / _profileEditorService.PixelsPerSecond * 11);
|
||||
if (desiredPosition <= newKeyframe.LayerProperty.ProfileElement.TimelineLength)
|
||||
newKeyframe.Position = desiredPosition;
|
||||
// Otherwise if possible shift it to the left by 11 pixels
|
||||
else
|
||||
{
|
||||
desiredPosition = newKeyframe.Position - TimeSpan.FromMilliseconds(1000f / _profileEditorService.PixelsPerSecond * 11);
|
||||
if (desiredPosition > TimeSpan.Zero)
|
||||
newKeyframe.Position = desiredPosition;
|
||||
}
|
||||
|
||||
LayerPropertyKeyframe.LayerProperty.AddKeyframe(newKeyframe);
|
||||
_profileEditorService.UpdateSelectedProfileElement();
|
||||
}
|
||||
|
||||
public override void Delete()
|
||||
{
|
||||
LayerPropertyKeyframe.LayerProperty.RemoveKeyframe(LayerPropertyKeyframe);
|
||||
_profileEditorService.UpdateSelectedProfileElement();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public abstract class TimelineKeyframeViewModel : PropertyChangedBase, IDisposable
|
||||
{
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private BindableCollection<TimelineEasingViewModel> _easingViewModels;
|
||||
private bool _isSelected;
|
||||
private int _pixelsPerSecond;
|
||||
private string _timestamp;
|
||||
private double _x;
|
||||
|
||||
protected TimelineKeyframeViewModel(IProfileEditorService profileEditorService, BaseLayerPropertyKeyframe baseLayerPropertyKeyframe)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
BaseLayerPropertyKeyframe = baseLayerPropertyKeyframe;
|
||||
EasingViewModels = new BindableCollection<TimelineEasingViewModel>();
|
||||
|
||||
BaseLayerPropertyKeyframe.PropertyChanged += BaseLayerPropertyKeyframeOnPropertyChanged;
|
||||
}
|
||||
|
||||
public BaseLayerPropertyKeyframe BaseLayerPropertyKeyframe { get; }
|
||||
|
||||
public BindableCollection<TimelineEasingViewModel> EasingViewModels
|
||||
{
|
||||
get => _easingViewModels;
|
||||
set => SetAndNotify(ref _easingViewModels, value);
|
||||
}
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
get => _isSelected;
|
||||
set => SetAndNotify(ref _isSelected, value);
|
||||
}
|
||||
|
||||
public double X
|
||||
{
|
||||
get => _x;
|
||||
set => SetAndNotify(ref _x, value);
|
||||
}
|
||||
|
||||
public string Timestamp
|
||||
{
|
||||
get => _timestamp;
|
||||
set => SetAndNotify(ref _timestamp, value);
|
||||
}
|
||||
|
||||
public void Update(int pixelsPerSecond)
|
||||
{
|
||||
_pixelsPerSecond = pixelsPerSecond;
|
||||
|
||||
X = pixelsPerSecond * BaseLayerPropertyKeyframe.Position.TotalSeconds;
|
||||
Timestamp = $"{Math.Floor(BaseLayerPropertyKeyframe.Position.TotalSeconds):00}.{BaseLayerPropertyKeyframe.Position.Milliseconds:000}";
|
||||
}
|
||||
|
||||
public abstract void Copy();
|
||||
|
||||
public abstract void Delete();
|
||||
|
||||
private void BaseLayerPropertyKeyframeOnPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(BaseLayerPropertyKeyframe.Position))
|
||||
Update(_pixelsPerSecond);
|
||||
}
|
||||
|
||||
#region Easing
|
||||
|
||||
public void CreateEasingViewModels()
|
||||
{
|
||||
EasingViewModels.AddRange(Enum.GetValues(typeof(Easings.Functions)).Cast<Easings.Functions>().Select(v => new TimelineEasingViewModel(this, v)));
|
||||
}
|
||||
|
||||
public void SelectEasingMode(TimelineEasingViewModel easingViewModel)
|
||||
{
|
||||
BaseLayerPropertyKeyframe.EasingFunction = easingViewModel.EasingFunction;
|
||||
// Set every selection to false except on the VM that made the change
|
||||
foreach (var propertyTrackEasingViewModel in EasingViewModels.Where(vm => vm != easingViewModel))
|
||||
propertyTrackEasingViewModel.IsEasingModeSelected = false;
|
||||
|
||||
|
||||
_profileEditorService.UpdateSelectedProfileElement();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Movement
|
||||
|
||||
private TimeSpan? _offset;
|
||||
|
||||
public void ReleaseMovement()
|
||||
{
|
||||
_offset = null;
|
||||
}
|
||||
|
||||
public void SaveOffsetToKeyframe(TimelineKeyframeViewModel keyframeViewModel)
|
||||
{
|
||||
if (keyframeViewModel == this)
|
||||
{
|
||||
_offset = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_offset != null)
|
||||
return;
|
||||
|
||||
_offset = BaseLayerPropertyKeyframe.Position - keyframeViewModel.BaseLayerPropertyKeyframe.Position;
|
||||
}
|
||||
|
||||
public void ApplyOffsetToKeyframe(TimelineKeyframeViewModel keyframeViewModel)
|
||||
{
|
||||
if (keyframeViewModel == this || _offset == null)
|
||||
return;
|
||||
|
||||
UpdatePosition(keyframeViewModel.BaseLayerPropertyKeyframe.Position + _offset.Value);
|
||||
}
|
||||
|
||||
public void UpdatePosition(TimeSpan position)
|
||||
{
|
||||
if (position < TimeSpan.Zero)
|
||||
BaseLayerPropertyKeyframe.Position = TimeSpan.Zero;
|
||||
else if (position > _profileEditorService.SelectedProfileElement.TimelineLength)
|
||||
BaseLayerPropertyKeyframe.Position = _profileEditorService.SelectedProfileElement.TimelineLength;
|
||||
else
|
||||
BaseLayerPropertyKeyframe.Position = position;
|
||||
|
||||
Update(_pixelsPerSecond);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
BaseLayerPropertyKeyframe.PropertyChanged -= BaseLayerPropertyKeyframeOnPropertyChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.TimelinePropertyGroupView"
|
||||
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"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:layerProperties1="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance local:TimelinePropertyGroupViewModel}"
|
||||
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 TimelineKeyframeViewModels}"
|
||||
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.Children}"
|
||||
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 layerProperties1:LayerPropertyGroupViewModel}">
|
||||
<ContentControl s:View.Model="{Binding TimelinePropertyGroupViewModel}" IsTabStop="False" HorizontalAlignment="Stretch"/>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type layerProperties1:LayerPropertyViewModel}">
|
||||
<ContentControl s:View.Model="{Binding TimelinePropertyBaseViewModel}" IsTabStop="False" HorizontalAlignment="Stretch" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.Resources>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Abstract;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
|
||||
{
|
||||
public class TimelinePropertyGroupViewModel : PropertyChangedBase
|
||||
{
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private BindableCollection<double> _timelineKeyframeViewModels;
|
||||
|
||||
public TimelinePropertyGroupViewModel(LayerPropertyBaseViewModel layerPropertyBaseViewModel, IProfileEditorService profileEditorService)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
LayerPropertyGroupViewModel = (LayerPropertyGroupViewModel) layerPropertyBaseViewModel;
|
||||
TimelineKeyframeViewModels = new BindableCollection<double>();
|
||||
|
||||
LayerPropertyGroupViewModel.ProfileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged;
|
||||
LayerPropertyGroupViewModel.PropertyChanged += LayerPropertyGroupViewModelOnPropertyChanged;
|
||||
UpdateKeyframes();
|
||||
}
|
||||
|
||||
public LayerPropertyGroupViewModel LayerPropertyGroupViewModel { get; }
|
||||
|
||||
public BindableCollection<double> TimelineKeyframeViewModels
|
||||
{
|
||||
get => _timelineKeyframeViewModels;
|
||||
set => SetAndNotify(ref _timelineKeyframeViewModels, value);
|
||||
}
|
||||
|
||||
public void UpdateKeyframes()
|
||||
{
|
||||
TimelineKeyframeViewModels.Clear();
|
||||
TimelineKeyframeViewModels.AddRange(LayerPropertyGroupViewModel.GetKeyframes(false)
|
||||
.Select(k => LayerPropertyGroupViewModel.ProfileEditorService.PixelsPerSecond * k.Position.TotalSeconds));
|
||||
}
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
LayerPropertyGroupViewModel.ProfileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged;
|
||||
LayerPropertyGroupViewModel.PropertyChanged -= LayerPropertyGroupViewModelOnPropertyChanged;
|
||||
}
|
||||
|
||||
private void LayerPropertyGroupViewModelOnPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(LayerPropertyGroupViewModel.IsExpanded))
|
||||
UpdateKeyframes();
|
||||
}
|
||||
|
||||
private void ProfileEditorServiceOnPixelsPerSecondChanged(object sender, EventArgs e)
|
||||
{
|
||||
UpdateKeyframes();
|
||||
}
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
<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:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline"
|
||||
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"
|
||||
d:DataContext="{d:DesignInstance local:TimelinePropertyViewModel}"
|
||||
Visibility="{Binding LayerPropertyBaseViewModel.IsVisible, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}"
|
||||
MinWidth="{Binding Width}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<Border Height="25" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource MaterialDesignDivider}">
|
||||
<ItemsControl ItemsSource="{Binding TimelineKeyframeViewModels}"
|
||||
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 ContextMenuOpening}"
|
||||
ContextMenuClosing="{s:Action ContextMenuClosing}">
|
||||
<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="Copy" Command="{s:Action Copy}" CommandParameter="{Binding}">
|
||||
<MenuItem.Icon>
|
||||
<materialDesign:PackIcon Kind="ContentCopy" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Delete" Command="{s:Action Delete}" CommandParameter="{Binding}">
|
||||
<MenuItem.Icon>
|
||||
<materialDesign:PackIcon Kind="Delete" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<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>
|
||||
<!-- <MenuItem Header="Easing mode" IsEnabled="{Binding CanSelectEasingMode}"> -->
|
||||
<!-- <MenuItem Header="Ease in" Command="{s:Action SetEasingMode}" CommandParameter="EaseIn" /> -->
|
||||
<!-- <MenuItem Header="Ease out" Command="{s:Action SetEasingMode}" CommandParameter="EaseOut" /> -->
|
||||
<!-- <MenuItem Header="Ease in and out" Command="{s:Action SetEasingMode}" CommandParameter="EaseInOut" /> -->
|
||||
<!-- </MenuItem> -->
|
||||
</ContextMenu>
|
||||
</Ellipse.ContextMenu>
|
||||
</Ellipse>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Border>
|
||||
</UserControl>
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Abstract;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
|
||||
{
|
||||
public class TimelinePropertyViewModel<T> : TimelinePropertyViewModel
|
||||
{
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
|
||||
public TimelinePropertyViewModel(LayerPropertyBaseViewModel layerPropertyBaseViewModel, IProfileEditorService profileEditorService) : base(layerPropertyBaseViewModel)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
LayerPropertyViewModel = (LayerPropertyViewModel<T>) layerPropertyBaseViewModel;
|
||||
|
||||
LayerPropertyViewModel.LayerProperty.KeyframeAdded += LayerPropertyOnKeyframeModified;
|
||||
LayerPropertyViewModel.LayerProperty.KeyframeRemoved += LayerPropertyOnKeyframeModified;
|
||||
LayerPropertyViewModel.LayerProperty.KeyframesToggled += LayerPropertyOnKeyframeModified;
|
||||
_profileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged;
|
||||
}
|
||||
|
||||
public LayerPropertyViewModel<T> LayerPropertyViewModel { get; }
|
||||
|
||||
public override void UpdateKeyframes()
|
||||
{
|
||||
// Only show keyframes if they are enabled
|
||||
if (LayerPropertyViewModel.LayerProperty.KeyframesEnabled)
|
||||
{
|
||||
var keyframes = LayerPropertyViewModel.LayerProperty.Keyframes.ToList();
|
||||
var toRemove = TimelineKeyframeViewModels.Where(t => !keyframes.Contains(t.BaseLayerPropertyKeyframe)).ToList();
|
||||
foreach (var timelineKeyframeViewModel in toRemove)
|
||||
timelineKeyframeViewModel.Dispose();
|
||||
|
||||
TimelineKeyframeViewModels.RemoveRange(toRemove);
|
||||
TimelineKeyframeViewModels.AddRange(keyframes
|
||||
.Where(k => TimelineKeyframeViewModels.All(t => t.BaseLayerPropertyKeyframe != k))
|
||||
.Select(k => new TimelineKeyframeViewModel<T>(_profileEditorService, k))
|
||||
);
|
||||
}
|
||||
else
|
||||
DisposeKeyframeViewModels();
|
||||
|
||||
foreach (var timelineKeyframeViewModel in TimelineKeyframeViewModels)
|
||||
timelineKeyframeViewModel.Update(_profileEditorService.PixelsPerSecond);
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
_profileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged;
|
||||
LayerPropertyViewModel.LayerProperty.KeyframeAdded -= LayerPropertyOnKeyframeModified;
|
||||
LayerPropertyViewModel.LayerProperty.KeyframeRemoved -= LayerPropertyOnKeyframeModified;
|
||||
LayerPropertyViewModel.LayerProperty.KeyframesToggled -= LayerPropertyOnKeyframeModified;
|
||||
DisposeKeyframeViewModels();
|
||||
}
|
||||
|
||||
private void DisposeKeyframeViewModels()
|
||||
{
|
||||
foreach (var timelineKeyframeViewModel in TimelineKeyframeViewModels)
|
||||
timelineKeyframeViewModel.Dispose();
|
||||
TimelineKeyframeViewModels.Clear();
|
||||
}
|
||||
|
||||
private void LayerPropertyOnKeyframeModified(object sender, EventArgs e)
|
||||
{
|
||||
UpdateKeyframes();
|
||||
}
|
||||
|
||||
private void ProfileEditorServiceOnPixelsPerSecondChanged(object sender, EventArgs e)
|
||||
{
|
||||
foreach (var timelineKeyframeViewModel in TimelineKeyframeViewModels)
|
||||
timelineKeyframeViewModel.Update(_profileEditorService.PixelsPerSecond);
|
||||
|
||||
Width = TimelineKeyframeViewModels.Any() ? TimelineKeyframeViewModels.Max(t => t.X) + 25 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class TimelinePropertyViewModel : PropertyChangedBase, IDisposable
|
||||
{
|
||||
private BindableCollection<TimelineKeyframeViewModel> _timelineKeyframeViewModels;
|
||||
private double _width;
|
||||
|
||||
protected TimelinePropertyViewModel(LayerPropertyBaseViewModel layerPropertyBaseViewModel)
|
||||
{
|
||||
LayerPropertyBaseViewModel = layerPropertyBaseViewModel;
|
||||
TimelineKeyframeViewModels = new BindableCollection<TimelineKeyframeViewModel>();
|
||||
}
|
||||
|
||||
public LayerPropertyBaseViewModel LayerPropertyBaseViewModel { get; }
|
||||
|
||||
public BindableCollection<TimelineKeyframeViewModel> TimelineKeyframeViewModels
|
||||
{
|
||||
get => _timelineKeyframeViewModels;
|
||||
set => SetAndNotify(ref _timelineKeyframeViewModels, value);
|
||||
}
|
||||
|
||||
public double Width
|
||||
{
|
||||
get => _width;
|
||||
set => SetAndNotify(ref _width, value);
|
||||
}
|
||||
|
||||
public abstract void Dispose();
|
||||
|
||||
public abstract void UpdateKeyframes();
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
<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:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline"
|
||||
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="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}"/>
|
||||
</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>
|
||||
+302
@@ -0,0 +1,302 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using Artemis.Core.Models.Profile;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Artemis.UI.Shared.Utilities;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
|
||||
{
|
||||
public class TimelineSegmentViewModel : PropertyChangedBase, IDisposable
|
||||
{
|
||||
private bool _draggingSegment;
|
||||
private bool _showSegmentName;
|
||||
private bool _showRepeatButton;
|
||||
private bool _showDisableButton;
|
||||
|
||||
public TimelineSegmentViewModel(IProfileEditorService profileEditorService, SegmentViewModelType segment)
|
||||
{
|
||||
ProfileEditorService = profileEditorService;
|
||||
Segment = segment;
|
||||
SelectedProfileElement = ProfileEditorService.SelectedProfileElement;
|
||||
|
||||
switch (Segment)
|
||||
{
|
||||
case SegmentViewModelType.Start:
|
||||
ToolTip = "This segment is played when a layer starts displaying because it's conditions are met";
|
||||
break;
|
||||
case SegmentViewModelType.Main:
|
||||
ToolTip = "This segment is played while a condition is met, either once or on a repeating loop";
|
||||
break;
|
||||
case SegmentViewModelType.End:
|
||||
ToolTip = "This segment is played once a condition is no longer met";
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(segment));
|
||||
}
|
||||
|
||||
UpdateDisplay();
|
||||
ProfileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged;
|
||||
SelectedProfileElement.PropertyChanged += SelectedProfileElementOnPropertyChanged;
|
||||
}
|
||||
|
||||
public RenderProfileElement SelectedProfileElement { get; }
|
||||
|
||||
public SegmentViewModelType Segment { get; }
|
||||
public IProfileEditorService ProfileEditorService { get; }
|
||||
public string ToolTip { get; }
|
||||
|
||||
public TimeSpan SegmentLength
|
||||
{
|
||||
get
|
||||
{
|
||||
return Segment switch
|
||||
{
|
||||
SegmentViewModelType.Start => SelectedProfileElement?.StartSegmentLength ?? TimeSpan.Zero,
|
||||
SegmentViewModelType.Main => SelectedProfileElement?.MainSegmentLength ?? TimeSpan.Zero,
|
||||
SegmentViewModelType.End => SelectedProfileElement?.EndSegmentLength ?? TimeSpan.Zero,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public double SegmentWidth => ProfileEditorService.PixelsPerSecond * SegmentLength.TotalSeconds;
|
||||
|
||||
public bool SegmentEnabled => SegmentLength != TimeSpan.Zero;
|
||||
public bool IsMainSegment => Segment == SegmentViewModelType.Main;
|
||||
|
||||
// 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?.DisplayContinuously ?? false;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (Segment != SegmentViewModelType.Main)
|
||||
return;
|
||||
|
||||
SelectedProfileElement.DisplayContinuously = value;
|
||||
ProfileEditorService.UpdateSelectedProfileElement();
|
||||
NotifyOfPropertyChange(nameof(RepeatSegment));
|
||||
}
|
||||
}
|
||||
|
||||
public double SegmentStartPosition
|
||||
{
|
||||
get
|
||||
{
|
||||
return Segment switch
|
||||
{
|
||||
SegmentViewModelType.Start => 0,
|
||||
SegmentViewModelType.Main => ProfileEditorService.PixelsPerSecond * SelectedProfileElement.StartSegmentLength.TotalSeconds,
|
||||
SegmentViewModelType.End => ProfileEditorService.PixelsPerSecond * (SelectedProfileElement.StartSegmentLength.TotalSeconds + SelectedProfileElement.MainSegmentLength.TotalSeconds),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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 void Dispose()
|
||||
{
|
||||
ProfileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged;
|
||||
SelectedProfileElement.PropertyChanged -= SelectedProfileElementOnPropertyChanged;
|
||||
}
|
||||
|
||||
public void DisableSegment()
|
||||
{
|
||||
var keyframes = SelectedProfileElement.GetAllKeyframes();
|
||||
var startSegmentEnd = SelectedProfileElement.StartSegmentLength;
|
||||
var mainSegmentEnd = SelectedProfileElement.StartSegmentLength + SelectedProfileElement.MainSegmentLength;
|
||||
|
||||
var oldSegmentLength = SegmentLength;
|
||||
|
||||
if (Segment == SegmentViewModelType.Start)
|
||||
{
|
||||
// Remove keyframes that fall in this segment
|
||||
foreach (var baseLayerPropertyKeyframe in keyframes.Where(k => k.Position <= startSegmentEnd))
|
||||
baseLayerPropertyKeyframe.Remove();
|
||||
SelectedProfileElement.StartSegmentLength = TimeSpan.Zero;
|
||||
}
|
||||
else if (Segment == SegmentViewModelType.Main)
|
||||
{
|
||||
// Remove keyframes that fall in this segment
|
||||
foreach (var baseLayerPropertyKeyframe in keyframes.Where(k => k.Position > startSegmentEnd && k.Position <= mainSegmentEnd))
|
||||
baseLayerPropertyKeyframe.Remove();
|
||||
SelectedProfileElement.MainSegmentLength = TimeSpan.Zero;
|
||||
}
|
||||
else if (Segment == SegmentViewModelType.End)
|
||||
{
|
||||
// Remove keyframes that fall in this segment
|
||||
foreach (var baseLayerPropertyKeyframe in keyframes.Where(k => k.Position > mainSegmentEnd))
|
||||
baseLayerPropertyKeyframe.Remove();
|
||||
SelectedProfileElement.EndSegmentLength = TimeSpan.Zero;
|
||||
}
|
||||
|
||||
NotifyOfPropertyChange(nameof(SegmentEnabled));
|
||||
ShiftNextSegment(SegmentLength - oldSegmentLength);
|
||||
ProfileEditorService.UpdateSelectedProfileElement();
|
||||
}
|
||||
|
||||
public void EnableSegment()
|
||||
{
|
||||
ShiftNextSegment(TimeSpan.FromSeconds(1));
|
||||
if (Segment == SegmentViewModelType.Start)
|
||||
SelectedProfileElement.StartSegmentLength = TimeSpan.FromSeconds(1);
|
||||
else if (Segment == SegmentViewModelType.Main)
|
||||
SelectedProfileElement.MainSegmentLength = TimeSpan.FromSeconds(1);
|
||||
else if (Segment == SegmentViewModelType.End)
|
||||
SelectedProfileElement.EndSegmentLength = TimeSpan.FromSeconds(1);
|
||||
|
||||
NotifyOfPropertyChange(nameof(SegmentEnabled));
|
||||
ProfileEditorService.UpdateSelectedProfileElement();
|
||||
UpdateDisplay();
|
||||
}
|
||||
|
||||
public void ShiftNextSegment(TimeSpan amount)
|
||||
{
|
||||
var segmentEnd = TimeSpan.Zero;
|
||||
if (Segment == SegmentViewModelType.Start)
|
||||
segmentEnd = SelectedProfileElement.StartSegmentLength;
|
||||
else if (Segment == SegmentViewModelType.Main)
|
||||
segmentEnd = SelectedProfileElement.StartSegmentLength + SelectedProfileElement.MainSegmentLength;
|
||||
else if (Segment == SegmentViewModelType.End)
|
||||
segmentEnd = SelectedProfileElement.TimelineLength;
|
||||
|
||||
foreach (var baseLayerPropertyKeyframe in SelectedProfileElement.GetAllKeyframes().Where(k => k.Position > segmentEnd))
|
||||
baseLayerPropertyKeyframe.Position += amount;
|
||||
}
|
||||
|
||||
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.UpdateSelectedProfileElement();
|
||||
}
|
||||
|
||||
public void SegmentMouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (e.LeftButton != MouseButtonState.Pressed || !_draggingSegment)
|
||||
return;
|
||||
|
||||
// Get the parent scroll viewer, need that for our position
|
||||
var parent = VisualTreeUtilities.FindParent<ScrollViewer>((DependencyObject) sender, "TimelineHeaderScrollViewer");
|
||||
|
||||
var x = Math.Max(0, e.GetPosition(parent).X);
|
||||
var 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))
|
||||
newTime = ProfileEditorService.SnapToTimeline(newTime, TimeSpan.FromMilliseconds(1000f / ProfileEditorService.PixelsPerSecond * 5), false, true, true);
|
||||
// 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);
|
||||
|
||||
var oldSegmentLength = SegmentLength;
|
||||
if (Segment == SegmentViewModelType.Start)
|
||||
{
|
||||
if (newTime < TimeSpan.FromMilliseconds(100))
|
||||
newTime = TimeSpan.FromMilliseconds(100);
|
||||
SelectedProfileElement.StartSegmentLength = newTime;
|
||||
}
|
||||
else if (Segment == SegmentViewModelType.Main)
|
||||
{
|
||||
if (newTime < SelectedProfileElement.StartSegmentLength + TimeSpan.FromMilliseconds(100))
|
||||
newTime = SelectedProfileElement.StartSegmentLength + TimeSpan.FromMilliseconds(100);
|
||||
SelectedProfileElement.MainSegmentLength = newTime - SelectedProfileElement.StartSegmentLength;
|
||||
}
|
||||
else if (Segment == SegmentViewModelType.End)
|
||||
{
|
||||
if (newTime < SelectedProfileElement.StartSegmentLength + SelectedProfileElement.MainSegmentLength + TimeSpan.FromMilliseconds(100))
|
||||
newTime = SelectedProfileElement.StartSegmentLength + SelectedProfileElement.MainSegmentLength + TimeSpan.FromMilliseconds(100);
|
||||
SelectedProfileElement.EndSegmentLength = newTime - SelectedProfileElement.StartSegmentLength - SelectedProfileElement.MainSegmentLength;
|
||||
}
|
||||
|
||||
NotifyOfPropertyChange(nameof(SegmentLength));
|
||||
NotifyOfPropertyChange(nameof(SegmentWidth));
|
||||
|
||||
ShiftNextSegment(SegmentLength - oldSegmentLength);
|
||||
UpdateDisplay();
|
||||
}
|
||||
|
||||
private void SelectedProfileElementOnPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(RenderProfileElement.StartSegmentLength) ||
|
||||
e.PropertyName == nameof(RenderProfileElement.MainSegmentLength) ||
|
||||
e.PropertyName == nameof(RenderProfileElement.EndSegmentLength))
|
||||
{
|
||||
NotifyOfPropertyChange(nameof(SegmentStartPosition));
|
||||
NotifyOfPropertyChange(nameof(SegmentWidth));
|
||||
}
|
||||
else if (e.PropertyName == nameof(RenderProfileElement.DisplayContinuously))
|
||||
NotifyOfPropertyChange(nameof(RepeatSegment));
|
||||
}
|
||||
|
||||
|
||||
private void ProfileEditorServiceOnPixelsPerSecondChanged(object? sender, EventArgs e)
|
||||
{
|
||||
NotifyOfPropertyChange(nameof(SegmentWidth));
|
||||
NotifyOfPropertyChange(nameof(SegmentStartPosition));
|
||||
|
||||
UpdateDisplay();
|
||||
}
|
||||
|
||||
private void UpdateDisplay()
|
||||
{
|
||||
if (!IsMainSegment)
|
||||
ShowSegmentName = SegmentWidth > 60;
|
||||
else
|
||||
ShowSegmentName = SegmentWidth > 80;
|
||||
|
||||
ShowRepeatButton = SegmentWidth > 45 && IsMainSegment;
|
||||
ShowDisableButton = SegmentWidth > 25;
|
||||
}
|
||||
}
|
||||
|
||||
public enum SegmentViewModelType
|
||||
{
|
||||
Start,
|
||||
Main,
|
||||
End
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<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"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="25"
|
||||
d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance local:TimelineViewModel}">
|
||||
<Grid x:Name="TimelineContainerGrid"
|
||||
Background="{DynamicResource MaterialDesignToolBarBackground}"
|
||||
MouseDown="{s:Action TimelineCanvasMouseDown}"
|
||||
MouseUp="{s:Action TimelineCanvasMouseUp}"
|
||||
MouseMove="{s:Action TimelineCanvasMouseMove}"
|
||||
Margin="0 0 -1 0">
|
||||
<Grid.Triggers>
|
||||
<EventTrigger RoutedEvent="UIElement.MouseLeftButtonDown">
|
||||
<BeginStoryboard>
|
||||
<Storyboard Storyboard.TargetName="MultiSelectionPath" Storyboard.TargetProperty="Opacity">
|
||||
<DoubleAnimation From="0" To="1" Duration="0:0:0.1" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</EventTrigger>
|
||||
<EventTrigger RoutedEvent="UIElement.MouseLeftButtonUp">
|
||||
<BeginStoryboard>
|
||||
<Storyboard Storyboard.TargetName="MultiSelectionPath" Storyboard.TargetProperty="Opacity">
|
||||
<DoubleAnimation From="1" To="0" Duration="0:0:0.2" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</EventTrigger>
|
||||
</Grid.Triggers>
|
||||
|
||||
<ItemsControl ItemsSource="{Binding LayerPropertyGroups}"
|
||||
MinWidth="{Binding TotalTimelineWidth}"
|
||||
HorizontalAlignment="Left">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<ContentControl s:View.Model="{Binding TimelinePropertyGroupViewModel}" 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 ActualHeight, ElementName=TimelineContainerGrid}"
|
||||
HorizontalAlignment="Left"
|
||||
Visibility="{Binding StartSegmentEnabled, 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 ActualHeight, ElementName=TimelineContainerGrid}" />
|
||||
<Line Stroke="{StaticResource PrimaryHueDarkBrush}"
|
||||
Opacity="0.5"
|
||||
StrokeDashArray="4 2"
|
||||
X1="{Binding EndSegmentEndPosition}"
|
||||
X2="{Binding EndSegmentEndPosition}"
|
||||
Y1="0"
|
||||
Y2="{Binding ActualHeight, ElementName=TimelineContainerGrid}"
|
||||
Visibility="{Binding EndSegmentEnabled, 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>
|
||||
@@ -0,0 +1,378 @@
|
||||
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.Models.Profile;
|
||||
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Abstract;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Artemis.UI.Shared.Utilities;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
|
||||
{
|
||||
public class TimelineViewModel : PropertyChangedBase, IViewAware, IDisposable
|
||||
{
|
||||
private readonly LayerPropertiesViewModel _layerPropertiesViewModel;
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private RectangleGeometry _selectionRectangle;
|
||||
|
||||
public TimelineViewModel(LayerPropertiesViewModel layerPropertiesViewModel, BindableCollection<LayerPropertyGroupViewModel> layerPropertyGroups, IProfileEditorService profileEditorService)
|
||||
{
|
||||
_layerPropertiesViewModel = layerPropertiesViewModel;
|
||||
_profileEditorService = profileEditorService;
|
||||
|
||||
LayerPropertyGroups = layerPropertyGroups;
|
||||
SelectionRectangle = new RectangleGeometry();
|
||||
SelectedProfileElement = layerPropertiesViewModel.SelectedProfileElement;
|
||||
|
||||
SelectedProfileElement.PropertyChanged += SelectedProfileElementOnPropertyChanged;
|
||||
_profileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged;
|
||||
|
||||
Update();
|
||||
}
|
||||
|
||||
public RenderProfileElement SelectedProfileElement { get; set; }
|
||||
|
||||
public BindableCollection<LayerPropertyGroupViewModel> LayerPropertyGroups { get; }
|
||||
|
||||
public RectangleGeometry SelectionRectangle
|
||||
{
|
||||
get => _selectionRectangle;
|
||||
set => SetAndNotify(ref _selectionRectangle, value);
|
||||
}
|
||||
|
||||
public double StartSegmentWidth => _profileEditorService.PixelsPerSecond * SelectedProfileElement.StartSegmentLength.TotalSeconds;
|
||||
public double StartSegmentEndPosition => StartSegmentWidth;
|
||||
public double MainSegmentWidth => _profileEditorService.PixelsPerSecond * SelectedProfileElement.MainSegmentLength.TotalSeconds;
|
||||
public double MainSegmentEndPosition => StartSegmentWidth + MainSegmentWidth;
|
||||
public double EndSegmentWidth => _profileEditorService.PixelsPerSecond * SelectedProfileElement.EndSegmentLength.TotalSeconds;
|
||||
public double EndSegmentEndPosition => StartSegmentWidth + MainSegmentWidth + EndSegmentWidth;
|
||||
public double TotalTimelineWidth => _profileEditorService.PixelsPerSecond * SelectedProfileElement.TimelineLength.TotalSeconds;
|
||||
|
||||
public bool StartSegmentEnabled => SelectedProfileElement.StartSegmentLength != TimeSpan.Zero;
|
||||
public bool EndSegmentEnabled => SelectedProfileElement.EndSegmentLength != TimeSpan.Zero;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_profileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged;
|
||||
SelectedProfileElement.PropertyChanged -= SelectedProfileElementOnPropertyChanged;
|
||||
}
|
||||
|
||||
private void SelectedProfileElementOnPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(_profileEditorService.SelectedProfileElement.StartSegmentLength))
|
||||
{
|
||||
NotifyOfPropertyChange(nameof(StartSegmentWidth));
|
||||
NotifyOfPropertyChange(nameof(StartSegmentEndPosition));
|
||||
NotifyOfPropertyChange(nameof(MainSegmentEndPosition));
|
||||
NotifyOfPropertyChange(nameof(EndSegmentEndPosition));
|
||||
NotifyOfPropertyChange(nameof(StartSegmentEnabled));
|
||||
NotifyOfPropertyChange(nameof(TotalTimelineWidth));
|
||||
}
|
||||
else if (e.PropertyName == nameof(_profileEditorService.SelectedProfileElement.MainSegmentLength))
|
||||
{
|
||||
NotifyOfPropertyChange(nameof(MainSegmentWidth));
|
||||
NotifyOfPropertyChange(nameof(MainSegmentEndPosition));
|
||||
NotifyOfPropertyChange(nameof(EndSegmentEndPosition));
|
||||
NotifyOfPropertyChange(nameof(TotalTimelineWidth));
|
||||
}
|
||||
else if (e.PropertyName == nameof(_profileEditorService.SelectedProfileElement.EndSegmentLength))
|
||||
{
|
||||
NotifyOfPropertyChange(nameof(EndSegmentWidth));
|
||||
NotifyOfPropertyChange(nameof(EndSegmentEndPosition));
|
||||
NotifyOfPropertyChange(nameof(EndSegmentEnabled));
|
||||
NotifyOfPropertyChange(nameof(TotalTimelineWidth));
|
||||
}
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
foreach (var layerPropertyGroupViewModel in LayerPropertyGroups)
|
||||
{
|
||||
layerPropertyGroupViewModel.TimelinePropertyGroupViewModel.UpdateKeyframes();
|
||||
|
||||
foreach (var layerPropertyBaseViewModel in layerPropertyGroupViewModel.GetAllChildren())
|
||||
{
|
||||
if (layerPropertyBaseViewModel is LayerPropertyViewModel layerPropertyViewModel)
|
||||
layerPropertyViewModel.TimelinePropertyBaseViewModel.UpdateKeyframes();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ProfileEditorServiceOnPixelsPerSecondChanged(object sender, EventArgs e)
|
||||
{
|
||||
NotifyOfPropertyChange(nameof(StartSegmentWidth));
|
||||
NotifyOfPropertyChange(nameof(StartSegmentEndPosition));
|
||||
NotifyOfPropertyChange(nameof(MainSegmentWidth));
|
||||
NotifyOfPropertyChange(nameof(MainSegmentEndPosition));
|
||||
NotifyOfPropertyChange(nameof(EndSegmentWidth));
|
||||
NotifyOfPropertyChange(nameof(EndSegmentEndPosition));
|
||||
NotifyOfPropertyChange(nameof(TotalTimelineWidth));
|
||||
}
|
||||
|
||||
#region Command handlers
|
||||
|
||||
public void KeyframeMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
// if (e.LeftButton == MouseButtonState.Released)
|
||||
// return;
|
||||
|
||||
var viewModel = (sender as Ellipse)?.DataContext as TimelineKeyframeViewModel;
|
||||
if (viewModel == null)
|
||||
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.UpdateSelectedProfileElement();
|
||||
ReleaseSelectedKeyframes();
|
||||
|
||||
((IInputElement) sender).ReleaseMouseCapture();
|
||||
}
|
||||
|
||||
public void KeyframeMouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
var viewModel = (sender as Ellipse)?.DataContext as TimelineKeyframeViewModel;
|
||||
if (viewModel == null)
|
||||
return;
|
||||
|
||||
if (e.LeftButton == MouseButtonState.Pressed)
|
||||
MoveSelectedKeyframes(GetCursorTime(e.GetPosition(View)), viewModel);
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
#region Context menu actions
|
||||
|
||||
public void ContextMenuOpening(object sender, ContextMenuEventArgs e)
|
||||
{
|
||||
var viewModel = (sender as Ellipse)?.DataContext as TimelineKeyframeViewModel;
|
||||
viewModel?.CreateEasingViewModels();
|
||||
}
|
||||
|
||||
public void ContextMenuClosing(object sender, ContextMenuEventArgs e)
|
||||
{
|
||||
var viewModel = (sender as Ellipse)?.DataContext as TimelineKeyframeViewModel;
|
||||
viewModel?.EasingViewModels.Clear();
|
||||
}
|
||||
|
||||
public void Copy(TimelineKeyframeViewModel viewModel)
|
||||
{
|
||||
viewModel.Copy();
|
||||
}
|
||||
|
||||
public void Delete(TimelineKeyframeViewModel viewModel)
|
||||
{
|
||||
viewModel.Delete();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private TimeSpan GetCursorTime(Point position)
|
||||
{
|
||||
// Get the parent grid, need that for our position
|
||||
var x = Math.Max(0, position.X);
|
||||
var 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))
|
||||
{
|
||||
var tolerance = 1000f / _profileEditorService.PixelsPerSecond * 5;
|
||||
if (Math.Abs(_profileEditorService.CurrentTime.TotalMilliseconds - time.TotalMilliseconds) < tolerance)
|
||||
time = _profileEditorService.CurrentTime;
|
||||
else if (Math.Abs(_profileEditorService.SelectedProfileElement.StartSegmentLength.TotalMilliseconds - time.TotalMilliseconds) < tolerance)
|
||||
time = _profileEditorService.SelectedProfileElement.StartSegmentLength;
|
||||
}
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Keyframe movement
|
||||
|
||||
public void MoveSelectedKeyframes(TimeSpan cursorTime, TimelineKeyframeViewModel sourceKeyframeViewModel)
|
||||
{
|
||||
// Ensure the selection rectangle doesn't show, the view isn't aware of different types of dragging
|
||||
SelectionRectangle.Rect = new Rect();
|
||||
|
||||
var keyframeViewModels = GetAllKeyframeViewModels();
|
||||
foreach (var 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,
|
||||
true,
|
||||
sourceKeyframeViewModel.BaseLayerPropertyKeyframe
|
||||
);
|
||||
}
|
||||
|
||||
sourceKeyframeViewModel.UpdatePosition(cursorTime);
|
||||
|
||||
foreach (var keyframeViewModel in keyframeViewModels.Where(k => k.IsSelected))
|
||||
keyframeViewModel.ApplyOffsetToKeyframe(sourceKeyframeViewModel);
|
||||
|
||||
_layerPropertiesViewModel.ProfileEditorService.UpdateProfilePreview();
|
||||
}
|
||||
|
||||
|
||||
public void ReleaseSelectedKeyframes()
|
||||
{
|
||||
var keyframeViewModels = GetAllKeyframeViewModels();
|
||||
foreach (var keyframeViewModel in keyframeViewModels.Where(k => k.IsSelected))
|
||||
keyframeViewModel.ReleaseMovement();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Keyframe selection
|
||||
|
||||
private Point _mouseDragStartPoint;
|
||||
private bool _mouseDragging;
|
||||
|
||||
// ReSharper disable once UnusedMember.Global - Called from view
|
||||
public void TimelineCanvasMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
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;
|
||||
|
||||
var position = e.GetPosition((IInputElement) sender);
|
||||
var selectedRect = new Rect(_mouseDragStartPoint, position);
|
||||
SelectionRectangle.Rect = selectedRect;
|
||||
|
||||
var keyframeViewModels = GetAllKeyframeViewModels();
|
||||
var selectedKeyframes = HitTestUtilities.GetHitViewModels<TimelineKeyframeViewModel>((Visual) sender, SelectionRectangle);
|
||||
foreach (var 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)
|
||||
{
|
||||
var position = e.GetPosition((IInputElement) sender);
|
||||
var selectedRect = new Rect(_mouseDragStartPoint, position);
|
||||
SelectionRectangle.Rect = selectedRect;
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void SelectKeyframe(TimelineKeyframeViewModel clicked, bool selectBetween, bool toggle)
|
||||
{
|
||||
var keyframeViewModels = GetAllKeyframeViewModels();
|
||||
if (selectBetween)
|
||||
{
|
||||
var selectedIndex = keyframeViewModels.FindIndex(k => k.IsSelected);
|
||||
// If nothing is selected, select only the clicked
|
||||
if (selectedIndex == -1)
|
||||
{
|
||||
clicked.IsSelected = true;
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var keyframeViewModel in keyframeViewModels)
|
||||
keyframeViewModel.IsSelected = false;
|
||||
|
||||
var clickedIndex = keyframeViewModels.IndexOf(clicked);
|
||||
if (clickedIndex < selectedIndex)
|
||||
{
|
||||
foreach (var keyframeViewModel in keyframeViewModels.Skip(clickedIndex).Take(selectedIndex - clickedIndex + 1))
|
||||
keyframeViewModel.IsSelected = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var 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 (var keyframeViewModel in keyframeViewModels)
|
||||
keyframeViewModel.IsSelected = false;
|
||||
clicked.IsSelected = true;
|
||||
}
|
||||
}
|
||||
|
||||
private List<TimelineKeyframeViewModel> GetAllKeyframeViewModels()
|
||||
{
|
||||
var viewModels = new List<LayerPropertyBaseViewModel>();
|
||||
foreach (var layerPropertyGroupViewModel in LayerPropertyGroups)
|
||||
viewModels.AddRange(layerPropertyGroupViewModel.GetAllChildren());
|
||||
|
||||
var keyframes = viewModels.Where(vm => vm is LayerPropertyViewModel)
|
||||
.SelectMany(vm => ((LayerPropertyViewModel) vm).TimelinePropertyBaseViewModel.TimelineKeyframeViewModels)
|
||||
.ToList();
|
||||
|
||||
return keyframes;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IViewAware
|
||||
|
||||
public void AttachView(UIElement view)
|
||||
{
|
||||
if (View != null)
|
||||
throw new InvalidOperationException(string.Format("Tried to attach View {0} to ViewModel {1}, but it already has a view attached", view.GetType().Name, GetType().Name));
|
||||
|
||||
View = view;
|
||||
}
|
||||
|
||||
public UIElement View { get; set; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree.TreePropertyGroupView"
|
||||
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"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance local:TreePropertyGroupViewModel}"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
<UserControl.Resources>
|
||||
<converters:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
|
||||
</UserControl.Resources>
|
||||
<StackPanel>
|
||||
<!-- Type: None -->
|
||||
<TextBlock
|
||||
dd:DragDrop.DragSourceIgnore="True"
|
||||
Text="{Binding LayerPropertyGroupViewModel.PropertyGroupDescription.Name}"
|
||||
ToolTip="{Binding LayerPropertyGroupViewModel.PropertyGroupDescription.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 LayerPropertyGroupViewModel.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 LayerPropertyGroupViewModel.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 LayerPropertyGroupViewModel.PropertyGroupDescription.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 LayerPropertyGroupViewModel.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 LayerPropertyGroupViewModel.PropertyGroupDescription.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 LayerPropertyGroupViewModel.GroupType}" Value="LayerBrushRoot">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Grid.Style>
|
||||
<materialDesign:PackIcon Grid.Column="0"
|
||||
Kind="{Binding LayerPropertyGroupViewModel.LayerPropertyGroup.LayerBrush.Descriptor.Icon}"
|
||||
Margin="0 5 5 0" />
|
||||
<TextBlock Grid.Column="1"
|
||||
ToolTip="{Binding LayerPropertyGroupViewModel.LayerPropertyGroup.LayerBrush.Descriptor.Description}"
|
||||
Margin="0 5 0 0">
|
||||
Brush - 
|
||||
</TextBlock>
|
||||
<TextBlock Grid.Column="2"
|
||||
Text="{Binding LayerPropertyGroupViewModel.LayerPropertyGroup.LayerBrush.Descriptor.DisplayName}"
|
||||
ToolTip="{Binding LayerPropertyGroupViewModel.LayerPropertyGroup.LayerBrush.Descriptor.Description}"
|
||||
Margin="0 5 0 0" />
|
||||
|
||||
<Button Grid.Column="3"
|
||||
Style="{StaticResource MaterialDesignIconButton}"
|
||||
ToolTip="Open brush settings"
|
||||
Width="24"
|
||||
Height="24"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Right"
|
||||
Command="{s:Action OpenBrushSettings}"
|
||||
Visibility="{Binding LayerPropertyGroupViewModel.LayerPropertyGroup.LayerBrush.HasConfigurationViewModel, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
|
||||
<materialDesign:PackIcon Kind="Settings" Height="16" Width="16" />
|
||||
</Button>
|
||||
</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 LayerPropertyGroupViewModel.GroupType}" Value="LayerEffectRoot">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Grid.Style>
|
||||
<materialDesign:PackIcon
|
||||
Grid.Column="0"
|
||||
Cursor="SizeNS"
|
||||
Kind="{Binding LayerPropertyGroupViewModel.LayerPropertyGroup.LayerEffect.Descriptor.Icon}"
|
||||
Margin="0 5 5 0"
|
||||
Background="Transparent" />
|
||||
<TextBlock Grid.Column="1" ToolTip="{Binding LayerPropertyGroupViewModel.LayerPropertyGroup.LayerEffect.Descriptor.Description}" Margin="0 5 0 0">
|
||||
Effect
|
||||
</TextBlock>
|
||||
<TextBlock Grid.Column="2"
|
||||
ToolTip="{Binding LayerPropertyGroupViewModel.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 LayerPropertyGroupViewModel.LayerPropertyGroup.LayerEffect.Descriptor.DisplayName}"
|
||||
ToolTip="{Binding LayerPropertyGroupViewModel.LayerPropertyGroup.LayerEffect.Descriptor.Description}"
|
||||
Margin="0 5"
|
||||
Visibility="{Binding LayerPropertyGroupViewModel.LayerPropertyGroup.LayerEffect.Name, Converter={StaticResource NullToVisibilityConverter}, ConverterParameter=Inverted}" />
|
||||
<TextBlock Grid.Column="4"
|
||||
Text="{Binding LayerPropertyGroupViewModel.LayerPropertyGroup.LayerEffect.Name}"
|
||||
ToolTip="{Binding LayerPropertyGroupViewModel.LayerPropertyGroup.LayerEffect.Descriptor.Description}"
|
||||
Margin="0 5"
|
||||
Visibility="{Binding LayerPropertyGroupViewModel.LayerPropertyGroup.LayerEffect.Name, Converter={StaticResource NullToVisibilityConverter}}" />
|
||||
|
||||
<StackPanel Grid.Column="5" Orientation="Horizontal">
|
||||
<ToggleButton
|
||||
Style="{StaticResource MaterialDesignFlatToggleButton}"
|
||||
ToolTip="Toggle enabled state"
|
||||
Width="18"
|
||||
Height="18"
|
||||
IsChecked="{Binding LayerPropertyGroupViewModel.LayerPropertyGroup.LayerEffect.Enabled}"
|
||||
VerticalAlignment="Center" Padding="-25"
|
||||
Margin="5 0"
|
||||
Command="{s:Action EnableToggled}">
|
||||
<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 LayerPropertyGroupViewModel.LayerPropertyGroup.LayerEffect.HasConfigurationViewModel, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
|
||||
<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>
|
||||
</UserControl>
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Artemis.Core.Services.Interfaces;
|
||||
using Artemis.UI.Screens.ProfileEditor.Dialogs;
|
||||
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Abstract;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree
|
||||
{
|
||||
public class TreePropertyGroupViewModel : PropertyChangedBase
|
||||
{
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly IWindowManager _windowManager;
|
||||
private readonly IRenderElementService _renderElementService;
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
|
||||
public TreePropertyGroupViewModel(LayerPropertyBaseViewModel layerPropertyBaseViewModel,
|
||||
IProfileEditorService profileEditorService, IRenderElementService renderElementService, IDialogService dialogService, IWindowManager windowManager)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
_renderElementService = renderElementService;
|
||||
_dialogService = dialogService;
|
||||
_windowManager = windowManager;
|
||||
LayerPropertyGroupViewModel = (LayerPropertyGroupViewModel) layerPropertyBaseViewModel;
|
||||
}
|
||||
|
||||
public LayerPropertyGroupViewModel LayerPropertyGroupViewModel { get; }
|
||||
|
||||
public void OpenBrushSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
var configurationViewModel = LayerPropertyGroupViewModel.LayerPropertyGroup.LayerBrush.GetConfigurationViewModel();
|
||||
if (configurationViewModel != null)
|
||||
_windowManager.ShowDialog(new LayerBrushSettingsWindowViewModel(configurationViewModel));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_dialogService.ShowExceptionDialog("An exception occured while trying to show the brush's settings window", e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void OpenEffectSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
var configurationViewModel = LayerPropertyGroupViewModel.LayerPropertyGroup.LayerEffect.GetConfigurationViewModel();
|
||||
if (configurationViewModel != null)
|
||||
_windowManager.ShowDialog(new LayerEffectSettingsWindowViewModel(configurationViewModel));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_dialogService.ShowExceptionDialog("An exception occured while trying to show the effect's settings window", e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async void RenameEffect()
|
||||
{
|
||||
var result = await _dialogService.ShowDialogAt<RenameViewModel>(
|
||||
"PropertyTreeDialogHost",
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{"subject", "effect"},
|
||||
{"currentName", LayerPropertyGroupViewModel.LayerPropertyGroup.LayerEffect.Name}
|
||||
}
|
||||
);
|
||||
if (result is string newName)
|
||||
{
|
||||
LayerPropertyGroupViewModel.LayerPropertyGroup.LayerEffect.Name = newName;
|
||||
LayerPropertyGroupViewModel.LayerPropertyGroup.LayerEffect.HasBeenRenamed = true;
|
||||
_profileEditorService.UpdateSelectedProfile();
|
||||
}
|
||||
}
|
||||
|
||||
public void DeleteEffect()
|
||||
{
|
||||
_renderElementService.RemoveLayerEffect(LayerPropertyGroupViewModel.LayerPropertyGroup.LayerEffect);
|
||||
_profileEditorService.UpdateSelectedProfile();
|
||||
}
|
||||
|
||||
public void EnableToggled()
|
||||
{
|
||||
_profileEditorService.UpdateSelectedProfile();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<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: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:tree="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance {x:Type tree:TreePropertyViewModel}}">
|
||||
<Grid Height="22" Margin="-20 0 0 0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ToggleButton Grid.Column="0"
|
||||
Style="{StaticResource MaterialDesignFlatToggleButton}"
|
||||
ToolTip="Toggle key-framing"
|
||||
Width="18"
|
||||
Height="18"
|
||||
IsChecked="{Binding KeyframesEnabled}"
|
||||
IsEnabled="{Binding LayerPropertyViewModel.LayerProperty.KeyframesSupported}"
|
||||
VerticalAlignment="Center" Padding="-25">
|
||||
<materialDesign:PackIcon Kind="Stopwatch" Height="13" Width="13" />
|
||||
</ToggleButton>
|
||||
|
||||
<TextBlock Grid.Column="1"
|
||||
Margin="5,0,0,0"
|
||||
Padding="0,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
Text="{Binding LayerPropertyViewModel.LayerProperty.PropertyDescription.Name}"
|
||||
ToolTip="{Binding LayerPropertyViewModel.LayerProperty.PropertyDescription.Description}"
|
||||
HorizontalAlignment="Left" />
|
||||
|
||||
<ContentControl Grid.Column="2" Margin="20 0" s:View.Model="{Binding PropertyInputViewModel}">
|
||||
<ContentControl.Resources>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Margin" Value="0 0 5 4" />
|
||||
<Setter Property="VerticalAlignment" Value="Bottom" />
|
||||
</Style>
|
||||
</ContentControl.Resources>
|
||||
</ContentControl>
|
||||
|
||||
<Button Grid.Column="3"
|
||||
Style="{StaticResource MaterialDesignIconButton}"
|
||||
ToolTip="Change the property's data binding"
|
||||
Width="24"
|
||||
Height="24"
|
||||
VerticalAlignment="Center">
|
||||
<materialDesign:PackIcon Kind="VectorLink" Height="16" Width="16" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Artemis.Core.Models.Profile.LayerProperties;
|
||||
using Artemis.Core.Utilities;
|
||||
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Abstract;
|
||||
using Artemis.UI.Shared.PropertyInput;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree
|
||||
{
|
||||
|
||||
public class TreePropertyViewModel<T> : TreePropertyViewModel
|
||||
{
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private PropertyInputViewModel<T> _propertyInputViewModel;
|
||||
|
||||
public TreePropertyViewModel(LayerPropertyBaseViewModel layerPropertyBaseViewModel, PropertyInputViewModel<T> propertyInputViewModel,
|
||||
IProfileEditorService profileEditorService) : base(layerPropertyBaseViewModel)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
LayerPropertyViewModel = (LayerPropertyViewModel<T>) layerPropertyBaseViewModel;
|
||||
PropertyInputViewModel = propertyInputViewModel;
|
||||
}
|
||||
|
||||
public LayerPropertyViewModel<T> LayerPropertyViewModel { get; }
|
||||
|
||||
public PropertyInputViewModel<T> PropertyInputViewModel
|
||||
{
|
||||
get => _propertyInputViewModel;
|
||||
set => SetAndNotify(ref _propertyInputViewModel, value);
|
||||
}
|
||||
|
||||
public bool KeyframesEnabled
|
||||
{
|
||||
get => LayerPropertyViewModel.LayerProperty.KeyframesEnabled;
|
||||
set => ApplyKeyframesEnabled(value);
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
PropertyInputViewModel.Dispose();
|
||||
}
|
||||
|
||||
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 && !LayerPropertyViewModel.LayerProperty.Keyframes.Any())
|
||||
{
|
||||
LayerPropertyViewModel.LayerProperty.AddKeyframe(new LayerPropertyKeyframe<T>(
|
||||
LayerPropertyViewModel.LayerProperty.CurrentValue,
|
||||
_profileEditorService.CurrentTime,
|
||||
Easings.Functions.Linear,
|
||||
LayerPropertyViewModel.LayerProperty
|
||||
));
|
||||
}
|
||||
// If disabling keyframes, set the base value to the current value
|
||||
else if (!enable && LayerPropertyViewModel.LayerProperty.Keyframes.Any())
|
||||
LayerPropertyViewModel.LayerProperty.BaseValue = LayerPropertyViewModel.LayerProperty.CurrentValue;
|
||||
|
||||
LayerPropertyViewModel.LayerProperty.KeyframesEnabled = enable;
|
||||
|
||||
_profileEditorService.UpdateSelectedProfileElement();
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class TreePropertyViewModel : PropertyChangedBase, IDisposable
|
||||
{
|
||||
protected TreePropertyViewModel(LayerPropertyBaseViewModel layerPropertyBaseViewModel)
|
||||
{
|
||||
LayerPropertyBaseViewModel = layerPropertyBaseViewModel;
|
||||
}
|
||||
|
||||
public LayerPropertyBaseViewModel LayerPropertyBaseViewModel { get; }
|
||||
public abstract void Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<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:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
xmlns:converters="clr-namespace:Artemis.UI.Converters"
|
||||
xmlns:abstract="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.Abstract"
|
||||
xmlns:layerProperties="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties"
|
||||
xmlns:dd="urn:gong-wpf-dragdrop"
|
||||
xmlns:tree="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree"
|
||||
xmlns:layerProperties1="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance {x:Type tree:TreeViewModel}}">
|
||||
<UserControl.Resources>
|
||||
<Style x:Key="PropertyTreeStyle" TargetType="{x:Type TreeViewItem}">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="{x:Null}" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource MaterialDesignBody}" />
|
||||
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />
|
||||
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
|
||||
<Setter Property="ScrollViewer.PanningMode" Value="Both" />
|
||||
<Setter Property="Stylus.IsFlicksEnabled" Value="False" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type TreeViewItem}">
|
||||
<ControlTemplate.Resources>
|
||||
<converters:LeftMarginMultiplierConverter Length="19" x:Key="lengthConverter" />
|
||||
</ControlTemplate.Resources>
|
||||
<StackPanel>
|
||||
<Border Name="Bd"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{DynamicResource MaterialDesignDivider}"
|
||||
BorderThickness="0,0,0,1"
|
||||
Height="25"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<Grid Margin="{Binding Converter={StaticResource lengthConverter}, RelativeSource={RelativeSource TemplatedParent}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="19" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ToggleButton x:Name="Expander"
|
||||
Foreground="{DynamicResource MaterialDesignBody}"
|
||||
Style="{StaticResource MaterialDesignExpandCollapseToggleStyle}"
|
||||
IsChecked="{Binding Path=IsExpanded, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
ClickMode="Press" />
|
||||
|
||||
<ContentPresenter x:Name="PART_Header"
|
||||
Grid.Column="1"
|
||||
ContentSource="Header"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
<ItemsPresenter x:Name="ItemsHost" />
|
||||
</StackPanel>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsExpanded" Value="false">
|
||||
<Setter TargetName="ItemsHost" Property="Visibility" Value="Collapsed" />
|
||||
</Trigger>
|
||||
<Trigger Property="HasItems" Value="false">
|
||||
<Setter TargetName="Expander" Property="Visibility" Value="Hidden" />
|
||||
</Trigger>
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="HasHeader" Value="false" />
|
||||
<Condition Property="Width" Value="Auto" />
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter TargetName="PART_Header" Property="MinWidth" Value="75" />
|
||||
</MultiTrigger>
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="HasHeader" Value="false" />
|
||||
<Condition Property="Height" Value="Auto" />
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter TargetName="PART_Header" Property="MinHeight" Value="19" />
|
||||
</MultiTrigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
<TreeView ItemsSource="{Binding LayerPropertyGroups}"
|
||||
VirtualizingStackPanel.IsVirtualizing="True"
|
||||
VirtualizingStackPanel.VirtualizationMode="Recycling"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Background="{DynamicResource MaterialDesignToolBarBackground}"
|
||||
PreviewMouseWheel="{s:Action PropertyTreePreviewMouseWheel}"
|
||||
dd:DragDrop.IsDragSource="True"
|
||||
dd:DragDrop.IsDropTarget="True"
|
||||
dd:DragDrop.DropHandler="{Binding LayerPropertiesViewModel}"
|
||||
Margin="0 -1">
|
||||
<TreeView.ItemContainerStyle>
|
||||
<Style TargetType="TreeViewItem" BasedOn="{StaticResource PropertyTreeStyle}">
|
||||
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
|
||||
<Setter Property="Visibility" Value="{Binding IsVisible, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}" />
|
||||
</Style>
|
||||
</TreeView.ItemContainerStyle>
|
||||
<TreeView.Resources>
|
||||
<HierarchicalDataTemplate DataType="{x:Type layerProperties1:LayerPropertyGroupViewModel}" ItemsSource="{Binding Children}">
|
||||
<ContentControl s:View.Model="{Binding TreePropertyGroupViewModel}" />
|
||||
</HierarchicalDataTemplate>
|
||||
<DataTemplate DataType="{x:Type layerProperties1:LayerPropertyViewModel}">
|
||||
<ContentControl s:View.Model="{Binding TreePropertyBaseViewModel}" dd:DragDrop.DragSourceIgnore="True" />
|
||||
</DataTemplate>
|
||||
</TreeView.Resources>
|
||||
</TreeView>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree
|
||||
{
|
||||
public class TreeViewModel : PropertyChangedBase
|
||||
{
|
||||
|
||||
public TreeViewModel(LayerPropertiesViewModel layerPropertiesViewModel, BindableCollection<LayerPropertyGroupViewModel> layerPropertyGroups)
|
||||
{
|
||||
LayerPropertiesViewModel = layerPropertiesViewModel;
|
||||
LayerPropertyGroups = layerPropertyGroups;
|
||||
}
|
||||
|
||||
public LayerPropertiesViewModel LayerPropertiesViewModel { get; }
|
||||
public BindableCollection<LayerPropertyGroupViewModel> LayerPropertyGroups { get; }
|
||||
|
||||
public void PropertyTreePreviewMouseWheel(object sender, MouseWheelEventArgs e)
|
||||
{
|
||||
if (e.Handled || !(sender is System.Windows.Controls.TreeView))
|
||||
return;
|
||||
|
||||
e.Handled = true;
|
||||
var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
|
||||
{
|
||||
RoutedEvent = UIElement.MouseWheelEvent,
|
||||
Source = sender
|
||||
};
|
||||
var parent = ((Control) sender).Parent as UIElement;
|
||||
parent?.RaiseEvent(eventArg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor
|
||||
{
|
||||
public class ProfileEditorPanelViewModel : Screen
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.ProfileEditorView"
|
||||
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:profileEditor="clr-namespace:Artemis.UI.Screens.ProfileEditor"
|
||||
xmlns:behaviors="clr-namespace:Artemis.UI.Behaviors"
|
||||
xmlns:profile="clr-namespace:Artemis.Core.Models.Profile;assembly=Artemis.Core"
|
||||
xmlns:layerBrush="clr-namespace:Artemis.Core.Plugins.LayerBrush;assembly=Artemis.Core"
|
||||
xmlns:dataTemplateSelectors="clr-namespace:Artemis.UI.DataTemplateSelectors"
|
||||
mc:Ignorable="d"
|
||||
behaviors:InputBindingBehavior.PropagateInputBindingsToWindow="True"
|
||||
d:DesignHeight="450" d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance Type=profileEditor:ProfileEditorViewModel, IsDesignTimeCreatable=False}">
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary
|
||||
Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.TextBlock.xaml" />
|
||||
<ResourceDictionary
|
||||
Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.PopupBox.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<DataTemplate x:Key="SimpleTemplate">
|
||||
<StackPanel d:DataContext="{d:DesignInstance {x:Type profile:ProfileDescriptor}}" Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding Name}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
<DataTemplate x:Key="ExtendedTemplate">
|
||||
<Grid d:DataContext="{d:DesignInstance {x:Type profile:ProfileDescriptor}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="{Binding Name}" VerticalAlignment="Center" />
|
||||
<Button Grid.Column="1"
|
||||
Style="{StaticResource MaterialDesignFloatingActionMiniDarkButton}"
|
||||
Width="26"
|
||||
Height="26"
|
||||
VerticalAlignment="Top"
|
||||
Command="{s:Action DeleteProfile}"
|
||||
CommandParameter="{Binding}">
|
||||
<materialDesign:PackIcon Kind="TrashCanOutline" Height="14" Width="14" HorizontalAlignment="Right" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
<UserControl.InputBindings>
|
||||
<KeyBinding Command="{s:Action Undo}" Modifiers="Control" Key="Z" />
|
||||
<KeyBinding Command="{s:Action Redo}" Modifiers="Control" Key="Y" />
|
||||
</UserControl.InputBindings>
|
||||
|
||||
<Grid Margin="16">
|
||||
<Grid.ColumnDefinitions>
|
||||
<!-- Left side -->
|
||||
<ColumnDefinition Width="*" MinWidth="100" />
|
||||
<!-- Side panels resize -->
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<!-- Side panels -->
|
||||
<ColumnDefinition Width="{Binding SidePanelsWidth.Value, Mode=TwoWay}" MinWidth="100" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Left side -->
|
||||
<Grid Grid.Row="0" Grid.Column="0">
|
||||
<Grid.RowDefinitions>
|
||||
<!-- Introduction -->
|
||||
<RowDefinition Height="35" />
|
||||
<!-- Design area -->
|
||||
<RowDefinition Height="*" MinHeight="200" />
|
||||
<!-- Bottom panels resize -->
|
||||
<RowDefinition Height="Auto" />
|
||||
<!-- Bottom panels -->
|
||||
<RowDefinition Height="{Binding BottomPanelsHeight.Value, Mode=TwoWay}" MinHeight="108" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Introduction -->
|
||||
<StackPanel Grid.Row="0">
|
||||
<TextBlock>
|
||||
<materialDesign:PackIcon Kind="AboutOutline" /> The profile defines what colors the LEDs will have. Multiple groups of LEDs are defined into layers. On these layers you can apply effects.
|
||||
</TextBlock>
|
||||
<Separator Style="{StaticResource MaterialDesignDarkSeparator}" Margin="0 -2.5 0 0" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Design area -->
|
||||
<materialDesign:Card Grid.Row="1" materialDesign:ShadowAssist.ShadowDepth="Depth1" VerticalAlignment="Stretch">
|
||||
<ContentControl s:View.Model="{Binding ProfileViewModel}" />
|
||||
</materialDesign:Card>
|
||||
|
||||
<!-- Bottom panels resize -->
|
||||
<GridSplitter Grid.Row="2" Grid.Column="0" Height="5" HorizontalAlignment="Stretch" Cursor="SizeNS" Margin="0 5" />
|
||||
|
||||
<!-- Bottom panels -->
|
||||
<Grid Grid.Row="3">
|
||||
<!-- Layer elements -->
|
||||
<materialDesign:Card Grid.Column="0" materialDesign:ShadowAssist.ShadowDepth="Depth1" VerticalAlignment="Stretch">
|
||||
<ContentControl s:View.Model="{Binding LayerPropertiesViewModel}" />
|
||||
</materialDesign:Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<!-- Side panels resize -->
|
||||
<GridSplitter Grid.Row="0" Grid.Column="1" Width="5" HorizontalAlignment="Stretch" Cursor="SizeWE" Margin="5 35 5 0" />
|
||||
|
||||
<!-- Side panels -->
|
||||
<Grid Grid.Row="0" Grid.Column="2">
|
||||
<Grid.RowDefinitions>
|
||||
<!-- Profile selection -->
|
||||
<RowDefinition Height="35" />
|
||||
<!-- Profile elements -->
|
||||
<RowDefinition Height="*" MinHeight="100" />
|
||||
<!-- Conditions resize -->
|
||||
<RowDefinition Height="Auto" />
|
||||
<!-- Display conditions -->
|
||||
<RowDefinition Height="{Binding DisplayConditionsHeight.Value, Mode=TwoWay}" MinHeight="100" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Profile selection -->
|
||||
<Grid Grid.Row="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ComboBox Height="26"
|
||||
VerticalAlignment="Top"
|
||||
ItemsSource="{Binding Profiles}"
|
||||
SelectedItem="{Binding SelectedProfile}"
|
||||
ItemTemplateSelector="{dataTemplateSelectors:ComboBoxTemplateSelector
|
||||
SelectedItemTemplate={StaticResource SimpleTemplate},
|
||||
DropdownItemsTemplate={StaticResource ExtendedTemplate}}" >
|
||||
<ComboBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<VirtualizingStackPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ComboBox.ItemsPanel>
|
||||
</ComboBox>
|
||||
<Button Style="{StaticResource MaterialDesignFloatingActionMiniButton}"
|
||||
Margin="5 0"
|
||||
Grid.Column="1"
|
||||
Width="26"
|
||||
Height="26"
|
||||
VerticalAlignment="Top"
|
||||
Command="{s:Action AddProfile}">
|
||||
<materialDesign:PackIcon Kind="Add" Height="14" Width="14" />
|
||||
</Button>
|
||||
<Button Style="{StaticResource MaterialDesignFloatingActionMiniDarkButton}"
|
||||
Margin="5 0"
|
||||
Grid.Column="2"
|
||||
Width="26"
|
||||
Height="26"
|
||||
VerticalAlignment="Top"
|
||||
Command="{s:Action DeleteActiveProfile}">
|
||||
<materialDesign:PackIcon Kind="TrashCanOutline" Height="14" Width="14" />
|
||||
</Button>
|
||||
<Button Style="{StaticResource MaterialDesignFloatingActionMiniButton}"
|
||||
Margin="5 0"
|
||||
Grid.Column="3"
|
||||
Width="26"
|
||||
Height="26"
|
||||
VerticalAlignment="Top"
|
||||
Command="{s:Action ExportActiveProfile}">
|
||||
<materialDesign:PackIcon Kind="Export" Height="14" Width="14" />
|
||||
</Button>
|
||||
<Button Style="{StaticResource MaterialDesignFloatingActionMiniButton}"
|
||||
Margin="5 0 0 0"
|
||||
Grid.Column="4"
|
||||
Width="26"
|
||||
Height="26"
|
||||
VerticalAlignment="Top"
|
||||
Command="{s:Action ImportProfile}">
|
||||
<materialDesign:PackIcon Kind="Import" Height="14" Width="14" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- Profile elements -->
|
||||
<materialDesign:Card Grid.Row="1" materialDesign:ShadowAssist.ShadowDepth="Depth1" VerticalAlignment="Stretch">
|
||||
<ContentControl s:View.Model="{Binding ProfileTreeViewModel}" Margin="0,-1,-0.2,1" />
|
||||
</materialDesign:Card>
|
||||
|
||||
<!-- Conditions resize -->
|
||||
<GridSplitter Grid.Row="2" Height="5" HorizontalAlignment="Stretch" Cursor="SizeNS" Margin="0 5" />
|
||||
|
||||
<!-- Display conditions -->
|
||||
<materialDesign:Card Grid.Row="3" materialDesign:ShadowAssist.ShadowDepth="Depth1" VerticalAlignment="Stretch">
|
||||
<ContentControl s:View.Model="{Binding DisplayConditionsViewModel}" />
|
||||
</materialDesign:Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,318 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using Artemis.Core.Models.Profile;
|
||||
using Artemis.Core.Plugins.Abstract;
|
||||
using Artemis.Core.Plugins.Models;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.Core.Services.Storage.Interfaces;
|
||||
using Artemis.UI.Screens.ProfileEditor.Dialogs;
|
||||
using Artemis.UI.Screens.ProfileEditor.DisplayConditions;
|
||||
using Artemis.UI.Screens.ProfileEditor.LayerProperties;
|
||||
using Artemis.UI.Screens.ProfileEditor.ProfileTree;
|
||||
using Artemis.UI.Screens.ProfileEditor.Visualization;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor
|
||||
{
|
||||
public class ProfileEditorViewModel : Conductor<ProfileEditorPanelViewModel>.Collection.AllActive
|
||||
{
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private readonly IProfileService _profileService;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ISnackbarMessageQueue _snackbarMessageQueue;
|
||||
private BindableCollection<ProfileDescriptor> _profiles;
|
||||
private PluginSetting<GridLength> _sidePanelsWidth;
|
||||
private PluginSetting<GridLength> _displayConditionsHeight;
|
||||
private PluginSetting<GridLength> _bottomPanelsHeight;
|
||||
private PluginSetting<GridLength> _elementPropertiesWidth;
|
||||
private ProfileViewModel _profileViewModel;
|
||||
private ProfileTreeViewModel _profileTreeViewModel;
|
||||
private LayerPropertiesViewModel _layerPropertiesViewModel;
|
||||
private DisplayConditionsViewModel _displayConditionsViewModel;
|
||||
private ProfileDescriptor _selectedProfile;
|
||||
|
||||
public ProfileEditorViewModel(ProfileModule module,
|
||||
ICollection<ProfileEditorPanelViewModel> viewModels,
|
||||
IProfileEditorService profileEditorService,
|
||||
IProfileService profileService,
|
||||
IDialogService dialogService,
|
||||
ISettingsService settingsService,
|
||||
ISnackbarMessageQueue snackbarMessageQueue)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
_profileService = profileService;
|
||||
_settingsService = settingsService;
|
||||
_snackbarMessageQueue = snackbarMessageQueue;
|
||||
|
||||
DisplayName = "Profile editor";
|
||||
Module = module;
|
||||
DialogService = dialogService;
|
||||
|
||||
Profiles = new BindableCollection<ProfileDescriptor>();
|
||||
|
||||
// Run this first to let VMs activate without causing constant UI updates
|
||||
Items.AddRange(viewModels);
|
||||
|
||||
// Populate the panels
|
||||
ProfileViewModel = (ProfileViewModel) viewModels.First(vm => vm is ProfileViewModel);
|
||||
ProfileTreeViewModel = (ProfileTreeViewModel) viewModels.First(vm => vm is ProfileTreeViewModel);
|
||||
DisplayConditionsViewModel = (DisplayConditionsViewModel) viewModels.First(vm => vm is DisplayConditionsViewModel);
|
||||
LayerPropertiesViewModel = (LayerPropertiesViewModel) viewModels.First(vm => vm is LayerPropertiesViewModel);
|
||||
}
|
||||
|
||||
public ProfileModule Module { get; }
|
||||
public IDialogService DialogService { get; }
|
||||
|
||||
public DisplayConditionsViewModel DisplayConditionsViewModel
|
||||
{
|
||||
get => _displayConditionsViewModel;
|
||||
set => SetAndNotify(ref _displayConditionsViewModel, value);
|
||||
}
|
||||
|
||||
public LayerPropertiesViewModel LayerPropertiesViewModel
|
||||
{
|
||||
get => _layerPropertiesViewModel;
|
||||
set => SetAndNotify(ref _layerPropertiesViewModel, value);
|
||||
}
|
||||
|
||||
public ProfileTreeViewModel ProfileTreeViewModel
|
||||
{
|
||||
get => _profileTreeViewModel;
|
||||
set => SetAndNotify(ref _profileTreeViewModel, value);
|
||||
}
|
||||
|
||||
public ProfileViewModel ProfileViewModel
|
||||
{
|
||||
get => _profileViewModel;
|
||||
set => SetAndNotify(ref _profileViewModel, value);
|
||||
}
|
||||
|
||||
public BindableCollection<ProfileDescriptor> Profiles
|
||||
{
|
||||
get => _profiles;
|
||||
set => SetAndNotify(ref _profiles, value);
|
||||
}
|
||||
|
||||
public PluginSetting<GridLength> SidePanelsWidth
|
||||
{
|
||||
get => _sidePanelsWidth;
|
||||
set => SetAndNotify(ref _sidePanelsWidth, value);
|
||||
}
|
||||
|
||||
public PluginSetting<GridLength> DisplayConditionsHeight
|
||||
{
|
||||
get => _displayConditionsHeight;
|
||||
set => SetAndNotify(ref _displayConditionsHeight, value);
|
||||
}
|
||||
|
||||
public PluginSetting<GridLength> BottomPanelsHeight
|
||||
{
|
||||
get => _bottomPanelsHeight;
|
||||
set => SetAndNotify(ref _bottomPanelsHeight, value);
|
||||
}
|
||||
|
||||
public PluginSetting<GridLength> ElementPropertiesWidth
|
||||
{
|
||||
get => _elementPropertiesWidth;
|
||||
set => SetAndNotify(ref _elementPropertiesWidth, value);
|
||||
}
|
||||
|
||||
public ProfileDescriptor SelectedProfile
|
||||
{
|
||||
get => _selectedProfile;
|
||||
set
|
||||
{
|
||||
if (!SetAndNotify(ref _selectedProfile, value)) return;
|
||||
NotifyOfPropertyChange(nameof(CanDeleteActiveProfile));
|
||||
ActivateSelectedProfile();
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanDeleteActiveProfile => SelectedProfile != null && Profiles.Count > 1;
|
||||
|
||||
public ProfileDescriptor CreateProfile(string name)
|
||||
{
|
||||
var profile = _profileService.CreateProfileDescriptor(Module, name);
|
||||
Profiles.Add(profile);
|
||||
return profile;
|
||||
}
|
||||
|
||||
public async Task AddProfile()
|
||||
{
|
||||
var result = await DialogService.ShowDialog<ProfileCreateViewModel>();
|
||||
if (result is string name)
|
||||
{
|
||||
var newProfile = CreateProfile(name);
|
||||
SelectedProfile = newProfile;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteProfile(ProfileDescriptor profileDescriptor)
|
||||
{
|
||||
var result = await DialogService.ShowConfirmDialog(
|
||||
"Delete profile",
|
||||
$"Are you sure you want to delete '{profileDescriptor.Name}'? This cannot be undone."
|
||||
);
|
||||
|
||||
if (result)
|
||||
RemoveProfile(profileDescriptor);
|
||||
}
|
||||
|
||||
public async Task DeleteActiveProfile()
|
||||
{
|
||||
var result = await DialogService.ShowConfirmDialog(
|
||||
"Delete active profile",
|
||||
"Are you sure you want to delete your currently active profile? This cannot be undone."
|
||||
);
|
||||
|
||||
if (result)
|
||||
RemoveProfile(SelectedProfile);
|
||||
}
|
||||
|
||||
public async Task ExportActiveProfile()
|
||||
{
|
||||
await DialogService.ShowDialog<ProfileExportViewModel>(new Dictionary<string, object>
|
||||
{
|
||||
{"profileDescriptor", SelectedProfile}
|
||||
});
|
||||
}
|
||||
|
||||
public async Task ImportProfile()
|
||||
{
|
||||
await DialogService.ShowDialog<ProfileImportViewModel>(new Dictionary<string, object>
|
||||
{
|
||||
{"profileModule", Module}
|
||||
});
|
||||
}
|
||||
|
||||
public void Undo()
|
||||
{
|
||||
// Expanded status is also undone because undoing works a bit crude, that's annoying
|
||||
var beforeGroups = LayerPropertiesViewModel.GetAllLayerPropertyGroupViewModels();
|
||||
var expandedPaths = beforeGroups.Where(g => g.IsExpanded).Select(g => g.LayerPropertyGroup.Path).ToList();
|
||||
|
||||
if (!_profileEditorService.UndoUpdateProfile())
|
||||
{
|
||||
_snackbarMessageQueue.Enqueue("Nothing to undo");
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore the expanded status
|
||||
foreach (var allLayerPropertyGroupViewModel in LayerPropertiesViewModel.GetAllLayerPropertyGroupViewModels())
|
||||
allLayerPropertyGroupViewModel.IsExpanded = expandedPaths.Contains(allLayerPropertyGroupViewModel.LayerPropertyGroup.Path);
|
||||
|
||||
_snackbarMessageQueue.Enqueue("Undid profile update", "REDO", Redo);
|
||||
}
|
||||
|
||||
public void Redo()
|
||||
{
|
||||
// Expanded status is also undone because undoing works a bit crude, that's annoying
|
||||
var beforeGroups = LayerPropertiesViewModel.GetAllLayerPropertyGroupViewModels();
|
||||
var expandedPaths = beforeGroups.Where(g => g.IsExpanded).Select(g => g.LayerPropertyGroup.Path).ToList();
|
||||
|
||||
if (!_profileEditorService.RedoUpdateProfile())
|
||||
{
|
||||
_snackbarMessageQueue.Enqueue("Nothing to redo");
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore the expanded status
|
||||
foreach (var allLayerPropertyGroupViewModel in LayerPropertiesViewModel.GetAllLayerPropertyGroupViewModels())
|
||||
allLayerPropertyGroupViewModel.IsExpanded = expandedPaths.Contains(allLayerPropertyGroupViewModel.LayerPropertyGroup.Path);
|
||||
|
||||
_snackbarMessageQueue.Enqueue("Redid profile update", "UNDO", Undo);
|
||||
}
|
||||
|
||||
protected override void OnInitialActivate()
|
||||
{
|
||||
LoadWorkspaceSettings();
|
||||
Module.IsProfileUpdatingDisabled = true;
|
||||
Module.ActiveProfileChanged += ModuleOnActiveProfileChanged;
|
||||
Execute.PostToUIThread(LoadProfiles);
|
||||
base.OnInitialActivate();
|
||||
}
|
||||
|
||||
protected override void OnClose()
|
||||
{
|
||||
SaveWorkspaceSettings();
|
||||
Module.IsProfileUpdatingDisabled = false;
|
||||
Module.ActiveProfileChanged -= ModuleOnActiveProfileChanged;
|
||||
base.OnClose();
|
||||
}
|
||||
|
||||
private void RemoveProfile(ProfileDescriptor profileDescriptor)
|
||||
{
|
||||
if (SelectedProfile == profileDescriptor && !CanDeleteActiveProfile)
|
||||
return;
|
||||
|
||||
var index = Profiles.IndexOf(profileDescriptor);
|
||||
|
||||
// Get a new active profile
|
||||
var newActiveProfile = index - 1 > -1 ? Profiles[index - 1] : Profiles[index + 1];
|
||||
|
||||
// Activate the new active profile
|
||||
SelectedProfile = newActiveProfile;
|
||||
|
||||
// Remove the old one
|
||||
Profiles.Remove(profileDescriptor);
|
||||
_profileService.DeleteProfile(profileDescriptor);
|
||||
}
|
||||
|
||||
private void ActivateSelectedProfile()
|
||||
{
|
||||
Execute.PostToUIThread(async () =>
|
||||
{
|
||||
var changeTask = _profileService.ActivateProfileAnimated(SelectedProfile);
|
||||
_profileEditorService.ChangeSelectedProfile(null);
|
||||
var profile = await changeTask;
|
||||
_profileEditorService.ChangeSelectedProfile(profile);
|
||||
});
|
||||
}
|
||||
|
||||
private void ModuleOnActiveProfileChanged(object sender, EventArgs e)
|
||||
{
|
||||
SelectedProfile = Profiles.FirstOrDefault(d => d.Id == Module.ActiveProfile.EntityId);
|
||||
}
|
||||
|
||||
private void LoadWorkspaceSettings()
|
||||
{
|
||||
SidePanelsWidth = _settingsService.GetSetting("ProfileEditor.SidePanelsWidth", new GridLength(385));
|
||||
DisplayConditionsHeight = _settingsService.GetSetting("ProfileEditor.DisplayConditionsHeight", new GridLength(345));
|
||||
BottomPanelsHeight = _settingsService.GetSetting("ProfileEditor.BottomPanelsHeight", new GridLength(265));
|
||||
ElementPropertiesWidth = _settingsService.GetSetting("ProfileEditor.ElementPropertiesWidth", new GridLength(545));
|
||||
}
|
||||
|
||||
private void SaveWorkspaceSettings()
|
||||
{
|
||||
SidePanelsWidth.Save();
|
||||
DisplayConditionsHeight.Save();
|
||||
BottomPanelsHeight.Save();
|
||||
ElementPropertiesWidth.Save();
|
||||
}
|
||||
|
||||
private void LoadProfiles()
|
||||
{
|
||||
// Get all profiles from the database
|
||||
Profiles.Clear();
|
||||
Profiles.AddRange(_profileService.GetProfileDescriptors(Module).OrderBy(d => d.Name));
|
||||
|
||||
// Populate the selected profile
|
||||
var lastActiveProfile = Profiles.FirstOrDefault(p => p.IsLastActiveProfile) ?? Profiles.FirstOrDefault();
|
||||
if (lastActiveProfile != null)
|
||||
{
|
||||
SelectedProfile = lastActiveProfile;
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a default profile if there is none
|
||||
var defaultProfile = _profileService.CreateProfileDescriptor(Module, "Default");
|
||||
Profiles.Add(defaultProfile);
|
||||
SelectedProfile = defaultProfile;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.ProfileTree.ProfileTreeView"
|
||||
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:dd="urn:gong-wpf-dragdrop"
|
||||
xmlns:profileTree="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileTree"
|
||||
xmlns:treeItem="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem"
|
||||
xmlns:behaviors="clr-namespace:Artemis.UI.Behaviors"
|
||||
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
|
||||
xmlns:profileTree1="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileTree"
|
||||
xmlns:treeItem1="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance {x:Type profileTree1:ProfileTreeViewModel}}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource MaterialDesignSubtitle1TextBlock}" Margin="10 5 0 -4">
|
||||
Profile elements
|
||||
</TextBlock>
|
||||
<Separator Style="{StaticResource MaterialDesignDarkSeparator}" Margin="8 0" />
|
||||
</StackPanel>
|
||||
|
||||
<TreeView Grid.Row="1"
|
||||
ItemsSource="{Binding RootFolder.Children}"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
dd:DragDrop.IsDragSource="True"
|
||||
dd:DragDrop.IsDropTarget="True"
|
||||
dd:DragDrop.DropHandler="{Binding}">
|
||||
<b:Interaction.Behaviors>
|
||||
<behaviors:TreeViewSelectionBehavior ExpandSelected="True" SelectedItem="{Binding SelectedTreeItem}" />
|
||||
</b:Interaction.Behaviors>
|
||||
<TreeView.Resources>
|
||||
<HierarchicalDataTemplate DataType="{x:Type treeItem1:FolderViewModel}" ItemsSource="{Binding Children}">
|
||||
<ContentControl s:View.Model="{Binding}" />
|
||||
</HierarchicalDataTemplate>
|
||||
<!-- TODO: Ensure this item source is required -->
|
||||
<HierarchicalDataTemplate DataType="{x:Type treeItem1:LayerViewModel}" ItemsSource="{Binding Children}">
|
||||
<ContentControl s:View.Model="{Binding}" />
|
||||
</HierarchicalDataTemplate>
|
||||
</TreeView.Resources>
|
||||
</TreeView>
|
||||
|
||||
<StackPanel HorizontalAlignment="Right" Grid.Row="2" Orientation="Horizontal" Margin="8">
|
||||
<Button Style="{StaticResource MaterialDesignIconForegroundButton}"
|
||||
Padding="2 0 2 0"
|
||||
Width="30"
|
||||
Height="30"
|
||||
materialDesign:RippleAssist.IsCentered="True"
|
||||
ToolTip="Add new folder to root"
|
||||
Command="{s:Action AddFolder}">
|
||||
<materialDesign:PackIcon Kind="CreateNewFolder" Width="15" />
|
||||
</Button>
|
||||
<Button Style="{StaticResource MaterialDesignIconForegroundButton}"
|
||||
Width="30"
|
||||
Height="30"
|
||||
Padding="2 0 2 0"
|
||||
materialDesign:RippleAssist.IsCentered="True"
|
||||
ToolTip="Add new layer to root"
|
||||
Command="{s:Action AddLayer}">
|
||||
<materialDesign:PackIcon Kind="LayersPlus" Width="15" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,211 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using Artemis.Core.Models.Profile;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem;
|
||||
using Artemis.UI.Shared.Events;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using GongSolutions.Wpf.DragDrop;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree
|
||||
{
|
||||
public class ProfileTreeViewModel : ProfileEditorPanelViewModel, IDropTarget
|
||||
{
|
||||
private readonly IFolderVmFactory _folderVmFactory;
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private TreeItemViewModel _selectedTreeItem;
|
||||
private bool _updatingTree;
|
||||
private FolderViewModel _rootFolder;
|
||||
|
||||
public ProfileTreeViewModel(IProfileEditorService profileEditorService, IFolderVmFactory folderVmFactory)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
_folderVmFactory = folderVmFactory;
|
||||
}
|
||||
|
||||
public FolderViewModel RootFolder
|
||||
{
|
||||
get => _rootFolder;
|
||||
set => SetAndNotify(ref _rootFolder, value);
|
||||
}
|
||||
|
||||
public TreeItemViewModel SelectedTreeItem
|
||||
{
|
||||
get => _selectedTreeItem;
|
||||
set
|
||||
{
|
||||
if (_updatingTree) return;
|
||||
if (!SetAndNotify(ref _selectedTreeItem, value)) return;
|
||||
|
||||
if (value != null && value.ProfileElement is RenderProfileElement renderElement)
|
||||
_profileEditorService.ChangeSelectedProfileElement(renderElement);
|
||||
else
|
||||
_profileEditorService.ChangeSelectedProfileElement(null);
|
||||
}
|
||||
}
|
||||
|
||||
public void DragOver(IDropInfo dropInfo)
|
||||
{
|
||||
var dragDropType = GetDragDropType(dropInfo);
|
||||
|
||||
switch (dragDropType)
|
||||
{
|
||||
case DragDropType.Add:
|
||||
dropInfo.DropTargetAdorner = DropTargetAdorners.Highlight;
|
||||
dropInfo.Effects = DragDropEffects.Move;
|
||||
break;
|
||||
case DragDropType.InsertBefore:
|
||||
case DragDropType.InsertAfter:
|
||||
dropInfo.DropTargetAdorner = DropTargetAdorners.Insert;
|
||||
dropInfo.Effects = DragDropEffects.Move;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void Drop(IDropInfo dropInfo)
|
||||
{
|
||||
var source = (TreeItemViewModel) dropInfo.Data;
|
||||
var target = (TreeItemViewModel) dropInfo.TargetItem;
|
||||
|
||||
var dragDropType = GetDragDropType(dropInfo);
|
||||
switch (dragDropType)
|
||||
{
|
||||
case DragDropType.Add:
|
||||
source.Parent.RemoveExistingElement(source);
|
||||
target.AddExistingElement(source);
|
||||
break;
|
||||
case DragDropType.InsertBefore:
|
||||
target.SetElementInFront(source);
|
||||
break;
|
||||
case DragDropType.InsertAfter:
|
||||
target.SetElementBehind(source);
|
||||
break;
|
||||
}
|
||||
|
||||
_profileEditorService.UpdateSelectedProfile();
|
||||
}
|
||||
|
||||
// ReSharper disable once UnusedMember.Global - Called from view
|
||||
public void AddFolder()
|
||||
{
|
||||
RootFolder?.AddFolder();
|
||||
}
|
||||
|
||||
// ReSharper disable once UnusedMember.Global - Called from view
|
||||
public void AddLayer()
|
||||
{
|
||||
RootFolder?.AddLayer();
|
||||
}
|
||||
|
||||
private void CreateRootFolderViewModel()
|
||||
{
|
||||
_updatingTree = true;
|
||||
var firstChild = _profileEditorService.SelectedProfile?.Children?.FirstOrDefault();
|
||||
if (!(firstChild is Folder folder))
|
||||
{
|
||||
RootFolder = null;
|
||||
return;
|
||||
}
|
||||
|
||||
RootFolder?.Dispose();
|
||||
RootFolder = _folderVmFactory.Create(folder);
|
||||
_updatingTree = false;
|
||||
|
||||
// Auto-select the first layer
|
||||
if (_profileEditorService.SelectedProfile != null && SelectedTreeItem == null)
|
||||
{
|
||||
if (_profileEditorService.SelectedProfile.GetRootFolder().Children.FirstOrDefault() is RenderProfileElement firstElement)
|
||||
Execute.PostToUIThread(() => _profileEditorService.ChangeSelectedProfileElement(firstElement));
|
||||
}
|
||||
}
|
||||
|
||||
private static DragDropType GetDragDropType(IDropInfo dropInfo)
|
||||
{
|
||||
var source = (TreeItemViewModel) dropInfo.Data;
|
||||
var target = (TreeItemViewModel) dropInfo.TargetItem;
|
||||
if (source == target)
|
||||
return DragDropType.None;
|
||||
|
||||
var parent = target;
|
||||
while (parent != null)
|
||||
{
|
||||
if (parent == source)
|
||||
return DragDropType.None;
|
||||
parent = parent.Parent;
|
||||
}
|
||||
|
||||
switch (dropInfo.InsertPosition)
|
||||
{
|
||||
case RelativeInsertPosition.AfterTargetItem | RelativeInsertPosition.TargetItemCenter:
|
||||
case RelativeInsertPosition.BeforeTargetItem | RelativeInsertPosition.TargetItemCenter:
|
||||
return target.SupportsChildren ? DragDropType.Add : DragDropType.None;
|
||||
case RelativeInsertPosition.BeforeTargetItem:
|
||||
return DragDropType.InsertBefore;
|
||||
case RelativeInsertPosition.AfterTargetItem:
|
||||
return DragDropType.InsertAfter;
|
||||
default:
|
||||
return DragDropType.None;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnInitialActivate()
|
||||
{
|
||||
_profileEditorService.ProfileSelected += OnProfileSelected;
|
||||
_profileEditorService.ProfileElementSelected += OnProfileElementSelected;
|
||||
CreateRootFolderViewModel();
|
||||
}
|
||||
|
||||
protected override void OnClose()
|
||||
{
|
||||
_profileEditorService.ProfileSelected -= OnProfileSelected;
|
||||
_profileEditorService.ProfileElementSelected -= OnProfileElementSelected;
|
||||
|
||||
RootFolder?.Dispose();
|
||||
RootFolder = null;
|
||||
base.OnClose();
|
||||
}
|
||||
|
||||
#region Event handlers
|
||||
|
||||
private void OnProfileElementSelected(object sender, RenderProfileElementEventArgs e)
|
||||
{
|
||||
if (e.RenderProfileElement == SelectedTreeItem?.ProfileElement)
|
||||
return;
|
||||
|
||||
if (RootFolder == null)
|
||||
{
|
||||
CreateRootFolderViewModel();
|
||||
return;
|
||||
}
|
||||
|
||||
_updatingTree = true;
|
||||
RootFolder.UpdateProfileElements();
|
||||
_updatingTree = false;
|
||||
if (e.RenderProfileElement == null)
|
||||
SelectedTreeItem = null;
|
||||
else
|
||||
{
|
||||
var match = RootFolder.GetAllChildren().FirstOrDefault(vm => vm.ProfileElement == e.RenderProfileElement);
|
||||
if (match != null)
|
||||
SelectedTreeItem = match;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnProfileSelected(object sender, EventArgs e)
|
||||
{
|
||||
CreateRootFolderViewModel();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public enum DragDropType
|
||||
{
|
||||
None,
|
||||
Add,
|
||||
InsertBefore,
|
||||
InsertAfter
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem.FolderView"
|
||||
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:treeItem="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem"
|
||||
xmlns:treeItem1="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance {x:Type treeItem1:FolderViewModel}}">
|
||||
<!-- Capture clicks on full tree view item -->
|
||||
<StackPanel Margin="-10" Background="Transparent">
|
||||
<StackPanel.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="Rename" Command="{s:Action RenameElement}">
|
||||
<MenuItem.Icon>
|
||||
<materialDesign:PackIcon Kind="RenameBox" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Delete" Command="{s:Action DeleteElement}">
|
||||
<MenuItem.Icon>
|
||||
<materialDesign:PackIcon Kind="TrashCan" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem Header="Add new folder" Command="{s:Action AddFolder}">
|
||||
<MenuItem.Icon>
|
||||
<materialDesign:PackIcon Kind="CreateNewFolder" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Add new layer" Command="{s:Action AddLayer}">
|
||||
<MenuItem.Icon>
|
||||
<materialDesign:PackIcon Kind="LayersPlus" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
</ContextMenu>
|
||||
</StackPanel.ContextMenu>
|
||||
<Grid Margin="10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<materialDesign:PackIcon Grid.Column="0"
|
||||
Kind="Folder"
|
||||
Visibility="{Binding IsExpanded, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TreeViewItem}}}" />
|
||||
<materialDesign:PackIcon Grid.Column="0"
|
||||
Kind="FolderOpen"
|
||||
Visibility="{Binding IsExpanded, Converter={x:Static s:BoolToVisibilityConverter.Instance}, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TreeViewItem}}}" />
|
||||
<TextBlock Grid.Column="1" Text="{Binding ProfileElement.Name}" Margin="10 0 0 0" VerticalAlignment="Center" />
|
||||
<ToggleButton Grid.Column="2"
|
||||
Style="{StaticResource MaterialDesignFlatToggleButton}"
|
||||
ToolTip="Toggle enabled state"
|
||||
Width="18"
|
||||
Height="18"
|
||||
IsChecked="{Binding ProfileElement.Enabled}"
|
||||
Command="{s:Action EnableToggled}"
|
||||
VerticalAlignment="Center" Padding="-25">
|
||||
<materialDesign:PackIcon Kind="Eye" Height="13" Width="13" />
|
||||
</ToggleButton>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,46 @@
|
||||
using Artemis.Core.Models.Profile;
|
||||
using Artemis.Core.Services.Interfaces;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem
|
||||
{
|
||||
public class FolderViewModel : TreeItemViewModel
|
||||
{
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
foreach (var treeItemViewModel in Children)
|
||||
treeItemViewModel.Dispose();
|
||||
Children.Clear();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
// I hate this about DI, oh well
|
||||
public FolderViewModel(ProfileElement folder,
|
||||
IProfileEditorService profileEditorService,
|
||||
IDialogService dialogService,
|
||||
IRenderElementService renderElementService,
|
||||
IFolderVmFactory folderVmFactory,
|
||||
ILayerVmFactory layerVmFactory) :
|
||||
base(null, folder, profileEditorService, dialogService, renderElementService, folderVmFactory, layerVmFactory)
|
||||
{
|
||||
}
|
||||
|
||||
public FolderViewModel(TreeItemViewModel parent,
|
||||
ProfileElement folder,
|
||||
IProfileEditorService profileEditorService,
|
||||
IDialogService dialogService,
|
||||
IRenderElementService renderElementService,
|
||||
IFolderVmFactory folderVmFactory,
|
||||
ILayerVmFactory layerVmFactory) :
|
||||
base(parent, folder, profileEditorService, dialogService, renderElementService, folderVmFactory, layerVmFactory)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool SupportsChildren => true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem.LayerView"
|
||||
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:treeItem="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem"
|
||||
xmlns:treeItem1="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance {x:Type treeItem1:LayerViewModel}}">
|
||||
<!-- Capture clicks on full tree view item -->
|
||||
<StackPanel Margin="-10" Background="Transparent">
|
||||
<StackPanel.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="Rename" Command="{s:Action RenameElement}">
|
||||
<MenuItem.Icon>
|
||||
<materialDesign:PackIcon Kind="RenameBox" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Delete" Command="{s:Action DeleteElement}">
|
||||
<MenuItem.Icon>
|
||||
<materialDesign:PackIcon Kind="TrashCan" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
</ContextMenu>
|
||||
</StackPanel.ContextMenu>
|
||||
<Grid Margin="10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<materialDesign:PackIcon Grid.Column="0" Kind="Layers" Width="16" VerticalAlignment="Center"/>
|
||||
<materialDesign:PackIcon Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Kind="{Binding Layer.LayerBrush.Descriptor.Icon}"
|
||||
Width="16"
|
||||
Margin="5 0 0 0"
|
||||
ToolTip="{Binding Layer.LayerBrush.Descriptor.DisplayName, Mode=OneWay}"
|
||||
Visibility="{Binding ShowIcons, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}"
|
||||
Background="Transparent" />
|
||||
<TextBlock Grid.Column="2" Text="{Binding Layer.Name}" VerticalAlignment="Center" Margin="5 0 0 0" />
|
||||
<ToggleButton Grid.Column="3"
|
||||
Style="{StaticResource MaterialDesignFlatToggleButton}"
|
||||
ToolTip="Toggle enabled state"
|
||||
Width="18"
|
||||
Height="18"
|
||||
IsChecked="{Binding ProfileElement.Enabled}"
|
||||
Command="{s:Action EnableToggled}"
|
||||
VerticalAlignment="Center" Padding="-25">
|
||||
<materialDesign:PackIcon Kind="Eye" Height="13" Width="13" />
|
||||
</ToggleButton>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,25 @@
|
||||
using Artemis.Core.Models.Profile;
|
||||
using Artemis.Core.Services.Interfaces;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem
|
||||
{
|
||||
public class LayerViewModel : TreeItemViewModel
|
||||
{
|
||||
public LayerViewModel(TreeItemViewModel parent,
|
||||
ProfileElement folder,
|
||||
IProfileEditorService profileEditorService,
|
||||
IDialogService dialogService,
|
||||
IRenderElementService renderElementService,
|
||||
IFolderVmFactory folderVmFactory,
|
||||
ILayerVmFactory layerVmFactory) :
|
||||
base(parent, folder, profileEditorService, dialogService, renderElementService, folderVmFactory, layerVmFactory)
|
||||
{
|
||||
}
|
||||
|
||||
public Layer Layer => ProfileElement as Layer;
|
||||
public bool ShowIcons => Layer?.LayerBrush != null;
|
||||
public override bool SupportsChildren => false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core.Models.Profile;
|
||||
using Artemis.Core.Services.Interfaces;
|
||||
using Artemis.UI.Exceptions;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Screens.ProfileEditor.Dialogs;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem
|
||||
{
|
||||
public abstract class TreeItemViewModel : PropertyChangedBase, IDisposable
|
||||
{
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly IFolderVmFactory _folderVmFactory;
|
||||
private readonly ILayerVmFactory _layerVmFactory;
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private readonly IRenderElementService _renderElementService;
|
||||
private TreeItemViewModel _parent;
|
||||
private ProfileElement _profileElement;
|
||||
|
||||
protected TreeItemViewModel(TreeItemViewModel parent,
|
||||
ProfileElement profileElement,
|
||||
IProfileEditorService profileEditorService,
|
||||
IDialogService dialogService,
|
||||
IRenderElementService renderElementService,
|
||||
IFolderVmFactory folderVmFactory,
|
||||
ILayerVmFactory layerVmFactory)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
_dialogService = dialogService;
|
||||
_renderElementService = renderElementService;
|
||||
_folderVmFactory = folderVmFactory;
|
||||
_layerVmFactory = layerVmFactory;
|
||||
|
||||
Parent = parent;
|
||||
ProfileElement = profileElement;
|
||||
|
||||
Children = new BindableCollection<TreeItemViewModel>();
|
||||
|
||||
ProfileElement.ChildAdded += ProfileElementOnChildAdded;
|
||||
ProfileElement.ChildRemoved += ProfileElementOnChildRemoved;
|
||||
|
||||
UpdateProfileElements();
|
||||
}
|
||||
|
||||
public TreeItemViewModel Parent
|
||||
{
|
||||
get => _parent;
|
||||
set => SetAndNotify(ref _parent, value);
|
||||
}
|
||||
|
||||
public ProfileElement ProfileElement
|
||||
{
|
||||
get => _profileElement;
|
||||
set => SetAndNotify(ref _profileElement, value);
|
||||
}
|
||||
|
||||
public BindableCollection<TreeItemViewModel> Children { get; }
|
||||
|
||||
public abstract bool SupportsChildren { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ProfileElement.ChildAdded -= ProfileElementOnChildAdded;
|
||||
ProfileElement.ChildRemoved -= ProfileElementOnChildRemoved;
|
||||
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public List<TreeItemViewModel> GetAllChildren()
|
||||
{
|
||||
var children = new List<TreeItemViewModel>();
|
||||
foreach (var childFolder in Children)
|
||||
{
|
||||
// Add all children in this element
|
||||
children.Add(childFolder);
|
||||
// Add all children of children inside this element
|
||||
children.AddRange(childFolder.GetAllChildren());
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
public void SetElementInFront(TreeItemViewModel source)
|
||||
{
|
||||
if (source.Parent != Parent)
|
||||
{
|
||||
source.Parent.RemoveExistingElement(source);
|
||||
Parent.AddExistingElement(source);
|
||||
}
|
||||
|
||||
Parent.ProfileElement.RemoveChild(source.ProfileElement);
|
||||
Parent.ProfileElement.AddChild(source.ProfileElement, ProfileElement.Order);
|
||||
}
|
||||
|
||||
public void SetElementBehind(TreeItemViewModel source)
|
||||
{
|
||||
if (source.Parent != Parent)
|
||||
{
|
||||
source.Parent.RemoveExistingElement(source);
|
||||
Parent.AddExistingElement(source);
|
||||
}
|
||||
|
||||
Parent.ProfileElement.RemoveChild(source.ProfileElement);
|
||||
Parent.ProfileElement.AddChild(source.ProfileElement, ProfileElement.Order + 1);
|
||||
}
|
||||
|
||||
public void RemoveExistingElement(TreeItemViewModel treeItem)
|
||||
{
|
||||
if (!SupportsChildren)
|
||||
throw new ArtemisUIException("Cannot remove a child from a profile element of type " + ProfileElement.GetType().Name);
|
||||
|
||||
ProfileElement.RemoveChild(treeItem.ProfileElement);
|
||||
Children.Remove(treeItem);
|
||||
treeItem.Parent = null;
|
||||
treeItem.Dispose();
|
||||
}
|
||||
|
||||
public void AddExistingElement(TreeItemViewModel treeItem)
|
||||
{
|
||||
if (!SupportsChildren)
|
||||
throw new ArtemisUIException("Cannot add a child to a profile element of type " + ProfileElement.GetType().Name);
|
||||
|
||||
ProfileElement.AddChild(treeItem.ProfileElement);
|
||||
Children.Add(treeItem);
|
||||
treeItem.Parent = this;
|
||||
}
|
||||
|
||||
public void AddFolder()
|
||||
{
|
||||
if (!SupportsChildren)
|
||||
throw new ArtemisUIException("Cannot add a folder to a profile element of type " + ProfileElement.GetType().Name);
|
||||
|
||||
ProfileElement.AddChild(new Folder(ProfileElement.Profile, ProfileElement, "New folder"));
|
||||
_profileEditorService.UpdateSelectedProfile();
|
||||
}
|
||||
|
||||
public void AddLayer()
|
||||
{
|
||||
if (!SupportsChildren)
|
||||
throw new ArtemisUIException("Cannot add a layer to a profile element of type " + ProfileElement.GetType().Name);
|
||||
|
||||
_renderElementService.CreateLayer(ProfileElement.Profile, ProfileElement, "New layer");
|
||||
_profileEditorService.UpdateSelectedProfile();
|
||||
}
|
||||
|
||||
// ReSharper disable once UnusedMember.Global - Called from view
|
||||
public async Task RenameElement()
|
||||
{
|
||||
var result = await _dialogService.ShowDialog<RenameViewModel>(
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{"subject", ProfileElement is Folder ? "folder" : "layer"},
|
||||
{"currentName", ProfileElement.Name}
|
||||
}
|
||||
);
|
||||
if (result is string newName)
|
||||
{
|
||||
ProfileElement.Name = newName;
|
||||
_profileEditorService.UpdateSelectedProfile();
|
||||
}
|
||||
}
|
||||
|
||||
// ReSharper disable once UnusedMember.Global - Called from view
|
||||
public async Task DeleteElement()
|
||||
{
|
||||
var result = await _dialogService.ShowConfirmDialog(
|
||||
"Delete profile element",
|
||||
"Are you sure you want to delete this element? This cannot be undone."
|
||||
);
|
||||
|
||||
if (!result)
|
||||
return;
|
||||
|
||||
// Farewell, cruel world
|
||||
var parent = Parent;
|
||||
ProfileElement.Parent?.RemoveChild(ProfileElement);
|
||||
parent.RemoveExistingElement(this);
|
||||
|
||||
_profileEditorService.UpdateSelectedProfile();
|
||||
}
|
||||
|
||||
public void UpdateProfileElements()
|
||||
{
|
||||
// Order the children
|
||||
var vmsList = Children.OrderBy(v => v.ProfileElement.Order).ToList();
|
||||
for (var index = 0; index < vmsList.Count; index++)
|
||||
{
|
||||
var profileElementViewModel = vmsList[index];
|
||||
Children.Move(Children.IndexOf(profileElementViewModel), index);
|
||||
}
|
||||
|
||||
// Ensure every child element has an up-to-date VM
|
||||
if (ProfileElement.Children == null)
|
||||
return;
|
||||
|
||||
var newChildren = new List<TreeItemViewModel>();
|
||||
foreach (var profileElement in ProfileElement.Children.OrderBy(c => c.Order))
|
||||
{
|
||||
if (profileElement is Folder folder)
|
||||
{
|
||||
if (Children.FirstOrDefault(p => p is FolderViewModel vm && vm.ProfileElement == folder) == null)
|
||||
newChildren.Add(_folderVmFactory.Create((FolderViewModel) this, folder));
|
||||
}
|
||||
else if (profileElement is Layer layer)
|
||||
{
|
||||
if (Children.FirstOrDefault(p => p is LayerViewModel vm && vm.ProfileElement == layer) == null)
|
||||
newChildren.Add(_layerVmFactory.Create((FolderViewModel) this, layer));
|
||||
}
|
||||
}
|
||||
|
||||
if (!newChildren.Any())
|
||||
return;
|
||||
|
||||
// Add the new children in one call, prevent extra UI events
|
||||
Children.AddRange(newChildren);
|
||||
foreach (var treeItemViewModel in newChildren)
|
||||
treeItemViewModel.UpdateProfileElements();
|
||||
}
|
||||
|
||||
public void EnableToggled()
|
||||
{
|
||||
_profileEditorService.UpdateSelectedProfile();
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void ProfileElementOnChildRemoved(object sender, EventArgs e)
|
||||
{
|
||||
UpdateProfileElements();
|
||||
}
|
||||
|
||||
private void ProfileElementOnChildAdded(object sender, EventArgs e)
|
||||
{
|
||||
UpdateProfileElements();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Visualization
|
||||
{
|
||||
public abstract class CanvasViewModel : PropertyChangedBase, IDisposable
|
||||
{
|
||||
private double _x;
|
||||
private double _y;
|
||||
|
||||
public double X
|
||||
{
|
||||
get => _x;
|
||||
set => SetAndNotify(ref _x, value);
|
||||
}
|
||||
|
||||
public double Y
|
||||
{
|
||||
get => _y;
|
||||
set => SetAndNotify(ref _y, value);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.Visualization.ProfileDeviceView"
|
||||
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:visualization="clr-namespace:Artemis.UI.Screens.ProfileEditor.Visualization"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:visualization1="clr-namespace:Artemis.UI.Screens.ProfileEditor.Visualization"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance {x:Type visualization1:ProfileDeviceViewModel}}">
|
||||
<UserControl.Resources>
|
||||
<converters:NullToImageConverter x:Key="NullToImageConverter" />
|
||||
<converters:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid Width="{Binding Device.RgbDevice.ActualSize.Width}" Height="{Binding Device.RgbDevice.ActualSize.Height}">
|
||||
<Grid.RenderTransform>
|
||||
<RotateTransform Angle="{Binding Device.Rotation}" />
|
||||
</Grid.RenderTransform>
|
||||
|
||||
<!-- Device image with fallback -->
|
||||
<Image VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch"
|
||||
Source="{Binding Device.RgbDevice.DeviceInfo.Image, Converter={StaticResource NullToImageConverter}}" />
|
||||
|
||||
<Rectangle Fill="{DynamicResource MaterialDesignCardBackground}"
|
||||
Stroke="{DynamicResource MaterialDesignTextBoxBorder}"
|
||||
Margin="-5"
|
||||
StrokeThickness="1"
|
||||
Visibility="{Binding Device.RgbDevice.DeviceInfo.Image, ConverterParameter=Inverted, Converter={StaticResource NullToVisibilityConverter}}" />
|
||||
<TextBlock Text="{Binding Device.RgbDevice.DeviceInfo.DeviceName}"
|
||||
Visibility="{Binding Device.RgbDevice.DeviceInfo.Image, ConverterParameter=Inverted, Converter={StaticResource NullToVisibilityConverter}}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center" />
|
||||
|
||||
<!-- LEDs -->
|
||||
<ItemsControl ItemsSource="{Binding Leds}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Canvas />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemContainerStyle>
|
||||
<Style TargetType="{x:Type ContentPresenter}">
|
||||
<Setter Property="Canvas.Left" Value="{Binding X}" />
|
||||
<Setter Property="Canvas.Top" Value="{Binding Y}" />
|
||||
</Style>
|
||||
</ItemsControl.ItemContainerStyle>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<ContentControl Width="{Binding Width}" Height="{Binding Height}" s:View.Model="{Binding}" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,96 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using Artemis.Core.Models.Surface;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Visualization
|
||||
{
|
||||
public class ProfileDeviceViewModel : CanvasViewModel
|
||||
{
|
||||
private ObservableCollection<ProfileLedViewModel> _leds;
|
||||
private ArtemisDevice _device;
|
||||
private bool _addedLeds;
|
||||
|
||||
public ProfileDeviceViewModel(ArtemisDevice device)
|
||||
{
|
||||
Device = device;
|
||||
Leds = new ObservableCollection<ProfileLedViewModel>();
|
||||
|
||||
Task.Run(AddLedsAsync);
|
||||
}
|
||||
|
||||
public ObservableCollection<ProfileLedViewModel> Leds
|
||||
{
|
||||
get => _leds;
|
||||
set => SetAndNotify(ref _leds, value);
|
||||
}
|
||||
|
||||
public ArtemisDevice Device
|
||||
{
|
||||
get => _device;
|
||||
set => SetAndNotify(ref _device, value);
|
||||
}
|
||||
|
||||
public bool AddedLeds
|
||||
{
|
||||
get => _addedLeds;
|
||||
private set => SetAndNotify(ref _addedLeds, value);
|
||||
}
|
||||
|
||||
public new double X
|
||||
{
|
||||
get => Device.X;
|
||||
set => Device.X = value;
|
||||
}
|
||||
|
||||
public new double Y
|
||||
{
|
||||
get => Device.Y;
|
||||
set => Device.Y = value;
|
||||
}
|
||||
|
||||
public int ZIndex
|
||||
{
|
||||
get => Device.ZIndex;
|
||||
set => Device.ZIndex = value;
|
||||
}
|
||||
|
||||
|
||||
public Rect DeviceRectangle => Device.RgbDevice == null
|
||||
? new Rect()
|
||||
: new Rect(X, Y, Device.RgbDevice.Size.Width, Device.RgbDevice.Size.Height);
|
||||
|
||||
/// <summary>
|
||||
/// Update the color of all LEDs if finished adding
|
||||
/// </summary>
|
||||
public void Update()
|
||||
{
|
||||
if (!AddedLeds)
|
||||
return;
|
||||
|
||||
foreach (var ledViewModel in Leds)
|
||||
ledViewModel.Update();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds LEDs in batches of 5 to avoid UI freezes
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private async Task AddLedsAsync()
|
||||
{
|
||||
var index = 0;
|
||||
foreach (var led in Device.Leds.ToList())
|
||||
{
|
||||
Execute.OnUIThreadSync(() => Leds.Add(new ProfileLedViewModel(led)));
|
||||
if (index % 5 == 0)
|
||||
await Task.Delay(1);
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
AddedLeds = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.Visualization.ProfileLayerView"
|
||||
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.Visualization"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
xmlns:visualization="clr-namespace:Artemis.UI.Screens.ProfileEditor.Visualization"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance {x:Type visualization:ProfileLayerViewModel}}">
|
||||
<UserControl.Resources>
|
||||
<Style TargetType="{x:Type Path}" x:Key="SelectedShapeStyle">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsSelected}" Value="False">
|
||||
<DataTrigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<ColorAnimation Storyboard.TargetProperty="(Path.Stroke).(SolidColorBrush.Color)" To="#99808080" Duration="0:0:0.5" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.EnterActions>
|
||||
<DataTrigger.ExitActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<ColorAnimation Storyboard.TargetProperty="(Path.Stroke).(SolidColorBrush.Color)" To="{StaticResource Accent700}" Duration="0:0:0.5" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.ExitActions>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
<Style TargetType="{x:Type Path}" x:Key="SelectedLayerStyle">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsSelected}" Value="False">
|
||||
<DataTrigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<ColorAnimation Storyboard.TargetProperty="(Path.Stroke).(SolidColorBrush.Color)" To="#A7A7A7" Duration="0:0:0.5" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.EnterActions>
|
||||
<DataTrigger.ExitActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<ColorAnimation Storyboard.TargetProperty="(Path.Stroke).(SolidColorBrush.Color)" To="#00A7A7A7" Duration="0:0:0.5" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.ExitActions>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
<Canvas>
|
||||
<Path Data="{Binding ShapeGeometry, Mode=OneWay}" StrokeThickness="{Binding StrokeThickness}" Style="{StaticResource SelectedShapeStyle}">
|
||||
<Path.Stroke>
|
||||
<SolidColorBrush Color="{StaticResource Primary700}" />
|
||||
</Path.Stroke>
|
||||
</Path>
|
||||
</Canvas>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,229 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using Artemis.Core.Models.Profile;
|
||||
using Artemis.Core.Models.Profile.LayerShapes;
|
||||
using Artemis.Core.Models.Surface;
|
||||
using Artemis.UI.Extensions;
|
||||
using Artemis.UI.Services.Interfaces;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Visualization
|
||||
{
|
||||
public class ProfileLayerViewModel : CanvasViewModel
|
||||
{
|
||||
private readonly ILayerEditorService _layerEditorService;
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private readonly ProfileViewModel _profileViewModel;
|
||||
private bool _isSelected;
|
||||
private Geometry _shapeGeometry;
|
||||
private Rect _viewportRectangle;
|
||||
|
||||
public ProfileLayerViewModel(Layer layer, ProfileViewModel profileViewModel, IProfileEditorService profileEditorService, ILayerEditorService layerEditorService)
|
||||
{
|
||||
_profileViewModel = profileViewModel;
|
||||
_profileEditorService = profileEditorService;
|
||||
_layerEditorService = layerEditorService;
|
||||
Layer = layer;
|
||||
|
||||
|
||||
Update();
|
||||
Layer.RenderPropertiesUpdated += LayerOnRenderPropertiesUpdated;
|
||||
_profileEditorService.ProfileElementSelected += OnProfileElementSelected;
|
||||
_profileEditorService.SelectedProfileElementUpdated += OnSelectedProfileElementUpdated;
|
||||
_profileEditorService.ProfilePreviewUpdated += ProfileEditorServiceOnProfilePreviewUpdated;
|
||||
_profileViewModel.PanZoomViewModel.PropertyChanged += PanZoomViewModelOnPropertyChanged;
|
||||
}
|
||||
|
||||
public Layer Layer { get; }
|
||||
|
||||
public Geometry ShapeGeometry
|
||||
{
|
||||
get => _shapeGeometry;
|
||||
set => SetAndNotify(ref _shapeGeometry, value);
|
||||
}
|
||||
|
||||
public Rect ViewportRectangle
|
||||
{
|
||||
get => _viewportRectangle;
|
||||
set
|
||||
{
|
||||
if (!SetAndNotify(ref _viewportRectangle, value)) return;
|
||||
NotifyOfPropertyChange(nameof(LayerPosition));
|
||||
}
|
||||
}
|
||||
|
||||
public double StrokeThickness
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsSelected)
|
||||
return Math.Max(2 / _profileViewModel.PanZoomViewModel.Zoom, 1);
|
||||
return Math.Max(2 / _profileViewModel.PanZoomViewModel.Zoom, 1) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
public double LayerStrokeThickness
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsSelected)
|
||||
return StrokeThickness / 2;
|
||||
return StrokeThickness;
|
||||
}
|
||||
}
|
||||
|
||||
public Thickness LayerPosition => new Thickness(ViewportRectangle.Left, ViewportRectangle.Top, 0, 0);
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
get => _isSelected;
|
||||
set
|
||||
{
|
||||
if (!SetAndNotify(ref _isSelected, value)) return;
|
||||
NotifyOfPropertyChange(nameof(StrokeThickness));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
Layer.RenderPropertiesUpdated -= LayerOnRenderPropertiesUpdated;
|
||||
_profileEditorService.ProfileElementSelected -= OnProfileElementSelected;
|
||||
_profileEditorService.SelectedProfileElementUpdated -= OnSelectedProfileElementUpdated;
|
||||
_profileEditorService.ProfilePreviewUpdated -= ProfileEditorServiceOnProfilePreviewUpdated;
|
||||
_profileViewModel.PanZoomViewModel.PropertyChanged -= PanZoomViewModelOnPropertyChanged;
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
private void PanZoomViewModelOnPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(_profileViewModel.PanZoomViewModel.Zoom))
|
||||
{
|
||||
NotifyOfPropertyChange(nameof(StrokeThickness));
|
||||
NotifyOfPropertyChange(nameof(LayerStrokeThickness));
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
IsSelected = _profileEditorService.SelectedProfileElement == Layer;
|
||||
|
||||
CreateShapeGeometry();
|
||||
CreateViewportRectangle();
|
||||
}
|
||||
|
||||
private void CreateShapeGeometry()
|
||||
{
|
||||
if (Layer.LayerShape == null || !Layer.Leds.Any())
|
||||
{
|
||||
ShapeGeometry = Geometry.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
Execute.PostToUIThread(() =>
|
||||
{
|
||||
var bounds = _layerEditorService.GetLayerBounds(Layer);
|
||||
var shapeGeometry = Geometry.Empty;
|
||||
switch (Layer.LayerShape)
|
||||
{
|
||||
case Ellipse _:
|
||||
shapeGeometry = new EllipseGeometry(bounds);
|
||||
break;
|
||||
case Rectangle _:
|
||||
shapeGeometry = new RectangleGeometry(bounds);
|
||||
break;
|
||||
}
|
||||
|
||||
if (Layer.LayerBrush == null || Layer.LayerBrush.SupportsTransformation)
|
||||
shapeGeometry.Transform = _layerEditorService.GetLayerTransformGroup(Layer);
|
||||
|
||||
ShapeGeometry = shapeGeometry;
|
||||
});
|
||||
}
|
||||
|
||||
private void CreateViewportRectangle()
|
||||
{
|
||||
if (!Layer.Leds.Any() || Layer.LayerShape == null)
|
||||
{
|
||||
ViewportRectangle = Rect.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
ViewportRectangle = _layerEditorService.GetLayerBounds(Layer);
|
||||
}
|
||||
|
||||
private Geometry CreateRectangleGeometry(ArtemisLed led)
|
||||
{
|
||||
var rect = led.RgbLed.AbsoluteLedRectangle.ToWindowsRect(1);
|
||||
return new RectangleGeometry(rect);
|
||||
}
|
||||
|
||||
private Geometry CreateCircleGeometry(ArtemisLed led)
|
||||
{
|
||||
var rect = led.RgbLed.AbsoluteLedRectangle.ToWindowsRect(1);
|
||||
return new EllipseGeometry(rect);
|
||||
}
|
||||
|
||||
private Geometry CreateCustomGeometry(ArtemisLed led, double deflateAmount)
|
||||
{
|
||||
var rect = led.RgbLed.AbsoluteLedRectangle.ToWindowsRect(1);
|
||||
try
|
||||
{
|
||||
var geometry = Geometry.Combine(
|
||||
Geometry.Empty,
|
||||
Geometry.Parse(led.RgbLed.ShapeData),
|
||||
GeometryCombineMode.Union,
|
||||
new TransformGroup
|
||||
{
|
||||
Children = new TransformCollection
|
||||
{
|
||||
new ScaleTransform(rect.Width, rect.Height),
|
||||
new TranslateTransform(rect.X, rect.Y)
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return geometry;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return CreateRectangleGeometry(led);
|
||||
}
|
||||
}
|
||||
|
||||
#region Event handlers
|
||||
|
||||
private void LayerOnRenderPropertiesUpdated(object sender, EventArgs e)
|
||||
{
|
||||
Update();
|
||||
}
|
||||
|
||||
private void OnProfileElementSelected(object sender, EventArgs e)
|
||||
{
|
||||
IsSelected = _profileEditorService.SelectedProfileElement == Layer;
|
||||
}
|
||||
|
||||
private void OnSelectedProfileElementUpdated(object sender, EventArgs e)
|
||||
{
|
||||
Update();
|
||||
}
|
||||
|
||||
private void ProfileEditorServiceOnProfilePreviewUpdated(object sender, EventArgs e)
|
||||
{
|
||||
if (!Layer.Transform.PropertiesInitialized || !Layer.General.PropertiesInitialized)
|
||||
return;
|
||||
|
||||
CreateShapeGeometry();
|
||||
CreateViewportRectangle();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.Visualization.ProfileLedView"
|
||||
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:converters="clr-namespace:Artemis.UI.Converters"
|
||||
xmlns:visualization="clr-namespace:Artemis.UI.Screens.ProfileEditor.Visualization"
|
||||
xmlns:visualization1="clr-namespace:Artemis.UI.Screens.ProfileEditor.Visualization"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance {x:Type visualization1:ProfileLedViewModel}}"
|
||||
d:DesignHeight="25" d:DesignWidth="25">
|
||||
<UserControl.Resources>
|
||||
<converters:NullToImageConverter x:Key="NullToImageConverter" />
|
||||
<Style TargetType="{x:Type Path}" x:Key="DimStyle">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsDimmed}" Value="True">
|
||||
<DataTrigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="(Shape.Opacity)" To="0.2" Duration="0:0:0.25" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.EnterActions>
|
||||
<DataTrigger.ExitActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="(Shape.Opacity)" To="1" Duration="0:0:0.25" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.ExitActions>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
<Canvas Width="{Binding Led.RgbLed.ActualSize.Width, Mode=OneWay}" Height="{Binding Led.RgbLed.ActualSize.Height, Mode=OneWay}">
|
||||
<Canvas.Background>
|
||||
<ImageBrush AlignmentX="Center" AlignmentY="Center" Stretch="Fill"
|
||||
ImageSource="{Binding Led.RgbLed.Image, Converter={StaticResource NullToImageConverter}, Mode=OneWay}" />
|
||||
</Canvas.Background>
|
||||
|
||||
<Path Data="{Binding DisplayGeometry, Mode=OneWay}" ClipToBounds="False" Style="{StaticResource DimStyle}">
|
||||
<Path.Fill>
|
||||
<SolidColorBrush Color="{Binding DisplayColor, Mode=OneWay}" Opacity="0.333" />
|
||||
</Path.Fill>
|
||||
</Path>
|
||||
<Path Data="{Binding StrokeGeometry, Mode=OneWay}" ClipToBounds="False" Style="{StaticResource DimStyle}">
|
||||
<Path.Fill>
|
||||
<SolidColorBrush Color="{Binding DisplayColor, Mode=OneWay}" />
|
||||
</Path.Fill>
|
||||
</Path>
|
||||
|
||||
<!-- Selection -->
|
||||
<Path Data="{Binding DisplayGeometry, Mode=OneWay}" ClipToBounds="False" StrokeThickness="1" StrokeLineJoin="Round">
|
||||
<Path.Style>
|
||||
<Style TargetType="{x:Type Path}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsSelected}" Value="True">
|
||||
<DataTrigger.EnterActions>
|
||||
<BeginStoryboard x:Name="ToSelected">
|
||||
<Storyboard>
|
||||
<ColorAnimation
|
||||
Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
|
||||
To="{StaticResource Accent700}" Duration="0:0:0.25" />
|
||||
<ColorAnimation
|
||||
Storyboard.TargetProperty="(Shape.Stroke).(SolidColorBrush.Color)"
|
||||
To="{StaticResource Accent400}" Duration="0:0:0.25" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.EnterActions>
|
||||
<DataTrigger.ExitActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<ColorAnimation
|
||||
Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
|
||||
To="Transparent" Duration="0:0:0.25" />
|
||||
<ColorAnimation
|
||||
Storyboard.TargetProperty="(Shape.Stroke).(SolidColorBrush.Color)"
|
||||
To="Transparent" Duration="0:0:0.25" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.ExitActions>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Path.Style>
|
||||
<Path.Fill>
|
||||
<SolidColorBrush Opacity="0.25" />
|
||||
</Path.Fill>
|
||||
<Path.Stroke>
|
||||
<SolidColorBrush Opacity="0.5" />
|
||||
</Path.Stroke>
|
||||
</Path>
|
||||
</Canvas>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,148 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using Artemis.Core.Models.Surface;
|
||||
using Artemis.UI.Extensions;
|
||||
using RGB.NET.Core;
|
||||
using Stylet;
|
||||
using Color = System.Windows.Media.Color;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Visualization
|
||||
{
|
||||
public class ProfileLedViewModel : PropertyChangedBase
|
||||
{
|
||||
private bool _isSelected;
|
||||
private bool _isDimmed;
|
||||
private Geometry _displayGeometry;
|
||||
private Geometry _strokeGeometry;
|
||||
private Color _displayColor;
|
||||
|
||||
public ProfileLedViewModel(ArtemisLed led)
|
||||
{
|
||||
Led = led;
|
||||
|
||||
// Don't want ActualLocation here since rotation is done in XAML
|
||||
X = led.RgbLed.Location.X * led.RgbLed.Device.Scale.Horizontal;
|
||||
Y = led.RgbLed.Location.Y * led.RgbLed.Device.Scale.Vertical;
|
||||
Width = led.RgbLed.ActualSize.Width;
|
||||
Height = led.RgbLed.ActualSize.Height;
|
||||
|
||||
Execute.PostToUIThread(CreateLedGeometry);
|
||||
}
|
||||
|
||||
public ArtemisLed Led { get; }
|
||||
|
||||
public double X { get; }
|
||||
public double Y { get; }
|
||||
public double Width { get; }
|
||||
public double Height { get; }
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
get => _isSelected;
|
||||
set => SetAndNotify(ref _isSelected, value);
|
||||
}
|
||||
|
||||
public bool IsDimmed
|
||||
{
|
||||
get => _isDimmed;
|
||||
set => SetAndNotify(ref _isDimmed, value);
|
||||
}
|
||||
|
||||
public Geometry DisplayGeometry
|
||||
{
|
||||
get => _displayGeometry;
|
||||
private set => SetAndNotify(ref _displayGeometry, value);
|
||||
}
|
||||
|
||||
public Geometry StrokeGeometry
|
||||
{
|
||||
get => _strokeGeometry;
|
||||
private set => SetAndNotify(ref _strokeGeometry, value);
|
||||
}
|
||||
|
||||
public Color DisplayColor
|
||||
{
|
||||
get => _displayColor;
|
||||
private set => SetAndNotify(ref _displayColor, value);
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
var newColor = Led.RgbLed.Color.ToMediaColor();
|
||||
Execute.PostToUIThread(() =>
|
||||
{
|
||||
if (!DisplayColor.Equals(newColor))
|
||||
DisplayColor = newColor;
|
||||
});
|
||||
}
|
||||
|
||||
private void CreateLedGeometry()
|
||||
{
|
||||
switch (Led.RgbLed.Shape)
|
||||
{
|
||||
case Shape.Custom:
|
||||
if (Led.RgbLed.Device.DeviceInfo.DeviceType == RGBDeviceType.Keyboard || Led.RgbLed.Device.DeviceInfo.DeviceType == RGBDeviceType.Keypad)
|
||||
CreateCustomGeometry(2.0);
|
||||
else
|
||||
CreateCustomGeometry(1.0);
|
||||
break;
|
||||
case Shape.Rectangle:
|
||||
if (Led.RgbLed.Device.DeviceInfo.DeviceType == RGBDeviceType.Keyboard || Led.RgbLed.Device.DeviceInfo.DeviceType == RGBDeviceType.Keypad)
|
||||
CreateKeyCapGeometry();
|
||||
else
|
||||
CreateRectangleGeometry();
|
||||
break;
|
||||
case Shape.Circle:
|
||||
CreateCircleGeometry();
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
// Stroke geometry is the display geometry excluding the inner geometry
|
||||
StrokeGeometry = DisplayGeometry.GetWidenedPathGeometry(new Pen(null, 1.0), 0.1, ToleranceType.Absolute);
|
||||
DisplayGeometry.Freeze();
|
||||
StrokeGeometry.Freeze();
|
||||
}
|
||||
|
||||
private void CreateRectangleGeometry()
|
||||
{
|
||||
DisplayGeometry = new RectangleGeometry(new Rect(0.5, 0.5, Width - 1, Height - 1));
|
||||
}
|
||||
|
||||
private void CreateCircleGeometry()
|
||||
{
|
||||
DisplayGeometry = new EllipseGeometry(new Rect(0.5, 0.5, Width - 1, Height - 1));
|
||||
}
|
||||
|
||||
private void CreateKeyCapGeometry()
|
||||
{
|
||||
DisplayGeometry = new RectangleGeometry(new Rect(1, 1, Width - 2, Height - 2), 1.6, 1.6);
|
||||
}
|
||||
|
||||
private void CreateCustomGeometry(double deflateAmount)
|
||||
{
|
||||
try
|
||||
{
|
||||
DisplayGeometry = Geometry.Combine(
|
||||
Geometry.Empty,
|
||||
Geometry.Parse(Led.RgbLed.ShapeData),
|
||||
GeometryCombineMode.Union,
|
||||
new TransformGroup
|
||||
{
|
||||
Children = new TransformCollection
|
||||
{
|
||||
new ScaleTransform(Width - deflateAmount, Height - deflateAmount),
|
||||
new TranslateTransform(deflateAmount / 2, deflateAmount / 2)
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
CreateRectangleGeometry();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.Visualization.ProfileView"
|
||||
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:profileEditor="clr-namespace:Artemis.UI.Screens.ProfileEditor.Visualization"
|
||||
xmlns:utilities="clr-namespace:Artemis.UI.Shared.Utilities;assembly=Artemis.UI.Shared"
|
||||
xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared"
|
||||
xmlns:visualization="clr-namespace:Artemis.UI.Screens.ProfileEditor.Visualization"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="510.9" d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance {x:Type visualization:ProfileViewModel}}">
|
||||
<UserControl.Resources>
|
||||
<Style TargetType="Grid" x:Key="InitializingFade">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsInitializing}" Value="False">
|
||||
<DataTrigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="1.0" To="0.0" Duration="0:0:0.5">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<QuadraticEase EasingMode="EaseInOut" />
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
<ObjectAnimationUsingKeyFrames BeginTime="0:0:0.5" Storyboard.TargetProperty="Visibility">
|
||||
<DiscreteObjectKeyFrame KeyTime="00:00:00" Value="{x:Static Visibility.Hidden}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.EnterActions>
|
||||
<DataTrigger.ExitActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="0.0" To="1.0" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.ExitActions>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ToolBarTray Orientation="Vertical" Width="58">
|
||||
<ToolBar Style="{DynamicResource MaterialDesignToolBar}" ClipToBounds="False" ToolBarTray.IsLocked="True">
|
||||
<ListBox SelectedIndex="{Binding ActiveToolIndex}" ToolBar.OverflowMode="Never">
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Vertical" />
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<ListBoxItem ToolTip="Pan over different parts of the surface">
|
||||
<materialDesign:PackIcon Kind="HandLeft" />
|
||||
</ListBoxItem>
|
||||
<ListBoxItem ToolTip="Edit shape in layer" IsEnabled="{Binding CanSelectEditTool}">
|
||||
<materialDesign:PackIcon Kind="Edit" />
|
||||
</ListBoxItem>
|
||||
<ListBoxItem ToolTip="Change layer selection (hold SHIFT to add to existing selection)">
|
||||
<materialDesign:PackIcon Kind="SelectionDrag" />
|
||||
</ListBoxItem>
|
||||
<ListBoxItem ToolTip="Remove from layer selection">
|
||||
<materialDesign:PackIcon Kind="SelectOff" />
|
||||
</ListBoxItem>
|
||||
</ListBox>
|
||||
</ToolBar>
|
||||
</ToolBarTray>
|
||||
<Grid Grid.Column="1"
|
||||
ClipToBounds="True"
|
||||
MouseWheel="{s:Action CanvasMouseWheel}"
|
||||
MouseUp="{s:Action CanvasMouseUp}"
|
||||
MouseDown="{s:Action CanvasMouseDown}"
|
||||
MouseMove="{s:Action CanvasMouseMove}"
|
||||
Cursor="{Binding ActiveToolViewModel.Cursor}"
|
||||
utilities:SizeObserver.Observe="True"
|
||||
utilities:SizeObserver.ObservedWidth="{Binding PanZoomViewModel.CanvasWidth, Mode=OneWayToSource}"
|
||||
utilities:SizeObserver.ObservedHeight="{Binding PanZoomViewModel.CanvasHeight, Mode=OneWayToSource}">
|
||||
|
||||
<Grid.Background>
|
||||
<VisualBrush TileMode="Tile" Stretch="Uniform" Viewport="{Binding PanZoomViewModel.BackgroundViewport}" ViewportUnits="Absolute">
|
||||
<VisualBrush.Visual>
|
||||
<Grid Width="25" Height="25">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Rectangle Grid.Row="0" Grid.Column="0" Fill="Black" Opacity="0.15" />
|
||||
<Rectangle Grid.Row="0" Grid.Column="1" />
|
||||
<Rectangle Grid.Row="1" Grid.Column="0" />
|
||||
<Rectangle Grid.Row="1" Grid.Column="1" Fill="Black" Opacity="0.15" />
|
||||
</Grid>
|
||||
</VisualBrush.Visual>
|
||||
</VisualBrush>
|
||||
</Grid.Background>
|
||||
|
||||
<Grid Name="DeviceDisplayGrid">
|
||||
<Grid.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform ScaleX="{Binding PanZoomViewModel.Zoom}" ScaleY="{Binding PanZoomViewModel.Zoom}" />
|
||||
<TranslateTransform X="{Binding PanZoomViewModel.PanX}" Y="{Binding PanZoomViewModel.PanY}" />
|
||||
</TransformGroup>
|
||||
</Grid.RenderTransform>
|
||||
<ItemsControl ItemsSource="{Binding Devices}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Canvas />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemContainerStyle>
|
||||
<Style TargetType="ContentPresenter">
|
||||
<Setter Property="Canvas.Left" Value="{Binding X}" />
|
||||
<Setter Property="Canvas.Top" Value="{Binding Y}" />
|
||||
</Style>
|
||||
</ItemsControl.ItemContainerStyle>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<controls:DeviceVisualizer Device="{Binding}"
|
||||
ShowColors="True"
|
||||
HighlightedLeds="{Binding DataContext.HighlightedLeds, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}, Mode=OneWay}" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
|
||||
<Grid Name="EditorDisplayGrid">
|
||||
<Grid.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform ScaleX="{Binding PanZoomViewModel.Zoom}" ScaleY="{Binding PanZoomViewModel.Zoom}" />
|
||||
<TranslateTransform X="{Binding PanZoomViewModel.PanX}" Y="{Binding PanZoomViewModel.PanY}" />
|
||||
</TransformGroup>
|
||||
</Grid.RenderTransform>
|
||||
<ItemsControl ItemsSource="{Binding CanvasViewModels}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Canvas />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemContainerStyle>
|
||||
<Style TargetType="ContentPresenter">
|
||||
<Setter Property="Canvas.Left" Value="{Binding X}" />
|
||||
<Setter Property="Canvas.Top" Value="{Binding Y}" />
|
||||
</Style>
|
||||
</ItemsControl.ItemContainerStyle>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<ContentControl s:View.Model="{Binding}" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
|
||||
<StackPanel ZIndex="1" VerticalAlignment="Bottom" HorizontalAlignment="Left" Margin="10">
|
||||
<materialDesign:Card Padding="8">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<CheckBox Style="{StaticResource MaterialDesignCheckBox}" IsChecked="{Binding OnlyShowSelectedShape.Value}" IsEnabled="False">
|
||||
Only show shape of selected layer (NYI)
|
||||
</CheckBox>
|
||||
<CheckBox Style="{StaticResource MaterialDesignCheckBox}" Margin="10 0 0 0" IsChecked="{Binding HighlightSelectedLayer.Value}">
|
||||
Highlight LEDs of selected layer
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</materialDesign:Card>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Vertical" VerticalAlignment="Bottom" HorizontalAlignment="Right"
|
||||
Margin="10" ZIndex="1">
|
||||
<Slider Margin="0,0,14,0"
|
||||
Orientation="Vertical"
|
||||
Minimum="10"
|
||||
Maximum="250"
|
||||
Height="100"
|
||||
FocusVisualStyle="{x:Null}"
|
||||
Value="{Binding PanZoomViewModel.ZoomPercentage}"
|
||||
Style="{StaticResource MaterialDesignDiscreteSlider}" />
|
||||
<Button Command="{s:Action ResetZoomAndPan}"
|
||||
Style="{StaticResource MaterialDesignFloatingActionMiniButton}"
|
||||
HorizontalAlignment="Right"
|
||||
ToolTip="Reset zoom & position">
|
||||
<materialDesign:PackIcon Kind="ImageFilterCenterFocus" Height="24" Width="24" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Loading indicator -->
|
||||
<Grid Background="{StaticResource MaterialDesignPaper}" Style="{StaticResource InitializingFade}" d:IsHidden="True">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<TextBlock FontSize="16" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
Initializing LED visualization...
|
||||
</TextBlock>
|
||||
<ProgressBar Style="{StaticResource MaterialDesignCircularProgressBar}" Value="0" IsIndeterminate="True" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,464 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using Artemis.Core.Events;
|
||||
using Artemis.Core.Models.Profile;
|
||||
using Artemis.Core.Models.Surface;
|
||||
using Artemis.Core.Plugins.Models;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.Core.Services.Storage.Interfaces;
|
||||
using Artemis.UI.Events;
|
||||
using Artemis.UI.Extensions;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Screens.ProfileEditor.Visualization.Tools;
|
||||
using Artemis.UI.Screens.Shared;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Visualization
|
||||
{
|
||||
public class ProfileViewModel : ProfileEditorPanelViewModel, IHandle<MainWindowFocusChangedEvent>, IHandle<MainWindowKeyEvent>
|
||||
{
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private readonly IProfileLayerVmFactory _profileLayerVmFactory;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ISurfaceService _surfaceService;
|
||||
private readonly IVisualizationToolVmFactory _visualizationToolVmFactory;
|
||||
|
||||
private int _activeToolIndex;
|
||||
private VisualizationToolViewModel _activeToolViewModel;
|
||||
private bool _canApplyToLayer;
|
||||
private bool _canSelectEditTool;
|
||||
private BindableCollection<CanvasViewModel> _canvasViewModels;
|
||||
private BindableCollection<ArtemisDevice> _devices;
|
||||
private BindableCollection<ArtemisLed> _highlightedLeds;
|
||||
private PluginSetting<bool> _highlightSelectedLayer;
|
||||
private bool _isInitializing;
|
||||
private PluginSetting<bool> _onlyShowSelectedShape;
|
||||
private PanZoomViewModel _panZoomViewModel;
|
||||
private Layer _previousSelectedLayer;
|
||||
private int _previousTool;
|
||||
private BindableCollection<ArtemisLed> _selectedLeds;
|
||||
|
||||
public ProfileViewModel(IProfileEditorService profileEditorService,
|
||||
ISurfaceService surfaceService,
|
||||
ISettingsService settingsService,
|
||||
IEventAggregator eventAggregator,
|
||||
IVisualizationToolVmFactory visualizationToolVmFactory,
|
||||
IProfileLayerVmFactory profileLayerVmFactory)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
_surfaceService = surfaceService;
|
||||
_settingsService = settingsService;
|
||||
_visualizationToolVmFactory = visualizationToolVmFactory;
|
||||
_profileLayerVmFactory = profileLayerVmFactory;
|
||||
|
||||
Execute.OnUIThreadSync(() =>
|
||||
{
|
||||
PanZoomViewModel = new PanZoomViewModel {LimitToZero = false};
|
||||
|
||||
CanvasViewModels = new BindableCollection<CanvasViewModel>();
|
||||
Devices = new BindableCollection<ArtemisDevice>();
|
||||
HighlightedLeds = new BindableCollection<ArtemisLed>();
|
||||
SelectedLeds = new BindableCollection<ArtemisLed>();
|
||||
});
|
||||
|
||||
ApplySurfaceConfiguration(_surfaceService.ActiveSurface);
|
||||
ActivateToolByIndex(0);
|
||||
|
||||
eventAggregator.Subscribe(this);
|
||||
}
|
||||
|
||||
public bool IsInitializing
|
||||
{
|
||||
get => _isInitializing;
|
||||
private set => SetAndNotify(ref _isInitializing, value);
|
||||
}
|
||||
|
||||
public bool CanSelectEditTool
|
||||
{
|
||||
get => _canSelectEditTool;
|
||||
set => SetAndNotify(ref _canSelectEditTool, value);
|
||||
}
|
||||
|
||||
public PanZoomViewModel PanZoomViewModel
|
||||
{
|
||||
get => _panZoomViewModel;
|
||||
set => SetAndNotify(ref _panZoomViewModel, value);
|
||||
}
|
||||
|
||||
public BindableCollection<CanvasViewModel> CanvasViewModels
|
||||
{
|
||||
get => _canvasViewModels;
|
||||
set => SetAndNotify(ref _canvasViewModels, value);
|
||||
}
|
||||
|
||||
public BindableCollection<ArtemisDevice> Devices
|
||||
{
|
||||
get => _devices;
|
||||
set => SetAndNotify(ref _devices, value);
|
||||
}
|
||||
|
||||
public BindableCollection<ArtemisLed> HighlightedLeds
|
||||
{
|
||||
get => _highlightedLeds;
|
||||
set => SetAndNotify(ref _highlightedLeds, value);
|
||||
}
|
||||
|
||||
public BindableCollection<ArtemisLed> SelectedLeds
|
||||
{
|
||||
get => _selectedLeds;
|
||||
set => SetAndNotify(ref _selectedLeds, value);
|
||||
}
|
||||
|
||||
public PluginSetting<bool> OnlyShowSelectedShape
|
||||
{
|
||||
get => _onlyShowSelectedShape;
|
||||
set => SetAndNotify(ref _onlyShowSelectedShape, value);
|
||||
}
|
||||
|
||||
public PluginSetting<bool> HighlightSelectedLayer
|
||||
{
|
||||
get => _highlightSelectedLayer;
|
||||
set => SetAndNotify(ref _highlightSelectedLayer, value);
|
||||
}
|
||||
|
||||
public VisualizationToolViewModel ActiveToolViewModel
|
||||
{
|
||||
get => _activeToolViewModel;
|
||||
private set
|
||||
{
|
||||
// Remove the tool from the canvas
|
||||
if (_activeToolViewModel != null)
|
||||
{
|
||||
lock (CanvasViewModels)
|
||||
{
|
||||
CanvasViewModels.Remove(_activeToolViewModel);
|
||||
NotifyOfPropertyChange(() => CanvasViewModels);
|
||||
}
|
||||
}
|
||||
|
||||
// Set the new tool
|
||||
SetAndNotify(ref _activeToolViewModel, value);
|
||||
// Add the new tool to the canvas
|
||||
if (_activeToolViewModel != null)
|
||||
{
|
||||
lock (CanvasViewModels)
|
||||
{
|
||||
CanvasViewModels.Add(_activeToolViewModel);
|
||||
NotifyOfPropertyChange(() => CanvasViewModels);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int ActiveToolIndex
|
||||
{
|
||||
get => _activeToolIndex;
|
||||
set
|
||||
{
|
||||
if (!SetAndNotify(ref _activeToolIndex, value)) return;
|
||||
ActivateToolByIndex(value);
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanApplyToLayer
|
||||
{
|
||||
get => _canApplyToLayer;
|
||||
set => SetAndNotify(ref _canApplyToLayer, value);
|
||||
}
|
||||
|
||||
public List<ArtemisLed> GetLedsInRectangle(Rect selectedRect)
|
||||
{
|
||||
return Devices.SelectMany(d => d.Leds)
|
||||
.Where(led => led.RgbLed.AbsoluteLedRectangle.ToWindowsRect(1).IntersectsWith(selectedRect))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
protected override void OnInitialActivate()
|
||||
{
|
||||
OnlyShowSelectedShape = _settingsService.GetSetting("ProfileEditor.OnlyShowSelectedShape", true);
|
||||
HighlightSelectedLayer = _settingsService.GetSetting("ProfileEditor.HighlightSelectedLayer", true);
|
||||
|
||||
HighlightSelectedLayer.SettingChanged += HighlightSelectedLayerOnSettingChanged;
|
||||
_surfaceService.ActiveSurfaceConfigurationSelected += OnActiveSurfaceConfigurationSelected;
|
||||
_profileEditorService.ProfileSelected += OnProfileSelected;
|
||||
_profileEditorService.ProfileElementSelected += OnProfileElementSelected;
|
||||
_profileEditorService.SelectedProfileElementUpdated += OnSelectedProfileElementUpdated;
|
||||
|
||||
base.OnInitialActivate();
|
||||
}
|
||||
|
||||
protected override void OnClose()
|
||||
{
|
||||
HighlightSelectedLayer.SettingChanged -= HighlightSelectedLayerOnSettingChanged;
|
||||
_surfaceService.ActiveSurfaceConfigurationSelected -= OnActiveSurfaceConfigurationSelected;
|
||||
_profileEditorService.ProfileSelected -= OnProfileSelected;
|
||||
_profileEditorService.ProfileElementSelected -= OnProfileElementSelected;
|
||||
_profileEditorService.SelectedProfileElementUpdated -= OnSelectedProfileElementUpdated;
|
||||
if (_previousSelectedLayer != null)
|
||||
_previousSelectedLayer.LayerBrushUpdated -= SelectedLayerOnLayerBrushUpdated;
|
||||
|
||||
OnlyShowSelectedShape.Save();
|
||||
HighlightSelectedLayer.Save();
|
||||
|
||||
foreach (var canvasViewModel in CanvasViewModels)
|
||||
canvasViewModel.Dispose();
|
||||
CanvasViewModels.Clear();
|
||||
|
||||
base.OnClose();
|
||||
}
|
||||
|
||||
private void OnActiveSurfaceConfigurationSelected(object sender, SurfaceConfigurationEventArgs e)
|
||||
{
|
||||
ApplySurfaceConfiguration(e.Surface);
|
||||
}
|
||||
|
||||
private void ApplyActiveProfile()
|
||||
{
|
||||
var layerViewModels = CanvasViewModels.Where(vm => vm is ProfileLayerViewModel).Cast<ProfileLayerViewModel>().ToList();
|
||||
var layers = _profileEditorService.SelectedProfile?.GetAllLayers() ?? new List<Layer>();
|
||||
|
||||
// Add new layers missing a VM
|
||||
foreach (var layer in layers)
|
||||
{
|
||||
if (layerViewModels.All(vm => vm.Layer != layer))
|
||||
CanvasViewModels.Add(_profileLayerVmFactory.Create(layer, this));
|
||||
}
|
||||
|
||||
// Remove layers that no longer exist
|
||||
var toRemove = layerViewModels.Where(vm => !layers.Contains(vm.Layer));
|
||||
foreach (var profileLayerViewModel in toRemove)
|
||||
{
|
||||
profileLayerViewModel.Dispose();
|
||||
CanvasViewModels.Remove(profileLayerViewModel);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnActivate()
|
||||
{
|
||||
ApplyActiveProfile();
|
||||
}
|
||||
|
||||
private void ApplySurfaceConfiguration(ArtemisSurface surface)
|
||||
{
|
||||
Devices.Clear();
|
||||
Devices.AddRange(surface.Devices.OrderBy(d => d.ZIndex));
|
||||
}
|
||||
|
||||
private void UpdateLedsDimStatus()
|
||||
{
|
||||
HighlightedLeds.Clear();
|
||||
if (HighlightSelectedLayer.Value && _profileEditorService.SelectedProfileElement is Layer layer)
|
||||
HighlightedLeds.AddRange(layer.Leds);
|
||||
}
|
||||
|
||||
private void UpdateCanSelectEditTool()
|
||||
{
|
||||
if (_profileEditorService.SelectedProfileElement is Layer layer)
|
||||
{
|
||||
CanApplyToLayer = true;
|
||||
CanSelectEditTool = (layer.LayerBrush == null || layer.LayerBrush.SupportsTransformation) && layer.Leds.Any();
|
||||
}
|
||||
else
|
||||
{
|
||||
CanApplyToLayer = false;
|
||||
CanSelectEditTool = false;
|
||||
}
|
||||
|
||||
if (CanSelectEditTool == false && ActiveToolIndex == 1)
|
||||
ActivateToolByIndex(2);
|
||||
}
|
||||
|
||||
#region Buttons
|
||||
|
||||
private void ActivateToolByIndex(int value)
|
||||
{
|
||||
// Consider using DI if dependencies start to add up
|
||||
switch (value)
|
||||
{
|
||||
case 0:
|
||||
ActiveToolViewModel = _visualizationToolVmFactory.ViewpointMoveToolViewModel(this);
|
||||
break;
|
||||
case 1:
|
||||
ActiveToolViewModel = _visualizationToolVmFactory.EditToolViewModel(this);
|
||||
break;
|
||||
case 2:
|
||||
ActiveToolViewModel = _visualizationToolVmFactory.SelectionToolViewModel(this);
|
||||
break;
|
||||
case 3:
|
||||
ActiveToolViewModel = _visualizationToolVmFactory.SelectionRemoveToolViewModel(this);
|
||||
break;
|
||||
}
|
||||
|
||||
ActiveToolIndex = value;
|
||||
}
|
||||
|
||||
public void ResetZoomAndPan()
|
||||
{
|
||||
PanZoomViewModel.Reset();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Mouse
|
||||
|
||||
public void CanvasMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
((IInputElement) sender).CaptureMouse();
|
||||
ActiveToolViewModel?.MouseDown(sender, e);
|
||||
}
|
||||
|
||||
public void CanvasMouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
((IInputElement) sender).ReleaseMouseCapture();
|
||||
ActiveToolViewModel?.MouseUp(sender, e);
|
||||
}
|
||||
|
||||
public void CanvasMouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
ActiveToolViewModel?.MouseMove(sender, e);
|
||||
}
|
||||
|
||||
public void CanvasMouseWheel(object sender, MouseWheelEventArgs e)
|
||||
{
|
||||
PanZoomViewModel.ProcessMouseScroll(sender, e);
|
||||
ActiveToolViewModel?.MouseWheel(sender, e);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Context menu actions
|
||||
|
||||
public void CreateLayer()
|
||||
{
|
||||
}
|
||||
|
||||
public void ApplyToLayer()
|
||||
{
|
||||
if (!(_profileEditorService.SelectedProfileElement is Layer layer))
|
||||
return;
|
||||
|
||||
layer.ClearLeds();
|
||||
layer.AddLeds(SelectedLeds);
|
||||
|
||||
_profileEditorService.UpdateSelectedProfileElement();
|
||||
}
|
||||
|
||||
public void SelectAll()
|
||||
{
|
||||
SelectedLeds.Clear();
|
||||
SelectedLeds.AddRange(Devices.SelectMany(d => d.Leds));
|
||||
}
|
||||
|
||||
public void InverseSelection()
|
||||
{
|
||||
var current = SelectedLeds.ToList();
|
||||
SelectedLeds.Clear();
|
||||
SelectedLeds.AddRange(Devices.SelectMany(d => d.Leds).Except(current));
|
||||
}
|
||||
|
||||
public void ClearSelection()
|
||||
{
|
||||
SelectedLeds.Clear();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event handlers
|
||||
|
||||
private void HighlightSelectedLayerOnSettingChanged(object sender, EventArgs e)
|
||||
{
|
||||
UpdateLedsDimStatus();
|
||||
}
|
||||
|
||||
private void OnProfileSelected(object sender, EventArgs e)
|
||||
{
|
||||
ApplyActiveProfile();
|
||||
}
|
||||
|
||||
private void OnProfileElementSelected(object sender, EventArgs e)
|
||||
{
|
||||
if (_previousSelectedLayer != null)
|
||||
_previousSelectedLayer.LayerBrushUpdated -= SelectedLayerOnLayerBrushUpdated;
|
||||
if (_profileEditorService.SelectedProfileElement is Layer layer)
|
||||
{
|
||||
_previousSelectedLayer = layer;
|
||||
_previousSelectedLayer.LayerBrushUpdated += SelectedLayerOnLayerBrushUpdated;
|
||||
}
|
||||
else
|
||||
_previousSelectedLayer = null;
|
||||
|
||||
UpdateLedsDimStatus();
|
||||
UpdateCanSelectEditTool();
|
||||
}
|
||||
|
||||
private void SelectedLayerOnLayerBrushUpdated(object sender, EventArgs e)
|
||||
{
|
||||
UpdateCanSelectEditTool();
|
||||
}
|
||||
|
||||
private void OnSelectedProfileElementUpdated(object sender, EventArgs e)
|
||||
{
|
||||
ApplyActiveProfile();
|
||||
UpdateLedsDimStatus();
|
||||
if (_profileEditorService.SelectedProfileElement is Layer layer)
|
||||
{
|
||||
CanApplyToLayer = true;
|
||||
CanSelectEditTool = layer.Leds.Any();
|
||||
}
|
||||
else
|
||||
{
|
||||
CanApplyToLayer = false;
|
||||
CanSelectEditTool = false;
|
||||
}
|
||||
|
||||
if (CanSelectEditTool == false && ActiveToolIndex == 1)
|
||||
ActivateToolByIndex(2);
|
||||
}
|
||||
|
||||
public void Handle(MainWindowFocusChangedEvent message)
|
||||
{
|
||||
// if (PauseRenderingOnFocusLoss == null || ScreenState != ScreenState.Active)
|
||||
// return;
|
||||
//
|
||||
// try
|
||||
// {
|
||||
// if (PauseRenderingOnFocusLoss.Value && !message.IsFocused)
|
||||
// _updateTrigger.Stop();
|
||||
// else if (PauseRenderingOnFocusLoss.Value && message.IsFocused)
|
||||
// _updateTrigger.Start();
|
||||
// }
|
||||
// catch (NullReferenceException)
|
||||
// {
|
||||
// // TODO: Remove when fixed in RGB.NET, or avoid double stopping
|
||||
// }
|
||||
}
|
||||
|
||||
public void Handle(MainWindowKeyEvent message)
|
||||
{
|
||||
if (message.KeyDown)
|
||||
{
|
||||
if (ActiveToolIndex != 0)
|
||||
{
|
||||
_previousTool = ActiveToolIndex;
|
||||
if ((message.EventArgs.Key == Key.LeftCtrl || message.EventArgs.Key == Key.RightCtrl) && message.EventArgs.IsDown)
|
||||
ActivateToolByIndex(0);
|
||||
}
|
||||
|
||||
ActiveToolViewModel?.KeyDown(message.EventArgs);
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((message.EventArgs.Key == Key.LeftCtrl || message.EventArgs.Key == Key.RightCtrl) && message.EventArgs.IsUp)
|
||||
ActivateToolByIndex(_previousTool);
|
||||
|
||||
ActiveToolViewModel?.KeyUp(message.EventArgs);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.Visualization.Tools.EditToolView"
|
||||
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.Visualization.Tools"
|
||||
xmlns:userControls="clr-namespace:Artemis.UI.Screens.ProfileEditor.Visualization.UserControls"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
xmlns:tools="clr-namespace:Artemis.UI.Screens.ProfileEditor.Visualization.Tools"
|
||||
xmlns:userControls1="clr-namespace:Artemis.UI.Screens.ProfileEditor.Visualization.UserControls"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance {x:Type tools:EditToolViewModel}}">
|
||||
<Canvas UseLayoutRounding="False">
|
||||
<userControls1:LayerShapeControl Zoom="{Binding ProfileViewModel.PanZoomViewModel.Zoom}"
|
||||
ShapePath="{Binding ShapePath}"
|
||||
ShapeAnchor="{Binding ShapeAnchor}"
|
||||
ShapeGeometry="{Binding ShapeGeometry}"
|
||||
RotateMouseDown="{s:Action RotateMouseDown}"
|
||||
RotateMouseUp="{s:Action RotateMouseUp}"
|
||||
RotateMouseMove="{s:Action RotateMouseMove}"
|
||||
ResizeMouseDown="{s:Action ResizeMouseDown}"
|
||||
ResizeMouseUp="{s:Action ResizeMouseUp}"
|
||||
ResizeMouseMove="{s:Action ResizeMouseMove}"
|
||||
MoveMouseDown="{s:Action MoveMouseDown}"
|
||||
MoveMouseUp="{s:Action MoveMouseUp}"
|
||||
MoveMouseMove="{s:Action MoveMouseMove}" />
|
||||
</Canvas>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,420 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using Artemis.Core.Models.Profile;
|
||||
using Artemis.UI.Events;
|
||||
using Artemis.UI.Services.Interfaces;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using SkiaSharp;
|
||||
using SkiaSharp.Views.WPF;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Visualization.Tools
|
||||
{
|
||||
public class EditToolViewModel : VisualizationToolViewModel
|
||||
{
|
||||
private readonly ILayerEditorService _layerEditorService;
|
||||
private SKPoint _dragOffset;
|
||||
private SKPoint _dragStart;
|
||||
private SKPoint _topLeft;
|
||||
private SKPath _shapePath;
|
||||
private SKPoint _shapeAnchor;
|
||||
private RectangleGeometry _shapeGeometry;
|
||||
|
||||
public EditToolViewModel(ProfileViewModel profileViewModel, IProfileEditorService profileEditorService, ILayerEditorService layerEditorService)
|
||||
: base(profileViewModel, profileEditorService)
|
||||
{
|
||||
_layerEditorService = layerEditorService;
|
||||
Cursor = Cursors.Arrow;
|
||||
Update();
|
||||
|
||||
profileEditorService.ProfileSelected += (sender, args) => Update();
|
||||
profileEditorService.ProfileElementSelected += (sender, args) => Update();
|
||||
profileEditorService.SelectedProfileElementUpdated += (sender, args) => Update();
|
||||
profileEditorService.ProfilePreviewUpdated += (sender, args) => Update();
|
||||
}
|
||||
|
||||
public SKPath ShapePath
|
||||
{
|
||||
get => _shapePath;
|
||||
set => SetAndNotify(ref _shapePath, value);
|
||||
}
|
||||
|
||||
public SKPoint ShapeAnchor
|
||||
{
|
||||
get => _shapeAnchor;
|
||||
set => SetAndNotify(ref _shapeAnchor, value);
|
||||
}
|
||||
|
||||
public RectangleGeometry ShapeGeometry
|
||||
{
|
||||
get => _shapeGeometry;
|
||||
set => SetAndNotify(ref _shapeGeometry, value);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!(ProfileEditorService.SelectedProfileElement is Layer layer))
|
||||
return;
|
||||
|
||||
ShapePath = _layerEditorService.GetLayerPath(layer, true, true, true);
|
||||
ShapeAnchor = _layerEditorService.GetLayerAnchorPosition(layer).ToSKPoint();
|
||||
Execute.PostToUIThread(() =>
|
||||
{
|
||||
var shapeGeometry = new RectangleGeometry(_layerEditorService.GetLayerBounds(layer))
|
||||
{
|
||||
Transform = _layerEditorService.GetLayerTransformGroup(layer)
|
||||
};
|
||||
shapeGeometry.Freeze();
|
||||
ShapeGeometry = shapeGeometry;
|
||||
});
|
||||
|
||||
// Store the last top-left for easy later on
|
||||
_topLeft = _layerEditorService.GetLayerPath(layer, true, true, true).Points[0];
|
||||
}
|
||||
|
||||
#region Rotation
|
||||
|
||||
private bool _rotating;
|
||||
private float _previousDragAngle;
|
||||
|
||||
public void RotateMouseDown(object sender, ShapeControlEventArgs e)
|
||||
{
|
||||
_rotating = true;
|
||||
if (ProfileEditorService.SelectedProfileElement is Layer layer)
|
||||
_previousDragAngle = CalculateAngle(layer, sender, e.MouseEventArgs);
|
||||
else
|
||||
_previousDragAngle = 0;
|
||||
}
|
||||
|
||||
public void RotateMouseUp(object sender, ShapeControlEventArgs e)
|
||||
{
|
||||
ProfileEditorService.UpdateSelectedProfileElement();
|
||||
_rotating = false;
|
||||
}
|
||||
|
||||
public void RotateMouseMove(object sender, ShapeControlEventArgs e)
|
||||
{
|
||||
if (!_rotating || !(ProfileEditorService.SelectedProfileElement is Layer layer))
|
||||
return;
|
||||
|
||||
var previousDragAngle = _previousDragAngle;
|
||||
var newRotation = CalculateAngle(layer, sender, e.MouseEventArgs);
|
||||
_previousDragAngle = newRotation;
|
||||
|
||||
// Allow the user to rotate the shape in increments of 5
|
||||
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
|
||||
newRotation = (float) Math.Round(newRotation / 5f) * 5f;
|
||||
|
||||
var difference = newRotation - previousDragAngle;
|
||||
if (difference < -350)
|
||||
difference += 360;
|
||||
else if (difference > 350)
|
||||
difference -= 360;
|
||||
|
||||
newRotation = layer.Transform.Rotation.CurrentValue + difference;
|
||||
|
||||
// Round the end-result to increments of 5 as well, to avoid staying on an offset
|
||||
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
|
||||
newRotation = (float) Math.Round(newRotation / 5f) * 5f;
|
||||
else
|
||||
newRotation = (float) Math.Round(newRotation, 2, MidpointRounding.AwayFromZero);
|
||||
layer.Transform.Rotation.SetCurrentValue(newRotation, ProfileEditorService.CurrentTime);
|
||||
ProfileEditorService.UpdateProfilePreview();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Size
|
||||
|
||||
private bool _isResizing;
|
||||
private SKSize _dragStartScale;
|
||||
|
||||
public void ResizeMouseDown(object sender, ShapeControlEventArgs e)
|
||||
{
|
||||
if (_isResizing || !(ProfileEditorService.SelectedProfileElement is Layer layer))
|
||||
return;
|
||||
|
||||
var dragStart = GetRelativePosition(sender, e.MouseEventArgs).ToSKPoint();
|
||||
_dragOffset = _layerEditorService.GetDragOffset(layer, dragStart);
|
||||
_dragStart = dragStart + _dragOffset;
|
||||
_dragStartScale = layer.Transform.Scale.CurrentValue;
|
||||
|
||||
_isResizing = true;
|
||||
}
|
||||
|
||||
public void ResizeMouseUp(object sender, ShapeControlEventArgs e)
|
||||
{
|
||||
ProfileEditorService.UpdateSelectedProfileElement();
|
||||
_isResizing = false;
|
||||
}
|
||||
|
||||
public void ResizeMouseMove(object sender, ShapeControlEventArgs e)
|
||||
{
|
||||
if (!_isResizing || !(ProfileEditorService.SelectedProfileElement is Layer layer))
|
||||
return;
|
||||
|
||||
float width, height;
|
||||
var position = GetRelativePosition(sender, e.MouseEventArgs).ToSKPoint() + _dragOffset;
|
||||
switch (e.ShapeControlPoint)
|
||||
{
|
||||
case ShapeControlPoint.TopLeft:
|
||||
height = VerticalResize(layer, position, ResizeOrigin.Top);
|
||||
width = HorizontalResize(layer, position, ResizeOrigin.Left);
|
||||
break;
|
||||
case ShapeControlPoint.TopRight:
|
||||
height = VerticalResize(layer, position, ResizeOrigin.Top);
|
||||
width = HorizontalResize(layer, position, ResizeOrigin.Right);
|
||||
break;
|
||||
case ShapeControlPoint.BottomRight:
|
||||
height = VerticalResize(layer, position, ResizeOrigin.Bottom);
|
||||
width = HorizontalResize(layer, position, ResizeOrigin.Right);
|
||||
break;
|
||||
case ShapeControlPoint.BottomLeft:
|
||||
height = VerticalResize(layer, position, ResizeOrigin.Bottom);
|
||||
width = HorizontalResize(layer, position, ResizeOrigin.Left);
|
||||
break;
|
||||
case ShapeControlPoint.TopCenter:
|
||||
height = VerticalResize(layer, position, ResizeOrigin.Top);
|
||||
width = layer.Transform.Scale.CurrentValue.Width;
|
||||
break;
|
||||
case ShapeControlPoint.RightCenter:
|
||||
width = HorizontalResize(layer, position, ResizeOrigin.Right);
|
||||
height = layer.Transform.Scale.CurrentValue.Height;
|
||||
break;
|
||||
case ShapeControlPoint.BottomCenter:
|
||||
width = layer.Transform.Scale.CurrentValue.Width;
|
||||
height = VerticalResize(layer, position, ResizeOrigin.Bottom);
|
||||
break;
|
||||
case ShapeControlPoint.LeftCenter:
|
||||
width = HorizontalResize(layer, position, ResizeOrigin.Left);
|
||||
height = layer.Transform.Scale.CurrentValue.Height;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
// Make the sides even if shift is held down
|
||||
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift) && e.ShapeControlPoint < ShapeControlPoint.TopCenter)
|
||||
{
|
||||
var bounds = _layerEditorService.GetLayerBounds(layer);
|
||||
var smallestSide = Math.Min(bounds.Width * width, bounds.Height * height);
|
||||
width = (float) Math.Round(1.0 / bounds.Width * smallestSide, 2, MidpointRounding.AwayFromZero);
|
||||
height = (float) Math.Round(1.0 / bounds.Height * smallestSide, 2, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
layer.Transform.Scale.SetCurrentValue(new SKSize(width, height), ProfileEditorService.CurrentTime);
|
||||
ProfileEditorService.UpdateProfilePreview();
|
||||
}
|
||||
|
||||
private float HorizontalResize(Layer layer, SKPoint position, ResizeOrigin origin)
|
||||
{
|
||||
// Apply rotation to the mouse
|
||||
var points = UnTransformPoints(new[] {position, _dragStart}, layer, ShapeAnchor, false);
|
||||
position = points[0];
|
||||
var dragStart = points[1];
|
||||
|
||||
var shapePath = _layerEditorService.GetLayerPath(layer, true, false, false);
|
||||
var scalePerPixel = 1f / shapePath.Bounds.Width;
|
||||
var anchorDistance = origin == ResizeOrigin.Left
|
||||
? shapePath.Bounds.Left - ShapeAnchor.X
|
||||
: shapePath.Bounds.Right - ShapeAnchor.X;
|
||||
var anchorOffset = anchorDistance / shapePath.Bounds.Width;
|
||||
|
||||
var pixelsToAdd = (position - dragStart).X / anchorOffset;
|
||||
var scaleToAdd = scalePerPixel * pixelsToAdd * 100f;
|
||||
|
||||
return (float) Math.Round(Math.Max(0, _dragStartScale.Width + scaleToAdd), 2, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
private float VerticalResize(Layer layer, SKPoint position, ResizeOrigin origin)
|
||||
{
|
||||
// Apply rotation to the mouse
|
||||
var points = UnTransformPoints(new[] {position, _dragStart}, layer, ShapeAnchor, false);
|
||||
position = points[0];
|
||||
var dragStart = points[1];
|
||||
|
||||
var shapePath = _layerEditorService.GetLayerPath(layer, true, false, false);
|
||||
var scalePerPixel = 1f / shapePath.Bounds.Height;
|
||||
var anchorDistance = origin == ResizeOrigin.Top
|
||||
? shapePath.Bounds.Top - ShapeAnchor.Y
|
||||
: shapePath.Bounds.Bottom - ShapeAnchor.Y;
|
||||
var anchorOffset = anchorDistance / shapePath.Bounds.Height;
|
||||
|
||||
var pixelsToAdd = (position - dragStart).Y / anchorOffset;
|
||||
var scaleToAdd = scalePerPixel * pixelsToAdd * 100f;
|
||||
|
||||
return (float) Math.Round(Math.Max(0, _dragStartScale.Height + scaleToAdd), 2, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Position
|
||||
|
||||
private bool _movingShape;
|
||||
private bool _movingAnchor;
|
||||
private bool _draggingHorizontally;
|
||||
private bool _draggingVertically;
|
||||
private SKPoint _dragStartAnchor;
|
||||
|
||||
public void MoveMouseDown(object sender, ShapeControlEventArgs e)
|
||||
{
|
||||
if (!(ProfileEditorService.SelectedProfileElement is Layer layer))
|
||||
return;
|
||||
|
||||
if (e.ShapeControlPoint == ShapeControlPoint.LayerShape)
|
||||
{
|
||||
var dragStart = GetRelativePosition(sender, e.MouseEventArgs).ToSKPoint();
|
||||
_dragOffset = _layerEditorService.GetDragOffset(layer, dragStart);
|
||||
_dragStart = dragStart + _dragOffset;
|
||||
_movingShape = true;
|
||||
}
|
||||
else if (e.ShapeControlPoint == ShapeControlPoint.Anchor)
|
||||
{
|
||||
var dragStartPosition = GetRelativePosition(sender, e.MouseEventArgs).ToSKPoint();
|
||||
|
||||
// Mouse doesn't care about rotation so get the layer path without rotation
|
||||
var path = _layerEditorService.GetLayerPath(layer, true, true, false);
|
||||
var topLeft = path.Points[0];
|
||||
// Measure from the top-left of the shape (without rotation)
|
||||
_dragOffset = topLeft + (dragStartPosition - topLeft);
|
||||
// Get the absolute layer anchor and make it relative to the unrotated shape
|
||||
_dragStartAnchor = _layerEditorService.GetLayerAnchorPosition(layer).ToSKPoint() - topLeft;
|
||||
// Ensure the anchor starts in the center of the shape it is now relative to
|
||||
_dragStartAnchor.X -= path.Bounds.Width / 2f;
|
||||
_dragStartAnchor.Y -= path.Bounds.Height / 2f;
|
||||
_movingAnchor = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void MoveMouseUp(object sender, ShapeControlEventArgs e)
|
||||
{
|
||||
ProfileEditorService.UpdateSelectedProfileElement();
|
||||
_movingShape = false;
|
||||
_movingAnchor = false;
|
||||
}
|
||||
|
||||
public void MoveMouseMove(object sender, ShapeControlEventArgs e)
|
||||
{
|
||||
if (_movingShape)
|
||||
MoveShape(sender, e.MouseEventArgs);
|
||||
else if (_movingAnchor)
|
||||
MoveAnchor(sender, e.MouseEventArgs);
|
||||
}
|
||||
|
||||
public void MoveShape(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (!(ProfileEditorService.SelectedProfileElement is Layer layer))
|
||||
return;
|
||||
|
||||
var position = GetRelativePosition(sender, e).ToSKPoint() + _dragOffset;
|
||||
// Allow the user to move the shape only horizontally or vertically when holding down shift
|
||||
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
|
||||
{
|
||||
// Keep the X position static if dragging vertically
|
||||
if (_draggingVertically)
|
||||
position.X = _dragStart.X;
|
||||
// Keep the Y position static if dragging horizontally
|
||||
else if (_draggingHorizontally)
|
||||
position.Y = _dragStart.Y;
|
||||
// Snap into place only if the mouse moved atleast a full pixel
|
||||
else if (Math.Abs(position.X - _dragStart.X) > 1 || Math.Abs(position.Y - _dragStart.Y) > 1)
|
||||
{
|
||||
// Pick between X and Y by comparing which moved the furthers from the starting point
|
||||
_draggingHorizontally = Math.Abs(position.X - _dragStart.X) > Math.Abs(position.Y - _dragStart.Y);
|
||||
_draggingVertically = Math.Abs(position.X - _dragStart.X) < Math.Abs(position.Y - _dragStart.Y);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Reset both states when shift is not held down
|
||||
else
|
||||
{
|
||||
_draggingVertically = false;
|
||||
_draggingHorizontally = false;
|
||||
}
|
||||
|
||||
// Scale down the resulting position and make it relative
|
||||
var scaled = _layerEditorService.GetScaledPoint(layer, position, true);
|
||||
// Round and update the position property
|
||||
layer.Transform.Position.SetCurrentValue(RoundPoint(scaled, 3), ProfileEditorService.CurrentTime);
|
||||
|
||||
ProfileEditorService.UpdateProfilePreview();
|
||||
}
|
||||
|
||||
public void MoveAnchor(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (!_movingAnchor || !(ProfileEditorService.SelectedProfileElement is Layer layer))
|
||||
return;
|
||||
|
||||
// The start anchor is relative to an unrotated version of the shape
|
||||
var start = _dragStartAnchor;
|
||||
// Add the current position to the start anchor to determine the new position
|
||||
var current = start + (GetRelativePosition(sender, e).ToSKPoint() - _dragOffset);
|
||||
// In order to keep the mouse movement unrotated, counter-act the active rotation
|
||||
var countered = UnTransformPoints(new[] {start, current}, layer, start, true);
|
||||
var scaled = _layerEditorService.GetScaledPoint(layer, countered[1], false);
|
||||
|
||||
// Update the anchor point, this causes the shape to move
|
||||
layer.Transform.AnchorPoint.SetCurrentValue(RoundPoint(scaled, 3), ProfileEditorService.CurrentTime);
|
||||
// TopLeft is not updated yet and acts as a snapshot of the top-left before changing the anchor
|
||||
var path = _layerEditorService.GetLayerPath(layer, true, true, true);
|
||||
// Calculate the (scaled) difference between the old and now position
|
||||
var difference = _layerEditorService.GetScaledPoint(layer, _topLeft - path.Points[0], false);
|
||||
// Apply the difference so that the shape effectively stays in place
|
||||
layer.Transform.Position.SetCurrentValue(RoundPoint(layer.Transform.Position.CurrentValue + difference, 3), ProfileEditorService.CurrentTime);
|
||||
|
||||
ProfileEditorService.UpdateProfilePreview();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private methods
|
||||
|
||||
private SKPoint RoundPoint(SKPoint point, int decimals)
|
||||
{
|
||||
return new SKPoint((float) Math.Round(point.X, decimals, MidpointRounding.AwayFromZero), (float) Math.Round(point.Y, decimals, MidpointRounding.AwayFromZero));
|
||||
}
|
||||
|
||||
private SKPoint[] UnTransformPoints(SKPoint[] skPoints, Layer layer, SKPoint pivot, bool includeScale)
|
||||
{
|
||||
using var counterRotatePath = new SKPath();
|
||||
counterRotatePath.AddPoly(skPoints, false);
|
||||
counterRotatePath.Transform(SKMatrix.MakeRotationDegrees(layer.Transform.Rotation.CurrentValue * -1, pivot.X, pivot.Y));
|
||||
if (includeScale)
|
||||
counterRotatePath.Transform(SKMatrix.MakeScale(1f / (layer.Transform.Scale.CurrentValue.Width / 100f), 1f / (layer.Transform.Scale.CurrentValue.Height / 100f)));
|
||||
|
||||
return counterRotatePath.Points;
|
||||
}
|
||||
|
||||
private Point GetRelativePosition(object sender, MouseEventArgs mouseEventArgs)
|
||||
{
|
||||
var parent = VisualTreeHelper.GetParent((DependencyObject) sender);
|
||||
return mouseEventArgs.GetPosition((IInputElement) parent);
|
||||
}
|
||||
|
||||
private float CalculateAngle(Layer layer, object mouseEventSender, MouseEventArgs mouseEvent)
|
||||
{
|
||||
var layerBounds = _layerEditorService.GetLayerBounds(layer);
|
||||
var start = _layerEditorService.GetLayerAnchorPosition(layer);
|
||||
var arrival = GetRelativePosition(mouseEventSender, mouseEvent);
|
||||
|
||||
var radian = (float) Math.Atan2(start.Y - arrival.Y, start.X - arrival.X);
|
||||
var angle = radian * (180f / (float) Math.PI);
|
||||
if (angle < 0f)
|
||||
angle += 360f;
|
||||
|
||||
return angle;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
internal enum ResizeOrigin
|
||||
{
|
||||
Left,
|
||||
Right,
|
||||
Top,
|
||||
Bottom
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.Visualization.Tools.SelectionRemoveToolView"
|
||||
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"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
<UserControl.Resources>
|
||||
<Style TargetType="Shape" x:Key="ShowIfMouseDown">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsMouseDown}" Value="True">
|
||||
<DataTrigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard TargetProperty="Opacity">
|
||||
<DoubleAnimation From="0" To="1" Duration="0:0:0.1" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.EnterActions>
|
||||
<DataTrigger.ExitActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard TargetProperty="Opacity">
|
||||
<DoubleAnimation From="1" To="0" Duration="0:0:0.2" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.ExitActions>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
<Canvas Background="Transparent" Width="50" Height="50">
|
||||
<Rectangle Style="{StaticResource ShowIfMouseDown}"
|
||||
Width="{Binding DragRectangle.Width}"
|
||||
Height="{Binding DragRectangle.Height}"
|
||||
Canvas.Left="{Binding DragRectangle.X}"
|
||||
Canvas.Top="{Binding DragRectangle.Y}"
|
||||
Stroke="{DynamicResource PrimaryHueLightBrush}"
|
||||
StrokeDashArray="4 4"
|
||||
StrokeThickness="1"
|
||||
Opacity="0">
|
||||
<Rectangle.Fill>
|
||||
<SolidColorBrush Color="{DynamicResource Primary400}" Opacity="0.25" />
|
||||
</Rectangle.Fill>
|
||||
</Rectangle>
|
||||
</Canvas>
|
||||
</UserControl>
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Visualization.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for SelectionRemoveToolView.xaml
|
||||
/// </summary>
|
||||
public partial class SelectionRemoveToolView : UserControl
|
||||
{
|
||||
public SelectionRemoveToolView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using Artemis.Core.Models.Profile;
|
||||
using Artemis.UI.Properties;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Visualization.Tools
|
||||
{
|
||||
public class SelectionRemoveToolViewModel : VisualizationToolViewModel
|
||||
{
|
||||
private Rect _dragRectangle;
|
||||
|
||||
public SelectionRemoveToolViewModel(ProfileViewModel profileViewModel, IProfileEditorService profileEditorService) : base(profileViewModel, profileEditorService)
|
||||
{
|
||||
using (var stream = new MemoryStream(Resources.aero_pen_min))
|
||||
{
|
||||
Cursor = new Cursor(stream);
|
||||
}
|
||||
}
|
||||
|
||||
public Rect DragRectangle
|
||||
{
|
||||
get => _dragRectangle;
|
||||
set => SetAndNotify(ref _dragRectangle, value);
|
||||
}
|
||||
|
||||
public override void MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
base.MouseUp(sender, e);
|
||||
|
||||
var position = ProfileViewModel.PanZoomViewModel.GetRelativeMousePosition(sender, e);
|
||||
var selectedRect = new Rect(MouseDownStartPosition, position);
|
||||
|
||||
// Get selected LEDs
|
||||
var selectedLeds = ProfileViewModel.GetLedsInRectangle(selectedRect);
|
||||
ProfileViewModel.SelectedLeds.Clear();
|
||||
ProfileViewModel.SelectedLeds.AddRange(selectedLeds);
|
||||
|
||||
// Apply the selection to the selected layer layer
|
||||
if (ProfileEditorService.SelectedProfileElement is Layer layer)
|
||||
{
|
||||
var remainingLeds = layer.Leds.Except(selectedLeds).ToList();
|
||||
layer.ClearLeds();
|
||||
layer.AddLeds(remainingLeds);
|
||||
|
||||
ProfileEditorService.UpdateSelectedProfileElement();
|
||||
}
|
||||
}
|
||||
|
||||
public override void MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
base.MouseMove(sender, e);
|
||||
if (!IsMouseDown)
|
||||
{
|
||||
DragRectangle = new Rect(-1, -1, 0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
var position = ProfileViewModel.PanZoomViewModel.GetRelativeMousePosition(sender, e);
|
||||
var selectedRect = new Rect(MouseDownStartPosition, position);
|
||||
var selectedLeds = ProfileViewModel.GetLedsInRectangle(selectedRect);
|
||||
|
||||
// Unless shift is held down, clear the current selection
|
||||
if (!Keyboard.IsKeyDown(Key.LeftShift) && !Keyboard.IsKeyDown(Key.RightShift))
|
||||
ProfileViewModel.SelectedLeds.Clear();
|
||||
ProfileViewModel.SelectedLeds.AddRange(selectedLeds.Except(ProfileViewModel.SelectedLeds));
|
||||
|
||||
DragRectangle = selectedRect;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.Visualization.Tools.SelectionToolView"
|
||||
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"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
<UserControl.Resources>
|
||||
<Style TargetType="Shape" x:Key="ShowIfMouseDown">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsMouseDown}" Value="True">
|
||||
<DataTrigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard TargetProperty="Opacity">
|
||||
<DoubleAnimation From="0" To="1" Duration="0:0:0.1" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.EnterActions>
|
||||
<DataTrigger.ExitActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard TargetProperty="Opacity">
|
||||
<DoubleAnimation From="1" To="0" Duration="0:0:0.2" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.ExitActions>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
<Canvas Background="Transparent" Width="50" Height="50">
|
||||
<Rectangle Style="{StaticResource ShowIfMouseDown}"
|
||||
Width="{Binding DragRectangle.Width}"
|
||||
Height="{Binding DragRectangle.Height}"
|
||||
Canvas.Left="{Binding DragRectangle.X}"
|
||||
Canvas.Top="{Binding DragRectangle.Y}"
|
||||
Stroke="{DynamicResource PrimaryHueLightBrush}"
|
||||
StrokeDashArray="4 4"
|
||||
StrokeThickness="1"
|
||||
Opacity="0">
|
||||
<Rectangle.Fill>
|
||||
<SolidColorBrush Color="{DynamicResource Primary400}" Opacity="0.25" />
|
||||
</Rectangle.Fill>
|
||||
</Rectangle>
|
||||
</Canvas>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Visualization.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for SelectionToolView.xaml
|
||||
/// </summary>
|
||||
public partial class SelectionToolView : UserControl
|
||||
{
|
||||
public SelectionToolView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using Artemis.Core.Models.Profile;
|
||||
using Artemis.Core.Services.Interfaces;
|
||||
using Artemis.UI.Properties;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Visualization.Tools
|
||||
{
|
||||
public class SelectionToolViewModel : VisualizationToolViewModel
|
||||
{
|
||||
private readonly IRenderElementService _renderElementService;
|
||||
private Rect _dragRectangle;
|
||||
|
||||
public SelectionToolViewModel(ProfileViewModel profileViewModel, IProfileEditorService profileEditorService, IRenderElementService renderElementService)
|
||||
: base(profileViewModel, profileEditorService)
|
||||
{
|
||||
_renderElementService = renderElementService;
|
||||
using (var stream = new MemoryStream(Resources.aero_crosshair))
|
||||
{
|
||||
Cursor = new Cursor(stream);
|
||||
}
|
||||
}
|
||||
|
||||
public Rect DragRectangle
|
||||
{
|
||||
get => _dragRectangle;
|
||||
set => SetAndNotify(ref _dragRectangle, value);
|
||||
}
|
||||
|
||||
public override void MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
base.MouseUp(sender, e);
|
||||
|
||||
var position = ProfileViewModel.PanZoomViewModel.GetRelativeMousePosition(sender, e);
|
||||
var selectedRect = new Rect(MouseDownStartPosition, position);
|
||||
|
||||
// Get selected LEDs
|
||||
var selectedLeds = ProfileViewModel.GetLedsInRectangle(selectedRect);
|
||||
|
||||
// Apply the selection to the selected layer layer
|
||||
if (ProfileEditorService.SelectedProfileElement is Layer layer)
|
||||
{
|
||||
// If shift is held down, add to the selection instead of replacing it
|
||||
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
|
||||
layer.AddLeds(selectedLeds.Except(layer.Leds));
|
||||
else
|
||||
{
|
||||
layer.ClearLeds();
|
||||
layer.AddLeds(selectedLeds);
|
||||
}
|
||||
|
||||
ProfileEditorService.UpdateSelectedProfileElement();
|
||||
}
|
||||
// If no layer selected, apply it to a new layer in the selected folder
|
||||
else if (ProfileEditorService.SelectedProfileElement is Folder folder)
|
||||
{
|
||||
var newLayer = _renderElementService.CreateLayer(folder.Profile, folder, "New layer");
|
||||
newLayer.AddLeds(selectedLeds);
|
||||
ProfileEditorService.ChangeSelectedProfileElement(newLayer);
|
||||
ProfileEditorService.UpdateSelectedProfileElement();
|
||||
}
|
||||
// If no folder selected, apply it to a new layer in the root folder
|
||||
else
|
||||
{
|
||||
var rootFolder = ProfileEditorService.SelectedProfile.GetRootFolder();
|
||||
var newLayer = _renderElementService.CreateLayer(rootFolder.Profile, rootFolder, "New layer");
|
||||
newLayer.AddLeds(selectedLeds);
|
||||
ProfileEditorService.ChangeSelectedProfileElement(newLayer);
|
||||
ProfileEditorService.UpdateSelectedProfileElement();
|
||||
}
|
||||
}
|
||||
|
||||
public override void MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
base.MouseMove(sender, e);
|
||||
if (!IsMouseDown)
|
||||
{
|
||||
DragRectangle = new Rect(-1, -1, 0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
var position = ProfileViewModel.PanZoomViewModel.GetRelativeMousePosition(sender, e);
|
||||
var selectedRect = new Rect(MouseDownStartPosition, position);
|
||||
var selectedLeds = ProfileViewModel.GetLedsInRectangle(selectedRect);
|
||||
|
||||
// Unless shift is held down, clear the current selection
|
||||
if (!Keyboard.IsKeyDown(Key.LeftShift) && !Keyboard.IsKeyDown(Key.RightShift))
|
||||
ProfileViewModel.SelectedLeds.Clear();
|
||||
ProfileViewModel.SelectedLeds.AddRange(selectedLeds.Except(ProfileViewModel.SelectedLeds));
|
||||
|
||||
DragRectangle = selectedRect;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.Visualization.Tools.ViewpointMoveToolView"
|
||||
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.Visualization.Tools"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
<Grid />
|
||||
</UserControl>
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Visualization.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for ViewpointMoveToolView.xaml
|
||||
/// </summary>
|
||||
public partial class ViewpointMoveToolView : UserControl
|
||||
{
|
||||
public ViewpointMoveToolView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
using System.Windows.Input;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Visualization.Tools
|
||||
{
|
||||
public class ViewpointMoveToolViewModel : VisualizationToolViewModel
|
||||
{
|
||||
public ViewpointMoveToolViewModel(ProfileViewModel profileViewModel, IProfileEditorService profileEditorService) : base(profileViewModel, profileEditorService)
|
||||
{
|
||||
Cursor = Cursors.Hand;
|
||||
ProfileViewModel.PanZoomViewModel.LastPanPosition = null;
|
||||
}
|
||||
|
||||
public override void MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
base.MouseMove(sender, e);
|
||||
ProfileViewModel.PanZoomViewModel.ProcessMouseMove(sender, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Visualization.Tools
|
||||
{
|
||||
public abstract class VisualizationToolViewModel : CanvasViewModel
|
||||
{
|
||||
private Cursor _cursor;
|
||||
private bool _isMouseDown;
|
||||
private Point _mouseDownStartPosition;
|
||||
|
||||
protected VisualizationToolViewModel(ProfileViewModel profileViewModel, IProfileEditorService profileEditorService)
|
||||
{
|
||||
// Not relevant for visualization tools as they overlay the entire canvas
|
||||
X = 0;
|
||||
Y = 0;
|
||||
|
||||
ProfileViewModel = profileViewModel;
|
||||
ProfileEditorService = profileEditorService;
|
||||
Cursor = Cursors.Arrow;
|
||||
}
|
||||
|
||||
public ProfileViewModel ProfileViewModel { get; }
|
||||
public IProfileEditorService ProfileEditorService { get; }
|
||||
|
||||
public Cursor Cursor
|
||||
{
|
||||
get => _cursor;
|
||||
protected set => SetAndNotify(ref _cursor, value);
|
||||
}
|
||||
|
||||
public bool IsMouseDown
|
||||
{
|
||||
get => _isMouseDown;
|
||||
protected set => SetAndNotify(ref _isMouseDown, value);
|
||||
}
|
||||
|
||||
public Point MouseDownStartPosition
|
||||
{
|
||||
get => _mouseDownStartPosition;
|
||||
protected set => SetAndNotify(ref _mouseDownStartPosition, value);
|
||||
}
|
||||
|
||||
public virtual void MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
IsMouseDown = true;
|
||||
MouseDownStartPosition = ProfileViewModel.PanZoomViewModel.GetRelativeMousePosition(sender, e);
|
||||
}
|
||||
|
||||
public virtual void MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
IsMouseDown = false;
|
||||
}
|
||||
|
||||
public virtual void MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void MouseWheel(object sender, MouseWheelEventArgs e)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void KeyUp(KeyEventArgs e)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void KeyDown(KeyEventArgs e)
|
||||
{
|
||||
}
|
||||
|
||||
protected Rect GetSquareRectBetweenPoints(Point start, Point end)
|
||||
{
|
||||
// Find the shortest side
|
||||
var size = Math.Min(Math.Abs(start.X - end.X), Math.Abs(start.Y - end.Y));
|
||||
|
||||
// There's probably a very elegant way to do this, and this is not it
|
||||
if (end.X < start.X && end.Y < start.Y)
|
||||
return new Rect(start.X - size, start.Y - size, size, size);
|
||||
if (end.X < start.X)
|
||||
return new Rect(start.X - size, Math.Min(start.Y, end.Y), size, size);
|
||||
if (end.Y < start.Y)
|
||||
return new Rect(Math.Min(start.X, end.X), start.Y - size, size, size);
|
||||
return new Rect(Math.Min(start.X, end.X), Math.Min(start.Y, end.Y), size, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.Visualization.UserControls.LayerShapeControl"
|
||||
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"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
<Canvas UseLayoutRounding="False">
|
||||
<!-- Render these first so that they are always behind the actual shape -->
|
||||
<Ellipse x:Name="RotateTopLeft"
|
||||
Fill="Transparent"
|
||||
Cursor="/Resources/Cursors/aero_rotate.cur"
|
||||
MouseDown="RotationOnMouseDown"
|
||||
MouseUp="RotationOnMouseUp"
|
||||
MouseMove="RotationOnMouseMove" />
|
||||
<Ellipse x:Name="RotateTopRight"
|
||||
Fill="Transparent"
|
||||
Cursor="/Resources/Cursors/aero_rotate.cur"
|
||||
MouseDown="RotationOnMouseDown"
|
||||
MouseUp="RotationOnMouseUp"
|
||||
MouseMove="RotationOnMouseMove" />
|
||||
<Ellipse x:Name="RotateBottomRight"
|
||||
Fill="Transparent"
|
||||
Cursor="/Resources/Cursors/aero_rotate.cur"
|
||||
MouseDown="RotationOnMouseDown"
|
||||
MouseUp="RotationOnMouseUp"
|
||||
MouseMove="RotationOnMouseMove" />
|
||||
<Ellipse x:Name="RotateBottomLeft"
|
||||
Fill="Transparent"
|
||||
Cursor="/Resources/Cursors/aero_rotate.cur"
|
||||
MouseDown="RotationOnMouseDown"
|
||||
MouseUp="RotationOnMouseUp"
|
||||
MouseMove="RotationOnMouseMove" />
|
||||
<!-- The part of the layer's shape that is inside the layer -->
|
||||
<Path x:Name="LayerShapeOutline"
|
||||
Data="{Binding ShapeGeometry, Mode=OneWay}"
|
||||
Fill="Transparent"
|
||||
Stroke="{DynamicResource PrimaryHueMidBrush}"
|
||||
StrokeThickness="{Binding OutlineThickness}"
|
||||
StrokeDashArray="2 2"
|
||||
Cursor="/Resources/Cursors/aero_drag.cur"
|
||||
MouseDown="MoveOnMouseDown"
|
||||
MouseUp="MoveOnMouseUp"
|
||||
MouseMove="MoveOnMouseMove" />
|
||||
|
||||
<!-- Mutation points -->
|
||||
<Rectangle x:Name="ResizeTopCenter"
|
||||
Fill="White"
|
||||
Stroke="{DynamicResource SecondaryAccentBrush}"
|
||||
StrokeThickness="0.5"
|
||||
Cursor="Hand"
|
||||
MouseDown="ResizeOnMouseDown"
|
||||
MouseUp="ResizeOnMouseUp"
|
||||
MouseMove="ResizeOnMouseMove" />
|
||||
<Rectangle x:Name="ResizeRightCenter"
|
||||
Fill="White"
|
||||
Stroke="{DynamicResource SecondaryAccentBrush}"
|
||||
StrokeThickness="0.5"
|
||||
Cursor="Hand"
|
||||
MouseDown="ResizeOnMouseDown"
|
||||
MouseUp="ResizeOnMouseUp"
|
||||
MouseMove="ResizeOnMouseMove" />
|
||||
<Rectangle x:Name="ResizeBottomCenter"
|
||||
Fill="White"
|
||||
Stroke="{DynamicResource SecondaryAccentBrush}"
|
||||
StrokeThickness="0.5"
|
||||
Cursor="Hand"
|
||||
MouseDown="ResizeOnMouseDown"
|
||||
MouseUp="ResizeOnMouseUp"
|
||||
MouseMove="ResizeOnMouseMove" />
|
||||
<Rectangle x:Name="ResizeLeftCenter"
|
||||
Fill="White"
|
||||
Stroke="{DynamicResource SecondaryAccentBrush}"
|
||||
StrokeThickness="0.5"
|
||||
Cursor="Hand"
|
||||
MouseDown="ResizeOnMouseDown"
|
||||
MouseUp="ResizeOnMouseUp"
|
||||
MouseMove="ResizeOnMouseMove" />
|
||||
|
||||
<Rectangle x:Name="ResizeTopLeft"
|
||||
Fill="White"
|
||||
Stroke="{DynamicResource SecondaryAccentBrush}"
|
||||
StrokeThickness="0.5"
|
||||
Cursor="Hand"
|
||||
MouseDown="ResizeOnMouseDown"
|
||||
MouseUp="ResizeOnMouseUp"
|
||||
MouseMove="ResizeOnMouseMove" />
|
||||
<Rectangle x:Name="ResizeTopRight"
|
||||
Fill="White"
|
||||
Stroke="{DynamicResource SecondaryAccentBrush}"
|
||||
StrokeThickness="0.5"
|
||||
Cursor="Hand"
|
||||
MouseDown="ResizeOnMouseDown"
|
||||
MouseUp="ResizeOnMouseUp"
|
||||
MouseMove="ResizeOnMouseMove" />
|
||||
<Rectangle x:Name="ResizeBottomRight"
|
||||
Fill="White"
|
||||
Stroke="{DynamicResource SecondaryAccentBrush}"
|
||||
StrokeThickness="0.5"
|
||||
Cursor="Hand"
|
||||
MouseDown="ResizeOnMouseDown"
|
||||
MouseUp="ResizeOnMouseUp"
|
||||
MouseMove="ResizeOnMouseMove" />
|
||||
<Rectangle x:Name="ResizeBottomLeft"
|
||||
Fill="White"
|
||||
Stroke="{DynamicResource SecondaryAccentBrush}"
|
||||
StrokeThickness="0.5"
|
||||
Cursor="Hand"
|
||||
MouseDown="ResizeOnMouseDown"
|
||||
MouseUp="ResizeOnMouseUp"
|
||||
MouseMove="ResizeOnMouseMove" />
|
||||
|
||||
<!-- Anchor point -->
|
||||
<Ellipse x:Name="AnchorPoint"
|
||||
Fill="White"
|
||||
Stroke="{DynamicResource SecondaryAccentBrush}"
|
||||
StrokeThickness="0.5"
|
||||
Cursor="SizeAll"
|
||||
MouseDown="MoveOnMouseDown"
|
||||
MouseUp="MoveOnMouseUp"
|
||||
MouseMove="MoveOnMouseMove" />
|
||||
</Canvas>
|
||||
</UserControl>
|
||||
+391
@@ -0,0 +1,391 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using Artemis.UI.Events;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Visualization.UserControls
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for LayerShapeControl.xaml
|
||||
/// </summary>
|
||||
public partial class LayerShapeControl : UserControl
|
||||
{
|
||||
public static readonly DependencyProperty ZoomProperty = DependencyProperty.Register(nameof(Zoom), typeof(double), typeof(LayerShapeControl),
|
||||
new FrameworkPropertyMetadata(default(double), FrameworkPropertyMetadataOptions.None, ZoomChanged));
|
||||
|
||||
public static readonly DependencyProperty ShapePathProperty = DependencyProperty.Register(nameof(ShapePath), typeof(SKPath), typeof(LayerShapeControl),
|
||||
new FrameworkPropertyMetadata(default(SKPath), FrameworkPropertyMetadataOptions.None, ShapePathChanged));
|
||||
|
||||
public static readonly DependencyProperty ShapeAnchorProperty = DependencyProperty.Register(nameof(ShapeAnchor), typeof(SKPoint), typeof(LayerShapeControl),
|
||||
new FrameworkPropertyMetadata(default(SKPoint), FrameworkPropertyMetadataOptions.None, ShapeAnchorChanged));
|
||||
|
||||
public static readonly DependencyProperty ShapeGeometryProperty = DependencyProperty.Register(nameof(ShapeGeometry), typeof(Geometry), typeof(LayerShapeControl),
|
||||
new FrameworkPropertyMetadata(default(Geometry), FrameworkPropertyMetadataOptions.None, ShapeGeometryChanged));
|
||||
|
||||
public LayerShapeControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
UpdatePositions();
|
||||
UpdateDimensions();
|
||||
}
|
||||
|
||||
public double Zoom
|
||||
{
|
||||
get => (double) GetValue(ZoomProperty);
|
||||
set => SetValue(ZoomProperty, value);
|
||||
}
|
||||
|
||||
public SKPath ShapePath
|
||||
{
|
||||
get => (SKPath) GetValue(ShapePathProperty);
|
||||
set => SetValue(ShapePathProperty, value);
|
||||
}
|
||||
|
||||
public SKPoint ShapeAnchor
|
||||
{
|
||||
get => (SKPoint) GetValue(ShapeAnchorProperty);
|
||||
set => SetValue(ShapeAnchorProperty, value);
|
||||
}
|
||||
|
||||
public Geometry ShapeGeometry
|
||||
{
|
||||
get => (Geometry) GetValue(ShapeGeometryProperty);
|
||||
set => SetValue(ShapeGeometryProperty, value);
|
||||
}
|
||||
|
||||
public void UpdateDimensions()
|
||||
{
|
||||
if (Zoom == 0)
|
||||
return;
|
||||
|
||||
// Rotation controls
|
||||
UpdateRotateDimensions(RotateTopLeft);
|
||||
UpdateRotateDimensions(RotateTopRight);
|
||||
UpdateRotateDimensions(RotateBottomRight);
|
||||
UpdateRotateDimensions(RotateBottomLeft);
|
||||
|
||||
// Size controls
|
||||
UpdateResizeDimensions(ResizeTopCenter);
|
||||
UpdateResizeDimensions(ResizeRightCenter);
|
||||
UpdateResizeDimensions(ResizeBottomCenter);
|
||||
UpdateResizeDimensions(ResizeLeftCenter);
|
||||
UpdateResizeDimensions(ResizeTopLeft);
|
||||
UpdateResizeDimensions(ResizeTopRight);
|
||||
UpdateResizeDimensions(ResizeBottomRight);
|
||||
UpdateResizeDimensions(ResizeBottomLeft);
|
||||
|
||||
// Anchor point
|
||||
UpdateResizeDimensions(AnchorPoint);
|
||||
|
||||
// Layer outline
|
||||
LayerShapeOutline.StrokeThickness = Math.Max(2 / Zoom, 1);
|
||||
}
|
||||
|
||||
private static void ZoomChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var layerShapeControl = (LayerShapeControl) d;
|
||||
|
||||
layerShapeControl.SetCurrentValue(ZoomProperty, e.NewValue);
|
||||
layerShapeControl.UpdateDimensions();
|
||||
}
|
||||
|
||||
private static void ShapePathChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var layerShapeControl = (LayerShapeControl) d;
|
||||
|
||||
layerShapeControl.SetCurrentValue(ShapePathProperty, e.NewValue);
|
||||
layerShapeControl.UpdatePositions();
|
||||
}
|
||||
|
||||
private static void ShapeAnchorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var layerShapeControl = (LayerShapeControl) d;
|
||||
|
||||
layerShapeControl.SetCurrentValue(ShapeAnchorProperty, e.NewValue);
|
||||
layerShapeControl.UpdateShapeAnchor();
|
||||
}
|
||||
|
||||
private static void ShapeGeometryChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var layerShapeControl = (LayerShapeControl) d;
|
||||
|
||||
layerShapeControl.SetCurrentValue(ShapeGeometryProperty, e.NewValue);
|
||||
layerShapeControl.UpdateShapeGeometry();
|
||||
}
|
||||
|
||||
private void UpdateShapeGeometry()
|
||||
{
|
||||
LayerShapeOutline.Data = ShapeGeometry;
|
||||
}
|
||||
|
||||
private void UpdateShapeAnchor()
|
||||
{
|
||||
UpdateControlPosition(AnchorPoint, ShapeAnchor);
|
||||
}
|
||||
|
||||
private void UpdatePositions()
|
||||
{
|
||||
if (ShapePath == null)
|
||||
return;
|
||||
|
||||
// Rotation controls
|
||||
UpdateControlPosition(RotateTopLeft, ShapePath.Points[0]);
|
||||
UpdateControlPosition(RotateTopRight, ShapePath.Points[1]);
|
||||
UpdateControlPosition(RotateBottomRight, ShapePath.Points[2]);
|
||||
UpdateControlPosition(RotateBottomLeft, ShapePath.Points[3]);
|
||||
|
||||
// Size controls
|
||||
UpdateControlPosition(ResizeTopCenter, ShapePath.Points[0], ShapePath.Points[1]);
|
||||
UpdateControlPosition(ResizeRightCenter, ShapePath.Points[1], ShapePath.Points[2]);
|
||||
UpdateControlPosition(ResizeBottomCenter, ShapePath.Points[2], ShapePath.Points[3]);
|
||||
UpdateControlPosition(ResizeLeftCenter, ShapePath.Points[3], ShapePath.Points[0]);
|
||||
UpdateControlPosition(ResizeTopLeft, ShapePath.Points[0]);
|
||||
UpdateControlPosition(ResizeTopRight, ShapePath.Points[1]);
|
||||
UpdateControlPosition(ResizeBottomRight, ShapePath.Points[2]);
|
||||
UpdateControlPosition(ResizeBottomLeft, ShapePath.Points[3]);
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
|
||||
private void UpdateControlPosition(UIElement control, SKPoint point)
|
||||
{
|
||||
if (float.IsInfinity(point.X) || float.IsInfinity(point.Y))
|
||||
return;
|
||||
Canvas.SetLeft(control, point.X);
|
||||
Canvas.SetTop(control, point.Y);
|
||||
}
|
||||
|
||||
private void UpdateControlPosition(UIElement control, SKPoint point1, SKPoint point2)
|
||||
{
|
||||
var point = new SKPoint((point1.X + point2.X) / 2, (point1.Y + point2.Y) / 2);
|
||||
UpdateControlPosition(control, point);
|
||||
}
|
||||
|
||||
private void UpdateRotateDimensions(FrameworkElement rotateControl)
|
||||
{
|
||||
var controlSize = Math.Max(10 / Zoom, 4);
|
||||
var rotateSize = controlSize * 8;
|
||||
|
||||
rotateControl.Width = rotateSize;
|
||||
rotateControl.Height = rotateSize;
|
||||
rotateControl.Margin = new Thickness(rotateSize / 2 * -1, rotateSize / 2 * -1, 0, 0);
|
||||
}
|
||||
|
||||
private void UpdateResizeDimensions(FrameworkElement resizeControl)
|
||||
{
|
||||
var controlSize = Math.Max(10 / Zoom, 4);
|
||||
|
||||
resizeControl.Width = controlSize;
|
||||
resizeControl.Height = controlSize;
|
||||
resizeControl.Margin = new Thickness(controlSize / 2 * -1, controlSize / 2 * -1, 0, 0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event handlers
|
||||
|
||||
private void RotationOnMouseDown(object sender, MouseEventArgs e)
|
||||
{
|
||||
((IInputElement) sender).CaptureMouse();
|
||||
e.Handled = true;
|
||||
|
||||
if (ReferenceEquals(sender, RotateTopLeft))
|
||||
OnRotateMouseDown(new ShapeControlEventArgs(e, ShapeControlPoint.TopLeft));
|
||||
else if (ReferenceEquals(sender, RotateTopRight))
|
||||
OnRotateMouseDown(new ShapeControlEventArgs(e, ShapeControlPoint.TopRight));
|
||||
else if (ReferenceEquals(sender, RotateBottomRight))
|
||||
OnRotateMouseDown(new ShapeControlEventArgs(e, ShapeControlPoint.BottomRight));
|
||||
else if (ReferenceEquals(sender, RotateBottomLeft))
|
||||
OnRotateMouseDown(new ShapeControlEventArgs(e, ShapeControlPoint.BottomLeft));
|
||||
}
|
||||
|
||||
private void RotationOnMouseUp(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (ReferenceEquals(sender, RotateTopLeft))
|
||||
OnRotateMouseUp(new ShapeControlEventArgs(e, ShapeControlPoint.TopLeft));
|
||||
else if (ReferenceEquals(sender, RotateTopRight))
|
||||
OnRotateMouseUp(new ShapeControlEventArgs(e, ShapeControlPoint.TopRight));
|
||||
else if (ReferenceEquals(sender, RotateBottomRight))
|
||||
OnRotateMouseUp(new ShapeControlEventArgs(e, ShapeControlPoint.BottomRight));
|
||||
else if (ReferenceEquals(sender, RotateBottomLeft))
|
||||
OnRotateMouseUp(new ShapeControlEventArgs(e, ShapeControlPoint.BottomLeft));
|
||||
|
||||
((IInputElement) sender).ReleaseMouseCapture();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void RotationOnMouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (ReferenceEquals(sender, RotateTopLeft))
|
||||
OnRotateMouseMove(new ShapeControlEventArgs(e, ShapeControlPoint.TopLeft));
|
||||
else if (ReferenceEquals(sender, RotateTopRight))
|
||||
OnRotateMouseMove(new ShapeControlEventArgs(e, ShapeControlPoint.TopRight));
|
||||
else if (ReferenceEquals(sender, RotateBottomRight))
|
||||
OnRotateMouseMove(new ShapeControlEventArgs(e, ShapeControlPoint.BottomRight));
|
||||
else if (ReferenceEquals(sender, RotateBottomLeft))
|
||||
OnRotateMouseMove(new ShapeControlEventArgs(e, ShapeControlPoint.BottomLeft));
|
||||
}
|
||||
|
||||
private void ResizeOnMouseDown(object sender, MouseEventArgs e)
|
||||
{
|
||||
((IInputElement) sender).CaptureMouse();
|
||||
e.Handled = true;
|
||||
|
||||
if (ReferenceEquals(sender, ResizeTopCenter))
|
||||
OnResizeMouseDown(new ShapeControlEventArgs(e, ShapeControlPoint.TopCenter));
|
||||
else if (ReferenceEquals(sender, ResizeRightCenter))
|
||||
OnResizeMouseDown(new ShapeControlEventArgs(e, ShapeControlPoint.RightCenter));
|
||||
else if (ReferenceEquals(sender, ResizeBottomCenter))
|
||||
OnResizeMouseDown(new ShapeControlEventArgs(e, ShapeControlPoint.BottomCenter));
|
||||
else if (ReferenceEquals(sender, ResizeLeftCenter))
|
||||
OnResizeMouseDown(new ShapeControlEventArgs(e, ShapeControlPoint.LeftCenter));
|
||||
else if (ReferenceEquals(sender, ResizeTopLeft))
|
||||
OnResizeMouseDown(new ShapeControlEventArgs(e, ShapeControlPoint.TopLeft));
|
||||
else if (ReferenceEquals(sender, ResizeTopRight))
|
||||
OnResizeMouseDown(new ShapeControlEventArgs(e, ShapeControlPoint.TopRight));
|
||||
else if (ReferenceEquals(sender, ResizeBottomRight))
|
||||
OnResizeMouseDown(new ShapeControlEventArgs(e, ShapeControlPoint.BottomRight));
|
||||
else if (ReferenceEquals(sender, ResizeBottomLeft))
|
||||
OnResizeMouseDown(new ShapeControlEventArgs(e, ShapeControlPoint.BottomLeft));
|
||||
}
|
||||
|
||||
private void ResizeOnMouseUp(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (ReferenceEquals(sender, ResizeTopCenter))
|
||||
OnResizeMouseUp(new ShapeControlEventArgs(e, ShapeControlPoint.TopCenter));
|
||||
else if (ReferenceEquals(sender, ResizeRightCenter))
|
||||
OnResizeMouseUp(new ShapeControlEventArgs(e, ShapeControlPoint.RightCenter));
|
||||
else if (ReferenceEquals(sender, ResizeBottomCenter))
|
||||
OnResizeMouseUp(new ShapeControlEventArgs(e, ShapeControlPoint.BottomCenter));
|
||||
else if (ReferenceEquals(sender, ResizeLeftCenter))
|
||||
OnResizeMouseUp(new ShapeControlEventArgs(e, ShapeControlPoint.LeftCenter));
|
||||
else if (ReferenceEquals(sender, ResizeTopLeft))
|
||||
OnResizeMouseUp(new ShapeControlEventArgs(e, ShapeControlPoint.TopLeft));
|
||||
else if (ReferenceEquals(sender, ResizeTopRight))
|
||||
OnResizeMouseUp(new ShapeControlEventArgs(e, ShapeControlPoint.TopRight));
|
||||
else if (ReferenceEquals(sender, ResizeBottomRight))
|
||||
OnResizeMouseUp(new ShapeControlEventArgs(e, ShapeControlPoint.BottomRight));
|
||||
else if (ReferenceEquals(sender, ResizeBottomLeft))
|
||||
OnResizeMouseUp(new ShapeControlEventArgs(e, ShapeControlPoint.BottomLeft));
|
||||
|
||||
((IInputElement) sender).ReleaseMouseCapture();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void ResizeOnMouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (ReferenceEquals(sender, ResizeTopCenter))
|
||||
OnResizeMouseMove(new ShapeControlEventArgs(e, ShapeControlPoint.TopCenter));
|
||||
else if (ReferenceEquals(sender, ResizeRightCenter))
|
||||
OnResizeMouseMove(new ShapeControlEventArgs(e, ShapeControlPoint.RightCenter));
|
||||
else if (ReferenceEquals(sender, ResizeBottomCenter))
|
||||
OnResizeMouseMove(new ShapeControlEventArgs(e, ShapeControlPoint.BottomCenter));
|
||||
else if (ReferenceEquals(sender, ResizeLeftCenter))
|
||||
OnResizeMouseMove(new ShapeControlEventArgs(e, ShapeControlPoint.LeftCenter));
|
||||
else if (ReferenceEquals(sender, ResizeTopLeft))
|
||||
OnResizeMouseMove(new ShapeControlEventArgs(e, ShapeControlPoint.TopLeft));
|
||||
else if (ReferenceEquals(sender, ResizeTopRight))
|
||||
OnResizeMouseMove(new ShapeControlEventArgs(e, ShapeControlPoint.TopRight));
|
||||
else if (ReferenceEquals(sender, ResizeBottomRight))
|
||||
OnResizeMouseMove(new ShapeControlEventArgs(e, ShapeControlPoint.BottomRight));
|
||||
else if (ReferenceEquals(sender, ResizeBottomLeft))
|
||||
OnResizeMouseMove(new ShapeControlEventArgs(e, ShapeControlPoint.BottomLeft));
|
||||
}
|
||||
|
||||
private void MoveOnMouseDown(object sender, MouseEventArgs e)
|
||||
{
|
||||
((IInputElement) sender).CaptureMouse();
|
||||
e.Handled = true;
|
||||
|
||||
if (ReferenceEquals(sender, LayerShapeOutline))
|
||||
OnMoveMouseDown(new ShapeControlEventArgs(e, ShapeControlPoint.LayerShape));
|
||||
else if (ReferenceEquals(sender, AnchorPoint))
|
||||
OnMoveMouseDown(new ShapeControlEventArgs(e, ShapeControlPoint.Anchor));
|
||||
}
|
||||
|
||||
private void MoveOnMouseUp(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (ReferenceEquals(sender, LayerShapeOutline))
|
||||
OnMoveMouseUp(new ShapeControlEventArgs(e, ShapeControlPoint.LayerShape));
|
||||
else if (ReferenceEquals(sender, AnchorPoint))
|
||||
OnMoveMouseUp(new ShapeControlEventArgs(e, ShapeControlPoint.Anchor));
|
||||
|
||||
((IInputElement) sender).ReleaseMouseCapture();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void MoveOnMouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (ReferenceEquals(sender, LayerShapeOutline))
|
||||
OnMoveMouseMove(new ShapeControlEventArgs(e, ShapeControlPoint.LayerShape));
|
||||
else if (ReferenceEquals(sender, AnchorPoint))
|
||||
OnMoveMouseMove(new ShapeControlEventArgs(e, ShapeControlPoint.Anchor));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
public event EventHandler<ShapeControlEventArgs> RotateMouseDown;
|
||||
public event EventHandler<ShapeControlEventArgs> RotateMouseUp;
|
||||
public event EventHandler<ShapeControlEventArgs> RotateMouseMove;
|
||||
|
||||
public event EventHandler<ShapeControlEventArgs> ResizeMouseDown;
|
||||
public event EventHandler<ShapeControlEventArgs> ResizeMouseUp;
|
||||
public event EventHandler<ShapeControlEventArgs> ResizeMouseMove;
|
||||
|
||||
public event EventHandler<ShapeControlEventArgs> MoveMouseDown;
|
||||
public event EventHandler<ShapeControlEventArgs> MoveMouseUp;
|
||||
public event EventHandler<ShapeControlEventArgs> MoveMouseMove;
|
||||
|
||||
protected virtual void OnMoveMouseMove(ShapeControlEventArgs e)
|
||||
{
|
||||
MoveMouseMove?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnMoveMouseUp(ShapeControlEventArgs e)
|
||||
{
|
||||
MoveMouseUp?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnMoveMouseDown(ShapeControlEventArgs e)
|
||||
{
|
||||
MoveMouseDown?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnResizeMouseMove(ShapeControlEventArgs e)
|
||||
{
|
||||
ResizeMouseMove?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnResizeMouseUp(ShapeControlEventArgs e)
|
||||
{
|
||||
ResizeMouseUp?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnResizeMouseDown(ShapeControlEventArgs e)
|
||||
{
|
||||
ResizeMouseDown?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnRotateMouseMove(ShapeControlEventArgs e)
|
||||
{
|
||||
RotateMouseMove?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnRotateMouseUp(ShapeControlEventArgs e)
|
||||
{
|
||||
RotateMouseUp?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnRotateMouseDown(ShapeControlEventArgs e)
|
||||
{
|
||||
RotateMouseDown?.Invoke(this, e);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user