Compare commits

...

86 Commits

Author SHA1 Message Date
Patrick Goldinger
e9e2563739 Release v0.3.13-beta02 2021-05-26 01:26:33 +02:00
Patrick Goldinger
87bb098445 Fix batch level preventing cached input from updating 2021-05-26 01:26:17 +02:00
Patrick Goldinger
da1944bedf Temporarily remove key shadow support (#943) 2021-05-26 01:09:50 +02:00
Patrick Goldinger
d4a92e0d46 Merge pull request #942 from florisboard/new-touch-logic
Introduce new touch logic to TextKeyboardView
2021-05-26 00:46:31 +02:00
yashpalgoyal1304
0fa6c1f235 Added Indic Numerals (#940)
* Indic Devanagari Numeric

* Fixed name and label

* Fixed file name

* Added indic scripts numerals
2021-05-26 00:43:21 +02:00
Patrick Goldinger
260b1ba5ca Improve touch logic 2021-05-26 00:19:35 +02:00
Patrick Goldinger
f0799a6a0e Rework text keyboard view touch logic 2021-05-25 20:48:17 +02:00
Patrick Goldinger
155238946a Merge pull request #866 from Hayleia/composing1
Composing input method (and Korean as the first subject)
2021-05-24 15:30:06 +02:00
Patrick Goldinger
45f91cf40c Merge pull request #928 from ostrya/fix-hint-merge
fix hint merge logic (#872)
2021-05-23 16:27:22 +02:00
Patrick Goldinger
94f5b56b6a Possibly fix key shadow performance 2021-05-23 16:19:28 +02:00
Kai Helbig
46db467073 fix hint merge logic (#872)
The merge of the hints depends on the underlying main key. Especially,
hints should only be shown for character keys, and if the hint is
identical to the main key, it should not be added at all. Since the
actual main key is only evaluated on demand with TextKey#compute, all
corresponding hint merge logic needs to be moved there too.
2021-05-23 12:16:33 +02:00
Patrick Goldinger
17dde536d9 Fix one-handed panel not correctly measuring sometimes (#896) 2021-05-23 03:50:17 +02:00
Patrick Goldinger
be67bf4b84 Fix Smartbar number row bugs in password fields (#905) 2021-05-23 03:19:17 +02:00
Patrick Goldinger
8f142548fe Merge pull request #920 from tsiflimagas/default-popup-fix-greek
Fix the default popup for some letters
2021-05-23 02:49:28 +02:00
Kostas Giapis
a68f439f39 Enforce the main popup character 2021-05-22 23:01:04 +03:00
Patrick Goldinger
7a0892bb36 Fix space bar text too large (#862) 2021-05-22 20:16:55 +02:00
Patrick Goldinger
8457390156 Fix keys not showing a shadow (#901, #921) 2021-05-22 19:54:12 +02:00
Hayleia
72be3898c1 move local function out, and fix firefox url bar? 2021-05-22 19:47:30 +02:00
Kostas Giapis
d35bf5af63 Fix the default popup for some letters 2021-05-22 16:23:13 +03:00
Patrick Goldinger
04d3af6484 Merge pull request #908 from Luensche/copy-versionstring
Copy version string to clipboard on click on the version
2021-05-22 12:59:46 +02:00
Björn Engel
26920e4a98 Move the toast outside of if 2021-05-20 14:44:23 +02:00
Björn Engel
7419966b51 Create ripple for click on head_area 2021-05-20 14:37:17 +02:00
Björn Engel
58b832c6c3 Add new area for long pressing and change to onLongClickListener 2021-05-20 10:20:49 +02:00
Hayleia
99f2ec1879 deprecated methods 2021-05-19 11:47:28 +02:00
Hayleia
4249f9ef86 add author 2021-05-19 11:39:13 +02:00
Hayleia
60107ae299 useless "public" keyword 2021-05-19 09:11:07 +02:00
Hayleia
6a95a865fa one spinner per linear layout 2021-05-19 09:09:14 +02:00
Hayleia
9e32589af5 style: space before colon 2021-05-19 09:04:30 +02:00
Hayleia
6133e225e1 add author 2021-05-19 09:03:34 +02:00
Hayleia
348c143d92 use case_selector to specify shift/non-shift characters 2021-05-19 08:59:52 +02:00
Hayleia
ce00785ffe Revert "support specifying uppercase and lowercase separately in json"
This reverts commit 1715e5ddfa.

Conflicts:
	app/src/main/java/dev/patrickgold/florisboard/ime/extension/AssetManager.kt
2021-05-19 08:24:51 +02:00
Hayleia
78cdce750d style in json 2021-05-19 08:22:25 +02:00
Patrick Goldinger
f3f95ae282 Fix crash loops from occurring after a crash (#910) 2021-05-19 01:33:53 +02:00
Björn Engel
018885eb30 Copy version string to clipboard on click on the version 2021-05-18 15:18:01 +02:00
Patrick Goldinger
c6c8a76dd6 Fix user dictionary max size (#898) 2021-05-18 01:51:49 +02:00
Patrick Goldinger
3cae8b7230 Release v0.3.13-beta01 2021-05-17 20:40:39 +02:00
Patrick Goldinger
814c8de0c2 Update translations from Crowdin 2021-05-17 20:30:37 +02:00
Patrick Goldinger
32fe175b48 Small code base improvements 2021-05-17 20:27:32 +02:00
Patrick Goldinger
b901f6de8d Fix space bar gestures for non-repeating actions (#886) 2021-05-17 20:13:50 +02:00
Patrick Goldinger
fe9ba3246c Merge pull request #884 from debnone/patch-1
Fix hebrew characters
2021-05-17 19:52:32 +02:00
Patrick Goldinger
71a39f0fc1 Merge pull request #876 from florisboard/android11-autofill-api
Add support for Android 11's Autofill API
2021-05-17 10:56:31 +02:00
Patrick Goldinger
f7556898e1 Document inline suggestions code / Fix some inconsistencies 2021-05-17 03:01:46 +02:00
Patrick Goldinger
578539f5d0 Add inline suggestions theme support 2021-05-17 02:04:52 +02:00
debnone
7c28c7fbea Fix hebrew characters
fixed bottom half layout its was reversed and incorrect.
2021-05-15 23:17:28 +03:00
Patrick Goldinger
88bcadff81 Fix inline suggestions state bugs and improve logic 2021-05-15 04:50:49 +02:00
Patrick Goldinger
25e25dfbf0 Add support for Android 11's Autofill API 2021-05-15 03:23:51 +02:00
Patrick Goldinger
ba3dc0178d Merge pull request #875 from X-yl/glide-number-row
Reinitialize pruner when layout changes
2021-05-15 03:20:23 +02:00
x-yl
91e7f424bb Reinitialize pruner when layout changes
Closes #854
2021-05-14 22:16:10 +04:00
Hayleia
b89f791eb0 rename south korean layout 2021-05-14 07:51:51 +02:00
Hayleia
ad3a0425ab fix config.json after merge 2021-05-14 07:51:40 +02:00
Hayleia
7cf52ecf3e Merge branch 'master' of https://github.com/florisboard/florisboard into composing1 2021-05-14 07:35:56 +02:00
Patrick Goldinger
b1ef18f4fd Improve C++ code base 2021-05-14 00:30:19 +02:00
Hayleia
b74af5bbe9 manage old subtype configurations 2021-05-13 20:48:00 +02:00
Hayleia
b8aa4bbfc4 fix subtype equals and hashcode (and javadoc) 2021-05-13 20:16:50 +02:00
Hayleia
e024ac9272 fix default subtype crash with no subtype declared 2021-05-13 20:03:47 +02:00
Hayleia
c5fa027a8e move composer dropdown in add/edit subtype dialog 2021-05-13 16:39:32 +02:00
Hayleia
b6ec2b25be Merge branch 'master' of https://github.com/florisboard/florisboard into composing1 2021-05-13 16:25:13 +02:00
Patrick Goldinger
a756b59c60 Merge pull request #606 from ostrya/improved-hints
Merge hints more flexibly
2021-05-13 14:04:08 +02:00
Patrick Goldinger
8687ce55ed Merge pull request #527 from ostrya/neo2-layout
Neo2 layout
2021-05-13 14:04:01 +02:00
ostrya
1ac6985dd0 Allow merging popups of hints #618
A new configuration was introduced to allow showing the popup keys of
the hint keys of a given character key in addition the character key's
normal popup keys.

The previous change allowed both number and symbol hint to be merged at
the same time, with the number hint being shown as popup only.
Therefore, when allowing the popups of the hint key to be shown as
popups, both hint keys need to be taken into account.

To ensure this and also take into account the separate key hint
settings for number and symbol hints, the MutablePopupSet was extended
to contain both hint keys as well as both lists of popup keys in
addition to the existing main key and relevant list. The logic that
chooses the key prioritization when rendering the popup has now also
been moved from the PopupManager to the PopupSet.

For performance, the prioritized collection of popup keys is generated
once and then cached for a given configuration in a new PopupKeys
object. This class now has the collection semantics previously present
in the PopupSet class. Different from before, the PopupKeys object now
explicitly contains the prioritized keys (those that should be shown
directly above the original key for easier reach) in order of priority.

The PopupManager now only needs to take the number of prioritized keys
(maximum 3: main key, number hint, symbol hint) when calculating the
key positions in the popup.
2021-05-13 11:52:53 +02:00
Patrick Goldinger
986b4a878f Merge pull request #858 from florisboard/java-jni-basics
Set up base for Kotlin/C++ interoperability
2021-05-13 00:33:10 +02:00
Patrick Goldinger
1ef38fe7f3 Fix GitHub workflows not setting up cmake 2021-05-12 20:31:34 +02:00
Patrick Goldinger
bcad0af35e Finalize base implementation for SuggestionList 2021-05-12 19:29:21 +02:00
Patrick Goldinger
b5b89fde4f Add native instance wrapper interface / Clean up code 2021-05-12 02:25:41 +02:00
Patrick Goldinger
be1fc710ed Set up base for Kotlin/C++ interoperability 2021-05-12 00:40:53 +02:00
Kai Helbig
aa55fd3070 Directly merge numeric and symbolic hints
Co-authored-by: Patrick Goldinger <patrick.goldinger@pm.me>
2021-05-11 23:58:31 +02:00
ostrya
a132462466 Merge hints more flexibly
To allow symbol layouts with the same or more rows as the character
layout to be hinted more consistently, the hinting of the numeric row
is split from the rest of the symbol layout.

If enabled, the numeric row hinting is always done in the first row.
If an actual numeric row is enabled as well, no additional numeric
hints will be shown (as they are only added to CHARACTER type keys).

The symbol hinting is now bottom-aligned: hints from the last symbol
row are shown in the last character row.

If the symbol layout (excluding numeric row) has at least the same
number of rows as the character layout, the numerical row is disabled
and numerical hinting is enabled, the symbol keys take precedence. The
numeric hints are instead added as additional popup characters.
2021-05-11 23:58:25 +02:00
Hayleia
df393ff607 composers can be specified in config.json
no compatibility with previous settings, need to update the regex
2021-05-11 19:03:30 +02:00
Hayleia
88a6f436ef Merge branch 'master' of https://github.com/florisboard/florisboard into composing1 2021-05-05 10:02:17 +02:00
ostrya
ee8f44d816 Use new currency set mechanism 2021-05-04 20:52:53 +02:00
ostrya
0308ec355f Adapt to new layout rework 2021-05-04 20:44:57 +02:00
Hayleia
3ac14f8a2a remove pointless reflection (going to use serialization anyways) 2021-05-04 20:16:23 +02:00
Hayleia
2b087b76dc korean double consonants and two vowels on shift key 2021-05-04 20:12:03 +02:00
Hayleia
1715e5ddfa support specifying uppercase and lowercase separately in json 2021-05-04 20:11:27 +02:00
Hayleia
6cc17161a5 factor stuff 2021-05-03 21:00:04 +02:00
Hayleia
5d1c20617b Merge branch 'master' of https://github.com/florisboard/florisboard into composing1 2021-05-03 19:22:23 +02:00
Hayleia
d9efa48c9c copy pasted code to compose texte with suggestions enabled too 2021-05-03 19:15:03 +02:00
ostrya
dedd4cb7f0 Use custom modifier for symbol layer
To make the switch from character to symbol layer more consistent,
a neo specific symbol modifier layout was added. This also allows
overriding the comma and full stop with their layer 3 equivalents.
2021-05-02 17:06:07 +02:00
ostrya
42b147b656 Add neo/bone locale variant for better compatibility
The default de locale already defines a lot of extended popups which
do not match the Neo2 / Bone layout logic. Adding a locale variant
allows overriding those defaults.

As the Locale class does not support arbitrary country keys, the new
locale was chosen as a variant of de_DE with variant name "neobone".
There is no deep meaning in the name, it is only the concatenation of
neo and bone, and according to the Javadoc of Locale, a valid variant
must have either 5 to 8 characters or start with a number.
2021-05-02 17:06:06 +02:00
ostrya
47ce490d6c Initial attempt at Neo2 / Bone layout (#498)
* For now, only layers 1, 2 and 3 are supported.
* Layer 2 is reachable via caps, apart from number row, comma and full
  stop (which I think are easier to use if not affected by caps).
  Instead, the relevant characters are added as popups.
* Layer 3 is set up as a separate neo2 symbol / number row layer

The overall layout is kept as much as possible, with the following
exceptions:
* The number row contains only numbers and minus sign, while circumflex
  and grave accents are not included.
* To not overcrowd the layout and have the same number of keys for
  first and second row, the acute accent is not included as separate
  key but can be reached as additional popup to sharp s.
* Comma and full stop are not put between m and j (or z and k
  respectively), because the backspace takes up too much space for both
  keys to be put in this row.
* Also, having comma and full stop on the same height with the space
  key makes the layout more consistent with the existing layouts and
  the special usage as ~left and ~right keys.
2021-05-02 17:05:59 +02:00
Hayleia
5563a1cadd merge compatibility 2021-05-01 20:30:24 +02:00
Hayleia
7beb2e5ef6 Merge branch 'master' of https://github.com/florisboard/florisboard into composing1 2021-05-01 19:22:19 +02:00
Hayleia
f00da13cba less kotlin warnings and slightly more usable code
still hardcoded korean composer for all layouts
but at least it's not instanciated at every keypress
2021-05-01 09:34:40 +02:00
Hayleia
bfed1747f7 better korean jsons 2021-05-01 09:19:59 +02:00
Hayleia
abb4b104fa fix input being ignored sometimes? 2021-04-30 13:12:28 +02:00
Hayleia
b69b1caa72 Test Korean composition
currency is wrong
code is plugged at the wrong place
input is ignored sometimes
there is reflection for what seems to be no reason
I know, this is just a test and this will either be done again (properly) on another branch or discarded altogether
2021-04-30 07:31:32 +02:00
108 changed files with 4407 additions and 1570 deletions

View File

@@ -9,7 +9,7 @@ insert_final_newline = true
max_line_length = 120
trim_trailing_whitespace = true
[{*.har,*.json}]
[{*.har,*.json,*yml}]
indent_size = 2
[*.kt]

View File

@@ -16,6 +16,8 @@ jobs:
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Setup CMake and Ninja
uses: lukka/get-cmake@v3.20.1
- uses: actions/cache@v2
with:
path: |
@@ -25,7 +27,7 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-
- name: Build with Gradle
run: ./gradlew clean assemble
run: ./gradlew clean assembleDebug
- uses: actions/upload-artifact@v2
with:
name: app-debug.apk

3
.gitignore vendored
View File

@@ -41,5 +41,8 @@ captures/
*.jks
crowdin.properties
# C++
.cxx/
# AndroidX Room schema JSONs
/app/schemas/

View File

@@ -96,6 +96,7 @@ milestones, please refer to the [Feature roadmap](#feature-roadmap).
* [x] User dictionary manager (system and internal)
### Other useful features
* [x] Support for Android 11+ inline autofill API
* [x] One-handed mode
* [x] Clipboard/cursor tools
* [x] Clipboard manager/history

View File

@@ -1,6 +1,6 @@
plugins {
id("com.android.application") version "4.2.0"
id("com.android.application") version "4.2.1"
kotlin("android") version "1.5.0"
kotlin("kapt") version "1.5.0"
kotlin("plugin.serialization") version "1.5.0"
@@ -24,8 +24,8 @@ android {
applicationId = "dev.patrickgold.florisboard"
minSdkVersion(23)
targetSdkVersion(30)
versionCode(43)
versionName("0.3.12")
versionCode(45)
versionName("0.3.13")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -38,17 +38,33 @@ android {
)
}
}
externalNativeBuild {
cmake {
cppFlags("-std=c++17", "-fexceptions", "-frtti")
arguments("-DANDROID_STL=c++_static")
}
}
}
buildFeatures {
viewBinding = true
}
externalNativeBuild {
cmake {
path("src/main/cpp/CMakeLists.txt")
}
}
buildTypes {
named("debug").configure {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
isDebuggable = true
isJniDebuggable = true
resValue("mipmap", "floris_app_icon", "@mipmap/ic_app_icon_debug")
resValue("mipmap", "floris_app_icon_round", "@mipmap/ic_app_icon_debug_round")
resValue("string", "floris_app_name", "FlorisBoard Debug")
@@ -57,7 +73,7 @@ android {
create("beta") // Needed because by default the "beta" BuildType does not exist
named("beta").configure {
applicationIdSuffix = ".beta"
versionNameSuffix = "-beta06"
versionNameSuffix = "-beta02"
proguardFiles.add(getDefaultProguardFile("proguard-android-optimize.txt"))
resValue("mipmap", "floris_app_icon", "@mipmap/ic_app_icon_beta")
@@ -89,6 +105,7 @@ android {
dependencies {
implementation("androidx.activity", "activity-ktx", "1.2.1")
implementation("androidx.appcompat", "appcompat", "1.2.0")
implementation("androidx.autofill", "autofill", "1.1.0")
implementation("androidx.core", "core-ktx", "1.3.2")
implementation("androidx.fragment", "fragment-ktx", "1.3.0")
implementation("androidx.preference", "preference-ktx", "1.1.1")

View File

@@ -34,13 +34,10 @@
android:name="dev.patrickgold.florisboard.ime.core.FlorisBoard"
android:label="@string/floris_app_name"
android:permission="android.permission.BIND_INPUT_METHOD">
<meta-data
android:name="android.view.im"
android:resource="@xml/method"/>
<intent-filter>
<action android:name="android.view.InputMethod"/>
</intent-filter>
<meta-data android:name="android.view.im" android:resource="@xml/method"/>
</service>
<!-- Settings Activity -->

View File

@@ -1,5 +1,9 @@
{
"package": "dev.patrickgold.florisboard",
"composers": [
{ "$": "appender" },
{ "$": "hangul-unicode" }
],
"currencySets": [
{
"name": "azerbaijani_manat",
@@ -246,6 +250,7 @@
{
"id": 101,
"languageTag": "en-US",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "qwerty"
@@ -254,6 +259,7 @@
{
"id": 102,
"languageTag": "en-UK",
"composer": "appender",
"currencySet": "pound",
"preferred": {
"characters": "qwerty"
@@ -262,6 +268,7 @@
{
"id": 103,
"languageTag": "en-CA",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "qwerty"
@@ -270,6 +277,7 @@
{
"id": 104,
"languageTag": "en-AU",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "qwerty"
@@ -278,6 +286,7 @@
{
"id": 201,
"languageTag": "de-DE",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "qwertz"
@@ -286,6 +295,7 @@
{
"id": 202,
"languageTag": "de-AT",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "qwertz"
@@ -294,14 +304,27 @@
{
"id": 203,
"languageTag": "de-CH",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "swiss_german"
}
},
{
"id": 204,
"languageTag": "de-DE-neobone",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "neo2",
"symbols": "neo2",
"numericRow": "neo2"
}
},
{
"id": 301,
"languageTag": "fr-FR",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "azerty"
@@ -310,6 +333,7 @@
{
"id": 302,
"languageTag": "fr-CA",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "canadian_french"
@@ -318,6 +342,7 @@
{
"id": 303,
"languageTag": "fr-CH",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "swiss_french"
@@ -326,6 +351,7 @@
{
"id": 401,
"languageTag": "it-IT",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "qwerty"
@@ -334,6 +360,7 @@
{
"id": 402,
"languageTag": "it-CH",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "swiss_italian"
@@ -342,6 +369,7 @@
{
"id": 501,
"languageTag": "es-ES",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "spanish"
@@ -350,6 +378,7 @@
{
"id": 502,
"languageTag": "es-US",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "spanish"
@@ -358,6 +387,7 @@
{
"id": 503,
"languageTag": "es-419",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "spanish"
@@ -366,6 +396,7 @@
{
"id": 601,
"languageTag": "pt-PT",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "qwerty"
@@ -374,6 +405,7 @@
{
"id": 602,
"languageTag": "pt-BR",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "qwerty"
@@ -382,6 +414,7 @@
{
"id": 701,
"languageTag": "nb-NO",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "norwegian"
@@ -390,6 +423,7 @@
{
"id": 702,
"languageTag": "nn-NO",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "norwegian"
@@ -398,6 +432,7 @@
{
"id": 711,
"languageTag": "sv-SE",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "swedish_finnish"
@@ -406,6 +441,7 @@
{
"id": 721,
"languageTag": "fi-FI",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "swedish_finnish"
@@ -414,6 +450,7 @@
{
"id": 731,
"languageTag": "da-DK",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "danish"
@@ -422,6 +459,7 @@
{
"id": 741,
"languageTag": "is-IS",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "icelandic"
@@ -430,6 +468,7 @@
{
"id": 751,
"languageTag": "fo",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "faroese"
@@ -438,6 +477,7 @@
{
"id": 801,
"languageTag": "fa-FA",
"composer": "appender",
"currencySet": "iranian_rial",
"preferred": {
"characters": "persian",
@@ -449,6 +489,7 @@
{
"id": 901,
"languageTag": "ar",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "arabic",
@@ -460,6 +501,7 @@
{
"id": 1001,
"languageTag": "hu",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "hungarian"
@@ -468,6 +510,7 @@
{
"id": 1101,
"languageTag": "eo",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "esperanto"
@@ -476,6 +519,7 @@
{
"id": 1201,
"languageTag": "hr",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "qwertz"
@@ -484,6 +528,7 @@
{
"id": 1301,
"languageTag": "ru",
"composer": "appender",
"currencySet": "russian_ruble",
"preferred": {
"characters": "jcuken_russian"
@@ -492,6 +537,7 @@
{
"id": 1351,
"languageTag": "uk",
"composer": "appender",
"currencySet": "ukrainian_hryvnia",
"preferred": {
"characters": "jcuken_ukrainian"
@@ -500,6 +546,7 @@
{
"id": 1401,
"languageTag": "el",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "greek"
@@ -508,6 +555,7 @@
{
"id": 1501,
"languageTag": "ro",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "qwerty"
@@ -516,6 +564,7 @@
{
"id": 1601,
"languageTag": "pl",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "qwerty"
@@ -524,6 +573,7 @@
{
"id": 1701,
"languageTag": "bg-bg",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "bulgarian_phonetic"
@@ -532,6 +582,7 @@
{
"id": 1801,
"languageTag": "tr",
"composer": "appender",
"currencySet": "turkish_lira",
"preferred": {
"characters": "qwerty"
@@ -540,6 +591,7 @@
{
"id": 1901,
"languageTag": "iw-IL",
"composer": "appender",
"currencySet": "israeli_new_shekel",
"preferred": {
"characters": "hebrew"
@@ -548,6 +600,7 @@
{
"id": 2001,
"languageTag": "ckb",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "kurdish",
@@ -559,6 +612,7 @@
{
"id": 2101,
"languageTag": "sr-RS",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "serbian_cyrillic"
@@ -567,6 +621,7 @@
{
"id": 2201,
"languageTag": "lv-LV",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "qwerty"
@@ -575,6 +630,7 @@
{
"id": 2301,
"languageTag": "ku",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "kurdish_kurmanci"
@@ -583,6 +639,7 @@
{
"id": 2501,
"languageTag": "ca",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "catalan"
@@ -591,6 +648,7 @@
{
"id": 2601,
"languageTag": "IPA-IPA",
"composer": "appender",
"currencySet": "dollar",
"preferred": {
"characters": "ipa",
@@ -601,6 +659,7 @@
{
"id": 2701,
"languageTag": "sk",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "qwertz"
@@ -609,10 +668,20 @@
{
"id": 2801,
"languageTag": "cs",
"composer": "appender",
"currencySet": "euro",
"preferred": {
"characters": "qwertz"
}
},
{
"id": 2900,
"languageTag": "ko",
"composer": "hangul-unicode",
"currencySet": "south_korean_won",
"preferred": {
"characters": "korean"
}
}
]
}

View File

@@ -0,0 +1,61 @@
{
"type": "characters",
"name": "bone",
"label": "Bone",
"authors": [ "ostrya" ],
"direction": "ltr",
"modifier": "neo2",
"arrangement": [
[
{ "$": "auto_text_key", "code": 106, "label": "j" },
{ "$": "auto_text_key", "code": 100, "label": "d" },
{ "$": "auto_text_key", "code": 117, "label": "u" },
{ "$": "auto_text_key", "code": 97, "label": "a" },
{ "$": "auto_text_key", "code": 120, "label": "x" },
{ "$": "auto_text_key", "code": 112, "label": "p" },
{ "$": "auto_text_key", "code": 104, "label": "h" },
{ "$": "auto_text_key", "code": 108, "label": "l" },
{ "$": "auto_text_key", "code": 109, "label": "m" },
{ "$": "auto_text_key", "code": 119, "label": "w" },
{ "$": "case_selector",
"lower": {
"code": 223, "label": "ß", "popup": {
"relevant": [
{ "code": 180, "label": "´" }
]
}
},
"upper": {
"code": 7838, "label": "ẞ", "popup": {
"relevant": [
{ "code": 180, "label": "´" }
]
}
}
}
],
[
{ "$": "auto_text_key", "code": 99, "label": "c" },
{ "$": "auto_text_key", "code": 116, "label": "t" },
{ "$": "auto_text_key", "code": 105, "label": "i" },
{ "$": "auto_text_key", "code": 101, "label": "e" },
{ "$": "auto_text_key", "code": 111, "label": "o" },
{ "$": "auto_text_key", "code": 98, "label": "b" },
{ "$": "auto_text_key", "code": 110, "label": "n" },
{ "$": "auto_text_key", "code": 114, "label": "r" },
{ "$": "auto_text_key", "code": 115, "label": "s" },
{ "$": "auto_text_key", "code": 103, "label": "g" },
{ "$": "auto_text_key", "code": 113, "label": "q" }
],
[
{ "$": "auto_text_key", "code": 102, "label": "f" },
{ "$": "auto_text_key", "code": 118, "label": "v" },
{ "$": "auto_text_key", "code": 252, "label": "ü" },
{ "$": "auto_text_key", "code": 228, "label": "ä" },
{ "$": "auto_text_key", "code": 246, "label": "ö" },
{ "$": "auto_text_key", "code": 121, "label": "y" },
{ "$": "auto_text_key", "code": 122, "label": "z" },
{ "$": "auto_text_key", "code": 107, "label": "k" }
]
]
}

View File

@@ -0,0 +1,19 @@
{
"type": "characters/extended_popups",
"name": "de-DE-neobone",
"authors": [ "ostrya" ],
"mapping": {
"uri": {
"~right": {
"main": { "code": -255, "label": ".com" },
"relevant": [
{ "code": -255, "label": ".ch" },
{ "code": -255, "label": ".de" },
{ "code": -255, "label": ".at" },
{ "code": -255, "label": ".net" }
]
}
}
}
}

View File

@@ -20,10 +20,10 @@
]
},
"ι": {
"main": { "$": "auto_text_key", "code": 943, "label": "ί" },
"relevant": [
{ "$": "auto_text_key", "code": 912, "label": "ΐ" },
{ "$": "auto_text_key", "code": 970, "label": "ϊ" },
{ "$": "auto_text_key", "code": 943, "label": "ί" }
{ "$": "auto_text_key", "code": 970, "label": "ϊ" }
]
},
"ο": {
@@ -32,10 +32,10 @@
]
},
"υ": {
"main": { "$": "auto_text_key", "code": 973, "label": "ύ" },
"relevant": [
{ "$": "auto_text_key", "code": 944, "label": "ΰ" },
{ "$": "auto_text_key", "code": 971, "label": "ϋ" },
{ "$": "auto_text_key", "code": 973, "label": "ύ" }
{ "$": "auto_text_key", "code": 971, "label": "ϋ" }
]
},
"ω": {

View File

@@ -0,0 +1,75 @@
{
"type": "characters/extended_popups",
"name": "ko",
"authors": [ "patrickgold", "Hayleia" ],
"mapping": {
"all": {
"ㅂ": {
"relevant": [
{ "$": "auto_text_key", "code": 12611, "label": "ㅃ" }
]
},
"ㅈ": {
"relevant": [
{ "$": "auto_text_key", "code": 12617, "label": "ㅉ" }
]
},
"ㄷ": {
"relevant": [
{ "$": "auto_text_key", "code": 12600, "label": "ㄸ" }
]
},
"ㄱ": {
"relevant": [
{ "$": "auto_text_key", "code": 12594, "label": "ㄲ" }
]
},
"ㅅ": {
"relevant": [
{ "$": "auto_text_key", "code": 12614, "label": "ㅆ" }
]
},
"ㅐ": {
"relevant": [
{ "$": "auto_text_key", "code": 12626, "label": "ㅒ" }
]
},
"ㅔ": {
"relevant": [
{ "$": "auto_text_key", "code": 12630, "label": "ㅖ" }
]
},
"~right": {
"main": { "code": 44, "label": "," },
"relevant": [
{ "code": 38, "label": "&" },
{ "code": 37, "label": "%" },
{ "code": 43, "label": "+" },
{ "code": 34, "label": "\"" },
{ "code": 45, "label": "-" },
{ "code": 58, "label": ":" },
{ "code": 39, "label": "'" },
{ "code": 64, "label": "@" },
{ "code": 59, "label": ";" },
{ "code": 47, "label": "/" },
{ "code": 40, "label": "(" },
{ "code": 41, "label": ")" },
{ "code": 35, "label": "#" },
{ "code": 33, "label": "!" },
{ "code": 63, "label": "?" }
]
}
},
"uri": {
"~right": {
"main": { "code": -255, "label": ".com" },
"relevant": [
{ "code": -255, "label": ".gov" },
{ "code": -255, "label": ".edu" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".net" }
]
}
}
}
}

View File

@@ -28,27 +28,27 @@
{ "code": 1508, "label": "פ" }
],
[
{ "code": 1513, "label": "ף" },
{ "code": 1491, "label": "ך" },
{ "code": 1490, "label": "ל" },
{ "code": 1499, "label": "ח" },
{ "code": 1506, "label": "י" },
{ "code": 1497, "label": "ע" },
{ "code": 1495, "label": "כ" },
{ "code": 1500, "label": "ג" },
{ "code": 1498, "label": "ד" },
{ "code": 1507, "label": "ש" }
{ "code": 1513, "label": "ש" },
{ "code": 1491, "label": "ד" },
{ "code": 1490, "label": "ג" },
{ "code": 1499, "label": "כ" },
{ "code": 1506, "label": "ע" },
{ "code": 1497, "label": "י" },
{ "code": 1495, "label": "ח" },
{ "code": 1500, "label": "ל" },
{ "code": 1498, "label": "ך" },
{ "code": 1507, "label": "ף" }
],
[
{ "code": 1494, "label": "ץ" },
{ "code": 1505, "label": "ת" },
{ "code": 1489, "label": "צ" },
{ "code": 1492, "label": "מ" },
{ "code": 1494, "label": "ז" },
{ "code": 1505, "label": "ס" },
{ "code": 1489, "label": "ב" },
{ "code": 1492, "label": "ה" },
{ "code": 1504, "label": "נ" },
{ "code": 1502, "label": "ה" },
{ "code": 1510, "label": "ב" },
{ "code": 1514, "label": "ס" },
{ "code": 1509, "label": "ז" }
{ "code": 1502, "label": "מ" },
{ "code": 1510, "label": "צ" },
{ "code": 1514, "label": "ת" },
{ "code": 1509, "label": "ץ" }
]
]
}

View File

@@ -0,0 +1,62 @@
{
"type": "characters",
"name": "korean",
"label": "South Korean standard",
"authors": [ "patrickgold", "Hayleia" ],
"direction": "ltr",
"arrangement": [
[
{ "$": "case_selector",
"lower": { "code": 12610, "label": "ㅂ" },
"upper": { "code": 12611, "label": "ㅃ" }
},
{ "$": "case_selector",
"lower": { "code": 12616, "label": "ㅈ" },
"upper": { "code": 12617, "label": "ㅉ" }
},
{ "$": "case_selector",
"lower": { "code": 12599, "label": "ㄷ" },
"upper": { "code": 12600, "label": "ㄸ" }
},
{ "$": "case_selector",
"lower": { "code": 12593, "label": "ㄱ" },
"upper": { "code": 12594, "label": "ㄲ" }
},
{ "$": "case_selector",
"lower": { "code": 12613, "label": "ㅅ" },
"upper": { "code": 12614, "label": "ㅆ" }
},
{ "$": "auto_text_key", "code": 12635, "label": "ㅛ"},
{ "$": "auto_text_key", "code": 12629, "label": "ㅕ"},
{ "$": "auto_text_key", "code": 12625, "label": "ㅑ"},
{ "$": "case_selector",
"lower": { "code": 12624, "label": "ㅐ" },
"upper": { "code": 12626, "label": "ㅒ" }
},
{ "$": "case_selector",
"lower": { "code": 12628, "label": "ㅔ" },
"upper": { "code": 12630, "label": "ㅖ" }
}
],
[
{ "$": "auto_text_key", "code": 12609, "label": "ㅁ"},
{ "$": "auto_text_key", "code": 12596, "label": "ㄴ"},
{ "$": "auto_text_key", "code": 12615, "label": "ㅇ"},
{ "$": "auto_text_key", "code": 12601, "label": "ㄹ"},
{ "$": "auto_text_key", "code": 12622, "label": "ㅎ"},
{ "$": "auto_text_key", "code": 12631, "label": "ㅗ"},
{ "$": "auto_text_key", "code": 12627, "label": "ㅓ"},
{ "$": "auto_text_key", "code": 12623, "label": "ㅏ"},
{ "$": "auto_text_key", "code": 12643, "label": "ㅣ"}
],
[
{ "$": "auto_text_key", "code": 12619, "label": "ㅋ"},
{ "$": "auto_text_key", "code": 12620, "label": "ㅌ"},
{ "$": "auto_text_key", "code": 12618, "label": "ㅊ"},
{ "$": "auto_text_key", "code": 12621, "label": "ㅍ"},
{ "$": "auto_text_key", "code": 12640, "label": "ㅠ"},
{ "$": "auto_text_key", "code": 12636, "label": "ㅜ"},
{ "$": "auto_text_key", "code": 12641, "label": "ㅡ"}
]
]
}

View File

@@ -0,0 +1,53 @@
{
"type": "characters/mod",
"name": "neo2",
"label": "Neo2",
"authors": [ "ostrya" ],
"direction": "ltr",
"arrangement": [
[
{ "code": -1, "label": "shift", "type": "modifier" },
{ "code": 0, "type": "placeholder" },
{ "code": -5, "label": "delete", "type": "enter_editing" }
],
[
{ "code": -202, "label": "view_symbols", "type": "system_gui" },
{ "code": -210, "label": "language_switch", "type": "system_gui" },
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui" },
{ "code": 32, "label": "space" },
{ "$": "variation_selector",
"default": { "code": 44, "label": ",", "groupId": 1,
"popup": {
"main": { "code": 34, "label": "\"" },
"relevant": [
{ "code": 8211, "label": "" }
]
} },
"email": { "code": 64, "label": "@", "groupId": 1,
"popup": {
"relevant": [
{ "code": 44, "label": "," }
]
} },
"uri": { "code": 47, "label": "/", "groupId": 1,
"popup": {
"relevant": [
{ "code": 44, "label": "," }
]
} }
},
{ "$": "variation_selector",
"default": { "code": 46, "label": ".", "groupId": 2,
"popup": {
"relevant": [
{ "code": 183, "label": "·" },
{ "code": 39, "label": "'" }
]
} },
"email": { "code": 46, "label": ".", "groupId": 2 },
"uri": { "code": 46, "label": ".", "groupId": 2 }
},
{ "code": 10, "label": "enter", "groupId": 3, "type": "enter_editing" }
]
]
}

View File

@@ -0,0 +1,61 @@
{
"type": "characters",
"name": "neo2",
"label": "Neo2",
"authors": [ "ostrya" ],
"direction": "ltr",
"modifier": "neo2",
"arrangement": [
[
{ "$": "auto_text_key", "code": 120, "label": "x" },
{ "$": "auto_text_key", "code": 118, "label": "v" },
{ "$": "auto_text_key", "code": 108, "label": "l" },
{ "$": "auto_text_key", "code": 99, "label": "c" },
{ "$": "auto_text_key", "code": 119, "label": "w" },
{ "$": "auto_text_key", "code": 107, "label": "k" },
{ "$": "auto_text_key", "code": 104, "label": "h" },
{ "$": "auto_text_key", "code": 103, "label": "g" },
{ "$": "auto_text_key", "code": 102, "label": "f" },
{ "$": "auto_text_key", "code": 113, "label": "q" },
{ "$": "case_selector",
"lower": {
"code": 223, "label": "ß", "popup": {
"relevant": [
{ "code": 180, "label": "´" }
]
}
},
"upper": {
"code": 7838, "label": "ẞ", "popup": {
"relevant": [
{ "code": 180, "label": "´" }
]
}
}
}
],
[
{ "$": "auto_text_key", "code": 117, "label": "u" },
{ "$": "auto_text_key", "code": 105, "label": "i" },
{ "$": "auto_text_key", "code": 97, "label": "a" },
{ "$": "auto_text_key", "code": 101, "label": "e" },
{ "$": "auto_text_key", "code": 111, "label": "o" },
{ "$": "auto_text_key", "code": 115, "label": "s" },
{ "$": "auto_text_key", "code": 110, "label": "n" },
{ "$": "auto_text_key", "code": 114, "label": "r" },
{ "$": "auto_text_key", "code": 116, "label": "t" },
{ "$": "auto_text_key", "code": 100, "label": "d" },
{ "$": "auto_text_key", "code": 121, "label": "y" }
],
[
{ "$": "auto_text_key", "code": 252, "label": "ü" },
{ "$": "auto_text_key", "code": 246, "label": "ö" },
{ "$": "auto_text_key", "code": 228, "label": "ä" },
{ "$": "auto_text_key", "code": 112, "label": "p" },
{ "$": "auto_text_key", "code": 122, "label": "z" },
{ "$": "auto_text_key", "code": 98, "label": "b" },
{ "$": "auto_text_key", "code": 109, "label": "m" },
{ "$": "auto_text_key", "code": 106, "label": "j" }
]
]
}

View File

@@ -0,0 +1,91 @@
{
"type": "numeric_row",
"name": "bengali",
"label": "Bengali",
"authors": [ "yashpalgoyal1304" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 2535, "label": "১", "type": "numeric", "popup": {
"main": { "code": 49, "label": "1" },
"relevant": [
{ "code": 8537, "label": "⅙" },
{ "code": 8528, "label": "⅐" },
{ "code": 8539, "label": "⅛" },
{ "code": 8529, "label": "⅑" },
{ "code": 8530, "label": "⅒" },
{ "code": 185, "label": "¹" },
{ "code": 189, "label": "½" },
{ "code": 8531, "label": "⅓" },
{ "code": 188, "label": "¼" },
{ "code": 8533, "label": "⅕" }
]
} },
{ "code": 2536, "label": "২", "type": "numeric", "popup": {
"main": { "code": 50, "label": "2" },
"relevant": [
{ "code": 8532, "label": "⅔" },
{ "code": 178, "label": "²" },
{ "code": 8534, "label": "⅖" }
]
} },
{ "code": 2537, "label": "৩", "type": "numeric", "popup": {
"main": { "code": 51, "label": "3" },
"relevant": [
{ "code": 8535, "label": "⅗" },
{ "code": 190, "label": "¾" },
{ "code": 179, "label": "³" },
{ "code": 8540, "label": "⅜" }
]
} },
{ "code": 2538, "label": "", "type": "numeric", "popup": {
"main": { "code": 52, "label": "4" },
"relevant": [
{ "code": 8536, "label": "⅘" },
{ "code": 8308, "label": "⁴" }
]
} },
{ "code": 2539, "label": "৫", "type": "numeric", "popup": {
"main": { "code": 53, "label": "5" },
"relevant": [
{ "code": 8538, "label": "⅚" },
{ "code": 8309, "label": "⁵" },
{ "code": 8541, "label": "⅝" }
]
} },
{ "code": 2540, "label": "৬", "type": "numeric", "popup": {
"main": { "code": 54, "label": "6" },
"relevant": [
{ "code": 8310, "label": "⁶" }
]
} },
{ "code": 2541, "label": "", "type": "numeric", "popup": {
"main": { "code": 55, "label": "7" },
"relevant": [
{ "code": 8542, "label": "⅞" },
{ "code": 8311, "label": "⁷" }
]
} },
{ "code": 2542, "label": "৮", "type": "numeric", "popup": {
"main": { "code": 56, "label": "8" },
"relevant": [
{ "code": 8312, "label": "⁸" }
]
} },
{ "code": 2543, "label": "৯", "type": "numeric", "popup": {
"main": { "code": 57, "label": "9" },
"relevant": [
{ "code": 8313, "label": "⁹" }
]
} },
{ "code": 2534, "label": "", "type": "numeric", "popup": {
"main": { "code": 48, "label": "0" },
"relevant": [
{ "code": 8319, "label": "ⁿ" },
{ "code": 8709, "label": "∅" },
{ "code": 8304, "label": "⁰" }
]
} }
]
]
}

View File

@@ -0,0 +1,91 @@
{
"type": "numeric_row",
"name": "devanagari",
"label": "Devanagari",
"authors": [ "yashpalgoyal1304" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 2047, "label": "१", "type": "numeric", "popup": {
"main": { "code": 49, "label": "1" },
"relevant": [
{ "code": 8537, "label": "⅙" },
{ "code": 8528, "label": "⅐" },
{ "code": 8539, "label": "⅛" },
{ "code": 8529, "label": "⅑" },
{ "code": 8530, "label": "⅒" },
{ "code": 185, "label": "¹" },
{ "code": 189, "label": "½" },
{ "code": 8531, "label": "⅓" },
{ "code": 188, "label": "¼" },
{ "code": 8533, "label": "⅕" }
]
} },
{ "code": 2048, "label": "२", "type": "numeric", "popup": {
"main": { "code": 50, "label": "2" },
"relevant": [
{ "code": 8532, "label": "⅔" },
{ "code": 178, "label": "²" },
{ "code": 8534, "label": "⅖" }
]
} },
{ "code": 2049, "label": "३", "type": "numeric", "popup": {
"main": { "code": 51, "label": "3" },
"relevant": [
{ "code": 8535, "label": "⅗" },
{ "code": 190, "label": "¾" },
{ "code": 179, "label": "³" },
{ "code": 8540, "label": "⅜" }
]
} },
{ "code": 2050, "label": "४", "type": "numeric", "popup": {
"main": { "code": 52, "label": "4" },
"relevant": [
{ "code": 8536, "label": "⅘" },
{ "code": 8308, "label": "⁴" }
]
} },
{ "code": 2051, "label": "५", "type": "numeric", "popup": {
"main": { "code": 53, "label": "5" },
"relevant": [
{ "code": 8538, "label": "⅚" },
{ "code": 8309, "label": "⁵" },
{ "code": 8541, "label": "⅝" }
]
} },
{ "code": 2052, "label": "६", "type": "numeric", "popup": {
"main": { "code": 54, "label": "6" },
"relevant": [
{ "code": 8310, "label": "⁶" }
]
} },
{ "code": 2053, "label": "७", "type": "numeric", "popup": {
"main": { "code": 55, "label": "7" },
"relevant": [
{ "code": 8542, "label": "⅞" },
{ "code": 8311, "label": "⁷" }
]
} },
{ "code": 2054, "label": "८", "type": "numeric", "popup": {
"main": { "code": 56, "label": "8" },
"relevant": [
{ "code": 8312, "label": "⁸" }
]
} },
{ "code": 2055, "label": "९", "type": "numeric", "popup": {
"main": { "code": 57, "label": "9" },
"relevant": [
{ "code": 8313, "label": "⁹" }
]
} },
{ "code": 2046, "label": "", "type": "numeric", "popup": {
"main": { "code": 48, "label": "0" },
"relevant": [
{ "code": 8319, "label": "ⁿ" },
{ "code": 8709, "label": "∅" },
{ "code": 8304, "label": "⁰" }
]
} }
]
]
}

View File

@@ -0,0 +1,91 @@
{
"type": "numeric_row",
"name": "gujarati",
"label": "Gujarati",
"authors": [ "yashpalgoyal1304" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 2791, "label": "૧", "type": "numeric", "popup": {
"main": { "code": 49, "label": "1" },
"relevant": [
{ "code": 8537, "label": "⅙" },
{ "code": 8528, "label": "⅐" },
{ "code": 8539, "label": "⅛" },
{ "code": 8529, "label": "⅑" },
{ "code": 8530, "label": "⅒" },
{ "code": 185, "label": "¹" },
{ "code": 189, "label": "½" },
{ "code": 8531, "label": "⅓" },
{ "code": 188, "label": "¼" },
{ "code": 8533, "label": "⅕" }
]
} },
{ "code": 2792, "label": "૨", "type": "numeric", "popup": {
"main": { "code": 50, "label": "2" },
"relevant": [
{ "code": 8532, "label": "⅔" },
{ "code": 178, "label": "²" },
{ "code": 8534, "label": "⅖" }
]
} },
{ "code": 2793, "label": "૩", "type": "numeric", "popup": {
"main": { "code": 51, "label": "3" },
"relevant": [
{ "code": 8535, "label": "⅗" },
{ "code": 190, "label": "¾" },
{ "code": 179, "label": "³" },
{ "code": 8540, "label": "⅜" }
]
} },
{ "code": 2794, "label": "૪", "type": "numeric", "popup": {
"main": { "code": 52, "label": "4" },
"relevant": [
{ "code": 8536, "label": "⅘" },
{ "code": 8308, "label": "⁴" }
]
} },
{ "code": 2795, "label": "૫", "type": "numeric", "popup": {
"main": { "code": 53, "label": "5" },
"relevant": [
{ "code": 8538, "label": "⅚" },
{ "code": 8309, "label": "⁵" },
{ "code": 8541, "label": "⅝" }
]
} },
{ "code": 2796, "label": "૬", "type": "numeric", "popup": {
"main": { "code": 54, "label": "6" },
"relevant": [
{ "code": 8310, "label": "⁶" }
]
} },
{ "code": 2797, "label": "૭", "type": "numeric", "popup": {
"main": { "code": 55, "label": "7" },
"relevant": [
{ "code": 8542, "label": "⅞" },
{ "code": 8311, "label": "⁷" }
]
} },
{ "code": 2798, "label": "૮", "type": "numeric", "popup": {
"main": { "code": 56, "label": "8" },
"relevant": [
{ "code": 8312, "label": "⁸" }
]
} },
{ "code": 2799, "label": "૯", "type": "numeric", "popup": {
"main": { "code": 57, "label": "9" },
"relevant": [
{ "code": 8313, "label": "⁹" }
]
} },
{ "code": 2790, "label": "", "type": "numeric", "popup": {
"main": { "code": 48, "label": "0" },
"relevant": [
{ "code": 8319, "label": "ⁿ" },
{ "code": 8709, "label": "∅" },
{ "code": 8304, "label": "⁰" }
]
} }
]
]
}

View File

@@ -0,0 +1,91 @@
{
"type": "numeric_row",
"name": "gurmukhi",
"label": "Gurmukhi",
"authors": [ "yashpalgoyal1304" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 2663, "label": "", "type": "numeric", "popup": {
"main": { "code": 49, "label": "1" },
"relevant": [
{ "code": 8537, "label": "⅙" },
{ "code": 8528, "label": "⅐" },
{ "code": 8539, "label": "⅛" },
{ "code": 8529, "label": "⅑" },
{ "code": 8530, "label": "⅒" },
{ "code": 185, "label": "¹" },
{ "code": 189, "label": "½" },
{ "code": 8531, "label": "⅓" },
{ "code": 188, "label": "¼" },
{ "code": 8533, "label": "⅕" }
]
} },
{ "code": 2664, "label": "੨", "type": "numeric", "popup": {
"main": { "code": 50, "label": "2" },
"relevant": [
{ "code": 8532, "label": "⅔" },
{ "code": 178, "label": "²" },
{ "code": 8534, "label": "⅖" }
]
} },
{ "code": 2665, "label": "੩", "type": "numeric", "popup": {
"main": { "code": 51, "label": "3" },
"relevant": [
{ "code": 8535, "label": "⅗" },
{ "code": 190, "label": "¾" },
{ "code": 179, "label": "³" },
{ "code": 8540, "label": "⅜" }
]
} },
{ "code": 2666, "label": "", "type": "numeric", "popup": {
"main": { "code": 52, "label": "4" },
"relevant": [
{ "code": 8536, "label": "⅘" },
{ "code": 8308, "label": "⁴" }
]
} },
{ "code": 2667, "label": "੫", "type": "numeric", "popup": {
"main": { "code": 53, "label": "5" },
"relevant": [
{ "code": 8538, "label": "⅚" },
{ "code": 8309, "label": "⁵" },
{ "code": 8541, "label": "⅝" }
]
} },
{ "code": 2668, "label": "੬", "type": "numeric", "popup": {
"main": { "code": 54, "label": "6" },
"relevant": [
{ "code": 8310, "label": "⁶" }
]
} },
{ "code": 2669, "label": "੭", "type": "numeric", "popup": {
"main": { "code": 55, "label": "7" },
"relevant": [
{ "code": 8542, "label": "⅞" },
{ "code": 8311, "label": "⁷" }
]
} },
{ "code": 2670, "label": "੮", "type": "numeric", "popup": {
"main": { "code": 56, "label": "8" },
"relevant": [
{ "code": 8312, "label": "⁸" }
]
} },
{ "code": 2671, "label": "੯", "type": "numeric", "popup": {
"main": { "code": 57, "label": "9" },
"relevant": [
{ "code": 8313, "label": "⁹" }
]
} },
{ "code": 2662, "label": "", "type": "numeric", "popup": {
"main": { "code": 48, "label": "0" },
"relevant": [
{ "code": 8319, "label": "ⁿ" },
{ "code": 8709, "label": "∅" },
{ "code": 8304, "label": "⁰" }
]
} }
]
]
}

View File

@@ -0,0 +1,91 @@
{
"type": "numeric_row",
"name": "kannada",
"label": "Kannada",
"authors": [ "yashpalgoyal1304" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 3303, "label": "೧", "type": "numeric", "popup": {
"main": { "code": 49, "label": "1" },
"relevant": [
{ "code": 8537, "label": "⅙" },
{ "code": 8528, "label": "⅐" },
{ "code": 8539, "label": "⅛" },
{ "code": 8529, "label": "⅑" },
{ "code": 8530, "label": "⅒" },
{ "code": 185, "label": "¹" },
{ "code": 189, "label": "½" },
{ "code": 8531, "label": "⅓" },
{ "code": 188, "label": "¼" },
{ "code": 8533, "label": "⅕" }
]
} },
{ "code": 3304, "label": "೨", "type": "numeric", "popup": {
"main": { "code": 50, "label": "2" },
"relevant": [
{ "code": 8532, "label": "⅔" },
{ "code": 178, "label": "²" },
{ "code": 8534, "label": "⅖" }
]
} },
{ "code": 3305, "label": "೩", "type": "numeric", "popup": {
"main": { "code": 51, "label": "3" },
"relevant": [
{ "code": 8535, "label": "⅗" },
{ "code": 190, "label": "¾" },
{ "code": 179, "label": "³" },
{ "code": 8540, "label": "⅜" }
]
} },
{ "code": 3306, "label": "೪", "type": "numeric", "popup": {
"main": { "code": 52, "label": "4" },
"relevant": [
{ "code": 8536, "label": "⅘" },
{ "code": 8308, "label": "⁴" }
]
} },
{ "code": 3307, "label": "೫", "type": "numeric", "popup": {
"main": { "code": 53, "label": "5" },
"relevant": [
{ "code": 8538, "label": "⅚" },
{ "code": 8309, "label": "⁵" },
{ "code": 8541, "label": "⅝" }
]
} },
{ "code": 3308, "label": "೬", "type": "numeric", "popup": {
"main": { "code": 54, "label": "6" },
"relevant": [
{ "code": 8310, "label": "⁶" }
]
} },
{ "code": 3309, "label": "೭", "type": "numeric", "popup": {
"main": { "code": 55, "label": "7" },
"relevant": [
{ "code": 8542, "label": "⅞" },
{ "code": 8311, "label": "⁷" }
]
} },
{ "code": 3310, "label": "೮", "type": "numeric", "popup": {
"main": { "code": 56, "label": "8" },
"relevant": [
{ "code": 8312, "label": "⁸" }
]
} },
{ "code": 3311, "label": "೯", "type": "numeric", "popup": {
"main": { "code": 57, "label": "9" },
"relevant": [
{ "code": 8313, "label": "⁹" }
]
} },
{ "code": 3302, "label": "", "type": "numeric", "popup": {
"main": { "code": 48, "label": "0" },
"relevant": [
{ "code": 8319, "label": "ⁿ" },
{ "code": 8709, "label": "∅" },
{ "code": 8304, "label": "⁰" }
]
} }
]
]
}

View File

@@ -0,0 +1,91 @@
{
"type": "numeric_row",
"name": "malayalam",
"label": "Malayalam",
"authors": [ "yashpalgoyal1304" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 3431, "label": "൧", "type": "numeric", "popup": {
"main": { "code": 49, "label": "1" },
"relevant": [
{ "code": 8537, "label": "⅙" },
{ "code": 8528, "label": "⅐" },
{ "code": 8539, "label": "⅛" },
{ "code": 8529, "label": "⅑" },
{ "code": 8530, "label": "⅒" },
{ "code": 185, "label": "¹" },
{ "code": 189, "label": "½" },
{ "code": 8531, "label": "⅓" },
{ "code": 188, "label": "¼" },
{ "code": 8533, "label": "⅕" }
]
} },
{ "code": 3432, "label": "൨", "type": "numeric", "popup": {
"main": { "code": 50, "label": "2" },
"relevant": [
{ "code": 8532, "label": "⅔" },
{ "code": 178, "label": "²" },
{ "code": 8534, "label": "⅖" }
]
} },
{ "code": 3433, "label": "൩", "type": "numeric", "popup": {
"main": { "code": 51, "label": "3" },
"relevant": [
{ "code": 8535, "label": "⅗" },
{ "code": 190, "label": "¾" },
{ "code": 179, "label": "³" },
{ "code": 8540, "label": "⅜" }
]
} },
{ "code": 3434, "label": "൪", "type": "numeric", "popup": {
"main": { "code": 52, "label": "4" },
"relevant": [
{ "code": 8536, "label": "⅘" },
{ "code": 8308, "label": "⁴" }
]
} },
{ "code": 3435, "label": "൫", "type": "numeric", "popup": {
"main": { "code": 53, "label": "5" },
"relevant": [
{ "code": 8538, "label": "⅚" },
{ "code": 8309, "label": "⁵" },
{ "code": 8541, "label": "⅝" }
]
} },
{ "code": 3436, "label": "൬", "type": "numeric", "popup": {
"main": { "code": 54, "label": "6" },
"relevant": [
{ "code": 8310, "label": "⁶" }
]
} },
{ "code": 3437, "label": "", "type": "numeric", "popup": {
"main": { "code": 55, "label": "7" },
"relevant": [
{ "code": 8542, "label": "⅞" },
{ "code": 8311, "label": "⁷" }
]
} },
{ "code": 3438, "label": "൮", "type": "numeric", "popup": {
"main": { "code": 56, "label": "8" },
"relevant": [
{ "code": 8312, "label": "⁸" }
]
} },
{ "code": 3439, "label": "൯", "type": "numeric", "popup": {
"main": { "code": 57, "label": "9" },
"relevant": [
{ "code": 8313, "label": "⁹" }
]
} },
{ "code": 3430, "label": "", "type": "numeric", "popup": {
"main": { "code": 48, "label": "0" },
"relevant": [
{ "code": 8319, "label": "ⁿ" },
{ "code": 8709, "label": "∅" },
{ "code": 8304, "label": "⁰" }
]
} }
]
]
}

View File

@@ -0,0 +1,80 @@
{
"type": "numeric_row",
"name": "neo2",
"label": "Neo2",
"authors": [ "ostrya" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 49, "label": "1", "type": "numeric", "popup": {
"relevant": [
{ "code": 176, "label": "°" },
{ "code": 185, "label": "¹" }
]
} },
{ "code": 50, "label": "2", "type": "numeric", "popup": {
"relevant": [
{ "code": 167, "label": "§" },
{ "code": 178, "label": "²" }
]
} },
{ "code": 51, "label": "3", "type": "numeric", "popup": {
"relevant": [
{ "code": 8467, "label": "" },
{ "code": 179, "label": "³" }
]
} },
{ "code": 52, "label": "4", "type": "numeric", "popup": {
"relevant": [
{ "code": 187, "label": "»" },
{ "code": 8250, "label": "" }
]
} },
{ "code": 53, "label": "5", "type": "numeric", "popup": {
"relevant": [
{ "code": 171, "label": "«" },
{ "code": 8249, "label": "" }
]
} },
{ "code": 54, "label": "6", "type": "numeric", "popup": {
"relevant": [
{ "code": 36, "label": "$" },
{ "code": 162, "label": "¢" }
]
} },
{ "code": 55, "label": "7", "type": "numeric", "popup": {
"main": { "code": -801, "label": "currency_slot_1" },
"relevant": [
{ "code": -802, "label": "currency_slot_2" },
{ "code": -803, "label": "currency_slot_3" },
{ "code": -804, "label": "currency_slot_4" },
{ "code": -805, "label": "currency_slot_5" },
{ "code": -806, "label": "currency_slot_6" }
]
} },
{ "code": 56, "label": "8", "type": "numeric", "popup": {
"relevant": [
{ "code": 8222, "label": "„" },
{ "code": 8218, "label": "" }
]
} },
{ "code": 57, "label": "9", "type": "numeric", "popup": {
"relevant": [
{ "code": 8220, "label": "“" },
{ "code": 8216, "label": "" }
]
} },
{ "code": 48, "label": "0", "type": "numeric", "popup": {
"relevant": [
{ "code": 8221, "label": "”" },
{ "code": 8217, "label": "" }
]
} },
{ "code": 45, "label": "-", "type": "numeric", "popup": {
"relevant": [
{ "code": 8212, "label": "—" }
]
} }
]
]
}

View File

@@ -0,0 +1,91 @@
{
"type": "numeric_row",
"name": "oriya",
"label": "Odia",
"authors": [ "yashpalgoyal1304" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 2919, "label": "୧", "type": "numeric", "popup": {
"main": { "code": 49, "label": "1" },
"relevant": [
{ "code": 8537, "label": "⅙" },
{ "code": 8528, "label": "⅐" },
{ "code": 8539, "label": "⅛" },
{ "code": 8529, "label": "⅑" },
{ "code": 8530, "label": "⅒" },
{ "code": 185, "label": "¹" },
{ "code": 189, "label": "½" },
{ "code": 8531, "label": "⅓" },
{ "code": 188, "label": "¼" },
{ "code": 8533, "label": "⅕" }
]
} },
{ "code": 2920, "label": "", "type": "numeric", "popup": {
"main": { "code": 50, "label": "2" },
"relevant": [
{ "code": 8532, "label": "⅔" },
{ "code": 178, "label": "²" },
{ "code": 8534, "label": "⅖" }
]
} },
{ "code": 2921, "label": "୩", "type": "numeric", "popup": {
"main": { "code": 51, "label": "3" },
"relevant": [
{ "code": 8535, "label": "⅗" },
{ "code": 190, "label": "¾" },
{ "code": 179, "label": "³" },
{ "code": 8540, "label": "⅜" }
]
} },
{ "code": 2922, "label": "୪", "type": "numeric", "popup": {
"main": { "code": 52, "label": "4" },
"relevant": [
{ "code": 8536, "label": "⅘" },
{ "code": 8308, "label": "⁴" }
]
} },
{ "code": 2923, "label": "୫", "type": "numeric", "popup": {
"main": { "code": 53, "label": "5" },
"relevant": [
{ "code": 8538, "label": "⅚" },
{ "code": 8309, "label": "⁵" },
{ "code": 8541, "label": "⅝" }
]
} },
{ "code": 2924, "label": "୬", "type": "numeric", "popup": {
"main": { "code": 54, "label": "6" },
"relevant": [
{ "code": 8310, "label": "⁶" }
]
} },
{ "code": 2925, "label": "୭", "type": "numeric", "popup": {
"main": { "code": 55, "label": "7" },
"relevant": [
{ "code": 8542, "label": "⅞" },
{ "code": 8311, "label": "⁷" }
]
} },
{ "code": 2926, "label": "୮", "type": "numeric", "popup": {
"main": { "code": 56, "label": "8" },
"relevant": [
{ "code": 8312, "label": "⁸" }
]
} },
{ "code": 2927, "label": "୯", "type": "numeric", "popup": {
"main": { "code": 57, "label": "9" },
"relevant": [
{ "code": 8313, "label": "⁹" }
]
} },
{ "code": 2918, "label": "", "type": "numeric", "popup": {
"main": { "code": 48, "label": "0" },
"relevant": [
{ "code": 8319, "label": "ⁿ" },
{ "code": 8709, "label": "∅" },
{ "code": 8304, "label": "⁰" }
]
} }
]
]
}

View File

@@ -0,0 +1,91 @@
{
"type": "numeric_row",
"name": "tamil",
"label": "Tamil",
"authors": [ "yashpalgoyal1304" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 3047, "label": "௧", "type": "numeric", "popup": {
"main": { "code": 49, "label": "1" },
"relevant": [
{ "code": 8537, "label": "⅙" },
{ "code": 8528, "label": "⅐" },
{ "code": 8539, "label": "⅛" },
{ "code": 8529, "label": "⅑" },
{ "code": 8530, "label": "⅒" },
{ "code": 185, "label": "¹" },
{ "code": 189, "label": "½" },
{ "code": 8531, "label": "⅓" },
{ "code": 188, "label": "¼" },
{ "code": 8533, "label": "⅕" }
]
} },
{ "code": 3048, "label": "௨", "type": "numeric", "popup": {
"main": { "code": 50, "label": "2" },
"relevant": [
{ "code": 8532, "label": "⅔" },
{ "code": 178, "label": "²" },
{ "code": 8534, "label": "⅖" }
]
} },
{ "code": 3049, "label": "௩", "type": "numeric", "popup": {
"main": { "code": 51, "label": "3" },
"relevant": [
{ "code": 8535, "label": "⅗" },
{ "code": 190, "label": "¾" },
{ "code": 179, "label": "³" },
{ "code": 8540, "label": "⅜" }
]
} },
{ "code": 3050, "label": "௪", "type": "numeric", "popup": {
"main": { "code": 52, "label": "4" },
"relevant": [
{ "code": 8536, "label": "⅘" },
{ "code": 8308, "label": "⁴" }
]
} },
{ "code": 3051, "label": "௫", "type": "numeric", "popup": {
"main": { "code": 53, "label": "5" },
"relevant": [
{ "code": 8538, "label": "⅚" },
{ "code": 8309, "label": "⁵" },
{ "code": 8541, "label": "⅝" }
]
} },
{ "code": 3052, "label": "௬", "type": "numeric", "popup": {
"main": { "code": 54, "label": "6" },
"relevant": [
{ "code": 8310, "label": "⁶" }
]
} },
{ "code": 3053, "label": "௭", "type": "numeric", "popup": {
"main": { "code": 55, "label": "7" },
"relevant": [
{ "code": 8542, "label": "⅞" },
{ "code": 8311, "label": "⁷" }
]
} },
{ "code": 3054, "label": "௮", "type": "numeric", "popup": {
"main": { "code": 56, "label": "8" },
"relevant": [
{ "code": 8312, "label": "⁸" }
]
} },
{ "code": 3055, "label": "௯", "type": "numeric", "popup": {
"main": { "code": 57, "label": "9" },
"relevant": [
{ "code": 8313, "label": "⁹" }
]
} },
{ "code": 3046, "label": "", "type": "numeric", "popup": {
"main": { "code": 48, "label": "0" },
"relevant": [
{ "code": 8319, "label": "ⁿ" },
{ "code": 8709, "label": "∅" },
{ "code": 8304, "label": "⁰" }
]
} }
]
]
}

View File

@@ -0,0 +1,91 @@
{
"type": "numeric_row",
"name": "telugu",
"label": "Telugu",
"authors": [ "yashpalgoyal1304" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 3175, "label": "౧", "type": "numeric", "popup": {
"main": { "code": 49, "label": "1" },
"relevant": [
{ "code": 8537, "label": "⅙" },
{ "code": 8528, "label": "⅐" },
{ "code": 8539, "label": "⅛" },
{ "code": 8529, "label": "⅑" },
{ "code": 8530, "label": "⅒" },
{ "code": 185, "label": "¹" },
{ "code": 189, "label": "½" },
{ "code": 8531, "label": "⅓" },
{ "code": 188, "label": "¼" },
{ "code": 8533, "label": "⅕" }
]
} },
{ "code": 3176, "label": "౨", "type": "numeric", "popup": {
"main": { "code": 50, "label": "2" },
"relevant": [
{ "code": 8532, "label": "⅔" },
{ "code": 178, "label": "²" },
{ "code": 8534, "label": "⅖" }
]
} },
{ "code": 3177, "label": "౩", "type": "numeric", "popup": {
"main": { "code": 51, "label": "3" },
"relevant": [
{ "code": 8535, "label": "⅗" },
{ "code": 190, "label": "¾" },
{ "code": 179, "label": "³" },
{ "code": 8540, "label": "⅜" }
]
} },
{ "code": 3178, "label": "౪", "type": "numeric", "popup": {
"main": { "code": 52, "label": "4" },
"relevant": [
{ "code": 8536, "label": "⅘" },
{ "code": 8308, "label": "⁴" }
]
} },
{ "code": 3179, "label": "౫", "type": "numeric", "popup": {
"main": { "code": 53, "label": "5" },
"relevant": [
{ "code": 8538, "label": "⅚" },
{ "code": 8309, "label": "⁵" },
{ "code": 8541, "label": "⅝" }
]
} },
{ "code": 3180, "label": "౬", "type": "numeric", "popup": {
"main": { "code": 54, "label": "6" },
"relevant": [
{ "code": 8310, "label": "⁶" }
]
} },
{ "code": 3181, "label": "౭", "type": "numeric", "popup": {
"main": { "code": 55, "label": "7" },
"relevant": [
{ "code": 8542, "label": "⅞" },
{ "code": 8311, "label": "⁷" }
]
} },
{ "code": 3182, "label": "౮", "type": "numeric", "popup": {
"main": { "code": 56, "label": "8" },
"relevant": [
{ "code": 8312, "label": "⁸" }
]
} },
{ "code": 3183, "label": "౯", "type": "numeric", "popup": {
"main": { "code": 57, "label": "9" },
"relevant": [
{ "code": 8313, "label": "⁹" }
]
} },
{ "code": 3174, "label": "", "type": "numeric", "popup": {
"main": { "code": 48, "label": "0" },
"relevant": [
{ "code": 8319, "label": "ⁿ" },
{ "code": 8709, "label": "∅" },
{ "code": 8304, "label": "⁰" }
]
} }
]
]
}

View File

@@ -0,0 +1,22 @@
{
"type": "symbols/mod",
"name": "neo2",
"label": "Neo2",
"authors": [ "ostrya" ],
"direction": "ltr",
"arrangement": [
[
{ "code": -203, "label": "view_symbols2", "type": "system_gui" },
{ "code": 0, "type": "placeholder" },
{ "code": -5, "label": "delete", "type": "enter_editing" }
],
[
{ "code": -201, "label": "view_characters", "type": "system_gui" },
{ "code": -205, "label": "view_numeric_advanced", "type": "system_gui" },
{ "code": 32, "label": "space" },
{ "code": 34, "label": "\"" },
{ "code": 39, "label": "'" },
{ "code": 10, "label": "enter", "groupId": 3, "type": "enter_editing" }
]
]
}

View File

@@ -0,0 +1,46 @@
{
"type": "symbols",
"name": "neo2",
"label": "Neo2",
"authors": [ "ostrya" ],
"direction": "ltr",
"modifier": "neo2",
"arrangement": [
[
{ "code": 8230, "label": "…" },
{ "code": 95, "label": "_" },
{ "code": 91, "label": "[" },
{ "code": 93, "label": "]" },
{ "code": 94, "label": "^" },
{ "code": 33, "label": "!" },
{ "code": 60, "label": "<" },
{ "code": 62, "label": ">" },
{ "code": 61, "label": "=" },
{ "code": 38, "label": "&" },
{ "code": 383, "label": "ſ" }
],
[
{ "code": 92, "label": "\\" },
{ "code": 47, "label": "/" },
{ "code": 123, "label": "{" },
{ "code": 125, "label": "}" },
{ "code": 42, "label": "*" },
{ "code": 63, "label": "?" },
{ "code": 40, "label": "(" },
{ "code": 41, "label": ")" },
{ "code": 45, "label": "-" },
{ "code": 58, "label": ":" },
{ "code": 64, "label": "@" }
],
[
{ "code": 35, "label": "#" },
{ "code": 36, "label": "$" },
{ "code": 124, "label": "|" },
{ "code": 126, "label": "~" },
{ "code": 96, "label": "`" },
{ "code": 43, "label": "+" },
{ "code": 37, "label": "%" },
{ "code": 59, "label": ";" }
]
]
}

View File

@@ -0,0 +1,38 @@
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
cmake_minimum_required(VERSION 3.10.2)
project("florisboard")
set(CMAKE_CXX_STANDARD 17)
add_subdirectory(ime/nlp)
add_library(
# Name
florisboard-native
# Type
SHARED
# Sources
dev_patrickgold_florisboard_ime_nlp_SuggestionList.cpp
)
find_library(
# Save to var
log-lib
# Original name
log
)
target_link_libraries(
# Destination
florisboard-native
# Sources
${log-lib}
ime-nlp
)

View File

@@ -0,0 +1,123 @@
/*
* 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.
*/
#include <jni.h>
#include "ime/nlp/suggestion_list.h"
#pragma ide diagnostic ignored "UnusedLocalVariable"
using namespace ime::nlp;
extern "C"
JNIEXPORT jlong JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeInitialize(
JNIEnv *env,
jobject thiz,
jint max_size) {
auto *suggestionList = new SuggestionList(max_size);
return reinterpret_cast<jlong>(suggestionList);
}
extern "C"
JNIEXPORT void JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeDispose(
JNIEnv *env,
jobject thiz,
jlong native_ptr) {
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
suggestionList->clear();
delete suggestionList;
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeAdd(
JNIEnv *env,
jobject thiz,
jlong native_ptr,
jstring word,
jint freq) {
const char *cWord = env->GetStringUTFChars(word, nullptr);
word_t stdWord = word_t(cWord);
env->ReleaseStringUTFChars(word, cWord);
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
return suggestionList->add(std::move(stdWord), freq);
}
extern "C"
JNIEXPORT void JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeClear(
JNIEnv *env,
jobject thiz,
jlong native_ptr) {
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
suggestionList->clear();
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeContains(
JNIEnv *env,
jobject thiz,
jlong native_ptr,
jstring element) {
const char *cWord = env->GetStringUTFChars(element, nullptr);
const word_t stdWord = word_t(cWord);
env->ReleaseStringUTFChars(element, cWord);
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
return suggestionList->containsWord(stdWord);
}
extern "C"
JNIEXPORT jstring JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeGetOrNull(
JNIEnv *env,
jobject thiz,
jlong native_ptr,
jint index) {
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
auto weightedToken = suggestionList->get(index);
if (weightedToken == nullptr) {
return nullptr;
}
return env->NewStringUTF(weightedToken->data.c_str());
}
extern "C"
JNIEXPORT jint JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeSize(
JNIEnv *env,
jobject thiz,
jlong native_ptr) {
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
return suggestionList->size();
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeGetIsPrimaryTokenAutoInsert(
JNIEnv *env, jobject thiz, jlong native_ptr) {
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
return suggestionList->isPrimaryTokenAutoInsert;
}
extern "C"
JNIEXPORT void JNICALL
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeSetIsPrimaryTokenAutoInsert(
JNIEnv *env, jobject thiz, jlong native_ptr, jboolean v) {
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
suggestionList->isPrimaryTokenAutoInsert = v;
}

View File

View File

@@ -0,0 +1,13 @@
add_library(
# Name
ime-nlp
# Headers
nlp.h
token.h
suggestion_list.h
# Sources
token.cpp
suggestion_list.cpp
)

View File

@@ -0,0 +1,32 @@
/*
* 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.
*/
#ifndef FLORISBOARD_NLP_H
#define FLORISBOARD_NLP_H
#include <string>
namespace ime::nlp {
typedef std::string word_t;
typedef uint16_t freq_t;
static const freq_t FREQ_VALUE_MASK = 0xFF;
static const freq_t FREQ_POSSIBLY_OFFENSIVE = 0x01;
} // namespace ime::nlp
#endif // FLORISBOARD_NLP_H

View File

@@ -0,0 +1,98 @@
/*
* 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.
*/
#include "suggestion_list.h"
#include <utility>
using namespace ime::nlp;
SuggestionList::SuggestionList(size_t _maxSize) :
maxSize(_maxSize), internalSize(0), tokens(_maxSize), isPrimaryTokenAutoInsert(false)
{ }
SuggestionList::~SuggestionList() = default;
bool SuggestionList::add(word_t &&word, freq_t &&freq) {
auto entryIndex = indexOfWord(word);
if (entryIndex.has_value()) {
// Word exists already
auto entry = tokens[entryIndex.value()];
if (entry.freq < freq) {
// Need to update freq
entry.freq = freq;
} else {
return false;
}
} else {
if (internalSize < maxSize) {
tokens[internalSize++] = WeightedToken(std::move(word), freq);
} else {
auto last = tokens[internalSize - 1];
if (last.freq < freq) {
tokens[internalSize - 1] = WeightedToken(std::move(word), freq);
} else {
return false;
}
}
}
std::sort(tokens.begin(), tokens.begin() + internalSize, std::greater<>());
return true;
}
void SuggestionList::clear() {
internalSize = 0;
isPrimaryTokenAutoInsert = false;
}
bool SuggestionList::contains(const WeightedToken &element) const {
return indexOf(element).has_value();
}
bool SuggestionList::containsWord(const word_t &word) const {
return indexOfWord(word).has_value();
}
const WeightedToken *SuggestionList::get(size_t index) const {
if (index < 0 || index >= internalSize) return nullptr;
return &tokens[index];
}
std::optional<size_t> SuggestionList::indexOf(const WeightedToken &element) const {
for (size_t n = 0; n < internalSize; n++) {
if (element == tokens[n]) {
return n;
}
}
return std::nullopt;
}
std::optional<size_t> SuggestionList::indexOfWord(const word_t &word) const {
for (size_t n = 0; n < internalSize; n++) {
if (word == tokens[n].data) {
return n;
}
}
return std::nullopt;
}
bool SuggestionList::isEmpty() const {
return internalSize == 0;
}
size_t SuggestionList::size() const {
return internalSize;
}

View File

@@ -0,0 +1,51 @@
/*
* 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.
*/
#ifndef FLORISBOARD_SUGGESTION_LIST_H
#define FLORISBOARD_SUGGESTION_LIST_H
#include <optional>
#include <vector>
#include "token.h"
namespace ime::nlp {
class SuggestionList {
public:
SuggestionList(size_t _maxSize);
~SuggestionList();
bool add(word_t &&word, freq_t &&freq);
void clear();
bool contains(const WeightedToken &element) const;
bool containsWord(const word_t &word) const;
const WeightedToken *get(size_t index) const;
std::optional<size_t> indexOf(const WeightedToken &element) const;
std::optional<size_t> indexOfWord(const word_t &word) const;
bool isEmpty() const;
size_t size() const;
bool isPrimaryTokenAutoInsert;
private:
std::vector<WeightedToken> tokens;
size_t internalSize;
size_t maxSize;
};
} // namespace ime::nlp
#endif // FLORISBOARD_SUGGESTION_LIST_H

View File

@@ -0,0 +1,61 @@
/*
* 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.
*/
#include "token.h"
#include <utility>
namespace ime::nlp {
Token::Token() : data() {}
Token::Token(word_t &&_data) : data(std::move(_data)) {}
bool operator==(const Token &t1, const Token &t2) {
return t1.data == t2.data;
}
bool operator!=(const Token &t1, const Token &t2) {
return !(t1 == t2);
}
WeightedToken::WeightedToken() : Token(), freq(0) {}
WeightedToken::WeightedToken(word_t &&_data, freq_t _freq) : Token(std::move(_data)), freq(_freq) {}
bool operator==(const WeightedToken &t1, const WeightedToken &t2) {
return t1.data == t2.data && t1.freq == t2.freq;
}
bool operator!=(const WeightedToken &t1, const WeightedToken &t2) {
return !(t1 == t2);
}
bool operator<(const WeightedToken &t1, const WeightedToken &t2) {
return t1.freq < t2.freq;
}
bool operator<=(const WeightedToken &t1, const WeightedToken &t2) {
return t1.freq <= t2.freq;
}
bool operator>(const WeightedToken &t1, const WeightedToken &t2) {
return t1.freq > t2.freq;
}
bool operator>=(const WeightedToken &t1, const WeightedToken &t2) {
return t1.freq >= t2.freq;
}
} // namespace ime::nlp

View File

@@ -0,0 +1,51 @@
/*
* 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.
*/
#ifndef FLORISBOARD_TOKEN_H
#define FLORISBOARD_TOKEN_H
#include "nlp.h"
#include <string>
namespace ime::nlp {
class Token {
public:
word_t data;
Token();
Token(word_t &&_data);
friend bool operator==(const Token &t1, const Token &t2);
friend bool operator!=(const Token &t1, const Token &t2);
};
class WeightedToken : public Token {
public:
freq_t freq;
WeightedToken();
WeightedToken(word_t &&_data, freq_t _freq);
friend bool operator==(const WeightedToken &t1, const WeightedToken &t2);
friend bool operator!=(const WeightedToken &t1, const WeightedToken &t2);
friend bool operator<(const WeightedToken &t1, const WeightedToken &t2);
friend bool operator<=(const WeightedToken &t1, const WeightedToken &t2);
friend bool operator>(const WeightedToken &t1, const WeightedToken &t2);
friend bool operator>=(const WeightedToken &t1, const WeightedToken &t2);
};
} // namespace ime::nlp
#endif // FLORISBOARD_TOKEN_H

View File

@@ -53,6 +53,7 @@ abstract class CrashUtility private constructor() {
private const val UNHANDLED_STACKTRACE_FILE_EXT = "stacktrace"
private var lastActivityCreated: WeakReference<Activity?> = WeakReference(null)
private var stagedException: Throwable? = null
/**
* Installs the CrashUtility crash handler for the given package [context]. Also registers
@@ -148,6 +149,22 @@ abstract class CrashUtility private constructor() {
return true
}
fun stageException(e: Throwable?) {
if (stagedException == null) {
stagedException = e
}
}
@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
fun handleStagedButUnhandledExceptions() {
val e = stagedException ?: return
val handler = Thread.getDefaultUncaughtExceptionHandler()
if (handler is UncaughtExceptionHandler) {
stagedException = null
handler.uncaughtException(null, e)
}
}
/**
* Reads and returns all unhandled stacktrace files.
*
@@ -362,7 +379,6 @@ abstract class CrashUtility private constructor() {
flogInfo(LogTopic.CRASH_UTILITY) {
"Detected application crash, executing custom crash handler."
}
thread ?: return
throwable ?: return
val timestamp = System.currentTimeMillis()
val stacktrace = Log.getStackTraceString(throwable)

View File

@@ -35,6 +35,7 @@ import dev.patrickgold.florisboard.ime.clip.FlorisClipboardManager
import dev.patrickgold.florisboard.ime.clip.provider.ClipboardItem
import dev.patrickgold.florisboard.ime.clip.provider.ItemType
import dev.patrickgold.florisboard.ime.text.TextInputManager
import dev.patrickgold.florisboard.ime.text.composing.Composer
import timber.log.Timber
/**
@@ -124,6 +125,9 @@ class EditorInstance private constructor(
newSelStart: Int, newSelEnd: Int,
candidatesStart: Int, candidatesEnd: Int
) {
if (newSelStart == oldSelStart && newSelEnd == oldSelEnd) {
return
}
// The Android Framework allows that start can be greater than end in some cases. To prevent bugs in the Floris
// input logic, we swap start and end here if this should really be the case.
if (newSelEnd < newSelStart) {
@@ -131,12 +135,12 @@ class EditorInstance private constructor(
} else {
selection.update(newSelStart, newSelEnd)
}
cachedInput.update()
if (isPhantomSpaceActive && wasPhantomSpaceActiveLastUpdate) {
isPhantomSpaceActive = false
} else if (isPhantomSpaceActive && !wasPhantomSpaceActiveLastUpdate) {
wasPhantomSpaceActiveLastUpdate = true
}
cachedInput.update()
if (isComposingEnabled && candidatesStart >= 0 && candidatesEnd >= 0) {
shouldReevaluateComposingSuggestions = true
}
@@ -172,6 +176,26 @@ class EditorInstance private constructor(
}
}
/**
* Internal helper, replacing a call to inputConnection.commitText with text composition in mind.
*/
fun doCommitText(text: String): Pair<Boolean, String> {
val ic = inputConnection ?: return Pair(false, "")
val composer: Composer = FlorisBoard.getInstance().composer
return if (text.length != 1) {
Pair(ic.commitText(text, 1), text)
} else {
ic.beginBatchEdit()
ic.finishComposingText()
val previous = getTextBeforeCursor(composer.toRead)
val (rm, finalText) = composer.getActions(previous, text[0])
if (rm != 0) ic.deleteSurroundingText(rm, 0)
ic.commitText(finalText, 1)
ic.endBatchEdit()
Pair(true, finalText)
}
}
/**
* Commits the given [text] to this editor instance and adjusts both the cursor position and
* composing region, if any.
@@ -186,7 +210,7 @@ class EditorInstance private constructor(
fun commitText(text: String): Boolean {
val ic = inputConnection ?: return false
return if (isRawInputEditor || selection.isSelectionMode || !isComposingEnabled) {
ic.commitText(text, 1)
doCommitText(text).first
} else {
ic.beginBatchEdit()
val isWordComponent = CachedInput.isWordComponent(text)
@@ -199,8 +223,8 @@ class EditorInstance private constructor(
}
!isPhantomSpace && isWordComponent -> {
ic.finishComposingText()
ic.commitText(text, 1)
ic.setComposingRegion(cachedInput.currentWord.start, cachedInput.currentWord.end + text.length)
val finalText = doCommitText(text).second
ic.setComposingRegion(cachedInput.currentWord.start, cachedInput.currentWord.end + finalText.length)
}
else -> {
ic.finishComposingText()
@@ -1001,8 +1025,8 @@ class CachedInput(private val editorInstance: EditorInstance) {
private set
companion object {
private const val CACHED_TEXT_N_CHARS_BEFORE_CURSOR: Int = 192
private const val CACHED_TEXT_N_CHARS_AFTER_CURSOR: Int = 64
private const val CACHED_TEXT_N_CHARS_BEFORE_CURSOR: Int = 128
private const val CACHED_TEXT_N_CHARS_AFTER_CURSOR: Int = 48
private val WORD_EVAL_REGEX = """[^\p{L}\']""".toRegex()
private val WORD_SPLIT_REGEX_EN = """((?<=$WORD_EVAL_REGEX)|(?=$WORD_EVAL_REGEX))""".toRegex()

View File

@@ -25,19 +25,35 @@ import android.inputmethodservice.ExtractEditText
import android.inputmethodservice.InputMethodService
import android.media.AudioManager
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.VibrationEffect
import android.os.Vibrator
import android.view.*
import android.util.Size
import android.view.ContextThemeWrapper
import android.view.Gravity
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.view.WindowManager
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InlineSuggestionsRequest
import android.view.inputmethod.InlineSuggestionsResponse
import android.view.inputmethod.InputConnection
import android.view.inputmethod.InputMethodManager
import android.widget.Button
import android.widget.FrameLayout
import android.widget.inline.InlinePresentationSpec
import androidx.annotation.RequiresApi
import androidx.annotation.StyleRes
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.lifecycle.*
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.crashutility.CrashUtility
import dev.patrickgold.florisboard.debug.*
import dev.patrickgold.florisboard.ime.clip.ClipboardInputManager
import dev.patrickgold.florisboard.ime.clip.FlorisClipboardManager
@@ -47,15 +63,26 @@ import dev.patrickgold.florisboard.ime.media.MediaInputManager
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
import dev.patrickgold.florisboard.ime.popup.PopupLayerView
import dev.patrickgold.florisboard.ime.text.TextInputManager
import dev.patrickgold.florisboard.ime.text.composing.Composer
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.key.*
import dev.patrickgold.florisboard.ime.text.key.CurrencySet
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
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.*
import dev.patrickgold.florisboard.util.AppVersionUtils
import dev.patrickgold.florisboard.util.ViewLayoutUtils
import dev.patrickgold.florisboard.util.debugSummarize
import dev.patrickgold.florisboard.util.findViewWithType
import dev.patrickgold.florisboard.util.refreshLayoutOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
@@ -75,6 +102,9 @@ private var florisboardInstance: FlorisBoard? = null
/**
* Core class responsible to link together both the text and media input managers as well as
* managing the one-handed UI.
*
* All inline suggestion code has been added based on this demo autofill IME provided by Android directly:
* https://cs.android.com/android/platform/superproject/+/master:development/samples/AutofillKeyboard/src/com/example/android/autofillkeyboard/AutofillImeService.java
*/
class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager.OnPrimaryClipChangedListener,
ThemeManager.OnThemeUpdatedListener {
@@ -109,37 +139,40 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
private var vibrator: Vibrator? = null
private var internalBatchNestingLevel: Int = 0
private val internalSelectionCache = object {
var selectionCatchCount: Int = 0
var oldSelStart: Int = -1
var oldSelEnd: Int = -1
var newSelStart: Int = -1
var newSelEnd: Int = -1
var candidatesStart: Int = -1
var candidatesEnd: Int = -1
}
var activeEditorInstance: EditorInstance = EditorInstance.default()
val subtypeManager: SubtypeManager get() = SubtypeManager.default()
val composer: Composer get() = subtypeManager.imeConfig.composers[subtypeManager.imeConfig.composerNames.indexOf(activeSubtype.composerName)]
lateinit var activeSubtype: Subtype
private var currentThemeIsNight: Boolean = false
private var currentThemeResId: Int = 0
private var isNumberRowVisible: Boolean = false
private var isWindowShown: Boolean = false
val textInputManager: TextInputManager
val mediaInputManager: MediaInputManager
val clipInputManager: ClipboardInputManager
private var responseState = ResponseState.RESET
private var pendingResponse: Runnable? = null
private val handler: Handler = Handler(Looper.getMainLooper())
lateinit var textInputManager: TextInputManager
lateinit var mediaInputManager: MediaInputManager
lateinit var clipInputManager: ClipboardInputManager
var isClipboardContextMenuShown = false
init {
florisboardInstance = this
// MUST WRAP all code within Service init in try..catch to prevent any crash loops
try {
florisboardInstance = this
textInputManager = TextInputManager.getInstance()
mediaInputManager = MediaInputManager.getInstance()
clipInputManager = ClipboardInputManager.getInstance()
textInputManager = TextInputManager.getInstance()
mediaInputManager = MediaInputManager.getInstance()
clipInputManager = ClipboardInputManager.getInstance()
System.loadLibrary("florisboard-native")
} catch (e: Exception) {
CrashUtility.stageException(e)
}
}
lateinit var asyncExecutor: ExecutorService
@@ -172,54 +205,51 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
}
override fun onCreate() {
/*if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork() // or .detectAll() for all detectable problems
.penaltyLog()
.build()
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.penaltyDeath()
.build()
)
}*/
flogInfo(LogTopic.IMS_EVENTS)
serviceLifecycleDispatcher.onServicePreSuperOnCreate()
// MUST WRAP all code within Service onCreate() in try..catch to prevent any crash loops
try {
// Additional try..catch wrapper as the event listeners chain or the super.onCreate() method could crash
// and lead to a crash loop
try {
// "Main" try..catch block
flogInfo(LogTopic.IMS_EVENTS)
serviceLifecycleDispatcher.onServicePreSuperOnCreate()
imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
audioManager = getSystemService(Context.AUDIO_SERVICE) as? AudioManager
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
prefs.sync()
activeSubtype = subtypeManager.getActiveSubtype() ?: Subtype.DEFAULT
imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
audioManager = getSystemService(Context.AUDIO_SERVICE) as? AudioManager
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
prefs.sync()
activeSubtype = subtypeManager.getActiveSubtype() ?: Subtype.DEFAULT
currentThemeIsNight = themeManager.activeTheme.isNightTheme
currentThemeResId = getDayNightBaseThemeId(currentThemeIsNight)
isNumberRowVisible = prefs.keyboard.numberRow
setTheme(currentThemeResId)
themeManager.registerOnThemeUpdatedListener(this)
currentThemeIsNight = themeManager.activeTheme.isNightTheme
currentThemeResId = getDayNightBaseThemeId(currentThemeIsNight)
isNumberRowVisible = prefs.keyboard.numberRow
setTheme(currentThemeResId)
themeManager.registerOnThemeUpdatedListener(this)
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
asyncExecutor = Executors.newSingleThreadExecutor()
florisClipboardManager = FlorisClipboardManager.getInstance().also {
it.initialize(this)
it.addPrimaryClipChangedListener(this)
asyncExecutor = Executors.newSingleThreadExecutor()
florisClipboardManager = FlorisClipboardManager.getInstance().also {
it.initialize(this)
it.addPrimaryClipChangedListener(this)
}
} catch (e: Exception) {
super.onCreate() // MUST CALL even if exception thrown or crash loop is imminent
CrashUtility.stageException(e)
return
}
// Code executed here indicates no crashes occurred, so we execute the onCreate() event as normal
super.onCreate()
eventListeners.toList().forEach { it?.onCreate() }
} catch (e: Exception) {
CrashUtility.stageException(e)
}
super.onCreate()
eventListeners.toList().forEach { it?.onCreate() }
}
@SuppressLint("InflateParams")
override fun onCreateInputView(): View? {
flogInfo(LogTopic.IMS_EVENTS)
CrashUtility.handleStagedButUnhandledExceptions()
updateThemeContext(currentThemeResId)
@@ -333,6 +363,11 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
flogInfo(LogTopic.IMS_EVENTS)
super.onStartInput(attribute, restarting)
responseState = if (responseState == ResponseState.RECEIVE_RESPONSE) {
ResponseState.START_INPUT
} else {
ResponseState.RESET
}
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_MONITOR)
}
@@ -353,6 +388,10 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
if (finishingInput) {
activeEditorInstance = EditorInstance.default()
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
textInputManager.smartbarView?.clearInlineSuggestions()
}
}
super.onFinishInputView(finishingInput)
@@ -366,6 +405,71 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
super.onFinishInput()
}
@RequiresApi(Build.VERSION_CODES.R)
override fun onCreateInlineSuggestionsRequest(uiExtras: Bundle): InlineSuggestionsRequest? {
return if (prefs.smartbar.enabled && prefs.suggestion.api30InlineSuggestionsEnabled) {
flogInfo(LogTopic.IMS_EVENTS) {
"Creating inline suggestions request because Smartbar and inline suggestions are enabled."
}
val stylesBundle = themeManager.createInlineSuggestionUiStyleBundle(themeContext)
InlinePresentationSpec.Builder(
Size(
inputView?.desiredInlineSuggestionsMinWidth ?: 0,
inputView?.desiredInlineSuggestionsMinHeight ?: 0
),
Size(
inputView?.desiredInlineSuggestionsMaxWidth ?: 0,
inputView?.desiredInlineSuggestionsMaxHeight ?: 0
)
).let { spec ->
spec.setStyle(stylesBundle)
InlineSuggestionsRequest.Builder(listOf(spec.build())).let { request ->
request.setMaxSuggestionCount(6)
request.build()
}
}
} else {
flogInfo(LogTopic.IMS_EVENTS) {
"Ignoring inline suggestions request because Smartbar and/or inline suggestions are disabled."
}
null
}
}
@RequiresApi(Build.VERSION_CODES.R)
override fun onInlineSuggestionsResponse(response: InlineSuggestionsResponse): Boolean {
flogInfo(LogTopic.IMS_EVENTS) {
"Received inline suggestions response with ${response.inlineSuggestions.size} suggestion(s) provided."
}
textInputManager.smartbarView?.clearInlineSuggestions()
postPendingResponse(response)
return true
}
@RequiresApi(Build.VERSION_CODES.R)
private fun cancelPendingResponse() {
pendingResponse?.let {
handler.removeCallbacks(it)
pendingResponse = null
}
}
@RequiresApi(Build.VERSION_CODES.R)
private fun postPendingResponse(response: InlineSuggestionsResponse) {
cancelPendingResponse()
val inlineSuggestions = response.inlineSuggestions
responseState = ResponseState.RECEIVE_RESPONSE
pendingResponse = Runnable {
pendingResponse = null
if (responseState == ResponseState.START_INPUT && inlineSuggestions.isEmpty()) {
textInputManager.smartbarView?.clearInlineSuggestions()
} else {
textInputManager.smartbarView?.showInlineSuggestions(inlineSuggestions)
}
responseState = ResponseState.RESET
}.also { handler.post(it) }
}
override fun onWindowShown() {
super.onWindowShown()
if (isWindowShown) {
@@ -383,10 +487,10 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
isNumberRowVisible = newIsNumberRowVisible
}
themeManager.update()
updateOneHandedPanelVisibility()
activeSubtype = subtypeManager.getActiveSubtype() ?: Subtype.DEFAULT
onSubtypeChanged(activeSubtype)
setActiveInput(R.id.text_input)
updateOneHandedPanelVisibility()
if (prefs.devtools.enabled && prefs.devtools.showHeapMemoryStats) {
devtoolsOverlaySyncJob?.cancel()
@@ -445,24 +549,6 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
*/
fun endInternalBatchEdit() {
internalBatchNestingLevel = (internalBatchNestingLevel - 1).coerceAtLeast(0)
if (internalBatchNestingLevel == 0) {
internalSelectionCache.apply {
if (selectionCatchCount > 0) {
onUpdateSelection(
oldSelStart, oldSelEnd,
newSelStart, newSelEnd,
candidatesStart, candidatesEnd
)
selectionCatchCount = 0
oldSelStart = -1
oldSelEnd = -1
newSelStart = -1
newSelEnd = -1
candidatesStart = -1
candidatesEnd = -1
}
}
}
}
override fun onUpdateSelection(
@@ -476,27 +562,13 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
candidatesStart, candidatesEnd
)
if (internalBatchNestingLevel == 0) {
flogInfo(LogTopic.IMS_EVENTS) { "$oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd" }
activeEditorInstance.onUpdateSelection(
oldSelStart, oldSelEnd,
newSelStart, newSelEnd,
candidatesStart, candidatesEnd
)
eventListeners.toList().forEach { it?.onUpdateSelection() }
} else {
flogInfo(LogTopic.IMS_EVENTS) {
"$oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd: caught due to internal batch level of $internalBatchNestingLevel!"
}
if (internalSelectionCache.selectionCatchCount++ == 0) {
internalSelectionCache.oldSelStart = oldSelStart
internalSelectionCache.oldSelEnd = oldSelEnd
}
internalSelectionCache.newSelStart = newSelStart
internalSelectionCache.newSelEnd = newSelEnd
internalSelectionCache.candidatesStart = candidatesStart
internalSelectionCache.candidatesEnd = candidatesEnd
}
flogInfo(LogTopic.IMS_EVENTS) { "$oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd" }
activeEditorInstance.onUpdateSelection(
oldSelStart, oldSelEnd,
newSelStart, newSelEnd,
candidatesStart, candidatesEnd
)
eventListeners.toList().forEach { it?.onUpdateSelection() }
}
override fun onThemeUpdated(theme: Theme) {
@@ -871,6 +943,10 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
fun onSubtypeChanged(newSubtype: Subtype) {}
}
private enum class ResponseState {
RESET, RECEIVE_RESPONSE, START_INPUT
}
/**
* Data class which holds the base information for this IME. Matches the structure of
* ime/config.json so it can be parsed. Used by [SubtypeManager] and by the prefs.
@@ -889,15 +965,34 @@ class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager
data class ImeConfig(
@SerialName("package")
val packageName: String,
@SerialName("composers")
val composers: List<Composer> = listOf(),
@SerialName("currencySets")
val currencySets: List<CurrencySet> = listOf(),
@SerialName("defaultSubtypes")
val defaultSubtypes: List<DefaultSubtype> = listOf()
) {
@Transient var currencySetNames: List<String> = listOf()
@Transient var currencySetLabels: List<String> = listOf()
@Transient var composerNames: List<String> = listOf()
@Transient var composerLabels: List<String> = listOf()
@Transient var defaultSubtypesLanguageCodes: List<String> = listOf()
@Transient var defaultSubtypesLanguageNames: List<String> = listOf()
init {
val tmpComposerList = composers.map { Pair(it.name, it.label) }.toMutableList()
// Sort composer list alphabetically by the label of a composer
tmpComposerList.sortBy { it.second }
// Move selected composers to the top of the list
for (composerName in listOf("appender")) {
val index: Int = tmpComposerList.indexOfFirst { it.first == composerName }
if (index > 0) {
tmpComposerList.add(0, tmpComposerList.removeAt(index))
}
}
composerNames = tmpComposerList.map { it.first }.toList()
composerLabels = tmpComposerList.map { it.second }.toList()
val tmpCurrencyList = mutableListOf<Pair<String, String>>()
for (currencySet in currencySets) {
tmpCurrencyList.add(Pair(currencySet.name, currencySet.label))

View File

@@ -17,6 +17,9 @@
package dev.patrickgold.florisboard.ime.core
import android.os.SystemClock
import android.util.SparseArray
import androidx.core.util.forEach
import androidx.core.util.set
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
@@ -36,8 +39,9 @@ class InputEventDispatcher private constructor(
private val repeatableKeyCodes: IntArray
) : InputKeyEventSender {
private val channel: Channel<InputKeyEvent> = Channel(channelCapacity)
private val scope: CoroutineScope = CoroutineScope(defaultDispatcher + SupervisorJob())
private val pressedKeys: HashMap<Int, PressedKeyInfo> = hashMapOf()
private val mainScope: CoroutineScope = CoroutineScope(mainDispatcher + SupervisorJob())
private val defaultScope: CoroutineScope = CoroutineScope(defaultDispatcher + SupervisorJob())
private val pressedKeys: SparseArray<PressedKeyInfo> = SparseArray()
var lastKeyEventDown: InputKeyEvent? = null
private set
var lastKeyEventUp: InputKeyEvent? = null
@@ -69,16 +73,26 @@ class InputEventDispatcher private constructor(
*/
fun new(
channelCapacity: Int = DEFAULT_CHANNEL_CAPACITY,
mainDispatcher: CoroutineDispatcher = Dispatchers.Main,
mainDispatcher: CoroutineDispatcher = Dispatchers.Main.immediate,
defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
repeatableKeyCodes: IntArray = intArrayOf()
): InputEventDispatcher = InputEventDispatcher(
channelCapacity, mainDispatcher, defaultDispatcher, repeatableKeyCodes.clone()
)
private fun <T> SparseArray<T>.removeAndReturn(key: Int): T? {
val elem = get(key)
return if (elem == null) {
null
} else {
remove(key)
elem
}
}
}
init {
scope.launch(defaultDispatcher) {
defaultScope.launch {
for (ev in channel) {
if (!isActive) break
val startTime = System.nanoTime()
@@ -87,11 +101,11 @@ class InputEventDispatcher private constructor(
}
when (ev.action) {
InputKeyEvent.Action.DOWN -> {
if (pressedKeys.containsKey(ev.data.code)) continue
if (pressedKeys.indexOfKey(ev.data.code) >= 0) continue
pressedKeys[ev.data.code] = PressedKeyInfo(
eventTimeDown = ev.eventTime,
repeatKeyPressJob = if (!repeatableKeyCodes.contains(ev.data.code)) { null } else {
scope.launch(defaultDispatcher) {
defaultScope.launch {
delay(600)
while (isActive) {
channel.send(InputKeyEvent.repeat(ev.data))
@@ -108,7 +122,7 @@ class InputEventDispatcher private constructor(
}
}
InputKeyEvent.Action.DOWN_UP -> {
pressedKeys.remove(ev.data.code)?.repeatKeyPressJob?.cancel()
pressedKeys.removeAndReturn(ev.data.code)?.repeatKeyPressJob?.cancel()
withContext(mainDispatcher) {
keyEventReceiver?.onInputKeyDown(ev)
keyEventReceiver?.onInputKeyUp(ev)
@@ -119,7 +133,7 @@ class InputEventDispatcher private constructor(
}
}
InputKeyEvent.Action.UP -> {
pressedKeys.remove(ev.data.code)?.repeatKeyPressJob?.cancel()
pressedKeys.removeAndReturn(ev.data.code)?.repeatKeyPressJob?.cancel()
withContext(mainDispatcher) {
keyEventReceiver?.onInputKeyUp(ev)
}
@@ -128,14 +142,14 @@ class InputEventDispatcher private constructor(
}
}
InputKeyEvent.Action.REPEAT -> {
if (pressedKeys.containsKey(ev.data.code)) {
if (pressedKeys.indexOfKey(ev.data.code) >= 0) {
withContext(mainDispatcher) {
keyEventReceiver?.onInputKeyRepeat(ev)
}
}
}
InputKeyEvent.Action.CANCEL -> {
pressedKeys.remove(ev.data.code)?.repeatKeyPressJob?.cancel()
pressedKeys.removeAndReturn(ev.data.code)?.repeatKeyPressJob?.cancel()
withContext(mainDispatcher) {
keyEventReceiver?.onInputKeyCancel(ev)
}
@@ -145,16 +159,13 @@ class InputEventDispatcher private constructor(
Timber.d("Time elapsed: ${(System.nanoTime() - startTime) / 1_000_000}")
}
}
val pressedKeysIterator = pressedKeys.iterator()
while (pressedKeysIterator.hasNext()) {
pressedKeysIterator.next().value.repeatKeyPressJob?.cancel()
pressedKeysIterator.remove()
}
pressedKeys.forEach { _, value -> value.repeatKeyPressJob?.cancel() }
pressedKeys.clear()
}
}
override fun send(ev: InputKeyEvent) {
scope.launch(mainDispatcher) {
mainScope.launch {
channel.send(ev)
}
}
@@ -167,7 +178,7 @@ class InputEventDispatcher private constructor(
* @return True if the given [code] is currently down, false otherwise.
*/
fun isPressed(code: Int): Boolean {
return pressedKeys.containsKey(code)
return pressedKeys.indexOfKey(code) >= 0
}
/**
@@ -175,7 +186,8 @@ class InputEventDispatcher private constructor(
*/
fun close() {
keyEventReceiver = null
scope.cancel()
mainScope.cancel()
defaultScope.cancel()
}
data class PressedKeyInfo(

View File

@@ -22,9 +22,11 @@ import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Typeface
import android.os.Build
import android.text.TextPaint
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.util.Size
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.ViewFlipper
@@ -56,6 +58,15 @@ class InputView : LinearLayout {
var shouldGiveAdditionalSpace: Boolean = false
private set
var desiredInlineSuggestionsMinWidth: Int = 0
private set
var desiredInlineSuggestionsMinHeight: Int = 0
private set
var desiredInlineSuggestionsMaxWidth: Int = 0
private set
var desiredInlineSuggestionsMaxHeight: Int = 0
private set
var mainViewFlipper: ViewFlipper? = null
private set
var oneHandedCtrlPanelStart: ViewGroup? = null
@@ -123,7 +134,7 @@ class InputView : LinearLayout {
baseTextInputHeight += additionalHeight
}
val smartbarDisabled = !prefs.smartbar.enabled ||
tim.keyVariation == KeyVariation.PASSWORD && prefs.keyboard.numberRow
tim.keyVariation == KeyVariation.PASSWORD && prefs.keyboard.numberRow && !prefs.suggestion.api30InlineSuggestionsEnabled
if (smartbarDisabled) {
baseHeight = baseTextInputHeight
baseSmartbarHeight = 0.0f
@@ -143,6 +154,14 @@ class InputView : LinearLayout {
context
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val width = MeasureSpec.getSize(widthMeasureSpec)
desiredInlineSuggestionsMinWidth = width / 3
desiredInlineSuggestionsMinHeight = desiredSmartbarHeight.toInt()
desiredInlineSuggestionsMaxWidth = (width / 1.5).toInt()
desiredInlineSuggestionsMaxHeight = desiredSmartbarHeight.toInt()
}
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(baseHeight.roundToInt(), MeasureSpec.EXACTLY))
}

View File

@@ -0,0 +1,48 @@
/*
* 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.core
/**
* Type alias for a native pointer.
*/
typealias NativePtr = Long
/**
* Constant value for a native null pointer.
*/
const val NATIVE_NULLPTR: NativePtr = 0
/**
* Generic interface for a native instance object. Defines the basic
* methods which each native instance wrapper should define and be able
* to handle to.
*/
interface NativeInstanceWrapper {
/**
* Returns the native pointer of this instance. The returned pointer
* is only valid if [dispose] has not been previously called.
*
* @return The native null pointer for this instance.
*/
fun nativePtr(): NativePtr
/**
* Deletes the native object and frees allocated resources. After
* invoking this method one MUST NOT touch this instance ever again.
*/
fun dispose()
}

View File

@@ -26,6 +26,7 @@ import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
import dev.patrickgold.florisboard.ime.text.gestures.DistanceThreshold
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.gestures.VelocityThreshold
import dev.patrickgold.florisboard.ime.text.key.KeyHintConfiguration
import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
import dev.patrickgold.florisboard.ime.text.key.UtilityKeyAction
import dev.patrickgold.florisboard.ime.text.smartbar.CandidateView
@@ -129,7 +130,9 @@ class Preferences(
}
companion object {
private val OLD_SUBTYPES_REGEX = """^([\-0-9]+/[\-a-zA-Z0-9]+/[a-zA-Z_]+[;]*)+${'$'}""".toRegex()
// old settings are id/language/layout and id/language/currencySet/layout
// new settings have composer
private val OLD_SUBTYPES_REGEX = """^([\-0-9]+/[\-a-zA-Z0-9]+(/[a-zA-Z_]+)?/[a-zA-Z_]+[;]*)+${'$'}""".toRegex()
private var defaultInstance: Preferences? = null
@Synchronized
@@ -397,6 +400,7 @@ class Preferences(
const val KEY_SPACING_VERTICAL = "keyboard__key_spacing_vertical"
const val LANDSCAPE_INPUT_UI_MODE = "keyboard__landscape_input_ui_mode"
const val LONG_PRESS_DELAY = "keyboard__long_press_delay"
const val MERGE_HINT_POPUPS_ENABLED = "keyboard__merge_hint_popups_enabled"
const val NUMBER_ROW = "keyboard__number_row"
const val ONE_HANDED_MODE = "keyboard__one_handed_mode"
const val ONE_HANDED_MODE_SCALE_FACTOR = "keyboard__one_handed_mode_scale_factor"
@@ -447,6 +451,9 @@ class Preferences(
var longPressDelay: Int = 0
get() = prefs.getPref(LONG_PRESS_DELAY, 300)
private set
var mergeHintPopupsEnabled: Boolean
get() = prefs.getPref(MERGE_HINT_POPUPS_ENABLED, false)
set(v) = prefs.setPref(MERGE_HINT_POPUPS_ENABLED, v)
var numberRow: Boolean
get() = prefs.getPref(NUMBER_ROW, false)
set(v) = prefs.setPref(NUMBER_ROW, v)
@@ -485,6 +492,10 @@ class Preferences(
var vibrationStrength: Int = 0
get() = prefs.getPref(VIBRATION_STRENGTH, -1)
private set
fun getKeyHintConfiguration(): KeyHintConfiguration {
return KeyHintConfiguration(hintedSymbolsMode, hintedNumberRowMode, mergeHintPopupsEnabled)
}
}
/**
@@ -522,14 +533,18 @@ class Preferences(
*/
class Suggestion(private val prefs: Preferences) {
companion object {
const val BLOCK_POSSIBLY_OFFENSIVE = "suggestion__block_possibly_offensive"
const val CLIPBOARD_CONTENT_ENABLED = "suggestion__clipboard_content_enabled"
const val CLIPBOARD_CONTENT_TIMEOUT = "suggestion__clipboard_content_timeout"
const val DISPLAY_MODE = "suggestion__display_mode"
const val ENABLED = "suggestion__enabled"
const val USE_PREV_WORDS = "suggestion__use_prev_words"
const val API30_INLINE_SUGGESTIONS_ENABLED = "suggestion__api30_inline_suggestions_enabled"
const val BLOCK_POSSIBLY_OFFENSIVE = "suggestion__block_possibly_offensive"
const val CLIPBOARD_CONTENT_ENABLED = "suggestion__clipboard_content_enabled"
const val CLIPBOARD_CONTENT_TIMEOUT = "suggestion__clipboard_content_timeout"
const val DISPLAY_MODE = "suggestion__display_mode"
const val ENABLED = "suggestion__enabled"
const val USE_PREV_WORDS = "suggestion__use_prev_words"
}
var api30InlineSuggestionsEnabled: Boolean
get() = prefs.getPref(API30_INLINE_SUGGESTIONS_ENABLED, true)
set(v) = prefs.setPref(API30_INLINE_SUGGESTIONS_ENABLED, v)
var blockPossiblyOffensive: Boolean
get() = prefs.getPref(BLOCK_POSSIBLY_OFFENSIVE, true)
set(v) = prefs.setPref(BLOCK_POSSIBLY_OFFENSIVE, v)

View File

@@ -16,6 +16,8 @@
package dev.patrickgold.florisboard.ime.core
import dev.patrickgold.florisboard.ime.text.composing.Appender
import dev.patrickgold.florisboard.ime.text.composing.Composer
import dev.patrickgold.florisboard.ime.text.layout.LayoutType
import dev.patrickgold.florisboard.util.LocaleUtils
import kotlinx.serialization.*
@@ -32,12 +34,14 @@ import java.util.*
* @property id The ID of this subtype. Although this can be any numeric value, its value
* typically matches the one of the [DefaultSubtype] with the same locale.
* @property locale The locale this subtype is bound to.
* @property composerName The composer name to composer characters the way they should.
* @property currencySetName The currency set name to display the correct currency symbols for this subtype.
* @property layoutMap The layout map to properly display the correct layout for each layout type.
*/
data class Subtype(
val id: Int,
val locale: Locale,
val composerName: String,
val currencySetName: String,
val layoutMap: SubtypeLayoutMap,
) {
@@ -50,6 +54,7 @@ data class Subtype(
val DEFAULT = Subtype(
id = -1,
locale = Locale.ENGLISH,
composerName = Appender.name,
currencySetName = "\$default",
layoutMap = SubtypeLayoutMap(characters = "qwerty")
)
@@ -67,17 +72,29 @@ data class Subtype(
*/
fun fromString(str: String): Subtype {
val data = str.split("/")
if (data.size != 4) {
throw InvalidPropertiesFormatException(
"Given string contains more or less than 4 properties..."
)
} else {
val locale = LocaleUtils.stringToLocale(data[1])
return Subtype(
data[0].toInt(),
locale,
data[2],
SubtypeLayoutMap.fromString(data[3])
when (data.size) {
4 -> {
val locale = LocaleUtils.stringToLocale(data[1])
return Subtype(
data[0].toInt(),
locale,
Appender.name,
data[2],
SubtypeLayoutMap.fromString(data[3])
)
}
5 -> {
val locale = LocaleUtils.stringToLocale(data[1])
return Subtype(
data[0].toInt(),
locale,
data[2],
data[3],
SubtypeLayoutMap.fromString(data[4])
)
}
else -> throw InvalidPropertiesFormatException(
"Given string contains more or less than 5 properties..."
)
}
}
@@ -86,6 +103,7 @@ data class Subtype(
init {
var result = id
result = 31 * result + locale.hashCode()
result = 31 * result + composerName.hashCode()
result = 31 * result + currencySetName.hashCode()
result = 31 * result + layoutMap.hashCode()
_hashCode = result
@@ -93,11 +111,11 @@ data class Subtype(
/**
* Converts this object into its string representation. Format:
* <id>/<language_tag>/<currency_set_name>/<layout_map>
* <id>/<language_tag>/<composer_name>/<currency_set_name>/<layout_map>
*/
override fun toString(): String {
val languageTag = locale.toLanguageTag()
return "$id/$languageTag/$currencySetName/$layoutMap"
return "$id/$languageTag/$composerName/$currencySetName/$layoutMap"
}
/**
@@ -117,6 +135,7 @@ data class Subtype(
if (id != other.id) return false
if (locale != other.locale) return false
if (composerName != other.composerName) return false
if (currencySetName != other.currencySetName) return false
if (layoutMap != other.layoutMap) return false
@@ -310,6 +329,8 @@ data class DefaultSubtype(
@Serializable(with = LocaleSerializer::class)
@SerialName("languageTag")
var locale: Locale,
@SerialName("composer")
var composerName: String,
@SerialName("currencySet")
var currencySetName: String,
var preferred: SubtypeLayoutMap

View File

@@ -112,16 +112,18 @@ class SubtypeManager(
* list, if it does not exist.
*
* @param locale The locale of the subtype to be added.
* @param composerName The composer name of the subtype to be added.
* @param currencySetName The currency set name of the subtype to be added.
* @param layoutMap The layout map of the subtype to be added.
* @return True if the subtype was added, false otherwise. A return value of false indicates
* that the subtype already exists.
*/
fun addSubtype(locale: Locale, currencySetName: String, layoutMap: SubtypeLayoutMap): Boolean {
fun addSubtype(locale: Locale, composerName: String, currencySetName: String, layoutMap: SubtypeLayoutMap): Boolean {
return addSubtype(
Subtype(
(locale.hashCode() + 31 * layoutMap.hashCode() + 31 * currencySetName.hashCode()),
locale,
composerName,
currencySetName,
layoutMap
)

View File

@@ -17,40 +17,35 @@
package dev.patrickgold.florisboard.ime.dictionary
import dev.patrickgold.florisboard.ime.extension.Asset
import dev.patrickgold.florisboard.ime.nlp.LanguageModel
import dev.patrickgold.florisboard.ime.nlp.MutableLanguageModel
import dev.patrickgold.florisboard.ime.nlp.Token
import dev.patrickgold.florisboard.ime.nlp.WeightedToken
import dev.patrickgold.florisboard.ime.nlp.SuggestionList
import dev.patrickgold.florisboard.ime.nlp.Word
/**
* Standardized dictionary interface for interacting with dictionaries.
*/
interface Dictionary<T : Any, F : Comparable<F>> : Asset {
val languageModel: LanguageModel<T, F>
interface Dictionary : Asset {
/**
* Gets token predictions based on the given [precedingTokens] and the [currentToken]. The
* length of the returned list is limited to [maxSuggestionCount]. Note that the returned list
* may at any time give back less items than [maxSuggestionCount] indicates.
*/
fun getTokenPredictions(
precedingTokens: List<Token<T>>,
currentToken: Token<T>?,
precedingTokens: List<Word>,
currentToken: Word?,
maxSuggestionCount: Int,
allowPossiblyOffensive: Boolean
): List<WeightedToken<T, F>>
allowPossiblyOffensive: Boolean,
destSuggestionList: SuggestionList
)
fun getDate(): Long
fun getVersion(): Int
}
interface MutableDictionary<T : Any, F : Comparable<F>> : Dictionary<T, F> {
override val languageModel: MutableLanguageModel<T, F>
interface MutableDictionary : Dictionary {
fun trainTokenPredictions(
precedingTokens: List<Token<T>>,
lastToken: Token<T>
precedingTokens: List<Word>,
lastToken: Word
)
fun setDate(date: Int)

View File

@@ -19,18 +19,27 @@ package dev.patrickgold.florisboard.ime.dictionary
import android.content.Context
import androidx.room.Room
import dev.patrickgold.florisboard.ime.core.Preferences
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.extension.AssetRef
import dev.patrickgold.florisboard.ime.nlp.SuggestionList
import dev.patrickgold.florisboard.ime.nlp.Word
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import timber.log.Timber
import java.lang.ref.WeakReference
import java.util.*
/**
* TODO: document
*/
class DictionaryManager private constructor(context: Context) {
class DictionaryManager private constructor(
context: Context,
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
private val applicationContext: WeakReference<Context> = WeakReference(context.applicationContext ?: context)
private val prefs get() = Preferences.default()
private val dictionaryCache: MutableMap<String, Dictionary<String, Int>> = mutableMapOf()
private val dictionaryCache: MutableMap<String, Dictionary> = mutableMapOf()
private var florisUserDictionaryDatabase: FlorisUserDictionaryDatabase? = null
private var systemUserDictionaryDatabase: SystemUserDictionaryDatabase? = null
@@ -56,25 +65,54 @@ class DictionaryManager private constructor(context: Context) {
}
}
fun loadDictionary(ref: AssetRef): Result<Dictionary<String, Int>> {
dictionaryCache[ref.toString()]?.let {
return Result.success(it)
inline fun suggest(
currentWord: Word,
preceidingWords: List<Word>,
subtype: Subtype,
allowPossiblyOffensive: Boolean,
maxSuggestionCount: Int,
block: (suggestions: SuggestionList) -> Unit
) {
val suggestions = SuggestionList.new(maxSuggestionCount)
queryUserDictionary(currentWord, subtype.locale, suggestions)
block(suggestions)
suggestions.dispose()
}
fun prepareDictionaries(subtype: Subtype) {
// TODO: Implement this
}
fun queryUserDictionary(word: Word, locale: Locale, destSuggestionList: SuggestionList) {
val florisDao = florisUserDictionaryDao()
val systemDao = systemUserDictionaryDao()
if (florisDao == null && systemDao == null) {
return
}
if (ref.path.endsWith(".flict")) {
// Assume this is a Flictionary
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)
if (prefs.dictionary.enableFlorisUserDictionary) {
florisDao?.query(word, locale)?.let {
for (entry in it) {
destSuggestionList.add(entry.word, entry.freq)
}
}
florisDao?.queryShortcut(word, locale)?.let {
for (entry in it) {
destSuggestionList.add(entry.word, entry.freq)
}
}
}
if (prefs.dictionary.enableSystemUserDictionary) {
systemDao?.query(word, locale)?.let {
for (entry in it) {
destSuggestionList.add(entry.word, entry.freq)
}
}
systemDao?.queryShortcut(word, locale)?.let {
for (entry in it) {
destSuggestionList.add(entry.word, entry.freq)
}
}
} 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

View File

@@ -30,15 +30,15 @@ import kotlin.jvm.Throws
* This class accepts binary dictionary files of the type "flict" as defined in here:
* https://github.com/florisboard/dictionary-tools/blob/main/flictionary.md
*/
/**
class Flictionary private constructor(
override val name: String,
override val label: String,
override val authors: List<String>,
private val date: Long,
private val version: Int,
private val headerStr: String,
override val languageModel: LanguageModel<String, Int>
) : Dictionary<String, Int> {
private val headerStr: String
) : Dictionary {
companion object {
private const val VERSION_0 = 0x0
@@ -427,3 +427,4 @@ fun InputStream.readNext(b: ByteArray, off: Int, len: Int): Int {
}
return lenRead
}
*/

View File

@@ -131,7 +131,7 @@ interface UserDictionaryDatabase {
fun reset()
fun importCombinedList(context: Context, uri: Uri): Result<Unit> {
return ExternalContentUtils.readFromUri(context, uri,2048) { src ->
return ExternalContentUtils.readFromUri(context, uri,6_192_000) { src ->
var isFirstLine = true
src.forEachLine { line ->
if (isFirstLine) {

View File

@@ -22,10 +22,8 @@ import dev.patrickgold.florisboard.ime.keyboard.CaseSelector
import dev.patrickgold.florisboard.ime.keyboard.KeyData
import dev.patrickgold.florisboard.ime.keyboard.VariationSelector
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyData
import dev.patrickgold.florisboard.ime.text.keyboard.AutoTextKeyData
import dev.patrickgold.florisboard.ime.text.keyboard.BasicTextKeyData
import dev.patrickgold.florisboard.ime.text.keyboard.MultiTextKeyData
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
import dev.patrickgold.florisboard.ime.text.composing.*
import dev.patrickgold.florisboard.ime.text.keyboard.*
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@@ -55,6 +53,12 @@ class AssetManager private constructor(val applicationContext: Context) {
subclass(MultiTextKeyData::class, MultiTextKeyData.serializer())
default { BasicTextKeyData.serializer() }
}
polymorphic(Composer::class) {
subclass(Appender::class, Appender.serializer())
subclass(HangulUnicode::class, HangulUnicode.serializer())
subclass(WithRules::class, WithRules.serializer())
default { Appender.serializer() }
}
}
}

View File

@@ -45,6 +45,7 @@ abstract class KeyboardView : View, ThemeManager.OnThemeUpdatedListener {
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
init {
layoutDirection = LAYOUT_DIRECTION_LTR
mainScope.launch {
for (event in touchEventChannel) {
if (!isActive) break

View File

@@ -28,7 +28,8 @@ import androidx.recyclerview.widget.RecyclerView
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.Preferences
import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
import dev.patrickgold.florisboard.ime.text.key.HINTS_DISABLED
import dev.patrickgold.florisboard.ime.text.key.KeyHintConfiguration
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import kotlinx.coroutines.CoroutineScope
@@ -39,8 +40,7 @@ import kotlinx.coroutines.MainScope
*
* @property florisboard Reference to instance of core class [FlorisBoard].
* @property emojiKeyboardView Reference to the parent [EmojiKeyboardView].
* @property data The data the current key represents. Is used to determine rendering and possible
* behaviour when events occur.
* @property key The current key. Is used to determine rendering and possible behaviour when events occur.
*/
@SuppressLint("ViewConstructor")
class EmojiKeyView(
@@ -104,8 +104,8 @@ class EmojiKeyView(
(parent as RecyclerView)
.requestDisallowInterceptTouchEvent(true)
emojiKeyboardView.isScrollBlocked = true
emojiKeyboardView.popupManager.show(key, KeyHintMode.DISABLED)
emojiKeyboardView.popupManager.extend(key, KeyHintMode.DISABLED)
emojiKeyboardView.popupManager.show(key, HINTS_DISABLED)
emojiKeyboardView.popupManager.extend(key, HINTS_DISABLED)
florisboard?.keyPressVibrate()
florisboard?.keyPressSound()
}, delayMillis.toLong())
@@ -174,7 +174,7 @@ class EmojiKeyView(
canvas ?: return
if (key.computedPopups.isNotEmpty()) {
if (key.computedPopups.getPopupKeys(HINTS_DISABLED).isNotEmpty()) {
triangleDrawable?.draw(canvas)
}
}

View File

@@ -1,294 +0,0 @@
/*
* 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.nlp
/**
* Represents the root node to a n-gram tree.
*/
open class NgramTree(
sameOrderChildren: MutableList<NgramNode> = mutableListOf(),
higherOrderChildren: MutableList<NgramNode> = mutableListOf()
) : NgramNode(0, '?', -1, sameOrderChildren, higherOrderChildren)
/**
* A node of a n-gram tree, which holds the character it represents, the corresponding frequency,
* a pre-computed string representing all parent characters and the current one as well as child
* nodes, one for the same order n-gram nodes and one for the higher order n-gram nodes.
*/
open class NgramNode(
val order: Int,
val char: Char,
val freq: Int,
val sameOrderChildren: MutableList<NgramNode> = mutableListOf(),
val higherOrderChildren: MutableList<NgramNode> = mutableListOf()
) {
companion object {
const val FREQ_CHARACTER = -1
const val FREQ_WORD_MIN = 0
const val FREQ_WORD_MAX = 255
const val FREQ_WORD_FILLER = -2
const val FREQ_IS_POSSIBLY_OFFENSIVE = 0
}
val isCharacter: Boolean
get() = freq == FREQ_CHARACTER
val isWord: Boolean
get() = freq in FREQ_WORD_MIN..FREQ_WORD_MAX
val isWordFiller: Boolean
get() = freq == FREQ_WORD_FILLER
val isPossiblyOffensive: Boolean
get() = freq == FREQ_IS_POSSIBLY_OFFENSIVE
fun findWord(word: String): NgramNode? {
var currentNode = this
for ((pos, char) in word.withIndex()) {
val childNode = if (pos == 0) {
currentNode.higherOrderChildren.find { it.char == char }
} else {
currentNode.sameOrderChildren.find { it.char == char }
}
if (childNode != null) {
currentNode = childNode
} else {
return null
}
}
return if (currentNode.isWord || currentNode.isWordFiller) {
currentNode
} else {
null
}
}
/**
* This function allows to search for a given [input] word with a given [maxEditDistance] and
* adds all matches in the trie to the [list].
*/
fun listSimilarWords(
input: String,
list: StagedSuggestionList<String, Int>,
word: StringBuilder,
allowPossiblyOffensive: Boolean,
maxEditDistance: Int,
deletionCost: Int = 0,
insertionCost: Int = 0,
substitutionCost: Int = 0,
pos: Int = -1
) {
if (pos > -1) {
word.append(char)
}
val costSum = deletionCost + insertionCost + substitutionCost
if (pos > -1 && (pos + 1 == input.length) && isWord && ((isPossiblyOffensive && allowPossiblyOffensive)
|| !isPossiblyOffensive)) {
// Using shift right instead of divide by 2^(costSum) as it is mathematically the
// same but faster.
if (list.canAdd(freq shr costSum)) {
list.add(word.toString(), freq shr costSum)
}
}
if (pos <= -1) {
for (childNode in higherOrderChildren) {
childNode.listSimilarWords(
input, list, word, allowPossiblyOffensive, maxEditDistance, 0, 0, 0, 0
)
}
} else if (maxEditDistance == costSum) {
if (pos + 1 < input.length) {
sameOrderChildren.find { it.char == input[pos + 1] }?.listSimilarWords(
input, list, word, allowPossiblyOffensive, maxEditDistance,
deletionCost, insertionCost, substitutionCost, pos + 1
)
}
} else {
// Delete
if (pos + 2 < input.length) {
sameOrderChildren.find { it.char == input[pos + 2] }?.listSimilarWords(
input, list, word, allowPossiblyOffensive, maxEditDistance,
deletionCost + 1, insertionCost, substitutionCost, pos + 2
)
}
for (childNode in sameOrderChildren) {
if (pos + 1 < input.length && childNode.char == input[pos + 1]) {
childNode.listSimilarWords(
input, list, word, allowPossiblyOffensive, maxEditDistance,
deletionCost, insertionCost, substitutionCost, pos + 1
)
} else {
// Insert
childNode.listSimilarWords(
input, list, word, allowPossiblyOffensive, maxEditDistance,
deletionCost, insertionCost + 1, substitutionCost, pos
)
if (pos + 1 < input.length) {
// Substitute
childNode.listSimilarWords(
input, list, word, allowPossiblyOffensive, maxEditDistance,
deletionCost, insertionCost, substitutionCost + 1, pos + 1
)
}
}
}
}
if (pos > -1) {
word.deleteAt(word.lastIndex)
}
}
fun listAllSameOrderWords(list: StagedSuggestionList<String, Int>, word: StringBuilder, allowPossiblyOffensive: Boolean) {
word.append(char)
if (isWord && ((isPossiblyOffensive && allowPossiblyOffensive) || !isPossiblyOffensive)) {
if (list.canAdd(freq)) {
list.add(word.toString(), freq)
}
}
for (childNode in sameOrderChildren) {
childNode.listAllSameOrderWords(list, word, allowPossiblyOffensive)
}
word.deleteAt(word.lastIndex)
}
}
open class FlorisLanguageModel(
initTreeObj: NgramTree? = null
) : LanguageModel<String, Int> {
protected val ngramTree: NgramTree = initTreeObj ?: NgramTree()
override fun getNgram(vararg tokens: String): Ngram<String, Int> {
val ngramOut = getNgramOrNull(*tokens)
if (ngramOut != null) {
return ngramOut
} else {
throw NullPointerException("No n-gram found matching the given tokens: $tokens")
}
}
override fun getNgram(ngram: Ngram<String, Int>): Ngram<String, Int> {
val ngramOut = getNgramOrNull(ngram)
if (ngramOut != null) {
return ngramOut
} else {
throw NullPointerException("No n-gram found matching the given ngram: $ngram")
}
}
override fun getNgramOrNull(vararg tokens: String): Ngram<String, Int>? {
var currentNode: NgramNode = ngramTree
for (token in tokens) {
val childNode = currentNode.findWord(token)
if (childNode != null) {
currentNode = childNode
} else {
return null
}
}
return Ngram(tokens.toList().map { Token(it) }, currentNode.freq)
}
override fun getNgramOrNull(ngram: Ngram<String, Int>): Ngram<String, Int>? {
return getNgramOrNull(*ngram.tokens.toStringList().toTypedArray())
}
override fun hasNgram(ngram: Ngram<String, Int>, doMatchFreq: Boolean): Boolean {
val result = getNgramOrNull(ngram)
return if (result != null) {
if (doMatchFreq) {
ngram.freq == result.freq
} else {
true
}
} else {
false
}
}
override fun matchAllNgrams(
ngram: Ngram<String, Int>,
maxEditDistance: Int,
maxTokenCount: Int,
allowPossiblyOffensive: Boolean
): List<WeightedToken<String, Int>> {
val ngramList = mutableListOf<WeightedToken<String, Int>>()
var currentNode: NgramNode = ngramTree
for ((t, token) in ngram.tokens.withIndex()) {
val word = token.data
if (t + 1 >= ngram.tokens.size) {
if (word.isNotEmpty()) {
// The last word is not complete, so find all possible words and sort
val splitWord = mutableListOf<Char>()
var splitNode: NgramNode? = currentNode
for ((pos, char) in word.withIndex()) {
val node = if (pos == 0) {
splitNode?.higherOrderChildren?.find { it.char == char }
} else {
splitNode?.sameOrderChildren?.find { it.char == char }
}
splitWord.add(char)
splitNode = node
if (node == null) {
break
}
}
if (splitNode != null) {
// Input thus far is valid
val wordNodes = StagedSuggestionList<String, Int>(maxTokenCount)
val strBuilder = StringBuilder().append(word.substring(0, word.length - 1))
splitNode.listAllSameOrderWords(wordNodes, strBuilder, allowPossiblyOffensive)
ngramList.addAll(wordNodes)
}
if (ngramList.size < maxTokenCount) {
val wordNodes = StagedSuggestionList<String, Int>(maxTokenCount)
val strBuilder = StringBuilder()
currentNode.listSimilarWords(word, wordNodes, strBuilder, allowPossiblyOffensive, maxEditDistance)
ngramList.addAll(wordNodes)
}
}
} else {
val node = currentNode.findWord(word)
if (node == null) {
return ngramList
} else {
currentNode = node
}
}
}
return ngramList
}
fun toFlorisMutableLanguageModel(): FlorisMutableLanguageModel = FlorisMutableLanguageModel(ngramTree)
}
open class FlorisMutableLanguageModel(
initTreeObj: NgramTree? = null
) : MutableLanguageModel<String, Int>, FlorisLanguageModel(initTreeObj) {
override fun deleteNgram(ngram: Ngram<String, Int>) {
TODO("Not yet implemented")
}
override fun insertNgram(ngram: Ngram<String, Int>) {
TODO("Not yet implemented")
}
override fun updateNgram(ngram: Ngram<String, Int>) {
TODO("Not yet implemented")
}
fun toFlorisLanguageModel(): FlorisLanguageModel = FlorisLanguageModel(ngramTree)
}

View File

@@ -1,74 +0,0 @@
/*
* 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.nlp
/**
* Abstract interface for a language model. Can house any n-grams with a minimum order of one.
*/
interface LanguageModel<T : Any, F : Comparable<F>> {
/**
* Tries to get the n-gram for the passed [tokens]. Throws a NPE if no match could be found.
*/
@Throws(NullPointerException::class)
fun getNgram(vararg tokens: T): Ngram<T, F>
/**
* Tries to get the n-gram for the passed [ngram], whereas the frequency is ignored while
* searching. Throws a NPE if no match could be found.
*/
@Throws(NullPointerException::class)
fun getNgram(ngram: Ngram<T, F>): Ngram<T, F>
/**
* Tries to get the n-gram for the passed [tokens]. Returns null if no match could be found.
*/
fun getNgramOrNull(vararg tokens: T): Ngram<T, F>?
/**
* Tries to get the n-gram for the passed [ngram], whereas the frequency is ignored while
* searching. Returns null if no match could be found.
*/
fun getNgramOrNull(ngram: Ngram<T, F>): Ngram<T, F>?
/**
* Checks if a given [ngram] exists within this model. If [doMatchFreq] is set to true, the
* frequency is also matched.
*/
fun hasNgram(ngram: Ngram<T, F>, doMatchFreq: Boolean = false): Boolean
/**
* Matches all n-grams which match the given [ngram], whereas the last item in the n-gram is
* is used to search for predictions.
*/
fun matchAllNgrams(
ngram: Ngram<T, F>,
maxEditDistance: Int,
maxTokenCount: Int,
allowPossiblyOffensive: Boolean
): List<WeightedToken<T, F>>
}
/**
* Mutable version of [LanguageModel].
*/
interface MutableLanguageModel<T : Any, F : Comparable<F>> : LanguageModel<T, F> {
fun deleteNgram(ngram: Ngram<T, F>)
fun insertNgram(ngram: Ngram<T, F>)
fun updateNgram(ngram: Ngram<T, F>)
}

View File

@@ -1,129 +0,0 @@
/*
* 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.nlp
/**
* Abstract interface representing a n-gram of tokens. Each n-gram instance can be assigned a
* unique frequency [freq].
*/
open class Ngram<T : Any, F : Comparable<F>>(_tokens: List<Token<T>>, _freq: F) {
companion object {
/** Constant order value for unigrams. */
const val ORDER_UNIGRAM: Int = 1
/** Constant order value for bigrams. */
const val ORDER_BIGRAM: Int = 2
/** Constant order value for trigrams. */
const val ORDER_TRIGRAM: Int = 3
}
init {
if (_tokens.size < ORDER_UNIGRAM) {
throw Exception("A n-gram must contain at least 1 token!")
}
}
/**
* A list of tokens for this n-gram. The length of this list is guaranteed to be matching
* [order].
*/
val tokens: List<Token<T>> = _tokens
/**
* The frequency value of this n-gram.
*/
val freq: F = _freq
/**
* The order of this n-gram (1, 2, 3, ...).
*/
val order: Int
get() = tokens.size
}
/**
* Abstract interface representing a token used in [Ngram].
*/
open class Token<T : Any>(_data: T) {
/**
* The data of this token.
*/
val data: T = _data
override fun toString(): String {
return "Token(\"$data\")"
}
override fun hashCode(): Int {
return data.hashCode()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Token<*>
if (data != other.data) return false
return true
}
}
/**
* Same as [Token] but allows to add a frequency value [freq].
*/
open class WeightedToken<T : Any, F : Comparable<F>>(_data: T, _freq: F) : Token<T>(_data) {
/**
* The frequency of this weighed token.
*/
val freq: F = _freq
override fun toString(): String {
return "WeightedToken(\"$data\", $freq)"
}
override fun hashCode(): Int {
return data.hashCode() + 31 * freq.hashCode()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as WeightedToken<*, *>
if (data != other.data || freq != other.freq) return false
return true
}
}
/**
* Converts a list of tokens carrying [CharSequence] data to a list of [CharSequence].
*/
fun List<Token<CharSequence>>.toCharSequenceList(): List<CharSequence> {
return this.map { it.data }
}
/**
* Converts a list of tokens carrying [String] data to a list of [String].
*/
fun List<Token<String>>.toStringList(): List<String> {
return this.map { it.data }
}

View File

@@ -1,90 +0,0 @@
/*
* 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.nlp
class StagedSuggestionList<T : Any, F : Comparable<F>>(
private val maxSize: Int
) : Collection<WeightedToken<T, F>> {
private val internalArray: Array<WeightedToken<T, F>?> = Array(maxSize) { null }
private var internalSize: Int = 0
override val size: Int
get() = internalSize
fun add(token: T, freq: F): Boolean {
if (internalSize < maxSize) {
internalArray[internalSize++] = WeightedToken(token, freq)
internalArray.sortByDescending { it?.freq }
return true
} else {
if (internalArray.last()!!.freq < freq) {
internalArray[internalArray.lastIndex] = WeightedToken(token, freq)
internalArray.sortByDescending { it?.freq }
return true
}
return false
}
}
fun canAdd(freq: F): Boolean {
return internalSize < maxSize || internalArray.last()!!.freq < freq
}
fun clear() {
for (n in internalArray.indices) {
internalArray[n] = null
}
internalSize = 0
}
override fun contains(element: WeightedToken<T, F>): Boolean = internalArray.contains(element)
override fun containsAll(elements: Collection<WeightedToken<T, F>>): Boolean {
elements.forEach { if (!contains(it)) return false }
return true
}
@Throws(IndexOutOfBoundsException::class)
operator fun get(index: Int): WeightedToken<T, F> {
val element = getOrNull(index)
if (element == null) {
throw IndexOutOfBoundsException("The specified index $index is not within the bounds of this list!")
} else {
return element
}
}
fun getOrNull(index: Int): WeightedToken<T, F>? {
return internalArray.getOrNull(index)
}
override fun isEmpty(): Boolean = internalSize <= 0
override fun iterator(): Iterator<WeightedToken<T, F>> {
return StagedIterator(this)
}
class StagedIterator<T : Any, F : Comparable<F>> internal constructor (
private val stagedSuggestionList: StagedSuggestionList<T, F>
) : Iterator<WeightedToken<T, F>> {
var index = 0
override fun next(): WeightedToken<T, F> = stagedSuggestionList[index++]
override fun hasNext(): Boolean = stagedSuggestionList.getOrNull(index) != null
}
}

View File

@@ -0,0 +1,104 @@
/*
* 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.nlp
import dev.patrickgold.florisboard.ime.core.NativeInstanceWrapper
import dev.patrickgold.florisboard.ime.core.NativePtr
@JvmInline
value class SuggestionList private constructor(
private val _nativePtr: NativePtr
) : Collection<String>, NativeInstanceWrapper {
companion object {
fun new(maxSize: Int): SuggestionList {
val nativePtr = nativeInitialize(maxSize)
return SuggestionList(nativePtr)
}
external fun nativeInitialize(maxSize: Int): NativePtr
external fun nativeDispose(nativePtr: NativePtr)
external fun nativeAdd(nativePtr: NativePtr, word: Word, freq: Freq): Boolean
external fun nativeClear(nativePtr: NativePtr)
external fun nativeContains(nativePtr: NativePtr, element: Word): Boolean
external fun nativeGetOrNull(nativePtr: NativePtr, index: Int): Word?
external fun nativeGetIsPrimaryTokenAutoInsert(nativePtr: NativePtr): Boolean
external fun nativeSetIsPrimaryTokenAutoInsert(nativePtr: NativePtr, v: Boolean)
external fun nativeSize(nativePtr: NativePtr): Int
}
override val size: Int
get() = nativeSize(_nativePtr)
fun add(word: Word, freq: Freq): Boolean {
return nativeAdd(_nativePtr, word, freq)
}
fun clear() {
nativeClear(_nativePtr)
}
override fun contains(element: Word): Boolean {
return nativeContains(_nativePtr, element)
}
override fun containsAll(elements: Collection<Word>): Boolean {
elements.forEach { if (!contains(it)) return false }
return true
}
@Throws(IndexOutOfBoundsException::class)
operator fun get(index: Int): Word {
val element = getOrNull(index)
if (element == null) {
throw IndexOutOfBoundsException("The specified index $index is not within the bounds of this list!")
} else {
return element
}
}
fun getOrNull(index: Int): Word? {
return nativeGetOrNull(_nativePtr, index)
}
override fun isEmpty(): Boolean = size <= 0
val isPrimaryTokenAutoInsert: Boolean
get() = nativeGetIsPrimaryTokenAutoInsert(_nativePtr)
override fun iterator(): Iterator<Word> {
return SuggestionListIterator(this)
}
override fun nativePtr(): NativePtr {
return _nativePtr
}
override fun dispose() {
nativeDispose(_nativePtr)
}
class SuggestionListIterator internal constructor (
private val suggestionList: SuggestionList
) : Iterator<Word> {
var index = 0
override fun next(): Word = suggestionList[index++]
override fun hasNext(): Boolean = suggestionList.getOrNull(index) != null
}
}

View File

@@ -16,9 +16,7 @@
package dev.patrickgold.florisboard.ime.nlp
class TextProcessor {
data class Word(
val word: String,
val isPossiblyOffensive: Boolean = false
)
}
typealias Word = String
typealias Freq = Int
const val NATIVE_NULLPTR = 0

View File

@@ -18,6 +18,7 @@ package dev.patrickgold.florisboard.ime.onehanded
import android.content.Context
import android.content.res.ColorStateList
import android.content.res.Resources
import android.util.AttributeSet
import android.view.Gravity
import android.widget.ImageButton
@@ -96,8 +97,7 @@ class OneHandedPanel : LinearLayout, ThemeManager.OnThemeUpdatedListener {
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val florisboard = florisboard ?: return super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val width = (florisboard.inputView?.measuredWidth ?: 0) *
val width = (Resources.getSystem().displayMetrics.widthPixels) *
((100 - prefs.keyboard.oneHandedModeScaleFactor) / 100.0f)
super.onMeasure(MeasureSpec.makeMeasureSpec(width.toInt(), MeasureSpec.EXACTLY), heightMeasureSpec)
}

View File

@@ -82,16 +82,19 @@ class PopupManager<V : View>(
* Helper function to create a element for the extended popup and preconfigure it.
*
* @param key Reference to the key currently controlling the popup.
* @param keyHintConfiguration The key hint configuration to use.
* @param adjustedIndex The index of the key in the key data popup array.
* @return A preconfigured extended popup element.
*/
private fun createElement(
key: Key,
keyHintConfiguration: KeyHintConfiguration,
adjustedIndex: Int
): PopupExtendedView.Element {
return when (key) {
is TextKey -> {
when (key.computedPopups[adjustedIndex].code) {
val popupKey = key.computedPopups.getPopupKeys(keyHintConfiguration)[adjustedIndex]
when (popupKey.code) {
KeyCode.SETTINGS -> {
getDrawable(keyboardView.context, R.drawable.ic_settings)?.let {
PopupExtendedView.Element.Icon(it, adjustedIndex)
@@ -114,7 +117,7 @@ class PopupManager<V : View>(
}
KeyCode.URI_COMPONENT_TLD -> {
PopupExtendedView.Element.Tld(
key.computedPopups[adjustedIndex].asString(isForDisplay = true), adjustedIndex
popupKey.asString(isForDisplay = true), adjustedIndex
)
}
KeyCode.TOGGLE_ONE_HANDED_MODE_LEFT,
@@ -125,14 +128,15 @@ class PopupManager<V : View>(
}
else -> {
PopupExtendedView.Element.Label(
key.computedPopups[adjustedIndex].asString(isForDisplay = true), adjustedIndex
popupKey.asString(isForDisplay = true), adjustedIndex
)
}
}
}
is EmojiKey -> {
val popupKey = key.computedPopups.getPopupKeys(keyHintConfiguration)[adjustedIndex]
PopupExtendedView.Element.Label(
key.computedPopups[adjustedIndex].asString(isForDisplay = true), adjustedIndex
popupKey.asString(isForDisplay = true), adjustedIndex
)
}
else -> {
@@ -141,6 +145,14 @@ class PopupManager<V : View>(
}
}
fun isSuitableForPopups(key: Key): Boolean {
return if (key is TextKey) {
key.computedData.code > KeyCode.SPACE && key.computedData.code != KeyCode.MULTIPLE_CODE_POINTS
} else {
true
}
}
/**
* Calculates all attributes required by both the normal and the extended popup, regardless of
* the passed [key]'s code.
@@ -179,11 +191,10 @@ class PopupManager<V : View>(
* key code is equal to or less than [KeyCode.SPACE].
*
* @param key Reference to the key currently controlling the popup.
* @param keyHintConfiguration The key hint configuration to use.
*/
fun show(key: Key, keyHintMode: KeyHintMode) {
if (key is TextKey && key.computedData.code <= KeyCode.SPACE && key.computedData.code != KeyCode.MULTIPLE_CODE_POINTS) {
return
}
fun show(key: Key, keyHintConfiguration: KeyHintConfiguration) {
if (!isSuitableForPopups(key)) return
calc(key)
@@ -200,8 +211,8 @@ class PopupManager<V : View>(
}
labelTextSize = keyPopupTextSize
shouldIndicateExtendedPopups = when (key) {
is TextKey -> key.computedPopups.size(keyHintMode) > 0
is EmojiKey -> key.computedPopups.isNotEmpty()
is TextKey -> key.computedPopups.getPopupKeys(keyHintConfiguration).isNotEmpty()
is EmojiKey -> key.computedPopups.getPopupKeys(keyHintConfiguration).isNotEmpty()
else -> false
}
}
@@ -228,9 +239,10 @@ class PopupManager<V : View>(
* K K ... K K
*
* @param key Reference to the key currently controlling the popup.
* @param keyHintConfiguration The key hint configuration to use.
*/
fun extend(key: Key, keyHintMode: KeyHintMode) {
if (key is TextKey && key.computedData.code <= KeyCode.SPACE && key.computedData.code != KeyCode.MULTIPLE_CODE_POINTS
fun extend(key: Key, keyHintConfiguration: KeyHintConfiguration) {
if (key is TextKey && !isSuitableForPopups(key)
&& !exceptionsForKeyCodes.contains(key.computedData.code)) {
return
}
@@ -245,8 +257,8 @@ class PopupManager<V : View>(
// Determine key counts for each row
val n = when (key) {
is TextKey -> key.computedPopups.size(keyHintMode)
is EmojiKey -> key.computedPopups.size(keyHintMode)
is TextKey -> key.computedPopups.getPopupKeys(keyHintConfiguration).size
is EmojiKey -> key.computedPopups.getPopupKeys(keyHintConfiguration).size
else -> 0
}
when {
@@ -303,43 +315,38 @@ class PopupManager<V : View>(
val uiIndices = IntRange(0, (n - 1).coerceAtLeast(0))
if (key is TextKey) {
popupIndices = IntArray(n) { 0 }
when (keyHintMode) {
KeyHintMode.ENABLED_ACCENT_PRIORITY -> when {
key.computedPopups.main != null -> {
popupIndices[initUiIndex] = PopupSet.MAIN_INDEX
if (key.computedPopups.hint != null) when {
initUiIndex + 1 < n -> popupIndices[initUiIndex + 1] = PopupSet.HINT_INDEX
initUiIndex - 1 >= 0 -> popupIndices[initUiIndex - 1] = PopupSet.HINT_INDEX
}
}
key.computedPopups.hint != null -> when {
initUiIndex + 1 < n -> popupIndices[initUiIndex + 1] = PopupSet.HINT_INDEX
initUiIndex - 1 >= 0 -> popupIndices[initUiIndex - 1] = PopupSet.HINT_INDEX
else -> popupIndices[initUiIndex] = PopupSet.HINT_INDEX
val popupKeys = key.computedPopups.getPopupKeys(keyHintConfiguration)
when (popupKeys.prioritized.size) {
// only one key: use initial position
1 -> {
popupIndices[initUiIndex] = PopupKeys.FIRST_PRIORITIZED
}
// two keys: use initial position and one to the right if available, otherwise one to the left
2 -> {
popupIndices[initUiIndex] = PopupKeys.FIRST_PRIORITIZED
when {
initUiIndex + 1 < n -> popupIndices[initUiIndex + 1] = PopupKeys.SECOND_PRIORITIZED
initUiIndex - 1 >= 0 -> popupIndices[initUiIndex - 1] = PopupKeys.SECOND_PRIORITIZED
}
}
KeyHintMode.ENABLED_HINT_PRIORITY -> when {
key.computedPopups.hint != null -> {
popupIndices[initUiIndex] = PopupSet.HINT_INDEX
if (key.computedPopups.main != null) when {
initUiIndex + 1 < n -> popupIndices[initUiIndex + 1] = PopupSet.MAIN_INDEX
initUiIndex - 1 >= 0 -> popupIndices[initUiIndex - 1] = PopupSet.MAIN_INDEX
// two keys: use initial position and one to either sides if available
// otherwise two to the right or two to the left with decreasing priority
3 -> {
popupIndices[initUiIndex] = PopupKeys.FIRST_PRIORITIZED
when {
initUiIndex + 1 < n && initUiIndex - 1 >= 0 -> {
popupIndices[initUiIndex + 1] = PopupKeys.SECOND_PRIORITIZED
popupIndices[initUiIndex - 1] = PopupKeys.THIRD_PRIORITIZED
}
initUiIndex + 2 < n -> {
popupIndices[initUiIndex + 1] = PopupKeys.SECOND_PRIORITIZED
popupIndices[initUiIndex + 2] = PopupKeys.THIRD_PRIORITIZED
}
initUiIndex - 2 >= 0 -> {
popupIndices[initUiIndex - 1] = PopupKeys.SECOND_PRIORITIZED
popupIndices[initUiIndex - 2] = PopupKeys.THIRD_PRIORITIZED
}
}
key.computedPopups.main != null -> popupIndices[initUiIndex] = PopupSet.MAIN_INDEX
}
KeyHintMode.ENABLED_SMART_PRIORITY -> when {
key.computedPopups.main != null -> {
popupIndices[initUiIndex] = PopupSet.MAIN_INDEX
if (key.computedPopups.hint != null) when {
initUiIndex + 1 < n -> popupIndices[initUiIndex + 1] = PopupSet.HINT_INDEX
initUiIndex - 1 >= 0 -> popupIndices[initUiIndex - 1] = PopupSet.HINT_INDEX
}
}
key.computedPopups.hint != null -> popupIndices[initUiIndex] = PopupSet.HINT_INDEX
}
KeyHintMode.DISABLED -> when {
key.computedPopups.main != null -> popupIndices[initUiIndex] = PopupSet.MAIN_INDEX
}
}
var offset = 0
@@ -360,7 +367,7 @@ class PopupManager<V : View>(
for (uiIndex in uiIndices) {
val rowIndex = if (uiIndex < row1count && row1count > 0) { 1 } else { 0 }
popupViewExt.properties.elements[rowIndex].add(
createElement(key, popupIndices[uiIndex])
createElement(key, keyHintConfiguration, popupIndices[uiIndex])
)
}
@@ -468,17 +475,18 @@ class PopupManager<V : View>(
}
/**
* Gets the [KeyData] of the currently active key. May be either the key of the popup preview
* Gets the [TextKeyData] of the currently active key. May be either the key of the popup preview
* or one of the keys in extended popup, if shown. Returns null if [key] is not a subclass of [TextKey].
*
* @param key Reference to the key currently controlling the popup.
* @return The [KeyData] object of the currently active key or null.
* @param keyHintConfiguration The key hint configuration to be used.
* @return The [TextKeyData] object of the currently active key or null.
*/
fun getActiveKeyData(key: Key): TextKeyData? {
fun getActiveKeyData(key: Key, keyHintConfiguration: KeyHintConfiguration): TextKeyData? {
return if (key is TextKey) {
val element = popupViewExt.properties.getElementOrNull()
if (element != null) {
key.computedPopups.getOrNull(element.adjustedIndex) ?: key.computedData
key.computedPopups.getPopupKeys(keyHintConfiguration).getOrNull(element.adjustedIndex) ?: key.computedData
} else {
key.computedData
}
@@ -498,7 +506,7 @@ class PopupManager<V : View>(
return if (key is EmojiKey) {
val element = popupViewExt.properties.getElementOrNull()
if (element != null) {
key.computedPopups.getOrNull(element.adjustedIndex) ?: key.computedData
key.computedPopups.getPopupKeys(HINTS_DISABLED).getOrNull(element.adjustedIndex) ?: key.computedData
} else {
key.computedData
}

View File

@@ -17,43 +17,215 @@
package dev.patrickgold.florisboard.ime.popup
import dev.patrickgold.florisboard.ime.keyboard.KeyData
import dev.patrickgold.florisboard.ime.text.key.KeyHintConfiguration
import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
import dev.patrickgold.florisboard.ime.text.keyboard.BasicTextKeyData
import dev.patrickgold.florisboard.ime.text.keyboard.TextComputingEvaluator
import kotlinx.serialization.Serializable
/**
* A popup set for a single key. This set describes, if the key has a [hint] character,
* a [main] character and other [relevant] popups.
* A popup set for a single key. This set describes, if the key has a [main] character and other [relevant] popups.
*
* Note, that a hint character should **never** be set in a json extended popup file, rather it
* Note that a hint character cannot and should not be set in a json extended popup file, rather it
* should only be dynamically set by the LayoutManager.
*
* The order in which these defined popups will be shown depends on the current [KeyHintMode],
* al well as the calculations made by the KeyPopupManager.
*
* The popup set can be accessed like an array with the addition that negative indexes defined
* within this companion object are allowed (as long as the corresponding [hint] or [main]
* character is *not* null).
* The order in which these defined popups will be shown depends on the current [KeyHintConfiguration].
*/
@Serializable
open class PopupSet<T : KeyData>(
open val hint: T? = null,
open val main: T? = null,
open val relevant: List<T> = listOf()
) {
private val popupKeys: PopupKeys<T> by lazy {
PopupKeys(null, listOfNotNull(main), relevant)
}
open fun getPopupKeys(keyHintConfiguration: KeyHintConfiguration): PopupKeys<T> {
return popupKeys
}
}
@Suppress("UNCHECKED_CAST")
class MutablePopupSet<T : KeyData>(
override var main: T? = null,
override val relevant: ArrayList<T> = arrayListOf(),
var symbolHint: T? = null,
var numberHint: T? = null,
private val symbolPopups: ArrayList<T> = arrayListOf(),
private val numberPopups: ArrayList<T> = arrayListOf(),
private val configCache: MutableMap<KeyHintConfiguration, PopupKeys<T>> = mutableMapOf()
) : PopupSet<T>(main, relevant) {
fun clear() {
symbolHint = null
numberHint = null
main = null
relevant.clear()
symbolPopups.clear()
numberPopups.clear()
configCache.clear()
}
override fun getPopupKeys(keyHintConfiguration: KeyHintConfiguration): PopupKeys<T> {
return configCache.getOrPut(keyHintConfiguration) {
initPopupList(keyHintConfiguration)
}
}
private fun initPopupList(keyHintConfiguration: KeyHintConfiguration): PopupKeys<T> {
val localMain = main
val localRelevant = relevant
val localSymbolHint = symbolHint
val localNumberHint = numberHint
if (localSymbolHint != null && keyHintConfiguration.symbolHintMode != KeyHintMode.DISABLED) {
if (localNumberHint != null && keyHintConfiguration.numberHintMode != KeyHintMode.DISABLED) {
val hintPopups = if (keyHintConfiguration.mergeHintPopups) { symbolPopups + numberPopups } else { listOf() }
return when (keyHintConfiguration.symbolHintMode) {
KeyHintMode.ENABLED_ACCENT_PRIORITY -> when (keyHintConfiguration.numberHintMode) {
// when both hints are present in accent priority, always have a non-hint key first if possible
KeyHintMode.ENABLED_ACCENT_PRIORITY -> when {
localMain != null -> PopupKeys(localSymbolHint, listOf(localMain, localSymbolHint, localNumberHint), localRelevant + hintPopups)
localRelevant.isNotEmpty() -> PopupKeys(localSymbolHint, listOf(localRelevant[0], localSymbolHint, localNumberHint), localRelevant.subList(1, localRelevant.size) + hintPopups)
else -> PopupKeys(localSymbolHint, listOf(localSymbolHint, localNumberHint), hintPopups)
}
// hint priority of number hint wins and overrules accent priority of symbol hint
KeyHintMode.ENABLED_HINT_PRIORITY -> PopupKeys(localSymbolHint, listOfNotNull(localNumberHint, localMain, localSymbolHint), localRelevant + hintPopups)
// due to smart priority of number hint, main wins if it exists, otherwise number hint overrules accent priority of symbol hint
else -> PopupKeys(localSymbolHint, listOfNotNull(localMain, localNumberHint, localSymbolHint), localRelevant + hintPopups)
}
KeyHintMode.ENABLED_HINT_PRIORITY -> when (keyHintConfiguration.symbolHintMode) {
// when both hints are present in hint priority, symbol hint wins
KeyHintMode.ENABLED_HINT_PRIORITY -> PopupKeys(localSymbolHint, listOfNotNull(localSymbolHint, localNumberHint, localMain), localRelevant + hintPopups)
// hint priority of symbol hint wins, and overrules potential accent priority of number hint
else -> PopupKeys(localSymbolHint, listOfNotNull(localSymbolHint, localMain, localNumberHint), localRelevant + hintPopups)
}
else -> when (keyHintConfiguration.numberHintMode) {
// smart priority of symbol hint wins, and overrules accent priority of number hint
KeyHintMode.ENABLED_ACCENT_PRIORITY -> PopupKeys(localSymbolHint, listOfNotNull(localMain, localSymbolHint, localNumberHint), localRelevant + hintPopups)
// hint priority of number hint wins
KeyHintMode.ENABLED_HINT_PRIORITY -> PopupKeys(localSymbolHint, listOfNotNull(localNumberHint, localMain, localSymbolHint), localRelevant + hintPopups)
// when both hints are in smart priority, always have main first if possible
else -> PopupKeys(localSymbolHint, listOfNotNull(localMain, localSymbolHint, localNumberHint), localRelevant + hintPopups)
}
}
} else {
val hintPopups = if (keyHintConfiguration.mergeHintPopups) { symbolPopups } else { listOf() }
return when (keyHintConfiguration.symbolHintMode) {
// in accent priority, always show a non-hint key first if possible
KeyHintMode.ENABLED_ACCENT_PRIORITY -> when {
localMain != null -> PopupKeys(localSymbolHint, listOf(localMain, localSymbolHint), localRelevant + hintPopups)
localRelevant.isNotEmpty() -> PopupKeys(localSymbolHint, listOf(localRelevant[0], localSymbolHint), localRelevant.subList(1, localRelevant.size) + hintPopups)
else -> PopupKeys(localSymbolHint, listOf(localSymbolHint), hintPopups)
}
// in hint priority, always show hint first
KeyHintMode.ENABLED_HINT_PRIORITY -> PopupKeys(localSymbolHint, listOfNotNull(localSymbolHint, localMain), localRelevant + hintPopups)
// in smart priority, show main first if possible
else -> PopupKeys(localSymbolHint, listOfNotNull(localMain, localSymbolHint), localRelevant + hintPopups)
}
}
} else if (localNumberHint != null && keyHintConfiguration.numberHintMode != KeyHintMode.DISABLED) {
val hintPopups = if (keyHintConfiguration.mergeHintPopups) { numberPopups } else { listOf() }
return when (keyHintConfiguration.numberHintMode) {
// in accent priority, always show a non-hint key first if possible
KeyHintMode.ENABLED_ACCENT_PRIORITY -> when {
localMain != null -> PopupKeys(localNumberHint, listOf(localMain, localNumberHint), localRelevant + hintPopups)
localRelevant.isNotEmpty() -> PopupKeys(localNumberHint, listOf(localRelevant[0], localNumberHint), localRelevant.subList(1, localRelevant.size) + hintPopups)
else -> PopupKeys(localNumberHint, listOf(localNumberHint), hintPopups)
}
// in hint priority, always show hint first
KeyHintMode.ENABLED_HINT_PRIORITY -> PopupKeys(localNumberHint, listOfNotNull(localNumberHint, localMain), localRelevant + hintPopups)
// in smart priority, show main first if possible
else -> PopupKeys(localNumberHint, listOfNotNull(localMain, localNumberHint), localRelevant + hintPopups)
}
} else {
// if no hints shall be shown, use main first if possible
return PopupKeys(null, listOfNotNull(localMain), localRelevant)
}
}
fun merge(other: PopupSet<T>, evaluator: TextComputingEvaluator) {
mergeInternal(other, evaluator, relevant, true)
}
fun mergeSymbolHint(hintPopups: PopupSet<T>, evaluator: TextComputingEvaluator) {
mergeInternal(hintPopups, evaluator, symbolPopups)
}
fun mergeNumberHint(hintPopups: PopupSet<T>, evaluator: TextComputingEvaluator) {
mergeInternal(hintPopups, evaluator, numberPopups)
}
private fun mergeInternal(other: PopupSet<T>, evaluator: TextComputingEvaluator, targetList: MutableList<T>, useMain: Boolean = false) {
other.relevant.forEach {
val data = it.computeTextKeyData(evaluator) as? T
if (data != null) {
targetList.add(data)
}
}
other.main?.let {
val data = it.computeTextKeyData(evaluator) as? T
if (data != null) {
if (useMain && main == null) {
main = data
} else {
targetList.add(data)
}
}
}
}
}
/**
* A fully configured collection of popup keys. It contains a list of keys to be prioritized
* during rendering (ordered by relevance descending) by showing those keys close to the
* popup spawning point.
*
* The keys contain a separate [hint] key to ease rendering the hint label, but the hint, if
* present, also occurs in the [prioritized] list.
*
* The popup keys can be accessed like an array with the addition that negative indexes defined
* within this companion object are allowed (as long as the corresponding [prioritized] list
* contains the corresponding amount of keys.
*/
class PopupKeys<T>(
val hint: T?,
val prioritized: List<T>,
val other: List<T>
) : Collection<T> {
companion object {
const val HINT_INDEX: Int = -2
const val MAIN_INDEX: Int = -1
const val FIRST_PRIORITIZED = -1
const val SECOND_PRIORITIZED = -2
const val THIRD_PRIORITIZED = -3
}
override val size: Int
get() = if (hint != null) { 1 } else { 0 } + if (main != null) { 1 } else { 0 } + relevant.size
get() = prioritized.size + other.size
fun size(keyHintMode: KeyHintMode): Int {
return if (keyHintMode == KeyHintMode.DISABLED && hint != null) {
size - 1
} else {
size
override fun contains(element: T): Boolean {
return prioritized.contains(element) || other.contains(element)
}
override fun containsAll(elements: Collection<T>): Boolean {
return (prioritized + other).containsAll(elements)
}
override fun isEmpty(): Boolean {
return prioritized.isEmpty() && other.isEmpty()
}
override fun iterator(): Iterator<T> {
return (prioritized + other).listIterator()
}
fun getOrNull(index: Int): T? {
if (index >= other.size || index < -prioritized.size) {
return null
}
return when (index) {
FIRST_PRIORITIZED -> prioritized[0]
SECOND_PRIORITIZED -> prioritized[1]
THIRD_PRIORITIZED -> prioritized[2]
else -> other.getOrNull(index)
}
}
@@ -61,125 +233,10 @@ open class PopupSet<T : KeyData>(
val item = getOrNull(index)
if (item == null) {
throw IndexOutOfBoundsException(
"Specified index $index is not an valid entry in this PopupSet!"
"Specified index $index is not an valid entry in this PopupKeys!"
)
} else {
return item
}
}
fun getOrNull(index: Int): T? {
if (index >= relevant.size || index < HINT_INDEX) {
return null
}
return when (index) {
HINT_INDEX -> hint
MAIN_INDEX -> main
else -> relevant.getOrNull(index)
}
}
override fun contains(element: T): Boolean {
return hint == element || main == element || relevant.contains(element)
}
override fun containsAll(elements: Collection<T>): Boolean {
for (element in elements) {
if (!contains(element)) {
return false
}
}
return true
}
override fun iterator(): Iterator<T> {
return PopupSetIterator(this)
}
override fun isEmpty(): Boolean {
return size == 0
}
class PopupSetIterator<T : KeyData> internal constructor (
private val popupSet: PopupSet<T>
) : Iterator<T> {
var index = HINT_INDEX
override fun next(): T = popupSet[index++]
override fun hasNext(): Boolean {
if (index == HINT_INDEX) {
if (popupSet.getOrNull(index) != null) {
return true
} else {
index++
}
}
if (index == MAIN_INDEX) {
if (popupSet.getOrNull(index) != null) {
return true
} else {
index++
}
}
return popupSet.getOrNull(index) != null
}
}
}
@Suppress("UNCHECKED_CAST")
class MutablePopupSet<T : KeyData>(
override var hint: T? = null,
override var main: T? = null,
override val relevant: ArrayList<T> = arrayListOf()
) : PopupSet<T>(hint, main, relevant) {
fun clear() {
hint = null
main = null
relevant.clear()
}
fun merge(other: PopupSet<T>) {
relevant.addAll(other.relevant)
other.hint?.let {
if (hint == null) {
hint = it
} else {
relevant.add(it)
}
}
other.main?.let {
if (main == null) {
main = it
} else {
relevant.add(it)
}
}
}
fun merge(other: PopupSet<T>, evaluator: TextComputingEvaluator) {
other.relevant.forEach {
val data = it.computeTextKeyData(evaluator) as? T
if (data != null) {
relevant.add(data)
}
}
other.hint?.let {
if (hint == null) {
hint = it
} else {
relevant.add(it)
}
}
other.main?.let {
val data = it.computeTextKeyData(evaluator) as? T
if (data != null) {
if (main == null) {
main = data
} else {
relevant.add(data)
}
}
}
}
}

View File

@@ -27,13 +27,10 @@ import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.debug.*
import dev.patrickgold.florisboard.ime.clip.provider.ClipboardItem
import dev.patrickgold.florisboard.ime.core.*
import dev.patrickgold.florisboard.ime.dictionary.Dictionary
import dev.patrickgold.florisboard.ime.dictionary.DictionaryManager
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.nlp.Token
import dev.patrickgold.florisboard.ime.nlp.toStringList
import dev.patrickgold.florisboard.ime.text.gestures.GlideTypingManager
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.key.*
@@ -42,7 +39,6 @@ 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
/**
@@ -74,8 +70,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
lateinit var textKeyboardIconSet: TextKeyboardIconSet
private set
private var textViewGroup: LinearLayout? = null
private val dictionaryManager: DictionaryManager = DictionaryManager.default()
private var activeDictionary: Dictionary<String, Int>? = null
private val dictionaryManager: DictionaryManager get() = DictionaryManager.default()
val inputEventDispatcher: InputEventDispatcher = InputEventDispatcher.new(
repeatableKeyCodes = intArrayOf(
KeyCode.ARROW_DOWN,
@@ -420,11 +415,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
override fun onSubtypeChanged(newSubtype: Subtype) {
launch {
if (activeEditorInstance.isComposingEnabled) {
withContext(Dispatchers.IO) {
dictionaryManager.loadDictionary(AssetRef(AssetSource.Assets, "ime/dict/en.flict")).let {
activeDictionary = it.getOrDefault(null)
}
}
dictionaryManager.prepareDictionaries(newSubtype)
}
if (prefs.glide.enabled) {
GlideTypingManager.getInstance().setWordData(newSubtype)
@@ -447,28 +438,24 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
if (activeEditorInstance.isComposingEnabled && !inputEventDispatcher.isPressed(KeyCode.DELETE) && !isGlidePostEffect) {
if (activeEditorInstance.shouldReevaluateComposingSuggestions) {
activeEditorInstance.shouldReevaluateComposingSuggestions = false
activeDictionary?.let {
launch(Dispatchers.Default) {
val startTime = System.nanoTime()
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())
if (BuildConfig.DEBUG) {
val elapsed = (System.nanoTime() - startTime) / 1000.0
flogInfo { "sugg fetch time: $elapsed us" }
}
launch(Dispatchers.Default) {
val startTime = System.nanoTime()
dictionaryManager.suggest(
currentWord = activeEditorInstance.cachedInput.currentWord.text,
preceidingWords = listOf(),
subtype = florisboard.activeSubtype,
allowPossiblyOffensive = !prefs.suggestion.blockPossiblyOffensive,
maxSuggestionCount = 16
) { suggestions ->
withContext(Dispatchers.Main) {
smartbarView?.setCandidateSuggestionWords(startTime, suggestions)
smartbarView?.updateCandidateSuggestionCapsState()
}
}
if (BuildConfig.DEBUG) {
val elapsed = (System.nanoTime() - startTime) / 1000.0
flogInfo { "sugg fetch time: $elapsed us" }
}
}
} else {
smartbarView?.setCandidateSuggestionWords(System.nanoTime(), null)
@@ -480,40 +467,6 @@ 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.

View File

@@ -0,0 +1,54 @@
package dev.patrickgold.florisboard.ime.text.composing
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
interface Composer {
val name: String
val label: String
val toRead: Int
fun getActions(s: String, c: Char): Pair<Int, String>
}
@Serializable
@SerialName("appender")
class Appender : Composer {
companion object {
const val name = "appender"
}
override val name: String = Appender.name
override val label: String = "Appender"
override val toRead: Int = 0
override fun getActions(s: String, c: Char): Pair<Int, String> {
return Pair(0, "$c")
}
}
@Serializable
@SerialName("with-rules")
class WithRules(
override val name: String,
override val label: String,
val rules: JsonObject
) : Composer {
override val toRead: Int = rules.keys.toList().sortedBy { it.length }.reversed()[0].length - 1
@Transient val ruleOrder: List<String> = rules.keys.toList().sortedBy { it.length }.reversed()
@Transient val ruleMap: Map<String, String> = rules.entries.map { Pair(it.key, (it.value as JsonPrimitive).content) }.toMap()
override fun getActions(s: String, c: Char): Pair<Int, String> {
val str = "${s}$c"
for (key in ruleOrder) {
if (str.endsWith(key)) {
return Pair(key.length-1, ruleMap.getValue(key))
}
}
return Pair(0, "$c")
}
}

View File

@@ -0,0 +1,102 @@
package dev.patrickgold.florisboard.ime.text.composing
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@SerialName("hangul-unicode")
class HangulUnicode : Composer {
override val name: String = "hangul-unicode"
override val label: String = "Hangul Unicode"
override val toRead: Int = 1
// Initial consonants, ordered for syllable creation
private val initials = "ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ"
// Medial vowels, ordered for syllable creation
private val medials = "ㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣ"
// Final consonants (including none), ordered for syllable creation
private val finals = "_ㄱㄲㄳㄴㄵㄶㄷㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅄㅅㅆㅇㅈㅊㅋㅌㅍㅎ"
private val medialComp = mapOf(
'ㅗ' to listOfNotNull("ㅏㅐㅣ", "ㅘㅙㅚ"),
'ㅜ' to listOfNotNull("ㅓㅔㅣ", "ㅝㅞㅟ"),
'ㅡ' to listOfNotNull("", ""),
)
private val finalComp = mapOf(
'ㄱ' to listOfNotNull("", ""),
'ㄴ' to listOfNotNull("ㅈㅎ", "ㄵㄶ"),
'ㄹ' to listOfNotNull("ㄱㅁㅂㅅㅌㅍㅎ", "ㄺㄻㄼㄽㄾㄿㅀ"),
'ㅂ' to listOfNotNull("", ""),
)
private fun reverseComp(map: Map<Char, List<String>>): Map<Char, List<Char>> {
val ret = mutableMapOf<Char, List<Char>>()
for ((first, v) in map) {
val (seconds, comps) = v
for (i in seconds.indices) {
ret[comps[i]] = listOf(first, seconds[i])
}
}
return ret
}
private val finalCompRev = reverseComp(finalComp)
private val medialCompRev = reverseComp(medialComp)
private fun syllable(ini: Int, med: Int, fin:Int): Char {
return (ini*588 + med*28 + fin + 44032).toChar()
}
private fun syllableBlocks(syllOrd: Int): List<Int> {
val initial = (syllOrd-44032)/588
val medial = (syllOrd-44032-initial*588)/28
val fin = (syllOrd-44032)%28
return listOf(initial, medial, fin)
}
override fun getActions(s: String, c: Char): Pair<Int, String> {
// s is "at least the last 1 character of what's currently here"
if (s.isEmpty()) {
return Pair(0, ""+c)
}
val lastChar = s.last()
val lastOrd = lastChar.toInt()
if (lastChar in initials && c in medials) {
return Pair(1, "${syllable(initials.indexOf(lastChar), medials.indexOf(c), 0)}")
} else if (lastOrd in 44032..55203) { // syllable
val (ini, med, fin) = syllableBlocks(lastOrd)
// underscore is a sentinel in the "finals" string
if (c == '_')
return Pair(0, ""+c)
// if there is no final and the new char is a final, merge
if (fin == 0 && c in finals)
return Pair(1, "${syllable(ini, med, finals.indexOf(c))}")
// if there is already a final but it is mergeable with the new char into a composed final, merge
if ((finals[fin] in finalComp) && c in finalComp[finals[fin]]!![0]) {
val tple = finalComp[finals[fin]]
return Pair(1, "${syllable(ini, med, finals.indexOf(tple!![1][tple[0].indexOf(c)]))}")
}
// if there is a simple final and the new char is a medial, split the old syllable
if (fin != 0 && finals[fin] !in finalCompRev && c in medials)
return Pair(1, "${syllable(ini, med, 0)}${syllable(initials.indexOf(finals[fin]), medials.indexOf(c), 0)}")
// if there is a composed final and the new char is a medial, split the old final
if (finals[fin] in finalCompRev && c in medials)
return Pair(1, "${syllable(ini, med, finals.indexOf(finalCompRev.getValue(finals[fin])[0]))}${syllable(initials.indexOf(finalCompRev.getValue(finals[fin])[1]), medials.indexOf(c), 0)}")
// if no final yet, and current medial can be composed with new char, merge
if (medials[med] in medialComp && c in medialComp.getValue(medials[med])[0] && fin == 0) {
val tple = medialComp[medials[med]]
return Pair(1, "${syllable(ini, medials.indexOf(tple!![1][tple[0].indexOf(c)]), 0)}")
}
}
return Pair(0, ""+c)
}
}

View File

@@ -105,10 +105,12 @@ class GlideTypingManager : GlideTypingGesture.Listener, CoroutineScope by MainSc
textInputManager.isGlidePostEffect = true
textInputManager.smartbarView?.setCandidateSuggestionWords(
time,
suggestions.subList(
// FIXME
/*suggestions.subList(
1.coerceAtMost(min(commit.compareTo(false), suggestions.size)),
maxSuggestionsToShow.coerceAtMost(suggestions.size)
).map { textInputManager.fixCase(it) }
).map { textInputManager.fixCase(it) }*/
null
)
textInputManager.smartbarView?.updateCandidateSuggestionCapsState()
if (commit && suggestions.isNotEmpty()) {

View File

@@ -92,10 +92,13 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
override fun setLayout(keyViews: List<TextKey>, subtype: Subtype) {
// stop duplicate calls
if (layoutSubtype == subtype) {
if (layoutSubtype == subtype && keys == keyViews) {
return
}
// if only layout changed but not subtype
val layoutChanged = layoutSubtype == subtype
keysByCharacter.clear()
keys.clear()
keyViews.forEach {
@@ -105,7 +108,13 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
layoutSubtype = subtype
distanceThresholdSquared = (keyViews.first().visibleBounds.width() / 4)
distanceThresholdSquared *= distanceThresholdSquared
initializePruner()
if (
(wordDataSubtype == layoutSubtype)
|| layoutChanged // should force a re-initialize
) {
initializePruner(layoutChanged)
}
}
override fun setWordData(words: HashMap<String, Int>, subtype: Subtype) {
@@ -118,7 +127,9 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
this.wordFrequencies = words
this.wordDataSubtype = subtype
initializePruner()
if (wordDataSubtype == layoutSubtype) {
initializePruner(false)
}
}
private val prunerCache = LruCache<Subtype, Pruner>(PRUNER_CACHE_SIZE)
@@ -127,13 +138,12 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
* Exists because Pruner requires both word data and layout are initialized,
* however we don't know what order they're initialized in.
*/
private fun initializePruner() {
if (this.layoutSubtype == null || this.wordDataSubtype != this.layoutSubtype) {
// not yet ready
return
}
private fun initializePruner(invalidateCache: Boolean) {
val currentSubtype = this.layoutSubtype!!
val cached = prunerCache.get(currentSubtype)
val cached = when {
invalidateCache -> null
else -> prunerCache.get(currentSubtype)
}
if (cached == null) {
this.pruner = Pruner(PRUNING_LENGTH_THRESHOLD, this.words, keysByCharacter)
prunerCache.put(currentSubtype, this.pruner)
@@ -361,7 +371,12 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
): Iterable<Int> {
val keyDistances = HashMap<TextKey, Float>()
for (key in keys) {
val distance = Gesture.distance(key.visibleBounds.centerX().toFloat(), key.visibleBounds.centerY().toFloat(), x, y)
val distance = Gesture.distance(
key.visibleBounds.centerX().toFloat(),
key.visibleBounds.centerY().toFloat(),
x,
y
)
keyDistances[key] = distance
}
@@ -413,26 +428,39 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
if (previousLetter == lc) {
// bottom right
idealGestureWithLoops.addPoint(
key.visibleBounds.centerX() + key.visibleBounds.width() / 4.0f, key.visibleBounds.centerY() + key.visibleBounds.height() / 4.0f
key.visibleBounds.centerX() + key.visibleBounds.width() / 4.0f,
key.visibleBounds.centerY() + key.visibleBounds.height() / 4.0f
)
// top right
idealGestureWithLoops.addPoint(
key.visibleBounds.centerX() + key.visibleBounds.width() / 4.0f, key.visibleBounds.centerY() - key.visibleBounds.height() / 4.0f
key.visibleBounds.centerX() + key.visibleBounds.width() / 4.0f,
key.visibleBounds.centerY() - key.visibleBounds.height() / 4.0f
)
// top left
idealGestureWithLoops.addPoint(
key.visibleBounds.centerX() - key.visibleBounds.width() / 4.0f, key.visibleBounds.centerY() - key.visibleBounds.height() / 4.0f
key.visibleBounds.centerX() - key.visibleBounds.width() / 4.0f,
key.visibleBounds.centerY() - key.visibleBounds.height() / 4.0f
)
// bottom left
idealGestureWithLoops.addPoint(
key.visibleBounds.centerX() - key.visibleBounds.width() / 4.0f, key.visibleBounds.centerY() + key.visibleBounds.height() / 4.0f
key.visibleBounds.centerX() - key.visibleBounds.width() / 4.0f,
key.visibleBounds.centerY() + key.visibleBounds.height() / 4.0f
)
hasLoops = true
idealGesture.addPoint(key.visibleBounds.centerX().toFloat(), key.visibleBounds.centerY().toFloat())
idealGesture.addPoint(
key.visibleBounds.centerX().toFloat(),
key.visibleBounds.centerY().toFloat()
)
} else {
idealGesture.addPoint(key.visibleBounds.centerX().toFloat(), key.visibleBounds.centerY().toFloat())
idealGestureWithLoops.addPoint(key.visibleBounds.centerX().toFloat(), key.visibleBounds.centerY().toFloat())
idealGesture.addPoint(
key.visibleBounds.centerX().toFloat(),
key.visibleBounds.centerY().toFloat()
)
idealGestureWithLoops.addPoint(
key.visibleBounds.centerX().toFloat(),
key.visibleBounds.centerY().toFloat()
)
}
previousLetter = lc
}

View File

@@ -22,6 +22,8 @@ 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.ime.view.Pointer
import dev.patrickgold.florisboard.ime.view.PointerMap
import dev.patrickgold.florisboard.util.ViewLayoutUtils
import kotlin.math.abs
import kotlin.math.atan2
@@ -67,7 +69,8 @@ abstract class SwipeGesture {
* @property listener The listener to report detected swipes to.
*/
class Detector(private val context: Context, private val listener: Listener) {
private var pointerDataMap: MutableMap<Int, PointerData> = mutableMapOf()
var isEnabled: Boolean = true
private var pointerMap: PointerMap<GesturePointer> = PointerMap { GesturePointer() }
private var thresholdSpeed: Double = numericValue(context, VelocityThreshold.NORMAL)
private var thresholdWidth: Double = numericValue(context, DistanceThreshold.NORMAL)
private var unitWidth: Double = thresholdWidth / 4.0
@@ -93,99 +96,92 @@ abstract class SwipeGesture {
* trigger, regardless of the distance from the previous event. Defaults to false.
* @return True if the given [event] is a gesture, false otherwise.
*/
fun onTouchEvent(event: MotionEvent, alwaysTriggerOnMove: Boolean = false): Boolean {
try {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN,
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 {
firstX = event.getX(pointerIndex)
firstY = event.getY(pointerIndex)
lastX = firstX
lastY = firstY
}
}
MotionEvent.ACTION_MOVE -> {
velocityTracker.addMovement(event)
for (pointerIndex in 0 until event.pointerCount) {
val pointerId = event.getPointerId(pointerIndex)
pointerDataMap[pointerId]?.apply {
val absDiffX = event.getX(pointerIndex) - firstX
val absDiffY = event.getY(pointerIndex) - firstY
val relDiffX = event.getX(pointerIndex) - lastX
val relDiffY = event.getY(pointerIndex) - lastY
return if (alwaysTriggerOnMove || abs(relDiffX) > (thresholdWidth / 2.0) || abs(relDiffY) > (thresholdWidth / 2.0)) {
lastX = event.getX(pointerIndex)
lastY = event.getY(pointerIndex)
val direction = detectDirection(relDiffX.toDouble(), relDiffY.toDouble())
val newAbsUnitCountX = (absDiffX / unitWidth).toInt()
val newAbsUnitCountY = (absDiffY / unitWidth).toInt()
val relUnitCountX = newAbsUnitCountX - absUnitCountX
val relUnitCountY = newAbsUnitCountY - absUnitCountY
absUnitCountX = newAbsUnitCountX
absUnitCountY = newAbsUnitCountY
listener.onSwipe(Event(
direction = direction,
type = Type.TOUCH_MOVE,
pointerId,
absUnitCountX,
absUnitCountY,
relUnitCountX,
relUnitCountY
))
} else {
false
}
}
}
}
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
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()
listener.onSwipe(Event(
direction = direction,
type = Type.TOUCH_UP,
pointerId,
absUnitCountX,
absUnitCountY,
absUnitCountX,
absUnitCountY
))
} else {
false
}
}
return false
}
MotionEvent.ACTION_CANCEL -> {
resetState()
}
else -> return false
}
return false
} catch (e: Exception) {
return false
fun onTouchEvent(event: MotionEvent) {
if (!isEnabled) return
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
resetState()
velocityTracker.clear()
}
velocityTracker.addMovement(event)
}
fun onTouchDown(event: MotionEvent, pointer: Pointer) {
if (!isEnabled) return
pointerMap.add(pointer.id, pointer.index)?.let { gesturePointer ->
gesturePointer.firstX = event.getX(pointer.index)
gesturePointer.firstY = event.getY(pointer.index)
gesturePointer.lastX = gesturePointer.firstX
gesturePointer.lastY = gesturePointer.firstY
}
}
fun onTouchMove(event: MotionEvent, pointer: Pointer, alwaysTriggerOnMove: Boolean): Boolean {
if (!isEnabled) return false
pointerMap.findById(pointer.id)?.let { gesturePointer ->
gesturePointer.index = pointer.index
val absDiffX = event.getX(pointer.index) - gesturePointer.firstX
val absDiffY = event.getY(pointer.index) - gesturePointer.firstY
val relDiffX = event.getX(pointer.index) - gesturePointer.lastX
val relDiffY = event.getY(pointer.index) - gesturePointer.lastY
return if (alwaysTriggerOnMove || abs(relDiffX) > (thresholdWidth / 2.0) || abs(relDiffY) > (thresholdWidth / 2.0)) {
gesturePointer.lastX = event.getX(pointer.index)
gesturePointer.lastY = event.getY(pointer.index)
val direction = detectDirection(relDiffX.toDouble(), relDiffY.toDouble())
val newAbsUnitCountX = (absDiffX / unitWidth).toInt()
val newAbsUnitCountY = (absDiffY / unitWidth).toInt()
val relUnitCountX = newAbsUnitCountX - gesturePointer.absUnitCountX
val relUnitCountY = newAbsUnitCountY - gesturePointer.absUnitCountY
gesturePointer.absUnitCountX = newAbsUnitCountX
gesturePointer.absUnitCountY = newAbsUnitCountY
listener.onSwipe(Event(
direction = direction,
type = Type.TOUCH_MOVE,
pointer.id,
gesturePointer.absUnitCountX,
gesturePointer.absUnitCountY,
relUnitCountX,
relUnitCountY
))
} else {
false
}
}
return false
}
fun onTouchUp(event: MotionEvent, pointer: Pointer): Boolean {
if (!isEnabled) return false
pointerMap.findById(pointer.id)?.let { gesturePointer ->
val absDiffX = event.getX(pointer.index) - gesturePointer.firstX
val absDiffY = event.getY(pointer.index) - gesturePointer.firstY
velocityTracker.computeCurrentVelocity(1000)
val velocityX = ViewLayoutUtils.convertDpToPixel(velocityTracker.getXVelocity(pointer.id), context)
val velocityY = ViewLayoutUtils.convertDpToPixel(velocityTracker.getYVelocity(pointer.id), context)
flogDebug(LogTopic.GESTURES) { "Velocity: $velocityX $velocityY dp/s" }
pointerMap.removeById(pointer.id)
return if ((abs(absDiffX) > thresholdWidth || abs(absDiffY) > thresholdWidth) && (abs(velocityX) > thresholdSpeed || abs(velocityY) > thresholdSpeed)) {
val direction = detectDirection(absDiffX.toDouble(), absDiffY.toDouble())
gesturePointer.absUnitCountX = (absDiffX / unitWidth).toInt()
gesturePointer.absUnitCountY = (absDiffY / unitWidth).toInt()
listener.onSwipe(Event(
direction = direction,
type = Type.TOUCH_UP,
pointer.id,
gesturePointer.absUnitCountX,
gesturePointer.absUnitCountY,
gesturePointer.absUnitCountX,
gesturePointer.absUnitCountY
))
} else {
false
}
}
return false
}
fun onTouchCancel(event: MotionEvent, pointer: Pointer) {
if (!isEnabled) return
pointerMap.removeById(pointer.id)
}
/**
@@ -222,16 +218,26 @@ abstract class SwipeGesture {
* Resets the state.
*/
private fun resetState() {
pointerDataMap.clear()
pointerMap.clear()
}
class PointerData {
class GesturePointer : Pointer() {
var firstX: Float = 0.0f
var firstY: Float = 0.0f
var lastX: Float = 0.0f
var lastY: Float = 0.0f
var absUnitCountX: Int = 0
var absUnitCountY: Int = 0
override fun reset() {
super.reset()
firstX = 0.0f
firstY = 0.0f
lastX = 0.0f
lastY = 0.0f
absUnitCountX = 0
absUnitCountY = 0
}
}
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright (C) 2021 ostrya
*
* 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.text.key
data class KeyHintConfiguration(
val symbolHintMode: KeyHintMode,
val numberHintMode: KeyHintMode,
val mergeHintPopups: Boolean
)
val HINTS_DISABLED: KeyHintConfiguration = KeyHintConfiguration(KeyHintMode.DISABLED, KeyHintMode.DISABLED, false)

View File

@@ -19,6 +19,7 @@ package dev.patrickgold.florisboard.ime.text.keyboard
import dev.patrickgold.florisboard.ime.keyboard.Key
import dev.patrickgold.florisboard.ime.keyboard.KeyData
import dev.patrickgold.florisboard.ime.popup.MutablePopupSet
import dev.patrickgold.florisboard.ime.popup.PopupMapping
import dev.patrickgold.florisboard.ime.popup.PopupSet
import dev.patrickgold.florisboard.ime.text.key.*
@@ -26,7 +27,8 @@ class TextKey(override val data: KeyData) : Key(data) {
var computedData: TextKeyData = TextKeyData.UNSPECIFIED
private set
val computedPopups: MutablePopupSet<TextKeyData> = MutablePopupSet()
var computedHint: TextKeyData? = null
var computedSymbolHint: TextKeyData? = null
var computedNumberHint: TextKeyData? = null
fun compute(evaluator: TextComputingEvaluator) {
val keyboardMode = evaluator.getKeyboard().mode
@@ -44,10 +46,7 @@ class TextKey(override val data: KeyData) : Key(data) {
} else {
computedData = computed
computedPopups.clear()
computedPopups.hint = computedHint?.computeTextKeyData(evaluator)
if (computed is BasicTextKeyData && computed.popup != null) {
computedPopups.merge(computed.popup, evaluator)
}
mergePopups(computed, evaluator, computedPopups::merge)
if (keyboardMode == KeyboardMode.CHARACTERS || keyboardMode == KeyboardMode.NUMERIC_ADVANCED ||
keyboardMode == KeyboardMode.SYMBOLS || keyboardMode == KeyboardMode.SYMBOLS2) {
val extLabel = when (computed.groupId) {
@@ -97,6 +96,9 @@ class TextKey(override val data: KeyData) : Key(data) {
keySpecificPopupSet?.let { merge(it, evaluator) }
popupSet?.let { merge(it, evaluator) }
}
if (computed.type == KeyType.CHARACTER) {
addComputedHints(computed.code, evaluator, extendedPopups, extendedPopupsDefault)
}
}
isEnabled = evaluator.evaluateEnabled(computed)
isVisible = true
@@ -150,4 +152,48 @@ class TextKey(override val data: KeyData) : Key(data) {
}
}
}
private fun addComputedHints(
keyCode: Int,
evaluator: TextComputingEvaluator,
extendedPopups: PopupMapping?,
extendedPopupsDefault: PopupMapping?
) {
val symbolHint = computedSymbolHint
if (symbolHint != null) {
val evaluatedSymbolHint = symbolHint.computeTextKeyData(evaluator)
if (symbolHint.code != keyCode) {
computedPopups.symbolHint = evaluatedSymbolHint
mergePopups(evaluatedSymbolHint, evaluator, computedPopups::mergeSymbolHint)
val hintSpecificPopupSet =
extendedPopups?.get(KeyVariation.ALL)?.get(symbolHint.label) ?: extendedPopupsDefault?.get(
KeyVariation.ALL
)?.get(symbolHint.label)
hintSpecificPopupSet?.let { computedPopups.mergeSymbolHint(it, evaluator) }
}
}
val numericHint = computedNumberHint
if (numericHint != null) {
val evaluatedNumberHint = numericHint.computeTextKeyData(evaluator)
if (numericHint.code != keyCode) {
computedPopups.numberHint = evaluatedNumberHint
mergePopups(evaluatedNumberHint, evaluator, computedPopups::mergeNumberHint)
val hintSpecificPopupSet =
extendedPopups?.get(KeyVariation.ALL)?.get(numericHint.label) ?: extendedPopupsDefault?.get(
KeyVariation.ALL
)?.get(numericHint.label)
hintSpecificPopupSet?.let { computedPopups.mergeNumberHint(it, evaluator) }
}
}
}
private fun mergePopups(
keyData: TextKeyData?,
evaluator: TextComputingEvaluator,
merge: (popups: PopupSet<TextKeyData>, evaluator: TextComputingEvaluator) -> Unit
) {
if (keyData is BasicTextKeyData && keyData.popup != null) {
merge(keyData.popup, evaluator)
}
}
}

View File

@@ -67,202 +67,202 @@ interface TextKeyData : KeyData {
companion object {
/** Predefined key data for [KeyCode.ARROW_DOWN] */
val ARROW_DOWN = AutoTextKeyData(
val ARROW_DOWN = BasicTextKeyData(
type = KeyType.NAVIGATION,
code = KeyCode.ARROW_DOWN,
label = "arrow_down"
)
/** Predefined key data for [KeyCode.ARROW_LEFT] */
val ARROW_LEFT = AutoTextKeyData(
val ARROW_LEFT = BasicTextKeyData(
type = KeyType.NAVIGATION,
code = KeyCode.ARROW_LEFT,
label = "arrow_left"
)
/** Predefined key data for [KeyCode.ARROW_RIGHT] */
val ARROW_RIGHT = AutoTextKeyData(
val ARROW_RIGHT = BasicTextKeyData(
type = KeyType.NAVIGATION,
code = KeyCode.ARROW_RIGHT,
label = "arrow_right"
)
/** Predefined key data for [KeyCode.ARROW_UP] */
val ARROW_UP = AutoTextKeyData(
val ARROW_UP = BasicTextKeyData(
type = KeyType.NAVIGATION,
code = KeyCode.ARROW_UP,
label = "arrow_up"
)
/** Predefined key data for [KeyCode.CLIPBOARD_COPY] */
val CLIPBOARD_COPY = AutoTextKeyData(
val CLIPBOARD_COPY = BasicTextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.CLIPBOARD_COPY,
label = "clipboard_copy"
)
/** Predefined key data for [KeyCode.CLIPBOARD_CUT] */
val CLIPBOARD_CUT = AutoTextKeyData(
val CLIPBOARD_CUT = BasicTextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.CLIPBOARD_CUT,
label = "clipboard_cut"
)
/** Predefined key data for [KeyCode.CLIPBOARD_PASTE] */
val CLIPBOARD_PASTE = AutoTextKeyData(
val CLIPBOARD_PASTE = BasicTextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.CLIPBOARD_PASTE,
label = "clipboard_paste"
)
/** Predefined key data for [KeyCode.CLIPBOARD_SELECT] */
val CLIPBOARD_SELECT = AutoTextKeyData(
val CLIPBOARD_SELECT = BasicTextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.CLIPBOARD_SELECT,
label = "clipboard_select"
)
/** Predefined key data for [KeyCode.CLIPBOARD_SELECT_ALL] */
val CLIPBOARD_SELECT_ALL = AutoTextKeyData(
val CLIPBOARD_SELECT_ALL = BasicTextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.CLIPBOARD_SELECT_ALL,
label = "clipboard_select_all"
)
/** Predefined key data for [KeyCode.DELETE] */
val DELETE = AutoTextKeyData(
val DELETE = BasicTextKeyData(
type = KeyType.ENTER_EDITING,
code = KeyCode.DELETE,
label = "delete"
)
/** Predefined key data for [KeyCode.DELETE_WORD] */
val DELETE_WORD = AutoTextKeyData(
val DELETE_WORD = BasicTextKeyData(
type = KeyType.ENTER_EDITING,
code = KeyCode.DELETE_WORD,
label = "delete_word"
)
/** Predefined key data for [KeyCode.INTERNAL_BATCH_EDIT] */
val INTERNAL_BATCH_EDIT = AutoTextKeyData(
val INTERNAL_BATCH_EDIT = BasicTextKeyData(
type = KeyType.FUNCTION,
code = KeyCode.INTERNAL_BATCH_EDIT,
label = "internal_batch_edit"
)
/** Predefined key data for [KeyCode.MOVE_START_OF_LINE] */
val MOVE_START_OF_LINE = AutoTextKeyData(
val MOVE_START_OF_LINE = BasicTextKeyData(
type = KeyType.NAVIGATION,
code = KeyCode.MOVE_START_OF_LINE,
label = "move_start_of_line"
)
/** Predefined key data for [KeyCode.MOVE_END_OF_LINE] */
val MOVE_END_OF_LINE = AutoTextKeyData(
val MOVE_END_OF_LINE = BasicTextKeyData(
type = KeyType.NAVIGATION,
code = KeyCode.MOVE_END_OF_LINE,
label = "move_end_of_line"
)
/** Predefined key data for [KeyCode.MOVE_START_OF_PAGE] */
val MOVE_START_OF_PAGE = AutoTextKeyData(
val MOVE_START_OF_PAGE = BasicTextKeyData(
type = KeyType.NAVIGATION,
code = KeyCode.MOVE_START_OF_PAGE,
label = "move_start_of_page"
)
/** Predefined key data for [KeyCode.MOVE_END_OF_PAGE] */
val MOVE_END_OF_PAGE = AutoTextKeyData(
val MOVE_END_OF_PAGE = BasicTextKeyData(
type = KeyType.NAVIGATION,
code = KeyCode.MOVE_END_OF_PAGE,
label = "move_end_of_page"
)
/** Predefined key data for [KeyCode.REDO] */
val REDO = AutoTextKeyData(
val REDO = BasicTextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.REDO,
label = "redo"
)
/** Predefined key data for [KeyCode.SHOW_INPUT_METHOD_PICKER] */
val SHOW_INPUT_METHOD_PICKER = AutoTextKeyData(
val SHOW_INPUT_METHOD_PICKER = BasicTextKeyData(
type = KeyType.FUNCTION,
code = KeyCode.SHOW_INPUT_METHOD_PICKER,
label = "show_input_method_picker"
)
/** Predefined key data for [KeyCode.SWITCH_TO_TEXT_CONTEXT] */
val SWITCH_TO_TEXT_CONTEXT = AutoTextKeyData(
val SWITCH_TO_TEXT_CONTEXT = BasicTextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.SWITCH_TO_TEXT_CONTEXT,
label = "switch_to_text_context"
)
/** Predefined key data for [KeyCode.SWITCH_TO_CLIPBOARD_CONTEXT] */
val SWITCH_TO_CLIPBOARD_CONTEXT = AutoTextKeyData(
val SWITCH_TO_CLIPBOARD_CONTEXT = BasicTextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.SWITCH_TO_CLIPBOARD_CONTEXT,
label = "switch_to_clipboard_context"
)
/** Predefined key data for [KeyCode.SHIFT] */
val SHIFT = AutoTextKeyData(
val SHIFT = BasicTextKeyData(
type = KeyType.MODIFIER,
code = KeyCode.SHIFT,
label = "shift"
)
/** Predefined key data for [KeyCode.SHIFT_LOCK] */
val SHIFT_LOCK = AutoTextKeyData(
val SHIFT_LOCK = BasicTextKeyData(
type = KeyType.MODIFIER,
code = KeyCode.SHIFT_LOCK,
label = "shift_lock"
)
/** Predefined key data for [KeyCode.SPACE] */
val SPACE = AutoTextKeyData(
val SPACE = BasicTextKeyData(
type = KeyType.CHARACTER,
code = KeyCode.SPACE,
label = "space"
)
/** Predefined key data for [KeyCode.UNDO] */
val UNDO = AutoTextKeyData(
val UNDO = BasicTextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.UNDO,
label = "undo"
)
/** Predefined key data for [KeyCode.UNSPECIFIED] */
val UNSPECIFIED = AutoTextKeyData(
val UNSPECIFIED = BasicTextKeyData(
type = KeyType.UNSPECIFIED,
code = KeyCode.UNSPECIFIED,
label = "unspecified"
)
/** Predefined key data for [KeyCode.VIEW_CHARACTERS] */
val VIEW_CHARACTERS = AutoTextKeyData(
val VIEW_CHARACTERS = BasicTextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.VIEW_CHARACTERS,
label = "view_characters"
)
/** Predefined key data for [KeyCode.VIEW_SYMBOLS] */
val VIEW_SYMBOLS = AutoTextKeyData(
val VIEW_SYMBOLS = BasicTextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.VIEW_SYMBOLS,
label = "view_symbols"
)
/** Predefined key data for [KeyCode.VIEW_SYMBOLS2] */
val VIEW_SYMBOLS2 = AutoTextKeyData(
val VIEW_SYMBOLS2 = BasicTextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.VIEW_SYMBOLS2,
label = "view_symbols2"
)
/** Predefined key data for [KeyCode.VIEW_NUMERIC_ADVANCED] */
val VIEW_NUMERIC_ADVANCED = AutoTextKeyData(
val VIEW_NUMERIC_ADVANCED = BasicTextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.VIEW_NUMERIC_ADVANCED,
label = "view_numeric_advanced"

View File

@@ -18,13 +18,14 @@ package dev.patrickgold.florisboard.ime.text.keyboard
import android.animation.ValueAnimator
import android.content.Context
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.graphics.*
import android.graphics.drawable.PaintDrawable
import android.os.Handler
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.animation.AccelerateInterpolator
import com.google.android.material.shape.MaterialShapeDrawable
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.debug.*
import dev.patrickgold.florisboard.ime.core.*
@@ -37,14 +38,18 @@ import dev.patrickgold.florisboard.ime.text.gestures.SwipeGesture
import dev.patrickgold.florisboard.ime.text.key.*
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeValue
import dev.patrickgold.florisboard.ime.view.Pointer
import dev.patrickgold.florisboard.ime.view.PointerMap
import dev.patrickgold.florisboard.util.ViewLayoutUtils
import dev.patrickgold.florisboard.util.cancelAll
import dev.patrickgold.florisboard.util.postDelayed
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.abs
import kotlin.math.pow
import kotlin.math.roundToInt
import kotlin.math.sqrt
@Suppress("UNUSED_PARAMETER")
class TextKeyboardView : KeyboardView, SwipeGesture.Listener, GlideTypingGesture.Listener {
private var computedKeyboard: TextKeyboard? = null
private var iconSet: TextKeyboardIconSet? = null
@@ -105,17 +110,13 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener, GlideTypingGesture
private var isPreviewMode: Boolean = false
private var isLoadingPlaceholderKeyboard: Boolean = false
private var initialKey: TextKey? = null
private var activeKey: TextKey? = null
private var activePointerId: Int? = null
private val longPressHandler: Handler = Handler(context.mainLooper)
private var keyHintConfiguration: KeyHintConfiguration = prefs.keyboard.getKeyHintConfiguration()
private val pointerMap: PointerMap<TouchPointer> = PointerMap { TouchPointer() }
private val popupManager: PopupManager<TextKeyboardView>
private var initSelectionStart: Int = 0
private var initSelectionEnd: Int = 0
private var isGliding: Boolean = false
private var hasTriggeredGestureMove: Boolean = false
private var shouldBlockNextUp: Boolean = false
private val swipeGestureDetector = SwipeGesture.Detector(context, this)
private val glideTypingDetector = GlideTypingGesture.Detector(context)
@@ -127,8 +128,9 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener, GlideTypingGesture
val desiredKey: TextKey = TextKey(data = TextKeyData.UNSPECIFIED)
private var keyBackgroundDrawable: PaintDrawable = PaintDrawable().apply {
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
private var keyBackgroundDrawable: MaterialShapeDrawable = MaterialShapeDrawable().also {
it.setCornerSize(ViewLayoutUtils.convertDpToPixel(6.0f, context))
//it.shadowCompatibilityMode = MaterialShapeDrawable.SHADOW_COMPAT_MODE_ALWAYS
}
private var backgroundDrawable: PaintDrawable = PaintDrawable()
@@ -171,6 +173,7 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener, GlideTypingGesture
flogError(LogTopic.TEXT_KEYBOARD_VIEW) { "PopupLayerView is null, cannot show popups!" }
}
popupManager = PopupManager(this, popupLayerView)
swipeGestureDetector.isEnabled = !isSmartbarKeyboardView
setWillNotDraw(false)
}
@@ -223,285 +226,329 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener, GlideTypingGesture
}
override fun onTouchEventInternal(event: MotionEvent) {
if (prefs.glide.enabled &&
computedKeyboard?.mode == KeyboardMode.CHARACTERS &&
glideTypingDetector.onTouchEvent(event, initialKey) &&
event.actionMasked != MotionEvent.ACTION_UP
) {
if (activePointerId != null) {
val pointerIndex = event.actionIndex
onTouchCancelInternal(event, pointerIndex, activePointerId!!)
flogDebug { "event=$event" }
val dispatcher = florisboard?.textInputManager?.inputEventDispatcher ?: return
swipeGestureDetector.onTouchEvent(event)
if (prefs.glide.enabled && computedKeyboard?.mode == KeyboardMode.CHARACTERS) {
val glidePointer = pointerMap.findById(0)
if (glideTypingDetector.onTouchEvent(event, glidePointer?.initialKey)) {
for (pointer in pointerMap) {
if (pointer.activeKey != null) {
onTouchCancelInternal(event, pointer)
}
}
if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) {
pointerMap.clear()
}
isGliding = true
invalidate()
return
}
isGliding = true
invalidate()
return
}
when (event.actionMasked) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_DOWN -> {
dispatcher.send(InputKeyEvent.down(TextKeyData.INTERNAL_BATCH_EDIT))
keyHintConfiguration = prefs.keyboard.getKeyHintConfiguration()
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
val pointer = pointerMap.add(pointerId, pointerIndex)
if (pointer != null) {
swipeGestureDetector.onTouchDown(event, pointer)
onTouchDownInternal(event, pointer)
}
}
MotionEvent.ACTION_POINTER_DOWN -> {
val pointerIndex = event.actionIndex
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)
val oldPointer = pointerMap.findById(pointerId)
if (oldPointer != null) {
swipeGestureDetector.onTouchCancel(event, oldPointer)
onTouchCancelInternal(event, oldPointer)
pointerMap.removeById(oldPointer.id)
}
// Search for active character keys and cancel them
for (pointer in pointerMap) {
val activeKey = pointer.activeKey
if (activeKey != null && popupManager.isSuitableForPopups(activeKey)) {
swipeGestureDetector.onTouchCancel(event, pointer)
onTouchCancelInternal(event, pointer)
}
}
val pointer = pointerMap.add(pointerId, pointerIndex)
if (pointer != null) {
swipeGestureDetector.onTouchDown(event, pointer)
onTouchDownInternal(event, pointer)
}
}
MotionEvent.ACTION_MOVE -> {
for (pointerIndex in 0 until event.pointerCount) {
val pointerId = event.getPointerId(pointerIndex)
if (activePointerId == pointerId) {
onTouchMoveInternal(event, pointerIndex, pointerId)
break // No need to continue looping at this point as multi-touch is not supported
val pointer = pointerMap.findById(pointerId)
if (pointer != null) {
pointer.index = pointerIndex
val alwaysTriggerOnMove = (pointer.hasTriggeredGestureMove
&& (pointer.initialKey?.computedData?.code == KeyCode.DELETE
&& prefs.gestures.deleteKeySwipeLeft == SwipeAction.DELETE_CHARACTERS_PRECISELY
|| pointer.initialKey?.computedData?.code == KeyCode.SPACE))
if (swipeGestureDetector.onTouchMove(event, pointer, alwaysTriggerOnMove) || pointer.hasTriggeredGestureMove) {
pointer.longPressJob?.cancel()
pointer.longPressJob = null
pointer.hasTriggeredGestureMove = true
pointer.activeKey?.let { activeKey ->
activeKey.isPressed = false
florisboard!!.textInputManager.inputEventDispatcher.let { dispatcher ->
if (dispatcher.isPressed(activeKey.computedData.code)) {
dispatcher.send(InputKeyEvent.cancel(activeKey.computedData))
}
}
}
} else {
onTouchMoveInternal(event, pointer)
}
}
}
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP -> {
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
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))
val pointer = pointerMap.findById(pointerId)
if (pointer != null) {
if (swipeGestureDetector.onTouchUp(event, pointer) || pointer.hasTriggeredGestureMove || pointer.shouldBlockNextUp) {
if (pointer.hasTriggeredGestureMove && pointer.initialKey?.computedData?.code == KeyCode.DELETE) {
florisboard!!.textInputManager.isGlidePostEffect = false
florisboard!!.activeEditorInstance.apply {
if (selection.isSelectionMode) {
deleteBackwards()
}
}
}
onTouchCancelInternal(event, pointer)
} else {
onTouchUpInternal(event, pointer)
}
activePointerId = null
pointerMap.removeById(pointer.id)
}
}
MotionEvent.ACTION_CANCEL -> {
MotionEvent.ACTION_UP -> {
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))
for (pointer in pointerMap) {
if (pointer.id == pointerId) {
pointer.index = pointerIndex
if (swipeGestureDetector.onTouchUp(event, pointer) || pointer.hasTriggeredGestureMove || pointer.shouldBlockNextUp) {
if (pointer.hasTriggeredGestureMove && pointer.initialKey?.computedData?.code == KeyCode.DELETE) {
florisboard!!.textInputManager.isGlidePostEffect = false
florisboard!!.activeEditorInstance.apply {
if (selection.isSelectionMode) {
deleteBackwards()
}
}
}
onTouchCancelInternal(event, pointer)
} else {
onTouchUpInternal(event, pointer)
}
} else {
swipeGestureDetector.onTouchCancel(event, pointer)
onTouchCancelInternal(event, pointer)
}
}
activePointerId = null
pointerMap.clear()
dispatcher.send(InputKeyEvent.up(TextKeyData.INTERNAL_BATCH_EDIT))
}
MotionEvent.ACTION_CANCEL -> {
for (pointer in pointerMap) {
swipeGestureDetector.onTouchCancel(event, pointer)
onTouchCancelInternal(event, pointer)
}
pointerMap.clear()
dispatcher.send(InputKeyEvent.up(TextKeyData.INTERNAL_BATCH_EDIT))
}
}
flogDebug(LogTopic.TEXT_KEYBOARD_VIEW) { "initialKey: ${initialKey?.computedData?.label} activeKey: ${activeKey?.computedData?.label}" }
invalidate()
}
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
private fun onTouchDownInternal(event: MotionEvent, pointer: TouchPointer) {
flogDebug(LogTopic.TEXT_KEYBOARD_VIEW) { "pointer=$pointer" }
activePointerId = pointerId
val key = computedKeyboard?.getKeyForPos(
event.getX(pointerIndex).roundToInt(), event.getY(pointerIndex).roundToInt()
event.getX(pointer.index).roundToInt(), event.getY(pointer.index).roundToInt()
)
if (key != null && key.isEnabled) {
var keyHintMode = KeyHintMode.DISABLED
if (prefs.keyboard.hintedNumberRowMode != KeyHintMode.DISABLED && key.computedPopups.hint?.type == KeyType.NUMERIC) {
keyHintMode = prefs.keyboard.hintedNumberRowMode
florisboard!!.textInputManager.inputEventDispatcher.let { dispatcher ->
dispatcher.send(InputKeyEvent.down(key.computedData))
}
if (prefs.keyboard.hintedSymbolsMode != KeyHintMode.DISABLED && key.computedPopups.hint?.type == KeyType.CHARACTER) {
keyHintMode = prefs.keyboard.hintedSymbolsMode
if (prefs.keyboard.popupEnabled && popupManager.isSuitableForPopups(key)) {
popupManager.show(key, keyHintConfiguration)
}
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.down(key.computedData))
if (prefs.keyboard.popupEnabled) {
popupManager.show(key, keyHintMode)
}
florisboard.keyPressVibrate()
florisboard.keyPressSound(key.computedData)
florisboard!!.keyPressVibrate()
florisboard!!.keyPressSound(key.computedData)
key.isPressed = true
if (resetInitialKey) {
initialKey = key
if (pointer.initialKey == null) {
pointer.initialKey = key
}
activeKey = key
val delayMillis = prefs.keyboard.longPressDelay.toLong()
when (key.computedData.code) {
KeyCode.SPACE -> {
initSelectionStart = florisboard.activeEditorInstance.selection.start
initSelectionEnd = florisboard.activeEditorInstance.selection.end
longPressHandler.postDelayed((delayMillis * 2.5f).toLong()) {
pointer.activeKey = key
pointer.longPressJob = mainScope.launch {
val delayMillis = prefs.keyboard.longPressDelay.toLong()
when (key.computedData.code) {
KeyCode.SPACE -> {
initSelectionStart = florisboard!!.activeEditorInstance.selection.start
initSelectionEnd = florisboard!!.activeEditorInstance.selection.end
delay((delayMillis * 2.5f).toLong())
when (prefs.gestures.spaceBarLongPress) {
SwipeAction.NO_ACTION,
SwipeAction.INSERT_SPACE -> {
}
else -> {
florisboard.executeSwipeAction(prefs.gestures.spaceBarLongPress)
shouldBlockNextUp = true
florisboard!!.executeSwipeAction(prefs.gestures.spaceBarLongPress)
pointer.shouldBlockNextUp = true
}
}
}
}
KeyCode.SHIFT -> {
longPressHandler.postDelayed((delayMillis * 2.5).toLong()) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(TextKeyData.SHIFT_LOCK))
florisboard.keyPressVibrate()
florisboard.keyPressSound(key.computedData)
KeyCode.SHIFT -> {
delay((delayMillis * 2.5f).toLong())
florisboard!!.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(TextKeyData.SHIFT_LOCK))
florisboard!!.keyPressVibrate()
florisboard!!.keyPressSound(key.computedData)
}
}
KeyCode.LANGUAGE_SWITCH -> {
longPressHandler.postDelayed((delayMillis * 2.0).toLong()) {
shouldBlockNextUp = true
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(TextKeyData.SHOW_INPUT_METHOD_PICKER))
KeyCode.LANGUAGE_SWITCH -> {
delay((delayMillis * 2.0f).toLong())
pointer.shouldBlockNextUp = true
florisboard!!.textInputManager.inputEventDispatcher.let { dispatcher ->
dispatcher.send(InputKeyEvent.downUp(TextKeyData.SHOW_INPUT_METHOD_PICKER))
}
}
}
else -> {
longPressHandler.postDelayed(delayMillis) {
if (key.computedPopups.isNotEmpty()) {
popupManager.extend(key, keyHintMode)
florisboard.keyPressVibrate()
florisboard.keyPressSound(key.computedData)
else -> {
delay(delayMillis)
if (popupManager.isSuitableForPopups(key) && key.computedPopups.getPopupKeys(keyHintConfiguration).isNotEmpty()) {
popupManager.extend(key, keyHintConfiguration)
florisboard!!.keyPressVibrate()
florisboard!!.keyPressSound(key.computedData)
}
}
}
}
if (!isSmartbarKeyboardView) {
swipeGestureDetector.onTouchEvent(event)
}
} else {
if (resetInitialKey) {
initialKey = null
}
activeKey = null
pointer.activeKey = null
}
invalidate()
}
private fun onTouchMoveInternal(event: MotionEvent, pointerIndex: Int, pointerId: Int) {
flogDebug(LogTopic.TEXT_KEYBOARD_VIEW) { "index=$pointerIndex id=$pointerId event=$event" }
val florisboard = florisboard ?: return
private fun onTouchMoveInternal(event: MotionEvent, pointer: TouchPointer) {
flogDebug(LogTopic.TEXT_KEYBOARD_VIEW) { "pointer=$pointer" }
val key = activeKey
if (key != null) {
if (!isSmartbarKeyboardView) {
val alwaysTriggerOnMove = (hasTriggeredGestureMove
&& (initialKey?.computedData?.code == KeyCode.DELETE
&& prefs.gestures.deleteKeySwipeLeft == SwipeAction.DELETE_CHARACTERS_PRECISELY
|| 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 (it.computedData.code != KeyCode.SHIFT && florisboard.textInputManager.inputEventDispatcher.isPressed(it.computedData.code)) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(it.computedData))
}
}
return
}
}
val initialKey = pointer.initialKey
val activeKey = pointer.activeKey
if (initialKey != null && activeKey != null) {
if (popupManager.isShowingExtendedPopup) {
if (!popupManager.propagateMotionEvent(key, event, pointerIndex)) {
onTouchCancelInternal(event, pointerIndex, pointerId)
onTouchDownInternal(event, pointerIndex, pointerId, resetInitialKey = false)
if (!popupManager.propagateMotionEvent(activeKey, event, pointer.index)) {
onTouchCancelInternal(event, pointer)
onTouchDownInternal(event, pointer)
}
} else {
if ((event.getX(pointerIndex) < key.visibleBounds.left - 0.1f * key.visibleBounds.width())
|| (event.getX(pointerIndex) > key.visibleBounds.right + 0.1f * key.visibleBounds.width())
|| (event.getY(pointerIndex) < key.visibleBounds.top - 0.35f * key.visibleBounds.height())
|| (event.getY(pointerIndex) > key.visibleBounds.bottom + 0.35f * key.visibleBounds.height())
if ((event.getX(pointer.index) < activeKey.visibleBounds.left - 0.1f * activeKey.visibleBounds.width())
|| (event.getX(pointer.index) > activeKey.visibleBounds.right + 0.1f * activeKey.visibleBounds.width())
|| (event.getY(pointer.index) < activeKey.visibleBounds.top - 0.35f * activeKey.visibleBounds.height())
|| (event.getY(pointer.index) > activeKey.visibleBounds.bottom + 0.35f * activeKey.visibleBounds.height())
) {
onTouchCancelInternal(event, pointerIndex, pointerId)
onTouchDownInternal(event, pointerIndex, pointerId, resetInitialKey = false)
onTouchCancelInternal(event, pointer)
onTouchDownInternal(event, pointer)
}
}
}
invalidate()
}
private fun onTouchUpInternal(event: MotionEvent, pointerIndex: Int, pointerId: Int) {
flogDebug(LogTopic.TEXT_KEYBOARD_VIEW) { "index=$pointerIndex id=$pointerId event=$event" }
val florisboard = florisboard ?: return
private fun onTouchUpInternal(event: MotionEvent, pointer: TouchPointer) {
flogDebug(LogTopic.TEXT_KEYBOARD_VIEW) { "pointer=$pointer" }
pointer.longPressJob?.cancel()
pointer.longPressJob = null
longPressHandler.cancelAll()
val key = activeKey
if (key != null) {
if (swipeGestureDetector.onTouchEvent(event) || hasTriggeredGestureMove || shouldBlockNextUp) {
if (hasTriggeredGestureMove && initialKey?.computedData?.code == KeyCode.DELETE) {
florisboard.textInputManager.isGlidePostEffect = false
florisboard.activeEditorInstance.apply {
if (selection.isSelectionMode) {
deleteBackwards()
val initialKey = pointer.initialKey
val activeKey = pointer.activeKey
if (initialKey != null && activeKey != null) {
activeKey.isPressed = false
florisboard!!.textInputManager.inputEventDispatcher.let { dispatcher ->
if (popupManager.isSuitableForPopups(activeKey)) {
val retData = popupManager.getActiveKeyData(activeKey, keyHintConfiguration)
if (retData != null && !pointer.hasTriggeredGestureMove) {
if (retData == activeKey.computedData) {
dispatcher.send(InputKeyEvent.up(activeKey.computedData))
} else {
if (dispatcher.isPressed(activeKey.computedData.code)) {
dispatcher.send(InputKeyEvent.cancel(activeKey.computedData))
}
dispatcher.send(InputKeyEvent.downUp(retData))
}
}
}
onTouchCancelInternal(event, pointerIndex, pointerId)
return
}
key.isPressed = false
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))
if (dispatcher.isPressed(activeKey.computedData.code)) {
dispatcher.send(InputKeyEvent.cancel(activeKey.computedData))
}
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(retData))
}
popupManager.hide()
} else {
if (florisboard.textInputManager.inputEventDispatcher.isPressed(key.computedData.code)) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(key.computedData))
if (dispatcher.isPressed(activeKey.computedData.code)) {
if (pointer.hasTriggeredGestureMove) {
dispatcher.send(InputKeyEvent.cancel(activeKey.computedData))
} else {
dispatcher.send(InputKeyEvent.up(activeKey.computedData))
}
}
}
}
pointer.activeKey = null
}
popupManager.hide()
activePointerId = null
hasTriggeredGestureMove = false
shouldBlockNextUp = false
invalidate()
pointer.hasTriggeredGestureMove = false
pointer.shouldBlockNextUp = false
}
private fun onTouchCancelInternal(event: MotionEvent, pointerIndex: Int, pointerId: Int) {
flogDebug(LogTopic.TEXT_KEYBOARD_VIEW) { "index=$pointerIndex id=$pointerId event=$event" }
private fun onTouchCancelInternal(event: MotionEvent, pointer: TouchPointer) {
flogDebug(LogTopic.TEXT_KEYBOARD_VIEW) { "pointer=$pointer" }
val florisboard = florisboard ?: return
pointer.longPressJob?.cancel()
pointer.longPressJob = null
longPressHandler.cancelAll()
val key = activeKey
if (key != null) {
key.isPressed = false
if (activeKey?.computedData?.code != KeyCode.SHIFT) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(key.computedData))
val activeKey = pointer.activeKey
if (activeKey != null) {
activeKey.isPressed = false
florisboard.textInputManager.inputEventDispatcher.let { dispatcher ->
dispatcher.send(InputKeyEvent.cancel(activeKey.computedData))
}
if (popupManager.isSuitableForPopups(activeKey)) {
popupManager.hide()
}
pointer.activeKey = null
}
popupManager.hide()
activePointerId = null
hasTriggeredGestureMove = false
shouldBlockNextUp = false
invalidate()
pointer.hasTriggeredGestureMove = false
pointer.shouldBlockNextUp = false
}
override fun onSwipe(event: SwipeGesture.Event): Boolean {
val florisboard = florisboard ?: return false
val initialKey = initialKey ?: return false
if (activePointerId != event.pointerId) return false
val pointer = pointerMap.findById(event.pointerId) ?: return false
val initialKey = pointer.initialKey ?: return false
val activeKey = pointer.activeKey ?: 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 &&
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 &&
initialKey.computedData.code == KeyCode.SHIFT && activeKey.computedData.code != KeyCode.SHIFT &&
event.type == SwipeGesture.Type.TOUCH_UP -> {
activeKey?.let {
activeKey.let {
florisboard.textInputManager.inputEventDispatcher.send(
InputKeyEvent.up(popupManager.getActiveKeyData(it) ?: it.computedData)
InputKeyEvent.up(popupManager.getActiveKeyData(it, prefs.keyboard.getKeyHintConfiguration()) ?: it.computedData)
)
}
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(TextKeyData.SHIFT))
true
}
initialKey.computedData.code > KeyCode.SPACE && !popupManager.isShowingExtendedPopup -> when {
!prefs.glide.enabled && !hasTriggeredGestureMove -> when (event.type) {
!prefs.glide.enabled && !pointer.hasTriggeredGestureMove -> when (event.type) {
SwipeGesture.Type.TOUCH_UP -> {
val swipeAction = when (event.direction) {
SwipeGesture.Direction.UP -> prefs.gestures.swipeUp
@@ -528,6 +575,7 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener, GlideTypingGesture
private fun handleDeleteSwipe(event: SwipeGesture.Event): Boolean {
val florisboard = florisboard ?: return false
val pointer = pointerMap.findById(event.pointerId) ?: return false
return when (event.type) {
SwipeGesture.Type.TOUCH_MOVE -> when (prefs.gestures.deleteKeySwipeLeft) {
@@ -540,7 +588,7 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener, GlideTypingGesture
(selection.end + event.absUnitCountX + 1).coerceIn(0, selection.end),
selection.end
)
shouldBlockNextUp = true
pointer.shouldBlockNextUp = true
}
true
}
@@ -550,7 +598,7 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener, GlideTypingGesture
florisboard.activeEditorInstance.apply {
leftAppendWordToSelection()
}
shouldBlockNextUp = true
pointer.shouldBlockNextUp = true
true
}
SwipeGesture.Direction.RIGHT -> {
@@ -558,7 +606,7 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener, GlideTypingGesture
florisboard.activeEditorInstance.apply {
leftPopWordFromSelection()
}
shouldBlockNextUp = true
pointer.shouldBlockNextUp = true
true
}
else -> false
@@ -580,47 +628,66 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener, GlideTypingGesture
private fun handleSpaceSwipe(event: SwipeGesture.Event): Boolean {
val florisboard = florisboard ?: return false
val pointer = pointerMap.findById(event.pointerId) ?: return false
return when (event.type) {
SwipeGesture.Type.TOUCH_MOVE -> when (event.direction) {
SwipeGesture.Direction.LEFT -> {
if (prefs.gestures.spaceBarSwipeLeft == SwipeAction.MOVE_CURSOR_LEFT) {
abs(event.relUnitCountX).let {
val count = if (!hasTriggeredGestureMove) { it - 1 } else { it }
val count = if (!pointer.hasTriggeredGestureMove) { it - 1 } else { it }
if (count > 0) {
florisboard.keyPressVibrate(isMovingGestureEffect = true)
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(
TextKeyData.ARROW_LEFT, count))
}
}
} else {
florisboard.executeSwipeAction(prefs.gestures.spaceBarSwipeLeft)
}
true
}
SwipeGesture.Direction.RIGHT -> {
if (prefs.gestures.spaceBarSwipeRight == SwipeAction.MOVE_CURSOR_RIGHT) {
abs(event.relUnitCountX).let {
val count = if (!hasTriggeredGestureMove) { it - 1 } else { it }
val count = if (!pointer.hasTriggeredGestureMove) { it - 1 } else { it }
if (count > 0) {
florisboard.keyPressVibrate(isMovingGestureEffect = true)
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(
TextKeyData.ARROW_RIGHT, count))
}
}
} else {
florisboard.executeSwipeAction(prefs.gestures.spaceBarSwipeRight)
}
true
}
else -> true // To prevent the popup display of nearby keys
}
SwipeGesture.Type.TOUCH_UP -> {
if (event.absUnitCountY.times(-1) > 6) {
florisboard.executeSwipeAction(prefs.gestures.spaceBarSwipeUp)
true
} else {
false
SwipeGesture.Type.TOUCH_UP -> when (event.direction) {
SwipeGesture.Direction.LEFT -> {
prefs.gestures.spaceBarSwipeLeft.let {
if (it != SwipeAction.MOVE_CURSOR_LEFT) {
florisboard.executeSwipeAction(it)
true
} else {
false
}
}
}
SwipeGesture.Direction.RIGHT -> {
prefs.gestures.spaceBarSwipeRight.let {
if (it != SwipeAction.MOVE_CURSOR_RIGHT) {
florisboard.executeSwipeAction(it)
true
} else {
false
}
}
}
else -> {
if (event.absUnitCountY < -6) {
florisboard.executeSwipeAction(prefs.gestures.spaceBarSwipeUp)
true
} else {
false
}
}
}
}
@@ -702,9 +769,10 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener, GlideTypingGesture
var spaceKey: TextKey? = null
if (isRecomputingRequested) {
isRecomputingRequested = false
val keyHintConfiguration = prefs.keyboard.getKeyHintConfiguration()
for (key in keyboard.keys()) {
key.compute(internalComputingEvaluator)
computeLabelsAndDrawables(key)
computeLabelsAndDrawables(key, keyHintConfiguration)
if (key.computedData.code == KeyCode.SPACE) {
spaceKey = key
}
@@ -738,9 +806,9 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener, GlideTypingGesture
spaceKey.visibleLabelBounds.width().toFloat(),
spaceKey.visibleLabelBounds.height().toFloat(),
spaceKey.label ?: "X",
fontSizeMultiplier.coerceAtMost(1.0)
fontSizeMultiplier
)
labelPaintSpaceTextSize = labelPaint.textSize
labelPaintSpaceTextSize = labelPaint.textSize.coerceAtMost(resources.getDimension(R.dimen.key_space_textSize))
}
setTextSizeFor(
@@ -826,9 +894,9 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener, GlideTypingGesture
}
if (isPreviewMode) {
backgroundDrawable.apply {
setBounds(0, 0, measuredWidth, measuredHeight)
draw(canvas)
backgroundDrawable.let {
it.setBounds(0, 0, measuredWidth, measuredHeight)
it.draw(canvas)
}
}
@@ -917,8 +985,8 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener, GlideTypingGesture
}
}
keyBackgroundDrawable.apply {
setBounds(
keyBackgroundDrawable.let {
it.setBounds(
key.visibleBounds.left,
if (isBorderless) {
(key.visibleBounds.top + key.visibleBounds.height() * 0.12).toInt()
@@ -932,19 +1000,19 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener, GlideTypingGesture
key.visibleBounds.bottom
}
)
elevation = if (shouldShowBorder) 4.0f else 0.0f
paint.color = keyBackground.toSolidColor().color
draw(canvas)
it.fillColor = ColorStateList.valueOf(keyBackground.toSolidColor().color)
//it.elevation = if (shouldShowBorder) 6.0f else 0.0f
it.draw(canvas)
}
val label = key.label
if (label != null) {
labelPaint.apply {
color = keyForeground.toSolidColor().color
labelPaint.let {
it.color = keyForeground.toSolidColor().color
if (computedKeyboard?.mode == KeyboardMode.CHARACTERS && key.computedData.code == KeyCode.SPACE) {
alpha = 120
it.alpha = 120
}
textSize = when (key.computedData.code) {
it.textSize = when (key.computedData.code) {
KeyCode.SPACE -> labelPaintSpaceTextSize
KeyCode.VIEW_CHARACTERS,
KeyCode.VIEW_SYMBOLS,
@@ -954,28 +1022,28 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener, GlideTypingGesture
else -> labelPaintTextSize
}
val centerX = key.visibleLabelBounds.exactCenterX()
val centerY = key.visibleLabelBounds.exactCenterY() + (labelPaint.textSize - labelPaint.descent()) / 2
val centerY = key.visibleLabelBounds.exactCenterY() + (it.textSize - it.descent()) / 2
if (label.contains("\n")) {
// Even if more lines may be existing only the first 2 are shown
val labelLines = label.split("\n")
val verticalAdjustment = key.visibleBounds.height() * 0.18f
canvas.drawText(labelLines[0], centerX, centerY - verticalAdjustment, labelPaint)
canvas.drawText(labelLines[1], centerX, centerY + verticalAdjustment, labelPaint)
canvas.drawText(labelLines[0], centerX, centerY - verticalAdjustment, it)
canvas.drawText(labelLines[1], centerX, centerY + verticalAdjustment, it)
} else {
canvas.drawText(label, centerX, centerY, labelPaint)
canvas.drawText(label, centerX, centerY, it)
}
}
}
val hintedLabel = key.hintedLabel
if (hintedLabel != null) {
hintedLabelPaint.apply {
color = keyForeground.toSolidColor().color
alpha = 170
textSize = hintedLabelPaintTextSize
hintedLabelPaint.let {
it.color = keyForeground.toSolidColor().color
it.alpha = 170
it.textSize = hintedLabelPaintTextSize
val centerX = key.visibleBounds.left + key.visibleBounds.width() * 5.0f / 6.0f
val centerY = key.visibleBounds.top + key.visibleBounds.height() * 1.0f / 6.0f + (hintedLabelPaint.textSize - hintedLabelPaint.descent()) / 2
canvas.drawText(hintedLabel, centerX, centerY, hintedLabelPaint)
val centerY = key.visibleBounds.top + key.visibleBounds.height() * 1.0f / 6.0f + (it.textSize - it.descent()) / 2
canvas.drawText(hintedLabel, centerX, centerY, it)
}
}
@@ -992,7 +1060,7 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener, GlideTypingGesture
/**
* Computes the labels and drawables needed to draw the key.
*/
private fun computeLabelsAndDrawables(key: TextKey) {
private fun computeLabelsAndDrawables(key: TextKey, keyHintConfiguration: KeyHintConfiguration) {
// Reset attributes first to avoid invalid states if not updated
key.label = null
key.hintedLabel = null
@@ -1003,12 +1071,8 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener, GlideTypingGesture
&& data.code != KeyCode.HALF_SPACE && data.code != KeyCode.KESHIDA || data.type == KeyType.NUMERIC
) {
key.label = data.asString(isForDisplay = true)
val hint = key.computedPopups.hint
if (prefs.keyboard.hintedNumberRowMode != KeyHintMode.DISABLED && hint?.type == KeyType.NUMERIC) {
key.hintedLabel = hint.asString(isForDisplay = true)
}
if (prefs.keyboard.hintedSymbolsMode != KeyHintMode.DISABLED && hint?.type == KeyType.CHARACTER) {
key.hintedLabel = hint.asString(isForDisplay = true)
key.computedPopups.getPopupKeys(keyHintConfiguration).hint?.asString(isForDisplay = true).let {
key.hintedLabel = it
}
} else {
when (data.code) {
@@ -1202,4 +1266,26 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener, GlideTypingGesture
invalidate()
}
}
private class TouchPointer : Pointer() {
var initialKey: TextKey? = null
var activeKey: TextKey? = null
var longPressJob: Job? = null
var hasTriggeredGestureMove: Boolean = false
var shouldBlockNextUp: Boolean = false
override fun reset() {
super.reset()
initialKey = null
activeKey = null
longPressJob?.cancel()
longPressJob = null
hasTriggeredGestureMove = false
shouldBlockNextUp = false
}
override fun toString(): String {
return "${TouchPointer::class.simpleName} { id=$id, index=$index, initialKey=$initialKey, activeKey=$activeKey }"
}
}
}

View File

@@ -150,7 +150,18 @@ class LayoutManager {
val mainLayout = loadLayoutAsync(main).await().getOrNull()
val modifierToLoad = if (mainLayout?.modifier != null) {
LTN(LayoutType.CHARACTERS_MOD, mainLayout.modifier)
val layoutType = when (mainLayout.type) {
LayoutType.SYMBOLS -> {
LayoutType.SYMBOLS_MOD
}
LayoutType.SYMBOLS2 -> {
LayoutType.SYMBOLS2_MOD
}
else -> {
LayoutType.CHARACTERS_MOD
}
}
LTN(layoutType, mainLayout.modifier)
} else {
modifier
}
@@ -207,22 +218,21 @@ class LayoutManager {
// Add hints to keys
if (keyboardMode == KeyboardMode.CHARACTERS) {
val symbolsComputedArrangement = computeKeyboardAsync(KeyboardMode.SYMBOLS, subtype).await().arrangement
val minRow = if (prefs.keyboard.numberRow) { 1 } else { 0 }
// number row hint always happens on first row
if (prefs.keyboard.hintedNumberRowMode != KeyHintMode.DISABLED) {
val row = computedArrangement[0]
val symbolRow = symbolsComputedArrangement[0]
addRowHints(row, symbolRow, KeyType.NUMERIC)
}
// all other symbols are added bottom-aligned
val rOffset = computedArrangement.size - symbolsComputedArrangement.size
for ((r, row) in computedArrangement.withIndex()) {
if (r >= (3 + minRow) || r < minRow) {
if (r < rOffset) {
continue
}
val symbolRow = symbolsComputedArrangement.getOrNull(r - minRow)
val symbolRow = symbolsComputedArrangement.getOrNull(r - rOffset)
if (symbolRow != null) {
for ((k, key) in row.withIndex()) {
val symbol = symbolRow.getOrNull(k)?.data?.computeTextKeyData(DefaultTextComputingEvaluator)
val type = (key.data as? TextKeyData)?.type ?: KeyType.UNSPECIFIED
if (r == minRow && type == KeyType.CHARACTER && symbol?.type == KeyType.NUMERIC) {
key.computedHint = symbol
} else if (r > minRow && type == KeyType.CHARACTER && symbol?.type == KeyType.CHARACTER) {
key.computedHint = symbol
}
}
addRowHints(row, symbolRow, KeyType.CHARACTER)
}
}
}
@@ -240,6 +250,27 @@ class LayoutManager {
)
}
private fun addRowHints(main: Array<TextKey>, hint: Array<TextKey>, hintType: KeyType) {
for ((k,key) in main.withIndex()) {
val hintKey = hint.getOrNull(k)?.data?.computeTextKeyData(DefaultTextComputingEvaluator)
if (hintKey?.type != hintType) {
continue
}
when (hintType) {
KeyType.CHARACTER -> {
key.computedSymbolHint = hintKey
}
KeyType.NUMERIC -> {
key.computedNumberHint = hintKey
}
else -> {
// do nothing
}
}
}
}
/**
* Computes a layout for [keyboardMode] based on the given [subtype] and returns it.
*

View File

@@ -36,6 +36,7 @@ import androidx.core.graphics.ColorUtils
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.clip.FlorisClipboardManager
import dev.patrickgold.florisboard.ime.clip.provider.ClipboardItem
import dev.patrickgold.florisboard.ime.nlp.SuggestionList
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import dev.patrickgold.florisboard.ime.theme.ThemeValue
@@ -100,7 +101,7 @@ class CandidateView : View, ThemeManager.OnThemeUpdatedListener {
themeManager = ThemeManager.defaultOrNull()
themeManager?.registerOnThemeUpdatedListener(this)
florisClipboardManager = FlorisClipboardManager.getInstanceOrNull()
updateCandidates(candidates)
recomputeCandidates()
}
override fun onDetachedFromWindow() {
@@ -113,7 +114,7 @@ class CandidateView : View, ThemeManager.OnThemeUpdatedListener {
velocityTracker = null
}
fun updateCandidates(newCandidates: List<String>?) {
fun updateCandidates(newCandidates: SuggestionList?) {
candidates.clear()
if (newCandidates != null) {
candidates.addAll(newCandidates)

View File

@@ -17,17 +17,25 @@
package dev.patrickgold.florisboard.ime.text.smartbar
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.util.Size
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InlineSuggestion
import android.widget.inline.InlineContentView
import androidx.annotation.IdRes
import androidx.annotation.RequiresApi
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.children
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.databinding.SmartbarBinding
import dev.patrickgold.florisboard.debug.*
import dev.patrickgold.florisboard.ime.clip.provider.ClipboardItem
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.Preferences
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.nlp.SuggestionList
import dev.patrickgold.florisboard.ime.text.key.KeyVariation
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
import dev.patrickgold.florisboard.ime.theme.Theme
@@ -38,6 +46,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.lang.ref.WeakReference
import java.util.*
import kotlin.math.roundToInt
/**
@@ -64,6 +73,7 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
binding.quickActionToggle.rotation = if (v) 180.0f else 0.0f
field = v
}
private var isShowingInlineSuggestions: Boolean = false
private lateinit var binding: SmartbarBinding
private var indexedActionStartArea: MutableList<Int> = mutableListOf()
@@ -217,7 +227,6 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
* editor instance, etc. Passes the evaluated attributes to [configureFeatureVisibility].
*/
fun updateSmartbarState() {
//binding.clipboardCursorRow.updateVisibility()
binding.candidates.updateDisplaySettings(prefs.suggestion.displayMode, prefs.suggestion.clipboardContentTimeout * 1_000)
when (florisboard) {
null -> configureFeatureVisibility(
@@ -236,21 +245,22 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
KeyboardMode.EDITING -> R.id.back_button
else -> R.id.quick_action_toggle
},
mainAreaId = when (florisboard.textInputManager.keyVariation) {
KeyVariation.PASSWORD -> R.id.number_row
else -> when (isQuickActionsVisible) {
true -> R.id.quick_actions
else -> when (florisboard.textInputManager.getActiveKeyboardMode()) {
KeyboardMode.EDITING -> null
KeyboardMode.NUMERIC,
KeyboardMode.PHONE,
KeyboardMode.PHONE2 -> R.id.clipboard_cursor_row
else -> when {
florisboard.activeEditorInstance.isComposingEnabled &&
florisboard.activeEditorInstance.selection.isCursorMode
-> R.id.candidates
else -> R.id.clipboard_cursor_row
}
mainAreaId = when {
isQuickActionsVisible -> R.id.quick_actions
isShowingInlineSuggestions -> R.id.inline_suggestions
florisboard.textInputManager.keyVariation == KeyVariation.PASSWORD -> {
if (!prefs.keyboard.numberRow) R.id.number_row else null
}
else -> when (florisboard.textInputManager.getActiveKeyboardMode()) {
KeyboardMode.EDITING -> null
KeyboardMode.NUMERIC,
KeyboardMode.PHONE,
KeyboardMode.PHONE2 -> R.id.clipboard_cursor_row
else -> when {
florisboard.activeEditorInstance.isComposingEnabled &&
florisboard.activeEditorInstance.selection.isCursorMode
-> R.id.candidates
else -> R.id.clipboard_cursor_row
}
}
},
@@ -275,7 +285,7 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
}
}
fun setCandidateSuggestionWords(suggestionInitDate: Long, suggestions: List<String>?) {
fun setCandidateSuggestionWords(suggestionInitDate: Long, suggestions: SuggestionList?) {
if (suggestionInitDate > lastSuggestionInitDate) {
lastSuggestionInitDate = suggestionInitDate
binding.candidates.updateCandidates(suggestions)
@@ -286,6 +296,68 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
//
}
/**
* Clears the inline suggestions and triggers thw Smartbar update cycle.
*/
@RequiresApi(Build.VERSION_CODES.R)
fun clearInlineSuggestions() {
updateInlineSuggestionStrip(listOf())
}
/**
* Inflates the given inline suggestions. Once all provided views are ready, the suggestions
* strip is updated and the Smartbar update cycle is triggered.
*
* @param inlineSuggestions A collection of inline suggestions to be inflated and shown.
*/
@RequiresApi(Build.VERSION_CODES.R)
fun showInlineSuggestions(inlineSuggestions: Collection<InlineSuggestion>) {
if (inlineSuggestions.isEmpty()) {
updateInlineSuggestionStrip(listOf())
} else {
val suggestionMap: TreeMap<Int, InlineContentView?> = TreeMap()
for ((i, inlineSuggestion) in inlineSuggestions.withIndex()) {
val size = Size(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
try {
inlineSuggestion.inflate(context, size, context.mainExecutor) { suggestionView ->
flogDebug { "New inline suggestion view ready" }
suggestionMap[i] = suggestionView
if (suggestionMap.size >= inlineSuggestions.size) {
updateInlineSuggestionStrip(suggestionMap.values)
}
}
} catch (e: Throwable) {
flogWarning { "Failed to inflate inline suggestion: $e" }
}
}
}
}
/**
* Updates the suggestion strip with given inline content views and triggers the Smartbar
* update cycle.
*
* @param suggestionViews A collection of inline content views to show.
*/
@RequiresApi(Build.VERSION_CODES.R)
private fun updateInlineSuggestionStrip(suggestionViews: Collection<InlineContentView?>) {
flogDebug { "Updating the inline suggestion strip with ${suggestionViews.size} items" }
binding.inlineSuggestionsStrip.removeAllViews()
if (suggestionViews.isEmpty()) {
isShowingInlineSuggestions = false
return
} else {
for (suggestionView in suggestionViews) {
if (suggestionView == null) {
continue
}
binding.inlineSuggestionsStrip.addView(suggestionView)
}
isShowingInlineSuggestions = true
}
updateSmartbarState()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec).toFloat()

View File

@@ -16,14 +16,26 @@
package dev.patrickgold.florisboard.ime.theme
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.drawable.Icon
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.util.TypedValue
import androidx.annotation.AttrRes
import androidx.annotation.RequiresApi
import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.common.ImageViewStyle
import androidx.autofill.inline.common.TextViewStyle
import androidx.autofill.inline.common.ViewStyle
import androidx.autofill.inline.v1.InlineSuggestionUi
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.Preferences
import dev.patrickgold.florisboard.ime.extension.AssetManager
import dev.patrickgold.florisboard.ime.extension.AssetRef
@@ -305,6 +317,82 @@ class ThemeManager private constructor(
}
}
/**
* Creates a new inline suggestion UI bundle based on the attributes of the given [theme].
*
* @param context The context of the parent view/controller.
* @param theme The theme from which the color attributes should be fetched. Defaults to [activeTheme].
*
* @return A bundle containing all necessary attributes for the inline suggestion views to properly display.
*/
@SuppressLint("RestrictedApi")
@RequiresApi(Build.VERSION_CODES.R)
fun createInlineSuggestionUiStyleBundle(context: Context, theme: Theme = activeTheme): Bundle {
val bgColor = theme.getAttr(Theme.Attr.SMARTBAR_BUTTON_BACKGROUND).toSolidColor().color
val fgColor = theme.getAttr(Theme.Attr.SMARTBAR_BUTTON_FOREGROUND).toSolidColor().color
val bgDrawableId = R.drawable.chip_background
val stylesBuilder = UiVersions.newStylesBuilder()
val style = InlineSuggestionUi.newStyleBuilder()
.setSingleIconChipStyle(
ViewStyle.Builder()
.setBackground(
Icon.createWithResource(context, bgDrawableId).setTint(bgColor)
)
.setPadding(0, 0, 0, 0)
.build()
)
.setChipStyle(
ViewStyle.Builder()
.setBackground(
Icon.createWithResource(context, bgDrawableId).setTint(bgColor)
)
.setPadding(
context.resources.getDimension(R.dimen.suggestion_chip_bg_padding_start).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_bg_padding_top).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_bg_padding_end).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_bg_padding_bottom).toInt(),
)
.build()
)
.setStartIconStyle(
ImageViewStyle.Builder()
.setLayoutMargin(0, 0, 0, 0)
.build()
)
.setTitleStyle(
TextViewStyle.Builder()
.setLayoutMargin(
context.resources.getDimension(R.dimen.suggestion_chip_fg_title_margin_start).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_fg_title_margin_top).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_fg_title_margin_end).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_fg_title_margin_bottom).toInt(),
)
.setTextColor(fgColor)
.setTextSize(16f)
.build()
)
.setSubtitleStyle(
TextViewStyle.Builder()
.setLayoutMargin(
context.resources.getDimension(R.dimen.suggestion_chip_fg_subtitle_margin_start).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_fg_subtitle_margin_top).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_fg_subtitle_margin_end).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_fg_subtitle_margin_bottom).toInt(),
)
.setTextColor(ColorUtils.setAlphaComponent(fgColor, 150))
.setTextSize(14f)
.build()
)
.setEndIconStyle(
ImageViewStyle.Builder()
.setLayoutMargin(0, 0, 0, 0)
.build()
)
.build()
stylesBuilder.addStyle(style)
return stylesBuilder.build()
}
data class RemoteColors(
val packageName: String,
val colorPrimary: ThemeValue.SolidColor?,

View File

@@ -0,0 +1,186 @@
/*
* 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.view
import androidx.annotation.RestrictTo
/**
* A simple helper object managing touch pointer objects. This class is designed to hold
* at max [capacity] at once. It tries to reduce the need to recreate objects and to resize
* arrays by creating a fixed-size list and by reusing pointers. This map supports iterating
* over all active pointers.
*
* @property capacity The capacity of this map, determining the maximum number of pointers this
* map can hold at once. This value must be greater than or equal to one. Should a smaller capacity
* be passed, automatically the minimum capacity `1` is assumed.
* @param init The initializer for each pointer. Note that [Pointer.reset] is called before
* storing the new object, to ensure that this pointer is not initialized with some pointer data.
*/
class PointerMap<P : Pointer>(val capacity: Int = 4, init: (Int) -> P) : Iterable<P> {
/**
* The internal list of pointers, is not intended for public access.
*/
private val pointers: List<P> = List(capacity.coerceAtLeast(1)) { i ->
init(i).also { pointer -> pointer.reset() }
}
/**
* Adds a new pointer with given [id] and [index] and returns it. If this map is already at max
* capacity, null is returned and the pointer could not be added.
*
* @param id The id of the pointer to add.
* @param index The index of the pointer to add.
*
* @return The newly added pointer or null if the map is already full.
*/
fun add(id: Int, index: Int): P? {
for (pointer in pointers) {
if (pointer.isNotUsed) {
pointer.id = id
pointer.index = index
return pointer
}
}
return null
}
/**
* Clears this map and resets all pointers.
*/
fun clear() {
for (pointer in pointers) {
pointer.reset()
}
}
/**
* Finds a pointer by given [id].
*
* @param id The id of the pointer which should be found.
*
* @return The pointer with given [id] or null.
*/
fun findById(id: Int): P? {
for (pointer in pointers) {
if (pointer.id == id) {
return pointer
}
}
return null
}
/**
* Gets a pointer from the internal array based on the internal array index. This method
* is intended to be used only by the [PointerIterator].
*
* @param index
*
* @return The pointer for given index or null, excluding unused pointers.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
fun get(index: Int): P? {
val pointer = pointers.getOrNull(index)
if (pointer != null && pointer.isUsed) {
return pointer
}
return null
}
override fun iterator(): Iterator<P> {
return PointerIterator(this)
}
/**
* Removes a pointer with given [id] and returns a boolean result.
*
* @param id The id of the pointer to remove. If the id is not existent, noting happens.
*
* @return True if a pointer was removed, false otherwise.
*/
fun removeById(id: Int): Boolean {
for (pointer in pointers) {
if (pointer.id == id) {
pointer.reset()
return true
}
}
return false
}
/**
* Returns the size of this map (only counting active pointers). This value is anywhere
* between 0 and [capacity].
*/
val size: Int
get() = pointers.count { it.isUsed }
}
class PointerIterator<P : Pointer>(private val pointerMap: PointerMap<P>) : Iterator<P> {
private var index: Int = 0
override fun hasNext(): Boolean {
do {
if (pointerMap.get(index) != null) {
return true
}
} while (++index < pointerMap.capacity)
return false
}
override fun next(): P {
return pointerMap.get(index++)!!
}
}
/**
* Abstract touch pointer definition.
*/
abstract class Pointer {
companion object {
const val UNUSED_P: Int = -1
}
/**
* The id of this pointer, corresponds to the motion event this pointer originated.
*/
var id: Int = UNUSED_P
/**
* The index of this pointer, corresponds to the motion event this pointer originated.
*/
var index: Int = UNUSED_P
/**
* True if this pointer is used and active, false otherwise.
*/
val isUsed: Boolean
get() = id >= 0
/**
* False if this pointer is used and active, true otherwise.
*/
val isNotUsed: Boolean
get() = !isUsed
/**
* Resets this pointer to be used again.
*/
open fun reset() {
id = UNUSED_P
index = UNUSED_P
}
}

View File

@@ -17,17 +17,23 @@
package dev.patrickgold.florisboard.settings
import android.annotation.SuppressLint
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.MenuItem
import android.webkit.WebView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.databinding.AboutActivityBinding
import dev.patrickgold.florisboard.ime.clip.FlorisClipboardManager
import dev.patrickgold.florisboard.util.AppVersionUtils
import dev.patrickgold.florisboard.util.checkIfImeIsSelected
class AboutActivity : AppCompatActivity() {
private lateinit var binding: AboutActivityBinding
@@ -43,7 +49,22 @@ class AboutActivity : AppCompatActivity() {
setSupportActionBar(toolbar)
// Set app version string
binding.appVersion.text = "v" + AppVersionUtils.getRawVersionName(this)
val appVersion = "v" + AppVersionUtils.getRawVersionName(this)
binding.appVersion.text = appVersion
// Set setOnLongClickListener for copying the version string
binding.headArea.setOnLongClickListener {
val isImeSelected = checkIfImeIsSelected(this)
if (isImeSelected) {
FlorisClipboardManager.getInstance().addNewPlaintext(appVersion)
} else {
val clipboard: ClipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Florisboard version", appVersion)
clipboard.setPrimaryClip(clip)
}
Toast.makeText(this, R.string.about__version_copied__title, Toast.LENGTH_LONG).show()
true
}
// Set onClickListeners for buttons
binding.privacyPolicyButton.setOnClickListener {

View File

@@ -98,6 +98,9 @@ class TypingFragment : SettingsMainActivity.SettingsFragment() {
dialogView.languageSpinner.setOnSelectedListener { pos ->
val selectedCode = subtypeManager.imeConfig.defaultSubtypesLanguageCodes[pos]
val defaultSubtype = subtypeManager.getDefaultSubtypeForLocale(LocaleUtils.stringToLocale(selectedCode)) ?: return@setOnSelectedListener
dialogView.composerSpinner.setSelection(
subtypeManager.imeConfig.composerNames.indexOf(defaultSubtype.composerName).coerceAtLeast(0)
)
dialogView.currencySetSpinner.setSelection(
subtypeManager.imeConfig.currencySetNames.indexOf(defaultSubtype.currencySetName).coerceAtLeast(0)
)
@@ -107,6 +110,9 @@ class TypingFragment : SettingsMainActivity.SettingsFragment() {
)
}
}
dialogView.composerSpinner.initItems(
labels = subtypeManager.imeConfig.composerLabels
)
dialogView.currencySetSpinner.initItems(
labels = subtypeManager.imeConfig.currencySetLabels
)
@@ -129,6 +135,7 @@ class TypingFragment : SettingsMainActivity.SettingsFragment() {
// already exists.
activeDialogWindow?.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener {
val languageCode = subtypeManager.imeConfig.defaultSubtypesLanguageCodes[dialogView.languageSpinner.selectedItemPosition]
val composerName = subtypeManager.imeConfig.composerNames[dialogView.composerSpinner.selectedItemPosition]
val currencySetName = subtypeManager.imeConfig.currencySetNames[dialogView.currencySetSpinner.selectedItemPosition]
val layoutMap = SubtypeLayoutMap(
characters = layoutManager.getMetaNameListFor(LayoutType.CHARACTERS)[dialogView.charactersLayoutSpinner.selectedItemPosition],
@@ -140,7 +147,7 @@ class TypingFragment : SettingsMainActivity.SettingsFragment() {
phone = layoutManager.getMetaNameListFor(LayoutType.PHONE)[dialogView.phoneLayoutSpinner.selectedItemPosition],
phone2 = layoutManager.getMetaNameListFor(LayoutType.PHONE2)[dialogView.phone2LayoutSpinner.selectedItemPosition],
)
val success = subtypeManager.addSubtype(LocaleUtils.stringToLocale(languageCode), currencySetName, layoutMap)
val success = subtypeManager.addSubtype(LocaleUtils.stringToLocale(languageCode), composerName, currencySetName, layoutMap)
if (!success) {
dialogView.errorBox.setText(R.string.settings__localization__subtype_error_already_exists)
dialogView.errorBox.visibility = View.VISIBLE
@@ -161,6 +168,11 @@ class TypingFragment : SettingsMainActivity.SettingsFragment() {
keys = subtypeManager.imeConfig.defaultSubtypesLanguageCodes,
defaultSelectedKey = subtype.locale.toString()
)
dialogView.composerSpinner.initItems(
labels = subtypeManager.imeConfig.composerLabels,
keys = subtypeManager.imeConfig.composerNames,
defaultSelectedKey = subtype.composerName
)
dialogView.currencySetSpinner.initItems(
labels = subtypeManager.imeConfig.currencySetLabels,
keys = subtypeManager.imeConfig.currencySetNames,
@@ -180,6 +192,7 @@ class TypingFragment : SettingsMainActivity.SettingsFragment() {
setView(dialogView.root)
setPositiveButton(R.string.settings__localization__subtype_apply) { _, _ ->
val languageCode = subtypeManager.imeConfig.defaultSubtypesLanguageCodes[dialogView.languageSpinner.selectedItemPosition]
val composerName = subtypeManager.imeConfig.composerNames[dialogView.composerSpinner.selectedItemPosition]
val currencySetName = subtypeManager.imeConfig.currencySetNames[dialogView.currencySetSpinner.selectedItemPosition]
val layoutMap = SubtypeLayoutMap(
characters = layoutManager.getMetaNameListFor(LayoutType.CHARACTERS)[dialogView.charactersLayoutSpinner.selectedItemPosition],
@@ -194,6 +207,7 @@ class TypingFragment : SettingsMainActivity.SettingsFragment() {
subtypeManager.modifySubtypeWithSameId(Subtype(
id = subtype.id,
locale = LocaleUtils.stringToLocale(languageCode),
composerName = composerName,
currencySetName = currencySetName,
layoutMap = layoutMap
))

View File

@@ -17,6 +17,7 @@
package dev.patrickgold.florisboard.settings.fragments
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
@@ -27,6 +28,9 @@ import dev.patrickgold.florisboard.settings.UdmActivity
class TypingInnerFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.prefs_typing)
findPreference<Preference>(Preferences.Suggestion.API30_INLINE_SUGGESTIONS_ENABLED)?.let {
it.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
}
}
override fun onPreferenceTreeClick(preference: Preference?): Boolean {

View File

@@ -25,7 +25,11 @@ object LocaleUtils {
return when {
string.contains(DELIMITER) -> {
val lc = string.split(DELIMITER)
Locale(lc[0], lc[1])
if (lc.size == 3) {
Locale(lc[0], lc[1], lc[2])
} else {
Locale(lc[0], lc[1])
}
}
else -> {
Locale(string)

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 2020 The Android Open Source Project
~ 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.
-->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#20000000">
<item
android:bottom="4dp"
android:left="4dp"
android:right="4dp"
android:shape="rectangle"
android:top="4dp">
<shape>
<corners android:radius="32dp" />
<solid android:color="#1F000000" />
</shape>
</item>
<item
android:bottom="5dp"
android:left="5dp"
android:right="5dp"
android:shape="rectangle"
android:top="5dp">
<shape>
<corners android:radius="32dp" />
<solid android:color="#FFFFFFFF" />
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/colorPrimary">
<item android:id="@android:id/background">
<shape android:shape="rectangle">
<corners android:radius="15dp" />
<solid android:color="@color/windowBackground" />
</shape>
</item>
</ripple>

View File

@@ -18,30 +18,39 @@
android:orientation="vertical"
android:gravity="center_horizontal">
<ImageView
android:id="@+id/app_icon"
android:background="@mipmap/floris_app_icon"
<LinearLayout
android:id="@+id/head_area"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginBottom="8dp"
android:elevation="8dp"
android:contentDescription="@string/about__app_icon_content_description"/>
android:gravity="center_horizontal"
android:orientation="vertical"
android:background="@drawable/ripple_click">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/floris_app_name"
android:textSize="24sp"
android:textStyle="bold"/>
<ImageView
android:id="@+id/app_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginBottom="8dp"
android:background="@mipmap/floris_app_icon"
android:contentDescription="@string/about__app_icon_content_description"
android:elevation="8dp" />
<TextView
android:id="@+id/app_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="16dp"
android:textSize="16sp"
android:textStyle="italic"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/floris_app_name"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:id="@+id/app_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="16dp"
android:textSize="16sp"
android:textStyle="italic" />
</LinearLayout>
<Button
android:id="@+id/license_button"

View File

@@ -42,6 +42,26 @@
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingVertical="8dp">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="8dp"
android:text="@string/settings__localization__subtype_composer" />
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/composer_spinner"
android:layout_width="match_parent"
android:layout_height="48dp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -112,8 +132,8 @@
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/settings__localization__subtype_currency_set"
android:paddingHorizontal="8dp"/>
android:paddingHorizontal="8dp"
android:text="@string/settings__localization__subtype_currency_set" />
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/currency_set_spinner"

View File

@@ -43,6 +43,18 @@
android:id="@+id/candidates"
style="@style/SmartbarContainer"/>
<HorizontalScrollView
android:id="@+id/inline_suggestions"
style="@style/SmartbarContainer">
<LinearLayout
android:id="@+id/inline_suggestions_strip"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"/>
</HorizontalScrollView>
<LinearLayout
android:id="@+id/quick_actions"
style="@style/SmartbarContainer">

View File

@@ -42,7 +42,7 @@
<string name="settings__menu" comment="Hint of top-right three-dot icon in Settings">المزيد من الخيارات</string>
<string name="settings__menu_help" comment="Three-dot menu entry for Help and Feedback web link">المساعدة والملاحظات</string>
<string name="settings__help" comment="General label for help buttons in Settings">مساعدة</string>
<string name="settings__navigation__home" comment="Long-press hint of bottom nav item Home in Settings">الصفحة الرئيسية</string>
<string name="settings__navigation__home" comment="Long-press hint of bottom nav item Home in Settings">الرئيسية</string>
<string name="settings__navigation__keyboard" comment="Long-press hint of bottom nav item Keyboard in Settings">لوحة المفاتيح</string>
<string name="settings__navigation__typing" comment="Long-press hint of bottom nav item Typing in Settings">الكتابة</string>
<string name="settings__navigation__theme" comment="Long-press hint of bottom nav item Theme in Settings">المظهر</string>
@@ -52,7 +52,7 @@
<string name="settings__home__title" comment="Title of the Home fragment">مرحبا بكم في %s</string>
<string name="settings__home__ime_not_enabled" comment="Error message shown in Home fragment when FlorisBoard is not enabled in the system">لم يتم تمكين FlorisBoard في النظام وبالتالي لن يكون متاحًا كطريقة إدخال في منتقي الإدخال. انقر هنا لحل هذه المشكلة.</string>
<string name="settings__home__ime_not_selected" comment="Warning message shown in Home fragment when FlorisBoard is not selected as the default keyboard">لم يتم اختيار FlorisBoard كطريقة الإدخال الافتراضية. انقر هنا لحل هذه المشكلة.</string>
<string name="settings__home__contribute" comment="Contributing message shown in Home fragment">شكرا على تجربة FlorisBoard! هذا المشروع لا يزال في قيد التطوير وبالتالي يفتقد بعض الميزات. إذا وجدت أي أخطاء أو تريد تقديم اقتراح ، فالرجاء مراجعة المشروع على GitHub وطرح مشكلتك. هذا يساعد في جعل FlorisBoard أفضل. شكرا جزيلا!</string>
<string name="settings__home__contribute" comment="Contributing message shown in Home fragment">شكرا على تجربة FlorisBoard! هذا المشروع لا يزال قيد التطوير وبالتالي يفتقد بعض الميزات. إذا وجدت أي أخطاء أو تريد تقديم اقتراح ، فالرجاء مراجعة المشروع على GitHub وطرح مشكلتك. هذا يساعد في جعل FlorisBoard أفضل. شكرا جزيلا!</string>
<string name="settings__localization__title" comment="Title of languages and layout box in the Typing fragment">اللغات وتخطيطات لوحة المفاتيح</string>
<string name="settings__localization__subtype_no_subtypes_configured_warning" comment="Warning message that no subtype has been defined in the Typing fragment">يبدو أنك لم تقم بإختيار أية أنواع فرعية. كبديل، سيتم استخدام النوع الفرعي English / QWERTY!</string>
<string name="settings__localization__subtype_add" comment="Subtype dialog add button">إضافة</string>
@@ -102,9 +102,9 @@
<string name="settings__theme_editor__type_label" comment="Label of type input">نوع</string>
<string name="settings__theme_editor__add_group_dialog_title" comment="Title of the add group dialog in the theme editor">إضافة مجموعة</string>
<string name="settings__theme_editor__edit_group_dialog_title" comment="Title of the edit group dialog in the theme editor">تحرير المجموعة</string>
<string name="settings__theme_editor__add_attr_dialog_title" comment="Title of the add attribute dialog in the theme editor">اضافة الخاصيه</string>
<string name="settings__theme_editor__add_attr_dialog_title" comment="Title of the add attribute dialog in the theme editor">إضافة خاصية</string>
<string name="settings__theme_editor__edit_attr_dialog_title" comment="Title of the edit attribute dialog in the theme editor">تحرير الصفة</string>
<string name="settings__theme_editor__edit_theme_name_dialog_title" comment="Title of the edit theme name dialog in the theme editor">تحرير اسم الثيم</string>
<string name="settings__theme_editor__edit_theme_name_dialog_title" comment="Title of the edit theme name dialog in the theme editor">تحرير اسم السمة</string>
<string name="settings__theme_editor__value_type_reference" comment="Theme value type">المرجع</string>
<string name="settings__theme_editor__value_type_reference_group" comment="Theme value type sub-field">مجموعة</string>
<string name="settings__theme_editor__value_type_reference_attr" comment="Theme value type sub-field">الخاصية</string>
@@ -115,8 +115,8 @@
<string name="settings__theme_editor__value_type_on_off_state" comment="Theme value type sub-field">الحالة</string>
<string name="settings__theme_editor__value_type_other" comment="Theme value type">أخرى</string>
<string name="settings__theme_editor__value_type_other_text" comment="Theme value type sub-field">النص</string>
<string name="settings__theme_editor__value_preview_content_description" comment="Theme value preview content description">معاينو قيم الثيم</string>
<string name="settings__theme_editor__error_theme_label_empty" comment="Error text for an empty theme label">يرجى ادخال اسم المظهر.</string>
<string name="settings__theme_editor__value_preview_content_description" comment="Theme value preview content description">معاينة قيم السمة</string>
<string name="settings__theme_editor__error_theme_label_empty" comment="Error text for an empty theme label">يرجى إدخال اسم المظهر.</string>
<string name="settings__theme_editor__error_group_name" comment="Error text for an invalid group name">الرجاء إدخال اسم مجموعة يحتوي فقط على أحرف (a-z و / أو A-Z) ، والنقطتان (:) للتجميع الفرعي أو بالإضافة إلى الأرقام (09) ، والمده (~) والتسطير (_) للمفتاح ضع الكلمة المناسبة.</string>
<string name="settings__theme_editor__error_group_name_empty" comment="Error text for an empty group name">الرجاء إدخال اسم المجموعة.</string>
<string name="settings__theme_editor__error_group_name_already_exists" comment="Error text for a duplicate group name">اسم هذه المجموعة موجود بالفعل في هذا الموضوع. الرجاء اختيار واحد آخر.</string>
@@ -156,8 +156,8 @@
<string name="pref__keyboard__group_keys__label" comment="Preference group title">المفاتيح</string>
<string name="pref__keyboard__number_row__label" comment="Preference title">صف الأعداد</string>
<string name="pref__keyboard__number_row__summary" comment="Preference summary">إظهار صف الأعداد فوق تخطيط الحروف</string>
<string name="pref__keyboard__hinted_number_row_mode__label" comment="Preference title">تلميح لصف الأعداد</string>
<string name="pref__keyboard__hinted_symbols_mode__label" comment="Preference title">تلميح للرموز</string>
<string name="pref__keyboard__hinted_number_row_mode__label" comment="Preference title">تلميحات صف الأعداد</string>
<string name="pref__keyboard__hinted_symbols_mode__label" comment="Preference title">تلميحات للرموز</string>
<string name="pref__keyboard__hint_mode__disabled" comment="Preference value">معطّل</string>
<string name="pref__keyboard__hint_mode__enabled_hint_priority" comment="Preference value">مفعّل (الأولوية للتلميح)</string>
<string name="pref__keyboard__hint_mode__enabled_accent_priority" comment="Preference value">مفعّل (الأولوية للحروف الإضافية)</string>
@@ -169,9 +169,11 @@
<string name="pref__keyboard__utility_key_action__switch_language" comment="Preference value">تغيير اللغة</string>
<string name="pref__keyboard__utility_key_action__switch_keyboard_app" comment="Preference value">تبديل تطبيق لوحة المفاتيح</string>
<string name="pref__keyboard__utility_key_action__dynamic_switch_language_emojis" comment="Preference value">ديناميكي: قم بالتبديل إلى الرموز التعبيرية / تبديل اللغة</string>
<string name="pref__keyboard__merge_hint_popups_enabled__label">عرض ايضا الرموز المنبثقة</string>
<string name="pref__keyboard_merge_hint_popups_enabled__summary">أضف النوافذ المنبثقة للرمز إلى التخطيط الافتراضي</string>
<string name="pref__keyboard__font_size_multiplier_portrait__label" comment="Preference title">مضاعف حجم الخط (عمودي)</string>
<string name="pref__keyboard__font_size_multiplier_landscape__label" comment="Preference title">مضاعف حجم الخط (أفقي)</string>
<string name="pref__keyboard__group_layout__label" comment="Preference group title">نظام التخطيط</string>
<string name="pref__keyboard__group_layout__label" comment="Preference group title">التخطيط</string>
<string name="pref__keyboard__one_handed_mode__label" comment="Preference title">وضع اليد الواحدة</string>
<string name="pref__keyboard__one_handed_mode__off" comment="Preference value">إيقاف</string>
<string name="pref__keyboard__one_handed_mode__right" comment="Preference value">وضع اليد اليمنى</string>
@@ -190,7 +192,7 @@
<string name="pref__keyboard__height_factor__tall" comment="Preference value">طويل</string>
<string name="pref__keyboard__height_factor__extra_tall" comment="Preference value">طويل جدا</string>
<string name="pref__keyboard__height_factor__custom" comment="Preference value">مخصص</string>
<string name="pref__keyboard__height_factor_custom__label" comment="Preference title">قيمة ارتفاع لوحة المفاتيح المخصصة</string>
<string name="pref__keyboard__height_factor_custom__label" comment="Preference title">قيمة مخصصة لأرتفاع لوحة المفاتيح</string>
<string name="pref__keyboard__bottom_offset_portrait__label" comment="Preference title">الإزاحة السفلية (عمودي)</string>
<string name="pref__keyboard__bottom_offset_landscape__label" comment="Preference title">الإزاحة السفلية (عمودي)</string>
<string name="pref__keyboard__key_spacing_vertical__label" comment="Preference title">تباعد المفاتيح (عموديًا)</string>
@@ -199,8 +201,8 @@
<string name="pref__keyboard__sound_enabled__label" comment="Preference title">الصوت عند ضغط المفتاح</string>
<string name="pref__keyboard__sound_volume__label" comment="Preference title">حجم الصوت عند ضغط المفتاح</string>
<string name="pref__keyboard__vibration_enabled__label" comment="Preference title">إهتزاز عند ضغط المفتاح</string>
<string name="pref__keyboard__vibration_duration__label" comment="Preference title">إهتزاز عند الضغط على الزر</string>
<string name="pref__keyboard__vibration_strength__label" comment="Preference title">شدة الإهتزاز عند ضغط المفتاح</string>
<string name="pref__keyboard__vibration_duration__label" comment="Preference title">اهتزاز عند الضغط على المفتاح</string>
<string name="pref__keyboard__vibration_strength__label" comment="Preference title">شدة الاهتزاز عند ضغط على المفتاح</string>
<string name="pref__keyboard__popup_visible__label" comment="Preference title">رؤية النافذة المنبثقة</string>
<string name="pref__keyboard__popup_visible__summary" comment="Preference summary">إظهار النافذة المنبثقة عندما تضغط على مفتاح</string>
<string name="pref__keyboard__long_press_delay__label" comment="Preference title">مدة الضغط المطوّل على المفتاح</string>
@@ -275,7 +277,7 @@
<string name="pref__gestures__swipe_action__shift" comment="Preference value for swipe action">مفتاح التحويل</string>
<string name="pref__gestures__swipe_action__redo" comment="Preference value for swipe action">إعادة</string>
<string name="pref__gestures__swipe_action__undo" comment="Preference value for swipe action">تراجع</string>
<string name="pref__gestures__swipe_action__show_input_method_picker" comment="Preference value for swipe action">إظهار طريقة الإدخال</string>
<string name="pref__gestures__swipe_action__show_input_method_picker" comment="Preference value for swipe action">إظهار مبدل طريقة الإدخال</string>
<string name="pref__gestures__swipe_action__switch_to_prev_keyboard" comment="Preference value for swipe action">التبديل إلى لوحة المفاتيح السابقة</string>
<string name="pref__gestures__swipe_action__switch_to_prev_subtype" comment="Preference value for swipe action">التبديل إلى النوع الفرعي السابق</string>
<string name="pref__gestures__swipe_action__switch_to_next_subtype" comment="Preference value for swipe action">التبديل إلى النوع الفرعي التالي</string>
@@ -286,7 +288,7 @@
<string name="pref__gestures__space_bar_swipe_up__label" comment="Preference title">السحب للفوق من خلال مفتاح المسافة</string>
<string name="pref__gestures__space_bar_swipe_left__label" comment="Preference title">السحب لليسار من خلال مفتاح المسافة</string>
<string name="pref__gestures__space_bar_swipe_right__label" comment="Preference title">السحب لليمين من خلال مفتاح المسافة</string>
<string name="pref__gestures__space_bar_long_press__label" comment="Preference title">الضغط المطول على شريط المسافه</string>
<string name="pref__gestures__space_bar_long_press__label" comment="Preference title">الضغط المطول على شريط المسافة</string>
<string name="pref__gestures__delete_key_swipe_left__label" comment="Preference title">السحب لليسار من خلال مفتاح الحذف</string>
<string name="pref__gestures__swipe_velocity_threshold__label" comment="Preference title">عتبة سرعة السحب</string>
<string name="pref__gestures__swipe_velocity_threshold__very_slow" comment="Preference value for swipe velocity threshold">بطيئة جداً</string>
@@ -304,13 +306,13 @@
<string name="pref__advanced__settings_theme__label" comment="Label of Settings theme preference in Advanced">إعدادات المظهر</string>
<string name="pref__advanced__settings_theme__light" comment="Possible value of Settings theme preference in Advanced">فاتح</string>
<string name="pref__advanced__settings_theme__dark" comment="Possible value of Settings theme preference in Advanced">داكن</string>
<string name="pref__advanced__show_app_icon__label" comment="Label of Show app icon preference in Advanced">إظهار أيقونة البرنامج في صفحة الهاتف الرئيسية</string>
<string name="pref__advanced__show_app_icon__label" comment="Label of Show app icon preference in Advanced">إظهار أيقونة البرنامج في درج التطبيقات</string>
<string name="pref__advanced__force_private_mode__label" comment="Label of Force private mode preference in Advanced">فرض الوضع الخاص</string>
<string name="pref__advanced__force_private_mode__summary" comment="Summary of Force private mode preference in Advanced">سيتم تعطيل أي ميزات تعمل مؤقتًا باستعمال بيانات الإدخال الخاصة بك</string>
<string name="pref__devtools__enabled__label" comment="Label of Enable developer tools in Advanced">تفعيل أدوات المطور</string>
<string name="pref__devtools__enabled__summary" comment="Summary of Enable developer tools in Advanced">أدوات مصممة خصيصًا لتصحيح الأخطاء واستكشاف العلل وإصلاحها</string>
<string name="pref__devtools__show_heap_memory_stats__label" comment="Label of Show heap memory stats in Advanced">إظهار إحصائيات كومة الذاكرة</string>
<string name="pref__devtools__show_heap_memory_stats__summary" comment="Summary of Show heap memory stats in Advanced">يعرض طبقة تراكبية في الزاوية العلوية اليمنى لاستخدام ذاكرة الكومة والحجم الأقصى لها</string>
<string name="pref__devtools__show_heap_memory_stats__summary" comment="Summary of Show heap memory stats in Advanced">يعرض طبقة تراكبية في الزاوية العلوية اليمنى على لوحة المفاتيح لاستخدام ذاكرة الكومة والحجم الأقصى لها</string>
<string name="pref__devtools__clear_udm_internal_database__label" comment="Label of Clear internal user dictionary database in Advanced">مسح قاعدة بيانات القاموس الداخلي للمستخدم</string>
<string name="pref__devtools__clear_udm_internal_database__summary" comment="Summary of Clear internal user dictionary database in Advanced">يمسح كل الكلمات من جدول قاعدة بيانات القاموس</string>
<!-- About UI strings -->
@@ -341,7 +343,7 @@
<string name="assets__action__delete_confirm_message">هل أنت متأكد من حذف \"%s\"؟ لا يمكن التراجع عن هذا الإجراء.</string>
<string name="assets__action__edit">تعديل</string>
<string name="assets__action__export">تصدير</string>
<string name="assets__action__import">إستيراد</string>
<string name="assets__action__import">استيراد</string>
<string name="assets__action__no">كلا</string>
<string name="assets__action__save">حفظ</string>
<string name="assets__action__yes">موافق</string>
@@ -361,7 +363,7 @@
<string name="setup__welcome__trust" comment="Paragraph in Welcome fragment in Setup">مصدر التعليمات البرمجية لـ FlorisBoard متاح للجميع ، لذا يمكنك بسهولة مراجعة ما يفعله FlorisBoard في الخلفية. يرجى مراجعة الرابط الخاص بالمستودع أدناه.</string>
<string name="setup__welcome__contribute" comment="Paragraph in Welcome fragment in Setup">شيء أخير قبل بدء الإعداد - إذا واجهت أي أخطاء / أعطال / مشكلات في FlorisBoard أو كان لديك طلب ميزة - توجه إلى مستودع GitHub الموجود في الرابط أدناه واطرح مشكلة. هذا يساعد في تحسين التجربة لجميع المستخدمين!</string>
<string name="setup__welcome__outro" comment="Paragraph in Welcome fragment in Setup">لبدء الإعداد إضغط على <i>التالي</i>.</string>
<string name="setup__enable_ime__title" comment="Title of Enable IME fragment in Setup">تفعيل FlorisBoard</string>
<string name="setup__enable_ime__title" comment="Title of Enable IME fragment in Setup">قم بتفعيل FlorisBoard</string>
<string name="setup__enable_ime__text_before_enabled" comment="Description of state in Enable IME fragment before user enabled">يتطلب Android تمكين كل لوحة مفاتيح مخصصة يدويًا قبل أن تتمكن من استخدامها. إضغط على الزر أدناه للتوجه إلى <i>اللغات و الإدخال</i> الإعدادات, ثم تأكد من إختيار \'<i>FlorisBoard</i>\'.</string>
<string name="setup__enable_ime__text_after_enabled" comment="Description of state in Enable IME fragment after user enabled">تم تفعيل FlorisBoard بنجاح. للمواصلة إضغط على <i>التالي</i>!</string>
<string name="setup__enable_ime__text_button_language_and_input" comment="Label of language and input button in Enable IME fragment">فتح إعدادات اللغات و الإدخال</string>

View File

@@ -212,9 +212,13 @@
<string name="pref__suggestion__title" comment="Preference group title">Предложения</string>
<string name="pref__dictionary__title" comment="Preference group title">Речник</string>
<string name="pref__dictionary__enable_system_user_dictionary__label" comment="Preference title">Използване на системния потребителски речник</string>
<string name="pref__dictionary__enable_system_user_dictionary__summary" comment="Preference summary">Предлагат се думи от системния потребителски речник</string>
<string name="pref__dictionary__manage_system_user_dictionary__label" comment="Preference title">Управление на системния потребителски речник</string>
<string name="pref__dictionary__enable_internal_user_dictionary__label" comment="Preference title">Използване на вътрешния потребителски речник</string>
<string name="pref__dictionary__manage_floris_user_dictionary__label" comment="Preference title">Управление на вътрешния потребителски речник</string>
<string name="pref__dictionary__manage_system_user_dictionary__summary" comment="Preference summary">Преглед, добавяне и премахване на думи от системния потребителски речник</string>
<string name="pref__dictionary__enable_internal_user_dictionary__label" comment="Preference title">Използване на потребителския речник към приложението</string>
<string name="pref__dictionary__enable_internal_user_dictionary__summary" comment="Preference summary">Предлагат се думи от потребителския речник към приложението</string>
<string name="pref__dictionary__manage_floris_user_dictionary__label" comment="Preference title">Управление на потребителския речник към приложението</string>
<string name="pref__dictionary__manage_floris_user_dictionary__summary" comment="Preference summary">Преглед, добавяне и премахване на думи от потребителския речник към приложението</string>
<string name="pref__correction__title" comment="Preference group title">Корекции</string>
<string name="pref__correction__auto_capitalization__label" comment="Preference title">Автоматични главни букви</string>
<string name="pref__correction__auto_capitalization__summary" comment="Preference summary">Думите се изписват с главни букви въз основа на текущия контекст</string>
@@ -222,11 +226,12 @@
<string name="pref__correction__remember_caps_lock_state__summary" comment="Preference summary">Клавишът Caps Lock да остане включен при преместване в друго текстово поле</string>
<string name="pref__correction__double_space_period__label" comment="Preference title">Точка при двоен интервал</string>
<string name="pref__correction__double_space_period__summary" comment="Preference summary">Двукратното докосване на интервала вмъква точка, последвана от интервал</string>
<string name="settings__udm__title_floris" comment="Title of the User Dictionary Manager activity for internal">Вътрешен потребителски речник</string>
<string name="settings__udm__title_floris" comment="Title of the User Dictionary Manager activity for internal">Потребителски речник към приложението</string>
<string name="settings__udm__title_system" comment="Title of the User Dictionary Manager activity for system">Системен потребителски речник</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.">Честота: %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.">Честота: %d | ключова дума: %s</string>
<string name="settings__udm__all_languages" comment="Label of the For all languages entry in the language list">За всички езици</string>
<string name="settings__udm__open_system_manager_ui" comment="Label of the Open system manager UI menu option">Отваряне на системни настройки</string>
<string name="settings__udm__dictionary_import_success" comment="Message for dictionary import success">Потребителският речник е изнесен!</string>
<string name="settings__udm__dictionary_export_success" comment="Message for dictionary export success">Потребителският речник е внесен!</string>
<string name="settings__udm__dialog__title_add" comment="Label for the title (when in adding mode) in the user dictionary add/edit dialog">Добавяне на дума</string>
@@ -240,6 +245,7 @@
<string name="settings__udm__dialog__shortcut_label" comment="Label for the shortcut in the user dictionary add/edit dialog">Ключова дума (по избор)</string>
<string name="settings__udm__dialog__shortcut_error_invalid" comment="Error label for the shortcut in the user dictionary add/edit dialog">Ключовата дума съдържа недействителни знаци.</string>
<string name="settings__udm__dialog__locale_label" comment="Label for the language code in the user dictionary add/edit dialog">Код на езика (по избор)</string>
<string name="settings__udm__dialog__locale_error_invalid" comment="Error label for the language code in the user dictionary add/edit dialog">Кодът на езика не отговаря на синтаксиса. Трябва да бъде или само език (bg), или език и държава (bg_BG), или език, държава и азбука (bg_BG-cyrillic).</string>
<string name="settings__gestures__title" comment="Title of Gestures fragment">Жестове и писане чрез плъзгане</string>
<string name="pref__glide__title" comment="Preference group title">Въвеждане чрез плъзгане</string>
<string name="pref__glide__enabled__label" comment="Preference title">Въвеждане чрез плъзгане</string>
@@ -305,7 +311,7 @@
<string name="pref__devtools__enabled__summary" comment="Summary of Enable developer tools in Advanced">Инструменти, в помощ при за отстраняване на дефекти</string>
<string name="pref__devtools__show_heap_memory_stats__label" comment="Label of Show heap memory stats in Advanced">Статистика за динамичната памет</string>
<string name="pref__devtools__show_heap_memory_stats__summary" comment="Summary of Show heap memory stats in Advanced">Показва се информация за динамичната памет в горния десен ъгъл</string>
<string name="pref__devtools__clear_udm_internal_database__label" comment="Label of Clear internal user dictionary database in Advanced">Изтриване на данните за вътрешния речник</string>
<string name="pref__devtools__clear_udm_internal_database__label" comment="Label of Clear internal user dictionary database in Advanced">Изтриване на данните от речника към приложението</string>
<string name="pref__devtools__clear_udm_internal_database__summary" comment="Summary of Clear internal user dictionary database in Advanced">Премахва всички думи от таблицата с речника</string>
<!-- About UI strings -->
<string name="about__title" comment="Title of About activity">Относно</string>

View File

@@ -210,6 +210,15 @@
<string name="pref__smartbar__enabled__label" comment="Preference title">Schnellzugriffsleiste einschalten</string>
<string name="pref__smartbar__enabled__summary" comment="Preference summary">Wird über der Tastatur angezeigt</string>
<string name="pref__suggestion__title" comment="Preference group title">Vorschläge</string>
<string name="pref__dictionary__title" comment="Preference group title">Wörterbuch</string>
<string name="pref__dictionary__enable_system_user_dictionary__label" comment="Preference title">System-Wörterbuch aktivieren</string>
<string name="pref__dictionary__enable_system_user_dictionary__summary" comment="Preference summary">Wörter vorschlagen, die im System-Benutzerwörterbuch gespeichert sind</string>
<string name="pref__dictionary__manage_system_user_dictionary__label" comment="Preference title">System-Benutzerwörterbuch verwalten</string>
<string name="pref__dictionary__manage_system_user_dictionary__summary" comment="Preference summary">Hinzufügen, ansehen und bearbeiten von Einträgen für das System-Benutzerwörterbuch</string>
<string name="pref__dictionary__enable_internal_user_dictionary__label" comment="Preference title">Internes Wörterbuch aktivieren</string>
<string name="pref__dictionary__enable_internal_user_dictionary__summary" comment="Preference summary">Wörter vorschlagen, die im internen Benutzerwörterbuch gespeichert sind</string>
<string name="pref__dictionary__manage_floris_user_dictionary__label" comment="Preference title">Internes Benutzerwörterbuch verwalten</string>
<string name="pref__dictionary__manage_floris_user_dictionary__summary" comment="Preference summary">Hinzufügen, ansehen und bearbeiten von Einträgen für das interne Benutzerwörterbuch</string>
<string name="pref__correction__title" comment="Preference group title">Korrekturen</string>
<string name="pref__correction__auto_capitalization__label" comment="Preference title">Autom. Groß-/Kleinschreibung</string>
<string name="pref__correction__auto_capitalization__summary" comment="Preference summary">Automatisches Großschreiben je nach aktuellem Kontext</string>
@@ -217,6 +226,14 @@
<string name="pref__correction__remember_caps_lock_state__summary" comment="Preference summary">Die festgestellte Umschalttaste bleibt auch beim Wechsel in ein anderes Textfeld aktiviert</string>
<string name="pref__correction__double_space_period__label" comment="Preference title">Doppeltes Leerzeichen durch Punkt ersetzen</string>
<string name="pref__correction__double_space_period__summary" comment="Preference summary">Doppeltes Tippen auf die Leertaste fügt Punkt und ein Leerzeichen ein</string>
<string name="settings__udm__title_floris" comment="Title of the User Dictionary Manager activity for internal">Internes Benutzerwörterbuch</string>
<string name="settings__udm__title_system" comment="Title of the User Dictionary Manager activity for system">System-Benutzerwörterbuch</string>
<string name="settings__udm__all_languages" comment="Label of the For all languages entry in the language list">Für alle Sprachen</string>
<string name="settings__udm__dictionary_import_success" comment="Message for dictionary import success">Benutzerwörterbuch erfolgreich importiert!</string>
<string name="settings__udm__dictionary_export_success" comment="Message for dictionary export success">Benutzerwörterbuch erfolgreich exportiert!</string>
<string name="settings__udm__dialog__word_label" comment="Label for the word in the user dictionary add/edit dialog">Wort</string>
<string name="settings__udm__dialog__word_error_invalid" comment="Error label for the word in the user dictionary add/edit dialog">Dieses Wort enthält ungültige Zeichen.</string>
<string name="settings__udm__dialog__freq_error_invalid" comment="Error label for the frequency in the user dictionary add/edit dialog">Bitte eine gültige Zahl innerhalb der Grenzen eintragen!</string>
<string name="settings__gestures__title" comment="Title of Gestures fragment">Gesten &amp; Glide Typing</string>
<string name="pref__glide__title" comment="Preference group title">Glide Typing</string>
<string name="pref__glide__enabled__label" comment="Preference title">Glide Typing aktivieren</string>
@@ -279,6 +296,7 @@
<string name="pref__advanced__force_private_mode__label" comment="Label of Force private mode preference in Advanced">Privaten Modus erzwingen</string>
<string name="pref__advanced__force_private_mode__summary" comment="Summary of Force private mode preference in Advanced">Wird alle Funktionen deaktivieren, die vorübergehend mit deinen Eingabedaten arbeiten</string>
<string name="pref__devtools__enabled__label" comment="Label of Enable developer tools in Advanced">Entwickler-Werkzeuge ein/aus</string>
<string name="pref__devtools__clear_udm_internal_database__label" comment="Label of Clear internal user dictionary database in Advanced">Interne Benutzerwörterbuch-Datenbank leeren</string>
<!-- About UI strings -->
<string name="about__title" comment="Title of About activity">Über</string>
<string name="about__app_icon_content_description" comment="Content description of app icon in About">App-Icon von FlorisBoard</string>

View File

@@ -65,6 +65,7 @@
<string name="settings__localization__subtype_characters_layout" comment="Label for layout dropdown in subtype dialog">Διάταξη χαρακτήρων</string>
<string name="settings__localization__subtype_symbols_layout" comment="Label for layout dropdown in subtype dialog">Πρωτεύουσα διάταξη συμβόλων</string>
<string name="settings__localization__subtype_symbols2_layout" comment="Label for layout dropdown in subtype dialog">Δευτερεύουσα διάταξη συμβόλων</string>
<string name="settings__localization__subtype_currency_set" comment="Label for currency set dropdown in subtype dialog. 'set' is used as a noun here and can be compared to a group of elements (in this case currency symbols).">Σύνολο συναλλαγμάτων</string>
<string name="settings__localization__subtype_numeric_layout" comment="Label for layout dropdown in subtype dialog">Αριθμητική διάταξη</string>
<string name="settings__localization__subtype_numeric_advanced_layout" comment="Label for layout dropdown in subtype dialog">(Προχωρημένη) αριθμητική διάταξη</string>
<string name="settings__localization__subtype_numeric_row_layout" comment="Label for layout dropdown in subtype dialog">Διάταξη σειράς αριθμών</string>
@@ -85,6 +86,7 @@
<string name="pref__theme__any_theme__label" comment="Label of the theme selector preference">Επιλεγμένο θέμα</string>
<string name="pref__theme__any_theme_adapt_to_app__label" comment="Label of the theme adapt to app preference">Προσαρμογή χρωμάτων στην εφαρμογή</string>
<string name="pref__theme__any_theme_adapt_to_app__summary" comment="Summary of the theme adapt to app preference">Τα χρώματα του θέματος προσαρμόζονται σε αυτά της τρέχουσας εφαρμογής, εάν αυτή η εφαρμογή το υποστηρίζει.</string>
<string name="pref__theme__source_assets" comment="Label for the theme source field">Πηγές της Εφαρμογής FlorisBoard</string>
<string name="pref__theme__source_internal" comment="Label for the theme source field">Εσωτερικός αποθηκευτικός χώρος</string>
<string name="pref__theme__source_external" comment="Label for the theme source field">Εξωτερικός Προμηθευτής</string>
<string name="settings__theme_manager__title_day" comment="Title of the theme manager activity for day theme">Διαχειριστής Θέματος (Μέρα)</string>
@@ -107,6 +109,8 @@
<string name="settings__theme_editor__value_type_reference_group" comment="Theme value type sub-field">Ομάδα</string>
<string name="settings__theme_editor__value_type_reference_attr" comment="Theme value type sub-field">Χαρακτηριστικό</string>
<string name="settings__theme_editor__value_type_solid_color" comment="Theme value type">Συμπαγές χρώμα</string>
<string name="settings__theme_editor__value_type_lin_grad" comment="Theme value type">Γραμμικό ντεγκραντέ</string>
<string name="settings__theme_editor__value_type_rad_grad" comment="Theme value type">Ακτινικό ντεγκραντέ</string>
<string name="settings__theme_editor__value_type_on_off" comment="Theme value type">Εναλλαγή</string>
<string name="settings__theme_editor__value_type_on_off_state" comment="Theme value type sub-field">Κατάσταση</string>
<string name="settings__theme_editor__value_type_other" comment="Theme value type">Άλλο</string>
@@ -131,6 +135,7 @@
<string name="settings__theme__group_smartbarButton" comment="Theme group label">Πλήκτρο έξυπνης μπάρας</string>
<string name="settings__theme__group_extractEditLayout" comment="Theme group label">Διάταξη πλήρους οθόνης τοπίου</string>
<string name="settings__theme__group_extractActionButton" comment="Theme group label">Πλήκτρο δράσης πλήρους οθόνης τοπίου</string>
<string name="settings__theme__group_glideTrail" comment="Theme group label">Διαδρομή ολίσθησης</string>
<string name="settings__theme__group_custom" comment="Theme group label (%s is custom group name)">Προσαρμοσμένη ομάδα (%s)</string>
<string name="settings__theme__attr_background" comment="Theme attribute label">Χρώμα φόντου</string>
<string name="settings__theme__attr_backgroundActive" comment="Theme attribute label">Χρώμα φόντου (ενεργό)</string>
@@ -157,10 +162,15 @@
<string name="pref__keyboard__hint_mode__enabled_hint_priority" comment="Preference value">Ενεργοποιημένο (Προτεραιότητα των υπονοούμενων)</string>
<string name="pref__keyboard__hint_mode__enabled_accent_priority" comment="Preference value">Ενεργοποιημένο (Προτεραιότητα του τονισμού)</string>
<string name="pref__keyboard__hint_mode__enabled_smart_priority" comment="Preference value">Ενεργοποιημένο (Έξυπνη προτεραιοποίηση)</string>
<string name="pref__keyboard__utility_key_enabled__label" comment="Preference title">Εμφάνιση πλήκτρου χρηστικότητας</string>
<string name="pref__keyboard__utility_key_enabled__summary" comment="Preference summary">Εμφανίζει ένα διαμορφώσιμο πλήκτρο χρηστικότητας δίπλα στο πλήκτρο κενού</string>
<string name="pref__keyboard__utility_key_action__label" comment="Preference title">Ενέργεια πλήκτρου χρηστικότητας</string>
<string name="pref__keyboard__utility_key_action__switch_to_emojis" comment="Preference value">Εναλλαγή σε emoji</string>
<string name="pref__keyboard__utility_key_action__switch_language" comment="Preference value">Εναλλαγή γλώσσας</string>
<string name="pref__keyboard__utility_key_action__switch_keyboard_app" comment="Preference value">Εναλλαγή εφαρμογής πληκτρολογίου</string>
<string name="pref__keyboard__utility_key_action__dynamic_switch_language_emojis" comment="Preference value">Δυναμικό: Εναλλαγή σε emojis / Εναλλαγή γλώσσας</string>
<string name="pref__keyboard__merge_hint_popups_enabled__label">Επιπλέον εμφάνιση αναδυόμενων πλήκτρων συμβόλων</string>
<string name="pref__keyboard_merge_hint_popups_enabled__summary">Προσθήκη των αναδυόμενων πλήκτρων συμβόλων στην προεπιλεγμένη διάταξη</string>
<string name="pref__keyboard__font_size_multiplier_portrait__label" comment="Preference title">Πολλαπλασιαστής μεγέθους γραμματοσειράς (πορτραίτο)</string>
<string name="pref__keyboard__font_size_multiplier_landscape__label" comment="Preference title">Πολλαπλασιαστής μεγέθους γραμματοσειράς (τοπίο)</string>
<string name="pref__keyboard__group_layout__label" comment="Preference group title">Διάταξη</string>
@@ -183,6 +193,10 @@
<string name="pref__keyboard__height_factor__extra_tall" comment="Preference value">Πολύ-ψηλό</string>
<string name="pref__keyboard__height_factor__custom" comment="Preference value">Προσαρμοσμένο</string>
<string name="pref__keyboard__height_factor_custom__label" comment="Preference title">Προσαρμοσμένη τιμή ύψους πληκτρολογίου</string>
<string name="pref__keyboard__bottom_offset_portrait__label" comment="Preference title">Μετατόπιση βάσης (πορτρέτο)</string>
<string name="pref__keyboard__bottom_offset_landscape__label" comment="Preference title">Μετατόπιση βάσης (τοπίο)</string>
<string name="pref__keyboard__key_spacing_vertical__label" comment="Preference title">Απόσταση μεταξύ πλήκτρων (κάθετα)</string>
<string name="pref__keyboard__key_spacing_horizontal__label" comment="Preference title">Απόσταση μεταξύ πλήκτρων (οριζόντια)</string>
<string name="pref__keyboard__group_keypress__label" comment="Preference group title">Πάτημα πλήκτρου</string>
<string name="pref__keyboard__sound_enabled__label" comment="Preference title">Ήχος κατά το πάτημα πλήκτρου</string>
<string name="pref__keyboard__sound_volume__label" comment="Preference title">Ένταση ήχου κατά το πάτημα πλήκτρου</string>
@@ -193,10 +207,20 @@
<string name="pref__keyboard__popup_visible__summary" comment="Preference summary">Επιπλέον εμφάνιση πλήκτρου όταν πατήσετε ένα πλήκτρο</string>
<string name="pref__keyboard__long_press_delay__label" comment="Preference title">Καθυστέρηση παρατεταμένου πατήματος πλήκτρου</string>
<string name="pref__keyboard__space_bar_switches_to_characters__label" comment="Preference title">Το πλήκτρο διαστήματος επιστρέφει στη διάταξη χαρακτήρων</string>
<string name="pref__keyboard__space_bar_switches_to_characters__summary" comment="Preference summary">Πατώντας κενό επαναφέρεστε αυτόματα στους χαρακτήρες όταν βρίσκεστε στα σύμβολα ή στους αριθμούς</string>
<string name="settings__typing__title" comment="Title of Typing experience fragment">Εμπειρία πληκτρολόγησης</string>
<string name="pref__smartbar__enabled__label" comment="Preference title">Ενεργοποίηση Έξυπνης Μπάρας</string>
<string name="pref__smartbar__enabled__summary" comment="Preference summary">Εμφάνιση στην κορυφή του πληκτρολογίου</string>
<string name="pref__suggestion__title" comment="Preference group title">Προτάσεις</string>
<string name="pref__dictionary__title" comment="Preference group title">Λεξικό</string>
<string name="pref__dictionary__enable_system_user_dictionary__label" comment="Preference title">Ενεργοποίηση λεξικού χρήστη στο σύστημα</string>
<string name="pref__dictionary__enable_system_user_dictionary__summary" comment="Preference summary">Προτάσεις λέξεων αποθηκευμένων στο λεξικό χρήστη στο σύστημα</string>
<string name="pref__dictionary__manage_system_user_dictionary__label" comment="Preference title">Διαχείριση λεξικού χρήστη στο σύστημα</string>
<string name="pref__dictionary__manage_system_user_dictionary__summary" comment="Preference summary">Προσθήκη, προβολή, και αφαίρεση εισαγωγών για το λεξικό χρήστη στο σύστημα</string>
<string name="pref__dictionary__enable_internal_user_dictionary__label" comment="Preference title">Ενεργοποίηση εσωτερικού λεξικού χρήστη</string>
<string name="pref__dictionary__enable_internal_user_dictionary__summary" comment="Preference summary">Προτάσεις λέξεων αποθηκευμένων στο εσωτερικό λεξικό χρήστη</string>
<string name="pref__dictionary__manage_floris_user_dictionary__label" comment="Preference title">Διαχείριση εσωτερικού λεξικού χρήστη</string>
<string name="pref__dictionary__manage_floris_user_dictionary__summary" comment="Preference summary">Προσθήκη, προβολή, και αφαίρεση εισαγωγών για το εσωτερικό λεξικό χρήστη</string>
<string name="pref__correction__title" comment="Preference group title">Διορθώσεις</string>
<string name="pref__correction__auto_capitalization__label" comment="Preference title">Αυτόματη χρήση κεφαλαίων</string>
<string name="pref__correction__auto_capitalization__summary" comment="Preference summary">Κεφαλαιοποίηση λέξεων βάσει του παρόντος περιεχομένου εισαγωγής</string>
@@ -204,13 +228,38 @@
<string name="pref__correction__remember_caps_lock_state__summary" comment="Preference summary">Τα κεφαλαία θα παραμείνουν ενεργοποιημένα όταν μεταβείτε σε διαφορετικό πεδίο κειμένου</string>
<string name="pref__correction__double_space_period__label" comment="Preference title">Τελεία με διπλό-κενό</string>
<string name="pref__correction__double_space_period__summary" comment="Preference summary">Το διπλό πάτημα του πλήκτρου διαστήματος εισάγει μία τελεία ακολουθούμενη από ένα διάστημα</string>
<string name="settings__udm__title_floris" comment="Title of the User Dictionary Manager activity for internal">Εσωτερικό Λεξικό Χρήστη</string>
<string name="settings__udm__title_system" comment="Title of the User Dictionary Manager activity for system">Λεξικό Χρήστη στο Σύστημα</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.">Συχνότητα: %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.">Συχνότητα: %d | Συντόμευση: %s</string>
<string name="settings__udm__all_languages" comment="Label of the For all languages entry in the language list">Για όλες τις γλώσσες</string>
<string name="settings__udm__open_system_manager_ui" comment="Label of the Open system manager UI menu option">Άνοιγμα περιβάλλοντος διαχειριστή συστήματος</string>
<string name="settings__udm__dictionary_import_success" comment="Message for dictionary import success">Η εισαγωγή λεξικού χρήστη ήταν επιτυχής!</string>
<string name="settings__udm__dictionary_export_success" comment="Message for dictionary export success">Η εξαγωγή λεξικού χρήστη ήταν επιτυχής!</string>
<string name="settings__udm__dialog__title_add" comment="Label for the title (when in adding mode) in the user dictionary add/edit dialog">Προσθήκη καταχώρισης λέξεως</string>
<string name="settings__udm__dialog__title_edit" comment="Label for the title (when in editing mode) in the user dictionary add/edit dialog">Επεξεργασία καταχώρισης λέξεως</string>
<string name="settings__udm__dialog__word_label" comment="Label for the word in the user dictionary add/edit dialog">Λέξη</string>
<string name="settings__udm__dialog__word_error_empty" comment="Error label for the word in the user dictionary add/edit dialog">Παρακαλώ εισάγετε μία λέξη!</string>
<string name="settings__udm__dialog__word_error_invalid" comment="Error label for the word in the user dictionary add/edit dialog">Αυτή η λέξη περιέχει μη έγκυρους χαρακτήρες.</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.">Συχνότητα (μεταξύ %d και %d)</string>
<string name="settings__udm__dialog__freq_error_empty" comment="Error label for the frequency in the user dictionary add/edit dialog">Παρακαλώ εισάγετε μία τιμή συχνότητας!</string>
<string name="settings__udm__dialog__freq_error_invalid" comment="Error label for the frequency in the user dictionary add/edit dialog">Παρακαλώ εισάγετε έναν έγκυρο αριθμό εντός των ορισμένων ορίων!</string>
<string name="settings__udm__dialog__shortcut_label" comment="Label for the shortcut in the user dictionary add/edit dialog">Συντόμευση (προαιρετική)</string>
<string name="settings__udm__dialog__shortcut_error_invalid" comment="Error label for the shortcut in the user dictionary add/edit dialog">Αυτή η συντόμευση περιέχει μη έγκυρους χαρακτήρες.</string>
<string name="settings__udm__dialog__locale_label" comment="Label for the language code in the user dictionary add/edit dialog">Κωδικός γλώσσας (προαιρετικός)</string>
<string name="settings__udm__dialog__locale_error_invalid" comment="Error label for the language code in the user dictionary add/edit dialog">Αυτός ο κωδικός γλώσσας δεν ανταποκρίνεται στην προβλεπόμενη σύνταξη. Ο κωδικός πρέπει να είναι είτε μία γλώσσα μόνο (όπως en), είτε μία γλώσσα και μία χώρα (όπως en_US) είτε μία γλώσσα, χώρα, και κείμενο (όπως en_US-κείμενο).</string>
<string name="settings__gestures__title" comment="Title of Gestures fragment">Κινήσεις &amp; Πληκτρολόγηση με ολίσθηση</string>
<string name="pref__glide__title" comment="Preference group title">Πληκτρολόγηση με ολίσθηση</string>
<string name="pref__glide__enabled__label" comment="Preference title">Ενεργοποίηση πληκτρολόγησης με ολίσθηση</string>
<string name="pref__glide__enabled__summary" comment="Preference summary">Πληκτρολογήστε μία λέξη με ολίσθηση του δαχτύλου μέσα από τα γράμματά της</string>
<string name="pref__glide__show_trail__label" comment="Preference title">Εμφάνιση διαδρομής ολίσθησης</string>
<string name="pref__glide__show_trail__summary" comment="Preference summary">Θα εξαφανίζεται μετά από κάθε λέξη</string>
<string name="pref__gestures__general_title" comment="Preference group title">Γενικές χειρονομίες</string>
<string name="pref__gestures__space_bar_title" comment="Preference group title">Χειρονομίες πλήκτρου διαστήματος</string>
<string name="pref__gestures__other_title" comment="Preference group title">Άλλες κινήσεις / Όρια κινήσεων</string>
<string name="pref__gestures__swipe_action__no_action" comment="Preference value for swipe action">Καμία ενέργεια</string>
<string name="pref__gestures__swipe_action__cycle_to_previous_keyboard_mode" comment="Preference value for swipe action">Εναλλαγή στην προηγούμενη λειτουργία πληκτρολογίου</string>
<string name="pref__gestures__swipe_action__cycle_to_next_keyboard_mode" comment="Preference value for swipe action">Εναλλαγή στην επόμενη λειτουργία πληκτρολογίου</string>
<string name="pref__gestures__swipe_action__delete_characters_precisely" comment="Preference value for swipe action">Διαγραφή χαρακτήρων με ακρίβεια</string>
<string name="pref__gestures__swipe_action__delete_word" comment="Preference value for swipe action">Διαγραφή της τρέχουσας λέξης</string>
<string name="pref__gestures__swipe_action__delete_words_precisely" comment="Preference value for swipe action">Διαγραφή λέξεων με ακρίβεια</string>
@@ -260,6 +309,12 @@
<string name="pref__advanced__show_app_icon__label" comment="Label of Show app icon preference in Advanced">Εμφάνιση του εικονιδίου της εφαρμογής στον εκκινητή</string>
<string name="pref__advanced__force_private_mode__label" comment="Label of Force private mode preference in Advanced">Εξαναγκασμός ιδιωτικής λειτουργίας</string>
<string name="pref__advanced__force_private_mode__summary" comment="Summary of Force private mode preference in Advanced">Θα απενεργοποιήσει χαρακτηριστικά που χρησιμοποιούν προσωρινά τα δεδομένα που εισάγετε</string>
<string name="pref__devtools__enabled__label" comment="Label of Enable developer tools in Advanced">Ενεργοποίηση εργαλείων προγραμματιστή</string>
<string name="pref__devtools__enabled__summary" comment="Summary of Enable developer tools in Advanced">Εργαλεία σχεδιασμένα συγκεκριμένα για αποσφαλμάτωση και επίλυση προβλημάτων</string>
<string name="pref__devtools__show_heap_memory_stats__label" comment="Label of Show heap memory stats in Advanced">Εμφάνιση στατιστικών μνήμης σωρού</string>
<string name="pref__devtools__show_heap_memory_stats__summary" comment="Summary of Show heap memory stats in Advanced">Δημιουργεί ένα επικάλυμμα με τη χρήση της μνήμης σωρού και του μέγιστου μεγέθους στην πάνω-δεξιά γωνία</string>
<string name="pref__devtools__clear_udm_internal_database__label" comment="Label of Clear internal user dictionary database in Advanced">Διαγραφή βάσης δεδομένων εσωτερικού λεξικού χρήστη</string>
<string name="pref__devtools__clear_udm_internal_database__summary" comment="Summary of Clear internal user dictionary database in Advanced">Διαγράφει όλες τις λέξεις από τον πίνακα δεδομένων του λεξικού</string>
<!-- About UI strings -->
<string name="about__title" comment="Title of About activity">Σχετικά με</string>
<string name="about__app_icon_content_description" comment="Content description of app icon in About">Εικονίδιο εφαρμογής του FlorisBoard</string>
@@ -275,6 +330,7 @@
<string name="assets__file__name">Όνομα</string>
<string name="assets__file__source">Πηγή</string>
<string name="assets__action__add">Προσθήκη</string>
<string name="assets__action__apply">Εφαρμογή</string>
<string name="assets__action__cancel">Ακύρωση</string>
<string name="assets__action__cancel_confirm_title">Επιβεβαίωση ακύρωσης</string>
<string name="assets__action__cancel_confirm_message">Είστε βέβαιοι ότι θέλετε να απορρίψετε τυχούσες μη αποθηκευμένες αλλαγές; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί εφόσον εκτελεστεί.</string>
@@ -314,6 +370,12 @@
<string name="setup__finish__title" comment="Title of Setup finished fragment in Setup">Η ρύθμιση ολοκληρώθηκε!</string>
<!-- Crash Dialog strings -->
<string name="crash_dialog__title" comment="Title of crash dialog">Αναφορά σφάλματος FlorisBoard</string>
<string name="crash_dialog__description" comment="Description of crash dialog">Συγγνώμη για την ταλαιπωρία, αλλά το FlorisBoard έχει τερματιστεί απρόοπτα εξαιτίας ενός απρόσμενου σφάλματος.</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.">Εάν επιθυμείτε να αναφέρετε αυτό το σφάλμα, πρώτα ελέγξτε αν το πρόβλημά σας δεν έχει ακόμα αναφερθεί στην παρακολούθηση προβλημάτων στο GitHub.\nΕάν δεν έχει αναφερθεί, αντιγράψτε την καταγραφή του τερματισμού που έχει προκύψει και ανοίξτε ένα νέο θέμα. Χρησιμοποιήστε το πρότυπο \"%s\" και συμπληρώσει την περιγραφή, τα βήματα για αναπαραγωγή, και αντιγράψτε την καταγραφή του τερματισμού στο τέλος. Αυτό βοηθάει να γίνει το FlorisBoard καλύτερο και πιο σταθερό για όλους. Ευχαριστώ!</string>
<string name="crash_dialog__copy_to_clipboard" comment="Label of Copy to clipboard button in crash dialog">Αντιγραφή στο πρόχειρο συστήματος</string>
<string name="crash_dialog__copy_to_clipboard_success" comment="Label of Copy to clipboard success message in crash dialog">Αντιγράφηκε στο πρόχειρο συστήματος</string>
<string name="crash_dialog__copy_to_clipboard_failure" comment="Label of Copy to clipboard failure message in crash dialog">Αδυναμία αντιγραφής στο πρόχειρο συστήματος: Η πραγμάτωση διαχειριστή προχείρου δε βρέθηκε</string>
<string name="crash_dialog__open_issue_tracker" comment="Label of Open issue tracker button in crash dialog">Άνοιγμα παρακολούθησης θεμάτων (github.com)</string>
<string name="crash_dialog__close" comment="Label of Close button in crash dialog">Κλείσιμο</string>
<string name="crash_notification_channel__title" comment="Title of crash notification channel">Αναφορές σφάλματος FlorisBoard</string>
<string name="crash_once_notification__title" comment="Title of the notification for a single crash">Το FlorisBoard έχει σταματήσει να λειτουργεί…</string>
@@ -327,6 +389,7 @@
<string name="clip__pin_item">Καρφίτσωμα στοιχείου</string>
<string name="clip__delete_item">Διαγραφή</string>
<string name="clip__paste_item">Επικόλληση</string>
<string name="clip__back_to_text_input">Πίσω στην εισαγωγή κειμένου</string>
<string name="clip__cant_paste">Η εφαρμογή αυτή δεν επιτρέπει την επικόλληση του περιεχομένου.</string>
<string name="pref__clipboard__clipboard_category__label">Πρόχειρο</string>
<string name="pref__clipboard__use_internal_clipboard___label">Χρήση εσωτερικού προχείρου</string>
@@ -344,5 +407,8 @@
<string name="pref__clipboard__max_history_size__label">Μέγ. μέγεθος ιστορικού</string>
<string name="pref__unit_items">" στοιχεία"</string>
<!-- glide strings -->
<string name="pref__glide_trail_fade_duration">Διάρκεια εξασθένησης διαδρομής ολίσθησης</string>
<string name="pref__glide_preview_refresh_delay">Προεπισκόπηση καθυστέρησης ανανέωσης</string>
<string name="pref__glide__show_preview">Εμφάνιση προεπισκόπησης κατά την πληκτρολόγηση με ολίσθηση</string>
<string name="pref__glide_trail_max_length">Μέγιστο μήκος διαδρομής ολίσθησης</string>
</resources>

View File

@@ -85,7 +85,7 @@
<string name="pref__theme__night" comment="Label of the night group (night means dark theme)">Tema oscuro</string>
<string name="pref__theme__any_theme__label" comment="Label of the theme selector preference">Tema seleccionado</string>
<string name="pref__theme__any_theme_adapt_to_app__label" comment="Label of the theme adapt to app preference">Adaptar colores a la aplicación</string>
<string name="pref__theme__any_theme_adapt_to_app__summary" comment="Summary of the theme adapt to app preference">Los colores del tema se adaptan a los de la aplicación actual, si la aplicación de destino lo admite.</string>
<string name="pref__theme__any_theme_adapt_to_app__summary" comment="Summary of the theme adapt to app preference">Los colores del teclado se adaptan a los de la aplicación actual, si la aplicación de destino lo admite.</string>
<string name="pref__theme__source_assets" comment="Label for the theme source field">Activos de la aplicación FlorisBoard</string>
<string name="pref__theme__source_internal" comment="Label for the theme source field">Almacenamiento interno</string>
<string name="pref__theme__source_external" comment="Label for the theme source field">Almacenamiento externo</string>
@@ -123,9 +123,9 @@
<string name="settings__theme_editor__error_attr_name" comment="Error text for an invalid attribute name">Por favor, ingresa un nombre que contenga solo las letras a-z y/o A-Z.</string>
<string name="settings__theme_editor__error_attr_name_empty" comment="Error text for an empty attribute name">Por favor, introduzca un nombre al atributo.</string>
<string name="settings__theme_editor__error_attr_name_already_exists" comment="Error text for a duplicate attribute name">Este nombre de atributo ya existe en este grupo. Por favor, escribe otro.</string>
<string name="settings__theme__group_window" comment="Theme group label">Ventana &amp; sistema</string>
<string name="settings__theme__group_window" comment="Theme group label">Ventana y sistema</string>
<string name="settings__theme__group_keyboard" comment="Theme group label">Teclado</string>
<string name="settings__theme__group_key" comment="Theme group label">Tecla</string>
<string name="settings__theme__group_key" comment="Theme group label">Teclas</string>
<string name="settings__theme__group_key_specific" comment="Theme group label (%s is specific modifier)">Tecla (%s)</string>
<string name="settings__theme__group_media" comment="Theme group label">Contexto de multimedia</string>
<string name="settings__theme__group_oneHanded" comment="Theme group label">Una mano</string>
@@ -146,7 +146,7 @@
<string name="settings__theme__attr_showBorder" comment="Theme attribute label">Ver borde</string>
<string name="settings__theme__attr_colorPrimary" comment="Theme attribute label">Color principal</string>
<string name="settings__theme__attr_colorPrimaryDark" comment="Theme attribute label">Color principal (oscuro)</string>
<string name="settings__theme__attr_colorAccent" comment="Theme attribute label">Color principal</string>
<string name="settings__theme__attr_colorAccent" comment="Theme attribute label">Color del énfasis</string>
<string name="settings__theme__attr_navBarColor" comment="Theme attribute label">Color de la barra de navegación</string>
<string name="settings__theme__attr_navBarLight" comment="Theme attribute label">Color principal de la barra de navegación en modo oscuro</string>
<string name="settings__theme__attr_semiTransparentColor" comment="Theme attribute label">Color semitransparente</string>
@@ -169,6 +169,8 @@
<string name="pref__keyboard__utility_key_action__switch_language" comment="Preference value">Cambiar idioma</string>
<string name="pref__keyboard__utility_key_action__switch_keyboard_app" comment="Preference value">Cambiar aplicación de teclado</string>
<string name="pref__keyboard__utility_key_action__dynamic_switch_language_emojis" comment="Preference value">Dinámico: cambiar a emojis / cambiar de idioma</string>
<string name="pref__keyboard__merge_hint_popups_enabled__label">Mostrar también ventanas emergentes de símbolos</string>
<string name="pref__keyboard_merge_hint_popups_enabled__summary">Añadir las ventanas emergentes de símbolos al diseño predeterminado</string>
<string name="pref__keyboard__font_size_multiplier_portrait__label" comment="Preference title">Multiplicador del tamaño de la fuente (vertical)</string>
<string name="pref__keyboard__font_size_multiplier_landscape__label" comment="Preference title">Multiplicador del tamaño de la fuente (horizontal)</string>
<string name="pref__keyboard__group_layout__label" comment="Preference group title">Distribución</string>
@@ -207,19 +209,48 @@
<string name="pref__keyboard__space_bar_switches_to_characters__label" comment="Preference title">La barra de espacio cambia a la disposición de los caracteres</string>
<string name="pref__keyboard__space_bar_switches_to_characters__summary" comment="Preference summary">Al pulsar la barra de espacio, se vuelve automáticamente a los caracteres cuando se trata de símbolos o números</string>
<string name="settings__typing__title" comment="Title of Typing experience fragment">Experiencia de escritura</string>
<string name="pref__smartbar__enabled__label" comment="Preference title">Habilitar barra inteligente</string>
<string name="pref__smartbar__enabled__label" comment="Preference title">Barra inteligente</string>
<string name="pref__smartbar__enabled__summary" comment="Preference summary">Se mostrará en la parte superior del teclado</string>
<string name="pref__suggestion__title" comment="Preference group title">Sugerencias</string>
<string name="pref__dictionary__title" comment="Preference group title">Diccionario</string>
<string name="pref__dictionary__enable_system_user_dictionary__label" comment="Preference title">Habilitar diccionario personal del sistema</string>
<string name="pref__dictionary__enable_system_user_dictionary__summary" comment="Preference summary">Sugerir palabras almacenadas en el diccionario personal del sistema</string>
<string name="pref__dictionary__manage_system_user_dictionary__label" comment="Preference title">Administrar diccionario personal del sistema</string>
<string name="pref__dictionary__manage_system_user_dictionary__summary" comment="Preference summary">Añadir, ver y remover entradas para el diccionario personal del sistema</string>
<string name="pref__dictionary__enable_internal_user_dictionary__label" comment="Preference title">Habilitar diccionario personal interno</string>
<string name="pref__dictionary__enable_internal_user_dictionary__summary" comment="Preference summary">Sugerir palabras almacenadas en el diccionario personal interno</string>
<string name="pref__dictionary__manage_floris_user_dictionary__label" comment="Preference title">Administrar diccionario personal interno</string>
<string name="pref__dictionary__manage_floris_user_dictionary__summary" comment="Preference summary">Añadir, ver y remover entradas para el diccionario personal interno</string>
<string name="pref__correction__title" comment="Preference group title">Correcciones</string>
<string name="pref__correction__auto_capitalization__label" comment="Preference title">Mayúsculas automáticas</string>
<string name="pref__correction__auto_capitalization__summary" comment="Preference summary">Poner en mayúsculas las palabras según el contexto de entrada actual</string>
<string name="pref__correction__remember_caps_lock_state__label" comment="Preference title">Recordar el estado de bloqueo de las mayúsculas</string>
<string name="pref__correction__remember_caps_lock_state__label" comment="Preference title">Recordar bloqueo de mayúsculas</string>
<string name="pref__correction__remember_caps_lock_state__summary" comment="Preference summary">El bloqueo de las mayúsculas se mantendrá cuando se pase a otro campo de texto</string>
<string name="pref__correction__double_space_period__label" comment="Preference title">Punto y doble espacio</string>
<string name="pref__correction__double_space_period__summary" comment="Preference summary">Al presionar dos veces en la barra espaciadora se inserta un punto seguido de un espacio</string>
<string name="settings__gestures__title" comment="Title of Gestures fragment">Gestos &amp; escritura deslizante</string>
<string name="pref__glide__title" comment="Preference group title">Escritura deslizante</string>
<string name="pref__glide__enabled__label" comment="Preference title">Habilitar escritura por gesto</string>
<string name="pref__correction__double_space_period__summary" comment="Preference summary">Pulsar espacio dos veces para insertar un punto seguido de un espacio</string>
<string name="settings__udm__title_floris" comment="Title of the User Dictionary Manager activity for internal">Diccionario personal interno</string>
<string name="settings__udm__title_system" comment="Title of the User Dictionary Manager activity for system">Diccionario personal del sistema</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.">Frecuencia: %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.">Frecuencia: %d | Atajo: %s</string>
<string name="settings__udm__all_languages" comment="Label of the For all languages entry in the language list">Para todos los idiomas</string>
<string name="settings__udm__open_system_manager_ui" comment="Label of the Open system manager UI menu option">Abrir IU del administrador del sistema</string>
<string name="settings__udm__dictionary_import_success" comment="Message for dictionary import success">¡Diccionario personal importado satisfactoriamente!</string>
<string name="settings__udm__dictionary_export_success" comment="Message for dictionary export success">¡Diccionario personal exportado satisfactoriamente!</string>
<string name="settings__udm__dialog__title_add" comment="Label for the title (when in adding mode) in the user dictionary add/edit dialog">Añadir palabra</string>
<string name="settings__udm__dialog__title_edit" comment="Label for the title (when in editing mode) in the user dictionary add/edit dialog">Editar palabra</string>
<string name="settings__udm__dialog__word_label" comment="Label for the word in the user dictionary add/edit dialog">Palabra</string>
<string name="settings__udm__dialog__word_error_empty" comment="Error label for the word in the user dictionary add/edit dialog">¡Por favor, ingresa una palabra!</string>
<string name="settings__udm__dialog__word_error_invalid" comment="Error label for the word in the user dictionary add/edit dialog">Esta palabra contiene caracteres inválidos.</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.">Frecuencia (entre %d y %d)</string>
<string name="settings__udm__dialog__freq_error_empty" comment="Error label for the frequency in the user dictionary add/edit dialog">¡Por favor, ingresa un valor de frecuencia!</string>
<string name="settings__udm__dialog__freq_error_invalid" comment="Error label for the frequency in the user dictionary add/edit dialog">¡Por favor, ingresa un número válido dentro de los valores especificados!</string>
<string name="settings__udm__dialog__shortcut_label" comment="Label for the shortcut in the user dictionary add/edit dialog">Atajo (opcional)</string>
<string name="settings__udm__dialog__shortcut_error_invalid" comment="Error label for the shortcut in the user dictionary add/edit dialog">Este atajo contiene caracteres inválidos.</string>
<string name="settings__udm__dialog__locale_label" comment="Label for the language code in the user dictionary add/edit dialog">Código del idioma (opcional)</string>
<string name="settings__udm__dialog__locale_error_invalid" comment="Error label for the language code in the user dictionary add/edit dialog">Este código de idioma no se ajusta a la sintaxis esperada. El código debe ser solo un idioma (como en), un idioma y país (como en_US) o un idioma, país y script (como en_US-script).</string>
<string name="settings__gestures__title" comment="Title of Gestures fragment">Gestos y escritura deslizante</string>
<string name="pref__glide__title" comment="Preference group title">Escritura por gestos</string>
<string name="pref__glide__enabled__label" comment="Preference title">Escritura por gestos</string>
<string name="pref__glide__enabled__summary" comment="Preference summary">Escriba una palabra deslizando su dedo a través de las letras</string>
<string name="pref__glide__show_trail__label" comment="Preference title">Mostrar recorrido del gesto</string>
<string name="pref__glide__show_trail__summary" comment="Preference summary">Desaparecerá después de cada palabra</string>
@@ -282,6 +313,8 @@
<string name="pref__devtools__enabled__summary" comment="Summary of Enable developer tools in Advanced">Herramientas diseñadas específicamente para depurar y detectar problemas</string>
<string name="pref__devtools__show_heap_memory_stats__label" comment="Label of Show heap memory stats in Advanced">Mostrar estadísticas de memoria</string>
<string name="pref__devtools__show_heap_memory_stats__summary" comment="Summary of Show heap memory stats in Advanced">Sobreponer el uso de la memoria y el tamaño maximo en la esquina superior derecha</string>
<string name="pref__devtools__clear_udm_internal_database__label" comment="Label of Clear internal user dictionary database in Advanced">Eliminar palabras del diccionario interno</string>
<string name="pref__devtools__clear_udm_internal_database__summary" comment="Summary of Clear internal user dictionary database in Advanced">Eliminar todas las palabras del diccionario</string>
<!-- About UI strings -->
<string name="about__title" comment="Title of About activity">Acerca de</string>
<string name="about__app_icon_content_description" comment="Content description of app icon in About">Icono de la aplicación de FlorisBoard</string>
@@ -297,6 +330,7 @@
<string name="assets__file__name">Nombre</string>
<string name="assets__file__source">Fuente</string>
<string name="assets__action__add">Agregar</string>
<string name="assets__action__apply">Aplicar</string>
<string name="assets__action__cancel">Cancelar</string>
<string name="assets__action__cancel_confirm_title">Confirmar cancelación</string>
<string name="assets__action__cancel_confirm_message">¿Está seguro de que desea descartar los cambios no guardados? Esta acción no se puede deshacer una vez ejecutada.</string>
@@ -359,7 +393,7 @@
<string name="clip__cant_paste">Esta aplicación no permite pegar el contenido.</string>
<string name="pref__clipboard__clipboard_category__label">Portapapeles</string>
<string name="pref__clipboard__use_internal_clipboard___label">Usar portapapeles interno</string>
<string name="pref__clipboard__use_internal_clipboard_title__summary">Usar portapapeles interno en vez de el portapapeles del sistema</string>
<string name="pref__clipboard__use_internal_clipboard_title__summary">Usar portapapeles interno en vez del portapapeles del sistema</string>
<string name="pref__clipboard__sync_from_system_clipboard__label">Sincronizar portapapeles del sistema</string>
<string name="pref__clipboard__keyboard_sync_from_system_clipboard__summary">Las actualizaciones del portapapeles del sistema también actualizan el portapapeles de Floris</string>
<string name="pref__clipboard__sync_to_system_clipboard__label">Sincronizar al portapapeles del sistema</string>

View File

@@ -169,6 +169,8 @@
<string name="pref__keyboard__utility_key_action__switch_language" comment="Preference value">تغییر زبان</string>
<string name="pref__keyboard__utility_key_action__switch_keyboard_app" comment="Preference value">تغییر برنامه کیبورد</string>
<string name="pref__keyboard__utility_key_action__dynamic_switch_language_emojis" comment="Preference value">متغیر: تغییر به شکلک‌ها / تغییر زبان</string>
<string name="pref__keyboard__merge_hint_popups_enabled__label">همچنین پاپ آپ نماد ها را نشان بده</string>
<string name="pref__keyboard_merge_hint_popups_enabled__summary">افزودن نمادهای پاپ آپ به چیدمان پیشفرض</string>
<string name="pref__keyboard__font_size_multiplier_portrait__label" comment="Preference title">اندازه فونت چند برابری(عمودی)</string>
<string name="pref__keyboard__font_size_multiplier_landscape__label" comment="Preference title">اندازه فونت چند برابری(افقی)</string>
<string name="pref__keyboard__group_layout__label" comment="Preference group title">چیدمان</string>

View File

@@ -169,6 +169,8 @@
<string name="pref__keyboard__utility_key_action__switch_language" comment="Preference value">Changer de langue</string>
<string name="pref__keyboard__utility_key_action__switch_keyboard_app" comment="Preference value">Changer d\'application de clavier</string>
<string name="pref__keyboard__utility_key_action__dynamic_switch_language_emojis" comment="Preference value">Dynamique : Changer vers emojis / Changer de langue</string>
<string name="pref__keyboard__merge_hint_popups_enabled__label">Afficher également les popups de symboles</string>
<string name="pref__keyboard_merge_hint_popups_enabled__summary">Ajouter les popups de symbole à la disposition par défaut</string>
<string name="pref__keyboard__font_size_multiplier_portrait__label" comment="Preference title">Multiplicateur de la taille de la police (portrait)</string>
<string name="pref__keyboard__font_size_multiplier_landscape__label" comment="Preference title">Multiplicateur de la taille de la police (paysage)</string>
<string name="pref__keyboard__group_layout__label" comment="Preference group title">Disposition</string>
@@ -216,9 +218,9 @@
<string name="pref__dictionary__manage_system_user_dictionary__label" comment="Preference title">Gérer le dictionnaire d\'utilisateur système</string>
<string name="pref__dictionary__manage_system_user_dictionary__summary" comment="Preference summary">Ajouter, voir et supprimer des entrées du dictionnaire d\'utilisateur système</string>
<string name="pref__dictionary__enable_internal_user_dictionary__label" comment="Preference title">Activer le dictionnaire d\'utilisateur interne</string>
<string name="pref__dictionary__enable_internal_user_dictionary__summary" comment="Preference summary">Suggérer des mots du dictionnaire d\'utilisateur interne</string>
<string name="pref__dictionary__manage_floris_user_dictionary__label" comment="Preference title">Gérer le dictionnaire d\'utilisateur interne</string>
<string name="pref__dictionary__manage_floris_user_dictionary__summary" comment="Preference summary">Ajouter, voir et supprimer des entrées du dictionnaire d\'utilisateur interne</string>
<string name="pref__dictionary__enable_internal_user_dictionary__summary" comment="Preference summary">Suggérer des mots stockés dans le dictionnaire utilisateur interne</string>
<string name="pref__dictionary__manage_floris_user_dictionary__label" comment="Preference title">Gérer le dictionnaire utilisateur interne</string>
<string name="pref__dictionary__manage_floris_user_dictionary__summary" comment="Preference summary">Ajouter, voir et supprimer des entrées du dictionnaire utilisateur interne</string>
<string name="pref__correction__title" comment="Preference group title">Corrections</string>
<string name="pref__correction__auto_capitalization__label" comment="Preference title">Auto-capitalisation</string>
<string name="pref__correction__auto_capitalization__summary" comment="Preference summary">Capitaliser les mots en fonction du contexte de saisie actuel</string>
@@ -226,14 +228,14 @@
<string name="pref__correction__remember_caps_lock_state__summary" comment="Preference summary">Le verrouillage des majuscules reste activé lorsque l\'on passe à un autre champ de texte</string>
<string name="pref__correction__double_space_period__label" comment="Preference title">Point de double espace</string>
<string name="pref__correction__double_space_period__summary" comment="Preference summary">En tappant deux fois sur la barre espace, insère un point suivi d\'un espace</string>
<string name="settings__udm__title_floris" comment="Title of the User Dictionary Manager activity for internal">Dictionnaire d\'utilisateur interne</string>
<string name="settings__udm__title_system" comment="Title of the User Dictionary Manager activity for system">Dictionnaire d\'utilisateur système</string>
<string name="settings__udm__title_floris" comment="Title of the User Dictionary Manager activity for internal">Dictionnaire utilisateur interne</string>
<string name="settings__udm__title_system" comment="Title of the User Dictionary Manager activity for system">Dictionnaire utilisateur système</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.">Fréquence : %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.">Fréquence : %d | Raccourci : %s</string>
<string name="settings__udm__all_languages" comment="Label of the For all languages entry in the language list">Pour toutes les langues</string>
<string name="settings__udm__open_system_manager_ui" comment="Label of the Open system manager UI menu option">Ouvrir l\'interface de gestion du système</string>
<string name="settings__udm__dictionary_import_success" comment="Message for dictionary import success">Dictionnaire d\'utilisateur importé avec succès !</string>
<string name="settings__udm__dictionary_export_success" comment="Message for dictionary export success">Dictionnaire d\'utilisateur exporté avec succès !</string>
<string name="settings__udm__dictionary_import_success" comment="Message for dictionary import success">Dictionnaire utilisateur importé avec succès !</string>
<string name="settings__udm__dictionary_export_success" comment="Message for dictionary export success">Dictionnaire utilisateur exporté avec succès !</string>
<string name="settings__udm__dialog__title_add" comment="Label for the title (when in adding mode) in the user dictionary add/edit dialog">Ajouter un mot</string>
<string name="settings__udm__dialog__title_edit" comment="Label for the title (when in editing mode) in the user dictionary add/edit dialog">Modifier un mot</string>
<string name="settings__udm__dialog__word_label" comment="Label for the word in the user dictionary add/edit dialog">Mot</string>
@@ -245,7 +247,7 @@
<string name="settings__udm__dialog__shortcut_label" comment="Label for the shortcut in the user dictionary add/edit dialog">Raccourci (facultatif)</string>
<string name="settings__udm__dialog__shortcut_error_invalid" comment="Error label for the shortcut in the user dictionary add/edit dialog">Ce raccourci contient des caractères non valides.</string>
<string name="settings__udm__dialog__locale_label" comment="Label for the language code in the user dictionary add/edit dialog">Code de langue (facultatif)</string>
<string name="settings__udm__dialog__locale_error_invalid" comment="Error label for the language code in the user dictionary add/edit dialog">Ce code de langue n\'est pas conforme à la syntaxe attendue. Le code doit être une langue uniquement (comme en), une langue et un pays (comme en_US) ou une langue, un pays et un script (comme en_US-script).</string>
<string name="settings__udm__dialog__locale_error_invalid" comment="Error label for the language code in the user dictionary add/edit dialog">Ce code de langue n\'est pas conforme à la syntaxe attendue. Le code doit être une langue uniquement (comme fr), une langue et un pays (comme fr_FR) ou une langue, un pays et un script (comme fr_FR-script).</string>
<string name="settings__gestures__title" comment="Title of Gestures fragment">Gestes &amp; Saisie en glissant</string>
<string name="pref__glide__title" comment="Preference group title">Saisie en glissant</string>
<string name="pref__glide__enabled__label" comment="Preference title">Activer la saisie en glissant</string>
@@ -328,6 +330,7 @@
<string name="assets__file__name">Nom</string>
<string name="assets__file__source">Source</string>
<string name="assets__action__add">Ajouter</string>
<string name="assets__action__apply">Appliquer</string>
<string name="assets__action__cancel">Annuler</string>
<string name="assets__action__cancel_confirm_title">Confirmer l\'annulation</string>
<string name="assets__action__cancel_confirm_message">Êtes-vous sûrs de vouloir abandonner tout changement non sauvegardé ? Cette action ne peut être annulée après son exécution.</string>

View File

@@ -169,6 +169,8 @@
<string name="pref__keyboard__utility_key_action__switch_language" comment="Preference value">Cambia lingua</string>
<string name="pref__keyboard__utility_key_action__switch_keyboard_app" comment="Preference value">Cambia app di tastiera</string>
<string name="pref__keyboard__utility_key_action__dynamic_switch_language_emojis" comment="Preference value">Dinamico: Passa agli emoji / Cambia lingua</string>
<string name="pref__keyboard__merge_hint_popups_enabled__label">Mostra anche i popup dei simboli</string>
<string name="pref__keyboard_merge_hint_popups_enabled__summary">Aggiungi i popup dei simboli alla disposizione predefinita</string>
<string name="pref__keyboard__font_size_multiplier_portrait__label" comment="Preference title">Moltiplicatore della dimensione del testo (ritratto)</string>
<string name="pref__keyboard__font_size_multiplier_landscape__label" comment="Preference title">Moltiplicatore della dimensione del testo (panorama)</string>
<string name="pref__keyboard__group_layout__label" comment="Preference group title">Layout</string>

View File

@@ -169,6 +169,8 @@
<string name="pref__keyboard__utility_key_action__switch_language" comment="Preference value">Mainīt valodu</string>
<string name="pref__keyboard__utility_key_action__switch_keyboard_app" comment="Preference value">Mainīt tastatūras lietotni</string>
<string name="pref__keyboard__utility_key_action__dynamic_switch_language_emojis" comment="Preference value">Mainīgs: pārslēgties uz sajūtu zīmēm (emoji) / pārslēgt valodu</string>
<string name="pref__keyboard__merge_hint_popups_enabled__label">Rādīt arī zīmju uzlecošos logus</string>
<string name="pref__keyboard_merge_hint_popups_enabled__summary">Pievienot zīmju izlecošos logus noklusējuma izkārtojumam</string>
<string name="pref__keyboard__font_size_multiplier_portrait__label" comment="Preference title">Fonta izmēra reizinātājs (statenisks novietojums)</string>
<string name="pref__keyboard__font_size_multiplier_landscape__label" comment="Preference title">Fonta izmēra reizinātājs (līmenisks novietojums)</string>
<string name="pref__keyboard__group_layout__label" comment="Preference group title">Izkārtojums</string>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">FlorisBoard</string>
<string name="key__phone_pause" comment="Label for the Pause key in the telephone keyboard layout">Pauze</string>
<string name="key__phone_pause" comment="Label for the Pause key in the telephone keyboard layout">Pauzeren</string>
<string name="key__phone_wait" comment="Label for the Wait key in the telephone keyboard layout">Wacht</string>
<string name="key_popup__threedots_alt" comment="Content description for the three-dots icon in a key popup">Doorlooppuntjes-symbool. Geeft aan dat meer letters gebruikt kunnen worden door de toets langer in te drukken, wanneer zichtbaar.</string>
<!-- One-handed strings -->
@@ -169,6 +169,8 @@
<string name="pref__keyboard__utility_key_action__switch_language" comment="Preference value">Taal wijzigen</string>
<string name="pref__keyboard__utility_key_action__switch_keyboard_app" comment="Preference value">Wissel toetsenbord</string>
<string name="pref__keyboard__utility_key_action__dynamic_switch_language_emojis" comment="Preference value">Dynamisch: Wissel naar emoji\'s / Wissel taal</string>
<string name="pref__keyboard__merge_hint_popups_enabled__label">Toon ook symboolpop-ups</string>
<string name="pref__keyboard_merge_hint_popups_enabled__summary">Voeg symboolpop-ups toe aan de standaardindeling</string>
<string name="pref__keyboard__font_size_multiplier_portrait__label" comment="Preference title">Factor lettergrootte (portret-modus)</string>
<string name="pref__keyboard__font_size_multiplier_landscape__label" comment="Preference title">Factor lettergrootte (landschapsmodus)</string>
<string name="pref__keyboard__group_layout__label" comment="Preference group title">Indeling</string>
@@ -210,6 +212,15 @@
<string name="pref__smartbar__enabled__label" comment="Preference title">Schakel Slimme Balk in</string>
<string name="pref__smartbar__enabled__summary" comment="Preference summary">Word getoond boven het toetsenbord</string>
<string name="pref__suggestion__title" comment="Preference group title">Suggesties</string>
<string name="pref__dictionary__title" comment="Preference group title">Woordenboek</string>
<string name="pref__dictionary__enable_system_user_dictionary__label" comment="Preference title">Activeer het persoonlijke woordenboek van het systeem</string>
<string name="pref__dictionary__enable_system_user_dictionary__summary" comment="Preference summary">Stel woorden voor uit het persoonlijke woordenboek van het systeem</string>
<string name="pref__dictionary__manage_system_user_dictionary__label" comment="Preference title">Beheer persoonlijke woordenboek van het systeem</string>
<string name="pref__dictionary__manage_system_user_dictionary__summary" comment="Preference summary">Voeg toe, bekijk of verwijder woorden uit het persoonlijke woordenboek van het systeem</string>
<string name="pref__dictionary__enable_internal_user_dictionary__label" comment="Preference title">Schakel intern persoonlijke woordenboek in</string>
<string name="pref__dictionary__enable_internal_user_dictionary__summary" comment="Preference summary">Stel woorden voor uit het interne persoonlijke woordenboek</string>
<string name="pref__dictionary__manage_floris_user_dictionary__label" comment="Preference title">Beheer interne persoonlijke woordenboek</string>
<string name="pref__dictionary__manage_floris_user_dictionary__summary" comment="Preference summary">Voeg toe, bekijk of verwijder woorden uit het interne persoonlijke woordenboek</string>
<string name="pref__correction__title" comment="Preference group title">Verbeteringen</string>
<string name="pref__correction__auto_capitalization__label" comment="Preference title">Auto-kapitalisatie</string>
<string name="pref__correction__auto_capitalization__summary" comment="Preference summary">Woorden kapitaliseren op basis van de huidige invoercontext</string>
@@ -217,6 +228,26 @@
<string name="pref__correction__remember_caps_lock_state__summary" comment="Preference summary">CapsLock blijft aan bij het verplaatsen naar een ander tekstveld</string>
<string name="pref__correction__double_space_period__label" comment="Preference title">Punt na twee spaties</string>
<string name="pref__correction__double_space_period__summary" comment="Preference summary">Twee keer tikken op spatiebalk voegt een punt toe gevolgd door een spatie</string>
<string name="settings__udm__title_floris" comment="Title of the User Dictionary Manager activity for internal">Intern persoonlijk woordenboek</string>
<string name="settings__udm__title_system" comment="Title of the User Dictionary Manager activity for system">Persoonlijk woordenboek van het systeem</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.">Frequentie: %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.">Frequentie: %d | Afkorting: %s</string>
<string name="settings__udm__all_languages" comment="Label of the For all languages entry in the language list">Voor alle talen</string>
<string name="settings__udm__open_system_manager_ui" comment="Label of the Open system manager UI menu option">Open systeembeheer UI</string>
<string name="settings__udm__dictionary_import_success" comment="Message for dictionary import success">Persoonlijk woordenboek met succes geïmporteerd!</string>
<string name="settings__udm__dictionary_export_success" comment="Message for dictionary export success">Persoonlijk woordenboek met succces geëxporteerd!</string>
<string name="settings__udm__dialog__title_add" comment="Label for the title (when in adding mode) in the user dictionary add/edit dialog">Woord invoeren</string>
<string name="settings__udm__dialog__title_edit" comment="Label for the title (when in editing mode) in the user dictionary add/edit dialog">Woord bewerken</string>
<string name="settings__udm__dialog__word_label" comment="Label for the word in the user dictionary add/edit dialog">Woord</string>
<string name="settings__udm__dialog__word_error_empty" comment="Error label for the word in the user dictionary add/edit dialog">Geef een woord in.</string>
<string name="settings__udm__dialog__word_error_invalid" comment="Error label for the word in the user dictionary add/edit dialog">Dit woord bevat ongeldige tekens.</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.">Frequentie (tussen %d en %d)</string>
<string name="settings__udm__dialog__freq_error_empty" comment="Error label for the frequency in the user dictionary add/edit dialog">Geef een frequentie in.</string>
<string name="settings__udm__dialog__freq_error_invalid" comment="Error label for the frequency in the user dictionary add/edit dialog">Geef een geldig nummer binnen het gespecifieerde bereik in.</string>
<string name="settings__udm__dialog__shortcut_label" comment="Label for the shortcut in the user dictionary add/edit dialog">Afkorting (optioneel)</string>
<string name="settings__udm__dialog__shortcut_error_invalid" comment="Error label for the shortcut in the user dictionary add/edit dialog">Deze afkorting bevat ongeldige tekens.</string>
<string name="settings__udm__dialog__locale_label" comment="Label for the language code in the user dictionary add/edit dialog">Taalcode (optioneel)</string>
<string name="settings__udm__dialog__locale_error_invalid" comment="Error label for the language code in the user dictionary add/edit dialog">Deze taalcode komt niet overeen met de verwachte syntax. De code moet ofwel enkel de taal (bv. \"en\"), een taal en een lan (bv. \"en_US\") of een taal, land en script (bv. \"en_US-script\") bevatten.</string>
<string name="settings__gestures__title" comment="Title of Gestures fragment">Gebaren &amp; Vegen</string>
<string name="pref__glide__title" comment="Preference group title">Vegend typen</string>
<string name="pref__glide__enabled__label" comment="Preference title">Glijdend typen inschakelen</string>
@@ -282,6 +313,8 @@
<string name="pref__devtools__enabled__summary" comment="Summary of Enable developer tools in Advanced">Hulpmiddelen specifiek voor debuggen en problemen verhelpen</string>
<string name="pref__devtools__show_heap_memory_stats__label" comment="Label of Show heap memory stats in Advanced">Toon heap memory stats</string>
<string name="pref__devtools__show_heap_memory_stats__summary" comment="Summary of Show heap memory stats in Advanced">Toont het heap-memorygebruik en maximale grootte in de rechterbovenhoek</string>
<string name="pref__devtools__clear_udm_internal_database__label" comment="Label of Clear internal user dictionary database in Advanced">Wis database van het interne persoonlijke woordenboek</string>
<string name="pref__devtools__clear_udm_internal_database__summary" comment="Summary of Clear internal user dictionary database in Advanced">Wis alle woorden van de woordenlijst in de datatabel</string>
<!-- About UI strings -->
<string name="about__title" comment="Title of About activity">Over</string>
<string name="about__app_icon_content_description" comment="Content description of app icon in About">App-pictogram van FlorisBoard</string>
@@ -297,6 +330,7 @@
<string name="assets__file__name">Naam</string>
<string name="assets__file__source">Bron</string>
<string name="assets__action__add">Toevoegen</string>
<string name="assets__action__apply">Toepassen</string>
<string name="assets__action__cancel">Annuleren</string>
<string name="assets__action__cancel_confirm_title">Annuleren bevestigen</string>
<string name="assets__action__cancel_confirm_message">Weet je zeker dat je onopgeslagen wijzigingen wil verwerpen? Deze actie kan niet ongedaan gemaakt worden.</string>
@@ -337,6 +371,7 @@
<!-- Crash Dialog strings -->
<string name="crash_dialog__title" comment="Title of crash dialog">Foutenrapport FlorisBoard</string>
<string name="crash_dialog__description" comment="Description of crash dialog">Sorry voor het ongemak, maar FlorisBoard is gecrashed omwille van een onverwachte fout.</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.">Als u deze fout wilt rapporteren, kijk dan eerst in de issuetracker op GitHub of uw crash nog niet gerapporteerd is.\nAls dat niet het geval is, kopieer dan de gegenereerde crashlog en open een nieuwe issue. Gebruik het \"%s\"-sjabloon en vul de beschrijving in, de stappen om te reproduceren, en plak het gegenereerde crash log aan het eind. Dit helpt om FlorisBoard beter en stabieler te maken voor iedereen. Bedankt!</string>
<string name="crash_dialog__copy_to_clipboard" comment="Label of Copy to clipboard button in crash dialog">Kopieer naar systeemklembord</string>
<string name="crash_dialog__copy_to_clipboard_success" comment="Label of Copy to clipboard success message in crash dialog">Gekopieerd naar systeemklembord</string>
<string name="crash_dialog__copy_to_clipboard_failure" comment="Label of Copy to clipboard failure message in crash dialog">Kan niet kopiëren naar systeemklembord: Klembordbeheerinstantie niet gevonden</string>

View File

@@ -40,7 +40,7 @@
<!-- Settings UI strings -->
<string name="settings__title" comment="Title of Settings">Configurações</string>
<string name="settings__menu" comment="Hint of top-right three-dot icon in Settings">Mais opções</string>
<string name="settings__menu_help" comment="Three-dot menu entry for Help and Feedback web link">Ajuda &amp; feedback</string>
<string name="settings__menu_help" comment="Three-dot menu entry for Help and Feedback web link">Ajuda e feedback</string>
<string name="settings__help" comment="General label for help buttons in Settings">Ajuda</string>
<string name="settings__navigation__home" comment="Long-press hint of bottom nav item Home in Settings">Início</string>
<string name="settings__navigation__keyboard" comment="Long-press hint of bottom nav item Keyboard in Settings">Teclado</string>
@@ -53,7 +53,7 @@
<string name="settings__home__ime_not_enabled" comment="Error message shown in Home fragment when FlorisBoard is not enabled in the system">O FlorisBoard não está ativado no sistema e, portanto, não estará disponível para ser selecionado no alternador de teclado. Clique aqui para resolver este problema.</string>
<string name="settings__home__ime_not_selected" comment="Warning message shown in Home fragment when FlorisBoard is not selected as the default keyboard">O FlorisBoard não foi selecionado como o teclado padrão. Clique aqui para resolver este problema.</string>
<string name="settings__home__contribute" comment="Contributing message shown in Home fragment">Obrigado por experimentar o FlorisBoard! Este projeto ainda está em alpha e, portanto, faltando recursos. Se você encontrar algum bug ou quiser fazer uma sugestão, por favor, confira o repo no GitHub e crie um issue. Isso ajuda a tornar o FlorisBoard melhor. Obrigado!</string>
<string name="settings__localization__title" comment="Title of languages and layout box in the Typing fragment">Idiomas &amp; Formatos do teclado</string>
<string name="settings__localization__title" comment="Title of languages and layout box in the Typing fragment">Idiomas e Formatos de Teclado</string>
<string name="settings__localization__subtype_no_subtypes_configured_warning" comment="Warning message that no subtype has been defined in the Typing fragment">Parece que você não configurou nenhum formato de digitação. Como alternativa, será utilizado o formato Inglês/QWERTY!</string>
<string name="settings__localization__subtype_add" comment="Subtype dialog add button">Adicionar</string>
<string name="settings__localization__subtype_add_title" comment="Title of subtype dialog when adding a new subtype">Adicionar formato de digitação</string>
@@ -123,7 +123,7 @@
<string name="settings__theme_editor__error_attr_name" comment="Error text for an invalid attribute name">Digite um nome de atributo que contenha apenas as letras a-z e/ou A-Z.</string>
<string name="settings__theme_editor__error_attr_name_empty" comment="Error text for an empty attribute name">Por favor, digite um nome para o atributo.</string>
<string name="settings__theme_editor__error_attr_name_already_exists" comment="Error text for a duplicate attribute name">Esse nome de atributo já existe dentro deste grupo. Por favor, especifique outro.</string>
<string name="settings__theme__group_window" comment="Theme group label">Janela &amp; Sistema</string>
<string name="settings__theme__group_window" comment="Theme group label">Janela e Sistema</string>
<string name="settings__theme__group_keyboard" comment="Theme group label">Teclado</string>
<string name="settings__theme__group_key" comment="Theme group label">Tecla</string>
<string name="settings__theme__group_key_specific" comment="Theme group label (%s is specific modifier)">Tecla (%s)</string>
@@ -169,6 +169,8 @@
<string name="pref__keyboard__utility_key_action__switch_language" comment="Preference value">Trocar idioma</string>
<string name="pref__keyboard__utility_key_action__switch_keyboard_app" comment="Preference value">Trocar aplicativo de teclado</string>
<string name="pref__keyboard__utility_key_action__dynamic_switch_language_emojis" comment="Preference value">Dinâmico: Mudar para emojis / Trocar idioma</string>
<string name="pref__keyboard__merge_hint_popups_enabled__label">Também mostrar popups de símbolo</string>
<string name="pref__keyboard_merge_hint_popups_enabled__summary">Adicionar os popups de símbolo ao layout padrão</string>
<string name="pref__keyboard__font_size_multiplier_portrait__label" comment="Preference title">Multiplicador de tamanho da fonte (retrato)</string>
<string name="pref__keyboard__font_size_multiplier_landscape__label" comment="Preference title">Multiplicador de tamanho da fonte (paisagem)</string>
<string name="pref__keyboard__group_layout__label" comment="Preference group title">Layout</string>
@@ -204,7 +206,7 @@
<string name="pref__keyboard__popup_visible__label" comment="Preference title">Visibilidade do PopUp</string>
<string name="pref__keyboard__popup_visible__summary" comment="Preference summary">Mostrar popup quando pressionar uma tecla</string>
<string name="pref__keyboard__long_press_delay__label" comment="Preference title">Atraso ao pressionar e segurar uma tecla</string>
<string name="pref__keyboard__space_bar_switches_to_characters__label" comment="Preference title">Barra de espaço muda de volta para o layout de caracteres</string>
<string name="pref__keyboard__space_bar_switches_to_characters__label" comment="Preference title">Barra de espaço alterna para o layout de caracteres</string>
<string name="pref__keyboard__space_bar_switches_to_characters__summary" comment="Preference summary">Apertar e segurar a barra de espaço muda automaticamente para os caracteres quando em símbolos ou números</string>
<string name="settings__typing__title" comment="Title of Typing experience fragment">Experiência de digitação</string>
<string name="pref__smartbar__enabled__label" comment="Preference title">Ativar barra inteligente</string>
@@ -246,7 +248,7 @@
<string name="settings__udm__dialog__shortcut_error_invalid" comment="Error label for the shortcut in the user dictionary add/edit dialog">Este atalho contém caracteres inválidos.</string>
<string name="settings__udm__dialog__locale_label" comment="Label for the language code in the user dictionary add/edit dialog">Código de idioma (opcional)</string>
<string name="settings__udm__dialog__locale_error_invalid" comment="Error label for the language code in the user dictionary add/edit dialog">Este código de idioma não está de acordo com a sintaxe esperada. O código deve ser apenas uma língua (como en), uma língua e um país (como en_US) ou uma língua, país e script (como en_US-script).</string>
<string name="settings__gestures__title" comment="Title of Gestures fragment">Gestos &amp; Digitação deslizante</string>
<string name="settings__gestures__title" comment="Title of Gestures fragment">Gestos e Digitação deslizante</string>
<string name="pref__glide__title" comment="Preference group title">Digitação deslizante</string>
<string name="pref__glide__enabled__label" comment="Preference title">Ativar digitação deslizante</string>
<string name="pref__glide__enabled__summary" comment="Preference summary">Digitar uma palavra deslizando o dedo através de suas letras</string>
@@ -358,9 +360,9 @@
<string name="setup__welcome__contribute" comment="Paragraph in Welcome fragment in Setup">Uma última coisa antes de iniciar a configuração - se você encontrar alguns bugs/travamentos/problemas com o FlorisBoard ou quiser fazer alguma solicitação de recurso - vá até o repositório do GitHub vinculado abaixo e crie um issue. Isso ajuda a melhorar a experiência de todos os usuários!</string>
<string name="setup__welcome__outro" comment="Paragraph in Welcome fragment in Setup">Para iniciar a configuração, clique em <i>PRÓXIMO</i>.</string>
<string name="setup__enable_ime__title" comment="Title of Enable IME fragment in Setup">Ativar FlorisBoard</string>
<string name="setup__enable_ime__text_before_enabled" comment="Description of state in Enable IME fragment before user enabled">O Android exige que todo teclado personalizado tenha que ser ativado manualmente antes de usá-lo. Clique no botão abaixo para ir as configurações de <i>Idioma &amp; Entrada</i>, em seguida, certifique-se de ativar o \'<i>FlorisBoard</i>\'.</string>
<string name="setup__enable_ime__text_before_enabled" comment="Description of state in Enable IME fragment before user enabled">O Android exige que todo teclado personalizado tenha que ser ativado manualmente antes de usá-lo. Clique no botão abaixo para ir as configurações de <i>Idioma e Entrada</i>, em seguida, certifique-se de ativar o \'<i>FlorisBoard</i>\'.</string>
<string name="setup__enable_ime__text_after_enabled" comment="Description of state in Enable IME fragment after user enabled">FlorisBoard foi ativado com sucesso. Para continuar clique em <i>PRÓXIMO</i>!</string>
<string name="setup__enable_ime__text_button_language_and_input" comment="Label of language and input button in Enable IME fragment">Abrir configurações de Idioma &amp; Entrada</string>
<string name="setup__enable_ime__text_button_language_and_input" comment="Label of language and input button in Enable IME fragment">Abrir configurações de Idioma e Entrada</string>
<string name="setup__make_default__title" comment="Title of Make IME default fragment in Setup">Selecionar FlorisBoard como padrão</string>
<string name="setup__make_default__text_before_switch" comment="Description of state in Make IME default fragment before user switched">O FlorisBoard agora está ativado em seu sistema. Para usá-lo ativamente, selecione o FlorisBoard como teclado padrão na caixa de seleção de teclado!</string>
<string name="setup__make_default__text_after_switch" comment="Description of state in Make IME default fragment after user switched">O FlorisBoard foi selecionado como teclado padrão com sucesso!</string>

View File

@@ -169,6 +169,8 @@
<string name="pref__keyboard__utility_key_action__switch_language" comment="Preference value">Mudar idioma</string>
<string name="pref__keyboard__utility_key_action__switch_keyboard_app" comment="Preference value">Mudar a aplicação de teclado</string>
<string name="pref__keyboard__utility_key_action__dynamic_switch_language_emojis" comment="Preference value">Dinâmica: mudar para emojis / mudar idioma</string>
<string name="pref__keyboard__merge_hint_popups_enabled__label">Mostrar popup para símbolos</string>
<string name="pref__keyboard_merge_hint_popups_enabled__summary">Adicionar popup de símbolos à disposição padrão</string>
<string name="pref__keyboard__font_size_multiplier_portrait__label" comment="Preference title">Multiplicador do tamanho do tipo de letra (vertical)</string>
<string name="pref__keyboard__font_size_multiplier_landscape__label" comment="Preference title">Multiplicador do tamanho do tipo de letra (horizontal)</string>
<string name="pref__keyboard__group_layout__label" comment="Preference group title">Disposição</string>
@@ -210,6 +212,15 @@
<string name="pref__smartbar__enabled__label" comment="Preference title">Ativar barra inteligente</string>
<string name="pref__smartbar__enabled__summary" comment="Preference summary">Mostrar na parte superior do teclado</string>
<string name="pref__suggestion__title" comment="Preference group title">Sugestões</string>
<string name="pref__dictionary__title" comment="Preference group title">Dicionário</string>
<string name="pref__dictionary__enable_system_user_dictionary__label" comment="Preference title">Ativar dicionário do sistema</string>
<string name="pref__dictionary__enable_system_user_dictionary__summary" comment="Preference summary">Sugerir palavras guardadas no dicionário do sistema</string>
<string name="pref__dictionary__manage_system_user_dictionary__label" comment="Preference title">Gerir dicionários do sistema</string>
<string name="pref__dictionary__manage_system_user_dictionary__summary" comment="Preference summary">Adicionar, ver e remover entradas ao dicionário do sistema</string>
<string name="pref__dictionary__enable_internal_user_dictionary__label" comment="Preference title">Ativar dicionário do utilizador</string>
<string name="pref__dictionary__enable_internal_user_dictionary__summary" comment="Preference summary">Sugerir palavras guardadas no dicionário do utilizador</string>
<string name="pref__dictionary__manage_floris_user_dictionary__label" comment="Preference title">Gerir dicionários do utilizador</string>
<string name="pref__dictionary__manage_floris_user_dictionary__summary" comment="Preference summary">Adicionar, ver e remover entradas ao dicionário do utilizador</string>
<string name="pref__correction__title" comment="Preference group title">Correções</string>
<string name="pref__correction__auto_capitalization__label" comment="Preference title">Maiúsculas automáticas</string>
<string name="pref__correction__auto_capitalization__summary" comment="Preference summary">Colocar letras maiúsculas tendo em conta o contexto das palavras</string>
@@ -217,6 +228,26 @@
<string name="pref__correction__remember_caps_lock_state__summary" comment="Preference summary">Manter CapsLock quando se muda para outro campo de texto</string>
<string name="pref__correction__double_space_period__label" comment="Preference title">Duplo espaço insere ponto final</string>
<string name="pref__correction__double_space_period__summary" comment="Preference summary">Ao tocar 2 vezes na barra de espaços, inserir um ponto final e um espaço</string>
<string name="settings__udm__title_floris" comment="Title of the User Dictionary Manager activity for internal">Dicionário do utilizador</string>
<string name="settings__udm__title_system" comment="Title of the User Dictionary Manager activity for system">Dicionário do sistema</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.">Frequência: %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.">Frequência: %d | Atalho: %s</string>
<string name="settings__udm__all_languages" comment="Label of the For all languages entry in the language list">Para todos os idiomas</string>
<string name="settings__udm__open_system_manager_ui" comment="Label of the Open system manager UI menu option">Abrir gestor do sistema</string>
<string name="settings__udm__dictionary_import_success" comment="Message for dictionary import success">Dicionário do utilizador importado com sucesso!</string>
<string name="settings__udm__dictionary_export_success" comment="Message for dictionary export success">Dicionário do utilizador exportado com sucesso!</string>
<string name="settings__udm__dialog__title_add" comment="Label for the title (when in adding mode) in the user dictionary add/edit dialog">Adicionar palavra</string>
<string name="settings__udm__dialog__title_edit" comment="Label for the title (when in editing mode) in the user dictionary add/edit dialog">Editar palavra</string>
<string name="settings__udm__dialog__word_label" comment="Label for the word in the user dictionary add/edit dialog">Palavra</string>
<string name="settings__udm__dialog__word_error_empty" comment="Error label for the word in the user dictionary add/edit dialog">Introduza uma palavra!</string>
<string name="settings__udm__dialog__word_error_invalid" comment="Error label for the word in the user dictionary add/edit dialog">Esta palavra tem caracteres inválidos.</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.">Frequência (entre %d e %d)</string>
<string name="settings__udm__dialog__freq_error_empty" comment="Error label for the frequency in the user dictionary add/edit dialog">Introduza um valor para a frequência!</string>
<string name="settings__udm__dialog__freq_error_invalid" comment="Error label for the frequency in the user dictionary add/edit dialog">Introduza um número válido dentro dos limites definidos!</string>
<string name="settings__udm__dialog__shortcut_label" comment="Label for the shortcut in the user dictionary add/edit dialog">Atalho (opcional)</string>
<string name="settings__udm__dialog__shortcut_error_invalid" comment="Error label for the shortcut in the user dictionary add/edit dialog">Este atalho tem caracteres inválidos.</string>
<string name="settings__udm__dialog__locale_label" comment="Label for the language code in the user dictionary add/edit dialog">Código do idioma (opcional)</string>
<string name="settings__udm__dialog__locale_error_invalid" comment="Error label for the language code in the user dictionary add/edit dialog">O código de idioma indicado não é coincidente com a sintaxe esperada. O código deve um idioma (pt), um idioma e um país (pt_PT) ou um idioma, um país e um script (pt_PT-script).</string>
<string name="settings__gestures__title" comment="Title of Gestures fragment">Gestos e deslize</string>
<string name="pref__glide__title" comment="Preference group title">Digitar ao deslizar</string>
<string name="pref__glide__enabled__label" comment="Preference title">Ativar escrita por egstos</string>
@@ -281,6 +312,8 @@
<string name="pref__devtools__enabled__label" comment="Label of Enable developer tools in Advanced">Ativar ferramentas de programador</string>
<string name="pref__devtools__enabled__summary" comment="Summary of Enable developer tools in Advanced">Ferramentas criadas para efeitos de depuração e resolução de erros</string>
<string name="pref__devtools__show_heap_memory_stats__label" comment="Label of Show heap memory stats in Advanced">Mostrar estatísticas de memória \'heap\'</string>
<string name="pref__devtools__clear_udm_internal_database__label" comment="Label of Clear internal user dictionary database in Advanced">Limpar base de dados do dicionário do utilizador</string>
<string name="pref__devtools__clear_udm_internal_database__summary" comment="Summary of Clear internal user dictionary database in Advanced">Remover todas as palavras da base de dados do dicionário</string>
<!-- About UI strings -->
<string name="about__title" comment="Title of About activity">Sobre o FlorisBoard</string>
<string name="about__app_icon_content_description" comment="Content description of app icon in About">Ícone do FlorisBoard</string>
@@ -296,6 +329,7 @@
<string name="assets__file__name">Nome</string>
<string name="assets__file__source">Fonte</string>
<string name="assets__action__add">Adicionar</string>
<string name="assets__action__apply">Aplicar</string>
<string name="assets__action__cancel">Cancelar</string>
<string name="assets__action__cancel_confirm_title">Confirmar</string>
<string name="assets__action__cancel_confirm_message">Tem a certeza de que deseja descartar as alterações? Esta ação não pode ser revertida.</string>

View File

@@ -169,6 +169,8 @@
<string name="pref__keyboard__utility_key_action__switch_language" comment="Preference value">Сменить язык</string>
<string name="pref__keyboard__utility_key_action__switch_keyboard_app" comment="Preference value">Переключить приложение клавиатуры</string>
<string name="pref__keyboard__utility_key_action__dynamic_switch_language_emojis" comment="Preference value">Динамическое: переключиться на эмодзи / переключить язык</string>
<string name="pref__keyboard__merge_hint_popups_enabled__label">Символы в всплывающей подсказке</string>
<string name="pref__keyboard_merge_hint_popups_enabled__summary">Включать символы в всплывающую подсказку раскладки по умолчанию</string>
<string name="pref__keyboard__font_size_multiplier_portrait__label" comment="Preference title">Мультипликатор размера шрифта (портрет)</string>
<string name="pref__keyboard__font_size_multiplier_landscape__label" comment="Preference title">Мультипликатор размера шрифта (ландшафт)</string>
<string name="pref__keyboard__group_layout__label" comment="Preference group title">Внешний вид</string>

View File

@@ -169,6 +169,8 @@
<string name="pref__keyboard__utility_key_action__switch_language" comment="Preference value">Dili değiştir</string>
<string name="pref__keyboard__utility_key_action__switch_keyboard_app" comment="Preference value">Klavye uygulamasını değiştir</string>
<string name="pref__keyboard__utility_key_action__dynamic_switch_language_emojis" comment="Preference value">Dinamik: Emojilerle değiştir / Dili değiştir</string>
<string name="pref__keyboard__merge_hint_popups_enabled__label">Sembol açılır pencerelerini de göster</string>
<string name="pref__keyboard_merge_hint_popups_enabled__summary">Sembol açılır pencerelerini varsayılan düzene ekle</string>
<string name="pref__keyboard__font_size_multiplier_portrait__label" comment="Preference title">Yazıtipi boyutu çarpanı (dikey)</string>
<string name="pref__keyboard__font_size_multiplier_landscape__label" comment="Preference title">Yazıtipi boyutu çarpanı (yatay)</string>
<string name="pref__keyboard__group_layout__label" comment="Preference group title">Yerleşim</string>
@@ -210,6 +212,15 @@
<string name="pref__smartbar__enabled__label" comment="Preference title">Akıllı Çubuğu Etkinleştir</string>
<string name="pref__smartbar__enabled__summary" comment="Preference summary">Klavyenin üstünde gösterilir</string>
<string name="pref__suggestion__title" comment="Preference group title">Öneriler</string>
<string name="pref__dictionary__title" comment="Preference group title">Sözlük</string>
<string name="pref__dictionary__enable_system_user_dictionary__label" comment="Preference title">Kullanıcı sözlüğünü etkinleştir</string>
<string name="pref__dictionary__enable_system_user_dictionary__summary" comment="Preference summary">Kullanıcı sözlüğünde saklanan kelimeleri öner</string>
<string name="pref__dictionary__manage_system_user_dictionary__label" comment="Preference title">Kullanıcı sözlüğünü yönet</string>
<string name="pref__dictionary__manage_system_user_dictionary__summary" comment="Preference summary">Kullanıcı sözlüğüne kelime ekle, çıkar veya görüntüle</string>
<string name="pref__dictionary__enable_internal_user_dictionary__label" comment="Preference title">Dahili kullanıcı sözlüğünü etkinleştir</string>
<string name="pref__dictionary__enable_internal_user_dictionary__summary" comment="Preference summary">Dahili kullanıcı sözlüğünde saklanan kelimeleri öner</string>
<string name="pref__dictionary__manage_floris_user_dictionary__label" comment="Preference title">Dahili kullanıcı sözlüğünü yönet</string>
<string name="pref__dictionary__manage_floris_user_dictionary__summary" comment="Preference summary">Dahili kullanıcı sözlüğüne kelime ekle, çıkar veya görüntüle</string>
<string name="pref__correction__title" comment="Preference group title">Düzeltmeler</string>
<string name="pref__correction__auto_capitalization__label" comment="Preference title">Otomatik olarak büyük harf yap</string>
<string name="pref__correction__auto_capitalization__summary" comment="Preference summary">İçeriğe göre kelimelerin ilk harfini büyüt</string>
@@ -217,6 +228,26 @@
<string name="pref__correction__remember_caps_lock_state__summary" comment="Preference summary">Başka bir yazım alanına geçerken büyük harf kilidini açık tut</string>
<string name="pref__correction__double_space_period__label" comment="Preference title">Çift boşlukta nokta</string>
<string name="pref__correction__double_space_period__summary" comment="Preference summary">Boşluk tuşuna 2 defa basmak bir nokta ve ardından bir boşluk koyar</string>
<string name="settings__udm__title_floris" comment="Title of the User Dictionary Manager activity for internal">Dahili Kullanıcı Sözlüğü</string>
<string name="settings__udm__title_system" comment="Title of the User Dictionary Manager activity for system">Kullanıcı Sözlüğü</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.">Sıklık: %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.">Sıklık: %d | Kısayol: %s</string>
<string name="settings__udm__all_languages" comment="Label of the For all languages entry in the language list">Tüm diller için</string>
<string name="settings__udm__open_system_manager_ui" comment="Label of the Open system manager UI menu option">Sistem yöneticisi arayüzünü aç</string>
<string name="settings__udm__dictionary_import_success" comment="Message for dictionary import success">Kullanıcı sözlüğü başarıyla içe aktarıldı!</string>
<string name="settings__udm__dictionary_export_success" comment="Message for dictionary export success">Kullanıcı sözlüğü başarıyla dışa aktarıldı!</string>
<string name="settings__udm__dialog__title_add" comment="Label for the title (when in adding mode) in the user dictionary add/edit dialog">Yeni sözcük ekle</string>
<string name="settings__udm__dialog__title_edit" comment="Label for the title (when in editing mode) in the user dictionary add/edit dialog">Sözcüğü düzenle</string>
<string name="settings__udm__dialog__word_label" comment="Label for the word in the user dictionary add/edit dialog">Sözcük</string>
<string name="settings__udm__dialog__word_error_empty" comment="Error label for the word in the user dictionary add/edit dialog">Lütfen bir sözcük girin!</string>
<string name="settings__udm__dialog__word_error_invalid" comment="Error label for the word in the user dictionary add/edit dialog">Bu sözcük geçersiz karakterler içeriyor.</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.">Sıklık (%d ve %d arasında)</string>
<string name="settings__udm__dialog__freq_error_empty" comment="Error label for the frequency in the user dictionary add/edit dialog">Lütfen bir sıklık değeri girin!</string>
<string name="settings__udm__dialog__freq_error_invalid" comment="Error label for the frequency in the user dictionary add/edit dialog">Lütfen sınırlar dahilinde geçerli bir sayı girin!</string>
<string name="settings__udm__dialog__shortcut_label" comment="Label for the shortcut in the user dictionary add/edit dialog">Kısayol (seçmeli)</string>
<string name="settings__udm__dialog__shortcut_error_invalid" comment="Error label for the shortcut in the user dictionary add/edit dialog">Bu kısayol geçersiz karakterler içeriyor.</string>
<string name="settings__udm__dialog__locale_label" comment="Label for the language code in the user dictionary add/edit dialog">Dil kodu (seçmeli)</string>
<string name="settings__udm__dialog__locale_error_invalid" comment="Error label for the language code in the user dictionary add/edit dialog">Bu dil kodu beklenen sözdizimine uymuyor. Kod ya sadece bir dil olmalı (tr gibi), ya hem dil hem de ülke kodu olmalı (tr_TR gibi), ya da hem dil hem ülke hem de betik olmalıdır (tr_TR-betik gibi).</string>
<string name="settings__gestures__title" comment="Title of Gestures fragment">Jestler &amp; Kaydırarak yazma</string>
<string name="pref__glide__title" comment="Preference group title">Kaydırarak Yazma</string>
<string name="pref__glide__enabled__label" comment="Preference title">Kaydırarak yazmayı etkinleştir</string>
@@ -282,6 +313,8 @@
<string name="pref__devtools__enabled__summary" comment="Summary of Enable developer tools in Advanced">Hata ayıklama ve sorun giderme için özel tasarlanmış araçlar</string>
<string name="pref__devtools__show_heap_memory_stats__label" comment="Label of Show heap memory stats in Advanced">Küme bellek istatistiklerini göster</string>
<string name="pref__devtools__show_heap_memory_stats__summary" comment="Summary of Show heap memory stats in Advanced">Küme bellek kullanımını ve en fazla boyutunu sağ üst köşede gösterir</string>
<string name="pref__devtools__clear_udm_internal_database__label" comment="Label of Clear internal user dictionary database in Advanced">Dahili kullanıcı sözlüğü veritabanını temizle</string>
<string name="pref__devtools__clear_udm_internal_database__summary" comment="Summary of Clear internal user dictionary database in Advanced">Sözlük veritabanı tablosundaki tüm sözcükleri temizler</string>
<!-- About UI strings -->
<string name="about__title" comment="Title of About activity">Hakkında</string>
<string name="about__app_icon_content_description" comment="Content description of app icon in About">FlorisBoardun uygulama simgesi</string>
@@ -297,6 +330,7 @@
<string name="assets__file__name">İsim</string>
<string name="assets__file__source">Kaynak</string>
<string name="assets__action__add">Ekle</string>
<string name="assets__action__apply">Uygula</string>
<string name="assets__action__cancel">İptal</string>
<string name="assets__action__cancel_confirm_title">İptali onayla</string>
<string name="assets__action__cancel_confirm_message">Kaydedilmemiş değişiklikleri iptal etmek istediğine emin misin? Bu işlem geri alınamaz.</string>

View File

@@ -25,6 +25,7 @@
<dimen name="key_textHintSize">10sp</dimen>
<dimen name="key_numeric_textSize">12sp</dimen>
<dimen name="key_popup_textSize">21sp</dimen>
<dimen name="key_space_textSize">12sp</dimen>
<dimen name="emoji_key_textSize">22sp</dimen>
<dimen name="devtools_memory_overlay_textSize">8sp</dimen>
@@ -56,16 +57,32 @@
<dimen name="fab_margin">16dp</dimen>
<dimen name="suggestion_chip_bg_padding_start">13dp</dimen>
<dimen name="suggestion_chip_bg_padding_top">0dp</dimen>
<dimen name="suggestion_chip_bg_padding_end">13dp</dimen>
<dimen name="suggestion_chip_bg_padding_bottom">0dp</dimen>
<dimen name="suggestion_chip_fg_title_margin_start">4dp</dimen>
<dimen name="suggestion_chip_fg_title_margin_top">0dp</dimen>
<dimen name="suggestion_chip_fg_title_margin_end">4dp</dimen>
<dimen name="suggestion_chip_fg_title_margin_bottom">0dp</dimen>
<dimen name="suggestion_chip_fg_subtitle_margin_start">4dp</dimen>
<dimen name="suggestion_chip_fg_subtitle_margin_top">0dp</dimen>
<dimen name="suggestion_chip_fg_subtitle_margin_end">4dp</dimen>
<dimen name="suggestion_chip_fg_subtitle_margin_bottom">0dp</dimen>
<dimen name="clipboard_text_item_pin_margin" >25dp</dimen>
<dimen name="gesture_distance_threshold_very_short">24dp</dimen>
<dimen name="gesture_distance_threshold_short">28dp</dimen>
<dimen name="gesture_distance_threshold_normal">32dp</dimen>
<dimen name="gesture_distance_threshold_long">36dp</dimen>
<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">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>
<integer name="gesture_velocity_threshold_very_slow">7000</integer>
<integer name="gesture_velocity_threshold_slow">10000</integer>
<integer name="gesture_velocity_threshold_normal">13000</integer>
<integer name="gesture_velocity_threshold_fast">16000</integer>
<integer name="gesture_velocity_threshold_very_fast">19000</integer>
</resources>

Some files were not shown because too many files have changed in this diff Show More