diff --git a/dependencies/ih264d/CMakeLists.txt b/dependencies/ih264d/CMakeLists.txt index 64ac0931..a8ef3776 100644 --- a/dependencies/ih264d/CMakeLists.txt +++ b/dependencies/ih264d/CMakeLists.txt @@ -182,7 +182,10 @@ target_sources(ih264d PRIVATE "decoder/arm/ih264d_function_selector_av8.c" "decoder/arm/ih264d_function_selector.c" ) -target_compile_options(ih264d PRIVATE -DARMV8) +target_compile_options(ih264d PRIVATE -DARMV8 $<$:-Wno-unused-command-line-argument>) +if(NOT MSVC) + set(CMAKE_ASM_FLAGS "${CFLAGS} -x assembler-with-cpp") +endif() if(APPLE) target_sources(ih264d PRIVATE "common/armv8/macos_arm_symbol_aliases.s") endif() diff --git a/src/config/CMakeLists.txt b/src/config/CMakeLists.txt index 61bd66ab..abcacc60 100644 --- a/src/config/CMakeLists.txt +++ b/src/config/CMakeLists.txt @@ -18,6 +18,7 @@ target_include_directories(CemuConfig PUBLIC "../") target_link_libraries(CemuConfig PRIVATE CemuCafe CemuCommon + CemuGui CemuUtil Boost::headers Boost::program_options diff --git a/src/config/CemuConfig.cpp b/src/config/CemuConfig.cpp index b40c5cf3..8022d139 100644 --- a/src/config/CemuConfig.cpp +++ b/src/config/CemuConfig.cpp @@ -1,4 +1,5 @@ #include "config/CemuConfig.h" +#include "WindowSystem.h" #include "util/helpers/helpers.h" #include "config/ActiveSettings.h" @@ -352,6 +353,15 @@ void CemuConfig::Load(XMLConfigParser& parser) dsu_client.host = dsuc.get_attribute("host", dsu_client.host); dsu_client.port = dsuc.get_attribute("port", dsu_client.port); + // hotkeys + auto xml_hotkeys = parser.get("Hotkeys"); + hotkeys.modifiers = xml_hotkeys.get("modifiers", WindowSystem::getDefaultHotkeyConfig(WindowSystem::HotKey::MODIFIERS)); + hotkeys.exitFullscreen = xml_hotkeys.get("ExitFullscreen", WindowSystem::getDefaultHotkeyConfig(WindowSystem::HotKey::EXIT_FULLSCREEN)); + hotkeys.toggleFullscreen = xml_hotkeys.get("ToggleFullscreen", WindowSystem::getDefaultHotkeyConfig(WindowSystem::HotKey::TOGGLE_FULLSCREEN)); + hotkeys.toggleFullscreenAlt = xml_hotkeys.get("ToggleFullscreenAlt", WindowSystem::getDefaultHotkeyConfig(WindowSystem::HotKey::TOGGLE_FULLSCREEN_ALT)); + hotkeys.takeScreenshot = xml_hotkeys.get("TakeScreenshot", WindowSystem::getDefaultHotkeyConfig(WindowSystem::HotKey::TAKE_SCREENSHOT)); + hotkeys.toggleFastForward = xml_hotkeys.get("ToggleFastForward", WindowSystem::getDefaultHotkeyConfig(WindowSystem::HotKey::TOGGLE_FAST_FORWARD)); + // emulatedusbdevices auto usbdevices = parser.get("EmulatedUsbDevices"); emulated_usb_devices.emulate_skylander_portal = usbdevices.get("EmulateSkylanderPortal", emulated_usb_devices.emulate_skylander_portal); @@ -555,6 +565,15 @@ void CemuConfig::Save(XMLConfigParser& parser) dsuc.set_attribute("host", dsu_client.host); dsuc.set_attribute("port", dsu_client.port); + // hotkeys + auto xml_hotkeys = config.set("Hotkeys"); + xml_hotkeys.set("modifiers", hotkeys.modifiers); + xml_hotkeys.set("ExitFullscreen", hotkeys.exitFullscreen); + xml_hotkeys.set("ToggleFullscreen", hotkeys.toggleFullscreen); + xml_hotkeys.set("ToggleFullscreenAlt", hotkeys.toggleFullscreenAlt); + xml_hotkeys.set("TakeScreenshot", hotkeys.takeScreenshot); + xml_hotkeys.set("ToggleFastForward", hotkeys.toggleFastForward); + // emulated usb devices auto usbdevices = config.set("EmulatedUsbDevices"); usbdevices.set("EmulateSkylanderPortal", emulated_usb_devices.emulate_skylander_portal.GetValue()); diff --git a/src/config/CemuConfig.h b/src/config/CemuConfig.h index 7fc8e48c..95fea00b 100644 --- a/src/config/CemuConfig.h +++ b/src/config/CemuConfig.h @@ -188,6 +188,49 @@ enum class CrashDump ENABLE_ENUM_ITERATORS(CrashDump, CrashDump::Disabled, CrashDump::Enabled); #endif +typedef union +{ + struct + { + uint16 key : 13; // enough bits for all keycodes + uint16 alt : 1; + uint16 ctrl : 1; + uint16 shift : 1; + }; + uint16 raw; +} uKeyboardHotkey; + +typedef sint16 ControllerHotkey_t; + +struct sHotkeyCfg +{ + static constexpr uint8 keyboardNone{0}; + static constexpr sint8 controllerNone{-1}; // no enums to work with, but buttons start from 0 + + uKeyboardHotkey keyboard{keyboardNone}; + ControllerHotkey_t controller{controllerNone}; + + /* for defaults */ + sHotkeyCfg(const uKeyboardHotkey& keyboard = {keyboardNone}, const ControllerHotkey_t& controller = {controllerNone}) : keyboard(keyboard), controller(controller) {}; + + /* for reading from xml */ + sHotkeyCfg(const char* xml_values) + { + std::istringstream iss(xml_values); + iss >> keyboard.raw >> controller; + } +}; + +template <> +struct fmt::formatter : formatter +{ + template + auto format(const sHotkeyCfg c, FormatContext &ctx) const { + std::string xml_values = fmt::format("{} {}", c.keyboard.raw, c.controller); + return formatter::format(xml_values, ctx); + } +}; + template <> struct fmt::formatter : formatter { template @@ -496,6 +539,17 @@ struct CemuConfig ConfigValue port{ 26760 }; }dsu_client{}; + // hotkeys + struct + { + sHotkeyCfg modifiers; + sHotkeyCfg toggleFullscreen; + sHotkeyCfg toggleFullscreenAlt; + sHotkeyCfg exitFullscreen; + sHotkeyCfg takeScreenshot; + sHotkeyCfg toggleFastForward; + } hotkeys{}; + // debug ConfigValueBounds crash_dump{ CrashDump::Disabled }; ConfigValue gdb_port{ 1337 }; diff --git a/src/config/XMLConfig.h b/src/config/XMLConfig.h index 2a32dc56..dbec8a6e 100644 --- a/src/config/XMLConfig.h +++ b/src/config/XMLConfig.h @@ -7,6 +7,9 @@ #include #include +template +concept HasConstCharConstructor = requires { T(std::declval()); }; + class XMLConfigParser { public: @@ -43,7 +46,7 @@ public: return element->Int64Text(default_value); else if constexpr (std::is_same_v) // doesnt support real uint64... return (uint64)element->Int64Text((sint64)default_value); - else if constexpr (std::is_same_v || std::is_same_v) + else if constexpr (std::is_same_v || std::is_same_v || HasConstCharConstructor) { const char* text = element->GetText(); return text ? text : default_value; diff --git a/src/gui/interface/WindowSystem.h b/src/gui/interface/WindowSystem.h index f3122cd6..48a30e1a 100644 --- a/src/gui/interface/WindowSystem.h +++ b/src/gui/interface/WindowSystem.h @@ -1,5 +1,8 @@ #pragma once +#include "config/CemuConfig.h" +#include "input/api/ControllerState.h" + namespace WindowSystem { struct WindowHandleInfo @@ -118,4 +121,19 @@ namespace WindowSystem void refreshGameList(); bool isFullScreen(); + + void captureInput(const ControllerState& currentState, const ControllerState& lastState); + + enum class HotKey + { + + MODIFIERS, + EXIT_FULLSCREEN, + TOGGLE_FULLSCREEN, + TOGGLE_FULLSCREEN_ALT, + TAKE_SCREENSHOT, + TOGGLE_FAST_FORWARD, + }; + + sHotkeyCfg getDefaultHotkeyConfig(HotKey hotKey); }; // namespace WindowSystem diff --git a/src/gui/wxgui/CMakeLists.txt b/src/gui/wxgui/CMakeLists.txt index fa35baa2..bacbdd1b 100644 --- a/src/gui/wxgui/CMakeLists.txt +++ b/src/gui/wxgui/CMakeLists.txt @@ -72,6 +72,8 @@ add_library(CemuWxGui helpers/wxLogEvent.h helpers/wxWayland.cpp helpers/wxWayland.h + input/HotkeySettings.cpp + input/HotkeySettings.h input/InputAPIAddWindow.cpp input/InputAPIAddWindow.h input/InputSettings2.cpp diff --git a/src/gui/wxgui/CemuApp.cpp b/src/gui/wxgui/CemuApp.cpp index 0fd26cb0..5d4b1668 100644 --- a/src/gui/wxgui/CemuApp.cpp +++ b/src/gui/wxgui/CemuApp.cpp @@ -10,6 +10,7 @@ #include "input/InputManager.h" #include "wxgui/helpers/wxHelpers.h" #include "Cemu/ncrypto/ncrypto.h" +#include "wxgui/input/HotkeySettings.h" #if BOOST_OS_LINUX && HAS_WAYLAND #include "wxgui/helpers/wxWayland.h" @@ -337,6 +338,8 @@ bool CemuApp::OnInit() std::unique_lock lock(g_mutex); g_window_info.app_active = true; + HotkeySettings::Init(m_mainFrame); + SetTopWindow(m_mainFrame); m_mainFrame->Show(); diff --git a/src/gui/wxgui/MainWindow.cpp b/src/gui/wxgui/MainWindow.cpp index c077d557..59a1e6a1 100644 --- a/src/gui/wxgui/MainWindow.cpp +++ b/src/gui/wxgui/MainWindow.cpp @@ -41,6 +41,7 @@ #include "wxgui/helpers/wxHelpers.h" #include "Cafe/HW/Latte/Renderer/Vulkan/VsyncDriver.h" #include "wxgui/input/InputSettings2.h" +#include "wxgui/input/HotkeySettings.h" #include "input/InputManager.h" #if BOOST_OS_WINDOWS @@ -92,6 +93,7 @@ enum MAINFRAME_MENU_ID_OPTIONS_GENERAL2, MAINFRAME_MENU_ID_OPTIONS_AUDIO, MAINFRAME_MENU_ID_OPTIONS_INPUT, + MAINFRAME_MENU_ID_OPTIONS_HOTKEY, MAINFRAME_MENU_ID_OPTIONS_MAC_SETTINGS, // options -> account MAINFRAME_MENU_ID_OPTIONS_ACCOUNT_1 = 20350, @@ -190,6 +192,7 @@ EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_GENERAL, MainWindow::OnOptionsInput) EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_GENERAL2, MainWindow::OnOptionsInput) EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_AUDIO, MainWindow::OnOptionsInput) EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_INPUT, MainWindow::OnOptionsInput) +EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_HOTKEY, MainWindow::OnOptionsInput) EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_MAC_SETTINGS, MainWindow::OnOptionsInput) // tools menu EVT_MENU(MAINFRAME_MENU_ID_TOOLS_MEMORY_SEARCHER, MainWindow::OnToolsInput) @@ -934,6 +937,12 @@ void MainWindow::OnOptionsInput(wxCommandEvent& event) break; } + case MAINFRAME_MENU_ID_OPTIONS_HOTKEY: + { + auto* frame = new HotkeySettings(this); + frame->Show(); + break; + } } } @@ -1440,102 +1449,6 @@ void MainWindow::OnSetWindowTitle(wxCommandEvent& event) this->SetTitle(event.GetString()); } -std::optional GenerateScreenshotFilename(bool isDRC) -{ - fs::path screendir = ActiveSettings::GetUserDataPath("screenshots"); - // build screenshot name with format Screenshot_YYYY-MM-DD_HH-MM-SS[_GamePad].png - // if the file already exists add a suffix counter (_2.png, _3.png etc) - std::time_t time_t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); - std::tm* tm = std::localtime(&time_t); - - std::string screenshotFileName = fmt::format("Screenshot_{:04}-{:02}-{:02}_{:02}-{:02}-{:02}", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec); - if (isDRC) - screenshotFileName.append("_GamePad"); - - fs::path screenshotPath; - for (sint32 i = 0; i < 999; i++) - { - screenshotPath = screendir; - if (i == 0) - screenshotPath.append(fmt::format("{}.png", screenshotFileName)); - else - screenshotPath.append(fmt::format("{}_{}.png", screenshotFileName, i + 1)); - - std::error_code ec; - bool exists = fs::exists(screenshotPath, ec); - - if (!ec && !exists) - return screenshotPath; - } - return std::nullopt; -} - -bool SaveScreenshotToFile(const fs::path& imagePath, const wxImage& image) -{ - std::error_code ec; - fs::create_directories(imagePath.parent_path(), ec); - if (ec) - return false; - - // suspend wxWidgets logging for the lifetime this object, to prevent a message box if wxImage::SaveFile fails - wxLogNull _logNo; - return image.SaveFile(imagePath.wstring()); -} - -bool SaveScreenshotToClipboard(const wxImage& image) -{ - static std::mutex s_clipboardMutex; - bool success = false; - - s_clipboardMutex.lock(); - if (wxTheClipboard->Open()) - { - wxTheClipboard->SetData(new wxImageDataObject(image)); - wxTheClipboard->Close(); - success = true; - } - s_clipboardMutex.unlock(); - - return success; -} - -std::optional SaveScreenshot(std::vector data, int width, int height, bool mainWindow) -{ -#if BOOST_OS_WINDOWS - // on Windows wxWidgets uses OLE API for the clipboard - // to make this work we need to call OleInitialize() on the same thread - OleInitialize(nullptr); -#endif - bool save_screenshot = g_config.data().save_screenshot; - wxImage image(width, height, data.data(), true); - if (mainWindow) - { - if (SaveScreenshotToClipboard(image)) - { - if (!save_screenshot) - return "Screenshot saved to clipboard"; - } - else - { - return "Failed to open clipboard"; - } - } - if (save_screenshot) - { - auto imagePath = GenerateScreenshotFilename(mainWindow); - if (imagePath.has_value() && SaveScreenshotToFile(imagePath.value(), image)) - { - if (mainWindow) - return "Screenshot saved"; - } - else - { - return "Failed to save screenshot to file"; - } - } - return std::nullopt; -} - void MainWindow::OnKeyUp(wxKeyEvent& event) { event.Skip(); @@ -1543,13 +1456,7 @@ void MainWindow::OnKeyUp(wxKeyEvent& event) if (swkbd_hasKeyboardInputHook()) return; - const auto code = event.GetKeyCode(); - if (code == WXK_ESCAPE) - SetFullScreen(false); - else if (code == WXK_RETURN && event.AltDown() || code == WXK_F11) - SetFullScreen(!IsFullScreen()); - else if (code == WXK_F12 && g_renderer) - g_renderer->RequestScreenshot(SaveScreenshot); // async screenshot request + HotkeySettings::CaptureInput(event); } void MainWindow::OnKeyDown(wxKeyEvent& event) @@ -2286,6 +2193,7 @@ void MainWindow::RecreateMenu() #endif optionsMenu->Append(MAINFRAME_MENU_ID_OPTIONS_GENERAL2, _("&General settings")); optionsMenu->Append(MAINFRAME_MENU_ID_OPTIONS_INPUT, _("&Input settings")); + optionsMenu->Append(MAINFRAME_MENU_ID_OPTIONS_HOTKEY, _("&Hotkey settings")); optionsMenu->AppendSeparator(); optionsMenu->AppendSubMenu(m_optionsAccountMenu, _("&Active account")); diff --git a/src/gui/wxgui/input/HotkeySettings.cpp b/src/gui/wxgui/input/HotkeySettings.cpp new file mode 100644 index 00000000..e1d87b56 --- /dev/null +++ b/src/gui/wxgui/input/HotkeySettings.cpp @@ -0,0 +1,488 @@ +#include "wxgui/input/HotkeySettings.h" +#include "Cafe/HW/Latte/Renderer/Renderer.h" +#include "interface/WindowSystem.h" +#include +#include "input/InputManager.h" +#include "HotkeySettings.h" + +#include + +#if BOOST_OS_LINUX || BOOST_OS_MACOS +#include "resource/embedded/resources.h" +#endif + +std::optional GenerateScreenshotFilename(bool isDRC) +{ + fs::path screendir = ActiveSettings::GetUserDataPath("screenshots"); + // build screenshot name with format Screenshot_YYYY-MM-DD_HH-MM-SS[_GamePad].png + // if the file already exists add a suffix counter (_2.png, _3.png etc) + std::time_t time_t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + std::tm* tm = std::localtime(&time_t); + + std::string screenshotFileName = fmt::format("Screenshot_{:04}-{:02}-{:02}_{:02}-{:02}-{:02}", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec); + if (isDRC) + screenshotFileName.append("_GamePad"); + + fs::path screenshotPath; + for (sint32 i = 0; i < 999; i++) + { + screenshotPath = screendir; + if (i == 0) + screenshotPath.append(fmt::format("{}.png", screenshotFileName)); + else + screenshotPath.append(fmt::format("{}_{}.png", screenshotFileName, i + 1)); + + std::error_code ec; + bool exists = fs::exists(screenshotPath, ec); + + if (!ec && !exists) + return screenshotPath; + } + return std::nullopt; +} + +bool SaveScreenshotToFile(const fs::path& imagePath, const wxImage& image) +{ + std::error_code ec; + fs::create_directories(imagePath.parent_path(), ec); + if (ec) + return false; + + // suspend wxWidgets logging for the lifetime this object, to prevent a message box if wxImage::SaveFile fails + wxLogNull _logNo; + return image.SaveFile(imagePath.wstring()); +} + +bool SaveScreenshotToClipboard(const wxImage& image) +{ + static std::mutex s_clipboardMutex; + bool success = false; + + s_clipboardMutex.lock(); + if (wxTheClipboard->Open()) + { + wxTheClipboard->SetData(new wxImageDataObject(image)); + wxTheClipboard->Close(); + success = true; + } + s_clipboardMutex.unlock(); + + return success; +} + +std::optional SaveScreenshot(std::vector data, int width, int height, bool mainWindow) +{ +#if BOOST_OS_WINDOWS + // on Windows wxWidgets uses OLE API for the clipboard + // to make this work we need to call OleInitialize() on the same thread + OleInitialize(nullptr); +#endif + bool save_screenshot = g_config.data().save_screenshot; + wxImage image(width, height, data.data(), true); + if (mainWindow) + { + if (SaveScreenshotToClipboard(image)) + { + if (!save_screenshot) + return "Screenshot saved to clipboard"; + } + else + { + return "Failed to open clipboard"; + } + } + if (save_screenshot) + { + auto imagePath = GenerateScreenshotFilename(mainWindow); + if (imagePath.has_value() && SaveScreenshotToFile(imagePath.value(), image)) + { + if (mainWindow) + return "Screenshot saved"; + } + else + { + return "Failed to save screenshot to file"; + } + } + return std::nullopt; +} + +extern WindowSystem::WindowInfo g_window_info; +const std::unordered_map> HotkeySettings::s_cfgHotkeyToFuncMap{ + {&s_cfgHotkeys.toggleFullscreen, [](void) { s_mainWindow->ShowFullScreen(!s_mainWindow->IsFullScreen()); }}, + {&s_cfgHotkeys.toggleFullscreenAlt, [](void) { s_mainWindow->ShowFullScreen(!s_mainWindow->IsFullScreen()); }}, + {&s_cfgHotkeys.exitFullscreen, [](void) { s_mainWindow->ShowFullScreen(false); }}, + {&s_cfgHotkeys.takeScreenshot, [](void) { if(g_renderer) g_renderer->RequestScreenshot(SaveScreenshot); }}, + {&s_cfgHotkeys.toggleFastForward, [](void) { ActiveSettings::SetTimerShiftFactor((ActiveSettings::GetTimerShiftFactor() < 3) ? 3 : 1); }}, +}; + +struct HotkeyEntry +{ + std::unique_ptr name; + std::unique_ptr keyInput; + std::unique_ptr controllerInput; + sHotkeyCfg& hotkey; + + HotkeyEntry(wxStaticText* name, wxButton* keyInput, wxButton* controllerInput, sHotkeyCfg& hotkey) + : name(name), keyInput(keyInput), controllerInput(controllerInput), hotkey(hotkey) + { + keyInput->SetClientData(&hotkey); + controllerInput->SetClientData(&hotkey); + } +}; + +HotkeySettings::HotkeySettings(wxWindow* parent) + : wxFrame(parent, wxID_ANY, _("Hotkey Settings")) +{ + SetIcon(wxICON(X_HOTKEY_SETTINGS)); + + m_sizer = new wxFlexGridSizer(0, 3, 5, 5); + m_sizer->AddGrowableCol(1); + m_sizer->AddGrowableCol(2); + + m_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_SIMPLE); + m_panel->SetSizer(m_sizer); + m_panel->SetBackgroundColour(*wxWHITE); + + Center(); + + SetActiveController(); + + CreateColumnHeaders(); + + /* global modifier */ + CreateHotkeyRow("Hotkey modifier", s_cfgHotkeys.modifiers); + m_hotkeys.at(0).keyInput->Hide(); + + /* hotkeys */ + CreateHotkeyRow("Toggle fullscreen", s_cfgHotkeys.toggleFullscreen); + CreateHotkeyRow("Take screenshot", s_cfgHotkeys.takeScreenshot); + CreateHotkeyRow("Toggle fast-forward", s_cfgHotkeys.toggleFastForward); + + m_controllerTimer = new wxTimer(this); + Bind(wxEVT_TIMER, &HotkeySettings::OnControllerTimer, this); + + m_sizer->SetSizeHints(this); +} + +HotkeySettings::~HotkeySettings() +{ + m_controllerTimer->Stop(); + if (m_needToSave) + { + g_config.Save(); + } +} + +void HotkeySettings::Init(wxFrame* mainWindowFrame) +{ + s_keyboardHotkeyToFuncMap.reserve(s_cfgHotkeyToFuncMap.size()); + for (const auto& [cfgHotkey, func] : s_cfgHotkeyToFuncMap) + { + auto keyboardHotkey = cfgHotkey->keyboard.raw; + if (keyboardHotkey > sHotkeyCfg::keyboardNone) + { + s_keyboardHotkeyToFuncMap[keyboardHotkey] = func; + } + auto controllerHotkey = cfgHotkey->controller; + if (controllerHotkey > sHotkeyCfg::controllerNone) + { + s_controllerHotkeyToFuncMap[controllerHotkey] = func; + } + } + s_mainWindow = mainWindowFrame; +} + +void HotkeySettings::CreateColumnHeaders(void) +{ + auto* emptySpace = new wxStaticText(m_panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER_HORIZONTAL); + auto* keyboard = new wxStaticText(m_panel, wxID_ANY, "Keyboard", wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER_HORIZONTAL); + auto* controller = new wxStaticText(m_panel, wxID_ANY, "Controller", wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER_HORIZONTAL); + + keyboard->SetMinSize(m_minButtonSize); + controller->SetMinSize(m_minButtonSize); + + auto flags = wxSizerFlags().Expand(); + m_sizer->Add(emptySpace, flags); + m_sizer->Add(keyboard, flags); + m_sizer->Add(controller, flags); +} + +void HotkeySettings::CreateHotkeyRow(const wxString& label, sHotkeyCfg& cfgHotkey) +{ + auto* name = new wxStaticText(m_panel, wxID_ANY, label); + auto* keyInput = new wxButton(m_panel, wxID_ANY, To_wxString(cfgHotkey.keyboard), wxDefaultPosition, wxDefaultSize, wxWANTS_CHARS | wxBU_EXACTFIT); + auto* controllerInput = new wxButton(m_panel, wxID_ANY, To_wxString(cfgHotkey.controller), wxDefaultPosition, wxDefaultSize, wxWANTS_CHARS | wxBU_EXACTFIT); + + /* for starting input */ + keyInput->Bind(wxEVT_BUTTON, &HotkeySettings::OnKeyboardHotkeyInputLeftClick, this); + controllerInput->Bind(wxEVT_BUTTON, &HotkeySettings::OnControllerHotkeyInputLeftClick, this); + + /* for cancelling and clearing input */ + keyInput->Connect(wxEVT_RIGHT_UP, wxMouseEventHandler(HotkeySettings::OnKeyboardHotkeyInputRightClick), NULL, this); + controllerInput->Connect(wxEVT_RIGHT_UP, wxMouseEventHandler(HotkeySettings::OnControllerHotkeyInputRightClick), NULL, this); + + keyInput->SetMinSize(m_minButtonSize); + controllerInput->SetMinSize(m_minButtonSize); + +#if BOOST_OS_WINDOWS + const wxColour inputButtonColor = 0xfafafa; +#else + const wxColour inputButtonColor = GetBackgroundColour(); +#endif + keyInput->SetBackgroundColour(inputButtonColor); + controllerInput->SetBackgroundColour(inputButtonColor); + + auto flags = wxSizerFlags(1).Expand().Border(wxALL, 5).CenterVertical(); + m_sizer->Add(name, flags); + m_sizer->Add(keyInput, flags); + m_sizer->Add(controllerInput, flags); + + m_hotkeys.emplace_back(name, keyInput, controllerInput, cfgHotkey); +} + +void HotkeySettings::OnControllerTimer(wxTimerEvent& event) +{ + if (m_activeController.expired()) + { + m_controllerTimer->Stop(); + return; + } + auto& controller = *m_activeController.lock(); + auto buttons = controller.update_state().buttons; + if (!buttons.IsIdle()) + { + for (const auto& newHotkey : buttons.GetButtonList()) + { + m_controllerTimer->Stop(); + auto* inputButton = static_cast(m_controllerTimer->GetClientData()); + auto& cfgHotkey = *static_cast(inputButton->GetClientData()); + const auto oldHotkey = cfgHotkey.controller; + const bool isModifier = (&cfgHotkey == &s_cfgHotkeys.modifiers); + /* ignore same hotkeys and block duplicate hotkeys */ + if ((newHotkey != oldHotkey) && (isModifier || (newHotkey != s_cfgHotkeys.modifiers.controller)) && + (s_controllerHotkeyToFuncMap.find(newHotkey) == s_controllerHotkeyToFuncMap.end())) + { + m_needToSave |= true; + cfgHotkey.controller = newHotkey; + /* don't bind modifier to map */ + if (!isModifier) + { + s_controllerHotkeyToFuncMap.erase(oldHotkey); + s_controllerHotkeyToFuncMap[newHotkey] = s_cfgHotkeyToFuncMap.at(&cfgHotkey); + } + } + FinalizeInput(inputButton); + return; + } + } +} + +void HotkeySettings::OnKeyboardHotkeyInputLeftClick(wxCommandEvent& event) +{ + auto* inputButton = static_cast(event.GetEventObject()); + if (m_activeInputButton) + { + /* ignore multiple clicks of the same button */ + if (inputButton == m_activeInputButton) return; + RestoreInputButton(); + } + inputButton->Bind(wxEVT_KEY_UP, &HotkeySettings::OnKeyUp, this); + inputButton->SetLabelText(m_editModeHotkeyText); + m_activeInputButton = inputButton; +} + +void HotkeySettings::OnControllerHotkeyInputLeftClick(wxCommandEvent& event) +{ + auto* inputButton = static_cast(event.GetEventObject()); + if (m_activeInputButton) + { + /* ignore multiple clicks of the same button */ + if (inputButton == m_activeInputButton) return; + RestoreInputButton(); + } + m_controllerTimer->Stop(); + if (!SetActiveController()) + { + return; + } + inputButton->SetLabelText(m_editModeHotkeyText); + m_controllerTimer->SetClientData(inputButton); + m_controllerTimer->Start(25); + m_activeInputButton = inputButton; +} + +void HotkeySettings::OnKeyboardHotkeyInputRightClick(wxMouseEvent& event) +{ + if (m_activeInputButton) + { + RestoreInputButton(); + return; + } + auto* inputButton = static_cast(event.GetEventObject()); + auto& cfgHotkey = *static_cast(inputButton->GetClientData()); + uKeyboardHotkey newHotkey{ sHotkeyCfg::keyboardNone }; + if (cfgHotkey.keyboard.raw != newHotkey.raw) + { + m_needToSave |= true; + s_keyboardHotkeyToFuncMap.erase(cfgHotkey.keyboard.raw); + cfgHotkey.keyboard = newHotkey; + FinalizeInput(inputButton); + } +} + +void HotkeySettings::OnControllerHotkeyInputRightClick(wxMouseEvent& event) +{ + if (m_activeInputButton) + { + RestoreInputButton(); + return; + } + auto* inputButton = static_cast(event.GetEventObject()); + auto& cfgHotkey = *static_cast(inputButton->GetClientData()); + ControllerHotkey_t newHotkey{ sHotkeyCfg::controllerNone }; + if (cfgHotkey.controller != newHotkey) + { + m_needToSave |= true; + s_controllerHotkeyToFuncMap.erase(cfgHotkey.controller); + cfgHotkey.controller = newHotkey; + FinalizeInput(inputButton); + } +} + +bool HotkeySettings::SetActiveController(void) +{ + auto emulatedController = InputManager::instance().get_controller(0); + if (emulatedController.use_count() <= 1) + { + return false; + } + const auto& controllers = emulatedController->get_controllers(); + if (controllers.empty()) + { + return false; + } + m_activeController = controllers.at(0); + return true; +} + +void HotkeySettings::OnKeyUp(wxKeyEvent& event) +{ + auto* inputButton = static_cast(event.GetEventObject()); + auto& cfgHotkey = *static_cast(inputButton->GetClientData()); + if (auto keycode = event.GetKeyCode(); IsValidKeycodeUp(keycode)) + { + auto oldHotkey = cfgHotkey.keyboard; + uKeyboardHotkey newHotkey{}; + newHotkey.key = keycode; + newHotkey.alt = event.AltDown(); + newHotkey.ctrl = event.ControlDown(); + newHotkey.shift = event.ShiftDown(); + if ((newHotkey.raw != oldHotkey.raw) && + (s_keyboardHotkeyToFuncMap.find(newHotkey.raw) == s_keyboardHotkeyToFuncMap.end())) + { + m_needToSave |= true; + cfgHotkey.keyboard = newHotkey; + s_keyboardHotkeyToFuncMap.erase(oldHotkey.raw); + s_keyboardHotkeyToFuncMap[newHotkey.raw] = s_cfgHotkeyToFuncMap.at(&cfgHotkey); + } + } + FinalizeInput(inputButton); +} + +template +void HotkeySettings::FinalizeInput(wxButton* inputButton) +{ + auto& cfgHotkey = *static_cast(inputButton->GetClientData()); + if constexpr (std::is_same_v) + { + inputButton->Unbind(wxEVT_KEY_UP, &HotkeySettings::OnKeyUp, this); + inputButton->SetLabelText(To_wxString(cfgHotkey.keyboard)); + } else if constexpr (std::is_same_v) + { + inputButton->SetLabelText(To_wxString(cfgHotkey.controller)); + } + m_activeInputButton = nullptr; +} + +template +void HotkeySettings::RestoreInputButton(void) +{ + FinalizeInput(m_activeInputButton); +} + +bool HotkeySettings::IsValidKeycodeUp(int keycode) +{ + switch (keycode) + { + case WXK_NONE: + case WXK_ESCAPE: + case WXK_ALT: + case WXK_CONTROL: + case WXK_SHIFT: + return false; + default: + return true; + } +} + +wxString HotkeySettings::To_wxString(uKeyboardHotkey hotkey) +{ + if (hotkey.raw == sHotkeyCfg::keyboardNone) { + return m_disabledHotkeyText; + } + wxString ret{}; + if (hotkey.alt) + { + ret.append(_("Alt + ")); + } + if (hotkey.ctrl) + { + ret.append(_("Ctrl + ")); + } + if (hotkey.shift) + { + ret.append(_("Shift + ")); + } + ret.append(wxAcceleratorEntry(0, hotkey.key).ToString()); + return ret; +} + +wxString HotkeySettings::To_wxString(ControllerHotkey_t hotkey) +{ + if ((hotkey == sHotkeyCfg::controllerNone) || m_activeController.expired()) { + return m_disabledHotkeyText; + } + return m_activeController.lock()->get_button_name(hotkey); +} + +void HotkeySettings::CaptureInput(wxKeyEvent& event) +{ + uKeyboardHotkey hotkey{}; + hotkey.key = event.GetKeyCode(); + hotkey.alt = event.AltDown(); + hotkey.ctrl = event.ControlDown(); + hotkey.shift = event.ShiftDown(); + const auto it = s_keyboardHotkeyToFuncMap.find(hotkey.raw); + if (it != s_keyboardHotkeyToFuncMap.end()) + it->second(); +} + +void HotkeySettings::CaptureInput(const ControllerState& currentState, const ControllerState& lastState) +{ + const auto& modifier = s_cfgHotkeys.modifiers.controller; + if ((modifier >= 0) && currentState.buttons.GetButtonState(modifier)) + { + for (const auto& buttonId : currentState.buttons.GetButtonList()) + { + const auto it = s_controllerHotkeyToFuncMap.find(buttonId); + if (it == s_controllerHotkeyToFuncMap.end()) + continue; + /* only capture clicks */ + if (lastState.buttons.GetButtonState(buttonId)) + break; + it->second(); + break; + } + } +} diff --git a/src/gui/wxgui/input/HotkeySettings.h b/src/gui/wxgui/input/HotkeySettings.h new file mode 100644 index 00000000..77478ea9 --- /dev/null +++ b/src/gui/wxgui/input/HotkeySettings.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include "config/CemuConfig.h" +#include "input/api/Controller.h" + +class HotkeyEntry; + +class HotkeySettings : public wxFrame +{ +public: + static void Init(wxFrame* mainWindowFrame); + + static void CaptureInput(wxKeyEvent& event); + static void CaptureInput(const ControllerState& currentState, const ControllerState& lastState); + + HotkeySettings(wxWindow* parent); + ~HotkeySettings(); + +private: + inline static wxFrame* s_mainWindow = nullptr; + static const std::unordered_map> s_cfgHotkeyToFuncMap; + inline static std::unordered_map> s_keyboardHotkeyToFuncMap{}; + inline static std::unordered_map> s_controllerHotkeyToFuncMap{}; + inline static auto& s_cfgHotkeys = GetConfig().hotkeys; + + wxPanel* m_panel; + wxFlexGridSizer* m_sizer; + wxButton* m_activeInputButton{ nullptr }; + wxTimer* m_controllerTimer{ nullptr }; + const wxSize m_minButtonSize{ 250, 45 }; + const wxString m_disabledHotkeyText{ _("----") }; + const wxString m_editModeHotkeyText{ _("") }; + + std::vector m_hotkeys; + std::weak_ptr m_activeController{}; + bool m_needToSave = false; + + /* helpers */ + void CreateColumnHeaders(void); + void CreateHotkeyRow(const wxString& label, sHotkeyCfg& cfgHotkey); + wxString To_wxString(uKeyboardHotkey hotkey); + wxString To_wxString(ControllerHotkey_t hotkey); + bool IsValidKeycodeUp(int keycode); + bool SetActiveController(void); + + template + void FinalizeInput(wxButton* inputButton); + + template + void RestoreInputButton(void); + + /* events */ + void OnKeyboardHotkeyInputLeftClick(wxCommandEvent& event); + void OnControllerHotkeyInputLeftClick(wxCommandEvent& event); + void OnKeyboardHotkeyInputRightClick(wxMouseEvent& event); + void OnControllerHotkeyInputRightClick(wxMouseEvent& event); + void OnKeyUp(wxKeyEvent& event); + void OnControllerTimer(wxTimerEvent& event); +}; diff --git a/src/gui/wxgui/wxWindowSystem.cpp b/src/gui/wxgui/wxWindowSystem.cpp index deff25c2..d717580c 100644 --- a/src/gui/wxgui/wxWindowSystem.cpp +++ b/src/gui/wxgui/wxWindowSystem.cpp @@ -1,3 +1,4 @@ +#include "input/HotkeySettings.h" #include "interface/WindowSystem.h" #include "helpers/wxHelpers.h" @@ -337,6 +338,28 @@ void WindowSystem::refreshGameList() } } +void WindowSystem::captureInput(const ControllerState& currentState, const ControllerState& lastState) +{ + HotkeySettings::CaptureInput(currentState, lastState); +} + +sHotkeyCfg WindowSystem::getDefaultHotkeyConfig(HotKey hotKey) +{ + switch (hotKey) + { + case HotKey::EXIT_FULLSCREEN: + return sHotkeyCfg{uKeyboardHotkey{WXK_ESCAPE}}; + case HotKey::TOGGLE_FULLSCREEN: + return sHotkeyCfg{uKeyboardHotkey{WXK_F11}}; + case HotKey::TOGGLE_FULLSCREEN_ALT: + return sHotkeyCfg{uKeyboardHotkey{WXK_CONTROL_M, true}}; // ALT+ENTER + case HotKey::TAKE_SCREENSHOT: + return sHotkeyCfg{uKeyboardHotkey{WXK_F12}}; + default: + return sHotkeyCfg{}; + } +} + bool WindowSystem::isFullScreen() { return g_window_info.is_fullscreen; diff --git a/src/input/api/Controller.cpp b/src/input/api/Controller.cpp index c5f1c783..940b1c3f 100644 --- a/src/input/api/Controller.cpp +++ b/src/input/api/Controller.cpp @@ -1,4 +1,6 @@ #include "input/api/Controller.h" +#include "config/CemuConfig.h" +#include "WindowSystem.h" ControllerBase::ControllerBase(std::string_view uuid, std::string_view display_name) : m_uuid{uuid}, m_display_name{display_name} @@ -65,6 +67,8 @@ const ControllerState& ControllerBase::update_state() #undef APPLY_AXIS_BUTTON + WindowSystem::captureInput(result, m_last_state); + m_last_state = std::move(result); return m_last_state; } diff --git a/src/resource/cemu.rc b/src/resource/cemu.rc index 6f78bfc3..fb397056 100644 --- a/src/resource/cemu.rc +++ b/src/resource/cemu.rc @@ -37,6 +37,7 @@ X_SETTINGS ICON "resource\\icons8_automatic_26_x X_GAME_PROFILE ICON "resource\\icons8-compose-filled-50.ico" +X_HOTKEY_SETTINGS ICON "resource\\icons8_hotkeys.ico" #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// diff --git a/src/resource/embedded/X_HOTKEY_SETTINGS.xpm b/src/resource/embedded/X_HOTKEY_SETTINGS.xpm new file mode 100644 index 00000000..473dac09 --- /dev/null +++ b/src/resource/embedded/X_HOTKEY_SETTINGS.xpm @@ -0,0 +1,70 @@ +/* XPM */ +const char *X_HOTKEY_SETTINGS_xpm[] = { +"64 64 2 1", +" c None", +". c #000000", +" ... ", +" .... ", +" .... ", +" .. .... ", +" .. ...... ", +" ... ...... ", +" ... ...... ", +" ... ....... ", +" .... ........ ... ", +" .... ......... ... ", +" .... ......... ... ", +" .... .......... .... ", +" .... ......... .... ", +" ..... ........... ..... ", +" ..... .......... ..... ", +" ........ ........... ...... ", +" ........ ... ....... ...... ", +" ........... ... ....... ...... ", +" ........... ..... ...... ....... ", +" ........... ..... ...... ........ ", +" ............. ...... ...... ......... ", +" ........ .... ...... .... ..... .... ", +" ........ .... ....... .... ...... .... ", +" ...... .... ...... ..... ....... ..... ", +" ...... ........... ............ .... ", +" ...... .......... .......... .... ", +" ..... .......... .......... .... ", +" ..... .......... ......... ..... ", +" ..... .......... ........ ..... ", +" ..... ........ ........ ...... ", +" ..... ..... ..... ..... ", +" ..... ..... .... ...... ", +" ..... ..... ", +" ... .... ", +" . ............................ .. ", +" ................................ ", +" ................................ ", +" .................................... ", +" .................................... ", +" .......... ........ .......... ", +" .......... ........ .......... ", +" .......... ........ .......... ", +" .......... ........ .......... ", +" .......... ........ .......... ", +" .......... ........ .......... ", +" .......... ........ .......... ", +" .......... .......... ", +" .......... .......... ", +" .......... .......... ", +" .......... ........ .......... ", +" .......... ........ .......... ", +" .......... ........ .......... ", +" .......... ........ .......... ", +" .......... ........ .......... ", +" .......... ........ .......... ", +" .......... ........ .......... ", +" .......... ........ .......... ", +" .................................... ", +" .................................... ", +" .................................... ", +" .................................... ", +" ................................ ", +" ................................ ", +" ............................. " +}; diff --git a/src/resource/embedded/resources.cpp b/src/resource/embedded/resources.cpp index 5dbb9d14..2b508c10 100644 --- a/src/resource/embedded/resources.cpp +++ b/src/resource/embedded/resources.cpp @@ -2,6 +2,7 @@ #include "M_WND_ICON128.xpm" #include "X_BOX.xpm" #include "X_SETTINGS.xpm" +#include "X_HOTKEY_SETTINGS.xpm" #include "icons8-checkmark-yes-32.hpng" #include "icons8-error-32.hpng" diff --git a/src/resource/embedded/resources.h b/src/resource/embedded/resources.h index 8cb9fe5d..26e9b759 100644 --- a/src/resource/embedded/resources.h +++ b/src/resource/embedded/resources.h @@ -2,6 +2,7 @@ extern const char* X_GAME_PROFILE_xpm[]; extern const char* M_WND_ICON128_xpm[]; extern const char* X_BOX_xpm[]; extern const char* X_SETTINGS_xpm[]; +extern const char* X_HOTKEY_SETTINGS_xpm[]; extern unsigned char PNG_CHECK_YES_png[573]; extern unsigned char PNG_ERROR_png[472]; diff --git a/src/resource/icons8_hotkeys.ico b/src/resource/icons8_hotkeys.ico new file mode 100644 index 00000000..68f34ac4 Binary files /dev/null and b/src/resource/icons8_hotkeys.ico differ diff --git a/src/resource/icons8_hotkeys.png b/src/resource/icons8_hotkeys.png new file mode 100644 index 00000000..24dd3b35 Binary files /dev/null and b/src/resource/icons8_hotkeys.png differ