diff --git a/FramePFX.Avalonia/App.axaml.cs b/FramePFX.Avalonia/App.axaml.cs index fba48bda..2b36785c 100644 --- a/FramePFX.Avalonia/App.axaml.cs +++ b/FramePFX.Avalonia/App.axaml.cs @@ -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 diff --git a/FramePFX.Avalonia/AvControls/FreeMoveViewPortV2.cs b/FramePFX.Avalonia/AvControls/FreeMoveViewPortV2.cs index 584aa661..b04268fd 100644 --- a/FramePFX.Avalonia/AvControls/FreeMoveViewPortV2.cs +++ b/FramePFX.Avalonia/AvControls/FreeMoveViewPortV2.cs @@ -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; diff --git a/FramePFX.Avalonia/Editing/Playheads/BasePlayHeadControl.cs b/FramePFX.Avalonia/Editing/Playheads/BasePlayHeadControl.cs index 9a7a66ed..845220d4 100644 --- a/FramePFX.Avalonia/Editing/Playheads/BasePlayHeadControl.cs +++ b/FramePFX.Avalonia/Editing/Playheads/BasePlayHeadControl.cs @@ -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; @@ -31,29 +33,75 @@ public abstract class BasePlayHeadControl : TemplatedControl { private IDisposable? zoomChangeHandler; private IDisposable? timelineChangeHandler; public static readonly StyledProperty TimelineControlProperty = AvaloniaProperty.Register(nameof(TimelineControl)); + public static readonly StyledProperty PlayHeadTypeProperty = AvaloniaProperty.Register(nameof(PlayHeadType), PlayHeadType.PlayHead); + public static readonly StyledProperty ScrollViewerReferenceProperty = AvaloniaProperty.Register(nameof(ScrollViewerReference)); + public static readonly StyledProperty AdditionalOffsetProperty = AvaloniaProperty.Register(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((d, e) => d.OnTimelineControlChanged(e.OldValue.GetValueOrDefault(), e.NewValue.GetValueOrDefault())); + PlayHeadTypeProperty.Changed.AddClassHandler((d, e) => d.OnPlayHeadTypeChanged(e.OldValue.GetValueOrDefault(), e.NewValue.GetValueOrDefault())); + ScrollViewerReferenceProperty.Changed.AddClassHandler((d, e) => d.OnScrollViewerReferenceChanged(e.OldValue.GetValueOrDefault(), e.NewValue.GetValueOrDefault())); + AdditionalOffsetProperty.Changed.AddClassHandler((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; @@ -69,39 +117,104 @@ protected virtual void OnTimelineControlChanged(TimelineControl? oldTimeline, Ti if (newTimeline != null) { this.timelineChangeHandler = TimelineControl.TimelineProperty.Changed.Subscribe(new AnonymousObserver>((e) => this.OnTimelineChanged(e.OldValue.GetValueOrDefault(), e.NewValue.GetValueOrDefault()))); this.zoomChangeHandler = TimelineControl.ZoomProperty.Changed.Subscribe(new AnonymousObserver>(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 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 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) { diff --git a/FramePFX.Avalonia/Editing/Playheads/StopHeadControl.cs b/FramePFX.Avalonia/Editing/Playheads/FlatLinePlayHeadControl.cs similarity index 76% rename from FramePFX.Avalonia/Editing/Playheads/StopHeadControl.cs rename to FramePFX.Avalonia/Editing/Playheads/FlatLinePlayHeadControl.cs index 4212c0b5..d20132bb 100644 --- a/FramePFX.Avalonia/Editing/Playheads/StopHeadControl.cs +++ b/FramePFX.Avalonia/Editing/Playheads/FlatLinePlayHeadControl.cs @@ -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; @@ -34,7 +34,7 @@ public class StopHeadControl : BasePlayHeadControl { private Point clickPoint; private int dragState; - public StopHeadControl() { + public FlatLinePlayHeadControl() { this.Focusable = true; } @@ -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) { @@ -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); } } } @@ -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); - } } \ No newline at end of file diff --git a/FramePFX.Avalonia/Editing/Playheads/PlayHeadControl.cs b/FramePFX.Avalonia/Editing/Playheads/GrippedPlayHeadControl.cs similarity index 69% rename from FramePFX.Avalonia/Editing/Playheads/PlayHeadControl.cs rename to FramePFX.Avalonia/Editing/Playheads/GrippedPlayHeadControl.cs index a44c03c2..1c7085b5 100644 --- a/FramePFX.Avalonia/Editing/Playheads/PlayHeadControl.cs +++ b/FramePFX.Avalonia/Editing/Playheads/GrippedPlayHeadControl.cs @@ -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) { @@ -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; } } } @@ -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); - } } \ No newline at end of file diff --git a/FramePFX.Avalonia/Editing/Playheads/PlayHeadStyles.axaml b/FramePFX.Avalonia/Editing/Playheads/PlayHeadStyles.axaml index 1723afa2..53e3da19 100644 --- a/FramePFX.Avalonia/Editing/Playheads/PlayHeadStyles.axaml +++ b/FramePFX.Avalonia/Editing/Playheads/PlayHeadStyles.axaml @@ -32,13 +32,13 @@ - + - + - + - + @@ -86,6 +86,6 @@ - + \ No newline at end of file diff --git a/FramePFX.Avalonia/Editing/Playheads/PlayHeadType.cs b/FramePFX.Avalonia/Editing/Playheads/PlayHeadType.cs new file mode 100644 index 00000000..a12cad87 --- /dev/null +++ b/FramePFX.Avalonia/Editing/Playheads/PlayHeadType.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) 2023-2024 REghZy +// +// This file is part of FramePFX. +// +// FramePFX is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either +// version 3.0 of the License, or (at your option) any later version. +// +// FramePFX is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with FramePFX. If not, see . +// + +namespace FramePFX.Avalonia.Editing.Playheads; + +public enum PlayHeadType { + None, + PlayHead, + StopHead +} \ No newline at end of file diff --git a/FramePFX.Avalonia/Editing/Timelines/TimelineControl.cs b/FramePFX.Avalonia/Editing/Timelines/TimelineControl.cs index 3081dc68..14b133e3 100644 --- a/FramePFX.Avalonia/Editing/Timelines/TimelineControl.cs +++ b/FramePFX.Avalonia/Editing/Timelines/TimelineControl.cs @@ -94,9 +94,11 @@ public bool IsClipAutomationVisible { public Border? TimestampBorder { get; private set; } - public PlayHeadControl? PlayHead { get; private set; } + public FlatLinePlayHeadControl? PlayHeadInSequence { get; private set; } - public StopHeadControl? StopHead { get; private set; } + public FlatLinePlayHeadControl? StopHeadInSequence { get; private set; } + + public GrippedPlayHeadControl? PlayHeadInRuler { get; private set; } public TimelineRuler? TimelineRuler { get; private set; } @@ -217,8 +219,9 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { this.TrackListScrollViewer = e.NameScope.GetTemplateChild("PART_TrackListScrollViewer"); this.TimelineBorder = e.NameScope.GetTemplateChild("PART_TimelineSequenceBorder"); this.TimestampBorder = e.NameScope.GetTemplateChild("PART_TimestampBoard"); - this.PlayHead = e.NameScope.GetTemplateChild("PART_PlayHeadControl"); - this.StopHead = e.NameScope.GetTemplateChild("PART_StopHeadControl"); + this.PlayHeadInSequence = e.NameScope.GetTemplateChild("PART_PlayHeadControl"); + this.StopHeadInSequence = e.NameScope.GetTemplateChild("PART_StopHeadControl"); + this.PlayHeadInRuler = e.NameScope.GetTemplateChild("PART_RulerPlayHead"); this.TimelineRuler = e.NameScope.GetTemplateChild("PART_Ruler"); this.PlayHeadInfoTextControl = e.NameScope.GetTemplateChild("PART_PlayheadPositionPreviewControl"); @@ -234,8 +237,9 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { this.ClipSelectionManager = new TimelineClipSelectionManager(this); ((ILightSelectionManager) this.ClipSelectionManager).SelectionChanged += this.OnSelectionChanged; - this.PlayHead!.TimelineControl = this; - this.StopHead!.TimelineControl = this; + this.PlayHeadInSequence!.TimelineControl = this; + this.StopHeadInSequence!.TimelineControl = this; + this.PlayHeadInRuler!.TimelineControl = this; this.TimelineContentGrid.PointerPressed += this.OnTimelineContentGridPointerPressed; this.TimestampBorder.PointerPressed += (s, ex) => this.MovePlayHeadToMouseCursor(ex.GetPosition((Visual?) s).X, true, false, ex); @@ -244,6 +248,7 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { // 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); + this.TimestampBorder.AddHandler(PointerWheelChangedEvent, this.TimeStampBoardScrollViewerOnPointerWheelChanged, RoutingStrategies.Tunnel); } private void OnIsTrackAutomationVisibilityChanged(bool oldValue, bool newValue) => this.UpdateIsTrackAutomationVisible(newValue, null); @@ -305,7 +310,8 @@ private void MovePlayHeadToMouseCursor(double x, bool enableThumbDragging = true } if (enableThumbDragging && ex != null) { - this.PlayHead!.EnableDragging(ex); + this.PlayHeadInRuler!.EnableDragging(ex); + // this.PlayHeadInSequence!.EnableDragging(ex); } } } @@ -441,7 +447,13 @@ private void UpdateContentGridSize() { } private void TimelineScrollViewerOnPointerWheelChanged(object? sender, PointerWheelEventArgs e) { - ScrollViewer scroller = (ScrollViewer) sender!; + this.OnMouseWheel((ScrollViewer) sender!, e); + } + private void TimeStampBoardScrollViewerOnPointerWheelChanged(object? sender, PointerWheelEventArgs e) { + this.OnMouseWheel(this.TimelineScrollViewer!, e); + } + + private void OnMouseWheel(ScrollViewer scroller, PointerWheelEventArgs e) { KeyModifiers mods = e.KeyModifiers; if ((mods & KeyModifiers.Alt) != 0) { if (VisualTreeUtils.TryGetParent(e.Source as AvaloniaObject, out TimelineTrackControl? track)) { @@ -495,6 +507,7 @@ private void TimelineScrollViewerOnPointerWheelChanged(object? sender, PointerWh private void OnTimelineZoomed(double oldZoom, double newZoom, ZoomType zoomType, SKPointD mPos) { this.TrackStorage?.OnZoomChanged(newZoom); + this.TimelineRuler.OnZoomChanged(newZoom); this.UpdateContentGridSize(); ScrollViewer? scroller = this.TimelineScrollViewer; diff --git a/FramePFX.Avalonia/Editing/Timelines/TimelineRuler.cs b/FramePFX.Avalonia/Editing/Timelines/TimelineRuler.cs index c80406c8..ef10c8fb 100644 --- a/FramePFX.Avalonia/Editing/Timelines/TimelineRuler.cs +++ b/FramePFX.Avalonia/Editing/Timelines/TimelineRuler.cs @@ -18,6 +18,7 @@ // using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using Avalonia; @@ -35,7 +36,6 @@ namespace FramePFX.Avalonia.Editing.Timelines; public class TimelineRuler : Control { - private static readonly int[] Steps = new[] { 1, 2, 5, 10, 50, 100 }; private const double MinRender = 0.01D; private const double MajorLineThickness = 1.0; private const double MinorStepRatio = 0.5; @@ -76,7 +76,7 @@ public IBrush? StepColour { get => this.GetValue(StepColourProperty); set => this.SetValue(StepColourProperty, value); } - + public ScrollViewer? ScrollViewerReference { get => this.GetValue(ScrollViewerReferenceProperty); set => this.SetValue(ScrollViewerReferenceProperty, value); @@ -89,6 +89,7 @@ public ScrollViewer? ScrollViewerReference { private Pen? majorLineStepColourPen; private Pen? minorLineStepColourPen; private Timeline? targetTimelineModel; + private double timelineZoom; public TimelineRuler() { this.ClipToBounds = true; @@ -101,7 +102,7 @@ static TimelineRuler() { AffectsRender(StepColourProperty); ScrollViewerReferenceProperty.Changed.AddClassHandler((d, e) => d.OnScrollViewerReferenceChanged(e.OldValue.GetValueOrDefault(), e.NewValue.GetValueOrDefault())); } - + private void OnScrollViewerReferenceChanged(ScrollViewer? oldValue, ScrollViewer? newValue) { if (oldValue != null) { oldValue.SizeChanged -= this.OnScrollerOnSizeChanged; @@ -122,15 +123,18 @@ private void OnEffectiveViewportChanged(object? sender, EffectiveViewportChanged private void OnTimelineChanged(TimelineControl? oldTimeline, TimelineControl? newTimeline) { this.InvalidateVisual(); - - if (oldTimeline != null) + + if (oldTimeline != null) { oldTimeline.TimelineModelChanged -= this.OnTimelineModelChanged; + } if (newTimeline != null) { newTimeline.TimelineModelChanged += this.OnTimelineModelChanged; if (newTimeline.Timeline is Timeline timeline) { this.OnTimelineModelChanged(newTimeline, null, timeline); } + + this.timelineZoom = newTimeline.Zoom; } } @@ -138,7 +142,7 @@ protected override void OnLoaded(RoutedEventArgs e) { base.OnLoaded(e); Dispatcher.UIThread.InvokeAsync(this.InvalidateVisual, DispatcherPriority.Background); } - + private void OnTimelineModelChanged(ITimelineElement element, Timeline? oldtimeline, Timeline? newtimeline) { this.targetTimelineModel = newtimeline; this.InvalidateVisual(); @@ -157,7 +161,7 @@ public override void Render(DrawingContext dc) { if (this.targetTimelineModel == null || !(this.TimelineControl is TimelineControl timelineControl) || !(this.ScrollViewerReference is ScrollViewer scrollViewer)) { return; } - + Rect myBounds = this.Bounds; if (myBounds.Width < MinRender || myBounds.Height < MinRender) { return; @@ -166,96 +170,60 @@ public override void Render(DrawingContext dc) { if (this.Background is Brush bg) { dc.DrawRectangle(bg, null, myBounds); } + + int[] Steps = [1, 2, 5, 10]; + + // ##################################################################################################################### double rulerWidth = myBounds.Width; - double zoom = timelineControl.Zoom; + double zoom = this.timelineZoom; double scrollH = scrollViewer.Offset.X; - long offset2 = (long) (scrollH / zoom); - double offset3 = (scrollH - offset2 * zoom); double timelineWidth = zoom * this.targetTimelineModel.MaxDuration; - // const int SubStepNumber = 10; - // const int MinPixelSize = 3; - // double minPixel = MinPixelSize * SubStepNumber / timelineWidth; - // double minStep = minPixel * (this.targetTimelineModel.MaxDuration / zoom); - // double minStepMagPow = Math.Pow(10, Math.Floor(Math.Log10(minStep))); - // double normMinStep = minStep / minStepMagPow; - // int finalStep = Steps.FirstOrDefault(step => step > normMinStep); - // if (finalStep < 1) { - // return; - // } - - double h = Maths.Map(1.0 / zoom, 0.0, 1.0, 0, 100); - int finalStep = Steps.FirstOrDefault(step => step > h); - if (finalStep < 1 || finalStep > 100) - finalStep = 100; - - double firstMajor = scrollH % zoom == 0D ? scrollH : scrollH + (zoom - scrollH % zoom); - double firstMajorRelative = zoom - (scrollH - firstMajor + zoom); - int majorStepCount = (int) TimelineUtils.PixelToFrame(scrollH + firstMajorRelative, zoom, true); - double start = zoom - offset3; + // Not using anymore but this is some witchcraft math + // double start = zoom - (scrollH - (long) (scrollH / zoom) * zoom); + // double firstMajor = scrollH % zoom == 0D ? scrollH : scrollH + (zoom - scrollH % zoom); + // double firstMajorRelative = zoom - (scrollH - firstMajor + zoom); - double subZoom = zoom / finalStep; - - for (double x = firstMajorRelative; x < rulerWidth; x += zoom) { - long theFrame = TimelineUtils.PixelToFrame(scrollH + x, zoom, true); - bool isMajor = theFrame % finalStep < 0.0001d; - if (isMajor) { - double size = Math.Min(myBounds.Height / 2.0, myBounds.Height); - dc.DrawLine(this.MajorStepColourPen, new Point(x, myBounds.Height - size), new Point(x, myBounds.Height)); - this.DrawText(dc, theFrame, x); - } - else { - double size = (myBounds.Height / 2.0) * (1 - MinorStepRatio); - dc.DrawLine(this.MinorStepColourPen, new Point(x, myBounds.Height - size), new Point(x, myBounds.Height)); - } + const int SubStepNumber = 10; + const int MinPixelSize = 4; + double minPixel = MinPixelSize * SubStepNumber / timelineWidth; + double minStep = minPixel * this.targetTimelineModel.MaxDuration; + double minStepMagPow = Math.Pow(10, Math.Floor(Math.Log10(minStep))); + double normMinStep = minStep / minStepMagPow; + int finalStep = Steps.FirstOrDefault(step => step > normMinStep); + if (finalStep < 1) { + return; } + + double valueStep = finalStep * minStepMagPow; + double pixelSize = timelineWidth * valueStep / this.targetTimelineModel.MaxDuration; - // double timelineWidth = scrollViewer.Extent.Width; - // const int SubStepNumber = 10; - // const int MinPixelSize = 4; - // double minPixel = MinPixelSize * SubStepNumber / timelineWidth; - // double minStep = minPixel * this.targetTimelineModel.MaxDuration; - // double minStepMagPow = Math.Pow(10, Math.Floor(Math.Log10(minStep))); - // double normMinStep = minStep / minStepMagPow; - // int finalStep = Steps.FirstOrDefault(step => step > normMinStep); - // if (finalStep < 1) { - // return; - // } - - // double valueStep = finalStep * minStepMagPow; - // double pixelSize = timelineWidth * valueStep / this.targetTimelineModel.MaxDuration; - - // int steps = Math.Min((int) Math.Floor(valueStep), SubStepNumber); - // double subpixelSize = pixelSize / steps; - - // // calculate an initial offset instead of looping until we get into a visible region - // // Flooring may result in us drawing things partially offscreen to the left, which is kinda required - // double pxLeft = myBounds.Left;// + (this.ScrollViewerReference?.Offset.X ?? 0); - // double height = myBounds.Height; + int steps = Math.Min((int) Math.Floor(valueStep), SubStepNumber); + double subpixelSize = pixelSize / steps; - // int i = (int) Math.Floor(pxLeft / pixelSize); - // int j = (int) Math.Ceiling((myBounds.Right + pixelSize) / pixelSize); - // do { - // double pixel = i * pixelSize; - // if (i > j) { - // break; - // } - - // // TODO: optimise smaller/minor lines, maybe using skia? - // for (int y = 1; y < steps; ++y) { - // double subpixel = pixel + y * subpixelSize; - // this.DrawMinorLine(dc, subpixel, height); - // } - - // double text_value = i * valueStep; - // if (Math.Abs(text_value - (int) text_value) < 0.00001d) { - // this.DrawMajorLine(dc, pixel, height); - // this.DrawText(dc, text_value, pixel); - // } - - // i++; - // } while (true); + int i = (int) Math.Floor(scrollH / pixelSize); + int j = (int) Math.Ceiling((scrollH + rulerWidth + pixelSize) / pixelSize); + do { + double pixel = i * pixelSize - scrollH; + if (i > j) { + break; + } + + // TODO: optimise smaller/minor lines, maybe using skia? + for (int y = 1; y < steps; ++y) { + double subpixel = pixel + y * subpixelSize; + this.DrawMinorLine(dc, subpixel, this.Bounds.Height); + } + + double text_value = i * valueStep; + if (Math.Abs(text_value - (int) text_value) < 0.00001d) { + this.DrawMajorLine(dc, pixel, this.Bounds.Height); + this.DrawText(dc, text_value, pixel); + } + + i++; + } while (true); } @@ -276,13 +244,13 @@ public void DrawText(DrawingContext dc, double value, double offset) { Point point; FormattedText format = this.GetFormattedText(value); - double gap = (height - majorSize); - if (gap >= (format.Height / 2d)) { - point = new Point((offset + MajorLineThickness) - (format.Width / 2d), gap - format.Height); + double gap = height - majorSize; + if (gap >= format.Height / 2d) { + point = new Point(offset + MajorLineThickness - format.Width / 2d, gap - format.Height); } else { // Draw above major if possible - point = new Point(offset + MajorLineThickness + 2d, (height / 2d) - (format.Height / 2d)); + point = new Point(offset + MajorLineThickness + 2d, height / 2d - format.Height / 2d); } dc.DrawText(format, point); @@ -297,4 +265,9 @@ protected FormattedText GetFormattedText(double value) { 12, this.Foreground); } + + public void OnZoomChanged(double newZoom) { + this.timelineZoom = newZoom; + this.InvalidateVisual(); + } } \ No newline at end of file diff --git a/FramePFX.Avalonia/Editing/Timelines/TimelineStyles.axaml b/FramePFX.Avalonia/Editing/Timelines/TimelineStyles.axaml index 8049bad7..2da136df 100644 --- a/FramePFX.Avalonia/Editing/Timelines/TimelineStyles.axaml +++ b/FramePFX.Avalonia/Editing/Timelines/TimelineStyles.axaml @@ -172,26 +172,21 @@ - - - - - - + x:Name="PART_RootGrid" RowDefinitions="38,1,*,Auto"> - + + + - + + + + - - - - - + + + + - - - + + + + +