Compare commits

...

106 Commits

Author SHA1 Message Date
Patrick Goldinger
26650d2a00 Release v0.4.0-alpha03 2022-08-28 21:52:32 +02:00
florisboard-bot
4395eac500 Update translations from Crowdin 2022-08-28 21:34:01 +02:00
Patrick Goldinger
90e60a5e03 Fix KeyboardManager observer causing crash (#2020) 2022-08-28 15:49:50 +02:00
Patrick Goldinger
e647e0d248 Fix quick actions menu for RTL languages 2022-08-27 21:09:44 +02:00
Patrick Goldinger
31c046720a Fix Smartbar source string still using old label 2022-08-26 21:07:34 +02:00
Patrick Goldinger
03b70b43a6 Update privacy policy link to new location
See https://github.com/florisboard/florisboard/discussions/2021
2022-08-25 22:39:57 +02:00
Patrick Goldinger
de2b3b9433 Merge pull request #2012 from florisboard/smartbar-actions-rework
Smartbar Quick Action Rework + directly related keyboard logic changes + incognito mode
2022-08-23 23:40:57 +02:00
Patrick Goldinger
a51f671c3c Clean up changes and fix theme element translations 2022-08-23 22:30:12 +02:00
Patrick Goldinger
314cdf79bf Fix numeric and telpad layouts row height being miscalculated 2022-08-23 21:48:40 +02:00
Patrick Goldinger
d7137b41fe Adapt all default themes to new Smartbar rules 2022-08-23 16:44:44 +02:00
Patrick Goldinger
acad9f66a6 Fix actions editor screen not clearing flag sometimes 2022-08-23 16:15:59 +02:00
Patrick Goldinger
8d0565854c Fix NLP manager bindings not setting private session flag 2022-08-23 16:08:26 +02:00
Patrick Goldinger
6932fecbbd Improve actions overflow theme and style capabilities 2022-08-23 15:32:51 +02:00
Patrick Goldinger
3e6ed3d7b0 Fix state bug with drag marker in customize action order screen 2022-08-23 14:44:47 +02:00
Patrick Goldinger
85e76892b7 Fix quick actions overflow crashing in landscape mode (#2020) 2022-08-23 14:24:14 +02:00
Patrick Goldinger
65cbc4bea3 Implement Smartbar action order customization screen (#1612) 2022-08-23 14:08:24 +02:00
Patrick Goldinger
79d177144a Add autocorrect toggle placeholder message 2022-08-22 12:56:24 +02:00
Patrick Goldinger
8cb2b0bfa7 Implement incognito mode and toggle (#153, #617) 2022-08-22 12:20:19 +02:00
Patrick Goldinger
201de6a6db Adjust and fix keyboard height calculation (#1561) 2022-08-21 23:03:21 +02:00
Patrick Goldinger
f65b11bc6d Remove obsolete clipboard cursor row 2022-08-20 14:45:51 +02:00
Patrick Goldinger
86031bb428 Fix candidate row scrollbar height being too tall 2022-08-20 13:46:06 +02:00
Patrick Goldinger
58f62e1bd5 Animate background color and add ripple effect to quick action 2022-08-20 13:42:21 +02:00
Patrick Goldinger
6212e35382 Fix FlorisImeTheme not initializing MaterialTheme at all 2022-08-20 13:41:36 +02:00
Patrick Goldinger
c8d0c6269f Properly implement actions overflow panel 2022-08-19 18:55:40 +02:00
Patrick Goldinger
e6f40932ed Rework Smartbar themeing and make minor sizing adjustments 2022-08-18 22:49:41 +02:00
Patrick Goldinger
f8af02c400 Add support for tooltips on Smartbar actions (#1094) 2022-08-16 18:59:31 +02:00
Patrick Goldinger
932a7c3126 Add base logic and UI for Smartbar actions overflow menu 2022-08-14 13:12:35 +02:00
Patrick Goldinger
f0c2ac566f Move Smartbar display mode pref from Typing to Smartbar screen 2022-08-13 14:44:55 +02:00
Patrick Goldinger
19224e5f18 Rework Smartbar screen and introduce new Smartbar layouts 2022-08-13 14:36:28 +02:00
Patrick Goldinger
6c325af80e Remove SmartbarRowType enum and related resources
Is superseded by the fact that all surfaces (except candidates and inline autofill) are now action buttons and freely reorder-able. As such there is no need anymore to distinguish between row types.
2022-08-12 18:32:10 +02:00
Patrick Goldinger
bb82b78cb7 Fix auto action expansion interfering with arrow keys (#1674) 2022-08-11 21:34:06 +02:00
Patrick Goldinger
eb30eed735 Fix quick action icon size too small for landscape (#1781) 2022-08-11 18:51:30 +02:00
Patrick Goldinger
3198977143 Rework base implementation of QuickAction and it's composables 2022-08-11 18:49:33 +02:00
Patrick Goldinger
f3b3c21aaa Add Incognito mode and Autocorrect toggle icons and key codes
For later use in the quick actions rework as a placeholder before it gets implemented in the logic
2022-08-11 18:48:05 +02:00
Patrick Goldinger
0606afbb64 Rework ComputingEvaluator interface and eliminate RenderInfo 2022-08-09 23:33:50 +02:00
Patrick Goldinger
5362df02a5 Merge pull request #2003 from florisboard/improve-sounds-and-vibration-screen
Improve sounds and vibration screen
2022-08-09 16:46:27 +02:00
Patrick Goldinger
3d15bd7f46 Transform "Use vibrator directly" into list pref (#1919) 2022-08-06 11:13:05 +02:00
Patrick Goldinger
15e94ecf2c Merge audio/vibration enable and ignore system prefs (#1919)
The enable pref is now a list preference with a built-in switch, this means the force-on (ignore) toggle is now a list pref, which can be used if audio/vibration is enabled via the built-in switch.
2022-08-06 10:25:16 +02:00
Patrick Goldinger
ebb3873fe4 Improve vibration duration and strength error messages (#1919) 2022-08-04 23:50:26 +02:00
Patrick Goldinger
c1231cd964 Add preview for vibration duration/strength (#1173)
Issue that remains for clicks on the slider bar: https://issuetracker.google.com/issues/181415195
2022-08-04 22:40:48 +02:00
Patrick Goldinger
53ab0a3fa0 Release v0.4.0-alpha02 2022-08-03 23:14:59 +02:00
Patrick Goldinger
aeeff67d2e Adjust Settings UI home message and version number (#1942) 2022-08-03 23:12:33 +02:00
florisboard-bot
4343703eb3 Update translations from Crowdin 2022-08-03 23:00:58 +02:00
Patrick Goldinger
5f09bdbce2 Add implementation for notifySuggestionReverted() 2022-08-03 12:18:25 +02:00
Kostas Giapis
3a3e3625f2 Fix uppercase Greek vowels popups (#1981)
Co-authored-by: Patrick Goldinger <patrick@patrickgold.dev>
2022-08-02 21:47:42 +02:00
Patrick Goldinger
bdf14c1997 Add numeric row manual shifting symbols (#1988) 2022-08-01 13:09:04 +02:00
Patrick Goldinger
d6e064ae00 Fix composer not allowing multiple code points (#1984) 2022-08-01 11:54:41 +02:00
Patrick Goldinger
1012094568 Fix accent ordering of z in Polish popup mapping (#1960) 2022-07-31 09:29:46 +02:00
Patrick Goldinger
4117537ff2 Disable unnecessary app icon sync in Settings UI for Android 10+ 2022-07-29 20:18:18 +02:00
Patrick Goldinger
2a72cb70d6 Remove deprecated Accompanist Insets library 2022-07-28 22:36:05 +02:00
Patrick Goldinger
b1cd9d9389 Rework and improve splash screen of Settings UI
Especially fixes the splash screen for Android 7-11 devices, which utilize the SplashScreen compat library and where the app icon did not draw correctly.

Additionally an unnecessary intermediate splash screen background step has been removed, which should improve Settings UI cold startup time slightly and make it seem more snappy.
2022-07-28 22:25:15 +02:00
Patrick Goldinger
568dfc973d Upgrade Compose to 1.2.0 / Upgrade other dependencies 2022-07-28 16:53:26 +02:00
Patrick Goldinger
2e74cec0db Upgrade Kotlin to 1.7.10 and Compose Compiler to 1.3.0-rc01 2022-07-28 15:51:57 +02:00
Patrick Goldinger
db378159d6 Decouple Jetpack Compose Compiler version from other Compose packages
This change has been done as Google decouples the Compiler release from the rest of Compose packages to allow for faster upgrades of the Kotlin version.

Source: https://android-developers.googleblog.com/2022/06/independent-versioning-of-Jetpack-Compose-libraries.html
2022-07-28 15:37:38 +02:00
Patrick Goldinger
50ff2d8f1b Merge pull request #1974 from florisboard/networkutils-and-clipboard-fixes
Clipboard and NetworkUtils regex fixes
2022-07-27 20:47:34 +02:00
Patrick Goldinger
061495fb27 Improve host regex accuracy for clipboard URL extraction (#1971) 2022-07-27 17:01:47 +02:00
Patrick Goldinger
40cb59ddfd Fix extracted URLs not checking for duplicates (#1971) 2022-07-27 16:51:06 +02:00
Patrick Goldinger
38affddc9e Fix extracted phone numbers not stripping parentheses (#1971) 2022-07-27 16:47:24 +02:00
Patrick Goldinger
17dcb90473 Partly disable smart clipboard on Android 7.0 and 7.1 (#1970)
Android 7.0 and 7.1 do not support named regex groups natively, which causes a crash.
2022-07-27 14:44:32 +02:00
Patrick Goldinger
772402b46f Fix domains get extracted from emails in clipboard (#1971) 2022-07-27 13:58:40 +02:00
Patrick Goldinger
29bd8a289c Add clipboard phone number detection (#1889, #1971) 2022-07-27 13:42:17 +02:00
Patrick Goldinger
79d9e73608 Fix phantom spacing for 1 letter words (#1940) 2022-07-26 15:44:12 +02:00
Patrick Goldinger
b04f8d75f3 Adjust AutoTextKey behavior to respect subtype locale (#1840) 2022-07-26 15:43:19 +02:00
Kostas Giapis
32b1d123d2 Add circumflex popups to Turkish layout (#1962)
Co-authored-by: Patrick Goldinger <patrick@patrickgold.dev>
2022-07-26 15:37:45 +02:00
Patrick Goldinger
b576cafaa4 Upgrade dependencies and adapt API changes 2022-07-24 17:50:15 +02:00
Patrick Goldinger
312ef93ffc Fix auto-spacing incorrectly triggered (#1947)
Issue only occurred for non-Appender composers
2022-07-19 00:07:34 +02:00
Patrick Goldinger
a1dda0c247 Upgrade JetPref to 0.1.0-beta12 2022-07-11 01:27:29 +02:00
Patrick Goldinger
0c36b96922 Update README.md to fix inconsistency with roadmap 2022-07-07 23:56:30 +02:00
Patrick Goldinger
07e92f052b Release v0.4.0-alpha01 2022-07-06 17:56:53 +02:00
florisboard-bot
244c834de9 Update translations from Crowdin 2022-07-06 17:51:08 +02:00
Vlad
61b5c2cffd Correct layout name for RU&UA (ЙЦУКЕН) (#1681) 2022-07-06 16:39:44 +02:00
Leonardo Hernández
1441bd63cb Refactor and improve C++ codebase (#1895)
* close unused fd

dup2 doesn't close old fds, it only duplicates them.

* use `extern "C"` by block instead individual

also formatting changes for function parameters

* fix a memory leak

* cpp refactor: add utils::log() which takes log_priority

* std{out,err} logger: various improvements

- use std::thread rather than pthread
- redirect std{out,err} to stdin to avoid read() calls
- don't use global variables, for avoid spawning unneeded threads use a static function variable
- check for errors in pipe()
- use a lambda function for thread
2022-07-06 16:15:27 +02:00
Patrick Goldinger
e7d0db0fc0 Merge pull request #1913 from florisboard/sug04-prepare-UI-logic-interface
0.4/Phase 1: Prepare UI, suggestions interface and adjust logic
2022-07-06 11:48:00 +02:00
Patrick Goldinger
a87d340b25 Adjust experimental and NYI banners in Typing screen 2022-07-06 02:11:24 +02:00
Patrick Goldinger
3f0d90cb7c Add auto-spacing after punctuation (#375)
Key notes:
- It only works in rich editors
- It intentionally does NOT work in URL, EMAIL and PASSWORD text fields
- May break for exotic characters (aka everything not representable with one char in UTF-16)
- There's no hardcoded language restriction, however it is tailored towards symbols used mostly in Latin-based languages atm
- Performance checking needs to be redone for the commitChar() method
2022-07-06 01:35:02 +02:00
Patrick Goldinger
b9e9f9b122 Implement suggestion user removal 2022-07-05 02:21:57 +02:00
Patrick Goldinger
f2d1cf3baf Rework clipboard suggestions logic and allow for multiple items (#739) 2022-07-05 01:07:58 +02:00
Patrick Goldinger
cf1112327a Rework typing preference screen and integrate spelling 2022-07-04 23:29:15 +02:00
Patrick Goldinger
c2cb28668d Implement candidate auto-commit logic 2022-07-04 19:30:39 +02:00
Patrick Goldinger
75f4fcb91a Expand provider API with suggestions removal and notify events 2022-07-04 02:50:18 +02:00
Patrick Goldinger
3d92bd0584 Document EditorContent getters and companion object 2022-07-04 23:10:03 +02:00
Patrick Goldinger
52ca98a14d Improve NlpManager and provider API 2022-07-02 20:40:37 +02:00
Patrick Goldinger
629a73a5cf Document and improve SpellingResult 2022-07-01 20:02:10 +02:00
Patrick Goldinger
077ec43855 Add Liberapay option to FUNDING.yml (#1434) 2022-07-01 17:18:17 +02:00
Patrick Goldinger
3ecd3618cb Add baseline for keyboard and provider logic bridge 2022-07-01 01:27:41 +02:00
Patrick Goldinger
38bc34913b Rework and improve internal APK assets file handling 2022-06-30 00:08:03 +02:00
Patrick Goldinger
c733e5ceea Extend Android asset manager API to simplify usage 2022-06-29 22:24:05 +02:00
Patrick Goldinger
e2536ceb92 Remove duplicate NATIVE_NULLPTR 2022-06-28 23:10:13 +02:00
Patrick Goldinger
c17b6f073d Switch from LiveData to StateFlow in some manager classes 2022-06-28 22:10:22 +02:00
Patrick Goldinger
936b177776 Rework spell checker config and add utility script 2022-06-27 22:42:59 +02:00
Patrick Goldinger
7d8036fe69 Remove Nuspell spell check implementation (#1921) 2022-06-27 18:49:59 +02:00
Patrick Goldinger
6d08d1a265 Add sentence break iterator caching 2022-06-27 16:25:57 +02:00
Patrick Goldinger
48aba1c055 Add skeleton for new NLP provider API 2022-06-26 23:49:42 +02:00
Patrick Goldinger
0b3d3317bf Add secondary text UI implementation for candidates 2022-06-25 16:42:59 +02:00
Patrick Goldinger
d1fbdc581b Update roadmap's milestone 0.4 phase 1 2022-06-24 18:46:08 +02:00
Patrick Goldinger
044170eb4b Fix auto-capitalization issues with invalid initial state (#1915) 2022-06-24 03:25:28 +02:00
Patrick Goldinger
a7c16b3ceb Improve state reset mechanism for restarts (#1916) 2022-06-24 01:18:21 +02:00
Patrick Goldinger
dd12be2275 Rework and document candidate item API 2022-06-24 00:39:44 +02:00
Patrick Goldinger
1049bc543a Move package smartbar from ime.text to ime 2022-06-23 19:50:22 +02:00
Patrick Goldinger
5c5ad3cd32 Remove unused TextProcessor class 2022-06-21 14:48:35 +02:00
Patrick Goldinger
6fce521122 Fix candidate completion logic not behaving as expected 2022-06-20 23:02:39 +02:00
Patrick Goldinger
2af9941ea6 Add Costa Rican colón currency set (#1914) 2022-06-20 22:00:47 +02:00
Patrick Goldinger
9b24f742d1 Disable auto-capitalization for Thai language (#1908) 2022-06-20 00:05:44 +02:00
Patrick Goldinger
b36bcf7733 Tie composing region indicator to suggestion enabled state (#1911) 2022-06-19 23:46:14 +02:00
Patrick Goldinger
9559dbdcd6 Update roadmap for 0.4 milestone 2022-06-19 21:53:42 +02:00
Patrick Goldinger
668dd4b5bf Fix changelog for 0.3.16 accidentally stored in beta metafolder 2022-06-13 10:57:29 +02:00
230 changed files with 8364 additions and 15241 deletions

1
.github/FUNDING.yml vendored
View File

@@ -1,2 +1,3 @@
github: [patrickgold]
liberapay: patrickgold
custom: ["https://paypal.me/devpatrickgold"]

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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"/>

View File

@@ -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 ($)",

View File

@@ -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"
},

View File

@@ -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"}
]
]

View File

@@ -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": "ⁿ" }
]
}
}
}
]
]

View File

@@ -17,8 +17,11 @@
{
"id": "default",
"label": "Default",
"symbolsPrecedingSpace": ".*[.,;:!?‽&%)\\]}»©®™\\p{L}0-9]",
"symbolsFollowingSpace": "[\\p{L}0-9].*"
"symbolsPrecedingAutoSpace": ".,?‽!\"&%)]}»",
"symbolsFollowingAutoSpace": "",
"symbolsPrecedingPhantomSpace": ".,;:?‽!&%)]}»©®™",
"symbolsFollowingPhantomSpace": "¿⸘¡([{",
"symbolsTerminatingSentence": ".?‽!"
}
],
"popupMappings": [

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": "," },

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"
]
}

View File

@@ -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
)

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -1,90 +0,0 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#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;
}

View File

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

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,10 +0,0 @@
add_library(
# Name
ime-spelling
# Headers
spellingdict.h
# Sources
spellingdict.cpp
)

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1 +0,0 @@
clang-format -style=file -i *.[ch]xx

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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

View File

@@ -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(

View File

@@ -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

View File

@@ -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()
}
}
}

View File

@@ -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)
}

View File

@@ -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 ->

View File

@@ -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()

View File

@@ -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}] { \"")

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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),

View File

@@ -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)) {

View File

@@ -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,

View File

@@ -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)

View File

@@ -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,

View File

@@ -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),

View File

@@ -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,
)
}

View File

@@ -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 },
)
}
}

View File

@@ -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")
}
}
}
}

View File

@@ -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),
)
}
}
}

View File

@@ -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),
)
}
}

View File

@@ -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 },
)
}
}
}

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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,
)
}
}
}

View File

@@ -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
)
}
}
}

View File

@@ -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",
)
}

View File

@@ -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

View File

@@ -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)
}
/**

View File

@@ -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,

View File

@@ -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
}
}

View File

@@ -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)
}

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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,
)
}

View File

@@ -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.
*/

View File

@@ -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),
)
}
}
}

View File

@@ -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,
)
}
}
}

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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