mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-21 16:12:01 +00:00
Preview slider now shows segments bounds. Forward/backward burrons jumps segments. Custom class for preview slider.
This commit is contained in:
parent
9760fbc2e6
commit
ddafb40ca7
299
Splitter-UI/Controls/PreviewSlider.cs
Normal file
299
Splitter-UI/Controls/PreviewSlider.cs
Normal file
@ -0,0 +1,299 @@
|
||||
using System;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Point = Avalonia.Point;
|
||||
|
||||
namespace Splitter_UI.Controls
|
||||
{
|
||||
public sealed class PreviewSlider : Control
|
||||
{
|
||||
public static readonly StyledProperty<double> MinimumProperty =
|
||||
AvaloniaProperty.Register<PreviewSlider, double>(nameof(Minimum), 0d);
|
||||
|
||||
public static readonly StyledProperty<double> MaximumProperty =
|
||||
AvaloniaProperty.Register<PreviewSlider, double>(nameof(Maximum), 100d);
|
||||
|
||||
public static readonly StyledProperty<double> ValueProperty =
|
||||
AvaloniaProperty.Register<PreviewSlider, double>(
|
||||
nameof(Value), 0d,
|
||||
coerce: (o, v) =>
|
||||
{
|
||||
var slider = (PreviewSlider)o;
|
||||
if (v < slider.Minimum) return slider.Minimum;
|
||||
if (v > slider.Maximum) return slider.Maximum;
|
||||
return v;
|
||||
});
|
||||
|
||||
public static readonly StyledProperty<double> SegmentDurationProperty =
|
||||
AvaloniaProperty.Register<PreviewSlider, double>(nameof(SegmentDuration), 1d);
|
||||
|
||||
public static readonly StyledProperty<double> TrackThicknessProperty =
|
||||
AvaloniaProperty.Register<PreviewSlider, double>(nameof(TrackThickness), 4d);
|
||||
|
||||
public static readonly StyledProperty<double> ThumbRadiusProperty =
|
||||
AvaloniaProperty.Register<PreviewSlider, double>(nameof(ThumbRadius), 8d);
|
||||
|
||||
public static readonly StyledProperty<IBrush> TrackBrushProperty =
|
||||
AvaloniaProperty.Register<PreviewSlider, IBrush>(nameof(TrackBrush), Brushes.Gray);
|
||||
|
||||
public static readonly StyledProperty<IBrush> TrackFillBrushProperty =
|
||||
AvaloniaProperty.Register<PreviewSlider, IBrush>(nameof(TrackFillBrush), Brushes.DodgerBlue);
|
||||
|
||||
public static readonly StyledProperty<IBrush> ThumbBrushProperty =
|
||||
AvaloniaProperty.Register<PreviewSlider, IBrush>(nameof(ThumbBrush), Brushes.White);
|
||||
|
||||
public static readonly StyledProperty<IBrush> ThumbBorderBrushProperty =
|
||||
AvaloniaProperty.Register<PreviewSlider, IBrush>(nameof(ThumbBorderBrush), Brushes.DodgerBlue);
|
||||
|
||||
public static readonly StyledProperty<double> ThumbBorderThicknessProperty =
|
||||
AvaloniaProperty.Register<PreviewSlider, double>(nameof(ThumbBorderThickness), 1d);
|
||||
|
||||
public static readonly StyledProperty<IBrush> SegmentLineBrushProperty =
|
||||
AvaloniaProperty.Register<PreviewSlider, IBrush>(nameof(SegmentLineBrush), Brushes.LightSalmon);
|
||||
|
||||
public static readonly StyledProperty<double> SegmentLineThicknessProperty =
|
||||
AvaloniaProperty.Register<PreviewSlider, double>(nameof(SegmentLineThickness), 1d);
|
||||
|
||||
private bool _isDragging;
|
||||
|
||||
public double Minimum
|
||||
{
|
||||
get => GetValue(MinimumProperty);
|
||||
set => SetValue(MinimumProperty, value);
|
||||
}
|
||||
|
||||
public double Maximum
|
||||
{
|
||||
get => GetValue(MaximumProperty);
|
||||
set => SetValue(MaximumProperty, value);
|
||||
}
|
||||
|
||||
public double Value
|
||||
{
|
||||
get => GetValue(ValueProperty);
|
||||
set => SetValue(ValueProperty, value);
|
||||
}
|
||||
|
||||
public double SegmentDuration
|
||||
{
|
||||
get => GetValue(SegmentDurationProperty);
|
||||
set => SetValue(SegmentDurationProperty, value);
|
||||
}
|
||||
|
||||
public double TrackThickness
|
||||
{
|
||||
get => GetValue(TrackThicknessProperty);
|
||||
set => SetValue(TrackThicknessProperty, value);
|
||||
}
|
||||
|
||||
public double ThumbRadius
|
||||
{
|
||||
get => GetValue(ThumbRadiusProperty);
|
||||
set => SetValue(ThumbRadiusProperty, value);
|
||||
}
|
||||
|
||||
public IBrush TrackBrush
|
||||
{
|
||||
get => GetValue(TrackBrushProperty);
|
||||
set => SetValue(TrackBrushProperty, value);
|
||||
}
|
||||
|
||||
public IBrush TrackFillBrush
|
||||
{
|
||||
get => GetValue(TrackFillBrushProperty);
|
||||
set => SetValue(TrackFillBrushProperty, value);
|
||||
}
|
||||
|
||||
public IBrush ThumbBrush
|
||||
{
|
||||
get => GetValue(ThumbBrushProperty);
|
||||
set => SetValue(ThumbBrushProperty, value);
|
||||
}
|
||||
|
||||
public IBrush ThumbBorderBrush
|
||||
{
|
||||
get => GetValue(ThumbBorderBrushProperty);
|
||||
set => SetValue(ThumbBorderBrushProperty, value);
|
||||
}
|
||||
|
||||
public double ThumbBorderThickness
|
||||
{
|
||||
get => GetValue(ThumbBorderThicknessProperty);
|
||||
set => SetValue(ThumbBorderThicknessProperty, value);
|
||||
}
|
||||
|
||||
public IBrush SegmentLineBrush
|
||||
{
|
||||
get => GetValue(SegmentLineBrushProperty);
|
||||
set => SetValue(SegmentLineBrushProperty, value);
|
||||
}
|
||||
|
||||
public double SegmentLineThickness
|
||||
{
|
||||
get => GetValue(SegmentLineThicknessProperty);
|
||||
set => SetValue(SegmentLineThicknessProperty, value);
|
||||
}
|
||||
|
||||
static PreviewSlider()
|
||||
{
|
||||
FocusableProperty.OverrideDefaultValue<PreviewSlider>(true);
|
||||
|
||||
ValueProperty.Changed.AddClassHandler<PreviewSlider>((s, _) => s.InvalidateVisual());
|
||||
MinimumProperty.Changed.AddClassHandler<PreviewSlider>((s, _) => s.InvalidateVisual());
|
||||
MaximumProperty.Changed.AddClassHandler<PreviewSlider>((s, _) => s.InvalidateVisual());
|
||||
SegmentDurationProperty.Changed.AddClassHandler<PreviewSlider>((s, _) => s.InvalidateVisual());
|
||||
}
|
||||
|
||||
|
||||
public PreviewSlider()
|
||||
{
|
||||
ClipToBounds = true;
|
||||
|
||||
AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Tunnel);
|
||||
AddHandler(PointerMovedEvent, OnPointerMoved, RoutingStrategies.Tunnel);
|
||||
AddHandler(PointerReleasedEvent, OnPointerReleased, RoutingStrategies.Tunnel);
|
||||
AddHandler(PointerCaptureLostEvent, OnPointerCaptureLost, RoutingStrategies.Tunnel);
|
||||
}
|
||||
|
||||
public override void Render(DrawingContext context)
|
||||
{
|
||||
base.Render(context);
|
||||
|
||||
var bounds = Bounds;
|
||||
if (bounds.Width <= 0 || bounds.Height <= 0)
|
||||
return;
|
||||
|
||||
var centerY = bounds.Height / 2.0;
|
||||
var left = ThumbRadius;
|
||||
var right = bounds.Width - ThumbRadius;
|
||||
|
||||
var trackThickness = TrackThickness;
|
||||
var trackRect = new Rect(left, centerY - trackThickness / 2.0, right - left, trackThickness);
|
||||
|
||||
context.FillRectangle(TrackBrush, trackRect);
|
||||
|
||||
var range = Maximum - Minimum;
|
||||
if (SegmentDuration > 0 && range > 0 && SegmentLineBrush != null && SegmentLineThickness > 0)
|
||||
{
|
||||
var pen = new Pen(SegmentLineBrush, SegmentLineThickness);
|
||||
var totalSegments = (int)Math.Floor(range / SegmentDuration);
|
||||
|
||||
for (var i = 1; i <= totalSegments; i++)
|
||||
{
|
||||
var segmentValue = Minimum + i * SegmentDuration;
|
||||
var tSeg = (segmentValue - Minimum) / range;
|
||||
var xSeg = left + tSeg * (right - left);
|
||||
|
||||
var p1 = new Point(xSeg, centerY - trackThickness);
|
||||
var p2 = new Point(xSeg, centerY + trackThickness);
|
||||
context.DrawLine(pen, p1, p2);
|
||||
}
|
||||
}
|
||||
|
||||
var t = (range <= 0) ? 0.0 : (Value - Minimum) / range;
|
||||
t = Math.Clamp(t, 0.0, 1.0);
|
||||
|
||||
var thumbX = left + t * (right - left);
|
||||
|
||||
var fillRect = new Rect(left, centerY - trackThickness / 2.0, thumbX - left, trackThickness);
|
||||
context.FillRectangle(TrackFillBrush, fillRect);
|
||||
|
||||
var thumbRadius = ThumbRadius;
|
||||
var thumbCenter = new Point(thumbX, centerY);
|
||||
|
||||
var ellipse = new EllipseGeometry(new Rect(
|
||||
thumbCenter.X - thumbRadius,
|
||||
thumbCenter.Y - thumbRadius,
|
||||
thumbRadius * 2,
|
||||
thumbRadius * 2));
|
||||
|
||||
context.DrawGeometry(ThumbBrush, null, ellipse);
|
||||
|
||||
if (ThumbBorderThickness > 0 && ThumbBorderBrush != null)
|
||||
{
|
||||
var pen = new Pen(ThumbBorderBrush, ThumbBorderThickness);
|
||||
context.DrawGeometry(null, pen, ellipse);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
|
||||
{
|
||||
base.OnPointerWheelChanged(e);
|
||||
|
||||
var delta = e.Delta.Y;
|
||||
if (delta == 0)
|
||||
return;
|
||||
|
||||
var step = (Maximum - Minimum) / 100.0;
|
||||
if (step <= 0)
|
||||
step = 1.0;
|
||||
|
||||
if (delta > 0)
|
||||
Value = Math.Clamp(Value - step, Minimum, Maximum);
|
||||
else
|
||||
Value = Math.Clamp(Value + step, Minimum, Maximum);
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (!IsEnabled)
|
||||
return;
|
||||
|
||||
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
return;
|
||||
|
||||
e.Pointer.Capture(this);
|
||||
UpdateValueFromPoint(e.GetPosition(this));
|
||||
_isDragging = true;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnPointerMoved(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (!_isDragging)
|
||||
return;
|
||||
|
||||
UpdateValueFromPoint(e.GetPosition(this));
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
if (!_isDragging)
|
||||
return;
|
||||
|
||||
_isDragging = false;
|
||||
e.Pointer.Capture(null);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnPointerCaptureLost(object? sender, PointerCaptureLostEventArgs e)
|
||||
{
|
||||
_isDragging = false;
|
||||
}
|
||||
|
||||
private void UpdateValueFromPoint(Point point)
|
||||
{
|
||||
var bounds = Bounds;
|
||||
var left = ThumbRadius;
|
||||
var right = bounds.Width - ThumbRadius;
|
||||
|
||||
if (right <= left)
|
||||
return;
|
||||
|
||||
var x = Math.Clamp(point.X, left, right);
|
||||
var t = (x - left) / (right - left);
|
||||
|
||||
var newValue = Minimum + t * (Maximum - Minimum);
|
||||
Value = newValue;
|
||||
|
||||
InvalidateVisual();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -22,6 +22,34 @@ public partial class JobViewModel : ObservableObject
|
||||
|
||||
public string InputFile => Job.InputFile;
|
||||
public double DurationSeconds => Probe?.Duration ?? 0;
|
||||
public double SegmentDuration
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Probe == null || Probe.Duration <= 0)
|
||||
return 58.0;
|
||||
|
||||
var target = Job.OverrideTargetDuration ?? 58.0;
|
||||
|
||||
int segments;
|
||||
double segmentLength;
|
||||
|
||||
if (Job.ForceFixed)
|
||||
{
|
||||
// Fixed chunk size, last one may be shorter
|
||||
segments = (int)Math.Ceiling(Probe.Duration / target);
|
||||
segmentLength = target;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Equalized segments
|
||||
segments = (int)Math.Ceiling(Probe.Duration / target);
|
||||
segmentLength = Probe.Duration / segments;
|
||||
}
|
||||
|
||||
return segmentLength;
|
||||
}
|
||||
}
|
||||
|
||||
public IRelayCommand StepForwardCommand { get; }
|
||||
public IRelayCommand StepBackwardCommand { get; }
|
||||
@ -354,6 +382,7 @@ public partial class JobViewModel : ObservableObject
|
||||
crop = ClampCrop(r, w, h);
|
||||
|
||||
Preview = new PreviewData(frame, detections ?? [], crop, Job.GravitateTo, pos, Job.Rotate);
|
||||
OnPropertyChanged(nameof(SegmentDuration));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -415,9 +444,10 @@ public partial class JobViewModel : ObservableObject
|
||||
if (DurationSeconds <= 0)
|
||||
return;
|
||||
|
||||
var step = DurationSeconds * 0.1; // 10% of total duration
|
||||
var duration = SegmentDuration;
|
||||
var segment = Math.Round(SliderLiveValue / duration, MidpointRounding.ToZero)+1;
|
||||
|
||||
SliderLiveValue = Math.Min(DurationSeconds, SliderLiveValue + step);
|
||||
SliderLiveValue = Math.Min(DurationSeconds - duration, segment * duration);
|
||||
// trigger seek in your playback pipeline here
|
||||
}
|
||||
|
||||
@ -426,9 +456,10 @@ public partial class JobViewModel : ObservableObject
|
||||
if (DurationSeconds <= 0)
|
||||
return;
|
||||
|
||||
var step = DurationSeconds * 0.1; // 10% of total duration
|
||||
var duration = SegmentDuration;
|
||||
var segment = Math.Max(0, Math.Round(SliderLiveValue / duration, MidpointRounding.ToZero)-1);
|
||||
|
||||
SliderLiveValue = Math.Max(0, SliderLiveValue - step);
|
||||
SliderLiveValue = segment * duration;
|
||||
// trigger seek in your playback pipeline here
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:Splitter_UI.ViewModels"
|
||||
xmlns:local="clr-namespace:Splitter_UI.Views"
|
||||
xmlns:controls="clr-namespace:Splitter_UI.Controls"
|
||||
x:Class="Splitter_UI.Views.PreviewPane"
|
||||
x:DataType="vm:PreviewPaneViewModel">
|
||||
|
||||
@ -35,12 +36,19 @@
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center" />
|
||||
</Button>
|
||||
|
||||
<!--
|
||||
<Slider Grid.Column="1"
|
||||
Minimum="0"
|
||||
Maximum="{Binding Selected.DurationSeconds}"
|
||||
Value="{Binding Selected.SliderLiveValue, Mode=TwoWay}"
|
||||
Margin="5,0,5,0" />
|
||||
Minimum="0"
|
||||
Maximum="{Binding Selected.DurationSeconds}"
|
||||
Value="{Binding Selected.SliderLiveValue, Mode=TwoWay}"
|
||||
Margin="5,0,5,0" />
|
||||
-->
|
||||
<controls:PreviewSlider Grid.Column="1"
|
||||
Minimum="0"
|
||||
Maximum="{Binding Selected.DurationSeconds}"
|
||||
Value="{Binding Selected.SliderLiveValue, Mode=TwoWay}"
|
||||
SegmentDuration="{Binding Selected.SegmentDuration}"
|
||||
Margin="5,0,5,0" />
|
||||
|
||||
<Button Grid.Column="2"
|
||||
HorizontalAlignment="Right"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user