diff options
Diffstat (limited to 'src/frontend/mame/ui/menu.cpp')
-rw-r--r-- | src/frontend/mame/ui/menu.cpp | 1622 |
1 files changed, 1234 insertions, 388 deletions
diff --git a/src/frontend/mame/ui/menu.cpp b/src/frontend/mame/ui/menu.cpp index dbe1b29a813..00710e073a9 100644 --- a/src/frontend/mame/ui/menu.cpp +++ b/src/frontend/mame/ui/menu.cpp @@ -13,7 +13,6 @@ #include "ui/ui.h" #include "ui/mainmenu.h" -#include "ui/utils.h" #include "ui/miscmenu.h" #include "cheat.h" @@ -25,9 +24,12 @@ #include "rendutil.h" #include "uiinput.h" -#include <algorithm> +#include "osdepend.h" + #include <cassert> -#include <cmath> +#include <cstdlib> +#include <limits> +#include <type_traits> namespace ui { @@ -74,6 +76,12 @@ menu::global_state::global_state(mame_ui_manager &ui) , m_stack() , m_free() , m_hide(false) + , m_current_pointer(-1) + , m_pointer_type(ui_event::pointer::UNKNOWN) + , m_pointer_buttons(0U) + , m_pointer_x(-1.0F) + , m_pointer_y(-1.0F) + , m_pointer_hit(false) { render_manager &render(ui.machine().render()); @@ -119,6 +127,21 @@ void menu::global_state::stack_push(std::unique_ptr<menu> &&menu) } menu->m_parent = std::move(m_stack); m_stack = std::move(menu); + + ui_event uievt; + while (m_stack->machine().ui_input().pop_event(&uievt)) + { + switch (uievt.event_type) + { + case ui_event::type::POINTER_UPDATE: + case ui_event::type::POINTER_LEAVE: + case ui_event::type::POINTER_ABORT: + use_pointer(m_stack->machine().render().ui_target(), m_stack->container(), uievt); + break; + default: + break; + } + } m_stack->machine().ui_input().reset(); } @@ -139,7 +162,22 @@ void menu::global_state::stack_pop() m_stack = std::move(menu->m_parent); menu->m_parent = std::move(m_free); m_free = std::move(menu); - m_ui.machine().ui_input().reset(); + + ui_event uievt; + while (m_free->machine().ui_input().pop_event(&uievt)) + { + switch (uievt.event_type) + { + case ui_event::type::POINTER_UPDATE: + case ui_event::type::POINTER_LEAVE: + case ui_event::type::POINTER_ABORT: + use_pointer(m_free->machine().render().ui_target(), m_free->container(), uievt); + break; + default: + break; + } + } + m_free->machine().ui_input().reset(); } } @@ -184,40 +222,152 @@ uint32_t menu::global_state::ui_handler(render_container &container) if (!m_stack) stack_push(std::make_unique<menu_main>(m_ui, container)); - // ensure topmost menu is active - need a loop because it could push another menu - while (m_stack && !m_stack->is_active()) + while (true) { - m_stack->m_active = true; - m_stack->menu_activated(); - } + // ensure topmost menu is active - need a loop because it could push another menu + while (m_stack && !m_stack->is_active()) + { + m_stack->activate_menu(); + if (m_stack && m_stack->is_active()) + { + // menu activated - draw it to ensure it's on-screen before it can process input + m_stack->check_metrics(); + m_stack->do_rebuild(); + m_stack->validate_selection(1); + m_stack->do_draw_menu(); + assert(m_stack); + assert(m_stack->is_active()); + + // display pointer if appropriate + mame_ui_manager::display_pointer pointers[1]{ { m_stack->machine().render().ui_target(), m_pointer_type, m_pointer_x, m_pointer_y } }; + if ((0 <= m_current_pointer) && (ui_event::pointer::TOUCH != m_pointer_type)) + m_ui.set_pointers(std::begin(pointers), std::end(pointers)); + else + m_ui.set_pointers(std::begin(pointers), std::begin(pointers)); - // update the menu state - m_hide = false; - if (m_stack) - m_stack->do_handle(); + return mame_ui_manager::HANDLER_UPDATE; + } + } - // clear up anything pending being released - clear_free_list(); + // update the menu state + m_hide = false; + bool need_update(m_stack && m_stack->do_handle()); - // if the menus are to be hidden, return a cancel here - if (m_ui.is_menu_active() && (m_hide || !m_stack)) - { - if (m_stack) + // clear up anything pending being released + clear_free_list(); + + // if the menus are to be hidden, return a cancel here + if (m_ui.is_menu_active() && (m_hide || !m_stack)) { - if (m_stack->is_one_shot()) + if (m_stack) { - stack_pop(); + if (m_stack->is_one_shot()) + { + stack_pop(); + } + else if (m_stack->is_active()) + { + m_stack->m_active = false; + m_stack->menu_deactivated(); + } } - else if (m_stack->is_active()) + + // forget about pointers while menus aren't handling events + m_current_pointer = -1; + m_pointer_type = ui_event::pointer::UNKNOWN; + m_pointer_buttons = 0U; + m_pointer_x = -1.0F; + m_pointer_y = -1.0F; + m_pointer_hit = false; + + return mame_ui_manager::HANDLER_CANCEL; + } + + // if the menu is still active, draw it, otherwise try again + if (m_stack->is_active()) + { + m_stack->do_draw_menu(); + + // display pointer if appropriate + mame_ui_manager::display_pointer pointers[1]{ { m_stack->machine().render().ui_target(), m_pointer_type, m_pointer_x, m_pointer_y } }; + if ((0 <= m_current_pointer) && (ui_event::pointer::TOUCH != m_pointer_type)) + m_ui.set_pointers(std::begin(pointers), std::end(pointers)); + else + m_ui.set_pointers(std::begin(pointers), std::begin(pointers)); + + return need_update ? mame_ui_manager::HANDLER_UPDATE : 0; + } + } +} + + +std::pair<bool, bool> menu::global_state::use_pointer(render_target &target, render_container &container, ui_event const &uievt) +{ + if (&target != uievt.target) + return std::make_pair(false, false); + + switch (uievt.event_type) + { + case ui_event::type::POINTER_UPDATE: + // if it's our current pointer, just update it + if (uievt.pointer_id == m_current_pointer) + { + assert(uievt.pointer_type == m_pointer_type); + assert(uievt.pointer_buttons == ((m_pointer_buttons & ~uievt.pointer_released) | uievt.pointer_pressed)); + + m_pointer_buttons = uievt.pointer_buttons; + m_pointer_hit = target.map_point_container( + uievt.pointer_x, + uievt.pointer_y, + container, + m_pointer_x, + m_pointer_y); + return std::make_pair(true, false); + } + + // don't change if the current pointer has buttons pressed and this one doesn't + if ((0 > m_current_pointer) || (!m_pointer_buttons && (!m_pointer_hit || uievt.pointer_pressed))) + { + float x, y; + bool const hit(target.map_point_container(uievt.pointer_x, uievt.pointer_y, container, x, y)); + if ((0 > m_current_pointer) || uievt.pointer_pressed || (!m_pointer_hit && hit)) { - m_stack->m_active = false; - m_stack->menu_deactivated(); + m_current_pointer = uievt.pointer_id; + m_pointer_type = uievt.pointer_type; + m_pointer_buttons = uievt.pointer_buttons; + m_pointer_x = x; + m_pointer_y = y; + m_pointer_hit = hit; + return std::make_pair(true, true); } } - return UI_HANDLER_CANCEL; - } - return 0; + // keep current pointer + return std::make_pair(false, false); + + case ui_event::type::POINTER_LEAVE: + case ui_event::type::POINTER_ABORT: + // irrelevant if it isn't our current pointer + if (uievt.pointer_id != m_current_pointer) + return std::make_pair(false, false); + + assert(uievt.pointer_type == m_pointer_type); + assert(uievt.pointer_released == m_pointer_buttons); + + // keep the coordinates where we lost the pointer + m_current_pointer = -1; + m_pointer_buttons = 0U; + m_pointer_hit = target.map_point_container( + uievt.pointer_x, + uievt.pointer_y, + container, + m_pointer_x, + m_pointer_y); + return std::make_pair(true, false); + + default: + std::abort(); + } } @@ -238,22 +388,40 @@ menu::menu(mame_ui_manager &mui, render_container &container) , m_heading() , m_items() , m_rebuilding(false) + , m_last_size(0, 0) + , m_last_aspect(0.0F) + , m_line_height(0.0F) + , m_gutter_width(0.0F) + , m_tb_border(0.0F) + , m_lr_border(0.0F) + , m_lr_arrow_width(0.0F) + , m_ud_arrow_width(0.0F) + , m_items_left(0.0F) + , m_items_right(0.0F) + , m_items_top(0.0F) + , m_adjust_top(0.0F) + , m_adjust_bottom(0.0F) + , m_decrease_left(0.0F) + , m_increase_left(0.0F) + , m_show_up_arrow(false) + , m_show_down_arrow(false) + , m_items_drawn(false) + , m_pointer_state(track_pointer::IDLE) + , m_pointer_down(0.0F, 0.0F) + , m_pointer_updated(0.0F, 0.0F) + , m_pointer_line(0) + , m_pointer_repeat(std::chrono::steady_clock::time_point::min()) + , m_accumulated_wheel(0) , m_process_flags(0) , m_selected(0) - , m_hover(1) , m_special_main_menu(false) , m_one_shot(false) , m_needs_prev_menu_item(true) , m_active(false) - , m_event() , m_customtop(0.0F) , m_custombottom(0.0F) , m_resetpos(0) , m_resetref(nullptr) - , m_mouse_hit(false) - , m_mouse_button(false) - , m_mouse_x(-1.0F) - , m_mouse_y(-1.0F) { reset(reset_options::SELECT_FIRST); @@ -278,6 +446,10 @@ menu::~menu() void menu::reset(reset_options options) { + // don't accept pointer input until the menu has been redrawn + m_items_drawn = false; + m_pointer_state = track_pointer::IDLE; + // based on the reset option, set the reset info m_resetpos = 0; m_resetref = nullptr; @@ -309,10 +481,13 @@ void menu::set_special_main_menu(bool special) // end of the menu //------------------------------------------------- -void menu::item_append(menu_item_type type, uint32_t flags) +int menu::item_append(menu_item_type type, uint32_t flags) { + assert(menu_item_type::SEPARATOR == type); if (type == menu_item_type::SEPARATOR) - item_append(MENU_SEPARATOR_ITEM, flags, nullptr, menu_item_type::SEPARATOR); + return item_append(MENU_SEPARATOR_ITEM, flags, nullptr, menu_item_type::SEPARATOR); + else + return -1; } //------------------------------------------------- @@ -320,7 +495,7 @@ void menu::item_append(menu_item_type type, uint32_t flags) // end of the menu //------------------------------------------------- -void menu::item_append(std::string &&text, std::string &&subtext, uint32_t flags, void *ref, menu_item_type type) +int menu::item_append(std::string &&text, std::string &&subtext, uint32_t flags, void *ref, menu_item_type type) { assert(m_rebuilding); @@ -346,6 +521,8 @@ void menu::item_append(std::string &&text, std::string &&subtext, uint32_t flags m_selected = index; if (m_resetpos == (m_items.size() - 1)) m_selected = m_items.size() - 1; + + return int(std::make_signed_t<decltype(index)>(index)); } @@ -354,68 +531,26 @@ void menu::item_append(std::string &&text, std::string &&subtext, uint32_t flags // item to the end of the menu //------------------------------------------------- -void menu::item_append_on_off(const std::string &text, bool state, uint32_t flags, void *ref, menu_item_type type) +int menu::item_append_on_off(const std::string &text, bool state, uint32_t flags, void *ref, menu_item_type type) { if (flags & FLAG_DISABLE) ref = nullptr; else flags |= state ? FLAG_LEFT_ARROW : FLAG_RIGHT_ARROW; - item_append(std::string(text), state ? _("On") : _("Off"), flags, ref, type); + return item_append(std::string(text), state ? _("On") : _("Off"), flags, ref, type); } //------------------------------------------------- -// process - process a menu, drawing it -// and returning any interesting events +// set_custom_space - set space required for +// custom rendering above and below menu //------------------------------------------------- -const menu::event *menu::process() +void menu::set_custom_space(float top, float bottom) { - // reset the event - m_event.iptkey = IPT_INVALID; - - // first make sure our selection is valid - validate_selection(1); - - // if we're not running the emulation, draw parent menus in the background - auto const draw_parent = - [] (auto &self, menu *parent) -> bool - { - if (!parent || !(parent->is_special_main_menu() || self(self, parent->m_parent.get()))) - return false; - else - parent->draw(PROCESS_NOINPUT); - return true; - }; - if (draw_parent(draw_parent, m_parent.get())) - container().add_rect(0.0F, 0.0F, 1.0F, 1.0F, rgb_t(114, 0, 0, 0), PRIMFLAG_BLENDMODE(BLENDMODE_ALPHA)); - - // draw the menu proper - draw(m_process_flags); - - // process input - if (!(m_process_flags & (PROCESS_NOKEYS | PROCESS_NOINPUT))) - { - // read events - handle_events(m_process_flags, m_event); - - // handle the keys if we don't already have an event - if (m_event.iptkey == IPT_INVALID) - handle_keys(m_process_flags, m_event.iptkey); - } - - // update the selected item in the event - if ((m_event.iptkey != IPT_INVALID) && selection_valid()) - { - m_event.itemref = get_selection_ref(); - m_event.item = &m_items[m_selected]; - return &m_event; - } - else - { - return nullptr; - } + m_customtop = top; + m_custombottom = bottom; } @@ -444,33 +579,37 @@ void menu::set_selection(void *selected_itemref) ***************************************************************************/ //------------------------------------------------- -// draw - draw a menu +// do_draw_menu - draw a menu //------------------------------------------------- -void menu::draw(uint32_t flags) +void menu::do_draw_menu() { - // first draw the FPS counter - // FIXME: provide a way to do this in the UI manager itself while menus are on-screen - if (ui().show_fps_counter()) - { - ui().draw_text_full( - container(), - machine().video().speed_text(), - 0.0F, 0.0F, 1.0F, - text_layout::text_justify::RIGHT, text_layout::word_wrapping::WORD, - mame_ui_manager::OPAQUE_, rgb_t::white(), rgb_t::black(), - nullptr, nullptr); - } + // if we're not running the emulation, draw parent menus in the background + auto const draw_parent = + [] (auto &self, menu *parent) -> bool + { + if (!parent || !(parent->is_special_main_menu() || self(self, parent->m_parent.get()))) + return false; + else + parent->draw(PROCESS_NOINPUT); + return true; + }; + if (draw_parent(draw_parent, m_parent.get())) + container().add_rect(0.0F, 0.0F, 1.0F, 1.0F, rgb_t(114, 0, 0, 0), PRIMFLAG_BLENDMODE(BLENDMODE_ALPHA)); + + // draw the menu proper + draw(m_process_flags); +} + + +//------------------------------------------------- +// draw - draw the menu itself +//------------------------------------------------- +void menu::draw(uint32_t flags) +{ bool const customonly = (flags & PROCESS_CUSTOM_ONLY); - bool const noinput = (flags & PROCESS_NOINPUT); - float const aspect = machine().render().ui_aspect(&container()); - float const line_height = ui().get_line_height(); - float const lr_arrow_width = 0.4F * line_height * aspect; - float const ud_arrow_width = line_height * aspect; - float const gutter_width = 0.5F * line_height * aspect; - float const lr_border = ui().box_lr_border() * aspect; - float const max_width = 1.0F - ((lr_border + (aspect * UI_LINE_WIDTH)) * 2.0F); + float const max_width = 1.0F - ((lr_border() + (x_aspect() * UI_LINE_WIDTH)) * 2.0F); if (is_special_main_menu()) draw_background(); @@ -481,31 +620,34 @@ void menu::draw(uint32_t flags) for (auto const &pitem : m_items) { // compute width of left hand side - float total_width = gutter_width + ui().get_string_width(pitem.text()) + gutter_width; + float total_width = gutter_width() + get_string_width(pitem.text()) + gutter_width(); // add in width of right hand side if (!pitem.subtext().empty()) - total_width += 2.0F * gutter_width + ui().get_string_width(pitem.subtext()); + total_width += 2.0F * gutter_width() + get_string_width(pitem.subtext()); else if (pitem.flags() & FLAG_UI_HEADING) - total_width += 4.0F * ud_arrow_width; + total_width += 4.0F * ud_arrow_width(); // track the maximum visible_width = std::max(total_width, visible_width); // track the height as well - visible_main_menu_height += line_height; + visible_main_menu_height += line_height(); } // lay out the heading if present std::optional<text_layout> heading_layout; if (m_heading) { - heading_layout.emplace(ui().create_layout(container(), max_width - (gutter_width * 2.0F), text_layout::text_justify::CENTER)); + heading_layout.emplace(create_layout(max_width - (gutter_width() * 2.0F), text_layout::text_justify::CENTER)); heading_layout->add_text(*m_heading, ui().colors().text_color()); + + // readjust visible width if heading width exceeds that of the menu + visible_width = std::max(gutter_width() + heading_layout->actual_width() + gutter_width(), visible_width); } // account for extra space at the top and bottom - float const top_extra_menu_height = m_customtop + (heading_layout ? (heading_layout->actual_height() + (ui().box_tb_border() * 3.0F)) : 0.0F); + float const top_extra_menu_height = m_customtop + (heading_layout ? (heading_layout->actual_height() + (tb_border() * 3.0F)) : 0.0F); float const visible_extra_menu_height = top_extra_menu_height + m_custombottom; // add a little bit of slop for rounding @@ -516,36 +658,33 @@ void menu::draw(uint32_t flags) visible_width = std::min(visible_width, max_width); // if the menu and extra menu won't fit, take away part of the regular menu, it will scroll - if (visible_main_menu_height + visible_extra_menu_height + 2.0F * ui().box_tb_border() > 1.0F) - visible_main_menu_height = 1.0F - 2.0F * ui().box_tb_border() - visible_extra_menu_height; + if (visible_main_menu_height + visible_extra_menu_height + 2.0F * tb_border() > 1.0F) + visible_main_menu_height = 1.0F - 2.0F * tb_border() - visible_extra_menu_height; - m_visible_lines = std::min(int(std::floor(visible_main_menu_height / line_height)), int(unsigned(m_items.size()))); - visible_main_menu_height = float(m_visible_lines) * line_height; + m_visible_lines = std::min(int(std::floor(visible_main_menu_height / line_height())), int(unsigned(m_items.size()))); + visible_main_menu_height = float(m_visible_lines) * line_height(); // compute top/left of inner menu area by centering float const visible_left = (1.0F - visible_width) * 0.5F; - float const visible_top = ((1.0F - visible_main_menu_height - visible_extra_menu_height) * 0.5F) + top_extra_menu_height; + m_items_top = std::round((((1.0F - visible_main_menu_height - visible_extra_menu_height) * 0.5F) + top_extra_menu_height) * float(m_last_size.second)) / float(m_last_size.second); // first add us a box - float const x1 = visible_left - lr_border; - float const y1 = visible_top - ui().box_tb_border(); - float const x2 = visible_left + visible_width + lr_border; - float const y2 = visible_top + visible_main_menu_height + ui().box_tb_border(); + float const x1 = visible_left - lr_border(); + float const y1 = m_items_top - tb_border(); + float const x2 = visible_left + visible_width + lr_border(); + float const y2 = m_items_top + visible_main_menu_height + tb_border(); if (!customonly) { if (heading_layout) { - float const heading_width = heading_layout->actual_width(); - float const heading_left = (1.0F - heading_width) * 0.5F; - float const hx1 = std::min(x1, heading_left - gutter_width - lr_border); - float const hx2 = std::max(x2, heading_left + heading_width + gutter_width + lr_border); ui().draw_outlined_box( container(), - hx1, y1 - top_extra_menu_height, - hx2, y1 - m_customtop - ui().box_tb_border(), + x1, y1 - top_extra_menu_height, + x2, y1 - m_customtop - tb_border(), UI_GREEN_COLOR); - heading_layout->emit(container(), (1.0F - heading_layout->width()) * 0.5F, y1 - top_extra_menu_height + ui().box_tb_border()); + heading_layout->emit(container(), (1.0F - heading_layout->width()) * 0.5F, y1 - top_extra_menu_height + tb_border()); } + ui().draw_outlined_box( container(), x1, y1, @@ -563,29 +702,43 @@ void menu::draw(uint32_t flags) top_line = m_selected - m_visible_lines + ((m_selected == (m_items.size() - 1)) ? 1: 2); // if scrolling, show arrows - bool const show_top_arrow((m_items.size() > m_visible_lines) && !first_item_visible()); - bool const show_bottom_arrow((m_items.size() > m_visible_lines) && !last_item_visible()); + m_show_up_arrow = (m_items.size() > m_visible_lines) && !first_item_visible(); + m_show_down_arrow = (m_items.size() > m_visible_lines) && !last_item_visible(); // set the number of visible lines, minus 1 for top arrow and 1 for bottom arrow - m_visible_items = m_visible_lines - (show_top_arrow ? 1 : 0) - (show_bottom_arrow ? 1 : 0); + m_visible_items = m_visible_lines - (m_show_up_arrow ? 1 : 0) - (m_show_down_arrow ? 1 : 0); // determine effective positions taking into account the hilighting arrows - float const effective_width = visible_width - 2.0F * gutter_width; - float const effective_left = visible_left + gutter_width; - - // locate mouse - if (!customonly && !noinput) - map_mouse(); - else - ignore_mouse(); + float const effective_width = visible_width - 2.0F * gutter_width(); + float const effective_left = visible_left + gutter_width(); // loop over visible lines - m_hover = m_items.size() + 1; bool selected_subitem_too_big = false; - float const line_x0 = x1 + 0.5F * UI_LINE_WIDTH; - float const line_x1 = x2 - 0.5F * UI_LINE_WIDTH; - if (!customonly) + m_items_left = x1 + 0.5F * UI_LINE_WIDTH; + m_items_right = x2 - 0.5F * UI_LINE_WIDTH; + if (customonly) { + m_items_drawn = false; + switch (m_pointer_state) + { + case track_pointer::IDLE: + case track_pointer::IGNORED: + case track_pointer::COMPLETED: + case track_pointer::CUSTOM: + break; + case track_pointer::TRACK_LINE: + case track_pointer::SCROLL: + case track_pointer::ADJUST: + m_pointer_state = track_pointer::COMPLETED; + } + } + else + { + m_adjust_top = 1.0F; + m_adjust_bottom = 0.0F; + m_decrease_left = -1.0F; + m_increase_left = -1.0F; + m_items_drawn = true; for (int linenum = 0; linenum < m_visible_lines; linenum++) { auto const itemnum = top_line + linenum; @@ -595,77 +748,80 @@ void menu::draw(uint32_t flags) rgb_t bgcolor = ui().colors().text_bg_color(); rgb_t fgcolor2 = ui().colors().subitem_color(); rgb_t fgcolor3 = ui().colors().clone_color(); - float const line_y0 = visible_top + (float)linenum * line_height; - float const line_y1 = line_y0 + line_height; + float const line_y0 = m_items_top + (float(linenum) * line_height()); + float const line_y1 = line_y0 + line_height(); // work out what we're dealing with - bool const uparrow = !linenum && show_top_arrow; - bool const downarrow = (linenum == (m_visible_lines - 1)) && show_bottom_arrow; - - // set the hover if this is our item - bool const hovered = mouse_in_rect(line_x0, line_y0, line_x1, line_y1); - if (hovered) - { - if (uparrow) - m_hover = HOVER_ARROW_UP; - else if (downarrow) - m_hover = HOVER_ARROW_DOWN; - else if (is_selectable(pitem)) - m_hover = itemnum; - } + bool const uparrow = !linenum && m_show_up_arrow; + bool const downarrow = (linenum == (m_visible_lines - 1)) && m_show_down_arrow; + // highlight if necessary if (is_selected(itemnum)) { // if we're selected, draw with a different background fgcolor = fgcolor2 = fgcolor3 = ui().colors().selected_color(); bgcolor = ui().colors().selected_bg_color(); } - else if (hovered && (uparrow || downarrow || is_selectable(pitem))) + else if (uparrow || downarrow || is_selectable(pitem)) { - // else if the mouse is over this item, draw with a different background - fgcolor = fgcolor2 = fgcolor3 = ui().colors().mouseover_color(); - bgcolor = ui().colors().mouseover_bg_color(); + bool pointerline(linenum == m_pointer_line); + if ((track_pointer::ADJUST == m_pointer_state) && pointerline) + { + // use the hover background if an adjust gesture is attempted on an item that isn't selected + fgcolor = fgcolor2 = fgcolor3 = ui().colors().mouseover_color(); + bgcolor = ui().colors().mouseover_bg_color(); + } + else if (have_pointer() && pointer_in_rect(m_items_left, line_y0, m_items_right, line_y1)) + { + if ((track_pointer::TRACK_LINE == m_pointer_state) && pointerline) + { + // use the selected background for an item being selected + fgcolor = fgcolor2 = fgcolor3 = ui().colors().selected_color(); + bgcolor = ui().colors().selected_bg_color(); + } + else if (track_pointer::IDLE == m_pointer_state) + { + // else if the pointer is over this item, draw with a different background + fgcolor = fgcolor2 = fgcolor3 = ui().colors().mouseover_color(); + bgcolor = ui().colors().mouseover_bg_color(); + } + } + else if ((track_pointer::TRACK_LINE == m_pointer_state) && pointerline) + { + // use the hover background if the pointer moved out of the tracked item + fgcolor = fgcolor2 = fgcolor3 = ui().colors().mouseover_color(); + bgcolor = ui().colors().mouseover_bg_color(); + } } // if we have some background hilighting to do, add a quad behind everything else if (bgcolor != ui().colors().text_bg_color()) - highlight(line_x0, line_y0, line_x1, line_y1, bgcolor); + highlight(m_items_left, line_y0, m_items_right, line_y1, bgcolor); - if (uparrow) - { - // if we're on the top line, display the up arrow - draw_arrow( - 0.5F * (x1 + x2) - 0.5F * ud_arrow_width, - line_y0 + 0.25F * line_height, - 0.5F * (x1 + x2) + 0.5F * ud_arrow_width, - line_y0 + 0.75F * line_height, - fgcolor, - ROT0); - } - else if (downarrow) + if (uparrow || downarrow) { - // if we're on the bottom line, display the down arrow + // if we're on the top or bottom line, display the up or down arrow draw_arrow( - 0.5F * (x1 + x2) - 0.5F * ud_arrow_width, - line_y0 + 0.25F * line_height, - 0.5F * (x1 + x2) + 0.5F * ud_arrow_width, - line_y0 + 0.75F * line_height, + 0.5F * (x1 + x2 - ud_arrow_width()), + line_y0 + (0.25F * line_height()), + 0.5F * (x1 + x2 + ud_arrow_width()), + line_y0 + (0.75F * line_height()), fgcolor, - ROT0 ^ ORIENTATION_FLIP_Y); + downarrow ? (ROT0 ^ ORIENTATION_FLIP_Y) : ROT0); } else if (pitem.type() == menu_item_type::SEPARATOR) { // if we're just a divider, draw a line - container().add_line(visible_left, line_y0 + 0.5F * line_height, visible_left + visible_width, line_y0 + 0.5F * line_height, UI_LINE_WIDTH, ui().colors().border_color(), PRIMFLAG_BLENDMODE(BLENDMODE_ALPHA)); + container().add_line(visible_left, line_y0 + 0.5F * line_height(), visible_left + visible_width, line_y0 + 0.5F * line_height(), UI_LINE_WIDTH, ui().colors().border_color(), PRIMFLAG_BLENDMODE(BLENDMODE_ALPHA)); } else if (pitem.subtext().empty()) { // if we don't have a subitem, just draw the string centered if (pitem.flags() & FLAG_UI_HEADING) { - float heading_width = ui().get_string_width(itemtext); - container().add_line(visible_left, line_y0 + 0.5F * line_height, visible_left + ((visible_width - heading_width) / 2) - lr_border, line_y0 + 0.5F * line_height, UI_LINE_WIDTH, ui().colors().border_color(), PRIMFLAG_BLENDMODE(BLENDMODE_ALPHA)); - container().add_line(visible_left + visible_width - ((visible_width - heading_width) / 2) + lr_border, line_y0 + 0.5F * line_height, visible_left + visible_width, line_y0 + 0.5F * line_height, UI_LINE_WIDTH, ui().colors().border_color(), PRIMFLAG_BLENDMODE(BLENDMODE_ALPHA)); + float heading_width = get_string_width(itemtext); + container().add_line(visible_left, line_y0 + 0.5F * line_height(), visible_left + ((visible_width - heading_width) / 2) - lr_border(), line_y0 + 0.5F * line_height(), UI_LINE_WIDTH, ui().colors().border_color(), PRIMFLAG_BLENDMODE(BLENDMODE_ALPHA)); + container().add_line(visible_left + visible_width - ((visible_width - heading_width) / 2) + lr_border(), line_y0 + 0.5F * line_height(), visible_left + visible_width, line_y0 + 0.5F * line_height(), UI_LINE_WIDTH, ui().colors().border_color(), PRIMFLAG_BLENDMODE(BLENDMODE_ALPHA)); } ui().draw_text_full( container(), @@ -673,7 +829,8 @@ void menu::draw(uint32_t flags) effective_left, line_y0, effective_width, text_layout::text_justify::CENTER, text_layout::word_wrapping::TRUNCATE, mame_ui_manager::NORMAL, fgcolor, bgcolor, - nullptr, nullptr); + nullptr, nullptr, + line_height()); } else { @@ -688,14 +845,15 @@ void menu::draw(uint32_t flags) effective_left, line_y0, effective_width, text_layout::text_justify::LEFT, text_layout::word_wrapping::TRUNCATE, mame_ui_manager::NORMAL, fgcolor, bgcolor, - &item_width, nullptr); + &item_width, nullptr, + line_height()); if (pitem.flags() & FLAG_COLOR_BOX) { rgb_t color = rgb_t((uint32_t)strtoul(pitem.subtext().c_str(), nullptr, 16)); // give 2 spaces worth of padding - subitem_width = ui().get_string_width("FF00FF00"); + subitem_width = get_string_width("FF00FF00"); ui().draw_outlined_box( container(), @@ -708,10 +866,10 @@ void menu::draw(uint32_t flags) std::string_view subitem_text(pitem.subtext()); // give 2 spaces worth of padding - item_width += 2.0F * gutter_width; + item_width += 2.0F * gutter_width(); // if the subitem doesn't fit here, display dots - if (ui().get_string_width(subitem_text) > effective_width - item_width) + if (get_string_width(subitem_text) > effective_width - item_width) { subitem_text = "..."; if (is_selected(itemnum)) @@ -719,13 +877,13 @@ void menu::draw(uint32_t flags) } // customize subitem text color - if (!core_stricmp(pitem.subtext().c_str(), _("On"))) + if (!core_stricmp(pitem.subtext(), _("On"))) fgcolor2 = rgb_t(0x00,0xff,0x00); - if (!core_stricmp(pitem.subtext().c_str(), _("Off"))) + if (!core_stricmp(pitem.subtext(), _("Off"))) fgcolor2 = rgb_t(0xff,0x00,0x00); - if (!core_stricmp(pitem.subtext().c_str(), _("Auto"))) + if (!core_stricmp(pitem.subtext(), _("Auto"))) fgcolor2 = rgb_t(0xff,0xff,0x00); // draw the subitem right-justified @@ -735,31 +893,27 @@ void menu::draw(uint32_t flags) effective_left + item_width, line_y0, effective_width - item_width, text_layout::text_justify::RIGHT, text_layout::word_wrapping::TRUNCATE, mame_ui_manager::NORMAL, subitem_invert ? fgcolor3 : fgcolor2, bgcolor, - &subitem_width, nullptr); + &subitem_width, nullptr, + line_height()); } // apply arrows - if (is_selected(itemnum) && (pitem.flags() & FLAG_LEFT_ARROW)) - { - float const l = effective_left + effective_width - subitem_width - gutter_width; - float const r = l + lr_arrow_width; - draw_arrow( - l, line_y0 + 0.1F * line_height, r, line_y0 + 0.9F * line_height, - fgcolor, - ROT90 ^ ORIENTATION_FLIP_X); - if (mouse_in_rect(l, line_y0 + 0.1F * line_height, r, line_y0 + 0.9F * line_height)) - m_hover = HOVER_UI_LEFT; - } - if (is_selected(itemnum) && (pitem.flags() & FLAG_RIGHT_ARROW)) + if (is_selected(itemnum) && (pitem.flags() & (FLAG_LEFT_ARROW | FLAG_RIGHT_ARROW))) { - float const r = effective_left + effective_width + gutter_width; - float const l = r - lr_arrow_width; - draw_arrow( - l, line_y0 + 0.1F * line_height, r, line_y0 + 0.9F * line_height, - fgcolor, - ROT90); - if (mouse_in_rect(l, line_y0 + 0.1F * line_height, r, line_y0 + 0.9F * line_height)) - m_hover = HOVER_UI_RIGHT; + m_adjust_top = line_y0 + (0.1F * line_height()); + m_adjust_bottom = line_y0 + (0.9F * line_height()); + if (pitem.flags() & FLAG_LEFT_ARROW) + { + m_decrease_left = effective_left + effective_width - subitem_width - gutter_width(); + float const r = m_decrease_left + lr_arrow_width(); + draw_arrow(m_decrease_left, m_adjust_top, r, m_adjust_bottom, fgcolor, ROT90 ^ ORIENTATION_FLIP_X); + } + if (pitem.flags() & FLAG_RIGHT_ARROW) + { + float const r = effective_left + effective_width + gutter_width(); + m_increase_left = r - lr_arrow_width(); + draw_arrow(m_increase_left, m_adjust_top, r, m_adjust_bottom, fgcolor, ROT90); + } } } } @@ -771,29 +925,25 @@ void menu::draw(uint32_t flags) menu_item const &pitem = selected_item(); bool const subitem_invert(pitem.flags() & FLAG_INVERT); auto const linenum = m_selected - top_line; - float const line_y = visible_top + (float)linenum * line_height; - float target_width, target_height; + float const line_y = m_items_top + float(linenum) * line_height(); // compute the multi-line target width/height - ui().draw_text_full( - container(), + auto const [target_width, target_height] = get_text_dimensions( pitem.subtext(), 0, 0, visible_width * 0.75F, - text_layout::text_justify::RIGHT, text_layout::word_wrapping::WORD, - mame_ui_manager::NONE, rgb_t::white(), rgb_t::black(), - &target_width, &target_height); + text_layout::text_justify::RIGHT, text_layout::word_wrapping::WORD); // determine the target location - float const target_x = visible_left + visible_width - target_width - lr_border; - float target_y = line_y + line_height + ui().box_tb_border(); - if (target_y + target_height + ui().box_tb_border() > visible_main_menu_height) - target_y = line_y - target_height - ui().box_tb_border(); + float const target_x = visible_left + visible_width - target_width - lr_border(); + float target_y = line_y + line_height() + tb_border(); + if (target_y + target_height + tb_border() > visible_main_menu_height) + target_y = line_y - target_height - tb_border(); // add a box around that ui().draw_outlined_box( container(), - target_x - lr_border, target_y - ui().box_tb_border(), - target_x + target_width + lr_border, target_y + target_height + ui().box_tb_border(), + target_x - lr_border(), target_y - tb_border(), + target_x + target_width + lr_border(), target_y + target_height + tb_border(), subitem_invert ? ui().colors().selected_bg_color() : ui().colors().background_color()); ui().draw_text_full( @@ -806,43 +956,30 @@ void menu::draw(uint32_t flags) } // if there is something special to add, do it by calling the virtual method - custom_render(get_selection_ref(), m_customtop, m_custombottom, x1, y1, x2, y2); + custom_render(flags, get_selection_ref(), m_customtop, m_custombottom, x1, y1, x2, y2); } -void menu::custom_render(void *selectedref, float top, float bottom, float x, float y, float x2, float y2) +void menu::recompute_metrics(uint32_t width, uint32_t height, float aspect) { -} + float const ui_line_height = ui().get_line_height(); + // force whole pixels for line height, gutters and borders + m_line_height = std::floor(ui_line_height * float(height)) / float(height); + m_gutter_width = std::floor(0.5F * ui_line_height * aspect * float(width)) / float(width); + m_tb_border = std::floor(ui().box_tb_border() * float(height)) / float(height); + m_lr_border = std::floor(ui().box_lr_border() * aspect * float(width)) / float(width); -//------------------------------------------------- -// map_mouse - map mouse pointer location to menu -// coordinates -//------------------------------------------------- - -void menu::map_mouse() -{ - ignore_mouse(); - int32_t mouse_target_x, mouse_target_y; - render_target *const mouse_target = machine().ui_input().find_mouse(&mouse_target_x, &mouse_target_y, &m_mouse_button); - if (mouse_target) - { - if (mouse_target->map_point_container(mouse_target_x, mouse_target_y, container(), m_mouse_x, m_mouse_y)) - m_mouse_hit = true; - } -} + m_lr_arrow_width = 0.4F * m_line_height * aspect; + m_ud_arrow_width = m_line_height * aspect; + // don't accept pointer input until the menu has been redrawn + m_items_drawn = false; + m_pointer_state = track_pointer::IDLE; -//------------------------------------------------- -// ignore_mouse - set members to ignore mouse -// input -//------------------------------------------------- +} -void menu::ignore_mouse() +void menu::custom_render(uint32_t flags, void *selectedref, float top, float bottom, float x, float y, float x2, float y2) { - m_mouse_hit = false; - m_mouse_button = false; - m_mouse_x = -1.0F; - m_mouse_y = -1.0F; } @@ -851,8 +988,9 @@ void menu::ignore_mouse() // input events for a menu //------------------------------------------------- -void menu::handle_events(uint32_t flags, event &ev) +bool menu::handle_events(uint32_t flags, event &ev) { + bool need_update = false; bool stop = false; ui_event local_menu_event; @@ -861,119 +999,100 @@ void menu::handle_events(uint32_t flags, event &ev) { switch (local_menu_event.event_type) { - // if we are hovering over a valid item, select it with a single click - case ui_event::type::MOUSE_DOWN: - if (custom_mouse_down()) - return; - - if (!(flags & PROCESS_ONLYCHAR)) + // deal with pointer-like input (mouse, pen, touch, etc.) + case ui_event::type::POINTER_UPDATE: { - if (m_hover >= 0 && m_hover < m_items.size()) - { - m_selected = m_hover; - } - else if (m_hover == HOVER_ARROW_UP) - { - if (flags & PROCESS_CUSTOM_NAV) - { - ev.iptkey = IPT_UI_PAGE_UP; - stop = true; - } - else - { - m_selected -= m_visible_items; - if (m_selected < 0) - m_selected = 0; - top_line -= m_visible_items - (last_item_visible() ? 1 : 0); - } - } - else if (m_hover == HOVER_ARROW_DOWN) - { - if (flags & PROCESS_CUSTOM_NAV) - { - ev.iptkey = IPT_UI_PAGE_DOWN; - stop = true; - } - else - { - m_selected += m_visible_lines - 2 + is_first_selected(); - if (m_selected > m_items.size() - 1) - m_selected = m_items.size() - 1; - top_line += m_visible_lines - 2; - } - } - else if (m_hover == HOVER_UI_LEFT) + auto const [key, redraw] = handle_pointer_update(flags, local_menu_event); + need_update = need_update || redraw; + if (IPT_INVALID != key) { - ev.iptkey = IPT_UI_LEFT; + ev.iptkey = key; stop = true; } - else if (m_hover == HOVER_UI_RIGHT) + } + break; + + // pointer left the normal way, possibly releasing buttons + case ui_event::type::POINTER_LEAVE: + { + auto const [key, redraw] = handle_pointer_leave(flags, local_menu_event); + need_update = need_update || redraw; + if (IPT_INVALID != key) { - ev.iptkey = IPT_UI_RIGHT; + ev.iptkey = key; stop = true; } } break; - // if we are hovering over a valid item, fake a UI_SELECT with a double-click - case ui_event::type::MOUSE_DOUBLE_CLICK: - if (!(flags & PROCESS_ONLYCHAR) && m_hover >= 0 && m_hover < m_items.size()) + // pointer left in some abnormal way - cancel any associated actions + case ui_event::type::POINTER_ABORT: { - m_selected = m_hover; - ev.iptkey = IPT_UI_SELECT; - if (is_last_selected() && m_needs_prev_menu_item) + auto const [key, redraw] = handle_pointer_abort(flags, local_menu_event); + need_update = need_update || redraw; + if (IPT_INVALID != key) { - ev.iptkey = IPT_UI_CANCEL; - stack_pop(); - if (is_special_main_menu()) - machine().schedule_exit(); + ev.iptkey = key; + stop = true; } - stop = true; } break; // caught scroll event case ui_event::type::MOUSE_WHEEL: - if (!custom_mouse_scroll((0 < local_menu_event.zdelta) ? -local_menu_event.num_lines : local_menu_event.num_lines) && !(flags & (PROCESS_ONLYCHAR | PROCESS_CUSTOM_NAV))) + if ((track_pointer::IDLE == m_pointer_state) || (track_pointer::IGNORED == m_pointer_state)) { - if (local_menu_event.zdelta > 0) + // the value is scaled to 120 units per "click" + m_accumulated_wheel += local_menu_event.zdelta * local_menu_event.num_lines; + int const lines((m_accumulated_wheel + ((0 < local_menu_event.zdelta) ? 36 : -36)) / 120); + if (!lines) + break; + m_accumulated_wheel -= lines * 120; + + if (!custom_mouse_scroll(-lines) && !(flags & (PROCESS_ONLYCHAR | PROCESS_CUSTOM_NAV))) { - if (is_first_selected()) + if (lines > 0) { - select_last_item(); + if (is_first_selected()) + { + select_last_item(); + } + else + { + m_selected -= lines; + validate_selection(-1); + } + top_line -= (m_selected <= top_line && top_line != 0); + if (m_selected <= top_line && m_visible_items != m_visible_lines) + top_line -= lines; } else { - m_selected -= local_menu_event.num_lines; - validate_selection(-1); + if (is_last_selected()) + { + select_first_item(); + } + else + { + m_selected -= lines; + validate_selection(1); + } + top_line += (m_selected >= top_line + m_visible_items + (top_line != 0)); + if (m_selected >= (top_line + m_visible_items + (top_line != 0))) + top_line -= lines; } - top_line -= (m_selected <= top_line && top_line != 0); - if (m_selected <= top_line && m_visible_items != m_visible_lines) - top_line -= local_menu_event.num_lines; - } - else - { - if (is_last_selected()) - { - select_first_item(); - } - else - { - m_selected += local_menu_event.num_lines; - validate_selection(1); - } - top_line += (m_selected >= top_line + m_visible_items + (top_line != 0)); - if (m_selected >= (top_line + m_visible_items + (top_line != 0))) - top_line += local_menu_event.num_lines; } } break; // translate CHAR events into specials case ui_event::type::IME_CHAR: - ev.iptkey = IPT_SPECIAL; - ev.unichar = local_menu_event.ch; - stop = true; + if ((track_pointer::IDLE == m_pointer_state) || (track_pointer::IGNORED == m_pointer_state)) + { + ev.iptkey = IPT_SPECIAL; + ev.unichar = local_menu_event.ch; + stop = true; + } break; // ignore everything else @@ -981,6 +1100,536 @@ void menu::handle_events(uint32_t flags, event &ev) break; } } + + // deal with repeating scroll arrows + if ((track_pointer::TRACK_LINE == m_pointer_state) && ((!m_pointer_line && m_show_up_arrow) || ((m_pointer_line == (m_visible_lines - 1)) && m_show_down_arrow))) + { + float const linetop(m_items_top + (float(m_pointer_line) * line_height())); + float const linebottom(m_items_top + (float(m_pointer_line + 1) * line_height())); + if (pointer_in_rect(m_items_left, linetop, m_items_right, linebottom)) + { + if (std::chrono::steady_clock::now() >= m_pointer_repeat) + { + if (!m_pointer_line) + { + // scroll up + assert(0 < top_line); + --top_line; + if (!top_line) + m_pointer_state = track_pointer::COMPLETED; + } + else + { + // scroll down + assert(m_items.size() > (top_line + m_visible_lines)); + ++top_line; + if (m_items.size() == (top_line + m_visible_lines)) + m_pointer_state = track_pointer::COMPLETED; + } + force_visible_selection(); + need_update = true; + m_pointer_repeat += std::chrono::milliseconds(100); + } + } + } + + return need_update; +} + + +//------------------------------------------------- +// handle_pointer_update - handle a regular +// pointer update +//------------------------------------------------- + +std::pair<int, bool> menu::handle_pointer_update(uint32_t flags, ui_event const &uievt) +{ + // decide whether to make this our current pointer + render_target &target(machine().render().ui_target()); + auto const [ours, changed] = m_global_state.use_pointer(target, container(), uievt); + if (!ours) + { + return std::make_pair(IPT_INVALID, false); + } + else if (changed) + { + // if the active pointer changed, ignore if any buttons were already down + if (uievt.pointer_buttons != uievt.pointer_pressed) + { + m_pointer_state = track_pointer::IGNORED; + return std::make_pair(IPT_INVALID, false); + } + else + { + m_pointer_state = track_pointer::IDLE; + } + } + else if ((track_pointer::IGNORED == m_pointer_state) || (track_pointer::COMPLETED == m_pointer_state)) + { + // stop ignoring the pointer if all buttons were released + if (uievt.pointer_buttons == uievt.pointer_pressed) + m_pointer_state = track_pointer::IDLE; + else + return std::make_pair(IPT_INVALID, false); + } + + // give derived class a chance to handle it + if ((track_pointer::IDLE == m_pointer_state) || (track_pointer::CUSTOM == m_pointer_state)) + { + auto const [key, take, redraw] = custom_pointer_updated(changed, uievt); + if (take) + { + m_pointer_state = track_pointer::CUSTOM; + return std::make_pair(key, redraw); + } + else if (track_pointer::CUSTOM == m_pointer_state) + { + if (uievt.pointer_buttons) + { + m_pointer_state = track_pointer::COMPLETED; + return std::make_pair(key, redraw); + } + else + { + m_pointer_state = track_pointer::IDLE; + } + } + + if (IPT_INVALID != key) + return std::make_pair(key, redraw); + } + + // ignore altogether if menu hasn't been drawn or flags say so + if (!m_items_drawn || (flags & (PROCESS_CUSTOM_ONLY | PROCESS_ONLYCHAR))) + { + if (uievt.pointer_pressed) + { + if (track_pointer::IDLE == m_pointer_state) + m_pointer_state = track_pointer::IGNORED; + } + else if (!uievt.pointer_buttons) + { + if ((track_pointer::IGNORED == m_pointer_state) || (track_pointer::COMPLETED == m_pointer_state)) + m_pointer_state = track_pointer::IDLE; + } + return std::make_pair(IPT_INVALID, false); + } + + switch (m_pointer_state) + { + case track_pointer::IDLE: + // ignore anything other than left click for now + if ((uievt.pointer_pressed & 0x01) && !(uievt.pointer_buttons & ~u32(0x1))) + return handle_primary_down(flags, uievt); + else if (uievt.pointer_pressed) + m_pointer_state = track_pointer::IGNORED; + break; + + case track_pointer::IGNORED: + case track_pointer::COMPLETED: + case track_pointer::CUSTOM: + std::abort(); // won't get here - handled earlier + + case track_pointer::TRACK_LINE: + { + auto const result(update_line_click(uievt)); + + // treat anything else being pressed as cancelling the click sequence + if (uievt.pointer_buttons & ~u32(0x01)) + m_pointer_state = track_pointer::COMPLETED; + else if (!uievt.pointer_buttons) + m_pointer_state = track_pointer::IDLE; + + return result; + } + + case track_pointer::SCROLL: + { + bool const redraw(update_drag_scroll(uievt)); + + // treat anything else being pressed as cancelling the drag + if (uievt.pointer_buttons & ~u32(0x01)) + m_pointer_state = track_pointer::COMPLETED; + else if (!uievt.pointer_buttons) + m_pointer_state = track_pointer::IDLE; + + return std::make_pair(IPT_INVALID, redraw); + } + + case track_pointer::ADJUST: + { + auto const result(update_drag_adjust(uievt)); + + // treat anything else being pressed as cancelling the drag + if (uievt.pointer_buttons & ~u32(0x01)) + m_pointer_state = track_pointer::COMPLETED; + else if (!uievt.pointer_buttons) + m_pointer_state = track_pointer::IDLE; + + return result; + } + } + + return std::make_pair(IPT_INVALID, false); +} + + +//------------------------------------------------- +// handle_pointer_leave - handle a pointer +// leaving the window +//------------------------------------------------- + +std::pair<int, bool> menu::handle_pointer_leave(uint32_t flags, ui_event const &uievt) +{ + // ignore pointer input in windows other than the one that displays the UI + render_target &target(machine().render().ui_target()); + auto const [ours, changed] = m_global_state.use_pointer(target, container(), uievt); + assert(!changed); + if (!ours) + return std::make_pair(IPT_INVALID, false); + + int key(IPT_INVALID); + bool redraw(false); + switch (m_pointer_state) + { + case track_pointer::IDLE: + case track_pointer::CUSTOM: + std::tie(key, std::ignore, redraw) = custom_pointer_updated(changed, uievt); + break; + + case track_pointer::IGNORED: + case track_pointer::COMPLETED: + break; // nothing to do + + case track_pointer::TRACK_LINE: + std::tie(key, redraw) = update_line_click(uievt); + break; + + case track_pointer::SCROLL: + redraw = update_drag_scroll(uievt); + break; + + case track_pointer::ADJUST: + std::tie(key, redraw) = update_drag_adjust(uievt); + break; + } + + m_pointer_state = track_pointer::IDLE; + return std::make_pair(key, redraw); +} + + +//------------------------------------------------- +// handle_pointer_abort - handle a pointer +// leaving in an abnormal way +//------------------------------------------------- + +std::pair<int, bool> menu::handle_pointer_abort(uint32_t flags, ui_event const &uievt) +{ + // ignore pointer input in windows other than the one that displays the UI + render_target &target(machine().render().ui_target()); + auto const [ours, changed] = m_global_state.use_pointer(target, container(), uievt); + assert(!changed); + if (!ours) + return std::make_pair(IPT_INVALID, false); + + int key(IPT_INVALID); + bool redraw(false); + if (track_pointer::CUSTOM == m_pointer_state) + std::tie(key, std::ignore, redraw) = custom_pointer_updated(false, uievt); + else if (track_pointer::TRACK_LINE == m_pointer_state) + redraw = true; + m_pointer_state = track_pointer::IDLE; + return std::make_pair(key, redraw); +} + + +//------------------------------------------------- +// handle_primary_down - handle the primary +// action for a pointer device +//------------------------------------------------- + +std::pair<int, bool> menu::handle_primary_down(uint32_t flags, ui_event const &uievt) +{ + // we handle touch differently to mouse or pen + bool const is_touch(ui_event::pointer::TOUCH == uievt.pointer_type); + auto const [x, y] = pointer_location(); // FIXME: need starting location for multi-click actions + + // check increase/decrease arrows first + // FIXME: should repeat if appropriate + if (!is_touch && (y >= m_adjust_top) && (y < m_adjust_bottom)) + { + if ((x >= m_decrease_left) && (x < (m_decrease_left + lr_arrow_width()))) + { + m_pointer_state = track_pointer::COMPLETED; + return std::make_pair(IPT_UI_LEFT, false); + } + else if ((x >= m_increase_left) && (x < (m_increase_left + lr_arrow_width()))) + { + m_pointer_state = track_pointer::COMPLETED; + return std::make_pair(IPT_UI_RIGHT, false); + } + } + + // work out if we’re pointing at an item + if ((x < m_items_left) || (x >= m_items_right) || (y < m_items_top) || (y >= (m_items_top + (float(m_visible_lines) * line_height())))) + { + m_pointer_state = track_pointer::IGNORED; + return std::make_pair(IPT_INVALID, false); + } + auto const lineno(int((y - m_items_top) / line_height())); + assert(lineno >= 0); + assert(lineno < m_visible_lines); + + // map to an action + if (!lineno && m_show_up_arrow) + { + // scroll up + assert(0 < top_line); + --top_line; + force_visible_selection(); + if (top_line) + { + m_pointer_state = track_pointer::TRACK_LINE; + m_pointer_down = std::make_pair(x, y); + m_pointer_updated = m_pointer_down; + m_pointer_line = lineno; + m_pointer_repeat = std::chrono::steady_clock::now() + std::chrono::milliseconds(300); + } + else + { + m_pointer_state = track_pointer::COMPLETED; + } + return std::make_pair(IPT_INVALID, true); + } + else if ((lineno == (m_visible_lines - 1)) && m_show_down_arrow) + { + // scroll down + assert(m_items.size() > (top_line + m_visible_lines)); + ++top_line; + force_visible_selection(); + if (m_items.size() > (top_line + m_visible_lines)) + { + m_pointer_state = track_pointer::TRACK_LINE; + m_pointer_down = std::make_pair(x, y); + m_pointer_updated = m_pointer_down; + m_pointer_line = lineno; + m_pointer_repeat = std::chrono::steady_clock::now() + std::chrono::milliseconds(300); + } + else + { + m_pointer_state = track_pointer::COMPLETED; + } + return std::make_pair(IPT_INVALID, true); + } + else + { + m_pointer_state = track_pointer::TRACK_LINE; + m_pointer_down = std::make_pair(x, y); + m_pointer_updated = m_pointer_down; + m_pointer_line = lineno; + + int const itemno(lineno + top_line); + assert(itemno >= 0); + assert(itemno < m_items.size()); + return std::make_pair(IPT_INVALID, is_selectable(m_items[itemno])); + } + + // nothing to do + m_pointer_state = track_pointer::IGNORED; + return std::make_pair(IPT_INVALID, false); +} + + +//------------------------------------------------- +// update_line_click - track pointer after +// clicking a menu line +//------------------------------------------------- + +std::pair<int, bool> menu::update_line_click(ui_event const &uievt) +{ + assert(track_pointer::TRACK_LINE == m_pointer_state); + assert((uievt.pointer_buttons | uievt.pointer_released) & 0x01); + + // arrows should scroll while held + if ((!m_pointer_line && m_show_up_arrow) || ((m_pointer_line == (m_visible_lines - 1)) && m_show_down_arrow)) + { + // check for re-entry + bool redraw(false); + auto const [x, y] = pointer_location(); + float const linetop(m_items_top + (float(m_pointer_line) * line_height())); + float const linebottom(m_items_top + (float(m_pointer_line + 1) * line_height())); + bool const reentered(reentered_rect(m_pointer_updated.first, m_pointer_updated.second, x, y, m_items_left, linetop, m_items_right, linebottom)); + if (reentered) + { + auto const now(std::chrono::steady_clock::now()); + if (now >= m_pointer_repeat) + { + if (!m_pointer_line) + { + // scroll up + assert(0 < top_line); + --top_line; + if (!top_line) + m_pointer_state = track_pointer::COMPLETED; + } + else + { + // scroll down + assert(m_items.size() > (top_line + m_visible_lines)); + ++top_line; + if (m_items.size() == (top_line + m_visible_lines)) + m_pointer_state = track_pointer::COMPLETED; + } + force_visible_selection(); + redraw = true; + m_pointer_repeat = now + std::chrono::milliseconds(100); + } + } + + // keep the pointer location where we updated + m_pointer_updated = std::make_pair(x, y); + return std::make_pair(IPT_INVALID, redraw); + } + + // check for conversion of a tap to a finger drag + auto const drag_result(check_touch_drag(uievt)); + if (track_pointer::TRACK_LINE != m_pointer_state) + return drag_result; + + // only take action if the primary button was released + if (!(uievt.pointer_released & 0x01)) + return std::make_pair(IPT_INVALID, false); + + // nothing to do if the item isn't selectable + int const itemno = m_pointer_line + top_line; + assert(itemno >= 0); + assert(itemno < m_items.size()); + if (!is_selectable(m_items[itemno])) + return std::make_pair(IPT_INVALID, false); + + // treat multi-click actions as not moving, otherwise check that pointer is still over the line + if (0 >= uievt.pointer_clicks) + { + auto const [x, y] = pointer_location(); + if ((x < m_items_left) || (x >= m_items_right) || (int((y - m_items_top) / line_height()) != m_pointer_line)) + return std::make_pair(IPT_INVALID, true); + } + + // anything other than a double-click just selects the item + m_selected = itemno; + if (2 != uievt.pointer_clicks) + return std::make_pair(IPT_INVALID, true); + + // activate regular items by simulating UI Select + if (!is_last_selected() || !m_needs_prev_menu_item) + return std::make_pair(IPT_UI_SELECT, true); + + // handle the magic final item that dismisses the menu + stack_pop(); + if (is_special_main_menu()) + machine().schedule_exit(); + return std::make_pair(IPT_UI_BACK, true); +} + + +//------------------------------------------------- +// update_drag_scroll - update menu position in +// response to a touch drag +//------------------------------------------------- + +bool menu::update_drag_scroll(ui_event const &uievt) +{ + assert(track_pointer::SCROLL == m_pointer_state); + assert((uievt.pointer_buttons | uievt.pointer_released) & 0x01); + + // get target location + int const newtop(drag_scroll( + pointer_location().second, m_pointer_down.second, m_pointer_updated.second, -line_height(), + m_pointer_line, 0, int(m_items.size() - m_visible_lines))); + if (newtop == top_line) + return false; + + // scroll and move the selection if necessary to keep it in the visible range + top_line = newtop; + force_visible_selection(); + return true; +} + + +//------------------------------------------------- +// update_drag_adjust - adjust value on +// horizontal drag +//------------------------------------------------- + +std::pair<int, bool> menu::update_drag_adjust(ui_event const &uievt) +{ + assert(track_pointer::ADJUST == m_pointer_state); + assert((uievt.pointer_buttons | uievt.pointer_released) & 0x01); + + // this is ugly because adjustment is implemented by faking keystrokes - can't give a count/distance + + // set thresholds depending on the direction for hysteresis + int const target(drag_scroll( + pointer_location().first, m_pointer_updated.first, m_pointer_updated.first, line_height() * x_aspect(), + 0, std::numeric_limits<int>::min(), std::numeric_limits<int>::max())); + + // ensure the item under the pointer is selected and adjustable + if ((top_line + m_pointer_line) == m_selected) + { + if (0 < target) + { + if (m_items[m_selected].flags() & FLAG_RIGHT_ARROW) + return std::make_pair(IPT_UI_RIGHT, true); + } + else if (0 > target) + { + if (m_items[m_selected].flags() & FLAG_LEFT_ARROW) + return std::make_pair(IPT_UI_LEFT, true); + } + } + + // looks like it wasn't to be + return std::make_pair(IPT_INVALID, false); +} + + +//------------------------------------------------- +// check_touch_drag - check for conversion of a +// touch to a scroll or adjust slide +//------------------------------------------------- + +std::pair<int, bool> menu::check_touch_drag(ui_event const &uievt) +{ + // we handle touch differently to mouse or pen + if (ui_event::pointer::TOUCH != uievt.pointer_type) + return std::make_pair(IPT_INVALID, false); + + // check distances + auto const [x, y] = pointer_location(); + auto const [h, v] = check_drag_conversion(x, y, m_pointer_down.first, m_pointer_down.second, line_height()); + if (h) + { + // only the selected line can be adjusted + if ((top_line + m_pointer_line) == m_selected) + { + m_pointer_state = track_pointer::ADJUST; + return update_drag_adjust(uievt); + } + else + { + m_pointer_state = track_pointer::COMPLETED; + } + } + else if (v) + { + m_pointer_state = track_pointer::SCROLL; + m_pointer_line = top_line; + return std::make_pair(IPT_INVALID, update_drag_scroll(uievt)); + } + + // no update needed + return std::make_pair(IPT_INVALID, false); } @@ -989,51 +1638,52 @@ void menu::handle_events(uint32_t flags, event &ev) // keys for a menu //------------------------------------------------- -void menu::handle_keys(uint32_t flags, int &iptkey) +bool menu::handle_keys(uint32_t flags, int &iptkey) { - bool const ignorepause = (flags & PROCESS_IGNOREPAUSE) || stack_has_special_main_menu(); - - // bail if no items + // bail if no items (happens if event handling triggered an item reset) if (m_items.empty()) - return; + return false; + + bool const ignorepause = (flags & PROCESS_IGNOREPAUSE) || stack_has_special_main_menu(); // if we hit select, return true or pop the stack, depending on the item if (exclusive_input_pressed(iptkey, IPT_UI_SELECT, 0)) { if (is_last_selected() && m_needs_prev_menu_item) { - iptkey = IPT_UI_CANCEL; + iptkey = IPT_INVALID; stack_pop(); if (is_special_main_menu()) machine().schedule_exit(); } - return; + return false; } // UI configure hides the menus - if (!(flags & PROCESS_NOKEYS) && exclusive_input_pressed(iptkey, IPT_UI_CONFIGURE, 0) && !m_global_state.stack_has_special_main_menu()) + if (!(flags & PROCESS_NOKEYS) && exclusive_input_pressed(iptkey, IPT_UI_MENU, 0) && !m_global_state.stack_has_special_main_menu()) { if (is_one_shot()) stack_pop(); else m_global_state.hide_menu(); - return; + return true; } // bail out if (flags & PROCESS_ONLYCHAR) - return; + return false; - // hitting cancel also pops the stack - if (exclusive_input_pressed(iptkey, IPT_UI_CANCEL, 0)) + // hitting back also pops the stack + if (exclusive_input_pressed(iptkey, IPT_UI_BACK, 0)) { - if (!custom_ui_cancel()) + if (!custom_ui_back()) { + iptkey = IPT_INVALID; stack_pop(); if (is_special_main_menu()) machine().schedule_exit(); } - return; + return false; } // validate the current selection @@ -1045,16 +1695,19 @@ void menu::handle_keys(uint32_t flags, int &iptkey) // accept left/right/prev/next keys as-is with repeat if appropriate if (!ignoreleft && exclusive_input_pressed(iptkey, IPT_UI_LEFT, (flags & PROCESS_LR_REPEAT) ? 6 : 0)) - return; + return false; if (!ignoreright && exclusive_input_pressed(iptkey, IPT_UI_RIGHT, (flags & PROCESS_LR_REPEAT) ? 6 : 0)) - return; + return false; + + // keep track of whether we changed anything + bool updated(false); // up backs up by one item if (exclusive_input_pressed(iptkey, IPT_UI_UP, 6)) { if (flags & PROCESS_CUSTOM_NAV) { - return; + return updated; } else if (is_first_selected()) { @@ -1068,6 +1721,7 @@ void menu::handle_keys(uint32_t flags, int &iptkey) top_line -= (m_selected <= top_line && top_line != 0); if (m_selected <= top_line && m_visible_items != m_visible_lines) top_line--; + updated = true; } // down advances by one item @@ -1075,7 +1729,7 @@ void menu::handle_keys(uint32_t flags, int &iptkey) { if (flags & PROCESS_CUSTOM_NAV) { - return; + return updated; } else if (is_last_selected()) { @@ -1089,47 +1743,52 @@ void menu::handle_keys(uint32_t flags, int &iptkey) top_line += (m_selected >= top_line + m_visible_items + (top_line != 0)); if (m_selected >= (top_line + m_visible_items + (top_line != 0))) top_line++; + updated = true; } // page up backs up by m_visible_items if (exclusive_input_pressed(iptkey, IPT_UI_PAGE_UP, 6)) { if (flags & PROCESS_CUSTOM_NAV) - return; + return updated; m_selected -= m_visible_items; top_line -= m_visible_items - (last_item_visible() ? 1 : 0); if (m_selected < 0) m_selected = 0; validate_selection(1); + updated = true; } // page down advances by m_visible_items if (exclusive_input_pressed(iptkey, IPT_UI_PAGE_DOWN, 6)) { if (flags & PROCESS_CUSTOM_NAV) - return; + return updated; m_selected += m_visible_lines - 2 + is_first_selected(); top_line += m_visible_lines - 2; if (m_selected > m_items.size() - 1) m_selected = m_items.size() - 1; validate_selection(-1); + updated = true; } // home goes to the start if (exclusive_input_pressed(iptkey, IPT_UI_HOME, 0)) { if (flags & PROCESS_CUSTOM_NAV) - return; + return updated; select_first_item(); + updated = true; } // end goes to the last if (exclusive_input_pressed(iptkey, IPT_UI_END, 0)) { if (flags & PROCESS_CUSTOM_NAV) - return; + return updated; select_last_item(); + updated = true; } // pause enables/disables pause @@ -1141,10 +1800,6 @@ void menu::handle_keys(uint32_t flags, int &iptkey) machine().pause(); } - // handle a toggle cheats request - if (machine().ui_input().pressed_repeat(IPT_UI_TOGGLE_CHEAT, 0)) - mame_machine_manager::instance()->cheat().set_enable(!mame_machine_manager::instance()->cheat().enabled()); - // see if any other UI keys are pressed if (iptkey == IPT_INVALID) { @@ -1169,6 +1824,39 @@ void menu::handle_keys(uint32_t flags, int &iptkey) break; } } + return updated; +} + + +//------------------------------------------------- +// default handler implementations +//------------------------------------------------- + +bool menu::custom_ui_back() +{ + return false; +} + +std::tuple<int, bool, bool> menu::custom_pointer_updated(bool changed, ui_event const &uievt) +{ + return std::make_tuple(IPT_INVALID, false, false); +} + +bool menu::custom_mouse_scroll(int lines) +{ + return false; +} + +void menu::menu_activated() +{ +} + +void menu::menu_deactivated() +{ +} + +void menu::menu_dismissed() +{ } @@ -1216,34 +1904,182 @@ void menu::validate_selection(int scandir) } +//------------------------------------------------- +// activate_menu - handle becoming top of the +// menu stack +//------------------------------------------------- + +void menu::activate_menu() +{ + m_items_drawn = false; + m_pointer_state = track_pointer::IDLE; + m_accumulated_wheel = 0; + m_active = true; + menu_activated(); +} + + +//------------------------------------------------- +// check_metrics - recompute metrics if target +// geometry has changed +//------------------------------------------------- + +bool menu::check_metrics() +{ + render_manager &render(machine().render()); + render_target &target(render.ui_target()); + std::pair<uint32_t, uint32_t> const uisize(target.width(), target.height()); + float const aspect = render.ui_aspect(&container()); + if ((uisize == m_last_size) && (aspect == m_last_aspect)) + return false; + + m_last_size = uisize; + m_last_aspect = aspect; + recompute_metrics(uisize.first, uisize.second, aspect); + return true; +} + + +//------------------------------------------------- +// do_rebuild - get the subclass to populate +// the menu items +//------------------------------------------------- + +bool menu::do_rebuild() +{ + if (!m_items.empty()) + return false; + + m_rebuilding = true; + try + { + // add an item to return - this is a really hacky way of doing this + if (m_needs_prev_menu_item) + item_append(_("Return to Previous Menu"), 0, nullptr); + + // let implementation add other items + populate(); + } + catch (...) + { + m_items.clear(); + m_rebuilding = false; + throw; + } + m_rebuilding = false; + return true; +} + + +//------------------------------------------------- +// force_visible_selection - if the selected item +// is not visible, move the selection it it's +// within the visible portion of the menu +//------------------------------------------------- + +void menu::force_visible_selection() +{ + int const first(top_line ? (top_line + 1) : 0); + int const last(top_line + m_visible_lines - ((m_items.size() > (top_line + m_visible_lines)) ? 1 : 0)); + if (first > m_selected) + { + m_selected = first; + while (!is_selectable(m_items[m_selected])) + ++m_selected; + assert(last > m_selected); + } + else if (last <= m_selected) + { + m_selected = last - 1; + while (!is_selectable(m_items[m_selected])) + --m_selected; + assert(first <= m_selected); + } +} + + /*************************************************************************** MENU STACK MANAGEMENT ***************************************************************************/ -void menu::do_handle() +bool menu::do_handle() { - if (m_items.empty()) + bool need_update = false; + + // let OSD do its thing + machine().osd().check_osd_inputs(); + + // recompute metrics if necessary + if (check_metrics()) + need_update = true; + + // get the implementation to rebuild the list of items if necessary + if (do_rebuild()) + need_update = true; + validate_selection(1); + + // reset the event + std::optional<event> result; + result.emplace(); + result->itemref = nullptr; + result->item = nullptr; + result->iptkey = IPT_INVALID; + + // process input + uint32_t flags(m_process_flags); + if (!(flags & (PROCESS_NOKEYS | PROCESS_NOINPUT))) { - m_rebuilding = true; - try - { - // add an item to return - this is a really hacky way of doing this - if (m_needs_prev_menu_item) - item_append(_("Return to Previous Menu"), 0, nullptr); + // read events + if (handle_events(flags, *result)) + need_update = true; - // let implementation add other items - populate(m_customtop, m_custombottom); - } - catch (...) + switch (m_pointer_state) { - m_items.clear(); - m_rebuilding = false; - throw; + case track_pointer::IDLE: + case track_pointer::IGNORED: + // handle keys if we don't already have an event and we aren't tracking a pointer action + if ((IPT_INVALID == result->iptkey) && handle_keys(flags, result->iptkey)) + need_update = true; + break; + default: + // ignore keys pressed while tracking a pointer action + for (int code = IPT_UI_FIRST + 1; IPT_UI_LAST > code; ++code) + machine().ui_input().pressed(code); + break; } - m_rebuilding = false; } - handle(process()); + + // deal with stack push/pop and rebuild + if (!is_active()) + return false; + if (do_rebuild()) + { + validate_selection(1); + need_update = true; + } + + // update the selected item in the event and let the implementation handle it + if ((result->iptkey != IPT_INVALID) && selection_valid()) + { + result->itemref = get_selection_ref(); + result->item = &m_items[m_selected]; + } + else + { + result.reset(); + } + need_update = handle(result ? &*result : nullptr) || need_update; + + // the implementation had another chance to push/pop or rebuild + if (!is_active()) + return false; + if (do_rebuild()) + { + validate_selection(1); + return true; + } + return need_update; } @@ -1267,6 +2103,16 @@ delegate<uint32_t (render_container &)> menu::get_ui_handler(mame_ui_manager &mu ***************************************************************************/ //------------------------------------------------- +// create_layout +//------------------------------------------------- + +text_layout menu::create_layout(float width, text_layout::text_justify justify, text_layout::word_wrapping wrap) +{ + return text_layout(*ui().get_font(), line_height() * x_aspect(), line_height(), width, justify, wrap); +} + + +//------------------------------------------------- // highlight //------------------------------------------------- @@ -1294,7 +2140,7 @@ void menu::draw_arrow(float x0, float y0, float x1, float y1, rgb_t fgcolor, uin void menu::extra_text_draw_box(float origx1, float origx2, float origy, float yspan, std::string_view text, int direction) { // get the size of the text - auto layout = ui().create_layout(container()); + auto layout = create_layout(); layout.add_text(text); // position this extra text @@ -1305,8 +2151,8 @@ void menu::extra_text_draw_box(float origx1, float origx2, float origy, float ys ui().draw_outlined_box(container(), x1, y1, x2, y2, ui().colors().background_color()); // take off the borders - x1 += ui().box_lr_border() * machine().render().ui_aspect(&container()); - y1 += ui().box_tb_border(); + x1 += lr_border(); + y1 += tb_border(); // draw the text within it layout.emit(container(), x1, y1); @@ -1329,14 +2175,14 @@ void menu::draw_background() void menu::extra_text_position(float origx1, float origx2, float origy, float yspan, text_layout &layout, int direction, float &x1, float &y1, float &x2, float &y2) { - float width = layout.actual_width() + (2 * ui().box_lr_border() * machine().render().ui_aspect(&container())); + float width = layout.actual_width() + (2 * lr_border()); float maxwidth = std::max(width, origx2 - origx1); // compute our bounds x1 = 0.5F - (0.5F * maxwidth); x2 = x1 + maxwidth; y1 = origy + (yspan * direction); - y2 = origy + (ui().box_tb_border() * direction); + y2 = origy + (tb_border() * direction); if (y1 > y2) std::swap(y1, y2); |