Skip to content

Commit

Permalink
Improved timeline ruler and play head functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
AngryCarrot789 committed Dec 6, 2024
1 parent ba4a1f5 commit f7f932f
Show file tree
Hide file tree
Showing 10 changed files with 295 additions and 220 deletions.
4 changes: 2 additions & 2 deletions FramePFX.Avalonia/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ public override async void OnFrameworkInitializationCompleted() {
#if !DEBUG
}
catch (Exception ex) {
await (new FramePFX.Avalonia.Services.MessageDialogServiceImpl().ShowMessage("App startup failed", "Failed to initialise application", ex.GetToString()));
Dispatcher.UIThread.InvokeShutdown();
await (new FramePFX.Avalonia.Services.MessageDialogServiceImpl().ShowMessage("App startup failed", "Failed to initialise application", ex.ToString()));
global::Avalonia.Threading.Dispatcher.UIThread.InvokeShutdown();
return;
}
#endif
Expand Down
2 changes: 1 addition & 1 deletion FramePFX.Avalonia/AvControls/FreeMoveViewPortV2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ private void OnPreviewMouseWheel(object? sender, PointerWheelEventArgs e) {
// https://gamedev.stackexchange.com/a/182177/160952
double oldzoom = this.ZoomScale;
double newzoom = oldzoom * (delta > 0 ? 1.1 : 0.9);
this.ZoomScale = newzoom;
this.ZoomScale = Math.Round(newzoom);
if (this.PanToCursorOnUserZoom) {
newzoom = this.ZoomScale;
Size size = this.Bounds.Size;
Expand Down
153 changes: 133 additions & 20 deletions FramePFX.Avalonia/Editing/Playheads/BasePlayHeadControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
using System;
using System.Diagnostics;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.Reactive;
using FramePFX.Avalonia.Editing.Timelines;
using FramePFX.Editing.Timelines;
Expand All @@ -31,29 +33,75 @@ public abstract class BasePlayHeadControl : TemplatedControl {
private IDisposable? zoomChangeHandler;
private IDisposable? timelineChangeHandler;
public static readonly StyledProperty<TimelineControl?> TimelineControlProperty = AvaloniaProperty.Register<BasePlayHeadControl, TimelineControl?>(nameof(TimelineControl));
public static readonly StyledProperty<PlayHeadType> PlayHeadTypeProperty = AvaloniaProperty.Register<BasePlayHeadControl, PlayHeadType>(nameof(PlayHeadType), PlayHeadType.PlayHead);
public static readonly StyledProperty<ScrollViewer?> ScrollViewerReferenceProperty = AvaloniaProperty.Register<BasePlayHeadControl, ScrollViewer?>(nameof(ScrollViewerReference));
public static readonly StyledProperty<Thickness> AdditionalOffsetProperty = AvaloniaProperty.Register<BasePlayHeadControl, Thickness>(nameof(AdditionalOffset));

public TimelineControl? TimelineControl {
get => this.GetValue(TimelineControlProperty);
set => this.SetValue(TimelineControlProperty, value);
}

public PlayHeadType PlayHeadType {
get => this.GetValue(PlayHeadTypeProperty);
set => this.SetValue(PlayHeadTypeProperty, value);
}

private Timeline? lastTimeline;
public ScrollViewer? ScrollViewerReference {
get => this.GetValue(ScrollViewerReferenceProperty);
set => this.SetValue(ScrollViewerReferenceProperty, value);
}

public Thickness AdditionalOffset {
get => this.GetValue(AdditionalOffsetProperty);
set => this.SetValue(AdditionalOffsetProperty, value);
}

private Timeline? currTimeline;
private long currFrame;
private double currZoom;

public long Frame {
get => this.currFrame;
set {
if (this.currTimeline == null)
throw new InvalidOperationException("No timeline attached");

if (value == this.currFrame)
return;

switch (this.PlayHeadType) {
case PlayHeadType.PlayHead: this.currTimeline.PlayHeadPosition = value; break;
case PlayHeadType.StopHead: this.currTimeline.StopHeadPosition = value; break;
}
}
}

protected BasePlayHeadControl() {
}

static BasePlayHeadControl() {
TimelineControlProperty.Changed.AddClassHandler<BasePlayHeadControl, TimelineControl?>((d, e) => d.OnTimelineControlChanged(e.OldValue.GetValueOrDefault(), e.NewValue.GetValueOrDefault()));
PlayHeadTypeProperty.Changed.AddClassHandler<BasePlayHeadControl, PlayHeadType>((d, e) => d.OnPlayHeadTypeChanged(e.OldValue.GetValueOrDefault(), e.NewValue.GetValueOrDefault()));
ScrollViewerReferenceProperty.Changed.AddClassHandler<BasePlayHeadControl, ScrollViewer?>((d, e) => d.OnScrollViewerReferenceChanged(e.OldValue.GetValueOrDefault(), e.NewValue.GetValueOrDefault()));
AdditionalOffsetProperty.Changed.AddClassHandler<BasePlayHeadControl, Thickness>((d, e) => d.OnAdditionalOffsetChanged(e.OldValue.GetValueOrDefault(), e.NewValue.GetValueOrDefault()));
}

public abstract long GetFrame(Timeline timeline);


private void OnAdditionalOffsetChanged(Thickness oldValue, Thickness newValue) {
this.UpdatePosition();
}

protected virtual void OnTimelineChanged(Timeline? oldTimeline, Timeline? newTimeline) {
Debug.Assert(this.lastTimeline == oldTimeline, "Different last timelines");
this.lastTimeline = newTimeline;
Debug.Assert(this.currTimeline == oldTimeline, "Different last timelines");
this.currTimeline = newTimeline;
if (oldTimeline != null) {
this.UnregisterPlayHeadEvents(oldTimeline, this.PlayHeadType);
}

if (newTimeline != null) {
// this.IsVisible = this.TimelineControl != null;
this.UpdateZoom();
this.RegisterPlayHeadEvents(newTimeline, this.PlayHeadType);
this.UpdatePosition();
}
else {
// this.IsVisible = false;
Expand All @@ -69,39 +117,104 @@ protected virtual void OnTimelineControlChanged(TimelineControl? oldTimeline, Ti
if (newTimeline != null) {
this.timelineChangeHandler = TimelineControl.TimelineProperty.Changed.Subscribe(new AnonymousObserver<AvaloniaPropertyChangedEventArgs<Timeline?>>((e) => this.OnTimelineChanged(e.OldValue.GetValueOrDefault(), e.NewValue.GetValueOrDefault())));
this.zoomChangeHandler = TimelineControl.ZoomProperty.Changed.Subscribe(new AnonymousObserver<AvaloniaPropertyChangedEventArgs<double>>(this.OnTimelineZoomed));
this.currZoom = newTimeline.Zoom;

Timeline? newTimelineModel = newTimeline.Timeline;
if (newTimelineModel != null) {
Debug.Assert(this.lastTimeline == oldTimeline?.Timeline, "Different last timelines");
this.lastTimeline = newTimelineModel;
Debug.Assert(this.currTimeline == oldTimeline?.Timeline, "Different last timelines");
this.currTimeline = newTimelineModel;
this.OnTimelineChanged(oldTimeline?.Timeline, newTimelineModel);
}

// this.IsVisible = newTimeline.Timeline != null;
this.UpdateZoom();
this.UpdatePosition();
}
else {
// this.IsVisible = false;
}
}

private void OnScrollViewerReferenceChanged(ScrollViewer? oldValue, ScrollViewer? newValue) {
if (oldValue != null) {
oldValue.ScrollChanged += this.OnScrollViewerScrollChanged;
oldValue.EffectiveViewportChanged += this.OnScrollViewerEffectiveViewPortChanged;
}

if (newValue != null) {
newValue.ScrollChanged += this.OnScrollViewerScrollChanged;
newValue.EffectiveViewportChanged += this.OnScrollViewerEffectiveViewPortChanged;
}
}

private void UpdateZoom() {
if (this.TimelineControl is TimelineControl control && control.Timeline is Timeline timeline)
this.SetPixelFromFrameAndZoom(this.GetFrame(timeline), control.Zoom);
private void OnScrollViewerEffectiveViewPortChanged(object? sender, EffectiveViewportChangedEventArgs e) => this.UpdatePosition();

private void OnScrollViewerScrollChanged(object? sender, ScrollChangedEventArgs e) => this.UpdatePosition();

private void OnPlayHeadTypeChanged(PlayHeadType oldValue, PlayHeadType newValue) {
if (this.currTimeline != null) {
this.UnregisterPlayHeadEvents(this.currTimeline, oldValue);
this.RegisterPlayHeadEvents(this.currTimeline, newValue);
}
}

private void OnTimelineZoomed(AvaloniaPropertyChangedEventArgs<double> e) {
if (this.TimelineControl?.Timeline is Timeline timeline)
this.SetPixelFromFrameAndZoom(this.GetFrame(timeline), e.NewValue.GetValueOrDefault(1.0));
private void UnregisterPlayHeadEvents(Timeline timeline, PlayHeadType playHeadType) {
switch (playHeadType) {
case PlayHeadType.None: break;
case PlayHeadType.PlayHead: timeline.PlayHeadChanged -= this.OnPlayHeadValueChanged; break;
case PlayHeadType.StopHead: timeline.StopHeadChanged -= this.OnPlayHeadValueChanged; break;
}
}

private void RegisterPlayHeadEvents(Timeline timeline, PlayHeadType playHeadType) {
switch (playHeadType) {
case PlayHeadType.None: break;
case PlayHeadType.PlayHead:
timeline.PlayHeadChanged += this.OnPlayHeadValueChanged;
this.currFrame = timeline.PlayHeadPosition;
return;
case PlayHeadType.StopHead:
timeline.StopHeadChanged += this.OnPlayHeadValueChanged;
this.currFrame = timeline.PlayHeadPosition;
return;
}
}

private void OnPlayHeadValueChanged(Timeline timeline, long oldValue, long newValue) {
this.currFrame = newValue;
this.UpdatePosition();
}

protected void SetPixelFromFrame(long frame) {
if (this.TimelineControl is TimelineControl control)
this.SetPixelFromFrameAndZoom(frame, control.Zoom);
private void OnTimelineZoomed(AvaloniaPropertyChangedEventArgs<double> e) {
this.currZoom = e.NewValue.GetValueOrDefault(1.0);
this.UpdatePosition();
}

private void UpdatePosition() {
this.SetPixelFromFrameAndZoom(this.currFrame, this.TimelineControl?.Zoom ?? 1.0);
}

protected virtual void SetPixelFromFrameAndZoom(long frame, double zoom) {
Thickness m = this.Margin;
this.SetPixelMargin(new Thickness(frame * zoom, m.Top, m.Right, m.Bottom));
double left;
if (this.ScrollViewerReference is ScrollViewer scroller) {
// Point translated = scroller.TranslatePoint(new Point(frame * zoom, 0.0), this) ?? new Point(scroller.Offset.X, 0.0);
double offset = scroller.Offset.X;
// double start = zoom - (offset - (long) (offset / zoom) * zoom);
// double firstMajor = offset % zoom == 0D ? offset : offset + (zoom - offset % zoom);
// double firstMajorRelative = zoom - (offset - firstMajor + zoom);
// // left = (frame * zoom) - offset;
// // left = (frame * zoom) - ((long) (offset / zoom) * zoom) - (this is GrippedPlayHeadControl ? 7 : 0);
// Point? translated2 = this.TranslatePoint(new Point(this.Bounds.Width / 2.0, 0.0), (Visual) scroller.Content!);
// double offset3 = (translated2?.X - m.Left) ?? 0.0;

left = (frame * zoom) - offset;
}
else {
left = (frame * zoom);
}

Thickness a = this.AdditionalOffset;
this.SetPixelMargin(new Thickness(left + a.Left, m.Top + a.Top, m.Right + a.Right, m.Bottom + a.Bottom));
}

protected virtual void SetPixelMargin(Thickness thickness) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

namespace FramePFX.Avalonia.Editing.Playheads;

public class StopHeadControl : BasePlayHeadControl {
public class FlatLinePlayHeadControl : BasePlayHeadControl {
private const int StateNone = 0;
private const int StateInit = 1;
private const int StateActive = 2;
Expand All @@ -34,7 +34,7 @@ public class StopHeadControl : BasePlayHeadControl {
private Point clickPoint;
private int dragState;

public StopHeadControl() {
public FlatLinePlayHeadControl() {
this.Focusable = true;
}

Expand Down Expand Up @@ -84,7 +84,7 @@ protected override void OnPointerMoved(PointerEventArgs e) {
}

Point diff = mPos - this.clickPoint;
long oldFrame = timeline.StopHeadPosition;
long oldFrame = this.Frame;
if (Math.Abs(diff.X) >= 1.0d) {
long offset = (long) Math.Round(diff.X / control.Zoom);
if (offset != 0) {
Expand All @@ -96,7 +96,7 @@ protected override void OnPointerMoved(PointerEventArgs e) {
}

if (offset != 0) {
timeline.StopHeadPosition = Math.Min(oldFrame + offset, timeline.MaxDuration - 1);
this.Frame = Math.Min(oldFrame + offset, timeline.MaxDuration - 1);
}
}
}
Expand All @@ -105,24 +105,4 @@ protected override void OnPointerMoved(PointerEventArgs e) {
private void SetDragState(int state) {
this.dragState = state;
}

public override long GetFrame(Timeline timeline) {
return timeline.StopHeadPosition;
}

protected override void OnTimelineChanged(Timeline? oldTimeline, Timeline? newTimeline) {
base.OnTimelineChanged(oldTimeline, newTimeline);
if (oldTimeline != null) {
oldTimeline.StopHeadChanged -= this.OnTimelineStopHeadChanged;
}

if (newTimeline != null) {
newTimeline.StopHeadChanged += this.OnTimelineStopHeadChanged;
}
}

private void OnTimelineStopHeadChanged(Timeline timeline, long oldvalue, long newvalue) {
if (this.TimelineControl is TimelineControl control)
this.SetPixelFromFrameAndZoom(newvalue, control.Zoom);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,30 +27,11 @@

namespace FramePFX.Avalonia.Editing.Playheads;

public class PlayHeadControl : BasePlayHeadControl {
public class GrippedPlayHeadControl : BasePlayHeadControl {
private Thumb? PART_ThumbHead;
private Thumb? PART_ThumbBody;

public PlayHeadControl() {
}

public override long GetFrame(Timeline timeline) {
return timeline.PlayHeadPosition;
}

protected override void OnTimelineChanged(Timeline? oldTimeline, Timeline? newTimeline) {
base.OnTimelineChanged(oldTimeline, newTimeline);
if (oldTimeline != null) {
oldTimeline.PlayHeadChanged -= this.OnTimelinePlayHeadChanged;
}

if (newTimeline != null) {
newTimeline.PlayHeadChanged += this.OnTimelinePlayHeadChanged;
}
}

private void OnTimelinePlayHeadChanged(Timeline timeline, long oldvalue, long newvalue) {
this.SetPixelFromFrameAndZoom(newvalue, this.TimelineControl?.Zoom ?? 1.0);
public GrippedPlayHeadControl() {
}

protected override void OnApplyTemplate(TemplateAppliedEventArgs e) {
Expand All @@ -73,17 +54,14 @@ private void PART_ThumbOnDragDelta(object? sender, VectorEventArgs e) {

long change = (long) Math.Round(e.Vector.X / control.Zoom);
if (change != 0) {
long oldFrame = timeline.PlayHeadPosition;
long oldFrame = this.Frame;
long newFrame = Math.Max(oldFrame + change, 0);
if (newFrame >= timeline.MaxDuration) {
newFrame = timeline.MaxDuration - 1;
}

if (newFrame != oldFrame) {
timeline.PlayHeadPosition = newFrame;

// Don't update stop head when dragging on the ruler
// timeline.StopHeadPosition = newFrame;
this.Frame = newFrame;
}
}
}
Expand Down Expand Up @@ -111,16 +89,4 @@ public void EnableDragging(PointerEventArgs e) {
e.PreventGestureRecognition();
thumb.RaiseEvent(ev);
}

protected override Size MeasureCore(Size availableSize) {
return base.MeasureCore(availableSize);
}

protected override void ArrangeCore(Rect finalRect) {
base.ArrangeCore(finalRect);
}

protected override Size ArrangeOverride(Size finalSize) {
return base.ArrangeOverride(finalSize);
}
}
Loading

0 comments on commit f7f932f

Please sign in to comment.