factor-out ux for q1 vs mk4
This commit is contained in:
parent
675b815ce0
commit
13af836ca1
@ -4,7 +4,7 @@
|
||||
#
|
||||
# Address Explorer menu functionality
|
||||
#
|
||||
import chains, stash
|
||||
import chains, stash, version
|
||||
from ux import ux_show_story, the_ux, ux_enter_bip32_index
|
||||
from menu import MenuSystem, MenuItem
|
||||
from public_constants import AFC_BECH32, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
|
||||
@ -21,7 +21,14 @@ def truncate_address(addr):
|
||||
# - 16 chars screen width
|
||||
# - but 2 lost at left (menu arrow, corner arrow)
|
||||
# - want to show not truncated on right side
|
||||
return addr[0:6] + '⋯' + addr[-6:]
|
||||
if not version.has_qwerty:
|
||||
# - 16 chars screen width
|
||||
# - but 2 lost at left (menu arrow, corner arrow)
|
||||
# - want to show not truncated on right side
|
||||
return addr[0:6] + '⋯' + addr[-6:]
|
||||
else:
|
||||
# tons of space on Q1
|
||||
return addr[0:12] + '⋯' + addr[-12:]
|
||||
|
||||
class KeypathMenu(MenuSystem):
|
||||
def __init__(self, path=None, nl=0):
|
||||
@ -252,7 +259,6 @@ Press (3) if you really understand and accept these risks.
|
||||
# - also for other {account} numbers
|
||||
# - or multisig case
|
||||
from glob import dis, NFC, VD
|
||||
import version
|
||||
|
||||
def make_msg(change=0):
|
||||
export_msg = "Press (1) to save Address summary file to SD Card."
|
||||
|
||||
@ -8,6 +8,7 @@ freeze_as_mpy('', [
|
||||
'nfc.py',
|
||||
'ndef.py',
|
||||
'trick_pins.py',
|
||||
'ux_mk4.py',
|
||||
], opt=0)
|
||||
|
||||
# Optimize data-like files, since no need to debug them.
|
||||
|
||||
@ -11,6 +11,7 @@ freeze_as_mpy('', [
|
||||
'nfc.py',
|
||||
'ndef.py',
|
||||
'trick_pins.py',
|
||||
'ux_q1.py',
|
||||
], opt=0)
|
||||
|
||||
# Optimize data-like files, since no need to debug them.
|
||||
|
||||
335
shared/ux.py
335
shared/ux.py
@ -11,15 +11,19 @@ from charcodes import (KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME,
|
||||
|
||||
DEFAULT_IDLE_TIMEOUT = const(4*3600) # (seconds) 4 hours
|
||||
|
||||
# see ux_mk or ux_q1 for these...
|
||||
|
||||
# How many characters can we fit on each line? How many lines?
|
||||
if version.has_qwerty:
|
||||
from lcd_display import CHARS_W, CHARS_H
|
||||
CH_PER_W = CHARS_W
|
||||
STORY_H = CHARS_H
|
||||
from ux_q1 import PressRelease, ux_enter_number, ux_input_numbers, ux_input_text
|
||||
else:
|
||||
# (using FontSmall)
|
||||
CH_PER_W = 17
|
||||
STORY_H = 5
|
||||
from ux_mk4 import PressRelease, ux_enter_number, ux_input_numbers, ux_input_text
|
||||
|
||||
# This signals the need to switch from current
|
||||
# menu (or whatever) to show something new. The
|
||||
@ -128,59 +132,6 @@ def ux_poll_key():
|
||||
|
||||
return ch
|
||||
|
||||
class PressRelease:
|
||||
def __init__(self, need_release='xy'):
|
||||
# Manage key-repeat: track last key, measure time it's held down, etc.
|
||||
self.need_release = need_release
|
||||
self.last_key = None
|
||||
self.num_repeats = 0
|
||||
|
||||
async def wait(self):
|
||||
from glob import numpad
|
||||
|
||||
armed = None
|
||||
|
||||
while 1:
|
||||
# two values here:
|
||||
# - (ms) time to wait before first key-repeat
|
||||
# - (ms) time between 2nd and Nth repeated events
|
||||
# - these values approved by @nvk
|
||||
rep_delay = 200 if not self.num_repeats else 20
|
||||
so_far = 0
|
||||
|
||||
while numpad.empty():
|
||||
if self.last_key and numpad.key_pressed == self.last_key:
|
||||
if so_far >= rep_delay:
|
||||
self.num_repeats += 1
|
||||
return self.last_key
|
||||
|
||||
await sleep_ms(1)
|
||||
so_far += 1
|
||||
|
||||
ch = numpad.get_nowait()
|
||||
|
||||
if ch == numpad.ABORT_KEY:
|
||||
raise AbortInteraction()
|
||||
|
||||
self.num_repeats = 0
|
||||
|
||||
if len(ch) > 1:
|
||||
# multipress: cancel press/release cycle and be a keyup
|
||||
# for other keys.
|
||||
armed = None
|
||||
continue
|
||||
|
||||
if ch == '':
|
||||
self.last_key = None
|
||||
if armed:
|
||||
return armed
|
||||
elif ch in self.need_release:
|
||||
# no key-repeat on these ones
|
||||
armed = ch
|
||||
else:
|
||||
self.last_key = ch
|
||||
return ch
|
||||
|
||||
async def ux_show_story(msg, title=None, escape=None, sensitive=False, strict_escape=False):
|
||||
# show a big long string, and wait for XY to continue
|
||||
# - returns character used to get out (X or Y)
|
||||
@ -361,283 +312,5 @@ async def ux_enter_bip32_index(prompt, can_cancel=False, unlimited=False):
|
||||
max_value = 9999
|
||||
return await ux_enter_number(prompt=prompt, max_value=max_value, can_cancel=can_cancel)
|
||||
|
||||
async def ux_enter_number(prompt, max_value, can_cancel=False):
|
||||
# return the decimal number which the user has entered
|
||||
# - default/blank value assumed to be zero
|
||||
# - clamps large values to the max
|
||||
if has_qwerty:
|
||||
return ux_enter_number_qwerty(prompt, max_value, can_cancel=can_cancel)
|
||||
|
||||
from glob import dis
|
||||
from display import FontTiny
|
||||
from math import log
|
||||
|
||||
# allow key repeat on X only
|
||||
press = PressRelease('1234567890y')
|
||||
|
||||
y = 26
|
||||
value = ''
|
||||
max_w = int(log(max_value, 10) + 1)
|
||||
|
||||
dis.clear()
|
||||
dis.text(0, 0, prompt)
|
||||
dis.text(None, -1, ("X to CANCEL, or OK when DONE." if can_cancel else
|
||||
"X to DELETE, or OK when DONE."), FontTiny)
|
||||
dis.save()
|
||||
|
||||
while 1:
|
||||
dis.restore()
|
||||
|
||||
# text centered
|
||||
if value:
|
||||
bx = dis.text(None, y, value)
|
||||
dis.icon(bx+1, y+11, 'space')
|
||||
else:
|
||||
dis.icon(64-7, y+11, 'space')
|
||||
|
||||
dis.show()
|
||||
|
||||
ch = await press.wait()
|
||||
if ch == 'y' or ch == KEY_SELECT:
|
||||
|
||||
if not value: return 0
|
||||
return min(max_value, int(value))
|
||||
|
||||
elif ch == 'x' or ch == KEY_CANCEL:
|
||||
if value:
|
||||
value = value[0:-1]
|
||||
elif can_cancel:
|
||||
# quit if they press X on empty screen
|
||||
return None
|
||||
else:
|
||||
if len(value) == max_w:
|
||||
value = value[0:-1] + ch
|
||||
else:
|
||||
value += ch
|
||||
|
||||
# cleanup leading zeros and such
|
||||
value = str(min(int(value), max_value))
|
||||
|
||||
async def ux_input_numbers(val, validate_func):
|
||||
# collect a series of digits
|
||||
from glob import dis
|
||||
from display import FontTiny
|
||||
|
||||
# allow key repeat on X only
|
||||
press = PressRelease('1234567890y')
|
||||
|
||||
footer = "X to DELETE, or OK when DONE."
|
||||
lx = 6
|
||||
y = 16
|
||||
here = ''
|
||||
|
||||
dis.clear()
|
||||
dis.text(None, -1, footer, FontTiny)
|
||||
dis.save()
|
||||
|
||||
while 1:
|
||||
dis.restore()
|
||||
|
||||
# text centered
|
||||
msg = here
|
||||
by = y
|
||||
bx = dis.text(lx, y, msg[0:16])
|
||||
dis.text(lx, y - 9, str(val, 'ascii').replace(' ', '_'), FontTiny)
|
||||
|
||||
if len(msg) > 16:
|
||||
# second line when needed (left just)
|
||||
by += 15
|
||||
bx = dis.text(lx, by, msg[16:])
|
||||
|
||||
if len(here) < 32:
|
||||
dis.icon(bx, by - 2, 'sm_box')
|
||||
|
||||
dis.show()
|
||||
|
||||
ch = await press.wait()
|
||||
if ch == 'y':
|
||||
val += here
|
||||
validate_func()
|
||||
return val
|
||||
elif ch == 'x':
|
||||
if here:
|
||||
here = here[0:-1]
|
||||
else:
|
||||
# quit if they press X on empty screen
|
||||
return
|
||||
else:
|
||||
if len(here) < 32:
|
||||
here += ch
|
||||
|
||||
async def ux_input_text(pw, confirm_exit=True, hex_only=False, max_len=100):
|
||||
# Allow them to pick each digit using "D-pad"
|
||||
from glob import dis
|
||||
from display import FontTiny, FontSmall
|
||||
|
||||
# Should allow full unicode, NKDN
|
||||
# - but limited to what we can show in FontSmall
|
||||
# - so really just ascii; not even latin-1
|
||||
# - 8-bit codepoints only
|
||||
my_rng = range(32, 127) # FontSmall.code_range
|
||||
if hex_only:
|
||||
new_expand = "0"
|
||||
symbols = b"0123456789abcdef"
|
||||
else:
|
||||
new_expand = " "
|
||||
symbols = b' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
|
||||
letters = b'abcdefghijklmnopqrstuvwxyz'
|
||||
Letters = b'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
numbers = b'1234567890'
|
||||
# assert len(set(symbols+letters+Letters+numbers)) == len(my_rng)
|
||||
|
||||
if hex_only:
|
||||
footer1 = "Enter Hexidecimal Number"
|
||||
footer2 = "58=Change 9=Next 7=Back"
|
||||
else:
|
||||
footer1 = "1=Letters 2=Numbers 3=Symbols"
|
||||
footer2 = "4=SwapCase 0=HELP"
|
||||
|
||||
y = 20
|
||||
pw = bytearray(pw or ('0' if hex_only else 'A'))
|
||||
|
||||
pos = len(pw) - 1 # which part being changed
|
||||
n_visible = const(9)
|
||||
scroll_x = max(pos - n_visible, 0)
|
||||
|
||||
def cycle_set(which, direction=1):
|
||||
# pick next item in set of choices
|
||||
for n, s in enumerate(which):
|
||||
if pw[pos] == s:
|
||||
try:
|
||||
pw[pos] = which[n + direction]
|
||||
except IndexError:
|
||||
pw[pos] = which[0 if direction == 1 else -1]
|
||||
return
|
||||
pw[pos] = which[0]
|
||||
|
||||
def change(dx):
|
||||
# next/prev within the same subset of related chars
|
||||
ch = pw[pos]
|
||||
if hex_only:
|
||||
return cycle_set(symbols, dx)
|
||||
for subset in [symbols, letters, Letters, numbers]:
|
||||
if ch in subset:
|
||||
return cycle_set(subset, dx)
|
||||
|
||||
# probably unreachable code: numeric up/down
|
||||
ch = pw[pos] + dx
|
||||
if ch not in my_rng:
|
||||
ch = (my_rng.stop - 1) if dx < 0 else my_rng.start
|
||||
assert ch in my_rng
|
||||
pw[pos] = ch
|
||||
|
||||
# pre-render the fixed stuff
|
||||
dis.clear()
|
||||
dis.text(None, -10, footer1, FontTiny)
|
||||
dis.text(None, -1, footer2, FontTiny)
|
||||
dis.save()
|
||||
|
||||
# no key-repeat on certain keys
|
||||
press = PressRelease('4xy')
|
||||
while 1:
|
||||
dis.restore()
|
||||
|
||||
lr = pos - scroll_x # left/right distance of cursor
|
||||
if lr < 4 and scroll_x:
|
||||
scroll_x -= 1
|
||||
elif lr < 0:
|
||||
scroll_x = pos
|
||||
elif lr >= (n_visible - 1):
|
||||
# past right edge
|
||||
scroll_x += 1
|
||||
|
||||
for i in range(n_visible):
|
||||
# calc abs position in string
|
||||
ax = scroll_x + i
|
||||
x = 4 + (13 * i)
|
||||
try:
|
||||
ch = pw[ax]
|
||||
except IndexError:
|
||||
continue
|
||||
|
||||
if ax == pos:
|
||||
# draw cursor
|
||||
if not hex_only and (len(pw) < 2 * n_visible):
|
||||
dis.text(x - 4, y - 19, '0x%02X' % ch, FontTiny)
|
||||
dis.icon(x - 2, y - 10, 'spin')
|
||||
|
||||
if ch == 0x20:
|
||||
dis.icon(x, y + 11, 'space')
|
||||
else:
|
||||
dis.text(x, y, chr(ch) if ch in my_rng else chr(215), FontSmall)
|
||||
|
||||
if scroll_x > 0:
|
||||
dis.text(2, y - 14, str(pw, 'ascii')[0:scroll_x].replace(' ', '_'), FontTiny)
|
||||
if scroll_x + n_visible < len(pw):
|
||||
dis.text(-1, 1, "MORE>", FontTiny)
|
||||
|
||||
dis.show()
|
||||
|
||||
ch = await press.wait()
|
||||
if ch == 'y':
|
||||
return str(pw, 'ascii')
|
||||
elif ch == 'x':
|
||||
if len(pw) > 1:
|
||||
# delete current char
|
||||
pw = pw[0:pos] + pw[pos + 1:]
|
||||
if pos >= len(pw):
|
||||
pos = len(pw) - 1
|
||||
else:
|
||||
if confirm_exit:
|
||||
pp = await ux_show_story(
|
||||
"OK to leave without any changes? Or X to cancel leaving.")
|
||||
if pp == 'x': continue
|
||||
return None
|
||||
|
||||
elif ch == '7': # left
|
||||
pos -= 1
|
||||
if pos < 0: pos = 0
|
||||
elif ch == '9': # right
|
||||
pos += 1
|
||||
if pos >= len(pw):
|
||||
if len(pw) < max_len and pw[-3:] != b' ':
|
||||
# expands with space in normal mode
|
||||
# expands with 0 in hex_only mode
|
||||
pw += new_expand
|
||||
else:
|
||||
pos -= 1 # abort addition
|
||||
|
||||
elif ch == '5': # up
|
||||
change(1)
|
||||
elif ch == '8': # down
|
||||
change(-1)
|
||||
elif hex_only:
|
||||
# just got back at the beginning of the loop
|
||||
# below branches are unreachable for hex_only mode
|
||||
pass
|
||||
elif ch == '1': # alpha
|
||||
cycle_set(b'Aa')
|
||||
elif ch == '4': # toggle case
|
||||
if (pw[pos] & ~0x20) in range(65, 91):
|
||||
pw[pos] ^= 0x20
|
||||
elif ch == '2': # numbers
|
||||
cycle_set(numbers)
|
||||
elif ch == '3': # symbols (all of them)
|
||||
cycle_set(symbols)
|
||||
elif ch == '0': # help
|
||||
help_msg = '''\
|
||||
Use arrow keys (5789) to select letter and move around.
|
||||
|
||||
1=Letters (Aa..)
|
||||
2=Numbers (12..)
|
||||
3=Symbols (!@#&*)
|
||||
4=Swap Case (q/Q)
|
||||
X=Delete char
|
||||
|
||||
Add more characters by moving past end (right side).'''
|
||||
|
||||
if confirm_exit:
|
||||
help_msg += '\nTo quit without changes, delete everything.'
|
||||
await ux_show_story(help_msg)
|
||||
|
||||
# EOF
|
||||
|
||||
337
shared/ux_mk4.py
Normal file
337
shared/ux_mk4.py
Normal file
@ -0,0 +1,337 @@
|
||||
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
# ux_mk4.py - UX/UI interactions that are Mk1-4 specific
|
||||
#
|
||||
from uasyncio import sleep_ms
|
||||
import utime, gc
|
||||
from charcodes import (KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME,
|
||||
KEY_END, KEY_PAGE_UP, KEY_PAGE_DOWN, KEY_SELECT, KEY_CANCEL)
|
||||
|
||||
class PressRelease:
|
||||
def __init__(self, need_release='xy'):
|
||||
# Manage key-repeat: track last key, measure time it's held down, etc.
|
||||
self.need_release = need_release
|
||||
self.last_key = None
|
||||
self.num_repeats = 0
|
||||
|
||||
async def wait(self):
|
||||
from glob import numpad
|
||||
|
||||
armed = None
|
||||
|
||||
while 1:
|
||||
# two values here:
|
||||
# - (ms) time to wait before first key-repeat
|
||||
# - (ms) time between 2nd and Nth repeated events
|
||||
# - these values approved by @nvk
|
||||
rep_delay = 200 if not self.num_repeats else 20
|
||||
so_far = 0
|
||||
|
||||
while numpad.empty():
|
||||
if self.last_key and numpad.key_pressed == self.last_key:
|
||||
if so_far >= rep_delay:
|
||||
self.num_repeats += 1
|
||||
return self.last_key
|
||||
|
||||
await sleep_ms(1)
|
||||
so_far += 1
|
||||
|
||||
ch = numpad.get_nowait()
|
||||
|
||||
if ch == numpad.ABORT_KEY:
|
||||
raise AbortInteraction()
|
||||
|
||||
self.num_repeats = 0
|
||||
|
||||
if len(ch) > 1:
|
||||
# multipress: cancel press/release cycle and be a keyup
|
||||
# for other keys.
|
||||
armed = None
|
||||
continue
|
||||
|
||||
if ch == '':
|
||||
self.last_key = None
|
||||
if armed:
|
||||
return armed
|
||||
elif ch in self.need_release:
|
||||
# no key-repeat on these ones
|
||||
armed = ch
|
||||
else:
|
||||
self.last_key = ch
|
||||
return ch
|
||||
|
||||
async def ux_enter_number(prompt, max_value, can_cancel=False):
|
||||
# return the decimal number which the user has entered
|
||||
# - default/blank value assumed to be zero
|
||||
# - clamps large values to the max
|
||||
from glob import dis
|
||||
from display import FontTiny
|
||||
from math import log
|
||||
|
||||
# allow key repeat on X only
|
||||
press = PressRelease('1234567890y')
|
||||
|
||||
y = 26
|
||||
value = ''
|
||||
max_w = int(log(max_value, 10) + 1)
|
||||
|
||||
dis.clear()
|
||||
dis.text(0, 0, prompt)
|
||||
dis.text(None, -1, ("X to CANCEL, or OK when DONE." if can_cancel else
|
||||
"X to DELETE, or OK when DONE."), FontTiny)
|
||||
dis.save()
|
||||
|
||||
while 1:
|
||||
dis.restore()
|
||||
|
||||
# text centered
|
||||
if value:
|
||||
bx = dis.text(None, y, value)
|
||||
dis.icon(bx+1, y+11, 'space')
|
||||
else:
|
||||
dis.icon(64-7, y+11, 'space')
|
||||
|
||||
dis.show()
|
||||
|
||||
ch = await press.wait()
|
||||
if ch == 'y' or ch == KEY_SELECT:
|
||||
|
||||
if not value: return 0
|
||||
return min(max_value, int(value))
|
||||
|
||||
elif ch == 'x' or ch == KEY_CANCEL:
|
||||
if value:
|
||||
value = value[0:-1]
|
||||
elif can_cancel:
|
||||
# quit if they press X on empty screen
|
||||
return None
|
||||
else:
|
||||
if len(value) == max_w:
|
||||
value = value[0:-1] + ch
|
||||
else:
|
||||
value += ch
|
||||
|
||||
# cleanup leading zeros and such
|
||||
value = str(min(int(value), max_value))
|
||||
|
||||
async def ux_input_numbers(val, validate_func):
|
||||
# collect a series of digits
|
||||
from glob import dis
|
||||
from display import FontTiny
|
||||
|
||||
# allow key repeat on X only
|
||||
press = PressRelease('1234567890y')
|
||||
|
||||
footer = "X to DELETE, or OK when DONE."
|
||||
lx = 6
|
||||
y = 16
|
||||
here = ''
|
||||
|
||||
dis.clear()
|
||||
dis.text(None, -1, footer, FontTiny)
|
||||
dis.save()
|
||||
|
||||
while 1:
|
||||
dis.restore()
|
||||
|
||||
# text centered
|
||||
msg = here
|
||||
by = y
|
||||
bx = dis.text(lx, y, msg[0:16])
|
||||
dis.text(lx, y - 9, str(val, 'ascii').replace(' ', '_'), FontTiny)
|
||||
|
||||
if len(msg) > 16:
|
||||
# second line when needed (left just)
|
||||
by += 15
|
||||
bx = dis.text(lx, by, msg[16:])
|
||||
|
||||
if len(here) < 32:
|
||||
dis.icon(bx, by - 2, 'sm_box')
|
||||
|
||||
dis.show()
|
||||
|
||||
ch = await press.wait()
|
||||
if ch == 'y':
|
||||
val += here
|
||||
validate_func()
|
||||
return val
|
||||
elif ch == 'x':
|
||||
if here:
|
||||
here = here[0:-1]
|
||||
else:
|
||||
# quit if they press X on empty screen
|
||||
return
|
||||
else:
|
||||
if len(here) < 32:
|
||||
here += ch
|
||||
|
||||
async def ux_input_text(pw, confirm_exit=True, hex_only=False, max_len=100):
|
||||
# Allow them to pick each digit using "D-pad"
|
||||
from glob import dis
|
||||
from display import FontTiny, FontSmall
|
||||
|
||||
# Should allow full unicode, NKDN
|
||||
# - but limited to what we can show in FontSmall
|
||||
# - so really just ascii; not even latin-1
|
||||
# - 8-bit codepoints only
|
||||
my_rng = range(32, 127) # FontSmall.code_range
|
||||
if hex_only:
|
||||
new_expand = "0"
|
||||
symbols = b"0123456789abcdef"
|
||||
else:
|
||||
new_expand = " "
|
||||
symbols = b' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
|
||||
letters = b'abcdefghijklmnopqrstuvwxyz'
|
||||
Letters = b'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
numbers = b'1234567890'
|
||||
# assert len(set(symbols+letters+Letters+numbers)) == len(my_rng)
|
||||
|
||||
if hex_only:
|
||||
footer1 = "Enter Hexidecimal Number"
|
||||
footer2 = "58=Change 9=Next 7=Back"
|
||||
else:
|
||||
footer1 = "1=Letters 2=Numbers 3=Symbols"
|
||||
footer2 = "4=SwapCase 0=HELP"
|
||||
|
||||
y = 20
|
||||
pw = bytearray(pw or ('0' if hex_only else 'A'))
|
||||
|
||||
pos = len(pw) - 1 # which part being changed
|
||||
n_visible = const(9)
|
||||
scroll_x = max(pos - n_visible, 0)
|
||||
|
||||
def cycle_set(which, direction=1):
|
||||
# pick next item in set of choices
|
||||
for n, s in enumerate(which):
|
||||
if pw[pos] == s:
|
||||
try:
|
||||
pw[pos] = which[n + direction]
|
||||
except IndexError:
|
||||
pw[pos] = which[0 if direction == 1 else -1]
|
||||
return
|
||||
pw[pos] = which[0]
|
||||
|
||||
def change(dx):
|
||||
# next/prev within the same subset of related chars
|
||||
ch = pw[pos]
|
||||
if hex_only:
|
||||
return cycle_set(symbols, dx)
|
||||
for subset in [symbols, letters, Letters, numbers]:
|
||||
if ch in subset:
|
||||
return cycle_set(subset, dx)
|
||||
|
||||
# probably unreachable code: numeric up/down
|
||||
ch = pw[pos] + dx
|
||||
if ch not in my_rng:
|
||||
ch = (my_rng.stop - 1) if dx < 0 else my_rng.start
|
||||
assert ch in my_rng
|
||||
pw[pos] = ch
|
||||
|
||||
# pre-render the fixed stuff
|
||||
dis.clear()
|
||||
dis.text(None, -10, footer1, FontTiny)
|
||||
dis.text(None, -1, footer2, FontTiny)
|
||||
dis.save()
|
||||
|
||||
# no key-repeat on certain keys
|
||||
press = PressRelease('4xy')
|
||||
while 1:
|
||||
dis.restore()
|
||||
|
||||
lr = pos - scroll_x # left/right distance of cursor
|
||||
if lr < 4 and scroll_x:
|
||||
scroll_x -= 1
|
||||
elif lr < 0:
|
||||
scroll_x = pos
|
||||
elif lr >= (n_visible - 1):
|
||||
# past right edge
|
||||
scroll_x += 1
|
||||
|
||||
for i in range(n_visible):
|
||||
# calc abs position in string
|
||||
ax = scroll_x + i
|
||||
x = 4 + (13 * i)
|
||||
try:
|
||||
ch = pw[ax]
|
||||
except IndexError:
|
||||
continue
|
||||
|
||||
if ax == pos:
|
||||
# draw cursor
|
||||
if not hex_only and (len(pw) < 2 * n_visible):
|
||||
dis.text(x - 4, y - 19, '0x%02X' % ch, FontTiny)
|
||||
dis.icon(x - 2, y - 10, 'spin')
|
||||
|
||||
if ch == 0x20:
|
||||
dis.icon(x, y + 11, 'space')
|
||||
else:
|
||||
dis.text(x, y, chr(ch) if ch in my_rng else chr(215), FontSmall)
|
||||
|
||||
if scroll_x > 0:
|
||||
dis.text(2, y - 14, str(pw, 'ascii')[0:scroll_x].replace(' ', '_'), FontTiny)
|
||||
if scroll_x + n_visible < len(pw):
|
||||
dis.text(-1, 1, "MORE>", FontTiny)
|
||||
|
||||
dis.show()
|
||||
|
||||
ch = await press.wait()
|
||||
if ch == 'y':
|
||||
return str(pw, 'ascii')
|
||||
elif ch == 'x':
|
||||
if len(pw) > 1:
|
||||
# delete current char
|
||||
pw = pw[0:pos] + pw[pos + 1:]
|
||||
if pos >= len(pw):
|
||||
pos = len(pw) - 1
|
||||
else:
|
||||
if confirm_exit:
|
||||
pp = await ux_show_story(
|
||||
"OK to leave without any changes? Or X to cancel leaving.")
|
||||
if pp == 'x': continue
|
||||
return None
|
||||
|
||||
elif ch == '7': # left
|
||||
pos -= 1
|
||||
if pos < 0: pos = 0
|
||||
elif ch == '9': # right
|
||||
pos += 1
|
||||
if pos >= len(pw):
|
||||
if len(pw) < max_len and pw[-3:] != b' ':
|
||||
# expands with space in normal mode
|
||||
# expands with 0 in hex_only mode
|
||||
pw += new_expand
|
||||
else:
|
||||
pos -= 1 # abort addition
|
||||
|
||||
elif ch == '5': # up
|
||||
change(1)
|
||||
elif ch == '8': # down
|
||||
change(-1)
|
||||
elif hex_only:
|
||||
# just got back at the beginning of the loop
|
||||
# below branches are unreachable for hex_only mode
|
||||
pass
|
||||
elif ch == '1': # alpha
|
||||
cycle_set(b'Aa')
|
||||
elif ch == '4': # toggle case
|
||||
if (pw[pos] & ~0x20) in range(65, 91):
|
||||
pw[pos] ^= 0x20
|
||||
elif ch == '2': # numbers
|
||||
cycle_set(numbers)
|
||||
elif ch == '3': # symbols (all of them)
|
||||
cycle_set(symbols)
|
||||
elif ch == '0': # help
|
||||
help_msg = '''\
|
||||
Use arrow keys (5789) to select letter and move around.
|
||||
|
||||
1=Letters (Aa..)
|
||||
2=Numbers (12..)
|
||||
3=Symbols (!@#&*)
|
||||
4=Swap Case (q/Q)
|
||||
X=Delete char
|
||||
|
||||
Add more characters by moving past end (right side).'''
|
||||
|
||||
if confirm_exit:
|
||||
help_msg += '\nTo quit without changes, delete everything.'
|
||||
await ux_show_story(help_msg)
|
||||
336
shared/ux_q1.py
Normal file
336
shared/ux_q1.py
Normal file
@ -0,0 +1,336 @@
|
||||
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
||||
#
|
||||
# ux_q1.py - UX/UI interactions that are Q1 specific and use big screen, keyboard.
|
||||
#
|
||||
from uasyncio import sleep_ms
|
||||
import utime, gc
|
||||
from charcodes import (KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME,
|
||||
KEY_END, KEY_PAGE_UP, KEY_PAGE_DOWN, KEY_SELECT, KEY_CANCEL)
|
||||
|
||||
class PressRelease:
|
||||
def __init__(self, need_release='xy'):
|
||||
# Manage key-repeat: track last key, measure time it's held down, etc.
|
||||
self.need_release = need_release
|
||||
self.last_key = None
|
||||
self.num_repeats = 0
|
||||
|
||||
async def wait(self):
|
||||
from glob import numpad
|
||||
|
||||
armed = None
|
||||
|
||||
while 1:
|
||||
# two values here:
|
||||
# - (ms) time to wait before first key-repeat
|
||||
# - (ms) time between 2nd and Nth repeated events
|
||||
# - these values approved by @nvk
|
||||
rep_delay = 200 if not self.num_repeats else 20
|
||||
so_far = 0
|
||||
|
||||
while numpad.empty():
|
||||
if self.last_key and numpad.key_pressed == self.last_key:
|
||||
if so_far >= rep_delay:
|
||||
self.num_repeats += 1
|
||||
return self.last_key
|
||||
|
||||
await sleep_ms(1)
|
||||
so_far += 1
|
||||
|
||||
ch = numpad.get_nowait()
|
||||
|
||||
if ch == numpad.ABORT_KEY:
|
||||
raise AbortInteraction()
|
||||
|
||||
self.num_repeats = 0
|
||||
|
||||
if len(ch) > 1:
|
||||
# multipress: cancel press/release cycle and be a keyup
|
||||
# for other keys.
|
||||
armed = None
|
||||
continue
|
||||
|
||||
if ch == '':
|
||||
self.last_key = None
|
||||
if armed:
|
||||
return armed
|
||||
elif ch in self.need_release:
|
||||
# no key-repeat on these ones
|
||||
armed = ch
|
||||
else:
|
||||
self.last_key = ch
|
||||
return ch
|
||||
|
||||
async def ux_enter_number(prompt, max_value, can_cancel=False):
|
||||
# return the decimal number which the user has entered
|
||||
# - default/blank value assumed to be zero
|
||||
# - clamps large values to the max
|
||||
from glob import dis
|
||||
from math import log
|
||||
|
||||
# allow key repeat on X only
|
||||
press = PressRelease('1234567890y')
|
||||
|
||||
y = 26
|
||||
value = ''
|
||||
max_w = int(log(max_value, 10) + 1)
|
||||
|
||||
dis.clear()
|
||||
dis.text(0, 0, prompt)
|
||||
dis.text(None, -1, ("X to CANCEL, or OK when DONE." if can_cancel else
|
||||
"X to DELETE, or OK when DONE."), FontTiny)
|
||||
dis.save()
|
||||
|
||||
while 1:
|
||||
dis.restore()
|
||||
|
||||
# text centered
|
||||
if value:
|
||||
bx = dis.text(None, y, value)
|
||||
dis.icon(bx+1, y+11, 'space')
|
||||
else:
|
||||
dis.icon(64-7, y+11, 'space')
|
||||
|
||||
dis.show()
|
||||
|
||||
ch = await press.wait()
|
||||
if ch == 'y' or ch == KEY_SELECT:
|
||||
|
||||
if not value: return 0
|
||||
return min(max_value, int(value))
|
||||
|
||||
elif ch == 'x' or ch == KEY_CANCEL:
|
||||
if value:
|
||||
value = value[0:-1]
|
||||
elif can_cancel:
|
||||
# quit if they press X on empty screen
|
||||
return None
|
||||
else:
|
||||
if len(value) == max_w:
|
||||
value = value[0:-1] + ch
|
||||
else:
|
||||
value += ch
|
||||
|
||||
# cleanup leading zeros and such
|
||||
value = str(min(int(value), max_value))
|
||||
|
||||
async def ux_input_numbers(val, validate_func):
|
||||
# collect a series of digits
|
||||
from glob import dis
|
||||
from display import FontTiny
|
||||
|
||||
# allow key repeat on X only
|
||||
press = PressRelease('1234567890y')
|
||||
|
||||
footer = "X to DELETE, or OK when DONE."
|
||||
lx = 6
|
||||
y = 16
|
||||
here = ''
|
||||
|
||||
dis.clear()
|
||||
dis.text(None, -1, footer, FontTiny)
|
||||
dis.save()
|
||||
|
||||
while 1:
|
||||
dis.restore()
|
||||
|
||||
# text centered
|
||||
msg = here
|
||||
by = y
|
||||
bx = dis.text(lx, y, msg[0:16])
|
||||
dis.text(lx, y - 9, str(val, 'ascii').replace(' ', '_'), FontTiny)
|
||||
|
||||
if len(msg) > 16:
|
||||
# second line when needed (left just)
|
||||
by += 15
|
||||
bx = dis.text(lx, by, msg[16:])
|
||||
|
||||
if len(here) < 32:
|
||||
dis.icon(bx, by - 2, 'sm_box')
|
||||
|
||||
dis.show()
|
||||
|
||||
ch = await press.wait()
|
||||
if ch == 'y':
|
||||
val += here
|
||||
validate_func()
|
||||
return val
|
||||
elif ch == 'x':
|
||||
if here:
|
||||
here = here[0:-1]
|
||||
else:
|
||||
# quit if they press X on empty screen
|
||||
return
|
||||
else:
|
||||
if len(here) < 32:
|
||||
here += ch
|
||||
|
||||
async def ux_input_text(pw, confirm_exit=True, hex_only=False, max_len=100):
|
||||
# Allow them to pick each digit using "D-pad"
|
||||
from glob import dis
|
||||
from display import FontTiny, FontSmall
|
||||
|
||||
# Should allow full unicode, NKDN
|
||||
# - but limited to what we can show in FontSmall
|
||||
# - so really just ascii; not even latin-1
|
||||
# - 8-bit codepoints only
|
||||
my_rng = range(32, 127) # FontSmall.code_range
|
||||
if hex_only:
|
||||
new_expand = "0"
|
||||
symbols = b"0123456789abcdef"
|
||||
else:
|
||||
new_expand = " "
|
||||
symbols = b' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
|
||||
letters = b'abcdefghijklmnopqrstuvwxyz'
|
||||
Letters = b'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
numbers = b'1234567890'
|
||||
# assert len(set(symbols+letters+Letters+numbers)) == len(my_rng)
|
||||
|
||||
if hex_only:
|
||||
footer1 = "Enter Hexidecimal Number"
|
||||
footer2 = "58=Change 9=Next 7=Back"
|
||||
else:
|
||||
footer1 = "1=Letters 2=Numbers 3=Symbols"
|
||||
footer2 = "4=SwapCase 0=HELP"
|
||||
|
||||
y = 20
|
||||
pw = bytearray(pw or ('0' if hex_only else 'A'))
|
||||
|
||||
pos = len(pw) - 1 # which part being changed
|
||||
n_visible = const(9)
|
||||
scroll_x = max(pos - n_visible, 0)
|
||||
|
||||
def cycle_set(which, direction=1):
|
||||
# pick next item in set of choices
|
||||
for n, s in enumerate(which):
|
||||
if pw[pos] == s:
|
||||
try:
|
||||
pw[pos] = which[n + direction]
|
||||
except IndexError:
|
||||
pw[pos] = which[0 if direction == 1 else -1]
|
||||
return
|
||||
pw[pos] = which[0]
|
||||
|
||||
def change(dx):
|
||||
# next/prev within the same subset of related chars
|
||||
ch = pw[pos]
|
||||
if hex_only:
|
||||
return cycle_set(symbols, dx)
|
||||
for subset in [symbols, letters, Letters, numbers]:
|
||||
if ch in subset:
|
||||
return cycle_set(subset, dx)
|
||||
|
||||
# probably unreachable code: numeric up/down
|
||||
ch = pw[pos] + dx
|
||||
if ch not in my_rng:
|
||||
ch = (my_rng.stop - 1) if dx < 0 else my_rng.start
|
||||
assert ch in my_rng
|
||||
pw[pos] = ch
|
||||
|
||||
# pre-render the fixed stuff
|
||||
dis.clear()
|
||||
dis.text(None, -10, footer1, FontTiny)
|
||||
dis.text(None, -1, footer2, FontTiny)
|
||||
dis.save()
|
||||
|
||||
# no key-repeat on certain keys
|
||||
press = PressRelease('4xy')
|
||||
while 1:
|
||||
dis.restore()
|
||||
|
||||
lr = pos - scroll_x # left/right distance of cursor
|
||||
if lr < 4 and scroll_x:
|
||||
scroll_x -= 1
|
||||
elif lr < 0:
|
||||
scroll_x = pos
|
||||
elif lr >= (n_visible - 1):
|
||||
# past right edge
|
||||
scroll_x += 1
|
||||
|
||||
for i in range(n_visible):
|
||||
# calc abs position in string
|
||||
ax = scroll_x + i
|
||||
x = 4 + (13 * i)
|
||||
try:
|
||||
ch = pw[ax]
|
||||
except IndexError:
|
||||
continue
|
||||
|
||||
if ax == pos:
|
||||
# draw cursor
|
||||
if not hex_only and (len(pw) < 2 * n_visible):
|
||||
dis.text(x - 4, y - 19, '0x%02X' % ch, FontTiny)
|
||||
dis.icon(x - 2, y - 10, 'spin')
|
||||
|
||||
if ch == 0x20:
|
||||
dis.icon(x, y + 11, 'space')
|
||||
else:
|
||||
dis.text(x, y, chr(ch) if ch in my_rng else chr(215), FontSmall)
|
||||
|
||||
if scroll_x > 0:
|
||||
dis.text(2, y - 14, str(pw, 'ascii')[0:scroll_x].replace(' ', '_'), FontTiny)
|
||||
if scroll_x + n_visible < len(pw):
|
||||
dis.text(-1, 1, "MORE>", FontTiny)
|
||||
|
||||
dis.show()
|
||||
|
||||
ch = await press.wait()
|
||||
if ch == 'y':
|
||||
return str(pw, 'ascii')
|
||||
elif ch == 'x':
|
||||
if len(pw) > 1:
|
||||
# delete current char
|
||||
pw = pw[0:pos] + pw[pos + 1:]
|
||||
if pos >= len(pw):
|
||||
pos = len(pw) - 1
|
||||
else:
|
||||
if confirm_exit:
|
||||
pp = await ux_show_story(
|
||||
"OK to leave without any changes? Or X to cancel leaving.")
|
||||
if pp == 'x': continue
|
||||
return None
|
||||
|
||||
elif ch == '7': # left
|
||||
pos -= 1
|
||||
if pos < 0: pos = 0
|
||||
elif ch == '9': # right
|
||||
pos += 1
|
||||
if pos >= len(pw):
|
||||
if len(pw) < max_len and pw[-3:] != b' ':
|
||||
# expands with space in normal mode
|
||||
# expands with 0 in hex_only mode
|
||||
pw += new_expand
|
||||
else:
|
||||
pos -= 1 # abort addition
|
||||
|
||||
elif ch == '5': # up
|
||||
change(1)
|
||||
elif ch == '8': # down
|
||||
change(-1)
|
||||
elif hex_only:
|
||||
# just got back at the beginning of the loop
|
||||
# below branches are unreachable for hex_only mode
|
||||
pass
|
||||
elif ch == '1': # alpha
|
||||
cycle_set(b'Aa')
|
||||
elif ch == '4': # toggle case
|
||||
if (pw[pos] & ~0x20) in range(65, 91):
|
||||
pw[pos] ^= 0x20
|
||||
elif ch == '2': # numbers
|
||||
cycle_set(numbers)
|
||||
elif ch == '3': # symbols (all of them)
|
||||
cycle_set(symbols)
|
||||
elif ch == '0': # help
|
||||
help_msg = '''\
|
||||
Use arrow keys (5789) to select letter and move around.
|
||||
|
||||
1=Letters (Aa..)
|
||||
2=Numbers (12..)
|
||||
3=Symbols (!@#&*)
|
||||
4=Swap Case (q/Q)
|
||||
X=Delete char
|
||||
|
||||
Add more characters by moving past end (right side).'''
|
||||
|
||||
if confirm_exit:
|
||||
help_msg += '\nTo quit without changes, delete everything.'
|
||||
await ux_show_story(help_msg)
|
||||
Loading…
Reference in New Issue
Block a user