diff --git a/Tools/OutWit.Database.Studio.Tests/ViewModels/ThePaletteTakesTheMouseTests.cs b/Tools/OutWit.Database.Studio.Tests/ViewModels/ThePaletteTakesTheMouseTests.cs new file mode 100644 index 0000000..2ce30d2 --- /dev/null +++ b/Tools/OutWit.Database.Studio.Tests/ViewModels/ThePaletteTakesTheMouseTests.cs @@ -0,0 +1,114 @@ +using OutWit.Database.Studio.Tests.Helpers; + +namespace OutWit.Database.Studio.Tests.ViewModels; + +/// +/// The command palette can be used and left with the mouse. +/// +/// +/// +/// 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 Esc, and the only way in was Enter. +/// +/// +/// A palette is a keyboard tool and nobody minds reaching for Esc - 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. +/// +/// +[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 +} diff --git a/Tools/OutWit.Database.Studio/Views/MainWindow.axaml b/Tools/OutWit.Database.Studio/Views/MainWindow.axaml index a211812..c6a4375 100644 --- a/Tools/OutWit.Database.Studio/Views/MainWindow.axaml +++ b/Tools/OutWit.Database.Studio/Views/MainWindow.axaml @@ -837,9 +837,14 @@ - + + + /// A press on the scrim behind the palette closes it - unless it landed on the palette. + /// + /// + /// The palette took the keyboard and nothing else. 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. + /// + 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; + } + + /// + /// A click on an entry runs it, the way Enter does. + /// + 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(() =>