From aafacbfd47010cd2de2c2b4117598a38cfecb7a1 Mon Sep 17 00:00:00 2001 From: Mabel Zhang Date: Thu, 14 Apr 2022 12:20:02 -0400 Subject: [PATCH 01/31] Sort plugin list in alphabetical order (including when filtering) (#387) Signed-off-by: Mabel Zhang --- include/ignition/gui/qml/PluginMenu.qml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/include/ignition/gui/qml/PluginMenu.qml b/include/ignition/gui/qml/PluginMenu.qml index 68de63a7c..cee476f70 100644 --- a/include/ignition/gui/qml/PluginMenu.qml +++ b/include/ignition/gui/qml/PluginMenu.qml @@ -95,6 +95,12 @@ Popup { IgnSortFilterModel { id: filteredModel + lessThan: function(left, right) { + var leftStr = left.modelData.toLowerCase(); + var rightStr = right.modelData.toLowerCase(); + return leftStr < rightStr; + } + filterAcceptsItem: function(item) { var itemStr = item.modelData.toLowerCase(); var filterStr = searchField.text.toLowerCase(); From c51789f27a4c3620f0bf652c5de5bd705d0fd3de Mon Sep 17 00:00:00 2001 From: "Addisu Z. Taddese" Date: Mon, 2 May 2022 17:54:37 -0500 Subject: [PATCH 02/31] Add repo specific issue templates (#393) Override the org level issue templates in order to make the issue templates more specific to this repo. Signed-off-by: Addisu Z. Taddese --- .github/ISSUE_TEMPLATE/bug_report.md | 72 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 23 ++++++++ 2 files changed, 95 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..8e30646e3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,72 @@ +--- +name: Bug report +about: Report a bug +labels: bug +--- + + + +## Environment +* OS Version: +* Source or binary build? + + +* If this is a GUI or sensor rendering bug, describe your GPU and rendering system. Otherwise delete this section. + - Rendering plugin: [ogre | ogre2]. + - [ ] Sensor rendering error. + - [ ] GUI rendering error. + - EGL headless mode: + - [ ] Running in EGL headless mode + - Generally, mention all circumstances that might affect rendering capabilities: + - [ ] running on a dual GPU machine (integrated GPU + discrete GPU) + - [ ] running on a multi-GPU machine (it has multiple discrete GPUs) + - [ ] running on real hardware + - [ ] running in virtual machine + - [ ] running in Docker/Singularity + - [ ] running remotely (e.g. via SSH) + - [ ] running in a cloud + - [ ] using VirtualGL, XVFB, Xdummy, XVNC or other indirect rendering utilities + - [ ] GPU is concurrently used for other tasks + - [ ] desktop acceleration + - [ ] video decoding (i.e. a playing Youtube video) + - [ ] video encoding + - [ ] CUDA/ROCm computations (Tensorflow, Torch, Caffe running) + - [ ] multiple simulators running at the same time + - [ ] other... + - Rendering system info: + - On Linux, provide the outputs of the following commands: + ```bash + LANG=C lspci -nn | grep VGA # might require installing pciutils + echo "$DISPLAY" + LANG=C glxinfo -B | grep -i '\(direct rendering\|opengl\|profile\)' # might require installing mesa-utils package + ps aux | grep Xorg + sudo env LANG=C X -version # if you don't have root access, try to tell the version of Xorg e.g. via package manager + ``` + - On Windows, run `dxdiag` and report the GPU-related information. + - On Mac OS, open a terminal and type `system_profiler SPDisplaysDataType`. Copy the output here. + + - [ ] Please, attach the ogre.log or ogre2.log file from `~/.ignition/rendering` + +
+ +``` +# paste log here +``` + +
+ +## Description +* Expected behavior: +* Actual behavior: + +## Steps to reproduce + + +1. +2. +3. + +## Output + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..87233a479 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: Feature request +about: Request a new feature +labels: enhancement +--- + + + +## Desired behavior + + +## Alternatives considered + + +## Implementation suggestion + + +## Additional context + From a54bdbc3ed53d96588bce84334289a9c136ee030 Mon Sep 17 00:00:00 2001 From: Mabel Zhang Date: Tue, 3 May 2022 15:11:34 -0400 Subject: [PATCH 03/31] Add config relative path environment variable (#386) Signed-off-by: Mabel Zhang --- src/Application.cc | 36 ++++++++++++++++++++++++++++++++---- src/Application_TEST.cc | 22 ++++++++++++++++++++++ src/cmd/cmdgui.rb.in | 5 ++++- tutorials/07_config.md | 14 ++++++++++++-- 4 files changed, 70 insertions(+), 7 deletions(-) diff --git a/src/Application.cc b/src/Application.cc index 95d1e2a55..38facd1c4 100644 --- a/src/Application.cc +++ b/src/Application.cc @@ -19,7 +19,9 @@ #include #include +#include #include +#include #include #include @@ -222,24 +224,50 @@ bool Application::LoadConfig(const std::string &_config) return false; } + std::string configFull = _config; + + // Check if the passed in config file exists. + // (If the default config path doesn't exist yet, it's expected behavior. + // It will be created the first time the user presses "Save configuration".) + if (!common::exists(configFull) && (configFull != this->DefaultConfigPath())) + { + // If not, then check environment variable + std::string configPathEnv; + common::env("GZ_GUI_RESOURCE_PATH", configPathEnv); + + if (!configPathEnv.empty()) + { + std::vector parentPaths = common::Split(configPathEnv, ':'); + for (auto parentPath : parentPaths) + { + std::string tempPath = common::joinPaths(parentPath, configFull); + if (common::exists(tempPath)) + { + configFull = tempPath; + break; + } + } + } + } + // Use tinyxml to read config tinyxml2::XMLDocument doc; - auto success = !doc.LoadFile(_config.c_str()); + auto success = !doc.LoadFile(configFull.c_str()); if (!success) { // We do not show an error message if the default config path doesn't exist // yet. It's expected behavior, it will be created the first time the user // presses "Save configuration". - if (_config != this->DefaultConfigPath()) + if (configFull != this->DefaultConfigPath()) { - ignerr << "Failed to load file [" << _config << "]: XMLError" + ignerr << "Failed to load file [" << configFull << "]: XMLError" << std::endl; } return false; } - ignmsg << "Loading config [" << _config << "]" << std::endl; + ignmsg << "Loading config [" << configFull << "]" << std::endl; // Clear all previous plugins auto plugins = this->dataPtr->mainWin->findChildren(); diff --git a/src/Application_TEST.cc b/src/Application_TEST.cc index 5d575a3c9..9ca02cc90 100644 --- a/src/Application_TEST.cc +++ b/src/Application_TEST.cc @@ -172,6 +172,28 @@ TEST(ApplicationTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(LoadConfig)) auto testSourcePath = std::string(PROJECT_SOURCE_PATH) + "/test/"; EXPECT_TRUE(app.LoadConfig(testSourcePath + "config/test.config")); } + + // Test environment variable and relative path + { + // Environment variable not set + Application app(g_argc, g_argv); + EXPECT_FALSE(app.LoadConfig("ignore.config")); + + // Invalid path + setenv("GZ_GUI_RESOURCE_PATH", "invalidPath", 1); + EXPECT_FALSE(app.LoadConfig("ignore.config")); + + // Valid path + setenv("GZ_GUI_RESOURCE_PATH", + (std::string(PROJECT_SOURCE_PATH) + "/test/config").c_str(), 1); + EXPECT_TRUE(app.LoadConfig("ignore.config")); + + // Multiple paths, one valid + setenv("GZ_GUI_RESOURCE_PATH", + ("banana:" + std::string(PROJECT_SOURCE_PATH) + "/test/config" + + ":orange").c_str(), 1); + EXPECT_TRUE(app.LoadConfig("ignore.config")); + } } ////////////////////////////////////////////////// diff --git a/src/cmd/cmdgui.rb.in b/src/cmd/cmdgui.rb.in index 2b766662f..5ecdd4260 100644 --- a/src/cmd/cmdgui.rb.in +++ b/src/cmd/cmdgui.rb.in @@ -52,7 +52,10 @@ COMMANDS = { 'gui' => " The default verbosity is 1, use -v without\n"\ " arguments for level 3.\n"\ "\n" + - COMMON_OPTIONS, + COMMON_OPTIONS + "\n\n" + + "Environment variables: \n"\ + " GZ_GUI_RESOURCE_PATH Colon separated paths used to locate GUI \n"\ + " resources such as configuration files. \n"\ } # diff --git a/tutorials/07_config.md b/tutorials/07_config.md index 240ac6786..8178cbda6 100644 --- a/tutorials/07_config.md +++ b/tutorials/07_config.md @@ -15,9 +15,19 @@ By default, Ignition GUI will load the config file at Configuration files can also be loaded from the command line or through the C++ API. -From the command line, use the `--config` / `-c` option, for example: +From the command line, use the `--config` / `-c` option. +For example, you can specify an absolute path: -`ign gui -c path/to/example.config` +`ign gui -c /absolute/path/to/example.config` + +Or a path relative to the current working directory: + +`ign gui -c relative/path/to/example.config` + +Or a path relative to a custom directory, which you can specify by setting the +environment variable `GZ_GUI_RESOURCE_PATH`, like so: + +`GZ_GUI_RESOURCE_PATH=/absolute/path/to/ ign gui --config example.config` From the C++ API, pass the file path to [Application::LoadConfig](https://ignitionrobotics.org/api/gui/3.0/classignition_1_1gui_1_1Application.html#a03c4c3a1b1e58cc4bff05658f21fff17). From 5c096882eb2e7a3bd120983a627cf80da66064b6 Mon Sep 17 00:00:00 2001 From: AzulRadio <50132891+AzulRadio@users.noreply.github.com> Date: Thu, 19 May 2022 11:17:54 -0500 Subject: [PATCH 04/31] User camera FOV control in SDF files (#400) * allow SDF FOV tag Signed-off-by: youhy --- src/plugins/minimal_scene/MinimalScene.cc | 28 ++++++++++++++++++++++- src/plugins/minimal_scene/MinimalScene.hh | 9 ++++++++ test/integration/minimal_scene.cc | 3 +++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/plugins/minimal_scene/MinimalScene.cc b/src/plugins/minimal_scene/MinimalScene.cc index 4c3d50e4a..56833ed16 100644 --- a/src/plugins/minimal_scene/MinimalScene.cc +++ b/src/plugins/minimal_scene/MinimalScene.cc @@ -585,7 +585,7 @@ std::string IgnRenderer::Initialize() this->dataPtr->camera->SetImageWidth(this->textureSize.width()); this->dataPtr->camera->SetImageHeight(this->textureSize.height()); this->dataPtr->camera->SetAntiAliasing(8); - this->dataPtr->camera->SetHFOV(M_PI * 0.5); + this->dataPtr->camera->SetHFOV(this->cameraHFOV); // setting the size and calling PreRender should cause the render texture to // be rebuilt this->dataPtr->camera->PreRender(); @@ -997,6 +997,12 @@ void RenderWindowItem::SetSkyEnabled(const bool &_sky) this->dataPtr->renderThread->ignRenderer.skyEnable = _sky; } +///////////////////////////////////////////////// +void RenderWindowItem::SetCameraHFOV(const math::Angle &_fov) +{ + this->dataPtr->renderThread->ignRenderer.cameraHFOV = _fov; +} + ///////////////////////////////////////////////// MinimalScene::MinimalScene() : Plugin(), dataPtr(utils::MakeUniqueImpl()) @@ -1116,6 +1122,26 @@ void MinimalScene::LoadConfig(const tinyxml2::XMLElement *_pluginElem) if (!elem->NoChildren()) ignwarn << "Child elements of are not supported yet" << std::endl; } + + elem = _pluginElem->FirstChildElement("horizontal_fov"); + if (nullptr != elem && nullptr != elem->GetText()) + { + double fovDeg; + math::Angle fov; + std::stringstream fovStr; + fovStr << std::string(elem->GetText()); + fovStr >> fovDeg; + if (fovStr.fail()) + { + ignerr << "Unable to set to '" << fovStr.str() + << "' using default horizontal field of view" << std::endl; + } + else + { + fov.SetDegree(fovDeg); + renderWindow->SetCameraHFOV(fov); + } + } } renderWindow->SetEngineName(cmdRenderEngine); diff --git a/src/plugins/minimal_scene/MinimalScene.hh b/src/plugins/minimal_scene/MinimalScene.hh index 2e2f89f70..29b41903b 100644 --- a/src/plugins/minimal_scene/MinimalScene.hh +++ b/src/plugins/minimal_scene/MinimalScene.hh @@ -61,6 +61,8 @@ namespace plugins /// * \ : Camera's near clipping plane distance, defaults to 0.01 /// * \ : Camera's far clipping plane distance, defaults to 1000.0 /// * \ : If present, sky is enabled. + /// * \ : Horizontal FOV of the user camera in degrees, + /// defaults to 90 class MinimalScene : public Plugin { Q_OBJECT @@ -237,6 +239,9 @@ namespace plugins /// \brief True if sky is enabled; public: bool skyEnable = false; + /// \brief Horizontal FOV of the camera; + public: math::Angle cameraHFOV = math::Angle(M_PI * 0.5); + /// \internal /// \brief Pointer to private data. IGN_UTILS_UNIQUE_IMPL_PTR(dataPtr) @@ -338,6 +343,10 @@ namespace plugins /// \param[in] _sky True to enable the sky, false otherwise. public: void SetSkyEnabled(const bool &_sky); + /// \brief Set the Horizontal FOV of the camera + /// \param[in] _fov FOV of the camera in degree + public: void SetCameraHFOV(const ignition::math::Angle &_fov); + /// \brief Slot called when thread is ready to be started public Q_SLOTS: void Ready(); diff --git a/test/integration/minimal_scene.cc b/test/integration/minimal_scene.cc index d6fc675cd..a482704ee 100644 --- a/test/integration/minimal_scene.cc +++ b/test/integration/minimal_scene.cc @@ -89,6 +89,7 @@ TEST(MinimalSceneTest, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(Config)) " 0.1" " 5000" "" + "60" ""; tinyxml2::XMLDocument pluginDoc; @@ -154,6 +155,8 @@ TEST(MinimalSceneTest, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(Config)) EXPECT_DOUBLE_EQ(0.1, camera->NearClipPlane()); EXPECT_DOUBLE_EQ(5000.0, camera->FarClipPlane()); + EXPECT_DOUBLE_EQ(60, camera->HFOV().Degree()); + // Cleanup auto plugins = win->findChildren(); EXPECT_EQ(1, plugins.size()); From 5d5755cf98a55394a92716ab6dc2ca12aade9e0b Mon Sep 17 00:00:00 2001 From: AzulRadio <50132891+AzulRadio@users.noreply.github.com> Date: Thu, 26 May 2022 10:38:17 -0500 Subject: [PATCH 05/31] Search menu keyboard control (#403) Signed-off-by: youhy --- include/ignition/gui/qml/PluginMenu.qml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/include/ignition/gui/qml/PluginMenu.qml b/include/ignition/gui/qml/PluginMenu.qml index 68de63a7c..732c7fc66 100644 --- a/include/ignition/gui/qml/PluginMenu.qml +++ b/include/ignition/gui/qml/PluginMenu.qml @@ -72,6 +72,18 @@ Popup { onTextEdited: { filteredModel.update(); } + Keys.onReturnPressed: { + MainWindow.OnAddPlugin( + pluginMenuListView.currentItem.pluginModel.modelData); + drawer.close(); + pluginMenu.close(); + } + Keys.onDownPressed: { + pluginMenuListView.incrementCurrentIndex(); + } + Keys.onUpPressed: { + pluginMenuListView.decrementCurrentIndex(); + } } } } @@ -104,6 +116,7 @@ Popup { model: MainWindow.PluginListModel() delegate: ItemDelegate { + property variant pluginModel: model width: parent.width text: modelData highlighted: ListView.isCurrentItem From cce99b57385716a4f9d71f7bbb2306f6ff90956e Mon Sep 17 00:00:00 2001 From: AzulRadio <50132891+AzulRadio@users.noreply.github.com> Date: Fri, 27 May 2022 11:32:26 -0500 Subject: [PATCH 06/31] Shortcut to search and auto highlight search bar (#405) * search bar text highlight * add search bar shortcut "/" Signed-off-by: youhy Co-authored-by: Jenn Nguyen --- include/ignition/gui/qml/Main.qml | 5 +++++ include/ignition/gui/qml/PluginMenu.qml | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/include/ignition/gui/qml/Main.qml b/include/ignition/gui/qml/Main.qml index 67aa99e87..4ce98ff64 100644 --- a/include/ignition/gui/qml/Main.qml +++ b/include/ignition/gui/qml/Main.qml @@ -151,6 +151,11 @@ ApplicationWindow onActivated: close() } + Shortcut { + sequence: "/" + onActivated: pluginMenu.open() + } + /** * Top toolbar */ diff --git a/include/ignition/gui/qml/PluginMenu.qml b/include/ignition/gui/qml/PluginMenu.qml index 732c7fc66..8ab68bdc7 100644 --- a/include/ignition/gui/qml/PluginMenu.qml +++ b/include/ignition/gui/qml/PluginMenu.qml @@ -38,7 +38,11 @@ Popup { Material.color(Material.Grey, Material.Shade200): Material.color(Material.Grey, Material.Shade900); - onOpened: searchField.forceActiveFocus() + + onOpened: { + searchField.forceActiveFocus() + searchField.selectAll() + } ColumnLayout { anchors.fill: parent From 54ecdc6dc9af8fa7c9ae57bc6141a38fb64fdacd Mon Sep 17 00:00:00 2001 From: AzulRadio <50132891+AzulRadio@users.noreply.github.com> Date: Thu, 26 May 2022 10:38:17 -0500 Subject: [PATCH 07/31] Search menu keyboard control (#403) Signed-off-by: youhy --- include/ignition/gui/qml/PluginMenu.qml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/include/ignition/gui/qml/PluginMenu.qml b/include/ignition/gui/qml/PluginMenu.qml index cee476f70..754c681e2 100644 --- a/include/ignition/gui/qml/PluginMenu.qml +++ b/include/ignition/gui/qml/PluginMenu.qml @@ -72,6 +72,18 @@ Popup { onTextEdited: { filteredModel.update(); } + Keys.onReturnPressed: { + MainWindow.OnAddPlugin( + pluginMenuListView.currentItem.pluginModel.modelData); + drawer.close(); + pluginMenu.close(); + } + Keys.onDownPressed: { + pluginMenuListView.incrementCurrentIndex(); + } + Keys.onUpPressed: { + pluginMenuListView.decrementCurrentIndex(); + } } } } @@ -110,6 +122,7 @@ Popup { model: MainWindow.PluginListModel() delegate: ItemDelegate { + property variant pluginModel: model width: parent.width text: modelData highlighted: ListView.isCurrentItem From 5cd426d2ce637ab7504839e8d1d9878488bfa780 Mon Sep 17 00:00:00 2001 From: AzulRadio <50132891+AzulRadio@users.noreply.github.com> Date: Fri, 27 May 2022 11:32:26 -0500 Subject: [PATCH 08/31] Shortcut to search and auto highlight search bar (#405) * search bar text highlight * add search bar shortcut "/" Signed-off-by: youhy Co-authored-by: Jenn Nguyen --- include/ignition/gui/qml/Main.qml | 5 +++++ include/ignition/gui/qml/PluginMenu.qml | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/include/ignition/gui/qml/Main.qml b/include/ignition/gui/qml/Main.qml index 67aa99e87..4ce98ff64 100644 --- a/include/ignition/gui/qml/Main.qml +++ b/include/ignition/gui/qml/Main.qml @@ -151,6 +151,11 @@ ApplicationWindow onActivated: close() } + Shortcut { + sequence: "/" + onActivated: pluginMenu.open() + } + /** * Top toolbar */ diff --git a/include/ignition/gui/qml/PluginMenu.qml b/include/ignition/gui/qml/PluginMenu.qml index 754c681e2..ab1c41e33 100644 --- a/include/ignition/gui/qml/PluginMenu.qml +++ b/include/ignition/gui/qml/PluginMenu.qml @@ -38,7 +38,11 @@ Popup { Material.color(Material.Grey, Material.Shade200): Material.color(Material.Grey, Material.Shade900); - onOpened: searchField.forceActiveFocus() + + onOpened: { + searchField.forceActiveFocus() + searchField.selectAll() + } ColumnLayout { anchors.fill: parent From e3cf22583d7e1640398595e213dfbb5d96f5c19e Mon Sep 17 00:00:00 2001 From: Jorge Perez Date: Tue, 14 Jun 2022 14:10:50 -0400 Subject: [PATCH 09/31] Disable failing test on Citadel (#416) Signed-off-by: Jorge Perez --- src/ign_TEST.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ign_TEST.cc b/src/ign_TEST.cc index cf3ba5ea7..eed3196cf 100644 --- a/src/ign_TEST.cc +++ b/src/ign_TEST.cc @@ -82,7 +82,8 @@ class CmdLine : public ::testing::Test }; // See https://github.com/ignitionrobotics/ign-gui/issues/75 -TEST_F(CmdLine, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(list)) +// See https://github.com/gazebosim/gz-gui/issues/415 +TEST_F(CmdLine, DETAIL_IGN_UTILS_ADD_DISABLED_PREFIX(list)) { // Clear home if it exists common::removeAll(this->kFakeHome); From c7bfb8acf88ce6f0e46aa5bb4d214e7184e41aea Mon Sep 17 00:00:00 2001 From: Mabel Zhang Date: Tue, 14 Jun 2022 19:05:52 -0400 Subject: [PATCH 10/31] Bash completion for flags (#392) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mabel Zhang Signed-off-by: Louise Poubel Co-authored-by: Alejandro Hernández Cordero Co-authored-by: Louise Poubel --- CMakeLists.txt | 5 +++- src/CMakeLists.txt | 28 ++++++++++++++++++ src/cmd/CMakeLists.txt | 12 +++++++- src/cmd/gui.bash_completion.sh | 54 ++++++++++++++++++++++++++++++++++ src/ign_TEST.cc | 34 +++++++++++++++++++++ 5 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 src/cmd/gui.bash_completion.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index 9c76b84f8..046d1bc55 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,8 +68,11 @@ ign_find_package(ignition-msgs5 REQUIRED) set(IGN_MSGS_VER ${ignition-msgs5_VERSION_MAJOR}) #-------------------------------------- -# Find if ign command is available +# Find if command is available. This is used to enable tests. +# Note that CLI files are installed regardless of whether the dependency is +# available during build time find_program(HAVE_IGN_TOOLS ign) +set (IGN_TOOLS_VER 1) #-------------------------------------- # Find QT diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 61f278399..2d1176c6e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -46,7 +46,35 @@ ign_build_tests(TYPE UNIT LIB_DEPS ${IGNITION-MATH_LIBRARIES} TINYXML2::TINYXML2 + TEST_LIST + gtest_targets ) +foreach(test ${gtest_targets}) + target_compile_definitions(${test} PRIVATE + "PROJECT_SOURCE_DIR=\"${PROJECT_SOURCE_DIR}\"") +endforeach() + +if(TARGET UNIT_ign_TEST) + # Running `ign gazebo` on macOS has problems when run with /usr/bin/ruby + # due to System Integrity Protection (SIP). Try to find ruby from + # homebrew as a workaround. + if (APPLE) + find_program(BREW_RUBY ruby HINTS /usr/local/opt/ruby/bin) + endif() + + target_compile_definitions(UNIT_ign_TEST PRIVATE + "BREW_RUBY=\"${BREW_RUBY} \"") + + target_compile_definitions(UNIT_ign_TEST PRIVATE + "IGN_PATH=\"${HAVE_IGN_TOOLS}\"") + + set(_env_vars) + list(APPEND _env_vars "IGN_CONFIG_PATH=${CMAKE_BINARY_DIR}/test/conf") + + set_tests_properties(UNIT_ign_TEST PROPERTIES + ENVIRONMENT "${_env_vars}") +endif() + add_subdirectory(cmd) add_subdirectory(plugins) diff --git a/src/cmd/CMakeLists.txt b/src/cmd/CMakeLists.txt index 45245a12e..25e6c9d58 100644 --- a/src/cmd/CMakeLists.txt +++ b/src/cmd/CMakeLists.txt @@ -1,4 +1,4 @@ -# Generate a the ruby script. +# Generate the ruby script. # Note that the major version of the library is included in the name. if (APPLE) set(IGN_LIBRARY_NAME lib${PROJECT_NAME_LOWER}.dylib) @@ -11,3 +11,13 @@ configure_file( # 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) + +# Tack version onto and install the bash completion script +configure_file( + "gui.bash_completion.sh" + "${CMAKE_CURRENT_BINARY_DIR}/gui${PROJECT_VERSION_MAJOR}.bash_completion.sh" @ONLY) +install( + FILES + ${CMAKE_CURRENT_BINARY_DIR}/gui${PROJECT_VERSION_MAJOR}.bash_completion.sh + DESTINATION + ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/gz/gz${IGN_TOOLS_VER}.completion.d) diff --git a/src/cmd/gui.bash_completion.sh b/src/cmd/gui.bash_completion.sh new file mode 100644 index 000000000..69b68b24d --- /dev/null +++ b/src/cmd/gui.bash_completion.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# +# 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. +# + +# bash tab-completion + +# This is a per-library function definition, used in conjunction with the +# top-level entry point in ign-tools. + +GZ_GUI_COMPLETION_LIST=" + -l --list + -s --standalone + -c --config + -v --verbose + -h --help + --force-version + --versions +" + +function _gz_gui +{ + if [[ ${COMP_WORDS[COMP_CWORD]} == -* ]]; then + # Specify options (-*) word list for this subcommand + COMPREPLY=($(compgen -W "$GZ_GUI_COMPLETION_LIST" \ + -- "${COMP_WORDS[COMP_CWORD]}" )) + return + else + # Just use bash default auto-complete, because we never have two + # subcommands in the same line. If that is ever needed, change here to + # detect subsequent subcommands + COMPREPLY=($(compgen -o default -- "${COMP_WORDS[COMP_CWORD]}")) + return + fi +} + +function _gz_gui_flags +{ + for word in $GZ_GUI_COMPLETION_LIST; do + echo "$word" + done +} diff --git a/src/ign_TEST.cc b/src/ign_TEST.cc index eed3196cf..5954542e2 100644 --- a/src/ign_TEST.cc +++ b/src/ign_TEST.cc @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -32,6 +33,9 @@ # define pclose _pclose #endif +static const std::string kIgnCommand( + std::string(BREW_RUBY) + std::string(IGN_PATH)); + ///////////////////////////////////////////////// std::string custom_exec_str(std::string _cmd) { @@ -97,3 +101,33 @@ TEST_F(CmdLine, DETAIL_IGN_UTILS_ADD_DISABLED_PREFIX(list)) EXPECT_TRUE(common::exists(common::joinPaths(this->kFakeHome, ".ignition", "gui"))); } + +////////////////////////////////////////////////// +/// \brief Check --help message and bash completion script for consistent flags +TEST(ignTest, GuiHelpVsCompletionFlags) +{ + // Flags in help message + std::string helpOutput = custom_exec_str(kIgnCommand + " gui --help"); + + // Call the output function in the bash completion script + std::string scriptPath = common::joinPaths(std::string(PROJECT_SOURCE_DIR), + "src", "cmd", "gui.bash_completion.sh"); + + // Equivalent to: + // sh -c "bash -c \". /path/to/gui.bash_completion.sh; _gz_gui_flags\"" + std::string cmd = "bash -c \". " + scriptPath + "; _gz_gui_flags\""; + std::string scriptOutput = custom_exec_str(cmd); + + // Tokenize script output + std::istringstream iss(scriptOutput); + std::vector flags((std::istream_iterator(iss)), + std::istream_iterator()); + + EXPECT_GT(flags.size(), 0u); + + // Match each flag in script output with help message + for (std::string flag : flags) + { + EXPECT_NE(std::string::npos, helpOutput.find(flag)) << helpOutput; + } +} From 6c83500e0852cc88ddc6223ecae013f9a2a1e898 Mon Sep 17 00:00:00 2001 From: Jorge Perez Date: Thu, 16 Jun 2022 13:22:16 -0400 Subject: [PATCH 11/31] Make display tests more robust (#419) Signed-off-by: Jorge Perez --- .github/ci/after_make.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ci/after_make.sh b/.github/ci/after_make.sh index e2801d7a5..378cb9518 100644 --- a/.github/ci/after_make.sh +++ b/.github/ci/after_make.sh @@ -10,7 +10,7 @@ export IGN_CONFIG_PATH=/usr/local/share/ignition export LD_LIBRARY_PATH=/usr/local/lib/:$LD_LIBRARY_PATH # For rendering / window tests -Xvfb :1 -screen 0 1280x1024x24 & +Xvfb :1 -ac -noreset -core -screen 0 1280x1024x24 & export DISPLAY=:1.0 export RENDER_ENGINE_VALUES=ogre2 export MESA_GL_VERSION_OVERRIDE=3.3 From 9b27bec4aefc9901611ff597c71c7c654b735013 Mon Sep 17 00:00:00 2001 From: Louise Poubel Date: Fri, 17 Jun 2022 10:54:39 -0700 Subject: [PATCH 12/31] Fix ign_TEST (#420) Signed-off-by: Louise Poubel --- src/ign_TEST.cc | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ign_TEST.cc b/src/ign_TEST.cc index 5954542e2..f1e03ee82 100644 --- a/src/ign_TEST.cc +++ b/src/ign_TEST.cc @@ -86,13 +86,13 @@ class CmdLine : public ::testing::Test }; // See https://github.com/ignitionrobotics/ign-gui/issues/75 -// See https://github.com/gazebosim/gz-gui/issues/415 -TEST_F(CmdLine, DETAIL_IGN_UTILS_ADD_DISABLED_PREFIX(list)) +TEST_F(CmdLine, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(list)) { // Clear home if it exists common::removeAll(this->kFakeHome); - EXPECT_FALSE(common::exists(this->kFakeHome)); + // This line is flaky, see https://github.com/gazebosim/gz-gui/issues/415 + // EXPECT_FALSE(common::exists(this->kFakeHome)); std::string output = custom_exec_str("ign gui -l"); EXPECT_NE(output.find("TopicEcho"), std::string::npos) << output; @@ -104,7 +104,8 @@ TEST_F(CmdLine, DETAIL_IGN_UTILS_ADD_DISABLED_PREFIX(list)) ////////////////////////////////////////////////// /// \brief Check --help message and bash completion script for consistent flags -TEST(ignTest, GuiHelpVsCompletionFlags) +// See https://github.com/gazebo-tooling/release-tools/issues/398 +TEST(ignTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(GuiHelpVsCompletionFlags)) { // Flags in help message std::string helpOutput = custom_exec_str(kIgnCommand + " gui --help"); @@ -126,7 +127,7 @@ TEST(ignTest, GuiHelpVsCompletionFlags) EXPECT_GT(flags.size(), 0u); // Match each flag in script output with help message - for (std::string flag : flags) + for (const auto &flag : flags) { EXPECT_NE(std::string::npos, helpOutput.find(flag)) << helpOutput; } From f29fe15fefe49fd8faa1773bcefaf0288a1a8bff Mon Sep 17 00:00:00 2001 From: AzulRadio <50132891+AzulRadio@users.noreply.github.com> Date: Tue, 28 Jun 2022 14:03:33 -0500 Subject: [PATCH 13/31] Common widget GzColor (#410) * common widget GzColor * implement Grid3D with the common widget Signed-off-by: youhy Co-authored-by: Jenn Nguyen --- include/ignition/gui/qml/GzColor.qml | 74 +++++++++++++++++++ include/ignition/gui/resources.qrc | 2 + src/plugins/grid_3d/Grid3D.qml | 106 ++++----------------------- 3 files changed, 89 insertions(+), 93 deletions(-) create mode 100644 include/ignition/gui/qml/GzColor.qml diff --git a/include/ignition/gui/qml/GzColor.qml b/include/ignition/gui/qml/GzColor.qml new file mode 100644 index 000000000..bcf455379 --- /dev/null +++ b/include/ignition/gui/qml/GzColor.qml @@ -0,0 +1,74 @@ +/* + * 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. + * +*/ +import QtQuick 2.9 +import QtQuick.Controls 2.1 +import QtQuick.Dialogs 1.0 +import QtQuick.Layouts 1.3 +import QtQuick.Controls.Material 2.1 + + +// RGBA using range 0 - 1.0 +Item { + id: gzColorRoot + + implicitWidth: 40 + implicitHeight: 40 + + property double r: 1.0 + property double g: 0.0 + property double b: 0.0 + property double a: 1.0 + + signal gzColorSet() + + Button { + id: gzColorButton + Layout.leftMargin: 5 + ToolTip.text: "Open color dialog" + ToolTip.visible: hovered + ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval + background: Rectangle { + implicitWidth: 40 + implicitHeight: 40 + radius: 5 + border.color: (Material.theme === Material.Light) ? Qt.rgba(0,0,0,1) : Qt.rgba(1,1,1,1) + border.width: 2 + color: Qt.rgba(r,g,b,a) + } + onClicked: gzColorDialog.open() + } + + ColorDialog { + id: gzColorDialog + title: "Choose a color" + visible: false + showAlphaChannel: true + modality: Qt.ApplicationModal + onAccepted: { + r = gzColorDialog.color.r + g = gzColorDialog.color.g + b = gzColorDialog.color.b + a = gzColorDialog.color.a + gzColorRoot.gzColorSet() + gzColorDialog.close() + } + onRejected: { + gzColorDialog.close() + } + } +} + diff --git a/include/ignition/gui/resources.qrc b/include/ignition/gui/resources.qrc index 1a7e9d551..df7f2ced7 100644 --- a/include/ignition/gui/resources.qrc +++ b/include/ignition/gui/resources.qrc @@ -2,6 +2,7 @@ qtquickcontrols2.conf + qml/GzColor.qml qml/IgnCard.qml qml/IgnCardSettings.qml qml/IgnHelpers.qml @@ -25,6 +26,7 @@ qml/qmldir + qml/GzColor.qml qml/IgnSnackBar.qml qml/IgnSpinBox.qml diff --git a/src/plugins/grid_3d/Grid3D.qml b/src/plugins/grid_3d/Grid3D.qml index bbea66d0e..c9dc46872 100644 --- a/src/plugins/grid_3d/Grid3D.qml +++ b/src/plugins/grid_3d/Grid3D.qml @@ -52,10 +52,10 @@ GridLayout { roll.value = _rot.x; pitch.value = _rot.y; yaw.value = _rot.z; - r.value = _color.r; - g.value = _color.g; - b.value = _color.b; - a.value = _color.a; + gzColorGrid.r = _color.r; + gzColorGrid.g = _color.g; + gzColorGrid.b = _color.b; + gzColorGrid.a = _color.a; } } @@ -286,98 +286,18 @@ GridLayout { } Text { - text: "R" - color: "dimgrey" - } - - IgnSpinBox { - Layout.fillWidth: true - id: r - maximumValue: 1.00 - minimumValue: 0.00 - value: 0.7 - stepSize: 0.01 - decimals: getDecimals(r.width) - onEditingFinished: Grid3D.SetColor(r.value, g.value, b.value, a.value) - } - - Text { - text: "G" - color: "dimgrey" - } - - IgnSpinBox { - Layout.fillWidth: true - id: g - maximumValue: 1.00 - minimumValue: 0.00 - value: 0.7 - stepSize: 0.01 - decimals: getDecimals(g.width) - onEditingFinished: Grid3D.SetColor(r.value, g.value, b.value, a.value) - } - - Text { - text: "B" - color: "dimgrey" - } - - IgnSpinBox { - Layout.fillWidth: true - id: b - maximumValue: 1.00 - minimumValue: 0.00 - value: 0.7 - stepSize: 0.01 - decimals: getDecimals(b.width) - onEditingFinished: Grid3D.SetColor(r.value, g.value, b.value, a.value) - } - - Text { - text: "A" + Layout.columnSpan: 2 color: "dimgrey" + text: "Grid Color" } - IgnSpinBox { - Layout.fillWidth: true - id: a - maximumValue: 1.00 - minimumValue: 0.00 - value: 1.0 - stepSize: 0.01 - decimals: getDecimals(a.width) - onEditingFinished: Grid3D.SetColor(r.value, g.value, b.value, a.value) - } - - Button { - Layout.alignment: Qt.AlignHCenter - Layout.columnSpan: 4 - id: color - text: qsTr("Custom Color") - onClicked: colorDialog.open() - - ColorDialog { - id: colorDialog - title: "Choose a grid color" - visible: false - onAccepted: { - r.value = colorDialog.color.r - g.value = colorDialog.color.g - b.value = colorDialog.color.b - a.value = colorDialog.color.a - Grid3D.SetColor(colorDialog.color.r, colorDialog.color.g, colorDialog.color.b, colorDialog.color.a) - colorDialog.close() - } - onRejected: { - colorDialog.close() - } - } - } - - // Bottom spacer - Item { - Layout.columnSpan: 4 - Layout.fillHeight: true + GzColor { + id: gzColorGrid + Layout.columnSpan: 2 + Layout.alignment: Qt.AlignRight + Layout.bottomMargin: 5 + Layout.rightMargin: 20 + onGzColorSet: Grid3D.SetColor(gzColorGrid.r, gzColorGrid.g, gzColorGrid.b, gzColorGrid.a) } } From 27afc95889c84516e7da21193783929e68bc8e9b Mon Sep 17 00:00:00 2001 From: Louise Poubel Date: Wed, 29 Jun 2022 22:25:11 -0700 Subject: [PATCH 14/31] Example running a dialog before the main window (#407) * Example running a dialog before the main window Signed-off-by: Louise Poubel * Revert FIXMEs Signed-off-by: Louise Poubel --- .../standalone/start_dialog/CMakeLists.txt | 25 ++++++ examples/standalone/start_dialog/README.md | 16 ++++ .../standalone/start_dialog/start_dialog.cc | 90 +++++++++++++++++++ .../standalone/start_dialog/start_dialog.qml | 26 ++++++ .../standalone/start_dialog/start_dialog.qrc | 5 ++ include/ignition/gui/Application.hh | 8 +- src/Application.cc | 6 ++ src/Application_TEST.cc | 13 +++ 8 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 examples/standalone/start_dialog/CMakeLists.txt create mode 100644 examples/standalone/start_dialog/README.md create mode 100644 examples/standalone/start_dialog/start_dialog.cc create mode 100644 examples/standalone/start_dialog/start_dialog.qml create mode 100644 examples/standalone/start_dialog/start_dialog.qrc diff --git a/examples/standalone/start_dialog/CMakeLists.txt b/examples/standalone/start_dialog/CMakeLists.txt new file mode 100644 index 000000000..7207a0bfe --- /dev/null +++ b/examples/standalone/start_dialog/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.10.2 FATAL_ERROR) + +project(gz-gui-start-dialog) + +if(POLICY CMP0100) + cmake_policy(SET CMP0100 NEW) +endif() + +set(CMAKE_AUTOMOC ON) + +find_package(ignition-gui3 REQUIRED) +set(GZ_GUI_VER ${ignition-gui3_VERSION_MAJOR}) + +set(EXEC_NAME "start_dialog") + +QT5_ADD_RESOURCES(resources_RCC ${EXEC_NAME}.qrc) + +add_executable(${EXEC_NAME} + ${EXEC_NAME}.cc + ${resources_RCC} +) +target_link_libraries(${EXEC_NAME} + ignition-gui${GZ_GUI_VER}::ignition-gui${GZ_GUI_VER} +) + diff --git a/examples/standalone/start_dialog/README.md b/examples/standalone/start_dialog/README.md new file mode 100644 index 000000000..4ab7c686c --- /dev/null +++ b/examples/standalone/start_dialog/README.md @@ -0,0 +1,16 @@ +Example for how to run a start dialog before the main window. + +## Build + + cd + mkdir build + cd build + cmake .. + make + +## Run + + cd /build + ./start_dialog + +First the dialog shows up, and after that's closed, the main window shows up. diff --git a/examples/standalone/start_dialog/start_dialog.cc b/examples/standalone/start_dialog/start_dialog.cc new file mode 100644 index 000000000..3ef1b7d43 --- /dev/null +++ b/examples/standalone/start_dialog/start_dialog.cc @@ -0,0 +1,90 @@ +/* + * 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 + +#include +#include +#include +#include + +////////////////////////////////////////////////// +int main(int _argc, char **_argv) +{ + // Increase verboosity so we see all messages + ignition::common::Console::SetVerbosity(4); + + // Create app + ignition::gui::Application app(_argc, _argv, ignition::gui::WindowType::kDialog); + + igndbg << "Open dialog" << std::endl; + + // Add and display a dialog + auto dialog = new ignition::gui::Dialog(); + dialog->QuickWindow(); + + std::string qmlFile(":start_dialog/start_dialog.qml"); + if (!QFile(QString::fromStdString(qmlFile)).exists()) + { + ignerr << "Can't find [" << qmlFile + << "]. Are you sure it was added to the .qrc file?" << std::endl; + return -1; + } + + QQmlComponent dialogComponent(ignition::gui::App()->Engine(), + QString(QString::fromStdString(qmlFile))); + if (dialogComponent.isError()) + { + std::stringstream errors; + errors << "Failed to instantiate QML file [" << qmlFile << "]." + << std::endl; + for (auto error : dialogComponent.errors()) + { + errors << "* " << error.toString().toStdString() << std::endl; + } + ignerr << errors.str(); + return -1; + } + + auto dialogItem = qobject_cast(dialogComponent.create()); + if (!dialogItem) + { + ignerr << "Failed to instantiate QML file [" << qmlFile << "]." << std::endl + << "Are you sure the file is valid QML? " + << "You can check with the `qmlscene` tool" << std::endl; + return -1; + } + + dialogItem->setParentItem(dialog->RootItem()); + + // Execute start dialog + app.exec(); + + // After dialog is shut, display the main window + igndbg << "Dialog closed, open main window" << std::endl; + + // Create main window + app.CreateMainWindow(); + + // Run main window + app.exec(); + + igndbg << "Main window closed" << std::endl; + + return 0; +} + diff --git a/examples/standalone/start_dialog/start_dialog.qml b/examples/standalone/start_dialog/start_dialog.qml new file mode 100644 index 000000000..a07f4ff52 --- /dev/null +++ b/examples/standalone/start_dialog/start_dialog.qml @@ -0,0 +1,26 @@ +/* + * 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. + * +*/ +import QtQuick 2.0 +import QtQuick.Controls 2.0 +Rectangle { + color: "green" + anchors.fill: parent + Text { + text: qsTr("Start\ndialog!") + font.pointSize: 30 + } +} diff --git a/examples/standalone/start_dialog/start_dialog.qrc b/examples/standalone/start_dialog/start_dialog.qrc new file mode 100644 index 000000000..578d9c3c2 --- /dev/null +++ b/examples/standalone/start_dialog/start_dialog.qrc @@ -0,0 +1,5 @@ + + + start_dialog.qml + + diff --git a/include/ignition/gui/Application.hh b/include/ignition/gui/Application.hh index b0813a8fc..45639a8e9 100644 --- a/include/ignition/gui/Application.hh +++ b/include/ignition/gui/Application.hh @@ -53,7 +53,8 @@ namespace ignition /// plugins kMainWindow = 0, - /// \brief One independent dialog per plugin + /// \brief One independent dialog per plugin. Also useful to open a + /// startup dialog before the main window. kDialog = 1 }; @@ -169,6 +170,11 @@ namespace ignition /// \brief Callback when user requests to close a plugin public slots: void OnPluginClose(); + /// \brief Create a main window. Just calls InitializeMainWindow. + /// \return True if successful + /// \sa InitializeMainWindow + public: bool CreateMainWindow(); + /// \brief Create a main window, populate with previously loaded plugins /// and apply previously loaded configuration. /// An empty window will be created if no plugins have been loaded. diff --git a/src/Application.cc b/src/Application.cc index 38facd1c4..39f063cb1 100644 --- a/src/Application.cc +++ b/src/Application.cc @@ -537,6 +537,12 @@ std::shared_ptr Application::PluginByName( return nullptr; } +///////////////////////////////////////////////// +bool Application::CreateMainWindow() +{ + return this->InitializeMainWindow(); +} + ///////////////////////////////////////////////// bool Application::InitializeMainWindow() { diff --git a/src/Application_TEST.cc b/src/Application_TEST.cc index 9ca02cc90..0ebc5c187 100644 --- a/src/Application_TEST.cc +++ b/src/Application_TEST.cc @@ -289,6 +289,19 @@ TEST(ApplicationTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(InitializeMainWindow)) // Show window app.exec(); } + + // Start in dialog mode and create main window later + { + Application app(g_argc, g_argv, WindowType::kDialog); + + auto win = App()->findChild(); + EXPECT_EQ(nullptr, win); + + app.CreateMainWindow(); + + win = App()->findChild(); + ASSERT_NE(nullptr, win); + } } ////////////////////////////////////////////////// From cc557e9ff69e4118575ebf20b6d7d50e879e91b8 Mon Sep 17 00:00:00 2001 From: AzulRadio <50132891+AzulRadio@users.noreply.github.com> Date: Fri, 1 Jul 2022 18:02:55 -0500 Subject: [PATCH 15/31] Add common widget for pose (#424) * Add common widget pose GUI Signed-off-by: youhy Co-authored-by: Jenn Nguyen --- include/ignition/gui/qml/GzPose.qml | 291 ++++++++++++++++++++++++ include/ignition/gui/qml/IgnHelpers.qml | 17 ++ include/ignition/gui/resources.qrc | 2 + 3 files changed, 310 insertions(+) create mode 100644 include/ignition/gui/qml/GzPose.qml diff --git a/include/ignition/gui/qml/GzPose.qml b/include/ignition/gui/qml/GzPose.qml new file mode 100644 index 000000000..3cc05efa3 --- /dev/null +++ b/include/ignition/gui/qml/GzPose.qml @@ -0,0 +1,291 @@ +/* + * 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. + * +*/ +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.3 +import QtQuick.Controls.Styles 1.4 + +/** + * Item displaying 3D pose information. + * + * Users should load values to xValues, yValues, etc. + * If readOnly == False, + * users can read from signal pararmeters of gzPoseSet: _x, _y, etc. + * + * Usage example: + * GzPose { + * id: gzPose + * readOnly: false + * xValue: xValueFromCPP + * yValue: yValueFromCPP + * zValue: zValueFromCPP + * rollValue: rollValueFromCPP + * pitchValue: pitchValueFromCPP + * yawValue: yawValueFromCPP + * onGzPoseSet: { + * myFunc(_x, _y, _z, _roll, _pitch, _yaw) + * } + * } +**/ + +Item { + id: gzPoseRoot + + // Read-only / write + property bool readOnly: false + + // User input value. + property double xValue + property double yValue + property double zValue + property double rollValue + property double pitchValue + property double yawValue + + /** + * Used to read spinbox values + * @params: _x, _y, _z, _roll, _pitch, _yaw: corresponding spinBoxes values + * @note: When readOnly == false, user should read spinbox value from its + * parameters. + * When readOnly == true, this signal is unused. + */ + signal gzPoseSet(double _x, double _y, double _z, double _roll, double _pitch, double _yaw) + + + /*** The following are private variables: ***/ + // Show Pose bar (used to control expand) + property bool show: true + + height: gzPoseContent.height + + // Left indentation + property int indentation: 10 + + // Horizontal margins + property int margin: 5 + + // Maximum spinbox value + property double spinMax: 1000000 + + // local variables to store spinbox values + property var xItem: {} + property var yItem: {} + property var zItem: {} + property var rollItem: {} + property var pitchItem: {} + property var yawItem: {} + + // Dummy component to use its functions. + IgnHelpers { + id: gzHelper + } + /*** Private variables end: ***/ + + /** + * Used to create a spin box + */ + Component { + id: writableNumber + IgnSpinBox { + id: writableSpin + value: numberValue + minimumValue: -spinMax + maximumValue: spinMax + decimals: gzHelper.getDecimals(writableSpin.width) + onEditingFinished: { + gzPoseRoot.gzPoseSet(xItem.value, yItem.value, zItem.value, rollItem.value, pitchItem.value, yawItem.value) + } + } + } + + /** + * Used to create a read-only number + */ + Component { + id: readOnlyNumber + Text { + id: numberText + anchors.fill: parent + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + text: { + var decimals = gzHelper.getDecimals(numberText.width) + return numberValue.toFixed(decimals) + } + } + } + + Rectangle { + id: gzPoseContent + width: parent.width + height: show ? gzPoseGrid.height : 0 + clip: true + color: "transparent" + + Behavior on height { + NumberAnimation { + duration: 200; + easing.type: Easing.InOutQuad + } + } + + GridLayout { + id: gzPoseGrid + width: parent.width + columns: 6 + + // Left spacer + Item { + Layout.rowSpan: 3 + width: margin + indentation + } + + Text { + text: 'X (m)' + leftPadding: 5 + color: Material.theme == Material.Light ? "#444444" : "#bbbbbb" + font.pointSize: 12 + } + + Item { + Layout.fillWidth: true + height: 40 + Loader { + id: xLoader + anchors.fill: parent + property double numberValue: xValue + sourceComponent: readOnly ? readOnlyNumber : writableNumber + onLoaded: { + xItem = xLoader.item + } + } + } + + Text { + text: 'Roll (rad)' + leftPadding: 5 + color: Material.theme == Material.Light ? "#444444" : "#bbbbbb" + font.pointSize: 12 + } + + Item { + Layout.fillWidth: true + height: 40 + Loader { + id: rollLoader + anchors.fill: parent + property double numberValue: rollValue + sourceComponent: readOnly ? readOnlyNumber : writableNumber + onLoaded: { + rollItem = rollLoader.item + } + } + } + + // Right spacer + Item { + Layout.rowSpan: 3 + width: margin + } + + Text { + text: 'Y (m)' + leftPadding: 5 + color: Material.theme == Material.Light ? "#444444" : "#bbbbbb" + font.pointSize: 12 + } + + Item { + Layout.fillWidth: true + height: 40 + Loader { + id: yLoader + anchors.fill: parent + property double numberValue: yValue + sourceComponent: readOnly ? readOnlyNumber : writableNumber + onLoaded: { + yItem = yLoader.item + } + } + } + + Text { + text: 'Pitch (rad)' + leftPadding: 5 + color: Material.theme == Material.Light ? "#444444" : "#bbbbbb" + font.pointSize: 12 + } + + Item { + Layout.fillWidth: true + height: 40 + Loader { + id: pitchLoader + anchors.fill: parent + property double numberValue: pitchValue + sourceComponent: readOnly ? readOnlyNumber : writableNumber + onLoaded: { + pitchItem = pitchLoader.item + } + } + } + + Text { + text: 'Z (m)' + leftPadding: 5 + color: Material.theme == Material.Light ? "#444444" : "#bbbbbb" + font.pointSize: 12 + } + + Item { + Layout.fillWidth: true + height: 40 + Loader { + id: zLoader + anchors.fill: parent + property double numberValue: zValue + sourceComponent: readOnly ? readOnlyNumber : writableNumber + onLoaded: { + zItem = zLoader.item + } + } + } + + Text { + text: 'Yaw (rad)' + leftPadding: 5 + color: Material.theme == Material.Light ? "#444444" : "#bbbbbb" + font.pointSize: 12 + } + + Item { + Layout.fillWidth: true + height: 40 + Loader { + id: yawLoader + anchors.fill: parent + property double numberValue: yawValue + sourceComponent: readOnly ? readOnlyNumber : writableNumber + onLoaded: { + yawItem = yawLoader.item + } + } + } + } // end of GridLayout + } // end of Rectangle (gzPoseContent) +} // end of Rectangle (gzPoseRoot) diff --git a/include/ignition/gui/qml/IgnHelpers.qml b/include/ignition/gui/qml/IgnHelpers.qml index 57c0619b3..9938872f8 100644 --- a/include/ignition/gui/qml/IgnHelpers.qml +++ b/include/ignition/gui/qml/IgnHelpers.qml @@ -39,4 +39,21 @@ Item { return result; } + + /** + * Helper function to get number of decimal digits based on a width value. + * @param _width Pixel width. + * @returns Number of decimals that fit with the provided width. + */ + function getDecimals(_width) { + // Use full decimals if the width is <= 0, which allows the value + // to appear correctly. + if (_width <= 0 || _width > 110) + return 6 + + if (_width <= 80) + return 2 + + return 4 + } } diff --git a/include/ignition/gui/resources.qrc b/include/ignition/gui/resources.qrc index df7f2ced7..2a01f294a 100644 --- a/include/ignition/gui/resources.qrc +++ b/include/ignition/gui/resources.qrc @@ -3,6 +3,7 @@ qtquickcontrols2.conf qml/GzColor.qml + qml/GzPose.qml qml/IgnCard.qml qml/IgnCardSettings.qml qml/IgnHelpers.qml @@ -27,6 +28,7 @@ qml/qmldir qml/GzColor.qml + qml/GzPose.qml qml/IgnSnackBar.qml qml/IgnSpinBox.qml From bc53940651195f977b35d55aaa628c783b4b23ba Mon Sep 17 00:00:00 2001 From: AzulRadio <50132891+AzulRadio@users.noreply.github.com> Date: Thu, 7 Jul 2022 13:25:18 -0500 Subject: [PATCH 16/31] Fix common widget Pose (#431) * common widget variables fix * remove spacer * change show to expand Signed-off-by: youhy --- include/ignition/gui/qml/GzPose.qml | 38 ++++++++--------------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/include/ignition/gui/qml/GzPose.qml b/include/ignition/gui/qml/GzPose.qml index 3cc05efa3..d863e6bfc 100644 --- a/include/ignition/gui/qml/GzPose.qml +++ b/include/ignition/gui/qml/GzPose.qml @@ -23,9 +23,9 @@ import QtQuick.Controls.Styles 1.4 /** * Item displaying 3D pose information. * - * Users should load values to xValues, yValues, etc. + * Users can set values to xValue, yValue, etc. * If readOnly == False, - * users can read from signal pararmeters of gzPoseSet: _x, _y, etc. + * users can read from signal parameters of gzPoseSet: _x, _y, etc. * * Usage example: * GzPose { @@ -66,21 +66,15 @@ Item { */ signal gzPoseSet(double _x, double _y, double _z, double _roll, double _pitch, double _yaw) + // Maximum spinbox value + property double spinMax: Number.MAX_VALUE - /*** The following are private variables: ***/ - // Show Pose bar (used to control expand) - property bool show: true - - height: gzPoseContent.height - - // Left indentation - property int indentation: 10 + // Expand/Collapse of this widget + property bool expand: true - // Horizontal margins - property int margin: 5 - // Maximum spinbox value - property double spinMax: 1000000 + /*** The following are private variables: ***/ + height: gzPoseContent.height // local variables to store spinbox values property var xItem: {} @@ -133,7 +127,7 @@ Item { Rectangle { id: gzPoseContent width: parent.width - height: show ? gzPoseGrid.height : 0 + height: expand ? gzPoseGrid.height : 0 clip: true color: "transparent" @@ -147,13 +141,7 @@ Item { GridLayout { id: gzPoseGrid width: parent.width - columns: 6 - - // Left spacer - Item { - Layout.rowSpan: 3 - width: margin + indentation - } + columns: 4 Text { text: 'X (m)' @@ -197,12 +185,6 @@ Item { } } - // Right spacer - Item { - Layout.rowSpan: 3 - width: margin - } - Text { text: 'Y (m)' leftPadding: 5 From f1ff48611907372d7a6b80440246df122ae69447 Mon Sep 17 00:00:00 2001 From: Mohamad Date: Wed, 13 Jul 2022 18:19:06 +0200 Subject: [PATCH 17/31] Allow Dialogs to have a MainWindow independent config (#418) Signed-off-by: Mohamad Signed-off-by: Louise Poubel Co-authored-by: Louise Poubel --- include/ignition/gui/Application.hh | 2 + include/ignition/gui/Dialog.hh | 23 +++++ include/ignition/gui/MainWindow.hh | 1 + src/Application_TEST.cc | 2 +- src/CMakeLists.txt | 1 + src/Dialog.cc | 143 ++++++++++++++++++++++++++++ src/Dialog_TEST.cc | 137 ++++++++++++++++++++++++++ 7 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 src/Dialog_TEST.cc diff --git a/include/ignition/gui/Application.hh b/include/ignition/gui/Application.hh index 45639a8e9..0a104848b 100644 --- a/include/ignition/gui/Application.hh +++ b/include/ignition/gui/Application.hh @@ -97,6 +97,7 @@ namespace ignition /// and plugins. This function doesn't instantiate the plugins, it just /// keeps them in memory and they can be applied later by either /// instantiating a window or several dialogs. + /// and plugins. /// \param[in] _path Full path to configuration file. /// \return True if successful /// \sa InitializeMainWindow @@ -156,6 +157,7 @@ namespace ignition /// \return True if successful public: bool RemovePlugin(const std::string &_pluginName); + /// \brief Get a plugin by its unique name. /// \param[in] _pluginName Plugn instance's unique name. This is the /// plugin card's object name. diff --git a/include/ignition/gui/Dialog.hh b/include/ignition/gui/Dialog.hh index 5da748d42..7271c415b 100644 --- a/include/ignition/gui/Dialog.hh +++ b/include/ignition/gui/Dialog.hh @@ -19,6 +19,7 @@ #define IGNITION_GUI_DIALOG_HH_ #include +#include #include "ignition/gui/qt.h" #include "ignition/gui/Export.hh" @@ -55,6 +56,28 @@ namespace ignition /// \return Pointer to the item public: QQuickItem *RootItem() const; + /// \brief Store dialog default config + /// \param[in] _config XML config as string + public: void SetDefaultConfig(const std::string &_config); + + /// \brief Write dialog config + /// \param[in] _path config path + /// \param[in] _attribute XMLElement attribute name + /// \param[in] _value XMLElement attribute value + /// \return true if written to config file + public: bool UpdateConfigAttribute( + const std::string &_path, const std::string &_attribute, + const bool _value) const; + + /// \brief Gets a config attribute value, if not found in config + /// write the default in the config and get it. + /// creates config file if it doesn't exist. + /// \param[in] _path config path + /// \param[in] _attribute attribute name + /// \return attribute value as string + public: std::string ReadConfigAttribute(const std::string &_path, + const std::string &_attribute) const; + /// \internal /// \brief Private data pointer private: std::unique_ptr dataPtr; diff --git a/include/ignition/gui/MainWindow.hh b/include/ignition/gui/MainWindow.hh index 39ad7619a..9f3cd5e33 100644 --- a/include/ignition/gui/MainWindow.hh +++ b/include/ignition/gui/MainWindow.hh @@ -671,6 +671,7 @@ namespace ignition /// \brief Concatenation of all plugin configurations. std::string plugins{""}; + }; } } diff --git a/src/Application_TEST.cc b/src/Application_TEST.cc index 0ebc5c187..c0ac12728 100644 --- a/src/Application_TEST.cc +++ b/src/Application_TEST.cc @@ -222,7 +222,7 @@ TEST(ApplicationTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(LoadDefaultConfig)) } ////////////////////////////////////////////////// -TEST(ApplicationTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(InitializeMainWindow)) +TEST(ApplicationTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(CreateMainWindow)) { ignition::common::Console::SetVerbosity(4); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2d1176c6e..2c3a3beeb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -16,6 +16,7 @@ set (sources set (gtest_sources Application_TEST Conversions_TEST + Dialog_TEST DragDropModel_TEST Helpers_TEST GuiEvents_TEST diff --git a/src/Dialog.cc b/src/Dialog.cc index 7463a7394..78c45157a 100644 --- a/src/Dialog.cc +++ b/src/Dialog.cc @@ -15,6 +15,8 @@ * */ +#include + #include #include "ignition/gui/Application.hh" #include "ignition/gui/Dialog.hh" @@ -25,6 +27,9 @@ namespace ignition { class DialogPrivate { + /// \brief default dialog config + public: std::string config{""}; + /// \brief Pointer to quick window public: QQuickWindow *quickWindow{nullptr}; }; @@ -75,3 +80,141 @@ QQuickItem *Dialog::RootItem() const return dialogItem; } +///////////////////////////////////////////////// +bool Dialog::UpdateConfigAttribute(const std::string &_path, + const std::string &_attribute, const bool _value) const +{ + if (_path.empty()) + { + ignerr << "Missing config file" << std::endl; + return false; + } + + // Use tinyxml to read config + tinyxml2::XMLDocument doc; + auto success = !doc.LoadFile(_path.c_str()); + if (!success) + { + ignerr << "Failed to load file [" << _path << "]: XMLError" + << std::endl; + return false; + } + + // Update attribute value for the correct dialog + for (auto dialogElem = doc.FirstChildElement("dialog"); + dialogElem != nullptr; + dialogElem = dialogElem->NextSiblingElement("dialog")) + { + if(dialogElem->Attribute("name") == this->objectName().toStdString()) + { + dialogElem->SetAttribute(_attribute.c_str(), _value); + } + } + + // Write config file + tinyxml2::XMLPrinter printer; + doc.Print(&printer); + + std::string config = printer.CStr(); + std::ofstream out(_path.c_str(), std::ios::out); + if (!out) + { + ignerr << "Unable to open file: " << _path + << ".\nCheck file permissions.\n"; + } + else + out << config; + + return true; +} + +///////////////////////////////////////////////// +void Dialog::SetDefaultConfig(const std::string &_config) +{ + this->dataPtr->config = _config; +} + +///////////////////////////////////////////////// +std::string Dialog::ReadConfigAttribute(const std::string &_path, + const std::string &_attribute) const +{ + tinyxml2::XMLDocument doc; + std::string value {""}; + std::string config = "\n\n"; + tinyxml2::XMLPrinter defaultPrinter; + bool configExists{true}; + std::string dialogName = this->objectName().toStdString(); + + auto Value = [&_attribute, &doc, &dialogName]() + { + // Process each dialog + // If multiple attributes share the same name, return the first one + for (auto dialogElem = doc.FirstChildElement("dialog"); + dialogElem != nullptr; + dialogElem = dialogElem->NextSiblingElement("dialog")) + { + if (dialogElem->Attribute("name") == dialogName) + { + if (dialogElem->Attribute(_attribute.c_str())) + return dialogElem->Attribute(_attribute.c_str()); + } + } + return ""; + }; + + // Check if the passed in config file exists. + // (If the default config path doesn't exist yet, it's expected behavior. + // It will be created the first time now.) + if (!common::exists(_path)) + { + configExists = false; + doc.Parse(this->dataPtr->config.c_str()); + value = Value(); + } + else + { + auto success = !doc.LoadFile(_path.c_str()); + if (!success) + { + ignerr << "Failed to load file [" << _path << "]: XMLError" + << std::endl; + return ""; + } + value = Value(); + + // config exists but attribute not there read from default config + if (value.empty()) + { + tinyxml2::XMLDocument missingDoc; + missingDoc.Parse(this->dataPtr->config.c_str()); + value = Value(); + missingDoc.Print(&defaultPrinter); + } + } + + // Write config file + tinyxml2::XMLPrinter printer; + doc.Print(&printer); + + // Don't write the xml version decleration if file exists + if (configExists) + { + config = ""; + } + + igndbg << "Setting dialog " << this->objectName().toStdString() + << " default config." << std::endl; + config += printer.CStr(); + config += defaultPrinter.CStr(); + std::ofstream out(_path.c_str(), std::ios::out); + if (!out) + { + ignerr << "Unable to open file: " << _path + << ".\nCheck file permissions.\n"; + return ""; + } + else + out << config; + + return value; +} diff --git a/src/Dialog_TEST.cc b/src/Dialog_TEST.cc new file mode 100644 index 000000000..c52b46a12 --- /dev/null +++ b/src/Dialog_TEST.cc @@ -0,0 +1,137 @@ +/* + * 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 + +#include +#include + +#include "test_config.h" // NOLINT(build/include) +#include "ignition/gui/Application.hh" +#include "ignition/gui/Dialog.hh" + +std::string kTestConfigFile = "/tmp/ign-gui-test.config"; // NOLINT(*) +int g_argc = 1; +char* g_argv[] = +{ + reinterpret_cast(const_cast("./Dialog_TEST")), +}; + +using namespace ignition; +using namespace gui; +using namespace std::chrono_literals; + +///////////////////////////////////////////////// +TEST(DialogTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(UpdateDialogConfig)) +{ + ignition::common::Console::SetVerbosity(4); + Application app(g_argc, g_argv, ignition::gui::WindowType::kDialog); + + // Change default config path + App()->SetDefaultConfigPath(kTestConfigFile); + + auto dialog = new Dialog; + ASSERT_NE(nullptr, dialog); + + // Read attribute value when the default the config is not set + { + EXPECT_FALSE(common::exists(kTestConfigFile)); + std::string allow = dialog->ReadConfigAttribute(app.DefaultConfigPath(), + "allow"); + EXPECT_EQ(allow, ""); + + // Config file is created when a read is attempted + EXPECT_TRUE(common::exists(kTestConfigFile)); + + // Delete file + std::remove(kTestConfigFile.c_str()); + } + + // Read a non existing attribute + { + EXPECT_FALSE(common::exists(kTestConfigFile)); + dialog->setObjectName("quick_menu"); + dialog->SetDefaultConfig(std::string( + "")); + std::string allow = dialog->ReadConfigAttribute(app.DefaultConfigPath(), + "allow"); + EXPECT_EQ(allow, ""); + + // Config file is created when a read is attempted + EXPECT_TRUE(common::exists(kTestConfigFile)); + + // Delete file + std::remove(kTestConfigFile.c_str()); + } + + // Read an existing attribute + { + EXPECT_FALSE(common::exists(kTestConfigFile)); + std::string show = dialog->ReadConfigAttribute(app.DefaultConfigPath(), + "show"); + EXPECT_EQ(show, "true"); + + // Config file is created when a read is attempted + EXPECT_TRUE(common::exists(kTestConfigFile)); + + // Delete file + std::remove(kTestConfigFile.c_str()); + } + + // Update a non existing attribute + { + EXPECT_FALSE(common::exists(kTestConfigFile)); + + // Call a read to create config file + std::string allow = dialog->ReadConfigAttribute(app.DefaultConfigPath(), + "allow"); + + // Empty string for a non existing attribute + EXPECT_EQ(allow, ""); + dialog->UpdateConfigAttribute(app.DefaultConfigPath(), "allow", true); + allow = dialog->ReadConfigAttribute(app.DefaultConfigPath(), + "allow"); + EXPECT_EQ(allow, "true"); + + // Config file is created when a read is attempted + EXPECT_TRUE(common::exists(kTestConfigFile)); + + // Delete file + std::remove(kTestConfigFile.c_str()); + } + + // Update a existing attribute + { + EXPECT_FALSE(common::exists(kTestConfigFile)); + + // Call a read to create config file + std::string allow = dialog->ReadConfigAttribute(app.DefaultConfigPath(), + "allow"); + dialog->UpdateConfigAttribute(app.DefaultConfigPath(), "allow", false); + allow = dialog->ReadConfigAttribute(app.DefaultConfigPath(), + "allow"); + EXPECT_EQ(allow, "false"); + + // Config file is created when a read is attempted + EXPECT_TRUE(common::exists(kTestConfigFile)); + + // Delete file + std::remove(kTestConfigFile.c_str()); + } + + delete dialog; +} From 110321c22e9509acad69c5e707d63c1a77958c7d Mon Sep 17 00:00:00 2001 From: AzulRadio <50132891+AzulRadio@users.noreply.github.com> Date: Wed, 13 Jul 2022 16:15:43 -0500 Subject: [PATCH 18/31] Add common widget for vector3 (#427) Signed-off-by: youhy Co-authored-by: Louise Poubel Co-authored-by: Jenn Nguyen --- include/ignition/gui/qml/GzVector3.qml | 210 +++++++++++++++++++++++++ include/ignition/gui/resources.qrc | 2 + 2 files changed, 212 insertions(+) create mode 100644 include/ignition/gui/qml/GzVector3.qml diff --git a/include/ignition/gui/qml/GzVector3.qml b/include/ignition/gui/qml/GzVector3.qml new file mode 100644 index 000000000..84b4cfcbb --- /dev/null +++ b/include/ignition/gui/qml/GzVector3.qml @@ -0,0 +1,210 @@ +/* + * 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. + * +*/ +import QtQuick 2.9 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.3 + +/** + * Item displaying a 3D vector + * + * Users can set values to xValue, yValue, and zValue. + * If readOnly == False, + * users can read from signal parameters of GzVectorSet: _x, _y, and _z + * + * Usage example: + * GzVector3 { + * id: gzVector + * xName: "Red" + * yName: "Green" + * zName: "Blue" + * gzUnit: "" + * readOnly: false + * xValue: xValueFromCPP + * yValue: yValueFromCPP + * zValue: zValueFromCPP + * onGzVectorSet: { + * myFunc(_x, _y, _z, _roll, _pitch, _yaw) + * } + * } +**/ +Item { + id: gzVectorRoot + + // Read-only / write + property bool readOnly: true + + // User input value + property double xValue + property double yValue + property double zValue + + /** + * Used to read spinbox values + * @params: _x, _y, _z: corresponding spinBoxes values + * @note: When readOnly == false, user should read spinbox value from its + * parameters. + * When readOnly == true, this signal is unused. + */ + signal gzVectorSet(double _x, double _y, double _z) + + // Names for XYZ + property string xName: "X" + property string yName: "Y" + property string zName: "Z" + + // Units, defaults to meters. + // Set to "" to omit units & the parentheses. + property string gzUnit: "m" + + // Expand/Collapse of this widget + property bool expand: true + + // Maximum spinbox value + property double spinMax: Number.MAX_VALUE + + /*** The following are private variables: ***/ + height: gzVectorContent.height + + // local variables to store spinbox values + property var xItem: {} + property var yItem: {} + property var zItem: {} + + // Dummy component to use its functions. + IgnHelpers { + id: gzHelper + } + /*** Private variables end: ***/ + + /** + * Used to create a spin box + */ + Component { + id: writableNumber + IgnSpinBox { + id: writableSpin + value: numberValue + minimumValue: -spinMax + maximumValue: spinMax + decimals: gzHelper.getDecimals(writableSpin.width) + onEditingFinished: { + gzVectorRoot.gzVectorSet(xItem.value, yItem.value, zItem.value) + } + } + } + + /** + * Used to create a read-only number + */ + Component { + id: readOnlyNumber + Text { + id: numberText + anchors.fill: parent + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + text: { + var decimals = gzHelper.getDecimals(numberText.width) + return numberValue.toFixed(decimals) + } + } + } + + Rectangle { + id: gzVectorContent + width: parent.width + height: expand ? gzVectorGrid.height : 0 + clip: true + color: "transparent" + + Behavior on height { + NumberAnimation { + duration: 200; + easing.type: Easing.InOutQuad + } + } + + GridLayout { + id: gzVectorGrid + width: parent.width + columns: 2 + + Text { + text: gzUnit == "" ? xName : xName + ' (' + gzUnit + ')' + leftPadding: 5 + color: Material.theme == Material.Light ? "#444444" : "#bbbbbb" + font.pointSize: 12 + } + + Item { + Layout.fillWidth: true + height: 40 + Loader { + id: xLoader + anchors.fill: parent + property double numberValue: xValue + sourceComponent: readOnly ? readOnlyNumber : writableNumber + onLoaded: { + xItem = xLoader.item + } + } + } + + Text { + text: gzUnit == "" ? yName : yName + ' (' + gzUnit + ')' + leftPadding: 5 + color: Material.theme == Material.Light ? "#444444" : "#bbbbbb" + font.pointSize: 12 + } + + Item { + Layout.fillWidth: true + height: 40 + Loader { + id: yLoader + anchors.fill: parent + property double numberValue: yValue + sourceComponent: readOnly ? readOnlyNumber : writableNumber + onLoaded: { + yItem = yLoader.item + } + } + } + + Text { + text: gzUnit == "" ? zName : zName + ' (' + gzUnit + ')' + leftPadding: 5 + color: Material.theme == Material.Light ? "#444444" : "#bbbbbb" + font.pointSize: 12 + } + + Item { + Layout.fillWidth: true + height: 40 + Loader { + id: zLoader + anchors.fill: parent + property double numberValue: zValue + sourceComponent: readOnly ? readOnlyNumber : writableNumber + onLoaded: { + zItem = zLoader.item + } + } + } + } + } +} diff --git a/include/ignition/gui/resources.qrc b/include/ignition/gui/resources.qrc index 2a01f294a..d3a8675e8 100644 --- a/include/ignition/gui/resources.qrc +++ b/include/ignition/gui/resources.qrc @@ -4,6 +4,7 @@ qml/GzColor.qml qml/GzPose.qml + qml/GzVector3.qml qml/IgnCard.qml qml/IgnCardSettings.qml qml/IgnHelpers.qml @@ -29,6 +30,7 @@ qml/GzColor.qml qml/GzPose.qml + qml/GzVector3.qml qml/IgnSnackBar.qml qml/IgnSpinBox.qml From f3561920607e8702c808ecd9728fb0c5db265612 Mon Sep 17 00:00:00 2001 From: Jenn Nguyen Date: Wed, 13 Jul 2022 15:11:59 -0700 Subject: [PATCH 19/31] =?UTF-8?q?=F0=9F=8E=88=203.10.0=20(#432)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jenn Nguyen --- CMakeLists.txt | 2 +- Changelog.md | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 046d1bc55..e04d4e602 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.9.0) +project(ignition-gui3 VERSION 3.10.0) #============================================================================ # Find ignition-cmake diff --git a/Changelog.md b/Changelog.md index 066362def..ff790cb63 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,63 @@ ## Ignition Gui 3 +### Ignition Gui 3.10.0 (2022-07-13) + +1. Add common widget for vector3 + * [Pull request #427](https://github.com/gazebosim/gz-gui/pull/427) + +1. Allow Dialogs to have a MainWindow independent config + * [Pull request #418](https://github.com/gazebosim/gz-gui/pull/418) + +1. Add common widget for pose + * [Pull request #424](https://github.com/gazebosim/gz-gui/pull/424) + * [Pull request #431](https://github.com/gazebosim/gz-gui/pull/431) + +1. Example running a dialog before the main window + * [Pull request #407](https://github.com/gazebosim/gz-gui/pull/407) + +1. Common widget GzColor + * [Pull request #410](https://github.com/gazebosim/gz-gui/pull/410) + +1. Fix ign_TEST + * [Pull request #420](https://github.com/gazebosim/gz-gui/pull/420) + +1. Make display tests more robust + * [Pull request #419](https://github.com/gazebosim/gz-gui/pull/419) + +1. Bash completion for flags + * [Pull request #392](https://github.com/gazebosim/gz-gui/pull/392) + +1. Disable failing test on Citadel + * [Pull request #416](https://github.com/gazebosim/gz-gui/pull/416) + +1. Search menu keyboard control + * [Pull request #403](https://github.com/gazebosim/gz-gui/pull/403) + * [Pull request #405](https://github.com/gazebosim/gz-gui/pull/405) + +1. Add config relative path environment variable + * [Pull request #386](https://github.com/gazebosim/gz-gui/pull/386) + +1. Sort plugin list in alphabetical order (including when filtering) + * [Pull request #387](https://github.com/gazebosim/gz-gui/pull/387) + +1. Added array to snackbar qml + * [Pull request #370](https://github.com/gazebosim/gz-gui/pull/370) + +1. Fix some Qt warnings + * [Pull request #376](https://github.com/gazebosim/gz-gui/pull/376) + +1. Added Snackbar qtquick object + * [Pull request #369](https://github.com/gazebosim/gz-gui/pull/369) + +1. Fix menu scrolling when a new plugin is added + * [Pull request #368](https://github.com/gazebosim/gz-gui/pull/368) + +1. Improve KeyPublisher's usability + * [Pull request #362](https://github.com/gazebosim/gz-gui/pull/362) + +1. Backport GridConfig improvements to Citadel's Grid3D + * [Pull request #363](https://github.com/gazebosim/gz-gui/pull/363) + ### Ignition Gui 3.9.0 (2022-01-14) 1. Added a button that allows shutting down both the client and server. From eb99c27a90afdc865fce270c0d85e220b72db0bb Mon Sep 17 00:00:00 2001 From: youhy Date: Thu, 14 Jul 2022 11:37:42 -0700 Subject: [PATCH 20/31] update package version (#434) Signed-off-by: youhy --- examples/standalone/start_dialog/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/standalone/start_dialog/CMakeLists.txt b/examples/standalone/start_dialog/CMakeLists.txt index 7207a0bfe..c35f06409 100644 --- a/examples/standalone/start_dialog/CMakeLists.txt +++ b/examples/standalone/start_dialog/CMakeLists.txt @@ -8,8 +8,8 @@ endif() set(CMAKE_AUTOMOC ON) -find_package(ignition-gui3 REQUIRED) -set(GZ_GUI_VER ${ignition-gui3_VERSION_MAJOR}) +find_package(ignition-gui6 REQUIRED) +set(GZ_GUI_VER ${ignition-gui6_VERSION_MAJOR}) set(EXEC_NAME "start_dialog") From 24bb4b38086d3c116a052b80005a60fb3ce261ff Mon Sep 17 00:00:00 2001 From: youhy Date: Thu, 14 Jul 2022 13:22:58 -0700 Subject: [PATCH 21/31] change FOV test to EXPECT_NEAR (#434) Signed-off-by: youhy --- test/integration/minimal_scene.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/minimal_scene.cc b/test/integration/minimal_scene.cc index a482704ee..dcb6b59fb 100644 --- a/test/integration/minimal_scene.cc +++ b/test/integration/minimal_scene.cc @@ -155,7 +155,7 @@ TEST(MinimalSceneTest, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(Config)) EXPECT_DOUBLE_EQ(0.1, camera->NearClipPlane()); EXPECT_DOUBLE_EQ(5000.0, camera->FarClipPlane()); - EXPECT_DOUBLE_EQ(60, camera->HFOV().Degree()); + EXPECT_NEAR(60, camera->HFOV().Degree(), 1e-4); // Cleanup auto plugins = win->findChildren(); From fa986158e9398cde405ac1cd4c526cda71851e25 Mon Sep 17 00:00:00 2001 From: Jenn Nguyen Date: Fri, 15 Jul 2022 10:38:44 -0700 Subject: [PATCH 22/31] Ignition -> Gazebo (#435) Signed-off-by: Jenn Nguyen --- README.md | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index d6adb5a86..be6e34427 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,24 @@ -# Ignition GUI : Graphical interfaces for robotics applications +# Gazebo GUI : Graphical interfaces for robotics applications **Maintainer:** louise [AT] openrobotics [DOT] org -[![GitHub open issues](https://img.shields.io/github/issues-raw/ignitionrobotics/ign-gui.svg)](https://github.com/ignitionrobotics/ign-gui/issues) -[![GitHub open pull requests](https://img.shields.io/github/issues-pr-raw/ignitionrobotics/ign-gui.svg)](https://github.com/ignitionrobotics/ign-gui/pulls) +[![GitHub open issues](https://img.shields.io/github/issues-raw/gazebosim/gz-gui.svg)](https://github.com/gazebosim/gz-gui/issues) +[![GitHub open pull requests](https://img.shields.io/github/issues-pr-raw/gazebosim/gz-gui.svg)](https://github.com/gazebosim/gz-gui/pulls) [![Discourse topics](https://img.shields.io/discourse/https/community.gazebosim.org/topics.svg)](https://community.gazebosim.org) [![Hex.pm](https://img.shields.io/hexpm/l/plug.svg)](https://www.apache.org/licenses/LICENSE-2.0) Build | Status -- | -- -Test coverage | [![codecov](https://codecov.io/gh/ignitionrobotics/ign-gui/branch/ign-gui3/graph/badge.svg)](https://codecov.io/gh/ignitionrobotics/ign-gui/branch/ign-gui3) +Test coverage | [![codecov](https://codecov.io/gh/gazebosim/gz-gui/branch/ign-gui3/graph/badge.svg)](https://codecov.io/gh/gazebosim/gz-gui/branch/ign-gui3) Ubuntu Bionic | [![Build Status](https://build.osrfoundation.org/buildStatus/icon?job=ignition_gui-ci-ign-gui3-bionic-amd64)](https://build.osrfoundation.org/job/ignition_gui-ci-ign-gui3-bionic-amd64) Homebrew | [![Build Status](https://build.osrfoundation.org/buildStatus/icon?job=ignition_gui-ci-ign-gui3-homebrew-amd64)](https://build.osrfoundation.org/job/ignition_gui-ci-ign-gui3-homebrew-amd64) Windows | [![Build Status](https://build.osrfoundation.org/buildStatus/icon?job=ign_gui-ign-3-win)](https://build.osrfoundation.org/job/ign_gui-ign-3-win) -Ignition GUI builds on top of [Qt](https://www.qt.io/) to provide widgets which are +Gazebo GUI builds on top of [Qt](https://www.qt.io/) to provide widgets which are useful when developing robotics applications, such as a 3D view, plots, dashboard, etc, and can be used together in a convenient unified interface. -Ignition GUI ships with several widgets ready to use and offers a plugin interface +Gazebo GUI ships with several widgets ready to use and offers a plugin interface which can be used to add custom widgets. # Table of Contents @@ -44,25 +44,25 @@ which can be used to add custom widgets. * Qt-based widgets, with support for both Qt5 widgets and QtQuick * Plugin-based interface, so it's easy to add new widgets * Several plugins ready to be used -* 3D scene integration using [Ignition Rendering](https://github.com/ignitionrobotics/ign-rendering/) +* 3D scene integration using [Gazebo Rendering](https://github.com/gazebosim/gz-rendering/) # Install -See [the install tutorial](https://ignitionrobotics.org/api/gui/3.0/install.html). +See [the install tutorial](https://gazebosim.org/api/gui/3.0/install.html). # Usage Take a look at the -[tutorials](https://ignitionrobotics.org/api/gui/3.0/tutorials.html) +[tutorials](https://gazebosim.org/api/gui/3.0/tutorials.html) for usage instructions and API documentation. ## Known issue of command line tools In the event that the installation is a mix of Debian and from source, command -line tools from `ign-tools` may not work correctly. +line tools from `gz-tools` may not work correctly. A workaround for a single package is to define the environment variable -`IGN_CONFIG_PATH` to point to the location of the Ignition library installation, +`IGN_CONFIG_PATH` to point to the location of the Gazebo library installation, where the YAML file for the package is found, such as ``` export IGN_CONFIG_PATH=/usr/local/share/ignition @@ -71,7 +71,7 @@ export IGN_CONFIG_PATH=/usr/local/share/ignition However, that environment variable only takes a single path, which means if the installations from source are in different locations, only one can be specified. -Another workaround for working with multiple Ignition libraries on the command +Another workaround for working with multiple Gazebo libraries on the command line is using symbolic links to each library's YAML file. ``` mkdir ~/.ignition/tools/configs -p @@ -83,7 +83,7 @@ ln -s /usr/local/share/ignition/transportlog7.yaml . export IGN_CONFIG_PATH=$HOME/.ignition/tools/configs ``` -This issue is tracked [here](https://github.com/ignitionrobotics/ign-tools/issues/8/too-strict-looking-for-config-paths). +This issue is tracked [here](https://github.com/gazebosim/gz-tools/issues/8/too-strict-looking-for-config-paths). # Folder Structure @@ -98,35 +98,35 @@ This issue is tracked [here](https://github.com/ignitionrobotics/ign-tools/issue * `test`: All integration, performance and regression tests go here, under their specific folders. -* `examples/standalone`: Example code for standalone applications using Ignition GUI +* `examples/standalone`: Example code for standalone applications using Gazebo GUI as a library. Each example has instructions in a README file. * `examples/config`: Example configuration files which can be loaded using `ign gui -c ` * `examples/plugin`: Example plugins which can be compiled and loaded as explained - in [this tutorial](https://ignitionrobotics.org/api/gui/1.0/plugins.html). + in [this tutorial](https://gazebosim.org/api/gui/1.0/plugins.html). -* `tutorials`: Markdown files for the [tutorials](https://ignitionrobotics.org/api/gui/1.0/tutorials.html). +* `tutorials`: Markdown files for the [tutorials](https://gazebosim.org/api/gui/1.0/tutorials.html). -* `conf`: Files needed by [ign-tools](https://github.com/ignitionrobotics/ign-tools). +* `conf`: Files needed by [gz-tools](https://github.com/gazebosim/gz-tools). * `doc`: Files used by Doxygen when generating documentation. # Contributing Please see -[CONTRIBUTING.md](https://ignitionrobotics.org/docs/all/contributing). +[CONTRIBUTING.md](https://gazebosim.org/docs/all/contributing). # Code of Conduct Please see -[CODE\_OF\_CONDUCT.md](https://github.com/ignitionrobotics/ign-gazebo/blob/main/CODE_OF_CONDUCT.md). +[CODE\_OF\_CONDUCT.md](https://github.com/gazebosim/gz-sim/blob/main/CODE_OF_CONDUCT.md). # Versioning -This library uses [Semantic Versioning](https://semver.org/). Additionally, this library is part of the [Ignition Robotics project](https://ignitionrobotics.org) which periodically releases a versioned set of compatible and complimentary libraries. See the [Ignition Robotics website](https://ignitionrobotics.org) for version and release information. +This library uses [Semantic Versioning](https://semver.org/). Additionally, this library is part of the [Gazebo project](https://gazebosim.org) which periodically releases a versioned set of compatible and complimentary libraries. See the [Gazebo website](https://gazebosim.org) for version and release information. # License -This library is licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0). See also the [LICENSE](https://github.com/ignitionrobotics/ign-gui/blob/main/LICENSE) file. +This library is licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0). See also the [LICENSE](https://github.com/gazebosim/gz-gui/blob/main/LICENSE) file. From c4d65598b8b69a67881584fee994b21069bedfd2 Mon Sep 17 00:00:00 2001 From: Nate Koenig Date: Fri, 22 Jul 2022 21:52:36 -0700 Subject: [PATCH 23/31] Change IGN_DESIGNATION to GZ_DESIGNATION (#437) Signed-off-by: Nate Koenig Signed-off-by: Louise Poubel Co-authored-by: Nate Koenig Co-authored-by: Louise Poubel --- CMakeLists.txt | 2 +- api.md.in | 4 ++-- include/ignition/gui/config.hh.in | 4 ++-- include/ignition/gui/ign_auto_headers.hh.in | 2 +- include/ignition/gui/plugins/CMakeLists.txt | 2 +- tutorials.md.in | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 79f581687..d5b47abe2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ project(ignition-gui1 VERSION 1.0.0) #============================================================================ # Find ignition-cmake #============================================================================ -find_package(ignition-cmake2 REQUIRED) +find_package(ignition-cmake2 2.13 REQUIRED) #============================================================================ # Configure the project diff --git a/api.md.in b/api.md.in index d25ca9164..31af86575 100644 --- a/api.md.in +++ b/api.md.in @@ -1,6 +1,6 @@ -## Ignition @IGN_DESIGNATION_CAP@ +## Ignition @GZ_DESIGNATION_CAP@ -Ignition @IGN_DESIGNATION_CAP@ is a component in Ignition Robotics, a set of libraries +Ignition @GZ_DESIGNATION_CAP@ is a component in Ignition Robotics, a set of libraries designed to rapidly develop robot and simulation applications. ## License diff --git a/include/ignition/gui/config.hh.in b/include/ignition/gui/config.hh.in index d2f3445e0..e439764c9 100644 --- a/include/ignition/gui/config.hh.in +++ b/include/ignition/gui/config.hh.in @@ -8,10 +8,10 @@ #define IGNITION_GUI_VERSION "${PROJECT_VERSION}" #define IGNITION_GUI_VERSION_FULL "${PROJECT_VERSION_FULL}" -#define IGNITION_GUI_VERSION_HEADER "Ignition ${IGN_DESIGNATION}, version ${PROJECT_VERSION_FULL}\nCopyright (C) 2017 Open Source Robotics Foundation.\nReleased under the Apache 2.0 License.\n\n" +#define IGNITION_GUI_VERSION_HEADER "Ignition ${GZ_DESIGNATION}, version ${PROJECT_VERSION_FULL}\nCopyright (C) 2017 Open Source Robotics Foundation.\nReleased under the Apache 2.0 License.\n\n" #cmakedefine BUILD_TYPE_PROFILE 1 #cmakedefine BUILD_TYPE_DEBUG 1 #cmakedefine BUILD_TYPE_RELEASE 1 -#define IGN_GUI_PLUGIN_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${IGN_LIB_INSTALL_DIR}/ign-${IGN_DESIGNATION}-${PROJECT_VERSION_MAJOR}/plugins" +#define IGN_GUI_PLUGIN_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${IGN_LIB_INSTALL_DIR}/ign-${GZ_DESIGNATION}-${PROJECT_VERSION_MAJOR}/plugins" diff --git a/include/ignition/gui/ign_auto_headers.hh.in b/include/ignition/gui/ign_auto_headers.hh.in index 62156f5f5..a92282ee3 100644 --- a/include/ignition/gui/ign_auto_headers.hh.in +++ b/include/ignition/gui/ign_auto_headers.hh.in @@ -1,3 +1,3 @@ // Automatically generated -#include +#include ${ign_headers} diff --git a/include/ignition/gui/plugins/CMakeLists.txt b/include/ignition/gui/plugins/CMakeLists.txt index 7d17722b5..fbbe77055 100644 --- a/include/ignition/gui/plugins/CMakeLists.txt +++ b/include/ignition/gui/plugins/CMakeLists.txt @@ -9,7 +9,7 @@ set (qt_headers_local ) set(IGNITION_GUI_PLUGIN_INSTALL_DIR - ${CMAKE_INSTALL_PREFIX}/${IGN_LIB_INSTALL_DIR}/ign-${IGN_DESIGNATION}-${PROJECT_VERSION_MAJOR}/plugins + ${CMAKE_INSTALL_PREFIX}/${IGN_LIB_INSTALL_DIR}/ign-${GZ_DESIGNATION}-${PROJECT_VERSION_MAJOR}/plugins ) # Plugin shared libraries diff --git a/tutorials.md.in b/tutorials.md.in index c321caeee..9db6c7d2a 100644 --- a/tutorials.md.in +++ b/tutorials.md.in @@ -1,8 +1,8 @@ \page tutorials Tutorials -Welcome to the Ignition @IGN_DESIGNATION_CAP@ tutorials. These tutorials +Welcome to the Ignition @GZ_DESIGNATION_CAP@ tutorials. These tutorials will guide you through the process of understanding the capabilities of the -Ignition @IGN_DESIGNATION_CAP@ library and how to use the library effectively. +Ignition @GZ_DESIGNATION_CAP@ library and how to use the library effectively. **The tutorials** From e526f0380b20c432759380d1d82edc7a1f58d272 Mon Sep 17 00:00:00 2001 From: Louise Poubel Date: Tue, 26 Jul 2022 18:10:23 -0700 Subject: [PATCH 24/31] Teleop: Refactor and support vertical (#440) Signed-off-by: Louise Poubel --- src/plugins/teleop/Teleop.cc | 222 +++++---- src/plugins/teleop/Teleop.hh | 123 +++-- src/plugins/teleop/Teleop.qml | 780 +++++++++++++++++------------- src/plugins/teleop/Teleop_TEST.cc | 294 ++++------- 4 files changed, 736 insertions(+), 683 deletions(-) diff --git a/src/plugins/teleop/Teleop.cc b/src/plugins/teleop/Teleop.cc index 89b7b9255..5e60fdb19 100644 --- a/src/plugins/teleop/Teleop.cc +++ b/src/plugins/teleop/Teleop.cc @@ -39,13 +39,19 @@ namespace gui { namespace plugins { - enum class KeyLinear{ + enum class KeyForward{ kForward, kBackward, kStop, }; - enum class KeyAngular{ + enum class KeyVertical{ + kUp, + kDown, + kStop, + }; + + enum class KeyYaw{ kLeft, kRight, kStop, @@ -62,20 +68,40 @@ namespace plugins /// \brief Publisher. public: ignition::transport::Node::Publisher cmdVelPub; - /// \brief Linear velocity. - public: double linearVel = 0; - /// \brief Angular velocity. - public: double angularVel = 0; + /// \brief Maximum forward velocity in m/s. GUI buttons and key presses + /// will use this velocity. Sliders will scale up to this value. + public: double maxForwardVel = 1.0; + + /// \brief Maximum vertical velocity in m/s. GUI buttons and key presses + /// will use this velocity. Sliders will scale up to this value. + public: double maxVerticalVel = 1.0; + + /// \brief Maximum yaw velocity in rad/s. GUI buttons and key presses + /// will use this velocity. Sliders will scale up to this value. + public: double maxYawVel = 0.5; + + /// \brief Forward scale to multiply by maxForwardVel, in the [-1, 1] range. + /// Negative values go backwards, zero stops movement in the forward axis. + public: int forwardKeyScale = 0; + + /// \brief Vertical scale to multiply by maxVerticalVel, in the [-1, 1] + /// range. Negative values go down, zero stops movement in the vertical + /// axis. + public: int verticalKeyScale = 0; - /// \brief Linear direction. - public: int linearDir = 0; - /// \brief Angular direction. - public: int angularDir = 0; + /// \brief Yaw scale to multiply by maxYawVel, in the [-1, 1] range. + /// Negative values rotate clockwise when looking from above, zero stops + /// movement in the yaw axis. + public: int yawKeyScale = 0; - /// \brief Linear state setted by keyboard input. - public: KeyLinear linearState = KeyLinear::kStop; - /// \brief Angular state setted by keyboard input. - public: KeyAngular angularState = KeyAngular::kStop; + /// \brief Forward state set by keyboard input. + public: KeyForward forwardKeyState = KeyForward::kStop; + + /// \brief Vertical state set by keyboard input. + public: KeyVertical verticalKeyState = KeyVertical::kStop; + + /// \brief Yaw state set by keyboard input. + public: KeyYaw yawKeyState = KeyYaw::kStop; /// \brief Indicates if the keyboard is enabled or /// disabled. @@ -103,32 +129,47 @@ Teleop::Teleop(): Plugin(), dataPtr(std::make_unique()) Teleop::~Teleop() = default; ///////////////////////////////////////////////// -void Teleop::LoadConfig(const tinyxml2::XMLElement *) +void Teleop::LoadConfig(const tinyxml2::XMLElement *_pluginElem) { if (this->title.empty()) this->title = "Teleop"; + if (_pluginElem) + { + auto topicElem = _pluginElem->FirstChildElement("topic"); + if (nullptr != topicElem && nullptr != topicElem->GetText()) + this->SetTopic(topicElem->GetText()); + } + ignition::gui::App()->findChild ()->QuickWindow()->installEventFilter(this); } ///////////////////////////////////////////////// -void Teleop::OnTeleopTwist() +void Teleop::OnTeleopTwist(double _forwardVel, double _verticalVel, + double _angVel) { ignition::msgs::Twist cmdVelMsg; - cmdVelMsg.mutable_linear()->set_x( - this->dataPtr->linearDir * this->dataPtr->linearVel); - cmdVelMsg.mutable_angular()->set_z( - this->dataPtr->angularDir * this->dataPtr->angularVel); + cmdVelMsg.mutable_linear()->set_x(_forwardVel); + cmdVelMsg.mutable_linear()->set_z(_verticalVel); + cmdVelMsg.mutable_angular()->set_z(_angVel); if (!this->dataPtr->cmdVelPub.Publish(cmdVelMsg)) + { ignerr << "ignition::msgs::Twist message couldn't be published at topic: " << this->dataPtr->topic << std::endl; + } } ///////////////////////////////////////////////// -void Teleop::OnTopicSelection(const QString &_topic) +QString Teleop::Topic() const +{ + return QString::fromStdString(this->dataPtr->topic); +} + +///////////////////////////////////////////////// +void Teleop::SetTopic(const QString &_topic) { this->dataPtr->topic = _topic.toStdString(); ignmsg << "A new topic has been entered: '" << @@ -139,7 +180,7 @@ void Teleop::OnTopicSelection(const QString &_topic) this->dataPtr->cmdVelPub = this->dataPtr->node.Advertise (this->dataPtr->topic); - if(!this->dataPtr->cmdVelPub) + if (!this->dataPtr->cmdVelPub) { App()->findChild()->notifyWithDuration( QString::fromStdString("Error when advertising topic: " + @@ -150,134 +191,145 @@ void Teleop::OnTopicSelection(const QString &_topic) else { App()->findChild()->notifyWithDuration( - QString::fromStdString("Subscribing to topic: '" + + QString::fromStdString("Advertising topic: '" + this->dataPtr->topic + "'"), 4000); } + this->TopicChanged(); } ///////////////////////////////////////////////// -void Teleop::OnLinearVelSelection(double _velocity) +void Teleop::SetMaxForwardVel(double _velocity) { - this->dataPtr->linearVel = _velocity; + this->dataPtr->maxForwardVel = _velocity; + this->MaxForwardVelChanged(); } ///////////////////////////////////////////////// -void Teleop::OnAngularVelSelection(double _velocity) +double Teleop::MaxForwardVel() const { - this->dataPtr->angularVel = _velocity; + return this->dataPtr->maxForwardVel; } ///////////////////////////////////////////////// -void Teleop::OnKeySwitch(bool _checked) +void Teleop::SetMaxVerticalVel(double _velocity) { - this->dataPtr->linearDir = 0; - this->dataPtr->angularDir = 0; - this->dataPtr->keyEnable = _checked; + this->dataPtr->maxVerticalVel = _velocity; + this->MaxVerticalVelChanged(); } ///////////////////////////////////////////////// -void Teleop::OnSlidersSwitch(bool _checked) +double Teleop::MaxVerticalVel() const { - if(_checked) - { - this->dataPtr->linearDir = 1; - this->dataPtr->angularDir = 1; - this->OnTeleopTwist(); - } + return this->dataPtr->maxVerticalVel; +} + +///////////////////////////////////////////////// +void Teleop::SetMaxYawVel(double _velocity) +{ + this->dataPtr->maxYawVel = _velocity; + this->MaxYawVelChanged(); +} + +///////////////////////////////////////////////// +double Teleop::MaxYawVel() const +{ + return this->dataPtr->maxYawVel; +} + +///////////////////////////////////////////////// +void Teleop::OnKeySwitch(bool _checked) +{ + this->dataPtr->keyEnable = _checked; } ///////////////////////////////////////////////// bool Teleop::eventFilter(QObject *_obj, QEvent *_event) { - if(this->dataPtr->keyEnable == true) + if (this->dataPtr->keyEnable == true) { - if(_event->type() == QEvent::KeyPress) + if (_event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast(_event); switch(keyEvent->key()) { case Qt::Key_W: - this->dataPtr->linearState = KeyLinear::kForward; + this->dataPtr->forwardKeyState = KeyForward::kForward; break; case Qt::Key_A: - this->dataPtr->angularState = KeyAngular::kLeft; + this->dataPtr->yawKeyState = KeyYaw::kLeft; break; case Qt::Key_D: - this->dataPtr->angularState = KeyAngular::kRight; + this->dataPtr->yawKeyState = KeyYaw::kRight; break; case Qt::Key_S: - this->dataPtr->linearState = KeyLinear::kBackward; + this->dataPtr->forwardKeyState = KeyForward::kBackward; + break; + case Qt::Key_Q: + this->dataPtr->verticalKeyState = KeyVertical::kUp; + break; + case Qt::Key_E: + this->dataPtr->verticalKeyState = KeyVertical::kDown; break; default: break; } - this->SetKeyDirection(); - this->OnTeleopTwist(); + this->SetKeyScale(); + this->OnTeleopTwist( + this->dataPtr->forwardKeyScale * this->dataPtr->maxForwardVel, + this->dataPtr->verticalKeyScale * this->dataPtr->maxVerticalVel, + this->dataPtr->yawKeyScale * this->dataPtr->maxYawVel); } - if(_event->type() == QEvent::KeyRelease) + if (_event->type() == QEvent::KeyRelease) { QKeyEvent *keyEvent = static_cast(_event); switch(keyEvent->key()) { case Qt::Key_W: - this->dataPtr->linearState = KeyLinear::kStop; + this->dataPtr->forwardKeyState = KeyForward::kStop; break; case Qt::Key_A: - this->dataPtr->angularState = KeyAngular::kStop; + this->dataPtr->yawKeyState = KeyYaw::kStop; break; case Qt::Key_D: - this->dataPtr->angularState = KeyAngular::kStop; + this->dataPtr->yawKeyState = KeyYaw::kStop; break; case Qt::Key_S: - this->dataPtr->linearState = KeyLinear::kStop; + this->dataPtr->forwardKeyState = KeyForward::kStop; + break; + case Qt::Key_Q: + this->dataPtr->verticalKeyState = KeyVertical::kStop; + break; + case Qt::Key_E: + this->dataPtr->verticalKeyState = KeyVertical::kStop; break; default: break; } - this->SetKeyDirection(); - this->OnTeleopTwist(); + this->SetKeyScale(); + this->OnTeleopTwist( + this->dataPtr->forwardKeyScale * this->dataPtr->maxForwardVel, + this->dataPtr->verticalKeyScale * this->dataPtr->maxVerticalVel, + this->dataPtr->yawKeyScale * this->dataPtr->maxYawVel); } } return QObject::eventFilter(_obj, _event); } ///////////////////////////////////////////////// -void Teleop::SetKeyDirection() +void Teleop::SetKeyScale() { - this->dataPtr->linearDir = this->dataPtr->linearState == - KeyLinear::kForward ? 1 : this->dataPtr->linearState == - KeyLinear::kBackward ? -1 : 0; + this->dataPtr->forwardKeyScale = this->dataPtr->forwardKeyState == + KeyForward::kForward ? 1 : this->dataPtr->forwardKeyState == + KeyForward::kBackward ? -1 : 0; - this->dataPtr->angularDir = this->dataPtr->angularState == - KeyAngular::kLeft ? 1 : this->dataPtr->angularState == - KeyAngular::kRight ? -1 : 0; -} + this->dataPtr->verticalKeyScale = this->dataPtr->verticalKeyState == + KeyVertical::kUp ? 1 : this->dataPtr->verticalKeyState == + KeyVertical::kDown ? -1 : 0; -///////////////////////////////////////////////// -int Teleop::LinearDirection() const -{ - return this->dataPtr->linearDir; -} - -///////////////////////////////////////////////// -void Teleop::setLinearDirection(int _linearDir) -{ - this->dataPtr->linearDir = _linearDir; - this->LinearDirectionChanged(); -} - -///////////////////////////////////////////////// -int Teleop::AngularDirection() const -{ - return this->dataPtr->angularDir; -} - -///////////////////////////////////////////////// -void Teleop::setAngularDirection(int _angularDir) -{ - this->dataPtr->angularDir = _angularDir; - this->AngularDirectionChanged(); + this->dataPtr->yawKeyScale = this->dataPtr->yawKeyState == + KeyYaw::kLeft ? 1 : this->dataPtr->yawKeyState == + KeyYaw::kRight ? -1 : 0; } // Register this plugin diff --git a/src/plugins/teleop/Teleop.hh b/src/plugins/teleop/Teleop.hh index 187a77323..1406f88d7 100644 --- a/src/plugins/teleop/Teleop.hh +++ b/src/plugins/teleop/Teleop.hh @@ -46,27 +46,43 @@ namespace plugins /// \brief Publish teleop stokes to a user selected topic, /// or to '/cmd_vel' if no topic is selected. /// Buttons, the keyboard or sliders can be used to move a - /// vehicle load to the world. + /// vehicle in the world. /// ## Configuration - /// This plugin doesn't accept any custom configuration. + /// * ``: Topic to publish twist messages to. class Teleop_EXPORTS_API Teleop : public Plugin { Q_OBJECT - /// \brief Linear direction + /// \brief Topic Q_PROPERTY( - int linearDir - READ LinearDirection - WRITE setLinearDirection - NOTIFY LinearDirectionChanged + QString topic + READ Topic + WRITE SetTopic + NOTIFY TopicChanged ) - /// \brief Angular direction + /// \brief Forward velocity Q_PROPERTY( - int angularDir - READ AngularDirection - WRITE setAngularDirection - NOTIFY AngularDirectionChanged + double maxForwardVel + READ MaxForwardVel + WRITE SetMaxForwardVel + NOTIFY MaxForwardVelChanged + ) + + /// \brief Vertical velocity + Q_PROPERTY( + double maxVerticalVel + READ MaxVerticalVel + WRITE SetMaxVerticalVel + NOTIFY MaxVerticalVelChanged + ) + + /// \brief Yaw velocity + Q_PROPERTY( + double maxYawVel + READ MaxYawVel + WRITE SetMaxYawVel + NOTIFY MaxYawVelChanged ) /// \brief Constructor @@ -82,59 +98,64 @@ namespace plugins protected: bool eventFilter(QObject *_obj, QEvent *_event) override; /// \brief Publish the twist message to the selected command velocity topic. - public slots: void OnTeleopTwist(); + /// \param[in] _forwardVel Forward velocity + /// \param[in] _verticalVel Vertical velocity + /// \param[in] _angVel Yaw velocity + public slots: void OnTeleopTwist(double _forwardVel, double _verticalVel, + double _angVel); - /// \brief Returns the linear direction variable value. - /// When the movement is forward it takes the value 1, when - /// is backward it takes the value -1, and when it's 0 the - /// movement stops. - public: Q_INVOKABLE int LinearDirection() const; + /// \brief Get the topic as a string, for example + /// '/echo' + /// \return Topic + public: Q_INVOKABLE QString Topic() const; - /// \brief Set the linear direction of the movement. - /// \param[in] _linearDir Modifier of the velocity for setting - /// the movement direction. - public: Q_INVOKABLE void setLinearDirection(int _linearDir); + /// \brief Callback in Qt thread when the topic changes. + /// \param[in] _topic variable to indicate the topic to + /// publish the Twist commands. + public slots: void SetTopic(const QString &_topic); - /// \brief Notify that the linear direction changed. - signals: void LinearDirectionChanged(); + /// \brief Notify that topic has changed + signals: void TopicChanged(); - /// \brief Returns the angular direction variable value. - /// When the turn is counterclockwise it takes the value 1, when - /// is clockwise it takes the value -1, and when it's 0 the - /// movement stops. - public: Q_INVOKABLE int AngularDirection() const; + /// \brief Get the forward velocity. + /// \return Forward velocity. + public: Q_INVOKABLE double MaxForwardVel() const; - /// \brief Set the angular direction of the movement. - /// \param[in] _angularDir Modifier of the velocity for setting - /// the direction of the rotation. - public: Q_INVOKABLE void setAngularDirection(int _angularDir); + /// \brief Callback in Qt thread when the forward velocity changes. + /// \param[in] _velocity variable to indicate the forward velocity. + public slots: void SetMaxForwardVel(double _velocity); - /// \brief Notify that the angular direction changed. - signals: void AngularDirectionChanged(); + /// \brief Notify that forward velocity has changed + signals: void MaxForwardVelChanged(); - /// \brief Callback in Qt thread when the topic changes. - /// \param[in] _topic variable to indicate the topic to - /// publish the Twist commands. - public slots: void OnTopicSelection(const QString &_topic); + /// \brief Get the vertical velocity. + /// \return Vertical velocity. + public: Q_INVOKABLE double MaxVerticalVel() const; + + /// \brief Callback in Qt thread when the vertical velocity changes. + /// \param[in] _velocity variable to indicate the vertical velocity. + public slots: void SetMaxVerticalVel(double _velocity); - /// \brief Callback in Qt thread when the linear velocity changes. - /// \param[in] _velocity variable to indicate the linear velocity. - public slots: void OnLinearVelSelection(double _velocity); + /// \brief Notify that vertical velocity has changed + signals: void MaxVerticalVelChanged(); - /// \brief Callback in Qt thread when the angular velocity changes. - /// \param[in] _velocity variable to indicate the angular velocity. - public slots: void OnAngularVelSelection(double _velocity); + /// \brief Get the yaw velocity. + /// \return Yaw velocity. + public: Q_INVOKABLE double MaxYawVel() const; + + /// \brief Callback in Qt thread when the yaw velocity changes. + /// \param[in] _velocity variable to indicate the yaw velocity. + public slots: void SetMaxYawVel(double _velocity); + + /// \brief Notify that yaw velocity has changed + signals: void MaxYawVelChanged(); /// \brief Callback in Qt thread when the keyboard is enabled or disabled. /// \param[in] _checked variable to indicate the state of the switch. public slots: void OnKeySwitch(bool _checked); - /// \brief Callback in Qt thread when the sliders is enabled or disabled. - /// \param[in] _checked variable to indicate the state of the switch. - public slots: void OnSlidersSwitch(bool _checked); - - /// \brief Sets the movement direction when the keyboard is used. - public: void SetKeyDirection(); + /// \brief Sets the movement scale when the keyboard is used. + public: void SetKeyScale(); /// \internal /// \brief Pointer to private data. diff --git a/src/plugins/teleop/Teleop.qml b/src/plugins/teleop/Teleop.qml index f4001a578..ce171f755 100644 --- a/src/plugins/teleop/Teleop.qml +++ b/src/plugins/teleop/Teleop.qml @@ -22,399 +22,489 @@ import QtQuick.Controls.Styles 1.4 import QtQuick.Layouts 1.3 import ignition.gui 1.0 -Rectangle { - color:"transparent" - Layout.minimumWidth: 300 - Layout.minimumHeight: 900 +ColumnLayout { + Layout.minimumWidth: 400 + Layout.minimumHeight: 650 + Layout.margins: 5 anchors.fill: parent focus: true + // Maximum forward velocity + property double maxForwardVel: Teleop.maxForwardVel + + // Maximum vertical velocity + property double maxVerticalVel: Teleop.maxVerticalVel + + // Maximum yaw velocity + property double maxYawVel: Teleop.maxYawVel + + // Send command according to given scale + function sendCommand(_forwardScale, _verticalScale, _yawScale) { + var forwardVel = _forwardScale * maxForwardVel; + var verticalVel = _verticalScale * maxVerticalVel; + var yawVel = _yawScale * maxYawVel; + Teleop.OnTeleopTwist(forwardVel, verticalVel, yawVel) + } + + // Forward scale based on button state + function forwardScale() { + if (forwardButton.checked) + return 1; + else if (backwardButton.checked) + return -1; + + return 0; + } + + // Vertical scale based on button state + function verticalScale() { + if (upButton.checked) + return 1; + else if (downButton.checked) + return -1; + + return 0; + } + + // Yaw scale based on button state + function yawScale() { + if (leftButton.checked) + return 1; + else if (rightButton.checked) + return -1; + + return 0; + } + // Topic input Label { id: topicLabel - text: "Topic:" - anchors.top: parent.top - anchors.topMargin: 10 - anchors.left: parent.left - anchors.leftMargin: 5 + text: "Topic" + Layout.fillWidth: true + Layout.margins: 10 } TextField { id: topicField - anchors.top: topicLabel.bottom - anchors.topMargin: 5 - anchors.left: parent.left - anchors.leftMargin: 5 Layout.fillWidth: true - text:"/cmd_vel" + Layout.margins: 10 + text: Teleop.topic placeholderText: qsTr("Topic to publish...") onEditingFinished: { - Teleop.OnTopicSelection(text) + Teleop.SetTopic(text) } } // Velocity input Label { id: velocityLabel - text: "Velocity:" - anchors.top: topicField.bottom - anchors.topMargin: 10 - anchors.left: parent.left - anchors.leftMargin: 5 - } - // Linear velocity input - Label { - id: linearVelLabel - text: "Linear" - color: "dimgrey" - anchors.top: velocityLabel.bottom - anchors.topMargin: 15 - anchors.left: parent.left - anchors.leftMargin: 5 - } - IgnSpinBox { - id: linearVelField - anchors.top: velocityLabel.bottom - anchors.topMargin: 5 - anchors.left: linearVelLabel.right - anchors.leftMargin: 5 - Layout.fillWidth: true - value: 0.0 - maximumValue: 10.0 - minimumValue: 0.0 - decimals: 2 - stepSize: 0.10 - onEditingFinished:{ - Teleop.OnLinearVelSelection(value) - } - } - - // Angular velocity input - Label { - id: angularVelLabel - text: "Angular" - color: "dimgrey" - anchors.top: velocityLabel.bottom - anchors.topMargin: 15 - anchors.left: linearVelField.right - anchors.leftMargin: 10 - } - IgnSpinBox { - id: angularVelField - anchors.top: velocityLabel.bottom - anchors.topMargin: 5 - anchors.left: angularVelLabel.right - anchors.leftMargin: 5 - Layout.fillWidth: true - value: 0.0 - maximumValue: 2.0 - minimumValue: 0.0 - decimals: 2 - stepSize: 0.10 - onEditingFinished:{ - Teleop.OnAngularVelSelection(value) - } + text: "Maximum velocity" + Layout.margins: 10 + ToolTip.text: "Value that's set by buttons and keys, and scaled by sliders." + ToolTip.visible: velocityLabelMA.containsMouse + MouseArea { + id: velocityLabelMA + anchors.fill: parent + hoverEnabled: true + } } - // Button grid GridLayout { - id: buttonsGrid - anchors.top: angularVelField.bottom - anchors.topMargin: 15 - anchors.left: parent.left - anchors.leftMargin: 40 Layout.fillWidth: true - columns: 4 - Button { - id: forwardButton - text: "\u25B2" - checkable: true - Layout.row: 0 - Layout.column: 1 - onClicked: { - Teleop.linearDir = forwardButton.checked ? 1 : 0 - if(backwardButton.checked) - backwardButton.checked = false - slidersSwitch.checked = false - Teleop.OnTeleopTwist() - } - ToolTip.visible: hovered - ToolTip.text: "Forward" - Material.background: Material.primary - contentItem: Label { - renderType: Text.NativeRendering - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - font.family: "Helvetica" - font.pointSize: 10 - color: "black" - text: forwardButton.text - } + Layout.margins: 10 + columns: 2 + + // Forward velocity input + Label { + id: maxForwardVelLabel + text: "Forward (m/s)" + color: "dimgrey" } - Button { - id: leftButton - text: "\u25C0" - checkable: true - Layout.row: 1 - Layout.column: 0 - onClicked: { - Teleop.angularDir = leftButton.checked ? 1 : 0 - if(rightButton.checked) - rightButton.checked = false - slidersSwitch.checked = false - Teleop.OnTeleopTwist() - } - Material.background: Material.primary - contentItem: Label { - renderType: Text.NativeRendering - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - font.family: "Helvetica" - font.pointSize: 10 - color: "black" - text: leftButton.text + IgnSpinBox { + id: maxForwardVelField + Layout.fillWidth: true + value: maxForwardVel + maximumValue: 10000.0 + minimumValue: 0.0 + decimals: 2 + stepSize: 0.10 + onEditingFinished:{ + Teleop.SetMaxForwardVel(value) } } - Button { - id: rightButton - text: "\u25B6" - checkable: true - Layout.row: 1 - Layout.column: 2 - onClicked: { - Teleop.angularDir = rightButton.checked ? -1 : 0 - if(leftButton.checked) - leftButton.checked = false - slidersSwitch.checked = false - Teleop.OnTeleopTwist() - } - Material.background: Material.primary - contentItem: Label { - renderType: Text.NativeRendering - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - font.family: "Helvetica" - font.pointSize: 10 - color: "black" - text: rightButton.text - } + + // Vertical velocity input + Label { + id: maxVerticalVelLabel + text: "Vertical (m/s)" + color: "dimgrey" } - Button { - id: backwardButton - text: "\u25BC" - checkable: true - Layout.row: 2 - Layout.column: 1 - onClicked: { - Teleop.linearDir = backwardButton.checked ? -1 : 0 - if(forwardButton.checked) - forwardButton.checked = false - slidersSwitch.checked = false - Teleop.OnTeleopTwist() - } - Material.background: Material.primary - contentItem: Label { - renderType: Text.NativeRendering - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - font.family: "Helvetica" - font.pointSize: 10 - color: "black" - text: backwardButton.text + IgnSpinBox { + id: maxVerticalVelField + Layout.fillWidth: true + value: maxVerticalVel + maximumValue: 10000.0 + minimumValue: 0.0 + decimals: 2 + stepSize: 0.10 + onEditingFinished:{ + Teleop.SetMaxVerticalVel(value) } } - Button { - id: stopButton - text: "Stop" - checkable: false - Layout.row: 1 - Layout.column: 1 - onClicked: { - Teleop.linearDir = 0 - Teleop.angularDir = 0 - forwardButton.checked = false - leftButton.checked = false - rightButton.checked = false - backwardButton.checked = false - linearVelSlider.value = 0 - angularVelSlider.value = 0 - slidersSwitch.checked = false - Teleop.OnTeleopTwist() - } - Material.background: Material.primary - contentItem: Label { - renderType: Text.NativeRendering - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - font.family: "Helvetica" - font.pointSize: 10 - color: "black" - text: stopButton.text + + // Yaw velocity input + Label { + id: maxYawVelLabel + text: "Yaw (rad/s)" + color: "dimgrey" + } + IgnSpinBox { + id: maxYawVelField + Layout.fillWidth: true + value: maxYawVel + maximumValue: 10000.0 + minimumValue: 0.0 + decimals: 2 + stepSize: 0.10 + onEditingFinished:{ + Teleop.SetMaxYawVel(value) } } } - //Keyboard's switch - Switch { - id: keySwitch - anchors.top: buttonsGrid.bottom - anchors.topMargin: 10 - anchors.left: parent.left - anchors.leftMargin: 5 - onClicked: { - forwardButton.checked = false - leftButton.checked = false - rightButton.checked = false - backwardButton.checked = false - Teleop.OnKeySwitch(checked); + TabBar { + id: tabs + Layout.fillWidth: true + Layout.margins: 10 + + onCurrentIndexChanged: { + Teleop.OnKeySwitch(currentIndex == 1); } - ToolTip.visible: hovered - ToolTip.text: checked ? qsTr("Disable keyboard") : qsTr("Enable keyboard") - } - Label { - id: keyboardSwitchLabel - text: "Input from keyboard (WASD)" - anchors.horizontalCenter : keySwitch.horizontalCenter - anchors.verticalCenter : keySwitch.verticalCenter - anchors.left: keySwitch.right - anchors.leftMargin: 5 - } - // Slider's switch - Switch { - id: slidersSwitch - anchors.top: keySwitch.bottom - anchors.topMargin: 10 - anchors.left: keySwitch.left - onClicked: { - Teleop.OnSlidersSwitch(checked); - if(checked){ - forwardButton.checked = false - leftButton.checked = false - rightButton.checked = false - backwardButton.checked = false - linearVelField.value = linearVelSlider.value.toFixed(2) - angularVelField.value = angularVelSlider.value.toFixed(2) - Teleop.OnLinearVelSelection(linearVelSlider.value) - Teleop.OnAngularVelSelection(angularVelSlider.value) - Teleop.OnTeleopTwist() - } + TabButton { + text: qsTr("Buttons") + } + TabButton { + text: qsTr("Keyboard") + } + TabButton { + text: qsTr("Sliders") } - ToolTip.visible: hovered - ToolTip.text: checked ? qsTr("Disable sliders") : qsTr("Enable sliders") - } - Label { - id: slidersSwitchLabel - text: "Input from sliders" - anchors.horizontalCenter : slidersSwitch.horizontalCenter - anchors.verticalCenter : slidersSwitch.verticalCenter - anchors.left: slidersSwitch.right - anchors.leftMargin: 5 } - TextField { - id: linearVelMaxTextField - anchors.top: slidersSwitch.bottom - anchors.topMargin: 10 - anchors.horizontalCenter : angularVelSlider.horizontalCenter - width: 40 - text:"1.0" - } + StackLayout { + Layout.fillHeight: true + Layout.fillWidth: true + Layout.margins: 10 + currentIndex: tabs.currentIndex - // Vertical slider - Slider { - id: linearVelSlider - height: 150 - width: 50 - orientation: Qt.Vertical - anchors.top: linearVelMaxTextField.bottom - anchors.horizontalCenter : angularVelSlider.horizontalCenter - handle: Rectangle { - y: linearVelSlider.topPadding + linearVelSlider.visualPosition * (linearVelSlider.availableHeight - height) - x: linearVelSlider.leftPadding + linearVelSlider.availableWidth / 2 - width / 2 - implicitWidth: 25 - implicitHeight: 10 - color: linearVelSlider.pressed ? "#f0f0f0" : "#f6f6f6" - border.color: "black" - } - enabled: slidersSwitch.checked + // Buttons + Item { + GridLayout { + id: buttonsGrid + width: parent.width + columns: 4 + + Button { + id: forwardButton + text: "\u25B2" + checkable: true + Layout.row: 0 + Layout.column: 1 + onClicked: { + if (backwardButton.checked) + backwardButton.checked = false + sendCommand(forwardScale(), verticalScale(), yawScale()); + } + ToolTip.visible: hovered + ToolTip.text: "Forward" + Material.background: Material.primary + contentItem: Label { + renderType: Text.NativeRendering + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + font.family: "Helvetica" + font.pointSize: 10 + color: "black" + text: forwardButton.text + } + } + Button { + id: leftButton + text: "\u25C0" + checkable: true + Layout.row: 1 + Layout.column: 0 + onClicked: { + if (rightButton.checked) + rightButton.checked = false + sendCommand(forwardScale(), verticalScale(), yawScale()); + } + ToolTip.visible: hovered + ToolTip.text: "Left" + Material.background: Material.primary + contentItem: Label { + renderType: Text.NativeRendering + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + font.family: "Helvetica" + font.pointSize: 10 + color: "black" + text: leftButton.text + } + } + Button { + id: rightButton + text: "\u25B6" + checkable: true + Layout.row: 1 + Layout.column: 2 + onClicked: { + if (leftButton.checked) + leftButton.checked = false + sendCommand(forwardScale(), verticalScale(), yawScale()); + } + ToolTip.visible: hovered + ToolTip.text: "Right" + Material.background: Material.primary + contentItem: Label { + renderType: Text.NativeRendering + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + font.family: "Helvetica" + font.pointSize: 10 + color: "black" + text: rightButton.text + } + } + Button { + id: backwardButton + text: "\u25BC" + checkable: true + Layout.row: 2 + Layout.column: 1 + onClicked: { + if (forwardButton.checked) + forwardButton.checked = false + sendCommand(forwardScale(), verticalScale(), yawScale()); + } + ToolTip.visible: hovered + ToolTip.text: "Back" + Material.background: Material.primary + contentItem: Label { + renderType: Text.NativeRendering + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + font.family: "Helvetica" + font.pointSize: 10 + color: "black" + text: backwardButton.text + } + } + Button { + id: stopButton + text: "Stop" + checkable: false + Layout.row: 1 + Layout.column: 1 + onClicked: { + forwardButton.checked = false + leftButton.checked = false + rightButton.checked = false + backwardButton.checked = false + upButton.checked = false + downButton.checked = false + sendCommand(forwardScale(), verticalScale(), yawScale()); + } + ToolTip.visible: hovered + ToolTip.text: "Stop" + Material.background: Material.primary + contentItem: Label { + renderType: Text.NativeRendering + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + font.family: "Helvetica" + font.pointSize: 10 + color: "black" + text: stopButton.text + } + } - from: linearVelMinTextField.text - to: linearVelMaxTextField.text - stepSize: 0.01 + Button { + id: upButton + text: "\u2191" + checkable: true + Layout.row: 0 + Layout.column: 3 + onClicked: { + if (downButton.checked) + downButton.checked = false + sendCommand(forwardScale(), verticalScale(), yawScale()); + } + ToolTip.visible: hovered + ToolTip.text: "Up" + Material.background: Material.primary + contentItem: Label { + renderType: Text.NativeRendering + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + font.family: "Helvetica" + font.pointSize: 10 + color: "black" + text: upButton.text + } + } - onMoved: { - linearVelField.value = linearVelSlider.value.toFixed(2) - Teleop.OnLinearVelSelection(linearVelSlider.value) - Teleop.OnTeleopTwist() + Button { + id: downButton + text: "\u2193" + checkable: true + Layout.row: 2 + Layout.column: 3 + onClicked: { + if (upButton.checked) + upButton.checked = false + sendCommand(forwardScale(), verticalScale(), yawScale()); + } + ToolTip.visible: hovered + ToolTip.text: "Down" + Material.background: Material.primary + contentItem: Label { + renderType: Text.NativeRendering + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + font.family: "Helvetica" + font.pointSize: 10 + color: "black" + text: downButton.text + } + } + // Bottom spacer + Item { + Layout.row: 3 + Layout.column: 0 + Layout.fillHeight: true + } + } } - } - TextField { - id: linearVelMinTextField - anchors.top: linearVelSlider.bottom - anchors.horizontalCenter : linearVelSlider.horizontalCenter - width: 40 - text:"-1.0" - } + // Keyboard + Item { + width: parent.width + Text { + textFormat: Text.RichText + text: "Hold keys:
    " + + "
  • W: Forward
  • " + + "
  • A: Left
  • " + + "
  • S: Back
  • " + + "
  • D: Right
  • " + + "
  • Q: Up
  • " + + "
  • E: Down
" + } + } - Label { - id: currentLinearVelSliderLabel - anchors.verticalCenter : linearVelSlider.verticalCenter - anchors.left : linearVelSlider.right - text: linearVelSlider.value.toFixed(2) + " m/s" - } + // Sliders + Item { + width: parent.width - TextField { - id: angularVelMinTextField - anchors.verticalCenter : angularVelSlider.verticalCenter - anchors.left : slidersSwitch.left - width: 40 - text:"-1.0" - } + GridLayout { + columns: 4 + columnSpacing: 10 + width: parent.width - // Horizontal slider - Slider { - id: angularVelSlider - height: 50 - width: 175 - anchors.top: linearVelSlider.bottom - anchors.topMargin: 50 - anchors.left : angularVelMinTextField.right - anchors.leftMargin: 10 - handle: Rectangle { - x: angularVelSlider.leftPadding + angularVelSlider.visualPosition * (angularVelSlider.availableWidth - width) - y: angularVelSlider.topPadding + angularVelSlider.availableHeight / 2 - height / 2 - implicitWidth: 10 - implicitHeight: 25 - color: angularVelSlider.pressed ? "#f0f0f0" : "#f6f6f6" - border.color: "black" - } - enabled: slidersSwitch.checked + // Forward + Label { + text: "Forward (m/s)" + } - from: angularVelMinTextField.text - to: angularVelMaxTextField.text - stepSize: 0.01 + Label { + text: (-maxForwardVel).toFixed(2) + } - onMoved: { - angularVelField.value = angularVelSlider.value.toFixed(2) - Teleop.OnAngularVelSelection(angularVelSlider.value) - Teleop.OnTeleopTwist() - } - } + Slider { + id: forwardVelSlider + Layout.fillWidth: true + from: -1.0 + to: 1.0 + stepSize: 0.01 - TextField { - id: angularVelMaxTextField - anchors.verticalCenter : angularVelSlider.verticalCenter - anchors.left : angularVelSlider.right - anchors.leftMargin: 10 - width: 40 - text:"1.0" - } + onMoved: { + sendCommand(forwardVelSlider.value, verticalVelSlider.value, yawVelSlider.value); + } + } - Label { - id: currentAngularVelSliderLabel - anchors.horizontalCenter : angularVelSlider.horizontalCenter - anchors.top : angularVelSlider.bottom - text: angularVelSlider.value.toFixed(2) + " rad/s" + Label { + text: maxForwardVel.toFixed(2) + } + + // Vertical + Label { + text: "Vertical (m/s)" + } + + Label { + text: (-maxVerticalVel).toFixed(2) + } + + Slider { + id: verticalVelSlider + Layout.fillWidth: true + from: -1.0 + to: 1.0 + stepSize: 0.01 + + onMoved: { + sendCommand(forwardVelSlider.value, verticalVelSlider.value, yawVelSlider.value); + } + } + + Label { + text: maxVerticalVel.toFixed(2) + } + + // Yaw + Label { + text: "Yaw (rad/s)" + } + + Label { + text: (-maxYawVel).toFixed(2) + } + + Slider { + id: yawVelSlider + Layout.fillWidth: true + from: -1.0 + to: 1.0 + stepSize: 0.01 + + onMoved: { + sendCommand(forwardVelSlider.value, verticalVelSlider.value, yawVelSlider.value); + } + } + + Label { + text: maxYawVel.toFixed(2) + } + + Button { + text: "Stop" + Layout.columnSpan: 4 + onClicked: { + forwardVelSlider.value = 0.0; + verticalVelSlider.value = 0.0; + yawVelSlider.value = 0.0; + sendCommand(forwardVelSlider.value, verticalVelSlider.value, yawVelSlider.value); + } + ToolTip.visible: hovered + ToolTip.text: "Stop" + Material.background: Material.primary + } + + // Bottom spacer + Item { + Layout.fillHeight: true + } + } + } } } diff --git a/src/plugins/teleop/Teleop_TEST.cc b/src/plugins/teleop/Teleop_TEST.cc index 90c9a7c2f..c2ac9d6ad 100644 --- a/src/plugins/teleop/Teleop_TEST.cc +++ b/src/plugins/teleop/Teleop_TEST.cc @@ -53,15 +53,17 @@ class TeleopTest : public ::testing::Test this->app.AddPluginPath(std::string(PROJECT_BINARY_PATH) + "/lib"); // Load plugin - const char *pluginStr = + const std::string kTopic{"/test/cmd_vel"}; + std::string pluginStr = "" "" "Teleop!" "" + "" + kTopic + "" ""; tinyxml2::XMLDocument pluginDoc; - EXPECT_EQ(tinyxml2::XML_SUCCESS, pluginDoc.Parse(pluginStr)); + EXPECT_EQ(tinyxml2::XML_SUCCESS, pluginDoc.Parse(pluginStr.c_str())); EXPECT_TRUE(this->app.LoadPlugin("Teleop", pluginDoc.FirstChildElement("plugin"))); @@ -80,35 +82,58 @@ class TeleopTest : public ::testing::Test EXPECT_EQ(plugin->Title(), "Teleop!"); // Subscribes to the command velocity topic. - node.Subscribe("/model/vehicle_blue/cmd_vel", - &TeleopTest::VerifyTwistMsgCb, this); + node.Subscribe(kTopic, &TeleopTest::VerifyTwistMsgCb, this); // Sets topic. This must be the same as the // one the node is subscribed to. - plugin->OnTopicSelection( - QString::fromStdString("/model/vehicle_blue/cmd_vel")); - - // Checks if the directions of the movement are set - // with the default value '0'. - EXPECT_EQ(plugin->LinearDirection(), 0); - EXPECT_EQ(plugin->AngularDirection(), 0); + plugin->SetTopic(QString::fromStdString(kTopic)); // Set velocity value and movement direction. - plugin->OnLinearVelSelection(linearVel); - plugin->OnAngularVelSelection(angularVel); + plugin->SetMaxForwardVel(this->kMaxForwardVel); + plugin->SetMaxVerticalVel(this->kMaxVerticalVel); + plugin->SetMaxYawVel(this->kMaxYawVel); + } + + // Set up function. + protected: void TearDown() override + { + // Cleanup + plugins.clear(); } // Subscriber call back function. Verifies if the Twist message is // sent correctly. protected: void VerifyTwistMsgCb(const msgs::Twist &_msg) { - EXPECT_DOUBLE_EQ(_msg.linear().x(), - plugin->LinearDirection() * linearVel); - EXPECT_DOUBLE_EQ(_msg.angular().z(), - plugin->AngularDirection() * angularVel); + EXPECT_DOUBLE_EQ(_msg.linear().x(), this->forwardVel); + EXPECT_DOUBLE_EQ(_msg.linear().z(), this->verticalVel); + EXPECT_DOUBLE_EQ(_msg.angular().z(), this->yawVel); received = true; } + // Subscriber call back function. Verifies if the Twist message is + // sent correctly. + protected: void KeyEvent(bool _press, int _key) + { + this->received = false; + + auto type = _press ? QKeyEvent::KeyPress : QKeyEvent::KeyRelease; + auto event = new QKeyEvent(type, _key, Qt::NoModifier); + app.sendEvent(win->QuickWindow(), event); + + int sleep = 0; + const 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}; @@ -123,19 +148,24 @@ class TeleopTest : public ::testing::Test protected: bool received = false; protected: transport::Node node; - // Define velocity values. - protected: const double linearVel = 1.0; - protected: const double angularVel = 0.5; + // Maximum velocities + protected: const double kMaxForwardVel = 1.0; + protected: const double kMaxVerticalVel = 1.0; + protected: const double kMaxYawVel = 0.5; + + // Current vel + protected: double forwardVel = 0.0; + protected: double verticalVel = 0.0; + protected: double yawVel = 0.0; }; ///////////////////////////////////////////////// TEST_F(TeleopTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(ButtonCommand)) { - // Forward movement. - plugin->setLinearDirection(1); - // Counterclockwise movement. - plugin->setAngularDirection(1); - plugin->OnTeleopTwist(); + this->forwardVel = 0.1; + this->verticalVel = 0.2; + this->yawVel = 0.3; + plugin->OnTeleopTwist(this->forwardVel, this->verticalVel, this->yawVel); int sleep = 0; const int maxSleep = 30; @@ -148,79 +178,11 @@ TEST_F(TeleopTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(ButtonCommand)) EXPECT_TRUE(received); received = false; - - // Change movement direction. - // Backward movement. - plugin->setLinearDirection(-1); - // Clockwise direction. - plugin->setAngularDirection(-1); - plugin->OnTeleopTwist(); - - sleep = 0; - while (!received && sleep < maxSleep) - { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - QCoreApplication::processEvents(); - sleep++; - } - - EXPECT_TRUE(received); - received = false; - - // Stops angular movement. - plugin->setAngularDirection(0); - plugin->OnTeleopTwist(); - - sleep = 0; - while (!received && sleep < maxSleep) - { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - QCoreApplication::processEvents(); - sleep++; - } - EXPECT_TRUE(received); - received = false; - - // Stops linear movement. - // Starts angular movement. - plugin->setLinearDirection(0); - plugin->setAngularDirection(1); - plugin->OnTeleopTwist(); - - sleep = 0; - while (!received && sleep < maxSleep) - { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - QCoreApplication::processEvents(); - sleep++; - } - - EXPECT_TRUE(received); - received = false; - - // Stops movement. - plugin->setAngularDirection(0); - plugin->setLinearDirection(0); - plugin->OnTeleopTwist(); - - sleep = 0; - while (!received && sleep < maxSleep) - { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - QCoreApplication::processEvents(); - sleep++; - } - - EXPECT_TRUE(received); - - // Cleanup - plugins.clear(); } ///////////////////////////////////////////////// TEST_F(TeleopTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(KeyboardCommand)) { - // Generates a key press event on the main window. QKeyEvent *keypress_W = new QKeyEvent(QKeyEvent::KeyPress, Qt::Key_W, Qt::NoModifier); app.sendEvent(win->QuickWindow(), keypress_W); @@ -230,126 +192,54 @@ TEST_F(TeleopTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(KeyboardCommand)) // Enables key input. plugin->OnKeySwitch(true); - app.sendEvent(win->QuickWindow(), keypress_W); - int sleep = 0; - const int maxSleep = 30; - while (!received && sleep < maxSleep) - { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - QCoreApplication::processEvents(); - sleep++; - } + igndbg << "Press W" << std::endl; + this->forwardVel = this->kMaxForwardVel; + this->KeyEvent(true, Qt::Key_W); - EXPECT_TRUE(received); - received = false; - - // Generates a key press event on the main window. - QKeyEvent *keypress_D = new QKeyEvent(QKeyEvent::KeyPress, - Qt::Key_D, Qt::NoModifier); - app.sendEvent(win->QuickWindow(), keypress_D); + igndbg << "Press D" << std::endl; + this->yawVel = -this->kMaxYawVel; + this->KeyEvent(true, Qt::Key_D); - sleep = 0; - while (!received && sleep < maxSleep) - { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - QCoreApplication::processEvents(); - sleep++; - } + igndbg << "Release D" << std::endl; + this->yawVel = 0.0; + this->KeyEvent(false, Qt::Key_D); - EXPECT_TRUE(received); - received = false; + igndbg << "Press A" << std::endl; + this->yawVel = this->kMaxYawVel; + this->KeyEvent(true, Qt::Key_A); - // Generates a key release event on the main window. - QKeyEvent *keyrelease_D = new QKeyEvent(QKeyEvent::KeyRelease, - Qt::Key_D, Qt::NoModifier); - app.sendEvent(win->QuickWindow(), keyrelease_D); + igndbg << "Release A" << std::endl; + this->yawVel = 0.0; + this->KeyEvent(false, Qt::Key_A); - sleep = 0; - while (!received && sleep < maxSleep) - { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - QCoreApplication::processEvents(); - sleep++; - } + igndbg << "Release W" << std::endl; + this->forwardVel = 0.0; + this->KeyEvent(false, Qt::Key_W); - EXPECT_TRUE(received); - received = false; + igndbg << "Press S" << std::endl; + this->forwardVel = -this->kMaxForwardVel; + this->KeyEvent(true, Qt::Key_S); - // Generates a key press event on the main window. - QKeyEvent *keypress_A = new QKeyEvent(QKeyEvent::KeyPress, - Qt::Key_A, Qt::NoModifier); - app.sendEvent(win->QuickWindow(), keypress_A); + igndbg << "Release S" << std::endl; + this->forwardVel = 0.0; + this->KeyEvent(false, Qt::Key_S); - sleep = 0; - while (!received && sleep < maxSleep) - { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - QCoreApplication::processEvents(); - sleep++; - } + igndbg << "Press X" << std::endl; + this->KeyEvent(true, Qt::Key_X); - EXPECT_TRUE(received); - received = false; + igndbg << "Release X" << std::endl; + this->KeyEvent(false, Qt::Key_X); - // Generates a key release event on the main window. - QKeyEvent *keyrelease_A = new QKeyEvent(QKeyEvent::KeyRelease, - Qt::Key_A, Qt::NoModifier); - app.sendEvent(win->QuickWindow(), keyrelease_A); + igndbg << "Press Q" << std::endl; + this->verticalVel = this->kMaxVerticalVel; + this->KeyEvent(true, Qt::Key_Q); - sleep = 0; - while (!received && sleep < maxSleep) - { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - QCoreApplication::processEvents(); - sleep++; - } - - EXPECT_TRUE(received); - received = false; - // Generates a key release event on the main window. - QKeyEvent *keyrelease_W = new QKeyEvent(QKeyEvent::KeyRelease, - Qt::Key_W, Qt::NoModifier); - app.sendEvent(win->QuickWindow(), keyrelease_W); - - sleep = 0; - while (!received && sleep < maxSleep) - { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - QCoreApplication::processEvents(); - sleep++; - } - - EXPECT_TRUE(received); - received = false; - // Generates a key press event on the main window. - QKeyEvent *keypress_X = new QKeyEvent(QKeyEvent::KeyPress, - Qt::Key_X, Qt::NoModifier); - app.sendEvent(win->QuickWindow(), keypress_X); - - sleep = 0; - while (!received && sleep < maxSleep) - { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - QCoreApplication::processEvents(); - sleep++; - } - - EXPECT_TRUE(received); - received = false; - // Generates a key release event on the main window. - QKeyEvent *keyrelease_X = new QKeyEvent(QKeyEvent::KeyRelease, - Qt::Key_X, Qt::NoModifier); - app.sendEvent(win->QuickWindow(), keyrelease_X); - - sleep = 0; - while (!received && sleep < maxSleep) - { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - QCoreApplication::processEvents(); - sleep++; - } + igndbg << "Press E" << std::endl; + this->verticalVel = -this->kMaxVerticalVel; + this->KeyEvent(true, Qt::Key_E); - // Cleanup - plugins.clear(); + igndbg << "Release E" << std::endl; + this->verticalVel = 0.0; + this->KeyEvent(false, Qt::Key_E); } From 1610733783947c8224073db10d29116050860703 Mon Sep 17 00:00:00 2001 From: Mohamad Date: Wed, 27 Jul 2022 17:48:11 +0200 Subject: [PATCH 25/31] dialog read config first time hotfix (#442) Signed-off-by: Mohamad Co-authored-by: Mohamad --- src/Dialog.cc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Dialog.cc b/src/Dialog.cc index 78c45157a..ffe1a1110 100644 --- a/src/Dialog.cc +++ b/src/Dialog.cc @@ -145,11 +145,11 @@ std::string Dialog::ReadConfigAttribute(const std::string &_path, bool configExists{true}; std::string dialogName = this->objectName().toStdString(); - auto Value = [&_attribute, &doc, &dialogName]() + auto Value = [&_attribute, &dialogName](const tinyxml2::XMLDocument &_doc) { // Process each dialog // If multiple attributes share the same name, return the first one - for (auto dialogElem = doc.FirstChildElement("dialog"); + for (auto dialogElem = _doc.FirstChildElement("dialog"); dialogElem != nullptr; dialogElem = dialogElem->NextSiblingElement("dialog")) { @@ -169,7 +169,7 @@ std::string Dialog::ReadConfigAttribute(const std::string &_path, { configExists = false; doc.Parse(this->dataPtr->config.c_str()); - value = Value(); + value = Value(doc); } else { @@ -180,14 +180,14 @@ std::string Dialog::ReadConfigAttribute(const std::string &_path, << std::endl; return ""; } - value = Value(); + value = Value(doc); // config exists but attribute not there read from default config if (value.empty()) { tinyxml2::XMLDocument missingDoc; missingDoc.Parse(this->dataPtr->config.c_str()); - value = Value(); + value = Value(missingDoc); missingDoc.Print(&defaultPrinter); } } From 04814f9b17f1da8488ed4b1b1ab0376ba22f8f7b Mon Sep 17 00:00:00 2001 From: Nate Koenig Date: Thu, 28 Jul 2022 12:32:30 -0700 Subject: [PATCH 26/31] cmake2 2.14 Signed-off-by: Nate Koenig --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index faf1cf371..54f490a86 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ project(ignition-gui3 VERSION 3.10.0) #============================================================================ # Find ignition-cmake #============================================================================ -find_package(ignition-cmake2 2.13 REQUIRED) +find_package(ignition-cmake2 2.14 REQUIRED) #============================================================================ # Configure the project From b543beb236e4ce880952d0f585f278640322f91e Mon Sep 17 00:00:00 2001 From: Jenn Nguyen Date: Tue, 2 Aug 2022 09:05:07 -0700 Subject: [PATCH 27/31] Fixed topic echo test (#448) Signed-off-by: Jenn Nguyen --- src/plugins/topic_echo/CMakeLists.txt | 3 +- src/plugins/topic_echo/TopicEcho.cc | 1 - src/plugins/topic_echo/TopicEcho.hh | 12 +- src/plugins/topic_echo/TopicEcho.qml | 5 + src/plugins/topic_echo/TopicEcho_TEST.cc | 272 +++++++++++++---------- 5 files changed, 173 insertions(+), 120 deletions(-) diff --git a/src/plugins/topic_echo/CMakeLists.txt b/src/plugins/topic_echo/CMakeLists.txt index 2073e501a..8460b96dd 100644 --- a/src/plugins/topic_echo/CMakeLists.txt +++ b/src/plugins/topic_echo/CMakeLists.txt @@ -4,6 +4,5 @@ ign_gui_add_plugin(TopicEcho QT_HEADERS TopicEcho.hh TEST_SOURCES - # TopicEcho_TEST.cc + TopicEcho_TEST.cc ) - diff --git a/src/plugins/topic_echo/TopicEcho.cc b/src/plugins/topic_echo/TopicEcho.cc index 629e2955c..a149a6f69 100644 --- a/src/plugins/topic_echo/TopicEcho.cc +++ b/src/plugins/topic_echo/TopicEcho.cc @@ -180,4 +180,3 @@ void TopicEcho::SetPaused(const bool &_paused) // Register this plugin IGNITION_ADD_PLUGIN(ignition::gui::plugins::TopicEcho, ignition::gui::Plugin) - diff --git a/src/plugins/topic_echo/TopicEcho.hh b/src/plugins/topic_echo/TopicEcho.hh index 9a53638c0..e9fd62252 100644 --- a/src/plugins/topic_echo/TopicEcho.hh +++ b/src/plugins/topic_echo/TopicEcho.hh @@ -26,6 +26,16 @@ #pragma warning(pop) #endif +#ifndef _WIN32 +# define TopicEcho_EXPORTS_API +#else +# if (defined(TopicEcho_EXPORTS)) +# define TopicEcho_EXPORTS_API __declspec(dllexport) +# else +# define TopicEcho_EXPORTS_API __declspec(dllimport) +# endif +#endif + #include #include "ignition/gui/Plugin.hh" @@ -42,7 +52,7 @@ namespace plugins /// /// ## Configuration /// This plugin doesn't accept any custom configuration. - class TopicEcho : public Plugin + class TopicEcho_EXPORTS_API TopicEcho : public Plugin { Q_OBJECT diff --git a/src/plugins/topic_echo/TopicEcho.qml b/src/plugins/topic_echo/TopicEcho.qml index aef2cd3a5..ceef81322 100644 --- a/src/plugins/topic_echo/TopicEcho.qml +++ b/src/plugins/topic_echo/TopicEcho.qml @@ -40,12 +40,14 @@ Rectangle { TextField { id: topicField + objectName: "topicField" text: TopicEcho.topic selectByMouse: true } } Switch { + objectName: "echoSwitch" text: qsTr("Echo") onToggled: { TopicEcho.topic = topicField.text @@ -64,6 +66,7 @@ Rectangle { SpinBox { id: bufferField + objectName: "bufferField" value: 10 onValueChanged: { TopicEcho.OnBuffer(value) @@ -71,6 +74,7 @@ Rectangle { } CheckBox { + objectName: "pauseCheck" text: qsTr("Pause") checked: TopicEcho.paused onClicked: { @@ -90,6 +94,7 @@ Rectangle { ListView { id: listView + objectName: "listView" clip: true anchors.fill: parent diff --git a/src/plugins/topic_echo/TopicEcho_TEST.cc b/src/plugins/topic_echo/TopicEcho_TEST.cc index dd28ebd6c..c3cbbc8ff 100644 --- a/src/plugins/topic_echo/TopicEcho_TEST.cc +++ b/src/plugins/topic_echo/TopicEcho_TEST.cc @@ -16,224 +16,264 @@ */ #include - -#include +#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 "TopicEcho.hh" + +int g_argc = 1; +char* g_argv[] = +{ + reinterpret_cast(const_cast("./TopicEcho_TEST")), +}; using namespace ignition; using namespace gui; ///////////////////////////////////////////////// -TEST(TopicEchoTest, Load) +TEST(TopicEchoTest, 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("TopicEcho")); + + // Get main window + auto win = app.findChild(); + ASSERT_NE(win, nullptr); + + // Get plugin + auto plugins = win->findChildren(); + EXPECT_EQ(plugins.size(), 1); - EXPECT_TRUE(loadPlugin("TopicEcho")); + auto plugin = plugins[0]; + EXPECT_EQ(plugin->Title(), "Topic echo"); - EXPECT_TRUE(stop()); + // Cleanup + plugins.clear(); } ///////////////////////////////////////////////// -TEST(TopicEchoTest, Echo) +TEST(TopicEchoTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(Echo)) { - 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("TopicEcho")); + EXPECT_TRUE(app.LoadPlugin("TopicEcho")); - // 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(), "Topic echo"); // Widgets - auto echoButton = plugin->findChild("echoButton"); - EXPECT_TRUE(echoButton != nullptr); - EXPECT_EQ(echoButton->text(), "Echo"); - - auto topicEdit = plugin->findChild("topicEdit"); - EXPECT_TRUE(topicEdit != nullptr); - EXPECT_EQ(topicEdit->text(), "/echo"); - - auto msgList = plugin->findChild("msgList"); - EXPECT_TRUE(msgList != nullptr); - EXPECT_EQ(msgList->count(), 0); - - auto bufferSpin = plugin->findChild("bufferSpin"); - EXPECT_TRUE(bufferSpin != nullptr); - EXPECT_EQ(bufferSpin->value(), 10); - - auto pauseCheck = plugin->findChild("pauseCheck"); - EXPECT_TRUE(pauseCheck != nullptr); - EXPECT_FALSE(pauseCheck->isChecked()); + auto echoSwitch = plugin->PluginItem()->findChild("echoSwitch"); + ASSERT_NE(echoSwitch, nullptr); + QVariant objProp = echoSwitch->property("text"); + EXPECT_TRUE(objProp.isValid()); + EXPECT_EQ(objProp.toString().toStdString(), "Echo"); + + auto msgList = plugin->PluginItem()->findChild("listView"); + ASSERT_NE(msgList, nullptr); + objProp = msgList->property("model"); + EXPECT_TRUE(objProp.isValid()); + auto msgStringList = objProp.value(); + ASSERT_NE(msgStringList, nullptr); + EXPECT_EQ(msgStringList->rowCount(), 0); + + auto bufferField = plugin->PluginItem()->findChild("bufferField"); + ASSERT_NE(bufferField, nullptr); + auto bufferProp = bufferField->property("value"); + EXPECT_TRUE(bufferProp.isValid()); + EXPECT_EQ(bufferProp.toInt(), 10); + + auto pauseCheck = plugin->PluginItem()->findChild("pauseCheck"); + ASSERT_NE(pauseCheck, nullptr); + auto pauseProp = pauseCheck->property("checked"); + EXPECT_TRUE(pauseProp.isValid()); + EXPECT_FALSE(pauseProp.toBool()); + EXPECT_FALSE(plugin->Paused()); // Start echoing - echoButton->click(); - EXPECT_EQ(echoButton->text(), "Stop echoing"); + plugin->OnEcho(true); // Publish string transport::Node node; auto pub = node.Advertise("/echo"); - - { - msgs::StringMsg msg; - msg.set_data("example string"); - pub.Publish(msg); - } + msgs::StringMsg msg; + msg.set_data("example string"); + pub.Publish(msg); int sleep = 0; int maxSleep = 30; - while (msgList->count() == 0 && sleep < maxSleep) + while(msgStringList->rowCount() == 0 && sleep < maxSleep) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); QCoreApplication::processEvents(); - sleep++; + ++sleep; } // Check message was echoed - ASSERT_EQ(msgList->count(), 1); - EXPECT_EQ(msgList->item(0)->text(), QString("data: \"example string\"\n")) - << msgList->item(0)->text().toStdString(); + ASSERT_EQ(msgStringList->rowCount(), 1); + EXPECT_EQ(msgStringList->stringList()[0].toStdString(), + "data: \"example string\"\n"); // Publish more than buffer size (messages numbered 0 to 14) - for (auto i = 0; i < bufferSpin->value() + 5; ++i) + for (auto i = 0; i < bufferProp.toInt() + 5; ++i) { - msgs::StringMsg msg; + msg.Clear(); msg.set_data("many messages: " + std::to_string(i)); pub.Publish(msg); } + QRegExp regExp13("*13"); + regExp13.setPatternSyntax(QRegExp::Wildcard); + QRegExp regExp14("*14"); + regExp14.setPatternSyntax(QRegExp::Wildcard); + // Wait until all 15 messages are received // To avoid flakiness due to messages coming out of order, we check for both // 13 and 14. There's a chance a lower number comes afterwards, but that's // just bad luck. sleep = 0; - while (msgList->findItems(QString::number(13), Qt::MatchContains).count() == 0 - && msgList->findItems(QString::number(14), Qt::MatchContains).count() == 0 + while (msgStringList->stringList().filter(regExp13).count() == 0 + && msgStringList->stringList().filter(regExp14).count() == 0 && sleep < maxSleep) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); QCoreApplication::processEvents(); - sleep++; + ++sleep; } EXPECT_LT(sleep, maxSleep); // Check we have only 10 messages listed - ASSERT_EQ(msgList->count(), 10); + ASSERT_EQ(msgStringList->rowCount(), 10); // We can't guarantee the order of messages // We expect that out of the 10 messages last, at least 6 belong to the [5-14] // range + QRegExp regExp; + regExp.setPatternSyntax(QRegExp::Wildcard); unsigned int count = 0; for (auto i = 5; i < 15; ++i) { - if (msgList->findItems(QString::number(i), Qt::MatchContains).count() > 0) - count++; + regExp.setPattern("*" + QString::number(i)); + if (msgStringList->stringList().filter(regExp).count() > 0) + ++count; } EXPECT_GE(count, 6u); // Increase buffer - bufferSpin->setValue(20); + bufferField->setProperty("value", 20); + bufferProp = bufferField->property("value"); + EXPECT_TRUE(bufferProp.isValid()); + EXPECT_EQ(bufferProp.toInt(), 20); // Publish another message and now it fits - { - msgs::StringMsg msg; - msg.set_data("new message"); - pub.Publish(msg); - } + msg.Clear(); + msg.set_data("new message"); + pub.Publish(msg); sleep = 0; - while (msgList->count() < 11 && sleep < maxSleep) + while (msgStringList->rowCount() < 11 && sleep < maxSleep) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); QCoreApplication::processEvents(); - sleep++; + ++sleep; } // We have 11 messages - ASSERT_EQ(msgList->count(), 11); + ASSERT_EQ(msgStringList->rowCount(), 11); // The last one is guaranteed to be the new message - EXPECT_EQ(msgList->item(10)->text(), QString("data: \"new message\"\n")) - << msgList->item(10)->text().toStdString(); + EXPECT_EQ(msgStringList->stringList().last().toStdString(), + "data: \"new message\"\n") + << msgStringList->stringList().last().toStdString(); // Pause - pauseCheck->click(); + plugin->SetPaused(true); + pauseProp = pauseCheck->property("checked"); + EXPECT_TRUE(pauseProp.toBool()); - // Publish another message and it is not received - { - msgs::StringMsg msg; - msg.set_data("dropped message"); - pub.Publish(msg); - } + // Publish another message and check it is not received + msg.Clear(); + msg.set_data("dropped message"); + pub.Publish(msg); sleep = 0; - while (msgList->count() < 11 && sleep < maxSleep) + while (sleep < maxSleep) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); QCoreApplication::processEvents(); - sleep++; + ++sleep; } - - ASSERT_EQ(msgList->count(), 11); - EXPECT_EQ(msgList->item(10)->text(), QString("data: \"new message\"\n")) - << msgList->item(10)->text().toStdString(); + ASSERT_EQ(msgStringList->rowCount(), 11); + EXPECT_EQ(msgStringList->stringList().last().toStdString(), + "data: \"new message\"\n") + << msgStringList->stringList().last().toStdString(); // Decrease buffer - bufferSpin->setValue(5); - - // Check we have less messages - ASSERT_EQ(msgList->count(), 5); + bufferField->setProperty("value", 5); + bufferProp = bufferField->property("value"); + EXPECT_TRUE(bufferProp.isValid()); + EXPECT_EQ(bufferProp.toInt(), 5); - // The last message is still the new one - EXPECT_EQ(msgList->item(4)->text(), QString("data: \"new message\"\n")) - << msgList->item(4)->text().toStdString(); - - // Stop echoing - echoButton->click(); - EXPECT_EQ(echoButton->text(), "Echo"); - ASSERT_EQ(msgList->count(), 0); + // Publish another message to decrease message list + plugin->SetPaused(false); + msg.Clear(); + msg.set_data("new message 2"); + pub.Publish(msg); sleep = 0; - while (msgList->count() == 0 && sleep < maxSleep) + while (msgStringList->rowCount() != 5 && sleep < maxSleep) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); QCoreApplication::processEvents(); - sleep++; + ++sleep; } - ASSERT_EQ(msgList->count(), 0); - - // Start echoing again - echoButton->click(); - EXPECT_EQ(echoButton->text(), "Stop echoing"); - - // Stop echoing by editing topic - topicEdit->setText("/another_topic"); - EXPECT_EQ(echoButton->text(), "Echo"); - ASSERT_EQ(msgList->count(), 0); + // Check we have less messages + ASSERT_EQ(msgStringList->rowCount(), 5); - sleep = 0; - while (msgList->count() == 0 && sleep < maxSleep) - { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - QCoreApplication::processEvents(); - sleep++; - } + // The last message is still the new one + EXPECT_EQ(msgStringList->stringList().last().toStdString(), + "data: \"new message 2\"\n") + << msgStringList->stringList().last().toStdString(); - ASSERT_EQ(msgList->count(), 0); + // Stop echoing + plugin->OnEcho(false); + EXPECT_EQ(msgStringList->rowCount(), 0); - EXPECT_TRUE(stop()); + // Cleanup + plugins.clear(); } - From 9f5c9c23a30633f377af1731b89f89b6be4a5f64 Mon Sep 17 00:00:00 2001 From: Louise Poubel Date: Tue, 2 Aug 2022 09:39:16 -0700 Subject: [PATCH 28/31] Dialog read attribute: don't create a file (#450) Signed-off-by: Louise Poubel --- Migration.md | 4 ++ include/ignition/gui/Dialog.hh | 6 +-- src/Dialog.cc | 88 +++++++------------------------- src/Dialog_TEST.cc | 93 ++++++++++++++++++---------------- 4 files changed, 74 insertions(+), 117 deletions(-) diff --git a/Migration.md b/Migration.md index d0550146d..4cfaf0201 100644 --- a/Migration.md +++ b/Migration.md @@ -5,6 +5,10 @@ Deprecated code produces compile-time warnings. These warning serve as notification to users that their code should be upgraded. The next major release will remove the deprecated code. +## Ignition GUI 3.10 to 3.11 + +* `Dialog::ReadConfigAttribute` doesn't create a missing file anymore. + ## Ignition GUI 3.6 to 3.7 * The `Application::PluginAdded` signal used to send empty strings. Now it diff --git a/include/ignition/gui/Dialog.hh b/include/ignition/gui/Dialog.hh index 7271c415b..e4d633472 100644 --- a/include/ignition/gui/Dialog.hh +++ b/include/ignition/gui/Dialog.hh @@ -69,9 +69,9 @@ namespace ignition const std::string &_path, const std::string &_attribute, const bool _value) const; - /// \brief Gets a config attribute value, if not found in config - /// write the default in the config and get it. - /// creates config file if it doesn't exist. + /// \brief Gets a config attribute value. + /// It will return an empty string if the config file or the attribute + /// don't exist. /// \param[in] _path config path /// \param[in] _attribute attribute name /// \return attribute value as string diff --git a/src/Dialog.cc b/src/Dialog.cc index ffe1a1110..25cc7e290 100644 --- a/src/Dialog.cc +++ b/src/Dialog.cc @@ -138,83 +138,33 @@ void Dialog::SetDefaultConfig(const std::string &_config) std::string Dialog::ReadConfigAttribute(const std::string &_path, const std::string &_attribute) const { - tinyxml2::XMLDocument doc; - std::string value {""}; - std::string config = "\n\n"; - tinyxml2::XMLPrinter defaultPrinter; - bool configExists{true}; - std::string dialogName = this->objectName().toStdString(); - - auto Value = [&_attribute, &dialogName](const tinyxml2::XMLDocument &_doc) - { - // Process each dialog - // If multiple attributes share the same name, return the first one - for (auto dialogElem = _doc.FirstChildElement("dialog"); - dialogElem != nullptr; - dialogElem = dialogElem->NextSiblingElement("dialog")) - { - if (dialogElem->Attribute("name") == dialogName) - { - if (dialogElem->Attribute(_attribute.c_str())) - return dialogElem->Attribute(_attribute.c_str()); - } - } - return ""; - }; - - // Check if the passed in config file exists. - // (If the default config path doesn't exist yet, it's expected behavior. - // It will be created the first time now.) if (!common::exists(_path)) { - configExists = false; - doc.Parse(this->dataPtr->config.c_str()); - value = Value(doc); + return std::string(); } - else - { - auto success = !doc.LoadFile(_path.c_str()); - if (!success) - { - ignerr << "Failed to load file [" << _path << "]: XMLError" - << std::endl; - return ""; - } - value = Value(doc); - // config exists but attribute not there read from default config - if (value.empty()) - { - tinyxml2::XMLDocument missingDoc; - missingDoc.Parse(this->dataPtr->config.c_str()); - value = Value(missingDoc); - missingDoc.Print(&defaultPrinter); - } - } - - // Write config file - tinyxml2::XMLPrinter printer; - doc.Print(&printer); - - // Don't write the xml version decleration if file exists - if (configExists) + tinyxml2::XMLDocument doc; + auto success = !doc.LoadFile(_path.c_str()); + if (!success) { - config = ""; + ignerr << "Failed to load file [" << _path << "]: XMLError" + << std::endl; + return std::string(); } - igndbg << "Setting dialog " << this->objectName().toStdString() - << " default config." << std::endl; - config += printer.CStr(); - config += defaultPrinter.CStr(); - std::ofstream out(_path.c_str(), std::ios::out); - if (!out) + // Process each dialog + // If multiple attributes share the same name, return the first one + std::string dialogName = this->objectName().toStdString(); + for (auto dialogElem = doc.FirstChildElement("dialog"); + dialogElem != nullptr; + dialogElem = dialogElem->NextSiblingElement("dialog")) { - ignerr << "Unable to open file: " << _path - << ".\nCheck file permissions.\n"; - return ""; + if (dialogElem->Attribute("name") == dialogName && + dialogElem->Attribute(_attribute.c_str())) + { + return dialogElem->Attribute(_attribute.c_str()); + } } - else - out << config; - return value; + return std::string(); } diff --git a/src/Dialog_TEST.cc b/src/Dialog_TEST.cc index c52b46a12..919395f2b 100644 --- a/src/Dialog_TEST.cc +++ b/src/Dialog_TEST.cc @@ -33,47 +33,45 @@ char* g_argv[] = using namespace ignition; using namespace gui; -using namespace std::chrono_literals; ///////////////////////////////////////////////// TEST(DialogTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(UpdateDialogConfig)) { - ignition::common::Console::SetVerbosity(4); - Application app(g_argc, g_argv, ignition::gui::WindowType::kDialog); - - // Change default config path - App()->SetDefaultConfigPath(kTestConfigFile); + common::Console::SetVerbosity(4); + Application app(g_argc, g_argv, WindowType::kDialog); auto dialog = new Dialog; ASSERT_NE(nullptr, dialog); + dialog->setObjectName("quick_menu"); + + // Start without a file + std::remove(kTestConfigFile.c_str()); - // Read attribute value when the default the config is not set + // Read attribute value when the config doesn't exist { EXPECT_FALSE(common::exists(kTestConfigFile)); - std::string allow = dialog->ReadConfigAttribute(app.DefaultConfigPath(), + std::string allow = dialog->ReadConfigAttribute(kTestConfigFile, "allow"); - EXPECT_EQ(allow, ""); - - // Config file is created when a read is attempted - EXPECT_TRUE(common::exists(kTestConfigFile)); + EXPECT_TRUE(allow.empty()); - // Delete file - std::remove(kTestConfigFile.c_str()); + // Config file still doesn't exist + EXPECT_FALSE(common::exists(kTestConfigFile)); } // Read a non existing attribute { EXPECT_FALSE(common::exists(kTestConfigFile)); - dialog->setObjectName("quick_menu"); - dialog->SetDefaultConfig(std::string( - "")); - std::string allow = dialog->ReadConfigAttribute(app.DefaultConfigPath(), - "allow"); - EXPECT_EQ(allow, ""); - // Config file is created when a read is attempted + // Create file + std::ofstream configFile(kTestConfigFile); + configFile << ""; + configFile.close(); EXPECT_TRUE(common::exists(kTestConfigFile)); + std::string allow = dialog->ReadConfigAttribute(kTestConfigFile, + "allow"); + EXPECT_TRUE(allow.empty()); + // Delete file std::remove(kTestConfigFile.c_str()); } @@ -81,13 +79,17 @@ TEST(DialogTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(UpdateDialogConfig)) // Read an existing attribute { EXPECT_FALSE(common::exists(kTestConfigFile)); - std::string show = dialog->ReadConfigAttribute(app.DefaultConfigPath(), - "show"); - EXPECT_EQ(show, "true"); - // Config file is created when a read is attempted + // Create file + std::ofstream configFile(kTestConfigFile); + configFile << ""; + configFile.close(); EXPECT_TRUE(common::exists(kTestConfigFile)); + std::string show = dialog->ReadConfigAttribute(kTestConfigFile, + "show"); + EXPECT_EQ(show, "true"); + // Delete file std::remove(kTestConfigFile.c_str()); } @@ -96,19 +98,18 @@ TEST(DialogTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(UpdateDialogConfig)) { EXPECT_FALSE(common::exists(kTestConfigFile)); - // Call a read to create config file - std::string allow = dialog->ReadConfigAttribute(app.DefaultConfigPath(), - "allow"); + // Create file + std::ofstream configFile(kTestConfigFile); + configFile << ""; + configFile.close(); + EXPECT_TRUE(common::exists(kTestConfigFile)); - // Empty string for a non existing attribute - EXPECT_EQ(allow, ""); - dialog->UpdateConfigAttribute(app.DefaultConfigPath(), "allow", true); - allow = dialog->ReadConfigAttribute(app.DefaultConfigPath(), - "allow"); - EXPECT_EQ(allow, "true"); + // Update value + dialog->UpdateConfigAttribute(kTestConfigFile, "allow", true); - // Config file is created when a read is attempted - EXPECT_TRUE(common::exists(kTestConfigFile)); + // Read value + auto allow = dialog->ReadConfigAttribute(kTestConfigFile, "allow"); + EXPECT_EQ(allow, "true"); // Delete file std::remove(kTestConfigFile.c_str()); @@ -118,17 +119,19 @@ TEST(DialogTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(UpdateDialogConfig)) { EXPECT_FALSE(common::exists(kTestConfigFile)); - // Call a read to create config file - std::string allow = dialog->ReadConfigAttribute(app.DefaultConfigPath(), - "allow"); - dialog->UpdateConfigAttribute(app.DefaultConfigPath(), "allow", false); - allow = dialog->ReadConfigAttribute(app.DefaultConfigPath(), - "allow"); - EXPECT_EQ(allow, "false"); - - // Config file is created when a read is attempted + // Create file + std::ofstream configFile(kTestConfigFile); + configFile << ""; + configFile.close(); EXPECT_TRUE(common::exists(kTestConfigFile)); + // Update value + dialog->UpdateConfigAttribute(kTestConfigFile, "allow", false); + + // Read value + auto allow = dialog->ReadConfigAttribute(kTestConfigFile, "allow"); + EXPECT_EQ(allow, "false"); + // Delete file std::remove(kTestConfigFile.c_str()); } From 33bbf41101f1d3bc46739e482bc96914be2264ba Mon Sep 17 00:00:00 2001 From: Louise Poubel Date: Tue, 2 Aug 2022 10:16:42 -0700 Subject: [PATCH 29/31] =?UTF-8?q?=F0=9F=8E=88=203.11.0=20(#451)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Louise Poubel --- CMakeLists.txt | 2 +- Changelog.md | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 54f490a86..af45259ce 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.10.0) +project(ignition-gui3 VERSION 3.11.0) #============================================================================ # Find ignition-cmake diff --git a/Changelog.md b/Changelog.md index ff790cb63..13bc350f5 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,4 +1,22 @@ -## Ignition Gui 3 +## Gazebo GUI 3 + +### Gazebo GUI 3.11.0 (2022-08-02) + +1. Dialog read attribute fixes + * [Pull request #450](https://github.com/gazebosim/gz-gui/pull/450) + * [Pull request #442](https://github.com/gazebosim/gz-gui/pull/442) + +1. Fixed topic echo test + * [Pull request #448](https://github.com/gazebosim/gz-gui/pull/448) + +1. Teleop: Refactor and support vertical + * [Pull request #440](https://github.com/gazebosim/gz-gui/pull/440) + +1. Change `IGN_DESIGNATION` to `GZ_DESIGNATION` + * [Pull request #437](https://github.com/gazebosim/gz-gui/pull/437) + +1. Ignition -> Gazebo + * [Pull request #435](https://github.com/gazebosim/gz-gui/pull/435) ### Ignition Gui 3.10.0 (2022-07-13) From 6059daacf60fcadfaee952806a710342eb4f1b2e Mon Sep 17 00:00:00 2001 From: Louise Poubel Date: Tue, 2 Aug 2022 14:35:48 -0700 Subject: [PATCH 30/31] Dialog_TEST and TopicEcho_TEST to gz (#446) Signed-off-by: Louise Poubel --- src/Dialog_TEST.cc | 16 ++++++++-------- src/plugins/topic_echo/TopicEcho_TEST.cc | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Dialog_TEST.cc b/src/Dialog_TEST.cc index 919395f2b..45af6071e 100644 --- a/src/Dialog_TEST.cc +++ b/src/Dialog_TEST.cc @@ -17,25 +17,25 @@ #include -#include -#include +#include +#include -#include "test_config.h" // NOLINT(build/include) -#include "ignition/gui/Application.hh" -#include "ignition/gui/Dialog.hh" +#include "test_config.hh" // NOLINT(build/include) +#include "gz/gui/Application.hh" +#include "gz/gui/Dialog.hh" -std::string kTestConfigFile = "/tmp/ign-gui-test.config"; // NOLINT(*) +std::string kTestConfigFile = "/tmp/gz-gui-test.config"; // NOLINT(*) int g_argc = 1; char* g_argv[] = { reinterpret_cast(const_cast("./Dialog_TEST")), }; -using namespace ignition; +using namespace gz; using namespace gui; ///////////////////////////////////////////////// -TEST(DialogTest, IGN_UTILS_TEST_DISABLED_ON_WIN32(UpdateDialogConfig)) +TEST(DialogTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(UpdateDialogConfig)) { common::Console::SetVerbosity(4); Application app(g_argc, g_argv, WindowType::kDialog); diff --git a/src/plugins/topic_echo/TopicEcho_TEST.cc b/src/plugins/topic_echo/TopicEcho_TEST.cc index 769d8e12a..f1f3aea3d 100644 --- a/src/plugins/topic_echo/TopicEcho_TEST.cc +++ b/src/plugins/topic_echo/TopicEcho_TEST.cc @@ -29,12 +29,12 @@ #include #include #include -#include +#include #include "gz/gui/Application.hh" #include "gz/gui/MainWindow.hh" #include "gz/gui/Plugin.hh" -#include "test_config.h" // NOLINT(build/include) +#include "test_config.hh" // NOLINT(build/include) #include "TopicEcho.hh" int g_argc = 1; From c7cac24e72c8dfc3aa8e3b873c0fcaada9c8adbd Mon Sep 17 00:00:00 2001 From: Louise Poubel Date: Thu, 4 Aug 2022 08:34:42 -0700 Subject: [PATCH 31/31] Set HFOV instead of AspectRatio when texture is dirty (#446) Signed-off-by: Louise Poubel --- src/plugins/minimal_scene/MinimalScene.cc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/plugins/minimal_scene/MinimalScene.cc b/src/plugins/minimal_scene/MinimalScene.cc index bd466d278..eb426cb4c 100644 --- a/src/plugins/minimal_scene/MinimalScene.cc +++ b/src/plugins/minimal_scene/MinimalScene.cc @@ -315,8 +315,7 @@ void GzRenderer::Render(RenderSync *_renderSync) // _renderSync->WaitForQtThreadAndBlock(lock); this->dataPtr->camera->SetImageWidth(this->textureSize.width()); this->dataPtr->camera->SetImageHeight(this->textureSize.height()); - this->dataPtr->camera->SetAspectRatio(this->textureSize.width() / - this->textureSize.height()); + this->dataPtr->camera->SetHFOV(this->cameraHFOV); // setting the size should cause the render texture to be rebuilt this->dataPtr->camera->PreRender(); this->textureDirty = false;