Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support emitting an event on play/pause/step #306

Merged
merged 15 commits into from
Nov 5, 2021
Merged
20 changes: 20 additions & 0 deletions include/ignition/gui/GuiEvents.hh
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include <ignition/common/MouseEvent.hh>
#include <ignition/math/Vector2.hh>
#include <ignition/math/Vector3.hh>
#include <ignition/msgs/world_control.pb.h>
#include <ignition/utils/ImplPtr.hh>

#include "ignition/gui/Export.hh"
Expand Down Expand Up @@ -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)
};
}
}
}
Expand Down
19 changes: 19 additions & 0 deletions src/GuiEvents.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Implementation>())
{
this->dataPtr->worldControl = _worldControl;
}

/////////////////////////////////////////////////
const msgs::WorldControl &WorldControl::WorldControlInfo() const
{
return this->dataPtr->worldControl;
}
29 changes: 29 additions & 0 deletions src/GuiEvents_TEST.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
28 changes: 19 additions & 9 deletions src/plugins/world_control/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
ign_gui_add_plugin(WorldControl
SOURCES
WorldControl.cc
QT_HEADERS
WorldControl.hh
TEST_SOURCES
WorldControl_TEST.cc
)

if (NOT MSVC)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, why did you need to separate Windows here? We're already disabling the test on Windows in the test's body.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because Windows would still compile the testing framework and produce compile time errors.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. It was compiling before. Not sure what causes the compiler error here, but I think it's ok to address this later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a follow-up, yes,the tests themselves ignore windows. For example: https://github.com/ignitionrobotics/ign-gui/blob/ignition-gui6_6.0.0/src/plugins/world_control/WorldControl_TEST.cc#L66

However, the new helper test class that was added for checking events (#306 (comment)) has to be added as a QT header, or else there will be linking errors. So, it seemed like this new QT header is what was causing issues on windows, and @nkoenig made a patch in #306 (comment) to prevent compiling the helper test class header on windows. I'm not sure if there's a better way to handle this or not, but hopefully that provides some more context/insights about this change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried a few different options to make the header compile on windows, and gave up.

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()
103 changes: 73 additions & 30 deletions src/plugins/world_control/WorldControl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
#include <ignition/common/StringUtils.hh>
#include <ignition/plugin/Register.hh>

#include "ignition/gui/Application.hh"
#include "ignition/gui/Helpers.hh"
#include "ignition/gui/GuiEvents.hh"
#include "ignition/gui/MainWindow.hh"

namespace ignition
{
Expand All @@ -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;

Expand All @@ -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};
};
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -212,18 +229,41 @@ 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";
}

/////////////////////////////////////////////////
void WorldControl::ProcessMsg()
{
std::lock_guard<std::recursive_mutex> 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();
}

/////////////////////////////////////////////////
Expand All @@ -238,33 +278,20 @@ void WorldControl::OnWorldStatsMsg(const ignition::msgs::WorldStatistics &_msg)
/////////////////////////////////////////////////
void WorldControl::OnPlay()
{
std::function<void(const ignition::msgs::Boolean &, const bool)> 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);
adlarkin marked this conversation as resolved.
Show resolved Hide resolved
this->dataPtr->SendEventMsg(msg);
}

/////////////////////////////////////////////////
void WorldControl::OnPause()
{
std::function<void(const ignition::msgs::Boolean &, const bool)> 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);
}

/////////////////////////////////////////////////
Expand All @@ -276,15 +303,31 @@ void WorldControl::OnStepCount(const unsigned int _steps)
/////////////////////////////////////////////////
void WorldControl::OnStep()
{
std::function<void(const ignition::msgs::Boolean &, const bool)> 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<MainWindow *>(), &event);
}
else
{
std::function<void(const ignition::msgs::Boolean &, const bool)> 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
Expand Down
48 changes: 48 additions & 0 deletions src/plugins/world_control/WorldControlEventListener.cc
Original file line number Diff line number Diff line change
@@ -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<gui::events::WorldControl *>(_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);
}
Loading