From b1c764415931c9700552cd456bfeb20fe41ba622 Mon Sep 17 00:00:00 2001 From: Vas Crabb Date: Thu, 21 Oct 2021 04:11:43 +1100 Subject: -plugins: Added an input macro plugin. -plugins: Sort input selection menus for autofire plugin. -frontend: Fixed another case where the menus may not automatically scroll the first item into view. --- plugins/autofire/autofire_menu.lua | 27 +- plugins/inputmacro/init.lua | 133 +++++++ plugins/inputmacro/inputmacro_menu.lua | 586 ++++++++++++++++++++++++++++++ plugins/inputmacro/inputmacro_persist.lua | 145 ++++++++ plugins/inputmacro/plugin.json | 10 + src/frontend/mame/ui/menu.cpp | 6 +- 6 files changed, 902 insertions(+), 5 deletions(-) create mode 100644 plugins/inputmacro/init.lua create mode 100644 plugins/inputmacro/inputmacro_menu.lua create mode 100644 plugins/inputmacro/inputmacro_persist.lua create mode 100644 plugins/inputmacro/plugin.json diff --git a/plugins/autofire/autofire_menu.lua b/plugins/autofire/autofire_menu.lua index 3742747d912..c9afa90e94f 100644 --- a/plugins/autofire/autofire_menu.lua +++ b/plugins/autofire/autofire_menu.lua @@ -234,13 +234,14 @@ end -- Button selection menu 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(manager.machine.ioport.ports) do + 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] = { @@ -251,7 +252,29 @@ local function populate_button_menu() end end end - -- TODO: group by device so we can sort table.sort(inputs, function(x, y) return x.ioport_field.name < y.ioport_field.name 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 diff --git a/plugins/inputmacro/init.lua b/plugins/inputmacro/init.lua new file mode 100644 index 00000000000..ef88efeadcd --- /dev/null +++ b/plugins/inputmacro/init.lua @@ -0,0 +1,133 @@ +-- license:BSD-3-Clause +-- copyright-holders:Vas Crabb +local exports = { + name = 'inputmacro', + version = '0.0.1', + description = 'Input macro plugin', + license = 'The BSD 3-Clause License', + author = { name = 'Vas Crabb' } } + + +local inputmacro = exports + +function inputmacro.startplugin() + --[[ + Configuration data: + * name: display name (string) + * binding: activation sequence (input sequence) + * earlycancel: cancel or complete on release (Boolean) + * loop: -1 = release, 0 = prolong, >0 = loop to step on hold (int) + * steps: + * inputs: + * port: port (I/O port) + * field: field (I/O port field) + * delay: delay before activating inputs in frames (integer) + * duration: duration to activate inputs for (integer) + + Live state: + * step: current step (integer or nil) + * frame: frame of current step, starting at 1 (integer) + ]] + local macros = { } + local active_inputs = { } + local shortname + local menu + local input + + local function activate_inputs(inputs) + for index, input in ipairs(inputs) do + active_inputs[string.format('%s.%d.%d', input.port.tag, input.field.mask, input.field.type)] = input.field + end + end + + local function process_frame() + previous_inputs = active_inputs + active_inputs = { } + + for index, macro in ipairs(macros) do + if macro.step then + if macro.earlycancel and (not input:seq_pressed(macro.binding)) then + -- stop immediately on release if early cancel set + macro.step = nil + else + -- advance frame + macro.frame = macro.frame + 1 + local step = macro.steps[macro.step] + if macro.frame > (step.delay + step.duration) then + if macro.step < #macro.steps then + -- not the last step, advance step + macro.step = macro.step + 1 + macro.frame = 1 + step = macro.steps[macro.step] + elseif not input:seq_pressed(macro.binding) then + -- input released and macro completed + macro.step = nil + step = nil + elseif macro.loop > 0 then + -- loop to step + macro.step = macro.loop + macro.frame = 1 + elseif macro.loop < 0 then + -- release if held + step = nil + end + end + if step and (macro.frame > step.delay) then + activate_inputs(step.inputs) + end + end + elseif input:seq_pressed(macro.binding) then + -- initial activation + macro.step = 1 + macro.frame = 1 + local step = macro.steps[1] + if step.delay == 0 then + -- no delay on first step, activate inputs + activate_inputs(step.inputs) + end + end + end + + for key, field in pairs(active_inputs) do + field:set_value(1) + end + for key, field in pairs(previous_inputs) do + if not active_inputs[key] then + field:set_value(0) + end + end + end + + local function start() + input = manager.machine.input + if shortname ~= emu.romname() then + local persister = require('inputmacro/inputmacro_persist') + macros = persister.load_settings() + shortname = emu.romname() + end + end + + local function stop() + local persister = require('inputmacro/inputmacro_persist') + persister:save_settings(macros) + end + + local function menu_callback(index, event) + return menu:handle_event(index, event) + end + + local function menu_populate() + if not menu then + menu = require('inputmacro/inputmacro_menu') + menu:init(macros) + end + return menu:populate() + end + + emu.register_frame_done(process_frame) + emu.register_start(start) + emu.register_stop(stop) + emu.register_menu(menu_callback, menu_populate, _('Input Macros')) +end + +return exports diff --git a/plugins/inputmacro/inputmacro_menu.lua b/plugins/inputmacro/inputmacro_menu.lua new file mode 100644 index 00000000000..24b963a91e9 --- /dev/null +++ b/plugins/inputmacro/inputmacro_menu.lua @@ -0,0 +1,586 @@ +-- Constants + +local MENU_TYPES = { MACROS = 0, ADD = 1, EDIT = 2, INPUT = 3 } + + +-- Globals + +local macros +local menu_stack + + +-- Helpers + +local function new_macro() + local function check_name(n) + for index, macro in ipairs(macros) do + if macro.name == n then + return false + end + end + return true + end + + local name = _p('plugin-inputmacro', 'New macro') + local number = 1 + while not check_name(name) do + number = number + 1 + name = string.format(_p('plugin-inputmacro', 'New macro %d'), number) + end + return { + name = name, + binding = nil, + earlycancel = true, + loop = -1, + steps = { + { + inputs = { + { + port = nil, + field = nil } }, + delay = 0, + duration = 1 } } } +end + + +-- Input menu + +local input_action +local input_choices +local input_item_first_choice +local input_item_cancel + +function handle_input(index, action) + if (action == 'cancel') or ((index == input_item_cancel) and (action == 'select')) then + table.remove(menu_stack) + input_action = nil + return true + elseif action == 'select' then + field = input_choices[index - input_item_first_choice + 1] + if field then + table.remove(menu_stack) + input_action(field) + input_action = nil + return true + end + end + return false +end + +function populate_input() + local items = { } + + items[#items + 1] = { _p('plugin-inputmacro', 'Set Input'), '', 'off' } + items[#items + 1] = { '---', '', '' } + + if not input_choices then + local ioport = manager.machine.ioport + + local function supported(f) + if f.is_analog or f.is_toggle then + return false + elseif (f.type_class == 'config') or (f.type_class == 'dipswitch') then + return false + else + return true + end + end + + local function compare(a, b) + if a.device.tag < b.device.tag then + return true + elseif a.device.tag > b.device.tag then + return false + end + groupa = ioport:type_group(a.type, a.player) + groupb = ioport:type_group(b.type, b.player) + if groupa < groupb then + return true + elseif groupa > groupb then + return false + elseif a.type < b.type then + return true + elseif a.type > b.type then + return false + else + return a.name < b.name + end + end + + input_choices = { } + 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 + end + end + end + table.sort(input_choices, compare) + end + + input_item_first_choice = #items + 1 + for index, field in ipairs(input_choices) do + items[#items + 1] = { _p('input-name', field.name), '', '' } + end + + items[#items + 1] = { '---', '', '' } + items[#items + 1] = { _p('plugin-inputmacro', 'Cancel'), '', '' } + input_item_cancel = #items + + return items +end + + +-- Add/edit menus + +local edit_current_macro +local edit_insert_position +local edit_name_buffer +local edit_items +local edit_item_exit + +local function current_macro_complete() + if not edit_current_macro.binding then + return false + end + local laststep = edit_current_macro.steps[#edit_current_macro.steps] + if not laststep.inputs[#laststep.inputs].field then + return false + end + return true +end + +local function set_binding() + local input = manager.machine.input + local poller = input:switch_sequence_poller() + manager.machine:popmessage(_p('plugin-inputmacro', 'Enter activation sequence or wait to leave unchanged')) + manager.machine.video:frame_update() + poller:start() + local time = os.clock() + local clearmsg = true + while (not poller:poll()) and (poller.modified or (os.clock() < (time + 1))) do + if poller.modified then + if not poller.valid then + manager.machine:popmessage(_p('plugin-inputmacro', 'Invalid sequence entered')) + clearmsg = false + break + end + manager.machine:popmessage(input:seq_name(poller.sequence)) + manager.machine.video:frame_update() + end + end + if clearmsg then + manager.machine:popmessage() + end + if poller.valid then + edit_current_macro.binding = poller.sequence + return true + end + return false +end + +local function handle_edit_items(index, event) + local command = edit_items[index] + + local namecancel = false + if edit_name_buffer and ((not command) or (command.action ~= 'name')) then + edit_name_buffer = nil + namecancel = true + end + + if not command then + return namecancel + elseif command.action == 'name' then + local function namechar() + local ch = tonumber(event) + if not ch then + return nil + elseif (ch >= 0x100) or ((ch & 0x7f) >= 0x20) or (ch == 0x08) then + return utf8.char(ch) + else + return nil + end + end + + if edit_name_buffer then + if event == 'select' then + if #edit_name_buffer > 0 then + edit_current_macro.name = edit_name_buffer + end + edit_name_buffer = nil + return true + elseif event == 'cancel' then + edit_name_buffer = nil + return true + else + local char = namechar() + if char == '\b' then + edit_name_buffer = edit_name_buffer:gsub('[%z\1-\127\192-\255][\128-\191]*$', '') + return true + elseif char then + edit_name_buffer = edit_name_buffer .. char + return true + end + end + elseif event == 'select' then + edit_name_buffer = edit_current_macro.name + return true + else + local char = namechar() + if char == '\b' then + edit_name_buffer = '' + return true + elseif char then + edit_name_buffer = char + return true + end + end + elseif command.action == 'binding' then + if event == 'select' then + return set_binding() + end + elseif command.action == 'releaseaction' then + if (event == 'select') or (event == 'left') or (event == 'right') then + edit_current_macro.earlycancel = not edit_current_macro.earlycancel + return true + end + elseif command.action == 'holdaction' then + if event == 'left' then + edit_current_macro.loop = edit_current_macro.loop - 1 + return true + elseif event == 'right' then + edit_current_macro.loop = edit_current_macro.loop + 1 + return true + elseif event == 'clear' then + edit_current_macro.loop = -1 + return true + end + elseif command.action == 'delay' then + local step = edit_current_macro.steps[command.step] + if event == 'left' then + step.delay = step.delay - 1 + return true + elseif event == 'right' then + step.delay = step.delay + 1 + return true + elseif event == 'clear' then + step.delay = 0 + return true + end + elseif command.action == 'duration' then + local step = edit_current_macro.steps[command.step] + if event == 'left' then + step.duration = step.duration - 1 + return true + elseif event == 'right' then + step.duration = step.duration + 1 + return true + elseif event == 'clear' then + step.duration = 1 + return true + end + elseif command.action == 'input' then + local inputs = edit_current_macro.steps[command.step].inputs + if event == 'select' then + input_action = + function(field) + inputs[command.input].port = field.port + inputs[command.input].field = field + end + table.insert(menu_stack, MENU_TYPES.INPUT) + return true + elseif event == 'clear' then + if #inputs > 1 then + table.remove(inputs, command.input) + return true + end + end + elseif command.action == 'addinput' then + if event == 'select' then + local inputs = edit_current_macro.steps[command.step].inputs + input_action = + function(field) + inputs[#inputs + 1] = { + port = field.port, + field = field } + end + table.insert(menu_stack, MENU_TYPES.INPUT) + return true + end + elseif command.action == 'deletestep' then + if event == 'select' then + table.remove(edit_current_macro.steps, command.step) + if edit_current_macro.loop > #edit_current_macro.steps then + edit_current_macro.loop = -1 + elseif edit_current_macro.loop > command.step then + edit_current_macro.loop = edit_current_macro.loop - 1 + end + if edit_insert_position > command.step then + edit_insert_position = edit_insert_position - 1 + end + return true + end + elseif command.action == 'addstep' then + if event == 'select' then + local steps = edit_current_macro.steps + input_action = + function(field) + local newstep = { + inputs = { + { + port = field.port, + field = field } }, + delay = 0, + duration = 1 } + table.insert(steps, edit_insert_position, newstep) + if edit_current_macro.loop >= edit_insert_position then + edit_current_macro.loop = edit_current_macro.loop + 1 + end + edit_insert_position = edit_insert_position + 1 + end + table.insert(menu_stack, MENU_TYPES.INPUT) + return true + elseif event == 'left' then + edit_insert_position = edit_insert_position - 1 + return true + elseif event == 'right' then + edit_insert_position = edit_insert_position + 1 + return true + end + end + + return namecancel +end + +local function add_edit_items(items) + edit_items = { } + local input = manager.machine.input + local arrows + + items[#items + 1] = { _p('plugin-inputmacro', 'Name'), edit_name_buffer and (edit_name_buffer .. '_') or edit_current_macro.name, '' } + edit_items[#items] = { action = 'name' } + + local binding = edit_current_macro.binding + local activation = binding and input:seq_name(binding) or _p('plugin-inputmacro', '[not set]') + items[#items + 1] = { _p('plugin-inputmacro', 'Activation sequence'), activation, '' } + edit_items[#items] = { action = 'binding' } + + local releaseaction = edit_current_macro.earlycancel and _p('plugin-inputmacro', 'Stop immediately') or _p('plugin-inputmacro', 'Complete macro') + items[#items + 1] = { _p('plugin-inputmacro', 'On release'), releaseaction, edit_current_macro.earlycancel and 'r' or 'l' } + edit_items[#items] = { action = 'releaseaction' } + + local holdaction + arrows = 'lr' + if edit_current_macro.loop < 0 then + holdaction = _p('plugin-inputmacro', 'Release') + arrows = 'r' + elseif edit_current_macro.loop > 0 then + holdaction = string.format(_p('plugin-inputmacro', 'Loop to step %d'), edit_current_macro.loop) + if edit_current_macro.loop >= #edit_current_macro.steps then + arrows = 'l' + end + else + holdaction = string.format(_p('plugin-inputmacro', 'Prolong step %d'), #edit_current_macro.steps) + end + items[#items + 1] = { _p('plugin-inputmacro', 'When held'), holdaction, arrows } + edit_items[#items] = { action = 'holdaction' } + + for i, step in ipairs(edit_current_macro.steps) do + items[#items + 1] = { '---', '', '' } + + items[#items + 1] = { string.format(_p('plugin-inputmacro', 'Step %d'), i), '', 'off' } + items[#items + 1] = { _p('plugin-inputmacro', 'Delay (frames)'), step.delay, (step.delay > 0) and 'lr' or 'r' } + edit_items[#items] = { action = 'delay', step = i } + + items[#items + 1] = { _p('plugin-inputmacro', 'Duration (frames)'), step.duration, (step.duration > 1) and 'lr' or 'r' } + edit_items[#items] = { action = 'duration', step = i } + + for j, input in ipairs(step.inputs) do + local inputname = input.field and _p('input-name', input.field.name) or _p('plugin-inputmacro', '[not set]') + items[#items + 1] = { string.format(_p('plugin-inputmacro', 'Input %d'), j), inputname, '' } + edit_items[#items] = { action = 'input', step = i, input = j } + end + + if step.inputs[#step.inputs].field then + items[#items + 1] = { _p('plugin-inputmacro', 'Add input'), '', '' } + edit_items[#items] = { action = 'addinput', step = i } + end + + if #edit_current_macro.steps > 1 then + items[#items + 1] = { _p('plugin-inputmacro', 'Delete step'), '', '' } + edit_items[#items] = { action = 'deletestep', step = i } + end + end + + local laststep = edit_current_macro.steps[#edit_current_macro.steps] + if laststep.inputs[#laststep.inputs].field then + items[#items + 1] = { '---', '', '' } + + arrows = 'lr' + if edit_insert_position > #edit_current_macro.steps then + arrows = 'l' + elseif edit_insert_position < 2 then + arrows = 'r' + end + items[#items + 1] = { _p('plugin-inputmacro', 'Add step at position'), edit_insert_position, arrows } + edit_items[#items] = { action = 'addstep', step = i } + end +end + +local function handle_add(index, event) + if handle_edit_items(index, event) then + return true + elseif event == 'cancel' then + edit_current_macro = nil + edit_items = nil + table.remove(menu_stack) + return true + elseif (index == edit_item_exit) and (event == 'select') then + if current_macro_complete() then + table.insert(macros, edit_current_macro) + end + edit_current_macro = nil + edit_items = nil + table.remove(menu_stack) + return true + end + return false +end + +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 + edit_current_macro = nil + edit_items = nil + table.remove(menu_stack) + return true + end + return false +end + +local function populate_add() + local items = { } + + items[#items + 1] = { _p('plugin-inputmacro', 'Add Input Macro'), '', 'off' } + items[#items + 1] = { '---', '', '' } + + add_edit_items(items) + + items[#items + 1] = { '---', '', '' } + if current_macro_complete() then + items[#items + 1] = { _p('plugin-inputmacro', 'Create'), '', '' } + else + items[#items + 1] = { _p('plugin-inputmacro', 'Cancel'), '', '' } + end + edit_item_exit = #items + + return items +end + +local function populate_edit() + local items = { } + + items[#items + 1] = { _p('plugin-inputmacro', 'Edit Input Macro'), '', 'off' } + items[#items + 1] = { '---', '', '' } + + add_edit_items(items) + + items[#items + 1] = { '---', '', '' } + items[#items + 1] = { _p('plugin-inputmacro', 'Done'), '', '' } + edit_item_exit = #items + + return items +end + + +-- Macros menu + +local macros_item_first_macro +local macros_item_add + +function handle_macros(index, event) + if index == macros_item_add then + if event == 'select' then + edit_current_macro = new_macro() + edit_insert_position = #edit_current_macro.steps + 1 + table.insert(menu_stack, MENU_TYPES.ADD) + return true + end + elseif index >= macros_item_first_macro then + macro = index - macros_item_first_macro + 1 + if event == 'select' then + edit_current_macro = macros[macro] + edit_insert_position = #edit_current_macro.steps + 1 + table.insert(menu_stack, MENU_TYPES.EDIT) + return true + elseif event == 'clear' then + table.remove(macros, macro) + return true + end + end + return false +end + +function populate_macros() + local input = manager.machine.input + local ioport = manager.machine.ioport + local items = { } + + items[#items + 1] = { _p('plugin-inputmacro', 'Input Macros'), '', 'off' } + items[#items + 1] = { string.format(_p('plugin-inputmacro', 'Press %s to delete'), input:seq_name(ioport:type_seq(ioport:token_to_input_type('UI_CLEAR')))), '', 'off' } + items[#items + 1] = { '---', '', '' } + + macros_item_first_macro = #items + 1 + if #macros > 0 then + for index, macro in ipairs(macros) do + items[#items + 1] = { macro.name, input:seq_name(macro.binding), '' } + end + else + items[#items + 1] = { _p('plugin-inputmacro', 'No macros'), '', 'off' } + end + + items[#items + 1] = { '---', '', '' } + items[#items + 1] = { _p('plugin-inputmacro', 'Add macro'), '', '' } + macros_item_add = #items + + return items +end + + +-- Entry points + +local lib = { } + +function lib:init(m) + macros = m + menu_stack = { MENU_TYPES.MACROS } +end + +function lib:handle_event(index, event) + local current = menu_stack[#menu_stack] + if current == MENU_TYPES.MACROS then + return handle_macros(index, event) + elseif current == MENU_TYPES.ADD then + return handle_add(index, event) + elseif current == MENU_TYPES.EDIT then + return handle_edit(index, event) + elseif current == MENU_TYPES.INPUT then + return handle_input(index, event) + end +end + +function lib:populate() + local current = menu_stack[#menu_stack] + if current == MENU_TYPES.MACROS then + return populate_macros() + elseif current == MENU_TYPES.ADD then + return populate_add() + elseif current == MENU_TYPES.EDIT then + return populate_edit() + elseif current == MENU_TYPES.INPUT then + return populate_input() + end +end + +return lib diff --git a/plugins/inputmacro/inputmacro_persist.lua b/plugins/inputmacro/inputmacro_persist.lua new file mode 100644 index 00000000000..04d310faeef --- /dev/null +++ b/plugins/inputmacro/inputmacro_persist.lua @@ -0,0 +1,145 @@ +-- Helpers + +local function settings_path() + return emu.subst_env(manager.machine.options.entries.homepath:value():match('([^;]+)')) .. '/inputmacro/' +end + +local function settings_filename() + return emu.romname() .. '.cfg' +end + +local function make_macro(setting) + if (setting.name == nil) or (setting.binding == nil) or (setting.earlycancel == nil) or (setting.loop == nil) or (setting.steps == nil) then + return nil + end + + local result = { + name = setting.name, + binding = manager.machine.input:seq_from_tokens(setting.binding), + earlycancel = setting.earlycancel, + loop = setting.loop, + steps = { } } + + local ioport = manager.machine.ioport + for i, step in ipairs(setting.steps) do + if step.inputs and step.delay and step.duration then + local s = { + inputs = { }, + delay = step.delay, + duration = step.duration } + for j, input in ipairs(step.inputs) do + if input.port and input.mask and input.type then + local port = ioport.ports[input.port] + if port then + local field = port:field(input.mask) + if field and (field.type == ioport:token_to_input_type(input.type)) then + table.insert(s.inputs, { port = port, field = field }) + end + end + end + end + if #s.inputs > 0 then + table.insert(result.steps, s) + end + end + end + + if result.loop > #result.steps then + result.loop = -1 + end + + if #result.steps > 0 then + return result + else + return nil + end +end + +local function make_settings(macros) + local input = manager.machine.input + local ioport = manager.machine.ioport + local result = { } + for i, macro in ipairs(macros) do + local m = { + name = macro.name, + binding = input:seq_to_tokens(macro.binding), + earlycancel = macro.earlycancel, + loop = macro.loop, + steps = { } } + table.insert(result, m) + for j, step in ipairs(macro.steps) do + local s = { + inputs = { }, + delay = step.delay, + duration = step.duration } + table.insert(m.steps, s) + for k, input in ipairs(step.inputs) do + local b = { + port = input.port.tag, + mask = input.field.mask, + type = ioport:input_type_to_token(input.field.type) } + table.insert(s.inputs, b) + end + end + end + return result +end + + +-- Entry points + +local lib = { } + +function lib:load_settings() + filename = settings_path() .. settings_filename() + local file = io.open(filename, 'r') + if not file then + return { } + end + local json = require('json') + local settings = json.parse(file:read('a')) + file:close() + if not settings then + emu.print_error(string.format('Error loading input macros: error parsing file "%s" as JSON\n', filename)) + return { } + end + + result = { } + for index, setting in ipairs(settings) do + local macro = make_macro(setting) + if macro then + table.insert(result, macro) + end + end + return result +end + +function lib:save_settings(macros) + local path = settings_path() + local stat = lfs.attributes(path) + if not stat then + lfs.mkdir(path) + elseif stat.mode ~= 'directory' then + emu.print_error(string.format('Error saving input macros: "%s" is not a directory\n', path)) + return + end + filename = path .. settings_filename() + + if #macros == 0 then + os.remove(filename) + return + end + + local json = require('json') + local settings = make_settings(macros) + local text = json.stringify(settings, { indent = true }) + local file = io.open(filename, 'w') + if not file then + emu.print_error(string.format('Error saving input macros: error opening file "%s" for writing\n', filename)) + return + end + file:write(text) + file:close() +end + +return lib diff --git a/plugins/inputmacro/plugin.json b/plugins/inputmacro/plugin.json new file mode 100644 index 00000000000..2d209fca116 --- /dev/null +++ b/plugins/inputmacro/plugin.json @@ -0,0 +1,10 @@ +{ + "plugin": { + "name": "inputmacro", + "description": "Input macro plugin", + "version": "0.0.1", + "author": "Vas Crabb", + "type": "plugin", + "start": "false" + } +} diff --git a/src/frontend/mame/ui/menu.cpp b/src/frontend/mame/ui/menu.cpp index ffbd52fa620..9e98511a882 100644 --- a/src/frontend/mame/ui/menu.cpp +++ b/src/frontend/mame/ui/menu.cpp @@ -518,11 +518,11 @@ void menu::draw(uint32_t flags) if (!customonly) ui().draw_outlined_box(container(), x1, y1, x2, y2, ui().colors().background_color()); + if ((m_selected >= (top_line + m_visible_lines)) || (m_selected < (top_line + 1))) + top_line = m_selected - (m_visible_lines / 2); if (top_line < 0 || is_first_selected()) top_line = 0; - else 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()) + else if ((top_line > (m_items.size() - m_visible_lines)) || is_last_selected()) top_line = m_items.size() - m_visible_lines; else if (m_selected >= (top_line + m_visible_lines - 2)) top_line = m_selected - m_visible_lines + ((m_selected == (m_items.size() - 1)) ? 1: 2); -- cgit v1.2.3