diff --git a/plugins/viewswitch/init.lua b/plugins/viewswitch/init.lua index 6706ff4..92d04d4 100644 --- a/plugins/viewswitch/init.lua +++ b/plugins/viewswitch/init.lua @@ -2,7 +2,7 @@ -- copyright-holders:Vas Crabb local exports = { name = 'viewswitch', - version = '0.0.2', + version = '0.0.4', description = 'Quick view switch plugin', license = 'BSD-3-Clause', author = { name = 'Vas Crabb' } } @@ -14,11 +14,124 @@ local stop_subscription function viewswitch.startplugin() local switch_hotkeys = { } local cycle_hotkeys = { } + local excluded_views = { } + local plugin_settings = { } local input_manager local ui_manager = { menu_active = true, ui_active = true } local render_targets local menu_handler + local utils -- Add utils module + + -- Helper function to check if viewswitch.txt exists at startup + local function should_hide_menu() + -- Try to get the homepath from environment or use a fallback + local homepath = os.getenv('HOME') or os.getenv('USERPROFILE') or '.' + + -- Common MAME paths to check + local possible_paths = { + homepath .. '/.mame/viewswitch/viewswitch.txt', + homepath .. '/mame/viewswitch/viewswitch.txt', + './viewswitch/viewswitch.txt', + 'viewswitch/viewswitch.txt' + } + + -- If manager.machine is available, use the proper path first + if manager.machine and manager.machine.options and manager.machine.options.entries.homepath then + local mame_path = manager.machine.options.entries.homepath:value():match('([^;]+)') .. '/viewswitch/viewswitch.txt' + table.insert(possible_paths, 1, mame_path) -- Insert at beginning + end + + -- Check all possible paths + for _, path in ipairs(possible_paths) do + local file = io.open(path, 'r') + if file then + file:close() + return true -- File exists, hide menu + end + end + + return false -- File doesn't exist anywhere, show menu + end + + local function get_next_view(target, current_view, increment) + local view_indices = {} + + -- Check if layout-only mode is enabled + if plugin_settings.layout_only_mode then + -- Use utils module to get layout views + if utils then + local layout_view_names = utils:parse_layout_views() + + if #layout_view_names > 0 then + -- Get layout view indices + local layout_views = utils:get_layout_view_indices(target, layout_view_names) + for _, view_info in ipairs(layout_views) do + table.insert(view_indices, view_info.index) + end + end + end + + -- If no layout views found, fall back to all views + if #view_indices == 0 then + for i = 1, #target.view_names do + table.insert(view_indices, i) + end + end + else + -- Use all views + for i = 1, #target.view_names do + table.insert(view_indices, i) + end + end + + if #view_indices == 0 then + return current_view -- No valid views + end + + local target_index = target.index + local excluded = excluded_views[target_index] or {} + + -- Find current position in views array + local current_pos = 1 + for pos, view_index in ipairs(view_indices) do + if view_index == current_view then + current_pos = pos + break + end + end + + -- If no exclusions, use simple cycling + if not next(excluded) then + local new_pos = current_pos + increment + if new_pos < 1 then + new_pos = #view_indices + elseif new_pos > #view_indices then + new_pos = 1 + end + return view_indices[new_pos] + end + + -- Find next non-excluded view + local tries = 0 + local pos = current_pos + repeat + pos = pos + increment + if pos < 1 then + pos = #view_indices + elseif pos > #view_indices then + pos = 1 + end + tries = tries + 1 + until (not excluded[view_indices[pos]]) or (tries >= #view_indices) + + -- If all views are excluded, return current view + if tries >= #view_indices then + return current_view + end + + return view_indices[pos] + end local function frame_done() if ui_manager.ui_active and (not ui_manager.menu_active) then @@ -31,9 +144,8 @@ function viewswitch.startplugin() if input_manager:seq_pressed(hotkey.sequence) then if not hotkey.pressed then local target = render_targets[hotkey.target] - local index = target.view_index + hotkey.increment - local count = #target.view_names - target.view_index = (index < 1) and count or (index > count) and 1 or index + local next_view = get_next_view(target, target.view_index, hotkey.increment) + target.view_index = next_view hotkey.pressed = true end else @@ -44,8 +156,17 @@ function viewswitch.startplugin() end local function start() + -- Load the shared utilities module + local status, msg = pcall(function () utils = require('viewswitch/viewswitch_utils') end) + if not status then + emu.print_error(string.format('Error loading viewswitch utilities: %s', msg)) + utils = nil + end + local persister = require('viewswitch/viewswitch_persist') - switch_hotkeys, cycle_hotkeys = persister:load_settings() + plugin_settings = persister:load_plugin_settings() + switch_hotkeys, cycle_hotkeys = persister:load_settings(plugin_settings.config_mode) + excluded_views = persister:load_excluded_views(plugin_settings.config_mode) local machine = manager.machine input_manager = machine.input @@ -55,7 +176,9 @@ function viewswitch.startplugin() local function stop() local persister = require('viewswitch/viewswitch_persist') - persister:save_settings(switch_hotkeys, cycle_hotkeys) + persister:save_plugin_settings(plugin_settings) + persister:save_settings(switch_hotkeys, cycle_hotkeys, plugin_settings.config_mode) + persister:save_excluded_views(excluded_views, plugin_settings.config_mode) menu_handler = nil render_targets = nil @@ -63,6 +186,9 @@ function viewswitch.startplugin() input_manager = nil switch_hotkeys = { } cycle_hotkeys = { } + excluded_views = { } + plugin_settings = { } + utils = nil end local function menu_callback(index, event) @@ -76,7 +202,7 @@ function viewswitch.startplugin() emu.print_error(string.format('Error loading quick view switch menu: %s', msg)) end if menu_handler then - menu_handler:init(switch_hotkeys, cycle_hotkeys) + menu_handler:init(switch_hotkeys, cycle_hotkeys, excluded_views, plugin_settings) end end if menu_handler then @@ -89,7 +215,11 @@ function viewswitch.startplugin() emu.register_frame_done(frame_done) emu.register_prestart(start) stop_subscription = emu.add_machine_stop_notifier(stop) - emu.register_menu(menu_callback, menu_populate, 'Quick View Switch') + + -- Only register the menu if viewswitch.txt doesn't exist + if not should_hide_menu() then + emu.register_menu(menu_callback, menu_populate, 'Quick View Switch') + end end -return exports +return exports \ No newline at end of file diff --git a/plugins/viewswitch/viewswitch_menu.lua b/plugins/viewswitch/viewswitch_menu.lua index 0c1cd58..9da1bf3 100644 --- a/plugins/viewswitch/viewswitch_menu.lua +++ b/plugins/viewswitch/viewswitch_menu.lua @@ -6,7 +6,9 @@ local MENU_TYPES = { MAIN = 0, SWITCH = 2, - CYCLE = 3 } + CYCLE = 3, + EXCLUDE = 4, + SETTINGS = 5 } -- helper functions @@ -35,11 +37,42 @@ local commonui local switch_hotkeys local cycle_hotkeys +local excluded_views +local plugin_settings local switch_target_start local switch_done local switch_poll +local exclude_target_start +local exclude_done + +local settings_done +local settings_config_mode_index +local settings_remove_overrides_index +local settings_layout_only_index -- Add this missing declaration +local pending_config_mode -- Track pending changes + +-- Use shared utility module for layout parsing +local utils + +local function get_filtered_views(target) + if not utils then + local status, msg = pcall(function () utils = require('viewswitch/viewswitch_utils') end) + if not status then + emu.print_error(string.format('Error loading viewswitch utilities: %s', msg)) + -- Fallback to all views if utils can't load + local all_views = {} + local view_names = target.view_names + for i, view_name in ipairs(view_names) do + table.insert(all_views, {index = i, name = view_name}) + end + return all_views + end + end + + return utils:get_filtered_views(target, plugin_settings) +end -- quick switch hotkeys menu @@ -105,7 +138,8 @@ local function populate_switch() switch_target_start = { } local items = { } - table.insert(items, { 'Quick Switch Hotkeys', '', 'off' }) + local mode_text = plugin_settings.layout_only_mode and 'Layout Views Only' or 'All Views' + table.insert(items, { 'Quick Switch Hotkeys (' .. mode_text .. ')', '', 'off' }) table.insert(items, { string.format('Press %s to clear hotkey', general_input_setting('UI_CLEAR')), '', 'off' }) if #targets == 0 then @@ -114,27 +148,40 @@ local function populate_switch() else local input = manager.machine.input for i, target in pairs(targets) do - -- add separator and target heading if multiple targets - table.insert(items, { '---', '', '' }) - if #targets > 1 then - table.insert(items, { string.format('Screen #%d', target.index - 1), '', 'off' }) - end - table.insert(switch_target_start, #items + 1) + -- Get views based on current setting + local filtered_views = get_filtered_views(target) + + -- Special handling for layout-only mode when no layout found + if plugin_settings.layout_only_mode and #filtered_views == 0 then + table.insert(items, { '---', '', '' }) + if #targets > 1 then + table.insert(items, { string.format('Screen #%d', target.index - 1), '', 'off' }) + end + table.insert(items, { 'No layout file found', '', 'off' }) + else + -- add separator and target heading if multiple targets + table.insert(items, { '---', '', '' }) + if #targets > 1 then + local target_desc = plugin_settings.layout_only_mode and 'Layout Views Only' or 'All Views' + table.insert(items, { string.format('Screen #%d (%s)', target.index - 1, target_desc), '', 'off' }) + end + table.insert(switch_target_start, #items + 1) - -- add an item for each view - for j, view in pairs(target.view_names) do - local seq = 'None' - for k, hotkey in pairs(switch_hotkeys) do - if (hotkey.target == target.index) and (hotkey.view == j) then - seq = input:seq_name(hotkey.sequence) - break + -- add an item for each view + for _, view_info in ipairs(filtered_views) do + local seq = 'None' + for k, hotkey in pairs(switch_hotkeys) do + if (hotkey.target == target.index) and (hotkey.view == view_info.index) then + seq = input:seq_name(hotkey.sequence) + break + end end + local flags = '' + if switch_poll and (switch_poll.target == target.index) and (switch_poll.view == view_info.index) then + flags = 'lr' + end + table.insert(items, { view_info.name, seq, flags }) end - local flags = '' - if switch_poll and (switch_poll.target == target.index) and (switch_poll.view == j) then - flags = 'lr' - end - table.insert(items, { view, seq, flags }) end end end @@ -215,7 +262,8 @@ local function populate_cycle() switch_target_start = { } local items = { } - table.insert(items, { 'Cycle Hotkeys', '', 'off' }) + local mode_text = plugin_settings.layout_only_mode and 'Layout Views Only' or 'All Views' + table.insert(items, { 'Cycle Hotkeys (' .. mode_text .. ')', '', 'off' }) table.insert(items, { string.format('Press %s to clear hotkey', general_input_setting('UI_CLEAR')), '', 'off' }) if #targets == 0 then @@ -227,7 +275,8 @@ local function populate_cycle() -- add separator and target heading if multiple targets table.insert(items, { '---', '', '' }) if #targets > 1 then - table.insert(items, { string.format('Screen #%d', target.index - 1), '', 'off' }) + local target_desc = plugin_settings.layout_only_mode and 'Layout Views Only' or 'All Views' + table.insert(items, { string.format('Screen #%d (%s)', target.index - 1, target_desc), '', 'off' }) end table.insert(switch_target_start, #items + 1) @@ -273,6 +322,345 @@ local function populate_cycle() end +-- exclude views menu + +local function handle_exclude(index, event) + if (event == 'back') or ((event == 'select') and (index == exclude_done)) then + exclude_target_start = nil + exclude_done = nil + table.remove(menu_stack) + return true + else + for target = #exclude_target_start, 1, -1 do + if index >= exclude_target_start[target] then + local view = index - exclude_target_start[target] + 1 + if event == 'select' then + -- toggle exclude status + if not excluded_views[target] then + excluded_views[target] = { } + end + excluded_views[target][view] = not excluded_views[target][view] + return true + end + return false + end + end + end + return false +end + +local function populate_exclude() + -- find targets with selectable views + local targets = get_targets() + + exclude_target_start = { } + local items = { } + + local mode_text = plugin_settings.layout_only_mode and 'Layout Views Only' or 'All Views' + table.insert(items, { 'Exclude Views from Cycle (' .. mode_text .. ')', '', 'off' }) + table.insert(items, { 'Select views to exclude from cycling', '', 'off' }) + + if #targets == 0 then + table.insert(items, { '---', '', '' }) + table.insert(items, { 'No selectable views', '', 'off' }) + else + for i, target in pairs(targets) do + -- Get views based on current setting (NOT hardcoded to layout only) + local filtered_views = get_filtered_views(target) + + -- Special handling for layout-only mode when no layout found + if plugin_settings.layout_only_mode and #filtered_views == 0 then + table.insert(items, { '---', '', '' }) + if #targets > 1 then + table.insert(items, { string.format('Screen #%d', target.index - 1), '', 'off' }) + end + table.insert(items, { 'No layout file found', '', 'off' }) + else + -- add separator and target heading if multiple targets + table.insert(items, { '---', '', '' }) + if #targets > 1 then + local target_desc = plugin_settings.layout_only_mode and 'Layout Views Only' or 'All Views' + table.insert(items, { string.format('Screen #%d (%s)', target.index - 1, target_desc), '', 'off' }) + end + table.insert(exclude_target_start, #items + 1) + + -- add an item for each view + for _, view_info in ipairs(filtered_views) do + local flags = '' + if excluded_views[target.index] and excluded_views[target.index][view_info.index] then + flags = 'lr' + end + local status = (excluded_views[target.index] and excluded_views[target.index][view_info.index]) and 'Excluded' or 'Included' + table.insert(items, { view_info.name, status, flags }) + end + end + end + end + + table.insert(items, { '---', '', '' }) + table.insert(items, { 'Done', '', '' }) + exclude_done = #items + + return items +end + + +-- plugin settings menu + +local function handle_settings(index, event) + if event == 'back' then + -- Cancel any pending changes + pending_config_mode = nil + settings_done = nil + settings_config_mode_index = nil + settings_remove_overrides_index = nil + settings_layout_only_index = nil + table.remove(menu_stack) + return true + elseif (event == 'select') and (index == settings_done) then + -- Apply pending changes before exiting + if pending_config_mode and pending_config_mode ~= plugin_settings.config_mode then + local old_config_mode = plugin_settings.config_mode + plugin_settings.config_mode = pending_config_mode + + local persister = require('viewswitch/viewswitch_persist') + + -- Handle the transition based on the modes + if old_config_mode == 'global' and pending_config_mode == 'rom_specific' then + persister:copy_to_rom_settings(switch_hotkeys, cycle_hotkeys, excluded_views) + elseif old_config_mode == 'rom_specific' and pending_config_mode == 'global' then + persister:save_settings_to_file(switch_hotkeys, cycle_hotkeys, true) + persister:save_excluded_views_to_file(excluded_views, true) + else + persister:save_settings(switch_hotkeys, cycle_hotkeys, old_config_mode) + persister:save_excluded_views(excluded_views, old_config_mode) + end + + -- Save plugin settings + persister:save_plugin_settings(plugin_settings) + + -- Clear current arrays and load settings for new mode + for i = #switch_hotkeys, 1, -1 do + table.remove(switch_hotkeys, i) + end + for i = #cycle_hotkeys, 1, -1 do + table.remove(cycle_hotkeys, i) + end + for k in pairs(excluded_views) do + excluded_views[k] = nil + end + + -- Load new settings + local new_switch, new_cycle = persister:load_settings(plugin_settings.config_mode) + local new_excluded = persister:load_excluded_views(plugin_settings.config_mode) + + -- Rebuild arrays with proper references + for k, hotkey in pairs(new_switch) do + table.insert(switch_hotkeys, hotkey) + end + for k, hotkey in pairs(new_cycle) do + hotkey.pressed = false + table.insert(cycle_hotkeys, hotkey) + end + for target_index, target_excludes in pairs(new_excluded) do + excluded_views[target_index] = { } + for view_index, excluded in pairs(target_excludes) do + excluded_views[target_index][view_index] = excluded + end + end + end + + pending_config_mode = nil + settings_done = nil + settings_config_mode_index = nil + settings_remove_overrides_index = nil + settings_layout_only_index = nil + table.remove(menu_stack) + return true + elseif event == 'select' then + if index == settings_config_mode_index then + -- Cycle through config modes + local current_mode = pending_config_mode or plugin_settings.config_mode + + if current_mode == 'global' then + pending_config_mode = 'rom_specific' + elseif current_mode == 'rom_specific' then + pending_config_mode = 'global_with_overrides' + else -- global_with_overrides + pending_config_mode = 'global' + end + + return true + elseif index == settings_layout_only_index then + -- Toggle layout-only mode + plugin_settings.layout_only_mode = not plugin_settings.layout_only_mode + + -- Save the setting immediately since it doesn't require mode transitions + local persister = require('viewswitch/viewswitch_persist') + persister:save_plugin_settings(plugin_settings) + + return true + elseif index == settings_remove_overrides_index then + -- Remove ROM overrides option + if plugin_settings.config_mode == 'global_with_overrides' then + local persister = require('viewswitch/viewswitch_persist') + persister:remove_rom_overrides() + + -- Reload settings to reflect the removal of overrides + for i = #switch_hotkeys, 1, -1 do + table.remove(switch_hotkeys, i) + end + for i = #cycle_hotkeys, 1, -1 do + table.remove(cycle_hotkeys, i) + end + for k in pairs(excluded_views) do + excluded_views[k] = nil + end + + local new_switch, new_cycle = persister:load_settings(plugin_settings.config_mode) + local new_excluded = persister:load_excluded_views(plugin_settings.config_mode) + + for k, hotkey in pairs(new_switch) do + table.insert(switch_hotkeys, hotkey) + end + for k, hotkey in pairs(new_cycle) do + hotkey.pressed = false + table.insert(cycle_hotkeys, hotkey) + end + for target_index, target_excludes in pairs(new_excluded) do + excluded_views[target_index] = { } + for view_index, excluded in pairs(target_excludes) do + excluded_views[target_index][view_index] = excluded + end + end + + return true + end + end + end + return false +end + +local function populate_settings() + local items = { } + local persister = require('viewswitch/viewswitch_persist') + + -- Use pending config mode for display if user has made changes + local display_mode = pending_config_mode or plugin_settings.config_mode + local settings_info = persister:get_current_settings_info(plugin_settings.config_mode) + + -- Reset indices + settings_config_mode_index = nil + settings_remove_overrides_index = nil + settings_layout_only_index = nil + + table.insert(items, { 'Plugin Settings', '', 'off' }) + table.insert(items, { '---', '', '' }) + + -- Show current/pending configuration mode + local config_description = '' + local mode_flags = '' + + if display_mode == 'global' then + config_description = 'Global' + elseif display_mode == 'rom_specific' then + config_description = 'ROM-specific' + else -- global_with_overrides + config_description = 'Global with ROM overrides' + end + + -- Add indicator if there are unsaved changes + if pending_config_mode and pending_config_mode ~= plugin_settings.config_mode then + config_description = config_description .. ' (unsaved)' + mode_flags = 'lr' + end + + table.insert(items, { 'Configuration mode', config_description, mode_flags }) + settings_config_mode_index = #items + table.insert(items, { '---', '', '' }) + + -- Add layout-only toggle setting + local layout_only_status = plugin_settings.layout_only_mode and 'Layout views only' or 'All views' + table.insert(items, { 'View filter', layout_only_status, '' }) + settings_layout_only_index = #items + table.insert(items, { '---', '', '' }) + + -- Show mode descriptions + table.insert(items, { 'Global: Same settings for all ROMs', '', 'off' }) + table.insert(items, { 'ROM-specific: Different settings per ROM', '', 'off' }) + table.insert(items, { 'Global with overrides: Global defaults,', '', 'off' }) + table.insert(items, { ' ROM-specific when customized', '', 'off' }) + table.insert(items, { '---', '', '' }) + table.insert(items, { 'Layout views only: Show only views from', '', 'off' }) + table.insert(items, { ' default.lay file (excludes built-in views)', '', 'off' }) + table.insert(items, { 'All views: Show all available views', '', 'off' }) + + -- Show preview of what will happen if there are pending changes + if pending_config_mode and pending_config_mode ~= plugin_settings.config_mode then + table.insert(items, { '---', '', '' }) + table.insert(items, { 'Preview of changes:', '', 'off' }) + + if pending_config_mode == 'rom_specific' then + table.insert(items, { ' Will create ROM-specific config files', '', 'off' }) + elseif pending_config_mode == 'global_with_overrides' then + if settings_info.has_rom_settings or settings_info.has_rom_excludes then + table.insert(items, { ' Will use existing ROM overrides', '', 'off' }) + else + table.insert(items, { ' Will use global settings (no overrides)', '', 'off' }) + end + elseif pending_config_mode == 'global' then + table.insert(items, { ' Will use global settings only', '', 'off' }) + end + end + + -- Show current status for current mode (not pending) + if plugin_settings.config_mode == 'global_with_overrides' and not pending_config_mode then + table.insert(items, { '---', '', '' }) + + -- Show what this ROM is currently using + local status_text = 'This ROM is using: ' + if settings_info.using_rom_settings or settings_info.using_rom_excludes then + status_text = status_text .. 'ROM overrides' + else + status_text = status_text .. 'Global settings' + end + table.insert(items, { status_text, '', 'off' }) + + -- Show detailed breakdown if there are mixed settings + if settings_info.using_rom_settings ~= settings_info.using_rom_excludes then + if settings_info.using_rom_settings then + table.insert(items, { ' Hotkeys: ROM override', '', 'off' }) + else + table.insert(items, { ' Hotkeys: Global', '', 'off' }) + end + if settings_info.using_rom_excludes then + table.insert(items, { ' Excluded views: ROM override', '', 'off' }) + else + table.insert(items, { ' Excluded views: Global', '', 'off' }) + end + end + + -- Add option to remove ROM overrides if they exist + if settings_info.has_rom_settings or settings_info.has_rom_excludes then + table.insert(items, { 'Remove ROM overrides', '', '' }) + settings_remove_overrides_index = #items + end + end + + table.insert(items, { '---', '', '' }) + + -- Change "Done" text based on whether there are pending changes + local done_text = 'Done' + if pending_config_mode and pending_config_mode ~= plugin_settings.config_mode then + done_text = 'Save & Exit' + end + table.insert(items, { done_text, '', '' }) + settings_done = #items + + return items +end + + -- main menu local function handle_main(index, event) @@ -283,6 +671,12 @@ local function handle_main(index, event) elseif index == 4 then table.insert(menu_stack, MENU_TYPES.CYCLE) return true + elseif index == 5 then + table.insert(menu_stack, MENU_TYPES.EXCLUDE) + return true + elseif index == 6 then + table.insert(menu_stack, MENU_TYPES.SETTINGS) + return true end end return false @@ -295,6 +689,8 @@ local function populate_main() table.insert(items, { '---', '', '' }) table.insert(items, { 'Quick switch hotkeys', '', '' }) table.insert(items, { 'Cycle hotkeys', '', '' }) + table.insert(items, { 'Exclude views', '', '' }) + table.insert(items, { 'Settings', '', '' }) return items end @@ -304,10 +700,13 @@ end local lib = { } -function lib:init(switch, cycle) +function lib:init(switch, cycle, exclude, settings) menu_stack = { MENU_TYPES.MAIN } switch_hotkeys = switch cycle_hotkeys = cycle + excluded_views = exclude or { } + plugin_settings = settings or { config_mode = 'global', layout_only_mode = false } + pending_config_mode = nil -- Reset pending changes when plugin initializes end function lib:handle_event(index, event) @@ -318,6 +717,10 @@ function lib:handle_event(index, event) return handle_switch(index, event) elseif current == MENU_TYPES.CYCLE then return handle_cycle(index, event) + elseif current == MENU_TYPES.EXCLUDE then + return handle_exclude(index, event) + elseif current == MENU_TYPES.SETTINGS then + return handle_settings(index, event) end end @@ -329,7 +732,11 @@ function lib:populate() return populate_switch() elseif current == MENU_TYPES.CYCLE then return populate_cycle() + elseif current == MENU_TYPES.EXCLUDE then + return populate_exclude() + elseif current == MENU_TYPES.SETTINGS then + return populate_settings() end end -return lib +return lib \ No newline at end of file diff --git a/plugins/viewswitch/viewswitch_persist.lua b/plugins/viewswitch/viewswitch_persist.lua index b012c2d..2aee8bb 100644 --- a/plugins/viewswitch/viewswitch_persist.lua +++ b/plugins/viewswitch/viewswitch_persist.lua @@ -7,8 +7,59 @@ local function settings_path() return manager.machine.options.entries.homepath:value():match('([^;]+)') .. '/viewswitch' end -local function settings_filename() - return emu.romname() .. '.cfg' +local function settings_filename(use_global) + if use_global then + return 'global.cfg' + else + return manager.machine.system.name .. '.cfg' + end +end + +local function exclude_filename(use_global) + if use_global then + return 'viewswitch_exclude.cfg' + else + return manager.machine.system.name .. '_exclude.cfg' + end +end + +local function global_settings_filename() + return 'viewswitch_settings.cfg' +end + +local function has_rom_settings() + local filename = settings_path() .. '/' .. settings_filename(false) + local file = io.open(filename, 'r') + if file then + local content = file:read('a') + file:close() + -- Check if file has meaningful content (not just empty or {}) + if content and content:match('%S') then + local json = require('json') + local settings = json.parse(content) + if settings and next(settings) then + return true + end + end + end + return false +end + +local function has_rom_excludes() + local filename = settings_path() .. '/' .. exclude_filename(false) + local file = io.open(filename, 'r') + if file then + local content = file:read('a') + file:close() + if content and content:match('%S') then + local json = require('json') + local settings = json.parse(content) + if settings and next(settings) then + return true + end + end + end + return false end @@ -16,12 +67,125 @@ end local lib = { } -function lib:load_settings() +function lib:load_plugin_settings() + local plugin_settings = { + config_mode = 'global', -- default to global + layout_only_mode = false -- default to show all views + } + + -- try to open the plugin settings file + local filename = settings_path() .. '/' .. global_settings_filename() + local file = io.open(filename, 'r') + if file then + -- try parsing settings as JSON + local json = require('json') + local settings = json.parse(file:read('a')) + file:close() + if not settings then + emu.print_error(string.format('Error loading plugin settings: error parsing file "%s" as JSON', filename)) + else + -- Merge loaded settings with defaults + for key, value in pairs(settings) do + plugin_settings[key] = value + end + + -- Migrate old use_global setting to new config_mode + if plugin_settings.use_global ~= nil then + plugin_settings.config_mode = plugin_settings.use_global and 'global' or 'rom_specific' + plugin_settings.use_global = nil + end + end + end + + return plugin_settings +end + +function lib:save_plugin_settings(plugin_settings) + -- make sure the settings path is a folder if it exists + local path = settings_path() + local stat = lfs.attributes(path) + if stat and (stat.mode ~= 'directory') then + emu.print_error(string.format('Error saving plugin settings: "%s" is not a directory', path)) + return + end + + if not stat then + lfs.mkdir(path) + stat = lfs.attributes(path) + end + + -- try to write the file + local filename = path .. '/' .. global_settings_filename() + local json = require('json') + local text = json.stringify(plugin_settings, { indent = true }) + local file = io.open(filename, 'w') + if not file then + emu.print_error(string.format('Error saving plugin settings: error opening file "%s" for writing', filename)) + else + file:write(text) + file:close() + end +end + +function lib:load_excluded_views_internal(use_global) + local excluded_views = { } + + -- try to open the exclude settings file + local filename = settings_path() .. '/' .. exclude_filename(use_global) + local file = io.open(filename, 'r') + if file then + -- try parsing settings as JSON + local json = require('json') + local exclude_settings = json.parse(file:read('a')) + file:close() + if not exclude_settings then + emu.print_error(string.format('Error loading view exclude settings: error parsing file "%s" as JSON', filename)) + else + -- try to interpret the exclude settings + local render_targets = manager.machine.render.targets + for i_str, target_excludes in pairs(exclude_settings) do + local i = tonumber(i_str) -- convert string key to number + local target = render_targets[i] + if target then + excluded_views[i] = { } + for j, view_name in pairs(target_excludes) do + for k, v in ipairs(target.view_names) do + if view_name == v then + excluded_views[i][k] = true + break + end + end + end + end + end + end + end + + return excluded_views +end + +function lib:load_excluded_views(config_mode) + if config_mode == 'global' then + return self:load_excluded_views_internal(true) + elseif config_mode == 'rom_specific' then + return self:load_excluded_views_internal(false) + elseif config_mode == 'global_with_overrides' then + -- Try ROM-specific first, fall back to global + if has_rom_excludes() then + return self:load_excluded_views_internal(false) + else + return self:load_excluded_views_internal(true) + end + end + return { } +end + +function lib:load_settings_internal(use_global) local switch_hotkeys = { } local cycle_hotkeys = { } -- try to open the system settings file - local filename = settings_path() .. '/' .. settings_filename() + local filename = settings_path() .. '/' .. settings_filename(use_global) local file = io.open(filename, 'r') if file then -- try parsing settings as JSON @@ -38,7 +202,7 @@ function lib:load_settings() local target = render_targets[i] if target then for view, hotkey in pairs(config.switch or { }) do - for j, v in pairs(target.view_names) do + for j, v in ipairs(target.view_names) do if view == v then table.insert(switch_hotkeys, { target = i, view = j, config = hotkey, sequence = input:seq_from_tokens(hotkey) }) break @@ -59,7 +223,24 @@ function lib:load_settings() return switch_hotkeys, cycle_hotkeys end -function lib:save_settings(switch_hotkeys, cycle_hotkeys) +function lib:load_settings(config_mode) + if config_mode == 'global' then + return self:load_settings_internal(true) + elseif config_mode == 'rom_specific' then + return self:load_settings_internal(false) + elseif config_mode == 'global_with_overrides' then + -- Try ROM-specific first, fall back to global + if has_rom_settings() then + return self:load_settings_internal(false) + else + return self:load_settings_internal(true) + end + end + return { }, { } +end + +-- Internal function to save to a specific file type +function lib:save_settings_to_file(switch_hotkeys, cycle_hotkeys, save_global) -- make sure the settings path is a folder if it exists local path = settings_path() local stat = lfs.attributes(path) @@ -68,8 +249,10 @@ function lib:save_settings(switch_hotkeys, cycle_hotkeys) return end + -- Determine filename based on save_global flag + local filename = path .. '/' .. settings_filename(save_global) + -- if nothing to save, remove existing settings file - local filename = path .. '/' .. settings_filename() if (#switch_hotkeys == 0) and (#cycle_hotkeys == 0) then os.remove(filename) else @@ -120,4 +303,217 @@ function lib:save_settings(switch_hotkeys, cycle_hotkeys) end end -return lib +-- Rest of the functions remain the same... +function lib:save_excluded_views_to_file(excluded_views, save_global) + -- make sure the settings path is a folder if it exists + local path = settings_path() + local stat = lfs.attributes(path) + if stat and (stat.mode ~= 'directory') then + emu.print_error(string.format('Error saving view exclude settings: "%s" is not a directory', path)) + return + end + + -- Determine filename based on save_global flag + local filename = path .. '/' .. exclude_filename(save_global) + + -- For global saves, we need to merge with existing exclusions + local final_exclude_settings = {} + + if save_global then + -- Load existing global exclusions first + local existing_file = io.open(filename, 'r') + if existing_file then + local json = require('json') + local existing_settings = json.parse(existing_file:read('a')) + existing_file:close() + if existing_settings then + final_exclude_settings = existing_settings + end + end + end + + -- Check if we have any new exclusions to add + local has_new_excludes = false + for target_index, target_excludes in pairs(excluded_views) do + for view_index, excluded in pairs(target_excludes) do + if excluded then + has_new_excludes = true + break + end + end + if has_new_excludes then break end + end + + -- If no new exclusions and we're saving globally, don't modify the file + if not has_new_excludes and save_global then + return + end + + -- If no new exclusions and not saving globally, remove the ROM-specific file + if not has_new_excludes and not save_global then + os.remove(filename) + return + end + + if not stat then + lfs.mkdir(path) + stat = lfs.attributes(path) + end + + -- Add/update current ROM's exclusions to the final settings + local render_targets = manager.machine.render.targets + for target_index, target_excludes in pairs(excluded_views) do + local target = render_targets[target_index] + if target then + -- Get existing exclusions for this target (for global saves) + local existing_target_excludes = {} + if save_global and final_exclude_settings[tostring(target_index)] then + existing_target_excludes = final_exclude_settings[tostring(target_index)] + end + + -- Build new exclusion list for this target + local excluded_list = {} + + -- Add existing exclusions first (only for global saves) + if save_global then + for _, view_name in pairs(existing_target_excludes) do + excluded_list[#excluded_list + 1] = view_name + end + end + + -- Add new exclusions, avoiding duplicates + for view_index, excluded in pairs(target_excludes) do + if excluded then + local view_name = target.view_names[view_index] + local already_exists = false + for _, existing_view in pairs(excluded_list) do + if existing_view == view_name then + already_exists = true + break + end + end + if not already_exists then + excluded_list[#excluded_list + 1] = view_name + end + end + end + + -- Only save if we have exclusions for this target + if #excluded_list > 0 then + final_exclude_settings[tostring(target_index)] = excluded_list + elseif not save_global then + -- For ROM-specific saves, if no exclusions, remove the target entry + final_exclude_settings[tostring(target_index)] = nil + end + end + end + + -- Check if final settings has any content + local has_any_excludes = next(final_exclude_settings) ~= nil + + if not has_any_excludes then + os.remove(filename) + else + -- try to write the file with pretty formatting + local json = require('json') + local file = io.open(filename, 'w') + if not file then + emu.print_error(string.format('Error saving view exclude settings: error opening file "%s" for writing', filename)) + else + file:write('{\n') + local target_keys = {} + for k in pairs(final_exclude_settings) do + table.insert(target_keys, k) + end + table.sort(target_keys, function(a, b) return tonumber(a) < tonumber(b) end) + + for i, target_key in ipairs(target_keys) do + local excluded_list = final_exclude_settings[target_key] + file:write(string.format(' "%s": [\n', target_key)) + + for j, view_name in ipairs(excluded_list) do + local comma = (j < #excluded_list) and ',' or '' + file:write(string.format(' "%s"%s\n', view_name, comma)) + end + + local target_comma = (i < #target_keys) and ',' or '' + file:write(string.format(' ]%s\n', target_comma)) + end + file:write('}\n') + file:close() + end + end +end + +function lib:save_settings(switch_hotkeys, cycle_hotkeys, config_mode) + -- Determine where to save based on config mode + local save_global = true -- Default to global + + if config_mode == 'rom_specific' then + save_global = false -- Only ROM-specific mode saves to ROM files + elseif config_mode == 'global_with_overrides' then + save_global = true -- Global with overrides saves to global (new defaults) + -- config_mode == 'global' uses default save_global = true + end + + self:save_settings_to_file(switch_hotkeys, cycle_hotkeys, save_global) +end + +function lib:save_excluded_views(excluded_views, config_mode) + -- Determine where to save based on config mode + local save_global = true -- Default to global + + if config_mode == 'rom_specific' then + save_global = false -- Only ROM-specific mode saves to ROM files + elseif config_mode == 'global_with_overrides' then + save_global = true -- Global with overrides saves to global (new defaults) + -- config_mode == 'global' uses default save_global = true + end + + self:save_excluded_views_to_file(excluded_views, save_global) +end + +-- New function to copy current settings to ROM files while preserving global files +function lib:copy_to_rom_settings(switch_hotkeys, cycle_hotkeys, excluded_views) + -- Save current settings as ROM-specific without removing global files + self:save_settings_to_file(switch_hotkeys, cycle_hotkeys, false) -- false = ROM file + self:save_excluded_views_to_file(excluded_views, false) -- false = ROM file +end + +-- New function to check which settings are currently being used +function lib:get_current_settings_info(config_mode) + local has_rom_settings_file = has_rom_settings() + local has_rom_excludes_file = has_rom_excludes() + + local info = { + using_rom_settings = false, + using_rom_excludes = false, + has_rom_settings = has_rom_settings_file, + has_rom_excludes = has_rom_excludes_file + } + + if config_mode == 'global_with_overrides' then + -- Only mark as "using ROM" if the files actually exist + info.using_rom_settings = has_rom_settings_file + info.using_rom_excludes = has_rom_excludes_file + elseif config_mode == 'rom_specific' then + -- In ROM-specific mode, we're always "using ROM settings" even if files don't exist yet + info.using_rom_settings = true + info.using_rom_excludes = true + end + -- For 'global' mode, everything stays false (using global) + + return info +end + +-- New function to remove ROM-specific overrides +function lib:remove_rom_overrides() + local path = settings_path() + local rom_settings_file = path .. '/' .. settings_filename(false) + local rom_excludes_file = path .. '/' .. exclude_filename(false) + + os.remove(rom_settings_file) + os.remove(rom_excludes_file) +end + +return lib \ No newline at end of file diff --git a/plugins/viewswitch/viewswitch_utils.lua b/plugins/viewswitch/viewswitch_utils.lua new file mode 100644 index 0000000..ab807da --- /dev/null +++ b/plugins/viewswitch/viewswitch_utils.lua @@ -0,0 +1,109 @@ +-- license:BSD-3-Clause +-- copyright-holders:Vas Crabb + +-- Shared utility functions for viewswitch plugin + +local lib = {} + +function lib:parse_layout_views() + -- Get the ROM name and artwork paths + local romname = manager.machine.system.name + local artpath = manager.machine.options.entries.artpath:value() + + local layout_views = {} + local layout_content = nil + + -- Try to find and read the default.lay file + for path in artpath:gmatch('[^;]+') do + -- Try direct file first + local layout_path = path .. '/' .. romname .. '/default.lay' + local file = io.open(layout_path, 'r') + if file then + layout_content = file:read('*all') + file:close() + break + end + + -- Try ZIP file (this is trickier - MAME handles ZIP internally) + -- We can try to use MAME's file loading if available + local zip_path = path .. '/' .. romname .. '.zip' + if lfs and lfs.attributes(zip_path) then + -- If we have lfs, the zip exists, but we can't easily extract from Lua + -- This would require MAME's internal file handling + -- For now, we'll skip ZIP files and recommend extracting them + end + end + + -- Parse the XML content to extract view names + if layout_content then + -- Simple XML parsing to find tags + for view_name in layout_content:gmatch(']*>') do + -- Avoid duplicates + local found = false + for _, existing in ipairs(layout_views) do + if existing == view_name then + found = true + break + end + end + if not found and view_name ~= "" then + table.insert(layout_views, view_name) + end + end + end + + return layout_views +end + +function lib:get_layout_view_indices(target, layout_view_names) + -- Match layout view names to actual view indices in the target + local view_indices = {} + local target_view_names = target.view_names + + for _, layout_view_name in ipairs(layout_view_names) do + for i, target_view_name in ipairs(target_view_names) do + if target_view_name == layout_view_name then + table.insert(view_indices, {index = i, name = target_view_name}) + break + end + end + end + + return view_indices +end + +function lib:get_layout_only_views(target) + -- Get view names from the layout file + local layout_view_names = self:parse_layout_views() + + -- If no layout views found, return empty (will trigger fallback) + if #layout_view_names == 0 then + return {} + end + + -- Match them to actual view indices + local layout_views = self:get_layout_view_indices(target, layout_view_names) + + return layout_views +end + +function lib:get_filtered_views(target, plugin_settings) + if plugin_settings.layout_only_mode then + return self:get_layout_only_views(target) + else + -- Return all views in the same format + local all_views = {} + local view_names = target.view_names + for i, view_name in ipairs(view_names) do + table.insert(all_views, {index = i, name = view_name}) + end + return all_views + end +end + +return lib \ No newline at end of file