You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
Fix horrible keyboard setup
This commit is contained in:
@@ -20,8 +20,6 @@ Usage:
|
||||
|
||||
import lvgl as lv
|
||||
import mpos.ui.theme
|
||||
import time
|
||||
|
||||
|
||||
class MposKeyboard:
|
||||
"""
|
||||
@@ -38,138 +36,97 @@ class MposKeyboard:
|
||||
# Keyboard layout labels
|
||||
LABEL_NUMBERS_SPECIALS = "?123"
|
||||
LABEL_SPECIALS = "=\<"
|
||||
LABEL_LETTERS = "abc"
|
||||
LABEL_LETTERS = "Abc" # using abc here will trigger the default lv.keyboard() mode switch
|
||||
LABEL_SPACE = " "
|
||||
|
||||
# Keyboard modes - use USER modes for our API
|
||||
# We'll also register to standard modes to catch LVGL's internal switches
|
||||
MODE_LOWERCASE = lv.keyboard.MODE.USER_1
|
||||
MODE_UPPERCASE = lv.keyboard.MODE.USER_2
|
||||
MODE_NUMBERS = lv.keyboard.MODE.USER_3
|
||||
MODE_SPECIALS = lv.keyboard.MODE.USER_4
|
||||
CUSTOM_MODE_LOWERCASE = lv.keyboard.MODE.USER_1
|
||||
CUSTOM_MODE_UPPERCASE = lv.keyboard.MODE.USER_2
|
||||
CUSTOM_MODE_NUMBERS = lv.keyboard.MODE.USER_3
|
||||
CUSTOM_MODE_SPECIALS = lv.keyboard.MODE.USER_4
|
||||
|
||||
# Lowercase letters
|
||||
_lowercase_map = [
|
||||
"q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "\n",
|
||||
"a", "s", "d", "f", "g", "h", "j", "k", "l", "\n",
|
||||
lv.SYMBOL.UP, "z", "x", "c", "v", "b", "n", "m", lv.SYMBOL.BACKSPACE, "\n",
|
||||
LABEL_NUMBERS_SPECIALS, ",", LABEL_SPACE, ".", lv.SYMBOL.NEW_LINE, None
|
||||
]
|
||||
_lowercase_ctrl = [10] * len(_lowercase_map)
|
||||
|
||||
# Uppercase letters
|
||||
_uppercase_map = [
|
||||
"Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "\n",
|
||||
"A", "S", "D", "F", "G", "H", "J", "K", "L", "\n",
|
||||
lv.SYMBOL.DOWN, "Z", "X", "C", "V", "B", "N", "M", lv.SYMBOL.BACKSPACE, "\n",
|
||||
LABEL_NUMBERS_SPECIALS, ",", LABEL_SPACE, ".", lv.SYMBOL.NEW_LINE, None
|
||||
]
|
||||
_uppercase_ctrl = [10] * len(_uppercase_map)
|
||||
|
||||
# Numbers and common special characters
|
||||
_numbers_map = [
|
||||
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "\n",
|
||||
"@", "#", "$", "_", "&", "-", "+", "(", ")", "/", "\n",
|
||||
LABEL_SPECIALS, "*", "\"", "'", ":", ";", "!", "?", lv.SYMBOL.BACKSPACE, "\n",
|
||||
LABEL_LETTERS, ",", LABEL_SPACE, ".", lv.SYMBOL.NEW_LINE, None
|
||||
]
|
||||
_numbers_ctrl = [10] * len(_numbers_map)
|
||||
|
||||
# Additional special characters with emoticons
|
||||
_specials_map = [
|
||||
"~", "`", "|", "•", ":-)", ";-)", ":-D", "\n",
|
||||
":-(" , ":'-(", "^", "°", "=", "{", "}", "\\", "\n",
|
||||
LABEL_NUMBERS_SPECIALS, ":-o", ":-P", "[", "]", lv.SYMBOL.BACKSPACE, "\n",
|
||||
LABEL_LETTERS, "<", LABEL_SPACE, ">", lv.SYMBOL.NEW_LINE, None
|
||||
]
|
||||
_specials_ctrl = [10] * len(_specials_map)
|
||||
|
||||
# Map modes to their layouts
|
||||
mode_info = {
|
||||
CUSTOM_MODE_LOWERCASE: (_lowercase_map, _lowercase_ctrl),
|
||||
CUSTOM_MODE_UPPERCASE: (_uppercase_map, _uppercase_ctrl),
|
||||
CUSTOM_MODE_NUMBERS: (_numbers_map, _numbers_ctrl),
|
||||
CUSTOM_MODE_SPECIALS: (_specials_map, _specials_ctrl),
|
||||
}
|
||||
|
||||
_current_mode = None
|
||||
|
||||
def __init__(self, parent):
|
||||
"""
|
||||
Create a custom keyboard.
|
||||
|
||||
Args:
|
||||
parent: Parent LVGL object to attach keyboard to
|
||||
"""
|
||||
# Create underlying LVGL keyboard widget
|
||||
self._keyboard = lv.keyboard(parent)
|
||||
|
||||
# Store textarea reference (we DON'T pass it to LVGL to avoid double-typing)
|
||||
self._textarea = None
|
||||
|
||||
# Track last mode switch time to prevent race conditions
|
||||
# When user rapidly clicks mode buttons, button indices can get confused
|
||||
# because index 29 is "abc" in numbers mode but "," in lowercase mode
|
||||
self._last_mode_switch_time = 0
|
||||
self.set_mode(self.CUSTOM_MODE_LOWERCASE)
|
||||
|
||||
# Re-entrancy guard to prevent recursive event processing during mode switches
|
||||
self._in_mode_switch = False
|
||||
|
||||
# Configure layouts
|
||||
self._setup_layouts()
|
||||
|
||||
# Set default mode to lowercase
|
||||
# IMPORTANT: We do NOT call set_map() here in __init__.
|
||||
# Instead, set_mode() will call set_map() immediately before set_mode().
|
||||
# This matches the proof-of-concept pattern and prevents crashes from
|
||||
# calling set_map() multiple times which can corrupt button matrix state.
|
||||
self.set_mode(self.MODE_LOWERCASE)
|
||||
|
||||
# Add event handler for custom behavior
|
||||
# We need to handle ALL events to catch mode changes that LVGL might trigger
|
||||
self._keyboard.add_event_cb(self._handle_events, lv.EVENT.ALL, None)
|
||||
|
||||
# Apply theme fix for light mode visibility
|
||||
mpos.ui.theme.fix_keyboard_button_style(self._keyboard)
|
||||
|
||||
# Set reasonable default height
|
||||
self._keyboard.set_style_min_height(145, 0)
|
||||
|
||||
def _setup_layouts(self):
|
||||
"""Configure all keyboard layout modes."""
|
||||
|
||||
# Lowercase letters
|
||||
self._lowercase_map = [
|
||||
"q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "\n",
|
||||
"a", "s", "d", "f", "g", "h", "j", "k", "l", "\n",
|
||||
lv.SYMBOL.UP, "z", "x", "c", "v", "b", "n", "m", lv.SYMBOL.BACKSPACE, "\n",
|
||||
self.LABEL_NUMBERS_SPECIALS, ",", self.LABEL_SPACE, ".", lv.SYMBOL.NEW_LINE, None
|
||||
]
|
||||
self._lowercase_ctrl = [10] * len(self._lowercase_map)
|
||||
|
||||
# Uppercase letters
|
||||
self._uppercase_map = [
|
||||
"Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "\n",
|
||||
"A", "S", "D", "F", "G", "H", "J", "K", "L", "\n",
|
||||
lv.SYMBOL.DOWN, "Z", "X", "C", "V", "B", "N", "M", lv.SYMBOL.BACKSPACE, "\n",
|
||||
self.LABEL_NUMBERS_SPECIALS, ",", self.LABEL_SPACE, ".", lv.SYMBOL.NEW_LINE, None
|
||||
]
|
||||
self._uppercase_ctrl = [10] * len(self._uppercase_map)
|
||||
|
||||
# Numbers and common special characters
|
||||
self._numbers_map = [
|
||||
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "\n",
|
||||
"@", "#", "$", "_", "&", "-", "+", "(", ")", "/", "\n",
|
||||
self.LABEL_SPECIALS, "*", "\"", "'", ":", ";", "!", "?", lv.SYMBOL.BACKSPACE, "\n",
|
||||
self.LABEL_LETTERS, ",", self.LABEL_SPACE, ".", lv.SYMBOL.NEW_LINE, None
|
||||
]
|
||||
self._numbers_ctrl = [10] * len(self._numbers_map)
|
||||
|
||||
# Additional special characters with emoticons
|
||||
self._specials_map = [
|
||||
"~", "`", "|", "•", ":-)", ";-)", ":-D", "\n",
|
||||
":-(" , ":'-(", "^", "°", "=", "{", "}", "\\", "\n",
|
||||
self.LABEL_NUMBERS_SPECIALS, ":-o", ":-P", "[", "]", lv.SYMBOL.BACKSPACE, "\n",
|
||||
self.LABEL_LETTERS, "<", self.LABEL_SPACE, ">", lv.SYMBOL.NEW_LINE, None
|
||||
]
|
||||
self._specials_ctrl = [10] * len(self._specials_map)
|
||||
# Set good default height
|
||||
self._keyboard.set_style_min_height(165, 0)
|
||||
|
||||
def _handle_events(self, event):
|
||||
"""
|
||||
Handle keyboard button presses.
|
||||
|
||||
Args:
|
||||
event: LVGL event object
|
||||
"""
|
||||
event_code = event.get_code()
|
||||
|
||||
# Intercept READY event to prevent LVGL from changing modes
|
||||
if event_code == lv.EVENT.READY:
|
||||
# Stop LVGL from processing READY (which might trigger mode changes)
|
||||
event.stop_processing()
|
||||
# Forward READY event to external handlers if needed
|
||||
event_code=event.get_code()
|
||||
if event_code in [19,23,24,25,26,27,28,29,30,31,32,33,39,49,52]:
|
||||
return
|
||||
|
||||
# Intercept CANCEL event similarly
|
||||
if event_code == lv.EVENT.CANCEL:
|
||||
event.stop_processing()
|
||||
return
|
||||
name = mpos.ui.get_event_name(event_code)
|
||||
print(f"lv_event_t: code={event_code}, name={name}")
|
||||
|
||||
# Get the pressed button and its text
|
||||
target_obj=event.get_target_obj() # keyboard
|
||||
button = target_obj.get_selected_button()
|
||||
text = target_obj.get_button_text(button)
|
||||
print(f"[KBD] btn={button}, mode={self._current_mode}, text='{text}'")
|
||||
|
||||
# Only process VALUE_CHANGED events for actual typing
|
||||
if event_code != lv.EVENT.VALUE_CHANGED:
|
||||
return
|
||||
|
||||
# Stop event propagation FIRST, before doing anything else
|
||||
# This prevents LVGL's default handler from interfering
|
||||
event.stop_processing()
|
||||
|
||||
# Re-entrancy guard: Skip processing if we're currently switching modes
|
||||
# This prevents set_mode() from triggering recursive event processing
|
||||
if self._in_mode_switch:
|
||||
return
|
||||
|
||||
# Get the pressed button and its text
|
||||
button = self._keyboard.get_selected_button()
|
||||
current_mode = self._keyboard.get_mode()
|
||||
text = self._keyboard.get_button_text(button)
|
||||
|
||||
# DEBUG
|
||||
print(f"[KBD] btn={button}, mode={current_mode}, text='{text}'")
|
||||
|
||||
# Ignore if no valid button text (can happen during mode switching)
|
||||
if text is None:
|
||||
return
|
||||
@@ -186,31 +143,25 @@ class MposKeyboard:
|
||||
if text == lv.SYMBOL.BACKSPACE:
|
||||
# Delete last character
|
||||
new_text = current_text[:-1]
|
||||
|
||||
elif text == lv.SYMBOL.UP:
|
||||
# Switch to uppercase
|
||||
self.set_mode(self.MODE_UPPERCASE)
|
||||
self.set_mode(self.CUSTOM_MODE_UPPERCASE)
|
||||
return # Don't modify text
|
||||
|
||||
elif text == lv.SYMBOL.DOWN or text == self.LABEL_LETTERS:
|
||||
# Switch to lowercase
|
||||
self.set_mode(self.MODE_LOWERCASE)
|
||||
self.set_mode(self.CUSTOM_MODE_LOWERCASE)
|
||||
return # Don't modify text
|
||||
|
||||
elif text == self.LABEL_NUMBERS_SPECIALS:
|
||||
# Switch to numbers/specials
|
||||
self.set_mode(self.MODE_NUMBERS)
|
||||
self.set_mode(self.CUSTOM_MODE_NUMBERS)
|
||||
return # Don't modify text
|
||||
|
||||
elif text == self.LABEL_SPECIALS:
|
||||
# Switch to additional specials
|
||||
self.set_mode(self.MODE_SPECIALS)
|
||||
self.set_mode(self.CUSTOM_MODE_SPECIALS)
|
||||
return # Don't modify text
|
||||
|
||||
elif text == self.LABEL_SPACE:
|
||||
# Space bar
|
||||
new_text = current_text + " "
|
||||
|
||||
elif text == lv.SYMBOL.NEW_LINE:
|
||||
# Handle newline (only for multi-line textareas)
|
||||
if ta.get_one_line():
|
||||
@@ -219,7 +170,6 @@ class MposKeyboard:
|
||||
return
|
||||
else:
|
||||
new_text = current_text + "\n"
|
||||
|
||||
else:
|
||||
# Regular character
|
||||
new_text = current_text + text
|
||||
@@ -253,45 +203,16 @@ class MposKeyboard:
|
||||
return self._textarea
|
||||
|
||||
def set_mode(self, mode):
|
||||
"""
|
||||
Set keyboard mode with proper map configuration.
|
||||
print(f"[kbc] setting mode to {mode}")
|
||||
self._current_mode = mode
|
||||
key_map, ctrl_map = self.mode_info[mode]
|
||||
self._keyboard.set_map(mode, key_map, ctrl_map)
|
||||
self._keyboard.set_mode(mode)
|
||||
|
||||
This method ensures set_map() is called before set_mode() to prevent
|
||||
LVGL crashes when switching between custom keyboard modes.
|
||||
|
||||
Args:
|
||||
mode: One of MODE_LOWERCASE, MODE_UPPERCASE, MODE_NUMBERS, MODE_SPECIALS
|
||||
(can also accept standard LVGL modes)
|
||||
"""
|
||||
# Map modes to their layouts
|
||||
mode_info = {
|
||||
self.MODE_LOWERCASE: (self._lowercase_map, self._lowercase_ctrl),
|
||||
self.MODE_UPPERCASE: (self._uppercase_map, self._uppercase_ctrl),
|
||||
self.MODE_NUMBERS: (self._numbers_map, self._numbers_ctrl),
|
||||
self.MODE_SPECIALS: (self._specials_map, self._specials_ctrl),
|
||||
}
|
||||
|
||||
# Set re-entrancy guard to block any events triggered during mode switch
|
||||
self._in_mode_switch = True
|
||||
|
||||
try:
|
||||
# Set the map for the new mode BEFORE calling set_mode()
|
||||
# This prevents crashes from set_mode() being called with no map set
|
||||
if mode in mode_info:
|
||||
key_map, ctrl_map = mode_info[mode]
|
||||
self._keyboard.set_map(mode, key_map, ctrl_map)
|
||||
|
||||
# Now switch to the new mode
|
||||
self._keyboard.set_mode(mode)
|
||||
finally:
|
||||
# Always clear the guard, even if an exception occurs
|
||||
self._in_mode_switch = False
|
||||
|
||||
# ========================================================================
|
||||
# Python magic method for automatic method forwarding
|
||||
# ========================================================================
|
||||
|
||||
def __getattr__(self, name):
|
||||
print(f"[kbd] __getattr__ {name}")
|
||||
"""
|
||||
Forward any undefined method/attribute to the underlying LVGL keyboard.
|
||||
|
||||
@@ -307,41 +228,3 @@ class MposKeyboard:
|
||||
"""
|
||||
# Forward to the underlying keyboard object
|
||||
return getattr(self._keyboard, name)
|
||||
|
||||
def get_lvgl_obj(self):
|
||||
"""
|
||||
Get the underlying LVGL keyboard object.
|
||||
|
||||
This is now rarely needed since __getattr__ forwards everything automatically.
|
||||
Kept for backwards compatibility.
|
||||
"""
|
||||
return self._keyboard
|
||||
|
||||
|
||||
def create_keyboard(parent, custom=False):
|
||||
"""
|
||||
Factory function to create a keyboard.
|
||||
|
||||
This provides a simple way to switch between standard LVGL keyboard
|
||||
and custom keyboard.
|
||||
|
||||
Args:
|
||||
parent: Parent LVGL object
|
||||
custom: If True, create MposKeyboard; if False, create standard lv.keyboard
|
||||
|
||||
Returns:
|
||||
MposKeyboard instance or lv.keyboard instance
|
||||
|
||||
Example:
|
||||
# Use custom keyboard
|
||||
keyboard = create_keyboard(screen, custom=True)
|
||||
|
||||
# Use standard LVGL keyboard
|
||||
keyboard = create_keyboard(screen, custom=False)
|
||||
"""
|
||||
if custom:
|
||||
return MposKeyboard(parent)
|
||||
else:
|
||||
keyboard = lv.keyboard(parent)
|
||||
mpos.ui.theme.fix_keyboard_button_style(keyboard)
|
||||
return keyboard
|
||||
|
||||
Reference in New Issue
Block a user