Public Access
The key bar above the software keyboard is now the user's own: which keys, in what order, up to sixteen in two rows that never scroll. Ctrl and Alt latch across the bar and the phone's keyboard, and Ctrl or Alt with a cursor key sends the modified CSI form. Seven predefined groups (essentials, cursor, control, symbols, tmux, vim, function) and keys of one's own, written in \e / ^C notation, are arranged on a customise sheet that saves as it goes. A grid button swaps the software keyboard for a quick-access keyboard of the same height, with KEYS, SNIPPETS, HISTORY and APPEARANCE tabs; it reopens to the tab last used. Snippets go through the shell's existing InsertSnippet. History keeps a typed command only once the remote has echoed it, so nothing typed at a password prompt is ever recorded; it is memory-only and LOCK clears it. Each session pump now keeps its last kilobyte of output and the alternate-screen state for that test (OutputTail). Long-press and drag on the terminal sends arrow keys, as a joystick with three speeds by distance; scrolling is unchanged and there is no mode switch. It takes the touch before Chromium's own long press, so a held finger no longer pastes; paste is on the bar by default instead. A switch in APPEARANCE turns the gesture off. The text-size buttons moved from the bar to APPEARANCE. On wide screens the same tabs open as a side panel beside the terminal, toggled from the top bar, with the bar's own keys repeated at the top of KEYS while the bar stands down. The model, gesture arithmetic, echo reading and history are in DodoSSH.Client.Shell and covered by TerminalKeyboardTests and OutputTailTests. Builds clean; not yet exercised on a device, because the emulator's install is locked behind a passphrase that was not recorded.
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" 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" 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>
|