mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-22 00:22: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 string InputFile => Job.InputFile;
|
||||||
public double DurationSeconds => Probe?.Duration ?? 0;
|
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 StepForwardCommand { get; }
|
||||||
public IRelayCommand StepBackwardCommand { get; }
|
public IRelayCommand StepBackwardCommand { get; }
|
||||||
@ -354,6 +382,7 @@ public partial class JobViewModel : ObservableObject
|
|||||||
crop = ClampCrop(r, w, h);
|
crop = ClampCrop(r, w, h);
|
||||||
|
|
||||||
Preview = new PreviewData(frame, detections ?? [], crop, Job.GravitateTo, pos, Job.Rotate);
|
Preview = new PreviewData(frame, detections ?? [], crop, Job.GravitateTo, pos, Job.Rotate);
|
||||||
|
OnPropertyChanged(nameof(SegmentDuration));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -415,9 +444,10 @@ public partial class JobViewModel : ObservableObject
|
|||||||
if (DurationSeconds <= 0)
|
if (DurationSeconds <= 0)
|
||||||
return;
|
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
|
// trigger seek in your playback pipeline here
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -426,9 +456,10 @@ public partial class JobViewModel : ObservableObject
|
|||||||
if (DurationSeconds <= 0)
|
if (DurationSeconds <= 0)
|
||||||
return;
|
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
|
// trigger seek in your playback pipeline here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:vm="clr-namespace:Splitter_UI.ViewModels"
|
xmlns:vm="clr-namespace:Splitter_UI.ViewModels"
|
||||||
xmlns:local="clr-namespace:Splitter_UI.Views"
|
xmlns:local="clr-namespace:Splitter_UI.Views"
|
||||||
|
xmlns:controls="clr-namespace:Splitter_UI.Controls"
|
||||||
x:Class="Splitter_UI.Views.PreviewPane"
|
x:Class="Splitter_UI.Views.PreviewPane"
|
||||||
x:DataType="vm:PreviewPaneViewModel">
|
x:DataType="vm:PreviewPaneViewModel">
|
||||||
|
|
||||||
@ -35,12 +36,19 @@
|
|||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
HorizontalAlignment="Center" />
|
HorizontalAlignment="Center" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<!--
|
||||||
<Slider Grid.Column="1"
|
<Slider Grid.Column="1"
|
||||||
Minimum="0"
|
Minimum="0"
|
||||||
Maximum="{Binding Selected.DurationSeconds}"
|
Maximum="{Binding Selected.DurationSeconds}"
|
||||||
Value="{Binding Selected.SliderLiveValue, Mode=TwoWay}"
|
Value="{Binding Selected.SliderLiveValue, Mode=TwoWay}"
|
||||||
Margin="5,0,5,0" />
|
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"
|
<Button Grid.Column="2"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user