Skip to content

Commit e0a41f8

Browse files
authored
Merge pull request #17 from relic-se/keyboard
Keyboard Navigation
2 parents af5ed85 + 076f9cb commit e0a41f8

File tree

1 file changed

+137
-6
lines changed

1 file changed

+137
-6
lines changed

code.py

Lines changed: 137 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def download_zip(url: str, name: str|None = None) -> str:
154154
fg_palette[0] = config.palette_fg if config is not None else 0xffffff
155155

156156
# setup display
157-
displayio.release_displays()
157+
#displayio.release_displays()
158158
try:
159159
adafruit_fruitjam.peripherals.request_display_config() # user display configuration
160160
except ValueError: # invalid user config or no user config provided
@@ -194,6 +194,8 @@ def download_zip(url: str, name: str|None = None) -> str:
194194
STATUS_HEIGHT = 16
195195
STATUS_PADDING = 4
196196

197+
HELP_MARGIN = 1
198+
197199
MENU_HEIGHT = 24
198200
MENU_GAP = 8
199201

@@ -314,6 +316,16 @@ def selected(self, value: bool) -> None:
314316
)
315317
status_group.append(page_label)
316318

319+
# add keyboard navigation help
320+
help_label = Label(
321+
font=FONT,
322+
text="[Arrow]: Move [Enter]: Select [1-9]: Category",
323+
color=(config.palette_fg if config is not None else 0xffffff),
324+
anchor_point=(0, 1.0),
325+
anchored_position=(STATUS_PADDING, display.height - STATUS_HEIGHT - HELP_MARGIN)
326+
)
327+
root_group.append(help_label)
328+
317329
def log(msg: str) -> None:
318330
status_label.text = msg
319331
print(msg)
@@ -527,6 +539,7 @@ def show_dialog(content: str, actions: list = None) -> None:
527539
width=button_width,
528540
**BUTTON_PROPS,
529541
))
542+
dialog_buttons[0].selected = True # initial selection
530543

531544
# hide other UI elements
532545
category_group.hidden = True
@@ -812,9 +825,12 @@ def open_application(full_name: str = None) -> None:
812825
supervisor.reload()
813826

814827
selected_application = None
815-
def select_application(index: int) -> None:
828+
def select_application(index: int|tuple) -> None:
816829
global selected_category, current_page, selected_application
817830

831+
if isinstance(index, tuple):
832+
index = index[1] * PAGE_COLUMNS + index[0]
833+
818834
index += current_page * PAGE_SIZE
819835
if index < 0 or index >= len(applications[selected_category]):
820836
return
@@ -888,6 +904,65 @@ def toggle_application(full_name: str = None) -> bool:
888904

889905
return result
890906

907+
# item selection
908+
909+
selected_item = None
910+
911+
def set_selected_item_color(value: bool, item: tuple|AnchoredTileGrid|None = None) -> None:
912+
if item is None:
913+
item = selected_item
914+
if item is not None:
915+
if isinstance(item, tuple):
916+
item_grid.get_content(item)[2].background_color = (config.palette_accent if config is not None else 0x008800) if value else None
917+
elif isinstance(item, AnchoredTileGrid):
918+
item.pixel_shader[2] = (config.palette_accent if config is not None else 0x008800) if value else original_arrow_btn_color
919+
920+
def select_item(value: tuple|AnchoredTileGrid|None) -> None:
921+
global selected_item
922+
set_selected_item_color(False) # reset selected state on previous item
923+
selected_item = value # assign selected
924+
set_selected_item_color(True) # set selected state on current item
925+
926+
def change_selected_item(dx: int, dy: int) -> bool:
927+
if (dx == 0 and dy == 0) or (dx != 0 and dy != 0) or abs(dx) > 1 or abs(dy) > 1: # only change 1 axis by 1
928+
return
929+
930+
# previous value
931+
value = selected_item
932+
if value is left_arrow:
933+
value = (-1, 0)
934+
elif value is right_arrow:
935+
value = (PAGE_COLUMNS, 0)
936+
elif value is None:
937+
# initial value
938+
value = (
939+
((PAGE_COLUMNS + 1) * (dx < 0) - 1) if dx != 0 else 0,
940+
((PAGE_ROWS + 1) * (dy < 0) - 1) if dy != 0 else 0
941+
)
942+
943+
while True:
944+
# apply delta
945+
value = (
946+
((value[0] + dx + 1) % (PAGE_COLUMNS + 2)) - 1, # allow -1 and PAGE_COLUMNS
947+
(value[1] + dy) % PAGE_ROWS
948+
)
949+
950+
# check visibility
951+
if value[0] < 0:
952+
if not left_arrow.hidden:
953+
value = left_arrow
954+
break
955+
elif value[0] >= PAGE_COLUMNS:
956+
if not right_arrow.hidden:
957+
value = right_arrow
958+
break
959+
elif not item_grid.get_content(value).hidden:
960+
break
961+
962+
select_item(value)
963+
964+
select_item((0, 0)) # initial selection
965+
891966
# mouse control
892967
mouse = None
893968
if config is not None and config.use_mouse and (mouse := adafruit_usb_host_mouse.find_and_init_boot_mouse()) is not None:
@@ -909,23 +984,79 @@ def atexit_callback() -> None:
909984
sys.stdin.read(1)
910985

