the start to a proper config editor

This commit is contained in:
Matthew Berry 2022-10-06 23:51:18 -07:00
parent 5e9720f9b0
commit 83bb9d29e3
6 changed files with 215 additions and 99 deletions

View file

@ -54,10 +54,16 @@ class Config
class GBA class GBA
include YAML::Serializable include YAML::Serializable
property bios : String = "bios.bin" property bios : String?
DEFAULT_BIOS = Path["#{__DIR__}/../../../bios.bin"].normalize
def initialize def initialize
end end
def bios : String
@bios || DEFAULT_BIOS.to_s
end
end end
class GBC class GBC

View file

@ -71,7 +71,7 @@ class SDLOpenGLImGuiFrontend < Frontend
pause(true) if stubbed? pause(true) if stubbed?
@file_explorer = ImGui::FileExplorer.new @config @file_explorer = ImGui::FileExplorer.new @config
@keybindings = ImGui::Keybindings.new @config @config_editor = ConfigEditor.new @config, @file_explorer
end end
def run : NoReturn def run : NoReturn
@ -119,31 +119,21 @@ class SDLOpenGLImGuiFrontend < Frontend
pause(false) pause(false)
end end
private def load_new_bios(bios : String) : Nil
if @controller.class == GBController
@config.gbc.bios = bios
elsif @controller.class == GBAController
@config.gba.bios = bios
else
abort "Internal error: Cannot set bios #{bios} for controller #{@controller}"
end
@config.commit
end
private def handle_input : Nil private def handle_input : Nil
while event = SDL::Event.poll while event = SDL::Event.poll
ImGui::SDL2.process_event(event) ImGui::SDL2.process_event(event)
case event case event
when SDL::Event::Keyboard when SDL::Event::Keyboard
if @keybindings.wants_input? next if @io.want_capture_keyboard # let ImGui handle keyboard input when focused
@keybindings.key_released(event.sym) unless event.pressed? # pass on key release if @config_editor.keybindings.wants_input?
@config_editor.keybindings.key_released(event.sym) unless event.pressed? # pass on key release
elsif event.mod.includes?(LibSDL::Keymod::LCTRL) elsif event.mod.includes?(LibSDL::Keymod::LCTRL)
case event.sym case event.sym
when LibSDL::Keycode::P then pause(!@pause) unless event.pressed? when LibSDL::Keycode::P then pause(!@pause) unless event.pressed?
when LibSDL::Keycode::F then @window.fullscreen = (@fullscreen = !@fullscreen) unless event.pressed? when LibSDL::Keycode::F then @window.fullscreen = (@fullscreen = !@fullscreen) unless event.pressed?
when LibSDL::Keycode::Q then exit when LibSDL::Keycode::Q then exit
end end
elsif input = @keybindings[event.sym]? elsif input = @config_editor.keybindings[event.sym]?
@controller.handle_input(input, event.pressed?) @controller.handle_input(input, event.pressed?)
elsif event.sym == LibSDL::Keycode::TAB elsif event.sym == LibSDL::Keycode::TAB
@controller.toggle_sync if event.pressed? @controller.toggle_sync if event.pressed?
@ -193,8 +183,7 @@ class SDLOpenGLImGuiFrontend < Frontend
private def show_menu_bar? : Bool private def show_menu_bar? : Bool
window_focused = LibSDL.get_mouse_focus == @window.to_unsafe window_focused = LibSDL.get_mouse_focus == @window.to_unsafe
mouse_timed_out = LibSDL.get_ticks - @last_mouse_motion > 3000 # 3 second timeout mouse_timed_out = LibSDL.get_ticks - @last_mouse_motion > 3000 # 3 second timeout
dialog_open = @file_explorer.open? || @keybindings.open? res = stubbed? || (window_focused && !mouse_timed_out)
res = stubbed? || (window_focused && !mouse_timed_out) || dialog_open
LibSDL.show_cursor(res) LibSDL.show_cursor(res)
res res
end end
@ -206,14 +195,11 @@ class SDLOpenGLImGuiFrontend < Frontend
overlay_height = 10.0 overlay_height = 10.0
open_rom_selection = false open_rom_selection = false
open_bios_selection = false
open_keybindings = false
if show_menu_bar? if show_menu_bar?
ImGui.main_menu_bar do ImGui.main_menu_bar do
ImGui.menu "File" do ImGui.menu "File" do
open_rom_selection = ImGui.menu_item "Open ROM" open_rom_selection = ImGui.menu_item "Open ROM"
open_bios_selection = ImGui.menu_item "Select BIOS" unless stubbed?
ImGui.menu "Recent", @config.recents.size > 0 do ImGui.menu "Recent", @config.recents.size > 0 do
@config.recents.each do |recent| @config.recents.each do |recent|
load_new_rom(recent) if ImGui.menu_item recent load_new_rom(recent) if ImGui.menu_item recent
@ -225,7 +211,7 @@ class SDLOpenGLImGuiFrontend < Frontend
end end
end end
ImGui.separator ImGui.separator
open_keybindings = ImGui.menu_item "Keybindings" @config_editor.open = true if ImGui.menu_item "Settings"
ImGui.separator ImGui.separator
exit if ImGui.menu_item "Exit", "Ctrl+Q" exit if ImGui.menu_item "Exit", "Ctrl+Q"
end end
@ -265,11 +251,8 @@ class SDLOpenGLImGuiFrontend < Frontend
@file_explorer.render("ROM", open_rom_selection, ROM_EXTENSIONS) do |path| @file_explorer.render("ROM", open_rom_selection, ROM_EXTENSIONS) do |path|
load_new_rom(path.to_s) load_new_rom(path.to_s)
end end
@file_explorer.render("BIOS", open_bios_selection) do |path|
load_new_bios(path.to_s)
end
@keybindings.render(open_keybindings) @config_editor.render
if @enable_overlay if @enable_overlay
ImGui.set_next_window_pos(ImGui::ImVec2.new 10, overlay_height) ImGui.set_next_window_pos(ImGui::ImVec2.new 10, overlay_height)

