using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; using Artemis.Core; using Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.Models; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; using Stylet; namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline { public sealed class TimelineViewModel : Screen { private readonly IProfileEditorService _profileEditorService; private RectangleGeometry _selectionRectangle; public TimelineViewModel(LayerPropertiesViewModel layerPropertiesViewModel, IObservableCollection layerPropertyGroups, IProfileEditorService profileEditorService) { _profileEditorService = profileEditorService; LayerPropertiesViewModel = layerPropertiesViewModel; LayerPropertyGroups = layerPropertyGroups; SelectionRectangle = new RectangleGeometry(); } public LayerPropertiesViewModel LayerPropertiesViewModel { get; } public IObservableCollection LayerPropertyGroups { get; } public RectangleGeometry SelectionRectangle { get => _selectionRectangle; set => SetAndNotify(ref _selectionRectangle, value); } public double StartSegmentEndPosition { get => _startSegmentEndPosition; set => SetAndNotify(ref _startSegmentEndPosition, value); } public double MainSegmentEndPosition { get => _mainSegmentEndPosition; set => SetAndNotify(ref _mainSegmentEndPosition, value); } public double EndSegmentEndPosition { get => _endSegmentEndPosition; set => SetAndNotify(ref _endSegmentEndPosition, value); } public double TotalTimelineWidth { get => _totalTimelineWidth; set => SetAndNotify(ref _totalTimelineWidth, value); } private void Update() { StartSegmentEndPosition = LayerPropertiesViewModel.StartTimelineSegmentViewModel.SegmentEnd.TotalSeconds * _profileEditorService.PixelsPerSecond; MainSegmentEndPosition = LayerPropertiesViewModel.MainTimelineSegmentViewModel.SegmentEnd.TotalSeconds * _profileEditorService.PixelsPerSecond; EndSegmentEndPosition = LayerPropertiesViewModel.EndTimelineSegmentViewModel.SegmentEnd.TotalSeconds * _profileEditorService.PixelsPerSecond; TotalTimelineWidth = EndSegmentEndPosition; } private void TimelineOnPropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(Core.Timeline.StartSegmentLength) || e.PropertyName == nameof(Core.Timeline.MainSegmentLength) || e.PropertyName == nameof(Core.Timeline.EndSegmentLength)) Update(); } private void ProfileEditorServiceOnPixelsPerSecondChanged(object sender, EventArgs e) { Update(); } private void SelectedProfileEditorServiceOnSelectedProfileElementChanged(object sender, RenderProfileElementEventArgs e) { if (e.PreviousRenderProfileElement != null) e.PreviousRenderProfileElement.Timeline.PropertyChanged -= TimelineOnPropertyChanged; if (e.RenderProfileElement != null) e.RenderProfileElement.Timeline.PropertyChanged += TimelineOnPropertyChanged; Update(); } #region Overrides of Screen /// protected override void OnInitialActivate() { _profileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged; _profileEditorService.SelectedProfileElementChanged += SelectedProfileEditorServiceOnSelectedProfileElementChanged; if (_profileEditorService.SelectedProfileElement != null) _profileEditorService.SelectedProfileElement.Timeline.PropertyChanged += TimelineOnPropertyChanged; Update(); base.OnInitialActivate(); } /// protected override void OnClose() { _profileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged; _profileEditorService.SelectedProfileElementChanged -= SelectedProfileEditorServiceOnSelectedProfileElementChanged; if (_profileEditorService.SelectedProfileElement != null) _profileEditorService.SelectedProfileElement.Timeline.PropertyChanged -= TimelineOnPropertyChanged; base.OnClose(); } #endregion #region Command handlers public void KeyframeMouseDown(object sender, MouseButtonEventArgs e) { // if (e.LeftButton == MouseButtonState.Released) // return; if (!((sender as Ellipse)?.DataContext is ITimelineKeyframeViewModel viewModel)) return; ((IInputElement) sender).CaptureMouse(); if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift) && !viewModel.IsSelected) SelectKeyframe(viewModel, true, false); else if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) SelectKeyframe(viewModel, false, true); else if (!viewModel.IsSelected) SelectKeyframe(viewModel, false, false); e.Handled = true; } public void KeyframeMouseUp(object sender, MouseButtonEventArgs e) { _profileEditorService.SaveSelectedProfileElement(); ReleaseSelectedKeyframes(); ((IInputElement) sender).ReleaseMouseCapture(); } public void KeyframeMouseMove(object sender, MouseEventArgs e) { if (sender is Ellipse ellipse && ellipse.DataContext is ITimelineKeyframeViewModel viewModel) { if (e.LeftButton == MouseButtonState.Pressed) MoveSelectedKeyframes(GetCursorTime(e.GetPosition(View)), viewModel); e.Handled = true; } } #region Context menu actions public bool CanDuplicateKeyframes => GetAllKeyframeViewModels().Any(k => k.IsSelected); public bool CanCopyKeyframes => GetAllKeyframeViewModels().Any(k => k.IsSelected); public bool CanDeleteKeyframes => GetAllKeyframeViewModels().Any(k => k.IsSelected); public bool CanPasteKeyframes => JsonClipboard.GetData() is KeyframesClipboardModel; private TimeSpan? _contextMenuOpenPosition; public void ContextMenuOpening(object sender, ContextMenuEventArgs e) { _contextMenuOpenPosition = GetCursorTime(new Point(e.CursorLeft, e.CursorTop)); NotifyOfPropertyChange(nameof(CanDuplicateKeyframes)); NotifyOfPropertyChange(nameof(CanCopyKeyframes)); NotifyOfPropertyChange(nameof(CanDeleteKeyframes)); NotifyOfPropertyChange(nameof(CanPasteKeyframes)); } public void ContextMenuClosing(object sender, ContextMenuEventArgs e) { _contextMenuOpenPosition = null; } public void KeyframeContextMenuOpening(object sender, ContextMenuEventArgs e) { if (sender is FrameworkElement fe && fe.DataContext is ITimelineKeyframeViewModel viewModel) viewModel.PopulateEasingViewModels(); } public void KeyframeContextMenuClosing(object sender, ContextMenuEventArgs e) { if (sender is Ellipse ellipse && ellipse.DataContext is ITimelineKeyframeViewModel viewModel) viewModel.ClearEasingViewModels(); } public void DeleteKeyframes() { List keyframeViewModels = GetAllKeyframeViewModels().Where(k => k.IsSelected).ToList(); foreach (ITimelineKeyframeViewModel keyframeViewModel in keyframeViewModels) keyframeViewModel.Delete(false); _profileEditorService.SaveSelectedProfileElement(); } public void DuplicateKeyframes(object sender) { TimeSpan pastePosition = GetPastePosition(sender as ITimelineKeyframeViewModel); List keyframes = GetAllKeyframeViewModels().Where(k => k.IsSelected).Select(k => k.Keyframe).ToList(); List newKeyframes = DuplicateKeyframes(keyframes, pastePosition); // Select only the newly duplicated keyframes foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in GetAllKeyframeViewModels()) timelineKeyframeViewModel.IsSelected = newKeyframes.Contains(timelineKeyframeViewModel.Keyframe); _profileEditorService.SaveSelectedProfileElement(); } public void CopyKeyframes() { List keyframes = GetAllKeyframeViewModels().Where(k => k.IsSelected).Select(k => k.Keyframe).ToList(); CopyKeyframes(keyframes); } public void PasteKeyframes(object sender) { TimeSpan pastePosition = GetPastePosition(sender as ITimelineKeyframeViewModel); List newKeyframes = PasteKeyframes(pastePosition); // Select only the newly pasted keyframes foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in GetAllKeyframeViewModels()) timelineKeyframeViewModel.IsSelected = newKeyframes.Contains(timelineKeyframeViewModel.Keyframe); _profileEditorService.SaveSelectedProfileElement(); } private TimeSpan GetPastePosition(ITimelineKeyframeViewModel viewModel) { TimeSpan pastePosition = _profileEditorService.CurrentTime; // If a keyframe VM is provided, paste onto there if (viewModel != null) pastePosition = viewModel.Position; // Paste at the position the context menu was opened else if (_contextMenuOpenPosition != null) pastePosition = _contextMenuOpenPosition.Value; return pastePosition; } private List DuplicateKeyframes(List keyframes, TimeSpan pastePosition) { KeyframesClipboardModel clipboardModel = CoreJson.DeserializeObject(CoreJson.SerializeObject(new KeyframesClipboardModel(keyframes), true), true); return PasteClipboardData(clipboardModel, pastePosition); } private void CopyKeyframes(List keyframes) { KeyframesClipboardModel clipboardModel = new(keyframes); JsonClipboard.SetObject(clipboardModel); } private List PasteKeyframes(TimeSpan pastePosition) { KeyframesClipboardModel clipboardObject = JsonClipboard.GetData(); return PasteClipboardData(clipboardObject, pastePosition); } private List PasteClipboardData(KeyframesClipboardModel clipboardModel, TimeSpan pastePosition) { List pasted = new(); if (clipboardModel == null) return pasted; RenderProfileElement target = _profileEditorService.SelectedProfileElement; if (target == null) return pasted; return clipboardModel.Paste(target, pastePosition); } #endregion private TimeSpan GetCursorTime(Point position) { // Get the parent grid, need that for our position double x = Math.Max(0, position.X); TimeSpan time = TimeSpan.FromSeconds(x / _profileEditorService.PixelsPerSecond); // Round the time to something that fits the current zoom level if (_profileEditorService.PixelsPerSecond < 200) time = TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds / 5.0) * 5.0); else if (_profileEditorService.PixelsPerSecond < 500) time = TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds / 2.0) * 2.0); else time = TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds)); // If shift is held, snap to the current time // Take a tolerance of 5 pixels (half a keyframe width) if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) time = _profileEditorService.SnapToTimeline(time, TimeSpan.FromMilliseconds(1000f / _profileEditorService.PixelsPerSecond * 5), false, true); return time; } #endregion #region Keyframe movement public void MoveSelectedKeyframes(TimeSpan cursorTime, ITimelineKeyframeViewModel sourceKeyframeViewModel) { // Ensure the selection rectangle doesn't show, the view isn't aware of different types of dragging SelectionRectangle.Rect = new Rect(); List keyframeViewModels = GetAllKeyframeViewModels(); foreach (ITimelineKeyframeViewModel keyframeViewModel in keyframeViewModels.Where(k => k.IsSelected)) keyframeViewModel.SaveOffsetToKeyframe(sourceKeyframeViewModel); if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) cursorTime = _profileEditorService.SnapToTimeline( cursorTime, TimeSpan.FromMilliseconds(1000f / _profileEditorService.PixelsPerSecond * 5), true, false, keyframeViewModels.Where(k => k != sourceKeyframeViewModel).Select(k => k.Position).ToList() ); // If holding down control, round to the closest 50ms else if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) cursorTime = TimeSpan.FromMilliseconds(Math.Round(cursorTime.TotalMilliseconds / 50.0) * 50.0); sourceKeyframeViewModel.UpdatePosition(cursorTime); foreach (ITimelineKeyframeViewModel keyframeViewModel in keyframeViewModels.Where(k => k.IsSelected)) keyframeViewModel.ApplyOffsetToKeyframe(sourceKeyframeViewModel); _profileEditorService.UpdateProfilePreview(); } public void ReleaseSelectedKeyframes() { List keyframeViewModels = GetAllKeyframeViewModels(); foreach (ITimelineKeyframeViewModel keyframeViewModel in keyframeViewModels.Where(k => k.IsSelected)) keyframeViewModel.ReleaseMovement(); } #endregion #region Keyframe selection private Point _mouseDragStartPoint; private bool _mouseDragging; private double _startSegmentEndPosition; private double _mainSegmentEndPosition; private double _endSegmentEndPosition; private double _totalTimelineWidth; // ReSharper disable once UnusedMember.Global - Called from view public void TimelineCanvasMouseDown(object sender, MouseButtonEventArgs e) { // Workaround for focus not being applied to the grid causing keybinds not to function ((IInputElement) sender).Focus(); if (e.LeftButton == MouseButtonState.Released) return; ((IInputElement) sender).CaptureMouse(); SelectionRectangle.Rect = new Rect(); _mouseDragStartPoint = e.GetPosition((IInputElement) sender); _mouseDragging = true; e.Handled = true; } // ReSharper disable once UnusedMember.Global - Called from view public void TimelineCanvasMouseUp(object sender, MouseEventArgs e) { if (!_mouseDragging) return; Point position = e.GetPosition((IInputElement) sender); Rect selectedRect = new(_mouseDragStartPoint, position); SelectionRectangle.Rect = selectedRect; List keyframeViewModels = GetAllKeyframeViewModels(); List selectedKeyframes = HitTestUtilities.GetHitViewModels((Visual) sender, SelectionRectangle); foreach (ITimelineKeyframeViewModel keyframeViewModel in keyframeViewModels) keyframeViewModel.IsSelected = selectedKeyframes.Contains(keyframeViewModel); _mouseDragging = false; e.Handled = true; ((IInputElement) sender).ReleaseMouseCapture(); } public void TimelineCanvasMouseMove(object sender, MouseEventArgs e) { if (_mouseDragging && e.LeftButton == MouseButtonState.Pressed) { Point position = e.GetPosition((IInputElement) sender); Rect selectedRect = new(_mouseDragStartPoint, position); SelectionRectangle.Rect = selectedRect; e.Handled = true; } } public void SelectKeyframe(ITimelineKeyframeViewModel clicked, bool selectBetween, bool toggle) { List keyframeViewModels = GetAllKeyframeViewModels(); if (selectBetween) { int selectedIndex = keyframeViewModels.FindIndex(k => k.IsSelected); // If nothing is selected, select only the clicked if (selectedIndex == -1) { clicked.IsSelected = true; return; } foreach (ITimelineKeyframeViewModel keyframeViewModel in keyframeViewModels) keyframeViewModel.IsSelected = false; int clickedIndex = keyframeViewModels.IndexOf(clicked); if (clickedIndex < selectedIndex) foreach (ITimelineKeyframeViewModel keyframeViewModel in keyframeViewModels.Skip(clickedIndex).Take(selectedIndex - clickedIndex + 1)) keyframeViewModel.IsSelected = true; else foreach (ITimelineKeyframeViewModel keyframeViewModel in keyframeViewModels.Skip(selectedIndex).Take(clickedIndex - selectedIndex + 1)) keyframeViewModel.IsSelected = true; } else if (toggle) { // Toggle only the clicked keyframe, leave others alone clicked.IsSelected = !clicked.IsSelected; } else { // Only select the clicked keyframe foreach (ITimelineKeyframeViewModel keyframeViewModel in keyframeViewModels) keyframeViewModel.IsSelected = false; clicked.IsSelected = true; } } private List GetAllKeyframeViewModels() { List viewModels = new(); foreach (LayerPropertyGroupViewModel layerPropertyGroupViewModel in LayerPropertyGroups) viewModels.AddRange(layerPropertyGroupViewModel.GetAllKeyframeViewModels(false)); return viewModels; } #endregion } }