diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e36175c3a..53e67b817 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,7 @@ jobs: uses: ignition-tooling/action-ignition-ci@bionic with: codecov-enabled: true + doxygen-enabled: true focal-ci: runs-on: ubuntu-latest name: Ubuntu Focal CI diff --git a/CMakeLists.txt b/CMakeLists.txt index 97aed8b4a..623dfc206 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -109,14 +109,15 @@ ign_create_docs( API_MAINPAGE_MD "${CMAKE_BINARY_DIR}/api.md" TUTORIALS_MAINPAGE_MD "${CMAKE_BINARY_DIR}/tutorials.md" ADDITIONAL_INPUT_DIRS "${CMAKE_SOURCE_DIR}/src/plugins" + IMAGE_PATH_DIRS "${CMAKE_SOURCE_DIR}/tutorials/images" TAGFILES - "${CMAKE_SOURCE_DIR}/doc/qt.tag.xml=http://doc.qt.io/qt-5/" - "${IGNITION-MATH_DOXYGEN_TAGFILE} = ${IGNITION-MATH_API_URL}" - "${IGNITION-MSGS_DOXYGEN_TAGFILE} = ${IGNITION-MSGS_API_URL}" - "${IGNITION-RENDERING_DOXYGEN_TAGFILE} = ${IGNITION-RENDERING_API_URL}" - "${IGNITION-TRANSPORT_DOXYGEN_TAGFILE} = ${IGNITION-TRANSPORT_API_URL}" - "${IGNITION-COMMON_DOXYGEN_TAGFILE} = ${IGNITION-COMMON_API_URL}" - ) + "${CMAKE_SOURCE_DIR}/doc/qt.tag.xml=http://doc.qt.io/qt-5/" + "${IGNITION-MATH_DOXYGEN_TAGFILE} = ${IGNITION-MATH_API_URL}" + "${IGNITION-MSGS_DOXYGEN_TAGFILE} = ${IGNITION-MSGS_API_URL}" + "${IGNITION-RENDERING_DOXYGEN_TAGFILE} = ${IGNITION-RENDERING_API_URL}" + "${IGNITION-TRANSPORT_DOXYGEN_TAGFILE} = ${IGNITION-TRANSPORT_API_URL}" + "${IGNITION-COMMON_DOXYGEN_TAGFILE} = ${IGNITION-COMMON_API_URL}" +) if(TARGET doc) file(COPY ${CMAKE_SOURCE_DIR}/tutorials/images/ DESTINATION ${CMAKE_BINARY_DIR}/doxygen/html/images/) diff --git a/Changelog.md b/Changelog.md index 3dfe096e8..2a1827e4b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,15 +1,32 @@ -## Ignition Gui 6 +## Ignition Gui 7 ### Ignition Gui 7.X.X ### Ignition Gui 7.0.0 (202X-XX-XX) -### Ignition Gui 6.X.X +## Ignition Gui 6 + +### Ignition Gui 6.1.0 (2021-11-05) + +1. Improved doxygen + * [Pull request #275](https://github.com/ignitionrobotics/ign-gui/pull/275) + +1. Fix mimimal scene deadlock on shutdown + * [Pull request #300](https://github.com/ignitionrobotics/ign-gui/pull/300) + +1. Fix memory leak + * [Pull request #287](https://github.com/ignitionrobotics/ign-gui/pull/287) + +1. Set near/far camera clipping distance + * [Pull request #309](https://github.com/ignitionrobotics/ign-gui/pull/309) + +1. Support emitting an event on play/pause/step + * [Pull request #306](https://github.com/ignitionrobotics/ign-gui/pull/306) ### Ignition Gui 6.0.0 (2021-09-XX) -1. Render engine GUI name argument to be set in the minimal scene - * [Pull request #286](https://github.com/ignitionrobotics/ign-gui/pull/286) +1. Add a filter to the plugin menu + * [Pull request #277](https://github.com/ignitionrobotics/ign-gui/pull/277) 1. New events @@ -39,6 +56,9 @@ 1. PIMPL GUI events * [Pull request #253](https://github.com/ignitionrobotics/ign-gui/pull/253) +1. Added winID to fix second windows in OGRE2.2 + * [Pull request #292](https://github.com/ignitionrobotics/ign-gui/pull/292) + 1. Added camera tracking * [Pull request #226](https://github.com/ignitionrobotics/ign-gui/pull/226) * [Pull request #254](https://github.com/ignitionrobotics/ign-gui/pull/254) @@ -49,6 +69,10 @@ * [Pull request #285](https://github.com/ignitionrobotics/ign-gui/pull/285) * [Pull request #268](https://github.com/ignitionrobotics/ign-gui/pull/268) * [Pull request #224](https://github.com/ignitionrobotics/ign-gui/pull/224) + * [Pull request #282](https://github.com/ignitionrobotics/ign-gui/pull/282) + * [Pull request #273](https://github.com/ignitionrobotics/ign-gui/pull/273) + * [Pull request #286](https://github.com/ignitionrobotics/ign-gui/pull/286) + * [Pull request #295](https://github.com/ignitionrobotics/ign-gui/pull/295) 1. Remove deprecations: tock * [Pull request #243](https://github.com/ignitionrobotics/ign-gui/pull/243) @@ -66,6 +90,23 @@ ### Ignition Gui 5.X.X (20XX-XX-XX) +### Ignition Gui 5.3.0 (2021-10-13) + +1. Improved doxygen + * [Pull request #275](https://github.com/ignitionrobotics/ign-gui/pull/275) + +1. Fix memory leak + * [Pull request #287](https://github.com/ignitionrobotics/ign-gui/pull/287) + +1. Add a filter to the plugin menu + * [Pull request #277](https://github.com/ignitionrobotics/ign-gui/pull/277) + +1. 👩‍🌾 Remove bitbucket-pipelines.yml + * [Pull request #274](https://github.com/ignitionrobotics/ign-gui/pull/274) + +1. Fix doc build error + * [Pull request #266](https://github.com/ignitionrobotics/ign-gui/pull/266) + ### Ignition Gui 5.2.0 (2021-07-27) 1. New teleop plugin implementation. @@ -329,6 +370,23 @@ ### Ignition Gui 3.X.X (202X-XX-XX) +### Ignition Gui 3.8.0 (2021-10-12) + +1. Improved doxygen + * [Pull request #275](https://github.com/ignitionrobotics/ign-gui/pull/275) + +1. Add a filter to the plugin menu + * [Pull request #277](https://github.com/ignitionrobotics/ign-gui/pull/277) + +1. 👩‍🌾 Remove bitbucket-pipelines.yml + * [Pull request #274](https://github.com/ignitionrobotics/ign-gui/pull/274) + +1. Require ign-rendering 3.5 + * [Pull request #264](https://github.com/ignitionrobotics/ign-gui/pull/264) + +1. New teleop plugin implementation. + * [Pull request #245](https://github.com/ignitionrobotics/ign-gui/pull/245) + ### Ignition Gui 3.7.0 (2021-07-14) 1. Fix codeowners diff --git a/include/ignition/gui/Application.hh b/include/ignition/gui/Application.hh index 21a97ed71..db9fd4513 100644 --- a/include/ignition/gui/Application.hh +++ b/include/ignition/gui/Application.hh @@ -96,11 +96,11 @@ namespace ignition /// and plugins. This function doesn't instantiate the plugins, it just /// keeps them in memory and they can be applied later by either /// instantiating a window or several dialogs. - /// \param[in] _config Full path to configuration file. + /// \param[in] _path Full path to configuration file. /// \return True if successful /// \sa InitializeMainWindow /// \sa InitializeDialogs - public: bool LoadConfig(const std::string &_config); + public: bool LoadConfig(const std::string &_path); /// \brief Load the configuration from the default config file. /// \return True if successful diff --git a/include/ignition/gui/GuiEvents.hh b/include/ignition/gui/GuiEvents.hh index 8b0c2f469..f61972ae0 100644 --- a/include/ignition/gui/GuiEvents.hh +++ b/include/ignition/gui/GuiEvents.hh @@ -27,6 +27,7 @@ #include #include #include +#include #include #include "ignition/gui/Export.hh" @@ -92,7 +93,7 @@ namespace ignition class IGNITION_GUI_VISIBLE SpawnFromDescription : public QEvent { /// \brief Constructor - /// \param[in] _string The resource's description as a string, such + /// \param[in] _description The resource's description as a string, such /// as an SDF file. public: explicit SpawnFromDescription(const std::string &_description); @@ -368,7 +369,7 @@ namespace ignition class IGNITION_GUI_VISIBLE DropOnScene : public QEvent { /// \brief Constructor - /// \param[in] _drop Dropped string. + /// \param[in] _dropText Dropped string. /// \param[in] _dropMouse x and y coordinate of mouse position. public: explicit DropOnScene( const std::string &_dropText, @@ -449,6 +450,25 @@ namespace ignition /// \brief Private data pointer IGN_UTILS_IMPL_PTR(dataPtr) }; + + /// \brief Event which is called to share WorldControl information. + class IGNITION_GUI_VISIBLE WorldControl : public QEvent + { + /// \brief Constructor + /// \param[in] _worldControl The WorldControl information + public: explicit WorldControl(const msgs::WorldControl &_worldControl); + + /// \brief Unique type for this event. + static const QEvent::Type kType = QEvent::Type(QEvent::MaxUser - 19); + + /// \brief Get the WorldControl information + /// \return The WorldControl information + public: const msgs::WorldControl &WorldControlInfo() const; + + /// \internal + /// \brief Private data pointer + IGN_UTILS_IMPL_PTR(dataPtr) + }; } } } diff --git a/include/ignition/gui/PlottingInterface.hh b/include/ignition/gui/PlottingInterface.hh index 9e0a2ed17..9f463202f 100644 --- a/include/ignition/gui/PlottingInterface.hh +++ b/include/ignition/gui/PlottingInterface.hh @@ -63,7 +63,7 @@ class IGNITION_GUI_VISIBLE PlotData public: double Value() const; /// \brief Set the field arrival time - /// \param[in] _value arrival time to set it + /// \param[in] _time arrival time to set it public: void SetTime(const double _time); /// \brief Get the arrival time @@ -273,7 +273,6 @@ class IGNITION_GUI_VISIBLE PlottingInterface : public QObject /// \brief called by Qml to remove a chart from a component attribute /// \param[in] _entity entity id which has the component /// \param[in] _typeId component type id - /// \param[in] _type component data type /// \param[in] _attribute component specefice attribte /// \param[in] _chart chart id public slots: void onComponentUnSubscribe(QString _entity, @@ -296,7 +295,6 @@ class IGNITION_GUI_VISIBLE PlottingInterface : public QObject /// \brief Notify the gazebo plugin to unsubscribe a component data /// \param[in] _entity entity id which has the component /// \param[in] _typeId component type id - /// \param[in] _type component data type /// \param[in] _attribute component specefice attribte /// \param[in] _chart chart id signals: void ComponentUnSubscribe(uint64_t _entity, @@ -338,4 +336,3 @@ class IGNITION_GUI_VISIBLE PlottingInterface : public QObject } #endif - diff --git a/include/ignition/gui/Plugin.hh b/include/ignition/gui/Plugin.hh index 07c69d600..b943b1e0d 100644 --- a/include/ignition/gui/Plugin.hh +++ b/include/ignition/gui/Plugin.hh @@ -102,7 +102,10 @@ namespace ignition /// \sa Load /// \param[in] _pluginElem Element containing configuration protected: virtual void LoadConfig( - const tinyxml2::XMLElement * /*_pluginElem*/) {} + const tinyxml2::XMLElement *_pluginElem) + { + (void)_pluginElem; + } /// \brief Get title /// \return Plugin title. diff --git a/src/GuiEvents.cc b/src/GuiEvents.cc index 16cacc25b..32185bfea 100644 --- a/src/GuiEvents.cc +++ b/src/GuiEvents.cc @@ -136,6 +136,12 @@ class ignition::gui::events::MousePressOnScene::Implementation public: common::MouseEvent mouse; }; +class ignition::gui::events::WorldControl::Implementation +{ + /// \brief WorldControl information. + public: msgs::WorldControl worldControl; +}; + using namespace ignition; using namespace gui; using namespace events; @@ -401,3 +407,16 @@ const common::MouseEvent &MousePressOnScene::Mouse() const { return this->dataPtr->mouse; } + +///////////////////////////////////////////////// +WorldControl::WorldControl(const msgs::WorldControl &_worldControl) + : QEvent(kType), dataPtr(utils::MakeImpl()) +{ + this->dataPtr->worldControl = _worldControl; +} + +///////////////////////////////////////////////// +const msgs::WorldControl &WorldControl::WorldControlInfo() const +{ + return this->dataPtr->worldControl; +} diff --git a/src/GuiEvents_TEST.cc b/src/GuiEvents_TEST.cc index a72e23864..253fa67d4 100644 --- a/src/GuiEvents_TEST.cc +++ b/src/GuiEvents_TEST.cc @@ -254,3 +254,32 @@ TEST(GuiEventsTest, MousePressOnScene) EXPECT_TRUE(event.Mouse().Alt()); EXPECT_FALSE(event.Mouse().Shift()); } + +///////////////////////////////////////////////// +TEST(GuiEventsTest, WorldControl) +{ + ignition::msgs::WorldControl worldControl; + worldControl.set_pause(true); + worldControl.set_step(true); + worldControl.set_multi_step(5u); + worldControl.mutable_reset()->set_all(true); + worldControl.mutable_reset()->set_time_only(true); + worldControl.mutable_reset()->set_model_only(false); + worldControl.set_seed(10u); + worldControl.mutable_run_to_sim_time()->set_sec(2); + worldControl.mutable_run_to_sim_time()->set_nsec(3); + events::WorldControl playEvent(worldControl); + + EXPECT_LT(QEvent::User, playEvent.type()); + EXPECT_FALSE(playEvent.WorldControlInfo().has_header()); + EXPECT_TRUE(playEvent.WorldControlInfo().pause()); + EXPECT_TRUE(playEvent.WorldControlInfo().step()); + EXPECT_EQ(5u, playEvent.WorldControlInfo().multi_step()); + EXPECT_FALSE(playEvent.WorldControlInfo().reset().has_header()); + EXPECT_TRUE(playEvent.WorldControlInfo().reset().all()); + EXPECT_TRUE(playEvent.WorldControlInfo().reset().time_only()); + EXPECT_FALSE(playEvent.WorldControlInfo().reset().model_only()); + EXPECT_EQ(10u, playEvent.WorldControlInfo().seed()); + EXPECT_EQ(2, playEvent.WorldControlInfo().run_to_sim_time().sec()); + EXPECT_EQ(3, playEvent.WorldControlInfo().run_to_sim_time().nsec()); +} diff --git a/src/plugins/grid_config/GridConfig.hh b/src/plugins/grid_config/GridConfig.hh index 48a357d46..859e8f4a5 100644 --- a/src/plugins/grid_config/GridConfig.hh +++ b/src/plugins/grid_config/GridConfig.hh @@ -49,7 +49,6 @@ namespace gui public: void UpdateGrid(); /// \brief Callback to retrieve existing grid or create a new one. - /// \param[in] _scene Scene to look for grid. public: void LoadGrid(); /// \brief Callback to update vertical cell count diff --git a/src/plugins/image_display/ImageDisplay.cc b/src/plugins/image_display/ImageDisplay.cc index 93a2ab9b6..0c736a139 100644 --- a/src/plugins/image_display/ImageDisplay.cc +++ b/src/plugins/image_display/ImageDisplay.cc @@ -212,6 +212,8 @@ void ImageDisplay::ProcessImage() image.setPixel(i, j, value); } } + + delete [] data; } this->dataPtr->provider->SetImage(image); diff --git a/src/plugins/key_publisher/KeyPublisher.hh b/src/plugins/key_publisher/KeyPublisher.hh index 759248882..38ecdd2e5 100644 --- a/src/plugins/key_publisher/KeyPublisher.hh +++ b/src/plugins/key_publisher/KeyPublisher.hh @@ -46,7 +46,8 @@ namespace gui public: virtual ~KeyPublisher(); // Documentation inherited - public: virtual void LoadConfig(const tinyxml2::XMLElement *) override; + public: virtual void LoadConfig( + const tinyxml2::XMLElement *_pluginElem) override; /// \brief Filter events in Qt /// \param[in] _obj The watched object diff --git a/src/plugins/minimal_scene/MinimalScene.cc b/src/plugins/minimal_scene/MinimalScene.cc index 5aee1d1ba..3485528d7 100644 --- a/src/plugins/minimal_scene/MinimalScene.cc +++ b/src/plugins/minimal_scene/MinimalScene.cc @@ -189,8 +189,11 @@ class ignition::gui::plugins::RenderWindowItem::Implementation /// \brief See RenderSync public: RenderSync renderSync; - //// \brief List of threads + /// \brief List of threads public: static QList threads; + + /// \brief List of our QT connections. + public: QList connections; }; /// \brief Private data class for MinimalScene @@ -236,6 +239,7 @@ void RenderSync::WaitForWorkerThread() // Worker thread asked us to wait! this->renderStallState = RenderStallState::WorkerCanProceed; + lock.unlock(); // Wake up worker thread this->cv.notify_one(); @@ -538,6 +542,8 @@ void IgnRenderer::Initialize() this->dataPtr->camera->SetUserData("user-camera", true); root->AddChild(this->dataPtr->camera); this->dataPtr->camera->SetLocalPose(this->cameraPose); + this->dataPtr->camera->SetNearClipPlane(this->cameraNearClip); + this->dataPtr->camera->SetFarClipPlane(this->cameraFarClip); this->dataPtr->camera->SetImageWidth(this->textureSize.width()); this->dataPtr->camera->SetImageHeight(this->textureSize.height()); this->dataPtr->camera->SetAntiAliasing(8); @@ -804,6 +810,10 @@ RenderWindowItem::RenderWindowItem(QQuickItem *_parent) ///////////////////////////////////////////////// RenderWindowItem::~RenderWindowItem() { + // Disconnect our QT connections. + for(auto conn : this->dataPtr->connections) + QObject::disconnect(conn); + this->dataPtr->renderSync.Shutdown(); QMetaObject::invokeMethod(this->dataPtr->renderThread, "ShutDown", @@ -882,13 +892,17 @@ QSGNode *RenderWindowItem::updatePaintNode(QSGNode *_node, // This rendering pipeline is throttled by vsync on the scene graph // rendering thread. - this->connect(this->dataPtr->renderThread, &RenderThread::TextureReady, - node, &TextureNode::NewTexture, Qt::DirectConnection); - this->connect(node, &TextureNode::PendingNewTexture, this->window(), + this->dataPtr->connections << this->connect(this->dataPtr->renderThread, + &RenderThread::TextureReady, node, &TextureNode::NewTexture, + Qt::DirectConnection); + this->dataPtr->connections << this->connect(node, + &TextureNode::PendingNewTexture, this->window(), &QQuickWindow::update, Qt::QueuedConnection); - this->connect(this->window(), &QQuickWindow::beforeRendering, node, - &TextureNode::PrepareNode, Qt::DirectConnection); - this->connect(node, &TextureNode::TextureInUse, this->dataPtr->renderThread, + this->dataPtr->connections << this->connect(this->window(), + &QQuickWindow::beforeRendering, node, &TextureNode::PrepareNode, + Qt::DirectConnection); + this->dataPtr->connections << this->connect(node, + &TextureNode::TextureInUse, this->dataPtr->renderThread, &RenderThread::RenderNext, Qt::QueuedConnection); // Get the production of FBO textures started.. @@ -932,6 +946,18 @@ void RenderWindowItem::SetCameraPose(const math::Pose3d &_pose) this->dataPtr->renderThread->ignRenderer.cameraPose = _pose; } +///////////////////////////////////////////////// +void RenderWindowItem::SetCameraNearClip(double _near) +{ + this->dataPtr->renderThread->ignRenderer.cameraNearClip = _near; +} + +///////////////////////////////////////////////// +void RenderWindowItem::SetCameraFarClip(double _far) +{ + this->dataPtr->renderThread->ignRenderer.cameraFarClip = _far; +} + ///////////////////////////////////////////////// void RenderWindowItem::SetSceneService(const std::string &_service) { @@ -1032,6 +1058,46 @@ void MinimalScene::LoadConfig(const tinyxml2::XMLElement *_pluginElem) renderWindow->SetCameraPose(pose); } + elem = _pluginElem->FirstChildElement("camera_clip"); + if (nullptr != elem && !elem->NoChildren()) + { + auto child = elem->FirstChildElement("near"); + if (nullptr != child && nullptr != child->GetText()) + { + double n; + std::stringstream nearStr; + nearStr << std::string(child->GetText()); + nearStr >> n; + if (nearStr.fail()) + { + ignerr << "Unable to set to '" << nearStr.str() + << "' using default near clip distance" << std::endl; + } + else + { + renderWindow->SetCameraNearClip(n); + } + } + + child = elem->FirstChildElement("far"); + if (nullptr != child && nullptr != child->GetText()) + { + double f; + std::stringstream farStr; + farStr << std::string(child->GetText()); + farStr >> f; + if (farStr.fail()) + { + ignerr << "Unable to set to '" << farStr.str() + << "' using default far clip distance" << std::endl; + } + else + { + renderWindow->SetCameraFarClip(f); + } + } + } + elem = _pluginElem->FirstChildElement("service"); if (nullptr != elem && nullptr != elem->GetText()) { diff --git a/src/plugins/minimal_scene/MinimalScene.hh b/src/plugins/minimal_scene/MinimalScene.hh index c1c02a6f9..49b72198b 100644 --- a/src/plugins/minimal_scene/MinimalScene.hh +++ b/src/plugins/minimal_scene/MinimalScene.hh @@ -52,6 +52,9 @@ namespace plugins /// (0.3, 0.3, 0.3, 1.0) /// * \ : Optional starting pose for the camera, defaults to /// (0, 0, 5, 0, 0, 0) + /// * \ : Optional near/far clipping distance for camera + /// * \ : Camera's near clipping plane distance, defaults to 0.01 + /// * \ : Camera's far clipping plane distance, defaults to 1000.0 /// * \ : If present, sky is enabled. class MinimalScene : public Plugin { @@ -181,6 +184,12 @@ namespace plugins /// \brief Initial Camera pose public: math::Pose3d cameraPose = math::Pose3d(0, 0, 2, 0, 0.4, 0); + /// \brief Default camera near clipping plane distance + public: double cameraNearClip = 0.01; + + /// \brief Default camera far clipping plane distance + public: double cameraFarClip = 1000.0; + /// \brief Scene background color public: math::Color backgroundColor = math::Color::Black; @@ -229,9 +238,10 @@ namespace plugins /// \brief Constructor public: RenderThread(); + /// \brief Render when safe /// \param[in] _renderSync RenderSync to safely /// synchronize Qt and worker thread (this) - public slots: void RenderNext(RenderSync *renderSync); + public slots: void RenderNext(RenderSync *_renderSync); /// \brief Shutdown the thread and the render engine public slots: void ShutDown(); @@ -283,9 +293,17 @@ namespace plugins public: void SetSceneName(const std::string &_name); /// \brief Set the initial pose the render window camera - /// \param[in] _pose Initical camera pose + /// \param[in] _pose Initial camera pose public: void SetCameraPose(const math::Pose3d &_pose); + /// \brief Set the render window camera's near clipping plane distance + /// \param[in] _near Near clipping plane distance + public: void SetCameraNearClip(double _near); + + /// \brief Set the render window camera's far clipping plane distance + /// \param[in] _far Far clipping plane distance + public: void SetCameraFarClip(double _far); + /// \brief Set scene service to use in this render window /// A service call will be made using ign-transport to get scene /// data using this service @@ -372,6 +390,8 @@ namespace plugins { Q_OBJECT + /// \brief Constructor + /// \param[in] _window Window to display the texture /// \param[in] _renderSync RenderSync to safely /// synchronize Qt (this) and worker thread public: explicit TextureNode(QQuickWindow *_window, diff --git a/src/plugins/world_control/CMakeLists.txt b/src/plugins/world_control/CMakeLists.txt index 7da2a0a0c..036f2ea61 100644 --- a/src/plugins/world_control/CMakeLists.txt +++ b/src/plugins/world_control/CMakeLists.txt @@ -1,9 +1,19 @@ -ign_gui_add_plugin(WorldControl - SOURCES - WorldControl.cc - QT_HEADERS - WorldControl.hh - TEST_SOURCES - WorldControl_TEST.cc -) - +if (NOT MSVC) + ign_gui_add_plugin(WorldControl + SOURCES + WorldControl.cc + WorldControlEventListener.cc + QT_HEADERS + WorldControl.hh + WorldControlEventListener.hh + TEST_SOURCES + WorldControl_TEST.cc + ) +else() + ign_gui_add_plugin(WorldControl + SOURCES + WorldControl.cc + QT_HEADERS + WorldControl.hh + ) +endif() diff --git a/src/plugins/world_control/WorldControl.cc b/src/plugins/world_control/WorldControl.cc index cbbe77bc1..91f32ee80 100644 --- a/src/plugins/world_control/WorldControl.cc +++ b/src/plugins/world_control/WorldControl.cc @@ -24,7 +24,10 @@ #include #include +#include "ignition/gui/Application.hh" #include "ignition/gui/Helpers.hh" +#include "ignition/gui/GuiEvents.hh" +#include "ignition/gui/MainWindow.hh" namespace ignition { @@ -34,6 +37,10 @@ namespace plugins { class WorldControlPrivate { + /// \brief Send the world control event or call the control service. + /// \param[in] _msg Message to send. + public: void SendEventMsg(const ignition::msgs::WorldControl &_msg); + /// \brief Message holding latest world statistics public: ignition::msgs::WorldStatistics msg; @@ -51,6 +58,15 @@ namespace plugins /// \brief True for paused public: bool pause{true}; + + /// \brief The paused state of the most recently received world stats msg + /// (true for paused) + public: bool lastStatsMsgPaused{true}; + + /// \brief Whether server communication should occur through an event (true) + /// or service (false). The service option is used by default for + /// ign-gui6, and should be changed to use the event by default in ign-gui7. + public: bool useEvent{false}; }; } } @@ -151,6 +167,7 @@ void WorldControl::LoadConfig(const tinyxml2::XMLElement *_pluginElem) pausedElem->QueryBoolText(&startPaused); } this->dataPtr->pause = startPaused; + this->dataPtr->lastStatsMsgPaused = startPaused; if (startPaused) this->paused(); else @@ -212,6 +229,14 @@ void WorldControl::LoadConfig(const tinyxml2::XMLElement *_pluginElem) ignerr << "Failed to create valid topic for world [" << worldName << "]" << std::endl; } + + if (auto elem = _pluginElem->FirstChildElement("use_event")) + elem->QueryBoolText(&this->dataPtr->useEvent); + + if (this->dataPtr->useEvent) + igndbg << "Using an event to share WorldControl msgs with the server\n"; + else + igndbg << "Using a service to share WorldControl msgs with the server\n"; } ///////////////////////////////////////////////// @@ -219,11 +244,26 @@ void WorldControl::ProcessMsg() { std::lock_guard lock(this->dataPtr->mutex); - if (!this->dataPtr->pause && this->dataPtr->msg.paused()) + // ignore the message if it's associated with a step + const auto &header = this->dataPtr->msg.header(); + if ((header.data_size() > 0) && (header.data(0).key() == "step")) + return; + + // If the pause state of the message doesn't match the pause state of this + // plugin, then play/pause must have occurred elsewhere (for example, the + // command line). If the pause state of the message matches the pause state + // of this plugin, but the pause state of the message differs from the + // previous message's pause state, this means that a pause/play request from + // this plugin has been registered by the server + if (this->dataPtr->msg.paused() && + (!this->dataPtr->pause || !this->dataPtr->lastStatsMsgPaused)) this->paused(); - else if (this->dataPtr->pause && !this->dataPtr->msg.paused()) + else if (!this->dataPtr->msg.paused() && + (this->dataPtr->pause || this->dataPtr->lastStatsMsgPaused)) this->playing(); + this->dataPtr->pause = this->dataPtr->msg.paused(); + this->dataPtr->lastStatsMsgPaused = this->dataPtr->msg.paused(); } ///////////////////////////////////////////////// @@ -238,33 +278,20 @@ void WorldControl::OnWorldStatsMsg(const ignition::msgs::WorldStatistics &_msg) ///////////////////////////////////////////////// void WorldControl::OnPlay() { - std::function cb = - [this](const ignition::msgs::Boolean &/*_rep*/, const bool _result) - { - if (_result) - QMetaObject::invokeMethod(this, "playing"); - }; - - ignition::msgs::WorldControl req; - req.set_pause(false); + ignition::msgs::WorldControl msg; + msg.set_pause(false); this->dataPtr->pause = false; - this->dataPtr->node.Request(this->dataPtr->controlService, req, cb); + this->dataPtr->SendEventMsg(msg); } ///////////////////////////////////////////////// void WorldControl::OnPause() { - std::function cb = - [this](const ignition::msgs::Boolean &/*_rep*/, const bool _result) - { - if (_result) - QMetaObject::invokeMethod(this, "paused"); - }; - - ignition::msgs::WorldControl req; - req.set_pause(true); + ignition::msgs::WorldControl msg; + msg.set_pause(true); this->dataPtr->pause = true; - this->dataPtr->node.Request(this->dataPtr->controlService, req, cb); + + this->dataPtr->SendEventMsg(msg); } ///////////////////////////////////////////////// @@ -276,15 +303,31 @@ void WorldControl::OnStepCount(const unsigned int _steps) ///////////////////////////////////////////////// void WorldControl::OnStep() { - std::function cb = - [](const ignition::msgs::Boolean &/*_rep*/, const bool /*_result*/) - { - }; + ignition::msgs::WorldControl msg; + msg.set_pause(this->dataPtr->pause); + msg.set_multi_step(this->dataPtr->multiStep); - ignition::msgs::WorldControl req; - req.set_pause(this->dataPtr->pause); - req.set_multi_step(this->dataPtr->multiStep); - this->dataPtr->node.Request(this->dataPtr->controlService, req, cb); + this->dataPtr->SendEventMsg(msg); +} + +///////////////////////////////////////////////// +void WorldControlPrivate::SendEventMsg(const ignition::msgs::WorldControl &_msg) +{ + if (this->useEvent) + { + gui::events::WorldControl event(_msg); + App()->sendEvent(App()->findChild(), &event); + } + else + { + std::function cb = + [](const ignition::msgs::Boolean &/*_rep*/, const bool /*_result*/) + { + // the service CB is empty because updates are handled in + // WorldControl::ProcessMsg + }; + this->node.Request(this->controlService, _msg, cb); + } } // Register this plugin diff --git a/src/plugins/world_control/WorldControlEventListener.cc b/src/plugins/world_control/WorldControlEventListener.cc new file mode 100644 index 000000000..c076748c1 --- /dev/null +++ b/src/plugins/world_control/WorldControlEventListener.cc @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2021 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +#include "WorldControlEventListener.hh" + +using namespace ignition; +using namespace gui; + +WorldControlEventListener::WorldControlEventListener() +{ + ignition::gui::App()->findChild< + ignition::gui::MainWindow *>()->installEventFilter(this); +} + +WorldControlEventListener::~WorldControlEventListener() = default; + +bool WorldControlEventListener::eventFilter(QObject *_obj, QEvent *_event) +{ + if (_event->type() == ignition::gui::events::WorldControl::kType) + { + auto worldControlEvent = + reinterpret_cast(_event); + if (worldControlEvent) + { + this->listenedToPlay = !worldControlEvent->WorldControlInfo().pause(); + this->listenedToPause = worldControlEvent->WorldControlInfo().pause(); + this->listenedToStep = + worldControlEvent->WorldControlInfo().multi_step() > 0u; + } + } + + // Standard event processing + return QObject::eventFilter(_obj, _event); +} diff --git a/src/plugins/world_control/WorldControlEventListener.hh b/src/plugins/world_control/WorldControlEventListener.hh new file mode 100644 index 000000000..8b4d34a8b --- /dev/null +++ b/src/plugins/world_control/WorldControlEventListener.hh @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2021 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ +#ifndef IGNITION_GUI_WORLDCONTROLEVENTLISTENER_HH_ +#define IGNITION_GUI_WORLDCONTROLEVENTLISTENER_HH_ + +#include "ignition/gui/Application.hh" +#include "ignition/gui/Export.hh" +#include "ignition/gui/GuiEvents.hh" +#include "ignition/gui/MainWindow.hh" +#include "ignition/gui/qt.h" + +namespace ignition +{ +namespace gui +{ + /// \brief Helper class for testing listening to events emitted by the + /// WorldControl plugin. This is used for testing the event behavior of + /// the WorldControl plugin. + class WorldControlEventListener : public QObject + { + Q_OBJECT + + /// \brief Constructor + public: WorldControlEventListener(); + + /// \brief Destructor + public: virtual ~WorldControlEventListener() override; + + // Documentation inherited + protected: bool eventFilter(QObject *_obj, QEvent *_event) override; + + /// \brief Whether a play event has been received (true) or not (false) + public: bool listenedToPlay{false}; + + /// \brief Whether a pause event has been received (true) or not (false) + public: bool listenedToPause{false}; + + /// \brief Whether a pause event has been received (true) or not (false) + public: bool listenedToStep{false}; + }; +} +} + +#endif diff --git a/src/plugins/world_control/WorldControl_TEST.cc b/src/plugins/world_control/WorldControl_TEST.cc index 809a934d0..4c8be21db 100644 --- a/src/plugins/world_control/WorldControl_TEST.cc +++ b/src/plugins/world_control/WorldControl_TEST.cc @@ -23,9 +23,10 @@ #include "test_config.h" // NOLINT(build/include) #include "ignition/gui/Application.hh" -#include "ignition/gui/Plugin.hh" #include "ignition/gui/MainWindow.hh" +#include "ignition/gui/Plugin.hh" #include "WorldControl.hh" +#include "WorldControlEventListener.hh" int g_argc = 1; char* g_argv[] = @@ -53,7 +54,7 @@ TEST(WorldControlTest, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(Load)) // Get plugin auto plugins = win->findChildren(); - EXPECT_EQ(plugins.size(), 1); + ASSERT_EQ(plugins.size(), 1); auto plugin = plugins[0]; EXPECT_EQ(plugin->Title(), "World control"); @@ -78,6 +79,7 @@ TEST(WorldControlTest, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(WorldControl)) "" "true" "/world_control_test" + "false" ""; tinyxml2::XMLDocument pluginDoc; @@ -94,7 +96,7 @@ TEST(WorldControlTest, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(WorldControl)) // Get plugin auto plugins = win->findChildren(); - EXPECT_EQ(plugins.size(), 1); + ASSERT_EQ(plugins.size(), 1); auto plugin = plugins[0]; EXPECT_EQ(plugin->Title(), "World Control!"); @@ -148,6 +150,7 @@ TEST(WorldControlTest, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(WorldNameNoService)) // Load plugin const char *pluginStr = "" + " false" ""; tinyxml2::XMLDocument pluginDoc; @@ -160,7 +163,7 @@ TEST(WorldControlTest, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(WorldNameNoService)) // Get plugin auto plugins = win->findChildren(); - EXPECT_EQ(plugins.size(), 1); + ASSERT_EQ(plugins.size(), 1); // World control service bool pauseCalled = false; @@ -201,6 +204,7 @@ TEST(WorldControlTest, const char *pluginStr = "" " /world/watermelon/control" + " false" ""; tinyxml2::XMLDocument pluginDoc; @@ -213,7 +217,7 @@ TEST(WorldControlTest, // Get plugin auto plugins = win->findChildren(); - EXPECT_EQ(plugins.size(), 1); + ASSERT_EQ(plugins.size(), 1); // World control service bool pauseCalled = false; @@ -252,6 +256,7 @@ TEST(WorldControlTest, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(WorldNameNoProp)) const char *pluginStr = "" " /world/watermelon/control" + " false" ""; tinyxml2::XMLDocument pluginDoc; @@ -264,7 +269,7 @@ TEST(WorldControlTest, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(WorldNameNoProp)) // Get plugin auto plugins = win->findChildren(); - EXPECT_EQ(plugins.size(), 1); + ASSERT_EQ(plugins.size(), 1); // World control service bool pauseCalled = false; @@ -276,7 +281,6 @@ TEST(WorldControlTest, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(WorldNameNoProp)) }; transport::Node node; - // banana, not watermelon node.Advertise("/world/watermelon/control", cb); // Pause @@ -286,3 +290,63 @@ TEST(WorldControlTest, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(WorldNameNoProp)) // Cleanup plugins.clear(); } + +///////////////////////////////////////////////// +TEST(WorldControlTest, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(WorldControlEvent)) +{ + common::Console::SetVerbosity(4); + + Application app(g_argc, g_argv); + app.AddPluginPath(std::string(PROJECT_BINARY_PATH) + "/lib"); + + // Load plugin + const char *pluginStr = + "" + "" + "World Control!" + "" + "true" + "/world_control_test" + "true" + ""; + + tinyxml2::XMLDocument pluginDoc; + EXPECT_EQ(tinyxml2::XML_SUCCESS, pluginDoc.Parse(pluginStr)); + EXPECT_TRUE(app.LoadPlugin("WorldControl", + pluginDoc.FirstChildElement("plugin"))); + + // Get main window + auto win = app.findChild(); + ASSERT_NE(nullptr, win); + + // Show, but don't exec, so we don't block + win->QuickWindow()->show(); + + // Get plugin + auto plugins = win->findChildren(); + ASSERT_EQ(plugins.size(), 1); + + auto plugin = plugins[0]; + EXPECT_EQ(plugin->Title(), "World Control!"); + + // World control event listener + ignition::gui::WorldControlEventListener eventListener; + EXPECT_FALSE(eventListener.listenedToPause); + EXPECT_FALSE(eventListener.listenedToStep); + EXPECT_FALSE(eventListener.listenedToPlay); + + // Pause + plugin->OnPause(); + EXPECT_TRUE(eventListener.listenedToPause); + + // Step + plugin->OnStep(); + EXPECT_TRUE(eventListener.listenedToStep); + + // Play + plugin->OnPlay(); + EXPECT_TRUE(eventListener.listenedToPlay); + + // Cleanup + plugins.clear(); +} diff --git a/test/integration/minimal_scene.cc b/test/integration/minimal_scene.cc index fc291050f..728be48c9 100644 --- a/test/integration/minimal_scene.cc +++ b/test/integration/minimal_scene.cc @@ -82,6 +82,10 @@ TEST(MinimalSceneTest, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(Config)) "1.0 0 0" "0 1 0" "1 2 3 0 0 1.57" + "" + " 0.1" + " 5000" + "" ""; tinyxml2::XMLDocument pluginDoc; @@ -126,6 +130,8 @@ TEST(MinimalSceneTest, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(Config)) ASSERT_NE(nullptr, camera); EXPECT_EQ(math::Pose3d(1, 2, 3, 0, 0, 1.57), camera->WorldPose()); + EXPECT_DOUBLE_EQ(0.1, camera->NearClipPlane()); + EXPECT_DOUBLE_EQ(5000.0, camera->FarClipPlane()); // Cleanup auto plugins = win->findChildren(); @@ -139,4 +145,3 @@ TEST(MinimalSceneTest, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(Config)) engine->DestroyScene(scene); EXPECT_TRUE(rendering::unloadEngine(engine->Name())); } - diff --git a/tutorials/03_plugins.md b/tutorials/03_plugins.md index 5c916d85b..df98479ef 100644 --- a/tutorials/03_plugins.md +++ b/tutorials/03_plugins.md @@ -48,11 +48,11 @@ Publish messages on an Ignition Transport topic. ign topic -e -t /echo -1. On a new terminal, open the publisher GUI: +2. On a new terminal, open the publisher GUI: ign gui -s Publisher -1. The GUI is pre-filled to publish "Hello" messages on the `/echo` topic. +3. The GUI is pre-filled to publish "Hello" messages on the `/echo` topic. Click "Publish" to publish a message and see it on the terminal. ### World stats and control @@ -101,7 +101,7 @@ plugins. ign gui -c examples/config/reqres.config -### Displays {#display-plugins} +### Displays **Not ported to versions 1 or higher yet** @@ -119,5 +119,5 @@ Ignition GUI will look for display plugins on the following paths, in this order: 1. All paths set on the `IGN_GUI_DISPLAY_PLUGIN_PATH` environment variable -1. `~/.ignition/gui/display_plugins` -1. [Display plugins that are installed with Ignition GUI](https://ignitionrobotics.org/api/gui/0.1/namespaceignition_1_1gui_1_1display_plugins.html) +2. `~/.ignition/gui/display_plugins` +3. [Display plugins that are installed with Ignition GUI](https://ignitionrobotics.org/api/gui/0.1/namespaceignition_1_1gui_1_1display_plugins.html)