diff --git a/src/backends/python/pythonserver.cpp b/src/backends/python/pythonserver.cpp index 101cf90c..fff804f8 100644 --- a/src/backends/python/pythonserver.cpp +++ b/src/backends/python/pythonserver.cpp @@ -1,221 +1,228 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2015 Minh Ngo */ #include "pythonserver.h" +#include -#include -#include #include -PythonServer::PythonServer(QObject* parent) : QObject(parent), m_pModule(nullptr) -{ -} +using namespace std; namespace { - QString pyObjectToQString(PyObject* obj) + string pyObjectToQString(PyObject* obj) { #if PY_MAJOR_VERSION == 3 - return QString::fromUtf8(PyUnicode_AsUTF8(obj)); + return string(PyUnicode_AsUTF8(obj)); #elif PY_MAJOR_VERSION == 2 - return QString::fromLocal8Bit(PyString_AsString(obj)); + return string(PyString_AsString(obj)); #else #warning Unknown Python version #endif } } void PythonServer::login() { Py_InspectFlag = 1; Py_Initialize(); m_pModule = PyImport_AddModule("__main__"); - filePath = QStringLiteral("python_cantor_worksheet"); + filePath = "python_cantor_worksheet"; } void PythonServer::interrupt() { PyErr_SetInterrupt(); } -void PythonServer::runPythonCommand(const QString& command) const +void PythonServer::runPythonCommand(const string& command) const { PyObject* py_dict = PyModule_GetDict(m_pModule); const char* prepareCommand = "import sys;\n"\ "class CatchOutPythonBackend:\n"\ " def __init__(self):\n"\ " self.value = ''\n"\ " def write(self, txt):\n"\ " self.value += txt\n"\ "outputPythonBackend = CatchOutPythonBackend()\n"\ "errorPythonBackend = CatchOutPythonBackend()\n"\ "sys.stdout = outputPythonBackend\n"\ "sys.stderr = errorPythonBackend\n"; PyRun_SimpleString(prepareCommand); #if PY_MAJOR_VERSION == 3 - PyObject* compile = Py_CompileString(command.toStdString().c_str(), filePath.toStdString().c_str(), Py_single_input); + PyObject* compile = Py_CompileString(command.c_str(), filePath.c_str(), Py_single_input); // There are two reasons for the error: // 1) This code is not single expression, so we can't compile this with flag Py_single_input // 2) There are errors in the code if (PyErr_Occurred()) { PyErr_Clear(); // Try to recompile code as sequence of expressions - compile = Py_CompileString(command.toStdString().c_str(), filePath.toStdString().c_str(), Py_file_input); + compile = Py_CompileString(command.c_str(), filePath.c_str(), Py_file_input); if (PyErr_Occurred()) { // We now know, that we have a syntax error, so print the traceback and exit PyErr_PrintEx(0); return; } } PyEval_EvalCode(compile, py_dict, py_dict); #elif PY_MAJOR_VERSION == 2 // Python 2.X don't check, that input string contains only one expression. // So for checking this, we compile string as file and as single expression and compare bytecode // FIXME? - PyObject* codeFile = Py_CompileString(command.toStdString().c_str(), filePath.toStdString().c_str(), Py_file_input); + PyObject* codeFile = Py_CompileString(command.c_str(), filePath.c_str(), Py_file_input); if (PyErr_Occurred()) { PyErr_PrintEx(0); return; } - PyObject* codeSingle = Py_CompileString(command.toStdString().c_str(), filePath.toStdString().c_str(), Py_single_input); + PyObject* codeSingle = Py_CompileString(command.c_str(), filePath.c_str(), Py_single_input); if (PyErr_Occurred()) { // We have error with Py_single_input, but haven't error with Py_file_input // So, the code can't be compiled as singel input -> use file input right away PyErr_Clear(); PyEval_EvalCode((PyCodeObject*)codeFile, py_dict, py_dict); } else { PyObject* bytecode1 = ((PyCodeObject*)codeSingle)->co_code; PyObject* bytecode2 = ((PyCodeObject*)codeFile)->co_code; if (PyObject_Length(bytecode1) >= PyObject_Length(bytecode2)) { PyEval_EvalCode((PyCodeObject*)codeSingle, py_dict, py_dict); } else { PyEval_EvalCode((PyCodeObject*)codeFile, py_dict, py_dict); } } #else #warning Unknown Python version #endif if (PyErr_Occurred()) PyErr_PrintEx(0); } -QString PythonServer::getError() const +string PythonServer::getError() const { PyObject *errorPython = PyObject_GetAttrString(m_pModule, "errorPythonBackend"); PyObject *error = PyObject_GetAttrString(errorPython, "value"); return pyObjectToQString(error); } -QString PythonServer::getOutput() const +string PythonServer::getOutput() const { PyObject *outputPython = PyObject_GetAttrString(m_pModule, "outputPythonBackend"); PyObject *output = PyObject_GetAttrString(outputPython, "value"); return pyObjectToQString(output); } -void PythonServer::setFilePath(const QString& path) +void PythonServer::setFilePath(const string& path, const string& dir) { - this->filePath = path; - PyRun_SimpleString(("import sys; sys.argv = ['" + path.toStdString() + "']").c_str()); - QString dir = QFileInfo(path).absoluteDir().absolutePath(); - PyRun_SimpleString(("import sys; sys.path.insert(0, '" + dir.toStdString() + "')").c_str()); - PyRun_SimpleString(("__file__ = '"+path.toStdString()+"'").c_str()); + PyRun_SimpleString(("import sys; sys.argv = ['" + path + "']").c_str()); + if (path.length() == 0) // New session, not from file + { + PyRun_SimpleString("import sys; sys.path.insert(0, '')"); + } + else + { + this->filePath = path; + PyRun_SimpleString(("import sys; sys.path.insert(0, '" + dir + "')").c_str()); + PyRun_SimpleString(("__file__ = '"+path+"'").c_str()); + } } -QString PythonServer::variables(bool parseValue) const +string PythonServer::variables(bool parseValue) const { PyRun_SimpleString( "try: \n" " import numpy \n" " __cantor_numpy_internal__ = numpy.get_printoptions()['threshold'] \n" " numpy.set_printoptions(threshold=100000000) \n" #if PY_MAJOR_VERSION == 3 "except ModuleNotFoundError: \n" #elif PY_MAJOR_VERSION == 2 "except ImportError: \n" #endif " pass \n" ); PyRun_SimpleString("__tmp_globals__ = globals()"); PyObject* globals = PyObject_GetAttrString(m_pModule,"__tmp_globals__"); PyObject *key, *value; Py_ssize_t pos = 0; - QStringList vars; + vector vars; while (PyDict_Next(globals, &pos, &key, &value)) { - const QString& keyString = pyObjectToQString(key); - if (keyString.startsWith(QLatin1String("__"))) + const string& keyString = pyObjectToQString(key); + if (keyString.substr(0, 2) == string("__")) continue; - if (keyString == QLatin1String("CatchOutPythonBackend") - || keyString == QLatin1String("errorPythonBackend") - || keyString == QLatin1String("outputPythonBackend")) + if (keyString == string("CatchOutPythonBackend") + || keyString == string("errorPythonBackend") + || keyString == string("outputPythonBackend")) continue; if (PyModule_Check(value)) continue; if (PyFunction_Check(value)) continue; if (PyType_Check(value)) continue; - QString valueString; + string valueString; if (parseValue) valueString = pyObjectToQString(PyObject_Repr(value)); - - vars.append(keyString + QChar(17) + valueString); + vars.push_back(keyString + char(17) + valueString); } PyRun_SimpleString( "try: \n" " import numpy \n" " numpy.set_printoptions(threshold=__cantor_numpy_internal__) \n" " del __cantor_numpy_internal__ \n" #if PY_MAJOR_VERSION == 3 "except ModuleNotFoundError: \n" #elif PY_MAJOR_VERSION == 2 "except ImportError: \n" #endif " pass \n" ); - return vars.join(QChar(18))+QChar(18); + + string result; + for (const string& s : vars) + result += s + char(18); + result += char(18); + return result; } diff --git a/src/backends/python/pythonserver.h b/src/backends/python/pythonserver.h index b720f1f1..76919cc3 100644 --- a/src/backends/python/pythonserver.h +++ b/src/backends/python/pythonserver.h @@ -1,49 +1,47 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2015 Minh Ngo */ #ifndef _PYTHONSERVER_H #define _PYTHONSERVER_H -#include -#include +#include struct _object; using PyObject = _object; -class PythonServer : public QObject +class PythonServer { - Q_OBJECT public: - explicit PythonServer(QObject* parent = nullptr); + explicit PythonServer() = default; - public Q_SLOTS: + public: void login(); void interrupt(); - void setFilePath(const QString& path); - void runPythonCommand(const QString& command) const; - QString getOutput() const; - QString getError() const; - QString variables(bool parseValue) const; + void setFilePath(const std::string& path, const std::string& dir); + void runPythonCommand(const std::string& command) const; + std::string getOutput() const; + std::string getError() const; + std::string variables(bool parseValue) const; private: - PyObject* m_pModule; - QString filePath; + PyObject* m_pModule{nullptr}; + std::string filePath; }; #endif diff --git a/src/backends/python/pythonservermain.cpp b/src/backends/python/pythonservermain.cpp index 4f05d2fe..99aab81c 100644 --- a/src/backends/python/pythonservermain.cpp +++ b/src/backends/python/pythonservermain.cpp @@ -1,142 +1,140 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2015 Minh Ngo */ #include #include - -#include -#include -#include -#include +#include +#include #include "pythonserver.h" -const QChar recordSep(30); -const QChar unitSep(31); +using namespace std; + const char messageEnd = 29; +const char recordSep = 30; +const char unitSep = 31; PythonServer server; bool isInterrupted = false; -QTimer inputTimer; -QMetaObject::Connection connection; - -QString inputBuffer; - -QLatin1String LOGIN("login"); -QLatin1String EXIT("exit"); -QLatin1String CODE("code"); -QLatin1String FILEPATH("setFilePath"); -QLatin1String MODEL("model"); - -void routeInput() { - QByteArray bytes; - char c; - while (std::cin.get(c)) - { - if (messageEnd == c) - break; - else - bytes.append(c); - } - inputBuffer.append(QString::fromLocal8Bit(bytes)); - if (inputBuffer.isEmpty()) - return; - - const QStringList& records = inputBuffer.split(recordSep); - inputBuffer.clear(); - if (records.size() == 2) - { - if (records[0] == EXIT) - { - QObject::disconnect(connection); - QObject::connect(&inputTimer, &QTimer::timeout, QCoreApplication::instance(), &QCoreApplication::quit); - } - else if (records[0] == LOGIN) - { - server.login(); - } - else if (records[0] == CODE) - { - server.runPythonCommand(records[1]); - - if (!isInterrupted) - { - const QString& result = - server.getOutput() - + unitSep - + server.getError() - + QLatin1Char(messageEnd); - - const QByteArray bytes = result.toLocal8Bit(); - std::cout << bytes.data(); - } - else - { - // No replay when interrupted - isInterrupted = false; - } - } - else if (records[0] == FILEPATH) - { - server.setFilePath(records[1]); - } - else if (records[0] == MODEL) - { - bool ok; - bool val = records[1].toInt(&ok); - - QString result; - if (ok) - result = server.variables(val) + unitSep; - else - result = unitSep + QLatin1String("Invalid argument %1 for 'model' command", val); - result += QLatin1Char(messageEnd); - - const QByteArray bytes = result.toLocal8Bit(); - std::cout << bytes.data(); - } - std::cout.flush(); - } -} +string LOGIN("login"); +string EXIT("exit"); +string CODE("code"); +string FILEPATH("setFilePath"); +string MODEL("model"); void signal_handler(int signal) { if (signal == SIGINT) { isInterrupted = true; server.interrupt(); } } +vector split(string s, char delimiter) +{ + vector results; + + size_t pos = 0; + std::string token; + while ((pos = s.find(delimiter)) != std::string::npos) { + token = s.substr(0, pos); + results.push_back(token); + s.erase(0, pos + 1); + } + results.push_back(s); + + return results; +} -int main(int argc, char *argv[]) +int main() { std::signal(SIGINT, signal_handler); - QCoreApplication app(argc, argv); - - connection = QObject::connect(&inputTimer, &QTimer::timeout, routeInput); - inputTimer.setInterval(100); - inputTimer.start(); std::cout << "ready" << std::endl; - return app.exec(); + std::string input; + while (getline(std::cin, input, messageEnd)) + { + const vector& records = split(input, recordSep); + + if (records.size() == 2) + { + if (records[0] == EXIT) + { + //Exit from cycle and finish program + break; + } + else if (records[0] == LOGIN) + { + server.login(); + } + if (records[0] == FILEPATH) + { + vector args = split(records[1], unitSep); + if (args.size() == 2) + server.setFilePath(args[1], args[2]); + } + else if (records[0] == CODE) + { + server.runPythonCommand(records[1]); + + if (!isInterrupted) + { + const string& result = + server.getOutput() + + unitSep + + server.getError() + + messageEnd; + + std::cout << result.c_str(); + } + else + { + // No replay when interrupted + isInterrupted = false; + } + } + else if (records[0] == MODEL) + { + bool ok, val; + try { + val = (bool)stoi(records[1]); + ok = true; + } catch (std::invalid_argument e) { + ok = false; + }; + + string result; + if (ok) + result = server.variables(val) + unitSep; + else + result = unitSep + string("Invalid argument for 'model' command"); + result += messageEnd; + + std::cout << result.c_str(); + } + std::cout.flush(); + } + } + + return 0; } diff --git a/src/backends/python/pythonsession.cpp b/src/backends/python/pythonsession.cpp index f0e43e4d..b814599e 100644 --- a/src/backends/python/pythonsession.cpp +++ b/src/backends/python/pythonsession.cpp @@ -1,235 +1,270 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2012 Filipe Saraiva Copyright (C) 2015 Minh Ngo */ #include #include "pythonsession.h" #include "pythonexpression.h" #include "pythonvariablemodel.h" #include "pythonhighlighter.h" #include "pythoncompletionobject.h" #include "pythonkeywords.h" #include "pythonutils.h" #include #include #include #include +#include #include +#include #ifndef Q_OS_WIN #include #endif const QChar recordSep(30); const QChar unitSep(31); const QChar messageEnd = 29; PythonSession::PythonSession(Cantor::Backend* backend, int pythonVersion, const QString serverName) : Session(backend) , m_process(nullptr) , serverName(serverName) , m_pythonVersion(pythonVersion) { setVariableModel(new PythonVariableModel(this)); } PythonSession::~PythonSession() { if (m_process) { + disconnect(m_process, &QProcess::errorOccurred, this, &PythonSession::reportServerProcessError); m_process->kill(); m_process->deleteLater(); m_process = nullptr; } } void PythonSession::login() { qDebug()<<"login"; emit loginStarted(); if (m_process) m_process->deleteLater(); m_process = new QProcess(this); m_process->setProcessChannelMode(QProcess::ForwardedErrorChannel); m_process->start(QStandardPaths::findExecutable(serverName)); m_process->waitForStarted(); m_process->waitForReadyRead(); QTextStream stream(m_process->readAllStandardOutput()); const QString& readyStatus = QString::fromLatin1("ready"); while (m_process->state() == QProcess::Running) { const QString& rl = stream.readLine(); if (rl == readyStatus) break; } connect(m_process, &QProcess::readyReadStandardOutput, this, &PythonSession::readOutput); + connect(m_process, &QProcess::errorOccurred, this, &PythonSession::reportServerProcessError); sendCommand(QLatin1String("login")); - sendCommand(QLatin1String("setFilePath"), QStringList(worksheetPath)); + QString dir; + if (!worksheetPath.isEmpty()) + dir = QFileInfo(worksheetPath).absoluteDir().absolutePath(); + sendCommand(QLatin1String("setFilePath"), QStringList() << worksheetPath << dir); const QStringList& scripts = autorunScripts(); if(!scripts.isEmpty()){ QString autorunScripts = scripts.join(QLatin1String("\n")); evaluateExpression(autorunScripts, Cantor::Expression::DeleteOnFinish, true); variableModel()->update(); } changeStatus(Session::Done); emit loginDone(); } void PythonSession::logout() { if (!m_process) return; sendCommand(QLatin1String("exit")); if(!m_process->waitForFinished(1000)) { + disconnect(m_process, &QProcess::errorOccurred, this, &PythonSession::reportServerProcessError); m_process->kill(); qDebug()<<"cantor_python server still running, process kill enforced"; } m_process->deleteLater(); m_process = nullptr; qDebug()<<"logout"; Session::logout(); } void PythonSession::interrupt() { if(!expressionQueue().isEmpty()) { qDebug()<<"interrupting " << expressionQueue().first()->command(); if(m_process && m_process->state() != QProcess::NotRunning) { #ifndef Q_OS_WIN const int pid=m_process->pid(); kill(pid, SIGINT); #else ; //TODO: interrupt the process on windows #endif } for (Cantor::Expression* expression : expressionQueue()) expression->setStatus(Cantor::Expression::Interrupted); expressionQueue().clear(); m_output.clear(); qDebug()<<"done interrupting"; } changeStatus(Cantor::Session::Done); } Cantor::Expression* PythonSession::evaluateExpression(const QString& cmd, Cantor::Expression::FinishingBehavior behave, bool internal) { qDebug() << "evaluating: " << cmd; PythonExpression* expr = new PythonExpression(this, internal); changeStatus(Cantor::Session::Running); expr->setFinishingBehavior(behave); expr->setCommand(cmd); expr->evaluate(); return expr; } QSyntaxHighlighter* PythonSession::syntaxHighlighter(QObject* parent) { return new PythonHighlighter(parent, this, m_pythonVersion); } Cantor::CompletionObject* PythonSession::completionFor(const QString& command, int index) { return new PythonCompletionObject(command, index, this); } void PythonSession::runFirstExpression() { if (expressionQueue().isEmpty()) return; Cantor::Expression* expr = expressionQueue().first(); const QString command = expr->internalCommand(); qDebug() << "run first expression" << command; expr->setStatus(Cantor::Expression::Computing); if (expr->isInternal() && command.startsWith(QLatin1String("%variables "))) { const QString arg = command.section(QLatin1String(" "), 1); sendCommand(QLatin1String("model"), QStringList(arg)); } else sendCommand(QLatin1String("code"), QStringList(expr->internalCommand())); } void PythonSession::sendCommand(const QString& command, const QStringList arguments) const { qDebug() << "send command: " << command << arguments; const QString& message = command + recordSep + arguments.join(unitSep) + messageEnd; m_process->write(message.toLocal8Bit()); } void PythonSession::readOutput() { while (m_process->bytesAvailable() > 0) - m_output.append(QString::fromLocal8Bit(m_process->readAll())); + { + const QByteArray& bytes = m_process->readAll(); + if (m_pythonVersion == 3) + m_output.append(QString::fromUtf8(bytes)); + else if (m_pythonVersion == 2) + m_output.append(QString::fromLocal8Bit(bytes)); + else + qCritical() << "Unsupported Python version" << m_pythonVersion; + } qDebug() << "m_output: " << m_output; if (!m_output.contains(messageEnd)) return; const QStringList packages = m_output.split(messageEnd, QString::SkipEmptyParts); if (m_output.endsWith(messageEnd)) m_output.clear(); else m_output = m_output.section(messageEnd, -1); for (const QString& message: packages) { if (expressionQueue().isEmpty()) break; const QString& output = message.section(unitSep, 0, 0); const QString& error = message.section(unitSep, 1, 1); if(error.isEmpty()){ static_cast(expressionQueue().first())->parseOutput(output); } else { static_cast(expressionQueue().first())->parseError(error); } finishFirstExpression(true); } } void PythonSession::setWorksheetPath(const QString& path) { worksheetPath = path; } + +void PythonSession::reportServerProcessError(QProcess::ProcessError serverError) +{ + switch(serverError) + { + case QProcess::Crashed: + emit error(i18n("Cantor Python server stopped working.")); + break; + + case QProcess::FailedToStart: + emit error(i18n("Failed to start Cantor python server.")); + break; + + default: + emit error(i18n("Communication with Cantor python server failed for unknown reasons.")); + break; + } + logout(); +} diff --git a/src/backends/python/pythonsession.h b/src/backends/python/pythonsession.h index 0e93dcf1..4b9f3258 100644 --- a/src/backends/python/pythonsession.h +++ b/src/backends/python/pythonsession.h @@ -1,68 +1,70 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2012 Filipe Saraiva Copyright (C) 2015 Minh Ngo */ #ifndef _PYTHONSESSION_H #define _PYTHONSESSION_H #include "session.h" #include #include - -class QProcess; +#include class CANTOR_PYTHONBACKEND_EXPORT PythonSession : public Cantor::Session { Q_OBJECT public: PythonSession(Cantor::Backend* backend, int pythonVersion, const QString serverName); ~PythonSession() override; void login() override; void logout() override; void interrupt() override; Cantor::Expression* evaluateExpression(const QString& command, Cantor::Expression::FinishingBehavior behave = Cantor::Expression::FinishingBehavior::DoNotDelete, bool internal = false) override; Cantor::CompletionObject* completionFor(const QString& command, int index=-1) override; QSyntaxHighlighter* syntaxHighlighter(QObject* parent) override; void setWorksheetPath(const QString& path) override; virtual bool integratePlots() const = 0; virtual QStringList autorunScripts() const = 0; virtual bool variableManagement() const = 0; private: QProcess* m_process; QString serverName; QString worksheetPath; int m_pythonVersion; QString m_output; + private Q_SLOT: + void readOutput(); + void reportServerProcessError(QProcess::ProcessError serverError); + private: void runFirstExpression() override; - void readOutput(); void sendCommand(const QString& command, const QStringList arguments = QStringList()) const; }; #endif /* _PYTHONSESSION_H */ diff --git a/src/backends/python2/CMakeLists.txt b/src/backends/python2/CMakeLists.txt index 7bfa1f38..109f4715 100644 --- a/src/backends/python2/CMakeLists.txt +++ b/src/backends/python2/CMakeLists.txt @@ -1,36 +1,36 @@ set( Python2Backend_SRCS python2backend.cpp python2session.cpp ) set(Python2Server_SRCS ../python/pythonservermain.cpp ../python/pythonserver.cpp ) kconfig_add_kcfg_files(Python2Backend_SRCS settings.kcfgc) if(MSVC) # ssize_t is typedef'd in both kdewin and python headers, this prevents using the kdewin one add_definitions(-D_SSIZE_T_DEFINED) endif(MSVC) include_directories(${PYTHON_LIBRARIES_DIR}) include_directories(${PYTHON_INCLUDE_DIR}) add_backend(python2backend ${Python2Backend_SRCS}) target_link_libraries(cantor_python2backend ${PYTHON_LIBRARIES} cantor_pythonbackend) add_executable(cantor_python2server ${Python2Server_SRCS}) set_target_properties(cantor_python2server PROPERTIES INSTALL_RPATH_USE_LINK_PATH false) -target_link_libraries(cantor_python2server ${PYTHON_LIBRARIES} Qt5::Widgets) +target_link_libraries(cantor_python2server ${PYTHON_LIBRARIES}) if(BUILD_TESTING) add_executable(testpython2 testpython2.cpp settings.cpp) target_link_libraries(testpython2 ${QT_QTTEST_LIBRARY} cantorlibs cantortest) add_test(NAME testpython2 COMMAND testpython2) endif() install(FILES cantor_python2.knsrc DESTINATION ${KDE_INSTALL_CONFDIR}) install(FILES python2backend.kcfg DESTINATION ${KDE_INSTALL_KCFGDIR}) install(TARGETS cantor_python2server ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/src/backends/python2/testpython2.cpp b/src/backends/python2/testpython2.cpp index b609c4f6..bc4b3889 100644 --- a/src/backends/python2/testpython2.cpp +++ b/src/backends/python2/testpython2.cpp @@ -1,308 +1,314 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2013 Tuukka Verho */ #include "testpython2.h" #include "session.h" #include "backend.h" #include "expression.h" #include "result.h" #include "defaultvariablemodel.h" #include "imageresult.h" #include "completionobject.h" #include "settings.h" QString TestPython2::backendName() { return QLatin1String("python2"); } void TestPython2::testCommandQueue() { Cantor::Expression* e1=session()->evaluateExpression(QLatin1String("0+1")); Cantor::Expression* e2=session()->evaluateExpression(QLatin1String("1+1")); Cantor::Expression* e3=evalExp(QLatin1String("1+2")); QVERIFY(e1!=nullptr); QVERIFY(e2!=nullptr); QVERIFY(e3!=nullptr); QVERIFY(e1->result()); QVERIFY(e2->result()); QVERIFY(e3->result()); QCOMPARE(cleanOutput(e1->result()->data().toString()), QLatin1String("1")); QCOMPARE(cleanOutput(e2->result()->data().toString()), QLatin1String("2")); QCOMPARE(cleanOutput(e3->result()->data().toString()), QLatin1String("3")); } void TestPython2::testImportStatement() { Cantor::Expression* e = evalExp(QLatin1String("import sys")); QVERIFY(e != nullptr); QCOMPARE(e->status(), Cantor::Expression::Done); } void TestPython2::testCodeWithComments() { { Cantor::Expression* e = evalExp(QLatin1String("#comment\n1+2")); QVERIFY(e != nullptr); QVERIFY(e->result()); QVERIFY(e->result()->data().toString() == QLatin1String("3")); } { Cantor::Expression* e = evalExp(QLatin1String(" #comment\n1+2")); QVERIFY(e != nullptr); QVERIFY(e->result()); QVERIFY(e->result()->data().toString() == QLatin1String("3")); } } void TestPython2::testSimpleCode() { Cantor::Expression* e=evalExp( QLatin1String("2+2")); QVERIFY( e!=nullptr ); QVERIFY( e->result()!=nullptr ); QCOMPARE( cleanOutput(e->result()->data().toString()), QLatin1String("4") ); } void TestPython2::testMultilineCode() { Cantor::Expression* e=evalExp(QLatin1String( "a = 2+2\n" "b = 3+3\n" "print a,b" )); QVERIFY( e!=nullptr ); QVERIFY( e->result()!=nullptr ); QCOMPARE( cleanOutput(e->result()->data().toString()), QLatin1String("4 6") ); evalExp(QLatin1String("del a; del b")); } void TestPython2::testVariablesCreatingFromCode() { + if (!PythonSettings::variableManagement()) + QSKIP("This test needs enabled variable management in Python2 settings", SkipSingle); + QAbstractItemModel* model = session()->variableModel(); QVERIFY(model != nullptr); Cantor::Expression* e=evalExp(QLatin1String("a = 15; b = 'S';")); QVERIFY(e!=nullptr); if(session()->status()==Cantor::Session::Running) waitForSignal(session(), SIGNAL(statusChanged(Cantor::Session::Status))); QCOMPARE(2, model->rowCount()); QCOMPARE(model->index(0,0).data().toString(), QLatin1String("a")); QCOMPARE(model->index(0,1).data().toString(), QLatin1String("15")); QCOMPARE(model->index(1,0).data().toString(), QLatin1String("b")); QCOMPARE(model->index(1,1).data().toString(), QLatin1String("'S'")); evalExp(QLatin1String("del a; del b")); } void TestPython2::testVariableCleanupAfterRestart() { Cantor::DefaultVariableModel* model = session()->variableModel(); QVERIFY(model != nullptr); Cantor::Expression* e=evalExp(QLatin1String("a = 15; b = 'S';")); QVERIFY(e!=nullptr); if(session()->status()==Cantor::Session::Running) waitForSignal(session(), SIGNAL(statusChanged(Cantor::Session::Status))); QCOMPARE(2, static_cast(model)->rowCount()); session()->logout(); session()->login(); QCOMPARE(0, static_cast(model)->rowCount()); } void TestPython2::testDictVariable() { + if (!PythonSettings::variableManagement()) + QSKIP("This test needs enabled variable management in Python2 settings", SkipSingle); + Cantor::DefaultVariableModel* model = session()->variableModel(); QVERIFY(model != nullptr); Cantor::Expression* e=evalExp(QLatin1String("d = {'value': 33}")); QVERIFY(e!=nullptr); if(session()->status()==Cantor::Session::Running) waitForSignal(session(), SIGNAL(statusChanged(Cantor::Session::Status))); QCOMPARE(1, static_cast(model)->rowCount()); QCOMPARE(model->index(0,0).data().toString(), QLatin1String("d")); QCOMPARE(model->index(0,1).data().toString(), QLatin1String("{'value': 33}")); evalExp(QLatin1String("del d")); } void TestPython2::testCommentExpression() { Cantor::Expression* e = evalExp(QLatin1String("#only comment")); QVERIFY(e != nullptr); QCOMPARE(e->status(), Cantor::Expression::Status::Done); QCOMPARE(e->results().size(), 0); } void TestPython2::testInvalidSyntax() { Cantor::Expression* e=evalExp( QLatin1String("2+2*+.") ); QVERIFY( e!=nullptr ); QCOMPARE( e->status(), Cantor::Expression::Error ); } void TestPython2::testSimplePlot() { if (!PythonSettings::integratePlots()) QSKIP("This test needs enabled plots integration in Python2 settings", SkipSingle); Cantor::Expression* e = evalExp(QLatin1String( "import matplotlib\n" "import matplotlib.pyplot as plt\n" "import numpy as np" )); QVERIFY(e != nullptr); QVERIFY(e->errorMessage().isEmpty() == true); //the variable model shouldn't have any entries after the module imports QAbstractItemModel* model = session()->variableModel(); QVERIFY(model != nullptr); if(session()->status()==Cantor::Session::Running) waitForSignal(session(), SIGNAL(statusChanged(Cantor::Session::Status))); QVERIFY(model->rowCount() == 0); //create data for plotting e = evalExp(QLatin1String( "t = np.arange(0.0, 2.0, 0.01)\n" "s = 1 + np.sin(2 * np.pi * t)" )); QVERIFY(e != nullptr); QVERIFY(e->errorMessage().isEmpty() == true); if(session()->status()==Cantor::Session::Running) waitForSignal(session(), SIGNAL(statusChanged(Cantor::Session::Status))); //the variable model should have two entries now QVERIFY(model->rowCount() == 2); //plot the data and check the results e = evalExp(QLatin1String( "plt.plot(t,s)\n" "plt.show()" )); QVERIFY(e != nullptr); if (e->result() == nullptr) waitForSignal(e, SIGNAL(gotResult())); QVERIFY(e->errorMessage().isEmpty() == true); QVERIFY(model->rowCount() == 2); //still only two variables //there must be one single image result QVERIFY(e->results().size() == 1); const Cantor::ImageResult* result = dynamic_cast(e->result()); QVERIFY(result != nullptr); evalExp(QLatin1String("del t; del s")); } void TestPython2::testSimpleExpressionWithComment() { Cantor::Expression* e = evalExp(QLatin1String("2+2 # comment")); QVERIFY(e != nullptr); QVERIFY(e->result()); QVERIFY(e->result()->data().toString() == QLatin1String("4")); } void TestPython2::testMultilineCommandWithComment() { Cantor::Expression* e = evalExp(QLatin1String( "print 2+2 \n" "#comment in middle \n" "print 7*5")); QVERIFY(e != nullptr); QVERIFY(e->result()); QVERIFY(e->result()->data().toString() == QLatin1String("4\n35")); } void TestPython2::testCompletion() { if(session()->status()==Cantor::Session::Running) waitForSignal(session(), SIGNAL(statusChanged(Cantor::Session::Status))); Cantor::CompletionObject* help = session()->completionFor(QLatin1String("ma"), 2); waitForSignal(help, SIGNAL(fetchingDone())); // Checks all completions for this request // This correct for Python 2.7.15 const QStringList& completions = help->completions(); qDebug() << completions; QCOMPARE(completions.size(), 2); QVERIFY(completions.contains(QLatin1String("map"))); QVERIFY(completions.contains(QLatin1String("max"))); } void TestPython2::testInterrupt() { Cantor::Expression* e1=session()->evaluateExpression(QLatin1String("import time; time.sleep(45)")); Cantor::Expression* e2=session()->evaluateExpression(QLatin1String("2")); // Wait, because server need time to read input QTest::qWait(100); QCOMPARE(e1->status(), Cantor::Expression::Computing); QCOMPARE(e2->status(), Cantor::Expression::Queued); while(session()->status() != Cantor::Session::Running) waitForSignal(session(), SIGNAL(statusChanged(Cantor::Session::Status))); session()->interrupt(); while(session()->status() != Cantor::Session::Done) waitForSignal(session(), SIGNAL(statusChanged(Cantor::Session::Status))); QCOMPARE(e1->status(), Cantor::Expression::Interrupted); QCOMPARE(e2->status(), Cantor::Expression::Interrupted); Cantor::Expression* e = evalExp(QLatin1String("2+2")); QVERIFY(e != nullptr); QVERIFY(e->result()); QCOMPARE(e->result()->data().toString(), QLatin1String("4")); } QTEST_MAIN(TestPython2) diff --git a/src/backends/python3/CMakeLists.txt b/src/backends/python3/CMakeLists.txt index 52b3b6ab..245401a5 100644 --- a/src/backends/python3/CMakeLists.txt +++ b/src/backends/python3/CMakeLists.txt @@ -1,35 +1,35 @@ set(Python3Backend_SRCS python3backend.cpp python3session.cpp ) set(Python3Server_SRCS ../python/pythonservermain.cpp ../python/pythonserver.cpp ) include_directories(${PYTHONLIBS3_INCLUDE_DIRS}) kconfig_add_kcfg_files(Python3Backend_SRCS settings.kcfgc) add_backend(python3backend ${Python3Backend_SRCS}) target_link_libraries(cantor_python3backend cantor_pythonbackend) add_executable(cantor_python3server ${Python3Server_SRCS}) set_target_properties(cantor_python3server PROPERTIES INSTALL_RPATH_USE_LINK_PATH false) -target_link_libraries(cantor_python3server ${PYTHONLIBS3_LIBRARIES} Qt5::Widgets) +target_link_libraries(cantor_python3server ${PYTHONLIBS3_LIBRARIES}) if(BUILD_TESTING) add_executable(testpython3 testpython3.cpp settings.cpp) add_test(NAME testpython3 COMMAND testpython3) target_link_libraries(testpython3 Qt5::Test KF5::ConfigCore KF5::ConfigGui cantorlibs cantortest ) endif(BUILD_TESTING) install(FILES cantor_python3.knsrc DESTINATION ${KDE_INSTALL_CONFDIR}) install(FILES python3backend.kcfg DESTINATION ${KDE_INSTALL_KCFGDIR}) install(TARGETS cantor_python3server ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/src/backends/python3/testpython3.cpp b/src/backends/python3/testpython3.cpp index c31209d2..a802fb31 100644 --- a/src/backends/python3/testpython3.cpp +++ b/src/backends/python3/testpython3.cpp @@ -1,320 +1,326 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2015 Minh Ngo */ #include "testpython3.h" #include "session.h" #include "backend.h" #include "expression.h" #include "imageresult.h" #include "defaultvariablemodel.h" #include "completionobject.h" #include "settings.h" QString TestPython3::backendName() { return QLatin1String("python3"); } void TestPython3::testSimpleCommand() { Cantor::Expression* e = evalExp(QLatin1String("2+2")); QVERIFY(e != nullptr); QVERIFY(e->result()); QVERIFY(e->result()->data().toString() == QLatin1String("4")); } void TestPython3::testMultilineCommand() { Cantor::Expression* e = evalExp(QLatin1String("print(2+2)\nprint(7*5)")); QVERIFY(e != nullptr); QVERIFY(e->result()); QVERIFY(e->result()->data().toString() == QLatin1String("4\n35")); } void TestPython3::testCommandQueue() { Cantor::Expression* e1=session()->evaluateExpression(QLatin1String("0+1")); Cantor::Expression* e2=session()->evaluateExpression(QLatin1String("1+1")); Cantor::Expression* e3=evalExp(QLatin1String("1+2")); QVERIFY(e1!=nullptr); QVERIFY(e2!=nullptr); QVERIFY(e3!=nullptr); QVERIFY(e1->result()); QVERIFY(e2->result()); QVERIFY(e3->result()); QCOMPARE(cleanOutput(e1->result()->data().toString()), QLatin1String("1")); QCOMPARE(cleanOutput(e2->result()->data().toString()), QLatin1String("2")); QCOMPARE(cleanOutput(e3->result()->data().toString()), QLatin1String("3")); } void TestPython3::testCommentExpression() { Cantor::Expression* e = evalExp(QLatin1String("#only comment")); QVERIFY(e != nullptr); QCOMPARE(e->status(), Cantor::Expression::Status::Done); QCOMPARE(e->results().size(), 0); } void TestPython3::testSimpleExpressionWithComment() { Cantor::Expression* e = evalExp(QLatin1String("2+2 # comment")); QVERIFY(e != nullptr); QVERIFY(e->result()); QVERIFY(e->result()->data().toString() == QLatin1String("4")); } void TestPython3::testMultilineCommandWithComment() { Cantor::Expression* e = evalExp(QLatin1String( "print(2+2) \n" "#comment in middle \n" "print(7*5)")); QVERIFY(e != nullptr); QVERIFY(e->result()); QVERIFY(e->result()->data().toString() == QLatin1String("4\n35")); } void TestPython3::testInvalidSyntax() { Cantor::Expression* e=evalExp( QLatin1String("2+2*+.") ); QVERIFY( e!=nullptr ); QCOMPARE( e->status(), Cantor::Expression::Error ); } void TestPython3::testCompletion() { if(session()->status()==Cantor::Session::Running) waitForSignal(session(), SIGNAL(statusChanged(Cantor::Session::Status))); Cantor::CompletionObject* help = session()->completionFor(QLatin1String("p"), 1); waitForSignal(help, SIGNAL(fetchingDone())); // Checks all completions for this request // This correct for Python 3.6.7 const QStringList& completions = help->completions(); qDebug() << completions; QCOMPARE(completions.size(), 4); QVERIFY(completions.contains(QLatin1String("pass"))); QVERIFY(completions.contains(QLatin1String("pow"))); QVERIFY(completions.contains(QLatin1String("print"))); QVERIFY(completions.contains(QLatin1String("property"))); } void TestPython3::testImportStatement() { Cantor::Expression* e = evalExp(QLatin1String("import sys")); QVERIFY(e != nullptr); QCOMPARE(e->status(), Cantor::Expression::Done); } void TestPython3::testCodeWithComments() { { Cantor::Expression* e = evalExp(QLatin1String("#comment\n1+2")); QVERIFY(e != nullptr); QVERIFY(e->result()); QVERIFY(e->result()->data().toString() == QLatin1String("3")); } { Cantor::Expression* e = evalExp(QLatin1String(" #comment\n1+2")); QVERIFY(e != nullptr); QVERIFY(e->result()); QVERIFY(e->result()->data().toString() == QLatin1String("3")); } } void TestPython3::testPython3Code() { { Cantor::Expression* e = evalExp(QLatin1String("print 1 + 2")); QVERIFY(e != nullptr); QVERIFY(!e->errorMessage().isEmpty()); } { Cantor::Expression* e = evalExp(QLatin1String("print(1 + 2)")); QVERIFY(e != nullptr); QVERIFY(e->result()); QVERIFY(e->result()->data().toString() == QLatin1String("3")); } } void TestPython3::testSimplePlot() { if (!PythonSettings::integratePlots()) QSKIP("This test needs enabled plots integration in Python3 settings", SkipSingle); Cantor::Expression* e = evalExp(QLatin1String( "import matplotlib\n" "import matplotlib.pyplot as plt\n" "import numpy as np" )); QVERIFY(e != nullptr); QVERIFY(e->errorMessage().isEmpty() == true); //the variable model shouldn't have any entries after the module imports QAbstractItemModel* model = session()->variableModel(); QVERIFY(model != nullptr); QVERIFY(model->rowCount() == 0); //create data for plotting e = evalExp(QLatin1String( "t = np.arange(0.0, 2.0, 0.01)\n" "s = 1 + np.sin(2 * np.pi * t)" )); QVERIFY(e != nullptr); QVERIFY(e->errorMessage().isEmpty() == true); if(session()->status()==Cantor::Session::Running) waitForSignal(session(), SIGNAL(statusChanged(Cantor::Session::Status))); //the variable model should have two entries now QVERIFY(model->rowCount() == 2); //plot the data and check the results e = evalExp(QLatin1String( "plt.plot(t,s)\n" "plt.show()" )); QVERIFY(e != nullptr); if (e->result() == nullptr) waitForSignal(e, SIGNAL(gotResult())); QVERIFY(e->errorMessage().isEmpty() == true); QVERIFY(model->rowCount() == 2); //still only two variables //there must be one single image result QVERIFY(e->results().size() == 1); const Cantor::ImageResult* result = dynamic_cast(e->result()); QVERIFY(result != nullptr); evalExp(QLatin1String("del t; del s")); } void TestPython3::testVariablesCreatingFromCode() { + if (!PythonSettings::variableManagement()) + QSKIP("This test needs enabled variable management in Python3 settings", SkipSingle); + QAbstractItemModel* model = session()->variableModel(); QVERIFY(model != nullptr); Cantor::Expression* e=evalExp(QLatin1String("a = 15; b = 'S';")); QVERIFY(e!=nullptr); if(session()->status()==Cantor::Session::Running) waitForSignal(session(), SIGNAL(statusChanged(Cantor::Session::Status))); QCOMPARE(2, model->rowCount()); QCOMPARE(model->index(0,0).data().toString(), QLatin1String("a")); QCOMPARE(model->index(0,1).data().toString(), QLatin1String("15")); QCOMPARE(model->index(1,0).data().toString(), QLatin1String("b")); QCOMPARE(model->index(1,1).data().toString(), QLatin1String("'S'")); evalExp(QLatin1String("del a; del b")); } void TestPython3::testVariableCleanupAfterRestart() { Cantor::DefaultVariableModel* model = session()->variableModel(); QVERIFY(model != nullptr); Cantor::Expression* e=evalExp(QLatin1String("a = 15; b = 'S';")); QVERIFY(e!=nullptr); if(session()->status()==Cantor::Session::Running) waitForSignal(session(), SIGNAL(statusChanged(Cantor::Session::Status))); QCOMPARE(2, static_cast(model)->rowCount()); session()->logout(); session()->login(); QCOMPARE(0, static_cast(model)->rowCount()); } void TestPython3::testDictVariable() { + if (!PythonSettings::variableManagement()) + QSKIP("This test needs enabled variable management in Python3 settings", SkipSingle); + Cantor::DefaultVariableModel* model = session()->variableModel(); QVERIFY(model != nullptr); Cantor::Expression* e=evalExp(QLatin1String("d = {'value': 33}")); QVERIFY(e!=nullptr); if(session()->status()==Cantor::Session::Running) waitForSignal(session(), SIGNAL(statusChanged(Cantor::Session::Status))); QCOMPARE(1, static_cast(model)->rowCount()); QCOMPARE(model->index(0,0).data().toString(), QLatin1String("d")); QCOMPARE(model->index(0,1).data().toString(), QLatin1String("{'value': 33}")); evalExp(QLatin1String("del d")); } void TestPython3::testInterrupt() { Cantor::Expression* e1=session()->evaluateExpression(QLatin1String("import time; time.sleep(45)")); Cantor::Expression* e2=session()->evaluateExpression(QLatin1String("2")); // Wait, because server need time to read input QTest::qWait(100); QCOMPARE(e1->status(), Cantor::Expression::Computing); QCOMPARE(e2->status(), Cantor::Expression::Queued); while(session()->status() != Cantor::Session::Running) waitForSignal(session(), SIGNAL(statusChanged(Cantor::Session::Status))); session()->interrupt(); while(session()->status() != Cantor::Session::Done) waitForSignal(session(), SIGNAL(statusChanged(Cantor::Session::Status))); QCOMPARE(e1->status(), Cantor::Expression::Interrupted); QCOMPARE(e2->status(), Cantor::Expression::Interrupted); Cantor::Expression* e = evalExp(QLatin1String("2+2")); QVERIFY(e != nullptr); QCOMPARE(e->status(), Cantor::Expression::Done); QVERIFY(e->result()); QCOMPARE(e->result()->data().toString(), QLatin1String("4")); } QTEST_MAIN(TestPython3) diff --git a/src/commandentry.cpp b/src/commandentry.cpp index 6cd8afae..248a72f9 100644 --- a/src/commandentry.cpp +++ b/src/commandentry.cpp @@ -1,1353 +1,1356 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2009 Alexander Rieder Copyright (C) 2012 Martin Kuettler Copyright (C) 2018 Alexander Semke */ #include "commandentry.h" #include "resultitem.h" #include "loadedexpression.h" #include "jupyterutils.h" #include "lib/result.h" #include "lib/helpresult.h" #include "lib/epsresult.h" #include "lib/latexresult.h" #include "lib/completionobject.h" #include "lib/syntaxhelpobject.h" #include "lib/session.h" #include #include #include #include #include #include #include #include #include #include #include #include #include const QString CommandEntry::Prompt = QLatin1String(">>> "); const QString CommandEntry::MidPrompt = QLatin1String(">> "); const QString CommandEntry::HidePrompt = QLatin1String("> "); const double CommandEntry::HorizontalSpacing = 4; const double CommandEntry::VerticalSpacing = 4; static const int colorsCount = 26; static QColor colors[colorsCount] = {QColor(255,255,255), QColor(0,0,0), QColor(192,0,0), QColor(255,0,0), QColor(255,192,192), //red QColor(0,192,0), QColor(0,255,0), QColor(192,255,192), //green QColor(0,0,192), QColor(0,0,255), QColor(192,192,255), //blue QColor(192,192,0), QColor(255,255,0), QColor(255,255,192), //yellow QColor(0,192,192), QColor(0,255,255), QColor(192,255,255), //cyan QColor(192,0,192), QColor(255,0,255), QColor(255,192,255), //magenta QColor(192,88,0), QColor(255,128,0), QColor(255,168,88), //orange QColor(128,128,128), QColor(160,160,160), QColor(195,195,195) //grey }; CommandEntry::CommandEntry(Worksheet* worksheet) : WorksheetEntry(worksheet), m_promptItem(new WorksheetTextItem(this, Qt::NoTextInteraction)), m_commandItem(new WorksheetTextItem(this, Qt::TextEditorInteraction)), m_resultsCollapsed(false), m_errorItem(nullptr), m_expression(nullptr), m_completionObject(nullptr), m_syntaxHelpObject(nullptr), m_evaluationOption(DoNothing), m_menusInitialized(false), m_backgroundColorActionGroup(nullptr), m_backgroundColorMenu(nullptr), m_textColorActionGroup(nullptr), m_textColorMenu(nullptr), m_fontMenu(nullptr) { m_promptItem->setPlainText(Prompt); m_promptItem->setItemDragable(true); m_commandItem->enableCompletion(true); KColorScheme scheme = KColorScheme(QPalette::Normal, KColorScheme::View); m_commandItem->setBackgroundColor(scheme.background(KColorScheme::AlternateBackground).color()); m_promptItemAnimation = new QPropertyAnimation(m_promptItem, "opacity"); m_promptItemAnimation->setDuration(600); m_promptItemAnimation->setStartValue(1); m_promptItemAnimation->setKeyValueAt(0.5, 0); m_promptItemAnimation->setEndValue(1); connect(m_promptItemAnimation, &QPropertyAnimation::finished, this, &CommandEntry::animatePromptItem); connect(m_commandItem, &WorksheetTextItem::tabPressed, this, &CommandEntry::showCompletion); connect(m_commandItem, &WorksheetTextItem::backtabPressed, this, &CommandEntry::selectPreviousCompletion); connect(m_commandItem, &WorksheetTextItem::applyCompletion, this, &CommandEntry::applySelectedCompletion); connect(m_commandItem, SIGNAL(execute()), this, SLOT(evaluate())); connect(m_commandItem, &WorksheetTextItem::moveToPrevious, this, &CommandEntry::moveToPreviousItem); connect(m_commandItem, &WorksheetTextItem::moveToNext, this, &CommandEntry::moveToNextItem); connect(m_commandItem, SIGNAL(receivedFocus(WorksheetTextItem*)), worksheet, SLOT(highlightItem(WorksheetTextItem*))); connect(m_promptItem, &WorksheetTextItem::drag, this, &CommandEntry::startDrag); connect(worksheet, SIGNAL(updatePrompt()), this, SLOT(updatePrompt())); } CommandEntry::~CommandEntry() { if (m_completionBox) m_completionBox->deleteLater(); } int CommandEntry::type() const { return Type; } void CommandEntry::initMenus() { //background color const QString colorNames[colorsCount] = {i18n("White"), i18n("Black"), i18n("Dark Red"), i18n("Red"), i18n("Light Red"), i18n("Dark Green"), i18n("Green"), i18n("Light Green"), i18n("Dark Blue"), i18n("Blue"), i18n("Light Blue"), i18n("Dark Yellow"), i18n("Yellow"), i18n("Light Yellow"), i18n("Dark Cyan"), i18n("Cyan"), i18n("Light Cyan"), i18n("Dark Magenta"), i18n("Magenta"), i18n("Light Magenta"), i18n("Dark Orange"), i18n("Orange"), i18n("Light Orange"), i18n("Dark Grey"), i18n("Grey"), i18n("Light Grey") }; //background color m_backgroundColorActionGroup = new QActionGroup(this); m_backgroundColorActionGroup->setExclusive(true); connect(m_backgroundColorActionGroup, &QActionGroup::triggered, this, &CommandEntry::backgroundColorChanged); m_backgroundColorMenu = new QMenu(i18n("Background Color")); m_backgroundColorMenu->setIcon(QIcon::fromTheme(QLatin1String("format-fill-color"))); QPixmap pix(16,16); QPainter p(&pix); for (int i=0; isetCheckable(true); m_backgroundColorMenu->addAction(action); } //text color m_textColorActionGroup = new QActionGroup(this); m_textColorActionGroup->setExclusive(true); connect(m_textColorActionGroup, &QActionGroup::triggered, this, &CommandEntry::textColorChanged); m_textColorMenu = new QMenu(i18n("Text Color")); m_textColorMenu->setIcon(QIcon::fromTheme(QLatin1String("format-text-color"))); for (int i=0; isetCheckable(true); m_textColorMenu->addAction(action); } //font m_fontMenu = new QMenu(i18n("Font")); m_fontMenu->setIcon(QIcon::fromTheme(QLatin1String("preferences-desktop-font"))); QAction* action = new QAction(QIcon::fromTheme(QLatin1String("format-text-bold")), i18n("Bold")); action->setCheckable(true); connect(action, &QAction::triggered, this, &CommandEntry::fontBoldTriggered); m_fontMenu->addAction(action); action = new QAction(QIcon::fromTheme(QLatin1String("format-text-italic")), i18n("Italic")); action->setCheckable(true); connect(action, &QAction::triggered, this, &CommandEntry::fontItalicTriggered); m_fontMenu->addAction(action); m_fontMenu->addSeparator(); action = new QAction(QIcon::fromTheme(QLatin1String("format-font-size-less")), i18n("Increase Size")); connect(action, &QAction::triggered, this, &CommandEntry::fontIncreaseTriggered); m_fontMenu->addAction(action); action = new QAction(QIcon::fromTheme(QLatin1String("format-font-size-more")), i18n("Decrease Size")); connect(action, &QAction::triggered, this, &CommandEntry::fontDecreaseTriggered); m_fontMenu->addAction(action); m_fontMenu->addSeparator(); action = new QAction(QIcon::fromTheme(QLatin1String("preferences-desktop-font")), i18n("Select")); connect(action, &QAction::triggered, this, &CommandEntry::fontSelectTriggered); m_fontMenu->addAction(action); m_menusInitialized = true; } void CommandEntry::backgroundColorChanged(QAction* action) { int index = m_backgroundColorActionGroup->actions().indexOf(action); if (index == -1 || index>=colorsCount) index = 0; m_commandItem->setBackgroundColor(colors[index]); } void CommandEntry::textColorChanged(QAction* action) { int index = m_textColorActionGroup->actions().indexOf(action); if (index == -1 || index>=colorsCount) index = 0; m_commandItem->setDefaultTextColor(colors[index]); } void CommandEntry::fontBoldTriggered() { QAction* action = static_cast(QObject::sender()); QFont font = m_commandItem->font(); font.setBold(action->isChecked()); m_commandItem->setFont(font); } void CommandEntry::fontItalicTriggered() { QAction* action = static_cast(QObject::sender()); QFont font = m_commandItem->font(); font.setItalic(action->isChecked()); m_commandItem->setFont(font); } void CommandEntry::fontIncreaseTriggered() { QFont font = m_commandItem->font(); const int currentSize = font.pointSize(); QFontDatabase fdb; QList sizes = fdb.pointSizes(font.family(), font.styleName()); for (int i = 0; i < sizes.size(); ++i) { const int size = sizes.at(i); if (currentSize == size) { if (i + 1 < sizes.size()) { font.setPointSize(sizes.at(i+1)); m_commandItem->setFont(font); } break; } } } void CommandEntry::fontDecreaseTriggered() { QFont font = m_commandItem->font(); const int currentSize = font.pointSize(); QFontDatabase fdb; QList sizes = fdb.pointSizes(font.family(), font.styleName()); for (int i = 0; i < sizes.size(); ++i) { const int size = sizes.at(i); if (currentSize == size) { if (i - 1 >= 0) { font.setPointSize(sizes.at(i-1)); m_commandItem->setFont(font); } break; } } } void CommandEntry::fontSelectTriggered() { bool ok; QFont font = QFontDialog::getFont(&ok, m_commandItem->font(), nullptr); if (ok) m_commandItem->setFont(font); } void CommandEntry::populateMenu(QMenu* menu, QPointF pos) { if (!m_menusInitialized) initMenus(); if (!m_resultItems.isEmpty()) { if (m_resultsCollapsed) menu->addAction(i18n("Show Results"), this, &CommandEntry::expandResults); else menu->addAction(i18n("Hide Results"), this, &CommandEntry::collapseResults); } menu->addMenu(m_backgroundColorMenu); menu->addMenu(m_textColorMenu); menu->addMenu(m_fontMenu); menu->addSeparator(); WorksheetEntry::populateMenu(menu, pos); } void CommandEntry::moveToNextItem(int pos, qreal x) { WorksheetTextItem* item = qobject_cast(sender()); if (!item) return; if (item == m_commandItem) { if (m_informationItems.isEmpty() || !currentInformationItem()->isEditable()) moveToNextEntry(pos, x); else currentInformationItem()->setFocusAt(pos, x); } else if (item == currentInformationItem()) { moveToNextEntry(pos, x); } } void CommandEntry::moveToPreviousItem(int pos, qreal x) { WorksheetTextItem* item = qobject_cast(sender()); if (!item) return; if (item == m_commandItem || item == nullptr) { moveToPreviousEntry(pos, x); } else if (item == currentInformationItem()) { m_commandItem->setFocusAt(pos, x); } } QString CommandEntry::command() { QString cmd = m_commandItem->toPlainText(); cmd.replace(QChar::ParagraphSeparator, QLatin1Char('\n')); //Replace the U+2029 paragraph break by a Normal Newline cmd.replace(QChar::LineSeparator, QLatin1Char('\n')); //Replace the line break by a Normal Newline return cmd; } void CommandEntry::setExpression(Cantor::Expression* expr) { /* if ( m_expression ) { if (m_expression->status() == Cantor::Expression::Computing) { qDebug() << "OLD EXPRESSION STILL ACTIVE"; m_expression->interrupt(); } m_expression->deleteLater(); }*/ // Delete any previous error if(m_errorItem) { m_errorItem->deleteLater(); m_errorItem = nullptr; } foreach(WorksheetTextItem* item, m_informationItems) { item->deleteLater(); } m_informationItems.clear(); // Delete any previous result clearResultItems(); m_expression = expr; m_resultsCollapsed = false; connect(expr, SIGNAL(gotResult()), this, SLOT(updateEntry())); connect(expr, SIGNAL(resultsCleared()), this, SLOT(clearResultItems())); connect(expr, SIGNAL(resultRemoved(int)), this, SLOT(removeResultItem(int))); connect(expr, SIGNAL(resultReplaced(int)), this, SLOT(replaceResultItem(int))); connect(expr, SIGNAL(idChanged()), this, SLOT(updatePrompt())); connect(expr, SIGNAL(statusChanged(Cantor::Expression::Status)), this, SLOT(expressionChangedStatus(Cantor::Expression::Status))); connect(expr, SIGNAL(needsAdditionalInformation(QString)), this, SLOT(showAdditionalInformationPrompt(QString))); connect(expr, SIGNAL(statusChanged(Cantor::Expression::Status)), this, SLOT(updatePrompt())); updatePrompt(); if(expr->result()) { worksheet()->gotResult(expr); updateEntry(); } expressionChangedStatus(expr->status()); } Cantor::Expression* CommandEntry::expression() { return m_expression; } bool CommandEntry::acceptRichText() { return false; } void CommandEntry::setContent(const QString& content) { m_commandItem->setPlainText(content); } void CommandEntry::setContent(const QDomElement& content, const KZip& file) { m_commandItem->setPlainText(content.firstChildElement(QLatin1String("Command")).text()); LoadedExpression* expr=new LoadedExpression( worksheet()->session() ); expr->loadFromXml(content, file); //background color QDomElement backgroundElem = content.firstChildElement(QLatin1String("Background")); if (!backgroundElem.isNull()) { QColor color; color.setRed(backgroundElem.attribute(QLatin1String("red")).toInt()); color.setGreen(backgroundElem.attribute(QLatin1String("green")).toInt()); color.setBlue(backgroundElem.attribute(QLatin1String("blue")).toInt()); m_commandItem->setBackgroundColor(color); } //text properties QDomElement textElem = content.firstChildElement(QLatin1String("Text")); if (!textElem.isNull()) { //text color QDomElement colorElem = textElem.firstChildElement(QLatin1String("Color")); QColor color; color.setRed(colorElem.attribute(QLatin1String("red")).toInt()); color.setGreen(colorElem.attribute(QLatin1String("green")).toInt()); color.setBlue(colorElem.attribute(QLatin1String("blue")).toInt()); m_commandItem->setDefaultTextColor(color); //font properties QDomElement fontElem = textElem.firstChildElement(QLatin1String("Font")); QFont font; font.setFamily(fontElem.attribute(QLatin1String("family"))); font.setPointSize(fontElem.attribute(QLatin1String("pointSize")).toInt()); font.setWeight(fontElem.attribute(QLatin1String("weight")).toInt()); font.setItalic(fontElem.attribute(QLatin1String("italic")).toInt()); m_commandItem->setFont(font); } setExpression(expr); } void CommandEntry::setContentFromJupyter(const QJsonObject& cell) { m_commandItem->setPlainText(JupyterUtils::getSource(cell)); LoadedExpression* expr=new LoadedExpression( worksheet()->session() ); expr->loadFromJupyter(cell); setExpression(expr); // https://nbformat.readthedocs.io/en/latest/format_description.html#cell-metadata // 'collapsed': + // 'scrolled', 'deletable', 'name', 'tags' don't supported Cantor, so ignore them // 'source_hidden' don't supported // 'format' for raw entry, so ignore // I haven't found 'outputs_hidden' inside Jupyter notebooks, and difference from 'collapsed' // not clear, so also ignore const QJsonObject& metadata = JupyterUtils::getMetadata(cell); const QJsonValue& collapsed = metadata.value(QLatin1String("collapsed")); if (collapsed.isBool() && collapsed.toBool() == true) { // Disable animation for hiding results, we don't need animation on document load stage bool animationValue = worksheet()->animationsEnabled(); worksheet()->enableAnimations(false); collapseResults(); worksheet()->enableAnimations(animationValue); } } QJsonValue CommandEntry::toJupyterJson() { QJsonObject entry; entry.insert(QLatin1String("cell_type"), QLatin1String("code")); QJsonValue executionCountValue; if (expression() && expression()->id() != -1) executionCountValue = QJsonValue(expression()->id()); entry.insert(QLatin1String("execution_count"), executionCountValue); QJsonObject metadata; if (m_resultsCollapsed) metadata.insert(QLatin1String("collapsed"), true); entry.insert(QLatin1String("metadata"), metadata); JupyterUtils::setSource(entry, command()); QJsonArray outputs; if (expression()) { Cantor::Expression::Status status = expression()->status(); if (status == Cantor::Expression::Error || status == Cantor::Expression::Interrupted) { QJsonObject errorOutput; errorOutput.insert(JupyterUtils::outputTypeKey, QLatin1String("error")); errorOutput.insert(QLatin1String("ename"), QLatin1String("Unknown")); errorOutput.insert(QLatin1String("evalue"), QLatin1String("Unknown")); QJsonArray traceback; if (status == Cantor::Expression::Error) { const QStringList& error = expression()->errorMessage().split(QLatin1Char('\n')); for (const QString& line: error) traceback.append(line); } else { traceback.append(i18n("Interrupted")); } errorOutput.insert(QLatin1String("traceback"), traceback); outputs.append(errorOutput); } for (Cantor::Result * const result: expression()->results()) { const QJsonValue& resultJson = result->toJupyterJson(); // Jupyter TODO: Convert EpsResult here? if (result->type() == Cantor::EpsResult::Type) { QJsonObject root; root.insert(QLatin1String("output_type"), QLatin1String("display_data")); QJsonObject data; data.insert(QLatin1String("text/plain"), QString()); const QImage& image = worksheet()->epsRenderer()->renderToImage(result->data().toUrl()); QByteArray ba; QBuffer buffer(&ba); buffer.open(QIODevice::WriteOnly); image.save(&buffer, "PNG"); data.insert(JupyterUtils::pngMime, QString::fromLatin1(ba.toBase64())); root.insert(QLatin1String("data"), data); QJsonObject metadata; QJsonObject size; size.insert(QLatin1String("width"), image.size().width()); size.insert(QLatin1String("height"), image.size().height()); metadata.insert(QLatin1String("image/png"), size); root.insert(QLatin1String("metadata"), metadata); outputs.append(root); } else if (!resultJson.isNull()) outputs.append(resultJson); } } entry.insert(QLatin1String("outputs"), outputs); return entry; } void CommandEntry::showCompletion() { const QString line = currentLine(); if(!worksheet()->completionEnabled() || line.trimmed().isEmpty()) { if (m_commandItem->hasFocus()) m_commandItem->insertTab(); return; } else if (isShowingCompletionPopup()) { QString comp = m_completionObject->completion(); qDebug() << "command" << m_completionObject->command(); qDebug() << "completion" << comp; if (comp != m_completionObject->command() || !m_completionObject->hasMultipleMatches()) { if (m_completionObject->hasMultipleMatches()) { completeCommandTo(comp, PreliminaryCompletion); } else { completeCommandTo(comp, FinalCompletion); m_completionBox->hide(); } } else { m_completionBox->down(); } } else { int p = m_commandItem->textCursor().positionInBlock(); Cantor::CompletionObject* tco=worksheet()->session()->completionFor(line, p); if(tco) setCompletion(tco); } } void CommandEntry::selectPreviousCompletion() { if (isShowingCompletionPopup()) m_completionBox->up(); } QString CommandEntry::toPlain(const QString& commandSep, const QString& commentStartingSeq, const QString& commentEndingSeq) { Q_UNUSED(commentStartingSeq); Q_UNUSED(commentEndingSeq); if (command().isEmpty()) return QString(); return command() + commandSep; } QDomElement CommandEntry::toXml(QDomDocument& doc, KZip* archive) { QDomElement exprElem = doc.createElement( QLatin1String("Expression") ); QDomElement cmdElem = doc.createElement( QLatin1String("Command") ); cmdElem.appendChild(doc.createTextNode( command() )); exprElem.appendChild(cmdElem); // save results of the expression if they exist if (expression()) { const QString& errorMessage = expression()->errorMessage(); if (!errorMessage.isEmpty()) { QDomElement errorElem = doc.createElement( QLatin1String("Error") ); errorElem.appendChild(doc.createTextNode(errorMessage)); exprElem.appendChild(errorElem); } for (Cantor::Result * const result: expression()->results()) { const QDomElement& resultElem = result->toXml(doc); exprElem.appendChild(resultElem); if (archive) result->saveAdditionalData(archive); } } //save the background color if it differs from the default one const QColor& backgroundColor = m_commandItem->backgroundColor(); KColorScheme scheme = KColorScheme(QPalette::Normal, KColorScheme::View); if (backgroundColor != scheme.background(KColorScheme::AlternateBackground).color()) { QDomElement colorElem = doc.createElement( QLatin1String("Background") ); colorElem.setAttribute(QLatin1String("red"), QString::number(backgroundColor.red())); colorElem.setAttribute(QLatin1String("green"), QString::number(backgroundColor.green())); colorElem.setAttribute(QLatin1String("blue"), QString::number(backgroundColor.blue())); exprElem.appendChild(colorElem); } //save the text properties if they differ from default values const QFont& font = m_commandItem->font(); if (font != QFontDatabase::systemFont(QFontDatabase::FixedFont)) { QDomElement textElem = doc.createElement(QLatin1String("Text")); //font properties QDomElement fontElem = doc.createElement(QLatin1String("Font")); fontElem.setAttribute(QLatin1String("family"), font.family()); fontElem.setAttribute(QLatin1String("pointSize"), QString::number(font.pointSize())); fontElem.setAttribute(QLatin1String("weight"), QString::number(font.weight())); fontElem.setAttribute(QLatin1String("italic"), QString::number(font.italic())); textElem.appendChild(fontElem); //text color const QColor& textColor = m_commandItem->defaultTextColor(); QDomElement colorElem = doc.createElement( QLatin1String("Color") ); colorElem.setAttribute(QLatin1String("red"), QString::number(textColor.red())); colorElem.setAttribute(QLatin1String("green"), QString::number(textColor.green())); colorElem.setAttribute(QLatin1String("blue"), QString::number(textColor.blue())); textElem.appendChild(colorElem); exprElem.appendChild(textElem); } return exprElem; } QString CommandEntry::currentLine() { if (!m_commandItem->hasFocus()) return QString(); QTextBlock block = m_commandItem->textCursor().block(); return block.text(); } bool CommandEntry::evaluateCurrentItem() { // we can't use m_commandItem->hasFocus() here, because // that doesn't work when the scene doesn't have the focus, // e.g. when an assistant is used. if (m_commandItem == worksheet()->focusItem()) { return evaluate(); } else if (informationItemHasFocus()) { addInformation(); return true; } return false; } bool CommandEntry::evaluate(EvaluationOption evalOp) { + if (worksheet()->session()->status() == Cantor::Session::Disable) + worksheet()->loginToSession(); + removeContextHelp(); QToolTip::hideText(); QString cmd = command(); m_evaluationOption = evalOp; if(cmd.isEmpty()) { removeResults(); foreach(WorksheetTextItem* item, m_informationItems) { item->deleteLater(); } m_informationItems.clear(); recalculateSize(); evaluateNext(m_evaluationOption); return false; } Cantor::Expression* expr; expr = worksheet()->session()->evaluateExpression(cmd); connect(expr, SIGNAL(gotResult()), worksheet(), SLOT(gotResult())); setExpression(expr); return true; } void CommandEntry::interruptEvaluation() { Cantor::Expression *expr = expression(); if(expr) expr->interrupt(); } void CommandEntry::updateEntry() { qDebug() << "update Entry"; Cantor::Expression* expr = expression(); if (expr == nullptr || expr->results().isEmpty()) return; if (expr->results().last()->type() == Cantor::HelpResult::Type) return; // Help is handled elsewhere //CommandEntry::updateEntry() is only called if the worksheet view is resized //or when we got a new result(s). //In the second case the number of results and the number of result graphic objects is different //and we add a new graphic objects for the new result(s) (new result(s) are located in the end). // NOTE: LatexResult could request update (change from rendered to code, for example) // So, just update results, if we haven't new results or something similar if (m_resultItems.size() < expr->results().size()) { if (m_resultsCollapsed) expandResults(); for (int i = m_resultItems.size(); i < expr->results().size(); i++) m_resultItems << ResultItem::create(this, expr->results()[i]); } else { for (ResultItem* item: m_resultItems) item->update(); } animateSizeChange(); } void CommandEntry::expressionChangedStatus(Cantor::Expression::Status status) { switch (status) { case Cantor::Expression::Computing: { //change the background of the promt item and start animating it (fade in/out). //don't start the animation immediately in order to avoid unwanted flickering for "short" commands, //start the animation after 1 second passed. if (worksheet()->animationsEnabled()) { const int id = m_expression->id(); QTimer::singleShot(1000, this, [this, id] () { if(m_expression->status() == Cantor::Expression::Computing && m_expression->id() == id) m_promptItemAnimation->start(); }); } break; } case Cantor::Expression::Error: case Cantor::Expression::Interrupted: m_promptItemAnimation->stop(); m_promptItem->setOpacity(1.); m_commandItem->setFocusAt(WorksheetTextItem::BottomRight, 0); if(!m_errorItem) { m_errorItem = new WorksheetTextItem(this, Qt::TextSelectableByMouse); } if (status == Cantor::Expression::Error) { QString error = m_expression->errorMessage().toHtmlEscaped(); while (error.endsWith(QLatin1Char('\n'))) error.chop(1); error.replace(QLatin1String("\n"), QLatin1String("
")); error.replace(QLatin1String(" "), QLatin1String(" ")); m_errorItem->setHtml(error); } else m_errorItem->setHtml(i18n("Interrupted")); recalculateSize(); break; case Cantor::Expression::Done: m_promptItemAnimation->stop(); m_promptItem->setOpacity(1.); evaluateNext(m_evaluationOption); m_evaluationOption = DoNothing; break; default: break; } } void CommandEntry::animatePromptItem() { if(m_expression->status() == Cantor::Expression::Computing) m_promptItemAnimation->start(); } bool CommandEntry::isEmpty() { if (m_commandItem->toPlainText().trimmed().isEmpty()) { if (!m_resultItems.isEmpty()) return false; return true; } return false; } bool CommandEntry::focusEntry(int pos, qreal xCoord) { if (aboutToBeRemoved()) return false; WorksheetTextItem* item; if (pos == WorksheetTextItem::TopLeft || pos == WorksheetTextItem::TopCoord) item = m_commandItem; else if (m_informationItems.size() && currentInformationItem()->isEditable()) item = currentInformationItem(); else item = m_commandItem; item->setFocusAt(pos, xCoord); return true; } void CommandEntry::setCompletion(Cantor::CompletionObject* tc) { if (m_completionObject) delete m_completionObject; m_completionObject = tc; connect(m_completionObject, &Cantor::CompletionObject::done, this, &CommandEntry::showCompletions); connect(m_completionObject, &Cantor::CompletionObject::lineDone, this, &CommandEntry::completeLineTo); } void CommandEntry::showCompletions() { disconnect(m_completionObject, &Cantor::CompletionObject::done, this, &CommandEntry::showCompletions); QString completion = m_completionObject->completion(); qDebug()<<"completion: "<allMatches(); if(m_completionObject->hasMultipleMatches()) { completeCommandTo(completion); QToolTip::showText(QPoint(), QString(), worksheetView()); if (!m_completionBox) m_completionBox = new KCompletionBox(worksheetView()); m_completionBox->clear(); m_completionBox->setItems(m_completionObject->allMatches()); QList items = m_completionBox->findItems(m_completionObject->command(), Qt::MatchFixedString|Qt::MatchCaseSensitive); if (!items.empty()) m_completionBox->setCurrentItem(items.first()); m_completionBox->setTabHandling(false); m_completionBox->setActivateOnSelect(true); connect(m_completionBox.data(), &KCompletionBox::activated, this, &CommandEntry::applySelectedCompletion); connect(m_commandItem->document(), SIGNAL(contentsChanged()), this, SLOT(completedLineChanged())); connect(m_completionObject, &Cantor::CompletionObject::done, this, &CommandEntry::updateCompletions); m_commandItem->activateCompletion(true); m_completionBox->popup(); m_completionBox->move(getPopupPosition()); } else { completeCommandTo(completion, FinalCompletion); } } bool CommandEntry::isShowingCompletionPopup() { return m_completionBox && m_completionBox->isVisible(); } void CommandEntry::applySelectedCompletion() { QListWidgetItem* item = m_completionBox->currentItem(); if(item) completeCommandTo(item->text(), FinalCompletion); m_completionBox->hide(); } void CommandEntry::completedLineChanged() { if (!isShowingCompletionPopup()) { // the completion popup is not visible anymore, so let's clean up removeContextHelp(); return; } const QString line = currentLine(); //FIXME: For some reason, this slot constantly triggeres, so I have added checking, is this update really needed if (line != m_completionObject->command()) m_completionObject->updateLine(line, m_commandItem->textCursor().positionInBlock()); } void CommandEntry::updateCompletions() { if (!m_completionObject) return; QString completion = m_completionObject->completion(); qDebug()<<"completion: "<allMatches(); if(m_completionObject->hasMultipleMatches() || !completion.isEmpty()) { QToolTip::showText(QPoint(), QString(), worksheetView()); m_completionBox->setItems(m_completionObject->allMatches()); QList items = m_completionBox->findItems(m_completionObject->command(), Qt::MatchFixedString|Qt::MatchCaseSensitive); if (!items.empty()) m_completionBox->setCurrentItem(items.first()); else if (m_completionBox->items().count() == 1) m_completionBox->setCurrentRow(0); else m_completionBox->clearSelection(); m_completionBox->move(getPopupPosition()); } else { removeContextHelp(); } } void CommandEntry::completeCommandTo(const QString& completion, CompletionMode mode) { qDebug() << "completion: " << completion; Cantor::CompletionObject::LineCompletionMode cmode; if (mode == FinalCompletion) { cmode = Cantor::CompletionObject::FinalCompletion; Cantor::SyntaxHelpObject* obj = worksheet()->session()->syntaxHelpFor(completion); if(obj) setSyntaxHelp(obj); } else { cmode = Cantor::CompletionObject::PreliminaryCompletion; if(m_syntaxHelpObject) m_syntaxHelpObject->deleteLater(); m_syntaxHelpObject=nullptr; } m_completionObject->completeLine(completion, cmode); } void CommandEntry::completeLineTo(const QString& line, int index) { qDebug() << "line completion: " << line; QTextCursor cursor = m_commandItem->textCursor(); cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::MoveAnchor); cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor); int startPosition = cursor.position(); cursor.insertText(line); cursor.setPosition(startPosition + index); m_commandItem->setTextCursor(cursor); if (m_syntaxHelpObject) { m_syntaxHelpObject->fetchSyntaxHelp(); // If we are about to show syntax help, then this was the final // completion, and we should clean up. removeContextHelp(); } } void CommandEntry::setSyntaxHelp(Cantor::SyntaxHelpObject* sh) { if(m_syntaxHelpObject) m_syntaxHelpObject->deleteLater(); m_syntaxHelpObject=sh; connect(sh, SIGNAL(done()), this, SLOT(showSyntaxHelp())); } void CommandEntry::showSyntaxHelp() { QString msg = m_syntaxHelpObject->toHtml(); const QPointF cursorPos = m_commandItem->cursorPosition(); // QToolTip don't support  , but support multiple spaces msg.replace(QLatin1String(" "), QLatin1String(" ")); // Don't support " too; msg.replace(QLatin1String("""), QLatin1String("\"")); QToolTip::showText(toGlobalPosition(cursorPos), msg, worksheetView()); } void CommandEntry::resultDeleted() { qDebug()<<"result got removed..."; } void CommandEntry::addInformation() { WorksheetTextItem *answerItem = currentInformationItem(); answerItem->setTextInteractionFlags(Qt::TextSelectableByMouse); QString inf = answerItem->toPlainText(); inf.replace(QChar::ParagraphSeparator, QLatin1Char('\n')); inf.replace(QChar::LineSeparator, QLatin1Char('\n')); qDebug()<<"adding information: "<addInformation(inf); } void CommandEntry::showAdditionalInformationPrompt(const QString& question) { WorksheetTextItem* questionItem = new WorksheetTextItem(this, Qt::TextSelectableByMouse); WorksheetTextItem* answerItem = new WorksheetTextItem(this, Qt::TextEditorInteraction); //change the color and the font for when asking for additional information in order to //better discriminate from the usual input in the command entry KColorScheme scheme = KColorScheme(QPalette::Normal, KColorScheme::View); QColor color = scheme.foreground(KColorScheme::PositiveText).color(); QFont font; font.setItalic(true); questionItem->setFont(font); questionItem->setDefaultTextColor(color); answerItem->setFont(font); answerItem->setDefaultTextColor(color); questionItem->setPlainText(question); m_informationItems.append(questionItem); m_informationItems.append(answerItem); connect(answerItem, &WorksheetTextItem::moveToPrevious, this, &CommandEntry::moveToPreviousItem); connect(answerItem, &WorksheetTextItem::moveToNext, this, &CommandEntry::moveToNextItem); connect(answerItem, &WorksheetTextItem::execute, this, &CommandEntry::addInformation); answerItem->setFocus(); recalculateSize(); } void CommandEntry::removeResults() { //clear the Result objects if(m_expression) m_expression->clearResults(); } void CommandEntry::removeResult(Cantor::Result* result) { if (m_expression) m_expression->removeResult(result); } void CommandEntry::removeResultItem(int index) { fadeOutItem(m_resultItems[index]->graphicsObject()); m_resultItems.remove(index); recalculateSize(); } void CommandEntry::clearResultItems() { //fade out all result graphic objects for(auto* item : m_resultItems) fadeOutItem(item->graphicsObject()); m_resultItems.clear(); recalculateSize(); } void CommandEntry::replaceResultItem(int index) { ResultItem* previousItem = m_resultItems[index]; m_resultItems[index] = ResultItem::create(this, m_expression->results()[index]); previousItem->deleteLater(); recalculateSize(); } void CommandEntry::removeContextHelp() { disconnect(m_commandItem->document(), SIGNAL(contentsChanged()), this, SLOT(completedLineChanged())); m_commandItem->activateCompletion(false); if (m_completionBox) m_completionBox->hide(); } void CommandEntry::updatePrompt(const QString& postfix) { KColorScheme color = KColorScheme( QPalette::Normal, KColorScheme::View); m_promptItem->setPlainText(QLatin1String("")); QTextCursor c = m_promptItem->textCursor(); QTextCharFormat cformat = c.charFormat(); cformat.clearForeground(); c.setCharFormat(cformat); cformat.setFontWeight(QFont::Bold); //insert the session id if available if(m_expression && worksheet()->showExpressionIds()&&m_expression->id()!=-1) c.insertText(QString::number(m_expression->id()),cformat); //detect the correct color for the prompt, depending on the //Expression state if(m_expression) { if(m_expression ->status() == Cantor::Expression::Computing&&worksheet()->isRunning()) cformat.setForeground(color.foreground(KColorScheme::PositiveText)); else if(m_expression ->status() == Cantor::Expression::Queued) cformat.setForeground(color.foreground(KColorScheme::InactiveText)); else if(m_expression ->status() == Cantor::Expression::Error) cformat.setForeground(color.foreground(KColorScheme::NegativeText)); else if(m_expression ->status() == Cantor::Expression::Interrupted) cformat.setForeground(color.foreground(KColorScheme::NeutralText)); else cformat.setFontWeight(QFont::Normal); } c.insertText(postfix, cformat); recalculateSize(); } WorksheetTextItem* CommandEntry::currentInformationItem() { if (m_informationItems.isEmpty()) return nullptr; return m_informationItems.last(); } bool CommandEntry::informationItemHasFocus() { if (m_informationItems.isEmpty()) return false; return m_informationItems.last()->hasFocus(); } bool CommandEntry::focusWithinThisItem() { return focusItem() != nullptr; } QPoint CommandEntry::getPopupPosition() { const QPointF cursorPos = m_commandItem->cursorPosition(); const QPoint globalPos = toGlobalPosition(cursorPos); const QDesktopWidget* desktop = QApplication::desktop(); const QRect screenRect = desktop->screenGeometry(globalPos); if (globalPos.y() + m_completionBox->height() < screenRect.bottom()) { return (globalPos); } else { QTextBlock block = m_commandItem->textCursor().block(); QTextLayout* layout = block.layout(); int pos = m_commandItem->textCursor().position() - block.position(); QTextLine line = layout->lineForTextPosition(pos); int dy = - m_completionBox->height() - line.height() - line.leading(); return QPoint(globalPos.x(), globalPos.y() + dy); } } void CommandEntry::invalidate() { qDebug() << "ToDo: Invalidate here"; } bool CommandEntry::wantToEvaluate() { return !isEmpty(); } QPoint CommandEntry::toGlobalPosition(QPointF localPos) { const QPointF scenePos = mapToScene(localPos); const QPoint viewportPos = worksheetView()->mapFromScene(scenePos); return worksheetView()->viewport()->mapToGlobal(viewportPos); } WorksheetCursor CommandEntry::search(const QString& pattern, unsigned flags, QTextDocument::FindFlags qt_flags, const WorksheetCursor& pos) { if (pos.isValid() && pos.entry() != this) return WorksheetCursor(); WorksheetCursor p = pos; QTextCursor cursor; if (flags & WorksheetEntry::SearchCommand) { cursor = m_commandItem->search(pattern, qt_flags, p); if (!cursor.isNull()) return WorksheetCursor(this, m_commandItem, cursor); } if (p.textItem() == m_commandItem) p = WorksheetCursor(); if (m_errorItem && flags & WorksheetEntry::SearchError) { cursor = m_errorItem->search(pattern, qt_flags, p); if (!cursor.isNull()) return WorksheetCursor(this, m_errorItem, cursor); } if (p.textItem() == m_errorItem) p = WorksheetCursor(); for (auto* resultItem : m_resultItems) { WorksheetTextItem* textResult = dynamic_cast (resultItem); if (textResult && flags & WorksheetEntry::SearchResult) { cursor = textResult->search(pattern, qt_flags, p); if (!cursor.isNull()) return WorksheetCursor(this, textResult, cursor); } } return WorksheetCursor(); } void CommandEntry::layOutForWidth(qreal w, bool force) { if (w == size().width() && !force) return; m_promptItem->setPos(0,0); double x = 0 + m_promptItem->width() + HorizontalSpacing; double y = 0; double width = 0; m_commandItem->setGeometry(x,y, w-x); width = qMax(width, m_commandItem->width()); y += qMax(m_commandItem->height(), m_promptItem->height()); foreach(WorksheetTextItem* information, m_informationItems) { y += VerticalSpacing; y += information->setGeometry(x,y,w-x); width = qMax(width, information->width()); } if (m_errorItem) { y += VerticalSpacing; y += m_errorItem->setGeometry(x,y,w-x); width = qMax(width, m_errorItem->width()); } for (auto* resultItem : m_resultItems) { if (!resultItem || !resultItem->graphicsObject()->isVisible()) continue; y += VerticalSpacing; y += resultItem->setGeometry(x, y, w-x); width = qMax(width, resultItem->width()); } y += VerticalMargin; QSizeF s(x+ width, y); if (animationActive()) { updateSizeAnimation(s); } else { setSize(s); } } void CommandEntry::startRemoving() { m_promptItem->setItemDragable(false); WorksheetEntry::startRemoving(); } WorksheetTextItem* CommandEntry::highlightItem() { return m_commandItem; } void CommandEntry::collapseResults() { for(auto* item : m_resultItems) { fadeOutItem(item->graphicsObject(), nullptr); item->graphicsObject()->hide(); } m_resultsCollapsed = true; if (worksheet()->animationsEnabled()) { QTimer::singleShot(100, this, &CommandEntry::setMidPrompt); QTimer::singleShot(200, this, &CommandEntry::setHidePrompt); } else setHidePrompt(); animateSizeChange(); } void CommandEntry::expandResults() { for(auto* item : m_resultItems) { fadeInItem(item->graphicsObject(), nullptr); item->graphicsObject()->show(); } m_resultsCollapsed = false; if (worksheet()->animationsEnabled()) { QTimer::singleShot(100, this, &CommandEntry::setMidPrompt); QTimer::singleShot(200, this, SLOT(updatePrompt())); } else this->updatePrompt(); animateSizeChange(); } void CommandEntry::setHidePrompt() { updatePrompt(HidePrompt); } void CommandEntry::setMidPrompt() { updatePrompt(MidPrompt); }