| 1 | /* |
| 2 | |
| 3 | mui.c |
| 4 | |
| 5 | Monochrome minimal user interface: Core library. |
| 6 | |
| 7 | Universal 8bit Graphics Library (https://github.com/olikraus/u8g2/) |
| 8 | |
| 9 | Copyright (c) 2021, olikraus@gmail.com |
| 10 | All rights reserved. |
| 11 | |
| 12 | Redistribution and use in source and binary forms, with or without |
| 13 | modification, are permitted provided that the following conditions are met: |
| 14 | |
| 15 | * Redistributions of source code must retain the above copyright notice, this |
| 16 | list of conditions and the following disclaimer. |
| 17 | |
| 18 | * Redistributions in binary form must reproduce the above copyright notice, |
| 19 | this list of conditions and the following disclaimer in the documentation |
| 20 | and/or other materials provided with the distribution. |
| 21 | |
| 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
| 23 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
| 24 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
| 25 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| 26 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR |
| 27 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| 28 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
| 29 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| 30 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| 31 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
| 32 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| 33 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
| 34 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 35 | |
| 36 | |
| 37 | |
| 38 | "mui.c" is a graphical user interface, developed as part of u8g2. |
| 39 | However "mui.c" is independent of u8g2 and can be used without u8g2 code. |
| 40 | The glue code between "mui.c" and u8g2 is located in "mui_u8g2.c" |
| 41 | |
| 42 | c: cmd |
| 43 | i: ID0 |
| 44 | j: ID1 |
| 45 | xy: Position (x and y) |
| 46 | /text/: some text. The text can start with any delimiter (except 0 and |), but |
| 47 | also has to end with the same delimiter a: Single char argument u: Single char |
| 48 | argument with the user interface form number |
| 49 | |
| 50 | "Uu" the interface --> no |
| 51 | ID |
| 52 | |
| 53 | Manual ID: |
| 54 | "Fijxy" Generic field: Places field with id ii at x/y --> ID=ij |
| 55 | "Bijxy/text/" Generic field (Button) with Text --> ID=ij |
| 56 | "Tiixya/text/" Generic field with argument and text --> ID = ij |
| 57 | "Aiixya" |
| 58 | |
| 59 | Fixed ID: |
| 60 | "Si" the style --> |
| 61 | ID=@i "Lxy/labeltext/" Places a text at the specified position, field with - |
| 62 | -> ID=.L, .l "Gxyu/menutext/" Go to the specified menu without placing the |
| 63 | user interface form on the stack --> ID=.G, .g |
| 64 | |
| 65 | |
| 66 | cijxy |
| 67 | cijxy/text/ |
| 68 | cijxya/text/ |
| 69 | |
| 70 | cxy/text/ |
| 71 | cxya/text/ |
| 72 | |
| 73 | */ |
| 74 | |
| 75 | #include "mui.h" |
| 76 | |
| 77 | // #define mui_get_fds_char(s) ((uint8_t)(*s)) |
| 78 | |
| 79 | // #include <stdio.h> |
| 80 | // #define MUI_DEBUG(...) printf(__VA_ARGS__) |
| 81 | #define MUI_DEBUG(...) |
| 82 | |
| 83 | uint8_t mui_get_fds_char(fds_t *s) { |
| 84 | // return (uint8_t)(*s); |
| 85 | return (uint8_t)mui_pgm_read(s); |
| 86 | } |
| 87 | |
| 88 | /* |
| 89 | s must point to a valid command within FDS |
| 90 | */ |
| 91 | static size_t mui_fds_get_cmd_size_without_text(fds_t *s) MUI_NOINLINE; |
| 92 | static size_t mui_fds_get_cmd_size_without_text(fds_t *s) { |
| 93 | uint8_t c = mui_get_fds_char(s); |
| 94 | c &= 0xdf; /* consider upper and lower case */ |
| 95 | switch (c) { |
| 96 | case 'U': |
| 97 | return 2; // User Form: CMD (1 Byte), Form-Id (1 Byte) |
| 98 | case 'S': |
| 99 | return 2; // Style: CMD (1 Byte), Style Id (1 Byte) |
| 100 | case 'D': |
| 101 | return 3; // Data within Text: CMD (1 Byte), ID (2 Bytes), Text (does |
| 102 | // not count here) |
| 103 | case 'Z': |
| 104 | return 3; // Zero field without x, y, arg & text: CMD (1 Byte), ID (2 |
| 105 | // Bytes) |
| 106 | case 'F': |
| 107 | return 5; // Field without arg & text: CMD (1 Byte), ID (2 Bytes), X, Y |
| 108 | case 'B': |
| 109 | return 5; // Field with text: CMD (1 Byte), ID (2 Bytes), X, Y, Text |
| 110 | // (does not count here) |
| 111 | case 'T': |
| 112 | return 6; // Field with arg & text: CMD (1 Byte), ID (2 Bytes), X, Y, |
| 113 | // Arg, Text (does not count here) |
| 114 | case 'A': |
| 115 | return 6; // Field with arg (no text): CMD (1 Byte), ID (2 Bytes), X, Y, |
| 116 | // Arg, Text |
| 117 | case 'L': |
| 118 | return 3; // Text Label: CMD (1 Byte), X, Y (same as 'B' but with fixed |
| 119 | // ID '.L', MUIF_LABEL, MUI_LABEL) |
| 120 | case 'G': |
| 121 | return 4; // Goto Btutton: CMD (1Byte), X, Y, Arg, Text (same as 'T' |
| 122 | // but with fixed ID '.G', MUIF_GOTO, MUI_GOTO) |
| 123 | case 0: |
| 124 | return 0; |
| 125 | } |
| 126 | return 1; |
| 127 | } |
| 128 | |
| 129 | /* |
| 130 | s must point to the string delimiter start: first '/' for "B00ab/ok/" |
| 131 | - '/' actually is 0xff |
| 132 | - return the total size of the string, including the delimiter |
| 133 | - copies the content of the string ("ok") to the ui text buffer |
| 134 | |
| 135 | */ |
| 136 | static size_t mui_fds_parse_text(mui_t *ui, fds_t *s) { |
| 137 | uint8_t i = 0; |
| 138 | ui->delimiter = mui_get_fds_char(s); |
| 139 | uint8_t c; |
| 140 | fds_t *t = s; |
| 141 | |
| 142 | // printf("mui_fds_parse_text del=%d\n", delimiter); |
| 143 | #ifdef MUI_CHECK_EOFDS |
| 144 | if (ui->delimiter == 0) |
| 145 | return 0; |
| 146 | #endif |
| 147 | t++; |
| 148 | for (;;) { |
| 149 | c = mui_get_fds_char(t); |
| 150 | // printf("mui_fds_parse_text i=%d, c=%c\n", i, c); |
| 151 | #ifdef MUI_CHECK_EOFDS |
| 152 | if (c == 0) |
| 153 | break; |
| 154 | #endif |
| 155 | if (c == ui->delimiter) { |
| 156 | t++; |
| 157 | break; |
| 158 | } |
| 159 | if (i < MUI_MAX_TEXT_LEN) { |
| 160 | ui->text[i++] = c; |
| 161 | } |
| 162 | t++; |
| 163 | } |
| 164 | ui->text[i] = '\0'; |
| 165 | return t - s; |
| 166 | } |
| 167 | |
| 168 | /* |
| 169 | get the first token within a text argument. |
| 170 | The text argument may look like this: |
| 171 | "B00ab/banana|apple|peach|cherry/" |
| 172 | The outer delimiter "/" is not fixed and can be any char except "|" and "\0" |
| 173 | The inner delimiter "|" is fixed. It must be the pipe symbol. |
| 174 | This function will place "banana" into ui->text if the result is not 0 |
| 175 | |
| 176 | if ( mui_fds_first_token(ui) ) |
| 177 | { |
| 178 | do |
| 179 | { |
| 180 | // handle token in ui->text |
| 181 | } while ( mui_fds_next_token(ui) ) |
| 182 | } |
| 183 | |
| 184 | */ |
| 185 | |
| 186 | uint8_t mui_fds_first_token(mui_t *ui) { |
| 187 | ui->token = ui->fds; |
| 188 | ui->token += mui_fds_get_cmd_size_without_text(ui->fds); |
| 189 | ui->delimiter = mui_get_fds_char(ui->token); |
| 190 | ui->token++; // place ui->token on the first char of the token |
| 191 | return mui_fds_next_token(ui); |
| 192 | } |
| 193 | |
| 194 | /* |
| 195 | The inner token delimiter "|" is fixed. It must be the pipe symbol. |
| 196 | */ |
| 197 | uint8_t mui_fds_next_token(mui_t *ui) { |
| 198 | uint8_t c; |
| 199 | uint8_t i = 0; |
| 200 | // printf("mui_fds_next_token: call, ui->token=%p\n", ui->token); |
| 201 | for (;;) { |
| 202 | c = mui_get_fds_char(ui->token); |
| 203 | // printf("mui_fds_next_token: i=%d c=%c\n", i, c); |
| 204 | #ifdef MUI_CHECK_EOFDS |
| 205 | if (c == 0) |
| 206 | break; |
| 207 | #endif |
| 208 | if (c == ui->delimiter) |
| 209 | break; |
| 210 | if (c == '|') { |
| 211 | ui->token++; // place ui->token on the first char of the next token |
| 212 | break; |
| 213 | } |
| 214 | |
| 215 | if (i < MUI_MAX_TEXT_LEN) { |
| 216 | ui->text[i++] = c; |
| 217 | } |
| 218 | |
| 219 | ui->token++; |
| 220 | } |
| 221 | ui->text[i] = '\0'; |
| 222 | if (i == 0) |
| 223 | return 0; // no further token found |
| 224 | return 1; // token placed in ui->text |
| 225 | } |
| 226 | |
| 227 | /* |
| 228 | find nth token ('|' delimiter), return 0 if n exceeds the number of tokens, 1 |
| 229 | otherwise the result is stored in ui->text |
| 230 | */ |
| 231 | uint8_t mui_fds_get_nth_token(mui_t *ui, uint8_t n) { |
| 232 | // printf("mui_fds_get_nth_token: call, n=%d\n", n); |
| 233 | if (mui_fds_first_token(ui)) { |
| 234 | do { |
| 235 | if (n == 0) { |
| 236 | // printf("mui_fds_get_nth_token: found"); |
| 237 | return 1; |
| 238 | } |
| 239 | n--; |
| 240 | } while (mui_fds_next_token(ui)); |
| 241 | } |
| 242 | // printf("mui_fds_get_nth_token: NOT found\n"); |
| 243 | return 0; |
| 244 | } |
| 245 | |
| 246 | uint8_t mui_fds_get_token_cnt(mui_t *ui) { |
| 247 | uint8_t n = 0; |
| 248 | if (mui_fds_first_token(ui)) { |
| 249 | do { |
| 250 | n++; |
| 251 | } while (mui_fds_next_token(ui)); |
| 252 | } |
| 253 | return n; |
| 254 | } |
| 255 | |
| 256 | #define mui_fds_is_text(c) \ |
| 257 | ((c) == 'U' || (c) == 'S' || (c) == 'F' || (c) == 'A' || (c) == 'Z' ? 0 : 1) |
| 258 | |
| 259 | /* |
| 260 | s must point to a valid command within FDS |
| 261 | return |
| 262 | The complete length of the command (including any text part) |
| 263 | sideeffect: |
| 264 | Any existing text part will be copied into ui->text |
| 265 | ui->text will be assigned to empty string if there is no text argument |
| 266 | */ |
| 267 | static size_t mui_fds_get_cmd_size(mui_t *ui, fds_t *s) MUI_NOINLINE; |
| 268 | static size_t mui_fds_get_cmd_size(mui_t *ui, fds_t *s) { |
| 269 | size_t l = mui_fds_get_cmd_size_without_text(s); |
| 270 | uint8_t c = mui_get_fds_char(s); |
| 271 | ui->text[0] = '\0'; /* always reset the text buffer */ |
| 272 | if (mui_fds_is_text(c)) { |
| 273 | l += mui_fds_parse_text(ui, s + l); |
| 274 | } |
| 275 | return l; |
| 276 | } |
| 277 | |
| 278 | /* |
| 279 | mui_Init() will setup the menu system but will not activate or display |
| 280 | anything. Use mui_GotoForm() after this command, then use mui_Draw() to draw |
| 281 | the menu on a display. |
| 282 | */ |
| 283 | void mui_Init(mui_t *ui, void *graphics_data, fds_t *fds, muif_t *muif_tlist, |
| 284 | size_t muif_tcnt) { |
| 285 | memset(ui, 0, sizeof(mui_t)); |
| 286 | ui->root_fds = fds; |
| 287 | // ui->current_form_fds = NULL; // not required, because there was a |
| 288 | // memset before |
| 289 | ui->muif_tlist = muif_tlist; |
| 290 | ui->muif_tcnt = muif_tcnt; |
| 291 | ui->graphics_data = graphics_data; |
| 292 | ui->last_form_stack_pos = -1; |
| 293 | } |
| 294 | |
| 295 | static int mui_find_uif(mui_t *ui, uint8_t id0, uint8_t id1) { |
| 296 | size_t i; |
| 297 | for (i = 0; i < ui->muif_tcnt; i++) { |
| 298 | /* |
| 299 | if ( ui->muif_tlist[i].id0 == id0 ) |
| 300 | if ( ui->muif_tlist[i].id1 == id1 ) |
| 301 | return i; |
| 302 | */ |
| 303 | if (muif_get_id0(ui->muif_tlist + i) == id0) |
| 304 | if (muif_get_id1(ui->muif_tlist + i) == id1) |
| 305 | return i; |
| 306 | } |
| 307 | return -1; |
| 308 | } |
| 309 | |
| 310 | /* |
| 311 | assumes a valid position in ui->fds and calculates all the other variables |
| 312 | some fields are always calculated like the ui->cmd and ui->len field |
| 313 | other member vars are calculated only if the return value is 1 |
| 314 | will return 1 if the field id was found. |
| 315 | will return 0 if the field id was not found in uif or if ui->fds points to |
| 316 | something else than a field |
| 317 | */ |
| 318 | static uint8_t mui_prepare_current_field(mui_t *ui) MUI_NOINLINE; |
| 319 | static uint8_t mui_prepare_current_field(mui_t *ui) { |
| 320 | int muif_tidx; |
| 321 | |
| 322 | ui->uif = NULL; |
| 323 | ui->dflags = 0; |
| 324 | ui->id0 = 0; |
| 325 | ui->id1 = 0; |
| 326 | ui->arg = 0; |
| 327 | |
| 328 | /* calculate the length of the command and copy the text argument */ |
| 329 | /* this will also clear the text in cases where there is no text argument */ |
| 330 | ui->len = mui_fds_get_cmd_size(ui, ui->fds); |
| 331 | // printf("mui_prepare_current_field len=%d\n", ui->len); |
| 332 | |
| 333 | /* get the command and check whether end of form is reached */ |
| 334 | ui->cmd = mui_get_fds_char(ui->fds); |
| 335 | // printf("mui_prepare_current_field cmd='%c' len=%d\n", ui->cmd, ui->len); |
| 336 | |
| 337 | /* Copy the cmd also to second id value. This is required for some commands, |
| 338 | * others will overwrite this below */ |
| 339 | ui->id1 = ui->cmd; |
| 340 | |
| 341 | /* now make the command uppercase so that both, upper and lower case are |
| 342 | * considered */ |
| 343 | ui->cmd &= 0xdf; /* consider upper and lower case */ |
| 344 | |
| 345 | if (ui->cmd == 'U' || ui->cmd == 0) |
| 346 | return 0; |
| 347 | |
| 348 | /* calculate the dynamic flags */ |
| 349 | if (ui->fds == ui->cursor_focus_fds) |
| 350 | ui->dflags |= MUIF_DFLAG_IS_CURSOR_FOCUS; |
| 351 | if (ui->fds == ui->touch_focus_fds) |
| 352 | ui->dflags |= MUIF_DFLAG_IS_TOUCH_FOCUS; |
| 353 | |
| 354 | /* get the id0 and id1 values */ |
| 355 | if (ui->cmd == 'F' || ui->cmd == 'B' || ui->cmd == 'T' || ui->cmd == 'A') { |
| 356 | ui->id0 = mui_get_fds_char(ui->fds + 1); |
| 357 | ui->id1 = mui_get_fds_char(ui->fds + 2); |
| 358 | ui->x = mui_get_fds_char(ui->fds + 3); |
| 359 | ui->y = mui_get_fds_char(ui->fds + 4); |
| 360 | if (ui->cmd == 'A' || ui->cmd == 'T') { |
| 361 | ui->arg = mui_get_fds_char(ui->fds + 5); |
| 362 | } |
| 363 | } else if (ui->cmd == 'D' || ui->cmd == 'Z') { |
| 364 | ui->id0 = mui_get_fds_char(ui->fds + 1); |
| 365 | ui->id1 = mui_get_fds_char(ui->fds + 2); |
| 366 | } else if (ui->cmd == 'S') { |
| 367 | ui->id0 = 'S'; |
| 368 | ui->id1 = mui_get_fds_char(ui->fds + 1); |
| 369 | } else { |
| 370 | ui->id0 = '.'; |
| 371 | /* note that ui->id1 contains the original cmd value */ |
| 372 | ui->x = mui_get_fds_char(ui->fds + 1); |
| 373 | ui->y = mui_get_fds_char(ui->fds + 2); |
| 374 | if (ui->cmd == 'G' || |
| 375 | ui->cmd == 'M') /* this is also true for 'g' or 'm' */ |
| 376 | { |
| 377 | ui->arg = mui_get_fds_char(ui->fds + 3); |
| 378 | } |
| 379 | } |
| 380 | |
| 381 | // MUI_DEBUG("mui_prepare_current_field cmd='%c' len=%d arg=%d\n", ui->cmd, |
| 382 | // ui->len, ui->arg); |
| 383 | |
| 384 | /* find the field */ |
| 385 | muif_tidx = mui_find_uif(ui, ui->id0, ui->id1); |
| 386 | // printf("mui_prepare_current_field: muif_tidx=%d\n", muif_tidx); |
| 387 | if (muif_tidx >= 0) { |
| 388 | ui->uif = ui->muif_tlist + muif_tidx; |
| 389 | return 1; |
| 390 | } |
| 391 | return 0; |
| 392 | } |
| 393 | |
| 394 | /* |
| 395 | assumes that ui->fds has been assigned correctly |
| 396 | and that ui->target_fds and ui->tmp_fds had been cleared if required |
| 397 | |
| 398 | Usually do not call this function directly, instead use mui_loop_over_form |
| 399 | |
| 400 | */ |
| 401 | |
| 402 | static void mui_inner_loop_over_form(mui_t *ui, |
| 403 | uint8_t (*task)(mui_t *ui)) MUI_NOINLINE; |
| 404 | static void mui_inner_loop_over_form(mui_t *ui, uint8_t (*task)(mui_t *ui)) { |
| 405 | uint8_t cmd; |
| 406 | |
| 407 | // MUI_DEBUG("mui_inner_loop_over_form start %p\n", task); |
| 408 | |
| 409 | ui->fds += mui_fds_get_cmd_size( |
| 410 | ui, ui->fds); // skip the first entry, it is U always |
| 411 | for (;;) { |
| 412 | // printf("fds=%p *fds='%c'\n", ui->fds, ui->fds[0]); |
| 413 | /* get the command and check whether end of form is reached */ |
| 414 | cmd = mui_get_fds_char(ui->fds); |
| 415 | if (cmd == 'U' || cmd == 0) |
| 416 | break; |
| 417 | if (mui_prepare_current_field(ui)) /* side effect: calculate ui->len */ |
| 418 | if (task(ui)) /* call the task, which was provided as argument to |
| 419 | this function */ |
| 420 | { |
| 421 | // MUI_DEBUG("mui_inner_loop_over_form break by task\n"); |
| 422 | break; |
| 423 | } |
| 424 | ui->fds += ui->len; |
| 425 | } |
| 426 | |
| 427 | // MUI_DEBUG("mui_inner_loop_over_form end %p\n", task); |
| 428 | } |
| 429 | |
| 430 | static void mui_loop_over_form(mui_t *ui, |
| 431 | uint8_t (*task)(mui_t *ui)) MUI_NOINLINE; |
| 432 | static void mui_loop_over_form(mui_t *ui, uint8_t (*task)(mui_t *ui)) { |
| 433 | if (mui_IsFormActive(ui) == 0) |
| 434 | return; |
| 435 | |
| 436 | ui->fds = ui->current_form_fds; |
| 437 | ui->target_fds = NULL; |
| 438 | ui->tmp_fds = NULL; |
| 439 | |
| 440 | mui_inner_loop_over_form(ui, task); |
| 441 | } |
| 442 | |
| 443 | /* |
| 444 | n is the form number |
| 445 | */ |
| 446 | static fds_t *mui_find_form(mui_t *ui, uint8_t n) { |
| 447 | fds_t *fds = ui->root_fds; |
| 448 | uint8_t cmd; |
| 449 | |
| 450 | for (;;) { |
| 451 | cmd = mui_get_fds_char(fds); |
| 452 | if (cmd == 0) |
| 453 | break; |
| 454 | if (cmd == 'U') { |
| 455 | if (mui_get_fds_char(fds + 1) == n) { |
| 456 | return fds; |
| 457 | } |
| 458 | /* not found, just coninue */ |
| 459 | } |
| 460 | |
| 461 | fds += mui_fds_get_cmd_size(ui, fds); |
| 462 | } |
| 463 | return NULL; |
| 464 | } |
| 465 | |
| 466 | /* === task procedures (arguments for mui_loop_over_form) === */ |
| 467 | /* ui->fds contains the current field */ |
| 468 | |
| 469 | static uint8_t mui_task_draw(mui_t *ui) { |
| 470 | // printf("mui_task_draw fds=%p uif=%p text=%s\n", ui->fds, ui->uif, |
| 471 | // ui->text); |
| 472 | muif_get_cb(ui->uif)(ui, MUIF_MSG_DRAW); |
| 473 | return 0; /* continue with the loop */ |
| 474 | } |
| 475 | |
| 476 | static uint8_t mui_task_form_start(mui_t *ui) { |
| 477 | muif_get_cb(ui->uif)(ui, MUIF_MSG_FORM_START); |
| 478 | return 0; /* continue with the loop */ |
| 479 | } |
| 480 | |
| 481 | static uint8_t mui_task_form_end(mui_t *ui) { |
| 482 | muif_get_cb(ui->uif)(ui, MUIF_MSG_FORM_END); |
| 483 | return 0; /* continue with the loop */ |
| 484 | } |
| 485 | |
| 486 | static uint8_t mui_uif_is_cursor_selectable(mui_t *ui) MUI_NOINLINE; |
| 487 | static uint8_t mui_uif_is_cursor_selectable(mui_t *ui) { |
| 488 | if (muif_get_cflags(ui->uif) & MUIF_CFLAG_IS_CURSOR_SELECTABLE) { |
| 489 | return 1; |
| 490 | } |
| 491 | return 0; |
| 492 | } |
| 493 | |
| 494 | static uint8_t mui_task_find_prev_cursor_uif(mui_t *ui) { |
| 495 | // if ( muif_get_cflags(ui->uif) & MUIF_CFLAG_IS_CURSOR_SELECTABLE ) |
| 496 | if (mui_uif_is_cursor_selectable(ui)) { |
| 497 | if (ui->fds == ui->cursor_focus_fds) { |
| 498 | ui->target_fds = ui->tmp_fds; |
| 499 | return 1; /* stop looping */ |
| 500 | } |
| 501 | ui->tmp_fds = ui->fds; |
| 502 | } |
| 503 | return 0; /* continue with the loop */ |
| 504 | } |
| 505 | |
| 506 | static uint8_t mui_task_find_first_cursor_uif(mui_t *ui) { |
| 507 | // if ( muif_get_cflags(ui->uif) & MUIF_CFLAG_IS_CURSOR_SELECTABLE ) |
| 508 | if (mui_uif_is_cursor_selectable(ui)) { |
| 509 | // if ( ui->target_fds == NULL ) |
| 510 | // { |
| 511 | ui->target_fds = ui->fds; |
| 512 | return 1; /* stop looping */ |
| 513 | // } |
| 514 | } |
| 515 | return 0; /* continue with the loop */ |
| 516 | } |
| 517 | |
| 518 | static uint8_t mui_task_find_last_cursor_uif(mui_t *ui) { |
| 519 | // if ( muif_get_cflags(ui->uif) & MUIF_CFLAG_IS_CURSOR_SELECTABLE ) |
| 520 | if (mui_uif_is_cursor_selectable(ui)) { |
| 521 | // ui->cursor_focus_position++; |
| 522 | ui->target_fds = ui->fds; |
| 523 | } |
| 524 | return 0; /* continue with the loop */ |
| 525 | } |
| 526 | |
| 527 | static uint8_t mui_task_find_next_cursor_uif(mui_t *ui) { |
| 528 | // if ( muif_get_cflags(ui->uif) & MUIF_CFLAG_IS_CURSOR_SELECTABLE ) |
| 529 | if (mui_uif_is_cursor_selectable(ui)) { |
| 530 | if (ui->tmp_fds != NULL) { |
| 531 | ui->target_fds = ui->fds; |
| 532 | ui->tmp_fds = NULL; |
| 533 | return 1; /* stop looping */ |
| 534 | } |
| 535 | if (ui->fds == ui->cursor_focus_fds) { |
| 536 | ui->tmp_fds = ui->fds; |
| 537 | } |
| 538 | } |
| 539 | return 0; /* continue with the loop */ |
| 540 | } |
| 541 | |
| 542 | static uint8_t mui_task_get_current_cursor_focus_position(mui_t *ui) { |
| 543 | // if ( muif_get_cflags(ui->uif) & MUIF_CFLAG_IS_CURSOR_SELECTABLE ) |
| 544 | if (mui_uif_is_cursor_selectable(ui)) { |
| 545 | if (ui->fds == ui->cursor_focus_fds) |
| 546 | return 1; /* stop looping */ |
| 547 | ui->tmp8++; |
| 548 | } |
| 549 | return 0; /* continue with the loop */ |
| 550 | } |
| 551 | |
| 552 | #ifdef OBSOLETE |
| 553 | static uint8_t mui_task_read_nth_selectable_field(mui_t *ui) { |
| 554 | // if ( muif_get_cflags(ui->uif) & MUIF_CFLAG_IS_CURSOR_SELECTABLE ) |
| 555 | if (mui_uif_is_cursor_selectable(ui)) { |
| 556 | if (ui->tmp8 == 0) |
| 557 | return 1; /* stop looping */ |
| 558 | ui->tmp8--; |
| 559 | } |
| 560 | return 0; /* continue with the loop */ |
| 561 | } |
| 562 | #endif |
| 563 | |
| 564 | static uint8_t mui_task_find_execute_on_select_field(mui_t *ui) { |
| 565 | if (muif_get_cflags(ui->uif) & MUIF_CFLAG_IS_EXECUTE_ON_SELECT) { |
| 566 | ui->target_fds = ui->fds; |
| 567 | return 1; /* stop looping */ |
| 568 | } |
| 569 | return 0; /* continue with the loop */ |
| 570 | } |
| 571 | |
| 572 | /* === utility functions for the user API === */ |
| 573 | |
| 574 | static uint8_t mui_send_cursor_msg(mui_t *ui, uint8_t msg) MUI_NOINLINE; |
| 575 | static uint8_t mui_send_cursor_msg(mui_t *ui, uint8_t msg) { |
| 576 | if (ui->cursor_focus_fds) { |
| 577 | ui->fds = ui->cursor_focus_fds; |
| 578 | if (mui_prepare_current_field(ui)) |
| 579 | return muif_get_cb(ui->uif)(ui, msg); |
| 580 | } |
| 581 | return 0; /* not called, msg not handled */ |
| 582 | } |
| 583 | |
| 584 | /* === user API === */ |
| 585 | |
| 586 | /* |
| 587 | returns the field pos which has the current focus |
| 588 | If the first selectable field has the focus, then 0 will be returned |
| 589 | Unselectable fields (for example labels) are skipped by this count. |
| 590 | If no fields are selectable, then 0 is returned |
| 591 | |
| 592 | The return value can be used as last argument for mui_EnterForm or |
| 593 | mui_GotoForm |
| 594 | |
| 595 | WARNING: This function will destroy current fds and field information. |
| 596 | */ |
| 597 | uint8_t mui_GetCurrentCursorFocusPosition(mui_t *ui) { |
| 598 | // fds_t *fds = ui->fds; |
| 599 | ui->tmp8 = 0; |
| 600 | mui_loop_over_form(ui, mui_task_get_current_cursor_focus_position); |
| 601 | // ui->fds = fds; |
| 602 | return ui->tmp8; |
| 603 | } |
| 604 | |
| 605 | void mui_Draw(mui_t *ui) { mui_loop_over_form(ui, mui_task_draw); } |
| 606 | |
| 607 | static void mui_next_field(mui_t *ui) { |
| 608 | mui_loop_over_form(ui, mui_task_find_next_cursor_uif); |
| 609 | // ui->cursor_focus_position++; |
| 610 | ui->cursor_focus_fds = ui->target_fds; // NULL is ok |
| 611 | if (ui->target_fds == NULL) { |
| 612 | mui_loop_over_form(ui, mui_task_find_first_cursor_uif); |
| 613 | ui->cursor_focus_fds = ui->target_fds; // NULL is ok |
| 614 | // ui->cursor_focus_position = 0; |
| 615 | } |
| 616 | } |
| 617 | |
| 618 | /* |
| 619 | this function will overwrite the ui field related member variables |
| 620 | nth_token can be 0 if the fiel text is not a option list |
| 621 | the result is stored in ui->text |
| 622 | |
| 623 | token delimiter is '|' (pipe symbol) |
| 624 | |
| 625 | fds: The start of a field (MUI_DATA) |
| 626 | nth_token: The position of the token, which should be returned |
| 627 | */ |
| 628 | uint8_t mui_GetSelectableFieldTextOption(mui_t *ui, fds_t *fds, |
| 629 | uint8_t nth_token) { |
| 630 | if (fds != NULL) { |
| 631 | fds_t *fds_backup = |
| 632 | ui->fds; // backup the current fds, so that this function can be |
| 633 | // called inside a task loop |
| 634 | int len = ui->len; // backup length of the current command, 26 sep 2021: |
| 635 | // probably this is not required any more |
| 636 | uint8_t is_found; |
| 637 | |
| 638 | ui->fds = fds; |
| 639 | // at this point ui->fds contains the field which contains the tokens |
| 640 | // now get the opion string out of the text field. nth_token can be 0 if |
| 641 | // this is no opion string |
| 642 | is_found = mui_fds_get_nth_token( |
| 643 | ui, nth_token); // return value is ignored here |
| 644 | |
| 645 | ui->fds = fds_backup; // restore the previous fds position |
| 646 | ui->len = len; |
| 647 | // result is stored in ui->text |
| 648 | return is_found; |
| 649 | } |
| 650 | ui->text[0] = '\0'; |
| 651 | return 0; |
| 652 | } |
| 653 | |
| 654 | uint8_t mui_GetSelectableFieldOptionCnt(mui_t *ui, fds_t *fds) { |
| 655 | if (fds != NULL) { |
| 656 | fds_t *fds_backup = |
| 657 | ui->fds; // backup the current fds, so that this function can be |
| 658 | // called inside a task loop |
| 659 | int len = ui->len; // backup length of the current command 26 sep |
| 660 | // 2021: probably this is not required any more |
| 661 | uint8_t cnt = 0; |
| 662 | |
| 663 | ui->fds = fds; |
| 664 | // at this point ui->fds contains the field which contains the tokens |
| 665 | // now get the opion string out of the text field. nth_token can be 0 if |
| 666 | // this is no opion string |
| 667 | cnt = mui_fds_get_token_cnt(ui); |
| 668 | |
| 669 | ui->fds = fds_backup; // restore the previous fds position |
| 670 | ui->len = len; |
| 671 | // result is stored in ui->text |
| 672 | return cnt; |
| 673 | } |
| 674 | ui->text[0] = '\0'; |
| 675 | return 0; |
| 676 | } |
| 677 | |
| 678 | // static void mui_send_cursor_enter_msg(mui_t *ui) MUI_NOINLINE; |
| 679 | static uint8_t mui_send_cursor_enter_msg(mui_t *ui) { |
| 680 | ui->is_mud = 0; |
| 681 | return mui_send_cursor_msg(ui, MUIF_MSG_CURSOR_ENTER); |
| 682 | } |
| 683 | |
| 684 | /* |
| 685 | if called from a field function, then the current field variables are |
| 686 | destroyed, so that call should be the last call in the field callback. |
| 687 | mui_EnterForm is similar to mui_GotoForm and differes only in the second |
| 688 | argument (which is the form id instead of the fds pointer) |
| 689 | */ |
| 690 | void mui_EnterForm(mui_t *ui, fds_t *fds, uint8_t initial_cursor_position) { |
| 691 | /* exit any previous form, will not do anything if there is no current form |
| 692 | */ |
| 693 | mui_LeaveForm(ui); |
| 694 | |
| 695 | /* clean focus fields */ |
| 696 | ui->touch_focus_fds = NULL; |
| 697 | ui->cursor_focus_fds = NULL; |
| 698 | |
| 699 | /* reset all the scoll values */ |
| 700 | ui->form_scroll_top = 0; |
| 701 | ui->form_scroll_visible = 0; |
| 702 | ui->form_scroll_total = 0; |
| 703 | |
| 704 | /* assign the form, which should be entered */ |
| 705 | ui->current_form_fds = fds; |
| 706 | |
| 707 | /* inform all fields that we start a new form */ |
| 708 | MUI_DEBUG("mui_EnterForm: form_start, initial_cursor_position=%d\n", |
| 709 | initial_cursor_position); |
| 710 | mui_loop_over_form(ui, mui_task_form_start); |
| 711 | |
| 712 | /* assign initional cursor focus */ |
| 713 | MUI_DEBUG("mui_EnterForm: find_first_cursor_uif\n"); |
| 714 | mui_loop_over_form(ui, mui_task_find_first_cursor_uif); |
| 715 | ui->cursor_focus_fds = ui->target_fds; // NULL is ok |
| 716 | MUI_DEBUG("mui_EnterForm: find_first_cursor_uif target_fds=%p\n", |
| 717 | ui->target_fds); |
| 718 | |
| 719 | while (initial_cursor_position > 0) { |
| 720 | mui_NextField( |
| 721 | ui); // mui_next_field(ui) is not sufficient in case of scrolling |
| 722 | initial_cursor_position--; |
| 723 | } |
| 724 | |
| 725 | while (mui_send_cursor_enter_msg(ui) == 255) { |
| 726 | mui_NextField( |
| 727 | ui); // mui_next_field(ui) is not sufficient in case of scrolling |
| 728 | } |
| 729 | } |
| 730 | |
| 731 | /* input: current_form_fds */ |
| 732 | /* |
| 733 | if called from a field function, then the current field variables are |
| 734 | destroyed, so that call should be the last call in the field callback. |
| 735 | */ |
| 736 | void mui_LeaveForm(mui_t *ui) { |
| 737 | if (mui_IsFormActive(ui) == 0) |
| 738 | return; |
| 739 | |
| 740 | mui_send_cursor_msg(ui, MUIF_MSG_CURSOR_LEAVE); |
| 741 | ui->cursor_focus_fds = NULL; |
| 742 | |
| 743 | /* inform all fields that we leave the form */ |
| 744 | MUI_DEBUG("mui_LeaveForm: form_end\n"); |
| 745 | mui_loop_over_form(ui, mui_task_form_end); |
| 746 | ui->current_form_fds = NULL; |
| 747 | } |
| 748 | |
| 749 | /* 0: error, form not found */ |
| 750 | /* |
| 751 | if called from a field function, then the current field variables are |
| 752 | destroyed, so that call should be the last call in the field callback. |
| 753 | */ |
| 754 | uint8_t mui_GotoForm(mui_t *ui, uint8_t form_id, |
| 755 | uint8_t initial_cursor_position) { |
| 756 | fds_t *fds = mui_find_form(ui, form_id); |
| 757 | if (fds == NULL) |
| 758 | return 0; |
| 759 | /* EnterForm will also leave any previous form */ |
| 760 | mui_EnterForm(ui, fds, initial_cursor_position); |
| 761 | return 1; |
| 762 | } |
| 763 | |
| 764 | /* |
| 765 | Save current form+cursor position. Used together with mui_RestoreForm |
| 766 | */ |
| 767 | void mui_SaveFormWithCursorPosition(mui_t *ui, uint8_t cursor_pos) { |
| 768 | if (mui_IsFormActive(ui) == 0) |
| 769 | return; |
| 770 | |
| 771 | if (ui->last_form_stack_pos < MUI_MENU_LAST_FORM_STACK_SIZE - 1) |
| 772 | ui->last_form_stack_pos++; |
| 773 | else // discard the oldes entry |
| 774 | { |
| 775 | uint8_t i; |
| 776 | for (i = 0; i < ui->last_form_stack_pos; i++) { |
| 777 | ui->last_form_id[i] = ui->last_form_id[i + 1]; |
| 778 | ui->last_form_cursor_focus_position[i] = |
| 779 | ui->last_form_cursor_focus_position[i + 1]; |
| 780 | } |
| 781 | } |
| 782 | |
| 783 | ui->last_form_fds = |
| 784 | ui->cursor_focus_fds; // 25 Aug 2024: I think this is not required, u8g2 |
| 785 | // data function will store the value manually in |
| 786 | // last_form_fds |
| 787 | ui->last_form_id[ui->last_form_stack_pos] = |
| 788 | mui_get_fds_char(ui->current_form_fds + 1); |
| 789 | ui->last_form_cursor_focus_position[ui->last_form_stack_pos] = cursor_pos; |
| 790 | } |
| 791 | |
| 792 | /* |
| 793 | Save current form+cursor position. Simplified version, which will not work |
| 794 | with pseudo scrolling forms |
| 795 | */ |
| 796 | void mui_SaveForm(mui_t *ui) { |
| 797 | mui_SaveFormWithCursorPosition(ui, mui_GetCurrentCursorFocusPosition(ui)); |
| 798 | } |
| 799 | |
| 800 | /* |
| 801 | Restore previous saved form and cursor position. |
| 802 | if called from a field function, then the current field variables are |
| 803 | destroyed, so that call should be the last call in the field callback. returns |
| 804 | 0 if there is no form to restore |
| 805 | */ |
| 806 | uint8_t mui_RestoreForm(mui_t *ui) { |
| 807 | MUI_DEBUG("mui_RestoreForm last_form_stack_pos=%d\n", |
| 808 | ui->last_form_stack_pos); |
| 809 | if (ui->last_form_stack_pos >= 0) { |
| 810 | uint8_t form_id = ui->last_form_id[ui->last_form_stack_pos]; |
| 811 | uint8_t focus = |
| 812 | ui->last_form_cursor_focus_position[ui->last_form_stack_pos]; |
| 813 | ui->last_form_stack_pos--; |
| 814 | return mui_GotoForm(ui, form_id, focus); |
| 815 | // ui->last_form_fds = NULL; |
| 816 | } |
| 817 | return 0; |
| 818 | } |
| 819 | |
| 820 | /* |
| 821 | Save a cursor position for mui_GotoFormAutoCursorPosition command |
| 822 | */ |
| 823 | void mui_SaveCursorPosition(mui_t *ui, uint8_t cursor_position) { |
| 824 | uint8_t form_id = mui_get_fds_char(ui->current_form_fds + 1); |
| 825 | uint8_t i; |
| 826 | MUI_DEBUG("mui_SaveCursorPosition form_id=%d cursor_position=%d\n", form_id, |
| 827 | cursor_position); |
| 828 | |
| 829 | for (i = 0; i < MUI_MENU_CACHE_CNT; i++) { |
| 830 | if (form_id == ui->menu_form_id[i]) { |
| 831 | ui->menu_form_last_added = i; |
| 832 | break; |
| 833 | } |
| 834 | } |
| 835 | if (i >= MUI_MENU_CACHE_CNT) { |
| 836 | ui->menu_form_last_added++; |
| 837 | if (ui->menu_form_last_added >= MUI_MENU_CACHE_CNT) |
| 838 | ui->menu_form_last_added = 0; |
| 839 | } |
| 840 | |
| 841 | /* |
| 842 | if ( form_id == ui->menu_form_id[0] ) |
| 843 | ui->menu_form_last_added = 0; |
| 844 | else if ( form_id == ui->menu_form_id[1] ) |
| 845 | ui->menu_form_last_added = 1; |
| 846 | else |
| 847 | ui->menu_form_last_added ^= 1; |
| 848 | */ |
| 849 | ui->menu_form_id[ui->menu_form_last_added] = form_id; |
| 850 | ui->menu_form_cursor_focus_position[ui->menu_form_last_added] = |
| 851 | cursor_position; |
| 852 | MUI_DEBUG("mui_SaveCursorPosition ui->menu_form_last_added=%d \n", |
| 853 | ui->menu_form_last_added); |
| 854 | } |
| 855 | |
| 856 | /* |
| 857 | Similar to mui_GotoForm, but will jump to previously stored cursor location |
| 858 | (mui_SaveCursorPosition) or 0 if the cursor position was not saved. |
| 859 | */ |
| 860 | uint8_t mui_GotoFormAutoCursorPosition(mui_t *ui, uint8_t form_id) { |
| 861 | uint8_t cursor_position = 0; |
| 862 | if (form_id == ui->menu_form_id[0]) |
| 863 | cursor_position = ui->menu_form_cursor_focus_position[0]; |
| 864 | if (form_id == ui->menu_form_id[1]) |
| 865 | cursor_position = ui->menu_form_cursor_focus_position[1]; |
| 866 | MUI_DEBUG("mui_GotoFormAutoCursorPosition form_id=%d cursor_position=%d\n", |
| 867 | form_id, cursor_position); |
| 868 | return mui_GotoForm(ui, form_id, cursor_position); |
| 869 | } |
| 870 | |
| 871 | /* |
| 872 | return current form id or -1 if the menu system is inactive |
| 873 | */ |
| 874 | int mui_GetCurrentFormId(mui_t *ui) { |
| 875 | if (mui_IsFormActive(ui) == 0) |
| 876 | return -1; |
| 877 | return mui_get_fds_char(ui->current_form_fds + 1); |
| 878 | } |
| 879 | |
| 880 | /* |
| 881 | updates "ui->cursor_focus_fds" |
| 882 | */ |
| 883 | /* |
| 884 | if called from a field function, then the current field variables are |
| 885 | destroyed, so that call should be the last call in the field callback. |
| 886 | */ |
| 887 | void mui_NextField(mui_t *ui) { |
| 888 | do { |
| 889 | if (mui_send_cursor_msg(ui, MUIF_MSG_EVENT_NEXT)) |
| 890 | return; |
| 891 | mui_send_cursor_msg(ui, MUIF_MSG_CURSOR_LEAVE); |
| 892 | mui_next_field(ui); |
| 893 | } while (mui_send_cursor_enter_msg(ui) == 255); |
| 894 | } |
| 895 | |
| 896 | /* |
| 897 | updates "ui->cursor_focus_fds" |
| 898 | */ |
| 899 | /* |
| 900 | if called from a field function, then the current field variables are |
| 901 | destroyed, so that call should be the last call in the field callback. |
| 902 | */ |
| 903 | void mui_PrevField(mui_t *ui) { |
| 904 | do { |
| 905 | if (mui_send_cursor_msg(ui, MUIF_MSG_EVENT_PREV)) |
| 906 | return; |
| 907 | mui_send_cursor_msg(ui, MUIF_MSG_CURSOR_LEAVE); |
| 908 | |
| 909 | mui_loop_over_form(ui, mui_task_find_prev_cursor_uif); |
| 910 | ui->cursor_focus_fds = ui->target_fds; // NULL is ok |
| 911 | if (ui->target_fds == NULL) { |
| 912 | // ui->cursor_focus_position = 0; |
| 913 | mui_loop_over_form(ui, mui_task_find_last_cursor_uif); |
| 914 | ui->cursor_focus_fds = ui->target_fds; // NULL is ok |
| 915 | } |
| 916 | } while (mui_send_cursor_enter_msg(ui) == 255); |
| 917 | } |
| 918 | |
| 919 | void mui_SendSelect(mui_t *ui) { |
| 920 | mui_send_cursor_msg(ui, MUIF_MSG_CURSOR_SELECT); |
| 921 | } |
| 922 | |
| 923 | /* |
| 924 | Same as mui_SendSelect(), but will try to find a field, which is marked as |
| 925 | "execute on select" (MUIF_EXECUTE_ON_SELECT_BUTTON). If such a field exists, |
| 926 | then this field is executed, otherwise the current field will receive the |
| 927 | select message. |
| 928 | |
| 929 | MUIF_EXECUTE_ON_SELECT_BUTTON is set by muif macro |
| 930 | MUIF_EXECUTE_ON_SELECT_BUTTON |
| 931 | |
| 932 | used by MUIInputVersatileRotaryEncoder.ino example |
| 933 | */ |
| 934 | void mui_SendSelectWithExecuteOnSelectFieldSearch(mui_t *ui) { |
| 935 | mui_loop_over_form( |
| 936 | ui, mui_task_find_execute_on_select_field); /* Is there a exec on select |
| 937 | field? */ |
| 938 | if (ui->target_fds != |
| 939 | NULL) /* yes, found, ui->fds already points to the field */ |
| 940 | { |
| 941 | fds_t *exec_on_select_field = ui->target_fds; |
| 942 | mui_send_cursor_msg(ui, MUIF_MSG_CURSOR_LEAVE); |
| 943 | ui->cursor_focus_fds = exec_on_select_field; /* more cursor on the "exec |
| 944 | on select" field */ |
| 945 | mui_send_cursor_enter_msg(ui); |
| 946 | mui_send_cursor_msg(ui, MUIF_MSG_CURSOR_SELECT); |
| 947 | } else { |
| 948 | /* no "exec on select" field found, just send the select message to the |
| 949 | * field */ |
| 950 | mui_send_cursor_msg(ui, MUIF_MSG_CURSOR_SELECT); |
| 951 | } |
| 952 | } |
| 953 | |
| 954 | void mui_SendValueIncrement(mui_t *ui) { |
| 955 | mui_send_cursor_msg(ui, MUIF_MSG_VALUE_INCREMENT); |
| 956 | } |
| 957 | |
| 958 | void mui_SendValueDecrement(mui_t *ui) { |
| 959 | mui_send_cursor_msg(ui, MUIF_MSG_VALUE_DECREMENT); |
| 960 | } |