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
15 changes: 15 additions & 0 deletions app/keyboard/src/main/java/ee/oyatl/ime/keyboard/FlickKeyCode.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ee.oyatl.ime.keyboard

object FlickKeyCode {
const val FLAG_FLICK = 0x2000000
const val MASK_KEYCODE = 0x00fffff
const val MASK_DIRECTION = 0x0f00000
const val DIRECTION_UP = 0x0100000
const val DIRECTION_DOWN = 0x0200000
const val DIRECTION_LEFT = 0x0300000
const val DIRECTION_RIGHT = 0x0400000
const val DIRECTION_UP_LEFT = 0x0500000
const val DIRECTION_UP_RIGHT = 0x0600000
const val DIRECTION_DOWN_LEFT = 0x0700000
const val DIRECTION_DOWN_RIGHT = 0x0800000
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import android.os.VibratorManager
import android.view.KeyEvent
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission
import ee.oyatl.ime.keyboard.listener.KeyboardListener
import ee.oyatl.ime.keyboard.FlickKeyCode
import ee.oyatl.ime.keyboard.KeyboardParams
import kotlin.math.min

Expand All @@ -28,6 +28,7 @@ class KeyFeedbackManager(
private var downTime: Long = 0

override fun onKeyDown(keyCode: Int, metaState: Int) {
if(keyCode >= 0 && keyCode and FlickKeyCode.FLAG_FLICK != 0) return
downTime = System.currentTimeMillis()
if(params.vibrationDuration > 0) {
vibrate(params.vibrationDuration)
Expand All @@ -44,6 +45,7 @@ class KeyFeedbackManager(
}

override fun onKeyUp(keyCode: Int, metaState: Int) {
if(keyCode >= 0 && keyCode and FlickKeyCode.FLAG_FLICK != 0) return
val diff = System.currentTimeMillis() - downTime
if(params.vibrationDuration > 0) {
val duration = params.vibrationDuration / 5f * min(diff / 100f, 1f)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ee.oyatl.ime.keyboard.listener
import android.os.Handler
import android.os.Looper
import android.view.KeyEvent
import ee.oyatl.ime.keyboard.FlickKeyCode
import ee.oyatl.ime.keyboard.listener.KeyboardListener
import ee.oyatl.ime.keyboard.KeyboardParams
import ee.oyatl.ime.keyboard.KeyboardState
Expand Down Expand Up @@ -40,14 +41,14 @@ class ShiftStateManager(
}

override fun onKeyDown(keyCode: Int, metaState: Int) {
when(keyCode) {
when(keyCode and FlickKeyCode.MASK_KEYCODE) {
KeyEvent.KEYCODE_DEL -> onDeletePressed(keyCode)
KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_SHIFT_RIGHT -> onShiftPressed(keyCode)
}
}

override fun onKeyUp(keyCode: Int, metaState: Int) {
when(keyCode) {
when(keyCode and FlickKeyCode.MASK_KEYCODE) {
KeyEvent.KEYCODE_DEL -> onDeleteReleased(keyCode)
KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_SHIFT_RIGHT -> onShiftReleased(keyCode)
else -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package ee.oyatl.ime.keyboard.touchhandler

import ee.oyatl.ime.keyboard.FlickKeyCode
import kotlin.math.PI

enum class FlickDirection(
val angle: Double,
val diagonal: Boolean,
val keyCodeFlag: Int
) {
Up(0.5 * PI, false, FlickKeyCode.DIRECTION_UP),
Down(1.5 * PI, false, FlickKeyCode.DIRECTION_DOWN),
Left(0.0 * PI, false, FlickKeyCode.DIRECTION_LEFT),
Right(1.0 * PI, false, FlickKeyCode.DIRECTION_RIGHT),
UpLeft(0.25 * PI, true, FlickKeyCode.DIRECTION_UP_LEFT),
UpRight(0.75 * PI, true, FlickKeyCode.DIRECTION_UP_RIGHT),
DownLeft(1.75 * PI, true, FlickKeyCode.DIRECTION_DOWN_LEFT),
DownRight(1.25 * PI, true, FlickKeyCode.DIRECTION_DOWN_RIGHT);

fun contains(angle: Double, range: Double): Boolean {
val start = this.angle - range / 2
val end = this.angle + range / 2
val range = start .. end
val range1 = start + 2 * PI .. 2 * PI
val range2 = 0.0 .. end - 2 * PI
return angle in range || (start < -0.0 && angle in range1) || (end > 2 * PI && angle in range2)
}

companion object {
fun valueOfKeycode(keyCode: Int): FlickDirection? {
val direction = keyCode and FlickKeyCode.MASK_DIRECTION
return entries.find { it.keyCodeFlag == direction }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package ee.oyatl.ime.keyboard.touchhandler

import ee.oyatl.ime.keyboard.FlickKeyCode
import ee.oyatl.ime.keyboard.popup.Popup
import kotlin.math.PI
import kotlin.math.atan2
import kotlin.math.pow
import kotlin.math.sqrt

class FlickTouchHandler(
override val keyboardView: TouchHandler.KeyboardViewInterface,
val threshold: Int,
val diagonal: Boolean = false,
val multiFlick: Boolean = false
): TouchHandler {
val pointers = mutableMapOf<Int, Pointer>()

override fun onReset() {
pointers.values.forEach { it.key?.onReleased() }
pointers.clear()
}

override fun onTouchDown(pointerId: Int, x: Int, y: Int) {
val key = keyboardView.findKey(x, y)
val popup = key?.let { keyboardView.popupManager.createPreviewPopup(key) }
val pointer = Pointer(pointerId, x, y, x, y, key, popup)
if(key != null) {
key.onPressed()
keyboardView.listener.onKeyDown(key.keyCode, 0)
}
popup?.show()
pointers += pointerId to pointer
}

override fun onTouchMove(pointerId: Int, x: Int, y: Int) {
val pointer = pointers[pointerId] ?: return
val diffX = (x - pointer.downX).toFloat()
val diffY = (y - pointer.downY).toFloat()
val dist = sqrt(diffX.pow(2) + diffY.pow(2))
if(dist > threshold) {
val angle = atan2(diffY, diffX) + PI
val directions = FlickDirection.entries.filter { !it.diagonal or this.diagonal }
val range = if(this.diagonal) 0.25 else 0.5
val direction = directions.firstOrNull {
it.contains(angle, range * PI)
}
val lastDirection = pointer.flicks.lastOrNull()
if(direction != null) {
val flicks = pointer.flicks.toMutableList()
if(direction != lastDirection && (multiFlick || flicks.isEmpty())) {
if(pointer.key != null && pointer.key.keyCode >= 0) {
val keyCode = FlickKeyCode.FLAG_FLICK or direction.keyCodeFlag or pointer.key.keyCode
keyboardView.listener.onKeyDown(keyCode, 0)
keyboardView.listener.onKeyUp(keyCode, 0)
}
flicks += direction
}
pointers += pointerId to pointer.copy(downX = x, downY = y, flicks = flicks.toList())
}
} else {
pointers += pointerId to pointer.copy(x = x, y = y)
}
}

override fun onTouchUp(pointerId: Int, x: Int, y: Int) {
val pointer = pointers[pointerId] ?: return
val key = pointer.key
if(key != null) {
key.onReleased()
if(pointer.flicks.isEmpty()) keyboardView.listener.onKeyUp(key.keyCode, 0)
}
pointer.popup?.hide()
pointers -= pointerId
}

data class Pointer(
val id: Int,
val downX: Int,
val downY: Int,
val x: Int,
val y: Int,
val key: TouchHandler.KeyInterface?,
val popup: Popup?,
val flicks: List<FlickDirection> = listOf()
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package ee.oyatl.ime.keyboard.touchhandler

import android.graphics.Rect
import ee.oyatl.ime.keyboard.listener.KeyboardListener
import ee.oyatl.ime.keyboard.popup.DefaultPopupManager
import ee.oyatl.ime.keyboard.popup.PopupManager

interface TouchHandler {
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/ee/oyatl/ime/fusion/Feature.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ enum class Feature(
MozcCandidateHeight(LocalDate.of(2025, 9, 25)),
NumberRow(LocalDate.of(2026, 3, 15)),
SplitKeyboard(LocalDate.of(2026, 3, 15)),
TouchMode(LocalDate.of(2026, 4, 16)),
;

val availableInPaidVersion: Boolean get() =
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/ee/oyatl/ime/fusion/FlickAction.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ee.oyatl.ime.fusion

enum class FlickAction {
None, Default, Shifted, Symbol, ShiftedSymbol
}
5 changes: 4 additions & 1 deletion app/src/main/java/ee/oyatl/ime/fusion/SettingsActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,10 @@ class SettingsActivity : AppCompatActivity(),

class BehaviourFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.pref_behaviour, rootKey)
addPreferencesFromResource(R.xml.pref_behaviour_feedback)
if(Feature.TouchMode.availableInCurrentVersion)
addPreferencesFromResource(R.xml.pref_behaviour_touch)
addPreferencesFromResource(R.xml.pref_behaviour_hardware)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ abstract class CangjieIMEMode(
private var bestCandidate: CangjieCandidate? = null

override suspend fun onLoad(context: Context) {
super.onLoad(context)
val table = TableLoader()
table.setPath(context.filesDir.absolutePath.encodeToByteArray())
table.initialize()
Expand Down
85 changes: 64 additions & 21 deletions app/src/main/java/ee/oyatl/ime/fusion/mode/CommonIMEMode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,9 @@ import androidx.preference.PreferenceManager
import ee.oyatl.ime.candidate.CandidateView
import ee.oyatl.ime.candidate.ScrollingCandidateView
import ee.oyatl.ime.fusion.Feature
import ee.oyatl.ime.fusion.FlickAction
import ee.oyatl.ime.fusion.KeyEventUtil
import ee.oyatl.ime.fusion.R
import ee.oyatl.ime.keyboard.listener.CompoundKeyboardListener
import ee.oyatl.ime.keyboard.DefaultKeyboardView
import ee.oyatl.ime.keyboard.listener.KeyFeedbackManager
import ee.oyatl.ime.keyboard.KeyboardConfiguration
import ee.oyatl.ime.keyboard.listener.KeyboardListener
import ee.oyatl.ime.keyboard.KeyboardParams
import ee.oyatl.ime.keyboard.KeyboardState
import ee.oyatl.ime.keyboard.KeyboardTemplate
import ee.oyatl.ime.keyboard.KeyboardView
import ee.oyatl.ime.keyboard.LayoutTable
import ee.oyatl.ime.keyboard.listener.ShiftStateManager
import ee.oyatl.ime.keyboard.SwitcherKeyboardView
import ee.oyatl.ime.fusion.layout.LayoutExt
import ee.oyatl.ime.fusion.layout.LayoutQwerty
import ee.oyatl.ime.fusion.layout.LayoutSymbol
Expand All @@ -35,8 +24,24 @@ import ee.oyatl.ime.fusion.layout.MobileKeyboardRows
import ee.oyatl.ime.fusion.layout.NumberKeyboard
import ee.oyatl.ime.fusion.layout.TabletKeyboard
import ee.oyatl.ime.fusion.layout.TabletKeyboardRows
import ee.oyatl.ime.keyboard.DefaultKeyboardView
import ee.oyatl.ime.keyboard.FlickKeyCode
import ee.oyatl.ime.keyboard.KeyboardConfiguration
import ee.oyatl.ime.keyboard.KeyboardParams
import ee.oyatl.ime.keyboard.KeyboardState
import ee.oyatl.ime.keyboard.KeyboardTemplate
import ee.oyatl.ime.keyboard.KeyboardView
import ee.oyatl.ime.keyboard.LayoutTable
import ee.oyatl.ime.keyboard.SwitcherKeyboardView
import ee.oyatl.ime.keyboard.listener.CompoundKeyboardListener
import ee.oyatl.ime.keyboard.listener.KeyFeedbackManager
import ee.oyatl.ime.keyboard.listener.KeyboardListener
import ee.oyatl.ime.keyboard.listener.ShiftStateManager
import ee.oyatl.ime.keyboard.popup.DefaultPopupManager
import ee.oyatl.ime.keyboard.touchhandler.FlickDirection
import ee.oyatl.ime.keyboard.touchhandler.FlickTouchHandler
import ee.oyatl.ime.keyboard.touchhandler.SeekTouchHandler
import ee.oyatl.ime.keyboard.touchhandler.TouchHandler
import kotlin.math.roundToInt

abstract class CommonIMEMode(
Expand Down Expand Up @@ -103,6 +108,8 @@ abstract class CommonIMEMode(
var symbolState: KeyboardState.Symbol = KeyboardState.Symbol.Text
var shiftState: KeyboardState.Shift = KeyboardState.Shift.Released

private var defaultFlickActions: Map<FlickDirection, FlickAction> = mapOf()

protected var util: KeyEventUtil? = null
private set
protected var passwordField: Boolean = false
Expand All @@ -117,7 +124,16 @@ abstract class CommonIMEMode(
util?.sendDownUpKeyEvents(keyCode)
}

override suspend fun onLoad(context: Context) = Unit
override suspend fun onLoad(context: Context) {
val preference = PreferenceManager.getDefaultSharedPreferences(context)

val flickActionUp = FlickAction.valueOf(preference.getString("default_flick_action_up", null) ?: FlickAction.Shifted.name)
val flickActionDown = FlickAction.valueOf(preference.getString("default_flick_action_down", null) ?: FlickAction.Symbol.name)
defaultFlickActions = mapOf(
FlickDirection.Up to flickActionUp,
FlickDirection.Down to flickActionDown
)
}

override fun onStart(inputConnection: InputConnection, editorInfo: EditorInfo) {
util = KeyEventUtil(inputConnection, editorInfo)
Expand Down Expand Up @@ -208,19 +224,19 @@ abstract class CommonIMEMode(
val textKeyboardView = DefaultKeyboardView(context, null).also {
it.keyboard = textKeyboard
it.listener = createKeyboardListener(context, textKeyboardParams)
it.touchHandler = SeekTouchHandler(it)
it.touchHandler = createTouchHandler(it, context)
if(params.previewPopups) it.popupManager = DefaultPopupManager(it, it)
}
val symbolKeyboardView = DefaultKeyboardView(context, null).also {
it.keyboard = symbolKeyboard
it.listener = createKeyboardListener(context, symbolKeyboardParams)
it.touchHandler = SeekTouchHandler(it)
it.touchHandler = createTouchHandler(it, context)
if(params.previewPopups) it.popupManager = DefaultPopupManager(it, it)
}
val numberKeyboardView = DefaultKeyboardView(context, null).also {
it.keyboard = numberKeyboard
it.listener = createKeyboardListener(context, numberKeyboardParams)
it.touchHandler = SeekTouchHandler(it)
it.touchHandler = createTouchHandler(it, context)
if(params.previewPopups) it.popupManager = DefaultPopupManager(it, it)
}

Expand Down Expand Up @@ -269,13 +285,25 @@ abstract class CommonIMEMode(
}
}

private fun createKeyboardListener(context: Context, params: KeyboardParams): KeyboardListener {
open fun createKeyboardListener(context: Context, params: KeyboardParams): KeyboardListener {
return CompoundKeyboardListener(
ShiftStateManager(this, params),
KeyFeedbackManager(context, params)
)
}

open fun createTouchHandler(keyboardView: TouchHandler.KeyboardViewInterface, context: Context): TouchHandler {
val preference = PreferenceManager.getDefaultSharedPreferences(context)
val touchMode = preference.getString("touch_mode", "seek")
if(touchMode == "flick" && Feature.TouchMode.availableInCurrentVersion) {
val defaultValue = context.resources.getInteger(R.integer.flick_sensitivity_default).toFloat()
val flickSensitivity = preference.getFloat("flick_sensitivity", defaultValue).toInt()
return FlickTouchHandler(keyboardView, flickSensitivity, diagonal = false, multiFlick = false)
} else {
return SeekTouchHandler(keyboardView)
}
}

protected fun setPreferredKeyboard(editorInfo: EditorInfo) {
when(editorInfo.inputType and EditorInfo.TYPE_MASK_CLASS) {
EditorInfo.TYPE_CLASS_NUMBER -> {
Expand Down Expand Up @@ -313,10 +341,25 @@ abstract class CommonIMEMode(
} else if(keyCode < 0) {
onChar(-keyCode)
} else if(keyCode > KeyEvent.getMaxKeyCode() || keyCharacterMap.isPrintingKey(keyCode)) {
onChar(
currentLayoutTable[keyCode]?.forShiftState(shiftState)
?: keyCharacterMap.get(keyCode, metaState)
)
val maskedKeyCode = keyCode and FlickKeyCode.MASK_KEYCODE
val default = keyCharacterMap.get(maskedKeyCode, metaState)
if(keyCode and FlickKeyCode.FLAG_FLICK != 0) {
val direction = FlickDirection.valueOfKeycode(keyCode)
when(defaultFlickActions[direction]) {
FlickAction.Default -> onChar(
currentLayoutTable[maskedKeyCode]?.forShiftState(shiftState) ?: default)
FlickAction.Shifted -> onChar(
currentLayoutTable[maskedKeyCode]?.forShiftState(KeyboardState.Shift.Pressed) ?: default)
FlickAction.Symbol -> onChar(
symbolLayoutTable[maskedKeyCode]?.forShiftState(shiftState) ?: default)
FlickAction.ShiftedSymbol -> onChar(
symbolLayoutTable[maskedKeyCode]?.forShiftState(KeyboardState.Shift.Pressed) ?: default)
else -> Unit
}
} else {
onChar(
currentLayoutTable[keyCode]?.forShiftState(shiftState) ?: default)
}
} else {
handleSpecialKey(keyCode)
}
Expand Down
Loading