Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions app/src/main/java/at/dasher/android/DasherImeService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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()
}

Expand Down
40 changes: 37 additions & 3 deletions app/src/main/java/at/dasher/android/EditingToolbar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,28 @@ 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
private val nightMode = (context.resources.configuration.uiMode and
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
Expand All @@ -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()
Expand All @@ -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) {
Expand Down
Loading