From 297d565f104a73f3f1cc5d1bdb973b6b1a135e11 Mon Sep 17 00:00:00 2001 From: Martin Pecka Date: Sat, 15 Jan 2022 01:46:30 +0100 Subject: [PATCH] Added a button that allows shutting down both the client and server. (#335) Signed-off-by: Martin Pecka Co-authored-by: Jenn Nguyen Co-authored-by: Louise Poubel --- .../config/dialog_on_exit_shutdown.config | 29 ++ include/ignition/gui/MainWindow.hh | 145 ++++++++ include/ignition/gui/qml/Main.qml | 67 +++- src/Application.cc | 79 +++++ src/MainWindow.cc | 164 +++++++++ src/MainWindow_TEST.cc | 334 ++++++++++++++++++ src/plugins/CMakeLists.txt | 1 + src/plugins/shutdown_button/CMakeLists.txt | 9 + src/plugins/shutdown_button/ShutdownButton.cc | 53 +++ src/plugins/shutdown_button/ShutdownButton.hh | 60 ++++ .../shutdown_button/ShutdownButton.qml | 57 +++ .../shutdown_button/ShutdownButton.qrc | 5 + .../shutdown_button/ShutdownButton_TEST.cc | 159 +++++++++ src/plugins/shutdown_button/test.config | 13 + test/config/close_dialog_auto_gui_only.config | 6 + test/config/close_dialog_auto_shutdown.config | 6 + test/config/close_dialog_buttons.config | 8 + test/config/close_dialog_buttons_text.config | 10 + ...lose_dialog_custom_shutdown_service.config | 7 + .../close_dialog_default_buttons.config | 5 + tutorials/04_layout.md | 30 +- 21 files changed, 1239 insertions(+), 8 deletions(-) create mode 100644 examples/config/dialog_on_exit_shutdown.config create mode 100644 src/plugins/shutdown_button/CMakeLists.txt create mode 100644 src/plugins/shutdown_button/ShutdownButton.cc create mode 100644 src/plugins/shutdown_button/ShutdownButton.hh create mode 100644 src/plugins/shutdown_button/ShutdownButton.qml create mode 100644 src/plugins/shutdown_button/ShutdownButton.qrc create mode 100644 src/plugins/shutdown_button/ShutdownButton_TEST.cc create mode 100644 src/plugins/shutdown_button/test.config create mode 100644 test/config/close_dialog_auto_gui_only.config create mode 100644 test/config/close_dialog_auto_shutdown.config create mode 100644 test/config/close_dialog_buttons.config create mode 100644 test/config/close_dialog_buttons_text.config create mode 100644 test/config/close_dialog_custom_shutdown_service.config create mode 100644 test/config/close_dialog_default_buttons.config diff --git a/examples/config/dialog_on_exit_shutdown.config b/examples/config/dialog_on_exit_shutdown.config new file mode 100644 index 000000000..d3156c89c --- /dev/null +++ b/examples/config/dialog_on_exit_shutdown.config @@ -0,0 +1,29 @@ + + + + + 1000 + 845 + SHUTDOWN_SERVER + true + + really? + true + true + Quit Server and GUI + + Quit GUI only + + + + + + Shutdown + 500 + 950 + 180 + 240 + + + diff --git a/include/ignition/gui/MainWindow.hh b/include/ignition/gui/MainWindow.hh index ea956e21e..300e54d72 100644 --- a/include/ignition/gui/MainWindow.hh +++ b/include/ignition/gui/MainWindow.hh @@ -39,9 +39,22 @@ namespace ignition { namespace gui { + Q_NAMESPACE class MainWindowPrivate; struct WindowConfig; + /// \brief The action executed when GUI is closed without prompt. + enum class ExitAction + { + /// \brief Close GUI and leave server running + CLOSE_GUI, + /// \brief Close GUI and shutdown server + SHUTDOWN_SERVER, + }; + /// \cond DO_NOT_DOCUMENT + Q_ENUM_NS(ExitAction) + /// \endcond + /// \brief The main window class creates a QQuickWindow and acts as an /// interface which provides properties and functions which can be called /// from Main.qml @@ -177,6 +190,14 @@ namespace ignition NOTIFY ShowPluginMenuChanged ) + /// \brief Flag to enable confirmation dialog on exit + Q_PROPERTY( + ExitAction defaultExitAction + READ DefaultExitAction + WRITE SetDefaultExitAction + NOTIFY DefaultExitActionChanged + ) + /// \brief Flag to enable confirmation dialog on exit Q_PROPERTY( bool showDialogOnExit @@ -185,6 +206,46 @@ namespace ignition NOTIFY ShowDialogOnExitChanged ) + /// \brief Text of the prompt in confirmation dialog on exit + Q_PROPERTY( + QString dialogOnExitText + READ DialogOnExitText + WRITE SetDialogOnExitText + NOTIFY DialogOnExitTextChanged + ) + + /// \brief Flag to show "shutdown" button in confirmation dialog on exit + Q_PROPERTY( + bool exitDialogShowShutdown + READ ExitDialogShowShutdown + WRITE SetExitDialogShowShutdown + NOTIFY ExitDialogShowShutdownChanged + ) + + /// \brief Flag to show "close GUI" button in confirmation dialog on exit + Q_PROPERTY( + bool exitDialogShowCloseGui + READ ExitDialogShowCloseGui + WRITE SetExitDialogShowCloseGui + NOTIFY ExitDialogShowCloseGuiChanged + ) + + /// \brief Text of the "shutdown" button in confirmation dialog on exit + Q_PROPERTY( + QString exitDialogShutdownText + READ ExitDialogShutdownText + WRITE SetExitDialogShutdownText + NOTIFY ExitDialogShutdownTextChanged + ) + + /// \brief Text of the "Close GUI" button in confirmation dialog on exit + Q_PROPERTY( + QString exitDialogCloseGuiText + READ ExitDialogCloseGuiText + WRITE SetExitDialogCloseGuiText + NOTIFY ExitDialogCloseGuiTextChanged + ) + /// \brief Constructor public: MainWindow(); @@ -352,6 +413,15 @@ namespace ignition /// \param[in] _showPluginMenu True to show. public: Q_INVOKABLE void SetShowPluginMenu(const bool _showPluginMenu); + /// \brief Get the action performed when GUI closes without prompt. + /// \return The action. + public: Q_INVOKABLE ExitAction DefaultExitAction() const; + + /// \brief Set the action performed when GUI closes without prompt. + /// \param[in] _defaultExitAction The action. + public: Q_INVOKABLE void SetDefaultExitAction( + enum ExitAction _defaultExitAction); + /// \brief Get the flag to show the plugin menu. /// \return True to show. public: Q_INVOKABLE bool ShowDialogOnExit() const; @@ -360,6 +430,60 @@ namespace ignition /// \param[in] _showDialogOnExit True to show. public: Q_INVOKABLE void SetShowDialogOnExit(bool _showDialogOnExit); + /// \brief Get the text of prompt in exit dialog. + /// \return Prompt text. + public: Q_INVOKABLE QString DialogOnExitText() const; + + /// \brief Set the text of the prompt in exit dialog. + /// \param[in] _dialogOnExitText Prompt text. + public: Q_INVOKABLE void SetDialogOnExitText( + const QString &_dialogOnExitText); + + /// \brief Get the flag to show "shutdown" button in exit dialog. + /// \return True to show. + public: Q_INVOKABLE bool ExitDialogShowShutdown() const; + + /// \brief Set the flag to show "shutdown" button in exit dialog. + /// \param[in] _exitDialogShowShutdown True to show. + public: Q_INVOKABLE void SetExitDialogShowShutdown( + bool _exitDialogShowShutdown); + + /// \brief Get the flag to show "Close GUI" button in exit dialog. + /// \return True to show. + public: Q_INVOKABLE bool ExitDialogShowCloseGui() const; + + /// \brief Set the flag to show "Close GUI" button in exit dialog. + /// \param[in] _exitDialogShowCloseGui True to show. + public: Q_INVOKABLE void SetExitDialogShowCloseGui( + bool _exitDialogShowCloseGui); + + /// \brief Get the text of the "shutdown" button in exit dialog. + /// \return Button text. + public: Q_INVOKABLE QString ExitDialogShutdownText() const; + + /// \brief Set the text of the "shutdown" button in exit dialog. + /// \param[in] _exitDialogShutdownText Button text. + public: Q_INVOKABLE void SetExitDialogShutdownText( + const QString &_exitDialogShutdownText); + + /// \brief Get the text of the "Close GUI" button in exit dialog. + /// \return Button text. + public: Q_INVOKABLE QString ExitDialogCloseGuiText() const; + + /// \brief Set the text of the "Close GUI" button in exit dialog. + /// \param[in] _exitDialogCloseGuiText Button text. + public: Q_INVOKABLE void SetExitDialogCloseGuiText( + const QString &_exitDialogCloseGuiText); + + /// \brief Get the topic of the server control service. + /// \return The service topic. + public: Q_INVOKABLE std::string ServerControlService() const; + + /// \brief Set the topic of the server control service. + /// \param[in] _service The service topic. + public: Q_INVOKABLE void SetServerControlService( + const std::string &_service); + /// \brief Callback when load configuration is selected public slots: void OnLoadConfig(const QString &_path); @@ -369,6 +493,9 @@ namespace ignition /// \brief Callback when "save configuration as" is selected public slots: void OnSaveConfigAs(const QString &_path); + /// \brief Callback when "shutdown simulation" is called + public slots: void OnStopServer(); + /// \brief Notifies when the number of plugins has changed. signals: void PluginCountChanged(); @@ -414,9 +541,27 @@ namespace ignition /// \brief Notifies when the show menu flag has changed. signals: void ShowPluginMenuChanged(); + /// \brief Notifies when the defaultExitAction has changed. + signals: void DefaultExitActionChanged(); + /// \brief Notifies when the showDialogOnExit flag has changed. signals: void ShowDialogOnExitChanged(); + /// \brief Notifies when dialogOnExitText has changed. + signals: void DialogOnExitTextChanged(); + + /// \brief Notifies when the exitDialogShowShutdown flag has changed. + signals: void ExitDialogShowShutdownChanged(); + + /// \brief Notifies when the exitDialogShowCloseGui flag has changed. + signals: void ExitDialogShowCloseGuiChanged(); + + /// \brief Notifies when exitDialogShutdownText has changed. + signals: void ExitDialogShutdownTextChanged(); + + /// \brief Notifies when exitDialogCloseGuiText has changed. + signals: void ExitDialogCloseGuiTextChanged(); + /// \brief Notifies when the window config has changed. signals: void configChanged(); diff --git a/include/ignition/gui/qml/Main.qml b/include/ignition/gui/qml/Main.qml index 0f1f3c00a..9d4f1b256 100644 --- a/include/ignition/gui/qml/Main.qml +++ b/include/ignition/gui/qml/Main.qml @@ -19,6 +19,7 @@ import QtQuick.Controls 2.2 import QtQuick.Controls.Material 2.1 import QtQuick.Dialogs 1.0 import QtQuick.Layouts 1.3 +import ExitAction 1.0 import "qrc:/qml" ApplicationWindow @@ -45,7 +46,19 @@ ApplicationWindow property string pluginToolBarTextColorLight: MainWindow.pluginToolBarTextColorLight property string pluginToolBarColorDark: MainWindow.pluginToolBarColorDark property string pluginToolBarTextColorDark: MainWindow.pluginToolBarTextColorDark + // Expose config properties to C++ + property int defaultExitAction: MainWindow.defaultExitAction property bool showDialogOnExit: MainWindow.showDialogOnExit + property string dialogOnExitText: MainWindow.dialogOnExitText + property bool exitDialogShowShutdown: MainWindow.exitDialogShowShutdown + property bool exitDialogShowCloseGui: MainWindow.exitDialogShowCloseGui + property string exitDialogShutdownText: MainWindow.exitDialogShutdownText + property string exitDialogCloseGuiText: MainWindow.exitDialogCloseGuiText + /** + * Flag to indicate if the close event was triggered by the close dialog. + */ + property bool closingFromDialog: false + /** * Tool bar background color */ @@ -75,7 +88,13 @@ ApplicationWindow onClosing: { close.accepted = !showDialogOnExit if(showDialogOnExit){ - confirmationDialogOnExit.open() + if (closingFromDialog) { + close.accepted = true; + } else { + confirmationDialogOnExit.open() + } + } else if (defaultExitAction == ExitAction.SHUTDOWN_SERVER) { + MainWindow.OnStopServer() } } @@ -325,24 +344,58 @@ ApplicationWindow } } + Timer { + id: timer + } + /** * Confirmation dialog on close button */ Dialog { id: confirmationDialogOnExit - title: "Do you really want to exit?" + title: (dialogOnExitText ? dialogOnExitText : "Do you really want to exit?") + objectName: "confirmationDialogOnExit" modal: true focus: true parent: ApplicationWindow.overlay - width: 300 + width: 500 x: (parent.width - width) / 2 y: (parent.height - height) / 2 closePolicy: Popup.CloseOnEscape - standardButtons: Dialog.Ok | Dialog.Cancel - - onAccepted: { - Qt.quit() + standardButtons: + (exitDialogShowCloseGui ? Dialog.Ok : Dialog.NoButton) | + (exitDialogShowShutdown ? Dialog.Discard : Dialog.NoButton) | + Dialog.Cancel + + // The button texts need to be changed later than in onCompleted as standardButtons change later + onAboutToShow: function () { + if (exitDialogShowCloseGui && exitDialogCloseGuiText) + footer.standardButton(Dialog.Ok).text = exitDialogCloseGuiText + if (exitDialogShowShutdown) + footer.standardButton(Dialog.Discard).text = + (exitDialogShutdownText ? exitDialogShutdownText : "Shutdown server and GUI") } + + footer: + DialogButtonBox { + onClicked: function (btn) { + if (btn == this.standardButton(Dialog.Ok)) { + closingFromDialog = true; + window.close(); + } + else if (btn == this.standardButton(Dialog.Discard)) { + MainWindow.OnStopServer() + // if GUI and server run in the same process, give server opportunity to kill the GUI + timer.interval = 100; + timer.repeat = false; + timer.triggered.connect(function() { + closingFromDialog = true; + window.close(); + }); + timer.start(); + } + } + } } } diff --git a/src/Application.cc b/src/Application.cc index 6ca02efd8..904713e8a 100644 --- a/src/Application.cc +++ b/src/Application.cc @@ -32,6 +32,8 @@ #include "ignition/gui/MainWindow.hh" #include "ignition/gui/Plugin.hh" +#include "ignition/transport/TopicUtils.hh" + namespace ignition { namespace gui @@ -271,12 +273,89 @@ bool Application::LoadConfig(const std::string &_config) this->dataPtr->windowConfig.MergeFromXML(std::string(printer.CStr())); // Closing behavior. + if (auto defaultExitActionElem = + winElem->FirstChildElement("default_exit_action")) + { + ExitAction action{ExitAction::CLOSE_GUI}; + const auto value = common::lowercase(defaultExitActionElem->GetText()); + if (value == "shutdown_server") + { + action = ExitAction::SHUTDOWN_SERVER; + } + else if (value != "close_gui" && !value.empty()) + { + ignwarn << "Value '" << value << "' of is " + << "invalid. Allowed values are CLOSE_GUI and SHUTDOWN_SERVER. " + << "Selecting CLOSE_GUI as fallback." << std::endl; + } + this->dataPtr->mainWin->SetDefaultExitAction(action); + } + + // Dialog on exit if (auto dialogOnExitElem = winElem->FirstChildElement("dialog_on_exit")) { bool showDialogOnExit{false}; dialogOnExitElem->QueryBoolText(&showDialogOnExit); this->dataPtr->mainWin->SetShowDialogOnExit(showDialogOnExit); } + + if (auto dialogOnExitOptionsElem = + winElem->FirstChildElement("dialog_on_exit_options")) + { + if (auto promptElem = + dialogOnExitOptionsElem->FirstChildElement("prompt_text")) + { + this->dataPtr->mainWin->SetDialogOnExitText( + QString::fromStdString(promptElem->GetText())); + } + if (auto showShutdownElem = + dialogOnExitOptionsElem->FirstChildElement("show_shutdown_button")) + { + bool showShutdownButton{false}; + showShutdownElem->QueryBoolText(&showShutdownButton); + this->dataPtr->mainWin->SetExitDialogShowShutdown(showShutdownButton); + } + if (auto showCloseGuiElem = + dialogOnExitOptionsElem->FirstChildElement("show_close_gui_button")) + { + bool showCloseGuiButton{false}; + showCloseGuiElem->QueryBoolText(&showCloseGuiButton); + this->dataPtr->mainWin->SetExitDialogShowCloseGui(showCloseGuiButton); + } + if (auto shutdownTextElem = + dialogOnExitOptionsElem->FirstChildElement("shutdown_button_text")) + { + this->dataPtr->mainWin->SetExitDialogShutdownText( + QString::fromStdString(shutdownTextElem->GetText())); + } + if (auto closeGuiTextElem = + dialogOnExitOptionsElem->FirstChildElement("close_gui_button_text")) + { + this->dataPtr->mainWin->SetExitDialogCloseGuiText( + QString::fromStdString(closeGuiTextElem->GetText())); + } + } + + // Server control service topic + std::string serverControlService{"/server_control"}; + auto serverControlElem = + winElem->FirstChildElement("server_control_service"); + if (nullptr != serverControlElem && nullptr != serverControlElem->GetText()) + { + serverControlService = transport::TopicUtils::AsValidTopic( + serverControlElem->GetText()); + } + + if (serverControlService.empty()) + { + ignerr << "Failed to create valid server control service" << std::endl; + } + else + { + ignmsg << "Using server control service [" << serverControlService + << "]" << std::endl; + this->dataPtr->mainWin->SetServerControlService(serverControlService); + } } this->ApplyConfig(); diff --git a/src/MainWindow.cc b/src/MainWindow.cc index d7d62c597..d6157010b 100644 --- a/src/MainWindow.cc +++ b/src/MainWindow.cc @@ -25,6 +25,9 @@ #include "ignition/gui/MainWindow.hh" #include "ignition/gui/Plugin.hh" #include "ignition/gui/qt.h" +#include "ignition/msgs/boolean.pb.h" +#include "ignition/msgs/server_control.pb.h" +#include "ignition/transport/Node.hh" namespace ignition { @@ -48,8 +51,32 @@ namespace ignition /// fully initialized. public: const unsigned int paintCountMin{20}; + /// \brief The action executed when GUI is closed without prompt. + public: ExitAction defaultExitAction{ExitAction::CLOSE_GUI}; + /// \brief Show the confirmation dialog on exit public: bool showDialogOnExit{false}; + + /// \brief Text of the prompt in the confirmation dialog on exit + public: QString dialogOnExitText; + + /// \brief Show "shutdown" button in exit dialog + public: bool exitDialogShowShutdown{false}; + + /// \brief Show "Close GUI" button in exit dialog + public: bool exitDialogShowCloseGui{true}; + + /// \brief Text of "shutdown" button in exit dialog + public: QString exitDialogShutdownText; + + /// \brief Text of "Close GUI" button in exit dialog + public: QString exitDialogCloseGuiText; + + /// \brief Service to send server control requests + public: std::string controlService{"/server_control"}; + + /// \brief Communication node + public: ignition::transport::Node node; }; } } @@ -70,6 +97,11 @@ std::string dirName(const std::string &_path) MainWindow::MainWindow() : dataPtr(new MainWindowPrivate) { + // Expose the ExitAction enum to QML via ExitAction 1.0 module + qRegisterMetaType("ExitAction"); + qmlRegisterUncreatableMetaObject(ignition::gui::staticMetaObject, + "ExitAction", 1, 0, "ExitAction", "Error: namespace enum"); + // Make MainWindow functions available from all QML files (using root) App()->Engine()->rootContext()->setContextProperty("MainWindow", this); @@ -161,6 +193,45 @@ void MainWindow::OnSaveConfigAs(const QString &_path) this->SaveConfig(localPath.toStdString()); } +///////////////////////////////////////////////// +void MainWindow::OnStopServer() +{ + std::function cb = + [](const ignition::msgs::Boolean &_rep, const bool _result) + { + if (_rep.data() && _result) + { + ignmsg << "Simulation server received shutdown request." + << std::endl; + } + else + { + ignerr << "There was a problem instructing the simulation server to " + << "shutdown. It may keep running." << std::endl; + } + }; + + ignition::msgs::ServerControl req; + req.set_stop(true); + const auto success = this->dataPtr->node.Request( + this->dataPtr->controlService, req, cb); + + if (success) + { + ignmsg << "Request to shutdown the simulation server sent. " + "Stopping client now." << std::endl; + } + else + { + ignerr << "Calling service [" << this->dataPtr->controlService << "] to " + << "stop the server failed. Please check that the " + << " of the GUI is configured correctly and " + << "that the server is running in the same IGN_PARTITION and with " + << "the same configuration of IGN_TRANSPORT_TOPIC_STATISTICS." + << std::endl; + } +} + ///////////////////////////////////////////////// void MainWindow::SaveConfig(const std::string &_path) { @@ -847,6 +918,19 @@ void MainWindow::SetShowPluginMenu(const bool _showPluginMenu) this->ShowPluginMenuChanged(); } +///////////////////////////////////////////////// +ExitAction MainWindow::DefaultExitAction() const +{ + return this->dataPtr->defaultExitAction; +} + +///////////////////////////////////////////////// +void MainWindow::SetDefaultExitAction(ExitAction _defaultExitAction) +{ + this->dataPtr->defaultExitAction = _defaultExitAction; + this->DefaultExitActionChanged(); +} + ///////////////////////////////////////////////// bool MainWindow::ShowDialogOnExit() const { @@ -859,3 +943,83 @@ void MainWindow::SetShowDialogOnExit(bool _showDialogOnExit) this->dataPtr->showDialogOnExit = _showDialogOnExit; this->ShowDialogOnExitChanged(); } + +///////////////////////////////////////////////// +QString MainWindow::DialogOnExitText() const +{ + return this->dataPtr->dialogOnExitText; +} + +///////////////////////////////////////////////// +void MainWindow::SetDialogOnExitText( + const QString &_dialogOnExitText) +{ + this->dataPtr->dialogOnExitText = _dialogOnExitText; + this->DialogOnExitTextChanged(); +} + +///////////////////////////////////////////////// +bool MainWindow::ExitDialogShowShutdown() const +{ + return this->dataPtr->exitDialogShowShutdown; +} + +///////////////////////////////////////////////// +void MainWindow::SetExitDialogShowShutdown(bool _exitDialogShowShutdown) +{ + this->dataPtr->exitDialogShowShutdown = _exitDialogShowShutdown; + this->ExitDialogShowShutdownChanged(); +} + +///////////////////////////////////////////////// +bool MainWindow::ExitDialogShowCloseGui() const +{ + return this->dataPtr->exitDialogShowCloseGui; +} + +///////////////////////////////////////////////// +void MainWindow::SetExitDialogShowCloseGui(bool _exitDialogShowCloseGui) +{ + this->dataPtr->exitDialogShowCloseGui = _exitDialogShowCloseGui; + this->ExitDialogShowCloseGuiChanged(); +} + +///////////////////////////////////////////////// +QString MainWindow::ExitDialogShutdownText() const +{ + return this->dataPtr->exitDialogShutdownText; +} + +///////////////////////////////////////////////// +void MainWindow::SetExitDialogShutdownText( + const QString &_exitDialogShutdownText) +{ + this->dataPtr->exitDialogShutdownText = _exitDialogShutdownText; + this->ExitDialogShutdownTextChanged(); +} + +///////////////////////////////////////////////// +QString MainWindow::ExitDialogCloseGuiText() const +{ + return this->dataPtr->exitDialogCloseGuiText; +} + +///////////////////////////////////////////////// +void MainWindow::SetExitDialogCloseGuiText( + const QString &_exitDialogCloseGuiText) +{ + this->dataPtr->exitDialogCloseGuiText = _exitDialogCloseGuiText; + this->ExitDialogCloseGuiTextChanged(); +} + +///////////////////////////////////////////////// +std::string MainWindow::ServerControlService() const +{ + return this->dataPtr->controlService; +} + +///////////////////////////////////////////////// +void MainWindow::SetServerControlService(const std::string &_service) +{ + this->dataPtr->controlService = _service; +} diff --git a/src/MainWindow_TEST.cc b/src/MainWindow_TEST.cc index f7b482acf..d5486d5e7 100644 --- a/src/MainWindow_TEST.cc +++ b/src/MainWindow_TEST.cc @@ -16,9 +16,15 @@ */ #include +#include #include +#include + #include +#include +#include +#include #include #include "test_config.h" // NOLINT(build/include) @@ -35,6 +41,7 @@ char* g_argv[] = using namespace ignition; using namespace gui; +using namespace std::chrono_literals; ///////////////////////////////////////////////// // See https://github.com/ignitionrobotics/ign-gui/issues/75 @@ -374,6 +381,333 @@ TEST(MainWindowTest, EXPECT_TRUE(closed); } +///////////////////////////////////////////////// +TEST(MainWindowTest, + IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(DefaultExitActionAutoShutdown)) +{ + ignition::common::Console::SetVerbosity(4); + Application app(g_argc, g_argv); + + app.LoadConfig(common::joinPaths( + PROJECT_SOURCE_PATH, "test", "config", + "close_dialog_auto_shutdown.config")); + // Get main window + auto mainWindow = App()->findChild(); + ASSERT_NE(nullptr, mainWindow); + + bool shutdownCalled{false}; + transport::Node node; + std::string serverControlService{"/server_control"}; + std::function + cb = [&](const ignition::msgs::ServerControl &_req, msgs::Boolean &_rep) { + shutdownCalled = _req.stop(); + _rep.set_data(true); + return true; + }; + node.Advertise(serverControlService, cb); + + EXPECT_TRUE(mainWindow->QuickWindow()->isVisible()); + mainWindow->QuickWindow()->close(); + EXPECT_FALSE(mainWindow->QuickWindow()->isVisible()); + + EXPECT_TRUE(shutdownCalled); +} + +///////////////////////////////////////////////// +TEST(MainWindowTest, + IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(ExitActionCustomShutdownService)) +{ + ignition::common::Console::SetVerbosity(4); + Application app(g_argc, g_argv); + + app.LoadConfig(common::joinPaths( + PROJECT_SOURCE_PATH, "test", "config", + "close_dialog_custom_shutdown_service.config")); + // Get main window + auto mainWindow = App()->findChild(); + ASSERT_NE(nullptr, mainWindow); + + bool shutdownCalled{false}; + bool wrongShutdownCalled{false}; + + transport::Node node; + + std::string serverControlService{"/test_service"}; + std::function + cb = [&](const ignition::msgs::ServerControl &_req, msgs::Boolean &_rep) { + shutdownCalled = _req.stop(); + _rep.set_data(true); + return true; + }; + node.Advertise(serverControlService, cb); + + std::string wrongServerControlService{"/server_control"}; + std::function + cb2 = [&](const ignition::msgs::ServerControl &, msgs::Boolean &_rep) { + wrongShutdownCalled = true; + _rep.set_data(true); + return true; + }; + node.Advertise(wrongServerControlService, cb2); + + EXPECT_TRUE(mainWindow->QuickWindow()->isVisible()); + mainWindow->QuickWindow()->close(); + EXPECT_FALSE(mainWindow->QuickWindow()->isVisible()); + + EXPECT_TRUE(shutdownCalled); + EXPECT_FALSE(wrongShutdownCalled); +} + +///////////////////////////////////////////////// +TEST(MainWindowTest, + IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(DefaultExitActionAutoCloseGui)) +{ + ignition::common::Console::SetVerbosity(4); + Application app(g_argc, g_argv); + + // Add test plugins to path + app.AddPluginPath(common::joinPaths(PROJECT_BINARY_PATH, "lib")); + app.LoadConfig(common::joinPaths( + PROJECT_SOURCE_PATH, "test", "config", + "close_dialog_auto_gui_only.config")); + // Get main window + auto mainWindow = App()->findChild(); + ASSERT_NE(nullptr, mainWindow); + + bool shutdownCalled{false}; + transport::Node node; + std::string serverControlService{"/server_control"}; + std::function + cb = [&](const ignition::msgs::ServerControl &, msgs::Boolean &_rep) { + shutdownCalled = true; + _rep.set_data(true); + return true; + }; + node.Advertise(serverControlService, cb); + + EXPECT_TRUE(mainWindow->QuickWindow()->isVisible()); + mainWindow->QuickWindow()->close(); + EXPECT_FALSE(mainWindow->QuickWindow()->isVisible()); + + EXPECT_FALSE(shutdownCalled); +} + +///////////////////////////////////////////////// +// Copied from private QPlatformDialogHelper::ButtonRole +enum ButtonRole { + InvalidRole = -1, + AcceptRole, + RejectRole, + DestructiveRole, + ActionRole, + HelpRole, + YesRole, + NoRole, + ResetRole, + ApplyRole, + NRoles +}; + +///////////////////////////////////////////////// +void FindExitDialogButtons( + MainWindow *_mainWindow, + std::unordered_set &_roles, + std::unordered_map &_buttonRoles) +{ + auto dialog = _mainWindow->QuickWindow()->findChild( + "confirmationDialogOnExit"); + ASSERT_NE(nullptr, dialog); + + QObject *buttonBox{nullptr}; + for (const auto& c : dialog->findChildren()) + { + if (c->metaObject()->className() == std::string("QQuickDialogButtonBox")) + { + const auto& p = c->property("standardButtons"); + if (p.isValid() && p.toInt() != 0) + { + buttonBox = c; + break; + } + } + } + ASSERT_NE(nullptr, buttonBox); + + const auto buttonCount = buttonBox->property("count").toInt(); + + std::vector buttons; + for (int index = 0; index < buttonCount; ++index) + { + QQuickItem *button; + QMetaObject::invokeMethod(buttonBox, "itemAt", Qt::DirectConnection, + Q_RETURN_ARG(QQuickItem *, button), + Q_ARG(int, index)); + + ASSERT_STREQ("QQuickButton", button->metaObject()->className()); + buttons.push_back(button); + } + + EXPECT_EQ(static_cast(buttonCount), buttons.size()); + + for (const auto& button : buttons) + { + QQmlProperty prop(button, "DialogButtonBox.buttonRole", qmlContext(button)); + const auto role = static_cast(prop.read().toInt()); + _roles.insert(role); + _buttonRoles[role] = button; + } +} + +///////////////////////////////////////////////// +TEST(MainWindowTest, + IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(ExitDialogShutdownButton)) +{ + ignition::common::Console::SetVerbosity(4); + Application app(g_argc, g_argv); + + app.LoadConfig(common::joinPaths( + PROJECT_SOURCE_PATH, "test", "config", + "close_dialog_buttons.config")); + // Get main window + auto mainWindow = App()->findChild(); + ASSERT_NE(nullptr, mainWindow); + + // Trigger the closing behavior + EXPECT_TRUE(mainWindow->QuickWindow()->isVisible()); + mainWindow->QuickWindow()->close(); + EXPECT_TRUE(mainWindow->QuickWindow()->isVisible()); + + QCoreApplication::processEvents(); + + std::unordered_set roles; + std::unordered_map buttonRoles; + FindExitDialogButtons(mainWindow, roles, buttonRoles); + + auto expectedRoles = + std::unordered_set({ + ButtonRole::AcceptRole, + ButtonRole::DestructiveRole, + ButtonRole::RejectRole + }); + ASSERT_EQ(expectedRoles, roles); + + bool shutdownCalled{false}; + transport::Node node; + std::string serverControlService{"/server_control"}; + std::function + cb = [&](const ignition::msgs::ServerControl &, msgs::Boolean &_rep) { + shutdownCalled = true; + _rep.set_data(true); + return true; + }; + node.Advertise(serverControlService, cb); + + EXPECT_TRUE(mainWindow->QuickWindow()->isVisible()); + QMetaObject::invokeMethod( + buttonRoles[ButtonRole::DestructiveRole], "clicked"); + + // Wait until the window closes (it may take some time, but not > 1 second) + int sleep = 0; + for (; mainWindow->QuickWindow()->isVisible() && sleep < 10; ++sleep) + { + std::this_thread::sleep_for(100ms); + QCoreApplication::processEvents(); + } + + EXPECT_TRUE(shutdownCalled); + EXPECT_FALSE(mainWindow->QuickWindow()->isVisible()); +} + +///////////////////////////////////////////////// +TEST(MainWindowTest, + IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(ExitDialogDefaultButtons)) +{ + ignition::common::Console::SetVerbosity(4); + Application app(g_argc, g_argv); + + app.LoadConfig(common::joinPaths( + PROJECT_SOURCE_PATH, "test", "config", + "close_dialog_default_buttons.config")); + // Get main window + auto mainWindow = App()->findChild(); + ASSERT_NE(nullptr, mainWindow); + + // Trigger the closing behavior + EXPECT_TRUE(mainWindow->QuickWindow()->isVisible()); + mainWindow->QuickWindow()->close(); + EXPECT_TRUE(mainWindow->QuickWindow()->isVisible()); + + QCoreApplication::processEvents(); + + std::unordered_set roles; + std::unordered_map buttonRoles; + FindExitDialogButtons(mainWindow, roles, buttonRoles); + + auto expectedRoles = + std::unordered_set({ + ButtonRole::AcceptRole, + ButtonRole::RejectRole + }); + ASSERT_EQ(expectedRoles, roles); + + bool shutdownCalled{false}; + transport::Node node; + std::string serverControlService{"/server_control"}; + std::function + cb = [&](const ignition::msgs::ServerControl &, msgs::Boolean &_rep) { + shutdownCalled = true; + _rep.set_data(true); + return true; + }; + node.Advertise(serverControlService, cb); + + EXPECT_TRUE(mainWindow->QuickWindow()->isVisible()); + QMetaObject::invokeMethod(buttonRoles[ButtonRole::AcceptRole], "clicked"); + EXPECT_FALSE(mainWindow->QuickWindow()->isVisible()); +} + +///////////////////////////////////////////////// +TEST(MainWindowTest, + IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(ExitDialogButtonsText)) +{ + ignition::common::Console::SetVerbosity(4); + Application app(g_argc, g_argv); + + app.LoadConfig(common::joinPaths( + PROJECT_SOURCE_PATH, "test", "config", + "close_dialog_buttons_text.config")); + // Get main window + auto mainWindow = App()->findChild(); + ASSERT_NE(nullptr, mainWindow); + + // Trigger the closing behavior + EXPECT_TRUE(mainWindow->QuickWindow()->isVisible()); + mainWindow->QuickWindow()->close(); + EXPECT_TRUE(mainWindow->QuickWindow()->isVisible()); + + QCoreApplication::processEvents(); + + std::unordered_set roles; + std::unordered_map buttonRoles; + FindExitDialogButtons(mainWindow, roles, buttonRoles); + + auto expectedRoles = + std::unordered_set({ + ButtonRole::AcceptRole, + ButtonRole::DestructiveRole, + ButtonRole::RejectRole + }); + ASSERT_EQ(expectedRoles, roles); + + auto closeGui = buttonRoles[ButtonRole::AcceptRole]; + EXPECT_EQ("close_gui", + closeGui->property("text").toString().toStdString()); + + auto shutdown = buttonRoles[ButtonRole::DestructiveRole]; + EXPECT_EQ("shutdown", + shutdown->property("text").toString().toStdString()); +} + ///////////////////////////////////////////////// TEST(MainWindowTest, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(ApplyConfig)) { diff --git a/src/plugins/CMakeLists.txt b/src/plugins/CMakeLists.txt index 3a82d2945..8084b7a56 100644 --- a/src/plugins/CMakeLists.txt +++ b/src/plugins/CMakeLists.txt @@ -120,6 +120,7 @@ add_subdirectory(key_publisher) add_subdirectory(publisher) add_subdirectory(scene3d) add_subdirectory(screenshot) +add_subdirectory(shutdown_button) add_subdirectory(teleop) add_subdirectory(topic_echo) add_subdirectory(topic_viewer) diff --git a/src/plugins/shutdown_button/CMakeLists.txt b/src/plugins/shutdown_button/CMakeLists.txt new file mode 100644 index 000000000..5aae18144 --- /dev/null +++ b/src/plugins/shutdown_button/CMakeLists.txt @@ -0,0 +1,9 @@ +ign_gui_add_plugin(ShutdownButton + SOURCES + ShutdownButton.cc + QT_HEADERS + ShutdownButton.hh + TEST_SOURCES + ShutdownButton_TEST.cc +) + diff --git a/src/plugins/shutdown_button/ShutdownButton.cc b/src/plugins/shutdown_button/ShutdownButton.cc new file mode 100644 index 000000000..87a2ba0c2 --- /dev/null +++ b/src/plugins/shutdown_button/ShutdownButton.cc @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2021 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +#include "ShutdownButton.hh" + +#include + +#include "ignition/gui/Application.hh" +#include "ignition/gui/MainWindow.hh" + +using namespace ignition; +using namespace gui; +using namespace plugins; + +///////////////////////////////////////////////// +ShutdownButton::ShutdownButton() : Plugin() +{ +} + +///////////////////////////////////////////////// +ShutdownButton::~ShutdownButton() = default; + +///////////////////////////////////////////////// +void ShutdownButton::LoadConfig(const tinyxml2::XMLElement * /*_pluginElem*/) +{ + // Default name in case user didn't define one + if (this->title.empty()) + this->title = "Shutdown"; +} + +///////////////////////////////////////////////// +void ShutdownButton::OnStop() +{ + ignition::gui::App()->findChild()->QuickWindow()->close(); +} + +// Register this plugin +IGNITION_ADD_PLUGIN(ignition::gui::plugins::ShutdownButton, + ignition::gui::Plugin) diff --git a/src/plugins/shutdown_button/ShutdownButton.hh b/src/plugins/shutdown_button/ShutdownButton.hh new file mode 100644 index 000000000..f4489cb43 --- /dev/null +++ b/src/plugins/shutdown_button/ShutdownButton.hh @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2021 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +#ifndef IGNITION_GUI_PLUGINS_SHUTDOWNBUTTON_HH_ +#define IGNITION_GUI_PLUGINS_SHUTDOWNBUTTON_HH_ + +#include "ignition/gui/Plugin.hh" + +#ifndef _WIN32 +# define ShutdownButton_EXPORTS_API +#else +# if (defined(ShutdownButton_EXPORTS)) +# define ShutdownButton_EXPORTS_API __declspec(dllexport) +# else +# define ShutdownButton_EXPORTS_API __declspec(dllimport) +# endif +#endif + +namespace ignition +{ +namespace gui +{ +namespace plugins +{ + /// \brief This plugin provides a shutdown button. + class ShutdownButton_EXPORTS_API ShutdownButton: public ignition::gui::Plugin + { + Q_OBJECT + + /// \brief Constructor + public: ShutdownButton(); + + /// \brief Destructor + public: virtual ~ShutdownButton(); + + // Documentation inherited + public: void LoadConfig(const tinyxml2::XMLElement *_pluginElem) override; + + /// \brief Callback in Qt thread when close button is clicked. + public slots: void OnStop(); + }; +} +} +} + +#endif diff --git a/src/plugins/shutdown_button/ShutdownButton.qml b/src/plugins/shutdown_button/ShutdownButton.qml new file mode 100644 index 000000000..8fbf5424c --- /dev/null +++ b/src/plugins/shutdown_button/ShutdownButton.qml @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2021 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.3 + +RowLayout { + id: shutdownButton + width: 64 + spacing: 2 + Layout.minimumWidth: 64 + Layout.minimumHeight: 64 + + /** + * Close icon + */ + property string closeIcon: "\u2A2F" + + property int tooltipDelay: 500 + property int tooltipTimeout: 1000 + + /** + * Close button + */ + RoundButton { + id: closeButton + visible: true + text: closeIcon + checkable: true + Layout.alignment : Qt.AlignVCenter + Layout.minimumWidth: width + Layout.leftMargin: 10 + onClicked: { + ShutdownButton.OnStop() + } + Material.background: Material.primary + ToolTip.visible: hovered + ToolTip.delay: tooltipDelay + ToolTip.timeout: tooltipTimeout + ToolTip.text: qsTr("Quit") + } +} diff --git a/src/plugins/shutdown_button/ShutdownButton.qrc b/src/plugins/shutdown_button/ShutdownButton.qrc new file mode 100644 index 000000000..8a1e25fe7 --- /dev/null +++ b/src/plugins/shutdown_button/ShutdownButton.qrc @@ -0,0 +1,5 @@ + + + ShutdownButton.qml + + diff --git a/src/plugins/shutdown_button/ShutdownButton_TEST.cc b/src/plugins/shutdown_button/ShutdownButton_TEST.cc new file mode 100644 index 000000000..07d6a7470 --- /dev/null +++ b/src/plugins/shutdown_button/ShutdownButton_TEST.cc @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2021 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +#include + +#include +#include +#include + +#include "test_config.h" // NOLINT(build/include) +#include "ignition/gui/Application.hh" +#include "ignition/gui/Plugin.hh" +#include "ignition/gui/MainWindow.hh" +#include "ShutdownButton.hh" + +int g_argc = 1; +char* g_argv[] = +{ + reinterpret_cast(const_cast("./ShutdownButton_TEST")), +}; + +using namespace ignition; +using namespace gui; + +// See https://github.com/ignitionrobotics/ign-gui/issues/75 +///////////////////////////////////////////////// +TEST(ShutdownButtonTest, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(Load)) +{ + common::Console::SetVerbosity(4); + + Application app(g_argc, g_argv); + app.AddPluginPath(std::string(PROJECT_BINARY_PATH) + "/lib"); + + EXPECT_TRUE(app.LoadPlugin("ShutdownButton")); + + // Get main window + auto win = app.findChild(); + ASSERT_NE(nullptr, win); + + // Get plugin + auto plugins = win->findChildren(); + EXPECT_EQ(plugins.size(), 1); + + auto plugin = plugins[0]; + EXPECT_EQ(plugin->Title(), "Shutdown"); + + // Cleanup + plugins.clear(); +} + +///////////////////////////////////////////////// +TEST(ShutdownButtonTest, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(ShutdownButton)) +{ + common::Console::SetVerbosity(4); + + Application app(g_argc, g_argv); + app.AddPluginPath(std::string(PROJECT_BINARY_PATH) + "/lib"); + app.LoadConfig(common::joinPaths(PROJECT_SOURCE_PATH, + "src", "plugins", "shutdown_button", "test.config")); + + // Get main window + auto win = app.findChild(); + ASSERT_NE(nullptr, win); + + // Show, but don't exec, so we don't block + win->QuickWindow()->show(); + + // Get plugin + auto plugins = win->findChildren(); + EXPECT_EQ(plugins.size(), 1); + + auto plugin = plugins[0]; + EXPECT_EQ(plugin->Title(), "Shutdown!"); + + // World control service + bool stopCalled = false; + std::function cb = + [&](const msgs::ServerControl &_req, msgs::Boolean &_resp) + { + stopCalled = _req.stop(); + _resp.set_data(true); + return true; + }; + transport::Node node; + node.Advertise("/server_control_test", cb); + + EXPECT_TRUE(win->QuickWindow()->isVisible()); + + plugin->OnStop(); + EXPECT_TRUE(stopCalled); + + EXPECT_FALSE(win->QuickWindow()->isVisible()); + + // Cleanup + plugins.clear(); +} + +///////////////////////////////////////////////// +TEST(ShutdownButtonTest, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(ShutdownGuiOnly)) +{ + common::Console::SetVerbosity(4); + + Application app(g_argc, g_argv); + app.AddPluginPath(std::string(PROJECT_BINARY_PATH) + "/lib"); + app.LoadConfig(common::joinPaths(PROJECT_SOURCE_PATH, + "src", "plugins", "shutdown_button", "test.config")); + + // Get main window + auto win = app.findChild(); + ASSERT_NE(nullptr, win); + + // override the SHUTDOWN_SERVER value from the test config + win->SetDefaultExitAction(ExitAction::CLOSE_GUI); + + // Show, but don't exec, so we don't block + win->QuickWindow()->show(); + + // Get plugin + auto plugins = win->findChildren(); + EXPECT_EQ(plugins.size(), 1); + + auto plugin = plugins[0]; + + // World control service + bool stopCalled = false; + std::function cb = + [&](const msgs::ServerControl &_req, msgs::Boolean &_resp) + { + stopCalled = _req.stop(); + _resp.set_data(true); + return true; + }; + transport::Node node; + node.Advertise("/server_control_test", cb); + + EXPECT_TRUE(win->QuickWindow()->isVisible()); + + plugin->OnStop(); + EXPECT_FALSE(stopCalled); + + EXPECT_FALSE(win->QuickWindow()->isVisible()); + + // Cleanup + plugins.clear(); +} diff --git a/src/plugins/shutdown_button/test.config b/src/plugins/shutdown_button/test.config new file mode 100644 index 000000000..616024f83 --- /dev/null +++ b/src/plugins/shutdown_button/test.config @@ -0,0 +1,13 @@ + + + + /server_control_test + SHUTDOWN_SERVER + + + + + Shutdown! + + + diff --git a/test/config/close_dialog_auto_gui_only.config b/test/config/close_dialog_auto_gui_only.config new file mode 100644 index 000000000..41a0669f9 --- /dev/null +++ b/test/config/close_dialog_auto_gui_only.config @@ -0,0 +1,6 @@ + + + + false + CLOSE_GUI + \ No newline at end of file diff --git a/test/config/close_dialog_auto_shutdown.config b/test/config/close_dialog_auto_shutdown.config new file mode 100644 index 000000000..9382d4e03 --- /dev/null +++ b/test/config/close_dialog_auto_shutdown.config @@ -0,0 +1,6 @@ + + + + false + shutdown_server + \ No newline at end of file diff --git a/test/config/close_dialog_buttons.config b/test/config/close_dialog_buttons.config new file mode 100644 index 000000000..625ff2e96 --- /dev/null +++ b/test/config/close_dialog_buttons.config @@ -0,0 +1,8 @@ + + + + true + + true + + \ No newline at end of file diff --git a/test/config/close_dialog_buttons_text.config b/test/config/close_dialog_buttons_text.config new file mode 100644 index 000000000..971d75981 --- /dev/null +++ b/test/config/close_dialog_buttons_text.config @@ -0,0 +1,10 @@ + + + + true + + true + close_gui + shutdown + + \ No newline at end of file diff --git a/test/config/close_dialog_custom_shutdown_service.config b/test/config/close_dialog_custom_shutdown_service.config new file mode 100644 index 000000000..272ebf2fe --- /dev/null +++ b/test/config/close_dialog_custom_shutdown_service.config @@ -0,0 +1,7 @@ + + + + false + SHUTDOWN_SERVER + /test_service + \ No newline at end of file diff --git a/test/config/close_dialog_default_buttons.config b/test/config/close_dialog_default_buttons.config new file mode 100644 index 000000000..38f467d97 --- /dev/null +++ b/test/config/close_dialog_default_buttons.config @@ -0,0 +1,5 @@ + + + + true + \ No newline at end of file diff --git a/tutorials/04_layout.md b/tutorials/04_layout.md index 6ad02c358..90ff70d7d 100644 --- a/tutorials/04_layout.md +++ b/tutorials/04_layout.md @@ -28,7 +28,35 @@ by adding a `` element to the config file. The child elements are: the menu. If `from_paths` is true, all plugins will be shown anyway, so adding `` has no effect. For the plugin to be shown, it must be on the path. -* ``: If true, a confirmation dialog will show up when closing the window. +* ``: Default `CLOSE_GUI`. If set to `SHUTDOWN_SERVER` and + `` is `false`, closing the window will + emit a server shutdown request with `stop = true` to the + `` topic. This can be used + in applications like Ignition Gazebo which can run a + server in a process separate from the GUI to stop both + the GUI and the server when the window is closed. The value is + case-insensitive. +* ``: Default `/server_control`. This is the name of `msgs::ServerControl` + service that allows e.g. stopping the server. It is usually not needed + to alter this value. +* ``: If `true`, a confirmation dialog will show up when closing the window. +* ``: Configuration of the dialog shown before exit (with all elements + optional). + * ``: Text of the prompt in the confirmation dialog. + * ``: Default `false`. If `true`, display a "Shutdown simulation" + button in the confirmation dialog, which shuts down the server, too. + Always set `` to a different string than "OK" + if both close GUI and shutdown buttons are shown, otherwise there + would be a dialog with options "OK", "Cancel" and "shutdown", which + is bad UX. + * ``: Text of the "Shutdown simulation" button. If empty, a default text + is used. + * ``: Default `true`. If `true`, display a "Close GUI" button in + the confirmation dialog, which leaves server running. + * ``: Text of the "Close GUI" button. If empty, a default text is used. + When both shutdown and close GUI buttons are shown, always change + the text of the close GUI button, otherwise there would be a dialog + with options "OK", "Cancel" and "shutdown", which is bad UX. ## Example layout