From 8af5eb6c381b76596c315d71cb3f5840836334cd Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Thu, 27 Mar 2025 13:26:36 -0500 Subject: [PATCH] dialogs can have a button respond to Escape I'm planning to make it so the Enter key will never cancel a yes/no dialog. To do this, I'm adding an Escape button to dialogs. So cancel/accept keyboard shortcuts will be predictable and intuitive. Dialogs that require extra confirmation will have a 'really confirm' LED. --- rsrc/dialogs/help-debug.xml | 2 +- rsrc/schemas/dialog.xsd | 5 ++++ src/dialogxml/dialogs/dialog.cpp | 49 +++++++++++++++++++++---------- src/dialogxml/dialogs/dialog.hpp | 3 ++ src/dialogxml/widgets/control.hpp | 4 +++ src/doxy/mainpage.md | 6 ++-- 6 files changed, 50 insertions(+), 19 deletions(-) diff --git a/rsrc/dialogs/help-debug.xml b/rsrc/dialogs/help-debug.xml index 83562000..4c65f5de 100644 --- a/rsrc/dialogs/help-debug.xml +++ b/rsrc/dialogs/help-debug.xml @@ -1,6 +1,6 @@ - + DEBUG MODE HELP diff --git a/rsrc/schemas/dialog.xsd b/rsrc/schemas/dialog.xsd index 0b936bf9..aed40ce4 100644 --- a/rsrc/schemas/dialog.xsd +++ b/rsrc/schemas/dialog.xsd @@ -398,6 +398,7 @@ + @@ -411,6 +412,10 @@ + + + + diff --git a/src/dialogxml/dialogs/dialog.cpp b/src/dialogxml/dialogs/dialog.cpp index eca659ac..8e23ed72 100644 --- a/src/dialogxml/dialogs/dialog.cpp +++ b/src/dialogxml/dialogs/dialog.cpp @@ -158,7 +158,7 @@ void cDialog::loadFromFile(const DialogDefn& file){ Iterator attr; Iterator node; - string type, name, val, defbtn; + string type, name, val, defbtn, escbtn; xml.FirstChildElement()->GetValue(&type); if(type != "dialog") throw xBadNode(type,xml.FirstChildElement()->Row(),xml.FirstChildElement()->Column(),fname); @@ -181,8 +181,10 @@ void cDialog::loadFromFile(const DialogDefn& file){ throw xBadVal("text",name,val,attr->Row(),attr->Column(),fname); } defTextClr = clr; - } else if(name == "defbtn") { + }else if(name == "defbtn"){ defbtn = val; + }else if(name == "escbtn"){ + escbtn = val; }else if(name != "debug") throw xBadAttr(type,name,attr->Row(),attr->Column(),fname); } @@ -337,6 +339,8 @@ void cDialog::loadFromFile(const DialogDefn& file){ // Set the default button. setDefaultButton(defbtn); + // Set the escape button. + setEscapeButton(escbtn); // Sort by tab order // First, fill any gaps that might have been left, using ones that had no specific tab order @@ -912,11 +916,12 @@ bool cDialog::addLabelFor(std::string key, std::string label, eLabelPos where, s void cDialog::process_keystroke(cKey keyHit){ ctrlIter iter = controls.begin(); bool enterKeyHit = keyHit.spec && keyHit.k == key_enter; + bool escapeKeyHit = keyHit.spec && keyHit.k == key_esc; while(iter != controls.end()){ cControl* ctrl = iter->second; if(ctrl->isVisible()){ if(ctrl->isClickable() && - (ctrl->getAttachedKey() == keyHit || (ctrl->isDefault() && enterKeyHit))){ + (ctrl->getAttachedKey() == keyHit || (ctrl->isDefault() && enterKeyHit) || (ctrl->isEscape() && escapeKeyHit))){ ctrl->handleKeyTriggered(*this); return; @@ -940,11 +945,6 @@ void cDialog::process_keystroke(cKey keyHit){ } iter++; } - // If you get an escape and it isn't processed, make it an enter. - if(keyHit.spec && keyHit.k == key_esc){ - keyHit.k = key_enter; - process_keystroke(keyHit); - } } void cDialog::process_click(location where, eKeyMod mods, cFramerateLimiter& fps_limiter){ @@ -1157,21 +1157,38 @@ bool cDialog::hasControl(std::string id) const { return false; } -void cDialog::setDefaultButton(std::string defbtn) { - if(!defbtn.empty() && !hasControl(defbtn)){ +void cDialog::setSpecialButton(std::string& name_ref, std::string name, bool escape) { + if(!name.empty() && !hasControl(name)){ // this is likely because the dialogxml is malformed. maybe the linter already checks this, // but the engine might as well also. - throw std::string { "Requested default button does not exist: " } + defbtn; + throw std::string { "Requested button does not exist: " } + name; } - if(!defaultButton.empty()){ - getControl(defaultButton).setDefault(false); + if(!name_ref.empty()){ + if(escape){ + getControl(name_ref).setEscape(false); + }else{ + getControl(name_ref).setDefault(false); + } + name_ref = ""; } - if(!defbtn.empty()){ - defaultButton = defbtn; - getControl(defaultButton).setDefault(true); + if(!name.empty()){ + name_ref = name; + if(escape){ + getControl(name).setEscape(true); + }else{ + getControl(name).setDefault(true); + } } } +void cDialog::setDefaultButton(std::string defbtn) { + setSpecialButton(defaultButton, defbtn, false); +} + +void cDialog::setEscapeButton(std::string escbtn) { + setSpecialButton(escapeButton, escbtn, true); +} + const char*const xBadVal::CONTENT = "$content$"; cDialogIterator::cDialogIterator() : parent(nullptr) {} diff --git a/src/dialogxml/dialogs/dialog.hpp b/src/dialogxml/dialogs/dialog.hpp index 1c52039d..7009d5e8 100644 --- a/src/dialogxml/dialogs/dialog.hpp +++ b/src/dialogxml/dialogs/dialog.hpp @@ -266,7 +266,9 @@ public: inline void setAnimPictFPS(int fps) { if(fps == -1) fps = 2; anim_pict_fps = fps; } inline void setDoAnimations(bool value) { doAnimations = value; } void setDefaultButton(std::string defbtn); + void setEscapeButton(std::string escbtn); private: + void setSpecialButton(std::string& name_ref, std::string name, bool escape); void draw(); void handle_events(); void handle_one_event(const sf::Event&, cFramerateLimiter& fps_limiter); @@ -277,6 +279,7 @@ private: boost::any result; std::string fname; std::string defaultButton; + std::string escapeButton; sf::Clock animTimer, paintTimer; friend class cControl; friend class cContainer; diff --git a/src/dialogxml/widgets/control.hpp b/src/dialogxml/widgets/control.hpp index d0a8d8cb..4adb827d 100644 --- a/src/dialogxml/widgets/control.hpp +++ b/src/dialogxml/widgets/control.hpp @@ -172,6 +172,8 @@ public: std::string getAttachedKeyDescription() const; inline void setDefault(bool value) { isDefaultControl = value; } inline bool isDefault() { return isDefaultControl; } + inline void setEscape(bool value) { isEscapeControl = value; } + inline bool isEscape() { return isEscapeControl; } /// Attach an event handler to this control. /// @tparam t The type of event to attach. /// @param handler The event handler function or functor. Its signature depends on the event type. @@ -477,6 +479,8 @@ protected: cKey key; /// Whether the control is the default control of its dialog. bool isDefaultControl = false; + /// Whether the control is the cancel control of its dialog. + bool isEscapeControl = false; /// Draw a frame around the control. /// @param amt How much to offset the frame from the control's bounding rect. diff --git a/src/doxy/mainpage.md b/src/doxy/mainpage.md index ef0ee2f5..32c97b7f 100644 --- a/src/doxy/mainpage.md +++ b/src/doxy/mainpage.md @@ -75,8 +75,10 @@ such, this attribute should be omitted for most dialogs. to `true`, the XSL stylesheet will draw the bounding rects of LEDs and other debug information. * `fore` - The default text colour. Generally this shouldn't be needed. -* `defbtn` - The ID (`name` attribute) of the default button. This is an -IDREF, so it must exist in the dialog. +* `defbtn` - The ID (`name` attribute) of button triggered by Enter/Return. +This is an IDREF, so it must exist in the dialog. +* `escbtn` - The ID (`name` attribute) of button triggered by Escape. +This is an IDREF, so it must exist in the dialog. The `` tag ----------------