_MatchChordInputHotkey 和 _MatchModifierReleaseHotkey 在 OnTestKeyDown、 OnKeyDown、OnTestKeyUp、OnKeyUp 里都要调 ReadConfiguredSwitchLanguageHotkeys, 每次都查注册表 DataDir、再逐行解析整个 config.toml,一次按键最多四遍, 全在宿主 UI 线程上,不在组合里的按键也照付。 配置路径每个进程只解析一次(环境变量固定,DataDir 只在重装时变);解析 结果按文件修改时间和大小缓存,最多每秒检查一次时间戳,设置页改了热键一秒 内生效。缓存全部常量初始化,路径对象按需分配且不释放,不引入全局构造器。
545 lines
16 KiB
C++
545 lines
16 KiB
C++
#include <algorithm>
|
|
#include <cstdlib>
|
|
#include <cwchar>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <string>
|
|
#include "Define.h"
|
|
#include "Globals.h"
|
|
#include "FanyUtils.h"
|
|
#include "Ipc.h"
|
|
#include <utf8cpp/utf8.h>
|
|
#include <fmt/xchar.h>
|
|
|
|
using namespace std;
|
|
|
|
namespace FanyUtils
|
|
{
|
|
namespace
|
|
{
|
|
std::string TrimAscii(const std::string &value)
|
|
{
|
|
size_t begin = 0;
|
|
while (begin < value.size() && (value[begin] == ' ' || value[begin] == '\t' || value[begin] == '\r'))
|
|
{
|
|
++begin;
|
|
}
|
|
size_t end = value.size();
|
|
while (end > begin && (value[end - 1] == ' ' || value[end - 1] == '\t' || value[end - 1] == '\r'))
|
|
{
|
|
--end;
|
|
}
|
|
return value.substr(begin, end - begin);
|
|
}
|
|
|
|
std::string UnquoteTomlBasicString(const std::string &value)
|
|
{
|
|
if (value.size() >= 2 && value.front() == '"' && value.back() == '"')
|
|
{
|
|
return value.substr(1, value.size() - 2);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
bool ParseTomlBool(const std::string &raw, bool fallback)
|
|
{
|
|
const std::string value = to_lower_copy(UnquoteTomlBasicString(TrimAscii(raw)));
|
|
if (value == "true" || value == "1")
|
|
{
|
|
return true;
|
|
}
|
|
if (value == "false" || value == "0")
|
|
{
|
|
return false;
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
// Build a wide path and open it as such. A narrow std::string path would be opened through the
|
|
// ANSI code page, which cannot round-trip a non-ASCII (e.g. Chinese) user profile path on a
|
|
// non-UTF-8 system, so the TSF would read the wrong file or fail to find the config.
|
|
// GetEnvironmentVariableW rather than _wgetenv: the CRT variant is deprecated (C4996) because
|
|
// it hands out a pointer into a buffer another thread can invalidate, and this DLL runs inside
|
|
// arbitrary hosts.
|
|
std::wstring EnvironmentValue(const wchar_t *name)
|
|
{
|
|
std::wstring value(MAX_PATH, L'\0');
|
|
DWORD length = GetEnvironmentVariableW(name, value.data(), static_cast<DWORD>(value.size()));
|
|
if (length > value.size())
|
|
{
|
|
// The variable is longer than MAX_PATH; length is now the required size including the NUL.
|
|
value.resize(length);
|
|
length = GetEnvironmentVariableW(name, value.data(), static_cast<DWORD>(value.size()));
|
|
}
|
|
if (length == 0 || length > value.size())
|
|
{
|
|
return {};
|
|
}
|
|
value.resize(length);
|
|
return value;
|
|
}
|
|
|
|
// The data directory the user picked during setup. This DLL is also built 32-bit and loads into
|
|
// 32-bit hosts, so KEY_WOW64_64KEY is mandatory: without it the read is redirected to
|
|
// Wow6432Node, where the 64-bit setup never wrote anything.
|
|
std::wstring InstalledDataDir()
|
|
{
|
|
HKEY key = nullptr;
|
|
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"Software\\Metasequoia\\MetasequoiaIME", 0,
|
|
KEY_QUERY_VALUE | KEY_WOW64_64KEY, &key) != ERROR_SUCCESS)
|
|
{
|
|
return {};
|
|
}
|
|
|
|
DWORD type = 0;
|
|
DWORD bytes = 0;
|
|
if (RegQueryValueExW(key, L"DataDir", nullptr, &type, nullptr, &bytes) != ERROR_SUCCESS || type != REG_SZ ||
|
|
bytes < sizeof(wchar_t))
|
|
{
|
|
RegCloseKey(key);
|
|
return {};
|
|
}
|
|
|
|
std::wstring value(bytes / sizeof(wchar_t), L'\0');
|
|
const LSTATUS status =
|
|
RegQueryValueExW(key, L"DataDir", nullptr, &type, reinterpret_cast<LPBYTE>(value.data()), &bytes);
|
|
RegCloseKey(key);
|
|
if (status != ERROR_SUCCESS)
|
|
{
|
|
return {};
|
|
}
|
|
// REG_SZ carries no guaranteed terminator and the stored length may or may not include one.
|
|
value.resize(std::wcslen(value.c_str()));
|
|
return value;
|
|
}
|
|
|
|
// Same resolution order as the Server (server/src/utils/ime_paths.cpp) and the engine
|
|
// (engine/core/data_path.h): environment override, then the location chosen during setup, then
|
|
// the default under LocalAppData. All three must agree or the TSF reads a different config than
|
|
// the one the Server writes.
|
|
std::filesystem::path SharedDataDirectory()
|
|
{
|
|
const std::wstring fromEnv = EnvironmentValue(L"METASEQUOIA_IME_DATA_DIR");
|
|
if (!fromEnv.empty() && std::filesystem::path(fromEnv).is_absolute())
|
|
{
|
|
return fromEnv;
|
|
}
|
|
|
|
const std::wstring installed = InstalledDataDir();
|
|
if (!installed.empty() && std::filesystem::path(installed).is_absolute())
|
|
{
|
|
return installed;
|
|
}
|
|
|
|
const std::wstring localAppData = EnvironmentValue(L"LOCALAPPDATA");
|
|
if (localAppData.empty())
|
|
{
|
|
return {};
|
|
}
|
|
return std::filesystem::path(localAppData) / L"metasequoiaime";
|
|
}
|
|
|
|
std::filesystem::path SharedConfigPath()
|
|
{
|
|
const std::filesystem::path directory = SharedDataDirectory();
|
|
if (directory.empty())
|
|
{
|
|
return {};
|
|
}
|
|
return directory / L"config.toml";
|
|
}
|
|
} // namespace
|
|
|
|
BOOL ReadConfiguredDefaultImeModeChinese()
|
|
{
|
|
const std::filesystem::path configPath = SharedConfigPath();
|
|
if (configPath.empty())
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
std::ifstream input(configPath);
|
|
if (!input)
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
bool inInputSection = false;
|
|
std::string line;
|
|
while (std::getline(input, line))
|
|
{
|
|
const size_t comment = line.find('#');
|
|
if (comment != std::string::npos)
|
|
{
|
|
line = line.substr(0, comment);
|
|
}
|
|
line = TrimAscii(line);
|
|
if (line.empty())
|
|
{
|
|
continue;
|
|
}
|
|
if (line.front() == '[' && line.back() == ']')
|
|
{
|
|
inInputSection = (line == "[input]");
|
|
continue;
|
|
}
|
|
if (!inInputSection)
|
|
{
|
|
continue;
|
|
}
|
|
const size_t eq = line.find('=');
|
|
if (eq == std::string::npos)
|
|
{
|
|
continue;
|
|
}
|
|
const std::string key = TrimAscii(line.substr(0, eq));
|
|
if (key != "default_ime_mode")
|
|
{
|
|
continue;
|
|
}
|
|
const std::string value = to_lower_copy(UnquoteTomlBasicString(TrimAscii(line.substr(eq + 1))));
|
|
return value != "english";
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
int ReadConfiguredPunctuationLock()
|
|
{
|
|
const std::filesystem::path configPath = SharedConfigPath();
|
|
if (configPath.empty())
|
|
{
|
|
return Global::PunctuationLock::Follow;
|
|
}
|
|
|
|
std::ifstream input(configPath);
|
|
if (!input)
|
|
{
|
|
return Global::PunctuationLock::Follow;
|
|
}
|
|
|
|
bool inInputSection = false;
|
|
std::string line;
|
|
while (std::getline(input, line))
|
|
{
|
|
const size_t comment = line.find('#');
|
|
if (comment != std::string::npos)
|
|
{
|
|
line = line.substr(0, comment);
|
|
}
|
|
line = TrimAscii(line);
|
|
if (line.empty())
|
|
{
|
|
continue;
|
|
}
|
|
if (line.front() == '[' && line.back() == ']')
|
|
{
|
|
inInputSection = (line == "[input]");
|
|
continue;
|
|
}
|
|
if (!inInputSection)
|
|
{
|
|
continue;
|
|
}
|
|
const size_t eq = line.find('=');
|
|
if (eq == std::string::npos)
|
|
{
|
|
continue;
|
|
}
|
|
const std::string key = TrimAscii(line.substr(0, eq));
|
|
if (key != "punctuation_lock")
|
|
{
|
|
continue;
|
|
}
|
|
const std::string value = to_lower_copy(UnquoteTomlBasicString(TrimAscii(line.substr(eq + 1))));
|
|
if (value == "chinese")
|
|
{
|
|
return Global::PunctuationLock::AlwaysChinese;
|
|
}
|
|
if (value == "english")
|
|
{
|
|
return Global::PunctuationLock::AlwaysEnglish;
|
|
}
|
|
return Global::PunctuationLock::Follow;
|
|
}
|
|
return Global::PunctuationLock::Follow;
|
|
}
|
|
|
|
void RefreshPunctuationLockFromConfig()
|
|
{
|
|
Global::PunctuationLockMode.store(ReadConfiguredPunctuationLock(), std::memory_order_relaxed);
|
|
}
|
|
|
|
BOOL ReadConfiguredJapaneseInputMode()
|
|
{
|
|
const std::filesystem::path configPath = SharedConfigPath();
|
|
if (configPath.empty())
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
std::ifstream input(configPath);
|
|
if (!input)
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
bool inInputSection = false;
|
|
std::string line;
|
|
while (std::getline(input, line))
|
|
{
|
|
const size_t comment = line.find('#');
|
|
if (comment != std::string::npos)
|
|
{
|
|
line = line.substr(0, comment);
|
|
}
|
|
line = TrimAscii(line);
|
|
if (line.empty())
|
|
{
|
|
continue;
|
|
}
|
|
if (line.front() == '[' && line.back() == ']')
|
|
{
|
|
inInputSection = (line == "[input]");
|
|
continue;
|
|
}
|
|
if (!inInputSection)
|
|
{
|
|
continue;
|
|
}
|
|
const size_t eq = line.find('=');
|
|
if (eq == std::string::npos)
|
|
{
|
|
continue;
|
|
}
|
|
const std::string key = TrimAscii(line.substr(0, eq));
|
|
if (key != "mode")
|
|
{
|
|
continue;
|
|
}
|
|
const std::string value = to_lower_copy(UnquoteTomlBasicString(TrimAscii(line.substr(eq + 1))));
|
|
return value == "japanese";
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
namespace
|
|
{
|
|
SwitchLanguageHotkeys ParseSwitchLanguageHotkeys(const std::filesystem::path &configPath)
|
|
{
|
|
SwitchLanguageHotkeys result;
|
|
std::ifstream input(configPath);
|
|
if (!input)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
bool inKeybindings = false;
|
|
bool sawShift = false;
|
|
bool sawCtrl = false;
|
|
bool sawCtrlAltSpace = false;
|
|
std::string line;
|
|
while (std::getline(input, line))
|
|
{
|
|
const size_t comment = line.find('#');
|
|
if (comment != std::string::npos)
|
|
{
|
|
line = line.substr(0, comment);
|
|
}
|
|
line = TrimAscii(line);
|
|
if (line.empty())
|
|
{
|
|
continue;
|
|
}
|
|
if (line.front() == '[' && line.back() == ']')
|
|
{
|
|
inKeybindings = (line == "[keybindings]");
|
|
continue;
|
|
}
|
|
if (!inKeybindings)
|
|
{
|
|
continue;
|
|
}
|
|
const size_t eq = line.find('=');
|
|
if (eq == std::string::npos)
|
|
{
|
|
continue;
|
|
}
|
|
const std::string key = TrimAscii(line.substr(0, eq));
|
|
const std::string raw = line.substr(eq + 1);
|
|
if (key == "switch_language_shift")
|
|
{
|
|
result.shift = ParseTomlBool(raw, true);
|
|
sawShift = true;
|
|
}
|
|
else if (key == "switch_language_ctrl")
|
|
{
|
|
result.ctrl = ParseTomlBool(raw, false);
|
|
sawCtrl = true;
|
|
}
|
|
else if (key == "switch_language_ctrl_alt_space")
|
|
{
|
|
result.ctrl_alt_space = ParseTomlBool(raw, true);
|
|
sawCtrlAltSpace = true;
|
|
}
|
|
else if (key == "toggle_character_set_ctrl_shift_f")
|
|
{
|
|
result.character_set_ctrl_shift_f = ParseTomlBool(raw, true);
|
|
}
|
|
else if (key == "switch_language" && !sawShift && !sawCtrlAltSpace)
|
|
{
|
|
// Legacy array: switch_language = ["Ctrl+Space", "Shift"]
|
|
result.shift = raw.find("Shift") != std::string::npos;
|
|
result.ctrl_alt_space =
|
|
raw.find("Ctrl+Alt+Space") != std::string::npos || raw.find("Ctrl+Space") != std::string::npos;
|
|
}
|
|
}
|
|
(void)sawCtrl;
|
|
return result;
|
|
}
|
|
|
|
// The key sink asks for these hotkeys on every key down and key up, on the host's UI thread.
|
|
// Resolving the path (environment + registry) and parsing the whole config.toml each time cost
|
|
// up to four file reads per keystroke. The path cannot change for the life of the host process
|
|
// (the environment is fixed and DataDir only changes on reinstall), so it is resolved once; the
|
|
// file is re-parsed only when its timestamp or size changes, checked at most once per interval
|
|
// so a settings change still lands within a second. Everything here is constant-initialized:
|
|
// this DLL avoids global constructors and is built with /Zc:threadSafeInit-.
|
|
constexpr ULONGLONG kHotkeyConfigRecheckMs = 1000;
|
|
|
|
struct HotkeyConfigCache
|
|
{
|
|
bool pathResolved = false;
|
|
bool haveStamp = false;
|
|
ULONGLONG checkedTick = 0;
|
|
FILETIME lastWrite = {};
|
|
DWORD sizeLow = 0;
|
|
DWORD sizeHigh = 0;
|
|
SwitchLanguageHotkeys value;
|
|
};
|
|
|
|
SRWLOCK g_hotkeyConfigLock = SRWLOCK_INIT;
|
|
HotkeyConfigCache g_hotkeyConfig;
|
|
// Heap-allocated on first use and intentionally never freed, so it needs no global destructor.
|
|
std::filesystem::path *g_hotkeyConfigPath = nullptr;
|
|
} // namespace
|
|
|
|
SwitchLanguageHotkeys ReadConfiguredSwitchLanguageHotkeys()
|
|
{
|
|
AcquireSRWLockExclusive(&g_hotkeyConfigLock);
|
|
SwitchLanguageHotkeys result = g_hotkeyConfig.value;
|
|
try
|
|
{
|
|
if (!g_hotkeyConfig.pathResolved)
|
|
{
|
|
g_hotkeyConfig.pathResolved = true;
|
|
const std::filesystem::path configPath = SharedConfigPath();
|
|
if (!configPath.empty())
|
|
{
|
|
g_hotkeyConfigPath = new std::filesystem::path(configPath);
|
|
}
|
|
}
|
|
|
|
const ULONGLONG now = GetTickCount64();
|
|
const bool due = !g_hotkeyConfig.haveStamp || now - g_hotkeyConfig.checkedTick >= kHotkeyConfigRecheckMs;
|
|
if (g_hotkeyConfigPath != nullptr && due)
|
|
{
|
|
g_hotkeyConfig.checkedTick = now;
|
|
WIN32_FILE_ATTRIBUTE_DATA attributes{};
|
|
if (GetFileAttributesExW(g_hotkeyConfigPath->c_str(), GetFileExInfoStandard, &attributes))
|
|
{
|
|
const bool changed = !g_hotkeyConfig.haveStamp ||
|
|
CompareFileTime(&attributes.ftLastWriteTime, &g_hotkeyConfig.lastWrite) != 0 ||
|
|
attributes.nFileSizeLow != g_hotkeyConfig.sizeLow ||
|
|
attributes.nFileSizeHigh != g_hotkeyConfig.sizeHigh;
|
|
if (changed)
|
|
{
|
|
g_hotkeyConfig.value = ParseSwitchLanguageHotkeys(*g_hotkeyConfigPath);
|
|
g_hotkeyConfig.lastWrite = attributes.ftLastWriteTime;
|
|
g_hotkeyConfig.sizeLow = attributes.nFileSizeLow;
|
|
g_hotkeyConfig.sizeHigh = attributes.nFileSizeHigh;
|
|
g_hotkeyConfig.haveStamp = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// A missing config means defaults, as the uncached read returned; the stamp
|
|
// stays unset so the file is picked up as soon as the Server writes it.
|
|
g_hotkeyConfig.value = SwitchLanguageHotkeys{};
|
|
g_hotkeyConfig.haveStamp = false;
|
|
}
|
|
}
|
|
result = g_hotkeyConfig.value;
|
|
}
|
|
catch (...)
|
|
{
|
|
// Keep the last known hotkeys; a read failure must not take the key path down.
|
|
}
|
|
ReleaseSRWLockExclusive(&g_hotkeyConfigLock);
|
|
return result;
|
|
}
|
|
|
|
void SendKeys(std::wstring pinyin)
|
|
{
|
|
for (wchar_t ch : pinyin)
|
|
{
|
|
INPUT in[2]{};
|
|
|
|
in[0].type = INPUT_KEYBOARD;
|
|
in[0].ki.wScan = ch;
|
|
in[0].ki.dwFlags = KEYEVENTF_UNICODE;
|
|
|
|
in[1] = in[0];
|
|
in[1].ki.dwFlags |= KEYEVENTF_KEYUP;
|
|
|
|
UINT sent = SendInput(2, in, sizeof(INPUT));
|
|
if (sent != 2)
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
std::wstring string_to_wstring(const std::string &str)
|
|
{
|
|
std::u16string utf16result;
|
|
utf8::utf8to16(str.begin(), str.end(), std::back_inserter(utf16result));
|
|
return std::wstring(utf16result.begin(), utf16result.end());
|
|
}
|
|
|
|
std::string wstring_to_string(const std::wstring &wstr)
|
|
{
|
|
std::string result;
|
|
utf8::utf16to8(wstr.begin(), wstr.end(), std::back_inserter(result));
|
|
return result;
|
|
}
|
|
|
|
std::string to_lower_copy(const std::string &str)
|
|
{
|
|
std::string result = str;
|
|
std::transform(result.begin(), result.end(), result.begin(), [](unsigned char c) { return std::tolower(c); });
|
|
return result;
|
|
}
|
|
|
|
std::wstring GetCurrentProcessName()
|
|
{
|
|
TCHAR fullPath[MAX_PATH] = {0};
|
|
if (GetModuleFileName(NULL, fullPath, MAX_PATH) == 0)
|
|
return L"";
|
|
|
|
std::wstring wfullPath(fullPath);
|
|
size_t pos = wfullPath.find_last_of(L"\\/");
|
|
std::wstring wname = (pos != std::wstring::npos) ? wfullPath.substr(pos + 1) : wfullPath;
|
|
return wname;
|
|
}
|
|
|
|
/**
|
|
* @brief Count UTF-8 chars
|
|
*
|
|
* @param str
|
|
* @return string::size_type
|
|
*/
|
|
string::size_type count_utf8_chars(const string &str)
|
|
{
|
|
return utf8::distance(str.begin(), str.end());
|
|
}
|
|
} // namespace FanyUtils
|