From a60222886201ea32a3b01c59b656e77417add4e3 Mon Sep 17 00:00:00 2001 From: Peter Albrecht <albrecpe@gmail.com> Date: Mon, 27 Nov 2023 16:47:47 +0100 Subject: [PATCH] Added SkillMemoryGUI --- .../libraries/skills_gui/CMakeLists.txt | 8 + .../skills_gui/PeriodicUpdateWidget.cpp | 141 ++++++++++++++++++ .../skills_gui/PeriodicUpdateWidget.h | 66 ++++++++ .../libraries/skills_gui/SkillMemoryGui.cpp | 52 +++++++ .../libraries/skills_gui/SkillMemoryGui.h | 45 ++++++ .../libraries/skills_gui/gui_utils.cpp | 99 ++++++++++++ .../RobotAPI/libraries/skills_gui/gui_utils.h | 74 +++++++++ .../skills_gui/memory/SkillMemoryProxy.h | 12 +- .../skills_gui/skill_issues/todo.txt | 1 + 9 files changed, 493 insertions(+), 5 deletions(-) create mode 100644 source/RobotAPI/libraries/skills_gui/PeriodicUpdateWidget.cpp create mode 100644 source/RobotAPI/libraries/skills_gui/PeriodicUpdateWidget.h create mode 100644 source/RobotAPI/libraries/skills_gui/SkillMemoryGui.cpp create mode 100644 source/RobotAPI/libraries/skills_gui/SkillMemoryGui.h create mode 100644 source/RobotAPI/libraries/skills_gui/gui_utils.cpp create mode 100644 source/RobotAPI/libraries/skills_gui/gui_utils.h create mode 100644 source/RobotAPI/libraries/skills_gui/skill_issues/todo.txt diff --git a/source/RobotAPI/libraries/skills_gui/CMakeLists.txt b/source/RobotAPI/libraries/skills_gui/CMakeLists.txt index ba5987e59..5efc75b58 100644 --- a/source/RobotAPI/libraries/skills_gui/CMakeLists.txt +++ b/source/RobotAPI/libraries/skills_gui/CMakeLists.txt @@ -56,6 +56,10 @@ set(SOURCES skill_details/SkillDetailsGroupBox.cpp skill_details/ProfileMenuWidget.cpp skill_details/SkillDetailsTreeWidget.cpp + + PeriodicUpdateWidget.cpp + SkillMemoryGui.cpp + gui_utils.cpp ) set(HEADERS aron_tree_widget/visitors/AronTreeWidgetCreator.h @@ -93,6 +97,10 @@ set(HEADERS skill_details/SkillDetailsGroupBox.h skill_details/ProfileMenuWidget.h skill_details/SkillDetailsTreeWidget.h + + PeriodicUpdateWidget.h + SkillMemoryGui.h + gui_utils.h ) armarx_gui_library("${LIB_NAME}" "${SOURCES}" "${GUI_MOC_HDRS}" "${GUI_UIS}" "" "${LIBRARIES}") diff --git a/source/RobotAPI/libraries/skills_gui/PeriodicUpdateWidget.cpp b/source/RobotAPI/libraries/skills_gui/PeriodicUpdateWidget.cpp new file mode 100644 index 000000000..e23920f1b --- /dev/null +++ b/source/RobotAPI/libraries/skills_gui/PeriodicUpdateWidget.cpp @@ -0,0 +1,141 @@ +#include "PeriodicUpdateWidget.h" + +#include <cmath> + +#include <QCheckBox> +#include <QDoubleSpinBox> +#include <QHBoxLayout> +#include <QPushButton> +#include <QTimer> + +namespace armarx +{ + PeriodicUpdateWidget::PeriodicUpdateWidget(double frequency, double maxFrequency) + { + setSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Minimum); + + QLayout* vlayout = new QVBoxLayout(); + QLayout* hlayout = new QHBoxLayout(); + this->setLayout(vlayout); + + const int margin = 0; + hlayout->setContentsMargins(margin, margin, margin, margin); + + _updateButton = new QPushButton("Update", this); + _autoCheckBox = new QCheckBox("Auto Update", this); + _frequencySpinBox = new QDoubleSpinBox(this); + _frequencySpinBox->setValue(frequency); + _frequencySpinBox->setMinimum(0); + _frequencySpinBox->setMaximum(maxFrequency); + _frequencySpinBox->setSingleStep(0.5); + _frequencySpinBox->setSuffix(" Hz"); + + hlayout->addWidget(_updateButton); + hlayout->addWidget(_autoCheckBox); + hlayout->addWidget(_frequencySpinBox); + + vlayout->addItem(hlayout); + + + _timer = new QTimer(this); + _updateTimerFrequency(); + _frequencySpinBox->setEnabled(_autoCheckBox->isChecked()); + + + // Private connections. + connect(_autoCheckBox, &QCheckBox::toggled, this, &This::_toggleAutoUpdates); + connect(_frequencySpinBox, + &QDoubleSpinBox::editingFinished, + this, + &This::_updateTimerFrequency); + + // Public connections. + connect(_updateButton, &QPushButton::pressed, this, &This::updateSingle); + connect(_timer, &QTimer::timeout, this, &This::updatePeriodic); + + connect(this, &This::updateSingle, this, &This::update); + connect(this, &This::updatePeriodic, this, &This::update); + + // See `startTimerIfEnabled` for the signal reasoning. + // qOverload is required because `QTimer::start()` is overloaded. + connect(this, &This::startTimerSignal, _timer, qOverload<>(&QTimer::start)); + connect(this, &This::stopTimerSignal, _timer, &QTimer::stop); + } + + QPushButton* + PeriodicUpdateWidget::updateButton() + { + return _updateButton; + } + + int + PeriodicUpdateWidget::getUpdateIntervalMs() const + { + return static_cast<int>(std::round(1000 / _frequencySpinBox->value())); + } + + void + PeriodicUpdateWidget::startTimerIfEnabled() + { + /* A QTimer can only be started and stopped within its own thread (the thread for which + * it has the greatest affinity). Since this method can be called from any thread, we + * need to take a detour through these signals, which can be emitted from any thread and + * will always be caught in this object's (and thus the timer's) native thread. + */ + if (_autoCheckBox->isChecked()) + { + emit startTimerSignal(); + } + else + { + emit stopTimerSignal(); + } + } + + void + PeriodicUpdateWidget::stopTimer() + { + // See `startTimerIfEnabled` for the signal reasoning. + emit stopTimerSignal(); + } + + void + PeriodicUpdateWidget::_updateTimerFrequency() + { + _timer->setInterval(getUpdateIntervalMs()); + } + + void + PeriodicUpdateWidget::_toggleAutoUpdates(bool enabled) + { + // This method is already a slot, so it doesn't need to use the timer signals. + _frequencySpinBox->setEnabled(enabled); + if (enabled) + { + _timer->start(); + } + else + { + _timer->stop(); + } + } + + QCheckBox* + PeriodicUpdateWidget::autoCheckBox() + { + return _autoCheckBox; + } + + QDoubleSpinBox* + PeriodicUpdateWidget::frequencySpinBox() + { + return _frequencySpinBox; + } + + QTimer* + PeriodicUpdateWidget::timer() + { + return _timer; + } + +} // namespace armarx diff --git a/source/RobotAPI/libraries/skills_gui/PeriodicUpdateWidget.h b/source/RobotAPI/libraries/skills_gui/PeriodicUpdateWidget.h new file mode 100644 index 000000000..9d199db39 --- /dev/null +++ b/source/RobotAPI/libraries/skills_gui/PeriodicUpdateWidget.h @@ -0,0 +1,66 @@ +#pragma once + +#include <QWidget> + + +class QCheckBox; +class QDoubleSpinBox; +class QPushButton; +class QTimer; + +namespace armarx +{ + + class PeriodicUpdateWidget : public QWidget + { + Q_OBJECT + using This = PeriodicUpdateWidget; + + public: + PeriodicUpdateWidget(double frequency = 2.0, double maxFrequency = 60); + + + QTimer* timer(); + + QCheckBox* autoCheckBox(); + QDoubleSpinBox* frequencySpinBox(); + QPushButton* updateButton(); + + bool isAutoEnabled() const; + double getUpdateFrequency() const; + int getUpdateIntervalMs() const; + + void startTimerIfEnabled(); + void stopTimer(); + + + public slots: + + signals: + + void update(); + + void updateSingle(); + void updatePeriodic(); + + private slots: + + void _updateTimerFrequency(); + void _toggleAutoUpdates(bool enabled); + + signals: + + void startTimerSignal(); + void stopTimerSignal(); + + private: + QPushButton* _updateButton; + QCheckBox* _autoCheckBox; + QDoubleSpinBox* _frequencySpinBox; + + QPushButton* _collapseAllButton; + + QTimer* _timer; + }; + +} // namespace armarx diff --git a/source/RobotAPI/libraries/skills_gui/SkillMemoryGui.cpp b/source/RobotAPI/libraries/skills_gui/SkillMemoryGui.cpp new file mode 100644 index 000000000..a3a6566ec --- /dev/null +++ b/source/RobotAPI/libraries/skills_gui/SkillMemoryGui.cpp @@ -0,0 +1,52 @@ +#include "SkillMemoryGui.h" + +#include "gui_utils.h" + +namespace armarx::skills::gui +{ + SkillMemoryGUI::SkillMemoryGUI(QWidget* root, + QTreeWidget* _skillExecutionTreeWidget, + QBoxLayout* _skillExecutionTreeWidgetParentLayout, + QGroupBox* _skillGroupBox, + QBoxLayout* _skillGroupBoxParentLayout, + QGroupBox* _skillDetailGroupBox, + QBoxLayout* _skillDetailGroupBoxParentLayout, + QBoxLayout* _updateWidgetLayout) : + QObject() + { + Logging::setTag("SkillMemoryGui"); + + // setup memory + this->memory = std::make_shared(SkillMemoryProxy); + + // Update widget + this->updateWidgetLayout = _updateWidgetLayout; + this->updateWidget = new PeriodicUpdateWidget(this); + this->updateWidgetLayout->insertWidget(0, updateWidget); + + // replace skillExecutionTreeWidget + this->skillExecutionTreeWidget = new SkillExecutionTreeWidget(root, memory); + armarx::gui::replaceWidget(_skillExecutionTreeWidget, + this->skillExecutionTreeWidget, + _skillDetailGroupBoxParentLayout); + + // replace skillGroupBox + this->skillGroupBox = new SkillGroupBox(root, memory); + armarx::gui::replaceWidget(_skillGroupBox, this->skillGroupBox, _skillGroupBoxParentLayout); + + // replace skillDetailGroupBox + this->skillDetailGroupBox = new SkillDetailGroupBox(root, memory); + armarx::gui::replaceWidget( + _skillDetailGroupBox, this->skillDetailGroupBox, _skillDetailGroupBoxParentLayout); + } + + void + SkillMemoryGUI::connectSignals() + { + // connect update widget to memory update + connect(this->updateWidget, + &PeriodicUpdateWidget::update, + this->memory, + &SkillMemoryProxy::fetchUpdates); + } +} // namespace armarx::skills::gui diff --git a/source/RobotAPI/libraries/skills_gui/SkillMemoryGui.h b/source/RobotAPI/libraries/skills_gui/SkillMemoryGui.h new file mode 100644 index 000000000..1d9c8a3e6 --- /dev/null +++ b/source/RobotAPI/libraries/skills_gui/SkillMemoryGui.h @@ -0,0 +1,45 @@ +#ifndef SKILLMEMORYGUI_H +#define SKILLMEMORYGUI_H + +#include <QBoxLayout> +#include <QTreeWidget> +#include <QWidget> + +#include <ArmarXCore/core/logging/Logging.h> + +#include "./PeriodicUpdateWidget.h" +#include "./executions/SkillExecutionTreeWidget.h" +#include "./memory/SkillMemoryProxy.h" +#include "./skill_details/SkillDetailsGroupBox.h" +#include "./skills/SkillGroupBox.h" + +namespace armarx::skills::gui +{ + class SkillMemoryGUI : public QObject, public armarx::Logging + { + Q_OBJECT + public: + SkillMemoryGUI(QWidget* root, + QTreeWidget* _skillExecutionTreeWidget, + QBoxLayout* _skillExecutionTreeWidgetParentLayout, + QGroupBox* _skillGroupBox, + QBoxLayout* _skillGroupBoxParentLayout, + QGroupBox* _skillDetailGroupBox, + QBoxLayout* _skillDetailGroupBoxParentLayout, + QBoxLayout* _updateWidgetLayout); + + private: + void connectSignals(); + + SkillExecutionTreeWidget* skillExecutionTreeWidget = nullptr; + SkillGroupBox* skillGroupBox = nullptr; + SkillDetailGroupBox* skillDetailGroupBox = nullptr; + QBoxLayout* updateWidgetLayout = nullptr; + + std::shared_ptr<SkillMemoryProxy> memory = nullptr; + + PeriodicUpdateWidget* updateWidget = nullptr; + }; +} // namespace armarx::skills::gui + +#endif // SKILLMEMORYGUI_H diff --git a/source/RobotAPI/libraries/skills_gui/gui_utils.cpp b/source/RobotAPI/libraries/skills_gui/gui_utils.cpp new file mode 100644 index 000000000..337ebc87d --- /dev/null +++ b/source/RobotAPI/libraries/skills_gui/gui_utils.cpp @@ -0,0 +1,99 @@ +#include "gui_utils.h" + +#include <QLayout> +#include <QLayoutItem> +#include <QSplitter> +#include <QWidget> +#include <QTreeWidgetItem> + +#include <ArmarXCore/core/exceptions/local/ExpressionException.h> + + +void armarx::gui::clearLayout(QLayout* layout) +{ + // Source: https://stackoverflow.com/a/4857631 + + ARMARX_CHECK(layout); + + QLayoutItem* item; + while ((item = layout->takeAt(0))) + { + if (item->layout()) + { + clearLayout(item->layout()); + delete item->layout(); + } + if (item->widget()) + { + delete item->widget(); + } + delete item; + } +} + +void armarx::gui::clearItem(QTreeWidgetItem* item) +{ + while (item->childCount() > 0) + { + delete item->takeChild(0); + } +} + + +QSplitter* armarx::gui::useSplitter(QLayout* layout) +{ + ARMARX_CHECK(layout); + + // Check all items + for (int i = 0; i < layout->count(); ++i) + { + ARMARX_CHECK_NOT_NULL(layout->itemAt(i)->widget()) + << "QSplitter only supports widgets, but layout item #" << i << " is not a widget."; + } + + QSplitter* splitter; + if (dynamic_cast<QHBoxLayout*>(layout)) + { + splitter = new QSplitter(Qt::Orientation::Horizontal); + } + else if (dynamic_cast<QVBoxLayout*>(layout)) + { + splitter = new QSplitter(Qt::Orientation::Vertical); + } + else + { + splitter = new QSplitter(); + } + + while (layout->count() > 0) + { + const int index = 0; + if (layout->itemAt(index)) + { + QLayoutItem* item = layout->takeAt(index); + + ARMARX_CHECK(item->widget()); + splitter->addWidget(item->widget()); + + delete item; + } + } + ARMARX_CHECK_EQUAL(layout->count(), 0); + + layout->addWidget(splitter); + ARMARX_CHECK_EQUAL(layout->count(), 1); + ARMARX_CHECK_EQUAL(layout->itemAt(0)->widget(), splitter); + + return splitter; +} + +armarx::gui::LeadingZeroSpinBox::LeadingZeroSpinBox(int numDigits, int base) : + numDigits(numDigits), base(base) +{ +} + +QString +armarx::gui::LeadingZeroSpinBox::textFromValue(int value) const +{ + return QString("%1").arg(value, numDigits, base, QChar('0')); +} diff --git a/source/RobotAPI/libraries/skills_gui/gui_utils.h b/source/RobotAPI/libraries/skills_gui/gui_utils.h new file mode 100644 index 000000000..1b7b810ad --- /dev/null +++ b/source/RobotAPI/libraries/skills_gui/gui_utils.h @@ -0,0 +1,74 @@ +#pragma once + +#include <QLayout> +#include <QLayoutItem> +#include <QSpinBox> +#include <QWidget> + +class QLayout; +class QSplitter; +class QTreeWidgetItem; + + +namespace armarx::gui +{ + /** + * @brief Clear a layout. + * + * Recursively take and delete all items in `layout`. + * + * @param layout The layout. + */ + void clearLayout(QLayout* layout); + + /** + * @brief Clear a tree widget item + * + * Take and delete all children of `item`. + * + * @param item The tree widget item. + */ + void clearItem(QTreeWidgetItem* item); + + + + template <class WidgetT> + void replaceWidget(WidgetT*& old, QWidget* neu, QLayout* parentLayout) + { + QLayoutItem* oldItem = parentLayout->replaceWidget(old, neu); + if (oldItem) + { + delete oldItem; + delete old; + old = nullptr; + } + } + + /** + * @brief Let items in `layout` be children of a splitter. + * + * Items in `layout` are moved to a new `QSplitter`, which will + * be the only child of `layout`. + * If `layout` is a Q{H,V}BoxLayout, the respective orientation + * will be adopted. + * + * @param The parent layout. + * @return The splitter item. + */ + QSplitter* useSplitter(QLayout* layout); + + // Source: https://stackoverflow.com/a/26538572 + class LeadingZeroSpinBox : public QSpinBox + { + using QSpinBox::QSpinBox; + + public: + LeadingZeroSpinBox(int numDigits, int base); + + QString textFromValue(int value) const override; + + private: + int numDigits; + int base; + }; +} diff --git a/source/RobotAPI/libraries/skills_gui/memory/SkillMemoryProxy.h b/source/RobotAPI/libraries/skills_gui/memory/SkillMemoryProxy.h index d5ba4676b..f1490bbd9 100644 --- a/source/RobotAPI/libraries/skills_gui/memory/SkillMemoryProxy.h +++ b/source/RobotAPI/libraries/skills_gui/memory/SkillMemoryProxy.h @@ -19,13 +19,15 @@ namespace armarx::skills::gui { } - // No memory = no good - SkillMemoryProxy() = delete; + // We can instantiate the proxy without memory, if it's not connected yet. + SkillMemoryProxy(QObject* parent = nullptr) : QObject(parent) + { + } /* - * Replaces the memory pointer. This should be called whenever the GUI reconnects. + * Updates the memory pointer. This should be called whenever the GUI connects/reconnects. */ - void reInitialize(skills::dti::SkillMemoryInterfacePrx* updatedMemory); + void initialize(skills::dti::SkillMemoryInterfacePrx* updatedMemory); // The most current snapshot of the memory. Updated with each memory fetch. struct Snapshot @@ -47,7 +49,7 @@ namespace armarx::skills::gui * If this doesn't succeed, it will log a message. */ void stopExecution(skills::SkillExecutionID const& executionId, - const unsigned int attempts = 3); + const unsigned int attempts = 1); /* * Returns a snapshot, which contains the most current update from memory. diff --git a/source/RobotAPI/libraries/skills_gui/skill_issues/todo.txt b/source/RobotAPI/libraries/skills_gui/skill_issues/todo.txt new file mode 100644 index 000000000..92939d259 --- /dev/null +++ b/source/RobotAPI/libraries/skills_gui/skill_issues/todo.txt @@ -0,0 +1 @@ +Outsource timer widget and gui utils to ArmarXGui -- GitLab