File indexing completed on 2024-05-12 15:27:51

0001 /***************************************************************************
0002     File                 : ImportProjectDialog.cpp
0003     Project              : LabPlot
0004     Description          : import project dialog
0005     --------------------------------------------------------------------
0006     Copyright            : (C) 2017-2021 Alexander Semke (alexander.semke@web.de)
0007 
0008  ***************************************************************************/
0009 
0010 /***************************************************************************
0011  *                                                                         *
0012  *  This program is free software; you can redistribute it and/or modify   *
0013  *  it under the terms of the GNU General Public License as published by   *
0014  *  the Free Software Foundation; either version 2 of the License, or      *
0015  *  (at your option) any later version.                                    *
0016  *                                                                         *
0017  *  This program is distributed in the hope that it will be useful,        *
0018  *  but WITHOUT ANY WARRANTY; without even the implied warranty of         *
0019  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          *
0020  *  GNU General Public License for more details.                           *
0021  *                                                                         *
0022  *   You should have received a copy of the GNU General Public License     *
0023  *   along with this program; if not, write to the Free Software           *
0024  *   Foundation, Inc., 51 Franklin Street, Fifth Floor,                    *
0025  *   Boston, MA  02110-1301  USA                                           *
0026  *                                                                         *
0027  ***************************************************************************/
0028 
0029 #include "ImportProjectDialog.h"
0030 #include "backend/core/AspectTreeModel.h"
0031 #include "backend/core/Project.h"
0032 #include "backend/datasources/projects/LabPlotProjectParser.h"
0033 #ifdef HAVE_LIBORIGIN
0034 #include "backend/datasources/projects/OriginProjectParser.h"
0035 #endif
0036 #include "kdefrontend/GuiTools.h"
0037 #include "kdefrontend/MainWin.h"
0038 #include "commonfrontend/widgets/TreeViewComboBox.h"
0039 
0040 #include <QDialogButtonBox>
0041 #include <QDir>
0042 #include <QElapsedTimer>
0043 #include <QFileDialog>
0044 #include <QInputDialog>
0045 #include <QProgressBar>
0046 #include <QStatusBar>
0047 #include <QWindow>
0048 
0049 #include <KLocalizedString>
0050 #include <KMessageBox>
0051 #include <KSharedConfig>
0052 #include <KUrlComboBox>
0053 #include <KWindowConfig>
0054 
0055 /*!
0056     \class ImportProjectDialog
0057     \brief Dialog for importing project files.
0058 
0059     \ingroup kdefrontend
0060  */
0061 ImportProjectDialog::ImportProjectDialog(MainWin* parent, ProjectType type) : QDialog(parent),
0062     m_mainWin(parent),
0063     m_projectType(type),
0064     m_aspectTreeModel(new AspectTreeModel(parent->project())) {
0065 
0066     auto* vLayout = new QVBoxLayout(this);
0067 
0068     //main widget
0069     QWidget* mainWidget = new QWidget(this);
0070     ui.setupUi(mainWidget);
0071     ui.chbUnusedObjects->hide();
0072 
0073     m_cbFileName = new KUrlComboBox(KUrlComboBox::Mode::Files, this);
0074     m_cbFileName->setMaxItems(7);
0075     auto* l = dynamic_cast<QHBoxLayout*>(ui.gbProject->layout());
0076     if (l)
0077         l->insertWidget(1, m_cbFileName);
0078 
0079     vLayout->addWidget(mainWidget);
0080 
0081     ui.tvPreview->setAnimated(true);
0082     ui.tvPreview->setAlternatingRowColors(true);
0083     ui.tvPreview->setSelectionBehavior(QAbstractItemView::SelectRows);
0084     ui.tvPreview->setSelectionMode(QAbstractItemView::ExtendedSelection);
0085     ui.tvPreview->setUniformRowHeights(true);
0086 
0087     ui.bOpen->setIcon( QIcon::fromTheme("document-open") );
0088 
0089     m_cbAddTo = new TreeViewComboBox(ui.gbImportTo);
0090     m_cbAddTo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
0091     ui.gbImportTo->layout()->addWidget(m_cbAddTo);
0092 
0093     QList<AspectType> list{AspectType::Folder};
0094     m_cbAddTo->setTopLevelClasses(list);
0095     m_aspectTreeModel->setSelectableAspects(list);
0096     m_cbAddTo->setModel(m_aspectTreeModel);
0097 
0098     m_bNewFolder = new QPushButton(ui.gbImportTo);
0099     m_bNewFolder->setIcon(QIcon::fromTheme("list-add"));
0100     m_bNewFolder->setToolTip(i18n("Add new folder"));
0101     ui.gbImportTo->layout()->addWidget(m_bNewFolder);
0102 
0103     //dialog buttons
0104     m_buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
0105     vLayout->addWidget(m_buttonBox);
0106 
0107     //ok-button is only enabled if some project objects were selected (s.a. ImportProjectDialog::selectionChanged())
0108     m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
0109 
0110     //Signals/Slots
0111     connect(m_cbFileName, &KUrlComboBox::urlActivated,
0112             this, [=](const QUrl &url){fileNameChanged(url.path());});
0113     connect(ui.bOpen, &QPushButton::clicked, this, &ImportProjectDialog::selectFile);
0114     connect(m_bNewFolder, &QPushButton::clicked, this, &ImportProjectDialog::newFolder);
0115     connect(ui.chbUnusedObjects, &QCheckBox::stateChanged, this, &ImportProjectDialog::refreshPreview);
0116     connect(m_buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
0117     connect(m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
0118 
0119     QString title;
0120     switch (m_projectType) {
0121     case ProjectType::LabPlot:
0122         m_projectParser = new LabPlotProjectParser();
0123         title = i18nc("@title:window", "Import LabPlot Project");
0124         break;
0125     case ProjectType::Origin:
0126 #ifdef HAVE_LIBORIGIN
0127         m_projectParser = new OriginProjectParser();
0128         title = i18nc("@title:window", "Import Origin Project");
0129 #endif
0130         break;
0131     }
0132 
0133     //dialog title and icon
0134     setWindowTitle(title);
0135     setWindowIcon(QIcon::fromTheme("document-import"));
0136 
0137     //"What's this?" texts
0138     QString info = i18n("Specify the file where the project content has to be imported from.");
0139     m_cbFileName->setWhatsThis(info);
0140 
0141     info = i18n("Select one or several objects to be imported into the current project.\n"
0142                 "Note, all children of the selected objects as well as all the dependent objects will be automatically selected.\n"
0143                 "To import the whole project, select the top-level project node."
0144                );
0145     ui.tvPreview->setWhatsThis(info);
0146 
0147     info = i18n("Specify the target folder in the current project where the selected objects have to be imported into.");
0148     m_cbAddTo->setWhatsThis(info);
0149 
0150     //restore saved settings if available
0151     create(); // ensure there's a window created
0152     KConfigGroup conf(KSharedConfig::openConfig(), "ImportProjectDialog");
0153     if (conf.exists()) {
0154         KWindowConfig::restoreWindowSize(windowHandle(), conf);
0155         resize(windowHandle()->size()); // workaround for QTBUG-40584
0156     } else
0157         resize(QSize(300, 0).expandedTo(minimumSize()));
0158 
0159     QString file;
0160     QString files;
0161     switch (m_projectType) {
0162     case ProjectType::LabPlot:
0163         file = QLatin1String("LastImportedLabPlotProject");
0164         files = QLatin1String("LastImportedLabPlotProjects");
0165         break;
0166     case ProjectType::Origin:
0167         file = QLatin1String("LastImportedOriginProject");
0168         files = QLatin1String("LastImportedOriginProjects");
0169         break;
0170     }
0171 
0172     QApplication::processEvents(QEventLoop::AllEvents, 100);
0173     m_cbFileName->setUrl(QUrl(conf.readEntry(file, "")));
0174     QStringList urls = m_cbFileName->urls();
0175     urls.append(conf.readXdgListEntry(files));
0176     m_cbFileName->setUrls(urls);
0177     fileNameChanged(m_cbFileName->currentText());
0178 }
0179 
0180 ImportProjectDialog::~ImportProjectDialog() {
0181     //save current settings
0182     KConfigGroup conf(KSharedConfig::openConfig(), "ImportProjectDialog");
0183     KWindowConfig::saveWindowSize(windowHandle(), conf);
0184 
0185     QString file;
0186     QString files;
0187     switch (m_projectType) {
0188     case ProjectType::LabPlot:
0189         file = QLatin1String("LastImportedLabPlotProject");
0190         files = QLatin1String("LastImportedLabPlotProjects");
0191         break;
0192     case ProjectType::Origin:
0193         file = QLatin1String("LastImportedOriginProject");
0194         files = QLatin1String("LastImportedOriginProjects");
0195         break;
0196     }
0197 
0198     conf.writeEntry(file, m_cbFileName->currentText());
0199     conf.writeXdgListEntry(files, m_cbFileName->urls());
0200 }
0201 
0202 void ImportProjectDialog::setCurrentFolder(const Folder* folder) {
0203     m_cbAddTo->setCurrentModelIndex(m_aspectTreeModel->modelIndexOfAspect(folder));
0204 }
0205 
0206 void ImportProjectDialog::importTo(QStatusBar* statusBar) const {
0207     DEBUG("ImportProjectDialog::importTo()");
0208 
0209     //determine the selected objects, convert the model indexes to string pathes
0210     const QModelIndexList& indexes = ui.tvPreview->selectionModel()->selectedIndexes();
0211     QStringList selectedPathes;
0212     for (int i = 0; i < indexes.size()/4; ++i) {
0213         QModelIndex index = indexes.at(i*4);
0214         const auto* aspect = static_cast<const AbstractAspect*>(index.internalPointer());
0215 
0216         //path of the current aspect and the pathes of all aspects it depends on
0217         selectedPathes << aspect->path();
0218         QDEBUG(" aspect path: " << aspect->path());
0219         for (const auto* depAspect : aspect->dependsOn())
0220             selectedPathes << depAspect->path();
0221     }
0222     selectedPathes.removeDuplicates();
0223 
0224     Folder* targetFolder = static_cast<Folder*>(m_cbAddTo->currentModelIndex().internalPointer());
0225 
0226     //check whether the selected pathes already exist in the target folder and warn the user
0227     const QString& targetFolderPath = targetFolder->path();
0228     const Project* targetProject = targetFolder->project();
0229     QStringList targetAllPathes;
0230     for (const auto* aspect : targetProject->children<AbstractAspect>(AbstractAspect::ChildIndexFlag::Recursive)) {
0231         if (aspect && !dynamic_cast<const Folder*>(aspect))
0232             targetAllPathes << aspect->path();
0233     }
0234 
0235     QStringList existingPathes;
0236     for (const auto& path : selectedPathes) {
0237         const QString& newPath = targetFolderPath + path.right(path.length() - path.indexOf('/'));
0238         if (targetAllPathes.indexOf(newPath) != -1)
0239             existingPathes << path;
0240     }
0241 
0242     QDEBUG("project objects to be imported: " << selectedPathes);
0243     QDEBUG("all already available project objects: " << targetAllPathes);
0244     QDEBUG("already available project objects to be overwritten: " << existingPathes);
0245 
0246     if (!existingPathes.isEmpty()) {
0247         QString msg = i18np("The object listed below already exists in target folder and will be overwritten:",
0248                             "The objects listed below already exist in target folder and will be overwritten:",
0249                             existingPathes.size());
0250         msg += '\n';
0251         for (const auto& path : existingPathes)
0252             msg += '\n' + path.right(path.length() - path.indexOf('/') - 1); //strip away the name of the root folder "Project"
0253         msg += "\n\n" + i18n("Do you want to proceed?");
0254 
0255         const int rc = KMessageBox::warningYesNo(nullptr, msg, i18n("Override existing objects?"));
0256         if (rc == KMessageBox::No)
0257             return;
0258     }
0259 
0260     //show a progress bar in the status bar
0261     auto* progressBar = new QProgressBar();
0262     progressBar->setMinimum(0);
0263     progressBar->setMaximum(100);
0264 
0265     statusBar->clearMessage();
0266     statusBar->addWidget(progressBar, 1);
0267 
0268     QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
0269     QApplication::processEvents(QEventLoop::AllEvents, 100);
0270 
0271     //import the selected project objects into the specified folder
0272     QElapsedTimer timer;
0273     timer.start();
0274     connect(m_projectParser, &ProjectParser::completed, progressBar, &QProgressBar::setValue);
0275 
0276 #ifdef HAVE_LIBORIGIN
0277     if (m_projectType == ProjectType::Origin && ui.chbUnusedObjects->isVisible() && ui.chbUnusedObjects->isChecked())
0278         reinterpret_cast<OriginProjectParser*>(m_projectParser)->setImportUnusedObjects(true);
0279 #endif
0280 
0281     m_projectParser->importTo(targetFolder, selectedPathes);
0282     statusBar->showMessage( i18n("Project data imported in %1 seconds.", (float)timer.elapsed()/1000) );
0283 
0284     QApplication::restoreOverrideCursor();
0285     statusBar->removeWidget(progressBar);
0286 }
0287 
0288 /*!
0289  * show the content of the project in the tree view
0290  */
0291 void ImportProjectDialog::refreshPreview() {
0292     const QString& project = m_cbFileName->currentText();
0293     m_projectParser->setProjectFileName(project);
0294 
0295 #ifdef HAVE_LIBORIGIN
0296     if (m_projectType == ProjectType::Origin) {
0297         auto* originParser = reinterpret_cast<OriginProjectParser*>(m_projectParser);
0298         if (originParser->hasUnusedObjects())
0299             ui.chbUnusedObjects->show();
0300         else
0301             ui.chbUnusedObjects->hide();
0302 
0303         originParser->setImportUnusedObjects(ui.chbUnusedObjects->isVisible() && ui.chbUnusedObjects->isChecked());
0304     }
0305 #endif
0306 
0307     ui.tvPreview->setModel(m_projectParser->model());
0308 
0309     connect(ui.tvPreview->selectionModel(), &QItemSelectionModel::selectionChanged,
0310         this, &ImportProjectDialog::selectionChanged);
0311 
0312     //show top-level containers only
0313     if (ui.tvPreview->model()) {
0314         QModelIndex root = ui.tvPreview->model()->index(0,0);
0315         showTopLevelOnly(root);
0316     }
0317 
0318     //select the first top-level node and
0319     //expand the tree to show all available top-level objects and adjust the header sizes
0320     ui.tvPreview->setCurrentIndex(ui.tvPreview->model()->index(0,0));
0321     ui.tvPreview->expandAll();
0322     ui.tvPreview->header()->resizeSections(QHeaderView::ResizeToContents);
0323 }
0324 
0325 /*!
0326     Hides the non-toplevel items of the model used in the tree view.
0327 */
0328 void ImportProjectDialog::showTopLevelOnly(const QModelIndex& index) {
0329     int rows = index.model()->rowCount(index);
0330     for (int i = 0; i < rows; ++i) {
0331         QModelIndex child = index.model()->index(i, 0, index);
0332         showTopLevelOnly(child);
0333         const auto* aspect = static_cast<const AbstractAspect*>(child.internalPointer());
0334         ui.tvPreview->setRowHidden(i, index, !isTopLevel(aspect));
0335     }
0336 }
0337 
0338 /*!
0339     checks whether \c aspect is one of the allowed top level types
0340 */
0341 bool ImportProjectDialog::isTopLevel(const AbstractAspect* aspect) const {
0342     foreach (AspectType type, m_projectParser->topLevelClasses()) {
0343         if (aspect->inherits(type))
0344             return true;
0345     }
0346     return false;
0347 }
0348 
0349 //##############################################################################
0350 //#################################  SLOTS  ####################################
0351 //##############################################################################
0352 void ImportProjectDialog::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) {
0353     Q_UNUSED(deselected);
0354 
0355     //determine the dependent objects and select/deselect them too
0356     const QModelIndexList& indexes = selected.indexes();
0357     if (indexes.isEmpty())
0358         return;
0359 
0360     //for the just selected aspect, determine all the objects it depends on and select them, too
0361     //TODO: we need a better "selection", maybe with tri-state check boxes in the tree view
0362     const auto* aspect = static_cast<const AbstractAspect*>(indexes.at(0).internalPointer());
0363     const QVector<AbstractAspect*> aspects = aspect->dependsOn();
0364 
0365     const auto* model = reinterpret_cast<AspectTreeModel*>(ui.tvPreview->model());
0366     for (const auto* aspect : aspects) {
0367         QModelIndex index = model->modelIndexOfAspect(aspect, 0);
0368         ui.tvPreview->selectionModel()->select(index, QItemSelectionModel::Select | QItemSelectionModel::Rows);
0369     }
0370 
0371     //Ok-button is only enabled if some project objects were selected
0372     bool enable = (selected.indexes().size() != 0);
0373     m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(enable);
0374     if (enable)
0375         m_buttonBox->button(QDialogButtonBox::Ok)->setToolTip(i18n("Close the dialog and import the selected objects."));
0376     else
0377         m_buttonBox->button(QDialogButtonBox::Ok)->setToolTip(i18n("Select object(s) to be imported."));
0378 }
0379 
0380 /*!
0381     opens a file dialog and lets the user select the project file.
0382 */
0383 void ImportProjectDialog::selectFile() {
0384     KConfigGroup conf(KSharedConfig::openConfig(), "ImportProjectDialog");
0385 
0386     QString title;
0387     QString lastDir;
0388     QString supportedFormats;
0389     QString lastDirConfEntryName;
0390     switch (m_projectType) {
0391     case ProjectType::LabPlot:
0392         title = i18nc("@title:window", "Open LabPlot Project");
0393         lastDirConfEntryName = QLatin1String("LastImportLabPlotProjectDir");
0394         supportedFormats = i18n("LabPlot Projects (%1)", Project::supportedExtensions());
0395         break;
0396     case ProjectType::Origin:
0397 #ifdef HAVE_LIBORIGIN
0398         title = i18nc("@title:window", "Open Origin Project");
0399         lastDirConfEntryName = QLatin1String("LastImportOriginProjecttDir");
0400         supportedFormats = i18n("Origin Projects (%1)", OriginProjectParser::supportedExtensions());
0401 #endif
0402         break;
0403     }
0404 
0405     lastDir = conf.readEntry(lastDirConfEntryName, "");
0406     QString path = QFileDialog::getOpenFileName(this, title, lastDir, supportedFormats);
0407     if (path.isEmpty())
0408         return; //cancel was clicked in the file-dialog
0409 
0410     int pos = path.lastIndexOf(QLatin1String("/"));
0411     if (pos != -1) {
0412         QString newDir = path.left(pos);
0413         if (newDir != lastDir)
0414             conf.writeEntry(lastDirConfEntryName, newDir);
0415     }
0416 
0417     QStringList urls = m_cbFileName->urls();
0418     urls.insert(0, QUrl::fromLocalFile(path).url());
0419     m_cbFileName->setUrls(urls);
0420     m_cbFileName->setCurrentText(urls.first());
0421     fileNameChanged(path); // why do I have to call this function separately
0422 
0423     refreshPreview();
0424 }
0425 
0426 void ImportProjectDialog::fileNameChanged(const QString& name) {
0427     QString fileName{name};
0428 
0429     // make relative path
0430 #ifdef HAVE_WINDOWS
0431     if ( !fileName.isEmpty() && fileName.at(1) != QLatin1String(":"))
0432 #else
0433     if ( !fileName.isEmpty() && fileName.at(0) != QLatin1String("/"))
0434 #endif
0435         fileName = QDir::homePath() + QLatin1String("/") + fileName;
0436 
0437     bool fileExists = QFile::exists(fileName);
0438     if (!fileExists) {
0439         //file doesn't exist -> delete the content preview that is still potentially
0440         //available from the previously selected file
0441         ui.tvPreview->setModel(nullptr);
0442         m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
0443         return;
0444     }
0445 
0446     refreshPreview();
0447 }
0448 
0449 void ImportProjectDialog::newFolder() {
0450     const QString& path = m_cbFileName->currentText();
0451     QString name = path.right( path.length() - path.lastIndexOf(QLatin1String("/"))-1 );
0452 
0453     bool ok;
0454     auto* dlg = new QInputDialog(this);
0455     name = dlg->getText(this, i18n("Add new folder"), i18n("Folder name:"), QLineEdit::Normal, name, &ok);
0456     if (ok) {
0457         auto* folder = new Folder(name);
0458         m_mainWin->addAspectToProject(folder);
0459         m_cbAddTo->setCurrentModelIndex(m_mainWin->model()->modelIndexOfAspect(folder));
0460     }
0461 
0462     delete dlg;
0463 }