1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Keyframes - Copy/paste WIP

Timeline - Improved sizing, avoid unnecessary scrolling
Timeline - Fix selection rectangle appearing on mousedown
This commit is contained in:
Robert 2020-12-02 19:11:39 +01:00
parent 7c955d1134
commit f110383ed4
18 changed files with 381 additions and 122 deletions

View File

@ -0,0 +1,36 @@
using System;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core
{
/// <summary>
/// Represents a keyframe on a <see cref="ILayerProperty" /> containing a value and a timestamp
/// </summary>
public interface ILayerPropertyKeyframe
{
/// <summary>
/// Gets an untyped reference to the layer property of this keyframe
/// </summary>
ILayerProperty UntypedLayerProperty { get; }
/// <summary>
/// The position of this keyframe in the timeline
/// </summary>
TimeSpan Position { get; set; }
/// <summary>
/// The easing function applied on the value of the keyframe
/// </summary>
Easings.Functions EasingFunction { get; set; }
/// <summary>
/// Gets the entity this keyframe uses for persistent storage
/// </summary>
KeyframeEntity GetKeyframeEntity();
/// <summary>
/// Removes the keyframe from the layer property
/// </summary>
void Remove();
}
}

View File

@ -559,12 +559,7 @@ namespace Artemis.Core
Entity.Value = CoreJson.SerializeObject(BaseValue); Entity.Value = CoreJson.SerializeObject(BaseValue);
Entity.KeyframesEnabled = KeyframesEnabled; Entity.KeyframesEnabled = KeyframesEnabled;
Entity.KeyframeEntities.Clear(); Entity.KeyframeEntities.Clear();
Entity.KeyframeEntities.AddRange(Keyframes.Select(k => new KeyframeEntity Entity.KeyframeEntities.AddRange(Keyframes.Select(k => k.GetKeyframeEntity()));
{
Value = CoreJson.SerializeObject(k.Value),
Position = k.Position,
EasingFunction = (int) k.EasingFunction
}));
Entity.DataBindingEntities.Clear(); Entity.DataBindingEntities.Clear();
foreach (IDataBinding dataBinding in _dataBindings) foreach (IDataBinding dataBinding in _dataBindings)

View File

@ -1,11 +1,12 @@
using System; using System;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core namespace Artemis.Core
{ {
/// <summary> /// <summary>
/// Represents a keyframe on a <see cref="LayerProperty{T}" /> containing a value and a timestamp /// Represents a keyframe on a <see cref="LayerProperty{T}" /> containing a value and a timestamp
/// </summary> /// </summary>
public class LayerPropertyKeyframe<T> : CorePropertyChanged public class LayerPropertyKeyframe<T> : CorePropertyChanged, ILayerPropertyKeyframe
{ {
private LayerProperty<T> _layerProperty; private LayerProperty<T> _layerProperty;
private TimeSpan _position; private TimeSpan _position;
@ -45,10 +46,10 @@ namespace Artemis.Core
set => SetAndNotify(ref _value, value); set => SetAndNotify(ref _value, value);
} }
/// <inheritdoc />
public ILayerProperty UntypedLayerProperty => LayerProperty;
/// <summary> /// <inheritdoc />
/// The position of this keyframe in the timeline
/// </summary>
public TimeSpan Position public TimeSpan Position
{ {
get => _position; get => _position;
@ -59,14 +60,21 @@ namespace Artemis.Core
} }
} }
/// <summary> /// <inheritdoc />
/// The easing function applied on the value of the keyframe
/// </summary>
public Easings.Functions EasingFunction { get; set; } public Easings.Functions EasingFunction { get; set; }
/// <summary> /// <inheritdoc />
/// Removes the keyframe from the layer property public KeyframeEntity GetKeyframeEntity()
/// </summary> {
return new KeyframeEntity
{
Value = CoreJson.SerializeObject(Value),
Position = Position,
EasingFunction = (int) EasingFunction
};
}
/// <inheritdoc />
public void Remove() public void Remove()
{ {
LayerProperty.RemoveKeyframe(this); LayerProperty.RemoveKeyframe(this);

View File

@ -123,7 +123,7 @@ namespace Artemis.Core
/// Adds a profile element to the <see cref="Children" /> collection, optionally at the given position (1-based) /// Adds a profile element to the <see cref="Children" /> collection, optionally at the given position (1-based)
/// </summary> /// </summary>
/// <param name="child">The profile element to add</param> /// <param name="child">The profile element to add</param>
/// <param name="order">The order where to place the child (1-based), defaults to the end of the collection</param> /// <param name="order">The order where to place the child (0-based), defaults to the end of the collection</param>
public virtual void AddChild(ProfileElement child, int? order = null) public virtual void AddChild(ProfileElement child, int? order = null)
{ {
if (Disposed) if (Disposed)
@ -136,31 +136,19 @@ namespace Artemis.Core
// Add to the end of the list // Add to the end of the list
if (order == null) if (order == null)
{
ChildrenList.Add(child); ChildrenList.Add(child);
child.Order = ChildrenList.Count; // Insert at the given index
}
// Shift everything after the given order
else else
{ {
if (order < 0) if (order < 0)
order = 0; order = 0;
foreach (ProfileElement profileElement in ChildrenList.Where(c => c.Order >= order).ToList()) if (order > ChildrenList.Count)
profileElement.Order++; order = ChildrenList.Count;
ChildrenList.Insert(order.Value, child);
int targetIndex;
if (order == 0)
targetIndex = 0;
else if (order > ChildrenList.Count)
targetIndex = ChildrenList.Count;
else
targetIndex = ChildrenList.FindIndex(c => c.Order == order + 1);
ChildrenList.Insert(targetIndex, child);
child.Order = order.Value;
} }
child.Parent = this; child.Parent = this;
StreamlineOrder();
} }
OnChildAdded(); OnChildAdded();
@ -178,10 +166,7 @@ namespace Artemis.Core
lock (ChildrenList) lock (ChildrenList)
{ {
ChildrenList.Remove(child); ChildrenList.Remove(child);
StreamlineOrder();
// Shift everything after the given order
foreach (ProfileElement profileElement in ChildrenList.Where(c => c.Order > child.Order).ToList())
profileElement.Order--;
child.Parent = null; child.Parent = null;
} }
@ -189,6 +174,12 @@ namespace Artemis.Core
OnChildRemoved(); OnChildRemoved();
} }
private void StreamlineOrder()
{
for (int index = 0; index < ChildrenList.Count; index++)
ChildrenList[index].Order = index;
}
/// <summary> /// <summary>
/// Returns a flattened list of all child folders /// Returns a flattened list of all child folders
/// </summary> /// </summary>

