diff --git a/src/ui/Features/Tools/AiReview/AiReviewWindow.cs b/src/ui/Features/Tools/AiReview/AiReviewWindow.cs index 0e2dc01c651..4a5c3cd7fd7 100644 --- a/src/ui/Features/Tools/AiReview/AiReviewWindow.cs +++ b/src/ui/Features/Tools/AiReview/AiReviewWindow.cs @@ -9,8 +9,8 @@ using Nikse.SubtitleEdit.Features.Files.Compare; using Nikse.SubtitleEdit.Features.Main; using Nikse.SubtitleEdit.Logic; -using Nikse.SubtitleEdit.Logic.Platform.Progress; using Nikse.SubtitleEdit.Logic.Config; +using Nikse.SubtitleEdit.Logic.Platform.Progress; namespace Nikse.SubtitleEdit.Features.Tools.AiReview; diff --git a/src/ui/Features/Video/BurnIn/BurnInWindow.cs b/src/ui/Features/Video/BurnIn/BurnInWindow.cs index 17176b3af15..4c2e62de562 100644 --- a/src/ui/Features/Video/BurnIn/BurnInWindow.cs +++ b/src/ui/Features/Video/BurnIn/BurnInWindow.cs @@ -9,8 +9,8 @@ using Nikse.SubtitleEdit.Controls; using Nikse.SubtitleEdit.Features.Main.Layout; using Nikse.SubtitleEdit.Logic; -using Nikse.SubtitleEdit.Logic.Platform.Progress; using Nikse.SubtitleEdit.Logic.Config; +using Nikse.SubtitleEdit.Logic.Platform.Progress; using Nikse.SubtitleEdit.Logic.ValueConverters; using Nikse.SubtitleEdit.UiLogic.Export; using System; diff --git a/src/ui/Features/Video/ShotChanges/ShotChangesWindow.cs b/src/ui/Features/Video/ShotChanges/ShotChangesWindow.cs index f79b18e8ed5..a32677e5235 100644 --- a/src/ui/Features/Video/ShotChanges/ShotChangesWindow.cs +++ b/src/ui/Features/Video/ShotChanges/ShotChangesWindow.cs @@ -6,8 +6,8 @@ using Avalonia.Layout; using Avalonia.Styling; using Nikse.SubtitleEdit.Logic; -using Nikse.SubtitleEdit.Logic.Platform.Progress; using Nikse.SubtitleEdit.Logic.Config; +using Nikse.SubtitleEdit.Logic.Platform.Progress; using Nikse.SubtitleEdit.Logic.ValueConverters; namespace Nikse.SubtitleEdit.Features.Video.ShotChanges; diff --git a/src/ui/Features/Video/SpeechToText/SpeechToTextWindow.cs b/src/ui/Features/Video/SpeechToText/SpeechToTextWindow.cs index 2eb5a96cb67..9ffa08e317e 100644 --- a/src/ui/Features/Video/SpeechToText/SpeechToTextWindow.cs +++ b/src/ui/Features/Video/SpeechToText/SpeechToTextWindow.cs @@ -10,9 +10,9 @@ using System.Linq; using Nikse.SubtitleEdit.Features.Video.SpeechToText.Engines; using Nikse.SubtitleEdit.Logic; -using Nikse.SubtitleEdit.Logic.Platform.Progress; using Nikse.SubtitleEdit.Logic.Config; using Nikse.SubtitleEdit.Logic.Download; +using Nikse.SubtitleEdit.Logic.Platform.Progress; using Nikse.SubtitleEdit.Logic.ValueConverters; namespace Nikse.SubtitleEdit.Features.Video.SpeechToText; diff --git a/src/ui/Logic/Platform/Progress/PlatformProgress.cs b/src/ui/Logic/Platform/Progress/PlatformProgress.cs index ee9fac78285..25bbdffac4a 100644 --- a/src/ui/Logic/Platform/Progress/PlatformProgress.cs +++ b/src/ui/Logic/Platform/Progress/PlatformProgress.cs @@ -13,8 +13,9 @@ public static class PlatformProgress AvaloniaProperty.RegisterAttached("IsActive", typeof(PlatformProgress)); private static readonly ConditionalWeakTable WindowGroups = new(); - private static readonly PlatformProgressGroup ApplicationGroup = new(() => - OperatingSystem.IsMacOS() ? new MacDockProgress() : new LinuxLauncherProgress()); + private static readonly PlatformProgressGroup ApplicationGroup = new( + () => OperatingSystem.IsMacOS() ? new MacDockProgress() : new LinuxLauncherProgress(), + keepBackendWhenIdle: OperatingSystem.IsLinux()); public static ProgressBar WithPlatformProgress(this ProgressBar progressBar, Window window, string activeProperty, IValueConverter? converter = null) diff --git a/src/ui/Logic/Platform/Progress/PlatformProgressGroup.cs b/src/ui/Logic/Platform/Progress/PlatformProgressGroup.cs index 04cedacfd43..eed132ca591 100644 --- a/src/ui/Logic/Platform/Progress/PlatformProgressGroup.cs +++ b/src/ui/Logic/Platform/Progress/PlatformProgressGroup.cs @@ -4,10 +4,11 @@ namespace Nikse.SubtitleEdit.Logic.Platform.Progress; -internal sealed class PlatformProgressGroup(Func createProgress) +internal sealed class PlatformProgressGroup(Func createProgress, bool keepBackendWhenIdle = false) { private readonly List<(object Source, double? Percentage, bool Indeterminate)> _sources = []; private IPlatformProgress? _progress; + private (object Source, int? Percentage, bool Indeterminate)? _lastUpdate; public void Update(object source, double? percentage, bool indeterminate) { @@ -30,14 +31,27 @@ public void Update(object source, double? percentage, bool indeterminate) if (_sources.Count == 0) { _progress?.Update(null, false); - _progress?.Dispose(); - _progress = null; + _lastUpdate = null; + // Keep the Linux connection alive so asynchronous clear requests can be sent. + if (!keepBackendWhenIdle) + { + _progress?.Dispose(); + _progress = null; + } return; } _progress ??= createProgress(); var current = _sources[^1]; - _progress.Update(current.Percentage, current.Indeterminate); + var effectivePercentage = !current.Indeterminate && current.Percentage is { } value + ? (int?)value : null; + // Source changes must be published even when their visible state is the same. + if (_lastUpdate is { } last && ReferenceEquals(last.Source, current.Source) && + last.Percentage == effectivePercentage && last.Indeterminate == current.Indeterminate) + return; + + _progress.Update(effectivePercentage, current.Indeterminate); + _lastUpdate = (current.Source, effectivePercentage, current.Indeterminate); } catch (Exception exception) { diff --git a/tests/UI/Logic/Platform/PlatformProgressTests.cs b/tests/UI/Logic/Platform/PlatformProgressTests.cs index af8dd2f54ac..37014cddcc7 100644 --- a/tests/UI/Logic/Platform/PlatformProgressTests.cs +++ b/tests/UI/Logic/Platform/PlatformProgressTests.cs @@ -39,11 +39,13 @@ private sealed class FakeProgress : IPlatformProgress public void Dispose() => IsDisposed = true; } - [Fact] - public void CompletingCurrentSourceRestoresOtherActiveSource() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void CompletingCurrentSourceRestoresOtherActiveSource(bool keepBackendWhenIdle) { var progress = new FakeProgress(); - var group = new PlatformProgressGroup(() => progress); + var group = new PlatformProgressGroup(() => progress, keepBackendWhenIdle); var first = new object(); var second = new object(); @@ -58,7 +60,7 @@ public void CompletingCurrentSourceRestoresOtherActiveSource() group.Update(first, null, false); Assert.Equal(((double?)null, false), progress.Updates[^1]); - Assert.True(progress.IsDisposed); + Assert.Equal(!keepBackendWhenIdle, progress.IsDisposed); } [Fact] @@ -73,6 +75,7 @@ public void CompletingBackgroundSourceKeepsCurrentSource() group.Update(second, 20, false); group.Update(first, null, false); + Assert.Equal(2, progress.Updates.Count); Assert.Equal(((double?)20, false), progress.Updates[^1]); Assert.False(progress.IsDisposed); group.Update(second, null, false); @@ -111,8 +114,110 @@ public void IndeterminateSourceRemainsActive() group.Update(source, null, false); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public void RetainedBackendClearsAndIsReusedForNextOperation(bool indeterminate) + { + var instances = new List(); + var group = new PlatformProgressGroup(() => + { + var progress = new FakeProgress(); + instances.Add(progress); + return progress; + }, keepBackendWhenIdle: true); + var first = new object(); + var second = new object(); + double? percentage = indeterminate ? null : 10; + + group.Update(first, null, false); + Assert.Empty(instances); + + group.Update(first, percentage, indeterminate); + var backend = Assert.Single(instances); + group.Update(first, null, false); + + Assert.Equal(2, backend.Updates.Count); + Assert.Equal((percentage, indeterminate), backend.Updates[0]); + Assert.Equal(((double?)null, false), backend.Updates[1]); + Assert.False(backend.IsDisposed); + + group.Update(second, 20, false); + + Assert.Same(backend, Assert.Single(instances)); + Assert.Equal(((double?)20, false), backend.Updates[^1]); + group.Update(second, null, false); + Assert.Equal(4, backend.Updates.Count); + Assert.Equal(((double?)null, false), backend.Updates[^1]); + Assert.False(backend.IsDisposed); + } + + [Fact] + public void UpdatesWithinSameWholePercentageAreSkipped() + { + var progress = new FakeProgress(); + var group = new PlatformProgressGroup(() => progress); + var source = new object(); + + group.Update(source, 10.1, false); + group.Update(source, 10.99, false); + group.Update(source, 11, false); + group.Update(source, 11.9, false); + group.Update(source, 10.9, false); + + Assert.Equal(new[] { ((double?)10, false), ((double?)11, false), ((double?)10, false) }, progress.Updates); + group.Update(source, null, false); + Assert.Equal(((double?)null, false), progress.Updates[^1]); + } + [Fact] - public void NewOperationCreatesNewBackendAfterCleanup() + public void IndeterminateTransitionsAreSentButNumericChangesAreSkipped() + { + var progress = new FakeProgress(); + var group = new PlatformProgressGroup(() => progress); + var source = new object(); + + group.Update(source, 35.2, false); + group.Update(source, 35.2, true); + group.Update(source, 80.9, true); + group.Update(source, null, true); + group.Update(source, 35.9, false); + + Assert.Equal(new[] { ((double?)35, false), ((double?)null, true), ((double?)35, false) }, progress.Updates); + group.Update(source, null, false); + Assert.Equal(((double?)null, false), progress.Updates[^1]); + } + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void NewSourceAndRestorationAreSentEvenWithSameEffectiveState(bool keepBackendWhenIdle, bool indeterminate) + { + var progress = new FakeProgress(); + var group = new PlatformProgressGroup(() => progress, keepBackendWhenIdle); + var first = new object(); + var second = new object(); + + group.Update(first, 20.2, indeterminate); + group.Update(second, 20.9, indeterminate); + group.Update(first, 20.7, indeterminate); + Assert.Equal(2, progress.Updates.Count); + + group.Update(second, null, false); + + double? effectivePercentage = indeterminate ? null : 20; + Assert.Equal(new[] { (effectivePercentage, indeterminate), (effectivePercentage, indeterminate), + (effectivePercentage, indeterminate) }, progress.Updates); + group.Update(first, null, false); + Assert.Equal(((double?)null, false), progress.Updates[^1]); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ClearAndRestartOfSameSourceAreSentEvenWithSameWholePercentage(bool keepBackendWhenIdle) { var instances = new List(); var group = new PlatformProgressGroup(() => @@ -120,19 +225,26 @@ public void NewOperationCreatesNewBackendAfterCleanup() var progress = new FakeProgress(); instances.Add(progress); return progress; - }); + }, keepBackendWhenIdle); var source = new object(); group.Update(source, null, false); Assert.Empty(instances); - group.Update(source, 10, false); + + group.Update(source, 20.2, false); + group.Update(source, 20.9, false); group.Update(source, null, false); - group.Update(source, 20, false); - Assert.Equal(2, instances.Count); - Assert.True(instances[0].IsDisposed); - Assert.Equal(((double?)20, false), instances[1].Updates[^1]); + Assert.Equal(new[] { ((double?)20, false), ((double?)null, false) }, instances[0].Updates); + Assert.Equal(!keepBackendWhenIdle, instances[0].IsDisposed); + + group.Update(source, 20.7, false); + + Assert.Equal(keepBackendWhenIdle ? 1 : 2, instances.Count); + Assert.Equal(keepBackendWhenIdle ? 3 : 1, instances[^1].Updates.Count); + Assert.Equal(((double?)20, false), instances[^1].Updates[^1]); group.Update(source, null, false); + Assert.Equal(((double?)null, false), instances[^1].Updates[^1]); } [Fact]