File indexing completed on 2024-05-12 03:47:20

0001 /*
0002     File                 : CantorWorksheet.cpp
0003     Project              : LabPlot
0004     Description          : Aspect providing a Cantor Worksheets for Multiple backends
0005     --------------------------------------------------------------------
0006     SPDX-FileCopyrightText: 2015 Garvit Khatri <garvitdelhi@gmail.com>
0007     SPDX-FileCopyrightText: 2016-2023 Alexander Semke <alexander.semke@web.de>
0008     SPDX-FileCopyrightText: 2022 Stefan Gerlach <stefan.gerlach@uni.kn>
0009     SPDX-License-Identifier: GPL-2.0-or-later
0010 */
0011 
0012 #include "CantorWorksheet.h"
0013 #include "VariableParser.h"
0014 #include "backend/core/Project.h"
0015 #include "backend/core/Settings.h"
0016 #include "backend/core/column/Column.h"
0017 #include "backend/core/column/ColumnPrivate.h"
0018 #include "backend/lib/XmlStreamReader.h"
0019 #include "commonfrontend/cantorWorksheet/CantorWorksheetView.h"
0020 
0021 #include "3rdparty/cantor/cantor_part.h"
0022 #include <cantor/cantorlibs_version.h>
0023 #include <cantor/worksheetaccess.h>
0024 
0025 #ifdef HAVE_NEW_CANTOR_LIBS
0026 #include <cantor/panelplugin.h>
0027 #include <cantor/panelpluginhandler.h>
0028 #else
0029 #include "3rdparty/cantor/panelplugin.h"
0030 #include "3rdparty/cantor/panelpluginhandler.h"
0031 #endif
0032 
0033 #include <KConfigGroup>
0034 #include <KLocalizedString>
0035 #include <KParts/ReadWritePart>
0036 #include <KPluginFactory>
0037 #include <KPluginMetaData>
0038 #include <kcoreaddons_version.h>
0039 
0040 #include <QAction>
0041 #include <QFileInfo>
0042 #include <QModelIndex>
0043 
0044 CantorWorksheet::CantorWorksheet(const QString& name, bool loading)
0045     : AbstractPart(name, AspectType::CantorWorksheet)
0046     , m_backendName(name) {
0047     if (!loading)
0048         init();
0049 }
0050 
0051 /*!
0052     initializes Cantor's part and plugins
0053 */
0054 bool CantorWorksheet::init(QByteArray* content) {
0055     DEBUG(Q_FUNC_INFO)
0056 
0057 #if KCOREADDONS_VERSION < QT_VERSION_CHECK(5, 86, 0)
0058     KPluginLoader loader(QLatin1String("kf5/parts/cantorpart"));
0059     KPluginLoader oldLoader(QLatin1String("cantorpart")); // old path
0060     KPluginFactory* factory = loader.factory();
0061 
0062     if (!factory) { // try old path
0063         WARN("Failed to load Cantor plugins; file name: " << STDSTRING(loader.fileName()))
0064         WARN("Error message: " << STDSTRING(loader.errorString()))
0065         factory = oldLoader.factory();
0066     }
0067     if (!factory) {
0068         // we can only get to this here if we open a project having Cantor content and Cantor plugins were not found.
0069         // return false here, a proper error message will be created in load() and propagated further.
0070         WARN("Failed to load Cantor plugins; file name: " << STDSTRING(oldLoader.fileName()))
0071         WARN("Error message: " << STDSTRING(oldLoader.errorString()))
0072         m_error = i18n("Couldn't find the dynamic library 'cantorpart'. Please check your installation.");
0073         return false;
0074     } else {
0075         m_part = factory->create<KParts::ReadWritePart>(this, QVariantList() << m_backendName << QLatin1String("--noprogress"));
0076 
0077 #else
0078     const auto result = KPluginFactory::instantiatePlugin<KParts::ReadWritePart>(KPluginMetaData(QStringLiteral("kf5/parts/cantorpart")),
0079                                                                                  this,
0080                                                                                  QVariantList() << m_backendName << QLatin1String("--noprogress"));
0081 
0082     if (!result) {
0083         WARN("Could not find cantorpart part");
0084         return false;
0085     } else {
0086         m_part = result.plugin;
0087 #endif
0088         if (!m_part) {
0089             WARN("Could not create the Cantor Part for backend " << STDSTRING(m_backendName))
0090             m_error = i18n("Couldn't find the plugin for %1. Please check your installation.", m_backendName);
0091             return false;
0092         }
0093 
0094         m_worksheetAccess = m_part->findChild<Cantor::WorksheetAccessInterface*>(Cantor::WorksheetAccessInterface::Name);
0095         if (!m_worksheetAccess)
0096             return false;
0097 
0098         // load worksheet content if available
0099         if (content)
0100             m_worksheetAccess->loadWorksheetFromByteArray(content);
0101 
0102         connect(m_worksheetAccess, SIGNAL(modified()), this, SLOT(modified()));
0103 
0104         // Cantor's session
0105 #ifdef HAVE_CANTOR_LIBS
0106         m_session = m_worksheetAccess->session();
0107         if (m_session) {
0108             connect(m_session, &Cantor::Session::statusChanged, this, &CantorWorksheet::statusChanged);
0109 
0110             // variable model
0111             m_variableModel = m_session->variableDataModel();
0112             connect(m_variableModel, &QAbstractItemModel::dataChanged, this, &CantorWorksheet::dataChanged);
0113             connect(m_variableModel, &QAbstractItemModel::rowsInserted, this, &CantorWorksheet::rowsInserted);
0114             connect(m_variableModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &CantorWorksheet::rowsAboutToBeRemoved);
0115             connect(m_variableModel, &QAbstractItemModel::modelReset, this, &CantorWorksheet::modelReset);
0116         }
0117 #endif
0118 
0119         // default settings
0120         const KConfigGroup group = Settings::group(QStringLiteral("Settings_Notebook"));
0121 
0122         // TODO: right now we don't have the direct accces to Cantor's worksheet and to all its public methods
0123         // and we need to go through the actions provided in cantor_part.
0124         //-> redesign this! expose Cantor's Worksheet directly and add more settings here.
0125         auto* action = m_part->action("enable_highlighting");
0126         if (action) {
0127             bool value = group.readEntry(QLatin1String("SyntaxHighlighting"), false);
0128             action->setChecked(value);
0129         }
0130 
0131         action = m_part->action("enable_completion");
0132         if (action) {
0133             bool value = group.readEntry(QLatin1String("SyntaxCompletion"), false);
0134             action->setChecked(value);
0135         }
0136 
0137         action = m_part->action("enable_expression_numbers");
0138         if (action) {
0139             bool value = group.readEntry(QLatin1String("LineNumbers"), false);
0140             action->setChecked(value);
0141         }
0142 
0143         action = m_part->action("enable_typesetting");
0144         if (action) {
0145             bool value = group.readEntry(QLatin1String("LatexTypesetting"), false);
0146             action->setChecked(value);
0147         }
0148 
0149         action = m_part->action("enable_animations");
0150         if (action) {
0151             bool value = group.readEntry(QLatin1String("Animations"), false);
0152             action->setChecked(value);
0153         }
0154 
0155         // bool value = group.readEntry(QLatin1String("ReevaluateEntries"), false);
0156         // value = group.readEntry(QLatin1String("AskConfirmation"), true);
0157     }
0158 
0159     return true;
0160 }
0161 
0162 const QString& CantorWorksheet::error() const {
0163     return m_error;
0164 }
0165 
0166 // SLots
0167 void CantorWorksheet::dataChanged(const QModelIndex& index) {
0168     parseData(index.row());
0169 }
0170 
0171 void CantorWorksheet::rowsInserted(const QModelIndex& /*parent*/, int first, int last) {
0172     for (int i = first; i <= last; ++i)
0173         parseData(i);
0174 
0175     project()->setChanged(true);
0176 }
0177 
0178 void CantorWorksheet::parseData(int row) {
0179     const QString& name = m_variableModel->data(m_variableModel->index(row, 0)).toString();
0180     QVariant dataValue = m_variableModel->data(m_variableModel->index(row, 1), 257);
0181     if (dataValue.isNull())
0182         dataValue = m_variableModel->data(m_variableModel->index(row, 1));
0183 
0184     const QString& value = dataValue.toString();
0185     VariableParser parser(m_backendName, value);
0186 
0187     if (parser.isParsed()) {
0188         auto* col = child<Column>(name);
0189         if (col) {
0190             switch (parser.dataType()) {
0191             case AbstractColumn::ColumnMode::Integer:
0192                 col->setColumnMode(AbstractColumn::ColumnMode::Integer);
0193                 col->setIntegers(parser.integers());
0194                 break;
0195             case AbstractColumn::ColumnMode::BigInt:
0196                 col->setColumnMode(AbstractColumn::ColumnMode::BigInt);
0197                 col->setBigInts(parser.bigInt());
0198                 break;
0199             case AbstractColumn::ColumnMode::Double:
0200                 col->setColumnMode(AbstractColumn::ColumnMode::Double);
0201                 col->setValues(parser.doublePrecision());
0202                 break;
0203             case AbstractColumn::ColumnMode::Month:
0204             case AbstractColumn::ColumnMode::Day:
0205             case AbstractColumn::ColumnMode::DateTime:
0206                 col->setColumnMode(AbstractColumn::ColumnMode::DateTime);
0207                 col->setDateTimes(parser.dateTime());
0208                 break;
0209             case AbstractColumn::ColumnMode::Text:
0210                 col->setColumnMode(AbstractColumn::ColumnMode::Text);
0211                 col->setText(parser.text());
0212                 break;
0213             }
0214         } else {
0215             // Column doesn't exist for this variable yet either because it was not defined yet or
0216             // because its values was changed now to an array-like structure after the initial definition.
0217             // -> create a new column for the current variable
0218             switch (parser.dataType()) {
0219             case AbstractColumn::ColumnMode::Integer:
0220                 col = new Column(name, parser.integers());
0221                 break;
0222             case AbstractColumn::ColumnMode::BigInt:
0223                 col = new Column(name, parser.bigInt());
0224                 break;
0225             case AbstractColumn::ColumnMode::Double:
0226                 col = new Column(name, parser.doublePrecision());
0227                 break;
0228             case AbstractColumn::ColumnMode::Month:
0229             case AbstractColumn::ColumnMode::Day:
0230             case AbstractColumn::ColumnMode::DateTime:
0231                 col = new Column(name, parser.dateTime(), parser.dataType());
0232                 break;
0233             case AbstractColumn::ColumnMode::Text:
0234                 col = new Column(name, parser.text());
0235                 break;
0236             }
0237             col->setUndoAware(false);
0238             col->setFixed(true);
0239             addChild(col);
0240 
0241             // TODO: Cantor currently ignores the order of variables in the worksheets
0242             // and adds new variables at the last position in the model.
0243             // Fix this in Cantor and switch to insertChildBefore here later.
0244             // insertChildBefore(col, child<Column>(i));
0245         }
0246     } else {
0247         // the already existing variable doesn't contain any numerical values -> remove it
0248         Column* col = child<Column>(name);
0249         if (col)
0250             removeChild(col);
0251     }
0252 }
0253 
0254 void CantorWorksheet::modified() {
0255     project()->setChanged(true);
0256 }
0257 
0258 void CantorWorksheet::modelReset() {
0259     for (auto* column : children<Column>())
0260         column->remove();
0261 }
0262 
0263 void CantorWorksheet::rowsAboutToBeRemoved(const QModelIndex& /*parent*/, int first, int last) {
0264     for (int i = first; i <= last; ++i) {
0265         const QString& name = m_variableModel->data(m_variableModel->index(first, 0)).toString();
0266         Column* column = child<Column>(name);
0267         if (column)
0268             column->remove();
0269     }
0270 }
0271 
0272 QList<Cantor::PanelPlugin*> CantorWorksheet::getPlugins() {
0273     if (!m_pluginsLoaded) {
0274 #ifdef HAVE_NEW_CANTOR_LIBS
0275         auto* handler = new Cantor::PanelPluginHandler(this);
0276         handler->loadPlugins();
0277         m_plugins = handler->activePluginsForSession(m_session, Cantor::PanelPluginHandler::PanelStates());
0278         for (auto* plugin : m_plugins)
0279             plugin->connectToShell(m_part);
0280 #else
0281         auto* handler = m_part->findChild<Cantor::PanelPluginHandler*>(QLatin1String("PanelPluginHandler"));
0282         if (!handler) {
0283             m_error = i18n("Couldn't find panel plugins. Please check your installation.");
0284             return false;
0285         }
0286         m_plugins = handler->plugins();
0287 #endif
0288 
0289         m_pluginsLoaded = true;
0290     }
0291 
0292     return m_plugins;
0293 }
0294 
0295 KParts::ReadWritePart* CantorWorksheet::part() {
0296     return m_part;
0297 }
0298 
0299 QIcon CantorWorksheet::icon() const {
0300 #ifdef HAVE_CANTOR_LIBS
0301     if (m_session)
0302         return QIcon::fromTheme(m_session->backend()->icon());
0303 #endif
0304     return {};
0305 }
0306 
0307 QWidget* CantorWorksheet::view() const {
0308     if (!m_partView) {
0309         m_view = new CantorWorksheetView(const_cast<CantorWorksheet*>(this));
0310         m_view->setBaseSize(1500, 1500);
0311         m_partView = m_view;
0312         connect(this, &CantorWorksheet::viewAboutToBeDeleted, [this]() {
0313             m_view = nullptr;
0314         });
0315         //  connect(m_view, SIGNAL(statusInfo(QString)), this, SIGNAL(statusInfo(QString)));
0316 
0317         // set the current path in the session to the path of the project file
0318 #ifdef HAVE_CANTOR_LIBS
0319         if (m_session) {
0320             const Project* project = const_cast<CantorWorksheet*>(this)->project();
0321             const QString& fileName = project->fileName();
0322             if (!fileName.isEmpty()) {
0323                 QFileInfo fi(fileName);
0324                 m_session->setWorksheetPath(fi.filePath());
0325             }
0326         }
0327 #endif
0328     }
0329     return m_partView;
0330 }
0331 
0332 //! Return a new context menu.
0333 /**
0334  * The caller takes ownership of the menu.
0335  */
0336 QMenu* CantorWorksheet::createContextMenu() {
0337     QMenu* menu = AbstractPart::createContextMenu();
0338     Q_ASSERT(menu);
0339     Q_EMIT requestProjectContextMenu(menu);
0340     return menu;
0341 }
0342 
0343 void CantorWorksheet::fillColumnContextMenu(QMenu* menu, Column* column) {
0344     if (m_view)
0345         m_view->fillColumnContextMenu(menu, column);
0346 }
0347 
0348 QString CantorWorksheet::backendName() {
0349     return this->m_backendName;
0350 }
0351 
0352 bool CantorWorksheet::exportView() const {
0353     // TODO: file_export_pdf exists starting with Cantor 23.12,
0354     // remove this check later once 23.12 is the minimal
0355     // supported version of Cantor.
0356     auto* action = m_part->action("file_export_pdf");
0357     if (action) {
0358         action->trigger();
0359         return true;
0360     } else
0361         return false;
0362 }
0363 
0364 bool CantorWorksheet::printView() {
0365     m_part->action("file_print")->trigger();
0366     return true;
0367 }
0368 
0369 bool CantorWorksheet::printPreview() const {
0370     m_part->action("file_print_preview")->trigger();
0371     return true;
0372 }
0373 
0374 void CantorWorksheet::evaluate() {
0375     m_part->action("evaluate_worksheet")->trigger();
0376 }
0377 
0378 void CantorWorksheet::restart() {
0379     m_part->action("restart_backend")->trigger();
0380 }
0381 
0382 // ##############################################################################
0383 // ##################  Serialization/Deserialization  ###########################
0384 // ##############################################################################
0385 
0386 //! Save as XML
0387 void CantorWorksheet::save(QXmlStreamWriter* writer) const {
0388     writer->writeStartElement(QStringLiteral("cantorWorksheet"));
0389     writeBasicAttributes(writer);
0390     writeCommentElement(writer);
0391 
0392     // general
0393     writer->writeStartElement(QStringLiteral("general"));
0394     writer->writeAttribute(QStringLiteral("backend_name"), m_backendName);
0395     // TODO: save worksheet settings
0396     writer->writeEndElement();
0397 
0398     // save the content of Cantor's worksheet
0399     QByteArray content = m_worksheetAccess->saveWorksheetToByteArray();
0400     writer->writeStartElement(QStringLiteral("worksheet"));
0401     writer->writeAttribute(QStringLiteral("content"), QLatin1String(content.toBase64()));
0402     writer->writeEndElement();
0403 
0404     // save columns(variables)
0405     for (auto* col : children<Column>(ChildIndexFlag::IncludeHidden))
0406         col->save(writer);
0407 
0408     writer->writeEndElement(); // close "cantorWorksheet" section
0409 }
0410 
0411 //! Load from XML
0412 bool CantorWorksheet::load(XmlStreamReader* reader, bool preview) {
0413     // reset the status of the reader differentiating between
0414     //"failed because of the missing CAS" and "failed because of the broken XML"
0415     reader->setFailedCASMissing(false);
0416 
0417     if (!readBasicAttributes(reader))
0418         return false;
0419 
0420     QXmlStreamAttributes attribs;
0421     bool rc = false;
0422 
0423     while (!reader->atEnd()) {
0424         reader->readNext();
0425         if (reader->isEndElement() && reader->name() == QLatin1String("cantorWorksheet"))
0426             break;
0427 
0428         if (!reader->isStartElement())
0429             continue;
0430 
0431         if (reader->name() == QLatin1String("comment")) {
0432             if (!readCommentElement(reader))
0433                 return false;
0434         } else if (!preview && reader->name() == QLatin1String("general")) {
0435             attribs = reader->attributes();
0436 
0437             m_backendName = attribs.value(QStringLiteral("backend_name")).toString().trimmed();
0438             if (m_backendName.isEmpty())
0439                 reader->raiseMissingAttributeWarning(QStringLiteral("backend_name"));
0440         } else if (!preview && reader->name() == QLatin1String("worksheet")) {
0441             attribs = reader->attributes();
0442 
0443             QString str = attribs.value(QStringLiteral("content")).toString().trimmed();
0444             if (str.isEmpty())
0445                 reader->raiseMissingAttributeWarning(QStringLiteral("content"));
0446 
0447             QByteArray content = QByteArray::fromBase64(str.toLatin1());
0448             rc = init(&content);
0449             if (!rc) {
0450                 reader->raiseMissingCASWarning(m_backendName);
0451 
0452                 // failed to load this object because of the missing CAS plugin
0453                 // and not because of the broken project XML. Set this flag to
0454                 // handle this case correctly.
0455                 // TODO: we also can fail in the limit in cases where Cantor's content is broken
0456                 // and not because of the missing CAS plugin. This also needs to be treated accrodingly...
0457                 reader->setFailedCASMissing(true);
0458                 return false;
0459             }
0460         } else if (!preview && reader->name() == QLatin1String("column")) {
0461             Column* column = new Column(QString());
0462             column->setUndoAware(false);
0463             if (!column->load(reader, preview)) {
0464                 delete column;
0465                 return false;
0466             }
0467             column->setFixed(true);
0468             addChild(column);
0469         } else { // unknown element
0470             reader->raiseUnknownElementWarning();
0471             if (!reader->skipToEndElement())
0472                 return false;
0473         }
0474     }
0475 
0476     return true;
0477 }