1
0
mirror of https://github.com/OpenRCT2/OpenRCT2 synced 2026-01-23 06:44:38 +01:00
Files
OpenRCT2/src/openrct2-ui/input/ShortcutManager.cpp
Ríša Szlachta 19035ec2eb Fix unlocalized word for conjunction when having multiple shortcuts for the same action (#24838)
* create STR_7000: "or"

* declare STR_OR

* enable localisation via LanguageGetString()

* Update ShortcutManager.cpp

* Update changelog.txt

---------

Co-authored-by: Tulio Leao <tupaschoal@gmail.com>
2025-07-27 16:25:03 +00:00

450 lines
14 KiB
C++

/*****************************************************************************
* Copyright (c) 2014-2025 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 <SDL.h>
#include <openrct2-ui/UiStringIds.h>
#include <openrct2/Context.h>
#include <openrct2/PlatformEnvironment.h>
#include <openrct2/core/Console.hpp>
#include <openrct2/core/DataSerialiser.h>
#include <openrct2/core/FileStream.h>
#include <openrct2/core/FileSystem.hpp>
#include <openrct2/core/Json.hpp>
#include <openrct2/core/String.hpp>
#include <openrct2/ui/WindowManager.h>
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.ToLocalisedString();
if (i < numChords - 1)
{
result += " ";
result += LanguageGetString(STR_OR);
result += " ";
}
}
return result;
}
ShortcutManager::ShortcutManager(IPlatformEnvironment& env)
: _env(env)
{
RegisterDefaultShortcuts();
}
void ShortcutManager::RegisterShortcut(RegisteredShortcut&& shortcut)
{
if (!shortcut.Id.empty() && GetShortcut(shortcut.Id) == nullptr)
{
auto id = std::make_unique<std::string>(shortcut.Id);
auto idView = std::string_view(*id);
_ids.push_back(std::move(id));
shortcut.OrderIndex = Shortcuts.size();
Shortcuts[idView] = shortcut;
}
}
RegisteredShortcut* ShortcutManager::GetShortcut(std::string_view id)
{
auto result = Shortcuts.find(id);
return result == Shortcuts.end() ? nullptr : &result->second;
}
void ShortcutManager::RemoveShortcut(std::string_view id)
{
Shortcuts.erase(id);
_ids.erase(
std::remove_if(_ids.begin(), _ids.end(), [id](const std::unique_ptr<std::string>& x) { return *x == id; }), _ids.end());
}
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.second.Matches(e))
{
shortcut.second.Action();
}
}
}
else
{
auto shortcut = GetShortcut(_pendingShortcutChange);
if (shortcut != nullptr && shortcut->IsSuitableInputEvent(e))
{
auto shortcutInput = ShortcutInput::FromInputEvent(e);
if (shortcutInput.has_value())
{
shortcut->Current.clear();
shortcut->Current.push_back(std::move(shortcutInput.value()));
}
_pendingShortcutChange.clear();
auto* windowMgr = Ui::GetWindowManager();
windowMgr->CloseByClass(WindowClass::ChangeKeyboardShortcut);
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::configShortcuts));
if (fs::exists(path))
{
LoadUserBindings(path);
}
else
{
try
{
Console::WriteLine("Importing legacy shortcuts...");
auto legacyPath = fs::u8path(_env.GetFilePath(PathId::configShortcutsLegacy));
if (fs::exists(legacyPath))
{
LoadLegacyBindings(legacyPath);
SaveUserBindings();
Console::WriteLine("Legacy shortcuts imported");
}
}
catch (const std::exception& e)
{
Console::Error::WriteLine("Unable to import legacy shortcut bindings: %s", e.what());
}
}
}
catch (const std::exception& e)
{
Console::Error::WriteLine("Unable to load shortcut bindings: %s", e.what());
}
}
std::optional<ShortcutInput> ShortcutManager::ConvertLegacyBinding(uint16_t binding)
{
constexpr uint16_t kNullBinding = 0xFFFF;
constexpr uint16_t kShift = 0x100;
constexpr uint16_t kCtrl = 0x200;
constexpr uint16_t kAlt = 0x400;
constexpr uint16_t kCmd = 0x800;
if (binding == kNullBinding)
{
return std::nullopt;
}
ShortcutInput result;
result.Kind = InputDeviceKind::Keyboard;
if (binding & kShift)
result.Modifiers |= KMOD_SHIFT;
if (binding & kCtrl)
result.Modifiers |= KMOD_CTRL;
if (binding & kAlt)
result.Modifiers |= KMOD_ALT;
if (binding & kCmd)
result.Modifiers |= KMOD_GUI;
result.Button = SDL_GetKeyFromScancode(static_cast<SDL_Scancode>(binding & 0xFF));
return result;
}
void ShortcutManager::LoadLegacyBindings(const fs::path& path)
{
constexpr int32_t kSupportedFileVersion = 1;
constexpr int32_t kMaxLegacyShortcuts = 85;
auto fs = FileStream(path, FileMode::open);
auto version = fs.ReadValue<uint16_t>();
if (version == kSupportedFileVersion)
{
for (size_t i = 0; i < kMaxLegacyShortcuts; i++)
{
auto value = fs.ReadValue<uint16_t>();
auto shortcutId = GetLegacyShortcutId(i);
if (!shortcutId.empty())
{
auto shortcut = GetShortcut(shortcutId);
if (shortcut != nullptr)
{
shortcut->Current.clear();
auto input = ConvertLegacyBinding(value);
if (input.has_value())
{
shortcut->Current.push_back(std::move(input.value()));
}
}
}
}
}
}
void ShortcutManager::LoadUserBindings(const fs::path& path)
{
auto root = Json::ReadFromFile(path.u8string());
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<std::string>());
}
else if (value.is_array())
{
for (auto& subValue : value)
{
shortcut->Current.emplace_back(subValue.get<std::string>());
}
}
}
}
}
}
void ShortcutManager::SaveUserBindings()
{
try
{
auto path = fs::u8path(_env.GetFilePath(PathId::configShortcuts));
SaveUserBindings(path);
}
catch (const std::exception& e)
{
Console::Error::WriteLine("Unable to save shortcut bindings: %s", e.what());
}
}
void ShortcutManager::SaveUserBindings(const fs::path& path)
{
json_t root;
if (fs::exists(path))
{
root = Json::ReadFromFile(path.u8string());
}
for (const auto& shortcut : Shortcuts)
{
auto& jShortcut = root[shortcut.second.Id];
if (shortcut.second.Current.size() == 1)
{
jShortcut = shortcut.second.Current[0].ToString();
}
else
{
jShortcut = nlohmann::json::array();
for (const auto& binding : shortcut.second.Current)
{
jShortcut.push_back(binding.ToString());
}
}
}
Json::WriteToFile(path.u8string(), root);
}
std::string_view ShortcutManager::GetLegacyShortcutId(size_t index)
{
static constexpr std::string_view _legacyMap[] = {
ShortcutId::kInterfaceCloseTop,
ShortcutId::kInterfaceCloseAll,
ShortcutId::kInterfaceCancelConstruction,
ShortcutId::kInterfacePause,
ShortcutId::kViewGeneralZoomOut,
ShortcutId::kViewGeneralZoomIn,
ShortcutId::kViewGeneralRotateClockwise,
ShortcutId::kViewGeneralRotateAnticlockwise,
ShortcutId::kInterfaceRotateConstruction,
ShortcutId::kViewToggleUnderground,
ShortcutId::kViewToggleBaseLand,
ShortcutId::kViewToggleVerticalLand,
ShortcutId::kViewToggleRides,
ShortcutId::kViewToggleScenery,
ShortcutId::kViewToggleSupports,
ShortcutId::kViewToggleGuests,
ShortcutId::kViewToggleLandHeightMarkers,
ShortcutId::kViewToggleTrackHeightMarkers,
ShortcutId::kViewToggleFootpathHeightMarkers,
ShortcutId::kInterfaceOpenLand,
ShortcutId::kInterfaceOpenWater,
ShortcutId::kInterfaceOpenScenery,
ShortcutId::kInterfaceOpenFootpaths,
ShortcutId::kInterfaceOpenNewRide,
ShortcutId::kInterfaceOpenFinances,
ShortcutId::kInterfaceOpenResearch,
ShortcutId::kInterfaceOpenRides,
ShortcutId::kInterfaceOpenPark,
ShortcutId::kInterfaceOpenGuests,
ShortcutId::kInterfaceOpenStaff,
ShortcutId::kInterfaceOpenMessages,
ShortcutId::kInterfaceOpenMap,
ShortcutId::kInterfaceScreenshot,
ShortcutId::kInterfaceDecreaseSpeed,
ShortcutId::kInterfaceIncreaseSpeed,
ShortcutId::kInterfaceOpenCheats,
ShortcutId::kInterfaceToggleToolbars,
ShortcutId::kViewScrollUp,
ShortcutId::kViewScrollLeft,
ShortcutId::kViewScrollDown,
ShortcutId::kViewScrollRight,
ShortcutId::kInterfaceMultiplayerChat,
ShortcutId::kInterfaceSaveGame,
ShortcutId::kInterfaceShowOptions,
ShortcutId::kInterfaceMute,
ShortcutId::kInterfaceScaleToggleWindowMode,
ShortcutId::kInterfaceMultiplayerShow,
std::string_view(),
ShortcutId::kDebugTogglePaintDebugWindow,
ShortcutId::kViewToggleFootpaths,
ShortcutId::kWindowRideConstructionTurnLeft,
ShortcutId::kWindowRideConstructionTurnRight,
ShortcutId::kWindowRideConstructionDefault,
ShortcutId::kWindowRideConstructionSlopeDown,
ShortcutId::kWindowRideConstructionSlopeUp,
ShortcutId::kWindowRideConstructionChainLift,
ShortcutId::kWindowRideConstructionBankLeft,
ShortcutId::kWindowRideConstructionBankRight,
ShortcutId::kWindowRideConstructionPrevious,
ShortcutId::kWindowRideConstructionNext,
ShortcutId::kWindowRideConstructionBuild,
ShortcutId::kWindowRideConstructionDemolish,
ShortcutId::kInterfaceLoadGame,
ShortcutId::kInterfaceClearScenery,
ShortcutId::kViewToggleGridlines,
ShortcutId::kViewToggleCutAway,
ShortcutId::kViewToggleFootpathIssues,
ShortcutId::kInterfaceOpenTileInspector,
ShortcutId::kDebugAdvanceTick,
ShortcutId::kInterfaceSceneryPicker,
ShortcutId::kInterfaceScaleIncrease,
ShortcutId::kInterfaceScaleDecrease,
ShortcutId::kWindowTileInspectorToggleInvisibility,
ShortcutId::kWindowTileInspectorCopy,
ShortcutId::kWindowTileInspectorPaste,
ShortcutId::kWindowTileInspectorRemove,
ShortcutId::kWindowTileInspectorMoveUp,
ShortcutId::kWindowTileInspectorMoveDown,
ShortcutId::kWindowTileInspectorIncreaseX,
ShortcutId::kWindowTileInspectorDecreaseX,
ShortcutId::kWindowTileInspectorIncreaseY,
ShortcutId::kWindowTileInspectorDecreaseY,
ShortcutId::kWindowTileInspectorIncreaseHeight,
ShortcutId::kWindowTileInspectorDecreaseHeight,
ShortcutId::kInterfaceDisableClearance,
};
return index < std::size(_legacyMap) ? _legacyMap[index] : std::string_view();
}