Files
Upsilon/apps/code/console_controller.cpp
Léa Saviot 02e79ad595 [apps/code] Do not refresh the print if the sandbox is displayed
Otherwise the first responder becomes the console edit line, and events
(such as Toolbox) are not intercepted by the sandbox anymore.
2020-02-25 15:31:25 +01:00

535 lines
18 KiB
C++
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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/global_preferences.h>
#include <apps/apps_container.h>
#include <python/port/helpers.h>
extern "C" {
#include <stdlib.h>
}
namespace Code {
static inline int minInt(int x, int y) { return x < y ? x : y; }
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_importScriptsWhenViewAppears(false),
m_selectableTableView(this, this, this, this),
m_editCell(this, pythonDelegate, this),
m_scriptStore(scriptStore),
m_sandboxController(this, this),
m_inputRunLoopActive(false),
m_preventEdition(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. */
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) {
const char * storedCommand = m_consoleStore.pushCommand(command);
assert(m_outputAccumulationBuffer[0] == '\0');
// Draw the console before running the code
m_preventEdition = true;
m_editCell.setText("");
m_editCell.setPrompt("");
refreshPrintOutput();
runCode(storedCommand);
m_preventEdition = false;
m_editCell.setPrompt(sStandardPromptText);
m_editCell.setEditing(true);
flushOutputAccumulationBufferToStore();
m_consoleStore.deleteLastLineIfEmpty();
}
void ConsoleController::terminateInputLoop() {
assert(m_inputRunLoopActive);
m_inputRunLoopActive = false;
interrupt();
}
const char * ConsoleController::inputText(const char * prompt) {
AppsContainer * appsContainer = AppsContainer::sharedAppsContainer();
m_inputRunLoopActive = true;
// Hide the sandbox if it is displayed
if (sandboxIsDisplayed()) {
hideSandbox();
}
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;
}
}
const char * previousPrompt = m_editCell.promptText();
m_editCell.setPrompt(promptText);
/* The user will input some text that is stored in the edit cell. When the
* input is finished, we want to clear that cell and return the input text.
* We choose to shift the input in the edit cell and put a null char in first
* position, so that the cell seems cleared but we can still use it to store
* the input.
* To do so, we need to reduce the cell buffer size by one, so that the input
* can be shifted afterwards, even if it has maxSize.
*
* Illustration of a input sequence:
* | | | | | | | | | <- the edit cell buffer
* |0| | | | | | |X| <- clear and reduce the size
* |a|0| | | | | |X| <- user input
* |a|b|0| | | | |X| <- user input
* |a|b|c|0| | | |X| <- user input
* |a|b|c|d|0| | |X| <- last user input
* | |a|b|c|d|0| | | <- increase the buffer size and shift the user input by one
* |0|a|b|c|d|0| | | <- put a zero in first position: the edit cell seems empty
*/
m_editCell.clearAndReduceSize();
// Reload the history
m_selectableTableView.reloadData();
m_selectableTableView.selectCellAtLocation(0, m_consoleStore.numberOfLines());
appsContainer->redrawWindow();
// Launch a new input loop
appsContainer->runWhile([](void * a){
ConsoleController * c = static_cast<ConsoleController *>(a);
return c->inputRunLoopActive();
}, this);
// Print the prompt and the input text
if (promptText != nullptr) {
printText(promptText, s - promptText);
}
const char * text = m_editCell.text();
size_t textSize = strlen(text);
printText(text, textSize);
flushOutputAccumulationBufferToStore();
// Clear the edit cell and return the input
text = m_editCell.shiftCurrentTextAndClear();
m_editCell.setPrompt(previousPrompt);
refreshPrintOutput();
return text;
}
void ConsoleController::viewWillAppear() {
ViewController::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() {
Container::activeApp()->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());
Container::activeApp()->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() const {
return m_consoleStore.numberOfLines()+1;
}
KDCoordinate ConsoleController::rowHeight(int j) {
return GlobalPreferences::sharedGlobalPreferences()->font()->glyphSize().height();
}
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, bool withinTemporarySelection) {
if (withinTemporarySelection) {
return;
}
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());
if (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 App::app()->textInputDidReceiveEvent(textField, event);
}
bool ConsoleController::textFieldDidFinishEditing(TextField * textField, const char * text, Ion::Events::Event event) {
if (m_inputRunLoopActive) {
m_inputRunLoopActive = false;
return false;
}
telemetryReportEvent("Console", text);
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::hideSandbox() {
if (!sandboxIsDisplayed()) {
return;
}
m_sandboxController.hide();
}
void ConsoleController::resetSandbox() {
if (!sandboxIsDisplayed()) {
return;
}
m_sandboxController.reset();
}
void ConsoleController::refreshPrintOutput() {
if (sandboxIsDisplayed()) {
return;
}
m_selectableTableView.reloadData();
m_selectableTableView.selectCellAtLocation(0, m_consoleStore.numberOfLines());
if (m_preventEdition) {
m_editCell.setEditing(false);
}
AppsContainer::sharedAppsContainer()->redrawWindow();
}
/* 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);
} else {
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();
micropython_port_vm_hook_refresh_print();
}
/* micropython_port_vm_hook_loop is not enough to detect user interruptions,
* because it calls micropython_port_interrupt_if_needed every 20000
* operations, and a print operation is quite long. We thus explicitely call
* micropython_port_interrupt_if_needed here. */
micropython_port_interrupt_if_needed();
}
void ConsoleController::autoImportScript(Script script, bool force) {
if (sandboxIsDisplayed()) {
/* The sandbox might be displayed, for instance if we are auto-importing
* several scripts that draw at importation. In this case, we want to remove
* the sandbox. */
hideSandbox();
}
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 = minInt(k_maxImportCommandSize - currentChar, strlen(scriptName) - strlen(ScriptStore::k_scriptExtension));
assert(copySizeWithNullTerminatingZero >= 0);
assert(copySizeWithNullTerminatingZero <= k_maxImportCommandSize - currentChar);
strlcpy(command+currentChar, scriptName, copySizeWithNullTerminatingZero);
currentChar += copySizeWithNullTerminatingZero-1;
// Copy " import *"
assert(k_maxImportCommandSize >= currentChar);
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);
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;
}
/* The text to append is too long for the buffer. We need to split it in
* chunks. We take special care not to break in the middle of code points! */
int maxAppendedTextLength = spaceLeft-1; // we keep the last char to null-terminate the buffer
int appendedTextLength = 0;
UTF8Decoder decoder(text);
while (decoder.stringPosition() - text <= maxAppendedTextLength) {
appendedTextLength = decoder.stringPosition() - text;
decoder.nextCodePoint();
}
memcpy(&m_outputAccumulationBuffer[endOfAccumulatedText], text, appendedTextLength);
// The last char of m_outputAccumulationBuffer is kept to 0 to ensure a null-terminated text.
assert(endOfAccumulatedText+appendedTextLength < k_outputAccumulationBufferSize);
m_outputAccumulationBuffer[endOfAccumulatedText+appendedTextLength] = 0;
flushOutputAccumulationBufferToStore();
appendTextToOutputAccumulationBuffer(&text[appendedTextLength], length - appendedTextLength);
}
// TODO: is it really needed? Maybe discard to optimize?
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());
}
}