2020
2121import lvgl as lv
2222import mpos .ui .theme
23- import time
24-
2523
2624class MposKeyboard :
2725 """
@@ -38,138 +36,97 @@ class MposKeyboard:
3836 # Keyboard layout labels
3937 LABEL_NUMBERS_SPECIALS = "?123"
4038 LABEL_SPECIALS = "=\<"
41- LABEL_LETTERS = "abc"
39+ LABEL_LETTERS = "Abc" # using abc here will trigger the default lv.keyboard() mode switch
4240 LABEL_SPACE = " "
4341
4442 # Keyboard modes - use USER modes for our API
4543 # We'll also register to standard modes to catch LVGL's internal switches
46- MODE_LOWERCASE = lv .keyboard .MODE .USER_1
47- MODE_UPPERCASE = lv .keyboard .MODE .USER_2
48- MODE_NUMBERS = lv .keyboard .MODE .USER_3
49- MODE_SPECIALS = lv .keyboard .MODE .USER_4
44+ CUSTOM_MODE_LOWERCASE = lv .keyboard .MODE .USER_1
45+ CUSTOM_MODE_UPPERCASE = lv .keyboard .MODE .USER_2
46+ CUSTOM_MODE_NUMBERS = lv .keyboard .MODE .USER_3
47+ CUSTOM_MODE_SPECIALS = lv .keyboard .MODE .USER_4
48+
49+ # Lowercase letters
50+ _lowercase_map = [
51+ "q" , "w" , "e" , "r" , "t" , "y" , "u" , "i" , "o" , "p" , "\n " ,
52+ "a" , "s" , "d" , "f" , "g" , "h" , "j" , "k" , "l" , "\n " ,
53+ lv .SYMBOL .UP , "z" , "x" , "c" , "v" , "b" , "n" , "m" , lv .SYMBOL .BACKSPACE , "\n " ,
54+ LABEL_NUMBERS_SPECIALS , "," , LABEL_SPACE , "." , lv .SYMBOL .NEW_LINE , None
55+ ]
56+ _lowercase_ctrl = [10 ] * len (_lowercase_map )
57+
58+ # Uppercase letters
59+ _uppercase_map = [
60+ "Q" , "W" , "E" , "R" , "T" , "Y" , "U" , "I" , "O" , "P" , "\n " ,
61+ "A" , "S" , "D" , "F" , "G" , "H" , "J" , "K" , "L" , "\n " ,
62+ lv .SYMBOL .DOWN , "Z" , "X" , "C" , "V" , "B" , "N" , "M" , lv .SYMBOL .BACKSPACE , "\n " ,
63+ LABEL_NUMBERS_SPECIALS , "," , LABEL_SPACE , "." , lv .SYMBOL .NEW_LINE , None
64+ ]
65+ _uppercase_ctrl = [10 ] * len (_uppercase_map )
66+
67+ # Numbers and common special characters
68+ _numbers_map = [
69+ "1" , "2" , "3" , "4" , "5" , "6" , "7" , "8" , "9" , "0" , "\n " ,
70+ "@" , "#" , "$" , "_" , "&" , "-" , "+" , "(" , ")" , "/" , "\n " ,
71+ LABEL_SPECIALS , "*" , "\" " , "'" , ":" , ";" , "!" , "?" , lv .SYMBOL .BACKSPACE , "\n " ,
72+ LABEL_LETTERS , "," , LABEL_SPACE , "." , lv .SYMBOL .NEW_LINE , None
73+ ]
74+ _numbers_ctrl = [10 ] * len (_numbers_map )
75+
76+ # Additional special characters with emoticons
77+ _specials_map = [
78+ "~" , "`" , "|" , "•" , ":-)" , ";-)" , ":-D" , "\n " ,
79+ ":-(" , ":'-(" , "^" , "°" , "=" , "{" , "}" , "\\ " , "\n " ,
80+ LABEL_NUMBERS_SPECIALS , ":-o" , ":-P" , "[" , "]" , lv .SYMBOL .BACKSPACE , "\n " ,
81+ LABEL_LETTERS , "<" , LABEL_SPACE , ">" , lv .SYMBOL .NEW_LINE , None
82+ ]
83+ _specials_ctrl = [10 ] * len (_specials_map )
84+
85+ # Map modes to their layouts
86+ mode_info = {
87+ CUSTOM_MODE_LOWERCASE : (_lowercase_map , _lowercase_ctrl ),
88+ CUSTOM_MODE_UPPERCASE : (_uppercase_map , _uppercase_ctrl ),
89+ CUSTOM_MODE_NUMBERS : (_numbers_map , _numbers_ctrl ),
90+ CUSTOM_MODE_SPECIALS : (_specials_map , _specials_ctrl ),
91+ }
92+
93+ _current_mode = None
5094
5195 def __init__ (self , parent ):
52- """
53- Create a custom keyboard.
54-
55- Args:
56- parent: Parent LVGL object to attach keyboard to
57- """
5896 # Create underlying LVGL keyboard widget
5997 self ._keyboard = lv .keyboard (parent )
6098
6199 # Store textarea reference (we DON'T pass it to LVGL to avoid double-typing)
62100 self ._textarea = None
63101
64- # Track last mode switch time to prevent race conditions
65- # When user rapidly clicks mode buttons, button indices can get confused
66- # because index 29 is "abc" in numbers mode but "," in lowercase mode
67- self ._last_mode_switch_time = 0
68-
69- # Re-entrancy guard to prevent recursive event processing during mode switches
70- self ._in_mode_switch = False
102+ self .set_mode (self .CUSTOM_MODE_LOWERCASE )
71103
72- # Configure layouts
73- self ._setup_layouts ()
74-
75- # Set default mode to lowercase
76- # IMPORTANT: We do NOT call set_map() here in __init__.
77- # Instead, set_mode() will call set_map() immediately before set_mode().
78- # This matches the proof-of-concept pattern and prevents crashes from
79- # calling set_map() multiple times which can corrupt button matrix state.
80- self .set_mode (self .MODE_LOWERCASE )
81-
82- # Add event handler for custom behavior
83- # We need to handle ALL events to catch mode changes that LVGL might trigger
84104 self ._keyboard .add_event_cb (self ._handle_events , lv .EVENT .ALL , None )
85105
86106 # Apply theme fix for light mode visibility
87107 mpos .ui .theme .fix_keyboard_button_style (self ._keyboard )
88108
89- # Set reasonable default height
90- self ._keyboard .set_style_min_height (145 , 0 )
91-
92- def _setup_layouts (self ):
93- """Configure all keyboard layout modes."""
94-
95- # Lowercase letters
96- self ._lowercase_map = [
97- "q" , "w" , "e" , "r" , "t" , "y" , "u" , "i" , "o" , "p" , "\n " ,
98- "a" , "s" , "d" , "f" , "g" , "h" , "j" , "k" , "l" , "\n " ,
99- lv .SYMBOL .UP , "z" , "x" , "c" , "v" , "b" , "n" , "m" , lv .SYMBOL .BACKSPACE , "\n " ,
100- self .LABEL_NUMBERS_SPECIALS , "," , self .LABEL_SPACE , "." , lv .SYMBOL .NEW_LINE , None
101- ]
102- self ._lowercase_ctrl = [10 ] * len (self ._lowercase_map )
103-
104- # Uppercase letters
105- self ._uppercase_map = [
106- "Q" , "W" , "E" , "R" , "T" , "Y" , "U" , "I" , "O" , "P" , "\n " ,
107- "A" , "S" , "D" , "F" , "G" , "H" , "J" , "K" , "L" , "\n " ,
108- lv .SYMBOL .DOWN , "Z" , "X" , "C" , "V" , "B" , "N" , "M" , lv .SYMBOL .BACKSPACE , "\n " ,
109- self .LABEL_NUMBERS_SPECIALS , "," , self .LABEL_SPACE , "." , lv .SYMBOL .NEW_LINE , None
110- ]
111- self ._uppercase_ctrl = [10 ] * len (self ._uppercase_map )
112-
113- # Numbers and common special characters
114- self ._numbers_map = [
115- "1" , "2" , "3" , "4" , "5" , "6" , "7" , "8" , "9" , "0" , "\n " ,
116- "@" , "#" , "$" , "_" , "&" , "-" , "+" , "(" , ")" , "/" , "\n " ,
117- self .LABEL_SPECIALS , "*" , "\" " , "'" , ":" , ";" , "!" , "?" , lv .SYMBOL .BACKSPACE , "\n " ,
118- self .LABEL_LETTERS , "," , self .LABEL_SPACE , "." , lv .SYMBOL .NEW_LINE , None
119- ]
120- self ._numbers_ctrl = [10 ] * len (self ._numbers_map )
121-
122- # Additional special characters with emoticons
123- self ._specials_map = [
124- "~" , "`" , "|" , "•" , ":-)" , ";-)" , ":-D" , "\n " ,
125- ":-(" , ":'-(" , "^" , "°" , "=" , "{" , "}" , "\\ " , "\n " ,
126- self .LABEL_NUMBERS_SPECIALS , ":-o" , ":-P" , "[" , "]" , lv .SYMBOL .BACKSPACE , "\n " ,
127- self .LABEL_LETTERS , "<" , self .LABEL_SPACE , ">" , lv .SYMBOL .NEW_LINE , None
128- ]
129- self ._specials_ctrl = [10 ] * len (self ._specials_map )
109+ # Set good default height
110+ self ._keyboard .set_style_min_height (165 , 0 )
130111
131112 def _handle_events (self , event ):
132- """
133- Handle keyboard button presses.
134-
135- Args:
136- event: LVGL event object
137- """
138- event_code = event .get_code ()
139-
140- # Intercept READY event to prevent LVGL from changing modes
141- if event_code == lv .EVENT .READY :
142- # Stop LVGL from processing READY (which might trigger mode changes)
143- event .stop_processing ()
144- # Forward READY event to external handlers if needed
113+ event_code = event .get_code ()
114+ if event_code in [19 ,23 ,24 ,25 ,26 ,27 ,28 ,29 ,30 ,31 ,32 ,33 ,39 ,49 ,52 ]:
145115 return
146116
147- # Intercept CANCEL event similarly
148- if event_code == lv .EVENT .CANCEL :
149- event .stop_processing ()
150- return
117+ name = mpos .ui .get_event_name (event_code )
118+ print (f"lv_event_t: code={ event_code } , name={ name } " )
119+
120+ # Get the pressed button and its text
121+ target_obj = event .get_target_obj () # keyboard
122+ button = target_obj .get_selected_button ()
123+ text = target_obj .get_button_text (button )
124+ print (f"[KBD] btn={ button } , mode={ self ._current_mode } , text='{ text } '" )
151125
152126 # Only process VALUE_CHANGED events for actual typing
153127 if event_code != lv .EVENT .VALUE_CHANGED :
154128 return
155129
156- # Stop event propagation FIRST, before doing anything else
157- # This prevents LVGL's default handler from interfering
158- event .stop_processing ()
159-
160- # Re-entrancy guard: Skip processing if we're currently switching modes
161- # This prevents set_mode() from triggering recursive event processing
162- if self ._in_mode_switch :
163- return
164-
165- # Get the pressed button and its text
166- button = self ._keyboard .get_selected_button ()
167- current_mode = self ._keyboard .get_mode ()
168- text = self ._keyboard .get_button_text (button )
169-
170- # DEBUG
171- print (f"[KBD] btn={ button } , mode={ current_mode } , text='{ text } '" )
172-
173130 # Ignore if no valid button text (can happen during mode switching)
174131 if text is None :
175132 return
@@ -186,31 +143,25 @@ def _handle_events(self, event):
186143 if text == lv .SYMBOL .BACKSPACE :
187144 # Delete last character
188145 new_text = current_text [:- 1 ]
189-
190146 elif text == lv .SYMBOL .UP :
191147 # Switch to uppercase
192- self .set_mode (self .MODE_UPPERCASE )
148+ self .set_mode (self .CUSTOM_MODE_UPPERCASE )
193149 return # Don't modify text
194-
195150 elif text == lv .SYMBOL .DOWN or text == self .LABEL_LETTERS :
196151 # Switch to lowercase
197- self .set_mode (self .MODE_LOWERCASE )
152+ self .set_mode (self .CUSTOM_MODE_LOWERCASE )
198153 return # Don't modify text
199-
200154 elif text == self .LABEL_NUMBERS_SPECIALS :
201155 # Switch to numbers/specials
202- self .set_mode (self .MODE_NUMBERS )
156+ self .set_mode (self .CUSTOM_MODE_NUMBERS )
203157 return # Don't modify text
204-
205158 elif text == self .LABEL_SPECIALS :
206159 # Switch to additional specials
207- self .set_mode (self .MODE_SPECIALS )
160+ self .set_mode (self .CUSTOM_MODE_SPECIALS )
208161 return # Don't modify text
209-
210162 elif text == self .LABEL_SPACE :
211163 # Space bar
212164 new_text = current_text + " "
213-
214165 elif text == lv .SYMBOL .NEW_LINE :
215166 # Handle newline (only for multi-line textareas)
216167 if ta .get_one_line ():
@@ -219,7 +170,6 @@ def _handle_events(self, event):
219170 return
220171 else :
221172 new_text = current_text + "\n "
222-
223173 else :
224174 # Regular character
225175 new_text = current_text + text
@@ -253,45 +203,16 @@ def get_textarea(self):
253203 return self ._textarea
254204
255205 def set_mode (self , mode ):
256- """
257- Set keyboard mode with proper map configuration.
206+ print (f"[kbc] setting mode to { mode } " )
207+ self ._current_mode = mode
208+ key_map , ctrl_map = self .mode_info [mode ]
209+ self ._keyboard .set_map (mode , key_map , ctrl_map )
210+ self ._keyboard .set_mode (mode )
258211
259- This method ensures set_map() is called before set_mode() to prevent
260- LVGL crashes when switching between custom keyboard modes.
261212
262- Args:
263- mode: One of MODE_LOWERCASE, MODE_UPPERCASE, MODE_NUMBERS, MODE_SPECIALS
264- (can also accept standard LVGL modes)
265- """
266- # Map modes to their layouts
267- mode_info = {
268- self .MODE_LOWERCASE : (self ._lowercase_map , self ._lowercase_ctrl ),
269- self .MODE_UPPERCASE : (self ._uppercase_map , self ._uppercase_ctrl ),
270- self .MODE_NUMBERS : (self ._numbers_map , self ._numbers_ctrl ),
271- self .MODE_SPECIALS : (self ._specials_map , self ._specials_ctrl ),
272- }
273-
274- # Set re-entrancy guard to block any events triggered during mode switch
275- self ._in_mode_switch = True
276-
277- try :
278- # Set the map for the new mode BEFORE calling set_mode()
279- # This prevents crashes from set_mode() being called with no map set
280- if mode in mode_info :
281- key_map , ctrl_map = mode_info [mode ]
282- self ._keyboard .set_map (mode , key_map , ctrl_map )
283-
284- # Now switch to the new mode
285- self ._keyboard .set_mode (mode )
286- finally :
287- # Always clear the guard, even if an exception occurs
288- self ._in_mode_switch = False
289-
290- # ========================================================================
291213 # Python magic method for automatic method forwarding
292- # ========================================================================
293-
294214 def __getattr__ (self , name ):
215+ print (f"[kbd] __getattr__ { name } " )
295216 """
296217 Forward any undefined method/attribute to the underlying LVGL keyboard.
297218
@@ -307,41 +228,3 @@ def __getattr__(self, name):
307228 """
308229 # Forward to the underlying keyboard object
309230 return getattr (self ._keyboard , name )
310-
311- def get_lvgl_obj (self ):
312- """
313- Get the underlying LVGL keyboard object.
314-
315- This is now rarely needed since __getattr__ forwards everything automatically.
316- Kept for backwards compatibility.
317- """
318- return self ._keyboard
319-
320-
321- def create_keyboard (parent , custom = False ):
322- """
323- Factory function to create a keyboard.
324-
325- This provides a simple way to switch between standard LVGL keyboard
326- and custom keyboard.
327-
328- Args:
329- parent: Parent LVGL object
330- custom: If True, create MposKeyboard; if False, create standard lv.keyboard
331-
332- Returns:
333- MposKeyboard instance or lv.keyboard instance
334-
335- Example:
336- # Use custom keyboard
337- keyboard = create_keyboard(screen, custom=True)
338-
339- # Use standard LVGL keyboard
340- keyboard = create_keyboard(screen, custom=False)
341- """
342- if custom :
343- return MposKeyboard (parent )
344- else :
345- keyboard = lv .keyboard (parent )
346- mpos .ui .theme .fix_keyboard_button_style (keyboard )
347- return keyboard
0 commit comments