Skip to content

Commit 3e8b09d

Browse files
feat: switch numeric inputs to symbols keyboard (#609)
* feat: switch numeric inputs to symbols keyboard * feat: implement numeric keyboard layout and input type handling * feat: enhance numeric keyboard handling and visibility logic * Run ktlint --format --------- Co-authored-by: Andrew Tavis McAllister <andrew.t.mcallister@gmail.com>
1 parent db9c267 commit 3e8b09d

11 files changed

Lines changed: 186 additions & 34 deletions

File tree

app/src/main/java/be/scri/helpers/KeyHandler.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ import be.scri.services.GeneralKeyboardIME.ScribeState
2020
class KeyHandler(
2121
private val ime: GeneralKeyboardIME,
2222
) {
23-
private val suggestionHandler = SuggestionHandler(ime)
23+
private val suggestionHandler = ime.suggestionHandler
2424
private val spaceKeyProcessor = SpaceKeyProcessor(ime, suggestionHandler)
25-
private val autocompletionHandler = AutocompletionHandler(ime)
25+
private val autocompletionHandler = ime.autocompletionHandler
2626

2727
/** Tracks if the last key pressed was a space, used for "period on double space" logic. */
2828
private var wasLastKeySpace: Boolean = false

app/src/main/java/be/scri/helpers/ui/KeyboardUIManager.kt

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,17 @@ class KeyboardUIManager(
6262

6363
fun getKeyboardLayoutXML(): Int
6464

65+
fun getCurrentKeyboardLayoutXML(): Int
66+
6567
fun getCurrentEnterKeyType(): Int
6668

6769
fun commitText(text: String)
6870

6971
fun onKeyboardActionListener(): KeyboardView.OnKeyboardActionListener
7072

7173
fun processLinguisticSuggestions(word: String)
74+
75+
fun isNumericKeyboardActive(): Boolean
7276
}
7377

7478
var keyboardView: KeyboardView = binding.keyboardView
@@ -159,7 +163,12 @@ class KeyboardUIManager(
159163
val isUserDarkMode = getIsDarkModeOrNot(context)
160164

161165
when (currentState) {
162-
ScribeState.IDLE -> setupIdleView(language, emojiAutoSuggestionEnabled, autoSuggestEmojis)
166+
ScribeState.IDLE ->
167+
setupIdleView(
168+
language,
169+
emojiAutoSuggestionEnabled,
170+
autoSuggestEmojis,
171+
)
163172
ScribeState.SELECT_COMMAND -> setupSelectCommandView(language)
164173
ScribeState.INVALID -> setupInvalidView(language, invalidCommandSource)
165174
ScribeState.TRANSLATE -> {
@@ -184,7 +193,7 @@ class KeyboardUIManager(
184193
emojiAutoSuggestionEnabled: Boolean,
185194
autoSuggestEmojis: MutableList<String>?,
186195
) {
187-
binding.commandOptionsBar.visibility = View.VISIBLE
196+
binding.commandOptionsBar.visibility = if (listener.isNumericKeyboardActive()) View.GONE else View.VISIBLE
188197
binding.toolbarBar.visibility = View.GONE
189198

190199
val isUserDarkMode = getIsDarkModeOrNot(context)
@@ -226,7 +235,11 @@ class KeyboardUIManager(
226235

227236
binding.scribeKeyOptions.foreground = AppCompatResources.getDrawable(context, R.drawable.ic_scribe_icon_vector)
228237

229-
initializeKeyboard(listener.getKeyboardLayoutXML())
238+
val keyboardXml = listener.getCurrentKeyboardLayoutXML()
239+
initializeKeyboard(keyboardXml)
240+
if (keyboardXml == R.xml.keys_symbols) {
241+
setupCurrencySymbol(language)
242+
}
230243

231244
updateButtonVisibility(ScribeState.IDLE, emojiAutoSuggestionEnabled, autoSuggestEmojis)
232245
updateEmojiSuggestion(ScribeState.IDLE, emojiAutoSuggestionEnabled, autoSuggestEmojis)
@@ -239,7 +252,7 @@ class KeyboardUIManager(
239252
* (Translate, Conjugate, Plural).
240253
*/
241254
private fun setupSelectCommandView(language: String) {
242-
binding.commandOptionsBar.visibility = View.VISIBLE
255+
binding.commandOptionsBar.visibility = if (listener.isNumericKeyboardActive()) View.GONE else View.VISIBLE
243256
binding.toolbarBar.visibility = View.GONE
244257

245258
val isUserDarkMode = getIsDarkModeOrNot(context)
@@ -786,6 +799,8 @@ class KeyboardUIManager(
786799
* Disables all auto-suggestions and resets the suggestion buttons to their default, inactive state.
787800
*/
788801
fun disableAutoSuggest(language: String) {
802+
val isNumericKeyboard = listener.getCurrentKeyboardLayoutXML() == R.xml.keys_numeric
803+
789804
binding.translateBtnRight.visibility = View.INVISIBLE
790805
binding.translateBtnLeft.visibility = View.INVISIBLE
791806
binding.translateBtn.visibility = View.VISIBLE
@@ -801,9 +816,20 @@ class KeyboardUIManager(
801816
binding.translateBtn.background = null
802817
binding.translateBtn.setOnClickListener(createSuggestionClickListener(suggestion1))
803818

804-
val suggestion2 = suggestions.getOrNull(1) ?: ""
805-
binding.conjugateBtn.text = suggestion2
806-
binding.conjugateBtn.setOnClickListener(createSuggestionClickListener(suggestion2))
819+
if (isNumericKeyboard) {
820+
binding.conjugateBtn.text = ""
821+
binding.conjugateBtn.setOnClickListener(null)
822+
binding.conjugateBtn.visibility = View.GONE
823+
binding.separator2.visibility = View.GONE
824+
binding.separator3.visibility = View.GONE
825+
} else {
826+
val suggestion2 = suggestions.getOrNull(1) ?: ""
827+
binding.conjugateBtn.visibility = View.VISIBLE
828+
binding.conjugateBtn.text = suggestion2
829+
binding.conjugateBtn.setOnClickListener(createSuggestionClickListener(suggestion2))
830+
binding.separator2.visibility = View.VISIBLE
831+
binding.separator3.visibility = View.VISIBLE
832+
}
807833

808834
val suggestion3 = suggestions.getOrNull(2) ?: ""
809835
binding.pluralBtn.text = suggestion3

app/src/main/java/be/scri/services/EnglishKeyboardIME.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ class EnglishKeyboardIME : GeneralKeyboardIME("English") {
3232
override var inputTypeClass: Int = InputType.TYPE_CLASS_TEXT
3333
override var enterKeyType: Int = IME_ACTION_NONE
3434
override var switchToLetters: Boolean = false
35-
override var hasTextBeforeCursor: Boolean = false
3635

3736
private val keyHandler by lazy { KeyHandler(this) }
3837

app/src/main/java/be/scri/services/GeneralKeyboardIME.kt

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,8 @@ abstract class GeneralKeyboardIME(
121121
private val shiftPermToggleSpeed: Int = DEFAULT_SHIFT_PERM_TOGGLE_SPEED
122122

123123
private lateinit var dbManagers: DatabaseManagers
124-
private lateinit var suggestionHandler: SuggestionHandler
125-
private lateinit var autocompletionHandler: AutocompletionHandler
124+
internal lateinit var suggestionHandler: SuggestionHandler
125+
internal lateinit var autocompletionHandler: AutocompletionHandler
126126
private lateinit var autocompletionManager: AutocompletionDataManager
127127
private var dataContract: DataContract? = null
128128

@@ -144,6 +144,7 @@ abstract class GeneralKeyboardIME(
144144
var wordSuggestions: List<String>? = null
145145
var checkIfPluralWord: Boolean = false
146146
private var currentEnterKeyType: Int? = null
147+
private var isNumericKeyboardActive: Boolean = false
147148

148149
internal var currentState: ScribeState = ScribeState.IDLE
149150
internal var invalidCommandSource: ScribeState = ScribeState.IDLE
@@ -176,6 +177,22 @@ abstract class GeneralKeyboardIME(
176177
internal const val MAX_TEXT_LENGTH = 1000
177178
const val COMMIT_TEXT_CURSOR_POSITION = 1
178179
internal const val CUSTOM_CURSOR = "" // special tall cursor character
180+
181+
internal fun shouldUseNumericKeyboard(inputType: Int): Boolean =
182+
when (inputType and TYPE_MASK_CLASS) {
183+
TYPE_CLASS_NUMBER, TYPE_CLASS_DATETIME, TYPE_CLASS_PHONE -> true
184+
else -> false
185+
}
186+
187+
internal fun getKeyboardLayoutXMLForInputType(
188+
inputType: Int,
189+
letterKeyboardLayoutXML: Int,
190+
): Int =
191+
if (shouldUseNumericKeyboard(inputType)) {
192+
R.xml.keys_numeric
193+
} else {
194+
letterKeyboardLayoutXML
195+
}
179196
}
180197

181198
enum class ScribeState { IDLE, SELECT_COMMAND, TRANSLATE, CONJUGATE, PLURAL, SELECT_VERB_CONJUNCTION, INVALID, ALREADY_PLURAL }
@@ -286,25 +303,16 @@ abstract class GeneralKeyboardIME(
286303
// This setter triggers the logic in the property override if not shadowed.
287304
hasTextBeforeCursor = currentInputConnection?.getTextBeforeCursor(1, 0)?.isNotEmpty() == true
288305

289-
val keyboardXml =
290-
when (inputTypeClass) {
291-
TYPE_CLASS_NUMBER, TYPE_CLASS_DATETIME, TYPE_CLASS_PHONE -> {
292-
keyboardMode = keyboardSymbols
293-
R.xml.keys_symbols
294-
}
295-
296-
else -> {
297-
keyboardMode = keyboardLetters
298-
getKeyboardLayoutXML()
299-
}
300-
}
306+
isNumericKeyboardActive = shouldUseNumericKeyboard(attribute.inputType)
307+
keyboardMode = if (isNumericKeyboardActive) keyboardSymbols else keyboardLetters
308+
val keyboardXml = getKeyboardLayoutXMLForInputType(attribute.inputType, getKeyboardLayoutXML())
301309

302310
loadLanguageData()
303311

304312
keyboard = KeyboardBase(this, keyboardXml, enterKeyType)
305313
keyboardView?.setKeyboard(keyboard!!)
306314

307-
if (keyboardXml == R.xml.keys_symbols) {
315+
if (this::uiManager.isInitialized && keyboardXml == R.xml.keys_symbols) {
308316
uiManager.setupCurrencySymbol(language)
309317
}
310318
}
@@ -334,7 +342,7 @@ abstract class GeneralKeyboardIME(
334342
banner.visibility =
335343
if (hasData) View.GONE else View.VISIBLE
336344
binding.commandOptionsBar.visibility =
337-
if (hasData) View.VISIBLE else View.GONE
345+
if (hasData && !isNumericKeyboardActive) View.VISIBLE else View.GONE
338346
val isDarkMode = getIsDarkModeOrNot(applicationContext)
339347
val bannerColor = if (isDarkMode) R.color.dark_tutorial_button_color else R.color.light_tutorial_button_color
340348
val bannerTextColor = if (isDarkMode) R.color.dark_button_outline_color else R.color.light_text_color
@@ -727,6 +735,22 @@ abstract class GeneralKeyboardIME(
727735

728736
override fun getCurrentEnterKeyType(): Int = enterKeyType
729737

738+
override fun isNumericKeyboardActive(): Boolean = isNumericKeyboardActive
739+
740+
override fun getCurrentKeyboardLayoutXML(): Int =
741+
when (keyboardMode) {
742+
keyboardSymbols -> getPrimarySymbolKeyboardLayoutXML()
743+
keyboardSymbolShift -> R.xml.keys_symbols_shift
744+
else -> getKeyboardLayoutXML()
745+
}
746+
747+
private fun getPrimarySymbolKeyboardLayoutXML(): Int =
748+
if (isNumericKeyboardActive) {
749+
R.xml.keys_numeric
750+
} else {
751+
R.xml.keys_symbols
752+
}
753+
730754
override fun onKeyboardActionListener(): KeyboardView.OnKeyboardActionListener = this
731755

732756
override fun processLinguisticSuggestions(word: String) {
@@ -1171,7 +1195,7 @@ abstract class GeneralKeyboardIME(
11711195
R.xml.keys_symbols_shift
11721196
} else {
11731197
this.keyboardMode = keyboardSymbols
1174-
R.xml.keys_symbols
1198+
getPrimarySymbolKeyboardLayoutXML()
11751199
}
11761200
keyboard = KeyboardBase(this, keyboardXml, enterKeyType)
11771201
keyboardView!!.setKeyboard(keyboard!!)
@@ -1196,7 +1220,7 @@ abstract class GeneralKeyboardIME(
11961220
val keyboardXml =
11971221
if (keyboardMode == keyboardLetters) {
11981222
this.keyboardMode = keyboardSymbols
1199-
R.xml.keys_symbols
1223+
getPrimarySymbolKeyboardLayoutXML()
12001224
} else {
12011225
this.keyboardMode = keyboardLetters
12021226
getKeyboardLayoutXML()

app/src/main/java/be/scri/services/ItalianKeyboardIME.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ class ItalianKeyboardIME : GeneralKeyboardIME("Italian") {
3232
override var inputTypeClass: Int = InputType.TYPE_CLASS_TEXT
3333
override var enterKeyType: Int = IME_ACTION_NONE
3434
override var switchToLetters: Boolean = false
35-
override var hasTextBeforeCursor: Boolean = false
3635

3736
private val keyHandler by lazy { KeyHandler(this) }
3837

app/src/main/java/be/scri/services/RussianKeyboardIME.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ class RussianKeyboardIME : GeneralKeyboardIME("Russian") {
3232
override var inputTypeClass: Int = InputType.TYPE_CLASS_TEXT
3333
override var enterKeyType: Int = IME_ACTION_NONE
3434
override var switchToLetters: Boolean = false
35-
override var hasTextBeforeCursor: Boolean = false
3635

3736
private val keyHandler by lazy { KeyHandler(this) }
3837

app/src/main/java/be/scri/services/SwedishKeyboardIME.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ class SwedishKeyboardIME : GeneralKeyboardIME("Swedish") {
4242
override var inputTypeClass: Int = InputType.TYPE_CLASS_TEXT
4343
override var enterKeyType: Int = IME_ACTION_NONE
4444
override var switchToLetters: Boolean = false
45-
override var hasTextBeforeCursor: Boolean = false
4645

4746
private val keyHandler by lazy { KeyHandler(this) }
4847

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Keyboard xmlns:app="http://schemas.android.com/apk/res-auto">
3+
<Row app:keyWidth="25%p">
4+
<Key
5+
app:keyEdgeFlags="left"
6+
app:keyLabel="1" />
7+
<Key app:keyLabel="2" />
8+
<Key app:keyLabel="3" />
9+
<Key
10+
app:code="-5"
11+
app:isRepeatable="true"
12+
app:keyEdgeFlags="right"
13+
app:keyIcon="@drawable/ic_clear_outline_vector" />
14+
</Row>
15+
<Row app:keyWidth="25%p">
16+
<Key
17+
app:keyEdgeFlags="left"
18+
app:keyLabel="4" />
19+
<Key app:keyLabel="5" />
20+
<Key app:keyLabel="6" />
21+
<Key app:keyLabel="-" />
22+
</Row>
23+
<Row app:keyWidth="25%p">
24+
<Key
25+
app:keyEdgeFlags="left"
26+
app:keyLabel="7" />
27+
<Key app:keyLabel="8" />
28+
<Key app:keyLabel="9" />
29+
<Key app:keyLabel="+" />
30+
</Row>
31+
<Row app:keyWidth="25%p">
32+
<Key
33+
app:code="-2"
34+
app:keyEdgeFlags="left"
35+
app:keyLabel="ABC" />
36+
<Key app:keyLabel="0" />
37+
<Key
38+
app:keyLabel="."
39+
app:popupCharacters=",/:"
40+
app:popupKeyboard="@xml/keyboard_popup_template" />
41+
<Key
42+
app:code="-4"
43+
app:keyEdgeFlags="right"
44+
app:keyIcon="@drawable/ic_enter_vector" />
45+
</Row>
46+
</Keyboard>

app/src/test/kotlin/be/scri/helpers/ui/KeyboardUIManagerTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ class KeyboardUIManagerTest {
5757
// Mock Listener
5858
listener = mockk(relaxed = true)
5959
every { listener.getKeyboardLayoutXML() } returns be.scri.R.xml.keys_letters_english
60+
every { listener.getCurrentKeyboardLayoutXML() } returns be.scri.R.xml.keys_letters_english
6061
every { listener.onKeyboardActionListener() } returns mockk()
6162

6263
// Init Manager
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
3+
package be.scri.services
4+
5+
import android.text.InputType
6+
import be.scri.R
7+
import org.junit.Assert.assertEquals
8+
import org.junit.Assert.assertFalse
9+
import org.junit.Assert.assertTrue
10+
import org.junit.Test
11+
12+
class GeneralKeyboardIMEInputTypeTest {
13+
@Test
14+
fun shouldUseNumericKeyboard_returnsTrueForNumberInputs() {
15+
val inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
16+
17+
assertTrue(GeneralKeyboardIME.shouldUseNumericKeyboard(inputType))
18+
}
19+
20+
@Test
21+
fun shouldUseNumericKeyboard_returnsTrueForDateTimeInputs() {
22+
val inputType = InputType.TYPE_CLASS_DATETIME or InputType.TYPE_DATETIME_VARIATION_DATE
23+
24+
assertTrue(GeneralKeyboardIME.shouldUseNumericKeyboard(inputType))
25+
}
26+
27+
@Test
28+
fun shouldUseNumericKeyboard_returnsTrueForPhoneInputs() {
29+
assertTrue(GeneralKeyboardIME.shouldUseNumericKeyboard(InputType.TYPE_CLASS_PHONE))
30+
}
31+
32+
@Test
33+
fun shouldUseNumericKeyboard_returnsFalseForTextInputs() {
34+
val inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
35+
36+
assertFalse(GeneralKeyboardIME.shouldUseNumericKeyboard(inputType))
37+
}
38+
39+
@Test
40+
fun getKeyboardLayoutXMLForInputType_returnsNumericLayoutForNumberInputs() {
41+
val inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
42+
43+
assertEquals(
44+
R.xml.keys_numeric,
45+
GeneralKeyboardIME.getKeyboardLayoutXMLForInputType(inputType, R.xml.keys_letters_english),
46+
)
47+
}
48+
49+
@Test
50+
fun getKeyboardLayoutXMLForInputType_returnsLetterLayoutForTextInputs() {
51+
val inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
52+
53+
assertEquals(
54+
R.xml.keys_letters_english,
55+
GeneralKeyboardIME.getKeyboardLayoutXMLForInputType(inputType, R.xml.keys_letters_english),
56+
)
57+
}
58+
}

0 commit comments

Comments
 (0)