Public Access
The bottom bar is Vaults · Connections · Settings, drawn as Material 3 pills with icons from the embedded Material font and a count of open sessions on Connections, which replaces the strip of shells above the bar. Vaults is the open vault: its header carries the name as a switch to the vaults screen, the sync light and a lock icon, and a Hosts · Keychain · Snippets segmented control sits under it. The keychain and snippets move there from Settings (IsVaultSurface lights the tab; IsMoreSurface no longer lists them), and back from either goes to the hosts. Settings becomes an account card and grouped sections: security, vaults and history, files, this app. Host rows are cards with a terminal tile and the live dot on its corner. The connect sheet names the host and its address, labels its box, focuses it as it opens, reveals the password on request, and puts CANCEL left and CONNECT right; every two-button row on the head now confirms on the right. While a hosts-screen sheet is up the header and bar are dimmed too and a tap on them lowers it, and the bar stands down while the software keyboard is up. The keychain gains a floating + and a sheet of the desktop's five adds (generate a key, paste a key, a password, a bucket, a tag) over the editors VaultViewModel already had, drawn as a full-screen page with labels above the boxes; a chosen item gets EDIT and COPY PUBLIC KEY beside MOVE and DELETE. Snippets get a floating + and cards that say RUNS or INSERTS. Files trade the seven-button block for a selection bar (save, and a ⋮ sheet with rename, move, permissions and a red delete), folder and file tiles, an upload button, and header actions for a terminal on the same host, refresh and close; a terminal's + sheet opens SFTP on that terminal's own host. The theme takes the audit's visual findings: Fluent's ClipToBounds is cleared on glowing buttons so the glow is no longer cut to a square (the desktop's Button.accent too), prose is Montserrat with mono kept for addresses and code, disabled secondary buttons are quieter than enabled ones, Fluent's dropdown, checkbox, switch, focus and selection brushes come from the palette, touch no longer leaves hover fills behind, and lists, nav entries and icon buttons carry names for TalkBack. Builds clean; checked on the emulator end to end: unlock, the three vault views, adding and deleting a password, connecting through the new sheet, files on the terminal's host with the selection bar and its sheet. App tests 727 of 733 pass with 5 skipped; the one failure, TheCountsAPhoneShows_ FollowEveryForwardsState, passes three runs of three alone and is untouched here. Layout tests pass.
326 lines
18 KiB
XML
326 lines
18 KiB
XML
<UserControl xmlns="https://github.com/avaloniaui"
|
||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
|
||
xmlns:kb="using:DodoSSH.Client.Shell.Keyboard"
|
||
xmlns:views="using:DodoSSH.Client.Android.Views"
|
||
x:Class="DodoSSH.Client.Android.Views.QuickAccessKeyboard"
|
||
x:DataType="vm:MainWindowViewModel"
|
||
Background="{StaticResource DeepChrome}">
|
||
|
||
<!--
|
||
The quick-access keyboard: every key, snippet and command from history, and the terminal's appearance,
|
||
one tap from the prompt.
|
||
|
||
One control in two places. On a phone it takes the software keyboard's place under the terminal, at the
|
||
keyboard's own height so the bar above it does not move; on a wide screen it is a panel beside the
|
||
terminal that stays up while the software keyboard comes and goes. Same tabs, same rows, same view model
|
||
either way — see TerminalScreen for which one is drawn when.
|
||
|
||
It reopens to the tab last used, which is a setting rather than a field on this control: the tab is the
|
||
one somebody reaches for on this device, and the next launch should know it too. See
|
||
TerminalKeyboardViewModel.SelectedTab.
|
||
-->
|
||
|
||
<UserControl.Styles>
|
||
|
||
<!--
|
||
◆ NOTHING HERE TAKES FOCUS, FOR THE REASON THE BAR'S KEYS DO NOT.
|
||
|
||
This panel is an extension of the keyboard, not a place for the keyboard to go. On a wide screen the
|
||
software keyboard can be up at the same time, and an ordinary button would take Avalonia's focus off
|
||
TerminalInputSurface on tap — tearing down the input connection and dropping the keyboard every time a
|
||
key in this panel was used. On a phone the keyboard is down while this is up, but the focus it would take
|
||
is the focus that brings the keyboard straight back when the terminal is tapped. One rule for the whole
|
||
control, so a button added to it later cannot forget.
|
||
-->
|
||
<Style Selector="Button">
|
||
<Setter Property="Focusable" Value="False" />
|
||
</Style>
|
||
|
||
<!-- The tab strip's buttons: an icon square, lit when it is the tab showing. -->
|
||
<Style Selector="Button.qtab">
|
||
<Setter Property="Width" Value="48" />
|
||
<Setter Property="Height" Value="40" />
|
||
<Setter Property="Padding" Value="0" />
|
||
<Setter Property="CornerRadius" Value="10" />
|
||
<Setter Property="Background" Value="Transparent" />
|
||
<Setter Property="BorderThickness" Value="0" />
|
||
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
||
<Setter Property="FontFamily" Value="{StaticResource IconFont}" />
|
||
<Setter Property="FontSize" Value="20" />
|
||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||
</Style>
|
||
<Style Selector="Button.qtab:pressed /template/ ContentPresenter">
|
||
<Setter Property="Background" Value="{StaticResource Hover}" />
|
||
</Style>
|
||
<Style Selector="Button.qtab.selected">
|
||
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
|
||
</Style>
|
||
<Style Selector="Button.qtab.selected /template/ ContentPresenter">
|
||
<Setter Property="Background" Value="{StaticResource AccentSoft}" />
|
||
</Style>
|
||
|
||
<!-- A group's caption over its keys. -->
|
||
<Style Selector="TextBlock.groupcaption">
|
||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
||
<Setter Property="FontSize" Value="9.5" />
|
||
<Setter Property="FontWeight" Value="SemiBold" />
|
||
<Setter Property="Margin" Value="4,10,0,4" />
|
||
</Style>
|
||
|
||
<!-- Keys in a panel are a little shorter than the bar's: there are many more of them, all reachable. -->
|
||
<Style Selector="Button.hotkey.panelkey">
|
||
<Setter Property="Height" Value="38" />
|
||
<Setter Property="MinHeight" Value="38" />
|
||
<Setter Property="Margin" Value="3" />
|
||
</Style>
|
||
|
||
</UserControl.Styles>
|
||
|
||
<Grid RowDefinitions="Auto,*">
|
||
|
||
<!-- ============ the tabs ============ -->
|
||
<!--
|
||
Icons rather than words, as the keys tab's own grid is: four words at 360dp leave no room for the
|
||
keyboard button at the end, and each glyph is the one every phone uses for the thing — a grid of keys,
|
||
braces for snippets, a clock for history, a palette for appearance. Each carries its name for
|
||
accessibility, which is where a word belongs when there is no room to print one.
|
||
-->
|
||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="8,6,8,2">
|
||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="4">
|
||
<Button Classes="qtab" Classes.selected="{Binding Keyboard.IsKeysTab}" Content=""
|
||
AutomationProperties.Name="Keys"
|
||
Command="{Binding Keyboard.SelectTabCommand}"
|
||
CommandParameter="{x:Static vm:QuickAccessTab.Keys}" />
|
||
<Button Classes="qtab" Classes.selected="{Binding Keyboard.IsSnippetsTab}" Content=""
|
||
AutomationProperties.Name="Snippets"
|
||
Command="{Binding Keyboard.SelectTabCommand}"
|
||
CommandParameter="{x:Static vm:QuickAccessTab.Snippets}" />
|
||
<Button Classes="qtab" Classes.selected="{Binding Keyboard.IsHistoryTab}" Content=""
|
||
AutomationProperties.Name="History"
|
||
Command="{Binding Keyboard.SelectTabCommand}"
|
||
CommandParameter="{x:Static vm:QuickAccessTab.History}" />
|
||
<Button Classes="qtab" Classes.selected="{Binding Keyboard.IsAppearanceTab}" Content=""
|
||
AutomationProperties.Name="Appearance"
|
||
Command="{Binding Keyboard.SelectTabCommand}"
|
||
CommandParameter="{x:Static vm:QuickAccessTab.Appearance}" />
|
||
</StackPanel>
|
||
|
||
<!--
|
||
Back to the phone's own keyboard. The one control here that is not a tab, so it sits apart behind a
|
||
hairline: it does not change what this panel shows, it puts the panel away.
|
||
-->
|
||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="4">
|
||
<Border Width="1" Height="22" Background="{StaticResource Border}" VerticalAlignment="Center" />
|
||
<Button Classes="qtab" Content="" AutomationProperties.Name="Show the keyboard"
|
||
Click="OnKeyboardRequested" />
|
||
</StackPanel>
|
||
</Grid>
|
||
|
||
<Panel Grid.Row="1">
|
||
|
||
<!-- ============ KEYS ============ -->
|
||
<ScrollViewer IsVisible="{Binding Keyboard.IsKeysTab}" HorizontalScrollBarVisibility="Disabled">
|
||
<StackPanel Margin="8,4,8,12">
|
||
|
||
<Button Classes="row" MinHeight="40" HorizontalContentAlignment="Center"
|
||
Command="{Binding Keyboard.OpenCustomizeCommand}">
|
||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||
<TextBlock FontFamily="{StaticResource IconFont}" FontSize="16" Text=""
|
||
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
|
||
<TextBlock Classes="label" FontSize="10" Text="CUSTOMIZE" VerticalAlignment="Center" />
|
||
</StackPanel>
|
||
</Button>
|
||
|
||
<!--
|
||
The bar's own keys, first, when the bar is not drawn — which is a wide screen with this panel
|
||
open beside the terminal. A key somebody put on their bar is one they want within reach, and
|
||
hiding the bar must not hide it.
|
||
-->
|
||
<StackPanel IsVisible="{Binding $parent[views:QuickAccessKeyboard].ShowsBarGroup}">
|
||
<TextBlock Classes="groupcaption" Text="YOUR BAR" />
|
||
<ItemsControl ItemsSource="{Binding Keyboard.BarRows}">
|
||
<ItemsControl.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:KeyRowViewModel">
|
||
<ItemsControl ItemsSource="{Binding Keys}">
|
||
<ItemsControl.ItemsPanel>
|
||
<ItemsPanelTemplate><UniformGrid Columns="4" /></ItemsPanelTemplate>
|
||
</ItemsControl.ItemsPanel>
|
||
<ItemsControl.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:KeyButtonViewModel">
|
||
<Button Classes="hotkey panelkey" Classes.latched="{Binding IsLatched}"
|
||
Content="{Binding Label}" AutomationProperties.Name="{Binding Hint}"
|
||
Command="{Binding PressCommand}" />
|
||
</DataTemplate>
|
||
</ItemsControl.ItemTemplate>
|
||
</ItemsControl>
|
||
</DataTemplate>
|
||
</ItemsControl.ItemTemplate>
|
||
</ItemsControl>
|
||
</StackPanel>
|
||
|
||
<ItemsControl ItemsSource="{Binding Keyboard.Groups}">
|
||
<ItemsControl.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:KeyGroupViewModel">
|
||
<StackPanel>
|
||
<TextBlock Classes="groupcaption" Text="{Binding Title}" />
|
||
<!--
|
||
Four across, which at 360dp is an 84dp key — wide enough for "shift+tab" and "ctrl+B %"
|
||
in the mono face without shrinking it, and still four keys a thumb can tell apart.
|
||
-->
|
||
<ItemsControl ItemsSource="{Binding Keys}">
|
||
<ItemsControl.ItemsPanel>
|
||
<ItemsPanelTemplate><UniformGrid Columns="4" /></ItemsPanelTemplate>
|
||
</ItemsControl.ItemsPanel>
|
||
<ItemsControl.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:KeyButtonViewModel">
|
||
<Button Classes="hotkey panelkey" Classes.latched="{Binding IsLatched}"
|
||
Content="{Binding Label}" AutomationProperties.Name="{Binding Hint}"
|
||
Command="{Binding PressCommand}" />
|
||
</DataTemplate>
|
||
</ItemsControl.ItemTemplate>
|
||
</ItemsControl>
|
||
</StackPanel>
|
||
</DataTemplate>
|
||
</ItemsControl.ItemTemplate>
|
||
</ItemsControl>
|
||
</StackPanel>
|
||
</ScrollViewer>
|
||
|
||
<!-- ============ SNIPPETS ============ -->
|
||
<!--
|
||
The keychain's snippets, through the shell's own InsertSnippet — the command the desktop sidebar's
|
||
rows run, which carries the snippets screen's whole safety story: typed rather than pasted for one
|
||
line, and no Enter unless the snippet was saved as one that runs. A second insert path here would be
|
||
a second place that story could go stale. The ▶ says which ones run.
|
||
-->
|
||
<ScrollViewer IsVisible="{Binding Keyboard.IsSnippetsTab}" HorizontalScrollBarVisibility="Disabled">
|
||
<StackPanel Margin="8,4,8,12" Spacing="2">
|
||
<ItemsControl ItemsSource="{Binding SnippetsScreen.Visible, FallbackValue={x:Null}}">
|
||
<ItemsControl.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:SnippetRowViewModel">
|
||
<Button Classes="row" MinHeight="48" Padding="12,6"
|
||
Command="{Binding $parent[views:QuickAccessKeyboard].((vm:MainWindowViewModel)DataContext).InsertSnippetCommand}"
|
||
CommandParameter="{Binding}">
|
||
<Grid ColumnDefinitions="*,Auto">
|
||
<StackPanel Grid.Column="0" Spacing="2" VerticalAlignment="Center">
|
||
<TextBlock Classes="mono" FontSize="12.5" FontWeight="SemiBold" Text="{Binding Label}"
|
||
TextTrimming="CharacterEllipsis" />
|
||
<TextBlock Classes="detail code" FontSize="10.5" Text="{Binding Preview}"
|
||
TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
|
||
</StackPanel>
|
||
<TextBlock Grid.Column="1" Classes="label" FontSize="9" Text="▶ RUNS" Margin="10,0,0,0"
|
||
Foreground="{StaticResource WarnText}" VerticalAlignment="Center"
|
||
IsVisible="{Binding RunsOnInsert}" />
|
||
</Grid>
|
||
</Button>
|
||
</DataTemplate>
|
||
</ItemsControl.ItemTemplate>
|
||
</ItemsControl>
|
||
|
||
<TextBlock Classes="body" Margin="4,8,4,0" TextWrapping="Wrap"
|
||
Text="Tap a snippet to type it at the prompt. Snippets live in the keychain, where they are added and edited." />
|
||
<Button Classes="row" MinHeight="40" HorizontalContentAlignment="Center" Margin="0,6,0,0"
|
||
Command="{Binding ShowScreenCommand}" CommandParameter="{x:Static vm:ShellScreen.Snippets}">
|
||
<TextBlock Classes="label" FontSize="9" Text="MANAGE SNIPPETS" />
|
||
</Button>
|
||
</StackPanel>
|
||
</ScrollViewer>
|
||
|
||
<!-- ============ HISTORY ============ -->
|
||
<!--
|
||
What was typed on this phone and echoed back by the remote — never a line typed at a password prompt,
|
||
which echoes nothing. Kept in memory only and emptied by LOCK; the sentence under the list says so,
|
||
because a list of commands is exactly the kind of thing somebody should know the lifetime of. See
|
||
TypedCommandHistory for the whole of what is and is not kept.
|
||
-->
|
||
<ScrollViewer IsVisible="{Binding Keyboard.IsHistoryTab}" HorizontalScrollBarVisibility="Disabled">
|
||
<StackPanel Margin="8,4,8,12" Spacing="2">
|
||
<ItemsControl ItemsSource="{Binding Keyboard.History}">
|
||
<ItemsControl.ItemTemplate>
|
||
<DataTemplate x:DataType="kb:HistoryEntry">
|
||
<Button Classes="row" MinHeight="44" Padding="12,6"
|
||
Command="{Binding $parent[views:QuickAccessKeyboard].((vm:MainWindowViewModel)DataContext).Keyboard.InsertHistoryCommand}"
|
||
CommandParameter="{Binding}">
|
||
<Grid ColumnDefinitions="*,Auto">
|
||
<TextBlock Grid.Column="0" Classes="mono" FontSize="12.5" Text="{Binding Command}"
|
||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
||
<TextBlock Grid.Column="1" Classes="detail code" FontSize="10" Text="{Binding Host}"
|
||
Margin="10,0,0,0" VerticalAlignment="Center" />
|
||
</Grid>
|
||
</Button>
|
||
</DataTemplate>
|
||
</ItemsControl.ItemTemplate>
|
||
</ItemsControl>
|
||
|
||
<TextBlock Classes="body" Margin="4,8,4,0" TextWrapping="Wrap"
|
||
IsVisible="{Binding !Keyboard.HasHistory}"
|
||
Text="Commands you type here appear in this list. Only what the remote echoes back is kept, so nothing typed at a password prompt ever is." />
|
||
<TextBlock Classes="body" Margin="4,8,4,0" TextWrapping="Wrap"
|
||
Text="Tap a command to type it at the prompt without running it. The list is kept in memory only, and locking the keychain clears it." />
|
||
<Button Classes="row" MinHeight="40" HorizontalContentAlignment="Center" Margin="0,6,0,0"
|
||
IsVisible="{Binding Keyboard.HasHistory}"
|
||
Command="{Binding Keyboard.ClearHistoryCommand}">
|
||
<TextBlock Classes="label" FontSize="9" Text="CLEAR HISTORY" />
|
||
</Button>
|
||
</StackPanel>
|
||
</ScrollViewer>
|
||
|
||
<!-- ============ APPEARANCE ============ -->
|
||
<!--
|
||
The text size, which used to ride at the end of the arrow row as A− and A+. A phone cannot press
|
||
Ctrl+plus, so these are still buttons; they moved here because the bar is now somebody's own and two
|
||
fixed cells at its end were cells they could not have. Disabled at the ends rather than clamping
|
||
silently, for the reason they always were: a button that accepts taps and does nothing reads as the
|
||
terminal having stopped responding.
|
||
|
||
And the cursor gesture's switch, for somebody whose long press means something else to them.
|
||
-->
|
||
<ScrollViewer IsVisible="{Binding Keyboard.IsAppearanceTab}" HorizontalScrollBarVisibility="Disabled">
|
||
<StackPanel Margin="12,6,12,12" Spacing="8">
|
||
|
||
<TextBlock Classes="groupcaption" Text="TEXT SIZE" Margin="0,4,0,0" />
|
||
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
|
||
<Button Grid.Column="0" Classes="hotkey" Width="56" Command="{Binding ShrinkTerminalFontCommand}"
|
||
IsEnabled="{Binding CanShrinkTerminalFont}">
|
||
<TextBlock Classes="mono" FontSize="13" Text="A−" />
|
||
</Button>
|
||
<TextBlock Grid.Column="1" Classes="mono" FontSize="14" FontWeight="SemiBold"
|
||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||
Text="{Binding TerminalFontSize, StringFormat='{}{0} px'}" />
|
||
<Button Grid.Column="2" Classes="hotkey" Width="56" Command="{Binding EnlargeTerminalFontCommand}"
|
||
IsEnabled="{Binding CanEnlargeTerminalFont}">
|
||
<TextBlock Classes="mono" FontSize="15" Text="A+" />
|
||
</Button>
|
||
<Button Grid.Column="3" Classes="row" MinHeight="40" Padding="12,0" Margin="8,0,0,0"
|
||
Command="{Binding ResetTerminalFontCommand}">
|
||
<TextBlock Classes="label" FontSize="9" Text="RESET" />
|
||
</Button>
|
||
</Grid>
|
||
|
||
<TextBlock Classes="groupcaption" Text="CURSOR GESTURE" Margin="0,10,0,0" />
|
||
<Button Classes="row" MinHeight="52" Padding="12,8" Command="{Binding Keyboard.ToggleCursorGesturesCommand}">
|
||
<Grid ColumnDefinitions="*,Auto">
|
||
<StackPanel Grid.Column="0" Spacing="2">
|
||
<TextBlock Classes="mono" FontSize="12.5" FontWeight="SemiBold" Text="Long-press and drag" />
|
||
<TextBlock Classes="detail" FontSize="10.5" TextWrapping="Wrap"
|
||
Text="Moves the cursor or walks through history. Drag further to go faster." />
|
||
</StackPanel>
|
||
<TextBlock Grid.Column="1" Classes="label" FontSize="9.5" Text="ON" Margin="10,0,0,0"
|
||
Foreground="{StaticResource Live}" VerticalAlignment="Center"
|
||
IsVisible="{Binding Keyboard.CursorGestures}" />
|
||
<TextBlock Grid.Column="1" Classes="label" FontSize="9.5" Text="OFF" Margin="10,0,0,0"
|
||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
|
||
IsVisible="{Binding !Keyboard.CursorGestures}" />
|
||
</Grid>
|
||
</Button>
|
||
|
||
</StackPanel>
|
||
</ScrollViewer>
|
||
|
||
</Panel>
|
||
</Grid>
|
||
</UserControl>
|