File indexing completed on 2024-04-28 04:37:48

0001 /*
0002     SPDX-FileCopyrightText: 2007 Alexander Dymo <adymo@kdevelop.org>
0003     SPDX-FileCopyrightText: 2011 Aleix Pol Gonzalez <aleixpol@kde.org>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "projectselectionpage.h"
0009 #include "debug.h"
0010 
0011 #include <QDir>
0012 #include <QFileDialog>
0013 
0014 #include <KConfig>
0015 #include <KConfigGroup>
0016 #include <KLineEdit>
0017 #include <KLocalizedString>
0018 #include <KMessageBox>
0019 #include <KMessageBox_KDevCompat>
0020 
0021 #include <interfaces/icore.h>
0022 #include <interfaces/iprojectcontroller.h>
0023 #include <language/codegen/templatepreviewicon.h>
0024 
0025 #include <util/scopeddialog.h>
0026 
0027 #include "ui_projectselectionpage.h"
0028 #include "projecttemplatesmodel.h"
0029 
0030 using namespace KDevelop;
0031 
0032 ProjectSelectionPage::ProjectSelectionPage(ProjectTemplatesModel *templatesModel, AppWizardDialog *wizardDialog)
0033     : AppWizardPageWidget(wizardDialog), m_templatesModel(templatesModel)
0034 {
0035     ui = new Ui::ProjectSelectionPage();
0036     ui->setupUi(this);
0037     ui->descriptionContent->setBackgroundRole(QPalette::Base);
0038     ui->descriptionContent->setForegroundRole(QPalette::Text);
0039 
0040     ui->locationUrl->setMode(KFile::Directory | KFile::ExistingOnly | KFile::LocalOnly );
0041     ui->locationUrl->setUrl(KDevelop::ICore::self()->projectController()->projectsBaseDirectory());
0042 
0043     ui->locationValidWidget->hide();
0044     ui->locationValidWidget->setMessageType(KMessageWidget::Error);
0045     ui->locationValidWidget->setCloseButtonVisible(false);
0046 
0047     connect( ui->locationUrl->lineEdit(), &KLineEdit::textEdited,
0048              this, &ProjectSelectionPage::urlEdited);
0049     connect( ui->locationUrl, &KUrlRequester::urlSelected,
0050              this, &ProjectSelectionPage::urlEdited);
0051     connect( ui->projectNameEdit, &QLineEdit::textEdited,
0052              this, &ProjectSelectionPage::nameChanged );
0053 
0054     ui->listView->setLevels(2);
0055     ui->listView->setHeaderLabels(QStringList{
0056         i18nc("@title:column", "Category"),
0057         i18nc("@title:column", "Project Type")
0058     });
0059     ui->listView->setModel(templatesModel);
0060     ui->listView->setLastLevelViewMode(MultiLevelListView::DirectChildren);
0061     connect (ui->listView, &MultiLevelListView::currentIndexChanged, this, &ProjectSelectionPage::typeChanged);
0062     typeChanged(ui->listView->currentIndex());
0063 
0064     connect( ui->templateType, QOverload<int>::of(&QComboBox::currentIndexChanged),
0065              this, &ProjectSelectionPage::templateChanged );
0066 
0067     auto* getMoreButton = new KNSWidgets::Button(i18nc("@action:button", "Get More Templates"),
0068                                                  QStringLiteral("kdevappwizard.knsrc"), ui->listView);
0069     connect(getMoreButton, &KNSWidgets::Button::dialogFinished, this,
0070             &ProjectSelectionPage::handleNewStuffDialogFinished);
0071     ui->listView->addWidget(0, getMoreButton);
0072 
0073     auto* loadButton = new QPushButton(ui->listView);
0074     loadButton->setText(i18nc("@action:button", "Load Template from File"));
0075     loadButton->setIcon(QIcon::fromTheme(QStringLiteral("application-x-archive")));
0076     connect (loadButton, &QPushButton::clicked, this, &ProjectSelectionPage::loadFileClicked);
0077     ui->listView->addWidget(0, loadButton);
0078 
0079     m_wizardDialog = wizardDialog;
0080 }
0081 
0082 void ProjectSelectionPage::nameChanged()
0083 {
0084     validateData();
0085     emit locationChanged( location() );
0086 }
0087 
0088 
0089 ProjectSelectionPage::~ProjectSelectionPage()
0090 {
0091     delete ui;
0092 }
0093 
0094 void ProjectSelectionPage::typeChanged(const QModelIndex& idx)
0095 {
0096     if (!idx.model())
0097     {
0098         qCDebug(PLUGIN_APPWIZARD) << "Index with no model";
0099         return;
0100     }
0101     int children = idx.model()->rowCount(idx);
0102     ui->templateType->setVisible(children);
0103     ui->templateType->setEnabled(children > 1);
0104     if (children) {
0105         ui->templateType->setModel(m_templatesModel);
0106         ui->templateType->setRootModelIndex(idx);
0107         ui->templateType->setCurrentIndex(0);
0108         itemChanged(idx.model()->index(0, 0, idx));
0109     } else {
0110         itemChanged(idx);
0111     }
0112 }
0113 
0114 void ProjectSelectionPage::templateChanged(int current)
0115 {
0116     QModelIndex idx=m_templatesModel->index(current, 0, ui->templateType->rootModelIndex());
0117     itemChanged(idx);
0118 }
0119 
0120 void ProjectSelectionPage::itemChanged( const QModelIndex& current)
0121 {
0122     TemplatePreviewIcon icon = current.data(KDevelop::TemplatesModel::PreviewIconRole).value<TemplatePreviewIcon>();
0123 
0124     QPixmap pixmap = icon.pixmap();
0125     ui->icon->setPixmap(pixmap);
0126     ui->icon->setFixedHeight(pixmap.height());
0127     // header name is either from this index directly or the parents if we show the combo box
0128     const QVariant headerData = ui->templateType->isVisible()
0129                                     ? current.parent().data()
0130                                     : current.data();
0131     ui->header->setText(QStringLiteral("<h1>%1</h1>").arg(headerData.toString().trimmed()));
0132     ui->description->setText(current.data(KDevelop::TemplatesModel::CommentRole).toString());
0133     validateData();
0134 
0135     ui->propertiesBox->setEnabled(true);
0136 }
0137 
0138 QString ProjectSelectionPage::selectedTemplate()
0139 {
0140     QStandardItem *item = currentItem();
0141     if (item)
0142         return item->data().toString();
0143     else
0144         return QString();
0145 }
0146 
0147 QUrl ProjectSelectionPage::location()
0148 {
0149     QUrl url = ui->locationUrl->url().adjusted(QUrl::StripTrailingSlash);
0150     url.setPath(url.path() + QLatin1Char('/') + QString::fromUtf8(encodedProjectName()));
0151     return url;
0152 }
0153 
0154 QString ProjectSelectionPage::projectName()
0155 {
0156     return ui->projectNameEdit->text();
0157 }
0158 
0159 void ProjectSelectionPage::urlEdited()
0160 {
0161     validateData();
0162     emit locationChanged( location() );
0163 }
0164 
0165 void ProjectSelectionPage::validateData()
0166 {
0167     QUrl url = ui->locationUrl->url();
0168     if( !url.isLocalFile() || url.isEmpty() )
0169     {
0170         ui->locationValidWidget->setText( i18n("Invalid location") );
0171         ui->locationValidWidget->animatedShow();
0172         emit invalid();
0173         return;
0174     }
0175 
0176     if (projectName().isEmpty()) {
0177         ui->locationValidWidget->setText( i18n("Empty project name") );
0178         ui->locationValidWidget->animatedShow();
0179         emit invalid();
0180         return;
0181     }
0182 
0183     if (!projectName().isEmpty()) {
0184         QString projectName = this->projectName();
0185         QString templatefile = m_wizardDialog->appInfo().appTemplate;
0186 
0187         // Read template file
0188         KConfig config(templatefile);
0189         KConfigGroup configgroup(&config, "General");
0190         QString pattern = configgroup.readEntry( "ValidProjectName" ,  "^[a-zA-Z][a-zA-Z0-9_-]+$" );
0191 
0192         // Validation
0193         int pos = 0;
0194         QRegExp regex( pattern );
0195         QRegExpValidator validator( regex );
0196         if( validator.validate(projectName, pos) == QValidator::Invalid )
0197         {
0198             ui->locationValidWidget->setText( i18n("Invalid project name") );
0199             ui->locationValidWidget->animatedShow();
0200             emit invalid();
0201             return;
0202         }
0203     }
0204 
0205     QDir tDir(url.toLocalFile());
0206     while (!tDir.exists() && !tDir.isRoot()) {
0207         if (!tDir.cdUp()) {
0208             break;
0209         }
0210     }
0211 
0212     if (tDir.exists())
0213     {
0214         QFileInfo tFileInfo(tDir.absolutePath());
0215         if (!tFileInfo.isWritable() || !tFileInfo.isExecutable())
0216         {
0217             ui->locationValidWidget->setText( i18n("Unable to create subdirectories, "
0218                                                   "missing permissions on: %1", tDir.absolutePath()) );
0219             ui->locationValidWidget->animatedShow();
0220             emit invalid();
0221             return;
0222         }
0223     }
0224 
0225     QStandardItem* item = currentItem();
0226     if( item && !item->hasChildren() )
0227     {
0228         ui->locationValidWidget->animatedHide();
0229         emit valid();
0230     } else
0231     {
0232         ui->locationValidWidget->setText( i18n("Invalid project template, please choose a leaf item") );
0233         ui->locationValidWidget->animatedShow();
0234         emit invalid();
0235         return;
0236     }
0237 
0238     // Check for non-empty target directory. Not an error, but need to display a warning.
0239     url.setPath(url.path() + QLatin1Char('/') + QString::fromUtf8(encodedProjectName()));
0240     QFileInfo fi( url.toLocalFile() );
0241     if( fi.exists() && fi.isDir() )
0242     {
0243         if( !QDir( fi.absoluteFilePath()).entryList( QDir::NoDotAndDotDot | QDir::AllEntries ).isEmpty() )
0244         {
0245             ui->locationValidWidget->setText( i18n("Path already exists and contains files. Open it as a project.") );
0246             ui->locationValidWidget->animatedShow();
0247             emit invalid();
0248             return;
0249         }
0250     }
0251 }
0252 
0253 QByteArray ProjectSelectionPage::encodedProjectName()
0254 {
0255     // : < > * ? / \ | " are invalid on windows
0256     QByteArray tEncodedName = projectName().toUtf8();
0257     for (int i = 0; i < tEncodedName.size(); ++i)
0258     {
0259         QChar tChar(QLatin1Char(tEncodedName.at(i)));
0260         if (tChar.isDigit() || tChar.isSpace() || tChar.isLetter() || tChar == QLatin1Char('%')) {
0261             continue;
0262         }
0263 
0264         QByteArray tReplace = QUrl::toPercentEncoding( tChar );
0265         tEncodedName.replace( tEncodedName.at( i ) ,tReplace );
0266         i =  i + tReplace.size() - 1;
0267     }
0268     return tEncodedName;
0269 }
0270 
0271 QStandardItem* ProjectSelectionPage::currentItem() const
0272 {
0273     QStandardItem* item = m_templatesModel->itemFromIndex( ui->listView->currentIndex() );
0274     if ( item && item->hasChildren() )
0275     {
0276         const int current = ui->templateType->currentIndex();
0277         const QModelIndex idx = m_templatesModel->index( current, 0, ui->templateType->rootModelIndex() );
0278         item = m_templatesModel->itemFromIndex(idx);
0279     }
0280     return item;
0281 }
0282 
0283 
0284 bool ProjectSelectionPage::shouldContinue()
0285 {
0286     QFileInfo fi(location().toLocalFile());
0287     if (fi.exists() && fi.isDir())
0288     {
0289         if (!QDir(fi.absoluteFilePath()).entryList(QDir::NoDotAndDotDot | QDir::AllEntries).isEmpty())
0290         {
0291             int res = KMessageBox::questionTwoActions(this,
0292                                                       i18n("The specified path already exists and contains files. "
0293                                                            "Are you sure you want to proceed?"),
0294                                                       {}, KStandardGuiItem::cont(), KStandardGuiItem::cancel());
0295             return res == KMessageBox::PrimaryAction;
0296         }
0297     }
0298     return true;
0299 }
0300 
0301 void ProjectSelectionPage::loadFileClicked()
0302 {
0303     const QStringList supportedMimeTypes {
0304         QStringLiteral("application/x-desktop"),
0305         QStringLiteral("application/x-bzip-compressed-tar"),
0306         QStringLiteral("application/zip")
0307     };
0308     ScopedDialog<QFileDialog> fileDialog(this, i18nc("@title:window", "Load Template from File"));
0309     fileDialog->setMimeTypeFilters(supportedMimeTypes);
0310     fileDialog->setFileMode(QFileDialog::ExistingFiles);
0311 
0312     if (!fileDialog->exec()) {
0313         return;
0314     }
0315 
0316     const auto& fileNames = fileDialog->selectedFiles();
0317     for (const auto& fileName : fileNames) {
0318         QString destination = m_templatesModel->loadTemplateFile(fileName);
0319         QModelIndexList indexes = m_templatesModel->templateIndexes(destination);
0320         if (indexes.size() > 2)
0321         {
0322             ui->listView->setCurrentIndex(indexes.at(1));
0323             ui->templateType->setCurrentIndex(indexes.at(2).row());
0324         }
0325     }
0326 }
0327 
0328 void ProjectSelectionPage::handleNewStuffDialogFinished(const QList<KNSCore::Entry>& changedEntries)
0329 {
0330     if (changedEntries.isEmpty()) {
0331         return;
0332     }
0333 
0334     m_templatesModel->refresh();
0335     bool updated = false;
0336 
0337     for (const auto& entry : changedEntries) {
0338         if (!entry.installedFiles().isEmpty())
0339         {
0340             updated = true;
0341             setCurrentTemplate(entry.installedFiles().at(0));
0342             break;
0343         }
0344     }
0345 
0346     if (!updated)
0347     {
0348         ui->listView->setCurrentIndex(QModelIndex());
0349     }
0350 }
0351 
0352 void ProjectSelectionPage::setCurrentTemplate (const QString& fileName)
0353 {
0354     QModelIndexList indexes = m_templatesModel->templateIndexes(fileName);
0355     if (indexes.size() > 1)
0356     {
0357         ui->listView->setCurrentIndex(indexes.at(1));
0358     }
0359     if (indexes.size() > 2)
0360     {
0361         ui->templateType->setCurrentIndex(indexes.at(2).row());
0362     }
0363 }
0364 
0365 #include "moc_projectselectionpage.cpp"