Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edc63aa680 | ||
|
|
23def145b2 | ||
|
|
3f7bd4f65d | ||
|
|
7b91d4f9d3 | ||
|
|
175369f7d7 | ||
|
|
79c5acc007 | ||
|
|
94d470dd96 | ||
|
|
ee9d61ad1e | ||
|
|
a3c7b538d0 | ||
|
|
ca4cd38bb2 | ||
|
|
7046c500ff | ||
|
|
0374a82f99 | ||
|
|
217acbd6f1 | ||
|
|
ef27d511be | ||
|
|
f9a4ffa5eb | ||
|
|
5533badd19 | ||
|
|
0f1b4b081d | ||
|
|
3feae09df0 | ||
|
|
34bb28d1fc | ||
|
|
551a294b05 | ||
|
|
671ff1d8b4 |
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,34 +1,34 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help fix a bug
|
||||
about: Create a report to help FlorisBoard improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
#### Short description of bug
|
||||
A short but clear and concise description of what the bug is.
|
||||
<!--
|
||||
- Describe the bug in a short but concise way.
|
||||
- If you have a screenshot or screen recording of the bug, link them at
|
||||
the end of this issue.
|
||||
- Please search existing bug reports to avoid creating duplicates.
|
||||
- Thank you for your help in making FlorisBoard better!
|
||||
-->
|
||||
|
||||
#### Steps to reproduce
|
||||
**Environment information**
|
||||
- FlorisBoard Version: <!-- e.g. 0.1.0 -->
|
||||
- Install Source: <!-- Google PlayStore/F-Droid/GitHub/? -->
|
||||
- Device: <!-- e.g. OnePlus 7T -->
|
||||
- Android version, ROM: <!-- e.g. 10, Stock -->
|
||||
|
||||
**Steps to reproduce**
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
#### Expected behavior
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
#### What happened instead?
|
||||
A detailed description of what you expected to happen. If you have screenshots or a screen recording, add it here.
|
||||
|
||||
#### Additional info
|
||||
- Version: [e.g. 0.1.0]
|
||||
- Source: [e.g. Google PlayStore/F-Droid/GitHub/?]
|
||||
- Device: [e.g. OnePlus 7T]
|
||||
- Android version, ROM: [e.g. 10, Stock]
|
||||
|
||||
#### Log
|
||||
<!--
|
||||
```
|
||||
If applicable, paste the captured debug log here.
|
||||
```
|
||||
-->
|
||||
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: General feedback
|
||||
url: https://github.com/florisboard/florisboard/blob/master/CONTRIBUTING.md
|
||||
about: Give general feedback about this project
|
||||
16
.github/ISSUE_TEMPLATE/feature_request.md
vendored
16
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,11 +1,19 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea or enhancement for this project
|
||||
name: Feature request / Suggestion
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: proposal
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Describe your idea in a short but concise way. If you have multiple ideas which are not directly connected to each other, file an issue per idea. This makes it easy to implement one feature proposal at a time. If you have any examples, e.g. screenshots or other keyboards which have the proposed feature implemented, link them here.
|
||||
Thank you for your help in making FlorisBoard better!
|
||||
<!--
|
||||
- Describe your idea in a short but concise way.
|
||||
- If you have multiple ideas which are not directly connected to each
|
||||
other, file an issue per idea. This makes it easy to implement one
|
||||
feature proposal at a time.
|
||||
- If you have any examples, e.g. screenshots or other keyboards which
|
||||
have the proposed feature implemented, link them here.
|
||||
- Please search existing proposals to avoid creating duplicates.
|
||||
- Thank you for your help in making FlorisBoard better!
|
||||
-->
|
||||
|
||||
16
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
16
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: Question
|
||||
about: Ask here if you have a question about FlorisBoard
|
||||
title: ''
|
||||
labels: question
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
- If you need assistance in using FlorisBoard, ask it here!
|
||||
- If you want to suggest an idea for this project, please use the
|
||||
Feature request template instead.
|
||||
- Please search existing questions to avoid creating duplicates.
|
||||
- Thank you for your help in making FlorisBoard better!
|
||||
-->
|
||||
@@ -5,6 +5,12 @@ First off, thanks for considering contributing to FlorisBoard!
|
||||
There are several ways to contribute to FlorisBoard. This document provides some
|
||||
general guidelines for each type of contribution.
|
||||
|
||||
## Giving general feedback
|
||||
|
||||
Either use the review function within Google Play or email me at
|
||||
[florisboard@patrickgold.dev](mailto:florisboard@patrickgold.dev). I
|
||||
love to hear from you!
|
||||
|
||||
## Adding a new feature or making large changes
|
||||
|
||||
If you intend to add a new feature or to make large changes, please discuss this
|
||||
@@ -51,6 +57,7 @@ Notes for tips below:
|
||||
will get accepted.
|
||||
|
||||
### Tips when updating a translation
|
||||
|
||||
- To update a translation, check the `strings.xml` in
|
||||
`app/src/main/res/values` for newly added strings and add them to the
|
||||
translation file in `app/src/main/res/values-<code>`
|
||||
|
||||
11
README.md
11
README.md
@@ -29,12 +29,8 @@ tester, follow these steps:
|
||||
_C. Use the APK provided in the release section of this repo_
|
||||
|
||||
##### Giving feedback
|
||||
If you want to give feedback to FlorisBoard, there are 2 ways to do so,
|
||||
as listed below:
|
||||
- *General feedback:* use the private feedback to developer section on
|
||||
the PlayStore listing.
|
||||
- *Bug reports or feature requests:* see the
|
||||
[contribution guidelines](CONTRIBUTING.md)
|
||||
If you want to give feedback to FlorisBoard, there are several ways to
|
||||
do so, as listed in the [contribution guidelines](CONTRIBUTING.md).
|
||||
|
||||
Thank you for contributing to FlorisBoard!
|
||||
|
||||
@@ -96,9 +92,10 @@ height="256" alt="Preview Image">
|
||||
### Other useful features
|
||||
* [x] One-handed mode
|
||||
* [x] Clipboard/cursor tools
|
||||
* [x] Integrated number row / symbols in character layouts (0.3.0)
|
||||
* [ ] Floating keyboard (0.4.0)
|
||||
* [x] Gesture support (0.3.0)
|
||||
* [ ] Glide typing (0.3.0)
|
||||
* [ ] Glide typing (0.4.0)
|
||||
* [x] Full integration in IME service list of Android (xml/method)
|
||||
(integration is internal-only, because Android's default subtype
|
||||
implementation not really allows for dynamic language/layout
|
||||
|
||||
@@ -10,8 +10,8 @@ android {
|
||||
applicationId "dev.patrickgold.florisboard"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 29
|
||||
versionCode 13
|
||||
versionName "0.2.1"
|
||||
versionCode 15
|
||||
versionName "0.2.3"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"a": [
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 226, "label": "â" },
|
||||
{ "code": 227, "label": "ã" },
|
||||
{ "code": 257, "label": "ā" },
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 230, "label": "æ" },
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 228, "label": "ä" }
|
||||
@@ -13,37 +13,37 @@
|
||||
{ "code": 240, "label": "ð" }
|
||||
],
|
||||
"e": [
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 281, "label": "ę" },
|
||||
{ "code": 279, "label": "ė" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 235, "label": "ë" },
|
||||
{ "code": 234, "label": "ê" }
|
||||
],
|
||||
"i": [
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 299, "label": "ī" },
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 303, "label": "į" },
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 239, "label": "ï" }
|
||||
],
|
||||
"l": [
|
||||
{ "code": 322, "label": "ł" }
|
||||
],
|
||||
"n": [
|
||||
{ "code": 324, "label": "ń" },
|
||||
{ "code": 241, "label": "ñ" }
|
||||
{ "code": 241, "label": "ñ" },
|
||||
{ "code": 324, "label": "ń" }
|
||||
],
|
||||
"o": [
|
||||
{ "code": 248, "label": "ø" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 339, "label": "œ" },
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 245, "label": "õ" },
|
||||
{ "code": 244, "label": "ô" },
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 248, "label": "ø" },
|
||||
{ "code": 246, "label": "ö" }
|
||||
],
|
||||
"s": [
|
||||
@@ -52,9 +52,9 @@
|
||||
{ "code": 353, "label": "š" }
|
||||
],
|
||||
"u": [
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 251, "label": "û" },
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 249, "label": "ù" }
|
||||
],
|
||||
@@ -69,6 +69,7 @@
|
||||
{ "code": 246, "label": "ö" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
@@ -83,14 +84,13 @@
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 63, "label": "?" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"a": [
|
||||
{ "code": 228, "label": "ä" },
|
||||
{ "code": 230, "label": "æ" },
|
||||
{ "code": 227, "label": "ã" },
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 257, "label": "ā" },
|
||||
{ "code": 228, "label": "ä" },
|
||||
{ "code": 226, "label": "â" },
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 225, "label": "á" }
|
||||
@@ -13,46 +13,47 @@
|
||||
{ "code": 231, "label": "ç" }
|
||||
],
|
||||
"e": [
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 234, "label": "ê" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 235, "label": "ë" }
|
||||
],
|
||||
"i": [
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 239, "label": "ï" },
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 299, "label": "ī" }
|
||||
],
|
||||
"n": [
|
||||
{ "code": 324, "label": "ń" },
|
||||
{ "code": 241, "label": "ñ" }
|
||||
{ "code": 241, "label": "ñ" },
|
||||
{ "code": 324, "label": "ń" }
|
||||
],
|
||||
"o": [
|
||||
{ "code": 246, "label": "ö" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 248, "label": "ø" },
|
||||
{ "code": 245, "label": "õ" },
|
||||
{ "code": 339, "label": "œ" },
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 246, "label": "ö" },
|
||||
{ "code": 244, "label": "ô" }
|
||||
],
|
||||
"s": [
|
||||
{ "code": 353, "label": "š" },
|
||||
{ "code": 223, "label": "ß" },
|
||||
{ "code": 353, "label": "š" },
|
||||
{ "code": 347, "label": "ś" }
|
||||
],
|
||||
"u": [
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 249, "label": "ù" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 251, "label": "û" },
|
||||
{ "code": 250, "label": "ú" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
@@ -67,14 +68,13 @@
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 63, "label": "?" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"a": [
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 230, "label": "æ" },
|
||||
{ "code": 227, "label": "ã" },
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 257, "label": "ā" },
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 226, "label": "â" },
|
||||
{ "code": 228, "label": "ä" }
|
||||
@@ -13,44 +13,45 @@
|
||||
{ "code": 231, "label": "ç" }
|
||||
],
|
||||
"e": [
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 234, "label": "ê" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 235, "label": "ë" }
|
||||
],
|
||||
"i": [
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 239, "label": "ï" },
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 299, "label": "ī" }
|
||||
],
|
||||
"n": [
|
||||
{ "code": 324, "label": "ń" },
|
||||
{ "code": 241, "label": "ñ" }
|
||||
{ "code": 241, "label": "ñ" },
|
||||
{ "code": 324, "label": "ń" }
|
||||
],
|
||||
"o": [
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 245, "label": "õ" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 339, "label": "œ" },
|
||||
{ "code": 248, "label": "ø" },
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 246, "label": "ö" },
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 244, "label": "ô" }
|
||||
],
|
||||
"s": [
|
||||
{ "code": 223, "label": "ß" }
|
||||
],
|
||||
"u": [
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 251, "label": "û" },
|
||||
{ "code": 249, "label": "ù" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
@@ -65,14 +66,13 @@
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 63, "label": "?" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,43 +1,44 @@
|
||||
{
|
||||
"a": [
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 261, "label": "ą" },
|
||||
{ "code": 230, "label": "æ" },
|
||||
{ "code": 257, "label": "ā" },
|
||||
{ "code": 170, "label": "ª" },
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 228, "label": "ä" },
|
||||
{ "code": 226, "label": "â" },
|
||||
{ "code": 227, "label": "ã" }
|
||||
],
|
||||
"c": [
|
||||
{ "code": 269, "label": "č" },
|
||||
{ "code": 231, "label": "ç" },
|
||||
{ "code": 269, "label": "č" },
|
||||
{ "code": 263, "label": "ć" }
|
||||
],
|
||||
"e": [
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 281, "label": "ę" },
|
||||
{ "code": 279, "label": "ė" },
|
||||
{ "code": 235, "label": "ë" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 234, "label": "ê" }
|
||||
],
|
||||
"i": [
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 299, "label": "ī" },
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 303, "label": "į" },
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 239, "label": "ï" }
|
||||
],
|
||||
"n": [
|
||||
{ "code": 324, "label": "ń" },
|
||||
{ "code": 241, "label": "ñ" }
|
||||
{ "code": 241, "label": "ñ" },
|
||||
{ "code": 324, "label": "ń" }
|
||||
],
|
||||
"o": [
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 186, "label": "º" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 248, "label": "ø" },
|
||||
@@ -45,20 +46,20 @@
|
||||
{ "code": 245, "label": "õ" },
|
||||
{ "code": 244, "label": "ô" },
|
||||
{ "code": 246, "label": "ö" },
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 242, "label": "ò" }
|
||||
],
|
||||
"s": [
|
||||
{ "code": 223, "label": "ß" }
|
||||
],
|
||||
"u": [
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 249, "label": "ù" },
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 251, "label": "û" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 58, "label": ":" },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 64, "label": "@" },
|
||||
@@ -73,14 +74,13 @@
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 63, "label": "?" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -34,9 +34,9 @@
|
||||
{ "code": 1610, "label": "ي" }
|
||||
],
|
||||
"ا": [
|
||||
{ "code": 1570, "label": "آ" },
|
||||
{ "code": 1649, "label": "ٱ" },
|
||||
{ "code": 1569, "label": "ء" },
|
||||
{ "code": 1570, "label": "آ" },
|
||||
{ "code": 1571, "label": "أ" },
|
||||
{ "code": 1573, "label": "إ" }
|
||||
],
|
||||
@@ -44,8 +44,8 @@
|
||||
{ "code": 1577, "label": "ة" }
|
||||
],
|
||||
"ک": [
|
||||
{ "code": 1603, "label": "ك" },
|
||||
{ "code": 1706, "label": "ڪ"}
|
||||
{ "code": 1706, "label": "ڪ"},
|
||||
{ "code": 1603, "label": "ك" }
|
||||
],
|
||||
"ز": [
|
||||
{ "code": 1688, "label": "ژ" }
|
||||
@@ -54,6 +54,7 @@
|
||||
{ "code": 1572, "label": "ؤ" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 1611, "label": "ً" },
|
||||
{ "code": 1622, "label": "ٖ" },
|
||||
{ "code": 1648, "label": "ٰ" },
|
||||
{ "code": 1619, "label": "ٓ" },
|
||||
@@ -66,15 +67,14 @@
|
||||
{ "code": 1617, "label": "ّ" },
|
||||
{ "code": 1612, "label": "ٌ" },
|
||||
{ "code": 1613, "label": "ٍ" },
|
||||
{ "code": 1611, "label": "ً" },
|
||||
{ "code": 1620, "label": "ٔ" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".ir"},
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".net" },
|
||||
{ "code": -255, "label": ".ir"}
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
{
|
||||
"a": [
|
||||
{ "code": 228, "label": "ä" },
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 226, "label": "â" },
|
||||
{ "code": 227, "label": "ã" },
|
||||
{ "code": 257, "label": "ā" },
|
||||
{ "code": 228, "label": "ä" },
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 230, "label": "æ" },
|
||||
{ "code": 224, "label": "à" }
|
||||
],
|
||||
"e": [
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 281, "label": "ę" },
|
||||
{ "code": 279, "label": "ė" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 235, "label": "ë" },
|
||||
{ "code": 234, "label": "ê" }
|
||||
],
|
||||
"i": [
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 299, "label": "ī" },
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 303, "label": "į" },
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 239, "label": "ï" }
|
||||
],
|
||||
"o": [
|
||||
{ "code": 246, "label": "ö" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 339, "label": "œ" },
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 245, "label": "õ" },
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 244, "label": "ô" },
|
||||
{ "code": 246, "label": "ö" },
|
||||
{ "code": 248, "label": "ø" }
|
||||
],
|
||||
"s": [
|
||||
@@ -42,15 +42,15 @@
|
||||
{ "code": 347, "label": "ś" }
|
||||
],
|
||||
"u": [
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 251, "label": "û" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 249, "label": "ù" }
|
||||
],
|
||||
"z": [
|
||||
{ "code": 380, "label": "ż" },
|
||||
{ "code": 382, "label": "ž" },
|
||||
{ "code": 380, "label": "ż" },
|
||||
{ "code": 378, "label": "ź" }
|
||||
],
|
||||
"ä": [
|
||||
@@ -60,6 +60,7 @@
|
||||
{ "code": 248, "label": "ø" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
@@ -74,14 +75,13 @@
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 63, "label": "?" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,42 +1,43 @@
|
||||
{
|
||||
"a": [
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 227, "label": "ã" },
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 257, "label": "ā" },
|
||||
{ "code": 170, "label": "ª" },
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 226, "label": "â" },
|
||||
{ "code": 230, "label": "æ" },
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 228, "label": "ä" }
|
||||
],
|
||||
"c": [
|
||||
{ "code": 269, "label": "č" },
|
||||
{ "code": 231, "label": "ç" },
|
||||
{ "code": 269, "label": "č" },
|
||||
{ "code": 263, "label": "ć" }
|
||||
],
|
||||
"e": [
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 281, "label": "ę" },
|
||||
{ "code": 279, "label": "ė" },
|
||||
{ "code": 234, "label": "ê" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 235, "label": "ë" }
|
||||
],
|
||||
"i": [
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 299, "label": "ī" },
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 303, "label": "į" },
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 239, "label": "ï" }
|
||||
],
|
||||
"n": [
|
||||
{ "code": 324, "label": "ń" },
|
||||
{ "code": 241, "label": "ñ" }
|
||||
{ "code": 241, "label": "ñ" },
|
||||
{ "code": 324, "label": "ń" }
|
||||
],
|
||||
"o": [
|
||||
{ "code": 244, "label": "ô" },
|
||||
{ "code": 186, "label": "º" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 245, "label": "õ" },
|
||||
@@ -44,18 +45,17 @@
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 246, "label": "ö" },
|
||||
{ "code": 244, "label": "ô" },
|
||||
{ "code": 339, "label": "œ" }
|
||||
],
|
||||
"s": [
|
||||
{ "code": 353, "label": "š" },
|
||||
{ "code": 223, "label": "ß" },
|
||||
{ "code": 353, "label": "š" },
|
||||
{ "code": 347, "label": "ś" }
|
||||
],
|
||||
"u": [
|
||||
{ "code": 249, "label": "ù" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 249, "label": "ù" },
|
||||
{ "code": 251, "label": "û" },
|
||||
{ "code": 250, "label": "ú" }
|
||||
],
|
||||
@@ -63,6 +63,7 @@
|
||||
{ "code": 255, "label": "ÿ" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
@@ -77,14 +78,13 @@
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 63, "label": "?" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"a": [
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 226, "label": "â" },
|
||||
{ "code": 227, "label": "ã" },
|
||||
{ "code": 257, "label": "ā" },
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 228, "label": "ä" },
|
||||
{ "code": 230, "label": "æ" },
|
||||
{ "code": 229, "label": "å" }
|
||||
@@ -13,39 +13,39 @@
|
||||
{ "code": 240, "label": "ð" }
|
||||
],
|
||||
"e": [
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 281, "label": "ę" },
|
||||
{ "code": 279, "label": "ė" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 235, "label": "ë" },
|
||||
{ "code": 234, "label": "ê" }
|
||||
],
|
||||
"i": [
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 299, "label": "ī" },
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 303, "label": "į" },
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 239, "label": "ï" }
|
||||
],
|
||||
"o": [
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 248, "label": "ø" },
|
||||
{ "code": 245, "label": "õ" },
|
||||
{ "code": 339, "label": "œ" },
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 244, "label": "ô" },
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 246, "label": "ö" }
|
||||
],
|
||||
"t": [
|
||||
{ "code": 254, "label": "þ" }
|
||||
],
|
||||
"u": [
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 251, "label": "û" },
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 249, "label": "ù" }
|
||||
],
|
||||
@@ -54,6 +54,7 @@
|
||||
{ "code": 255, "label": "ÿ" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
@@ -68,14 +69,13 @@
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 63, "label": "?" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,37 +1,38 @@
|
||||
{
|
||||
"a": [
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 227, "label": "ã" },
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 257, "label": "ā" },
|
||||
{ "code": 170, "label": "ª" },
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 226, "label": "â" },
|
||||
{ "code": 228, "label": "ä" },
|
||||
{ "code": 230, "label": "æ" }
|
||||
],
|
||||
"e": [
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 281, "label": "ę" },
|
||||
{ "code": 279, "label": "ė" },
|
||||
{ "code": 234, "label": "ê" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 235, "label": "ë" }
|
||||
],
|
||||
"i": [
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 299, "label": "ī" },
|
||||
{ "code": 239, "label": "ï" },
|
||||
{ "code": 303, "label": "į" },
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 237, "label": "í" }
|
||||
],
|
||||
"n": [
|
||||
{ "code": 324, "label": "ń" },
|
||||
{ "code": 241, "label": "ñ" }
|
||||
{ "code": 241, "label": "ñ" },
|
||||
{ "code": 324, "label": "ń" }
|
||||
],
|
||||
"o": [
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 186, "label": "º" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 339, "label": "œ" },
|
||||
@@ -39,17 +40,17 @@
|
||||
{ "code": 245, "label": "õ" },
|
||||
{ "code": 246, "label": "ö" },
|
||||
{ "code": 244, "label": "ô" },
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 243, "label": "ó" }
|
||||
],
|
||||
"u": [
|
||||
{ "code": 249, "label": "ù" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 251, "label": "û" },
|
||||
{ "code": 249, "label": "ù" },
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 252, "label": "ü" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
@@ -64,14 +65,13 @@
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 63, "label": "?" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"a": [
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 226, "label": "â" },
|
||||
{ "code": 227, "label": "ã" },
|
||||
{ "code": 257, "label": "ā" },
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 230, "label": "æ" },
|
||||
{ "code": 228, "label": "ä" },
|
||||
{ "code": 224, "label": "à" }
|
||||
@@ -13,28 +13,28 @@
|
||||
{ "code": 231, "label": "ç" }
|
||||
],
|
||||
"e": [
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 281, "label": "ę" },
|
||||
{ "code": 279, "label": "ė" },
|
||||
{ "code": 234, "label": "ê" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 235, "label": "ë" }
|
||||
],
|
||||
"o": [
|
||||
{ "code": 248, "label": "ø" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 339, "label": "œ" },
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 245, "label": "õ" },
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 244, "label": "ô" },
|
||||
{ "code": 248, "label": "ø" },
|
||||
{ "code": 246, "label": "ö" }
|
||||
],
|
||||
"u": [
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 249, "label": "ù" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 251, "label": "û" },
|
||||
{ "code": 250, "label": "ú" }
|
||||
],
|
||||
@@ -45,6 +45,7 @@
|
||||
{ "code": 246, "label": "ö" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
@@ -59,14 +60,13 @@
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 63, "label": "?" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"a": [
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 226, "label": "â" },
|
||||
{ "code": 227, "label": "ã" },
|
||||
{ "code": 257, "label": "ā" },
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 230, "label": "æ" },
|
||||
{ "code": 228, "label": "ä" },
|
||||
{ "code": 224, "label": "à" }
|
||||
@@ -13,11 +13,11 @@
|
||||
{ "code": 231, "label": "ç" }
|
||||
],
|
||||
"e": [
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 281, "label": "ę" },
|
||||
{ "code": 279, "label": "ė" },
|
||||
{ "code": 234, "label": "ê" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 235, "label": "ë" }
|
||||
],
|
||||
@@ -25,19 +25,19 @@
|
||||
{ "code": 236, "label": "ì" }
|
||||
],
|
||||
"o": [
|
||||
{ "code": 248, "label": "ø" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 339, "label": "œ" },
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 245, "label": "õ" },
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 244, "label": "ô" },
|
||||
{ "code": 248, "label": "ø" },
|
||||
{ "code": 246, "label": "ö" }
|
||||
],
|
||||
"u": [
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 249, "label": "ù" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 251, "label": "û" },
|
||||
{ "code": 250, "label": "ú" }
|
||||
],
|
||||
@@ -51,6 +51,7 @@
|
||||
{ "code": 246, "label": "ö" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
@@ -65,14 +66,13 @@
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 63, "label": "?" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,41 +1,42 @@
|
||||
{
|
||||
"a": [
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 228, "label": "ä" },
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 230, "label": "æ" },
|
||||
{ "code": 170, "label": "ª" },
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 227, "label": "ã" },
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 226, "label": "â" }
|
||||
],
|
||||
"c": [
|
||||
{ "code": 263, "label": "ć" },
|
||||
{ "code": 231, "label": "ç" },
|
||||
{ "code": 263, "label": "ć" },
|
||||
{ "code": 269, "label": "č" }
|
||||
],
|
||||
"e": [
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 235, "label": "ë" },
|
||||
{ "code": 279, "label": "ė" },
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 234, "label": "ê" },
|
||||
{ "code": 281, "label": "ę" }
|
||||
],
|
||||
"i": [
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 299, "label": "ī" },
|
||||
{ "code": 239, "label": "ï" },
|
||||
{ "code": 303, "label": "į" },
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 238, "label": "î" }
|
||||
],
|
||||
"n": [
|
||||
{ "code": 324, "label": "ń" },
|
||||
{ "code": 241, "label": "ñ" }
|
||||
{ "code": 241, "label": "ñ" },
|
||||
{ "code": 324, "label": "ń" }
|
||||
],
|
||||
"o": [
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 186, "label": "º" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 248, "label": "ø" },
|
||||
@@ -43,17 +44,17 @@
|
||||
{ "code": 246, "label": "ö" },
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 244, "label": "ô" },
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 245, "label": "õ" }
|
||||
],
|
||||
"u": [
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 249, "label": "ù" },
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 251, "label": "û" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
@@ -68,14 +69,13 @@
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 63, "label": "?" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"a": [
|
||||
{ "code": 228, "label": "ä" },
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 226, "label": "â" },
|
||||
{ "code": 261, "label": "ą" },
|
||||
{ "code": 227, "label": "ã" },
|
||||
{ "code": 228, "label": "ä" },
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 230, "label": "æ" },
|
||||
{ "code": 225, "label": "á" }
|
||||
],
|
||||
"c": [
|
||||
{ "code": 269, "label": "č" },
|
||||
{ "code": 231, "label": "ç" },
|
||||
{ "code": 269, "label": "č" },
|
||||
{ "code": 263, "label": "ć" }
|
||||
],
|
||||
"d": [
|
||||
@@ -19,36 +19,36 @@
|
||||
{ "code": 271, "label": "ď" }
|
||||
],
|
||||
"e": [
|
||||
{ "code": 234, "label": "ê" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 234, "label": "ê" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 235, "label": "ë" },
|
||||
{ "code": 281, "label": "ę" }
|
||||
],
|
||||
"i": [
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 239, "label": "ï" },
|
||||
{ "code": 299, "label": "ī" },
|
||||
{ "code": 303, "label": "į" },
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 236, "label": "ì" }
|
||||
],
|
||||
"l": [
|
||||
{ "code": 322, "label": "ł" }
|
||||
],
|
||||
"n": [
|
||||
{ "code": 328, "label": "ň" },
|
||||
{ "code": 324, "label": "ń" },
|
||||
{ "code": 328, "label": "ň" },
|
||||
{ "code": 241, "label": "ñ" }
|
||||
],
|
||||
"o": [
|
||||
{ "code": 246, "label": "ö" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 245, "label": "õ" },
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 244, "label": "ô" },
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 339, "label": "œ" },
|
||||
{ "code": 246, "label": "ö" },
|
||||
{ "code": 248, "label": "ø" }
|
||||
],
|
||||
"r": [
|
||||
@@ -65,9 +65,9 @@
|
||||
{ "code": 254, "label": "þ" }
|
||||
],
|
||||
"u": [
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 249, "label": "ù" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 251, "label": "û" }
|
||||
],
|
||||
@@ -76,18 +76,19 @@
|
||||
{ "code": 255, "label": "ÿ" }
|
||||
],
|
||||
"z": [
|
||||
{ "code": 380, "label": "ż" },
|
||||
{ "code": 378, "label": "ź" },
|
||||
{ "code": 380, "label": "ż" },
|
||||
{ "code": 382, "label": "ž" }
|
||||
],
|
||||
"ä": [
|
||||
{ "code": 230, "label": "æ" }
|
||||
],
|
||||
"ö": [
|
||||
{ "code": 339, "label": "œ" },
|
||||
{ "code": 248, "label": "ø" }
|
||||
{ "code": 248, "label": "ø" },
|
||||
{ "code": 339, "label": "œ" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
@@ -102,14 +103,13 @@
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 63, "label": "?" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,739 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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
|
||||
|
||||
import android.inputmethodservice.InputMethodService
|
||||
import android.os.Build
|
||||
import android.text.InputType
|
||||
import android.view.KeyEvent
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.ExtractedTextRequest
|
||||
import androidx.annotation.RequiresApi
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
import java.lang.StringBuilder
|
||||
|
||||
// Constants for detectLastUnicodeCharacterLengthBeforeCursor method
|
||||
private const val LIGHT_SKIN_TONE = 0x1F3FB
|
||||
private const val MEDIUM_LIGHT_SKIN_TONE = 0x1F3FC
|
||||
private const val MEDIUM_SKIN_TONE = 0x1F3FD
|
||||
private const val MEDIUM_DARK_SKIN_TONE = 0x1F3FE
|
||||
private const val DARK_SKIN_TONE = 0x1F3FF
|
||||
private const val RED_HAIR = 0x1F9B0
|
||||
private const val CURLY_HAIR = 0x1F9B1
|
||||
private const val WHITE_HAIR = 0x1F9B2
|
||||
private const val BALD = 0x1F9B3
|
||||
private const val ZERO_WIDTH_JOINER = 0x200D
|
||||
private const val VARIATION_SELECTOR = 0xFE0F
|
||||
|
||||
// Array which holds all variations for easier checking (convenience only)
|
||||
private val emojiVariationArray: Array<Int> = arrayOf(
|
||||
LIGHT_SKIN_TONE,
|
||||
MEDIUM_LIGHT_SKIN_TONE,
|
||||
MEDIUM_SKIN_TONE,
|
||||
MEDIUM_DARK_SKIN_TONE,
|
||||
DARK_SKIN_TONE,
|
||||
RED_HAIR,
|
||||
CURLY_HAIR,
|
||||
WHITE_HAIR,
|
||||
BALD
|
||||
)
|
||||
|
||||
/**
|
||||
* Class which holds information relevant to an editor instance like the input [cachedText], [selection],
|
||||
* [inputAttributes], [imeOptions], etc. This class is thought to be an improved [EditorInfo]
|
||||
* object which also holds the state of the currently focused input editor.
|
||||
*/
|
||||
class EditorInstance private constructor(private val ims: InputMethodService?) {
|
||||
val cursorCapsMode: InputAttributes.CapsMode
|
||||
get() {
|
||||
val ic = ims?.currentInputConnection ?: return InputAttributes.CapsMode.NONE
|
||||
return InputAttributes.CapsMode.fromFlags(
|
||||
ic.getCursorCapsMode(inputAttributes.capsMode.toFlags())
|
||||
)
|
||||
}
|
||||
var currentWord: Region = Region(this)
|
||||
private set
|
||||
var imeOptions: ImeOptions = ImeOptions.fromImeOptionsInt(EditorInfo.IME_NULL)
|
||||
private set
|
||||
var inputAttributes: InputAttributes = InputAttributes.fromInputTypeInt(InputType.TYPE_NULL)
|
||||
private set
|
||||
var isComposingEnabled: Boolean = false
|
||||
set(v) {
|
||||
field = v
|
||||
reevaluate()
|
||||
if (v) {
|
||||
markComposingRegion(currentWord)
|
||||
} else {
|
||||
markComposingRegion(null)
|
||||
}
|
||||
}
|
||||
var isNewSelectionInBoundsOfOld: Boolean = false
|
||||
private set
|
||||
var packageName: String = "undefined"
|
||||
private set
|
||||
var selection: Selection = Selection(this)
|
||||
private set
|
||||
val cachedText: String
|
||||
get() = cachedTextInternal.toString()
|
||||
private var cachedTextInternal: StringBuilder = StringBuilder("")
|
||||
|
||||
companion object {
|
||||
fun default(): EditorInstance {
|
||||
return EditorInstance(null)
|
||||
}
|
||||
|
||||
fun from(editorInfo: EditorInfo?, ims: InputMethodService?): EditorInstance {
|
||||
return if (editorInfo == null) { default() } else {
|
||||
EditorInstance(ims).apply {
|
||||
imeOptions = ImeOptions.fromImeOptionsInt(editorInfo.imeOptions)
|
||||
inputAttributes = InputAttributes.fromInputTypeInt(editorInfo.inputType)
|
||||
packageName = editorInfo.packageName
|
||||
/*selection = Selection(this).apply {
|
||||
start = editorInfo.initialSelStart
|
||||
end = editorInfo.initialSelEnd
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
fetchExtractedTextFromInputConnection()
|
||||
reevaluate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler which reacts to selection updates coming from the target app's editor.
|
||||
*/
|
||||
fun onUpdateSelection(
|
||||
oldSelStart: Int, oldSelEnd: Int,
|
||||
newSelStart: Int, newSelEnd: Int
|
||||
) {
|
||||
fetchExtractedTextFromInputConnection()
|
||||
isNewSelectionInBoundsOfOld =
|
||||
newSelStart >= (oldSelStart - 1) &&
|
||||
newSelStart <= (oldSelStart + 1) &&
|
||||
newSelEnd >= (oldSelEnd - 1) &&
|
||||
newSelEnd <= (oldSelEnd + 1)
|
||||
selection.apply {
|
||||
start = newSelStart
|
||||
end = newSelEnd
|
||||
}
|
||||
reevaluate()
|
||||
if (selection.isCursorMode && isComposingEnabled) {
|
||||
markComposingRegion(currentWord)
|
||||
} else {
|
||||
markComposingRegion(null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes the given [text] in the current composing region. Does nothing if the current
|
||||
* composing region is of zero length or null.
|
||||
*
|
||||
* @param text The text to complete in this editor's composing region.
|
||||
*
|
||||
* @returns True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun commitCompletion(text: String): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits the given [text] to this editor instance and adjusts both the cursor position and
|
||||
* composing region, if any.
|
||||
*
|
||||
* This method overwrites any selected text and replaces it with given [text]. If there is no
|
||||
* text selected (selection is in cursor mode), then this method will insert the [text] after
|
||||
* the cursor, then set the cursor position to the first character after the inserted text.
|
||||
*
|
||||
* @param text The text to commit.
|
||||
*
|
||||
* @returns True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun commitText(text: String): Boolean {
|
||||
val ic = ims?.currentInputConnection ?: return false
|
||||
ic.beginBatchEdit()
|
||||
markComposingRegion(null)
|
||||
if (selection.isCursorMode) {
|
||||
cachedTextInternal.insert(selection.start, text)
|
||||
} else if (selection.isSelectionMode) {
|
||||
cachedTextInternal.replace(selection.start, selection.end, text)
|
||||
}
|
||||
selection.apply {
|
||||
start += text.length
|
||||
end = start
|
||||
}
|
||||
reevaluate()
|
||||
ic.commitText(text, 1)
|
||||
if (isComposingEnabled) {
|
||||
markComposingRegion(currentWord)
|
||||
}
|
||||
ic.setSelection(selection.start, selection.end)
|
||||
ic.endBatchEdit()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a backward delete on this editor's text. If a text selection is active, all
|
||||
* characters inside this selection will be removed, else only the left-most character from
|
||||
* the cursor's position.
|
||||
*
|
||||
* @returns True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun deleteBackwards(): Boolean {
|
||||
val ic = ims?.currentInputConnection ?: return false
|
||||
ic.beginBatchEdit()
|
||||
markComposingRegion(null)
|
||||
if (selection.isCursorMode && selection.start > 0) {
|
||||
val length = detectLastUnicodeCharacterLengthBeforeCursor()
|
||||
cachedTextInternal.replace(selection.start - length, selection.start, "")
|
||||
selection.apply {
|
||||
start -= length
|
||||
end = start
|
||||
}
|
||||
ic.deleteSurroundingText(length, 0)
|
||||
} else if (selection.isSelectionMode) {
|
||||
cachedTextInternal.replace(selection.start, selection.end, "")
|
||||
selection.apply {
|
||||
end = start
|
||||
}
|
||||
ic.commitText("", 1)
|
||||
}
|
||||
reevaluate()
|
||||
if (isComposingEnabled) {
|
||||
markComposingRegion(currentWord)
|
||||
}
|
||||
ic.setSelection(selection.start, selection.end)
|
||||
ic.endBatchEdit()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes [n] words before the current cursor's position.
|
||||
* NOTE: this implementation does currently only delete currentWord. This is due to change in
|
||||
* future versions.
|
||||
*
|
||||
* @param n The number of words to delete before the cursor. Must be greater than 0 or this
|
||||
* method will fail.
|
||||
*
|
||||
* @returns True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun deleteWordsBeforeCursor(n: Int): Boolean {
|
||||
val ic = ims?.currentInputConnection ?: return false
|
||||
if (n < 1) {
|
||||
return false
|
||||
}
|
||||
ic.beginBatchEdit()
|
||||
markComposingRegion(null)
|
||||
if (currentWord.isValid) {
|
||||
cachedTextInternal.replace(currentWord.start, currentWord.end, "")
|
||||
selection.apply {
|
||||
start = currentWord.start
|
||||
end = start
|
||||
}
|
||||
ic.setSelection(currentWord.start, currentWord.end)
|
||||
ic.commitText("", 1)
|
||||
}
|
||||
reevaluate()
|
||||
ic.setSelection(selection.start, selection.end)
|
||||
ic.endBatchEdit()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets [n] characters after the cursor's current position. The resulting string may be any
|
||||
* length ranging from 0 to n.
|
||||
*
|
||||
* @param n The number of characters to get after the cursor. Must be greater than 0 or this
|
||||
* method will fail.
|
||||
*
|
||||
* @returns [n] or less characters after the cursor.
|
||||
*/
|
||||
fun getTextAfterCursor(n: Int): String {
|
||||
if (!selection.isValid || n < 1) {
|
||||
return ""
|
||||
}
|
||||
val from = selection.end
|
||||
val to = (selection.end + n).coerceAtMost(cachedTextInternal.length)
|
||||
return cachedTextInternal.substring(from, to)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets [n] characters before the cursor's current position. The resulting string may be any
|
||||
* length ranging from 0 to n.
|
||||
*
|
||||
* @param n The number of characters to get before the cursor. Must be greater than 0 or this
|
||||
* method will fail.
|
||||
*
|
||||
* @returns [n] or less characters after the cursor.
|
||||
*/
|
||||
fun getTextBeforeCursor(n: Int): String {
|
||||
if (!selection.isValid || n < 1) {
|
||||
return ""
|
||||
}
|
||||
val from = (selection.start - n).coerceAtLeast(0)
|
||||
val to = selection.start
|
||||
return cachedTextInternal.substring(from, to)
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a given [action] on the current input editor.
|
||||
*
|
||||
* @param action The action to be performed on this editor instance.
|
||||
*
|
||||
* @returns True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun performEnterAction(action: ImeOptions.Action): Boolean {
|
||||
val ic = ims?.currentInputConnection ?: return false
|
||||
return ic.performEditorAction(action.toInt())
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a given [keyCode] as a [KeyEvent.ACTION_DOWN].
|
||||
*
|
||||
* @param ic The input connection on which this operation should be performed.
|
||||
* @param keyCode The key code to send, use a key code defined in Android's [KeyEvent], not in
|
||||
* [KeyCode] or this call may send a weird character, as this key codes do not match!!
|
||||
*
|
||||
* @returns True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun sendSystemKeyEvent(keyCode: Int): Boolean {
|
||||
val ic = ims?.currentInputConnection ?: return false
|
||||
return ic.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, keyCode))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a given [keyCode] as a [KeyEvent.ACTION_DOWN] with ALT pressed.
|
||||
*
|
||||
* @param ic The input connection on which this operation should be performed.
|
||||
* @param keyCode The key code to send, use a key code defined in Android's [KeyEvent], not in
|
||||
* [KeyCode] or this call may send a weird character, as this key codes do not match!!
|
||||
*
|
||||
* @returns True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun sendSystemKeyEventAlt(keyCode: Int): Boolean {
|
||||
val ic = ims?.currentInputConnection ?: return false
|
||||
return ic.sendKeyEvent(
|
||||
KeyEvent(
|
||||
0,
|
||||
1,
|
||||
KeyEvent.ACTION_DOWN, keyCode,
|
||||
0,
|
||||
KeyEvent.META_ALT_LEFT_ON
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the selection region of this instance and notifies the input connection.
|
||||
*
|
||||
* @param from The start index of the selection in characters (inclusive).
|
||||
* @param to The end index of the selection in characters (exclusive).
|
||||
*
|
||||
* @returns True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun setSelection(from: Int, to: Int): Boolean {
|
||||
val ic = ims?.currentInputConnection ?: return false
|
||||
selection.apply {
|
||||
start = from
|
||||
end = to
|
||||
}
|
||||
return ic.setSelection(from, to)
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the length of the character before the cursor, as many Unicode characters nowadays
|
||||
* are longer than 1 Java char and thus the length has to be calculated in order to avoid
|
||||
* deleting only half of an emoji...
|
||||
* Is used primarily in [deleteBackwards].
|
||||
*
|
||||
* @returns The length of the last Unicode character, in Java characters or 0 if the current
|
||||
* selection is invalid.
|
||||
*/
|
||||
private fun detectLastUnicodeCharacterLengthBeforeCursor(): Int {
|
||||
if (!selection.isValid) {
|
||||
return 0
|
||||
}
|
||||
var charIndex = 0
|
||||
var charLength = 0
|
||||
var charShouldGlue = false
|
||||
val textToSearch = cachedTextInternal.substring(0, selection.start.coerceAtMost(cachedTextInternal.length))
|
||||
var i = 0
|
||||
while (i < textToSearch.length) {
|
||||
val cp = textToSearch.codePointAt(i)
|
||||
val cpLength = Character.charCount(cp)
|
||||
when {
|
||||
charShouldGlue || cp == VARIATION_SELECTOR || emojiVariationArray.contains(cp) -> {
|
||||
charLength += cpLength
|
||||
charShouldGlue = false
|
||||
}
|
||||
cp == ZERO_WIDTH_JOINER -> {
|
||||
charLength += cpLength
|
||||
charShouldGlue = true
|
||||
}
|
||||
else -> {
|
||||
charIndex = i
|
||||
charLength = 0
|
||||
charShouldGlue = false
|
||||
}
|
||||
}
|
||||
i += cpLength
|
||||
}
|
||||
return textToSearch.length - charIndex
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current text from the app's editor view.
|
||||
*
|
||||
* @returns The target editor's content string.
|
||||
*/
|
||||
private fun fetchExtractedTextFromInputConnection() {
|
||||
val ic = ims?.currentInputConnection ?: return
|
||||
val et = ic.getExtractedText(
|
||||
ExtractedTextRequest(), 0
|
||||
) ?: return
|
||||
val text = et.text ?: ""
|
||||
cachedTextInternal.setLength(0)
|
||||
cachedTextInternal.append(text)
|
||||
selection.apply {
|
||||
start = et.selectionStart.coerceAtMost(cachedTextInternal.length)
|
||||
end = et.selectionEnd.coerceAtMost(cachedTextInternal.length)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a given [region] as composing and notifies the input connection.
|
||||
*
|
||||
* @param region The region which should be marked as composing.
|
||||
*
|
||||
* @returns True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
private fun markComposingRegion(region: Region?): Boolean {
|
||||
val ic = ims?.currentInputConnection ?: return false
|
||||
return when (region) {
|
||||
null -> ic.finishComposingText()
|
||||
else -> if (region.isValid) {
|
||||
ic.setComposingRegion(region.start, region.end)
|
||||
} else {
|
||||
ic.finishComposingText()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the current word in this editor instance based on the current cursor position and
|
||||
* given delimiter [regex].
|
||||
*
|
||||
* @param regex The delimiter regex which should be used to split up the content text and find
|
||||
* words. May differ from locale to locale.
|
||||
*
|
||||
* @returns True on success, false if no current word could be found.
|
||||
*/
|
||||
private fun reevaluateCurrentWord(regex: Regex): Boolean {
|
||||
var foundValidWord = false
|
||||
if (selection.isValid && selection.isCursorMode) {
|
||||
val words = cachedText.split("((?<=$regex)|(?=$regex))".toRegex())
|
||||
var pos = 0
|
||||
for (word in words) {
|
||||
if (selection.start >= pos && selection.start <= pos + word.length &&
|
||||
word.isNotEmpty() && !word.matches(regex)) {
|
||||
currentWord.apply {
|
||||
start = pos
|
||||
end = pos + word.length
|
||||
}
|
||||
foundValidWord = true
|
||||
break
|
||||
} else {
|
||||
pos += word.length
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!foundValidWord) {
|
||||
currentWord.apply {
|
||||
start = -1
|
||||
end = -1
|
||||
}
|
||||
}
|
||||
return foundValidWord
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers all reevaluation processes.
|
||||
*/
|
||||
private fun reevaluate() {
|
||||
val regex = "[^\\p{L}]".toRegex()
|
||||
reevaluateCurrentWord(regex)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class which holds the same information as an [EditorInfo.imeOptions] int but more accessible and
|
||||
* readable.
|
||||
*/
|
||||
class ImeOptions private constructor(imeOptions: Int) {
|
||||
val action: Action = Action.fromInt(imeOptions)
|
||||
val flagForceAscii: Boolean = imeOptions and EditorInfo.IME_FLAG_FORCE_ASCII > 0
|
||||
val flagNavigateNext: Boolean = imeOptions and EditorInfo.IME_FLAG_NAVIGATE_NEXT > 0
|
||||
val flagNavigatePrevious: Boolean = imeOptions and EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS > 0
|
||||
val flagNoAccessoryAction: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_ACCESSORY_ACTION > 0
|
||||
val flagNoEnterAction: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_ENTER_ACTION > 0
|
||||
val flagNoExtractUi: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_EXTRACT_UI > 0
|
||||
val flagNoFullscreen: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_FULLSCREEN > 0
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
val flagNoPersonalizedLearning: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING > 0
|
||||
|
||||
companion object {
|
||||
fun fromImeOptionsInt(imeOptions: Int): ImeOptions {
|
||||
return ImeOptions(imeOptions)
|
||||
}
|
||||
}
|
||||
|
||||
enum class Action {
|
||||
DONE,
|
||||
GO,
|
||||
NEXT,
|
||||
NONE,
|
||||
PREVIOUS,
|
||||
SEARCH,
|
||||
SEND,
|
||||
UNSPECIFIED;
|
||||
|
||||
companion object {
|
||||
fun fromInt(raw: Int): Action {
|
||||
return when (raw and EditorInfo.IME_MASK_ACTION) {
|
||||
EditorInfo.IME_ACTION_DONE -> DONE
|
||||
EditorInfo.IME_ACTION_GO -> GO
|
||||
EditorInfo.IME_ACTION_NEXT -> NEXT
|
||||
EditorInfo.IME_ACTION_NONE -> NONE
|
||||
EditorInfo.IME_ACTION_PREVIOUS -> PREVIOUS
|
||||
EditorInfo.IME_ACTION_SEARCH -> SEARCH
|
||||
EditorInfo.IME_ACTION_SEND -> SEND
|
||||
EditorInfo.IME_ACTION_UNSPECIFIED -> UNSPECIFIED
|
||||
else -> NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toInt(): Int {
|
||||
return when (this) {
|
||||
DONE -> EditorInfo.IME_ACTION_DONE
|
||||
GO -> EditorInfo.IME_ACTION_GO
|
||||
NEXT -> EditorInfo.IME_ACTION_NEXT
|
||||
NONE -> EditorInfo.IME_ACTION_NONE
|
||||
PREVIOUS -> EditorInfo.IME_ACTION_PREVIOUS
|
||||
SEARCH -> EditorInfo.IME_ACTION_SEARCH
|
||||
SEND -> EditorInfo.IME_ACTION_SEND
|
||||
UNSPECIFIED-> EditorInfo.IME_ACTION_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class which holds the same information as an [EditorInfo.inputType] int but more accessible and
|
||||
* readable.
|
||||
*/
|
||||
class InputAttributes private constructor(inputType: Int) {
|
||||
val type: Type
|
||||
val variation: Variation
|
||||
val capsMode: CapsMode
|
||||
var flagNumberDecimal: Boolean = false
|
||||
private set
|
||||
var flagNumberSigned: Boolean = false
|
||||
private set
|
||||
var flagTextAutoComplete: Boolean = false
|
||||
private set
|
||||
var flagTextAutoCorrect: Boolean = false
|
||||
private set
|
||||
var flagTextImeMultiLine: Boolean = false
|
||||
private set
|
||||
var flagTextMultiLine: Boolean = false
|
||||
private set
|
||||
var flagTextNoSuggestions: Boolean = false
|
||||
private set
|
||||
|
||||
init {
|
||||
when (inputType and InputType.TYPE_MASK_CLASS) {
|
||||
InputType.TYPE_CLASS_DATETIME -> {
|
||||
type = Type.DATETIME
|
||||
variation = when (inputType and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_DATETIME_VARIATION_DATE -> Variation.DATE
|
||||
InputType.TYPE_DATETIME_VARIATION_NORMAL -> Variation.NORMAL
|
||||
InputType.TYPE_DATETIME_VARIATION_TIME -> Variation.TIME
|
||||
else -> Variation.NORMAL
|
||||
}
|
||||
capsMode = CapsMode.NONE
|
||||
}
|
||||
InputType.TYPE_CLASS_NUMBER -> {
|
||||
type = Type.NUMBER
|
||||
variation = when (inputType and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_NUMBER_VARIATION_NORMAL -> Variation.NORMAL
|
||||
InputType.TYPE_NUMBER_VARIATION_PASSWORD -> Variation.PASSWORD
|
||||
else -> Variation.NORMAL
|
||||
}
|
||||
capsMode = CapsMode.NONE
|
||||
flagNumberDecimal = inputType and InputType.TYPE_NUMBER_FLAG_DECIMAL > 0
|
||||
flagNumberSigned = inputType and InputType.TYPE_NUMBER_FLAG_SIGNED > 0
|
||||
}
|
||||
InputType.TYPE_CLASS_PHONE -> {
|
||||
type = Type.PHONE
|
||||
variation = Variation.NORMAL
|
||||
capsMode = CapsMode.NONE
|
||||
}
|
||||
InputType.TYPE_CLASS_TEXT -> {
|
||||
type = Type.TEXT
|
||||
variation = when (inputType and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS -> Variation.EMAIL_ADDRESS
|
||||
InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT -> Variation.EMAIL_SUBJECT
|
||||
InputType.TYPE_TEXT_VARIATION_FILTER -> Variation.FILTER
|
||||
InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE -> Variation.LONG_MESSAGE
|
||||
InputType.TYPE_TEXT_VARIATION_NORMAL -> Variation.NORMAL
|
||||
InputType.TYPE_TEXT_VARIATION_PASSWORD -> Variation.PASSWORD
|
||||
InputType.TYPE_TEXT_VARIATION_PERSON_NAME -> Variation.PERSON_NAME
|
||||
InputType.TYPE_TEXT_VARIATION_PHONETIC -> Variation.PHONETIC
|
||||
InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS -> Variation.POSTAL_ADDRESS
|
||||
InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE -> Variation.SHORT_MESSAGE
|
||||
InputType.TYPE_TEXT_VARIATION_URI -> Variation.URI
|
||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD -> Variation.VISIBLE_PASSWORD
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT -> Variation.WEB_EDIT_TEXT
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS -> Variation.WEB_EMAIL_ADDRESS
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD -> Variation.WEB_PASSWORD
|
||||
else -> Variation.NORMAL
|
||||
}
|
||||
capsMode = CapsMode.fromFlags(inputType)
|
||||
flagTextAutoComplete = inputType and InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE > 0
|
||||
flagTextAutoCorrect = inputType and InputType.TYPE_TEXT_FLAG_AUTO_CORRECT > 0
|
||||
flagTextImeMultiLine = inputType and InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE > 0
|
||||
flagTextMultiLine = inputType and InputType.TYPE_TEXT_FLAG_MULTI_LINE > 0
|
||||
flagTextNoSuggestions = inputType and InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS > 0
|
||||
}
|
||||
else -> {
|
||||
type = Type.TEXT
|
||||
variation = Variation.NORMAL
|
||||
capsMode = CapsMode.NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromInputTypeInt(inputType: Int): InputAttributes {
|
||||
return InputAttributes(inputType)
|
||||
}
|
||||
}
|
||||
|
||||
enum class Type {
|
||||
DATETIME,
|
||||
NUMBER,
|
||||
PHONE,
|
||||
TEXT;
|
||||
}
|
||||
|
||||
enum class Variation {
|
||||
DATE,
|
||||
EMAIL_ADDRESS,
|
||||
EMAIL_SUBJECT,
|
||||
FILTER,
|
||||
LONG_MESSAGE,
|
||||
NORMAL,
|
||||
PASSWORD,
|
||||
PERSON_NAME,
|
||||
PHONETIC,
|
||||
POSTAL_ADDRESS,
|
||||
SHORT_MESSAGE,
|
||||
TIME,
|
||||
URI,
|
||||
VISIBLE_PASSWORD,
|
||||
WEB_EDIT_TEXT,
|
||||
WEB_EMAIL_ADDRESS,
|
||||
WEB_PASSWORD;
|
||||
}
|
||||
|
||||
enum class CapsMode {
|
||||
ALL,
|
||||
NONE,
|
||||
SENTENCES,
|
||||
WORDS;
|
||||
|
||||
companion object {
|
||||
fun fromFlags(flags: Int): CapsMode {
|
||||
return when {
|
||||
flags and InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS > 0 -> ALL
|
||||
flags and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES > 0 -> SENTENCES
|
||||
flags and InputType.TYPE_TEXT_FLAG_CAP_WORDS > 0 -> WORDS
|
||||
else -> NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toFlags(): Int {
|
||||
return when (this) {
|
||||
ALL -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
|
||||
SENTENCES -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
||||
WORDS -> InputType.TYPE_TEXT_FLAG_CAP_WORDS
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class which marks a region of the [text] in [editorInstance].
|
||||
*/
|
||||
open class Region(private val editorInstance: EditorInstance) {
|
||||
var start: Int = -1
|
||||
var end: Int = -1
|
||||
val isValid: Boolean
|
||||
get() = start >= 0 && end >= 0 && length >= 0
|
||||
val length: Int
|
||||
get() = end - start
|
||||
val text: String
|
||||
get() {
|
||||
val eiText = editorInstance.cachedText
|
||||
return if (!isValid || start >= eiText.length) {
|
||||
""
|
||||
} else {
|
||||
val end = if (end >= eiText.length) { eiText.length } else { end }
|
||||
editorInstance.cachedText.substring(start, end)
|
||||
}
|
||||
}
|
||||
|
||||
override operator fun equals(other: Any?): Boolean {
|
||||
return if (other is Region) {
|
||||
start == other.start && end == other.end
|
||||
} else {
|
||||
super.equals(other)
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = start
|
||||
result = 31 * result + end
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class which holds selection attributes and returns the correct text for set selection based on
|
||||
* the text in [editorInstance].
|
||||
*/
|
||||
class Selection(private val editorInstance: EditorInstance) : Region(editorInstance) {
|
||||
val isCursorMode: Boolean
|
||||
get() = length == 0 && isValid
|
||||
val isSelectionMode: Boolean
|
||||
get() = length != 0 && isValid
|
||||
}
|
||||
@@ -30,7 +30,6 @@ import android.util.Log
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.CursorAnchorInfo
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputConnection
|
||||
import android.widget.ImageButton
|
||||
@@ -71,6 +70,8 @@ class FlorisBoard : InputMethodService() {
|
||||
private var vibrator: Vibrator? = null
|
||||
private val osHandler = Handler()
|
||||
|
||||
var activeEditorInstance: EditorInstance = EditorInstance.default()
|
||||
|
||||
lateinit var subtypeManager: SubtypeManager
|
||||
lateinit var activeSubtype: Subtype
|
||||
private var currentThemeIsNight: Boolean = false
|
||||
@@ -88,6 +89,7 @@ class FlorisBoard : InputMethodService() {
|
||||
|
||||
companion object {
|
||||
private const val IME_ID: String = "dev.patrickgold.florisboard/.ime.core.FlorisBoard"
|
||||
private val TAG: String? = FlorisBoard::class.simpleName
|
||||
|
||||
fun checkIfImeIsEnabled(context: Context): Boolean {
|
||||
val activeImeIds = Settings.Secure.getString(
|
||||
@@ -144,7 +146,7 @@ class FlorisBoard : InputMethodService() {
|
||||
.build()
|
||||
)
|
||||
}
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onCreate()")
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onCreate()")
|
||||
|
||||
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
@@ -168,7 +170,7 @@ class FlorisBoard : InputMethodService() {
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
override fun onCreateInputView(): View? {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onCreateInputView()")
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onCreateInputView()")
|
||||
|
||||
baseContext.setTheme(currentThemeResId)
|
||||
|
||||
@@ -180,7 +182,7 @@ class FlorisBoard : InputMethodService() {
|
||||
}
|
||||
|
||||
fun registerInputView(inputView: InputView) {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "registerInputView(inputView)")
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "registerInputView($inputView)")
|
||||
|
||||
this.inputView = inputView
|
||||
initializeOneHandedEnvironment()
|
||||
@@ -192,7 +194,7 @@ class FlorisBoard : InputMethodService() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onDestroy()")
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onDestroy()")
|
||||
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
florisboardInstance = null
|
||||
@@ -202,22 +204,44 @@ class FlorisBoard : InputMethodService() {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onStartInput(attribute: EditorInfo?, restarting: Boolean) {
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onStartInput($attribute, $restarting)")
|
||||
|
||||
super.onStartInput(attribute, restarting)
|
||||
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_IMMEDIATE)
|
||||
}
|
||||
|
||||
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
|
||||
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_MONITOR)
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onStartInputView($info, $restarting)")
|
||||
Log.i(TAG, "onStartInputView: " + info?.debugSummarize())
|
||||
|
||||
super.onStartInputView(info, restarting)
|
||||
eventListeners.toList().forEach { it.onStartInputView(info, restarting) }
|
||||
activeEditorInstance = EditorInstance.from(info, this)
|
||||
eventListeners.toList().forEach {
|
||||
it.onStartInputView(activeEditorInstance, restarting)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinishInputView(finishingInput: Boolean) {
|
||||
currentInputConnection?.requestCursorUpdates(0)
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onFinishInputView($finishingInput)")
|
||||
|
||||
if (finishingInput) {
|
||||
activeEditorInstance = EditorInstance.default()
|
||||
}
|
||||
|
||||
super.onFinishInputView(finishingInput)
|
||||
eventListeners.toList().forEach { it.onFinishInputView(finishingInput) }
|
||||
}
|
||||
|
||||
override fun onFinishInput() {
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onFinishInput()")
|
||||
|
||||
super.onFinishInput()
|
||||
currentInputConnection?.requestCursorUpdates(0)
|
||||
}
|
||||
|
||||
override fun onWindowShown() {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onWindowShown()")
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onWindowShown()")
|
||||
|
||||
prefs.sync()
|
||||
updateTheme()
|
||||
@@ -231,13 +255,14 @@ class FlorisBoard : InputMethodService() {
|
||||
}
|
||||
|
||||
override fun onWindowHidden() {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onWindowHidden()")
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onWindowHidden()")
|
||||
|
||||
super.onWindowHidden()
|
||||
eventListeners.toList().forEach { it.onWindowHidden() }
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onConfigurationChanged($newConfig)")
|
||||
if (isInputViewShown) {
|
||||
updateOneHandedPanelVisibility()
|
||||
}
|
||||
@@ -245,37 +270,23 @@ class FlorisBoard : InputMethodService() {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
}
|
||||
|
||||
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
|
||||
super.onUpdateCursorAnchorInfo(cursorAnchorInfo)
|
||||
eventListeners.toList().forEach { it.onUpdateCursorAnchorInfo(cursorAnchorInfo) }
|
||||
}
|
||||
|
||||
override fun onUpdateSelection(
|
||||
oldSelStart: Int,
|
||||
oldSelEnd: Int,
|
||||
newSelStart: Int,
|
||||
newSelEnd: Int,
|
||||
candidatesStart: Int,
|
||||
candidatesEnd: Int
|
||||
oldSelStart: Int, oldSelEnd: Int,
|
||||
newSelStart: Int, newSelEnd: Int,
|
||||
candidatesStart: Int, candidatesEnd: Int
|
||||
) {
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onUpdateSelection($oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd)")
|
||||
|
||||
super.onUpdateSelection(
|
||||
oldSelStart,
|
||||
oldSelEnd,
|
||||
newSelStart,
|
||||
newSelEnd,
|
||||
candidatesStart,
|
||||
candidatesEnd
|
||||
oldSelStart, oldSelEnd,
|
||||
newSelStart, newSelEnd,
|
||||
candidatesStart, candidatesEnd
|
||||
)
|
||||
eventListeners.toList().forEach {
|
||||
it.onUpdateSelection(
|
||||
oldSelStart,
|
||||
oldSelEnd,
|
||||
newSelStart,
|
||||
newSelEnd,
|
||||
candidatesStart,
|
||||
candidatesEnd
|
||||
)
|
||||
}
|
||||
activeEditorInstance.onUpdateSelection(
|
||||
oldSelStart, oldSelEnd,
|
||||
newSelStart, newSelEnd
|
||||
)
|
||||
eventListeners.toList().forEach { it.onUpdateSelection() }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -550,21 +561,13 @@ class FlorisBoard : InputMethodService() {
|
||||
fun onRegisterInputView(inputView: InputView) {}
|
||||
fun onDestroy() {}
|
||||
|
||||
fun onStartInputView(info: EditorInfo?, restarting: Boolean) {}
|
||||
fun onStartInputView(instance: EditorInstance, restarting: Boolean) {}
|
||||
fun onFinishInputView(finishingInput: Boolean) {}
|
||||
|
||||
fun onWindowShown() {}
|
||||
fun onWindowHidden() {}
|
||||
|
||||
fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {}
|
||||
fun onUpdateSelection(
|
||||
oldSelStart: Int,
|
||||
oldSelEnd: Int,
|
||||
newSelStart: Int,
|
||||
newSelEnd: Int,
|
||||
candidatesStart: Int,
|
||||
candidatesEnd: Int
|
||||
) {}
|
||||
fun onUpdateSelection() {}
|
||||
|
||||
fun onApplyThemeAttributes() {}
|
||||
fun onSubtypeChanged(newSubtype: Subtype) {}
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.widget.LinearLayout
|
||||
import android.widget.ViewFlipper
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.util.ViewLayoutUtils
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
@@ -83,11 +84,14 @@ class InputView : LinearLayout {
|
||||
"extra_tall" -> 1.15f
|
||||
else -> 1.00f
|
||||
}
|
||||
val height = (resources.getDimension(R.dimen.inputView_baseHeight) * heightFactor).roundToInt()
|
||||
var height = (resources.getDimension(R.dimen.inputView_baseHeight) * heightFactor).roundToInt()
|
||||
desiredInputViewHeight = height
|
||||
desiredSmartbarHeight = (0.16129 * height).roundToInt()
|
||||
desiredTextKeyboardViewHeight = height - desiredSmartbarHeight
|
||||
desiredMediaKeyboardViewHeight = height
|
||||
// Add bottom offset for curved screens here. As the desired heights have already been set,
|
||||
// adding a value to the height now will result in a bottom padding (aka offset).
|
||||
height += ViewLayoutUtils.convertDpToPixel(florisboard.prefs.keyboard.bottomOffset.toFloat(), context).toInt()
|
||||
|
||||
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY))
|
||||
}
|
||||
|
||||
@@ -180,12 +180,20 @@ class PrefHelper(
|
||||
*/
|
||||
class Correction(private val prefHelper: PrefHelper) {
|
||||
companion object {
|
||||
const val DOUBLE_SPACE_PERIOD = "correction__double_space_period"
|
||||
const val AUTO_CAPITALIZATION = "correction__auto_capitalization"
|
||||
const val DOUBLE_SPACE_PERIOD = "correction__double_space_period"
|
||||
const val REMEMBER_CAPS_LOCK_STATE = "correction__remember_caps_lock_state"
|
||||
}
|
||||
|
||||
var doubleSpacePeriod: Boolean = false
|
||||
get() = prefHelper.getPref(DOUBLE_SPACE_PERIOD, true)
|
||||
private set
|
||||
var autoCapitalization: Boolean
|
||||
get() = prefHelper.getPref(AUTO_CAPITALIZATION, true)
|
||||
set(v) = prefHelper.setPref(AUTO_CAPITALIZATION, v)
|
||||
var doubleSpacePeriod: Boolean
|
||||
get() = prefHelper.getPref(DOUBLE_SPACE_PERIOD, true)
|
||||
set(v) = prefHelper.setPref(DOUBLE_SPACE_PERIOD, v)
|
||||
var rememberCapsLockState: Boolean
|
||||
get() = prefHelper.getPref(REMEMBER_CAPS_LOCK_STATE, false)
|
||||
set(v) = prefHelper.setPref(REMEMBER_CAPS_LOCK_STATE, v)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -293,7 +301,10 @@ class PrefHelper(
|
||||
*/
|
||||
class Keyboard(private val prefHelper: PrefHelper) {
|
||||
companion object {
|
||||
const val BOTTOM_OFFSET = "keyboard__bottom_offset"
|
||||
const val HEIGHT_FACTOR = "keyboard__height_factor"
|
||||
const val HINTED_NUMBER_ROW = "keyboard__hinted_number_row"
|
||||
const val HINTED_SYMBOLS = "keyboard__hinted_symbols"
|
||||
const val LONG_PRESS_DELAY = "keyboard__long_press_delay"
|
||||
const val ONE_HANDED_MODE = "keyboard__one_handed_mode"
|
||||
const val POPUP_ENABLED = "keyboard__popup_enabled"
|
||||
@@ -303,9 +314,18 @@ class PrefHelper(
|
||||
const val VIBRATION_STRENGTH = "keyboard__vibration_strength"
|
||||
}
|
||||
|
||||
var bottomOffset: Int = 0
|
||||
get() = prefHelper.getPref(BOTTOM_OFFSET, 0)
|
||||
private set
|
||||
var heightFactor: String = ""
|
||||
get() = prefHelper.getPref(HEIGHT_FACTOR, "normal")
|
||||
private set
|
||||
var hintedNumberRow: Boolean
|
||||
get() = prefHelper.getPref(HINTED_NUMBER_ROW, true)
|
||||
set(v) = prefHelper.setPref(HINTED_NUMBER_ROW, v)
|
||||
var hintedSymbols: Boolean
|
||||
get() = prefHelper.getPref(HINTED_SYMBOLS, true)
|
||||
set(v) = prefHelper.setPref(HINTED_SYMBOLS, v)
|
||||
var longPressDelay: Int = 0
|
||||
get() = prefHelper.getPref(LONG_PRESS_DELAY, 300)
|
||||
private set
|
||||
@@ -341,10 +361,10 @@ class PrefHelper(
|
||||
}
|
||||
|
||||
var activeSubtypeId: Int
|
||||
get() = prefHelper.getPref(ACTIVE_SUBTYPE_ID, Subtype.DEFAULT.id)
|
||||
get() = prefHelper.getPref(ACTIVE_SUBTYPE_ID, Subtype.DEFAULT.id)
|
||||
set(v) = prefHelper.setPref(ACTIVE_SUBTYPE_ID, v)
|
||||
var subtypes: String
|
||||
get() = prefHelper.getPref(SUBTYPES, "")
|
||||
get() = prefHelper.getPref(SUBTYPES, "")
|
||||
set(v) = prefHelper.setPref(SUBTYPES, v)
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.widget.*
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.EditorInstance
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.InputView
|
||||
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyData
|
||||
@@ -50,6 +51,8 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
FlorisBoard.EventListener {
|
||||
|
||||
private val florisboard = FlorisBoard.getInstance()
|
||||
private val activeEditorInstance: EditorInstance
|
||||
get() = florisboard.activeEditorInstance
|
||||
|
||||
private var activeTab: Tab? = null
|
||||
private var mediaViewFlipper: ViewFlipper? = null
|
||||
@@ -199,18 +202,14 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* Sends a given [emojiKeyData] to the current input editor.
|
||||
*/
|
||||
fun sendEmojiKeyPress(emojiKeyData: EmojiKeyData) {
|
||||
val ic = florisboard.currentInputConnection
|
||||
ic?.finishComposingText()
|
||||
ic?.commitText(emojiKeyData.getCodePointsAsString(), 1)
|
||||
activeEditorInstance.commitText(emojiKeyData.getCodePointsAsString())
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a given [emoticonKeyData] to the current input editor.
|
||||
*/
|
||||
fun sendEmoticonKeyPress(emoticonKeyData: EmoticonKeyData) {
|
||||
val ic = florisboard.currentInputConnection
|
||||
ic?.finishComposingText()
|
||||
ic?.commitText(emoticonKeyData.icon, 1)
|
||||
activeEditorInstance.commitText(emoticonKeyData.icon)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -61,7 +61,7 @@ class MediaInputView : LinearLayout, FlorisBoard.EventListener {
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val height = florisboard?.inputView?.desiredInputViewHeight ?: 0
|
||||
val height = florisboard?.inputView?.desiredMediaKeyboardViewHeight ?: 0
|
||||
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,14 +18,12 @@ package dev.patrickgold.florisboard.ime.media.emoji
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Handler
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
import android.view.MotionEvent
|
||||
import android.widget.HorizontalScrollView
|
||||
import android.widget.ScrollView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.BlendModeColorFilterCompat
|
||||
import androidx.core.graphics.BlendModeCompat
|
||||
@@ -91,7 +89,7 @@ class EmojiKeyView(
|
||||
osHandler = Handler()
|
||||
}
|
||||
osHandler?.postDelayed({
|
||||
(parent.parent as HorizontalScrollView)
|
||||
(parent.parent as ScrollView)
|
||||
.requestDisallowInterceptTouchEvent(true)
|
||||
emojiKeyboardView.isScrollBlocked = true
|
||||
emojiKeyboardView.popupManager.show(this)
|
||||
|
||||
@@ -21,14 +21,13 @@ import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.HorizontalScrollView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ViewFlipper
|
||||
import android.widget.*
|
||||
import com.google.android.flexbox.FlexDirection
|
||||
import com.google.android.flexbox.FlexWrap
|
||||
import com.google.android.flexbox.FlexboxLayout
|
||||
import com.google.android.flexbox.JustifyContent
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
@@ -55,7 +54,7 @@ class EmojiKeyboardView : LinearLayout, FlorisBoard.EventListener {
|
||||
private var layouts: Deferred<EmojiLayoutDataMap>
|
||||
private val mainScope = MainScope()
|
||||
private val tabLayout: TabLayout
|
||||
private val uiLayouts = EnumMap<EmojiCategory, HorizontalScrollView>(EmojiCategory::class.java)
|
||||
private val uiLayouts = EnumMap<EmojiCategory, ScrollView>(EmojiCategory::class.java)
|
||||
|
||||
var isScrollBlocked: Boolean = false
|
||||
var popupManager = KeyPopupManager<EmojiKeyboardView, EmojiKeyView>(this)
|
||||
@@ -66,12 +65,15 @@ class EmojiKeyboardView : LinearLayout, FlorisBoard.EventListener {
|
||||
layouts = mainScope.async(Dispatchers.IO) {
|
||||
parseRawEmojiSpecsFile(context, "ime/media/emoji/emoji-test.txt")
|
||||
}
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
orientation = VERTICAL
|
||||
|
||||
emojiViewFlipper = ViewFlipper(context)
|
||||
emojiViewFlipper.layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
emojiViewFlipper.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, 0).apply {
|
||||
weight = 1.0f
|
||||
}
|
||||
emojiViewFlipper.measureAllChildren = false
|
||||
addView(emojiViewFlipper)
|
||||
|
||||
@@ -117,10 +119,10 @@ class EmojiKeyboardView : LinearLayout, FlorisBoard.EventListener {
|
||||
*/
|
||||
private suspend fun buildLayout() = withContext(Dispatchers.Default) {
|
||||
for (category in EmojiCategory.values()) {
|
||||
val hsv = buildLayoutForCategory(category)
|
||||
uiLayouts[category] = hsv
|
||||
val scrollView = buildLayoutForCategory(category)
|
||||
uiLayouts[category] = scrollView
|
||||
withContext(Dispatchers.Main) {
|
||||
emojiViewFlipper.addView(hsv)
|
||||
emojiViewFlipper.addView(scrollView)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,18 +132,19 @@ class EmojiKeyboardView : LinearLayout, FlorisBoard.EventListener {
|
||||
* context and will not block the main UI thread.
|
||||
*
|
||||
* @param category The category for which a layout should be built.
|
||||
* @return The layout (top-most view is a [HorizontalScrollView]).
|
||||
* @return The layout (top-most view is a [ScrollView]).
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private suspend fun buildLayoutForCategory(
|
||||
category: EmojiCategory
|
||||
): HorizontalScrollView = withContext(Dispatchers.Default) {
|
||||
val hsv = HorizontalScrollView(context)
|
||||
hsv.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
||||
): ScrollView = withContext(Dispatchers.Default) {
|
||||
val scrollView = ScrollView(context)
|
||||
scrollView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||
val flexboxLayout = FlexboxLayout(context)
|
||||
flexboxLayout.layoutParams =
|
||||
LayoutParams(LayoutParams.WRAP_CONTENT, emojiKeyHeight * 3)
|
||||
flexboxLayout.flexDirection = FlexDirection.COLUMN
|
||||
LayoutParams(LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
flexboxLayout.flexDirection = FlexDirection.ROW
|
||||
flexboxLayout.justifyContent = JustifyContent.SPACE_BETWEEN
|
||||
flexboxLayout.flexWrap = FlexWrap.WRAP
|
||||
for (emojiKeyData in layouts.await()[category].orEmpty()) {
|
||||
val emojiKeyView =
|
||||
@@ -151,11 +154,30 @@ class EmojiKeyboardView : LinearLayout, FlorisBoard.EventListener {
|
||||
)
|
||||
flexboxLayout.addView(emojiKeyView)
|
||||
}
|
||||
hsv.setOnTouchListener { _, _ ->
|
||||
// Add empty placeholder emojis at the end so the grid view. Below is an illustration how
|
||||
// the UI looks with and without an placeholder (e = emoji):
|
||||
// Without placeholder With placeholder
|
||||
// e e e e e e e e e e e e e e
|
||||
// ............. .............
|
||||
// e e e e e e e e e e e e e e
|
||||
// e e e e e e e e
|
||||
//
|
||||
// Based on this SO's answer idea (by La Nube - Luis R. Díaz Muñiz):
|
||||
// https://stackoverflow.com/a/31478004/6801193
|
||||
//
|
||||
// 24 items are chosen here because that's probably the max items that will be shown per
|
||||
// row, even in landscape mode.
|
||||
for (n in 0 until 24) {
|
||||
val gridPlaceholderView = View(context).apply {
|
||||
layoutParams = LayoutParams(emojiKeyWidth, 0)
|
||||
}
|
||||
flexboxLayout.addView(gridPlaceholderView)
|
||||
}
|
||||
scrollView.setOnTouchListener { _, _ ->
|
||||
return@setOnTouchListener isScrollBlocked
|
||||
}
|
||||
hsv.addView(flexboxLayout)
|
||||
return@withContext hsv
|
||||
scrollView.addView(flexboxLayout)
|
||||
return@withContext scrollView
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,7 +30,7 @@ import dev.patrickgold.florisboard.util.*
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
class KeyPopupExtendedSingleView(
|
||||
context: Context, var isActive: Boolean = false
|
||||
context: Context, val adjustedIndex: Int, var isActive: Boolean = false
|
||||
) : androidx.appcompat.widget.AppCompatTextView(
|
||||
context, null, 0
|
||||
) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.core.content.ContextCompat.getDrawable
|
||||
import androidx.core.view.get
|
||||
import com.google.android.flexbox.FlexboxLayout
|
||||
import com.google.android.flexbox.JustifyContent
|
||||
import dev.patrickgold.florisboard.R
|
||||
@@ -42,7 +43,6 @@ import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
|
||||
* @property keyboardView Reference to the keyboard view to which this manager class belongs to.
|
||||
*/
|
||||
class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD) {
|
||||
|
||||
private var anchorLeft: Boolean = false
|
||||
private var anchorRight: Boolean = false
|
||||
private var anchorOffset: Int = 0
|
||||
@@ -101,14 +101,14 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
isInitActive: Boolean = false,
|
||||
isWrapBefore: Boolean = false
|
||||
): KeyPopupExtendedSingleView? {
|
||||
val textView = KeyPopupExtendedSingleView(keyView.context, isInitActive)
|
||||
val textView = KeyPopupExtendedSingleView(keyView.context, k, isInitActive)
|
||||
val lp = FlexboxLayout.LayoutParams(keyPopupWidth, keyView.measuredHeight)
|
||||
lp.isWrapBefore = isWrapBefore
|
||||
textView.layoutParams = lp
|
||||
textView.gravity = Gravity.CENTER
|
||||
val textSize = keyboardView.resources.getDimension(R.dimen.key_popup_textSize)
|
||||
if (keyView is KeyView) {
|
||||
when (keyView.data.popup[k].code) {
|
||||
when (keyView.dataPopupWithHint[k].code) {
|
||||
KeyCode.SETTINGS -> {
|
||||
textView.iconDrawable = getDrawable(
|
||||
keyView.context, R.drawable.ic_settings
|
||||
@@ -129,13 +129,13 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
}
|
||||
else -> {
|
||||
textView.setTextSize(
|
||||
TypedValue.COMPLEX_UNIT_PX, when (keyView.data.popup[k].code) {
|
||||
TypedValue.COMPLEX_UNIT_PX, when (keyView.dataPopupWithHint[k].code) {
|
||||
KeyCode.URI_COMPONENT_TLD,
|
||||
KeyCode.SWITCH_TO_TEXT_CONTEXT -> textSize * 0.6f
|
||||
else -> textSize
|
||||
}
|
||||
)
|
||||
textView.text = keyView.getComputedLetter(keyView.data.popup[k])
|
||||
textView.text = keyView.getComputedLetter(keyView.dataPopupWithHint[k])
|
||||
}
|
||||
}
|
||||
} else if (keyView is EmojiKeyView) {
|
||||
@@ -211,7 +211,7 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
if (keyView is KeyView) {
|
||||
popupView.findViewById<TextView>(R.id.key_popup_text)?.text = keyView.getComputedLetter()
|
||||
popupView.findViewById<ImageView>(R.id.key_popup_threedots)?.visibility = when {
|
||||
keyView.data.popup.isEmpty() -> View.INVISIBLE
|
||||
keyView.dataPopupWithHint.isEmpty() -> View.INVISIBLE
|
||||
else -> View.VISIBLE
|
||||
}
|
||||
} else if (keyView is EmojiKeyView) {
|
||||
@@ -256,17 +256,12 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
}
|
||||
|
||||
// Anchor left if keyView is in left half of keyboardView, else anchor right
|
||||
if (keyView is KeyView) {
|
||||
anchorLeft = keyView.x < keyboardView.measuredWidth / 2
|
||||
} else if (keyView is EmojiKeyView) {
|
||||
val hsv = (keyView.parent.parent as HorizontalScrollView)
|
||||
anchorLeft = (keyView.x - hsv.scrollX) < keyboardView.measuredWidth / 2
|
||||
}
|
||||
anchorLeft = keyView.x < keyboardView.measuredWidth / 2
|
||||
anchorRight = !anchorLeft
|
||||
|
||||
// Determine key counts for each row
|
||||
val n = when (keyView) {
|
||||
is KeyView -> keyView.data.popup.size
|
||||
is KeyView -> keyView.dataPopupWithHint.size
|
||||
is EmojiKeyView -> keyView.data.popup.size
|
||||
else -> 0
|
||||
}
|
||||
@@ -315,17 +310,29 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
// Build UI
|
||||
popupViewExt.removeAllViews()
|
||||
val indices = when (keyView) {
|
||||
is KeyView -> keyView.data.popup.indices
|
||||
is KeyView -> keyView.dataPopupWithHint.indices
|
||||
is EmojiKeyView -> keyView.data.popup.indices
|
||||
else -> IntRange(0, 0)
|
||||
}
|
||||
var hasShownFirst = false
|
||||
for (k in indices) {
|
||||
val isInitActive =
|
||||
anchorLeft && (k - row1count == anchorOffset) ||
|
||||
anchorRight && (k - row1count == row0count - 1 - anchorOffset)
|
||||
val kk = when (keyView) {
|
||||
is KeyView -> when {
|
||||
isInitActive -> {
|
||||
hasShownFirst = true
|
||||
0
|
||||
}
|
||||
hasShownFirst -> k
|
||||
else -> k + 1
|
||||
}
|
||||
else -> k
|
||||
}
|
||||
popupViewExt.addView(
|
||||
createTextView(
|
||||
keyView, k, isInitActive, (row1count > 0) && (k - row1count == 0)
|
||||
keyView, kk, isInitActive, (row1count > 0) && (k - row1count == 0)
|
||||
)
|
||||
)
|
||||
if (isInitActive) {
|
||||
@@ -441,7 +448,7 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
}
|
||||
|
||||
if (keyView is KeyView) {
|
||||
for (k in keyView.data.popup.indices) {
|
||||
for (k in keyView.dataPopupWithHint.indices) {
|
||||
val view = popupViewExt.getChildAt(k)
|
||||
if (view != null) {
|
||||
val textView = view as KeyPopupExtendedSingleView
|
||||
@@ -471,7 +478,13 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
*/
|
||||
fun getActiveKeyData(keyView: T_KV): KeyData? {
|
||||
return if (keyView is KeyView) {
|
||||
keyView.data.popup.getOrNull(activeExtIndex ?: -1) ?: keyView.data
|
||||
val activeExtIndex = activeExtIndex
|
||||
if (activeExtIndex != null) {
|
||||
val singleView = popupViewExt[activeExtIndex] as KeyPopupExtendedSingleView
|
||||
keyView.dataPopupWithHint.getOrNull(singleView.adjustedIndex) ?: keyView.data
|
||||
} else {
|
||||
keyView.data
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ package dev.patrickgold.florisboard.ime.text
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.text.InputType
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.inputmethod.*
|
||||
@@ -27,9 +26,7 @@ import android.widget.LinearLayout
|
||||
import android.widget.ViewFlipper
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.InputView
|
||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||
import dev.patrickgold.florisboard.ime.core.*
|
||||
import dev.patrickgold.florisboard.ime.text.editing.EditingKeyboardView
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
@@ -59,6 +56,8 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
FlorisBoard.EventListener {
|
||||
|
||||
private val florisboard = FlorisBoard.getInstance()
|
||||
private val activeEditorInstance: EditorInstance
|
||||
get() = florisboard.activeEditorInstance
|
||||
|
||||
private var activeKeyboardMode: KeyboardMode? = null
|
||||
private val keyboardViews = EnumMap<KeyboardMode, KeyboardView>(KeyboardMode::class.java)
|
||||
@@ -69,35 +68,23 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
|
||||
var keyVariation: KeyVariation = KeyVariation.NORMAL
|
||||
private val layoutManager = LayoutManager(florisboard)
|
||||
lateinit var smartbarManager: SmartbarManager
|
||||
private lateinit var smartbarManager: SmartbarManager
|
||||
|
||||
// Caps/Space related properties
|
||||
var caps: Boolean = false
|
||||
private set
|
||||
var capsLock: Boolean = false
|
||||
private set
|
||||
private var cursorCapsMode: CapsMode = CapsMode.NONE
|
||||
private var editorCapsMode: CapsMode = CapsMode.NONE
|
||||
private var hasCapsRecentlyChanged: Boolean = false
|
||||
private var hasSpaceRecentlyPressed: Boolean = false
|
||||
|
||||
// Composing text related properties
|
||||
private var composingText: String? = null
|
||||
private var composingTextStart: Int? = null
|
||||
private var cursorPos: Int = 0
|
||||
private var isComposingEnabled: Boolean = false
|
||||
var isManualSelectionMode: Boolean = false
|
||||
private var isManualSelectionModeLeft: Boolean = false
|
||||
private var isManualSelectionModeRight: Boolean = false
|
||||
val isTextSelected: Boolean
|
||||
get() = selectionEnd - selectionStart != 0
|
||||
private var lastCursorAnchorInfo: CursorAnchorInfo? = null
|
||||
private var selectionStart: Int = 0
|
||||
private val selectionStartMin: Int = 0
|
||||
private var selectionEnd: Int = 0
|
||||
private var selectionEndMax: Int = 0
|
||||
|
||||
companion object {
|
||||
private val TAG: String? = TextInputManager::class.simpleName
|
||||
private var instance: TextInputManager? = null
|
||||
|
||||
@Synchronized
|
||||
@@ -118,19 +105,15 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* background).
|
||||
*/
|
||||
override fun onCreate() {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onCreate()")
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onCreate()")
|
||||
|
||||
for (mode in KeyboardMode.values()) {
|
||||
if (mode == KeyboardMode.CHARACTERS) {
|
||||
var subtypes = florisboard.subtypeManager.subtypes
|
||||
if (subtypes.isEmpty()) {
|
||||
subtypes = listOf(Subtype.DEFAULT)
|
||||
}
|
||||
for (subtype in subtypes) {
|
||||
layoutManager.preloadComputedLayout(mode, subtype)
|
||||
}
|
||||
} else {
|
||||
layoutManager.preloadComputedLayout(mode, florisboard.activeSubtype)
|
||||
var subtypes = florisboard.subtypeManager.subtypes
|
||||
if (subtypes.isEmpty()) {
|
||||
subtypes = listOf(Subtype.DEFAULT)
|
||||
}
|
||||
for (subtype in subtypes) {
|
||||
for (mode in KeyboardMode.values()) {
|
||||
layoutManager.preloadComputedLayout(mode, subtype)
|
||||
}
|
||||
}
|
||||
smartbarManager = SmartbarManager.getInstance()
|
||||
@@ -149,7 +132,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* Sets up the newly registered input view.
|
||||
*/
|
||||
override fun onRegisterInputView(inputView: InputView) {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onRegisterInputView(inputView)")
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onRegisterInputView(inputView)")
|
||||
|
||||
launch(Dispatchers.Default) {
|
||||
textViewGroup = inputView.findViewById(R.id.text_input)
|
||||
@@ -173,7 +156,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* Cancels all coroutines and cleans up.
|
||||
*/
|
||||
override fun onDestroy() {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onDestroy()")
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onDestroy()")
|
||||
|
||||
cancel()
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
@@ -183,58 +166,60 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the [activeKeyboardMode], [keyVariation] and [isComposingEnabled] property values
|
||||
* when starting to interact with a input editor. Also resets the composing texts and sets the
|
||||
* initial caps mode accordingly.
|
||||
* Evaluates the [activeKeyboardMode], [keyVariation] and [EditorInstance.isComposingEnabled]
|
||||
* property values when starting to interact with a input editor. Also resets the composing
|
||||
* texts and sets the initial caps mode accordingly.
|
||||
*/
|
||||
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
|
||||
val keyboardMode = when (info) {
|
||||
null -> KeyboardMode.CHARACTERS
|
||||
else -> when (info.inputType and InputType.TYPE_MASK_CLASS) {
|
||||
InputType.TYPE_CLASS_NUMBER -> {
|
||||
keyVariation = KeyVariation.NORMAL
|
||||
KeyboardMode.NUMERIC
|
||||
}
|
||||
InputType.TYPE_CLASS_PHONE -> {
|
||||
keyVariation = KeyVariation.NORMAL
|
||||
KeyboardMode.PHONE
|
||||
}
|
||||
InputType.TYPE_CLASS_TEXT -> {
|
||||
keyVariation = when (info.inputType and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS -> {
|
||||
KeyVariation.EMAIL_ADDRESS
|
||||
}
|
||||
InputType.TYPE_TEXT_VARIATION_PASSWORD,
|
||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD -> {
|
||||
KeyVariation.PASSWORD
|
||||
}
|
||||
InputType.TYPE_TEXT_VARIATION_URI -> {
|
||||
KeyVariation.URI
|
||||
}
|
||||
else -> {
|
||||
KeyVariation.NORMAL
|
||||
}
|
||||
override fun onStartInputView(instance: EditorInstance, restarting: Boolean) {
|
||||
val keyboardMode = when (instance.inputAttributes.type) {
|
||||
InputAttributes.Type.NUMBER -> {
|
||||
keyVariation = KeyVariation.NORMAL
|
||||
KeyboardMode.NUMERIC
|
||||
}
|
||||
InputAttributes.Type.PHONE -> {
|
||||
keyVariation = KeyVariation.NORMAL
|
||||
KeyboardMode.PHONE
|
||||
}
|
||||
InputAttributes.Type.TEXT -> {
|
||||
keyVariation = when (instance.inputAttributes.variation) {
|
||||
InputAttributes.Variation.EMAIL_ADDRESS,
|
||||
InputAttributes.Variation.WEB_EMAIL_ADDRESS -> {
|
||||
KeyVariation.EMAIL_ADDRESS
|
||||
}
|
||||
InputAttributes.Variation.PASSWORD,
|
||||
InputAttributes.Variation.VISIBLE_PASSWORD,
|
||||
InputAttributes.Variation.WEB_PASSWORD -> {
|
||||
KeyVariation.PASSWORD
|
||||
}
|
||||
InputAttributes.Variation.URI -> {
|
||||
KeyVariation.URI
|
||||
}
|
||||
else -> {
|
||||
KeyVariation.NORMAL
|
||||
}
|
||||
KeyboardMode.CHARACTERS
|
||||
}
|
||||
else -> {
|
||||
keyVariation = KeyVariation.NORMAL
|
||||
KeyboardMode.CHARACTERS
|
||||
}
|
||||
KeyboardMode.CHARACTERS
|
||||
}
|
||||
else -> {
|
||||
keyVariation = KeyVariation.NORMAL
|
||||
KeyboardMode.CHARACTERS
|
||||
}
|
||||
}
|
||||
isComposingEnabled = when (keyboardMode) {
|
||||
instance.isComposingEnabled = when (keyboardMode) {
|
||||
KeyboardMode.NUMERIC,
|
||||
KeyboardMode.PHONE,
|
||||
KeyboardMode.PHONE2 -> false
|
||||
else -> keyVariation != KeyVariation.PASSWORD && florisboard.prefs.suggestion.enabled
|
||||
else -> keyVariation != KeyVariation.PASSWORD &&
|
||||
florisboard.prefs.suggestion.enabled &&
|
||||
//!instance.inputAttributes.flagTextAutoComplete &&
|
||||
!instance.inputAttributes.flagTextNoSuggestions
|
||||
}
|
||||
if (!florisboard.prefs.correction.rememberCapsLockState) {
|
||||
capsLock = false
|
||||
}
|
||||
updateCapsState()
|
||||
resetComposingText()
|
||||
setActiveKeyboardMode(keyboardMode)
|
||||
smartbarManager.onStartInputView(keyboardMode, isComposingEnabled)
|
||||
smartbarManager.onStartInputView(keyboardMode, instance.isComposingEnabled)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -288,147 +273,27 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* Main logic point for processing cursor updates as well as parsing the current composing word
|
||||
* and passing this info on to the [SmartbarManager] to turn it into candidate suggestions.
|
||||
*/
|
||||
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
|
||||
cursorAnchorInfo ?: return
|
||||
lastCursorAnchorInfo = cursorAnchorInfo
|
||||
|
||||
val ic = florisboard.currentInputConnection
|
||||
|
||||
val isNewSelectionInBoundsOfOld =
|
||||
cursorAnchorInfo.selectionStart >= (selectionStart - 1) &&
|
||||
cursorAnchorInfo.selectionStart <= (selectionStart + 1) &&
|
||||
cursorAnchorInfo.selectionEnd >= (selectionEnd - 1) &&
|
||||
cursorAnchorInfo.selectionEnd <= (selectionEnd + 1)
|
||||
selectionStart = cursorAnchorInfo.selectionStart
|
||||
selectionEnd = cursorAnchorInfo.selectionEnd
|
||||
val inputText =
|
||||
(ic?.getExtractedText(ExtractedTextRequest(), 0)?.text ?: "").toString()
|
||||
selectionEndMax = inputText.length
|
||||
// TODO: separate composing text from delete swipe word detection
|
||||
//if (isComposingEnabled) {
|
||||
if (!isTextSelected) {
|
||||
val newCursorPos = cursorAnchorInfo.selectionStart
|
||||
val prevComposingText = (cursorAnchorInfo.composingText ?: "").toString()
|
||||
setComposingTextBasedOnInput(inputText, newCursorPos)
|
||||
if ((newCursorPos == cursorPos) && (composingText == prevComposingText)) {
|
||||
// Ignore this, as nothing has changed
|
||||
} else {
|
||||
cursorPos = newCursorPos
|
||||
if (composingText != null && composingTextStart != null) {
|
||||
ic?.setComposingRegion(
|
||||
composingTextStart!!,
|
||||
composingTextStart!! + composingText!!.length
|
||||
)
|
||||
} else {
|
||||
resetComposingText()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resetComposingText()
|
||||
}
|
||||
smartbarManager.generateCandidatesFromComposing(composingText)
|
||||
//}
|
||||
if (!isNewSelectionInBoundsOfOld) {
|
||||
override fun onUpdateSelection() {
|
||||
if (activeEditorInstance.selection.isCursorMode) {
|
||||
smartbarManager.generateCandidatesFromComposing(activeEditorInstance.currentWord.text)
|
||||
}
|
||||
if (!activeEditorInstance.isNewSelectionInBoundsOfOld) {
|
||||
isManualSelectionMode = false
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = false
|
||||
}
|
||||
updateCapsState()
|
||||
smartbarManager.onUpdateCursorAnchorInfo(cursorAnchorInfo)
|
||||
smartbarManager.onUpdateSelection()
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the [composingText] and [composingTextStart] properties. Does NOT sync with
|
||||
* [SmartbarManager]!
|
||||
*
|
||||
* @param notifyInputConnection If the current input connection should be notified.
|
||||
*/
|
||||
private fun resetComposingText(notifyInputConnection: Boolean = true) {
|
||||
if (notifyInputConnection) {
|
||||
val ic = florisboard.currentInputConnection
|
||||
ic?.finishComposingText()
|
||||
}
|
||||
composingText = null
|
||||
composingTextStart = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to parse the [composingText] from a given [inputCursorPos] within [inputText].
|
||||
* Sets both [composingText] and [composingTextStart] to null if it fails, else to its
|
||||
* parsed values.
|
||||
*
|
||||
* @param inputText The input text to search in.
|
||||
* @param inputCursorPos The position where to search in [inputText].
|
||||
*/
|
||||
private fun setComposingTextBasedOnInput(inputText: String, inputCursorPos: Int) {
|
||||
val words = inputText.split("[^\\p{L}]".toRegex())
|
||||
var pos = 0
|
||||
resetComposingText(false)
|
||||
for (word in words) {
|
||||
if (inputCursorPos >= pos && inputCursorPos <= pos + word.length && word.isNotEmpty()) {
|
||||
composingText = word
|
||||
composingTextStart = pos
|
||||
break
|
||||
} else {
|
||||
pos += word.length + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should primarily pe used by [SmartbarManager.candidateViewOnClickListener] to commit
|
||||
* a candidate if a user has pressed on it.
|
||||
*/
|
||||
fun commitCandidate(candidateText: String) {
|
||||
val ic = florisboard.currentInputConnection
|
||||
ic?.setComposingText(candidateText, 1)
|
||||
ic?.finishComposingText()
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the [CapsMode] out of the given [flags].
|
||||
*
|
||||
* @param flags The input flags.
|
||||
* @return A [CapsMode] value.
|
||||
*/
|
||||
private fun parseCapsModeFromFlags(flags: Int): CapsMode {
|
||||
return when {
|
||||
flags and InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS > 0 -> {
|
||||
CapsMode.ALL
|
||||
}
|
||||
flags and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES > 0 -> {
|
||||
CapsMode.SENTENCES
|
||||
}
|
||||
flags and InputType.TYPE_TEXT_FLAG_CAP_WORDS > 0 -> {
|
||||
CapsMode.WORDS
|
||||
}
|
||||
else -> {
|
||||
CapsMode.NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the current cursor caps mode from the current input connection.
|
||||
*
|
||||
* @return The [CapsMode] according to the returned flags by the current input connection.
|
||||
*/
|
||||
private fun fetchCurrentCursorCapsMode(): CapsMode {
|
||||
val ic = florisboard.currentInputConnection
|
||||
val info = florisboard.currentInputEditorInfo
|
||||
val capsFlags = ic?.getCursorCapsMode(info.inputType) ?: 0
|
||||
return parseCapsModeFromFlags(capsFlags)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current caps state according to the [cursorCapsMode], while respecting
|
||||
* [capsLock] property.
|
||||
* Updates the current caps state according to the [EditorInstance.cursorCapsMode], while
|
||||
* respecting [capsLock] property and the correction.autoCapitalization preference.
|
||||
*/
|
||||
private fun updateCapsState() {
|
||||
cursorCapsMode = fetchCurrentCursorCapsMode()
|
||||
editorCapsMode = parseCapsModeFromFlags(florisboard.currentInputEditorInfo.inputType)
|
||||
if (!capsLock) {
|
||||
caps = cursorCapsMode != CapsMode.NONE
|
||||
caps = florisboard.prefs.correction.autoCapitalization &&
|
||||
activeEditorInstance.cursorCapsMode != InputAttributes.CapsMode.NONE
|
||||
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
|
||||
}
|
||||
}
|
||||
@@ -449,90 +314,43 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a given [keyCode] as a [KeyEvent.ACTION_DOWN].
|
||||
*
|
||||
* @param ic The input connection on which this operation should be performed.
|
||||
* @param keyCode The key code to send, use a key code defined in Android's [KeyEvent], not in
|
||||
* [KeyCode] or this call may send a weird character, as this key codes do not match!!
|
||||
*/
|
||||
private fun sendSystemKeyEvent(ic: InputConnection?, keyCode: Int) {
|
||||
ic?.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, keyCode))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a given [keyCode] as a [KeyEvent.ACTION_DOWN] with ALT pressed.
|
||||
*
|
||||
* @param ic The input connection on which this operation should be performed.
|
||||
* @param keyCode The key code to send, use a key code defined in Android's [KeyEvent], not in
|
||||
* [KeyCode] or this call may send a weird character, as this key codes do not match!!
|
||||
*/
|
||||
private fun sendSystemKeyEventAlt(ic: InputConnection?, keyCode: Int) {
|
||||
ic?.sendKeyEvent(
|
||||
KeyEvent(
|
||||
0,
|
||||
1,
|
||||
KeyEvent.ACTION_DOWN, keyCode,
|
||||
0,
|
||||
KeyEvent.META_ALT_LEFT_ON
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.DELETE] event.
|
||||
*/
|
||||
private fun handleDelete() {
|
||||
val ic = florisboard.currentInputConnection
|
||||
ic?.beginBatchEdit()
|
||||
resetComposingText()
|
||||
isManualSelectionMode = false
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = false
|
||||
sendSystemKeyEvent(ic, KeyEvent.KEYCODE_DEL)
|
||||
ic?.endBatchEdit()
|
||||
activeEditorInstance.deleteBackwards()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.DELETE_WORD] event.
|
||||
*/
|
||||
private fun handleDeleteWord() {
|
||||
val ic = florisboard.currentInputConnection
|
||||
ic?.beginBatchEdit()
|
||||
isManualSelectionMode = false
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = false
|
||||
ic?.setComposingText("", 1)
|
||||
ic?.finishComposingText()
|
||||
if (ic?.getTextBeforeCursor(1, 0)?.length ?: 0 > 0) {
|
||||
ic?.deleteSurroundingText(1, 0)
|
||||
}
|
||||
composingText = null
|
||||
composingTextStart = null
|
||||
ic?.endBatchEdit()
|
||||
activeEditorInstance.deleteWordsBeforeCursor(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.ENTER] event.
|
||||
*/
|
||||
private fun handleEnter() {
|
||||
val ic = florisboard.currentInputConnection
|
||||
resetComposingText()
|
||||
val action = florisboard.currentInputEditorInfo?.imeOptions ?: 0
|
||||
val actionMasked = action and EditorInfo.IME_MASK_ACTION
|
||||
if (action and EditorInfo.IME_FLAG_NO_ENTER_ACTION > 0) {
|
||||
sendSystemKeyEvent(ic, KeyEvent.KEYCODE_ENTER)
|
||||
if (activeEditorInstance.imeOptions.flagNoEnterAction) {
|
||||
activeEditorInstance.commitText("\n")
|
||||
} else {
|
||||
when (actionMasked) {
|
||||
EditorInfo.IME_ACTION_DONE,
|
||||
EditorInfo.IME_ACTION_GO,
|
||||
EditorInfo.IME_ACTION_NEXT,
|
||||
EditorInfo.IME_ACTION_PREVIOUS,
|
||||
EditorInfo.IME_ACTION_SEARCH,
|
||||
EditorInfo.IME_ACTION_SEND -> {
|
||||
ic?.performEditorAction(actionMasked)
|
||||
when (activeEditorInstance.imeOptions.action) {
|
||||
ImeOptions.Action.DONE,
|
||||
ImeOptions.Action.GO,
|
||||
ImeOptions.Action.NEXT,
|
||||
ImeOptions.Action.PREVIOUS,
|
||||
ImeOptions.Action.SEARCH,
|
||||
ImeOptions.Action.SEND -> {
|
||||
activeEditorInstance.performEnterAction(activeEditorInstance.imeOptions.action)
|
||||
}
|
||||
else -> sendSystemKeyEvent(ic, KeyEvent.KEYCODE_ENTER)
|
||||
else -> activeEditorInstance.commitText("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -562,14 +380,13 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* enabled by the user.
|
||||
*/
|
||||
private fun handleSpace() {
|
||||
val ic = florisboard.currentInputConnection
|
||||
if (florisboard.prefs.correction.doubleSpacePeriod) {
|
||||
if (hasSpaceRecentlyPressed) {
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
val text = ic?.getTextBeforeCursor(2, 0) ?: ""
|
||||
val text = activeEditorInstance.getTextBeforeCursor(2)
|
||||
if (text.length == 2 && !text.matches("""[.!?‽\s][\s]""".toRegex())) {
|
||||
ic?.deleteSurroundingText(1, 0)
|
||||
ic?.commitText(".", 1)
|
||||
activeEditorInstance.deleteBackwards()
|
||||
activeEditorInstance.commitText(".")
|
||||
}
|
||||
hasSpaceRecentlyPressed = false
|
||||
} else {
|
||||
@@ -579,107 +396,107 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
ic?.commitText(KeyCode.SPACE.toChar().toString(), 1)
|
||||
activeEditorInstance.commitText(KeyCode.SPACE.toChar().toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles [KeyCode] arrow and move events, behaves differently depending on text selection.
|
||||
*/
|
||||
private fun handleArrow(code: Int) {
|
||||
val ic = florisboard.currentInputConnection
|
||||
resetComposingText()
|
||||
if (isTextSelected && isManualSelectionMode) {
|
||||
private fun handleArrow(code: Int) = activeEditorInstance.apply {
|
||||
val selectionStartMin = 0
|
||||
val selectionEndMax = cachedText.length
|
||||
if (selection.isSelectionMode && isManualSelectionMode) {
|
||||
// Text is selected and it is manual selection -> Expand selection depending on started
|
||||
// direction.
|
||||
when (code) {
|
||||
KeyCode.ARROW_DOWN -> {}
|
||||
KeyCode.ARROW_LEFT -> {
|
||||
if (isManualSelectionModeLeft) {
|
||||
ic?.setSelection(
|
||||
(selectionStart - 1).coerceAtLeast(selectionStartMin),
|
||||
selectionEnd
|
||||
setSelection(
|
||||
(selection.start - 1).coerceAtLeast(selectionStartMin),
|
||||
selection.end
|
||||
)
|
||||
} else {
|
||||
ic?.setSelection(selectionStart, selectionEnd - 1)
|
||||
setSelection(selection.start, selection.end - 1)
|
||||
}
|
||||
}
|
||||
KeyCode.ARROW_RIGHT -> {
|
||||
if (isManualSelectionModeRight) {
|
||||
ic?.setSelection(
|
||||
selectionStart,
|
||||
(selectionEnd + 1).coerceAtMost(selectionEndMax)
|
||||
setSelection(
|
||||
selection.start,
|
||||
(selection.end + 1).coerceAtMost(selectionEndMax)
|
||||
)
|
||||
} else {
|
||||
ic?.setSelection(selectionStart + 1, selectionEnd)
|
||||
setSelection(selection.start + 1, selection.end)
|
||||
}
|
||||
}
|
||||
KeyCode.ARROW_UP -> {}
|
||||
KeyCode.MOVE_HOME -> {
|
||||
if (isManualSelectionModeLeft) {
|
||||
ic?.setSelection(selectionStartMin, selectionEnd)
|
||||
setSelection(selectionStartMin, selection.end)
|
||||
} else {
|
||||
ic?.setSelection(selectionStartMin, selectionStart)
|
||||
setSelection(selectionStartMin, selection.start)
|
||||
}
|
||||
}
|
||||
KeyCode.MOVE_END -> {
|
||||
if (isManualSelectionModeRight) {
|
||||
ic?.setSelection(selectionStart, selectionEndMax)
|
||||
setSelection(selection.start, selectionEndMax)
|
||||
} else {
|
||||
ic?.setSelection(selectionEnd, selectionEndMax)
|
||||
setSelection(selection.end, selectionEndMax)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isTextSelected && !isManualSelectionMode) {
|
||||
} else if (selection.isSelectionMode && !isManualSelectionMode) {
|
||||
// Text is selected but no manual selection mode -> arrows behave as if selection was
|
||||
// started in manual left mode
|
||||
when (code) {
|
||||
KeyCode.ARROW_DOWN -> {}
|
||||
KeyCode.ARROW_LEFT -> {
|
||||
ic?.setSelection(selectionStart, selectionEnd - 1)
|
||||
setSelection(selection.start, selection.end - 1)
|
||||
}
|
||||
KeyCode.ARROW_RIGHT -> {
|
||||
ic?.setSelection(
|
||||
selectionStart,
|
||||
(selectionEnd + 1).coerceAtMost(selectionEndMax)
|
||||
setSelection(
|
||||
selection.start,
|
||||
(selection.end + 1).coerceAtMost(selectionEndMax)
|
||||
)
|
||||
}
|
||||
KeyCode.ARROW_UP -> {}
|
||||
KeyCode.MOVE_HOME -> {
|
||||
ic?.setSelection(selectionStartMin, selectionStart)
|
||||
setSelection(selectionStartMin, selection.start)
|
||||
}
|
||||
KeyCode.MOVE_END -> {
|
||||
ic?.setSelection(selectionStart, selectionEndMax)
|
||||
setSelection(selection.start, selectionEndMax)
|
||||
}
|
||||
}
|
||||
} else if (!isTextSelected && isManualSelectionMode) {
|
||||
} else if (!selection.isSelectionMode && isManualSelectionMode) {
|
||||
// No text is selected but manual selection mode is active, user wants to start a new
|
||||
// selection. Must set manual selection direction.
|
||||
when (code) {
|
||||
KeyCode.ARROW_DOWN -> {}
|
||||
KeyCode.ARROW_LEFT -> {
|
||||
ic?.setSelection(
|
||||
(selectionStart - 1).coerceAtLeast(selectionStartMin),
|
||||
selectionStart
|
||||
setSelection(
|
||||
(selection.start - 1).coerceAtLeast(selectionStartMin),
|
||||
selection.start
|
||||
)
|
||||
isManualSelectionModeLeft = true
|
||||
isManualSelectionModeRight = false
|
||||
}
|
||||
KeyCode.ARROW_RIGHT -> {
|
||||
ic?.setSelection(
|
||||
selectionEnd,
|
||||
(selectionEnd + 1).coerceAtMost(selectionEndMax)
|
||||
setSelection(
|
||||
selection.end,
|
||||
(selection.end + 1).coerceAtMost(selectionEndMax)
|
||||
)
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = true
|
||||
}
|
||||
KeyCode.ARROW_UP -> {}
|
||||
KeyCode.MOVE_HOME -> {
|
||||
ic?.setSelection(selectionStartMin, selectionStart)
|
||||
setSelection(selectionStartMin, selection.start)
|
||||
isManualSelectionModeLeft = true
|
||||
isManualSelectionModeRight = false
|
||||
}
|
||||
KeyCode.MOVE_END -> {
|
||||
ic?.setSelection(selectionEnd, selectionEndMax)
|
||||
setSelection(selection.end, selectionEndMax)
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = true
|
||||
}
|
||||
@@ -687,12 +504,12 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
} else {
|
||||
// No selection and no manual selection mode -> move cursor around
|
||||
when (code) {
|
||||
KeyCode.ARROW_DOWN -> sendSystemKeyEvent(ic, KeyEvent.KEYCODE_DPAD_DOWN)
|
||||
KeyCode.ARROW_LEFT -> sendSystemKeyEvent(ic, KeyEvent.KEYCODE_DPAD_LEFT)
|
||||
KeyCode.ARROW_RIGHT -> sendSystemKeyEvent(ic, KeyEvent.KEYCODE_DPAD_RIGHT)
|
||||
KeyCode.ARROW_UP -> sendSystemKeyEvent(ic, KeyEvent.KEYCODE_DPAD_UP)
|
||||
KeyCode.MOVE_HOME -> sendSystemKeyEventAlt(ic, KeyEvent.KEYCODE_DPAD_UP)
|
||||
KeyCode.MOVE_END -> sendSystemKeyEventAlt(ic, KeyEvent.KEYCODE_DPAD_DOWN)
|
||||
KeyCode.ARROW_DOWN -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN)
|
||||
KeyCode.ARROW_LEFT -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT)
|
||||
KeyCode.ARROW_RIGHT -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT)
|
||||
KeyCode.ARROW_UP -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_UP)
|
||||
KeyCode.MOVE_HOME -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_UP)
|
||||
KeyCode.MOVE_END -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_DOWN)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -702,14 +519,10 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* TODO: handle other data than text too, e.g. Uri, Intent, ...
|
||||
*/
|
||||
private fun handleClipboardCut() {
|
||||
val ic = florisboard.currentInputConnection
|
||||
val selectedText = ic?.getSelectedText(0)
|
||||
if (selectedText != null) {
|
||||
florisboard.clipboardManager
|
||||
?.setPrimaryClip(ClipData.newPlainText(selectedText, selectedText))
|
||||
}
|
||||
resetComposingText()
|
||||
ic?.commitText("", 1)
|
||||
val selectedText = activeEditorInstance.selection.text
|
||||
florisboard.clipboardManager
|
||||
?.setPrimaryClip(ClipData.newPlainText(selectedText, selectedText))
|
||||
activeEditorInstance.commitText("")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -717,14 +530,10 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* TODO: handle other data than text too, e.g. Uri, Intent, ...
|
||||
*/
|
||||
private fun handleClipboardCopy() {
|
||||
val ic = florisboard.currentInputConnection
|
||||
val selectedText = ic?.getSelectedText(0)
|
||||
if (selectedText != null) {
|
||||
florisboard.clipboardManager
|
||||
?.setPrimaryClip(ClipData.newPlainText(selectedText, selectedText))
|
||||
}
|
||||
resetComposingText()
|
||||
ic?.setSelection(selectionEnd, selectionEnd)
|
||||
val selectedText = activeEditorInstance.selection.text
|
||||
florisboard.clipboardManager
|
||||
?.setPrimaryClip(ClipData.newPlainText(selectedText, selectedText))
|
||||
activeEditorInstance.apply { setSelection(selection.end, selection.end) }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -732,42 +541,36 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* TODO: handle other data than text too, e.g. Uri, Intent, ...
|
||||
*/
|
||||
private fun handleClipboardPaste() {
|
||||
val ic = florisboard.currentInputConnection
|
||||
val item = florisboard.clipboardManager?.primaryClip?.getItemAt(0)
|
||||
val pasteText = item?.text
|
||||
if (pasteText != null) {
|
||||
resetComposingText()
|
||||
ic?.commitText(pasteText, 1)
|
||||
activeEditorInstance.commitText(pasteText.toString())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.CLIPBOARD_SELECT] event.
|
||||
*/
|
||||
private fun handleClipboardSelect() {
|
||||
val ic = florisboard.currentInputConnection
|
||||
resetComposingText()
|
||||
if (isTextSelected) {
|
||||
private fun handleClipboardSelect() = activeEditorInstance.apply {
|
||||
if (selection.isSelectionMode) {
|
||||
if (isManualSelectionMode && isManualSelectionModeLeft) {
|
||||
ic?.setSelection(selectionStart, selectionStart)
|
||||
setSelection(selection.start, selection.start)
|
||||
} else {
|
||||
ic?.setSelection(selectionEnd, selectionEnd)
|
||||
setSelection(selection.end, selection.end)
|
||||
}
|
||||
isManualSelectionMode = false
|
||||
} else {
|
||||
isManualSelectionMode = !isManualSelectionMode
|
||||
// Must recall to update UI properly
|
||||
florisboard.onUpdateCursorAnchorInfo(lastCursorAnchorInfo)
|
||||
// Must call to update UI properly
|
||||
editingKeyboardView?.onUpdateSelection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.CLIPBOARD_SELECT_ALL] event.
|
||||
*/
|
||||
private fun handleClipboardSelectAll() {
|
||||
val ic = florisboard.currentInputConnection
|
||||
resetComposingText()
|
||||
ic?.setSelection(selectionStartMin, selectionEndMax)
|
||||
activeEditorInstance.setSelection(0, activeEditorInstance.cachedText.length)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -778,8 +581,6 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* @param keyData The [KeyData] object which should be sent.
|
||||
*/
|
||||
fun sendKeyPress(keyData: KeyData) {
|
||||
val ic = florisboard.currentInputConnection
|
||||
|
||||
when (keyData.code) {
|
||||
KeyCode.ARROW_DOWN,
|
||||
KeyCode.ARROW_LEFT,
|
||||
@@ -813,8 +614,6 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
KeyCode.VIEW_SYMBOLS -> setActiveKeyboardMode(KeyboardMode.SYMBOLS)
|
||||
KeyCode.VIEW_SYMBOLS2 -> setActiveKeyboardMode(KeyboardMode.SYMBOLS2)
|
||||
else -> {
|
||||
ic?.beginBatchEdit()
|
||||
resetComposingText()
|
||||
when (activeKeyboardMode) {
|
||||
KeyboardMode.NUMERIC,
|
||||
KeyboardMode.NUMERIC_ADVANCED,
|
||||
@@ -823,13 +622,13 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
KeyType.CHARACTER,
|
||||
KeyType.NUMERIC -> {
|
||||
val text = keyData.code.toChar().toString()
|
||||
ic?.commitText(text, 1)
|
||||
activeEditorInstance.commitText(text)
|
||||
}
|
||||
else -> when (keyData.code) {
|
||||
KeyCode.PHONE_PAUSE,
|
||||
KeyCode.PHONE_WAIT -> {
|
||||
val text = keyData.code.toChar().toString()
|
||||
ic?.commitText(text, 1)
|
||||
activeEditorInstance.commitText(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -841,7 +640,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
true -> keyData.label.toUpperCase(Locale.getDefault())
|
||||
false -> keyData.label.toLowerCase(Locale.getDefault())
|
||||
}
|
||||
ic?.commitText(tld, 1)
|
||||
activeEditorInstance.commitText(tld)
|
||||
}
|
||||
else -> {
|
||||
var text = keyData.code.toChar().toString()
|
||||
@@ -849,26 +648,15 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
true -> text.toUpperCase(Locale.getDefault())
|
||||
false -> text.toLowerCase(Locale.getDefault())
|
||||
}
|
||||
ic?.commitText(text, 1)
|
||||
activeEditorInstance.commitText(text)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Log.e(
|
||||
this::class.simpleName,
|
||||
"sendKeyPress(keyData): Received unknown key: $keyData"
|
||||
)
|
||||
Log.e(TAG,"sendKeyPress(keyData): Received unknown key: $keyData")
|
||||
}
|
||||
}
|
||||
}
|
||||
ic?.endBatchEdit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class CapsMode {
|
||||
ALL,
|
||||
NONE,
|
||||
SENTENCES,
|
||||
WORDS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.inputmethod.CursorAnchorInfo
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
@@ -60,8 +59,8 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener {
|
||||
pasteKey = findViewById(R.id.clipboard_paste)
|
||||
}
|
||||
|
||||
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
|
||||
val isSelectionActive = florisboard?.textInputManager?.isTextSelected ?: false
|
||||
override fun onUpdateSelection() {
|
||||
val isSelectionActive = florisboard?.activeEditorInstance?.selection?.isSelectionMode ?: false
|
||||
val isSelectionMode = florisboard?.textInputManager?.isManualSelectionMode ?: false
|
||||
arrowUpKey?.isEnabled = !(isSelectionActive || isSelectionMode)
|
||||
arrowDownKey?.isEnabled = !(isSelectionActive || isSelectionMode)
|
||||
|
||||
@@ -23,7 +23,6 @@ import dev.patrickgold.florisboard.R
|
||||
import java.lang.Exception
|
||||
import kotlin.math.*
|
||||
|
||||
|
||||
/**
|
||||
* Wrapper class which holds all enums, interfaces and classes for detecting a swipe gesture.
|
||||
*/
|
||||
@@ -70,14 +69,15 @@ abstract class SwipeGesture {
|
||||
val diffX = event.x - firstEvent.x
|
||||
val diffY = event.y - firstEvent.y
|
||||
val distanceThresholdNV = numericValue(distanceThreshold)
|
||||
val velocityThresholdNV = numericValue(velocityThreshold)
|
||||
/*val velocityThresholdNV = numericValue(velocityThreshold)
|
||||
val velocity =
|
||||
((convertPixelsToDp(
|
||||
sqrt(diffX.pow(2) + diffY.pow(2)),
|
||||
context
|
||||
) / event.downTime) * 10.0f.pow(8)).toInt()
|
||||
) / event.downTime) * 10.0f.pow(8)).toInt()*/
|
||||
clearEventList()
|
||||
return if ((abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV) && velocity >= velocityThresholdNV) {
|
||||
// return if ((abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV) && velocity >= velocityThresholdNV) {
|
||||
return if ((abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV)) {
|
||||
val direction = detectDirection(diffX.toDouble(), diffY.toDouble())
|
||||
listener.onSwipe(direction, Type.TOUCH_UP)
|
||||
} else {
|
||||
@@ -173,20 +173,6 @@ abstract class SwipeGesture {
|
||||
VelocityThreshold.VERY_FAST -> context.resources.getInteger(R.integer.gesture_velocity_threshold_very_fast)
|
||||
}.toDouble()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method converts device specific pixels to density independent pixels.
|
||||
*
|
||||
* Source: https://stackoverflow.com/a/9563438/6801193 (by Muhammad Nabeel Arif)
|
||||
*
|
||||
* @param px A value in px (pixels) unit. Which we need to convert into db
|
||||
* @param context Context to get resources and device specific display metrics
|
||||
* @return A float value to represent dp equivalent to px value
|
||||
*/
|
||||
private fun convertPixelsToDp(px: Float, context: Context): Float {
|
||||
return px / (context.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
|
||||
}
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.text.key
|
||||
|
||||
object KeyCode {
|
||||
|
||||
@@ -1,8 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.text.key
|
||||
|
||||
/**
|
||||
* Data class which describes a single key and its variants.
|
||||
*
|
||||
* @property code The UTF-8 encoded code of the character. The code defined here is used as the
|
||||
* data passed to the system.
|
||||
* @property label The string used to display the key in the UI. Is not used for the actual data
|
||||
* passed to the system. Should normally be the exact same as the [code]. Defaults to an empty
|
||||
* string.
|
||||
* @property hintedNumber The hinted number which will be dynamically inserted into the long-press
|
||||
* [popup]. Leave null to disable the hinted popup for this key. The visibility of the hinted number
|
||||
* is controlled by the preferences. Defaults to null.
|
||||
* @property hintedSymbol The hinted symbol which will be dynamically inserted into the long-press
|
||||
* [popup]. Leave null to disable the hinted popup for this key. The visibility of the hinted symbol
|
||||
* is controlled by the preferences. Defaults to null.
|
||||
* @property popup List of keys which will be accessible while long pressing the key. Defaults to
|
||||
* an empty list (no extended popup).
|
||||
* @property type The type of the key. Some actions require both [code] and [type] to match in order
|
||||
* to be successfully executed. Defaults to [KeyType.CHARACTER].
|
||||
* @property variation Controls if the key should only be shown in some contexts (e.g.: url input)
|
||||
* or if the key should always be visible. Defaults to [KeyVariation.ALL].
|
||||
*/
|
||||
data class KeyData(
|
||||
var code: Int,
|
||||
var label: String = "",
|
||||
var hintedNumber: KeyData? = null,
|
||||
var hintedSymbol: KeyData? = null,
|
||||
var popup: MutableList<KeyData> = mutableListOf(),
|
||||
var type: KeyType = KeyType.CHARACTER,
|
||||
var variation: KeyVariation = KeyVariation.ALL
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.text.key
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.text.key
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package dev.patrickgold.florisboard.ime.text.key
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.*
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Handler
|
||||
@@ -39,6 +38,7 @@ import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
|
||||
import dev.patrickgold.florisboard.util.setBackgroundTintColor2
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* View class for managing the rendering and the events of a single keyboard key.
|
||||
@@ -53,7 +53,7 @@ class KeyView(
|
||||
private val keyboardView: KeyboardView,
|
||||
val data: KeyData
|
||||
) : View(keyboardView.context), SwipeGesture.Listener {
|
||||
|
||||
val dataPopupWithHint: MutableList<KeyData>
|
||||
private var isKeyPressed: Boolean = false
|
||||
set(value) {
|
||||
field = value
|
||||
@@ -64,6 +64,8 @@ class KeyView(
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
private var shouldBlockNextKeyCode: Boolean = false
|
||||
|
||||
private var desiredWidth: Int = 0
|
||||
private var desiredHeight: Int = 0
|
||||
private var drawable: Drawable? = null
|
||||
private var drawableColor: Int = 0
|
||||
private var drawablePadding: Int = 0
|
||||
@@ -77,6 +79,17 @@ class KeyView(
|
||||
textSize = resources.getDimension(R.dimen.key_textSize)
|
||||
typeface = Typeface.DEFAULT
|
||||
}
|
||||
private var hintedLabel: String? = null
|
||||
private var hintedLabelPaint: Paint = Paint().apply {
|
||||
alpha = 120
|
||||
color = 0
|
||||
isAntiAlias = true
|
||||
isFakeBoldText = true
|
||||
textAlign = Paint.Align.CENTER
|
||||
textSize = resources.getDimension(R.dimen.key_textHintSize)
|
||||
typeface = Typeface.DEFAULT
|
||||
}
|
||||
private val tempRect: Rect = Rect()
|
||||
|
||||
var florisboard: FlorisBoard? = null
|
||||
private val swipeGestureDetector = SwipeGesture.Detector(context, this)
|
||||
@@ -126,6 +139,23 @@ class KeyView(
|
||||
background = getDrawable(context, R.drawable.shape_rect_rounded)
|
||||
elevation = 4.0f
|
||||
|
||||
var hintKeyData: KeyData? = null
|
||||
val hintedNumber = data.hintedNumber
|
||||
if (prefs.keyboard.hintedNumberRow && hintedNumber != null) {
|
||||
hintKeyData = hintedNumber
|
||||
}
|
||||
val hintedSymbol = data.hintedSymbol
|
||||
if (prefs.keyboard.hintedSymbols && hintedSymbol != null) {
|
||||
hintKeyData = hintedSymbol
|
||||
}
|
||||
dataPopupWithHint = if (hintKeyData == null) {
|
||||
data.popup.toMutableList()
|
||||
} else {
|
||||
val popupList = data.popup.toMutableList()
|
||||
popupList.add(hintKeyData)
|
||||
popupList
|
||||
}
|
||||
|
||||
updateKeyPressedBackground()
|
||||
}
|
||||
|
||||
@@ -211,7 +241,7 @@ class KeyView(
|
||||
osHandler = Handler()
|
||||
}
|
||||
osHandler?.postDelayed({
|
||||
if (data.popup.isNotEmpty()) {
|
||||
if (dataPopupWithHint.isNotEmpty()) {
|
||||
keyboardView.popupManager.extend(this)
|
||||
}
|
||||
if (data.code == KeyCode.SPACE) {
|
||||
@@ -299,7 +329,7 @@ class KeyView(
|
||||
* by Devunwired
|
||||
*/
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val desiredWidth = when (keyboardView.computedLayout?.mode) {
|
||||
desiredWidth = when (keyboardView.computedLayout?.mode) {
|
||||
KeyboardMode.NUMERIC,
|
||||
KeyboardMode.PHONE,
|
||||
KeyboardMode.PHONE2 -> (keyboardView.desiredKeyWidth * 2.68f).toInt()
|
||||
@@ -318,7 +348,7 @@ class KeyView(
|
||||
else -> keyboardView.desiredKeyWidth
|
||||
}
|
||||
}
|
||||
val desiredHeight = keyboardView.desiredKeyHeight
|
||||
desiredHeight = keyboardView.desiredKeyHeight
|
||||
|
||||
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
|
||||
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
|
||||
@@ -470,6 +500,45 @@ class KeyView(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically sets the text size of [boxPaint] for given [text] so it fits within the given
|
||||
* bounds.
|
||||
*
|
||||
* Implementation based on this SO answer by Michael Scheper, but has been modified to
|
||||
* incorporate the height as well: https://stackoverflow.com/a/21895626/6801193
|
||||
*
|
||||
* @param boxPaint The [Paint] object which the text size should be applied to.
|
||||
* @param boxWidth The max width for the surrounding box of [text].
|
||||
* @param boxHeight The max height for the surrounding box of [text].
|
||||
* @param text The text for which the size should be calculated.
|
||||
*/
|
||||
private fun setTextSizeFor(boxPaint: Paint, boxWidth: Float, boxHeight: Float, text: String) {
|
||||
var textSize = 64.0f
|
||||
// Must loop twice as there can be bot with and height which are too big, which requires
|
||||
// 2 iterations to adjust
|
||||
for (n in 0..1) {
|
||||
boxPaint.textSize = textSize
|
||||
boxPaint.getTextBounds(text, 0, text.length, tempRect)
|
||||
val diffWidth = tempRect.width() - boxWidth
|
||||
val diffHeight = tempRect.height() - boxHeight
|
||||
val factor = if (diffWidth < 0 && diffHeight < 0) {
|
||||
// Text box is smaller as given box, text size must be increased
|
||||
if (abs(diffWidth) < abs(diffHeight)) {
|
||||
boxWidth / tempRect.width()
|
||||
} else {
|
||||
boxHeight / tempRect.height()
|
||||
}
|
||||
} else if (diffWidth > diffHeight) {
|
||||
// Text box is larger on minimum one side than given box, text size must be decreased
|
||||
boxWidth / tempRect.width()
|
||||
} else {
|
||||
boxHeight / tempRect.height()
|
||||
}
|
||||
textSize *= factor
|
||||
}
|
||||
boxPaint.textSize = textSize
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the key label / drawable.
|
||||
*/
|
||||
@@ -484,6 +553,15 @@ class KeyView(
|
||||
&& data.code != KeyCode.HALF_SPACE && data.code != KeyCode.KESHIDA || data.type == KeyType.NUMERIC
|
||||
) {
|
||||
label = getComputedLetter()
|
||||
val hintedNumber = data.hintedNumber
|
||||
if (prefs.keyboard.hintedNumberRow && hintedNumber != null) {
|
||||
hintedLabel = getComputedLetter(hintedNumber)
|
||||
}
|
||||
val hintedSymbol = data.hintedSymbol
|
||||
if (prefs.keyboard.hintedSymbols && hintedSymbol != null) {
|
||||
hintedLabel = getComputedLetter(hintedSymbol)
|
||||
}
|
||||
|
||||
} else {
|
||||
when (data.code) {
|
||||
KeyCode.DELETE -> {
|
||||
@@ -602,20 +680,40 @@ class KeyView(
|
||||
// Draw label
|
||||
val label = label
|
||||
if (label != null) {
|
||||
if (data.code == KeyCode.VIEW_NUMERIC || data.code == KeyCode.VIEW_NUMERIC_ADVANCED
|
||||
|| data.code == KeyCode.SPACE) {
|
||||
labelPaint.textSize = resources.getDimension(R.dimen.key_numeric_textSize)
|
||||
} else {
|
||||
labelPaint.textSize = resources.getDimension(R.dimen.key_textSize)
|
||||
when (data.code) {
|
||||
KeyCode.VIEW_NUMERIC, KeyCode.VIEW_NUMERIC_ADVANCED -> {
|
||||
labelPaint.textSize = resources.getDimension(R.dimen.key_numeric_textSize)
|
||||
}
|
||||
else -> when {
|
||||
data.type == KeyType.CHARACTER && data.code != KeyCode.SPACE -> {
|
||||
setTextSizeFor(
|
||||
labelPaint,
|
||||
desiredWidth - (2.6f * drawablePadding),
|
||||
desiredHeight - (3.6f * drawablePadding),
|
||||
// Note: taking a "X" here because it is one of the biggest letters and
|
||||
// the keys must have the same base character for calculation, else
|
||||
// they will all look different and weird...
|
||||
"X"
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
setTextSizeFor(
|
||||
labelPaint,
|
||||
measuredWidth - (2.6f * drawablePadding),
|
||||
measuredHeight - (3.6f * drawablePadding),
|
||||
when (data.code) {
|
||||
KeyCode.VIEW_CHARACTERS, KeyCode.VIEW_SYMBOLS, KeyCode.VIEW_SYMBOLS2 -> {
|
||||
resources.getString(R.string.key__view_symbols)
|
||||
}
|
||||
else -> label
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
labelPaint.color = prefs.theme.keyFgColor
|
||||
labelPaint.alpha = if (keyboardView.computedLayout?.mode == KeyboardMode.CHARACTERS &&
|
||||
data.code == KeyCode.SPACE) { 120 } else { 255 }
|
||||
val isPortrait =
|
||||
resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
|
||||
if (prefs.keyboard.oneHandedMode != "off" && isPortrait) {
|
||||
labelPaint.textSize *= 0.9f
|
||||
}
|
||||
val centerX = measuredWidth / 2.0f
|
||||
val centerY = measuredHeight / 2.0f + (labelPaint.textSize - labelPaint.descent()) / 2
|
||||
if (label.contains("\n")) {
|
||||
@@ -627,6 +725,25 @@ class KeyView(
|
||||
canvas.drawText(label, centerX, centerY, labelPaint)
|
||||
}
|
||||
}
|
||||
|
||||
// Draw hinted label
|
||||
val hintedLabel = hintedLabel
|
||||
if (hintedLabel != null) {
|
||||
setTextSizeFor(
|
||||
hintedLabelPaint,
|
||||
desiredWidth * 1.0f / 6.0f,
|
||||
desiredHeight * 1.0f / 6.0f,
|
||||
// Note: taking a "X" here because it is one of the biggest letters and
|
||||
// the keys must have the same base character for calculation, else
|
||||
// they will all look different and weird...
|
||||
"X"
|
||||
)
|
||||
hintedLabelPaint.color = prefs.theme.keyFgColor
|
||||
hintedLabelPaint.alpha = 120
|
||||
val centerX = measuredWidth * 5.0f / 6.0f
|
||||
val centerY = measuredHeight * 1.0f / 6.0f + (hintedLabelPaint.textSize - hintedLabelPaint.descent()) / 2
|
||||
canvas.drawText(hintedLabel, centerX, centerY, hintedLabelPaint)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,10 +22,7 @@ import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyData
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyTypeAdapter
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyVariation
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyVariationAdapter
|
||||
import dev.patrickgold.florisboard.ime.text.key.*
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.*
|
||||
@@ -37,7 +34,7 @@ private typealias KMS = Pair<KeyboardMode, Subtype>
|
||||
* Class which manages layout loading and caching.
|
||||
*/
|
||||
class LayoutManager(private val context: Context) : CoroutineScope by MainScope() {
|
||||
private val layoutCache: HashMap<KMS, Deferred<ComputedLayoutData>> = hashMapOf()
|
||||
private val computedLayoutCache: HashMap<KMS, Deferred<ComputedLayoutData>> = hashMapOf()
|
||||
|
||||
/**
|
||||
* Loads the layout for the specified type and name.
|
||||
@@ -49,6 +46,7 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
|
||||
if (type == null || name == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val rawJsonData: String = try {
|
||||
context.assets.open("ime/text/$type/$name.json").bufferedReader().use { it.readText() }
|
||||
} catch (e: Exception) {
|
||||
@@ -110,7 +108,7 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
|
||||
* @param extension The extension layout type and name.
|
||||
* @returns a [ComputedLayoutData] object, regardless of the specified LTNs or errors.
|
||||
*/
|
||||
private fun mergeLayouts(
|
||||
private suspend fun mergeLayoutsAsync(
|
||||
keyboardMode: KeyboardMode,
|
||||
subtype: Subtype,
|
||||
main: LTN? = null,
|
||||
@@ -194,6 +192,28 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
|
||||
}
|
||||
}
|
||||
|
||||
// Add hints to keys
|
||||
if (keyboardMode == KeyboardMode.CHARACTERS) {
|
||||
val symbolsComputedArrangement = fetchComputedLayoutAsync(KeyboardMode.SYMBOLS, subtype).await().arrangement
|
||||
for ((r, row) in computedArrangement.withIndex()) {
|
||||
if (r >= 3) {
|
||||
break
|
||||
}
|
||||
if (symbolsComputedArrangement.getOrNull(r) != null) {
|
||||
for ((k, key) in row.withIndex()) {
|
||||
if (key.type == KeyType.CHARACTER) {
|
||||
val symbol = symbolsComputedArrangement[r].getOrNull(k)
|
||||
if (r == 0) {
|
||||
key.hintedNumber = symbol
|
||||
} else {
|
||||
key.hintedSymbol = symbol
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ComputedLayoutData(
|
||||
keyboardMode,
|
||||
"computed",
|
||||
@@ -210,7 +230,7 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
|
||||
* @param keyboardMode The keyboard mode for which the layout should be computed.
|
||||
* @param subtype The subtype which localizes the computed layout.
|
||||
*/
|
||||
private fun computeLayoutFor(
|
||||
private suspend fun computeLayoutFor(
|
||||
keyboardMode: KeyboardMode,
|
||||
subtype: Subtype
|
||||
): ComputedLayoutData {
|
||||
@@ -223,6 +243,9 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
|
||||
main = LTN(LayoutType.CHARACTERS, subtype.layout)
|
||||
modifier = LTN(LayoutType.CHARACTERS_MOD, "default")
|
||||
}
|
||||
KeyboardMode.EDITING -> {
|
||||
// Layout for this mode is defined in custom layout xml file.
|
||||
}
|
||||
KeyboardMode.NUMERIC -> {
|
||||
main = LTN(LayoutType.NUMERIC, "default")
|
||||
}
|
||||
@@ -246,7 +269,7 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
|
||||
}
|
||||
}
|
||||
|
||||
return mergeLayouts(keyboardMode, subtype, main, modifier, extension)
|
||||
return mergeLayoutsAsync(keyboardMode, subtype, main, modifier, extension)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -263,14 +286,14 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
|
||||
subtype: Subtype
|
||||
): Deferred<ComputedLayoutData> {
|
||||
val kms = KMS(keyboardMode, subtype)
|
||||
val cachedComputedLayout = layoutCache[kms]
|
||||
val cachedComputedLayout = computedLayoutCache[kms]
|
||||
return if (cachedComputedLayout != null) {
|
||||
cachedComputedLayout
|
||||
} else {
|
||||
val computedLayout = async(Dispatchers.IO) {
|
||||
computeLayoutFor(keyboardMode, subtype)
|
||||
}
|
||||
layoutCache[kms] = computedLayout
|
||||
computedLayoutCache[kms] = computedLayout
|
||||
computedLayout
|
||||
}
|
||||
}
|
||||
@@ -289,8 +312,8 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
|
||||
subtype: Subtype
|
||||
) {
|
||||
val kms = KMS(keyboardMode, subtype)
|
||||
if (layoutCache[kms] == null) {
|
||||
layoutCache[kms] = async(Dispatchers.IO) {
|
||||
if (computedLayoutCache[kms] == null) {
|
||||
computedLayoutCache[kms] = async(Dispatchers.IO) {
|
||||
computeLayoutFor(keyboardMode, subtype)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class SmartbarManager private constructor() : FlorisBoard.EventListener {
|
||||
val view = v as Button
|
||||
val text = view.text.toString()
|
||||
if (text.isNotEmpty()) {
|
||||
textInputManager.commitCandidate(text)
|
||||
florisboard.activeEditorInstance.commitCompletion(text)
|
||||
}
|
||||
}
|
||||
private val candidateViewOnLongClickListener = View.OnLongClickListener { v ->
|
||||
@@ -161,12 +161,13 @@ class SmartbarManager private constructor() : FlorisBoard.EventListener {
|
||||
//spellCheckerSession?.close()
|
||||
}
|
||||
|
||||
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
|
||||
val isSelectionActive = florisboard.textInputManager.isTextSelected
|
||||
override fun onUpdateSelection() {
|
||||
val isSelectionActive = florisboard.activeEditorInstance.selection.isSelectionMode
|
||||
smartbarView?.findViewById<View>(R.id.cc_cut)?.isEnabled = isSelectionActive
|
||||
smartbarView?.findViewById<View>(R.id.cc_copy)?.isEnabled = isSelectionActive
|
||||
smartbarView?.findViewById<View>(R.id.cc_paste)?.isEnabled =
|
||||
florisboard.clipboardManager?.hasPrimaryClip() ?: false
|
||||
smartbarView?.invalidate()
|
||||
}
|
||||
|
||||
fun deleteCandidateFromDictionary(candidate: String) {
|
||||
@@ -177,10 +178,10 @@ class SmartbarManager private constructor() : FlorisBoard.EventListener {
|
||||
//
|
||||
}
|
||||
|
||||
fun generateCandidatesFromComposing(composingText: String?) {
|
||||
fun generateCandidatesFromComposing(composingText: String) {
|
||||
val smartbarView = smartbarView ?: return
|
||||
|
||||
if (composingText == null) {
|
||||
if (composingText == "") {
|
||||
smartbarView.candidateViewList[0].text = "candidate"
|
||||
smartbarView.candidateViewList[1].text = "suggestions"
|
||||
smartbarView.candidateViewList[2].text = "nyi"
|
||||
|
||||
@@ -16,11 +16,14 @@
|
||||
|
||||
package dev.patrickgold.florisboard.util
|
||||
|
||||
import android.content.Context
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
|
||||
|
||||
/**
|
||||
* This file has been taken from the Android LatinIME project. Following modifications were done to
|
||||
* the original source code:
|
||||
@@ -71,4 +74,30 @@ object ViewLayoutUtils {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method converts dp unit to equivalent pixels, depending on device density.
|
||||
*
|
||||
* Source: https://stackoverflow.com/a/9563438/6801193 (by Muhammad Nabeel Arif)
|
||||
*
|
||||
* @param dp A value in dp (density independent pixels) unit. Which we need to convert into pixels
|
||||
* @param context Context to get resources and device specific display metrics
|
||||
* @return A float value to represent px equivalent to dp depending on device density
|
||||
*/
|
||||
fun convertDpToPixel(dp: Float, context: Context): Float {
|
||||
return dp * (context.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method converts device specific pixels to density independent pixels.
|
||||
*
|
||||
* Source: https://stackoverflow.com/a/9563438/6801193 (by Muhammad Nabeel Arif)
|
||||
*
|
||||
* @param px A value in px (pixels) unit. Which we need to convert into db
|
||||
* @param context Context to get resources and device specific display metrics
|
||||
* @return A float value to represent dp equivalent to px value
|
||||
*/
|
||||
fun convertPixelsToDp(px: Float, context: Context): Float {
|
||||
return px / (context.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.util
|
||||
|
||||
import android.text.InputType
|
||||
import android.text.TextUtils
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
fun EditorInfo.debugSummarize(): String {
|
||||
var summary = this::class.qualifiedName + "\r\n"
|
||||
summary += "imeOptions: " + this.imeOptions.debugSummarize(EditorInfo::class) + "\r\n"
|
||||
summary += "initialCapsMode: " + this.initialCapsMode.debugSummarize(TextUtils::class) + "\r\n"
|
||||
summary += "initialSelStart: " + this.initialSelStart + "\r\n"
|
||||
summary += "initialSelEnd: " + this.initialSelEnd + "\r\n"
|
||||
summary += "inputType: " + this.inputType.debugSummarize(InputType::class) + "\r\n"
|
||||
summary += "packageName: " + this.packageName
|
||||
return summary
|
||||
}
|
||||
|
||||
fun <T: Any> Int.debugSummarize(type: KClass<T>): String {
|
||||
var summary = ""
|
||||
when (type) {
|
||||
EditorInfo::class -> {
|
||||
when (this) {
|
||||
EditorInfo.IME_NULL -> {
|
||||
summary += "IME_NULL"
|
||||
}
|
||||
else -> {
|
||||
val tAction = when (this and EditorInfo.IME_MASK_ACTION) {
|
||||
EditorInfo.IME_ACTION_DONE -> "IME_ACTION_DONE"
|
||||
EditorInfo.IME_ACTION_GO -> "IME_ACTION_GO"
|
||||
EditorInfo.IME_ACTION_NEXT -> "IME_ACTION_NEXT"
|
||||
EditorInfo.IME_ACTION_NONE -> "IME_ACTION_NONE"
|
||||
EditorInfo.IME_ACTION_PREVIOUS -> "IME_ACTION_PREVIOUS"
|
||||
EditorInfo.IME_ACTION_SEARCH -> "IME_ACTION_SEARCH"
|
||||
EditorInfo.IME_ACTION_SEND -> "IME_ACTION_SEND"
|
||||
EditorInfo.IME_ACTION_UNSPECIFIED -> "IME_ACTION_UNSPECIFIED"
|
||||
else -> String.format("0x%08x", this and EditorInfo.IME_MASK_ACTION)
|
||||
}
|
||||
var tFlags = ""
|
||||
if (this and EditorInfo.IME_FLAG_FORCE_ASCII > 0) {
|
||||
tFlags += "IME_FLAG_FORCE_ASCII|"
|
||||
}
|
||||
if (this and EditorInfo.IME_FLAG_NAVIGATE_NEXT > 0) {
|
||||
tFlags += "IME_FLAG_NAVIGATE_NEXT|"
|
||||
}
|
||||
if (this and EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS > 0) {
|
||||
tFlags += "IME_FLAG_NAVIGATE_PREVIOUS|"
|
||||
}
|
||||
if (this and EditorInfo.IME_FLAG_NO_ACCESSORY_ACTION > 0) {
|
||||
tFlags += "IME_FLAG_NO_ACCESSORY_ACTION|"
|
||||
}
|
||||
if (this and EditorInfo.IME_FLAG_NO_ENTER_ACTION > 0) {
|
||||
tFlags += "IME_FLAG_NO_ENTER_ACTION|"
|
||||
}
|
||||
if (this and EditorInfo.IME_FLAG_NO_EXTRACT_UI > 0) {
|
||||
tFlags += "IME_FLAG_NO_EXTRACT_UI|"
|
||||
}
|
||||
if (this and EditorInfo.IME_FLAG_NO_FULLSCREEN > 0) {
|
||||
tFlags += "IME_FLAG_NO_FULLSCREEN|"
|
||||
}
|
||||
if (this and EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING > 0) {
|
||||
tFlags += "IME_FLAG_NO_PERSONALIZED_LEARNING|"
|
||||
}
|
||||
if (tFlags.isEmpty()) {
|
||||
tFlags = "(none)"
|
||||
}
|
||||
if (tFlags.endsWith("|")) {
|
||||
tFlags = tFlags.substring(0, tFlags.length - 1)
|
||||
}
|
||||
summary += "action=$tAction flags=$tFlags"
|
||||
}
|
||||
}
|
||||
}
|
||||
InputType::class -> {
|
||||
when (this) {
|
||||
InputType.TYPE_NULL -> {
|
||||
summary += "TYPE_NULL"
|
||||
}
|
||||
else -> {
|
||||
val tClass: String
|
||||
val tVariation: String
|
||||
var tFlags = ""
|
||||
when (this and InputType.TYPE_MASK_CLASS) {
|
||||
InputType.TYPE_CLASS_DATETIME -> {
|
||||
tClass = "TYPE_CLASS_DATETIME"
|
||||
tVariation = when (this and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_DATETIME_VARIATION_DATE -> "TYPE_DATETIME_VARIATION_DATE"
|
||||
InputType.TYPE_DATETIME_VARIATION_NORMAL -> "TYPE_DATETIME_VARIATION_NORMAL"
|
||||
InputType.TYPE_DATETIME_VARIATION_TIME -> "TYPE_DATETIME_VARIATION_TIME"
|
||||
else -> String.format("0x%08x", this and InputType.TYPE_MASK_VARIATION)
|
||||
}
|
||||
}
|
||||
InputType.TYPE_CLASS_NUMBER -> {
|
||||
tClass = "TYPE_CLASS_NUMBER"
|
||||
tVariation = when (this and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_NUMBER_VARIATION_NORMAL -> "TYPE_NUMBER_VARIATION_NORMAL"
|
||||
InputType.TYPE_NUMBER_VARIATION_PASSWORD -> "TYPE_NUMBER_VARIATION_PASSWORD"
|
||||
else -> String.format("0x%08x", this and InputType.TYPE_MASK_VARIATION)
|
||||
}
|
||||
if (this and InputType.TYPE_NUMBER_FLAG_DECIMAL > 0) {
|
||||
tFlags += "TYPE_NUMBER_FLAG_DECIMAL|"
|
||||
}
|
||||
if (this and InputType.TYPE_NUMBER_FLAG_SIGNED > 0) {
|
||||
tFlags += "TYPE_NUMBER_FLAG_SIGNED|"
|
||||
}
|
||||
}
|
||||
InputType.TYPE_CLASS_PHONE -> {
|
||||
tClass = "TYPE_CLASS_PHONE"
|
||||
tVariation = String.format("0x%08x", this and InputType.TYPE_MASK_VARIATION)
|
||||
}
|
||||
InputType.TYPE_CLASS_TEXT -> {
|
||||
tClass = "TYPE_CLASS_TEXT"
|
||||
tVariation = when (this and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS -> "TYPE_TEXT_VARIATION_EMAIL_ADDRESS"
|
||||
InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT -> "TYPE_TEXT_VARIATION_EMAIL_SUBJECT"
|
||||
InputType.TYPE_TEXT_VARIATION_FILTER -> "TYPE_TEXT_VARIATION_FILTER"
|
||||
InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE -> "TYPE_TEXT_VARIATION_LONG_MESSAGE"
|
||||
InputType.TYPE_TEXT_VARIATION_NORMAL -> "TYPE_TEXT_VARIATION_NORMAL"
|
||||
InputType.TYPE_TEXT_VARIATION_PASSWORD -> "TYPE_TEXT_VARIATION_PASSWORD"
|
||||
InputType.TYPE_TEXT_VARIATION_PERSON_NAME -> "TYPE_TEXT_VARIATION_PERSON_NAME"
|
||||
InputType.TYPE_TEXT_VARIATION_PHONETIC -> "TYPE_TEXT_VARIATION_PHONETIC"
|
||||
InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS -> "TYPE_TEXT_VARIATION_POSTAL_ADDRESS"
|
||||
InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE -> "TYPE_TEXT_VARIATION_SHORT_MESSAGE"
|
||||
InputType.TYPE_TEXT_VARIATION_URI -> "TYPE_TEXT_VARIATION_URI"
|
||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD -> "TYPE_TEXT_VARIATION_VISIBLE_PASSWORD"
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT -> "TYPE_TEXT_VARIATION_WEB_EDIT_TEXT"
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS -> "TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS"
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD -> "TYPE_TEXT_VARIATION_WEB_PASSWORD"
|
||||
else -> String.format("0x%08x", this and InputType.TYPE_MASK_VARIATION)
|
||||
}
|
||||
if (this and InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE > 0) {
|
||||
tFlags += "TYPE_TEXT_FLAG_AUTO_COMPLETE|"
|
||||
}
|
||||
if (this and InputType.TYPE_TEXT_FLAG_AUTO_CORRECT > 0) {
|
||||
tFlags += "TYPE_TEXT_FLAG_AUTO_CORRECT|"
|
||||
}
|
||||
if (this and InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS > 0) {
|
||||
tFlags += "TYPE_TEXT_FLAG_CAP_CHARACTERS|"
|
||||
}
|
||||
if (this and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES > 0) {
|
||||
tFlags += "TYPE_TEXT_FLAG_CAP_SENTENCES|"
|
||||
}
|
||||
if (this and InputType.TYPE_TEXT_FLAG_CAP_WORDS > 0) {
|
||||
tFlags += "TYPE_TEXT_FLAG_CAP_WORDS|"
|
||||
}
|
||||
if (this and InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE > 0) {
|
||||
tFlags += "TYPE_TEXT_FLAG_IME_MULTI_LINE|"
|
||||
}
|
||||
if (this and InputType.TYPE_TEXT_FLAG_MULTI_LINE > 0) {
|
||||
tFlags += "TYPE_TEXT_FLAG_MULTI_LINE|"
|
||||
}
|
||||
if (this and InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS > 0) {
|
||||
tFlags += "TYPE_TEXT_FLAG_NO_SUGGESTIONS|"
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
tClass = String.format("0x%08x", this and InputType.TYPE_MASK_CLASS)
|
||||
tVariation = String.format("0x%08x", this and InputType.TYPE_MASK_VARIATION)
|
||||
}
|
||||
}
|
||||
if (tFlags.isEmpty()) {
|
||||
tFlags = "(none)"
|
||||
}
|
||||
if (tFlags.endsWith("|")) {
|
||||
tFlags = tFlags.substring(0, tFlags.length - 1)
|
||||
}
|
||||
summary += "class=$tClass variation=$tVariation flags=$tFlags"
|
||||
}
|
||||
}
|
||||
}
|
||||
TextUtils::class -> {
|
||||
var tFlags = ""
|
||||
if (this and TextUtils.CAP_MODE_CHARACTERS > 0) {
|
||||
tFlags += "CAP_MODE_CHARACTERS|"
|
||||
}
|
||||
if (this and TextUtils.CAP_MODE_SENTENCES > 0) {
|
||||
tFlags += "CAP_MODE_SENTENCES|"
|
||||
}
|
||||
if (this and TextUtils.CAP_MODE_WORDS > 0) {
|
||||
tFlags += "CAP_MODE_WORDS|"
|
||||
}
|
||||
if (this and TextUtils.SAFE_STRING_FLAG_FIRST_LINE > 0) {
|
||||
tFlags += "SAFE_STRING_FLAG_FIRST_LINE|"
|
||||
}
|
||||
if (this and TextUtils.SAFE_STRING_FLAG_SINGLE_LINE > 0) {
|
||||
tFlags += "SAFE_STRING_FLAG_SINGLE_LINE|"
|
||||
}
|
||||
if (this and TextUtils.SAFE_STRING_FLAG_TRIM > 0) {
|
||||
tFlags += "SAFE_STRING_FLAG_TRIM|"
|
||||
}
|
||||
if (tFlags.isEmpty()) {
|
||||
tFlags = "(none)"
|
||||
}
|
||||
if (tFlags.endsWith("|")) {
|
||||
tFlags = tFlags.substring(0, tFlags.length - 1)
|
||||
}
|
||||
summary += "flags=$tFlags"
|
||||
}
|
||||
}
|
||||
return summary
|
||||
}
|
||||
@@ -39,6 +39,16 @@
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
style="@style/SettingsCardView">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Note: Preferences tagged with [NYI] are not yet implemented and thus won\'t do anything or do some basic placeholder stuff only. Please do not file a bug report for these."/>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/repo_url_card"
|
||||
style="@style/SettingsCardView.Clickable">
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<dimen name="key_borderRadius">6dp</dimen>
|
||||
|
||||
<dimen name="key_textSize">18sp</dimen>
|
||||
<dimen name="key_textHintSize">10sp</dimen>
|
||||
<dimen name="key_numeric_textSize">12sp</dimen>
|
||||
<dimen name="key_popup_textSize">21sp</dimen>
|
||||
<dimen name="emoji_key_textSize">22sp</dimen>
|
||||
|
||||
@@ -101,6 +101,11 @@
|
||||
<string name="pref__theme__navBarIsLight_summary">Set to ON for dark or to OFF for light foreground.</string>
|
||||
|
||||
<string name="settings__keyboard__title">Keyboard Preferences</string>
|
||||
<string name="pref__keyboard__group_keys__label">Keys</string>
|
||||
<string name="pref__keyboard__hinted_number_row__label">Number row</string>
|
||||
<string name="pref__keyboard__hinted_number_row__summary">First row of character layout hints number row</string>
|
||||
<string name="pref__keyboard__hinted_symbols__label">Symbols</string>
|
||||
<string name="pref__keyboard__hinted_symbols__summary">Second and third row of character layout hint symbols</string>
|
||||
<string name="pref__keyboard__group_layout__label">Layout</string>
|
||||
<string name="pref__keyboard__one_handed_mode__label">One-handed mode</string>
|
||||
<string name="pref__keyboard__one_handed_mode__off">Off</string>
|
||||
@@ -114,6 +119,7 @@
|
||||
<string name="pref__keyboard__height_factor__mid_tall">Mid-tall</string>
|
||||
<string name="pref__keyboard__height_factor__tall">Tall</string>
|
||||
<string name="pref__keyboard__height_factor__extra_tall">Extra-tall</string>
|
||||
<string name="pref__keyboard__bottom_offset__label">Bottom offset (for curved screens)</string>
|
||||
<string name="pref__keyboard__group_keypress__label">Key press</string>
|
||||
<string name="pref__keyboard__sound_enabled__label">Sound on key press</string>
|
||||
<string name="pref__keyboard__sound_volume__label">Sound volume on key press</string>
|
||||
@@ -133,6 +139,10 @@
|
||||
<string name="pref__suggestion__use_pref_words__label">[NYI] Next-word suggestions</string>
|
||||
<string name="pref__suggestion__use_pref_words__summary">Use previous words for generating suggestions</string>
|
||||
<string name="pref__correction__title">Corrections</string>
|
||||
<string name="pref__correction__auto_capitalization__label">Auto-capitalization</string>
|
||||
<string name="pref__correction__auto_capitalization__summary">Capitalize words based on the current input context</string>
|
||||
<string name="pref__correction__remember_caps_lock_state__label">Remember caps lock state</string>
|
||||
<string name="pref__correction__remember_caps_lock_state__summary">Caps lock will stay on when moving to another text field</string>
|
||||
<string name="pref__correction__double_space_period__label">Double-space period</string>
|
||||
<string name="pref__correction__double_space_period__summary">Tapping twice on spacebar inserts a period followed by a space</string>
|
||||
|
||||
|
||||
@@ -96,14 +96,14 @@
|
||||
app:title="@string/pref__gestures__delete_key_swipe_left__label"
|
||||
app:useSimpleSummaryProvider="true"/>
|
||||
|
||||
<ListPreference
|
||||
<!--<ListPreference
|
||||
app:iconSpaceReserved="false"
|
||||
android:defaultValue="normal"
|
||||
app:entries="@array/pref__gestures__swipe_velocity_threshold__entries"
|
||||
app:entryValues="@array/pref__gestures__swipe_velocity_threshold__values"
|
||||
app:key="gestures__swipe_velocity_threshold"
|
||||
app:title="@string/pref__gestures__swipe_velocity_threshold__label"
|
||||
app:useSimpleSummaryProvider="true"/>
|
||||
app:useSimpleSummaryProvider="true"/>-->
|
||||
|
||||
<ListPreference
|
||||
app:iconSpaceReserved="false"
|
||||
|
||||
@@ -2,6 +2,26 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<PreferenceCategory
|
||||
app:iconSpaceReserved="false"
|
||||
app:title="@string/pref__keyboard__group_keys__label">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="keyboard__hinted_number_row"
|
||||
app:iconSpaceReserved="false"
|
||||
app:title="@string/pref__keyboard__hinted_number_row__label"
|
||||
app:summary="@string/pref__keyboard__hinted_number_row__summary"/>
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="keyboard__hinted_symbols"
|
||||
app:iconSpaceReserved="false"
|
||||
app:title="@string/pref__keyboard__hinted_symbols__label"
|
||||
app:summary="@string/pref__keyboard__hinted_symbols__summary"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
app:iconSpaceReserved="false"
|
||||
app:title="@string/pref__keyboard__group_layout__label">
|
||||
@@ -24,6 +44,17 @@
|
||||
app:title="@string/pref__keyboard__height_factor__label"
|
||||
app:useSimpleSummaryProvider="true"/>
|
||||
|
||||
<dev.patrickgold.florisboard.settings.components.DialogSeekBarPreference
|
||||
app:allowDividerAbove="false"
|
||||
android:defaultValue="0"
|
||||
app:key="keyboard__bottom_offset"
|
||||
app:min="0"
|
||||
app:max="24"
|
||||
app:iconSpaceReserved="false"
|
||||
app:title="@string/pref__keyboard__bottom_offset__label"
|
||||
app:seekBarIncrement="1"
|
||||
app:unit=" dp"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
app:enabled="false"
|
||||
app:key="suggestion__enabled"
|
||||
app:iconSpaceReserved="false"
|
||||
app:title="@string/pref__suggestion__enabled__label"
|
||||
@@ -36,6 +37,20 @@
|
||||
app:iconSpaceReserved="false"
|
||||
app:title="@string/pref__correction__title">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
app:key="correction__auto_capitalization"
|
||||
app:iconSpaceReserved="false"
|
||||
app:title="@string/pref__correction__auto_capitalization__label"
|
||||
app:summary="@string/pref__correction__auto_capitalization__summary"/>
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
app:key="correction__remember_caps_lock_state"
|
||||
app:iconSpaceReserved="false"
|
||||
app:title="@string/pref__correction__remember_caps_lock_state__label"
|
||||
app:summary="@string/pref__correction__remember_caps_lock_state__summary"/>
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
app:key="correction__double_space_period"
|
||||
|
||||
14
fastlane/metadata/android/en-US/changelogs/14.txt
Normal file
14
fastlane/metadata/android/en-US/changelogs/14.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
- Add number row / underlying symbol support in character layouts
|
||||
- First row of each character layout has numbers 1-9 and 0 integrated
|
||||
- Second and third row have symbols according to the symbol layout
|
||||
- Number row / Underlying symbols can be enabled/disabled seperately
|
||||
in the preferences
|
||||
- Add bottom offset option to accommodate for curved screens (#20)
|
||||
- Add option to turn off auto-capitalization (#21)
|
||||
- Fix clipboard/cursor UI not updating in Smartbar when text selection
|
||||
has changed
|
||||
- Improve emoji layout
|
||||
- Scroll orientation is now vertical to better scale to different sizes
|
||||
of the keyboard
|
||||
- Temporarily remove swipe velocity threshold as it causes gestures to be
|
||||
ignored in certain circumstances
|
||||
5
fastlane/metadata/android/en-US/changelogs/15.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/15.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
- Rework core to better implement interface between FlorisBoard and other apps
|
||||
- Shift state should now update after a key press (#35)
|
||||
- Send key should now send the desired action or a newline character (#33)
|
||||
- Adjusting keyboard height also affects font size of keys (#32)
|
||||
- Add option to remember / forget caps lock state throughout different input fields (#30)
|
||||
14
fastlane/metadata/android/it/changelogs/14.txt
Normal file
14
fastlane/metadata/android/it/changelogs/14.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
- Aggiungere la riga del numero / il supporto del simbolo sottostante nei layout dei caratteri
|
||||
- La prima riga di ogni disposizione dei caratteri ha i numeri 1-9 e 0 integrati
|
||||
- La seconda e la terza riga hanno i simboli secondo la disposizione dei simboli
|
||||
- Riga dei numeri / I simboli sottostanti possono essere abilitati/disabilitati separatamente
|
||||
nelle preferenze
|
||||
- Aggiungete l'opzione di offset dal basso per accogliere gli schermi curvi (#20)
|
||||
- Aggiungere l'opzione per disattivare l'autocapitalizzazione (#21)
|
||||
- Fix clipboard/cursore UI non aggiornato in Smartbar quando si seleziona il testo
|
||||
è cambiato
|
||||
- Migliorare il layout emoji
|
||||
- L'orientamento di scorrimento è ora verticale per meglio scalare le diverse dimensioni
|
||||
della tastiera
|
||||
- Rimuovere temporaneamente la soglia di velocità di strisciata in quanto fa sì che i gesti siano
|
||||
ignorato in determinate circostanze
|
||||
5
fastlane/metadata/android/it/changelogs/15.txt
Normal file
5
fastlane/metadata/android/it/changelogs/15.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
- Riorganizzazione del core per implementare al meglio l'interfaccia tra FlorisBoard e le altre applicazioni
|
||||
- Lo stato del turno dovrebbe ora aggiornarsi dopo la pressione di un tasto (#35)
|
||||
- Il tasto Invio dovrebbe ora inviare l'azione desiderata o un carattere di nuova linea (#33)
|
||||
- La regolazione dell'altezza della tastiera influisce anche sulla dimensione dei caratteri dei tasti (#32)
|
||||
- Aggiungere l'opzione per ricordare / dimenticare lo stato di blocco dei maiuscoli in diversi campi di input (#30)
|
||||
Reference in New Issue
Block a user