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"