diff --git a/source/RobotAPI/gui-plugins/ArViz/ArVizWidgetController.cpp b/source/RobotAPI/gui-plugins/ArViz/ArVizWidgetController.cpp index 945013a9905234d8eb807745bea34e04c8456b31..fc014c5adab046771815191a4c1e997602dca519 100644 --- a/source/RobotAPI/gui-plugins/ArViz/ArVizWidgetController.cpp +++ b/source/RobotAPI/gui-plugins/ArViz/ArVizWidgetController.cpp @@ -22,10 +22,10 @@ #include "ArVizWidgetController.h" -#include <ArmarXCore/observers/variant/Variant.h> - #include <SimoxUtility/algorithm/string/string_tools.h> +#include <ArmarXCore/observers/variant/Variant.h> + #include <QLineEdit> #include <QMessageBox> #include <QTimer> @@ -93,7 +93,6 @@ namespace armarx connect(this, &This::disconnectGui, this, &This::onDisconnectGui, Qt::QueuedConnection); // Layer info tree. - connect(widget.layerTree, &QTreeWidget::currentItemChanged, this, &This::updateSelectedLayer); connect(widget.layerInfoTreeGroupBox, &QGroupBox::toggled, &layerInfoTree, &LayerInfoTree::setEnabled); @@ -105,7 +104,6 @@ namespace armarx layerInfoTree.setEnabled(widget.layerInfoTreeGroupBox->isChecked()); layerInfoTree.registerVisualizerCallbacks(visualizer); - // We need a callback from the visualizer, when the layers have changed // So we can update the tree accordingly visualizer.layersChangedCallback = [this](std::vector<viz::CoinLayerID> const & layers) @@ -113,6 +111,12 @@ namespace armarx layersChanged(layers); }; + // Arviz Profile tree. + arvizeProfileLayerWidget = new ArvizProfileManagerWidget(); + widget.verticalLayout->addWidget(arvizeProfileLayerWidget); + connect(arvizeProfileLayerWidget, &ArvizProfileManagerWidget::publishUpdate, this, &This::updateLayersFromProfile); + connect(arvizeProfileLayerWidget, &ArvizProfileManagerWidget::fetchUpdate, this, &This::updateLayers); + replayTimer->start(33); } @@ -167,6 +171,38 @@ namespace armarx emit disconnectGui(); } + void ArVizWidgetController::updateLayers() + { + // Update shown/hidden layers + layerTreeChanged(nullptr, 0); + } + + void ArVizWidgetController::updateLayersFromProfile() + { + { + QSignalBlocker blocker(widget.layerTree); + + // Iterate over all items and activate/deactivate layers accordingly + for (int compIndex = 0; compIndex < widget.layerTree->topLevelItemCount(); ++compIndex) + { + QTreeWidgetItem* componentItem = widget.layerTree->topLevelItem(compIndex); + bool parentChecked = true; + const std::string component = componentItem->text(0).toStdString(); + for (int layerIndex = 0; layerIndex < componentItem->childCount(); ++layerIndex) + { + QTreeWidgetItem* layerItem = componentItem->child(layerIndex); + const std::string layer = layerItem->text(0).toStdString(); + const viz::CoinLayerID layerID(component, layer); + const bool visible = not arvizeProfileLayerWidget->isHidden(layerID); + layerItem->setCheckState(0, visible ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); + parentChecked &= visible; + visualizer.showLayer(layerID, visible); + } + componentItem->setCheckState(0, parentChecked ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); + } + } + } + void ArVizWidgetController::onConnectGui() { onRefreshRecordings(); @@ -194,6 +230,8 @@ namespace armarx Qt::CheckState componentCheck = componentItem->checkState(0); int layerCount = componentItem->childCount(); + arvizeProfileLayerWidget->update(component, componentCheck == Qt::Checked); + if (componentItem == item) { // The parent was selected or deselected, so all children should be set accordingly @@ -208,7 +246,6 @@ namespace armarx } return; } - for (int layerIndex = 0; layerIndex < layerCount; ++layerIndex) { QTreeWidgetItem* layerItem = componentItem->child(layerIndex); @@ -217,6 +254,9 @@ namespace armarx ARMARX_VERBOSE << "Layer: " << layer << ", Visible: " << layerVisible; viz::CoinLayerID layerID(component, layer); + + arvizeProfileLayerWidget->update(layerID, layerVisible); + visualizer.showLayer(layerID, layerVisible); } } @@ -261,10 +301,10 @@ namespace armarx if (iter == currentComponents.end()) { // Create a new item + QSignalBlocker blocker(tree); // otherwise added as unchecked and directly updated currentItem = new QTreeWidgetItem(tree); currentItem->setText(0, QString::fromStdString(component)); - // A new item is visible by default - currentItem->setCheckState(0, Qt::Checked); + currentItem->setCheckState(0, arvizeProfileLayerWidget->isHidden(component) ? Qt::Unchecked : Qt::Checked); componentWasNew = true; } @@ -286,8 +326,7 @@ namespace armarx std::string const& layer = entry.second; QTreeWidgetItem* layerItem = new QTreeWidgetItem; layerItem->setText(0, QString::fromStdString(layer)); - // A new item is visible by default - layerItem->setCheckState(0, Qt::Checked); + layerItem->setCheckState(0, arvizeProfileLayerWidget->isHidden(entry) ? Qt::Unchecked : Qt::Checked); if (currentItem) { @@ -308,7 +347,11 @@ namespace armarx } else { - // Item exists already ==> nothing to be done + // Item exists already ==> make sure that it is not shown if necessary + if (arvizeProfileLayerWidget->isHidden(iter->first) or iter->second->checkState(0) == Qt::Unchecked) + { + visualizer.showLayer(iter->first, false); + } } } } @@ -398,8 +441,7 @@ namespace armarx widget.layerTree->blockSignals(false); - // Update shown/hidden layers - layerTreeChanged(nullptr, 0); + updateLayers(); } void ArVizWidgetController::showFilteredLayers(bool visible) @@ -422,8 +464,7 @@ namespace armarx widget.layerTree->blockSignals(false); - // Update shown/hidden layers - layerTreeChanged(nullptr, 0); + updateLayers(); } void ArVizWidgetController::onDeselectElement() diff --git a/source/RobotAPI/gui-plugins/ArViz/ArVizWidgetController.h b/source/RobotAPI/gui-plugins/ArViz/ArVizWidgetController.h index cb10e285bee2e0b69497a1032703ca2aa5b5462e..065d04253da98d4c84c9d6f96313b094cfd95237 100644 --- a/source/RobotAPI/gui-plugins/ArViz/ArVizWidgetController.h +++ b/source/RobotAPI/gui-plugins/ArViz/ArVizWidgetController.h @@ -21,6 +21,8 @@ */ #pragma once +#include <unordered_set> + #include <RobotAPI/gui-plugins/ArViz/ui_ArVizWidget.h> #include <RobotAPI/components/ArViz/Coin/Visualizer.h> @@ -34,6 +36,7 @@ #include <ArmarXCore/core/system/ImportExportComponent.h> #include "LayerInfoTree.h" +#include "ArvizProfileManagerWidget.h" namespace armarx @@ -106,8 +109,10 @@ namespace armarx void onGetBatchAsync(viz::data::RecordingBatch const& batch); - public slots: + private slots: /* QT slot declarations */ + void updateLayers(); + void updateLayersFromProfile(); signals: /* QT signal declarations */ @@ -224,6 +229,8 @@ namespace armarx std::vector<double> timings; + ArvizProfileManagerWidget* arvizeProfileLayerWidget; + public: static QIcon GetWidgetIcon() { diff --git a/source/RobotAPI/gui-plugins/ArViz/ArvizProfileManagerWidget.cpp b/source/RobotAPI/gui-plugins/ArViz/ArvizProfileManagerWidget.cpp new file mode 100644 index 0000000000000000000000000000000000000000..4af5a0fd4db95eb36733d5bcb501d1453bdfc5b6 --- /dev/null +++ b/source/RobotAPI/gui-plugins/ArViz/ArvizProfileManagerWidget.cpp @@ -0,0 +1,362 @@ +#include "ArvizProfileManagerWidget.h" + +#include <string> + +#include <QVBoxLayout> +#include <QMessageBox> +#include <QBoxLayout> +#include <QLabel> +#include <QFormLayout> +#include <QDialogButtonBox> +#include <QPushButton> +#include <QBoxLayout> +#include <QRadioButton> + +#include <ArmarXCore/core/system/ArmarXDataPath.h> +#include <ArmarXCore/core/logging/Logging.h> + +namespace armarx +{ + +const QString ArvizProfileManagerWidget::SETTINGS_DEFAULT_PROFILE_KEY = "defaultProfileName"; + + +ProfileDialog::ProfileDialog(QWidget *parent, const QString &name, bool additive, bool addDialog, bool isDefault) + : QDialog(parent) +{ + setWindowTitle(addDialog ? "Add ArViz Profile" : "Edit ArViz Profile"); + + // Set up UI components + nameInput = new QLineEdit(this); + nameInput->setText(name); + + additiveRadioButton = new QRadioButton("Save added layers", this); + additiveRadioButton->setChecked(additive); + substractiveRadioButton = new QRadioButton("Save removed layers", this); + substractiveRadioButton->setChecked(not additive); + + defaultCheckBox = new QCheckBox("Mark default", this); + defaultCheckBox->setChecked(isDefault); + + // Save/Cancel buttons + QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel, this); + connect(buttonBox, &QDialogButtonBox::accepted, this, &ProfileDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &ProfileDialog::reject); + buttonBox->setCenterButtons(true); + + // Delete button + deleteButton = new QPushButton("Delete", this); + connect(deleteButton, &QPushButton::clicked, this, &ProfileDialog::deleteProfile); + + // Layouts + QFormLayout *formLayout = new QFormLayout; + formLayout->addRow("Profile Name:", nameInput); + formLayout->addRow(additiveRadioButton); + formLayout->addRow(substractiveRadioButton); + formLayout->addRow(defaultCheckBox); + formLayout->addRow(buttonBox); + formLayout->addRow(deleteButton); + + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + setLayout(formLayout); + adjustSize(); +} + +ProfileDialog::~ProfileDialog() +{ +} + +QString ProfileDialog::getName() const +{ + return nameInput->text(); +} + +bool ProfileDialog::isAdditive() const +{ + return additiveRadioButton->isChecked(); +} + +bool ProfileDialog::isDefaultProfile() const +{ + return defaultCheckBox->isChecked(); +} + +void ProfileDialog::deleteProfile() +{ + // Handle the profile deletion + emit deleteProfileSignal(); + reject(); // Close the dialog after deletion +} + + + +ArvizProfileManagerWidget::ArvizProfileManagerWidget(QWidget* parent) : + QWidget(parent), + settings(QString((armarx::ArmarXDataPath::GetDefaultUserConfigPath() + "/ArvizProfileManager.conf").c_str()), + QSettings::NativeFormat), + currentProfile(Profile()) +{ + QHBoxLayout* layout = new QHBoxLayout(this); + + // Setup UI elements + layersComboBox = new QComboBox(this); + connect(layersComboBox, QOverload<int>::of(&QComboBox::activated), + this, &ArvizProfileManagerWidget::onProfileSelected); + + addButton = new QPushButton("Add", this); + editButton = new QPushButton("Edit", this); + saveButton = new QPushButton("Save", this); + + layout->addWidget(new QLabel("ArViz Profile: ")); + layout->addWidget(layersComboBox); + layout->addWidget(addButton); + layout->addWidget(editButton); + layout->addWidget(saveButton); + + setLayout(layout); + + connect(addButton, &QPushButton::clicked, this, &ArvizProfileManagerWidget::addProfile); + connect(editButton, &QPushButton::clicked, this, &ArvizProfileManagerWidget::editProfile); + connect(saveButton, &QPushButton::clicked, this, &ArvizProfileManagerWidget::saveCurrentProfile); + + loadLayerNames(); + + // Load default profile + const QString defaultProfileName + = settings.value(SETTINGS_DEFAULT_PROFILE_KEY).toString(); + if (int i = layersComboBox->findText(defaultProfileName); i >= 0) + { + layersComboBox->setCurrentText(defaultProfileName); + onProfileSelected(i); + } + else + { + // Not found. Load empty profile + layersComboBox->setCurrentIndex(-1); + } +} + +void ArvizProfileManagerWidget::onProfileSelected(int index) +{ + if (index >= 0 and index < layersComboBox->count()) + { + loadProfile(layersComboBox->itemText(index)); + emit publishUpdate(); + } +} + +void ArvizProfileManagerWidget::saveCurrentProfile() +{ + const QString name = layersComboBox->currentText(); + if (name.isEmpty()) + { + QMessageBox::warning(this, "Warning", "Please enter a valid name."); + return; + } + + if (saveProfile(name, currentProfile)) + { + if (layersComboBox->findText(name) == -1) + { + layersComboBox->addItem(name); + } + layersComboBox->setCurrentText(name); + } +} + +void ArvizProfileManagerWidget::deleteCurrentProfileSave() +{ + const QString name = layersComboBox->currentText(); + settings.remove(name); + + const int index = layersComboBox->currentIndex(); + layersComboBox->removeItem(index); + settings.remove(name); + + if (layersComboBox->count() == 0) + { + editButton->setDisabled(true); + saveButton->setDisabled(true); + } + + emit publishUpdate(); +} + +void ArvizProfileManagerWidget::addProfile() +{ + if (dialog == nullptr || !dialog->isVisible()) + { + const QString defaultName = "Profile"; + const bool defaultAdditive = false; + QString name = defaultName; + unsigned int counter = 2; + while (layersComboBox->count() > 0 and layersComboBox->findText(name) >= 0) + { + name = defaultName + QString::fromStdString(std::to_string(counter++)); + } + dialog = new ProfileDialog(this, name, defaultAdditive, true, + settings.value(SETTINGS_DEFAULT_PROFILE_KEY) == name); + const int result = dialog->exec(); + + if (result == QDialog::Accepted) + { + if (dialog->getName().isEmpty()) + { + ARMARX_WARNING << "Empty name for profile set."; + return; + } + if (layersComboBox->findText(dialog->getName()) >= 0) + { + ARMARX_WARNING << "Profile with name " << dialog->getName().toStdString() << " already exists!"; + return; + } + + if (dialog->isDefaultProfile()) + { + settings.setValue(SETTINGS_DEFAULT_PROFILE_KEY, dialog->getName()); + } + + currentProfile.layers = {}; + currentProfile.additive = dialog->isAdditive(); + emit fetchUpdate(); + + saveProfile(dialog->getName(), currentProfile); + loadLayerNames(); + layersComboBox->setCurrentText(dialog->getName()); + } + else + { + // do nothing + } + } + else + { + QMessageBox::information(this, "Dialog Already Open", "The dialog is already open."); + } +} + +void ArvizProfileManagerWidget::editProfile() +{ + if (layersComboBox->count() > 0) + { + if (dialog == nullptr || !dialog->isVisible()) + { + const auto name = layersComboBox->currentText(); + dialog = new ProfileDialog(this, name, currentProfile.additive, + false, settings.value(SETTINGS_DEFAULT_PROFILE_KEY) == name); + connect(dialog, &ProfileDialog::deleteProfileSignal, this, &ArvizProfileManagerWidget::deleteCurrentProfileSave); + const int result = dialog->exec(); + + if (result == QDialog::Accepted) + { + const auto newName = dialog->getName(); + + if (newName.isEmpty()) + { + ARMARX_WARNING << "Empty name for profile set."; + return; + } + + if (newName != name) + { + // Rename + if (layersComboBox->findText(newName) >= 0) + { + ARMARX_WARNING << "Profile with name " << newName.toStdString() << " already exists!"; + return; + } + settings.remove(layersComboBox->currentText()); + } + + if (dialog->isAdditive() != currentProfile.additive) + { + currentProfile.layers.clear(); + currentProfile.additive = dialog->isAdditive(); + emit fetchUpdate(); + } + + if (dialog->isDefaultProfile()) + { + settings.setValue(SETTINGS_DEFAULT_PROFILE_KEY, newName); + } + else if (settings.value(SETTINGS_DEFAULT_PROFILE_KEY) == name) + { + settings.setValue(SETTINGS_DEFAULT_PROFILE_KEY, ""); + } + + saveProfile(dialog->getName(), currentProfile); + loadLayerNames(); + layersComboBox->setCurrentText(newName); + } + else + { + // do nothing + } + } + else + { + QMessageBox::information(this, "Dialog Already Open", "The dialog is already open."); + } + } +} + +void ArvizProfileManagerWidget::loadLayerNames() +{ + layersComboBox->clear(); + for (const QString& groups : settings.childGroups()) + { + layersComboBox->addItem(groups); + } + if (layersComboBox->count() == 0) + { + editButton->setDisabled(true); + saveButton->setDisabled(true); + } + else + { + editButton->setEnabled(true); + saveButton->setEnabled(true); + } +} + +void ArvizProfileManagerWidget::loadProfile(const QString& name) +{ + ARMARX_INFO << "Loading ArViz profile " << name.toStdString(); + + std::scoped_lock lock(mtx); + currentProfile.layers.clear(); + + settings.beginGroup(name); + currentProfile.additive = settings.value("additive").toBool(); + const int size = settings.beginReadArray("layers"); + for (int i = 0; i < size; ++i) + { + settings.setArrayIndex(i); + QString first = settings.value("component").toString(); + QString second = settings.value("layer").toString(); + currentProfile.layers.insert({first.toStdString(), second.toStdString()}); + } + settings.endArray(); + settings.endGroup(); + editButton->setEnabled(true); +} + +bool ArvizProfileManagerWidget::saveProfile(const QString& name, const Profile& profile) +{ + settings.beginGroup(name); + settings.setValue("additive", profile.additive); + settings.remove("layers"); + settings.beginWriteArray("layers", profile.layers.size()); + int i = 0; + for (const auto& pair : profile.layers) + { + settings.setArrayIndex(i++); + settings.setValue("component", QString::fromStdString(pair.first)); + settings.setValue("layer", QString::fromStdString(pair.second)); + } + settings.endArray(); + settings.endGroup(); + return true; +} + +} \ No newline at end of file diff --git a/source/RobotAPI/gui-plugins/ArViz/ArvizProfileManagerWidget.h b/source/RobotAPI/gui-plugins/ArViz/ArvizProfileManagerWidget.h new file mode 100644 index 0000000000000000000000000000000000000000..1f5a97151f4043c723356e6db76cb491e0479a87 --- /dev/null +++ b/source/RobotAPI/gui-plugins/ArViz/ArvizProfileManagerWidget.h @@ -0,0 +1,175 @@ +/* + * This file is part of ArmarX. + * + * ArmarX is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * ArmarX is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @package RobotAPI::gui-plugins::ArVizProfileManagerWidget + * @author Andre Meixner ( andre dot meixner at kit dot edu ) + * @date 2024 + * @copyright http://www.gnu.org/licenses/gpl-2.0.txt + * GNU General Public License + */ +#pragma once + +#include <QWidget> +#include <QDialog> +#include <QComboBox> +#include <QPushButton> +#include <QLineEdit> +#include <QSettings> +#include <QString> +#include <QCheckBox> +#include <QRadioButton> + +#include <mutex> +#include <qcheckbox.h> +#include <unordered_set> +#include <string> +#include <utility> + +#include <RobotAPI/components/ArViz/Coin/Visualizer.h> + +namespace armarx +{ + struct pair_hash + { + template <typename T1, typename T2> + std::size_t operator ()(const std::pair<T1, T2>& p) const + { + auto h1 = std::hash<T1>{}(p.first); + auto h2 = std::hash<T2>{}(p.second); + return h1 ^ (h2 << 1); + } + }; + + using Layers = std::unordered_set<viz::CoinLayerID, pair_hash>; + + struct Profile + { + Layers layers; + bool additive = false; + }; + + + class ProfileDialog : public QDialog + { + Q_OBJECT + + public: + explicit ProfileDialog(QWidget *parent = nullptr, const QString &name = "", + bool additive = false, bool addDialog = true, bool isDefault = false); + ~ProfileDialog(); + + QString getName() const; + bool isAdditive() const; + bool isDefaultProfile() const; + + signals: + void deleteProfileSignal(); + + private slots: + void deleteProfile(); + + private: + QLineEdit *nameInput; + QRadioButton *additiveRadioButton; + QRadioButton *substractiveRadioButton; + QPushButton *deleteButton; + QCheckBox *defaultCheckBox; + }; + + + class ArvizProfileManagerWidget : public QWidget + { + Q_OBJECT + + public: + explicit ArvizProfileManagerWidget(QWidget* parent = nullptr); + + inline void update(const viz::CoinLayerID& layerID, bool visible) + { + std::scoped_lock lock(mtx); + + if (currentProfile.additive == visible) + { + currentProfile.layers.insert(layerID); + } + else if (currentProfile.layers.count(layerID) > 0) + { + currentProfile.layers.erase(layerID); + currentProfile.layers.erase({layerID.first, "*"}); + } + + } + + inline void update(const std::string& componentName, bool visible) + { + update({componentName, "*"}, visible); + } + + inline bool isHidden(const viz::CoinLayerID& layerID) const + { + std::scoped_lock lock(mtx); + + const bool result = currentProfile.layers.count(layerID) > 0 or currentProfile.layers.count({layerID.first, "*"}) > 0; + + if (currentProfile.additive) + { + return not result; + } + else + { + return result; + } + } + + inline bool isHidden(const std::string& componentName) const + { + std::scoped_lock lock(mtx); + + return currentProfile.additive == (currentProfile.layers.count({componentName, "*"}) == 0); + } + + signals: + void publishUpdate(); + void fetchUpdate(); + + public slots: + void onProfileSelected(int index); + + private slots: + void saveCurrentProfile(); + void deleteCurrentProfileSave(); + + void addProfile(); + void editProfile(); + + private: + void loadLayerNames(); + void loadProfile(const QString& name); + bool saveProfile(const QString& name, const Profile& profile); + + ProfileDialog* dialog = nullptr; + QComboBox* layersComboBox; + QPushButton* addButton; + QPushButton* editButton; + QPushButton* saveButton; + QSettings settings; + mutable std::mutex mtx; + + Profile currentProfile; + + static const QString SETTINGS_DEFAULT_PROFILE_KEY; + }; + +} diff --git a/source/RobotAPI/gui-plugins/ArViz/CMakeLists.txt b/source/RobotAPI/gui-plugins/ArViz/CMakeLists.txt index c9f30ffe5c0d845c057bf9346ca2425e3e146832..94928007fbb4c707002d5f9347b632a32d4e5b12 100644 --- a/source/RobotAPI/gui-plugins/ArViz/CMakeLists.txt +++ b/source/RobotAPI/gui-plugins/ArViz/CMakeLists.txt @@ -10,6 +10,7 @@ armarx_build_if(ArmarXGui_FOUND "ArmarXGui not available") set(SOURCES ArVizGuiPlugin.cpp ArVizWidgetController.cpp + ArvizProfileManagerWidget.cpp LayerInfoTree.cpp ) @@ -18,6 +19,7 @@ set(SOURCES set(HEADERS ArVizGuiPlugin.h ArVizWidgetController.h + ArvizProfileManagerWidget.h LayerInfoTree.h )