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 }