-- license:BSD-3-Clause -- copyright-holders:Vas Crabb -- Constants local MENU_TYPES = { MACROS = 0, ADD = 1, EDIT = 2, INPUT = 3 } -- Globals local macros local menu_stack local macros_start_macro -- really for the macros menu, but has to be declared local before edit menu functions -- 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_start_field 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 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 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 items[#items + 1] = { '---', '', '' } items[#items + 1] = { _p('plugin-inputmacro', 'Cancel'), '', '' } input_item_cancel = #items return items, selection end -- Add/edit menus 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 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 input_start_field = inputs[command.input].field edit_start_selection = index 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 edit_start_selection = index 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 edit_start_step = command.step if edit_start_step > #edit_current_macro.steps then edit_start_step = edit_start_step - 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_start_step = edit_insert_position edit_insert_position = edit_insert_position + 1 end edit_start_selection = index 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' } 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]') 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] = { string.format(_p('plugin-inputmacro', 'Step %d'), i), '', 'heading' } items[#items + 1] = { _p('plugin-inputmacro', 'Delay (frames)'), step.delay, (step.delay > 0) and 'lr' or 'r' } edit_items[#items] = { action = 'delay', step = i } if edit_start_step == i then edit_start_selection = #items end 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 edit_start_step = nil 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 input_choices = nil edit_current_macro = nil edit_menu_active = false 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) macros_start_macro = #macros end input_choices = nil edit_menu_active = false 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 input_choices = nil edit_current_macro = nil edit_menu_active = false 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 local selection = edit_start_selection edit_start_selection = nil return items, selection, 'lrrepeat' 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 local selection = edit_start_selection edit_start_selection = nil return items, selection, 'lrrepeat' end -- Macros menu local macros_item_first_macro local macros_selection_save 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 macros_selection_save = index 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 macros_selection_save = index table.insert(menu_stack, MENU_TYPES.EDIT) return true elseif event == 'clear' then table.remove(macros, macro) if #macros > 0 then macros_selection_save = index if macro > #macros then macros_selection_save = macros_selection_save - 1 end end 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), '' } if macros_start_macro == index then macros_selection_save = #items end end else items[#items + 1] = { _p('plugin-inputmacro', '[no macros]'), '', 'off' } end macros_start_macro = nil items[#items + 1] = { '---', '', '' } items[#items + 1] = { _p('plugin-inputmacro', 'Add macro'), '', '' } macros_item_add = #items local selection = macros_selection_save macros_selection_save = nil return items, selection 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