From 1fb424e9fe0af103ef7ae2dfa41baf596e480bbb Mon Sep 17 00:00:00 2001
From: Malte Laurin Matthey <malte.matthey@student.kit.edu>
Date: Thu, 16 Jan 2025 12:10:48 +0100
Subject: [PATCH] Add toast messages into subtasks (for confirming showing
 solution, resetting etc.), small documentation improvement

---
 README.md                                     |  4 +-
 .../src/learn_environment/CMakeLists.txt      |  2 +
 .../learn_environment/subtask_item.hpp        | 13 +++-
 .../include/learn_environment/toast.hpp       | 23 ++++++
 .../learn_environment/src/subtask_item.cpp    | 41 +++++++++-
 catkin_ws/src/learn_environment/src/toast.cpp | 78 +++++++++++++++++++
 .../tasks/GETTING_STARTED.md                  |  1 +
 7 files changed, 154 insertions(+), 8 deletions(-)
 create mode 100644 catkin_ws/src/learn_environment/include/learn_environment/toast.hpp
 create mode 100644 catkin_ws/src/learn_environment/src/toast.cpp

diff --git a/README.md b/README.md
index caf4f08..4d0e5bf 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # ROS Noetic Standard Container with Preinstalled Learn Environment Plugin
 
-> **⚠️ WARNING:** This README is for setting up the container with all functionalities. For the Learn Environment Plugin, follow the instructions in this README first. Then, refer to [GETTING_STARTED.md](./catkin_ws/src/learn_environment/tasks/GETTING_STARTED.md) for starting the turotial or [CONTRIBUTE.md](./catkin_ws/src/learn_environment/developer_docs/CONTRIBUTE.md) for contribution guidelines (creating new tasks & extending the plugin).
+> **⚠️ WARNING:** This README is for setting up the container with all functionalities. For the Learn Environment Plugin, follow the instructions in this README first. Then, refer to [GETTING_STARTED.md](./catkin_ws/src/learn_environment/tasks/GETTING_STARTED.md) for starting the tutorial or [CONTRIBUTE.md](./catkin_ws/src/learn_environment/developer_docs/CONTRIBUTE.md) for contribution guidelines (creating new tasks & extending the plugin).
 
 
 This repository provides a Visual Studio Code development container with ROS Noetic installed to control a Franka Panda Robot in both simulation and real environments. It also has the Learn Environment plugin for RViz preinstalled so you can start learning how to work with the robot immediatly.
