|
| 1 | +""" |
| 2 | +Custom keyboard for MicroPythonOS. |
| 3 | +
|
| 4 | +This module provides an enhanced on-screen keyboard with better layout, |
| 5 | +more characters (including emoticons), and improved usability compared |
| 6 | +to the default LVGL keyboard. |
| 7 | +
|
| 8 | +Usage: |
| 9 | + from mpos.ui.keyboard import CustomKeyboard |
| 10 | +
|
| 11 | + # Create keyboard |
| 12 | + keyboard = CustomKeyboard(parent_obj) |
| 13 | + keyboard.set_textarea(my_textarea) |
| 14 | + keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0) |
| 15 | +
|
| 16 | + # Or use factory function for drop-in replacement |
| 17 | + from mpos.ui.keyboard import create_keyboard |
| 18 | + keyboard = create_keyboard(parent_obj, custom=True) |
| 19 | +""" |
| 20 | + |
| 21 | +import lvgl as lv |
| 22 | +import mpos.ui.theme |
| 23 | + |
| 24 | + |
| 25 | +class CustomKeyboard: |
| 26 | + """ |
| 27 | + Enhanced keyboard widget with multiple layouts and emoticons. |
| 28 | +
|
| 29 | + Features: |
| 30 | + - Lowercase and uppercase letter modes |
| 31 | + - Numbers and special characters |
| 32 | + - Additional special characters with emoticons |
| 33 | + - Automatic mode switching |
| 34 | + - Compatible with LVGL keyboard API |
| 35 | + """ |
| 36 | + |
| 37 | + # Keyboard layout labels |
| 38 | + LABEL_NUMBERS_SPECIALS = "?123" |
| 39 | + LABEL_SPECIALS = "=\<" |
| 40 | + LABEL_LETTERS = "abc" |
| 41 | + LABEL_SPACE = " " |
| 42 | + |
| 43 | + # Keyboard modes (using LVGL's USER modes) |
| 44 | + MODE_LOWERCASE = lv.keyboard.MODE.USER_1 |
| 45 | + MODE_UPPERCASE = lv.keyboard.MODE.USER_2 |
| 46 | + MODE_NUMBERS = lv.keyboard.MODE.USER_3 |
| 47 | + MODE_SPECIALS = lv.keyboard.MODE.USER_4 |
| 48 | + |
| 49 | + def __init__(self, parent): |
| 50 | + """ |
| 51 | + Create a custom keyboard. |
| 52 | +
|
| 53 | + Args: |
| 54 | + parent: Parent LVGL object to attach keyboard to |
| 55 | + """ |
| 56 | + # Create underlying LVGL keyboard widget |
| 57 | + self._keyboard = lv.keyboard(parent) |
| 58 | + |
| 59 | + # Configure layouts |
| 60 | + self._setup_layouts() |
| 61 | + |
| 62 | + # Set default mode to lowercase |
| 63 | + self._keyboard.set_mode(self.MODE_LOWERCASE) |
| 64 | + |
| 65 | + # Add event handler for custom behavior |
| 66 | + self._keyboard.add_event_cb(self._handle_events, lv.EVENT.VALUE_CHANGED, None) |
| 67 | + |
| 68 | + # Apply theme fix for light mode visibility |
| 69 | + mpos.ui.theme.fix_keyboard_button_style(self._keyboard) |
| 70 | + |
| 71 | + # Set reasonable default height |
| 72 | + self._keyboard.set_style_min_height(145, 0) |
| 73 | + |
| 74 | + def _setup_layouts(self): |
| 75 | + """Configure all keyboard layout modes.""" |
| 76 | + |
| 77 | + # Lowercase letters |
| 78 | + lowercase_map = [ |
| 79 | + "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "\n", |
| 80 | + "a", "s", "d", "f", "g", "h", "j", "k", "l", "\n", |
| 81 | + lv.SYMBOL.UP, "z", "x", "c", "v", "b", "n", "m", lv.SYMBOL.BACKSPACE, "\n", |
| 82 | + self.LABEL_NUMBERS_SPECIALS, ",", self.LABEL_SPACE, ".", lv.SYMBOL.NEW_LINE, None |
| 83 | + ] |
| 84 | + lowercase_ctrl = [10] * len(lowercase_map) |
| 85 | + self._keyboard.set_map(self.MODE_LOWERCASE, lowercase_map, lowercase_ctrl) |
| 86 | + |
| 87 | + # Uppercase letters |
| 88 | + uppercase_map = [ |
| 89 | + "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "\n", |
| 90 | + "A", "S", "D", "F", "G", "H", "J", "K", "L", "\n", |
| 91 | + lv.SYMBOL.DOWN, "Z", "X", "C", "V", "B", "N", "M", lv.SYMBOL.BACKSPACE, "\n", |
| 92 | + self.LABEL_NUMBERS_SPECIALS, ",", self.LABEL_SPACE, ".", lv.SYMBOL.NEW_LINE, None |
| 93 | + ] |
| 94 | + uppercase_ctrl = [10] * len(uppercase_map) |
| 95 | + self._keyboard.set_map(self.MODE_UPPERCASE, uppercase_map, uppercase_ctrl) |
| 96 | + |
| 97 | + # Numbers and common special characters |
| 98 | + numbers_map = [ |
| 99 | + "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "\n", |
| 100 | + "@", "#", "$", "_", "&", "-", "+", "(", ")", "/", "\n", |
| 101 | + self.LABEL_SPECIALS, "*", "\"", "'", ":", ";", "!", "?", lv.SYMBOL.BACKSPACE, "\n", |
| 102 | + self.LABEL_LETTERS, ",", self.LABEL_SPACE, ".", lv.SYMBOL.NEW_LINE, None |
| 103 | + ] |
| 104 | + numbers_ctrl = [10] * len(numbers_map) |
| 105 | + self._keyboard.set_map(self.MODE_NUMBERS, numbers_map, numbers_ctrl) |
| 106 | + |
| 107 | + # Additional special characters with emoticons |
| 108 | + specials_map = [ |
| 109 | + "~", "`", "|", "•", ":-)", ";-)", ":-D", "\n", |
| 110 | + ":-(" , ":'-(", "^", "°", "=", "{", "}", "\\", "\n", |
| 111 | + self.LABEL_NUMBERS_SPECIALS, ":-o", ":-P", "[", "]", lv.SYMBOL.BACKSPACE, "\n", |
| 112 | + self.LABEL_LETTERS, "<", self.LABEL_SPACE, ">", lv.SYMBOL.NEW_LINE, None |
| 113 | + ] |
| 114 | + specials_ctrl = [10] * len(specials_map) |
| 115 | + self._keyboard.set_map(self.MODE_SPECIALS, specials_map, specials_ctrl) |
| 116 | + |
| 117 | + def _handle_events(self, event): |
| 118 | + """ |
| 119 | + Handle keyboard button presses. |
| 120 | +
|
| 121 | + Args: |
| 122 | + event: LVGL event object |
| 123 | + """ |
| 124 | + # Get the pressed button and its text |
| 125 | + button = self._keyboard.get_selected_button() |
| 126 | + text = self._keyboard.get_button_text(button) |
| 127 | + |
| 128 | + # Get current textarea content |
| 129 | + ta = self._keyboard.get_textarea() |
| 130 | + if not ta: |
| 131 | + return |
| 132 | + |
| 133 | + current_text = ta.get_text() |
| 134 | + new_text = current_text |
| 135 | + |
| 136 | + # Handle special keys |
| 137 | + if text == lv.SYMBOL.BACKSPACE: |
| 138 | + # Delete last character |
| 139 | + new_text = current_text[:-1] |
| 140 | + |
| 141 | + elif text == lv.SYMBOL.UP: |
| 142 | + # Switch to uppercase |
| 143 | + self._keyboard.set_mode(self.MODE_UPPERCASE) |
| 144 | + return # Don't modify text |
| 145 | + |
| 146 | + elif text == lv.SYMBOL.DOWN or text == self.LABEL_LETTERS: |
| 147 | + # Switch to lowercase |
| 148 | + self._keyboard.set_mode(self.MODE_LOWERCASE) |
| 149 | + return # Don't modify text |
| 150 | + |
| 151 | + elif text == self.LABEL_NUMBERS_SPECIALS: |
| 152 | + # Switch to numbers/specials |
| 153 | + self._keyboard.set_mode(self.MODE_NUMBERS) |
| 154 | + return # Don't modify text |
| 155 | + |
| 156 | + elif text == self.LABEL_SPECIALS: |
| 157 | + # Switch to additional specials |
| 158 | + self._keyboard.set_mode(self.MODE_SPECIALS) |
| 159 | + return # Don't modify text |
| 160 | + |
| 161 | + elif text == self.LABEL_SPACE: |
| 162 | + # Space bar |
| 163 | + new_text = current_text + " " |
| 164 | + |
| 165 | + elif text == lv.SYMBOL.NEW_LINE: |
| 166 | + # Handle newline (only for multi-line textareas) |
| 167 | + if ta.get_one_line(): |
| 168 | + # For single-line, trigger READY event |
| 169 | + self._keyboard.send_event(lv.EVENT.READY, None) |
| 170 | + return |
| 171 | + else: |
| 172 | + new_text = current_text + "\n" |
| 173 | + |
| 174 | + else: |
| 175 | + # Regular character |
| 176 | + new_text = current_text + text |
| 177 | + |
| 178 | + # Update textarea |
| 179 | + ta.set_text(new_text) |
| 180 | + |
| 181 | + # ======================================================================== |
| 182 | + # LVGL keyboard-compatible API |
| 183 | + # ======================================================================== |
| 184 | + |
| 185 | + def set_textarea(self, textarea): |
| 186 | + """Set the textarea that this keyboard should edit.""" |
| 187 | + self._keyboard.set_textarea(textarea) |
| 188 | + |
| 189 | + def get_textarea(self): |
| 190 | + """Get the currently associated textarea.""" |
| 191 | + return self._keyboard.get_textarea() |
| 192 | + |
| 193 | + def set_mode(self, mode): |
| 194 | + """Set keyboard mode (use MODE_* constants).""" |
| 195 | + self._keyboard.set_mode(mode) |
| 196 | + |
| 197 | + def align(self, align_type, x_offset=0, y_offset=0): |
| 198 | + """Align the keyboard.""" |
| 199 | + self._keyboard.align(align_type, x_offset, y_offset) |
| 200 | + |
| 201 | + def set_style_min_height(self, height, selector): |
| 202 | + """Set minimum height.""" |
| 203 | + self._keyboard.set_style_min_height(height, selector) |
| 204 | + |
| 205 | + def set_style_height(self, height, selector): |
| 206 | + """Set height.""" |
| 207 | + self._keyboard.set_style_height(height, selector) |
| 208 | + |
| 209 | + def set_style_max_height(self, height, selector): |
| 210 | + """Set maximum height.""" |
| 211 | + self._keyboard.set_style_max_height(height, selector) |
| 212 | + |
| 213 | + def set_style_opa(self, opacity, selector): |
| 214 | + """Set opacity (required for fade animations).""" |
| 215 | + self._keyboard.set_style_opa(opacity, selector) |
| 216 | + |
| 217 | + def get_x(self): |
| 218 | + """Get X position.""" |
| 219 | + return self._keyboard.get_x() |
| 220 | + |
| 221 | + def set_x(self, x): |
| 222 | + """Set X position.""" |
| 223 | + self._keyboard.set_x(x) |
| 224 | + |
| 225 | + def get_y(self): |
| 226 | + """Get Y position.""" |
| 227 | + return self._keyboard.get_y() |
| 228 | + |
| 229 | + def set_y(self, y): |
| 230 | + """Set Y position.""" |
| 231 | + self._keyboard.set_y(y) |
| 232 | + |
| 233 | + def set_pos(self, x, y): |
| 234 | + """Set position.""" |
| 235 | + self._keyboard.set_pos(x, y) |
| 236 | + |
| 237 | + def get_height(self): |
| 238 | + """Get height.""" |
| 239 | + return self._keyboard.get_height() |
| 240 | + |
| 241 | + def get_width(self): |
| 242 | + """Get width.""" |
| 243 | + return self._keyboard.get_width() |
| 244 | + |
| 245 | + def add_flag(self, flag): |
| 246 | + """Add object flag (e.g., HIDDEN).""" |
| 247 | + self._keyboard.add_flag(flag) |
| 248 | + |
| 249 | + def remove_flag(self, flag): |
| 250 | + """Remove object flag.""" |
| 251 | + self._keyboard.remove_flag(flag) |
| 252 | + |
| 253 | + def has_flag(self, flag): |
| 254 | + """Check if object has flag.""" |
| 255 | + return self._keyboard.has_flag(flag) |
| 256 | + |
| 257 | + def add_event_cb(self, callback, event_code, user_data): |
| 258 | + """Add event callback.""" |
| 259 | + self._keyboard.add_event_cb(callback, event_code, user_data) |
| 260 | + |
| 261 | + def remove_event_cb(self, callback): |
| 262 | + """Remove event callback.""" |
| 263 | + self._keyboard.remove_event_cb(callback) |
| 264 | + |
| 265 | + def send_event(self, event_code, param): |
| 266 | + """Send event to keyboard.""" |
| 267 | + self._keyboard.send_event(event_code, param) |
| 268 | + |
| 269 | + def get_lvgl_obj(self): |
| 270 | + """ |
| 271 | + Get the underlying LVGL keyboard object. |
| 272 | +
|
| 273 | + Use this if you need direct access to LVGL methods not wrapped here. |
| 274 | + """ |
| 275 | + return self._keyboard |
| 276 | + |
| 277 | + |
| 278 | +def create_keyboard(parent, custom=False): |
| 279 | + """ |
| 280 | + Factory function to create a keyboard. |
| 281 | +
|
| 282 | + This provides a simple way to switch between standard LVGL keyboard |
| 283 | + and custom keyboard. |
| 284 | +
|
| 285 | + Args: |
| 286 | + parent: Parent LVGL object |
| 287 | + custom: If True, create CustomKeyboard; if False, create standard lv.keyboard |
| 288 | +
|
| 289 | + Returns: |
| 290 | + CustomKeyboard instance or lv.keyboard instance |
| 291 | +
|
| 292 | + Example: |
| 293 | + # Use custom keyboard |
| 294 | + keyboard = create_keyboard(screen, custom=True) |
| 295 | +
|
| 296 | + # Use standard LVGL keyboard |
| 297 | + keyboard = create_keyboard(screen, custom=False) |
| 298 | + """ |
| 299 | + if custom: |
| 300 | + return CustomKeyboard(parent) |
| 301 | + else: |
| 302 | + keyboard = lv.keyboard(parent) |
| 303 | + mpos.ui.theme.fix_keyboard_button_style(keyboard) |
| 304 | + return keyboard |
0 commit comments