Skip to content

Commit 9a5694e

Browse files
committed
Merge branch 'nikitauchaev-feature/cyrillic-layout-layer'
2 parents da31ac4 + 900f26e commit 9a5694e

File tree

16 files changed

+791
-8
lines changed

16 files changed

+791
-8
lines changed

app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ android {
1212
minSdk = 29
1313
targetSdk = 33
1414
versionCode = 1
15-
versionName = "0.77.2"
15+
versionName = "0.78"
1616
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
1717
}
1818

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package io.github.rickybrent.minimal_symlayer_keyboard
2+
3+
import android.view.KeyEvent
4+
5+
/**
6+
* Provides mappings from Latin QWERTY keys to Cyrillic characters.
7+
* Maps both lowercase and uppercase variants.
8+
*/
9+
object CyrillicMappings {
10+
11+
/**
12+
* Mapping from Latin key codes to Cyrillic characters (lowercase)
13+
* Classic PC (JCUKEN-like) layout adapted to available keys.
14+
*/
15+
private val lowercaseMap: Map<Int, Char> = mapOf(
16+
// Top row
17+
KeyEvent.KEYCODE_Q to 'й',
18+
KeyEvent.KEYCODE_W to 'ц',
19+
KeyEvent.KEYCODE_E to 'у',
20+
KeyEvent.KEYCODE_R to 'к',
21+
KeyEvent.KEYCODE_T to 'е',
22+
KeyEvent.KEYCODE_Y to 'н',
23+
KeyEvent.KEYCODE_U to 'г',
24+
KeyEvent.KEYCODE_I to 'ш',
25+
KeyEvent.KEYCODE_O to 'щ',
26+
KeyEvent.KEYCODE_P to 'з',
27+
// Removed direct mappings for х and ъ (moved to Alt-combos)
28+
// KeyEvent.KEYCODE_LEFT_BRACKET to 'х',
29+
// KeyEvent.KEYCODE_RIGHT_BRACKET to 'ъ',
30+
// Removed MINUS to 'х' fallback per request
31+
// Removed EQUALS to 'э' fallback per request
32+
33+
// Home row
34+
KeyEvent.KEYCODE_A to 'ф',
35+
KeyEvent.KEYCODE_S to 'ы',
36+
KeyEvent.KEYCODE_D to 'в',
37+
KeyEvent.KEYCODE_F to 'а',
38+
KeyEvent.KEYCODE_G to 'п',
39+
KeyEvent.KEYCODE_H to 'р',
40+
KeyEvent.KEYCODE_J to 'о',
41+
KeyEvent.KEYCODE_K to 'л',
42+
KeyEvent.KEYCODE_L to 'д',
43+
// Removed direct mappings for ж and э (moved to Alt-combos)
44+
// KeyEvent.KEYCODE_SEMICOLON to 'ж',
45+
// KeyEvent.KEYCODE_APOSTROPHE to 'э',
46+
47+
// Bottom row
48+
KeyEvent.KEYCODE_Z to 'я',
49+
KeyEvent.KEYCODE_X to 'ч',
50+
KeyEvent.KEYCODE_C to 'с',
51+
KeyEvent.KEYCODE_V to 'м',
52+
KeyEvent.KEYCODE_B to 'и',
53+
KeyEvent.KEYCODE_N to 'т',
54+
KeyEvent.KEYCODE_M to 'ь',
55+
// Removed direct mappings for б and ю (moved to Alt-combos)
56+
// KeyEvent.KEYCODE_COMMA to 'б',
57+
// KeyEvent.KEYCODE_PERIOD to 'ю',
58+
KeyEvent.KEYCODE_SLASH to '.',
59+
KeyEvent.KEYCODE_BACKSLASH to '/',
60+
61+
// Removed direct mapping for ё (moved to Alt-combo on K)
62+
// KeyEvent.KEYCODE_GRAVE to 'ё'
63+
)
64+
65+
/**
66+
* Mapping from Latin key codes to Cyrillic characters (uppercase)
67+
*/
68+
private val uppercaseMap: Map<Int, Char> = mapOf(
69+
// Top row
70+
KeyEvent.KEYCODE_Q to 'Й',
71+
KeyEvent.KEYCODE_W to 'Ц',
72+
KeyEvent.KEYCODE_E to 'У',
73+
KeyEvent.KEYCODE_R to 'К',
74+
KeyEvent.KEYCODE_T to 'Е',
75+
KeyEvent.KEYCODE_Y to 'Н',
76+
KeyEvent.KEYCODE_U to 'Г',
77+
KeyEvent.KEYCODE_I to 'Ш',
78+
KeyEvent.KEYCODE_O to 'Щ',
79+
KeyEvent.KEYCODE_P to 'З',
80+
// Removed direct mappings for Х and Ъ
81+
// Removed MINUS/EQUALS fallbacks
82+
83+
// Home row
84+
KeyEvent.KEYCODE_A to 'Ф',
85+
KeyEvent.KEYCODE_S to 'Ы',
86+
KeyEvent.KEYCODE_D to 'В',
87+
KeyEvent.KEYCODE_F to 'А',
88+
KeyEvent.KEYCODE_G to 'П',
89+
KeyEvent.KEYCODE_H to 'Р',
90+
KeyEvent.KEYCODE_J to 'О',
91+
KeyEvent.KEYCODE_K to 'Л',
92+
KeyEvent.KEYCODE_L to 'Д',
93+
// Removed direct mappings for Ж and Э
94+
95+
// Bottom row
96+
KeyEvent.KEYCODE_Z to 'Я',
97+
KeyEvent.KEYCODE_X to 'Ч',
98+
KeyEvent.KEYCODE_C to 'С',
99+
KeyEvent.KEYCODE_V to 'М',
100+
KeyEvent.KEYCODE_B to 'И',
101+
KeyEvent.KEYCODE_N to 'Т',
102+
KeyEvent.KEYCODE_M to 'Ь',
103+
// Removed direct mappings for Б and Ю
104+
KeyEvent.KEYCODE_SLASH to '.',
105+
KeyEvent.KEYCODE_BACKSLASH to '/',
106+
107+
// Removed direct mapping for Ё
108+
)
109+
110+
/**
111+
* Alt-combo mappings used when Cyrillic layer is active and ALT is held.
112+
* 1) Alt+Y -> х
113+
* 2) Alt+U -> ъ
114+
* 3) Alt+G -> ж
115+
* 4) Alt+H -> э
116+
* 5) Alt+J -> ю
117+
* 6) Alt+K -> ё
118+
* 7) Alt+M -> б
119+
*/
120+
private val altLowercaseMap: Map<Int, Char> = mapOf(
121+
KeyEvent.KEYCODE_Y to 'х',
122+
KeyEvent.KEYCODE_U to 'ъ',
123+
KeyEvent.KEYCODE_G to 'ж',
124+
KeyEvent.KEYCODE_H to 'э',
125+
KeyEvent.KEYCODE_J to 'ю',
126+
KeyEvent.KEYCODE_K to 'ё',
127+
KeyEvent.KEYCODE_M to 'б',
128+
)
129+
private val altUppercaseMap: Map<Int, Char> = mapOf(
130+
KeyEvent.KEYCODE_Y to 'Х',
131+
KeyEvent.KEYCODE_U to 'Ъ',
132+
KeyEvent.KEYCODE_G to 'Ж',
133+
KeyEvent.KEYCODE_H to 'Э',
134+
KeyEvent.KEYCODE_J to 'Ю',
135+
KeyEvent.KEYCODE_K to 'Ё',
136+
KeyEvent.KEYCODE_M to 'Б',
137+
)
138+
139+
/**
140+
* Get the Cyrillic character for the given key code and shift state
141+
* @param keyCode The key code to look up
142+
* @param isShifted Whether shift is active (uppercase)
143+
* @return The Cyrillic character, or null if no mapping exists
144+
*/
145+
fun getCyrillicChar(keyCode: Int, isShifted: Boolean): Char? {
146+
return if (isShifted) {
147+
uppercaseMap[keyCode]
148+
} else {
149+
lowercaseMap[keyCode]
150+
}
151+
}
152+
153+
/**
154+
* Get the Cyrillic character for Alt+key when Cyrillic layer is active.
155+
*/
156+
fun getAltCyrillicChar(keyCode: Int, isShifted: Boolean): Char? {
157+
return if (isShifted) {
158+
altUppercaseMap[keyCode]
159+
} else {
160+
altLowercaseMap[keyCode]
161+
}
162+
}
163+
164+
/**
165+
* Check if a key code has a Cyrillic mapping
166+
* @param keyCode The key code to check
167+
* @return True if the key code has a Cyrillic mapping
168+
*/
169+
fun hasCyrillicMapping(keyCode: Int): Boolean {
170+
return lowercaseMap.containsKey(keyCode)
171+
}
172+
173+
/**
174+
* Check if a key code has an ALT Cyrillic mapping
175+
*/
176+
fun hasAltCyrillicMapping(keyCode: Int): Boolean {
177+
return altLowercaseMap.containsKey(keyCode)
178+
}
179+
180+
/**
181+
* Get all supported key codes for Cyrillic input
182+
* @return Set of key codes that have Cyrillic mappings
183+
*/
184+
fun getSupportedKeyCodes(): Set<Int> = lowercaseMap.keys
185+
}

