diff --git a/app/src/main/java/at/dasher/android/DasherImeService.kt b/app/src/main/java/at/dasher/android/DasherImeService.kt index ba22539..8966eb2 100644 --- a/app/src/main/java/at/dasher/android/DasherImeService.kt +++ b/app/src/main/java/at/dasher/android/DasherImeService.kt @@ -43,6 +43,7 @@ class DasherImeService : InputMethodService() { private var floating = false private var floatingView: LinearLayout? = null private var floatDockBtn: Button? = null + private var emojiToggleBtn: Button? = null // Last-used mode (dock/float), restored on the next keyboard show so // the IME opens where the user left it (Heide: "start it directly as @@ -66,7 +67,7 @@ class DasherImeService : InputMethodService() { private fun toggleEmojiMode() { val eng = engine ?: return - val toolbar = editingToolbar ?: return + val btn = emojiToggleBtn ?: return val current = eng.getCurrentAlphabet() if (current == "Emoji") { val target = emojiReturnAlphabet @@ -79,7 +80,8 @@ class DasherImeService : InputMethodService() { } emojiReturnAlphabet = null imePrefs.edit().remove("emoji_return_alphabet").apply() - toolbar.setEmojiMode(false) + btn.text = "😀" + btn.contentDescription = "Switch to emoji keyboard" // 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). @@ -88,7 +90,8 @@ class DasherImeService : InputMethodService() { emojiReturnAlphabet = current imePrefs.edit().putString("emoji_return_alphabet", current).apply() eng.setAlphabet("Emoji") - toolbar.setEmojiMode(true) + btn.text = "Abc" + btn.contentDescription = "Switch back to text keyboard" } } @@ -152,18 +155,30 @@ class DasherImeService : InputMethodService() { } this.canvasHost = canvasHost - // Top bar (shared look): Hide + Float toggle. + // Top bar (shared look): Float toggle + emoji mode + Hide. Mode + // controls cluster here so the editing strip below stays a pure + // edit-action row (Heide feedback: too many buttons up top). val top = LinearLayout(this).apply { orientation = LinearLayout.HORIZONTAL gravity = Gravity.CENTER_VERTICAL or Gravity.END setPadding(dp(8, density), dp(2, density), dp(8, density), dp(2, density)) } val floatBtn = Button(this).apply { text = "Float" } + // Emoji toggle (#61 option 2): GONE until createEngine() confirms + // the data bundle ships the Emoji alphabet (DasherCore b00a4c8a+). + val emojiBtn = Button(this).apply { + text = "😀" + contentDescription = "Switch to emoji keyboard" + visibility = View.GONE + setOnClickListener { toggleEmojiMode() } + } + emojiToggleBtn = emojiBtn val hideBtn = Button(this).apply { text = "Hide" setOnClickListener { requestHideSelf(0) } } top.addView(floatBtn) + top.addView(emojiBtn) top.addView(hideBtn) // RFC 0019 editing toolbar (#50): backspace, cursor, clipboard. @@ -174,9 +189,6 @@ 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 @@ -456,18 +468,18 @@ 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. + // actually ships the Emoji alphabet; 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) + emojiToggleBtn?.visibility = if (emojiAvailable) View.VISIBLE else View.GONE if (emojiAvailable && eng.getCurrentAlphabet() == "Emoji") { if (emojiReturnAlphabet == null) { emojiReturnAlphabet = imePrefs.getString("emoji_return_alphabet", null) } - editingToolbar?.setEmojiMode(true) + emojiToggleBtn?.text = "Abc" + emojiToggleBtn?.contentDescription = "Switch back to text keyboard" } eng.start() } @@ -604,6 +616,7 @@ class DasherImeService : InputMethodService() { loadingOverlay = null dockedRoot = null floatDockBtn = null + emojiToggleBtn = null editingToolbar = null super.onDestroy() } diff --git a/app/src/main/java/at/dasher/android/EditingToolbar.kt b/app/src/main/java/at/dasher/android/EditingToolbar.kt index 70f9217..4879689 100644 --- a/app/src/main/java/at/dasher/android/EditingToolbar.kt +++ b/app/src/main/java/at/dasher/android/EditingToolbar.kt @@ -18,18 +18,13 @@ import android.widget.LinearLayout * 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. + * Edit actions ONLY: mode toggles (emoji, float/dock) live in the IME's + * top bar so this strip stays a predictable editing cluster. */ 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 @@ -37,8 +32,6 @@ 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 @@ -56,28 +49,6 @@ 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() @@ -104,15 +75,13 @@ class EditingToolbar( } private fun addTool(label: String, tooltip: String, large: Boolean = false, - mutates: Boolean = true, action: () -> Unit): Button { - val btn = toolButton(label, tooltip, large).apply { + mutates: Boolean = true, action: () -> Unit) { + addView(toolButton(label, tooltip, large).apply { setOnClickListener { action() if (mutates) onBufferChanged() } - } - addView(btn) - return btn + }) } private fun addSpacer(widthPx: Int) { diff --git a/app/src/main/java/at/dasher/android/MainActivity.kt b/app/src/main/java/at/dasher/android/MainActivity.kt index c729f5d..143a949 100644 --- a/app/src/main/java/at/dasher/android/MainActivity.kt +++ b/app/src/main/java/at/dasher/android/MainActivity.kt @@ -117,6 +117,11 @@ class MainActivity : ComponentActivity() { AlphabetIndex.get(this).associateBy { it.id } } private var currentAlphabet by mutableStateOf("") + // Emoji mode (#61 option 2): the return alphabet is shared with the IME + // via the dasher_ime prefs — toggle in the app, restore in the keyboard, + // and vice versa. + private var emojiReturnAlphabet: String? = null + private val imePrefs get() = getSharedPreferences("dasher_ime", MODE_PRIVATE) private var palettes by mutableStateOf>(emptyList()) private var currentPalette by mutableStateOf("") private var speedPercent by mutableStateOf(100) @@ -351,6 +356,9 @@ class MainActivity : ComponentActivity() { typingRate = typingRate, alphabets = alphabets, currentAlphabet = currentAlphabet, + emojiAvailable = alphabets.contains("Emoji"), + emojiActive = currentAlphabet == "Emoji", + onToggleEmoji = { toggleEmojiMode() }, speedPercent = speedPercent, autoSpeed = autoSpeed, isPlaying = isPlaying, @@ -445,6 +453,34 @@ class MainActivity : ComponentActivity() { .putBoolean(AlphabetPrefs.KEY_FOLLOWS_LOCALE, false).apply() } + /** + * Emoji toggle (#61 option 2): switch to the Emoji alphabet and remember + * the way back (shared with the IME via dasher_ime prefs). Explicit + * choice → pins, like any picker selection. + */ + private fun toggleEmojiMode() { + val eng = engine ?: return + val current = eng.getCurrentAlphabet() + if (current == "Emoji") { + val target = emojiReturnAlphabet + if (target.isNullOrEmpty() || target == "Emoji") { + eng.setAlphabet("English with limited punctuation") + } else { + eng.setAlphabet(target) + } + emojiReturnAlphabet = null + imePrefs.edit().remove("emoji_return_alphabet").apply() + } else { + emojiReturnAlphabet = current + imePrefs.edit().putString("emoji_return_alphabet", current).apply() + eng.setAlphabet("Emoji") + } + currentAlphabet = eng.getCurrentAlphabet() + eng.saveSettings() + pinAlphabetChoice() + AnalyticsService.capture("emoji_toggled", mapOf("active" to (currentAlphabet == "Emoji"))) + } + /** * Switches the active input method (RFC 0010 settings IA — the control lives * in Settings → Input, not the toolbar). Registers/releases the providers @@ -687,6 +723,9 @@ class MainActivity : ComponentActivity() { typingRate: String, alphabets: List, currentAlphabet: String, + emojiAvailable: Boolean, + emojiActive: Boolean, + onToggleEmoji: () -> Unit, speedPercent: Int, autoSpeed: Boolean, isPlaying: Boolean, @@ -787,6 +826,9 @@ class MainActivity : ComponentActivity() { alphabets = alphabets, currentAlphabet = currentAlphabet, onAlphabetSelected = onAlphabetSelected, + emojiAvailable = emojiAvailable, + emojiActive = emojiActive, + onToggleEmoji = onToggleEmoji, speedPercent = speedPercent, onSpeedChanged = onSpeedChanged, autoSpeed = autoSpeed, @@ -882,18 +924,32 @@ class MainActivity : ComponentActivity() { alphabets: List, currentAlphabet: String, onAlphabetSelected: (String) -> Unit, + emojiAvailable: Boolean, + emojiActive: Boolean, + onToggleEmoji: () -> Unit, speedPercent: Int, onSpeedChanged: (Int) -> Unit, autoSpeed: Boolean, onAutoSpeedChanged: (Boolean) -> Unit ) { - // Windows-style single-row bottom bar: alphabet picker | speed stepper | auto toggle. + // Windows-style single-row bottom bar: emoji toggle | alphabet picker | speed stepper | auto toggle. Surface(color = MaterialTheme.colorScheme.surface) { Row( modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp) ) { + // Emoji toggle (#61): small mode switch beside the picker — + // the alphabet stays selected; the toggle is a shortcut with + // a remembered way back. + if (emojiAvailable) { + IconButton(onClick = onToggleEmoji, modifier = Modifier.size(36.dp)) { + Text( + if (emojiActive) "Abc" else "😀", + style = MaterialTheme.typography.titleMedium + ) + } + } DropdownPicker( leadingIcon = Lucide.Languages, options = alphabets,