using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using DodoSSH.Client.Shell.ViewModels; namespace DodoSSH.Client.App.Views; /// The tab strip, above every screen. internal sealed partial class TerminalTabs : UserControl { public TerminalTabs() => InitializeComponent(); /// /// Closes a tab on a middle click. /// /// /// /// Wired on the tab's own template root, which is the whole answer to "and not on the strip itself". /// A middle press on the background, on the sentence, or on the button that opens a connection reaches /// no handler at all, because there is none there to reach. Nothing has to test what was clicked. /// /// /// PointerUpdateKind, not IsMiddleButtonPressed. The latter reports button /// state: it is equally true for a left press made while the middle button happens to be held, /// and for every press during a middle drag. The question here is which button caused this press, and /// that is the one thing only PointerUpdateKind answers. /// /// /// On press rather than on release, which is what every browser and every terminal does. Matching a /// release to its press would need capture tracking, to buy the ability to change your mind about a /// middle click — a gesture nobody makes by accident and nobody aborts. /// /// private void OnTabPointerPressed(object? sender, PointerPressedEventArgs e) { if (sender is not Visual { DataContext: TerminalTabViewModel tab } || DataContext is not MainWindowViewModel shell) { return; } if (e.GetCurrentPoint((Visual)sender).Properties.PointerUpdateKind is not PointerUpdateKind.MiddleButtonPressed) { return; } // Handled, so the strip's ScrollViewer does not also take this as the start of a pan. e.Handled = true; // Fire-and-forget, as the host sidebar's double-tap connect is: CloseTabCommand is asynchronous — // it waits for the workspace to tear the session down — and an event handler has nowhere to await // it. Its failures are the workspace's to report, not this strip's. shell.CloseTabCommand.Execute(tab); } /// /// Opens the vault menu, on the Vaults tab. /// /// /// /// The tab is selected before the menu opens, and that order is the whole reason this is a handler /// rather than Button.Flyout. Selecting it puts the shell on a page, which collapses the /// renderer — so the popup never has to drop over the WebView's native child window, and the question /// this strip's comment refuses to answer without a screenshot does not come up. See the comment on the /// caret in the markup, and docs/platform-flags.md for what treating such a question as settled /// has already cost this project. /// /// /// It is also what a user expects. The caret belongs to the Vaults tab, so pressing it arriving at /// Vaults is the same gesture as pressing the tab, with a menu on the end. /// /// private void OnVaultMenuPressed(object? sender, RoutedEventArgs e) { if (DataContext is not MainWindowViewModel shell || sender is not Control caret) { return; } shell.ShowVaultsCommand.Execute(null); FlyoutBase.ShowAttachedFlyout(caret); } /// Leaves for the teams screen with the new-vault form open, shutting the menu behind it. /// /// The menu is closed first, because the command navigates and a flyout left open would be hanging over /// a screen it has nothing to do with. A Flyout does not close when something inside it is /// pressed — which is what the switches above it want, and not what this wants. /// private void OnNewVaultPressed(object? sender, RoutedEventArgs e) { if (DataContext is not MainWindowViewModel shell) { return; } if (this.FindControl