View File

@ -156,6 +156,11 @@ namespace Artemis.UI.Shared.Services
/// <returns>The pasted render element</returns> /// <returns>The pasted render element</returns>
ProfileElement? PasteProfileElement(Folder target, int position); ProfileElement? PasteProfileElement(Folder target, int position);
/// <summary>
/// Gets a boolean indicating whether a profile element is on the clipboard
/// </summary>
bool GetCanPasteProfileElement();
/// <summary> /// <summary>
/// Occurs when a new profile is selected /// Occurs when a new profile is selected
/// </summary> /// </summary>

View File

@ -384,6 +384,12 @@ namespace Artemis.UI.Shared.Services
return clipboardObject != null ? PasteClipboardData(clipboardObject, target, position) : null; return clipboardObject != null ? PasteClipboardData(clipboardObject, target, position) : null;
} }
public bool GetCanPasteProfileElement()
{
object? clipboardObject = JsonClipboard.GetData();
return clipboardObject is LayerEntity || clipboardObject is FolderClipboardModel;
}
private RenderProfileElement? PasteClipboardData(object clipboardObject, Folder target, int position) private RenderProfileElement? PasteClipboardData(object clipboardObject, Folder target, int position)
{ {
RenderProfileElement? pasted = null; RenderProfileElement? pasted = null;

View File

@ -160,7 +160,9 @@
VerticalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"
ScrollChanged="TimelineScrollChanged"> ScrollChanged="TimelineScrollChanged">
<Border BorderThickness="0,0,1,0" BorderBrush="{DynamicResource MaterialDesignDivider}"> <Border BorderThickness="0,0,1,0" BorderBrush="{DynamicResource MaterialDesignDivider}">
<ContentControl s:View.Model="{Binding TreeViewModel}" /> <ContentControl s:View.Model="{Binding TreeViewModel}"
shared:SizeObserver.Observe="True"
shared:SizeObserver.ObservedHeight="{Binding TreeViewModelHeight, Mode=OneWayToSource}"/>
</Border> </Border>
</ScrollViewer> </ScrollViewer>
<materialDesign:TransitionerSlide> <materialDesign:TransitionerSlide>

View File

@ -35,6 +35,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties
private int _rightSideIndex; private int _rightSideIndex;
private RenderProfileElement _selectedProfileElement; private RenderProfileElement _selectedProfileElement;
private DateTime _lastEffectsViewModelToggle; private DateTime _lastEffectsViewModelToggle;
private double _treeViewModelHeight;
public LayerPropertiesViewModel(IProfileEditorService profileEditorService, public LayerPropertiesViewModel(IProfileEditorService profileEditorService,
ICoreService coreService, ICoreService coreService,
@ -157,6 +158,12 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties
public Layer SelectedLayer => SelectedProfileElement as Layer; public Layer SelectedLayer => SelectedProfileElement as Layer;
public Folder SelectedFolder => SelectedProfileElement as Folder; public Folder SelectedFolder => SelectedProfileElement as Folder;
public double TreeViewModelHeight
{
get => _treeViewModelHeight;
set => SetAndNotify(ref _treeViewModelHeight, value);
}
#region Segments #region Segments

View File

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using Artemis.Core;
using Artemis.Storage.Entities.Profile;
using Artemis.UI.Exceptions;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.Models
{
public class KeyframeClipboardModel
{
public Dictionary<string, KeyframeEntity> KeyframeEntities { get; set; }
public KeyframeClipboardModel(List<ILayerPropertyKeyframe> keyframes)
{
KeyframeEntities = new Dictionary<string, KeyframeEntity>();
foreach (ILayerPropertyKeyframe keyframe in keyframes)
{
KeyframeEntities.Add(keyframe.UntypedLayerProperty.Path, );
}
}
public void Paste(RenderProfileElement target, TimeSpan pastePosition)
{
if (target == null) throw new ArgumentNullException(nameof(target));
if (HasBeenPasted)
throw new ArtemisUIException("Clipboard model can only be pasted once");
HasBeenPasted = true;
}
public bool HasBeenPasted { get; set; }
}
}

View File

@ -48,6 +48,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
} }
public TimeSpan Position => LayerPropertyKeyframe.Position; public TimeSpan Position => LayerPropertyKeyframe.Position;
public ILayerPropertyKeyframe Keyframe => LayerPropertyKeyframe;
public void Dispose() public void Dispose()
{ {
@ -159,34 +160,11 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
#region Context menu actions #region Context menu actions
public void Copy() public void Delete(bool save = true)
{
LayerPropertyKeyframe<T> newKeyframe = new LayerPropertyKeyframe<T>(
LayerPropertyKeyframe.Value,
LayerPropertyKeyframe.Position,
LayerPropertyKeyframe.EasingFunction,
LayerPropertyKeyframe.LayerProperty
);
// If possible, shift the keyframe to the right by 11 pixels
TimeSpan desiredPosition = newKeyframe.Position + TimeSpan.FromMilliseconds(1000f / _profileEditorService.PixelsPerSecond * 11);
if (desiredPosition <= newKeyframe.LayerProperty.ProfileElement.Timeline.Length)
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 void Delete()
{ {
LayerPropertyKeyframe.LayerProperty.RemoveKeyframe(LayerPropertyKeyframe); LayerPropertyKeyframe.LayerProperty.RemoveKeyframe(LayerPropertyKeyframe);
_profileEditorService.UpdateSelectedProfileElement(); if (save)
_profileEditorService.UpdateSelectedProfileElement();
} }
#endregion #endregion
@ -196,6 +174,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
{ {
bool IsSelected { get; set; } bool IsSelected { get; set; }
TimeSpan Position { get; } TimeSpan Position { get; }
ILayerPropertyKeyframe Keyframe { get; }
#region Movement #region Movement
@ -210,8 +189,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
void PopulateEasingViewModels(); void PopulateEasingViewModels();
void ClearEasingViewModels(); void ClearEasingViewModels();
void Copy(); void Delete(bool save = true);
void Delete();
#endregion #endregion
} }

View File

@ -36,8 +36,8 @@
MouseDown="{s:Action KeyframeMouseDown}" MouseDown="{s:Action KeyframeMouseDown}"
MouseUp="{s:Action KeyframeMouseUp}" MouseUp="{s:Action KeyframeMouseUp}"
MouseMove="{s:Action KeyframeMouseMove}" MouseMove="{s:Action KeyframeMouseMove}"
ContextMenuOpening="{s:Action ContextMenuOpening}" ContextMenuOpening="{s:Action KeyframeContextMenuOpening}"
ContextMenuClosing="{s:Action ContextMenuClosing}"> ContextMenuClosing="{s:Action KeyframeContextMenuClosing}">
<Ellipse.Style> <Ellipse.Style>
<Style TargetType="{x:Type Ellipse}"> <Style TargetType="{x:Type Ellipse}">
<Style.Triggers> <Style.Triggers>
@ -62,17 +62,6 @@
</Ellipse.Style> </Ellipse.Style>
<Ellipse.ContextMenu> <Ellipse.ContextMenu>
<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 Header="Easing" ItemsSource="{Binding EasingViewModels}">
<MenuItem.Icon> <MenuItem.Icon>
<materialDesign:PackIcon Kind="Creation" /> <materialDesign:PackIcon Kind="Creation" />
@ -98,6 +87,28 @@
</DataTemplate> </DataTemplate>
</MenuItem.ItemTemplate> </MenuItem.ItemTemplate>
</MenuItem> </MenuItem>
<Separator />
<MenuItem Header="Duplicate" Command="{s:Action DuplicateKeyframes}" CommandParameter="{Binding}" InputGestureText="Ctrl+D">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="ContentDuplicate" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Copy" Command="{s:Action CopyKeyframes}" CommandParameter="{Binding}" InputGestureText="Ctrl+C">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="ContentCopy" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Paste" Command="{s:Action PasteKeyframes}" CommandParameter="{Binding}" InputGestureText="Ctrl+V">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="ContentPaste" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Delete" Command="{s:Action DeleteKeyframes}" InputGestureText="Del">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="Delete" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu> </ContextMenu>
</Ellipse.ContextMenu> </Ellipse.ContextMenu>
</Ellipse> </Ellipse>

View File

@ -5,33 +5,67 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline" xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline"
xmlns:s="https://github.com/canton7/Stylet" xmlns:s="https://github.com/canton7/Stylet"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="25" d:DesignHeight="25"
d:DesignWidth="800" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:TimelineViewModel}"> d:DataContext="{d:DesignInstance local:TimelineViewModel}">
<Grid x:Name="TimelineContainerGrid" <Grid Background="{DynamicResource MaterialDesignToolBarBackground}"
Background="{DynamicResource MaterialDesignToolBarBackground}"
MouseDown="{s:Action TimelineCanvasMouseDown}" MouseDown="{s:Action TimelineCanvasMouseDown}"
MouseUp="{s:Action TimelineCanvasMouseUp}" MouseUp="{s:Action TimelineCanvasMouseUp}"
MouseMove="{s:Action TimelineCanvasMouseMove}" MouseMove="{s:Action TimelineCanvasMouseMove}"
Margin="0 0 -1 0"> ContextMenuOpening="{s:Action ContextMenuOpening}"
ContextMenuClosing="{s:Action ContextMenuClosing}"
Height="{Binding LayerPropertiesViewModel.TreeViewModelHeight}"
VerticalAlignment="Top"
Focusable="True">
<Grid.InputBindings>
<KeyBinding Key="Delete" Command="{s:Action DeleteKeyframes}" />
<KeyBinding Key="D" Modifiers="Control" Command="{s:Action DuplicateKeyframes}" />
<KeyBinding Key="C" Modifiers="Control" Command="{s:Action CopyKeyframes}" />
<KeyBinding Key="V" Modifiers="Control" Command="{s:Action PasteKeyframes}" />
</Grid.InputBindings>
<Grid.Triggers> <Grid.Triggers>
<EventTrigger RoutedEvent="UIElement.MouseLeftButtonDown"> <EventTrigger RoutedEvent="UIElement.MouseLeftButtonDown">
<BeginStoryboard> <BeginStoryboard>
<Storyboard Storyboard.TargetName="MultiSelectionPath" Storyboard.TargetProperty="Opacity"> <Storyboard Storyboard.TargetName="MultiSelectionPath" Storyboard.TargetProperty="Opacity">
<DoubleAnimation From="0" To="1" Duration="0:0:0.1" /> <DoubleAnimation To="1" Duration="0:0:0.1" />
</Storyboard> </Storyboard>
</BeginStoryboard> </BeginStoryboard>
</EventTrigger> </EventTrigger>
<EventTrigger RoutedEvent="UIElement.MouseLeftButtonUp"> <EventTrigger RoutedEvent="UIElement.MouseLeftButtonUp">
<BeginStoryboard> <BeginStoryboard>
<Storyboard Storyboard.TargetName="MultiSelectionPath" Storyboard.TargetProperty="Opacity"> <Storyboard Storyboard.TargetName="MultiSelectionPath" Storyboard.TargetProperty="Opacity">
<DoubleAnimation From="1" To="0" Duration="0:0:0.2" /> <DoubleAnimation To="0" Duration="0:0:0.2" />
</Storyboard> </Storyboard>
</BeginStoryboard> </BeginStoryboard>
</EventTrigger> </EventTrigger>
</Grid.Triggers> </Grid.Triggers>
<Grid.ContextMenu>
<ContextMenu>
<MenuItem Header="Duplicate" Command="{s:Action DuplicateKeyframes}" CommandParameter="{Binding}" InputGestureText="Ctrl+D" IsEnabled="False">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="ContentDuplicate" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Copy" Command="{s:Action CopyKeyframes}" CommandParameter="{Binding}" InputGestureText="Ctrl+C" IsEnabled="False">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="ContentCopy" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Paste" Command="{s:Action PasteKeyframes}" CommandParameter="{Binding}" InputGestureText="Ctrl+V" >
<MenuItem.Icon>
<materialDesign:PackIcon Kind="ContentPaste" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Delete" Command="{s:Action DeleteKeyframes}" InputGestureText="Del" IsEnabled="False">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="Delete" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</Grid.ContextMenu>
<ItemsControl ItemsSource="{Binding LayerPropertyGroups}" <ItemsControl ItemsSource="{Binding LayerPropertyGroups}"
MinWidth="{Binding TotalTimelineWidth}" MinWidth="{Binding TotalTimelineWidth}"
HorizontalAlignment="Left"> HorizontalAlignment="Left">
@ -48,7 +82,7 @@
X1="{Binding StartSegmentEndPosition}" X1="{Binding StartSegmentEndPosition}"
X2="{Binding StartSegmentEndPosition}" X2="{Binding StartSegmentEndPosition}"
Y1="0" Y1="0"
Y2="{Binding ActualHeight, ElementName=TimelineContainerGrid}" Y2="{Binding LayerPropertiesViewModel.TreeViewModelHeight}"
HorizontalAlignment="Left" HorizontalAlignment="Left"
Visibility="{Binding LayerPropertiesViewModel.StartTimelineSegmentViewModel.SegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}" /> Visibility="{Binding LayerPropertiesViewModel.StartTimelineSegmentViewModel.SegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}" />
<Line Stroke="{StaticResource PrimaryHueDarkBrush}" <Line Stroke="{StaticResource PrimaryHueDarkBrush}"
@ -57,14 +91,14 @@
X1="{Binding MainSegmentEndPosition}" X1="{Binding MainSegmentEndPosition}"
X2="{Binding MainSegmentEndPosition}" X2="{Binding MainSegmentEndPosition}"
Y1="0" Y1="0"
Y2="{Binding ActualHeight, ElementName=TimelineContainerGrid}" /> Y2="{Binding LayerPropertiesViewModel.TreeViewModelHeight}" />
<Line Stroke="{StaticResource PrimaryHueDarkBrush}" <Line Stroke="{StaticResource PrimaryHueDarkBrush}"
Opacity="0.5" Opacity="0.5"
StrokeDashArray="4 2" StrokeDashArray="4 2"
X1="{Binding EndSegmentEndPosition}" X1="{Binding EndSegmentEndPosition}"
X2="{Binding EndSegmentEndPosition}" X2="{Binding EndSegmentEndPosition}"
Y1="0" Y1="0"
Y2="{Binding ActualHeight, ElementName=TimelineContainerGrid}" Y2="{Binding LayerPropertiesViewModel.TreeViewModelHeight}"
Visibility="{Binding LayerPropertiesViewModel.EndTimelineSegmentViewModel.SegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}" /> Visibility="{Binding LayerPropertiesViewModel.EndTimelineSegmentViewModel.SegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}" />
<!-- Multi-selection rectangle --> <!-- Multi-selection rectangle -->

View File

@ -3,11 +3,15 @@ using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Windows; using System.Windows;
using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Shapes; using System.Windows.Shapes;
using Artemis.Core;
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.Models;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Models;
using Stylet; using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
@ -151,31 +155,108 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
#region Context menu actions #region Context menu actions
public void ContextMenuOpening(object sender, EventArgs e) public bool CanDuplicateKeyframes => GetAllKeyframeViewModels().Any(k => k.IsSelected);
public bool CanCopyKeyframes => GetAllKeyframeViewModels().Any(k => k.IsSelected);
public bool CanDeleteKeyframes => GetAllKeyframeViewModels().Any(k => k.IsSelected);
public bool CanPasteKeyframes => JsonClipboard.GetData() is KeyframeClipboardModel;
private TimeSpan? _contextMenuOpenPosition;
public void ContextMenuOpening(object sender, ContextMenuEventArgs e)
{ {
if (sender is Ellipse ellipse && ellipse.DataContext is ITimelineKeyframeViewModel viewModel) _contextMenuOpenPosition = GetCursorTime(new Point(e.CursorLeft, e.CursorTop));
}
public void ContextMenuClosing(object sender, ContextMenuEventArgs e)
{
_contextMenuOpenPosition = null;
}
public void KeyframeContextMenuOpening(object sender, ContextMenuEventArgs e)
{
if (sender is FrameworkElement fe && fe.DataContext is ITimelineKeyframeViewModel viewModel)
viewModel.PopulateEasingViewModels(); viewModel.PopulateEasingViewModels();
} }
public void ContextMenuClosing(object sender, EventArgs e) public void KeyframeContextMenuClosing(object sender, ContextMenuEventArgs e)
{ {
if (sender is Ellipse ellipse && ellipse.DataContext is ITimelineKeyframeViewModel viewModel) if (sender is Ellipse ellipse && ellipse.DataContext is ITimelineKeyframeViewModel viewModel)
viewModel.ClearEasingViewModels(); viewModel.ClearEasingViewModels();
} }
public void Copy(ITimelineKeyframeViewModel viewModel) public void DeleteKeyframes()
{ {
// viewModel.Copy(); List<ITimelineKeyframeViewModel> keyframeViewModels = GetAllKeyframeViewModels().Where(k => k.IsSelected).ToList();
List<ITimelineKeyframeViewModel> keyframeViewModels = GetAllKeyframeViewModels(); foreach (ITimelineKeyframeViewModel keyframeViewModel in keyframeViewModels)
foreach (ITimelineKeyframeViewModel keyframeViewModel in keyframeViewModels.Where(k => k.IsSelected)) keyframeViewModel.Delete(false);
keyframeViewModel.Copy();
_profileEditorService.UpdateSelectedProfileElement();
} }
public void Delete(ITimelineKeyframeViewModel viewModel) public void DuplicateKeyframes(ITimelineKeyframeViewModel viewModel = null)
{ {
List<ITimelineKeyframeViewModel> keyframeViewModels = GetAllKeyframeViewModels(); TimeSpan pastePosition = GetPastePosition(viewModel);
foreach (ITimelineKeyframeViewModel keyframeViewModel in keyframeViewModels.Where(k => k.IsSelected))
keyframeViewModel.Delete(); List<ILayerPropertyKeyframe> keyframes = GetAllKeyframeViewModels().Where(k => k.IsSelected).Select(k => k.Keyframe).ToList();
DuplicateKeyframes(keyframes, pastePosition);
}
public void CopyKeyframes()
{
List<ILayerPropertyKeyframe> keyframes = GetAllKeyframeViewModels().Where(k => k.IsSelected).Select(k => k.Keyframe).ToList();
CopyKeyframes(keyframes);
}
public void PasteKeyframes(ITimelineKeyframeViewModel viewModel = null)
{
TimeSpan pastePosition = GetPastePosition(viewModel);
PasteKeyframes(pastePosition);
}
private TimeSpan GetPastePosition(ITimelineKeyframeViewModel viewModel)
{
TimeSpan pastePosition = _profileEditorService.CurrentTime;
// If a keyframe VM is provided, paste onto there
if (viewModel != null)
pastePosition = viewModel.Position;
// Paste at the position the context menu was opened
else if (_contextMenuOpenPosition != null)
pastePosition = _contextMenuOpenPosition.Value;
return pastePosition;
}
public List<ILayerPropertyKeyframe> DuplicateKeyframes(List<ILayerPropertyKeyframe> keyframes, TimeSpan pastePosition)
{
KeyframeClipboardModel clipboardModel = CoreJson.DeserializeObject<KeyframeClipboardModel>(CoreJson.SerializeObject(new KeyframeClipboardModel(keyframes), true), true);
return PasteClipboardData(clipboardModel, pastePosition);
}
public void CopyKeyframes(List<ILayerPropertyKeyframe> keyframes)
{
KeyframeClipboardModel clipboardModel = new KeyframeClipboardModel(keyframes);
JsonClipboard.SetObject(clipboardModel);
}
public List<ILayerPropertyKeyframe> PasteKeyframes(TimeSpan pastePosition)
{
KeyframeClipboardModel clipboardObject = JsonClipboard.GetData<KeyframeClipboardModel>();
return PasteClipboardData(clipboardObject, pastePosition);
}
private List<ILayerPropertyKeyframe> PasteClipboardData(KeyframeClipboardModel clipboardModel, TimeSpan pastePosition)
{
List<ILayerPropertyKeyframe> pasted = new List<ILayerPropertyKeyframe>();
if (clipboardModel == null)
return pasted;
RenderProfileElement target = _profileEditorService.SelectedProfileElement;
if (target == null)
return pasted;
clipboardModel.Paste(target, pastePosition);
return pasted;
} }
#endregion #endregion
@ -254,6 +335,9 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
// ReSharper disable once UnusedMember.Global - Called from view // ReSharper disable once UnusedMember.Global - Called from view
public void TimelineCanvasMouseDown(object sender, MouseButtonEventArgs e) public void TimelineCanvasMouseDown(object sender, MouseButtonEventArgs e)
{ {
// Workaround for focus not being applied to the grid causing keybinds not to function
((IInputElement) sender).Focus();
if (e.LeftButton == MouseButtonState.Released) if (e.LeftButton == MouseButtonState.Released)
return; return;

View File

@ -32,7 +32,8 @@
HorizontalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"
dd:DragDrop.IsDragSource="True" dd:DragDrop.IsDragSource="True"
dd:DragDrop.IsDropTarget="True" dd:DragDrop.IsDropTarget="True"
dd:DragDrop.DropHandler="{Binding}"> dd:DragDrop.DropHandler="{Binding}"
ContextMenuOpening="{s:Action ContextMenuOpening}">
<TreeView.InputBindings> <TreeView.InputBindings>
<KeyBinding Key="F2" Command="{s:Action RenameElement}" s:View.ActionTarget="{Binding SelectedTreeItem}" /> <KeyBinding Key="F2" Command="{s:Action RenameElement}" s:View.ActionTarget="{Binding SelectedTreeItem}" />
<KeyBinding Key="Delete" Command="{s:Action DeleteElement}" s:View.ActionTarget="{Binding SelectedTreeItem}" /> <KeyBinding Key="Delete" Command="{s:Action DeleteElement}" s:View.ActionTarget="{Binding SelectedTreeItem}" />
@ -40,6 +41,47 @@
<KeyBinding Key="C" Modifiers="Control" Command="{s:Action CopyElement}" s:View.ActionTarget="{Binding SelectedTreeItem}" /> <KeyBinding Key="C" Modifiers="Control" Command="{s:Action CopyElement}" s:View.ActionTarget="{Binding SelectedTreeItem}" />
<KeyBinding Key="V" Modifiers="Control" Command="{s:Action PasteElement}" s:View.ActionTarget="{Binding SelectedTreeItem}" /> <KeyBinding Key="V" Modifiers="Control" Command="{s:Action PasteElement}" s:View.ActionTarget="{Binding SelectedTreeItem}" />
</TreeView.InputBindings> </TreeView.InputBindings>
<TreeView.ContextMenu>
<ContextMenu>
<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>
<Separator />
<MenuItem Header="Duplicate" Command="{s:Action DuplicateElement}" InputGestureText="Ctrl+D" IsEnabled="False">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="ContentDuplicate" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Copy" Command="{s:Action CopyElement}" InputGestureText="Ctrl+C" IsEnabled="False">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="ContentCopy" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Paste" Command="{s:Action PasteElement}" InputGestureText="Ctrl+V">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="ContentPaste" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Rename" Command="{s:Action RenameElement}" InputGestureText="F2" IsEnabled="False">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="RenameBox" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Delete" Command="{s:Action DeleteElement}" InputGestureText="Del" IsEnabled="False">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="TrashCan" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</TreeView.ContextMenu>
<b:Interaction.Behaviors> <b:Interaction.Behaviors>
<behaviors:TreeViewSelectionBehavior ExpandSelected="True" SelectedItem="{Binding SelectedTreeItem}" /> <behaviors:TreeViewSelectionBehavior ExpandSelected="True" SelectedItem="{Binding SelectedTreeItem}" />
</b:Interaction.Behaviors> </b:Interaction.Behaviors>

View File

@ -2,6 +2,7 @@
using System.Linq; using System.Linq;
using System.Windows; using System.Windows;
using Artemis.Core; using Artemis.Core;
using Artemis.Storage.Entities.Profile;
using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem; using Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem;
using Artemis.UI.Shared; using Artemis.UI.Shared;
@ -41,17 +42,7 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree
} }
} }
// ReSharper disable once UnusedMember.Global - Called from view public bool CanPasteElement => _profileEditorService.GetCanPaste();
public void AddFolder()
{
ActiveItem?.AddFolder();
}
// ReSharper disable once UnusedMember.Global - Called from view
public void AddLayer()
{
ActiveItem?.AddLayer();
}
protected override void OnInitialActivate() protected override void OnInitialActivate()
{ {
@ -79,10 +70,12 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree
_updatingTree = false; _updatingTree = false;
} }
#region IDropTarget
private static DragDropType GetDragDropType(IDropInfo dropInfo) private static DragDropType GetDragDropType(IDropInfo dropInfo)
{ {
TreeItemViewModel source = (TreeItemViewModel) dropInfo.Data; TreeItemViewModel source = (TreeItemViewModel)dropInfo.Data;
TreeItemViewModel target = (TreeItemViewModel) dropInfo.TargetItem; TreeItemViewModel target = (TreeItemViewModel)dropInfo.TargetItem;
if (source == target) if (source == target)
return DragDropType.None; return DragDropType.None;
@ -128,14 +121,14 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree
public void Drop(IDropInfo dropInfo) public void Drop(IDropInfo dropInfo)
{ {
TreeItemViewModel source = (TreeItemViewModel) dropInfo.Data; TreeItemViewModel source = (TreeItemViewModel)dropInfo.Data;
TreeItemViewModel target = (TreeItemViewModel) dropInfo.TargetItem; TreeItemViewModel target = (TreeItemViewModel)dropInfo.TargetItem;
DragDropType dragDropType = GetDragDropType(dropInfo); DragDropType dragDropType = GetDragDropType(dropInfo);
switch (dragDropType) switch (dragDropType)
{ {
case DragDropType.Add: case DragDropType.Add:
((TreeItemViewModel) source.Parent).RemoveExistingElement(source); ((TreeItemViewModel)source.Parent).RemoveExistingElement(source);
target.AddExistingElement(source); target.AddExistingElement(source);
break; break;
case DragDropType.InsertBefore: case DragDropType.InsertBefore:
@ -151,6 +144,34 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree
Subscribe(); Subscribe();
} }
#endregion
#region Context menu
public void AddFolder()
{
ActiveItem?.AddFolder();
}
public void AddLayer()
{
ActiveItem?.AddLayer();
}
public void PasteElement()
{
Folder rootFolder = _profileEditorService.SelectedProfile?.GetRootFolder();
if (rootFolder != null)
_profileEditorService.PasteProfileElement(rootFolder, rootFolder.Children.Count);
}
public void ContextMenuOpening(object sender, EventArgs e)
{
NotifyOfPropertyChange(nameof(CanPasteElement));
}
#endregion
#region Event handlers #region Event handlers
private void Subscribe() private void Subscribe()

View File

@ -10,7 +10,7 @@
d:DesignHeight="450" d:DesignWidth="800" d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance {x:Type treeItem1:FolderViewModel}}"> d:DataContext="{d:DesignInstance {x:Type treeItem1:FolderViewModel}}">
<!-- Capture clicks on full tree view item --> <!-- Capture clicks on full tree view item -->
<StackPanel Margin="-10" Background="Transparent"> <StackPanel Margin="-10" Background="Transparent" ContextMenuOpening="{s:Action ContextMenuOpening}">
<StackPanel.ContextMenu> <StackPanel.ContextMenu>
<ContextMenu> <ContextMenu>
<MenuItem Header="Add new folder" Command="{s:Action AddFolder}"> <MenuItem Header="Add new folder" Command="{s:Action AddFolder}">

View File

@ -10,7 +10,7 @@
d:DesignHeight="450" d:DesignWidth="800" d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance {x:Type treeItem1:LayerViewModel}}"> d:DataContext="{d:DesignInstance {x:Type treeItem1:LayerViewModel}}">
<!-- Capture clicks on full tree view item --> <!-- Capture clicks on full tree view item -->
<StackPanel Margin="-10" Background="Transparent"> <StackPanel Margin="-10" Background="Transparent" ContextMenuOpening="{s:Action ContextMenuOpening}">
<StackPanel.ContextMenu> <StackPanel.ContextMenu>
<ContextMenu> <ContextMenu>
<MenuItem Header="Duplicate" Command="{s:Action DuplicateElement}" InputGestureText="Ctrl+D"> <MenuItem Header="Duplicate" Command="{s:Action DuplicateElement}" InputGestureText="Ctrl+D">

View File

@ -48,6 +48,8 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem
set => SetAndNotify(ref _profileElement, value); set => SetAndNotify(ref _profileElement, value);
} }
public bool CanPasteElement => _profileEditorService.GetCanPaste();
public abstract bool SupportsChildren { get; } public abstract bool SupportsChildren { get; }
public List<TreeItemViewModel> GetAllChildren() public List<TreeItemViewModel> GetAllChildren()
@ -254,6 +256,11 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem
_profileEditorService.UpdateSelectedProfile(); _profileEditorService.UpdateSelectedProfile();
} }
public void ContextMenuOpening(object sender, EventArgs e)
{
NotifyOfPropertyChange(nameof(CanPasteElement));
}
private void Subscribe() private void Subscribe()
{ {
ProfileElement.ChildAdded += ProfileElementOnChildAdded; ProfileElement.ChildAdded += ProfileElementOnChildAdded;