using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using Artemis.Core; using Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.Dialogs; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; using Stylet; namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline { public sealed class TimelineSegmentViewModel : Screen { private readonly IDialogService _dialogService; private bool _draggingSegment; private bool _segmentEnabled; private TimeSpan _segmentEnd; private TimeSpan _segmentLength; private TimeSpan _segmentStart; private double _segmentStartPosition; private double _segmentWidth; private bool _showDisableButton; private bool _showRepeatButton; private bool _showSegmentName; public TimelineSegmentViewModel(SegmentViewModelType segment, IObservableCollection layerPropertyGroups, IProfileEditorService profileEditorService, IDialogService dialogService) { _dialogService = dialogService; ProfileEditorService = profileEditorService; Segment = segment; LayerPropertyGroups = layerPropertyGroups; if (Segment == SegmentViewModelType.Start) ToolTip = "This segment is played when a layer starts displaying because it's conditions are met"; else if (Segment == SegmentViewModelType.Main) ToolTip = "This segment is played while a condition is met, either once or on a repeating loop"; else if (Segment == SegmentViewModelType.End) ToolTip = "This segment is played once a condition is no longer met"; IsMainSegment = Segment == SegmentViewModelType.Main; } public RenderProfileElement SelectedProfileElement => ProfileEditorService.SelectedProfileElement; public SegmentViewModelType Segment { get; } public IObservableCollection LayerPropertyGroups { get; } public IProfileEditorService ProfileEditorService { get; } public string ToolTip { get; } public bool IsMainSegment { get; } public TimeSpan SegmentLength { get => _segmentLength; set => SetAndNotify(ref _segmentLength, value); } public TimeSpan SegmentStart { get => _segmentStart; set => SetAndNotify(ref _segmentStart, value); } public TimeSpan SegmentEnd { get => _segmentEnd; set => SetAndNotify(ref _segmentEnd, value); } public double SegmentWidth { get => _segmentWidth; set => SetAndNotify(ref _segmentWidth, value); } public double SegmentStartPosition { get => _segmentStartPosition; set => SetAndNotify(ref _segmentStartPosition, value); } public bool SegmentEnabled { get => _segmentEnabled; set => SetAndNotify(ref _segmentEnabled, value); } // Only the main segment supports this, for any other segment the getter always returns false and the setter does nothing public bool RepeatSegment { get { if (Segment != SegmentViewModelType.Main) return false; return SelectedProfileElement?.Timeline.PlayMode == TimelinePlayMode.Repeat; } set { if (Segment != SegmentViewModelType.Main || SelectedProfileElement == null) return; SelectedProfileElement.Timeline.PlayMode = value ? TimelinePlayMode.Repeat : TimelinePlayMode.Once; ProfileEditorService.UpdateSelectedProfileElement(); NotifyOfPropertyChange(nameof(RepeatSegment)); } } public bool ShowSegmentName { get => _showSegmentName; set => SetAndNotify(ref _showSegmentName, value); } public bool ShowRepeatButton { get => _showRepeatButton; set => SetAndNotify(ref _showRepeatButton, value); } public bool ShowDisableButton { get => _showDisableButton; set => SetAndNotify(ref _showDisableButton, value); } public async Task OpenSettingsDialog() { await _dialogService.ShowDialog(new Dictionary {{"segment", this}}); } public void ShiftNextSegment(TimeSpan amount) { if (Segment == SegmentViewModelType.Start) ShiftKeyframes(SelectedProfileElement.Timeline.StartSegmentEndPosition, null, amount); else if (Segment == SegmentViewModelType.Main) ShiftKeyframes(SelectedProfileElement.Timeline.MainSegmentEndPosition, null, amount); else if (Segment == SegmentViewModelType.End) ShiftKeyframes(SelectedProfileElement.Timeline.EndSegmentEndPosition, null, amount); } private void WipeKeyframes(TimeSpan? start, TimeSpan? end) { foreach (LayerPropertyGroupViewModel layerPropertyGroupViewModel in LayerPropertyGroups) layerPropertyGroupViewModel.WipeKeyframes(start, end); } private void ShiftKeyframes(TimeSpan? start, TimeSpan? end, TimeSpan amount) { foreach (LayerPropertyGroupViewModel layerPropertyGroupViewModel in LayerPropertyGroups) layerPropertyGroupViewModel.ShiftKeyframes(start, end, amount); } #region Overrides of Screen /// protected override void OnInitialActivate() { ProfileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged; ProfileEditorService.ProfileElementSelected += ProfileEditorServiceOnProfileElementSelected; if (ProfileEditorService.SelectedProfileElement != null) ProfileEditorService.SelectedProfileElement.Timeline.PropertyChanged += TimelineOnPropertyChanged; Update(); base.OnInitialActivate(); } /// protected override void OnClose() { ProfileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged; ProfileEditorService.ProfileElementSelected -= ProfileEditorServiceOnProfileElementSelected; if (SelectedProfileElement != null) SelectedProfileElement.Timeline.PropertyChanged -= TimelineOnPropertyChanged; base.OnClose(); } #endregion #region Updating private void UpdateHeader() { if (!IsMainSegment) ShowSegmentName = SegmentWidth > 60; else ShowSegmentName = SegmentWidth > 80; ShowRepeatButton = SegmentWidth > 45 && IsMainSegment; ShowDisableButton = SegmentWidth > 25; if (Segment == SegmentViewModelType.Main) NotifyOfPropertyChange(nameof(RepeatSegment)); } private void Update() { if (SelectedProfileElement == null) { SegmentStart = TimeSpan.Zero; SegmentEnd = TimeSpan.Zero; SegmentLength = TimeSpan.Zero; SegmentStartPosition = 0; SegmentWidth = 0; SegmentEnabled = false; return; } // It would be nice to do this differently if this patterns end up appearing in more places if (Segment == SegmentViewModelType.Start) { SegmentStart = TimeSpan.Zero; SegmentEnd = SelectedProfileElement.Timeline.StartSegmentEndPosition; SegmentLength = SelectedProfileElement.Timeline.StartSegmentLength; } else if (Segment == SegmentViewModelType.Main) { SegmentStart = SelectedProfileElement.Timeline.MainSegmentStartPosition; SegmentEnd = SelectedProfileElement.Timeline.MainSegmentEndPosition; SegmentLength = SelectedProfileElement.Timeline.MainSegmentLength; } else if (Segment == SegmentViewModelType.End) { SegmentStart = SelectedProfileElement.Timeline.EndSegmentStartPosition; SegmentEnd = SelectedProfileElement.Timeline.EndSegmentEndPosition; SegmentLength = SelectedProfileElement.Timeline.EndSegmentLength; } SegmentStartPosition = SegmentStart.TotalSeconds * ProfileEditorService.PixelsPerSecond; SegmentWidth = SegmentLength.TotalSeconds * ProfileEditorService.PixelsPerSecond; SegmentEnabled = SegmentLength != TimeSpan.Zero; UpdateHeader(); } #endregion #region Controls public void DisableSegment() { TimeSpan oldSegmentLength = SegmentLength; if (Segment == SegmentViewModelType.Start) { // Remove keyframes that fall in this segment WipeKeyframes(null, SelectedProfileElement.Timeline.StartSegmentEndPosition); SelectedProfileElement.Timeline.StartSegmentLength = TimeSpan.Zero; } else if (Segment == SegmentViewModelType.Main) { // Remove keyframes that fall in this segment WipeKeyframes(SelectedProfileElement.Timeline.MainSegmentStartPosition, SelectedProfileElement.Timeline.MainSegmentEndPosition); SelectedProfileElement.Timeline.MainSegmentLength = TimeSpan.Zero; } else if (Segment == SegmentViewModelType.End) { // Remove keyframes that fall in this segment WipeKeyframes(SelectedProfileElement.Timeline.EndSegmentStartPosition, SelectedProfileElement.Timeline.EndSegmentEndPosition); SelectedProfileElement.Timeline.EndSegmentLength = TimeSpan.Zero; } ShiftNextSegment(SegmentLength - oldSegmentLength); ProfileEditorService.UpdateSelectedProfileElement(); Update(); } public void EnableSegment() { ShiftNextSegment(TimeSpan.FromSeconds(1)); if (Segment == SegmentViewModelType.Start) SelectedProfileElement.Timeline.StartSegmentLength = TimeSpan.FromSeconds(1); else if (Segment == SegmentViewModelType.Main) SelectedProfileElement.Timeline.MainSegmentLength = TimeSpan.FromSeconds(1); else if (Segment == SegmentViewModelType.End) SelectedProfileElement.Timeline.EndSegmentLength = TimeSpan.FromSeconds(1); ProfileEditorService.UpdateSelectedProfileElement(); Update(); } #endregion #region Mouse events public void SegmentMouseDown(object sender, MouseButtonEventArgs e) { ((IInputElement) sender).CaptureMouse(); _draggingSegment = true; } public void SegmentMouseUp(object sender, MouseButtonEventArgs e) { ((IInputElement) sender).ReleaseMouseCapture(); _draggingSegment = false; ProfileEditorService.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 ScrollViewer parent = VisualTreeUtilities.FindParent((DependencyObject) sender, "TimelineHeaderScrollViewer"); double x = Math.Max(0, e.GetPosition(parent).X); TimeSpan newTime = TimeSpan.FromSeconds(x / ProfileEditorService.PixelsPerSecond); // Round the time to something that fits the current zoom level if (ProfileEditorService.PixelsPerSecond < 200) newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 5.0) * 5.0); else if (ProfileEditorService.PixelsPerSecond < 500) newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 2.0) * 2.0); else newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds)); // If holding down shift, snap to the closest element on the timeline if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) { List keyframeTimes = LayerPropertyGroups.SelectMany(g => g.GetAllKeyframeViewModels(true)).Select(k => k.Position).ToList(); newTime = ProfileEditorService.SnapToTimeline(newTime, TimeSpan.FromMilliseconds(1000f / ProfileEditorService.PixelsPerSecond * 5), false, true, keyframeTimes); } // If holding down control, round to the closest 50ms else if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) { newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 50.0) * 50.0); } UpdateEndPosition(newTime); } public void UpdateEndPosition(TimeSpan end) { if (end < TimeSpan.FromMilliseconds(100)) end = TimeSpan.FromMilliseconds(100); if (Segment == SegmentViewModelType.Start) SelectedProfileElement.Timeline.StartSegmentEndPosition = end; else if (Segment == SegmentViewModelType.Main) SelectedProfileElement.Timeline.MainSegmentEndPosition = end; else if (Segment == SegmentViewModelType.End) SelectedProfileElement.Timeline.EndSegmentEndPosition = end; Update(); } public void UpdateLength(TimeSpan length) { if (length < TimeSpan.FromMilliseconds(100)) length = TimeSpan.FromMilliseconds(100); if (Segment == SegmentViewModelType.Start) SelectedProfileElement.Timeline.StartSegmentLength = length; else if (Segment == SegmentViewModelType.Main) SelectedProfileElement.Timeline.MainSegmentLength = length; else if (Segment == SegmentViewModelType.End) SelectedProfileElement.Timeline.EndSegmentLength = length; Update(); } #endregion #region Event handlers private void ProfileEditorServiceOnProfileElementSelected(object sender, RenderProfileElementEventArgs e) { if (e.PreviousRenderProfileElement != null) e.PreviousRenderProfileElement.Timeline.PropertyChanged -= TimelineOnPropertyChanged; if (e.RenderProfileElement != null) e.RenderProfileElement.Timeline.PropertyChanged += TimelineOnPropertyChanged; Update(); } private void TimelineOnPropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(Core.Timeline.StartSegmentLength) || e.PropertyName == nameof(Core.Timeline.MainSegmentLength) || e.PropertyName == nameof(Core.Timeline.EndSegmentLength)) Update(); else if (e.PropertyName == nameof(Core.Timeline.PlayMode)) NotifyOfPropertyChange(nameof(RepeatSegment)); } private void ProfileEditorServiceOnPixelsPerSecondChanged(object sender, EventArgs e) { Update(); } #endregion } public enum SegmentViewModelType { Start, Main, End } }