From 5ef383844aac21cdef65a0f4fd68f61503a9290f Mon Sep 17 00:00:00 2001 From: Dmitry Ratner <6830384+dmitrat@users.noreply.github.com> Date: Wed, 19 Aug 2026 10:31:23 +0300 Subject: [PATCH] A window that traps the pointer with no visible way out Phase 8 of the fix plan, and the last of the code phases. Studio 1003 green, 1000 before. The command palette answered the keyboard and nothing else. Clicking outside did not close it, clicking the button that opened it did not close it, a single click on an entry moved the highlight and stopped there, and a double click did the same. Esc and Enter were the only way in and the only way out - which is fine for a keyboard tool and not fine for a window that has taken the pointer. A press on the scrim closes it now, unless the press landed on the palette itself: a press inside is a press on a box, a list or an entry, and closing on those would take the window away mid-use. A click on an entry runs it, the way Enter does. Neither gesture replaces a key; they stop being the only way. It was found while automating the screenshots, where it stopped the run until a key could be pressed by hand. That is the clearest statement of what was wrong with it. The plan also asked to check finding 17 here - the palette listed a multi-column index twice. It reads the tree, which reads GetIndexesAsync, which phase 1 collapsed; closed there. Phases 1 through 8 are now all in main. What is left of the plan is phase 0 - the probes that need a running Studio - and the release. Co-Authored-By: Claude Opus 5 (1M context) --- .../ThePaletteTakesTheMouseTests.cs | 114 ++++++++++++++++++ .../Views/MainWindow.axaml | 10 +- .../Views/MainWindow.axaml.cs | 36 ++++++ 3 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 Tools/OutWit.Database.Studio.Tests/ViewModels/ThePaletteTakesTheMouseTests.cs 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(() =>