// license:BSD-3-Clause // copyright-holders:Maurizio Petrarota, Vas Crabb /*************************************************************************** ui/utils.cpp Internal UI user interface. ***************************************************************************/ #include "emu.h" #include "ui/utils.h" #include "ui/inifile.h" #include "ui/selector.h" #include "language.h" #include "mame.h" #include "drivenum.h" #include "rendfont.h" #include "romload.h" #include "softlist.h" #include #include #include #include #include #include #include namespace ui { namespace { constexpr char const *SOFTWARE_REGIONS[] = { "arab", "arg", "asia", "aus", "aut", "bel", "blr", "bra", "can", "chi", "chn", "cze", "den", "ecu", "esp", "euro", "fin", "fra", "gbr", "ger", "gre", "hkg", "hun", "irl", "isr", "isv", "ita", "jpn", "kaz", "kor", "lat", "lux", "mex", "ned", "nld", "nor", "nzl", "pol", "rus", "slo", "spa", "sui", "swe", "tha", "tpe", "tw", "uk", "ukr", "usa" }; constexpr char const *MACHINE_FILTER_NAMES[machine_filter::COUNT] = { __("Unfiltered"), __("Available"), __("Unavailable"), __("Working"), __("Not Working"), __("Mechanical"), __("Not Mechanical"), __("Category"), __("Favorites"), __("BIOS"), __("Not BIOS"), __("Parents"), __("Clones"), __("Manufacturer"), __("Year"), __("Save Supported"), __("Save Unsupported"), __("CHD Required"), __("No CHD Required"), __("Vertical Screen"), __("Horizontal Screen"), __("Custom Filter") }; constexpr char const *SOFTWARE_FILTER_NAMES[software_filter::COUNT] = { __("Unfiltered"), __("Available"), __("Unavailable"), __("Favorites"), __("Parents"), __("Clones"), __("Year"), __("Publisher"), __("Supported"), __("Partially Supported"), __("Unsupported"), __("Release Region"), __("Device Type"), __("Software List"), __("Custom Filter") }; //------------------------------------------------- // base implementation for simple filters //------------------------------------------------- template class simple_filter_impl_base : public Base { public: virtual char const *config_name() const override { return Base::config_name(Type); } virtual char const *display_name() const override { return Base::display_name(Type); } virtual char const *filter_text() const override { return nullptr; } virtual void show_ui(mame_ui_manager &mui, render_container &container, std::function &&handler) override { handler(*this); } virtual bool wants_adjuster() const override { return false; } virtual char const *adjust_text() const override { return filter_text(); } virtual uint32_t arrow_flags() const override { return 0; } virtual bool adjust_left() override { return false; } virtual bool adjust_right() override { return false; } virtual void save_ini(emu_file &file, unsigned indent) const override { file.puts(util::string_format("%2$*1$s%3$s = 1\n", 2 * indent, "", config_name()).c_str()); } virtual typename Base::type get_type() const override { return Type; } virtual std::string adorned_display_name(typename Base::type n) const override { std::string result; if (Type == n) { result = "_> "; convert_command_glyph(result); } result.append(Base::display_name(n)); return result; } using Base::config_name; using Base::display_name; protected: simple_filter_impl_base() { } }; //------------------------------------------------- // base implementation for single-choice filters //------------------------------------------------- template class choice_filter_impl_base : public simple_filter_impl_base { public: virtual char const *filter_text() const override { return selection_valid() ? selection_text().c_str() : nullptr; } virtual void show_ui(mame_ui_manager &mui, render_container &container, std::function &&handler) override { if (m_choices.empty()) { handler(*this); } else { menu::stack_push( mui, container, std::vector(m_choices), // ouch, a vector copy! m_selection, [this, cb = std::move(handler)] (int selection) { m_selection = selection; cb(*this); }); } } virtual bool wants_adjuster() const override { return have_choices(); } virtual uint32_t arrow_flags() const override { return ((have_choices() && m_selection) ? menu::FLAG_LEFT_ARROW : 0) | ((m_choices.size() > (m_selection + 1)) ? menu::FLAG_RIGHT_ARROW : 0); } virtual bool adjust_left() override { if (!have_choices() || !m_selection) return false; m_selection = (std::min)(m_selection - 1, unsigned(m_choices.size() - 1)); return true; } virtual bool adjust_right() override { if (m_choices.size() <= (m_selection + 1)) return false; ++m_selection; return true; } virtual void save_ini(emu_file &file, unsigned indent) const override { char const *const text(filter_text()); file.puts(util::string_format("%2$*1$s%3$s = %4$s\n", 2 * indent, "", this->config_name(), text ? text : "").c_str()); } protected: choice_filter_impl_base(std::vector const &choices, char const *value) : m_choices(choices) , m_selection(0U) { if (value) { std::vector::const_iterator const found(std::find(choices.begin(), choices.end(), value)); if (choices.end() != found) m_selection = std::distance(choices.begin(), found); } } bool have_choices() const { return !m_choices.empty(); } bool selection_valid() const { return m_choices.size() > m_selection; } unsigned selection_index() const { return m_selection; } std::string const &selection_text() const { return m_choices[m_selection]; } private: std::vector const &m_choices; unsigned m_selection; }; //------------------------------------------------- // base implementation for composite filters //------------------------------------------------- template class composite_filter_impl_base : public simple_filter_impl_base { public: virtual void show_ui(mame_ui_manager &mui, render_container &container, std::function &&handler) override; virtual bool wants_adjuster() const override { return true; } virtual char const *adjust_text() const override { return _(""); } virtual void save_ini(emu_file &file, unsigned indent) const override { auto const tail(std::find_if(std::begin(m_filters), std::end(m_filters), [] (typename Base::ptr const &flt) { return !flt; })); file.puts(util::string_format("%2$*1$s%3$s = %4$d\n", 2 * indent, "", this->config_name(), std::distance(std::begin(m_filters), tail)).c_str()); for (auto it = std::begin(m_filters); tail != it; ++it) (*it)->save_ini(file, indent + 1); } virtual std::string adorned_display_name(typename Base::type n) const override { std::string result; if (Type == n) { result = "_> "; convert_command_glyph(result); } else { for (unsigned i = 0; (MAX > i) && m_filters[i]; ++i) { if (m_filters[i]->get_type() == n) { result = util::string_format("@custom%u ", i + 1); convert_command_glyph(result); break; } } } result.append(Base::display_name(n)); return result; } virtual bool apply(typename Base::entry_type const &info) const override { std::bitset inclusions, included; for (typename Base::ptr const &flt : m_filters) { if (!flt) break; typename Base::type const t(flt->get_type()); if (Impl::is_inclusion(t)) { if (!included.test(t)) { inclusions.set(t); included.set(t, flt->apply(info)); } } else if (!flt->apply(info)) { return false; } } return inclusions == included; } protected: composite_filter_impl_base() { } void populate(char const *value, emu_file *file, unsigned indent) { // try to load filters from a file if (value && file) { unsigned const cnt(unsigned((std::max)(std::min(int(MAX), std::atoi(value)), 0))); for (unsigned i = 0; cnt > i; ++i) { typename Base::ptr flt(static_cast(*this).create(*file, indent + 1)); if (!flt || !check_type(i, flt->get_type())) break; m_filters[i] = std::move(flt); } } // instantiate first allowed filter type if we're still empty for (typename Base::type t = Base::FIRST; (Base::COUNT > t) && !m_filters[0]; ++t) { if (Impl::type_allowed(0, t)) m_filters[0] = static_cast(*this).create(t); } } private: static constexpr unsigned MAX = 8; class menu_configure : public menu { public: menu_configure( mame_ui_manager &mui, render_container &container, Impl &parent, std::function &&handler) : menu(mui, container) , m_parent(parent) , m_handler(std::move(handler)) , m_added(false) { } virtual ~menu_configure() override { m_handler(m_parent); } protected: virtual void custom_render(void *selectedref, float top, float bottom, float x, float y, float x2, float y2) override { char const *const text[] = { _("Select custom filters:") }; draw_text_box( std::begin(text), std::end(text), x, x2, y - top, y - UI_BOX_TB_BORDER, ui::text_layout::CENTER, ui::text_layout::NEVER, false, UI_TEXT_COLOR, UI_GREEN_COLOR, 1.0f); } private: enum : uintptr_t { FILTER_FIRST = 1, FILTER_LAST = FILTER_FIRST + MAX - 1, ADJUST_FIRST, ADJUST_LAST = ADJUST_FIRST + MAX - 1, REMOVE_FILTER, ADD_FILTER }; virtual void populate(float &customtop, float &custombottom) override; virtual void handle() override; bool set_filter_type(unsigned pos, typename Base::type n) { if (!m_parent.m_filters[pos] || (m_parent.m_filters[pos]->get_type())) { save_filter(pos); retrieve_filter(pos, n); return true; } else { return false; } } bool append_filter() { unsigned pos = 0; while (m_parent.m_filters[pos]) { if (MAX <= ++pos) return false; } for (typename Base::type candidate = Base::FIRST; Base::COUNT > candidate; ++candidate) { if (m_parent.check_type(pos, candidate)) { set_filter_type(pos, candidate); return true; } } return false; } bool drop_last_filter() { for (unsigned i = 2; MAX >= i; ++i) { if ((MAX <= i) || !m_parent.m_filters[i]) { save_filter(i - 1); m_parent.m_filters[i - 1].reset(); return true; } } return false; } void save_filter(unsigned pos) { typename Base::ptr &flt(m_parent.m_filters[pos]); if (flt && flt->wants_adjuster()) m_saved_filters[pos][flt->get_type()] = std::move(flt); } void retrieve_filter(unsigned pos, typename Base::type n) { typename Base::ptr &flt(m_parent.m_filters[pos]); auto const found(m_saved_filters[pos].find(n)); if (m_saved_filters[pos].end() != found) { flt = std::move(found->second); m_saved_filters[pos].erase(found); } else { flt = m_parent.create(n); } } uint32_t get_arrow_flags(unsigned pos) { uint32_t result(0); typename Base::type const current(m_parent.m_filters[pos]->get_type()); // look for a lower type that's allowed and isn't contradictory typename Base::type prev(current); while ((Base::FIRST < prev) && !(FLAG_LEFT_ARROW & result)) { if (m_parent.check_type(pos, --prev)) result |= FLAG_LEFT_ARROW; } // look for a higher type that's allowed and isn't contradictory typename Base::type next(current); while ((Base::LAST > next) && !(FLAG_RIGHT_ARROW & result)) { if (m_parent.check_type(pos, ++next)) result |= FLAG_RIGHT_ARROW; } return result; } Impl &m_parent; std::map m_saved_filters[MAX]; std::function m_handler; bool m_added; }; bool check_type(unsigned pos, typename Base::type candidate) { if (!Impl::type_allowed(pos, candidate)) return false; unsigned j = 0; while ((MAX > j) && m_filters[j] && ((pos == j) || !Impl::types_contradictory(m_filters[j]->get_type(), candidate))) ++j; return (MAX <= j) || !m_filters[j]; }; typename Base::ptr m_filters[MAX]; }; template void composite_filter_impl_base::show_ui( mame_ui_manager &mui, render_container &container, std::function &&handler) { menu::stack_push(mui, container, static_cast(*this), std::move(handler)); } template void composite_filter_impl_base::menu_configure::populate(float &customtop, float &custombottom) { // add items for each active filter unsigned i = 0; for (i = 0; (MAX > i) && m_parent.m_filters[i]; ++i) { item_append(util::string_format(_("Filter %1$u"), i + 1), m_parent.m_filters[i]->display_name(), get_arrow_flags(i), (void *)(FILTER_FIRST + i)); if (m_added) set_selected_index(item_count() - 2); if (m_parent.m_filters[i]->wants_adjuster()) { std::string name("^!"); convert_command_glyph(name); item_append(name, m_parent.m_filters[i]->adjust_text(), m_parent.m_filters[i]->arrow_flags(), (void *)(ADJUST_FIRST + i)); } item_append(menu_item_type::SEPARATOR); } m_added = false; // add remove/add handlers if (1 < i) item_append(_("Remove last filter"), "", 0, (void *)REMOVE_FILTER); if (MAX > i) item_append(_("Add filter"), "", 0, (void *)ADD_FILTER); item_append(menu_item_type::SEPARATOR); // leave space for heading customtop = ui().get_line_height() + 3.0f * UI_BOX_TB_BORDER; } template void composite_filter_impl_base::menu_configure::handle() { const event *menu_event = process(PROCESS_LR_REPEAT); if (menu_event && menu_event->itemref) { m_added = false; bool changed(false); uintptr_t const ref(reinterpret_cast(menu_event->itemref)); switch (menu_event->iptkey) { case IPT_UI_LEFT: case IPT_UI_RIGHT: if ((FILTER_FIRST <= ref) && (FILTER_LAST >= ref)) { // change filter type unsigned const pos(ref - FILTER_FIRST); typename Base::type const current(m_parent.m_filters[pos]->get_type()); if (IPT_UI_LEFT == menu_event->iptkey) { typename Base::type n(current); while ((Base::FIRST < n) && !changed) { if (m_parent.check_type(pos, --n)) changed = set_filter_type(pos, n); } } else { typename Base::type n(current); while ((Base::LAST > n) && !changed) { if (m_parent.check_type(pos, ++n)) changed = set_filter_type(pos, n); } } } else if ((ADJUST_FIRST <= ref) && (ADJUST_LAST >= ref)) { // change filter value Base &pos(*m_parent.m_filters[ref - ADJUST_FIRST]); changed = (IPT_UI_LEFT == menu_event->iptkey) ? pos.adjust_left() : pos.adjust_right(); } break; case IPT_UI_SELECT: if ((FILTER_FIRST <= ref) && (FILTER_LAST >= ref)) { // show selector with non-contradictory types std::vector types; std::vector names; types.reserve(Base::COUNT); names.reserve(Base::COUNT); int sel(-1); unsigned const pos(ref - FILTER_FIRST); typename Base::type const current(m_parent.m_filters[pos]->get_type()); for (typename Base::type candidate = Base::FIRST; Base::COUNT > candidate; ++candidate) { if (Impl::type_allowed(pos, candidate)) { if (current == candidate) sel = types.size(); unsigned i = 0; while ((MAX > i) && m_parent.m_filters[i] && ((pos == i) || !Impl::types_contradictory(m_parent.m_filters[i]->get_type(), candidate))) ++i; if ((MAX <= i) || !m_parent.m_filters[i]) { types.emplace_back(candidate); names.emplace_back(Base::display_name(candidate)); } } } menu::stack_push( ui(), container(), std::move(names), sel, [this, pos, t = std::move(types)] (int selection) { if (set_filter_type(pos, t[selection])) reset(reset_options::REMEMBER_REF); }); } else if ((ADJUST_FIRST <= ref) && (ADJUST_LAST >= ref)) { // show selected filter's UI m_parent.m_filters[ref - ADJUST_FIRST]->show_ui(ui(), container(), [this] (Base &filter) { reset(reset_options::REMEMBER_REF); }); } else if (REMOVE_FILTER == ref) { changed = drop_last_filter(); } else if (ADD_FILTER == ref) { m_added = append_filter(); } break; } // rebuild if anything changed if (changed) reset(reset_options::REMEMBER_REF); else if (m_added) reset(reset_options::SELECT_FIRST); } } //------------------------------------------------- // invertable machine filters //------------------------------------------------- template class available_machine_filter_impl : public simple_filter_impl_base { public: available_machine_filter_impl(machine_filter_data const &data, char const *value, emu_file *file, unsigned indent) { } virtual bool apply(ui_system_info const &system) const override { return system.available; } }; template class working_machine_filter_impl : public simple_filter_impl_base { public: working_machine_filter_impl(machine_filter_data const &data, char const *value, emu_file *file, unsigned indent) { } virtual bool apply(ui_system_info const &system) const override { return !(system.driver->flags & machine_flags::NOT_WORKING); } }; template class mechanical_machine_filter_impl : public simple_filter_impl_base { public: mechanical_machine_filter_impl(machine_filter_data const &data, char const *value, emu_file *file, unsigned indent) { } virtual bool apply(ui_system_info const &system) const override { return system.driver->flags & machine_flags::MECHANICAL; } }; template class bios_machine_filter_impl : public simple_filter_impl_base { public: bios_machine_filter_impl(machine_filter_data const &data, char const *value, emu_file *file, unsigned indent) { } virtual bool apply(ui_system_info const &system) const override { return system.driver->flags & machine_flags::IS_BIOS_ROOT; } }; template class parents_machine_filter_impl : public simple_filter_impl_base { public: parents_machine_filter_impl(machine_filter_data const &data, char const *value, emu_file *file, unsigned indent) { } virtual bool apply(ui_system_info const &system) const override { bool const have_parent(strcmp(system.driver->parent, "0")); auto const parent_idx(have_parent ? driver_list::find(system.driver->parent) : -1); return !have_parent || (0 > parent_idx) || (driver_list::driver(parent_idx).flags & machine_flags::IS_BIOS_ROOT); } }; template class chd_machine_filter_impl : public simple_filter_impl_base { public: chd_machine_filter_impl(machine_filter_data const &data, char const *value, emu_file *file, unsigned indent) { } virtual bool apply(ui_system_info const &system) const override { for (tiny_rom_entry const *rom = system.driver->rom; !ROMENTRY_ISEND(rom); ++rom) { if (ROMENTRY_ISREGION(rom) && ROMREGION_ISDISKDATA(rom)) return true; } return false; } }; template class save_machine_filter_impl : public simple_filter_impl_base { public: save_machine_filter_impl(machine_filter_data const &data, char const *value, emu_file *file, unsigned indent) { } virtual bool apply(ui_system_info const &system) const override { return system.driver->flags & machine_flags::SUPPORTS_SAVE; } }; template class vertical_machine_filter_impl : public simple_filter_impl_base { public: vertical_machine_filter_impl(machine_filter_data const &data, char const *value, emu_file *file, unsigned indent) { } virtual bool apply(ui_system_info const &system) const override { return system.driver->flags & machine_flags::SWAP_XY; } }; //------------------------------------------------- // concrete machine filters //------------------------------------------------- class manufacturer_machine_filter : public choice_filter_impl_base { public: manufacturer_machine_filter(machine_filter_data const &data, char const *value, emu_file *file, unsigned indent) : choice_filter_impl_base(data.manufacturers(), value) { } virtual bool apply(ui_system_info const &system) const override { if (!have_choices()) return true; else if (!selection_valid()) return false; std::string const name(machine_filter_data::extract_manufacturer(system.driver->manufacturer)); return !name.empty() && (selection_text() == name); } }; class year_machine_filter : public choice_filter_impl_base { public: year_machine_filter(machine_filter_data const &data, char const *value, emu_file *file, unsigned indent) : choice_filter_impl_base(data.years(), value) { } virtual bool apply(ui_system_info const &system) const override { return !have_choices() || (selection_valid() && (selection_text() == system.driver->year)); } }; //------------------------------------------------- // complementary machine filters //------------------------------------------------- template