mirror of
https://github.com/UpsilonNumworks/Upsilon.git
synced 2026-01-19 00:37:25 +01:00
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.
436 lines
14 KiB
C++
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());
|
|
}
|
|
|
|
}
|