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
39 changes: 26 additions & 13 deletions app/src/main/java/at/dasher/android/DasherImeService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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).
Expand All @@ -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"
}
}

Expand Down Expand Up @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -604,6 +616,7 @@ class DasherImeService : InputMethodService() {
loadingOverlay = null
dockedRoot = null
floatDockBtn = null
emojiToggleBtn = null
editingToolbar = null
super.onDestroy()
}
Expand Down
41 changes: 5 additions & 36 deletions app/src/main/java/at/dasher/android/EditingToolbar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,20 @@ 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
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 @@ -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()
Expand All @@ -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) {
Expand Down
58 changes: 57 additions & 1 deletion app/src/main/java/at/dasher/android/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<List<String>>(emptyList())
private var currentPalette by mutableStateOf("")
private var speedPercent by mutableStateOf(100)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -687,6 +723,9 @@ class MainActivity : ComponentActivity() {
typingRate: String,
alphabets: List<String>,
currentAlphabet: String,
emojiAvailable: Boolean,
emojiActive: Boolean,
onToggleEmoji: () -> Unit,
speedPercent: Int,
autoSpeed: Boolean,
isPlaying: Boolean,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -882,18 +924,32 @@ class MainActivity : ComponentActivity() {
alphabets: List<String>,
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,
Expand Down
Loading