Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ version it publishes at the top of the GitHub release notes, above the list of p
Add a `## <version>` section before tagging a release. Write what someone using Filee notices: what's new, where to
find it, what it does and doesn't do.

## 1.5.1

### Fixes

- Sometimes, after Ctrl+drag opened the donut, the format under the dragged files didn't light up and dropping
didn't pick it, until a while later. The donut's animation could wait forever for a frame that never came; it now
starts again after a quarter of a second.
- Each drag over the donut writes one line to the log (`%APPDATA%\Filee\logs`), so if a donut ever ignores a drag
again, the log shows whether the drag reached it.

## 1.5.0

### FFmpeg, calibre and the others you already have are used by themselves
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
parameters of a member is fine (CS1573). -->
<NoWarn>$(NoWarn);CS1591;CS1573</NoWarn>

<Version>1.5.0</Version>
<Version>1.5.1</Version>
<Product>Filee</Product>
<Authors>Filee contributors</Authors>
<Copyright>Copyright (c) Filee contributors</Copyright>
Expand Down
29 changes: 25 additions & 4 deletions src/Filee.App/Controls/DonutMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ public void SetExternalHighlight(int index, bool centerHot = false)
StartAnimation();
}

/// <summary>How far slice <paramref name="index"/> is drawn lit up right now (0 to 1), for the drag log.</summary>
public double LitAmount(int index) => index >= 0 && index < _hover.Length ? _hover[index].Value : 0;

/// <summary>Edit mode: the slice being dragged is drawn faded.</summary>
public void SetDimmedIndex(int index)
{
Expand Down Expand Up @@ -485,28 +488,46 @@ public bool Step(double target, double dt, double stiffness = 320, double dampin
}
}

/// <summary>
/// A frame loop that got no frame for this long is treated as stopped: a frame request can be lost (the window
/// hidden in between, for one), and every later animation would wait for it, so the slice under a dragged file
/// would never light up.
/// </summary>
internal const long StalledLoopMs = 250;

private int _loop; // the current frame loop; frames of an older one are ignored
private long _lastTick; // Environment.TickCount64 of the last frame or frame request

private void StartAnimation()
{
InvalidateVisual();
if (_animating)
if (_animating && Environment.TickCount64 - _lastTick < StalledLoopMs)
return;
var top = TopLevel.GetTopLevel(this);
if (top is null)
{
_animating = false;
return;
}
_animating = true;
_lastFrame = null;
top.RequestAnimationFrame(OnFrame);
_lastTick = Environment.TickCount64;
var loop = ++_loop;
top.RequestAnimationFrame(time => OnFrame(time, loop));
}

private void OnFrame(TimeSpan time)
private void OnFrame(TimeSpan time, int loop)
{
if (loop != _loop)
return; // a loop that was given up on as stalled, replaced by a newer one
_lastTick = Environment.TickCount64;
var dt = _lastFrame is { } last ? Math.Clamp((time - last).TotalSeconds, 0, 0.05) : 1 / 60.0;
_lastFrame = time;
var active = Advance(dt);

InvalidateVisual();
if (active && TopLevel.GetTopLevel(this) is { } top)
top.RequestAnimationFrame(OnFrame);
top.RequestAnimationFrame(next => OnFrame(next, loop));
else
_animating = false;
}
Expand Down
5 changes: 5 additions & 0 deletions src/Filee.App/Services/RadialController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ private RadialWindow EnsureWindow()
else
Close();
};
// One line per drag, to tell "the drag never reached the donut" from "it did but nothing lit up" when someone
// reports a donut that doesn't react.
window.DragSummary += (events, hit, lit) =>
log.LogInformation("Drag over the donut: {Events} move event(s), ended on {Kind} {Index}, drawn lit {Lit:0.00}",
events, hit.Kind, hit.Index, lit);
window.EscapePressed += () => Close();
window.Deactivated += (_, _) =>
{
Expand Down
14 changes: 14 additions & 0 deletions src/Filee.App/Views/RadialWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ public RadialWindow()

public event Action? DragLeft;

/// <summary>
/// After a drag ends (drop or leave): how many drag-over events came in, where the last one was and how far that
/// slice was drawn lit up (0 to 1).
/// </summary>
public event Action<int, DonutHit, double>? DragSummary;

private int _dragOverEvents;
private DonutHit _lastDragHit;

private void OnDragEnter(object? sender, DragEventArgs e)
{
var files = LocalFiles(e);
Expand All @@ -39,13 +48,16 @@ private void OnDragEnter(object? sender, DragEventArgs e)
e.DragEffects = DragDropEffects.None;
return;
}
_dragOverEvents = 0;
FilesEntered?.Invoke(files);
OnDragOver(sender, e);
}

