Skip to content

Commit

Permalink
Added automation editor to tracks, and implemented a somewhat working…
Browse files Browse the repository at this point in the history
… ruler
  • Loading branch information
AngryCarrot789 committed Dec 6, 2024
1 parent b688c6e commit ba4a1f5
Show file tree
Hide file tree
Showing 31 changed files with 585 additions and 140 deletions.
2 changes: 0 additions & 2 deletions FramePFX.Avalonia/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,8 @@
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using FramePFX.Avalonia.Shortcuts.Avalonia;
using FramePFX.Editing;
using FramePFX.Utils;

namespace FramePFX.Avalonia;

Expand Down
39 changes: 27 additions & 12 deletions FramePFX.Avalonia/Editing/Automation/AutomationEditorControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public class AutomationEditorControl : Control {

public static readonly StyledProperty<double> HorizontalZoomProperty = AvaloniaProperty.Register<AutomationEditorControl, double>(nameof(HorizontalZoom));
public static readonly StyledProperty<AutomationSequence?> AutomationSequenceProperty = AvaloniaProperty.Register<AutomationEditorControl, AutomationSequence?>(nameof(AutomationSequence));
public static readonly StyledProperty<IBrush?> ShapeBrushProperty = AvaloniaProperty.Register<AutomationEditorControl, IBrush?>(nameof(ShapeBrush), Brushes.OrangeRed);
public static readonly StyledProperty<IBrush?> OverrideShapeBrushProperty = AvaloniaProperty.Register<AutomationEditorControl, IBrush?>(nameof(OverrideShapeBrush), Brushes.DarkGray);

public double HorizontalZoom {
get => this.GetValue(HorizontalZoomProperty);
Expand All @@ -53,6 +55,16 @@ public AutomationSequence? AutomationSequence {
get => this.GetValue(AutomationSequenceProperty);
set => this.SetValue(AutomationSequenceProperty, value);
}

public IBrush? ShapeBrush {
get => this.GetValue(ShapeBrushProperty);
set => this.SetValue(ShapeBrushProperty, value);
}

public IBrush? OverrideShapeBrush {
get => this.GetValue(OverrideShapeBrushProperty);
set => this.SetValue(OverrideShapeBrushProperty, value);
}

public bool IsValueRangeHuge { get; private set; }

Expand Down Expand Up @@ -140,23 +152,21 @@ public override void Render(DrawingContext ctx) {
return;
}


// Screen offset is used to offset all rendering due to margins and extra borders on the parent.
// Clips are -1,0 because clips have a natural border thickness of 1,0 so we need to move back to 0,0.
// Tracks are 0,0 since they have no borders. The track panel has a spacing of 1 but that doesn't count
Point offset = this.ScreenOffset;
double zoom = this.HorizontalZoom;

// TODO: cache maybe
IBrush? overrideBrush = sequence.IsOverrideEnabled ? Brushes.Gray : null;
IBrush theBrush = overrideBrush ?? Brushes.DeepSkyBlue;
IBrush? overrideBrush = sequence.IsOverrideEnabled ? (this.OverrideShapeBrush ?? Brushes.Gray) : null;
IBrush theBrush = overrideBrush ?? this.ShapeBrush ?? Brushes.OrangeRed;
Pen ellipsePen = new Pen(theBrush, EllipseThickness);
Pen linePen = new Pen(theBrush, LineThickness);

if (sequence.IsEmpty) {
Pen dottedLinePen = new Pen(theBrush, LineThickness, new ImmutableDashStyle([2, 3], 0), PenLineCap.Square);
double y = size.Height / 2.0;
ctx.DrawLine(dottedLinePen, new Point(0, y), new Point(size.Width, y));
// TODO: uncomment when we can actually modify key frames via this UI
// DrawDottedLine(ctx, theBrush, sequence.IsOverrideEnabled ? this.GetYHelper(sequence.DefaultKeyFrame, size.Height) : (size.Height / 2.0), size.Width);
return;
}

Expand All @@ -177,27 +187,32 @@ public override void Render(DrawingContext ctx) {

if (sequence.IsOverrideEnabled) {
double y = this.GetYHelper(sequence.DefaultKeyFrame, size.Height);
ctx.DrawLine(linePen, new Point(0, y), new Point(size.Width, y));
DrawDottedLine(ctx, theBrush, y, size.Width);
}

return;

Point Selector(KeyFrame x) => this.PointForKeyFrame(x, size, offset, zoom);
}

private static void DrawDottedLine(DrawingContext ctx, IBrush brush, double y, double width) {
Pen pen = new Pen(brush, LineThickness, new ImmutableDashStyle([2, 3], 0), PenLineCap.Square);
ctx.DrawLine(pen, new Point(0, y), new Point(width, y));
}

public Point PointForKeyFrame(KeyFrame keyFrame, Size size, Point offset, double zoom) {
double height = size.Height + offset.Y;
double px = TimelineUtils.FrameToPixel(keyFrame.Frame, zoom) + offset.X;
double offset_y = this.GetYHelper(keyFrame, height);
if (double.IsNaN(offset_y)) {
offset_y = 0;
double py = this.GetYHelper(keyFrame, height);
if (double.IsNaN(py)) {
py = 0;
}

return new Point(px, height - offset_y);
return new Point(px, py);
}

public double GetYHelper(KeyFrame keyFrame, double height) {
return this.IsValueRangeHuge ? (height / 2d) : GetY(keyFrame, height);
return this.IsValueRangeHuge ? (height / 2d) : (height - GetY(keyFrame, height));
}

[SwitchAutomationDataType]
Expand Down
8 changes: 4 additions & 4 deletions FramePFX.Avalonia/Editing/Playheads/BasePlayHeadControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ protected virtual void OnTimelineChanged(Timeline? oldTimeline, Timeline? newTim
Debug.Assert(this.lastTimeline == oldTimeline, "Different last timelines");
this.lastTimeline = newTimeline;
if (newTimeline != null) {
this.IsVisible = this.TimelineControl != null;
// this.IsVisible = this.TimelineControl != null;
this.UpdateZoom();
}
else {
this.IsVisible = false;
// this.IsVisible = false;
}
}

Expand All @@ -76,11 +76,11 @@ protected virtual void OnTimelineControlChanged(TimelineControl? oldTimeline, Ti
this.OnTimelineChanged(oldTimeline?.Timeline, newTimelineModel);
}

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

Expand Down
12 changes: 12 additions & 0 deletions FramePFX.Avalonia/Editing/Playheads/PlayHeadControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,16 @@ 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);
}
}
2 changes: 2 additions & 0 deletions FramePFX.Avalonia/Editing/Timelines/ClipStoragePanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ public void InsertClip(TimelineClipControl control, Clip clip, int index) {
control.ApplyTemplate();
control.ApplyTemplate();
control.OnConnected();

TimelineControl.UpdateClipAutomationVisible(control, this.TimelineControl!.IsTrackAutomationVisible, this.TimelineControl!.IsClipAutomationVisible);
}

