// license:BSD-3-Clause
// copyright-holders:Maurizio Petrarota, Vas Crabb
/***************************************************************************
ui/selsoft.cpp
UI software menu.
***************************************************************************/
#include "emu.h"
#include "ui/selsoft.h"
#include "ui/ui.h"
#include "ui/icorender.h"
#include "ui/inifile.h"
#include "ui/selector.h"
#include "audit.h"
#include "drivenum.h"
#include "emuopts.h"
#include "mame.h"
#include "rendutil.h"
#include "softlist_dev.h"
#include "uiinput.h"
#include "luaengine.h"
#include <algorithm>
#include <iterator>
#include <functional>
namespace ui {
namespace {
//-------------------------------------------------
// compares two items in the software list and
// sort them by parent-clone
//-------------------------------------------------
bool compare_software(ui_software_info const &a, ui_software_info const &b)
{
bool const clonex = !a.parentname.empty() && !a.parentlongname.empty();
bool const cloney = !b.parentname.empty() && !b.parentlongname.empty();
if (!clonex && !cloney)
{
return 0 > core_stricmp(a.longname.c_str(), b.longname.c_str());
}
else if (!clonex && cloney)
{
if ((a.shortname == b.parentname) && (a.instance == b.instance))
return true;
else
return 0 > core_stricmp(a.longname.c_str(), b.parentlongname.c_str());
}
else if (clonex && !cloney)
{
if ((a.parentname == b.shortname) && (a.instance == b.instance))
return false;
else
return 0 > core_stricmp(a.parentlongname.c_str(), b.longname.c_str());
}
else if ((a.parentname == b.parentname) && (a.instance == b.instance))
{
return 0 > core_stricmp(a.longname.c_str(), b.longname.c_str());
}
else
{
return 0 > core_stricmp(a.parentlongname.c_str(), b.parentlongname.c_str());
}
}
} // anonymous namespace
menu_select_software::search_item::search_item(ui_software_info const &s)
: software(s)
, ucs_shortname(ustr_from_utf8(normalize_unicode(s.shortname, unicode_normalization_form::D, true)))
, ucs_longname(ustr_from_utf8(normalize_unicode(s.longname, unicode_normalization_form::D, true)))
, penalty(1.0)
{
}
void menu_select_software::search_item::set_penalty(std::u32string const &search)
{
// TODO: search alternate title as well
penalty = util::edit_distance(search, ucs_shortname);
if (penalty)
penalty = (std::min)(penalty, util::edit_distance(search, ucs_longname));
}
//-------------------------------------------------
// ctor
//-------------------------------------------------
menu_select_software::menu_select_software(mame_ui_manager &mui, render_container &container, game_driver const &driver)
: menu_select_launch(mui, container, true)
, m_icon_paths()
, m_icons(MAX_ICONS_RENDER)
, m_driver(driver)
, m_has_empty_start(false)
, m_filter_data()
, m_filters()
, m_filter_type(software_filter::ALL)
, m_swinfo()
, m_searchlist()
, m_displaylist()
{
reselect_last::reselect(false);
build_software_list();
load_sw_custom_filters();
m_filter_highlight = m_filter_type;
set_switch_image();
ui_globals::cur_sw_dats_view = 0;
ui_globals::cur_sw_dats_total = 1;
}
//-------------------------------------------------
// dtor
//-------------------------------------------------
menu_select_software::~menu_select_software()
{
}
//-------------------------------------------------
// handle
//-------------------------------------------------
void menu_select_software::handle()
{
if (m_prev_selected == nullptr)
m_prev_selected = item(0).ref;
// ignore pause keys by swallowing them before we process the menu
machine().ui_input().pressed(IPT_UI_PAUSE);
// process the menu
const event *menu_event = process(PROCESS_LR_REPEAT);
if (menu_event)
{
if (dismiss_error())
{
// reset the error on any future event
}
else switch (menu_event->iptkey)
{
case IPT_UI_SELECT:
if ((get_focus() == focused_menu::MAIN) && menu_event->itemref)
inkey_select(menu_event);
break;
case IPT_UI_LEFT:
if (ui_globals::rpanel == RP_IMAGES)
{
// Images
previous_image_view();
}
else if (ui_globals::rpanel == RP_INFOS && ui_globals::cur_sw_dats_view > 0)
{
// Infos
ui_globals::cur_sw_dats_view--;
m_topline_datsview = 0;
}
break;
case IPT_UI_RIGHT:
if (ui_globals::rpanel == RP_IMAGES)
{
// Images
next_image_view();
}
else if (ui_globals::rpanel == RP_INFOS && ui_globals::cur_sw_dats_view < (ui_globals::cur_sw_dats_total - 1))
{
// Infos
ui_globals::cur_sw_dats_view++;
m_topline_datsview = 0;
}
break;
case IPT_UI_UP:
if ((get_focus() == focused_menu::LEFT) && (software_filter::FIRST < m_filter_highlight))
--m_filter_highlight;
break;
case IPT_UI_DOWN:
if ((get_focus() == focused_menu::LEFT) && (software_filter::LAST > m_filter_highlight))
++m_filter_highlight;
break;
case IPT_UI_HOME:
if (get_focus() == focused_menu::LEFT)
m_filter_highlight = software_filter::FIRST;
break;
case IPT_UI_END:
if (get_focus() == focused_menu::LEFT)
m_filter_highlight = software_filter::LAST;
break;
case IPT_UI_DATS:
inkey_dats();
break;
default:
if (menu_event->itemref)
{
if (menu_event->iptkey == IPT_UI_FAVORITES)
{
// handle UI_FAVORITES
ui_software_info *swinfo = (ui_software_info *)menu_event->itemref;
if ((uintptr_t)swinfo > 2)
{
favorite_manager &mfav = mame_machine_manager::instance()->favorite();
if (!mfav.is_favorite_system_software(*swinfo))
{
mfav.add_favorite_software(*swinfo);
machine().popmessage(_("%s\n added to favorites list."), swinfo->longname);
}
else
{
machine().popmessage(_("%s\n removed from favorites list."), swinfo->longname);
mfav.remove_favorite_software(*swinfo);
}
}
}
}
}
}
// if we're in an error state, overlay an error message
draw_error_text();
}
//-------------------------------------------------
// populate
//-------------------------------------------------
void menu_select_software::populate(float &customtop, float &custombottom)
{
for (auto &icon : m_icons) // TODO: why is this here? maybe better on resize or setting change?
icon.second.texture.reset();
uint32_t flags_ui = FLAG_LEFT_ARROW | FLAG_RIGHT_ARROW;
m_has_empty_start = true;
int old_software = -1;
// FIXME: why does it do this relatively expensive operation every time?
machine_config config(m_driver, machine().options());
for (device_image_interface &image : image_interface_iterator(config.root_device()))
{
if (!image.filename() && image.must_be_loaded())
{
m_has_empty_start = false;
break;
}
}
// start with an empty list
m_displaylist.clear();
filter_map::const_iterator const flt(m_filters.find(m_filter_type));
// no active search
if (m_search.empty())
{
// if the device can be loaded empty, add an item
if (m_has_empty_start)
item_append("[Start empty]", "", flags_ui, (void *)&m_swinfo[0]);
if (m_filters.end() == flt)
std::copy(std::next(m_swinfo.begin()), m_swinfo.end(), std::back_inserter(m_displaylist));
else
flt->second->apply(std::next(m_swinfo.begin()), m_swinfo.end(), std::back_inserter(m_displaylist));
}
else
{
find_matches();
if (m_filters.end() == flt)
{
std::transform(
m_searchlist.begin(),
std::next(m_searchlist.begin(), (std::min)(m_searchlist.size(), MAX_VISIBLE_SEARCH)),
std::back_inserter(m_displaylist),
[] (search_item const &entry) { return entry.software; });
}
else
{
for (auto it = m_searchlist.begin(); (m_searchlist.end() != it) && (MAX_VISIBLE_SEARCH > m_displaylist.size()); ++it)
{
if (flt->second->apply(it->software))
m_displaylist.emplace_back(it->software);
}
}
}
// iterate over entries
for (size_t curitem = 0; curitem < m_displaylist.size(); ++curitem)
{
if (reselect_last::software() == "[Start empty]" && !reselect_last::driver().empty())
old_software = 0;
else if (m_displaylist[curitem].get().shortname == reselect_last::software() && m_displaylist[curitem].get().listname == reselect_last::swlist())
old_software = m_has_empty_start ? curitem + 1 : curitem;
item_append(
m_displaylist[curitem].get().longname, m_displaylist[curitem].get().devicetype,
m_displaylist[curitem].get().parentname.empty() ? flags_ui : (FLAG_INVERT | flags_ui), (void *)&m_displaylist[curitem].get());
}
item_append(menu_item_type::SEPARATOR, flags_ui);
// configure the custom rendering
customtop = 4.0f * ui().get_line_height() + 5.0f * ui().box_tb_border();
custombottom = 5.0f * ui().get_line_height() + 4.0f * ui().box_tb_border();
if (old_software != -1)
{
set_selected_index(old_software);
top_line = selected_index() - (ui_globals::visible_sw_lines / 2);
}
reselect_last::reset();
}
//-------------------------------------------------
// build a list of software
//-------------------------------------------------
void menu_select_software::build_software_list()
{
// add start empty item
m_swinfo.emplace_back(m_driver);
machine_config config(m_driver, machine().options());
// iterate through all software lists
std::vector<std::size_t> orphans;
struct orphan_less
{
std::vector<ui_software_info> &swinfo;
bool operator()(std::string const &a, std::string const &b) const { return a < b; };
bool operator()(std::string const &a, std::size_t b) const { return a < swinfo[b].parentname; };
bool operator()(std::size_t a, std::string const &b) const { return swinfo[a].parentname < b; };
bool operator()(std::size_t a, std::size_t b) const { return swinfo[a].parentname < swinfo[b].parentname; };
};
orphan_less const orphan_cmp{ m_swinfo };
for (software_list_device &swlist : software_list_device_iterator(config.root_device()))
{
m_filter_data.add_list(swlist.list_name(), swlist.description());
check_for_icons(swlist.list_name().c_str());
orphans.clear();
std::map<std::string, std::string> parentnames;
std::map<std::string, std::string>::const_iterator prevparent(parentnames.end());
for (const software_info &swinfo : swlist.get_info())
{
// check for previously-encountered clones
if (swinfo.parentname().empty())
{
if (parentnames.emplace(swinfo.shortname(), swinfo.longname()).second)
{
auto const clones(std::equal_range(orphans.begin(), orphans.end(), swinfo.shortname(), orphan_cmp));
for (auto it = clones.first; clones.second != it; ++it)
m_swinfo[*it].parentlongname = swinfo.longname();
orphans.erase(clones.first, clones.second);
}
else
{
assert([] (auto const x) { return x.first == x.second; } (std::equal_range(orphans.begin(), orphans.end(), swinfo.shortname(), orphan_cmp)));
}
}
const software_part &part = swinfo.parts().front();
if (swlist.is_compatible(part) == SOFTWARE_IS_COMPATIBLE)
{
char const *instance_name(nullptr);
char const *type_name(nullptr);
for (device_image_interface &image : image_interface_iterator(config.root_device()))
{
char const *const interface = image.image_interface();
if (interface && part.matches_interface(interface))
{
instance_name = image.instance_name().c_str();
type_name = image.image_type_name();
break;
}
}
if (instance_name && type_name)
{
// add to collection and try to resolve parent if applicable
auto const ins(m_swinfo.emplace(m_swinfo.end(), swinfo, part, m_driver, swlist.list_name(), instance_name, type_name));
if (!swinfo.parentname().empty())
{
if ((parentnames.end() == prevparent) || (swinfo.parentname() != prevparent->first))
prevparent = parentnames.find(swinfo.parentname());
if (parentnames.end() != prevparent)
{
ins->parentlongname = prevparent->second;
}
else
{
orphans.emplace(
std::upper_bound(orphans.begin(), orphans.end(), swinfo.parentname(), orphan_cmp),
std::distance(m_swinfo.begin(), ins));
}
}
// populate filter choices
m_filter_data.add_region(ins->longname);
m_filter_data.add_publisher(ins->publisher);
m_filter_data.add_year(ins->year);
m_filter_data.add_device_type(ins->devicetype);
}
}
}
}
std::string searchstr, curpath;
for (auto & elem : m_filter_data.list_names())
{
path_iterator path(machine().options().media_path());
while (path.next(curpath))
{
searchstr.assign(curpath).append(PATH_SEPARATOR).append(elem).append(";");
file_enumerator fpath(searchstr.c_str());
// iterate while we get new objects
osd::directory::entry const *dir;
while ((dir = fpath.next()) != nullptr)
{
std::string name;
if (dir->type == osd::directory::entry::entry_type::FILE)
name = core_filename_extract_base(dir->name, true);
else if (dir->type == osd::directory::entry::entry_type::DIR && strcmp(dir->name, ".") != 0)
name = dir->name;
else
continue;
strmakelower(name);
for (auto & yelem : m_swinfo)
if (yelem.shortname == name && yelem.listname == elem)
{
yelem.available = true;
break;
}
}
}
}
// sort array
std::stable_sort(m_swinfo.begin() + 1, m_swinfo.end(), compare_software);
m_filter_data.finalise();
}
//-------------------------------------------------
// handle select key event
//-------------------------------------------------
void menu_select_software::inkey_select(const event *menu_event)
{
ui_software_info *ui_swinfo = (ui_software_info *)menu_event->itemref;
if (ui_swinfo->startempty == 1)
{
if (!select_bios(*ui_swinfo->driver, true))
{
reselect_last::reselect(true);
launch_system(*ui_swinfo->driver, *ui_swinfo);
}
}
else
{
// first validate
driver_enumerator drivlist(machine().options(), *ui_swinfo->driver);
media_auditor auditor(drivlist);
drivlist.next();
software_list_device *swlist = software_list_device::find_by_name(*drivlist.config(), ui_swinfo->listname);
const software_info *swinfo = swlist->find(ui_swinfo->shortname);
media_auditor::summary const summary = auditor.audit_software(*swlist, *swinfo, AUDIT_VALIDATE_FAST);
if (summary == media_auditor::CORRECT || summary == media_auditor::BEST_AVAILABLE || summary == media_auditor::NONE_NEEDED)
{
if (!select_bios(*ui_swinfo, false) && !select_part(*swinfo, *ui_swinfo))
{
reselect_last::reselect(true);
launch_system(drivlist.driver(), *ui_swinfo);
}
}
else
{
// otherwise, display an error
std::ostringstream str;
str << _("The selected software is missing one or more required files. Please select a different software.\n\n");
if (media_auditor::NOTFOUND != summary)
{
auditor.summarize(nullptr, &str);
str << "\n";
}
str << _("Press any key to continue."),
set_error(reset_options::REMEMBER_POSITION, str.str());
}
}
}
//-------------------------------------------------
// load custom filters info from file
//-------------------------------------------------
void menu_select_software::load_sw_custom_filters()
{
// attempt to open the output file
emu_file file(ui().options().ui_path(), OPEN_FLAG_READ);
if (file.open(util::string_format("custom_%s_filter.ini", m_driver.name)) == osd_file::error::NONE)
{
software_filter::ptr flt(software_filter::create(file, m_filter_data));
if (flt)
m_filters.emplace(flt->get_type(), std::move(flt));
file.close();
}
}
//-------------------------------------------------
// find approximate matches
//-------------------------------------------------
void menu_select_software::find_matches()
{
// ensure search list is populated
if (m_searchlist.empty())
{
m_searchlist.reserve(m_swinfo.size());
std::copy(m_swinfo.begin(), m_swinfo.end(), std::back_inserter(m_searchlist));
}
// update search
const std::u32string ucs_search(ustr_from_utf8(normalize_unicode(m_search, unicode_normalization_form::D, true)));
for (search_item &entry : m_searchlist)
entry.set_penalty(ucs_search);
// sort according to edit distance
std::stable_sort(
m_searchlist.begin(),
m_searchlist.end(),
[] (search_item const &lhs, search_item const &rhs) { return lhs.penalty < rhs.penalty; });
}
//-------------------------------------------------
// draw left box
//-------------------------------------------------
float menu_select_software::draw_left_panel(float x1, float y1, float x2, float y2)
{
return menu_select_launch::draw_left_panel<software_filter>(m_filter_type, m_filters, x1, y1, x2, y2);
}
//-------------------------------------------------
// get (possibly cached) icon texture
//-------------------------------------------------
render_texture *menu_select_software::get_icon_texture(int linenum, void *selectedref)
{
ui_software_info const *const swinfo(reinterpret_cast<ui_software_info const *>(selectedref));
assert(swinfo);
if (swinfo->startempty)
return nullptr;
icon_cache::iterator icon(m_icons.find(swinfo));
if ((m_icons.end() == icon) || !icon->second.texture)
{
std::map<std::string, std::string>::iterator paths(m_icon_paths.find(swinfo->listname));
if (m_icon_paths.end() == paths)
paths = m_icon_paths.emplace(swinfo->listname, make_icon_paths(swinfo->listname.c_str())).first;
// allocate an entry or allocate a texture on forced redraw
if (m_icons.end() == icon)
{
icon = m_icons.emplace(swinfo, texture_ptr(machine().render().texture_alloc(), machine().render())).first;
}
else
{
assert(!icon->second.texture);
icon->second.texture.reset(machine().render().texture_alloc());
}
bitmap_argb32 tmp;
emu_file snapfile(std::string(paths->second), OPEN_FLAG_READ);
if (snapfile.open(std::string(swinfo->shortname) + ".ico") == osd_file::error::NONE)
{
render_load_ico_highest_detail(snapfile, tmp);
snapfile.close();
}
if (!tmp.valid() && !swinfo->parentname.empty() && (snapfile.open(std::string(swinfo->parentname) + ".ico") == osd_file::error::NONE))
{
render_load_ico_highest_detail(snapfile, tmp);
snapfile.close();
}
scale_icon(std::move(tmp), icon->second);
}
return icon->second.bitmap.valid() ? icon->second.texture.get() : nullptr;
}
//-------------------------------------------------
// get selected software and/or driver
//-------------------------------------------------
void menu_select_software::get_selection(ui_software_info const *&software, game_driver const *&driver) const
{
software = reinterpret_cast<ui_software_info const *>(get_selection_ptr());
driver = software ? software->driver : nullptr;
}
void menu_select_software::make_topbox_text(std::string &line0, std::string &line1, std::string &line2) const
{
// determine the text for the header
int vis_item = !m_search.empty() ? m_available_items : (m_has_empty_start ? m_available_items - 1 : m_available_items);
line0 = string_format(_("%1$s %2$s ( %3$d / %4$d software packages )"), emulator_info::get_appname(), bare_build_version, vis_item, m_swinfo.size() - 1);
line1 = string_format(_("Driver: \"%1$s\" software list "), m_driver.type.fullname());
filter_map::const_iterator const it(m_filters.find(m_filter_type));
char const *const filter((m_filters.end() != it) ? it->second->filter_text() : nullptr);
if (filter)
line2 = string_format(_("%1$s: %2$s - Search: %3$s_"), it->second->display_name(), filter, m_search);
else
line2 = string_format(_("Search: %1$s_"), m_search);
}
std::string menu_select_software::make_driver_description(game_driver const &driver) const
{
// first line is game description
return string_format(_("%1$-.100s"), driver.type.fullname());
}
std::string menu_select_software::make_software_description(ui_software_info const &software) const
{
// first line is long name
return string_format(_("%1$-.100s"), software.longname);
}
void menu_select_software::filter_selected()
{
if ((software_filter::FIRST <= m_filter_highlight) && (software_filter::LAST >= m_filter_highlight))
{
filter_map::const_iterator it(m_filters.find(software_filter::type(m_filter_highlight)));
if (m_filters.end() == it)
it = m_filters.emplace(software_filter::type(m_filter_highlight), software_filter::create(software_filter::type(m_filter_highlight), m_filter_data)).first;
it->second->show_ui(
ui(),
container(),
[this] (software_filter &filter)
{
software_filter::type const new_type(filter.get_type());
if (software_filter::CUSTOM == new_type)
{
emu_file file(ui().options().ui_path(), OPEN_FLAG_WRITE | OPEN_FLAG_CREATE | OPEN_FLAG_CREATE_PATHS);
if (file.open(util::string_format("custom_%s_filter.ini", m_driver.name)) == osd_file::error::NONE)
{
filter.save_ini(file, 0);
file.close();
}
}
m_filter_type = new_type;
reset(reset_options::SELECT_FIRST);
});
}
}
} // namespace ui