Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8e35be1
Observe actual Back key delivery without changing app dispatch
moikdev Nov 5, 2026
6126b3c
Trace pre-IME and dispatcher Back stages without altering handlers
moikdev Nov 5, 2026
1dc1a1c
Restore window observer independently of Back trace serialization
moikdev Nov 5, 2026
074f14b
Let the Back dispatcher own menu dismissal for the complete press
moikdev Nov 5, 2026
323c383
Verify menu Back ownership, cancellation and caller input across dism…
moikdev Nov 5, 2026
0b535d8
Probe menu priority against a later registered native route callback
moikdev Nov 5, 2026
bc338ac
Give each open menu Back priority and witness range setup around IME
moikdev Nov 5, 2026
c0d9a06
Retain committed Emoji selection while a palette menu owns focus
moikdev Nov 5, 2026
319d9c8
Make repeated keyboard setup idempotent in menu return contracts
moikdev Nov 5, 2026
58eb26d
Publish Back ownership evidence for palette menus
moikdev Nov 5, 2026
867f991
Define keyboard-first Back for searchable menus and assert one owner …
moikdev Nov 5, 2026
ded3f81
Settle the Actions menu's own keyboard intent before toggling it
moikdev Nov 5, 2026
782f22e
Publish final-pair Back evidence with keyboard-first searchable menus
moikdev Nov 5, 2026
577ef51
Correct round-2 evidence wording and publish marker and capture records
moikdev Nov 5, 2026
4d7a34b
Link the API 26 menu keyboard follow-up
moikdev Nov 5, 2026
746a454
Tighten owner-assertion scope and marker timing; publish per-cohort m…
moikdev Nov 5, 2026
13c2729
Present phone action menus as a keyboard-docked sheet
moikdev Nov 6, 2026
6b7d5fa
Keep the phone action sheet's body height at large font
moikdev Nov 6, 2026
32bab7e
Publish final-pair evidence for the phone action sheet
moikdev Nov 6, 2026
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

Large diffs are not rendered by default.

