Skip to content
CleverKeys Wiki
implemented v1.2.7 User guide

Text Selection Technical Specification

Overview

Text selection integrates with Selection-Delete mode, TrackPoint navigation, and modifier key shortcuts to provide efficient text selection capabilities.

Key Components

ComponentFilePurpose
SelectionHandlerSelectionHandler.ktSelection coordination
PointersPointers.kt:1000-1200Selection-delete gesture
TrackPointControllerTrackPointController.ktCursor navigation
ModifierHandlerModifierHandler.ktShift selection
ConfigConfig.ktSelection preferences

Selection Methods

Selection-Delete Mode

// Pointers.kt:~1100
private fun handleSelectionDeleteMovement(ptr: Pointer) {
    val dx = ptr.x - selectionDeleteCenter.x
    val dy = ptr.y - selectionDeleteCenter.y

    // Horizontal selection (characters)
    if (abs(dx) > DEAD_ZONE) {
        val direction = if (dx < 0)
            KeyEvent.KEYCODE_DPAD_LEFT
        else
            KeyEvent.KEYCODE_DPAD_RIGHT

        sendShiftArrow(direction)
        scheduleRepeat(calculateDelay(abs(dx)))
    }

    // Vertical selection (lines)
    val verticalThreshold = keyHeight * config.selection_vertical_threshold / 100f
    if (abs(dy) > verticalThreshold) {
        val direction = if (dy < 0)
            KeyEvent.KEYCODE_DPAD_UP
        else
            KeyEvent.KEYCODE_DPAD_DOWN

        sendShiftArrow(direction)
        scheduleRepeat(calculateDelay(abs(dy)) / config.selection_vertical_speed)
    }
}

private fun sendShiftArrow(keyCode: Int) {
    val ic = inputConnection ?: return

    // Send Shift+Arrow to select
    ic.sendKeyEvent(KeyEvent(
        SystemClock.uptimeMillis(),
        SystemClock.uptimeMillis(),
        KeyEvent.ACTION_DOWN,
        keyCode,
        0,
        KeyEvent.META_SHIFT_ON
    ))
    ic.sendKeyEvent(KeyEvent(
        SystemClock.uptimeMillis(),
        SystemClock.uptimeMillis(),
        KeyEvent.ACTION_UP,
        keyCode,
        0,
        KeyEvent.META_SHIFT_ON
    ))
}

TrackPoint with Shift

// TrackPointController.kt
private fun handleTrackPointMovement(dx: Float, dy: Float) {
    val isShiftHeld = modifierHandler.isShiftActive()

    // Calculate direction
    val direction = calculatePrimaryDirection(dx, dy)

    if (isShiftHeld) {
        // Selection mode - send Shift+Arrow
        sendShiftArrow(directionToKeyCode(direction))
    } else {
        // Cursor mode - send plain Arrow
        sendArrowKey(directionToKeyCode(direction))
    }
}

Modifier Key Selection

// ModifierHandler.kt
class ModifierHandler {
    private var activeModifiers = 0

    fun onShiftPressed() {
        activeModifiers = activeModifiers or META_SHIFT_ON
    }

    fun onShiftReleased() {
        activeModifiers = activeModifiers and META_SHIFT_ON.inv()
    }

    fun isShiftActive(): Boolean {
        return activeModifiers and META_SHIFT_ON != 0
    }

    fun applyModifiersToKeyEvent(keyCode: Int): KeyEvent {
        return KeyEvent(
            SystemClock.uptimeMillis(),
            SystemClock.uptimeMillis(),
            KeyEvent.ACTION_DOWN,
            keyCode,
            0,
            activeModifiers
        )
    }
}

Selection Actions

Select All

// SelectionHandler.kt
fun selectAll() {
    val ic = inputConnection ?: return

    // Method 1: InputConnection API
    ic.performContextMenuAction(android.R.id.selectAll)

    // Fallback: Send Ctrl+A
    // ic.sendKeyEvent(KeyEvent(ACTION_DOWN, KEYCODE_A, 0, META_CTRL_ON))
}

Word Selection

// SelectionHandler.kt
fun selectWord() {
    val ic = inputConnection ?: return

    // Get text around cursor
    val before = ic.getTextBeforeCursor(50, 0) ?: return
    val after = ic.getTextAfterCursor(50, 0) ?: ""

    // Find word boundaries
    val wordStart = before.indexOfLast { !it.isLetterOrDigit() } + 1
    val wordEnd = after.indexOfFirst { !it.isLetterOrDigit() }.let {
        if (it < 0) after.length else it
    }

    // Select the word
    ic.setSelection(
        ic.getTextBeforeCursor(Int.MAX_VALUE, 0)?.length?.minus(before.length - wordStart) ?: 0,
        (ic.getTextBeforeCursor(Int.MAX_VALUE, 0)?.length ?: 0) + wordEnd
    )
}

Speed Calculation

// SelectionHandler.kt
private fun calculateRepeatDelay(distance: Float): Long {
    // Proportional to distance from center
    val maxDistance = keyWidth
    val normalized = (distance / maxDistance).coerceIn(0f, 1f)

    // Inverse relationship: further = faster
    val minDelay = 30L   // Fastest (far from center)
    val maxDelay = 200L  // Slowest (close to center)

    return (maxDelay - (normalized * (maxDelay - minDelay))).toLong()
}

Selection State

// SelectionHandler.kt
data class SelectionState(
    val hasSelection: Boolean,
    val selectionStart: Int,
    val selectionEnd: Int,
    val selectedText: String?
)

fun getSelectionState(): SelectionState {
    val ic = inputConnection ?: return SelectionState(false, 0, 0, null)

    val selected = ic.getSelectedText(0)
    val hasSelection = selected?.isNotEmpty() == true

    return SelectionState(
        hasSelection = hasSelection,
        selectionStart = -1,  // Would need to track
        selectionEnd = -1,
        selectedText = selected?.toString()
    )
}

Clipboard Integration

// SelectionHandler.kt
fun copySelection() {
    inputConnection?.performContextMenuAction(android.R.id.copy)
}

fun cutSelection() {
    inputConnection?.performContextMenuAction(android.R.id.cut)
}

fun deleteSelection() {
    val ic = inputConnection ?: return
    val selected = ic.getSelectedText(0)
    if (selected?.isNotEmpty() == true) {
        ic.commitText("", 1)  // Replace selection with empty
    }
}

Configuration

SettingKeyDefaultRange
Vertical Thresholdselection_vertical_threshold4020-80 (% of key height)
Vertical Speedselection_vertical_speed0.40.1-1.0
Selection Hapticshaptic_selectiontruebool
Dead Zoneselection_dead_zone10dp