Public Access
Android: a customisable key bar, a quick-access keyboard, a long-press cursor gesture, and a side panel on tablets
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.
This commit is contained in:
1 parent
c21de4ade4
commit
367cebae9b
22 files changed
+3949
-344
No files matched your search
@@ -0,0 +1,171 @@
|
||||
namespace DodoSSH.Client.Shell.Keyboard;
|
||||
|
||||
/// <summary>Which arrow key a cursor gesture is pressing, if any.</summary>
|
||||
internal enum ArrowDirection
|
||||
{
|
||||
None,
|
||||
Left,
|
||||
Down,
|
||||
Up,
|
||||
Right,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Long-press-and-drag on the terminal as a joystick for the arrow keys.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>A joystick rather than a trackpad, and the difference is what the gesture is for.</b> A trackpad would
|
||||
/// send one arrow per so many pixels of travel, which runs out of screen long before a long command line
|
||||
/// runs out of characters, and does nothing at all for the other half of the job — holding Up to walk back
|
||||
/// through history. Here the finger's distance from where the press landed is a speed, and the key repeats
|
||||
/// for as long as it is held there: a short way out steps slowly enough to stop on the character wanted, and
|
||||
/// further out in the same direction shifts up through the second and third speeds.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>The first step is immediate and the second waits.</b> Crossing the dead zone sends one key at once, so
|
||||
/// a quick nudge is exactly one character, and the slowest speed then holds off for
|
||||
/// <see cref="FirstRepeatDelayMs"/> before repeating — the delay every hardware keyboard puts between a
|
||||
/// keypress and auto-repeat, for the same reason.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>The axis is sticky.</b> A thumb dragging left does not travel in a straight line, and without some
|
||||
/// hysteresis a drift upwards of a few pixels would flip the gesture onto Up and walk into history mid-edit.
|
||||
/// The other axis has to win by <see cref="AxisSwitchRatio"/> to take over.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Pure arithmetic over displacements in density-independent pixels and times in milliseconds, which is
|
||||
/// what lets it be tested without a touchscreen. The platform half — noticing the long press, taking the
|
||||
/// touch stream away from the page, running the clock — is in the Android head's <c>RendererFocus</c>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal sealed class ArrowGesture
|
||||
{
|
||||
/// <summary>How far from the press the finger can wander before anything is sent, in dp.</summary>
|
||||
internal const double DeadZone = 16;
|
||||
|
||||
/// <summary>Where the second speed starts, in dp from the press.</summary>
|
||||
internal const double SecondSpeedAt = 64;
|
||||
|
||||
/// <summary>Where the third speed starts, in dp from the press.</summary>
|
||||
internal const double ThirdSpeedAt = 128;
|
||||
|
||||
/// <summary>How long the slowest speed waits after its first key before repeating.</summary>
|
||||
internal const long FirstRepeatDelayMs = 400;
|
||||
|
||||
/// <summary>How much the other axis has to dominate before the direction changes axis.</summary>
|
||||
internal const double AxisSwitchRatio = 1.4;
|
||||
|
||||
/// <summary>The most keys one call reports, so a stalled clock cannot come back as a burst.</summary>
|
||||
private const int MaximumBurst = 3;
|
||||
|
||||
private long nextDue;
|
||||
|
||||
/// <summary>The key being held, or <see cref="ArrowDirection.None"/> inside the dead zone.</summary>
|
||||
internal ArrowDirection Direction { get; private set; }
|
||||
|
||||
/// <summary>The speed, from 1 to 3, or 0 inside the dead zone.</summary>
|
||||
internal int Speed { get; private set; }
|
||||
|
||||
/// <summary>The interval between repeats at a speed.</summary>
|
||||
internal static long IntervalMs(int speed) => speed switch
|
||||
{
|
||||
1 => 220,
|
||||
2 => 90,
|
||||
_ => 35,
|
||||
};
|
||||
|
||||
/// <summary>Takes the finger's new displacement from where the press landed.</summary>
|
||||
/// <returns>How many keys to send now, in <see cref="Direction"/>.</returns>
|
||||
internal int Move(double dx, double dy, long nowMs)
|
||||
{
|
||||
var direction = DirectionFor(dx, dy);
|
||||
|
||||
if (direction is ArrowDirection.None)
|
||||
{
|
||||
Direction = ArrowDirection.None;
|
||||
Speed = 0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
var along = direction is ArrowDirection.Left or ArrowDirection.Right ? Math.Abs(dx) : Math.Abs(dy);
|
||||
var speed = along >= ThirdSpeedAt ? 3 : along >= SecondSpeedAt ? 2 : 1;
|
||||
|
||||
if (direction != Direction)
|
||||
{
|
||||
Direction = direction;
|
||||
Speed = speed;
|
||||
nextDue = nowMs + (speed == 1 ? FirstRepeatDelayMs : IntervalMs(speed));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (speed > Speed)
|
||||
{
|
||||
// Faster takes effect at once rather than after the slower interval already running out.
|
||||
nextDue = Math.Min(nextDue, nowMs + IntervalMs(speed));
|
||||
}
|
||||
|
||||
Speed = speed;
|
||||
|
||||
return Advance(nowMs);
|
||||
}
|
||||
|
||||
/// <summary>Lets time pass with the finger held still.</summary>
|
||||
/// <returns>How many keys have come due, in <see cref="Direction"/>.</returns>
|
||||
internal int Advance(long nowMs)
|
||||
{
|
||||
if (Direction is ArrowDirection.None)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var interval = IntervalMs(Speed);
|
||||
var count = 0;
|
||||
|
||||
while (nowMs >= nextDue && count < MaximumBurst)
|
||||
{
|
||||
count++;
|
||||
nextDue += interval;
|
||||
}
|
||||
|
||||
if (nowMs >= nextDue)
|
||||
{
|
||||
// Behind by more than a burst: drop the backlog rather than owe it.
|
||||
nextDue = nowMs + interval;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>Lets go.</summary>
|
||||
internal void Reset()
|
||||
{
|
||||
Direction = ArrowDirection.None;
|
||||
Speed = 0;
|
||||
}
|
||||
|
||||
private ArrowDirection DirectionFor(double dx, double dy)
|
||||
{
|
||||
var ax = Math.Abs(dx);
|
||||
var ay = Math.Abs(dy);
|
||||
|
||||
if (Math.Max(ax, ay) < DeadZone)
|
||||
{
|
||||
return ArrowDirection.None;
|
||||
}
|
||||
|
||||
var horizontal = Direction switch
|
||||
{
|
||||
ArrowDirection.Left or ArrowDirection.Right => ay <= ax * AxisSwitchRatio,
|
||||
ArrowDirection.Up or ArrowDirection.Down => ax > ay * AxisSwitchRatio,
|
||||
_ => ax >= ay,
|
||||
};
|
||||
|
||||
// Screen coordinates: y grows downwards, so a drag towards the top of the screen is Up.
|
||||
return horizontal
|
||||
? dx < 0 ? ArrowDirection.Left : ArrowDirection.Right
|
||||
: dy < 0 ? ArrowDirection.Up : ArrowDirection.Down;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user