/***************************************************************************** * Copyright (c) 2014-2020 OpenRCT2 developers * * For a complete list of all authors, please refer to contributors.md * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 * * OpenRCT2 is licensed under the GNU General Public License version 3. *****************************************************************************/ #include "ShortcutManager.h" #include "ShortcutIds.h" #include #include #include #include #include #include #include #include #include #include using namespace OpenRCT2::Ui; std::string_view RegisteredShortcut::GetTopLevelGroup() const { auto fullstopIndex = Id.find('.'); if (fullstopIndex != std::string::npos) { return std::string_view(Id.c_str(), fullstopIndex); } return {}; } std::string_view RegisteredShortcut::GetGroup() const { auto fullstopIndex = Id.find_last_of('.'); if (fullstopIndex != std::string::npos) { return std::string_view(Id.c_str(), fullstopIndex); } return {}; } bool RegisteredShortcut::Matches(const InputEvent& e) const { if (IsSuitableInputEvent(e)) { auto result = std::find_if( Current.begin(), Current.end(), [e](const ShortcutInput& action) { return action.Matches(e); }); return result != Current.end(); } return false; } bool RegisteredShortcut::IsSuitableInputEvent(const InputEvent& e) const { // Do not intercept button releases if (e.State == InputEventState::Release) { return false; } if (e.DeviceKind == InputDeviceKind::Mouse) { // Do not allow LMB or RMB to be shortcut if (e.Button == 0 || e.Button == 1) { return false; } } else if (e.DeviceKind == InputDeviceKind::Keyboard) { // Do not allow modifier keys alone switch (e.Button) { case SDLK_LCTRL: case SDLK_RCTRL: case SDLK_LSHIFT: case SDLK_RSHIFT: case SDLK_LALT: case SDLK_RALT: case SDLK_LGUI: case SDLK_RGUI: return false; } } return true; } std::string RegisteredShortcut::GetDisplayString() const { std::string result; auto numChords = Current.size(); for (size_t i = 0; i < numChords; i++) { const auto& kc = Current[i]; result += kc.ToString(); if (i < numChords - 1) { // TODO localise... result += " or "; } } return result; } ShortcutManager::ShortcutManager(const std::shared_ptr& env) : _env(env) { RegisterDefaultShortcuts(); } void ShortcutManager::RegisterShortcut(RegisteredShortcut&& shortcut) { if (!shortcut.Id.empty() && GetShortcut(shortcut.Id) == nullptr) { Shortcuts.push_back(shortcut); } } RegisteredShortcut* ShortcutManager::GetShortcut(std::string_view id) { auto result = std::find_if(Shortcuts.begin(), Shortcuts.end(), [id](const RegisteredShortcut& s) { return s.Id == id; }); return result == Shortcuts.end() ? nullptr : &(*result); } void ShortcutManager::RemoveShortcut(std::string_view id) { Shortcuts.erase(std::remove_if( Shortcuts.begin(), Shortcuts.end(), [id](const RegisteredShortcut& shortcut) { return shortcut.Id == id; })); } bool ShortcutManager::IsPendingShortcutChange() const { return !_pendingShortcutChange.empty(); } void ShortcutManager::SetPendingShortcutChange(std::string_view id) { _pendingShortcutChange = id; } void ShortcutManager::ProcessEvent(const InputEvent& e) { if (!IsPendingShortcutChange()) { for (const auto& shortcut : Shortcuts) { if (shortcut.Matches(e)) { shortcut.Action(); } } } else { auto shortcut = GetShortcut(_pendingShortcutChange); if (shortcut != nullptr && shortcut->IsSuitableInputEvent(e)) { auto shortcutInput = ShortcutInput::FromInputEvent(e); if (shortcutInput) { shortcut->Current.clear(); shortcut->Current.push_back(std::move(*shortcutInput)); } _pendingShortcutChange.clear(); window_close_by_class(WC_CHANGE_KEYBOARD_SHORTCUT); SaveUserBindings(); } } } bool ShortcutManager::ProcessEventForSpecificShortcut(const InputEvent& e, std::string_view id) { auto shortcut = GetShortcut(id); if (shortcut != nullptr && shortcut->Matches(e)) { shortcut->Action(); return true; } return false; } void ShortcutManager::LoadUserBindings() { try { auto path = fs::u8path(_env->GetFilePath(PATHID::CONFIG_SHORTCUTS)); if (fs::exists(path)) { LoadUserBindings(path); } else { try { std::printf("Importing legacy shortcuts...\n"); auto legacyPath = fs::u8path(_env->GetFilePath(PATHID::CONFIG_SHORTCUTS_LEGACY)); if (fs::exists(legacyPath)) { LoadLegacyBindings(legacyPath); SaveUserBindings(); std::printf("Legacy shortcuts imported\n"); } } catch (const std::exception& e) { std::fprintf(stderr, "Unable to import legacy shortcut bindings: %s\n", e.what()); } } } catch (const std::exception& e) { std::fprintf(stderr, "Unable to load shortcut bindings: %s\n", e.what()); } } std::optional ShortcutManager::ConvertLegacyBinding(uint16_t binding) { constexpr uint16_t nullBinding = 0xFFFF; constexpr uint16_t shift = 0x100; constexpr uint16_t ctrl = 0x200; constexpr uint16_t alt = 0x400; constexpr uint16_t cmd = 0x800; if (binding == nullBinding) { return {}; } else { ShortcutInput result; result.Kind = InputDeviceKind::Keyboard; if (binding & shift) result.Modifiers |= KMOD_SHIFT; if (binding & ctrl) result.Modifiers |= KMOD_CTRL; if (binding & alt) result.Modifiers |= KMOD_ALT; if (binding & cmd) result.Modifiers |= KMOD_GUI; result.Button = SDL_GetKeyFromScancode(static_cast(binding & 0xFF)); return result; } } void ShortcutManager::LoadLegacyBindings(const fs::path& path) { constexpr int32_t SUPPORTED_FILE_VERSION = 1; constexpr int32_t MAX_LEGACY_SHORTCUTS = 85; auto fs = FileStream(path, FILE_MODE_OPEN); auto version = fs.ReadValue(); if (version == SUPPORTED_FILE_VERSION) { for (size_t i = 0; i < MAX_LEGACY_SHORTCUTS; i++) { auto value = fs.ReadValue(); auto shortcutId = GetLegacyShortcutId(i); if (!shortcutId.empty()) { auto shortcut = GetShortcut(shortcutId); if (shortcut != nullptr) { shortcut->Current.clear(); auto input = ConvertLegacyBinding(value); if (input) { shortcut->Current.push_back(std::move(*input)); } } } } } } void ShortcutManager::LoadUserBindings(const fs::path& path) { auto root = Json::ReadFromFile(path); if (root.is_object()) { for (auto it = root.begin(); it != root.end(); ++it) { const auto& key = it.key(); const auto& value = it.value(); const auto& shortcut = GetShortcut(key); if (shortcut != nullptr) { shortcut->Current.clear(); if (value.is_string()) { shortcut->Current.emplace_back(value.get()); } else if (value.is_array()) { for (auto& subValue : value) { shortcut->Current.emplace_back(subValue.get()); } } } } } } void ShortcutManager::SaveUserBindings() { try { auto path = fs::u8path(_env->GetFilePath(PATHID::CONFIG_SHORTCUTS)); SaveUserBindings(path); } catch (const std::exception& e) { std::fprintf(stderr, "Unable to save shortcut bindings: %s\n", e.what()); } } void ShortcutManager::SaveUserBindings(const fs::path& path) { json_t root; if (fs::exists(path)) { root = Json::ReadFromFile(path); } for (const auto& shortcut : Shortcuts) { auto& jShortcut = root[shortcut.Id]; if (shortcut.Current.size() == 1) { jShortcut = shortcut.Current[0].ToString(); } else { jShortcut = nlohmann::json::array(); for (const auto& binding : shortcut.Current) { jShortcut.push_back(binding.ToString()); } } } Json::WriteToFile(path, root); } std::string_view ShortcutManager::GetLegacyShortcutId(size_t index) { static constexpr const char* LegacyMap[] = { ShortcutId::InterfaceCloseTop, ShortcutId::InterfaceCloseAll, ShortcutId::InterfaceCancelConstruction, ShortcutId::InterfacePause, ShortcutId::InterfaceZoomOut, ShortcutId::InterfaceZoomIn, ShortcutId::InterfaceRotateClockwise, ShortcutId::InterfaceRotateAnticlockwise, ShortcutId::InterfaceRotateConstruction, ShortcutId::ViewToggleUnderground, ShortcutId::ViewToggleBaseLand, ShortcutId::ViewToggleVerticalLand, ShortcutId::ViewToggleRides, ShortcutId::ViewToggleScenery, ShortcutId::ViewToggleSupports, ShortcutId::ViewTogglePeeps, ShortcutId::ViewToggleLandHeightMarkers, ShortcutId::ViewToggleTrackHeightMarkers, ShortcutId::ViewToggleFootpathHeightMarkers, ShortcutId::InterfaceOpenLand, ShortcutId::InterfaceOpenWater, ShortcutId::InterfaceOpenScenery, ShortcutId::InterfaceOpenFootpaths, ShortcutId::InterfaceOpenNewRide, ShortcutId::InterfaceOpenFinances, ShortcutId::InterfaceOpenResearch, ShortcutId::InterfaceOpenRides, ShortcutId::InterfaceOpenPark, ShortcutId::InterfaceOpenGuests, ShortcutId::InterfaceOpenStaff, ShortcutId::InterfaceOpenMessages, ShortcutId::InterfaceOpenMap, ShortcutId::InterfaceScreenshot, ShortcutId::InterfaceDecreaseSpeed, ShortcutId::InterfaceIncreaseSpeed, ShortcutId::InterfaceOpenCheats, ShortcutId::InterfaceToggleToolbars, ShortcutId::ScrollUp, ShortcutId::ScrollLeft, ShortcutId::ScrollRight, ShortcutId::ScrollDown, ShortcutId::MultiplayerChat, ShortcutId::InterfaceSaveGame, ShortcutId::InterfaceShowOptions, ShortcutId::InterfaceMute, ShortcutId::ScaleToggleWindowMode, ShortcutId::MultiplayerShow, nullptr, ShortcutId::DebugTogglePaintDebugWindow, ShortcutId::ViewToggleFootpaths, ShortcutId::WindowRideConstructionTurnLeft, ShortcutId::WindowRideConstructionTurnRight, ShortcutId::WindowRideConstructionDefault, ShortcutId::WindowRideConstructionSlopeDown, ShortcutId::WindowRideConstructionSlopeUp, ShortcutId::WindowRideConstructionChainLift, ShortcutId::WindowRideConstructionBankLeft, ShortcutId::WindowRideConstructionBankRight, ShortcutId::WindowRideConstructionPrevious, ShortcutId::WindowRideConstructionNext, ShortcutId::WindowRideConstructionBuild, ShortcutId::WindowRideConstructionDemolish, ShortcutId::InterfaceLoadGame, ShortcutId::InterfaceClearScenery, ShortcutId::ViewToggleGridlines, ShortcutId::ViewToggleCutAway, ShortcutId::ViewToogleFootpathIssues, ShortcutId::InterfaceOpenTileInspector, ShortcutId::DebugAdvanceTick, ShortcutId::InterfaceSceneryPicker, ShortcutId::InterfaceScaleIncrease, ShortcutId::InterfaceScaleDecrease, ShortcutId::WindowTileInspectorInsertCorrupt, ShortcutId::WindowTileInspectorCopy, ShortcutId::WindowTileInspectorPaste, ShortcutId::WindowTileInspectorRemove, ShortcutId::WindowTileInspectorMoveUp, ShortcutId::WindowTileInspectorMoveDown, ShortcutId::WindowTileInspectorIncreaseX, ShortcutId::WindowTileInspectorDecreaseX, ShortcutId::WindowTileInspectorIncreaseY, ShortcutId::WindowTileInspectorDecreaseY, ShortcutId::WindowTileInspectorIncreaseHeight, ShortcutId::WindowTileInspectorDecreaseHeight, ShortcutId::InterfaceDisableClearance, }; auto sz = index < std::size(LegacyMap) ? LegacyMap[index] : nullptr; return sz == nullptr ? std::string_view() : std::string_view(sz); }