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