From 83bb9d29e3908c33e69c0892713e9c92a1565ea2 Mon Sep 17 00:00:00 2001 From: Matthew Berry Date: Thu, 6 Oct 2022 23:51:18 -0700 Subject: [PATCH] the start to a proper config editor --- src/crab/common/config.cr | 8 +- .../frontend/sdl_opengl_imgui_frontend.cr | 33 ++--- .../common/frontend/widgets/bios_selection.cr | 80 +++++++++++ .../common/frontend/widgets/config_editor.cr | 56 ++++++++ .../common/frontend/widgets/keybindings.cr | 124 +++++++----------- .../common/frontend/widgets/resolvable.cr | 13 ++ 6 files changed, 215 insertions(+), 99 deletions(-) create mode 100644 src/crab/common/frontend/widgets/bios_selection.cr create mode 100644 src/crab/common/frontend/widgets/config_editor.cr create mode 100644 src/crab/common/frontend/widgets/resolvable.cr diff --git a/src/crab/common/config.cr b/src/crab/common/config.cr index a63d90f..f125756 100644 --- a/src/crab/common/config.cr +++ b/src/crab/common/config.cr @@ -54,10 +54,16 @@ class Config class GBA include YAML::Serializable - property bios : String = "bios.bin" + property bios : String? + + DEFAULT_BIOS = Path["#{__DIR__}/../../../bios.bin"].normalize def initialize end + + def bios : String + @bios || DEFAULT_BIOS.to_s + end end class GBC diff --git a/src/crab/common/frontend/sdl_opengl_imgui_frontend.cr b/src/crab/common/frontend/sdl_opengl_imgui_frontend.cr index 6494938..4da1e5f 100644 --- a/src/crab/common/frontend/sdl_opengl_imgui_frontend.cr +++ b/src/crab/common/frontend/sdl_opengl_imgui_frontend.cr @@ -71,7 +71,7 @@ class SDLOpenGLImGuiFrontend < Frontend pause(true) if stubbed? @file_explorer = ImGui::FileExplorer.new @config - @keybindings = ImGui::Keybindings.new @config + @config_editor = ConfigEditor.new @config, @file_explorer end def run : NoReturn @@ -119,31 +119,21 @@ class SDLOpenGLImGuiFrontend < Frontend pause(false) 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 while event = SDL::Event.poll ImGui::SDL2.process_event(event) case event when SDL::Event::Keyboard - if @keybindings.wants_input? - @keybindings.key_released(event.sym) unless event.pressed? # pass on key release + next if @io.want_capture_keyboard # let ImGui handle keyboard input when focused + 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) case event.sym 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::Q then exit end - elsif input = @keybindings[event.sym]? + elsif input = @config_editor.keybindings[event.sym]? @controller.handle_input(input, event.pressed?) elsif event.sym == LibSDL::Keycode::TAB @controller.toggle_sync if event.pressed? @@ -193,8 +183,7 @@ class SDLOpenGLImGuiFrontend < Frontend private def show_menu_bar? : Bool window_focused = LibSDL.get_mouse_focus == @window.to_unsafe 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) || dialog_open + res = stubbed? || (window_focused && !mouse_timed_out) LibSDL.show_cursor(res) res end @@ -206,14 +195,11 @@ class SDLOpenGLImGuiFrontend < Frontend overlay_height = 10.0 open_rom_selection = false - open_bios_selection = false - open_keybindings = false if show_menu_bar? ImGui.main_menu_bar do ImGui.menu "File" do 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 @config.recents.each do |recent| load_new_rom(recent) if ImGui.menu_item recent @@ -225,7 +211,7 @@ class SDLOpenGLImGuiFrontend < Frontend end end ImGui.separator - open_keybindings = ImGui.menu_item "Keybindings" + @config_editor.open = true if ImGui.menu_item "Settings" ImGui.separator exit if ImGui.menu_item "Exit", "Ctrl+Q" end @@ -265,11 +251,8 @@ class SDLOpenGLImGuiFrontend < Frontend @file_explorer.render("ROM", open_rom_selection, ROM_EXTENSIONS) do |path| load_new_rom(path.to_s) 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 ImGui.set_next_window_pos(ImGui::ImVec2.new 10, overlay_height) diff --git a/src/crab/common/frontend/widgets/bios_selection.cr b/src/crab/common/frontend/widgets/bios_selection.cr new file mode 100644 index 0000000..4c441c6 --- /dev/null +++ b/src/crab/common/frontend/widgets/bios_selection.cr @@ -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 diff --git a/src/crab/common/frontend/widgets/config_editor.cr b/src/crab/common/frontend/widgets/config_editor.cr new file mode 100644 index 0000000..235635b --- /dev/null +++ b/src/crab/common/frontend/widgets/config_editor.cr @@ -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 diff --git a/src/crab/common/frontend/widgets/keybindings.cr b/src/crab/common/frontend/widgets/keybindings.cr index 14a212d..c5adba8 100644 --- a/src/crab/common/frontend/widgets/keybindings.cr +++ b/src/crab/common/frontend/widgets/keybindings.cr @@ -1,82 +1,60 @@ -module ImGui - class Keybindings - POPUP_NAME = "Keybindings" - BUTTON_SIZE = ImGui::ImVec2.new(32, 0) +require "./resolvable" - @config : Config - @open = false - @selection : Input? = nil - @editing_keycodes : Hash(LibSDL::Keycode, Input) = {} of LibSDL::Keycode => Input +class Keybindings < Resolvable + BUTTON_SIZE = ImGui::ImVec2.new(32, 0) - delegate :[]?, to: @config.keybindings + @config : Config + @selection : Input? = nil + @editing_keycodes : Hash(LibSDL::Keycode, Input) = {} of LibSDL::Keycode => Input - def initialize(@config : Config) - overwrite_hash(@editing_keycodes, @config.keybindings) - end + def initialize(@config : Config) + @hovered_button_color = ImGui.get_style_color_vec4(ImGui::ImGuiCol::ButtonHovered) + end - def open? : Bool - @open - end + delegate :[]?, to: @config.keybindings - def wants_input? : Bool - @open && !@selection.nil? - end + def wants_input? : Bool + @visible && !@selection.nil? + end - def key_released(keycode : LibSDL::Keycode) : Nil - if selection = @selection - @editing_keycodes.reject!(@editing_keycodes.key_for?(selection)) - @editing_keycodes[keycode] = selection - @selection = Input.from_value?(selection.value + 1) - else - puts "Something went wrong when setting keybinding.." - end - end - - def render(open_popup : Bool) : 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| - selected = @selection == input - keycode = @editing_keycodes.key_for?(input) - button_text = keycode ? String.new(LibSDL.get_key_name(keycode)) : "" - x_pos = ImGui.get_window_content_region_max.x - BUTTON_SIZE.x - 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) - @selection = input - end - ImGui.pop_style_color if selected - end - apply if ImGui.button "Apply" - ImGui.same_line - close if ImGui.button "Cancel" - end - end - - private def overwrite_hash(to_hash : Hash(K, V), from_hash : Hash(K, V)) : Hash(K, V) forall K, V - to_hash.clear - from_hash.each { |key, val| to_hash[key] = val } - to_hash - 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 + def key_released(keycode : LibSDL::Keycode) : Nil + if selection = @selection + @editing_keycodes.reject!(@editing_keycodes.key_for?(selection)) + @editing_keycodes[keycode] = selection + @selection = Input.from_value?(selection.value + 1) + else + puts "Something went wrong when setting keybinding.." end end + + def render : Nil + Input.each do |input| + selected = @selection == input + keycode = @editing_keycodes.key_for?(input) + button_text = keycode ? String.new(LibSDL.get_key_name(keycode)) : "" + ImGui.push_style_color(ImGui::ImGuiCol::Button, @hovered_button_color) if selected + if ImGui.button(button_text, BUTTON_SIZE) + @selection = input + end + ImGui.pop_style_color if selected + ImGui.same_line + ImGui.text(input.to_s) + 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 + to_hash.clear + from_hash.each { |key, val| to_hash[key] = val } + to_hash + end end diff --git a/src/crab/common/frontend/widgets/resolvable.cr b/src/crab/common/frontend/widgets/resolvable.cr new file mode 100644 index 0000000..7c4bc31 --- /dev/null +++ b/src/crab/common/frontend/widgets/resolvable.cr @@ -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