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: 39 additions & 0 deletions app/src/main/java/at/dasher/android/DasherImeService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,27 @@ class DasherImeService : InputMethodService() {
private var floating = false
private var floatingView: LinearLayout? = null
private var floatDockBtn: android.widget.ImageButton? = null

/**
* RFC 0012 direct-entry clause (2026-10-30 amendment): compact live WPM
* in the top bar. Refreshed at 2 Hz — the visibility is re-read each
* tick so toggling the setting takes effect without restarting the IME.
*/
private var wpmView: android.widget.TextView? = null
private val wpmTicker = object : Runnable {
override fun run() {
val view = wpmView ?: return
val eng = engine
if (eng == null || !TypingRateSetting.isEnabled(this@DasherImeService)) {
view.visibility = View.GONE
} else {
view.text = "${eng.wpm().toInt()} wpm"
view.visibility = View.VISIBLE
}
handler.postDelayed(this, 500)
}
}
private val handler = Handler(Looper.getMainLooper())
private var emojiToggleBtn: Button? = null

// Last-used mode (dock/float), restored on the next keyboard show so
Expand Down Expand Up @@ -210,6 +231,18 @@ class DasherImeService : InputMethodService() {
gravity = Gravity.CENTER_VERTICAL or Gravity.END
setPadding(dp(8, density), dp(2, density), dp(8, density), dp(2, density))
}
// RFC 0012 (2026-10-30 amendment) direct-entry clause: compact live
// WPM in the IME's control strip, current rate only, ~2 Hz, visible
// only when Settings → Output → Show typing rate is on. The original
// exemption ("no persistent strip") predates the stable top bar.
wpmView = android.widget.TextView(this).apply {
textSize = 13f
setTextColor(topBarTint())
visibility = View.GONE
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
marginEnd = dp(6, density)
}
}
val floatBtn = iconButton(R.drawable.ic_lucide_picture_in_picture_2, "Float the keyboard") { enterFloatingMode() }
// Emoji toggle (#61 option 2): GONE until createEngine() confirms
// the data bundle ships the Emoji alphabet (DasherCore b00a4c8a+).
Expand All @@ -234,6 +267,7 @@ class DasherImeService : InputMethodService() {
}
emojiToggleBtn = emojiBtn
val hideBtn = iconButton(R.drawable.ic_lucide_keyboard_off, "Hide the keyboard") { requestHideSelf(0) }
top.addView(wpmView)
top.addView(floatBtn)
top.addView(emojiBtn)
top.addView(hideBtn)
Expand Down Expand Up @@ -866,6 +900,9 @@ class DasherImeService : InputMethodService() {
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
super.onStartInputView(info, restarting)
Log.d(TAG, "onStartInputView(restarting=$restarting, floating=$floating)")
// RFC 0012: start the compact-WPM ticker with the keyboard.
handler.removeCallbacks(wpmTicker)
handler.postDelayed(wpmTicker, 500)
// #78 review I2 — defense-in-depth for the LayoutParams stamp: the
// framework replaces our docked-root LayoutParams on attach; the
// posted-handler re-apply covers the attach, this covers any later
Expand All @@ -888,6 +925,8 @@ class DasherImeService : InputMethodService() {

override fun onFinishInputView(finishingInput: Boolean) {
super.onFinishInputView(finishingInput)
handler.removeCallbacks(wpmTicker)
wpmView?.visibility = View.GONE
engine?.stop()
}

Expand Down
10 changes: 9 additions & 1 deletion app/src/main/java/at/dasher/android/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,16 @@ class MainActivity : ComponentActivity() {
// RFC 0012: typing-rate overlay — CPS + WPM from the engine's rolling
// window (dasher_get_cps / dasher_get_wpm), refreshed once a second
// while playing. Apple shows the same "X.X cps · N wpm" readout.
// Opt-in via Settings → Output (2026-10-30: was always-on; the
// RFC's toggle now governs this AND the IME's compact WPM).
var typingRate by remember { mutableStateOf("") }
LaunchedEffect(isPlaying) {
var typingRateEnabled by remember { mutableStateOf(TypingRateSetting.isEnabled(applicationContext)) }
val appCtx = applicationContext
LaunchedEffect(showSettings) { // re-read when Settings closes
typingRateEnabled = TypingRateSetting.isEnabled(appCtx)
}
LaunchedEffect(isPlaying, typingRateEnabled) {
if (!typingRateEnabled) { typingRate = ""; return@LaunchedEffect }
while (isPlaying) {
engine?.let { e ->
typingRate = getString(R.string.typing_rate, e.cps(), e.wpm().toInt())
Expand Down
43 changes: 43 additions & 0 deletions app/src/main/java/at/dasher/android/SettingsScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@ fun SettingsScreen(
// keyboard from the shared dasher_ime prefs.
item { KeyboardStartSection() }
}
if (tabGroup == "Output") {
// RFC 0012 (2026-10-30 amendment): the opt-in toggle —
// gates the main-app readout AND the IME's compact WPM.
item { TypingRateSection() }
}
if (tabGroup == "Customization") {
item { AppearanceSection(engine, bump) }
// Output-pane font lives with the other appearance
Expand Down Expand Up @@ -251,6 +256,44 @@ private fun PrivacyContent(context: android.content.Context) {
* compatibility filter.
*/

/**
* RFC 0012: the typing-rate opt-in. One toggle governs both surfaces (the
* main status-bar readout and the IME top bar's compact live-WPM — the
* 2026-10-30 amendment). Default OFF per the RFC.
*/
@Composable
private fun TypingRateSection() {
val context = androidx.compose.ui.platform.LocalContext.current
var enabled by remember { mutableStateOf(TypingRateSetting.isEnabled(context)) }
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp)) {
Row(
modifier = Modifier
.fillMaxWidth()
.toggleable(
value = enabled,
role = Role.Switch,
onValueChange = { v ->
TypingRateSetting.setEnabled(context, v)
enabled = v
UiTelemetry.setting("Show typing rate", v, "settings")
},
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Column(modifier = Modifier.weight(1f)) {
Text("Show typing rate", style = MaterialTheme.typography.bodyLarge)
Text(
"Live characters-per-second and words-per-minute while writing — in the status bar here, and a compact words-per-minute figure on the keyboard's top bar.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Switch(checked = enabled, onCheckedChange = null)
}
}
}

/**
* #75: IME start behaviour. The keyboard normally reopens in its last-used
* mode (docked or floating); users who want it docked EVERY time —
Expand Down
31 changes: 31 additions & 0 deletions app/src/main/java/at/dasher/android/TypingRateSetting.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package at.dasher.android

import android.content.Context

/**
* RFC 0012 typing-rate display toggle. Frontend setting (DasherCore has no
* BP_SHOW_TYPING_RATE — Windows/Apple also implement it locally). Default
* OFF per the RFC ("doesn't clutter the main writing view").
*
* Governs both surfaces that show a rate:
* - the main app's status-bar readout ("X.X cps · N wpm")
* - the IME top bar's compact live-WPM (RFC 0012 direct-entry clause, added
* 2026-10-30 amendment — the "no persistent strip" exemption predates the
* IME's stable top bar)
*
* Stored in the app's default SharedPreferences so the main activity and the
* IME service (same package, same process) share one value.
*/
object TypingRateSetting {
private const val PREFS = "at.dasher.android_preferences"
private const val KEY = "show_typing_rate"

fun isEnabled(context: Context): Boolean =
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.getBoolean(KEY, false)

fun setEnabled(context: Context, value: Boolean) {
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.edit().putBoolean(KEY, value).apply()
}
}
Loading