@@ -148,7 +148,7 @@ Access the desktop environment of the container in your browser at [http://local
 
 To get started with the plugin, follow the instructions provided [here](./catkin_ws/src/learn_environment/tasks/GETTING_STARTED.md). If VS Code does not automatically open the `/catkin_ws/src/learn_environment/tasks` folder, please navigate to it manually. This is the starting point of the tutorial.
 
-![Gazebo RVIZ Sim](/screenshots/plugin.png)
+![Learn Environment](/screenshots/plugin.png)
 
 ### Control the Real Panda
 
diff --git a/catkin_ws/src/learn_environment/CMakeLists.txt b/catkin_ws/src/learn_environment/CMakeLists.txt
index bac241f..3c96415 100644
--- a/catkin_ws/src/learn_environment/CMakeLists.txt
+++ b/catkin_ws/src/learn_environment/CMakeLists.txt
@@ -58,6 +58,7 @@ set(${PROJECT_NAME}_HDRS
   include/${PROJECT_NAME}/notebook_converter.hpp
   include/${PROJECT_NAME}/folder_structure_constants.hpp
   include/${PROJECT_NAME}/execute_frame.hpp
+  include/${PROJECT_NAME}/toast.hpp
 )
 
 set(${PROJECT_NAME}_SRCS
@@ -73,6 +74,7 @@ set(${PROJECT_NAME}_SRCS
   src/task_ui.cpp
   src/notebook_converter.cpp
   src/execute_frame.cpp
+  src/toast.cpp
 )
 
 set(${PROJECT_NAME}_UIS
diff --git a/catkin_ws/src/learn_environment/include/learn_environment/subtask_item.hpp b/catkin_ws/src/learn_environment/include/learn_environment/subtask_item.hpp
index 139b144..2b2a447 100644
--- a/catkin_ws/src/learn_environment/include/learn_environment/subtask_item.hpp
+++ b/catkin_ws/src/learn_environment/include/learn_environment/subtask_item.hpp
@@ -4,6 +4,7 @@
 #include "task.hpp"
 #include "task_manager.hpp"
 #include "execute_frame.hpp"
+#include "toast.hpp"
 
 #include <QWidget>
 #include <QPushButton>
@@ -59,7 +60,10 @@ private Q_SLOTS:
     void handleStartOwnScript();
     void handleStartSolution();
     void handleToggleSolution();
-    void handleResetNotebook();    
+    void handleResetNotebook(); 
+
+protected:
+    void resizeEvent(QResizeEvent* event) override;  
 
 private:
     /**
@@ -72,6 +76,7 @@ private:
     void initializeHelpMenu();
     void initializeStartMenu();
     void setExecutionFrame(const QString& imagePath, const QString& text);
+    void showToast(const QString &message); ///< Displays a toast message.
 
     TaskManager *taskManager; ///< Pointer to the TaskManager object.
     Subtask *subtask; ///< Pointer to the subtask object.
@@ -89,8 +94,10 @@ private:
     QPushButton *menuToggleSolutionBtn; ///< Button to toggle the solution.
     QPushButton *menuResetNotebookBtn; ///< Button to reset the notebook.
 
-    QMenu* startMenu; ///< Menu for starting the subtask.
-    QMenu* helpMenu; ///< Menu for showing the help options.
+    QMenu *startMenu; ///< Menu for starting the subtask.
+    QMenu *helpMenu; ///< Menu for showing the help options.
+
+    Toast *toast; ///< Toast message for subtask.
 };
 
 #endif // SUBTASK_ITEM_HPP
\ No newline at end of file
diff --git a/catkin_ws/src/learn_environment/include/learn_environment/toast.hpp b/catkin_ws/src/learn_environment/include/learn_environment/toast.hpp
new file mode 100644
index 0000000..3d982e1
--- /dev/null
+++ b/catkin_ws/src/learn_environment/include/learn_environment/toast.hpp
@@ -0,0 +1,23 @@
+#ifndef TOAST_HPP
+#define TOAST_HPP
+
+#include <QWidget>
+#include <QLabel>
+#include <QTimer>
+
+class Toast : public QWidget {
+    Q_OBJECT
+
+public:
+    explicit Toast(QWidget *parent = nullptr);
+    void showToast(const QString &message);
+
+private Q_SLOTS:
+    void fadeOut();
+
+private:
+    QLabel *toastLabel;
+    QTimer *toastTimer;
+};
+
+#endif // TOAST_HPP
\ No newline at end of file
diff --git a/catkin_ws/src/learn_environment/src/subtask_item.cpp b/catkin_ws/src/learn_environment/src/subtask_item.cpp
index de37eb4..287af31 100644
--- a/catkin_ws/src/learn_environment/src/subtask_item.cpp
+++ b/catkin_ws/src/learn_environment/src/subtask_item.cpp
@@ -51,6 +51,11 @@ namespace {
     const char* HIDE_SOLUTION_TEXT = "Hide Solution";
     const char* RESET_NOTEBOOK_TEXT = "Reset Notebook";
 
+    const char* SHOWING_SOLUTION_TOAST = "Solution added to your notebook.";
+    const char* SHOWING_SOLUTION_FAILED_TOAST = "There is no solution for this task.";
+    const char* HIDE_SOLUTION_TOAST = "Solution removed from your notebook.";
+    const char* NOTEBOOK_RESET_TOAST = "Notebook has been reset.";
+
     const char* RESET_NOTEBOOK_CONFIRMATION_TEXT = "This will remove all changes made to the notebook and can't be undone. Are you sure you want to proceed?";
 }
 
@@ -61,12 +66,23 @@ SubtaskItem::SubtaskItem(QWidget *parent, Subtask *subtask)
       bodyText(subtask->description),
       subtask(subtask),
       taskManager(nullptr),
-      executeSubtaskFrame(nullptr) {
+      executeSubtaskFrame(nullptr),
+      toast(new Toast(this)) {
     
     setupItemUI(headerText, linkText, bodyText);
     updateUI(true);
     initializeHelpMenu();
     initializeStartMenu();
+
+    resizeEvent(nullptr); // Initial positioning of toast
+}
+
+void SubtaskItem::resizeEvent(QResizeEvent* event) {
+    QWidget::resizeEvent(event);
+}
+
+void SubtaskItem::showToast(const QString &message) {
+    toast->showToast(message);
 }
 
 void SubtaskItem::updateUI(bool constructorCall)
@@ -184,9 +200,27 @@ void SubtaskItem::handleStartSolution() {
 }
 
 void SubtaskItem::handleToggleSolution() {
-    if (taskManager && subtask) {
-        taskManager->toggleSolution(*subtask);
+    if (!taskManager || !subtask) {
+        return;
+    }
+
+    QString notebookPath = subtask->filePath;
+    if (!QFile::exists(notebookPath)) {
+        return;
     }
+
+    bool hadSolutionBeforeToggle = NotebookConverter::hasSolutionCells(notebookPath);
+    taskManager->toggleSolution(*subtask);
+    bool hasSolutionAfterToggle = NotebookConverter::hasSolutionCells(notebookPath);
+    
+    if (hasSolutionAfterToggle && !hadSolutionBeforeToggle) {
+        showToast(SHOWING_SOLUTION_TOAST);
+    } else if (!hasSolutionAfterToggle && !hadSolutionBeforeToggle) {
+        showToast(SHOWING_SOLUTION_FAILED_TOAST);
+    } else {
+        showToast(HIDE_SOLUTION_TOAST);
+    }
+    
     QMenu* menu = qobject_cast<QMenu*>(sender()->parent()->parent());
     if (menu) {
         menu->close();
@@ -204,6 +238,7 @@ void SubtaskItem::handleResetNotebook() {
         if (taskManager && subtask) {
             NotebookConverter* converter = new NotebookConverter();
             converter->resetNotebook(subtask->filePath, subtask->solutionFilePath);
+            showToast(NOTEBOOK_RESET_TOAST);
         }
     }
     QMenu* menu = qobject_cast<QMenu*>(sender()->parent()->parent());
diff --git a/catkin_ws/src/learn_environment/src/toast.cpp b/catkin_ws/src/learn_environment/src/toast.cpp
new file mode 100644
index 0000000..54ef293
--- /dev/null
+++ b/catkin_ws/src/learn_environment/src/toast.cpp
@@ -0,0 +1,78 @@
+#include "learn_environment/toast.hpp"
+
+#include <QGraphicsOpacityEffect>
+#include <QPropertyAnimation>
+#include <QVBoxLayout>
+
+Toast::Toast(QWidget *parent) : QWidget(parent), toastLabel(new QLabel(this)), toastTimer(new QTimer(this)) {
+    this->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
+    
+    QVBoxLayout *layout = new QVBoxLayout(this);
+    layout->addWidget(toastLabel);
+    layout->setContentsMargins(0, 0, 0, 0);
+    
+    toastLabel->setStyleSheet("background-color: rgba(0, 0, 0, 160); color: white; padding: 6px; border-radius: 6px;");
+    toastLabel->setAlignment(Qt::AlignCenter);
+    toastLabel->setWordWrap(true);
+    QFont toastFont = toastLabel->font();
+    toastFont.setPointSize(10);
+    toastLabel->setFont(toastFont);
+    toastLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
+    toastLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
+    toastLabel->hide();
+
+    connect(toastTimer, &QTimer::timeout, this, &Toast::fadeOut);
+}
+
+void Toast::showToast(const QString &message) {
+    toastLabel->setText(message);
+    toastLabel->adjustSize();
+
+    this->resize(toastLabel->sizeHint());
+    toastLabel->move(0, 0);
+
+    int margin = 10;
+    int x = (parentWidget()->width() - this->width()) / 2;
+    int y = parentWidget()->height() - this->height() - margin;
+    this->move(x, y);
+
+    QGraphicsEffect* existingEffect = toastLabel->graphicsEffect();
+    if (existingEffect) {
+        delete existingEffect;
+        toastLabel->setGraphicsEffect(nullptr);
+    }
+
+    QGraphicsOpacityEffect* opacityEffect = new QGraphicsOpacityEffect(toastLabel);
+    toastLabel->setGraphicsEffect(opacityEffect);
+
+    QPropertyAnimation* fadeIn = new QPropertyAnimation(opacityEffect, "opacity");
+    fadeIn->setDuration(500);
+    fadeIn->setStartValue(0);
+    fadeIn->setEndValue(1);
+    fadeIn->setEasingCurve(QEasingCurve::InOutQuad);
+    fadeIn->start(QPropertyAnimation::DeleteWhenStopped);
+
+    this->raise();
+    toastLabel->show();
+    this->show();
+    toastTimer->start(2500);
+}
+
+void Toast::fadeOut() {
+    if (!toastLabel || !toastLabel->graphicsEffect() || toastLabel->isHidden())
+        return;
+
+    QGraphicsOpacityEffect* effect = qobject_cast<QGraphicsOpacityEffect*>(toastLabel->graphicsEffect());
+    if (!effect) return;
+
+    QPropertyAnimation* fadeOut = new QPropertyAnimation(effect, "opacity");
+    fadeOut->setDuration(500);
+    fadeOut->setStartValue(1);
+    fadeOut->setEndValue(0);
+    fadeOut->setEasingCurve(QEasingCurve::InOutQuad);
+    connect(fadeOut, &QPropertyAnimation::finished, this, [this, effect]() {
+        toastLabel->hide();
+        toastLabel->setGraphicsEffect(nullptr);
+    });
+    fadeOut->start(QPropertyAnimation::DeleteWhenStopped);
+}
\ No newline at end of file
diff --git a/catkin_ws/src/learn_environment/tasks/GETTING_STARTED.md b/catkin_ws/src/learn_environment/tasks/GETTING_STARTED.md
index 058cae8..23e09d4 100644
--- a/catkin_ws/src/learn_environment/tasks/GETTING_STARTED.md
+++ b/catkin_ws/src/learn_environment/tasks/GETTING_STARTED.md
@@ -3,6 +3,7 @@ To get started, follow these steps:
 
 1. **Open a Terminal in VS Code**
     Go to `Terminal` > `New Terminal` in the VS Code menu. The shortcut is ``Ctrl + ` ``
+    - If you don't use VS Code, open any terminal. If you installed this with a docker setup as recommended , make sure to open the terminal in the container.
 
 2. **Execute the Launch Command**
     
-- 
GitLab