FabGL
ESP32 Display Controller and Graphics Library
inputbox.cpp
1 /*
2  Created by Fabrizio Di Vittorio (fdivitto2013@gmail.com) - <http://www.fabgl.com>
3  Copyright (c) 2019-2021 Fabrizio Di Vittorio.
4  All rights reserved.
5 
6 
7 * Please contact fdivitto2013@gmail.com if you need a commercial license.
8 
9 
10 * This library and related software is available under GPL v3. Feel free to use FabGL in free software and hardware:
11 
12  FabGL is free software: you can redistribute it and/or modify
13  it under the terms of the GNU General Public License as published by
14  the Free Software Foundation, either version 3 of the License, or
15  (at your option) any later version.
16 
17  FabGL is distributed in the hope that it will be useful,
18  but WITHOUT ANY WARRANTY; without even the implied warranty of
19  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20  GNU General Public License for more details.
21 
22  You should have received a copy of the GNU General Public License
23  along with FabGL. If not, see <http://www.gnu.org/licenses/>.
24  */
25 
26 
27 #include <string.h>
28 
29 #include "inputbox.h"
30 
31 
32 #pragma GCC optimize ("O2")
33 
34 
35 namespace fabgl {
36 
37 
38 
40 // InputBox
41 
43  : m_vga16Ctrl(nullptr),
44  m_backgroundColor(RGB888(64, 64, 64))
45 {
46 }
47 
48 
49 InputBox::~InputBox()
50 {
51  end();
52 }
53 
54 
55 void InputBox::begin(char const * modeline, int viewPortWidth, int viewPortHeight)
56 {
57  // setup display controller
58  m_vga16Ctrl = new VGA16Controller;
59  m_dispCtrl = m_vga16Ctrl;
60  m_vga16Ctrl->begin();
61  m_vga16Ctrl->setResolution(modeline ? modeline : VESA_640x480_75Hz, viewPortWidth, viewPortHeight);
62 
63  // setup keyboard and mouse
64  if (!PS2Controller::initialized())
66 }
67 
68 
70 {
71  m_dispCtrl = displayController;
72 }
73 
74 
76 {
77  if (m_vga16Ctrl) {
78  m_vga16Ctrl->end();
79  delete m_vga16Ctrl;
80  m_vga16Ctrl = nullptr;
81  }
82 }
83 
84 
85 InputResult InputBox::textInput(char const * titleText, char const * labelText, char * inOutString, int maxLength, char const * buttonCancelText, char const * buttonOKText, bool passwordMode)
86 {
87  TextInputApp app;
88  app.backgroundColor = m_backgroundColor;
89  app.titleText = titleText;
90  app.labelText = labelText;
91  app.inOutString = inOutString;
92  app.maxLength = maxLength;
93  app.buttonCancelText = buttonCancelText;
94  app.buttonOKText = buttonOKText;
95  app.passwordMode = passwordMode;
96  app.autoOK = 0;
97 
98  app.run(m_dispCtrl);
99 
100  return app.retval;
101 }
102 
103 
104 InputResult InputBox::message(char const * titleText, char const * messageText, char const * buttonCancelText, char const * buttonOKText)
105 {
106  MessageApp app;
107  app.backgroundColor = m_backgroundColor;
108  app.titleText = titleText;
109  app.messageText = messageText;
110  app.buttonCancelText = buttonCancelText;
111  app.buttonOKText = buttonOKText;
112  app.autoOK = 0;
113 
114  app.run(m_dispCtrl);
115 
116  return app.retval;
117 }
118 
119 
120 InputResult InputBox::messageFmt(char const * titleText, char const * buttonCancelText, char const * buttonOKText, const char * format, ...)
121 {
122  auto r = InputResult::Cancel;
123  va_list ap;
124  va_start(ap, format);
125  int size = vsnprintf(nullptr, 0, format, ap) + 1;
126  if (size > 0) {
127  va_end(ap);
128  va_start(ap, format);
129  char buf[size + 1];
130  vsnprintf(buf, size, format, ap);
131  r = message(titleText, buf, buttonCancelText, buttonOKText);
132  }
133  va_end(ap);
134  return r;
135 }
136 
137 
138 int InputBox::select(char const * titleText, char const * messageText, char const * itemsText, char separator, char const * buttonCancelText, char const * buttonOKText, int OKAfter)
139 {
140  SelectApp app;
141  app.backgroundColor = m_backgroundColor;
142  app.titleText = titleText;
143  app.messageText = messageText;
144  app.items = itemsText;
145  app.separator = separator;
146  app.itemsList = nullptr;
147  app.buttonCancelText = buttonCancelText;
148  app.buttonOKText = buttonOKText;
149  app.menuMode = false;
150  app.autoOK = OKAfter;
151 
152  app.run(m_dispCtrl);
153 
154  return app.outSelected;
155 }
156 
157 
158 InputResult InputBox::select(char const * titleText, char const * messageText, StringList * items, char const * buttonCancelText, char const * buttonOKText, int OKAfter)
159 {
160  SelectApp app;
161  app.backgroundColor = m_backgroundColor;
162  app.titleText = titleText;
163  app.messageText = messageText;
164  app.items = nullptr;
165  app.separator = 0;
166  app.itemsList = items;
167  app.buttonCancelText = buttonCancelText;
168  app.buttonOKText = buttonOKText;
169  app.menuMode = false;
170  app.autoOK = OKAfter;
171 
172  app.run(m_dispCtrl);
173 
174  return app.retval;
175 }
176 
177 
178 int InputBox::menu(char const * titleText, char const * messageText, char const * itemsText, char separator)
179 {
180  SelectApp app;
181  app.backgroundColor = m_backgroundColor;
182  app.titleText = titleText;
183  app.messageText = messageText;
184  app.items = itemsText;
185  app.separator = separator;
186  app.itemsList = nullptr;
187  app.buttonCancelText = nullptr;
188  app.buttonOKText = nullptr;
189  app.menuMode = true;
190  app.autoOK = 0;
191 
192  app.run(m_dispCtrl);
193 
194  return app.outSelected;
195 }
196 
197 
198 int InputBox::menu(char const * titleText, char const * messageText, StringList * items)
199 {
200  SelectApp app;
201  app.backgroundColor = m_backgroundColor;
202  app.titleText = titleText;
203  app.messageText = messageText;
204  app.items = nullptr;
205  app.separator = 0;
206  app.itemsList = items;
207  app.buttonCancelText = nullptr;
208  app.buttonOKText = nullptr;
209  app.menuMode = true;
210  app.autoOK = 0;
211 
212  app.run(m_dispCtrl);
213 
214  return items->getFirstSelected();
215 }
216 
217 
218 InputResult InputBox::progressBoxImpl(ProgressApp & app, char const * titleText, char const * buttonCancelText, bool hasProgressBar, int width)
219 {
220  app.backgroundColor = m_backgroundColor;
221  app.titleText = titleText;
222  app.buttonCancelText = buttonCancelText;
223  app.buttonOKText = nullptr;
224  app.hasProgressBar = hasProgressBar;
225  app.width = width;
226  app.autoOK = 0;
227 
228  app.run(m_dispCtrl);
229 
230  return app.retval;
231 }
232 
233 
234 
236 // InputApp
237 
238 
239 void InputApp::init()
240 {
241  retval = InputResult::None;
242 
243  rootWindow()->frameStyle().backgroundColor = backgroundColor;
244  font = &FONT_std_14;
245 
246  const int titleHeight = titleText && strlen(titleText) ? font->height : 0;
247  const bool buttonsExist = buttonCancelText || buttonOKText;
248  const int buttonCancelExtent = buttonCancelText ? canvas()->textExtent(font, buttonCancelText) + 10 : 0;
249  const int buttonOKExtent = buttonOKText ? canvas()->textExtent(font, buttonOKText) + 10 : 0;
250  const int buttonsWidth = imax(imax(buttonCancelExtent, buttonOKExtent), 40);
251  const int totButtons = (buttonCancelExtent ? 1 : 0) + (buttonOKExtent ? 1 : 0);
252  const int buttonsHeight = buttonsExist ? font->height + 6 : 0;
253  constexpr int buttonsSpace = 10;
254 
255  requiredWidth = buttonsWidth * totButtons + (2 * buttonsSpace) * totButtons;
256  requiredHeight = buttonsHeight + titleHeight + font->height * 2 + 5;
257 
258  calcRequiredSize();
259 
260  requiredWidth = imin(requiredWidth, canvas()->getWidth());
261 
262  mainFrame = new uiFrame(rootWindow(), titleText, UIWINDOW_PARENTCENTER, Size(requiredWidth, requiredHeight), false);
263  mainFrame->frameProps().resizeable = false;
264  mainFrame->frameProps().hasMaximizeButton = false;
265  mainFrame->frameProps().hasMinimizeButton = false;
266  mainFrame->frameProps().hasCloseButton = false;
267  mainFrame->onKeyUp = [&](uiKeyEventInfo key) {
268  if (key.VK == VK_RETURN || key.VK == VK_KP_ENTER) {
269  retval = InputResult::Enter;
270  finalize();
271  } else if (key.VK == VK_ESCAPE) {
272  retval = InputResult::Cancel;
273  finalize();
274  }
275  };
276 
277  autoOKLabel = nullptr;
278 
279  uiWindow * controlToFocus = nullptr;
280 
281  if (buttonsExist) {
282 
283  // setup panel (where buttons are positioned)
284 
285  int panelHeight = buttonsHeight + 10;
286  panel = new uiPanel(mainFrame, Point(mainFrame->clientPos().X - 1, mainFrame->clientPos().Y + mainFrame->clientSize().height - panelHeight), Size(mainFrame->clientSize().width + 2, panelHeight));
287  panel->windowStyle().borderColor = RGB888(128, 128, 128);
288  panel->panelStyle().backgroundColor = mainFrame->frameStyle().backgroundColor;
289 
290  // setup buttons
291 
292  int y = (panelHeight - buttonsHeight) / 2;
293  int x = panel->clientSize().width - buttonsWidth * totButtons - buttonsSpace * totButtons; // right aligned
294 
295  if (buttonCancelText) {
296  auto buttonCancel = new uiButton(panel, buttonCancelText, Point(x, y), Size(buttonsWidth, buttonsHeight));
297  buttonCancel->onClick = [&]() {
298  retval = InputResult::Cancel;
299  finalize();
300  };
301  x += buttonsWidth + buttonsSpace;
302  controlToFocus = buttonCancel;
303  }
304 
305  if (buttonOKText) {
306  auto buttonOK = new uiButton(panel, buttonOKText, Point(x, y), Size(buttonsWidth, buttonsHeight));
307  buttonOK->onClick = [&]() {
308  retval = InputResult::Enter;
309  finalize();
310  };
311  controlToFocus = buttonOK;
312  }
313 
314  if (autoOK > 0) {
315  autoOKLabel = new uiLabel(panel, "", Point(4, y + 2));
316 
317  onTimer = [&](uiTimerHandle t) {
318  int now = esp_timer_get_time() / 1000;
319  if (lastUserActionTime() + 900 > now) {
320  killTimer(t);
321  destroyWindow(autoOKLabel);
322  return;
323  }
324  if (autoOK <= 0) {
325  killTimer(t);
326  retval = InputResult::Enter;
327  finalize();
328  }
329  --autoOK;
330  autoOKLabel->setTextFmt("%d", autoOK);
331  };
332  setTimer(this, 1000);
333 
334  }
335 
336  } else {
337  panel = nullptr;
338  }
339 
340  addControls();
341 
342  showWindow(mainFrame, true);
343  setActiveWindow(mainFrame);
344  if (controlToFocus)
345  setFocusedWindow(controlToFocus);
346 }
347 
348 
349 
351 // TextInputApp
352 
353 
354 void TextInputApp::calcRequiredSize()
355 {
356  labelExtent = canvas()->textExtent(font, labelText);
357  editExtent = imin(maxLength * canvas()->textExtent(font, "M"), rootWindow()->clientSize().width / 2 - labelExtent);
358  requiredWidth = imax(requiredWidth, editExtent + labelExtent + 10);
359  requiredHeight += font->height;
360 }
361 
362 
363 void TextInputApp::addControls()
364 {
365  const Point clientPos = mainFrame->clientPos();
366 
367  int x = clientPos.X + 4;
368  int y = clientPos.Y + 8;
369 
370  new uiLabel(mainFrame, labelText, Point(x, y));
371 
372  edit = new uiTextEdit(mainFrame, inOutString, Point(x + labelExtent + 5, y - 4), Size(editExtent - 15, font->height + 6));
373  edit->textEditProps().passwordMode = passwordMode;
374 
375  mainFrame->onShow = [&]() { setFocusedWindow(edit); };
376 }
377 
378 
379 void TextInputApp::finalize()
380 {
381  if (retval == InputResult::Enter) {
382  int len = imin(maxLength, strlen(edit->text()));
383  memcpy(inOutString, edit->text(), len);
384  inOutString[len] = 0;
385  }
386  quit(0);
387 }
388 
389 
390 
392 // MessageApp
393 
394 
395 void MessageApp::calcRequiredSize()
396 {
397  messageExtent = canvas()->textExtent(font, messageText);
398  requiredWidth = imax(requiredWidth, messageExtent + 20);
399  requiredHeight += font->height;
400 }
401 
402 
403 void MessageApp::addControls()
404 {
405  int x = mainFrame->clientPos().X + (mainFrame->clientSize().width - messageExtent) / 2;
406  int y = mainFrame->clientPos().Y + 6;
407 
408  new uiLabel(mainFrame, messageText, Point(x, y));
409 }
410 
411 
412 void MessageApp::finalize()
413 {
414  quit(0);
415 }
416 
417 
418 
420 // SelectApp
421 
422 
423 void SelectApp::calcRequiredSize()
424 {
425  auto messageExtent = canvas()->textExtent(font, messageText);
426  requiredWidth = imax(requiredWidth, messageExtent + 20);
427 
428  // calc space for message
429  requiredHeight += font->height;
430 
431  // calc space for list box
432  size_t maxLength;
433  auto itemsCount = countItems(&maxLength);
434  listBoxHeight = (font->height + 4) * itemsCount;
435  int requiredHeightUnCut = requiredHeight + listBoxHeight;
436  requiredHeight = imin(requiredHeightUnCut, canvas()->getHeight());
437  requiredWidth = imax(requiredWidth, maxLength * canvas()->textExtent(font, "M"));
438  if (requiredHeightUnCut > requiredHeight)
439  listBoxHeight -= requiredHeightUnCut - requiredHeight;
440 }
441 
442 
443 void SelectApp::addControls()
444 {
445  int x = mainFrame->clientPos().X + 4;
446  int y = mainFrame->clientPos().Y + 6;
447 
448  new uiLabel(mainFrame, messageText, Point(x, y));
449 
450  y += font->height + 6;
451 
452  listBox = new uiListBox(mainFrame, Point(x, y), Size(mainFrame->clientSize().width - 10, listBoxHeight));
453  if (items) {
454  listBox->items().appendSepList(items, separator);
455  } else {
456  listBox->items().copyFrom(*itemsList);
457  listBox->items().copySelectionMapFrom(*itemsList);
458  }
459  if (menuMode) {
460  listBox->listBoxProps().allowMultiSelect = false;
461  listBox->listBoxProps().selectOnMouseOver = true;
462  listBox->onClick = [&]() {
463  retval = InputResult::Enter;
464  finalize();
465  };
466  }
467 
468  mainFrame->onShow = [&]() { setFocusedWindow(listBox); };
469 }
470 
471 
472 void SelectApp::finalize()
473 {
474  if (items) {
475  outSelected = (retval == InputResult::Enter ? listBox->firstSelectedItem() : -1);
476  } else {
477  itemsList->copySelectionMapFrom(listBox->items());
478  }
479  quit(0);
480 }
481 
482 
483 int SelectApp::countItems(size_t * maxLength)
484 {
485  *maxLength = 0;
486  int count = 0;
487  if (items) {
488  char const * start = items;
489  while (*start) {
490  auto end = strchr(start, separator);
491  if (!end)
492  end = strchr(start, 0);
493  int len = end - start;
494  *maxLength = imax(*maxLength, len);
495  start += len + (*end == 0 ? 0 : 1);
496  ++count;
497  }
498  } else if (itemsList) {
499  for (int i = 0; i < itemsList->count(); ++i)
500  *maxLength = imax(*maxLength, strlen(itemsList->get(i)));
501  count += itemsList->count();
502  }
503  return count;
504 }
505 
506 
507 
509 // ProgressApp
510 
511 
512 void ProgressApp::calcRequiredSize()
513 {
514  requiredWidth = imax(requiredWidth, width);
515  requiredHeight += font->height + (hasProgressBar ? progressBarHeight : 0);
516 }
517 
518 
519 void ProgressApp::addControls()
520 {
521  int x = mainFrame->clientPos().X + 4;
522  int y = mainFrame->clientPos().Y + 6;
523 
524  label = new uiLabel(mainFrame, "", Point(x, y));
525 
526  if (hasProgressBar) {
527  y += font->height + 4;
528 
529  progressBar = new uiProgressBar(mainFrame, Point(x, y), Size(mainFrame->clientSize().width - 8, font->height));
530  }
531 
532  mainFrame->onShow = [&]() {
533  execFunc(this);
534  if (retval != InputResult::Cancel)
535  retval = InputResult::Enter;
536  quit(0);
537  };
538 }
539 
540 
541 void ProgressApp::finalize()
542 {
543 }
544 
545 
546 // return True if not Abort
547 bool ProgressApp::update(int percentage, char const * format, ...)
548 {
549  if (hasProgressBar)
550  progressBar->setPercentage(percentage);
551 
552  va_list ap;
553  va_start(ap, format);
554  int size = vsnprintf(nullptr, 0, format, ap) + 1;
555  if (size > 0) {
556  va_end(ap);
557  va_start(ap, format);
558  char buf[size + 1];
559  vsnprintf(buf, size, format, ap);
560  label->setText(buf);
561  }
562  va_end(ap);
563 
564  processEvents();
565  return retval == InputResult::None;
566 }
567 
568 
569 } // namespace fabgl
Represents a 24 bit RGB color.
void begin(char const *modeline=nullptr, int viewPortWidth=-1, int viewPortHeight=-1)
Initializes InputBox from VGA modeline, using a VGA16Controller.
Definition: inputbox.cpp:55
This file contains the InputBox class.
int select(char const *titleText, char const *messageText, char const *itemsText, char separator=';', char const *buttonCancelText="Cancel", char const *buttonOKText="OK", int OKAfter=0)
Shows a dialog with a label and a list box.
Definition: inputbox.cpp:138
void end()
Cleanup resources and eventually disable VGA output.
Definition: inputbox.cpp:75
InputBox()
Creates a new InputBox instance.
Definition: inputbox.cpp:42
Represents the base abstract class for bitmapped display controllers.
InputResult
Result of InputBox dialogs helper class.
Definition: inputbox.h:56
Definition: canvas.cpp:36
Represents the VGA 16 colors bitmapped controller.
InputResult textInput(char const *titleText, char const *labelText, char *inOutString, int maxLength, char const *buttonCancelText="Cancel", char const *buttonOKText="OK", bool passwordMode=false)
Shows a dialog with a label and a text edit box.
Definition: inputbox.cpp:85
#define VESA_640x480_75Hz
Definition: fabglconf.h:252
int menu(char const *titleText, char const *messageText, char const *itemsText, char separator=';')
Shows a dialog with a label and a list box. The dialog exits when an item is selected, just like a menu.
Definition: inputbox.cpp:178
InputResult message(char const *titleText, char const *messageText, char const *buttonCancelText=nullptr, char const *buttonOKText="OK")
Shows a dialog with just a label.
Definition: inputbox.cpp:104
uint8_t width
InputResult messageFmt(char const *titleText, char const *buttonCancelText, char const *buttonOKText, const char *format,...)
Shows a dialog with a just a label. Allows printf like formatted text.
Definition: inputbox.cpp:120
static void begin(gpio_num_t port0_clkGPIO, gpio_num_t port0_datGPIO, gpio_num_t port1_clkGPIO=GPIO_UNUSED, gpio_num_t port1_datGPIO=GPIO_UNUSED)
Initializes PS2 device controller.