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; +}