Compare commits
106 Commits
v0.3.16
...
v0.4.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26650d2a00 | ||
|
|
4395eac500 | ||
|
|
90e60a5e03 | ||
|
|
e647e0d248 | ||
|
|
31c046720a | ||
|
|
03b70b43a6 | ||
|
|
de2b3b9433 | ||
|
|
a51f671c3c | ||
|
|
314cdf79bf | ||
|
|
d7137b41fe | ||
|
|
acad9f66a6 | ||
|
|
8d0565854c | ||
|
|
6932fecbbd | ||
|
|
3e6ed3d7b0 | ||
|
|
85e76892b7 | ||
|
|
65cbc4bea3 | ||
|
|
79d177144a | ||
|
|
8cb2b0bfa7 | ||
|
|
201de6a6db | ||
|
|
f65b11bc6d | ||
|
|
86031bb428 | ||
|
|
58f62e1bd5 | ||
|
|
6212e35382 | ||
|
|
c8d0c6269f | ||
|
|
e6f40932ed | ||
|
|
f8af02c400 | ||
|
|
932a7c3126 | ||
|
|
f0c2ac566f | ||
|
|
19224e5f18 | ||
|
|
6c325af80e | ||
|
|
bb82b78cb7 | ||
|
|
eb30eed735 | ||
|
|
3198977143 | ||
|
|
f3b3c21aaa | ||
|
|
0606afbb64 | ||
|
|
5362df02a5 | ||
|
|
3d15bd7f46 | ||
|
|
15e94ecf2c | ||
|
|
ebb3873fe4 | ||
|
|
c1231cd964 | ||
|
|
53ab0a3fa0 | ||
|
|
aeeff67d2e | ||
|
|
4343703eb3 | ||
|
|
5f09bdbce2 | ||
|
|
3a3e3625f2 | ||
|
|
bdf14c1997 | ||
|
|
d6e064ae00 | ||
|
|
1012094568 | ||
|
|
4117537ff2 | ||
|
|
2a72cb70d6 | ||
|
|
b1cd9d9389 | ||
|
|
568dfc973d | ||
|
|
2e74cec0db | ||
|
|
db378159d6 | ||
|
|
50ff2d8f1b | ||
|
|
061495fb27 | ||
|
|
40cb59ddfd | ||
|
|
38affddc9e | ||
|
|
17dcb90473 | ||
|
|
772402b46f | ||
|
|
29bd8a289c | ||
|
|
79d9e73608 | ||
|
|
b04f8d75f3 | ||
|
|
32b1d123d2 | ||
|
|
b576cafaa4 | ||
|
|
312ef93ffc | ||
|
|
a1dda0c247 | ||
|
|
0c36b96922 | ||
|
|
07e92f052b | ||
|
|
244c834de9 | ||
|
|
61b5c2cffd | ||
|
|
1441bd63cb | ||
|
|
e7d0db0fc0 | ||
|
|
a87d340b25 | ||
|
|
3f0d90cb7c | ||
|
|
b9e9f9b122 | ||
|
|
f2d1cf3baf | ||
|
|
cf1112327a | ||
|
|
c2cb28668d | ||
|
|
75f4fcb91a | ||
|
|
3d92bd0584 | ||
|
|
52ca98a14d | ||
|
|
629a73a5cf | ||
|
|
077ec43855 | ||
|
|
3ecd3618cb | ||
|
|
38bc34913b | ||
|
|
c733e5ceea | ||
|
|
e2536ceb92 | ||
|
|
c17b6f073d | ||
|
|
936b177776 | ||
|
|
7d8036fe69 | ||
|
|
6d08d1a265 | ||
|
|
48aba1c055 | ||
|
|
0b3d3317bf | ||
|
|
d1fbdc581b | ||
|
|
044170eb4b | ||
|
|
a7c16b3ceb | ||
|
|
dd12be2275 | ||
|
|
1049bc543a | ||
|
|
5c5ad3cd32 | ||
|
|
6fce521122 | ||
|
|
2af9941ea6 | ||
|
|
9b24f742d1 | ||
|
|
b36bcf7733 | ||
|
|
9559dbdcd6 | ||
|
|
668dd4b5bf |
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1,2 +1,3 @@
|
||||
github: [patrickgold]
|
||||
liberapay: patrickgold
|
||||
custom: ["https://paypal.me/devpatrickgold"]
|
||||
|
||||
@@ -54,7 +54,8 @@ fully respecting your privacy. Currently in early-beta state.
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
Beginning with v0.4.0 FlorisBoard will follow [SemVer](https://semver.org/#summary) versioning scheme and enter the public beta on Google Play.
|
||||
Beginning with v0.4.0 FlorisBoard will follow [SemVer](https://semver.org/#summary) versioning scheme.
|
||||
Beginning with v0.5.0 FlorisBoard will enter the public beta on Google Play.
|
||||
|
||||
## Highlighted features
|
||||
- Integrated clipboard manager / history
|
||||
|
||||
101
ROADMAP.md
101
ROADMAP.md
@@ -1,4 +1,3 @@
|
||||
|
||||
# FlorisBoard's feature roadmap & milestones
|
||||
|
||||
This feature roadmap intents to provide transparency to what I want to add to FlorisBoard in the foreseeable future.
|
||||
@@ -9,35 +8,83 @@ out a bit on the stable track. If you are interested in following the developmen
|
||||
along the beta track releases! These are generally more unstable but you get new stuff faster and can provide early
|
||||
feedback, which helps a lot!
|
||||
|
||||
## 0.3.x
|
||||
## 0.4
|
||||
|
||||
Releases in this section still follow the old versioning scheme, meaning the patch number is a feature upgrade. As this
|
||||
naming convention is more confusing than useful, beginning with v0.4.0 development a new release/development cycle will
|
||||
be introduced.
|
||||
Major release which mainly focuses on adding proper word suggestions and inline autocorrect (for Latin-based languages
|
||||
only at first). This is a big effort which will take some time to be fully completed. Additionally general small bug
|
||||
fixes and improvements will be made alongside the development of the main objective.
|
||||
|
||||
### 0.3.15 & 0.3.16 (currently 0.3.15 done, 0.3.16 in work)
|
||||
With this release the versioning scheme changes to `0.x.y`, where `x` specifies the major changes, and `y` are just
|
||||
small bug fixes and improvements for the former major stable release `x`. This is different to `0.3.x`, where the
|
||||
version scheme just did not make any sense anymore, especially with the latest `0.3.x` releases. As for the beta track,
|
||||
major developments (`0.x`) will have alpha, beta and release candidate releases on the beta track before it goes live on
|
||||
the stable track. Small follow-up bug fixes (`0.x.y`) will be published on both the stable and beta track without
|
||||
release candidates.
|
||||
|
||||
- Hotfix releases for possible bugs in the preference rework (in work)
|
||||
- Lots and lots of bug fixing in general (in work)
|
||||
- Preparation work for 0.4.0, fixing text state logic and use break iterator (done)
|
||||
- Reducing or getting rid of input lag some devices experience (done)
|
||||
- Clean up of project structure for better future development (done)
|
||||
### Word suggestions / Autocorrect
|
||||
|
||||
## 0.4.0
|
||||
The development effort of this feature is quite big, thus it is split into multiple phases:
|
||||
|
||||
- Re-adding word suggestions (at least for Latin-based languages at first)
|
||||
- Importing the dictionaries as well as management relies on the Flex extension core and UI in Kotlin
|
||||
- Actually parsing and generating suggestions happens in C++ to avoid another OOM catastrophe like in 0.3.9/10
|
||||
- The actual format of the dictionary and word list source is not decided yet
|
||||
- Community repository on GitHub for theme sharing across users (may be 0.5.0)
|
||||
**Phase 1: Preparations of suggestions UI & interfacing API (first alpha release(s))**
|
||||
|
||||
With this release the versioning scheme changes: the second number now indicates new features, changes in the third "
|
||||
patch" number now indicates bug fixes and minor feature additions for the stable track. The development cycle for each
|
||||
0.x release will have `-alphaXX` (optional and only for large releases), `-betaXX` and `-rcXX` (release candidate)
|
||||
releases on the beta track for interested people to follow along the development. The first release to follow the new
|
||||
scheme will be `0.4.0-alpha01` on the beta track.
|
||||
- Rework Smartbar suggestions UI
|
||||
- Allow for primary and optionally secondary label (in a smaller font) to be shown per suggestion
|
||||
- Better integrate clipboard suggestions into word suggestion flow
|
||||
- Add long-press suggestion action for user to prevent from showing again
|
||||
- Generally fix and polish suggestions UI design (3-column mode and scrollable mode)
|
||||
- Add a `SuggestionProvider` interface API to allow for any specialized implementation to be written
|
||||
- A provider's main task is to receive updates on the current state of the editor (except for raw inputs) and
|
||||
provide both current word autocorrect/suggestions or next word suggestions if there is no current word
|
||||
- The provider can utilize the basic APK asset and file APIs for reading dictionary files, however there is no
|
||||
standardization in parsing as different languages may require different dictionary structures and thus have
|
||||
different requirements
|
||||
- Document API and add dummy implementation to test API
|
||||
- Try to add toggle for not underlining the current word (composing region) while not loosing the caching benefits
|
||||
- In parallel: Do local research and preps for phase 2
|
||||
|
||||
## 0.5.0
|
||||
_[Anomaly](https://www.anomaly.ltd/), an Australian software company, will sponsor this project with 1000€ so this phase
|
||||
gets implemented first, as they want to use FlorisBoard as a base for
|
||||
their [WCC Language Program project (Gurray)](https://www.anomaly.ltd/portfolio/wcc/gurray/). As this fits in perfectly
|
||||
with the current dev cycle and this had to be done anyways (some parts like documenting and UI polishing just later in
|
||||
the 0.4 milestone), I have accepted this. However in general this does not mean this project accepts sponsoring for any
|
||||
feature to be prioritized, as the project's main goals and planned feature timeline must always come first and human dev
|
||||
resources are limited._
|
||||
|
||||
**Phase 2: Add native (C++) Latin word suggestion core (alpha releases)**
|
||||
|
||||
- Research and experiment with different approaches/data sources for Latin-based language prediction and autocorrect
|
||||
- Research will mainly be done first locally on Linux to decide what to use
|
||||
- Implementation will be in C++ using STL libraries and if needed other open-source libraries, with compatibility
|
||||
and CPU/memory restrictions on Android devices in mind
|
||||
- Once an experiment runs well locally it will be included in the main project and tested out within the keyboard UI
|
||||
in different alpha releases
|
||||
- Especially at the beginning an idea may be scrapped and replaced by something else if found that another approach
|
||||
is better
|
||||
- (Based on research) Introduce new dictionary/language model format
|
||||
- Importing the dictionaries/models as well as management relies on the Flex extension core and UI in Kotlin
|
||||
- Actually parsing and generating suggestions happens in C++
|
||||
- The actual format of the dictionary/model source is not decided yet
|
||||
- Add system in preprocessing stage to properly mark slightly offensive words and prevent extremely offensive words
|
||||
from being included at all
|
||||
- Add system in preprocessing stage to filter out email addresses and phone numbers that may be included in the
|
||||
large datasets which are used for building the models
|
||||
|
||||
**Phase 3: Add support for more languages & Allow glide typing to utilize new word prediction system (beta releases)**
|
||||
|
||||
- Glide typing: Utilize new prediction system and get rid of current English (US) json dictionary
|
||||
- Add support for more languages (Latin-based), may need to utilize datasets like Opensubtitles or Wikimedia, although
|
||||
those need extensive cleaning and are not as reliable
|
||||
- Focus on improving performance and stabilizing the Latin suggestion core
|
||||
- Possibly address some language-specific issues and ensure suggested word capitalization is correct
|
||||
- Finalize Settings and keyboard UI regarding word suggestions.
|
||||
|
||||
### Other planned features for 0.4
|
||||
|
||||
- General small fixes and improvements
|
||||
- Community repository on GitHub for extension sharing across users (may be 0.5.0 though)
|
||||
- Localized emoji suggestions (may be 0.5.0 though)
|
||||
|
||||
## 0.5
|
||||
|
||||
- Complete rework of the Emoji panel
|
||||
- Recently used / Emoji history (already implemented with 0.3.14)
|
||||
@@ -51,18 +98,16 @@ scheme will be `0.4.0-alpha01` on the beta track.
|
||||
- Rework branding images and texts of FlorisBoard for the app stores
|
||||
- Focus on stability and experience improvements of the app and keyboard
|
||||
|
||||
## 0.6.0
|
||||
## Backlog
|
||||
|
||||
**Features that MAY be added (even in versions mentioned above) or dismissed altogether**
|
||||
|
||||
- Full on-board layout editor which allows users to create their own layouts without writing a JSON file
|
||||
- Import/Export of custom layout files packed in Flex extensions
|
||||
|
||||
## Backlog / Features that MAY be added, even in versions not mentioned above if the feature implementation fits perfectly with another feature
|
||||
|
||||
- Theme rework part II
|
||||
- Adaptive themes v2
|
||||
- Voice-to-text with Mozilla's open-source voice service
|
||||
- Text translation
|
||||
- Glide typing better word detection
|
||||
- Proximity-based key typo detection
|
||||
- Floating keyboard
|
||||
- Tablet mode / Optimizations for landscape input
|
||||
|
||||
@@ -31,7 +31,7 @@ plugins {
|
||||
|
||||
android {
|
||||
namespace = "dev.patrickgold.florisboard"
|
||||
compileSdk = 31
|
||||
compileSdk = 32
|
||||
buildToolsVersion = "31.0.0"
|
||||
ndkVersion = "22.1.7171670"
|
||||
|
||||
@@ -44,9 +44,8 @@ android {
|
||||
jvmTarget = "1.8"
|
||||
freeCompilerArgs = listOf(
|
||||
"-Xallow-result-return-type",
|
||||
"-Xopt-in=kotlin.RequiresOptIn",
|
||||
"-Xopt-in=kotlin.contracts.ExperimentalContracts",
|
||||
"-Xjvm-default=compatibility",
|
||||
"-opt-in=kotlin.contracts.ExperimentalContracts",
|
||||
"-Xjvm-default=all-compatibility",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -54,8 +53,8 @@ android {
|
||||
applicationId = "dev.patrickgold.florisboard"
|
||||
minSdk = 24
|
||||
targetSdk = 31
|
||||
versionCode = 86
|
||||
versionName = "0.3.16"
|
||||
versionCode = 89
|
||||
versionName = "0.4.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -108,7 +107,7 @@ android {
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = libs.versions.androidx.compose.get()
|
||||
kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get()
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
@@ -138,7 +137,7 @@ android {
|
||||
|
||||
create("beta") {
|
||||
applicationIdSuffix = ".beta"
|
||||
versionNameSuffix = ""
|
||||
versionNameSuffix = "-alpha03"
|
||||
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
isMinifyEnabled = true
|
||||
@@ -193,7 +192,6 @@ tasks.withType<Test> {
|
||||
|
||||
dependencies {
|
||||
implementation(libs.accompanist.flowlayout)
|
||||
implementation(libs.accompanist.insets)
|
||||
implementation(libs.accompanist.systemuicontroller)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.activity.ktx)
|
||||
@@ -219,6 +217,7 @@ dependencies {
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.mikepenz.aboutlibraries.core)
|
||||
implementation(libs.mikepenz.aboutlibraries.compose)
|
||||
implementation(libs.patrickgold.compose.tooltip)
|
||||
|
||||
testImplementation(libs.equalsverifier)
|
||||
testImplementation(libs.kotest.assertions.core)
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
android:launchMode="singleTask"
|
||||
android:roundIcon="@mipmap/floris_app_icon_round"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/FlorisAppTheme"
|
||||
android:theme="@style/FlorisAppTheme.Splash"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<data android:scheme="florisboard" android:host="app-ui"/>
|
||||
@@ -95,7 +95,7 @@
|
||||
android:launchMode="singleTask"
|
||||
android:roundIcon="@mipmap/floris_app_icon_round"
|
||||
android:targetActivity="dev.patrickgold.florisboard.app.FlorisAppActivity"
|
||||
android:theme="@style/FlorisAppTheme"
|
||||
android:theme="@style/FlorisAppTheme.Splash"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
Binary file not shown.
@@ -45,6 +45,18 @@
|
||||
{ "code": 165, "label": "¥" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "costa_rican_colon",
|
||||
"label": "Costa Rican colón (₡)",
|
||||
"slots": [
|
||||
{ "code": 8353, "label": "₡" },
|
||||
{ "code": 36, "label": "$" },
|
||||
{ "code": 8364, "label": "€" },
|
||||
{ "code": 162, "label": "¢" },
|
||||
{ "code": 163, "label": "£" },
|
||||
{ "code": 165, "label": "¥" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dollar",
|
||||
"label": "Dollar ($)",
|
||||
|
||||
@@ -199,13 +199,13 @@
|
||||
},
|
||||
{
|
||||
"id": "jcuken_russian",
|
||||
"label": "Russian (JCUKEN)",
|
||||
"label": "Russian (ЙЦУКЕН)",
|
||||
"authors": [ "williamtheaker" ],
|
||||
"direction": "ltr"
|
||||
},
|
||||
{
|
||||
"id": "jcuken_ukrainian",
|
||||
"label": "Ukrainian (JCUKEN)",
|
||||
"label": "Ukrainian (ЙЦУКЕН)",
|
||||
"authors": [ "williamtheaker", "33kk" ],
|
||||
"direction": "ltr"
|
||||
},
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
[
|
||||
[
|
||||
{ "code": -35, "label": "clipboard_select_all", "type": "enter_editing" },
|
||||
{ "code": -31, "label": "clipboard_copy", "type": "enter_editing" },
|
||||
{ "code": -32, "label": "clipboard_cut", "type": "enter_editing" },
|
||||
{ "code": -21, "label": "arrow_left", "type": "navigation" },
|
||||
{ "code": -22, "label": "arrow_right", "type": "navigation" },
|
||||
{ "code": -33, "label": "clipboard_paste", "type": "enter_editing" },
|
||||
{ "code": -38, "label": "clipboard_clear_primary_clip", "type": "system_gui"},
|
||||
{ "code": -213, "label": "ime_ui_mode_clipboard", "type": "system_gui"}
|
||||
]
|
||||
]
|
||||
@@ -1,68 +1,200 @@
|
||||
[
|
||||
[
|
||||
{ "code": 49, "label": "1", "type": "numeric", "popup": {
|
||||
"main": { "code": 185, "label": "¹" },
|
||||
"relevant": [
|
||||
{ "code": 8537, "label": "⅙" },
|
||||
{ "code": 8528, "label": "⅐" },
|
||||
{ "code": 8539, "label": "⅛" },
|
||||
{ "code": 8529, "label": "⅑" },
|
||||
{ "code": 8530, "label": "⅒" },
|
||||
{ "code": 189, "label": "½" },
|
||||
{ "code": 8531, "label": "⅓" },
|
||||
{ "code": 188, "label": "¼" },
|
||||
{ "code": 8533, "label": "⅕" }
|
||||
]
|
||||
} },
|
||||
{ "code": 50, "label": "2", "type": "numeric", "popup": {
|
||||
"main": { "code": 178, "label": "²" },
|
||||
"relevant": [
|
||||
{ "code": 8532, "label": "⅔" },
|
||||
{ "code": 8534, "label": "⅖" }
|
||||
]
|
||||
} },
|
||||
{ "code": 51, "label": "3", "type": "numeric", "popup": {
|
||||
"main": { "code": 179, "label": "³" },
|
||||
"relevant": [
|
||||
{ "code": 8535, "label": "⅗" },
|
||||
{ "code": 190, "label": "¾" },
|
||||
{ "code": 8540, "label": "⅜" }
|
||||
]
|
||||
} },
|
||||
{ "code": 52, "label": "4", "type": "numeric", "popup": {
|
||||
"main": { "code": 8308, "label": "⁴" },
|
||||
"relevant": [
|
||||
{ "code": 8536, "label": "⅘" }
|
||||
]
|
||||
} },
|
||||
{ "code": 53, "label": "5", "type": "numeric", "popup": {
|
||||
"main": { "code": 8309, "label": "⁵" },
|
||||
"relevant": [
|
||||
{ "code": 8538, "label": "⅚" },
|
||||
{ "code": 8541, "label": "⅝" }
|
||||
]
|
||||
} },
|
||||
{ "code": 54, "label": "6", "type": "numeric", "popup": {
|
||||
"main": { "code": 8310, "label": "⁶" }
|
||||
} },
|
||||
{ "code": 55, "label": "7", "type": "numeric", "popup": {
|
||||
"main": { "code": 8311, "label": "⁷" },
|
||||
"relevant": [
|
||||
{ "code": 8542, "label": "⅞" }
|
||||
]
|
||||
} },
|
||||
{ "code": 56, "label": "8", "type": "numeric", "popup": {
|
||||
"main": { "code": 8312, "label": "⁸" }
|
||||
} },
|
||||
{ "code": 57, "label": "9", "type": "numeric", "popup": {
|
||||
"main": { "code": 8313, "label": "⁹" }
|
||||
} },
|
||||
{ "code": 48, "label": "0", "type": "numeric", "popup": {
|
||||
"main": { "code": 8304, "label": "⁰" },
|
||||
"relevant": [
|
||||
{ "code": 8709, "label": "∅" },
|
||||
{ "code": 8319, "label": "ⁿ" }
|
||||
]
|
||||
} }
|
||||
{ "$": "shift_state_selector",
|
||||
"shiftedManual": {
|
||||
"code": 33, "label": "!", "type": "numeric", "popup": {
|
||||
"main": { "code": 161, "label": "¡" }
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"code": 49, "label": "1", "type": "numeric", "popup": {
|
||||
"main": { "code": 185, "label": "¹" },
|
||||
"relevant": [
|
||||
{ "code": 8537, "label": "⅙" },
|
||||
{ "code": 8528, "label": "⅐" },
|
||||
{ "code": 8539, "label": "⅛" },
|
||||
{ "code": 8529, "label": "⅑" },
|
||||
{ "code": 8530, "label": "⅒" },
|
||||
{ "code": 189, "label": "½" },
|
||||
{ "code": 8531, "label": "⅓" },
|
||||
{ "code": 188, "label": "¼" },
|
||||
{ "code": 8533, "label": "⅕" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "$": "shift_state_selector",
|
||||
"shiftedManual": { "code": 64, "label": "@", "type": "numeric" },
|
||||
"default": {
|
||||
"code": 50, "label": "2", "type": "numeric", "popup": {
|
||||
"main": { "code": 178, "label": "²" },
|
||||
"relevant": [
|
||||
{ "code": 8532, "label": "⅔" },
|
||||
{ "code": 8534, "label": "⅖" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "$": "shift_state_selector",
|
||||
"shiftedManual": {
|
||||
"code": 35, "label": "#", "type": "numeric", "popup": {
|
||||
"main": { "code": 8470, "label": "№" }
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"code": 51, "label": "3", "type": "numeric", "popup": {
|
||||
"main": { "code": 179, "label": "³" },
|
||||
"relevant": [
|
||||
{ "code": 8535, "label": "⅗" },
|
||||
{ "code": 190, "label": "¾" },
|
||||
{ "code": 8540, "label": "⅜" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "$": "shift_state_selector",
|
||||
"shiftedManual": {
|
||||
"code": -801, "label": "currency_slot_1", "type": "numeric", "popup": {
|
||||
"main": { "code": -802, "label": "currency_slot_2" },
|
||||
"relevant": [
|
||||
{ "code": -806, "label": "currency_slot_6" },
|
||||
{ "code": -803, "label": "currency_slot_3" },
|
||||
{ "code": -804, "label": "currency_slot_4" },
|
||||
{ "code": -805, "label": "currency_slot_5" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"code": 52, "label": "4", "type": "numeric", "popup": {
|
||||
"main": { "code": 8308, "label": "⁴" },
|
||||
"relevant": [
|
||||
{ "code": 8536, "label": "⅘" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "$": "shift_state_selector",
|
||||
"shiftedManual": {
|
||||
"code": 37, "label": "%", "type": "numeric", "popup": {
|
||||
"main": { "code": 8240, "label": "‰" },
|
||||
"relevant": [
|
||||
{ "code": 8453, "label": "℅" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"code": 53, "label": "5", "type": "numeric", "popup": {
|
||||
"main": { "code": 8309, "label": "⁵" },
|
||||
"relevant": [
|
||||
{ "code": 8538, "label": "⅚" },
|
||||
{ "code": 8541, "label": "⅝" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "$": "shift_state_selector",
|
||||
"shiftedManual": {
|
||||
"code": 94, "label": "^", "type": "numeric", "popup": {
|
||||
"main": { "code": 8593, "label": "↑" },
|
||||
"relevant": [
|
||||
{ "code": 8592, "label": "←" },
|
||||
{ "code": 8595, "label": "↓" },
|
||||
{ "code": 8594, "label": "→" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"code": 54, "label": "6", "type": "numeric", "popup": {
|
||||
"main": { "code": 8310, "label": "⁶" }
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "$": "shift_state_selector",
|
||||
"shiftedManual": { "code": 38, "label": "&", "type": "numeric" },
|
||||
"default": {
|
||||
"code": 55, "label": "7", "type": "numeric", "popup": {
|
||||
"main": { "code": 8311, "label": "⁷" },
|
||||
"relevant": [
|
||||
{ "code": 8542, "label": "⅞" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "$": "shift_state_selector",
|
||||
"shiftedManual": {
|
||||
"code": 42, "label": "*", "type": "numeric", "popup": {
|
||||
"main": { "code": 8224, "label": "†" },
|
||||
"relevant": [
|
||||
{ "code": 9733, "label": "★" },
|
||||
{ "code": 8225, "label": "‡" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"code": 56, "label": "8", "type": "numeric", "popup": {
|
||||
"main": { "code": 8312, "label": "⁸" }
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "$": "shift_state_selector",
|
||||
"shiftedManual": {
|
||||
"$": "layout_direction_selector",
|
||||
"ltr": {
|
||||
"code": 40, "label": "(", "type": "numeric", "popup": {
|
||||
"main": { "code": 60, "label": "<" },
|
||||
"relevant": [
|
||||
{ "code": 91, "label": "[" },
|
||||
{ "code": 123, "label": "{" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"rtl": {
|
||||
"code": 41, "label": "(", "type": "numeric", "popup": {
|
||||
"main": { "code": 62, "label": "<" },
|
||||
"relevant": [
|
||||
{ "code": 93, "label": "[" },
|
||||
{ "code": 125, "label": "{" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"code": 57, "label": "9", "type": "numeric", "popup": {
|
||||
"main": { "code": 8313, "label": "⁹" }
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "$": "shift_state_selector",
|
||||
"shiftedManual": {
|
||||
"$": "layout_direction_selector",
|
||||
"ltr": {
|
||||
"code": 41, "label": ")", "type": "numeric", "popup": {
|
||||
"main": { "code": 62, "label": ">" },
|
||||
"relevant": [
|
||||
{ "code": 93, "label": "]" },
|
||||
{ "code": 125, "label": "}" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"rtl": {
|
||||
"code": 40, "label": ")", "type": "numeric", "popup": {
|
||||
"main": { "code": 60, "label": ">" },
|
||||
"relevant": [
|
||||
{ "code": 91, "label": "]" },
|
||||
{ "code": 123, "label": "}" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"code": 48, "label": "0", "type": "numeric", "popup": {
|
||||
"main": { "code": 8304, "label": "⁰" },
|
||||
"relevant": [
|
||||
{ "code": 8709, "label": "∅" },
|
||||
{ "code": 8319, "label": "ⁿ" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -17,8 +17,11 @@
|
||||
{
|
||||
"id": "default",
|
||||
"label": "Default",
|
||||
"symbolsPrecedingSpace": ".*[.,;:!?‽&%)\\]}»©®™\\p{L}0-9]",
|
||||
"symbolsFollowingSpace": "[\\p{L}0-9].*"
|
||||
"symbolsPrecedingAutoSpace": ".,?‽!\"&%)]}»",
|
||||
"symbolsFollowingAutoSpace": "",
|
||||
"symbolsPrecedingPhantomSpace": ".,;:?‽!&%)]}»©®™",
|
||||
"symbolsFollowingPhantomSpace": "¿⸘¡([{",
|
||||
"symbolsTerminatingSentence": ".?‽!"
|
||||
}
|
||||
],
|
||||
"popupMappings": [
|
||||
|
||||
@@ -2,41 +2,74 @@
|
||||
"all": {
|
||||
"α": {
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 940, "label": "ά" }
|
||||
{ "$": "case_selector",
|
||||
"lower": { "code": 940, "label": "ά" },
|
||||
"upper": { "code": 902, "label": "Ά" }
|
||||
}
|
||||
]
|
||||
},
|
||||
"ε": {
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 941, "label": "έ" }
|
||||
{ "$": "case_selector",
|
||||
"lower": { "code": 941, "label": "έ" },
|
||||
"upper": { "code": 904, "label": "Έ" }
|
||||
}
|
||||
]
|
||||
},
|
||||
"η": {
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 942, "label": "ή" }
|
||||
{ "$": "case_selector",
|
||||
"lower": { "code": 942, "label": "ή" },
|
||||
"upper": { "code": 905, "label": "Ή" }
|
||||
}
|
||||
]
|
||||
},
|
||||
"ι": {
|
||||
"main": { "$": "auto_text_key", "code": 943, "label": "ί" },
|
||||
"main": { "$": "case_selector",
|
||||
"lower": { "code": 943, "label": "ί" },
|
||||
"upper": { "code": 906, "label": "Ί" }
|
||||
},
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 912, "label": "ΐ" },
|
||||
{ "$": "auto_text_key", "code": 970, "label": "ϊ" }
|
||||
{ "$": "case_selector",
|
||||
"lower": { "code": 912, "label": "ΐ" },
|
||||
"upper": { "$": "multi_text_key", "codePoints": [921, 776, 769], "label": "Ϊ́" }
|
||||
},
|
||||
{ "$": "case_selector",
|
||||
"lower": { "code": 970, "label": "ϊ" },
|
||||
"upper": { "code": 938, "label": "Ϊ" }
|
||||
}
|
||||
]
|
||||
},
|
||||
"ο": {
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 972, "label": "ό" }
|
||||
{ "$": "case_selector",
|
||||
"lower": { "code": 972, "label": "ό" },
|
||||
"upper": { "code": 908, "label": "Ό" }
|
||||
}
|
||||
]
|
||||
},
|
||||
"υ": {
|
||||
"main": { "$": "auto_text_key", "code": 973, "label": "ύ" },
|
||||
"main": { "$": "case_selector",
|
||||
"lower": { "code": 973, "label": "ύ" },
|
||||
"upper": { "code": 910, "label": "Ύ" }
|
||||
},
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 944, "label": "ΰ" },
|
||||
{ "$": "auto_text_key", "code": 971, "label": "ϋ" }
|
||||
{ "$": "case_selector",
|
||||
"lower": { "code": 944, "label": "ΰ" },
|
||||
"upper": { "$": "multi_text_key", "codePoints": [933, 776, 769], "label": "Ϋ́" }
|
||||
},
|
||||
{ "$": "case_selector",
|
||||
"lower": { "code": 971, "label": "ϋ" },
|
||||
"upper": { "code": 939, "label": "Ϋ" }
|
||||
}
|
||||
]
|
||||
},
|
||||
"ω": {
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 974, "label": "ώ" }
|
||||
{ "$": "case_selector",
|
||||
"lower": { "code": 974, "label": "ώ" },
|
||||
"upper": { "code": 911, "label": "Ώ" }
|
||||
}
|
||||
]
|
||||
},
|
||||
"~right": {
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
},
|
||||
"z": {
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 378, "label": "ź" },
|
||||
{ "$": "auto_text_key", "code": 380, "label": "ż" }
|
||||
{ "$": "auto_text_key", "code": 380, "label": "ż" },
|
||||
{ "$": "auto_text_key", "code": 378, "label": "ź" }
|
||||
]
|
||||
},
|
||||
"~right": {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"all": {
|
||||
"a": {
|
||||
"main": { "$": "auto_text_key", "code": 226, "label": "â" }
|
||||
},
|
||||
"c": {
|
||||
"main": { "$": "auto_text_key", "code": 231, "label": "ç" }
|
||||
},
|
||||
@@ -10,13 +13,19 @@
|
||||
"main": { "$": "case_selector",
|
||||
"lower": { "code": 305, "label": "ı" },
|
||||
"upper": { "code": 73, "label": "I" }
|
||||
}
|
||||
},
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 238, "label": "î" }
|
||||
]
|
||||
},
|
||||
"ı": {
|
||||
"main": { "$": "case_selector",
|
||||
"lower": { "code": 105, "label": "i" },
|
||||
"upper": { "code": 304, "label": "İ" }
|
||||
}
|
||||
},
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 238, "label": "î" }
|
||||
]
|
||||
},
|
||||
"o": {
|
||||
"main": { "$": "auto_text_key", "code": 246, "label": "ö" }
|
||||
@@ -25,7 +34,10 @@
|
||||
"main": { "$": "auto_text_key", "code": 351, "label": "ş" }
|
||||
},
|
||||
"u": {
|
||||
"main": { "$": "auto_text_key", "code": 252, "label": "ü" }
|
||||
"main": { "$": "auto_text_key", "code": 252, "label": "ü" },
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 251, "label": "û" }
|
||||
]
|
||||
},
|
||||
"~right": {
|
||||
"main": { "code": 44, "label": "," },
|
||||
|
||||
@@ -64,36 +64,65 @@
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
|
||||
"smartbar-primary-actions-toggle": {
|
||||
"smartbar-shared-actions-toggle": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"shape": "circle()",
|
||||
"shadow-elevation": "2dp"
|
||||
},
|
||||
"smartbar-secondary-actions-toggle": {
|
||||
"smartbar-extended-actions-toggle": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-surface-variant)",
|
||||
"shape": "circle()"
|
||||
},
|
||||
"smartbar-quick-action": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"shape": "circle()"
|
||||
},
|
||||
"smartbar-key": {
|
||||
"smartbar-action-key": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "18sp",
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"smartbar-key:pressed": {
|
||||
"smartbar-action-key:pressed": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
"smartbar-key:disabled": {
|
||||
"smartbar-action-key:disabled": {
|
||||
"background": "transparent",
|
||||
"foreground": "#12121248"
|
||||
},
|
||||
"smartbar-action-tile": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "18sp",
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"smartbar-action-tile:pressed": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
"smartbar-action-tile:disabled": {
|
||||
"background": "transparent",
|
||||
"foreground": "#12121248"
|
||||
},
|
||||
"smartbar-actions-overflow-customize-button": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "14sp",
|
||||
"shape": "circle()",
|
||||
"shadow-elevation": "2dp"
|
||||
},
|
||||
"smartbar-actions-editor": {
|
||||
"background": "var(--background)",
|
||||
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
|
||||
},
|
||||
"smartbar-actions-editor-header": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "16sp"
|
||||
},
|
||||
"smartbar-actions-editor-subheader": {
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "16sp"
|
||||
},
|
||||
"smartbar-candidate-word": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
@@ -183,6 +212,10 @@
|
||||
"foreground": "var(--primary)"
|
||||
},
|
||||
|
||||
"incognito-mode-indicator": {
|
||||
"foreground": "#00000011"
|
||||
},
|
||||
|
||||
"one-handed-panel": {
|
||||
"background": "#e8f5e9",
|
||||
"foreground": "#424242"
|
||||
|
||||
@@ -63,35 +63,65 @@
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
|
||||
"smartbar-primary-actions-toggle": {
|
||||
"smartbar-shared-actions-toggle": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"shape": "circle()"
|
||||
"shape": "circle()",
|
||||
"shadow-elevation": "2dp"
|
||||
},
|
||||
"smartbar-secondary-actions-toggle": {
|
||||
"smartbar-extended-actions-toggle": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-surface-variant)",
|
||||
"shape": "circle()"
|
||||
},
|
||||
"smartbar-quick-action": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"shape": "circle()"
|
||||
},
|
||||
"smartbar-key": {
|
||||
"smartbar-action-key": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "18sp",
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"smartbar-key:pressed": {
|
||||
"smartbar-action-key:pressed": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
"smartbar-key:disabled": {
|
||||
"smartbar-action-key:disabled": {
|
||||
"background": "transparent",
|
||||
"foreground": "#12121248"
|
||||
},
|
||||
"smartbar-action-tile": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "18sp",
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"smartbar-action-tile:pressed": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
"smartbar-action-tile:disabled": {
|
||||
"background": "transparent",
|
||||
"foreground": "#12121248"
|
||||
},
|
||||
"smartbar-actions-overflow-customize-button": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "14sp",
|
||||
"shape": "circle()",
|
||||
"shadow-elevation": "2dp"
|
||||
},
|
||||
"smartbar-actions-editor": {
|
||||
"background": "var(--background)",
|
||||
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
|
||||
},
|
||||
"smartbar-actions-editor-header": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "16sp"
|
||||
},
|
||||
"smartbar-actions-editor-subheader": {
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "16sp"
|
||||
},
|
||||
"smartbar-candidate-word": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
@@ -181,6 +211,10 @@
|
||||
"foreground": "var(--primary)"
|
||||
},
|
||||
|
||||
"incognito-mode-indicator": {
|
||||
"foreground": "#00000011"
|
||||
},
|
||||
|
||||
"one-handed-panel": {
|
||||
"background": "#e8f5e9",
|
||||
"foreground": "#424242"
|
||||
|
||||
@@ -64,36 +64,65 @@
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
|
||||
"smartbar-primary-actions-toggle": {
|
||||
"smartbar-shared-actions-toggle": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"shape": "circle()",
|
||||
"shadow-elevation": "2dp"
|
||||
},
|
||||
"smartbar-secondary-actions-toggle": {
|
||||
"smartbar-extended-actions-toggle": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-surface-variant)",
|
||||
"shape": "circle()"
|
||||
},
|
||||
"smartbar-quick-action": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"shape": "circle()"
|
||||
},
|
||||
"smartbar-key": {
|
||||
"smartbar-action-key": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "18sp",
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"smartbar-key:pressed": {
|
||||
"smartbar-action-key:pressed": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
"smartbar-key:disabled": {
|
||||
"smartbar-action-key:disabled": {
|
||||
"background": "transparent",
|
||||
"foreground": "#dcdcdc48"
|
||||
},
|
||||
"smartbar-action-tile": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "18sp",
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"smartbar-action-tile:pressed": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
"smartbar-action-tile:disabled": {
|
||||
"background": "transparent",
|
||||
"foreground": "#dcdcdc48"
|
||||
},
|
||||
"smartbar-actions-overflow-customize-button": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "14sp",
|
||||
"shape": "circle()",
|
||||
"shadow-elevation": "2dp"
|
||||
},
|
||||
"smartbar-actions-editor": {
|
||||
"background": "var(--background)",
|
||||
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
|
||||
},
|
||||
"smartbar-actions-editor-header": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "16sp"
|
||||
},
|
||||
"smartbar-actions-editor-subheader": {
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "16sp"
|
||||
},
|
||||
"smartbar-candidate-word": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
@@ -183,6 +212,10 @@
|
||||
"foreground": "var(--primary)"
|
||||
},
|
||||
|
||||
"incognito-mode-indicator": {
|
||||
"foreground": "#ffffff11"
|
||||
},
|
||||
|
||||
"one-handed-panel": {
|
||||
"background": "#1b5e20",
|
||||
"foreground": "#eeeeee"
|
||||
|
||||
@@ -63,35 +63,65 @@
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
|
||||
"smartbar-primary-actions-toggle": {
|
||||
"smartbar-shared-actions-toggle": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"shape": "circle()"
|
||||
"shape": "circle()",
|
||||
"shadow-elevation": "2dp"
|
||||
},
|
||||
"smartbar-secondary-actions-toggle": {
|
||||
"smartbar-extended-actions-toggle": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-surface-variant)",
|
||||
"shape": "circle()"
|
||||
},
|
||||
"smartbar-quick-action": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"shape": "circle()"
|
||||
},
|
||||
"smartbar-key": {
|
||||
"smartbar-action-key": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "18sp",
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"smartbar-key:pressed": {
|
||||
"smartbar-action-key:pressed": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
"smartbar-key:disabled": {
|
||||
"smartbar-action-key:disabled": {
|
||||
"background": "transparent",
|
||||
"foreground": "#dcdcdc48"
|
||||
},
|
||||
"smartbar-action-tile": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "18sp",
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"smartbar-action-tile:pressed": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
"smartbar-action-tile:disabled": {
|
||||
"background": "transparent",
|
||||
"foreground": "#dcdcdc48"
|
||||
},
|
||||
"smartbar-actions-overflow-customize-button": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "14sp",
|
||||
"shape": "circle()",
|
||||
"shadow-elevation": "2dp"
|
||||
},
|
||||
"smartbar-actions-editor": {
|
||||
"background": "var(--background)",
|
||||
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
|
||||
},
|
||||
"smartbar-actions-editor-header": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "16sp"
|
||||
},
|
||||
"smartbar-actions-editor-subheader": {
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "16sp"
|
||||
},
|
||||
"smartbar-candidate-word": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
@@ -181,6 +211,10 @@
|
||||
"foreground": "var(--primary)"
|
||||
},
|
||||
|
||||
"incognito-mode-indicator": {
|
||||
"foreground": "#ffffff11"
|
||||
},
|
||||
|
||||
"one-handed-panel": {
|
||||
"background": "#1b5e20",
|
||||
"foreground": "#eeeeee"
|
||||
|
||||
@@ -64,36 +64,65 @@
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
|
||||
"smartbar-primary-actions-toggle": {
|
||||
"smartbar-shared-actions-toggle": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"shape": "circle()",
|
||||
"shadow-elevation": "2dp"
|
||||
},
|
||||
"smartbar-secondary-actions-toggle": {
|
||||
"smartbar-extended-actions-toggle": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-surface-variant)",
|
||||
"shape": "circle()"
|
||||
},
|
||||
"smartbar-quick-action": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"shape": "circle()"
|
||||
},
|
||||
"smartbar-key": {
|
||||
"smartbar-action-key": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "18sp",
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"smartbar-key:pressed": {
|
||||
"smartbar-action-key:pressed": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
"smartbar-key:disabled": {
|
||||
"smartbar-action-key:disabled": {
|
||||
"background": "transparent",
|
||||
"foreground": "#dcdcdc48"
|
||||
},
|
||||
"smartbar-action-tile": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "18sp",
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"smartbar-action-tile:pressed": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
"smartbar-action-tile:disabled": {
|
||||
"background": "transparent",
|
||||
"foreground": "#dcdcdc48"
|
||||
},
|
||||
"smartbar-actions-overflow-customize-button": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "14sp",
|
||||
"shape": "circle()",
|
||||
"shadow-elevation": "2dp"
|
||||
},
|
||||
"smartbar-actions-editor": {
|
||||
"background": "var(--background)",
|
||||
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
|
||||
},
|
||||
"smartbar-actions-editor-header": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "16sp"
|
||||
},
|
||||
"smartbar-actions-editor-subheader": {
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "16sp"
|
||||
},
|
||||
"smartbar-candidate-word": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
@@ -183,6 +212,10 @@
|
||||
"foreground": "var(--primary-variant)"
|
||||
},
|
||||
|
||||
"incognito-mode-indicator": {
|
||||
"foreground": "#ffffff11"
|
||||
},
|
||||
|
||||
"one-handed-panel": {
|
||||
"background": "#000000",
|
||||
"foreground": "#eeeeee"
|
||||
|
||||
@@ -63,36 +63,65 @@
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
|
||||
"smartbar-primary-actions-toggle": {
|
||||
"smartbar-shared-actions-toggle": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"shape": "circle()",
|
||||
"shadow-elevation": "2dp"
|
||||
},
|
||||
"smartbar-secondary-actions-toggle": {
|
||||
"smartbar-extended-actions-toggle": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-surface-variant)",
|
||||
"shape": "circle()"
|
||||
},
|
||||
"smartbar-quick-action": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"shape": "circle()"
|
||||
},
|
||||
"smartbar-key": {
|
||||
"smartbar-action-key": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "18sp",
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"smartbar-key:pressed": {
|
||||
"smartbar-action-key:pressed": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
"smartbar-key:disabled": {
|
||||
"smartbar-action-key:disabled": {
|
||||
"background": "transparent",
|
||||
"foreground": "#dcdcdc48"
|
||||
},
|
||||
"smartbar-action-tile": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "18sp",
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"smartbar-action-tile:pressed": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
"smartbar-action-tile:disabled": {
|
||||
"background": "transparent",
|
||||
"foreground": "#dcdcdc48"
|
||||
},
|
||||
"smartbar-actions-overflow-customize-button": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "14sp",
|
||||
"shape": "circle()",
|
||||
"shadow-elevation": "2dp"
|
||||
},
|
||||
"smartbar-actions-editor": {
|
||||
"background": "var(--background)",
|
||||
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
|
||||
},
|
||||
"smartbar-actions-editor-header": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "16sp"
|
||||
},
|
||||
"smartbar-actions-editor-subheader": {
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "16sp"
|
||||
},
|
||||
"smartbar-candidate-word": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
@@ -182,6 +211,10 @@
|
||||
"foreground": "var(--primary-variant)"
|
||||
},
|
||||
|
||||
"incognito-mode-indicator": {
|
||||
"foreground": "#ffffff11"
|
||||
},
|
||||
|
||||
"one-handed-panel": {
|
||||
"background": "#000000",
|
||||
"foreground": "#eeeeee"
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"uniqueId": "pictogrammers-material-icons",
|
||||
"name": "Pictogrammers Material Icons",
|
||||
"developers": [
|
||||
{
|
||||
"name": "Pictogrammers Icon Group",
|
||||
"organisationUrl": "https://pictogrammers.com"
|
||||
}
|
||||
],
|
||||
"website": "https://github.com/Templarian/MaterialDesign",
|
||||
"licenses": [
|
||||
"Apache-2.0"
|
||||
]
|
||||
}
|
||||
@@ -18,17 +18,12 @@ add_library(ICU::uc STATIC IMPORTED)
|
||||
set_property(TARGET ICU::uc PROPERTY IMPORTED_LOCATION "${JNI_LIBS}/libicuuc.a")
|
||||
|
||||
### FlorisBoard ###
|
||||
add_subdirectory(nuspell)
|
||||
add_subdirectory(utils)
|
||||
add_subdirectory(ime/nlp)
|
||||
add_subdirectory(ime/spelling)
|
||||
|
||||
add_library(
|
||||
florisboard-native
|
||||
SHARED
|
||||
dev_patrickgold_florisboard_FlorisApplication.cpp
|
||||
dev_patrickgold_florisboard_ime_nlp_SuggestionList.cpp
|
||||
dev_patrickgold_florisboard_ime_spelling_SpellingDict.cpp
|
||||
)
|
||||
|
||||
target_compile_options(florisboard-native PRIVATE -ffunction-sections -fdata-sections -fexceptions)
|
||||
@@ -41,8 +36,5 @@ target_link_libraries(
|
||||
log
|
||||
ICU::uc
|
||||
ICU::data
|
||||
Nuspell::nuspell
|
||||
utils
|
||||
ime-nlp
|
||||
ime-spelling
|
||||
)
|
||||
|
||||
@@ -25,9 +25,8 @@
|
||||
extern "C"
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_dev_patrickgold_florisboard_FlorisApplication_00024Companion_nativeInitICUData(
|
||||
JNIEnv *env,
|
||||
jobject thiz,
|
||||
jobject path) {
|
||||
JNIEnv *env, jobject thiz, jobject path)
|
||||
{
|
||||
auto path_str = utils::j2std_string(env, path);
|
||||
std::ifstream in_file(path_str, std::ios::in | std::ios::binary);
|
||||
if (!in_file) {
|
||||
@@ -42,6 +41,7 @@ Java_dev_patrickgold_florisboard_FlorisApplication_00024Companion_nativeInitICUD
|
||||
char *icu_data = new char[size + 1];
|
||||
in_file.read(icu_data, size);
|
||||
if (!in_file) {
|
||||
delete[] icu_data;
|
||||
in_file.close();
|
||||
return U_FILE_ACCESS_ERROR;
|
||||
}
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <jni.h>
|
||||
#include "ime/nlp/suggestion_list.h"
|
||||
|
||||
#pragma ide diagnostic ignored "UnusedLocalVariable"
|
||||
|
||||
using namespace ime::nlp;
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeInitialize(
|
||||
JNIEnv *env,
|
||||
jobject thiz,
|
||||
jint max_size) {
|
||||
auto *suggestionList = new SuggestionList(max_size);
|
||||
return reinterpret_cast<jlong>(suggestionList);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeDispose(
|
||||
JNIEnv *env,
|
||||
jobject thiz,
|
||||
jlong native_ptr) {
|
||||
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
|
||||
suggestionList->clear();
|
||||
delete suggestionList;
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeAdd(
|
||||
JNIEnv *env,
|
||||
jobject thiz,
|
||||
jlong native_ptr,
|
||||
jstring word,
|
||||
jint freq) {
|
||||
const char *cWord = env->GetStringUTFChars(word, nullptr);
|
||||
word_t stdWord = word_t(cWord);
|
||||
env->ReleaseStringUTFChars(word, cWord);
|
||||
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
|
||||
return suggestionList->add(std::move(stdWord), freq);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeClear(
|
||||
JNIEnv *env,
|
||||
jobject thiz,
|
||||
jlong native_ptr) {
|
||||
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
|
||||
suggestionList->clear();
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeContains(
|
||||
JNIEnv *env,
|
||||
jobject thiz,
|
||||
jlong native_ptr,
|
||||
jstring element) {
|
||||
const char *cWord = env->GetStringUTFChars(element, nullptr);
|
||||
const word_t stdWord = word_t(cWord);
|
||||
env->ReleaseStringUTFChars(element, cWord);
|
||||
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
|
||||
return suggestionList->containsWord(stdWord);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeGetOrNull(
|
||||
JNIEnv *env,
|
||||
jobject thiz,
|
||||
jlong native_ptr,
|
||||
jint index) {
|
||||
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
|
||||
auto weightedToken = suggestionList->get(index);
|
||||
if (weightedToken == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return env->NewStringUTF(weightedToken->data.c_str());
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeSize(
|
||||
JNIEnv *env,
|
||||
jobject thiz,
|
||||
jlong native_ptr) {
|
||||
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
|
||||
return suggestionList->size();
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeGetIsPrimaryTokenAutoInsert(
|
||||
JNIEnv *env, jobject thiz, jlong native_ptr) {
|
||||
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
|
||||
return suggestionList->isPrimaryTokenAutoInsert;
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_dev_patrickgold_florisboard_ime_nlp_SuggestionList_00024Companion_nativeSetIsPrimaryTokenAutoInsert(
|
||||
JNIEnv *env, jobject thiz, jlong native_ptr, jboolean v) {
|
||||
auto *suggestionList = reinterpret_cast<SuggestionList *>(native_ptr);
|
||||
suggestionList->isPrimaryTokenAutoInsert = v;
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <jni.h>
|
||||
#include <algorithm>
|
||||
#include "ime/spelling/spellingdict.h"
|
||||
#include "utils/jni_utils.h"
|
||||
|
||||
#pragma ide diagnostic ignored "UnusedLocalVariable"
|
||||
|
||||
using namespace ime::spellcheck;
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_dev_patrickgold_florisboard_ime_spelling_SpellingDict_00024Companion_nativeInitialize(
|
||||
JNIEnv *env,
|
||||
jobject thiz,
|
||||
jobject base_path) {
|
||||
auto strBasePath = utils::j2std_string(env, base_path);
|
||||
|
||||
auto *spellingDict = SpellingDict::load(strBasePath);
|
||||
|
||||
if (spellingDict == nullptr) {
|
||||
return 0L;
|
||||
} else {
|
||||
return reinterpret_cast<jlong>(spellingDict);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_dev_patrickgold_florisboard_ime_spelling_SpellingDict_00024Companion_nativeDispose(
|
||||
JNIEnv *env,
|
||||
jobject thiz,
|
||||
jlong native_ptr) {
|
||||
auto spellingDict = reinterpret_cast<SpellingDict *>(native_ptr);
|
||||
|
||||
delete spellingDict;
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_dev_patrickgold_florisboard_ime_spelling_SpellingDict_00024Companion_nativeSpell(
|
||||
JNIEnv *env,
|
||||
jobject thiz,
|
||||
jlong native_ptr,
|
||||
jobject word) {
|
||||
auto strWord = utils::j2std_string(env, word);
|
||||
|
||||
auto spellingDict = reinterpret_cast<SpellingDict *>(native_ptr);
|
||||
auto result = spellingDict->spell(strWord);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jobjectArray JNICALL
|
||||
Java_dev_patrickgold_florisboard_ime_spelling_SpellingDict_00024Companion_nativeSuggest(
|
||||
JNIEnv *env,
|
||||
jobject thiz,
|
||||
jlong native_ptr,
|
||||
jobject word,
|
||||
jint limit) {
|
||||
auto strWord = utils::j2std_string(env, word);
|
||||
|
||||
auto spellingDict = reinterpret_cast<SpellingDict *>(native_ptr);
|
||||
auto result = spellingDict->suggest(strWord);
|
||||
auto retSize = std::min(result.size(), (size_t)std::max(0, limit));
|
||||
|
||||
jclass jByteArrayClass = env->FindClass("java/nio/ByteBuffer");
|
||||
jobjectArray jSuggestions = env->NewObjectArray(retSize, jByteArrayClass, nullptr);
|
||||
for (int n = 0; n < retSize; n++) {
|
||||
env->SetObjectArrayElement(jSuggestions, n, utils::std2j_string(env, result[n]));
|
||||
}
|
||||
|
||||
return jSuggestions;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
add_library(
|
||||
# Name
|
||||
ime-nlp
|
||||
|
||||
# Headers
|
||||
nlp.h
|
||||
token.h
|
||||
suggestion_list.h
|
||||
|
||||
# Sources
|
||||
token.cpp
|
||||
suggestion_list.cpp
|
||||
)
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef FLORISBOARD_NLP_H
|
||||
#define FLORISBOARD_NLP_H
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ime::nlp {
|
||||
|
||||
typedef std::string word_t;
|
||||
typedef uint16_t freq_t;
|
||||
|
||||
static const freq_t FREQ_VALUE_MASK = 0xFF;
|
||||
static const freq_t FREQ_POSSIBLY_OFFENSIVE = 0x01;
|
||||
|
||||
} // namespace ime::nlp
|
||||
|
||||
#endif // FLORISBOARD_NLP_H
|
||||
@@ -1,98 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "suggestion_list.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
using namespace ime::nlp;
|
||||
|
||||
SuggestionList::SuggestionList(size_t _maxSize) :
|
||||
maxSize(_maxSize), internalSize(0), tokens(_maxSize), isPrimaryTokenAutoInsert(false)
|
||||
{ }
|
||||
|
||||
SuggestionList::~SuggestionList() = default;
|
||||
|
||||
bool SuggestionList::add(word_t &&word, freq_t &&freq) {
|
||||
auto entryIndex = indexOfWord(word);
|
||||
if (entryIndex.has_value()) {
|
||||
// Word exists already
|
||||
auto entry = tokens[entryIndex.value()];
|
||||
if (entry.freq < freq) {
|
||||
// Need to update freq
|
||||
entry.freq = freq;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (internalSize < maxSize) {
|
||||
tokens[internalSize++] = WeightedToken(std::move(word), freq);
|
||||
} else {
|
||||
auto last = tokens[internalSize - 1];
|
||||
if (last.freq < freq) {
|
||||
tokens[internalSize - 1] = WeightedToken(std::move(word), freq);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::sort(tokens.begin(), tokens.begin() + internalSize, std::greater<>());
|
||||
return true;
|
||||
}
|
||||
|
||||
void SuggestionList::clear() {
|
||||
internalSize = 0;
|
||||
isPrimaryTokenAutoInsert = false;
|
||||
}
|
||||
|
||||
bool SuggestionList::contains(const WeightedToken &element) const {
|
||||
return indexOf(element).has_value();
|
||||
}
|
||||
|
||||
bool SuggestionList::containsWord(const word_t &word) const {
|
||||
return indexOfWord(word).has_value();
|
||||
}
|
||||
|
||||
const WeightedToken *SuggestionList::get(size_t index) const {
|
||||
if (index < 0 || index >= internalSize) return nullptr;
|
||||
return &tokens[index];
|
||||
}
|
||||
|
||||
std::optional<size_t> SuggestionList::indexOf(const WeightedToken &element) const {
|
||||
for (size_t n = 0; n < internalSize; n++) {
|
||||
if (element == tokens[n]) {
|
||||
return n;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<size_t> SuggestionList::indexOfWord(const word_t &word) const {
|
||||
for (size_t n = 0; n < internalSize; n++) {
|
||||
if (word == tokens[n].data) {
|
||||
return n;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool SuggestionList::isEmpty() const {
|
||||
return internalSize == 0;
|
||||
}
|
||||
|
||||
size_t SuggestionList::size() const {
|
||||
return internalSize;
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef FLORISBOARD_SUGGESTION_LIST_H
|
||||
#define FLORISBOARD_SUGGESTION_LIST_H
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
#include "token.h"
|
||||
|
||||
namespace ime::nlp {
|
||||
|
||||
class SuggestionList {
|
||||
public:
|
||||
SuggestionList(size_t _maxSize);
|
||||
~SuggestionList();
|
||||
|
||||
bool add(word_t &&word, freq_t &&freq);
|
||||
void clear();
|
||||
bool contains(const WeightedToken &element) const;
|
||||
bool containsWord(const word_t &word) const;
|
||||
const WeightedToken *get(size_t index) const;
|
||||
std::optional<size_t> indexOf(const WeightedToken &element) const;
|
||||
std::optional<size_t> indexOfWord(const word_t &word) const;
|
||||
bool isEmpty() const;
|
||||
size_t size() const;
|
||||
|
||||
bool isPrimaryTokenAutoInsert;
|
||||
|
||||
private:
|
||||
std::vector<WeightedToken> tokens;
|
||||
size_t internalSize;
|
||||
size_t maxSize;
|
||||
};
|
||||
|
||||
} // namespace ime::nlp
|
||||
|
||||
#endif // FLORISBOARD_SUGGESTION_LIST_H
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "token.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace ime::nlp {
|
||||
|
||||
Token::Token() : data() {}
|
||||
Token::Token(word_t &&_data) : data(std::move(_data)) {}
|
||||
|
||||
bool operator==(const Token &t1, const Token &t2) {
|
||||
return t1.data == t2.data;
|
||||
}
|
||||
|
||||
bool operator!=(const Token &t1, const Token &t2) {
|
||||
return !(t1 == t2);
|
||||
}
|
||||
|
||||
WeightedToken::WeightedToken() : Token(), freq(0) {}
|
||||
WeightedToken::WeightedToken(word_t &&_data, freq_t _freq) : Token(std::move(_data)), freq(_freq) {}
|
||||
|
||||
bool operator==(const WeightedToken &t1, const WeightedToken &t2) {
|
||||
return t1.data == t2.data && t1.freq == t2.freq;
|
||||
}
|
||||
|
||||
bool operator!=(const WeightedToken &t1, const WeightedToken &t2) {
|
||||
return !(t1 == t2);
|
||||
}
|
||||
|
||||
bool operator<(const WeightedToken &t1, const WeightedToken &t2) {
|
||||
return t1.freq < t2.freq;
|
||||
}
|
||||
|
||||
bool operator<=(const WeightedToken &t1, const WeightedToken &t2) {
|
||||
return t1.freq <= t2.freq;
|
||||
}
|
||||
|
||||
bool operator>(const WeightedToken &t1, const WeightedToken &t2) {
|
||||
return t1.freq > t2.freq;
|
||||
}
|
||||
|
||||
bool operator>=(const WeightedToken &t1, const WeightedToken &t2) {
|
||||
return t1.freq >= t2.freq;
|
||||
}
|
||||
|
||||
} // namespace ime::nlp
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef FLORISBOARD_TOKEN_H
|
||||
#define FLORISBOARD_TOKEN_H
|
||||
|
||||
#include "nlp.h"
|
||||
#include <string>
|
||||
|
||||
namespace ime::nlp {
|
||||
|
||||
class Token {
|
||||
public:
|
||||
word_t data;
|
||||
Token();
|
||||
Token(word_t &&_data);
|
||||
|
||||
friend bool operator==(const Token &t1, const Token &t2);
|
||||
friend bool operator!=(const Token &t1, const Token &t2);
|
||||
};
|
||||
|
||||
class WeightedToken : public Token {
|
||||
public:
|
||||
freq_t freq;
|
||||
WeightedToken();
|
||||
WeightedToken(word_t &&_data, freq_t _freq);
|
||||
|
||||
friend bool operator==(const WeightedToken &t1, const WeightedToken &t2);
|
||||
friend bool operator!=(const WeightedToken &t1, const WeightedToken &t2);
|
||||
friend bool operator<(const WeightedToken &t1, const WeightedToken &t2);
|
||||
friend bool operator<=(const WeightedToken &t1, const WeightedToken &t2);
|
||||
friend bool operator>(const WeightedToken &t1, const WeightedToken &t2);
|
||||
friend bool operator>=(const WeightedToken &t1, const WeightedToken &t2);
|
||||
};
|
||||
|
||||
} // namespace ime::nlp
|
||||
|
||||
#endif // FLORISBOARD_TOKEN_H
|
||||
@@ -1,10 +0,0 @@
|
||||
add_library(
|
||||
# Name
|
||||
ime-spelling
|
||||
|
||||
# Headers
|
||||
spellingdict.h
|
||||
|
||||
# Sources
|
||||
spellingdict.cpp
|
||||
)
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "spellingdict.h"
|
||||
#include "utils/log.h"
|
||||
|
||||
using namespace ime::spellcheck;
|
||||
|
||||
SpellingDict::SpellingDict(const nuspell::Dictionary& dict) : dictionary(std::make_unique<nuspell::Dictionary>(dict))
|
||||
{ }
|
||||
|
||||
SpellingDict::~SpellingDict() = default;
|
||||
|
||||
SpellingDict* SpellingDict::load(const std::string &basePath) {
|
||||
utils::start_stdout_stderr_logger("spell-floris");
|
||||
try {
|
||||
auto temp = nuspell::Dictionary::load_from_path(basePath);
|
||||
auto spellingDict = new SpellingDict(temp);
|
||||
return spellingDict;
|
||||
} catch (const nuspell::Dictionary_Loading_Error& e) {
|
||||
utils::log_error("SpellingDict.load()", e.what());
|
||||
return nullptr;
|
||||
} catch (...) {
|
||||
utils::log_error("SpellingDict.load()", "An unknown error occurred!");
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool SpellingDict::spell(const std::string& word) {
|
||||
bool result = dictionary->spell(word);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::string> SpellingDict::suggest(const std::string &word) {
|
||||
auto result = std::vector<std::string>();
|
||||
dictionary->suggest(word, result);
|
||||
return result;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef FLORISBOARD_SPELLINGDICT_H
|
||||
#define FLORISBOARD_SPELLINGDICT_H
|
||||
|
||||
#include "nuspell/dictionary.hxx"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ime::spellcheck {
|
||||
|
||||
class SpellingDict {
|
||||
public:
|
||||
SpellingDict(const nuspell::Dictionary& dict);
|
||||
~SpellingDict();
|
||||
|
||||
static SpellingDict* load(const std::string& basePath);
|
||||
|
||||
bool spell(const std::string& word);
|
||||
std::vector<std::string> suggest(const std::string& word);
|
||||
|
||||
private:
|
||||
std::unique_ptr<nuspell::Dictionary> dictionary;
|
||||
};
|
||||
|
||||
} // namespace ime::spellcheck
|
||||
|
||||
#endif // FLORISBOARD_SPELLINGDICT_H
|
||||
@@ -1,10 +0,0 @@
|
||||
add_library(nuspell
|
||||
aff_data.cxx aff_data.hxx
|
||||
checker.cxx checker.hxx
|
||||
suggester.cxx suggester.hxx
|
||||
dictionary.cxx dictionary.hxx
|
||||
unicode.hxx
|
||||
utils.cxx utils.hxx
|
||||
structures.hxx)
|
||||
|
||||
add_library(Nuspell::nuspell ALIAS nuspell)
|
||||
@@ -1,165 +0,0 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,173 +0,0 @@
|
||||
/* Copyright 2016-2021 Dimitrij Mijoski
|
||||
*
|
||||
* This file is part of Nuspell.
|
||||
*
|
||||
* Nuspell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Nuspell is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Nuspell. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef NUSPELL_AFF_DATA_HXX
|
||||
#define NUSPELL_AFF_DATA_HXX
|
||||
|
||||
#include "nuspell_export.h"
|
||||
#include "structures.hxx"
|
||||
|
||||
#include <iosfwd>
|
||||
#include <unicode/locid.h>
|
||||
|
||||
namespace nuspell {
|
||||
inline namespace v5 {
|
||||
|
||||
class Encoding {
|
||||
std::string name;
|
||||
|
||||
NUSPELL_EXPORT auto normalize_name() -> void;
|
||||
|
||||
public:
|
||||
enum Enc_Type { SINGLEBYTE = false, UTF8 = true };
|
||||
|
||||
Encoding() = default;
|
||||
explicit Encoding(const std::string& e) : name(e) { normalize_name(); }
|
||||
explicit Encoding(std::string&& e) : name(move(e)) { normalize_name(); }
|
||||
explicit Encoding(const char* e) : name(e) { normalize_name(); }
|
||||
auto& operator=(const std::string& e)
|
||||
{
|
||||
name = e;
|
||||
normalize_name();
|
||||
return *this;
|
||||
}
|
||||
auto& operator=(std::string&& e)
|
||||
{
|
||||
name = move(e);
|
||||
normalize_name();
|
||||
return *this;
|
||||
}
|
||||
auto& operator=(const char* e)
|
||||
{
|
||||
name = e;
|
||||
normalize_name();
|
||||
return *this;
|
||||
}
|
||||
auto empty() const { return name.empty(); }
|
||||
auto& value() const { return name; }
|
||||
auto is_utf8() const { return name == "UTF-8"; }
|
||||
auto value_or_default() const -> std::string
|
||||
{
|
||||
if (name.empty())
|
||||
return "ISO8859-1";
|
||||
else
|
||||
return name;
|
||||
}
|
||||
operator Enc_Type() const { return is_utf8() ? UTF8 : SINGLEBYTE; }
|
||||
};
|
||||
|
||||
enum class Flag_Type { SINGLE_CHAR, DOUBLE_CHAR, NUMBER, UTF8 };
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @brief Map between words and word_flags.
|
||||
*
|
||||
* Flags are stored as part of the container. Maybe for the future flags should
|
||||
* be stored elsewhere (flag aliases) and this should store pointers.
|
||||
*
|
||||
* Does not store morphological data as is low priority feature and is out of
|
||||
* scope.
|
||||
*/
|
||||
using Word_List = Hash_Multimap<std::string, Flag_Set>;
|
||||
|
||||
struct Aff_Data {
|
||||
static constexpr auto HIDDEN_HOMONYM_FLAG = char16_t(-1);
|
||||
static constexpr auto MAX_SUGGESTIONS = size_t(16);
|
||||
|
||||
// spell checking options
|
||||
Word_List words;
|
||||
Prefix_Table prefixes;
|
||||
Suffix_Table suffixes;
|
||||
|
||||
bool complex_prefixes;
|
||||
bool fullstrip;
|
||||
bool checksharps;
|
||||
bool forbid_warn;
|
||||
char16_t compound_onlyin_flag;
|
||||
char16_t circumfix_flag;
|
||||
char16_t forbiddenword_flag;
|
||||
char16_t keepcase_flag;
|
||||
char16_t need_affix_flag;
|
||||
char16_t warn_flag;
|
||||
|
||||
// compounding options
|
||||
char16_t compound_flag;
|
||||
char16_t compound_begin_flag;
|
||||
char16_t compound_last_flag;
|
||||
char16_t compound_middle_flag;
|
||||
Compound_Rule_Table compound_rules;
|
||||
|
||||
// spell checking options
|
||||
Break_Table break_table;
|
||||
Substr_Replacer input_substr_replacer;
|
||||
std::string ignored_chars;
|
||||
icu::Locale icu_locale;
|
||||
Substr_Replacer output_substr_replacer;
|
||||
|
||||
// suggestion options
|
||||
Replacement_Table replacements;
|
||||
std::vector<Similarity_Group> similarities;
|
||||
std::string keyboard_closeness;
|
||||
std::string try_chars;
|
||||
// Phonetic_Table phonetic_table;
|
||||
|
||||
char16_t nosuggest_flag;
|
||||
char16_t substandard_flag;
|
||||
unsigned short max_compound_suggestions;
|
||||
unsigned short max_ngram_suggestions;
|
||||
unsigned short max_diff_factor;
|
||||
bool only_max_diff;
|
||||
bool no_split_suggestions;
|
||||
bool suggest_with_dots;
|
||||
|
||||
// compounding options
|
||||
unsigned short compound_min_length;
|
||||
unsigned short compound_max_word_count;
|
||||
char16_t compound_permit_flag;
|
||||
char16_t compound_forbid_flag;
|
||||
char16_t compound_root_flag;
|
||||
char16_t compound_force_uppercase;
|
||||
bool compound_more_suffixes;
|
||||
bool compound_check_duplicate;
|
||||
bool compound_check_rep;
|
||||
bool compound_check_case;
|
||||
bool compound_check_triple;
|
||||
bool compound_simplified_triple;
|
||||
bool compound_syllable_num;
|
||||
unsigned short compound_syllable_max;
|
||||
std::string compound_syllable_vowels;
|
||||
std::vector<Compound_Pattern> compound_patterns;
|
||||
|
||||
// data members used only while parsing
|
||||
Flag_Type flag_type;
|
||||
Encoding encoding;
|
||||
std::vector<Flag_Set> flag_aliases;
|
||||
std::string wordchars; // deprecated?
|
||||
|
||||
auto parse_aff(std::istream& in) -> bool;
|
||||
auto parse_dic(std::istream& in) -> bool;
|
||||
auto parse_aff_dic(std::istream& aff, std::istream& dic)
|
||||
{
|
||||
if (parse_aff(aff))
|
||||
return parse_dic(dic);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
} // namespace v5
|
||||
} // namespace nuspell
|
||||
#endif // NUSPELL_AFF_DATA_HXX
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,352 +0,0 @@
|
||||
/* Copyright 2016-2021 Dimitrij Mijoski
|
||||
*
|
||||
* This file is part of Nuspell.
|
||||
*
|
||||
* Nuspell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Nuspell is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Nuspell. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef NUSPELL_CHECKER_HXX
|
||||
#define NUSPELL_CHECKER_HXX
|
||||
|
||||
#include "aff_data.hxx"
|
||||
|
||||
namespace nuspell {
|
||||
inline namespace v5 {
|
||||
|
||||
enum Affixing_Mode {
|
||||
FULL_WORD,
|
||||
AT_COMPOUND_BEGIN,
|
||||
AT_COMPOUND_END,
|
||||
AT_COMPOUND_MIDDLE
|
||||
};
|
||||
|
||||
struct Affixing_Result_Base {
|
||||
Word_List::const_pointer root_word = {};
|
||||
|
||||
operator Word_List::const_pointer() const { return root_word; }
|
||||
auto& operator*() const { return *root_word; }
|
||||
auto operator->() const { return root_word; }
|
||||
};
|
||||
|
||||
template <class T1 = void, class T2 = void>
|
||||
struct Affixing_Result : Affixing_Result_Base {
|
||||
const T1* a = {};
|
||||
const T2* b = {};
|
||||
|
||||
Affixing_Result() = default;
|
||||
Affixing_Result(Word_List::const_reference r, const T1& a, const T2& b)
|
||||
: Affixing_Result_Base{&r}, a{&a}, b{&b}
|
||||
{
|
||||
}
|
||||
};
|
||||
template <class T1>
|
||||
struct Affixing_Result<T1, void> : Affixing_Result_Base {
|
||||
const T1* a = {};
|
||||
|
||||
Affixing_Result() = default;
|
||||
Affixing_Result(Word_List::const_reference r, const T1& a)
|
||||
: Affixing_Result_Base{&r}, a{&a}
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct Affixing_Result<void, void> : Affixing_Result_Base {
|
||||
Affixing_Result() = default;
|
||||
Affixing_Result(Word_List::const_reference r) : Affixing_Result_Base{&r}
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
struct Compounding_Result {
|
||||
Word_List::const_pointer word_entry = {};
|
||||
unsigned char num_words_modifier = {};
|
||||
signed char num_syllable_modifier = {};
|
||||
bool affixed_and_modified = {}; /**< non-zero affix */
|
||||
operator Word_List::const_pointer() const { return word_entry; }
|
||||
auto& operator*() const { return *word_entry; }
|
||||
auto operator->() const { return word_entry; }
|
||||
};
|
||||
|
||||
struct Checker : public Aff_Data {
|
||||
enum Forceucase : bool {
|
||||
FORBID_BAD_FORCEUCASE = false,
|
||||
ALLOW_BAD_FORCEUCASE = true
|
||||
};
|
||||
|
||||
enum Hidden_Homonym : bool {
|
||||
ACCEPT_HIDDEN_HOMONYM = false,
|
||||
SKIP_HIDDEN_HOMONYM = true
|
||||
};
|
||||
Checker()
|
||||
: Aff_Data() // we explicity do value init so content is zeroed
|
||||
{
|
||||
}
|
||||
auto spell_priv(std::string& s) const -> bool;
|
||||
auto spell_break(std::string& s, size_t depth = 0) const -> bool;
|
||||
auto spell_casing(std::string& s) const -> const Flag_Set*;
|
||||
auto spell_casing_upper(std::string& s) const -> const Flag_Set*;
|
||||
auto spell_casing_title(std::string& s) const -> const Flag_Set*;
|
||||
auto spell_sharps(std::string& base, size_t n_pos = 0, size_t n = 0,
|
||||
size_t rep = 0) const -> const Flag_Set*;
|
||||
|
||||
auto check_word(std::string& s, Forceucase allow_bad_forceucase = {},
|
||||
Hidden_Homonym skip_hidden_homonym = {}) const
|
||||
-> const Flag_Set*;
|
||||
auto check_simple_word(std::string& word,
|
||||
Hidden_Homonym skip_hidden_homonym = {}) const
|
||||
-> const Flag_Set*;
|
||||
|
||||
template <Affixing_Mode m>
|
||||
auto affix_NOT_valid(const Prefix& a) const;
|
||||
template <Affixing_Mode m>
|
||||
auto affix_NOT_valid(const Suffix& a) const;
|
||||
template <Affixing_Mode m, class AffixT>
|
||||
auto outer_affix_NOT_valid(const AffixT& a) const;
|
||||
template <class AffixT>
|
||||
auto is_circumfix(const AffixT& a) const;
|
||||
template <Affixing_Mode m>
|
||||
auto is_valid_inside_compound(const Flag_Set& flags) const;
|
||||
|
||||
template <Affixing_Mode m = FULL_WORD>
|
||||
auto strip_prefix_only(std::string& s,
|
||||
Hidden_Homonym skip_hidden_homonym = {}) const
|
||||
-> Affixing_Result<Prefix>;
|
||||
|
||||
template <Affixing_Mode m = FULL_WORD>
|
||||
auto strip_suffix_only(std::string& s,
|
||||
Hidden_Homonym skip_hidden_homonym = {}) const
|
||||
-> Affixing_Result<Suffix>;
|
||||
|
||||
template <Affixing_Mode m = FULL_WORD>
|
||||
auto
|
||||
strip_prefix_then_suffix(std::string& s,
|
||||
Hidden_Homonym skip_hidden_homonym = {}) const
|
||||
-> Affixing_Result<Suffix, Prefix>;
|
||||
|
||||
template <Affixing_Mode m>
|
||||
auto strip_pfx_then_sfx_2(const Prefix& pe, std::string& s,
|
||||
Hidden_Homonym skip_hidden_homonym) const
|
||||
-> Affixing_Result<Suffix, Prefix>;
|
||||
|
||||
template <Affixing_Mode m = FULL_WORD>
|
||||
auto
|
||||
strip_suffix_then_prefix(std::string& s,
|
||||
Hidden_Homonym skip_hidden_homonym = {}) const
|
||||
-> Affixing_Result<Prefix, Suffix>;
|
||||
|
||||
template <Affixing_Mode m>
|
||||
auto strip_sfx_then_pfx_2(const Suffix& se, std::string& s,
|
||||
Hidden_Homonym skip_hidden_homonym) const
|
||||
-> Affixing_Result<Prefix, Suffix>;
|
||||
|
||||
template <Affixing_Mode m = FULL_WORD>
|
||||
auto strip_prefix_then_suffix_commutative(
|
||||
std::string& word, Hidden_Homonym skip_hidden_homonym = {}) const
|
||||
-> Affixing_Result<Suffix, Prefix>;
|
||||
|
||||
template <Affixing_Mode m = FULL_WORD>
|
||||
auto strip_pfx_then_sfx_comm_2(const Prefix& pe, std::string& word,
|
||||
Hidden_Homonym skip_hidden_homonym) const
|
||||
-> Affixing_Result<Suffix, Prefix>;
|
||||
|
||||
template <Affixing_Mode m = FULL_WORD>
|
||||
auto
|
||||
strip_suffix_then_suffix(std::string& s,
|
||||
Hidden_Homonym skip_hidden_homonym = {}) const
|
||||
-> Affixing_Result<Suffix, Suffix>;
|
||||
|
||||
template <Affixing_Mode m>
|
||||
auto strip_sfx_then_sfx_2(const Suffix& se1, std::string& s,
|
||||
Hidden_Homonym skip_hidden_homonym) const
|
||||
-> Affixing_Result<Suffix, Suffix>;
|
||||
|
||||
template <Affixing_Mode m = FULL_WORD>
|
||||
auto
|
||||
strip_prefix_then_prefix(std::string& s,
|
||||
Hidden_Homonym skip_hidden_homonym = {}) const
|
||||
-> Affixing_Result<Prefix, Prefix>;
|
||||
|
||||
template <Affixing_Mode m>
|
||||
auto strip_pfx_then_pfx_2(const Prefix& pe1, std::string& s,
|
||||
Hidden_Homonym skip_hidden_homonym) const
|
||||
-> Affixing_Result<Prefix, Prefix>;
|
||||
|
||||
template <Affixing_Mode m = FULL_WORD>
|
||||
auto strip_prefix_then_2_suffixes(
|
||||
std::string& s, Hidden_Homonym skip_hidden_homonym = {}) const
|
||||
-> Affixing_Result<>;
|
||||
|
||||
template <Affixing_Mode m>
|
||||
auto strip_pfx_2_sfx_3(const Prefix& pe1, const Suffix& se1,
|
||||
std::string& s,
|
||||
Hidden_Homonym skip_hidden_homonym) const
|
||||
-> Affixing_Result<>;
|
||||
|
||||
template <Affixing_Mode m = FULL_WORD>
|
||||
auto strip_suffix_prefix_suffix(
|
||||
std::string& s, Hidden_Homonym skip_hidden_homonym = {}) const
|
||||
-> Affixing_Result<>;
|
||||
|
||||
template <Affixing_Mode m>
|
||||
auto strip_s_p_s_3(const Suffix& se1, const Prefix& pe1,
|
||||
std::string& word,
|
||||
Hidden_Homonym skip_hidden_homonym) const
|
||||
-> Affixing_Result<>;
|
||||
|
||||
template <Affixing_Mode m = FULL_WORD>
|
||||
auto strip_2_suffixes_then_prefix(
|
||||
std::string& s, Hidden_Homonym skip_hidden_homonym = {}) const
|
||||
-> Affixing_Result<>;
|
||||
|
||||
template <Affixing_Mode m>
|
||||
auto strip_2_sfx_pfx_3(const Suffix& se1, const Suffix& se2,
|
||||
std::string& word,
|
||||
Hidden_Homonym skip_hidden_homonym) const
|
||||
-> Affixing_Result<>;
|
||||
|
||||
template <Affixing_Mode m = FULL_WORD>
|
||||
auto strip_suffix_then_2_prefixes(
|
||||
std::string& s, Hidden_Homonym skip_hidden_homonym = {}) const
|
||||
-> Affixing_Result<>;
|
||||
|
||||
template <Affixing_Mode m>
|
||||
auto strip_sfx_2_pfx_3(const Suffix& se1, const Prefix& pe1,
|
||||
std::string& s,
|
||||
Hidden_Homonym skip_hidden_homonym) const
|
||||
-> Affixing_Result<>;
|
||||
|
||||
template <Affixing_Mode m = FULL_WORD>
|
||||
auto strip_prefix_suffix_prefix(
|
||||
std::string& word, Hidden_Homonym skip_hidden_homonym = {}) const
|
||||
-> Affixing_Result<>;
|
||||
|
||||
template <Affixing_Mode m>
|
||||
auto strip_p_s_p_3(const Prefix& pe1, const Suffix& se1,
|
||||
std::string& word,
|
||||
Hidden_Homonym skip_hidden_homonym) const
|
||||
-> Affixing_Result<>;
|
||||
|
||||
template <Affixing_Mode m = FULL_WORD>
|
||||
auto strip_2_prefixes_then_suffix(
|
||||
std::string& word, Hidden_Homonym skip_hidden_homonym = {}) const
|
||||
-> Affixing_Result<>;
|
||||
|
||||
template <Affixing_Mode m>
|
||||
auto strip_2_pfx_sfx_3(const Prefix& pe1, const Prefix& pe2,
|
||||
std::string& word,
|
||||
Hidden_Homonym skip_hidden_homonym) const
|
||||
-> Affixing_Result<>;
|
||||
|
||||
auto check_compound(std::string& word,
|
||||
Forceucase allow_bad_forceucase) const
|
||||
-> Compounding_Result;
|
||||
|
||||
template <Affixing_Mode m = AT_COMPOUND_BEGIN>
|
||||
auto check_compound(std::string& word, size_t start_pos,
|
||||
size_t num_part, std::string& part,
|
||||
Forceucase allow_bad_forceucase) const
|
||||
-> Compounding_Result;
|
||||
|
||||
template <Affixing_Mode m = AT_COMPOUND_BEGIN>
|
||||
auto check_compound_classic(std::string& word, size_t start_pos,
|
||||
size_t i, size_t num_part,
|
||||
std::string& part,
|
||||
Forceucase allow_bad_forceucase) const
|
||||
-> Compounding_Result;
|
||||
|
||||
template <Affixing_Mode m = AT_COMPOUND_BEGIN>
|
||||
auto check_compound_with_pattern_replacements(
|
||||
std::string& word, size_t start_pos, size_t i, size_t num_part,
|
||||
std::string& part, Forceucase allow_bad_forceucase) const
|
||||
-> Compounding_Result;
|
||||
|
||||
template <Affixing_Mode m>
|
||||
auto check_word_in_compound(std::string& s) const -> Compounding_Result;
|
||||
|
||||
auto calc_num_words_modifier(const Prefix& pfx) const -> unsigned char;
|
||||
|
||||
template <Affixing_Mode m>
|
||||
auto calc_syllable_modifier(Word_List::const_reference we) const
|
||||
-> signed char;
|
||||
|
||||
template <Affixing_Mode m>
|
||||
auto calc_syllable_modifier(Word_List::const_reference we,
|
||||
const Suffix& sfx) const -> signed char;
|
||||
|
||||
auto count_syllables(std::string_view word) const -> size_t;
|
||||
|
||||
auto check_compound_with_rules(std::string& word,
|
||||
std::vector<const Flag_Set*>& words_data,
|
||||
size_t start_pos, std::string& part,
|
||||
Forceucase allow_bad_forceucase) const
|
||||
|
||||
-> Compounding_Result;
|
||||
auto is_rep_similar(std::string& word) const -> bool;
|
||||
};
|
||||
|
||||
template <Affixing_Mode m>
|
||||
auto Checker::affix_NOT_valid(const Prefix& e) const
|
||||
{
|
||||
if (m == FULL_WORD && e.cont_flags.contains(compound_onlyin_flag))
|
||||
return true;
|
||||
if (m == AT_COMPOUND_END &&
|
||||
!e.cont_flags.contains(compound_permit_flag))
|
||||
return true;
|
||||
if (m != FULL_WORD && e.cont_flags.contains(compound_forbid_flag))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
template <Affixing_Mode m>
|
||||
auto Checker::affix_NOT_valid(const Suffix& e) const
|
||||
{
|
||||
if (m == FULL_WORD && e.cont_flags.contains(compound_onlyin_flag))
|
||||
return true;
|
||||
if (m == AT_COMPOUND_BEGIN &&
|
||||
!e.cont_flags.contains(compound_permit_flag))
|
||||
return true;
|
||||
if (m != FULL_WORD && e.cont_flags.contains(compound_forbid_flag))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
template <Affixing_Mode m, class AffixT>
|
||||
auto Checker::outer_affix_NOT_valid(const AffixT& e) const
|
||||
{
|
||||
if (affix_NOT_valid<m>(e))
|
||||
return true;
|
||||
if (e.cont_flags.contains(need_affix_flag))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
template <class AffixT>
|
||||
auto Checker::is_circumfix(const AffixT& a) const
|
||||
{
|
||||
return a.cont_flags.contains(circumfix_flag);
|
||||
}
|
||||
|
||||
template <class AffixInner, class AffixOuter>
|
||||
auto cross_valid_inner_outer(const AffixInner& inner, const AffixOuter& outer)
|
||||
{
|
||||
return inner.cont_flags.contains(outer.flag);
|
||||
}
|
||||
|
||||
template <class Affix>
|
||||
auto cross_valid_inner_outer(const Flag_Set& word_flags, const Affix& afx)
|
||||
{
|
||||
return word_flags.contains(afx.flag);
|
||||
}
|
||||
|
||||
} // namespace v5
|
||||
} // namespace nuspell
|
||||
#endif // NUSPELL_CHECKER_HXX
|
||||
@@ -1 +0,0 @@
|
||||
clang-format -style=file -i *.[ch]xx
|
||||
@@ -1,115 +0,0 @@
|
||||
/* Copyright 2016-2021 Dimitrij Mijoski
|
||||
*
|
||||
* This file is part of Nuspell.
|
||||
*
|
||||
* Nuspell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Nuspell is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Nuspell. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "dictionary.hxx"
|
||||
#include "utils.hxx"
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace nuspell {
|
||||
inline namespace v5 {
|
||||
|
||||
Dictionary::Dictionary(std::istream& aff, std::istream& dic)
|
||||
{
|
||||
if (!parse_aff_dic(aff, dic))
|
||||
throw Dictionary_Loading_Error("error parsing");
|
||||
}
|
||||
|
||||
Dictionary::Dictionary() = default;
|
||||
|
||||
/**
|
||||
* @brief Create a dictionary from opened files as iostreams
|
||||
*
|
||||
* Prefer using load_from_path(). Use this if you have a specific use case,
|
||||
* like when .aff and .dic are in-memory buffers istringstream.
|
||||
*
|
||||
* @param aff The iostream of the .aff file
|
||||
* @param dic The iostream of the .dic file
|
||||
* @return Dictionary object
|
||||
* @throws Dictionary_Loading_Error on error
|
||||
*/
|
||||
auto Dictionary::load_from_aff_dic(std::istream& aff, std::istream& dic)
|
||||
-> Dictionary
|
||||
{
|
||||
return Dictionary(aff, dic);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Create a dictionary from files
|
||||
* @param file_path_without_extension path *without* extensions (without .dic or
|
||||
* .aff)
|
||||
* @return Dictionary object
|
||||
* @throws Dictionary_Loading_Error on error
|
||||
*/
|
||||
auto Dictionary::load_from_path(const std::string& file_path_without_extension)
|
||||
-> Dictionary
|
||||
{
|
||||
auto path = file_path_without_extension;
|
||||
path += ".aff";
|
||||
std::ifstream aff_file(path);
|
||||
if (aff_file.fail()) {
|
||||
auto err = "Aff file " + path + " not found";
|
||||
throw Dictionary_Loading_Error(err);
|
||||
}
|
||||
path.replace(path.size() - 3, 3, "dic");
|
||||
std::ifstream dic_file(path);
|
||||
if (dic_file.fail()) {
|
||||
auto err = "Dic file " + path + " not found";
|
||||
throw Dictionary_Loading_Error(err);
|
||||
}
|
||||
return load_from_aff_dic(aff_file, dic_file);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Checks if a given word is correct
|
||||
* @param word any word
|
||||
* @return true if correct, false otherwise
|
||||
*/
|
||||
auto Dictionary::spell(std::string_view word) const -> bool
|
||||
{
|
||||
auto ok_enc = validate_utf8(word);
|
||||
if (unlikely(word.size() > 360))
|
||||
return false;
|
||||
if (unlikely(!ok_enc))
|
||||
return false;
|
||||
auto word_buf = string(word);
|
||||
return spell_priv(word_buf);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Suggests correct words for a given incorrect word
|
||||
* @param[in] word incorrect word
|
||||
* @param[out] out this object will be populated with the suggestions
|
||||
*/
|
||||
auto Dictionary::suggest(std::string_view word,
|
||||
std::vector<std::string>& out) const -> void
|
||||
{
|
||||
out.clear();
|
||||
auto ok_enc = validate_utf8(word);
|
||||
if (unlikely(word.size() > 360))
|
||||
return;
|
||||
if (unlikely(!ok_enc))
|
||||
return;
|
||||
suggest_priv(word, out);
|
||||
}
|
||||
} // namespace v5
|
||||
} // namespace nuspell
|
||||
@@ -1,59 +0,0 @@
|
||||
/* Copyright 2016-2021 Dimitrij Mijoski
|
||||
*
|
||||
* This file is part of Nuspell.
|
||||
*
|
||||
* Nuspell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Nuspell is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Nuspell. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file
|
||||
* @brief Dictionary spelling.
|
||||
*/
|
||||
|
||||
#ifndef NUSPELL_DICTIONARY_HXX
|
||||
#define NUSPELL_DICTIONARY_HXX
|
||||
|
||||
#include "suggester.hxx"
|
||||
|
||||
namespace nuspell {
|
||||
inline namespace v5 {
|
||||
|
||||
/**
|
||||
* @brief The only important public exception
|
||||
*/
|
||||
class Dictionary_Loading_Error : public std::runtime_error {
|
||||
public:
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The only important public class
|
||||
*/
|
||||
class NUSPELL_EXPORT Dictionary : private Suggester {
|
||||
Dictionary(std::istream& aff, std::istream& dic);
|
||||
|
||||
public:
|
||||
Dictionary();
|
||||
auto static load_from_aff_dic(std::istream& aff, std::istream& dic)
|
||||
-> Dictionary;
|
||||
auto static load_from_path(
|
||||
const std::string& file_path_without_extension) -> Dictionary;
|
||||
auto spell(std::string_view word) const -> bool;
|
||||
auto suggest(std::string_view word, std::vector<std::string>& out) const
|
||||
-> void;
|
||||
};
|
||||
|
||||
} // namespace v5
|
||||
} // namespace nuspell
|
||||
#endif // NUSPELL_DICTIONARY_HXX
|
||||
@@ -1,18 +0,0 @@
|
||||
#ifndef NUSPELL_EXPORT_H
|
||||
#define NUSPELL_EXPORT_H
|
||||
|
||||
#ifdef NUSPELL_STATIC_DEFINE
|
||||
# define NUSPELL_EXPORT
|
||||
#elif defined(_WIN32) || defined(__CYGWIN__)
|
||||
# ifdef nuspell_EXPORTS // Define this only when building Nuspell as DLL on Windows, not when using the DLL.
|
||||
# define NUSPELL_EXPORT __declspec(dllexport)
|
||||
# else
|
||||
# define NUSPELL_EXPORT __declspec(dllimport)
|
||||
# endif
|
||||
#elif __GNUC__ >= 4
|
||||
# define NUSPELL_EXPORT __attribute__((visibility("default")))
|
||||
#else
|
||||
# define NUSPELL_EXPORT
|
||||
#endif
|
||||
|
||||
#endif /* NUSPELL_EXPORT_H */
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,97 +0,0 @@
|
||||
/* Copyright 2016-2021 Dimitrij Mijoski
|
||||
*
|
||||
* This file is part of Nuspell.
|
||||
*
|
||||
* Nuspell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Nuspell is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Nuspell. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef NUSPELL_SUGGESTER_HXX
|
||||
#define NUSPELL_SUGGESTER_HXX
|
||||
|
||||
#include "checker.hxx"
|
||||
|
||||
namespace nuspell {
|
||||
inline namespace v5 {
|
||||
|
||||
struct NUSPELL_EXPORT Suggester : public Checker {
|
||||
|
||||
enum High_Quality_Sugs : bool {
|
||||
ALL_LOW_QUALITY_SUGS = false,
|
||||
HAS_HIGH_QUALITY_SUGS = true
|
||||
};
|
||||
|
||||
auto suggest_priv(std::string_view input_word, List_Strings& out) const
|
||||
-> void;
|
||||
|
||||
auto suggest_low(std::string& word, List_Strings& out) const
|
||||
-> High_Quality_Sugs;
|
||||
|
||||
auto add_sug_if_correct(std::string& word, List_Strings& out) const
|
||||
-> bool;
|
||||
|
||||
auto uppercase_suggest(const std::string& word, List_Strings& out) const
|
||||
-> void;
|
||||
|
||||
auto rep_suggest(std::string& word, List_Strings& out) const -> void;
|
||||
|
||||
auto try_rep_suggestion(std::string& word, List_Strings& out) const
|
||||
-> void;
|
||||
|
||||
auto max_attempts_for_long_alogs(std::string_view word) const -> size_t;
|
||||
|
||||
auto map_suggest(std::string& word, List_Strings& out) const -> void;
|
||||
|
||||
auto map_suggest(std::string& word, List_Strings& out, size_t i,
|
||||
size_t& remaining_attempts) const -> void;
|
||||
|
||||
auto adjacent_swap_suggest(std::string& word, List_Strings& out) const
|
||||
-> void;
|
||||
|
||||
auto distant_swap_suggest(std::string& word, List_Strings& out) const
|
||||
-> void;
|
||||
|
||||
auto keyboard_suggest(std::string& word, List_Strings& out) const
|
||||
-> void;
|
||||
|
||||
auto extra_char_suggest(std::string& word, List_Strings& out) const
|
||||
-> void;
|
||||
|
||||
auto forgotten_char_suggest(std::string& word, List_Strings& out) const
|
||||
-> void;
|
||||
|
||||
auto move_char_suggest(std::string& word, List_Strings& out) const
|
||||
-> void;
|
||||
|
||||
auto bad_char_suggest(std::string& word, List_Strings& out) const
|
||||
-> void;
|
||||
|
||||
auto doubled_two_chars_suggest(std::string& word,
|
||||
List_Strings& out) const -> void;
|
||||
|
||||
auto two_words_suggest(const std::string& word, List_Strings& out) const
|
||||
-> void;
|
||||
|
||||
auto ngram_suggest(const std::string& word_u8, List_Strings& out) const
|
||||
-> void;
|
||||
|
||||
auto expand_root_word_for_ngram(Word_List::const_reference root,
|
||||
std::string_view wrong,
|
||||
List_Strings& expanded_list,
|
||||
std::vector<bool>& cross_affix) const
|
||||
-> void;
|
||||
};
|
||||
|
||||
} // namespace v5
|
||||
} // namespace nuspell
|
||||
#endif // NUSPELL_SUGGESTER_HXX
|
||||
@@ -1,383 +0,0 @@
|
||||
/* Copyright 2021 Dimitrij Mijoski
|
||||
*
|
||||
* This file is part of Nuspell.
|
||||
*
|
||||
* Nuspell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Nuspell is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Nuspell. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef NUSPELL_UNICODE_HXX
|
||||
#define NUSPELL_UNICODE_HXX
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unicode/utf16.h>
|
||||
#include <unicode/utf8.h>
|
||||
|
||||
namespace nuspell {
|
||||
inline namespace v5 {
|
||||
|
||||
// UTF-8, work on malformed
|
||||
|
||||
inline constexpr auto u8_max_cp_length = U8_MAX_LENGTH;
|
||||
|
||||
auto inline u8_is_cp_error(int32_t cp) -> bool { return cp < 0; }
|
||||
|
||||
template <class Range>
|
||||
auto u8_advance_cp(const Range& str, size_t& i, int32_t& cp) -> void
|
||||
{
|
||||
using std::size, std::data;
|
||||
#if U_ICU_VERSION_MAJOR_NUM <= 60
|
||||
auto s_ptr = data(str);
|
||||
int32_t idx = i;
|
||||
int32_t len = size(str);
|
||||
U8_NEXT(s_ptr, idx, len, cp);
|
||||
i = idx;
|
||||
#else
|
||||
auto len = size(str);
|
||||
U8_NEXT(str, i, len, cp);
|
||||
#endif
|
||||
}
|
||||
|
||||
template <class Range>
|
||||
auto u8_advance_index(const Range& str, size_t& i) -> void
|
||||
{
|
||||
using std::size;
|
||||
auto len = size(str);
|
||||
U8_FWD_1(str, i, len);
|
||||
}
|
||||
|
||||
template <class Range>
|
||||
auto u8_reverse_cp(const Range& str, size_t& i, int32_t& cp) -> void
|
||||
{
|
||||
using std::size, std::data;
|
||||
auto ptr = data(str);
|
||||
int32_t idx = i;
|
||||
U8_PREV(ptr, 0, idx, cp);
|
||||
i = idx;
|
||||
}
|
||||
|
||||
template <class Range>
|
||||
auto u8_reverse_index(const Range& str, size_t& i) -> void
|
||||
{
|
||||
using std::size, std::data;
|
||||
auto ptr = data(str);
|
||||
int32_t idx = i;
|
||||
U8_BACK_1(ptr, 0, idx);
|
||||
i = idx;
|
||||
}
|
||||
|
||||
template <class Range>
|
||||
auto u8_write_cp_and_advance(Range& buf, size_t& i, int32_t cp, bool& error)
|
||||
-> void
|
||||
{
|
||||
using std::size, std::data;
|
||||
#if U_ICU_VERSION_MAJOR_NUM <= 60
|
||||
auto ptr = data(buf);
|
||||
int32_t idx = i;
|
||||
int32_t len = size(buf);
|
||||
U8_APPEND(buf, idx, len, cp, error);
|
||||
i = idx;
|
||||
#else
|
||||
auto len = size(buf);
|
||||
U8_APPEND(buf, i, len, cp, error);
|
||||
#endif
|
||||
}
|
||||
|
||||
// UTF-8, valid
|
||||
|
||||
template <class Range>
|
||||
auto valid_u8_advance_cp(const Range& str, size_t& i, char32_t& cp) -> void
|
||||
{
|
||||
U8_NEXT_UNSAFE(str, i, cp);
|
||||
}
|
||||
|
||||
template <class Range>
|
||||
auto valid_u8_advance_index(const Range& str, size_t& i) -> void
|
||||
{
|
||||
U8_FWD_1_UNSAFE(str, i);
|
||||
}
|
||||
|
||||
template <class Range>
|
||||
auto valid_u8_reverse_cp(const Range& str, size_t& i, char32_t& cp) -> void
|
||||
{
|
||||
U8_PREV_UNSAFE(str, i, cp);
|
||||
}
|
||||
|
||||
template <class Range>
|
||||
auto valid_u8_reverse_index(const Range& str, size_t& i) -> void
|
||||
{
|
||||
U8_BACK_1_UNSAFE(str, i);
|
||||
}
|
||||
|
||||
template <class Range>
|
||||
auto valid_u8_write_cp_and_advance(Range& buf, size_t& i, char32_t cp) -> void
|
||||
{
|
||||
U8_APPEND_UNSAFE(buf, i, cp);
|
||||
}
|
||||
|
||||
// UTF-16, work on malformed
|
||||
|
||||
inline constexpr auto u16_max_cp_length = U16_MAX_LENGTH;
|
||||
|
||||
auto inline u16_is_cp_error(int32_t cp) -> bool { return U_IS_SURROGATE(cp); }
|
||||
|
||||
template <class Range>
|
||||
auto u16_advance_cp(const Range& str, size_t& i, int32_t& cp) -> void
|
||||
{
|
||||
using std::size;
|
||||
auto len = size(str);
|
||||
U16_NEXT(str, i, len, cp);
|
||||
}
|
||||
|
||||
template <class Range>
|
||||
auto u16_advance_index(const Range& str, size_t& i) -> void
|
||||
{
|
||||
using std::size;
|
||||
auto len = size(str);
|
||||
U16_FWD_1(str, i, len);
|
||||
}
|
||||
|
||||
template <class Range>
|
||||
auto u16_reverse_cp(const Range& str, size_t& i, int32_t& cp) -> void
|
||||
{
|
||||
U16_PREV(str, 0, i, cp);
|
||||
}
|
||||
|
||||
template <class Range>
|
||||
auto u16_reverse_index(const Range& str, size_t& i) -> void
|
||||
{
|
||||
U16_BACK_1(str, 0, i);
|
||||
}
|
||||
|
||||
template <class Range>
|
||||
auto u16_write_cp_and_advance(Range& buf, size_t& i, int32_t cp, bool& error)
|
||||
-> void
|
||||
{
|
||||
using std::size;
|
||||
auto len = size(buf);
|
||||
U16_APPEND(buf, i, len, cp, error);
|
||||
}
|
||||
|
||||
// UTF-16, valid
|
||||
|
||||
template <class Range>
|
||||
auto valid_u16_advance_cp(const Range& str, size_t& i, char32_t& cp) -> void
|
||||
{
|
||||
U16_NEXT_UNSAFE(str, i, cp);
|
||||
}
|
||||
|
||||
template <class Range>
|
||||
auto valid_u16_advance_index(const Range& str, size_t& i) -> void
|
||||
{
|
||||
U16_FWD_1_UNSAFE(str, i);
|
||||
}
|
||||
|
||||
template <class Range>
|
||||
auto valid_u16_reverse_cp(const Range& str, size_t& i, char32_t& cp) -> void
|
||||
{
|
||||
U16_PREV_UNSAFE(str, i, cp);
|
||||
}
|
||||
|
||||
template <class Range>
|
||||
auto valid_u16_reverse_index(const Range& str, size_t& i) -> void
|
||||
{
|
||||
U16_BACK_1_UNSAFE(str, i);
|
||||
}
|
||||
|
||||
template <class Range>
|
||||
auto valid_u16_write_cp_and_advance(Range& buf, size_t& i, char32_t cp) -> void
|
||||
{
|
||||
U16_APPEND_UNSAFE(buf, i, cp);
|
||||
}
|
||||
|
||||
// higer level funcs
|
||||
|
||||
struct U8_CP_Pos {
|
||||
size_t begin_i = 0;
|
||||
size_t end_i = begin_i;
|
||||
};
|
||||
|
||||
class U8_Encoded_CP {
|
||||
char d[u8_max_cp_length];
|
||||
int sz;
|
||||
|
||||
public:
|
||||
explicit U8_Encoded_CP(std::string_view str, U8_CP_Pos pos)
|
||||
: sz(pos.end_i - pos.begin_i)
|
||||
{
|
||||
auto i = sz;
|
||||
auto j = pos.end_i;
|
||||
auto max_len = 4;
|
||||
do {
|
||||
d[--i] = str[--j];
|
||||
} while (i && --max_len);
|
||||
}
|
||||
U8_Encoded_CP(char32_t cp)
|
||||
{
|
||||
size_t z = 0;
|
||||
valid_u8_write_cp_and_advance(d, z, cp);
|
||||
sz = z;
|
||||
}
|
||||
auto size() const noexcept -> size_t { return sz; }
|
||||
auto data() const noexcept -> const char* { return d; }
|
||||
operator std::string_view() const noexcept
|
||||
{
|
||||
return std::string_view(data(), size());
|
||||
}
|
||||
auto copy_to(std::string& str, size_t j) const
|
||||
{
|
||||
auto i = sz;
|
||||
j += sz;
|
||||
auto max_len = 4;
|
||||
do {
|
||||
str[--j] = d[--i];
|
||||
} while (i && --max_len);
|
||||
}
|
||||
};
|
||||
|
||||
auto inline u8_swap_adjacent_cp(std::string& str, size_t i1, size_t i2,
|
||||
size_t i3) -> size_t
|
||||
{
|
||||
auto cp1 = U8_Encoded_CP(str, {i1, i2});
|
||||
auto cp2 = U8_Encoded_CP(str, {i2, i3});
|
||||
auto new_i2 = i1 + std::size(cp2);
|
||||
cp1.copy_to(str, new_i2);
|
||||
cp2.copy_to(str, i1);
|
||||
return new_i2;
|
||||
}
|
||||
|
||||
auto inline u8_swap_cp(std::string& str, U8_CP_Pos pos1, U8_CP_Pos pos2)
|
||||
-> std::pair<size_t, size_t>
|
||||
{
|
||||
using std::size;
|
||||
auto cp1 = U8_Encoded_CP(str, pos1);
|
||||
auto cp2 = U8_Encoded_CP(str, pos2);
|
||||
auto new_p1_end_i = pos1.begin_i + size(cp2);
|
||||
auto new_p2_begin_i = pos2.end_i - size(cp1);
|
||||
std::char_traits<char>::move(&str[new_p1_end_i], &str[pos1.end_i],
|
||||
pos2.begin_i - pos1.end_i);
|
||||
cp2.copy_to(str, pos1.begin_i);
|
||||
cp1.copy_to(str, new_p2_begin_i);
|
||||
return {new_p1_end_i, new_p2_begin_i};
|
||||
}
|
||||
|
||||
// bellow go func without out-parametars
|
||||
|
||||
// UTF-8, can be malformed, no out-parametars
|
||||
|
||||
struct Idx_And_Next_CP {
|
||||
size_t end_i;
|
||||
int32_t cp;
|
||||
};
|
||||
|
||||
struct Idx_And_Prev_CP {
|
||||
size_t begin_i;
|
||||
int32_t cp;
|
||||
};
|
||||
|
||||
struct Write_CP_Idx_and_Error {
|
||||
size_t end_i;
|
||||
bool error;
|
||||
};
|
||||
|
||||
template <class Range>
|
||||
[[nodiscard]] auto u8_next_cp(const Range& str, size_t i) -> Idx_And_Next_CP
|
||||
{
|
||||
int32_t cp;
|
||||
u8_advance_cp(str, i, cp);
|
||||
return {i, cp};
|
||||
}
|
||||
|
||||
template <class Range>
|
||||
[[nodiscard]] auto u8_next_index(const Range& str, size_t i) -> size_t
|
||||
{
|
||||
u8_advance_index(str, i);
|
||||
return i;
|
||||
}
|
||||
|
||||
template <class Range>
|
||||
[[nodiscard]] auto u8_prev_cp(const Range& str, size_t i) -> Idx_And_Prev_CP
|
||||
{
|
||||
int32_t cp;
|
||||
u8_reverse_cp(str, i, cp);
|
||||
return {i, cp};
|
||||
}
|
||||
|
||||
template <class Range>
|
||||
[[nodiscard]] auto u8_prev_index(const Range& str, size_t i) -> size_t
|
||||
{
|
||||
u8_reverse_index(str, i);
|
||||
return i;
|
||||
}
|
||||
|
||||
template <class Range>
|
||||
[[nodiscard]] auto u8_write_cp(Range& buf, size_t i, int32_t cp)
|
||||
-> Write_CP_Idx_and_Error
|
||||
{
|
||||
bool err;
|
||||
u8_write_cp_and_advance(buf, i, cp, err);
|
||||
return {i, err};
|
||||
}
|
||||
|
||||
// UTF-8, valid, no out-parametars
|
||||
|
||||
struct Idx_And_Next_CP_Valid {
|
||||
size_t end_i;
|
||||
char32_t cp;
|
||||
};
|
||||
|
||||
struct Idx_And_Prev_CP_Valid {
|
||||
size_t begin_i;
|
||||
char32_t cp;
|
||||
};
|
||||
|
||||
template <class Range>
|
||||
[[nodiscard]] auto valid_u8_next_cp(const Range& str, size_t i)
|
||||
-> Idx_And_Next_CP_Valid
|
||||
{
|
||||
char32_t cp;
|
||||
valid_u8_advance_cp(str, i, cp);
|
||||
return {i, cp};
|
||||
}
|
||||
|
||||
template <class Range>
|
||||
[[nodiscard]] auto valid_u8_next_index(const Range& str, size_t i) -> size_t
|
||||
{
|
||||
valid_u8_advance_index(str, i);
|
||||
return i;
|
||||
}
|
||||
|
||||
template <class Range>
|
||||
[[nodiscard]] auto valid_u8_prev_cp(const Range& str, size_t i)
|
||||
-> Idx_And_Prev_CP_Valid
|
||||
{
|
||||
char32_t cp;
|
||||
valid_u8_reverse_cp(str, i, cp);
|
||||
return {i, cp};
|
||||
}
|
||||
|
||||
template <class Range>
|
||||
[[nodiscard]] auto valid_u8_prev_index(const Range& str, size_t i) -> size_t
|
||||
{
|
||||
valid_u8_reverse_index(str, i);
|
||||
return i;
|
||||
}
|
||||
|
||||
template <class Range>
|
||||
[[nodiscard]] auto valid_u8_write_cp(Range& buf, size_t i, int32_t cp) -> size_t
|
||||
{
|
||||
valid_u8_write_cp_and_advance(buf, i, cp);
|
||||
return i;
|
||||
}
|
||||
} // namespace v5
|
||||
} // namespace nuspell
|
||||
#endif // NUSPELL_UNICODE_HXX
|
||||
@@ -1,465 +0,0 @@
|
||||
/* Copyright 2016-2021 Dimitrij Mijoski
|
||||
*
|
||||
* This file is part of Nuspell.
|
||||
*
|
||||
* Nuspell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Nuspell is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Nuspell. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "utils.hxx"
|
||||
#include "unicode.hxx"
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
#include <unicode/uchar.h>
|
||||
#include <unicode/ucnv.h>
|
||||
#include <unicode/unistr.h>
|
||||
#include <unicode/ustring.h>
|
||||
|
||||
#if ' ' != 32 || '.' != 46 || 'A' != 65 || 'Z' != 90 || 'a' != 97 || 'z' != 122
|
||||
#error "Basic execution character set is not ASCII"
|
||||
#endif
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace nuspell {
|
||||
inline namespace v5 {
|
||||
|
||||
template <class SepT>
|
||||
static auto& split_on_any_of_low(std::string_view s, const SepT& sep,
|
||||
std::vector<std::string>& out)
|
||||
{
|
||||
size_t i1 = 0;
|
||||
size_t i2;
|
||||
do {
|
||||
i2 = s.find_first_of(sep, i1);
|
||||
out.emplace_back(s.substr(i1, i2 - i1));
|
||||
i1 = i2 + 1; // we can only add +1 if separator is single char.
|
||||
|
||||
// i2 gets s.npos after the last separator.
|
||||
// Length of i2-i1 will always go past the end. That is defined.
|
||||
} while (i2 != s.npos);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @brief Splits string on single char seperator.
|
||||
*
|
||||
* Consecutive separators are treated as separate and will emit empty strings.
|
||||
*
|
||||
* @param s string to split.
|
||||
* @param sep char that acts as separator to split on.
|
||||
* @param out vector where separated strings are appended.
|
||||
* @return @p out.
|
||||
*/
|
||||
auto split(std::string_view s, char sep, std::vector<std::string>& out)
|
||||
-> std::vector<std::string>&
|
||||
{
|
||||
return split_on_any_of_low(s, sep, out);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @brief Splits string on set of single char seperators.
|
||||
*
|
||||
* Consecutive separators are treated as separate and will emit empty strings.
|
||||
*
|
||||
* @param s string to split.
|
||||
* @param sep seperator(s) to split on.
|
||||
* @param out vector where separated strings are appended.
|
||||
* @return @p out.
|
||||
*/
|
||||
auto split_on_any_of(std::string_view s, const char* sep,
|
||||
std::vector<std::string>& out) -> std::vector<std::string>&
|
||||
{
|
||||
return split_on_any_of_low(s, sep, out);
|
||||
}
|
||||
|
||||
auto utf32_to_utf8(std::u32string_view in, std::string& out) -> void
|
||||
{
|
||||
out.clear();
|
||||
for (size_t i = 0; i != size(in); ++i) {
|
||||
auto cp = in[i];
|
||||
auto enc_cp = U8_Encoded_CP(cp);
|
||||
out += enc_cp;
|
||||
}
|
||||
}
|
||||
auto utf32_to_utf8(std::u32string_view in) -> std::string
|
||||
{
|
||||
auto out = string();
|
||||
utf32_to_utf8(in, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
auto valid_utf8_to_32(std::string_view in, std::u32string& out) -> void
|
||||
{
|
||||
out.clear();
|
||||
for (size_t i = 0; i != size(in);) {
|
||||
char32_t cp;
|
||||
valid_u8_advance_cp(in, i, cp);
|
||||
out.push_back(cp);
|
||||
}
|
||||
}
|
||||
auto valid_utf8_to_32(std::string_view in) -> std::u32string
|
||||
{
|
||||
auto out = u32string();
|
||||
valid_utf8_to_32(in, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
auto utf8_to_16(std::string_view in) -> std::u16string
|
||||
{
|
||||
auto out = u16string();
|
||||
utf8_to_16(in, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
bool utf8_to_16(std::string_view in, std::u16string& out)
|
||||
{
|
||||
int32_t len;
|
||||
auto err = U_ZERO_ERROR;
|
||||
u_strFromUTF8(data(out), size(out), &len, data(in), size(in), &err);
|
||||
out.resize(len);
|
||||
if (err == U_BUFFER_OVERFLOW_ERROR) {
|
||||
err = U_ZERO_ERROR;
|
||||
u_strFromUTF8(data(out), size(out), &len, data(in), size(in),
|
||||
&err);
|
||||
}
|
||||
if (U_SUCCESS(err))
|
||||
return true;
|
||||
out.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool validate_utf8(string_view s)
|
||||
{
|
||||
auto err = U_ZERO_ERROR;
|
||||
u_strFromUTF8(nullptr, 0, nullptr, data(s), size(s), &err);
|
||||
if (err == U_INVALID_CHAR_FOUND)
|
||||
return false;
|
||||
return err == U_BUFFER_OVERFLOW_ERROR || U_SUCCESS(err);
|
||||
}
|
||||
|
||||
auto static is_ascii(char c) -> bool
|
||||
{
|
||||
return static_cast<unsigned char>(c) <= 127;
|
||||
}
|
||||
|
||||
auto is_all_ascii(std::string_view s) -> bool
|
||||
{
|
||||
return all_of(begin(s), end(s), is_ascii);
|
||||
}
|
||||
|
||||
auto static widen_latin1(char c) -> char16_t
|
||||
{
|
||||
return static_cast<unsigned char>(c);
|
||||
}
|
||||
|
||||
auto latin1_to_ucs2(std::string_view s) -> std::u16string
|
||||
{
|
||||
u16string ret;
|
||||
latin1_to_ucs2(s, ret);
|
||||
return ret;
|
||||
}
|
||||
auto latin1_to_ucs2(std::string_view s, std::u16string& out) -> void
|
||||
{
|
||||
out.resize(s.size());
|
||||
transform(begin(s), end(s), begin(out), widen_latin1);
|
||||
}
|
||||
|
||||
auto static is_surrogate_pair(char16_t c) -> bool
|
||||
{
|
||||
return 0xD800 <= c && c <= 0xDFFF;
|
||||
}
|
||||
auto is_all_bmp(std::u16string_view s) -> bool
|
||||
{
|
||||
return none_of(begin(s), end(s), is_surrogate_pair);
|
||||
}
|
||||
|
||||
auto to_upper_ascii(std::string& s) -> void
|
||||
{
|
||||
auto& char_type = use_facet<ctype<char>>(locale::classic());
|
||||
char_type.toupper(begin_ptr(s), end_ptr(s));
|
||||
}
|
||||
|
||||
auto static utf32_to_icu(u32string_view in) -> icu::UnicodeString
|
||||
{
|
||||
static_assert(sizeof(UChar32) == sizeof(char32_t));
|
||||
return icu::UnicodeString::fromUTF32(
|
||||
reinterpret_cast<const UChar32*>(in.data()), in.size());
|
||||
}
|
||||
auto static icu_to_utf32(const icu::UnicodeString& in, std::u32string& out)
|
||||
-> bool
|
||||
{
|
||||
out.resize(in.length());
|
||||
auto err = U_ZERO_ERROR;
|
||||
auto len =
|
||||
in.toUTF32(reinterpret_cast<UChar32*>(out.data()), out.size(), err);
|
||||
if (U_SUCCESS(err)) {
|
||||
out.erase(len);
|
||||
return true;
|
||||
}
|
||||
out.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
auto to_upper(std::string_view in, const icu::Locale& loc) -> std::string
|
||||
{
|
||||
auto out = std::string();
|
||||
to_upper(in, loc, out);
|
||||
return out;
|
||||
}
|
||||
auto to_title(std::string_view in, const icu::Locale& loc) -> std::string
|
||||
{
|
||||
auto out = std::string();
|
||||
to_title(in, loc, out);
|
||||
return out;
|
||||
}
|
||||
auto to_lower(std::string_view in, const icu::Locale& loc) -> std::string
|
||||
{
|
||||
auto out = std::string();
|
||||
to_lower(in, loc, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
auto to_upper(string_view in, const icu::Locale& loc, string& out) -> void
|
||||
{
|
||||
auto sp = icu::StringPiece(data(in), size(in));
|
||||
auto us = icu::UnicodeString::fromUTF8(sp);
|
||||
us.toUpper(loc);
|
||||
out.clear();
|
||||
us.toUTF8String(out);
|
||||
}
|
||||
auto to_title(string_view in, const icu::Locale& loc, string& out) -> void
|
||||
{
|
||||
auto sp = icu::StringPiece(data(in), size(in));
|
||||
auto us = icu::UnicodeString::fromUTF8(sp);
|
||||
us.toTitle(nullptr, loc);
|
||||
out.clear();
|
||||
us.toUTF8String(out);
|
||||
}
|
||||
auto to_lower(u32string_view in, const icu::Locale& loc, u32string& out) -> void
|
||||
{
|
||||
auto us = utf32_to_icu(in);
|
||||
us.toLower(loc);
|
||||
icu_to_utf32(us, out);
|
||||
}
|
||||
auto to_lower(string_view in, const icu::Locale& loc, string& out) -> void
|
||||
{
|
||||
auto sp = icu::StringPiece(data(in), size(in));
|
||||
auto us = icu::UnicodeString::fromUTF8(sp);
|
||||
us.toLower(loc);
|
||||
out.clear();
|
||||
us.toUTF8String(out);
|
||||
}
|
||||
|
||||
auto to_lower_char_at(std::string& s, size_t i, const icu::Locale& loc) -> void
|
||||
{
|
||||
auto cp = valid_u8_next_cp(s, i);
|
||||
auto us = icu::UnicodeString(UChar32(cp.cp));
|
||||
us.toLower(loc);
|
||||
auto u8_low = string();
|
||||
us.toUTF8String(u8_low);
|
||||
s.replace(i, cp.end_i - i, u8_low);
|
||||
}
|
||||
auto to_title_char_at(std::string& s, size_t i, const icu::Locale& loc) -> void
|
||||
{
|
||||
auto cp = valid_u8_next_cp(s, i);
|
||||
auto us = icu::UnicodeString(UChar32(cp.cp));
|
||||
us.toTitle(nullptr, loc);
|
||||
auto u8_title = string();
|
||||
us.toUTF8String(u8_title);
|
||||
s.replace(i, cp.end_i - i, u8_title);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @brief Determines casing (capitalization) type for a word.
|
||||
*
|
||||
* Casing is sometimes referred to as capitalization.
|
||||
*
|
||||
* @param s word.
|
||||
* @return The casing type.
|
||||
*/
|
||||
auto classify_casing(string_view s) -> Casing
|
||||
{
|
||||
size_t upper = 0;
|
||||
size_t lower = 0;
|
||||
for (size_t i = 0; i != size(s);) {
|
||||
char32_t c;
|
||||
valid_u8_advance_cp(s, i, c);
|
||||
if (u_isupper(c))
|
||||
upper++;
|
||||
else if (u_islower(c))
|
||||
lower++;
|
||||
// else neutral
|
||||
}
|
||||
if (upper == 0) // all lowercase, maybe with some neutral
|
||||
return Casing::SMALL; // most common case
|
||||
|
||||
auto first_cp = valid_u8_next_cp(s, 0);
|
||||
auto first_capital = u_isupper(first_cp.cp);
|
||||
if (first_capital && upper == 1)
|
||||
return Casing::INIT_CAPITAL; // second most common
|
||||
|
||||
if (lower == 0)
|
||||
return Casing::ALL_CAPITAL;
|
||||
|
||||
if (first_capital)
|
||||
return Casing::PASCAL;
|
||||
else
|
||||
return Casing::CAMEL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @brief Check if word[i] or word[i-1] are uppercase
|
||||
*
|
||||
* Check if the two chars are alphabetic and at least one of them is in
|
||||
* uppercase.
|
||||
*
|
||||
* @return true if at least one is uppercase, false otherwise.
|
||||
*/
|
||||
auto has_uppercase_at_compound_word_boundary(string_view word, size_t i) -> bool
|
||||
{
|
||||
auto cp = valid_u8_next_cp(word, i);
|
||||
auto cp_prev = valid_u8_prev_cp(word, i);
|
||||
if (u_isupper(cp.cp)) {
|
||||
if (u_isalpha(cp_prev.cp))
|
||||
return true;
|
||||
}
|
||||
else if (u_isupper(cp_prev.cp) && u_isalpha(cp.cp))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
Encoding_Converter::Encoding_Converter(const char* enc)
|
||||
{
|
||||
auto err = UErrorCode();
|
||||
cnv = ucnv_open(enc, &err);
|
||||
}
|
||||
|
||||
Encoding_Converter::~Encoding_Converter()
|
||||
{
|
||||
if (cnv)
|
||||
ucnv_close(cnv);
|
||||
}
|
||||
|
||||
Encoding_Converter::Encoding_Converter(const Encoding_Converter& other)
|
||||
{
|
||||
auto err = UErrorCode();
|
||||
cnv = ucnv_safeClone(other.cnv, nullptr, nullptr, &err);
|
||||
}
|
||||
|
||||
auto Encoding_Converter::operator=(const Encoding_Converter& other)
|
||||
-> Encoding_Converter&
|
||||
{
|
||||
this->~Encoding_Converter();
|
||||
auto err = UErrorCode();
|
||||
cnv = ucnv_safeClone(other.cnv, nullptr, nullptr, &err);
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto Encoding_Converter::to_utf8(string_view in, string& out) -> bool
|
||||
{
|
||||
if (ucnv_getType(cnv) == UCNV_UTF8) {
|
||||
if (validate_utf8(in)) {
|
||||
out = in;
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
out.clear();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
auto err = U_ZERO_ERROR;
|
||||
auto len = ucnv_toAlgorithmic(UCNV_UTF8, cnv, out.data(), out.size(),
|
||||
in.data(), in.size(), &err);
|
||||
out.resize(len);
|
||||
if (err == U_BUFFER_OVERFLOW_ERROR) {
|
||||
err = U_ZERO_ERROR;
|
||||
ucnv_toAlgorithmic(UCNV_UTF8, cnv, out.data(), out.size(),
|
||||
in.data(), in.size(), &err);
|
||||
}
|
||||
return U_SUCCESS(err);
|
||||
}
|
||||
|
||||
auto replace_ascii_char(string& s, char from, char to) -> void
|
||||
{
|
||||
for (auto i = s.find(from); i != s.npos; i = s.find(from, i + 1)) {
|
||||
s[i] = to;
|
||||
}
|
||||
}
|
||||
|
||||
auto erase_chars(string& s, string_view erase_chars) -> void
|
||||
{
|
||||
if (erase_chars.empty())
|
||||
return;
|
||||
for (size_t i = 0, next_i = 0; i != size(s); i = next_i) {
|
||||
valid_u8_advance_index(s, next_i);
|
||||
auto enc_cp = string_view(&s[i], next_i - i);
|
||||
if (erase_chars.find(enc_cp) != erase_chars.npos) {
|
||||
s.erase(i, next_i - i);
|
||||
next_i = i;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @brief Tests if word is a number.
|
||||
*
|
||||
* Allow numbers with dot ".", dash "-" or comma "," inbetween the digits, but
|
||||
* forbids double separators such as "..", "--" and ".,".
|
||||
*/
|
||||
auto is_number(string_view s) -> bool
|
||||
{
|
||||
if (s.empty())
|
||||
return false;
|
||||
|
||||
auto it = begin(s);
|
||||
if (s[0] == '-')
|
||||
++it;
|
||||
while (it != end(s)) {
|
||||
auto next = std::find_if(
|
||||
it, end(s), [](auto c) { return c < '0' || c > '9'; });
|
||||
if (next == it)
|
||||
return false;
|
||||
if (next == end(s))
|
||||
return true;
|
||||
it = next;
|
||||
auto c = *it;
|
||||
if (c == '.' || c == ',' || c == '-')
|
||||
++it;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto count_appereances_of(string_view haystack, string_view needles) -> size_t
|
||||
{
|
||||
auto ret = size_t(0);
|
||||
for (size_t i = 0, next_i = 0; i != size(haystack); i = next_i) {
|
||||
valid_u8_advance_index(haystack, next_i);
|
||||
auto enc_cp = string_view(&haystack[i], next_i - i);
|
||||
ret += needles.find(enc_cp) != needles.npos;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace v5
|
||||
} // namespace nuspell
|
||||
@@ -1,228 +0,0 @@
|
||||
/* Copyright 2016-2021 Dimitrij Mijoski
|
||||
*
|
||||
* This file is part of Nuspell.
|
||||
*
|
||||
* Nuspell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Nuspell is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Nuspell. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef NUSPELL_UTILS_HXX
|
||||
#define NUSPELL_UTILS_HXX
|
||||
|
||||
#include "nuspell_export.h"
|
||||
|
||||
#include <clocale>
|
||||
#include <locale>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#if !defined(_WIN32) && \
|
||||
(defined(__unix__) || defined(__unix) || \
|
||||
(defined(__APPLE__) && defined(__MACH__)) || defined(__HAIKU__))
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include <unicode/locid.h>
|
||||
|
||||
#ifdef __GNUC__
|
||||
#define likely(expr) __builtin_expect(!!(expr), 1)
|
||||
#define unlikely(expr) __builtin_expect(!!(expr), 0)
|
||||
#else
|
||||
#define likely(expr) (expr)
|
||||
#define unlikely(expr) (expr)
|
||||
#endif
|
||||
|
||||
struct UConverter; // unicode/ucnv.h
|
||||
|
||||
namespace nuspell {
|
||||
inline namespace v5 {
|
||||
|
||||
auto split(std::string_view s, char sep, std::vector<std::string>& out)
|
||||
-> std::vector<std::string>&;
|
||||
NUSPELL_EXPORT auto split_on_any_of(std::string_view s, const char* sep,
|
||||
std::vector<std::string>& out)
|
||||
-> std::vector<std::string>&;
|
||||
|
||||
NUSPELL_EXPORT auto utf32_to_utf8(std::u32string_view in, std::string& out)
|
||||
-> void;
|
||||
NUSPELL_EXPORT auto utf32_to_utf8(std::u32string_view in) -> std::string;
|
||||
|
||||
auto valid_utf8_to_32(std::string_view in, std::u32string& out) -> void;
|
||||
auto valid_utf8_to_32(std::string_view in) -> std::u32string;
|
||||
|
||||
auto utf8_to_16(std::string_view in) -> std::u16string;
|
||||
auto utf8_to_16(std::string_view in, std::u16string& out) -> bool;
|
||||
|
||||
auto validate_utf8(std::string_view s) -> bool;
|
||||
|
||||
NUSPELL_EXPORT auto is_all_ascii(std::string_view s) -> bool;
|
||||
|
||||
NUSPELL_EXPORT auto latin1_to_ucs2(std::string_view s) -> std::u16string;
|
||||
auto latin1_to_ucs2(std::string_view s, std::u16string& out) -> void;
|
||||
|
||||
NUSPELL_EXPORT auto is_all_bmp(std::u16string_view s) -> bool;
|
||||
|
||||
auto to_upper_ascii(std::string& s) -> void;
|
||||
|
||||
[[nodiscard]] NUSPELL_EXPORT auto to_upper(std::string_view in,
|
||||
const icu::Locale& loc)
|
||||
-> std::string;
|
||||
[[nodiscard]] NUSPELL_EXPORT auto to_title(std::string_view in,
|
||||
const icu::Locale& loc)
|
||||
-> std::string;
|
||||
[[nodiscard]] NUSPELL_EXPORT auto to_lower(std::string_view in,
|
||||
const icu::Locale& loc)
|
||||
-> std::string;
|
||||
|
||||
auto to_upper(std::string_view in, const icu::Locale& loc, std::string& out)
|
||||
-> void;
|
||||
auto to_title(std::string_view in, const icu::Locale& loc, std::string& out)
|
||||
-> void;
|
||||
auto to_lower(std::u32string_view in, const icu::Locale& loc,
|
||||
std::u32string& out) -> void;
|
||||
auto to_lower(std::string_view in, const icu::Locale& loc, std::string& out)
|
||||
-> void;
|
||||
auto to_lower_char_at(std::string& s, size_t i, const icu::Locale& loc) -> void;
|
||||
auto to_title_char_at(std::string& s, size_t i, const icu::Locale& loc) -> void;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @brief Enum that identifies the casing type of a word.
|
||||
*
|
||||
* Neutral characters like numbers are ignored, so "abc" and "abc123abc" are
|
||||
* both classified as small.
|
||||
*/
|
||||
enum class Casing : char {
|
||||
SMALL,
|
||||
INIT_CAPITAL,
|
||||
ALL_CAPITAL,
|
||||
CAMEL /**< @internal camelCase i.e. mixed case with first small */,
|
||||
PASCAL /**< @internal PascalCase i.e. mixed case with first capital */
|
||||
};
|
||||
|
||||
NUSPELL_EXPORT auto classify_casing(std::string_view s) -> Casing;
|
||||
|
||||
auto has_uppercase_at_compound_word_boundary(std::string_view word, size_t i)
|
||||
-> bool;
|
||||
|
||||
class Encoding_Converter {
|
||||
UConverter* cnv = nullptr;
|
||||
|
||||
public:
|
||||
Encoding_Converter() = default;
|
||||
explicit Encoding_Converter(const char* enc);
|
||||
explicit Encoding_Converter(const std::string& enc)
|
||||
: Encoding_Converter(enc.c_str())
|
||||
{
|
||||
}
|
||||
~Encoding_Converter();
|
||||
Encoding_Converter(const Encoding_Converter& other);
|
||||
Encoding_Converter(Encoding_Converter&& other) noexcept
|
||||
{
|
||||
cnv = other.cnv;
|
||||
cnv = nullptr;
|
||||
}
|
||||
auto operator=(const Encoding_Converter& other) -> Encoding_Converter&;
|
||||
auto operator=(Encoding_Converter&& other) noexcept
|
||||
-> Encoding_Converter&
|
||||
{
|
||||
std::swap(cnv, other.cnv);
|
||||
return *this;
|
||||
}
|
||||
auto to_utf8(std::string_view in, std::string& out) -> bool;
|
||||
auto valid() -> bool { return cnv != nullptr; }
|
||||
};
|
||||
|
||||
//#if _POSIX_VERSION >= 200809L
|
||||
#if defined(_POSIX_VERSION) && !defined(__NetBSD__) && !defined(__HAIKU__)
|
||||
class Setlocale_To_C_In_Scope {
|
||||
locale_t old_loc = nullptr;
|
||||
|
||||
public:
|
||||
Setlocale_To_C_In_Scope()
|
||||
: old_loc{uselocale(newlocale(0, "C", nullptr))}
|
||||
{
|
||||
}
|
||||
~Setlocale_To_C_In_Scope()
|
||||
{
|
||||
auto new_loc = uselocale(old_loc);
|
||||
if (new_loc != old_loc)
|
||||
freelocale(new_loc);
|
||||
}
|
||||
Setlocale_To_C_In_Scope(const Setlocale_To_C_In_Scope&) = delete;
|
||||
};
|
||||
#else
|
||||
class Setlocale_To_C_In_Scope {
|
||||
std::string old_name;
|
||||
#ifdef _WIN32
|
||||
int old_per_thread;
|
||||
#endif
|
||||
public:
|
||||
Setlocale_To_C_In_Scope() : old_name(setlocale(LC_ALL, nullptr))
|
||||
{
|
||||
#ifdef _WIN32
|
||||
old_per_thread = _configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
|
||||
#endif
|
||||
auto x = setlocale(LC_ALL, "C");
|
||||
if (!x)
|
||||
old_name.clear();
|
||||
}
|
||||
~Setlocale_To_C_In_Scope()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
_configthreadlocale(old_per_thread);
|
||||
if (old_per_thread == _ENABLE_PER_THREAD_LOCALE)
|
||||
#endif
|
||||
{
|
||||
if (!old_name.empty())
|
||||
setlocale(LC_ALL, old_name.c_str());
|
||||
}
|
||||
}
|
||||
Setlocale_To_C_In_Scope(const Setlocale_To_C_In_Scope&) = delete;
|
||||
};
|
||||
#endif
|
||||
|
||||
auto replace_ascii_char(std::string& s, char from, char to) -> void;
|
||||
auto erase_chars(std::string& s, std::string_view erase_chars) -> void;
|
||||
NUSPELL_EXPORT auto is_number(std::string_view s) -> bool;
|
||||
auto count_appereances_of(std::string_view haystack, std::string_view needles)
|
||||
-> size_t;
|
||||
|
||||
auto inline begins_with(std::string_view haystack, std::string_view needle)
|
||||
-> bool
|
||||
{
|
||||
return haystack.compare(0, needle.size(), needle) == 0;
|
||||
}
|
||||
|
||||
auto inline ends_with(std::string_view haystack, std::string_view needle)
|
||||
-> bool
|
||||
{
|
||||
return haystack.size() >= needle.size() &&
|
||||
haystack.compare(haystack.size() - needle.size(), needle.size(),
|
||||
needle) == 0;
|
||||
}
|
||||
|
||||
template <class T>
|
||||
auto begin_ptr(T& x)
|
||||
{
|
||||
return x.data();
|
||||
}
|
||||
template <class T>
|
||||
auto end_ptr(T& x)
|
||||
{
|
||||
return x.data() + x.size();
|
||||
}
|
||||
} // namespace v5
|
||||
} // namespace nuspell
|
||||
#endif // NUSPELL_UTILS_HXX
|
||||
@@ -21,12 +21,12 @@ std::string utils::j2std_string(JNIEnv *env, jobject jStr) {
|
||||
auto cStr = reinterpret_cast<const char *>(env->GetDirectBufferAddress(jStr));
|
||||
auto size = env->GetDirectBufferCapacity(jStr);
|
||||
std::string stdStr(cStr, size);
|
||||
log_debug("spell j2s", stdStr);
|
||||
utils::log(ANDROID_LOG_DEBUG, "spell j2s", stdStr);
|
||||
return stdStr;
|
||||
}
|
||||
|
||||
jobject utils::std2j_string(JNIEnv *env, const std::string& stdStr) {
|
||||
log_debug("spell s2j", stdStr);
|
||||
utils::log(ANDROID_LOG_DEBUG, "spell s2j", stdStr);
|
||||
size_t byteCount = stdStr.length();
|
||||
auto cStr = stdStr.c_str();
|
||||
auto buffer = env->NewDirectByteBuffer((void *) cStr, byteCount);
|
||||
|
||||
@@ -15,67 +15,61 @@
|
||||
*/
|
||||
|
||||
#include <android/log.h>
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "log.h"
|
||||
|
||||
void utils::log_debug(const std::string &tag, const std::string &msg) {
|
||||
__android_log_print(ANDROID_LOG_DEBUG, tag.c_str(), "%s", msg.c_str());
|
||||
}
|
||||
|
||||
void utils::log_info(const std::string &tag, const std::string &msg) {
|
||||
__android_log_print(ANDROID_LOG_INFO, tag.c_str(), "%s", msg.c_str());
|
||||
}
|
||||
|
||||
void utils::log_warning(const std::string &tag, const std::string &msg) {
|
||||
__android_log_print(ANDROID_LOG_WARN, tag.c_str(), "%s", msg.c_str());
|
||||
}
|
||||
|
||||
void utils::log_error(const std::string &tag, const std::string &msg) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, tag.c_str(), "%s", msg.c_str());
|
||||
}
|
||||
|
||||
void utils::log_wtf(const std::string &tag, const std::string &msg) {
|
||||
__android_log_print(ANDROID_LOG_FATAL, tag.c_str(), "%s", msg.c_str());
|
||||
void utils::log(int log_priority, const std::string &tag, const std::string &msg) {
|
||||
__android_log_print(log_priority, tag.c_str(), "%s", msg.c_str());
|
||||
}
|
||||
|
||||
/**
|
||||
* Code below taken from here:
|
||||
* Code below based on:
|
||||
* https://codelab.wordpress.com/2014/11/03/how-to-use-standard-output-streams-for-logging-in-android-apps/
|
||||
*/
|
||||
static int pfd[2];
|
||||
static pthread_t thr;
|
||||
static const char *tag = "myapp";
|
||||
static bool already_started = false;
|
||||
int utils::start_stdout_stderr_logger(const std::string &app_name) {
|
||||
static bool already_started = false;
|
||||
if (already_started)
|
||||
return 0;
|
||||
|
||||
static void *thread_func(void*) {
|
||||
ssize_t rdsz;
|
||||
char buf[2048];
|
||||
while ((rdsz = read(pfd[0], buf, sizeof buf - 1)) > 0) {
|
||||
if (buf[rdsz - 1] == '\n') --rdsz;
|
||||
buf[rdsz] = 0; /* add null-terminator */
|
||||
__android_log_write(ANDROID_LOG_DEBUG, tag, buf);
|
||||
int piperw[2];
|
||||
if (pipe(piperw) < 0) {
|
||||
std::string msg = "pipe(): ";
|
||||
msg += strerror(errno);
|
||||
utils::log(ANDROID_LOG_ERROR, "stdout/stderr logger", std::ref(msg));
|
||||
return 1;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int utils::start_stdout_stderr_logger(const char *app_name) {
|
||||
if (already_started) return 0;
|
||||
already_started = true;
|
||||
tag = app_name;
|
||||
|
||||
/* make stdout line-buffered and stderr unbuffered */
|
||||
setvbuf(stdout, nullptr, _IOLBF, 0);
|
||||
setvbuf(stderr, nullptr, _IONBF, 0);
|
||||
|
||||
/* create the pipe and redirect stdout and stderr */
|
||||
pipe(pfd);
|
||||
dup2(pfd[1], 1);
|
||||
dup2(pfd[1], 2);
|
||||
dup2(piperw[0], STDIN_FILENO);
|
||||
dup2(piperw[1], STDOUT_FILENO);
|
||||
dup2(piperw[1], STDERR_FILENO);
|
||||
close(piperw[0]);
|
||||
close(piperw[1]);
|
||||
|
||||
auto f = [](const std::string &tag) {
|
||||
std::string buf;
|
||||
while (std::getline(std::cin, buf)) {
|
||||
char &back = buf.back();
|
||||
if (back == '\n')
|
||||
back = '\0';
|
||||
utils::log(ANDROID_LOG_DEBUG, tag, std::ref(buf));
|
||||
}
|
||||
};
|
||||
|
||||
/* spawn the logging thread */
|
||||
if (pthread_create(&thr, nullptr, thread_func, nullptr) != 0) {
|
||||
return -1;
|
||||
}
|
||||
pthread_detach(thr);
|
||||
std::thread thr(f, app_name);
|
||||
thr.detach();
|
||||
|
||||
already_started = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -17,17 +17,14 @@
|
||||
#ifndef FLORISBOARD_LOG_H
|
||||
#define FLORISBOARD_LOG_H
|
||||
|
||||
#include <android/log.h>
|
||||
#include <string>
|
||||
|
||||
namespace utils {
|
||||
|
||||
void log_debug(const std::string& tag, const std::string& msg);
|
||||
void log_info(const std::string& tag, const std::string& msg);
|
||||
void log_warning(const std::string& tag, const std::string& msg);
|
||||
void log_error(const std::string& tag, const std::string& msg);
|
||||
void log_wtf(const std::string& tag, const std::string& msg);
|
||||
void log(int log_priority, const std::string &tag, const std::string &msg);
|
||||
|
||||
int start_stdout_stderr_logger(const char *app_name);
|
||||
int start_stdout_stderr_logger(const std::string &app_name);
|
||||
|
||||
} // namespace utils
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ package dev.patrickgold.florisboard
|
||||
import android.app.Application
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Handler
|
||||
@@ -31,8 +32,6 @@ import dev.patrickgold.florisboard.ime.editor.EditorInstance
|
||||
import dev.patrickgold.florisboard.ime.keyboard.KeyboardManager
|
||||
import dev.patrickgold.florisboard.ime.media.emoji.FlorisEmojiCompat
|
||||
import dev.patrickgold.florisboard.ime.nlp.NlpManager
|
||||
import dev.patrickgold.florisboard.ime.spelling.SpellingManager
|
||||
import dev.patrickgold.florisboard.ime.spelling.SpellingService
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.GlideTypingManager
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
import dev.patrickgold.florisboard.lib.NativeStr
|
||||
@@ -45,9 +44,17 @@ import dev.patrickgold.florisboard.lib.devtools.flogInfo
|
||||
import dev.patrickgold.florisboard.lib.ext.ExtensionManager
|
||||
import dev.patrickgold.florisboard.lib.io.AssetManager
|
||||
import dev.patrickgold.florisboard.lib.io.deleteContentsRecursively
|
||||
import dev.patrickgold.florisboard.lib.io.subFile
|
||||
import dev.patrickgold.florisboard.lib.kotlin.tryOrNull
|
||||
import dev.patrickgold.florisboard.lib.toNativeStr
|
||||
import dev.patrickgold.jetpref.datastore.JetPref
|
||||
import java.io.File
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
* Global weak reference for the [FlorisApplication] class. This is needed as in certain scenarios an application
|
||||
* reference is needed, but the Android framework hasn't finished setting up
|
||||
*/
|
||||
private var FlorisApplicationReference = WeakReference<FlorisApplication?>(null)
|
||||
|
||||
@Suppress("unused")
|
||||
class FlorisApplication : Application() {
|
||||
@@ -75,13 +82,12 @@ class FlorisApplication : Application() {
|
||||
val glideTypingManager = lazy { GlideTypingManager(this) }
|
||||
val keyboardManager = lazy { KeyboardManager(this) }
|
||||
val nlpManager = lazy { NlpManager(this) }
|
||||
val spellingManager = lazy { SpellingManager(this) }
|
||||
val spellingService = lazy { SpellingService(this) }
|
||||
val subtypeManager = lazy { SubtypeManager(this) }
|
||||
val themeManager = lazy { ThemeManager(this) }
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
FlorisApplicationReference = WeakReference(this)
|
||||
try {
|
||||
JetPref.configure(saveIntervalMs = 500)
|
||||
Flog.install(
|
||||
@@ -111,8 +117,8 @@ class FlorisApplication : Application() {
|
||||
fun init() {
|
||||
initICU(this)
|
||||
cacheDir?.deleteContentsRecursively()
|
||||
extensionManager.value.init()
|
||||
prefs.initializeBlocking(this)
|
||||
extensionManager.value.init()
|
||||
clipboardManager.value.initializeForContext(this)
|
||||
DictionaryManager.init(this)
|
||||
}
|
||||
@@ -120,7 +126,7 @@ class FlorisApplication : Application() {
|
||||
fun initICU(context: Context): Boolean {
|
||||
try {
|
||||
val androidAssetManager = context.assets ?: return false
|
||||
val icuTmpDataFile = File(context.cacheDir, "icudt.dat")
|
||||
val icuTmpDataFile = context.cacheDir.subFile("icudt.dat")
|
||||
icuTmpDataFile.outputStream().use { os ->
|
||||
androidAssetManager.open(ICU_DATA_ASSET_PATH).use { it.copyTo(os) }
|
||||
}
|
||||
@@ -154,35 +160,35 @@ class FlorisApplication : Application() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.florisApplication(): FlorisApplication {
|
||||
private tailrec fun Context.florisApplication(): FlorisApplication {
|
||||
return when (this) {
|
||||
is FlorisApplication -> this
|
||||
else -> this.applicationContext as FlorisApplication
|
||||
is ContextWrapper -> when {
|
||||
this.baseContext != null -> this.baseContext.florisApplication()
|
||||
else -> FlorisApplicationReference.get()!!
|
||||
}
|
||||
else -> tryOrNull { this.applicationContext as FlorisApplication } ?: FlorisApplicationReference.get()!!
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.appContext() = lazy { this.florisApplication() }
|
||||
fun Context.appContext() = lazyOf(this.florisApplication())
|
||||
|
||||
fun Context.assetManager() = lazy { this.florisApplication().assetManager.value }
|
||||
fun Context.assetManager() = this.florisApplication().assetManager
|
||||
|
||||
fun Context.cacheManager() = lazy { this.florisApplication().cacheManager.value }
|
||||
fun Context.cacheManager() = this.florisApplication().cacheManager
|
||||
|
||||
fun Context.clipboardManager() = lazy { this.florisApplication().clipboardManager.value }
|
||||
fun Context.clipboardManager() = this.florisApplication().clipboardManager
|
||||
|
||||
fun Context.editorInstance() = lazy { this.florisApplication().editorInstance.value }
|
||||
fun Context.editorInstance() = this.florisApplication().editorInstance
|
||||
|
||||
fun Context.extensionManager() = lazy { this.florisApplication().extensionManager.value }
|
||||
fun Context.extensionManager() = this.florisApplication().extensionManager
|
||||
|
||||
fun Context.glideTypingManager() = lazy { this.florisApplication().glideTypingManager.value }
|
||||
fun Context.glideTypingManager() = this.florisApplication().glideTypingManager
|
||||
|
||||
fun Context.keyboardManager() = lazy { this.florisApplication().keyboardManager.value }
|
||||
fun Context.keyboardManager() = this.florisApplication().keyboardManager
|
||||
|
||||
fun Context.nlpManager() = lazy { this.florisApplication().nlpManager.value }
|
||||
fun Context.nlpManager() = this.florisApplication().nlpManager
|
||||
|
||||
fun Context.spellingManager() = lazy { this.florisApplication().spellingManager.value }
|
||||
fun Context.subtypeManager() = this.florisApplication().subtypeManager
|
||||
|
||||
fun Context.spellingService() = lazy { this.florisApplication().spellingService.value }
|
||||
|
||||
fun Context.subtypeManager() = lazy { this.florisApplication().subtypeManager.value }
|
||||
|
||||
fun Context.themeManager() = lazy { this.florisApplication().themeManager.value }
|
||||
fun Context.themeManager() = this.florisApplication().themeManager
|
||||
|
||||
@@ -70,11 +70,14 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import dev.patrickgold.florisboard.app.FlorisAppActivity
|
||||
import dev.patrickgold.florisboard.app.devtools.DevtoolsOverlay
|
||||
import dev.patrickgold.florisboard.app.florisPreferenceModel
|
||||
import dev.patrickgold.florisboard.ime.ImeUiMode
|
||||
import dev.patrickgold.florisboard.ime.clipboard.ClipboardInputLayout
|
||||
import dev.patrickgold.florisboard.ime.sheet.BottomSheetHostUi
|
||||
import dev.patrickgold.florisboard.ime.sheet.isBottomSheetShowing
|
||||
import dev.patrickgold.florisboard.ime.editor.EditorRange
|
||||
import dev.patrickgold.florisboard.ime.editor.FlorisEditorInfo
|
||||
import dev.patrickgold.florisboard.ime.input.InputFeedbackController
|
||||
@@ -86,8 +89,9 @@ import dev.patrickgold.florisboard.ime.lifecycle.LifecycleInputMethodService
|
||||
import dev.patrickgold.florisboard.ime.media.MediaInputLayout
|
||||
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
|
||||
import dev.patrickgold.florisboard.ime.onehanded.OneHandedPanel
|
||||
import dev.patrickgold.florisboard.ime.smartbar.ExtendedActionsPlacement
|
||||
import dev.patrickgold.florisboard.ime.smartbar.SmartbarLayout
|
||||
import dev.patrickgold.florisboard.ime.text.TextInputLayout
|
||||
import dev.patrickgold.florisboard.ime.text.smartbar.SecondaryRowPlacement
|
||||
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
|
||||
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
|
||||
import dev.patrickgold.florisboard.lib.android.AndroidInternalR
|
||||
@@ -105,6 +109,7 @@ import dev.patrickgold.florisboard.lib.devtools.LogTopic
|
||||
import dev.patrickgold.florisboard.lib.devtools.flogError
|
||||
import dev.patrickgold.florisboard.lib.devtools.flogInfo
|
||||
import dev.patrickgold.florisboard.lib.devtools.flogWarning
|
||||
import dev.patrickgold.florisboard.lib.kotlin.collectLatestIn
|
||||
import dev.patrickgold.florisboard.lib.observeAsTransformingState
|
||||
import dev.patrickgold.florisboard.lib.snygg.ui.SnyggSurface
|
||||
import dev.patrickgold.florisboard.lib.snygg.ui.shape
|
||||
@@ -265,7 +270,7 @@ class FlorisImeService : LifecycleInputMethodService() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
FlorisImeServiceReference = WeakReference(this)
|
||||
subtypeManager.activeSubtype.observe(this) { subtype ->
|
||||
subtypeManager.activeSubtypeFlow.collectLatestIn(lifecycleScope) { subtype ->
|
||||
val config = Configuration(resources.configuration)
|
||||
config.setLocale(subtype.primaryLocale)
|
||||
resourcesContext = createConfigurationContext(config)
|
||||
@@ -324,7 +329,7 @@ class FlorisImeService : LifecycleInputMethodService() {
|
||||
activeState.batchEdit {
|
||||
activeState.imeUiMode = ImeUiMode.TEXT
|
||||
activeState.isSelectionMode = editorInfo.initialSelection.isSelectionMode
|
||||
editorInstance.handleStartInputView(editorInfo)
|
||||
editorInstance.handleStartInputView(editorInfo, isRestart = restarting)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,6 +388,10 @@ class FlorisImeService : LifecycleInputMethodService() {
|
||||
flogInfo(LogTopic.IMS_EVENTS)
|
||||
}
|
||||
isWindowShown = false
|
||||
activeState.batchEdit {
|
||||
activeState.isActionsOverflowVisible = false
|
||||
activeState.isActionsEditorVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEvaluateFullscreenMode(): Boolean {
|
||||
@@ -401,7 +410,7 @@ class FlorisImeService : LifecycleInputMethodService() {
|
||||
|
||||
override fun onUpdateExtractingVisibility(info: EditorInfo?) {
|
||||
if (info != null) {
|
||||
editorInstance.handleStartInputView(FlorisEditorInfo.wrap(info))
|
||||
editorInstance.handleStartInputView(FlorisEditorInfo.wrap(info), isRestart = true)
|
||||
}
|
||||
when (prefs.keyboard.landscapeInputUiMode.get()) {
|
||||
LandscapeInputUiMode.DYNAMICALLY_SHOW -> super.onUpdateExtractingVisibility(info)
|
||||
@@ -462,16 +471,18 @@ class FlorisImeService : LifecycleInputMethodService() {
|
||||
val visibleTopY = inputWindowView.height - inputViewSize.height
|
||||
val needAdditionalOverlay =
|
||||
prefs.smartbar.enabled.get() &&
|
||||
prefs.smartbar.secondaryActionsEnabled.get() &&
|
||||
prefs.smartbar.secondaryActionsExpanded.get() &&
|
||||
prefs.smartbar.secondaryActionsPlacement.get() == SecondaryRowPlacement.OVERLAY_APP_UI &&
|
||||
prefs.smartbar.layout.get() == SmartbarLayout.SUGGESTIONS_ACTIONS_EXTENDED &&
|
||||
prefs.smartbar.extendedActionsExpanded.get() &&
|
||||
prefs.smartbar.extendedActionsPlacement.get() == ExtendedActionsPlacement.OVERLAY_APP_UI &&
|
||||
keyboardManager.activeState.imeUiMode == ImeUiMode.TEXT
|
||||
|
||||
outInsets.contentTopInsets = visibleTopY
|
||||
outInsets.visibleTopInsets = visibleTopY
|
||||
outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_REGION
|
||||
val left = 0
|
||||
val top = visibleTopY - if (needAdditionalOverlay) FlorisImeSizing.Static.smartbarHeightPx else 0
|
||||
val top = if (keyboardManager.activeState.isBottomSheetShowing()) { 0 } else {
|
||||
visibleTopY - if (needAdditionalOverlay) FlorisImeSizing.Static.smartbarHeightPx else 0
|
||||
}
|
||||
val right = inputViewSize.width
|
||||
val bottom = inputWindowView.height
|
||||
outInsets.touchableRegion.set(left, top, right, bottom)
|
||||
@@ -531,6 +542,7 @@ class FlorisImeService : LifecycleInputMethodService() {
|
||||
}
|
||||
ImeUi()
|
||||
}
|
||||
BottomSheetHostUi()
|
||||
SystemUiIme()
|
||||
}
|
||||
}
|
||||
@@ -548,7 +560,9 @@ class FlorisImeService : LifecycleInputMethodService() {
|
||||
)
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
SideEffect {
|
||||
keyboardManager.activeState.layoutDirection = layoutDirection
|
||||
if (keyboardManager.activeState.layoutDirection != layoutDirection) {
|
||||
keyboardManager.activeState.layoutDirection = layoutDirection
|
||||
}
|
||||
}
|
||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||
SnyggSurface(
|
||||
|
||||
@@ -21,19 +21,21 @@ import android.view.textservice.SentenceSuggestionsInfo
|
||||
import android.view.textservice.SuggestionsInfo
|
||||
import android.view.textservice.TextInfo
|
||||
import dev.patrickgold.florisboard.app.florisPreferenceModel
|
||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||
import dev.patrickgold.florisboard.ime.dictionary.DictionaryManager
|
||||
import dev.patrickgold.florisboard.ime.spelling.SpellingLanguageMode
|
||||
import dev.patrickgold.florisboard.ime.spelling.SpellingService
|
||||
import dev.patrickgold.florisboard.ime.nlp.SpellingResult
|
||||
import dev.patrickgold.florisboard.ime.nlp.SpellingLanguageMode
|
||||
import dev.patrickgold.florisboard.lib.FlorisLocale
|
||||
import dev.patrickgold.florisboard.lib.devtools.LogTopic
|
||||
import dev.patrickgold.florisboard.lib.devtools.flogInfo
|
||||
import dev.patrickgold.florisboard.lib.kotlin.map
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class FlorisSpellCheckerService : SpellCheckerService() {
|
||||
private val prefs by florisPreferenceModel()
|
||||
private val dictionaryManager get() = DictionaryManager.default()
|
||||
private val spellingManager by spellingManager()
|
||||
private val spellingService by spellingService()
|
||||
private val nlpManager by nlpManager()
|
||||
private val subtypeManager by subtypeManager()
|
||||
|
||||
override fun onCreate() {
|
||||
@@ -56,7 +58,7 @@ class FlorisSpellCheckerService : SpellCheckerService() {
|
||||
}
|
||||
|
||||
private inner class FlorisSpellCheckerSession : Session() {
|
||||
private var cachedSpellingLocale: FlorisLocale? = null
|
||||
private var cachedSpellingSubtype: Subtype? = null
|
||||
|
||||
override fun onCreate() {
|
||||
flogInfo(LogTopic.SPELL_EVENTS) { "Session requested locale: $locale" }
|
||||
@@ -65,33 +67,36 @@ class FlorisSpellCheckerService : SpellCheckerService() {
|
||||
}
|
||||
|
||||
private fun setupSpellingIfNecessary() {
|
||||
val evaluatedLocale = when (prefs.spelling.languageMode.get()) {
|
||||
val evaluatedSubtype = when (prefs.spelling.languageMode.get()) {
|
||||
SpellingLanguageMode.USE_KEYBOARD_SUBTYPES -> {
|
||||
subtypeManager.activeSubtype().primaryLocale
|
||||
subtypeManager.activeSubtype
|
||||
}
|
||||
else -> {
|
||||
FlorisLocale.default()
|
||||
Subtype.DEFAULT.copy(primaryLocale = FlorisLocale.default())
|
||||
}
|
||||
}
|
||||
|
||||
if (evaluatedLocale != cachedSpellingLocale) {
|
||||
cachedSpellingLocale = evaluatedLocale
|
||||
if (evaluatedSubtype != cachedSpellingSubtype) {
|
||||
cachedSpellingSubtype = evaluatedSubtype
|
||||
nlpManager.preload(evaluatedSubtype)
|
||||
}
|
||||
flogInfo(LogTopic.SPELL_EVENTS) {
|
||||
"Session actual locale: ${cachedSpellingSubtype?.primaryLocale?.languageTag()}"
|
||||
}
|
||||
flogInfo(LogTopic.SPELL_EVENTS) { "Session actual locale: ${cachedSpellingLocale?.languageTag()}" }
|
||||
}
|
||||
|
||||
private fun spellMultiple(
|
||||
spellingLocale: FlorisLocale,
|
||||
spellingSubtype: Subtype,
|
||||
textInfos: Array<out TextInfo>,
|
||||
suggestionsLimit: Int,
|
||||
): Array<SuggestionsInfo> = runBlocking {
|
||||
): Array<SpellingResult> = runBlocking {
|
||||
val retInfos = Array(textInfos.size) { n ->
|
||||
val word = textInfos[n].text ?: ""
|
||||
spellingService.spellAsync(spellingLocale, word, suggestionsLimit)
|
||||
async { nlpManager.spell(spellingSubtype, word, emptyList(), emptyList(), suggestionsLimit) }
|
||||
}
|
||||
Array(textInfos.size) { n ->
|
||||
retInfos[n].await().apply {
|
||||
setCookieAndSequence(textInfos[n].cookie, textInfos[n].sequence)
|
||||
suggestionsInfo.setCookieAndSequence(textInfos[n].cookie, textInfos[n].sequence)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,13 +104,16 @@ class FlorisSpellCheckerService : SpellCheckerService() {
|
||||
override fun onGetSuggestions(textInfo: TextInfo?, suggestionsLimit: Int): SuggestionsInfo {
|
||||
flogInfo(LogTopic.SPELL_EVENTS) { "text=${textInfo?.text}, limit=$suggestionsLimit" }
|
||||
|
||||
textInfo?.text ?: return SpellingService.emptySuggestionsInfo()
|
||||
textInfo?.text ?: return SpellingResult.unspecified().suggestionsInfo
|
||||
setupSpellingIfNecessary()
|
||||
val spellingLocale = cachedSpellingLocale ?: return SpellingService.emptySuggestionsInfo()
|
||||
val spellingSubtype = cachedSpellingSubtype ?: return SpellingResult.unspecified().suggestionsInfo
|
||||
|
||||
return spellingService
|
||||
.spell(spellingLocale, textInfo.text, suggestionsLimit)
|
||||
.sendToDebugOverlayIfEnabled(textInfo)
|
||||
return runBlocking {
|
||||
nlpManager
|
||||
.spell(spellingSubtype, textInfo.text, emptyList(), emptyList(), suggestionsLimit)
|
||||
.sendToDebugOverlayIfEnabled(textInfo)
|
||||
.suggestionsInfo
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGetSuggestionsMultiple(
|
||||
@@ -117,9 +125,11 @@ class FlorisSpellCheckerService : SpellCheckerService() {
|
||||
|
||||
textInfos ?: return emptyArray()
|
||||
setupSpellingIfNecessary()
|
||||
val spellingLocale = cachedSpellingLocale ?: return emptyArray()
|
||||
val spellingSubtype = cachedSpellingSubtype ?: return emptyArray()
|
||||
|
||||
return spellMultiple(spellingLocale, textInfos, suggestionsLimit).sendToDebugOverlayIfEnabled(textInfos)
|
||||
return spellMultiple(spellingSubtype, textInfos, suggestionsLimit)
|
||||
.sendToDebugOverlayIfEnabled(textInfos)
|
||||
.map { it.suggestionsInfo }
|
||||
}
|
||||
|
||||
override fun onGetSentenceSuggestionsMultiple(
|
||||
@@ -137,7 +147,7 @@ class FlorisSpellCheckerService : SpellCheckerService() {
|
||||
|
||||
super.onCancel()
|
||||
if (prefs.devtools.showSpellingOverlay.get()) {
|
||||
spellingManager.clearDebugOverlay()
|
||||
nlpManager.clearDebugOverlay()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,25 +156,25 @@ class FlorisSpellCheckerService : SpellCheckerService() {
|
||||
|
||||
super.onClose()
|
||||
if (prefs.devtools.showSpellingOverlay.get()) {
|
||||
spellingManager.clearDebugOverlay()
|
||||
nlpManager.clearDebugOverlay()
|
||||
}
|
||||
}
|
||||
|
||||
fun SuggestionsInfo.sendToDebugOverlayIfEnabled(
|
||||
fun SpellingResult.sendToDebugOverlayIfEnabled(
|
||||
textInfo: TextInfo,
|
||||
): SuggestionsInfo {
|
||||
): SpellingResult {
|
||||
if (prefs.devtools.showSpellingOverlay.get()) {
|
||||
spellingManager.addToDebugOverlay(textInfo.text, this)
|
||||
nlpManager.addToDebugOverlay(textInfo.text, this)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun Array<SuggestionsInfo>.sendToDebugOverlayIfEnabled(
|
||||
fun Array<SpellingResult>.sendToDebugOverlayIfEnabled(
|
||||
textInfos: Array<out TextInfo>,
|
||||
): Array<SuggestionsInfo> {
|
||||
): Array<SpellingResult> {
|
||||
if (prefs.devtools.showSpellingOverlay.get()) {
|
||||
for ((n, info) in this.withIndex()) {
|
||||
spellingManager.addToDebugOverlay(textInfos[n].text, info)
|
||||
nlpManager.addToDebugOverlay(textInfos[n].text, info)
|
||||
}
|
||||
}
|
||||
return this
|
||||
|
||||
@@ -23,19 +23,23 @@ import dev.patrickgold.florisboard.app.settings.theme.DisplayColorsAs
|
||||
import dev.patrickgold.florisboard.app.settings.theme.DisplayKbdAfterDialogs
|
||||
import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn
|
||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||
import dev.patrickgold.florisboard.ime.input.HapticVibrationMode
|
||||
import dev.patrickgold.florisboard.ime.input.InputFeedbackActivationMode
|
||||
import dev.patrickgold.florisboard.ime.keyboard.IncognitoMode
|
||||
import dev.patrickgold.florisboard.ime.landscapeinput.LandscapeInputUiMode
|
||||
import dev.patrickgold.florisboard.ime.media.emoji.EmojiHairStyle
|
||||
import dev.patrickgold.florisboard.ime.media.emoji.EmojiRecentlyUsedHelper
|
||||
import dev.patrickgold.florisboard.ime.media.emoji.EmojiSkinTone
|
||||
import dev.patrickgold.florisboard.ime.nlp.SpellingLanguageMode
|
||||
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
|
||||
import dev.patrickgold.florisboard.ime.spelling.SpellingLanguageMode
|
||||
import dev.patrickgold.florisboard.ime.smartbar.CandidatesDisplayMode
|
||||
import dev.patrickgold.florisboard.ime.smartbar.ExtendedActionsPlacement
|
||||
import dev.patrickgold.florisboard.ime.smartbar.SmartbarLayout
|
||||
import dev.patrickgold.florisboard.ime.smartbar.quickaction.QuickActionArrangement
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyHintConfiguration
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
|
||||
import dev.patrickgold.florisboard.ime.text.key.UtilityKeyAction
|
||||
import dev.patrickgold.florisboard.ime.text.smartbar.CandidatesDisplayMode
|
||||
import dev.patrickgold.florisboard.ime.text.smartbar.SecondaryRowPlacement
|
||||
import dev.patrickgold.florisboard.ime.text.smartbar.SmartbarRowType
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeMode
|
||||
import dev.patrickgold.florisboard.ime.theme.extCoreTheme
|
||||
import dev.patrickgold.florisboard.lib.android.isOrientationPortrait
|
||||
@@ -44,7 +48,9 @@ import dev.patrickgold.florisboard.lib.observeAsTransformingState
|
||||
import dev.patrickgold.florisboard.lib.snygg.SnyggLevel
|
||||
import dev.patrickgold.florisboard.lib.util.VersionName
|
||||
import dev.patrickgold.jetpref.datastore.JetPref
|
||||
import dev.patrickgold.jetpref.datastore.model.PreferenceMigrationEntry
|
||||
import dev.patrickgold.jetpref.datastore.model.PreferenceModel
|
||||
import dev.patrickgold.jetpref.datastore.model.PreferenceType
|
||||
import dev.patrickgold.jetpref.datastore.model.observeAsState
|
||||
|
||||
fun florisPreferenceModel() = JetPref.getOrCreatePreferenceModel(AppPrefs::class, ::AppPrefs)
|
||||
@@ -64,8 +70,13 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
|
||||
key = "advanced__show_app_icon",
|
||||
default = true,
|
||||
)
|
||||
val forcePrivateMode = boolean(
|
||||
key = "advanced__force_private_mode",
|
||||
val incognitoMode = enum(
|
||||
key = "advanced__incognito_mode",
|
||||
default = IncognitoMode.DYNAMIC_ON_OFF,
|
||||
)
|
||||
// Internal pref
|
||||
val forceIncognitoModeFromDynamic = boolean(
|
||||
key = "advanced__force_incognito_mode_from_dynamic",
|
||||
default = false,
|
||||
)
|
||||
}
|
||||
@@ -116,6 +127,10 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
|
||||
key = "correction__auto_capitalization",
|
||||
default = true,
|
||||
)
|
||||
val autoSpacePunctuation = boolean(
|
||||
key = "correction__auto_space_punctuation",
|
||||
default = false,
|
||||
)
|
||||
val doubleSpacePeriod = boolean(
|
||||
key = "correction__double_space_period",
|
||||
default = true,
|
||||
@@ -152,6 +167,10 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
|
||||
key = "devtools__show_touch_boundaries",
|
||||
default = false,
|
||||
)
|
||||
val showDragAndDropHelpers = boolean(
|
||||
key = "devtools__show_drag_and_drop_helpers",
|
||||
default = false,
|
||||
)
|
||||
}
|
||||
|
||||
val dictionary = Dictionary()
|
||||
@@ -252,9 +271,9 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
|
||||
key = "input_feedback__audio_enabled",
|
||||
default = true,
|
||||
)
|
||||
val audioIgnoreSystemSettings = boolean(
|
||||
key = "input_feedback__audio_ignore_system_settings",
|
||||
default = false,
|
||||
val audioActivationMode = enum(
|
||||
key = "input_feedback__audio_activation_mode",
|
||||
default = InputFeedbackActivationMode.RESPECT_SYSTEM_SETTINGS,
|
||||
)
|
||||
val audioVolume = int(
|
||||
key = "input_feedback__audio_volume",
|
||||
@@ -285,13 +304,13 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
|
||||
key = "input_feedback__haptic_enabled",
|
||||
default = true,
|
||||
)
|
||||
val hapticIgnoreSystemSettings = boolean(
|
||||
key = "input_feedback__haptic_ignore_system_settings",
|
||||
default = false,
|
||||
val hapticActivationMode = enum(
|
||||
key = "input_feedback__haptic_activation_mode",
|
||||
default = InputFeedbackActivationMode.RESPECT_SYSTEM_SETTINGS,
|
||||
)
|
||||
val hapticUseVibrator = boolean(
|
||||
key = "input_feedback__haptic_use_vibrator",
|
||||
default = true,
|
||||
val hapticVibrationMode = enum(
|
||||
key = "input_feedback__haptic_vibration_mode",
|
||||
default = HapticVibrationMode.USE_VIBRATOR_DIRECTLY,
|
||||
)
|
||||
val hapticVibrationDuration = int(
|
||||
key = "input_feedback__haptic_vibration_duration",
|
||||
@@ -326,7 +345,7 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
|
||||
val internal = Internal()
|
||||
inner class Internal {
|
||||
val homeIsBetaToolboxCollapsed = boolean(
|
||||
key = "internal__home_is_beta_toolbox_collapsed_0316",
|
||||
key = "internal__home_is_beta_toolbox_collapsed_040a01",
|
||||
default = false,
|
||||
)
|
||||
val isImeSetUp = boolean(
|
||||
@@ -518,45 +537,38 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
|
||||
key = "smartbar__enabled",
|
||||
default = true,
|
||||
)
|
||||
val layout = enum(
|
||||
key = "smartbar__layout",
|
||||
default = SmartbarLayout.SUGGESTIONS_ACTIONS_SHARED,
|
||||
)
|
||||
val actionArrangement = custom(
|
||||
key = "smartbar__action_arrangement",
|
||||
default = QuickActionArrangement.Default,
|
||||
serializer = QuickActionArrangement.Serializer,
|
||||
)
|
||||
val flipToggles = boolean(
|
||||
key = "smartbar__flip_toggles",
|
||||
default = false,
|
||||
)
|
||||
val primaryActionsExpanded = boolean(
|
||||
key = "smartbar__primary_actions_expanded",
|
||||
val sharedActionsExpanded = boolean(
|
||||
key = "smartbar__shared_actions_expanded",
|
||||
default = false,
|
||||
)
|
||||
val primaryActionsRowType = enum(
|
||||
key = "smartbar__primary_actions_row_type",
|
||||
default = SmartbarRowType.QUICK_ACTIONS,
|
||||
)
|
||||
val primaryActionsAutoExpandCollapse = boolean(
|
||||
key = "smartbar__primary_actions_auto_expand_collapse",
|
||||
val sharedActionsAutoExpandCollapse = boolean(
|
||||
key = "smartbar__shared_actions_auto_expand_collapse",
|
||||
default = true,
|
||||
)
|
||||
val primaryActionsExpandWithAnimation = boolean(
|
||||
key = "smartbar__primary_actions_expand_with_animation",
|
||||
val sharedActionsExpandWithAnimation = boolean(
|
||||
key = "smartbar__shared_actions_expand_with_animation",
|
||||
default = true,
|
||||
)
|
||||
val secondaryActionsEnabled = boolean(
|
||||
key = "smartbar__secondary_actions_enabled",
|
||||
default = true,
|
||||
)
|
||||
val secondaryActionsExpanded = boolean(
|
||||
key = "smartbar__secondary_actions_expanded",
|
||||
val extendedActionsExpanded = boolean(
|
||||
key = "smartbar__extended_actions_expanded",
|
||||
default = false,
|
||||
)
|
||||
val secondaryActionsPlacement = enum(
|
||||
key = "smartbar__secondary_actions_placement",
|
||||
default = SecondaryRowPlacement.ABOVE_PRIMARY,
|
||||
)
|
||||
val secondaryActionsRowType = enum(
|
||||
key = "smartbar__secondary_actions_row_type",
|
||||
default = SmartbarRowType.CLIPBOARD_CURSOR_TOOLS,
|
||||
)
|
||||
val quickActions = string(
|
||||
key = "smartbar__quick_actions",
|
||||
default = "[]",
|
||||
val extendedActionsPlacement = enum(
|
||||
key = "smartbar__extended_actions_placement",
|
||||
default = ExtendedActionsPlacement.ABOVE_CANDIDATES,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -590,10 +602,6 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
|
||||
key = "suggestion__display_mode",
|
||||
default = CandidatesDisplayMode.DYNAMIC_SCROLLABLE,
|
||||
)
|
||||
val usePrevWords = boolean(
|
||||
key = "suggestion__use_prev_words",
|
||||
default = true,
|
||||
)
|
||||
val blockPossiblyOffensive = boolean(
|
||||
key = "suggestion__block_possibly_offensive",
|
||||
default = true,
|
||||
@@ -604,7 +612,7 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
|
||||
)
|
||||
val clipboardContentTimeout = int(
|
||||
key = "suggestion__clipboard_content_timeout",
|
||||
default = 30,
|
||||
default = 60,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -653,4 +661,41 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
|
||||
default = SnyggLevel.ADVANCED,
|
||||
)
|
||||
}
|
||||
|
||||
override fun migrate(entry: PreferenceMigrationEntry): PreferenceMigrationEntry {
|
||||
return when (entry.key) {
|
||||
// Migrate enums from their lowercase to uppercase representation
|
||||
// Keep migration rule until: 0.5 dev cycle
|
||||
"advanced__settings_theme", "gestures__swipe_up", "gestures__swipe_down", "gestures__swipe_left",
|
||||
"gestures__swipe_right", "gestures__space_bar_swipe_up", "gestures__space_bar_swipe_left",
|
||||
"gestures__space_bar_swipe_right", "gestures__space_bar_long_press", "gestures__delete_key_swipe_left",
|
||||
"gestures__delete_key_long_press", "keyboard__hinted_number_row_mode", "keyboard__hinted_symbols_mode",
|
||||
"keyboard__utility_key_action", "keyboard__one_handed_mode", "keyboard__landscape_input_ui_mode",
|
||||
"localization__display_language_names_in", "media__emoji_preferred_skin_tone",
|
||||
"media__emoji_preferred_hair_style", "smartbar__primary_actions_row_type",
|
||||
"smartbar__secondary_actions_placement", "smartbar__secondary_actions_row_type", "spelling__language_mode",
|
||||
"suggestion__display_mode", "theme__mode", "theme__editor_display_colors_as",
|
||||
"theme__editor_display_kbd_after_dialogs", "theme__editor_level",
|
||||
-> {
|
||||
entry.transform(rawValue = entry.rawValue.uppercase())
|
||||
}
|
||||
|
||||
// Migrate old private mode force flag as this is a sensitive preference
|
||||
// Keep migration rule until: 0.5 dev cycle
|
||||
"advanced__force_private_mode" -> {
|
||||
if (entry.rawValue.toBoolean()) {
|
||||
entry.transform(
|
||||
type = PreferenceType.string(),
|
||||
key = "advanced__incognito_mode",
|
||||
rawValue = IncognitoMode.FORCE_ON.toString(),
|
||||
)
|
||||
} else {
|
||||
entry.reset()
|
||||
}
|
||||
}
|
||||
|
||||
// Default: keep entry
|
||||
else -> entry.keepAsIs()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -36,9 +39,6 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.google.accompanist.insets.ProvideWindowInsets
|
||||
import com.google.accompanist.insets.navigationBarsWithImePadding
|
||||
import com.google.accompanist.insets.statusBarsPadding
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.apptheme.FlorisAppTheme
|
||||
import dev.patrickgold.florisboard.lib.FlorisLocale
|
||||
@@ -53,6 +53,7 @@ import dev.patrickgold.florisboard.lib.compose.SystemUiApp
|
||||
import dev.patrickgold.florisboard.lib.compose.rememberPreviewFieldController
|
||||
import dev.patrickgold.florisboard.lib.compose.stringRes
|
||||
import dev.patrickgold.florisboard.lib.util.AppVersionUtils
|
||||
import dev.patrickgold.jetpref.datastore.model.observeAsState
|
||||
import dev.patrickgold.jetpref.datastore.ui.ProvideDefaultDialogPrefStrings
|
||||
|
||||
enum class AppTheme(val id: String) {
|
||||
@@ -74,14 +75,13 @@ class FlorisAppActivity : ComponentActivity() {
|
||||
private var resourcesContext by mutableStateOf(this as Context)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
installSplashScreen()
|
||||
|
||||
prefs.datastoreReadyStatus.observe(this) { ready ->
|
||||
if (ready) {
|
||||
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
|
||||
}
|
||||
// Splash screen should be installed before calling super.onCreate()
|
||||
installSplashScreen().apply {
|
||||
setKeepOnScreenCondition { !prefs.datastoreReadyStatus.get() }
|
||||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
prefs.advanced.settingsTheme.observe(this) {
|
||||
appTheme = it
|
||||
}
|
||||
@@ -96,12 +96,13 @@ class FlorisAppActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
setContent {
|
||||
ProvideLocalizedResources(resourcesContext) {
|
||||
FlorisAppTheme(theme = appTheme) {
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = false) {
|
||||
// We defer the setContent call until the datastore model is loaded, until then the splash screen stays drawn
|
||||
prefs.datastoreReadyStatus.observe(this) { isModelLoaded ->
|
||||
if (!isModelLoaded) return@observe
|
||||
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
|
||||
setContent {
|
||||
ProvideLocalizedResources(resourcesContext) {
|
||||
FlorisAppTheme(theme = appTheme) {
|
||||
Surface(color = MaterialTheme.colors.background) {
|
||||
SystemUiApp()
|
||||
AppContent()
|
||||
@@ -123,8 +124,6 @@ class FlorisAppActivity : ComponentActivity() {
|
||||
} else {
|
||||
this.hideAppIcon()
|
||||
}
|
||||
} else {
|
||||
this.showAppIcon()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +132,8 @@ class FlorisAppActivity : ComponentActivity() {
|
||||
val navController = rememberNavController()
|
||||
val previewFieldController = rememberPreviewFieldController()
|
||||
|
||||
val isImeSetUp by prefs.internal.isImeSetUp.observeAsState()
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalNavController provides navController,
|
||||
LocalPreviewFieldController provides previewFieldController,
|
||||
@@ -145,12 +146,13 @@ class FlorisAppActivity : ComponentActivity() {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.statusBarsPadding()
|
||||
.navigationBarsWithImePadding(),
|
||||
.navigationBarsPadding()
|
||||
.imePadding(),
|
||||
) {
|
||||
Routes.AppNavHost(
|
||||
modifier = Modifier.weight(1.0f),
|
||||
navController = navController,
|
||||
startDestination = Routes.Splash.Screen,
|
||||
startDestination = if (isImeSetUp) Routes.Settings.Home else Routes.Setup.Screen,
|
||||
)
|
||||
PreviewKeyboardField(previewFieldController)
|
||||
}
|
||||
|
||||
@@ -49,24 +49,15 @@ import dev.patrickgold.florisboard.app.settings.localization.SelectLocaleScreen
|
||||
import dev.patrickgold.florisboard.app.settings.localization.SubtypeEditorScreen
|
||||
import dev.patrickgold.florisboard.app.settings.media.MediaScreen
|
||||
import dev.patrickgold.florisboard.app.settings.smartbar.SmartbarScreen
|
||||
import dev.patrickgold.florisboard.app.settings.spelling.ImportSpellingArchiveScreen
|
||||
import dev.patrickgold.florisboard.app.settings.spelling.ManageSpellingDictsScreen
|
||||
import dev.patrickgold.florisboard.app.settings.spelling.SpellingInfoScreen
|
||||
import dev.patrickgold.florisboard.app.settings.spelling.SpellingScreen
|
||||
import dev.patrickgold.florisboard.app.settings.theme.ThemeManagerScreen
|
||||
import dev.patrickgold.florisboard.app.settings.theme.ThemeManagerScreenAction
|
||||
import dev.patrickgold.florisboard.app.settings.theme.ThemeScreen
|
||||
import dev.patrickgold.florisboard.app.settings.typing.TypingScreen
|
||||
import dev.patrickgold.florisboard.app.setup.SetupScreen
|
||||
import dev.patrickgold.florisboard.app.splash.SplashScreen
|
||||
import dev.patrickgold.florisboard.lib.kotlin.curlyFormat
|
||||
|
||||
@Suppress("FunctionName")
|
||||
object Routes {
|
||||
object Splash {
|
||||
const val Screen = "splash"
|
||||
}
|
||||
|
||||
object Setup {
|
||||
const val Screen = "setup"
|
||||
}
|
||||
@@ -91,12 +82,6 @@ object Routes {
|
||||
|
||||
const val Typing = "settings/typing"
|
||||
|
||||
const val Spelling = "settings/spelling"
|
||||
const val SpellingInfo = "settings/spelling/info"
|
||||
const val ManageSpellingDicts = "settings/spelling/manage-dicts"
|
||||
const val ImportSpellingArchive = "settings/spelling/import-archive"
|
||||
const val ImportSpellingAffDic = "settings/spelling/import-aff-dic"
|
||||
|
||||
const val Dictionary = "settings/dictionary"
|
||||
const val UserDictionary = "settings/dictionary/user-dictionary/{type}"
|
||||
fun UserDictionary(type: UserDictionaryType) = UserDictionary.curlyFormat("type" to type.id)
|
||||
@@ -156,8 +141,6 @@ object Routes {
|
||||
navController = navController,
|
||||
startDestination = startDestination,
|
||||
) {
|
||||
composable(Splash.Screen) { SplashScreen() }
|
||||
|
||||
composable(Setup.Screen) { SetupScreen() }
|
||||
|
||||
composable(Settings.Home) { HomeScreen() }
|
||||
@@ -185,11 +168,6 @@ object Routes {
|
||||
|
||||
composable(Settings.Typing) { TypingScreen() }
|
||||
|
||||
composable(Settings.Spelling) { SpellingScreen() }
|
||||
composable(Settings.SpellingInfo) { SpellingInfoScreen() }
|
||||
composable(Settings.ManageSpellingDicts) { ManageSpellingDictsScreen() }
|
||||
composable(Settings.ImportSpellingArchive) { ImportSpellingArchiveScreen() }
|
||||
|
||||
composable(Settings.Dictionary) { DictionaryScreen() }
|
||||
composable(Settings.UserDictionary) { navBackStack ->
|
||||
val type = navBackStack.arguments?.getString("type")?.let { typeId ->
|
||||
|
||||
@@ -26,11 +26,17 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn
|
||||
import dev.patrickgold.florisboard.lib.android.showLongToast
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisIconButton
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
|
||||
import dev.patrickgold.florisboard.lib.compose.stringRes
|
||||
import dev.patrickgold.florisboard.lib.io.subDir
|
||||
import dev.patrickgold.florisboard.lib.io.subFile
|
||||
import dev.patrickgold.jetpref.datastore.model.observeAsState
|
||||
import java.util.*
|
||||
|
||||
@@ -39,8 +45,38 @@ fun AndroidLocalesScreen() = FlorisScreen {
|
||||
title = stringRes(R.string.devtools__android_locales__title)
|
||||
scrollable = false
|
||||
|
||||
val context = LocalContext.current
|
||||
val availableLocales = remember { Locale.getAvailableLocales().sortedBy { it.toLanguageTag() } }
|
||||
|
||||
actions {
|
||||
FlorisIconButton(
|
||||
onClick = {
|
||||
try {
|
||||
val devtoolsDir = context.noBackupFilesDir.subDir("devtools")
|
||||
devtoolsDir.mkdirs()
|
||||
val txtFile = devtoolsDir.subFile("system_locales.tsv")
|
||||
txtFile.bufferedWriter().use { out ->
|
||||
for (locale in availableLocales) {
|
||||
out.append(locale.toLanguageTag())
|
||||
out.append('\t')
|
||||
out.append(locale.getDisplayName(Locale.ENGLISH))
|
||||
out.append('\t')
|
||||
out.append(locale.getDisplayName(locale))
|
||||
out.appendLine()
|
||||
}
|
||||
}
|
||||
context.showLongToast("Exported available system locales to \"${txtFile.path}\"")
|
||||
} catch (e: Exception) {
|
||||
context.showLongToast(
|
||||
R.string.error__snackbar_message_template,
|
||||
"error_message" to e.message.toString(),
|
||||
)
|
||||
}
|
||||
},
|
||||
icon = painterResource(R.drawable.ic_save),
|
||||
)
|
||||
}
|
||||
|
||||
content {
|
||||
val displayLanguageNamesIn by prefs.localization.displayLanguageNamesIn.observeAsState()
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package dev.patrickgold.florisboard.app.devtools
|
||||
|
||||
import android.view.textservice.SuggestionsInfo
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
@@ -28,7 +27,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -44,7 +42,7 @@ import dev.patrickgold.florisboard.clipboardManager
|
||||
import dev.patrickgold.florisboard.editorInstance
|
||||
import dev.patrickgold.florisboard.lib.FlorisLocale
|
||||
import dev.patrickgold.florisboard.lib.observeAsNonNullState
|
||||
import dev.patrickgold.florisboard.spellingManager
|
||||
import dev.patrickgold.florisboard.nlpManager
|
||||
import dev.patrickgold.jetpref.datastore.model.observeAsState
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
@@ -84,7 +82,7 @@ private fun DevtoolsClipboardOverlay() {
|
||||
val clipboardManager by context.clipboardManager()
|
||||
|
||||
DevtoolsOverlayBox(title = "Clipboard overlay") {
|
||||
val primaryClip by clipboardManager.primaryClip.observeAsState()
|
||||
val primaryClip by clipboardManager.primaryClipFlow.collectAsState()
|
||||
Text(
|
||||
modifier = Modifier.padding(bottom = 8.dp, start = 8.dp, end = 8.dp),
|
||||
text = primaryClip.toString(),
|
||||
@@ -112,7 +110,8 @@ private fun DevtoolsInputStateOverlay() {
|
||||
DevtoolsText(text = "Before: \"${content.textBeforeSelection}\"")
|
||||
DevtoolsText(text = "Selected: \"${content.selectedText}\"")
|
||||
DevtoolsText(text = "After: \"${content.textAfterSelection}\"")
|
||||
DevtoolsText(text = "ComposingWord: ${content.composing}")
|
||||
DevtoolsText(text = "Composing: ${content.composing}")
|
||||
DevtoolsText(text = "CurrentWord: ${content.currentWord}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,17 +120,16 @@ private fun DevtoolsInputStateOverlay() {
|
||||
@Composable
|
||||
private fun DevtoolsSpellingOverlay() {
|
||||
val context = LocalContext.current
|
||||
val spellingManager by context.spellingManager()
|
||||
val nlpManager by context.nlpManager()
|
||||
|
||||
val debugOverlayVersion by spellingManager.debugOverlayVersion.observeAsNonNullState()
|
||||
val suggestionsInfos = remember(debugOverlayVersion) { spellingManager.debugOverlaySuggestionsInfos.snapshot() }
|
||||
val debugOverlayVersion by nlpManager.debugOverlayVersion.observeAsNonNullState()
|
||||
val suggestionsInfos = remember(debugOverlayVersion) { nlpManager.debugOverlaySuggestionsInfos.snapshot() }
|
||||
|
||||
val sortedEntries = suggestionsInfos.entries.sortedByDescending { it.key }
|
||||
DevtoolsOverlayBox(title = "Spelling overlay (${sortedEntries.size})") {
|
||||
for ((timestamp, wordInfoPair) in sortedEntries) {
|
||||
val (word, info) = wordInfoPair
|
||||
val isTypo = (info.suggestionsAttributes and SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) > 0
|
||||
val suggestions = Array(info.suggestionsCount) { n -> info.getSuggestionAt(n) }
|
||||
val suggestions = info.suggestions()
|
||||
Column(modifier = Modifier.padding(horizontal = 8.dp)) {
|
||||
val date = DateFormat.format(Date(timestamp))
|
||||
Text(
|
||||
@@ -141,8 +139,8 @@ private fun DevtoolsSpellingOverlay() {
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
val details = buildString {
|
||||
appendLine("isTypo: $isTypo")
|
||||
if (isTypo) {
|
||||
appendLine("isTypo: ${info.isTypo} | isGrammarError: ${info.isGrammarError}")
|
||||
if (info.isTypo || info.isGrammarError) {
|
||||
appendLine("providing corrections list of size n=${suggestions.size}")
|
||||
for ((n, suggestion) in suggestions.withIndex()) {
|
||||
append(" [$n] = string[${suggestion.length}] { \"")
|
||||
|
||||
@@ -90,6 +90,12 @@ fun DevtoolsScreen() = FlorisScreen {
|
||||
summary = stringRes(R.string.devtools__show_key_touch_boundaries__summary),
|
||||
enabledIf = { prefs.devtools.enabled isEqualTo true },
|
||||
)
|
||||
SwitchPreference(
|
||||
prefs.devtools.showDragAndDropHelpers,
|
||||
title = stringRes(R.string.devtools__show_drag_and_drop_helpers__label),
|
||||
summary = stringRes(R.string.devtools__show_drag_and_drop_helpers__summary),
|
||||
enabledIf = { prefs.devtools.enabled isEqualTo true },
|
||||
)
|
||||
Preference(
|
||||
title = stringRes(R.string.devtools__clear_udm_internal_database__label),
|
||||
summary = stringRes(R.string.devtools__clear_udm_internal_database__summary),
|
||||
@@ -99,14 +105,7 @@ fun DevtoolsScreen() = FlorisScreen {
|
||||
Preference(
|
||||
title = stringRes(R.string.devtools__reset_flag__label, "flag_name" to "isImeSetUp"),
|
||||
summary = stringRes(R.string.devtools__reset_flag_is_ime_set_up__summary),
|
||||
onClick = {
|
||||
prefs.internal.isImeSetUp.set(false)
|
||||
navController.navigate(Routes.Setup.Screen) {
|
||||
popUpTo(Routes.Settings.Home) {
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = { prefs.internal.isImeSetUp.set(false) },
|
||||
enabledIf = { prefs.devtools.enabled isEqualTo true },
|
||||
)
|
||||
Preference(
|
||||
@@ -184,13 +183,6 @@ fun DevtoolsScreen() = FlorisScreen {
|
||||
context.showLongToast(extensionManager.keyboardExtensions.internalModuleDir.absolutePath)
|
||||
},
|
||||
)
|
||||
Preference(
|
||||
title = "spellingDicts",
|
||||
summary = extensionManager.spellingDicts.internalModuleDir.absolutePath,
|
||||
onClick = {
|
||||
context.showLongToast(extensionManager.spellingDicts.internalModuleDir.absolutePath)
|
||||
},
|
||||
)
|
||||
Preference(
|
||||
title = "themes",
|
||||
summary = extensionManager.themes.internalModuleDir.absolutePath,
|
||||
|
||||
@@ -28,7 +28,6 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
@@ -51,7 +50,6 @@ import dev.patrickgold.florisboard.app.settings.theme.ThemeEditorScreen
|
||||
import dev.patrickgold.florisboard.cacheManager
|
||||
import dev.patrickgold.florisboard.extensionManager
|
||||
import dev.patrickgold.florisboard.ime.keyboard.KeyboardExtension
|
||||
import dev.patrickgold.florisboard.ime.spelling.SpellingExtension
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtension
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponent
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponentEditor
|
||||
@@ -218,7 +216,6 @@ private fun ExtensionEditScreenSheetSwitcher(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
private fun EditScreen(
|
||||
workspace: CacheManager.ExtEditorWorkspace<*>,
|
||||
@@ -227,14 +224,12 @@ private fun EditScreen(
|
||||
title = stringRes(if (isCreateExt) {
|
||||
when (workspace.ext) {
|
||||
is KeyboardExtension -> R.string.ext__editor__title_create_keyboard
|
||||
is SpellingExtension -> R.string.ext__editor__title_create_spelling
|
||||
is ThemeExtension -> R.string.ext__editor__title_create_theme
|
||||
else -> R.string.ext__editor__title_create_any
|
||||
}
|
||||
} else {
|
||||
when (workspace.ext) {
|
||||
is KeyboardExtension -> R.string.ext__editor__title_edit_keyboard
|
||||
is SpellingExtension -> R.string.ext__editor__title_edit_spelling
|
||||
is ThemeExtension -> R.string.ext__editor__title_edit_theme
|
||||
else -> R.string.ext__editor__title_edit_any
|
||||
}
|
||||
|
||||
@@ -48,9 +48,8 @@ import dev.patrickgold.florisboard.app.LocalNavController
|
||||
import dev.patrickgold.florisboard.cacheManager
|
||||
import dev.patrickgold.florisboard.extensionManager
|
||||
import dev.patrickgold.florisboard.ime.keyboard.KeyboardExtension
|
||||
import dev.patrickgold.florisboard.ime.nlp.NATIVE_NULLPTR
|
||||
import dev.patrickgold.florisboard.ime.spelling.SpellingExtension
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtension
|
||||
import dev.patrickgold.florisboard.lib.NATIVE_NULLPTR
|
||||
import dev.patrickgold.florisboard.lib.android.showLongToast
|
||||
import dev.patrickgold.florisboard.lib.cache.CacheManager
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisBulletSpacer
|
||||
@@ -79,11 +78,6 @@ enum class ExtensionImportScreenType(
|
||||
titleResId = R.string.ext__import__ext_keyboard,
|
||||
supportedFiles = listOf(FileRegistry.FlexExtension),
|
||||
),
|
||||
EXT_SPELLING(
|
||||
id = "ext-spelling",
|
||||
titleResId = R.string.ext__import__ext_spelling,
|
||||
supportedFiles = listOf(FileRegistry.FlexExtension),
|
||||
),
|
||||
EXT_THEME(
|
||||
id = "ext-theme",
|
||||
titleResId = R.string.ext__import__ext_theme,
|
||||
@@ -116,14 +110,14 @@ fun ExtensionImportScreen(type: ExtensionImportScreenType, initUuid: String?) =
|
||||
if (extensionManager.getExtensionById(ext.meta.id)?.sourceRef?.isAssets == true) {
|
||||
R.string.ext__import__file_skip_ext_core
|
||||
} else {
|
||||
NATIVE_NULLPTR
|
||||
NATIVE_NULLPTR.toInt()
|
||||
}
|
||||
}
|
||||
fileInfo.mediaType == FileRegistry.FlexExtension.mediaType -> {
|
||||
R.string.ext__import__file_skip_ext_corrupted
|
||||
}
|
||||
else -> {
|
||||
NATIVE_NULLPTR
|
||||
NATIVE_NULLPTR.toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,7 +150,7 @@ fun ExtensionImportScreen(type: ExtensionImportScreenType, initUuid: String?) =
|
||||
}
|
||||
val enabled = remember(importResult) {
|
||||
importResult?.getOrNull()?.takeIf { workspace ->
|
||||
workspace.inputFileInfos.any { it.skipReason == NATIVE_NULLPTR }
|
||||
workspace.inputFileInfos.any { it.skipReason == NATIVE_NULLPTR.toInt() }
|
||||
} != null
|
||||
}
|
||||
ButtonBarButton(
|
||||
@@ -166,7 +160,7 @@ fun ExtensionImportScreen(type: ExtensionImportScreenType, initUuid: String?) =
|
||||
val workspace = importResult!!.getOrThrow()
|
||||
runCatching {
|
||||
for (fileInfo in workspace.inputFileInfos) {
|
||||
if (fileInfo.skipReason != NATIVE_NULLPTR) {
|
||||
if (fileInfo.skipReason != NATIVE_NULLPTR.toInt()) {
|
||||
continue
|
||||
}
|
||||
val ext = fileInfo.ext
|
||||
@@ -177,9 +171,6 @@ fun ExtensionImportScreen(type: ExtensionImportScreenType, initUuid: String?) =
|
||||
ExtensionImportScreenType.EXT_KEYBOARD -> {
|
||||
ext.takeIf { it is KeyboardExtension }?.let { extensionManager.import(it) }
|
||||
}
|
||||
ExtensionImportScreenType.EXT_SPELLING -> {
|
||||
ext.takeIf { it is SpellingExtension }?.let { extensionManager.import(it) }
|
||||
}
|
||||
ExtensionImportScreenType.EXT_THEME -> {
|
||||
ext.takeIf { it is ThemeExtension }?.let { extensionManager.import(it) }
|
||||
}
|
||||
@@ -313,7 +304,7 @@ private fun FileInfoView(
|
||||
)
|
||||
}
|
||||
}
|
||||
if (fileInfo.skipReason != NATIVE_NULLPTR) {
|
||||
if (fileInfo.skipReason != NATIVE_NULLPTR.toInt()) {
|
||||
Box(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(19.dp)
|
||||
|
||||
@@ -39,8 +39,6 @@ import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.LocalNavController
|
||||
import dev.patrickgold.florisboard.app.Routes
|
||||
import dev.patrickgold.florisboard.lib.android.launchUrl
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisButton
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisErrorCard
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisWarningCard
|
||||
@@ -83,7 +81,7 @@ fun HomeScreen() = FlorisScreen {
|
||||
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = "Welcome to 0.3.16!",
|
||||
text = "Welcome to the 0.4 alpha series!",
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
@@ -100,8 +98,8 @@ fun HomeScreen() = FlorisScreen {
|
||||
}
|
||||
}
|
||||
if (!isCollapsed) {
|
||||
Text("This release focuses on improving the stability of this keyboard (see changelog for all details). I want to thank all my beta testers who were able to identify and report a lot of bugs, this helped a lot in ironing out bugs!\n")
|
||||
Text("This is the last stable release on the 0.3.x track, the development focus now shifts to the 0.4.0 dev cycle, which will introduce word suggestions and inline autocorrect (at first for Latin-based languages) within the keyboard UI. If you are interested in the early steps of this new feature, make sure to follow the beta track, as the development of proper word suggestions will take quite some time.\n")
|
||||
Text("0.4 will be quite a big release and finally work on adding support for word suggestion and inline autocorrect within the keyboard UI, at first for Latin-based languages. Additionally general improvements and bug fixes will also be made.\n")
|
||||
Text("Currently the alpha releases are preparations for the suggestions implementation and general improvements and bug fixes.\n")
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text("Note that this release does not contain support for word suggestions (will show the current word plus numbers as a placeholder).", color = Color.Red)
|
||||
Text("Please DO NOT file an issue for this. It is already more than known and a major goal for implementation in 0.4.0. Thank you!\n")
|
||||
@@ -130,15 +128,10 @@ fun HomeScreen() = FlorisScreen {
|
||||
onClick = { navController.navigate(Routes.Settings.Smartbar) },
|
||||
)
|
||||
Preference(
|
||||
iconId = R.drawable.ic_settings_suggest,
|
||||
iconId = R.drawable.ic_spellcheck,
|
||||
title = stringRes(R.string.settings__typing__title),
|
||||
onClick = { navController.navigate(Routes.Settings.Typing) },
|
||||
)
|
||||
Preference(
|
||||
iconId = R.drawable.ic_spellcheck,
|
||||
title = stringRes(R.string.settings__spelling__title),
|
||||
onClick = { navController.navigate(Routes.Settings.Spelling) },
|
||||
)
|
||||
Preference(
|
||||
iconId = R.drawable.ic_library_books,
|
||||
title = stringRes(R.string.settings__dictionary__title),
|
||||
|
||||
@@ -23,6 +23,7 @@ import dev.patrickgold.florisboard.app.AppTheme
|
||||
import dev.patrickgold.florisboard.app.LocalNavController
|
||||
import dev.patrickgold.florisboard.app.Routes
|
||||
import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn
|
||||
import dev.patrickgold.florisboard.ime.keyboard.IncognitoMode
|
||||
import dev.patrickgold.florisboard.lib.FlorisLocale
|
||||
import dev.patrickgold.florisboard.lib.android.AndroidVersion
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
|
||||
@@ -143,11 +144,11 @@ fun AdvancedScreen() = FlorisScreen {
|
||||
},
|
||||
enabledIf = { AndroidVersion.ATMOST_API28_P },
|
||||
)
|
||||
SwitchPreference(
|
||||
prefs.advanced.forcePrivateMode,
|
||||
iconId = R.drawable.ic_security,
|
||||
title = stringRes(R.string.pref__advanced__force_private_mode__label),
|
||||
summary = stringRes(R.string.pref__advanced__force_private_mode__summary),
|
||||
ListPreference(
|
||||
prefs.advanced.incognitoMode,
|
||||
iconId = R.drawable.ic_incognito,
|
||||
title = stringRes(R.string.pref__advanced__incognito_mode__label),
|
||||
entries = IncognitoMode.listEntries(),
|
||||
)
|
||||
|
||||
PreferenceGroup(title = stringRes(R.string.backup_and_restore__title)) {
|
||||
|
||||
@@ -71,11 +71,10 @@ object Backup {
|
||||
class FilesSelector {
|
||||
var jetprefDatastore by mutableStateOf(true)
|
||||
var imeKeyboard by mutableStateOf(true)
|
||||
var imeSpelling by mutableStateOf(true)
|
||||
var imeTheme by mutableStateOf(true)
|
||||
|
||||
fun atLeastOneSelected(): Boolean {
|
||||
return jetprefDatastore || imeKeyboard || imeSpelling || imeTheme
|
||||
return jetprefDatastore || imeKeyboard || imeTheme
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,11 +138,6 @@ fun BackupScreen() = FlorisScreen {
|
||||
dir.copyRecursively(workspaceFilesDir.subDir(ExtensionManager.IME_KEYBOARD_PATH))
|
||||
}
|
||||
}
|
||||
if (backupFilesSelector.imeSpelling) {
|
||||
context.filesDir.subDir(ExtensionManager.IME_SPELLING_PATH).let { dir ->
|
||||
dir.copyRecursively(workspaceFilesDir.subDir(ExtensionManager.IME_SPELLING_PATH))
|
||||
}
|
||||
}
|
||||
if (backupFilesSelector.imeTheme) {
|
||||
context.filesDir.subDir(ExtensionManager.IME_THEME_PATH).let { dir ->
|
||||
dir.copyRecursively(workspaceFilesDir.subDir(ExtensionManager.IME_THEME_PATH))
|
||||
@@ -254,11 +248,6 @@ internal fun BackupFilesSelector(
|
||||
checked = filesSelector.imeKeyboard,
|
||||
text = stringRes(R.string.backup_and_restore__back_up__files_ime_keyboard),
|
||||
)
|
||||
CheckboxListItem(
|
||||
onClick = { filesSelector.imeSpelling = !filesSelector.imeSpelling },
|
||||
checked = filesSelector.imeSpelling,
|
||||
text = stringRes(R.string.backup_and_restore__back_up__files_ime_spelling),
|
||||
)
|
||||
CheckboxListItem(
|
||||
onClick = { filesSelector.imeTheme = !filesSelector.imeTheme },
|
||||
checked = filesSelector.imeTheme,
|
||||
|
||||
@@ -159,16 +159,6 @@ fun RestoreScreen() = FlorisScreen {
|
||||
srcDir.copyRecursively(dstDir, overwrite = true)
|
||||
}
|
||||
}
|
||||
if (restoreFilesSelector.imeSpelling) {
|
||||
val srcDir = workspaceFilesDir.subDir(ExtensionManager.IME_SPELLING_PATH)
|
||||
val dstDir = context.filesDir.subDir(ExtensionManager.IME_SPELLING_PATH)
|
||||
if (shouldReset) {
|
||||
dstDir.deleteContentsRecursively()
|
||||
}
|
||||
if (srcDir.exists()) {
|
||||
srcDir.copyRecursively(dstDir, overwrite = true)
|
||||
}
|
||||
}
|
||||
if (restoreFilesSelector.imeTheme) {
|
||||
val srcDir = workspaceFilesDir.subDir(ExtensionManager.IME_THEME_PATH)
|
||||
val dstDir = context.filesDir.subDir(ExtensionManager.IME_THEME_PATH)
|
||||
|
||||
@@ -17,12 +17,18 @@
|
||||
package dev.patrickgold.florisboard.app.settings.keyboard
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.input.InputFeedbackController
|
||||
import dev.patrickgold.florisboard.ime.input.InputFeedbackActivationMode
|
||||
import dev.patrickgold.florisboard.ime.input.HapticVibrationMode
|
||||
import dev.patrickgold.florisboard.lib.android.AndroidVersion
|
||||
import dev.patrickgold.florisboard.lib.android.systemVibratorOrNull
|
||||
import dev.patrickgold.florisboard.lib.android.vibrate
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
|
||||
import dev.patrickgold.florisboard.lib.compose.stringRes
|
||||
import dev.patrickgold.jetpref.datastore.ui.DialogSliderPreference
|
||||
import dev.patrickgold.jetpref.datastore.ui.ExperimentalJetPrefDatastoreUi
|
||||
import dev.patrickgold.jetpref.datastore.ui.ListPreference
|
||||
import dev.patrickgold.jetpref.datastore.ui.PreferenceGroup
|
||||
import dev.patrickgold.jetpref.datastore.ui.SwitchPreference
|
||||
|
||||
@@ -31,19 +37,19 @@ import dev.patrickgold.jetpref.datastore.ui.SwitchPreference
|
||||
fun InputFeedbackScreen() = FlorisScreen {
|
||||
title = stringRes(R.string.settings__input_feedback__title)
|
||||
previewFieldVisible = true
|
||||
iconSpaceReserved = false
|
||||
|
||||
val context = LocalContext.current
|
||||
val vibrator = context.systemVibratorOrNull()
|
||||
|
||||
content {
|
||||
PreferenceGroup(title = stringRes(R.string.pref__input_feedback__group_audio__label)) {
|
||||
SwitchPreference(
|
||||
prefs.inputFeedback.audioEnabled,
|
||||
ListPreference(
|
||||
listPref = prefs.inputFeedback.audioActivationMode,
|
||||
switchPref = prefs.inputFeedback.audioEnabled,
|
||||
title = stringRes(R.string.pref__input_feedback__audio_enabled__label),
|
||||
summary = stringRes(R.string.pref__input_feedback__audio_enabled__summary),
|
||||
)
|
||||
SwitchPreference(
|
||||
prefs.inputFeedback.audioIgnoreSystemSettings,
|
||||
title = stringRes(R.string.pref__input_feedback__audio_ignore_system_settings__label),
|
||||
summary = stringRes(R.string.pref__input_feedback__audio_ignore_system_settings__summary),
|
||||
enabledIf = { prefs.inputFeedback.audioEnabled isEqualTo true },
|
||||
summarySwitchDisabled = stringRes(R.string.pref__input_feedback__audio_enabled__summary_disabled),
|
||||
entries = InputFeedbackActivationMode.audioListEntries(),
|
||||
)
|
||||
DialogSliderPreference(
|
||||
prefs.inputFeedback.audioVolume,
|
||||
@@ -87,44 +93,71 @@ fun InputFeedbackScreen() = FlorisScreen {
|
||||
}
|
||||
|
||||
PreferenceGroup(title = stringRes(R.string.pref__input_feedback__group_haptic__label)) {
|
||||
SwitchPreference(
|
||||
prefs.inputFeedback.hapticEnabled,
|
||||
ListPreference(
|
||||
listPref = prefs.inputFeedback.hapticActivationMode,
|
||||
switchPref = prefs.inputFeedback.hapticEnabled,
|
||||
title = stringRes(R.string.pref__input_feedback__haptic_enabled__label),
|
||||
summary = stringRes(R.string.pref__input_feedback__haptic_enabled__summary),
|
||||
summarySwitchDisabled = stringRes(R.string.pref__input_feedback__haptic_enabled__summary_disabled),
|
||||
entries = InputFeedbackActivationMode.hapticListEntries(),
|
||||
)
|
||||
SwitchPreference(
|
||||
prefs.inputFeedback.hapticIgnoreSystemSettings,
|
||||
title = stringRes(R.string.pref__input_feedback__haptic_ignore_system_settings__label),
|
||||
summary = stringRes(R.string.pref__input_feedback__haptic_ignore_system_settings__summary),
|
||||
enabledIf = { prefs.inputFeedback.hapticEnabled isEqualTo true },
|
||||
)
|
||||
SwitchPreference(
|
||||
prefs.inputFeedback.hapticUseVibrator,
|
||||
title = stringRes(R.string.pref__input_feedback__haptic_use_vibrator__label),
|
||||
summary = stringRes(R.string.pref__input_feedback__haptic_use_vibrator__summary),
|
||||
ListPreference(
|
||||
prefs.inputFeedback.hapticVibrationMode,
|
||||
title = stringRes(R.string.pref__input_feedback__haptic_vibration_mode__label),
|
||||
enabledIf = { prefs.inputFeedback.hapticEnabled isEqualTo true },
|
||||
entries = HapticVibrationMode.listEntries(),
|
||||
)
|
||||
DialogSliderPreference(
|
||||
prefs.inputFeedback.hapticVibrationDuration,
|
||||
title = stringRes(R.string.pref__input_feedback__haptic_vibration_duration__label),
|
||||
valueLabel = { stringRes(R.string.unit__milliseconds__symbol, "v" to it) },
|
||||
summary = {
|
||||
if (vibrator == null || !vibrator.hasVibrator()) {
|
||||
stringRes(R.string.pref__input_feedback__haptic_vibration_strength__summary_no_vibrator)
|
||||
} else {
|
||||
stringRes(R.string.unit__milliseconds__symbol, "v" to it)
|
||||
}
|
||||
},
|
||||
min = 1,
|
||||
max = 100,
|
||||
stepIncrement = 1,
|
||||
enabledIf = { prefs.inputFeedback.hapticEnabled isEqualTo true && prefs.inputFeedback.hapticUseVibrator isEqualTo true },
|
||||
onPreviewSelectedValue = { duration ->
|
||||
val strength = prefs.inputFeedback.hapticVibrationStrength.get()
|
||||
vibrator?.vibrate(duration, strength)
|
||||
},
|
||||
enabledIf = {
|
||||
prefs.inputFeedback.hapticEnabled isEqualTo true &&
|
||||
prefs.inputFeedback.hapticVibrationMode isEqualTo HapticVibrationMode.USE_VIBRATOR_DIRECTLY &&
|
||||
vibrator != null && vibrator.hasVibrator()
|
||||
},
|
||||
)
|
||||
DialogSliderPreference(
|
||||
prefs.inputFeedback.hapticVibrationStrength,
|
||||
title = stringRes(R.string.pref__input_feedback__haptic_vibration_strength__label),
|
||||
valueLabel = { stringRes(R.string.unit__percent__symbol, "v" to it) },
|
||||
summary = { strength ->
|
||||
InputFeedbackController.generateVibrationStrengthErrorSummary()
|
||||
?: stringRes(R.string.unit__percent__symbol, "v" to strength)
|
||||
if (vibrator == null || !vibrator.hasVibrator()) {
|
||||
stringRes(R.string.pref__input_feedback__haptic_vibration_strength__summary_no_vibrator)
|
||||
} else if (AndroidVersion.ATMOST_API25_N_MR1) {
|
||||
stringRes(R.string.pref__input_feedback__haptic_vibration_strength__summary_unsupported_android_version)
|
||||
} else if (!vibrator.hasAmplitudeControl()) {
|
||||
stringRes(R.string.pref__input_feedback__haptic_vibration_strength__summary_no_amplitude_ctrl)
|
||||
} else {
|
||||
stringRes(R.string.unit__percent__symbol, "v" to strength)
|
||||
}
|
||||
},
|
||||
min = 1,
|
||||
max = 100,
|
||||
stepIncrement = 1,
|
||||
enabledIf = { prefs.inputFeedback.hapticEnabled isEqualTo true && prefs.inputFeedback.hapticUseVibrator isEqualTo true && InputFeedbackController.hasAmplitudeControl() },
|
||||
onPreviewSelectedValue = { strength ->
|
||||
val duration = prefs.inputFeedback.hapticVibrationDuration.get()
|
||||
vibrator?.vibrate(duration, strength)
|
||||
},
|
||||
enabledIf = {
|
||||
prefs.inputFeedback.hapticEnabled isEqualTo true &&
|
||||
prefs.inputFeedback.hapticVibrationMode isEqualTo HapticVibrationMode.USE_VIBRATOR_DIRECTLY &&
|
||||
vibrator != null && vibrator.hasVibrator() &&
|
||||
AndroidVersion.ATLEAST_API26_O && vibrator.hasAmplitudeControl()
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
prefs.inputFeedback.hapticFeatKeyPress,
|
||||
|
||||
@@ -21,6 +21,7 @@ import androidx.compose.material.ExtendedFloatingActionButton
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -73,8 +74,8 @@ fun LocalizationScreen() = FlorisScreen {
|
||||
entries = DisplayLanguageNamesIn.listEntries(),
|
||||
)
|
||||
PreferenceGroup(title = stringRes(R.string.settings__localization__group_subtypes__label)) {
|
||||
val subtypes by subtypeManager.subtypes.observeAsNonNullState()
|
||||
if (subtypes.isNullOrEmpty()) {
|
||||
val subtypes by subtypeManager.subtypesFlow.collectAsState()
|
||||
if (subtypes.isEmpty()) {
|
||||
FlorisWarningCard(
|
||||
modifier = Modifier.padding(all = 8.dp),
|
||||
text = stringRes(R.string.settings__localization__subtype_no_subtypes_configured_warning),
|
||||
|
||||
@@ -58,6 +58,7 @@ import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn
|
||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||
import dev.patrickgold.florisboard.ime.core.SubtypeJsonConfig
|
||||
import dev.patrickgold.florisboard.ime.core.SubtypeLayoutMap
|
||||
import dev.patrickgold.florisboard.ime.core.SubtypeNlpProviderMap
|
||||
import dev.patrickgold.florisboard.ime.core.SubtypePreset
|
||||
import dev.patrickgold.florisboard.ime.keyboard.LayoutArrangementComponent
|
||||
import dev.patrickgold.florisboard.ime.keyboard.LayoutType
|
||||
@@ -79,6 +80,10 @@ import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
||||
private val SelectComponentName = ExtensionComponentName("00", "00")
|
||||
private val SelectNlpProviderId = SelectComponentName.toString()
|
||||
private val SelectNlpProviders = SubtypeNlpProviderMap(
|
||||
spelling = SelectNlpProviderId,
|
||||
)
|
||||
private val SelectLayoutMap = SubtypeLayoutMap(
|
||||
characters = SelectComponentName,
|
||||
symbols = SelectComponentName,
|
||||
@@ -100,6 +105,7 @@ private class SubtypeEditorState(init: Subtype?) {
|
||||
id = editor.id.value,
|
||||
primaryLocale = editor.primaryLocale.value,
|
||||
secondaryLocales = editor.secondaryLocales.value,
|
||||
nlpProviders = editor.nlpProviders.value,
|
||||
composer = editor.composer.value,
|
||||
currencySet = editor.currencySet.value,
|
||||
punctuationRule = editor.punctuationRule.value,
|
||||
@@ -118,9 +124,10 @@ private class SubtypeEditorState(init: Subtype?) {
|
||||
val id: MutableState<Long> = mutableStateOf(init?.id ?: -1)
|
||||
val primaryLocale: MutableState<FlorisLocale> = mutableStateOf(init?.primaryLocale ?: SelectLocale)
|
||||
val secondaryLocales: MutableState<List<FlorisLocale>> = mutableStateOf(init?.secondaryLocales ?: listOf())
|
||||
val nlpProviders: MutableState<SubtypeNlpProviderMap> = mutableStateOf(init?.nlpProviders ?: Subtype.DEFAULT.nlpProviders)
|
||||
val composer: MutableState<ExtensionComponentName> = mutableStateOf(init?.composer ?: SelectComponentName)
|
||||
val currencySet: MutableState<ExtensionComponentName> = mutableStateOf(init?.currencySet ?: SelectComponentName)
|
||||
val punctuationRule: MutableState<ExtensionComponentName> = mutableStateOf(init?.punctuationRule ?: SelectComponentName)
|
||||
val punctuationRule: MutableState<ExtensionComponentName> = mutableStateOf(init?.punctuationRule ?: Subtype.DEFAULT.punctuationRule)
|
||||
val popupMapping: MutableState<ExtensionComponentName> = mutableStateOf(init?.popupMapping ?: SelectComponentName)
|
||||
val layoutMap: MutableState<SubtypeLayoutMap> = mutableStateOf(init?.layoutMap ?: SelectLayoutMap)
|
||||
|
||||
@@ -137,8 +144,11 @@ private class SubtypeEditorState(init: Subtype?) {
|
||||
|
||||
fun toSubtype() = runCatching<Subtype> {
|
||||
check(primaryLocale.value != SelectLocale)
|
||||
check(nlpProviders.value.spelling != SelectNlpProviderId)
|
||||
check(nlpProviders.value.suggestion != SelectNlpProviderId)
|
||||
check(composer.value != SelectComponentName)
|
||||
check(currencySet.value != SelectComponentName)
|
||||
check(punctuationRule.value != SelectComponentName)
|
||||
check(popupMapping.value != SelectComponentName)
|
||||
check(layoutMap.value.characters != SelectComponentName)
|
||||
check(layoutMap.value.symbols != SelectComponentName)
|
||||
@@ -149,7 +159,7 @@ private class SubtypeEditorState(init: Subtype?) {
|
||||
check(layoutMap.value.phone != SelectComponentName)
|
||||
check(layoutMap.value.phone2 != SelectComponentName)
|
||||
Subtype(
|
||||
id.value, primaryLocale.value, secondaryLocales.value, composer.value,
|
||||
id.value, primaryLocale.value, secondaryLocales.value, nlpProviders.value, composer.value,
|
||||
currencySet.value, punctuationRule.value, popupMapping.value, layoutMap.value,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,8 +18,9 @@ package dev.patrickgold.florisboard.app.settings.smartbar
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.text.smartbar.SecondaryRowPlacement
|
||||
import dev.patrickgold.florisboard.ime.text.smartbar.SmartbarRowType
|
||||
import dev.patrickgold.florisboard.ime.smartbar.CandidatesDisplayMode
|
||||
import dev.patrickgold.florisboard.ime.smartbar.ExtendedActionsPlacement
|
||||
import dev.patrickgold.florisboard.ime.smartbar.SmartbarLayout
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
|
||||
import dev.patrickgold.florisboard.lib.compose.stringRes
|
||||
import dev.patrickgold.jetpref.datastore.ui.ListPreference
|
||||
@@ -37,41 +38,44 @@ fun SmartbarScreen() = FlorisScreen {
|
||||
title = stringRes(R.string.pref__smartbar__enabled__label),
|
||||
summary = stringRes(R.string.pref__smartbar__enabled__summary),
|
||||
)
|
||||
SwitchPreference(
|
||||
prefs.smartbar.flipToggles,
|
||||
title = stringRes(R.string.pref__smartbar__flip_toggles__label),
|
||||
summary = stringRes(R.string.pref__smartbar__flip_toggles__summary),
|
||||
ListPreference(
|
||||
listPref = prefs.smartbar.layout,
|
||||
title = stringRes(R.string.pref__smartbar__layout__label),
|
||||
entries = SmartbarLayout.listEntries(),
|
||||
enabledIf = { prefs.smartbar.enabled isEqualTo true },
|
||||
)
|
||||
|
||||
PreferenceGroup(title = stringRes(R.string.pref__smartbar__group_primary_actions__label)) {
|
||||
PreferenceGroup(title = stringRes(R.string.pref__smartbar__group_layout_specific__label)) {
|
||||
ListPreference(
|
||||
prefs.suggestion.displayMode,
|
||||
title = stringRes(R.string.pref__suggestion__display_mode__label),
|
||||
entries = CandidatesDisplayMode.listEntries(),
|
||||
enabledIf = { prefs.smartbar.enabled isEqualTo true },
|
||||
visibleIf = { prefs.smartbar.layout isNotEqualTo SmartbarLayout.ACTIONS_ONLY },
|
||||
)
|
||||
SwitchPreference(
|
||||
prefs.smartbar.primaryActionsAutoExpandCollapse,
|
||||
title = stringRes(R.string.pref__smartbar__primary_actions_auto_expand_collapse__label),
|
||||
summary = stringRes(R.string.pref__smartbar__primary_actions_auto_expand_collapse__summary),
|
||||
prefs.smartbar.flipToggles,
|
||||
title = stringRes(R.string.pref__smartbar__flip_toggles__label),
|
||||
summary = stringRes(R.string.pref__smartbar__flip_toggles__summary),
|
||||
enabledIf = { prefs.smartbar.enabled isEqualTo true },
|
||||
visibleIf = {
|
||||
prefs.smartbar.layout isEqualTo SmartbarLayout.SUGGESTIONS_ACTIONS_SHARED ||
|
||||
prefs.smartbar.layout isEqualTo SmartbarLayout.SUGGESTIONS_ACTIONS_EXTENDED
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
prefs.smartbar.sharedActionsAutoExpandCollapse,
|
||||
title = stringRes(R.string.pref__smartbar__shared_actions_auto_expand_collapse__label),
|
||||
summary = stringRes(R.string.pref__smartbar__shared_actions_auto_expand_collapse__summary),
|
||||
enabledIf = { prefs.smartbar.enabled isEqualTo true },
|
||||
visibleIf = { prefs.smartbar.layout isEqualTo SmartbarLayout.SUGGESTIONS_ACTIONS_SHARED },
|
||||
)
|
||||
ListPreference(
|
||||
prefs.smartbar.primaryActionsRowType,
|
||||
title = stringRes(R.string.pref__smartbar__any_row_type__label),
|
||||
entries = SmartbarRowType.listEntries(),
|
||||
listPref = prefs.smartbar.extendedActionsPlacement,
|
||||
title = stringRes(R.string.pref__smartbar__extended_actions_placement__label),
|
||||
entries = ExtendedActionsPlacement.listEntries(),
|
||||
enabledIf = { prefs.smartbar.enabled isEqualTo true },
|
||||
)
|
||||
}
|
||||
|
||||
PreferenceGroup(title = stringRes(R.string.pref__smartbar__group_secondary_actions__label)) {
|
||||
ListPreference(
|
||||
listPref = prefs.smartbar.secondaryActionsPlacement,
|
||||
switchPref = prefs.smartbar.secondaryActionsEnabled,
|
||||
title = stringRes(R.string.pref__smartbar__secondary_actions_enabled__label),
|
||||
entries = SecondaryRowPlacement.listEntries(),
|
||||
enabledIf = { prefs.smartbar.enabled isEqualTo true },
|
||||
)
|
||||
ListPreference(
|
||||
prefs.smartbar.secondaryActionsRowType,
|
||||
title = stringRes(R.string.pref__smartbar__any_row_type__label),
|
||||
entries = SmartbarRowType.listEntries(),
|
||||
enabledIf = { prefs.smartbar.enabled isEqualTo true && prefs.smartbar.secondaryActionsEnabled isEqualTo true },
|
||||
visibleIf = { prefs.smartbar.layout isEqualTo SmartbarLayout.SUGGESTIONS_ACTIONS_EXTENDED },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.settings.spelling
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.LocalNavController
|
||||
import dev.patrickgold.florisboard.extensionManager
|
||||
import dev.patrickgold.florisboard.ime.spelling.SpellingExtensionEditor
|
||||
import dev.patrickgold.florisboard.ime.spelling.SpellingManager
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisDropdownMenu
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisStep
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisStepLayout
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisStepState
|
||||
import dev.patrickgold.florisboard.lib.compose.stringRes
|
||||
import dev.patrickgold.florisboard.spellingManager
|
||||
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
|
||||
|
||||
private object Step {
|
||||
const val SelectSource: Int = 1
|
||||
const val ImportArchive: Int = 2
|
||||
const val VerifyImport: Int = 3
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ImportSpellingArchiveScreen() = FlorisScreen {
|
||||
title = stringRes(R.string.settings__spelling__import__title)
|
||||
scrollable = false
|
||||
|
||||
val navController = LocalNavController.current
|
||||
val context = LocalContext.current
|
||||
val extensionManager by context.extensionManager()
|
||||
val spellingManager by context.spellingManager()
|
||||
|
||||
val sources = remember { listOf("-") + SpellingManager.Config.importSources.map { it.label } }
|
||||
var sourceExpanded by remember { mutableStateOf(false) }
|
||||
var sourceSelectedIndex by rememberSaveable { mutableStateOf(0) }
|
||||
|
||||
var importArchiveUri by remember { mutableStateOf<Uri?>(null) }
|
||||
var importArchiveEditor by remember { mutableStateOf<SpellingExtensionEditor?>(null) }
|
||||
var importArchiveError by remember { mutableStateOf<Throwable?>(null) }
|
||||
var writeExtError by remember { mutableStateOf<Throwable?>(null) }
|
||||
var errorDialogVisible by remember { mutableStateOf(false) }
|
||||
val importArchiveLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.GetContent(),
|
||||
onResult = { uri ->
|
||||
// If uri is null it indicates that the selection activity
|
||||
// was cancelled (mostly by pressing the back button), so
|
||||
// we don't display an error message here.
|
||||
if (uri == null) return@rememberLauncherForActivityResult
|
||||
val importSource = SpellingManager.Config.importSources[sourceSelectedIndex - 1]
|
||||
spellingManager.prepareImport(importSource.id, uri).fold(
|
||||
onSuccess = {
|
||||
importArchiveUri = uri
|
||||
importArchiveEditor = it
|
||||
importArchiveError = null
|
||||
writeExtError = null
|
||||
},
|
||||
onFailure = {
|
||||
importArchiveUri = null
|
||||
importArchiveEditor = null
|
||||
importArchiveError = it
|
||||
writeExtError = null
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
val stepState = rememberSaveable(saver = FlorisStepState.Saver) {
|
||||
FlorisStepState.new(init = Step.SelectSource)
|
||||
}
|
||||
|
||||
content {
|
||||
LaunchedEffect(sourceSelectedIndex, importArchiveEditor) {
|
||||
stepState.setCurrentAuto(when {
|
||||
sourceSelectedIndex <= 0 -> Step.SelectSource
|
||||
importArchiveEditor == null -> Step.ImportArchive
|
||||
else -> Step.VerifyImport
|
||||
})
|
||||
}
|
||||
|
||||
FlorisStepLayout(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp),
|
||||
stepState = stepState,
|
||||
steps = listOf(
|
||||
FlorisStep(
|
||||
id = Step.SelectSource,
|
||||
title = if (stepState.getCurrent().value > Step.SelectSource) {
|
||||
sources.getOrElse(sourceSelectedIndex) { "undefined" }
|
||||
} else {
|
||||
stringRes(R.string.settings__spelling__import_archive_s1__title)
|
||||
},
|
||||
) {
|
||||
StepText(
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
text = stringRes(R.string.settings__spelling__import_archive_s1__p1),
|
||||
)
|
||||
FlorisDropdownMenu(
|
||||
items = sources,
|
||||
expanded = sourceExpanded,
|
||||
selectedIndex = sourceSelectedIndex,
|
||||
onSelectItem = { sourceSelectedIndex = it },
|
||||
onExpandRequest = { sourceExpanded = true },
|
||||
onDismissRequest = { sourceExpanded = false },
|
||||
)
|
||||
},
|
||||
FlorisStep(
|
||||
id = Step.ImportArchive,
|
||||
title = if (stepState.getCurrent().value > Step.ImportArchive && importArchiveEditor != null) {
|
||||
importArchiveEditor?.meta?.title ?: "undefined"
|
||||
} else {
|
||||
stringRes(R.string.settings__spelling__import_archive_s2__title)
|
||||
},
|
||||
) {
|
||||
StepText(
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
text = stringRes(R.string.settings__spelling__import_archive_s2__p1),
|
||||
)
|
||||
StepText(
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
text = importArchiveUri?.toString() ?: "No file selected.",
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
if (importArchiveError != null) {
|
||||
ErrorCard(
|
||||
onActionClick = { errorDialogVisible = true }
|
||||
)
|
||||
}
|
||||
StepButton(
|
||||
onClick = { importArchiveLauncher.launch("*/*") },
|
||||
label = stringRes(R.string.action__select_file),
|
||||
)
|
||||
},
|
||||
FlorisStep(
|
||||
id = Step.VerifyImport,
|
||||
title = stringRes(R.string.settings__spelling__import_any_s3__title),
|
||||
) {
|
||||
StepText(
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
text = stringRes(R.string.settings__spelling__import_any_s3__p1),
|
||||
)
|
||||
StepText(
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
// TODO: add verify view
|
||||
text = "TODO: add verify view",
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
if (writeExtError != null) {
|
||||
ErrorCard(
|
||||
onActionClick = { errorDialogVisible = true }
|
||||
)
|
||||
}
|
||||
StepButton(
|
||||
onClick = {
|
||||
runCatching {
|
||||
extensionManager.import(importArchiveEditor!!.build())
|
||||
}.fold(
|
||||
onSuccess = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
onFailure = {
|
||||
writeExtError = it
|
||||
},
|
||||
)
|
||||
},
|
||||
label = stringRes(R.string.action__import),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
if (errorDialogVisible) {
|
||||
JetPrefAlertDialog(
|
||||
title = "Detailed crash log",
|
||||
onDismiss = { errorDialogVisible = false },
|
||||
) {
|
||||
if (importArchiveError != null) {
|
||||
Text(
|
||||
text = importArchiveError.toString(),
|
||||
style = MaterialTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
if (writeExtError != null) {
|
||||
Text(
|
||||
text = writeExtError.toString(),
|
||||
style = MaterialTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ErrorCard(
|
||||
onActionClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Card(modifier = modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier = Modifier.padding(start = 16.dp, end = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1.0f)
|
||||
.padding(end = 8.dp),
|
||||
text = "Something went wrong!",
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
TextButton(onClick = { onActionClick() }) {
|
||||
Text(text = "Details")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.settings.spelling
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.FloatingActionButton
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.LocalNavController
|
||||
import dev.patrickgold.florisboard.app.Routes
|
||||
import dev.patrickgold.florisboard.app.ext.ExtensionList
|
||||
import dev.patrickgold.florisboard.extensionManager
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisInfoCard
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
|
||||
import dev.patrickgold.florisboard.lib.compose.stringRes
|
||||
|
||||
@Composable
|
||||
fun ManageSpellingDictsScreen() = FlorisScreen {
|
||||
title = stringRes(R.string.settings__spelling__manage_dicts__title)
|
||||
previewFieldVisible = true
|
||||
|
||||
val navController = LocalNavController.current
|
||||
val context = LocalContext.current
|
||||
val extensionManager by context.extensionManager()
|
||||
|
||||
floatingActionButton {
|
||||
FloatingActionButton(
|
||||
onClick = { navController.navigate(Routes.Settings.ImportSpellingArchive) },
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_add),
|
||||
contentDescription = "Add dictionary",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
content {
|
||||
FlorisInfoCard(
|
||||
modifier = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 16.dp),
|
||||
text = stringRes(R.string.settings__spelling__dict_sources_info__title),
|
||||
onClick = { navController.navigate(Routes.Settings.SpellingInfo) },
|
||||
)
|
||||
|
||||
val spellingDicts by extensionManager.spellingDicts.observeAsState()
|
||||
if (spellingDicts != null && spellingDicts!!.isNotEmpty()) {
|
||||
ExtensionList(
|
||||
extList = spellingDicts!!,
|
||||
summaryProvider = { ext ->
|
||||
"${ext.spelling.locale.languageTag()} | ${ext.meta.version} | ${ext.spelling.originalSourceId}"
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
text = stringRes(R.string.settings__spelling__manage_dicts__no_dicts_installed),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.settings.spelling
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.spelling.SpellingManager
|
||||
import dev.patrickgold.florisboard.lib.android.launchUrl
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
|
||||
import dev.patrickgold.florisboard.lib.compose.stringRes
|
||||
import dev.patrickgold.jetpref.datastore.ui.Preference
|
||||
|
||||
@Composable
|
||||
fun SpellingInfoScreen() = FlorisScreen {
|
||||
title = stringRes(R.string.settings__spelling__dict_sources_info__title)
|
||||
iconSpaceReserved = false
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
content {
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
text = stringRes(R.string.settings__spelling__dict_sources_info__intro),
|
||||
)
|
||||
for (source in SpellingManager.Config.importSources) {
|
||||
Preference(
|
||||
title = source.label,
|
||||
summary = source.url,
|
||||
onClick = {
|
||||
source.url?.let { context.launchUrl(it) }
|
||||
},
|
||||
)
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
text = stringRes(R.string.settings__spelling__dict_sources_info__other),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.settings.spelling
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.LocalNavController
|
||||
import dev.patrickgold.florisboard.app.Routes
|
||||
import dev.patrickgold.florisboard.extensionManager
|
||||
import dev.patrickgold.florisboard.ime.spelling.SpellingLanguageMode
|
||||
import dev.patrickgold.florisboard.lib.android.AndroidSettings
|
||||
import dev.patrickgold.florisboard.lib.android.launchActivity
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisCanvasIcon
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisErrorCard
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisSimpleCard
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisWarningCard
|
||||
import dev.patrickgold.florisboard.lib.compose.stringRes
|
||||
import dev.patrickgold.jetpref.datastore.ui.ListPreference
|
||||
import dev.patrickgold.jetpref.datastore.ui.Preference
|
||||
import dev.patrickgold.jetpref.datastore.ui.PreferenceGroup
|
||||
import dev.patrickgold.jetpref.datastore.ui.SwitchPreference
|
||||
|
||||
@Composable
|
||||
fun SpellingScreen() = FlorisScreen {
|
||||
title = stringRes(R.string.settings__spelling__title)
|
||||
previewFieldVisible = true
|
||||
|
||||
val navController = LocalNavController.current
|
||||
val context = LocalContext.current
|
||||
val extensionManager by context.extensionManager()
|
||||
|
||||
val systemSpellCheckerId by AndroidSettings.Secure.observeAsState(
|
||||
key = "selected_spell_checker",
|
||||
foregroundOnly = true,
|
||||
)
|
||||
val systemSpellCheckerEnabled by AndroidSettings.Secure.observeAsState(
|
||||
key = "spell_checker_enabled",
|
||||
foregroundOnly = true,
|
||||
)
|
||||
val systemSpellCheckerSubtypeIndex by AndroidSettings.Secure.observeAsState(
|
||||
key = "selected_spell_checker_subtype",
|
||||
foregroundOnly = true,
|
||||
)
|
||||
val systemSpellCheckerPkgName = remember(systemSpellCheckerId) {
|
||||
runCatching {
|
||||
ComponentName.unflattenFromString(systemSpellCheckerId!!)!!.packageName
|
||||
}.getOrDefault("null")
|
||||
}
|
||||
val openSystemSpellCheckerSettings = {
|
||||
val componentToLaunch = ComponentName(
|
||||
"com.android.settings",
|
||||
"com.android.settings.Settings\$SpellCheckersSettingsActivity",
|
||||
)
|
||||
context.launchActivity {
|
||||
it.addCategory(Intent.CATEGORY_DEFAULT)
|
||||
it.component = componentToLaunch
|
||||
}
|
||||
}
|
||||
val florisSpellCheckerEnabled =
|
||||
systemSpellCheckerEnabled == "1" &&
|
||||
systemSpellCheckerPkgName == context.packageName &&
|
||||
systemSpellCheckerSubtypeIndex != "0"
|
||||
|
||||
content {
|
||||
PreferenceGroup(title = stringRes(R.string.pref__spelling__active_spellchecker__label)) {
|
||||
Column(modifier = Modifier.padding(horizontal = 8.dp)) {
|
||||
if (systemSpellCheckerEnabled == "1") {
|
||||
if (systemSpellCheckerId == null) {
|
||||
FlorisWarningCard(
|
||||
text = stringRes(R.string.pref__spelling__active_spellchecker__summary_none),
|
||||
onClick = openSystemSpellCheckerSettings,
|
||||
)
|
||||
} else {
|
||||
var spellCheckerIcon: Drawable?
|
||||
var spellCheckerLabel = "Unknown"
|
||||
try {
|
||||
val pm = context.packageManager
|
||||
val remoteAppInfo = pm.getApplicationInfo(systemSpellCheckerPkgName, 0)
|
||||
spellCheckerIcon = pm.getApplicationIcon(remoteAppInfo)
|
||||
spellCheckerLabel = pm.getApplicationLabel(remoteAppInfo).toString()
|
||||
} catch (e: Exception) {
|
||||
spellCheckerIcon = null
|
||||
}
|
||||
FlorisSimpleCard(
|
||||
icon = {
|
||||
if (spellCheckerIcon != null) {
|
||||
FlorisCanvasIcon(
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp)
|
||||
.requiredSize(32.dp),
|
||||
drawable = spellCheckerIcon,
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp)
|
||||
.requiredSize(32.dp),
|
||||
painter = painterResource(R.drawable.ic_help_outline),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
text = spellCheckerLabel,
|
||||
secondaryText = systemSpellCheckerPkgName,
|
||||
contentPadding = PaddingValues(all = 8.dp),
|
||||
onClick = openSystemSpellCheckerSettings,
|
||||
)
|
||||
if (systemSpellCheckerPkgName == context.packageName && systemSpellCheckerSubtypeIndex == "0") {
|
||||
FlorisWarningCard(
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
text = stringRes(
|
||||
R.string.pref__spelling__active_spellchecker__summary_use_sys_lang_set,
|
||||
"use_floris_config" to stringRes(R.string.settings__spelling__use_floris_config),
|
||||
),
|
||||
onClick = openSystemSpellCheckerSettings,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
FlorisErrorCard(
|
||||
text = stringRes(R.string.pref__spelling__active_spellchecker__summary_disabled),
|
||||
onClick = openSystemSpellCheckerSettings,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val spellingDicts by extensionManager.spellingDicts.observeAsState()
|
||||
Preference(
|
||||
iconId = R.drawable.ic_library_books,
|
||||
title = stringRes(R.string.settings__spelling__manage_dicts__title),
|
||||
summary = stringRes(
|
||||
R.string.settings__spelling__manage_dicts__n_installed,
|
||||
"n" to (spellingDicts?.size ?: 0).toString(),
|
||||
),
|
||||
onClick = { navController.navigate(Routes.Settings.ManageSpellingDicts) },
|
||||
enabledIf = { florisSpellCheckerEnabled },
|
||||
)
|
||||
|
||||
PreferenceGroup(title = stringRes(R.string.pref__spelling__group_spellchecker_config__title)) {
|
||||
ListPreference(
|
||||
prefs.spelling.languageMode,
|
||||
iconId = R.drawable.ic_language,
|
||||
title = stringRes(R.string.pref__spelling__language_mode__label),
|
||||
entries = SpellingLanguageMode.listEntries(),
|
||||
enabledIf = { florisSpellCheckerEnabled },
|
||||
)
|
||||
SwitchPreference(
|
||||
prefs.spelling.useContacts,
|
||||
iconId = R.drawable.ic_contacts,
|
||||
title = stringRes(R.string.pref__spelling__use_contacts__label),
|
||||
summary = stringRes(R.string.pref__spelling__use_contacts__summary),
|
||||
enabledIf = { florisSpellCheckerEnabled },
|
||||
)
|
||||
SwitchPreference(
|
||||
prefs.spelling.useUdmEntries,
|
||||
iconId = R.drawable.ic_library_books,
|
||||
title = stringRes(R.string.pref__spelling__use_udm_entries__label),
|
||||
summary = stringRes(R.string.pref__spelling__use_udm_entries__summary),
|
||||
enabledIf = { florisSpellCheckerEnabled },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,12 +72,12 @@ import dev.patrickgold.florisboard.ime.keyboard.Keyboard
|
||||
import dev.patrickgold.florisboard.ime.keyboard.KeyboardMode
|
||||
import dev.patrickgold.florisboard.ime.keyboard.computeIconResId
|
||||
import dev.patrickgold.florisboard.ime.keyboard.computeLabel
|
||||
import dev.patrickgold.florisboard.ime.nlp.NATIVE_NULLPTR
|
||||
import dev.patrickgold.florisboard.ime.input.InputShiftState
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
|
||||
import dev.patrickgold.florisboard.ime.theme.FlorisImeUiSpec
|
||||
import dev.patrickgold.florisboard.keyboardManager
|
||||
import dev.patrickgold.florisboard.lib.NATIVE_NULLPTR
|
||||
import dev.patrickgold.florisboard.lib.android.showShortToast
|
||||
import dev.patrickgold.florisboard.lib.android.stringRes
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisChip
|
||||
@@ -242,7 +242,7 @@ internal fun EditRuleDialog(
|
||||
text = stringRes(R.string.settings__theme_editor__rule_codes),
|
||||
trailingIconTitle = {
|
||||
FlorisIconButton(
|
||||
onClick = { editCodeDialogValue = NATIVE_NULLPTR },
|
||||
onClick = { editCodeDialogValue = NATIVE_NULLPTR.toInt() },
|
||||
modifier = Modifier.offset(x = 12.dp),
|
||||
icon = painterResource(R.drawable.ic_add),
|
||||
)
|
||||
@@ -349,7 +349,7 @@ private fun EditCodeValueDialog(
|
||||
}
|
||||
var showKeyCodesHelp by rememberSaveable(codeValue) { mutableStateOf(false) }
|
||||
var showError by rememberSaveable(codeValue) { mutableStateOf(false) }
|
||||
var errorId by rememberSaveable(codeValue) { mutableStateOf(NATIVE_NULLPTR) }
|
||||
var errorId by rememberSaveable(codeValue) { mutableStateOf(NATIVE_NULLPTR.toInt()) }
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val isFlorisBoardEnabled by InputMethodUtils.observeIsFlorisboardEnabled(foregroundOnly = true)
|
||||
@@ -413,12 +413,12 @@ private fun EditCodeValueDialog(
|
||||
}
|
||||
|
||||
JetPrefAlertDialog(
|
||||
title = stringRes(if (codeValue == NATIVE_NULLPTR) {
|
||||
title = stringRes(if (codeValue == NATIVE_NULLPTR.toInt()) {
|
||||
R.string.settings__theme_editor__add_code
|
||||
} else {
|
||||
R.string.settings__theme_editor__edit_code
|
||||
}),
|
||||
confirmLabel = stringRes(if (codeValue == NATIVE_NULLPTR) {
|
||||
confirmLabel = stringRes(if (codeValue == NATIVE_NULLPTR.toInt()) {
|
||||
R.string.action__add
|
||||
} else {
|
||||
R.string.action__apply
|
||||
@@ -438,7 +438,7 @@ private fun EditCodeValueDialog(
|
||||
showError = true
|
||||
}
|
||||
else -> {
|
||||
if (codeValue != NATIVE_NULLPTR) {
|
||||
if (codeValue != NATIVE_NULLPTR.toInt()) {
|
||||
onDelete(codeValue)
|
||||
}
|
||||
onAdd(code)
|
||||
@@ -448,7 +448,7 @@ private fun EditCodeValueDialog(
|
||||
},
|
||||
dismissLabel = stringRes(R.string.action__cancel),
|
||||
onDismiss = onDismiss,
|
||||
neutralLabel = if (codeValue != NATIVE_NULLPTR) {
|
||||
neutralLabel = if (codeValue != NATIVE_NULLPTR.toInt()) {
|
||||
stringRes(R.string.action__delete)
|
||||
} else {
|
||||
null
|
||||
@@ -559,7 +559,7 @@ private fun TextKeyDataPreviewBox(
|
||||
val context = LocalContext.current
|
||||
val evaluator = remember(context) {
|
||||
object : ComputingEvaluator by DefaultComputingEvaluator {
|
||||
val keyboard = object : Keyboard() {
|
||||
override val keyboard = object : Keyboard() {
|
||||
override val mode = KeyboardMode.NUMERIC_ADVANCED
|
||||
override fun getKeyForPos(pointerX: Float, pointerY: Float) = error("not implemented")
|
||||
override fun keys() = error("not implemented")
|
||||
@@ -567,7 +567,6 @@ private fun TextKeyDataPreviewBox(
|
||||
extendTouchBoundariesDownwards: Boolean) = error("not implemented")
|
||||
}
|
||||
override fun context() = context
|
||||
override fun keyboard() = keyboard
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,10 +19,13 @@ package dev.patrickgold.florisboard.app.settings.theme
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.isImeVisible
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
@@ -60,7 +63,6 @@ import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.insets.LocalWindowInsets
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.ext.ExtensionComponentView
|
||||
import dev.patrickgold.florisboard.app.florisPreferenceModel
|
||||
@@ -106,6 +108,7 @@ internal val IntListSaver = Saver<SnapshotStateList<Int>, ArrayList<Int>>(
|
||||
restore = { it.toMutableStateList() },
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun ThemeEditorScreen(
|
||||
workspace: CacheManager.ExtEditorWorkspace<*>,
|
||||
@@ -194,7 +197,7 @@ fun ThemeEditorScreen(
|
||||
handleBackPress()
|
||||
}
|
||||
|
||||
val isImeVisible = LocalWindowInsets.current.ime.isVisible
|
||||
val isImeVisible = WindowInsets.isImeVisible
|
||||
LaunchedEffect(showEditComponentMetaDialog, showFineTuneDialog, snyggRuleToEdit, snyggPropertyToEdit) {
|
||||
val visible = showEditComponentMetaDialog || showFineTuneDialog ||
|
||||
snyggRuleToEdit != null || snyggPropertyToEdit != null
|
||||
|
||||
@@ -75,13 +75,21 @@ internal fun translateElementName(element: String, level: SnyggLevel): String? {
|
||||
FlorisImeUi.ExtractedLandscapeInputField -> R.string.snygg__rule_element__extracted_landscape_input_field
|
||||
FlorisImeUi.ExtractedLandscapeInputAction -> R.string.snygg__rule_element__extracted_landscape_input_action
|
||||
FlorisImeUi.GlideTrail -> R.string.snygg__rule_element__glide_trail
|
||||
FlorisImeUi.IncognitoModeIndicator -> R.string.snygg__rule_element__incognito_mode_indicator
|
||||
FlorisImeUi.OneHandedPanel -> R.string.snygg__rule_element__one_handed_panel
|
||||
FlorisImeUi.SmartbarPrimaryRow -> R.string.snygg__rule_element__smartbar_primary_row
|
||||
FlorisImeUi.SmartbarSecondaryRow -> R.string.snygg__rule_element__smartbar_secondary_row
|
||||
FlorisImeUi.SmartbarPrimaryActionsToggle -> R.string.snygg__rule_element__smartbar_primary_actions_toggle
|
||||
FlorisImeUi.SmartbarSecondaryActionsToggle -> R.string.snygg__rule_element__smartbar_secondary_actions_toggle
|
||||
FlorisImeUi.SmartbarQuickAction -> R.string.snygg__rule_element__smartbar_quick_action
|
||||
FlorisImeUi.SmartbarKey -> R.string.snygg__rule_element__smartbar_key
|
||||
FlorisImeUi.Smartbar -> R.string.snygg__rule_element__smartbar
|
||||
FlorisImeUi.SmartbarSharedActionsRow -> R.string.snygg__rule_element__smartbar_shared_actions_row
|
||||
FlorisImeUi.SmartbarSharedActionsToggle -> R.string.snygg__rule_element__smartbar_shared_actions_toggle
|
||||
FlorisImeUi.SmartbarExtendedActionsRow -> R.string.snygg__rule_element__smartbar_extended_actions_row
|
||||
FlorisImeUi.SmartbarExtendedActionsToggle -> R.string.snygg__rule_element__smartbar_extended_actions_toggle
|
||||
FlorisImeUi.SmartbarActionKey -> R.string.snygg__rule_element__smartbar_action_key
|
||||
FlorisImeUi.SmartbarActionTile -> R.string.snygg__rule_element__smartbar_action_tile
|
||||
FlorisImeUi.SmartbarActionsOverflow -> R.string.snygg__rule_element__smartbar_actions_overflow
|
||||
FlorisImeUi.SmartbarActionsOverflowCustomizeButton -> R.string.snygg__rule_element__smartbar_actions_overflow_customize_button
|
||||
FlorisImeUi.SmartbarActionsEditor -> R.string.snygg__rule_element__smartbar_actions_editor
|
||||
FlorisImeUi.SmartbarActionsEditorHeader -> R.string.snygg__rule_element__smartbar_actions_editor_header
|
||||
FlorisImeUi.SmartbarActionsEditorSubheader -> R.string.snygg__rule_element__smartbar_actions_editor_subheader
|
||||
FlorisImeUi.SmartbarCandidatesRow -> R.string.snygg__rule_element__smartbar_candidates_row
|
||||
FlorisImeUi.SmartbarCandidateWord -> R.string.snygg__rule_element__smartbar_candidate_word
|
||||
FlorisImeUi.SmartbarCandidateClip -> R.string.snygg__rule_element__smartbar_candidate_clip
|
||||
FlorisImeUi.SmartbarCandidateSpacer -> R.string.snygg__rule_element__smartbar_candidate_spacer
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.settings.typing
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.lib.android.AndroidSettings
|
||||
import dev.patrickgold.florisboard.lib.android.launchActivity
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisCanvasIcon
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisErrorCard
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisSimpleCard
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisWarningCard
|
||||
import dev.patrickgold.florisboard.lib.compose.stringRes
|
||||
|
||||
@Composable
|
||||
fun SpellCheckerServiceSelector(florisSpellCheckerEnabled: MutableState<Boolean>) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val systemSpellCheckerId by AndroidSettings.Secure.observeAsState(
|
||||
key = "selected_spell_checker",
|
||||
foregroundOnly = true,
|
||||
)
|
||||
val systemSpellCheckerEnabled by AndroidSettings.Secure.observeAsState(
|
||||
key = "spell_checker_enabled",
|
||||
foregroundOnly = true,
|
||||
)
|
||||
val systemSpellCheckerPkgName = remember(systemSpellCheckerId) {
|
||||
runCatching {
|
||||
ComponentName.unflattenFromString(systemSpellCheckerId!!)!!.packageName
|
||||
}.getOrDefault("null")
|
||||
}
|
||||
val openSystemSpellCheckerSettings = {
|
||||
val componentToLaunch = ComponentName(
|
||||
"com.android.settings",
|
||||
"com.android.settings.Settings\$SpellCheckersSettingsActivity",
|
||||
)
|
||||
context.launchActivity {
|
||||
it.addCategory(Intent.CATEGORY_DEFAULT)
|
||||
it.component = componentToLaunch
|
||||
}
|
||||
}
|
||||
florisSpellCheckerEnabled.value =
|
||||
systemSpellCheckerEnabled == "1" &&
|
||||
systemSpellCheckerPkgName == context.packageName
|
||||
|
||||
Column(modifier = Modifier.padding(horizontal = 8.dp)) {
|
||||
if (systemSpellCheckerEnabled == "1") {
|
||||
if (systemSpellCheckerId == null) {
|
||||
FlorisWarningCard(
|
||||
text = stringRes(R.string.pref__spelling__active_spellchecker__summary_none),
|
||||
onClick = openSystemSpellCheckerSettings,
|
||||
)
|
||||
} else {
|
||||
var spellCheckerIcon: Drawable?
|
||||
var spellCheckerLabel = "Unknown"
|
||||
try {
|
||||
val pm = context.packageManager
|
||||
val remoteAppInfo = pm.getApplicationInfo(systemSpellCheckerPkgName, 0)
|
||||
spellCheckerIcon = pm.getApplicationIcon(remoteAppInfo)
|
||||
spellCheckerLabel = pm.getApplicationLabel(remoteAppInfo).toString()
|
||||
} catch (e: Exception) {
|
||||
spellCheckerIcon = null
|
||||
}
|
||||
FlorisSimpleCard(
|
||||
icon = {
|
||||
if (spellCheckerIcon != null) {
|
||||
FlorisCanvasIcon(
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp)
|
||||
.requiredSize(32.dp),
|
||||
drawable = spellCheckerIcon,
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp)
|
||||
.requiredSize(32.dp),
|
||||
painter = painterResource(R.drawable.ic_help_outline),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
text = spellCheckerLabel,
|
||||
secondaryText = systemSpellCheckerPkgName,
|
||||
contentPadding = PaddingValues(all = 8.dp),
|
||||
onClick = openSystemSpellCheckerSettings,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
FlorisErrorCard(
|
||||
text = stringRes(R.string.pref__spelling__active_spellchecker__summary_disabled),
|
||||
onClick = openSystemSpellCheckerSettings,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,16 +16,24 @@
|
||||
|
||||
package dev.patrickgold.florisboard.app.settings.typing
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.text.smartbar.CandidatesDisplayMode
|
||||
import dev.patrickgold.florisboard.ime.nlp.SpellingLanguageMode
|
||||
import dev.patrickgold.florisboard.lib.android.AndroidVersion
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisErrorCard
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisHyperlinkText
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
|
||||
import dev.patrickgold.florisboard.lib.compose.stringRes
|
||||
import dev.patrickgold.jetpref.datastore.model.observeAsState
|
||||
import dev.patrickgold.jetpref.datastore.ui.DialogSliderPreference
|
||||
import dev.patrickgold.jetpref.datastore.ui.ExperimentalJetPrefDatastoreUi
|
||||
import dev.patrickgold.jetpref.datastore.ui.ListPreference
|
||||
@@ -39,31 +47,25 @@ fun TypingScreen() = FlorisScreen {
|
||||
previewFieldVisible = true
|
||||
|
||||
content {
|
||||
// This card is temporary and is therefore not using a string resource
|
||||
FlorisErrorCard(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
text = """
|
||||
Suggestions (except system autofill) and spell checking are not available in this alpha release. All
|
||||
preferences in the "Corrections" group are properly implemented though.
|
||||
""".trimIndent().replace('\n', ' '),
|
||||
)
|
||||
|
||||
PreferenceGroup(title = stringRes(R.string.pref__suggestion__title)) {
|
||||
SwitchPreference(
|
||||
prefs.suggestion.api30InlineSuggestionsEnabled,
|
||||
title = stringRes(R.string.pref__suggestion__api30_inline_suggestions_enabled__label),
|
||||
summary = stringRes(R.string.pref__suggestion__api30_inline_suggestions_enabled__summary),
|
||||
visibleIf = { AndroidVersion.ATLEAST_API30_R },
|
||||
)
|
||||
// This card is temporary and is therefore not using a string resource
|
||||
FlorisErrorCard(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
text = if (AndroidVersion.ATLEAST_API30_R) {
|
||||
"Suggestions (except autofill) are not available in this release"
|
||||
} else {
|
||||
"Suggestions are not available in this release"
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
prefs.suggestion.enabled,
|
||||
title = stringRes(R.string.pref__suggestion__enabled__label),
|
||||
//summary = stringRes(R.string.pref__suggestion__enabled__summary),
|
||||
summary = stringRes(R.string.pref__suggestion__enabled__summary),
|
||||
)
|
||||
ListPreference(
|
||||
prefs.suggestion.displayMode,
|
||||
title = stringRes(R.string.pref__suggestion__display_mode__label),
|
||||
entries = CandidatesDisplayMode.listEntries(),
|
||||
SwitchPreference(
|
||||
prefs.suggestion.blockPossiblyOffensive,
|
||||
title = stringRes(R.string.pref__suggestion__block_possibly_offensive__label),
|
||||
summary = stringRes(R.string.pref__suggestion__block_possibly_offensive__summary),
|
||||
enabledIf = { prefs.suggestion.enabled isEqualTo true },
|
||||
)
|
||||
SwitchPreference(
|
||||
@@ -75,13 +77,18 @@ fun TypingScreen() = FlorisScreen {
|
||||
DialogSliderPreference(
|
||||
prefs.suggestion.clipboardContentTimeout,
|
||||
title = stringRes(R.string.pref__suggestion__clipboard_content_timeout__label),
|
||||
valueLabel = { stringRes(R.string.unit__seconds__symbol, "v" to it) },
|
||||
valueLabel = { stringRes(R.string.pref__suggestion__clipboard_content_timeout__summary, "v" to it) },
|
||||
min = 30,
|
||||
max = 300,
|
||||
stepIncrement = 5,
|
||||
enabledIf = {
|
||||
(prefs.suggestion.enabled isEqualTo true) && (prefs.suggestion.clipboardContentEnabled isEqualTo true)
|
||||
}
|
||||
enabledIf = { prefs.suggestion.enabled isEqualTo true },
|
||||
visibleIf = { prefs.suggestion.clipboardContentEnabled isEqualTo true },
|
||||
)
|
||||
SwitchPreference(
|
||||
prefs.suggestion.api30InlineSuggestionsEnabled,
|
||||
title = stringRes(R.string.pref__suggestion__api30_inline_suggestions_enabled__label),
|
||||
summary = stringRes(R.string.pref__suggestion__api30_inline_suggestions_enabled__summary),
|
||||
visibleIf = { AndroidVersion.ATLEAST_API30_R },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -91,6 +98,30 @@ fun TypingScreen() = FlorisScreen {
|
||||
title = stringRes(R.string.pref__correction__auto_capitalization__label),
|
||||
summary = stringRes(R.string.pref__correction__auto_capitalization__summary),
|
||||
)
|
||||
val isAutoSpacePunctuationEnabled by prefs.correction.autoSpacePunctuation.observeAsState()
|
||||
SwitchPreference(
|
||||
prefs.correction.autoSpacePunctuation,
|
||||
iconId = R.drawable.ic_space_bar,
|
||||
title = stringRes(R.string.pref__correction__auto_space_punctuation__label),
|
||||
summary = stringRes(R.string.pref__correction__auto_space_punctuation__summary),
|
||||
)
|
||||
if (isAutoSpacePunctuationEnabled) {
|
||||
Card(modifier = Modifier.padding(8.dp)) {
|
||||
Column(modifier = Modifier.padding(8.dp)) {
|
||||
Text(
|
||||
text = """
|
||||
Auto-space after punctuation is an experimental feature which may break or behave
|
||||
unexpectedly. If you want, please give feedback about it in below linked feedback
|
||||
thread. This helps a lot in improving this feature. Thanks!
|
||||
""".trimIndent().replace('\n', ' '),
|
||||
)
|
||||
FlorisHyperlinkText(
|
||||
text = "Feedback thread (GitHub)",
|
||||
url = "https://github.com/florisboard/florisboard/discussions/1935",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
SwitchPreference(
|
||||
prefs.correction.rememberCapsLockState,
|
||||
title = stringRes(R.string.pref__correction__remember_caps_lock_state__label),
|
||||
@@ -102,5 +133,33 @@ fun TypingScreen() = FlorisScreen {
|
||||
summary = stringRes(R.string.pref__correction__double_space_period__summary),
|
||||
)
|
||||
}
|
||||
|
||||
PreferenceGroup(title = stringRes(R.string.pref__spelling__title)) {
|
||||
val florisSpellCheckerEnabled = remember { mutableStateOf(false) }
|
||||
SpellCheckerServiceSelector(florisSpellCheckerEnabled)
|
||||
ListPreference(
|
||||
prefs.spelling.languageMode,
|
||||
iconId = R.drawable.ic_language,
|
||||
title = stringRes(R.string.pref__spelling__language_mode__label),
|
||||
entries = SpellingLanguageMode.listEntries(),
|
||||
enabledIf = { florisSpellCheckerEnabled.value },
|
||||
)
|
||||
SwitchPreference(
|
||||
prefs.spelling.useContacts,
|
||||
iconId = R.drawable.ic_contacts,
|
||||
title = stringRes(R.string.pref__spelling__use_contacts__label),
|
||||
summary = stringRes(R.string.pref__spelling__use_contacts__summary),
|
||||
enabledIf = { florisSpellCheckerEnabled.value },
|
||||
visibleIf = { false }, // For now
|
||||
)
|
||||
SwitchPreference(
|
||||
prefs.spelling.useUdmEntries,
|
||||
iconId = R.drawable.ic_library_books,
|
||||
title = stringRes(R.string.pref__spelling__use_udm_entries__label),
|
||||
summary = stringRes(R.string.pref__spelling__use_udm_entries__summary),
|
||||
enabledIf = { florisSpellCheckerEnabled.value },
|
||||
visibleIf = { false }, // For now
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.splash
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.LocalNavController
|
||||
import dev.patrickgold.florisboard.app.Routes
|
||||
import dev.patrickgold.florisboard.app.florisPreferenceModel
|
||||
import dev.patrickgold.florisboard.lib.compose.FlorisCanvasIcon
|
||||
import dev.patrickgold.florisboard.lib.compose.LocalPreviewFieldController
|
||||
import dev.patrickgold.jetpref.datastore.model.observeAsState
|
||||
|
||||
@Composable
|
||||
fun SplashScreen() = Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val prefs by florisPreferenceModel()
|
||||
|
||||
val isModelLoaded by prefs.datastoreReadyStatus.observeAsState()
|
||||
val isImeSetUp by prefs.internal.isImeSetUp.observeAsState()
|
||||
val navController = LocalNavController.current
|
||||
val previewFieldController = LocalPreviewFieldController.current
|
||||
|
||||
SideEffect {
|
||||
previewFieldController?.isVisible = false
|
||||
}
|
||||
|
||||
LaunchedEffect(isModelLoaded) {
|
||||
if (isModelLoaded) {
|
||||
navController.navigate(if (isImeSetUp) Routes.Settings.Home else Routes.Setup.Screen) {
|
||||
popUpTo(Routes.Splash.Screen) {
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FlorisCanvasIcon(
|
||||
modifier = Modifier.requiredSize(92.dp),
|
||||
iconId = R.mipmap.floris_app_icon,
|
||||
contentDescription = "FlorisBoard app icon",
|
||||
)
|
||||
}
|
||||
@@ -131,7 +131,7 @@ fun ClipboardInputLayout(
|
||||
val historyEnabled by prefs.clipboard.historyEnabled.observeAsState()
|
||||
val history by clipboardManager.history.observeAsNonNullState()
|
||||
|
||||
val innerHeight = FlorisImeSizing.keyboardUiHeight() - FlorisImeSizing.smartbarHeight
|
||||
val innerHeight = FlorisImeSizing.imeUiHeight() - FlorisImeSizing.smartbarHeight
|
||||
var popupItem by remember(history) { mutableStateOf<ClipboardItem?>(null) }
|
||||
var showClearAllHistory by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -625,13 +625,17 @@ private fun ClipTextItemDescription(
|
||||
val iconId: Int?
|
||||
val description: String?
|
||||
when {
|
||||
NetworkUtils.isEmailAddress(text) -> {
|
||||
iconId = R.drawable.ic_email
|
||||
description = stringRes(R.string.clipboard__item_description_email)
|
||||
}
|
||||
NetworkUtils.isUrl(text) -> {
|
||||
iconId = R.drawable.ic_link
|
||||
description = stringRes(R.string.clipboard__item_description_url)
|
||||
}
|
||||
NetworkUtils.isEmailAddress(text) -> {
|
||||
iconId = R.drawable.ic_email
|
||||
description = stringRes(R.string.clipboard__item_description_email)
|
||||
NetworkUtils.isPhoneNumber(text) -> {
|
||||
iconId = R.drawable.ic_phone
|
||||
description = stringRes(R.string.clipboard__item_description_phone)
|
||||
}
|
||||
else -> {
|
||||
iconId = null
|
||||
|
||||
@@ -39,6 +39,8 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@@ -104,8 +106,11 @@ class ClipboardManager(
|
||||
|
||||
private val primaryClipLastFromCallbackGuard = Mutex(locked = false)
|
||||
private var primaryClipLastFromCallback: ClipData? = null
|
||||
private val _primaryClip = MutableLiveData<ClipboardItem?>(null)
|
||||
val primaryClip: LiveData<ClipboardItem?> get() = _primaryClip
|
||||
private val _primaryClipFlow = MutableStateFlow<ClipboardItem?>(null)
|
||||
val primaryClipFlow = _primaryClipFlow.asStateFlow()
|
||||
inline var primaryClip
|
||||
get() = primaryClipFlow.value
|
||||
private set(v) { _primaryClipFlow.value = v }
|
||||
|
||||
init {
|
||||
systemClipboardManager.addPrimaryClipChangedListener(this)
|
||||
@@ -139,15 +144,11 @@ class ClipboardManager(
|
||||
|
||||
fun history(): ClipboardHistory = history.value!!
|
||||
|
||||
fun primaryClip(): ClipboardItem? {
|
||||
return primaryClip.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current primary clip without updating the internal clipboard history.
|
||||
*/
|
||||
fun setPrimaryClip(item: ClipboardItem?) {
|
||||
_primaryClip.postValue(item)
|
||||
fun updatePrimaryClip(item: ClipboardItem?) {
|
||||
primaryClip = item
|
||||
if (prefs.clipboard.useInternalClipboard.get()) {
|
||||
// Purposely do not sync to system if disabled in prefs
|
||||
if (prefs.clipboard.syncToSystem.get()) {
|
||||
@@ -178,22 +179,22 @@ class ClipboardManager(
|
||||
}
|
||||
if (isDuplicate) return@launch
|
||||
|
||||
val internalPrimaryClip = primaryClip.value
|
||||
val internalPrimaryClip = primaryClip
|
||||
|
||||
if (systemPrimaryClip == null) {
|
||||
_primaryClip.postValue(null)
|
||||
primaryClip = null
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (systemPrimaryClip.getItemAt(0).let { it.text == null && it.uri == null }) {
|
||||
_primaryClip.postValue(null)
|
||||
primaryClip = null
|
||||
return@launch
|
||||
}
|
||||
|
||||
val isEqual = internalPrimaryClip?.isEqualTo(systemPrimaryClip) == true
|
||||
if (!isEqual) {
|
||||
val item = ClipboardItem.fromClipData(appContext, systemPrimaryClip, cloneUri = true)
|
||||
_primaryClip.postValue(item)
|
||||
primaryClip = item
|
||||
insertOrMoveBeginning(item)
|
||||
}
|
||||
}
|
||||
@@ -205,7 +206,7 @@ class ClipboardManager(
|
||||
*/
|
||||
private fun addNewClip(item: ClipboardItem) {
|
||||
insertOrMoveBeginning(item)
|
||||
setPrimaryClip(item)
|
||||
updatePrimaryClip(item)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,11 +23,11 @@ import dev.patrickgold.florisboard.ime.keyboard.extCoreCurrencySet
|
||||
import dev.patrickgold.florisboard.ime.keyboard.extCoreLayout
|
||||
import dev.patrickgold.florisboard.ime.keyboard.extCorePopupMapping
|
||||
import dev.patrickgold.florisboard.ime.keyboard.extCorePunctuationRule
|
||||
import dev.patrickgold.florisboard.ime.nlp.latin.LatinLanguageProvider
|
||||
import dev.patrickgold.florisboard.lib.FlorisLocale
|
||||
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
|
||||
/**
|
||||
* Data class which represents an user-specified set of language and layout. String representations
|
||||
@@ -36,6 +36,7 @@ import kotlinx.serialization.Transient
|
||||
* @property id The ID of this subtype.
|
||||
* @property primaryLocale The primary locale of this subtype.
|
||||
* @property secondaryLocales The secondary locales of this subtype. May be an empty list.
|
||||
* @property nlpProviders The NLP provider map to instantiate the correct provider for each category.
|
||||
* @property composer The composer name to composer characters the way they should.
|
||||
* @property currencySet The currency set name to display the correct currency symbols for this subtype.
|
||||
* @property punctuationRule The punctuation rule to correctly insert auto-spaces.
|
||||
@@ -47,6 +48,7 @@ data class Subtype(
|
||||
val id: Long,
|
||||
val primaryLocale: FlorisLocale,
|
||||
val secondaryLocales: List<FlorisLocale>,
|
||||
val nlpProviders: SubtypeNlpProviderMap = SubtypeNlpProviderMap(),
|
||||
val composer: ExtensionComponentName,
|
||||
val currencySet: ExtensionComponentName,
|
||||
val punctuationRule: ExtensionComponentName = extCorePunctuationRule("default"),
|
||||
@@ -61,6 +63,7 @@ data class Subtype(
|
||||
id = -1,
|
||||
primaryLocale = FlorisLocale.from("en", "US"),
|
||||
secondaryLocales = emptyList(),
|
||||
nlpProviders = SubtypeNlpProviderMap(),
|
||||
composer = extCoreComposer("appender"),
|
||||
currencySet = extCoreCurrencySet("dollar"),
|
||||
punctuationRule = extCorePunctuationRule("default"),
|
||||
@@ -81,8 +84,10 @@ data class Subtype(
|
||||
fun equalsExcludingId(other: Subtype): Boolean {
|
||||
if (other.primaryLocale != primaryLocale) return false
|
||||
if (other.secondaryLocales != secondaryLocales) return false
|
||||
if (other.nlpProviders != nlpProviders) return false
|
||||
if (other.composer != composer) return false
|
||||
if (other.currencySet != currencySet) return false
|
||||
if (other.punctuationRule != punctuationRule) return false
|
||||
if (other.popupMapping != popupMapping) return false
|
||||
if (other.layoutMap != layoutMap) return false
|
||||
|
||||
@@ -109,8 +114,6 @@ data class SubtypeLayoutMap(
|
||||
@SerialName(LayoutTypeId.PHONE2)
|
||||
val phone2: ExtensionComponentName = PHONE2_DEFAULT,
|
||||
) {
|
||||
@Transient private var _hashCode: Int = 0
|
||||
|
||||
companion object {
|
||||
private const val EQUALS = "="
|
||||
private const val DELIMITER = ","
|
||||
@@ -125,18 +128,6 @@ data class SubtypeLayoutMap(
|
||||
private val PHONE2_DEFAULT = extCoreLayout("telpad")
|
||||
}
|
||||
|
||||
init {
|
||||
var result = characters.hashCode()
|
||||
result = 31 * result + symbols.hashCode()
|
||||
result = 31 * result + symbols2.hashCode()
|
||||
result = 31 * result + numeric.hashCode()
|
||||
result = 31 * result + numericAdvanced.hashCode()
|
||||
result = 31 * result + numericRow.hashCode()
|
||||
result = 31 * result + phone.hashCode()
|
||||
result = 31 * result + phone2.hashCode()
|
||||
_hashCode = result
|
||||
}
|
||||
|
||||
operator fun get(layoutType: LayoutType): ExtensionComponentName? {
|
||||
return when (layoutType) {
|
||||
LayoutType.CHARACTERS -> characters
|
||||
@@ -212,27 +203,16 @@ data class SubtypeLayoutMap(
|
||||
append(EQUALS)
|
||||
append(phone2)
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as SubtypeLayoutMap
|
||||
|
||||
if (characters != other.characters) return false
|
||||
if (symbols != other.symbols) return false
|
||||
if (symbols2 != other.symbols2) return false
|
||||
if (numeric != other.numeric) return false
|
||||
if (numericAdvanced != other.numericAdvanced) return false
|
||||
if (numericRow != other.numericRow) return false
|
||||
if (phone != other.phone) return false
|
||||
if (phone2 != other.phone2) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return _hashCode
|
||||
@Serializable
|
||||
data class SubtypeNlpProviderMap(
|
||||
val spelling: String = LatinLanguageProvider.ProviderId,
|
||||
val suggestion: String = LatinLanguageProvider.ProviderId,
|
||||
) {
|
||||
inline fun forEach(action: (String, String) -> Unit) {
|
||||
action("spelling", spelling)
|
||||
action("suggestion", suggestion)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,6 +228,7 @@ data class SubtypePreset(
|
||||
@Serializable(with = FlorisLocale.Serializer::class)
|
||||
@SerialName("languageTag")
|
||||
val locale: FlorisLocale,
|
||||
val nlpProviders: SubtypeNlpProviderMap = SubtypeNlpProviderMap(),
|
||||
val composer: ExtensionComponentName,
|
||||
val currencySet: ExtensionComponentName,
|
||||
val punctuationRule: ExtensionComponentName = extCorePunctuationRule("default"),
|
||||
@@ -258,7 +239,8 @@ data class SubtypePreset(
|
||||
return Subtype(
|
||||
id = -1,
|
||||
primaryLocale = locale,
|
||||
secondaryLocales = listOf(),
|
||||
secondaryLocales = emptyList(),
|
||||
nlpProviders = nlpProviders,
|
||||
composer = composer,
|
||||
currencySet = currencySet,
|
||||
punctuationRule = punctuationRule,
|
||||
|
||||
@@ -17,15 +17,13 @@
|
||||
package dev.patrickgold.florisboard.ime.core
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dev.patrickgold.florisboard.app.florisPreferenceModel
|
||||
import dev.patrickgold.florisboard.ime.keyboard.CurrencySet
|
||||
import dev.patrickgold.florisboard.keyboardManager
|
||||
import dev.patrickgold.florisboard.lib.FlorisLocale
|
||||
import dev.patrickgold.florisboard.lib.devtools.flogDebug
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -37,21 +35,24 @@ val SubtypeJsonConfig = Json {
|
||||
}
|
||||
|
||||
/**
|
||||
* Class which acts as a high level helper for the raw implementation of subtypes in the prefs.
|
||||
* Also interprets the default subtype list defined in ime/config.json and provides helper
|
||||
* arrays for the language spinner.
|
||||
*
|
||||
* @property prefs Reference to the preferences, where the raw subtype settings are accessible.
|
||||
* @property subtypes The currently active subtypes.
|
||||
* Class which acts as a high level helper for the raw implementation of subtypes in the prefs. Additionally provides
|
||||
* helper methods for the in-keyboard language switch process.
|
||||
*/
|
||||
class SubtypeManager(context: Context) : CoroutineScope by MainScope() {
|
||||
class SubtypeManager(context: Context) {
|
||||
private val prefs by florisPreferenceModel()
|
||||
private val keyboardManager by context.keyboardManager()
|
||||
|
||||
private val _subtypes = MutableLiveData<List<Subtype>>(emptyList())
|
||||
val subtypes: LiveData<List<Subtype>> get() = _subtypes
|
||||
private val _activeSubtype = MutableLiveData(Subtype.DEFAULT)
|
||||
val activeSubtype: LiveData<Subtype> get() = _activeSubtype
|
||||
private val _subtypesFlow = MutableStateFlow(listOf<Subtype>())
|
||||
val subtypesFlow = _subtypesFlow.asStateFlow()
|
||||
inline var subtypes
|
||||
get() = subtypesFlow.value
|
||||
private set(v) { _subtypesFlow.value = v }
|
||||
|
||||
private val _activeSubtypeFlow = MutableStateFlow(Subtype.DEFAULT)
|
||||
val activeSubtypeFlow = _activeSubtypeFlow.asStateFlow()
|
||||
inline var activeSubtype
|
||||
get() = activeSubtypeFlow.value
|
||||
private set(v) { _activeSubtypeFlow.value = v }
|
||||
|
||||
init {
|
||||
prefs.localization.subtypes.observeForever { listRaw ->
|
||||
@@ -61,15 +62,11 @@ class SubtypeManager(context: Context) : CoroutineScope by MainScope() {
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
_subtypes.postValue(list)
|
||||
subtypes = list
|
||||
evaluateActiveSubtype(list)
|
||||
}
|
||||
}
|
||||
|
||||
fun subtypes() = _subtypes.value!!
|
||||
|
||||
fun activeSubtype() = _activeSubtype.value!!
|
||||
|
||||
private fun persistNewSubtypeList(list: List<Subtype>) {
|
||||
val listRaw = SubtypeJsonConfig.encodeToString(list)
|
||||
prefs.localization.subtypes.set(listRaw)
|
||||
@@ -88,7 +85,7 @@ class SubtypeManager(context: Context) : CoroutineScope by MainScope() {
|
||||
if (subtype.id != activeSubtypeId) {
|
||||
prefs.localization.activeSubtypeId.set(subtype.id)
|
||||
}
|
||||
_activeSubtype.postValue(subtype)
|
||||
activeSubtype = subtype
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,7 +97,7 @@ class SubtypeManager(context: Context) : CoroutineScope by MainScope() {
|
||||
*/
|
||||
fun addSubtype(subtype: Subtype): Boolean {
|
||||
val subtypeToAdd = subtype.copy(id = System.currentTimeMillis())
|
||||
val subtypeList = _subtypes.value!!
|
||||
val subtypeList = subtypes
|
||||
if (subtypeList.find { it.equalsExcludingId(subtype) } != null) {
|
||||
return false
|
||||
}
|
||||
@@ -126,7 +123,7 @@ class SubtypeManager(context: Context) : CoroutineScope by MainScope() {
|
||||
* @return The subtype or null, if no matching subtype could be found.
|
||||
*/
|
||||
fun getSubtypeById(id: Long): Subtype? {
|
||||
val subtypeList = _subtypes.value!!
|
||||
val subtypeList = subtypes
|
||||
return subtypeList.find { it.id == id }
|
||||
}
|
||||
|
||||
@@ -149,7 +146,7 @@ class SubtypeManager(context: Context) : CoroutineScope by MainScope() {
|
||||
* @param subtypeToModify The subtype with the new details but same id.
|
||||
*/
|
||||
fun modifySubtypeWithSameId(subtypeToModify: Subtype) {
|
||||
val subtypeList = _subtypes.value!!
|
||||
val subtypeList = subtypes
|
||||
val index = subtypeList.indexOfFirst { subtypeToModify.id == it.id }
|
||||
if (index >= 0 && index < subtypeList.size) {
|
||||
val newSubtypeList = subtypeList.mapIndexed { n, subtype ->
|
||||
@@ -170,7 +167,7 @@ class SubtypeManager(context: Context) : CoroutineScope by MainScope() {
|
||||
* @param subtypeToRemove The subtype which should be removed.
|
||||
*/
|
||||
fun removeSubtype(subtypeToRemove: Subtype) {
|
||||
val subtypeList = _subtypes.value!!
|
||||
val subtypeList = subtypes
|
||||
val indexToRemove = subtypeList.indexOf(subtypeToRemove)
|
||||
if (indexToRemove in subtypeList.indices) {
|
||||
val newSubtypeList = subtypeList.mapIndexedNotNull { n, subtype ->
|
||||
@@ -189,15 +186,15 @@ class SubtypeManager(context: Context) : CoroutineScope by MainScope() {
|
||||
* Switch to the previous subtype in the subtype list if possible.
|
||||
*/
|
||||
fun switchToPrevSubtype() {
|
||||
val subtypeList = _subtypes.value!!
|
||||
val activeSubtype = _activeSubtype.value!!
|
||||
val subtypeList = subtypes
|
||||
val cachedActiveSubtype = activeSubtype
|
||||
var triggerNextSubtype = false
|
||||
var newActiveSubtype: Subtype = Subtype.DEFAULT
|
||||
for (subtype in subtypeList.asReversed()) {
|
||||
if (triggerNextSubtype) {
|
||||
triggerNextSubtype = false
|
||||
newActiveSubtype = subtype
|
||||
} else if (subtype == activeSubtype) {
|
||||
} else if (subtype == cachedActiveSubtype) {
|
||||
triggerNextSubtype = true
|
||||
}
|
||||
}
|
||||
@@ -205,22 +202,22 @@ class SubtypeManager(context: Context) : CoroutineScope by MainScope() {
|
||||
newActiveSubtype = subtypeList.last()
|
||||
}
|
||||
prefs.localization.activeSubtypeId.set(newActiveSubtype.id)
|
||||
_activeSubtype.postValue(newActiveSubtype)
|
||||
activeSubtype = newActiveSubtype
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to the next subtype in the subtype list if possible.
|
||||
*/
|
||||
fun switchToNextSubtype() {
|
||||
val subtypeList = _subtypes.value!!
|
||||
val activeSubtype = _activeSubtype.value!!
|
||||
val subtypeList = subtypes
|
||||
val cachedActiveSubtype = activeSubtype
|
||||
var triggerNextSubtype = false
|
||||
var newActiveSubtype: Subtype = Subtype.DEFAULT
|
||||
for (subtype in subtypeList) {
|
||||
if (triggerNextSubtype) {
|
||||
triggerNextSubtype = false
|
||||
newActiveSubtype = subtype
|
||||
} else if (subtype == activeSubtype) {
|
||||
} else if (subtype == cachedActiveSubtype) {
|
||||
triggerNextSubtype = true
|
||||
}
|
||||
}
|
||||
@@ -228,6 +225,6 @@ class SubtypeManager(context: Context) : CoroutineScope by MainScope() {
|
||||
newActiveSubtype = subtypeList.first()
|
||||
}
|
||||
prefs.localization.activeSubtypeId.set(newActiveSubtype.id)
|
||||
_activeSubtype.postValue(newActiveSubtype)
|
||||
activeSubtype = newActiveSubtype
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.dictionary
|
||||
|
||||
import dev.patrickgold.florisboard.ime.nlp.SuggestionList
|
||||
import dev.patrickgold.florisboard.ime.nlp.Word
|
||||
import dev.patrickgold.florisboard.lib.io.Asset
|
||||
|
||||
/**
|
||||
* Standardized dictionary interface for interacting with dictionaries.
|
||||
*/
|
||||
interface Dictionary : Asset {
|
||||
/**
|
||||
* Gets token predictions based on the given [precedingTokens] and the [currentToken]. The
|
||||
* length of the returned list is limited to [maxSuggestionCount]. Note that the returned list
|
||||
* may at any time give back less items than [maxSuggestionCount] indicates.
|
||||
*/
|
||||
fun getTokenPredictions(
|
||||
precedingTokens: List<Word>,
|
||||
currentToken: Word?,
|
||||
maxSuggestionCount: Int,
|
||||
allowPossiblyOffensive: Boolean,
|
||||
destSuggestionList: SuggestionList
|
||||
)
|
||||
|
||||
fun getDate(): Long
|
||||
|
||||
fun getVersion(): Int
|
||||
}
|
||||
|
||||
interface MutableDictionary : Dictionary {
|
||||
fun trainTokenPredictions(
|
||||
precedingTokens: List<Word>,
|
||||
lastToken: Word
|
||||
)
|
||||
|
||||
fun setDate(date: Int)
|
||||
|
||||
fun setVersion(version: Int)
|
||||
}
|
||||
@@ -19,34 +19,22 @@ package dev.patrickgold.florisboard.ime.dictionary
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import dev.patrickgold.florisboard.app.florisPreferenceModel
|
||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||
import dev.patrickgold.florisboard.ime.nlp.SuggestionList
|
||||
import dev.patrickgold.florisboard.ime.nlp.Word
|
||||
import dev.patrickgold.florisboard.ime.nlp.SuggestionCandidate
|
||||
import dev.patrickgold.florisboard.ime.nlp.WordSuggestionCandidate
|
||||
import dev.patrickgold.florisboard.lib.FlorisLocale
|
||||
import dev.patrickgold.florisboard.lib.devtools.flogError
|
||||
import dev.patrickgold.florisboard.lib.io.FlorisRef
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
* TODO: document
|
||||
*/
|
||||
class DictionaryManager private constructor(
|
||||
context: Context,
|
||||
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
|
||||
) {
|
||||
class DictionaryManager private constructor(context: Context) {
|
||||
private val applicationContext: WeakReference<Context> = WeakReference(context.applicationContext ?: context)
|
||||
private val prefs by florisPreferenceModel()
|
||||
|
||||
private val dictionaryCache: MutableMap<String, Dictionary> = mutableMapOf()
|
||||
|
||||
private var florisUserDictionaryDatabase: FlorisUserDictionaryDatabase? = null
|
||||
private var systemUserDictionaryDatabase: SystemUserDictionaryDatabase? = null
|
||||
|
||||
companion object {
|
||||
val FLORIS_EN_REF = FlorisRef.assets("ime/dict/en.flict")
|
||||
|
||||
private var defaultInstance: DictionaryManager? = null
|
||||
|
||||
fun init(applicationContext: Context): DictionaryManager {
|
||||
@@ -67,81 +55,42 @@ class DictionaryManager private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
inline fun suggest(
|
||||
currentWord: Word,
|
||||
preceidingWords: List<Word>,
|
||||
subtype: Subtype,
|
||||
allowPossiblyOffensive: Boolean,
|
||||
maxSuggestionCount: Int,
|
||||
block: (suggestions: SuggestionList) -> Unit
|
||||
) {
|
||||
val suggestions = SuggestionList.new(maxSuggestionCount)
|
||||
queryUserDictionary(currentWord, subtype.primaryLocale, suggestions)
|
||||
loadDictionary(FLORIS_EN_REF).onSuccess {
|
||||
it.getTokenPredictions(preceidingWords, currentWord, maxSuggestionCount, allowPossiblyOffensive, suggestions)
|
||||
}
|
||||
block(suggestions)
|
||||
suggestions.dispose()
|
||||
}
|
||||
|
||||
fun loadDictionary(ref: FlorisRef): Result<Dictionary> {
|
||||
dictionaryCache[ref.toString()]?.let {
|
||||
return Result.success(it)
|
||||
}
|
||||
if (ref.relativePath.endsWith(".flict")) {
|
||||
// Assume this is a Flictionary
|
||||
applicationContext.get()?.let {
|
||||
Flictionary.load(it, ref).onSuccess { flict ->
|
||||
dictionaryCache[ref.toString()] = flict
|
||||
return Result.success(flict)
|
||||
}.onFailure { err ->
|
||||
flogError { err.toString() }
|
||||
return Result.failure(err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Result.failure(Exception("Unable to determine supported type for given AssetRef!"))
|
||||
}
|
||||
return Result.failure(Exception("If this message is ever thrown, something is completely broken..."))
|
||||
}
|
||||
|
||||
fun prepareDictionaries(subtype: Subtype) {
|
||||
loadDictionary(FLORIS_EN_REF)
|
||||
}
|
||||
|
||||
fun queryUserDictionary(word: Word, locale: FlorisLocale, destSuggestionList: SuggestionList) {
|
||||
fun queryUserDictionary(word: String, locale: FlorisLocale): List<SuggestionCandidate> {
|
||||
val florisDao = florisUserDictionaryDao()
|
||||
val systemDao = systemUserDictionaryDao()
|
||||
if (florisDao == null && systemDao == null) {
|
||||
return
|
||||
return emptyList()
|
||||
}
|
||||
if (prefs.dictionary.enableFlorisUserDictionary.get()) {
|
||||
florisDao?.query(word, locale)?.let {
|
||||
for (entry in it) {
|
||||
destSuggestionList.add(entry.word, entry.freq)
|
||||
return buildList {
|
||||
if (prefs.dictionary.enableFlorisUserDictionary.get()) {
|
||||
florisDao?.query(word, locale)?.let {
|
||||
for (entry in it) {
|
||||
add(WordSuggestionCandidate(entry.word, confidence = entry.freq / 255.0))
|
||||
}
|
||||
}
|
||||
florisDao?.queryShortcut(word, locale)?.let {
|
||||
for (entry in it) {
|
||||
add(WordSuggestionCandidate(entry.word, confidence = entry.freq / 255.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
florisDao?.queryShortcut(word, locale)?.let {
|
||||
for (entry in it) {
|
||||
destSuggestionList.add(entry.word, entry.freq)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (prefs.dictionary.enableSystemUserDictionary.get()) {
|
||||
systemDao?.query(word, locale)?.let {
|
||||
for (entry in it) {
|
||||
destSuggestionList.add(entry.word, entry.freq)
|
||||
}
|
||||
}
|
||||
systemDao?.queryShortcut(word, locale)?.let {
|
||||
for (entry in it) {
|
||||
destSuggestionList.add(entry.word, entry.freq)
|
||||
if (prefs.dictionary.enableSystemUserDictionary.get()) {
|
||||
systemDao?.query(word, locale)?.let {
|
||||
for (entry in it) {
|
||||
add(WordSuggestionCandidate(entry.word, confidence = entry.freq / 255.0))
|
||||
}
|
||||
}
|
||||
systemDao?.queryShortcut(word, locale)?.let {
|
||||
for (entry in it) {
|
||||
add(WordSuggestionCandidate(entry.word, confidence = entry.freq / 255.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun spell(word: Word, locale: FlorisLocale): Boolean {
|
||||
fun spell(word: String, locale: FlorisLocale): Boolean {
|
||||
val florisDao = florisUserDictionaryDao()
|
||||
val systemDao = systemUserDictionaryDao()
|
||||
if (florisDao == null && systemDao == null) {
|
||||
|
||||
@@ -1,427 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.dictionary
|
||||
|
||||
import android.content.Context
|
||||
import dev.patrickgold.florisboard.ime.nlp.SuggestionList
|
||||
import dev.patrickgold.florisboard.ime.nlp.Word
|
||||
import dev.patrickgold.florisboard.lib.io.FlorisRef
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* Class Flictionary which takes care of loading the binary asset as well as providing words for
|
||||
* queries.
|
||||
*
|
||||
* This class accepts binary dictionary files of the type "flict" as defined in here:
|
||||
* https://github.com/florisboard/dictionary-tools/blob/main/flictionary.md
|
||||
*/
|
||||
class Flictionary private constructor(
|
||||
override val name: String,
|
||||
override val label: String,
|
||||
override val authors: List<String>,
|
||||
private val date: Long,
|
||||
private val version: Int,
|
||||
private val headerStr: String,
|
||||
private val languageModel: FlorisLanguageModel
|
||||
) : Dictionary {
|
||||
companion object {
|
||||
private const val VERSION_0 = 0x0
|
||||
|
||||
private const val MASK_BEGIN_PTREE_NODE = 0x80
|
||||
private const val CMDB_BEGIN_PTREE_NODE = 0x00
|
||||
private const val ATTR_PTREE_NODE_ORDER = 0x70
|
||||
private const val ATTR_PTREE_NODE_TYPE = 0x0C
|
||||
private const val ATTR_PTREE_NODE_TYPE_CHAR = 0
|
||||
private const val ATTR_PTREE_NODE_TYPE_WORD_FILLER = 1
|
||||
private const val ATTR_PTREE_NODE_TYPE_WORD = 2
|
||||
private const val ATTR_PTREE_NODE_TYPE_SHORTCUT = 3
|
||||
private const val ATTR_PTREE_NODE_SIZE = 0x03
|
||||
|
||||
private const val MASK_END = 0xC0
|
||||
private const val CMDB_END = 0x80
|
||||
private const val ATTR_END_COUNT = 0x3F
|
||||
|
||||
private const val MASK_BEGIN_HEADER = 0xE0
|
||||
private const val CMDB_BEGIN_HEADER = 0xC0
|
||||
private const val ATTR_HEADER_VERSION = 0x1F
|
||||
|
||||
private const val MASK_DEFINE_SHORTCUT = 0xF0
|
||||
private const val CMDB_DEFINE_SHORTCUT = 0xE0
|
||||
|
||||
/**
|
||||
* Loads a Flictionary binary asset from given [assetRef] and returns a result containing
|
||||
* either the parsed dictionary or an exception giving information about the error which
|
||||
* occurred.
|
||||
*/
|
||||
fun load(context: Context, assetRef: FlorisRef): Result<Flictionary> {
|
||||
val buffer = ByteArray(5000) { 0 }
|
||||
val inputStream: InputStream
|
||||
if (assetRef.isAssets) {
|
||||
inputStream = context.assets.open(assetRef.relativePath)
|
||||
} else {
|
||||
return Result.failure(Exception("Only AssetSource.Assets is currently supported!"))
|
||||
}
|
||||
|
||||
var headerStr: String? = null
|
||||
var date: Long = 0
|
||||
var version = 0
|
||||
val ngramTree = NgramTree()
|
||||
|
||||
var pos = 0
|
||||
val ngramOrderStack = mutableListOf<Int>()
|
||||
val ngramTreeStack = mutableListOf<NgramNode>()
|
||||
|
||||
while (true) {
|
||||
if (inputStream.readNext(buffer, 0, 1) <= 0) {
|
||||
break
|
||||
}
|
||||
val cmd = buffer[0].toInt() and 0xFF
|
||||
when {
|
||||
(cmd and MASK_BEGIN_PTREE_NODE) == CMDB_BEGIN_PTREE_NODE -> {
|
||||
if (pos == 0) {
|
||||
inputStream.close()
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_CMD_BEGIN_PTREE_NODE,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
val order = ((cmd and ATTR_PTREE_NODE_ORDER) shr 4) + 1
|
||||
val type = ((cmd and ATTR_PTREE_NODE_TYPE) shr 2)
|
||||
val size = (cmd and ATTR_PTREE_NODE_SIZE) + 1
|
||||
val freq: Int
|
||||
val freqSize: Int
|
||||
when (type) {
|
||||
ATTR_PTREE_NODE_TYPE_CHAR -> {
|
||||
freq = NgramNode.FREQ_CHARACTER
|
||||
freqSize = 0
|
||||
}
|
||||
ATTR_PTREE_NODE_TYPE_WORD_FILLER -> {
|
||||
freq = NgramNode.FREQ_WORD_FILLER
|
||||
freqSize = 0
|
||||
}
|
||||
ATTR_PTREE_NODE_TYPE_WORD -> {
|
||||
if (inputStream.readNext(buffer, 1, 1) > 0) {
|
||||
freq = buffer[1].toInt() and 0xFF
|
||||
} else {
|
||||
inputStream.close()
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_EOF,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
freqSize = 1
|
||||
}
|
||||
else -> return Result.failure(Exception("TODO: shortcut not supported"))
|
||||
}
|
||||
if (inputStream.readNext(buffer, freqSize + 1, size) > 0) {
|
||||
val char = String(buffer, freqSize + 1, size, Charsets.UTF_8)[0]
|
||||
val node = NgramNode(order, char, freq)
|
||||
val lastOrder = ngramOrderStack.lastOrNull()
|
||||
if (lastOrder == null) {
|
||||
ngramTree.higherOrderChildren.add(node)
|
||||
} else {
|
||||
if (lastOrder == order) {
|
||||
ngramTreeStack.last().sameOrderChildren.add(node)
|
||||
} else {
|
||||
ngramTreeStack.last().higherOrderChildren.add(node)
|
||||
}
|
||||
}
|
||||
ngramOrderStack.add(order)
|
||||
ngramTreeStack.add(node)
|
||||
pos += (freqSize + 1 + size)
|
||||
} else {
|
||||
inputStream.close()
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_EOF,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
(cmd and MASK_BEGIN_HEADER) == CMDB_BEGIN_HEADER -> {
|
||||
if (pos != 0) {
|
||||
inputStream.close()
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_CMD_BEGIN_HEADER,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
version = cmd and ATTR_HEADER_VERSION
|
||||
if (version != VERSION_0) {
|
||||
inputStream.close()
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNSUPPORTED_FLICTIONARY_VERSION,
|
||||
address = pos,
|
||||
cmdByte = cmd.toByte(),
|
||||
absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
if (inputStream.readNext(buffer, 1, 9) > 0) {
|
||||
val size = (buffer[1].toInt() and 0xFF)
|
||||
date =
|
||||
((buffer[2].toInt() and 0xFF).toLong() shl 56) +
|
||||
((buffer[3].toInt() and 0xFF).toLong() shl 48) +
|
||||
((buffer[4].toInt() and 0xFF).toLong() shl 40) +
|
||||
((buffer[5].toInt() and 0xFF).toLong() shl 32) +
|
||||
((buffer[6].toInt() and 0xFF).toLong() shl 24) +
|
||||
((buffer[7].toInt() and 0xFF).toLong() shl 16) +
|
||||
((buffer[8].toInt() and 0xFF).toLong() shl 8) +
|
||||
((buffer[9].toInt() and 0xFF).toLong() shl 0)
|
||||
if (inputStream.readNext(buffer, 10, size) > 0) {
|
||||
headerStr = String(buffer, 10, size, Charsets.UTF_8)
|
||||
ngramOrderStack.add(-1)
|
||||
ngramTreeStack.add(NgramTree())
|
||||
pos += (10 + size)
|
||||
} else {
|
||||
inputStream.close()
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_EOF,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
inputStream.close()
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_EOF,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
(cmd and MASK_END) == CMDB_END -> {
|
||||
if (pos == 0) {
|
||||
inputStream.close()
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_CMD_END,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
val n = (cmd and ATTR_END_COUNT)
|
||||
if (n > 0) {
|
||||
if (n <= ngramTreeStack.size) {
|
||||
for (c in 0 until n) {
|
||||
ngramOrderStack.removeLast()
|
||||
ngramTreeStack.removeLast()
|
||||
}
|
||||
} else {
|
||||
inputStream.close()
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_ABSOLUTE_DEPTH_DECREASE_BELOW_ZERO,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size - n
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
inputStream.close()
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_CMD_END_ZERO_VALUE,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
pos += 1
|
||||
}
|
||||
else -> {
|
||||
inputStream.close()
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.INVALID_CMD_BYTE_PROVIDED,
|
||||
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
inputStream.close()
|
||||
|
||||
if (ngramTreeStack.size != 0) {
|
||||
return Result.failure(
|
||||
ParseException(
|
||||
errorType = ParseException.ErrorType.UNEXPECTED_ABSOLUTE_DEPTH_NOT_ZERO_AT_EOF,
|
||||
address = pos, cmdByte = 0x00.toByte(), absoluteDepth = ngramTreeStack.size
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return Result.success(
|
||||
Flictionary(
|
||||
name = "flict",
|
||||
label = "flict",
|
||||
authors = listOf(),
|
||||
headerStr = headerStr ?: "",
|
||||
date = date,
|
||||
version = version,
|
||||
languageModel = FlorisLanguageModel(ngramTree)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDate(): Long = date
|
||||
|
||||
override fun getVersion(): Int = version
|
||||
|
||||
// TODO: preceding tokens are currently ignored
|
||||
override fun getTokenPredictions(
|
||||
precedingTokens: List<Word>,
|
||||
currentToken: Word?,
|
||||
maxSuggestionCount: Int,
|
||||
allowPossiblyOffensive: Boolean,
|
||||
destSuggestionList: SuggestionList
|
||||
) {
|
||||
currentToken ?: return
|
||||
|
||||
if (currentToken.isNotBlank()) {
|
||||
val retList = languageModel.matchAllNgrams(
|
||||
ngram = Ngram(
|
||||
_tokens = listOf(Token(currentToken.lowercase())),
|
||||
_freq = -1
|
||||
),
|
||||
maxEditDistance = 2,
|
||||
maxTokenCount = maxSuggestionCount,
|
||||
allowPossiblyOffensive = allowPossiblyOffensive
|
||||
)
|
||||
retList.forEach { destSuggestionList.add(it.data, 128) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A parse exception to be used by [Flictionary] to indicate where the parsing of a binary file
|
||||
* failed, while also providing some additional information.
|
||||
*/
|
||||
class ParseException(
|
||||
private val errorType: ErrorType,
|
||||
private val address: Int,
|
||||
private val cmdByte: Byte,
|
||||
private val absoluteDepth: Int
|
||||
) : Exception() {
|
||||
enum class ErrorType {
|
||||
UNSUPPORTED_FLICTIONARY_VERSION,
|
||||
UNEXPECTED_CMD_BEGIN_HEADER,
|
||||
UNEXPECTED_CMD_BEGIN_PTREE_NODE,
|
||||
UNEXPECTED_CMD_DEFINE_SHORTCUT,
|
||||
UNEXPECTED_CMD_END,
|
||||
UNEXPECTED_CMD_END_ZERO_VALUE,
|
||||
UNEXPECTED_ABSOLUTE_DEPTH_DECREASE_BELOW_ZERO,
|
||||
UNEXPECTED_ABSOLUTE_DEPTH_NOT_ZERO_AT_EOF,
|
||||
UNEXPECTED_EOF,
|
||||
INVALID_CMD_BYTE_PROVIDED,
|
||||
}
|
||||
|
||||
override val message: String
|
||||
get() = toString()
|
||||
override fun toString(): String {
|
||||
return StringBuilder().run {
|
||||
append(
|
||||
when (errorType) {
|
||||
ErrorType.UNSUPPORTED_FLICTIONARY_VERSION -> {
|
||||
"Unexpected Flictionary version: ${(cmdByte.toInt() and 0xFF) and ATTR_HEADER_VERSION}"
|
||||
}
|
||||
ErrorType.UNEXPECTED_CMD_BEGIN_HEADER -> {
|
||||
"Unexpected command: BEGIN_HEADER"
|
||||
}
|
||||
ErrorType.UNEXPECTED_CMD_BEGIN_PTREE_NODE -> {
|
||||
"Unexpected command: BEGIN_PTREE_NODE"
|
||||
}
|
||||
ErrorType.UNEXPECTED_CMD_DEFINE_SHORTCUT -> {
|
||||
"Unexpected command: DEFINE_SHORTCUT"
|
||||
}
|
||||
ErrorType.UNEXPECTED_CMD_END -> {
|
||||
"Unexpected command: END"
|
||||
}
|
||||
ErrorType.UNEXPECTED_CMD_END_ZERO_VALUE -> {
|
||||
"Unexpected zero value provided for command END"
|
||||
}
|
||||
ErrorType.UNEXPECTED_ABSOLUTE_DEPTH_DECREASE_BELOW_ZERO -> {
|
||||
"Unexpected decrease in absolute depth: cannot go below zero"
|
||||
}
|
||||
ErrorType.UNEXPECTED_ABSOLUTE_DEPTH_NOT_ZERO_AT_EOF -> {
|
||||
"Unexpected non-zero value in absolute depth at end of file"
|
||||
}
|
||||
ErrorType.UNEXPECTED_EOF -> {
|
||||
"Unexpected end of file while try to do look-ahead"
|
||||
}
|
||||
ErrorType.INVALID_CMD_BYTE_PROVIDED -> {
|
||||
"Invalid command byte provided"
|
||||
}
|
||||
}
|
||||
)
|
||||
append(
|
||||
String.format(
|
||||
"\n at address 0x%08X where cmd_byte=0x%02X and section_depth=%d",
|
||||
address,
|
||||
cmdByte,
|
||||
absoluteDepth
|
||||
)
|
||||
)
|
||||
toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the next [len] bytes from the input stream into the given byte array [b]. This method guarantees to either
|
||||
* read the full length requested or if an EOF file is encountered, -1 is returned. The first byte written is at
|
||||
* `b[off]`, the second byte at `b[off+1]` and so on.
|
||||
*
|
||||
* @param b The byte array to read the next [len] bytes into.
|
||||
* @param off The offset of the first byte written in the byte array [b]. Must be non-negative.
|
||||
* @param len The number of bytes to read. Must be non-negative.
|
||||
*
|
||||
* @return The number of bytes read, always matching [len] or -1 if EOF was encountered.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if either [off] or [len] is negative or the byte array has insufficient space to
|
||||
* write the request [len] bytes into it.
|
||||
*/
|
||||
@Throws(IndexOutOfBoundsException::class)
|
||||
fun InputStream.readNext(b: ByteArray, off: Int, len: Int): Int {
|
||||
if (off < 0 || len < 0 || len > b.size - off) {
|
||||
throw IndexOutOfBoundsException()
|
||||
} else if (len == 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
var lenRead = 0
|
||||
while (lenRead < len) {
|
||||
val c = read()
|
||||
if (c == -1) {
|
||||
return -1
|
||||
} else {
|
||||
b[off + lenRead++] = c.toByte()
|
||||
}
|
||||
}
|
||||
return lenRead
|
||||
}
|
||||
@@ -1,441 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.dictionary
|
||||
|
||||
import dev.patrickgold.florisboard.ime.nlp.SuggestionList
|
||||
|
||||
/*
|
||||
* ====================== IMPORTANT ========================
|
||||
*
|
||||
* All code in this file is only temporary added back in so the stable track has suggestions again.
|
||||
* In 0.3.15 a renewed suggestion algorithm will be built and this mess will be removed!
|
||||
*
|
||||
* ==========================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* Abstract interface representing a n-gram of tokens. Each n-gram instance can be assigned a
|
||||
* unique frequency [freq].
|
||||
*/
|
||||
open class Ngram<T : Any, F : Comparable<F>>(_tokens: List<Token<T>>, _freq: F) {
|
||||
companion object {
|
||||
/** Constant order value for unigrams. */
|
||||
const val ORDER_UNIGRAM: Int = 1
|
||||
|
||||
/** Constant order value for bigrams. */
|
||||
const val ORDER_BIGRAM: Int = 2
|
||||
|
||||
/** Constant order value for trigrams. */
|
||||
const val ORDER_TRIGRAM: Int = 3
|
||||
}
|
||||
|
||||
init {
|
||||
if (_tokens.size < ORDER_UNIGRAM) {
|
||||
throw Exception("A n-gram must contain at least 1 token!")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of tokens for this n-gram. The length of this list is guaranteed to be matching
|
||||
* [order].
|
||||
*/
|
||||
val tokens: List<Token<T>> = _tokens
|
||||
|
||||
/**
|
||||
* The frequency value of this n-gram.
|
||||
*/
|
||||
val freq: F = _freq
|
||||
|
||||
/**
|
||||
* The order of this n-gram (1, 2, 3, ...).
|
||||
*/
|
||||
val order: Int
|
||||
get() = tokens.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract interface representing a token used in [Ngram].
|
||||
*/
|
||||
open class Token<T : Any>(_data: T) {
|
||||
/**
|
||||
* The data of this token.
|
||||
*/
|
||||
val data: T = _data
|
||||
|
||||
override fun toString(): String {
|
||||
return "Token(\"$data\")"
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return data.hashCode()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Token<*>
|
||||
|
||||
if (data != other.data) return false
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a list of tokens carrying [CharSequence] data to a list of [CharSequence].
|
||||
*/
|
||||
fun List<Token<CharSequence>>.toCharSequenceList(): List<CharSequence> {
|
||||
return this.map { it.data }
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a list of tokens carrying [String] data to a list of [String].
|
||||
*/
|
||||
fun List<Token<String>>.toStringList(): List<String> {
|
||||
return this.map { it.data }
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract interface for a language model. Can house any n-grams with a minimum order of one.
|
||||
*/
|
||||
interface LanguageModel<T : Any, F : Comparable<F>> {
|
||||
/**
|
||||
* Tries to get the n-gram for the passed [tokens]. Throws a NPE if no match could be found.
|
||||
*/
|
||||
@Throws(NullPointerException::class)
|
||||
fun getNgram(vararg tokens: T): Ngram<T, F>
|
||||
|
||||
/**
|
||||
* Tries to get the n-gram for the passed [ngram], whereas the frequency is ignored while
|
||||
* searching. Throws a NPE if no match could be found.
|
||||
*/
|
||||
@Throws(NullPointerException::class)
|
||||
fun getNgram(ngram: Ngram<T, F>): Ngram<T, F>
|
||||
|
||||
/**
|
||||
* Tries to get the n-gram for the passed [tokens]. Returns null if no match could be found.
|
||||
*/
|
||||
fun getNgramOrNull(vararg tokens: T): Ngram<T, F>?
|
||||
|
||||
/**
|
||||
* Tries to get the n-gram for the passed [ngram], whereas the frequency is ignored while
|
||||
* searching. Returns null if no match could be found.
|
||||
*/
|
||||
fun getNgramOrNull(ngram: Ngram<T, F>): Ngram<T, F>?
|
||||
|
||||
/**
|
||||
* Checks if a given [ngram] exists within this model. If [doMatchFreq] is set to true, the
|
||||
* frequency is also matched.
|
||||
*/
|
||||
fun hasNgram(ngram: Ngram<T, F>, doMatchFreq: Boolean = false): Boolean
|
||||
|
||||
/**
|
||||
* Matches all n-grams which match the given [ngram], whereas the last item in the n-gram is
|
||||
* is used to search for predictions.
|
||||
*/
|
||||
fun matchAllNgrams(
|
||||
ngram: Ngram<T, F>,
|
||||
maxEditDistance: Int,
|
||||
maxTokenCount: Int,
|
||||
allowPossiblyOffensive: Boolean
|
||||
): List<Token<T>>
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutable version of [LanguageModel].
|
||||
*/
|
||||
interface MutableLanguageModel<T : Any, F : Comparable<F>> : LanguageModel<T, F> {
|
||||
fun deleteNgram(ngram: Ngram<T, F>)
|
||||
|
||||
fun insertNgram(ngram: Ngram<T, F>)
|
||||
|
||||
fun updateNgram(ngram: Ngram<T, F>)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the root node to a n-gram tree.
|
||||
*/
|
||||
open class NgramTree(
|
||||
sameOrderChildren: MutableList<NgramNode> = mutableListOf(),
|
||||
higherOrderChildren: MutableList<NgramNode> = mutableListOf()
|
||||
) : NgramNode(0, '?', -1, sameOrderChildren, higherOrderChildren)
|
||||
|
||||
/**
|
||||
* A node of a n-gram tree, which holds the character it represents, the corresponding frequency,
|
||||
* a pre-computed string representing all parent characters and the current one as well as child
|
||||
* nodes, one for the same order n-gram nodes and one for the higher order n-gram nodes.
|
||||
*/
|
||||
open class NgramNode(
|
||||
val order: Int,
|
||||
val char: Char,
|
||||
val freq: Int,
|
||||
val sameOrderChildren: MutableList<NgramNode> = mutableListOf(),
|
||||
val higherOrderChildren: MutableList<NgramNode> = mutableListOf()
|
||||
) {
|
||||
companion object {
|
||||
const val FREQ_CHARACTER = -1
|
||||
const val FREQ_WORD_MIN = 0
|
||||
const val FREQ_WORD_MAX = 255
|
||||
const val FREQ_WORD_FILLER = -2
|
||||
const val FREQ_IS_POSSIBLY_OFFENSIVE = 0
|
||||
}
|
||||
|
||||
val isCharacter: Boolean
|
||||
get() = freq == FREQ_CHARACTER
|
||||
|
||||
val isWord: Boolean
|
||||
get() = freq in FREQ_WORD_MIN..FREQ_WORD_MAX
|
||||
|
||||
val isWordFiller: Boolean
|
||||
get() = freq == FREQ_WORD_FILLER
|
||||
|
||||
val isPossiblyOffensive: Boolean
|
||||
get() = freq == FREQ_IS_POSSIBLY_OFFENSIVE
|
||||
|
||||
fun findWord(word: String): NgramNode? {
|
||||
var currentNode = this
|
||||
for ((pos, char) in word.withIndex()) {
|
||||
val childNode = if (pos == 0) {
|
||||
currentNode.higherOrderChildren.find { it.char == char }
|
||||
} else {
|
||||
currentNode.sameOrderChildren.find { it.char == char }
|
||||
}
|
||||
if (childNode != null) {
|
||||
currentNode = childNode
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return if (currentNode.isWord || currentNode.isWordFiller) {
|
||||
currentNode
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function allows to search for a given [input] word with a given [maxEditDistance] and
|
||||
* adds all matches in the trie to the [list].
|
||||
*/
|
||||
fun listSimilarWords(
|
||||
input: String,
|
||||
list: SuggestionList,
|
||||
word: StringBuilder,
|
||||
allowPossiblyOffensive: Boolean,
|
||||
maxEditDistance: Int,
|
||||
deletionCost: Int = 0,
|
||||
insertionCost: Int = 0,
|
||||
substitutionCost: Int = 0,
|
||||
pos: Int = -1
|
||||
) {
|
||||
if (pos > -1) {
|
||||
word.append(char)
|
||||
}
|
||||
val costSum = deletionCost + insertionCost + substitutionCost
|
||||
if (pos > -1 && (pos + 1 == input.length) && isWord && ((isPossiblyOffensive && allowPossiblyOffensive)
|
||||
|| !isPossiblyOffensive)) {
|
||||
// Using shift right instead of divide by 2^(costSum) as it is mathematically the
|
||||
// same but faster.
|
||||
list.add(word.toString(), freq shr costSum)
|
||||
}
|
||||
if (pos <= -1) {
|
||||
for (childNode in higherOrderChildren) {
|
||||
childNode.listSimilarWords(
|
||||
input, list, word, allowPossiblyOffensive, maxEditDistance, 0, 0, 0, 0
|
||||
)
|
||||
}
|
||||
} else if (maxEditDistance == costSum) {
|
||||
if (pos + 1 < input.length) {
|
||||
sameOrderChildren.find { it.char == input[pos + 1] }?.listSimilarWords(
|
||||
input, list, word, allowPossiblyOffensive, maxEditDistance,
|
||||
deletionCost, insertionCost, substitutionCost, pos + 1
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Delete
|
||||
if (pos + 2 < input.length) {
|
||||
sameOrderChildren.find { it.char == input[pos + 2] }?.listSimilarWords(
|
||||
input, list, word, allowPossiblyOffensive, maxEditDistance,
|
||||
deletionCost + 1, insertionCost, substitutionCost, pos + 2
|
||||
)
|
||||
}
|
||||
for (childNode in sameOrderChildren) {
|
||||
if (pos + 1 < input.length && childNode.char == input[pos + 1]) {
|
||||
childNode.listSimilarWords(
|
||||
input, list, word, allowPossiblyOffensive, maxEditDistance,
|
||||
deletionCost, insertionCost, substitutionCost, pos + 1
|
||||
)
|
||||
} else {
|
||||
// Insert
|
||||
childNode.listSimilarWords(
|
||||
input, list, word, allowPossiblyOffensive, maxEditDistance,
|
||||
deletionCost, insertionCost + 1, substitutionCost, pos
|
||||
)
|
||||
if (pos + 1 < input.length) {
|
||||
// Substitute
|
||||
childNode.listSimilarWords(
|
||||
input, list, word, allowPossiblyOffensive, maxEditDistance,
|
||||
deletionCost, insertionCost, substitutionCost + 1, pos + 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pos > -1) {
|
||||
word.deleteAt(word.lastIndex)
|
||||
}
|
||||
}
|
||||
|
||||
fun listAllSameOrderWords(list: SuggestionList, word: StringBuilder, allowPossiblyOffensive: Boolean) {
|
||||
word.append(char)
|
||||
if (isWord && ((isPossiblyOffensive && allowPossiblyOffensive) || !isPossiblyOffensive)) {
|
||||
list.add(word.toString(), freq)
|
||||
}
|
||||
for (childNode in sameOrderChildren) {
|
||||
childNode.listAllSameOrderWords(list, word, allowPossiblyOffensive)
|
||||
}
|
||||
word.deleteAt(word.lastIndex)
|
||||
}
|
||||
}
|
||||
|
||||
open class FlorisLanguageModel(
|
||||
initTreeObj: NgramTree? = null
|
||||
) : LanguageModel<String, Int> {
|
||||
protected val ngramTree: NgramTree = initTreeObj ?: NgramTree()
|
||||
|
||||
override fun getNgram(vararg tokens: String): Ngram<String, Int> {
|
||||
val ngramOut = getNgramOrNull(*tokens)
|
||||
if (ngramOut != null) {
|
||||
return ngramOut
|
||||
} else {
|
||||
throw NullPointerException("No n-gram found matching the given tokens: $tokens")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNgram(ngram: Ngram<String, Int>): Ngram<String, Int> {
|
||||
val ngramOut = getNgramOrNull(ngram)
|
||||
if (ngramOut != null) {
|
||||
return ngramOut
|
||||
} else {
|
||||
throw NullPointerException("No n-gram found matching the given ngram: $ngram")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNgramOrNull(vararg tokens: String): Ngram<String, Int>? {
|
||||
var currentNode: NgramNode = ngramTree
|
||||
for (token in tokens) {
|
||||
val childNode = currentNode.findWord(token)
|
||||
if (childNode != null) {
|
||||
currentNode = childNode
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return Ngram(tokens.toList().map { Token(it) }, currentNode.freq)
|
||||
}
|
||||
|
||||
override fun getNgramOrNull(ngram: Ngram<String, Int>): Ngram<String, Int>? {
|
||||
return getNgramOrNull(*ngram.tokens.toStringList().toTypedArray())
|
||||
}
|
||||
|
||||
override fun hasNgram(ngram: Ngram<String, Int>, doMatchFreq: Boolean): Boolean {
|
||||
val result = getNgramOrNull(ngram)
|
||||
return if (result != null) {
|
||||
if (doMatchFreq) {
|
||||
ngram.freq == result.freq
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun matchAllNgrams(
|
||||
ngram: Ngram<String, Int>,
|
||||
maxEditDistance: Int,
|
||||
maxTokenCount: Int,
|
||||
allowPossiblyOffensive: Boolean
|
||||
): List<Token<String>> {
|
||||
val ngramList = mutableListOf<Token<String>>()
|
||||
var currentNode: NgramNode = ngramTree
|
||||
for ((t, token) in ngram.tokens.withIndex()) {
|
||||
val word = token.data
|
||||
if (t + 1 >= ngram.tokens.size) {
|
||||
if (word.isNotEmpty()) {
|
||||
// The last word is not complete, so find all possible words and sort
|
||||
val splitWord = mutableListOf<Char>()
|
||||
var splitNode: NgramNode? = currentNode
|
||||
for ((pos, char) in word.withIndex()) {
|
||||
val node = if (pos == 0) {
|
||||
splitNode?.higherOrderChildren?.find { it.char == char }
|
||||
} else {
|
||||
splitNode?.sameOrderChildren?.find { it.char == char }
|
||||
}
|
||||
splitWord.add(char)
|
||||
splitNode = node
|
||||
if (node == null) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (splitNode != null) {
|
||||
// Input thus far is valid
|
||||
val wordNodes = SuggestionList.new(maxTokenCount)
|
||||
val strBuilder = StringBuilder().append(word.substring(0, word.length - 1))
|
||||
splitNode.listAllSameOrderWords(wordNodes, strBuilder, allowPossiblyOffensive)
|
||||
ngramList.addAll(wordNodes.map { Token(it) })
|
||||
}
|
||||
if (ngramList.size < maxTokenCount) {
|
||||
val wordNodes = SuggestionList.new(maxTokenCount)
|
||||
val strBuilder = StringBuilder()
|
||||
currentNode.listSimilarWords(word, wordNodes, strBuilder, allowPossiblyOffensive, maxEditDistance)
|
||||
ngramList.addAll(wordNodes.map { Token(it) })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val node = currentNode.findWord(word)
|
||||
if (node == null) {
|
||||
return ngramList
|
||||
} else {
|
||||
currentNode = node
|
||||
}
|
||||
}
|
||||
}
|
||||
return ngramList
|
||||
}
|
||||
|
||||
fun toFlorisMutableLanguageModel(): FlorisMutableLanguageModel = FlorisMutableLanguageModel(ngramTree)
|
||||
}
|
||||
|
||||
open class FlorisMutableLanguageModel(
|
||||
initTreeObj: NgramTree? = null
|
||||
) : MutableLanguageModel<String, Int>, FlorisLanguageModel(initTreeObj) {
|
||||
override fun deleteNgram(ngram: Ngram<String, Int>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun insertNgram(ngram: Ngram<String, Int>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun updateNgram(ngram: Ngram<String, Int>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
fun toFlorisLanguageModel(): FlorisLanguageModel = FlorisLanguageModel(ngramTree)
|
||||
}
|
||||
@@ -94,10 +94,13 @@ abstract class AbstractEditorInstance(context: Context) {
|
||||
currentInputConnection()?.requestCursorUpdates(CursorUpdateAll)
|
||||
}
|
||||
|
||||
open fun handleStartInputView(editorInfo: FlorisEditorInfo) {
|
||||
open fun handleStartInputView(editorInfo: FlorisEditorInfo, isRestart: Boolean) {
|
||||
if (isRestart) {
|
||||
reset() // Just to make sure our state is correct after a restart
|
||||
}
|
||||
val ic = currentInputConnection()
|
||||
activeInfo = editorInfo
|
||||
val selection = editorInfo.initialSelection
|
||||
var selection = editorInfo.initialSelection
|
||||
if (ic == null || selection.isNotValid || editorInfo.isRawInputEditor) {
|
||||
activeCursorCapsMode = InputAttributes.CapsMode.NONE
|
||||
activeContent = EditorContent.Unspecified
|
||||
@@ -105,13 +108,19 @@ abstract class AbstractEditorInstance(context: Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get Text
|
||||
val textBeforeSelection = editorInfo.getInitialTextBeforeCursor(NumCharsBeforeCursor)
|
||||
?: ic.getTextBeforeCursor(NumCharsBeforeCursor, 0) ?: ""
|
||||
val textAfterSelection = editorInfo.getInitialTextAfterCursor(NumCharsAfterCursor)
|
||||
?: ic.getTextAfterCursor(NumCharsAfterCursor, 0) ?: ""
|
||||
val selectedText = editorInfo.getInitialSelectedText()
|
||||
?: ic.getSelectedText(0) ?: ""
|
||||
// Get text (ignore initial text of EditorInfo because some apps like to provide an old or invalid state)
|
||||
val textBeforeSelection = ic.getTextBeforeCursor(NumCharsBeforeCursor, 0) ?: ""
|
||||
val textAfterSelection = ic.getTextAfterCursor(NumCharsAfterCursor, 0) ?: ""
|
||||
val selectedText = ic.getSelectedText(0) ?: ""
|
||||
|
||||
// Adjust initial selection issues as some apps like to do everything but provide the correct initial selection
|
||||
if (selection.length != selectedText.length) {
|
||||
selection = EditorRange(textBeforeSelection.length, textBeforeSelection.length + selectedText.length)
|
||||
} else if (selection.start > 0 || selection.end > 0) {
|
||||
if (textBeforeSelection.isEmpty() && textAfterSelection.isEmpty() && selectedText.isEmpty()) {
|
||||
selection = EditorRange(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
val content = generateContent(
|
||||
@@ -218,12 +227,13 @@ abstract class AbstractEditorInstance(context: Context) {
|
||||
}
|
||||
|
||||
// Determine local composing word range, if any
|
||||
val localComposing =
|
||||
val localCurrentWord =
|
||||
if (shouldDetermineComposingRegion(editorInfo) && localSelection.isCursorMode && textBeforeSelection.isNotEmpty()) {
|
||||
determineLocalComposing(textBeforeSelection)
|
||||
} else {
|
||||
EditorRange.Unspecified
|
||||
}
|
||||
val localComposing = if (determineComposingEnabled()) localCurrentWord else EditorRange.Unspecified
|
||||
|
||||
// Build and publish text and content
|
||||
val text = buildString {
|
||||
@@ -231,7 +241,7 @@ abstract class AbstractEditorInstance(context: Context) {
|
||||
append(selectedText)
|
||||
append(textAfterSelection)
|
||||
}
|
||||
return EditorContent(text, offset, localSelection, localComposing)
|
||||
return EditorContent(text, offset, localSelection, localComposing, localCurrentWord)
|
||||
}
|
||||
|
||||
private suspend fun EditorContent.generateCopy(
|
||||
@@ -255,6 +265,8 @@ abstract class AbstractEditorInstance(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun determineComposingEnabled(): Boolean
|
||||
|
||||
abstract fun determineComposer(composerName: ExtensionComponentName): Composer
|
||||
|
||||
protected open fun shouldDetermineComposingRegion(editorInfo: FlorisEditorInfo): Boolean {
|
||||
@@ -262,7 +274,7 @@ abstract class AbstractEditorInstance(context: Context) {
|
||||
}
|
||||
|
||||
private suspend fun determineLocalComposing(textBeforeSelection: CharSequence): EditorRange {
|
||||
return breakIterators.word(subtypeManager.activeSubtype().primaryLocale) {
|
||||
return breakIterators.word(subtypeManager.activeSubtype.primaryLocale) {
|
||||
it.setText(textBeforeSelection.toString())
|
||||
val end = it.last()
|
||||
val isWord = it.ruleStatus != BreakIterator.WORD_NONE
|
||||
@@ -302,18 +314,40 @@ abstract class AbstractEditorInstance(context: Context) {
|
||||
}
|
||||
|
||||
open fun commitChar(char: String): Boolean {
|
||||
return commitChar(
|
||||
char = char,
|
||||
deletePreviousSpace = false,
|
||||
insertSpaceBeforeChar = false,
|
||||
insertSpaceAfterChar = false,
|
||||
)
|
||||
}
|
||||
|
||||
protected fun commitChar(
|
||||
char: String,
|
||||
deletePreviousSpace: Boolean,
|
||||
insertSpaceBeforeChar: Boolean,
|
||||
insertSpaceAfterChar: Boolean,
|
||||
): Boolean {
|
||||
val content = activeContent
|
||||
val selection = content.selection
|
||||
// TODO: length enforcement to 1 may be an issue for some Unicode chars which are 2 Java chars
|
||||
if (char.length != 1 || selection.isNotValid || selection.isSelectionMode || activeInfo.isRawInputEditor) {
|
||||
return commitText(char)
|
||||
val isSingleChar = runBlocking {
|
||||
breakIterators.measureUChars(char, 1, subtypeManager.activeSubtype.primaryLocale)
|
||||
} == char.length
|
||||
if (!isSingleChar || selection.isNotValid || selection.isSelectionMode || activeInfo.isRawInputEditor) {
|
||||
return commitTextInternal(char)
|
||||
}
|
||||
val ic = currentInputConnection() ?: return false
|
||||
val composer = determineComposer(subtypeManager.activeSubtype().composer)
|
||||
val previous = content.textBeforeSelection.takeLast(composer.toRead)
|
||||
val (rm, finalText) = composer.getActions(previous, char[0])
|
||||
val composer = determineComposer(subtypeManager.activeSubtype.composer)
|
||||
val previous = content.textBeforeSelection.takeLast(composer.toRead.coerceAtLeast(if (deletePreviousSpace) 1 else 0))
|
||||
val (tempRm, tempText) = composer.getActions(previous, char)
|
||||
val rm = if (deletePreviousSpace && previous.isNotEmpty() && previous.last() == ' ') tempRm + 1 else tempRm
|
||||
val finalText = buildString(tempText.length + 2) {
|
||||
if (insertSpaceBeforeChar) append(' ')
|
||||
append(tempText)
|
||||
if (insertSpaceAfterChar) append(' ')
|
||||
}
|
||||
if (rm <= 0) {
|
||||
commitText(finalText)
|
||||
commitTextInternal(finalText)
|
||||
} else runBlocking {
|
||||
ic.beginBatchEdit()
|
||||
val newSelection = EditorRange.cursor(selection.start - rm + finalText.length)
|
||||
@@ -337,7 +371,9 @@ abstract class AbstractEditorInstance(context: Context) {
|
||||
return true
|
||||
}
|
||||
|
||||
open fun commitText(text: String): Boolean {
|
||||
open fun commitText(text: String): Boolean = commitTextInternal(text)
|
||||
|
||||
private fun commitTextInternal(text: String): Boolean {
|
||||
val ic = currentInputConnection() ?: return false
|
||||
val content = activeContent
|
||||
val selection = content.selection
|
||||
@@ -363,6 +399,32 @@ abstract class AbstractEditorInstance(context: Context) {
|
||||
return true
|
||||
}
|
||||
|
||||
open fun finalizeComposingText(text: String): Boolean {
|
||||
val ic = currentInputConnection() ?: return false
|
||||
val content = activeContent
|
||||
val composing = content.composing
|
||||
ic.beginBatchEdit()
|
||||
if (activeInfo.isRawInputEditor || composing.isNotValid) {
|
||||
return false
|
||||
} else runBlocking {
|
||||
val newSelection = EditorRange.cursor(composing.end + (text.length - content.composingText.length))
|
||||
val newContent = content.generateCopy(
|
||||
selection = newSelection,
|
||||
textBeforeSelection = buildString {
|
||||
append(content.textBeforeSelection)
|
||||
removeSuffix(content.composingText)
|
||||
append(text)
|
||||
},
|
||||
selectedText = "",
|
||||
)
|
||||
expectedContentQueue.push(newContent)
|
||||
ic.setComposingText(text, 1)
|
||||
ic.finishComposingText()
|
||||
}
|
||||
ic.endBatchEdit()
|
||||
return true
|
||||
}
|
||||
|
||||
protected fun deleteBeforeCursor(type: TextType, n: Int): Boolean {
|
||||
val ic = currentInputConnection()
|
||||
if (ic == null || n < 1) return false
|
||||
@@ -379,7 +441,7 @@ abstract class AbstractEditorInstance(context: Context) {
|
||||
}
|
||||
} else {
|
||||
runBlocking {
|
||||
val locale = subtypeManager.activeSubtype().primaryLocale
|
||||
val locale = subtypeManager.activeSubtype.primaryLocale
|
||||
val length = when (type) {
|
||||
TextType.CHARACTERS -> breakIterators.measureLastUChars(oldTextBeforeSelection, n, locale)
|
||||
TextType.WORDS -> breakIterators.measureLastUWords(oldTextBeforeSelection, n, locale)
|
||||
@@ -414,7 +476,7 @@ abstract class AbstractEditorInstance(context: Context) {
|
||||
if (n < 1 || text.isEmpty()) return ""
|
||||
return runBlocking {
|
||||
val text = textBeforeSelection
|
||||
val length = breakIterators.measureLastUChars(text, n, subtypeManager.activeSubtype().primaryLocale)
|
||||
val length = breakIterators.measureLastUChars(text, n, subtypeManager.activeSubtype.primaryLocale)
|
||||
text.takeLast(length)
|
||||
}
|
||||
}
|
||||
@@ -433,7 +495,7 @@ abstract class AbstractEditorInstance(context: Context) {
|
||||
if (n < 1 || text.isEmpty()) return ""
|
||||
return runBlocking {
|
||||
val text = textAfterSelection
|
||||
val length = breakIterators.measureUChars(text, n, subtypeManager.activeSubtype().primaryLocale)
|
||||
val length = breakIterators.measureUChars(text, n, subtypeManager.activeSubtype.primaryLocale)
|
||||
text.take(length)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,36 +21,88 @@ import dev.patrickgold.florisboard.lib.kotlin.safeSubstring
|
||||
/**
|
||||
* A snapshot window of an input editor content around the selection/cursor.
|
||||
*
|
||||
* @property text The raw text of the editor content.
|
||||
* @property offset The offset of the whole editor content snapshot. Must be at least 0.
|
||||
* @property text The raw text of the editor content. May be the full text or only a partial view.
|
||||
* @property offset The offset of the whole editor content snapshot. `-1` indicates the value is unknown.
|
||||
* @property localSelection The selection reported by the editor, without [offset] included.
|
||||
* @property localComposing The composing region for the editor, without [offset] included.
|
||||
* @property localCurrentWord The current word for the editor (typically the same as [localComposing]), without
|
||||
* [offset] included.
|
||||
*/
|
||||
data class EditorContent(
|
||||
val text: String,
|
||||
val offset: Int,
|
||||
val localSelection: EditorRange,
|
||||
val localComposing: EditorRange,
|
||||
val localCurrentWord: EditorRange,
|
||||
) {
|
||||
/**
|
||||
* The text before the selection as a new string. This may be the whole text before the selection or only a subset,
|
||||
* depending on the cache and app configuration. Can also be empty for raw editors or if there is no text before
|
||||
* the selection.
|
||||
*/
|
||||
val textBeforeSelection: String
|
||||
get() = if (localSelection.isValid) text.safeSubstring(0, localSelection.start) else ""
|
||||
|
||||
/**
|
||||
* The selected text as a new string. This is always either the entire selected text or an empty string.
|
||||
*/
|
||||
val selectedText: String
|
||||
get() = if (localSelection.isValid) text.safeSubstring(localSelection.start, localSelection.end) else ""
|
||||
|
||||
/**
|
||||
* The text after the selection as a new string. This may be the whole text after the selection or only a subset,
|
||||
* depending on the cache and app configuration. Can also be empty for raw editors or if there is no text after the
|
||||
* selection.
|
||||
*/
|
||||
val textAfterSelection: String
|
||||
get() = if (localSelection.isValid) text.safeSubstring(localSelection.end) else ""
|
||||
|
||||
val composingText: String
|
||||
get() = if (localComposing.isValid) text.safeSubstring(localComposing.start, localComposing.end) else ""
|
||||
|
||||
/**
|
||||
* The selection reported by the editor, with [offset] included.
|
||||
*/
|
||||
val selection: EditorRange
|
||||
get() = if (offset > 0) localSelection.translatedBy(offset) else localSelection
|
||||
|
||||
/**
|
||||
* The composing region for the editor, with [offset] included. May be intentionally invalid even if there is a
|
||||
* current word if the user has requested to disable the composing region.
|
||||
*/
|
||||
val composing: EditorRange
|
||||
get() = if (offset > 0) localComposing.translatedBy(offset) else localComposing
|
||||
|
||||
companion object {
|
||||
val Unspecified = EditorContent("", -1, EditorRange.Unspecified, EditorRange.Unspecified)
|
||||
/**
|
||||
* The composing region text as a new string. May be intentionally empty even if there is a current word if the
|
||||
* user has requested to disable the composing region. This is always either the entire composing text or an empty
|
||||
* string.
|
||||
*/
|
||||
val composingText: String
|
||||
get() = if (localComposing.isValid) text.safeSubstring(localComposing.start, localComposing.end) else ""
|
||||
|
||||
fun selectionOnly(selection: EditorRange) = EditorContent("", -1, selection, EditorRange.Unspecified)
|
||||
/**
|
||||
* The current word for the editor (typically the same as [localComposing]), with [offset] included.
|
||||
*/
|
||||
val currentWord: EditorRange
|
||||
get() = if (offset > 0) localCurrentWord.translatedBy(offset) else localCurrentWord
|
||||
|
||||
/**
|
||||
* The current word for the editor (typically the same as [localComposing]) as a new string. This is always either
|
||||
* the current word or an empty string.
|
||||
*/
|
||||
val currentWordText: String
|
||||
get() = if (localCurrentWord.isValid) text.safeSubstring(localCurrentWord.start, localCurrentWord.end) else ""
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Default editor content which indicates an unspecified content. This is used for raw editors or if there is
|
||||
* an error in the communication between the keyboard and the app.
|
||||
*/
|
||||
val Unspecified =
|
||||
EditorContent("", -1, EditorRange.Unspecified, EditorRange.Unspecified, EditorRange.Unspecified)
|
||||
|
||||
/**
|
||||
* Allows to instantiate a selection-only content, primarily used for mass-selection event handling.
|
||||
*/
|
||||
fun selectionOnly(selection: EditorRange) =
|
||||
EditorContent("", -1, selection, EditorRange.Unspecified, EditorRange.Unspecified)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ import dev.patrickgold.florisboard.ime.clipboard.provider.ClipboardItem
|
||||
import dev.patrickgold.florisboard.ime.clipboard.provider.ItemType
|
||||
import dev.patrickgold.florisboard.ime.keyboard.KeyboardMode
|
||||
import dev.patrickgold.florisboard.ime.input.InputShiftState
|
||||
import dev.patrickgold.florisboard.ime.keyboard.IncognitoMode
|
||||
import dev.patrickgold.florisboard.ime.nlp.SuggestionCandidate
|
||||
import dev.patrickgold.florisboard.ime.text.composing.Appender
|
||||
import dev.patrickgold.florisboard.ime.text.composing.Composer
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyVariation
|
||||
@@ -55,15 +57,19 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
|
||||
private val nlpManager by context.nlpManager()
|
||||
|
||||
private val activeState get() = keyboardManager.activeState
|
||||
val autoSpace = AutoSpaceState()
|
||||
val phantomSpace = PhantomSpaceState()
|
||||
val massSelection = MassSelectionState()
|
||||
|
||||
private fun currentInputConnection() = FlorisImeService.currentInputConnection()
|
||||
|
||||
override fun handleStartInputView(editorInfo: FlorisEditorInfo) {
|
||||
phantomSpace.setInactive()
|
||||
massSelection.reset()
|
||||
super.handleStartInputView(editorInfo)
|
||||
override fun handleStartInputView(editorInfo: FlorisEditorInfo, isRestart: Boolean) {
|
||||
if (!prefs.correction.rememberCapsLockState.get()) {
|
||||
activeState.inputShiftState = InputShiftState.UNSHIFTED
|
||||
}
|
||||
activeState.isActionsOverflowVisible = false
|
||||
activeState.isActionsEditorVisible = false
|
||||
super.handleStartInputView(editorInfo, isRestart)
|
||||
val keyboardMode = when (editorInfo.inputAttributes.type) {
|
||||
InputAttributes.Type.NUMBER -> {
|
||||
activeState.keyVariation = KeyVariation.NORMAL
|
||||
@@ -111,12 +117,17 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
|
||||
//!instance.inputAttributes.flagTextAutoComplete &&
|
||||
//!instance.inputAttributes.flagTextNoSuggestions
|
||||
}
|
||||
if (!prefs.correction.rememberCapsLockState.get()) {
|
||||
activeState.inputShiftState = InputShiftState.UNSHIFTED
|
||||
activeState.isIncognitoMode = when (prefs.advanced.incognitoMode.get()) {
|
||||
IncognitoMode.FORCE_OFF -> false
|
||||
IncognitoMode.FORCE_ON -> true
|
||||
IncognitoMode.DYNAMIC_ON_OFF -> {
|
||||
editorInfo.imeOptions.flagNoPersonalizedLearning || prefs.advanced.forceIncognitoModeFromDynamic.get()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleSelectionUpdate(oldSelection: EditorRange, newSelection: EditorRange, composing: EditorRange) {
|
||||
autoSpace.setInactiveFromUpdate()
|
||||
phantomSpace.setInactiveFromUpdate()
|
||||
if (massSelection.isActive) {
|
||||
super.handleMassSelectionUpdate(newSelection, composing)
|
||||
@@ -125,14 +136,12 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleFinishInputView() {
|
||||
phantomSpace.setInactive()
|
||||
massSelection.reset()
|
||||
super.handleFinishInputView()
|
||||
override fun determineComposingEnabled(): Boolean {
|
||||
return prefs.suggestion.enabled.get()
|
||||
}
|
||||
|
||||
override fun determineComposer(composerName: ExtensionComponentName): Composer {
|
||||
return keyboardManager.resources.composers.value?.get(composerName) ?: Appender.DefaultInstance
|
||||
return keyboardManager.resources.composers.value?.get(composerName) ?: Appender
|
||||
}
|
||||
|
||||
override fun shouldDetermineComposingRegion(editorInfo: FlorisEditorInfo): Boolean {
|
||||
@@ -150,19 +159,59 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
|
||||
* @return True on success or if the selection is already at specified position, false otherwise.
|
||||
*/
|
||||
fun setSelection(start: Int, end: Int): Boolean {
|
||||
autoSpace.setInactive()
|
||||
phantomSpace.setInactive()
|
||||
val selection = EditorRange.normalized(start, end)
|
||||
return super.setSelection(selection)
|
||||
}
|
||||
|
||||
private fun shouldInsertAutoSpaceBefore(text: String): Boolean {
|
||||
if (!prefs.correction.autoSpacePunctuation.get() || text.isEmpty()) return false
|
||||
if (activeInfo.isRawInputEditor) return false
|
||||
if (activeState.keyVariation != KeyVariation.NORMAL) return false
|
||||
|
||||
val punctuationRule = nlpManager.getActivePunctuationRule()
|
||||
val textBefore = activeContent.getTextBeforeCursor(1)
|
||||
return textBefore.isNotEmpty() && !textBefore.last().isWhitespace() &&
|
||||
punctuationRule.symbolsFollowingAutoSpace.contains(text.first())
|
||||
}
|
||||
|
||||
private fun shouldInsertAutoSpaceAfter(text: String): Boolean {
|
||||
if (!prefs.correction.autoSpacePunctuation.get() || text.isEmpty()) return false
|
||||
if (activeInfo.isRawInputEditor) return false
|
||||
if (activeState.keyVariation != KeyVariation.NORMAL) return false
|
||||
|
||||
val punctuationRule = nlpManager.getActivePunctuationRule()
|
||||
val content = activeContent
|
||||
val textBefore = content.getTextBeforeCursor(3).let { textBefore ->
|
||||
if (autoSpace.isActive && textBefore.isNotEmpty() && textBefore.last() == ' ') {
|
||||
textBefore.dropLast(1)
|
||||
} else {
|
||||
textBefore
|
||||
}
|
||||
}
|
||||
return textBefore.isNotEmpty() && !textBefore.last().isWhitespace() &&
|
||||
content.currentWordText.all { !it.isDigit() } &&
|
||||
punctuationRule.symbolsPrecedingAutoSpace.contains(text.first())
|
||||
}
|
||||
|
||||
override fun commitChar(char: String): Boolean {
|
||||
val isInsertAutoSpaceBeforeChar = shouldInsertAutoSpaceBefore(char)
|
||||
val isInsertAutoSpaceAfterChar = shouldInsertAutoSpaceAfter(char)
|
||||
val isDeletePreviousSpace = isInsertAutoSpaceAfterChar && autoSpace.isActive
|
||||
if (isInsertAutoSpaceAfterChar) {
|
||||
autoSpace.setActive()
|
||||
} else {
|
||||
autoSpace.setInactive()
|
||||
}
|
||||
val isPhantomSpaceActive = phantomSpace.determine(char)
|
||||
phantomSpace.setInactive()
|
||||
return if (isPhantomSpaceActive) {
|
||||
super.commitChar("$SPACE$char")
|
||||
} else {
|
||||
super.commitChar(char)
|
||||
}
|
||||
return super.commitChar(
|
||||
char = char,
|
||||
deletePreviousSpace = isDeletePreviousSpace,
|
||||
insertSpaceBeforeChar = isInsertAutoSpaceBeforeChar || isPhantomSpaceActive,
|
||||
insertSpaceAfterChar = isInsertAutoSpaceAfterChar,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,6 +228,7 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
|
||||
*/
|
||||
override fun commitText(text: String): Boolean {
|
||||
val isPhantomSpaceActive = phantomSpace.determine(text)
|
||||
autoSpace.setInactive()
|
||||
phantomSpace.setInactive()
|
||||
return if (isPhantomSpaceActive) {
|
||||
super.commitText("$SPACE$text")
|
||||
@@ -188,19 +238,32 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes the given [text] in the current composing region. Does nothing if the current
|
||||
* Completes the given [candidate] in the current composing region. Does nothing if the current
|
||||
* input editor is not rich or if the input connection is invalid.
|
||||
*
|
||||
* Current phantom space state is respected and a space char will be inserted accordingly.
|
||||
* Phantom space will be activated if the text is committed.
|
||||
*
|
||||
* @param text The text to complete in this editor.
|
||||
* @param candidate The candidate to complete in this editor.
|
||||
*
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun commitCompletion(text: String): Boolean {
|
||||
fun commitCompletion(candidate: SuggestionCandidate): Boolean {
|
||||
val text = candidate.text.toString()
|
||||
if (text.isEmpty() || activeInfo.isRawInputEditor) return false
|
||||
return commitText(text).also { phantomSpace.setActive(showComposingRegion = false) }
|
||||
val content = activeContent
|
||||
return if (content.composing.isValid) {
|
||||
phantomSpace.setActive(showComposingRegion = false, candidate = candidate)
|
||||
super.finalizeComposingText(text)
|
||||
} else {
|
||||
val isPhantomSpaceActive = phantomSpace.determine(text)
|
||||
phantomSpace.setActive(showComposingRegion = false, candidate = candidate)
|
||||
return if (isPhantomSpaceActive) {
|
||||
super.commitText("$SPACE$text")
|
||||
} else {
|
||||
super.commitText(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -276,9 +339,10 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
|
||||
*/
|
||||
fun deleteBackwards(): Boolean {
|
||||
val content = activeContent
|
||||
if (phantomSpace.isActive && content.composing.isValid && prefs.glide.immediateBackspaceDeletesWord.get()) {
|
||||
if (phantomSpace.isActive && content.currentWord.isValid && prefs.glide.immediateBackspaceDeletesWord.get()) {
|
||||
return deleteWordBackwards()
|
||||
}
|
||||
autoSpace.setInactive()
|
||||
phantomSpace.setInactive()
|
||||
return if (content.selection.isSelectionMode) {
|
||||
commitText("")
|
||||
@@ -295,6 +359,7 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun deleteWordBackwards(): Boolean {
|
||||
autoSpace.setInactive()
|
||||
phantomSpace.setInactive()
|
||||
return if (activeContent.selection.isSelectionMode) {
|
||||
commitText("")
|
||||
@@ -304,6 +369,7 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
|
||||
}
|
||||
|
||||
fun selectionSetNWordsLeft(n: Int): Boolean {
|
||||
autoSpace.setInactive()
|
||||
phantomSpace.setInactive()
|
||||
val content = activeContent
|
||||
val selection = content.selection
|
||||
@@ -323,6 +389,7 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun performClipboardCut(): Boolean {
|
||||
autoSpace.setInactive()
|
||||
phantomSpace.setInactive()
|
||||
val text = activeContent.selectedText.ifBlank { currentInputConnection()?.getSelectedText(0) }
|
||||
if (text != null) {
|
||||
@@ -340,6 +407,7 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun performClipboardCopy(): Boolean {
|
||||
autoSpace.setInactive()
|
||||
phantomSpace.setInactive()
|
||||
val text = activeContent.selectedText.ifBlank { currentInputConnection()?.getSelectedText(0) }
|
||||
if (text != null) {
|
||||
@@ -358,8 +426,9 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun performClipboardPaste(): Boolean {
|
||||
autoSpace.setInactive()
|
||||
phantomSpace.setInactive()
|
||||
return commitClipboardItem(clipboardManager.primaryClip.value).also { result ->
|
||||
return commitClipboardItem(clipboardManager.primaryClip).also { result ->
|
||||
if (!result) {
|
||||
appContext.showShortToast("Failed to paste item.")
|
||||
}
|
||||
@@ -373,6 +442,7 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun performClipboardSelectAll(): Boolean {
|
||||
autoSpace.setInactive()
|
||||
phantomSpace.setInactive()
|
||||
val ic = currentInputConnection() ?: return false
|
||||
ic.finishComposingText()
|
||||
@@ -389,6 +459,7 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun performEnter(): Boolean {
|
||||
autoSpace.setInactive()
|
||||
phantomSpace.setInactive()
|
||||
return if (activeInfo.isRawInputEditor) {
|
||||
sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER)
|
||||
@@ -405,6 +476,7 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun performEnterAction(action: ImeOptions.Action): Boolean {
|
||||
autoSpace.setInactive()
|
||||
phantomSpace.setInactive()
|
||||
val ic = currentInputConnection() ?: return false
|
||||
return ic.performEditorAction(action.toInt())
|
||||
@@ -416,6 +488,7 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun performUndo(): Boolean {
|
||||
autoSpace.setInactive()
|
||||
phantomSpace.setInactive()
|
||||
return sendDownUpKeyEvent(KeyEvent.KEYCODE_Z, meta(ctrl = true))
|
||||
}
|
||||
@@ -426,29 +499,33 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun performRedo(): Boolean {
|
||||
autoSpace.setInactive()
|
||||
phantomSpace.setInactive()
|
||||
return sendDownUpKeyEvent(KeyEvent.KEYCODE_Z, meta(ctrl = true, shift = true))
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
super.reset()
|
||||
autoSpace.setInactive()
|
||||
phantomSpace.setInactive()
|
||||
massSelection.reset()
|
||||
}
|
||||
|
||||
private fun PhantomSpaceState.determine(text: String, forceActive: Boolean = false): Boolean {
|
||||
val content = activeContent
|
||||
val selection = content.selection
|
||||
if (!(isActive || forceActive) || selection.isNotValid || selection.start <= 0) return false
|
||||
val textBefore = content.getTextBeforeCursor(2)
|
||||
if (!(isActive || forceActive) || selection.isNotValid || selection.start <= 0 || text.isEmpty()) return false
|
||||
val textBefore = content.getTextBeforeCursor(1)
|
||||
val punctuationRule = nlpManager.getActivePunctuationRule()
|
||||
return punctuationRule.symbolsPrecedingSpace.matches(textBefore) &&
|
||||
punctuationRule.symbolsFollowingSpace.matches(text)
|
||||
return textBefore.isNotEmpty() &&
|
||||
(punctuationRule.symbolsPrecedingPhantomSpace.contains(textBefore[textBefore.length - 1]) ||
|
||||
textBefore[textBefore.length - 1].isLetterOrDigit()) &&
|
||||
(punctuationRule.symbolsFollowingPhantomSpace.contains(text[0]) || text[0].isLetterOrDigit())
|
||||
}
|
||||
|
||||
class PhantomSpaceState {
|
||||
class AutoSpaceState {
|
||||
companion object {
|
||||
private const val F_IS_ACTIVE = 0x1
|
||||
private const val F_SHOW_COMPOSING_REGION = 0x2
|
||||
private const val F_STAY_ACTIVE_NEXT_UPDATE = 0x4
|
||||
}
|
||||
|
||||
@@ -460,15 +537,8 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
|
||||
val isInactive: Boolean
|
||||
get() = !isActive
|
||||
|
||||
val showComposingRegion: Boolean
|
||||
get() = state.get() and F_SHOW_COMPOSING_REGION != 0
|
||||
|
||||
fun setActive(showComposingRegion: Boolean, stayActiveNextUpdate: Boolean = true) {
|
||||
state.set(
|
||||
F_IS_ACTIVE
|
||||
or (if (showComposingRegion) F_SHOW_COMPOSING_REGION else 0)
|
||||
or (if (stayActiveNextUpdate) F_STAY_ACTIVE_NEXT_UPDATE else 0)
|
||||
)
|
||||
fun setActive(stayActiveNextUpdate: Boolean = true) {
|
||||
state.set(F_IS_ACTIVE or (if (stayActiveNextUpdate) F_STAY_ACTIVE_NEXT_UPDATE else 0))
|
||||
}
|
||||
|
||||
fun setInactive() {
|
||||
@@ -482,6 +552,54 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
|
||||
}
|
||||
}
|
||||
|
||||
class PhantomSpaceState {
|
||||
companion object {
|
||||
private const val F_IS_ACTIVE = 0x1
|
||||
private const val F_SHOW_COMPOSING_REGION = 0x2
|
||||
private const val F_STAY_ACTIVE_NEXT_UPDATE = 0x4
|
||||
}
|
||||
|
||||
private val state = AtomicInteger(0)
|
||||
var candidateForRevert: SuggestionCandidate? = null
|
||||
private set
|
||||
|
||||
val isActive: Boolean
|
||||
get() = state.get() and F_IS_ACTIVE != 0
|
||||
|
||||
val isInactive: Boolean
|
||||
get() = !isActive
|
||||
|
||||
val showComposingRegion: Boolean
|
||||
get() = state.get() and F_SHOW_COMPOSING_REGION != 0
|
||||
|
||||
fun setActive(
|
||||
showComposingRegion: Boolean,
|
||||
stayActiveNextUpdate: Boolean = true,
|
||||
candidate: SuggestionCandidate? = null,
|
||||
) {
|
||||
state.set(
|
||||
F_IS_ACTIVE
|
||||
or (if (showComposingRegion) F_SHOW_COMPOSING_REGION else 0)
|
||||
or (if (stayActiveNextUpdate) F_STAY_ACTIVE_NEXT_UPDATE else 0)
|
||||
)
|
||||
candidateForRevert = candidate
|
||||
}
|
||||
|
||||
fun setInactive() {
|
||||
state.set(0)
|
||||
candidateForRevert = null
|
||||
}
|
||||
|
||||
fun setInactiveFromUpdate() {
|
||||
val prevStateValue = state.getAndUpdate { state ->
|
||||
if ((state and F_STAY_ACTIVE_NEXT_UPDATE) != 0) (state and F_STAY_ACTIVE_NEXT_UPDATE.inv()) else 0
|
||||
}
|
||||
if ((prevStateValue and F_STAY_ACTIVE_NEXT_UPDATE) == 0) {
|
||||
candidateForRevert = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class MassSelectionState {
|
||||
private val state = AtomicInteger(0)
|
||||
|
||||
|
||||
@@ -14,33 +14,30 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.text.smartbar
|
||||
package dev.patrickgold.florisboard.ime.input
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.lib.compose.stringRes
|
||||
import dev.patrickgold.jetpref.datastore.ui.listPrefEntries
|
||||
|
||||
/**
|
||||
* Enum class defining the possible row types for the Smartbar action rows.
|
||||
*/
|
||||
enum class SmartbarRowType {
|
||||
QUICK_ACTIONS,
|
||||
CLIPBOARD_CURSOR_TOOLS;
|
||||
enum class HapticVibrationMode {
|
||||
USE_VIBRATOR_DIRECTLY,
|
||||
USE_HAPTIC_FEEDBACK_INTERFACE;
|
||||
|
||||
companion object {
|
||||
@Composable
|
||||
fun listEntries() = listPrefEntries {
|
||||
entry(
|
||||
key = QUICK_ACTIONS,
|
||||
label = stringRes(R.string.enum__smartbar_row_type__quick_actions),
|
||||
description = stringRes(R.string.enum__smartbar_row_type__quick_actions__description),
|
||||
key = USE_VIBRATOR_DIRECTLY,
|
||||
label = stringRes(R.string.enum__haptic_vibration_mode__use_vibrator_directly),
|
||||
description = stringRes(R.string.enum__haptic_vibration_mode__use_vibrator_directly__description),
|
||||
showDescriptionOnlyIfSelected = true,
|
||||
)
|
||||
entry(
|
||||
key = CLIPBOARD_CURSOR_TOOLS,
|
||||
label = stringRes(R.string.enum__smartbar_row_type__clipboard_cursor_tools),
|
||||
description = stringRes(R.string.enum__smartbar_row_type__clipboard_cursor_tools__description),
|
||||
key = USE_HAPTIC_FEEDBACK_INTERFACE,
|
||||
label = stringRes(R.string.enum__haptic_vibration_mode__use_haptic_feedback_interface),
|
||||
description = stringRes(R.string.enum__haptic_vibration_mode__use_haptic_feedback_interface__description),
|
||||
showDescriptionOnlyIfSelected = true,
|
||||
)
|
||||
}
|
||||
@@ -73,7 +73,7 @@ class InputEventDispatcher private constructor(private val repeatableKeyCodes: I
|
||||
|
||||
private fun determineRepeatDelay(data: KeyData): Long {
|
||||
val factor = when (data.code) {
|
||||
KeyCode.DELETE_WORD, KeyCode.FORWARD_DELETE_WORD -> 5.0f
|
||||
KeyCode.DELETE_WORD, KeyCode.FORWARD_DELETE_WORD, KeyCode.UNDO, KeyCode.REDO -> 5.0f
|
||||
else -> 1.0f
|
||||
}
|
||||
return (KeyRepeatDelay * factor).toLong()
|
||||
@@ -207,6 +207,15 @@ class InputEventDispatcher private constructor(private val repeatableKeyCodes: I
|
||||
return lastKeyEventDown.data.code == data.code
|
||||
}
|
||||
|
||||
fun isRepeatable(data: KeyData): Boolean {
|
||||
return repeatableKeyCodes.contains(data.code)
|
||||
}
|
||||
|
||||
fun isRepeatableCodeLastDown(): Boolean {
|
||||
val event = lastKeyEventDown
|
||||
return repeatableKeyCodes.contains(event.data.code)
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this dispatcher and cancels the local coroutine scope.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.input
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.lib.compose.stringRes
|
||||
import dev.patrickgold.jetpref.datastore.ui.listPrefEntries
|
||||
|
||||
enum class InputFeedbackActivationMode {
|
||||
RESPECT_SYSTEM_SETTINGS,
|
||||
IGNORE_SYSTEM_SETTINGS;
|
||||
|
||||
companion object {
|
||||
@Composable
|
||||
fun audioListEntries() = listPrefEntries {
|
||||
entry(
|
||||
key = RESPECT_SYSTEM_SETTINGS,
|
||||
label = stringRes(R.string.enum__input_feedback_activation_mode__audio_respect_system_settings),
|
||||
)
|
||||
entry(
|
||||
key = IGNORE_SYSTEM_SETTINGS,
|
||||
label = stringRes(R.string.enum__input_feedback_activation_mode__audio_ignore_system_settings),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun hapticListEntries() = listPrefEntries {
|
||||
entry(
|
||||
key = RESPECT_SYSTEM_SETTINGS,
|
||||
label = stringRes(R.string.enum__input_feedback_activation_mode__haptic_respect_system_settings),
|
||||
)
|
||||
entry(
|
||||
key = IGNORE_SYSTEM_SETTINGS,
|
||||
label = stringRes(R.string.enum__input_feedback_activation_mode__haptic_ignore_system_settings),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,25 +16,19 @@
|
||||
|
||||
package dev.patrickgold.florisboard.ime.input
|
||||
|
||||
import android.content.Context
|
||||
import android.inputmethodservice.InputMethodService
|
||||
import android.media.AudioManager
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import android.os.VibratorManager
|
||||
import android.provider.Settings
|
||||
import android.view.HapticFeedbackConstants
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.florisPreferenceModel
|
||||
import dev.patrickgold.florisboard.ime.keyboard.KeyData
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
|
||||
import dev.patrickgold.florisboard.lib.android.AndroidVersion
|
||||
import dev.patrickgold.florisboard.lib.android.systemServiceOrNull
|
||||
import dev.patrickgold.florisboard.lib.compose.stringRes
|
||||
import dev.patrickgold.florisboard.lib.android.systemVibratorOrNull
|
||||
import dev.patrickgold.florisboard.lib.android.vibrate
|
||||
import dev.patrickgold.florisboard.lib.devtools.flogDebug
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -50,39 +44,6 @@ val LocalInputFeedbackController = staticCompositionLocalOf<InputFeedbackControl
|
||||
class InputFeedbackController private constructor(private val ims: InputMethodService) {
|
||||
companion object {
|
||||
fun new(ims: InputMethodService) = InputFeedbackController(ims)
|
||||
|
||||
@Composable
|
||||
fun hasAmplitudeControl(): Boolean {
|
||||
val vibrator = LocalContext.current.systemVibratorOrNull()
|
||||
return when {
|
||||
AndroidVersion.ATLEAST_API26_O -> vibrator != null && vibrator.hasAmplitudeControl()
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun generateVibrationStrengthErrorSummary(): String? {
|
||||
val vibrator = LocalContext.current.systemVibratorOrNull()
|
||||
return when {
|
||||
AndroidVersion.ATLEAST_API26_O -> when {
|
||||
vibrator == null || !vibrator.hasAmplitudeControl() -> {
|
||||
stringRes(R.string.pref__input_feedback__haptic_vibration_strength__summary_no_amplitude_ctrl)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
else -> {
|
||||
stringRes(R.string.pref__input_feedback__haptic_vibration_strength__summary_unsupported_android_version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.systemVibratorOrNull(): Vibrator? {
|
||||
return if (AndroidVersion.ATLEAST_API31_S) {
|
||||
this.systemServiceOrNull(VibratorManager::class)?.defaultVibrator
|
||||
} else {
|
||||
this.systemServiceOrNull(Vibrator::class)
|
||||
}?.takeIf { it.hasVibrator() }
|
||||
}
|
||||
}
|
||||
|
||||
private val prefs by florisPreferenceModel()
|
||||
@@ -133,7 +94,8 @@ class InputFeedbackController private constructor(private val ims: InputMethodSe
|
||||
private fun performAudioFeedback(data: KeyData, factor: Double) {
|
||||
if (audioManager == null) return
|
||||
if (!prefs.inputFeedback.audioEnabled.get()) return
|
||||
if (!prefs.inputFeedback.audioIgnoreSystemSettings.get() && !systemAudioEnabled) return
|
||||
if (prefs.inputFeedback.audioActivationMode.get() ==
|
||||
InputFeedbackActivationMode.RESPECT_SYSTEM_SETTINGS && !systemAudioEnabled) return
|
||||
|
||||
scope.launch {
|
||||
val volume = (prefs.inputFeedback.audioVolume.get() * factor) / 100.0
|
||||
@@ -153,10 +115,11 @@ class InputFeedbackController private constructor(private val ims: InputMethodSe
|
||||
private fun performHapticFeedback(data: KeyData, factor: Double) {
|
||||
if (vibrator == null) return
|
||||
if (!prefs.inputFeedback.hapticEnabled.get()) return
|
||||
if (!prefs.inputFeedback.hapticIgnoreSystemSettings.get() && !systemHapticEnabled) return
|
||||
if (prefs.inputFeedback.hapticActivationMode.get() ==
|
||||
InputFeedbackActivationMode.RESPECT_SYSTEM_SETTINGS && !systemHapticEnabled) return
|
||||
|
||||
scope.launch {
|
||||
if (!prefs.inputFeedback.hapticUseVibrator.get()) {
|
||||
if (prefs.inputFeedback.hapticVibrationMode.get() == HapticVibrationMode.USE_HAPTIC_FEEDBACK_INTERFACE) {
|
||||
val view = ims.window?.window?.decorView ?: return@launch
|
||||
val hfc = if (factor < 1.0 && AndroidVersion.ATLEAST_API27_O_MR1) {
|
||||
HapticFeedbackConstants.TEXT_HANDLE_MOVE
|
||||
@@ -171,29 +134,11 @@ class InputFeedbackController private constructor(private val ims: InputMethodSe
|
||||
// If not performed fall back to using the vibrator directly
|
||||
}
|
||||
|
||||
val duration = prefs.inputFeedback.hapticVibrationDuration.get()
|
||||
if (duration != 0) {
|
||||
val effectiveDuration = (duration * factor).toLong().coerceAtLeast(1L)
|
||||
if (AndroidVersion.ATLEAST_API26_O) {
|
||||
val strength = when {
|
||||
vibrator.hasAmplitudeControl() -> prefs.inputFeedback.hapticVibrationStrength.get()
|
||||
else -> VibrationEffect.DEFAULT_AMPLITUDE
|
||||
}
|
||||
if (strength != 0) {
|
||||
val effectiveStrength = when {
|
||||
vibrator.hasAmplitudeControl() -> (255.0 * ((strength * factor) / 100.0)).toInt().coerceIn(1, 255)
|
||||
else -> strength
|
||||
}
|
||||
flogDebug { "Perform haptic with duration=$effectiveDuration and strength=$effectiveStrength" }
|
||||
val effect = VibrationEffect.createOneShot(effectiveDuration, effectiveStrength)
|
||||
vibrator.vibrate(effect)
|
||||
}
|
||||
} else {
|
||||
flogDebug { "Perform haptic with duration=$effectiveDuration" }
|
||||
@Suppress("DEPRECATION")
|
||||
vibrator.vibrate(effectiveDuration)
|
||||
}
|
||||
}
|
||||
vibrator.vibrate(
|
||||
duration = prefs.inputFeedback.hapticVibrationDuration.get(),
|
||||
strength = prefs.inputFeedback.hapticVibrationStrength.get(),
|
||||
factor = factor,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,11 +28,15 @@ import dev.patrickgold.florisboard.ime.text.key.KeyType
|
||||
import dev.patrickgold.florisboard.lib.FlorisLocale
|
||||
|
||||
interface ComputingEvaluator {
|
||||
fun activeEditorInfo(): FlorisEditorInfo
|
||||
val version: Int
|
||||
|
||||
fun activeState(): KeyboardState
|
||||
val keyboard: Keyboard
|
||||
|
||||
fun activeSubtype(): Subtype
|
||||
val editorInfo: FlorisEditorInfo
|
||||
|
||||
val state: KeyboardState
|
||||
|
||||
val subtype: Subtype
|
||||
|
||||
fun context(): Context?
|
||||
|
||||
@@ -42,19 +46,21 @@ interface ComputingEvaluator {
|
||||
|
||||
fun evaluateVisible(data: KeyData): Boolean
|
||||
|
||||
fun keyboard(): Keyboard
|
||||
|
||||
fun isSlot(data: KeyData): Boolean
|
||||
|
||||
fun slotData(data: KeyData): KeyData?
|
||||
}
|
||||
|
||||
object DefaultComputingEvaluator : ComputingEvaluator {
|
||||
override fun activeEditorInfo(): FlorisEditorInfo = FlorisEditorInfo.Unspecified
|
||||
override val version = -1
|
||||
|
||||
override fun activeState(): KeyboardState = KeyboardState.new()
|
||||
override val keyboard = PlaceholderLoadingKeyboard
|
||||
|
||||
override fun activeSubtype(): Subtype = Subtype.DEFAULT
|
||||
override val editorInfo = FlorisEditorInfo.Unspecified
|
||||
|
||||
override val state = KeyboardState.new()
|
||||
|
||||
override val subtype = Subtype.DEFAULT
|
||||
|
||||
override fun context(): Context? = null
|
||||
|
||||
@@ -64,8 +70,6 @@ object DefaultComputingEvaluator : ComputingEvaluator {
|
||||
|
||||
override fun evaluateVisible(data: KeyData): Boolean = true
|
||||
|
||||
override fun keyboard(): Keyboard = PlaceholderLoadingKeyboard
|
||||
|
||||
override fun isSlot(data: KeyData): Boolean = false
|
||||
|
||||
override fun slotData(data: KeyData): KeyData? = null
|
||||
@@ -103,8 +107,8 @@ fun ComputingEvaluator.computeLabel(data: KeyData): String? {
|
||||
KeyCode.PHONE_PAUSE -> evaluator.context()?.getString(R.string.key__phone_pause)
|
||||
KeyCode.PHONE_WAIT -> evaluator.context()?.getString(R.string.key__phone_wait)
|
||||
KeyCode.SPACE, KeyCode.CJK_SPACE -> {
|
||||
when (evaluator.keyboard().mode) {
|
||||
KeyboardMode.CHARACTERS -> evaluator.activeSubtype().primaryLocale.let { locale ->
|
||||
when (evaluator.keyboard.mode) {
|
||||
KeyboardMode.CHARACTERS -> evaluator.subtype.primaryLocale.let { locale ->
|
||||
computeLanguageDisplayName(locale, evaluator.displayLanguageNamesIn())
|
||||
}
|
||||
else -> null
|
||||
@@ -183,8 +187,8 @@ fun ComputingEvaluator.computeIconResId(data: KeyData): Int? {
|
||||
R.drawable.ic_backspace
|
||||
}
|
||||
KeyCode.ENTER -> {
|
||||
val imeOptions = evaluator.activeEditorInfo().imeOptions
|
||||
val inputAttributes = evaluator.activeEditorInfo().inputAttributes
|
||||
val imeOptions = evaluator.editorInfo.imeOptions
|
||||
val inputAttributes = evaluator.editorInfo.inputAttributes
|
||||
if (imeOptions.flagNoEnterAction || inputAttributes.flagTextMultiLine) {
|
||||
R.drawable.ic_keyboard_return
|
||||
} else {
|
||||
@@ -213,13 +217,13 @@ fun ComputingEvaluator.computeIconResId(data: KeyData): Int? {
|
||||
R.drawable.ic_settings
|
||||
}
|
||||
KeyCode.SHIFT -> {
|
||||
when (evaluator.activeState().inputShiftState != InputShiftState.UNSHIFTED) {
|
||||
when (evaluator.state.inputShiftState != InputShiftState.UNSHIFTED) {
|
||||
true -> R.drawable.ic_keyboard_capslock
|
||||
else -> R.drawable.ic_keyboard_arrow_up
|
||||
}
|
||||
}
|
||||
KeyCode.SPACE, KeyCode.CJK_SPACE -> {
|
||||
when (evaluator.keyboard().mode) {
|
||||
when (evaluator.keyboard.mode) {
|
||||
KeyboardMode.NUMERIC,
|
||||
KeyboardMode.NUMERIC_ADVANCED,
|
||||
KeyboardMode.PHONE,
|
||||
@@ -235,15 +239,28 @@ fun ComputingEvaluator.computeIconResId(data: KeyData): Int? {
|
||||
KeyCode.REDO -> {
|
||||
R.drawable.ic_redo
|
||||
}
|
||||
KeyCode.TOGGLE_ACTIONS_OVERFLOW -> {
|
||||
R.drawable.ic_more_horiz
|
||||
}
|
||||
KeyCode.TOGGLE_INCOGNITO_MODE -> {
|
||||
if (evaluator.state.isIncognitoMode) {
|
||||
R.drawable.ic_incognito
|
||||
} else {
|
||||
R.drawable.ic_incognito_off
|
||||
}
|
||||
}
|
||||
KeyCode.TOGGLE_AUTOCORRECT -> {
|
||||
R.drawable.ic_font_download
|
||||
}
|
||||
KeyCode.KANA_SWITCHER -> {
|
||||
if (evaluator.activeState().isKanaKata) {
|
||||
if (evaluator.state.isKanaKata) {
|
||||
R.drawable.ic_keyboard_kana_switcher_kata
|
||||
} else {
|
||||
R.drawable.ic_keyboard_kana_switcher_hira
|
||||
}
|
||||
}
|
||||
KeyCode.CHAR_WIDTH_SWITCHER -> {
|
||||
if (evaluator.activeState().isCharHalfWidth) {
|
||||
if (evaluator.state.isCharHalfWidth) {
|
||||
R.drawable.ic_keyboard_char_width_switcher_full
|
||||
} else {
|
||||
R.drawable.ic_keyboard_char_width_switcher_half
|
||||
@@ -255,6 +272,12 @@ fun ComputingEvaluator.computeIconResId(data: KeyData): Int? {
|
||||
KeyCode.CHAR_WIDTH_HALF -> {
|
||||
R.drawable.ic_keyboard_char_width_switcher_half
|
||||
}
|
||||
KeyCode.DRAG_MARKER -> {
|
||||
if (evaluator.state.debugShowDragAndDropHelpers) R.drawable.ic_close else null
|
||||
}
|
||||
KeyCode.NOOP -> {
|
||||
R.drawable.ic_close
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
@@ -31,7 +32,10 @@ import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.florisPreferenceModel
|
||||
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
|
||||
import dev.patrickgold.florisboard.ime.text.smartbar.SecondaryRowPlacement
|
||||
import dev.patrickgold.florisboard.ime.smartbar.ExtendedActionsPlacement
|
||||
import dev.patrickgold.florisboard.ime.smartbar.SmartbarLayout
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyboard
|
||||
import dev.patrickgold.florisboard.keyboardManager
|
||||
import dev.patrickgold.florisboard.lib.android.isOrientationLandscape
|
||||
import dev.patrickgold.florisboard.lib.observeAsTransformingState
|
||||
import dev.patrickgold.florisboard.lib.util.ViewUtils
|
||||
@@ -53,26 +57,46 @@ object FlorisImeSizing {
|
||||
|
||||
@Composable
|
||||
fun keyboardUiHeight(): Dp {
|
||||
val context = LocalContext.current
|
||||
val keyboardManager by context.keyboardManager()
|
||||
val evaluator by keyboardManager.activeEvaluator.collectAsState()
|
||||
val lastCharactersEvaluator by keyboardManager.lastCharactersEvaluator.collectAsState()
|
||||
val rowCount = when (evaluator.keyboard.mode) {
|
||||
KeyboardMode.CHARACTERS,
|
||||
KeyboardMode.NUMERIC_ADVANCED,
|
||||
KeyboardMode.SYMBOLS,
|
||||
KeyboardMode.SYMBOLS2 -> lastCharactersEvaluator.keyboard as TextKeyboard
|
||||
else -> evaluator.keyboard as TextKeyboard
|
||||
}.rowCount.coerceAtLeast(4)
|
||||
return (keyboardRowBaseHeight * rowCount)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun smartbarUiHeight(): Dp {
|
||||
val prefs by florisPreferenceModel()
|
||||
val numberRowEnabled by prefs.keyboard.numberRow.observeAsState()
|
||||
val smartbarEnabled by prefs.smartbar.enabled.observeAsState()
|
||||
val secondaryRowEnabled by prefs.smartbar.secondaryActionsEnabled.observeAsState()
|
||||
val secondaryRowExpanded by prefs.smartbar.secondaryActionsExpanded.observeAsState()
|
||||
val secondaryRowPlacement by prefs.smartbar.secondaryActionsPlacement.observeAsState()
|
||||
val smartbarLayout by prefs.smartbar.layout.observeAsState()
|
||||
val extendedActionsExpanded by prefs.smartbar.extendedActionsExpanded.observeAsState()
|
||||
val extendedActionsPlacement by prefs.smartbar.extendedActionsPlacement.observeAsState()
|
||||
val height =
|
||||
if (smartbarEnabled) {
|
||||
if (secondaryRowEnabled && secondaryRowExpanded &&
|
||||
secondaryRowPlacement != SecondaryRowPlacement.OVERLAY_APP_UI) {
|
||||
if (smartbarLayout == SmartbarLayout.SUGGESTIONS_ACTIONS_EXTENDED && extendedActionsExpanded &&
|
||||
extendedActionsPlacement != ExtendedActionsPlacement.OVERLAY_APP_UI) {
|
||||
smartbarHeight * 2
|
||||
} else {
|
||||
smartbarHeight
|
||||
}
|
||||
} else {
|
||||
0.dp
|
||||
} + (keyboardRowBaseHeight * (if (numberRowEnabled) 5 else 4))
|
||||
}
|
||||
return height
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun imeUiHeight(): Dp {
|
||||
return keyboardUiHeight() + smartbarUiHeight()
|
||||
}
|
||||
|
||||
object Static {
|
||||
var keyboardRowBaseHeightPx: Int = 0
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.keyboard
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.lib.compose.stringRes
|
||||
import dev.patrickgold.jetpref.datastore.ui.listPrefEntries
|
||||
|
||||
enum class IncognitoMode {
|
||||
FORCE_OFF,
|
||||
FORCE_ON,
|
||||
DYNAMIC_ON_OFF;
|
||||
|
||||
companion object {
|
||||
@Composable
|
||||
fun listEntries() = listPrefEntries {
|
||||
entry(
|
||||
key = FORCE_OFF,
|
||||
label = stringRes(R.string.enum__incognito_mode__force_off),
|
||||
description = stringRes(R.string.enum__incognito_mode__force_off__description),
|
||||
showDescriptionOnlyIfSelected = true,
|
||||
)
|
||||
entry(
|
||||
key = DYNAMIC_ON_OFF,
|
||||
label = stringRes(R.string.enum__incognito_mode__dynamic_on_off),
|
||||
description = stringRes(R.string.enum__incognito_mode__dynamic_on_off__description),
|
||||
showDescriptionOnlyIfSelected = true,
|
||||
)
|
||||
entry(
|
||||
key = FORCE_ON,
|
||||
label = stringRes(R.string.enum__incognito_mode__force_on),
|
||||
description = stringRes(R.string.enum__incognito_mode__force_on__description),
|
||||
showDescriptionOnlyIfSelected = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user