Close formspecs with a single tap outside (#14605)

This commit is contained in:
grorp 2024-05-09 19:16:08 +02:00 committed by GitHub
parent 178591b6d5
commit e0e1d0855d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 90 additions and 55 deletions

@ -4740,6 +4740,8 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
list_selected->changeItem(m_selected_item->i, stack_from); list_selected->changeItem(m_selected_item->i, stack_from);
} }
bool absorb_event = false;
// Possibly send inventory action to server // Possibly send inventory action to server
if (move_amount > 0) { if (move_amount > 0) {
// Send IAction::Move // Send IAction::Move
@ -4882,6 +4884,10 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
a->from_i = m_selected_item->i; a->from_i = m_selected_item->i;
m_invmgr->inventoryAction(a); m_invmgr->inventoryAction(a);
// Formspecs usually close when you click outside them, we absorb
// the event to prevent that. See GUIModalMenu::remapClickOutside.
absorb_event = true;
} else if (craft_amount > 0) { } else if (craft_amount > 0) {
assert(s.isValid()); assert(s.isValid());
@ -4911,6 +4917,9 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
m_selected_dragging = false; m_selected_dragging = false;
} }
m_old_pointer = m_pointer; m_old_pointer = m_pointer;
if (absorb_event)
return true;
} }
if (event.EventType == EET_GUI_EVENT) { if (event.EventType == EET_GUI_EVENT) {

@ -25,19 +25,39 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "client/renderingengine.h" #include "client/renderingengine.h"
#include "modalMenu.h" #include "modalMenu.h"
#include "gettext.h" #include "gettext.h"
#include "gui/guiInventoryList.h"
#include "porting.h" #include "porting.h"
#include "settings.h" #include "settings.h"
#include "touchscreengui.h" #include "touchscreengui.h"
PointerAction PointerAction::fromEvent(const SEvent &event) {
switch (event.EventType) {
case EET_MOUSE_INPUT_EVENT:
return {v2s32(event.MouseInput.X, event.MouseInput.Y), porting::getTimeMs()};
case EET_TOUCH_INPUT_EVENT:
return {v2s32(event.TouchInput.X, event.TouchInput.Y), porting::getTimeMs()};
default:
FATAL_ERROR("SEvent given to PointerAction::fromEvent has wrong EventType");
}
}
bool PointerAction::isRelated(PointerAction previous) {
u64 time_delta = porting::getDeltaMs(previous.time, time);
v2s32 pos_delta = pos - previous.pos;
f32 distance_sq = (f32)pos_delta.X * pos_delta.X + (f32)pos_delta.Y * pos_delta.Y;
return time_delta < 400 && distance_sq < (30.0f * 30.0f);
}
GUIModalMenu::GUIModalMenu(gui::IGUIEnvironment* env, gui::IGUIElement* parent, GUIModalMenu::GUIModalMenu(gui::IGUIEnvironment* env, gui::IGUIElement* parent,
s32 id, IMenuManager *menumgr, bool remap_dbl_click) : s32 id, IMenuManager *menumgr, bool remap_click_outside) :
IGUIElement(gui::EGUIET_ELEMENT, env, parent, id, IGUIElement(gui::EGUIET_ELEMENT, env, parent, id,
core::rect<s32>(0, 0, 100, 100)), core::rect<s32>(0, 0, 100, 100)),
#ifdef __ANDROID__ #ifdef __ANDROID__
m_jni_field_name(""), m_jni_field_name(""),
#endif #endif
m_menumgr(menumgr), m_menumgr(menumgr),
m_remap_dbl_click(remap_dbl_click) m_remap_click_outside(remap_click_outside)
{ {
m_gui_scale = std::max(g_settings->getFloat("gui_scaling"), 0.5f); m_gui_scale = std::max(g_settings->getFloat("gui_scaling"), 0.5f);
const float screen_dpi_scale = RenderingEngine::getDisplayDensity(); const float screen_dpi_scale = RenderingEngine::getDisplayDensity();
@ -50,9 +70,6 @@ GUIModalMenu::GUIModalMenu(gui::IGUIEnvironment* env, gui::IGUIElement* parent,
setVisible(true); setVisible(true);
m_menumgr->createdMenu(this); m_menumgr->createdMenu(this);
m_last_touch.time = 0;
m_last_touch.pos = v2s32(0, 0);
} }
GUIModalMenu::~GUIModalMenu() GUIModalMenu::~GUIModalMenu()
@ -115,42 +132,53 @@ static bool isChild(gui::IGUIElement *tocheck, gui::IGUIElement *parent)
return false; return false;
} }
bool GUIModalMenu::remapDoubleClick(const SEvent &event) bool GUIModalMenu::remapClickOutside(const SEvent &event)
{ {
/* The following code is for capturing double-clicks of the mouse button if (!m_remap_click_outside || event.EventType != EET_MOUSE_INPUT_EVENT ||
* and translating the double-click into an EET_KEY_INPUT_EVENT event (event.MouseInput.Event != EMIE_LMOUSE_PRESSED_DOWN &&
* -- which closes the form -- under some circumstances. event.MouseInput.Event != EMIE_LMOUSE_LEFT_UP))
*
* There have been many github issues reporting this as a bug even though it
* was an intended feature. For this reason, remapping the double-click as
* an ESC must be explicitly set when creating this class via the
* /p remap_dbl_click parameter of the constructor.
*/
if (!m_remap_dbl_click)
return false; return false;
if (event.EventType != EET_MOUSE_INPUT_EVENT || // The formspec must only be closed if both the EMIE_LMOUSE_PRESSED_DOWN and
event.MouseInput.Event != EMIE_LMOUSE_DOUBLE_CLICK) // the EMIE_LMOUSE_LEFT_UP event haven't been absorbed by something else.
return false;
PointerAction last = m_last_click_outside;
m_last_click_outside = {}; // always reset
PointerAction current = PointerAction::fromEvent(event);
// Only exit if the double-click happened outside the menu.
gui::IGUIElement *hovered = gui::IGUIElement *hovered =
Environment->getRootGUIElement()->getElementFromPoint(m_pointer); Environment->getRootGUIElement()->getElementFromPoint(current.pos);
if (isChild(hovered, this)) if (isChild(hovered, this))
return false; return false;
// Translate double-click to escape. // Dropping items is also done by tapping outside the formspec. If an item
SEvent translated{}; // is selected, make sure it is dropped without closing the formspec.
translated.EventType = EET_KEY_INPUT_EVENT; // We have to explicitly restrict this to GUIInventoryList because other
translated.KeyInput.Key = KEY_ESCAPE; // GUI elements like text fields like to absorb events for no reason.
translated.KeyInput.Control = false; GUIInventoryList *focused = dynamic_cast<GUIInventoryList *>(Environment->getFocus());
translated.KeyInput.Shift = false; if (focused && focused->OnEvent(event))
translated.KeyInput.PressedDown = true; // Return true since the event was handled, even if it wasn't handled by us.
translated.KeyInput.Char = 0; return true;
OnEvent(translated);
return true; if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) {
m_last_click_outside = current;
return true;
}
if (event.MouseInput.Event == EMIE_LMOUSE_LEFT_UP &&
current.isRelated(last)) {
SEvent translated{};
translated.EventType = EET_KEY_INPUT_EVENT;
translated.KeyInput.Key = KEY_ESCAPE;
translated.KeyInput.Control = false;
translated.KeyInput.Shift = false;
translated.KeyInput.PressedDown = true;
translated.KeyInput.Char = 0;
OnEvent(translated);
return true;
}
return false;
} }
bool GUIModalMenu::simulateMouseEvent(ETOUCH_INPUT_EVENT touch_event, bool second_try) bool GUIModalMenu::simulateMouseEvent(ETOUCH_INPUT_EVENT touch_event, bool second_try)
@ -319,20 +347,12 @@ bool GUIModalMenu::preprocessEvent(const SEvent &event)
// Detect double-taps and convert them into double-click events. // Detect double-taps and convert them into double-click events.
if (event.TouchInput.Event == ETIE_PRESSED_DOWN) { if (event.TouchInput.Event == ETIE_PRESSED_DOWN) {
u64 time_now = porting::getTimeMs(); PointerAction current = PointerAction::fromEvent(event);
u64 time_delta = porting::getDeltaMs(m_last_touch.time, time_now); if (current.isRelated(m_last_touch)) {
v2s32 pos_delta = m_pointer - m_last_touch.pos;
f32 distance_sq = (f32)pos_delta.X * pos_delta.X +
(f32)pos_delta.Y * pos_delta.Y;
if (time_delta < 400 && distance_sq < (30 * 30)) {
// ETIE_COUNT is used for double-tap events. // ETIE_COUNT is used for double-tap events.
simulateMouseEvent(ETIE_COUNT); simulateMouseEvent(ETIE_COUNT);
} }
m_last_touch = current;
m_last_touch.time = time_now;
m_last_touch.pos = m_pointer;
} }
return ret; return ret;
@ -361,7 +381,7 @@ bool GUIModalMenu::preprocessEvent(const SEvent &event)
m_touch_hovered.reset(); m_touch_hovered.reset();
} }
if (remapDoubleClick(event)) if (remapClickOutside(event))
return true; return true;
} }