private void OnDragOver(object? sender, DragEventArgs e)
{
var hit = Donut.HitTest(e.GetPosition(Donut));
_dragOverEvents++;
_lastDragHit = hit;
var files = e.DataTransfer.Contains(DataFormat.File);
Donut.SetExternalHighlight(hit.Kind == DonutHitKind.Segment ? hit.Index : -1, hit.Kind == DonutHitKind.Center);

Expand All @@ -56,13 +68,15 @@ private void OnDragOver(object? sender, DragEventArgs e)

private void OnDragLeave(object? sender, DragEventArgs e)
{
DragSummary?.Invoke(_dragOverEvents, _lastDragHit, Donut.LitAmount(_lastDragHit.Index));
Donut.SetExternalHighlight(-1);
DragLeft?.Invoke();
}

private void OnDrop(object? sender, DragEventArgs e)
{
var hit = Donut.HitTest(e.GetPosition(Donut));
DragSummary?.Invoke(_dragOverEvents, hit, Donut.LitAmount(hit.Index));
Donut.SetExternalHighlight(-1);
e.DragEffects = hit.Kind == DonutHitKind.Segment ? DragDropEffects.Copy : DragDropEffects.None;
Dropped?.Invoke(hit, LocalFiles(e));
Expand Down
43 changes: 43 additions & 0 deletions tests/Filee.App.Tests/DonutHighlightTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// The slice under a dragged file lights up even after a frame of the donut's animation loop got lost.

using System.Reflection;
using Avalonia.Controls;
using Avalonia.Headless;
using Avalonia.Headless.XUnit;
using Avalonia.Threading;
using Filee.App.Controls;

namespace Filee.App.Tests;

public class DonutHighlightTests
{
[AvaloniaFact]
public void A_lost_animation_frame_does_not_keep_the_slice_dark()
{
TestServices.EnsureInitialized("ko");
var donut = new DonutMenu
{
Items = new[] { "PNG", "JPG", "WEBP", "PDF" }.Select(l => new DonutItem { Id = l, Label = l }).ToList(),
};
var window = new Window { Content = donut, SizeToContent = SizeToContent.WidthAndHeight };
window.Show();
Dispatcher.UIThread.RunJobs();

// As after a frame request that never came back: the loop still counts as running, its last frame long ago.
Set(donut, "_animating", true);
Set(donut, "_lastTick", Environment.TickCount64 - DonutMenu.StalledLoopMs * 4);

donut.SetExternalHighlight(2);
for (var i = 0; i < 90 && donut.LitAmount(2) < 0.9; i++)
{
AvaloniaHeadlessPlatform.ForceRenderTimerTick();
Dispatcher.UIThread.RunJobs();
}

Assert.True(donut.LitAmount(2) > 0.9, $"lit {donut.LitAmount(2):0.00}");
window.Close();
}

private static void Set(DonutMenu donut, string field, object value) =>
typeof(DonutMenu).GetField(field, BindingFlags.Instance | BindingFlags.NonPublic)!.SetValue(donut, value);
}
Loading