From 8f28f8d968a79e8cea8a4c7c8ebf5c10a80745b3 Mon Sep 17 00:00:00 2001 From: Victor Sergienko Date: Sat, 23 Jun 2018 23:24:54 -0700 Subject: [PATCH] iOS: Long-touch context menu. Work around event queue delays. --- src/controller_base.cpp | 62 +++++++++++++++++- src/controller_base.hpp | 6 ++ src/gui/dialogs/drop_down_menu.cpp | 102 ++++++++++++++++------------- src/gui/dialogs/drop_down_menu.hpp | 11 ++++ src/mouse_handler_base.cpp | 5 ++ src/mouse_handler_base.hpp | 3 + 6 files changed, 143 insertions(+), 46 deletions(-) diff --git a/src/controller_base.cpp b/src/controller_base.cpp index 6cb7140d203d..b3c9a4e96dad 100644 --- a/src/controller_base.cpp +++ b/src/controller_base.cpp @@ -17,8 +17,8 @@ #include "display.hpp" #include "events.hpp" +#include "preferences/game.hpp" #include "hotkey/command_executor.hpp" -#include "hotkey/hotkey_command.hpp" #include "log.hpp" #include "map/map.hpp" #include "mouse_handler_base.hpp" @@ -26,9 +26,13 @@ #include "scripting/plugins/context.hpp" #include "show_dialog.hpp" //gui::in_dialog #include "soundsource.hpp" +#include "gui/core/timer.hpp" + static lg::log_domain log_display("display"); #define ERR_DP LOG_STREAM(err, log_display) +static const int long_touch_duration_ms = 800; + controller_base::controller_base(const config& game_config) : game_config_(game_config) , key_() @@ -39,11 +43,48 @@ controller_base::controller_base(const config& game_config) , scroll_right_(false) , joystick_manager_() , key_release_listener_(*this) + , last_mouse_is_touch_(false) + , long_touch_timer_(0) { } controller_base::~controller_base() { + if(long_touch_timer_ != 0) { + gui2::remove_timer(long_touch_timer_); + long_touch_timer_ = 0; + } +} + +void controller_base::long_touch_callback(int x, int y) +{ + if(long_touch_timer_ != 0 && !get_mouse_handler_base().dragging_started()) { + int x_now; + int y_now; + uint32_t mouse_state = SDL_GetMouseState(&x_now, &y_now); + +#ifdef MOUSE_TOUCH_EMULATION + if(mouse_state & SDL_BUTTON(SDL_BUTTON_RIGHT)) { + // Monkey-patch touch controls again to make them look like left button. + mouse_state = SDL_BUTTON(SDL_BUTTON_LEFT); + } +#endif + + // Workaround for double-menu b/c of slow events processing, or I don't know. + int dx = x - x_now; + int dy = y - y_now; + int threshold = get_mouse_handler_base().drag_threshold(); + bool yes_actually_dragging = dx * dx + dy * dy >= threshold * threshold; + + if(!yes_actually_dragging && (mouse_state & SDL_BUTTON(SDL_BUTTON_LEFT)) != 0) { + const theme::menu* const m = get_mouse_handler_base().gui().get_theme().context_menu(); + if(m != nullptr) { + show_menu(get_display().get_theme().context_menu()->items(), x_now, y_now, true, get_display()); + } + } + } + + long_touch_timer_ = 0; } void controller_base::handle_event(const SDL_Event& event) @@ -129,6 +170,14 @@ void controller_base::handle_event(const SDL_Event& event) break; case SDL_MOUSEBUTTONDOWN: + last_mouse_is_touch_ = event.button.which == SDL_TOUCH_MOUSEID; + + if(last_mouse_is_touch_ && long_touch_timer_ == 0) { + long_touch_timer_ = gui2::add_timer( + long_touch_duration_ms, + std::bind(&controller_base::long_touch_callback, this, event.button.x, event.button.y)); + } + mh_base.mouse_press(event.button, is_browsing()); hotkey::mbutton_event(event, get_hotkey_command_executor()); break; @@ -138,6 +187,13 @@ void controller_base::handle_event(const SDL_Event& event) break; case SDL_MOUSEBUTTONUP: + if(long_touch_timer_ != 0) { + gui2::remove_timer(long_touch_timer_); + long_touch_timer_ = 0; + } + + last_mouse_is_touch_ = event.button.which == SDL_TOUCH_MOUSEID; + mh_base.mouse_press(event.button, is_browsing()); if(mh_base.get_show_menu()) { show_menu(get_display().get_theme().context_menu()->items(), event.button.x, event.button.y, true, @@ -157,6 +213,10 @@ void controller_base::handle_event(const SDL_Event& event) #endif break; + case TIMER_EVENT: + gui2::execute_timer(reinterpret_cast(event.user.data1)); + break; + // TODO: Support finger specifically, like pan the map. For now, SDL's "shadow mouse" events will do. case SDL_MULTIGESTURE: default: diff --git a/src/controller_base.hpp b/src/controller_base.hpp index f3538c1892e4..9d04d6a50916 100644 --- a/src/controller_base.hpp +++ b/src/controller_base.hpp @@ -178,6 +178,8 @@ class controller_base : public video2::draw_layering, public events::pump_monito virtual void execute_action(const std::vector& items_arg, int xloc, int yloc, bool context_menu); virtual bool in_context_menu(hotkey::HOTKEY_COMMAND command) const; + + void long_touch_callback(int x, int y); const config& game_config_; @@ -213,4 +215,8 @@ class controller_base : public video2::draw_layering, public events::pump_monito }; keyup_listener key_release_listener_; + + bool last_mouse_is_touch_; + /** Context menu timer */ + size_t long_touch_timer_; }; diff --git a/src/gui/dialogs/drop_down_menu.cpp b/src/gui/dialogs/drop_down_menu.cpp index 0abaaad95678..87dbba7b44b7 100644 --- a/src/gui/dialogs/drop_down_menu.cpp +++ b/src/gui/dialogs/drop_down_menu.cpp @@ -38,54 +38,12 @@ REGISTER_DIALOG(drop_down_menu) namespace { - void click_callback(window& window, bool keep_open, bool&, bool&, point coordinate) - { - listbox& list = find_widget(&window, "list", true); - - /* Disregard clicks on scrollbars and toggle buttons so the dropdown menu can be scrolled or have an embedded - * toggle button selected without the menu closing. - * - * This works since this click_callback function is called before widgets' left-button-up handlers. - * - * Additionally, this is done before row deselection so selecting/deselecting a toggle button doesn't also leave - * the list with no row visually selected. Oddly, the visial deselection doesn't seem to cause any crashes, and - * the previously selected row is reselected when the menu is opened again. Still, it's odd to see your selection - * vanish. - */ - if(list.vertical_scrollbar()->get_state() == scrollbar_base::PRESSED) { - return; - } - - if(dynamic_cast(window.find_at(coordinate, true)) != nullptr) { - return; - } - - /* FIXME: This dialog uses a listbox with 'has_minimum = false'. This allows a listbox to have 0 or 1 selections, - * and selecting the same entry toggles that entry's state (ie, if it was selected, it will be deselected). Because - * of this, selecting the same entry in the dropdown list essentially sets the list's selected row to -1, causing problems. - * - * In order to work around this, we first manually deselect the selected entry here. This handler is called *before* - * the listbox's click handler, and as such the selected item will remain toggled on when the click handler fires. - */ - const int sel = list.get_selected_row(); - if(sel >= 0) { - list.select_row(sel, false); - } - - SDL_Rect rect = window.get_rectangle(); - if(!sdl::point_in_rect(coordinate, rect)) { - window.set_retval(retval::CANCEL); - } else if(!keep_open) { - window.set_retval(retval::OK); - } - } - void callback_flip_embedded_toggle(window& window) { listbox& list = find_widget(&window, "list", true); /* If the currently selected row has a toggle button, toggle it. - * Note this cannot be handled in click_callback since at that point the new row selection has not registered, + * Note this cannot be handled in mouse_up_callback since at that point the new row selection has not registered, * meaning the currently selected row's button is toggled. */ grid* row_grid = list.get_row_grid(list.get_selected_row()); @@ -101,6 +59,57 @@ namespace } } +void drop_down_menu::mouse_up_callback(window& window, bool&, bool&, const point& coordinate) +{ + if(!mouse_down_happened_) { + return; + } + + listbox& list = find_widget(&window, "list", true); + + /* Disregard clicks on scrollbars and toggle buttons so the dropdown menu can be scrolled or have an embedded + * toggle button selected without the menu closing. + * + * This works since this mouse_up_callback function is called before widgets' left-button-up handlers. + * + * Additionally, this is done before row deselection so selecting/deselecting a toggle button doesn't also leave + * the list with no row visually selected. Oddly, the visial deselection doesn't seem to cause any crashes, and + * the previously selected row is reselected when the menu is opened again. Still, it's odd to see your selection + * vanish. + */ + if(list.vertical_scrollbar()->get_state() == scrollbar_base::PRESSED) { + return; + } + + if(dynamic_cast(window.find_at(coordinate, true)) != nullptr) { + return; + } + + /* FIXME: This dialog uses a listbox with 'has_minimum = false'. This allows a listbox to have 0 or 1 selections, + * and selecting the same entry toggles that entry's state (ie, if it was selected, it will be deselected). Because + * of this, selecting the same entry in the dropdown list essentially sets the list's selected row to -1, causing problems. + * + * In order to work around this, we first manually deselect the selected entry here. This handler is called *before* + * the listbox's click handler, and as such the selected item will remain toggled on when the click handler fires. + */ + const int sel = list.get_selected_row(); + if(sel >= 0) { + list.select_row(sel, false); + } + + SDL_Rect rect = window.get_rectangle(); + if(!sdl::point_in_rect(coordinate, rect)) { + window.set_retval(retval::CANCEL); + } else if(!keep_open_) { + window.set_retval(retval::OK); + } +} + +void drop_down_menu::mouse_down_callback() +{ + mouse_down_happened_ = true; +} + void drop_down_menu::pre_show(window& window) { window.set_variable("button_x", wfl::variant(button_pos_.x)); @@ -170,10 +179,13 @@ void drop_down_menu::pre_show(window& window) // Dismiss on click outside the window window.connect_signal( - std::bind(&click_callback, std::ref(window), keep_open_, _3, _4, _5), event::dispatcher::front_child); + std::bind(&drop_down_menu::mouse_up_callback, this, std::ref(window), _3, _4, _5), event::dispatcher::front_child); window.connect_signal( - std::bind(&click_callback, std::ref(window), keep_open_, _3, _4, _5), event::dispatcher::front_child); + std::bind(&drop_down_menu::mouse_up_callback, this, std::ref(window), _3, _4, _5), event::dispatcher::front_child); + + window.connect_signal( + std::bind(&drop_down_menu::mouse_down_callback, this), event::dispatcher::front_child); // Handle embedded button toggling. // For some reason this works as a listbox value callback but don't ask me why. diff --git a/src/gui/dialogs/drop_down_menu.hpp b/src/gui/dialogs/drop_down_menu.hpp index 0abc4cd25c90..45b4b7d13126 100644 --- a/src/gui/dialogs/drop_down_menu.hpp +++ b/src/gui/dialogs/drop_down_menu.hpp @@ -37,6 +37,7 @@ class drop_down_menu : public modal_dialog , selected_item_(selected_item) , use_markup_(use_markup) , keep_open_(keep_open) + , mouse_down_happened_(false) , callback_toggle_state_change_(callback_toggle_state_change) { set_restore(true); @@ -71,6 +72,12 @@ class drop_down_menu : public modal_dialog */ bool keep_open_; + /** + * When menu is invoked on a long-touch timer, a following mouse-up event will close it. + * This flag prevents that: the menu will only be closed on a mouse-up that follows a mouse-down. + * */ + bool mouse_down_happened_; + /** * If toggle buttons are used, this callback is called whenever the state of any toggle * button changes. @@ -85,6 +92,10 @@ class drop_down_menu : public modal_dialog /** Inherited from modal_dialog. */ virtual void post_show(window& window) override; + + void mouse_up_callback(window& window, bool&, bool&, const point& coordinate); + + void mouse_down_callback(); }; } // namespace dialogs diff --git a/src/mouse_handler_base.cpp b/src/mouse_handler_base.cpp index 02364ff7ce99..606af32a899b 100644 --- a/src/mouse_handler_base.cpp +++ b/src/mouse_handler_base.cpp @@ -66,6 +66,11 @@ mouse_handler_base::mouse_handler_base() { } +bool mouse_handler_base::dragging_started() const +{ + return dragging_started_; +} + bool mouse_handler_base::is_dragging() const { return dragging_left_ || dragging_right_ || dragging_touch_; diff --git a/src/mouse_handler_base.hpp b/src/mouse_handler_base.hpp index 7855fdbf2093..cccb2642bf1f 100644 --- a/src/mouse_handler_base.hpp +++ b/src/mouse_handler_base.hpp @@ -48,6 +48,9 @@ class mouse_handler_base /** Const version of @ref gui */ virtual const display& gui() const = 0; + + /** If mouse/finger has moved far enough to consider it move/swipe, and not a click/touch */ + bool dragging_started() const; /** * @return true when the class in the "dragging" state.