diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cc33f0a5b..2b05814de 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,7 +56,11 @@ jobs: sudo add-apt-repository -y 'deb http://apt.llvm.org/focal/ llvm-toolchain-focal-15 main' sudo apt-get update sudo apt-get -y install --no-install-recommends clang-${{ matrix.llvm-major }} llvm-${{ matrix.llvm-major }}-dev clang-tidy-14 - + - name: Install Tool Dependencies + shell: bash + run: | + sudo apt-get update + sudo apt-get install qtbase5-dev qtdeclarative5-dev - name: Workaround for sanitizer shell: bash run: | diff --git a/.gitmodules b/.gitmodules index 4ad0e6193..e1523618e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,6 +13,9 @@ [submodule "external/csv-parser"] path = external/csv-parser url = https://github.com/se-sic/csv-parser.git +[submodule "external/qsourcehighlite"] + path = external/qsourcehighlite + url = git@github.com:Waqar144/QSourceHighlite.git [submodule "external/z3"] path = external/z3 url = https://github.com/fabianbs96/z3.git diff --git a/external/qsourcehighlite b/external/qsourcehighlite new file mode 160000 index 000000000..f1ed26f26 --- /dev/null +++ b/external/qsourcehighlite @@ -0,0 +1 @@ +Subproject commit f1ed26f26d980360422a0295e49bf676abe7e416 diff --git a/include/vara/Feature/FeatureModelTransaction.h b/include/vara/Feature/FeatureModelTransaction.h index 1946bc180..c1f4753bb 100644 --- a/include/vara/Feature/FeatureModelTransaction.h +++ b/include/vara/Feature/FeatureModelTransaction.h @@ -91,7 +91,7 @@ class FeatureModelTransaction /// \returns a pointer to the inserted Feature in CopyMode, otherwise, /// nothing. decltype(auto) addFeature(std::unique_ptr NewFeature, - Feature *Parent = nullptr) { + FeatureTreeNode *Parent = nullptr) { if constexpr (IsCopyMode) { return this->addFeatureImpl(std::move(NewFeature), Parent); } else { @@ -321,11 +321,11 @@ class AddFeatureToModel : public FeatureModelModification { private: AddFeatureToModel(std::unique_ptr NewFeature, - Feature *Parent = nullptr) + FeatureTreeNode *Parent = nullptr) : NewFeature(std::move(NewFeature)), Parent(Parent) {} std::unique_ptr NewFeature; - Feature *Parent; + FeatureTreeNode *Parent; }; //===----------------------------------------------------------------------===// @@ -829,7 +829,7 @@ class FeatureModelCopyTransactionBase { // Modifications Result - addFeatureImpl(std::unique_ptr NewFeature, Feature *Parent) { + addFeatureImpl(std::unique_ptr NewFeature, FeatureTreeNode *Parent) { if (!FM) { return ERROR; } @@ -968,6 +968,28 @@ class FeatureModelCopyTransactionBase { return FM->getFeature(F.getName()); } + [[nodiscard]] FeatureTreeNode *translateFeature(FeatureTreeNode &F) { + if (F.getKind() == FeatureTreeNode::NodeKind::NK_RELATIONSHIP) { + FeatureTreeNode *Parent = F.getParent(); + auto *ParentFeature = llvm::dyn_cast(Parent); + if (ParentFeature == nullptr) { + // The Parent of a Relationship should always be a Feature + abort(); + } + ParentFeature = FM->getFeature(ParentFeature->getName()); + Relationship *Base = *ParentFeature->getChildren(0).begin(); + Base = *Base->getChildren(0).begin(); + return Base; + } + auto *CastF = llvm::dyn_cast(&F); + if (CastF == nullptr) { + // There are only Features and Relationship nodes if F was not a + // Relationship it has to be a Feature + abort(); + } + return FM->getFeature(CastF->getName()); + } + std::unique_ptr FM; }; @@ -1008,7 +1030,8 @@ class FeatureModelModifyTransactionBase { //===--------------------------------------------------------------------===// // Modifications - void addFeatureImpl(std::unique_ptr NewFeature, Feature *Parent) { + void addFeatureImpl(std::unique_ptr NewFeature, + FeatureTreeNode *Parent) { assert(FM && "FeatureModel is null."); Modifications.push_back( diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index d975a7f2d..c01583383 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -2,3 +2,4 @@ if(VARA_FEATURE_USE_Z3_SOLVER) add_subdirectory(config-generator) endif() add_subdirectory(fm-viewer) +add_subdirectory(fm-editor) diff --git a/tools/fm-editor/CMakeLists.txt b/tools/fm-editor/CMakeLists.txt new file mode 100644 index 000000000..1a8b5d1a9 --- /dev/null +++ b/tools/fm-editor/CMakeLists.txt @@ -0,0 +1,58 @@ +set(LLVM_LINK_COMPONENTS Support Demangle Core) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +find_package( + QT + NAMES + Qt6 + Qt5 + REQUIRED + COMPONENTS Widgets +) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets) +include_directories(../../external/qsourcehighlite) +include_directories(.) + +add_vara_executable( + fm-editor + main.cpp + graph/FeatureModelGraph.h + graph/FeatureModelGraph.cpp + graph/FeatureNode.h + graph/FeatureNode.cpp + graph/FeatureEdge.h + graph/FeatureEdge.cpp + tree/FeatureTreeViewModel.h + tree/FeatureTreeViewModel.cpp + tree/FeatureTreeItem.h + tree/FeatureTreeItem.cpp + FeatureModelEditor.h + FeatureModelEditor.cpp + FeatureAddDialog.h + FeatureAddDialog.cpp + ../../external/qsourcehighlite/qsourcehighliter.h + ../../external/qsourcehighlite/qsourcehighliter.cpp + ../../external/qsourcehighlite/languagedata.h + ../../external/qsourcehighlite/languagedata.cpp + ../../external/qsourcehighlite/qsourcehighliterthemes.h + ../../external/qsourcehighlite/qsourcehighliterthemes.cpp +) +target_link_libraries( + fm-editor + LINK_PRIVATE + VaRAFeature + Qt${QT_VERSION_MAJOR}::Widgets + ${STD_FS_LIB} +) + +add_custom_target( + check-vara-fm-editor + COMMAND fm-editor xml/test.xml -viewer cat | dot | grep . -q + COMMENT "Unittests" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../../unittests/resources +) + +file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/fm-editor_autogen/.clang-tidy" + "Checks: '-*,llvm-twine-local' + " +) diff --git a/tools/fm-editor/FeatureAddDialog.cpp b/tools/fm-editor/FeatureAddDialog.cpp new file mode 100644 index 000000000..db7485d7b --- /dev/null +++ b/tools/fm-editor/FeatureAddDialog.cpp @@ -0,0 +1,101 @@ +#include "FeatureAddDialog.h" +#include "graph/FeatureModelGraph.h" +#include "graph/FeatureNode.h" + +using vara::feature::Feature; +FeatureAddDialog::FeatureAddDialog(FeatureModelGraph *Graph, QWidget *Parent, + Feature *ParentFeature) + : QDialog(Parent) { + setupUi(this); + NodeNames = QStringList(); + if (ParentFeature) { + NodeNames.push_back(QString::fromStdString(ParentFeature->getName().str())); + Nodes->setEnabled(false); + } else { + for (const auto &Node : *Graph->getNodes()) { + NodeNames.push_back(Node->getQName()); + } + } + this->Nodes->addItems(NodeNames); + this->FeatureKind->addItems(QStringList{"Binary", "Numeric"}); + stepOperator->addItems(QStringList{"Add +", "Multiply *", "Exp ^"}); + NumericFeature->setVisible(false); + connect(FeatureKind, QOverload::of(&QComboBox::activated), this, + &FeatureAddDialog::featureType); +} + +QString FeatureAddDialog::getName() const { return name->text(); } + +QString FeatureAddDialog::getParent() const { return Nodes->currentText(); } + +void FeatureAddDialog::featureType(int Index) { + if (Index == 1) { + NumericFeature->setVisible(true); + } else { + NumericFeature->setVisible(false); + } +} + +vara::feature::Feature::FeatureKind FeatureAddDialog::getFeatureKind() { + return vara::feature::Feature::FeatureKind(FeatureKind->currentIndex()); +} + +bool FeatureAddDialog::isOptional() const { return optionalCheck->isChecked(); } + +QString FeatureAddDialog::getOutputString() const { + return outputString->text(); +} + +std::vector stringToIntVector(string &Input) { + std::stringstream InStream(Input); + std::vector Out{}; + + for (std::string Substring; std::getline(InStream, Substring, ',');) { + Out.push_back(std::stoi(Substring)); + } + return Out; +} + +/// Retrieve the Feature defined by the dialog this should only be called after +/// the dialog was accepted +std::unique_ptr FeatureAddDialog::getFeature() { + const std::string Name = getName().toStdString(); + const bool Optional = isOptional(); + const std::string OutputString = getOutputString().toStdString(); + switch (getFeatureKind()) { + case Feature::FeatureKind::FK_BINARY: + return std::make_unique( + Name, Optional, std::vector(), + OutputString); + case Feature::FeatureKind::FK_NUMERIC: { + return getNumericFeature(); + } + default: + return std::make_unique(Name); + } +} + +std::unique_ptr FeatureAddDialog::getNumericFeature() const { + const std::string Name = getName().toStdString(); + const bool Optional = isOptional(); + const std::string OutputString = getOutputString().toStdString(); + std::unique_ptr SF{}; + vara::feature::NumericFeature::ValuesVariantType ValueRange; + + if (range->isChecked()) { + ValueRange = vara::feature::NumericFeature::ValueRangeType(min->value(), + max->value()); + if (lhs->isChecked()) { + SF = std::make_unique( + stepOperand->value(), vara::feature::StepFunction::StepOperation( + stepOperator->currentIndex())); + } + } else { + auto ValueString = values->text().toStdString(); + ValueRange = stringToIntVector(ValueString); + } + return std::make_unique( + Name, ValueRange, Optional, + std::vector(), OutputString, + std::move(SF)); +} diff --git a/tools/fm-editor/FeatureAddDialog.h b/tools/fm-editor/FeatureAddDialog.h new file mode 100644 index 000000000..7933397e3 --- /dev/null +++ b/tools/fm-editor/FeatureAddDialog.h @@ -0,0 +1,32 @@ +#ifndef VARA_FEATURE_FEATUREADDDIALOG_H +#define VARA_FEATURE_FEATUREADDDIALOG_H + +#include "graph/FeatureModelGraph.h" +#include "ui_FeatureAddDialog.h" + +#include +#include + +class FeatureAddDialog : public QDialog, public Ui::Add { + Q_OBJECT + +public: + FeatureAddDialog(FeatureModelGraph *Graph, QWidget *Parent, + vara::feature::Feature *ParentFeature = nullptr); + QString getName() const; + QString getParent() const; + QString getOutputString() const; + std::unique_ptr getFeature(); + vara::feature::Feature::FeatureKind getFeatureKind(); + bool isOptional() const; + +public slots: + void featureType(int Index); + +private: + QStringList NodeNames; + + std::unique_ptr getNumericFeature() const; +}; + +#endif // VARA_FEATURE_FEATUREADDDIALOG_H diff --git a/tools/fm-editor/FeatureAddDialog.ui b/tools/fm-editor/FeatureAddDialog.ui new file mode 100644 index 000000000..fd27514d7 --- /dev/null +++ b/tools/fm-editor/FeatureAddDialog.ui @@ -0,0 +1,238 @@ + + + Add + + + + 0 + 0 + 410 + 392 + + + + + 0 + 0 + + + + Dialog + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + Parent + + + + + + + + + + Optional + + + + + + + FeatureType + + + + + + + + + + Output String + + + + + + + + + + + + + Name + + + + + + + + + + Range + + + + + + + true + + + 0 + + + + + + + Values + + + + + + + 1,2,10 + + + + + + + + + + + + + 100000000 + + + 0 + + + + + + + Min + + + + + + + 10000000.000000000000000 + + + 1.000000000000000 + + + + + + + 1000000000 + + + + + + + StepFunction + + + + + + + + + + Max + + + + + + + Value is Left Operand + + + + + + + + + + + + + + + + buttonBox + accepted() + Add + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Add + reject() + + + 316 + 260 + + + 286 + 274 + + + + + range + stateChanged(int) + NumericValues + setCurrentIndex(int) + + + 51 + 232 + + + 239 + 232 + + + + + diff --git a/tools/fm-editor/FeatureModelEditor.cpp b/tools/fm-editor/FeatureModelEditor.cpp new file mode 100644 index 000000000..8aff92af5 --- /dev/null +++ b/tools/fm-editor/FeatureModelEditor.cpp @@ -0,0 +1,371 @@ +#include "FeatureModelEditor.h" +#include "FeatureAddDialog.h" +#include "graph/FeatureModelGraph.h" +#include "graph/FeatureNode.h" +#include "ui_FeatureModelEditor.h" +#include "vara/Feature/FeatureModel.h" +#include "vara/Feature/FeatureModelTransaction.h" +#include "vara/Feature/FeatureModelWriter.h" + +#include +#include +#include + +using Transaction = vara::feature::FeatureModelTransaction< + vara::feature::detail::ModifyTransactionMode>; +using vara::feature::Feature; + +FeatureModelEditor::FeatureModelEditor(QWidget *Parent) + : QMainWindow(Parent), Ui(new Ui::FeatureModelEditor) { + + Ui->setupUi(this); + Ui->textEdit->setReadOnly(true); + Highlighter = std::make_unique( + Ui->textEdit->document()); + Highlighter->setCurrentLanguage(QSourceHighlite::QSourceHighliter::CodeCpp); + QObject::connect(Ui->loadModel, &QPushButton::pressed, this, + &FeatureModelEditor::loadGraph); + QObject::connect(Ui->actionAddFeature, &QAction::triggered, this, + &FeatureModelEditor::featureAddDialog); + connect(Ui->actionSave, &QAction::triggered, this, &FeatureModelEditor::save); + connect(Ui->actionSaveAs, &QAction::triggered, this, + &FeatureModelEditor::saveAs); + connect(Ui->addSource, &QPushButton::pressed, this, + &FeatureModelEditor::addSource); + connect(Ui->addSourceFile, &QPushButton::pressed, this, + &FeatureModelEditor::addSourceFile); + connect(Ui->actionCreateNewFM, &QAction::triggered, this, + &FeatureModelEditor::createNewModel); +} + +/// Display the information of a Feature +void FeatureModelEditor::loadFeature(const vara::feature::Feature *Feature) { + Ui->featureInfo->setText(QString::fromStdString(Feature->toString())); +} + +/// Get a Feature from an Index of the TreeView and display its information. +void FeatureModelEditor::loadFeatureFromSelection( + const QItemSelection &Selection) { + if (Selection.size() != 1) { + return; + } + + auto Index = Selection.indexes().at(0); + + if (Index.isValid()) { + auto *Item = static_cast(Index.internalPointer()) + ->child(Index.row()); + if (Item->getKind() == ItemKind::IK_Feature) { + loadFeature(dynamic_cast(Item)->getFeature()); + } + } +} + +/// Clear all Fields that should be emptied when loading a new Model +void FeatureModelEditor::clean() { + SavePath.clear(); + Repository.clear(); + Ui->tabWidget->clear(); + Ui->sources->clear(); + Ui->textEdit->clear(); + Ui->sourcesLable->setText("Sources for: "); + Ui->featureInfo->clear(); +} + +/// Load the Feature Model at the Path in ModelFile field and build the Views +void FeatureModelEditor::loadGraph() { + clean(); + ModelPath = Ui->ModelFile->text(); + if (ModelPath.isEmpty()) { + QString const Path = QFileDialog::getOpenFileName( + this, tr("Open Model"), + QString::fromStdString(std::filesystem::current_path().string()), + tr("XML files (*.xml)")); + if (Path.isEmpty()) { + return; + } + + ModelPath = SavePath = Path; + Ui->ModelFile->setText(Path); + FeatureModel = vara::feature::loadFeatureModel(Path.toStdString()); + } else { + FeatureModel = vara::feature::loadFeatureModel(ModelPath.toStdString()); + } + if (!FeatureModel) { + return; + } + // create Graph view + buildGraph(); + + // create Tree View + buildTree(); + + Ui->tabWidget->addTab(Graph.get(), "GraphView"); + Ui->tabWidget->addTab(TreeView.get(), "TreeView"); + connect(Ui->sources, &QComboBox::currentTextChanged, this, + &FeatureModelEditor::loadSource); + Ui->actionSave->setEnabled(true); + Ui->actionSaveAs->setEnabled(true); + Ui->actionAddFeature->setEnabled(true); +} + +/// Build the Treeview +void FeatureModelEditor::buildTree() { + if (!TreeView) { + TreeView = std::make_unique(this); + } + TreeModel = std::make_unique(FeatureModel.get(), + TreeView.get()); + for (auto &Item : *TreeModel->getItems()) { + connect(Item.get(), &FeatureTreeItem::inspectSource, this, + &FeatureModelEditor::inspectFeatureSources); + connect(Item.get(), &FeatureTreeItem::addChildFeature, this, + &FeatureModelEditor::featureAddDialogChild); + connect(Item.get(), &FeatureTreeItem::removeFeature, this, + &FeatureModelEditor::removeFeature); + } + TreeView->setModel(TreeModel.get()); + TreeView->setContextMenuPolicy(Qt::CustomContextMenu); + connect(TreeView.get(), SIGNAL(customContextMenuRequested(QPoint)), this, + SLOT(createTreeContextMenu(QPoint))); + connect(TreeView->selectionModel(), &QItemSelectionModel::selectionChanged, + this, &FeatureModelEditor::loadFeatureFromSelection); +} + +/// Build the graph view +void FeatureModelEditor::buildGraph() { + Graph = std::make_unique(FeatureModel.get()); + for (auto &Node : *Graph->getNodes()) { + QObject::connect(Node, &FeatureNode::clicked, this, + &FeatureModelEditor::loadFeature); + QObject::connect(Node, &FeatureNode::inspectSource, this, + &FeatureModelEditor::inspectFeatureSources); + } +} + +void FeatureModelEditor::featureAddDialog() { featureAddDialogChild(nullptr); } + +/// Spawn a Dialog to select data to add a Feature +void FeatureModelEditor::featureAddDialogChild(Feature *ParentFeature) { + FeatureAddDialog AddDialog(Graph.get(), this, ParentFeature); + if (AddDialog.exec() == QDialog::Accepted) { + Feature *Parent = + FeatureModel->getFeature(AddDialog.getParent().toStdString()); + auto NewFeature = AddDialog.getFeature(); + + auto *NewNode = Graph->addNode(NewFeature.get(), + Graph->getNode(Parent->getName().str())); + auto *NewTreeItem = + TreeModel->addFeature(NewFeature.get(), Parent->getName().str()); + connect(NewTreeItem, &FeatureTreeItem::inspectSource, this, + &FeatureModelEditor::inspectFeatureSources); + connect(NewNode, &FeatureNode::clicked, this, + &FeatureModelEditor::loadFeature); + connect(NewNode, &FeatureNode::inspectSource, this, + &FeatureModelEditor::inspectFeatureSources); + connect(NewTreeItem, &FeatureTreeItem::addChildFeature, this, + &FeatureModelEditor::featureAddDialogChild); + connect(NewTreeItem, &FeatureTreeItem::removeFeature, this, + &FeatureModelEditor::removeFeature); + auto Transaction = vara::feature::FeatureModelTransaction< + vara::feature::detail::ModifyTransactionMode>:: + openTransaction(*FeatureModel); + auto PotentialRelations = + Parent->getChildren(1); + if (!PotentialRelations.empty()) { + Transaction.addFeature(std::move(NewFeature), + *PotentialRelations.begin()); + } else { + Transaction.addFeature(std::move(NewFeature), Parent); + } + Transaction.commit(); + } +} + +void FeatureModelEditor::removeFeature(bool Recursive, Feature *Feature) { + TreeModel->deleteFeatureItem(Recursive, Feature); + Graph->deleteNode(Recursive, Feature); + Ui->featureInfo->clear(); + vara::feature::removeFeature(*FeatureModel, Feature, Recursive); +} + +/// Save the current State of the Feature Model +void FeatureModelEditor::save() { + if (SavePath.isEmpty()) { + SavePath = QFileDialog::getSaveFileName(this, tr("Save File"), ModelPath, + tr("XML files (*.xml)")); + } + vara::feature::FeatureModelXmlWriter FMWrite{*FeatureModel}; + FMWrite.writeFeatureModel(SavePath.toStdString()); +} + +void FeatureModelEditor::saveAs() { + auto Path = QFileDialog::getSaveFileName(this, tr("Save File"), ModelPath, + tr("XML files (*.xml)")); + if (!Path.isEmpty()) { + SavePath = Path; + save(); + } +} + +/// Load the source files of the Feature to be selectable by the user and set +/// the Feature as CurrentFeature. +/// +/// \param Feature Selected Feature +void FeatureModelEditor::inspectFeatureSources( + vara::feature::Feature *Feature) { + CurrentFeature = Feature; + if (Repository.isEmpty()) { + Repository = QFileDialog::getExistingDirectory( + this, tr("Select Repository"), + QString::fromStdString(std::filesystem::current_path().string())); + } + Ui->sources->clear(); + QSet Locations = {}; + for (const auto &Source : Feature->getLocations()) { + Locations.insert(QString::fromStdString(Source.getPath().string())); + } + Ui->sources->addItems(Locations.values()); + Ui->sourcesLable->setText( + QString::fromStdString("Sources for: " + Feature->getName().str())); + if (Ui->sources->count() == 1) { + loadSource(Ui->sources->itemText(0)); + } else { + Ui->sources->setPlaceholderText("Select File"); + } +} + +/// Create the Context menu for inspecting sources in the tree view +/// +/// \param Pos Position of the cursor used to find the clicked item +void FeatureModelEditor::createTreeContextMenu(const QPoint &Pos) { + auto Index = TreeView->indexAt(Pos); + if (Index.isValid()) { + FeatureTreeItem *Item = + static_cast(Index.internalPointer()) + ->child(Index.row()); + Item->contextMenu(TreeView->mapToGlobal(Pos)); + } +} + +/// Load the selected file into the textedit and mark the sources of the +/// selected feature +/// +/// \param RelativePath path to the source file relative to the repository path +void FeatureModelEditor::loadSource(const QString &RelativePath) { + Ui->textEdit->clear(); + auto SourcePath = Repository + "/" + RelativePath; + QFile File(SourcePath); + if (File.exists()) { + File.open(QFile::ReadOnly | QFile::Text); + QTextStream ReadFile(&File); + Ui->textEdit->setLineWrapMode(QTextEdit::LineWrapMode::NoWrap); + Ui->textEdit->setText(ReadFile.readAll()); + std::vector Locations{}; + std::copy_if( + CurrentFeature->getLocationsBegin(), CurrentFeature->getLocationsEnd(), + std::back_inserter(Locations), [&RelativePath](auto const &Loc) { + return RelativePath.toStdString() == Loc.getPath(); + }); + for (auto &Location : Locations) { + markLocation(Location); + } + } +} +void setCursorLineAndColumn(QTextCursor &Cursor, int Line, int Col, + QTextCursor::MoveMode Mode) { + QTextBlock const B = Cursor.document()->findBlockByLineNumber(Line); + Cursor.setPosition(B.position() + Col, Mode); +} + +/// Mark the given SourceRange with the given Format +/// +/// \param Fmt Format to mark with +/// \param Cursor Cursor +/// \param Location Location to mark +void FeatureModelEditor::markLocation( + vara::feature::FeatureSourceRange &Location) const { + QTextCharFormat Fmt; + Fmt.setBackground(Qt::darkYellow); + QTextCursor Cursor(Ui->textEdit->document()); + Cursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor); + setCursorLineAndColumn(Cursor, int(Location.getStart()->getLineNumber()) - 1, + int(Location.getStart()->getColumnOffset()) - 1, + QTextCursor::MoveAnchor); + setCursorLineAndColumn(Cursor, int(Location.getEnd()->getLineNumber()) - 1, + int(Location.getEnd()->getColumnOffset()) - 1, + QTextCursor::KeepAnchor); + Cursor.setCharFormat(Fmt); +} + +/// Load a sourcefile to then add a location from it +void FeatureModelEditor::addSourceFile() { + if (!Repository.isEmpty()) { + QString const Path = QFileDialog::getOpenFileName( + this, tr("Select Source File"), Repository, + tr("C Files (*.c *c++ *.h) ;; All Files (*.*)")); + Ui->sources->addItem(Path.mid(Repository.length())); + Ui->sources->setCurrentIndex(Ui->sources->count() - 1); + } +} + +/// Add the user selected Part of the textedit as a source for the active +/// Feature +void FeatureModelEditor::addSource() { + if (Ui->sources->currentText().isEmpty()) { + return; + } + + auto *TextEdit = Ui->textEdit; + auto Cursor = TextEdit->textCursor(); + int Start = Cursor.selectionStart(); + int End = Cursor.selectionEnd(); + + if (Start == End) { + return; + } + + if (Start > End) { + std::swap(Start, End); + } + Cursor.movePosition(QTextCursor::MoveOperation::StartOfLine); + const int LineStart = Cursor.position(); + int Lines = 1; + auto Block = Cursor.block(); + while (Cursor.position() > Block.position()) { + Cursor.movePosition(QTextCursor::MoveOperation::Up); + Lines++; + } + Block = Block.previous(); + while (Block.isValid()) { + Lines += Block.lineCount(); + Block = Block.previous(); + } + auto Range = vara::feature::FeatureSourceRange( + Ui->sources->currentText().toStdString(), + vara::feature::FeatureSourceRange::FeatureSourceLocation( + Lines, Start - LineStart + 1), + vara::feature::FeatureSourceRange::FeatureSourceLocation( + Lines, End - LineStart + 1)); + auto LocationTransAction = Transaction::openTransaction(*FeatureModel); + LocationTransAction.addLocation(CurrentFeature, Range); + LocationTransAction.commit(); + markLocation(Range); +} + +void FeatureModelEditor::createNewModel() { + FeatureModel = std::make_unique(); + + // create Graph view + buildGraph(); + + // create Tree View + buildTree(); + + Ui->tabWidget->addTab(Graph.get(), "GraphView"); + Ui->tabWidget->addTab(TreeView.get(), "TreeView"); + connect(Ui->sources, &QComboBox::currentTextChanged, this, + &FeatureModelEditor::loadSource); + Ui->actionSave->setEnabled(true); + Ui->actionAddFeature->setEnabled(true); +} diff --git a/tools/fm-editor/FeatureModelEditor.h b/tools/fm-editor/FeatureModelEditor.h new file mode 100644 index 000000000..add54ae30 --- /dev/null +++ b/tools/fm-editor/FeatureModelEditor.h @@ -0,0 +1,64 @@ +#ifndef VARA_FEATURE_FEATUREMODELEDITOR_H +#define VARA_FEATURE_FEATUREMODELEDITOR_H + +#include "graph/FeatureModelGraph.h" +#include "qsourcehighliter.h" +#include "tree/FeatureTreeViewModel.h" +#include "vara/Feature/Feature.h" +#include "vara/Feature/FeatureModel.h" +#include "vara/Feature/FeatureModelTransaction.h" + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE +namespace Ui { +class FeatureModelEditor; +} // namespace Ui +QT_END_NAMESPACE + +class FeatureModelEditor : public QMainWindow { + Q_OBJECT + +public: + explicit FeatureModelEditor(QWidget *Parent = nullptr); + ~FeatureModelEditor() override = default; + +private: + Ui::FeatureModelEditor *Ui; + std::unique_ptr Graph{}; + std::unique_ptr TreeView; + std::unique_ptr TreeModel{}; + std::unique_ptr FeatureModel{}; + QString Repository{}; + vara::feature::Feature *CurrentFeature = nullptr; + QString SavePath{}; + QString ModelPath{}; + +public slots: + void addSource(); + void loadFeature(const vara::feature::Feature *Feature); + void inspectFeatureSources(vara::feature::Feature *Feature); + void loadGraph(); + void featureAddDialogChild(vara::feature::Feature * = nullptr); + void loadSource(const QString &RelativePath); + void createTreeContextMenu(const QPoint &Pos); + void addSourceFile(); + void loadFeatureFromSelection(const QItemSelection &Selection); + void save(); + void saveAs(); + void featureAddDialog(); + void removeFeature(bool Recursive, vara::feature::Feature *Feature); + void createNewModel(); + +private: + void clean(); + void buildGraph(); + void buildTree(); + void markLocation(vara::feature::FeatureSourceRange &Location) const; + std::unique_ptr Highlighter; +}; + +#endif // VARA_FEATURE_FEATUREMODELEDITOR_H diff --git a/tools/fm-editor/FeatureModelEditor.ui b/tools/fm-editor/FeatureModelEditor.ui new file mode 100644 index 000000000..2538eacf1 --- /dev/null +++ b/tools/fm-editor/FeatureModelEditor.ui @@ -0,0 +1,236 @@ + + + FeatureModelEditor + + + + 0 + 0 + 943 + 896 + + + + MainWindow + + + + + + + + 0 + 0 + + + + + 500 + 0 + + + + + + + QTextEdit::NoWrap + + + + + + + AddSource + + + + + + + + + + AddSourceFile + + + + + + + Sources for: + + + + + + + + + + + 200 + 0 + + + + + + + + 0 + 100 + + + + + 16777215 + 100 + + + + true + + + + + + + -1 + + + + + + + + 16777215 + 50 + + + + + + + + + + + + + Feature Model Path + + + true + + + + + + + Load + + + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 40 + 20 + + + + + + + + + + 0 + 0 + 943 + 34 + + + + + Edit + + + + + + + + + + + + false + + + AddFeature + + + + + true + + + Create New Model + + + + + false + + + Save + + + Ctrl+S + + + + + false + + + Save As + + + Ctrl+Alt+S + + + + + + diff --git a/tools/fm-editor/graph/FeatureEdge.cpp b/tools/fm-editor/graph/FeatureEdge.cpp new file mode 100644 index 000000000..6c218f342 --- /dev/null +++ b/tools/fm-editor/graph/FeatureEdge.cpp @@ -0,0 +1,74 @@ +#include "FeatureEdge.h" +#include "FeatureNode.h" + +#include +#include + +FeatureEdge::FeatureEdge(FeatureNode *SourceNode, FeatureNode *TargetNode) + : Source(SourceNode), Target(TargetNode) { + setAcceptedMouseButtons(Qt::NoButton); + Source->addChildEdge(this); + TargetNode->setParentEdge(this); + adjust(); +} + +FeatureNode *FeatureEdge::sourceNode() const { return Source; } + +FeatureNode *FeatureEdge::targetNode() const { return Target; } + +void FeatureEdge::adjust() { + if (!Source || !Target) { + return; + } + + QLineF Line(mapFromItem(Source, 0, 10), mapFromItem(Target, 0, -10)); + qreal Length = Line.length(); + + prepareGeometryChange(); + + if (Length > qreal(20.)) { + SourcePoint = Line.p1(); + TargetPoint = Line.p2(); + } else { + SourcePoint = TargetPoint = Line.p1(); + } +} + +QRectF FeatureEdge::boundingRect() const { + if (!Source || !Target) { + return {}; + } + + qreal PenWidth = 1; + qreal Extra = (PenWidth + ArrowSize) / 2.0; + + return QRectF(SourcePoint, QSizeF(TargetPoint.x() - SourcePoint.x(), + TargetPoint.y() - SourcePoint.y())) + .normalized() + .adjusted(-Extra, -Extra, Extra, Extra); +} + +void FeatureEdge::paint(QPainter *Painter, + const QStyleOptionGraphicsItem *Option, + QWidget *Widget) { + if (!Source || !Target) { + return; + } + + QLineF Line(SourcePoint, TargetPoint); + + if (qFuzzyCompare(Line.length(), qreal(0.))) { + return; + } + + Painter->setPen( + QPen(Qt::black, 1, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); + Painter->drawLine(Line); + Painter->setBrush(QBrush(Target->isOptional() ? Qt::white : Qt::black)); + Painter->drawEllipse(TargetPoint, 4, 4); +} + +void FeatureEdge::setSourceNode(FeatureNode *Node) { + Source = Node; + Node->addChildEdge(this); +} diff --git a/tools/fm-editor/graph/FeatureEdge.h b/tools/fm-editor/graph/FeatureEdge.h new file mode 100644 index 000000000..b10d85938 --- /dev/null +++ b/tools/fm-editor/graph/FeatureEdge.h @@ -0,0 +1,32 @@ +#ifndef VARA_FEATURE_FEATUREEDGE_H +#define VARA_FEATURE_FEATUREEDGE_H + +#include + +class FeatureNode; + +class FeatureEdge : public QGraphicsItem { +public: + FeatureEdge(FeatureNode *SourceNode, FeatureNode *TargetNode); + + [[nodiscard]] FeatureNode *sourceNode() const; + [[nodiscard]] FeatureNode *targetNode() const; + void setSourceNode(FeatureNode *Node); + void adjust(); + enum { Type = UserType + 2 }; + [[nodiscard]] int type() const override { return Type; } + +protected: + QRectF boundingRect() const override; + void paint(QPainter *Painter, const QStyleOptionGraphicsItem *Option, + QWidget *Widget) override; + +private: + FeatureNode *Source, *Target; + + QPointF SourcePoint; + QPointF TargetPoint; + qreal ArrowSize = 10; +}; + +#endif // VARA_FEATURE_FEATUREEDGE_H diff --git a/tools/fm-editor/graph/FeatureModelGraph.cpp b/tools/fm-editor/graph/FeatureModelGraph.cpp new file mode 100644 index 000000000..12914a7e2 --- /dev/null +++ b/tools/fm-editor/graph/FeatureModelGraph.cpp @@ -0,0 +1,216 @@ +#include "FeatureModelGraph.h" +#include "vara/Feature/Feature.h" +#include "vara/Feature/FeatureModel.h" +#include "vara/Feature/FeatureModelTransaction.h" + +#include +#include + +#include + +using vara::feature::Feature; + +FeatureModelGraph::FeatureModelGraph(vara::feature::FeatureModel *FeatureModel, + QWidget *Parent) + : QGraphicsView(Parent), + EntryNode(new FeatureNode(FeatureModel->getRoot())), + FeatureModel(FeatureModel) { + + Scene = std::make_unique(this); + Scene->setItemIndexMethod(QGraphicsScene::NoIndex); + setScene(Scene.get()); + setCacheMode(CacheBackground); + setViewportUpdateMode(BoundingRectViewportUpdate); + setRenderHint(QPainter::Antialiasing); + setTransformationAnchor(AnchorUnderMouse); + scale(qreal(0.8), qreal(0.8)); + reload(); + setDragMode(ScrollHandDrag); + Scene->setSceneRect(0, 0, EntryNode->childrenWidth() + 100, + 100 * EntryNode->childrenDepth() + 100); +} + +void FeatureModelGraph::reload() { + Nodes.push_back(EntryNode); + auto *Scene = this->scene(); + Scene->clear(); + Scene->addItem(EntryNode); + buildRec(EntryNode); + auto NextChildren = std::vector(EntryNode->children().size()); + auto CurrentChildren = EntryNode->children(); + std::transform(CurrentChildren.begin(), CurrentChildren.end(), + NextChildren.begin(), + [](FeatureEdge *Edge) { return Edge->targetNode(); }); + positionRec(1, NextChildren, 5); + EntryNode->setPos(EntryNode->childrenWidth() / 2.0, 10); +} + +void FeatureModelGraph::buildRec(FeatureNode *CurrentFeatureNode) { + for (auto *Feature : + CurrentFeatureNode->getFeature()->getChildren( + 1)) { + auto *Node = new FeatureNode(Feature); + auto *Edge = new FeatureEdge(CurrentFeatureNode, Node); + scene()->addItem(Edge); + scene()->addItem(Node); + Nodes.push_back(Node); + buildRec(Node); + } + for (auto *Relation : CurrentFeatureNode->getFeature() + ->getChildren(1)) { + for (auto *Feature : Relation->getChildren(1)) { + auto *Node = new FeatureNode(Feature); + auto *Edge = new FeatureEdge(CurrentFeatureNode, Node); + scene()->addItem(Edge); + scene()->addItem(Node); + buildRec(Node); + Nodes.push_back(Node); + } + } +} + +int FeatureModelGraph::positionRec(const int CurrentDepth, + const std::vector &Children, + const unsigned long Offset) { + if (Children.empty()) { + return CurrentDepth - 1; + } + + int MaxDepth = CurrentDepth; + auto NextOffset = Offset; + for (FeatureNode *Node : Children) { + auto NextChildren = std::vector(Node->children().size()); + auto CurrentChildren = Node->children(); + std::transform(CurrentChildren.begin(), CurrentChildren.end(), + NextChildren.begin(), + [](FeatureEdge *Edge) { return Edge->targetNode(); }); + int const Depth = positionRec(CurrentDepth + 1, NextChildren, NextOffset); + int const Width = Node->childrenWidth(); + Node->setPos(double(NextOffset) + Width / 2.0, 100 * CurrentDepth); + NextOffset += Width; + MaxDepth = MaxDepth < Depth ? Depth : MaxDepth; + } + + return MaxDepth; +} + +void FeatureModelGraph::keyPressEvent(QKeyEvent *Event) { + switch (Event->key()) { + case Qt::Key_Plus: + zoomIn(); + break; + case Qt::Key_Minus: + zoomOut(); + break; + default: + QGraphicsView::keyPressEvent(Event); + } +} + +#if QT_CONFIG(wheelevent) +void FeatureModelGraph::wheelEvent(QWheelEvent *Event) { + scaleView(pow(2., -Event->angleDelta().y() / 240.0)); +} +#endif + +void FeatureModelGraph::drawBackground(QPainter *Painter, const QRectF &Rect) { + Q_UNUSED(Rect) + + // Shadow + const QRectF SceneRect = this->sceneRect(); + const QRectF RightShadow(SceneRect.right(), SceneRect.top() + 5, 5, + SceneRect.height()); + const QRectF BottomShadow(SceneRect.left() + 5, SceneRect.bottom(), + SceneRect.width(), 5); + if (RightShadow.intersects(Rect) || RightShadow.contains(Rect)) { + Painter->fillRect(RightShadow, Qt::darkGray); + } + if (BottomShadow.intersects(Rect) || BottomShadow.contains(Rect)) { + Painter->fillRect(BottomShadow, Qt::darkGray); + } + + // Fill + QLinearGradient Gradient(SceneRect.topLeft(), SceneRect.bottomRight()); + Gradient.setColorAt(0, Qt::white); + Gradient.setColorAt(1, Qt::lightGray); + Painter->fillRect(Rect.intersected(SceneRect), Gradient); + Painter->setBrush(Qt::NoBrush); + Painter->drawRect(SceneRect); + + QFont Font = Painter->font(); + Font.setBold(true); + Font.setPointSize(14); + Painter->setFont(Font); + Painter->setPen(Qt::lightGray); + Painter->setPen(Qt::black); +} + +void FeatureModelGraph::scaleView(qreal ScaleFactor) { + const qreal Factor = transform() + .scale(ScaleFactor, ScaleFactor) + .mapRect(QRectF(0, 0, 1, 1)) + .width(); + if (Factor < 0.3 || Factor > 5) { + return; + } + + scale(ScaleFactor, ScaleFactor); +} + +void FeatureModelGraph::zoomIn() { scaleView(qreal(1.2)); } + +void FeatureModelGraph::zoomOut() { scaleView(1 / qreal(1.2)); } + +FeatureNode *FeatureModelGraph::addNode(Feature *Feature, FeatureNode *Parent) { + auto *NewNode = new FeatureNode(Feature); + auto *NewEdge = new FeatureEdge(Parent, NewNode); + scene()->addItem(NewEdge); + scene()->addItem(NewNode); + Nodes.push_back(NewNode); + auto NextChildren = std::vector(EntryNode->children().size()); + auto CurrentChildren = EntryNode->children(); + std::transform(CurrentChildren.begin(), CurrentChildren.end(), + NextChildren.begin(), + [](FeatureEdge *Edge) { return Edge->targetNode(); }); + positionRec(1, NextChildren, 5); + EntryNode->setPos(EntryNode->childrenWidth() / 2.0, 10); + + return NewNode; +} + +FeatureNode *FeatureModelGraph::getNode(std::string Name) { + auto It = std::find_if(Nodes.begin(), Nodes.end(), [&Name](const auto &Node) { + return Node->getName() == Name; + }); + if (It != Nodes.end()) { + return *It; + } + + return nullptr; +} + +void FeatureModelGraph::deleteNode(bool Recursive, FeatureNode *Node) { + auto *Parent = Node->parent()->sourceNode(); + if (Recursive) { + for (auto *Child : Node->children()) { + deleteNode(true, Child->targetNode()); + } + } else { + for (auto *Child : Node->children()) { + Child->setSourceNode(Parent); + } + + Node->children().clear(); + } + Parent->removeChild(Node); + scene()->removeItem(Node); + scene()->removeItem(Node->parent()); + + Nodes.erase(std::find_if(Nodes.begin(), Nodes.end(), + [Node](auto &N) { return N == Node; })); +} + +void FeatureModelGraph::deleteNode(bool Recursive, + vara::feature::Feature *Feature) { + deleteNode(Recursive, getNode(Feature->getName().str())); +} diff --git a/tools/fm-editor/graph/FeatureModelGraph.h b/tools/fm-editor/graph/FeatureModelGraph.h new file mode 100644 index 000000000..b36bb2a45 --- /dev/null +++ b/tools/fm-editor/graph/FeatureModelGraph.h @@ -0,0 +1,48 @@ +#ifndef VARA_FEATURE_FEATUREMODELGRAPH_H +#define VARA_FEATURE_FEATUREMODELGRAPH_H + +#include "FeatureEdge.h" +#include "FeatureNode.h" +#include "vara/Feature/FeatureModel.h" + +#include + +class FeatureModelGraph : public QGraphicsView { + Q_OBJECT + +public: + FeatureModelGraph(vara::feature::FeatureModel *FeatureModel, + QWidget *Parent = nullptr); + auto getNodes() { return &Nodes; }; + FeatureNode *getNode(std::string Name); + FeatureNode *addNode(vara::feature::Feature *Feature, FeatureNode *Parent); + void deleteNode(bool Recursive, vara::feature::Feature *Feature); + void deleteNode(bool Recursive, FeatureNode *Node); + +public slots: + void zoomIn(); + void zoomOut(); + +protected: + void keyPressEvent(QKeyEvent *Event) override; + +#if QT_CONFIG(wheelevent) + void wheelEvent(QWheelEvent *Event) override; +#endif + + void drawBackground(QPainter *Painter, const QRectF &Rect) override; + + void scaleView(qreal ScaleFactor); + +private: + void reload(); + void buildRec(FeatureNode *CurrentFeatureNode); + FeatureNode *EntryNode; + int positionRec(int CurrentDepth, const std::vector &Children, + unsigned long Offset); + vara::feature::FeatureModel *FeatureModel; + std::vector Nodes; + std::unique_ptr Scene; +}; + +#endif // VARA_FEATURE_FEATUREMODELGRAPH_H diff --git a/tools/fm-editor/graph/FeatureNode.cpp b/tools/fm-editor/graph/FeatureNode.cpp new file mode 100644 index 000000000..8a8db6590 --- /dev/null +++ b/tools/fm-editor/graph/FeatureNode.cpp @@ -0,0 +1,143 @@ +#include "FeatureNode.h" +#include "FeatureEdge.h" +#include "FeatureModelGraph.h" + +#include +#include +#include +#include + +FeatureNode::FeatureNode(vara::feature::Feature *Feature) : Feature(Feature) { + setFlag(ItemIsMovable); + setFlag(ItemSendsGeometryChanges); + setCacheMode(DeviceCoordinateCache); + setZValue(-1); + ContextMenu = std::make_unique(); + ContextMenu->addAction("Inspect Sources", this, &FeatureNode::inspect); +} + +void FeatureNode::addChildEdge(FeatureEdge *Edge) { + ChildEdges.push_back(Edge); + Edge->adjust(); +} + +void FeatureNode::setParentEdge(FeatureEdge *Edge) { + ParentEdge = Edge; + Edge->adjust(); +} + +std::vector FeatureNode::children() { return ChildEdges; } + +FeatureEdge *FeatureNode::parent() { return ParentEdge; } + +QRectF FeatureNode::boundingRect() const { + + const qreal Adjust = 2; + const int W = width(); + return {-(W + widthAdjust) / 2.0 - Adjust, -10 - Adjust, + W + widthAdjust + Adjust, 23 + 2 * Adjust}; +} + +QPainterPath FeatureNode::shape() const { + QPainterPath Path; + const int W = width(); + Path.addRect(-(W + widthAdjust) / 2.0, -10, W + widthAdjust, 20); + return Path; +} + +void FeatureNode::paint(QPainter *Painter, + const QStyleOptionGraphicsItem *Option, + QWidget *Widget) { + const auto Name = getQName(); + QBrush Brush(Qt::darkYellow); + if (Option->state & QStyle::State_Sunken) { + Brush.setColor(QColor(Qt::yellow).lighter(120)); + } + + Painter->setBrush(Brush); + Painter->setPen(QPen(Qt::black, 0)); + + int const W = width(); + Painter->drawRect(-(W + widthAdjust) / 2, -10, W + widthAdjust, 20); + Painter->setPen(QPen(Qt::black, 1)); + Painter->drawText(-W / 2, 5, Name); +} + +QVariant FeatureNode::itemChange(QGraphicsItem::GraphicsItemChange Change, + const QVariant &Value) { + switch (Change) { + case ItemPositionHasChanged: + for (FeatureEdge *Edge : std::as_const(ChildEdges)) { + Edge->adjust(); + } + if (ParentEdge) { + ParentEdge->adjust(); + } + break; + default: + break; + } + + return QGraphicsItem::itemChange(Change, Value); +} + +void FeatureNode::removeChild(FeatureNode *Child) { + auto EdgePos = + std::find_if(ChildEdges.begin(), ChildEdges.end(), + [Child](auto *Edge) { return Edge->targetNode() == Child; }); + if (EdgePos != ChildEdges.end()) { + ChildEdges.erase(EdgePos); + } +} + +void FeatureNode::mousePressEvent(QGraphicsSceneMouseEvent *Event) { + update(); + QGraphicsItem::mousePressEvent(Event); + emit clicked(Feature); +} + +void FeatureNode::mouseReleaseEvent(QGraphicsSceneMouseEvent *Event) { + update(); + QGraphicsItem::mouseReleaseEvent(Event); +} + +void FeatureNode::contextMenuEvent(QGraphicsSceneContextMenuEvent *Event) { + ContextMenu->popup(Event->screenPos()); +} +void FeatureNode::inspect() { emit(inspectSource(Feature)); } + +int FeatureNode::width() const { + const QFontMetrics Fm((QFont())); + + return Fm.boundingRect(getQName()).width(); +} + +int FeatureNode::childrenWidth() const { + if (ChildEdges.empty()) { + return width() + int(1.5 * widthAdjust); + } + + int Result = 0; + for (auto *Child : ChildEdges) { + Result += Child->targetNode()->childrenWidth(); + Result += widthAdjust / 2; + } + + return std::max(Result, width() + widthAdjust); +} + +int FeatureNode::childrenDepth() const { + if (ChildEdges.empty()) { + return 1; + } + + int MaxDepth = 0; + for (auto *Child : ChildEdges) { + int const ChildDepth = Child->targetNode()->childrenDepth(); + if (ChildDepth + 1 > MaxDepth) { + MaxDepth = ChildDepth + 1; + } + } + + return MaxDepth; +} diff --git a/tools/fm-editor/graph/FeatureNode.h b/tools/fm-editor/graph/FeatureNode.h new file mode 100644 index 000000000..c2e4ec7d5 --- /dev/null +++ b/tools/fm-editor/graph/FeatureNode.h @@ -0,0 +1,67 @@ +#ifndef VARA_FEATURE_FEATURENODE_H +#define VARA_FEATURE_FEATURENODE_H + +#include "FeatureNode.h" +#include "vara/Feature/Feature.h" + +#include +#include +#include +#include + +class FeatureEdge; +class FeatureModelGraph; + +class FeatureNode : public QObject, public QGraphicsItem { + Q_OBJECT + Q_INTERFACES(QGraphicsItem) +public: + void removeChild(FeatureNode *Child); + FeatureNode(vara::feature::Feature *Feature); + [[nodiscard]] int width() const; + void addChildEdge(FeatureEdge *Edge); + void setParentEdge(FeatureEdge *Edge); + [[nodiscard]] std::vector children(); + [[nodiscard]] FeatureEdge *parent(); + [[nodiscard]] int childrenWidth() const; + [[nodiscard]] int childrenDepth() const; + enum { Type = UserType + 1 }; + [[nodiscard]] int type() const override { return Type; } + vara::feature::Feature *getFeature() { return Feature; }; + [[nodiscard]] QRectF boundingRect() const override; + [[nodiscard]] QPainterPath shape() const override; + void paint(QPainter *Painter, const QStyleOptionGraphicsItem *Option, + QWidget *Widget) override; + bool isOptional() { return Feature->isOptional(); } + [[nodiscard]] QString getQName() const { + return QString::fromStdString(Feature->getName().str()); + }; + [[nodiscard]] std::string getName() const { + return Feature->getName().str(); + }; + ~FeatureNode() override { + std::destroy(ChildEdges.begin(), ChildEdges.end()); + } + +signals: + void clicked(const vara::feature::Feature *Feature); + void inspectSource(vara::feature::Feature *Feature); + +protected: + QVariant itemChange(GraphicsItemChange Change, + const QVariant &Value) override; + + void mousePressEvent(QGraphicsSceneMouseEvent *Event) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent *Event) override; + void contextMenuEvent(QGraphicsSceneContextMenuEvent *Event) override; + +private: + std::vector ChildEdges; + FeatureEdge *ParentEdge = nullptr; + vara::feature::Feature *Feature; + std::unique_ptr ContextMenu; + void inspect(); + const int widthAdjust = 10; +}; + +#endif // VARA_FEATURE_FEATURENODE_H diff --git a/tools/fm-editor/main.cpp b/tools/fm-editor/main.cpp new file mode 100644 index 000000000..2920654a4 --- /dev/null +++ b/tools/fm-editor/main.cpp @@ -0,0 +1,10 @@ +#include "FeatureModelEditor.h" + +#include + +int main(int argc, char **argv) { + const QApplication App(argc, argv); + FeatureModelEditor W; + W.show(); + return QApplication::exec(); +} diff --git a/tools/fm-editor/tree/FeatureTreeItem.cpp b/tools/fm-editor/tree/FeatureTreeItem.cpp new file mode 100644 index 000000000..792fb5f9e --- /dev/null +++ b/tools/fm-editor/tree/FeatureTreeItem.cpp @@ -0,0 +1,108 @@ +#include "FeatureTreeItem.h" + +#include + +#include + +QVariant numericValue(vara::feature::Feature *Item) { + if (Item->getKind() == vara::feature::Feature::FeatureKind::FK_NUMERIC) { + auto *NumItem = llvm::dyn_cast(Item); + if (NumItem != nullptr) { + string Result = "["; + if (std::holds_alternative( + NumItem->getValues())) { + auto Range = std::get( + NumItem->getValues()); + Result += std::to_string(Range.first) + ", " + + std::to_string(Range.second) + "]"; + } else { + auto Range = std::get( + NumItem->getValues()); + for (auto It = Range.begin(); It != Range.end(); It++) { + if (It != Range.begin()) { + Result += ","; + } + Result += std::to_string(*It.base()); + } + Result += "]"; + } + + return QString::fromStdString(Result); + } + } + + return {}; +} + +QVariant locationString(vara::feature::Feature *Item) { + auto Locs = Item->getLocations(); + std::stringstream StrS; + if (Item->hasLocations()) { + std::for_each(Locs.begin(), Locs.end(), + [&StrS](const vara::feature::FeatureSourceRange &Fsr) { + StrS << llvm::formatv("{0}; ", Fsr.toString()).str(); + }); + } + + return QString::fromStdString(StrS.str()); +} + +std::unique_ptr +FeatureTreeItem::createFeatureTreeItem(vara::feature::FeatureTreeNode *Item) { + if (Item->getKind() == + vara::feature::FeatureTreeNode::NodeKind::NK_RELATIONSHIP) { + return std::make_unique( + llvm::dyn_cast(Item)); + } + + return std::make_unique( + llvm::dyn_cast( + Item)); +} + +void FeatureTreeItem::addChild(FeatureTreeItem *Child) { + if (!Children.empty() && Children[0]->getKind() == ItemKind::IK_Relation) { + Children[0]->addChild(Child); + } else { + Children.push_back(Child); + Child->setParent(this); + } +} + +std::vector FeatureTreeItem::getChildrenRecursive() { + auto Nodes = std::vector{Children}; + for (auto *Child : Children) { + auto ChildNodes = Child->getChildrenRecursive(); + Nodes.insert(Nodes.end(), ChildNodes.begin(), ChildNodes.end()); + } + + return Nodes; +} + +QVariant FeatureTreeItemFeature::data(int Column) const { + switch (Column) { + case 0: + return QString::fromStdString(Item->getName().str()); + case 1: + return Item->isOptional() ? QVariant("✓") : QVariant("x"); + case 2: + return numericValue(Item); + case 3: + return locationString(Item); + case 4: + return QString::fromStdString(Item->getOutputString().str()); + default: + return {}; + } +} + +void FeatureTreeItemFeature::inspect() { emit(inspectSource(Item)); } + +void FeatureTreeItemFeature::contextMenu(QPoint Pos) { + ContextMenu->popup(Pos); +} + +void FeatureTreeItemFeature::remove() { emit(removeFeature(false, Item)); } + +void FeatureTreeItemFeature::addChild() { emit(addChildFeature(Item)); } diff --git a/tools/fm-editor/tree/FeatureTreeItem.h b/tools/fm-editor/tree/FeatureTreeItem.h new file mode 100644 index 000000000..e94909313 --- /dev/null +++ b/tools/fm-editor/tree/FeatureTreeItem.h @@ -0,0 +1,165 @@ +#ifndef VARA_FEATURE_FEATURETREEITEM_H +#define VARA_FEATURE_FEATURETREEITEM_H + +#include "vara/Feature/Feature.h" +#include "vara/Feature/Relationship.h" + +#include + +#include +#include +#include + +#include + +enum ItemKind { + IK_Feature, + IK_Relation, + IK_Root + +}; + +class FeatureTreeItem : public QObject { + Q_OBJECT + +public: + FeatureTreeItem *child(size_t Row) { + if (Row < 0 || Row > Children.size()) { + + return nullptr; + } + + return Children[Row]; + } + + int childCount() { return Children.size(); } + + std::vector getChildrenRecursive(); + + int row() { + if (Parent) { + auto pos = + std::find(Parent->Children.begin(), Parent->Children.end(), this); + if (pos != Parent->Children.end()) { + return pos - Parent->Children.begin(); + } + } + + return 0; + } + + FeatureTreeItem *parent() { return Parent; } + void addChild(FeatureTreeItem *Child); + std::vector &getChildren() { return Children; } + [[nodiscard]] virtual int columnCount() const = 0; + [[nodiscard]] virtual QVariant data(int Column) const = 0; + std::unique_ptr static createFeatureTreeItem( + vara::feature::FeatureTreeNode *Item); + bool booleanColumn(int Column) { return false; }; + virtual void contextMenu(QPoint Pos) = 0; + virtual vara::feature::FeatureTreeNode *getItem() const = 0; + virtual ItemKind getKind() = 0; + virtual string getName() { return ""; }; + void setParent(FeatureTreeItem *ParentItem) { this->Parent = ParentItem; } + +signals: + void inspectSource(vara::feature::Feature *Feature); + void addChildFeature(vara::feature::Feature *Feature); + void removeFeature(bool Recursive, vara::feature::Feature *Feature); + +protected: + FeatureTreeItem() = default; + + FeatureTreeItem *Parent = nullptr; + + std::vector Children = {}; +}; + +class FeatureTreeItemRoot : public FeatureTreeItem { + Q_OBJECT + +public: + FeatureTreeItemRoot(){}; + [[nodiscard]] int columnCount() const override { return 5; }; + [[nodiscard]] QVariant data(int Column) const override { return {}; }; + void contextMenu(QPoint Pos) override{}; + [[nodiscard]] vara::feature::FeatureTreeNode *getItem() const override { + return nullptr; + }; + ItemKind getKind() override { return IK_Root; }; +}; + +class FeatureTreeItemFeature : public FeatureTreeItem { + Q_OBJECT + +public: + FeatureTreeItemFeature(vara::feature::Feature *Item) + : FeatureTreeItem(), Item(Item) { + ContextMenu = std::make_unique(); + ContextMenu->addAction("Inspect Sources", this, + &FeatureTreeItemFeature::inspect); + ContextMenu->addAction("Add Child", this, + &FeatureTreeItemFeature::addChild); + ContextMenu->addAction("Delete", this, &FeatureTreeItemFeature::remove); + } + ~FeatureTreeItemFeature() override = default; + + [[nodiscard]] QVariant data(int Column) const override; + [[nodiscard]] int columnCount() const override { return 5; } + bool booleanColumn(int Column) { return Column == 1; } + void contextMenu(QPoint Pos) override; + [[nodiscard]] vara::feature::FeatureTreeNode *getItem() const override { + return Item; + } + [[nodiscard]] const vara::feature::Feature *getFeature() const { + return Item; + } + string getName() override { return Item->getName().str(); } + ItemKind getKind() override { return IK_Feature; } +public slots: + void inspect(); + void addChild(); + void remove(); + +private: + vara::feature::Feature *Item; + std::unique_ptr ContextMenu; + std::unique_ptr RemoveAction; +}; + +class FeatureTreeItemRelation : public FeatureTreeItem { +public: + FeatureTreeItemRelation(vara::feature::Relationship *Item) : Item(Item){}; + ~FeatureTreeItemRelation() override = default; + + [[nodiscard]] QVariant data(int Column) const override { + if (Column == 0) { + return QString::fromStdString(relationType()); + } + return {}; + } + [[nodiscard]] int columnCount() const override { return 1; } + void contextMenu(QPoint Pos) override {} + [[nodiscard]] vara::feature::FeatureTreeNode *getItem() const override { + return Item; + } + ItemKind getKind() override { return IK_Relation; } + +private: + vara::feature::Relationship *Item; + [[nodiscard]] std::string relationType() const { + std::string Type; + switch (Item->getKind()) { + + case vara::feature::Relationship::RelationshipKind::RK_ALTERNATIVE: + Type = "⊕ Alternative"; + break; + case vara::feature::Relationship::RelationshipKind::RK_OR: + Type = "+ Or"; + break; + } + return Type; + } +}; + +#endif // VARA_FEATURE_FEATURETREEITEM_H diff --git a/tools/fm-editor/tree/FeatureTreeViewModel.cpp b/tools/fm-editor/tree/FeatureTreeViewModel.cpp new file mode 100644 index 000000000..aeee9e6e3 --- /dev/null +++ b/tools/fm-editor/tree/FeatureTreeViewModel.cpp @@ -0,0 +1,167 @@ +#include "FeatureTreeViewModel.h" + +#include + +QModelIndex FeatureTreeViewModel::index(int Row, int Column, + const QModelIndex &Parent) const { + FeatureTreeItem *ParentItem; + + if (!Parent.isValid()) { + ParentItem = RootItem; + } else { + ParentItem = static_cast(Parent.internalPointer()) + ->child(Parent.row()); + } + + if (ParentItem->childCount() <= 0) { + return {}; + } + + auto *ChildItem = ParentItem->child(Row); + if (ChildItem) { + return createIndex(Row, Column, ParentItem); + } + + return {}; +} + +QModelIndex FeatureTreeViewModel::parent(const QModelIndex &Child) const { + if (!Child.isValid()) { + return {}; + } + + auto *ParentItem = static_cast(Child.internalPointer()); + if (ParentItem && ParentItem != RootItem) { + return createIndex(ParentItem->row(), 0, ParentItem->parent()); + } + + return {}; +} + +int FeatureTreeViewModel::rowCount(const QModelIndex &Parent) const { + if (Parent.column() > 0) { + return 0; + } + + FeatureTreeItem *ParentItem; + if (!Parent.isValid()) { + ParentItem = RootItem; + } else { + ParentItem = static_cast(Parent.internalPointer()) + ->child(Parent.row()); + } + + return ParentItem->childCount(); +} + +int FeatureTreeViewModel::columnCount(const QModelIndex &Parent) const { + if (Parent.isValid()) { + auto *Item = static_cast(Parent.internalPointer()) + ->child(Parent.row()); + return Item->columnCount(); + } + + return RootItem->columnCount(); +} + +QVariant FeatureTreeViewModel::data(const QModelIndex &Index, int Role) const { + if (!Index.isValid() || Role != Qt::DisplayRole) { + return {}; + } + + auto *Item = static_cast(Index.internalPointer()) + ->child(Index.row()); + return Item->data(Index.column()); +} + +Qt::ItemFlags FeatureTreeViewModel::flags(const QModelIndex &Index) const { + if (Index.isValid()) { + auto *Item = static_cast(Index.internalPointer()) + ->child(Index.row()); + if (Item->booleanColumn(Index.column())) { + return Qt::ItemIsUserCheckable | Qt::ItemIsSelectable | Qt::ItemIsEnabled; + } + } + + return QAbstractItemModel::flags(Index); +} + +QVariant FeatureTreeViewModel::headerData(int Section, + Qt::Orientation Orientation, + int Role) const { + if (Orientation == Qt::Orientation::Horizontal && Role == Qt::DisplayRole) { + switch (Section) { + case 0: + return QString("Feature"); + case 1: + return QString("Optional"); + case 2: + return QString("NumericValues"); + case 3: + return QString("Locations"); + case 4: + return QString("ConfigurationOption"); + default: + return QString("Todo"); + } + } + + return {}; +} + +std::vector> * +FeatureTreeViewModel::getItems() { + return &Items; +} + +FeatureTreeItem * +FeatureTreeViewModel::addFeature(vara::feature::Feature *Feature, + std::string Parent) { + auto *Item = getItem(std::move(Parent)); + if (Item) { + emit(layoutAboutToBeChanged()); + auto NewItem = FeatureTreeItem::createFeatureTreeItem(Feature); + Item->addChild(NewItem.get()); + auto *NewItemRaw = NewItem.get(); + Items.push_back(std::move(NewItem)); + emit(layoutChanged()); + return NewItemRaw; + } + + return nullptr; +} + +void FeatureTreeViewModel::deleteFeatureItem(bool Recursive, + vara::feature::Feature *Feature) { + emit(layoutAboutToBeChanged()); + auto *Item = getItem(Feature->getName().str()); + if (Item) { + deleteItem(Recursive, Item); + } + + emit(layoutChanged()); +} + +void FeatureTreeViewModel::deleteItem(bool Recursive, FeatureTreeItem *Item) { + + if (!Recursive) { + auto *Parent = Item->parent(); + if (Parent) { + for (auto *Child : Item->getChildren()) { + Parent->addChild(Child); + } + auto ItemPos = std::find(Parent->getChildren().begin(), + Parent->getChildren().end(), Item); + Parent->getChildren().erase(ItemPos); + } + } + + if (Recursive) { + for (auto *Child : Item->getChildren()) { + deleteItem(Recursive, Child); + } + } + + Items.erase(std::find_if(Items.begin(), Items.end(), + [Item](auto &I) { return I.get() == Item; })); +} diff --git a/tools/fm-editor/tree/FeatureTreeViewModel.h b/tools/fm-editor/tree/FeatureTreeViewModel.h new file mode 100644 index 000000000..00ae1acc3 --- /dev/null +++ b/tools/fm-editor/tree/FeatureTreeViewModel.h @@ -0,0 +1,79 @@ +#ifndef VARA_FEATURE_FEATURETREEVIEWMODEL_H +#define VARA_FEATURE_FEATURETREEVIEWMODEL_H + +#include "FeatureTreeItem.h" +#include "vara/Feature/FeatureModel.h" + +#include + +class FeatureTreeViewModel : public QAbstractItemModel { +public: + FeatureTreeViewModel(vara::feature::FeatureModel *Model, QObject *Parent) + : QAbstractItemModel(Parent) { + auto UniqueRoot = FeatureTreeItem::createFeatureTreeItem(Model->getRoot()); + RootItem = new FeatureTreeItemRoot(); + RootItem->addChild(UniqueRoot.get()); + auto RawRoot = UniqueRoot.get(); + Items.push_back(std::move(UniqueRoot)); + buildRecursive(RawRoot); + } + ~FeatureTreeViewModel() override { + delete RootItem; + std::destroy(Items.begin(), Items.end()); + } + + std::vector> *getItems(); + [[nodiscard]] QVariant data(const QModelIndex &Index, + int Role = Qt::DisplayRole) const override; + [[nodiscard]] int + rowCount(const QModelIndex &Parent = QModelIndex()) const override; + [[nodiscard]] QModelIndex + index(int Row, int Column, + const QModelIndex &Parent = QModelIndex()) const override; + [[nodiscard]] QModelIndex parent(const QModelIndex &Child) const override; + [[nodiscard]] int + columnCount(const QModelIndex &Parent = QModelIndex()) const override; + [[nodiscard]] Qt::ItemFlags flags(const QModelIndex &Index) const override; + [[nodiscard]] QVariant headerData(int Section, Qt::Orientation Orientation, + int Role = Qt::DisplayRole) const override; + FeatureTreeItem *addFeature(vara::feature::Feature *Feature, string Parent); + void deleteFeatureItem(bool Recursive, vara::feature::Feature *Feature); + void deleteItem(bool Recursive, FeatureTreeItem *Item); + FeatureTreeItem *getItem(string Name) { + auto Item = + std::find_if(Items.begin(), Items.end(), + [&Name](const auto &I) { return I->getName() == Name; }); + if (Item != Items.end()) { + return Item->get(); + } + + return nullptr; + } + +private: + void buildRecursive(FeatureTreeItem *Parent) { + for (auto *ChildItem : Parent->getItem()->children()) { + FeatureTreeItem *RawChild; + if (vara::feature::Relationship::classof(ChildItem)) { + auto Child = FeatureTreeItem::createFeatureTreeItem( + dynamic_cast(ChildItem)); + Parent->addChild(Child.get()); + Child->setParent(Parent); + RawChild = Child.get(); + Items.push_back(std::move(Child)); + } else { + auto Child = FeatureTreeItem::createFeatureTreeItem( + llvm::dyn_cast(ChildItem)); + Parent->addChild(Child.get()); + Child->setParent(Parent); + RawChild = Child.get(); + Items.push_back(std::move(Child)); + } + buildRecursive(RawChild); + } + } + FeatureTreeItem *RootItem; + std::vector> Items; +}; + +#endif // VARA_FEATURE_FEATURETREEVIEWMODEL_H