Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c3dd42354 | ||
|
|
55a7bd2643 | ||
|
|
292788aafa | ||
|
|
afbc926cde | ||
|
|
23dc605bf0 | ||
|
|
149373226a | ||
|
|
939e8da252 |
@@ -1,3 +1,6 @@
|
||||
[submodule "vendor/cdict"]
|
||||
path = vendor/cdict
|
||||
url = https://github.com/Julow/cdict
|
||||
[submodule "vendor/libswipetype"]
|
||||
path = vendor/libswipetype
|
||||
url = http://10.0.1.49:3010/harley/libswipetype.git
|
||||
|
||||
Binary file not shown.
+6
-1
@@ -3,12 +3,16 @@ import java.io.FileOutputStream
|
||||
|
||||
plugins {
|
||||
id("com.android.application") version "8.13.2"
|
||||
kotlin("android") version "2.1.20"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Following versions of androidx.window require sdk version 23
|
||||
implementation("androidx.window:window-java:1.4.0")
|
||||
implementation("androidx.core:core:1.16.0") // Version 1.17.0 available with sdk 36
|
||||
implementation(project(":swipetype-android"))
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib:2.1.20")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
}
|
||||
|
||||
@@ -27,7 +31,7 @@ android {
|
||||
sourceSets {
|
||||
named("main") {
|
||||
manifest.srcFile("AndroidManifest.xml")
|
||||
java.srcDirs("srcs/juloo.keyboard2", "vendor/cdict/java/juloo.cdict")
|
||||
java.srcDirs("srcs/juloo.keyboard2", "srcs/gestures", "vendor/cdict/java/juloo.cdict")
|
||||
res.srcDirs("res", "build/generated-resources")
|
||||
assets.srcDirs("assets")
|
||||
}
|
||||
@@ -93,6 +97,7 @@ android {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions.jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<row height="0.95">
|
||||
<key width="1.5" role="action" key0="ctrl" key1="loc meta" key6="loc switch_clipboard"/>
|
||||
<key width="1.3" role="action" key0="alt"/>
|
||||
<key width="6.0" role="space_bar" key0="space" key7="switch_forward" key8="switch_backward" key5="cursor_left" key6="cursor_right"/>
|
||||
<key width="1.4" role="action" key0="fn" key1="config" key2="tux" key6="loc voice_typing"/>
|
||||
<key width="1.6" role="action" key0="enter" key2="action"/>
|
||||
<key width="1.5" role="action" key0="ctrl" key1="loc meta" key3="loc switch_clipboard"/>
|
||||
<key width="1.3" role="action" key0="alt" key1="config"/>
|
||||
<key width="1.0" role="action" key7="pgup" key8="pgdn"/>
|
||||
<key width="5.5" role="space_bar" key0="space" key7="switch_forward" key8="switch_backward" key5="cursor_left" key6="cursor_right"/>
|
||||
<key width="1.4" role="action" key0="fn" key2="meta" key3="loc voice_typing"/>
|
||||
<key width="1.5" role="action" key0="enter" key2="action"/>
|
||||
</row>
|
||||
|
||||
@@ -15,3 +15,6 @@ dependencyResolutionManagement {
|
||||
}
|
||||
|
||||
rootProject.name = "Unexpected-Keyboard"
|
||||
|
||||
include(":swipetype-android")
|
||||
project(":swipetype-android").projectDir = File("vendor/libswipetype/swipetype-android")
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright (C) 2025 The FlorisBoard Contributors
|
||||
* Licensed under the Apache License, Version 2.0
|
||||
* Adapted for Morn's Keyboard (GPL-3.0)
|
||||
*
|
||||
* Gesture recognition using statistical shape + location matching.
|
||||
* Based on Étienne Desticourt's algorithm (AnySoftKeyboard PR #1870).
|
||||
* https://github.com/AnySoftKeyboard/AnySoftKeyboard/pull/1870
|
||||
*/
|
||||
|
||||
package juloo.keyboard2
|
||||
|
||||
import kotlin.math.*
|
||||
|
||||
/**
|
||||
* Gesture recognition engine for swipe typing.
|
||||
* Takes a sequence of touch points, generates ideal paths for dictionary words,
|
||||
* and scores them using combined shape + location Gaussian probability.
|
||||
*/
|
||||
class MornsGestureEngine(
|
||||
private val keysByCodePoint: Map<Int, KeyInfo>,
|
||||
private val words: List<String>
|
||||
) {
|
||||
companion object {
|
||||
const val SAMPLING_POINTS = 200
|
||||
private const val SHAPE_STD = 22.08f
|
||||
private const val LOCATION_STD = 0.5109f
|
||||
private const val PRUNING_LENGTH_THRESHOLD = 8.42
|
||||
private const val MAX_GESTURE_SIZE = 500
|
||||
private const val MAX_CANDIDATES = 8
|
||||
}
|
||||
|
||||
data class KeyInfo(
|
||||
val codePoint: Int,
|
||||
val centerX: Float,
|
||||
val centerY: Float,
|
||||
val width: Float,
|
||||
val height: Float
|
||||
)
|
||||
|
||||
/** Accumulated gesture touch points. */
|
||||
class Gesture {
|
||||
private val xs = FloatArray(MAX_GESTURE_SIZE)
|
||||
private val ys = FloatArray(MAX_GESTURE_SIZE)
|
||||
private var size = 0
|
||||
val isEmpty: Boolean get() = size == 0
|
||||
fun addPoint(x: Float, y: Float) {
|
||||
if (size < MAX_GESTURE_SIZE) { xs[size] = x; ys[size] = y; size++ }
|
||||
}
|
||||
fun getX(i: Int): Float = if (i < size) xs[i] else 0f
|
||||
fun getY(i: Int): Float = if (i < size) ys[i] else 0f
|
||||
val firstX: Float get() = getX(0)
|
||||
val firstY: Float get() = getY(0)
|
||||
val lastX: Float get() = getX(size - 1)
|
||||
val lastY: Float get() = getY(size - 1)
|
||||
fun clear() { size = 0 }
|
||||
|
||||
private fun d(x1: Float, y1: Float, x2: Float, y2: Float): Float =
|
||||
sqrt((x1 - x2).pow(2f) + (y1 - y2).pow(2f))
|
||||
|
||||
val length: Float get() {
|
||||
var len = 0f
|
||||
for (i in 1 until size) len += d(xs[i-1], ys[i-1], xs[i], ys[i])
|
||||
return len
|
||||
}
|
||||
|
||||
fun resample(numPoints: Int): Gesture {
|
||||
val interval = length / numPoints
|
||||
val out = Gesture(); out.addPoint(xs[0], ys[0])
|
||||
var lastX = xs[0]; var lastY = ys[0]
|
||||
var cumErr = 0f
|
||||
if (size == 1) { repeat(numPoints) { out.addPoint(xs[0], ys[0]) }; return out }
|
||||
for (i in 0 until size - 1) {
|
||||
var dx = xs[i+1] - xs[i]; var dy = ys[i+1] - ys[i]
|
||||
val norm = sqrt(dx.pow(2f) + dy.pow(2f))
|
||||
if (norm < 0.001f) continue
|
||||
dx /= norm; dy /= norm
|
||||
var n = norm / interval
|
||||
cumErr += n - n.toInt()
|
||||
if (cumErr > 1) { n = n.toInt() + cumErr.toInt().toFloat(); cumErr %= 1f }
|
||||
repeat(n.toInt()) {
|
||||
lastX += dx * interval; lastY += dy * interval
|
||||
out.addPoint(lastX, lastY)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** Normalize to centroid-centered bounding box, preserving aspect ratio. */
|
||||
fun normalizeByBoxSide(): Gesture {
|
||||
val out = Gesture()
|
||||
var maxX = -1e10f; var maxY = -1e10f
|
||||
var minX = 1e10f; var minY = 1e10f
|
||||
for (i in 0 until size) { maxX = max(xs[i], maxX); maxY = max(ys[i], maxY); minX = min(xs[i], minX); minY = min(ys[i], minY) }
|
||||
val w = maxX - minX; val h = maxY - minY
|
||||
val side = max(max(w, h), 0.00001f)
|
||||
val cx = (w / 2f + minX) / side
|
||||
val cy = (h / 2f + minY) / side
|
||||
for (i in 0 until size) { out.addPoint(xs[i] / side - cx, ys[i] / side - cy) }
|
||||
return out
|
||||
}
|
||||
|
||||
fun clone(): Gesture { val g = Gesture(); xs.copyInto(g.xs); ys.copyInto(g.ys); g.size = size; return g }
|
||||
}
|
||||
|
||||
/** Generate ideal gesture path for a word by connecting key centers. */
|
||||
private fun generateIdealGesture(word: String): Gesture {
|
||||
val g = Gesture()
|
||||
for (c in word) {
|
||||
val key = keysByCodePoint[Character.toLowerCase(c).code] ?: continue
|
||||
g.addPoint(key.centerX, key.centerY)
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
/** Find the n closest keys to a point. Returns their code points. */
|
||||
private fun findNClosestKeys(x: Float, y: Float, n: Int): List<Int> {
|
||||
return keysByCodePoint.entries
|
||||
.map { (cp, k) -> cp to sqrt((x - k.centerX).pow(2f) + (y - k.centerY).pow(2f)) }
|
||||
.sortedBy { it.second }
|
||||
.take(n)
|
||||
.map { it.first }
|
||||
}
|
||||
|
||||
/** Score a gesture against a word. Lower is better. */
|
||||
private fun scoreGesture(userGesture: Gesture, wordGesture: Gesture): Float {
|
||||
val resampledUser = userGesture.resample(SAMPLING_POINTS)
|
||||
val resampledWord = wordGesture.resample(SAMPLING_POINTS)
|
||||
// Shape distance (normalized)
|
||||
val normUser = resampledUser.normalizeByBoxSide()
|
||||
val normWord = resampledWord.normalizeByBoxSide()
|
||||
var shapeDist = 0f
|
||||
for (i in 0 until SAMPLING_POINTS) {
|
||||
shapeDist += distance(normWord.getX(i), normWord.getY(i), normUser.getX(i), normUser.getY(i))
|
||||
}
|
||||
// Location distance (unnormalized)
|
||||
var locDist = 0f
|
||||
for (i in 0 until SAMPLING_POINTS) {
|
||||
locDist += abs(resampledWord.getX(i) - resampledUser.getX(i)) + abs(resampledWord.getY(i) - resampledUser.getY(i))
|
||||
}
|
||||
locDist /= SAMPLING_POINTS * 2f
|
||||
// Combined Gaussian probability
|
||||
val shapeProb = gaussianProb(shapeDist, 0f, SHAPE_STD)
|
||||
val locProb = gaussianProb(locDist, 0f, LOCATION_STD * 50f)
|
||||
return 1f / (shapeProb * locProb + 1e-10f)
|
||||
}
|
||||
|
||||
private fun distance(x1: Float, y1: Float, x2: Float, y2: Float): Float =
|
||||
sqrt((x1 - x2).pow(2f) + (y1 - y2).pow(2f))
|
||||
|
||||
private fun gaussianProb(value: Float, mean: Float, std: Float): Float {
|
||||
val factor = 1f / (std * sqrt(2f * PI.toFloat()))
|
||||
val exponent = ((value - mean) / std).toDouble().pow(2.0)
|
||||
return (factor * exp(-0.5 * exponent)).toFloat()
|
||||
}
|
||||
|
||||
/**
|
||||
* Recognize the gesture and return candidate words sorted by confidence.
|
||||
*/
|
||||
fun recognize(points: List<MornsGlideAdapter.Pair>): List<String> {
|
||||
val gesture = Gesture()
|
||||
points.forEach { p -> gesture.addPoint(p.first, p.second) }
|
||||
if (gesture.isEmpty) return emptyList()
|
||||
|
||||
val startKeys = findNClosestKeys(gesture.firstX, gesture.firstY, 2)
|
||||
val endKeys = findNClosestKeys(gesture.lastX, gesture.lastY, 2)
|
||||
|
||||
// Filter words by start/end letters
|
||||
val candidateWords = mutableListOf<String>()
|
||||
for (w in words) {
|
||||
if (w.isEmpty()) continue
|
||||
val first = Character.toLowerCase(w[0])
|
||||
val last = Character.toLowerCase(w[w.length - 1])
|
||||
if (first.code in startKeys && last.code in endKeys) {
|
||||
candidateWords.add(w)
|
||||
}
|
||||
}
|
||||
|
||||
// Score candidates
|
||||
val scored = mutableListOf<Pair<String, Float>>()
|
||||
val userGesture = gesture.resample(SAMPLING_POINTS)
|
||||
|
||||
for (word in candidateWords) {
|
||||
val ideal = generateIdealGesture(word)
|
||||
if (ideal.isEmpty) continue
|
||||
val score = scoreGesture(userGesture, ideal)
|
||||
scored.add(word to score)
|
||||
}
|
||||
|
||||
scored.sortBy { it.second }
|
||||
return scored.take(MAX_CANDIDATES).map { it.first }
|
||||
}
|
||||
|
||||
fun clear() {}
|
||||
}
|
||||
@@ -107,7 +107,7 @@ public final class Config
|
||||
marginTop = res.getDimension(R.dimen.margin_top);
|
||||
keyPadding = res.getDimension(R.dimen.key_padding);
|
||||
labelTextSize = 0.33f;
|
||||
sublabelTextSize = 0.22f;
|
||||
sublabelTextSize = 0.21f;
|
||||
// from prefs
|
||||
refresh(res, foldableUnfolded, dicts);
|
||||
// initialized later
|
||||
@@ -151,15 +151,15 @@ public final class Config
|
||||
// The option value uses an unnamed scale where the baseline is around 25.
|
||||
float dpi_ratio = Math.max(dm.xdpi, dm.ydpi) / Math.min(dm.xdpi, dm.ydpi);
|
||||
float swipe_scaling = Math.min(dm.widthPixels, dm.heightPixels) / 10.f * dpi_ratio;
|
||||
float swipe_dist_value = Float.valueOf(_prefs.getString("swipe_dist", "15"));
|
||||
float swipe_dist_value = Float.valueOf(_prefs.getString("swipe_dist", "25"));
|
||||
swipe_dist_px = swipe_dist_value / 25.f * swipe_scaling;
|
||||
float slider_sensitivity = Float.valueOf(_prefs.getString("slider_sensitivity", "30")) / 100.f;
|
||||
slide_step_px = slider_sensitivity * swipe_scaling;
|
||||
vibrate_custom = _prefs.getBoolean("vibrate_custom", false);
|
||||
vibrate_duration = _prefs.getInt("vibrate_duration", 20);
|
||||
longPressTimeout = _prefs.getInt("longpress_timeout", 700);
|
||||
longPressTimeout = _prefs.getInt("longpress_timeout", 600);
|
||||
longPressInterval = _prefs.getInt("longpress_interval", 65);
|
||||
keyrepeat_enabled = _prefs.getBoolean("keyrepeat_enabled", true);
|
||||
keyrepeat_enabled = false;
|
||||
margin_bottom = get_dip_pref_oriented(dm, "margin_bottom", 7, 3);
|
||||
key_vertical_margin = get_dip_pref(dm, "key_vertical_margin", 1.5f) / 100;
|
||||
key_horizontal_margin = get_dip_pref(dm, "key_horizontal_margin", 2) / 100;
|
||||
@@ -179,7 +179,7 @@ public final class Config
|
||||
// a layout in KeyboardData unit. The keyboard will be higher if the layout
|
||||
// has more rows and smaller if it has less because rows stay the same
|
||||
// height.
|
||||
keyboard_rows_height_pixels = screenHeightPixels * keyboardHeightPercent / 395;
|
||||
keyboard_rows_height_pixels = (int)(screenHeightPixels * keyboardHeightPercent / 395 * 0.9f);
|
||||
horizontal_margin =
|
||||
get_dip_pref_oriented(dm, "horizontal_margin", 3, 28);
|
||||
double_tap_lock_shift = _prefs.getBoolean("lock_double_tap", false);
|
||||
|
||||
@@ -589,7 +589,7 @@ public final class KeyValue implements Comparable<KeyValue>
|
||||
case "box": return modifierKey("Box", Modifier.BOX, 0);
|
||||
case "fn": return modifierKey("Fn", Modifier.FN, 0);
|
||||
case "meta": return modifierKey("Meta", Modifier.META, 0);
|
||||
case "tux": return modifierKey(String.valueOf((char)0xE002), Modifier.META, FLAG_KEY_FONT);
|
||||
case "tux": return modifierKey("Meta", Modifier.META, 0);
|
||||
|
||||
/* Combining diacritics */
|
||||
/* Glyphs is the corresponding dead-key + 0x0100. */
|
||||
@@ -672,6 +672,8 @@ public final class KeyValue implements Comparable<KeyValue>
|
||||
case "left": return keyeventKey(0xE008, KeyEvent.KEYCODE_DPAD_LEFT, FLAG_SMALLER_FONT);
|
||||
case "page_up": return keyeventKey(0xE002, KeyEvent.KEYCODE_PAGE_UP, 0);
|
||||
case "page_down": return keyeventKey(0xE003, KeyEvent.KEYCODE_PAGE_DOWN, 0);
|
||||
case "pgup": return keyeventKey("pgup", KeyEvent.KEYCODE_PAGE_UP, FLAG_SMALLER_FONT);
|
||||
case "pgdn": return keyeventKey("pgdn", KeyEvent.KEYCODE_PAGE_DOWN, FLAG_SMALLER_FONT);
|
||||
case "home": return keyeventKey(0xE00B, KeyEvent.KEYCODE_MOVE_HOME, FLAG_SMALLER_FONT);
|
||||
case "end": return keyeventKey(0xE00C, KeyEvent.KEYCODE_MOVE_END, FLAG_SMALLER_FONT);
|
||||
case "delete": return keyeventKey(0xE010, KeyEvent.KEYCODE_FORWARD_DEL, 0);
|
||||
|
||||
@@ -52,6 +52,7 @@ public class Keyboard2 extends InputMethodService
|
||||
private Handler _handler;
|
||||
|
||||
private Config _config;
|
||||
private MornsGlideAdapter _glideAdapter;
|
||||
|
||||
private FoldStateTracker _foldStateTracker;
|
||||
|
||||
@@ -133,6 +134,14 @@ public class Keyboard2 extends InputMethodService
|
||||
Config.initGlobalConfig(prefs, getResources(),
|
||||
_foldStateTracker.isUnfolded(), _dictionaries);
|
||||
_config = Config.globalConfig();
|
||||
_glideAdapter = new MornsGlideAdapter();
|
||||
_glideAdapter.init(new MornsGlideAdapter.CandidatesCallback() {
|
||||
public void onCandidates(String[] words) {
|
||||
if (words.length > 0) {
|
||||
getCurrentInputConnection().commitText(words[0], 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
Receiver recvr = this.new Receiver();
|
||||
_suggestions = new Suggestions(recvr, _config);
|
||||
_keyeventhandler = new KeyEventHandler(recvr, _suggestions);
|
||||
@@ -150,6 +159,8 @@ public class Keyboard2 extends InputMethodService
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
if (_glideAdapter != null)
|
||||
_glideAdapter.cancelGesture();
|
||||
_foldStateTracker.close();
|
||||
}
|
||||
|
||||
@@ -158,6 +169,8 @@ public class Keyboard2 extends InputMethodService
|
||||
_keyboard_container_view = (ViewGroup)inflate_view(R.layout.keyboard);
|
||||
_keyboard_layout_view = (Keyboard2View)_keyboard_container_view.findViewById(R.id.keyboard_view);
|
||||
_candidates_view = (CandidatesView)_keyboard_container_view.findViewById(R.id.candidates_view);
|
||||
if (_glideAdapter != null)
|
||||
_keyboard_layout_view.setGlideAdapter(_glideAdapter);
|
||||
}
|
||||
|
||||
InputMethodManager get_imm()
|
||||
|
||||
@@ -17,6 +17,7 @@ import android.view.Window;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
import android.view.WindowMetrics;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@@ -33,6 +34,15 @@ public class Keyboard2View extends View
|
||||
private KeyboardData.Key _compose_key;
|
||||
|
||||
private Pointers _pointers;
|
||||
private MornsGlideAdapter _glideAdapter;
|
||||
/** Glide gesture tracking. */
|
||||
private int _glideLastKey = -1;
|
||||
private int[] _glideKeys = new int[32];
|
||||
private int _glideKeyCount = 0;
|
||||
private boolean _inGlide = false;
|
||||
/** Trail points for the swipe gesture (pixels). */
|
||||
private List<float[]> _trailPoints = new ArrayList<>();
|
||||
private Paint _trailPaint;
|
||||
|
||||
private Pointers.Modifiers _mods;
|
||||
|
||||
@@ -68,6 +78,13 @@ public class Keyboard2View extends View
|
||||
_theme = new Theme(getContext(), attrs);
|
||||
_config = Config.globalConfig();
|
||||
_pointers = new Pointers(this, _config);
|
||||
_trailPaint = new Paint();
|
||||
_trailPaint.setColor(0x8044AAFF);
|
||||
_trailPaint.setStyle(Paint.Style.STROKE);
|
||||
_trailPaint.setStrokeWidth(6f);
|
||||
_trailPaint.setStrokeCap(Paint.Cap.ROUND);
|
||||
_trailPaint.setStrokeJoin(Paint.Join.ROUND);
|
||||
_trailPaint.setAntiAlias(true);
|
||||
refresh_navigation_bar(context);
|
||||
setOnTouchListener(this);
|
||||
int layout_id = (attrs == null) ? 0 :
|
||||
@@ -78,6 +95,10 @@ public class Keyboard2View extends View
|
||||
setKeyboard(KeyboardData.load(getResources(), layout_id));
|
||||
}
|
||||
|
||||
public void setGlideAdapter(MornsGlideAdapter a) {
|
||||
_glideAdapter = a;
|
||||
}
|
||||
|
||||
private Window getParentWindow(Context context)
|
||||
{
|
||||
if (context instanceof InputMethodService)
|
||||
@@ -197,10 +218,6 @@ public class Keyboard2View extends View
|
||||
int p;
|
||||
switch (event.getActionMasked())
|
||||
{
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_POINTER_UP:
|
||||
_pointers.onTouchUp(event.getPointerId(event.getActionIndex()));
|
||||
break;
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
case MotionEvent.ACTION_POINTER_DOWN:
|
||||
p = event.getActionIndex();
|
||||
@@ -209,12 +226,71 @@ public class Keyboard2View extends View
|
||||
KeyboardData.Key key = getKeyAtPosition(tx, ty);
|
||||
if (key != null)
|
||||
_pointers.onTouchDown(tx, ty, event.getPointerId(p), key);
|
||||
// Reset glide tracking
|
||||
_glideKeyCount = 0;
|
||||
_glideKeys[0] = -1;
|
||||
_inGlide = false;
|
||||
_glideLastKey = -1;
|
||||
_trailPoints.clear();
|
||||
if (_glideAdapter != null) _glideAdapter.cancelGesture();
|
||||
if (key != null && key.keys[0] != null
|
||||
&& key.keys[0].getKind() == KeyValue.Kind.Char)
|
||||
{
|
||||
_glideLastKey = key.keys[0].getChar();
|
||||
_trailPoints.add(new float[]{tx, ty});
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
for (p = 0; p < event.getPointerCount(); p++)
|
||||
_pointers.onTouchMove(event.getX(p), event.getY(p), event.getPointerId(p));
|
||||
{
|
||||
float mx = event.getX(p);
|
||||
float my = event.getY(p);
|
||||
_pointers.onTouchMove(mx, my, event.getPointerId(p));
|
||||
// Collect trail + detect glide in one pass
|
||||
if (_glideAdapter != null)
|
||||
{
|
||||
KeyboardData.Key k = getKeyAtPosition(mx, my);
|
||||
_trailPoints.add(new float[]{mx, my});
|
||||
invalidate();
|
||||
if (k != null && k.keys[0] != null
|
||||
&& k.keys[0].getKind() == KeyValue.Kind.Char)
|
||||
{
|
||||
int cp = k.keys[0].getChar();
|
||||
if (cp != _glideLastKey)
|
||||
{
|
||||
_glideLastKey = cp;
|
||||
if (!_inGlide)
|
||||
{
|
||||
_inGlide = true;
|
||||
_glideAdapter.startGesture();
|
||||
_pointers.cancelPointer(event.getPointerId(p));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Feed points during glide (convert to dp for the engine)
|
||||
if (_inGlide && _glideAdapter != null)
|
||||
{
|
||||
float density = getResources().getDisplayMetrics().density;
|
||||
_glideAdapter.addPoint(mx / density, my / density);
|
||||
_trailPoints.add(new float[]{mx, my});
|
||||
invalidate(); // trigger redraw to show trail
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_POINTER_UP:
|
||||
if (_inGlide && _glideAdapter != null)
|
||||
_glideAdapter.finishGesture();
|
||||
_inGlide = false;
|
||||
_trailPoints.clear();
|
||||
invalidate();
|
||||
_pointers.onTouchUp(event.getPointerId(event.getActionIndex()));
|
||||
break;
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
_inGlide = false;
|
||||
_trailPoints.clear();
|
||||
invalidate();
|
||||
_pointers.onTouchCancel();
|
||||
break;
|
||||
default:
|
||||
@@ -285,6 +361,57 @@ public class Keyboard2View extends View
|
||||
(int)(_tc.row_height * _keyboard.keysHeight
|
||||
+ _config.marginTop + _marginBottom);
|
||||
setMeasuredDimension(width, height);
|
||||
// Pass real key positions to the glide adapter
|
||||
if (_glideAdapter != null && _keyboard != null)
|
||||
{
|
||||
float density = getResources().getDisplayMetrics().density;
|
||||
List<MornsGlideAdapter.KeyInfo> keys = new ArrayList<>();
|
||||
float yy = _tc.margin_top / density;
|
||||
for (KeyboardData.Row row : _keyboard.rows)
|
||||
{
|
||||
yy += row.shift * _tc.row_height / density;
|
||||
float xx = _marginLeft / density;
|
||||
float keyH = (row.height * _tc.row_height - _tc.vertical_margin) / density;
|
||||
for (KeyboardData.Key k : row.keys)
|
||||
{
|
||||
xx += k.shift * _keyWidth / density;
|
||||
float keyW = (_keyWidth * k.width - _tc.horizontal_margin) / density;
|
||||
if (k.keys != null && k.keys.length > 0 && k.keys[0] != null
|
||||
&& k.keys[0].getKind() == KeyValue.Kind.Char)
|
||||
{
|
||||
char c = k.keys[0].getChar();
|
||||
MornsGlideAdapter.KeyInfo ki = new MornsGlideAdapter.KeyInfo();
|
||||
ki.codePoint = (int)c;
|
||||
ki.centerX = xx + keyW/2;
|
||||
ki.centerY = yy + keyH/2;
|
||||
ki.width = keyW;
|
||||
ki.height = keyH;
|
||||
keys.add(ki);
|
||||
}
|
||||
xx += (k.width * _keyWidth) / density;
|
||||
}
|
||||
yy += row.height * _tc.row_height / density;
|
||||
}
|
||||
_glideAdapter.setKeys(keys, loadGestureWords());
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> loadGestureWords()
|
||||
{
|
||||
List<String> words = new ArrayList<>();
|
||||
try (java.io.BufferedReader br = new java.io.BufferedReader(
|
||||
new java.io.InputStreamReader(
|
||||
getResources().openRawResource(R.raw.gesture_words))))
|
||||
{
|
||||
String line;
|
||||
while ((line = br.readLine()) != null)
|
||||
{
|
||||
line = line.trim();
|
||||
if (line.length() >= 2) words.add(line);
|
||||
}
|
||||
}
|
||||
catch (Exception e) { /* ignore */ }
|
||||
return words;
|
||||
}
|
||||
|
||||
Rect _cached_exclusion_rect = new Rect();
|
||||
@@ -376,6 +503,15 @@ public class Keyboard2View extends View
|
||||
}
|
||||
y += row.height * _tc.row_height;
|
||||
}
|
||||
// Draw swipe gesture trail
|
||||
if (_trailPoints != null && _trailPoints.size() >= 2)
|
||||
{
|
||||
android.graphics.Path trail = new android.graphics.Path();
|
||||
trail.moveTo(_trailPoints.get(0)[0], _trailPoints.get(0)[1]);
|
||||
for (int i = 1; i < _trailPoints.size(); i++)
|
||||
trail.lineTo(_trailPoints.get(i)[0], _trailPoints.get(i)[1]);
|
||||
canvas.drawPath(trail, _trailPaint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package juloo.keyboard2;
|
||||
|
||||
import android.util.Log;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Glide gesture adapter using MornsGestureEngine (FlorisBoard-derived algorithm).
|
||||
* Replaces the libswipetype-based SwipeTypeEngine.
|
||||
*/
|
||||
public class MornsGlideAdapter
|
||||
{
|
||||
private static final String TAG = "MornsGlide";
|
||||
|
||||
private MornsGestureEngine _engine;
|
||||
private boolean _ready = false;
|
||||
|
||||
/** Touch points accumulated during the current gesture (dp). */
|
||||
private List<Pair> _points = new ArrayList<>();
|
||||
private boolean _active = false;
|
||||
|
||||
/** Callback for delivering candidates. */
|
||||
private CandidatesCallback _callback;
|
||||
|
||||
private List<Integer> _allWordCps = new ArrayList<>();
|
||||
|
||||
public interface CandidatesCallback
|
||||
{
|
||||
void onCandidates(String[] words);
|
||||
}
|
||||
|
||||
/** Key position info passed from the view. */
|
||||
public static class KeyInfo
|
||||
{
|
||||
public int codePoint;
|
||||
public float centerX;
|
||||
public float centerY;
|
||||
public float width;
|
||||
public float height;
|
||||
}
|
||||
|
||||
public MornsGlideAdapter() {}
|
||||
|
||||
public void init(CandidatesCallback cb)
|
||||
{
|
||||
_callback = cb;
|
||||
}
|
||||
|
||||
/** Called by the view when keyboard layout is known. */
|
||||
public void setKeys(List<KeyInfo> keys, List<String> words)
|
||||
{
|
||||
Map<Integer, MornsGestureEngine.KeyInfo> map = new HashMap<>();
|
||||
List<Integer> allCps = new ArrayList<>();
|
||||
for (KeyInfo k : keys)
|
||||
{
|
||||
map.put(k.codePoint, new MornsGestureEngine.KeyInfo(
|
||||
k.codePoint, k.centerX, k.centerY, k.width, k.height));
|
||||
allCps.add(k.codePoint);
|
||||
}
|
||||
_allWordCps = allCps;
|
||||
_engine = new MornsGestureEngine(map, words);
|
||||
_ready = true;
|
||||
Log.i(TAG, "Engine ready: " + keys.size() + " keys, " + words.size() + " words");
|
||||
}
|
||||
|
||||
public void startGesture() { _points.clear(); _active = true; }
|
||||
|
||||
public void addPoint(float xDp, float yDp)
|
||||
{
|
||||
if (_active) _points.add(new Pair(xDp, yDp));
|
||||
}
|
||||
|
||||
public void finishGesture()
|
||||
{
|
||||
_active = false;
|
||||
if (!_ready || _points.size() < 3) { _points.clear(); return; }
|
||||
// Convert points to list of float pairs
|
||||
List<Pair> pts = new ArrayList<>(_points);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> results = (List<String>)_engine.recognize(pts);
|
||||
_points.clear();
|
||||
if (results == null || results.size() == 0) return;
|
||||
String[] words = new String[results.size()];
|
||||
for (int i = 0; i < results.size(); i++)
|
||||
words[i] = results.get(i);
|
||||
if (_callback != null) _callback.onCandidates(words);
|
||||
}
|
||||
|
||||
public void cancelGesture() { _active = false; _points.clear(); }
|
||||
|
||||
/** Simple pair class for Kotlin <-> Java interop. */
|
||||
public static class Pair
|
||||
{
|
||||
public float first;
|
||||
public float second;
|
||||
public Pair(float f, float s) { first = f; second = s; }
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ public final class Pointers implements Handler.Callback
|
||||
private ArrayList<Pointer> _ptrs = new ArrayList<Pointer>();
|
||||
private IPointerEventHandler _handler;
|
||||
private Config _config;
|
||||
private UnexpectedKeyboardSwipeAdapter _swipeAdapter;
|
||||
|
||||
public Pointers(IPointerEventHandler h, Config c)
|
||||
{
|
||||
@@ -36,6 +37,8 @@ public final class Pointers implements Handler.Callback
|
||||
_config = c;
|
||||
}
|
||||
|
||||
public void setSwipeAdapter(UnexpectedKeyboardSwipeAdapter a) { _swipeAdapter = a; }
|
||||
|
||||
/** Return the list of modifiers currently activated. */
|
||||
public Modifiers getModifiers()
|
||||
{
|
||||
@@ -192,6 +195,22 @@ public final class Pointers implements Handler.Callback
|
||||
_handler.onPointerFlagsChanged(true);
|
||||
}
|
||||
|
||||
/** Cancel a single pointer by its ID — stops long-press and removes it. */
|
||||
public void cancelPointer(int pointerId)
|
||||
{
|
||||
for (int i = _ptrs.size() - 1; i >= 0; i--)
|
||||
{
|
||||
Pointer ptr = _ptrs.get(i);
|
||||
if (ptr.pointerId == pointerId && !ptr.hasFlagsAny(FLAG_P_LATCHED))
|
||||
{
|
||||
_longpress_handler.removeMessages(ptr.timeoutWhat);
|
||||
_ptrs.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
_handler.onPointerFlagsChanged(false);
|
||||
}
|
||||
|
||||
/* Whether an other pointer is down on a non-special key. */
|
||||
private boolean isOtherPointerDown()
|
||||
{
|
||||
@@ -214,7 +233,13 @@ public final class Pointers implements Handler.Callback
|
||||
KeyValue value = _handler.modifyKey(key.keys[0], mods);
|
||||
Pointer ptr = make_pointer(pointerId, key, value, x, y, mods);
|
||||
_ptrs.add(ptr);
|
||||
startLongPress(ptr);
|
||||
// Space bar gets a slightly shorter long-press (500ms) so the joypad
|
||||
// activates quickly when holding space
|
||||
if (value != null && value.getKind() == KeyValue.Kind.Editing
|
||||
&& value.getEditing() == KeyValue.Editing.SPACE_BAR)
|
||||
startLongPress(ptr, 500);
|
||||
else
|
||||
startLongPress(ptr);
|
||||
_handler.onPointerDown(value, false);
|
||||
}
|
||||
|
||||
@@ -442,6 +467,14 @@ public final class Pointers implements Handler.Callback
|
||||
_longpress_handler.sendEmptyMessageDelayed(what, _config.longPressTimeout);
|
||||
}
|
||||
|
||||
/** Start long-press timer with a custom timeout override (ms). */
|
||||
private void startLongPress(Pointer ptr, long timeoutMs)
|
||||
{
|
||||
int what = (uniqueTimeoutWhat++);
|
||||
ptr.timeoutWhat = what;
|
||||
_longpress_handler.sendEmptyMessageDelayed(what, timeoutMs);
|
||||
}
|
||||
|
||||
private void stopLongPress(Pointer ptr)
|
||||
{
|
||||
_longpress_handler.removeMessages(ptr.timeoutWhat);
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package juloo.keyboard2;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import dev.dettmer.swipetype.android.GesturePoint;
|
||||
import dev.dettmer.swipetype.android.KeyboardLayoutDescriptor;
|
||||
import dev.dettmer.swipetype.android.KeyboardLayoutDescriptor.KeyInfo;
|
||||
import dev.dettmer.swipetype.android.SwipeTypeAdapter;
|
||||
import dev.dettmer.swipetype.android.SwipeTypeCandidate;
|
||||
import dev.dettmer.swipetype.android.SwipeTypeEngine;
|
||||
import dev.dettmer.swipetype.android.SwipeTypeError;
|
||||
|
||||
public class UnexpectedKeyboardSwipeAdapter implements SwipeTypeAdapter
|
||||
{
|
||||
private static final String TAG = "SwipeAdapter";
|
||||
|
||||
private SwipeTypeEngine _engine;
|
||||
private Context _context;
|
||||
|
||||
private List<GesturePoint> _gesturePoints = new ArrayList<>();
|
||||
private boolean _gestureActive = false;
|
||||
private CandidatesCallback _candidatesCallback;
|
||||
|
||||
/** Key positions (dp) set by the view after layout. */
|
||||
private List<KeyInfo> _cachedKeys = null;
|
||||
private float _layoutWidthDp = 0;
|
||||
private float _layoutHeightDp = 0;
|
||||
|
||||
public interface CandidatesCallback
|
||||
{
|
||||
void onSwipeCandidates(String[] words, float[] scores);
|
||||
}
|
||||
|
||||
public UnexpectedKeyboardSwipeAdapter(Context context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
/** Called by the view after layout with all key positions in dp. */
|
||||
public void setKeyLayout(List<KeyInfo> keys, float widthDp, float heightDp)
|
||||
{
|
||||
_cachedKeys = keys;
|
||||
_layoutWidthDp = widthDp;
|
||||
_layoutHeightDp = heightDp;
|
||||
if (_engine != null && _engine.isInitialized())
|
||||
_engine.notifyLayoutChanged();
|
||||
}
|
||||
|
||||
public void init(CandidatesCallback callback)
|
||||
{
|
||||
_candidatesCallback = callback;
|
||||
_engine = new SwipeTypeEngine();
|
||||
_engine.init(_context, this);
|
||||
loadDict();
|
||||
}
|
||||
|
||||
private void loadDict()
|
||||
{
|
||||
int resId = _context.getResources().getIdentifier(
|
||||
"en_us_sample", "raw", _context.getPackageName());
|
||||
if (resId == 0) { Log.w(TAG, "Dictionary resource not found"); return; }
|
||||
try (InputStream is = _context.getResources().openRawResource(resId))
|
||||
{
|
||||
boolean ok = _engine.loadDictionary("en-US", is);
|
||||
Log.i(TAG, "loadDictionary: " + ok);
|
||||
}
|
||||
catch (Exception e) { Log.e(TAG, "Failed: " + e.getMessage()); }
|
||||
}
|
||||
|
||||
public void startGesture() { _gesturePoints.clear(); _gestureActive = true; }
|
||||
|
||||
public void addPoint(float x, float y, long timestamp)
|
||||
{
|
||||
if (_gestureActive) _gesturePoints.add(new GesturePoint(x, y, timestamp));
|
||||
}
|
||||
|
||||
public void finishGesture()
|
||||
{
|
||||
_gestureActive = false;
|
||||
if (_gesturePoints.size() < 3) { _gesturePoints.clear(); return; }
|
||||
_engine.processGesture(_gesturePoints);
|
||||
_gesturePoints.clear();
|
||||
}
|
||||
|
||||
public void cancelGesture() { _gestureActive = false; _gesturePoints.clear(); }
|
||||
|
||||
// ===== SwipeTypeAdapter =====
|
||||
|
||||
@Override
|
||||
public void onInit(SwipeTypeEngine engine) { Log.i(TAG, "Engine initialized"); }
|
||||
|
||||
@Override
|
||||
public KeyboardLayoutDescriptor getKeyboardLayout()
|
||||
{
|
||||
if (_cachedKeys != null && !_cachedKeys.isEmpty())
|
||||
return new KeyboardLayoutDescriptor("en-US", _cachedKeys,
|
||||
_layoutWidthDp, _layoutHeightDp);
|
||||
// Fallback — should not be reached once the view reports layout
|
||||
List<KeyInfo> stub = new ArrayList<>();
|
||||
float d = _context.getResources().getDisplayMetrics().density;
|
||||
String[] rows = {"qwertyuiop", "asdfghjkl", "zxcvbnm"};
|
||||
float y = 0;
|
||||
for (String r : rows) {
|
||||
float x = 0;
|
||||
for (int i = 0; i < r.length(); i++) {
|
||||
char c = r.charAt(i);
|
||||
stub.add(new KeyInfo(String.valueOf(c), (int)c, x+12f/d, y+12f/d, 24f/d, 24f/d));
|
||||
x += 24f/d;
|
||||
}
|
||||
y += 24f/d;
|
||||
}
|
||||
return new KeyboardLayoutDescriptor("en-US", stub, 240f/d, 72f/d);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCandidatesReady(List<SwipeTypeCandidate> candidates)
|
||||
{
|
||||
if (_candidatesCallback == null || candidates.isEmpty()) return;
|
||||
String[] words = new String[candidates.size()];
|
||||
float[] scores = new float[candidates.size()];
|
||||
for (int i = 0; i < candidates.size(); i++)
|
||||
{
|
||||
words[i] = candidates.get(i).word;
|
||||
scores[i] = candidates.get(i).confidence;
|
||||
}
|
||||
_candidatesCallback.onSwipeCandidates(words, scores);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(SwipeTypeError error) { Log.e(TAG, "Error: " + error.name()); }
|
||||
|
||||
public void shutdown()
|
||||
{
|
||||
if (_engine != null) _engine.shutdown();
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,7 @@ public class CandidatesView extends LinearLayout
|
||||
float row_height = config.keyboard_rows_height_pixels * (1 - config.key_vertical_margin);
|
||||
ViewGroup.MarginLayoutParams p =
|
||||
(ViewGroup.MarginLayoutParams)getLayoutParams();
|
||||
p.height = (int)row_height;
|
||||
p.height = (int)(row_height * 0.6f);
|
||||
setLayoutParams(p);
|
||||
// Match the size of labels on the keyboard.
|
||||
float text_size = row_height * config.characterSize * config.labelTextSize;
|
||||
|
||||
@@ -66,10 +66,11 @@ Useful links.
|
||||
<key c="z"/>
|
||||
<key c="x" ne="loc †"/>
|
||||
<key c="c" ne="<" sw="."/>
|
||||
<key c="v" ne=">" sw=","/>
|
||||
<key c="v" ne=">"/>
|
||||
<key c="b" ne="\?" sw="/"/>
|
||||
<key c="n" ne=":" sw=";"/>
|
||||
<key c="m" ne=""" sw="'"/>
|
||||
<key c="." key3=","/>
|
||||
<key role="action" c="backspace" ne="delete"/>
|
||||
</row>
|
||||
</keyboard>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Tux (Linux penguin) outline icon for meta/super key -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M12,3C8.69,3 6,5.69 6,9C6,10.09 6.29,11.12 6.79,12H6C4.9,12 4,12.9 4,14C4,14.75 4.42,15.42 5.03,15.79C5.01,15.86 5,15.93 5,16C5,17.1 5.9,18 7,18C7.28,18 7.54,17.93 7.78,17.82C8.43,19.22 9.84,20.17 11.5,20.17C13.16,20.17 14.57,19.22 15.22,17.82C15.46,17.93 15.72,18 16,18C17.1,18 18,17.1 18,16C18,15.93 17.99,15.86 17.97,15.79C18.58,15.42 19,14.75 19,14C19,12.9 18.1,12 17,12H17.21C17.71,11.12 18,10.09 18,9C18,5.69 15.31,3 12,3M11,5V6H10V5H11M12,5H13V6H12V5M8.5,7C9.33,7 10,7.67 10,8.5C10,9.33 9.33,10 8.5,10C7.67,10 7,9.33 7,8.5C7,7.67 7.67,7 8.5,7M15.5,7C16.33,7 17,7.67 17,8.5C17,9.33 16.33,10 15.5,10C14.67,10 14,9.33 14,8.5C14,7.67 14.67,7 15.5,7M11,11H13V12.5C13,12.78 12.78,13 12.5,13H11.5C11.22,13 11,12.78 11,12.5V11Z" /></svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
+1
Submodule vendor/libswipetype added at e4aef2faaa
Reference in New Issue
Block a user