diff --git a/include/ignition/gui/GuiEvents.hh b/include/ignition/gui/GuiEvents.hh index 86fac1fad..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" @@ -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/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/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(); +}