From f6c2a9fa75831ac279fe1ee22d91c9db29dc98a8 Mon Sep 17 00:00:00 2001
From: Rainer Kartmann <rainer.kartmann@kit.edu>
Date: Fri, 3 Nov 2023 11:07:34 +0100
Subject: [PATCH] Enable Markdown support in display of skill description in
 Skill Manager Gui

---
 .../SkillManagerPlugin/CMakeLists.txt         |  10 +-
 .../SkillManagerMonitorWidget.ui              | 199 ++++++++++++------
 .../SkillManagerMonitorWidgetController.cpp   |  48 ++---
 .../SkillManagerMonitorWidgetController.h     |   3 +
 .../widgets/SkillDescriptionWidget.cpp        |  72 +++++++
 .../widgets/SkillDescriptionWidget.h          |  27 +++
 6 files changed, 255 insertions(+), 104 deletions(-)
 create mode 100644 source/RobotAPI/gui-plugins/SkillManagerPlugin/aronTreeWidget/widgets/SkillDescriptionWidget.cpp
 create mode 100644 source/RobotAPI/gui-plugins/SkillManagerPlugin/aronTreeWidget/widgets/SkillDescriptionWidget.h

diff --git a/source/RobotAPI/gui-plugins/SkillManagerPlugin/CMakeLists.txt b/source/RobotAPI/gui-plugins/SkillManagerPlugin/CMakeLists.txt
index a6d2ff63c..113ccd870 100644
--- a/source/RobotAPI/gui-plugins/SkillManagerPlugin/CMakeLists.txt
+++ b/source/RobotAPI/gui-plugins/SkillManagerPlugin/CMakeLists.txt
@@ -10,12 +10,13 @@ set(SOURCES
     aronTreeWidget/visitors/AronTreeWidgetSetter.cpp
     aronTreeWidget/visitors/AronTreeWidgetModalCreator.cpp
     aronTreeWidget/visitors/AronTreeWidgetContextMenu.cpp
-    aronTreeWidget/Data.cpp
     aronTreeWidget/widgets/CustomWidget.cpp
     aronTreeWidget/widgets/EditMatrixWidget.cpp
     aronTreeWidget/widgets/IntEnumWidget.cpp
-    aronTreeWidget/ListDictHelper.cpp
     aronTreeWidget/widgets/QuaternionWidget.cpp
+    aronTreeWidget/widgets/SkillDescriptionWidget.cpp
+    aronTreeWidget/Data.cpp
+    aronTreeWidget/ListDictHelper.cpp
     aronTreeWidget/AronTreeWidgetItem.cpp
     aronTreeWidget/AronTreeWidgetController.cpp
     aronTreeWidget/modal/text/AronTreeWidgetTextInputModalController.cpp
@@ -34,13 +35,14 @@ set(HEADERS
     aronTreeWidget/visitors/AronTreeWidgetSetter.h
     aronTreeWidget/visitors/AronTreeWidgetModalCreator.h
     aronTreeWidget/visitors/AronTreeWidgetContextMenu.h
-    aronTreeWidget/Data.h
     aronTreeWidget/widgets/NDArrayHelper.h
     aronTreeWidget/widgets/EditMatrixWidget.h
     aronTreeWidget/widgets/CustomWidget.h
     aronTreeWidget/widgets/IntEnumWidget.h
-    aronTreeWidget/ListDictHelper.h
     aronTreeWidget/widgets/QuaternionWidget.h
+    aronTreeWidget/widgets/SkillDescriptionWidget.h
+    aronTreeWidget/Data.h
+    aronTreeWidget/ListDictHelper.h
     aronTreeWidget/AronTreeWidgetItem.h
     aronTreeWidget/AronTreeWidgetController.h
     aronTreeWidget/modal/AronTreeWidgetModal.h
diff --git a/source/RobotAPI/gui-plugins/SkillManagerPlugin/SkillManagerMonitorWidget.ui b/source/RobotAPI/gui-plugins/SkillManagerPlugin/SkillManagerMonitorWidget.ui
index b96d3850e..83e522b9d 100644
--- a/source/RobotAPI/gui-plugins/SkillManagerPlugin/SkillManagerMonitorWidget.ui
+++ b/source/RobotAPI/gui-plugins/SkillManagerPlugin/SkillManagerMonitorWidget.ui
@@ -20,6 +20,9 @@
    <string>SkillManagerMonitorWidget</string>
   </property>
   <layout class="QGridLayout" name="gridLayout_3">
+   <item row="3" column="2">
+    <widget class="QDoubleSpinBox" name="doubleSpinBoxUpdateFreq"/>
+   </item>
    <item row="3" column="0">
     <widget class="QCheckBox" name="checkBoxAutoUpdate">
      <property name="text">
@@ -27,16 +30,6 @@
      </property>
     </widget>
    </item>
-   <item row="3" column="3">
-    <widget class="QPushButton" name="pushButtonRefreshNow">
-     <property name="text">
-      <string>Refresh Now</string>
-     </property>
-    </widget>
-   </item>
-   <item row="3" column="2">
-    <widget class="QDoubleSpinBox" name="doubleSpinBoxUpdateFreq"/>
-   </item>
    <item row="3" column="1">
     <widget class="QLabel" name="label">
      <property name="text">
@@ -47,6 +40,13 @@
      </property>
     </widget>
    </item>
+   <item row="3" column="3">
+    <widget class="QPushButton" name="pushButtonRefreshNow">
+     <property name="text">
+      <string>Refresh Now</string>
+     </property>
+    </widget>
+   </item>
    <item row="4" column="0" colspan="4">
     <widget class="QSplitter" name="splitter_2">
      <property name="enabled">
@@ -190,72 +190,137 @@
        <property name="title">
         <string>Skill Details</string>
        </property>
-       <layout class="QGridLayout" name="gridLayout_2">
-        <item row="8" column="3">
-         <widget class="QPushButton" name="pushButtonExecuteSkill">
-          <property name="text">
-           <string>Request Execution</string>
+       <layout class="QVBoxLayout" name="verticalLayout">
+        <item>
+         <widget class="QSplitter" name="splitter_2">
+          <property name="enabled">
+           <bool>true</bool>
           </property>
-         </widget>
-        </item>
-        <item row="0" column="0">
-         <widget class="QPushButton" name="pushButtonPaste">
-          <property name="text">
-           <string>Set args from clipboard</string>
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
           </property>
-         </widget>
-        </item>
-        <item row="0" column="3">
-         <widget class="QPushButton" name="pushButtonReset">
-          <property name="text">
-           <string>Reset args to profile</string>
-          </property>
-         </widget>
-        </item>
-        <item row="3" column="0" colspan="4">
-         <widget class="QTreeWidget" name="treeWidgetSkillDetails">
-          <property name="contextMenuPolicy">
-           <enum>Qt::CustomContextMenu</enum>
+          <property name="orientation">
+           <enum>Qt::Vertical</enum>
           </property>
-          <property name="editTriggers">
-           <set>QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed</set>
+          <property name="childrenCollapsible">
+           <bool>false</bool>
           </property>
-          <column>
-           <property name="text">
-            <string>Key</string>
+          <widget class="QWidget" name="groupBoxSkillDetailsTop" native="true">
+          <layout class="QVBoxLayout" name="verticalLayout_2">
+           <property name="leftMargin">
+            <number>0</number>
            </property>
-          </column>
-          <column>
-           <property name="text">
-            <string>Value</string>
+           <property name="topMargin">
+            <number>0</number>
            </property>
-          </column>
-          <column>
-           <property name="text">
-            <string>Type</string>
+           <property name="rightMargin">
+            <number>0</number>
            </property>
-          </column>
-          <column>
-           <property name="text">
-            <string>defaultValue (hidden in GUI)</string>
+           <property name="bottomMargin">
+            <number>0</number>
            </property>
-          </column>
-         </widget>
-        </item>
-        <item row="0" column="1">
-         <widget class="QPushButton" name="pushButtonCopy">
-          <property name="text">
-           <string>Copy args to clipboard</string>
-          </property>
+           <item>
+            <layout class="QGridLayout" name="gridLayout_5">
+             <item row="0" column="0">
+              <widget class="QPushButton" name="pushButtonPaste">
+               <property name="text">
+                <string>Set args from clipboard</string>
+               </property>
+              </widget>
+             </item>
+             <item row="0" column="3">
+              <widget class="QPushButton" name="pushButtonReset">
+               <property name="text">
+                <string>Reset args to profile</string>
+               </property>
+              </widget>
+             </item>
+             <item row="1" column="0" colspan="4">
+              <widget class="QComboBox" name="comboBoxProfiles">
+               <item>
+                <property name="text">
+                 <string>&lt;No Profile selected. Using root&gt;</string>
+                </property>
+               </item>
+              </widget>
+             </item>
+             <item row="0" column="1">
+              <widget class="QPushButton" name="pushButtonCopy">
+               <property name="text">
+                <string>Copy args to clipboard</string>
+               </property>
+              </widget>
+             </item>
+             <item row="0" column="2">
+              <widget class="QLabel" name="label_2">
+               <property name="text">
+                <string/>
+               </property>
+              </widget>
+             </item>
+            </layout>
+           </item>
+           <item>
+            <widget class="QWidget" name="skillDescription" native="true"/>
+           </item>
+          </layout>
          </widget>
-        </item>
-        <item row="1" column="1" colspan="3">
-         <widget class="QComboBox" name="comboBoxProfiles">
-          <item>
-           <property name="text">
-            <string>&lt;No Profile selected. Using root&gt;</string>
-           </property>
-          </item>
+          <widget class="QWidget" name="groupBoxSkillDetailsBottom" native="true">
+           <layout class="QVBoxLayout" name="verticalLayout_3">
+            <property name="leftMargin">
+             <number>0</number>
+            </property>
+            <property name="topMargin">
+             <number>0</number>
+            </property>
+            <property name="rightMargin">
+             <number>0</number>
+            </property>
+            <property name="bottomMargin">
+             <number>0</number>
+            </property>
+            <item>
+             <widget class="QTreeWidget" name="treeWidgetSkillDetails">
+              <property name="contextMenuPolicy">
+               <enum>Qt::CustomContextMenu</enum>
+              </property>
+              <property name="editTriggers">
+               <set>QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed</set>
+              </property>
+              <column>
+               <property name="text">
+                <string>Key</string>
+               </property>
+              </column>
+              <column>
+               <property name="text">
+                <string>Value</string>
+               </property>
+              </column>
+              <column>
+               <property name="text">
+                <string>Type</string>
+               </property>
+              </column>
+              <column>
+               <property name="text">
+                <string>defaultValue (hidden in GUI)</string>
+               </property>
+              </column>
+             </widget>
+            </item>
+            <item>
+             <widget class="QPushButton" name="pushButtonExecuteSkill">
+              <property name="text">
+               <string>Request Execution</string>
+              </property>
+             </widget>
+            </item>
+           </layout>
+          </widget>
          </widget>
         </item>
        </layout>
diff --git a/source/RobotAPI/gui-plugins/SkillManagerPlugin/SkillManagerMonitorWidgetController.cpp b/source/RobotAPI/gui-plugins/SkillManagerPlugin/SkillManagerMonitorWidgetController.cpp
index b35fb56f3..8b88718f9 100644
--- a/source/RobotAPI/gui-plugins/SkillManagerPlugin/SkillManagerMonitorWidgetController.cpp
+++ b/source/RobotAPI/gui-plugins/SkillManagerPlugin/SkillManagerMonitorWidgetController.cpp
@@ -24,25 +24,22 @@
 
 #include <string>
 
-#include <RobotAPI/libraries/skills/core/Skill.h>
-
-#include "aronTreeWidget/visitors/AronTreeWidgetConverter.h"
-#include "aronTreeWidget/visitors/AronTreeWidgetCreator.h"
-#include "aronTreeWidget/visitors/AronTreeWidgetModalCreator.h"
-
-// modals
-#include "aronTreeWidget/modal/text/AronTreeWidgetTextInputModalController.h"
-
-// debug
 #include <QClipboard>
 #include <QDoubleSpinBox>
+#include <QGridLayout>
+#include <QTextBrowser>
 
 #include <RobotAPI/libraries/aron/converter/json/NLohmannJSONConverter.h>
+#include <RobotAPI/libraries/skills/core/Skill.h>
 #include <RobotAPI/libraries/skills/core/SkillExecutionRequest.h>
 
 #include "aronTreeWidget/Data.h"
+#include "aronTreeWidget/modal/text/AronTreeWidgetTextInputModalController.h"
+#include "aronTreeWidget/visitors/AronTreeWidgetConverter.h"
+#include "aronTreeWidget/visitors/AronTreeWidgetCreator.h"
+#include "aronTreeWidget/visitors/AronTreeWidgetModalCreator.h"
+#include "aronTreeWidget/widgets/SkillDescriptionWidget.h"
 
-//configSk
 namespace armarx
 {
     QPointer<QDialog>
@@ -112,6 +109,12 @@ namespace armarx
         widget.doubleSpinBoxUpdateFreq->setSingleStep(0.5);
         widget.doubleSpinBoxUpdateFreq->setSuffix(" Hz");
 
+        skillDescriptionWidget = new SkillDescriptionWidget();
+        widget.skillDescription->parentWidget()->layout()->replaceWidget(widget.skillDescription,
+                                                                         skillDescriptionWidget);
+        widget.skillDescription = skillDescriptionWidget;
+
+
         refreshSkillsResultTimer = new QTimer(this);
         updateTimerFrequency();
         refreshSkillsResultTimer->start();
@@ -534,28 +537,7 @@ namespace armarx
         ARMARX_CHECK(skills.at(*selectedSkill.skillId.providerId).count(selectedSkill.skillId) > 0);
         auto skillDesc = skills.at(*selectedSkill.skillId.providerId).at(selectedSkill.skillId);
 
-        {
-            auto it = new QTreeWidgetItem(widget.treeWidgetSkillDetails,
-                                          {QString::fromStdString("Name"),
-                                           QString::fromStdString(skillDesc.skillId.skillName)});
-            widget.treeWidgetSkillDetails->addTopLevelItem(it);
-        }
-
-        {
-            auto it = new QTreeWidgetItem(widget.treeWidgetSkillDetails,
-                                          {QString::fromStdString("Description"),
-                                           QString::fromStdString(skillDesc.description)});
-            widget.treeWidgetSkillDetails->addTopLevelItem(it);
-        }
-
-        {
-            auto it = new QTreeWidgetItem(
-                widget.treeWidgetSkillDetails,
-                {QString::fromStdString("Timeout"),
-                 QString::fromStdString(std::to_string(skillDesc.timeout.toMilliSeconds())) +
-                     " ms"});
-            widget.treeWidgetSkillDetails->addTopLevelItem(it);
-        }
+        skillDescriptionWidget->setSkillDescription(skillDesc);
 
         // select root profile
         widget.comboBoxProfiles->setCurrentIndex(0);
diff --git a/source/RobotAPI/gui-plugins/SkillManagerPlugin/SkillManagerMonitorWidgetController.h b/source/RobotAPI/gui-plugins/SkillManagerPlugin/SkillManagerMonitorWidgetController.h
index 1fd2e2f11..9f251bf9e 100644
--- a/source/RobotAPI/gui-plugins/SkillManagerPlugin/SkillManagerMonitorWidgetController.h
+++ b/source/RobotAPI/gui-plugins/SkillManagerPlugin/SkillManagerMonitorWidgetController.h
@@ -46,6 +46,8 @@
 
 namespace armarx
 {
+    class SkillDescriptionWidget;
+
     class SkillInfoTreeWidgetItem : public QTreeWidgetItem
     {
     public:
@@ -166,6 +168,7 @@ namespace armarx
 
         // others
         QTimer* refreshSkillsResultTimer;
+        SkillDescriptionWidget* skillDescriptionWidget = nullptr;
 
         // connected flag
         std::atomic_bool connected = false;
diff --git a/source/RobotAPI/gui-plugins/SkillManagerPlugin/aronTreeWidget/widgets/SkillDescriptionWidget.cpp b/source/RobotAPI/gui-plugins/SkillManagerPlugin/aronTreeWidget/widgets/SkillDescriptionWidget.cpp
new file mode 100644
index 000000000..60f5908c7
--- /dev/null
+++ b/source/RobotAPI/gui-plugins/SkillManagerPlugin/aronTreeWidget/widgets/SkillDescriptionWidget.cpp
@@ -0,0 +1,72 @@
+#include "SkillDescriptionWidget.h"
+
+#include <sstream>
+
+#include <QGridLayout>
+#include <QLabel>
+#include <QTextEdit>
+
+#include <ArmarXGui/libraries/ArmarXGuiBase/widgets/cpp-markdown/markdown.h> // ToDo: Move cpp-markdown to own Axii module.
+
+namespace armarx
+{
+
+    static std::string
+    markdownToHtml(const std::string& markdownText, size_t spacesPerTab = 2)
+    {
+        ::markdown::Document document(spacesPerTab);
+        document.read(markdownText);
+
+        std::stringstream html;
+        document.write(html);
+        return html.str();
+    }
+
+    SkillDescriptionWidget::SkillDescriptionWidget(QWidget* parent) : QWidget{parent}
+    {
+        QGridLayout* layout = new QGridLayout;
+        setLayout(layout);
+
+        layout->setMargin(0);
+
+        int row = 0;
+        int col = 0;
+
+        layout->addWidget(new QLabel("Name:"), row, col++);
+        name = new QLabel;
+        layout->addWidget(name, row, col++);
+
+        // Add some padding.
+        layout->addWidget(new QLabel(), row, col++);
+
+        {
+            QLabel* label = new QLabel("Timeout:");
+            label->setAlignment(Qt::AlignmentFlag::AlignRight);
+            layout->addWidget(label, row, col++);
+        }
+        timeout = new QLabel;
+        timeout->setAlignment(Qt::AlignmentFlag::AlignRight);
+        layout->addWidget(timeout, row, col++);
+
+        ++row;
+        int numCols = col;
+        col = 0;
+
+        description = new QTextEdit;
+        description->setReadOnly(true);
+        layout->addWidget(description, row, col, 1, numCols);
+        ++row;
+    }
+
+    void
+    SkillDescriptionWidget::setSkillDescription(const skills::SkillDescription& desc)
+    {
+        name->setText(QString::fromStdString(desc.skillId.skillName));
+        description->setHtml(QString::fromStdString(markdownToHtml(desc.description)));
+
+        std::stringstream timeoutStr;
+        timeoutStr << desc.timeout;
+        timeout->setText(QString::fromStdString(timeoutStr.str()));
+    }
+
+} // namespace armarx
diff --git a/source/RobotAPI/gui-plugins/SkillManagerPlugin/aronTreeWidget/widgets/SkillDescriptionWidget.h b/source/RobotAPI/gui-plugins/SkillManagerPlugin/aronTreeWidget/widgets/SkillDescriptionWidget.h
new file mode 100644
index 000000000..b58f06d0d
--- /dev/null
+++ b/source/RobotAPI/gui-plugins/SkillManagerPlugin/aronTreeWidget/widgets/SkillDescriptionWidget.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include <QWidget>
+
+#include <RobotAPI/libraries/skills/core/SkillDescription.h>
+
+class QLabel;
+class QTextEdit;
+
+namespace armarx
+{
+
+    class SkillDescriptionWidget : public QWidget
+    {
+    public:
+        SkillDescriptionWidget(QWidget* parent = nullptr);
+
+        void setSkillDescription(const skills::SkillDescription& desc);
+
+    private:
+        QLabel* name = nullptr;
+        QTextEdit* description = nullptr;
+        QLabel* timeout = nullptr;
+    };
+
+
+} // namespace armarx
-- 
GitLab