diff --git a/libs/s25main/WindowManager.cpp b/libs/s25main/WindowManager.cpp index ae874f87bf..117484fc0d 100644 --- a/libs/s25main/WindowManager.cpp +++ b/libs/s25main/WindowManager.cpp @@ -136,8 +136,13 @@ void WindowManager::RelayKeyboardMessage(KeyboardMsgHandler msg, const KeyEvent& const auto itActiveWnd = std::find_if(windows.rbegin(), windows.rend(), [escape](const auto& wnd) { return !wnd->ShouldBeClosed() && !(escape && wnd->IsPinned()); }); - if(itActiveWnd != windows.rend() && (*itActiveWnd)->getCloseBehavior() != CloseBehavior::Custom) - (*itActiveWnd)->Close(); + if(itActiveWnd != windows.rend()) + { + if((*itActiveWnd)->getCloseBehavior() == CloseBehavior::Custom) + CALL_MEMBER_FN(**itActiveWnd, msg)(ke); + else + (*itActiveWnd)->Close(); + } } else if(!CALL_MEMBER_FN(*windows.back(), msg)(ke)) // send to active window { // If not handled yet, relay to active window diff --git a/libs/s25main/desktops/dskTest.cpp b/libs/s25main/desktops/dskTest.cpp index 6b350cd8f4..5ef058cbac 100644 --- a/libs/s25main/desktops/dskTest.cpp +++ b/libs/s25main/desktops/dskTest.cpp @@ -16,6 +16,7 @@ #include "desktops/dskTextureTest.h" #include "dskBenchmark.h" #include "files.h" +#include "ingameWindows/iwMsgbox.h" #include "ogl/FontStyle.h" #include "s25util/colors.h" @@ -36,7 +37,9 @@ enum ID_txtTest, ID_cbTxtSize, ID_btHideCtrls, - ID_btShowBenchmark + ID_btShowBenchmark, + ID_btPopupWarning, + ID_btPopupConfirm }; } @@ -91,6 +94,11 @@ dskTest::dskTest() : curBGIdx(LOAD_SCREENS.size()) btPos.y += 11; AddText(ID_txtTest, btPos, "Enter something", COLOR_YELLOW, FontStyle::VCENTER, SmallFont); + AddTextButton(ID_btPopupWarning, DrawPoint(10, 260), Extent(150, 22), TextureColor::Green1, "Warning popup", + NormalFont); + AddTextButton(ID_btPopupConfirm, DrawPoint(165, 260), Extent(150, 22), TextureColor::Green1, "Confirm popup", + NormalFont); + AddTextButton(ID_btDisable, DrawPoint(10, 540), Extent(150, 22), TextureColor::Green1, "Enable/Disable buttons", NormalFont); AddTextButton(ID_btAnimate, DrawPoint(165, 540), Extent(80, 22), TextureColor::Green1, "Animate", NormalFont); @@ -131,6 +139,26 @@ void dskTest::Msg_ButtonClick(const unsigned ctrl_id) { case ID_btTextureTest: WINDOWMANAGER.Switch(std::make_unique()); break; case ID_btShowBenchmark: WINDOWMANAGER.Switch(std::make_unique()); break; + case ID_btPopupWarning: + WINDOWMANAGER.Show( + std::make_unique("Warning", "This addon may heavily alter intended map design.", this, + MsgboxConfig{{{"OK", MsgboxResult::Ok, TextureColor::Green2}, + {"Cancel", MsgboxResult::Cancel, TextureColor::Red1}}, + 0, + 1, + 1}, + MsgboxIcon::ExclamationRed, ID_btPopupWarning)); + break; + case ID_btPopupConfirm: + WINDOWMANAGER.Show( + std::make_unique("Confirm", "Do you want to continue?", this, + MsgboxConfig{{{"Yes", MsgboxResult::Yes, TextureColor::Green2}, + {"No", MsgboxResult::No, TextureColor::Red1}}, + 0, + 1, + 1}, + MsgboxIcon::QuestionRed, ID_btPopupConfirm)); + break; case ID_btDisable: for(unsigned i = ID_grpBtStart; i < ID_grpBtEnd; i++) { diff --git a/libs/s25main/ingameWindows/iwMsgbox.cpp b/libs/s25main/ingameWindows/iwMsgbox.cpp index 927b6fae07..fcb968efb8 100644 --- a/libs/s25main/ingameWindows/iwMsgbox.cpp +++ b/libs/s25main/ingameWindows/iwMsgbox.cpp @@ -7,11 +7,12 @@ #include "WindowManager.h" #include "controls/ctrlImage.h" #include "controls/ctrlMultiline.h" +#include "driver/KeyEvent.h" #include "drivers/VideoDriverWrapper.h" #include "enum_cast.hpp" -#include "helpers/EnumArray.h" #include "ogl/glArchivItem_Bitmap.h" #include "gameData/const_gui_ids.h" +#include namespace { enum IDS @@ -24,25 +25,96 @@ const Extent btSize(90, 20); const unsigned short paddingX = 15; /// Padding in X/to image const unsigned short minTextWidth = 150; const unsigned short maxTextHeight = 200; + +MsgboxConfig makeLegacyConfig(const MsgboxButton button) +{ + MsgboxConfig config; + switch(button) + { + case MsgboxButton::Ok: + config.buttons.push_back({_("OK"), MsgboxResult::Ok, TextureColor::Green2}); + config.defaultButton = 0; + config.cancelButton = -1; + config.focusedButton = 0; + break; + + case MsgboxButton::OkCancel: + config.buttons.push_back({_("OK"), MsgboxResult::Ok, TextureColor::Green2}); + config.buttons.push_back({_("Cancel"), MsgboxResult::Cancel, TextureColor::Red1}); + config.defaultButton = 0; + config.cancelButton = 1; + config.focusedButton = 1; + break; + + case MsgboxButton::YesNo: + config.buttons.push_back({_("Yes"), MsgboxResult::Yes, TextureColor::Green2}); + config.buttons.push_back({_("No"), MsgboxResult::No, TextureColor::Red1}); + config.defaultButton = 0; + config.cancelButton = 1; + config.focusedButton = 1; + break; + + case MsgboxButton::YesNoCancel: + config.buttons.push_back({_("Yes"), MsgboxResult::Yes, TextureColor::Green2}); + config.buttons.push_back({_("No"), MsgboxResult::No, TextureColor::Red1}); + config.buttons.push_back({_("Cancel"), MsgboxResult::Cancel, TextureColor::Grey}); + config.defaultButton = 0; + config.cancelButton = 2; + config.focusedButton = 2; + break; + } + return config; +} } // namespace iwMsgbox::iwMsgbox(const std::string& title, const std::string& text, Window* msgHandler, MsgboxButton button, MsgboxIcon icon, unsigned msgboxid) - : iwMsgbox(title, text, msgHandler, button, "io", rttr::enum_cast(icon), msgboxid) + : iwMsgbox(title, text, msgHandler, makeLegacyConfig(button), icon, msgboxid) {} iwMsgbox::iwMsgbox(const std::string& title, const std::string& text, Window* msgHandler, MsgboxButton button, const ResourceId& iconFile, unsigned iconIdx, unsigned msgboxid /* = 0 */) + : iwMsgbox(title, text, msgHandler, makeLegacyConfig(button), iconFile, iconIdx, msgboxid) +{} + +iwMsgbox::iwMsgbox(const std::string& title, const std::string& text, Window* msgHandler, MsgboxConfig config, + unsigned msgboxid /* = 0 */) : IngameWindow(CGI_MSGBOX, IngameWindow::posLastOrCenter, Extent(420, 140), title, LOADER.GetImageN("resource", 41), true, CloseBehavior::Custom), - button(button), msgboxid(msgboxid), msgHandler_(msgHandler) + msgboxid(msgboxid), buttons_(std::move(config.buttons)), defaultButton_(config.defaultButton), + cancelButton_(config.cancelButton), focusedButton_(config.focusedButton), msgHandler_(msgHandler) { - Init(text, iconFile, iconIdx); + Init(text, nullptr); } -void iwMsgbox::Init(const std::string& text, const ResourceId& iconFile, unsigned iconIdx) +iwMsgbox::iwMsgbox(const std::string& title, const std::string& text, Window* msgHandler, MsgboxConfig config, + MsgboxIcon icon, unsigned msgboxid) + : iwMsgbox(title, text, msgHandler, std::move(config), "io", rttr::enum_cast(icon), msgboxid) +{} + +iwMsgbox::iwMsgbox(const std::string& title, const std::string& text, Window* msgHandler, MsgboxConfig config, + const ResourceId& iconFile, unsigned iconIdx, unsigned msgboxid /* = 0 */) + : IngameWindow(CGI_MSGBOX, IngameWindow::posLastOrCenter, Extent(420, 140), title, LOADER.GetImageN("resource", 41), + true, CloseBehavior::Custom), + msgboxid(msgboxid), buttons_(std::move(config.buttons)), defaultButton_(config.defaultButton), + cancelButton_(config.cancelButton), focusedButton_(config.focusedButton), msgHandler_(msgHandler) +{ + Init(text, LOADER.GetImageN(iconFile, iconIdx)); +} + +void iwMsgbox::Init(const std::string& text, glArchivItem_Bitmap* icon) { - glArchivItem_Bitmap* icon = LOADER.GetImageN(iconFile, iconIdx); + if(buttons_.empty()) + buttons_.push_back({_("OK"), MsgboxResult::Ok, TextureColor::Green2}); + if(buttons_.size() > 3) + buttons_.resize(3); + if(defaultButton_ >= buttons_.size()) + defaultButton_ = 0; + if(cancelButton_ >= static_cast(buttons_.size())) + cancelButton_ = -1; + if(focusedButton_ >= static_cast(buttons_.size())) + focusedButton_ = -1; + if(icon) AddImage(ID_ICON, contentOffset + DrawPoint(30, 20), icon); int textX = icon ? icon->getWidth() - icon->getNx() + GetCtrl(ID_ICON)->GetPos().x : contentOffset.x; @@ -58,35 +130,17 @@ void iwMsgbox::Init(const std::string& text, const ResourceId& iconFile, unsigne // Increase window size if required SetIwSize(elMax(GetIwSize(), newIwSize)); - unsigned defaultBt = 0; - // Buttons erstellen - switch(button) + const auto numButtons = static_cast(buttons_.size()); + const int spacing = 6; + const int totalBtWidth = numButtons * btSize.x + (numButtons - 1) * spacing; + int x = GetSize().x / 2 - totalBtWidth / 2; + for(unsigned i = 0; i < numButtons; ++i) { - case MsgboxButton::Ok: - AddButton(ID_BT_0, GetSize().x / 2 - 45, _("OK"), TextureColor::Green2); - defaultBt = 0; - break; - - case MsgboxButton::OkCancel: - AddButton(ID_BT_0, GetSize().x / 2 - 3 - 90, _("OK"), TextureColor::Green2); - AddButton(ID_BT_0 + 1, GetSize().x / 2 + 3, _("Cancel"), TextureColor::Red1); - defaultBt = 1; - break; - - case MsgboxButton::YesNo: - AddButton(ID_BT_0, GetSize().x / 2 - 3 - 90, _("Yes"), TextureColor::Green2); - AddButton(ID_BT_0 + 1, GetSize().x / 2 + 3, _("No"), TextureColor::Red1); - defaultBt = 1; - break; - - case MsgboxButton::YesNoCancel: - AddButton(ID_BT_0, GetSize().x / 2 - 45 - 6 - 90, _("Yes"), TextureColor::Green2); - AddButton(ID_BT_0 + 1, GetSize().x / 2 - 45, _("No"), TextureColor::Red1); - AddButton(ID_BT_0 + 2, GetSize().x / 2 + 45 + 6, _("Cancel"), TextureColor::Grey); - defaultBt = 2; - break; + AddButton(ID_BT_0 + i, x, buttons_[i].text, buttons_[i].color); + x += btSize.x + spacing; } - const Window* defBt = GetCtrl(defaultBt + ID_BT_0); + const unsigned focusedButton = focusedButton_ >= 0 ? static_cast(focusedButton_) : defaultButton_; + const Window* defBt = GetCtrl(focusedButton + ID_BT_0); if(defBt) VIDEODRIVER.SetMousePos(defBt->GetDrawPos() + DrawPoint(defBt->GetSize()) / 2); WINDOWMANAGER.SetCursor(); @@ -152,16 +206,25 @@ void iwMsgbox::MoveIcon(const DrawPoint& pos) } } -constexpr helpers::EnumArray, MsgboxButton> RET_IDS = { - {{MsgboxResult::Ok}, - {MsgboxResult::Ok, MsgboxResult::Cancel}, - {MsgboxResult::Yes, MsgboxResult::No}, - {MsgboxResult::Yes, MsgboxResult::No, MsgboxResult::Cancel}}}; +bool iwMsgbox::Msg_KeyDown(const KeyEvent& ke) +{ + if(ke.kt == KeyType::Return) + { + Msg_ButtonClick(ID_BT_0 + defaultButton_); + return true; + } + if(ke.kt == KeyType::Escape && cancelButton_ >= 0) + { + Msg_ButtonClick(ID_BT_0 + static_cast(cancelButton_)); + return true; + } + return false; +} void iwMsgbox::Msg_ButtonClick(const unsigned ctrl_id) { if(msgHandler_) - msgHandler_->Msg_MsgBoxResult(msgboxid, RET_IDS[button][ctrl_id - ID_BT_0]); + msgHandler_->Msg_MsgBoxResult(msgboxid, buttons_[ctrl_id - ID_BT_0].result); Close(); } diff --git a/libs/s25main/ingameWindows/iwMsgbox.h b/libs/s25main/ingameWindows/iwMsgbox.h index 53dd37e8bd..ff65290ea2 100644 --- a/libs/s25main/ingameWindows/iwMsgbox.h +++ b/libs/s25main/ingameWindows/iwMsgbox.h @@ -5,18 +5,38 @@ #pragma once #include "IngameWindow.h" +#include +#include + +class glArchivItem_Bitmap; class ResourceId; class Window; +struct KeyEvent; + +struct MsgboxButtonConfig +{ + std::string text; + MsgboxResult result; + TextureColor color; +}; + +struct MsgboxConfig +{ + std::vector buttons; + unsigned defaultButton = 0; + int cancelButton = -1; + int focusedButton = -1; +}; class iwMsgbox : public IngameWindow { - /// Buttons, die auf der Box erscheinen sollen - MsgboxButton button; /// ID für die Msgbox, um unterschiedliche unsigned msgboxid; - /// Einzelne Stringzeilen, die durch die Umbrechung ggf. zu Stande kommen - std::vector strings; + std::vector buttons_; + unsigned defaultButton_; + int cancelButton_; + int focusedButton_; Window* msgHandler_; @@ -25,6 +45,12 @@ class iwMsgbox : public IngameWindow MsgboxIcon icon, unsigned msgboxid = 0); iwMsgbox(const std::string& title, const std::string& text, Window* msgHandler, MsgboxButton button, const ResourceId& iconFile, unsigned iconIdx, unsigned msgboxid = 0); + iwMsgbox(const std::string& title, const std::string& text, Window* msgHandler, MsgboxConfig config, + unsigned msgboxid = 0); + iwMsgbox(const std::string& title, const std::string& text, Window* msgHandler, MsgboxConfig config, + MsgboxIcon icon, unsigned msgboxid = 0); + iwMsgbox(const std::string& title, const std::string& text, Window* msgHandler, MsgboxConfig config, + const ResourceId& iconFile, unsigned iconIdx, unsigned msgboxid = 0); ~iwMsgbox() override; @@ -32,9 +58,10 @@ class iwMsgbox : public IngameWindow void MoveIcon(const DrawPoint& pos); private: - void Init(const std::string& text, const ResourceId& iconFile, unsigned iconIdx); + void Init(const std::string& text, glArchivItem_Bitmap* icon); void AddButton(unsigned short id, int x, const std::string& text, TextureColor tc); + bool Msg_KeyDown(const KeyEvent& ke) override; void Msg_ButtonClick(unsigned ctrl_id) override; }; diff --git a/tests/s25Main/UI/testIngameWindow.cpp b/tests/s25Main/UI/testIngameWindow.cpp index 8ef7fc6e48..d59406a20a 100644 --- a/tests/s25Main/UI/testIngameWindow.cpp +++ b/tests/s25Main/UI/testIngameWindow.cpp @@ -11,10 +11,13 @@ #include "controls/ctrlButton.h" #include "controls/ctrlComboBox.h" #include "controls/ctrlEdit.h" +#include "controls/ctrlImage.h" #include "controls/ctrlOptionGroup.h" #include "controls/ctrlPercent.h" #include "controls/ctrlProgress.h" +#include "controls/ctrlTextButton.h" #include "desktops/dskGameLobby.h" +#include "driver/KeyEvent.h" #include "drivers/VideoDriverWrapper.h" #include "helpers/format.hpp" #include "ingameWindows/IngameWindow.h" @@ -451,6 +454,62 @@ void WindowPositioning_testOne(IngameWindow& wnd, const char* context, const std } } // namespace +namespace { +class MsgboxResultCatcher : public Window +{ +public: + MsgboxResultCatcher() : Window(nullptr, 1, DrawPoint(0, 0)) {} + + void Msg_MsgBoxResult(unsigned msgbox_id, MsgboxResult mbr) override + { + id = msgbox_id; + result = mbr; + } + + unsigned id = 0; + MsgboxResult result = MsgboxResult::Cancel; +}; +} // namespace + +BOOST_AUTO_TEST_CASE(MsgboxCustomButtons) +{ + MsgboxResultCatcher handler; + { + iwMsgbox wnd("Custom", "Choose a custom result", &handler, + MsgboxConfig{{{"Apply", MsgboxResult::Ok, TextureColor::Green2}, + {"Later", MsgboxResult::No, TextureColor::Grey}, + {"Abort", MsgboxResult::Cancel, TextureColor::Red1}}, + 0, + 2}, + MsgboxIcon::QuestionRed, 42); + + const auto buttons = wnd.GetCtrls(); + BOOST_TEST_REQUIRE(buttons.size() == 3u); + BOOST_TEST(buttons[0]->GetText() == "Apply"); + BOOST_TEST(buttons[1]->GetText() == "Later"); + BOOST_TEST(buttons[2]->GetText() == "Abort"); + + static_cast(wnd).Msg_KeyDown(KeyEvent(KeyType::Return)); + BOOST_TEST(handler.id == 42u); + BOOST_TEST(static_cast(handler.result) == static_cast(MsgboxResult::Ok)); + BOOST_TEST(wnd.ShouldBeClosed()); + } + { + iwMsgbox wnd( + "Custom", "Escape should use the configured cancel button", &handler, + MsgboxConfig{ + {{"Yes", MsgboxResult::Yes, TextureColor::Green2}, {"No", MsgboxResult::No, TextureColor::Red1}}, 0, 1}, + 43); + + BOOST_TEST(wnd.GetCtrls().empty()); + + static_cast(wnd).Msg_KeyDown(KeyEvent(KeyType::Escape)); + BOOST_TEST(handler.id == 43u); + BOOST_TEST(static_cast(handler.result) == static_cast(MsgboxResult::No)); + BOOST_TEST(wnd.ShouldBeClosed()); + } +} + BOOST_AUTO_TEST_CASE(WindowPositioning) { VIDEODRIVER.ResizeScreen(VideoMode(800, 600), DisplayMode::Windowed); diff --git a/tests/s25Main/UI/testWindowManager.cpp b/tests/s25Main/UI/testWindowManager.cpp index aa1af96c05..f22a9b4d37 100644 --- a/tests/s25Main/UI/testWindowManager.cpp +++ b/tests/s25Main/UI/testWindowManager.cpp @@ -488,6 +488,7 @@ BOOST_FIXTURE_TEST_CASE(EscClosesWindow, uiHelper::Fixture) // ESC does not close non-user-closable windows wnd1 = &WINDOWMANAGER.Show(std::make_unique(CGI_HELP, false, CloseBehavior::Custom)); BOOST_TEST_REQUIRE(WINDOWMANAGER.GetTopMostWindow() == wnd1); + MOCK_EXPECT(wnd1->Msg_KeyDown).once().returns(false); WINDOWMANAGER.Msg_KeyDown(evEsc); REQUIRE_WINDOW_ALIVE(wnd1); MOCK_EXPECT(wnd1->DrawContent).once(); @@ -496,6 +497,7 @@ BOOST_FIXTURE_TEST_CASE(EscClosesWindow, uiHelper::Fixture) wnd2 = &WINDOWMANAGER.Show(std::make_unique(CGI_HELP, true, CloseBehavior::Custom)); BOOST_TEST_REQUIRE(WINDOWMANAGER.GetTopMostWindow() == wnd2); + MOCK_EXPECT(wnd2->Msg_KeyDown).once().returns(false); WINDOWMANAGER.Msg_KeyDown(evEsc); REQUIRE_WINDOW_ALIVE(wnd1); REQUIRE_WINDOW_ALIVE(wnd2);