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
165 changes: 152 additions & 13 deletions app/src/main/java/at/dasher/android/DasherImeService.kt

Large diffs are not rendered by default.

73 changes: 43 additions & 30 deletions app/src/main/java/at/dasher/android/EditingToolbar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,28 @@ package at.dasher.android

import android.content.Context
import android.graphics.drawable.GradientDrawable
import android.view.Gravity
import android.view.KeyEvent
import android.view.View
import android.view.inputmethod.InputConnection
import android.widget.Button
import android.widget.ImageButton
import android.view.Gravity
import android.widget.LinearLayout

/**
* RFC 0019 editing toolbar for the IME (#50): backspace, cursor movement,
* and clipboard actions — the tools Heide needs so she doesn't "go back
* and forth with SwiftKey" to fix mistakes.
*
* All actions go through [InputConnection] against the TARGET app's field.
* 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).
*
* Edit actions ONLY: mode toggles (emoji, float/dock) live in the IME's
* top bar so this strip stays a predictable editing cluster.
*
* Icons: Lucide (RFC 0002), rasterised for these classic Views via
* [LucideIcons] — text labels ("⌫", "Sel", "Cp", "Ps") read as clutter and
* depended on glyph coverage; the icon set matches the main app and the
* cross-platform design guide. Touch targets stay ≥48dp (#48).
*/
class EditingToolbar(
context: Context,
Expand All @@ -32,6 +36,12 @@ class EditingToolbar(
android.content.res.Configuration.UI_MODE_NIGHT_MASK) ==
android.content.res.Configuration.UI_MODE_NIGHT_YES

// Design-guide tokens via the shared palette (#78 review M4): see
// ImeToolbarPalette — one definition, used by both IME toolbars.
private val onSurface = ImeToolbarPalette.onSurface(nightMode)
private val border = ImeToolbarPalette.border(nightMode)
private val pressFill = ImeToolbarPalette.pressFill(nightMode)

init {
orientation = HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
Expand All @@ -41,42 +51,45 @@ class EditingToolbar(
// Buffer-mutating actions re-anchor the engine (buffer changed);
// non-mutating (Copy, Select All) don't — replacing the edit buffer
// on every action would reset the model mid-sentence (review 2).
addTool("←", "Move cursor left", mutates = true) { moveCursor(-1) }
addTool("→", "Move cursor right", mutates = true) { moveCursor(1) }
addTool(R.drawable.ic_lucide_arrow_left, "Move cursor left", mutates = true) { moveCursor(-1) }
addTool(R.drawable.ic_lucide_arrow_right, "Move cursor right", mutates = true) { moveCursor(1) }
addSpacer(dp(8))
addTool("⌫", "Delete", large = true, mutates = true) { backspace() }
addTool(R.drawable.ic_lucide_delete, "Delete", large = true, mutates = true) { backspace() }
addSpacer(dp(8))
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) }
addTool(R.drawable.ic_lucide_text_select, "Select all", mutates = false) { performAction(android.R.id.selectAll) }
addTool(R.drawable.ic_lucide_copy, "Copy", mutates = false) { performAction(android.R.id.copy) }
addTool(R.drawable.ic_lucide_clipboard_paste, "Paste", mutates = true) { performAction(android.R.id.paste) }
}

private fun dp(v: Int) = (v * density).toInt()

