File indexing completed on 2024-05-19 05:41:57

0001 // ct_lvtcgn_codegendialog.cpp                                       -*-C++-*-
0002 
0003 /*
0004 // Copyright 2023 Codethink Ltd <codethink@codethink.co.uk>
0005 // SPDX-License-Identifier: Apache-2.0
0006 //
0007 // Licensed under the Apache License, Version 2.0 (the "License");
0008 // you may not use this file except in compliance with the License.
0009 // You may obtain a copy of the License at
0010 //
0011 //     http://www.apache.org/licenses/LICENSE-2.0
0012 //
0013 // Unless required by applicable law or agreed to in writing, software
0014 // distributed under the License is distributed on an "AS IS" BASIS,
0015 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0016 // See the License for the specific language governing permissions and
0017 // limitations under the License.
0018 */
0019 
0020 #include <ct_lvtcgn_codegendialog.h>
0021 #include <ct_lvtcgn_cogedentreemodel.h>
0022 
0023 #include <QDesktopServices>
0024 #include <QFileDialog>
0025 #include <QPainter>
0026 #include <QStyledItemDelegate>
0027 #include <QWidget>
0028 #include <filesystem>
0029 #include <kwidgetsaddons_version.h>
0030 #include <memory>
0031 
0032 namespace Codethink::lvtcgn::gui {
0033 
0034 using namespace Codethink::lvtcgn::mdl;
0035 
0036 class FindOnTreeModel : public QObject {
0037   public:
0038     void setTreeView(QTreeView *t)
0039     {
0040         treeView = t;
0041     }
0042 
0043     void findText(QString const& text)
0044     {
0045         auto *treeModel = dynamic_cast<CodeGenerationEntitiesTreeModel *>(treeView->model());
0046 
0047         currentNeedle = text;
0048         foundItems.clear();
0049         currentFindItem = foundItems.end();
0050 
0051         if (currentNeedle.isEmpty()) {
0052             treeModel->recursiveExec([&](QStandardItem *item) {
0053                 auto entityName = item->data(CodeGenerationDataRole::EntityNameRole).toString();
0054                 item->setData(entityName, Qt::DisplayRole);
0055                 return CodeGenerationEntitiesTreeModel::RecursiveExec::ContinueSearch;
0056             });
0057             return;
0058         }
0059 
0060         // cppcheck things we can move 'firstFoundItem' to inside the lambda, but this would change the intended
0061         // behavior and introduce a bug, so a 'cppcheck-suppress' has been added below.
0062         auto firstFoundItem = true; // cppcheck-suppress variableScope
0063         treeModel->recursiveExec([&](QStandardItem *item) {
0064             auto entityName = item->data(CodeGenerationDataRole::EntityNameRole).toString();
0065             if (entityName.contains(currentNeedle)) {
0066                 highlightNeedleOnItem(item, currentNeedle, FOUND_COLOR);
0067                 foundItems.push_back(item);
0068                 currentFindItem = foundItems.begin();
0069                 if (firstFoundItem) {
0070                     goToItem(*currentFindItem, nullptr);
0071                     firstFoundItem = false;
0072                 }
0073             } else {
0074                 item->setData(entityName, Qt::DisplayRole);
0075             }
0076             return CodeGenerationEntitiesTreeModel::RecursiveExec::ContinueSearch;
0077         });
0078     }
0079 
0080     void goToNextItem()
0081     {
0082         if (foundItems.empty()) {
0083             return;
0084         }
0085 
0086         auto prevFindItem = currentFindItem;
0087         if (currentFindItem + 1 >= foundItems.end()) {
0088             currentFindItem = foundItems.begin();
0089         } else {
0090             currentFindItem++;
0091         }
0092 
0093         goToItem(*currentFindItem, *prevFindItem);
0094     }
0095 
0096     void goToPrevItem()
0097     {
0098         if (foundItems.empty()) {
0099             return;
0100         }
0101 
0102         auto prevFindItem = currentFindItem;
0103         if (currentFindItem - 1 < foundItems.begin()) {
0104             currentFindItem = foundItems.end() - 1;
0105         } else {
0106             currentFindItem--;
0107         }
0108 
0109         goToItem(*currentFindItem, *prevFindItem);
0110     };
0111 
0112   private:
0113     void recursivelyExpandItem(QStandardItem *item)
0114     {
0115         while (item) {
0116             treeView->setExpanded(item->index(), true);
0117             item = item->parent();
0118         }
0119     }
0120 
0121     static void highlightNeedleOnItem(QStandardItem *item, QString const& needle, QString const& color)
0122     {
0123         auto highlightedText = item->data(CodeGenerationDataRole::EntityNameRole).toString();
0124         highlightedText.replace(needle, "<span style='background-color: " + color + "'>" + needle + "</span>");
0125         item->setData(highlightedText, Qt::DisplayRole);
0126     }
0127 
0128     void goToItem(QStandardItem *currentItem, QStandardItem *prevItem)
0129     {
0130         recursivelyExpandItem(currentItem);
0131         treeView->scrollTo(currentItem->index());
0132         if (prevItem) {
0133             highlightNeedleOnItem(prevItem, currentNeedle, FOUND_COLOR);
0134         }
0135         highlightNeedleOnItem(currentItem, currentNeedle, SELECTED_COLOR);
0136     }
0137 
0138     QTreeView *treeView = nullptr;
0139     std::vector<QStandardItem *> foundItems;
0140     decltype(foundItems)::iterator currentFindItem = foundItems.end();
0141     QString currentNeedle;
0142 
0143     static auto constexpr SELECTED_COLOR = "orange";
0144     static auto constexpr FOUND_COLOR = "yellow";
0145 };
0146 
0147 struct CodeGenerationDialog::Private {
0148     Ui::CodeGenerationDialogUi ui;
0149     CodeGenerationEntitiesTreeModel treeModel;
0150     ICodeGenerationDataProvider& dataProvider;
0151     FindOnTreeModel findOnTreeModel;
0152 
0153     explicit Private(ICodeGenerationDataProvider& dataProvider):
0154         ui{}, treeModel(dataProvider), dataProvider(dataProvider)
0155     {
0156     }
0157 };
0158 
0159 QString CodeGenerationDialog::Detail::getExistingDirectory(QDialog& dialog, QString const& defaultPath)
0160 {
0161     auto selectedPath = defaultPath.isEmpty() ? QDir::currentPath() : defaultPath;
0162     return QFileDialog::getExistingDirectory(&dialog, tr("Output directory"), selectedPath);
0163 }
0164 
0165 void CodeGenerationDialog::Detail::handleOutputDirEmpty(Ui::CodeGenerationDialogUi& ui)
0166 {
0167     showErrorMessage(ui, tr("Please select the output directory."));
0168 }
0169 
0170 void CodeGenerationDialog::Detail::showErrorMessage(Ui::CodeGenerationDialogUi& ui, QString const& message)
0171 {
0172 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0)
0173     ui.topMessageWidget->clearActions();
0174 #else
0175     const auto ourActions = ui.topMessageWidget->actions();
0176     for (auto *action : ourActions) {
0177         ui.topMessageWidget->removeAction(action);
0178     }
0179 #endif
0180     ui.topMessageWidget->setText(message);
0181     ui.topMessageWidget->animatedShow();
0182 }
0183 
0184 void CodeGenerationDialog::Detail::codeGenerationIterationCallback(Ui::CodeGenerationDialogUi& ui,
0185                                                                    const mdl::CodeGeneration::CodeGenerationStep& step)
0186 {
0187     {
0188         const auto *s = dynamic_cast<const mdl::CodeGeneration::ProcessEntityStep *>(&step);
0189         if (s != nullptr) {
0190             ui.progressBar->setValue(ui.progressBar->value() + 1);
0191             ui.codeGenLogsTextArea->appendPlainText(tr("Processing ") + QString::fromStdString(s->entityName())
0192                                                     + "...");
0193         }
0194     }
0195 }
0196 
0197 void CodeGenerationDialog::Detail::handleCodeGenerationError(Ui::CodeGenerationDialogUi& ui,
0198                                                              CodeGenerationError const& error)
0199 {
0200     switch (error.kind) {
0201     case CodeGenerationError::Kind::PythonError: {
0202         ui.codeGenLogsTextArea->appendPlainText(tr("Python script error:"));
0203         ui.codeGenLogsTextArea->appendPlainText(QString::fromStdString(error.message));
0204         return;
0205     }
0206     case CodeGenerationError::Kind::ScriptDefinitionError: {
0207         ui.codeGenLogsTextArea->appendPlainText(tr("User provided an invalid Python script."));
0208         ui.codeGenLogsTextArea->appendPlainText(tr("Code generation module returned the following message:"));
0209         ui.codeGenLogsTextArea->appendPlainText(QString::fromStdString(error.message));
0210         ui.codeGenLogsTextArea->appendPlainText("");
0211         ui.codeGenLogsTextArea->appendPlainText(
0212             tr("For more information about how to build the generation scripts, please visit the documentation."));
0213         return;
0214     }
0215     }
0216 }
0217 
0218 QString CodeGenerationDialog::Detail::executablePath() const
0219 {
0220     return QCoreApplication::applicationDirPath();
0221 }
0222 
0223 class HtmlPainterDelegate : public QStyledItemDelegate {
0224   public:
0225     using QStyledItemDelegate::QStyledItemDelegate;
0226 
0227     void paint(QPainter *painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override
0228     {
0229         // Intercept the drawing for each item to add HTML
0230         auto rect = option.rect;
0231         auto textOffset = QPointF{20, -4};
0232         auto doc = QTextDocument{};
0233         doc.setHtml(index.data(Qt::DisplayRole).toString());
0234         painter->save();
0235         painter->translate(rect.left() + textOffset.x(), rect.top() + textOffset.y());
0236         doc.drawContents(painter, QRect(0, 0, rect.width(), rect.height()));
0237         painter->restore();
0238 
0239         QStyledItemDelegate::paint(painter, option, index);
0240     }
0241 
0242     [[nodiscard]] QString displayText(const QVariant& value, const QLocale& locale) const override
0243     {
0244         // This is a hack to trick QStyledItemDelegate::paint and avoiding displaying the text.
0245         // This is necessary because this class is drawing the text using HTML, so the QStyledItemDelegate must not
0246         // write anything.
0247         return "";
0248     }
0249 };
0250 
0251 CodeGenerationDialog::CodeGenerationDialog(ICodeGenerationDataProvider& dataProvider,
0252                                            std::unique_ptr<Detail> impl,
0253                                            QWidget *parent):
0254     d(std::make_unique<CodeGenerationDialog::Private>(dataProvider)),
0255     impl(impl == nullptr ? std::make_unique<CodeGenerationDialog::Detail>() : std::move(impl))
0256 {
0257     d->ui.setupUi(this);
0258     d->ui.runCancelGroup->setStyleSheet("QGroupBox { border: 0; }");
0259     d->ui.searchGroup->setStyleSheet("QGroupBox { border: 0; }");
0260     d->ui.titleLabel->setStyleSheet("QLabel { font-weight: bold; }");
0261     d->ui.topMessageWidget->hide();
0262     d->ui.physicalEntitiesTree->setModel(&d->treeModel);
0263     d->ui.physicalEntitiesTree->setItemDelegate(new HtmlPainterDelegate{});
0264 
0265     d->findOnTreeModel.setTreeView(d->ui.physicalEntitiesTree);
0266     connect(d->ui.searchValue, &QLineEdit::textChanged, &d->findOnTreeModel, &FindOnTreeModel::findText);
0267     connect(d->ui.searchGoToNextBtn, &QPushButton::pressed, &d->findOnTreeModel, &FindOnTreeModel::goToNextItem);
0268     connect(d->ui.searchGoToPrevBtn, &QPushButton::pressed, &d->findOnTreeModel, &FindOnTreeModel::goToPrevItem);
0269 
0270     d->ui.statusGroup->hide();
0271 
0272     populateAvailableScriptsCombobox();
0273 
0274     connect(d->ui.findOutputDirBtn, &QAbstractButton::clicked, this, &CodeGenerationDialog::searchOutputDir);
0275     connect(d->ui.runCodeGenerationBtn, &QAbstractButton::clicked, this, &CodeGenerationDialog::runCodeGeneration);
0276     connect(d->ui.cancelBtn, &QAbstractButton::clicked, this, &CodeGenerationDialog::close);
0277     connect(d->ui.openOutputDir, &QAbstractButton::clicked, this, &CodeGenerationDialog::openOutputDir);
0278 
0279     connect(d->ui.selectAllCheckbox, &QCheckBox::stateChanged, this, [&](int newState) {
0280         if (newState == Qt::CheckState::PartiallyChecked) {
0281             d->ui.selectAllCheckbox->setCheckState(Qt::CheckState::Checked);
0282             return;
0283         }
0284 
0285         if (newState == Qt::CheckState::Checked) {
0286             d->treeModel.recursiveExec([&](QStandardItem *item) {
0287                 item->setCheckState(Qt::CheckState::Checked);
0288                 return CodeGenerationEntitiesTreeModel::RecursiveExec::ContinueSearch;
0289             });
0290         }
0291 
0292         if (newState == Qt::CheckState::Unchecked) {
0293             d->treeModel.recursiveExec([&](QStandardItem *item) {
0294                 item->setCheckState(Qt::CheckState::Unchecked);
0295                 return CodeGenerationEntitiesTreeModel::RecursiveExec::ContinueSearch;
0296             });
0297         }
0298     });
0299 }
0300 
0301 CodeGenerationDialog::~CodeGenerationDialog() = default;
0302 
0303 void CodeGenerationDialog::runCodeGeneration()
0304 {
0305     d->ui.topMessageWidget->hide();
0306     d->ui.progressBar->reset();
0307     d->ui.codeGenLogsTextArea->clear();
0308 
0309     if (d->ui.outputDirInput->text().isEmpty()) {
0310         impl->handleOutputDirEmpty(d->ui);
0311         return;
0312     }
0313 
0314     d->ui.progressBar->setMinimum(0);
0315     d->ui.progressBar->setMaximum(d->dataProvider.numberOfPhysicalEntities());
0316     auto iterationCallback = [&](const mdl::CodeGeneration::CodeGenerationStep& step) {
0317         impl->codeGenerationIterationCallback(d->ui, step);
0318         qApp->processEvents();
0319     };
0320 
0321     d->ui.statusGroup->show();
0322 
0323     // TODO [#441]: Make a CANCEL button for the code generation
0324     d->ui.runCodeGenerationBtn->setEnabled(false);
0325     auto result = CodeGeneration::generateCodeFromScript(selectedScriptPath().toStdString(),
0326                                                          d->ui.outputDirInput->text().toStdString(),
0327                                                          d->dataProvider,
0328                                                          iterationCallback);
0329     d->ui.runCodeGenerationBtn->setEnabled(true);
0330 
0331     if (result.has_error()) {
0332         impl->handleCodeGenerationError(d->ui, result.error());
0333         return;
0334     }
0335 
0336     d->ui.codeGenLogsTextArea->appendPlainText(tr("All files processed successfully."));
0337     d->ui.progressBar->setValue(d->ui.progressBar->maximum());
0338 }
0339 
0340 void CodeGenerationDialog::populateAvailableScriptsCombobox()
0341 {
0342     auto const SCRIPTS_PATH = (impl->executablePath() + "/python/codegeneration/").toStdString();
0343     if (!std::filesystem::exists(SCRIPTS_PATH)) {
0344         impl->showErrorMessage(d->ui, tr("Couldn't find any scripts for code generation."));
0345         return;
0346     }
0347     for (auto const& entry : std::filesystem::directory_iterator(SCRIPTS_PATH)) {
0348         auto const& path = entry.path();
0349         d->ui.availableScriptsCombobox->addItem(QString::fromStdString(path.stem().string()),
0350                                                 QString::fromStdString(path.string()));
0351     }
0352 }
0353 
0354 void CodeGenerationDialog::searchOutputDir()
0355 {
0356     auto directory = impl->getExistingDirectory(*this, d->ui.outputDirInput->text());
0357     if (directory.isEmpty()) {
0358         return;
0359     }
0360     d->ui.outputDirInput->setText(directory);
0361 }
0362 
0363 void CodeGenerationDialog::setOutputDir(const QString& dir)
0364 {
0365     d->ui.outputDirInput->setText(dir);
0366 }
0367 
0368 QString CodeGenerationDialog::outputDir() const
0369 {
0370     return d->ui.outputDirInput->text();
0371 }
0372 
0373 QString CodeGenerationDialog::selectedScriptPath() const
0374 {
0375     return d->ui.availableScriptsCombobox->currentData().toString() + "/codegenerator.py";
0376 }
0377 
0378 void CodeGenerationDialog::openOutputDir() const
0379 {
0380     QDesktopServices::openUrl(d->ui.outputDirInput->text());
0381 }
0382 
0383 } // namespace Codethink::lvtcgn::gui