View file

@ -0,0 +1,80 @@
require "./resolvable"
class BiosSelection < Resolvable
RED_TEXT_COL = ImGui::ImVec4.new(1, 0.5, 0.5, 1)
@config : Config
@file_explorer : ImGui::FileExplorer
@gbc_bios_text_buffer = ImGui::TextBuffer.new(128)
@gba_bios_text_buffer = ImGui::TextBuffer.new(128)
@gbc_bios_text_buffer_valid = false
@gba_bios_text_buffer_valid = false
@run_bios : Bool = false # initialized on reset
def initialize(@config : Config, @file_explorer : ImGui::FileExplorer)
end
def render : Nil
ImGui.text("GBC BIOS File:")
ImGui.same_line
gbc_bios_text_buffer_valid = @gbc_bios_text_buffer_valid
ImGui.push_style_color(ImGui::ImGuiCol::Text, RED_TEXT_COL) unless gbc_bios_text_buffer_valid
ImGui.input_text_with_hint("##gbc_bios", "optional", @gbc_bios_text_buffer, ImGui::ImGuiInputTextFlags::CallbackAlways) do
@gbc_bios_text_buffer_valid = @gbc_bios_text_buffer.bytesize == 0 || File.file?(@gbc_bios_text_buffer.to_s)
0 # allow input to proceed
end
ImGui.pop_style_color unless gbc_bios_text_buffer_valid
ImGui.same_line
gbc_bios_browse = ImGui.button("Browse##gbc_bios")
ImGui.text("GBA BIOS File:")
ImGui.same_line
gba_bios_text_buffer_valid = @gba_bios_text_buffer_valid
ImGui.push_style_color(ImGui::ImGuiCol::Text, RED_TEXT_COL) unless gba_bios_text_buffer_valid
ImGui.input_text_with_hint("##gba_bios", "optional", @gba_bios_text_buffer, ImGui::ImGuiInputTextFlags::CallbackAlways) do |data|
@gba_bios_text_buffer_valid = @gba_bios_text_buffer.bytesize == 0 || File.file?(@gba_bios_text_buffer.to_s)
0 # allow input to proceed
end
ImGui.pop_style_color unless gba_bios_text_buffer_valid
ImGui.same_line
gba_bios_browse = ImGui.button("Browse##gba_bios")
ImGui.indent(106) # align with text boxes above
ImGui.checkbox("Run BIOS intro", pointerof(@run_bios))
ImGui.unindent(106)
@file_explorer.render("GBC BIOS", gbc_bios_browse) do |path|
@gbc_bios_text_buffer.clear
@gbc_bios_text_buffer.write(path.to_s.to_slice)
@gbc_bios_text_buffer_valid = @gbc_bios_text_buffer.bytesize == 0 || File.file?(@gbc_bios_text_buffer.to_s)
end
@file_explorer.render("GBA BIOS", gba_bios_browse) do |path|
@gba_bios_text_buffer.clear
@gba_bios_text_buffer.write(path.to_s.to_slice)
@gba_bios_text_buffer_valid = @gba_bios_text_buffer.bytesize == 0 || File.file?(@gba_bios_text_buffer.to_s)
end
end
def reset : Nil
@gbc_bios_text_buffer.clear
@gba_bios_text_buffer.clear
if gbc_bios = @config.gbc.bios
@gbc_bios_text_buffer.write(gbc_bios.to_slice)
@gbc_bios_text_buffer_valid = File.file?(@gbc_bios_text_buffer.to_s)
end
if gba_bios = @config.gba.bios
@gba_bios_text_buffer.write(gba_bios.to_slice) if Path[gba_bios].normalize != Path[Config::GBA::DEFAULT_BIOS].normalize
@gba_bios_text_buffer_valid = File.file?(@gba_bios_text_buffer.to_s)
end
@run_bios = @config.run_bios
end
def apply : Nil
@config.gbc.bios = nil
@config.gbc.bios = @gbc_bios_text_buffer.to_s if @gbc_bios_text_buffer.bytesize != 0
@config.gba.bios = nil
@config.gba.bios = @gba_bios_text_buffer.to_s if @gba_bios_text_buffer.bytesize != 0
@config.run_bios = @run_bios
end
end

