diff --git a/src/backends/octave/CMakeLists.txt b/src/backends/octave/CMakeLists.txt index 5ac78ff2..0ea66166 100644 --- a/src/backends/octave/CMakeLists.txt +++ b/src/backends/octave/CMakeLists.txt @@ -1,52 +1,53 @@ set( OctaveBackend_SRCS octavebackend.cpp octavesession.cpp octaveexpression.cpp octaveextensions.cpp octavehighlighter.cpp octavekeywords.cpp octavecompletionobject.cpp octavesyntaxhelpobject.cpp octavevariablemodel.cpp ) add_subdirectory(scripts) if (WITH_EPS) set(DEFAULT_PLOT_FORMAT "eps") set(EPS_PLOT_FORMAT_CHOICE "") file(READ with_eps_ui_part.txt EPS_PLOT_FORMAT_UI_ELEMENT) else (WITH_EPS) set(DEFAULT_PLOT_FORMAT "png") endif (WITH_EPS) message(STATUS ${CMAKE_CURRENT_SOURCE_DIR}) configure_file(octavebackend.kcfg.in ${CMAKE_CURRENT_BINARY_DIR}/octavebackend.kcfg) configure_file(settings.ui.in ${CMAKE_CURRENT_BINARY_DIR}/settings.ui) kconfig_add_kcfg_files(OctaveBackend_SRCS settings.kcfgc) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/octavebackend.kcfg DESTINATION ${KDE_INSTALL_KCFGDIR}) ki18n_wrap_ui(OctaveBackend_SRCS ${CMAKE_CURRENT_BINARY_DIR}/settings.ui) add_backend(octavebackend ${OctaveBackend_SRCS}) target_link_libraries(cantor_octavebackend KF5::KIOCore KF5::ConfigCore KF5::ConfigGui KF5::SyntaxHighlighting ) if(BUILD_TESTING) - add_executable( testoctave testoctave.cpp octaveexpression.cpp settings.cpp) + add_executable( testoctave testoctave.cpp ${OctaveBackend_SRCS}) add_test(NAME testoctave COMMAND testoctave) ecm_mark_as_test(testoctave) target_link_libraries( testoctave Qt5::Test + KF5::SyntaxHighlighting cantorlibs cantortest ) endif(BUILD_TESTING) install(FILES cantor_octave.knsrc DESTINATION ${KDE_INSTALL_CONFDIR} ) diff --git a/src/backends/octave/octaveexpression.cpp b/src/backends/octave/octaveexpression.cpp index cd70c5cb..d7cac673 100644 --- a/src/backends/octave/octaveexpression.cpp +++ b/src/backends/octave/octaveexpression.cpp @@ -1,243 +1,243 @@ /* Copyright (C) 2010 Miha Čančula 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. */ #include "octaveexpression.h" #include "octavesession.h" #include "defaultvariablemodel.h" #include "textresult.h" #include "epsresult.h" #include "imageresult.h" #include #include #include #include #include #include #include #include "settings.h" static const QString printCommandTemplate = QString::fromLatin1("cantor_print('%1', '%2');"); static const QStringList plotCommands({ QLatin1String("plot"), QLatin1String("semilogx"), QLatin1String("semilogy"), QLatin1String("loglog"), QLatin1String("polar"), QLatin1String("contour"), QLatin1String("bar"), QLatin1String("stairs"), QLatin1String("errorbar"), QLatin1String("sombrero"), QLatin1String("hist"), QLatin1String("fplot"), QLatin1String("imshow"), QLatin1String("stem"), QLatin1String("stem3"), QLatin1String("scatter"), QLatin1String("pareto"), QLatin1String("rose"), QLatin1String("pie"), QLatin1String("quiver"), QLatin1String("compass"), QLatin1String("feather"), QLatin1String("pcolor"), QLatin1String("area"), QLatin1String("fill"), QLatin1String("comet"), QLatin1String("plotmatrix"), /* 3d-plots */ QLatin1String("plot3"), QLatin1String("mesh"), QLatin1String("meshc"), QLatin1String("meshz"), QLatin1String("surf"), QLatin1String("surfc"), QLatin1String("surfl"), QLatin1String("surfnorm"), QLatin1String("isosurface"), QLatin1String("isonormals"), QLatin1String("isocaps"), /* 3d-plots defined by a function */ QLatin1String("ezplot3"), QLatin1String("ezmesh"), QLatin1String("ezmeshc"), QLatin1String("ezsurf"), QLatin1String("ezsurfc"), QLatin1String("cantor_plot2d"), QLatin1String("cantor_plot3d")}); const QStringList OctaveExpression::plotExtensions({ #ifdef WITH_EPS QLatin1String("eps"), #endif QLatin1String("png"), QLatin1String("svg"), QLatin1String("jpeg") }); OctaveExpression::OctaveExpression(Cantor::Session* session, bool internal): Expression(session, internal) { } OctaveExpression::~OctaveExpression() { if(m_tempFile) { delete m_tempFile; m_tempFile = nullptr; } } void OctaveExpression::interrupt() { qDebug() << "interrupt"; setStatus(Interrupted); } void OctaveExpression::evaluate() { if(m_tempFile) { delete m_tempFile; m_tempFile = nullptr; } qDebug() << "evaluate"; QString cmd = command(); QStringList cmdWords = cmd.split(QRegularExpression(QStringLiteral("\\b")), QString::SkipEmptyParts); - if (OctaveSettings::integratePlots() && !cmdWords.contains(QLatin1String("help")) && !cmdWords.contains(QLatin1String("completion_matches"))) + if (static_cast(session())->isIntegratedPlotsEnabled() && !cmdWords.contains(QLatin1String("help")) && !cmdWords.contains(QLatin1String("completion_matches"))) { for (const QString& plotCmd : plotCommands) { if (cmdWords.contains(plotCmd)) { qDebug() << "Executing a plot command"; /* #ifdef WITH_EPS QLatin1String ext(".eps"); #else QLatin1String ext(".png"); #endif */ m_tempFile = new QTemporaryFile(QDir::tempPath() + QLatin1String("/cantor_octave-XXXXXX.")+plotExtensions[OctaveSettings::inlinePlotFormat()]); m_tempFile->open(); qDebug() << "plot temp file" << m_tempFile->fileName(); QFileSystemWatcher* watcher = fileWatcher(); if (!watcher->files().isEmpty()) watcher->removePaths(watcher->files()); watcher->addPath(m_tempFile->fileName()); connect(watcher, &QFileSystemWatcher::fileChanged, this, &OctaveExpression::imageChanged, Qt::UniqueConnection); m_plotPending = true; break; } } } m_finished = false; session()->enqueueExpression(this); } QString OctaveExpression::internalCommand() { QString cmd = command(); if (m_plotPending) { if (!cmd.endsWith(QLatin1Char(';')) && !cmd.endsWith(QLatin1Char(','))) cmd += QLatin1Char(','); cmd += printCommandTemplate.arg(QFileInfo(m_tempFile->fileName()).suffix()).arg(m_tempFile->fileName()); } // We need remove all comments here, because below we merge all strings to one long string // Otherwise, all code after line with comment will be commented out after merging // So, this small state machine remove all comments // FIXME better implementation QString tmp; // 0 - command mode, 1 - string mode for ', 2 - string mode for ", 3 - comment mode int status = 0; for (int i = 0; i < cmd.size(); i++) { const char ch = cmd[i].toLatin1(); if (status == 0 && (ch == '#' || ch == '%')) status = 3; else if (status == 0 && ch == '\'') status = 1; else if (status == 0 && ch == '"') status = 2; else if (status == 1 && ch == '\'') status = 0; else if (status == 2 && ch == '"') status = 0; else if (status == 3 && ch == '\n') status = 0; if (status != 3) tmp += cmd[i]; } cmd = tmp; cmd.replace(QLatin1String(";\n"), QLatin1String(";")); cmd.replace(QLatin1Char('\n'), QLatin1Char(',')); cmd += QLatin1Char('\n'); return cmd; } void OctaveExpression::parseOutput(const QString& output) { qDebug() << "parseOutput: " << output; if (!output.trimmed().isEmpty()) { // TODO: what about help in comment? printf with '... help ...'? // This must be corrected. if (command().contains(QLatin1String("help"))) { addResult(new Cantor::HelpResult(output)); } else { addResult(new Cantor::TextResult(output)); } } m_finished = true; if (!m_plotPending) setStatus(Done); } void OctaveExpression::parseError(const QString& error) { if (error.startsWith(QLatin1String("warning: "))) { // It's warning, so add as result addResult(new Cantor::TextResult(error)); } else { setErrorMessage(error); setStatus(Error); } } void OctaveExpression::imageChanged() { if(m_tempFile->size() <= 0) return; const QUrl& url = QUrl::fromLocalFile(m_tempFile->fileName()); Cantor::Result* newResult; if (m_tempFile->fileName().endsWith(QLatin1String(".eps"))) newResult = new Cantor::EpsResult(url); else newResult = new Cantor::ImageResult(url); bool found = false; for (int i = 0; i < results().size(); i++) if (results()[i]->type() == newResult->type()) { replaceResult(i, newResult); found = true; } if (!found) addResult(newResult); m_plotPending = false; if (m_finished && status() != Expression::Done) { setStatus(Done); } } diff --git a/src/backends/octave/octavesession.cpp b/src/backends/octave/octavesession.cpp index 46cd5dad..935ae38e 100644 --- a/src/backends/octave/octavesession.cpp +++ b/src/backends/octave/octavesession.cpp @@ -1,343 +1,389 @@ /* Copyright (C) 2010 Miha Čančula 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. */ #include "octavesession.h" #include "octaveexpression.h" #include "octavecompletionobject.h" #include "octavesyntaxhelpobject.h" #include "octavehighlighter.h" #include "result.h" #include "textresult.h" +#include #include "settings.h" #include #include #include +#include #include #include #include #include #ifndef Q_OS_WIN #include #endif #include "octavevariablemodel.h" const QRegularExpression OctaveSession::PROMPT_UNCHANGEABLE_COMMAND = QRegularExpression(QStringLiteral("^(?:,|;)+$")); OctaveSession::OctaveSession ( Cantor::Backend* backend ) : Session ( backend), m_process(nullptr), m_prompt(QStringLiteral("CANTOR_OCTAVE_BACKEND_PROMPT:([0-9]+)> ")), m_subprompt(QStringLiteral("CANTOR_OCTAVE_BACKEND_SUBPROMPT:([0-9]+)> ")), m_previousPromptNumber(1), -m_syntaxError(false) +m_syntaxError(false), +m_isIntegratedPlotsEnabled(false) { setVariableModel(new OctaveVariableModel(this)); } OctaveSession::~OctaveSession() { if (m_process) { m_process->kill(); m_process->deleteLater(); m_process = nullptr; } } void OctaveSession::login() { qDebug() << "login"; if (m_process) return; emit loginStarted(); + bool isIntegratedPlots = OctaveSettings::integratePlots(); + QString tmpWritableErrorReason; + if (isIntegratedPlots) + { + QString filename = QDir::tempPath() + QLatin1String("/cantor_octave_plot_integration_test.txt"); + QFile::remove(filename); // Remove previous file, if precents + int test_number = rand() % 1000; + + QStringList args; + args << QLatin1String("--no-init-file"); + args << QLatin1String("--no-gui"); + args << QLatin1String("--eval"); + args << QString::fromLatin1("file_id = fopen('%1', 'w'); fdisp(file_id, %2); fclose(file_id);").arg(filename).arg(test_number); + + QString errorMsg; + isIntegratedPlots = Cantor::Backend::testProgramWritable( + OctaveSettings::path().toLocalFile(), + args, + filename, + QString::number(test_number), + &errorMsg + ); + + // If we in this branch, then isIntegratedPlots was true, but if it false now, then it means, that the writabl test is failed + if (isIntegratedPlots == false) + { + KMessageBox::error(nullptr, + i18n("Plot integration test failed.")+ + QLatin1String("\n\n")+ + errorMsg+ + QLatin1String("\n\n")+ + i18n("The integration of plots will be disabled."), + i18n("Cantor") + ); + } + } + m_isIntegratedPlotsEnabled = isIntegratedPlots; + m_process = new KProcess ( this ); QStringList args; args << QLatin1String("--silent"); args << QLatin1String("--interactive"); args << QLatin1String("--persist"); // Setting prompt and subprompt args << QLatin1String("--eval"); args << QLatin1String("PS1('CANTOR_OCTAVE_BACKEND_PROMPT:\\#> ');"); args << QLatin1String("--eval"); args << QLatin1String("PS2('CANTOR_OCTAVE_BACKEND_SUBPROMPT:\\#> ');"); // Add the cantor script directory to octave script search path const QStringList& scriptDirs = locateAllCantorFiles(QLatin1String("octavebackend"), QStandardPaths::LocateDirectory); if (scriptDirs.isEmpty()) qCritical() << "Octave script directory not found, needed for integrated plots"; else { for (const QString& dir : scriptDirs) args << QLatin1String("--eval") << QString::fromLatin1("addpath \"%1\";").arg(dir); } // Do not show extra text in help commands args << QLatin1String("--eval"); args << QLatin1String("suppress_verbose_help_message(1);"); - if (OctaveSettings::integratePlots()) + if (isIntegratedPlots) { // Do not show the popup when plotting, rather only print to a file args << QLatin1String("--eval"); args << QLatin1String("set (0, \"defaultfigurevisible\",\"off\");"); args << QLatin1String("--eval"); args << QLatin1String("if strcmp(graphics_toolkit(), \"fltk\") graphics_toolkit(\"gnuplot\") endif;"); } else { args << QLatin1String("--eval"); args << QLatin1String("set (0, \"defaultfigurevisible\",\"on\");"); } m_process->setProgram ( OctaveSettings::path().toLocalFile(), args ); qDebug() << "starting " << m_process->program(); m_process->setOutputChannelMode ( KProcess::SeparateChannels ); m_process->start(); m_process->waitForStarted(); connect ( m_process, SIGNAL (readyReadStandardOutput()), SLOT (readOutput()) ); connect ( m_process, SIGNAL (readyReadStandardError()), SLOT (readError()) ); connect ( m_process, SIGNAL (error(QProcess::ProcessError)), SLOT (processError()) ); if(!OctaveSettings::self()->autorunScripts().isEmpty()){ QString autorunScripts = OctaveSettings::self()->autorunScripts().join(QLatin1String("\n")); evaluateExpression(autorunScripts, OctaveExpression::DeleteOnFinish, true); updateVariables(); } changeStatus(Cantor::Session::Done); emit loginDone(); qDebug()<<"login done"; } void OctaveSession::logout() { qDebug()<<"logout"; if(!m_process) return; disconnect(m_process, nullptr, this, nullptr); if(status() == Cantor::Session::Running) interrupt(); m_process->write("exit\n"); qDebug()<<"send exit command to octave"; if(!m_process->waitForFinished(1000)) { m_process->kill(); qDebug()<<"octave still running, process kill enforced"; } m_process->deleteLater(); m_process = nullptr; expressionQueue().clear(); m_output.clear(); m_previousPromptNumber = 1; Session::logout(); qDebug()<<"logout done"; } void OctaveSession::interrupt() { qDebug() << expressionQueue().size(); 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 } foreach (Cantor::Expression* expression, expressionQueue()) expression->setStatus(Cantor::Expression::Interrupted); expressionQueue().clear(); // Cleanup inner state and call octave prompt printing // If we move this code for interruption to Session, we need add function for // cleaning before setting Done status m_output.clear(); m_process->write("\n"); qDebug()<<"done interrupting"; } changeStatus(Cantor::Session::Done); } void OctaveSession::processError() { qDebug() << "processError"; emit error(m_process->errorString()); } Cantor::Expression* OctaveSession::evaluateExpression ( const QString& command, Cantor::Expression::FinishingBehavior finishingBehavior, bool internal ) { qDebug() << "evaluating: " << command; OctaveExpression* expression = new OctaveExpression ( this, internal); expression->setCommand ( command ); expression->setFinishingBehavior ( finishingBehavior ); expression->evaluate(); return expression; } void OctaveSession::runFirstExpression() { OctaveExpression* expression = static_cast(expressionQueue().first()); connect(expression, SIGNAL(statusChanged(Cantor::Expression::Status)), this, SLOT(currentExpressionStatusChanged(Cantor::Expression::Status))); QString command = expression->internalCommand(); expression->setStatus(Cantor::Expression::Computing); if (isDoNothingCommand(command)) expression->setStatus(Cantor::Expression::Done); else { m_process->write ( command.toLocal8Bit() ); } } void OctaveSession::readError() { qDebug() << "readError"; QString error = QString::fromLocal8Bit(m_process->readAllStandardError()); if (!expressionQueue().isEmpty() && !error.isEmpty()) { OctaveExpression* const exp = static_cast(expressionQueue().first()); if (m_syntaxError) { m_syntaxError = false; exp->parseError(i18n("Syntax Error")); } else exp->parseError(error); m_output.clear(); } } void OctaveSession::readOutput() { qDebug() << "readOutput"; while (m_process->bytesAvailable() > 0) { QString line = QString::fromLocal8Bit(m_process->readLine()); qDebug()<<"start parsing " << " " << line; QRegularExpressionMatch match = m_prompt.match(line); if (match.hasMatch()) { const int promptNumber = match.captured(1).toInt(); // Add all text before prompt, if exists m_output += QStringRef(&line, 0, match.capturedStart(0)).toString(); if (!expressionQueue().isEmpty()) { const QString& command = expressionQueue().first()->command(); if (m_previousPromptNumber + 1 == promptNumber || isSpecialOctaveCommand(command)) { if (!expressionQueue().isEmpty()) { readError(); static_cast(expressionQueue().first())->parseOutput(m_output); } } else { // Error command don't increase octave prompt number (usually, but not always) readError(); } } m_previousPromptNumber = promptNumber; m_output.clear(); } else if ((match = m_subprompt.match(line)).hasMatch() && match.captured(1).toInt() == m_previousPromptNumber) { // User don't write finished octave statement (for example, write 'a = [1,2, ' only), so // octave print subprompt and waits input finish. m_syntaxError = true; qDebug() << "subprompt catch"; m_process->write(")]'\"\n"); // force exit from subprompt m_output.clear(); } else m_output += line; } } void OctaveSession::currentExpressionStatusChanged(Cantor::Expression::Status status) { qDebug() << "currentExpressionStatusChanged" << status << expressionQueue().first()->command(); switch (status) { case Cantor::Expression::Done: case Cantor::Expression::Error: finishFirstExpression(); break; default: break; } } Cantor::CompletionObject* OctaveSession::completionFor ( const QString& cmd, int index ) { return new OctaveCompletionObject ( cmd, index, this ); } Cantor::SyntaxHelpObject* OctaveSession::syntaxHelpFor ( const QString& cmd ) { return new OctaveSyntaxHelpObject ( cmd, this ); } QSyntaxHighlighter* OctaveSession::syntaxHighlighter ( QObject* parent ) { return new OctaveHighlighter ( parent, this ); } void OctaveSession::runSpecificCommands() { m_process->write("figure(1,'visible','off')"); } bool OctaveSession::isDoNothingCommand(const QString& command) { return PROMPT_UNCHANGEABLE_COMMAND.match(command).hasMatch() || command.isEmpty() || command == QLatin1String("\n"); } bool OctaveSession::isSpecialOctaveCommand(const QString& command) { return command.contains(QLatin1String("completion_matches")); } + +bool OctaveSession::isIntegratedPlotsEnabled() const +{ + return m_isIntegratedPlotsEnabled; +} diff --git a/src/backends/octave/octavesession.h b/src/backends/octave/octavesession.h index d76eba6e..581b3735 100644 --- a/src/backends/octave/octavesession.h +++ b/src/backends/octave/octavesession.h @@ -1,80 +1,83 @@ /* Copyright (C) 2010 Miha Čančula 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. */ #ifndef OCTAVESESSION_H #define OCTAVESESSION_H #include #include #include #include #include namespace Cantor { class DefaultVariableModel; } class KDirWatch; class OctaveExpression; class KProcess; class OctaveSession : public Cantor::Session { Q_OBJECT public: explicit OctaveSession(Cantor::Backend* backend); ~OctaveSession() override; void interrupt() override; Cantor::Expression* evaluateExpression(const QString& command, Cantor::Expression::FinishingBehavior finishingBehavior = Cantor::Expression::FinishingBehavior::DoNotDelete, bool internal = false) override; void logout() override; void login() override; Cantor::CompletionObject* completionFor(const QString& cmd, int index=-1) override; Cantor::SyntaxHelpObject* syntaxHelpFor(const QString& cmd) override; QSyntaxHighlighter* syntaxHighlighter(QObject* parent) override; void runFirstExpression() override; + bool isIntegratedPlotsEnabled() const; + private: const static QRegularExpression PROMPT_UNCHANGEABLE_COMMAND; private: KProcess* m_process; QTextStream m_stream; QRegularExpression m_prompt; QRegularExpression m_subprompt; int m_previousPromptNumber; bool m_syntaxError; QString m_output; + bool m_isIntegratedPlotsEnabled; // Better move it in worksheet, like isCompletion, etc. private: void readFromOctave(QByteArray data); bool isDoNothingCommand(const QString& command); bool isSpecialOctaveCommand(const QString& command); private Q_SLOTS: void readOutput(); void readError(); void currentExpressionStatusChanged(Cantor::Expression::Status status); void processError(); void runSpecificCommands(); }; #endif // OCTAVESESSION_H diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt index b8359368..fbcc9e3c 100644 --- a/src/lib/CMakeLists.txt +++ b/src/lib/CMakeLists.txt @@ -1,117 +1,117 @@ if(LIBSPECTRE_FOUND) include_directories(${LIBSPECTRE_INCLUDE_DIR}) endif(LIBSPECTRE_FOUND) set( cantor_LIB_SRCS session.cpp expression.cpp backend.cpp result.cpp textresult.cpp imageresult.cpp mimeresult.cpp epsresult.cpp latexresult.cpp latexrenderer.cpp renderer.cpp helpresult.cpp animationresult.cpp htmlresult.cpp extension.cpp assistant.cpp completionobject.cpp syntaxhelpobject.cpp defaulthighlighter.cpp defaultvariablemodel.cpp panelplugin.cpp panelpluginhandler.cpp worksheetaccess.cpp directives/plotdirectives.cpp jupyterutils.cpp ) Set( cantor_LIB_HDRS cantor_macros.h #base classes backend.h session.h expression.h extension.h syntaxhelpobject.h completionobject.h #results animationresult.h epsresult.h helpresult.h imageresult.h latexresult.h renderer.h result.h textresult.h mimeresult.h htmlresult.h #helper classes defaulthighlighter.h defaultvariablemodel.h worksheetaccess.h jupyterutils.h ) ki18n_wrap_ui(cantor_LIB_SRCS directives/axisrange.ui directives/plottitle.ui) kconfig_add_kcfg_files(cantor_LIB_SRCS settings.kcfgc) install(FILES cantor_libs.kcfg DESTINATION ${KDE_INSTALL_KCFGDIR}) configure_file (config-cantorlib.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-cantorlib.h ) add_library( cantorlibs SHARED ${cantor_LIB_SRCS} ) generate_export_header(cantorlibs BASE_NAME cantor) kcoreaddons_desktop_to_json(cantorlibs cantor_assistant.desktop DEFAULT_SERVICE_TYPE) kcoreaddons_desktop_to_json(cantorlibs cantor_backend.desktop DEFAULT_SERVICE_TYPE) kcoreaddons_desktop_to_json(cantorlibs cantor_panelplugin.desktop DEFAULT_SERVICE_TYPE) target_link_libraries( cantorlibs KF5::Completion KF5::IconThemes KF5::KIOCore KF5::KIOFileWidgets KF5::KIOWidgets KF5::Archive KF5::ConfigCore KF5::ConfigGui KF5::I18n KF5::XmlGui ${QT5_LIBRARIES} Qt5::Xml Qt5::Svg Poppler::Qt5 ) if(LIBSPECTRE_FOUND) target_link_libraries(cantorlibs ${LIBSPECTRE_LIBRARY}) endif(LIBSPECTRE_FOUND) -set (CANTORLIBS_SOVERSION 24) +set (CANTORLIBS_SOVERSION 25) set_target_properties( cantorlibs PROPERTIES VERSION ${RELEASE_SERVICE_VERSION} SOVERSION ${CANTORLIBS_SOVERSION}) ecm_setup_version(${RELEASE_SERVICE_VERSION} VARIABLE_PREFIX CANTOR SOVERSION ${CANTORLIBS_SOVERSION} VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/cantorlibs_version.h ) install( TARGETS cantorlibs EXPORT CantorTargets ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} ) install( FILES ${cantor_LIB_HDRS} ${CMAKE_CURRENT_BINARY_DIR}/cantor_export.h ${CMAKE_CURRENT_BINARY_DIR}/cantorlibs_version.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/cantor COMPONENT Devel ) if(BUILD_TESTING) add_subdirectory(test) endif() diff --git a/src/lib/backend.cpp b/src/lib/backend.cpp index dc33d40e..f8bb1587 100644 --- a/src/lib/backend.cpp +++ b/src/lib/backend.cpp @@ -1,212 +1,250 @@ /* 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 */ #include "backend.h" #include "extension.h" #include #include #include +#include #include #include using namespace Cantor; class Cantor::BackendPrivate { public: QString name; QString comment; QString icon; QString url; bool enabled{true}; }; Backend::Backend(QObject* parent, const QList& args) : QObject(parent), d(new BackendPrivate) { Q_UNUSED(args) } Backend::~Backend() { delete d; } QString Backend::name() const { return d->name; } QString Backend::comment() const { return d->comment; } QString Backend::description() const { return comment(); } QString Backend::icon() const { return d->icon; } QString Backend::url() const { return d->url; } bool Backend::isEnabled() const { return d->enabled && requirementsFullfilled(); } void Backend::setEnabled(bool enabled) { d->enabled = enabled; } QStringList Backend::listAvailableBackends() { QStringList l; for (Backend* b : availableBackends()) { if(b->isEnabled()) l<name(); } return l; } QList Backend::availableBackends() { static QList backendCache; //if we already have all backends Cached, just return the cache. //otherwise create the available backends if(!backendCache.isEmpty()) { return backendCache; } QStringList pluginDirs; for (const QString& dir : QCoreApplication::libraryPaths()){ pluginDirs << dir + QDir::separator() + QLatin1String("cantor/backends"); } QPluginLoader loader; for (const QString &dir : pluginDirs){ qDebug() << "dir: " << dir; QStringList plugins; QDir pluginDir = QDir(dir); plugins = pluginDir.entryList(); for (const QString &plugin : plugins){ if (plugin==QLatin1String(".") || plugin==QLatin1String("..")) continue; loader.setFileName(dir + QDir::separator() + plugin); if (!loader.load()){ qDebug() << "Error while loading plugin: " << plugin; continue; } KPluginFactory* factory = KPluginLoader(loader.fileName()).factory(); Backend* backend = factory->create(QCoreApplication::instance()); if (!backend){ qDebug() << "Error using plugin " << loader.fileName(); qDebug() << "Error message: " << loader.errorString(); continue; } KPluginMetaData info(loader); backend->d->name=info.name(); backend->d->comment=info.description(); backend->d->icon=info.iconName(); backend->d->url=info.website(); backendCache<name().toLower()==name.toLower() || b->id().toLower()==name.toLower()) return b; } return nullptr; } QStringList Backend::extensions() const { QList extensions = findChildren(QRegularExpression(QLatin1String(".*Extension"))); QStringList names; for (Extension* e : extensions) names << e->objectName(); return names; } Extension* Backend::extension(const QString& name) const { return findChild(name); } bool Backend::checkExecutable(const QString& name, const QString& path, QString* reason) { if (path.isEmpty()) { if (reason) *reason = i18n("No path for the %1 executable specified. " "Please provide the correct path in the application settings and try again.", name); return false; } QFileInfo info(path); if (!info.exists()) { if (reason) *reason = i18n("The specified file '%1' for the %2 executable doesn't exist. " "Please provide the correct path in the application settings and try again.", path, name); return false; } if (!info.isExecutable()) { if (reason) *reason = i18n("The specified file '%1' doesn't point to an executable. " "Please provide the correct path in the application settings and try again.", path); return false; } return true; } + +bool Cantor::Backend::testProgramWritable(const QString& program, const QStringList& args, const QString& filename, const QString& expectedContent, QString* reason, int timeOut) +{ + QProcess process; + process.setProgram(program); + process.setArguments(args); + process.start(); + + if (process.waitForFinished(timeOut) == false) + { + if (reason) + *reason = i18n("The program %1 didn't finish the execution after %2 milliseconds during the plot integration test.", QFileInfo(program).fileName(), timeOut); + + return false; + } + + QFile file(filename); + if (!file.open(QIODevice::ReadOnly)) + { + if (reason) + *reason = i18n("Failed to open the file %1 during the plot integration test.", filename); + return false; + } + + QString fileContent = QString::fromLocal8Bit(file.readAll()); + if (fileContent.trimmed() != expectedContent) + { + if (reason) + *reason = i18n("Failed to parse the result during the plot integration test."); + return false; + } + + file.close(); + file.remove(); + + return true; +} diff --git a/src/lib/backend.h b/src/lib/backend.h index 641e6f30..b9b83f69 100644 --- a/src/lib/backend.h +++ b/src/lib/backend.h @@ -1,228 +1,235 @@ /* 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 */ #ifndef _BACKEND_H #define _BACKEND_H #include #include #include #include #include "cantor_export.h" class KConfigSkeleton; class QWidget; /** * Namespace collecting all Classes of the Cantor Libraries */ namespace Cantor { class Session; class Extension; class BackendPrivate; /** * The Backend class provides access to information about the backend. * It provides access to what features are supported by the backend, and * a factory method to create a new Session * It needs to be subclassed by all Backends. * * @author Alexander Rieder */ class CANTOR_EXPORT Backend : public QObject, public KXMLGUIClient { Q_OBJECT public: /** * This enum is used to specify the Features, supported by a backend. */ enum Capability{ Nothing = 0x0, ///< the Backend doesn't support any of the optional features LaTexOutput = 0x1, ///< it can output results as LaTeX code InteractiveMode = 0x2, /**< it supports an interactive workflow. (It means a command can ask for additional input while running */ SyntaxHighlighting = 0x4, ///< it offers a custom Syntax Highlighter Completion = 0x8, ///< it offers completion of partially typed commands SyntaxHelp = 0x10, /**< it offers help about a commands syntax, that will be shown in a tooltip */ VariableManagement= 0x20 ///< it offers access to the variables (for variable management panel) }; Q_DECLARE_FLAGS(Capabilities, Capability) protected: /** * Create a new Backend. Normally the static createBackend factory method * should be used. * @param parent the Parent object * @param args optional arguments (not used) */ explicit Backend( QObject* parent = nullptr,const QList& args=QList() ); /** * Destructor. Doesn't anything. */ ~Backend() override; public: /** * Creates a new Session. It is the way to go to create a Session, * don't call new Session on your own. * @return a new Session of this Backend, or 0 if creating failed */ virtual Session* createSession() = 0; /** * Returns list of the supported optional features * @return a list of features, containing items of the Capability enum, ORed together */ virtual Capabilities capabilities() const = 0; /** * Returns whether all of this backends requirements are fulfilled, or if some are missing. * @param reason if set, backend write information about missing requirements, if they exists * @return @c true if all the requirements needed to use this Backend are fulfilled * @return @c false some requirements are missing. e.g. the maxima executable can not be found * @see Capability */ virtual bool requirementsFullfilled(QString* const reason = nullptr) const = 0; /** * Returns a unique string to identify this backend. * In contrast to name() this string isn't translated * @return string to identify backend */ virtual QString id() const = 0; /** * Returns the recommended version of the backend supported by Cantor * @return the recommended version of the backend */ virtual QString version() const = 0; //Stuff extracted from the .desktop file /** * Returns the name of the backend * @return the backends name */ QString name() const; /** * Returns a short comment about the backend. * @return comment about the backend */ QString comment() const; /** * Returns the icon to use with this backend * @return name of the icon */ QString icon() const; /** * Returns the Url of the Homepage for the Backend * @return the url */ QString url() const; /** * Returns an Url pointing to the Help of the Backend * The method should be overwritten by all Backends(who have an online help) * You should make the returned Url translatable, e.g. by doing something like: * return i18nc("the url to the documentation of KAlgebra, please check if there is a translated version and use the correct url", * "https://docs.kde.org/stable5/en/kdeedu/kalgebra/index.html"); * @return Url of the help */ virtual QUrl helpUrl() const = 0; /** * Returns if the backend should be enabled (shown in the Backend dialog) * @return @c true, if the enabled flag is set to true, and the requirements are fulfilled * @return @c false, if the backend was purposely disabled, or requirements are missing * @see requirementsFullfilled() */ bool isEnabled() const; /** * Enables/disables this backend * @param enabled true to enable backend false to disable */ void setEnabled(bool enabled); /** * Returns a longer description of the Backend, e.g. purpose, strengths etc. * It should help the user to decide between the different Backends * @return a description of the backend. It can contain html */ virtual QString description() const; /** * Returns a Widget for configuring this backend * @return Widget for usage in the Settings dialog */ virtual QWidget* settingsWidget(QWidget* parent) const = 0; /** * Returns a KConfig object, containing all the settings, * the backend might need * @return a KConfigSkeleton object, for configuring this backend */ virtual KConfigSkeleton* config() const = 0; /** * Returns a list of the names of all the Extensions supported by this backend * @return a list of the names of all the Extensions supported by this backend * @see extension(const QString& name) */ QStringList extensions() const; /** * Returns an Extension of this backend for the given name, or null * if the Backend doesn't have an extension with this name. * @return Pointer to the Extension object with the given name */ Extension* extension(const QString& name) const; /** * Returns a list of the names of all the installed and enabled backends * @return a list of the names of all the installed and enabled backends * @see isEnabled() */ static QStringList listAvailableBackends(); /** * Returns Pointers to all the installed backends * @return Pointers to all the installed backends */ static QList availableBackends(); /** * Returns the backend with the given name, or null if it isn't found * @return the backend with the given name, or null if it isn't found */ static Backend* getBackend(const QString& name); /** * @return @c true if all the requirements (the path is correct, the file is executable, etc.) are fulfilled * for the backend @c Name with the path to the executable @c path and false otherwise. * In case the requrements are not fulfilled, the reason is written to @c reason. */ - static bool checkExecutable(const QString& name, const QString& path, QString* reason); + static bool checkExecutable(const QString& name, const QString& path, QString* reason = nullptr); + + /** + * + */ + static bool testProgramWritable( + const QString& program, const QStringList& args, const QString& filename, const QString& expectedContent, QString* reason = nullptr, int timeOut = 5000 + ); private: BackendPrivate* d; }; Q_DECLARE_OPERATORS_FOR_FLAGS(Backend::Capabilities) } #endif /* _BACKEND_H */