Compare commits
33 Commits
v0.3.11-be
...
v0.3.11-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3e8d35e5d | ||
|
|
da8073141e | ||
|
|
030665732a | ||
|
|
cc042dd77c | ||
|
|
773624769d | ||
|
|
0b1d0c74fe | ||
|
|
760d307478 | ||
|
|
084c2abfc2 | ||
|
|
df6b08024f | ||
|
|
25498695ef | ||
|
|
5c81179d60 | ||
|
|
58d150bb03 | ||
|
|
2b1951ea5f | ||
|
|
5a5089c413 | ||
|
|
dcd20e4b73 | ||
|
|
dfec1f3804 | ||
|
|
1fffe7f6e5 | ||
|
|
862a6cc82a | ||
|
|
068caaf09b | ||
|
|
93fb6d6016 | ||
|
|
28f0657bd7 | ||
|
|
8c53c2a057 | ||
|
|
6251fb2ef6 | ||
|
|
cba2b873b8 | ||
|
|
d7ee61f316 | ||
|
|
cf309f43a4 | ||
|
|
93acee778e | ||
|
|
c7f2f31c99 | ||
|
|
ebb8837d8a | ||
|
|
f04f185034 | ||
|
|
20de007d3b | ||
|
|
df01f6fe57 | ||
|
|
f9e6d7b09c |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -39,4 +39,7 @@ captures/
|
||||
|
||||
# Keystore files
|
||||
*.jks
|
||||
crowdin.properties
|
||||
crowdin.properties
|
||||
|
||||
# AndroidX Room schema JSONs
|
||||
/app/schemas/
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": "ة" },
|
||||
|
||||
@@ -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": "ë" }
|
||||
|
||||
@@ -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": "ü" }
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
]
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalContracts::class, ExperimentalUnsignedTypes::class)
|
||||
@file:OptIn(ExperimentalContracts::class)
|
||||
|
||||
package dev.patrickgold.florisboard.debug
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
})
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ class TypingFragment : SettingsMainActivity.SettingsFragment() {
|
||||
.beginTransaction()
|
||||
.replace(
|
||||
binding.prefsFrame.id,
|
||||
SettingsMainActivity.PrefFragment.createFromResource(R.xml.prefs_typing)
|
||||
TypingInnerFragment()
|
||||
)
|
||||
.commit()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
33
app/src/main/res/layout/udm_activity.xml
Normal file
33
app/src/main/res/layout/udm_activity.xml
Normal 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>
|
||||
94
app/src/main/res/layout/udm_entry_dialog.xml
Normal file
94
app/src/main/res/layout/udm_entry_dialog.xml
Normal 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>
|
||||
29
app/src/main/res/menu/udm_extra_menu.xml
Normal file
29
app/src/main/res/menu/udm_extra_menu.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 & 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 & 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -9,7 +9,7 @@ subprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
jcenter()
|
||||
jcenter() // Cannot remove jcenter as of now because flexbox depends on it
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
7
fastlane/metadata/androidbeta/en-US/changelogs/38.txt
Normal file
7
fastlane/metadata/androidbeta/en-US/changelogs/38.txt
Normal 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)
|
||||
9
fastlane/metadata/androidbeta/en-US/changelogs/39.txt
Normal file
9
fastlane/metadata/androidbeta/en-US/changelogs/39.txt
Normal 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)
|
||||
5
fastlane/metadata/androidbeta/en-US/changelogs/40.txt
Normal file
5
fastlane/metadata/androidbeta/en-US/changelogs/40.txt
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user