From d2da15d4a04ceb1aec6d8541d10b11d5481727e4 Mon Sep 17 00:00:00 2001 From: arghs15 <141374139+arghs15@users.noreply.github.com> Date: Thu, 7 Aug 2025 11:41:53 +1200 Subject: [PATCH] Update viewswitch 1. add three modes: Global Only = all settings saved will be saved for all ROMs ROM Only = all settings saved will only save for that ROM. Global configs will be ignored Global Overrides: use ROM settings if exists, else fall back to Global New tab menu to exclude certain views New tab menu to exclude MAME views, and show only views contained in the ROMs lay file --- plugins/viewswitch/init.lua | 148 ++++++- plugins/viewswitch/viewswitch_menu.lua | 455 ++++++++++++++++++++-- plugins/viewswitch/viewswitch_persist.lua | 412 +++++++++++++++++++- plugins/viewswitch/viewswitch_utils.lua | 109 ++++++ 4 files changed, 1083 insertions(+), 41 deletions(-) create mode 100644 plugins/viewswitch/viewswitch_utils.lua 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