Merge branch 'citron-neo:main' into codex/nce-relocate-diagnostics

This commit is contained in:
RWC
2026-07-01 16:47:00 +08:00
committed by GitHub
10 changed files with 201 additions and 65 deletions
@@ -342,6 +342,13 @@ object NativeLibrary {
*/
external fun submitInlineKeyboardText(text: String?)
/**
* Replaces inline keyboard text with the IME editor contents.
* @param text Current text from the Android IME editor.
* @param cursor_position Cursor position in the editor text.
*/
external fun replaceInlineKeyboardText(text: String?, cursor_position: Int)
/**
* Submits inline keyboard input. Used to indicate keys pressed that are not text.
* @param key_code Android Key Code associated with the keyboard input.
@@ -25,6 +25,9 @@ object SoftwareKeyboard {
lateinit var data: KeyboardData
val dataLock = Object()
@Volatile
private var inlineConfig: KeyboardConfig? = null
private fun executeNormalImpl(config: KeyboardConfig) {
val activity = NativeLibrary.sEmulationActivity.get() ?: return
@@ -40,6 +43,8 @@ object SoftwareKeyboard {
val previousVisibility = overlayView.visibility
val previousAlpha = overlayView.alpha
inlineConfig = config
// Make sure the overlay can receive input
overlayView.visibility = View.VISIBLE
if (previousVisibility != View.VISIBLE) {
@@ -84,10 +89,19 @@ object SoftwareKeyboard {
overlayView.visibility = previousVisibility
overlayView.alpha = previousAlpha
NativeLibrary.submitInlineKeyboardInput(KeyEvent.KEYCODE_BACK)
clearInlineConfig()
}
}, delayMs)
}
fun getInlineInitialText(): String = inlineConfig?.initial_text.orEmpty()
fun getInlineInitialCursorPosition(): Int = inlineConfig?.initial_cursor_position ?: 0
fun clearInlineConfig() {
inlineConfig = null
}
@JvmStatic
fun executeNormal(config: KeyboardConfig): KeyboardData {
NativeLibrary.sEmulationActivity.get()!!.runOnUiThread {
@@ -13,19 +13,28 @@ import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.graphics.drawable.VectorDrawable
import android.os.Build
import android.text.Editable
import android.text.InputType
import android.text.Selection
import android.util.AttributeSet
import android.view.HapticFeedbackConstants
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.SurfaceView
import android.view.View
import android.view.View.OnTouchListener
import android.view.WindowInsets
import android.view.inputmethod.BaseInputConnection
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import androidx.core.content.ContextCompat
import androidx.window.layout.WindowMetricsCalculator
import kotlin.math.max
import kotlin.math.min
import org.citron.citron_emu.features.input.NativeInput
import org.citron.citron_emu.NativeLibrary
import org.citron.citron_emu.R
import org.citron.citron_emu.applets.keyboard.SoftwareKeyboard
import org.citron.citron_emu.features.input.NativeInput
import org.citron.citron_emu.features.input.model.NativeAnalog
import org.citron.citron_emu.features.input.model.NativeButton
import org.citron.citron_emu.features.input.model.NpadStyleIndex
@@ -36,13 +45,6 @@ import org.citron.citron_emu.overlay.model.OverlayControlData
import org.citron.citron_emu.overlay.model.OverlayLayout
import org.citron.citron_emu.utils.NativeConfig
import android.view.inputmethod.BaseInputConnection
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import org.citron.citron_emu.NativeLibrary
import android.view.KeyEvent
import android.text.Editable
import android.text.InputType
/**
* Draws the interactive input overlay on top of the
* [SurfaceView] that is rendering emulation.
@@ -54,6 +56,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
private val overlayDpads: MutableSet<InputOverlayDrawableDpad> = HashSet()
private val overlayJoysticks: MutableSet<InputOverlayDrawableJoystick> = HashSet()
private val keyboardEditable = Editable.Factory.getInstance().newEditable("")
private var keyboardLastSubmittedText = ""
private var inEditMode = false
private var buttonBeingConfigured: InputOverlayDrawableButton? = null
@@ -89,35 +92,96 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
requestFocus()
}
// Support for Android virtual keyboard (Software Keyboard)
// Support for Android virtual keyboard (Software Keyboard)
override fun onCheckIsTextEditor(): Boolean = true
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection {
val initialText = SoftwareKeyboard.getInlineInitialText()
val initialCursorPosition =
SoftwareKeyboard.getInlineInitialCursorPosition().coerceIn(0, initialText.length)
keyboardEditable.clear()
keyboardEditable.append(initialText)
Selection.setSelection(keyboardEditable, initialCursorPosition)
keyboardLastSubmittedText = initialText
outAttrs.inputType = InputType.TYPE_CLASS_TEXT or
InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI or EditorInfo.IME_ACTION_DONE
outAttrs.initialSelStart = 0
outAttrs.initialSelEnd = 0
outAttrs.initialSelStart = initialCursorPosition
outAttrs.initialSelEnd = initialCursorPosition
return object : BaseInputConnection(this, true) {
override fun getEditable(): Editable = keyboardEditable
override fun commitText(text: CharSequence?, newCursorPosition: Int): Boolean {
if (!text.isNullOrEmpty()) {
forwardTextToNative(text)
if (text == null) {
return true
}
return super.commitText(text, newCursorPosition)
val submittedText = text.toString()
val newlineIndex = submittedText.indexOf('\n')
if (newlineIndex >= 0) {
val prefix = submittedText.substring(0, newlineIndex)
if (prefix.isNotEmpty()) {
super.commitText(prefix, newCursorPosition)
submitEditableTextToNative()
}
NativeLibrary.submitInlineKeyboardInput(KeyEvent.KEYCODE_ENTER)
SoftwareKeyboard.clearInlineConfig()
return true
}
val result = super.commitText(text, newCursorPosition)
submitEditableTextToNative()
return result
}
override fun setComposingText(text: CharSequence?, newCursorPosition: Int): Boolean {
if (text == null) {
return true
}
val submittedText = text.toString()
val newlineIndex = submittedText.indexOf('\n')
if (newlineIndex >= 0) {
val prefix = submittedText.substring(0, newlineIndex)
if (prefix.isNotEmpty()) {
super.setComposingText(prefix, newCursorPosition)
submitEditableTextToNative()
}
NativeLibrary.submitInlineKeyboardInput(KeyEvent.KEYCODE_ENTER)
SoftwareKeyboard.clearInlineConfig()
return true
}
val result = super.setComposingText(text, newCursorPosition)
submitEditableTextToNative()
return result
}
override fun finishComposingText(): Boolean {
val result = super.finishComposingText()
submitEditableTextToNative()
return result
}
override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean {
repeat(beforeLength.coerceAtLeast(0)) {
NativeLibrary.submitInlineKeyboardInput(KeyEvent.KEYCODE_DEL)
}
return super.deleteSurroundingText(beforeLength, afterLength)
val result = super.deleteSurroundingText(beforeLength, afterLength)
submitEditableTextToNative()
return result
}
override fun deleteSurroundingTextInCodePoints(
beforeLength: Int,
afterLength: Int
): Boolean {
val result = super.deleteSurroundingTextInCodePoints(beforeLength, afterLength)
submitEditableTextToNative()
return result
}
override fun sendKeyEvent(event: KeyEvent): Boolean {
@@ -125,13 +189,19 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
when (event.keyCode) {
KeyEvent.KEYCODE_BACK,
KeyEvent.KEYCODE_DEL,
KeyEvent.KEYCODE_ENTER,
KeyEvent.KEYCODE_ESCAPE -> NativeLibrary.submitInlineKeyboardInput(event.keyCode)
KeyEvent.KEYCODE_ESCAPE -> {
NativeLibrary.submitInlineKeyboardInput(event.keyCode)
SoftwareKeyboard.clearInlineConfig()
}
KeyEvent.KEYCODE_ENTER -> {
NativeLibrary.submitInlineKeyboardInput(event.keyCode)
SoftwareKeyboard.clearInlineConfig()
}
KeyEvent.KEYCODE_DEL -> deleteSurroundingText(1, 0)
else -> {
val unicode = event.unicodeChar
if (unicode != 0) {
NativeLibrary.submitInlineKeyboardText(unicode.toChar().toString())
commitText(unicode.toChar().toString(), 1)
}
}
}
@@ -140,28 +210,21 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
override fun performEditorAction(actionCode: Int): Boolean {
NativeLibrary.submitInlineKeyboardInput(KeyEvent.KEYCODE_ENTER)
SoftwareKeyboard.clearInlineConfig()
return true
}
}
}
private fun forwardTextToNative(text: CharSequence) {
val buffer = StringBuilder()
text.forEach { char ->
when (char) {
'\n' -> {
if (buffer.isNotEmpty()) {
NativeLibrary.submitInlineKeyboardText(buffer.toString())
buffer.clear()
}
NativeLibrary.submitInlineKeyboardInput(KeyEvent.KEYCODE_ENTER)
}
else -> buffer.append(char)
}
}
if (buffer.isNotEmpty()) {
NativeLibrary.submitInlineKeyboardText(buffer.toString())
private fun submitEditableTextToNative() {
val text = keyboardEditable.toString()
if (text == keyboardLastSubmittedText) {
return
}
keyboardLastSubmittedText = text
val cursorPosition = Selection.getSelectionEnd(keyboardEditable).coerceIn(0, text.length)
NativeLibrary.replaceInlineKeyboardText(text, cursorPosition)
}
override fun draw(canvas: Canvas) {
+8
View File
@@ -735,6 +735,14 @@ void Java_org_citron_citron_1emu_NativeLibrary_submitInlineKeyboardText(JNIEnv*
EmulationSession::GetInstance().SoftwareKeyboard()->SubmitInlineKeyboardText(input);
}
void Java_org_citron_citron_1emu_NativeLibrary_replaceInlineKeyboardText(JNIEnv* env, jclass clazz,
jstring j_text,
jint j_cursor_position) {
const std::u16string input = Common::UTF8ToUTF16(Common::Android::GetJString(env, j_text));
EmulationSession::GetInstance().SoftwareKeyboard()->ReplaceInlineKeyboardText(
input, static_cast<s32>(j_cursor_position));
}
void Java_org_citron_citron_1emu_NativeLibrary_submitInlineKeyboardInput(JNIEnv* env, jclass clazz,
jint j_key_code) {
EmulationSession::GetInstance().SoftwareKeyboard()->SubmitInlineKeyboardInput(j_key_code);
+2 -3
View File
@@ -1622,7 +1622,7 @@ void QtSoftwareKeyboard::ShowTextCheckDialog(
}
void QtSoftwareKeyboard::ShowInlineKeyboard(
Core::Frontend::InlineAppearParameters appear_parameters) const {
Core::Frontend::InlineAppearParameters appear_parameters) {
LOG_INFO(Service_AM,
"\nInlineAppearParameters:"
"\nmax_text_length={}"
@@ -1651,8 +1651,7 @@ void QtSoftwareKeyboard::HideInlineKeyboard() const {
emit MainWindowHideInlineKeyboard();
}
void QtSoftwareKeyboard::InlineTextChanged(
Core::Frontend::InlineTextParameters text_parameters) const {
void QtSoftwareKeyboard::InlineTextChanged(Core::Frontend::InlineTextParameters text_parameters) {
LOG_INFO(Service_AM,
"\nInlineTextParameters:"
"\ninput_text={}"
+2 -3
View File
@@ -247,12 +247,11 @@ public:
void ShowTextCheckDialog(Service::AM::Frontend::SwkbdTextCheckResult text_check_result,
std::u16string text_check_message) const override;
void ShowInlineKeyboard(
Core::Frontend::InlineAppearParameters appear_parameters) const override;
void ShowInlineKeyboard(Core::Frontend::InlineAppearParameters appear_parameters) override;
void HideInlineKeyboard() const override;
void InlineTextChanged(Core::Frontend::InlineTextParameters text_parameters) const override;
void InlineTextChanged(Core::Frontend::InlineTextParameters text_parameters) override;
void ExitKeyboard() const override;
@@ -21,6 +21,19 @@ static jmethodID s_swkbd_execute_inline;
namespace Common::Android::SoftwareKeyboard {
static s32 ClampCursorPosition(s32 cursor_position, std::size_t text_size) {
if (cursor_position < 0) {
return 0;
}
const auto max_position = static_cast<s32>(text_size);
if (cursor_position > max_position) {
return max_position;
}
return cursor_position;
}
static jobject ToJKeyboardParams(const Core::Frontend::KeyboardInitializeParameters& config) {
JNIEnv* env = GetEnvForThread();
jobject object = env->AllocObject(s_keyboard_config_class);
@@ -155,7 +168,7 @@ void AndroidKeyboard::ShowTextCheckDialog(
}
void AndroidKeyboard::ShowInlineKeyboard(
Core::Frontend::InlineAppearParameters appear_parameters) const {
Core::Frontend::InlineAppearParameters appear_parameters) {
LOG_WARNING(Frontend,
"(STUBBED) called, backend requested to show the inline software keyboard.");
@@ -182,6 +195,8 @@ void AndroidKeyboard::ShowInlineKeyboard(
// Pivot to a new thread, as we cannot call GetEnvForThread() from a Fiber.
m_current_text = parameters.initial_text;
m_current_cursor_position =
ClampCursorPosition(parameters.initial_cursor_position, m_current_text.size());
m_is_inline_active = true;
std::thread([&] {
GetEnvForThread()->CallStaticVoidMethod(s_software_keyboard_class, s_swkbd_execute_inline,
@@ -194,8 +209,7 @@ void AndroidKeyboard::HideInlineKeyboard() const {
"(STUBBED) called, backend requested to hide the inline software keyboard.");
}
void AndroidKeyboard::InlineTextChanged(
Core::Frontend::InlineTextParameters text_parameters) const {
void AndroidKeyboard::InlineTextChanged(Core::Frontend::InlineTextParameters text_parameters) {
LOG_WARNING(Frontend,
"(STUBBED) called, backend requested to change the inline keyboard text.");
@@ -205,8 +219,14 @@ void AndroidKeyboard::InlineTextChanged(
"\ncursor_position={}",
Common::UTF16ToUTF8(text_parameters.input_text), text_parameters.cursor_position);
m_current_text = text_parameters.input_text;
m_current_cursor_position =
ClampCursorPosition(text_parameters.cursor_position, m_current_text.size());
parameters.initial_text = m_current_text;
parameters.initial_cursor_position = m_current_cursor_position;
submit_inline_callback(Service::AM::Frontend::SwkbdReplyType::ChangedString,
text_parameters.input_text, text_parameters.cursor_position);
m_current_text, m_current_cursor_position);
}
void AndroidKeyboard::ExitKeyboard() const {
@@ -214,14 +234,33 @@ void AndroidKeyboard::ExitKeyboard() const {
}
void AndroidKeyboard::SubmitInlineKeyboardText(std::u16string submitted_text) {
if (!m_is_inline_active || submitted_text.empty()) {
return;
}
m_current_cursor_position =
ClampCursorPosition(m_current_cursor_position, m_current_text.size());
m_current_text.insert(static_cast<std::size_t>(m_current_cursor_position), submitted_text);
m_current_cursor_position += static_cast<s32>(submitted_text.size());
parameters.initial_text = m_current_text;
parameters.initial_cursor_position = m_current_cursor_position;
submit_inline_callback(Service::AM::Frontend::SwkbdReplyType::ChangedString, m_current_text,
m_current_cursor_position);
}
void AndroidKeyboard::ReplaceInlineKeyboardText(std::u16string submitted_text, s32 cursor_position) {
if (!m_is_inline_active) {
return;
}
m_current_text += submitted_text;
m_current_text = std::move(submitted_text);
m_current_cursor_position = ClampCursorPosition(cursor_position, m_current_text.size());
parameters.initial_text = m_current_text;
parameters.initial_cursor_position = m_current_cursor_position;
submit_inline_callback(Service::AM::Frontend::SwkbdReplyType::ChangedString, m_current_text,
static_cast<int>(m_current_text.size()));
m_current_cursor_position);
}
void AndroidKeyboard::SubmitInlineKeyboardInput(int key_code) {
@@ -238,20 +277,26 @@ void AndroidKeyboard::SubmitInlineKeyboardInput(int key_code) {
case KEYCODE_ENTER:
m_is_inline_active = false;
submit_inline_callback(Service::AM::Frontend::SwkbdReplyType::DecidedEnter, m_current_text,
static_cast<s32>(m_current_text.size()));
m_current_cursor_position);
break;
case KEYCODE_BACK:
case KEYCODE_ESCAPE:
m_is_inline_active = false;
submit_inline_callback(Service::AM::Frontend::SwkbdReplyType::DecidedCancel, m_current_text,
static_cast<s32>(m_current_text.size()));
m_current_cursor_position);
break;
case KEYCODE_DEL:
if (m_current_text.empty()) {
m_current_cursor_position =
ClampCursorPosition(m_current_cursor_position, m_current_text.size());
if (m_current_cursor_position <= 0 || m_current_text.empty()) {
return;
}
--m_current_cursor_position;
m_current_text.erase(static_cast<std::size_t>(m_current_cursor_position), 1);
parameters.initial_text = m_current_text;
parameters.initial_cursor_position = m_current_cursor_position;
submit_inline_callback(Service::AM::Frontend::SwkbdReplyType::ChangedString, m_current_text,
static_cast<int>(m_current_text.size()));
m_current_cursor_position);
break;
}
}
@@ -27,17 +27,18 @@ public:
void ShowTextCheckDialog(Service::AM::Frontend::SwkbdTextCheckResult text_check_result,
std::u16string text_check_message) const override;
void ShowInlineKeyboard(
Core::Frontend::InlineAppearParameters appear_parameters) const override;
void ShowInlineKeyboard(Core::Frontend::InlineAppearParameters appear_parameters) override;
void HideInlineKeyboard() const override;
void InlineTextChanged(Core::Frontend::InlineTextParameters text_parameters) const override;
void InlineTextChanged(Core::Frontend::InlineTextParameters text_parameters) override;
void ExitKeyboard() const override;
void SubmitInlineKeyboardText(std::u16string submitted_text);
void ReplaceInlineKeyboardText(std::u16string submitted_text, s32 cursor_position);
void SubmitInlineKeyboardInput(int key_code);
private:
@@ -56,8 +57,9 @@ private:
mutable SubmitInlineCallback submit_inline_callback;
private:
mutable bool m_is_inline_active{};
bool m_is_inline_active{};
std::u16string m_current_text;
s32 m_current_cursor_position{};
};
// Should be called in JNI_Load
@@ -75,8 +75,7 @@ void DefaultSoftwareKeyboardApplet::ShowTextCheckDialog(
LOG_WARNING(Service_AM, "(STUBBED) called, backend requested to show the text check dialog.");
}
void DefaultSoftwareKeyboardApplet::ShowInlineKeyboard(
InlineAppearParameters appear_parameters) const {
void DefaultSoftwareKeyboardApplet::ShowInlineKeyboard(InlineAppearParameters appear_parameters) {
LOG_WARNING(Service_AM,
"(STUBBED) called, backend requested to show the inline software keyboard.");
@@ -109,7 +108,7 @@ void DefaultSoftwareKeyboardApplet::HideInlineKeyboard() const {
"(STUBBED) called, backend requested to hide the inline software keyboard.");
}
void DefaultSoftwareKeyboardApplet::InlineTextChanged(InlineTextParameters text_parameters) const {
void DefaultSoftwareKeyboardApplet::InlineTextChanged(InlineTextParameters text_parameters) {
LOG_WARNING(Service_AM,
"(STUBBED) called, backend requested to change the inline keyboard text.");
@@ -72,11 +72,11 @@ public:
virtual void ShowTextCheckDialog(Service::AM::Frontend::SwkbdTextCheckResult text_check_result,
std::u16string text_check_message) const = 0;
virtual void ShowInlineKeyboard(InlineAppearParameters appear_parameters) const = 0;
virtual void ShowInlineKeyboard(InlineAppearParameters appear_parameters) = 0;
virtual void HideInlineKeyboard() const = 0;
virtual void InlineTextChanged(InlineTextParameters text_parameters) const = 0;
virtual void InlineTextChanged(InlineTextParameters text_parameters) = 0;
virtual void ExitKeyboard() const = 0;
};
@@ -96,11 +96,11 @@ public:
void ShowTextCheckDialog(Service::AM::Frontend::SwkbdTextCheckResult text_check_result,
std::u16string text_check_message) const override;
void ShowInlineKeyboard(InlineAppearParameters appear_parameters) const override;
void ShowInlineKeyboard(InlineAppearParameters appear_parameters) override;
void HideInlineKeyboard() const override;
void InlineTextChanged(InlineTextParameters text_parameters) const override;
void InlineTextChanged(InlineTextParameters text_parameters) override;
void ExitKeyboard() const override;