View file

@ -0,0 +1,56 @@
class ConfigEditor
getter keybindings : Keybindings
property open : Bool = false
@previously_open : Bool = false
def initialize(@config : Config, @file_explorer : ImGui::FileExplorer)
@bios_selection = BiosSelection.new(@config, @file_explorer)
@keybindings = Keybindings.new @config
end
def render : Nil
reset if @open && !@previously_open
@previously_open = @open
if @open
ImGui.begin("Settings", pointerof(@open), flags: ImGui::ImGuiWindowFlags::AlwaysAutoResize)
apply if ImGui.button("Apply")
ImGui.same_line
reset if ImGui.button("Revert")
ImGui.same_line
if ImGui.button("OK")
apply
@open = false
end
ImGui.separator
ImGui.tab_bar("SettingsTabBar") do
render_resolvable_tab(@bios_selection, "BIOS")
render_resolvable_tab(@keybindings, "Keybindings")
end
ImGui.end
end
end
private def reset : Nil
@bios_selection.reset
@keybindings.reset
end
private def apply : Nil
@bios_selection.apply
@keybindings.apply
@config.commit
end
# Render a Resolvable in a tab item and set its `vislble` property.
private def render_resolvable_tab(res : Resolvable, name : String) : Nil
if res.visible = ImGui.begin_tab_item(name)
ImGui.group do
res.render
end
ImGui.end_tab_item
end
end
end

View file

