// license:BSD-3-Clause // copyright-holders:Nicola Salmoria, Aaron Giles, Nathan Woods, Maurizio Petrarota /********************************************************************* ui/menu.cpp Internal MAME menus for the user interface. *********************************************************************/ #include "emu.h" #include "ui/menu.h" #include "ui/ui.h" #include "ui/mainmenu.h" #include "ui/utils.h" #include "ui/miscmenu.h" #include "cheat.h" #include "mame.h" #include "drivenum.h" #include "rendutil.h" #include "uiinput.h" #include #include #include #include namespace ui { /*************************************************************************** GLOBAL VARIABLES ***************************************************************************/ std::mutex menu::s_global_state_guard; menu::global_state_map menu::s_global_states; /*************************************************************************** INLINE FUNCTIONS ***************************************************************************/ menu::global_state_ptr menu::get_global_state(running_machine &machine) { std::lock_guard guard(s_global_state_guard); auto const it(s_global_states.find(&machine)); return (it != s_global_states.end()) ? it->second : global_state_ptr(); } //------------------------------------------------- // exclusive_input_pressed - return true if the // given key is pressed and we haven't already // reported a key //------------------------------------------------- bool menu::exclusive_input_pressed(int &iptkey, int key, int repeat) { if ((iptkey == IPT_INVALID) && machine().ui_input().pressed_repeat(key, repeat)) { iptkey = key; return true; } else { return false; } } /*************************************************************************** CORE SYSTEM MANAGEMENT ***************************************************************************/ menu::global_state::global_state(running_machine &machine, ui_options const &options) : widgets_manager(machine) , m_machine(machine) , m_cleanup_callbacks() , m_bgrnd_bitmap() , m_bgrnd_texture(nullptr, machine.render()) , m_stack() , m_free() { render_manager &render(machine.render()); // create a texture for main menu background m_bgrnd_texture.reset(render.texture_alloc(render_texture::hq_scale)); if (options.use_background_image() && (&machine.system() == &GAME_NAME(___empty))) { m_bgrnd_bitmap = std::make_unique(0, 0); emu_file backgroundfile(".", OPEN_FLAG_READ); render_load_jpeg(*m_bgrnd_bitmap, backgroundfile, nullptr, "background.jpg"); if (!m_bgrnd_bitmap->valid()) render_load_png(*m_bgrnd_bitmap, backgroundfile, nullptr, "background.png"); if (m_bgrnd_bitmap->valid()) m_bgrnd_texture->set_bitmap(*m_bgrnd_bitmap, m_bgrnd_bitmap->cliprect(), TEXFORMAT_ARGB32); else m_bgrnd_bitmap->reset(); } } menu::global_state::~global_state() { // it shouldn't really be possible to get here with active menus because of reference loops assert(!m_stack); assert(!m_free); stack_reset(); clear_free_list(); for (auto const &callback : m_cleanup_callbacks) callback(m_machine); } void menu::global_state::add_cleanup_callback(cleanup_callback &&callback) { m_cleanup_callbacks.emplace_back(std::move(callback)); } void menu::global_state::stack_push(std::unique_ptr &&menu) { menu->m_parent = std::move(m_stack); m_stack = std::move(menu); m_stack->reset(reset_options::SELECT_FIRST); m_stack->machine().ui_input().reset(); } void menu::global_state::stack_pop() { if (m_stack) { std::unique_ptr menu(std::move(m_stack)); m_stack = std::move(menu->m_parent); menu->m_parent = std::move(m_free); m_free = std::move(menu); m_machine.ui_input().reset(); } } void menu::global_state::stack_reset() { while (m_stack) stack_pop(); } void menu::global_state::clear_free_list() { // free stack is in reverse order - unwind it properly std::unique_ptr reversed; while (m_free) { std::unique_ptr menu(std::move(m_free)); m_free = std::move(menu->m_parent); menu->m_parent = std::move(reversed); reversed = std::move(menu); } while (reversed) reversed = std::move(reversed->m_parent); } bool menu::global_state::stack_has_special_main_menu() const { for (auto menu = m_stack.get(); menu != nullptr; menu = menu->m_parent.get()) { if (menu->is_special_main_menu()) return true; } return false; } //------------------------------------------------- // init - initialize the menu system //------------------------------------------------- void menu::init(running_machine &machine, ui_options &mopt) { // initialize the menu stack { std::lock_guard guard(s_global_state_guard); auto const ins(s_global_states.emplace(&machine, std::make_shared(machine, mopt))); assert(ins.second); // calling init twice is bad if (ins.second) machine.add_notifier(MACHINE_NOTIFY_EXIT, machine_notify_delegate(&menu::exit, &machine)); // add an exit callback to free memory else ins.first->second->stack_reset(); } } //------------------------------------------------- // exit - clean up after ourselves //------------------------------------------------- void menu::exit(running_machine &machine) { // free menus global_state_ptr const state(get_global_state(machine)); state->stack_reset(); state->clear_free_list(); std::lock_guard guard(s_global_state_guard); s_global_states.erase(&machine); } /*************************************************************************** CORE MENU MANAGEMENT ***************************************************************************/ //------------------------------------------------- // menu - menu constructor //------------------------------------------------- menu::menu(mame_ui_manager &mui, render_container &container) : m_selected(0) , m_items() , m_visible_lines(0) , m_visible_items(0) , m_global_state(get_global_state(mui.machine())) , m_special_main_menu(false) , m_ui(mui) , m_container(container) , m_parent() , 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) { assert(m_global_state); // not calling init is bad reset(reset_options::SELECT_FIRST); top_line = 0; } //------------------------------------------------- // ~menu - menu destructor //------------------------------------------------- menu::~menu() { } //------------------------------------------------- // reset - free all items in the menu //------------------------------------------------- void menu::reset(reset_options options) { // based on the reset option, set the reset info m_resetpos = 0; m_resetref = nullptr; if (options == reset_options::REMEMBER_POSITION) m_resetpos = m_selected; else if (options == reset_options::REMEMBER_REF) m_resetref = get_selection_ref(); // reset the item count back to 0 m_items.clear(); m_visible_items = 0; m_selected = 0; // add an item to return if (!m_parent) { item_append(_("Return to Machine"), "", 0, nullptr); } else if (m_parent->is_special_main_menu()) { if (machine().options().ui() == emu_options::UI_SIMPLE) item_append(_("Exit"), "", 0, nullptr); else item_append(_("Exit"), "", FLAG_LEFT_ARROW | FLAG_RIGHT_ARROW, nullptr); } else { if (machine().options().ui() != emu_options::UI_SIMPLE && stack_has_special_main_menu()) item_append(_("Return to Previous Menu"), "", FLAG_LEFT_ARROW | FLAG_RIGHT_ARROW, nullptr); else item_append(_("Return to Previous Menu"), "", 0, nullptr); } } //------------------------------------------------- // is_special_main_menu - returns whether the // menu has special needs //------------------------------------------------- bool menu::is_special_main_menu() const { return m_special_main_menu; } //------------------------------------------------- // set_special_main_menu - set whether the // menu has special needs //------------------------------------------------- void menu::set_special_main_menu(bool special) { m_special_main_menu = special; } //------------------------------------------------- // item_append - append a new item to the // end of the menu //------------------------------------------------- void menu::item_append(menu_item item) { item_append(item.text, item.subtext, item.flags, item.ref, item.type); } //------------------------------------------------- // item_append - append a new item to the // end of the menu //------------------------------------------------- void menu::item_append(menu_item_type type, uint32_t flags) { if (type == menu_item_type::SEPARATOR) item_append(MENU_SEPARATOR_ITEM, "", flags, nullptr, menu_item_type::SEPARATOR); } //------------------------------------------------- // item_append - append a new item to the // end of the menu //------------------------------------------------- void menu::item_append(const std::string &text, const std::string &subtext, uint32_t flags, void *ref, menu_item_type type) { item_append(std::string(text), std::string(subtext), flags, ref, type); } //------------------------------------------------- // item_append - append a new item to the // end of the menu //------------------------------------------------- void menu::item_append(std::string &&text, std::string &&subtext, uint32_t flags, void *ref, menu_item_type type) { // only allow multiline as the first item if ((flags & FLAG_MULTILINE) != 0) assert(m_items.size() == 1); // only allow a single multi-line item else if (m_items.size() >= 2) assert((m_items[0].flags & FLAG_MULTILINE) == 0); // allocate a new item and populate it menu_item pitem; pitem.text = std::move(text); pitem.subtext = std::move(subtext); pitem.flags = flags; pitem.ref = ref; pitem.type = type; // append to array auto index = m_items.size(); if (!m_items.empty()) { m_items.emplace(m_items.end() - 1, std::move(pitem)); --index; } else m_items.emplace_back(std::move(pitem)); // update the selection if we need to if (m_resetpos == index || (m_resetref != nullptr && m_resetref == ref)) m_selected = index; if (m_resetpos == (m_items.size() - 1)) m_selected = m_items.size() - 1; } //------------------------------------------------- // item_append_on_off - append a new "On"/"Off" // 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) { 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); } //------------------------------------------------- // repopulate - repopulate menu items //------------------------------------------------- void menu::repopulate(reset_options options) { reset(options); populate(m_customtop, m_custombottom); } //------------------------------------------------- // process - process a menu, drawing it // and returning any interesting events //------------------------------------------------- const menu::event *menu::process(uint32_t flags, float x0, float y0) { // reset the event m_event.iptkey = IPT_INVALID; // first make sure our selection is valid validate_selection(1); // draw the menu if (m_items.size() > 1 && (m_items[0].flags & FLAG_MULTILINE) != 0) draw_text_box(); else draw(flags); // process input if (!(flags & PROCESS_NOKEYS) && !(flags & PROCESS_NOINPUT)) { // read events handle_events(flags, m_event); // handle the keys if we don't already have an event if (m_event.iptkey == IPT_INVALID) handle_keys(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.type = m_items[m_selected].type; return &m_event; } else { return nullptr; } } //------------------------------------------------- // set_selection - changes the index // of the currently selected menu item //------------------------------------------------- void menu::set_selection(void *selected_itemref) { m_selected = -1; for (int itemnum = 0; itemnum < m_items.size(); itemnum++) { if (m_items[itemnum].ref == selected_itemref) { m_selected = itemnum; break; } } } /*************************************************************************** INTERNAL MENU PROCESSING ***************************************************************************/ //------------------------------------------------- // draw - draw a menu //------------------------------------------------- void menu::draw(uint32_t flags) { // first draw the FPS counter if (ui().show_fps_counter()) { ui().draw_text_full(container(), machine().video().speed_text().c_str(), 0.0f, 0.0f, 1.0f, ui::text_layout::RIGHT, ui::text_layout::WORD, mame_ui_manager::OPAQUE_, rgb_t::white(), rgb_t::black(), nullptr, nullptr); } bool const customonly = (flags & PROCESS_CUSTOM_ONLY); bool const noimage = (flags & PROCESS_NOIMAGE); 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 = lr_arrow_width * 1.3f; float const lr_border = ui().box_lr_border() * aspect; if (&machine().system() == &GAME_NAME(___empty) && !noimage) draw_background(); // compute the width and height of the full menu float visible_width = 0; float visible_main_menu_height = 0; for (auto const &pitem : m_items) { // compute width of left hand side float total_width = gutter_width + ui().get_string_width(pitem.text.c_str()) + 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.c_str()); // track the maximum if (total_width > visible_width) visible_width = total_width; // track the height as well visible_main_menu_height += line_height; } // account for extra space at the top and bottom float const visible_extra_menu_height = m_customtop + m_custombottom; // add a little bit of slop for rounding visible_width += 0.01f; visible_main_menu_height += 0.01f; // if we are too wide or too tall, clamp it down if (visible_width + 2.0f * lr_border > 1.0f) visible_width = 1.0f - 2.0f * lr_border; // 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; 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) + m_customtop; // 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(); if (!customonly) ui().draw_outlined_box(container(), x1, y1, x2, y2, ui().colors().background_color()); if (top_line < 0 || is_first_selected()) top_line = 0; if (m_selected >= (top_line + m_visible_lines)) top_line = m_selected - (m_visible_lines / 2); if ((top_line > (m_items.size() - m_visible_lines)) || is_last_selected()) top_line = m_items.size() - m_visible_lines; // 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()); // 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); // 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(); // 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) { for (int linenum = 0; linenum < m_visible_lines; linenum++) { auto const itemnum = top_line + linenum; menu_item const &pitem = m_items[itemnum]; char const *const itemtext = pitem.text.c_str(); rgb_t fgcolor = ui().colors().text_color(); 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; // set the hover if this is our item if (mouse_in_rect(line_x0, line_y0, line_x1, line_y1) && is_selectable(pitem)) m_hover = itemnum; // if we're selected, draw with a different background if (is_selected(itemnum)) { fgcolor = fgcolor2 = fgcolor3 = ui().colors().selected_color(); bgcolor = ui().colors().selected_bg_color(); } // else if the mouse is over this item, draw with a different background else if (itemnum == m_hover) { 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); if (linenum == 0 && show_top_arrow) { // 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); if (m_hover == itemnum) m_hover = HOVER_ARROW_UP; } else if (linenum == m_visible_lines - 1 && show_bottom_arrow) { // if we're on the bottom line, display the 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, fgcolor, ROT0 ^ ORIENTATION_FLIP_Y); if (m_hover == itemnum) m_hover = HOVER_ARROW_DOWN; } 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)); } 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)); } ui().draw_text_full(container(), itemtext, effective_left, line_y0, effective_width, ui::text_layout::CENTER, ui::text_layout::TRUNCATE, mame_ui_manager::NORMAL, fgcolor, bgcolor, nullptr, nullptr); } else { // otherwise, draw the item on the left and the subitem text on the right bool const subitem_invert(pitem.flags & FLAG_INVERT); char const *subitem_text(pitem.subtext.c_str()); float item_width, subitem_width; // draw the left-side text ui().draw_text_full(container(), itemtext, effective_left, line_y0, effective_width, ui::text_layout::LEFT, ui::text_layout::TRUNCATE, mame_ui_manager::NORMAL, fgcolor, bgcolor, &item_width, nullptr); if (pitem.flags & FLAG_COLOR_BOX) { rgb_t color = rgb_t((uint32_t)strtoul(subitem_text, nullptr, 16)); // give 2 spaces worth of padding subitem_width = ui().get_string_width("FF00FF00"); ui().draw_outlined_box(container(), effective_left + effective_width - subitem_width, line_y0, effective_left + effective_width, line_y1, color); } else { // give 2 spaces worth of padding 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) { subitem_text = "..."; if (is_selected(itemnum)) selected_subitem_too_big = true; } // customize subitem text color if (!core_stricmp(subitem_text, _("On"))) fgcolor2 = rgb_t(0x00,0xff,0x00); if (!core_stricmp(subitem_text, _("Off"))) fgcolor2 = rgb_t(0xff,0x00,0x00); if (!core_stricmp(subitem_text, _("Auto"))) fgcolor2 = rgb_t(0xff,0xff,0x00); // draw the subitem right-justified ui().draw_text_full(container(), subitem_text, effective_left + item_width, line_y0, effective_width - item_width, ui::text_layout::RIGHT, ui::text_layout::TRUNCATE, mame_ui_manager::NORMAL, subitem_invert ? fgcolor3 : fgcolor2, bgcolor, &subitem_width, nullptr); } // apply arrows if (is_selected(itemnum) && (pitem.flags & FLAG_LEFT_ARROW)) { draw_arrow( effective_left + effective_width - subitem_width - gutter_width, line_y0 + 0.1f * line_height, effective_left + effective_width - subitem_width - gutter_width + lr_arrow_width, line_y0 + 0.9f * line_height, fgcolor, ROT90 ^ ORIENTATION_FLIP_X); } if (is_selected(itemnum) && (pitem.flags & FLAG_RIGHT_ARROW)) { draw_arrow( effective_left + effective_width + gutter_width - lr_arrow_width, line_y0 + 0.1f * line_height, effective_left + effective_width + gutter_width, line_y0 + 0.9f * line_height, fgcolor, ROT90); } } } } // if the selected subitem is too big, display it in a separate offset box if (selected_subitem_too_big) { 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; // compute the multi-line target width/height ui().draw_text_full(container(), pitem.subtext.c_str(), 0, 0, visible_width * 0.75f, ui::text_layout::RIGHT, ui::text_layout::WORD, mame_ui_manager::NONE, rgb_t::white(), rgb_t::black(), &target_width, &target_height); // 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(); // 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(), subitem_invert ? ui().colors().selected_bg_color() : ui().colors().background_color()); ui().draw_text_full(container(), pitem.subtext.c_str(), target_x, target_y, target_width, ui::text_layout::RIGHT, ui::text_layout::WORD, mame_ui_manager::NORMAL, ui().colors().selected_color(), ui().colors().selected_bg_color(), nullptr, nullptr); } // 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); } void menu::custom_render(void *selectedref, float top, float bottom, float x, float y, float x2, float y2) { } //------------------------------------------------- // draw_text_box - draw a multiline // word-wrapped text box with a menu item at the // bottom //------------------------------------------------- void menu::draw_text_box() { const char *text = m_items[0].text.c_str(); const char *backtext = m_items[1].text.c_str(); float const aspect = machine().render().ui_aspect(&container()); float line_height = ui().get_line_height(); float lr_arrow_width = 0.4f * line_height * aspect; float gutter_width = lr_arrow_width; float const lr_border = ui().box_lr_border() * aspect; float target_width, target_height, prior_width; float target_x, target_y; // compute the multi-line target width/height ui().draw_text_full(container(), text, 0, 0, 1.0f - 2.0f * lr_border - 2.0f * gutter_width, ui::text_layout::LEFT, ui::text_layout::WORD, mame_ui_manager::NONE, rgb_t::white(), rgb_t::black(), &target_width, &target_height); target_height += 2.0f * line_height; if (target_height > 1.0f - 2.0f * ui().box_tb_border()) target_height = floorf((1.0f - 2.0f * ui().box_tb_border()) / line_height) * line_height; // maximum against "return to prior menu" text prior_width = ui().get_string_width(backtext) + 2.0f * gutter_width; target_width = std::max(target_width, prior_width); // determine the target location target_x = 0.5f - 0.5f * target_width; target_y = 0.5f - 0.5f * target_height; // make sure we stay on-screen if (target_x < lr_border + gutter_width) target_x = lr_border + gutter_width; if (target_x + target_width + gutter_width + lr_border > 1.0f) target_x = 1.0f - lr_border - gutter_width - target_width; if (target_y < ui().box_tb_border()) target_y = ui().box_tb_border(); if (target_y + target_height + ui().box_tb_border() > 1.0f) target_y = 1.0f - ui().box_tb_border() - target_height; // add a box around that ui().draw_outlined_box(container(), target_x - lr_border - gutter_width, target_y - ui().box_tb_border(), target_x + target_width + gutter_width + lr_border, target_y + target_height + ui().box_tb_border(), (m_items[0].flags & FLAG_REDTEXT) ? UI_RED_COLOR : ui().colors().background_color()); ui().draw_text_full(container(), text, target_x, target_y, target_width, ui::text_layout::LEFT, ui::text_layout::WORD, mame_ui_manager::NORMAL, ui().colors().text_color(), ui().colors().text_bg_color(), nullptr, nullptr); // draw the "return to prior menu" text with a hilight behind it highlight( target_x + 0.5f * UI_LINE_WIDTH, target_y + target_height - line_height, target_x + target_width - 0.5f * UI_LINE_WIDTH, target_y + target_height, ui().colors().selected_bg_color()); ui().draw_text_full(container(), backtext, target_x, target_y + target_height - line_height, target_width, ui::text_layout::CENTER, ui::text_layout::TRUNCATE, mame_ui_manager::NORMAL, ui().colors().selected_color(), ui().colors().selected_bg_color(), nullptr, nullptr); // artificially set the hover to the last item so a double-click exits m_hover = m_items.size() - 1; } //------------------------------------------------- // 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; } } //------------------------------------------------- // ignore_mouse - set members to ignore mouse // input //------------------------------------------------- void menu::ignore_mouse() { m_mouse_hit = false; m_mouse_button = false; m_mouse_x = -1.0f; m_mouse_y = -1.0f; } //------------------------------------------------- // handle_events - generically handle // input events for a menu //------------------------------------------------- void menu::handle_events(uint32_t flags, event &ev) { bool stop = false; ui_event local_menu_event; // loop while we have interesting events while (!stop && machine().ui_input().pop_event(&local_menu_event)) { switch (local_menu_event.event_type) { // if we are hovering over a valid item, select it with a single click case ui_event::MOUSE_DOWN: if (custom_mouse_down()) return; if ((flags & PROCESS_ONLYCHAR) == 0) { if (m_hover >= 0 && m_hover < m_items.size()) m_selected = m_hover; else if (m_hover == HOVER_ARROW_UP) { if ((flags & FLAG_UI_DATS) != 0) { top_line -= m_visible_items - (last_item_visible() ? 1 : 0); return; } 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 & FLAG_UI_DATS) != 0) { top_line += m_visible_lines - 2; return; } 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; } } break; // if we are hovering over a valid item, fake a UI_SELECT with a double-click case ui_event::MOUSE_DOUBLE_CLICK: if (!(flags & PROCESS_ONLYCHAR) && m_hover >= 0 && m_hover < m_items.size()) { m_selected = m_hover; ev.iptkey = IPT_UI_SELECT; if (is_last_selected()) { ev.iptkey = IPT_UI_CANCEL; stack_pop(); } stop = true; } break; // caught scroll event case ui_event::MOUSE_WHEEL: if (!(flags & PROCESS_ONLYCHAR)) { if (local_menu_event.zdelta > 0) { if ((flags & FLAG_UI_DATS) != 0) { top_line -= local_menu_event.num_lines; return; } if (is_first_selected()) select_last_item(); else { m_selected -= local_menu_event.num_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 -= local_menu_event.num_lines; } else { if ((flags & FLAG_UI_DATS)) { top_line += local_menu_event.num_lines; return; } 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::IME_CHAR: ev.iptkey = IPT_SPECIAL; ev.unichar = local_menu_event.ch; stop = true; break; // ignore everything else default: break; } } } //------------------------------------------------- // handle_keys - generically handle // keys for a menu //------------------------------------------------- void menu::handle_keys(uint32_t flags, int &iptkey) { bool ignorepause = stack_has_special_main_menu(); int code; // bail if no items if (m_items.empty()) return; // 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()) { iptkey = IPT_UI_CANCEL; stack_pop(); } return; } // bail out if ((flags & PROCESS_ONLYCHAR)) return; // hitting cancel also pops the stack if (exclusive_input_pressed(iptkey, IPT_UI_CANCEL, 0)) { if (!menu_has_search_active()) stack_pop(); return; } // validate the current selection validate_selection(1); // swallow left/right keys if they are not appropriate bool ignoreleft = ((selected_item().flags & FLAG_LEFT_ARROW) == 0); bool ignoreright = ((selected_item().flags & FLAG_RIGHT_ARROW) == 0); if ((m_items[0].flags & FLAG_UI_DATS)) ignoreleft = ignoreright = false; // accept left/right keys as-is with repeat if (!ignoreleft && exclusive_input_pressed(iptkey, IPT_UI_LEFT, (flags & PROCESS_LR_REPEAT) ? 6 : 0)) return; if (!ignoreright && exclusive_input_pressed(iptkey, IPT_UI_RIGHT, (flags & PROCESS_LR_REPEAT) ? 6 : 0)) return; // up backs up by one item if (exclusive_input_pressed(iptkey, IPT_UI_UP, 6)) { if ((m_items[0].flags & FLAG_UI_DATS)) { top_line--; return; } if (is_first_selected()) select_last_item(); else { --m_selected; 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--; } // down advances by one item if (exclusive_input_pressed(iptkey, IPT_UI_DOWN, 6)) { if ((m_items[0].flags & FLAG_UI_DATS)) { top_line++; return; } if (is_last_selected()) select_first_item(); else { ++m_selected; 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++; } // page up backs up by m_visible_items if (exclusive_input_pressed(iptkey, IPT_UI_PAGE_UP, 6)) { 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); } // page down advances by m_visible_items if (exclusive_input_pressed(iptkey, IPT_UI_PAGE_DOWN, 6)) { 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); } // home goes to the start if (exclusive_input_pressed(iptkey, IPT_UI_HOME, 0)) select_first_item(); // end goes to the last if (exclusive_input_pressed(iptkey, IPT_UI_END, 0)) select_last_item(); // pause enables/disables pause if (!ignorepause && exclusive_input_pressed(iptkey, IPT_UI_PAUSE, 0)) { if (machine().paused()) machine().resume(); else 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) { for (code = IPT_UI_FIRST + 1; code < IPT_UI_LAST; code++) { if (code == IPT_UI_CONFIGURE || (code == IPT_UI_LEFT && ignoreleft) || (code == IPT_UI_RIGHT && ignoreright) || (code == IPT_UI_PAUSE && ignorepause)) continue; if (exclusive_input_pressed(iptkey, code, 0)) break; } } } //------------------------------------------------- // select_first_item - select the first item in // the menu //------------------------------------------------- void menu::select_first_item() { m_selected = top_line = 0; validate_selection(1); } //------------------------------------------------- // select_last_item - select the last item in the // menu //------------------------------------------------- void menu::select_last_item() { m_selected = top_line = m_items.size() - 1; validate_selection(-1); } //------------------------------------------------- // validate_selection - validate the // current selection and ensure it is on a // correct item //------------------------------------------------- void menu::validate_selection(int scandir) { // clamp to be in range if (m_selected < 0) m_selected = 0; else if (m_selected >= m_items.size()) m_selected = m_items.size() - 1; // skip past unselectable items while (!is_selectable(m_items[m_selected])) m_selected = (m_selected + m_items.size() + scandir) % m_items.size(); } /*************************************************************************** MENU STACK MANAGEMENT ***************************************************************************/ void menu::do_handle() { if (m_items.size() < 2) populate(m_customtop, m_custombottom); handle(); } /*************************************************************************** UI SYSTEM INTERACTION ***************************************************************************/ //------------------------------------------------- // ui_menu_ui_handler - displays the current menu // and calls the menu handler //------------------------------------------------- uint32_t menu::ui_handler(render_container &container, mame_ui_manager &mui) { global_state_ptr const state(get_global_state(mui.machine())); // if we have no menus stacked up, start with the main menu if (!state->topmost_menu()) state->stack_push(std::unique_ptr(global_alloc_clear(mui, container))); // update the menu state if (state->topmost_menu()) state->topmost_menu()->do_handle(); // clear up anything pending to be released state->clear_free_list(); // if the menus are to be hidden, return a cancel here if (mui.is_menu_active() && ((mui.machine().ui_input().pressed(IPT_UI_CONFIGURE) && !state->stack_has_special_main_menu()) || !state->topmost_menu())) return UI_HANDLER_CANCEL; return 0; } /*************************************************************************** MENU HELPERS ***************************************************************************/ //------------------------------------------------- // highlight //------------------------------------------------- void menu::highlight(float x0, float y0, float x1, float y1, rgb_t bgcolor) { container().add_quad(x0, y0, x1, y1, bgcolor, m_global_state->hilight_texture(), PRIMFLAG_BLENDMODE(BLENDMODE_ALPHA) | PRIMFLAG_TEXWRAP(1) | PRIMFLAG_PACKABLE); } //------------------------------------------------- // draw_arrow //------------------------------------------------- void menu::draw_arrow(float x0, float y0, float x1, float y1, rgb_t fgcolor, uint32_t orientation) { container().add_quad(x0, y0, x1, y1, fgcolor, m_global_state->arrow_texture(), PRIMFLAG_BLENDMODE(BLENDMODE_ALPHA) | PRIMFLAG_TEXORIENT(orientation) | PRIMFLAG_PACKABLE); } //------------------------------------------------- // extra_text_draw_box - generically adds header // or footer text //------------------------------------------------- void menu::extra_text_draw_box(float origx1, float origx2, float origy, float yspan, const char *text, int direction) { // get the size of the text auto layout = ui().create_layout(container()); layout.add_text(text); // position this extra text float x1, y1, x2, y2; extra_text_position(origx1, origx2, origy, yspan, layout, direction, x1, y1, x2, y2); // draw a box 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(); // draw the text within it layout.emit(container(), x1, y1); } void menu::draw_background() { // draw background image if available if (ui().options().use_background_image() && m_global_state->bgrnd_bitmap() && m_global_state->bgrnd_bitmap()->valid()) container().add_quad(0.0f, 0.0f, 1.0f, 1.0f, rgb_t::white(), m_global_state->bgrnd_texture(), PRIMFLAG_BLENDMODE(BLENDMODE_ALPHA)); } //------------------------------------------------- // extra_text_position - given extra text that has // been put into a layout, position it //------------------------------------------------- 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 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); if (y1 > y2) std::swap(y1, y2); } //------------------------------------------------- // extra_text_render - generically adds header // and footer text //------------------------------------------------- void menu::extra_text_render(float top, float bottom, float origx1, float origy1, float origx2, float origy2, const char *header, const char *footer) { header = (header && *header) ? header : nullptr; footer = (footer && *footer) ? footer : nullptr; if (header != nullptr) extra_text_draw_box(origx1, origx2, origy1, top, header, -1); if (footer != nullptr) extra_text_draw_box(origx1, origx2, origy2, bottom, footer, +1); } } // namespace ui