Compare commits

...

33 Commits

Author SHA1 Message Date
Patrick Goldinger
d3e8d35e5d Release v0.3.11-beta05 2021-05-08 15:40:58 +02:00
Patrick Goldinger
da8073141e Fix dynamic text size infinite loop bug (#825) 2021-05-08 15:35:16 +02:00
Patrick Goldinger
030665732a Merge pull request #817 from florisboard/user-dictionary
System / internal user dictionary
2021-05-08 11:22:32 +02:00
Patrick Goldinger
cc042dd77c Add input validation for user dictionary add/edit dialogs 2021-05-07 20:52:30 +02:00
Patrick Goldinger
773624769d Add shortcut support for user dictionary 2021-05-07 19:21:15 +02:00
Patrick Goldinger
0b1d0c74fe Fix query syntax issues for the system user dictionary 2021-05-07 04:22:17 +02:00
Patrick Goldinger
760d307478 Improve user dictionary UI 2021-05-07 04:01:47 +02:00
Patrick Goldinger
084c2abfc2 Add user dictionary manager UI for system and internal 2021-05-07 03:51:40 +02:00
Patrick Goldinger
df6b08024f Fix SQL user input causing crash 2021-05-06 19:04:36 +02:00
Patrick Goldinger
25498695ef Add basic UI wrapper for managing user dictionaries 2021-05-06 18:16:38 +02:00
Patrick Goldinger
5c81179d60 Add import/export backend logic for user dictionaries 2021-05-06 18:16:01 +02:00
Patrick Goldinger
58d150bb03 Update Kotlin to 1.5.0 2021-05-06 01:05:45 +02:00
Patrick Goldinger
2b1951ea5f Add internal and system user dictionary 2021-05-05 21:07:59 +02:00
Patrick Goldinger
5a5089c413 Fix AppCompat theme crash for Huawei devices (#799, #809) 2021-05-05 20:34:16 +02:00
Patrick Goldinger
dcd20e4b73 Add user dictionary preferences 2021-05-05 18:32:20 +02:00
Patrick Goldinger
dfec1f3804 Release v0.3.11-beta04 2021-05-04 20:37:39 +02:00
Patrick Goldinger
1fffe7f6e5 Fix ؤ Arabic Letter Waw with Hamza Above not written correctly (#438) 2021-05-04 19:48:54 +02:00
Patrick Goldinger
862a6cc82a Fix font size multiplier and also scale drawables (#540) 2021-05-04 18:56:21 +02:00
Patrick Goldinger
068caaf09b Add schwa symbol (ə) in Italian extended popups (#693) 2021-05-04 18:04:20 +02:00
Patrick Goldinger
93fb6d6016 Fix English (US) store description being cut off in F-Droid (#709) 2021-05-04 17:55:25 +02:00
Patrick Goldinger
28f0657bd7 Improve and fix KeyboardIconSet (#778) 2021-05-04 17:46:06 +02:00
Patrick Goldinger
8c53c2a057 Fix Hungarian layout not containing special keys 2021-05-04 17:34:32 +02:00
Patrick Goldinger
6251fb2ef6 Fix bottom row keys not shifted in Dvorak layout (#805) 2021-05-04 17:07:50 +02:00
Patrick Goldinger
cba2b873b8 Add devtool overlay for heap memory usage 2021-05-04 16:55:14 +02:00
Patrick Goldinger
d7ee61f316 Merge pull request #718 from X-yl/emoji-key-mem
Recycle EmojiKeyViews to reduce memory usage
2021-05-04 15:17:00 +02:00
X-yl
cf309f43a4 Recycle EmojiKeyViews for 15%-20% memory savings
Instead of creating an EmojiKeyView for every emoji, you can use a
RecyclerView to only create the ones which are visible on screen, and
then reuse them later.
2021-05-04 09:48:31 +04:00
Patrick Goldinger
93acee778e Release v0.3.11-beta03 2021-05-03 20:52:30 +02:00
Patrick Goldinger
c7f2f31c99 Fix gestures (except space+shift) 2021-05-03 20:11:08 +02:00
Patrick Goldinger
ebb8837d8a Fix Double NaN crashes (#774, #790) 2021-05-03 15:47:55 +02:00
Patrick Goldinger
f04f185034 Fix adaptive theme memory management (#763) 2021-05-02 12:17:35 +02:00
Patrick Goldinger
20de007d3b Add "Copied to system clipboard" toast to crash dialog (#724) 2021-05-01 12:20:56 +02:00
Patrick Goldinger
df01f6fe57 Fix theme manager buttons not wrapping (#777) 2021-05-01 11:51:15 +02:00
Patrick Goldinger
f9e6d7b09c Fix keyboard preview visual bugs (#776) 2021-05-01 11:51:15 +02:00
68 changed files with 2129 additions and 463 deletions

5
.gitignore vendored
View File

@@ -39,4 +39,7 @@ captures/
# Keystore files
*.jks
crowdin.properties
crowdin.properties
# AndroidX Room schema JSONs
/app/schemas/

View File

@@ -1,9 +1,9 @@
plugins {
id("com.android.application") version "4.1.3"
kotlin("android") version "1.5.0-RC"
kotlin("kapt") version "1.5.0-RC"
kotlin("plugin.serialization") version "1.5.0-RC"
id("com.android.application") version "4.2.0"
kotlin("android") version "1.5.0"
kotlin("kapt") version "1.5.0"
kotlin("plugin.serialization") version "1.5.0"
}
android {
@@ -24,10 +24,20 @@ android {
applicationId = "dev.patrickgold.florisboard"
minSdkVersion(23)
targetSdkVersion(30)
versionCode(37)
versionCode(40)
versionName("0.3.11")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions {
annotationProcessorOptions {
arguments += mapOf(
Pair("room.schemaLocation", "$projectDir/schemas"),
Pair("room.incremental", "true"),
Pair("room.expandProjection", "true")
)
}
}
}
buildFeatures {
@@ -47,7 +57,7 @@ android {
create("beta") // Needed because by default the "beta" BuildType does not exist
named("beta").configure {
applicationIdSuffix = ".beta"
versionNameSuffix = "-beta02"
versionNameSuffix = "-beta05"
proguardFiles.add(getDefaultProguardFile("proguard-android-optimize.txt"))
resValue("mipmap", "floris_app_icon", "@mipmap/ic_app_icon_beta")
@@ -84,7 +94,7 @@ dependencies {
implementation("androidx.preference", "preference-ktx", "1.1.1")
implementation("androidx.constraintlayout", "constraintlayout", "2.0.4")
implementation("androidx.lifecycle", "lifecycle-service", "2.2.0")
implementation("com.google.android", "flexbox", "2.0.1") // requires jcenter as of version 2.0.1
implementation("com.google.android", "flexbox", "2.0.1")
implementation("com.google.android.material", "material", "1.3.0")
implementation("org.jetbrains.kotlinx", "kotlinx-coroutines-android", "1.4.2")
implementation("org.jetbrains.kotlinx", "kotlinx-serialization-json", "1.1.0")

View File

@@ -66,6 +66,13 @@
</intent-filter>
</activity-alias>
<!-- User Dictionary Manager Activity -->
<activity
android:name="dev.patrickgold.florisboard.settings.UdmActivity"
android:icon="@mipmap/floris_app_icon"
android:label="@string/settings__title"
android:theme="@style/SettingsTheme"/>
<!-- Theme Selector Activity -->
<activity
android:name="dev.patrickgold.florisboard.settings.ThemeManagerActivity"

View File

@@ -35,7 +35,7 @@
[
{ "code": 1584, "label": "ذ" },
{ "code": 1569, "label": "ء" },
{ "code": 65157, "label": "" },
{ "code": 1572, "label": "ؤ" },
{ "code": 1585, "label": "ر" },
{ "code": 1609, "label": "ى" },
{ "code": 1577, "label": "ة" },

View File

@@ -20,9 +20,10 @@
"e": {
"main": { "$": "auto_text_key", "code": 232, "label": "è" },
"relevant": [
{ "$": "auto_text_key", "code": 275, "label": "ē" },
{ "$": "auto_text_key", "code": 281, "label": "ę" },
{ "$": "auto_text_key", "code": 279, "label": "ė" },
{ "$": "auto_text_key", "code": 601, "label": "ə" },
{ "$": "auto_text_key", "code": 281, "label": "ę" },
{ "$": "auto_text_key", "code": 275, "label": "ē" },
{ "$": "auto_text_key", "code": 234, "label": "ê" },
{ "$": "auto_text_key", "code": 233, "label": "é" },
{ "$": "auto_text_key", "code": 235, "label": "ë" }

View File

@@ -1,8 +1,8 @@
{
"type": "characters",
"name": "hungarian",
"label": "Hungarian (QWERTZ)",
"authors": [ "zoli111, gabik65" ],
"label": "Hungarian",
"authors": [ "zoli111, gabik65", "patrickgold" ],
"direction": "ltr",
"arrangement": [
[
@@ -15,7 +15,8 @@
{ "$": "auto_text_key", "code": 117, "label": "u" },
{ "$": "auto_text_key", "code": 105, "label": "i" },
{ "$": "auto_text_key", "code": 111, "label": "o" },
{ "$": "auto_text_key", "code": 112, "label": "p" }
{ "$": "auto_text_key", "code": 112, "label": "p" },
{ "$": "auto_text_key", "code": 246, "label": "ö" }
],
[
{ "$": "auto_text_key", "code": 97, "label": "a" },
@@ -26,7 +27,9 @@
{ "$": "auto_text_key", "code": 104, "label": "h" },
{ "$": "auto_text_key", "code": 106, "label": "j" },
{ "$": "auto_text_key", "code": 107, "label": "k" },
{ "$": "auto_text_key", "code": 108, "label": "l" }
{ "$": "auto_text_key", "code": 108, "label": "l" },
{ "$": "auto_text_key", "code": 233, "label": "é" },
{ "$": "auto_text_key", "code": 225, "label": "á" }
],
[
{ "$": "auto_text_key", "code": 121, "label": "y" },
@@ -35,7 +38,8 @@
{ "$": "auto_text_key", "code": 118, "label": "v" },
{ "$": "auto_text_key", "code": 98, "label": "b" },
{ "$": "auto_text_key", "code": 110, "label": "n" },
{ "$": "auto_text_key", "code": 109, "label": "m" }
{ "$": "auto_text_key", "code": 109, "label": "m" },
{ "$": "auto_text_key", "code": 252, "label": "ü" }
]
]
}

View File

@@ -12,11 +12,11 @@
],
[
{ "code": -202, "label": "view_symbols", "type": "system_gui" },
{ "code": 113, "label": "q", "groupId": 1 },
{ "$": "auto_text_key", "code": 113, "label": "q", "groupId": 1 },
{ "code": -210, "label": "language_switch", "type": "system_gui" },
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui" },
{ "code": 32, "label": "space" },
{ "code": 122, "label": "z", "groupId": 2 },
{ "$": "auto_text_key", "code": 122, "label": "z", "groupId": 2 },
{ "code": 10, "label": "enter", "groupId": 3, "type": "enter_editing" }
]
]

View File

@@ -20,13 +20,13 @@ import android.content.*
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.databinding.CrashDialogBinding
import dev.patrickgold.florisboard.debug.*
import dev.patrickgold.florisboard.ime.core.PrefHelper
import java.util.*
class CrashDialogActivity : AppCompatActivity() {
private lateinit var binding: CrashDialogBinding
@@ -93,9 +93,13 @@ class CrashDialogActivity : AppCompatActivity() {
binding.copyToClipboard.setOnClickListener {
val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE)
if (clipboardManager != null && clipboardManager is ClipboardManager) {
val toastMessage: String = if (clipboardManager != null && clipboardManager is ClipboardManager) {
clipboardManager.setPrimaryClip(ClipData.newPlainText(errorReport, errorReport))
resources.getString(R.string.crash_dialog__copy_to_clipboard_success)
} else {
resources.getString(R.string.crash_dialog__copy_to_clipboard_failure)
}
Toast.makeText(this, toastMessage, Toast.LENGTH_SHORT).show()
}
binding.openBugReportForm.setOnClickListener {
@@ -131,10 +135,10 @@ class CrashDialogActivity : AppCompatActivity() {
private fun getDeviceName(): String {
val manufacturer = Build.MANUFACTURER
val model = Build.MODEL
return if (model.toLowerCase(Locale.ENGLISH).startsWith(manufacturer.toLowerCase(Locale.ENGLISH))) {
model.capitalize(Locale.ENGLISH)
return if (model.lowercase().startsWith(manufacturer.lowercase())) {
model.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
} else {
"${manufacturer.capitalize(Locale.ENGLISH)} $model"
"${manufacturer.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }} $model"
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
@file:OptIn(ExperimentalContracts::class, ExperimentalUnsignedTypes::class)
@file:OptIn(ExperimentalContracts::class)
package dev.patrickgold.florisboard.debug

View File

@@ -14,8 +14,6 @@
* limitations under the License.
*/
@file:OptIn(ExperimentalUnsignedTypes::class)
package dev.patrickgold.florisboard.debug
/**
@@ -37,6 +35,7 @@ object LogTopic {
const val SUBTYPE_MANAGER: FlogTopic = 4u
const val LAYOUT_MANAGER: FlogTopic = 8u
const val TEXT_KEYBOARD_VIEW: FlogTopic = 16u
const val GESTURES: FlogTopic = 32u
const val GLIDE: FlogTopic = 512u
const val CLIPBOARD: FlogTopic = 1024u

View File

@@ -102,7 +102,7 @@ abstract class FlorisActivity<V : ViewBinding> : AppCompatActivity(), CoroutineS
errorDialog?.dismiss()
errorDialog = AlertDialog.Builder(this@FlorisActivity).run {
setTitle(R.string.assets__error__details)
setMessage(errorThrowable.toString())
setMessage(errorThrowable?.stackTraceToString())
setPositiveButton(android.R.string.ok, null)
setNeutralButton(R.string.crash_dialog__copy_to_clipboard) { _, _ ->
val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE)

View File

@@ -57,7 +57,7 @@ import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import dev.patrickgold.florisboard.setup.SetupActivity
import dev.patrickgold.florisboard.util.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
@@ -84,13 +84,14 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
private val serviceLifecycleDispatcher: ServiceLifecycleDispatcher = ServiceLifecycleDispatcher(this)
private val uiScope: LifecycleCoroutineScope
get() = lifecycle.coroutineScope
private var devtoolsOverlaySyncJob: Job? = null
/**
* The theme context for the UI. Must only be used for inflating/creating Views for the keyboard UI, else the IME
* service class should be used directly.
*/
private var _themeContext: Context? = null
private val themeContext: Context
val themeContext: Context
get() = _themeContext ?: this
lateinit var prefs: PrefHelper
@@ -147,7 +148,6 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
lateinit var asyncExecutor: ExecutorService
companion object {
@Synchronized
fun getInstance(): FlorisBoard {
return florisboardInstance!!
@@ -393,6 +393,17 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
onSubtypeChanged(activeSubtype)
setActiveInput(R.id.text_input)
if (prefs.devtools.enabled && prefs.devtools.showHeapMemoryStats) {
devtoolsOverlaySyncJob?.cancel()
devtoolsOverlaySyncJob = uiScope.launch(Dispatchers.Default) {
while (true) {
if (!isActive) break
withContext(Dispatchers.Main) { inputView?.invalidate() }
delay(1000)
}
}
}
eventListeners.toList().forEach { it?.onWindowShown() }
}
@@ -406,6 +417,9 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
}
isWindowShown = false
devtoolsOverlaySyncJob?.cancel()
devtoolsOverlaySyncJob = null
eventListeners.toList().forEach { it?.onWindowHidden() }
}

View File

@@ -18,6 +18,11 @@ package dev.patrickgold.florisboard.ime.core
import android.content.Context
import android.content.res.Configuration
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Typeface
import android.text.TextPaint
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.view.ViewGroup
@@ -58,6 +63,13 @@ class InputView : LinearLayout {
var oneHandedCtrlPanelEnd: ViewGroup? = null
private set
private val overlayTextPaint: TextPaint = TextPaint().apply {
color = Color.GREEN
textAlign = Paint.Align.RIGHT
textSize = resources.getDimension(R.dimen.devtools_memory_overlay_textSize)
typeface = Typeface.MONOSPACE
}
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
@@ -161,4 +173,34 @@ class InputView : LinearLayout {
resources.getDimension(R.dimen.inputView_baseHeight)
)
}
override fun dispatchDraw(canvas: Canvas?) {
super.dispatchDraw(canvas)
canvas ?: return
if (prefs.devtools.enabled && prefs.devtools.showHeapMemoryStats) {
try {
// Note: the below code only gets the heap size in MB, the actual RAM usage (native or others) can be
// a lot higher
// Source: https://stackoverflow.com/a/19267315/6801193
val runtime = Runtime.getRuntime()
val usedMemInMB = (runtime.totalMemory() - runtime.freeMemory()) / 1048576L
val maxHeapSizeInMB = runtime.maxMemory() / 1048576L
val availHeapSizeInMB = maxHeapSizeInMB - usedMemInMB
val output = listOf(
"heap mem:",
String.format("used=%4dMB", usedMemInMB),
String.format("max=%4dMB", maxHeapSizeInMB),
String.format("avail=%4dMB", availHeapSizeInMB),
)
val x = measuredWidth.toFloat()
var y = overlayTextPaint.descent() - overlayTextPaint.ascent()
for (line in output) {
canvas.drawText(line, x, y, overlayTextPaint)
y += overlayTextPaint.descent() - overlayTextPaint.ascent()
}
} catch (_: Throwable) {
}
}
}
}

View File

@@ -48,7 +48,10 @@ class PrefHelper(
private val cacheString: HashMap<String, String> = hashMapOf()
val advanced = Advanced(this)
val clipboard = Clipboard(this)
val correction = Correction(this)
val devtools = Devtools(this)
val dictionary = Dictionary(this)
val gestures = Gestures(this)
val glide = Glide(this)
val internal = Internal(this)
@@ -57,7 +60,6 @@ class PrefHelper(
val smartbar = Smartbar(this)
val suggestion = Suggestion(this)
val theme = Theme(this)
val clipboard = Clipboard(this)
/**
* Checks the cache if an entry for [key] exists, else calls [getPrefInternal] to retrieve the
@@ -222,6 +224,43 @@ class PrefHelper(
set(v) = prefHelper.setPref(REMEMBER_CAPS_LOCK_STATE, v)
}
/**
* Wrapper class for devtools preferences.
*/
class Devtools(private val prefHelper: PrefHelper) {
companion object {
const val ENABLED = "devtools__enabled"
const val SHOW_HEAP_MEMORY_STATS = "devtools__show_heap_memory_stats"
const val CLEAR_UDM_INTERNAL_DATABASE = "devtools__clear_udm_internal_database"
}
var enabled: Boolean
get() = prefHelper.getPref(ENABLED, false)
set(v) = prefHelper.setPref(ENABLED, v)
var showHeapMemoryStats: Boolean
get() = prefHelper.getPref(SHOW_HEAP_MEMORY_STATS, false)
set(v) = prefHelper.setPref(SHOW_HEAP_MEMORY_STATS, v)
}
/**
* Wrapper class for dictionary preferences.
*/
class Dictionary(private val prefHelper: PrefHelper) {
companion object {
const val ENABLE_SYSTEM_USER_DICTIONARY = "suggestion__enable_system_user_dictionary"
const val MANAGE_SYSTEM_USER_DICTIONARY = "suggestion__manage_system_user_dictionary"
const val ENABLE_FLORIS_USER_DICTIONARY = "suggestion__enable_floris_user_dictionary"
const val MANAGE_FLORIS_USER_DICTIONARY = "suggestion__manage_floris_user_dictionary"
}
var enableSystemUserDictionary: Boolean
get() = prefHelper.getPref(ENABLE_SYSTEM_USER_DICTIONARY, true)
set(v) = prefHelper.setPref(ENABLE_SYSTEM_USER_DICTIONARY, v)
var enableFlorisUserDictionary: Boolean
get() = prefHelper.getPref(ENABLE_FLORIS_USER_DICTIONARY, true)
set(v) = prefHelper.setPref(ENABLE_FLORIS_USER_DICTIONARY, v)
}
/**
* Wrapper class for gestures preferences.
*/

View File

@@ -17,15 +17,24 @@
package dev.patrickgold.florisboard.ime.dictionary
import android.content.Context
import androidx.room.Room
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.extension.AssetRef
import timber.log.Timber
import java.lang.ref.WeakReference
/**
* TODO: document
*/
class DictionaryManager private constructor(private val applicationContext: Context) {
class DictionaryManager private constructor(context: Context) {
private val applicationContext: WeakReference<Context> = WeakReference(context.applicationContext ?: context)
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
private val dictionaryCache: MutableMap<String, Dictionary<String, Int>> = mutableMapOf()
private var florisUserDictionaryDatabase: FlorisUserDictionaryDatabase? = null
private var systemUserDictionaryDatabase: SystemUserDictionaryDatabase? = null
companion object {
private var defaultInstance: DictionaryManager? = null
@@ -53,16 +62,83 @@ class DictionaryManager private constructor(private val applicationContext: Cont
}
if (ref.path.endsWith(".flict")) {
// Assume this is a Flictionary
Flictionary.load(applicationContext, ref).onSuccess { flict ->
dictionaryCache[ref.toString()] = flict
return Result.success(flict)
}.onFailure { err ->
Timber.i(err)
return Result.failure(err)
applicationContext.get()?.let {
Flictionary.load(it, ref).onSuccess { flict ->
dictionaryCache[ref.toString()] = flict
return Result.success(flict)
}.onFailure { err ->
Timber.i(err)
return Result.failure(err)
}
}
} else {
return Result.failure(Exception("Unable to determine supported type for given AssetRef!"))
}
return Result.failure(Exception("If this message is ever thrown, something is completely broken..."))
}
@Synchronized
fun florisUserDictionaryDao(): UserDictionaryDao? {
return if (prefs.suggestion.enabled && prefs.dictionary.enableFlorisUserDictionary) {
florisUserDictionaryDatabase?.userDictionaryDao()
} else {
null
}
}
@Synchronized
fun florisUserDictionaryDatabase(): FlorisUserDictionaryDatabase? {
return if (prefs.suggestion.enabled && prefs.dictionary.enableFlorisUserDictionary) {
florisUserDictionaryDatabase
} else {
null
}
}
@Synchronized
fun systemUserDictionaryDao(): UserDictionaryDao? {
return if (prefs.suggestion.enabled && prefs.dictionary.enableSystemUserDictionary) {
systemUserDictionaryDatabase?.userDictionaryDao()
} else {
null
}
}
@Synchronized
fun systemUserDictionaryDatabase(): SystemUserDictionaryDatabase? {
return if (prefs.suggestion.enabled && prefs.dictionary.enableSystemUserDictionary) {
systemUserDictionaryDatabase
} else {
null
}
}
@Synchronized
fun loadUserDictionariesIfNecessary() {
val context = applicationContext.get() ?: return
if (prefs.suggestion.enabled) {
if (florisUserDictionaryDatabase == null && prefs.dictionary.enableFlorisUserDictionary) {
florisUserDictionaryDatabase = Room.databaseBuilder(
context,
FlorisUserDictionaryDatabase::class.java,
FlorisUserDictionaryDatabase.DB_FILE_NAME
).allowMainThreadQueries().build()
}
if (systemUserDictionaryDatabase == null && prefs.dictionary.enableSystemUserDictionary) {
systemUserDictionaryDatabase = SystemUserDictionaryDatabase(context)
}
}
}
@Synchronized
fun unloadUserDictionariesIfNecessary() {
if (florisUserDictionaryDatabase != null) {
florisUserDictionaryDatabase?.close()
florisUserDictionaryDatabase = null
}
if (systemUserDictionaryDatabase != null) {
systemUserDictionaryDatabase = null
}
}
}

View File

@@ -21,7 +21,6 @@ import dev.patrickgold.florisboard.ime.extension.AssetRef
import dev.patrickgold.florisboard.ime.extension.AssetSource
import dev.patrickgold.florisboard.ime.nlp.*
import java.io.InputStream
import java.util.*
import kotlin.jvm.Throws
/**
@@ -307,7 +306,7 @@ class Flictionary private constructor(
return if (currentToken.data.isNotEmpty()) {
val retList = languageModel.matchAllNgrams(
ngram = Ngram(
_tokens = listOf(Token(currentToken.data.toLowerCase(Locale.ENGLISH))),
_tokens = listOf(Token(currentToken.data.lowercase())),
_freq = -1
),
maxEditDistance = 2,

View File

@@ -0,0 +1,445 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.dictionary
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.UserDictionary
import androidx.room.ColumnInfo
import androidx.room.Dao
import androidx.room.Database
import androidx.room.Delete
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.RoomDatabase
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import androidx.room.Update
import dev.patrickgold.florisboard.ime.extension.ExternalContentUtils
import dev.patrickgold.florisboard.util.LocaleUtils
import java.lang.ref.WeakReference
import java.util.*
private const val WORDS_TABLE = "words"
const val FREQUENCY_MIN = 1
const val FREQUENCY_MAX = 255
const val FREQUENCY_DEFAULT = 128
private const val SORT_BY_WORD_ASC = "${UserDictionary.Words.WORD} ASC"
private const val SORT_BY_WORD_DESC = "${UserDictionary.Words.WORD} DESC"
private const val SORT_BY_FREQ_ASC = "${UserDictionary.Words.FREQUENCY} ASC"
private const val SORT_BY_FREQ_DESC = "${UserDictionary.Words.FREQUENCY} DESC"
private val PROJECTIONS: Array<String> = arrayOf(
UserDictionary.Words._ID,
UserDictionary.Words.WORD,
UserDictionary.Words.FREQUENCY,
UserDictionary.Words.LOCALE,
UserDictionary.Words.SHORTCUT,
)
private val PROJECTIONS_LANGUAGE: Array<String> = arrayOf(
UserDictionary.Words.LOCALE,
)
@Entity(tableName = WORDS_TABLE)
data class UserDictionaryEntry(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = UserDictionary.Words._ID, index = true)
val id: Long,
@ColumnInfo(name = UserDictionary.Words.WORD)
val word: String,
@ColumnInfo(name = UserDictionary.Words.FREQUENCY)
val freq: Int,
@ColumnInfo(name = UserDictionary.Words.LOCALE)
val locale: String?,
@ColumnInfo(name = UserDictionary.Words.SHORTCUT)
val shortcut: String?,
)
@Dao
interface UserDictionaryDao {
companion object {
private const val SELECT_ALL_FROM_WORDS =
"SELECT * FROM $WORDS_TABLE"
private const val LOCALE_MATCHES =
"(${UserDictionary.Words.LOCALE} = :locale OR ${UserDictionary.Words.LOCALE} IS NULL)"
}
@Query("$SELECT_ALL_FROM_WORDS WHERE ${UserDictionary.Words.WORD} LIKE '%' || :word || '%'")
fun query(word: String): List<UserDictionaryEntry>
@Query("$SELECT_ALL_FROM_WORDS WHERE ${UserDictionary.Words.WORD} LIKE '%' || :word || '%' AND $LOCALE_MATCHES")
fun query(word: String, locale: Locale?): List<UserDictionaryEntry>
@Query("$SELECT_ALL_FROM_WORDS WHERE ${UserDictionary.Words.SHORTCUT} = :shortcut")
fun queryShortcut(shortcut: String): List<UserDictionaryEntry>
@Query("$SELECT_ALL_FROM_WORDS WHERE ${UserDictionary.Words.SHORTCUT} = :shortcut AND $LOCALE_MATCHES")
fun queryShortcut(shortcut: String, locale: Locale?): List<UserDictionaryEntry>
@Query(SELECT_ALL_FROM_WORDS)
fun queryAll(): List<UserDictionaryEntry>
@Query("$SELECT_ALL_FROM_WORDS WHERE (${UserDictionary.Words.LOCALE} = :locale AND :locale IS NOT NULL) OR (${UserDictionary.Words.LOCALE} IS NULL AND :locale IS NULL)")
fun queryAll(locale: Locale?): List<UserDictionaryEntry>
@Query("$SELECT_ALL_FROM_WORDS WHERE ${UserDictionary.Words.WORD} = :word")
fun queryExact(word: String): List<UserDictionaryEntry>
@Query("$SELECT_ALL_FROM_WORDS WHERE ${UserDictionary.Words.WORD} = :word AND (${UserDictionary.Words.LOCALE} = :locale OR (${UserDictionary.Words.LOCALE} IS NULL AND :locale IS NULL))")
fun queryExact(word: String, locale: Locale?): List<UserDictionaryEntry>
@Query("SELECT DISTINCT ${UserDictionary.Words.LOCALE} FROM $WORDS_TABLE")
fun queryLanguageList(): List<Locale?>
@Insert
fun insert(entry: UserDictionaryEntry)
@Update
fun update(entry: UserDictionaryEntry)
@Delete
fun delete(entry: UserDictionaryEntry)
@Query("DELETE FROM $WORDS_TABLE")
fun deleteAll()
}
interface UserDictionaryDatabase {
fun userDictionaryDao(): UserDictionaryDao
fun reset()
fun importCombinedList(context: Context, uri: Uri): Result<Unit> {
return ExternalContentUtils.readFromUri(context, uri,2048) { src ->
var isFirstLine = true
src.forEachLine { line ->
if (isFirstLine) {
// Ignore
isFirstLine = false
} else {
var word: String? = null
var freq: Int? = null
var locale: String? = null
var shortcut: String? = null
line.split(';').forEach { property ->
val keyValuePair = property.split('=')
if (keyValuePair.size == 2) {
val key = keyValuePair[0].trim().lowercase()
val value = keyValuePair[1].trim()
when (key) {
"w", "word" -> word = value.ifBlank { null }
"f", "freq" -> runCatching { value.toInt(10) }.onSuccess {
freq = it.coerceIn(FREQUENCY_MIN, FREQUENCY_MAX)
}
"l", "locale" -> locale = when (value) {
"all", "null", "" -> null
else -> value.ifBlank { null }
}
"s", "shortcut" -> shortcut = value.ifBlank { null }
}
}
}
if (word != null && freq != null) {
val alreadyExistingEntries = userDictionaryDao().queryExact(
word!!, locale?.let { LocaleUtils.stringToLocale(it) }
)
if (alreadyExistingEntries.isNotEmpty()) {
userDictionaryDao().update(UserDictionaryEntry(alreadyExistingEntries[0].id, word!!, freq!!, locale, shortcut))
} else {
userDictionaryDao().insert(UserDictionaryEntry(0, word!!, freq!!, locale, shortcut))
}
}
}
}
}
}
fun exportCombinedList(context: Context, uri: Uri): Result<Unit> {
return ExternalContentUtils.writeToUri(context, uri) { dst ->
StringBuilder().apply {
append("dictionary=")
append(uri.lastPathSegment)
append(";date=")
append(System.currentTimeMillis())
append(";generated-by=")
append(context.packageName)
append(";version=1")
appendLine()
dst.write(toString())
}
for (entry in userDictionaryDao().queryAll()) {
StringBuilder().apply {
append(" w=")
append(entry.word)
append(";f=")
append(entry.freq)
append(";l=")
append(entry.locale) // always append locale even if null
if (entry.shortcut != null) {
append(";s=")
append(entry.shortcut)
}
appendLine()
dst.write(toString())
}
}
}
}
}
@Database(entities = [UserDictionaryEntry::class], version = 1)
@TypeConverters(FlorisUserDictionaryDatabase.Converters::class)
abstract class FlorisUserDictionaryDatabase : RoomDatabase(), UserDictionaryDatabase {
companion object {
const val DB_FILE_NAME = "floris_user_dictionary"
}
abstract override fun userDictionaryDao(): UserDictionaryDao
override fun reset() {
TODO("Not yet implemented")
}
class Converters {
@TypeConverter
fun localeToString(locale: Locale?): String? {
return when (locale) {
null -> null
else -> locale.toString()
}
}
@TypeConverter
fun stringToLocale(string: String?): Locale? {
return when (string) {
null, "all", "null", "" -> null
else -> LocaleUtils.stringToLocale(string)
}
}
}
}
class SystemUserDictionaryDatabase(context: Context) : UserDictionaryDatabase {
private val applicationContext: WeakReference<Context> = WeakReference(context.applicationContext ?: context)
private val dao = object : UserDictionaryDao {
override fun query(word: String): List<UserDictionaryEntry> {
return queryResolver(
selection = "${UserDictionary.Words.WORD} LIKE ?",
selectionArgs = arrayOf("%$word%"),
sortOrder = SORT_BY_FREQ_DESC,
)
}
override fun query(word: String, locale: Locale?): List<UserDictionaryEntry> {
return if (locale == null) {
queryResolver(
selection = "${UserDictionary.Words.WORD} LIKE ? AND ${UserDictionary.Words.LOCALE} IS NULL",
selectionArgs = arrayOf("%$word%"),
sortOrder = SORT_BY_FREQ_DESC,
)
} else {
queryResolver(
selection = "${UserDictionary.Words.WORD} LIKE ? AND (${UserDictionary.Words.LOCALE} = ? OR ${UserDictionary.Words.LOCALE} = ? OR ${UserDictionary.Words.LOCALE} IS NULL)",
selectionArgs = arrayOf("%$word%", locale.toString(), locale.language.toString()),
sortOrder = SORT_BY_FREQ_DESC,
)
}
}
override fun queryShortcut(shortcut: String): List<UserDictionaryEntry> {
return queryResolver(
selection = "${UserDictionary.Words.SHORTCUT} = ?",
selectionArgs = arrayOf(shortcut),
sortOrder = SORT_BY_FREQ_DESC,
)
}
override fun queryShortcut(shortcut: String, locale: Locale?): List<UserDictionaryEntry> {
return if (locale == null) {
queryResolver(
selection = "${UserDictionary.Words.SHORTCUT} = ? AND ${UserDictionary.Words.LOCALE} IS NULL",
selectionArgs = arrayOf(shortcut),
sortOrder = SORT_BY_FREQ_DESC,
)
} else {
queryResolver(
selection = "${UserDictionary.Words.SHORTCUT} = ? AND (${UserDictionary.Words.LOCALE} = ? OR ${UserDictionary.Words.LOCALE} = ? OR ${UserDictionary.Words.LOCALE} IS NULL)",
selectionArgs = arrayOf(shortcut, locale.toString(), locale.language.toString()),
sortOrder = SORT_BY_FREQ_DESC,
)
}
}
override fun queryAll(): List<UserDictionaryEntry> {
return queryResolver(
selection = null,
selectionArgs = null,
sortOrder = SORT_BY_FREQ_DESC,
)
}
override fun queryAll(locale: Locale?): List<UserDictionaryEntry> {
return if (locale == null) {
queryResolver(
selection = "${UserDictionary.Words.LOCALE} IS NULL",
selectionArgs = null,
sortOrder = SORT_BY_FREQ_DESC,
)
} else {
queryResolver(
selection = "${UserDictionary.Words.LOCALE} = ?",
selectionArgs = arrayOf(locale.toString()),
sortOrder = SORT_BY_FREQ_DESC,
)
}
}
override fun queryExact(word: String): List<UserDictionaryEntry> {
return queryResolver(
selection = "${UserDictionary.Words.WORD} = ?",
selectionArgs = arrayOf(word),
sortOrder = null,
)
}
override fun queryExact(word: String, locale: Locale?): List<UserDictionaryEntry> {
return if (locale == null) {
queryResolver(
selection = "${UserDictionary.Words.WORD} = ? AND ${UserDictionary.Words.LOCALE} IS NULL",
selectionArgs = arrayOf(word),
sortOrder = SORT_BY_FREQ_DESC,
)
} else {
queryResolver(
selection = "${UserDictionary.Words.WORD} LIKE ? AND ${UserDictionary.Words.LOCALE} = ?",
selectionArgs = arrayOf(word, locale.toString()),
sortOrder = SORT_BY_FREQ_DESC,
)
}
}
override fun queryLanguageList(): List<Locale?> {
val resolver = applicationContext.get()?.contentResolver ?: return listOf()
val cursor = resolver.query(
UserDictionary.Words.CONTENT_URI,
PROJECTIONS_LANGUAGE,
null,
null,
null
) ?: return listOf()
if (cursor.count <= 0) {
return listOf()
}
val localeIndex = cursor.getColumnIndex(UserDictionary.Words.LOCALE)
val retList = mutableSetOf<Locale?>()
while (cursor.moveToNext()) {
val localeStr = cursor.getString(localeIndex)
if (localeStr == null) {
retList.add(null)
} else {
retList.add(LocaleUtils.stringToLocale(localeStr))
}
}
cursor.close()
return retList.toList()
}
private fun queryResolver(selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): List<UserDictionaryEntry> {
val resolver = applicationContext.get()?.contentResolver ?: return listOf()
val cursor = resolver.query(
UserDictionary.Words.CONTENT_URI,
PROJECTIONS,
selection,
selectionArgs,
sortOrder
) ?: return listOf()
return parseEntries(cursor).also { cursor.close() }
}
private fun parseEntries(cursor: Cursor): List<UserDictionaryEntry> {
if (cursor.count <= 0) {
return listOf()
}
val idIndex = cursor.getColumnIndex(UserDictionary.Words._ID)
val wordIndex = cursor.getColumnIndex(UserDictionary.Words.WORD)
val freqIndex = cursor.getColumnIndex(UserDictionary.Words.FREQUENCY)
val localeIndex = cursor.getColumnIndex(UserDictionary.Words.LOCALE)
val shortcutIndex = cursor.getColumnIndex(UserDictionary.Words.SHORTCUT)
val retList = mutableListOf<UserDictionaryEntry>()
while (cursor.moveToNext()) {
retList.add(
UserDictionaryEntry(
id = cursor.getLong(idIndex),
word = cursor.getString(wordIndex),
freq = cursor.getInt(freqIndex),
locale = cursor.getString(localeIndex),
shortcut = cursor.getString(shortcutIndex)
)
)
}
return retList
}
override fun insert(entry: UserDictionaryEntry) {
val resolver = applicationContext.get()?.contentResolver ?: return
val contentValues = ContentValues(5).apply {
put(UserDictionary.Words.WORD, entry.word)
put(UserDictionary.Words.FREQUENCY, entry.freq)
put(UserDictionary.Words.LOCALE, entry.locale)
put(UserDictionary.Words.APP_ID, 0)
put(UserDictionary.Words.SHORTCUT, entry.shortcut)
}
resolver.insert(UserDictionary.Words.CONTENT_URI, contentValues)
}
override fun update(entry: UserDictionaryEntry) {
val resolver = applicationContext.get()?.contentResolver ?: return
val contentValues = ContentValues(4).apply {
put(UserDictionary.Words.WORD, entry.word)
put(UserDictionary.Words.FREQUENCY, entry.freq)
put(UserDictionary.Words.LOCALE, entry.locale)
put(UserDictionary.Words.SHORTCUT, entry.shortcut)
}
resolver.update(UserDictionary.Words.CONTENT_URI, contentValues, "${UserDictionary.Words._ID} = ${entry.id}", null)
}
override fun delete(entry: UserDictionaryEntry) {
val resolver = applicationContext.get()?.contentResolver ?: return
resolver.delete(UserDictionary.Words.CONTENT_URI, "${UserDictionary.Words._ID} = ${entry.id}", null)
}
override fun deleteAll() {
// Unsupported action
}
}
override fun userDictionaryDao(): UserDictionaryDao {
return dao
}
override fun reset() {
TODO("Not yet implemented")
}
}

View File

@@ -16,8 +16,6 @@
package dev.patrickgold.florisboard.ime.extension
import java.util.*
/**
* Sealed class which specifies where an asset comes from. There are 3 different types, all of which
* require a different approach on how to access the actual asset.
@@ -50,7 +48,7 @@ sealed class AssetSource {
private val externalRegex: Regex = """^external\\(([a-z]+\\.)*[a-z]+\\)\$""".toRegex()
fun fromString(str: String): Result<AssetSource> {
return when (val string = str.toLowerCase(Locale.ENGLISH)) {
return when (val string = str.lowercase()) {
"assets" -> Result.success(Assets)
"internal" -> Result.success(Internal)
else -> {

View File

@@ -14,35 +14,58 @@
* limitations under the License.
*/
@file:OptIn(ExperimentalContracts::class)
package dev.patrickgold.florisboard.ime.extension
import android.content.Context
import android.net.Uri
import java.io.BufferedReader
import java.io.BufferedWriter
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
class ExternalContentUtils private constructor() {
companion object {
fun readTextFromUri(context: Context, uri: Uri, maxSize: Int): Result<String> {
val contentResolver = context.contentResolver
?: return Result.failure(NullPointerException("System content resolver not available"))
val inputStream = contentResolver.openInputStream(uri)
?: return Result.failure(NullPointerException("Cannot open input stream for given uri '$uri'"))
val assetFileDescriptor = contentResolver.openAssetFileDescriptor(uri, "r")
?: return Result.failure(NullPointerException("Cannot open asset file descriptor for given uri '$uri'"))
if (assetFileDescriptor.length > maxSize) {
return Result.failure(Exception("Contents of given uri '$uri' exceeds maximum size of $maxSize bytes!"))
inline fun <R> readFromUri(context: Context, uri: Uri, maxSize: Int, block: (it: BufferedReader) -> R): Result<R> {
contract {
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
}
return runCatching {
val contentResolver = context.contentResolver
?: throw NullPointerException("System content resolver not available")
val inputStream = contentResolver.openInputStream(uri)
?: throw NullPointerException("Cannot open input stream for given uri '$uri'")
val assetFileDescriptor = contentResolver.openAssetFileDescriptor(uri, "r")
?: throw NullPointerException("Cannot open asset file descriptor for given uri '$uri'")
if (assetFileDescriptor.length > maxSize) {
throw Exception("Contents of given uri '$uri' exceeds maximum size of $maxSize bytes!")
}
inputStream.bufferedReader(Charsets.UTF_8).use { block(it) }
}
}
fun readTextFromUri(context: Context, uri: Uri, maxSize: Int): Result<String> {
return readFromUri(context, uri, maxSize) { it.readText() }
}
inline fun writeToUri(context: Context, uri: Uri, block: (it: BufferedWriter) -> Unit): Result<Unit> {
contract {
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
}
return runCatching {
val contentResolver = context.contentResolver
?: throw NullPointerException("System content resolver not available")
// Must use "rwt" mode to ensure destination file length is truncated after writing.
val outputStream = contentResolver.openOutputStream(uri, "rwt")
?: throw NullPointerException("Cannot open output stream for given uri '$uri'")
outputStream.bufferedWriter(Charsets.UTF_8).use { block(it) }
}
val rawText = inputStream.bufferedReader(Charsets.UTF_8).use { it.readText() }
return Result.success(rawText)
}
fun writeTextToUri(context: Context, uri: Uri, text: String): Result<Unit> {
val contentResolver = context.contentResolver
?: return Result.failure(NullPointerException("System content resolver not available"))
// Must use "rwt" mode to ensure destination file length is truncated after writing.
val outputStream = contentResolver.openOutputStream(uri, "rwt")
?: return Result.failure(NullPointerException("Cannot open output stream for given uri '$uri'"))
outputStream.bufferedWriter(Charsets.UTF_8).use { it.write(text) }
return Result.success(Unit)
return writeToUri(context, uri) { it.write(text) }
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:OptIn(ExperimentalContracts::class)
package dev.patrickgold.florisboard.ime.keyboard
import android.graphics.drawable.Drawable
import androidx.annotation.DrawableRes
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
/**
* Abstract class definition for providing icons to a keyboard view. This class has been introduced to remove the need
* for keyboard vies to re-fetch drawable resources every time they draw on the canvas. The exact implementation is
* dependent on the subclass.
*/
abstract class KeyboardIconSet {
/**
* Get the drawable for the given [id].
*
* @param id The Android resource id of the drawable which should be returned.
*
* @return The drawable for given [id] or null if this icon set does not contain a drawable for this id.
*/
abstract fun getDrawable(@DrawableRes id: Int): Drawable?
/**
* Performs [block] on the drawable with the given [id]. If no drawable for the id exists,[block] will not be
* called at all.
*
* @param id The Android resource id of the drawable which should be used to execute block with.
* @param block The block which should be executed with the returned drawable.
*/
inline fun withDrawable(@DrawableRes id: Int, block: Drawable.() -> Unit) {
contract {
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
}
val drawable = getDrawable(id)
if (drawable != null) {
synchronized(drawable) {
block(drawable)
}
}
}
}

View File

@@ -37,6 +37,8 @@ abstract class KeyboardView : View, ThemeManager.OnThemeUpdatedListener {
protected val themeManager: ThemeManager?
get() = ThemeManager.defaultOrNull()
var isMeasured: Boolean = false
private set
protected var isTouchable: Boolean = true
protected val touchEventChannel: Channel<MotionEvent> = Channel(16)
protected val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())
@@ -85,6 +87,11 @@ abstract class KeyboardView : View, ThemeManager.OnThemeUpdatedListener {
protected abstract fun onTouchEventInternal(event: MotionEvent)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
isMeasured = true
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
onLayoutInternal()

View File

@@ -16,8 +16,6 @@
package dev.patrickgold.florisboard.ime.landscapeinput
import java.util.*
enum class LandscapeInputUiMode {
DYNAMICALLY_SHOW,
NEVER_SHOW,
@@ -25,7 +23,7 @@ enum class LandscapeInputUiMode {
companion object {
fun fromString(string: String): LandscapeInputUiMode {
return valueOf(string.toUpperCase(Locale.ENGLISH))
return valueOf(string.uppercase())
}
}
}

View File

@@ -161,9 +161,9 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
*/
private fun createTabViewFor(tab: Tab): LinearLayout {
return when (tab) {
Tab.EMOJI -> EmojiKeyboardView(florisboard)
Tab.EMOTICON -> EmoticonKeyboardView(florisboard)
else -> LinearLayout(florisboard).apply {
Tab.EMOJI -> EmojiKeyboardView(florisboard.themeContext)
Tab.EMOTICON -> EmoticonKeyboardView(florisboard.themeContext)
else -> LinearLayout(florisboard.themeContext).apply {
addView(TextView(context).apply {
text = "not yet implemented"
})

View File

@@ -16,8 +16,6 @@
package dev.patrickgold.florisboard.ime.media.emoji
import java.util.*
/**
* Enum for emoji category.
* List taken from https://unicode.org/Public/emoji/13.0/emoji-test.txt
@@ -39,7 +37,7 @@ enum class EmojiCategory {
companion object {
fun fromString(string: String): EmojiCategory {
return valueOf(string.replace(" & ", "_").toUpperCase(Locale.ENGLISH))
return valueOf(string.replace(" & ", "_").uppercase())
}
}
}

View File

@@ -26,6 +26,10 @@ class EmojiKey(override val data: KeyData) : Key(data) {
var computedPopups: PopupSet<EmojiKeyData> = PopupSet()
private set
companion object {
val EMPTY = EmojiKey(EmojiKeyData.EMPTY)
}
fun dummyCompute() {
computedData = data as? EmojiKeyData ?: computedData
computedPopups = PopupSet(relevant = (data as? EmojiKeyData)?.popup ?: listOf())

View File

@@ -0,0 +1,59 @@
package dev.patrickgold.florisboard.ime.media.emoji
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.google.android.flexbox.FlexboxLayoutManager
class EmojiKeyAdapter(
private val dataSet: List<EmojiKey>,
private val emojiKeyboardView: EmojiKeyboardView,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
private const val PLACEHOLDER_EMOJI_COUNT = 24
}
class EmojiKeyViewHolder(view: View) : RecyclerView.ViewHolder(view)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return EmojiKeyViewHolder(EmojiKeyView(emojiKeyboardView, EmojiKey(EmojiKeyData.EMPTY)))
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (position < dataSet.size) {
(holder.itemView as EmojiKeyView).key = dataSet[position]
holder.itemView.layoutParams = FlexboxLayoutManager.LayoutParams(
emojiKeyboardView.emojiKeyWidth, emojiKeyboardView.emojiKeyHeight
)
} else {
(holder.itemView as EmojiKeyView).key = EmojiKey.EMPTY
holder.itemView.layoutParams = FlexboxLayoutManager.LayoutParams(
emojiKeyboardView.emojiKeyWidth, 0
)
}
}
override fun getItemCount(): Int {
// Add empty placeholder emojis at the end so the grid view. Below is an illustration how
// the UI looks with and without an placeholder (e = emoji):
// Without placeholder With placeholder
// e e e e e e e e e e e e e e
// ............. .............
// e e e e e e e e e e e e e e
// e e e e e e e e
//
// Based on this SO's answer idea (by La Nube - Luis R. Díaz Muñiz):
// https://stackoverflow.com/a/31478004/6801193
//
// 24 items are chosen here because that's probably the max items that will be shown per
// row, even in landscape mode.
return dataSet.size + PLACEHOLDER_EMOJI_COUNT
}
override fun getItemViewType(position: Int): Int {
return 0
}
}

View File

@@ -41,13 +41,22 @@ class EmojiKeyData(
return null
}
private var string: String? = null
override fun asString(isForDisplay: Boolean): String {
return StringBuilder().run {
for (codePoint in codePoints) {
append(Character.toChars(codePoint))
if (string == null) {
string = StringBuilder().run {
for (codePoint in codePoints) {
append(Character.toChars(codePoint))
}
toString()
}
toString()
}
return string!!
}
companion object {
val EMPTY = EmojiKeyData(listOf())
}
override fun toString(): String {

View File

@@ -25,6 +25,7 @@ import android.view.Gravity
import android.view.MotionEvent
import android.widget.ScrollView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.PrefHelper
@@ -32,7 +33,9 @@ import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
/**
* View class for managing the rendering and the events of a single emoji keyboard key.
@@ -45,7 +48,7 @@ import kotlinx.coroutines.MainScope
@SuppressLint("ViewConstructor")
class EmojiKeyView(
private val emojiKeyboardView: EmojiKeyboardView,
val key: EmojiKey
key: EmojiKey
) : androidx.appcompat.widget.AppCompatTextView(emojiKeyboardView.context), CoroutineScope by MainScope(),
FlorisBoard.EventListener, ThemeManager.OnThemeUpdatedListener {
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
@@ -55,6 +58,12 @@ class EmojiKeyView(
private var osHandler: Handler? = null
private var triangleDrawable: Drawable? = null
var key: EmojiKey = key
set(value) {
field = value
text = value.data.asString(true)
}
init {
background = null
gravity = Gravity.CENTER
@@ -95,7 +104,7 @@ class EmojiKeyView(
osHandler = Handler()
}
osHandler?.postDelayed({
(parent.parent as ScrollView)
(parent as RecyclerView)
.requestDisallowInterceptTouchEvent(true)
emojiKeyboardView.isScrollBlocked = true
emojiKeyboardView.popupManager.show(key, KeyHintMode.DISABLED)
@@ -126,7 +135,8 @@ class EmojiKeyView(
emojiKeyboardView.popupManager.getActiveEmojiKeyData(key)
emojiKeyboardView.popupManager.hide()
if (event.actionMasked != MotionEvent.ACTION_CANCEL &&
retData != null && !isCancelled) {
retData != null && !isCancelled
) {
if (!emojiKeyboardView.isScrollBlocked) {
florisboard?.keyPressVibrate()
florisboard?.keyPressSound()

View File

@@ -21,13 +21,10 @@ import android.content.Context
import android.content.res.ColorStateList
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.*
import com.google.android.flexbox.FlexDirection
import com.google.android.flexbox.FlexWrap
import com.google.android.flexbox.FlexboxLayout
import com.google.android.flexbox.JustifyContent
import androidx.recyclerview.widget.RecyclerView
import com.google.android.flexbox.*
import com.google.android.material.tabs.TabLayout
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
@@ -51,12 +48,13 @@ class EmojiKeyboardView : LinearLayout, FlorisBoard.EventListener,
private var activeCategory: EmojiCategory = EmojiCategory.SMILEYS_EMOTION
private var emojiViewFlipper: ViewFlipper
private val emojiKeyWidth = resources.getDimension(R.dimen.emoji_key_width).toInt()
private val emojiKeyHeight = resources.getDimension(R.dimen.emoji_key_height).toInt()
val emojiKeyWidth = resources.getDimension(R.dimen.emoji_key_width).toInt()
val emojiKeyHeight = resources.getDimension(R.dimen.emoji_key_height).toInt()
private var layouts: Deferred<EmojiLayoutDataMap>
private val mainScope = MainScope()
private val tabLayout: TabLayout
private val uiLayouts = EnumMap<EmojiCategory, ScrollView>(EmojiCategory::class.java)
private val uiLayouts = EnumMap<EmojiCategory, RecyclerView>(EmojiCategory::class.java)
private val layoutAdapters = EnumMap<EmojiCategory, EmojiKeyAdapter>(EmojiCategory::class.java)
var isScrollBlocked: Boolean = false
var popupManager = PopupManager(this, florisboard?.popupLayerView)
@@ -83,18 +81,20 @@ class EmojiKeyboardView : LinearLayout, FlorisBoard.EventListener,
ViewGroup.inflate(context, R.layout.media_input_emoji_tabs, null) as TabLayout
tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
setActiveCategory(when (tab?.position) {
0 -> EmojiCategory.SMILEYS_EMOTION
1 -> EmojiCategory.PEOPLE_BODY
2 -> EmojiCategory.ANIMALS_NATURE
3 -> EmojiCategory.FOOD_DRINK
4 -> EmojiCategory.TRAVEL_PLACES
5 -> EmojiCategory.ACTIVITIES
6 -> EmojiCategory.OBJECTS
7 -> EmojiCategory.SYMBOLS
8 -> EmojiCategory.FLAGS
else -> EmojiCategory.SMILEYS_EMOTION
})
setActiveCategory(
when (tab?.position) {
0 -> EmojiCategory.SMILEYS_EMOTION
1 -> EmojiCategory.PEOPLE_BODY
2 -> EmojiCategory.ANIMALS_NATURE
3 -> EmojiCategory.FOOD_DRINK
4 -> EmojiCategory.TRAVEL_PLACES
5 -> EmojiCategory.ACTIVITIES
6 -> EmojiCategory.OBJECTS
7 -> EmojiCategory.SYMBOLS
8 -> EmojiCategory.FLAGS
else -> EmojiCategory.SMILEYS_EMOTION
}
)
}
override fun onTabReselected(tab: TabLayout.Tab?) {}
@@ -118,6 +118,8 @@ class EmojiKeyboardView : LinearLayout, FlorisBoard.EventListener,
override fun onDetachedFromWindow() {
themeManager.unregisterOnThemeUpdatedListener(this)
florisboard?.removeEventListener(this)
layoutAdapters.clear()
uiLayouts.clear()
super.onDetachedFromWindow()
}
@@ -127,11 +129,13 @@ class EmojiKeyboardView : LinearLayout, FlorisBoard.EventListener,
* when it attaches a built category layout to the view hierarchy.
*/
private suspend fun buildLayout() = withContext(Dispatchers.Default) {
val recycledViewPool = RecyclerView.RecycledViewPool()
recycledViewPool.setMaxRecycledViews(0, 64)
for (category in EmojiCategory.values()) {
val scrollView = buildLayoutForCategory(category)
uiLayouts[category] = scrollView
val recyclerView = buildLayoutForCategory(category, recycledViewPool)
uiLayouts[category] = recyclerView
withContext(Dispatchers.Main) {
emojiViewFlipper.addView(scrollView)
emojiViewFlipper.addView(recyclerView)
}
}
}
@@ -145,48 +149,27 @@ class EmojiKeyboardView : LinearLayout, FlorisBoard.EventListener,
*/
@SuppressLint("ClickableViewAccessibility")
private suspend fun buildLayoutForCategory(
category: EmojiCategory
): ScrollView = withContext(Dispatchers.Default) {
val scrollView = ScrollView(context)
scrollView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
val flexboxLayout = FlexboxLayout(context)
flexboxLayout.layoutParams =
LayoutParams(LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
flexboxLayout.flexDirection = FlexDirection.ROW
flexboxLayout.justifyContent = JustifyContent.SPACE_BETWEEN
flexboxLayout.flexWrap = FlexWrap.WRAP
for (emojiKeyData in layouts.await()[category].orEmpty()) {
val emojiKeyView =
EmojiKeyView(this@EmojiKeyboardView, EmojiKey(emojiKeyData).also { it.dummyCompute() })
emojiKeyView.layoutParams = FlexboxLayout.LayoutParams(
emojiKeyWidth, emojiKeyHeight
)
flexboxLayout.addView(emojiKeyView)
}
// Add empty placeholder emojis at the end so the grid view. Below is an illustration how
// the UI looks with and without an placeholder (e = emoji):
// Without placeholder With placeholder
// e e e e e e e e e e e e e e
// ............. .............
// e e e e e e e e e e e e e e
// e e e e e e e e
//
// Based on this SO's answer idea (by La Nube - Luis R. Díaz Muñiz):
// https://stackoverflow.com/a/31478004/6801193
//
// 24 items are chosen here because that's probably the max items that will be shown per
// row, even in landscape mode.
for (n in 0 until 24) {
val gridPlaceholderView = View(context).apply {
layoutParams = LayoutParams(emojiKeyWidth, 0)
}
flexboxLayout.addView(gridPlaceholderView)
}
scrollView.setOnTouchListener { _, _ ->
category: EmojiCategory,
recycledViewPool: RecyclerView.RecycledViewPool
): RecyclerView = withContext(Dispatchers.Default) {
val recyclerView = RecyclerView(context)
val layoutManager = FlexboxLayoutManager(context)
recyclerView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
layoutManager.justifyContent = JustifyContent.SPACE_BETWEEN
layoutManager.flexWrap = FlexWrap.WRAP
recyclerView.layoutManager = layoutManager
recyclerView.setRecycledViewPool(recycledViewPool)
val adapter = EmojiKeyAdapter(
layouts.await()[category].orEmpty().map { data -> EmojiKey(data).also { it.dummyCompute() } },
this@EmojiKeyboardView
)
layoutAdapters[category] = adapter
recyclerView.adapter = adapter
recyclerView.setOnTouchListener { _, _ ->
return@setOnTouchListener isScrollBlocked
}
scrollView.addView(flexboxLayout)
return@withContext scrollView
return@withContext recyclerView
}
/**
@@ -195,6 +178,8 @@ class EmojiKeyboardView : LinearLayout, FlorisBoard.EventListener,
* @param newActiveCategory The new active category.
*/
fun setActiveCategory(newActiveCategory: EmojiCategory) {
// setting adapter forces recyclerview to return its views, so it can be used for new category
uiLayouts[activeCategory]?.adapter = layoutAdapters[activeCategory]
emojiViewFlipper.displayedChild =
emojiViewFlipper.indexOfChild(uiLayouts[newActiveCategory])
activeCategory = newActiveCategory
@@ -219,9 +204,11 @@ class EmojiKeyboardView : LinearLayout, FlorisBoard.EventListener,
* of focus and prevents the HorizontalScrollView to scroll within this MotionEvent.
*/
fun dismissKeyView(keyView: EmojiKeyView) {
keyView.onTouchEvent(MotionEvent.obtain(
0, 0, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0
))
keyView.onTouchEvent(
MotionEvent.obtain(
0, 0, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0
)
)
isScrollBlocked = true
}

View File

@@ -106,7 +106,7 @@ fun parseRawEmojiSpecsFile(
if (line.startsWith(GROUP_IDENTIFIER, true)) {
// A new group begins
val rawGroupName = line.trim().substring(GROUP_IDENTIFIER.length)
if (rawGroupName.toUpperCase(Locale.ENGLISH) == "COMPONENT") {
if (rawGroupName.uppercase() == "COMPONENT") {
skipUntilNextGroup = true
continue
} else {
@@ -130,7 +130,7 @@ fun parseRawEmojiSpecsFile(
val dataC = data2[0].trim()
val dataQ = data2[1].trim()
val dataN = data[1].split(NAME_JUNK_SPLIT_REGEX)[1]
if (dataQ.toLowerCase(Locale.ENGLISH) == FULLY_QUALIFIED) {
if (dataQ.lowercase() == FULLY_QUALIFIED) {
// Only fully-qualified emojis are accepted
val dataCPs = dataC.split(" ")
val key = EmojiKeyData(listStringToListInt(dataCPs), dataN)

View File

@@ -42,6 +42,7 @@ import dev.patrickgold.florisboard.ime.text.layout.LayoutManager
import dev.patrickgold.florisboard.ime.text.smartbar.SmartbarView
import kotlinx.coroutines.*
import org.json.JSONArray
import java.util.*
import kotlin.math.roundToLong
/**
@@ -299,6 +300,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
keyboards.clear()
inputEventDispatcher.keyEventReceiver = null
inputEventDispatcher.close()
dictionaryManager.unloadUserDictionariesIfNecessary()
cancel()
layoutManager.onDestroy()
instance = null
@@ -375,6 +377,9 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
}
override fun onWindowShown() {
launch(Dispatchers.Default) {
dictionaryManager.loadUserDictionariesIfNecessary()
}
smartbarView?.updateSmartbarState()
}
@@ -446,12 +451,16 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
activeDictionary?.let {
launch(Dispatchers.Default) {
val startTime = System.nanoTime()
val suggestions = it.getTokenPredictions(
val suggestions = queryUserDictionary(
activeEditorInstance.cachedInput.currentWord.text,
florisboard.activeSubtype.locale
).toMutableList()
suggestions.addAll(it.getTokenPredictions(
precedingTokens = listOf(),
currentToken = Token(activeEditorInstance.cachedInput.currentWord.text),
maxSuggestionCount = 16,
allowPossiblyOffensive = !prefs.suggestion.blockPossiblyOffensive
).toStringList()
).toStringList())
if (BuildConfig.DEBUG) {
val elapsed = (System.nanoTime() - startTime) / 1000.0
flogInfo { "sugg fetch time: $elapsed us" }
@@ -472,6 +481,40 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
smartbarView?.onPrimaryClipChanged()
}
private fun queryUserDictionary(word: String, locale: Locale): Set<String> {
val florisDao = dictionaryManager.florisUserDictionaryDao()
val systemDao = dictionaryManager.systemUserDictionaryDao()
if (florisDao == null && systemDao == null) {
return setOf()
}
val retList = mutableSetOf<String>()
if (prefs.dictionary.enableFlorisUserDictionary) {
florisDao?.query(word, locale)?.let {
for (entry in it) {
retList.add(entry.word)
}
}
florisDao?.queryShortcut(word, locale)?.let {
for (entry in it) {
retList.add(entry.word)
}
}
}
if (prefs.dictionary.enableSystemUserDictionary) {
systemDao?.query(word, locale)?.let {
for (entry in it) {
retList.add(entry.word)
}
}
systemDao?.queryShortcut(word, locale)?.let {
for (entry in it) {
retList.add(entry.word)
}
}
}
return retList
}
/**
* Updates the current caps state according to the [EditorInstance.cursorCapsMode], while
* respecting [capsLock] property and the correction.autoCapitalization preference.
@@ -918,8 +961,8 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
*/
fun fixCase(word: String): String {
return when {
capsLock -> word.toUpperCase(florisboard.activeSubtype.locale)
caps -> word.capitalize(florisboard.activeSubtype.locale)
capsLock -> word.uppercase(florisboard.activeSubtype.locale)
caps -> word.replaceFirstChar { if (it.isLowerCase()) it.titlecase(florisboard.activeSubtype.locale) else it.toString() }
else -> word
}
}

View File

@@ -16,8 +16,6 @@
package dev.patrickgold.florisboard.ime.text.gestures
import java.util.*
/**
* Enum for declaring the distance thresholds for swipe gestures.
*/
@@ -30,11 +28,11 @@ enum class DistanceThreshold {
companion object {
fun fromString(string: String): DistanceThreshold {
return valueOf(string.toUpperCase(Locale.ENGLISH))
return valueOf(string.uppercase())
}
}
override fun toString(): String {
return super.toString().toLowerCase(Locale.ENGLISH)
return super.toString().lowercase()
}
}

View File

@@ -16,8 +16,6 @@
package dev.patrickgold.florisboard.ime.text.gestures
import java.util.*
/**
* Enum for declaring the possible actions for swipe gestures.
*/
@@ -49,11 +47,11 @@ enum class SwipeAction {
companion object {
fun fromString(string: String): SwipeAction {
return valueOf(string.toUpperCase(Locale.ENGLISH))
return valueOf(string.uppercase())
}
}
override fun toString(): String {
return super.toString().toLowerCase(Locale.ENGLISH)
return super.toString().lowercase()
}
}

View File

@@ -18,7 +18,11 @@ package dev.patrickgold.florisboard.ime.text.gestures
import android.content.Context
import android.view.MotionEvent
import android.view.VelocityTracker
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.debug.LogTopic
import dev.patrickgold.florisboard.debug.flogDebug
import dev.patrickgold.florisboard.util.ViewLayoutUtils
import kotlin.math.abs
import kotlin.math.atan2
@@ -64,8 +68,10 @@ abstract class SwipeGesture {
*/
class Detector(private val context: Context, private val listener: Listener) {
private var pointerDataMap: MutableMap<Int, PointerData> = mutableMapOf()
private var thresholdSpeed: Double = numericValue(context, VelocityThreshold.NORMAL)
private var thresholdWidth: Double = numericValue(context, DistanceThreshold.NORMAL)
private var unitWidth: Double = thresholdWidth / 4.0
private val velocityTracker: VelocityTracker = VelocityTracker.obtain()
var distanceThreshold: DistanceThreshold = DistanceThreshold.NORMAL
set(value) {
@@ -74,6 +80,10 @@ abstract class SwipeGesture {
unitWidth = thresholdWidth / 4.0
}
var velocityThreshold: VelocityThreshold = VelocityThreshold.NORMAL
set(value) {
field = value
thresholdSpeed = numericValue(context, value)
}
/**
* Method which evaluates if a given [event] is a gesture.
@@ -90,7 +100,9 @@ abstract class SwipeGesture {
MotionEvent.ACTION_POINTER_DOWN -> {
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
resetState()
velocityTracker.clear()
}
velocityTracker.addMovement(event)
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
pointerDataMap[pointerId] = PointerData().apply {
@@ -101,6 +113,7 @@ abstract class SwipeGesture {
}
}
MotionEvent.ACTION_MOVE -> {
velocityTracker.addMovement(event)
for (pointerIndex in 0 until event.pointerCount) {
val pointerId = event.getPointerId(pointerIndex)
pointerDataMap[pointerId]?.apply {
@@ -135,19 +148,17 @@ abstract class SwipeGesture {
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP -> {
velocityTracker.addMovement(event)
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
pointerDataMap.remove(pointerId)?.apply {
val absDiffX = event.getX(pointerIndex) - firstX
val absDiffY = event.getY(pointerIndex) - firstY
/*val velocityThresholdNV = numericValue(velocityThreshold)
val velocity =
((convertPixelsToDp(
sqrt(diffX.pow(2) + diffY.pow(2)),
context
) / event.downTime) * 10.0f.pow(8)).toInt()*/
// return if ((abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV) && velocity >= velocityThresholdNV) {
return if ((abs(absDiffX) > thresholdWidth || abs(absDiffY) > thresholdWidth)) {
velocityTracker.computeCurrentVelocity(1000)
val velocityX = ViewLayoutUtils.convertDpToPixel(velocityTracker.getXVelocity(pointerId), context)
val velocityY = ViewLayoutUtils.convertDpToPixel(velocityTracker.getYVelocity(pointerId), context)
flogDebug(LogTopic.GESTURES) { "Velocity: $velocityX $velocityY dp/s" }
return if ((abs(absDiffX) > thresholdWidth || abs(absDiffY) > thresholdWidth) && (abs(velocityX) > thresholdSpeed || abs(velocityY) > thresholdSpeed)) {
val direction = detectDirection(absDiffX.toDouble(), absDiffY.toDouble())
absUnitCountX = (absDiffX / unitWidth).toInt()
absUnitCountY = (absDiffY / unitWidth).toInt()

View File

@@ -16,8 +16,6 @@
package dev.patrickgold.florisboard.ime.text.gestures
import java.util.*
/**
* Enum for declaring the velocity thresholds for swipe gestures.
*/
@@ -30,11 +28,11 @@ enum class VelocityThreshold {
companion object {
fun fromString(string: String): VelocityThreshold {
return valueOf(string.toUpperCase(Locale.ENGLISH))
return valueOf(string.uppercase())
}
}
override fun toString(): String {
return super.toString().toLowerCase(Locale.ENGLISH)
return super.toString().lowercase()
}
}

View File

@@ -16,8 +16,6 @@
package dev.patrickgold.florisboard.ime.text.key
import java.util.*
/**
* Enum for the key hint modes.
*/
@@ -29,11 +27,11 @@ enum class KeyHintMode {
companion object {
fun fromString(string: String): KeyHintMode {
return valueOf(string.toUpperCase(Locale.ENGLISH))
return valueOf(string.uppercase())
}
}
override fun toString(): String {
return super.toString().toLowerCase(Locale.ENGLISH)
return super.toString().lowercase()
}
}

View File

@@ -23,7 +23,6 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.util.*
/**
* Enum for declaring the type of the key.
@@ -44,12 +43,12 @@ enum class KeyType {
UNSPECIFIED;
override fun toString(): String {
return super.toString().toLowerCase(Locale.ENGLISH)
return super.toString().lowercase()
}
companion object {
fun fromString(string: String): KeyType {
return valueOf(string.toUpperCase(Locale.ENGLISH))
return valueOf(string.uppercase())
}
}
}

View File

@@ -16,8 +16,6 @@
package dev.patrickgold.florisboard.ime.text.key
import java.util.*
/**
* Enum for declaring the utility key actions.
*/
@@ -30,7 +28,7 @@ enum class UtilityKeyAction {
companion object {
fun fromString(string: String): UtilityKeyAction {
return valueOf(string.toUpperCase(Locale.ENGLISH))
return valueOf(string.uppercase())
}
}
}

View File

@@ -61,7 +61,7 @@ class TextKey(override val data: KeyData) : Key(data) {
"~right"
}
else -> {
computed.label.toLowerCase()
computed.label.lowercase(evaluator.getActiveSubtype().locale)
}
}
val extendedPopupsDefault = evaluator.getKeyboard().extendedPopupMappingDefault

View File

@@ -342,9 +342,9 @@ class AutoTextKeyData(
val popup: PopupSet<TextKeyData>? = null
) : TextKeyData {
@Transient private val lower: BasicTextKeyData =
BasicTextKeyData(type, Character.toLowerCase(code), label.toLowerCase(Locale.getDefault()), groupId, popup)
BasicTextKeyData(type, Character.toLowerCase(code), label.lowercase(Locale.getDefault()), groupId, popup)
@Transient private val upper: BasicTextKeyData =
BasicTextKeyData(type, Character.toUpperCase(code), label.toUpperCase(Locale.getDefault()), groupId, popup)
BasicTextKeyData(type, Character.toUpperCase(code), label.uppercase(Locale.getDefault()), groupId, popup)
override fun computeTextKeyData(evaluator: TextComputingEvaluator): TextKeyData? {
return if (evaluator.isSlot(this)) {

View File

@@ -34,8 +34,8 @@ class TextKeyboard(
get() = arrangement.size
companion object {
fun layoutDrawableBounds(key: TextKey) {
layoutForegroundBounds(key, key.visibleDrawableBounds, 0.21, isLabel = false)
fun layoutDrawableBounds(key: TextKey, factor: Double) {
layoutForegroundBounds(key, key.visibleDrawableBounds, 0.21 * (1.0 / factor), isLabel = false)
}
fun layoutLabelBounds(key: TextKey) {
@@ -131,7 +131,7 @@ class TextKeyboard(
}
bottom = key.touchBounds.bottom - abs(desiredTouchBounds.bottom - desiredVisibleBounds.bottom)
}
layoutDrawableBounds(key)
layoutDrawableBounds(key, keyboardView.fontSizeMultiplier)
layoutLabelBounds(key)
posX += keyWidth
// After-adjust touch bounds for the row margin
@@ -165,7 +165,7 @@ class TextKeyboard(
right = key.touchBounds.right - abs(desiredTouchBounds.right - desiredVisibleBounds.right)
bottom = key.touchBounds.bottom - abs(desiredTouchBounds.bottom - desiredVisibleBounds.bottom)
}
layoutDrawableBounds(key)
layoutDrawableBounds(key, keyboardView.fontSizeMultiplier)
layoutLabelBounds(key)
posX += keyWidth
// After-adjust touch bounds for the row margin

View File

@@ -14,8 +14,6 @@
* limitations under the License.
*/
@file:OptIn(ExperimentalContracts::class)
package dev.patrickgold.florisboard.ime.text.keyboard
import android.content.Context
@@ -23,51 +21,14 @@ import android.graphics.drawable.Drawable
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import dev.patrickgold.florisboard.R
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
/**
* Abstract class definition for providing icons to a keyboard view. This class has been introduced to remove the need
* for keyboard vies to re-fetch drawable resources every time they draw on the canvas. The exact implementation is
* dependent on the subclass.
*/
abstract class KeyboardIconSet {
/**
* Get the drawable for the given [id].
*
* @param id The Android resource id of the drawable which should be returned.
*
* @return The drawable for given [id] or null if this icon set does not contain a drawable for this id.
*/
abstract fun getDrawable(@DrawableRes id: Int): Drawable?
/**
* Performs [block] on the drawable with the given [id]. If no drawable for the id exists,[block] will not be
* called at all.
*
* @param id The Android resource id of the drawable which should be used to execute block with.
* @param block The block which should be executed with the returned drawable.
*/
inline fun withDrawable(@DrawableRes id: Int, block: Drawable.() -> Unit) {
contract {
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
}
val drawable = getDrawable(id)
if (drawable != null) {
synchronized(drawable) {
block(drawable)
}
}
}
}
import dev.patrickgold.florisboard.ime.keyboard.KeyboardIconSet
/**
* An icon set for [TextKeyboardView].
*/
class TextKeyboardIconSet private constructor(context: Context) : KeyboardIconSet() {
private val drawableCache: Array<Drawable?> = Array(RES_ICON_IDS.size) {
ContextCompat.getDrawable(context.applicationContext, RES_ICON_IDS[it])
ContextCompat.getDrawable(context.applicationContext, RES_ICON_IDS[it])?.mutate()
}
companion object {

View File

@@ -17,6 +17,7 @@
package dev.patrickgold.florisboard.ime.text.keyboard
import android.content.Context
import android.content.res.Configuration
import android.graphics.*
import android.graphics.drawable.PaintDrawable
import android.os.Handler
@@ -107,7 +108,7 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
private var initSelectionStart: Int = 0
private var initSelectionEnd: Int = 0
private var hasTriggeredGestureMove: Boolean = false
private var hasTriggeredGestureUp: Boolean = false
private var shouldBlockNextUp: Boolean = false
private val swipeGestureDetector = SwipeGesture.Detector(context, this)
val desiredKey: TextKey = TextKey(data = TextKeyData.UNSPECIFIED)
@@ -117,6 +118,8 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
}
private var backgroundDrawable: PaintDrawable = PaintDrawable()
var fontSizeMultiplier: Double = 1.0
private set
private var labelPaintTextSize: Float = resources.getDimension(R.dimen.key_textSize)
private var labelPaintSpaceTextSize: Float = resources.getDimension(R.dimen.key_textSize)
private var labelPaint: Paint = Paint().apply {
@@ -176,8 +179,10 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
fun notifyStateChanged() {
flogInfo(LogTopic.TEXT_KEYBOARD_VIEW)
isRecomputingRequested = true
onLayoutInternal()
invalidate()
if (isMeasured) {
onLayoutInternal()
invalidate()
}
}
override fun onDetachedFromWindow() {
@@ -193,8 +198,10 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
val pointerId = event.getPointerId(pointerIndex)
if (activePointerId != null) {
onTouchUpInternal(event, pointerIndex, activePointerId!!)
onTouchDownInternal(event, pointerIndex, pointerId, resetInitialKey = false)
} else {
onTouchDownInternal(event, pointerIndex, pointerId, resetInitialKey = true)
}
onTouchDownInternal(event, pointerIndex, pointerId)
}
MotionEvent.ACTION_MOVE -> {
for (pointerIndex in 0 until event.pointerCount) {
@@ -212,16 +219,35 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
if (activePointerId == pointerId) {
onTouchUpInternal(event, pointerIndex, pointerId)
}
if (event.actionMasked == MotionEvent.ACTION_UP) {
florisboard?.let {
if (it.textInputManager.inputEventDispatcher.isPressed(KeyCode.SHIFT)) {
if (initialKey?.computedData?.code == KeyCode.SHIFT && activeKey?.computedData?.code == KeyCode.SHIFT) {
it.textInputManager.inputEventDispatcher.send(InputKeyEvent.up(TextKeyData.SHIFT))
} else {
it.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(TextKeyData.SHIFT))
}
}
}
activePointerId = null
}
}
MotionEvent.ACTION_CANCEL -> {
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
onTouchCancelInternal(event, pointerIndex, pointerId)
florisboard?.let {
if (it.textInputManager.inputEventDispatcher.isPressed(KeyCode.SHIFT)) {
it.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(TextKeyData.SHIFT))
}
}
activePointerId = null
}
}
flogDebug(LogTopic.TEXT_KEYBOARD_VIEW) { "initialKey: ${initialKey?.computedData?.label} activeKey: ${activeKey?.computedData?.label}" }
}
private fun onTouchDownInternal(event: MotionEvent, pointerIndex: Int, pointerId: Int) {
private fun onTouchDownInternal(event: MotionEvent, pointerIndex: Int, pointerId: Int, resetInitialKey: Boolean) {
flogDebug(LogTopic.TEXT_KEYBOARD_VIEW) { "index=$pointerIndex id=$pointerId event=$event" }
val florisboard = florisboard ?: return
@@ -245,7 +271,9 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
florisboard.keyPressVibrate()
florisboard.keyPressSound(key.computedData)
key.isPressed = true
initialKey = key
if (resetInitialKey) {
initialKey = key
}
activeKey = key
val delayMillis = prefs.keyboard.longPressDelay.toLong()
when (key.computedData.code) {
@@ -259,7 +287,7 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
}
else -> {
florisboard.executeSwipeAction(prefs.gestures.spaceBarLongPress)
hasTriggeredGestureUp = true
shouldBlockNextUp = true
}
}
}
@@ -273,7 +301,7 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
}
KeyCode.LANGUAGE_SWITCH -> {
longPressHandler.postDelayed((delayMillis * 2.0).toLong()) {
hasTriggeredGestureUp = true
shouldBlockNextUp = true
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(TextKeyData.SHOW_INPUT_METHOD_PICKER))
}
}
@@ -291,7 +319,9 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
swipeGestureDetector.onTouchEvent(event)
}
} else {
initialKey = null
if (resetInitialKey) {
initialKey = null
}
activeKey = null
}
invalidate()
@@ -307,12 +337,12 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
val alwaysTriggerOnMove = (hasTriggeredGestureMove
&& (initialKey?.computedData?.code == KeyCode.DELETE
&& prefs.gestures.deleteKeySwipeLeft == SwipeAction.DELETE_CHARACTERS_PRECISELY
|| initialKey?.computedData?.code == KeyCode.SPACE))
|| initialKey?.computedData?.code == KeyCode.SPACE || (initialKey?.computedData?.code == KeyCode.SHIFT && activeKey?.computedData?.code == KeyCode.SPACE)))
if (swipeGestureDetector.onTouchEvent(event, alwaysTriggerOnMove) || hasTriggeredGestureMove) {
longPressHandler.cancelAll()
hasTriggeredGestureMove = true
initialKey?.let {
if (florisboard.textInputManager.inputEventDispatcher.isPressed(it.computedData.code)) {
if (it.computedData.code != KeyCode.SHIFT && florisboard.textInputManager.inputEventDispatcher.isPressed(it.computedData.code)) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(it.computedData))
}
}
@@ -322,7 +352,7 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
if (popupManager.isShowingExtendedPopup) {
if (!popupManager.propagateMotionEvent(key, event, pointerIndex)) {
onTouchCancelInternal(event, pointerIndex, pointerId)
onTouchDownInternal(event, pointerIndex, pointerId)
onTouchDownInternal(event, pointerIndex, pointerId, resetInitialKey = false)
}
} else {
if ((event.getX(pointerIndex) < key.visibleBounds.left - 0.1f * key.visibleBounds.width())
@@ -331,7 +361,7 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
|| (event.getY(pointerIndex) > key.visibleBounds.bottom + 0.35f * key.visibleBounds.height())
) {
onTouchCancelInternal(event, pointerIndex, pointerId)
onTouchDownInternal(event, pointerIndex, pointerId)
onTouchDownInternal(event, pointerIndex, pointerId, resetInitialKey = false)
}
}
}
@@ -345,7 +375,7 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
longPressHandler.cancelAll()
val key = activeKey
if (key != null) {
if (swipeGestureDetector.onTouchEvent(event) || hasTriggeredGestureMove || hasTriggeredGestureUp) {
if (swipeGestureDetector.onTouchEvent(event) || hasTriggeredGestureMove || shouldBlockNextUp) {
if (hasTriggeredGestureMove && initialKey?.computedData?.code == KeyCode.DELETE) {
florisboard.textInputManager.isGlidePostEffect = false
florisboard.activeEditorInstance.apply {
@@ -358,28 +388,28 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
return
}
key.isPressed = false
val retData = popupManager.getActiveKeyData(key)
if (retData != null) {
if (retData == key.computedData) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.up(key.computedData))
if (activeKey?.computedData?.code != KeyCode.SHIFT) {
val retData = popupManager.getActiveKeyData(key)
if (retData != null) {
if (retData == key.computedData) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.up(key.computedData))
} else {
if (florisboard.textInputManager.inputEventDispatcher.isPressed(key.computedData.code)) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(key.computedData))
}
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(retData))
}
} else {
if (florisboard.textInputManager.inputEventDispatcher.isPressed(key.computedData.code)) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(key.computedData))
}
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(retData))
}
} else {
if (florisboard.textInputManager.inputEventDispatcher.isPressed(key.computedData.code)) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(key.computedData))
}
}
}
popupManager.hide()
initialKey = null
activeKey = null
activePointerId = null
hasTriggeredGestureMove = false
hasTriggeredGestureUp = false
shouldBlockNextUp = false
invalidate()
}
@@ -391,40 +421,41 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
val key = activeKey
if (key != null) {
key.isPressed = false
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(key.computedData))
if (activeKey?.computedData?.code != KeyCode.SHIFT) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(key.computedData))
}
}
popupManager.hide()
initialKey = null
activeKey = null
activePointerId = null
hasTriggeredGestureMove = false
hasTriggeredGestureUp = false
shouldBlockNextUp = false
invalidate()
}
override fun onSwipe(event: SwipeGesture.Event): Boolean {
val florisboard = florisboard ?: return false
val initialKey = initialKey ?: return false
if (activePointerId != event.pointerId) return false
flogDebug(LogTopic.TEXT_KEYBOARD_VIEW)
return when (initialKey.computedData.code) {
KeyCode.DELETE -> handleDeleteSwipe(event)
KeyCode.SPACE -> handleSpaceSwipe(event)
else -> when {
initialKey.computedData.code == KeyCode.SHIFT && activeKey?.computedData?.code == KeyCode.SPACE &&
event.type == SwipeGesture.Type.TOUCH_MOVE -> handleSpaceSwipe(event)
initialKey.computedData.code == KeyCode.SHIFT && activeKey?.computedData?.code != KeyCode.SHIFT &&
event.type == SwipeGesture.Type.TOUCH_UP -> {
/*activeKeyViews[event.pointerId]?.let {
florisboard?.textInputManager?.inputEventDispatcher?.send(
InputKeyEvent.up(
it.popupManager.getActiveKeyData(it)
?: it.data
)
activeKey?.let {
florisboard.textInputManager.inputEventDispatcher.send(
InputKeyEvent.up(popupManager.getActiveKeyData(it) ?: it.computedData)
)
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.cancel(KeyData.SHIFT))
}*/
}
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(TextKeyData.SHIFT))
true
}
initialKey.computedData.code > KeyCode.SPACE && !popupManager.isShowingExtendedPopup -> when {
!prefs.glide.enabled -> when (event.type) {
!prefs.glide.enabled && !hasTriggeredGestureMove -> when (event.type) {
SwipeGesture.Type.TOUCH_UP -> {
val swipeAction = when (event.direction) {
SwipeGesture.Direction.UP -> prefs.gestures.swipeUp
@@ -463,7 +494,7 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
(selection.end + event.absUnitCountX + 1).coerceIn(0, selection.end),
selection.end
)
hasTriggeredGestureUp = true
shouldBlockNextUp = true
}
true
}
@@ -473,7 +504,7 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
florisboard.activeEditorInstance.apply {
leftAppendWordToSelection()
}
hasTriggeredGestureUp = true
shouldBlockNextUp = true
true
}
SwipeGesture.Direction.RIGHT -> {
@@ -481,7 +512,7 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
florisboard.activeEditorInstance.apply {
leftPopWordFromSelection()
}
hasTriggeredGestureUp = true
shouldBlockNextUp = true
true
}
else -> false
@@ -536,7 +567,7 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
}
true
}
else -> false
else -> true // To prevent the popup display of nearby keys
}
SwipeGesture.Type.TOUCH_UP -> {
if (event.absUnitCountY.times(-1) > 6) {
@@ -619,7 +650,7 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
}
}
}
TextKeyboard.layoutDrawableBounds(desiredKey)
TextKeyboard.layoutDrawableBounds(desiredKey, 1.0)
TextKeyboard.layoutLabelBounds(desiredKey)
var spaceKey: TextKey? = null
@@ -634,13 +665,24 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
}
}
fontSizeMultiplier = when (resources.configuration.orientation) {
Configuration.ORIENTATION_PORTRAIT -> {
prefs.keyboard.fontSizeMultiplierPortrait.toFloat() / 100.0
}
Configuration.ORIENTATION_LANDSCAPE -> {
prefs.keyboard.fontSizeMultiplierLandscape.toFloat() / 100.0
}
else -> 1.0
}
keyboard.layout(this)
setTextSizeFor(
labelPaint,
desiredKey.visibleLabelBounds.width().toFloat(),
desiredKey.visibleLabelBounds.height().toFloat(),
"X"
"X",
fontSizeMultiplier
)
labelPaintTextSize = labelPaint.textSize
@@ -649,7 +691,8 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
labelPaint,
spaceKey.visibleLabelBounds.width().toFloat(),
spaceKey.visibleLabelBounds.height().toFloat(),
spaceKey.label ?: "X"
spaceKey.label ?: "X",
fontSizeMultiplier.coerceAtMost(1.0)
)
labelPaintSpaceTextSize = labelPaint.textSize
}
@@ -658,11 +701,13 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
hintedLabelPaint,
desiredKey.visibleBounds.width() * 1.0f / 5.0f,
desiredKey.visibleBounds.height() * 1.0f / 5.0f,
"X"
"X",
fontSizeMultiplier
)
hintedLabelPaintTextSize = hintedLabelPaint.textSize
}
private val baselineTextSize = resources.getDimension(R.dimen.key_textSize)
/**
* Automatically sets the text size of [boxPaint] for given [text] so it fits within the given
* bounds.
@@ -674,26 +719,40 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
* @param boxWidth The max width for the surrounding box of [text].
* @param boxHeight The max height for the surrounding box of [text].
* @param text The text for which the size should be calculated.
* @param multiplier The factor by which the resulting text size should be multiplied with.
*/
private fun setTextSizeFor(boxPaint: Paint, boxWidth: Float, boxHeight: Float, text: String, multiplier: Float = 1.0f): Float {
var stage = 1
var textSize = 0.0f
while (stage < 3) {
if (stage == 1) {
textSize += 10.0f
} else if (stage == 2) {
textSize -= 1.0f
private fun setTextSizeFor(boxPaint: Paint, boxWidth: Float, boxHeight: Float, text: String, multiplier: Double = 1.0): Float {
var size = baselineTextSize
boxPaint.textSize = size
boxPaint.getTextBounds(text, 0, text.length, tempRect)
val w = tempRect.width().toFloat()
val h = tempRect.height().toFloat()
val diffW = abs(boxWidth - w)
val diffH = abs(boxHeight - h)
if (w < boxWidth && h < boxHeight) {
// Text fits, scale up on axis which has less room
size *= if (diffW < diffH) {
1.0f + diffW / w
} else {
1.0f + diffH / h
}
boxPaint.textSize = textSize
boxPaint.getTextBounds(text, 0, text.length, tempRect)
val fits = tempRect.width() < boxWidth && tempRect.height() < boxHeight
if (stage == 1 && !fits || stage == 2 && fits) {
stage++
} else if (w >= boxWidth && h < boxHeight) {
// Text does not fit on x-axis
size *= (1.0f - diffW / w)
} else if (w < boxWidth && h >= boxHeight) {
// Text does not fit on y-axis
size *= (1.0f - diffH / h)
} else {
// Text does not fit at all, scale down on axis which has most overshoot
size *= if (diffW < diffH) {
1.0f - diffH / h
} else {
1.0f - diffW / w
}
}
textSize *= multiplier
boxPaint.textSize = textSize
return textSize
size *= multiplier.toFloat()
boxPaint.textSize = size
return size
}
override fun onThemeUpdated(theme: Theme) {

View File

@@ -23,7 +23,6 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.util.*
/**
* Defines the type of the layout.
@@ -44,12 +43,12 @@ enum class LayoutType {
SYMBOLS2_MOD;
override fun toString(): String {
return super.toString().replace("_", "/").toLowerCase(Locale.ENGLISH)
return super.toString().replace("_", "/").lowercase()
}
companion object {
fun fromString(string: String): LayoutType {
return valueOf(string.replace("/", "_").toUpperCase(Locale.ENGLISH))
return valueOf(string.replace("/", "_").uppercase())
}
}
}

View File

@@ -40,7 +40,6 @@ import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import dev.patrickgold.florisboard.ime.theme.ThemeValue
import java.lang.ref.WeakReference
import java.util.*
import kotlin.collections.ArrayList
import kotlin.math.abs
@@ -533,12 +532,12 @@ class CandidateView : View, ThemeManager.OnThemeUpdatedListener {
companion object {
fun fromString(string: String): DisplayMode {
return valueOf(string.toUpperCase(Locale.ENGLISH))
return valueOf(string.uppercase())
}
}
override fun toString(): String {
return super.toString().toLowerCase(Locale.ENGLISH)
return super.toString().lowercase()
}
}
}

View File

@@ -34,50 +34,50 @@ class AdaptiveThemeOverlay(
Attr.KEY_BACKGROUND_PRESSED,
Attr.SMARTBAR_BACKGROUND,
Attr.WINDOW_NAVIGATION_BAR_COLOR -> {
themeManager.remoteColorPrimaryVariant ?: super.getAttr(ref, s1, s2)
themeManager.remote.colorPrimaryVariant ?: super.getAttr(ref, s1, s2)
}
Attr.KEY_FOREGROUND_PRESSED,
Attr.SMARTBAR_FOREGROUND -> {
themeManager.remoteColorPrimaryVariant?.complimentaryTextColor() ?: super.getAttr(ref, s1, s2)
themeManager.remote.colorPrimaryVariant?.complimentaryTextColor() ?: super.getAttr(ref, s1, s2)
}
Attr.SMARTBAR_FOREGROUND_ALT -> {
themeManager.remoteColorPrimaryVariant?.complimentaryTextColor(true) ?: super.getAttr(ref, s1, s2)
themeManager.remote.colorPrimaryVariant?.complimentaryTextColor(true) ?: super.getAttr(ref, s1, s2)
}
Attr.KEY_BACKGROUND,
Attr.SMARTBAR_BUTTON_BACKGROUND -> {
themeManager.remoteColorPrimary ?: super.getAttr(ref, s1, s2)
themeManager.remote.colorPrimary ?: super.getAttr(ref, s1, s2)
}
Attr.KEY_FOREGROUND,
Attr.SMARTBAR_BUTTON_FOREGROUND -> {
if (s1 == "shift" && s2 == "capslock") {
themeManager.remoteColorSecondary ?: super.getAttr(ref, s1, s2)
themeManager.remote.colorSecondary ?: super.getAttr(ref, s1, s2)
} else {
themeManager.remoteColorPrimary?.complimentaryTextColor() ?: super.getAttr(ref, s1, s2)
themeManager.remote.colorPrimary?.complimentaryTextColor() ?: super.getAttr(ref, s1, s2)
}
}
Attr.KEY_SHOW_BORDER -> {
if (themeManager.remoteColorPrimary != null) {
if (themeManager.remote.colorPrimary != null) {
ThemeValue.OnOff(true)
} else {
super.getAttr(ref, s1, s2)
}
}
Attr.WINDOW_NAVIGATION_BAR_LIGHT -> {
if (themeManager.remoteColorPrimaryVariant != null) {
ThemeValue.OnOff(themeManager.remoteColorPrimaryVariant?.complimentaryTextColor()?.color == Color.BLACK)
if (themeManager.remote.colorPrimaryVariant != null) {
ThemeValue.OnOff(themeManager.remote.colorPrimaryVariant?.complimentaryTextColor()?.color == Color.BLACK)
} else {
super.getAttr(ref, s1, s2)
}
}
Attr.POPUP_BACKGROUND,
Attr.GLIDE_TRAIL_COLOR -> {
themeManager.remoteColorSecondary ?: super.getAttr(ref, s1, s2)
themeManager.remote.colorSecondary ?: super.getAttr(ref, s1, s2)
}
Attr.POPUP_BACKGROUND_ACTIVE -> {
themeManager.remoteColorSecondary?.complimentaryTextColor(true) ?: super.getAttr(ref, s1, s2)
themeManager.remote.colorSecondary?.complimentaryTextColor(true) ?: super.getAttr(ref, s1, s2)
}
Attr.POPUP_FOREGROUND -> {
themeManager.remoteColorSecondary?.complimentaryTextColor() ?: super.getAttr(ref, s1, s2)
themeManager.remote.colorSecondary?.complimentaryTextColor() ?: super.getAttr(ref, s1, s2)
}
else -> super.getAttr(ref, s1, s2)
}

View File

@@ -17,12 +17,14 @@
package dev.patrickgold.florisboard.ime.theme
import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Color
import android.net.Uri
import android.util.TypedValue
import androidx.annotation.AttrRes
import androidx.core.content.ContextCompat
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.extension.AssetManager
import dev.patrickgold.florisboard.ime.extension.AssetRef
@@ -31,6 +33,7 @@ import dev.patrickgold.florisboard.util.TimeUtil
import timber.log.Timber
import java.util.concurrent.CopyOnWriteArrayList
/**
* Core class which manages the keyboard theme. Note, that this does not affect the UI theme of the
* Settings Activities.
@@ -50,12 +53,9 @@ class ThemeManager private constructor(
var isAdaptiveThemeEnabled: Boolean = false
private set
var remoteColorPrimary: ThemeValue.SolidColor? = null
private set
var remoteColorPrimaryVariant: ThemeValue.SolidColor? = null
private set
var remoteColorSecondary: ThemeValue.SolidColor? = null
var remote: RemoteColors = RemoteColors.DEFAULT
private set
private val remoteCache: ArrayList<RemoteColors> = arrayListOf()
companion object {
/** The static relative path where a theme is located, regardless of the [AssetSource]. */
@@ -101,127 +101,95 @@ class ThemeManager private constructor(
indexThemeRefs()
val ref = evaluateActiveThemeRef()
Timber.i(ref.toString())
activeTheme = AdaptiveThemeOverlay(this, if (ref == null) {
Theme.BASE_THEME
} else {
loadTheme(ref).getOrDefault(Theme.BASE_THEME)
})
activeTheme = AdaptiveThemeOverlay(
this, if (ref == null) {
Theme.BASE_THEME
} else {
loadTheme(ref).getOrDefault(Theme.BASE_THEME)
}
)
Timber.i(activeTheme.label)
notifyCallbackReceivers()
}
/**
* Gets the primary and ark variants of the app with given [packageName].
* Based on a Stock Overflow answer by adneal.
* Source: https://stackoverflow.com/a/27138913/6801193
* Gets the primary and ark variants of the app with given [remotePackageName].
* Based AnySoftKeyboard's way of getting remote colors:
* https://github.com/AnySoftKeyboard/AnySoftKeyboard/blob/master/ime/overlay/src/main/java/com/anysoftkeyboard/overlay/OverlyDataCreatorForAndroid.java
*
* @param packageName The package name from which the colors should be extracted.
* @param remotePackageName The package name from which the colors should be extracted.
*/
@SuppressLint("ResourceType")
@Suppress("UNNECESSARY_SAFE_CALL")
fun updateRemoteColorValues(packageName: String) {
return // See why: https://github.com/florisboard/florisboard/issues/763
fun updateRemoteColorValues(remotePackageName: String) {
if (!isAdaptiveThemeEnabled) return
try {
val tempRemote = remoteCache.find { it.packageName == remotePackageName }
if (tempRemote != null) {
remote = tempRemote
return
}
val colorPrimary: Int?
val colorPrimaryVariant: Int?
val colorSecondary: Int?
val pm = packageManager ?: return
val res = pm.getResourcesForApplication(packageName)
val attrs = listOf(
res.getIdentifier("colorPrimary", "attr", packageName),
android.R.attr.colorPrimary,
res.getIdentifier("colorPrimaryDark", "attr", packageName),
android.R.attr.colorPrimaryDark,
res.getIdentifier("colorPrimaryVariant", "attr", packageName),
res.getIdentifier("colorAccent", "attr", packageName),
android.R.attr.colorAccent,
res.getIdentifier("colorSecondary", "attr", packageName)
val remoteApp = pm.getLaunchIntentForPackage(remotePackageName)?.component ?: return
val activityInfo = pm.getActivityInfo(remoteApp, PackageManager.GET_META_DATA)
val remoteContext = applicationContext.createPackageContext(
remoteApp.packageName,
Context.CONTEXT_IGNORE_SECURITY
)
val androidTheme = res.newTheme()
val defColor = if (activeTheme.isNightTheme) {
Color.BLACK
} else {
Color.WHITE
}
val themeIds = mutableListOf<Int>()
pm.getLaunchIntentForPackage(packageName)?.component?.let { cn ->
pm.getActivityInfo(cn, 0)?.let { launchActivity ->
if (launchActivity.targetActivity != null) {
pm.getActivityInfo(ComponentName(packageName, launchActivity.targetActivity), 0)?.let {
themeIds.add(it.theme)
}
} else {
themeIds.add(launchActivity.theme)
}
}
}
pm.getApplicationInfo(packageName, 0)?.let { applicationInfo ->
themeIds.add(applicationInfo.theme)
}
remoteColorPrimary = null
remoteColorPrimaryVariant = null
remoteColorSecondary = null
for (themeId in themeIds) {
if (remoteColorPrimary != null && remoteColorPrimaryVariant != null &&
remoteColorSecondary != null) {
break
}
androidTheme.applyStyle(themeId, false)
androidTheme.obtainStyledAttributes(attrs.toIntArray()).let { a ->
remoteColorPrimary = when {
a.hasValue(0) -> {
ThemeValue.SolidColor(a.getColor(0, defColor))
}
a.hasValue(1) -> {
ThemeValue.SolidColor(a.getColor(1, defColor))
}
else -> {
remoteColorPrimary
}
}
remoteColorPrimaryVariant = when {
a.hasValue(2) -> {
ThemeValue.SolidColor(a.getColor(2, defColor))
}
a.hasValue(3) -> {
ThemeValue.SolidColor(a.getColor(3, defColor))
}
a.hasValue(4) -> {
ThemeValue.SolidColor(a.getColor(4, defColor))
}
else -> {
remoteColorPrimaryVariant
}
}
remoteColorSecondary = when {
a.hasValue(5) -> {
ThemeValue.SolidColor(a.getColor(5, defColor))
}
a.hasValue(6) -> {
ThemeValue.SolidColor(a.getColor(6, defColor))
}
a.hasValue(7) -> {
ThemeValue.SolidColor(a.getColor(7, defColor))
}
else -> {
remoteColorSecondary
}
}
a.recycle()
}
}
remoteContext.setTheme(activityInfo.themeResource)
val res = remoteContext.resources
val attrs = intArrayOf(
res.getIdentifier("colorPrimary", "attr", remotePackageName),
android.R.attr.colorPrimary,
res.getIdentifier("colorPrimaryDark", "attr", remotePackageName),
android.R.attr.colorPrimaryDark,
res.getIdentifier("colorPrimaryVariant", "attr", remotePackageName),
res.getIdentifier("colorAccent", "attr", remotePackageName),
android.R.attr.colorAccent,
res.getIdentifier("colorSecondary", "attr", remotePackageName)
)
val typedValue = TypedValue()
colorPrimary =
getColorFromThemeAttribute(remoteContext, typedValue, attrs[0]) ?:
getColorFromThemeAttribute(remoteContext, typedValue, attrs[1])
colorPrimaryVariant =
getColorFromThemeAttribute(remoteContext, typedValue, attrs[2]) ?:
getColorFromThemeAttribute(remoteContext, typedValue, attrs[3]) ?:
getColorFromThemeAttribute(remoteContext, typedValue, attrs[4])
colorSecondary =
getColorFromThemeAttribute(remoteContext, typedValue, attrs[5]) ?:
getColorFromThemeAttribute(remoteContext, typedValue, attrs[6]) ?:
getColorFromThemeAttribute(remoteContext, typedValue, attrs[7])
val newRemote = RemoteColors(
packageName = remotePackageName,
colorPrimary = colorPrimary?.let { ThemeValue.SolidColor(it or Color.BLACK) },
colorPrimaryVariant = colorPrimaryVariant?.let { ThemeValue.SolidColor(it or Color.BLACK) },
colorSecondary = colorSecondary?.let { ThemeValue.SolidColor(it or Color.BLACK) }
)
remoteCache.add(newRemote)
remote = newRemote
} catch (e: Exception) {
remote = RemoteColors.DEFAULT
e.printStackTrace()
}
remoteColorPrimary?.let {
remoteColorPrimary = ThemeValue.SolidColor(it.color or Color.BLACK)
}
remoteColorPrimaryVariant?.let {
remoteColorPrimaryVariant = ThemeValue.SolidColor(it.color or Color.BLACK)
}
remoteColorSecondary?.let {
remoteColorSecondary = ThemeValue.SolidColor(it.color or Color.BLACK)
}
notifyCallbackReceivers()
}
private fun getColorFromThemeAttribute(
context: Context, typedValue: TypedValue, @AttrRes attr: Int
): Int? {
return if (context.theme.resolveAttribute(attr, typedValue, true)) {
if (typedValue.type == TypedValue.TYPE_REFERENCE) {
ContextCompat.getColor(context, typedValue.resourceId)
} else {
typedValue.data
}
} else {
null
}
}
/**
* Sends a theme update to the given [onThemeUpdatedListener], regardless if it is currently
* registered or not.
@@ -274,37 +242,39 @@ class ThemeManager private constructor(
Timber.i(prefs.theme.mode.toString())
Timber.i(prefs.theme.dayThemeRef)
Timber.i(prefs.theme.nightThemeRef)
return AssetRef.fromString(when (prefs.theme.mode) {
ThemeMode.ALWAYS_DAY -> {
isAdaptiveThemeEnabled = prefs.theme.dayThemeAdaptToApp
prefs.theme.dayThemeRef
}
ThemeMode.ALWAYS_NIGHT -> {
isAdaptiveThemeEnabled = prefs.theme.nightThemeAdaptToApp
prefs.theme.nightThemeRef
}
ThemeMode.FOLLOW_SYSTEM -> if (applicationContext.resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
) {
isAdaptiveThemeEnabled = prefs.theme.nightThemeAdaptToApp
prefs.theme.nightThemeRef
} else {
isAdaptiveThemeEnabled = prefs.theme.dayThemeAdaptToApp
prefs.theme.dayThemeRef
}
ThemeMode.FOLLOW_TIME -> {
val current = TimeUtil.currentLocalTime()
val sunrise = TimeUtil.decode(prefs.theme.sunriseTime)
val sunset = TimeUtil.decode(prefs.theme.sunsetTime)
if (TimeUtil.isNightTime(sunrise, sunset, current)) {
return AssetRef.fromString(
when (prefs.theme.mode) {
ThemeMode.ALWAYS_DAY -> {
isAdaptiveThemeEnabled = prefs.theme.dayThemeAdaptToApp
prefs.theme.dayThemeRef
}
ThemeMode.ALWAYS_NIGHT -> {
isAdaptiveThemeEnabled = prefs.theme.nightThemeAdaptToApp
prefs.theme.nightThemeRef
}
ThemeMode.FOLLOW_SYSTEM -> if (applicationContext.resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
) {
isAdaptiveThemeEnabled = prefs.theme.nightThemeAdaptToApp
prefs.theme.nightThemeRef
} else {
isAdaptiveThemeEnabled = prefs.theme.dayThemeAdaptToApp
prefs.theme.dayThemeRef
}
ThemeMode.FOLLOW_TIME -> {
val current = TimeUtil.currentLocalTime()
val sunrise = TimeUtil.decode(prefs.theme.sunriseTime)
val sunset = TimeUtil.decode(prefs.theme.sunsetTime)
if (TimeUtil.isNightTime(sunrise, sunset, current)) {
isAdaptiveThemeEnabled = prefs.theme.nightThemeAdaptToApp
prefs.theme.nightThemeRef
} else {
isAdaptiveThemeEnabled = prefs.theme.dayThemeAdaptToApp
prefs.theme.dayThemeRef
}
}
}
}).onFailure { Timber.e(it) }.getOrDefault(null)
).onFailure { Timber.e(it) }.getOrDefault(null)
}
private fun indexThemeRefs() {
@@ -338,6 +308,17 @@ class ThemeManager private constructor(
}
}
data class RemoteColors(
val packageName: String,
val colorPrimary: ThemeValue.SolidColor?,
val colorPrimaryVariant: ThemeValue.SolidColor?,
val colorSecondary: ThemeValue.SolidColor?
) {
companion object {
val DEFAULT = RemoteColors("undefined", null, null, null)
}
}
/**
* Functional interface which should be implemented by event listeners to be able to receive
* theme updates.

View File

@@ -16,8 +16,6 @@
package dev.patrickgold.florisboard.ime.theme
import java.util.*
/**
* Enum class which specifies all theme modes available. Used in the Settings to properly manage
* different use cases when the day or night theme should be active.
@@ -30,7 +28,7 @@ enum class ThemeMode {
companion object {
fun fromString(string: String): ThemeMode {
return valueOf(string.toUpperCase(Locale.ENGLISH))
return valueOf(string.uppercase())
}
}
}

View File

@@ -39,7 +39,8 @@ import dev.patrickgold.florisboard.ime.extension.AssetManager
import dev.patrickgold.florisboard.ime.extension.AssetRef
import dev.patrickgold.florisboard.ime.extension.AssetSource
import dev.patrickgold.florisboard.ime.text.key.CurrencySet
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.keyboard.*
import dev.patrickgold.florisboard.ime.text.layout.LayoutManager
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
@@ -53,6 +54,21 @@ class ThemeManagerActivity : FlorisActivity<ThemeManagerActivityBinding>() {
private val themeManager: ThemeManager get() = ThemeManager.default()
private val assetManager: AssetManager get() = AssetManager.default()
private lateinit var textKeyboardIconSet: TextKeyboardIconSet
private val textComputingEvaluator = object : TextComputingEvaluator by DefaultTextComputingEvaluator {
override fun evaluateVisible(data: TextKeyData): Boolean {
return data.code != KeyCode.SWITCH_TO_MEDIA_CONTEXT
}
override fun isSlot(data: TextKeyData): Boolean {
return CurrencySet.isCurrencySlot(data.code)
}
override fun getSlotData(data: TextKeyData): TextKeyData {
return BasicTextKeyData(label = "$")
}
}
private var key: String = ""
private var defaultValue: String = ""
private var selectedTheme: Theme = Theme.empty()
@@ -145,6 +161,10 @@ class ThemeManagerActivity : FlorisActivity<ThemeManagerActivityBinding>() {
binding.themeEditBtn.setOnClickListener { onActionClicked(it) }
binding.themeExportBtn.setOnClickListener { onActionClicked(it) }
textKeyboardIconSet = TextKeyboardIconSet.new(this)
binding.keyboardPreview.setIconSet(textKeyboardIconSet)
binding.keyboardPreview.setComputingEvaluator(textComputingEvaluator)
buildUi()
}

View File

@@ -0,0 +1,455 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.settings
import android.app.AlertDialog
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.Toolbar
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.databinding.UdmActivityBinding
import dev.patrickgold.florisboard.databinding.UdmEntryDialogBinding
import dev.patrickgold.florisboard.ime.core.FlorisActivity
import dev.patrickgold.florisboard.ime.dictionary.DictionaryManager
import dev.patrickgold.florisboard.ime.dictionary.FREQUENCY_DEFAULT
import dev.patrickgold.florisboard.ime.dictionary.FREQUENCY_MAX
import dev.patrickgold.florisboard.ime.dictionary.FREQUENCY_MIN
import dev.patrickgold.florisboard.ime.dictionary.UserDictionaryDao
import dev.patrickgold.florisboard.ime.dictionary.UserDictionaryDatabase
import dev.patrickgold.florisboard.ime.dictionary.UserDictionaryEntry
import dev.patrickgold.florisboard.ime.text.keyboard.*
import dev.patrickgold.florisboard.util.LocaleUtils
import java.util.*
interface OnListItemCLickListener {
fun onListItemClick(pos: Int)
}
class LanguageEntryAdapter(
private val data: List<String>,
private val onListItemCLickListener: OnListItemCLickListener
) : RecyclerView.Adapter<LanguageEntryAdapter.ViewHolder>() {
class ViewHolder(view: View, private val onListItemCLickListener: OnListItemCLickListener) :
RecyclerView.ViewHolder(view) {
val titleView: TextView = view.findViewById(android.R.id.title)
val summaryView: TextView = view.findViewById(android.R.id.summary)
init {
view.setOnClickListener {
onListItemCLickListener.onListItemClick(adapterPosition)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val listItemView = LayoutInflater.from(parent.context)
.inflate(R.layout.list_item, parent, false)
return ViewHolder(listItemView, onListItemCLickListener)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.titleView.text = data[position]
holder.summaryView.isVisible = false
}
override fun getItemCount(): Int {
return data.size
}
}
class UserDictionaryEntryAdapter(
private val data: List<UserDictionaryEntry>,
private val onListItemCLickListener: OnListItemCLickListener
) : RecyclerView.Adapter<UserDictionaryEntryAdapter.ViewHolder>() {
class ViewHolder(view: View, private val onListItemCLickListener: OnListItemCLickListener) :
RecyclerView.ViewHolder(view) {
val titleView: TextView = view.findViewById(android.R.id.title)
val summaryView: TextView = view.findViewById(android.R.id.summary)
init {
view.setOnClickListener {
onListItemCLickListener.onListItemClick(adapterPosition)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val listItemView = LayoutInflater.from(parent.context)
.inflate(R.layout.list_item, parent, false)
return ViewHolder(listItemView, onListItemCLickListener)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.titleView.text = data[position].word
val shortcut = data[position].shortcut
holder.summaryView.text = if (shortcut == null) {
String.format(
holder.summaryView.context.resources.getString(R.string.settings__udm__word_summary_freq),
data[position].freq
)
} else {
String.format(
holder.summaryView.context.resources.getString(R.string.settings__udm__word_summary_freq_shortcut),
data[position].freq,
shortcut
)
}
}
override fun getItemCount(): Int {
return data.size
}
}
class UdmActivity : FlorisActivity<UdmActivityBinding>() {
private val dictionaryManager: DictionaryManager get() = DictionaryManager.default()
private var userDictionaryType: Int = -1
private var currentLevel: Int = LEVEL_LANGUAGES
private var currentLocale: Locale? = null
private var activeDialogWindow: AlertDialog? = null
private var languageList: List<Locale?> = listOf()
private var wordList: List<UserDictionaryEntry> = listOf()
private val languageListItemClickListener = object : OnListItemCLickListener {
override fun onListItemClick(pos: Int) {
if (currentLevel == LEVEL_LANGUAGES) {
currentLocale = languageList[pos]
currentLevel = LEVEL_WORDS
buildUi()
}
}
}
private val wordListItemClickListener = object : OnListItemCLickListener {
override fun onListItemClick(pos: Int) {
if (currentLevel == LEVEL_WORDS) {
val entry = wordList[pos]
showEditWordDialog(entry)
}
}
}
private val importUserDictionary = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
// If uri is null it indicates that the selection activity was cancelled (mostly by pressing the back button),
// so we don't display an error message here.
if (uri == null) return@registerForActivityResult
val db: UserDictionaryDatabase? = when (userDictionaryType) {
USER_DICTIONARY_TYPE_FLORIS -> {
dictionaryManager.florisUserDictionaryDatabase()
}
USER_DICTIONARY_TYPE_SYSTEM -> {
dictionaryManager.systemUserDictionaryDatabase()
}
else -> {
showError(Exception("User dictionary type '$userDictionaryType' is not valid!"))
return@registerForActivityResult
}
}
if (db == null) {
showError(NullPointerException("Database handle is null!"))
return@registerForActivityResult
}
db.importCombinedList(this, uri).onSuccess {
showMessage(R.string.settings__udm__dictionary_export_success)
}.onFailure {
showError(it)
}
}
private val exportUserDictionary = registerForActivityResult(ActivityResultContracts.CreateDocument()) { uri: Uri? ->
// If uri is null it indicates that the selection activity was cancelled (mostly by pressing the back button,
// so we don't display an error message here.
if (uri == null) return@registerForActivityResult
val db: UserDictionaryDatabase? = when (userDictionaryType) {
USER_DICTIONARY_TYPE_FLORIS -> {
dictionaryManager.florisUserDictionaryDatabase()
}
USER_DICTIONARY_TYPE_SYSTEM -> {
dictionaryManager.systemUserDictionaryDatabase()
}
else -> {
showError(Exception("User dictionary type '$userDictionaryType' is not valid!"))
return@registerForActivityResult
}
}
if (db == null) {
showError(NullPointerException("Database handle is null!"))
return@registerForActivityResult
}
db.exportCombinedList(this, uri).onSuccess {
showMessage(R.string.settings__udm__dictionary_export_success)
}.onFailure {
showError(it)
}
}
companion object {
const val EXTRA_USER_DICTIONARY_TYPE: String = "key"
const val USER_DICTIONARY_TYPE_SYSTEM: Int = 1
const val USER_DICTIONARY_TYPE_FLORIS: Int = 2
private const val LEVEL_LANGUAGES: Int = 1
private const val LEVEL_WORDS: Int = 2
private const val SYSTEM_USER_DICTIONARY_SETTINGS_INTENT_ACTION: String =
"android.settings.USER_DICTIONARY_SETTINGS"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
userDictionaryType = intent.getIntExtra(EXTRA_USER_DICTIONARY_TYPE, -1)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setTitle(
when (userDictionaryType) {
USER_DICTIONARY_TYPE_FLORIS -> R.string.settings__udm__title_floris
USER_DICTIONARY_TYPE_SYSTEM -> R.string.settings__udm__title_system
else -> R.string.settings__title
}
)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
dictionaryManager.loadUserDictionariesIfNecessary()
binding.fabAddWord.setOnClickListener { showAddWordDialog() }
binding.recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
buildUi()
}
override fun onCreateBinding(): UdmActivityBinding {
return UdmActivityBinding.inflate(layoutInflater)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.udm_extra_menu, menu)
if (userDictionaryType == USER_DICTIONARY_TYPE_FLORIS) {
menu?.findItem(R.id.udm__open_system_manager_ui)?.isVisible = false
}
return true
}
override fun onDestroy() {
super.onDestroy()
activeDialogWindow?.dismiss()
activeDialogWindow = null
currentLocale = null
}
override fun onResume() {
super.onResume()
buildUi()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
onBackPressed()
true
}
R.id.udm__import -> {
importUserDictionary.launch("*/*")
true
}
R.id.udm__export -> {
exportUserDictionary.launch("my-personal-dictionary.clb")
true
}
R.id.udm__open_system_manager_ui -> {
startActivity(Intent(SYSTEM_USER_DICTIONARY_SETTINGS_INTENT_ACTION))
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onBackPressed() {
if (currentLevel == LEVEL_WORDS) {
currentLevel = LEVEL_LANGUAGES
currentLocale = null
buildUi()
} else {
super.onBackPressed()
}
}
private fun userDictionaryDao(): UserDictionaryDao? {
return when (userDictionaryType) {
USER_DICTIONARY_TYPE_FLORIS -> dictionaryManager.florisUserDictionaryDao()
USER_DICTIONARY_TYPE_SYSTEM -> dictionaryManager.systemUserDictionaryDao()
else -> null
}
}
private fun buildUi() {
when (currentLevel) {
LEVEL_LANGUAGES -> {
languageList = userDictionaryDao()?.queryLanguageList()?.sortedBy { it?.displayLanguage } ?: listOf()
binding.recyclerView.adapter = LanguageEntryAdapter(
languageList.map { getDisplayNameForLocale(it) },
languageListItemClickListener
)
supportActionBar?.subtitle = null
}
LEVEL_WORDS -> {
wordList = userDictionaryDao()?.queryAll(currentLocale) ?: listOf()
binding.recyclerView.adapter = UserDictionaryEntryAdapter(
wordList,
wordListItemClickListener
)
supportActionBar?.subtitle = getDisplayNameForLocale(currentLocale)
}
}
}
private fun getDisplayNameForLocale(locale: Locale?): String {
return locale?.displayName ?: resources.getString(R.string.settings__udm__all_languages)
}
private fun showAddWordDialog() {
val dialogBinding = UdmEntryDialogBinding.inflate(layoutInflater)
dialogBinding.freq.setText(FREQUENCY_DEFAULT.toString())
dialogBinding.freqLabel.hint = String.format(
resources.getString(R.string.settings__udm__dialog__freq_label),
FREQUENCY_MIN,
FREQUENCY_MAX
)
if (currentLevel == LEVEL_WORDS) {
currentLocale?.let {
dialogBinding.locale.setText(it.toString())
}
}
AlertDialog.Builder(this).apply {
setTitle(R.string.settings__udm__dialog__title_add)
setCancelable(true)
setView(dialogBinding.root)
setPositiveButton(R.string.assets__action__add, null)
setNegativeButton(R.string.assets__action__cancel, null)
setOnDismissListener { activeDialogWindow = null }
create()
activeDialogWindow = show()
activeDialogWindow?.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener {
if (processInput(dialogBinding, null)) {
activeDialogWindow?.dismiss()
activeDialogWindow = null
buildUi()
}
}
}
}
private fun showEditWordDialog(entry: UserDictionaryEntry) {
val dialogBinding = UdmEntryDialogBinding.inflate(layoutInflater)
dialogBinding.word.setText(entry.word)
dialogBinding.freq.setText(entry.freq.toString())
dialogBinding.freqLabel.hint = String.format(
resources.getString(R.string.settings__udm__dialog__freq_label),
FREQUENCY_MIN,
FREQUENCY_MAX
)
dialogBinding.shortcut.setText(entry.shortcut ?: "")
dialogBinding.locale.setText(entry.locale ?: "")
AlertDialog.Builder(this).apply {
setTitle(R.string.settings__udm__dialog__title_edit)
setCancelable(true)
setView(dialogBinding.root)
setPositiveButton(R.string.assets__action__apply, null)
setNegativeButton(R.string.assets__action__cancel, null)
setNeutralButton(R.string.assets__action__delete) { _, _ ->
userDictionaryDao()?.delete(entry)
buildUi()
}
setOnDismissListener { activeDialogWindow = null }
create()
activeDialogWindow = show()
activeDialogWindow?.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener {
if (processInput(dialogBinding, entry)) {
activeDialogWindow?.dismiss()
activeDialogWindow = null
buildUi()
}
}
}
}
private fun processInput(dialogBinding: UdmEntryDialogBinding, entry: UserDictionaryEntry?): Boolean {
val word = dialogBinding.word.text?.toString()?.ifBlank { null }
val freq = dialogBinding.freq.text?.toString()?.ifBlank { null }
val shortcut = dialogBinding.shortcut.text?.toString()?.ifBlank { null }
val locale = dialogBinding.locale.text?.toString()?.ifBlank { null }
var isValid = true
if (word == null) {
dialogBinding.word.error = resources.getString(R.string.settings__udm__dialog__word_error_empty)
isValid = false
} else if (word.contains(' ') || word.contains(';') || word.contains('=')) {
dialogBinding.word.error = resources.getString(R.string.settings__udm__dialog__word_error_invalid)
isValid = false
}
if (freq == null) {
dialogBinding.freq.error = resources.getString(R.string.settings__udm__dialog__freq_error_empty)
isValid = false
} else if (runCatching { freq.toInt(10) }.getOrNull() !in FREQUENCY_MIN..FREQUENCY_MAX) {
dialogBinding.freq.error = resources.getString(R.string.settings__udm__dialog__freq_error_invalid)
isValid = false
}
if (shortcut != null && (shortcut.contains(' ') || shortcut.contains(';') || shortcut.contains('='))) {
dialogBinding.shortcut.error = resources.getString(R.string.settings__udm__dialog__shortcut_error_invalid)
isValid = false
}
if (locale != null && (runCatching { LocaleUtils.stringToLocale(locale).isO3Language.ifBlank { null } }.getOrNull() == null)) {
dialogBinding.locale.error = resources.getString(R.string.settings__udm__dialog__locale_error_invalid)
isValid = false
}
if (isValid) {
val localeStr = if (locale == null) { null } else {
LocaleUtils.stringToLocale(locale).toString()
}
if (entry != null) {
userDictionaryDao()?.update(
UserDictionaryEntry(entry.id, word!!, freq!!.toInt(10), localeStr, shortcut)
)
} else {
userDictionaryDao()?.insert(
UserDictionaryEntry(0, word!!, freq!!.toInt(10), localeStr, shortcut)
)
}
}
return isValid
}
}

View File

@@ -17,11 +17,38 @@
package dev.patrickgold.florisboard.settings.fragments
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.dictionary.DictionaryManager
import dev.patrickgold.florisboard.ime.dictionary.FlorisUserDictionaryDatabase
class AdvancedFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.prefs_advanced)
}
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
return when (preference?.key) {
PrefHelper.Devtools.CLEAR_UDM_INTERNAL_DATABASE -> {
AlertDialog.Builder(requireContext()).apply {
setTitle(R.string.assets__action__delete_confirm_title)
setMessage(String.format(resources.getString(R.string.assets__action__delete_confirm_message), FlorisUserDictionaryDatabase.DB_FILE_NAME))
setPositiveButton(R.string.assets__action__delete) { _, _ ->
DictionaryManager.default().let {
it.loadUserDictionariesIfNecessary()
it.florisUserDictionaryDao()?.deleteAll()
}
}
setNegativeButton(android.R.string.cancel, null)
create()
show()
}
true
}
else -> super.onPreferenceTreeClick(preference)
}
}
}

View File

@@ -60,7 +60,7 @@ class TypingFragment : SettingsMainActivity.SettingsFragment() {
.beginTransaction()
.replace(
binding.prefsFrame.id,
SettingsMainActivity.PrefFragment.createFromResource(R.xml.prefs_typing)
TypingInnerFragment()
)
.commit()

View File

@@ -0,0 +1,49 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.settings.fragments
import android.content.Intent
import android.os.Bundle
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.settings.UdmActivity
class TypingInnerFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.prefs_typing)
}
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
return when (preference?.key) {
PrefHelper.Dictionary.MANAGE_SYSTEM_USER_DICTIONARY -> {
val intent = Intent(context, UdmActivity::class.java)
intent.putExtra(UdmActivity.EXTRA_USER_DICTIONARY_TYPE, UdmActivity.USER_DICTIONARY_TYPE_SYSTEM)
startActivity(intent)
true
}
PrefHelper.Dictionary.MANAGE_FLORIS_USER_DICTIONARY -> {
val intent = Intent(context, UdmActivity::class.java)
intent.putExtra(UdmActivity.EXTRA_USER_DICTIONARY_TYPE, UdmActivity.USER_DICTIONARY_TYPE_FLORIS)
startActivity(intent)
true
}
else -> super.onPreferenceTreeClick(preference)
}
}
}

View File

@@ -109,10 +109,11 @@
</LinearLayout>
<LinearLayout
<com.google.android.flexbox.FlexboxLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
app:flexDirection="row"
app:flexWrap="wrap">
<Button
android:id="@+id/theme_export_btn"
@@ -129,7 +130,7 @@
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1.0"/>
app:layout_flexGrow="1"/>
<Button
android:id="@+id/theme_delete_btn"
@@ -155,7 +156,7 @@
android:drawablePadding="8dp"
android:drawableTint="?colorAccent"/>
</LinearLayout>
</com.google.android.flexbox.FlexboxLayout>
</LinearLayout>

View File

@@ -0,0 +1,33 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:theme="@style/SettingsTheme"
tools:context=".settings.UdmActivity">
<include layout="@layout/toolbar"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:windowBackground"
android:layout_marginTop="?actionBarSize"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_add_word"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginBottom="@dimen/fab_margin"
android:layout_marginEnd="@dimen/fab_margin"
android:backgroundTint="?colorPrimary"
app:borderWidth="0dp"
android:src="@drawable/ic_add"
android:contentDescription=""/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,94 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="20dp"
android:paddingTop="16dp"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/word_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/settings__udm__dialog__word_label"
app:boxBackgroundMode="outline"
app:boxBackgroundColor="?android:windowBackground"
app:boxStrokeColor="?colorAccent"
app:boxStrokeErrorColor="?colorError"
app:boxStrokeWidth="1dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/word"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:importantForAutofill="no"
android:inputType="textFilter"
android:imeOptions="flagNoExtractUi"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/freq_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/settings__udm__dialog__freq_label"
app:boxBackgroundMode="outline"
app:boxBackgroundColor="?android:windowBackground"
app:boxStrokeColor="?colorAccent"
app:boxStrokeErrorColor="?colorError"
app:boxStrokeWidth="1dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/freq"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:importantForAutofill="no"
android:inputType="textFilter"
android:imeOptions="flagNoExtractUi"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/shortcut_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/settings__udm__dialog__shortcut_label"
app:boxBackgroundMode="outline"
app:boxBackgroundColor="?android:windowBackground"
app:boxStrokeColor="?colorAccent"
app:boxStrokeErrorColor="?colorError"
app:boxStrokeWidth="1dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/shortcut"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:importantForAutofill="no"
android:inputType="textFilter"
android:imeOptions="flagNoExtractUi"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/locale_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/settings__udm__dialog__locale_label"
app:boxBackgroundMode="outline"
app:boxBackgroundColor="?android:windowBackground"
app:boxStrokeColor="?colorAccent"
app:boxStrokeErrorColor="?colorError"
app:boxStrokeWidth="1dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/locale"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:importantForAutofill="no"
android:inputType="textFilter"
android:imeOptions="flagNoExtractUi"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:icon="@drawable/ic_more_vert"
android:title="@string/settings__menu"
app:showAsAction="always">
<menu>
<item
android:id="@+id/udm__import"
android:orderInCategory="1"
android:title="@string/assets__action__import"/>
<item
android:id="@+id/udm__export"
android:orderInCategory="2"
android:title="@string/assets__action__export"/>
<item
android:id="@+id/udm__open_system_manager_ui"
android:orderInCategory="3"
android:title="@string/settings__udm__open_system_manager_ui"/>
</menu>
</item>
</menu>

View File

@@ -26,6 +26,7 @@
<dimen name="key_numeric_textSize">12sp</dimen>
<dimen name="key_popup_textSize">21sp</dimen>
<dimen name="emoji_key_textSize">22sp</dimen>
<dimen name="devtools_memory_overlay_textSize">8sp</dimen>
<dimen name="landscapeInputUi_padding">8dp</dimen>
<dimen name="landscapeInputUi_actionButton_cornerRadius">6dp</dimen>
@@ -62,9 +63,9 @@
<dimen name="gesture_distance_threshold_very_long">40dp</dimen>
<dimen name="clipboard_text_item_pin_margin" >25dp</dimen>
<integer name="gesture_velocity_threshold_very_slow">50</integer>
<integer name="gesture_velocity_threshold_slow">60</integer>
<integer name="gesture_velocity_threshold_normal">70</integer>
<integer name="gesture_velocity_threshold_fast">80</integer>
<integer name="gesture_velocity_threshold_very_fast">90</integer>
<integer name="gesture_velocity_threshold_very_slow">8000</integer>
<integer name="gesture_velocity_threshold_slow">11000</integer>
<integer name="gesture_velocity_threshold_normal">14000</integer>
<integer name="gesture_velocity_threshold_fast">17000</integer>
<integer name="gesture_velocity_threshold_very_fast">20000</integer>
</resources>

View File

@@ -240,6 +240,15 @@
<string translatable="false" name="pref__suggestion__display_mode__classic" comment="Preference value">Classic (3 columns)</string>
<string translatable="false" name="pref__suggestion__display_mode__dynamic" comment="Preference value">Dynamic width</string>
<string translatable="false" name="pref__suggestion__display_mode__dynamic_scrollable" comment="Preference value">Dynamic width &amp; scrollable</string>
<string name="pref__dictionary__title" comment="Preference group title">Dictionary</string>
<string name="pref__dictionary__enable_system_user_dictionary__label" comment="Preference title">Enable system user dictionary</string>
<string name="pref__dictionary__enable_system_user_dictionary__summary" comment="Preference summary">Suggest words stored in the system user dictionary</string>
<string name="pref__dictionary__manage_system_user_dictionary__label" comment="Preference title">Manage system user dictionary</string>
<string name="pref__dictionary__manage_system_user_dictionary__summary" comment="Preference summary">Add, view, and remove entries for the system user dictionary</string>
<string name="pref__dictionary__enable_internal_user_dictionary__label" comment="Preference title">Enable internal user dictionary</string>
<string name="pref__dictionary__enable_internal_user_dictionary__summary" comment="Preference summary">Suggest words stored in the internal user dictionary</string>
<string name="pref__dictionary__manage_floris_user_dictionary__label" comment="Preference title">Manage internal user dictionary</string>
<string name="pref__dictionary__manage_floris_user_dictionary__summary" comment="Preference summary">Add, view, and remove entries for the internal user dictionary</string>
<string name="pref__correction__title" comment="Preference group title">Corrections</string>
<string name="pref__correction__auto_capitalization__label" comment="Preference title">Auto-capitalization</string>
<string name="pref__correction__auto_capitalization__summary" comment="Preference summary">Capitalize words based on the current input context</string>
@@ -248,6 +257,27 @@
<string name="pref__correction__double_space_period__label" comment="Preference title">Double-space period</string>
<string name="pref__correction__double_space_period__summary" comment="Preference summary">Tapping twice on spacebar inserts a period followed by a space</string>
<string name="settings__udm__title_floris" comment="Title of the User Dictionary Manager activity for internal">Internal User Dictionary</string>
<string name="settings__udm__title_system" comment="Title of the User Dictionary Manager activity for system">System User Dictionary</string>
<string name="settings__udm__word_summary_freq" comment="Summary label for a word entry. The decimal placeholder inserts the frequency for the word it summarizes.">Frequency: %d</string>
<string name="settings__udm__word_summary_freq_shortcut" comment="Summary label for a word entry. The first placeholder inserts the frequency for the word it summarizes, the second placeholder the shortcut defined.">Frequency: %d | Shortcut: %s</string>
<string name="settings__udm__all_languages" comment="Label of the For all languages entry in the language list">For all languages</string>
<string name="settings__udm__open_system_manager_ui" comment="Label of the Open system manager UI menu option">Open system manager UI</string>
<string name="settings__udm__dictionary_import_success" comment="Message for dictionary import success">User dictionary imported successfully!</string>
<string name="settings__udm__dictionary_export_success" comment="Message for dictionary export success">User dictionary exported successfully!</string>
<string name="settings__udm__dialog__title_add" comment="Label for the title (when in adding mode) in the user dictionary add/edit dialog">Add word entry</string>
<string name="settings__udm__dialog__title_edit" comment="Label for the title (when in editing mode) in the user dictionary add/edit dialog">Edit word entry</string>
<string name="settings__udm__dialog__word_label" comment="Label for the word in the user dictionary add/edit dialog">Word</string>
<string name="settings__udm__dialog__word_error_empty" comment="Error label for the word in the user dictionary add/edit dialog">Please enter a word!</string>
<string name="settings__udm__dialog__word_error_invalid" comment="Error label for the word in the user dictionary add/edit dialog">This word contains invalid characters.</string>
<string name="settings__udm__dialog__freq_label" comment="Label for the frequency in the user dictionary add/edit dialog. The two decimal placeholders are the minimum and maximum frequency, both inclusive.">Frequency (between %d and %d)</string>
<string name="settings__udm__dialog__freq_error_empty" comment="Error label for the frequency in the user dictionary add/edit dialog">Please enter a frequency value!</string>
<string name="settings__udm__dialog__freq_error_invalid" comment="Error label for the frequency in the user dictionary add/edit dialog">Please enter a valid number within the specified bounds!</string>
<string name="settings__udm__dialog__shortcut_label" comment="Label for the shortcut in the user dictionary add/edit dialog">Shortcut (optional)</string>
<string name="settings__udm__dialog__shortcut_error_invalid" comment="Error label for the shortcut in the user dictionary add/edit dialog">This shortcut contains invalid characters.</string>
<string name="settings__udm__dialog__locale_label" comment="Label for the language code in the user dictionary add/edit dialog">Language code (optional)</string>
<string name="settings__udm__dialog__locale_error_invalid" comment="Error label for the language code in the user dictionary add/edit dialog">This language code does not conform to the expected syntax. The code must either be a language only (like en), a language and country (like en_US) or a language, country, and script (like en_US-script).</string>
<string name="settings__gestures__title" comment="Title of Gestures fragment">Gestures &amp; Glide typing</string>
<string name="pref__glide__title" comment="Preference group title">Glide typing</string>
<string name="pref__glide__enabled__label" comment="Preference title">Enable glide typing</string>
@@ -310,6 +340,12 @@
<string name="pref__advanced__show_app_icon__label" comment="Label of Show app icon preference in Advanced">Show app icon in launcher</string>
<string name="pref__advanced__force_private_mode__label" comment="Label of Force private mode preference in Advanced">Force private mode</string>
<string name="pref__advanced__force_private_mode__summary" comment="Summary of Force private mode preference in Advanced">Will disable any features which have to temporarily work with your input data</string>
<string name="pref__devtools__enabled__label" comment="Label of Enable developer tools in Advanced">Enable developer tools</string>
<string name="pref__devtools__enabled__summary" comment="Summary of Enable developer tools in Advanced">Tools specifically designed for debugging and troubleshooting</string>
<string name="pref__devtools__show_heap_memory_stats__label" comment="Label of Show heap memory stats in Advanced">Show heap memory stats</string>
<string name="pref__devtools__show_heap_memory_stats__summary" comment="Summary of Show heap memory stats in Advanced">Overlays the heap memory usage and max size in the top-right corner</string>
<string name="pref__devtools__clear_udm_internal_database__label" comment="Label of Clear internal user dictionary database in Advanced">Clear internal user dictionary database</string>
<string name="pref__devtools__clear_udm_internal_database__summary" comment="Summary of Clear internal user dictionary database in Advanced">Clears all words from the dictionary database table</string>
<!-- About UI strings -->
<string name="about__title" comment="Title of About activity">About</string>
@@ -328,6 +364,7 @@
<string name="assets__file__name">Name</string>
<string name="assets__file__source">Source</string>
<string name="assets__action__add">Add</string>
<string name="assets__action__apply">Apply</string>
<string name="assets__action__cancel">Cancel</string>
<string name="assets__action__cancel_confirm_title">Confirm cancel</string>
<string name="assets__action__cancel_confirm_message">Are you sure you want to discard any unsaved changes? This action can not be undone once executed.</string>
@@ -376,7 +413,9 @@
<string name="crash_dialog__description" comment="Description of crash dialog">Sorry for the inconvenience, but FlorisBoard has crashed due to an unexpected error.</string>
<string name="crash_dialog__report_instructions" comment="Issue tracker report instructions for the crash dialog. The %s placeholder is the name of the crash report template and always in English/LTR.">If you wish to report this error, first check out the issue tracker on GitHub if your crash has not already been reported.\nIf it hasn\'t, copy the generated crash log and open a new issue. Use the \"%s\" template and fill out the description, the steps to reproduce, and paste the generated crash log at the end. This helps in making FlorisBoard better and more stable for everyone. Thank you!</string>
<string name="crash_dialog__bug_report_template" comment="Name of the template to use in the GitHub issue tracker (is always in English)" translatable="false">Crash report</string>
<string name="crash_dialog__copy_to_clipboard" comment="Label of Copy to clipboard button in crash dialog">Copy to clipboard</string>
<string name="crash_dialog__copy_to_clipboard" comment="Label of Copy to clipboard button in crash dialog">Copy to system clipboard</string>
<string name="crash_dialog__copy_to_clipboard_success" comment="Label of Copy to clipboard success message in crash dialog">Copied to system clipboard</string>
<string name="crash_dialog__copy_to_clipboard_failure" comment="Label of Copy to clipboard failure message in crash dialog">Cannot copy to system clipboard: Clipboard manager instance not found</string>
<string name="crash_dialog__open_issue_tracker" comment="Label of Open issue tracker button in crash dialog">Open issue tracker (github.com)</string>
<string name="crash_dialog__close" comment="Label of Close button in crash dialog">Close</string>
<string name="crash_notification_channel__title" comment="Title of crash notification channel">FlorisBoard error reports</string>

View File

@@ -26,4 +26,29 @@
app:summary="@string/pref__advanced__force_private_mode__summary"
app:useSimpleSummaryProvider="true"/>
<SwitchPreferenceCompat
android:defaultValue="false"
app:key="devtools__enabled"
app:iconSpaceReserved="false"
app:title="@string/pref__devtools__enabled__label"
app:summary="@string/pref__devtools__enabled__summary"
app:useSimpleSummaryProvider="true"/>
<SwitchPreferenceCompat
android:defaultValue="false"
app:dependency="devtools__enabled"
app:key="devtools__show_heap_memory_stats"
app:iconSpaceReserved="false"
app:title="@string/pref__devtools__show_heap_memory_stats__label"
app:summary="@string/pref__devtools__show_heap_memory_stats__summary"
app:useSimpleSummaryProvider="true"/>
<Preference
app:dependency="devtools__enabled"
app:key="devtools__clear_udm_internal_database"
app:iconSpaceReserved="false"
app:title="@string/pref__devtools__clear_udm_internal_database__label"
app:summary="@string/pref__devtools__clear_udm_internal_database__summary"
app:useSimpleSummaryProvider="true"/>
</PreferenceScreen>

View File

@@ -171,14 +171,14 @@
app:title="@string/pref__gestures__delete_key_swipe_left__label"
app:useSimpleSummaryProvider="true"/>
<!--<ListPreference
<ListPreference
app:iconSpaceReserved="false"
android:defaultValue="normal"
app:entries="@array/pref__gestures__swipe_velocity_threshold__entries"
app:entryValues="@array/pref__gestures__swipe_velocity_threshold__values"
app:key="gestures__swipe_velocity_threshold"
app:title="@string/pref__gestures__swipe_velocity_threshold__label"
app:useSimpleSummaryProvider="true"/>-->
app:useSimpleSummaryProvider="true"/>
<ListPreference
app:iconSpaceReserved="false"

View File

@@ -69,6 +69,42 @@
</PreferenceCategory>
<PreferenceCategory
app:iconSpaceReserved="false"
app:title="@string/pref__dictionary__title">
<SwitchPreferenceCompat
android:defaultValue="true"
app:dependency="suggestion__enabled"
app:key="suggestion__enable_system_user_dictionary"
app:iconSpaceReserved="false"
app:title="@string/pref__dictionary__enable_system_user_dictionary__label"
app:summary="@string/pref__dictionary__enable_system_user_dictionary__summary"/>
<Preference
app:dependency="suggestion__enable_system_user_dictionary"
app:key="suggestion__manage_system_user_dictionary"
app:iconSpaceReserved="false"
app:title="@string/pref__dictionary__manage_system_user_dictionary__label"
app:summary="@string/pref__dictionary__manage_system_user_dictionary__summary"/>
<SwitchPreferenceCompat
android:defaultValue="true"
app:dependency="suggestion__enabled"
app:key="suggestion__enable_floris_user_dictionary"
app:iconSpaceReserved="false"
app:title="@string/pref__dictionary__enable_internal_user_dictionary__label"
app:summary="@string/pref__dictionary__enable_internal_user_dictionary__summary"/>
<Preference
app:dependency="suggestion__enable_floris_user_dictionary"
app:key="suggestion__manage_floris_user_dictionary"
app:iconSpaceReserved="false"
app:title="@string/pref__dictionary__manage_floris_user_dictionary__label"
app:summary="@string/pref__dictionary__manage_floris_user_dictionary__summary"/>
</PreferenceCategory>
<PreferenceCategory
app:iconSpaceReserved="false"
app:title="@string/pref__correction__title">

View File

@@ -9,7 +9,7 @@ subprojects {
repositories {
mavenCentral()
google()
jcenter()
jcenter() // Cannot remove jcenter as of now because flexbox depends on it
}
}

View File

@@ -1 +1 @@
An open-source keyboard which respects your privacy. Currently in early-beta stage.
An open-source keyboard which respects your privacy. Currently in early-beta.

View File

@@ -0,0 +1,7 @@
- Add "Copied to system clipboard" to crash dialog (#724)
- Fix adaptive theme memory management (#763)
- Adaptive themes are now available again
- Fix keyboard preview visual bugs (#776)
- Fix theme manager buttons not wrapping (#777)
- Fix Double NaN crashes (#774, #790)
- Fix gestures (shift+space and space bar up now work again properly)

View File

@@ -0,0 +1,9 @@
- Add Schwa symbol (ə) in Italian extended popups (#693)
- Add developer tools: Overlay display for used heap memory (#807)
- Icons are now auto-scale based on the set font size multiplier (#540)
- Fix emoji key view initial memory usage (#718)
- Fix font size multiplier not applied (#808)
- Fix text key drawables not applying the color correctly (#778)
- Fix bottom row keys not shifted in Dvorak layout (#805)
- Fix Hungarian layout not containing special keys
- Fix Arabic Letter Waw with Hamza Above not written correctly (#438)

View File

@@ -0,0 +1,5 @@
- Add support for system and internal user dictionary (#817)
- Fix AppCompat theme crash for Huawei devices (#799, #809)
- Fix dynamic text size infinite loop bug (#825)
- Possibly fixed a lot of hanging keyboard errors
which were reported since beta02.

View File

@@ -1 +1 @@
Beta track of FlorisBoard, the open-source keyboard which respects your privacy.
Beta of FlorisBoard, the open-source keyboard which respects your privacy.