diff --git a/Changelog.md b/Changelog.md index 9d59c77ee..a43209547 100644 --- a/Changelog.md +++ b/Changelog.md @@ -600,6 +600,11 @@ ## 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 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/include/ignition/gui/qml/GzPose.qml b/include/ignition/gui/qml/GzPose.qml index 8f0567324..60355cbfa 100644 --- a/include/ignition/gui/qml/GzPose.qml +++ b/include/ignition/gui/qml/GzPose.qml @@ -34,6 +34,7 @@ import QtQuick.Controls.Styles 1.4 * GzPose { * id: gzPose * readOnly: false + * useRadian: true * xValue: xValueFromCPP * yValue: yValueFromCPP * zValue: zValueFromCPP @@ -59,6 +60,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 @@ -222,7 +226,7 @@ Item { Text { id: rollText - text: 'Roll (rad)' + text: 'Roll ' + (useRadian ? '(rad)' : '(deg)') leftPadding: 5 color: Material.theme == Material.Light ? "#444444" : "#bbbbbb" font.pointSize: 12 @@ -292,7 +296,7 @@ Item { Text { id: pitchText - text: 'Pitch (rad)' + text: 'Pitch ' + (useRadian ? '(rad)' : '(deg)') leftPadding: 5 color: Material.theme == Material.Light ? "#444444" : "#bbbbbb" font.pointSize: 12 @@ -362,7 +366,7 @@ Item { Text { id: yawText - text: 'Yaw (rad)' + text: 'Yaw ' + (useRadian ? '(rad)' : '(deg)') leftPadding: 5 color: Material.theme == Material.Light ? "#444444" : "#bbbbbb" font.pointSize: 12 diff --git a/src/Application.cc b/src/Application.cc index c9d6f0215..4c955e8aa 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())); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e79fb1b01..aaf0b9b74 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -80,5 +80,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 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 15abb4d0d..58a93482a 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. @@ -236,7 +202,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(); @@ -247,8 +217,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 c445a9b4e..c8425a2b0 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()); } diff --git a/src/plugins/key_publisher/CMakeLists.txt b/src/plugins/key_publisher/CMakeLists.txt index b4b77d4d1..d4a7fff2b 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); +}