@ -1,25 +1,20 @@
module ImGui require "./resolvable"
class Keybindings
POPUP_NAME = "Keybindings" class Keybindings < Resolvable
BUTTON_SIZE = ImGui::ImVec2.new(32, 0) BUTTON_SIZE = ImGui::ImVec2.new(32, 0)
@config : Config @config : Config
@open = false
@selection : Input? = nil @selection : Input? = nil
@editing_keycodes : Hash(LibSDL::Keycode, Input) = {} of LibSDL::Keycode => Input @editing_keycodes : Hash(LibSDL::Keycode, Input) = {} of LibSDL::Keycode => Input
def initialize(@config : Config)
@hovered_button_color = ImGui.get_style_color_vec4(ImGui::ImGuiCol::ButtonHovered)
end
delegate :[]?, to: @config.keybindings delegate :[]?, to: @config.keybindings
def initialize(@config : Config)
overwrite_hash(@editing_keycodes, @config.keybindings)
end
def open? : Bool
@open
end
def wants_input? : Bool def wants_input? : Bool
@open && !@selection.nil? @visible && !@selection.nil?
end end
def key_released(keycode : LibSDL::Keycode) : Nil def key_released(keycode : LibSDL::Keycode) : Nil
@ -32,51 +27,34 @@ module ImGui
end end
end end
def render(open_popup : Bool) : Nil def render : Nil
@open ||= open_popup
if open_popup
overwrite_hash(@editing_keycodes, @config.keybindings)
ImGui.open_popup(POPUP_NAME)
end
center = ImGui.get_main_viewport.get_center
ImGui.set_next_window_pos(center, ImGui::ImGuiCond::Appearing, ImGui::ImVec2.new(0.5, 0.5))
hovered_button_color = ImGui.get_style_color_vec4(ImGui::ImGuiCol::ButtonHovered)
ImGui.popup_modal(POPUP_NAME, flags: ImGui::ImGuiWindowFlags::AlwaysAutoResize) do
Input.each do |input| Input.each do |input|
selected = @selection == input selected = @selection == input
keycode = @editing_keycodes.key_for?(input) keycode = @editing_keycodes.key_for?(input)
button_text = keycode ? String.new(LibSDL.get_key_name(keycode)) : "" button_text = keycode ? String.new(LibSDL.get_key_name(keycode)) : ""
x_pos = ImGui.get_window_content_region_max.x - BUTTON_SIZE.x ImGui.push_style_color(ImGui::ImGuiCol::Button, @hovered_button_color) if selected
ImGui.text(input.to_s)
ImGui.same_line(x_pos)
ImGui.push_style_color(ImGui::ImGuiCol::Button, hovered_button_color) if selected
if ImGui.button(button_text, BUTTON_SIZE) if ImGui.button(button_text, BUTTON_SIZE)
@selection = input @selection = input
end end
ImGui.pop_style_color if selected ImGui.pop_style_color if selected
end
apply if ImGui.button "Apply"
ImGui.same_line ImGui.same_line
close if ImGui.button "Cancel" ImGui.text(input.to_s)
end end
end end
def reset : Nil
@selection = nil
overwrite_hash(@editing_keycodes, @config.keybindings)
end
def apply : Nil
overwrite_hash(@config.keybindings, @editing_keycodes)
@selection = nil
end
private def overwrite_hash(to_hash : Hash(K, V), from_hash : Hash(K, V)) : Hash(K, V) forall K, V private def overwrite_hash(to_hash : Hash(K, V), from_hash : Hash(K, V)) : Hash(K, V) forall K, V
to_hash.clear to_hash.clear
from_hash.each { |key, val| to_hash[key] = val } from_hash.each { |key, val| to_hash[key] = val }
to_hash to_hash
end end
private def apply : Nil
overwrite_hash(@config.keybindings, @editing_keycodes)
@config.commit
close
end
private def close : Nil
@open = false
@selection = nil
ImGui.close_current_popup
end
end
end end

View file

@ -0,0 +1,13 @@
# Represents a widget which stores temporary state that can be committed or
# dropped on request.
abstract class Resolvable
# Whether the widget is visible.
property visible : Bool = false
# Called once per frame for rendering the widget.
abstract def render : Nil
# Called to indicate the widget should be reset.
abstract def reset : Nil
# Called to indicate the selection should be written back to the config.
abstract def apply : Nil
end