File indexing completed on 2024-05-12 07:41:27

0001 /*
0002     File                 : PlotTemplateDialog.cpp
0003     Project              : LabPlot
0004     Description          : dialog to load user-defined plot definitions
0005     --------------------------------------------------------------------
0006     SPDX-FileCopyrightText: 2022 Martin Marmsoler <martin.marmsoler@gmail.com>
0007     SPDX-FileCopyrightText: 2022 Alexander Semke <alexander.semke@web.de>
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 
0011 #include "PlotTemplateDialog.h"
0012 #include "ui_PlotTemplateDialog.h"
0013 
0014 #include "backend/core/Project.h"
0015 #include "backend/core/Settings.h"
0016 #include "backend/lib/XmlStreamReader.h"
0017 #include "backend/worksheet/Worksheet.h"
0018 #include "backend/worksheet/plots/cartesian/XYEquationCurve.h"
0019 
0020 #include <QDirIterator>
0021 #include <QFileDialog>
0022 #include <QFileInfo>
0023 #include <QWindow>
0024 
0025 #include <KConfig>
0026 #include <KConfigGroup>
0027 #include <KLocalizedString>
0028 #include <KWindowConfig>
0029 
0030 namespace {
0031 const QLatin1String lastDirConfigEntry = QLatin1String("LastPlotTemplateDir");
0032 }
0033 
0034 const QString PlotTemplateDialog::format = QLatin1String(".labplot_template");
0035 
0036 PlotTemplateDialog::PlotTemplateDialog(QWidget* parent)
0037     : QDialog(parent)
0038     , ui(new Ui::PlotTemplateDialog) {
0039     ui->setupUi(this);
0040 
0041     setWindowTitle(i18nc("@title:window", "Plot Area Templates"));
0042     setWindowIcon(QIcon::fromTheme(QLatin1String("document-new-from-template")));
0043 
0044     ui->cbLocation->addItem(i18n("Default"));
0045     ui->cbLocation->addItem(i18n("Custom Folder"));
0046     ui->pbCustomFolder->setIcon(QIcon::fromTheme(QLatin1String("document-open-folder")));
0047 
0048     QString info = i18n("Location of plot area templates");
0049     ui->lLocation->setToolTip(info);
0050     ui->cbLocation->setToolTip(info);
0051 
0052     info = i18n("Custom folder for the location of plot area templates");
0053     ui->lCustomFolder->setToolTip(info);
0054     ui->leCustomFolder->setToolTip(info);
0055 
0056     ui->pbCustomFolder->setToolTip(i18n("Open the folder for the location of plot area templates"));
0057 
0058     KConfigGroup conf = Settings::group(QLatin1String("PlotTemplateDialog"));
0059 
0060     m_project = new Project;
0061 
0062     m_worksheet = new Worksheet(QString());
0063     m_worksheet->setInteractive(false);
0064     m_worksheet->setUseViewSize(true);
0065     m_worksheet->setLayoutTopMargin(0.);
0066     m_worksheet->setLayoutBottomMargin(0.);
0067     m_worksheet->setLayoutLeftMargin(0.);
0068     m_worksheet->setLayoutRightMargin(0.);
0069     m_worksheetView = m_worksheet->view();
0070     m_project->addChild(m_worksheet);
0071     ui->lPreview->addWidget(m_worksheetView);
0072     m_worksheetView->hide();
0073 
0074     mTemplateListModelDefault = new TemplateListModel(defaultTemplateInstallPath(), this);
0075     mTemplateListModelCustom = new TemplateListModel(conf.readEntry(lastDirConfigEntry, defaultTemplateInstallPath()), this);
0076     ui->leCustomFolder->setText(mTemplateListModelCustom->searchPath());
0077 
0078     connect(ui->pbCustomFolder, &QPushButton::pressed, this, &PlotTemplateDialog::chooseTemplateSearchPath);
0079     connect(ui->leCustomFolder, &QLineEdit::textChanged, this, &PlotTemplateDialog::customTemplatePathChanged);
0080     connect(ui->lvInstalledTemplates->selectionModel(), &QItemSelectionModel::currentChanged, this, &PlotTemplateDialog::listViewTemplateChanged);
0081     connect(ui->cbLocation, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &PlotTemplateDialog::changePreviewSource);
0082 
0083     // restore saved settings if available
0084     create(); // ensure there's a window created
0085     if (conf.exists()) {
0086         KWindowConfig::restoreWindowSize(windowHandle(), conf);
0087         resize(windowHandle()->size()); // workaround for QTBUG-40584
0088     } else
0089         resize(QSize(0, 0).expandedTo(minimumSize()));
0090 
0091     ui->cbLocation->setCurrentIndex(conf.readEntry(QLatin1String("Location"), 0));
0092     if (ui->cbLocation->currentIndex() == 0) // if index=0 no signal is emitted above, call this slot directly
0093         changePreviewSource(0);
0094 }
0095 
0096 PlotTemplateDialog::~PlotTemplateDialog() {
0097     // save current settings
0098     KConfigGroup conf = Settings::group(QLatin1String("PlotTemplateDialog"));
0099     KWindowConfig::saveWindowSize(windowHandle(), conf);
0100     conf.writeEntry(QLatin1String("Location"), ui->cbLocation->currentIndex());
0101 
0102     delete ui;
0103     delete m_project;
0104 }
0105 
0106 void PlotTemplateDialog::customTemplatePathChanged(const QString& path) {
0107     KConfigGroup conf = Settings::group(QLatin1String("PlotTemplateDialog"));
0108     if (!path.isEmpty())
0109         conf.writeEntry(lastDirConfigEntry, path);
0110 
0111     mTemplateListModelCustom->setSearchPath(path);
0112     ui->cbLocation->setToolTip(path); // custom path index is selected
0113     auto index = mTemplateListModelCustom->index(0, 0);
0114     ui->lvInstalledTemplates->setCurrentIndex(index);
0115 
0116     // Because if the modelindex is invalid showPreview() will not be
0117     // called because currentIndex will not trigger indexChange
0118     if (!index.isValid())
0119         showPreview();
0120 }
0121 
0122 QString PlotTemplateDialog::defaultTemplateInstallPath() {
0123     // folder where config files will be stored in object specific sub-folders:
0124     // Linux    - ~/.local/share/labplot2/plot_templates/
0125     // Mac      - /Library/Application Support/labplot2
0126     // Windows  - C:/Users/<USER>/AppData/Roaming/labplot2/plot_templates/
0127     return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1String("/plot_templates/");
0128 }
0129 
0130 void PlotTemplateDialog::chooseTemplateSearchPath() {
0131     // Lock lock(mLoading); // No lock needed
0132     KConfigGroup conf = Settings::group(QLatin1String("PlotTemplateDialog"));
0133     const QString& dir = conf.readEntry(lastDirConfigEntry, QStandardPaths::writableLocation(QStandardPaths::HomeLocation));
0134 
0135     const QString& path = QFileDialog::getExistingDirectory(nullptr, i18nc("@title:window", "Select template search path"), dir);
0136 
0137     ui->leCustomFolder->setText(path);
0138 }
0139 
0140 CartesianPlot* PlotTemplateDialog::generatePlot() {
0141     const QString path = templatePath();
0142     if (path.isEmpty()) {
0143         updateErrorMessage(i18n("No templates found."));
0144         return nullptr;
0145     }
0146 
0147     QFile file(path);
0148     if (!file.exists()) {
0149         updateErrorMessage(i18n("File does not exist."));
0150         return nullptr;
0151     }
0152 
0153     if (!file.open(QIODevice::OpenModeFlag::ReadOnly)) {
0154         updateErrorMessage(i18n("Unable to read the file"));
0155         return nullptr;
0156     }
0157 
0158     XmlStreamReader reader(&file);
0159 
0160     while (!(reader.isStartDocument() || reader.atEnd()))
0161         reader.readNext();
0162 
0163     if (reader.atEnd()) {
0164         DEBUG("XML error: No start document token found");
0165         updateErrorMessage(i18n("Failed to load the selected plot template"));
0166         return nullptr;
0167     }
0168 
0169     reader.readNext();
0170     if (!reader.isDTD()) {
0171         DEBUG("XML error: No DTD token found");
0172         updateErrorMessage(i18n("Failed to load the selected plot template"));
0173         return nullptr;
0174     }
0175 
0176     reader.readNext();
0177     if (!reader.isStartElement() || reader.name() != QLatin1String("PlotTemplate")) {
0178         DEBUG("XML error: No PlotTemplate found");
0179         updateErrorMessage(i18n("Failed to load the selected plot template"));
0180         return nullptr;
0181     }
0182 
0183     bool ok;
0184     int xmlVersion = reader.readAttributeInt(QLatin1String("xmlVersion"), &ok);
0185     if (!ok) {
0186         DEBUG("XML error: xmlVersion found");
0187         updateErrorMessage(i18n("Failed to load the selected plot template"));
0188         return nullptr;
0189     }
0190     Project::setXmlVersion(xmlVersion);
0191     reader.readNext();
0192 
0193     while (!((reader.isStartElement() && reader.name() == QLatin1String("cartesianPlot")) || reader.atEnd()))
0194         reader.readNext();
0195 
0196     if (reader.atEnd()) {
0197         updateErrorMessage(i18n("XML error: No cartesianPlot found"));
0198         updateErrorMessage(i18n("Failed to load the selected plot template"));
0199         return nullptr;
0200     }
0201 
0202     auto* plot = new CartesianPlot(QLatin1String("plot"));
0203     plot->setIsLoading(true);
0204     if (!plot->load(&reader, false)) {
0205         DEBUG("Failed to load the selected plot template" + reader.errorString().toStdString());
0206         updateErrorMessage(i18n("Failed to load the selected plot template"));
0207         delete plot;
0208         return nullptr;
0209     }
0210     plot->setIsLoading(false);
0211 
0212     const auto& children = plot->children<WorksheetElement>(AbstractAspect::ChildIndexFlag::Recursive | AbstractAspect::ChildIndexFlag::IncludeHidden);
0213     for (auto* child : children)
0214         child->setIsLoading(false);
0215 
0216     for (auto* equationCurve : plot->children<XYEquationCurve>())
0217         static_cast<XYEquationCurve*>(equationCurve)->recalculate();
0218 
0219     plot->retransform();
0220     return plot;
0221 }
0222 
0223 void PlotTemplateDialog::showPreview() {
0224     for (auto* plot : m_worksheet->children<CartesianPlot>())
0225         m_worksheet->removeChild(plot);
0226 
0227     auto* plot = generatePlot();
0228     if (plot) {
0229         m_worksheet->addChild(plot);
0230         updateErrorMessage(QLatin1String("")); // hide error text edit
0231     }
0232 }
0233 
0234 void PlotTemplateDialog::updateErrorMessage(const QString& message) {
0235     if (message.isEmpty()) {
0236         ui->teMessage->hide();
0237         m_worksheetView->show();
0238         auto* button = ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok);
0239         assert(button);
0240         button->setEnabled(true);
0241         return;
0242     }
0243 
0244     m_worksheetView->hide();
0245     ui->teMessage->setText(message);
0246     ui->teMessage->show();
0247     auto* button = ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok);
0248     assert(button);
0249     button->setEnabled(false);
0250 }
0251 
0252 QString PlotTemplateDialog::templatePath() const {
0253     return ui->lvInstalledTemplates->model()->data(ui->lvInstalledTemplates->currentIndex(), TemplateListModel::Roles::FilePathRole).toString();
0254 }
0255 
0256 void PlotTemplateDialog::listViewTemplateChanged(const QModelIndex& current, const QModelIndex& previous) {
0257     Q_UNUSED(current);
0258     Q_UNUSED(previous);
0259     if (mLoading)
0260         return;
0261 
0262     Lock lock(mLoading);
0263     showPreview();
0264 }
0265 
0266 void PlotTemplateDialog::changePreviewSource(int row) {
0267     if (mLoading)
0268         return;
0269 
0270     TemplateListModel* model = mTemplateListModelDefault;
0271     if (row == 1)
0272         model = mTemplateListModelCustom;
0273 
0274     ui->cbLocation->setToolTip(model->searchPath());
0275 
0276     ui->lCustomFolder->setVisible(row == 1);
0277     ui->leCustomFolder->setVisible(row == 1);
0278     ui->pbCustomFolder->setVisible(row == 1);
0279     ui->lvInstalledTemplates->setModel(model);
0280 
0281     // must be done every time the model changes
0282     connect(ui->lvInstalledTemplates->selectionModel(), &QItemSelectionModel::currentChanged, this, &PlotTemplateDialog::listViewTemplateChanged);
0283     ui->lvInstalledTemplates->setCurrentIndex(model->index(0, 0));
0284 
0285     if (!model->index(0, 0).isValid())
0286         showPreview();
0287 }
0288 
0289 // ##########################################################################################################
0290 //  Listmodel
0291 // ##########################################################################################################
0292 TemplateListModel::TemplateListModel(const QString& searchPath, QObject* parent)
0293     : QAbstractListModel(parent) {
0294     setSearchPath(searchPath);
0295 }
0296 
0297 void TemplateListModel::setSearchPath(const QString& searchPath) {
0298     beginResetModel();
0299     mSearchPath = searchPath;
0300     mFiles.clear();
0301     QStringList filter(QLatin1String("*") % PlotTemplateDialog::format);
0302     QDirIterator it(searchPath, filter, QDir::AllEntries | QDir::NoSymLinks | QDir::NoDotAndDotDot, QDirIterator::Subdirectories);
0303     QDir sPath(searchPath);
0304     while (it.hasNext()) {
0305         QFileInfo f(it.next());
0306         File file{f.absoluteFilePath(), sPath.relativeFilePath(f.absoluteFilePath()).split(PlotTemplateDialog::format).at(0)};
0307         mFiles << file;
0308     }
0309     endResetModel();
0310 }
0311 
0312 int TemplateListModel::rowCount(const QModelIndex& parent) const {
0313     Q_UNUSED(parent);
0314     return mFiles.count();
0315 }
0316 
0317 QVariant TemplateListModel::data(const QModelIndex& index, int role) const {
0318     const int row = index.row();
0319     if (!index.isValid() || row > mFiles.count() || row < 0)
0320         return QVariant();
0321 
0322     switch (role) {
0323     case FilenameRole: // fall through
0324     case Qt::ItemDataRole::DisplayRole:
0325         return mFiles.at(row).filename;
0326     case FilePathRole:
0327         return mFiles.at(row).path;
0328     }
0329 
0330     return QVariant();
0331 }