911986
# control loop
987+
def str_unshift(value: str, data: str = "", count: int = 1) -> tuple:
988+
if len(value) < count:
989+
return None, value
990+
return data + value[:count], value[count:]
991+
992+
def key_unshift(buffer: str) -> tuple:
993+
key, buffer = str_unshift(buffer)
994+
if key is None:
995+
return None, buffer
996+
if key == "\x1b" and buffer and buffer[0] == "[":
997+
key, buffer = str_unshift(buffer, key, 2)
998+
return key, buffer
999+
9121000
try:
9131001
previous_mouse_state = False
9141002
while True:
9151003

9161004
# keyboard input
9171005
if (available := supervisor.runtime.serial_bytes_available) > 0:
918-
key = sys.stdin.read(available)
919-
if key == "\x1b": # escape
920-
reset()
1006+
buffer = sys.stdin.read(available)
1007+
while True:
1008+
key, buffer = key_unshift(buffer)
1009+
if key is None:
1010+
break
1011+
1012+
if dialog_buttons.hidden:
1013+
if key == "\x1b": # escape
1014+
reset()
1015+
elif key == "\x1b[A": # up
1016+
change_selected_item(0, -1)
1017+
elif key == "\x1b[B": # down
1018+
change_selected_item(0, 1)
1019+
elif key == "\x1b[C": # right
1020+
change_selected_item(1, 0)
1021+
elif key == "\x1b[D": # left
1022+
change_selected_item(-1, 0)
1023+
elif key == "\n": # enter
1024+
if selected_item is right_arrow:
1025+
next_page()
1026+
elif selected_item is left_arrow:
1027+
previous_page()
1028+
elif isinstance(selected_item, tuple):
1029+
select_application(selected_item)
1030+
elif key in "1234567890":
1031+
key = (int(key) - 1) % 11 # map from 0 to 10 (inclusive)
1032+
if key < len(categories):
1033+
select_category(categories[key])
1034+
else:
1035+
if key == "\x1b": # escape
1036+
deselect_application()
1037+
elif key == "\x1b[C" or key == "\x1b[D": # right or left
1038+
dx = (ord("C") - ord(key[2])) * 2 + 1
1039+
try:
1040+
i = next((i for i, x in enumerate(dialog_buttons) if x.selected))
1041+
except StopIteration:
1042+
i = len(dialog_buttons) - 1 if dx > 0 else 0
1043+
dialog_buttons[i].selected = False
1044+
dialog_buttons[(i + dx) % len(dialog_buttons)].selected = True
1045+
elif key == "\n": # enter
1046+
try:
1047+
button = next((x for x in dialog_buttons if x.selected))
1048+
except StopIteration:
1049+
pass
1050+
else:
1051+
button.click()
9211052

9221053
# mouse input
9231054
if mouse is not None and mouse.update() is not None:
9241055
mouse_state = "left" in mouse.pressed_btns
9251056
if mouse_state and not previous_mouse_state:
9261057
if dialog_buttons.hidden:
9271058
if (clicked_cell := item_grid.which_cell_contains((mouse.x * SCALE, mouse.y * SCALE))) is not None:
928-
select_application(clicked_cell[1] * PAGE_COLUMNS + clicked_cell[0])
1059+
select_application(clicked_cell)
9291060
elif not right_arrow.hidden and right_arrow.contains((mouse.x, mouse.y, 0)):
9301061
next_page()
9311062
elif not left_arrow.hidden and left_arrow.contains((mouse.x, mouse.y, 0)):

0 commit comments

Comments
 (0)