@ -31,6 +31,14 @@ enum class PointerType {
Touch, Touch,
}; };
struct PointerAction {
v2s32 pos;
u64 time; // ms
static PointerAction fromEvent(const SEvent &event);
bool isRelated(PointerAction other);
};
class GUIModalMenu; class GUIModalMenu;
class IMenuManager class IMenuManager
@ -94,14 +102,14 @@ class GUIModalMenu : public gui::IGUIElement
private: private:
IMenuManager *m_menumgr; IMenuManager *m_menumgr;
/* If true, remap a double-click (or double-tap) action to ESC. This is so /* If true, remap a click outside the formspec to ESC. This is so that, for
* that, for example, Android users can double-tap to close a formspec. * example, touchscreen users can close formspecs.
* * The default for this setting is true. Currently, it's set to false for
* This value can (currently) only be set by the class constructor * the mainmenu to prevent Minetest from closing unexpectedly.
* and the default value for the setting is true.
*/ */
bool m_remap_dbl_click; bool m_remap_click_outside;
bool remapDoubleClick(const SEvent &event); bool remapClickOutside(const SEvent &event);
PointerAction m_last_click_outside{};
// This might be necessary to expose to the implementation if it // This might be necessary to expose to the implementation if it
// wants to launch other menus // wants to launch other menus
@ -111,13 +119,11 @@ class GUIModalMenu : public gui::IGUIElement
irr_ptr<gui::IGUIElement> m_touch_hovered; irr_ptr<gui::IGUIElement> m_touch_hovered;
// Converts touches into clicks.
bool simulateMouseEvent(ETOUCH_INPUT_EVENT touch_event, bool second_try=false); bool simulateMouseEvent(ETOUCH_INPUT_EVENT touch_event, bool second_try=false);
void enter(gui::IGUIElement *element); void enter(gui::IGUIElement *element);
void leave(); void leave();
// Used to detect double-taps and convert them into double-click events. // Used to detect double-taps and convert them into double-click events.
struct { PointerAction m_last_touch{};
v2s32 pos;
s64 time;
} m_last_touch;
}; };