From c3df98d68170aad8fed0b0554aee5eac438ebb05 Mon Sep 17 00:00:00 2001 From: Louise Poubel Date: Wed, 17 Aug 2022 13:27:11 -0700 Subject: [PATCH 1/6] =?UTF-8?q?=F0=9F=8E=88=203.11.2=20(#474)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Louise Poubel Signed-off-by: Louise Poubel --- CMakeLists.txt | 2 +- Changelog.md | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fd19a1d78..3dd418888 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.10.2 FATAL_ERROR) #============================================================================ # Initialize the project #============================================================================ -project(ignition-gui3 VERSION 3.11.1) +project(ignition-gui3 VERSION 3.11.2) #============================================================================ # Find ignition-cmake diff --git a/Changelog.md b/Changelog.md index 430df0e4c..c1069f0a2 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,10 @@ ## Gazebo GUI 3 +### Gazebo GUI 3.11.2 (2022-08-17) + +1. Fix mistaken dialog error message + * [Pull request #472](https://github.com/gazebosim/gz-gui/pull/472) + ### Gazebo GUI 3.11.1 (2022-08-15) 1. Replace pose in Grid3d with GzPose From d27c387ff676b3fb6795b13706dc2eb2dfef66ff Mon Sep 17 00:00:00 2001 From: AzulRadio <50132891+AzulRadio@users.noreply.github.com> Date: Thu, 18 Aug 2022 13:47:08 -0500 Subject: [PATCH 2/6] Add degree as an optional unit for rotation in GzPose (#475) * Add degree as an optional unit for rotation in GzPose Signed-off-by: youhy --- include/ignition/gui/qml/GzPose.qml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/include/ignition/gui/qml/GzPose.qml b/include/ignition/gui/qml/GzPose.qml index d863e6bfc..2988664fc 100644 --- a/include/ignition/gui/qml/GzPose.qml +++ b/include/ignition/gui/qml/GzPose.qml @@ -31,6 +31,7 @@ import QtQuick.Controls.Styles 1.4 * GzPose { * id: gzPose * readOnly: false + * useRadian: true * xValue: xValueFromCPP * yValue: yValueFromCPP * zValue: zValueFromCPP @@ -49,6 +50,9 @@ Item { // Read-only / write property bool readOnly: false + // Radian / Degree as the unit for Rotation + property bool useRadian: true + // User input value. property double xValue property double yValue @@ -165,7 +169,7 @@ Item { } Text { - text: 'Roll (rad)' + text: 'Roll ' + (useRadian ? '(rad)' : '(deg)') leftPadding: 5 color: Material.theme == Material.Light ? "#444444" : "#bbbbbb" font.pointSize: 12 @@ -207,7 +211,7 @@ Item { } Text { - text: 'Pitch (rad)' + text: 'Pitch ' + (useRadian ? '(rad)' : '(deg)') leftPadding: 5 color: Material.theme == Material.Light ? "#444444" : "#bbbbbb" font.pointSize: 12 @@ -249,7 +253,7 @@ Item { } Text { - text: 'Yaw (rad)' + text: 'Yaw ' + (useRadian ? '(rad)' : '(deg)') leftPadding: 5 color: Material.theme == Material.Light ? "#444444" : "#bbbbbb" font.pointSize: 12 From d43ea6ac033030118e52823033e69ce3a0149fd7 Mon Sep 17 00:00:00 2001 From: Jenn Nguyen Date: Fri, 19 Aug 2022 13:27:42 -0700 Subject: [PATCH 3/6] Fix image display test (#468) Signed-off-by: Jenn Nguyen --- src/plugins/image_display/CMakeLists.txt | 3 +- src/plugins/image_display/ImageDisplay.cc | 40 +- src/plugins/image_display/ImageDisplay.hh | 46 +- src/plugins/image_display/ImageDisplay.qml | 2 + .../image_display/ImageDisplay_TEST.cc | 573 ++++++++++++++---- 5 files changed, 515 insertions(+), 149 deletions(-) diff --git a/src/plugins/image_display/CMakeLists.txt b/src/plugins/image_display/CMakeLists.txt index 85da0c34a..e606d0141 100644 --- a/src/plugins/image_display/CMakeLists.txt +++ b/src/plugins/image_display/CMakeLists.txt @@ -4,6 +4,5 @@ ign_gui_add_plugin(ImageDisplay QT_HEADERS ImageDisplay.hh TEST_SOURCES - # ImageDisplay_TEST.cc + ImageDisplay_TEST.cc ) - diff --git a/src/plugins/image_display/ImageDisplay.cc b/src/plugins/image_display/ImageDisplay.cc index dd5baa0de..2959d66ef 100644 --- a/src/plugins/image_display/ImageDisplay.cc +++ b/src/plugins/image_display/ImageDisplay.cc @@ -17,9 +17,6 @@ #include "ImageDisplay.hh" -#include - -#include #include #include #include @@ -39,37 +36,6 @@ namespace gui { namespace plugins { - class ImageProvider : public QQuickImageProvider - { - public: ImageProvider() - : QQuickImageProvider(QQuickImageProvider::Image) - { - } - - public: QImage requestImage(const QString &, QSize *, - const QSize &) override - { - if (!this->img.isNull()) - { - // Must return a copy - QImage copy(this->img); - return copy; - } - - // Placeholder in case we have no image yet - QImage i(400, 400, QImage::Format_RGB888); - i.fill(QColor(128, 128, 128, 100)); - return i; - } - - public: void SetImage(const QImage &_image) - { - this->img = _image; - } - - private: QImage img; - }; - class ImageDisplayPrivate { /// \brief List of topics publishing image messages. @@ -184,7 +150,11 @@ void ImageDisplay::OnTopic(const QString _topic) { auto topic = _topic.toStdString(); if (topic.empty()) + { + // LCOV_EXCL_START return; + // LCOV_EXCL_STOP + } // Unsubscribe auto subs = this->dataPtr->node.SubscribedTopics(); @@ -195,8 +165,10 @@ void ImageDisplay::OnTopic(const QString _topic) if (!this->dataPtr->node.Subscribe(topic, &ImageDisplay::OnImageMsg, this)) { + // LCOV_EXCL_START ignerr << "Unable to subscribe to topic [" << topic << "]" << std::endl; return; + // LCOV_EXCL_STOP } App()->findChild()->notifyWithDuration( QString::fromStdString("Subscribed to: " + topic + ""), 4000); diff --git a/src/plugins/image_display/ImageDisplay.hh b/src/plugins/image_display/ImageDisplay.hh index 240bce24f..8aaf62a70 100644 --- a/src/plugins/image_display/ImageDisplay.hh +++ b/src/plugins/image_display/ImageDisplay.hh @@ -18,7 +18,10 @@ #ifndef IGNITION_GUI_PLUGINS_IMAGEDISPLAY_HH_ #define IGNITION_GUI_PLUGINS_IMAGEDISPLAY_HH_ +#include #include +#include + #ifdef _MSC_VER #pragma warning(push, 0) #endif @@ -27,6 +30,16 @@ #pragma warning(pop) #endif +#ifndef _WIN32 +# define ImageDisplay_EXPORTS_API +#else +# if (defined(ImageDisplay_EXPORTS)) +# define ImageDisplay_EXPORTS_API __declspec(dllexport) +# else +# define ImageDisplay_EXPORTS_API __declspec(dllimport) +# endif +#endif + #include "ignition/gui/Plugin.hh" namespace ignition @@ -37,6 +50,37 @@ namespace plugins { class ImageDisplayPrivate; + class ImageProvider : public QQuickImageProvider + { + public: ImageProvider() + : QQuickImageProvider(QQuickImageProvider::Image) + { + } + + public: QImage requestImage(const QString &, QSize *, + const QSize &) override + { + if (!this->img.isNull()) + { + // Must return a copy + QImage copy(this->img); + return copy; + } + + // Placeholder in case we have no image yet + QImage i(400, 400, QImage::Format_RGB888); + i.fill(QColor(128, 128, 128, 100)); + return i; + } + + public: void SetImage(const QImage &_image) + { + this->img = _image; + } + + private: QImage img; + }; + /// \brief Display images coming through an Ignition transport topic. /// /// ## Configuration @@ -44,7 +88,7 @@ namespace plugins /// \ : Set the topic to receive image messages. /// \ : Whether to show the topic picker, true by default. If /// this is false, a \ must be specified. - class ImageDisplay : public Plugin + class ImageDisplay_EXPORTS_API ImageDisplay : public Plugin { Q_OBJECT diff --git a/src/plugins/image_display/ImageDisplay.qml b/src/plugins/image_display/ImageDisplay.qml index b36bdb3e4..b11021a74 100644 --- a/src/plugins/image_display/ImageDisplay.qml +++ b/src/plugins/image_display/ImageDisplay.qml @@ -60,6 +60,7 @@ Rectangle { RowLayout { visible: showPicker RoundButton { + objectName: "refreshButton" text: "\u21bb" Material.background: Material.primary onClicked: { @@ -72,6 +73,7 @@ Rectangle { } ComboBox { id: combo + objectName: "topicsCombo" Layout.fillWidth: true model: ImageDisplay.topicList onCurrentIndexChanged: { diff --git a/src/plugins/image_display/ImageDisplay_TEST.cc b/src/plugins/image_display/ImageDisplay_TEST.cc index e20b5b53d..bbd7fd3bc 100644 --- a/src/plugins/image_display/ImageDisplay_TEST.cc +++ b/src/plugins/image_display/ImageDisplay_TEST.cc @@ -16,70 +16,125 @@ */ #include + +#ifdef _MSC_VER +#pragma warning(push, 0) +#endif +#include +#ifdef _MSC_VER +#pragma warning(pop) +#endif + #include #include #include +#include -#include "ignition/gui/Iface.hh" -#include "ignition/gui/Plugin.hh" +#include "ignition/gui/Application.hh" #include "ignition/gui/MainWindow.hh" +#include "ignition/gui/Plugin.hh" +#include "test_config.h" // NOLINT(build/include) +#include "ImageDisplay.hh" + +int g_argc = 1; +char* g_argv[] = +{ + reinterpret_cast(const_cast("./ImageDisplay_TEST")), +}; using namespace ignition; using namespace gui; ///////////////////////////////////////////////// -TEST(ImageDisplayTest, Load) +TEST(ImageDisplayTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(Load)) { - EXPECT_TRUE(initApp()); + common::Console::SetVerbosity(4); + + Application app(g_argc, g_argv); + app.AddPluginPath( + common::joinPaths(std::string(PROJECT_BINARY_PATH), "lib")); + + // Load plugin + EXPECT_TRUE(app.LoadPlugin("ImageDisplay")); - EXPECT_TRUE(loadPlugin("ImageDisplay")); + // Get main window + auto win = app.findChild(); + ASSERT_NE(win, nullptr); - EXPECT_TRUE(stop()); + // Get plugin + auto plugins = win->findChildren(); + EXPECT_EQ(plugins.size(), 1); + + auto plugin = plugins[0]; + EXPECT_EQ(plugin->Title(), "Image display"); + + // Cleanup + plugins.clear(); } ///////////////////////////////////////////////// -TEST(ImageDisplayTest, DefaultConfig) +TEST(ImageDisplayTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(DefaultConfig)) { - setVerbosity(4); - EXPECT_TRUE(initApp()); + common::Console::SetVerbosity(4); + + Application app(g_argc, g_argv); + app.AddPluginPath( + common::joinPaths(std::string(PROJECT_BINARY_PATH), "lib")); // Load plugin - EXPECT_TRUE(loadPlugin("ImageDisplay")); + EXPECT_TRUE(app.LoadPlugin("ImageDisplay")); - // Create main window - EXPECT_TRUE(createMainWindow()); - auto win = mainWindow(); - EXPECT_TRUE(win != nullptr); + // Get main window + auto win = app.findChild(); + ASSERT_NE(win, nullptr); // Get plugin - auto plugins = win->findChildren(); + auto plugins = win->findChildren(); EXPECT_EQ(plugins.size(), 1); auto plugin = plugins[0]; EXPECT_EQ(plugin->Title(), "Image display"); // Has a topic picker - auto topicsCombo = plugin->findChild("topicsCombo"); - EXPECT_TRUE(topicsCombo != nullptr); - EXPECT_EQ(topicsCombo->count(), 0); - - auto refreshButton = plugin->findChild("refreshButton"); - EXPECT_TRUE(refreshButton != nullptr); - - // No images - auto label = plugin->findChild(); - EXPECT_TRUE(label != nullptr); - EXPECT_EQ(label->text(), "No image"); + auto topicsCombo = plugin->PluginItem()->findChild("topicsCombo"); + ASSERT_NE(topicsCombo, nullptr); + auto topicProp = topicsCombo->property("model"); + EXPECT_TRUE(topicProp.isValid()); + auto topicList = topicProp.toStringList(); + EXPECT_EQ(topicList.size(), 0); + + auto refreshButton = + plugin->PluginItem()->findChild("refreshButton"); + ASSERT_NE(refreshButton, nullptr); + + auto picker = + topicsCombo->parent(); // RowLayout that holds `visible: showPicker` + ASSERT_NE(picker, nullptr); + auto pickerProp = picker->property("visible"); + EXPECT_TRUE(pickerProp.isValid()); + EXPECT_TRUE(pickerProp.toBool()); + + // No images (gray image) + auto providerBase = app.Engine()->imageProvider( + plugin->CardItem()->objectName() + "imagedisplay"); + ASSERT_NE(providerBase, nullptr); + auto imageProvider = static_cast(providerBase); + ASSERT_NE(imageProvider, nullptr); + QSize dummySize; + QImage img = imageProvider->requestImage(QString(), &dummySize, dummySize); + EXPECT_TRUE(img.allGray()); // Cleanup plugins.clear(); - EXPECT_TRUE(stop()); } ///////////////////////////////////////////////// -TEST(ImageDisplayTest, NoPickerNeedsTopic) +TEST(ImageDisplayTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(NoPickerNeedsTopic)) { - setVerbosity(4); - EXPECT_TRUE(initApp()); + common::Console::SetVerbosity(4); + + Application app(g_argc, g_argv); + app.AddPluginPath( + common::joinPaths(std::string(PROJECT_BINARY_PATH), "lib")); // Load plugin const char *pluginStr = @@ -89,43 +144,60 @@ TEST(ImageDisplayTest, NoPickerNeedsTopic) tinyxml2::XMLDocument pluginDoc; pluginDoc.Parse(pluginStr); - EXPECT_TRUE(loadPlugin("ImageDisplay", + EXPECT_TRUE(app.LoadPlugin("ImageDisplay", pluginDoc.FirstChildElement("plugin"))); - // Create main window - EXPECT_TRUE(createMainWindow()); - auto win = mainWindow(); - EXPECT_TRUE(win != nullptr); + // Get main window + auto win = app.findChild(); + ASSERT_NE(win, nullptr); // Get plugin - auto plugins = win->findChildren(); + auto plugins = win->findChildren(); EXPECT_EQ(plugins.size(), 1); auto plugin = plugins[0]; EXPECT_EQ(plugin->Title(), "Image display"); // Has a topic picker anyway - auto topicsCombo = plugin->findChild("topicsCombo"); - EXPECT_TRUE(topicsCombo != nullptr); - EXPECT_EQ(topicsCombo->count(), 0); - - auto refreshButton = plugin->findChild("refreshButton"); - EXPECT_TRUE(refreshButton != nullptr); - - // No images - auto label = plugin->findChild(); - EXPECT_TRUE(label != nullptr); - EXPECT_EQ(label->text(), "No image"); + auto topicsCombo = plugin->PluginItem()->findChild("topicsCombo"); + ASSERT_NE(topicsCombo, nullptr); + auto topicProp = topicsCombo->property("model"); + EXPECT_TRUE(topicProp.isValid()); + auto topicList = topicProp.toStringList(); + EXPECT_EQ(topicList.size(), 0); + + auto refreshButton = + plugin->PluginItem()->findChild("refreshButton"); + ASSERT_NE(refreshButton, nullptr); + + auto picker = + topicsCombo->parent(); // RowLayout that holds `visible: showPicker` + ASSERT_NE(picker, nullptr); + auto pickerProp = picker->property("visible"); + EXPECT_TRUE(pickerProp.isValid()); + EXPECT_TRUE(pickerProp.toBool()); + + // No images (gray image) + auto providerBase = app.Engine()->imageProvider( + plugin->CardItem()->objectName() + "imagedisplay"); + ASSERT_NE(providerBase, nullptr); + auto imageProvider = static_cast(providerBase); + ASSERT_NE(imageProvider, nullptr); + QSize dummySize; + QImage img = imageProvider->requestImage(QString(), &dummySize, dummySize); + EXPECT_TRUE(img.allGray()); // Cleanup plugins.clear(); - EXPECT_TRUE(stop()); } ///////////////////////////////////////////////// -TEST(ImageDisplayTest, ReceiveImage) +TEST(ImageDisplayTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(ReceiveImage)) { - setVerbosity(4); - EXPECT_TRUE(initApp()); + common::Console::SetVerbosity(4); + + Application app(g_argc, g_argv); + app.AddPluginPath( + common::joinPaths(std::string(PROJECT_BINARY_PATH), "lib")); // Load plugin const char *pluginStr = @@ -136,29 +208,38 @@ TEST(ImageDisplayTest, ReceiveImage) tinyxml2::XMLDocument pluginDoc; pluginDoc.Parse(pluginStr); - EXPECT_TRUE(loadPlugin("ImageDisplay", + EXPECT_TRUE(app.LoadPlugin("ImageDisplay", pluginDoc.FirstChildElement("plugin"))); - // Create main window - EXPECT_TRUE(createMainWindow()); - auto win = mainWindow(); - EXPECT_TRUE(win != nullptr); + // Get main window + auto win = app.findChild(); + ASSERT_NE(win, nullptr); // Get plugin - auto plugins = win->findChildren(); + auto plugins = win->findChildren(); EXPECT_EQ(plugins.size(), 1); auto plugin = plugins[0]; EXPECT_EQ(plugin->Title(), "Image display"); - // Doesn't have a topic picker - EXPECT_EQ(plugin->findChildren().size(), 0); - EXPECT_EQ(plugin->findChildren().size(), 0); - - // Starts with no image - auto label = plugin->findChild(); - EXPECT_TRUE(label != nullptr); - EXPECT_EQ(label->text(), "No image"); - EXPECT_TRUE(label->pixmap() == nullptr); + // Doesn't have a topic picker by checking `showPicker == false` + auto topicsCombo = plugin->PluginItem()->findChild("topicsCombo"); + ASSERT_NE(topicsCombo, nullptr); + auto picker = + topicsCombo->parent(); // RowLayout that holds `visible: showPicker` + ASSERT_NE(picker, nullptr); + auto pickerProp = picker->property("visible"); + EXPECT_TRUE(pickerProp.isValid()); + EXPECT_FALSE(pickerProp.toBool()); + + // Starts with no image (gray image) + auto providerBase = app.Engine()->imageProvider( + plugin->CardItem()->objectName() + "imagedisplay"); + ASSERT_NE(providerBase, nullptr); + auto imageProvider = static_cast(providerBase); + ASSERT_NE(imageProvider, nullptr); + QSize dummySize; + QImage img = imageProvider->requestImage(QString(), &dummySize, dummySize); + EXPECT_TRUE(img.allGray()); // Publish images transport::Node node; @@ -169,84 +250,339 @@ TEST(ImageDisplayTest, ReceiveImage) msgs::Image msg; msg.set_height(100); msg.set_width(200); - msg.set_pixel_format(common::Image::RGB_FLOAT32); + msg.set_pixel_format_type(msgs::PixelFormatType::RGB_FLOAT32); + pub.Publish(msg); + } + + // Give it time to be processed + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + QCoreApplication::processEvents(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Still no image + img = imageProvider->requestImage(QString(), &dummySize, dummySize); + EXPECT_TRUE(img.allGray()); + + // Good message + { + msgs::Image msg; + msg.set_height(100); + msg.set_width(200); + msg.set_pixel_format_type(msgs::PixelFormatType::RGB_INT8); + // bytes per pixel = channels * bytes = 3 * 1 + int bpp = 3; + msg.set_step(msg.width() * bpp); + + // red image + int bufferSize = msg.width() * msg.height() * bpp; + std::shared_ptr buffer(new unsigned char[bufferSize]); + for (int i = 0; i < bufferSize; i += bpp) + { + buffer.get()[i] = 255u; + buffer.get()[i + 1] = 0u; + buffer.get()[i + 2] = 0u; + } + msg.set_data(buffer.get(), bufferSize); pub.Publish(msg); } // Give it time to be processed int sleep = 0; - int maxSleep = 10; - while (!label->text().isEmpty() && sleep < maxSleep) + int maxSleep = 30; + while (img.allGray() && sleep < maxSleep) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); QCoreApplication::processEvents(); - sleep++; + img = imageProvider->requestImage(QString(), &dummySize, dummySize); + ++sleep; } - // Still no image - EXPECT_EQ(label->text(), "No image"); - EXPECT_TRUE(label->pixmap() == nullptr); + // Now it has an image + EXPECT_FALSE(img.allGray()); + EXPECT_EQ(img.height(), 100); + EXPECT_EQ(img.width(), 200); + + // check image is red + for (int y = 0; y < img.height(); ++y) + { + for (int x = 0; x < img.width(); ++x) + { + EXPECT_EQ(img.pixelColor(x, y).red(), 255); + EXPECT_EQ(img.pixelColor(x, y).green(), 0); + EXPECT_EQ(img.pixelColor(x, y).blue(), 0); + } + } + + // Cleanup + plugins.clear(); +} + +///////////////////////////////////////////////// +TEST(ImageDisplayTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(ReceiveImageFloat32)) +{ + common::Console::SetVerbosity(4); + + Application app(g_argc, g_argv); + app.AddPluginPath( + common::joinPaths(std::string(PROJECT_BINARY_PATH), "lib")); + + // Load plugin + const char *pluginStr = + "" + "/image_test" + ""; + + tinyxml2::XMLDocument pluginDoc; + pluginDoc.Parse(pluginStr); + EXPECT_TRUE(app.LoadPlugin("ImageDisplay", + pluginDoc.FirstChildElement("plugin"))); + + // Get main window + auto win = app.findChild(); + ASSERT_NE(win, nullptr); + + // Get plugin + auto plugins = win->findChildren(); + EXPECT_EQ(plugins.size(), 1); + auto plugin = plugins[0]; + EXPECT_EQ(plugin->Title(), "Image display"); + + // Starts with no image + auto providerBase = app.Engine()->imageProvider( + plugin->CardItem()->objectName() + "imagedisplay"); + ASSERT_NE(providerBase, nullptr); + auto imageProvider = static_cast(providerBase); + ASSERT_NE(imageProvider, nullptr); + QSize dummySize; + QImage img = imageProvider->requestImage(QString(), &dummySize, dummySize); + // When there is no image yet, a placeholder image with size 400 is given + // See ImageDisplay.hh, ImageProvider for more details + int placeholderSize = 400; + EXPECT_EQ(img.width(), placeholderSize); + EXPECT_EQ(img.height(), placeholderSize); + + // Publish images + transport::Node node; + auto pub = node.Advertise("/image_test"); // Good message { msgs::Image msg; - msg.set_height(100); - msg.set_width(200); - msg.set_pixel_format(common::Image::RGB_INT8); + msg.set_height(32); + msg.set_width(32); + msg.set_pixel_format_type(msgs::PixelFormatType::R_FLOAT32); + // bytes per pixel = channels * bytes = 1 * 4 + int bpp = 4; + msg.set_step(msg.width() * bpp); + + // first half is gray, second half is black + int bufferSize = msg.width() * msg.height() * bpp; + std::shared_ptr buffer(new float[bufferSize]); + for (unsigned int y = 0; y < msg.width(); ++y) + { + float v = 0.5f * static_cast(y / (msg.height() / 2.0) + 1); + for (unsigned int x = 0; x < msg.height(); ++x) + { + buffer.get()[y * msg.width() + x] = v; + } + } + + msg.set_data(buffer.get(), bufferSize); pub.Publish(msg); } // Give it time to be processed - sleep = 0; - while (!label->text().isEmpty() && sleep < maxSleep) + int sleep = 0; + int maxSleep = 30; + while (img.width() == placeholderSize && sleep < maxSleep) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); QCoreApplication::processEvents(); - sleep++; + img = imageProvider->requestImage(QString(), &dummySize, dummySize); + ++sleep; } - // Now it has an image - EXPECT_TRUE(label->text().isEmpty()); - ASSERT_NE(nullptr, label->pixmap()); - EXPECT_EQ(label->pixmap()->height(), 100); - EXPECT_EQ(label->pixmap()->width(), 200); + EXPECT_EQ(img.width(), 32); + EXPECT_EQ(img.height(), 32); + + // check image half gray & half black + for (int y = 0; y < img.height(); ++y) + { + for (int x = 0; x < img.width(); ++x) + { + if (y < img.height() / 2) + { + // expect gray + EXPECT_EQ(img.pixelColor(x, y).red(), 127); + EXPECT_EQ(img.pixelColor(x, y).green(), 127); + EXPECT_EQ(img.pixelColor(x, y).blue(), 127); + } + else + { + // expect black + EXPECT_EQ(img.pixelColor(x, y).red(), 0); + EXPECT_EQ(img.pixelColor(x, y).green(), 0); + EXPECT_EQ(img.pixelColor(x, y).blue(), 0); + } + } + } // Cleanup plugins.clear(); - EXPECT_TRUE(stop()); } ///////////////////////////////////////////////// -TEST(ImageDisplayTest, TopicPicker) +TEST(ImageDisplayTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(ReceiveImageInt16)) { - setVerbosity(4); - EXPECT_TRUE(initApp()); + common::Console::SetVerbosity(4); + + Application app(g_argc, g_argv); + app.AddPluginPath( + common::joinPaths(std::string(PROJECT_BINARY_PATH), "lib")); // Load plugin - EXPECT_TRUE(loadPlugin("ImageDisplay")); + const char *pluginStr = + "" + "/image_test" + ""; + + tinyxml2::XMLDocument pluginDoc; + pluginDoc.Parse(pluginStr); + EXPECT_TRUE(app.LoadPlugin("ImageDisplay", + pluginDoc.FirstChildElement("plugin"))); - // Create main window - EXPECT_TRUE(createMainWindow()); - auto win = mainWindow(); - EXPECT_TRUE(win != nullptr); + // Get main window + auto win = app.findChild(); + ASSERT_NE(win, nullptr); // Get plugin - auto plugins = win->findChildren(); + auto plugins = win->findChildren(); EXPECT_EQ(plugins.size(), 1); auto plugin = plugins[0]; EXPECT_EQ(plugin->Title(), "Image display"); - // Topic picker starts empty - auto topicsCombo = plugin->findChild("topicsCombo"); - EXPECT_TRUE(topicsCombo != nullptr); - EXPECT_EQ(topicsCombo->count(), 0); + // Starts with no image + auto providerBase = app.Engine()->imageProvider( + plugin->CardItem()->objectName() + "imagedisplay"); + ASSERT_NE(providerBase, nullptr); + auto imageProvider = static_cast(providerBase); + ASSERT_NE(imageProvider, nullptr); + QSize dummySize; + QImage img = imageProvider->requestImage(QString(), &dummySize, dummySize); + // When there is no image yet, a placeholder image with size 400 is given + // See ImageDisplay.hh, ImageProvider for more details + int placeholderSize = 400; + EXPECT_EQ(img.width(), placeholderSize); + EXPECT_EQ(img.height(), placeholderSize); - auto refreshButton = plugin->findChild("refreshButton"); - EXPECT_TRUE(refreshButton != nullptr); + // Publish images + transport::Node node; + auto pub = node.Advertise("/image_test"); + + // Good message + { + msgs::Image msg; + msg.set_height(32); + msg.set_width(32); + msg.set_pixel_format_type(msgs::PixelFormatType::L_INT16); + // bytes per pixel = channels * bytes = 1 * 2 + int bpp = 2; + msg.set_step(msg.width() * bpp); + + // first half is black, second half is white + int bufferSize = msg.width() * msg.height() * bpp; + std::shared_ptr buffer(new uint16_t[bufferSize]); + for (unsigned int y = 0; y < msg.width(); ++y) + { + uint16_t v = 100 * static_cast(y / (msg.height() / 2.0) + 1); + for (unsigned int x = 0; x < msg.height(); ++x) + { + buffer.get()[y * msg.width() + x] = v; + } + } + + msg.set_data(buffer.get(), bufferSize); + pub.Publish(msg); + } + + // Give it time to be processed + int sleep = 0; + int maxSleep = 30; + while (img.width() == placeholderSize && sleep < maxSleep) + { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + QCoreApplication::processEvents(); + img = imageProvider->requestImage(QString(), &dummySize, dummySize); + ++sleep; + } + + EXPECT_EQ(img.width(), 32); + EXPECT_EQ(img.height(), 32); + + // check image half gray & half black + for (int y = 0; y < img.height(); ++y) + { + for (int x = 0; x < img.width(); ++x) + { + if (y < img.height() / 2) + { + // expect black + EXPECT_EQ(img.pixelColor(x, y).red(), 0); + EXPECT_EQ(img.pixelColor(x, y).green(), 0); + EXPECT_EQ(img.pixelColor(x, y).blue(), 0); + } + else + { + // expect white + EXPECT_EQ(img.pixelColor(x, y).red(), 255); + EXPECT_EQ(img.pixelColor(x, y).green(), 255); + EXPECT_EQ(img.pixelColor(x, y).blue(), 255); + } + } + } + + // Cleanup + plugins.clear(); +} + +///////////////////////////////////////////////// +TEST(ImageDisplayTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(TopicPicker)) +{ + common::Console::SetVerbosity(4); + + Application app(g_argc, g_argv); + app.AddPluginPath( + common::joinPaths(std::string(PROJECT_BINARY_PATH), "lib")); + + // Load plugin + EXPECT_TRUE(app.LoadPlugin("ImageDisplay")); + + // Get main window + auto win = app.findChild(); + ASSERT_NE(win, nullptr); + + // Get plugin + auto plugins = win->findChildren(); + EXPECT_EQ(plugins.size(), 1); + auto plugin = plugins[0]; + EXPECT_EQ(plugin->Title(), "Image display"); + + // Topic picker starts empty + auto topicsCombo = plugin->PluginItem()->findChild("topicsCombo"); + ASSERT_NE(topicsCombo, nullptr); + auto topicProp = topicsCombo->property("model"); + EXPECT_TRUE(topicProp.isValid()); + auto topicList = topicProp.toStringList(); + EXPECT_EQ(topicList.size(), 0); + EXPECT_EQ(topicList.size(), plugin->TopicList().size()); // Refresh and still empty - refreshButton->click(); - EXPECT_EQ(topicsCombo->count(), 0); + plugin->OnRefresh(); + topicProp = topicsCombo->property("model"); + EXPECT_TRUE(topicProp.isValid()); + topicList = topicProp.toStringList(); + EXPECT_EQ(topicList.size(), 0); + EXPECT_EQ(topicList.size(), plugin->TopicList().size()); // Advertise topics transport::Node node; @@ -255,17 +591,30 @@ TEST(ImageDisplayTest, TopicPicker) auto pubString = node.Advertise("/string_test"); // Refresh and now we have image topics - refreshButton->click(); - EXPECT_EQ(topicsCombo->count(), 2); - EXPECT_EQ(topicsCombo->itemText(0), "/image_test"); - EXPECT_EQ(topicsCombo->itemText(1), "/image_test_2"); - - // Pick topics - topicsCombo->setCurrentIndex(1); - topicsCombo->setCurrentIndex(0); - topicsCombo->setCurrentIndex(1); + plugin->OnRefresh(); + topicProp = topicsCombo->property("model"); + EXPECT_TRUE(topicProp.isValid()); + topicList = topicProp.toStringList(); + EXPECT_EQ(topicList.size(), 2); + EXPECT_EQ(topicList.size(), plugin->TopicList().size()); + + EXPECT_EQ(topicList.at(0).toStdString(), "/image_test"); + EXPECT_EQ(topicList.at(1).toStdString(), "/image_test_2"); + EXPECT_EQ(topicList.at(0), plugin->TopicList().at(0)); + EXPECT_EQ(topicList.at(1), plugin->TopicList().at(1)); + + // Set image topics + QStringList newTopicList = {"/new_image_test"}; + plugin->SetTopicList(newTopicList); + + topicProp = topicsCombo->property("model"); + EXPECT_TRUE(topicProp.isValid()); + topicList = topicProp.toStringList(); + EXPECT_EQ(topicList.size(), 1); + EXPECT_EQ(topicList.size(), plugin->TopicList().size()); + EXPECT_EQ(topicList.at(0).toStdString(), "/new_image_test"); + EXPECT_EQ(topicList.at(0), plugin->TopicList().at(0)); // Cleanup plugins.clear(); - EXPECT_TRUE(stop()); } From 0948063279e5164e0ecfff4a1f80d269cbfbf5b0 Mon Sep 17 00:00:00 2001 From: "Addisu Z. Taddese" Date: Tue, 30 Aug 2022 13:52:49 -0500 Subject: [PATCH 4/6] Update cmd/CMakeLists to conform with all other gz libraries (#478) This fixes the library path obtained by cmdgui.rb such that it points to the .so file contained in the non-dev debian pack Signed-off-by: Addisu Z. Taddese --- conf/CMakeLists.txt | 19 +++++++++------- src/CMakeLists.txt | 4 +++- src/cmd/CMakeLists.txt | 49 ++++++++++++++++++++++++++++++++++-------- src/cmd/cmdgui.rb.in | 12 +++++++++-- 4 files changed, 64 insertions(+), 20 deletions(-) diff --git a/conf/CMakeLists.txt b/conf/CMakeLists.txt index 6c32a6a7c..ffb8836a6 100644 --- a/conf/CMakeLists.txt +++ b/conf/CMakeLists.txt @@ -1,20 +1,23 @@ -set(ign_library_path "${CMAKE_BINARY_DIR}/src/cmd/cmdgui${PROJECT_VERSION_MAJOR}") +# Used only for internal testing. +set(ign_library_path "${CMAKE_BINARY_DIR}/test/lib/ruby/ignition/cmd${GZ_DESIGNATION}${PROJECT_VERSION_MAJOR}") # Generate a configuration file for internal testing. # Note that the major version of the library is included in the name. # Ex: gui0.yaml configure_file( - "gui.yaml.in" - "${CMAKE_BINARY_DIR}/test/conf/gui${PROJECT_VERSION_MAJOR}.yaml" @ONLY) + "${GZ_DESIGNATION}.yaml.in" + "${CMAKE_BINARY_DIR}/test/conf/${GZ_DESIGNATION}${PROJECT_VERSION_MAJOR}.yaml" @ONLY) -set(ign_library_path "${CMAKE_INSTALL_PREFIX}/lib/ruby/ignition/cmdgui${PROJECT_VERSION_MAJOR}") +# Used for the installed version. +set(ign_library_path "${CMAKE_INSTALL_PREFIX}/lib/ruby/ignition/cmd${GZ_DESIGNATION}${PROJECT_VERSION_MAJOR}") -# Generate a configuration file. +# Generate the configuration file that is installed. # Note that the major version of the library is included in the name. # Ex: gui0.yaml configure_file( - "gui.yaml.in" - "${CMAKE_CURRENT_BINARY_DIR}/gui${PROJECT_VERSION_MAJOR}.yaml" @ONLY) + "${GZ_DESIGNATION}.yaml.in" + "${CMAKE_CURRENT_BINARY_DIR}/${GZ_DESIGNATION}${PROJECT_VERSION_MAJOR}.yaml" @ONLY) # Install the yaml configuration files in an unversioned location. -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/gui${PROJECT_VERSION_MAJOR}.yaml DESTINATION ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/ignition/) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${GZ_DESIGNATION}${PROJECT_VERSION_MAJOR}.yaml + DESTINATION ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/ignition/) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2c3a3beeb..6d92b8912 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -77,5 +77,7 @@ if(TARGET UNIT_ign_TEST) ENVIRONMENT "${_env_vars}") endif() -add_subdirectory(cmd) +if(NOT WIN32) + add_subdirectory(cmd) +endif() add_subdirectory(plugins) diff --git a/src/cmd/CMakeLists.txt b/src/cmd/CMakeLists.txt index 25e6c9d58..6b37e1108 100644 --- a/src/cmd/CMakeLists.txt +++ b/src/cmd/CMakeLists.txt @@ -1,16 +1,47 @@ -# Generate the ruby script. +#=============================================================================== +# Generate the ruby script for internal testing. # Note that the major version of the library is included in the name. -if (APPLE) - set(IGN_LIBRARY_NAME lib${PROJECT_NAME_LOWER}.dylib) -else() - set(IGN_LIBRARY_NAME lib${PROJECT_NAME_LOWER}.so) -endif() +# Ex: cmdgui3.rb +set(cmd_script_generated_test "${CMAKE_BINARY_DIR}/test/lib/ruby/ignition/cmd${GZ_DESIGNATION}${PROJECT_VERSION_MAJOR}.rb") +set(cmd_script_configured_test "${cmd_script_generated_test}.configured") + +# Set the library_location variable to the full path of the library file within +# the build directory. +set(library_location "$") + +configure_file( + "cmd${GZ_DESIGNATION}.rb.in" + "${cmd_script_configured_test}" + @ONLY) + +file(GENERATE + OUTPUT "${cmd_script_generated_test}" + INPUT "${cmd_script_configured_test}") + + +#=============================================================================== +# Used for the installed version. +# Generate the ruby script that gets installed. +# Note that the major version of the library is included in the name. +# Ex: cmdgui3.rb +set(cmd_script_generated "${CMAKE_CURRENT_BINARY_DIR}/cmd${GZ_DESIGNATION}${PROJECT_VERSION_MAJOR}.rb") +set(cmd_script_configured "${cmd_script_generated}.configured") + +# Set the library_location variable to the relative path to the library file +# within the install directory structure. +set(library_location "../../../${CMAKE_INSTALL_LIBDIR}/$") + configure_file( - "cmdgui.rb.in" - "${CMAKE_CURRENT_BINARY_DIR}/cmdgui${PROJECT_VERSION_MAJOR}.rb" @ONLY) + "cmd${GZ_DESIGNATION}.rb.in" + "${cmd_script_configured}" + @ONLY) + +file(GENERATE + OUTPUT "${cmd_script_generated}" + INPUT "${cmd_script_configured}") # Install the ruby command line library in an unversioned location. -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/cmdgui${PROJECT_VERSION_MAJOR}.rb DESTINATION lib/ruby/ignition) +install(FILES ${cmd_script_generated} DESTINATION lib/ruby/ignition) # Tack version onto and install the bash completion script configure_file( diff --git a/src/cmd/cmdgui.rb.in b/src/cmd/cmdgui.rb.in index 5ecdd4260..ddc84f728 100644 --- a/src/cmd/cmdgui.rb.in +++ b/src/cmd/cmdgui.rb.in @@ -28,7 +28,7 @@ end require 'optparse' # Constants. -LIBRARY_NAME = '@IGN_LIBRARY_NAME@' +LIBRARY_NAME = '@library_location@' LIBRARY_VERSION = '@PROJECT_VERSION_FULL@' COMMON_OPTIONS = " -h [ --help ] Print this help message.\n"\ @@ -134,7 +134,15 @@ class Cmd # puts options # Read the plugin that handles the command. - plugin = LIBRARY_NAME + if LIBRARY_NAME[0] == '/' + # If the first character is a slash, we'll assume that we've been given an + # absolute path to the library. This is only used during test mode. + plugin = LIBRARY_NAME + else + # We're assuming that the library path is relative to the current + # location of this script. + plugin = File.expand_path(File.join(File.dirname(__FILE__), LIBRARY_NAME)) + end conf_version = LIBRARY_VERSION begin From b09d084c737695725f1c6e99b330eeba67b4ea4a Mon Sep 17 00:00:00 2001 From: Jenn Nguyen Date: Mon, 7 Nov 2022 09:17:00 -0800 Subject: [PATCH 5/6] Add key publisher test (#477) Signed-off-by: Jenn Nguyen --- src/plugins/key_publisher/CMakeLists.txt | 6 +- src/plugins/key_publisher/KeyPublisher.hh | 12 +- .../key_publisher/KeyPublisher_TEST.cc | 133 ++++++++++++++++++ 3 files changed, 147 insertions(+), 4 deletions(-) create mode 100644 src/plugins/key_publisher/KeyPublisher_TEST.cc diff --git a/src/plugins/key_publisher/CMakeLists.txt b/src/plugins/key_publisher/CMakeLists.txt index b4b77d4d1..b75b996fc 100644 --- a/src/plugins/key_publisher/CMakeLists.txt +++ b/src/plugins/key_publisher/CMakeLists.txt @@ -3,6 +3,6 @@ ign_gui_add_plugin(KeyPublisher KeyPublisher.cc QT_HEADERS KeyPublisher.hh - TEST_SOURCES # todo - # KeyPublisher_TEST.cc -) \ No newline at end of file + TEST_SOURCES + KeyPublisher_TEST.cc +) diff --git a/src/plugins/key_publisher/KeyPublisher.hh b/src/plugins/key_publisher/KeyPublisher.hh index 38ecdd2e5..f375feaf4 100644 --- a/src/plugins/key_publisher/KeyPublisher.hh +++ b/src/plugins/key_publisher/KeyPublisher.hh @@ -18,6 +18,16 @@ #ifndef IGNITION_GUI_PLUGINS_KEYPUBLISHER_HH_ #define IGNITION_GUI_PLUGINS_KEYPUBLISHER_HH_ +#ifndef _WIN32 +# define KeyPublisher_EXPORTS_API +#else +# if (defined(KeyPublisher_EXPORTS)) +# define KeyPublisher_EXPORTS_API __declspec(dllexport) +# else +# define KeyPublisher_EXPORTS_API __declspec(dllimport) +# endif +#endif + #include #include @@ -35,7 +45,7 @@ namespace gui /// /// ## Configuration /// This plugin doesn't accept any custom configuration. - class KeyPublisher : public ignition::gui::Plugin + class KeyPublisher_EXPORTS_API KeyPublisher : public ignition::gui::Plugin { Q_OBJECT diff --git a/src/plugins/key_publisher/KeyPublisher_TEST.cc b/src/plugins/key_publisher/KeyPublisher_TEST.cc new file mode 100644 index 000000000..6dae5d1fd --- /dev/null +++ b/src/plugins/key_publisher/KeyPublisher_TEST.cc @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2022 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 +#ifdef _MSC_VER +#pragma warning(push, 0) +#endif +#include +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +#include +#include +#include + +#include "ignition/gui/Application.hh" +#include "ignition/gui/MainWindow.hh" +#include "ignition/gui/Plugin.hh" +#include "ignition/gui/qt.h" +#include "test_config.h" // NOLINT(build/include) + +#include "KeyPublisher.hh" + +int g_argc = 1; +char* g_argv[] = +{ + reinterpret_cast(const_cast("./KeyPublisher_TEST")), +}; + +using namespace ignition; +using namespace gui; + +class KeyPublisherTest : public ::testing::Test +{ + // Set up function. + protected: void SetUp() override + { + common::Console::SetVerbosity(4); + + this->app.AddPluginPath( + common::joinPaths(std::string(PROJECT_BINARY_PATH), "lib")); + + // Load plugin + EXPECT_TRUE(this->app.LoadPlugin("KeyPublisher")); + + // Get main window + this->win = this->app.findChild(); + ASSERT_NE(win, nullptr); + + // Get plugin + this->plugins = win->findChildren(); + ASSERT_EQ(plugins.size(), 1); + this->plugin = plugins[0]; + EXPECT_EQ(this->plugin->Title(), "Key publisher"); + + // Subscribes to keyboard/keypress topic + const std::string kTopic{"keyboard/keypress"}; + node.Subscribe(kTopic, &KeyPublisherTest::VerifyKeypressCb, this); + } + + // Tear down function + protected: void TearDown() override + { + // Cleanup + plugins.clear(); + } + + // Callback function to verify key message was sent correctly + protected: void VerifyKeypressCb(const msgs::Int32 &_msg) + { + this->received = true; + EXPECT_EQ(_msg.data(), this->currentKey); + } + + protected: void VerifyKeyEvent(int _key) + { + this->received = false; + this->currentKey = _key; + auto event = new QKeyEvent(QKeyEvent::KeyPress, _key, Qt::NoModifier); + this->app.sendEvent(this->win->QuickWindow(), event); + + int sleep = 0; + int maxSleep = 30; + while (!this->received && sleep < maxSleep) + { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + QCoreApplication::processEvents(); + ++sleep; + } + + EXPECT_LT(sleep, maxSleep); + EXPECT_TRUE(this->received); + } + + // Provides an API to load plugins and configuration files. + protected: Application app{g_argc, g_argv}; + + // Instance of the main window. + protected: MainWindow *win; + + // List of plugins. + protected: QList plugins; + protected: KeyPublisher *plugin; + + // Checks if a new key has been received. + protected: bool received = false; + protected: transport::Node node; + + // Current key + protected: int currentKey = 0; +}; + +///////////////////////////////////////////////// +TEST_F(KeyPublisherTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(KeyPublisher)) +{ + this->VerifyKeyEvent(Qt::Key_W); + this->VerifyKeyEvent(Qt::Key_A); + this->VerifyKeyEvent(Qt::Key_D); +} From 721eca00b5d4e1b8167d5ff19cb4b1fdf3b98a77 Mon Sep 17 00:00:00 2001 From: Nate Koenig Date: Thu, 10 Nov 2022 16:45:34 -0800 Subject: [PATCH 6/6] Add pointer check in Application::RemovePlugin (#501) Signed-off-by: Nate Koenig --- src/Application.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Application.cc b/src/Application.cc index 77073ffdd..9928224f6 100644 --- a/src/Application.cc +++ b/src/Application.cc @@ -203,7 +203,7 @@ bool Application::RemovePlugin(const std::string &_pluginName) // Remove split on QML auto bgItem = this->dataPtr->mainWin->QuickWindow() ->findChild("background"); - if (bgItem) + if (bgItem && cardItem->parentItem()) { QMetaObject::invokeMethod(bgItem, "removeSplitItem", Q_ARG(QVariant, cardItem->parentItem()->objectName()));