app/src/main/java/io/github/rickybrent/minimal_symlayer_keyboard/InputMethodService.kt

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,17 @@ class InputMethodService : AndroidInputMethodService() {
170170
private val dotCtrl = TripleModifier()
171171
private val emojiMeta = TripleModifier()
172172
private val caps = Modifier()
173+
private val cyrillicLayer = CyrillicLayerModifier()
173174

174175
private var lastShift = false
175176
private var lastAlt = false
176177
private var lastSym = false
177178
private var lastDotCtrl = false
178179
private var lastEmojiMeta = false
179180
private var lastCaps = false
181+
private var lastCyrillicLayer = false
182+
183+
private var cyrillicLayerToggleEnabled = false
180184

181185
private var autoCapitalize = false
182186
private var showToolbar = false
@@ -346,14 +350,24 @@ class InputMethodService : AndroidInputMethodService() {
346350
alt.onKeyDown()
347351
updateStatusIconIfNeeded(true)
348352
}
349-
KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_SHIFT_RIGHT -> {
353+
KeyEvent.KEYCODE_SHIFT_LEFT -> {
350354
if (caps.get()) {
351355
caps.reset()
352356
} else {
353357
shift.onKeyDown()
354358
}
355359
updateStatusIconIfNeeded(true)
356360
}
361+
KeyEvent.KEYCODE_SHIFT_RIGHT -> {
362+
if (caps.get()) {
363+
caps.reset()
364+
} else {
365+
shift.onKeyDown()
366+
}
367+
if (cyrillicLayerToggleEnabled)
368+
cyrillicLayer.onRightShiftDown()
369+
updateStatusIconIfNeeded(true)
370+
}
357371
KeyEvent.KEYCODE_SYM -> {
358372
sym.onKeyDown()
359373
onSymPossiblyChanged()
@@ -419,7 +433,22 @@ class InputMethodService : AndroidInputMethodService() {
419433

420434
// Print something if it is a simple printing key press
421435
if((event.isPrintingKey || event.keyCode == KeyEvent.KEYCODE_SPACE || (event.keyCode == KeyEvent.KEYCODE_ENTER && shift.get()))) {
422-
val str = event.getUnicodeChar(enhancedMetaState(event)).toChar().toString()
436+
val isShifted = shift.get() || caps.get()
437+
val str = if (cyrillicLayer.isActive()) {
438+
if (alt.get() && CyrillicMappings.hasAltCyrillicMapping(event.keyCode)) {
439+
CyrillicMappings.getAltCyrillicChar(event.keyCode, isShifted)?.toString()
440+
?: event.getUnicodeChar(enhancedMetaState(event)).toChar().toString()
441+
} else if (CyrillicMappings.hasCyrillicMapping(event.keyCode)) {
442+
CyrillicMappings.getCyrillicChar(event.keyCode, isShifted)?.toString()
443+
?: event.getUnicodeChar(enhancedMetaState(event)).toChar().toString()
444+
} else {
445+
// No mapping: fall back to default Latin character
446+
event.getUnicodeChar(enhancedMetaState(event)).toChar().toString()
447+
}
448+
} else {
449+
// Cyrillic layer not active: default Latin behavior
450+
event.getUnicodeChar(enhancedMetaState(event)).toChar().toString()
451+
}
423452
currentInputConnection?.commitText(str, 1)
424453

425454
consumeModifierNext()
@@ -500,10 +529,20 @@ class InputMethodService : AndroidInputMethodService() {
500529
alt.onKeyUp()
501530
updateStatusIconIfNeeded(true)
502531
}
503-
KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_SHIFT_RIGHT -> {
532+
KeyEvent.KEYCODE_SHIFT_LEFT -> {
504533
shift.onKeyUp()
505534
updateStatusIconIfNeeded(true)
506535
}
536+
KeyEvent.KEYCODE_SHIFT_RIGHT -> {
537+
shift.onKeyUp()
538+
if (cyrillicLayerToggleEnabled)
539+
cyrillicLayer.onRightShiftUp()
540+
// Check if Cyrillic layer was toggled and provide haptic feedback
541+
if (cyrillicLayer.wasJustToggled()) {
542+
vibrate()
543+
}
544+
updateStatusIconIfNeeded(true)
545+
}
507546
KeyEvent.KEYCODE_SYM -> {
508547
sym.onKeyUp()
509548
onSymPossiblyChanged()
@@ -760,7 +799,8 @@ class InputMethodService : AndroidInputMethodService() {
760799
val ctrlState = dotCtrl.get()
761800
val capsState = caps.get()
762801
val metaState = emojiMeta.get()
763-
if(force || symState != lastSym || altState != lastAlt || shiftState != lastShift || capsState != lastCaps || ctrlState != lastDotCtrl || metaState != lastEmojiMeta) {
802+
val cyrillicState = cyrillicLayer.isActive()
803+
if(force || symState != lastSym || altState != lastAlt || shiftState != lastShift || capsState != lastCaps || ctrlState != lastDotCtrl || metaState != lastEmojiMeta || cyrillicState != lastCyrillicLayer) {
764804
if(sym.get()) {
765805
if (shift.get()) {
766806
showStatusIcon(R.drawable.symshift)
@@ -769,10 +809,15 @@ class InputMethodService : AndroidInputMethodService() {
769809
}
770810
} else if(emojiMeta.get()) {
771811
showStatusIcon(R.drawable.meta)
772-
} else if(alt.get()) {
773-
showStatusIcon(if (alt.isLocked()) R.drawable.altlock else R.drawable.alt)
774812
} else if (dotCtrl.get()) {
775813
showStatusIcon(if (dotCtrl.isLocked()) R.drawable.ctrllock else R.drawable.ctrl)
814+
} else if(cyrillicLayer.isActive()) {
815+
if(shift.get() || caps.get())
816+
showStatusIcon(if (alt.get()) R.drawable.cyrillicshiftalt else R.drawable.cyrillicshift)
817+
else
818+
showStatusIcon(if (alt.get()) R.drawable.cyrillicalt else R.drawable.cyrillic)
819+
} else if(alt.get()) {
820+
showStatusIcon(if (alt.isLocked()) R.drawable.altlock else R.drawable.alt)
776821
} else if(shift.get()) {
777822
showStatusIcon(if(shift.isLocked()) R.drawable.shiftlock else R.drawable.shift)
778823
} else if(caps.get()) {
@@ -787,6 +832,7 @@ class InputMethodService : AndroidInputMethodService() {
787832
lastDotCtrl = ctrlState
788833
lastCaps = capsState
789834
lastEmojiMeta = metaState
835+
lastCyrillicLayer = cyrillicState
790836
}
791837

792838
override fun showStatusIcon(iconResId: Int) {
@@ -893,6 +939,7 @@ class InputMethodService : AndroidInputMethodService() {
893939
multipress.ignoreConsonantsOnFirstLevel = preferences.getBoolean("FirstLevelOnlyVowels", false)
894940
multipress.ligaturesEnabled = preferences.getBoolean("pref_enable_ligatures", false)
895941

942+
cyrillicLayerToggleEnabled = preferences.getBoolean("pref_enable_cyrillic_layer", false)
896943

897944
val templateId = preferences.getString("FirstLevelTemplate", "fr")
898945
if(templates.containsKey(templateId)) {
@@ -906,6 +953,8 @@ class InputMethodService : AndroidInputMethodService() {
906953
emojiMeta.shortPressKeyCode = preferenceToKeyCode(preferences.getString("pref_emojimeta_tap", "emoji"))
907954
emojiMeta.longPressKeyCode = preferenceToKeyCode(preferences.getString("pref_emojimeta_long_press", "0"))
908955
emojiMeta.modKeyCode = preferenceToKeyCode(preferences.getString("pref_emojimeta_hold", "meta"))
956+
957+
// TODO: Separate modifier and special-key logic and add better handling for sym and right shift.
909958
}
910959

911960
private fun preferenceToKeyCode(preferenceValue: String?): Int {
@@ -967,6 +1016,7 @@ class InputMethodService : AndroidInputMethodService() {
9671016
dotCtrl.reset()
9681017
emojiMeta.reset()
9691018
caps.reset()
1019+
cyrillicLayer.reset()
9701020
updateStatusIconIfNeeded(true)
9711021
}
9721022
}

0 commit comments

Comments
 (0)