mirror of
https://github.com/shadps4-emu/shadPS4.git
synced 2025-12-09 21:31:04 +00:00
Ime fixes (#3731)
* Changes -Added support for OrbisImeParamExtended (extended IME parameters) in ImeHandler, ImeState, and ImeUi -Updated all relevant constructors and logic to propagate and store the extended parameter - Now fully supports passing extended options from sceImeOpen to the IME UI and backend * Potential CUSA00434 [Debug] <Critical> assert.cpp:30 assert_fail_debug_msg: Assertion Failed! buf_len + 1 <= buf_size && "Is your input buffer properly zero-terminated?" at C:/VS/shadPS4-ime-fixes/externals/dear_imgui/imgui_widgets.cpp:4601 fix * Attempting to resolve an assertion failure in Diablo III: - Adjusted buffer sizes - Updated the calculation of text‑length values * ime-lib another hotfix Fixed incorrect param->title validation, which caused the IME dialog to fail to appear in Stardew Valley. Need to be checked. * Clang fix * FF9 ImeDialog Hotfix * Removed the validation that disallowed null text and null placeholder, since using null values is valid in `ImeDialog`. * Added additional debug logs to aid troubleshooting. * IME Fixes - Add missing flags to `OrbisImeExtOption` - Improve debug logging - Resolve nonstop `sceImeKeyboardOpen` calls in Stardew Valley (MonoGame engine) for `userId = 254` * IME: guard null params for CUSA04909 - Add null checks in IME constructors to prevent crashes seen in CUSA04909. - Leave a clear note about deferring keyboard event dispatch until guest-space translation is ready. * Some improvements - Added debug logs so every IME event and host callback (text/caret updates) shows what the guest sent back. - Updated ImeState to respect the guest’s text-length limit, keep buffers in sync, and record caret/text changes without duplicates. - Fixed shutdown by actually destroying the handler on close and letting sceImeUpdate exit quietly once the IME is gone. * CLang * IME: simplify handlers, add param checks, fix caret index - Unify ImeHandler init; support optional OrbisImeParamExtended; drop userId from keyboard handler. - Add basic null checks for work and inputTextBuffer; early error logging. - Fixed incorrect caret position. Make caret and text area indices 1-based in ImeUi::InputTextCallback. - Set default user_id to ORBIS_USER_SERVICE_USER_ID_INVALID in sceImeParamInit. - Reduce noisy debug logs; promote key calls to LOG_INFO. - Remove unused extended fields from ImeState; minor cleanups. * IME: text/caret sync fixes; add Enter payload - Sync UI input and work buffers on UpdateText - Sync caret position on mouse click by emiting multiple UpdateCaret events for jumps (loop over delta) - Add text payload to PressEnter (and Close); fixes IME in God Eater 2 - Queue initial Open event after open - Fix UTF-8 → UTF-16 conversion bounds - Add debug logs for all queued events * CLang * fixed accidental copy / paste replacement in text update event that broke text deletion. * IME: Add code-point limited InputText and use in IME UI - Add InputTextExLimited helper to cap Unicode code points and forward callbacks - Switch IME input to InputTextExLimited with ime_param->maxTextLength and CallbackAlways --------- Co-authored-by: w1naenator <valdis.bogdans@hotmail.com>
This commit is contained in:
@@ -1,10 +1,6 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <array>
|
|
||||||
#include <string>
|
|
||||||
#include <string_view>
|
|
||||||
#include <queue>
|
#include <queue>
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
#include "core/libraries/ime/ime.h"
|
#include "core/libraries/ime/ime.h"
|
||||||
@@ -21,83 +17,47 @@ static ImeUi g_ime_ui;
|
|||||||
|
|
||||||
class ImeHandler {
|
class ImeHandler {
|
||||||
public:
|
public:
|
||||||
ImeHandler(const OrbisImeKeyboardParam* param,
|
ImeHandler(const OrbisImeKeyboardParam* param) {
|
||||||
Libraries::UserService::OrbisUserServiceUserId userId)
|
Init(param, nullptr, false);
|
||||||
: m_user_id(userId) {
|
|
||||||
LOG_DEBUG(Lib_Ime, "Creating ImeHandler for keyboard (user {})", userId);
|
|
||||||
LOG_DEBUG(Lib_Ime, "Keyboard constructor received param_ptr={:p}",
|
|
||||||
static_cast<const void*>(param));
|
|
||||||
if (!param) {
|
|
||||||
LOG_ERROR(Lib_Ime, "ImeHandler keyboard constructor received null param");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Init(param, false);
|
|
||||||
}
|
}
|
||||||
ImeHandler(const OrbisImeParam* param, const OrbisImeParamExtended* extended = nullptr) {
|
ImeHandler(const OrbisImeParam* param, const OrbisImeParamExtended* extended = nullptr) {
|
||||||
LOG_DEBUG(Lib_Ime, "Creating ImeHandler for IME");
|
|
||||||
LOG_DEBUG(Lib_Ime, "IME constructor received param_ptr={:p} extended_ptr={:p}",
|
LOG_DEBUG(Lib_Ime, "param->work: 0x{:X}",
|
||||||
static_cast<const void*>(param), static_cast<const void*>(extended));
|
static_cast<u64>(reinterpret_cast<uintptr_t>(param->work)));
|
||||||
if (!param) {
|
LOG_DEBUG(Lib_Ime, "param->inputTextBuffer: 0x{:X}",
|
||||||
LOG_ERROR(Lib_Ime, "ImeHandler IME constructor received null param");
|
static_cast<u64>(reinterpret_cast<uintptr_t>(param->arg)));
|
||||||
return;
|
Init(param, extended, true);
|
||||||
}
|
|
||||||
if (extended) {
|
|
||||||
m_extended = *extended;
|
|
||||||
m_has_extended = true;
|
|
||||||
}
|
|
||||||
Init(param, true);
|
|
||||||
}
|
}
|
||||||
~ImeHandler() = default;
|
~ImeHandler() = default;
|
||||||
|
|
||||||
void Init(const void* param, bool ime_mode) {
|
void Init(const void* param, const OrbisImeParamExtended* extended, bool ime_mode) {
|
||||||
LOG_DEBUG(Lib_Ime, "ImeHandler::Init begin (ime_mode={}, param_ptr={:p})", ime_mode, param);
|
|
||||||
|
|
||||||
if (ime_mode) {
|
if (ime_mode) {
|
||||||
LOG_DEBUG(Lib_Ime, "Copying OrbisImeParam from guest");
|
|
||||||
m_param.ime = *(OrbisImeParam*)param;
|
m_param.ime = *(OrbisImeParam*)param;
|
||||||
|
LOG_DEBUG(Lib_Ime, "m_param.ime.work: 0x{:X}",
|
||||||
|
static_cast<u64>(reinterpret_cast<uintptr_t>(m_param.ime.work)));
|
||||||
|
LOG_DEBUG(Lib_Ime, "m_param.ime.inputTextBuffer: 0x{:X}",
|
||||||
|
static_cast<u64>(reinterpret_cast<uintptr_t>(m_param.ime.inputTextBuffer)));
|
||||||
|
if (extended)
|
||||||
|
m_param.ime_ext = *extended;
|
||||||
|
else
|
||||||
|
m_param.ime_ext = {};
|
||||||
} else {
|
} else {
|
||||||
LOG_DEBUG(Lib_Ime, "Copying OrbisImeKeyboardParam from guest");
|
|
||||||
m_param.key = *(OrbisImeKeyboardParam*)param;
|
m_param.key = *(OrbisImeKeyboardParam*)param;
|
||||||
}
|
}
|
||||||
m_ime_mode = ime_mode;
|
m_ime_mode = ime_mode;
|
||||||
LOG_DEBUG(Lib_Ime, "Ime mode flag set to {}", ime_mode);
|
|
||||||
|
|
||||||
// Open an event to let the game know the IME has started
|
// Open an event to let the game know the IME has started
|
||||||
OrbisImeEvent openEvent{};
|
OrbisImeEvent openEvent{};
|
||||||
openEvent.id = (ime_mode ? OrbisImeEventId::Open : OrbisImeEventId::KeyboardOpen);
|
openEvent.id = (ime_mode ? OrbisImeEventId::Open : OrbisImeEventId::KeyboardOpen);
|
||||||
LOG_DEBUG(Lib_Ime, "Prepared initial event with id {}", static_cast<u32>(openEvent.id));
|
|
||||||
|
|
||||||
if (ime_mode) {
|
if (ime_mode) {
|
||||||
LOG_DEBUG(Lib_Ime, "calling sceImeGetPanelSize");
|
sceImeGetPanelSize(&m_param.ime, &openEvent.param.rect.width,
|
||||||
Error e = sceImeGetPanelSize(&m_param.ime, &openEvent.param.rect.width,
|
|
||||||
&openEvent.param.rect.height);
|
&openEvent.param.rect.height);
|
||||||
if (e != Error::OK) {
|
|
||||||
LOG_ERROR(Lib_Ime, "sceImeGetPanelSize returned 0x{:X}", static_cast<u32>(e));
|
|
||||||
} else {
|
|
||||||
LOG_DEBUG(Lib_Ime, "Panel size width={} height={}", openEvent.param.rect.width,
|
|
||||||
openEvent.param.rect.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
openEvent.param.rect.x = m_param.ime.posx;
|
openEvent.param.rect.x = m_param.ime.posx;
|
||||||
openEvent.param.rect.y = m_param.ime.posy;
|
openEvent.param.rect.y = m_param.ime.posy;
|
||||||
LOG_DEBUG(Lib_Ime, "Panel position set to ({}, {})", openEvent.param.rect.x,
|
|
||||||
openEvent.param.rect.y);
|
|
||||||
} else {
|
} else {
|
||||||
LOG_DEBUG(Lib_Ime, "Initializing keyboard handler for user {}", m_user_id);
|
openEvent.param.resource_id_array.user_id = 1;
|
||||||
|
openEvent.param.resource_id_array.resource_id[0] = 1;
|
||||||
// Report the real PS4 user ID, and mark “no physical keyboard” so games know
|
|
||||||
openEvent.param.resource_id_array.user_id = m_user_id;
|
|
||||||
for (auto& rid : openEvent.param.resource_id_array.resource_id) {
|
|
||||||
rid = -1;
|
|
||||||
}
|
|
||||||
LOG_DEBUG(Lib_Ime, "Reporting virtual keyboard resource ids initialized to -1");
|
|
||||||
LOG_DEBUG(Lib_Ime, "Dispatching initial keyboard event to handler 0x{:X} (arg={:p})",
|
|
||||||
static_cast<u64>(reinterpret_cast<uintptr_t>(m_param.key.handler)),
|
|
||||||
static_cast<const void*>(m_param.key.arg));
|
|
||||||
// The guest handler expects PS4 address space; skip dispatch until we provide a proper
|
|
||||||
// translation.
|
|
||||||
LOG_WARNING(Lib_Ime, "Skipping initial keyboard event dispatch (guest handler requires "
|
|
||||||
"guest-accessible payload)");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Are we supposed to call the event handler on init with
|
// Are we supposed to call the event handler on init with
|
||||||
@@ -107,13 +67,15 @@ public:
|
|||||||
}*/
|
}*/
|
||||||
|
|
||||||
if (ime_mode) {
|
if (ime_mode) {
|
||||||
LOG_DEBUG(Lib_Ime, "Creating IME state and UI instances");
|
g_ime_state = ImeState(&m_param.ime, &m_param.ime_ext);
|
||||||
g_ime_state = ImeState(&m_param.ime, m_has_extended ? &m_extended : nullptr);
|
g_ime_ui = ImeUi(&g_ime_state, &m_param.ime, &m_param.ime_ext);
|
||||||
g_ime_ui = ImeUi(&g_ime_state, &m_param.ime, m_has_extended ? &m_extended : nullptr);
|
|
||||||
LOG_DEBUG(Lib_Ime, "ImeState and ImeUi initialized");
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_DEBUG(Lib_Ime, "ImeHandler::Init complete");
|
// Queue the Open event so it is delivered on next sceImeUpdate
|
||||||
|
LOG_DEBUG(Lib_Ime, "IME Event queued: Open rect x={}, y={}, w={}, h={}",
|
||||||
|
openEvent.param.rect.x, openEvent.param.rect.y, openEvent.param.rect.width,
|
||||||
|
openEvent.param.rect.height);
|
||||||
|
g_ime_state.SendEvent(&openEvent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Error Update(OrbisImeEventHandler handler) {
|
Error Update(OrbisImeEventHandler handler) {
|
||||||
@@ -127,13 +89,6 @@ public:
|
|||||||
while (!g_ime_state.event_queue.empty()) {
|
while (!g_ime_state.event_queue.empty()) {
|
||||||
OrbisImeEvent event = g_ime_state.event_queue.front();
|
OrbisImeEvent event = g_ime_state.event_queue.front();
|
||||||
g_ime_state.event_queue.pop();
|
g_ime_state.event_queue.pop();
|
||||||
|
|
||||||
const auto event_id = event.id;
|
|
||||||
const auto event_name = magic_enum::enum_name(event_id);
|
|
||||||
const char* event_name_cstr = event_name.empty() ? "Unknown" : event_name.data();
|
|
||||||
LOG_DEBUG(Lib_Ime, "ImeHandler::Update dispatching event_id={} ({}) remaining_queue={}",
|
|
||||||
static_cast<u32>(event_id), event_name_cstr, g_ime_state.event_queue.size());
|
|
||||||
|
|
||||||
Execute(handler, &event, false);
|
Execute(handler, &event, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,17 +96,6 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Execute(OrbisImeEventHandler handler, OrbisImeEvent* event, bool use_param_handler) {
|
void Execute(OrbisImeEventHandler handler, OrbisImeEvent* event, bool use_param_handler) {
|
||||||
const auto param_arg = m_ime_mode ? static_cast<const void*>(m_param.ime.arg)
|
|
||||||
: static_cast<const void*>(m_param.key.arg);
|
|
||||||
const OrbisImeEventHandler target_handler =
|
|
||||||
use_param_handler ? (m_ime_mode ? m_param.ime.handler : m_param.key.handler) : handler;
|
|
||||||
LOG_DEBUG(Lib_Ime,
|
|
||||||
"Execute (ime_mode={}, use_param_handler={}, target_handler=0x{:X}, "
|
|
||||||
"param_arg={:p}, event={:p}, event_id={})",
|
|
||||||
m_ime_mode, use_param_handler,
|
|
||||||
static_cast<u64>(reinterpret_cast<uintptr_t>(target_handler)), param_arg,
|
|
||||||
static_cast<const void*>(event), event ? static_cast<int>(event->id) : -1);
|
|
||||||
|
|
||||||
if (m_ime_mode) {
|
if (m_ime_mode) {
|
||||||
OrbisImeParam param = m_param.ime;
|
OrbisImeParam param = m_param.ime;
|
||||||
if (use_param_handler) {
|
if (use_param_handler) {
|
||||||
@@ -174,34 +118,7 @@ public:
|
|||||||
LOG_WARNING(Lib_Ime, "ImeHandler::SetText received null text pointer");
|
LOG_WARNING(Lib_Ime, "ImeHandler::SetText received null text pointer");
|
||||||
return Error::INVALID_ADDRESS;
|
return Error::INVALID_ADDRESS;
|
||||||
}
|
}
|
||||||
|
g_ime_state.SetText(text, length);
|
||||||
std::size_t requested_length = length;
|
|
||||||
if (requested_length == 0) {
|
|
||||||
requested_length = std::char_traits<char16_t>::length(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr std::size_t kMaxOrbisLength = ORBIS_IME_MAX_TEXT_LENGTH;
|
|
||||||
const std::size_t effective_length =
|
|
||||||
std::min<std::size_t>(requested_length, kMaxOrbisLength);
|
|
||||||
|
|
||||||
std::array<char, ORBIS_IME_MAX_TEXT_LENGTH * 4 + 1> utf8_buffer{};
|
|
||||||
std::string preview;
|
|
||||||
if (g_ime_state.ConvertOrbisToUTF8(text, effective_length, utf8_buffer.data(),
|
|
||||||
utf8_buffer.size())) {
|
|
||||||
std::string_view utf8_view{utf8_buffer.data()};
|
|
||||||
constexpr std::size_t kPreviewLength = 64;
|
|
||||||
preview = std::string{utf8_view.substr(0, std::min(kPreviewLength, utf8_view.size()))};
|
|
||||||
if (utf8_view.size() > kPreviewLength) {
|
|
||||||
preview += "...";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
preview = "<conversion failed>";
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_DEBUG(Lib_Ime, "ImeHandler::SetText game feedback length={} (effective={}) preview={}",
|
|
||||||
length, effective_length, preview);
|
|
||||||
|
|
||||||
g_ime_state.SetText(text, static_cast<u32>(effective_length));
|
|
||||||
return Error::OK;
|
return Error::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,10 +127,6 @@ public:
|
|||||||
LOG_WARNING(Lib_Ime, "ImeHandler::SetCaret received null caret pointer");
|
LOG_WARNING(Lib_Ime, "ImeHandler::SetCaret received null caret pointer");
|
||||||
return Error::INVALID_ADDRESS;
|
return Error::INVALID_ADDRESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_DEBUG(Lib_Ime, "ImeHandler::SetCaret game feedback index={} pos=({}, {}) height={}",
|
|
||||||
caret->index, caret->x, caret->y, caret->height);
|
|
||||||
|
|
||||||
g_ime_state.SetCaret(caret->index);
|
g_ime_state.SetCaret(caret->index);
|
||||||
return Error::OK;
|
return Error::OK;
|
||||||
}
|
}
|
||||||
@@ -223,15 +136,12 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Libraries::UserService::OrbisUserServiceUserId m_user_id{};
|
struct ImeParam {
|
||||||
union ImeParam {
|
|
||||||
OrbisImeKeyboardParam key;
|
OrbisImeKeyboardParam key;
|
||||||
OrbisImeParam ime;
|
OrbisImeParam ime;
|
||||||
|
OrbisImeParamExtended ime_ext;
|
||||||
} m_param{};
|
} m_param{};
|
||||||
bool m_ime_mode = false;
|
bool m_ime_mode = false;
|
||||||
|
|
||||||
OrbisImeParamExtended m_extended{};
|
|
||||||
bool m_has_extended = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static std::unique_ptr<ImeHandler> g_ime_handler;
|
static std::unique_ptr<ImeHandler> g_ime_handler;
|
||||||
@@ -348,7 +258,7 @@ int PS4_SYSV_ABI sceImeGetPanelPositionAndForm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Error PS4_SYSV_ABI sceImeGetPanelSize(const OrbisImeParam* param, u32* width, u32* height) {
|
Error PS4_SYSV_ABI sceImeGetPanelSize(const OrbisImeParam* param, u32* width, u32* height) {
|
||||||
LOG_DEBUG(Lib_Ime, "sceImeGetPanelSize called");
|
LOG_INFO(Lib_Ime, "called");
|
||||||
|
|
||||||
if (!param) {
|
if (!param) {
|
||||||
LOG_ERROR(Lib_Ime, "Invalid param: NULL");
|
LOG_ERROR(Lib_Ime, "Invalid param: NULL");
|
||||||
@@ -406,7 +316,7 @@ Error PS4_SYSV_ABI sceImeGetPanelSize(const OrbisImeParam* param, u32* width, u3
|
|||||||
}
|
}
|
||||||
|
|
||||||
Error PS4_SYSV_ABI sceImeKeyboardClose(Libraries::UserService::OrbisUserServiceUserId userId) {
|
Error PS4_SYSV_ABI sceImeKeyboardClose(Libraries::UserService::OrbisUserServiceUserId userId) {
|
||||||
LOG_DEBUG(Lib_Ime, "called");
|
LOG_INFO(Lib_Ime, "called");
|
||||||
|
|
||||||
if (!g_keyboard_handler) {
|
if (!g_keyboard_handler) {
|
||||||
LOG_ERROR(Lib_Ime, "No keyboard handler is open");
|
LOG_ERROR(Lib_Ime, "No keyboard handler is open");
|
||||||
@@ -419,7 +329,7 @@ Error PS4_SYSV_ABI sceImeKeyboardClose(Libraries::UserService::OrbisUserServiceU
|
|||||||
return Error::INVALID_USER_ID;
|
return Error::INVALID_USER_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
g_keyboard_handler.release();
|
g_keyboard_handler.reset();
|
||||||
if (g_keyboard_handler) {
|
if (g_keyboard_handler) {
|
||||||
LOG_ERROR(Lib_Ime, "failed to close keyboard handler, it is still open");
|
LOG_ERROR(Lib_Ime, "failed to close keyboard handler, it is still open");
|
||||||
return Error::INTERNAL;
|
return Error::INTERNAL;
|
||||||
@@ -436,7 +346,7 @@ int PS4_SYSV_ABI sceImeKeyboardGetInfo() {
|
|||||||
Error PS4_SYSV_ABI
|
Error PS4_SYSV_ABI
|
||||||
sceImeKeyboardGetResourceId(Libraries::UserService::OrbisUserServiceUserId userId,
|
sceImeKeyboardGetResourceId(Libraries::UserService::OrbisUserServiceUserId userId,
|
||||||
OrbisImeKeyboardResourceIdArray* resourceIdArray) {
|
OrbisImeKeyboardResourceIdArray* resourceIdArray) {
|
||||||
LOG_DEBUG(Lib_Ime, "(partial) called");
|
LOG_INFO(Lib_Ime, "(partial) called");
|
||||||
|
|
||||||
if (!resourceIdArray) {
|
if (!resourceIdArray) {
|
||||||
LOG_ERROR(Lib_Ime, "Invalid resourceIdArray: NULL");
|
LOG_ERROR(Lib_Ime, "Invalid resourceIdArray: NULL");
|
||||||
@@ -446,10 +356,12 @@ sceImeKeyboardGetResourceId(Libraries::UserService::OrbisUserServiceUserId userI
|
|||||||
// TODO: Check for valid user IDs.
|
// TODO: Check for valid user IDs.
|
||||||
if (userId == Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID) {
|
if (userId == Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID) {
|
||||||
LOG_ERROR(Lib_Ime, "Invalid userId: {}", userId);
|
LOG_ERROR(Lib_Ime, "Invalid userId: {}", userId);
|
||||||
|
/*
|
||||||
resourceIdArray->user_id = userId;
|
resourceIdArray->user_id = userId;
|
||||||
for (u32& id : resourceIdArray->resource_id) {
|
for (u32& id : resourceIdArray->resource_id) {
|
||||||
id = 0;
|
id = 0;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
return Error::INVALID_USER_ID;
|
return Error::INVALID_USER_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,7 +388,7 @@ sceImeKeyboardGetResourceId(Libraries::UserService::OrbisUserServiceUserId userI
|
|||||||
|
|
||||||
Error PS4_SYSV_ABI sceImeKeyboardOpen(Libraries::UserService::OrbisUserServiceUserId userId,
|
Error PS4_SYSV_ABI sceImeKeyboardOpen(Libraries::UserService::OrbisUserServiceUserId userId,
|
||||||
const OrbisImeKeyboardParam* param) {
|
const OrbisImeKeyboardParam* param) {
|
||||||
LOG_DEBUG(Lib_Ime, "called");
|
LOG_INFO(Lib_Ime, "called");
|
||||||
if (!param) {
|
if (!param) {
|
||||||
LOG_ERROR(Lib_Ime, "Invalid param: NULL");
|
LOG_ERROR(Lib_Ime, "Invalid param: NULL");
|
||||||
return Error::INVALID_ADDRESS;
|
return Error::INVALID_ADDRESS;
|
||||||
@@ -531,11 +443,13 @@ Error PS4_SYSV_ABI sceImeKeyboardOpen(Libraries::UserService::OrbisUserServiceUs
|
|||||||
LOG_ERROR(Lib_Ime, "USB keyboard some special kind of failure");
|
LOG_ERROR(Lib_Ime, "USB keyboard some special kind of failure");
|
||||||
return Error::CONNECTION_FAILED;
|
return Error::CONNECTION_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (g_keyboard_handler) {
|
if (g_keyboard_handler) {
|
||||||
LOG_ERROR(Lib_Ime, "Keyboard handler is already open");
|
LOG_ERROR(Lib_Ime, "Keyboard handler is already open");
|
||||||
return Error::BUSY;
|
return Error::BUSY;
|
||||||
}
|
}
|
||||||
g_keyboard_handler = std::make_unique<ImeHandler>(param, userId);
|
|
||||||
|
g_keyboard_handler = std::make_unique<ImeHandler>(param);
|
||||||
if (!g_keyboard_handler) {
|
if (!g_keyboard_handler) {
|
||||||
LOG_ERROR(Lib_Ime, "Failed to create keyboard handler");
|
LOG_ERROR(Lib_Ime, "Failed to create keyboard handler");
|
||||||
return Error::INTERNAL; // or Error::NO_MEMORY;
|
return Error::INTERNAL; // or Error::NO_MEMORY;
|
||||||
@@ -560,7 +474,7 @@ int PS4_SYSV_ABI sceImeKeyboardUpdate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Error PS4_SYSV_ABI sceImeOpen(const OrbisImeParam* param, const OrbisImeParamExtended* extended) {
|
Error PS4_SYSV_ABI sceImeOpen(const OrbisImeParam* param, const OrbisImeParamExtended* extended) {
|
||||||
LOG_DEBUG(Lib_Ime, "called");
|
LOG_INFO(Lib_Ime, "called");
|
||||||
|
|
||||||
if (!param) {
|
if (!param) {
|
||||||
LOG_ERROR(Lib_Ime, "Invalid param: NULL");
|
LOG_ERROR(Lib_Ime, "Invalid param: NULL");
|
||||||
@@ -715,13 +629,13 @@ Error PS4_SYSV_ABI sceImeOpen(const OrbisImeParam* param, const OrbisImeParamExt
|
|||||||
|
|
||||||
// Todo: validate arg
|
// Todo: validate arg
|
||||||
if (false) {
|
if (false) {
|
||||||
LOG_ERROR(Lib_Ime, "Invalid arg: NULL");
|
LOG_ERROR(Lib_Ime, "Invalid arg");
|
||||||
return Error::INVALID_ARG;
|
return Error::INVALID_ARG;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Todo: validate handler
|
// Todo: validate handler
|
||||||
if (false) {
|
if (false) {
|
||||||
LOG_ERROR(Lib_Ime, "Invalid handler: NULL");
|
LOG_ERROR(Lib_Ime, "Invalid handler");
|
||||||
return Error::INVALID_HANDLER;
|
return Error::INVALID_HANDLER;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -737,7 +651,11 @@ Error PS4_SYSV_ABI sceImeOpen(const OrbisImeParam* param, const OrbisImeParamExt
|
|||||||
return Error::BUSY;
|
return Error::BUSY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (extended) {
|
||||||
g_ime_handler = std::make_unique<ImeHandler>(param, extended);
|
g_ime_handler = std::make_unique<ImeHandler>(param, extended);
|
||||||
|
} else {
|
||||||
|
g_ime_handler = std::make_unique<ImeHandler>(param);
|
||||||
|
}
|
||||||
if (!g_ime_handler) {
|
if (!g_ime_handler) {
|
||||||
LOG_ERROR(Lib_Ime, "Failed to create IME handler");
|
LOG_ERROR(Lib_Ime, "Failed to create IME handler");
|
||||||
return Error::NO_MEMORY; // or Error::INTERNAL
|
return Error::NO_MEMORY; // or Error::INTERNAL
|
||||||
@@ -753,14 +671,14 @@ int PS4_SYSV_ABI sceImeOpenInternal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void PS4_SYSV_ABI sceImeParamInit(OrbisImeParam* param) {
|
void PS4_SYSV_ABI sceImeParamInit(OrbisImeParam* param) {
|
||||||
LOG_DEBUG(Lib_Ime, "sceImeParamInit called");
|
LOG_INFO(Lib_Ime, "called");
|
||||||
|
|
||||||
if (!param) {
|
if (!param) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
memset(param, 0, sizeof(OrbisImeParam));
|
memset(param, 0, sizeof(OrbisImeParam));
|
||||||
param->user_id = -1;
|
param->user_id = Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID;
|
||||||
}
|
}
|
||||||
|
|
||||||
int PS4_SYSV_ABI sceImeSetCandidateIndex() {
|
int PS4_SYSV_ABI sceImeSetCandidateIndex() {
|
||||||
@@ -811,7 +729,7 @@ Error PS4_SYSV_ABI sceImeUpdate(OrbisImeEventHandler handler) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!g_ime_handler && !g_keyboard_handler) {
|
if (!g_ime_handler && !g_keyboard_handler) {
|
||||||
LOG_TRACE(Lib_Ime, "sceImeUpdate called with no active handler");
|
LOG_ERROR(Lib_Ime, "sceImeUpdate called with no active handler");
|
||||||
return Error::OK;
|
return Error::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <vector>
|
||||||
#include "ime_ui.h"
|
#include "ime_ui.h"
|
||||||
#include "imgui/imgui_std.h"
|
#include "imgui/imgui_std.h"
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <string>
|
|
||||||
#include <string_view>
|
|
||||||
|
|
||||||
#include "common/logging/log.h"
|
|
||||||
|
|
||||||
namespace Libraries::Ime {
|
namespace Libraries::Ime {
|
||||||
|
|
||||||
using namespace ImGui;
|
using namespace ImGui;
|
||||||
@@ -18,23 +14,31 @@ static constexpr ImVec2 BUTTON_SIZE{100.0f, 30.0f};
|
|||||||
|
|
||||||
ImeState::ImeState(const OrbisImeParam* param, const OrbisImeParamExtended* extended) {
|
ImeState::ImeState(const OrbisImeParam* param, const OrbisImeParamExtended* extended) {
|
||||||
if (!param) {
|
if (!param) {
|
||||||
|
LOG_ERROR(Lib_Ime, "Invalid IME parameters");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!param->work) {
|
||||||
|
LOG_ERROR(Lib_Ime, "Invalid work buffer pointer");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!param->inputTextBuffer) {
|
||||||
|
LOG_ERROR(Lib_Ime, "Invalid text buffer pointer");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
work_buffer = param->work;
|
work_buffer = param->work;
|
||||||
text_buffer = param->inputTextBuffer;
|
text_buffer = param->inputTextBuffer;
|
||||||
max_text_length = param->maxTextLength;
|
// Respect both the absolute IME limit and the caller-provided limit
|
||||||
|
max_text_length = std::min(param->maxTextLength, ORBIS_IME_MAX_TEXT_LENGTH);
|
||||||
|
|
||||||
if (extended) {
|
if (extended) {
|
||||||
extended_param = *extended;
|
LOG_INFO(Lib_Ime, "Extended IME parameters provided");
|
||||||
has_extended = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (text_buffer) {
|
if (text_buffer) {
|
||||||
const std::size_t text_len = std::char_traits<char16_t>::length(text_buffer);
|
const std::size_t text_len = std::char_traits<char16_t>::length(text_buffer);
|
||||||
if (!ConvertOrbisToUTF8(text_buffer, text_len, current_text.begin(),
|
if (!ConvertOrbisToUTF8(text_buffer, text_len, current_text.begin(),
|
||||||
ORBIS_IME_MAX_TEXT_LENGTH * 4)) {
|
ORBIS_IME_MAX_TEXT_LENGTH * 4 + 1)) {
|
||||||
LOG_ERROR(Lib_ImeDialog, "Failed to convert text to utf8 encoding");
|
LOG_ERROR(Lib_Ime, "Failed to convert text to utf8 encoding");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,36 +66,75 @@ ImeState& ImeState::operator=(ImeState&& other) noexcept {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ImeState::SendEvent(OrbisImeEvent* event) {
|
void ImeState::SendEvent(OrbisImeEvent* event) {
|
||||||
if (!event) {
|
|
||||||
LOG_WARNING(Lib_Ime, "ImeState::SendEvent called with null event");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto event_id = event->id;
|
|
||||||
const auto event_name = magic_enum::enum_name(event_id);
|
|
||||||
const char* event_name_cstr = event_name.empty() ? "Unknown" : event_name.data();
|
|
||||||
|
|
||||||
std::unique_lock<std::mutex> lock{queue_mutex};
|
std::unique_lock<std::mutex> lock{queue_mutex};
|
||||||
event_queue.push(*event);
|
event_queue.push(*event);
|
||||||
|
|
||||||
LOG_DEBUG(Lib_Ime, "ImeState queued event_id={} ({}) queue_size={}", static_cast<u32>(event_id),
|
|
||||||
event_name_cstr, event_queue.size());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ImeState::SendEnterEvent() {
|
void ImeState::SendEnterEvent() {
|
||||||
LOG_DEBUG(Lib_Ime, "ImeState::SendEnterEvent triggered");
|
|
||||||
|
|
||||||
OrbisImeEvent enterEvent{};
|
OrbisImeEvent enterEvent{};
|
||||||
enterEvent.id = OrbisImeEventId::PressEnter;
|
enterEvent.id = OrbisImeEventId::PressEnter;
|
||||||
|
|
||||||
|
// Include current text payload for consumers expecting text with Enter
|
||||||
|
OrbisImeEditText text{};
|
||||||
|
text.str = reinterpret_cast<char16_t*>(work_buffer);
|
||||||
|
// Sync work and input buffers with the latest UTF-8 text
|
||||||
|
if (current_text.begin()) {
|
||||||
|
ConvertUTF8ToOrbis(current_text.begin(), current_text.size(),
|
||||||
|
reinterpret_cast<char16_t*>(work_buffer), max_text_length);
|
||||||
|
if (text_buffer) {
|
||||||
|
ConvertUTF8ToOrbis(current_text.begin(), current_text.size(), text_buffer,
|
||||||
|
max_text_length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (text.str) {
|
||||||
|
const u32 len = static_cast<u32>(std::char_traits<char16_t>::length(text.str));
|
||||||
|
// 0-based caret at end
|
||||||
|
text.caret_index = len;
|
||||||
|
text.area_num = 1;
|
||||||
|
text.text_area[0].mode = OrbisImeTextAreaMode::Edit;
|
||||||
|
// No edit happening on Enter: length=0; index can be caret
|
||||||
|
text.text_area[0].index = len;
|
||||||
|
text.text_area[0].length = 0;
|
||||||
|
enterEvent.param.text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG(Lib_Ime,
|
||||||
|
"IME Event queued: PressEnter caret={} area_num={} edit.index={} edit.length={}",
|
||||||
|
text.caret_index, text.area_num, text.text_area[0].index, text.text_area[0].length);
|
||||||
SendEvent(&enterEvent);
|
SendEvent(&enterEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ImeState::SendCloseEvent() {
|
void ImeState::SendCloseEvent() {
|
||||||
LOG_DEBUG(Lib_Ime, "ImeState::SendCloseEvent triggered (work_buffer={:p})", work_buffer);
|
|
||||||
|
|
||||||
OrbisImeEvent closeEvent{};
|
OrbisImeEvent closeEvent{};
|
||||||
closeEvent.id = OrbisImeEventId::PressClose;
|
closeEvent.id = OrbisImeEventId::PressClose;
|
||||||
closeEvent.param.text.str = reinterpret_cast<char16_t*>(work_buffer);
|
|
||||||
|
// Populate text payload with current buffer snapshot
|
||||||
|
OrbisImeEditText text{};
|
||||||
|
text.str = reinterpret_cast<char16_t*>(work_buffer);
|
||||||
|
// Sync work and input buffers with the latest UTF-8 text
|
||||||
|
if (current_text.begin()) {
|
||||||
|
ConvertUTF8ToOrbis(current_text.begin(), current_text.size(),
|
||||||
|
reinterpret_cast<char16_t*>(work_buffer), max_text_length);
|
||||||
|
if (text_buffer) {
|
||||||
|
ConvertUTF8ToOrbis(current_text.begin(), current_text.size(), text_buffer,
|
||||||
|
max_text_length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (text.str) {
|
||||||
|
const u32 len = static_cast<u32>(std::char_traits<char16_t>::length(text.str));
|
||||||
|
// 0-based caret at end
|
||||||
|
text.caret_index = len;
|
||||||
|
text.area_num = 1;
|
||||||
|
text.text_area[0].mode = OrbisImeTextAreaMode::Edit;
|
||||||
|
// No edit happening on Close: length=0; index can be caret
|
||||||
|
text.text_area[0].index = len;
|
||||||
|
text.text_area[0].length = 0;
|
||||||
|
closeEvent.param.text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG(Lib_Ime,
|
||||||
|
"IME Event queued: PressClose caret={} area_num={} edit.index={} edit.length={}",
|
||||||
|
text.caret_index, text.area_num, text.text_area[0].index, text.text_area[0].length);
|
||||||
SendEvent(&closeEvent);
|
SendEvent(&closeEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,46 +144,14 @@ void ImeState::SetText(const char16_t* text, u32 length) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t requested_length = length;
|
// Clamp to the effective maximum number of characters
|
||||||
if (requested_length == 0) {
|
const u32 clamped_len = std::min(length, max_text_length);
|
||||||
requested_length = std::char_traits<char16_t>::length(text);
|
if (!ConvertOrbisToUTF8(text, clamped_len, current_text.begin(), current_text.capacity())) {
|
||||||
}
|
|
||||||
|
|
||||||
const std::size_t buffer_capacity =
|
|
||||||
max_text_length != 0 ? max_text_length : ORBIS_IME_MAX_TEXT_LENGTH;
|
|
||||||
const std::size_t effective_length = std::min<std::size_t>(requested_length, buffer_capacity);
|
|
||||||
|
|
||||||
if (text_buffer && buffer_capacity > 0) {
|
|
||||||
const std::size_t copy_length = std::min<std::size_t>(effective_length, buffer_capacity);
|
|
||||||
std::copy_n(text, copy_length, text_buffer);
|
|
||||||
const std::size_t terminator_index =
|
|
||||||
copy_length < buffer_capacity ? copy_length : buffer_capacity - 1;
|
|
||||||
text_buffer[terminator_index] = u'\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ConvertOrbisToUTF8(text, effective_length, current_text.begin(),
|
|
||||||
current_text.capacity())) {
|
|
||||||
LOG_ERROR(Lib_Ime, "ImeState::SetText failed to convert updated text to UTF-8");
|
LOG_ERROR(Lib_Ime, "ImeState::SetText failed to convert updated text to UTF-8");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto utf8_view = current_text.to_view();
|
|
||||||
constexpr std::size_t kPreviewLength = 64;
|
|
||||||
std::string preview =
|
|
||||||
std::string{utf8_view.substr(0, std::min(kPreviewLength, utf8_view.size()))};
|
|
||||||
if (utf8_view.size() > kPreviewLength) {
|
|
||||||
preview += "...";
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_DEBUG(Lib_Ime, "ImeState::SetText stored game feedback length={} preview={}",
|
|
||||||
effective_length, preview);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImeState::SetCaret(u32 position) {
|
|
||||||
LOG_DEBUG(Lib_Ime,
|
|
||||||
"ImeState::SetCaret stored game feedback caret_index={} current_text_length={}",
|
|
||||||
position, current_text.size());
|
|
||||||
}
|
}
|
||||||
|
void ImeState::SetCaret(u32 position) {}
|
||||||
|
|
||||||
bool ImeState::ConvertOrbisToUTF8(const char16_t* orbis_text, std::size_t orbis_text_len,
|
bool ImeState::ConvertOrbisToUTF8(const char16_t* orbis_text, std::size_t orbis_text_len,
|
||||||
char* utf8_text, std::size_t utf8_text_len) {
|
char* utf8_text, std::size_t utf8_text_len) {
|
||||||
@@ -154,7 +165,8 @@ bool ImeState::ConvertOrbisToUTF8(const char16_t* orbis_text, std::size_t orbis_
|
|||||||
bool ImeState::ConvertUTF8ToOrbis(const char* utf8_text, std::size_t utf8_text_len,
|
bool ImeState::ConvertUTF8ToOrbis(const char* utf8_text, std::size_t utf8_text_len,
|
||||||
char16_t* orbis_text, std::size_t orbis_text_len) {
|
char16_t* orbis_text, std::size_t orbis_text_len) {
|
||||||
std::fill(orbis_text, orbis_text + orbis_text_len, u'\0');
|
std::fill(orbis_text, orbis_text + orbis_text_len, u'\0');
|
||||||
ImTextStrFromUtf8(reinterpret_cast<ImWchar*>(orbis_text), orbis_text_len, utf8_text, nullptr);
|
const char* end = utf8_text ? (utf8_text + utf8_text_len) : nullptr;
|
||||||
|
ImTextStrFromUtf8(reinterpret_cast<ImWchar*>(orbis_text), orbis_text_len, utf8_text, end);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -260,9 +272,10 @@ void ImeUi::DrawInputText() {
|
|||||||
if (first_render) {
|
if (first_render) {
|
||||||
SetKeyboardFocusHere();
|
SetKeyboardFocusHere();
|
||||||
}
|
}
|
||||||
if (InputTextEx("##ImeInput", nullptr, state->current_text.begin(),
|
if (InputTextExLimited("##ImeInput", nullptr, state->current_text.begin(),
|
||||||
ime_param->maxTextLength * 4 + 1, input_size,
|
ime_param->maxTextLength * 4 + 1, input_size,
|
||||||
ImGuiInputTextFlags_CallbackAlways, InputTextCallback, this)) {
|
ImGuiInputTextFlags_CallbackAlways, ime_param->maxTextLength,
|
||||||
|
InputTextCallback, this)) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,69 +284,69 @@ int ImeUi::InputTextCallback(ImGuiInputTextCallbackData* data) {
|
|||||||
ASSERT(ui);
|
ASSERT(ui);
|
||||||
|
|
||||||
static std::string lastText;
|
static std::string lastText;
|
||||||
|
static int lastCaretPos = -1;
|
||||||
std::string currentText(data->Buf, data->BufTextLen);
|
std::string currentText(data->Buf, data->BufTextLen);
|
||||||
if (currentText != lastText) {
|
if (currentText != lastText) {
|
||||||
OrbisImeEditText eventParam{};
|
OrbisImeEditText eventParam{};
|
||||||
eventParam.str = reinterpret_cast<char16_t*>(ui->ime_param->work);
|
eventParam.str = reinterpret_cast<char16_t*>(ui->ime_param->work);
|
||||||
eventParam.caret_index = data->CursorPos;
|
|
||||||
eventParam.area_num = 1;
|
eventParam.area_num = 1;
|
||||||
|
|
||||||
eventParam.text_area[0].mode = OrbisImeTextAreaMode::Edit;
|
eventParam.text_area[0].mode = OrbisImeTextAreaMode::Edit;
|
||||||
eventParam.text_area[0].index = data->CursorPos;
|
|
||||||
eventParam.text_area[0].length = data->BufTextLen;
|
|
||||||
|
|
||||||
if (!ui->state->ConvertUTF8ToOrbis(data->Buf, data->BufTextLen, eventParam.str,
|
if (!ui->state->ConvertUTF8ToOrbis(data->Buf, data->BufTextLen, eventParam.str,
|
||||||
ui->ime_param->maxTextLength)) {
|
ui->state->max_text_length)) {
|
||||||
LOG_ERROR(Lib_ImeDialog, "Failed to convert Orbis char to UTF-8");
|
LOG_ERROR(Lib_Ime, "Failed to convert UTF-8 to Orbis for eventParam.str");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ui->state->ConvertUTF8ToOrbis(data->Buf, data->BufTextLen,
|
if (!ui->state->ConvertUTF8ToOrbis(data->Buf, data->BufTextLen,
|
||||||
ui->ime_param->inputTextBuffer,
|
ui->ime_param->inputTextBuffer,
|
||||||
ui->ime_param->maxTextLength)) {
|
ui->state->max_text_length)) {
|
||||||
LOG_ERROR(Lib_ImeDialog, "Failed to convert Orbis char to UTF-8");
|
LOG_ERROR(Lib_Ime, "Failed to convert UTF-8 to Orbis for inputTextBuffer");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventParam.caret_index = data->CursorPos;
|
||||||
|
eventParam.text_area[0].index = data->CursorPos;
|
||||||
|
eventParam.text_area[0].length =
|
||||||
|
(data->CursorPos > lastCaretPos) ? 1 : -1; // data->CursorPos;
|
||||||
|
|
||||||
OrbisImeEvent event{};
|
OrbisImeEvent event{};
|
||||||
event.id = OrbisImeEventId::UpdateText;
|
event.id = OrbisImeEventId::UpdateText;
|
||||||
event.param.text = eventParam;
|
event.param.text = eventParam;
|
||||||
|
LOG_DEBUG(Lib_Ime,
|
||||||
constexpr std::size_t kPreviewLength = 64;
|
"IME Event queued: UpdateText(type, "
|
||||||
std::string preview = currentText.substr(0, kPreviewLength);
|
"delete)\neventParam.caret_index={}\narea_num={}\neventParam.text_area[0].mode={}"
|
||||||
if (currentText.size() > kPreviewLength) {
|
"\neventParam.text_area[0].index={}\neventParam.text_area[0].length={}",
|
||||||
preview += "...";
|
eventParam.caret_index, eventParam.area_num,
|
||||||
}
|
static_cast<s32>(eventParam.text_area[0].mode), eventParam.text_area[0].index,
|
||||||
LOG_DEBUG(Lib_Ime, "ImeUi enqueuing UpdateText: caret_index={} length={} preview={}",
|
eventParam.text_area[0].length);
|
||||||
eventParam.caret_index, data->BufTextLen, preview);
|
|
||||||
|
|
||||||
lastText = currentText;
|
lastText = currentText;
|
||||||
|
lastCaretPos = -1;
|
||||||
ui->state->SendEvent(&event);
|
ui->state->SendEvent(&event);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int lastCaretPos = -1;
|
|
||||||
if (lastCaretPos == -1) {
|
if (lastCaretPos == -1) {
|
||||||
lastCaretPos = data->CursorPos;
|
lastCaretPos = data->CursorPos;
|
||||||
} else if (data->CursorPos != lastCaretPos) {
|
} else if (data->CursorPos != lastCaretPos) {
|
||||||
OrbisImeCaretMovementDirection caretDirection = OrbisImeCaretMovementDirection::Still;
|
const int delta = data->CursorPos - lastCaretPos;
|
||||||
if (data->CursorPos < lastCaretPos) {
|
|
||||||
caretDirection = OrbisImeCaretMovementDirection::Left;
|
// Emit one UpdateCaret per delta step (delta may be ±1 or a jump)
|
||||||
} else if (data->CursorPos > lastCaretPos) {
|
const bool move_right = delta > 0;
|
||||||
caretDirection = OrbisImeCaretMovementDirection::Right;
|
const u32 steps = static_cast<u32>(std::abs(delta));
|
||||||
|
OrbisImeCaretMovementDirection dir = move_right ? OrbisImeCaretMovementDirection::Right
|
||||||
|
: OrbisImeCaretMovementDirection::Left;
|
||||||
|
|
||||||
|
for (u32 i = 0; i < steps; ++i) {
|
||||||
|
OrbisImeEvent caret_step{};
|
||||||
|
caret_step.id = OrbisImeEventId::UpdateCaret;
|
||||||
|
caret_step.param.caret_move = dir;
|
||||||
|
LOG_DEBUG(Lib_Ime, "IME Event queued: UpdateCaret(step {}/{}), dir={}", i + 1, steps,
|
||||||
|
static_cast<u32>(dir));
|
||||||
|
ui->state->SendEvent(&caret_step);
|
||||||
}
|
}
|
||||||
|
|
||||||
OrbisImeEvent event{};
|
|
||||||
event.id = OrbisImeEventId::UpdateCaret;
|
|
||||||
event.param.caret_move = caretDirection;
|
|
||||||
|
|
||||||
const auto caret_direction_name = magic_enum::enum_name(caretDirection);
|
|
||||||
const char* caret_direction_cstr =
|
|
||||||
caret_direction_name.empty() ? "Unknown" : caret_direction_name.data();
|
|
||||||
LOG_DEBUG(Lib_Ime, "ImeUi enqueuing UpdateCaret: caret_pos={} direction={} ({})",
|
|
||||||
data->CursorPos, static_cast<u32>(caretDirection), caret_direction_cstr);
|
|
||||||
|
|
||||||
lastCaretPos = data->CursorPos;
|
lastCaretPos = data->CursorPos;
|
||||||
ui->state->SendEvent(&event);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -26,9 +26,6 @@ class ImeState {
|
|||||||
char16_t* text_buffer{};
|
char16_t* text_buffer{};
|
||||||
u32 max_text_length = 0;
|
u32 max_text_length = 0;
|
||||||
|
|
||||||
OrbisImeParamExtended extended_param{};
|
|
||||||
bool has_extended = false;
|
|
||||||
|
|
||||||
// A character can hold up to 4 bytes in UTF-8
|
// A character can hold up to 4 bytes in UTF-8
|
||||||
Common::CString<ORBIS_IME_MAX_TEXT_LENGTH * 4 + 1> current_text;
|
Common::CString<ORBIS_IME_MAX_TEXT_LENGTH * 4 + 1> current_text;
|
||||||
|
|
||||||
|
|||||||
@@ -88,4 +88,112 @@ static void DrawCenteredText(const char* text, const char* text_end = nullptr,
|
|||||||
SetCursorPos(pos + content);
|
SetCursorPos(pos + content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Limited-length InputTextEx wrapper (limits UTF-8 code points)
|
||||||
|
// - max_chars counts Unicode code points, not bytes
|
||||||
|
// - Works for single-line and multi-line
|
||||||
|
// - Chains user callbacks if provided
|
||||||
|
struct InputTextLimitCtx {
|
||||||
|
int max_chars;
|
||||||
|
ImGuiInputTextCallback user_cb;
|
||||||
|
void* user_user_data;
|
||||||
|
ImGuiInputTextFlags forward_flags; // original flags requested by caller
|
||||||
|
};
|
||||||
|
|
||||||
|
inline int InputTextLimitCallback(ImGuiInputTextCallbackData* data) {
|
||||||
|
InputTextLimitCtx* ctx = static_cast<InputTextLimitCtx*>(data->UserData);
|
||||||
|
if (ctx && ctx->user_cb) {
|
||||||
|
const ImGuiInputTextFlags ev = data->EventFlag;
|
||||||
|
const ImGuiInputTextFlags ff = ctx->forward_flags;
|
||||||
|
const bool should_forward =
|
||||||
|
((ev == ImGuiInputTextFlags_CallbackAlways) &&
|
||||||
|
(ff & ImGuiInputTextFlags_CallbackAlways)) ||
|
||||||
|
((ev == ImGuiInputTextFlags_CallbackEdit) && (ff & ImGuiInputTextFlags_CallbackEdit)) ||
|
||||||
|
((ev == ImGuiInputTextFlags_CallbackCharFilter) &&
|
||||||
|
(ff & ImGuiInputTextFlags_CallbackCharFilter)) ||
|
||||||
|
((ev == ImGuiInputTextFlags_CallbackCompletion) &&
|
||||||
|
(ff & ImGuiInputTextFlags_CallbackCompletion)) ||
|
||||||
|
((ev == ImGuiInputTextFlags_CallbackHistory) &&
|
||||||
|
(ff & ImGuiInputTextFlags_CallbackHistory)) ||
|
||||||
|
((ev == ImGuiInputTextFlags_CallbackResize) &&
|
||||||
|
(ff & ImGuiInputTextFlags_CallbackResize));
|
||||||
|
if (should_forward) {
|
||||||
|
void* orig = data->UserData;
|
||||||
|
data->UserData = ctx->user_user_data;
|
||||||
|
int user_ret = ctx->user_cb(data);
|
||||||
|
data->UserData = orig;
|
||||||
|
if (user_ret != 0) {
|
||||||
|
return user_ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx || ctx->max_chars < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enforce limit: discard extra characters on filter, trim on edit
|
||||||
|
if (data->EventFlag == ImGuiInputTextFlags_CallbackCharFilter) {
|
||||||
|
ImGuiContext* g = data->Ctx;
|
||||||
|
if (!g) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
ImGuiInputTextState* st = &g->InputTextState;
|
||||||
|
if (st == nullptr || st->TextSrc == nullptr) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int cur_chars = ImTextCountCharsFromUtf8(st->TextSrc, st->TextSrc + st->TextLen);
|
||||||
|
int sel_chars = 0;
|
||||||
|
if (st->HasSelection()) {
|
||||||
|
const int ib = st->GetSelectionStart();
|
||||||
|
const int ie = st->GetSelectionEnd();
|
||||||
|
sel_chars = ImTextCountCharsFromUtf8(st->TextSrc + ib, st->TextSrc + ie);
|
||||||
|
}
|
||||||
|
const int remaining = ctx->max_chars - (cur_chars - sel_chars);
|
||||||
|
if (remaining <= 0) {
|
||||||
|
data->EventChar = 0;
|
||||||
|
return 1; // discard
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data->EventFlag == ImGuiInputTextFlags_CallbackEdit) {
|
||||||
|
// Trim tail to ensure text length <= max_chars code points
|
||||||
|
const char* s = data->Buf;
|
||||||
|
const char* end = s + data->BufTextLen;
|
||||||
|
const char* p = s;
|
||||||
|
int codepoints = 0;
|
||||||
|
while (p < end && codepoints < ctx->max_chars) {
|
||||||
|
unsigned int c;
|
||||||
|
int len = ImTextCharFromUtf8(&c, p, end);
|
||||||
|
if (len <= 0)
|
||||||
|
break;
|
||||||
|
p += len;
|
||||||
|
codepoints++;
|
||||||
|
}
|
||||||
|
if (p < end) {
|
||||||
|
const int keep_bytes = static_cast<int>(p - s);
|
||||||
|
data->DeleteChars(keep_bytes, data->BufTextLen - keep_bytes);
|
||||||
|
if (data->CursorPos > data->BufTextLen)
|
||||||
|
data->CursorPos = data->BufTextLen;
|
||||||
|
if (data->SelectionStart > data->BufTextLen)
|
||||||
|
data->SelectionStart = data->BufTextLen;
|
||||||
|
if (data->SelectionEnd > data->BufTextLen)
|
||||||
|
data->SelectionEnd = data->BufTextLen;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool InputTextExLimited(const char* label, const char* hint, char* buf, int buf_size,
|
||||||
|
const ImVec2& size_arg, ImGuiInputTextFlags flags, int max_chars,
|
||||||
|
ImGuiInputTextCallback callback = nullptr,
|
||||||
|
void* user_data = nullptr) {
|
||||||
|
InputTextLimitCtx ctx{max_chars, callback, user_data, flags};
|
||||||
|
ImGuiInputTextFlags flags2 =
|
||||||
|
flags | ImGuiInputTextFlags_CallbackCharFilter | ImGuiInputTextFlags_CallbackEdit;
|
||||||
|
return InputTextEx(label, hint, buf, buf_size, size_arg, flags2, InputTextLimitCallback, &ctx);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace ImGui
|
} // namespace ImGui
|
||||||
|
|||||||
Reference in New Issue
Block a user