Public Access
Desktop: decide a titlebar double-click on the press, before the drag can swallow it
This commit is contained in:
1 parent
079a273d46
commit
a2362d1e3e
6 files changed
+127
-3
No files matched your search
@@ -0,0 +1,54 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
|
||||
namespace DodoSSH.Client.App.Views;
|
||||
|
||||
/// <summary>
|
||||
/// Tells a drawn titlebar's double-click from a press that starts a drag.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Both titlebars used to answer a double-click through <c>DoubleTapped</c> and start a drag on every press.
|
||||
/// The two do not mix: <c>BeginMoveDrag</c> hands the press to the operating system's own move loop, which
|
||||
/// eats the release — and a double tap is a gesture Avalonia assembles from presses <em>and</em> releases,
|
||||
/// so the second click often never completed one and the window did not maximise. The convention it was
|
||||
/// imitating is one nobody thinks about until it does not work.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// So the press decides, before the drag begins: a second press inside the platform's double-click time and
|
||||
/// distance is the maximise, and does not start a drag at all. The click count is honoured where the platform
|
||||
/// reports one, and the time and distance are measured here for where the move loop kept it from counting.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal sealed class CaptionPresses
|
||||
{
|
||||
private ulong lastAt;
|
||||
|
||||
private Point lastWhere;
|
||||
|
||||
private bool waitingForASecond;
|
||||
|
||||
/// <summary>Whether this press is the second of a double-click on <paramref name="bar"/>.</summary>
|
||||
internal bool IsSecond(PointerPressedEventArgs e, Visual bar)
|
||||
{
|
||||
var settings = Application.Current?.PlatformSettings;
|
||||
var time = settings?.GetDoubleTapTime(e.Pointer.Type) ?? TimeSpan.FromMilliseconds(500);
|
||||
var size = settings?.GetDoubleTapSize(e.Pointer.Type) ?? new Size(4, 4);
|
||||
var where = e.GetPosition(bar);
|
||||
|
||||
var second = e.ClickCount == 2
|
||||
|| (waitingForASecond
|
||||
&& e.Timestamp - lastAt <= (ulong)time.TotalMilliseconds
|
||||
&& Math.Abs(where.X - lastWhere.X) <= size.Width
|
||||
&& Math.Abs(where.Y - lastWhere.Y) <= size.Height);
|
||||
|
||||
// A double-click spends both presses: a third that follows quickly starts again rather than toggling
|
||||
// the window straight back.
|
||||
waitingForASecond = !second;
|
||||
lastAt = e.Timestamp;
|
||||
lastWhere = where;
|
||||
|
||||
return second;
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@
|
||||
-->
|
||||
|
||||
<Border Height="53" Background="{StaticResource DeepChrome}"
|
||||
PointerPressed="OnDrag" DoubleTapped="OnToggleMaximised">
|
||||
PointerPressed="OnDrag">
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="26,0,20,0">
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ internal sealed partial class SettingsTitleBar : UserControl
|
||||
{
|
||||
public SettingsTitleBar() => InitializeComponent();
|
||||
|
||||
private readonly CaptionPresses presses = new();
|
||||
|
||||
private Window? Host => TopLevel.GetTopLevel(this) as Window;
|
||||
|
||||
/// <summary>Left button only, and only on a press nothing inside the bar has already handled.</summary>
|
||||
@@ -19,6 +21,15 @@ internal sealed partial class SettingsTitleBar : UserControl
|
||||
return;
|
||||
}
|
||||
|
||||
// Decided on the press, before the drag would take it: see CaptionPresses.
|
||||
if (presses.IsSecond(e, this))
|
||||
{
|
||||
OnToggleMaximised(sender, e);
|
||||
e.Handled = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Host?.BeginMoveDrag(e);
|
||||
}
|
||||
|
||||
@@ -31,6 +42,7 @@ internal sealed partial class SettingsTitleBar : UserControl
|
||||
}
|
||||
|
||||
/// <summary>Both the button and a double-click on the bar arrive here, as Windows convention expects.</summary>
|
||||
/// <remarks>The double-click by way of <see cref="OnDrag"/>; see <see cref="CaptionPresses"/>.</remarks>
|
||||
private void OnToggleMaximised(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Host is not { } window)
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
-->
|
||||
|
||||
<Border Height="53" Background="{StaticResource DeepChrome}"
|
||||
PointerPressed="OnDrag" DoubleTapped="OnToggleMaximised">
|
||||
PointerPressed="OnDrag">
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="24,0,20,0">
|
||||
|
||||
|
||||
@@ -41,6 +41,8 @@ internal sealed partial class TitleBar : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
private readonly CaptionPresses presses = new();
|
||||
|
||||
private Window? Host => TopLevel.GetTopLevel(this) as Window;
|
||||
|
||||
/// <remarks>
|
||||
@@ -55,6 +57,15 @@ internal sealed partial class TitleBar : UserControl
|
||||
return;
|
||||
}
|
||||
|
||||
// Decided on the press, before the drag would take it: see CaptionPresses.
|
||||
if (presses.IsSecond(e, this))
|
||||
{
|
||||
OnToggleMaximised(sender, e);
|
||||
e.Handled = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Host?.BeginMoveDrag(e);
|
||||
}
|
||||
|
||||
@@ -68,7 +79,8 @@ internal sealed partial class TitleBar : UserControl
|
||||
|
||||
/// <remarks>
|
||||
/// Both the button and a double-click on the bar arrive here, which is the convention Windows sets and
|
||||
/// the one people reach for without thinking about it.
|
||||
/// the one people reach for without thinking about it. The double-click comes from <see cref="OnDrag"/>
|
||||
/// rather than <c>DoubleTapped</c>; see <see cref="CaptionPresses"/>.
|
||||
/// </remarks>
|
||||
private void OnToggleMaximised(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
|
||||
@@ -66,6 +66,52 @@ public sealed class TitleBarTests : IAsyncLifetime
|
||||
caches.Dispose();
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// A double-click on the bar maximises, and a second one restores. It was a <c>DoubleTapped</c> handler
|
||||
/// beside a press that started a drag, and the drag handed the press to the operating system's move loop,
|
||||
/// which ate the release a double tap is assembled from — so on Windows the click often did nothing. The
|
||||
/// press decides now; see <c>CaptionPresses</c>. A single press must still be a drag and nothing else.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task DoubleClickingTheBar_MaximisesAndRestores_AndASinglePressDoesNot()
|
||||
{
|
||||
await LayoutHarness.OnTheUiThreadAsync(
|
||||
() =>
|
||||
{
|
||||
var bar = new TitleBar { DataContext = shell };
|
||||
var window = LayoutHarness.HostAtMinimumSize(
|
||||
bar, LayoutHarness.MinimumWidth, LayoutHarness.TitleBarHeight);
|
||||
|
||||
try
|
||||
{
|
||||
// The top edge, past everything the bar draws: the drag surface and nothing else.
|
||||
var empty = new Point(bar.Bounds.Width * 0.6, 2);
|
||||
|
||||
window.MouseDown(empty, MouseButton.Left);
|
||||
window.MouseUp(empty, MouseButton.Left);
|
||||
|
||||
window.WindowState.ShouldBe(WindowState.Normal, "one press is a drag");
|
||||
|
||||
window.MouseDown(empty, MouseButton.Left);
|
||||
window.MouseUp(empty, MouseButton.Left);
|
||||
|
||||
window.WindowState.ShouldBe(WindowState.Maximized);
|
||||
|
||||
window.MouseDown(empty, MouseButton.Left);
|
||||
window.MouseUp(empty, MouseButton.Left);
|
||||
window.MouseDown(empty, MouseButton.Left);
|
||||
window.MouseUp(empty, MouseButton.Left);
|
||||
|
||||
window.WindowState.ShouldBe(WindowState.Normal);
|
||||
}
|
||||
finally
|
||||
{
|
||||
window.Close();
|
||||
}
|
||||
},
|
||||
Token);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClickingTheSearchPill_OpensTheQuickConnectPalette()
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user