24 changes: 20 additions & 4 deletions app/src/main/java/dev/tinycast/android/EmojiUi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,11 @@ fun EmojiPicker(store: EmojiStore, searchEpoch: Int, actionCurrent: () -> Boolea
return true
}
fun back() { if(actionsMenu) actionsMenu=false else if (menu) menu = false else if (queryEditing) queryEditing = false else if (store.query.isNotEmpty()) store.search("") else close() }
BackHandler(onBack = ::back)
BackHandler {
PaletteBackTrace.handler("emoji-dispatcher-before", menu || actionsMenu, store.query.length)
back()
PaletteBackTrace.handler("emoji-dispatcher-after", menu || actionsMenu, store.query.length)
}
PaletteViewport(emptyFeedback=results.isEmpty()) { wide, top, height ->
val compactEmpty = results.isEmpty() && height < 180.dp
val columns = ((minOf(maxWidth - 24.dp, PaletteTheme.width).value - 24) / 64).toInt().coerceIn(3, 10)
Expand Down Expand Up @@ -183,8 +187,16 @@ fun EmojiPicker(store: EmojiStore, searchEpoch: Int, actionCurrent: () -> Boolea
IconButton(onClick = close, modifier = Modifier.size(48.dp).semantics { contentDescription = "Back to commands" }.testTag("emoji-back")) { Glyph("back",PaletteTheme.ink,24) }
Spacer(Modifier.width(2.dp))
BasicTextField(displayedSearch, {
searchValue = it
if (it.text != store.query) { store.search(it.text); copiedName = null }
// A menu borrows focus. CoreTextField's blur collapse must not
// overwrite the caller's committed range; every other update,
// including deliberate selection changes, still applies.
val current = searchValue
val keepMenuRange = (menu || actionsMenu || paletteMenus.current != null) &&
it.text == current.text && it.composition == null && current.composition == null &&
!current.selection.collapsed && it.selection == TextRange(current.selection.max)
val next = if (keepMenuRange) it.copy(selection = current.selection) else it
searchValue = next
if (next.text != store.query) { store.search(next.text); copiedName = null }
}, singleLine = true,
modifier = Modifier.weight(1f).heightIn(min=48.dp).paletteFocusBookmark(searchFocus).onFocusChanged { searchFocused = it.isFocused }.testTag("emoji-search"),
textStyle = TextStyle(color = PaletteTheme.ink, fontSize = 20.sp, lineHeight = 26.sp), cursorBrush = SolidColor(PaletteTheme.accent),
Expand Down Expand Up @@ -255,6 +267,9 @@ fun EmojiPicker(store: EmojiStore, searchEpoch: Int, actionCurrent: () -> Boolea
Column(modifier) {
Text(selected?.name ?: "Emoji & Symbols", fontSize=12.sp,lineHeight=16.sp,maxLines=2,
overflow=TextOverflow.Ellipsis,modifier=Modifier.testTag("emoji-selected-name"))
// Phones drop the selection hint once a symbol is selected;
// copy/edit status and the empty-selection hint still show.
if(wide || selected == null || copiedName != null || queryEditing)
Text(copiedName ?: if(queryEditing) "Editing query" else if(wide) "Arrows select · Enter copy · Ctrl L edit" else "Tap a symbol to select",
fontSize=11.sp,lineHeight=15.sp,color=PaletteTheme.secondary,maxLines=2,overflow=TextOverflow.Ellipsis,
modifier=Modifier.semantics { liveRegion=LiveRegionMode.Polite }.testTag("emoji-copy-status"))
Expand All @@ -266,7 +281,8 @@ fun EmojiPicker(store: EmojiStore, searchEpoch: Int, actionCurrent: () -> Boolea
}
}) {
Box {
PaletteActionCapsule("Copy to Clipboard",{currentEntry()?.let(::copyEntry)},{actionsMenu=true},"emoji-copy","emoji-actions",compact=!wide,enabled=selected!=null)
PaletteActionCapsule(if(wide) "Copy to Clipboard" else "Copy",{currentEntry()?.let(::copyEntry)},{actionsMenu=true},"emoji-copy","emoji-actions",
compact=!wide,enabled=selected!=null,description="Copy to Clipboard")
PaletteChoiceMenu(actionsMenu,{actionsMenu=false},listOf(
PaletteChoice("emoji-action-copy","Copy to Clipboard",enabled=selected!=null,shortcut=listOf("↵"),icon="clipboard") { currentEntry()?.let(::copyEntry) },
PaletteChoice("emoji-action-favorite",if(selected?.id in store.favorites)"Remove Favorite"else"Add Favorite",enabled=selected!=null,
Expand Down
10 changes: 5 additions & 5 deletions app/src/main/java/dev/tinycast/android/PaletteActionCapsule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import androidx.compose.ui.unit.sp
internal fun PaletteActionCapsule(title: String, primary: () -> Unit, actions: () -> Unit,
primaryTag: String, actionsTag: String, compact: Boolean,
enabled: Boolean = true, actionsEnabled: Boolean = enabled, showActions: Boolean = true,
modifier: Modifier = Modifier) {
modifier: Modifier = Modifier, description: String = title) {
val largeFont = LocalDensity.current.fontScale > 1.3f
val reflow = compact || largeFont
val shape = RoundedCornerShape(24.dp)
Expand All @@ -30,15 +30,15 @@ internal fun PaletteActionCapsule(title: String, primary: () -> Unit, actions: (
.testTag("palette-action-capsule")
if (reflow && largeFont && maxWidth < 300.dp) {
Column(surface.fillMaxWidth()) {
PrimaryControl(title,primary,primaryTag,enabled,3,false,Modifier.fillMaxWidth().heightIn(min=48.dp))
PrimaryControl(title,description,primary,primaryTag,enabled,3,false,Modifier.fillMaxWidth().heightIn(min=48.dp))
if(showActions) {
Box(Modifier.fillMaxWidth().height(1.dp).padding(horizontal=14.dp).background(PaletteTheme.separator))
ActionsControl(actions,actionsTag,actionsEnabled,false,Modifier.fillMaxWidth().heightIn(min=48.dp))
}
}
} else {
Row(surface.height(IntrinsicSize.Min).heightIn(min=48.dp),verticalAlignment=Alignment.CenterVertically) {
PrimaryControl(title,primary,primaryTag,enabled,2,!reflow,
PrimaryControl(title,description,primary,primaryTag,enabled,2,!reflow,
Modifier.weight(1f,fill=reflow).widthIn(min=48.dp).fillMaxHeight())
if(showActions) {
Box(Modifier.width(1.dp).fillMaxHeight().padding(vertical=12.dp).background(PaletteTheme.separator))
Expand All @@ -50,8 +50,8 @@ internal fun PaletteActionCapsule(title: String, primary: () -> Unit, actions: (
}

@Composable
private fun PrimaryControl(title:String,click:()->Unit,tag:String,enabled:Boolean,lines:Int,hints:Boolean,modifier:Modifier) {
Row(modifier.clickable(enabled=enabled,role=Role.Button,onClick=click).semantics { contentDescription=title }
private fun PrimaryControl(title:String,description:String,click:()->Unit,tag:String,enabled:Boolean,lines:Int,hints:Boolean,modifier:Modifier) {
Row(modifier.clickable(enabled=enabled,role=Role.Button,onClick=click).semantics { contentDescription=description }
.testTag(tag).padding(horizontal=14.dp,vertical=10.dp),verticalAlignment=Alignment.CenterVertically) {
Text(title,color=if(enabled)PaletteTheme.ink else PaletteTheme.tertiary,fontSize=14.sp,lineHeight=20.sp,
fontWeight=FontWeight.Medium,maxLines=lines,overflow=TextOverflow.Ellipsis,modifier=Modifier.weight(1f,fill=false))
Expand Down
47 changes: 47 additions & 0 deletions app/src/main/java/dev/tinycast/android/PaletteBackTrace.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package dev.tinycast.android

import android.os.SystemClock
import android.view.KeyEvent

internal data class PaletteBackEvent(
val stage: String,
val uptime: Long,
val action: Int? = null,
val downTime: Long? = null,
val eventTime: Long? = null,
val repeat: Int? = null,
val canceled: Boolean? = null,
val menuOpen: Boolean? = null,
val queryLength: Int? = null,
val handled: Boolean? = null
)

internal data class PaletteBackSnapshot(val enabled: Boolean, val dropped: Int, val events: List<PaletteBackEvent>)

/** Debug-only, opt-in primitive metadata. Never retains text, callbacks or views. */
internal object PaletteBackTrace {
const val property = "dev.droidcast.paletteBackTrace"
private var history: ArrayDeque<PaletteBackEvent>? = null
private var dropped = 0
private fun enabled() = BuildConfig.DEBUG && System.getProperty(property) == "true"

@Synchronized private fun append(event: PaletteBackEvent) {
val events = history ?: ArrayDeque<PaletteBackEvent>().also { history = it }
if (events.size == 128) { events.removeFirst(); dropped++ }
events.addLast(event)
}

fun key(stage: String, phase: String, event: KeyEvent, menuOpen: Boolean, handled: Boolean? = null) {
if (!enabled() || event.keyCode != KeyEvent.KEYCODE_BACK) return
append(PaletteBackEvent("$stage-$phase", SystemClock.uptimeMillis(), event.action, event.downTime,
event.eventTime, event.repeatCount, event.isCanceled, menuOpen, handled = handled))
}

fun handler(stage: String, menuOpen: Boolean, queryLength: Int? = null) {
if (enabled()) append(PaletteBackEvent(stage, SystemClock.uptimeMillis(),
menuOpen = menuOpen, queryLength = queryLength))
}

@Synchronized fun snapshot() = PaletteBackSnapshot(enabled(), dropped, history?.toList().orEmpty())
@Synchronized fun clear() { history = null; dropped = 0 }
}
Loading
Loading