public void RemoveClipInternal(int index, bool canCache = true) {
Expand Down
5 changes: 4 additions & 1 deletion FramePFX.Avalonia/Editing/Timelines/TimelineClipControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
Expand Down Expand Up @@ -825,4 +824,8 @@ protected async void OnDrop(DragEventArgs e) {
}

#endregion

public void OnIsAutomationVisibilityChanged(bool isVisible) {
this.PART_AutomationEditor!.IsVisible = isVisible;
}
}
92 changes: 75 additions & 17 deletions FramePFX.Avalonia/Editing/Timelines/TimelineControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using FramePFX.Avalonia.Editing.Playheads;
using FramePFX.Avalonia.Editing.Timelines.Selection;
Expand All @@ -48,6 +50,8 @@ namespace FramePFX.Avalonia.Editing.Timelines;
public class TimelineControl : TemplatedControl, ITimelineElement {
public static readonly StyledProperty<Timeline?> TimelineProperty = AvaloniaProperty.Register<TimelineControl, Timeline?>(nameof(Timeline));
public static readonly DirectProperty<TimelineControl, double> ZoomProperty = AvaloniaProperty.RegisterDirect<TimelineControl, double>(nameof(Zoom), o => o.Zoom, unsetValue: 1.0);
public static readonly StyledProperty<bool> IsTrackAutomationVisibleProperty = AvaloniaProperty.Register<TimelineControl, bool>(nameof(IsTrackAutomationVisible), true);
public static readonly StyledProperty<bool> IsClipAutomationVisibleProperty = AvaloniaProperty.Register<TimelineControl, bool>(nameof(IsClipAutomationVisible), true);

private readonly ModelControlDictionary<Track, ITrackElement> trackElementMap;

Expand All @@ -58,10 +62,18 @@ public Timeline? Timeline {
set => this.SetValue(TimelineProperty, value);
}

public double Zoom {
get => this.myZoomFactor;
public bool IsTrackAutomationVisible {
get => this.GetValue(IsTrackAutomationVisibleProperty);
set => this.SetValue(IsTrackAutomationVisibleProperty, value);
}

public bool IsClipAutomationVisible {
get => this.GetValue(IsClipAutomationVisibleProperty);
set => this.SetValue(IsClipAutomationVisibleProperty, value);
}

public double Zoom => this.myZoomFactor;

/// <summary>
/// Gets the list box which stores the control surfaces for the tracks
/// </summary>
Expand All @@ -86,6 +98,8 @@ public double Zoom {

public StopHeadControl? StopHead { get; private set; }

public TimelineRuler? TimelineRuler { get; private set; }

public PlayheadPositionTextControl PlayHeadInfoTextControl { get; private set; }

/// <summary>
Expand Down Expand Up @@ -146,10 +160,10 @@ public TrackElementImpl(TimelineControl timeline, TrackControlSurfaceItem surfac

this.SurfaceControl.contextData.Set(DataKeys.TrackKey, this.Track).Set(DataKeys.TrackUIKey, this.SurfaceControl.TrackElement = this);
this.TrackControl.contextData.Set(DataKeys.TrackKey, this.Track).Set(DataKeys.TrackUIKey, this.TrackControl.TrackElement = this);

// We can invalidate the surface control since it's in a different branch of the visual tree
DataManager.InvalidateInheritedContext(this.SurfaceControl);

// Don't invalidate if the TimelineControl is adding the clips for the TimelineChanged event,
// because there's no point since the timeline invalidates its own context after all tracks
// are loaded which
Expand Down Expand Up @@ -178,7 +192,7 @@ public void UpdateSelected(bool state) {

public event UITimelineModelChanged? TimelineModelChanging;
public event UITimelineModelChanged? TimelineModelChanged;

public TimelineControl() {
this.Tracks = new TrackListImpl(this);
this.myTrackElements = new List<TrackElementImpl>();
Expand All @@ -188,6 +202,8 @@ public TimelineControl() {

static TimelineControl() {
TimelineProperty.Changed.AddClassHandler<TimelineControl, Timeline?>((d, e) => d.OnTimelineChanged(e.OldValue.GetValueOrDefault(), e.NewValue.GetValueOrDefault()));
IsTrackAutomationVisibleProperty.Changed.AddClassHandler<TimelineControl, bool>((d, e) => d.OnIsTrackAutomationVisibilityChanged(e.OldValue.GetValueOrDefault(), e.NewValue.GetValueOrDefault()));
IsClipAutomationVisibleProperty.Changed.AddClassHandler<TimelineControl, bool>((d, e) => d.OnIsClipAutomationVisibilityChanged(e.OldValue.GetValueOrDefault(), e.NewValue.GetValueOrDefault()));
}

ITrackElement ITimelineElement.GetTrackFromModel(Track track) => this.trackElementMap.GetControl(track);
Expand All @@ -203,8 +219,15 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) {
this.TimestampBorder = e.NameScope.GetTemplateChild<Border>("PART_TimestampBoard");
this.PlayHead = e.NameScope.GetTemplateChild<PlayHeadControl>("PART_PlayHeadControl");
this.StopHead = e.NameScope.GetTemplateChild<StopHeadControl>("PART_StopHeadControl");
this.TimelineRuler = e.NameScope.GetTemplateChild<TimelineRuler>("PART_Ruler");
this.PlayHeadInfoTextControl = e.NameScope.GetTemplateChild<PlayheadPositionTextControl>("PART_PlayheadPositionPreviewControl");

ToggleButton toggleTrackAutomationButton = e.NameScope.GetTemplateChild<ToggleButton>("PART_ToggleTrackAutomation");
toggleTrackAutomationButton.Bind(ToggleButton.IsCheckedProperty, new Binding(nameof(this.IsTrackAutomationVisible), BindingMode.TwoWay) { Source = this });

ToggleButton toggleClipAutomationButton = e.NameScope.GetTemplateChild<ToggleButton>("PART_ToggleClipAutomation");
toggleClipAutomationButton.Bind(ToggleButton.IsCheckedProperty, new Binding(nameof(this.IsClipAutomationVisible), BindingMode.TwoWay) { Source = this });

this.TrackSelectionManager = new TrackSelectionManager(this, this.myTrackElements);
((ILightSelectionManager<ITrackElement>) this.TrackSelectionManager).SelectionChanged += this.OnTrackChanged;

Expand All @@ -216,6 +239,43 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) {
this.TimelineContentGrid.PointerPressed += this.OnTimelineContentGridPointerPressed;

this.TimestampBorder.PointerPressed += (s, ex) => this.MovePlayHeadToMouseCursor(ex.GetPosition((Visual?) s).X, true, false, ex);

this.UpdateIsTrackAutomationVisible(true, true);

// Has to be a 'preview' handler in WPF speak, since we need to prevent the base scroll viewer scrolling down even if CTRL is held
this.TimelineScrollViewer.AddHandler(PointerWheelChangedEvent, this.TimelineScrollViewerOnPointerWheelChanged, RoutingStrategies.Tunnel);
}

private void OnIsTrackAutomationVisibilityChanged(bool oldValue, bool newValue) => this.UpdateIsTrackAutomationVisible(newValue, null);

private void OnIsClipAutomationVisibilityChanged(bool oldValue, bool newValue) => this.UpdateIsTrackAutomationVisible(null, newValue);

private void UpdateIsTrackAutomationVisible(bool? trackVisible, bool? clipVisible) {
if (!clipVisible.HasValue && !trackVisible.HasValue) {
return;
}

foreach (TimelineTrackControl track in this.TrackStorage!.GetTracks()) {
UpdateTrackAutomationVisible(track, trackVisible, clipVisible, true);
}
}

public static void UpdateTrackAutomationVisible(TimelineTrackControl track, bool? trackVisible, bool? clipVisible, bool clipsToo) {
if (trackVisible.HasValue)
track.OnIsAutomationVisibilityChanged(trackVisible.Value);

if (clipsToo) {
foreach (TimelineClipControl clip in track.ClipStoragePanel!.GetClips()) {
UpdateClipAutomationVisible(clip, trackVisible, clipVisible);
}
}
}

public static void UpdateClipAutomationVisible(TimelineClipControl clip, bool? trackVisible, bool? clipVisible) {
if (clipVisible.HasValue)
clip.OnIsAutomationVisibilityChanged(clipVisible.Value);
if (trackVisible.HasValue)
clip.Opacity = trackVisible.Value ? 0.8 : 1.0;
}

private void OnSelectionChanged(ILightSelectionManager<IClipElement> sender) {
Expand Down Expand Up @@ -266,7 +326,7 @@ private void OnTimelineContentGridPointerPressed(object? sender, PointerPressedE
if (e.GetCurrentPoint(this).Properties.PointerUpdateKind != PointerUpdateKind.LeftButtonPressed) {
return;
}

if ((e.Source == sender || e.Source is ClipStoragePanel) && this.Timeline is Timeline timeline) {
timeline.PlayHeadPosition = timeline.StopHeadPosition = TimelineClipControl.GetCursorFrame(this.TrackStorage!, e);
this.ClipSelectionManager?.Clear();
Expand All @@ -278,7 +338,7 @@ private void OnTimelineChanged(Timeline? oldTimeline, Timeline? newTimeline) {
// Should never reach this, but just for clarity, might as well check it
return;
}

this.TimelineModelChanging?.Invoke(this, oldTimeline, newTimeline);
if (oldTimeline != null) {
oldTimeline.MaxDurationChanged -= this.OnTimelineMaxDurationChanged;
Expand All @@ -297,6 +357,7 @@ private void OnTimelineChanged(Timeline? oldTimeline, Timeline? newTimeline) {
this.SurfaceTrackList!.TimelineControl = this;
this.SurfaceTrackList.Timeline = newTimeline;
this.PlayHeadInfoTextControl.Timeline = newTimeline;
this.TimelineRuler!.TimelineControl = this;
if (newTimeline != null) {
newTimeline.MaxDurationChanged += this.OnTimelineMaxDurationChanged;
newTimeline.TrackAdded += this.OnTrackAddedEvent;
Expand All @@ -309,10 +370,10 @@ private void OnTimelineChanged(Timeline? oldTimeline, Timeline? newTimeline) {

this.contextData.Set(DataKeys.TimelineKey, newTimeline);
}

this.TrackSelectionManager!.UpdateSelection();
DataManager.InvalidateInheritedContext(this);

this.TimelineModelChanged?.Invoke(this, oldTimeline, newTimeline);
}

Expand All @@ -321,6 +382,7 @@ private void InsertTrackElement(Track track, int i, bool isLoadingTimeline = fal
TrackControlSurfaceItem surfaceItem = this.SurfaceTrackList!.GetTrack(i);
TimelineTrackControl trackControl = this.TrackStorage!.GetTrack(i);
this.myTrackElements.Insert(i, new TrackElementImpl(this, surfaceItem, trackControl, track, isLoadingTimeline));
UpdateTrackAutomationVisible(trackControl, this.IsTrackAutomationVisible, this.IsClipAutomationVisible, true);
}

private void RemoveTrackElement(int i) {
Expand Down Expand Up @@ -377,14 +439,9 @@ private void UpdateContentGridSize() {
this.TimelineContentGrid.ClearValue(Layoutable.WidthProperty);
}
}

protected override void OnPointerWheelChanged(PointerWheelEventArgs e) {
base.OnPointerWheelChanged(e);
ScrollViewer? scroller = this.TimelineScrollViewer;
if (scroller == null) {
return;
}


private void TimelineScrollViewerOnPointerWheelChanged(object? sender, PointerWheelEventArgs e) {
ScrollViewer scroller = (ScrollViewer) sender!;
KeyModifiers mods = e.KeyModifiers;
if ((mods & KeyModifiers.Alt) != 0) {
if (VisualTreeUtils.TryGetParent(e.Source as AvaloniaObject, out TimelineTrackControl? track)) {
Expand Down Expand Up @@ -418,6 +475,7 @@ protected override void OnPointerWheelChanged(PointerWheelEventArgs e) {
double scaled_target_offset = target_offset * newZoom;
double new_offset = scaled_target_offset - mouse_x;
scroller.Offset = new Vector(new_offset, scroller.Offset.Y);
e.Handled = true;
}
else if ((mods & KeyModifiers.Shift) != 0) {
if (e.Delta.Y < 0 || e.Delta.X < 0) {
Expand Down
Loading

0 comments on commit ba4a1f5

Please sign in to comment.