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