File indexing completed on 2024-05-05 04:39:49
0001 /* 0002 SPDX-FileCopyrightText: 2012 Miha Čančula <miha@noughmad.eu> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "templateclassassistant.h" 0008 0009 #include "templateselectionpage.h" 0010 #include "templateoptionspage.h" 0011 #include "classmemberspage.h" 0012 #include "classidentifierpage.h" 0013 #include "overridespage.h" 0014 #include "licensepage.h" 0015 #include "outputpage.h" 0016 #include "testcasespage.h" 0017 #include "defaultcreateclasshelper.h" 0018 #include "debug.h" 0019 0020 #include <language/codegen/templateclassgenerator.h> 0021 #include <language/codegen/sourcefiletemplate.h> 0022 #include <language/codegen/documentchangeset.h> 0023 #include <language/codegen/templaterenderer.h> 0024 #include <language/interfaces/icreateclasshelper.h> 0025 #include <language/interfaces/ilanguagesupport.h> 0026 0027 #include <interfaces/icore.h> 0028 #include <interfaces/ilanguagecontroller.h> 0029 #include <interfaces/idocumentcontroller.h> 0030 #include <interfaces/iproject.h> 0031 #include <interfaces/iprojectcontroller.h> 0032 #include <project/projectmodel.h> 0033 #include <project/interfaces/iprojectfilemanager.h> 0034 #include <project/interfaces/ibuildsystemmanager.h> 0035 #include <util/scopeddialog.h> 0036 0037 #include <QDialog> 0038 #include <QDialogButtonBox> 0039 #include <QLabel> 0040 #include <QListWidget> 0041 #include <QPushButton> 0042 #include <QVBoxLayout> 0043 0044 #include <KIO/Global> 0045 #include <KLocalizedString> 0046 0047 #define REMOVE_PAGE(name) \ 0048 if (d->name##Page) \ 0049 { \ 0050 removePage(d->name##Page); \ 0051 d->name##Page = nullptr; \ 0052 d->name##PageWidget = nullptr; \ 0053 } 0054 0055 #define ZERO_PAGE(name) \ 0056 d->name##Page = nullptr; \ 0057 d->name##PageWidget = nullptr; 0058 0059 using namespace KDevelop; 0060 0061 class KDevelop::TemplateClassAssistantPrivate 0062 { 0063 public: 0064 explicit TemplateClassAssistantPrivate(const QUrl& baseUrl); 0065 ~TemplateClassAssistantPrivate(); 0066 0067 void addFilesToTarget (const QHash<QString, QUrl>& fileUrls); 0068 0069 KPageWidgetItem* templateSelectionPage; 0070 KPageWidgetItem* classIdentifierPage; 0071 KPageWidgetItem* overridesPage; 0072 KPageWidgetItem* membersPage; 0073 KPageWidgetItem* testCasesPage; 0074 KPageWidgetItem* licensePage; 0075 KPageWidgetItem* templateOptionsPage; 0076 KPageWidgetItem* outputPage; 0077 KPageWidgetItem* dummyPage; 0078 0079 TemplateSelectionPage* templateSelectionPageWidget; 0080 ClassIdentifierPage* classIdentifierPageWidget; 0081 OverridesPage* overridesPageWidget; 0082 ClassMembersPage* membersPageWidget; 0083 TestCasesPage* testCasesPageWidget; 0084 LicensePage* licensePageWidget; 0085 TemplateOptionsPage* templateOptionsPageWidget; 0086 OutputPage* outputPageWidget; 0087 0088 QUrl baseUrl; 0089 SourceFileTemplate fileTemplate; 0090 ICreateClassHelper* helper; 0091 TemplateClassGenerator* generator; 0092 TemplateRenderer* renderer; 0093 0094 QVariantHash templateOptions; 0095 }; 0096 0097 TemplateClassAssistantPrivate::TemplateClassAssistantPrivate(const QUrl& baseUrl) 0098 : baseUrl(baseUrl) 0099 , helper(nullptr) 0100 , generator(nullptr) 0101 , renderer(nullptr) 0102 { 0103 } 0104 0105 TemplateClassAssistantPrivate::~TemplateClassAssistantPrivate() 0106 { 0107 delete helper; 0108 if (generator) 0109 { 0110 delete generator; 0111 } 0112 else 0113 { 0114 // if we got a generator, it should keep ownership of the renderer 0115 // otherwise, we created a templaterenderer on our own 0116 delete renderer; 0117 } 0118 } 0119 0120 void TemplateClassAssistantPrivate::addFilesToTarget (const QHash< QString, QUrl >& fileUrls) 0121 { 0122 // Add the generated files to a target, if one is found 0123 QUrl url = baseUrl; 0124 if (!url.isValid()) 0125 { 0126 // This was probably not launched from the project manager view 0127 // Still, we try to find the common URL where the generated files are located 0128 0129 if (!fileUrls.isEmpty()) 0130 { 0131 url = fileUrls.constBegin().value().adjusted(QUrl::RemoveFilename); 0132 } 0133 } 0134 qCDebug(PLUGIN_FILETEMPLATES) << "Searching for targets with URL" << url; 0135 IProject* project = ICore::self()->projectController()->findProjectForUrl(url); 0136 if (!project || !project->buildSystemManager()) 0137 { 0138 qCDebug(PLUGIN_FILETEMPLATES) << "No suitable project found"; 0139 return; 0140 } 0141 0142 const QList<ProjectBaseItem*> items = project->itemsForPath(IndexedString(url)); 0143 if (items.isEmpty()) 0144 { 0145 qCDebug(PLUGIN_FILETEMPLATES) << "No suitable project items found"; 0146 return; 0147 } 0148 0149 QList<ProjectTargetItem*> targets; 0150 ProjectTargetItem* target = nullptr; 0151 0152 for (ProjectBaseItem* item : items) { 0153 if (ProjectTargetItem* target = item->target()) 0154 { 0155 targets << target; 0156 } 0157 } 0158 0159 if (targets.isEmpty()) 0160 { 0161 // If no target was explicitly found yet, try all the targets in the current folder 0162 targets.reserve(items.size()); 0163 for (ProjectBaseItem* item : items) { 0164 targets << item->targetList(); 0165 } 0166 } 0167 0168 if (targets.isEmpty()) 0169 { 0170 // If still no targets, we traverse the tree up to the first directory with targets 0171 ProjectBaseItem* item = items.first()->parent(); 0172 while (targets.isEmpty() && item) 0173 { 0174 targets = item->targetList(); 0175 item = item->parent(); 0176 } 0177 } 0178 0179 if (targets.size() == 1) 0180 { 0181 qCDebug(PLUGIN_FILETEMPLATES) << "Only one candidate target," << targets.first()->text() << ", using it"; 0182 target = targets.first(); 0183 } 0184 else if (targets.size() > 1) 0185 { 0186 // More than one candidate target, show the chooser dialog 0187 ScopedDialog<QDialog> d; 0188 0189 auto mainLayout = new QVBoxLayout(d); 0190 mainLayout->addWidget(new QLabel(i18n("Choose one target to add the file or cancel if you do not want to do so."))); 0191 0192 auto* targetsWidget = new QListWidget(d); 0193 targetsWidget->setSelectionMode(QAbstractItemView::SingleSelection); 0194 for (ProjectTargetItem* target : qAsConst(targets)) { 0195 targetsWidget->addItem(target->text()); 0196 } 0197 targetsWidget->setCurrentRow(0); 0198 mainLayout->addWidget(targetsWidget); 0199 0200 auto* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); 0201 QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); 0202 okButton->setDefault(true); 0203 okButton->setShortcut(Qt::CTRL | Qt::Key_Return); 0204 d->connect(buttonBox, &QDialogButtonBox::accepted, d.data(), &QDialog::accept); 0205 d->connect(buttonBox, &QDialogButtonBox::rejected, d.data(), &QDialog::reject); 0206 mainLayout->addWidget(buttonBox); 0207 0208 if(d->exec() == QDialog::Accepted) 0209 { 0210 if (!targetsWidget->selectedItems().isEmpty()) 0211 { 0212 target = targets[targetsWidget->currentRow()]; 0213 } 0214 else 0215 { 0216 qCDebug(PLUGIN_FILETEMPLATES) << "Did not select anything, not adding to a target"; 0217 return; 0218 } 0219 } 0220 else { 0221 qCDebug(PLUGIN_FILETEMPLATES) << "Canceled select target dialog, not adding to a target"; 0222 return; 0223 } 0224 } 0225 else 0226 { 0227 // No target, not doing anything 0228 qCDebug(PLUGIN_FILETEMPLATES) << "No possible targets for URL" << url; 0229 return; 0230 } 0231 0232 Q_ASSERT(target); 0233 0234 QList<ProjectFileItem*> fileItems; 0235 for (const QUrl& fileUrl : fileUrls) { 0236 const auto items = project->itemsForPath(IndexedString(KIO::upUrl(fileUrl))); 0237 for (ProjectBaseItem* item : items) { 0238 if (ProjectFolderItem* folder = item->folder()) 0239 { 0240 ///FIXME: use Path instead of QUrl in the template class assistant 0241 if (ProjectFileItem* file = project->projectFileManager()->addFile(Path(fileUrl), folder)) { 0242 fileItems << file; 0243 break; 0244 } 0245 } 0246 } 0247 } 0248 0249 if (!fileItems.isEmpty()) { 0250 project->buildSystemManager()->addFilesToTarget(fileItems, target); 0251 } 0252 0253 } 0254 0255 TemplateClassAssistant::TemplateClassAssistant(QWidget* parent, const QUrl& baseUrl) 0256 : KAssistantDialog(parent) 0257 , d(new TemplateClassAssistantPrivate(baseUrl)) 0258 { 0259 ZERO_PAGE(templateSelection) 0260 ZERO_PAGE(templateOptions) 0261 ZERO_PAGE(members) 0262 ZERO_PAGE(classIdentifier) 0263 ZERO_PAGE(overrides) 0264 ZERO_PAGE(license) 0265 ZERO_PAGE(output) 0266 ZERO_PAGE(testCases) 0267 0268 setup(); 0269 } 0270 0271 TemplateClassAssistant::~TemplateClassAssistant() 0272 { 0273 delete d; 0274 } 0275 0276 void TemplateClassAssistant::setup() 0277 { 0278 if (d->baseUrl.isValid()) 0279 { 0280 setWindowTitle(xi18nc("@title:window", "Create Files from Template in <filename>%1</filename>", d->baseUrl.toDisplayString(QUrl::PreferLocalFile))); 0281 } 0282 else 0283 { 0284 setWindowTitle(i18nc("@title:window", "Create Files from Template")); 0285 } 0286 0287 d->templateSelectionPageWidget = new TemplateSelectionPage(this); 0288 connect(this, &TemplateClassAssistant::accepted, d->templateSelectionPageWidget, &TemplateSelectionPage::saveConfig); 0289 d->templateSelectionPage = addPage(d->templateSelectionPageWidget, i18nc("@title:tab", "Language and Template")); 0290 d->templateSelectionPage->setIcon(QIcon::fromTheme(QStringLiteral("project-development-new-template"))); 0291 0292 d->dummyPage = addPage(new QWidget(this), QStringLiteral("Dummy Page")); 0293 0294 // KAssistantDialog creates a help button by default, no option to prevent that 0295 QPushButton *helpButton = button(QDialogButtonBox::Help); 0296 if (helpButton) { 0297 buttonBox()->removeButton(helpButton); 0298 delete helpButton; 0299 } 0300 } 0301 0302 void TemplateClassAssistant::templateChosen(const QString& templateDescription) 0303 { 0304 d->fileTemplate.setTemplateDescription(templateDescription); 0305 const auto type = d->fileTemplate.type(); 0306 0307 d->generator = nullptr; 0308 0309 if (!d->fileTemplate.isValid()) 0310 { 0311 return; 0312 } 0313 0314 qCDebug(PLUGIN_FILETEMPLATES) << "Selected template" << templateDescription << "of type" << type; 0315 removePage(d->dummyPage); 0316 0317 if (d->baseUrl.isValid()) 0318 { 0319 setWindowTitle(xi18nc("@title:window", "Create Files from Template <filename>%1</filename> in <filename>%2</filename>", 0320 d->fileTemplate.name(), 0321 d->baseUrl.toDisplayString(QUrl::PreferLocalFile))); 0322 } 0323 else 0324 { 0325 setWindowTitle(xi18nc("@title:window", "Create Files from Template <filename>%1</filename>", d->fileTemplate.name())); 0326 } 0327 0328 if (type == QLatin1String("Class")) 0329 { 0330 d->classIdentifierPageWidget = new ClassIdentifierPage(this); 0331 d->classIdentifierPage = addPage(d->classIdentifierPageWidget, i18nc("@title:tab", "Class Basics")); 0332 d->classIdentifierPage->setIcon(QIcon::fromTheme(QStringLiteral("classnew"))); 0333 connect(d->classIdentifierPageWidget, &ClassIdentifierPage::isValid, this, &TemplateClassAssistant::setCurrentPageValid); 0334 setValid(d->classIdentifierPage, false); 0335 0336 d->overridesPageWidget = new OverridesPage(this); 0337 d->overridesPage = addPage(d->overridesPageWidget, i18nc("@title:tab", "Override Methods")); 0338 d->overridesPage->setIcon(QIcon::fromTheme(QStringLiteral("code-class"))); 0339 setValid(d->overridesPage, true); 0340 0341 d->membersPageWidget = new ClassMembersPage(this); 0342 d->membersPage = addPage(d->membersPageWidget, i18nc("@title:tab", "Class Members")); 0343 d->membersPage->setIcon(QIcon::fromTheme(QStringLiteral("field"))); 0344 setValid(d->membersPage, true); 0345 0346 d->helper = nullptr; 0347 QString languageName = d->fileTemplate.languageName(); 0348 auto language = ICore::self()->languageController()->language(languageName); 0349 if (language) 0350 { 0351 d->helper = language->createClassHelper(); 0352 } 0353 0354 if (!d->helper) 0355 { 0356 qCDebug(PLUGIN_FILETEMPLATES) << "No class creation helper for language" << languageName; 0357 d->helper = new DefaultCreateClassHelper; 0358 } 0359 0360 d->generator = d->helper->createGenerator(d->baseUrl); 0361 Q_ASSERT(d->generator); 0362 d->generator->setTemplateDescription(d->fileTemplate); 0363 d->renderer = d->generator->renderer(); 0364 } 0365 else 0366 { 0367 if (type == QLatin1String("Test")) 0368 { 0369 d->testCasesPageWidget = new TestCasesPage(this); 0370 d->testCasesPage = addPage(d->testCasesPageWidget, i18nc("@title:tab", "Test Cases")); 0371 connect(d->testCasesPageWidget, &TestCasesPage::isValid, this, &TemplateClassAssistant::setCurrentPageValid); 0372 setValid(d->testCasesPage, false); 0373 } 0374 0375 d->renderer = new TemplateRenderer; 0376 d->renderer->setEmptyLinesPolicy(TemplateRenderer::TrimEmptyLines); 0377 } 0378 0379 d->licensePageWidget = new LicensePage(this); 0380 d->licensePage = addPage(d->licensePageWidget, i18nc("@title:tab", "License")); 0381 d->licensePage->setIcon(QIcon::fromTheme(QStringLiteral("text-x-copying"))); 0382 setValid(d->licensePage, true); 0383 0384 d->outputPageWidget = new OutputPage(this); 0385 d->outputPageWidget->prepareForm(d->fileTemplate); 0386 d->outputPage = addPage(d->outputPageWidget, i18nc("@title:tab", "Output")); 0387 d->outputPage->setIcon(QIcon::fromTheme(QStringLiteral("document-save"))); 0388 connect(d->outputPageWidget, &OutputPage::isValid, this, &TemplateClassAssistant::setCurrentPageValid); 0389 setValid(d->outputPage, false); 0390 0391 if (d->fileTemplate.hasCustomOptions()) 0392 { 0393 qCDebug(PLUGIN_FILETEMPLATES) << "Class generator has custom options"; 0394 d->templateOptionsPageWidget = new TemplateOptionsPage(this); 0395 d->templateOptionsPage = insertPage(d->outputPage, d->templateOptionsPageWidget, 0396 i18nc("@title:tab", "Template Options")); 0397 } 0398 0399 setCurrentPage(d->templateSelectionPage); 0400 } 0401 0402 void TemplateClassAssistant::next() 0403 { 0404 qCDebug(PLUGIN_FILETEMPLATES) << currentPage()->name() << currentPage()->header(); 0405 if (currentPage() == d->templateSelectionPage) 0406 { 0407 // We have chosen the template 0408 // Depending on the template's language, we can now create a helper 0409 QString description = d->templateSelectionPageWidget->selectedTemplate(); 0410 templateChosen(description); 0411 if (!d->fileTemplate.isValid()) 0412 { 0413 return; 0414 } 0415 } 0416 else if (currentPage() == d->classIdentifierPage) 0417 { 0418 d->generator->setIdentifier(d->classIdentifierPageWidget->identifier()); 0419 d->generator->setBaseClasses(d->classIdentifierPageWidget->inheritanceList()); 0420 } 0421 else if (currentPage() == d->overridesPage) 0422 { 0423 ClassDescription desc = d->generator->description(); 0424 desc.methods.clear(); 0425 const auto overrides = d->overridesPageWidget->selectedOverrides(); 0426 desc.methods.reserve(overrides.size()); 0427 for (const DeclarationPointer& declaration : overrides) { 0428 desc.methods << FunctionDescription(declaration); 0429 } 0430 d->generator->setDescription(desc); 0431 } 0432 else if (currentPage() == d->membersPage) 0433 { 0434 ClassDescription desc = d->generator->description(); 0435 desc.members = d->membersPageWidget->members(); 0436 d->generator->setDescription(desc); 0437 } 0438 else if (currentPage() == d->licensePage) 0439 { 0440 if (d->generator) 0441 { 0442 d->generator->setLicense(d->licensePageWidget->license()); 0443 } 0444 else 0445 { 0446 d->renderer->addVariable(QStringLiteral("license"), d->licensePageWidget->license()); 0447 } 0448 } 0449 else if (d->templateOptionsPage && (currentPage() == d->templateOptionsPage)) 0450 { 0451 if (d->generator) 0452 { 0453 d->generator->addVariables(d->templateOptionsPageWidget->templateOptions()); 0454 } 0455 else 0456 { 0457 d->renderer->addVariables(d->templateOptionsPageWidget->templateOptions()); 0458 } 0459 } 0460 else if (currentPage() == d->testCasesPage) 0461 { 0462 d->renderer->addVariable(QStringLiteral("name"), d->testCasesPageWidget->name()); 0463 d->renderer->addVariable(QStringLiteral("testCases"), d->testCasesPageWidget->testCases()); 0464 } 0465 0466 KAssistantDialog::next(); 0467 0468 if (currentPage() == d->classIdentifierPage) 0469 { 0470 d->classIdentifierPageWidget->setInheritanceList(d->fileTemplate.defaultBaseClasses()); 0471 } 0472 else if (currentPage() == d->membersPage) 0473 { 0474 d->membersPageWidget->setMembers(d->generator->description().members); 0475 } 0476 else if (currentPage() == d->overridesPage) 0477 { 0478 d->overridesPageWidget->clear(); 0479 d->overridesPageWidget->addCustomDeclarations(i18nc("@item default declarations", "Default"), 0480 d->helper->defaultMethods(d->generator->name())); 0481 d->overridesPageWidget->addBaseClasses(d->generator->directBaseClasses(), 0482 d->generator->allBaseClasses()); 0483 } 0484 else if (d->templateOptionsPage && (currentPage() == d->templateOptionsPage)) 0485 { 0486 d->templateOptionsPageWidget->load(d->fileTemplate, d->renderer); 0487 } 0488 else if (currentPage() == d->outputPage) 0489 { 0490 d->outputPageWidget->loadFileTemplate(d->fileTemplate, d->baseUrl, d->renderer); 0491 } 0492 0493 if (auto* pageFocus = dynamic_cast<KDevelop::IPageFocus*>(currentPage()->widget())) { 0494 pageFocus->setFocusToFirstEditWidget(); 0495 } 0496 } 0497 0498 void TemplateClassAssistant::back() 0499 { 0500 KAssistantDialog::back(); 0501 if (currentPage() == d->templateSelectionPage) 0502 { 0503 REMOVE_PAGE(classIdentifier) 0504 REMOVE_PAGE(overrides) 0505 REMOVE_PAGE(members) 0506 REMOVE_PAGE(testCases) 0507 REMOVE_PAGE(output) 0508 REMOVE_PAGE(templateOptions) 0509 REMOVE_PAGE(license) 0510 0511 delete d->helper; 0512 d->helper = nullptr; 0513 0514 if (d->generator) 0515 { 0516 delete d->generator; 0517 } 0518 else 0519 { 0520 delete d->renderer; 0521 } 0522 d->generator = nullptr; 0523 d->renderer = nullptr; 0524 0525 if (d->baseUrl.isValid()) 0526 { 0527 setWindowTitle(xi18nc("@title:window", "Create Files from Template in <filename>%1</filename>", d->baseUrl.toDisplayString(QUrl::PreferLocalFile))); 0528 } 0529 else 0530 { 0531 setWindowTitle(i18nc("@title:window", "Create Files from Template")); 0532 } 0533 d->dummyPage = addPage(new QWidget(this), QStringLiteral("Dummy Page")); 0534 } 0535 } 0536 0537 void TemplateClassAssistant::accept() 0538 { 0539 // next() is not called for the last page (when the user clicks Finish), so we have to set output locations here 0540 const QHash<QString, QUrl> fileUrls = d->outputPageWidget->fileUrls(); 0541 QHash<QString, KTextEditor::Cursor> filePositions = d->outputPageWidget->filePositions(); 0542 0543 DocumentChangeSet changes; 0544 if (d->generator) 0545 { 0546 QHash<QString, QUrl>::const_iterator it = fileUrls.constBegin(); 0547 for (; it != fileUrls.constEnd(); ++it) 0548 { 0549 d->generator->setFileUrl(it.key(), it.value()); 0550 d->generator->setFilePosition(it.key(), filePositions.value(it.key())); 0551 } 0552 0553 d->generator->addVariables(d->templateOptions); 0554 changes = d->generator->generate(); 0555 } 0556 else 0557 { 0558 changes = d->renderer->renderFileTemplate(d->fileTemplate, d->baseUrl, fileUrls); 0559 } 0560 0561 d->addFilesToTarget(fileUrls); 0562 changes.applyAllChanges(); 0563 0564 // Open the generated files in the editor 0565 for (const QUrl& url : fileUrls) { 0566 ICore::self()->documentController()->openDocument(url); 0567 } 0568 0569 KAssistantDialog::accept(); 0570 } 0571 0572 void TemplateClassAssistant::setCurrentPageValid(bool valid) 0573 { 0574 setValid(currentPage(), valid); 0575 } 0576 0577 QUrl TemplateClassAssistant::baseUrl() const 0578 { 0579 return d->baseUrl; 0580 } 0581 0582 #include "moc_templateclassassistant.cpp"