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