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
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using OutWit.Database.Studio.Tests.Helpers;

namespace OutWit.Database.Studio.Tests.ViewModels;

/// <summary>
/// The command palette can be used and left with the mouse.
/// </summary>
/// <remarks>
/// <para>
/// Finding 18, found while automating the screenshots - where it stopped the run until a key could be
/// pressed by hand. Once open, the palette ignored the pointer for everything except moving the
/// highlight: clicking outside did not close it, clicking the header button again did not close it, a
/// single click on an entry moved the highlight and nothing else, and a double click did the same.
/// The only way out was <c>Esc</c>, and the only way in was <c>Enter</c>.
/// </para>
/// <para>
/// <b>A palette is a keyboard tool and nobody minds reaching for Esc</b> - what is wrong is a window
/// that traps the pointer with no visible way out. So the keyboard keeps everything it had, and the
/// mouse gets the two things it is entitled to: a click on an entry runs it, and a click outside
/// closes the window.
/// </para>
/// </remarks>
[TestFixture]
public class ThePaletteTakesTheMouseTests
{
#region What the window wires

[Test]
public void AClickOutsideClosesItTest()
{
var markup = Markup("Views/MainWindow.axaml");
var code = Markup("Views/MainWindow.axaml.cs");

Assert.Multiple(() =>
{
Assert.That(markup, Does.Contain("PointerPressed=\"OnPaletteScrimPressed\""),
"the scrim behind the palette answers the pointer");

Assert.That(code, Does.Contain("OnPaletteScrimPressed"),
"and the handler is there to answer with");

Assert.That(code, Does.Contain("PaletteVm.CloseCommand"),
"what it does is close the palette");
});
}

[Test]
public void AClickOnAnEntryRunsItTest()
{
var markup = Markup("Views/MainWindow.axaml");
var code = Markup("Views/MainWindow.axaml.cs");

Assert.Multiple(() =>
{
Assert.That(markup, Does.Contain("Tapped=\"OnPaletteItemTapped\""),
"the list answers a click on one of its entries");

Assert.That(code, Does.Contain("OnPaletteItemTapped"));

Assert.That(code, Does.Contain("AcceptCommand"),
"and a click runs the entry, which is what the palette is for");
});
}

#endregion

#region What the palette does

[Test]
public async Task ClosingItLeavesNothingBehindTest()
{
await using var studio = await StudioFixture.CreateAsync();

var palette = studio.App.PaletteVm;

palette.OpenCommand.Execute(null);

Assume.That(palette.IsOpen, Is.True);

palette.CloseCommand.Execute(null);

Assert.That(palette.IsOpen, Is.False,
"the window a click outside closes is the same window Esc closes");
}

#endregion

#region Tools

private static string Markup(string relative)
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);

while (directory != null)
{
var candidate = Path.Combine(directory.FullName, "Tools", "OutWit.Database.Studio");

if (Directory.Exists(Path.Combine(candidate, "Views")))
{
var path = Path.Combine(candidate, relative.Replace('/', Path.DirectorySeparatorChar));

Assert.That(File.Exists(path), Is.True, $"{relative} must be where this fixture says");

return File.ReadAllText(path);
}

directory = directory.Parent;
}

throw new AssertionException("the Studio project was not found from " + AppContext.BaseDirectory);
}

#endregion
}
10 changes: 9 additions & 1 deletion Tools/OutWit.Database.Studio/Views/MainWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -837,9 +837,14 @@
</Grid>
</Border>

<!-- The command palette (WS-9), over everything. -->
<!-- The command palette (WS-9), over everything.

The scrim answers the pointer: a window that traps it with no visible way out is a
corner somebody can get stuck in, and this one stopped the screenshot automation
until a key could be pressed by hand. Esc keeps working; it is not the only way. -->
<Border Grid.Row="0" Grid.RowSpan="4"
IsVisible="{Binding PaletteVm.IsOpen}"
PointerPressed="OnPaletteScrimPressed"
Background="{DynamicResource Wit.Scrim}">
<Border Width="560"
VerticalAlignment="Top"
Expand All @@ -857,7 +862,10 @@
Watermark="{DynamicResource S.Shell.Palette}"
BorderThickness="0"/>

<!-- A click on an entry RUNS it. It used to move the highlight and stop there, so the
only way to open anything was Enter. -->
<ListBox AutomationProperties.AutomationId="PaletteItems"
Tapped="OnPaletteItemTapped"
ItemsSource="{Binding PaletteVm.Items}"
SelectedItem="{Binding PaletteVm.SelectedItem, Mode=TwoWay}"
MaxHeight="320"
Expand Down
36 changes: 36 additions & 0 deletions Tools/OutWit.Database.Studio/Views/MainWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,42 @@ private void OnFindClicked(object? sender, Avalonia.Interactivity.RoutedEventArg
{
FocusSearchTerm();
}
/// <summary>
/// A press on the scrim behind the palette closes it - unless it landed on the palette.
/// </summary>
/// <remarks>
/// <b>The palette took the keyboard and nothing else.</b> Clicking outside did not close it,
/// clicking the header button that opened it did not close it, and a click on an entry moved
/// the highlight and stopped there. Esc and Enter were the only ways in and out, which is fine
/// for a keyboard tool and not fine for a window that has trapped the pointer.
/// </remarks>
private void OnPaletteScrimPressed(object? sender, Avalonia.Input.PointerPressedEventArgs e)
{
if (DataContext is not ApplicationViewModel app)
return;

// Only a press on the scrim ITSELF: one that landed inside the panel is a press on a box,
// a list or an entry, and closing on those would take the window away mid-use.
if (!ReferenceEquals(e.Source, sender))
return;

app.PaletteVm.CloseCommand.Execute(null);

e.Handled = true;
}

/// <summary>
/// A click on an entry runs it, the way Enter does.
/// </summary>
private void OnPaletteItemTapped(object? sender, Avalonia.Input.TappedEventArgs e)
{
if (DataContext is not ApplicationViewModel app || app.PaletteVm.SelectedItem == null)
return;

app.PaletteVm.AcceptCommand.Execute(null);

e.Handled = true;
}
private void FocusSearchTerm()
{
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
Expand Down
Loading