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 }