private fun toolButton(label: String, tooltip: String, large: Boolean): Button {
val width = if (large) dp(56) else dp(48)
return Button(context).apply {
text = label
textSize = if (large) 20f else 14f
isAllCaps = false
minimumWidth = width // exact width; LayoutParams below uses this
minimumHeight = dp(48) // #48: 48dp minimum touch target for motor-impaired users
setPadding(dp(4), dp(2), dp(4), dp(2))
private fun toolButton(iconRes: Int, tooltip: String, large: Boolean): ImageButton {
val size = dp(if (large) 56 else 48)
return ImageButton(context).apply {
layoutParams = LayoutParams(size, size).apply { marginEnd = dp(2) }
contentDescription = tooltip
background = GradientDrawable().apply {
cornerRadius = dp(6).toFloat()
setColor(0)
setStroke(1, if (nightMode) 0xFF3A4550.toInt() else 0xFFB8C4C8.toInt())
}
layoutParams = LayoutParams(width, LayoutParams.WRAP_CONTENT).apply {
marginEnd = dp(2)
scaleType = android.widget.ImageView.ScaleType.CENTER_INSIDE
setImageDrawable(LucideIcons.tinted(context, iconRes, onSurface))
// Bordered pill with a real pressed state — the flat
// GradientDrawable gave no touch feedback ("can't tell what
// the icons mean / no hover state").
background = android.graphics.drawable.StateListDrawable().apply {
addState(
intArrayOf(android.R.attr.state_pressed),
GradientDrawable().apply { cornerRadius = dp(8).toFloat(); setColor(pressFill) }
)
addState(
intArrayOf(),
GradientDrawable().apply { cornerRadius = dp(8).toFloat(); setColor(0); setStroke(1, border) }
)
}
if (android.os.Build.VERSION.SDK_INT >= 26) tooltipText = tooltip
}
}

private fun addTool(label: String, tooltip: String, large: Boolean = false,
mutates: Boolean = true, action: () -> Unit) {
addView(toolButton(label, tooltip, large).apply {
private fun addTool(iconRes: Int, tooltip: String,
large: Boolean = false, mutates: Boolean = true, action: () -> Unit) {
addView(toolButton(iconRes, tooltip, large).apply {
setOnClickListener {
action()
if (mutates) onBufferChanged()
Expand All @@ -85,7 +98,7 @@ class EditingToolbar(
}

private fun addSpacer(widthPx: Int) {
addView(View(context).apply {
addView(android.view.View(context).apply {
layoutParams = LayoutParams(widthPx, 1)
})
}
Expand Down
22 changes: 22 additions & 0 deletions app/src/main/java/at/dasher/android/ImeToolbarPalette.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package at.dasher.android

/**
* Design-guide tokens (dasher-design-guide DESIGN.md) for the IME's classic-
* View toolbars, as one definition shared by DasherImeService and
* EditingToolbar (#78 review M4 — the hexes previously lived twice and a
* third copy would have drifted).
*
* The border stroke is deliberately a DARKENED variant of the border token
* (#E0E6E8/#2A353D): the token itself doubles as the toolbar strip
* background, and a same-colour stroke on it is invisible.
*/
object ImeToolbarPalette {
fun onSurface(nightMode: Boolean): Int =
if (nightMode) 0xFFE6EDF3.toInt() else 0xFF0F4B75.toInt() // text token

fun border(nightMode: Boolean): Int =
if (nightMode) 0xFF3A4550.toInt() else 0xFFB8C4C8.toInt() // darkened border

fun pressFill(nightMode: Boolean): Int =
if (nightMode) 0xFF1D2D35.toInt() else 0xFFE9F2F1.toInt() // highlight tokens
}
26 changes: 26 additions & 0 deletions app/src/main/java/at/dasher/android/LucideIcons.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package at.dasher.android

import android.content.Context
import android.graphics.drawable.Drawable
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat

/**
* RFC 0002 (Lucide icons) for the IME's classic-View toolbars. The main app
* renders Lucide via the Compose library; the IME is built from LinearLayouts
* and Buttons (no composition, no lifecycle owner), so the same icons are
* baked as tintable vector drawables (res/drawable/ic_lucide_*.xml, generated
* from lucide-static, ISC licence) and tinted here. One icon set, one visual
* language, per the cross-platform design guide.
*/
object LucideIcons {

/**
* @param tint applied via mutate()+setTint so shared drawable constants
* are never re-tinted across widgets.
*/
fun tinted(context: Context, @DrawableRes res: Int, tint: Int): Drawable =
requireNotNull(ContextCompat.getDrawable(context, res)) {
"Lucide drawable $res missing"
}.mutate().apply { setTint(tint) }
}
22 changes: 22 additions & 0 deletions app/src/main/res/drawable/ic_lucide_arrow_left.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!-- Lucide arrow-left (lucide-static v1.62.0, ISC licence — dasher-project/governance RFC 0002).
Stroke-only 24dp vector; tint applied at the ImageView for theming. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="m12 19-7-7 7-7"
android:strokeColor="#FF000000"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<path
android:pathData="M19 12H5"
android:strokeColor="#FF000000"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
</vector>
22 changes: 22 additions & 0 deletions app/src/main/res/drawable/ic_lucide_arrow_right.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!-- Lucide arrow-right (lucide-static v1.62.0, ISC licence — dasher-project/governance RFC 0002).
Stroke-only 24dp vector; tint applied at the ImageView for theming. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M5 12h14"
android:strokeColor="#FF000000"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<path
android:pathData="m12 5 7 7-7 7"
android:strokeColor="#FF000000"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
</vector>
43 changes: 43 additions & 0 deletions app/src/main/res/drawable/ic_lucide_clipboard_paste.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!-- Lucide clipboard-paste (lucide-static v1.62.0, ISC licence — dasher-project/governance RFC 0002).
Stroke-only 24dp vector; tint applied at the ImageView for theming. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M11 14h10"
android:strokeColor="#FF000000"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<path
android:pathData="M16 4h2a2 2 0 0 1 2 2v1.344"
android:strokeColor="#FF000000"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<path
android:pathData="m17 18 4-4-4-4"
android:strokeColor="#FF000000"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<path
android:pathData="M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 1.793-1.113"
android:strokeColor="#FF000000"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<path
android:pathData="M9.0 2.0 h6.0 a1.0 1.0 0 0 1 1.0 1.0 v2.0 a1.0 1.0 0 0 1 -1.0 1.0 h-6.0 a1.0 1.0 0 0 1 -1.0 -1.0 v-2.0 a1.0 1.0 0 0 1 1.0 -1.0 z"
android:strokeColor="#FF000000"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
</vector>
22 changes: 22 additions & 0 deletions app/src/main/res/drawable/ic_lucide_copy.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!-- Lucide copy (lucide-static v1.62.0, ISC licence — dasher-project/governance RFC 0002).
Stroke-only 24dp vector; tint applied at the ImageView for theming. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
android:strokeColor="#FF000000"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<path
android:pathData="M10.0 8.0 h10.0 a2.0 2.0 0 0 1 2.0 2.0 v10.0 a2.0 2.0 0 0 1 -2.0 2.0 h-10.0 a2.0 2.0 0 0 1 -2.0 -2.0 v-10.0 a2.0 2.0 0 0 1 2.0 -2.0 z"
android:strokeColor="#FF000000"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
</vector>
29 changes: 29 additions & 0 deletions app/src/main/res/drawable/ic_lucide_delete.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!-- Lucide delete (lucide-static v1.62.0, ISC licence — dasher-project/governance RFC 0002).
Stroke-only 24dp vector; tint applied at the ImageView for theming. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M10 5a2 2 0 0 0-1.344.519l-6.328 5.74a1 1 0 0 0 0 1.481l6.328 5.741A2 2 0 0 0 10 19h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2z"
android:strokeColor="#FF000000"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<path
android:pathData="m12 9 6 6"
android:strokeColor="#FF000000"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<path
android:pathData="m18 9-6 6"
android:strokeColor="#FF000000"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
</vector>
78 changes: 78 additions & 0 deletions app/src/main/res/drawable/ic_lucide_keyboard_off.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<!-- Lucide keyboard-off (lucide-static v1.62.0, ISC licence — dasher-project/governance RFC 0002).
Stroke-only 24dp vector; tint applied at the ImageView for theming. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M 20 4 A2 2 0 0 1 22 6"
android:strokeColor="#FF000000"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<path
android:pathData="M 22 6 L 22 16.41"
android:strokeColor="#FF000000"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<path
android:pathData="M 7 16 L 16 16"
android:strokeColor="#FF000000"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<path
android:pathData="M 9.69 4 L 20 4"
android:strokeColor="#FF000000"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<path
android:pathData="M14 8h.01"
android:strokeColor="#FF000000"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<path
android:pathData="M18 8h.01"
android:strokeColor="#FF000000"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<path
android:pathData="m2 2 20 20"
android:strokeColor="#FF000000"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<path
android:pathData="M20 20H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2"
android:strokeColor="#FF000000"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<path
android:pathData="M6 8h.01"
android:strokeColor="#FF000000"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<path
android:pathData="M8 12h.01"
android:strokeColor="#FF000000"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
</vector>
Loading
Loading