Plugin usability improvements:

* autofire, inputmacro: Made left/right repeat when held (makes setting
  long delays/durations easier).
* autofire, inputmacro: Added headings for devices in input selection
  menus (helps when controller buttons have identical names, e.g. AES)
* autofire: Made intial selection when moving between menus intuitive,
  log some errors on saving/loading configuration.
* autofire: Fixed two errors in Chinese localisations.
This commit is contained in:
Vas Crabb 2021-10-22 01:14:05 +11:00
parent 9c61152a20
commit f459eb6e13
10 changed files with 281 additions and 103 deletions

View File

@ -6697,7 +6697,7 @@ msgstr "总计发现 %d 符合"
#: plugins/cheatfind/init.lua:623
#, lua-format
msgid "Perform Compare : Slot %d %s Slot %d"
msgstr "执行比较:插槽 $d %s 插槽 %d"
msgstr "执行比较:插槽 %d %s 插槽 %d"
#: plugins/cheatfind/init.lua:624
#, lua-format
@ -6886,7 +6886,7 @@ msgstr "连发按钮"
#: plugins/autofire/autofire_menu.lua:59
#, lua-format
msgid "Press %s to delete"
msgstr "按下 $s 删除"
msgstr "按下 %s 删除"
#: plugins/autofire/autofire_menu.lua:68
#, lua-format

View File

@ -6697,7 +6697,7 @@ msgstr "總計發現 %d 符合"
#: plugins/cheatfind/init.lua:623
#, lua-format
msgid "Perform Compare : Slot %d %s Slot %d"
msgstr "執行比較:插槽 $d %s 插槽 %d"
msgstr "執行比較:插槽 %d %s 插槽 %d"
#: plugins/cheatfind/init.lua:624
#, lua-format
@ -6886,7 +6886,7 @@ msgstr "連射按鈕"
#: plugins/autofire/autofire_menu.lua:59
#, lua-format
msgid "Press %s to delete"
msgstr "按下 $s 刪除"
msgstr "按下 %s 刪除"
#: plugins/autofire/autofire_menu.lua:68
#, lua-format

View File

