@@ -154,7 +154,7 @@ def download_zip(url: str, name: str|None = None) -> str:
154154fg_palette [0 ] = config .palette_fg if config is not None else 0xffffff
155155
156156# setup display
157- displayio .release_displays ()
157+ # displayio.release_displays()
158158try :
159159 adafruit_fruitjam .peripherals .request_display_config () # user display configuration
160160except ValueError : # invalid user config or no user config provided
@@ -194,6 +194,8 @@ def download_zip(url: str, name: str|None = None) -> str:
194194STATUS_HEIGHT = 16
195195STATUS_PADDING = 4
196196
197+ HELP_MARGIN = 1
198+
197199MENU_HEIGHT = 24
198200MENU_GAP = 8
199201
@@ -314,6 +316,16 @@ def selected(self, value: bool) -> None:
314316)
315317status_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+
317329def 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
814827selected_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
892967mouse = None
893968if 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+
9121000try :
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