From 19d988ab797c87a065368e75c2c3307c195c3c4e Mon Sep 17 00:00:00 2001 From: Michael Vinther Date: Fri, 13 Feb 2026 15:05:04 +0100 Subject: [PATCH 1/9] Dropdown sliders --- PhotoLocator/Controls/DropDownSlider.xaml | 35 ++++++++ PhotoLocator/Controls/DropDownSlider.xaml.cs | 83 +++++++++++++++++++ PhotoLocator/Helpers/DropDownSliderService.cs | 19 +++++ PhotoLocator/VideoTransformWindow.xaml | 17 ++-- 4 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 PhotoLocator/Controls/DropDownSlider.xaml create mode 100644 PhotoLocator/Controls/DropDownSlider.xaml.cs create mode 100644 PhotoLocator/Helpers/DropDownSliderService.cs diff --git a/PhotoLocator/Controls/DropDownSlider.xaml b/PhotoLocator/Controls/DropDownSlider.xaml new file mode 100644 index 0000000..225461a --- /dev/null +++ b/PhotoLocator/Controls/DropDownSlider.xaml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/PhotoLocator/Controls/DropDownSlider.xaml.cs b/PhotoLocator/Controls/DropDownSlider.xaml.cs new file mode 100644 index 0000000..48a9130 --- /dev/null +++ b/PhotoLocator/Controls/DropDownSlider.xaml.cs @@ -0,0 +1,83 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Controls; + +namespace PhotoLocator.Controls; + +public partial class DropDownSlider : UserControl +{ + public DropDownSlider() + { + InitializeComponent(); + } + + public static readonly DependencyProperty TextProperty = DependencyProperty.Register( + nameof(Text), typeof(string), typeof(DropDownSlider), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnTextChanged)); + + public string Text + { + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + static void OnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is DropDownSlider control) + { + if (double.TryParse(control.Text, NumberStyles.Float, CultureInfo.InvariantCulture, out var v)) + { + // Clamp to range + if (v < control.Minimum) v = control.Minimum; + if (v > control.Maximum) v = control.Maximum; + control.NumericValue = v; + } + } + } + + public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register( + nameof(Minimum), typeof(double), typeof(DropDownSlider), new PropertyMetadata(0.0)); + public double Minimum { get => (double)GetValue(MinimumProperty); set => SetValue(MinimumProperty, value); } + + public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register( + nameof(Maximum), typeof(double), typeof(DropDownSlider), new PropertyMetadata(100.0)); + public double Maximum { get => (double)GetValue(MaximumProperty); set => SetValue(MaximumProperty, value); } + + public static readonly DependencyProperty NumericValueProperty = DependencyProperty.Register( + nameof(NumericValue), typeof(double), typeof(DropDownSlider), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnNumericValueChanged)); + public double NumericValue { get => (double)GetValue(NumericValueProperty); set => SetValue(NumericValueProperty, value); } + + public static readonly DependencyProperty DecimalsProperty = DependencyProperty.Register( + nameof(Decimals), typeof(int), typeof(DropDownSlider), new PropertyMetadata(2, OnDecimalsChanged)); + public int Decimals { get => (int)GetValue(DecimalsProperty); set => SetValue(DecimalsProperty, value); } + + static void OnDecimalsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is DropDownSlider control) + control.UpdateFormattedValue(); + } + + public static readonly DependencyProperty TickFrequencyProperty = DependencyProperty.Register( + nameof(TickFrequency), typeof(double), typeof(DropDownSlider), new PropertyMetadata(0.01)); + public double TickFrequency { get => (double)GetValue(TickFrequencyProperty); set => SetValue(TickFrequencyProperty, value); } + + public static readonly DependencyProperty FormattedNumericValueProperty = DependencyProperty.Register( + nameof(FormattedNumericValue), typeof(string), typeof(DropDownSlider), new PropertyMetadata(string.Empty)); + public string FormattedNumericValue { get => (string)GetValue(FormattedNumericValueProperty); set => SetValue(FormattedNumericValueProperty, value); } + + static void OnNumericValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is DropDownSlider control) + { + // Format with configured number of decimals + control.Text = control.NumericValue.ToString("F" + control.Decimals.ToString(CultureInfo.InvariantCulture), CultureInfo.InvariantCulture); + control.UpdateFormattedValue(); + } + } + + void UpdateFormattedValue() + { + var fmt = "F" + Decimals.ToString(CultureInfo.InvariantCulture); + FormattedNumericValue = NumericValue.ToString(fmt, CultureInfo.InvariantCulture); + } +} diff --git a/PhotoLocator/Helpers/DropDownSliderService.cs b/PhotoLocator/Helpers/DropDownSliderService.cs new file mode 100644 index 0000000..c620db1 --- /dev/null +++ b/PhotoLocator/Helpers/DropDownSliderService.cs @@ -0,0 +1,19 @@ +using System.Windows; + +namespace PhotoLocator.Helpers +{ + public static class DropDownSliderService + { + public static readonly DependencyProperty MinimumProperty = DependencyProperty.RegisterAttached( + "Minimum", typeof(double), typeof(DropDownSliderService), new PropertyMetadata(0.0)); + + public static void SetMinimum(DependencyObject element, double value) => element.SetValue(MinimumProperty, value); + public static double GetMinimum(DependencyObject element) => (double)element.GetValue(MinimumProperty); + + public static readonly DependencyProperty MaximumProperty = DependencyProperty.RegisterAttached( + "Maximum", typeof(double), typeof(DropDownSliderService), new PropertyMetadata(100.0)); + + public static void SetMaximum(DependencyObject element, double value) => element.SetValue(MaximumProperty, value); + public static double GetMaximum(DependencyObject element) => (double)element.GetValue(MaximumProperty); + } +} diff --git a/PhotoLocator/VideoTransformWindow.xaml b/PhotoLocator/VideoTransformWindow.xaml index 91b9a8b..74baa17 100644 --- a/PhotoLocator/VideoTransformWindow.xaml +++ b/PhotoLocator/VideoTransformWindow.xaml @@ -5,6 +5,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:helpers="clr-namespace:PhotoLocator.Helpers" xmlns:local="clr-namespace:PhotoLocator" + xmlns:controls="clr-namespace:PhotoLocator.Controls" mc:Ignorable="d" d:DataContext="{d:DesignInstance Type=local:VideoTransformCommands, IsDesignTimeCreatable=false}" Background="Black" @@ -23,22 +24,22 @@ - + - + - + - + From 923927e463a6fcb5a99204068f65410df156c427 Mon Sep 17 00:00:00 2001 From: Michael Vinther Date: Fri, 13 Feb 2026 19:42:10 +0100 Subject: [PATCH 2/9] Delay frame update --- PhotoLocator/Controls/DropDownSlider.xaml | 2 +- PhotoLocator/Controls/DropDownSlider.xaml.cs | 4 +- PhotoLocator/Helpers/StringExtensions.cs | 5 +++ PhotoLocator/VideoTransformCommands.cs | 40 +++++++++++++++----- PhotoLocator/VideoTransformWindow.xaml | 8 ++-- 5 files changed, 42 insertions(+), 17 deletions(-) diff --git a/PhotoLocator/Controls/DropDownSlider.xaml b/PhotoLocator/Controls/DropDownSlider.xaml index 225461a..d5ef83c 100644 --- a/PhotoLocator/Controls/DropDownSlider.xaml +++ b/PhotoLocator/Controls/DropDownSlider.xaml @@ -14,7 +14,7 @@ IsEnabled="{Binding IsEnabled, RelativeSource={RelativeSource AncestorType=UserControl}}" VerticalContentAlignment="Center" /> - (double)GetValue(NumericValueProperty); set => SetValue(NumericValueProperty, value); } public static readonly DependencyProperty DecimalsProperty = DependencyProperty.Register( - nameof(Decimals), typeof(int), typeof(DropDownSlider), new PropertyMetadata(2, OnDecimalsChanged)); + nameof(Decimals), typeof(int), typeof(DropDownSlider), new PropertyMetadata(1, OnDecimalsChanged)); public int Decimals { get => (int)GetValue(DecimalsProperty); set => SetValue(DecimalsProperty, value); } static void OnDecimalsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) @@ -58,7 +58,7 @@ static void OnDecimalsChanged(DependencyObject d, DependencyPropertyChangedEvent } public static readonly DependencyProperty TickFrequencyProperty = DependencyProperty.Register( - nameof(TickFrequency), typeof(double), typeof(DropDownSlider), new PropertyMetadata(0.01)); + nameof(TickFrequency), typeof(double), typeof(DropDownSlider), new PropertyMetadata(0.1)); public double TickFrequency { get => (double)GetValue(TickFrequencyProperty); set => SetValue(TickFrequencyProperty, value); } public static readonly DependencyProperty FormattedNumericValueProperty = DependencyProperty.Register( diff --git a/PhotoLocator/Helpers/StringExtensions.cs b/PhotoLocator/Helpers/StringExtensions.cs index fcf4607..80e393d 100644 --- a/PhotoLocator/Helpers/StringExtensions.cs +++ b/PhotoLocator/Helpers/StringExtensions.cs @@ -6,5 +6,10 @@ public static string TrimPath(this string path) { return path.Trim(' ', '"'); } + + public static string TrimInvariantValue(this string str) + { + return str.Trim().Replace(',', '.'); + } } } diff --git a/PhotoLocator/VideoTransformCommands.cs b/PhotoLocator/VideoTransformCommands.cs index ecd3cc9..d2b2c38 100644 --- a/PhotoLocator/VideoTransformCommands.cs +++ b/PhotoLocator/VideoTransformCommands.cs @@ -19,6 +19,7 @@ using System.Windows; using System.Windows.Input; using System.Windows.Media.Imaging; +using System.Windows.Threading; namespace PhotoLocator; @@ -32,6 +33,8 @@ public class VideoTransformCommands : INotifyPropertyChanged readonly IMainViewModel _mainViewModel; readonly VideoProcessing _videoTransforms; Action? _progressCallback; + DispatcherTimer? _previewUpdateDelay; + string? _previewSkipTo; double _progressOffset, _progressScale; TimeSpan _inputDuration; double _fps; @@ -81,12 +84,12 @@ public string SkipTo get; set { - if (!SetProperty(ref field, value.Trim())) + if (!SetProperty(ref field, value.TrimInvariantValue())) return; UpdateInputArgs(); UpdateOutputArgs(); _localContrastSetup?.SourceBitmap = null; - _mainViewModel.UpdatePreviewPictureAsync(SkipTo).WithExceptionLogging(); + BeginPreviewUpdate(SkipTo); } } = string.Empty; @@ -95,17 +98,33 @@ public string Duration get; set { - if (!SetProperty(ref field, value.Trim())) + if (!SetProperty(ref field, value.TrimInvariantValue())) return; UpdateInputArgs(); UpdateOutputArgs(); if (string.IsNullOrEmpty(SkipTo)) - _mainViewModel.UpdatePreviewPictureAsync(Duration).WithExceptionLogging(); + BeginPreviewUpdate(Duration); else if (double.TryParse(SkipTo, CultureInfo.InvariantCulture, out var skipToSeconds) && double.TryParse(Duration, CultureInfo.InvariantCulture, out var durationSeconds)) - _mainViewModel.UpdatePreviewPictureAsync((skipToSeconds + durationSeconds).ToString(CultureInfo.InvariantCulture)).WithExceptionLogging(); + BeginPreviewUpdate((skipToSeconds + durationSeconds).ToString(CultureInfo.InvariantCulture)); } } = string.Empty; + private void BeginPreviewUpdate(string skipTo) + { + if (_previewUpdateDelay is null) + { + _previewUpdateDelay = new DispatcherTimer { Interval = TimeSpan.FromSeconds(0.5) }; + _previewUpdateDelay.Tick += (s, e) => + { + _previewUpdateDelay.Stop(); + _mainViewModel.UpdatePreviewPictureAsync(_previewSkipTo).WithExceptionLogging(); + }; + } + _previewSkipTo = skipTo; + _previewUpdateDelay.Stop(); + _previewUpdateDelay.Start(); + } + public bool IsRotateChecked { get; @@ -121,7 +140,7 @@ public string RotationAngle get; set { - if (SetProperty(ref field, value.Trim())) + if (SetProperty(ref field, value.TrimInvariantValue())) UpdateProcessArgs(); } } = string.Empty; @@ -141,7 +160,7 @@ public string SpeedupBy get; set { - if (SetProperty(ref field, value.Trim())) + if (SetProperty(ref field, value.TrimInvariantValue())) UpdateProcessArgs(); } } = string.Empty; @@ -188,7 +207,7 @@ public string ScaleTo get; set { - if (SetProperty(ref field, value.Trim())) + if (SetProperty(ref field, value.TrimInvariantValue())) UpdateProcessArgs(); } } = "w:h"; @@ -254,7 +273,7 @@ public string? EffectParameter get => _effectParameter; set { - if (SetProperty(ref _effectParameter, value?.Trim().Replace(',', '.'))) + if (SetProperty(ref _effectParameter, value?.TrimInvariantValue())) UpdateProcessArgs(); } } @@ -519,7 +538,7 @@ public string VideoBitRate get; set { - if (SetProperty(ref field, value.Trim())) + if (SetProperty(ref field, value.TrimInvariantValue())) UpdateOutputArgs(); } } = string.Empty; @@ -896,6 +915,7 @@ internal void CropSelected(Rect cropRectangle) } finally { + _previewUpdateDelay?.Stop(); if (_localContrastSetup is not null && _localContrastSetup.SourceBitmap is not null) _localContrastSetup.SourceBitmap = null; window.DataContext = null; diff --git a/PhotoLocator/VideoTransformWindow.xaml b/PhotoLocator/VideoTransformWindow.xaml index 74baa17..0aa1f93 100644 --- a/PhotoLocator/VideoTransformWindow.xaml +++ b/PhotoLocator/VideoTransformWindow.xaml @@ -25,21 +25,21 @@ + ToolTip="Starting position in seconds" Maximum="600" /> + ToolTip="Clip duration in seconds" Maximum="600" /> + ToolTip="Clockwise rotation angle in degrees" Minimum="-180" Maximum="180" TickFrequency="0.1" Decimals="2" /> + ToolTip="Speedup factor (below 1 means slowdown)" Minimum="0.1" Maximum="10" TickFrequency="0.1" Decimals="2" /> From bc2a99462dfe939257fe4b588726365e5a0c9f18 Mon Sep 17 00:00:00 2001 From: Michael Vinther Date: Sat, 14 Feb 2026 13:08:10 +0100 Subject: [PATCH 3/9] Slider range from first clip --- .../ColorToneControl.xaml | 4 +- .../ColorToneControl.xaml.cs | 3 +- .../{Helpers => Controls}/CropControl.xaml | 4 +- .../{Helpers => Controls}/CropControl.xaml.cs | 5 +- PhotoLocator/Controls/DropDownSlider.xaml | 2 +- PhotoLocator/Helpers/IntMath.cs | 3 + PhotoLocator/Helpers/RealMath.cs | 6 ++ PhotoLocator/LocalContrastView.xaml | 4 +- PhotoLocator/MainViewModel.cs | 1 + PhotoLocator/MainWindow.xaml | 4 +- PhotoLocator/VideoTransformCommands.cs | 58 ++++++++++++++----- PhotoLocator/VideoTransformWindow.xaml | 4 +- 12 files changed, 69 insertions(+), 29 deletions(-) rename PhotoLocator/{Helpers => Controls}/ColorToneControl.xaml (84%) rename PhotoLocator/{Helpers => Controls}/ColorToneControl.xaml.cs (99%) rename PhotoLocator/{Helpers => Controls}/CropControl.xaml (97%) rename PhotoLocator/{Helpers => Controls}/CropControl.xaml.cs (99%) diff --git a/PhotoLocator/Helpers/ColorToneControl.xaml b/PhotoLocator/Controls/ColorToneControl.xaml similarity index 84% rename from PhotoLocator/Helpers/ColorToneControl.xaml rename to PhotoLocator/Controls/ColorToneControl.xaml index 0be2700..eb4974e 100644 --- a/PhotoLocator/Helpers/ColorToneControl.xaml +++ b/PhotoLocator/Controls/ColorToneControl.xaml @@ -1,10 +1,10 @@ - diff --git a/PhotoLocator/Helpers/ColorToneControl.xaml.cs b/PhotoLocator/Controls/ColorToneControl.xaml.cs similarity index 99% rename from PhotoLocator/Helpers/ColorToneControl.xaml.cs rename to PhotoLocator/Controls/ColorToneControl.xaml.cs index a3c7897..d69e50b 100644 --- a/PhotoLocator/Helpers/ColorToneControl.xaml.cs +++ b/PhotoLocator/Controls/ColorToneControl.xaml.cs @@ -1,4 +1,5 @@ using PhotoLocator.BitmapOperations; +using PhotoLocator.Helpers; using System; using System.ComponentModel; using System.Threading.Tasks; @@ -8,7 +9,7 @@ using System.Windows.Media; using System.Windows.Media.Imaging; -namespace PhotoLocator.Helpers +namespace PhotoLocator.Controls { /// /// Interaction logic for ColorToneControl.xaml diff --git a/PhotoLocator/Helpers/CropControl.xaml b/PhotoLocator/Controls/CropControl.xaml similarity index 97% rename from PhotoLocator/Helpers/CropControl.xaml rename to PhotoLocator/Controls/CropControl.xaml index fd85a30..4d34aaa 100644 --- a/PhotoLocator/Helpers/CropControl.xaml +++ b/PhotoLocator/Controls/CropControl.xaml @@ -1,9 +1,9 @@ - - diff --git a/PhotoLocator/Helpers/IntMath.cs b/PhotoLocator/Helpers/IntMath.cs index 89e0186..c53b1d3 100644 --- a/PhotoLocator/Helpers/IntMath.cs +++ b/PhotoLocator/Helpers/IntMath.cs @@ -17,6 +17,9 @@ public static int Round(double a) return (int)Math.Round(a); } + /// + /// Clamp without min/max check, for better performance when the caller can guarantee the value is within range. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int Clamp(int value, int min, int max) { diff --git a/PhotoLocator/Helpers/RealMath.cs b/PhotoLocator/Helpers/RealMath.cs index b5bec1e..9db9400 100644 --- a/PhotoLocator/Helpers/RealMath.cs +++ b/PhotoLocator/Helpers/RealMath.cs @@ -5,6 +5,9 @@ namespace PhotoLocator.Helpers { public static class RealMath { + /// + /// Clamp without min/max check, for better performance when the caller can guarantee the value is within range. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float Clamp(float value, float min, float max) { @@ -15,6 +18,9 @@ public static float Clamp(float value, float min, float max) return value; } + /// + /// Clamp without min/max check, for better performance when the caller can guarantee the value is within range. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double Clamp(double value, double min, double max) { diff --git a/PhotoLocator/LocalContrastView.xaml b/PhotoLocator/LocalContrastView.xaml index 136b21f..4c958ef 100644 --- a/PhotoLocator/LocalContrastView.xaml +++ b/PhotoLocator/LocalContrastView.xaml @@ -4,7 +4,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:sys="clr-namespace:System;assembly=mscorlib" - xmlns:local="clr-namespace:PhotoLocator" xmlns:helpers="clr-namespace:PhotoLocator.Helpers" + xmlns:local="clr-namespace:PhotoLocator" xmlns:controls="clr-namespace:PhotoLocator.Controls" mc:Ignorable="d" d:DataContext="{d:DesignInstance Type=local:LocalContrastViewModel, IsDesignTimeCreatable=True}" Background="Black" @@ -181,7 +181,7 @@ - +