File indexing completed on 2024-04-28 11:20:48

0001 /*
0002     SPDX-License-Identifier: GPL-2.0-or-later
0003     SPDX-FileCopyrightText: 2009 Alexander Rieder <alexanderrieder@gmail.com>
0004     SPDX-FileCopyrightText: 2019 Alexander Semke <alexander.semke@web.de>
0005 */
0006 
0007 #include "session.h"
0008 using namespace Cantor;
0009 
0010 #include <map>
0011 
0012 #include "backend.h"
0013 #include "textresult.h"
0014 
0015 #include <QDebug>
0016 #include <QEventLoop>
0017 #include <QQueue>
0018 #include <QTimer>
0019 
0020 #include <KMessageBox>
0021 #include <KLocalizedString>
0022 
0023 class Cantor::SessionPrivate
0024 {
0025   public:
0026     SessionPrivate() { }
0027 
0028     Backend* backend{nullptr};
0029     Session::Status status{Session::Disable};
0030     bool typesettingEnabled{false};
0031     int expressionCount{0};
0032     QList<Cantor::Expression*> expressionQueue;
0033     DefaultVariableModel* variableModel{nullptr};
0034     QList<GraphicPackage> usableGraphicPackages;
0035     QList<GraphicPackage> enabledGraphicPackages;
0036     QList<QString> ignorableGraphicPackageIds;
0037     bool needUpdate{false};
0038 };
0039 
0040 Session::Session(Backend* backend ) : QObject(backend), d(new SessionPrivate)
0041 {
0042     d->backend = backend;
0043 
0044 #ifdef WITH_EPS
0045     if (Cantor::LatexRenderer::isLatexAvailable())
0046         d->typesettingEnabled = Settings::self()->typesetDefault();
0047 #endif
0048 }
0049 
0050 Session::Session(Backend* backend, DefaultVariableModel* model) : QObject(backend), d(new SessionPrivate)
0051 {
0052     d->backend = backend;
0053     d->variableModel = model;
0054 
0055 #ifdef WITH_EPS
0056     if (Cantor::LatexRenderer::isLatexAvailable())
0057         d->typesettingEnabled = Settings::self()->typesetDefault();
0058 #endif
0059 }
0060 
0061 Session::~Session()
0062 {
0063     delete d;
0064 }
0065 
0066 void Cantor::Session::testGraphicsPackages(QList<GraphicPackage> packages)
0067 {
0068     std::map<QString, bool> handlingStatus;
0069 
0070     QEventLoop loop;
0071     for (GraphicPackage& package : packages)
0072     {
0073         if (GraphicPackage::findById(package, d->usableGraphicPackages) != -1)
0074             continue;
0075 
0076         handlingStatus[package.id()] = false;
0077         Expression* expr = package.isAvailable(this);
0078 
0079         connect(expr, &Expression::expressionFinished, [this, expr, &package, &loop, &handlingStatus](Expression::Status status) {
0080             if (status == Expression::Status::Done) {
0081                 if (expr->result() != nullptr
0082                     && expr->result()->type() == TextResult::Type
0083                     && expr->result()->data().toString() == QLatin1String("1")) {
0084                     this->d->usableGraphicPackages.push_back(package);
0085                 }
0086             } else {
0087                 qDebug() << "test presence command for" << package.id() << "finished because of" << (status == Expression::Error ? "error" : "interrupt");
0088                 if (status == Expression::Error && expr)
0089                     qDebug() << "error message:" << expr->errorMessage();
0090             }
0091 
0092             handlingStatus[package.id()] = true;
0093 
0094             bool allExpersionsFinished = true;
0095             for (auto& iter : handlingStatus)
0096             {
0097                 if (iter.second == false)
0098                 {
0099                     allExpersionsFinished = false;
0100                     break;
0101                 }
0102             }
0103 
0104             if (allExpersionsFinished)
0105                 loop.exit();
0106         });
0107     }
0108     // If handlingStatus size is empty (it means, that no connections have been done), then we will stay in the 'loop' event loop forever
0109     if (handlingStatus.size() != 0)
0110         loop.exec();
0111 }
0112 
0113 void Session::logout()
0114 {
0115     if (d->status == Session::Running)
0116         interrupt();
0117 
0118     if (d->variableModel)
0119     {
0120         d->variableModel->clearVariables();
0121         d->variableModel->clearFunctions();
0122     }
0123 
0124     d->expressionCount = 0;
0125     changeStatus(Status::Disable);
0126 
0127     // Clean graphic package state
0128     d->enabledGraphicPackages.clear();
0129     d->ignorableGraphicPackageIds.clear();
0130     d->usableGraphicPackages.clear();
0131 }
0132 
0133 QList<Expression*>& Cantor::Session::expressionQueue() const
0134 {
0135     return d->expressionQueue;
0136 }
0137 
0138 void Session::enqueueExpression(Expression* expr)
0139 {
0140     d->expressionQueue.append(expr);
0141 
0142     //run the newly added expression immediately if it's the only one in the queue
0143     if (d->expressionQueue.size() == 1)
0144     {
0145         changeStatus(Cantor::Session::Running);
0146         runFirstExpression();
0147     }
0148     else
0149         expr->setStatus(Cantor::Expression::Queued);
0150 }
0151 
0152 void Session::runFirstExpression()
0153 {
0154 
0155 }
0156 
0157 void Session::finishFirstExpression(bool setDoneAfterUpdate)
0158 {
0159     if (!d->expressionQueue.isEmpty())
0160     {
0161         auto first = d->expressionQueue.takeFirst();
0162         d->needUpdate |= !first->isInternal() && !first->isHelpRequest();
0163     }
0164 
0165     if (d->expressionQueue.isEmpty())
0166         if (d->variableModel && d->needUpdate)
0167         {
0168             d->variableModel->update();
0169             d->needUpdate = false;
0170 
0171             // Some variable models could update internal lists without running expressions
0172             // or don't need to be updated at all like for Maxima being in Lisp-mode.
0173             // So, if after update queue still empty, set status to Done
0174             // setDoneAfterUpdate used for compatibility with some backends, like R - TODO: check why this is requried
0175             if (setDoneAfterUpdate && d->expressionQueue.isEmpty())
0176                 changeStatus(Done);
0177             else if (d->expressionQueue.isEmpty())
0178                 changeStatus(Done);
0179         }
0180         else
0181             changeStatus(Done);
0182     else
0183         runFirstExpression();
0184 }
0185 
0186 void Session::currentExpressionStatusChanged(Cantor::Expression::Status status)
0187 {
0188     auto* expression = expressionQueue().first();
0189     qDebug() << "expression status changed: command = " << expression->command() << ", status = " << status;
0190 
0191     switch (status)
0192     {
0193     case Cantor::Expression::Done:
0194     case Cantor::Expression::Error:
0195         qDebug()<<"################################## EXPRESSION END ###############################################";
0196         disconnect(expression, &Cantor::Expression::statusChanged, this, &Session::currentExpressionStatusChanged);
0197         finishFirstExpression();
0198         break;
0199     default:
0200         break;
0201     }
0202 }
0203 
0204 Backend* Session::backend()
0205 {
0206     return d->backend;
0207 }
0208 
0209 Cantor::Session::Status Session::status()
0210 {
0211     return d->status;
0212 }
0213 
0214 void Session::changeStatus(Session::Status newStatus)
0215 {
0216     d->status = newStatus;
0217     emit statusChanged(newStatus);
0218 }
0219 
0220 void Session::setTypesettingEnabled(bool enable)
0221 {
0222     d->typesettingEnabled = enable;
0223 }
0224 
0225 bool Session::isTypesettingEnabled()
0226 {
0227     return d->typesettingEnabled;
0228 }
0229 
0230 void Session::setWorksheetPath(const QString&) { }
0231 
0232 CompletionObject* Session::completionFor(const QString&, int)
0233 {
0234     //Return nullptr per default, so Backends not offering tab completions don't have
0235     //to reimplement this. This method should only be called on backends with
0236     //the Completion Capability flag
0237 
0238     return nullptr;
0239 }
0240 
0241 SyntaxHelpObject* Session::syntaxHelpFor(const QString&)
0242 {
0243     //Return nullptr per default, so Backends not offering tab completions don't have
0244     //to reimplement this. This method should only be called on backends with
0245     //the SyntaxHelp Capability flag
0246     return nullptr;
0247 }
0248 
0249 QSyntaxHighlighter* Session::syntaxHighlighter(QObject*)
0250 {
0251     return nullptr;
0252 }
0253 
0254 DefaultVariableModel* Session::variableModel() const
0255 {
0256     //By default, there is variableModel in session, used by syntax higlighter for variable analyzing
0257     //The model store only variable names by default.
0258     //In backends with VariableManagement Capability flag, this model also used for Cantor variable doc panel
0259     return d->variableModel;
0260 }
0261 
0262 QAbstractItemModel* Session::variableDataModel() const
0263 {
0264     return variableModel();
0265 }
0266 
0267 void Session::updateVariables()
0268 {
0269     if (d->variableModel)
0270     {
0271         d->variableModel->update();
0272         d->needUpdate = false;
0273     }
0274 }
0275 
0276 void Cantor::Session::setVariableModel(Cantor::DefaultVariableModel* model)
0277 {
0278     d->variableModel = model;
0279 }
0280 
0281 int Session::nextExpressionId()
0282 {
0283     return d->expressionCount++;
0284 }
0285 
0286 QString Session::locateCantorFile(const QString& partialPath, QStandardPaths::LocateOptions options)
0287 {
0288     QString file = QStandardPaths::locate(QStandardPaths::AppDataLocation, partialPath, options);
0289 
0290     if (file.isEmpty())
0291         file = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("cantor/") + partialPath, options);
0292 
0293     return file;
0294 }
0295 
0296 QStringList Session::locateAllCantorFiles(const QString& partialPath, QStandardPaths::LocateOptions options)
0297 {
0298     QStringList files = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, partialPath, options);
0299 
0300     if (files.isEmpty())
0301         files = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QLatin1String("cantor/") + partialPath, options);
0302 
0303     return files;
0304 }
0305 
0306 void Cantor::Session::reportSessionCrash(const QString& additionalInfo)
0307 {
0308     // Reporting about crashing backend in session without backend has not sense
0309     if (d->backend == nullptr)
0310         return;
0311 
0312     if (additionalInfo.isEmpty())
0313         KMessageBox::error(nullptr, i18n("%1 process has died unexpectedly. All calculation results are lost.", d->backend->name()), i18n("Error - Cantor"));
0314     else
0315         KMessageBox::error(nullptr, i18n("%1 process has died unexpectedly with message \"%2\". All calculation results are lost.", d->backend->name(), additionalInfo), i18n("Error - Cantor"));
0316     logout();
0317 }
0318 
0319 QList<Cantor::GraphicPackage> Cantor::Session::usableGraphicPackages()
0320 {
0321     return d->usableGraphicPackages;
0322 }
0323 
0324 const QList<Cantor::GraphicPackage>& Cantor::Session::enabledGraphicPackages() const
0325 {
0326     return d->enabledGraphicPackages;
0327 }
0328 
0329 QString Cantor::Session::graphicPackageErrorMessage(QString packageId) const
0330 {
0331     Q_UNUSED(packageId);
0332     return QString();
0333 }
0334 
0335 void Cantor::Session::updateEnabledGraphicPackages(const QList<Cantor::GraphicPackage>& newEnabledPackages, const QString& additionalInfo)
0336 {
0337     if (newEnabledPackages.isEmpty())
0338     {
0339         if (!d->enabledGraphicPackages.isEmpty())
0340         {
0341             for (const GraphicPackage& package : d->enabledGraphicPackages)
0342                 evaluateExpression(package.disableSupportCommand(), Cantor::Expression::DeleteOnFinish, true);
0343         }
0344         d->enabledGraphicPackages.clear();
0345     }
0346     else
0347     {
0348         QList<GraphicPackage> packagesExceptIgnored;
0349         for (const GraphicPackage& package : newEnabledPackages)
0350             if (d->ignorableGraphicPackageIds.contains(package.id()) == false)
0351                 packagesExceptIgnored.append(package);
0352 
0353         testGraphicsPackages(packagesExceptIgnored);
0354 
0355         QList<GraphicPackage> unavailablePackages;
0356         QList<GraphicPackage> willEnabledPackages;
0357 
0358         for (const GraphicPackage& package : packagesExceptIgnored)
0359         {
0360             if (GraphicPackage::findById(package, usableGraphicPackages()) != -1)
0361                 willEnabledPackages.append(package);
0362             else
0363                 unavailablePackages.append(package);
0364         }
0365 
0366         for (const GraphicPackage& package : d->enabledGraphicPackages)
0367             if (GraphicPackage::findById(package, willEnabledPackages) == -1)
0368                 evaluateExpression(package.disableSupportCommand(), Cantor::Expression::DeleteOnFinish, true);
0369 
0370         for (const GraphicPackage& newPackage : willEnabledPackages)
0371             if (GraphicPackage::findById(newPackage, d->enabledGraphicPackages) == -1)
0372                 evaluateExpression(newPackage.enableSupportCommand(additionalInfo), Cantor::Expression::DeleteOnFinish, true);
0373 
0374         d->enabledGraphicPackages = willEnabledPackages;
0375 
0376         for (const Cantor::GraphicPackage& notEnabledPackage : unavailablePackages)
0377         {
0378             if (d->ignorableGraphicPackageIds.contains(notEnabledPackage.id()) == false)
0379             {
0380                 KMessageBox::information(nullptr, i18n(
0381                     "You choose support for %1 graphic package, but the support can't be "\
0382                     "activated due to the missing requirements, so integration for this package will be disabled. %2",
0383                     notEnabledPackage.name(), graphicPackageErrorMessage(notEnabledPackage.id())), i18n("Cantor")
0384                 );
0385 
0386                 d->ignorableGraphicPackageIds.append(notEnabledPackage.id());
0387             }
0388         }
0389     }
0390 }