diff --git a/app/src/main/java/at/dasher/android/DasherImeService.kt b/app/src/main/java/at/dasher/android/DasherImeService.kt index cf2770c..ba22539 100644 --- a/app/src/main/java/at/dasher/android/DasherImeService.kt +++ b/app/src/main/java/at/dasher/android/DasherImeService.kt @@ -56,6 +56,42 @@ class DasherImeService : InputMethodService() { private fun startModeIsFloating(): Boolean = imePrefs.getString("start_mode", "docked") == "floating" + // ── Emoji alphabet toggle (#61 option 2) ─────────────────────────────── + + // The alphabet to return to after emoji mode. Persisted so a keyboard + // process death mid-emoji-mode doesn't strand the user without a way + // back (the engine persists its own alphabet choice in settings — if it + // restarts in Emoji, the previous one must still be knowable). + private var emojiReturnAlphabet: String? = null + + private fun toggleEmojiMode() { + val eng = engine ?: return + val toolbar = editingToolbar ?: return + val current = eng.getCurrentAlphabet() + if (current == "Emoji") { + val target = emojiReturnAlphabet + if (target.isNullOrEmpty() || target == "Emoji") { + // No remembered alphabet (e.g. pref lost) — fall back to the + // engine default rather than a no-op toggle. + eng.setAlphabet("English with limited punctuation") + } else { + eng.setAlphabet(target) + } + emojiReturnAlphabet = null + imePrefs.edit().remove("emoji_return_alphabet").apply() + toolbar.setEmojiMode(false) + // The alphabet switch rebuilt the model from scratch — re-seed + // from the target field so predictions continue mid-sentence + // (same machinery as the editing actions, RFC 0015 tier 2). + reanchorEngineToTarget() + } else { + emojiReturnAlphabet = current + imePrefs.edit().putString("emoji_return_alphabet", current).apply() + eng.setAlphabet("Emoji") + toolbar.setEmojiMode(true) + } + } + // The docked root's parent is a FrameLayout inside the IME window's // decor. setLayoutParams performs NO type conversion — LinearLayout // .LayoutParams survives until the next measure pass casts them and @@ -138,6 +174,9 @@ class DasherImeService : InputMethodService() { context = this, inputConnection = { currentInputConnection }, onBufferChanged = { reanchorEngineToTarget() }, + // editingToolbar is assigned below, before the button can be + // tapped — the deferred capture is why this isn't a cycle. + onToggleEmoji = { toggleEmojiMode() }, ) editingToolbar = toolbar @@ -416,6 +455,20 @@ class DasherImeService : InputMethodService() { } // Engine is live and rendering — drop the first-show loading overlay. loadingOverlay?.visibility = View.GONE + // Emoji toggle (#61): show the button only when the data bundle + // actually ships the Emoji alphabet (next DasherCore release); if + // the engine restarted while in emoji mode (alphabet persists in + // the engine's own settings), restore the toggle state and the + // remembered return alphabet. + val names = eng.getAlphabetNames() + val emojiAvailable = names.contains("Emoji") + editingToolbar?.setEmojiAvailable(emojiAvailable) + if (emojiAvailable && eng.getCurrentAlphabet() == "Emoji") { + if (emojiReturnAlphabet == null) { + emojiReturnAlphabet = imePrefs.getString("emoji_return_alphabet", null) + } + editingToolbar?.setEmojiMode(true) + } eng.start() } diff --git a/app/src/main/java/at/dasher/android/EditingToolbar.kt b/app/src/main/java/at/dasher/android/EditingToolbar.kt index 25f0e71..70f9217 100644 --- a/app/src/main/java/at/dasher/android/EditingToolbar.kt +++ b/app/src/main/java/at/dasher/android/EditingToolbar.kt @@ -17,11 +17,19 @@ import android.widget.LinearLayout * All actions go through [InputConnection] against the TARGET app's field. * After each action, [onBufferChanged] fires so the IME can re-anchor the * engine (re-read the target text and re-seed — RFC 0015 tier 2). + * + * Emoji toggle (#61, option 2): when [onToggleEmoji] is supplied and the + * engine ships the Emoji alphabet, a 😀/Abc button switches the engine's + * alphabet. The IME owns the switch logic (remembering the previous + * alphabet); the toolbar only renders state. + * + * @param onToggleEmoji invoked on button tap; null hides the button. */ class EditingToolbar( context: Context, private val inputConnection: () -> InputConnection?, private val onBufferChanged: () -> Unit, + private val onToggleEmoji: (() -> Unit)? = null, ) : LinearLayout(context) { private val density = context.resources.displayMetrics.density @@ -29,6 +37,8 @@ class EditingToolbar( android.content.res.Configuration.UI_MODE_NIGHT_MASK) == android.content.res.Configuration.UI_MODE_NIGHT_YES + private var emojiButton: Button? = null + init { orientation = HORIZONTAL gravity = Gravity.CENTER_VERTICAL @@ -46,6 +56,28 @@ class EditingToolbar( addTool("Sel", "Select all", mutates = false) { performAction(android.R.id.selectAll) } addTool("Cp", "Copy", mutates = false) { performAction(android.R.id.copy) } addTool("Ps", "Paste", mutates = true) { performAction(android.R.id.paste) } + // Emoji toggle (#61 option 2). Hidden until the IME confirms the + // engine's data bundle contains the Emoji alphabet (next DasherCore + // release) — setEmojiAvailable(true) makes it appear. + onToggleEmoji?.let { toggle -> + addSpacer(dp(8)) + emojiButton = addTool("😀", "Switch to emoji keyboard", mutates = false) { toggle() } + .also { it.visibility = GONE } + } + } + + /** Show/hide the emoji toggle (call once the engine's alphabets are known). */ + fun setEmojiAvailable(available: Boolean) { + emojiButton?.visibility = if (available) VISIBLE else GONE + } + + /** Relabel the toggle for the current mode: 😀 = enter emoji, Abc = back. */ + fun setEmojiMode(active: Boolean) { + emojiButton?.let { btn -> + btn.text = if (active) "Abc" else "😀" + btn.contentDescription = + if (active) "Switch back to text keyboard" else "Switch to emoji keyboard" + } } private fun dp(v: Int) = (v * density).toInt() @@ -72,13 +104,15 @@ class EditingToolbar( } private fun addTool(label: String, tooltip: String, large: Boolean = false, - mutates: Boolean = true, action: () -> Unit) { - addView(toolButton(label, tooltip, large).apply { + mutates: Boolean = true, action: () -> Unit): Button { + val btn = toolButton(label, tooltip, large).apply { setOnClickListener { action() if (mutates) onBufferChanged() } - }) + } + addView(btn) + return btn } private fun addSpacer(widthPx: Int) {