@ -15,11 +15,26 @@ local content_height = 0
-- Stack of menus (see MENU_TYPES)
local menu_stack = { MENU_TYPES.MAIN }
-- Button to select when showing the main menu (so newly added button can be selected)
local initial_button
-- Saved selection on main menu (to restore after configure menu is dismissed)
local main_selection_save
-- Whether configure menu is active (so first item can be selected initially)
local configure_menu_active = false
-- Saved selection on configure menu (to restore after button menu is dismissed)
local configure_selection_save
-- Button being created/edited
local current_button = {}
-- Initial button to select when opening buttons menu
local initial_input
-- Inputs that can be autofired (to list in BUTTON menu)
local inputs = {}
local inputs
-- Returns the section (from MENU_SECTIONS) and the index within that section
local function menu_section(index)
@ -60,35 +75,54 @@ local function populate_main_menu(buttons)
menu[#menu + 1] = {'---', '', ''}
header_height = #menu
-- Use frame rate of first screen or 60Hz if no screens
local freq = 60
local screen = manager.machine.screens:at(1)
if screen then
freq = 1 / screen.frame_period
end
for index, button in ipairs(buttons) do
-- Assume refresh rate of 60 Hz; maybe better to use screen_device refresh()?
local rate = 60 / (button.on_frames + button.off_frames)
-- Round to two decimal places
-- Round rate to two decimal places
local rate = freq / (button.on_frames + button.off_frames)
rate = math.floor(rate * 100) / 100
local text = string.format(_('%s [%d Hz]'), _p('input-name', button.button.name), rate)
local text = string.format(_('%s [%g Hz]'), _p('input-name', button.button.name), rate)
local subtext = input:seq_name(button.key)
menu[#menu + 1] = {text, subtext, ''}
if index == initial_button then
main_selection_save = #menu
end
end
initial_button = nil
content_height = #menu
menu[#menu + 1] = {'---', '', ''}
menu[#menu + 1] = {_('Add autofire button'), '', ''}
return menu
local selection = main_selection_save
main_selection_save = nil
return menu, selection
end
local function handle_main_menu(index, event, buttons)
local section, adjusted_index = menu_section(index)
if section == MENU_SECTIONS.CONTENT then
if event == 'select' then
main_selection_save = index
current_button = buttons[adjusted_index]
table.insert(menu_stack, MENU_TYPES.EDIT)
return true
elseif event == 'clear' then
table.remove(buttons, adjusted_index)
main_selection_save = index
if adjusted_index > #buttons then
main_selection_save = main_selection_save - 1
end
return true
end
elseif section == MENU_SECTIONS.FOOTER then
if event == 'select' then
main_selection_save = index
current_button = create_new_button()
table.insert(menu_stack, MENU_TYPES.ADD)
return true
@ -103,9 +137,13 @@ local function populate_configure_menu(menu)
local button_name = current_button.button and _p('input-name', current_button.button.name) or _('NOT SET')
local key_name = current_button.key and manager.machine.input:seq_name(current_button.key) or _('NOT SET')
menu[#menu + 1] = {_('Input'), button_name, ''}
if not (configure_menu_active or configure_selection_save) then
configure_selection_save = #menu
end
menu[#menu + 1] = {_('Hotkey'), key_name, ''}
menu[#menu + 1] = {_('On frames'), current_button.on_frames, current_button.on_frames > 1 and 'lr' or 'r'}
menu[#menu + 1] = {_('Off frames'), current_button.off_frames, current_button.off_frames > 1 and 'lr' or 'r'}
configure_menu_active = true
end
-- Borrowed from the cheat plugin
@ -138,7 +176,11 @@ local function handle_configure_menu(index, event)
if index == 1 then
-- Input
if event == 'select' then
configure_selection_save = header_height + index
table.insert(menu_stack, MENU_TYPES.BUTTON)
if current_button.port and current_button.field then
initial_input = {port_name = current_button.port, ioport_field = current_button.button}
end
return true
end
elseif index == 2 then
@ -159,6 +201,9 @@ local function handle_configure_menu(index, event)
elseif event == 'right' then
current_button.on_frames = current_button.on_frames + 1
return true
elseif event == 'clear' then
current_button.on_frames = 1
return true
end
elseif index == 4 then
-- Off frames
@ -169,6 +214,9 @@ local function handle_configure_menu(index, event)
elseif event == 'right' then
current_button.off_frames = current_button.off_frames + 1
return true
elseif event == 'clear' then
current_button.off_frames = 1
return true
end
end
return false
@ -185,12 +233,17 @@ local function populate_edit_menu()
menu[#menu + 1] = {'---', '', ''}
menu[#menu + 1] = {_('Done'), '', ''}
return menu
local selection = configure_selection_save
configure_selection_save = nil
return menu, selection, 'lrrepeat'
end
local function handle_edit_menu(index, event, buttons)
local section, adjusted_index = menu_section(index)
if ((section == MENU_SECTIONS.FOOTER) and (event == 'select')) or (event == 'cancel') then
inputs = nil
configure_menu_active = false
table.remove(menu_stack)
return true
elseif section == MENU_SECTIONS.CONTENT then
@ -214,15 +267,21 @@ local function populate_add_menu()
else
menu[#menu + 1] = {_('Cancel'), '', ''}
end
return menu
local selection = configure_selection_save
configure_selection_save = nil
return menu, selection, 'lrrepeat'
end
local function handle_add_menu(index, event, buttons)
local section, adjusted_index = menu_section(index)
if ((section == MENU_SECTIONS.FOOTER) and (event == 'select')) or (event == 'cancel') then
inputs = nil
configure_menu_active = false
table.remove(menu_stack)
if is_button_complete(current_button) and (event == 'select') then
buttons[#buttons + 1] = current_button
table.insert(buttons, current_button)
initial_button = #buttons
end
return true
elseif section == MENU_SECTIONS.CONTENT then
@ -236,54 +295,85 @@ end
local function populate_button_menu()
local ioport = manager.machine.ioport
menu = {}
inputs = {}
menu[#menu + 1] = {_('Select an input for autofire'), '', 'off'}
menu[#menu + 1] = {'---', '', ''}
header_height = #menu
for port_key, port in pairs(ioport.ports) do
for field_key, field in pairs(port.fields) do
if is_supported_input(field) then
inputs[#inputs + 1] = {
port_name = port_key,
field_name = field_key,
ioport_field = field
}
if not inputs then
inputs = {}
for port_key, port in pairs(ioport.ports) do
for field_key, field in pairs(port.fields) do
if is_supported_input(field) then
inputs[#inputs + 1] = {
port_name = port_key,
field_name = field_key,
ioport_field = field
}
end
end
end
local function compare(x, y)
if x.ioport_field.device.tag < y.ioport_field.device.tag then
return true
elseif x.ioport_field.device.tag > y.ioport_field.device.tag then
return false
end
groupx = ioport:type_group(x.ioport_field.type, x.ioport_field.player)
groupy = ioport:type_group(y.ioport_field.type, y.ioport_field.player)
if groupx < groupy then
return true
elseif groupx > groupy then
return false
elseif x.ioport_field.type < y.ioport_field.type then
return true
elseif x.ioport_field.type > y.ioport_field.type then
return false
else
return x.ioport_field.name < y.ioport_field.name
end
end
table.sort(inputs, compare)
local i = 1
local prev
while i <= #inputs do
local current = inputs[i]
if (not prev) or (prev.ioport_field.device.tag ~= current.ioport_field.device.tag) then
table.insert(inputs, i, false)
i = i + 2
else
i = i + 1
end
prev = current
end
end
local selection = header_height + 1
for i, input in ipairs(inputs) do
if input then
menu[header_height + i] = { _p('input-name', input.ioport_field.name), '', '' }
if initial_input and (initial_input.port_name == input.port_name) and (initial_input.ioport_field.mask == input.ioport_field.mask) and (initial_input.ioport_field.type == input.ioport_field.type) then
selection = header_height + i
initial_input = nil
end
else
local device = inputs[i + 1].ioport_field.device
if device.owner then
menu[header_height + i] = {string.format(_('%s [root%s]'), device.name, device.tag), '', 'heading'}
else
menu[header_height + i] = {string.format(_('[root%s]'), device.tag), '', 'heading'}
end
end
end
local function compare(x, y)
if x.ioport_field.device.tag < y.ioport_field.device.tag then
return true
elseif x.ioport_field.device.tag > y.ioport_field.device.tag then
return false
end
groupx = ioport:type_group(x.ioport_field.type, x.ioport_field.player)
groupy = ioport:type_group(y.ioport_field.type, y.ioport_field.player)
if groupx < groupy then
return true
elseif groupx > groupy then
return false
elseif x.ioport_field.type < y.ioport_field.type then
return true
elseif x.ioport_field.type > y.ioport_field.type then
return false
else
return x.ioport_field.name < y.ioport_field.name
end
end
table.sort(inputs, compare)
for i, input in pairs(inputs) do
menu[header_height + i] = { _p('input-name', input.ioport_field.name), '', '' }
end
content_height = #menu
initial_input = nil
menu[#menu + 1] = {'---', '', ''}
menu[#menu + 1] = {_('Cancel'), '', ''}
return menu
return menu, selection
end
local function handle_button_menu(index, event)
@ -307,7 +397,7 @@ function lib:init_menu(buttons)
content_height = 0
menu_stack = { MENU_TYPES.MAIN }
current_button = {}
inputs = {}
inputs = nil
end
function lib:populate_menu(buttons)

View File

@ -50,13 +50,15 @@ end
function lib:load_settings()
local buttons = {}
local json = require('json')
local file = io.open(get_settings_path() .. get_settings_filename(), 'r')
local filename = get_settings_path() .. get_settings_filename()
local file = io.open(filename, 'r')
if not file then
return buttons
end
local loaded_settings = json.parse(file:read('a'))
file:close()
if not loaded_settings then
emu.print_error(string.format('Error loading autofire settings: error parsing file "%s" as JSON\n', filename))
return buttons
end
for index, button_settings in ipairs(loaded_settings) do
@ -74,6 +76,7 @@ function lib:save_settings(buttons)
if not attr then
lfs.mkdir(path)
elseif attr.mode ~= 'directory' then
emu.print_error(string.format('Error autofire settings macros: "%s" is not a directory\n', path))
return
end
if #buttons == 0 then
@ -83,11 +86,14 @@ function lib:save_settings(buttons)
local json = require('json')
local settings = serialize_settings(buttons)
local data = json.stringify(settings, {indent = true})
local file = io.open(path .. get_settings_filename(), 'w')
if file then
file:write(data)
file:close()
local filename = path .. get_settings_filename()
local file = io.open(filename, 'w')
if not file then
emu.print_error(string.format('Error saving autofire settings: error opening file "%s" for writing\n', filename))
return
end
file:write(data)
file:close()
end
return lib

View File

@ -114,20 +114,42 @@ function populate_input()
for tag, port in pairs(manager.machine.ioport.ports) do
for name, field in pairs(port.fields) do
if supported(field) then
input_choices[#input_choices + 1] = field
table.insert(input_choices, field)
end
end
end
table.sort(input_choices, compare)
local index = 1
local prev
while index <= #input_choices do
local current = input_choices[index]
if (not prev) or (prev.device.tag ~= current.device.tag) then
table.insert(input_choices, index, false)
index = index + 2
else
index = index + 1
end
prev = current
end
end
input_item_first_choice = #items + 1
local selection = input_item_first_choice
for index, field in ipairs(input_choices) do
items[#items + 1] = { _p('input-name', field.name), '', '' }
if input_start_field and (field.port.tag == input_start_field.port.tag) and (field.mask == input_start_field.mask) and (field.type == input_start_field.type) then
selection = #items
input_start_field = nil
if field then
items[#items + 1] = { _p('input-name', field.name), '', '' }
if input_start_field and (field.port.tag == input_start_field.port.tag) and (field.mask == input_start_field.mask) and (field.type == input_start_field.type) then
selection = #items
input_start_field = nil
end
else
local device = input_choices[index + 1].device
if device.owner then
items[#items + 1] = { string.format(_('plugin-inputmacro', '%s [root%s]'), device.name, device.tag), '', 'heading' }
else
items[#items + 1] = { string.format(_('plugin-inputmacro', '[root%s]'), device.tag), '', 'heading' }
end
end
end
input_start_field = nil
@ -145,6 +167,7 @@ end
local edit_current_macro
local edit_start_selection
local edit_start_step
local edit_menu_active
local edit_insert_position
local edit_name_buffer
local edit_items
@ -379,6 +402,10 @@ local function add_edit_items(items)
items[#items + 1] = { _p('plugin-inputmacro', 'Name'), edit_name_buffer and (edit_name_buffer .. '_') or edit_current_macro.name, '' }
edit_items[#items] = { action = 'name' }
if not (edit_start_selection or edit_start_step or edit_menu_active) then
edit_start_selection = #items
end
edit_menu_active = true
local binding = edit_current_macro.binding
local activation = binding and input:seq_name(binding) or _p('plugin-inputmacro', '[not set]')
@ -453,7 +480,9 @@ local function handle_add(index, event)
if handle_edit_items(index, event) then
return true
elseif event == 'cancel' then
input_choices = nil
edit_current_macro = nil
edit_menu_active = false
edit_items = nil
table.remove(menu_stack)
return true
@ -462,6 +491,8 @@ local function handle_add(index, event)
table.insert(macros, edit_current_macro)
macros_start_macro = #macros
end
input_choices = nil
edit_menu_active = false
edit_current_macro = nil
edit_items = nil
table.remove(menu_stack)
@ -474,7 +505,9 @@ local function handle_edit(index, event)
if handle_edit_items(index, event) then
return true
elseif (event == 'cancel') or ((index == edit_item_exit) and (event == 'select')) then
input_choices = nil
edit_current_macro = nil
edit_menu_active = false
edit_items = nil
table.remove(menu_stack)
return true
@ -500,7 +533,7 @@ local function populate_add()
local selection = edit_start_selection
edit_start_selection = nil
return items, selection
return items, selection, 'lrrepeat'
end
local function populate_edit()
@ -517,7 +550,7 @@ local function populate_edit()
local selection = edit_start_selection
edit_start_selection = nil
return items, selection
return items, selection, 'lrrepeat'
end
@ -532,7 +565,6 @@ function handle_macros(index, event)
if event == 'select' then
edit_current_macro = new_macro()
edit_insert_position = #edit_current_macro.steps + 1
edit_start_selection = 1 -- not actually selectable, but it will take the first item
macros_selection_save = index
table.insert(menu_stack, MENU_TYPES.ADD)
return true

View File

@ -432,7 +432,7 @@ sol::object lua_engine::call_plugin(const std::string &name, sol::object in)
return sol::lua_nil;
}
std::optional<long> lua_engine::menu_populate(const std::string &menu, std::vector<std::tuple<std::string, std::string, std::string>> &menu_list)
std::optional<long> lua_engine::menu_populate(const std::string &menu, std::vector<std::tuple<std::string, std::string, std::string> > &menu_list, std::string &flags)
{
std::string field = "menu_pop_" + menu;
sol::object obj = sol().registry()[field];
@ -446,7 +446,7 @@ std::optional<long> lua_engine::menu_populate(const std::string &menu, std::vect
}
else
{
std::tuple<sol::table, std::optional<long> > table = res;
std::tuple<sol::table, std::optional<long>, std::string> table = res;
for (auto &entry : std::get<0>(table))
{
if (entry.second.is<sol::table>())
@ -455,27 +455,31 @@ std::optional<long> lua_engine::menu_populate(const std::string &menu, std::vect
menu_list.emplace_back(enttable.get<std::string, std::string, std::string>(1, 2, 3));
}
}
flags = std::get<2>(table);
return std::get<1>(table);
}
}
flags.clear();
return std::nullopt;
}
bool lua_engine::menu_callback(const std::string &menu, int index, const std::string &event)
std::pair<bool, std::optional<long> > lua_engine::menu_callback(const std::string &menu, int index, const std::string &event)
{
std::string field = "menu_cb_" + menu;
bool ret = false;
std::pair<bool, std::optional<long> > ret(false, std::nullopt);
sol::object obj = sol().registry()[field];
if(obj.is<sol::protected_function>())
if (obj.is<sol::protected_function>())
{
auto res = invoke(obj.as<sol::protected_function>(), index, event);
if(!res.valid())
if (!res.valid())
{
sol::error err = res;
osd_printf_error("[LUA ERROR] in menu_callback: %s\n", err.what());
}
else
{
ret = res;
}
}
return ret;
}

View File

@ -48,8 +48,8 @@ public:
bool frame_hook();
std::optional<long> menu_populate(const std::string &menu, std::vector<std::tuple<std::string, std::string, std::string>> &menu_list);
bool menu_callback(const std::string &menu, int index, const std::string &event);
std::optional<long> menu_populate(const std::string &menu, std::vector<std::tuple<std::string, std::string, std::string> > &menu_list, std::string &flags);
std::pair<bool, std::optional<long> > menu_callback(const std::string &menu, int index, const std::string &event);
void set_machine(running_machine *machine);
std::vector<std::string> &get_menu() { return m_menu; }

View File

@ -37,7 +37,7 @@ class menu
{
public:
// flags for menu items
enum : unsigned
enum : uint32_t
{
FLAG_LEFT_ARROW = 1U << 0,
FLAG_RIGHT_ARROW = 1U << 1,

View File

@ -62,15 +62,17 @@ menu_plugin::~menu_plugin()
{
}
menu_plugin_opt::menu_plugin_opt(mame_ui_manager &mui, render_container &container, char *menu) :
ui::menu(mui, container),
m_menu(menu)
menu_plugin_opt::menu_plugin_opt(mame_ui_manager &mui, render_container &container, std::string_view menu) :
ui::menu(mui, container),
m_menu(menu),
m_process_flags(0U),
m_need_idle(false)
{
}
void menu_plugin_opt::handle()
{
const event *menu_event = process(0);
const event *menu_event = process(m_process_flags);
if (menu_event)
{
@ -105,9 +107,13 @@ void menu_plugin_opt::handle()
key = std::to_string((u32)menu_event->unichar);
break;
default:
return;
if (!m_need_idle)
return;
}
if (mame_machine_manager::instance()->lua()->menu_callback(m_menu, uintptr_t(menu_event->itemref), key))
auto const result = mame_machine_manager::instance()->lua()->menu_callback(m_menu, uintptr_t(menu_event->itemref), key);
if (result.second)
set_selection(reinterpret_cast<void *>(uintptr_t(*result.second)));
if (result.first)
reset(reset_options::REMEMBER_REF);
else if (menu_event->iptkey == IPT_UI_CANCEL)
stack_pop();
@ -117,41 +123,78 @@ void menu_plugin_opt::handle()
void menu_plugin_opt::populate(float &customtop, float &custombottom)
{
std::vector<std::tuple<std::string, std::string, std::string>> menu_list;
auto const sel = mame_machine_manager::instance()->lua()->menu_populate(m_menu, menu_list);
std::string flags;
auto const sel = mame_machine_manager::instance()->lua()->menu_populate(m_menu, menu_list, flags);
uintptr_t i = 1;
for(auto &item : menu_list)
for (auto &item : menu_list)
{
const std::string &text = std::get<0>(item);
const std::string &subtext = std::get<1>(item);
const std::string &tflags = std::get<2>(item);
std::string &text = std::get<0>(item);
std::string &subtext = std::get<1>(item);
std::string_view tflags = std::get<2>(item);
uint32_t flags = 0;
if (tflags == "off")
flags = FLAG_DISABLE;
else if (tflags == "heading")
flags = FLAG_DISABLE | FLAG_UI_HEADING;
else if (tflags == "l")
flags = FLAG_LEFT_ARROW;
else if (tflags == "r")
flags = FLAG_RIGHT_ARROW;
else if (tflags == "lr")
flags = FLAG_RIGHT_ARROW | FLAG_LEFT_ARROW;
if(text == "---")
uint32_t item_flags_or = uint32_t(0);
uint32_t item_flags_and = ~uint32_t(0);
auto flag_start = tflags.find_first_not_of(' ');
while (std::string_view::npos != flag_start)
{
tflags.remove_prefix(flag_start);
auto const flag_end = tflags.find(' ');
auto const flag = tflags.substr(0, flag_end);
tflags.remove_prefix(flag.length());
flag_start = tflags.find_first_not_of(' ');
if (flag == "off")
item_flags_or |= FLAG_DISABLE;
else if (flag == "on")
item_flags_and &= ~FLAG_DISABLE;
else if (flag == "l")
item_flags_or |= FLAG_LEFT_ARROW;
else if (flag == "r")
item_flags_or |= FLAG_RIGHT_ARROW;
else if (flag == "lr")
item_flags_or |= FLAG_RIGHT_ARROW | FLAG_LEFT_ARROW;
else if (flag == "invert")
item_flags_or |= FLAG_INVERT;
else if (flag == "heading")
item_flags_or |= FLAG_DISABLE | FLAG_UI_HEADING;
}
if (text == "---")
item_append(menu_item_type::SEPARATOR);
i++;
}
else
{
item_append(text, subtext, flags, reinterpret_cast<void *>(i++));
}
item_append(std::move(text), std::move(subtext), item_flags_or & item_flags_and, reinterpret_cast<void *>(i));
++i;
}
item_append(menu_item_type::SEPARATOR);
if (sel)
set_selection(reinterpret_cast<void *>(uintptr_t(*sel)));
m_process_flags = 0U;
m_need_idle = false;
if (!flags.empty())
{
std::string_view mflags = flags;
auto flag_start = mflags.find_first_not_of(' ');
while (std::string_view::npos != flag_start)
{
mflags.remove_prefix(flag_start);
auto const flag_end = mflags.find(' ');
auto const flag = mflags.substr(0, flag_end);
mflags.remove_prefix(flag.length());
flag_start = mflags.find_first_not_of(' ');
if (flag == "lralways")
m_process_flags |= PROCESS_LR_ALWAYS;
else if (flag == "lrrepeat")
m_process_flags |= PROCESS_LR_REPEAT;
else if (flag == "customnav")
m_process_flags |= PROCESS_CUSTOM_NAV;
else if (flag == "idle")
m_need_idle = true;
}
}
}
menu_plugin_opt::~menu_plugin_opt()

View File

@ -16,6 +16,7 @@
#include "ui/ui.h"
#include <string>
#include <string_view>
#include <vector>
@ -40,7 +41,7 @@ private:
class menu_plugin_opt : public menu
{
public:
menu_plugin_opt(mame_ui_manager &mui, render_container &container, char *menu);
menu_plugin_opt(mame_ui_manager &mui, render_container &container, std::string_view menu);
virtual ~menu_plugin_opt();
protected:
@ -50,7 +51,9 @@ private:
virtual void populate(float &customtop, float &custombottom) override;
virtual void handle() override;
std::string m_menu;
std::string const m_menu;
uint32_t m_process_flags;
bool m_need_idle;
};
} // namespace ui