Public Access
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aff776cb42 | ||
|
|
91f7586b22 |
No files matched your search
@@ -26,7 +26,8 @@
|
|||||||
"post.logout.redirect.uris": "http://127.0.0.1/*"
|
"post.logout.redirect.uris": "http://127.0.0.1/*"
|
||||||
},
|
},
|
||||||
"redirectUris": [
|
"redirectUris": [
|
||||||
"http://127.0.0.1/callback"
|
"http://127.0.0.1/callback",
|
||||||
|
"dev.dodotech.dodossh:/callback"
|
||||||
],
|
],
|
||||||
"webOrigins": [],
|
"webOrigins": [],
|
||||||
"protocolMappers": [
|
"protocolMappers": [
|
||||||
|
|||||||
@@ -1810,6 +1810,27 @@ bottom and no scroll position left part way down.
|
|||||||
**Failure means:** the inset is being applied but not cleared — the closed state is not being read from the
|
**Failure means:** the inset is being applied but not cleared — the closed state is not being read from the
|
||||||
event, or the margin is only ever added to.
|
event, or the margin is only ever added to.
|
||||||
|
|
||||||
|
### 10.4a Nothing is stranded when the keyboard is still up on the way *out* · **the one 10.4 missed**
|
||||||
|
|
||||||
|
Focus any box, and with the keyboard still on screen leave the application — Home, or the task switcher,
|
||||||
|
or anything that puts another app in front. Come back to it.
|
||||||
|
|
||||||
|
**Pass:** the interface fills the screen. Do it from the terminal too, where the keypad and the shell should
|
||||||
|
both be where they were.
|
||||||
|
|
||||||
|
**Failure means:** `PhoneShell` is not re-reading the inset on the way back in. This is a separate path from
|
||||||
|
10.4 and it is the one that has actually shipped broken: Android takes the keyboard away when the activity
|
||||||
|
stops and Avalonia's input pane raises no `StateChanged` for it, because the pane goes with the window
|
||||||
|
rather than closing in front of one. So the last value applied was for a keyboard that was up, and the
|
||||||
|
application returns holding a whole keyboard's height of empty canvas — measured at 838 of 2340 pixels on a
|
||||||
|
Pixel 5, on every screen, and staying that way until something opens and closes the keyboard. See
|
||||||
|
`PhoneShell.OnActivityResumed` and `PhoneEnvironment.Resumed`.
|
||||||
|
|
||||||
|
Worth doing in both directions: a resume that *should* keep the inset is the other half. Trigger a
|
||||||
|
permission prompt or the biometric sheet with the keyboard up, dismiss it, and check the box being typed
|
||||||
|
into is still clear of a keyboard that is genuinely still there — a fix that flattened the inset on every
|
||||||
|
resume would pass the paragraph above and fail this one.
|
||||||
|
|
||||||
### 10.5 Rotating with the keyboard up
|
### 10.5 Rotating with the keyboard up
|
||||||
|
|
||||||
Focus a passphrase box, then turn the phone sideways.
|
Focus a passphrase box, then turn the phone sideways.
|
||||||
@@ -2060,6 +2081,25 @@ cause would look unrelated. See the correction in [ADR 0015](adr/0015-android-te
|
|||||||
Worth pressing Enter on an empty prompt a few times too: each one should give a fresh prompt and nothing
|
Worth pressing Enter on an empty prompt a few times too: each one should give a fresh prompt and nothing
|
||||||
else. A doubled prompt per press means a line ending is arriving as both CR and LF.
|
else. A doubled prompt per press means a line ending is arriving as both CR and LF.
|
||||||
|
|
||||||
|
### 11.10f Every key on the keypad is there, and pressing one presses it
|
||||||
|
|
||||||
|
With a shell open, look at the two rows above the keyboard without touching anything: **esc tab ctrl | - ~ /**
|
||||||
|
on the upper row and **← ↓ ↑ →** with **A−** and **A+** on the lower one. All thirteen, at once, on a 360dp
|
||||||
|
phone in portrait.
|
||||||
|
|
||||||
|
Then press them with a thumb rather than a fingertip, at an angle, twenty or so times across both rows —
|
||||||
|
including the keys at the far left and far right ends.
|
||||||
|
|
||||||
|
**Pass:** every press registers, the key darkens under the thumb while it is down, and **nothing slides
|
||||||
|
sideways**. The row is exactly where it was after twenty presses as it was before the first.
|
||||||
|
|
||||||
|
**Failure means:** a key that misses is a target that has got smaller — `Button.hotkey` is 40 tall for a
|
||||||
|
reason and Android asks for 48. A row that *moves* is the one that matters: it means something has put a
|
||||||
|
`ScrollViewer` back around the keys. That is how this row shipped originally and it does not survive contact
|
||||||
|
with a thumb — a pan and a press begin with the same few pixels of travel, the scroller claims them, and the
|
||||||
|
press is never delivered. If the set of keys outgrows the width, add a third row; do not make it scroll.
|
||||||
|
See the keypad's own remark in `TerminalScreen.axaml`.
|
||||||
|
|
||||||
### 11.11 Closing a connection and opening a new one both take you somewhere real
|
### 11.11 Closing a connection and opening a new one both take you somewhere real
|
||||||
|
|
||||||
Open a shell, close its tab, then open a different one from HOSTS.
|
Open a shell, close its tab, then open a different one from HOSTS.
|
||||||
|
|||||||
@@ -90,6 +90,11 @@ public sealed class MainActivity : AvaloniaMainActivity
|
|||||||
PhoneEnvironment.CurrentActivity = this;
|
PhoneEnvironment.CurrentActivity = this;
|
||||||
|
|
||||||
AndroidRedirectCallback.Abandon();
|
AndroidRedirectCallback.Abandon();
|
||||||
|
|
||||||
|
// And the keyboard may have gone away while this activity was not on screen, without Avalonia's
|
||||||
|
// input pane raising anything to say so. PhoneShell re-reads the inset from this. See
|
||||||
|
// PhoneEnvironment.Resumed, which carries the whole of why.
|
||||||
|
PhoneEnvironment.RaiseResumed();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -100,6 +100,35 @@ internal static class PhoneEnvironment
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public static global::Android.App.Activity? CurrentActivity { get; set; }
|
public static global::Android.App.Activity? CurrentActivity { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Raised each time the launcher activity comes back to the foreground.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// ◆ <b>Exists because the software keyboard can go away without Avalonia being told.</b>
|
||||||
|
/// <c>IInputPane.StateChanged</c> is the only signal <c>PhoneShell</c> has for the keyboard opening and
|
||||||
|
/// closing, and it is not raised when Android dismisses the keyboard on the way out of the app — the
|
||||||
|
/// activity is being paused, and the pane is simply gone by the time anything could have reported it.
|
||||||
|
/// So the inset that holds the interface clear of the keyboard was still applied on the way back in,
|
||||||
|
/// and the whole phone drew an entire keyboard's height above the bottom of a screen with no keyboard
|
||||||
|
/// on it. Reproduced every time: open the keyboard anywhere, press Home, return — 838 of 2340 pixels
|
||||||
|
/// dead, and staying that way until the keyboard was opened and closed again.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// An event here rather than a call into the view, because the view is Avalonia's and this is Android's:
|
||||||
|
/// <c>MainActivity</c> already owns <c>OnResume</c> and already writes <see cref="CurrentActivity"/>
|
||||||
|
/// from it, and a static event is the same shape of hand-off one step further. Anything else that needs
|
||||||
|
/// to re-read platform state on the way back in can subscribe without <c>MainActivity</c> learning what
|
||||||
|
/// it is.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Subscribers are responsible for unsubscribing — this is a static event and holds whatever it is
|
||||||
|
/// given for the life of the process. <c>PhoneShell</c> attaches and detaches with its visual tree.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public static event EventHandler? Resumed;
|
||||||
|
|
||||||
|
/// <summary>Announces that the launcher activity is foreground again.</summary>
|
||||||
|
internal static void RaiseResumed() => Resumed?.Invoke(null, EventArgs.Empty);
|
||||||
|
|
||||||
/// <summary>The application context, once <see cref="Attach"/> has run.</summary>
|
/// <summary>The application context, once <see cref="Attach"/> has run.</summary>
|
||||||
public static Context Require() =>
|
public static Context Require() =>
|
||||||
context ?? throw new InvalidOperationException(
|
context ?? throw new InvalidOperationException(
|
||||||
|
|||||||
@@ -167,10 +167,124 @@
|
|||||||
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
|
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
|
||||||
<Setter Property="FontSize" Value="20" />
|
<Setter Property="FontSize" Value="20" />
|
||||||
</Style>
|
</Style>
|
||||||
|
<!--
|
||||||
|
◆ THE FILL BELOW IS NOT DECORATION. IT IS UNDOING ONE FLUENT PUTS THERE.
|
||||||
|
|
||||||
|
`accent` is not a name this theme invented — Fluent ships a `Button.accent` style of its own, and a
|
||||||
|
button classed `icon accent` matches it. So these three buttons were being painted with Fluent's
|
||||||
|
system-accent slab, #6275AC, a blue that appears nowhere in Palette.axaml and reads as a control from
|
||||||
|
a different application sitting in this one's header. Measured off a screenshot rather than guessed:
|
||||||
|
the fill was #6275AC where the accent is #5D42DE.
|
||||||
|
|
||||||
|
The rule above could not stop it, because it sets Foreground and FontSize and Fluent's fill is on the
|
||||||
|
ContentPresenter — which is also why `Button.icon`'s own Background="Transparent" does not reach it.
|
||||||
|
App.axaml has carried the desktop's answer to exactly this since v5b: override the ContentPresenter,
|
||||||
|
not the Button. This head simply never had one, so the desktop looked right and the phone did not.
|
||||||
|
|
||||||
|
Transparent rather than the desktop's gradient, and the difference is the control rather than an
|
||||||
|
inconsistency. The desktop's Button.accent is a filled primary slab — this head has one of those too,
|
||||||
|
and it is Button.primary. `icon accent` is a 44-pixel glyph in a header row beside other glyphs; what
|
||||||
|
"accent" asks for there is the accent *colour on the mark*, which is what the rule above gives it. A
|
||||||
|
slab would make one header icon a button and its neighbours not.
|
||||||
|
|
||||||
|
Pressed is deliberately not restated: Button.icon:pressed below sets ChromeHover on every icon button
|
||||||
|
and, coming later in this file, wins over this rule for the moment a finger is down. That is the
|
||||||
|
feedback the plain icon buttons give and there is no reason this one should differ.
|
||||||
|
-->
|
||||||
|
<Style Selector="Button.icon.accent /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.icon.accent:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.icon.accent:disabled /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
||||||
|
</Style>
|
||||||
<Style Selector="Button.icon:pressed /template/ ContentPresenter">
|
<Style Selector="Button.icon:pressed /template/ ContentPresenter">
|
||||||
<Setter Property="Background" Value="{StaticResource ChromeHover}" />
|
<Setter Property="Background" Value="{StaticResource ChromeHover}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
============ one key of the terminal's accessory keypad ============
|
||||||
|
|
||||||
|
A class rather than eleven sets of attributes assigned in code, and the move out of the code-behind is
|
||||||
|
most of what makes the latched state honest: Ctrl on and Ctrl off used to be two Background values
|
||||||
|
written by hand in TerminalScreen.axaml.cs, which meant the row named two of this palette's brushes
|
||||||
|
itself and any third state would have named a third. It is `latched` here, and the code toggles a class.
|
||||||
|
|
||||||
|
── the numbers, and why they are these ─────────────────────────────────────────────────────────────────
|
||||||
|
40 tall, where the row this replaced was 30. Android asks for 48 on a touch target and every terminal on
|
||||||
|
the platform lands near it; 30 was chosen to save a strip of the terminal above and took the keypad past
|
||||||
|
the point of working, which is the wrong end of that trade. 40 with a 2-pixel margin gives 44 of pitch,
|
||||||
|
which is the same figure TextBox.field uses for a box a thumb has to hit.
|
||||||
|
|
||||||
|
HorizontalAlignment Stretch, because these live in UniformGrid cells and the cell is the share of the
|
||||||
|
width the key is entitled to. Left to Center a key would shrink to its label and "|" would be a sliver
|
||||||
|
between two wide neighbours — which is the layout the flexed row exists to stop.
|
||||||
|
|
||||||
|
── pressed has to be visible, and it was not ───────────────────────────────────────────────────────────
|
||||||
|
These keys are Focusable="False" so that using one does not take the keyboard away from the terminal
|
||||||
|
(see TerminalScreen.axaml.cs), and a button that never takes focus also never shows the focus adorner
|
||||||
|
Fluent would otherwise give it. That left a press with no feedback at all on a control whose entire job
|
||||||
|
is to be pressed while somebody is looking at the line of output above it. The pressed fill below is the
|
||||||
|
only thing that says a key was hit — and on a keypad over a remote shell, "did that send?" is a question
|
||||||
|
worth answering in the interface rather than in the scrollback.
|
||||||
|
-->
|
||||||
|
<Style Selector="Button.hotkey">
|
||||||
|
<Setter Property="Height" Value="40" />
|
||||||
|
<Setter Property="MinHeight" Value="40" />
|
||||||
|
<Setter Property="MinWidth" Value="0" />
|
||||||
|
<Setter Property="Margin" Value="2,0" />
|
||||||
|
<Setter Property="Padding" Value="0" />
|
||||||
|
<Setter Property="CornerRadius" Value="10" />
|
||||||
|
<Setter Property="Background" Value="{StaticResource Panel}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
||||||
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||||
|
<Setter Property="FontSize" Value="12" />
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.hotkey /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="{StaticResource Panel}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
<Setter Property="CornerRadius" Value="10" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.hotkey:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="{StaticResource Panel}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.hotkey:pressed /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="{StaticResource Active}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource Accent}" />
|
||||||
|
</Style>
|
||||||
|
<!--
|
||||||
|
Latched Ctrl. The same fill a press gives, held rather than flashed — a modifier that is on and does not
|
||||||
|
look on is how somebody sends ^L to a database prompt believing they typed an l.
|
||||||
|
-->
|
||||||
|
<Style Selector="Button.hotkey.latched">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.hotkey.latched /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="{StaticResource Active}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource Accent}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.hotkey:disabled /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="{StaticResource Panel}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.hotkey:disabled">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
The floating action button the design has asked for since v2, and which this theme refused twice — on
|
The floating action button the design has asked for since v2, and which this theme refused twice — on
|
||||||
the grounds that it would open an editor this head had not got, and that an unused style is a claim the
|
the grounds that it would open an editor this head had not got, and that an unused style is a claim the
|
||||||
@@ -398,6 +512,58 @@
|
|||||||
<Setter Property="FontSize" Value="12" />
|
<Setter Property="FontSize" Value="12" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
◆ The two port boxes, which are NumericUpDowns and were the only unthemed controls on this head.
|
||||||
|
|
||||||
|
They sit in a two-column Grid with a plain TextBox.field beside them — port next to username in the host
|
||||||
|
editor, and port next to default-user in the group editor — so the mismatch was not subtle: a bright
|
||||||
|
Fluent border and a near-black fill against this theme's #101019 and hairline, at the one place on the
|
||||||
|
screen where the two are a hand's width apart and directly comparable.
|
||||||
|
|
||||||
|
Not `Classes="field"` on the two call sites, because that class is a TextBox class and this is not a
|
||||||
|
TextBox: NumericUpDown is a ButtonSpinner wrapping one, so half the chrome being corrected belongs to
|
||||||
|
the spinner and would not take a TextBox setter. Typing the control instead also means a third port box
|
||||||
|
added later is right without anybody remembering, which is the property the desktop's own bare
|
||||||
|
`Selector="NumericUpDown"` in App.axaml has had since it was written.
|
||||||
|
|
||||||
|
Three selectors rather than one for the same reason Button.icon.accent needs two: Fluent puts the fill
|
||||||
|
and the border on template parts, and a value set on the control alone is a TemplateBinding the theme's
|
||||||
|
own wins over. The numbers are TextBox.field's, exactly — 44 tall, 10 radius, Field on BorderMid — so
|
||||||
|
the pair reads as one row of two boxes rather than as a box and a visitor.
|
||||||
|
-->
|
||||||
|
<Style Selector="NumericUpDown">
|
||||||
|
<Setter Property="Height" Value="44" />
|
||||||
|
<Setter Property="MinHeight" Value="44" />
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||||
|
<Setter Property="Background" Value="{StaticResource Field}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
<Setter Property="CornerRadius" Value="10" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||||
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||||
|
<Setter Property="FontSize" Value="12" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="NumericUpDown /template/ ButtonSpinner">
|
||||||
|
<Setter Property="Background" Value="{StaticResource Field}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
<Setter Property="CornerRadius" Value="10" />
|
||||||
|
<Setter Property="MinHeight" Value="44" />
|
||||||
|
</Style>
|
||||||
|
<!--
|
||||||
|
The inner box gives its chrome up entirely: the spinner above is drawing the border and the fill, and a
|
||||||
|
TextBox that kept its own would put a second rounded rectangle a pixel inside the first.
|
||||||
|
-->
|
||||||
|
<Style Selector="NumericUpDown /template/ TextBox">
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
|
<Setter Property="Padding" Value="14,0" />
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||||
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||||
|
<Setter Property="FontSize" Value="12" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Every box on this head that takes a secret, and it is a class rather than two attributes repeated five
|
Every box on this head that takes a secret, and it is a class rather than two attributes repeated five
|
||||||
times because the two attributes are not interchangeable and both are needed.
|
times because the two attributes are not interchangeable and both are needed.
|
||||||
@@ -473,10 +639,29 @@
|
|||||||
<Setter Property="FontSize" Value="9.5" />
|
<Setter Property="FontSize" Value="9.5" />
|
||||||
<Setter Property="FontWeight" Value="SemiBold" />
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
</Style>
|
</Style>
|
||||||
|
<!--
|
||||||
|
◆ Wraps, like .body below it, and the omission was doing real damage rather than none.
|
||||||
|
|
||||||
|
A TextBlock that neither wraps nor trims does not shrink to fit and does not report that it did not:
|
||||||
|
it lays out at its full single-line width and the parent clips whatever runs past the edge. So a
|
||||||
|
.detail long enough to overflow ended mid-word against the screen, with no ellipsis to say anything
|
||||||
|
was missing — "A machine to connect to, filed under a group if you" on the add sheet, which reads as
|
||||||
|
a sentence somebody forgot to finish rather than as text that was cut.
|
||||||
|
|
||||||
|
It is a style rather than an attribute on the ones that overflow today, because which of the fifty-odd
|
||||||
|
.detail blocks overflows is a function of the device's width and the user's font scale, not of the
|
||||||
|
string: every one of them is a candidate at 320dp that is fine at 411dp, and fixing the handful that
|
||||||
|
happen to be visibly wrong on a Pixel 5 would leave the rest to be found one narrow phone at a time.
|
||||||
|
|
||||||
|
The nine that genuinely want one line — list rows and log lines, where a wrapped detail would push the
|
||||||
|
row's own height around — now say TextWrapping="NoWrap" beside their TextTrimming, which is the pairing
|
||||||
|
that was always meant and was previously being had by accident.
|
||||||
|
-->
|
||||||
<Style Selector="TextBlock.detail">
|
<Style Selector="TextBlock.detail">
|
||||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||||
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
||||||
<Setter Property="FontSize" Value="10" />
|
<Setter Property="FontSize" Value="10" />
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="TextBlock.body">
|
<Style Selector="TextBlock.body">
|
||||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||||
|
|||||||
@@ -551,7 +551,7 @@
|
|||||||
Foreground="{StaticResource Accent}"
|
Foreground="{StaticResource Accent}"
|
||||||
Background="{StaticResource Raised}" />
|
Background="{StaticResource Raised}" />
|
||||||
<TextBlock Classes="detail" Text="{Binding Progress}"
|
<TextBlock Classes="detail" Text="{Binding Progress}"
|
||||||
TextTrimming="CharacterEllipsis" />
|
TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|||||||
@@ -49,12 +49,29 @@
|
|||||||
|
|
||||||
<TextBlock Classes="title" Text="UNKNOWN HOST KEY" FontSize="13" />
|
<TextBlock Classes="title" Text="UNKNOWN HOST KEY" FontSize="13" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
◆ The port and the full stop are one Run, and that is the whole of the fix.
|
||||||
|
|
||||||
|
Avalonia puts a space between adjacent Runs, which is what makes "First contact with", the
|
||||||
|
host and the separator read as a sentence without any of them carrying padding. The trailing
|
||||||
|
Run began with a full stop, so it got that space too and the sheet said
|
||||||
|
|
||||||
|
First contact with 10.0.2.2 · 2222 . Nothing in this keychain…
|
||||||
|
|
||||||
|
— a gap before the stop, on the one dialogue in this application whose entire job is to be
|
||||||
|
read carefully before somebody answers it. StringFormat welds the stop to the number inside a
|
||||||
|
single Run, where no inter-Run space can get in, and the sentence that follows starts a Run of
|
||||||
|
its own so it still gets the space it wants.
|
||||||
|
|
||||||
|
The `{}` prefix is required: a StringFormat beginning with a brace would otherwise be parsed
|
||||||
|
as a markup extension.
|
||||||
|
-->
|
||||||
<TextBlock Classes="body" Margin="0,6,0,0">
|
<TextBlock Classes="body" Margin="0,6,0,0">
|
||||||
<Run Text="First contact with" />
|
<Run Text="First contact with" />
|
||||||
<Run Text="{Binding PendingHostKey.Host}" Foreground="{StaticResource Text}" />
|
<Run Text="{Binding PendingHostKey.Host}" Foreground="{StaticResource Text}" />
|
||||||
<Run Text="·" />
|
<Run Text="·" />
|
||||||
<Run Text="{Binding PendingHostKey.Port}" />
|
<Run Text="{Binding PendingHostKey.Port, StringFormat='{}{0}.'}" />
|
||||||
<Run Text=". Nothing in this keychain has approved this key." />
|
<Run Text="Nothing in this keychain has approved this key." />
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
|
|
||||||
<TextBlock Classes="label" Margin="0,14,0,5" Foreground="{StaticResource TextFaint}"
|
<TextBlock Classes="label" Margin="0,14,0,5" Foreground="{StaticResource TextFaint}"
|
||||||
|
|||||||
@@ -465,7 +465,7 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||||
<TextBlock Classes="detail" FontSize="10.5" Text="{Binding Address}"
|
<TextBlock Classes="detail" FontSize="10.5" Text="{Binding Address}"
|
||||||
TextTrimming="CharacterEllipsis" />
|
TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
|
||||||
<TextBlock Classes="detail" FontSize="10.5" Text="·" />
|
<TextBlock Classes="detail" FontSize="10.5" Text="·" />
|
||||||
<TextBlock Classes="detail" FontSize="10.5" Text="{Binding Authentication}" />
|
<TextBlock Classes="detail" FontSize="10.5" Text="{Binding Authentication}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@@ -287,7 +287,7 @@
|
|||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<!-- What is known *about* an item, never the secret. -->
|
<!-- What is known *about* an item, never the secret. -->
|
||||||
<TextBlock Classes="detail" Text="{Binding Detail}" TextTrimming="CharacterEllipsis" />
|
<TextBlock Classes="detail" Text="{Binding Detail}" TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<TextBlock Grid.Column="2" Text="›" Foreground="{StaticResource TextGhost}" FontSize="15"
|
<TextBlock Grid.Column="2" Text="›" Foreground="{StaticResource TextGhost}" FontSize="15"
|
||||||
|
|||||||
@@ -106,11 +106,11 @@
|
|||||||
|
|
||||||
<TextBlock Classes="detail" FontSize="10" Text="{Binding HostLabel}"
|
<TextBlock Classes="detail" FontSize="10" Text="{Binding HostLabel}"
|
||||||
Foreground="{StaticResource TextDim}" VerticalAlignment="Center"
|
Foreground="{StaticResource TextDim}" VerticalAlignment="Center"
|
||||||
TextTrimming="CharacterEllipsis" />
|
TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<TextBlock Classes="detail" FontSize="10.5" Foreground="{StaticResource Text}"
|
<TextBlock Classes="detail" FontSize="10.5" Foreground="{StaticResource Text}"
|
||||||
Text="{Binding Address}" TextTrimming="CharacterEllipsis" />
|
Text="{Binding Address}" TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
TextDim rather than the `detail` class's own faint step. This block is drawn on
|
TextDim rather than the `detail` class's own faint step. This block is drawn on
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
<TextBlock Classes="detail" FontSize="9.5" Foreground="{StaticResource TextDim}"
|
<TextBlock Classes="detail" FontSize="9.5" Foreground="{StaticResource TextDim}"
|
||||||
Text="·" />
|
Text="·" />
|
||||||
<TextBlock Classes="detail" FontSize="9.5" Foreground="{StaticResource TextDim}"
|
<TextBlock Classes="detail" FontSize="9.5" Foreground="{StaticResource TextDim}"
|
||||||
Text="{Binding DeviceName}" TextTrimming="CharacterEllipsis" />
|
Text="{Binding DeviceName}" TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- A refused host key is the one outcome worth a colour of its own. -->
|
<!-- A refused host key is the one outcome worth a colour of its own. -->
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<TextBlock Classes="detail" FontSize="10.5" Foreground="{StaticResource Text}"
|
<TextBlock Classes="detail" FontSize="10.5" Foreground="{StaticResource Text}"
|
||||||
Text="{Binding ItemLabel}" TextTrimming="CharacterEllipsis" />
|
Text="{Binding ItemLabel}" TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
|
||||||
|
|
||||||
<!-- The names of the fields that changed, never their values. -->
|
<!-- The names of the fields that changed, never their values. -->
|
||||||
<TextBlock Classes="detail" FontSize="9.5" IsVisible="{Binding HasChangedFields}"
|
<TextBlock Classes="detail" FontSize="9.5" IsVisible="{Binding HasChangedFields}"
|
||||||
|
|||||||
@@ -391,11 +391,15 @@ internal sealed partial class PhoneShell : UserControl
|
|||||||
ApplyKeyboardInset(keyboard);
|
ApplyKeyboardInset(keyboard);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PhoneEnvironment.Resumed += OnActivityResumed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||||
{
|
{
|
||||||
|
PhoneEnvironment.Resumed -= OnActivityResumed;
|
||||||
|
|
||||||
if (TopLevel.GetTopLevel(this) is { } top)
|
if (TopLevel.GetTopLevel(this) is { } top)
|
||||||
{
|
{
|
||||||
top.BackRequested -= OnBackRequested;
|
top.BackRequested -= OnBackRequested;
|
||||||
@@ -410,6 +414,43 @@ internal sealed partial class PhoneShell : UserControl
|
|||||||
base.OnDetachedFromVisualTree(e);
|
base.OnDetachedFromVisualTree(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Re-reads the keyboard inset on the way back into the application.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// ◆ <b>The margin can be stale here and there is no other moment it gets corrected.</b> Android takes
|
||||||
|
/// the software keyboard away when this activity stops, and Avalonia's input pane raises no
|
||||||
|
/// <c>StateChanged</c> for it — the pane is gone with the window rather than closed in front of one. So
|
||||||
|
/// <see cref="ApplyKeyboardInset(double)"/> last ran for a keyboard that was up, and the interface comes
|
||||||
|
/// back holding a whole keyboard's worth of empty space it no longer needs. It is not subtle and it does
|
||||||
|
/// not recover: a third of the display, on every screen, until something opens and closes the keyboard.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Asking the pane rather than assuming zero, because resuming does not always mean the keyboard went.
|
||||||
|
/// A permission dialogue, the biometric sheet and the sign-in browser all pause this activity and can
|
||||||
|
/// all hand it back with the keyboard exactly as it was — and a handler that flattened the inset on the
|
||||||
|
/// way in would then strand the focused box under a keyboard that is genuinely there. The pane knows
|
||||||
|
/// which of the two happened; this only has to ask it again.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Posted rather than run inline. <c>OnResume</c> is early — the window has not necessarily been laid
|
||||||
|
/// out again and the pane's own state is settled by the insets that arrive with the next frame, so
|
||||||
|
/// reading it on this turn can return the state that is on the way out. Background priority puts the
|
||||||
|
/// read after that.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
private void OnActivityResumed(object? sender, EventArgs e) =>
|
||||||
|
Dispatcher.UIThread.Post(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
if (keyboard is not null)
|
||||||
|
{
|
||||||
|
ApplyKeyboardInset(keyboard);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DispatcherPriority.Background);
|
||||||
|
|
||||||
private void OnKeyboardChanged(object? sender, InputPaneStateEventArgs e)
|
private void OnKeyboardChanged(object? sender, InputPaneStateEventArgs e)
|
||||||
=> ApplyKeyboardInset(e.NewState is InputPaneState.Open ? e.EndRect.Height : 0);
|
=> ApplyKeyboardInset(e.NewState is InputPaneState.Open ? e.EndRect.Height : 0);
|
||||||
|
|
||||||
|
|||||||
@@ -341,7 +341,7 @@
|
|||||||
<TextBlock Classes="mono" FontSize="13" FontWeight="SemiBold"
|
<TextBlock Classes="mono" FontSize="13" FontWeight="SemiBold"
|
||||||
Text="{Binding HostLabel}" TextTrimming="CharacterEllipsis" />
|
Text="{Binding HostLabel}" TextTrimming="CharacterEllipsis" />
|
||||||
<TextBlock Classes="detail" FontSize="10.5" Text="{Binding Address}"
|
<TextBlock Classes="detail" FontSize="10.5" Text="{Binding Address}"
|
||||||
TextTrimming="CharacterEllipsis" />
|
TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<TextBlock Grid.Column="1" Classes="detail" FontSize="10"
|
<TextBlock Grid.Column="1" Classes="detail" FontSize="10"
|
||||||
VerticalAlignment="Center" Text="{Binding Started}" />
|
VerticalAlignment="Center" Text="{Binding Started}" />
|
||||||
@@ -505,13 +505,50 @@
|
|||||||
-->
|
-->
|
||||||
<!-- Only with a pane to type into: the keys send bytes at a session, and a connecting tab has none. -->
|
<!-- Only with a pane to type into: the keys send bytes at a session, and a connecting tab has none. -->
|
||||||
<!--
|
<!--
|
||||||
33 tall rather than 50, a third off it like the bar at the top, with the keys inside coming down from
|
◆ TWO FIXED ROWS THAT FILL THE WIDTH, AND NOTHING HERE SCROLLS. THAT IS THE WHOLE CHANGE.
|
||||||
38 to 30 for the reason the pills did: a row that shrank around its own contents would clip them.
|
|
||||||
|
|
||||||
The five pixels above it are a gap and not a border. The renderer is a native child view, so nothing
|
What this replaced was one 33-pixel row holding eleven 30-by-44 keys inside a horizontal ScrollViewer,
|
||||||
Avalonia draws can sit on top of it — a hairline between the two would have to be a row of its own —
|
and it was wrong in two ways that compounded each other.
|
||||||
and a terminal whose last line of output is flush against a row of grey keys reads as one surface
|
|
||||||
that has gone wrong rather than as two that are different things.
|
── the scroller was eating the presses ──────────────────────────────────────────────────────────────
|
||||||
|
A horizontally scrollable strip and a row of buttons want opposite things from the same gesture. Every
|
||||||
|
tap starts as a press and a few pixels of travel, and a ScrollViewer that can pan claims exactly that:
|
||||||
|
a thumb coming down at an angle — which is how a thumb comes down — is read as the start of a pan, the
|
||||||
|
button never sees a click, and the row slides slightly instead. Pressing a key and having the keys
|
||||||
|
move is the single most confidence-destroying thing a keyboard can do, and it happened often enough
|
||||||
|
to be the first thing anybody noticed about this row.
|
||||||
|
|
||||||
|
── and the keys were too small to hit anyway ─────────────────────────────────────────────────────────
|
||||||
|
30 tall. Android's own minimum touch target is 48 and every accessory row in every terminal on the
|
||||||
|
platform is somewhere near it. The height had been taken down from 38 to save a strip of terminal,
|
||||||
|
which is a trade worth making right up until the control stops working, and 30 is past that.
|
||||||
|
|
||||||
|
── what the other clients do, because all of them agree ─────────────────────────────────────────────
|
||||||
|
Termux, Termius, JuiceSSH, ConnectBot and Blink all ship this row and not one of them scrolls it.
|
||||||
|
They divide the width evenly between a fixed set of keys and, where the set does not fit, they use a
|
||||||
|
second row — Termux ships two by default. That is not convention for its own sake: a key whose
|
||||||
|
position is fixed can be hit without looking, which is the entire point of a modifier row you use
|
||||||
|
while reading the output above it. A key that might have scrolled has to be found first.
|
||||||
|
|
||||||
|
So: two rows, every key stretched to an equal share of the width, none of it scrollable, and 40 tall
|
||||||
|
instead of 30. Nothing was dropped — all eleven keys are here, and the four that mattered most are the
|
||||||
|
ones that benefit, because the arrows were the keys living past the right edge.
|
||||||
|
|
||||||
|
── what it costs, stated rather than buried ─────────────────────────────────────────────────────────
|
||||||
|
About 51 pixels of terminal, which is four or five lines of output. That is a real price and it is the
|
||||||
|
right one: the row that was cheaper did not work, and a terminal with five more lines and no reachable
|
||||||
|
Ctrl is not the better surface.
|
||||||
|
|
||||||
|
── the split ────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
Modifiers and punctuation on top, cursor keys underneath. The arrows go on the lower row because they
|
||||||
|
are the keys pressed repeatedly and in sequence — history, then along the line — so they want the row
|
||||||
|
the thumb rests nearest, and there are only four of them, which leaves them the widest targets on the
|
||||||
|
whole keypad. The text-size buttons ride at that row's right-hand end, outside the flex.
|
||||||
|
|
||||||
|
The five pixels above the whole thing are a gap and not a border. The renderer is a native child view,
|
||||||
|
so nothing Avalonia draws can sit on top of it — a hairline between the two would have to be a row of
|
||||||
|
its own — and a terminal whose last line of output is flush against a row of grey keys reads as one
|
||||||
|
surface that has gone wrong rather than as two that are different things.
|
||||||
-->
|
-->
|
||||||
<!-- ============ what a terminal says once its shell has stopped ============ -->
|
<!-- ============ what a terminal says once its shell has stopped ============ -->
|
||||||
<!--
|
<!--
|
||||||
@@ -562,62 +599,70 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Border Grid.Row="3" IsVisible="{Binding IsTerminalShowing}" Height="33" Margin="0,5,0,0">
|
<Border Grid.Row="3" IsVisible="{Binding IsTerminalShowing}" Margin="0,5,0,0">
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
<StackPanel Spacing="4">
|
||||||
|
|
||||||
<ScrollViewer Grid.Column="0" HorizontalScrollBarVisibility="Auto"
|
|
||||||
VerticalScrollBarVisibility="Disabled">
|
|
||||||
<StackPanel x:Name="AccessoryKeys" Orientation="Horizontal" Spacing="5" Margin="12,0"
|
|
||||||
VerticalAlignment="Center" />
|
|
||||||
</ScrollViewer>
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
The text-size buttons, pinned at this row's right-hand end rather than scrolling with the keys
|
Modifiers and punctuation. Populated from the table in the code-behind, which is also where the
|
||||||
beside them.
|
bytes each one sends live; a UniformGrid rather than a StackPanel so the seven divide the width
|
||||||
|
between them instead of taking what their labels happen to need — "ctrl" is four characters and
|
||||||
They used to have a row of their own above the terminal, on the argument that the accessory row
|
"|" is one, and on a row of keys those two want the same target, not proportional ones.
|
||||||
scrolls and these two must never be off-screen — a terminal too small to read is exactly the state
|
|
||||||
in which hunting for the control that fixes it is worst. That argument is answered rather than
|
|
||||||
abandoned: outside the ScrollViewer they cannot scroll away, and the row they had costs 36 pixels
|
|
||||||
on a surface this change exists to give back.
|
|
||||||
|
|
||||||
A phone cannot press Ctrl+plus. The desktop head has that chord and needs no buttons; this head
|
|
||||||
has no keyboard to press it with, which is why the two heads differ here and nowhere else in this
|
|
||||||
screen.
|
|
||||||
|
|
||||||
Disabled at the ends rather than clamping silently. A button that keeps accepting taps and does
|
|
||||||
nothing reads as the terminal having stopped responding, which is the one thing this screen must
|
|
||||||
never look like.
|
|
||||||
|
|
||||||
◆ Focusable="False", for the reason the accessory keys carry in code — see
|
|
||||||
TerminalScreen.axaml.cs. Every button on this row sits over a terminal somebody is typing into,
|
|
||||||
and an ordinary one takes Avalonia's focus off the NativeWebView on tap, which makes the package
|
|
||||||
resign the page's focus. Making the text smaller must not cost the keyboard.
|
|
||||||
-->
|
-->
|
||||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="5" Margin="8,0,12,0"
|
<UniformGrid x:Name="AccessoryKeys" Columns="7" Margin="8,0" />
|
||||||
VerticalAlignment="Center">
|
|
||||||
<Border Width="1" Height="18" Background="{StaticResource Border}" Margin="0,0,3,0"
|
|
||||||
VerticalAlignment="Center" />
|
|
||||||
|
|
||||||
<Button Classes="row" MinHeight="30" Height="30" MinWidth="40" Padding="0" CornerRadius="10"
|
<!--
|
||||||
HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
|
The cursor keys, and the two text-size buttons riding at the end.
|
||||||
Background="{StaticResource Panel}" BorderBrush="{StaticResource BorderMid}"
|
|
||||||
BorderThickness="1" Focusable="False"
|
|
||||||
Command="{Binding ShrinkTerminalFontCommand}"
|
|
||||||
IsEnabled="{Binding CanShrinkTerminalFont}">
|
|
||||||
<TextBlock Classes="mono" FontSize="13" Text="A−" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button Classes="row" MinHeight="30" Height="30" MinWidth="40" Padding="0" CornerRadius="10"
|
Grid rather than a second UniformGrid across the whole width: the arrows share what is left after
|
||||||
HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
|
A− and A+ have taken their fixed 44 each, so the four of them flex and the two of them do not.
|
||||||
Background="{StaticResource Panel}" BorderBrush="{StaticResource BorderMid}"
|
Putting all six in one uniform row would make the font buttons as wide as an arrow key and move
|
||||||
BorderThickness="1" Focusable="False"
|
them every time the row's contents changed.
|
||||||
Command="{Binding EnlargeTerminalFontCommand}"
|
-->
|
||||||
IsEnabled="{Binding CanEnlargeTerminalFont}">
|
<Grid ColumnDefinitions="*,Auto" Margin="8,0">
|
||||||
<TextBlock Classes="mono" FontSize="15" Text="A+" />
|
|
||||||
</Button>
|
<UniformGrid x:Name="ArrowKeys" Grid.Column="0" Columns="4" />
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
<!--
|
||||||
|
The text-size buttons.
|
||||||
|
|
||||||
|
They used to have a row of their own above the terminal, on the argument that the accessory row
|
||||||
|
scrolls and these two must never be off-screen — a terminal too small to read is exactly the
|
||||||
|
state in which hunting for the control that fixes it is worst. Nothing on this keypad scrolls
|
||||||
|
any more, so that argument is now satisfied by construction rather than by placement.
|
||||||
|
|
||||||
|
A phone cannot press Ctrl+plus. The desktop head has that chord and needs no buttons; this head
|
||||||
|
has no keyboard to press it with, which is why the two heads differ here and nowhere else in
|
||||||
|
this screen.
|
||||||
|
|
||||||
|
Disabled at the ends rather than clamping silently. A button that keeps accepting taps and does
|
||||||
|
nothing reads as the terminal having stopped responding, which is the one thing this screen must
|
||||||
|
never look like.
|
||||||
|
|
||||||
|
◆ Focusable="False", for the reason the accessory keys carry in code — see
|
||||||
|
TerminalScreen.axaml.cs. Every button on this keypad sits over a terminal somebody is typing
|
||||||
|
into, and an ordinary one takes Avalonia's focus off the input surface on tap. Making the text
|
||||||
|
smaller must not cost the keyboard.
|
||||||
|
-->
|
||||||
|
<!-- No Spacing: Button.hotkey carries a 2-pixel side margin, so the gaps here match the
|
||||||
|
UniformGrid cells' without being counted twice. -->
|
||||||
|
<StackPanel Grid.Column="1" Orientation="Horizontal" Margin="8,0,0,0">
|
||||||
|
<Border Width="1" Height="20" Background="{StaticResource Border}" Margin="0,0,4,0"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
|
||||||
|
<Button Classes="hotkey" Width="44" Focusable="False"
|
||||||
|
Command="{Binding ShrinkTerminalFontCommand}"
|
||||||
|
IsEnabled="{Binding CanShrinkTerminalFont}">
|
||||||
|
<TextBlock Classes="mono" FontSize="13" Text="A−" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button Classes="hotkey" Width="44" Focusable="False"
|
||||||
|
Command="{Binding EnlargeTerminalFontCommand}"
|
||||||
|
IsEnabled="{Binding CanEnlargeTerminalFont}">
|
||||||
|
<TextBlock Classes="mono" FontSize="15" Text="A+" />
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Text;
|
|||||||
|
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Layout;
|
using Avalonia.Layout;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
@@ -39,7 +40,14 @@ internal sealed partial class TerminalScreen : UserControl
|
|||||||
/// keys to fit 360dp produces ten targets too small to hit.
|
/// keys to fit 360dp produces ten targets too small to hit.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
private static readonly (string Label, byte[] Bytes, bool Latches)[] Keys =
|
/// <summary>The upper row: the modifiers, and the punctuation a symbols layer is two taps away from.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Seven, which is what the row divides between — see the keypad's remark in TerminalScreen.axaml for
|
||||||
|
/// why the set is fixed and flexed rather than scrollable. Adding an eighth is a matter of adding it
|
||||||
|
/// here and changing the <c>Columns</c> on the UniformGrid; the two numbers have to agree, and the row
|
||||||
|
/// simply gets narrower keys.
|
||||||
|
/// </remarks>
|
||||||
|
private static readonly (string Label, byte[] Bytes, bool Latches)[] ModifierKeys =
|
||||||
[
|
[
|
||||||
("esc", [0x1B], false),
|
("esc", [0x1B], false),
|
||||||
("tab", [0x09], false),
|
("tab", [0x09], false),
|
||||||
@@ -48,12 +56,31 @@ internal sealed partial class TerminalScreen : UserControl
|
|||||||
("-", [(byte)'-'], false),
|
("-", [(byte)'-'], false),
|
||||||
("~", [(byte)'~'], false),
|
("~", [(byte)'~'], false),
|
||||||
("/", [(byte)'/'], false),
|
("/", [(byte)'/'], false),
|
||||||
|
];
|
||||||
|
|
||||||
// The cursor keys, as the ANSI sequences a PTY expects. Written out rather than composed, because
|
/// <summary>The lower row: the cursor keys, which is where they became reachable.</summary>
|
||||||
// the difference between CSI A and SS3 A is the difference between working in bash and not.
|
/// <remarks>
|
||||||
("↑", [0x1B, (byte)'[', (byte)'A'], false),
|
/// <para>
|
||||||
("↓", [0x1B, (byte)'[', (byte)'B'], false),
|
/// The ANSI sequences a PTY expects, written out rather than composed, because the difference between
|
||||||
|
/// CSI A and SS3 A is the difference between working in bash and not.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// ◆ <b>Left, down, up, right</b> — the order every other client on the platform puts them in, and not
|
||||||
|
/// the order they were in here. On one flat row the pairs have to read as pairs: horizontal at the ends
|
||||||
|
/// and vertical in the middle means the two keys that undo each other are always neighbours, and a
|
||||||
|
/// thumb learns the row's shape rather than its labels. Up-down-left-right is the order they are named
|
||||||
|
/// in, which is a different thing from the order they are used in.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// These four were the keys that used to live past the right-hand edge of the scroller — the most
|
||||||
|
/// pressed keys on the keypad, and the ones you had to go looking for.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
private static readonly (string Label, byte[] Bytes, bool Latches)[] CursorKeys =
|
||||||
|
[
|
||||||
("←", [0x1B, (byte)'[', (byte)'D'], false),
|
("←", [0x1B, (byte)'[', (byte)'D'], false),
|
||||||
|
("↓", [0x1B, (byte)'[', (byte)'B'], false),
|
||||||
|
("↑", [0x1B, (byte)'[', (byte)'A'], false),
|
||||||
("→", [0x1B, (byte)'[', (byte)'C'], false),
|
("→", [0x1B, (byte)'[', (byte)'C'], false),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -232,8 +259,6 @@ internal sealed partial class TerminalScreen : UserControl
|
|||||||
|
|
||||||
private void BuildAccessoryRow()
|
private void BuildAccessoryRow()
|
||||||
{
|
{
|
||||||
var row = this.FindControl<StackPanel>("AccessoryKeys")!;
|
|
||||||
|
|
||||||
// ── nothing to hand back any more ──────────────────────────────────────────────────────────────
|
// ── nothing to hand back any more ──────────────────────────────────────────────────────────────
|
||||||
// This row used to counter both halves of every press with a posted TerminalFocus.Return(), because
|
// This row used to counter both halves of every press with a posted TerminalFocus.Return(), because
|
||||||
// the platform requests Android's own focus for Avalonia's view after dispatching each handled
|
// the platform requests Android's own focus for Avalonia's view after dispatching each handled
|
||||||
@@ -242,7 +267,18 @@ internal sealed partial class TerminalScreen : UserControl
|
|||||||
// the keyboard already wants, and the counter-move is not merely unnecessary but wrong. The keys
|
// the keyboard already wants, and the counter-move is not merely unnecessary but wrong. The keys
|
||||||
// stay Focusable=false, which is now what keeps Avalonia's own focus on the input surface.
|
// stay Focusable=false, which is now what keeps Avalonia's own focus on the input surface.
|
||||||
|
|
||||||
foreach (var (label, bytes, latches) in Keys)
|
Fill(this.FindControl<UniformGrid>("AccessoryKeys")!, ModifierKeys);
|
||||||
|
Fill(this.FindControl<UniformGrid>("ArrowKeys")!, CursorKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Puts one table's worth of keys into one row of the keypad.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The rows differ in what they hold and in nothing else — same control, same class, same wiring — so
|
||||||
|
/// they are filled by one method rather than by two that would drift.
|
||||||
|
/// </remarks>
|
||||||
|
private void Fill(UniformGrid row, (string Label, byte[] Bytes, bool Latches)[] keys)
|
||||||
|
{
|
||||||
|
foreach (var (label, bytes, latches) in keys)
|
||||||
{
|
{
|
||||||
var key = CreateKey(label);
|
var key = CreateKey(label);
|
||||||
|
|
||||||
@@ -265,29 +301,17 @@ internal sealed partial class TerminalScreen : UserControl
|
|||||||
private static Button CreateKey(string label) =>
|
private static Button CreateKey(string label) =>
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
Content = new TextBlock
|
Content = label,
|
||||||
{
|
|
||||||
Text = label,
|
|
||||||
FontFamily = (FontFamily)Application.Current!.FindResource("MonoFont")!,
|
|
||||||
FontSize = 11,
|
|
||||||
HorizontalAlignment = HorizontalAlignment.Center,
|
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
|
||||||
},
|
|
||||||
|
|
||||||
// 44 wide, and that is the number that matters: the design draws these flexed across the
|
// ◆ Every dimension and brush this used to set by hand is now Button.hotkey in Phone.axaml,
|
||||||
// width, which at 360dp with ten keys is 32 pixels each — under every thumb-target
|
// and the move is what lets the row have a pressed state at all: a style can say :pressed and
|
||||||
// guideline there is. The height came down with the row it sits in, from 38 to 30, and it
|
// an assignment cannot. It also takes this file out of the business of naming the palette's
|
||||||
// costs nothing a width does: the keys are a single row with the terminal above and the
|
// brushes, which is how a keypad ends up with a grey nothing else uses.
|
||||||
// system's gesture bar below, so there is no neighbour a short press can land on instead.
|
//
|
||||||
MinWidth = 44,
|
// Content is the string rather than a TextBlock built here for the same reason — the class
|
||||||
Height = 30,
|
// carries the font and the alignment, so a wrapper element would only be somewhere for them to
|
||||||
Padding = new Thickness(10, 0),
|
// disagree.
|
||||||
CornerRadius = new CornerRadius(9),
|
Classes = { "hotkey" },
|
||||||
Background = Palette("Panel"),
|
|
||||||
BorderBrush = Palette("BorderMid"),
|
|
||||||
BorderThickness = new Thickness(1),
|
|
||||||
Foreground = Palette("TextDim"),
|
|
||||||
HorizontalContentAlignment = HorizontalAlignment.Center,
|
|
||||||
|
|
||||||
// ◆ NOT FOCUSABLE, AND THAT IS THE WHOLE CONTROL RATHER THAN A DETAIL.
|
// ◆ NOT FOCUSABLE, AND THAT IS THE WHOLE CONTROL RATHER THAN A DETAIL.
|
||||||
//
|
//
|
||||||
@@ -316,18 +340,12 @@ internal sealed partial class TerminalScreen : UserControl
|
|||||||
|
|
||||||
// Latched state has to be visible. A modifier that is on and does not look on is how somebody sends
|
// Latched state has to be visible. A modifier that is on and does not look on is how somebody sends
|
||||||
// ^L to a database prompt believing they typed an l.
|
// ^L to a database prompt believing they typed an l.
|
||||||
controlKey.Background = Palette(controlLatched ? "Active" : "Panel");
|
//
|
||||||
controlKey.Foreground = Palette(controlLatched ? "AccentText" : "TextDim");
|
// A class rather than two brush assignments, so what "latched" looks like is stated once in
|
||||||
|
// Phone.axaml beside what "pressed" looks like — which is where somebody changing either will be.
|
||||||
|
controlKey.Classes.Set("latched", controlLatched);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>One brush from <c>Theme/Palette.axaml</c>, by key.</summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// The accessory row is built in code because its keys come from a table, so its colours cannot be set in
|
|
||||||
/// XAML with the rest of the screen's. Resolving them by name is the next best thing: a row that named
|
|
||||||
/// its own blues is how the palette ends up with a fifth surface nobody meant to add.
|
|
||||||
/// </remarks>
|
|
||||||
private static IBrush? Palette(string key) => Application.Current?.FindResource(key) as IBrush;
|
|
||||||
|
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// The control translation is the ASCII one and nothing cleverer: Ctrl-A through Ctrl-Z are 0x01 to
|
/// The control translation is the ASCII one and nothing cleverer: Ctrl-A through Ctrl-Z are 0x01 to
|
||||||
/// 0x1A, which is letter & 0x1F. Applied only to letters, because Ctrl with an arrow key is a
|
/// 0x1A, which is letter & 0x1F. Applied only to letters, because Ctrl with an arrow key is a
|
||||||
|
|||||||
@@ -220,7 +220,7 @@
|
|||||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="{Binding Name}"
|
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="{Binding Name}"
|
||||||
TextTrimming="CharacterEllipsis" />
|
TextTrimming="CharacterEllipsis" />
|
||||||
<TextBlock Classes="detail" FontSize="10.5" Text="{Binding Email}"
|
<TextBlock Classes="detail" FontSize="10.5" Text="{Binding Email}"
|
||||||
TextTrimming="CharacterEllipsis" />
|
TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
◆ The one fact on this row that decides whether the button at the foot of the screen
|
◆ The one fact on this row that decides whether the button at the foot of the screen
|
||||||
|
|||||||
Reference in new issue
Block a user