Files
Upsilon/apps/code/console_controller.cpp
Léa Saviot 4a8c0a6bd0 [apps/code] Do not change the responders in autoImportScript if sandbox
This fixes the following scenario: create a script that draws at
importation (for instance, add "squares()" at the end of squares.py) and
execute this script. The sandbox is displayed but events are handled by
the underneath text field, thus leading to the display of the variable
box and other bugs.
2019-02-06 11:45:03 +01:00

436 lines
14 KiB
C++

#include "console_controller.h"
#include "app.h"
#include "script.h"
#include "variable_box_controller.h"
#include <apps/i18n.h>
#include <assert.h>
#include <escher/metric.h>
#include "../apps_container.h"
extern "C" {
#include <stdlib.h>
}
namespace Code {
static const char * sStandardPromptText = ">>> ";
ConsoleController::ConsoleController(Responder * parentResponder, App * pythonDelegate, ScriptStore * scriptStore
#if EPSILON_GETOPT
, bool lockOnConsole
#endif
) :
ViewController(parentResponder),
SelectableTableViewDataSource(),
TextFieldDelegate(),
MicroPython::ExecutionEnvironment(),
m_pythonDelegate(pythonDelegate),
m_rowHeight(k_font->glyphSize().height()),
m_importScriptsWhenViewAppears(false),
m_selectableTableView(this, this, this, this),
m_editCell(this, pythonDelegate, this),
m_scriptStore(scriptStore),
m_sandboxController(this, this),
m_inputRunLoopActive(false)
#if EPSILON_GETOPT
, m_locked(lockOnConsole)
#endif
{
m_selectableTableView.setMargins(0, Metric::CommonRightMargin, 0, Metric::TitleBarExternHorizontalMargin);
m_selectableTableView.setBackgroundColor(KDColorWhite);
m_editCell.setPrompt(sStandardPromptText);
for (int i = 0; i < k_numberOfLineCells; i++) {
m_cells[i].setParentResponder(&m_selectableTableView);
}
}
bool ConsoleController::loadPythonEnvironment() {
if (m_pythonDelegate->isPythonUser(this)) {
return true;
}
emptyOutputAccumulationBuffer();
m_pythonDelegate->initPythonWithUser(this);
MicroPython::registerScriptProvider(m_scriptStore);
m_importScriptsWhenViewAppears = m_autoImportScripts;
/* We load functions and variables names in the variable box before running
* any other python code to avoid failling to load functions and variables
* due to memory exhaustion. */
static_cast<App *>(app())->variableBoxController()->loadFunctionsAndVariables();
return true;
}
void ConsoleController::unloadPythonEnvironment() {
if (!m_pythonDelegate->isPythonUser(nullptr)) {
m_consoleStore.startNewSession();
m_pythonDelegate->deinitPython();
}
}
void ConsoleController::autoImport() {
for (int i = 0; i < m_scriptStore->numberOfScripts(); i++) {
autoImportScript(m_scriptStore->scriptAtIndex(i));
}
}
void ConsoleController::runAndPrintForCommand(const char * command) {
m_consoleStore.pushCommand(command, strlen(command));
assert(m_outputAccumulationBuffer[0] == '\0');
runCode(command);
flushOutputAccumulationBufferToStore();
m_consoleStore.deleteLastLineIfEmpty();
}
void ConsoleController::terminateInputLoop() {
assert(m_inputRunLoopActive);
m_inputRunLoopActive = false;
interrupt();
}
const char * ConsoleController::inputText(const char * prompt) {
AppsContainer * a = (AppsContainer *)(app()->container());
m_inputRunLoopActive = true;
const char * promptText = prompt;
char * s = const_cast<char *>(prompt);
if (promptText != nullptr) {
/* Set the prompt text. If the prompt text has a '\n', put the prompt text in
* the history until the last '\n', and put the remaining prompt text in the
* edit cell's prompt. */
char * lastCarriageReturn = nullptr;
while (*s != 0) {
if (*s == '\n') {
lastCarriageReturn = s;
}
s++;
}
if (lastCarriageReturn != nullptr) {
printText(prompt, lastCarriageReturn-prompt+1);
promptText = lastCarriageReturn+1;
}
}
m_editCell.setPrompt(promptText);
m_editCell.setText("");
// Reload the history
m_selectableTableView.reloadData();
m_selectableTableView.selectCellAtLocation(0, m_consoleStore.numberOfLines());
a->redrawWindow();
// Launch a new input loop
a->runWhile([](void * a){
ConsoleController * c = static_cast<ConsoleController *>(a);
return c->inputRunLoopActive();
}, this);
// Handle the input text
if (promptText != nullptr) {
printText(promptText, s - promptText);
}
const char * text = m_editCell.text();
printText(text, strlen(text));
flushOutputAccumulationBufferToStore();
m_editCell.setPrompt(sStandardPromptText);
return text;
}
void ConsoleController::viewWillAppear() {
loadPythonEnvironment();
if (m_importScriptsWhenViewAppears) {
m_importScriptsWhenViewAppears = false;
autoImport();
}
m_selectableTableView.reloadData();
m_selectableTableView.selectCellAtLocation(0, m_consoleStore.numberOfLines());
m_editCell.setEditing(true);
m_editCell.setText("");
}
void ConsoleController::didBecomeFirstResponder() {
app()->setFirstResponder(&m_editCell);
}
bool ConsoleController::handleEvent(Ion::Events::Event event) {
if (event == Ion::Events::OK || event == Ion::Events::EXE) {
if (m_consoleStore.numberOfLines() > 0 && m_selectableTableView.selectedRow() < m_consoleStore.numberOfLines()) {
const char * text = m_consoleStore.lineAtIndex(m_selectableTableView.selectedRow()).text();
m_editCell.setEditing(true);
m_selectableTableView.selectCellAtLocation(0, m_consoleStore.numberOfLines());
app()->setFirstResponder(&m_editCell);
return m_editCell.insertText(text);
}
} else if (event == Ion::Events::Clear) {
m_selectableTableView.deselectTable();
m_consoleStore.clear();
m_selectableTableView.reloadData();
m_selectableTableView.selectCellAtLocation(0, m_consoleStore.numberOfLines());
return true;
} else if (event == Ion::Events::Backspace) {
int selectedRow = m_selectableTableView.selectedRow();
assert(selectedRow >= 0 && selectedRow < m_consoleStore.numberOfLines());
m_selectableTableView.deselectTable();
int firstDeletedLineIndex = m_consoleStore.deleteCommandAndResultsAtIndex(selectedRow);
m_selectableTableView.reloadData();
m_selectableTableView.selectCellAtLocation(0, firstDeletedLineIndex);
return true;
}
#if EPSILON_GETOPT
if (m_locked && (event == Ion::Events::Home || event == Ion::Events::Back)) {
if (m_inputRunLoopActive) {
terminateInputLoop();
}
return true;
}
#endif
return false;
}
int ConsoleController::numberOfRows() {
return m_consoleStore.numberOfLines()+1;
}
KDCoordinate ConsoleController::rowHeight(int j) {
return m_rowHeight;
}
KDCoordinate ConsoleController::cumulatedHeightFromIndex(int j) {
return j*rowHeight(0);
}
int ConsoleController::indexFromCumulatedHeight(KDCoordinate offsetY ){
return offsetY/rowHeight(0);
}
HighlightCell * ConsoleController::reusableCell(int index, int type) {
assert(index >= 0);
if (type == LineCellType) {
assert(index < k_numberOfLineCells);
return m_cells+index;
} else {
assert(type == EditCellType);
assert(index == 0);
return &m_editCell;
}
}
int ConsoleController::reusableCellCount(int type) {
if (type == LineCellType) {
return k_numberOfLineCells;
} else {
return 1;
}
}
int ConsoleController::typeAtLocation(int i, int j) {
assert(i == 0);
assert(j >= 0);
if (j < m_consoleStore.numberOfLines()) {
return LineCellType;
} else {
assert(j == m_consoleStore.numberOfLines());
return EditCellType;
}
}
void ConsoleController::willDisplayCellAtLocation(HighlightCell * cell, int i, int j) {
assert(i == 0);
if (j < m_consoleStore.numberOfLines()) {
static_cast<ConsoleLineCell *>(cell)->setLine(m_consoleStore.lineAtIndex(j));
}
}
void ConsoleController::tableViewDidChangeSelection(SelectableTableView * t, int previousSelectedCellX, int previousSelectedCellY) {
if (t->selectedRow() == m_consoleStore.numberOfLines()) {
m_editCell.setEditing(true);
return;
}
if (t->selectedRow()>-1) {
if (previousSelectedCellY > -1 && previousSelectedCellY < m_consoleStore.numberOfLines()) {
// Reset the scroll of the previous cell
ConsoleLineCell * previousCell = (ConsoleLineCell *)(t->cellAtLocation(previousSelectedCellX, previousSelectedCellY));
if (previousCell) {
previousCell->reloadCell();
}
}
ConsoleLineCell * selectedCell = (ConsoleLineCell *)(t->selectedCell());
selectedCell->reloadCell();
}
}
bool ConsoleController::textFieldShouldFinishEditing(TextField * textField, Ion::Events::Event event) {
assert(textField->isEditing());
return (textField->draftTextLength() > 0
&& (event == Ion::Events::OK || event == Ion::Events::EXE));
}
bool ConsoleController::textFieldDidReceiveEvent(TextField * textField, Ion::Events::Event event) {
if (m_inputRunLoopActive
&& (event == Ion::Events::Up
|| event == Ion::Events::OK
|| event == Ion::Events::EXE))
{
m_inputRunLoopActive = false;
/* We need to return true here because we want to actually exit from the
* input run loop, which requires ending a dispatchEvent cycle. */
return true;
}
if (event == Ion::Events::Up) {
if (m_consoleStore.numberOfLines() > 0 && m_selectableTableView.selectedRow() == m_consoleStore.numberOfLines()) {
m_editCell.setEditing(false);
m_selectableTableView.selectCellAtLocation(0, m_consoleStore.numberOfLines()-1);
return true;
}
}
return static_cast<App *>(textField->app())->textInputDidReceiveEvent(textField, event);
}
bool ConsoleController::textFieldDidFinishEditing(TextField * textField, const char * text, Ion::Events::Event event) {
if (m_inputRunLoopActive) {
m_inputRunLoopActive = false;
return false;
}
runAndPrintForCommand(text);
if (!sandboxIsDisplayed()) {
m_selectableTableView.reloadData();
m_editCell.setEditing(true);
textField->setText("");
m_selectableTableView.selectCellAtLocation(0, m_consoleStore.numberOfLines());
}
return true;
}
bool ConsoleController::textFieldDidAbortEditing(TextField * textField) {
if (m_inputRunLoopActive) {
m_inputRunLoopActive = false;
} else {
#if EPSILON_GETOPT
/* In order to lock the console controller, we disable poping controllers
* below the console controller included. The stack should only hold:
* - the menu controller
* - the console controller
* The depth of the stack controller must always be above or equal to 2. */
if (!m_locked || stackViewController()->depth() > 2) {
#endif
stackViewController()->pop();
#if EPSILON_GETOPT
} else {
textField->setEditing(true);
}
#endif
}
return true;
}
void ConsoleController::displaySandbox() {
if (sandboxIsDisplayed()) {
return;
}
stackViewController()->push(&m_sandboxController);
}
void ConsoleController::resetSandbox() {
if (!sandboxIsDisplayed()) {
return;
}
m_sandboxController.reset();
}
/* printText is called by the Python machine.
* The text argument is not always null-terminated. */
void ConsoleController::printText(const char * text, size_t length) {
size_t textCutIndex = firstNewLineCharIndex(text, length);
if (textCutIndex >= length) {
/* If there is no new line in text, just append it to the output
* accumulation buffer. */
appendTextToOutputAccumulationBuffer(text, length);
return;
}
if (textCutIndex < length - 1) {
/* If there is a new line in the middle of the text, we have to store at
* least two new console lines in the console store. */
printText(text, textCutIndex + 1);
printText(&text[textCutIndex+1], length - (textCutIndex + 1));
return;
}
/* There is a new line at the end of the text, we have to store the line in
* the console store. */
assert(textCutIndex == length - 1);
appendTextToOutputAccumulationBuffer(text, length-1);
flushOutputAccumulationBufferToStore();
}
void ConsoleController::autoImportScript(Script script, bool force) {
if (script.importationStatus() || force) {
// Step 1 - Create the command "from scriptName import *".
assert(strlen(k_importCommand1) + strlen(script.fullName()) - strlen(ScriptStore::k_scriptExtension) - 1 + strlen(k_importCommand2) + 1 <= k_maxImportCommandSize);
char command[k_maxImportCommandSize];
// Copy "from "
size_t currentChar = strlcpy(command, k_importCommand1, k_maxImportCommandSize);
const char * scriptName = script.fullName();
/* Copy the script name without the extension ".py". The '.' is overwritten
* by the null terminating char. */
int copySizeWithNullTerminatingZero = min(k_maxImportCommandSize - currentChar, strlen(scriptName) - strlen(ScriptStore::k_scriptExtension));
strlcpy(command+currentChar, scriptName, copySizeWithNullTerminatingZero);
currentChar += copySizeWithNullTerminatingZero-1;
// Copy " import *"
strlcpy(command+currentChar, k_importCommand2, k_maxImportCommandSize - currentChar);
// Step 2 - Run the command
runAndPrintForCommand(command);
}
if (!sandboxIsDisplayed() && force) {
m_selectableTableView.reloadData();
m_selectableTableView.selectCellAtLocation(0, m_consoleStore.numberOfLines());
m_editCell.setEditing(true);
m_editCell.setText("");
}
}
void ConsoleController::flushOutputAccumulationBufferToStore() {
m_consoleStore.pushResult(m_outputAccumulationBuffer, strlen(m_outputAccumulationBuffer));
emptyOutputAccumulationBuffer();
}
void ConsoleController::appendTextToOutputAccumulationBuffer(const char * text, size_t length) {
int endOfAccumulatedText = strlen(m_outputAccumulationBuffer);
int spaceLeft = k_outputAccumulationBufferSize - endOfAccumulatedText;
if (spaceLeft > (int)length) {
memcpy(&m_outputAccumulationBuffer[endOfAccumulatedText], text, length);
return;
}
memcpy(&m_outputAccumulationBuffer[endOfAccumulatedText], text, spaceLeft-1);
flushOutputAccumulationBufferToStore();
appendTextToOutputAccumulationBuffer(&text[spaceLeft-1], length - (spaceLeft - 1));
}
void ConsoleController::emptyOutputAccumulationBuffer() {
for (int i = 0; i < k_outputAccumulationBufferSize; i++) {
m_outputAccumulationBuffer[i] = 0;
}
}
size_t ConsoleController::firstNewLineCharIndex(const char * text, size_t length) {
size_t index = 0;
while (index < length) {
if (text[index] == '\n') {
return index;
}
index++;
}
return index;
}
StackViewController * ConsoleController::stackViewController() {
return static_cast<StackViewController *>(parentResponder());
}
}