Files
Upsilon/apps/code/menu_controller.cpp
Léa Saviot e74e15eaf7 [apps/code] Handle too many Script default names used
When adding to many scripts without naming them, one can overflow the
number of default generated script names, in which case we force the
user to enter a name.
2018-11-23 12:04:05 +01:00

409 lines
15 KiB
C++

#include "menu_controller.h"
#include "app.h"
#include "../i18n.h"
#include "../apps_container.h"
#include <assert.h>
#include <escher/metric.h>
#include <ion/events.h>
namespace Code {
MenuController::MenuController(Responder * parentResponder, App * pythonDelegate, ScriptStore * scriptStore, ButtonRowController * footer) :
ViewController(parentResponder),
ButtonRowDelegate(nullptr, footer),
m_scriptStore(scriptStore),
m_addNewScriptCell(),
m_consoleButton(this, I18n::Message::Console, Invocation([](void * context, void * sender) {
MenuController * menu = (MenuController *)context;
menu->consoleController()->setAutoImport(true);
menu->stackViewController()->push(menu->consoleController());
return true;
}, this), KDFont::LargeFont),
m_selectableTableView(this, this, this, this),
m_scriptParameterController(nullptr, I18n::Message::ScriptOptions, this),
m_editorController(this, pythonDelegate),
m_reloadConsoleWhenBecomingFirstResponder(false),
m_shouldDisplayAddScriptRow(true)
{
m_selectableTableView.setMargins(0);
m_selectableTableView.setShowsIndicators(false);
m_addNewScriptCell.setMessage(I18n::Message::AddScript);
for (int i = 0; i < k_maxNumberOfDisplayableScriptCells; i++) {
m_scriptCells[i].setParentResponder(&m_selectableTableView);
m_scriptCells[i].textField()->setDelegates(nullptr, this);
}
}
ConsoleController * MenuController::consoleController() {
return static_cast<App *>(app())->consoleController();
}
StackViewController * MenuController::stackViewController() {
return static_cast<StackViewController *>(parentResponder()->parentResponder());
}
void MenuController::willExitResponderChain(Responder * nextFirstResponder) {
int selectedRow = m_selectableTableView.selectedRow();
int selectedColumn = m_selectableTableView.selectedColumn();
if (selectedRow >= 0 && selectedRow < m_scriptStore->numberOfScripts() && selectedColumn == 0) {
TextField * tf = static_cast<ScriptNameCell *>(m_selectableTableView.selectedCell())->textField();
if (tf->isEditing()) {
tf->setEditing(false, false);
textFieldDidAbortEditing(tf);
}
}
}
void MenuController::didBecomeFirstResponder() {
if (m_reloadConsoleWhenBecomingFirstResponder) {
reloadConsole();
}
if (footer()->selectedButton() == 0) {
assert(m_selectableTableView.selectedRow() < 0);
app()->setFirstResponder(&m_consoleButton);
return;
}
if (m_selectableTableView.selectedRow() < 0) {
m_selectableTableView.selectCellAtLocation(0,0);
}
assert(m_selectableTableView.selectedRow() < m_scriptStore->numberOfScripts() + 1);
app()->setFirstResponder(&m_selectableTableView);
#if EPSILON_GETOPT
if (consoleController()->locked() && consoleController()->loadPythonEnvironment()) {
stackViewController()->push(consoleController());
return;
}
#endif
}
void MenuController::viewWillAppear() {
updateAddScriptRowDisplay();
}
bool MenuController::handleEvent(Ion::Events::Event event) {
if (event == Ion::Events::Down) {
m_selectableTableView.deselectTable();
footer()->setSelectedButton(0);
return true;
}
if (event == Ion::Events::Up) {
if (footer()->selectedButton() == 0) {
footer()->setSelectedButton(-1);
m_selectableTableView.selectCellAtLocation(0, numberOfRows()-1);
app()->setFirstResponder(&m_selectableTableView);
return true;
}
}
if (event == Ion::Events::OK || event == Ion::Events::EXE) {
int selectedRow = m_selectableTableView.selectedRow();
int selectedColumn = m_selectableTableView.selectedColumn();
if (selectedRow >= 0 && selectedRow < m_scriptStore->numberOfScripts()) {
if (selectedColumn == 1) {
configureScript();
return true;
}
assert(selectedColumn == 0);
editScriptAtIndex(selectedRow);
return true;
} else if (m_shouldDisplayAddScriptRow
&& selectedColumn == 0
&& selectedRow == m_scriptStore->numberOfScripts())
{
addScript();
return true;
}
}
return false;
}
void MenuController::renameSelectedScript() {
assert(m_selectableTableView.selectedRow() >= 0);
assert(m_selectableTableView.selectedRow() < m_scriptStore->numberOfScripts());
static_cast<AppsContainer *>(const_cast<Container *>(app()->container()))->setShiftAlphaStatus(Ion::Events::ShiftAlphaStatus::AlphaLock);
m_selectableTableView.selectCellAtLocation(0, (m_selectableTableView.selectedRow()));
ScriptNameCell * myCell = static_cast<ScriptNameCell *>(m_selectableTableView.selectedCell());
app()->setFirstResponder(myCell);
myCell->setHighlighted(false);
myCell->textField()->setEditing(true, false);
myCell->textField()->setCursorLocation(strlen(myCell->textField()->text()));
}
void MenuController::deleteScript(Script script) {
assert(!script.isNull());
script.destroy();
updateAddScriptRowDisplay();
}
void MenuController::reloadConsole() {
consoleController()->unloadPythonEnvironment();
m_reloadConsoleWhenBecomingFirstResponder = false;
}
void MenuController::openConsoleWithScript(Script script) {
reloadConsole();
consoleController()->setAutoImport(false);
stackViewController()->push(consoleController());
consoleController()->autoImportScript(script, true);
m_reloadConsoleWhenBecomingFirstResponder = true;
}
void MenuController::scriptContentEditionDidFinish(){
reloadConsole();
}
int MenuController::numberOfRows() {
return m_scriptStore->numberOfScripts() + m_shouldDisplayAddScriptRow;
}
void MenuController::willDisplayCellAtLocation(HighlightCell * cell, int i, int j) {
if (i == 0 && j < m_scriptStore->numberOfScripts()) {
willDisplayScriptTitleCellForIndex(cell, j);
}
static_cast<EvenOddCell *>(cell)->setEven(j%2 == 0);
cell->setHighlighted(i == selectedColumn() && j == selectedRow());
}
KDCoordinate MenuController::columnWidth(int i) {
switch (i) {
case 0:
return m_selectableTableView.bounds().width()-k_parametersColumnWidth;
case 1:
return k_parametersColumnWidth;
default:
assert(false);
return 0;
}
}
KDCoordinate MenuController::cumulatedWidthFromIndex(int i) {
switch (i) {
case 0:
return 0;
case 1:
return m_selectableTableView.bounds().width()-k_parametersColumnWidth;
case 2:
return m_selectableTableView.bounds().width();
default:
assert(false);
return 0;
}
}
KDCoordinate MenuController::cumulatedHeightFromIndex(int j) {
return Metric::StoreRowHeight * j;
}
int MenuController::indexFromCumulatedWidth(KDCoordinate offsetX) {
if (offsetX <= m_selectableTableView.bounds().width()-k_parametersColumnWidth) {
return 0;
}
if (offsetX <= m_selectableTableView.bounds().width()) {
return 1;
}
else {
return 2;
}
assert(false);
return 0;
}
int MenuController::indexFromCumulatedHeight(KDCoordinate offsetY) {
if (Metric::StoreRowHeight == 0) {
return 0;
}
return (offsetY - 1) / Metric::StoreRowHeight;
}
HighlightCell * MenuController::reusableCell(int index, int type) {
assert(index >= 0);
if (type == ScriptCellType) {
assert(index >=0 && index < k_maxNumberOfDisplayableScriptCells);
return &m_scriptCells[index];
}
if (type == ScriptParameterCellType) {
assert(index >=0 && index < k_maxNumberOfDisplayableScriptCells);
return &m_scriptParameterCells[index];
}
if (type == AddScriptCellType) {
assert(index == 0);
return &m_addNewScriptCell;
}
if(type == EmptyCellType) {
return &m_emptyCell;
}
assert(false);
return nullptr;
}
int MenuController::reusableCellCount(int type) {
if (type == AddScriptCellType) {
return 1;
}
if (type == ScriptCellType || type == ScriptParameterCellType) {
return k_maxNumberOfDisplayableScriptCells;
}
if (type == EmptyCellType) {
return 1;
}
assert(false);
return 0;
}
int MenuController::typeAtLocation(int i, int j) {
assert(i >= 0 && i < numberOfColumns());
assert(j >= 0 && j < numberOfRows());
if (i == 0) {
if (j == numberOfRows()-1 && m_shouldDisplayAddScriptRow) {
return AddScriptCellType;
}
return ScriptCellType;
}
assert(i == 1);
if (j == numberOfRows()-1 && m_shouldDisplayAddScriptRow) {
return EmptyCellType;
}
return ScriptParameterCellType;
}
void MenuController::willDisplayScriptTitleCellForIndex(HighlightCell * cell, int index) {
assert(index >= 0 && index < m_scriptStore->numberOfScripts());
(static_cast<ScriptNameCell *>(cell))->textField()->setText(m_scriptStore->scriptAtIndex(index).fullName());
}
void MenuController::tableViewDidChangeSelection(SelectableTableView * t, int previousSelectedCellX, int previousSelectedCellY) {
if (selectedRow() == numberOfRows() - 1 && selectedColumn() == 1 && m_shouldDisplayAddScriptRow) {
t->selectCellAtLocation(0, numberOfRows()-1);
}
}
bool MenuController::textFieldShouldFinishEditing(TextField * textField, Ion::Events::Event event) {
return event == Ion::Events::OK || event == Ion::Events::EXE
|| event == Ion::Events::Down || event == Ion::Events::Up;
}
bool MenuController::textFieldDidReceiveEvent(TextField * textField, Ion::Events::Event event) {
if (event == Ion::Events::Right && textField->isEditing() && textField->cursorLocation() == textField->draftTextLength()) {
return true;
}
if (event == Ion::Events::Clear && textField->isEditing()) {
constexpr size_t k_bufferSize = 4;
char buffer[k_bufferSize] = {'.', 0, 0, 0};
assert(k_bufferSize >= 1 + strlen(ScriptStore::k_scriptExtension) + 1);
strlcpy(&buffer[1], ScriptStore::k_scriptExtension, strlen(ScriptStore::k_scriptExtension) + 1);
textField->setText(buffer);
textField->setCursorLocation(0);
return true;
}
return false;
}
bool MenuController::textFieldDidFinishEditing(TextField * textField, const char * text, Ion::Events::Event event) {
const char * newName;
static constexpr int bufferSize = Script::k_defaultScriptNameMaxSize + 1 + ScriptStore::k_scriptExtensionLength; //"script99" + "." + "py"
char numberedDefaultName[bufferSize];
if (strlen(text) > 1 + strlen(ScriptStore::k_scriptExtension)) {
newName = text;
} else {
// The user entered an empty name. Use a numbered default script name.
bool foundDefaultName = Script::DefaultName(numberedDefaultName, Script::k_defaultScriptNameMaxSize);
int defaultNameLength = strlen(numberedDefaultName);
numberedDefaultName[defaultNameLength++] = '.';
strlcpy(&numberedDefaultName[defaultNameLength], ScriptStore::k_scriptExtension, bufferSize - defaultNameLength);
/* If there are already scripts named script1.py, script2.py,... until
* Script::k_maxNumberOfDefaultScriptNames, we want to write the last tried
* default name and let the user modify it. */
if (!foundDefaultName) {
textField->setText(numberedDefaultName);
textField->setCursorLocation(defaultNameLength);
}
newName = const_cast<const char *>(numberedDefaultName);
}
Script::ErrorStatus error = Script::nameCompliant(newName) ? m_scriptStore->scriptAtIndex(m_selectableTableView.selectedRow()).setName(newName) : Script::ErrorStatus::NonCompliantName;
if (error == Script::ErrorStatus::None) {
updateAddScriptRowDisplay();
textField->setText(newName);
int currentRow = m_selectableTableView.selectedRow();
if (event == Ion::Events::Down && currentRow < numberOfRows() - 1) {
m_selectableTableView.selectCellAtLocation(m_selectableTableView.selectedColumn(), currentRow + 1);
} else if (event == Ion::Events::Up && currentRow > 0) {
m_selectableTableView.selectCellAtLocation(m_selectableTableView.selectedColumn(), currentRow - 1);
}
m_selectableTableView.selectedCell()->setHighlighted(true);
reloadConsole();
app()->setFirstResponder(&m_selectableTableView);
static_cast<AppsContainer *>(const_cast<Container *>(app()->container()))->setShiftAlphaStatus(Ion::Events::ShiftAlphaStatus::Default);
return true;
} else if (error == Script::ErrorStatus::NameTaken) {
app()->displayWarning(I18n::Message::NameTaken);
} else if (error == Script::ErrorStatus::NonCompliantName) {
app()->displayWarning(I18n::Message::NonCompliantName);
} else {
assert(error == Script::ErrorStatus::NotEnoughSpaceAvailable);
app()->displayWarning(I18n::Message::NameTooLong);
}
return false;
}
bool MenuController::textFieldDidAbortEditing(TextField * textField) {
Script script = m_scriptStore->scriptAtIndex(m_selectableTableView.selectedRow());
const char * scriptName = script.fullName();
if (strlen(scriptName) <= 1 + strlen(ScriptStore::k_scriptExtension)) {
// The previous text was an empty name. Use a numbered default script name.
char numberedDefaultName[Script::k_defaultScriptNameMaxSize];
Script::DefaultName(numberedDefaultName, Script::k_defaultScriptNameMaxSize);
Script::ErrorStatus error = script.setBaseNameWithExtension(numberedDefaultName, ScriptStore::k_scriptExtension);
scriptName = m_scriptStore->scriptAtIndex(m_selectableTableView.selectedRow()).fullName();
/* Because we use the numbered default name, the name should not be
* already taken. Plus, the script could be added only if the storage has
* enough available space to add a script named 'script99.py' */
(void) error; // Silence the "variable unused" warning if assertions are not enabled
assert(error == Script::ErrorStatus::None);
updateAddScriptRowDisplay();
}
textField->setText(scriptName);
m_selectableTableView.selectCellAtLocation(m_selectableTableView.selectedColumn(), m_selectableTableView.selectedRow());
app()->setFirstResponder(&m_selectableTableView);
static_cast<AppsContainer *>(const_cast<Container *>(app()->container()))->setShiftAlphaStatus(Ion::Events::ShiftAlphaStatus::Default);
return true;
}
bool MenuController::textFieldDidHandleEvent(TextField * textField, bool returnValue, bool textHasChanged) {
int scriptExtensionLength = 1 + strlen(ScriptStore::k_scriptExtension);
if (textField->isEditing() && textField->cursorLocation() > textField->draftTextLength() - scriptExtensionLength) {
textField->setCursorLocation(textField->draftTextLength() - scriptExtensionLength);
}
return returnValue;
}
void MenuController::addScript() {
Script::ErrorStatus error = m_scriptStore->addNewScript();
if (error == Script::ErrorStatus::None) {
updateAddScriptRowDisplay();
renameSelectedScript();
return;
}
assert(false); // Adding a new script is called when !m_scriptStore.isFull() which guarantees that the available space in the storage is big enough
}
void MenuController::configureScript() {
assert(m_selectableTableView.selectedRow() >= 0);
assert(m_selectableTableView.selectedRow() < m_scriptStore->numberOfScripts());
m_scriptParameterController.setScript(m_scriptStore->scriptAtIndex(m_selectableTableView.selectedRow()));
stackViewController()->push(&m_scriptParameterController);
}
void MenuController::editScriptAtIndex(int scriptIndex) {
assert(scriptIndex >=0 && scriptIndex < m_scriptStore->numberOfScripts());
Script script = m_scriptStore->scriptAtIndex(scriptIndex);
m_editorController.setScript(script);
stackViewController()->push(&m_editorController);
}
void MenuController::updateAddScriptRowDisplay() {
m_shouldDisplayAddScriptRow = !m_scriptStore->isFull();
m_selectableTableView.reloadData();
}
}