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"