Numworks Epsilon  1.4.1
Graphing Calculator Operating System
menu_controller.cpp
Go to the documentation of this file.
1 #include "menu_controller.h"
2 #include "../i18n.h"
3 #include "../apps_container.h"
4 #include <assert.h>
5 #include <escher/metric.h>
6 #include <ion/events.h>
7 
8 namespace Code {
9 
10 MenuController::MenuController(Responder * parentResponder, ScriptStore * scriptStore, ButtonRowController * footer
11 #if EPSILON_GETOPT
12  , bool lockOnConsole
13 #endif
14  ) :
15  ViewController(parentResponder),
16  ButtonRowDelegate(nullptr, footer),
17  m_scriptStore(scriptStore),
18  m_addNewScriptCell(I18n::Message::AddScript),
19  m_consoleButton(this, I18n::Message::Console, Invocation([](void * context, void * sender) {
20  MenuController * menu = (MenuController *)context;
21  if (menu->consoleController()->loadPythonEnvironment()) {
22  menu->stackViewController()->push(menu->consoleController());
23  return;
24  }
25  //TODO: Pop up warning message: not enough space to load Python
26  }, this), KDText::FontSize::Large),
27  m_selectableTableView(this, this, this, this),
28  m_consoleController(parentResponder, m_scriptStore
29 #if EPSILON_GETOPT
30  , lockOnConsole
31 #endif
32  ),
33  m_scriptParameterController(nullptr, I18n::Message::ScriptOptions, this),
34  m_editorController(this),
35  m_reloadConsoleWhenBecomingFirstResponder(false),
36  m_shouldDisplayAddScriptRow(true)
37 {
38  m_selectableTableView.setMargins(0);
39  m_selectableTableView.setShowsIndicators(false);
40  for (int i = 0; i < k_maxNumberOfDisplayableScriptCells; i++) {
41  m_scriptCells[i].setParentResponder(&m_selectableTableView);
42  m_scriptCells[i].editableTextCell()->textField()->setDelegate(this);
43  m_scriptCells[i].editableTextCell()->textField()->setDraftTextBuffer(m_draftTextBuffer);
44  m_scriptCells[i].editableTextCell()->textField()->setAlignment(0.0f, 0.5f);
45  m_scriptCells[i].editableTextCell()->setMargins(0, 0, 0, Metric::HistoryHorizontalMargin);
46  }
47 }
48 
50  return static_cast<StackViewController *>(parentResponder()->parentResponder());
51 }
52 
54  int selectedRow = m_selectableTableView.selectedRow();
55  int selectedColumn = m_selectableTableView.selectedColumn();
56  if (selectedRow >= 0 && selectedRow < m_scriptStore->numberOfScripts() && selectedColumn == 0) {
57  TextField * tf = static_cast<EvenOddEditableTextCell *>(m_selectableTableView.selectedCell())->editableTextCell()->textField();
58  if (tf->isEditing()) {
59  tf->setEditing(false);
60  textFieldDidAbortEditing(tf, tf->text());
61  }
62  }
63 }
64 
66  if (m_reloadConsoleWhenBecomingFirstResponder) {
67  reloadConsole();
68  }
69  if (footer()->selectedButton() == 0) {
70  assert(m_selectableTableView.selectedRow() < 0);
71  app()->setFirstResponder(&m_consoleButton);
72  return;
73  }
74  if (m_selectableTableView.selectedRow() < 0) {
75  m_selectableTableView.selectCellAtLocation(0,0);
76  }
77  assert(m_selectableTableView.selectedRow() < m_scriptStore->numberOfScripts() + 1);
78  app()->setFirstResponder(&m_selectableTableView);
79 #if EPSILON_GETOPT
80  if (consoleController()->locked() && consoleController()->loadPythonEnvironment()) {
82  return;
83  }
84 #endif
85 }
86 
88  updateAddScriptRowDisplay();
89 }
90 
92  if (event == Ion::Events::Down) {
93  m_selectableTableView.deselectTable();
95  return true;
96  }
97  if (event == Ion::Events::Up) {
98  if (footer()->selectedButton() == 0) {
100  m_selectableTableView.selectCellAtLocation(0, numberOfRows()-1);
101  app()->setFirstResponder(&m_selectableTableView);
102  return true;
103  }
104  }
105  if (event == Ion::Events::OK || event == Ion::Events::EXE) {
106  int selectedRow = m_selectableTableView.selectedRow();
107  int selectedColumn = m_selectableTableView.selectedColumn();
108  if (selectedRow >= 0 && selectedRow < m_scriptStore->numberOfScripts()) {
109  if (selectedColumn == 1) {
110  configureScript();
111  return true;
112  }
113  assert(selectedColumn == 0);
114  editScriptAtIndex(selectedRow);
115  return true;
116  } else if (m_shouldDisplayAddScriptRow
117  && selectedColumn == 0
118  && selectedRow == m_scriptStore->numberOfScripts())
119  {
120  addScript();
121  return true;
122  }
123  }
124  return false;
125 }
126 
128  assert(m_selectableTableView.selectedRow() >= 0);
129  assert(m_selectableTableView.selectedRow() < m_scriptStore->numberOfScripts());
131  m_selectableTableView.selectCellAtLocation(0, (m_selectableTableView.selectedRow()));
132  EvenOddEditableTextCell * myCell = static_cast<EvenOddEditableTextCell *>(m_selectableTableView.selectedCell());
133  app()->setFirstResponder(myCell);
134  myCell->setHighlighted(false);
135  const char * previousText = myCell->editableTextCell()->textField()->text();
136  myCell->editableTextCell()->textField()->setEditing(true);
137  myCell->editableTextCell()->textField()->setText(previousText);
138  myCell->editableTextCell()->textField()->setCursorLocation(strlen(previousText) - strlen(ScriptStore::k_scriptExtension));
139 }
140 
142  assert(!script.isNull());
143  script.destroy();
144  updateAddScriptRowDisplay();
145 }
146 
148  m_consoleController.unloadPythonEnvironment();
149  m_reloadConsoleWhenBecomingFirstResponder = false;
150 }
151 
153  m_consoleController.loadPythonEnvironment(false);
154 }
155 
157  reloadConsole();
158  if (m_consoleController.loadPythonEnvironment(false)) {
159  stackViewController()->push(&m_consoleController);
160  m_consoleController.autoImportScript(script, true);
161  }
162  m_reloadConsoleWhenBecomingFirstResponder = true;
163 }
164 
166  reloadConsole();
167 }
168 
170  return m_scriptStore->numberOfScripts() + m_shouldDisplayAddScriptRow;
171 }
172 
174  if (i == 0 && j < m_scriptStore->numberOfScripts()) {
176  }
177  static_cast<EvenOddCell *>(cell)->setEven(j%2 == 0);
178  cell->setHighlighted(i == selectedColumn() && j == selectedRow());
179 }
180 
182  switch (i) {
183  case 0:
184  return m_selectableTableView.bounds().width()-k_parametersColumnWidth;
185  case 1:
186  return k_parametersColumnWidth;
187  default:
188  assert(false);
189  return 0;
190  }
191 }
192 
194  switch (i) {
195  case 0:
196  return 0;
197  case 1:
198  return m_selectableTableView.bounds().width()-k_parametersColumnWidth;
199  case 2:
200  return m_selectableTableView.bounds().width();
201  default:
202  assert(false);
203  return 0;
204  }
205 }
206 
208  return Metric::StoreRowHeight * j;
209 }
210 
212  if (offsetX <= m_selectableTableView.bounds().width()-k_parametersColumnWidth) {
213  return 0;
214  }
215  if (offsetX <= m_selectableTableView.bounds().width()) {
216  return 1;
217  }
218  else {
219  return 2;
220  }
221  assert(false);
222  return 0;
223 }
224 
226  if (Metric::StoreRowHeight == 0) {
227  return 0;
228  }
229  return (offsetY - 1) / Metric::StoreRowHeight;
230 }
231 
233  assert(index >= 0);
234  if (type == ScriptCellType) {
235  assert(index >=0 && index < k_maxNumberOfDisplayableScriptCells);
236  return &m_scriptCells[index];
237  }
238  if (type == ScriptParameterCellType) {
239  assert(index >=0 && index < k_maxNumberOfDisplayableScriptCells);
240  return &m_scriptParameterCells[index];
241  }
242  if (type == AddScriptCellType) {
243  assert(index == 0);
244  return &m_addNewScriptCell;
245  }
246  if(type == EmptyCellType) {
247  return &m_emptyCell;
248  }
249  assert(false);
250  return nullptr;
251 }
252 
254  if (type == AddScriptCellType) {
255  return 1;
256  }
257  if (type == ScriptCellType || type == ScriptParameterCellType) {
258  return k_maxNumberOfDisplayableScriptCells;
259  }
260  if (type == EmptyCellType) {
261  return 1;
262  }
263  assert(false);
264  return 0;
265 }
266 
268  assert(i >= 0 && i < numberOfColumns());
269  assert(j >= 0 && j < numberOfRows());
270  if (i == 0) {
271  if (j == numberOfRows()-1 && m_shouldDisplayAddScriptRow) {
272  return AddScriptCellType;
273  }
274  return ScriptCellType;
275  }
276  assert(i == 1);
277  if (j == numberOfRows()-1 && m_shouldDisplayAddScriptRow) {
278  return EmptyCellType;
279  }
280  return ScriptParameterCellType;
281 }
282 
284  assert(index >= 0 && index < m_scriptStore->numberOfScripts());
285  EditableTextCell * editableTextCell = static_cast<EvenOddEditableTextCell *>(cell)->editableTextCell();
286  editableTextCell->textField()->setText(m_scriptStore->scriptAtIndex(index).name());
287 }
288 
289 void MenuController::tableViewDidChangeSelection(SelectableTableView * t, int previousSelectedCellX, int previousSelectedCellY) {
290  if (selectedRow() == numberOfRows() - 1 && selectedColumn() == 1 && m_shouldDisplayAddScriptRow) {
292  }
293 }
294 
296  return event == Ion::Events::OK || event == Ion::Events::EXE
297  || event == Ion::Events::Down || event == Ion::Events::Up;
298 }
299 
301  if (event == Ion::Events::Right && textField->isEditing() && textField->cursorLocation() == textField->draftTextLength()) {
302  return true;
303  }
304  if (event == Ion::Events::Clear && textField->isEditing()) {
306  textField->setCursorLocation(0);
307  return true;
308  }
309  return false;
310 }
311 
312 bool MenuController::textFieldDidFinishEditing(TextField * textField, const char * text, Ion::Events::Event event) {
313  const char * newName;
314  char numberedDefaultName[k_defaultScriptNameMaxSize];
316  // The user entered an empty name. Use a numbered default script name.
317  numberedDefaultScriptName(numberedDefaultName);
318  newName = const_cast<const char *>(numberedDefaultName);
319  } else {
320  newName = text;
321  }
322  Script::ErrorStatus error = m_scriptStore->scriptAtIndex(m_selectableTableView.selectedRow()).setName(newName);
323  if (error == Script::ErrorStatus::None) {
324  updateAddScriptRowDisplay();
325  textField->setText(newName);
326  int currentRow = m_selectableTableView.selectedRow();
327  if (event == Ion::Events::Down && currentRow < numberOfRows() - 1) {
328  m_selectableTableView.selectCellAtLocation(m_selectableTableView.selectedColumn(), currentRow + 1);
329  } else if (event == Ion::Events::Up && currentRow > 0) {
330  m_selectableTableView.selectCellAtLocation(m_selectableTableView.selectedColumn(), currentRow - 1);
331  }
332  m_selectableTableView.selectedCell()->setHighlighted(true);
333  reloadConsole();
334  app()->setFirstResponder(&m_selectableTableView);
336  return true;
337  } else if (error == Script::ErrorStatus::NameTaken) {
338  app()->displayWarning(I18n::Message::NameTaken);
339  } else if (error == Script::ErrorStatus::NonCompliantName) {
340  app()->displayWarning(I18n::Message::NonCompliantName);
341  } else {
343  app()->displayWarning(I18n::Message::NameTooLong);
344  }
345  return false;
346 }
347 
348 bool MenuController::textFieldDidAbortEditing(TextField * textField, const char * text) {
350  // The previous text was an empty name. Use a numbered default script name.
351  char numberedDefaultName[k_defaultScriptNameMaxSize];
352  numberedDefaultScriptName(numberedDefaultName);
353  Script::ErrorStatus error = m_scriptStore->scriptAtIndex(m_selectableTableView.selectedRow()).setName(numberedDefaultName);
354  if (error != Script::ErrorStatus::None) {
355  assert(false);
356  /* Because we use the numbered default name, the name should not be
357  * already taken. Plus, the script could be added only if the storage has
358  * enough available space to add a script named 'script99.py' */
359  }
361  updateAddScriptRowDisplay();
362  }
363  m_selectableTableView.selectCellAtLocation(m_selectableTableView.selectedColumn(), m_selectableTableView.selectedRow());
364  app()->setFirstResponder(&m_selectableTableView);
366  return true;
367 }
368 
369 bool MenuController::textFieldDidHandleEvent(TextField * textField, bool returnValue, bool textHasChanged) {
370  int scriptExtensionLength = strlen(ScriptStore::k_scriptExtension);
371  if (textField->isEditing() && textField->cursorLocation() > textField->draftTextLength() - scriptExtensionLength) {
372  textField->setCursorLocation(textField->draftTextLength() - scriptExtensionLength);
373  }
374  return returnValue;
375 }
376 
377 void MenuController::addScript() {
378  Script::ErrorStatus error = m_scriptStore->addNewScript();
379  if (error == Script::ErrorStatus::None) {
380  updateAddScriptRowDisplay();
382  return;
383  }
384  assert(false); // Adding a new script is called when !m_scriptStore.isFull() which guarantees that the available space in the storage is big enough
385 }
386 
387 void MenuController::configureScript() {
388  assert(m_selectableTableView.selectedRow() >= 0);
389  assert(m_selectableTableView.selectedRow() < m_scriptStore->numberOfScripts());
390  m_scriptParameterController.setScript(m_scriptStore->scriptAtIndex(m_selectableTableView.selectedRow()));
391  stackViewController()->push(&m_scriptParameterController);
392 }
393 
394 void MenuController::editScriptAtIndex(int scriptIndex) {
395  assert(scriptIndex >=0 && scriptIndex < m_scriptStore->numberOfScripts());
396  Script script = m_scriptStore->scriptAtIndex(scriptIndex);
397  m_editorController.setScript(script);
398  stackViewController()->push(&m_editorController);
399 }
400 
401 void MenuController::numberedDefaultScriptName(char * buffer) {
402  bool foundNewScriptNumber = false;
403  int currentScriptNumber = 1;
404  char newName[k_defaultScriptNameMaxSize];
406  // We will only name scripts from script1.py to script99.py.
407  while (!foundNewScriptNumber && currentScriptNumber < 100) {
408  // Change the number in the script name.
409  intToText(currentScriptNumber, &newName[strlen(ScriptStore::k_defaultScriptName)-strlen(ScriptStore::k_scriptExtension)]);
411  if (m_scriptStore->scriptNamed(const_cast<const char *>(newName)).isNull()) {
412  foundNewScriptNumber = true;
413  }
414  currentScriptNumber++;
415  }
416  if (foundNewScriptNumber) {
417  memcpy(buffer, newName, strlen(newName)+1);
418  return;
419  }
421 }
422 
423 void MenuController::intToText(int i, char * buffer) {
424  // We only support integers from 0 to 99
425  // buffer should have the space for three chars.
426  assert(i>=0);
427  assert(i<100);
428  if (i/10 == 0) {
429  buffer[0] = i+'0';
430  buffer[1] = 0;
431  return;
432  }
433  buffer[0] = i/10+'0';
434  buffer[1] = i-10*(i/10)+'0';
435  buffer[2] = 0;
436 }
437 
438 void MenuController::updateAddScriptRowDisplay() {
439  m_shouldDisplayAddScriptRow = !m_scriptStore->isFull();
440  m_selectableTableView.reloadData();
441 }
442 
443 }
static constexpr KDCoordinate StoreRowHeight
Definition: metric.h:26
bool setCursorLocation(int location)
Definition: text_input.cpp:97
bool isEditing() const
Definition: text_field.cpp:175
int indexFromCumulatedHeight(KDCoordinate offsetY) override
void viewWillAppear() override
bool textFieldDidReceiveEvent(TextField *textField, Ion::Events::Event event) override
Definition: i18n.h:6
#define assert(e)
Definition: assert.h:9
HighlightCell * selectedCell()
ConsoleController * consoleController()
void setText(const char *text)
Definition: text_field.cpp:184
size_t cursorLocation() const
Definition: text_input.h:19
static constexpr KDCoordinate HistoryHorizontalMargin
Definition: metric.h:12
bool isNull() const
Definition: storage.h:38
constexpr Event EXE
Definition: events.h:114
int16_t KDCoordinate
Definition: coordinate.h:6
void push(ViewController *vc, KDColor textColor=Palette::SubTab, KDColor backgroundColor=KDColorWhite, KDColor separatorColor=Palette::GreyBright)
Responder * parentResponder() const
Definition: responder.cpp:12
void autoImportScript(Script script, bool force=false)
constexpr Event Up
Definition: events.h:62
constexpr Event Down
Definition: events.h:63
HighlightCell * reusableCell(int index, int type) override
bool selectCellAtLocation(int i, int j, bool setFirstResponder=true)
void willExitResponderChain(Responder *nextFirstResponder) override
TextField * textField()
bool textFieldDidFinishEditing(TextField *textField, const char *text, Ion::Events::Event event) override
virtual void setEditing(bool isEditing, bool reinitDraftBuffer=true)
Definition: text_field.cpp:196
bool textFieldDidAbortEditing(TextField *textField, const char *text) override
bool handleEvent(Ion::Events::Event event) override
void setScript(Script script)
void reloadData(bool setFirstResponder=true)
Script scriptNamed(const char *name)
Definition: script_store.h:24
Definition: app.cpp:7
void setShiftAlphaStatus(Ion::Events::ShiftAlphaStatus newStatus)
size_t strlen(const char *s)
Definition: strlen.c:3
ButtonRowController * footer()
void displayWarning(I18n::Message warningMessage)
Definition: app.cpp:97
void tableViewDidChangeSelection(SelectableTableView *t, int previousSelectedCellX, int previousSelectedCellY) override
void openConsoleWithScript(Script script)
int typeAtLocation(int i, int j) override
Script scriptAtIndex(int index)
Definition: script_store.h:21
int numberOfColumns() override
int numberOfRows() override
KDCoordinate columnWidth(int i) override
int reusableCellCount(int type) override
KDCoordinate cumulatedHeightFromIndex(int j) override
StackViewController * stackViewController()
const char * name() const
Definition: storage.h:41
const char * text() const
Definition: text_input.h:14
KDCoordinate cumulatedWidthFromIndex(int i) override
void willDisplayScriptTitleCellForIndex(HighlightCell *cell, int index)
bool setSelectedButton(int selectedButton)
const Container * container() const
Definition: app.cpp:102
size_t draftTextLength() const
Definition: text_field.cpp:179
const ToolboxMessageTree menu[menuChildrenCount]
MenuController(Responder *parentResponder, ScriptStore *scriptStore, ButtonRowController *footer)
bool textFieldDidHandleEvent(TextField *textField, bool returnValue, bool textHasChanged) override
void setFirstResponder(Responder *responder)
Definition: app.cpp:62
constexpr Event Right
Definition: events.h:64
KDCoordinate width() const
Definition: rect.h:39
Ion::Storage::Record::ErrorStatus addNewScript()
Definition: script_store.h:30
bool textFieldShouldFinishEditing(TextField *textField, Ion::Events::Event event) override
App * app()
Definition: responder.cpp:77
constexpr Event OK
Definition: events.h:65
virtual void setHighlighted(bool highlight)
bool loadPythonEnvironment(bool autoImportScripts=true)
void didBecomeFirstResponder() override
constexpr Event Clear
Definition: events.h:125
KDRect bounds() const
Definition: view.cpp:157
static constexpr char k_defaultScriptName[]
Definition: script_store.h:17
void willDisplayCellAtLocation(HighlightCell *cell, int i, int j) override
int indexFromCumulatedWidth(KDCoordinate offsetX) override
void * memcpy(void *dst, const void *src, size_t n)
Definition: memcpy.c:3
static constexpr char k_scriptExtension[]
Definition: script_store.h:16
void deleteScript(Script script)