diff --git a/README.md b/README.md index caf4f080f6188b8baf9f40b02ec4d0dee9f0f292..4d0e5bf9c321f89559e99b05ce82b32323404881 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. - + ### Control the Real Panda diff --git a/catkin_ws/src/learn_environment/CMakeLists.txt b/catkin_ws/src/learn_environment/CMakeLists.txt index bac241fb2903b836c996015279875fa6c2c52384..3c964159705d6b58953bc543412d98731de74603 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 139b1444c1d4aa141ed30905143fabe442bf3a1d..2b2a4472c8b3377a1d3aa2df56065b1826420138 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 0000000000000000000000000000000000000000..3d982e1d9f01710993fd15f61ec28f90add90bb8 --- /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 de37eb452d82d1f3ed6a2283f66aad94f918a36a..287af312fff888a835cd2e55d2b3b464274d0f98 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 0000000000000000000000000000000000000000..54ef293bf5ee21c9ad09e30c629c7ac752e33c4e --- /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 058cae8890d11367c26e7a395c54b25701910985..23e09d4222cd06fa705aec954aad8d4abe4d801d 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**