// license:BSD-3-Clause // copyright-holders:Nathan Woods /*************************************************************************** ui/filesel.cpp MAME's clunky built-in file manager TODO - Restrict empty slot if image required ***************************************************************************/ #include "emu.h" #include "ui/filesel.h" #include "ui/ui.h" #include "ui/utils.h" #include "imagedev/floppy.h" #include "uiinput.h" #include "util/corestr.h" #include "util/zippath.h" #include #include namespace ui { /*************************************************************************** CONSTANTS ***************************************************************************/ // conditional compilation to enable chosing of image formats - this is not // yet fully implemented #define ENABLE_FORMATS 0 // time (in seconds) to display errors #define ERROR_MESSAGE_TIME 5 /*************************************************************************** FILE SELECTOR MENU ***************************************************************************/ //------------------------------------------------- // ctor //------------------------------------------------- menu_file_selector::menu_file_selector( mame_ui_manager &mui, render_container &container, device_image_interface *image, std::string_view directory, std::string_view file, bool has_empty, bool has_softlist, bool has_create, handler_function &&handler) : menu(mui, container) , m_handler(std::move(handler)) , m_image(image) , m_current_directory(directory) , m_current_file(file) , m_result(result::INVALID) , m_has_empty(has_empty) , m_has_softlist(has_softlist) , m_has_create(has_create) , m_clicked_directory(std::string::npos, std::string::npos) { (void)m_image; set_process_flags(PROCESS_IGNOREPAUSE); } //------------------------------------------------- // dtor //------------------------------------------------- menu_file_selector::~menu_file_selector() { if (m_handler) m_handler(m_result, std::move(m_current_directory), std::move(m_current_file)); } //------------------------------------------------- // recompute_metrics - recompute metrics //------------------------------------------------- void menu_file_selector::recompute_metrics(uint32_t width, uint32_t height, float aspect) { menu::recompute_metrics(width, height, aspect); m_path_layout.reset(); m_clicked_directory = std::make_pair(std::string::npos, std::string::npos); set_custom_space(line_height() + 3.0F * tb_border(), 0.0F); } //------------------------------------------------- // custom_render - perform our special rendering //------------------------------------------------- void menu_file_selector::custom_render(uint32_t flags, void *selectedref, float top, float bottom, float origx1, float origy1, float origx2, float origy2) { // lay out extra text if (!m_path_layout) { m_path_layout.emplace(create_layout()); m_path_layout->add_text(m_current_directory); } else { rgb_t const fgcolor = ui().colors().text_color(); rgb_t const bgcolor = rgb_t::transparent(); m_path_layout->restyle(0, m_current_directory.length(), &fgcolor, &bgcolor); } // position this extra text float x2, y2; extra_text_position(origx1, origx2, origy1, top, *m_path_layout, -1, m_path_position.first, m_path_position.second, x2, y2); // draw a box ui().draw_outlined_box(container(), m_path_position.first, m_path_position.second, x2, y2, ui().colors().background_color()); // take off the borders m_path_position.first += lr_border(); m_path_position.second += tb_border(); if (m_clicked_directory.second > m_clicked_directory.first) { // see if it's still over the clicked path component auto const [x, y] = pointer_location(); size_t start = 0, span = 0; if (m_path_layout->hit_test(x - m_path_position.first, y - m_path_position.second, start, span)) { if ((start >= m_clicked_directory.first) && ((start + span) <= m_clicked_directory.second)) { rgb_t const fgcolor = ui().colors().selected_color(); rgb_t const bgcolor = ui().colors().selected_bg_color(); m_path_layout->restyle(m_clicked_directory.first, m_clicked_directory.second - m_clicked_directory.first, &fgcolor, &bgcolor); } } } else if (pointer_idle()) { // see if it's hovering over a path component auto const [x, y] = pointer_location(); auto const [target_dir_start, target_dir_end] = get_directory_range(x, y); if (target_dir_end > target_dir_start) { rgb_t const fgcolor = ui().colors().mouseover_color(); rgb_t const bgcolor = ui().colors().mouseover_bg_color(); m_path_layout->restyle(target_dir_start, target_dir_end - target_dir_start, &fgcolor, &bgcolor); } } // draw the text within it m_path_layout->emit(container(), m_path_position.first, m_path_position.second); } //------------------------------------------------- // custom_pointer_updated - perform our special // pointer handling //------------------------------------------------- std::tuple menu_file_selector::custom_pointer_updated(bool changed, ui_event const &uievt) { // track pointer after clicking a path component if (m_clicked_directory.second > m_clicked_directory.first) { if (ui_event::type::POINTER_ABORT == uievt.event_type) { // abort always cancels m_clicked_directory = std::make_pair(std::string::npos, std::string::npos); return std::make_tuple(IPT_INVALID, false, true); } else if (uievt.pointer_released & 0x01) { // releasing the primary button - check for dragging out auto const [x, y] = pointer_location(); size_t start = 0, span = 0; if (m_path_layout->hit_test(x - m_path_position.first, y - m_path_position.second, start, span)) { // abuse IPT_CUSTOM to change to the clicked directory if ((start >= m_clicked_directory.first) && ((start + span) <= m_clicked_directory.second)) return std::make_tuple(IPT_CUSTOM, false, true); } m_clicked_directory = std::make_pair(std::string::npos, std::string::npos); return std::make_tuple(IPT_INVALID, false, true); } else if (uievt.pointer_buttons & ~u32(1)) { // pressing more buttons cancels m_clicked_directory = std::make_pair(std::string::npos, std::string::npos); return std::make_tuple(IPT_INVALID, false, true); } else { // keep tracking the pointer return std::make_tuple(IPT_INVALID, true, false); } } // check for clicks if we have up-to-date content on-screen if (m_path_layout && pointer_idle() && (uievt.pointer_buttons & 0x01) && !(uievt.pointer_buttons & ~u32(0x01))) { auto const [x, y] = pointer_location(); auto const [target_dir_start, target_dir_end] = get_directory_range(x, y); if (target_dir_end > target_dir_start) { m_clicked_directory = std::make_pair(target_dir_start, target_dir_end); return std::make_tuple(IPT_INVALID, true, true); } } return std::make_tuple(IPT_INVALID, false, false); } //------------------------------------------------- // menu_activated - menu has gained focus //------------------------------------------------- void menu_file_selector::menu_activated() { m_clicked_directory = std::make_pair(std::string::npos, std::string::npos); } //------------------------------------------------- // append_entry - appends a new // file selector entry to an entry list //------------------------------------------------- menu_file_selector::file_selector_entry &menu_file_selector::append_entry( file_selector_entry_type entry_type, const std::string &entry_basename, const std::string &entry_fullpath) { return append_entry(entry_type, std::string(entry_basename), std::string(entry_fullpath)); } //------------------------------------------------- // append_entry - appends a new // file selector entry to an entry list //------------------------------------------------- menu_file_selector::file_selector_entry &menu_file_selector::append_entry( file_selector_entry_type entry_type, std::string &&entry_basename, std::string &&entry_fullpath) { // allocate a new entry file_selector_entry entry; entry.type = entry_type; entry.basename = std::move(entry_basename); entry.fullpath = std::move(entry_fullpath); // find the end of the list return m_entrylist.emplace_back(std::move(entry)); } //------------------------------------------------- // append_dirent_entry - appends // a menu item for a file selector entry //------------------------------------------------- menu_file_selector::file_selector_entry *menu_file_selector::append_dirent_entry(const osd::directory::entry *dirent) { file_selector_entry_type entry_type; switch (dirent->type) { case osd::directory::entry::entry_type::FILE: entry_type = SELECTOR_ENTRY_TYPE_FILE; break; case osd::directory::entry::entry_type::DIR: entry_type = SELECTOR_ENTRY_TYPE_DIRECTORY; break; default: // exceptional case; do not add a menu item return nullptr; } // determine the full path std::string buffer = util::zippath_combine(m_current_directory, dirent->name); // create the file selector entry return &append_entry( entry_type, dirent->name, std::move(buffer)); } //------------------------------------------------- // append_entry_menu_item - appends // a menu item for a file selector entry //------------------------------------------------- void menu_file_selector::append_entry_menu_item(const file_selector_entry *entry) { std::string text; std::string subtext; switch(entry->type) { case SELECTOR_ENTRY_TYPE_EMPTY: text = _("[empty slot]"); break; case SELECTOR_ENTRY_TYPE_CREATE: text = _("[create]"); break; case SELECTOR_ENTRY_TYPE_SOFTWARE_LIST: text = _("[software list]"); break; case SELECTOR_ENTRY_TYPE_DRIVE: text = entry->basename; subtext = "[DRIVE]"; break; case SELECTOR_ENTRY_TYPE_DIRECTORY: text = entry->basename; subtext = "[DIR]"; break; case SELECTOR_ENTRY_TYPE_FILE: text = entry->basename; subtext = "[FILE]"; break; } item_append(std::move(text), std::move(subtext), 0, (void *) entry); } //------------------------------------------------- // select_item //------------------------------------------------- void menu_file_selector::select_item(const file_selector_entry &entry) { switch (entry.type) { case SELECTOR_ENTRY_TYPE_EMPTY: // empty slot - unload m_result = result::EMPTY; stack_pop(); break; case SELECTOR_ENTRY_TYPE_CREATE: // create m_result = result::CREATE; stack_pop(); break; case SELECTOR_ENTRY_TYPE_SOFTWARE_LIST: m_result = result::SOFTLIST; stack_pop(); break; case SELECTOR_ENTRY_TYPE_DRIVE: case SELECTOR_ENTRY_TYPE_DIRECTORY: { // drive/directory - first check the path util::zippath_directory::ptr dir; std::error_condition const err = util::zippath_directory::open(entry.fullpath, dir); if (err) { // this path is problematic; present the user with an error and bail ui().popup_time(1, _("Error accessing %s"), entry.fullpath); break; } } m_current_directory = entry.fullpath; m_path_layout.reset(); m_clicked_directory = std::make_pair(std::string::npos, std::string::npos); reset(reset_options::SELECT_FIRST); break; case SELECTOR_ENTRY_TYPE_FILE: // file m_current_file.assign(entry.fullpath); m_result = result::FILE; stack_pop(); break; } } //------------------------------------------------- // update_search //------------------------------------------------- void menu_file_selector::update_search() { ui().popup_time(ERROR_MESSAGE_TIME, "%s", m_filename); file_selector_entry const *const cur_selected(reinterpret_cast(get_selection_ref())); // if it's a perfect match for the current selection, don't move it if (!cur_selected || core_strnicmp(cur_selected->basename.c_str(), m_filename.c_str(), m_filename.size())) { std::string::size_type bestmatch(0); file_selector_entry const *selected_entry(cur_selected); for (auto &entry : m_entrylist) { // TODO: more efficient "common prefix" code std::string::size_type match(0); for (std::string::size_type i = 1; m_filename.size() >= i; ++i) { if (!core_strnicmp(entry.basename.c_str(), m_filename.c_str(), i)) match = i; else break; } if (match > bestmatch) { bestmatch = match; selected_entry = &entry; } } if (selected_entry && (selected_entry != cur_selected)) { set_selection((void *)selected_entry); centre_selection(); } } } //------------------------------------------------- // get_directory_range //------------------------------------------------- std::pair menu_file_selector::get_directory_range(float x, float y) { size_t start = 0, span = 0; if (m_path_layout->hit_test(x - m_path_position.first, y - m_path_position.second, start, span)) { if (std::string_view(m_current_directory).substr(start, span) != PATH_SEPARATOR) { auto target_start = m_current_directory.rfind(PATH_SEPARATOR, start); if (std::string::npos == target_start) target_start = 0; else target_start += 1; auto target_end = m_current_directory.find(PATH_SEPARATOR, start + span); if (std::string::npos == target_end) target_end = m_current_directory.length(); return std::make_pair(target_start, target_end); } } return std::make_pair(std::string::npos, std::string::npos); } //------------------------------------------------- // populate //------------------------------------------------- void menu_file_selector::populate() { const file_selector_entry *selected_entry = nullptr; // clear out the menu entries m_entrylist.clear(); // open the directory util::zippath_directory::ptr directory; std::error_condition const err = util::zippath_directory::open(m_current_directory, directory); // add the "[empty slot]" entry if available if (m_has_empty) append_entry(SELECTOR_ENTRY_TYPE_EMPTY, "", ""); // add the "[create]" entry if (m_has_create && directory && !directory->is_archive()) append_entry(SELECTOR_ENTRY_TYPE_CREATE, "", ""); // add and select the "[software list]" entry if available if (m_has_softlist) selected_entry = &append_entry(SELECTOR_ENTRY_TYPE_SOFTWARE_LIST, "", ""); // add the drives for (std::string const &volume_name : osd_get_volume_names()) append_entry(SELECTOR_ENTRY_TYPE_DRIVE, volume_name, volume_name); // mark first filename entry std::size_t const first = m_entrylist.size() + 1; // build the menu for each item if (err) { osd_printf_verbose( "menu_file_selector::populate: error opening directory '%s' (%s:%d %s)\n", m_current_directory, err.category().name(), err.value(), err.message()); } else { for (osd::directory::entry const *dirent = directory->readdir(); dirent; dirent = directory->readdir()) { // append a dirent entry file_selector_entry const *entry = append_dirent_entry(dirent); if (entry) { // set the selected item to be the first non-parent directory or file if (!selected_entry && strcmp(dirent->name, "..")) selected_entry = entry; // do we have to select this file? if (!core_stricmp(m_current_file, dirent->name)) selected_entry = entry; } } } directory.reset(); if (m_entrylist.size() > first) { // sort the menu entries std::locale const lcl; std::collate const &coll = std::use_facet >(lcl); std::sort( m_entrylist.begin() + first, m_entrylist.end(), [&coll] (file_selector_entry const &x, file_selector_entry const &y) { std::wstring const xstr = wstring_from_utf8(x.basename); std::wstring const ystr = wstring_from_utf8(y.basename); return coll.compare(xstr.data(), xstr.data() + xstr.size(), ystr.data(), ystr.data() + ystr.size()) < 0; }); } // append all of the menu entries for (file_selector_entry const &entry : m_entrylist) append_entry_menu_item(&entry); // set the selection (if we have one) if (selected_entry) set_selection((void *)selected_entry); } //------------------------------------------------- // handle //------------------------------------------------- bool menu_file_selector::handle(event const *ev) { if (!ev) return false; if (ev->iptkey == IPT_SPECIAL) { // if it's any other key and we're not maxed out, update if (input_character(m_filename, ev->unichar, uchar_is_printable)) { update_search(); return true; } } else if (ev->iptkey == IPT_UI_PASTE) { if (paste_text(m_filename, uchar_is_printable)) { update_search(); return true; } } else if (ev->iptkey == IPT_UI_CANCEL) { // reset the char buffer also in this case if (!m_filename.empty()) { m_filename.clear(); ui().popup_time(ERROR_MESSAGE_TIME, "%s", m_filename); return true; } } else if (ev->iptkey == IPT_CUSTOM) { // clicked a path component if (m_clicked_directory.second > m_clicked_directory.first) { m_current_directory.resize(m_clicked_directory.second + strlen(PATH_SEPARATOR)); m_path_layout.reset(); m_clicked_directory = std::make_pair(std::string::npos, std::string::npos); reset(reset_options::SELECT_FIRST); return true; } } else if (ev->itemref && (ev->iptkey == IPT_UI_SELECT)) { // handle selections select_item(*reinterpret_cast(ev->itemref)); // reset the char buffer when pressing IPT_UI_SELECT m_filename.clear(); return true; } return false; } /*************************************************************************** SELECT RW ***************************************************************************/ //------------------------------------------------- // ctor //------------------------------------------------- menu_select_rw::menu_select_rw( mame_ui_manager &mui, render_container &container, bool can_in_place, handler_function &&handler) : menu(mui, container) , m_handler(std::move(handler)) , m_can_in_place(can_in_place) , m_result(result::INVALID) { } //------------------------------------------------- // dtor //------------------------------------------------- menu_select_rw::~menu_select_rw() { if (m_handler) m_handler(m_result); } //------------------------------------------------- // populate //------------------------------------------------- void menu_select_rw::populate() { set_heading(_("Select access mode")); item_append(_("Read-only"), 0, itemref_from_result(result::READONLY)); if (m_can_in_place) item_append(_("Read-write"), 0, itemref_from_result(result::READWRITE)); item_append(_("Read this image, write to another image"), 0, itemref_from_result(result::WRITE_OTHER)); item_append(_("Read this image, write to diff"), 0, itemref_from_result(result::WRITE_DIFF)); } //------------------------------------------------- // handle //------------------------------------------------- bool menu_select_rw::handle(event const *ev) { if (ev && ev->iptkey == IPT_UI_SELECT) { m_result = result_from_itemref(ev->itemref); stack_pop(); } return false; } //------------------------------------------------- // itemref_from_result //------------------------------------------------- void *menu_select_rw::itemref_from_result(menu_select_rw::result result) { return (void *)(uintptr_t)(unsigned int)result; } //------------------------------------------------- // result_from_itemref //------------------------------------------------- menu_select_rw::result menu_select_rw::result_from_itemref(void *itemref) { return (menu_select_rw::result) (unsigned int) (uintptr_t)itemref; } } // namespace ui