Compare commits

...

99 Commits

Author SHA1 Message Date
Patrick Goldinger
b2ec115505 Release v0.3.10-beta04 2021-03-29 14:46:14 +02:00
Patrick Goldinger
670e6ca5e1 Fix emoji ABC button not leading back to characters (#521) 2021-03-29 02:55:23 +02:00
Patrick Goldinger
f2403d00e5 Add long-press caps-lock activation vibration (#523) 2021-03-29 02:46:46 +02:00
Patrick Goldinger
224d3e00e3 Merge pull request #518 from florisboard/fix-popup-width-landscape
Adjust popups in landscape mode
2021-03-28 19:47:40 +02:00
Patrick Goldinger
e89a374ce0 Adjust popup width in landscape (#504) 2021-03-28 19:35:43 +02:00
Patrick Goldinger
538e2dd9a2 Merge pull request #514 from florisboard/suggestions-phase2-frontend
Suggestions frontend rework
2021-03-28 19:15:37 +02:00
Patrick Goldinger
1d3d85c211 Fix crash for image clipboard suggestions 2021-03-28 19:08:49 +02:00
Patrick Goldinger
d6121baca9 Polish and document candidate view 2021-03-28 18:16:59 +02:00
Patrick Goldinger
8c0337d6c9 Fix suggestions not resetting when switching apps (#429) 2021-03-27 19:53:51 +01:00
Patrick Goldinger
563a4a919d Add new candidate+clipboard suggestion view (#38, #424, #425, #426) 2021-03-27 19:45:53 +01:00
Patrick Goldinger
2f0d607d02 Potential fix for #484 2021-03-24 20:20:01 +01:00
Patrick Goldinger
65ae6c2b66 Merge pull request #491 from X-yl/clipboard-stuff
Fix for #481
2021-03-24 19:21:33 +01:00
x-yl
14513ec0f1 kotlin-ify 2021-03-24 18:07:07 +04:00
x-yl
3c58144a3d fix #481 2021-03-24 17:52:12 +04:00
Patrick Goldinger
d65b706f78 Release v0.3.10-beta03 2021-03-23 20:00:49 +01:00
Patrick Goldinger
9d820677db Update translations from Crowdin 2021-03-23 19:50:06 +01:00
Patrick Goldinger
69c52c00f6 Fix Ž key not available in Dvorak/Serbian (#381) 2021-03-23 19:43:41 +01:00
Patrick Goldinger
c8cf256577 Merge pull request #488 from florisboard/turkish-layouts
Add Turkish-Q / Turkish-F layouts
2021-03-23 14:35:11 +01:00
Patrick Goldinger
386a0999c4 Add Turkish-Q / Turkish-F layouts (#182) 2021-03-23 14:07:42 +01:00
Patrick Goldinger
d4ef2ea827 Merge pull request #486 from Netscaping/patch-1
Create gboard_night.json
2021-03-22 15:28:11 +01:00
Patrick Goldinger
381ec68e6c Merge pull request #482 from florisboard/rework-symbols-sizing
Rework symbols sizing when number row is enabled
2021-03-22 15:18:40 +01:00
Netscaping
a5706167b2 Create gboard_night.json
Since there is a Gboard Day theme I added the night version.
2021-03-22 15:17:46 +01:00
Patrick Goldinger
660871d6c8 Rework symbols sizing when number row is enabled 2021-03-22 00:55:47 +01:00
Patrick Goldinger
6607ad1739 Fix language selector size for keyboard height greater than 125% 2021-03-22 00:01:56 +01:00
Patrick Goldinger
55c1bc05f2 Add auto-switching to characters in symbols (#347) 2021-03-21 19:12:10 +01:00
Patrick Goldinger
7eb7f0ef80 Release v0.3.10-beta02 2021-03-19 19:42:08 +01:00
Patrick Goldinger
78e5e417ce Update README.md to include new beta track 2021-03-19 18:49:40 +01:00
Patrick Goldinger
ffbf7f8ea7 Merge pull request #454 from X-yl/clipboard-stuff
Added support for private clipboard and clipboard history
2021-03-19 17:49:16 +01:00
Patrick Goldinger
27cc4897c3 Merge pull request #479 from florisboard/fix-import-theme-crash
Fix import theme crash for big files
2021-03-19 17:18:49 +01:00
Patrick Goldinger
e5111a8efe Fix import theme crash for big files (#465) 2021-03-19 17:04:48 +01:00
Patrick Goldinger
80fd5ca84a Add beta metadata 2021-03-19 00:57:11 +01:00
x-yl
e8f2c6ce74 fix bug when history size is reduced 2021-03-18 23:21:50 +04:00
x-yl
5676cbf18e Stupid telegram, not using ContentResolver... smh 2021-03-18 17:50:48 +04:00
x-yl
2bdaea6189 revoke URI permissions, support API <25 2021-03-18 17:10:28 +04:00
Patrick Goldinger
da2287a739 Fix symbols layouts applying the caps state once again (#298) 2021-03-17 23:29:58 +01:00
Patrick Goldinger
3fafe0fac8 Release v0.3.10-beta01 2021-03-17 14:48:31 +01:00
x-yl
86042bb1e1 make popup buttons extend to the edge of popup 2021-03-17 15:30:22 +04:00
x-yl
c99673ff1d mime type fixes, remove from history after pressing delete 2021-03-17 15:20:12 +04:00
x-yl
8b89b27fb0 Misc. fixes 2021-03-17 10:57:56 +04:00
x-yl
b56c976fa0 code cleanup 2021-03-17 10:26:22 +04:00
x-yl
08889fdc60 docs 2021-03-17 10:20:21 +04:00
x-yl
e8d657e81c free storage after images leave clipboard 2021-03-17 09:38:03 +04:00
Patrick Goldinger
e581d6cbc4 Release v0.3.9 2021-03-16 20:15:45 +01:00
Patrick Goldinger
ec13d008fb Fix Greek uppercase bug (#452) 2021-03-16 20:03:51 +01:00
Patrick Goldinger
edfea1afcb Merge pull request #461 from florisboard/metadata-refresh
App Store presence metadata update
2021-03-16 16:45:10 +01:00
Patrick Goldinger
25fc23d721 Update store presence metadata to represent all implemented features 2021-03-16 16:34:54 +01:00
x-yl
bfcea8b718 Make pins persistent 2021-03-16 18:58:14 +04:00
Patrick Goldinger
c701141be2 Remove Italian store metadata 2021-03-16 15:15:04 +01:00
x-yl
7f07686b6c added proper mime type support to content provider 2021-03-16 16:38:01 +04:00
Patrick Goldinger
e5b956857e Merge pull request #459 from florisboard/beta-track-prep
Beta track preperation / App icon revamp
2021-03-16 11:53:12 +01:00
Patrick Goldinger
67236ef58d Add beta build variant 2021-03-16 03:17:34 +01:00
Patrick Goldinger
2da17a0654 Add new app icons for all build variants 2021-03-16 03:16:56 +01:00
Patrick Goldinger
1f3221a886 Merge pull request #457 from florisboard/one-handed-improvements
Add one-handed width option / Improve one-handed code
2021-03-15 20:13:02 +01:00
Patrick Goldinger
47f80d00c4 Add one-handed width option / Improve one-handed code 2021-03-15 17:49:18 +01:00
x-yl
e4ecc63b9d Added an abstraction around ClipData 2021-03-15 15:12:30 +04:00
Patrick Goldinger
d648c480b5 Merge pull request #455 from florisboard/theme-import-export
Add theme import/export / Fix theme editor jumping to top
2021-03-15 09:33:18 +01:00
Patrick Goldinger
9e26720674 Fix export UI not requesting to create document 2021-03-15 01:51:11 +01:00
Patrick Goldinger
a20c6bf148 Fix theme editor jumping to top (#379) 2021-03-15 00:57:35 +01:00
Patrick Goldinger
d2df5cfcdf Switch to Kotlin Result 2021-03-15 00:08:10 +01:00
Patrick Goldinger
93b5503dfc Fix file write bug and improve UI 2021-03-14 23:39:57 +01:00
Patrick Goldinger
4d4b54074a Improve import/export feature stability 2021-03-14 19:44:13 +01:00
Patrick Goldinger
904fd9b85a Add simple theme import/export functionality 2021-03-14 02:15:47 +01:00
x-yl
aacb33bd5d fixed issue when floris clipboard is disabled 2021-03-13 20:46:27 +04:00
x-yl
a0aa446988 Change back button 2021-03-13 18:06:10 +04:00
x-yl
fe086ed6d8 removed some debug logging 2021-03-13 17:39:11 +04:00
x-yl
64ddd0f421 fixed a stupid bug somehow 2021-03-13 17:35:35 +04:00
x-yl
40fe72e33c fix a few bugs 2021-03-13 14:55:58 +04:00
x-yl
b229970ec3 cleanup and documentation 2021-03-13 13:04:34 +04:00
x-yl
ec32c211f1 added delete and paste. pretty much feature complete now. 2021-03-12 23:39:23 +04:00
x-yl
e66b8a052a Pin/unpin support 2021-03-12 22:18:40 +04:00
x-yl
4a22c2698c added more ways to open clipboard context, fixed popups, refactored some code 2021-03-12 21:50:24 +04:00
x-yl
ae95bbd7c4 Added a mock popup 2021-03-11 18:03:08 +04:00
x-yl
0bdeeaa340 VERY work in progress 2021-03-11 10:24:40 +04:00
x-yl
92a885a34c Little bit of preference stuff 2021-03-11 10:24:35 +04:00
x-yl
bc2f03a920 light refactoring, some theme stuff 2021-03-11 10:24:26 +04:00
x-yl
f60827b634 small theme fix 2021-03-11 10:23:59 +04:00
x-yl
dcf81b27a0 Fixed animations, added image support, some documentation 2021-03-11 10:23:59 +04:00
x-yl
0d8601cb15 Text-only clipboard history implemented 2021-03-11 10:23:59 +04:00
x-yl
ecf3c6bf27 All clipboard actions now use FlorisClipboardManager. Added support for commiting non-text content. Added simple clipboard history layout. 2021-03-11 10:23:46 +04:00
Patrick Goldinger
e4f5fcf74b Merge pull request #451 from florisboard/alternate-shift-code
Add option for an alternate key code when caps state is active
2021-03-10 23:15:19 +01:00
Patrick Goldinger
15f0316839 Add shift variants for Colemak and Dvorak (#145) 2021-03-10 19:38:11 +01:00
Patrick Goldinger
93654c4f88 Add alternate key code option for FlorisKeyData (#145) 2021-03-10 19:37:50 +01:00
Patrick Goldinger
62fc549ea9 Fix crash on setup when no other IME is installed (#423) 2021-03-10 18:37:27 +01:00
Patrick Goldinger
d0dbd1cd4e Merge pull request #444 from florisboard/input-logic-rework
Input logic rework
2021-03-10 16:08:10 +01:00
Patrick Goldinger
af28f84b69 Fix delete precise char selection init value always 2 units (#448) 2021-03-10 12:09:18 +01:00
Patrick Goldinger
db7ee52029 Fix label text size decreasing bug in selection keyboard 2021-03-10 11:59:51 +01:00
Patrick Goldinger
7343617792 Fix space bar arrow movement initial count always 2 (#448) 2021-03-10 11:31:23 +01:00
Patrick Goldinger
5898d7006b Add internal batch edit level to prevent stuttering UI 2021-03-09 20:17:30 +01:00
Patrick Goldinger
058be7a169 Fix editor instance commit text logic 2021-03-09 02:03:17 +01:00
Patrick Goldinger
e6f2a25021 Improve input event logic / Fix extended popup bug 2021-03-08 19:51:37 +01:00
Patrick Goldinger
3a485a1574 Fix bugs and improve code 2021-03-08 01:09:43 +01:00
Patrick Goldinger
0ee0f24119 Add shift slide behavior / Improve performance of input logic 2021-03-07 19:35:08 +01:00
Patrick Goldinger
004e999259 Document InputEventDispatcher 2021-03-07 16:26:45 +01:00
Patrick Goldinger
11775c4619 Separate input event dispatcher logic into another file 2021-03-07 15:27:20 +01:00
Patrick Goldinger
177bad95b3 Clean up static KeyData object definitions 2021-03-06 19:33:53 +01:00
Patrick Goldinger
610526d845 Add multi-pointer support for gestures 2021-03-06 14:19:30 +01:00
Patrick Goldinger
55e489bc07 Complete overhaul of core input logic 2021-03-05 20:13:35 +01:00
Patrick Goldinger
589063be61 Rework cursor/selection implementation 2021-03-03 23:43:27 +01:00
Patrick Goldinger
aa73ac706a Update target SDK to API 30 (Android 11) 2021-03-01 20:13:59 +01:00
232 changed files with 5879 additions and 1441 deletions

View File

@@ -11,3 +11,7 @@ trim_trailing_whitespace = true
[{*.har,*.json}]
indent_size = 2
[*.kt]
ij_kotlin_name_count_to_use_star_import = 99
ij_kotlin_name_count_to_use_star_import_for_members = 99

View File

@@ -1,14 +1,15 @@
<img align="left" width="80" height="80"
src="fastlane/metadata/android/en-US/images/icon.png" alt="App icon">
# FlorisBoard [![Release](https://img.shields.io/github/v/release/florisboard/florisboard)](https://github.com/florisboard/florisboard/releases) [![Crowdin](https://badges.crowdin.net/florisboard/localized.svg)](https://crowdin.florisboard.patrickgold.dev) ![FlorisBoard CI](https://github.com/florisboard/florisboard/workflows/FlorisBoard%20CI/badge.svg?event=push)
# FlorisBoard [![Crowdin](https://badges.crowdin.net/florisboard/localized.svg)](https://crowdin.florisboard.patrickgold.dev) ![FlorisBoard CI](https://github.com/florisboard/florisboard/workflows/FlorisBoard%20CI/badge.svg?event=push)
**FlorisBoard** is a free and open-source keyboard for Android 6.0+
devices. It aims at being modern, user-friendly and customizable while
fully respecting your privacy. Currently in alpha/early-beta state.
fully respecting your privacy. Currently in early-beta state.
## Public Alpha Test Programme
Wanna try it out on your device? Use one of the following options:
### Stable [![Latest stable release](https://img.shields.io/github/v/release/florisboard/florisboard)](https://github.com/florisboard/florisboard/releases/latest)
Releases on this track are in general stable and ready for everyday use, except for features marked as experimental. Use one of the following options to receive FlorisBoard's stable releases:
_A. Get it on F-Droid_:
@@ -36,6 +37,16 @@ for and download FlorisBoard without prior joining the alpha group.
_C. Use the APK provided in the release section of this repo_
### Beta [![Latest beta release](https://img.shields.io/github/v/release/florisboard/florisboard?include_prereleases)](https://github.com/florisboard/florisboard/releases)
Releases on this track are also in general stable and should be ready for everyday use, though crashes and bugs are more likely to occur. Use releases from this track if you want to get new features faster and give feedback for brand-new stuff. Options to get beta releases:
_A. IzzySoft's repo for F-Droid_:
[<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" height="64" alt="IzzySoft repo badge">](https://apt.izzysoft.de/fdroid/index/apk/dev.patrickgold.florisboard.beta)
_B. Use the APK provided in the release section of this repo_
### Giving feedback
If you want to give feedback to FlorisBoard, there are several ways to
do so, as listed [here](CONTRIBUTING.md#giving-general-feedback).
@@ -62,11 +73,9 @@ milestones, please refer to the [Feature roadmap](#feature-roadmap).
* [x] Landscape orientation support (needs tweaks)
### Layouts
* [x] Latin character layouts (QWERTY, QWERTZ, AZERTY, Swiss, Spanish,
Norwegian, Swedish/Finnish, Icelandic, Danish, Hungarian,
Croatian, Polish, Romanian, Colemak, Dvorak); more coming in future versions
* [x] Non-latin character layouts (Arabic, Persian, Greek, Russian
(JCUKEN))
* [x] Latin character layouts (QWERTY, QWERTZ, AZERTY, Swiss, Spanish, Norwegian, Swedish/Finnish, Icelandic, Danish,
Hungarian, Croatian, Polish, Romanian, Colemak, Dvorak, Turkish-Q, Turkish-F, ...)
* [x] Non-latin character layouts (Arabic, Persian, Greek, Russian (JCUKEN))
* [x] Adapt to situation in app (password, url, text, etc. )
* [x] Special character layout(s)
* [x] Numeric layout
@@ -88,6 +97,7 @@ milestones, please refer to the [Feature roadmap](#feature-roadmap).
### Other useful features
* [x] One-handed mode
* [x] Clipboard/cursor tools
* [x] Clipboard manager/history
* [x] Integrated number row / symbols in character layouts
* [x] Gesture support
* [x] Full integration in IME service list of Android (xml/method)
@@ -138,6 +148,7 @@ close as possible.
and `Follow time`)
- Define a separate theme both for day and night theme
- Adapt to app theme if possible
- Theme import/export
### [v0.5.0](https://github.com/florisboard/florisboard/milestone/5)
There's no exact roadmap yet but it is planned that the media part of
@@ -150,7 +161,6 @@ passes...
Backlog (currently not assigned to any milestone):
- Theme import/export
- Floating keyboard
[#91]: https://github.com/florisboard/florisboard/pull/91
@@ -179,8 +189,6 @@ to get more information on this topic.
[Jared Rummler](https://github.com/jaredrummler)
* [Timber](https://github.com/JakeWharton/timber) by
[JakeWharton](https://github.com/JakeWharton)
* [kotlin-result](https://github.com/michaelbull/kotlin-result) by
[Michael Bull](https://github.com/michaelbull)
* [expandable-fab](https://github.com/nambicompany/expandable-fab) by
[Nambi](https://github.com/nambicompany)

View File

@@ -1,11 +1,13 @@
plugins {
id("com.android.application") version "4.1.2"
kotlin("android") version "1.4.30"
kotlin("kapt") version "1.4.30"
}
android {
compileSdkVersion(29)
buildToolsVersion("29.0.2")
compileSdkVersion(30)
buildToolsVersion("30.0.3")
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
@@ -20,9 +22,9 @@ android {
defaultConfig {
applicationId = "dev.patrickgold.florisboard"
minSdkVersion(23)
targetSdkVersion(29)
versionCode(27)
versionName("0.3.8")
targetSdkVersion(30)
versionCode(32)
versionName("0.3.10")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -34,11 +36,29 @@ android {
buildTypes {
named("debug").configure {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
resValue("mipmap", "floris_app_icon", "@mipmap/ic_app_icon_debug")
resValue("mipmap", "floris_app_icon_round", "@mipmap/ic_app_icon_debug_round")
resValue("string", "floris_app_name", "FlorisBoard Debug")
}
create("beta") // Needed because by default the "beta" BuildType does not exist
named("beta").configure {
applicationIdSuffix = ".beta"
versionNameSuffix = "-beta04"
proguardFiles.add(getDefaultProguardFile("proguard-android-optimize.txt"))
resValue("mipmap", "floris_app_icon", "@mipmap/ic_app_icon_beta")
resValue("mipmap", "floris_app_icon_round", "@mipmap/ic_app_icon_beta_round")
resValue("string", "floris_app_name", "FlorisBoard Beta")
}
named("release").configure {
proguardFiles.add(getDefaultProguardFile("proguard-android-optimize.txt"))
resValue("mipmap", "floris_app_icon", "@mipmap/ic_app_icon_release")
resValue("mipmap", "floris_app_icon_round", "@mipmap/ic_app_icon_release_round")
resValue("string", "floris_app_name", "@string/app_name")
}
}
@@ -54,11 +74,15 @@ android {
}
}
dependencies {
implementation("androidx.activity", "activity-ktx", "1.2.1")
implementation("androidx.appcompat", "appcompat", "1.2.0")
implementation("androidx.core", "core-ktx", "1.3.2")
implementation("androidx.fragment", "fragment-ktx", "1.3.0")
implementation("androidx.preference", "preference-ktx", "1.1.1")
implementation("androidx.constraintlayout", "constraintlayout", "2.0.4")
implementation("androidx.lifecycle", "lifecycle-service", "2.2.0")
implementation("com.google.android", "flexbox", "2.0.1") // requires jcenter as of version 2.0.1
implementation("com.squareup.moshi", "moshi-kotlin", "1.11.0")
implementation("com.squareup.moshi", "moshi-adapters", "1.11.0")
@@ -66,8 +90,9 @@ dependencies {
implementation("org.jetbrains.kotlinx", "kotlinx-coroutines-android", "1.4.2")
implementation("com.jaredrummler", "colorpicker", "1.1.0")
implementation("com.jakewharton.timber", "timber", "4.7.1")
implementation("com.michael-bull.kotlin-result", "kotlin-result", "1.1.10")
implementation("com.nambimobile.widgets", "expandable-fab", "1.0.2")
implementation("androidx.room", "room-runtime", "2.2.6")
kapt("androidx.room", "room-compiler","2.2.6")
testImplementation("junit", "junit", "4.13.1")
testImplementation("org.mockito", "mockito-inline", "3.7.7")

View File

@@ -23,9 +23,9 @@
<application
android:name=".ime.core.FlorisApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:icon="@mipmap/floris_app_icon"
android:label="@string/floris_app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:roundIcon="@mipmap/floris_app_icon_round"
android:supportsRtl="true"
android:theme="@style/SettingsTheme">
@@ -46,19 +46,19 @@
<!-- Settings Activity -->
<activity
android:name="dev.patrickgold.florisboard.settings.SettingsMainActivity"
android:icon="@mipmap/ic_launcher"
android:icon="@mipmap/floris_app_icon"
android:label="@string/settings__title"
android:launchMode="singleTask"
android:roundIcon="@mipmap/ic_launcher_round"
android:roundIcon="@mipmap/floris_app_icon_round"
android:theme="@style/SettingsTheme"/>
<!-- Using an activity alias to disable/enable the app icon in the launcher -->
<activity-alias
android:name="dev.patrickgold.florisboard.SettingsLauncherAlias"
android:icon="@mipmap/ic_launcher"
android:icon="@mipmap/floris_app_icon"
android:label="@string/floris_app_name"
android:launchMode="singleTask"
android:roundIcon="@mipmap/ic_launcher_round"
android:roundIcon="@mipmap/floris_app_icon_round"
android:targetActivity="dev.patrickgold.florisboard.setup.SetupActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@@ -69,48 +69,56 @@
<!-- Theme Selector Activity -->
<activity
android:name="dev.patrickgold.florisboard.settings.ThemeManagerActivity"
android:icon="@mipmap/ic_launcher"
android:icon="@mipmap/floris_app_icon"
android:label="@string/settings__title"
android:theme="@style/SettingsTheme"/>
<!-- Theme Editor Activity -->
<activity
android:name="dev.patrickgold.florisboard.settings.ThemeEditorActivity"
android:icon="@mipmap/ic_launcher"
android:icon="@mipmap/floris_app_icon"
android:label="@string/settings__theme_editor__title"
android:theme="@style/SettingsTheme"/>
<!-- About Activity -->
<activity
android:name="dev.patrickgold.florisboard.settings.AboutActivity"
android:icon="@mipmap/ic_launcher"
android:icon="@mipmap/floris_app_icon"
android:label="@string/about__title"
android:roundIcon="@mipmap/ic_launcher_round"
android:roundIcon="@mipmap/floris_app_icon_round"
android:theme="@style/SettingsTheme"/>
<!-- Advanced Activity -->
<activity
android:name="dev.patrickgold.florisboard.settings.AdvancedActivity"
android:icon="@mipmap/ic_launcher"
android:icon="@mipmap/floris_app_icon"
android:label="@string/settings__advanced__title"
android:roundIcon="@mipmap/ic_launcher_round"
android:roundIcon="@mipmap/floris_app_icon_round"
android:theme="@style/SettingsTheme"/>
<!-- Setup Activity -->
<activity
android:name="dev.patrickgold.florisboard.setup.SetupActivity"
android:icon="@mipmap/ic_launcher"
android:icon="@mipmap/floris_app_icon"
android:label="@string/setup__title"
android:roundIcon="@mipmap/ic_launcher_round"
android:roundIcon="@mipmap/floris_app_icon_round"
android:theme="@style/SettingsTheme"/>
<!-- Crash Dialog Activity -->
<activity
android:name="dev.patrickgold.florisboard.crashutility.CrashDialogActivity"
android:icon="@mipmap/ic_launcher"
android:icon="@mipmap/floris_app_icon"
android:label="@string/crash_dialog__title"
android:theme="@style/CrashDialogTheme"/>
<provider
android:name="dev.patrickgold.florisboard.ime.clip.provider.FlorisContentProvider"
android:authorities="dev.patrickgold.florisboard.provider.clip"
android:grantUriPermissions="true"
android:exported="false">
</provider>
</application>
</manifest>

View File

@@ -4,31 +4,33 @@
"qwerty": "QWERTY",
"qwertz": "QWERTZ",
"azerty": "AZERTY",
"arabic": "Arabic",
"bepo": "BÉPO",
"bulgarian_bds": "Bulgarian (BDS)",
"bulgarian_phonetic": "Bulgarian (Phonetic)",
"spanish": "Spanish (QWERTY)",
"norwegian": "Norwegian (QWERTY)",
"swedish_finnish": "Swedish/Finnish (QWERTY)",
"canadian_french": "Canadian French (QWERTY)",
"colemak": "Colemak",
"danish": "Danish (QWERTY)",
"dvorak": "Dvorak",
"esperanto": "Esperanto",
"esperanto_with_hx": "Esperanto with 'ĥ'",
"greek": "Ελληνικά",
"hebrew": "עברית",
"hungarian": "Hungarian (QWERTZ)",
"icelandic": "Icelandic (QWERTY)",
"kurdish": "کوردی",
"norwegian": "Norwegian (QWERTY)",
"persian": "Persian",
"jcuken_russian": "Russian (JCUKEN)",
"serbian_latin": "Serbian (QWERTZ)",
"serbian_cyrillic": "Serbian (ЉЊЕРТЗ)",
"spanish": "Spanish (QWERTY)",
"swedish_finnish": "Swedish/Finnish (QWERTY)",
"swiss_german": "Swiss German (QWERTZ)",
"swiss_french": "Swiss French (QWERTZ)",
"swiss_italian": "Swiss Italian (QWERTZ)",
"hungarian": "Hungarian (QWERTZ)",
"persian": "Persian",
"arabic": "Arabic",
"esperanto": "Esperanto",
"esperanto_with_hx": "Esperanto with 'ĥ'",
"colemak": "Colemak",
"dvorak": "Dvorak",
"jcuken_russian": "Russian (JCUKEN)",
"canadian_french": "Canadian French (QWERTY)",
"greek": "Ελληνικά",
"hebrew": "עברית",
"serbian_latin": "Serbian (QWERTZ)",
"serbian_cyrillic": "Serbian (ЉЊЕРТЗ)",
"kurdish": "کوردی"
"turkish_q": "Turkish-Q",
"turkish_f": "Turkish-F"
},
"defaultSubtypes": [
{

View File

@@ -18,7 +18,7 @@
"relevant": [
{ "code": 58, "label": ":" }
]
} }
}, "shift": { "code": 58, "label": ":" } }
],
[
{ "code": 97, "label": "a" },

View File

@@ -12,25 +12,25 @@
{ "code": 33, "label": "!" },
{ "code": 34, "label": "\"" }
]
} },
}, "shift": { "code": 34, "label": "\"" } },
{ "code": 39, "label": "'", "groupId": 101, "variation": "password", "popup": {
"relevant": [
{ "code": 33, "label": "!" },
{ "code": 34, "label": "\"" }
]
} },
}, "shift": { "code": 34, "label": "\"" } },
{ "code": 47, "label": "/", "groupId": 101, "variation": "uri" },
{ "code": 44, "label": ",", "popup": {
"relevant": [
{ "code": 60, "label": "<" },
{ "code": 63, "label": "?" }
]
} },
}, "shift": { "code": 60, "label": "<" } },
{ "code": 46, "label": ".", "popup": {
"relevant": [
{ "code": 62, "label": ">" }
]
} },
}, "shift": { "code": 62, "label": ">" } },
{ "code": 112, "label": "p" },
{ "code": 121, "label": "y" },
{ "code": 102, "label": "f" },

View File

@@ -7,7 +7,8 @@
"~enter": {
"main": { "code": -213, "label": "switch_to_media_context", "type": "system_gui" },
"relevant": [
{ "code": -216, "label": "toggle_one_handed_mode_right", "type": "system_gui" }
{ "code": -216, "label": "toggle_one_handed_mode_right", "type": "system_gui" },
{ "code": -214, "label": "switch_to_clipboard_context", "type": "system_gui"}
]
},
"~left": {

View File

@@ -21,9 +21,7 @@
]
},
"z": {
"relevant": [
{ "code": 382, "label": "ž" }
]
"main": { "code": 382, "label": "ž" }
},
"~right": {
"main": { "code": 44, "label": "," },
@@ -50,9 +48,9 @@
"~right": {
"main": { "code": -255, "label": ".com" },
"relevant": [
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".eu" },
{ "code": -255, "label": ".rs" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".net" }
]
}

View File

@@ -1,46 +1,100 @@
{
"type": "characters/extended_popups",
"name": "tr",
"authors": [ "kisekinopureya" ],
"authors": [ "kisekinopureya", "patrickgold" ],
"mapping": {
"all": {
"a": {
"relevant": [
{ "code": 226, "label": "â" }
{ "code": 226, "label": "â" },
{ "code": 228, "label": "ä" },
{ "code": 225, "label": "á" }
]
},
"c": {
"main": { "code": 231, "label": "ç" },
"relevant": [
{ "code": 231, "label": "ç" }
{ "code": 269, "label": "č" },
{ "code": 263, "label": "ć" }
]
},
"e": {
"relevant": [
{ "code": 233, "label": "é" },
{ "code": 601, "label": "ə" },
{ "code": 234, "label": "ê" }
]
},
"g": {
"relevant": [
{ "code": 287, "label": "ğ" }
]
"main": { "code": 287, "label": "ğ" }
},
"i": {
"main": { "code": 305, "label": "ı" },
"relevant": [
{ "code": 303, "label": "į" },
{ "code": 236, "label": "ì" },
{ "code": 237, "label": "í" },
{ "code": 299, "label": "ī" },
{ "code": 238, "label": "î" },
{ "code": 305, "label": "ı" }
{ "code": 239, "label": "ï" }
]
},
"ı": {
"main": { "code": 105, "label": "i" },
"relevant": [
{ "code": 303, "label": "į" },
{ "code": 236, "label": "ì" },
{ "code": 237, "label": "í" },
{ "code": 299, "label": "ī" },
{ "code": 238, "label": "î" },
{ "code": 239, "label": "ï" }
]
},
"n": {
"relevant": [
{ "code": 328, "label": "ň" },
{ "code": 241, "label": "ñ" }
]
},
"o": {
"main": { "code": 246, "label": "ö" },
"relevant": [
{ "code": 246, "label": "ö" }
{ "code": 333, "label": "ō" },
{ "code": 248, "label": "ø" },
{ "code": 243, "label": "ó" },
{ "code": 245, "label": "õ" },
{ "code": 242, "label": "ò" },
{ "code": 339, "label": "œ" },
{ "code": 244, "label": "ô" }
]
},
"s": {
"main": { "code": 351, "label": "ş" },
"relevant": [
{ "code": 351, "label": "ş" }
{ "code": 347, "label": "ś" },
{ "code": 223, "label": "ß" },
{ "code": 353, "label": "š" }
]
},
"u": {
"main": { "code": 252, "label": "ü" },
"relevant": [
{ "code": 252, "label": "ü" },
{ "code": 363, "label": "ū" },
{ "code": 249, "label": "ù" },
{ "code": 250, "label": "ú" },
{ "code": 251, "label": "û" }
]
},
"y": {
"relevant": [
{ "code": 253, "label": "ý" }
]
},
"z": {
"relevant": [
{ "code": 382, "label": "ž" }
]
},
"~right": {
"main": { "code": 44, "label": "," },
"relevant": [
@@ -67,9 +121,9 @@
"main": { "code": -255, "label": ".com" },
"relevant": [
{ "code": -255, "label": ".gov" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".edu" },
{ "code": -255, "label": ".tr" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".net" }
]
}

View File

@@ -0,0 +1,46 @@
{
"type": "characters",
"name": "turkish_f",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 102, "label": "f" },
{ "code": 103, "label": "g" },
{ "code": 287, "label": "ğ" },
{ "code": 305, "label": "ı" },
{ "code": 111, "label": "o" },
{ "code": 100, "label": "d" },
{ "code": 114, "label": "r" },
{ "code": 110, "label": "n" },
{ "code": 104, "label": "h" },
{ "code": 112, "label": "p" },
{ "code": 113, "label": "q" },
{ "code": 119, "label": "w" }
],
[
{ "code": 117, "label": "u" },
{ "code": 105, "label": "i" },
{ "code": 101, "label": "e" },
{ "code": 97, "label": "a" },
{ "code": 252, "label": "ü" },
{ "code": 116, "label": "t" },
{ "code": 107, "label": "k" },
{ "code": 109, "label": "m" },
{ "code": 108, "label": "l" },
{ "code": 121, "label": "y" },
{ "code": 351, "label": "ş" }
],
[
{ "code": 106, "label": "j" },
{ "code": 246, "label": "ö" },
{ "code": 118, "label": "v" },
{ "code": 99, "label": "c" },
{ "code": 231, "label": "ç" },
{ "code": 122, "label": "z" },
{ "code": 115, "label": "s" },
{ "code": 98, "label": "b" },
{ "code": 120, "label": "x" }
]
]
}

View File

@@ -0,0 +1,46 @@
{
"type": "characters",
"name": "turkish_q",
"authors": [ "patrickgold" ],
"direction": "ltr",
"arrangement": [
[
{ "code": 113, "label": "q" },
{ "code": 119, "label": "w" },
{ "code": 101, "label": "e" },
{ "code": 114, "label": "r" },
{ "code": 116, "label": "t" },
{ "code": 121, "label": "y" },
{ "code": 117, "label": "u" },
{ "code": 305, "label": "ı" },
{ "code": 111, "label": "o" },
{ "code": 112, "label": "p" },
{ "code": 287, "label": "ğ" },
{ "code": 252, "label": "ü" }
],
[
{ "code": 97, "label": "a" },
{ "code": 115, "label": "s" },
{ "code": 100, "label": "d" },
{ "code": 102, "label": "f" },
{ "code": 103, "label": "g" },
{ "code": 104, "label": "h" },
{ "code": 106, "label": "j" },
{ "code": 107, "label": "k" },
{ "code": 108, "label": "l" },
{ "code": 351, "label": "ş" },
{ "code": 105, "label": "i" }
],
[
{ "code": 122, "label": "z" },
{ "code": 120, "label": "x" },
{ "code": 99, "label": "c" },
{ "code": 118, "label": "v" },
{ "code": 98, "label": "b" },
{ "code": 110, "label": "n" },
{ "code": 109, "label": "m" },
{ "code": 246, "label": "ö" },
{ "code": 231, "label": "ç" }
]
]
}

View File

@@ -10,7 +10,8 @@
{ "code": -20, "label": "arrow_left", "type": "navigation" },
{ "code": -21, "label": "arrow_right", "type": "navigation" },
{ "code": -131, "label": "clipboard_cut", "type": "enter_editing" },
{ "code": -132, "label": "clipboard_paste", "type": "enter_editing" }
{ "code": -132, "label": "clipboard_paste", "type": "enter_editing" },
{ "code": -214, "label": "switch_to_clipboard_context", "type": "system_gui"}
]
]
}

View File

@@ -0,0 +1,64 @@
{
"$type": "dev.patrickgold.florisboard.ime.theme.Theme",
"name": "gboard_night",
"label": "Gboard Night",
"authors": [ "Netscaping" ],
"isNightTheme": true,
"attributes": {
"window": {
"colorPrimary": "#5e97f6",
"colorPrimaryDark": "#4285f4",
"colorAccent": "#FF9800",
"navigationBarColor": "@keyboard/background",
"navigationBarLight": "false",
"semiTransparentColor": "#20FFFFFF",
"textColor": "#FFFFFF"
},
"keyboard": {
"background": "#292e33"
},
"key": {
"background": "#484c4f",
"backgroundPressed": "#5e5e60",
"foreground": "@window/textColor",
"foregroundPressed": "@window/textColor",
"showBorder": "true"
},
"key:enter": {
"background": "@window/colorPrimary",
"backgroundPressed": "@window/colorPrimaryDark",
"foreground": "#FFFFFF",
"foregroundPressed": "#FFFFFF"
},
"key:shift:capslock": {
"foreground": "@window/colorAccent",
"foregroundPressed": "@window/colorAccent"
},
"media": {
"foreground": "@window/textColor",
"foregroundAlt": "#BDBDBD"
},
"oneHanded": {
"background": "#373c41",
"foreground": "#9b9da0"
},
"popup": {
"background": "#373c41",
"backgroundActive": "#5a5e60",
"foreground": "@window/textColor"
},
"privateMode": {
"background": "#A000FF",
"foreground": "#FFFFFF"
},
"smartbar": {
"background": "transparent",
"foreground": "#d4d5d6",
"foregroundAlt": "#73FFFFFF"
},
"smartbarButton": {
"background": "#FFFFFF",
"foreground": "#686868"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,114 @@
package dev.patrickgold.florisboard.ime.clip
import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.clip.provider.ClipboardItem
import dev.patrickgold.florisboard.ime.clip.provider.ItemType
import dev.patrickgold.florisboard.ime.core.FlorisBoard
class ClipboardHistoryItemAdapter(
private val dataSet: ArrayDeque<FlorisClipboardManager.TimedClipData>,
private val pins: ArrayDeque<ClipboardItem>
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
class ClipboardHistoryTextViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val textView: TextView = view.findViewById(R.id.clipboard_history_item_text)
}
class ClipboardHistoryImageViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val imgView: ImageView = view.findViewById(R.id.clipboard_history_item_img)
}
companion object {
private const val MAX_SIZE: Int = 256
}
override fun getItemViewType(position: Int): Int {
return if (position < pins.size) {
// is a pin
pins[position].type.value
}else {
// regular history item
dataSet[position - pins.size].data.type.value
}
}
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
// Create a new view, which defines the UI of the list item
val vh = when (viewType) {
ItemType.IMAGE.value -> {
val view = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.clipboard_history_item_image, viewGroup, false)
ClipboardHistoryImageViewHolder(view)
}
ItemType.TEXT.value -> {
val view = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.clipboard_history_item_text, viewGroup, false)
ClipboardHistoryTextViewHolder(view)
}
else -> null
}!!
val clipboardInputManager = ClipboardInputManager.getInstance()
(vh.itemView as ClipboardHistoryItemView).keyboardView = clipboardInputManager.getClipboardHistoryView()
return vh
}
// Replace the contents of a view (invoked by the layout manager)
override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
when (viewHolder) {
is ClipboardHistoryTextViewHolder -> {
var text = if (position < pins.size) {
(viewHolder.itemView as ClipboardHistoryItemView).setPinned()
pins[position].text
}else {
(viewHolder.itemView as ClipboardHistoryItemView).setUnpinned()
dataSet[position - pins.size].data.text
}
if (text!!.length > MAX_SIZE) {
text = text.subSequence(0 until MAX_SIZE).toString() + "..."
}
viewHolder.textView.text = text
}
is ClipboardHistoryImageViewHolder -> {
val uri = if (position < pins.size) {
(viewHolder.itemView as ClipboardHistoryItemView).setPinned()
pins[position].uri
}else {
(viewHolder.itemView as ClipboardHistoryItemView).setUnpinned()
dataSet[position - pins.size].data.uri
}
viewHolder.imgView.clipToOutline = true
viewHolder.imgView.visibility = GONE
// For very large images, this can take a bit
FlorisClipboardManager.getInstance().executor.execute {
val resolver = FlorisBoard.getInstance().context.contentResolver
val inputStream = resolver.openInputStream(uri!!)
val drawable = Drawable.createFromStream(inputStream, "clipboard URI")
viewHolder.itemView.post {
viewHolder.imgView.setImageDrawable(drawable)
viewHolder.imgView.visibility = VISIBLE
}
}
}
}
}
override fun getItemCount() = pins.size + dataSet.size
}

View File

@@ -0,0 +1,86 @@
package dev.patrickgold.florisboard.ime.clip
import android.content.Context
import android.util.AttributeSet
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.constraintlayout.widget.ConstraintLayout
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
class ClipboardHistoryItemView: ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
lateinit var keyboardView: ClipboardHistoryView
constructor(context: Context) : this(context, null as AttributeSet?)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
private var popupManager: ClipboardPopupManager? = null
override fun onAttachedToWindow() {
super.onAttachedToWindow()
popupManager = ClipboardPopupManager(keyboardView, FlorisBoard.getInstance().popupLayerView, this)
setOnClickListener{
onClickItem()
}
setOnLongClickListener{
onLongClickItem()
}
val themeManager = ThemeManager.default()
themeManager.registerOnThemeUpdatedListener(this)
}
override fun onThemeUpdated(theme: Theme) {
background.setTint(theme.getAttr(Theme.Attr.KEY_BACKGROUND).toSolidColor().color)
val pin = findViewById<ImageView>(R.id.clipboard_pin).drawable
pin?.setTint(theme.getAttr(Theme.Attr.KEY_FOREGROUND).toSolidColor().color)
}
private fun onLongClickItem() : Boolean {
popupManager?.show(this)
return true
}
private fun onClickItem(){
val position = ClipboardInputManager.getInstance().getPositionOfView(this)
val instance = FlorisClipboardManager.getInstance()
val canPaste = instance.canBePasted(instance.peekHistoryOrPin(position))
if (canPaste) {
instance.pasteItem(position)
}else {
Toast.makeText(context, context.getString(R.string.clip__cant_paste), Toast.LENGTH_SHORT).show()
}
}
fun setPinned() {
val view = findViewById<TextView>(R.id.clipboard_history_item_text)
view?.run {
val params = layoutParams as LayoutParams
params.marginEnd = resources.getDimensionPixelSize(R.dimen.clipboard_text_item_pin_margin)
layoutParams = params
}
findViewById<ImageView>(R.id.clipboard_pin).visibility = VISIBLE
invalidate()
val themeManager = ThemeManager.default()
onThemeUpdated(themeManager.activeTheme)
}
fun setUnpinned(){
val view = findViewById<TextView>(R.id.clipboard_history_item_text)
// if text view, also update margin.
view?.run {
val params = layoutParams as LayoutParams
params.marginEnd = 0
layoutParams = params
invalidate()
}
findViewById<ImageView>(R.id.clipboard_pin).visibility = INVISIBLE
}
}

View File

@@ -0,0 +1,71 @@
package dev.patrickgold.florisboard.ime.clip
import android.content.Context
import android.util.AttributeSet
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import kotlin.math.roundToInt
class ClipboardHistoryView : LinearLayout, FlorisBoard.EventListener,
ThemeManager.OnThemeUpdatedListener {
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
private val themeManager: ThemeManager = ThemeManager.default()
var backButton: ImageButton? = null
private set
var clipText: TextView? = null
private set
var clipboardBar: LinearLayout? = null
private set
private var clipboardHistory: RecyclerView? = null
private var clearAll: ImageButton? = null
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
override fun onAttachedToWindow() {
super.onAttachedToWindow()
florisboard?.addEventListener(this)
themeManager.registerOnThemeUpdatedListener(this)
backButton = findViewById(R.id.back_to_keyboard_button)
clipText = findViewById(R.id.clipboard_text)
clipboardBar = findViewById(R.id.clipboard_bar)
clipboardHistory = findViewById(R.id.clipboard_history_items)
clearAll = findViewById(R.id.clear_clipboard_history)
onApplyThemeAttributes()
// lord alone knows why it doesn't work without this..
onThemeUpdated(themeManager.activeTheme)
}
override fun onDetachedFromWindow() {
themeManager.unregisterOnThemeUpdatedListener(this)
florisboard?.removeEventListener(this)
super.onDetachedFromWindow()
}
override fun onThemeUpdated(theme: Theme) {
val fgColor = theme.getAttr(Theme.Attr.KEY_FOREGROUND).toSolidColor().color
clipText?.setTextColor(fgColor)
backButton?.drawable?.setTint(fgColor)
clearAll?.setColorFilter(fgColor)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val height = florisboard?.inputView?.desiredMediaKeyboardViewHeight ?: 0.0f
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height.roundToInt(), MeasureSpec.EXACTLY))
}
}

View File

@@ -0,0 +1,218 @@
package dev.patrickgold.florisboard.ime.clip
import android.annotation.SuppressLint
import android.os.Handler
import android.os.Looper
import android.view.MotionEvent
import android.view.View
import android.widget.*
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.clip.provider.ClipboardItem
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.InputKeyEvent
import dev.patrickgold.florisboard.ime.core.InputView
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyData
import kotlinx.coroutines.*
import kotlin.math.pow
/**
* Handles the clipboard view and allows for communication between UI and logic.
*/
class ClipboardInputManager private constructor() : CoroutineScope by MainScope(),
FlorisBoard.EventListener{
private val florisboard = FlorisBoard.getInstance()
private var repeatedKeyPressHandler: Handler? = null
private var recyclerView: RecyclerView? = null
private var adapter: ClipboardHistoryItemAdapter? = null
companion object {
private var instance: ClipboardInputManager? = null
@Synchronized
fun getInstance(): ClipboardInputManager {
if (instance == null) {
instance = ClipboardInputManager()
}
return instance!!
}
}
init {
florisboard.addEventListener(this)
}
override fun onCreateInputView() {
super.onCreateInputView()
repeatedKeyPressHandler = Handler(florisboard.context.mainLooper)
}
/**
* Called when a new input view has been registered. Used to initialize all media-relevant
* views and layouts.
*/
@SuppressLint("ClickableViewAccessibility")
override fun onRegisterInputView(inputView: InputView) {
launch(Dispatchers.Default) {
inputView.findViewById<ImageButton>(R.id.back_to_keyboard_button)
.setOnTouchListener { view, event -> onButtonPressEvent(view, event) }
inputView.findViewById<ImageButton>(R.id.clear_clipboard_history)
.setOnTouchListener { view, event -> onButtonPressEvent(view, event) }
recyclerView = inputView.findViewById(R.id.clipboard_history_items)
if (BuildConfig.DEBUG && adapter == null) {
error("initClipboard() not called")
}
recyclerView!!.adapter = adapter
val manager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
recyclerView!!.layoutManager = manager
}
}
/**
* Clean-up of resources and stopping all coroutines.
*/
override fun onDestroy() {
cancel()
instance = null
}
/**
* Returns a reference to the [ClipboardHistoryView]
*/
fun getClipboardHistoryView() : ClipboardHistoryView{
return FlorisBoard.getInstance().inputView?.mainViewFlipper?.getChildAt(2) as ClipboardHistoryView
}
/**
* Returns the adapter position of the view, i.e the position that the item is displayed at (including pins and
* history items).
*
* @param view The ClipboardHistoryItemView whose position is to be determined.
* @return The adapter position of the view
*/
fun getPositionOfView(view: View): Int {
return recyclerView?.getChildLayoutPosition(view)!!
}
/**
* Notify adapter that an item was inserted.
*
* @param position The position the item was inserted at
*/
fun notifyItemInserted(position: Int) = adapter?.notifyItemInserted(position)
/**
* Notify adapter that an item was removed
* @param position The position the item was removed from
*/
fun notifyItemRemoved(position: Int) = adapter?.notifyItemRemoved(position)
/**
* Notify adapter that an item range was removed.
* @param start The index the range starts at (inclusive)
* @param numberOfItems The number of items removed
*/
fun notifyItemRangeRemoved(start: Int, numberOfItems: Int) = adapter?.notifyItemRangeRemoved(start, numberOfItems)
/**
* Notify adapter that an item was moved
* @param from The original position
* @param to The final position
*/
fun notifyItemMoved(from: Int, to: Int) = adapter?.notifyItemMoved(from, to)
/**
* Notify adapter that an item was changed.
*
* @param i The position of the item
*/
fun notifyItemChanged(i: Int) = adapter?.notifyItemChanged(i)
/**
* Handles clicks on the back to keyboard button.
*/
private fun onButtonPressEvent(view: View, event: MotionEvent?): Boolean {
event ?: return false
val data = when (view.id) {
R.id.back_to_keyboard_button -> KeyData(code = KeyCode.SWITCH_TO_TEXT_CONTEXT)
R.id.clear_clipboard_history -> KeyData(code = KeyCode.CLEAR_CLIPBOARD_HISTORY)
else -> null
}!!
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
florisboard.keyPressVibrate()
florisboard.keyPressSound(data)
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.down(data))
}
MotionEvent.ACTION_UP -> {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.up(data))
}
MotionEvent.ACTION_CANCEL -> {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
}
}
// MUST return false here so the background selector for showing a transparent bg works
return false
}
/**
* [recyclerView] will be linked to [dataSet] and [pins] when initialized.
*
* @param dataSet the data set to link to
* @param pins The pins to link to
*/
fun initClipboard(dataSet: ArrayDeque<FlorisClipboardManager.TimedClipData>, pins: ArrayDeque<ClipboardItem>) {
this.adapter = ClipboardHistoryItemAdapter(dataSet = dataSet, pins= pins)
}
/**
* Plays an animation of all items moving off the the clipboard from the top.
*
* @param start The index to start at (to ignore pins)
* @param size The size of the clipboard
* @return The time in millis till the last animation will complete.
*/
fun clearClipboardWithAnimation(start: Int, size: Int): Long {
// list of views to animate
val views = arrayListOf<View>()
for(i in 0 until size){
recyclerView?.findViewHolderForLayoutPosition(i + start)?.let {
views.add(it.itemView)
}
}
// animate the views
var delay = 1L
for (view in views) {
delay += (10 * delay.toDouble().pow(0.1)).toLong()
val an = view.animate().translationX(1500f)
an.startDelay = delay
an.duration = 250
}
// a little while later we reset the views so they can be reused.
Handler(Looper.getMainLooper()).postDelayed({
for (view in views) {
view.translationX = 0f
}
}, 450 + delay)
return 280 + delay
}
}

View File

@@ -0,0 +1,127 @@
package dev.patrickgold.florisboard.ime.clip
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.widget.LinearLayout
import android.widget.Space
import android.widget.TextView
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.popup.PopupLayerView
import kotlin.math.max
class ClipboardPopupManager(private val keyboardView: ClipboardHistoryView,
private val popupLayerView: PopupLayerView?,
private val clipboardHistoryItem: ClipboardHistoryItemView) {
private val popupView: ClipboardPopupView = LayoutInflater.from(keyboardView.context).inflate(R.layout.clip_popup_layout, null) as ClipboardPopupView
private var width = 0
private var height = 0
private var xOffset = 0
private var yOffset = 0
init {
popupLayerView?.addView(popupView)
}
private fun pinButtonListener() {
val pos = ClipboardInputManager.getInstance().getPositionOfView(clipboardHistoryItem)
val pinned = FlorisClipboardManager.getInstance().isPinned(pos)
if (pinned) {
FlorisClipboardManager.getInstance().unpinClip(pos)
hide()
} else {
FlorisClipboardManager.getInstance().pinClip(pos)
hide()
}
}
/**
* Show a popup.
*/
fun show(view: ClipboardHistoryItemView) {
val pinButton = popupView.findViewById<LinearLayout>(R.id.pin_clip_item)
pinButton.setOnClickListener {
pinButtonListener()
}
val pos = ClipboardInputManager.getInstance().getPositionOfView(clipboardHistoryItem)
val pinned = FlorisClipboardManager.getInstance().isPinned(pos)
if (pinned) {
pinButton.findViewById<TextView>(R.id.pin_clip_item_text).text = view.context.getString(R.string.clip__unpin_item)
}
val delete = popupView.findViewById<LinearLayout>(R.id.remove_from_history)
delete.setOnClickListener {
FlorisClipboardManager.getInstance().removeClip(pos)
hide()
}
val clipboardManager = FlorisClipboardManager.getInstance()
val clipItem = clipboardManager.peekHistoryOrPin(pos)
val pasteShouldBeEnabled = FlorisClipboardManager.getInstance().canBePasted(clipItem)
// the clipboard item has any of the supported mime types of the editor OR is plain text.
val paste = popupView.findViewById<LinearLayout>(R.id.paste_clip_item)
if (pasteShouldBeEnabled) {
paste.setOnClickListener {
FlorisClipboardManager.getInstance().pasteItem(pos)
hide()
}
popupView.findViewById<Space>(R.id.paste_clip_item_space).visibility = VISIBLE
paste.visibility = VISIBLE
}else {
popupView.findViewById<Space>(R.id.paste_clip_item_space).visibility = GONE
paste.visibility = GONE
}
FlorisBoard.getInstance().isClipboardContextMenuShown = true
popupLayerView?.clipboardPopupManager = this
popupLayerView?.intercept = popupView
calc(view)
popupView.properties.let {
it.width = this.width
it.height = this.height
it.xOffset = this.xOffset
it.yOffset = this.yOffset
}
popupView.show(keyboardView)
}
/**
* Calculate sizes of popup.
*/
private fun calc(view: ClipboardHistoryItemView) {
val widthMeasureSpec: Int = View.MeasureSpec.makeMeasureSpec(view.width, View.MeasureSpec.AT_MOST)
val heightMeasureSpec: Int = View.MeasureSpec.makeMeasureSpec(100000, View.MeasureSpec.AT_MOST)
popupView.invalidate()
popupView.measure(widthMeasureSpec, heightMeasureSpec)
width = view.width * 4 / 5
height = popupView.measuredHeight
xOffset = view.x.toInt() + (view.width - width) / 2
// y offset is either where the top of the item is OR if the top is off screen, the top of the keyboard.
yOffset = max(view.y.toInt() - keyboardView.height - height / 2 - 20, keyboardView.y.toInt() - keyboardView.height - height / 2 - 20)
}
/**
* Hides a popup.
*/
fun hide() {
popupView.hide()
popupLayerView?.intercept = null
popupLayerView?.clipboardPopupManager = null
FlorisBoard.getInstance().isClipboardContextMenuShown = false
popupView.apply {
visibility = GONE
}
}
}

View File

@@ -0,0 +1,130 @@
package dev.patrickgold.florisboard.ime.clip
import android.content.Context
import android.graphics.drawable.PaintDrawable
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import dev.patrickgold.florisboard.util.ViewLayoutUtils
class ClipboardPopupView: LinearLayout, ThemeManager.OnThemeUpdatedListener {
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
private var backgroundDrawable: PaintDrawable = PaintDrawable().apply {
setCornerRadius(ViewLayoutUtils.convertDpToPixel(6.0f, context))
}
private val themeManager: ThemeManager = ThemeManager.default()
val properties: Properties = Properties(
width = 0,
height = 0,
xOffset = 0,
yOffset = 0
)
private val isShowing: Boolean
get() = visibility == VISIBLE
init {
visibility = GONE
background = backgroundDrawable
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
themeManager.registerOnThemeUpdatedListener(this)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
themeManager.unregisterOnThemeUpdatedListener(this)
}
override fun onThemeUpdated(theme: Theme) {
backgroundDrawable.apply {
setTint(theme.getAttr(Theme.Attr.POPUP_BACKGROUND).toSolidColor().color)
}
this.findViewById<ImageView>(R.id.pin_clip_item_icon).drawable.apply {
setTint(theme.getAttr(Theme.Attr.WINDOW_TEXT_COLOR).toSolidColor().color)
}
this.findViewById<ImageView>(R.id.remove_from_history_icon).drawable.apply {
setTint(theme.getAttr(Theme.Attr.WINDOW_TEXT_COLOR).toSolidColor().color)
}
this.findViewById<ImageView>(R.id.paste_clip_item_icon).drawable.apply {
setTint(theme.getAttr(Theme.Attr.WINDOW_TEXT_COLOR).toSolidColor().color)
}
if (isShowing) {
invalidate()
}
}
private fun applyProperties(anchor: View) {
val anchorCoords = IntArray(2)
anchor.getLocationInWindow(anchorCoords)
val anchorX = anchorCoords[0]
val anchorY = anchorCoords[1] + anchor.measuredHeight
when (val lp = layoutParams) {
is FrameLayout.LayoutParams -> lp.apply {
width = properties.width
height = properties.height
setMargins(
anchorX + properties.xOffset,
anchorY + properties.yOffset,
0,
0
)
}
else -> {
layoutParams = FrameLayout.LayoutParams(properties.width, properties.height).apply {
setMargins(
anchorX + properties.xOffset,
anchorY + properties.yOffset,
0,
0
)
}
}
}
if (isShowing) {
requestLayout()
invalidate()
}
}
fun show(anchor: View) {
applyProperties(anchor)
visibility = VISIBLE
requestLayout()
invalidate()
}
fun hide() {
visibility = GONE
requestLayout()
invalidate()
}
data class Properties(
var width: Int,
var height: Int,
var xOffset: Int,
var yOffset: Int
)
}

View File

@@ -0,0 +1,405 @@
package dev.patrickgold.florisboard.ime.clip
import android.content.ClipboardManager
import android.content.Context
import android.content.Context.CLIPBOARD_SERVICE
import android.os.Handler
import android.os.Looper
import dev.patrickgold.florisboard.ime.clip.provider.*
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.util.cancelAll
import dev.patrickgold.florisboard.util.postAtScheduledRate
import timber.log.Timber
import java.io.Closeable
import java.util.*
import java.util.concurrent.ExecutorService
import kotlin.collections.ArrayDeque
/**
* [FlorisClipboardManager] manages the clipboard and clipboard history.
*
* Also just going to document how all the classes here work.
*
* [FlorisClipboardManager] handles storage and retrieval of clipboard items. All manipulation of the
* clipboard goes through here.
*
* [ClipboardInputManager] handles the input view and allows for communication between UI and logic
*
* [ClipboardHistoryView] is the view representing the clipboard context. Only does some theme stuff.
*
* [ClipboardHistoryItemView] is the view representing an item in the clipboard history (either image or text). Only
* does UI stuff.
*
* [ClipboardHistoryItemAdapter] is the recyclerview adapter that backs the clipboard history.
*
* [ClipboardPopupManager] handles the popups for each [ClipboardHistoryItemView] (each item has its own popup manager)
*
* [ClipboardPopupView] is the view representing a popup displayed when long pressing on a clipboard history item.
*/
class FlorisClipboardManager private constructor() : ClipboardManager.OnPrimaryClipChangedListener, Closeable {
private lateinit var pinsDao: PinnedClipboardItemDao
lateinit var executor: ExecutorService
// Using ArrayDeque because it's "technically" the correct data structure (I think).
// Newest stored first, oldest stored last.
private var history: ArrayDeque<TimedClipData> = ArrayDeque()
private var pins: ArrayDeque<ClipboardItem> = ArrayDeque()
private var current: ClipboardItem? = null
private var onPrimaryClipChangedListeners: ArrayList<OnPrimaryClipChangedListener> = arrayListOf()
private lateinit var systemClipboardManager: ClipboardManager
private lateinit var handler: Handler
private lateinit var prefHelper: PrefHelper
data class TimedClipData(val data: ClipboardItem, val timeUTC: Long)
interface OnPrimaryClipChangedListener {
fun onPrimaryClipChanged()
}
companion object {
private var instance: FlorisClipboardManager? = null
// 1 minute
private const val INTERVAL = 60 * 1000L
@Synchronized
fun getInstance(): FlorisClipboardManager {
if (instance == null) {
instance = FlorisClipboardManager()
}
return instance!!
}
@Synchronized
fun getInstanceOrNull(): FlorisClipboardManager? = instance
/**
* Taken from ClipboardDescription.java from the AOSP
*
* Helper to compare two MIME types, where one may be a pattern.
* @param concreteType A fully-specified MIME type.
* @param desiredType A desired MIME type that may be a pattern such as * / *.
* @return Returns true if the two MIME types match.
*/
fun compareMimeTypes(concreteType: String, desiredType: String): Boolean {
val typeLength = desiredType.length
if (typeLength == 3 && desiredType == "*/*") {
return true
}
val slashpos = desiredType.indexOf('/')
if (slashpos > 0) {
if (typeLength == slashpos + 2 && desiredType[slashpos + 1] == '*') {
if (desiredType.regionMatches(0, concreteType, 0, slashpos + 1)) {
return true
}
} else if (desiredType == concreteType) {
return true
}
}
return false
}
}
/**
* Adds a new item to the clipboard history (if enabled).
*/
fun updateHistory(newData: ClipboardItem) {
val clipboardPrefs = prefHelper.clipboard
if (clipboardPrefs.enableHistory) {
if (clipboardPrefs.limitHistorySize) {
var numRemoved = 0
while (history.size >= clipboardPrefs.maxHistorySize) {
numRemoved += 1
history.removeLast().data.close()
}
ClipboardInputManager.getInstance().notifyItemRangeRemoved(history.size, numRemoved)
}
val timed = TimedClipData(newData, System.currentTimeMillis())
history.addFirst(timed)
ClipboardInputManager.getInstance().notifyItemInserted(pins.size)
}
}
/**
* Used so that [onPrimaryClipChanged] knows whether it was called by [changeCurrent] (and hence shouldn't update
* history)
*/
private var shouldUpdateHistory = true
/**
* Changes current clipboard item. WITHOUT updating the history.
*/
fun changeCurrent(newData: ClipboardItem, closePrevious: Boolean) {
if (prefHelper.clipboard.enableInternal) {
if (closePrevious) current?.close()
current = newData
val isEqual = when (newData.type) {
ItemType.TEXT -> newData.text == systemClipboardManager.primaryClip?.getItemAt(0)?.text
ItemType.IMAGE -> newData.uri == systemClipboardManager.primaryClip?.getItemAt(0)?.uri
}
if (prefHelper.clipboard.syncToSystem && !isEqual)
systemClipboardManager.setPrimaryClip(newData.toClipData())
} else {
shouldUpdateHistory = false
systemClipboardManager.setPrimaryClip(newData.toClipData())
}
onPrimaryClipChangedListeners.forEach { it.onPrimaryClipChanged() }
}
/**
* Change the current text on clipboard, update history (if enabled).
*
*/
fun addNewClip(newData: ClipboardItem) {
updateHistory(newData)
// If history is disabled, this new item will replace the old one and hence should be closed.
changeCurrent(newData, !prefHelper.clipboard.enableHistory)
}
/**
* Wraps some plaintext in a ClipData and calls [addNewClip]
*/
fun addNewPlaintext(newText: String) {
val newData = ClipboardItem(null, ItemType.TEXT, null, newText, ClipboardItem.TEXT_PLAIN)
addNewClip(newData)
}
val primaryClip: ClipboardItem?
get() = if (prefHelper.clipboard.enableInternal) {
current
} else {
systemClipboardManager.primaryClip?.let { ClipboardItem.fromClipData(it, false) }
}
fun peekHistory(index: Int): ClipboardItem? {
return history.getOrNull(index)?.data
}
fun addPrimaryClipChangedListener(listener: OnPrimaryClipChangedListener) {
onPrimaryClipChangedListeners.add(listener)
}
fun removePrimaryClipChangedListener(listener: OnPrimaryClipChangedListener) {
onPrimaryClipChangedListeners.remove(listener)
}
/**
* Called by system clipboard when the contents are changed
*/
override fun onPrimaryClipChanged() {
// Run on async thread to avoid blocking.
if (systemClipboardManager.primaryClip?.getItemAt(0)?.text == null &&
systemClipboardManager.primaryClip?.getItemAt(0)?.uri == null) {
return
}
val isEqual = when (primaryClip?.type) {
ItemType.TEXT -> primaryClip?.text == systemClipboardManager.primaryClip?.getItemAt(0)?.text
ItemType.IMAGE -> primaryClip?.uri == systemClipboardManager.primaryClip?.getItemAt(0)?.uri
null -> false
}
systemClipboardManager.primaryClip?.let {
if (prefHelper.clipboard.enableInternal) {
// In the event that the internal clipboard is enabled, sync to internal clipboard is enabled
// and the item is not already in internal clipboard, add it.
if (prefHelper.clipboard.syncToFloris && !isEqual) {
addNewClip(ClipboardItem.fromClipData(it, true))
}
} else if (prefHelper.clipboard.enableHistory) {
// in the event history is enabled, and it should be updated it is updated
if (shouldUpdateHistory) {
updateHistory(ClipboardItem.fromClipData(it, false))
} else {
shouldUpdateHistory = true
}
}
}
}
fun hasPrimaryClip(): Boolean {
return this.primaryClip != null
}
/**
* Cleans up.
*
* Sets [instance] to null for GC. Unregisters the system clipboard listener, cancels clipboard clean ups.
*/
override fun close() {
systemClipboardManager.removePrimaryClipChangedListener(this)
handler.cancelAll()
instance = null
}
/**
* Initialize the floris clipboard manager. Exists to avoid dependency loop due to reference
* to [FlorisBoard.context]
*
* Sets up the clipboard cleanup task, links the recycler view in clipInputManager to [history].
*
* @param context Required to register as an onPrimaryClipChangedListener of ClipboardManager
*/
fun initialize(context: Context) {
this.systemClipboardManager = (context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager)
systemClipboardManager.addPrimaryClipChangedListener(this)
prefHelper = PrefHelper.getDefaultInstance(context)
val cleanUpClipboard = Runnable {
if (!prefHelper.clipboard.cleanUpOld) {
return@Runnable
}
val currentTime = System.currentTimeMillis()
var numToPop = 0
val expiryTime = prefHelper.clipboard.cleanUpAfter * 60 * 1000
for (item in history.asReversed()) {
if (item.timeUTC + expiryTime < currentTime) {
numToPop += 1
} else {
break
}
}
for (i in 0 until numToPop) {
history.removeLast().data.close()
}
ClipboardInputManager.getInstance().notifyItemRangeRemoved(pins.size + history.size, numToPop)
}
FlorisBoard.getInstance().clipInputManager.initClipboard(this.history, this.pins)
handler = Handler(Looper.getMainLooper())
prefHelper
handler.postAtScheduledRate(0, INTERVAL, cleanUpClipboard)
executor = FlorisBoard.getInstance().asyncExecutor
executor.execute {
pinsDao = PinnedItemsDatabase.getInstance().clipboardItemDao()
pinsDao.getAll().toCollection(this.pins)
FlorisContentProvider.getInstance().initIfNotAlready()
}
}
/**
* Clears the history with an animation.
*/
fun clearHistoryWithAnimation() {
val clipInputManager = FlorisBoard.getInstance().clipInputManager
val delay = clipInputManager.clearClipboardWithAnimation(pins.size, history.size)
handler.postDelayed({
val size = history.size
for (item in history) {
item.data.close()
}
history.clear()
clipInputManager.notifyItemRangeRemoved(pins.size, size)
}, delay)
}
fun pinClip(adapterPos: Int) {
val clipInputManager = FlorisBoard.getInstance().clipInputManager
val pin = history.removeAt(adapterPos - pins.size)
pins.addFirst(pin.data)
clipInputManager.notifyItemMoved(adapterPos, 0)
clipInputManager.notifyItemChanged(0)
executor.execute {
val uid = pinsDao.insert(pin.data)
pin.data.uid = uid
}
}
/**
* Get the item at a particular [adapterPos] (i.e the position the item is displayed at.)
*/
fun peekHistoryOrPin(adapterPos: Int): ClipboardItem {
return when {
adapterPos < pins.size -> pins[adapterPos]
else -> history[adapterPos - pins.size].data
}
}
fun isPinned(position: Int): Boolean {
return when {
position < pins.size -> true
else -> false
}
}
fun unpinClip(adapterPos: Int) {
val clipInputManager = FlorisBoard.getInstance().clipInputManager
val item = pins.removeAt(adapterPos)
val clipboardPrefs = prefHelper.clipboard
if (clipboardPrefs.limitHistorySize) {
var numRemoved = 0
while (history.size >= clipboardPrefs.maxHistorySize) {
numRemoved += 1
history.removeLast().data.close()
}
ClipboardInputManager.getInstance().notifyItemRangeRemoved(history.size, numRemoved)
}
val timed = TimedClipData(item, System.currentTimeMillis())
history.addFirst(timed)
clipInputManager.notifyItemMoved(adapterPos, pins.size)
clipInputManager.notifyItemChanged(pins.size)
executor.execute {
pinsDao.delete(item)
}
}
fun removeClip(pos: Int) {
when {
pos < pins.size -> {
val item = pins.removeAt(pos)
executor.execute {
Timber.d("removing pin")
pinsDao.delete(item)
}
item.close()
}
else -> {
history.removeAt(pos - pins.size).data.close()
}
}
val clipboardInputManager = ClipboardInputManager.getInstance()
clipboardInputManager.notifyItemRemoved(pos)
}
fun pasteItem(pos: Int) {
val item = peekHistoryOrPin(pos)
FlorisBoard.getInstance().activeEditorInstance.commitClipboardItem(item)
}
/**
* Returns true if the editor can accept the clip item, else false.
*/
fun canBePasted(clipItem: ClipboardItem?): Boolean {
if (clipItem == null) return false
return clipItem.mimeTypes.contains("text/plain") || FlorisBoard.getInstance().activeEditorInstance.contentMimeTypes?.any { editorType ->
clipItem.mimeTypes.any { clipType ->
if (editorType != null) {
compareMimeTypes(clipType, editorType)
}else { false }
}
} == true
}
}

View File

@@ -0,0 +1,75 @@
package dev.patrickgold.florisboard.ime.clip.provider
import android.net.Uri
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import timber.log.Timber
import java.io.File
/**
* Backend class which is used by [FlorisContentProvider] to serve content.
*/
class FileStorage private constructor() {
companion object {
private const val BUF_SIZE = 1024 * 8
private var instance: FileStorage? = null
private var offset = 0
fun getInstance() : FileStorage {
if (this.instance == null){
this.instance = FileStorage()
}
return instance!!
}
}
/**
* Clones a content URI to internal storage.
* @param uri The URI
* @return the file's name which is a unique long
*/
@Synchronized
fun cloneURI(uri: Uri) : Long {
val context = FlorisBoard.getInstance().context
// nanoTime + the number of items created so that it's unique.
val name = (System.nanoTime() + offset)
// Just a normal copy from input stream to output stream.
val source = context.contentResolver.openInputStream(uri)!!
val sink = File(context.filesDir, name.toString()).outputStream()
var nread = 0L
val buf = ByteArray(BUF_SIZE)
var n: Int
while (source.read(buf).also { n = it } > 0) {
sink.write(buf, 0, n)
nread += n.toLong()
}
source.close()
sink.close()
return name
}
/**
* Deletes the file corresponding to an id.
*/
fun deleteById(id: Long) {
Timber.d("Cleaning up $id")
val file = File(FlorisBoard.getInstance().filesDir, id.toString())
file.delete()
}
/**
* Get the file address of an id.
*/
fun getAddress(id: Long): String {
return FlorisBoard.getInstance().filesDir.toString() + "/$id"
}
}

View File

@@ -0,0 +1,132 @@
package dev.patrickgold.florisboard.ime.clip.provider
import android.content.*
import android.database.Cursor
import android.net.Uri
import android.os.ParcelFileDescriptor
import androidx.room.Room
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import timber.log.Timber
import java.io.File
import java.util.concurrent.ExecutorService
/**
* Allows apps to access images on the clipboard.
*
* This is sometimes called by the UI thread, so all functions are non blocking.
* Database accesses are performed async.
*/
class FlorisContentProvider : ContentProvider() {
private lateinit var fileUriDao: FileUriDao
private val mimeTypes: HashMap<Long, Array<String>> = hashMapOf()
private lateinit var executor: ExecutorService
override fun onCreate(): Boolean {
instance = this
return true
}
fun initIfNotAlready(){
if (this::fileUriDao.isInitialized){
return
}
fileUriDao = Room.databaseBuilder(
context!!,
FileUriDatabase::class.java, "fileuridb"
).build().fileUriDao()
executor = FlorisBoard.getInstance().asyncExecutor
for (fileUri in fileUriDao.getAll()) {
mimeTypes[fileUri.fileName] = fileUri.mimeTypes
}
}
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
// just return nothing, nothing should call this function at all.
return null
}
override fun getType(uri: Uri): String {
return when (matcher.match(uri)) {
CLIP_ITEM -> mimeTypes.getOrElse(ContentUris.parseId(uri), { throw IllegalArgumentException("Don't have this item!") })[0]
CLIPS_TABLE -> "vnd.android.cursor.dir/$AUTHORITY.clip"
else -> throw IllegalArgumentException("Don't know what this is $uri")
}
}
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor {
val id = ContentUris.parseId(uri)
val path = File(FileStorage.getInstance().getAddress(id))
// Nothing has permission to write anyway.
return ParcelFileDescriptor.open(path, ParcelFileDescriptor.MODE_READ_ONLY)
}
override fun insert(uri: Uri, values: ContentValues?): Uri {
when (matcher.match(uri)){
CLIPS_TABLE -> {
val id = FileStorage.getInstance().cloneURI(Uri.parse(values?.getAsString("uri")))
val mimes = values?.getAsString("mimetypes")?.split(",")?.toTypedArray()
mimes?.let {
mimeTypes[id] = mimes
executor.execute {
Timber.d("Inserted file uri $id")
fileUriDao.insert(FileUri(id, mimes))
}
}
return ContentUris.withAppendedId(CLIPS_URI, id)
}
else -> throw IllegalArgumentException("Don't know what this is $uri")
}
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
when (matcher.match(uri)){
CLIP_ITEM -> {
val id = ContentUris.parseId(uri)
FileStorage.getInstance().deleteById(id)
mimeTypes.remove(id)
context?.revokeUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
executor.execute {
fileUriDao.delete(id)
}
return 1
}
else -> throw IllegalArgumentException("Don't know what this is $uri")
}
}
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int {
throw IllegalArgumentException("This ContentProvider does not support update.")
}
companion object {
private var instance: FlorisContentProvider? = null
const val AUTHORITY = "dev.patrickgold.florisboard.provider.clip"
val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY")
val CLIPS_URI: Uri = Uri.parse("content://$AUTHORITY/clips")
fun getInstance(): FlorisContentProvider {
return instance!!
}
private const val CLIPS_TABLE = 1
private const val CLIP_ITEM = 0
val matcher: UriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
addURI(AUTHORITY, "clips/#", CLIP_ITEM)
addURI(AUTHORITY, "clips", CLIPS_TABLE)
}
}
}

View File

@@ -0,0 +1,258 @@
package dev.patrickgold.florisboard.ime.clip.provider
import android.content.ClipData
import android.content.ContentValues
import android.net.Uri
import android.provider.BaseColumns
import androidx.room.*
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import java.io.Closeable
enum class ItemType(val value: Int) {
TEXT(1),
IMAGE(2);
companion object {
fun fromInt(value : Int) : ItemType {
return values().first { it.value == value }
}
}
}
/**
* Represents an item on the clipboard.
* The URI stored belongs to FlorisContentProvider, not whatever app copied the image
*
* If type == ItemType.IMAGE there must be a uri set
* if type == ItemType.TEXT there must be a text set
*/
@Entity(tableName = "pins")
data class ClipboardItem(
/** Only used for pins */
@PrimaryKey(autoGenerate = true) @ColumnInfo(name=BaseColumns._ID, index=true) var uid: Long?,
val type: ItemType,
val uri: Uri?,
val text: String?,
val mimeTypes: Array<String>) : Closeable{
/**
* Creates a new ClipData which has the same contents as this.
*/
fun toClipData(): ClipData {
return when (type) {
ItemType.IMAGE -> {
ClipData.newUri(FlorisBoard.getInstance().context.contentResolver, "Clipboard data", uri)
}
ItemType.TEXT -> {
ClipData.newPlainText("Clipboard data", text)
}
}
}
/**
* Instructs the content provider to delete this URI. If not an image, is a noop
*/
override fun close() {
if (type == ItemType.IMAGE) {
FlorisBoard.getInstance().context.contentResolver.delete(this.uri!!, null, null)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ClipboardItem
if (uid != other.uid) return false
if (type != other.type) return false
if (uri != other.uri) return false
if (text != other.text) return false
if (!mimeTypes.contentEquals(other.mimeTypes)) return false
return true
}
override fun hashCode(): Int {
var result = uid.hashCode()
result = 31 * result + type.hashCode()
result = 31 * result + (uri?.hashCode() ?: 0)
result = 31 * result + (text?.hashCode() ?: 0)
result = 31 * result + mimeTypes.contentHashCode()
return result
}
fun stringRepresentation(): String {
return when {
uri != null -> "(Image) $uri"
text != null -> text
else -> "#ERROR"
}
}
companion object {
/**
* So that every item doesn't have to allocate its own array.
*/
val TEXT_PLAIN = arrayOf("text/plain")
/**
* Returns a new ClipboardItem based on a ClipData
*
* @param data The ClipData to clone.
* @param cloneUri Whether to store the image using [FlorisContentProvider].
*/
fun fromClipData(data: ClipData, cloneUri: Boolean) : ClipboardItem {
val type = when {
data.getItemAt(0)?.uri != null -> ItemType.IMAGE
data.getItemAt(0)?.text != null -> ItemType.TEXT
else -> null
}!!
val uri = if (type == ItemType.IMAGE) {
if (data.getItemAt(0).uri.authority == FlorisContentProvider.CONTENT_URI.authority || !cloneUri){
data.getItemAt(0).uri
}else {
val values = ContentValues().apply{
put("uri", data.getItemAt(0).uri.toString())
put("mimetypes", data.description.filterMimeTypes("*/*").joinToString(","))
}
FlorisBoard.getInstance().context.contentResolver.insert(FlorisContentProvider.CLIPS_URI, values)
}
} else { null }
val text = data.getItemAt(0).text?.toString()
val mimeTypes = when (type) {
ItemType.IMAGE -> {
(0 until data.description.mimeTypeCount).map {
data.description.getMimeType(it)
}.toTypedArray()
}
ItemType.TEXT -> { TEXT_PLAIN }
}
return ClipboardItem(null, type, uri, text, mimeTypes)
}
}
}
class Converters {
@TypeConverter
fun uriFromString(value: String?): Uri? {
return Uri.parse(value)
}
@TypeConverter
fun stringFromUri(value: Uri?): String {
return value.toString()
}
@TypeConverter
fun itemTypeToInt(value: ItemType?): Int? {
return value?.value
}
@TypeConverter
fun intToItemType(value: Int?): ItemType? {
return value?.let { ItemType.fromInt(it) }
}
/**
* Only works because the string array is a mimetype.
* DOES NOT USE A GENERALIZED FORMAT.
*/
@TypeConverter
fun mimeTypesToString(mimeTypes: Array<String>): String {
return mimeTypes.joinToString(",")
}
@TypeConverter
fun stringToMimeTypes(value: String): Array<String> {
return value.split(",").toTypedArray()
}
}
@Dao
interface PinnedClipboardItemDao {
@Query("SELECT * FROM pins")
fun getAll(): List<ClipboardItem>
@Insert
fun insert(item: ClipboardItem) : Long
@Delete
fun delete(item: ClipboardItem)
}
@Database(entities = [ClipboardItem::class], version = 1)
@TypeConverters(Converters::class)
abstract class PinnedItemsDatabase : RoomDatabase() {
abstract fun clipboardItemDao() : PinnedClipboardItemDao
companion object {
private var instance: PinnedItemsDatabase? = null
fun getInstance(): PinnedItemsDatabase {
if (instance == null) {
instance = Room.databaseBuilder(
FlorisBoard.getInstance().context,
PinnedItemsDatabase::class.java,
"pins").build()
}
return instance!!
}
}
}
@Entity(tableName = "file_uris")
data class FileUri(
@PrimaryKey @ColumnInfo(name=BaseColumns._ID, index=true) val fileName: Long,
val mimeTypes: Array<String>
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as FileUri
if (fileName != other.fileName) return false
if (!mimeTypes.contentEquals(other.mimeTypes)) return false
return true
}
override fun hashCode(): Int {
var result = 31 + fileName.hashCode()
result = 31 * result + mimeTypes.contentHashCode()
return result
}
}
@Dao
interface FileUriDao {
@Query("SELECT * FROM file_uris WHERE ${BaseColumns._ID} == (:uid)")
fun getById(uid: Long) : FileUri
@Query("DELETE FROM file_uris WHERE ${BaseColumns._ID} == (:id)")
fun delete(id: Long)
@Insert
fun insert(vararg fileUris: FileUri)
@Query("SELECT COUNT(*) FROM file_uris WHERE ${BaseColumns._ID} == (:id)")
fun numberWithId(id: Long): Int
@Query("SELECT * FROM file_uris")
fun getAll(): List<FileUri>
}
@Database(entities = [FileUri::class], version = 1)
@TypeConverters(Converters::class)
abstract class FileUriDatabase : RoomDatabase() {
abstract fun fileUriDao() : FileUriDao
}

View File

@@ -16,15 +16,24 @@
package dev.patrickgold.florisboard.ime.core
import android.content.ClipDescription
import android.content.Intent
import android.inputmethodservice.InputMethodService
import android.os.Build
import android.os.SystemClock
import android.text.InputType
import android.view.InputDevice
import android.view.KeyCharacterMap
import android.view.KeyEvent
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import androidx.annotation.RequiresApi
import androidx.core.view.inputmethod.InputConnectionCompat
import androidx.core.view.inputmethod.InputContentInfoCompat
import dev.patrickgold.florisboard.ime.clip.FlorisClipboardManager
import dev.patrickgold.florisboard.ime.clip.provider.ClipboardItem
import dev.patrickgold.florisboard.ime.clip.provider.ItemType
import timber.log.Timber
/**
* Class which holds information relevant to an editor instance like the [cachedInput], [selection],
@@ -35,10 +44,12 @@ class EditorInstance private constructor(
private val ims: InputMethodService?,
val imeOptions: ImeOptions,
val inputAttributes: InputAttributes,
val packageName: String
val packageName: String,
private val editorInfo: EditorInfo
) {
val cachedInput: CachedInput = CachedInput(this)
var contentMimeTypes: Array<out String?>? = null
private val florisClipboardManager: FlorisClipboardManager = FlorisClipboardManager.getInstance()
val cursorCapsMode: InputAttributes.CapsMode
get() {
val ic = inputConnection ?: return InputAttributes.CapsMode.NONE
@@ -59,8 +70,6 @@ class EditorInstance private constructor(
}
}
var shouldReevaluateComposingSuggestions: Boolean = false
var isNewSelectionInBoundsOfOld: Boolean = false
private set
var isPrivateMode: Boolean = false
val isRawInputEditor: Boolean
get() = inputAttributes.type == InputAttributes.Type.NULL
@@ -76,7 +85,8 @@ class EditorInstance private constructor(
ims = null,
imeOptions = ImeOptions.fromImeOptionsInt(EditorInfo.IME_NULL),
inputAttributes = InputAttributes.fromInputTypeInt(InputType.TYPE_NULL),
packageName = "undefined"
packageName = "undefined",
editorInfo = EditorInfo()
)
}
@@ -86,7 +96,8 @@ class EditorInstance private constructor(
ims = ims,
imeOptions = ImeOptions.fromImeOptionsInt(editorInfo.imeOptions),
inputAttributes = InputAttributes.fromInputTypeInt(editorInfo.inputType),
packageName = editorInfo.packageName
packageName = editorInfo.packageName,
editorInfo = editorInfo
).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
contentMimeTypes = editorInfo.contentMimeTypes
@@ -109,24 +120,25 @@ class EditorInstance private constructor(
newSelStart: Int, newSelEnd: Int,
candidatesStart: Int, candidatesEnd: Int
) {
isNewSelectionInBoundsOfOld =
newSelStart >= (oldSelStart - 1) &&
newSelStart <= (oldSelStart + 1) &&
newSelEnd >= (oldSelEnd - 1) &&
newSelEnd <= (oldSelEnd + 1)
selection.update(newSelStart, newSelEnd)
cachedInput.update()
// The Android Framework allows that start can be greater than end in some cases. To prevent bugs in the Floris
// input logic, we swap start and end here if this should really be the case.
if (newSelEnd < newSelStart) {
selection.update(newSelEnd, newSelStart)
} else {
selection.update(newSelStart, newSelEnd)
}
if (isPhantomSpaceActive && wasPhantomSpaceActiveLastUpdate) {
isPhantomSpaceActive = false
} else if (isPhantomSpaceActive && !wasPhantomSpaceActiveLastUpdate) {
wasPhantomSpaceActiveLastUpdate = true
}
cachedInput.update()
if (isComposingEnabled && candidatesStart >= 0 && candidatesEnd >= 0) {
shouldReevaluateComposingSuggestions = true
}
if (selection.isCursorMode && isComposingEnabled && !isRawInputEditor && !isPhantomSpaceActive) {
markComposingRegion(cachedInput.currentWord)
} else {
} else if (newSelStart >= 0) {
markComposingRegion(null)
}
}
@@ -169,22 +181,70 @@ class EditorInstance private constructor(
*/
fun commitText(text: String): Boolean {
val ic = inputConnection ?: return false
return if (isRawInputEditor) {
return if (isRawInputEditor || selection.isSelectionMode || !isComposingEnabled) {
ic.commitText(text, 1)
} else {
ic.beginBatchEdit()
markComposingRegion(null)
if (isPhantomSpaceActive && selection.start > 0 && getTextBeforeCursor(1) != " " && text != " ") {
ic.commitText(" ", 1)
val isWordComponent = CachedInput.isWordComponent(text)
val isPhantomSpace = isPhantomSpaceActive && selection.start > 0 && getTextBeforeCursor(1) != " "
when {
isPhantomSpace && isWordComponent -> {
ic.finishComposingText()
ic.commitText(" ", 1)
ic.setComposingText(text, 1)
}
!isPhantomSpace && isWordComponent -> {
ic.finishComposingText()
ic.commitText(text, 1)
ic.setComposingRegion(cachedInput.currentWord.start, cachedInput.currentWord.end + text.length)
}
else -> {
ic.finishComposingText()
ic.commitText(text, 1)
}
}
isPhantomSpaceActive = false
wasPhantomSpaceActiveLastUpdate = false
ic.commitText(text, 1)
ic.endBatchEdit()
true
}
}
/**
* Commits the given [ClipboardItem]. If the clip data is text (incl. HTML), it delegates to [commitText].
* If the item has a content URI (and the EditText supports it), the item is committed as rich data.
* This allows for committing (e.g) images.
*
* @param item The ClipboardItem to commit
* @return True on success, false if something went wrong.
*/
fun commitClipboardItem(item: ClipboardItem): Boolean {
val mimeTypes = item.mimeTypes
return when (item.type){
ItemType.IMAGE -> {
val inputContentInfo = InputContentInfoCompat(
item.uri!!,
ClipDescription("clipboard image", mimeTypes),
null
)
val ic = inputConnection ?: return false
ic.finishComposingText()
var flags = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
flags = flags or InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION
}else {
FlorisBoard.getInstance().context.grantUriPermission(editorInfo.packageName, item.uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
InputConnectionCompat.commitContent(ic, editorInfo, inputContentInfo, flags, null)
}
ItemType.TEXT -> {
commitText(item.text.toString())
}
}
}
/**
* Executes a backward delete on this editor's text. If a text selection is active, all
* characters inside this selection will be removed, else only the left-most character from
@@ -193,22 +253,9 @@ class EditorInstance private constructor(
* @return True on success, false if an error occurred or the input connection is invalid.
*/
fun deleteBackwards(): Boolean {
val ic = inputConnection ?: return false
isPhantomSpaceActive = false
wasPhantomSpaceActiveLastUpdate = false
return if (isRawInputEditor) {
sendSystemKeyEvent(KeyEvent.KEYCODE_DEL)
} else {
ic.beginBatchEdit()
markComposingRegion(null)
sendSystemKeyEvent(KeyEvent.KEYCODE_DEL)
cachedInput.update()
if (isComposingEnabled) {
markComposingRegion(cachedInput.currentWord)
}
ic.endBatchEdit()
true
}
return sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL)
}
/**
@@ -230,12 +277,14 @@ class EditorInstance private constructor(
ic.beginBatchEdit()
markComposingRegion(null)
getWordsInString(cachedInput.rawText.substring(0,
(selection.start - cachedInput.offset).coerceAtLeast(0))).run {
get(size - n.coerceAtLeast(0)).range
}.run {
ic.setSelection(first + cachedInput.offset, selection.start)
}
try {
getWordsInString(cachedInput.rawText.substring(0,
(selection.start - cachedInput.offset).coerceAtLeast(0))).run {
get(size - n.coerceAtLeast(0)).range
}.run {
ic.setSelection(first + cachedInput.offset, selection.start)
}
} catch (e: Exception) {}
ic.commitText("", 1)
@@ -355,9 +404,11 @@ class EditorInstance private constructor(
* @return True on success, false if an error occurred or the input connection is invalid.
*/
fun performClipboardCut(): Boolean {
Timber.d("performClipboardCut")
isPhantomSpaceActive = false
wasPhantomSpaceActiveLastUpdate = false
return sendSystemKeyEventCtrl(KeyEvent.KEYCODE_X)
florisClipboardManager.addNewPlaintext(selection.text)
return sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL)
}
/**
@@ -367,10 +418,11 @@ class EditorInstance private constructor(
* @return True on success, false if an error occurred or the input connection is invalid.
*/
fun performClipboardCopy(): Boolean {
Timber.d("performClipboardCopy")
isPhantomSpaceActive = false
wasPhantomSpaceActiveLastUpdate = false
return sendSystemKeyEventCtrl(KeyEvent.KEYCODE_C) &&
selection.updateAndNotify(selection.end, selection.end)
florisClipboardManager.addNewPlaintext(selection.text)
return selection.updateAndNotify(selection.end, selection.end)
}
/**
@@ -382,7 +434,8 @@ class EditorInstance private constructor(
fun performClipboardPaste(): Boolean {
isPhantomSpaceActive = false
wasPhantomSpaceActiveLastUpdate = false
return sendSystemKeyEventCtrl(KeyEvent.KEYCODE_V)
Timber.d("Before commit clip data")
return commitClipboardItem(florisClipboardManager.primaryClip!!)
}
/**
@@ -394,7 +447,14 @@ class EditorInstance private constructor(
fun performClipboardSelectAll(): Boolean {
isPhantomSpaceActive = false
wasPhantomSpaceActiveLastUpdate = false
return sendSystemKeyEventCtrl(KeyEvent.KEYCODE_A)
markComposingRegion(null)
val ic = inputConnection ?: return false
if (isRawInputEditor) {
sendDownUpKeyEvent(KeyEvent.KEYCODE_A, meta(ctrl = true))
} else {
ic.performContextMenuAction(android.R.id.selectAll)
}
return true
}
/**
@@ -406,7 +466,7 @@ class EditorInstance private constructor(
isPhantomSpaceActive = false
wasPhantomSpaceActiveLastUpdate = false
return if (isRawInputEditor) {
sendSystemKeyEvent(KeyEvent.KEYCODE_ENTER)
sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER)
} else {
commitText("\n")
}
@@ -434,21 +494,7 @@ class EditorInstance private constructor(
fun performUndo(): Boolean {
isPhantomSpaceActive = false
wasPhantomSpaceActiveLastUpdate = false
val ic = inputConnection ?: return false
return if (isRawInputEditor) {
sendSystemKeyEventCtrl(KeyEvent.KEYCODE_Z)
true
} else {
ic.beginBatchEdit()
markComposingRegion(null)
sendSystemKeyEventCtrl(KeyEvent.KEYCODE_Z)
cachedInput.update()
if (isComposingEnabled) {
markComposingRegion(cachedInput.currentWord)
}
ic.endBatchEdit()
true
}
return sendDownUpKeyEvent(KeyEvent.KEYCODE_Z, meta(ctrl = true))
}
/**
@@ -459,73 +505,40 @@ class EditorInstance private constructor(
fun performRedo(): Boolean {
isPhantomSpaceActive = false
wasPhantomSpaceActiveLastUpdate = false
val ic = inputConnection ?: return false
return if (isRawInputEditor) {
sendSystemKeyEventCtrlShift(KeyEvent.KEYCODE_Z)
true
} else {
ic.beginBatchEdit()
markComposingRegion(null)
sendSystemKeyEventCtrlShift(KeyEvent.KEYCODE_Z)
cachedInput.update()
if (isComposingEnabled) {
markComposingRegion(cachedInput.currentWord)
}
ic.endBatchEdit()
true
return sendDownUpKeyEvent(KeyEvent.KEYCODE_Z, meta(ctrl = true, shift = true))
}
/**
* Constructs a meta state integer flag which can be used for setting the `metaState` field when sending a KeyEvent
* to the input connection. If this method is called without a meta modifier set to true, the default value `0` is
* returned.
*
* @param ctrl Set to true to enable the CTRL meta modifier. Defaults to false.
* @param alt Set to true to enable the ALT meta modifier. Defaults to false.
* @param shift Set to true to enable the SHIFT meta modifier. Defaults to false.
*
* @return An integer containing all meta flags passed and formatted for use in a [KeyEvent].
*/
fun meta(
ctrl: Boolean = false,
alt: Boolean = false,
shift: Boolean = false
): Int {
var metaState = 0
if (ctrl) {
metaState = metaState or KeyEvent.META_CTRL_ON or KeyEvent.META_CTRL_LEFT_ON
}
if (alt) {
metaState = metaState or KeyEvent.META_ALT_ON or KeyEvent.META_ALT_LEFT_ON
}
if (shift) {
metaState = metaState or KeyEvent.META_SHIFT_ON or KeyEvent.META_SHIFT_LEFT_ON
}
return metaState
}
/**
* Sends a given [keyEventCode] with [sendDownUpKeyEvent].
*
* @param keyEventCode The key code to send, use a key code defined in Android's [KeyEvent].
* @return True on success, false if an error occurred or the input connection is invalid.
*/
fun sendSystemKeyEvent(keyEventCode: Int): Boolean {
return sendDownUpKeyEvent(keyEventCode, 0)
}
/**
* Sends a given [keyEventCode] with Ctrl pressed with [sendDownUpKeyEvent].
*
* @param keyEventCode The key code to send, use a key code defined in Android's [KeyEvent].
* @return True on success, false if an error occurred or the input connection is invalid.
*/
private fun sendSystemKeyEventCtrl(keyEventCode: Int): Boolean {
return sendDownUpKeyEvent(keyEventCode, KeyEvent.META_CTRL_ON)
}
/**
* Sends a given [keyEventCode] with Ctrl and Shift pressed with [sendDownUpKeyEvent].
*
* @param keyEventCode The key code to send, use a key code defined in Android's [KeyEvent].
* @return True on success, false if an error occurred or the input connection is invalid.
*/
private fun sendSystemKeyEventCtrlShift(keyEventCode: Int): Boolean {
return sendDownUpKeyEvent(keyEventCode, KeyEvent.META_CTRL_ON or KeyEvent.META_SHIFT_ON)
}
/**
* Sends a given [keyEventCode] with Alt pressed with [sendDownUpKeyEvent].
*
* @param keyEventCode The key code to send, use a key code defined in Android's [KeyEvent].
* @return True on success, false if an error occurred or the input connection is invalid.
*/
fun sendSystemKeyEventAlt(keyEventCode: Int): Boolean {
return sendDownUpKeyEvent(keyEventCode, KeyEvent.META_ALT_LEFT_ON)
}
/**
* Same as [InputMethodService.sendDownUpKeyEvents] but also allows to set meta state.
*
* @param keyEventCode The key code to send, use a key code defined in Android's [KeyEvent].
* @param metaState Flags indicating which meta keys are currently pressed.
* @return True on success, false if an error occurred or the input connection is invalid.
*/
private fun sendDownUpKeyEvent(keyEventCode: Int, metaState: Int): Boolean {
private fun sendDownKeyEvent(eventTime: Long, keyEventCode: Int, metaState: Int): Boolean {
val ic = inputConnection ?: return false
val eventTime = SystemClock.uptimeMillis()
return ic.sendKeyEvent(
KeyEvent(
eventTime,
@@ -536,9 +549,15 @@ class EditorInstance private constructor(
metaState,
KeyCharacterMap.VIRTUAL_KEYBOARD,
0,
KeyEvent.FLAG_SOFT_KEYBOARD or KeyEvent.FLAG_KEEP_TOUCH_MODE
KeyEvent.FLAG_SOFT_KEYBOARD or KeyEvent.FLAG_KEEP_TOUCH_MODE,
InputDevice.SOURCE_KEYBOARD
)
) && ic.sendKeyEvent(
)
}
private fun sendUpKeyEvent(eventTime: Long, keyEventCode: Int, metaState: Int): Boolean {
val ic = inputConnection ?: return false
return ic.sendKeyEvent(
KeyEvent(
eventTime,
SystemClock.uptimeMillis(),
@@ -548,10 +567,52 @@ class EditorInstance private constructor(
metaState,
KeyCharacterMap.VIRTUAL_KEYBOARD,
0,
KeyEvent.FLAG_SOFT_KEYBOARD or KeyEvent.FLAG_KEEP_TOUCH_MODE
KeyEvent.FLAG_SOFT_KEYBOARD or KeyEvent.FLAG_KEEP_TOUCH_MODE,
InputDevice.SOURCE_KEYBOARD
)
)
}
/**
* Same as [InputMethodService.sendDownUpKeyEvents] but also allows to set meta state.
*
* @param keyEventCode The key code to send, use a key code defined in Android's [KeyEvent].
* @param metaState Flags indicating which meta keys are currently pressed.
* @param count How often the key is pressed while the meta keys passed are down. Must be greater than or equal to
* `1`, else this method will immediately return false.
*
* @return True on success, false if an error occurred or the input connection is invalid.
*/
fun sendDownUpKeyEvent(keyEventCode: Int, metaState: Int = meta(), count: Int = 1): Boolean {
if (count < 1) return false
val ic = inputConnection ?: return false
ic.beginBatchEdit()
val eventTime = SystemClock.uptimeMillis()
if (metaState and KeyEvent.META_CTRL_ON > 0) {
sendDownKeyEvent(eventTime, KeyEvent.KEYCODE_CTRL_LEFT, 0)
}
if (metaState and KeyEvent.META_ALT_ON > 0) {
sendDownKeyEvent(eventTime, KeyEvent.KEYCODE_ALT_LEFT, 0)
}
if (metaState and KeyEvent.META_SHIFT_ON > 0) {
sendDownKeyEvent(eventTime, KeyEvent.KEYCODE_SHIFT_LEFT, 0)
}
for (n in 0 until count) {
sendDownKeyEvent(eventTime, keyEventCode, metaState)
sendUpKeyEvent(eventTime, keyEventCode, metaState)
}
if (metaState and KeyEvent.META_SHIFT_ON > 0) {
sendUpKeyEvent(eventTime, KeyEvent.KEYCODE_SHIFT_LEFT, 0)
}
if (metaState and KeyEvent.META_ALT_ON > 0) {
sendUpKeyEvent(eventTime, KeyEvent.KEYCODE_ALT_LEFT, 0)
}
if (metaState and KeyEvent.META_CTRL_ON > 0) {
sendUpKeyEvent(eventTime, KeyEvent.KEYCODE_CTRL_LEFT, 0)
}
ic.endBatchEdit()
return true
}
}
/**
@@ -893,7 +954,7 @@ class CachedInput(private val editorInstance: EditorInstance) {
* the target app's editor text is bigger than [CACHED_TEXT_N_CHARS_BEFORE_CURSOR] and
* [CACHED_TEXT_N_CHARS_AFTER_CURSOR], but always caches the relevant text around the cursor.
*/
var rawText: String = ""
var rawText: StringBuilder = StringBuilder()
private set
companion object {
@@ -902,6 +963,10 @@ class CachedInput(private val editorInstance: EditorInstance) {
private val WORD_EVAL_REGEX = """[^\p{L}\']""".toRegex()
private val WORD_SPLIT_REGEX_EN = """((?<=$WORD_EVAL_REGEX)|(?=$WORD_EVAL_REGEX))""".toRegex()
fun isWordComponent(string: String): Boolean {
return !WORD_EVAL_REGEX.matches(string)
}
}
/**
@@ -945,18 +1010,18 @@ class CachedInput(private val editorInstance: EditorInstance) {
val ic = inputConnection
if (ic == null) {
offset = 0
rawText = ""
rawText.clear()
expectedMaxLength = 0
} else {
val textBefore = getTextBeforeCursor(CACHED_TEXT_N_CHARS_BEFORE_CURSOR)
val textSelected = ic.getSelectedText(0) ?: ""
val textAfter = getTextAfterCursor(CACHED_TEXT_N_CHARS_AFTER_CURSOR)
offset = (selection.start - textBefore.length).coerceAtLeast(0)
rawText = StringBuilder().run {
rawText.apply {
clear()
append(textBefore)
append(textSelected)
append(textAfter)
toString()
}
expectedMaxLength = offset + rawText.length
}

View File

@@ -0,0 +1,117 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.core
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.viewbinding.ViewBinding
import com.google.android.material.snackbar.Snackbar
import dev.patrickgold.florisboard.R
abstract class FlorisActivity<V : ViewBinding> : AppCompatActivity() {
private var _binding: V? = null
protected val binding: V
get() = _binding!!
private var _prefs: PrefHelper? = null
protected val prefs: PrefHelper
get() = _prefs!!
private var errorDialog: AlertDialog? = null
private var errorSnackbar: Snackbar? = null
private var errorThrowable: Throwable? = null
private var messageSnackbar: Snackbar? = null
protected abstract fun onCreateBinding(): V
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_prefs = PrefHelper.getDefaultInstance(applicationContext)
onCreateBinding().let {
_binding = it
setContentView(it.root)
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
_prefs = null
errorDialog?.dismiss()
errorDialog = null
errorSnackbar?.dismiss()
errorSnackbar = null
errorThrowable = null
messageSnackbar?.dismiss()
messageSnackbar = null
}
protected fun showMessage(@StringRes snackbarMessageResId: Int) {
val snackbarMessage = resources.getString(snackbarMessageResId)
showMessage(snackbarMessage)
}
protected fun showMessage(snackbarMessage: String) {
messageSnackbar?.dismiss()
messageSnackbar = Snackbar.make(binding.root, snackbarMessage, Snackbar.LENGTH_LONG).apply {
setAction(android.R.string.ok) {
messageSnackbar?.dismiss()
}
show() }
}
protected fun showError(throwable: Throwable) {
val snackbarMessage = resources.getString(R.string.assets__error__snackbar_message)
showError(snackbarMessage, throwable)
}
protected fun showError(@StringRes snackbarMessageResId: Int, throwable: Throwable) {
val snackbarMessage = resources.getString(snackbarMessageResId)
showError(snackbarMessage, throwable)
}
protected fun showError(snackbarMessage: String, throwable: Throwable) {
errorDialog?.dismiss()
errorDialog = null
errorSnackbar?.dismiss()
errorSnackbar = Snackbar.make(binding.root, snackbarMessage, Snackbar.LENGTH_LONG).apply {
setAction(R.string.assets__error__details) {
errorDialog?.dismiss()
errorDialog = AlertDialog.Builder(this@FlorisActivity).run {
setTitle(R.string.assets__error__details)
setMessage(errorThrowable.toString())
setPositiveButton(android.R.string.ok, null)
setNeutralButton(R.string.crash_dialog__copy_to_clipboard) { _, _ ->
val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE)
if (clipboardManager != null && clipboardManager is ClipboardManager) {
clipboardManager.setPrimaryClip(ClipData.newPlainText(errorThrowable.toString(), errorThrowable.toString()))
}
}
create()
show()
}
}
show()
}
errorThrowable = throwable
}
}

View File

@@ -17,10 +17,8 @@
package dev.patrickgold.florisboard.ime.core
import android.annotation.SuppressLint
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.graphics.Color
import android.inputmethodservice.ExtractEditText
@@ -39,11 +37,15 @@ import android.widget.Button
import android.widget.FrameLayout
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.lifecycle.*
import com.squareup.moshi.Json
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.clip.ClipboardInputManager
import dev.patrickgold.florisboard.ime.clip.FlorisClipboardManager
import dev.patrickgold.florisboard.ime.landscapeinput.LandscapeInputUiMode
import dev.patrickgold.florisboard.ime.media.MediaInputManager
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
import dev.patrickgold.florisboard.ime.popup.PopupLayerView
import dev.patrickgold.florisboard.ime.text.TextInputManager
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
@@ -57,6 +59,9 @@ import dev.patrickgold.florisboard.util.*
import timber.log.Timber
import java.lang.ref.WeakReference
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.Executor
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
/**
* Variable which holds the current [FlorisBoard] instance. To get this instance from another
@@ -68,13 +73,16 @@ private var florisboardInstance: FlorisBoard? = null
* Core class responsible to link together both the text and media input managers as well as
* managing the one-handed UI.
*/
class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedListener,
class FlorisBoard : InputMethodService(), LifecycleOwner, FlorisClipboardManager.OnPrimaryClipChangedListener,
ThemeManager.OnThemeUpdatedListener {
lateinit var prefs: PrefHelper
private set
val context: Context
get() = inputWindowView?.context ?: this
private val serviceLifecycleDispatcher: ServiceLifecycleDispatcher = ServiceLifecycleDispatcher(this)
private var extractEditLayout: WeakReference<ViewGroup?> = WeakReference(null)
var inputView: InputView? = null
private set
@@ -85,11 +93,22 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
private var audioManager: AudioManager? = null
var imeManager:InputMethodManager? = null
var clipboardManager: ClipboardManager? = null
var florisClipboardManager: FlorisClipboardManager? = null
private val themeManager: ThemeManager = ThemeManager.default()
private var vibrator: Vibrator? = null
private val osHandler = Handler()
private var internalBatchNestingLevel: Int = 0
private val internalSelectionCache = object {
var selectionCatchCount: Int = 0
var oldSelStart: Int = -1
var oldSelEnd: Int = -1
var newSelStart: Int = -1
var newSelEnd: Int = -1
var candidatesStart: Int = -1
var candidatesEnd: Int = -1
}
var activeEditorInstance: EditorInstance = EditorInstance.default()
lateinit var subtypeManager: SubtypeManager
@@ -97,31 +116,42 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
private var currentThemeIsNight: Boolean = false
private var currentThemeResId: Int = 0
private var isNumberRowVisible: Boolean = false
private var isWindowShown: Boolean = false
val textInputManager: TextInputManager
val mediaInputManager: MediaInputManager
val clipInputManager: ClipboardInputManager
var isClipboardContextMenuShown = false
init {
florisboardInstance = this
textInputManager = TextInputManager.getInstance()
mediaInputManager = MediaInputManager.getInstance()
clipInputManager = ClipboardInputManager.getInstance()
}
lateinit var asyncExecutor: ExecutorService
companion object {
private const val IME_ID: String = "dev.patrickgold.florisboard/.ime.core.FlorisBoard"
private const val IME_ID_BETA: String = "dev.patrickgold.florisboard.beta/dev.patrickgold.florisboard.ime.core.FlorisBoard"
private const val IME_ID_DEBUG: String = "dev.patrickgold.florisboard.debug/dev.patrickgold.florisboard.ime.core.FlorisBoard"
fun checkIfImeIsEnabled(context: Context): Boolean {
val activeImeIds = Settings.Secure.getString(
context.contentResolver,
Settings.Secure.ENABLED_INPUT_METHODS
)
) ?: "(none)"
Timber.i("List of active IMEs: $activeImeIds")
return when {
BuildConfig.DEBUG -> {
activeImeIds.split(":").contains(IME_ID_DEBUG)
}
context.packageName.endsWith(".beta") -> {
activeImeIds.split(":").contains(IME_ID_BETA)
}
else -> {
activeImeIds.split(":").contains(IME_ID)
}
@@ -132,12 +162,15 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
val selectedImeId = Settings.Secure.getString(
context.contentResolver,
Settings.Secure.DEFAULT_INPUT_METHOD
)
) ?: "(none)"
Timber.i("Selected IME: $selectedImeId")
return when {
BuildConfig.DEBUG -> {
selectedImeId == IME_ID_DEBUG
}
context.packageName.endsWith(".beta") -> {
selectedImeId.split(":").contains(IME_ID_BETA)
}
else -> {
selectedImeId == IME_ID
}
@@ -162,6 +195,10 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
}
}
override fun getLifecycle(): Lifecycle {
return serviceLifecycleDispatcher.lifecycle
}
override fun onCreate() {
/*if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
@@ -182,11 +219,10 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
)
}*/
Timber.i("onCreate()")
serviceLifecycleDispatcher.onServicePreSuperOnCreate()
imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
audioManager = getSystemService(Context.AUDIO_SERVICE) as? AudioManager
clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
clipboardManager?.addPrimaryClipChangedListener(this)
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
prefs = PrefHelper.getDefaultInstance(this)
prefs.initDefaultPreferences()
@@ -202,6 +238,11 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
asyncExecutor = Executors.newSingleThreadExecutor()
florisClipboardManager = FlorisClipboardManager.getInstance()
florisClipboardManager!!.initialize(context)
florisClipboardManager?.addPrimaryClipChangedListener(this)
super.onCreate()
eventListeners.toList().forEach { it?.onCreate() }
}
@@ -249,6 +290,22 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
return eel
}
override fun onDestroy() {
Timber.i("onDestroy()")
themeManager.unregisterOnThemeUpdatedListener(this)
florisClipboardManager!!.removePrimaryClipChangedListener(this)
florisClipboardManager!!.close()
osHandler.removeCallbacksAndMessages(null)
florisboardInstance = null
serviceLifecycleDispatcher.onServicePreSuperOnDestroy()
eventListeners.toList().forEach { it?.onDestroy() }
eventListeners.clear()
super.onDestroy()
}
override fun onEvaluateFullscreenMode(): Boolean {
return resources?.configuration?.let { config ->
if (config.orientation != Configuration.ORIENTATION_LANDSCAPE) {
@@ -286,7 +343,6 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
}
}
this.inputView = inputView
initializeOneHandedEnvironment()
updateSoftInputWindowLayoutParameters()
updateOneHandedPanelVisibility()
themeManager.notifyCallbackReceivers()
@@ -295,24 +351,10 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
eventListeners.toList().forEach { it?.onRegisterInputView(inputView) }
}
override fun onDestroy() {
Timber.i("onDestroy()")
themeManager.unregisterOnThemeUpdatedListener(this)
clipboardManager?.removePrimaryClipChangedListener(this)
osHandler.removeCallbacksAndMessages(null)
florisboardInstance = null
eventListeners.toList().forEach { it?.onDestroy() }
eventListeners.clear()
super.onDestroy()
}
override fun onStartInput(attribute: EditorInfo?, restarting: Boolean) {
Timber.i("onStartInput($attribute, $restarting)")
super.onStartInput(attribute, restarting)
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_IMMEDIATE)
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_MONITOR)
}
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
@@ -346,7 +388,13 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
}
override fun onWindowShown() {
Timber.i("onWindowShown()")
if (isWindowShown) {
Timber.i("Ignoring onWindowShown()")
return
} else {
Timber.i("onWindowShown()")
}
isWindowShown = true
prefs.sync()
val newIsNumberRowVisible = prefs.keyboard.numberRow
@@ -365,7 +413,13 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
}
override fun onWindowHidden() {
Timber.i("onWindowHidden()")
if (!isWindowShown) {
Timber.i("Ignoring onWindowHidden()")
return
} else {
Timber.i("onWindowHidden()")
}
isWindowShown = false
super.onWindowHidden()
eventListeners.toList().forEach { it?.onWindowHidden() }
@@ -380,24 +434,74 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
super.onConfigurationChanged(newConfig)
}
/**
* Begins a FlorisBoard internal batch edit. This enables the application to continue sending selection updates
* (some apps need to to this else they absolutely refuse to give visual feedback on cursor movement etc.). The
* selection update is then caught if [internalBatchNestingLevel] is greater than 0, thus not delegating the
* update to the editor instance. This is needed because else the UI stutters when too many updates arrive in a
* row.
*/
fun beginInternalBatchEdit() {
internalBatchNestingLevel++
}
/**
* Ends an internal batch edit, if [internalBatchNestingLevel] is <= 1 and calls [onUpdateSelection] with the
* corresponding reported selection values. This call is not caught and the editor instance and other classes are
* able to update the UI. Resets the internal selection cache and is ready for the next batch edit.
*/
fun endInternalBatchEdit() {
internalBatchNestingLevel = (internalBatchNestingLevel - 1).coerceAtLeast(0)
if (internalBatchNestingLevel == 0) {
internalSelectionCache.apply {
if (selectionCatchCount > 0) {
onUpdateSelection(
oldSelStart, oldSelEnd,
newSelStart, newSelEnd,
candidatesStart, candidatesEnd
)
selectionCatchCount = 0
oldSelStart = -1
oldSelEnd = -1
newSelStart = -1
newSelEnd = -1
candidatesStart = -1
candidatesEnd = -1
}
}
}
}
override fun onUpdateSelection(
oldSelStart: Int, oldSelEnd: Int,
newSelStart: Int, newSelEnd: Int,
candidatesStart: Int, candidatesEnd: Int
) {
Timber.i("onUpdateSelection($oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd)")
super.onUpdateSelection(
oldSelStart, oldSelEnd,
newSelStart, newSelEnd,
candidatesStart, candidatesEnd
)
activeEditorInstance.onUpdateSelection(
oldSelStart, oldSelEnd,
newSelStart, newSelEnd,
candidatesStart, candidatesEnd
)
eventListeners.toList().forEach { it?.onUpdateSelection() }
if (internalBatchNestingLevel == 0) {
Timber.i("onUpdateSelection($oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd)")
activeEditorInstance.onUpdateSelection(
oldSelStart, oldSelEnd,
newSelStart, newSelEnd,
candidatesStart, candidatesEnd
)
eventListeners.toList().forEach { it?.onUpdateSelection() }
} else {
Timber.i("onUpdateSelection($oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd): caught due to internal batch level of $internalBatchNestingLevel!")
if (internalSelectionCache.selectionCatchCount++ == 0) {
internalSelectionCache.oldSelStart = oldSelStart
internalSelectionCache.oldSelEnd = oldSelEnd
}
internalSelectionCache.newSelStart = newSelStart
internalSelectionCache.newSelEnd = newSelEnd
internalSelectionCache.candidatesStart = candidatesStart
internalSelectionCache.candidatesEnd = candidatesEnd
}
}
override fun onThemeUpdated(theme: Theme) {
@@ -439,14 +543,6 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
// Update InputView theme
inputView?.setBackgroundColor(theme.getAttr(Theme.Attr.KEYBOARD_BACKGROUND).toSolidColor().color)
inputView?.oneHandedCtrlPanelStart?.setBackgroundColor(theme.getAttr(Theme.Attr.ONE_HANDED_BACKGROUND).toSolidColor().color)
inputView?.oneHandedCtrlPanelEnd?.setBackgroundColor(theme.getAttr(Theme.Attr.ONE_HANDED_BACKGROUND).toSolidColor().color)
ColorStateList.valueOf(theme.getAttr(Theme.Attr.ONE_HANDED_FOREGROUND).toSolidColor().color).also {
inputView?.oneHandedCtrlMoveStart?.imageTintList = it
inputView?.oneHandedCtrlMoveEnd?.imageTintList = it
inputView?.oneHandedCtrlCloseStart?.imageTintList = it
inputView?.oneHandedCtrlCloseEnd?.imageTintList = it
}
inputView?.invalidate()
// Update ExtractTextView theme and attributes
@@ -484,6 +580,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
val inputView = this.inputView ?: return
val inputWindowView = this.inputWindowView ?: return
// TODO: Check also if the keyboard is currently suppressed by a hardware keyboard
if (!isInputViewShown) {
outInsets?.contentTopInsets = inputWindowView.height
outInsets?.visibleTopInsets = inputWindowView.height
@@ -492,6 +589,11 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
val visibleTopY = inputWindowView.height - inputView.measuredHeight
outInsets?.contentTopInsets = visibleTopY
outInsets?.visibleTopInsets = visibleTopY
if (isClipboardContextMenuShown) {
outInsets?.touchableInsets = Insets.TOUCHABLE_INSETS_FRAME
outInsets?.touchableRegion?.setEmpty()
}
}
/**
@@ -586,7 +688,7 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
i.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED or
Intent.FLAG_ACTIVITY_CLEAR_TOP
startActivity(i)
applicationContext.startActivity(i)
}
/**
@@ -641,71 +743,47 @@ class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedL
private fun onSubtypeChanged(newSubtype: Subtype) {
textInputManager.onSubtypeChanged(newSubtype)
mediaInputManager.onSubtypeChanged(newSubtype)
clipInputManager.onSubtypeChanged(newSubtype)
}
fun setActiveInput(type: Int) {
when (type) {
R.id.text_input -> {
inputView?.mainViewFlipper?.displayedChild = 0
textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(KeyData.VIEW_CHARACTERS))
}
R.id.media_input -> {
inputView?.mainViewFlipper?.displayedChild = 1
}
}
}
private fun initializeOneHandedEnvironment() {
{ v:View -> onOneHandedPanelButtonClick(v) }.also {
inputView?.oneHandedCtrlMoveStart?.setOnClickListener(it)
inputView?.oneHandedCtrlMoveEnd?.setOnClickListener(it)
inputView?.oneHandedCtrlCloseStart?.setOnClickListener(it)
inputView?.oneHandedCtrlCloseEnd?.setOnClickListener(it)
}
}
private fun onOneHandedPanelButtonClick(v: View) {
when (v.id) {
R.id.one_handed_ctrl_move_start -> {
prefs.keyboard.oneHandedMode = "start"
}
R.id.one_handed_ctrl_move_end -> {
prefs.keyboard.oneHandedMode = "end"
}
R.id.one_handed_ctrl_close_start,
R.id.one_handed_ctrl_close_end -> {
prefs.keyboard.oneHandedMode = "off"
R.id.clip_input -> {
inputView?.mainViewFlipper?.displayedChild = 2
}
}
updateOneHandedPanelVisibility()
}
fun toggleOneHandedMode(isRight: Boolean) {
when (prefs.keyboard.oneHandedMode) {
"off" -> {
prefs.keyboard.oneHandedMode = if (isRight) { "end" } else { "start" }
}
else -> {
prefs.keyboard.oneHandedMode = "off"
}
prefs.keyboard.oneHandedMode = when (prefs.keyboard.oneHandedMode) {
OneHandedMode.OFF -> if (isRight) { OneHandedMode.END } else { OneHandedMode.START }
else -> OneHandedMode.OFF
}
updateOneHandedPanelVisibility()
}
private fun updateOneHandedPanelVisibility() {
fun updateOneHandedPanelVisibility() {
if (resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) {
inputView?.oneHandedCtrlPanelStart?.visibility = View.GONE
inputView?.oneHandedCtrlPanelEnd?.visibility = View.GONE
} else {
when (prefs.keyboard.oneHandedMode) {
"off" -> {
OneHandedMode.OFF -> {
inputView?.oneHandedCtrlPanelStart?.visibility = View.GONE
inputView?.oneHandedCtrlPanelEnd?.visibility = View.GONE
}
"start" -> {
OneHandedMode.START -> {
inputView?.oneHandedCtrlPanelStart?.visibility = View.GONE
inputView?.oneHandedCtrlPanelEnd?.visibility = View.VISIBLE
}
"end" -> {
OneHandedMode.END -> {
inputView?.oneHandedCtrlPanelStart?.visibility = View.VISIBLE
inputView?.oneHandedCtrlPanelEnd?.visibility = View.GONE
}

View File

@@ -0,0 +1,373 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.core
import android.os.SystemClock
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyData
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
/**
* The main logic point of processing input events and delegating them to the registered event receivers. Currently,
* only [InputKeyEvent]s are supported, but in the future this class is thought to be the single point where input
* events can be dispatched.
*/
class InputEventDispatcher private constructor(
parentScope: CoroutineScope,
channelCapacity: Int,
private val mainDispatcher: CoroutineDispatcher,
private val defaultDispatcher: CoroutineDispatcher,
private val repeatableKeyCodes: IntArray
) : InputKeyEventSender {
private val channel: Channel<InputKeyEvent> = Channel(channelCapacity)
private val scope: CoroutineScope = CoroutineScope(parentScope.coroutineContext)
private val pressedKeys: HashMap<Int, PressedKeyInfo> = hashMapOf()
var lastKeyEventDown: InputKeyEvent? = null
private set
var lastKeyEventUp: InputKeyEvent? = null
private set
/**
* The input key event register. If null, the dispatcher will still process input, but won't dispatch them to an
* event receiver.
*/
var keyEventReceiver: InputKeyEventReceiver? = null
companion object {
/**
* The default input event channel capacity to be used in [new].
*/
private const val DEFAULT_CHANNEL_CAPACITY: Int = 32
/**
* Creates a new [InputEventDispatcher] instance from given arguments and returns it.
*
* @param parentScope The parent coroutine scope which this dispatcher will attach its own scope to.
* @param channelCapacity The capacity of this input channel, defaults to [DEFAULT_CHANNEL_CAPACITY].
* @param mainDispatcher The main dispatcher used to switch the context to call the receiver callbacks.
* Defaults to [Dispatchers.Main].
* @param defaultDispatcher The default dispatcher used to switch the context to call the receiver callbacks.
* Defaults to [Dispatchers.Default].
* @param repeatableKeyCodes An int array of all key codes which are repeatable while being pressed down.
*
* @return A new [InputEventDispatcher] instance initialized with given arguments.
*/
fun new(
parentScope: CoroutineScope,
channelCapacity: Int = DEFAULT_CHANNEL_CAPACITY,
mainDispatcher: CoroutineDispatcher = Dispatchers.Main,
defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
repeatableKeyCodes: IntArray = intArrayOf()
): InputEventDispatcher = InputEventDispatcher(
parentScope, channelCapacity, mainDispatcher, defaultDispatcher, repeatableKeyCodes.clone()
)
}
init {
scope.launch(defaultDispatcher) {
for (ev in channel) {
if (!isActive) break
val startTime = System.nanoTime()
if (BuildConfig.DEBUG) {
Timber.d(ev.toString())
}
when (ev.action) {
InputKeyEvent.Action.DOWN -> {
if (pressedKeys.containsKey(ev.data.code)) continue
pressedKeys[ev.data.code] = PressedKeyInfo(
eventTimeDown = ev.eventTime,
repeatKeyPressJob = if (!repeatableKeyCodes.contains(ev.data.code)) { null } else {
scope.launch(defaultDispatcher) {
delay(600)
while (isActive) {
channel.send(InputKeyEvent.repeat(ev.data))
delay(50)
}
}
}
)
withContext(mainDispatcher) {
keyEventReceiver?.onInputKeyDown(ev)
}
if (ev.data.code != KeyCode.INTERNAL_BATCH_EDIT) {
lastKeyEventDown = ev
}
}
InputKeyEvent.Action.DOWN_UP -> {
pressedKeys.remove(ev.data.code)?.repeatKeyPressJob?.cancel()
withContext(mainDispatcher) {
keyEventReceiver?.onInputKeyDown(ev)
keyEventReceiver?.onInputKeyUp(ev)
}
if (ev.data.code != KeyCode.INTERNAL_BATCH_EDIT) {
lastKeyEventDown = ev
lastKeyEventUp = ev
}
}
InputKeyEvent.Action.UP -> {
pressedKeys.remove(ev.data.code)?.repeatKeyPressJob?.cancel()
withContext(mainDispatcher) {
keyEventReceiver?.onInputKeyUp(ev)
}
if (ev.data.code != KeyCode.INTERNAL_BATCH_EDIT) {
lastKeyEventUp = ev
}
}
InputKeyEvent.Action.REPEAT -> {
if (pressedKeys.containsKey(ev.data.code)) {
withContext(mainDispatcher) {
keyEventReceiver?.onInputKeyRepeat(ev)
}
}
}
InputKeyEvent.Action.CANCEL -> {
pressedKeys.remove(ev.data.code)?.repeatKeyPressJob?.cancel()
withContext(mainDispatcher) {
keyEventReceiver?.onInputKeyCancel(ev)
}
}
}
if (BuildConfig.DEBUG) {
Timber.d("Time elapsed: ${(System.nanoTime() - startTime) / 1_000_000}")
}
}
val pressedKeysIterator = pressedKeys.iterator()
while (pressedKeysIterator.hasNext()) {
pressedKeysIterator.next().value.repeatKeyPressJob?.cancel()
pressedKeysIterator.remove()
}
}
}
override fun send(ev: InputKeyEvent) {
scope.launch(mainDispatcher) {
channel.send(ev)
}
}
/**
* Checks if there's currently a key down with given [code].
*
* @param code The key code to check for.
*
* @return True if the given [code] is currently down, false otherwise.
*/
fun isPressed(code: Int): Boolean {
return pressedKeys.containsKey(code)
}
/**
* Closes this dispatcher and cancels the local coroutine scope.
*/
fun close() {
keyEventReceiver = null
scope.cancel()
}
data class PressedKeyInfo(
val eventTimeDown: Long,
val repeatKeyPressJob: Job?
)
}
/**
* Data class representing a single input key event.
*
* @property eventTime The exact event time when this event occurred, measured in milliseconds since a static point in
* the past. The exact point is irrelevant, but while this input dispatcher is active, the point must not change in
* order for difference time calculation to succeed.
* @property action The action of this event.
* @property data The data of this event.
* @property count The count how often this event occurred. Is only respected by other methods if the [action] of this
* event is [Action.DOWN_UP] or [Action.REPEAT], else always 1 is assumed.
*/
data class InputKeyEvent(
val eventTime: Long,
val action: Action,
val data: KeyData,
val count: Int
) {
companion object {
/**
* Creates a new input key event with given [keyData] and sets the action to [Action.DOWN].
*
* @param keyData The key data of the input key event event to create.
*
* @return The created input key event.
*/
fun down(keyData: KeyData): InputKeyEvent {
return InputKeyEvent(
eventTime = SystemClock.uptimeMillis(),
action = Action.DOWN,
data = keyData,
count = 1
)
}
/**
* Creates a new input key event with given [keyData] and sets the action to [Action.DOWN_UP].
*
* @param keyData The key data of the input key event event to create.
* @param count How often this event occurred. Must be grater or equal to 1, defaults to 1.
*
* @return The created input key event.
*/
fun downUp(keyData: KeyData, count: Int = 1): InputKeyEvent {
return InputKeyEvent(
eventTime = SystemClock.uptimeMillis(),
action = Action.DOWN_UP,
data = keyData,
count = count
)
}
/**
* Creates a new input key event with given [keyData] and sets the action to [Action.UP].
*
* @param keyData The key data of the input key event event to create.
*
* @return The created input key event.
*/
fun up(keyData: KeyData): InputKeyEvent {
return InputKeyEvent(
eventTime = SystemClock.uptimeMillis(),
action = Action.UP,
data = keyData,
count = 1
)
}
/**
* Creates a new input key event with given [keyData] and sets the action to [Action.REPEAT].
*
* @param keyData The key data of the input key event event to create.
* @param count How often this event occurred. Must be grater or equal to 1, defaults to 1.
*
* @return The created input key event.
*/
fun repeat(keyData: KeyData, count: Int = 1): InputKeyEvent {
return InputKeyEvent(
eventTime = SystemClock.uptimeMillis(),
action = Action.REPEAT,
data = keyData,
count = count
)
}
/**
* Creates a new input key event with given [keyData] and sets the action to [Action.CANCEL].
*
* @param keyData The key data of the input key event event to create.
*
* @return The created input key event.
*/
fun cancel(keyData: KeyData): InputKeyEvent {
return InputKeyEvent(
eventTime = SystemClock.uptimeMillis(),
action = Action.CANCEL,
data = keyData,
count = 1
)
}
}
/**
* Checks if the [other] input key event is a consecutive event while respecting [maxEventTimeDiff].
*
* @param other The other input key event to compare with this one.
* @param maxEventTimeDiff The maximum event time diff between this event and [other], in milliseconds.
*
* @return True if this event is a consecutive event of [other], false otherwise.
*/
fun isConsecutiveEventOf(other: InputKeyEvent?, maxEventTimeDiff: Long): Boolean {
return other != null && data.code == other.data.code && eventTime - other.eventTime <= maxEventTimeDiff
}
/**
* Returns a string representation of this input key event.
*/
override fun toString(): String {
return "FlorisKeyEvent { eventTime=${eventTime}ms, action=$action, data=$data, count=$count }"
}
/**
* The action of an input key event.
*/
enum class Action {
DOWN,
DOWN_UP,
UP,
REPEAT,
CANCEL,
}
}
/**
* Interface which represents an input key event sender.
*/
interface InputKeyEventSender {
/**
* Sends given input key event [ev] to the underlying input channel, awaiting to be processed.
*
* @param ev The input key event to send.
*/
fun send(ev: InputKeyEvent)
}
/**
* Interface which represents an input key event receiver.
*/
interface InputKeyEventReceiver {
/**
* Event method which gets called when a key went down.
*
* @param ev The associated input key event.
*/
fun onInputKeyDown(ev: InputKeyEvent)
/**
* Event method which gets called when a key went up.
*
* @param ev The associated input key event.
*/
fun onInputKeyUp(ev: InputKeyEvent)
/**
* Event method which gets called when a key is called repeatedly while being pressed down.
*
* @param ev The associated input key event.
*/
fun onInputKeyRepeat(ev: InputKeyEvent)
/**
* Event method which gets called when a key press is cancelled.
*
* @param ev The associated input key event.
*/
fun onInputKeyCancel(ev: InputKeyEvent)
}

View File

@@ -20,10 +20,11 @@ import android.content.Context
import android.content.res.Configuration
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.widget.ImageButton
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.ViewFlipper
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
import dev.patrickgold.florisboard.ime.text.key.KeyVariation
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
import dev.patrickgold.florisboard.util.ViewLayoutUtils
@@ -45,20 +46,16 @@ class InputView : LinearLayout {
private set
var desiredMediaKeyboardViewHeight: Float = resources.getDimension(R.dimen.mediaKeyboardView_baseHeight)
private set
var heightFactor: Float = 1.0f
private set
var shouldGiveAdditionalSpace: Boolean = false
private set
var mainViewFlipper: ViewFlipper? = null
private set
var oneHandedCtrlPanelStart: LinearLayout? = null
var oneHandedCtrlPanelStart: ViewGroup? = null
private set
var oneHandedCtrlPanelEnd: LinearLayout? = null
private set
var oneHandedCtrlMoveStart: ImageButton? = null
private set
var oneHandedCtrlMoveEnd: ImageButton? = null
private set
var oneHandedCtrlCloseStart: ImageButton? = null
private set
var oneHandedCtrlCloseEnd: ImageButton? = null
var oneHandedCtrlPanelEnd: ViewGroup? = null
private set
constructor(context: Context) : this(context, null)
@@ -77,19 +74,15 @@ class InputView : LinearLayout {
mainViewFlipper = findViewById(R.id.main_view_flipper)
oneHandedCtrlPanelStart = findViewById(R.id.one_handed_ctrl_panel_start)
oneHandedCtrlPanelEnd = findViewById(R.id.one_handed_ctrl_panel_end)
oneHandedCtrlMoveStart = findViewById(R.id.one_handed_ctrl_move_start)
oneHandedCtrlMoveEnd = findViewById(R.id.one_handed_ctrl_move_end)
oneHandedCtrlCloseStart = findViewById(R.id.one_handed_ctrl_close_start)
oneHandedCtrlCloseEnd = findViewById(R.id.one_handed_ctrl_close_end)
florisboard.registerInputView(this)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val heightFactor = when (resources.configuration.orientation) {
heightFactor = when (resources.configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> 1.0f
else -> if (prefs.keyboard.oneHandedMode != "off") {
0.9f
else -> if (prefs.keyboard.oneHandedMode != OneHandedMode.OFF) {
prefs.keyboard.oneHandedModeScaleFactor / 100.0f
} else {
1.0f
}
@@ -108,12 +101,12 @@ class InputView : LinearLayout {
var baseSmartbarHeight = 0.16129f * baseHeight
var baseTextInputHeight = baseHeight - baseSmartbarHeight
val tim = florisboard.textInputManager
val shouldGiveAdditionalSpace = prefs.keyboard.numberRow &&
shouldGiveAdditionalSpace = prefs.keyboard.numberRow &&
!(tim.getActiveKeyboardMode() == KeyboardMode.NUMERIC ||
tim.getActiveKeyboardMode() == KeyboardMode.PHONE ||
tim.getActiveKeyboardMode() == KeyboardMode.PHONE2)
if (shouldGiveAdditionalSpace) {
val additionalHeight = desiredTextKeyboardViewHeight * 0.18f
val additionalHeight = baseTextInputHeight * 0.25f
baseHeight += additionalHeight
baseTextInputHeight += additionalHeight
}
@@ -171,5 +164,4 @@ class InputView : LinearLayout {
resources.getDimension(R.dimen.inputView_baseHeight)
)
}
}

View File

@@ -22,11 +22,13 @@ import android.provider.Settings
import androidx.preference.PreferenceManager
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.landscapeinput.LandscapeInputUiMode
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
import dev.patrickgold.florisboard.ime.text.gestures.DistanceThreshold
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.gestures.VelocityThreshold
import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
import dev.patrickgold.florisboard.ime.text.key.UtilityKeyAction
import dev.patrickgold.florisboard.ime.text.smartbar.CandidateView
import dev.patrickgold.florisboard.ime.theme.ThemeMode
import dev.patrickgold.florisboard.util.TimeUtil
import dev.patrickgold.florisboard.util.VersionName
@@ -52,6 +54,7 @@ class PrefHelper(
val smartbar = Smartbar(this)
val suggestion = Suggestion(this)
val theme = Theme(this)
val clipboard = Clipboard(this)
/**
* Checks the cache if an entry for [key] exists, else calls [getPrefInternal] to retrieve the
@@ -309,26 +312,28 @@ class PrefHelper(
class Keyboard(private val prefHelper: PrefHelper) {
companion object {
const val BOTTOM_OFFSET_PORTRAIT = "keyboard__bottom_offset_portrait"
const val BOTTOM_OFFSET_LANDSCAPE = "keyboard__bottom_offset_landscape"
const val FONT_SIZE_MULTIPLIER_PORTRAIT = "keyboard__font_size_multiplier_portrait"
const val FONT_SIZE_MULTIPLIER_LANDSCAPE = "keyboard__font_size_multiplier_landscape"
const val HEIGHT_FACTOR = "keyboard__height_factor"
const val HEIGHT_FACTOR_CUSTOM = "keyboard__height_factor_custom"
const val HINTED_NUMBER_ROW_MODE = "keyboard__hinted_number_row_mode"
const val HINTED_SYMBOLS_MODE = "keyboard__hinted_symbols_mode"
const val KEY_SPACING_HORIZONTAL = "keyboard__key_spacing_horizontal"
const val KEY_SPACING_VERTICAL = "keyboard__key_spacing_vertical"
const val LANDSCAPE_INPUT_UI_MODE = "keyboard__landscape_input_ui_mode"
const val LONG_PRESS_DELAY = "keyboard__long_press_delay"
const val NUMBER_ROW = "keyboard__number_row"
const val ONE_HANDED_MODE = "keyboard__one_handed_mode"
const val POPUP_ENABLED = "keyboard__popup_enabled"
const val SOUND_ENABLED = "keyboard__sound_enabled"
const val SOUND_VOLUME = "keyboard__sound_volume"
const val UTILITY_KEY_ACTION = "keyboard__utility_key_action"
const val UTILITY_KEY_ENABLED = "keyboard__utility_key_enabled"
const val VIBRATION_ENABLED = "keyboard__vibration_enabled"
const val VIBRATION_STRENGTH = "keyboard__vibration_strength"
const val BOTTOM_OFFSET_LANDSCAPE = "keyboard__bottom_offset_landscape"
const val FONT_SIZE_MULTIPLIER_PORTRAIT = "keyboard__font_size_multiplier_portrait"
const val FONT_SIZE_MULTIPLIER_LANDSCAPE = "keyboard__font_size_multiplier_landscape"
const val HEIGHT_FACTOR = "keyboard__height_factor"
const val HEIGHT_FACTOR_CUSTOM = "keyboard__height_factor_custom"
const val HINTED_NUMBER_ROW_MODE = "keyboard__hinted_number_row_mode"
const val HINTED_SYMBOLS_MODE = "keyboard__hinted_symbols_mode"
const val KEY_SPACING_HORIZONTAL = "keyboard__key_spacing_horizontal"
const val KEY_SPACING_VERTICAL = "keyboard__key_spacing_vertical"
const val LANDSCAPE_INPUT_UI_MODE = "keyboard__landscape_input_ui_mode"
const val LONG_PRESS_DELAY = "keyboard__long_press_delay"
const val NUMBER_ROW = "keyboard__number_row"
const val ONE_HANDED_MODE = "keyboard__one_handed_mode"
const val ONE_HANDED_MODE_SCALE_FACTOR = "keyboard__one_handed_mode_scale_factor"
const val POPUP_ENABLED = "keyboard__popup_enabled"
const val SOUND_ENABLED = "keyboard__sound_enabled"
const val SOUND_VOLUME = "keyboard__sound_volume"
const val SPACE_BAR_SWITCHES_TO_CHARACTERS = "keyboard__space_bar_switches_to_characters"
const val UTILITY_KEY_ACTION = "keyboard__utility_key_action"
const val UTILITY_KEY_ENABLED = "keyboard__utility_key_enabled"
const val VIBRATION_ENABLED = "keyboard__vibration_enabled"
const val VIBRATION_STRENGTH = "keyboard__vibration_strength"
}
var bottomOffsetPortrait: Int = 0
@@ -371,8 +376,11 @@ class PrefHelper(
get() = prefHelper.getPref(NUMBER_ROW, false)
set(v) = prefHelper.setPref(NUMBER_ROW, v)
var oneHandedMode: String
get() = prefHelper.getPref(ONE_HANDED_MODE, "off")
get() = prefHelper.getPref(ONE_HANDED_MODE, OneHandedMode.OFF)
set(value) = prefHelper.setPref(ONE_HANDED_MODE, value)
var oneHandedModeScaleFactor: Int
get() = prefHelper.getPref(ONE_HANDED_MODE_SCALE_FACTOR, 87)
set(v) = prefHelper.setPref(ONE_HANDED_MODE_SCALE_FACTOR, v)
var popupEnabled: Boolean = false
get() = prefHelper.getPref(POPUP_ENABLED, true)
private set
@@ -383,6 +391,9 @@ class PrefHelper(
var soundVolume: Int = 0
get() = prefHelper.getPref(SOUND_VOLUME, -1)
private set
var spaceBarSwitchesToCharacters: Boolean
get() = prefHelper.getPref(SPACE_BAR_SWITCHES_TO_CHARACTERS, true)
set(v) = prefHelper.setPref(SPACE_BAR_SWITCHES_TO_CHARACTERS, v)
var utilityKeyAction: UtilityKeyAction
get() = UtilityKeyAction.fromString(prefHelper.getPref(UTILITY_KEY_ACTION, UtilityKeyAction.DYNAMIC_SWITCH_LANGUAGE_EMOJIS.toString()))
set(v) = prefHelper.setPref(UTILITY_KEY_ACTION, v)
@@ -434,20 +445,28 @@ class PrefHelper(
class Suggestion(private val prefHelper: PrefHelper) {
companion object {
const val BLOCK_POSSIBLY_OFFENSIVE = "suggestion__block_possibly_offensive"
const val CLIPBOARD_CONTENT_ENABLED = "suggestion__clipboard_content_enabled"
const val CLIPBOARD_CONTENT_TIMEOUT = "suggestion__clipboard_content_timeout"
const val DISPLAY_MODE = "suggestion__display_mode"
const val ENABLED = "suggestion__enabled"
const val SUGGEST_CLIPBOARD_CONTENT = "suggestion__suggest_clipboard_content"
const val USE_PREV_WORDS = "suggestion__use_prev_words"
}
var blockPossiblyOffensive: Boolean
get() = prefHelper.getPref(BLOCK_POSSIBLY_OFFENSIVE, true)
set(v) = prefHelper.setPref(BLOCK_POSSIBLY_OFFENSIVE, v)
var clipboardContentEnabled: Boolean
get() = prefHelper.getPref(CLIPBOARD_CONTENT_ENABLED, false)
set(v) = prefHelper.setPref(CLIPBOARD_CONTENT_ENABLED, v)
var clipboardContentTimeout: Int
get() = prefHelper.getPref(CLIPBOARD_CONTENT_TIMEOUT, 30)
set(v) = prefHelper.setPref(CLIPBOARD_CONTENT_TIMEOUT, v)
var displayMode: CandidateView.DisplayMode
get() = CandidateView.DisplayMode.fromString(prefHelper.getPref(DISPLAY_MODE, CandidateView.DisplayMode.DYNAMIC_SCROLLABLE.toString()))
set(v) = prefHelper.setPref(DISPLAY_MODE, v)
var enabled: Boolean
get() = prefHelper.getPref(ENABLED, true)
set(v) = prefHelper.setPref(ENABLED, v)
var suggestClipboardContent: Boolean
get() = prefHelper.getPref(SUGGEST_CLIPBOARD_CONTENT, false)
set(v) = prefHelper.setPref(SUGGEST_CLIPBOARD_CONTENT, v)
var usePrevWords: Boolean
get() = prefHelper.getPref(USE_PREV_WORDS, true)
set(v) = prefHelper.setPref(USE_PREV_WORDS, v)
@@ -489,4 +508,52 @@ class PrefHelper(
get() = prefHelper.getPref(SUNSET_TIME, TimeUtil.encode(18, 0))
set(v) = prefHelper.setPref(SUNSET_TIME, v)
}
/**
* Wrapper class for clipboard preferences
*/
class Clipboard(private val prefHelper: PrefHelper) {
companion object {
const val ENABLE_INTERNAL = "clipboard__enable_internal"
const val SYNC_TO_SYSTEM = "clipboard__sync_to_system"
const val SYNC_TO_FLORIS = "clipboard__sync_to_floris"
const val ENABLE_HISTORY = "clipboard__enable_history"
const val CLEAN_UP_OLD = "clipboard__clean_up_old"
const val LIMIT_HISTORY_SIZE = "clipboard__limit_history_size"
const val CLEAN_UP_AFTER = "clipboard__clean_up_after"
const val MAX_HISTORY_SIZE = "clipboard__max_history_size"
}
var enableInternal: Boolean
get() = prefHelper.getPref(ENABLE_INTERNAL, false)
set(v) = prefHelper.setPref(ENABLE_INTERNAL, v)
var syncToSystem: Boolean
get() = prefHelper.getPref(SYNC_TO_SYSTEM, false)
set(v) = prefHelper.setPref(SYNC_TO_SYSTEM, v)
var syncToFloris: Boolean
get() = prefHelper.getPref(SYNC_TO_FLORIS, true)
set(v) = prefHelper.setPref(SYNC_TO_FLORIS, v)
var enableHistory: Boolean
get() = prefHelper.getPref(ENABLE_HISTORY, false)
set(v) = prefHelper.setPref(ENABLE_HISTORY, v)
var cleanUpOld: Boolean
get() = prefHelper.getPref(CLEAN_UP_OLD, false)
set(v) = prefHelper.setPref(CLEAN_UP_OLD, v)
var limitHistorySize: Boolean
get() = prefHelper.getPref(LIMIT_HISTORY_SIZE, true)
set(v) = prefHelper.setPref(LIMIT_HISTORY_SIZE, v)
var cleanUpAfter: Int
get() = prefHelper.getPref(CLEAN_UP_AFTER, 20)
set(v) = prefHelper.setPref(CLEAN_UP_AFTER, v)
var maxHistorySize: Int
get() = prefHelper.getPref(MAX_HISTORY_SIZE, 20)
set(v) = prefHelper.setPref(MAX_HISTORY_SIZE, v)
}
}

View File

@@ -17,7 +17,6 @@
package dev.patrickgold.florisboard.ime.dictionary
import android.content.Context
import com.github.michaelbull.result.*
import dev.patrickgold.florisboard.ime.extension.AssetRef
import timber.log.Timber
@@ -48,22 +47,22 @@ class DictionaryManager private constructor(private val applicationContext: Cont
}
}
fun loadDictionary(ref: AssetRef): Result<Dictionary<String, Int>, Throwable> {
fun loadDictionary(ref: AssetRef): Result<Dictionary<String, Int>> {
dictionaryCache[ref.toString()]?.let {
return Ok(it)
return Result.success(it)
}
if (ref.path.endsWith(".flict")) {
// Assume this is a Flictionary
Flictionary.load(applicationContext, ref).onSuccess { flict ->
dictionaryCache[ref.toString()] = flict
return Ok(flict)
return Result.success(flict)
}.onFailure { err ->
Timber.i(err)
return Err(err)
return Result.failure(err)
}
} else {
return Err(Exception("Unable to determine supported type for given AssetRef!"))
return Result.failure(Exception("Unable to determine supported type for given AssetRef!"))
}
return Err(Exception("If this message is ever thrown, something is completely broken..."))
return Result.failure(Exception("If this message is ever thrown, something is completely broken..."))
}
}

View File

@@ -17,9 +17,6 @@
package dev.patrickgold.florisboard.ime.dictionary
import android.content.Context
import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
import dev.patrickgold.florisboard.ime.extension.AssetRef
import dev.patrickgold.florisboard.ime.extension.AssetSource
import dev.patrickgold.florisboard.ime.nlp.*
@@ -72,13 +69,13 @@ class Flictionary private constructor(
* either the parsed dictionary or an exception giving information about the error which
* occurred.
*/
fun load(context: Context, assetRef: AssetRef): Result<Flictionary, Exception> {
fun load(context: Context, assetRef: AssetRef): Result<Flictionary> {
val buffer = ByteArray(5000) { 0 }
val inputStream: InputStream
if (assetRef.source == AssetSource.Assets) {
inputStream = context.assets.open(assetRef.path)
} else {
return Err(Exception("Only AssetSource.Assets is currently supported!"))
return Result.failure(Exception("Only AssetSource.Assets is currently supported!"))
}
var headerStr: String? = null
@@ -99,7 +96,7 @@ class Flictionary private constructor(
(cmd and MASK_BEGIN_PTREE_NODE) == CMDB_BEGIN_PTREE_NODE -> {
if (pos == 0) {
inputStream.close()
return Err(
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_CMD_BEGIN_PTREE_NODE,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
@@ -125,7 +122,7 @@ class Flictionary private constructor(
freq = buffer[1].toInt() and 0xFF
} else {
inputStream.close()
return Err(
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_EOF,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
@@ -134,7 +131,7 @@ class Flictionary private constructor(
}
freqSize = 1
}
else -> return Err(Exception("TODO: shortcut not supported"))
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]
@@ -154,7 +151,7 @@ class Flictionary private constructor(
pos += (freqSize + 1 + size)
} else {
inputStream.close()
return Err(
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_EOF,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
@@ -166,7 +163,7 @@ class Flictionary private constructor(
(cmd and MASK_BEGIN_HEADER) == CMDB_BEGIN_HEADER -> {
if (pos != 0) {
inputStream.close()
return Err(
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_CMD_BEGIN_HEADER,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
@@ -176,7 +173,7 @@ class Flictionary private constructor(
version = cmd and ATTR_HEADER_VERSION
if (version != VERSION_0) {
inputStream.close()
return Err(
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNSUPPORTED_FLICTIONARY_VERSION,
address = pos,
@@ -203,7 +200,7 @@ class Flictionary private constructor(
pos += (10 + size)
} else {
inputStream.close()
return Err(
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_EOF,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
@@ -212,7 +209,7 @@ class Flictionary private constructor(
}
} else {
inputStream.close()
return Err(
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_EOF,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
@@ -224,7 +221,7 @@ class Flictionary private constructor(
(cmd and MASK_END) == CMDB_END -> {
if (pos == 0) {
inputStream.close()
return Err(
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_CMD_END,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
@@ -240,7 +237,7 @@ class Flictionary private constructor(
}
} else {
inputStream.close()
return Err(
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_ABSOLUTE_DEPTH_DECREASE_BELOW_ZERO,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size - n
@@ -249,7 +246,7 @@ class Flictionary private constructor(
}
} else {
inputStream.close()
return Err(
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_CMD_END_ZERO_VALUE,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
@@ -260,7 +257,7 @@ class Flictionary private constructor(
}
else -> {
inputStream.close()
return Err(
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.INVALID_CMD_BYTE_PROVIDED,
address = pos, cmdByte = cmd.toByte(), absoluteDepth = ngramTreeStack.size
@@ -272,7 +269,7 @@ class Flictionary private constructor(
inputStream.close()
if (ngramTreeStack.size != 0) {
return Err(
return Result.failure(
ParseException(
errorType = ParseException.ErrorType.UNEXPECTED_ABSOLUTE_DEPTH_NOT_ZERO_AT_EOF,
address = pos, cmdByte = 0x00.toByte(), absoluteDepth = ngramTreeStack.size
@@ -280,7 +277,7 @@ class Flictionary private constructor(
)
}
return Ok(
return Result.success(
Flictionary(
name = "flict",
label = "flict",

View File

@@ -17,8 +17,6 @@
package dev.patrickgold.florisboard.ime.extension
import android.content.Context
import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Result
/**
* Interface for an Asset to use within FlorisBoard. An asset is everything from a dictionary to a
@@ -61,6 +59,6 @@ interface Asset {
/**
* Loads an Asset of type [T] from the specified path.
*/
fun fromFile(context: Context, path: String): Result<T, Throwable> = Err(NotImplementedError())
fun fromFile(context: Context, path: String): Result<T> = Result.failure(NotImplementedError())
}
}

View File

@@ -17,7 +17,7 @@
package dev.patrickgold.florisboard.ime.extension
import android.content.Context
import com.github.michaelbull.result.*
import android.net.Uri
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dev.patrickgold.florisboard.ime.text.key.KeyTypeAdapter
@@ -25,6 +25,7 @@ import dev.patrickgold.florisboard.ime.text.key.KeyVariationAdapter
import dev.patrickgold.florisboard.ime.text.layout.LayoutTypeAdapter
import timber.log.Timber
import java.io.File
import kotlin.reflect.KClass
class AssetManager private constructor(private val applicationContext: Context) {
private val moshi: Moshi = Moshi.Builder()
@@ -59,22 +60,22 @@ class AssetManager private constructor(private val applicationContext: Context)
}
}
fun deleteAsset(ref: AssetRef): Result<Nothing?, Throwable> {
fun deleteAsset(ref: AssetRef): Result<Unit> {
return when (ref.source) {
AssetSource.Internal -> {
val file = File(applicationContext.filesDir.absolutePath + "/" + ref.path)
if (file.isFile) {
val success = file.delete()
if (success) {
Ok(null)
Result.success(Unit)
} else {
Err(Exception("Could not delete file."))
Result.failure(Exception("Could not delete file."))
}
} else {
Err(Exception("Provided reference is not a file."))
Result.failure(Exception("Provided reference is not a file."))
}
}
else -> Err(Exception("Can not delete an asset in source '${ref.source}'"))
else -> Result.failure(Exception("Can not delete an asset in source '${ref.source}'"))
}
}
@@ -97,7 +98,7 @@ class AssetManager private constructor(private val applicationContext: Context)
}
}
fun <T: Asset> listAssets(ref: AssetRef, assetClass: Class<T>): Result<Map<AssetRef, T>, Throwable> {
fun <T : Asset> listAssets(ref: AssetRef, assetClass: KClass<T>): Result<Map<AssetRef, T>> {
val retMap = mutableMapOf<AssetRef, T>()
return when (ref.source) {
AssetSource.Assets -> {
@@ -114,9 +115,9 @@ class AssetManager private constructor(private val applicationContext: Context)
}
}
}
Ok(retMap.toMap())
Result.success(retMap.toMap())
} catch (e: Exception) {
Err(e)
Result.failure(e)
}
}
AssetSource.Internal -> {
@@ -136,19 +137,19 @@ class AssetManager private constructor(private val applicationContext: Context)
}
}
}
Ok(retMap.toMap())
Result.success(retMap.toMap())
}
else -> Ok(retMap.toMap())
else -> Result.success(retMap.toMap())
}
}
fun <T: Asset> loadAsset(ref: AssetRef, assetClass: Class<T>): Result<T, Throwable> {
fun <T : Asset> loadAsset(ref: AssetRef, assetClass: KClass<T>): Result<T> {
val rawJsonData = when (ref.source) {
is AssetSource.Assets -> {
try {
applicationContext.assets.open(ref.path).bufferedReader().use { it.readText() }
} catch (e: Exception) {
return Err(e)
return Result.failure(e)
}
}
is AssetSource.Internal -> {
@@ -163,28 +164,67 @@ class AssetManager private constructor(private val applicationContext: Context)
else -> "{}"
}
return try {
val adapter = moshi.adapter(assetClass)
val adapter = moshi.adapter(assetClass.java)
val asset = adapter.fromJson(rawJsonData)
if (asset != null) {
Ok(asset)
Result.success(asset)
} else {
Err(NullPointerException("Asset failed to load!"))
Result.failure(NullPointerException("Asset failed to load!"))
}
} catch (e: Exception) {
Err(e)
Result.failure(e)
}
}
fun <T: Asset> writeAsset(ref: AssetRef, assetClass: Class<T>, asset: T): Result<Boolean, Throwable> {
fun <T : Asset> loadAsset(uri: Uri, assetClass: KClass<T>, maxSize: Int): Result<T> {
val rawJsonData = ExternalContentUtils.readTextFromUri(applicationContext, uri, maxSize).getOrElse {
return Result.failure(it)
}
return try {
val adapter = moshi.adapter(assetClass.java)
val asset = adapter.fromJson(rawJsonData)
if (asset != null) {
Result.success(asset)
} else {
Result.failure(NullPointerException("Asset failed to load!"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
fun loadAssetRaw(ref: AssetRef): Result<String> {
return when (ref.source) {
is AssetSource.Assets -> {
try {
Result.success(applicationContext.assets.open(ref.path).bufferedReader().use { it.readText() })
} catch (e: Exception) {
Result.failure(e)
}
}
is AssetSource.Internal -> {
val file = File(applicationContext.filesDir.absolutePath + "/" + ref.path)
val contents = readFile(file)
if (contents.isBlank()) {
Result.failure(Exception("File is blank!"))
} else {
Result.success(contents)
}
}
else -> Result.failure(Exception("Unsupported asset ref!"))
}
}
fun <T : Asset> writeAsset(ref: AssetRef, assetClass: KClass<T>, asset: T): Result<Unit> {
return when (ref.source) {
AssetSource.Internal -> {
val adapter = moshi.adapter(assetClass)
val adapter = moshi.adapter(assetClass.java)
val rawJson = adapter.toJson(asset)
val file = File(applicationContext.filesDir.absolutePath + "/" + ref.path)
writeToFile(file, rawJson)
Ok(true)
Result.success(Unit)
}
else -> Err(Exception("Can not write an asset in source '${ref.source}'"))
else -> Result.failure(Exception("Can not write an asset in source '${ref.source}'"))
}
}

View File

@@ -16,11 +16,6 @@
package dev.patrickgold.florisboard.ime.extension
import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
import com.github.michaelbull.result.getOrElse
/**
* Data class which is a reference to an asset file. It indicates in which storage medium the asset
* is as well as the relative path to it.
@@ -36,15 +31,15 @@ data class AssetRef(
companion object {
private const val DELIMITER: String = ":"
fun fromString(str: String): Result<AssetRef, String> {
fun fromString(str: String): Result<AssetRef> {
val items = str.split(DELIMITER)
if (items.size != 2) {
return Err("Unexpected length of given asset ref. Make sure that the asset ref string contains exactly 2 items separated by '$DELIMITER'!")
return Result.failure(Exception("Unexpected length of given asset ref. Make sure that the asset ref string contains exactly 2 items separated by '$DELIMITER'!"))
}
val retSource = AssetSource.fromString(items[0]).getOrElse {
return Err(it)
return Result.failure(Exception(it))
}
return Ok(AssetRef(retSource, items[1]))
return Result.success(AssetRef(retSource, items[1]))
}
}

View File

@@ -16,9 +16,6 @@
package dev.patrickgold.florisboard.ime.extension
import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
import java.util.*
/**
@@ -52,16 +49,16 @@ sealed class AssetSource {
companion object {
private val externalRegex: Regex = """^external\\(([a-z]+\\.)*[a-z]+\\)\$""".toRegex()
fun fromString(str: String): Result<AssetSource, String> {
fun fromString(str: String): Result<AssetSource> {
return when (val string = str.toLowerCase(Locale.ENGLISH)) {
"assets" -> Ok(Assets)
"internal" -> Ok(Internal)
"assets" -> Result.success(Assets)
"internal" -> Result.success(Internal)
else -> {
if (string.matches(externalRegex)) {
val packageName = string.substring(9, string.length - 1)
Ok(External(packageName))
Result.success(External(packageName))
} else {
Err("'$str' is not a valid AssetSource.")
Result.failure(Exception("'$str' is not a valid AssetSource."))
}
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.extension
import android.content.Context
import android.net.Uri
class ExternalContentUtils private constructor() {
companion object {
fun readTextFromUri(context: Context, uri: Uri, maxSize: Int): Result<String> {
val contentResolver = context.contentResolver
?: return Result.failure(NullPointerException("System content resolver not available"))
val inputStream = contentResolver.openInputStream(uri)
?: return Result.failure(NullPointerException("Cannot open input stream for given uri '$uri'"))
val assetFileDescriptor = contentResolver.openAssetFileDescriptor(uri, "r")
?: return Result.failure(NullPointerException("Cannot open asset file descriptor for given uri '$uri'"))
if (assetFileDescriptor.length > maxSize) {
return Result.failure(Exception("Contents of given uri '$uri' exceeds maximum size of $maxSize bytes!"))
}
val rawText = inputStream.bufferedReader(Charsets.UTF_8).use { it.readText() }
return Result.success(rawText)
}
fun writeTextToUri(context: Context, uri: Uri, text: String): Result<Unit> {
val contentResolver = context.contentResolver
?: return Result.failure(NullPointerException("System content resolver not available"))
// Must use "rwt" mode to ensure destination file length is truncated after writing.
val outputStream = contentResolver.openOutputStream(uri, "rwt")
?: return Result.failure(NullPointerException("Cannot open output stream for given uri '$uri'"))
outputStream.bufferedWriter(Charsets.UTF_8).use { it.write(text) }
return Result.success(Unit)
}
}
}

View File

@@ -17,7 +17,6 @@
package dev.patrickgold.florisboard.ime.media
import android.annotation.SuppressLint
import android.os.Handler
import android.view.MotionEvent
import android.view.View
import android.widget.*
@@ -25,16 +24,13 @@ import com.google.android.material.tabs.TabLayout
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.EditorInstance
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.InputKeyEvent
import dev.patrickgold.florisboard.ime.core.InputView
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyData
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyboardView
import dev.patrickgold.florisboard.ime.media.emoticon.EmoticonKeyData
import dev.patrickgold.florisboard.ime.media.emoticon.EmoticonKeyboardView
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyData
import dev.patrickgold.florisboard.ime.text.key.KeyType
import dev.patrickgold.florisboard.util.cancelAll
import dev.patrickgold.florisboard.util.postAtScheduledRate
import kotlinx.coroutines.*
import timber.log.Timber
import java.util.*
@@ -58,11 +54,10 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
private var activeTab: Tab? = null
private var mediaViewFlipper: ViewFlipper? = null
private var repeatedKeyPressHandler: Handler? = null
private var tabLayout: TabLayout? = null
private val tabViews = EnumMap<Tab, LinearLayout>(Tab::class.java)
var mediaViewGroup: LinearLayout? = null
private var mediaViewGroup: LinearLayout? = null
companion object {
private var instance: MediaInputManager? = null
@@ -80,11 +75,6 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
florisboard.addEventListener(this)
}
override fun onCreateInputView() {
super.onCreateInputView()
repeatedKeyPressHandler = Handler(florisboard.context.mainLooper)
}
/**
* Called when a new input view has been registered. Used to initialize all media-relevant
* views and layouts.
@@ -145,30 +135,21 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
private fun onBottomButtonEvent(view: View, event: MotionEvent?): Boolean {
event ?: return false
val data = when (view.id) {
R.id.media_input_switch_to_text_input_button -> {
KeyData(code = KeyCode.SWITCH_TO_TEXT_CONTEXT)
}
R.id.media_input_backspace_button -> {
KeyData(code = KeyCode.DELETE, type = KeyType.ENTER_EDITING)
}
else -> null
R.id.media_input_switch_to_text_input_button -> KeyData.SWITCH_TO_TEXT_CONTEXT
R.id.media_input_backspace_button -> KeyData.DELETE
else -> return false
}
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
florisboard.keyPressVibrate()
florisboard.keyPressSound(data)
if (data?.code == KeyCode.DELETE && data.type == KeyType.ENTER_EDITING) {
val delayMillis = florisboard.prefs.keyboard.longPressDelay.toLong()
repeatedKeyPressHandler?.postAtScheduledRate(delayMillis, 25) {
florisboard.textInputManager.sendKeyPress(data)
}
}
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.down(data))
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
repeatedKeyPressHandler?.cancelAll()
if (event.actionMasked != MotionEvent.ACTION_CANCEL && data != null) {
florisboard.textInputManager.sendKeyPress(data)
}
MotionEvent.ACTION_UP -> {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.up(data))
}
MotionEvent.ACTION_CANCEL -> {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
}
}
// MUST return false here so the background selector for showing a transparent bg works

View File

@@ -33,6 +33,8 @@ import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
/**
* View class for managing the rendering and the events of a single emoji keyboard key.
@@ -46,7 +48,7 @@ import dev.patrickgold.florisboard.ime.theme.ThemeManager
class EmojiKeyView(
private val emojiKeyboardView: EmojiKeyboardView,
val data: EmojiKeyData
) : androidx.appcompat.widget.AppCompatTextView(emojiKeyboardView.context),
) : androidx.appcompat.widget.AppCompatTextView(emojiKeyboardView.context), CoroutineScope by MainScope(),
FlorisBoard.EventListener, ThemeManager.OnThemeUpdatedListener {
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)

View File

@@ -0,0 +1,26 @@
/*
* 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.onehanded
/**
* Static object which contains all possible one-handed mode strings.
*/
object OneHandedMode {
const val OFF: String = "off"
const val START: String = "start"
const val END: String = "end"
}

View File

@@ -0,0 +1,102 @@
/*
* 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.onehanded
import android.content.Context
import android.content.res.ColorStateList
import android.util.AttributeSet
import android.view.Gravity
import android.widget.ImageButton
import android.widget.LinearLayout
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
class OneHandedPanel : LinearLayout, ThemeManager.OnThemeUpdatedListener {
private var florisboard: FlorisBoard? = null
private var themeManager: ThemeManager? = null
private var closeBtn: ImageButton? = null
private var moveBtn: ImageButton? = null
private val panelSide: String
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
context.obtainStyledAttributes(attrs, R.styleable.OneHandedPanel).apply {
panelSide = getString(R.styleable.OneHandedPanel_panelSide) ?: OneHandedMode.START
recycle()
}
orientation = VERTICAL
gravity = Gravity.CENTER_VERTICAL
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
florisboard = FlorisBoard.getInstanceOrNull()
themeManager = ThemeManager.defaultOrNull()
closeBtn = findViewWithTag("one_handed_ctrl_close")
closeBtn?.setOnClickListener {
florisboard?.let {
it.prefs.keyboard.oneHandedMode = OneHandedMode.OFF
it.updateOneHandedPanelVisibility()
}
}
moveBtn = findViewWithTag("one_handed_ctrl_move")
moveBtn?.setOnClickListener {
florisboard?.let {
it.prefs.keyboard.oneHandedMode = panelSide
it.updateOneHandedPanelVisibility()
}
}
themeManager?.registerOnThemeUpdatedListener(this)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
florisboard = null
themeManager?.unregisterOnThemeUpdatedListener(this)
themeManager = null
closeBtn?.setOnClickListener(null)
closeBtn = null
moveBtn?.setOnClickListener(null)
moveBtn = null
}
override fun onThemeUpdated(theme: Theme) {
setBackgroundColor(theme.getAttr(Theme.Attr.ONE_HANDED_BACKGROUND).toSolidColor().color)
ColorStateList.valueOf(theme.getAttr(Theme.Attr.ONE_HANDED_FOREGROUND).toSolidColor().color).also {
closeBtn?.imageTintList = it
moveBtn?.imageTintList = it
}
closeBtn?.invalidate()
moveBtn?.invalidate()
invalidate()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val florisboard = florisboard ?: return super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val width = (florisboard.inputView?.measuredWidth ?: 0) *
((100 - florisboard.prefs.keyboard.oneHandedModeScaleFactor) / 100.0f)
super.onMeasure(MeasureSpec.makeMeasureSpec(width.toInt(), MeasureSpec.EXACTLY), heightMeasureSpec)
}
}

View File

@@ -18,10 +18,15 @@ package dev.patrickgold.florisboard.ime.popup
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Rect
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import dev.patrickgold.florisboard.ime.clip.ClipboardPopupManager
import dev.patrickgold.florisboard.ime.clip.ClipboardPopupView
import timber.log.Timber
/**
* Basic helper view class which acts as a non-interactive layer view, which sits above the whole
@@ -41,12 +46,29 @@ class PopupLayerView : FrameLayout {
)
}
var clipboardPopupManager: ClipboardPopupManager? = null
var intercept: ClipboardPopupView? = null
var shouldIntercept = true
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
if (ev != null) {
intercept?.run {
val viewRect = Rect()
getGlobalVisibleRect(viewRect)
return when {
!viewRect.contains(ev.x.toInt(), ev.y.toInt()) -> {
clipboardPopupManager?.hide()
true
}
else -> false
}
}
}
return true
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean {
override fun onTouchEvent(ev: MotionEvent?): Boolean {
return false
}
}

View File

@@ -48,7 +48,8 @@ class PopupManager<T_KBD: View, T_KV: View>(
KeyCode.ENTER,
KeyCode.LANGUAGE_SWITCH,
KeyCode.SWITCH_TO_TEXT_CONTEXT,
KeyCode.SWITCH_TO_MEDIA_CONTEXT
KeyCode.SWITCH_TO_MEDIA_CONTEXT,
KeyCode.SWITCH_TO_CLIPBOARD_CONTEXT
)
private var keyPopupWidth: Int
private var keyPopupHeight: Int
@@ -108,6 +109,11 @@ class PopupManager<T_KBD: View, T_KV: View>(
PopupExtendedView.Element.Icon(it, adjustedIndex)
} ?: PopupExtendedView.Element.Undefined
}
KeyCode.SWITCH_TO_CLIPBOARD_CONTEXT -> {
getDrawable(keyView.context, R.drawable.ic_assignment)?.let {
PopupExtendedView.Element.Icon(it, adjustedIndex)
} ?: PopupExtendedView.Element.Undefined
}
KeyCode.URI_COMPONENT_TLD -> {
PopupExtendedView.Element.Tld(
keyView.data.popup[adjustedIndex].label, adjustedIndex
@@ -146,10 +152,10 @@ class PopupManager<T_KBD: View, T_KV: View>(
when (keyboardView.resources.configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> {
if (keyboardView.isSmartbarKeyboardView) {
keyPopupWidth = (keyView.measuredWidth * 0.6f).toInt()
keyPopupWidth = (keyView.measuredWidth * 1.0f).toInt()
keyPopupHeight = (keyboardView.desiredKeyHeight * 3.0f * 1.2f).toInt()
} else {
keyPopupWidth = (keyboardView.desiredKeyWidth * 0.6f).toInt()
keyPopupWidth = (keyboardView.desiredKeyWidth * 1.0f).toInt()
keyPopupHeight = (keyboardView.desiredKeyHeight * 3.0f).toInt()
}
}

View File

@@ -18,14 +18,13 @@ package dev.patrickgold.florisboard.ime.text
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.os.Handler
import android.view.KeyEvent
import android.widget.LinearLayout
import android.widget.Toast
import android.widget.ViewFlipper
import com.github.michaelbull.result.getOr
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.clip.provider.ClipboardItem
import dev.patrickgold.florisboard.ime.core.*
import dev.patrickgold.florisboard.ime.dictionary.Dictionary
import dev.patrickgold.florisboard.ime.dictionary.DictionaryManager
@@ -56,7 +55,7 @@ import kotlin.math.roundToLong
* TextInputManager is also the hub in the communication between the system, the active editor
* instance and the Smartbar.
*/
class TextInputManager private constructor() : CoroutineScope by MainScope(),
class TextInputManager private constructor() : CoroutineScope by MainScope(), InputKeyEventReceiver,
FlorisBoard.EventListener, SmartbarView.EventListener {
private val florisboard = FlorisBoard.getInstance()
@@ -68,28 +67,37 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
private val keyboardViews = EnumMap<KeyboardMode, KeyboardView>(KeyboardMode::class.java)
private var editingKeyboardView: EditingKeyboardView? = null
private var loadingPlaceholderKeyboard: KeyboardView? = null
private val osHandler = Handler()
private var textViewFlipper: ViewFlipper? = null
private var textViewGroup: LinearLayout? = null
private val dictionaryManager: DictionaryManager = DictionaryManager.default()
private var activeDictionary: Dictionary<String, Int>? = null
val inputEventDispatcher: InputEventDispatcher = InputEventDispatcher.new(
parentScope = this,
repeatableKeyCodes = intArrayOf(
KeyCode.ARROW_DOWN,
KeyCode.ARROW_LEFT,
KeyCode.ARROW_RIGHT,
KeyCode.ARROW_UP,
KeyCode.DELETE,
KeyCode.FORWARD_DELETE
)
)
var keyVariation: KeyVariation = KeyVariation.NORMAL
val layoutManager = LayoutManager(florisboard)
private var smartbarView: SmartbarView? = null
// Caps/Space related properties
// Caps/Shift related properties
var caps: Boolean = false
private set
var capsLock: Boolean = false
private set
private var hasCapsRecentlyChanged: Boolean = false
private var hasSpaceRecentlyPressed: Boolean = false
private var newCapsState: Boolean = false
// Composing text related properties
var isManualSelectionMode: Boolean = false
private var isManualSelectionModeLeft: Boolean = false
private var isManualSelectionModeRight: Boolean = false
private var isManualSelectionModeStart: Boolean = false
private var isManualSelectionModeEnd: Boolean = false
companion object {
private var instance: TextInputManager? = null
@@ -114,6 +122,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
override fun onCreate() {
Timber.i("onCreate()")
inputEventDispatcher.keyEventReceiver = this
var subtypes = florisboard.subtypeManager.subtypes
if (subtypes.isEmpty()) {
subtypes = listOf(Subtype.DEFAULT)
@@ -199,8 +208,9 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
override fun onDestroy() {
Timber.i("onDestroy()")
inputEventDispatcher.keyEventReceiver = null
inputEventDispatcher.close()
cancel()
osHandler.removeCallbacksAndMessages(null)
layoutManager.onDestroy()
instance = null
}
@@ -263,6 +273,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
}
updateCapsState()
setActiveKeyboardMode(keyboardMode)
smartbarView?.setCandidateSuggestionWords(System.nanoTime(), null)
smartbarView?.updateSmartbarState()
}
@@ -300,8 +311,8 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
keyboardViews[mode]?.requestLayoutAllKeys()
activeKeyboardMode = mode
isManualSelectionMode = false
isManualSelectionModeLeft = false
isManualSelectionModeRight = false
isManualSelectionModeStart = false
isManualSelectionModeEnd = false
smartbarView?.isQuickActionsVisible = false
smartbarView?.updateSmartbarState()
}
@@ -311,7 +322,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
if (activeEditorInstance.isComposingEnabled) {
withContext(Dispatchers.IO) {
dictionaryManager.loadDictionary(AssetRef(AssetSource.Assets,"ime/dict/en.flict")).let {
activeDictionary = it.getOr(null)
activeDictionary = it.getOrDefault(null)
}
}
}
@@ -326,17 +337,14 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* and passing this info on to the [SmartbarView] to turn it into candidate suggestions.
*/
override fun onUpdateSelection() {
if (!activeEditorInstance.isNewSelectionInBoundsOfOld) {
isManualSelectionMode = false
isManualSelectionModeLeft = false
isManualSelectionModeRight = false
if (!inputEventDispatcher.isPressed(KeyCode.SHIFT)) {
updateCapsState()
}
updateCapsState()
smartbarView?.updateSmartbarState()
if (BuildConfig.DEBUG) {
Timber.i("current word: ${activeEditorInstance.cachedInput.currentWord.text}")
}
if (activeEditorInstance.isComposingEnabled) {
if (activeEditorInstance.isComposingEnabled && !inputEventDispatcher.isPressed(KeyCode.DELETE)) {
if (activeEditorInstance.shouldReevaluateComposingSuggestions) {
activeEditorInstance.shouldReevaluateComposingSuggestions = false
activeDictionary?.let {
@@ -345,7 +353,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
val suggestions = it.getTokenPredictions(
precedingTokens = listOf(),
currentToken = Token(activeEditorInstance.cachedInput.currentWord.text),
maxSuggestionCount = 3,
maxSuggestionCount = 16,
allowPossiblyOffensive = !florisboard.prefs.suggestion.blockPossiblyOffensive
).toStringList()
if (BuildConfig.DEBUG) {
@@ -359,7 +367,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
}
}
} else {
smartbarView?.setCandidateSuggestionWords(System.nanoTime(), listOf())
smartbarView?.setCandidateSuggestionWords(System.nanoTime(), null)
}
}
}
@@ -376,9 +384,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
if (!capsLock) {
caps = florisboard.prefs.correction.autoCapitalization &&
activeEditorInstance.cursorCapsMode != InputAttributes.CapsMode.NONE
launch(Dispatchers.Main) {
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
}
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
}
}
@@ -387,22 +393,24 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* class.
*/
fun executeSwipeAction(swipeAction: SwipeAction) {
when (swipeAction) {
SwipeAction.DELETE_WORD -> handleDeleteWord()
SwipeAction.INSERT_SPACE -> handleSpace()
SwipeAction.MOVE_CURSOR_DOWN -> handleArrow(KeyCode.ARROW_DOWN)
SwipeAction.MOVE_CURSOR_UP -> handleArrow(KeyCode.ARROW_UP)
SwipeAction.MOVE_CURSOR_LEFT -> handleArrow(KeyCode.ARROW_LEFT)
SwipeAction.MOVE_CURSOR_RIGHT -> handleArrow(KeyCode.ARROW_RIGHT)
SwipeAction.MOVE_CURSOR_START_OF_LINE -> handleArrow(KeyCode.MOVE_START_OF_LINE)
SwipeAction.MOVE_CURSOR_END_OF_LINE -> handleArrow(KeyCode.MOVE_END_OF_LINE)
SwipeAction.MOVE_CURSOR_START_OF_PAGE -> handleArrow(KeyCode.MOVE_START_OF_PAGE)
SwipeAction.MOVE_CURSOR_END_OF_PAGE -> handleArrow(KeyCode.MOVE_END_OF_PAGE)
SwipeAction.SHIFT -> handleShift()
SwipeAction.SHOW_INPUT_METHOD_PICKER -> sendKeyPress(
KeyData(type = KeyType.FUNCTION, code = KeyCode.SHOW_INPUT_METHOD_PICKER)
)
else -> {}
val keyData = when (swipeAction) {
SwipeAction.DELETE_WORD -> KeyData.DELETE_WORD
SwipeAction.INSERT_SPACE -> KeyData.SPACE
SwipeAction.MOVE_CURSOR_DOWN -> KeyData.ARROW_DOWN
SwipeAction.MOVE_CURSOR_UP -> KeyData.ARROW_UP
SwipeAction.MOVE_CURSOR_LEFT -> KeyData.ARROW_LEFT
SwipeAction.MOVE_CURSOR_RIGHT -> KeyData.ARROW_RIGHT
SwipeAction.MOVE_CURSOR_START_OF_LINE -> KeyData.MOVE_START_OF_LINE
SwipeAction.MOVE_CURSOR_END_OF_LINE -> KeyData.MOVE_END_OF_LINE
SwipeAction.MOVE_CURSOR_START_OF_PAGE -> KeyData.MOVE_START_OF_PAGE
SwipeAction.MOVE_CURSOR_END_OF_PAGE -> KeyData.MOVE_END_OF_PAGE
SwipeAction.SHIFT -> KeyData.SHIFT
SwipeAction.SWITCH_TO_CLIPBOARD_CONTEXT -> KeyData.SWITCH_TO_CLIPBOARD_CONTEXT
SwipeAction.SHOW_INPUT_METHOD_PICKER -> KeyData.SHOW_INPUT_METHOD_PICKER
else -> null
}
if (keyData != null) {
inputEventDispatcher.send(InputKeyEvent.downUp(keyData))
}
}
@@ -414,6 +422,10 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
activeEditorInstance.commitCompletion(word)
}
override fun onSmartbarClipboardCandidatePressed(clipboardItem: ClipboardItem) {
activeEditorInstance.commitClipboardItem(clipboardItem)
}
override fun onSmartbarPrivateModeButtonClicked() {
Toast.makeText(florisboard.context, R.string.private_mode_dialog__title, Toast.LENGTH_LONG).show()
}
@@ -431,11 +443,11 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
R.id.quick_action_open_settings -> florisboard.launchSettings()
R.id.quick_action_one_handed_toggle -> florisboard.toggleOneHandedMode(isRight = true)
R.id.quick_action_undo -> {
handleUndo()
activeEditorInstance.performUndo()
return
}
R.id.quick_action_redo -> {
handleRedo()
activeEditorInstance.performRedo()
return
}
}
@@ -443,23 +455,13 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
smartbarView?.updateSmartbarState()
}
private fun handleUndo(){
activeEditorInstance.performUndo()
}
private fun handleRedo(){
activeEditorInstance.performRedo()
}
/**
* Handles a [KeyCode.DELETE] event.
*/
private fun handleDelete() {
hasCapsRecentlyChanged = false
hasSpaceRecentlyPressed = false
isManualSelectionMode = false
isManualSelectionModeLeft = false
isManualSelectionModeRight = false
isManualSelectionModeStart = false
isManualSelectionModeEnd = false
activeEditorInstance.deleteBackwards()
}
@@ -467,11 +469,9 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* Handles a [KeyCode.DELETE_WORD] event.
*/
private fun handleDeleteWord() {
hasCapsRecentlyChanged = false
hasSpaceRecentlyPressed = false
isManualSelectionMode = false
isManualSelectionModeLeft = false
isManualSelectionModeRight = false
isManualSelectionModeStart = false
isManualSelectionModeEnd = false
activeEditorInstance.deleteWordsBeforeCursor(1)
}
@@ -479,8 +479,6 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* Handles a [KeyCode.ENTER] event.
*/
private fun handleEnter() {
hasCapsRecentlyChanged = false
hasSpaceRecentlyPressed = false
if (activeEditorInstance.imeOptions.flagNoEnterAction) {
activeEditorInstance.performEnter()
} else {
@@ -511,45 +509,70 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
}
/**
* Handles a [KeyCode.SHIFT] event.
* Handles a [KeyCode.SHIFT] down event.
*/
private fun handleShift() {
if (hasCapsRecentlyChanged) {
osHandler.removeCallbacksAndMessages(null)
private fun handleShiftDown(ev: InputKeyEvent) {
if (ev.isConsecutiveEventOf(inputEventDispatcher.lastKeyEventDown, florisboard.prefs.keyboard.longPressDelay.toLong())) {
newCapsState = true
caps = true
capsLock = true
hasCapsRecentlyChanged = false
} else {
caps = !caps
newCapsState = !caps
caps = true
capsLock = false
hasCapsRecentlyChanged = true
osHandler.postDelayed({
hasCapsRecentlyChanged = false
}, 300)
}
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
smartbarView?.updateCandidateSuggestionCapsState()
}
/**
* Handles a [KeyCode.SHIFT] up event.
*/
private fun handleShiftUp() {
caps = newCapsState
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
smartbarView?.updateCandidateSuggestionCapsState()
}
/**
* Handles a [KeyCode.SHIFT] cancel event.
*/
private fun handleShiftCancel() {
caps = false
capsLock = false
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
smartbarView?.updateCandidateSuggestionCapsState()
}
/**
* Handles a [KeyCode.SHIFT] up event.
*/
private fun handleShiftLock() {
val lastKeyEvent = inputEventDispatcher.lastKeyEventDown ?: return
if (lastKeyEvent.data.code == KeyCode.SHIFT && lastKeyEvent.action == InputKeyEvent.Action.DOWN) {
newCapsState = true
caps = true
capsLock = true
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
smartbarView?.updateCandidateSuggestionCapsState()
}
}
/**
* Handles a [KeyCode.SPACE] event. Also handles the auto-correction of two space taps if
* enabled by the user.
*/
private fun handleSpace() {
private fun handleSpace(ev: InputKeyEvent) {
if (florisboard.prefs.keyboard.spaceBarSwitchesToCharacters && getActiveKeyboardMode() != KeyboardMode.CHARACTERS) {
setActiveKeyboardMode(KeyboardMode.CHARACTERS)
}
if (florisboard.prefs.correction.doubleSpacePeriod) {
if (hasSpaceRecentlyPressed) {
osHandler.removeCallbacksAndMessages(null)
if (ev.isConsecutiveEventOf(inputEventDispatcher.lastKeyEventUp, florisboard.prefs.keyboard.longPressDelay.toLong())) {
val text = activeEditorInstance.getTextBeforeCursor(2)
if (text.length == 2 && !text.matches("""[.!?‽\s][\s]""".toRegex())) {
activeEditorInstance.deleteBackwards()
activeEditorInstance.commitText(".")
}
hasSpaceRecentlyPressed = false
} else {
hasSpaceRecentlyPressed = true
osHandler.postDelayed({
hasSpaceRecentlyPressed = false
}, 300)
}
}
activeEditorInstance.commitText(KeyCode.SPACE.toChar().toString())
@@ -558,119 +581,66 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
/**
* Handles [KeyCode] arrow and move events, behaves differently depending on text selection.
*/
private fun handleArrow(code: Int) = activeEditorInstance.run {
val selectionStartMin = 0
val selectionEndMax = cachedInput.expectedMaxLength
if (selection.isSelectionMode && isManualSelectionMode) {
// Text is selected and it is manual selection -> Expand selection depending on started
// direction.
when (code) {
KeyCode.ARROW_DOWN -> {}
KeyCode.ARROW_LEFT -> {
if (isManualSelectionModeLeft) {
selection.updateAndNotify(
(selection.start - 1).coerceAtLeast(selectionStartMin),
selection.end
)
} else {
selection.updateAndNotify(selection.start, selection.end - 1)
}
}
KeyCode.ARROW_RIGHT -> {
if (isManualSelectionModeRight) {
selection.updateAndNotify(
selection.start,
(selection.end + 1).coerceAtMost(selectionEndMax)
)
} else {
selection.updateAndNotify(selection.start + 1, selection.end)
}
}
KeyCode.ARROW_UP -> {}
KeyCode.MOVE_START_OF_LINE -> {
if (isManualSelectionModeLeft) {
selection.updateAndNotify(selectionStartMin, selection.end)
} else {
selection.updateAndNotify(selectionStartMin, selection.start)
}
}
KeyCode.MOVE_END_OF_LINE -> {
if (isManualSelectionModeRight) {
selection.updateAndNotify(selection.start, selectionEndMax)
} else {
selection.updateAndNotify(selection.end, selectionEndMax)
}
private fun handleArrow(code: Int, count: Int) = activeEditorInstance.apply {
val isShiftPressed = isManualSelectionMode || inputEventDispatcher.isPressed(KeyCode.SHIFT)
when (code) {
KeyCode.ARROW_DOWN -> {
if (!selection.isSelectionMode && isManualSelectionMode) {
isManualSelectionModeStart = false
isManualSelectionModeEnd = true
}
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, meta(shift = isShiftPressed), count)
}
} else if (selection.isSelectionMode && !isManualSelectionMode) {
// Text is selected but no manual selection mode -> arrows behave as if selection was
// started in manual left mode
when (code) {
KeyCode.ARROW_DOWN -> {}
KeyCode.ARROW_LEFT -> {
selection.updateAndNotify(selection.start, selection.end - 1)
}
KeyCode.ARROW_RIGHT -> {
selection.updateAndNotify(
selection.start,
(selection.end + 1).coerceAtMost(selectionEndMax)
)
}
KeyCode.ARROW_UP -> {}
KeyCode.MOVE_START_OF_LINE -> {
selection.updateAndNotify(selectionStartMin, selection.start)
}
KeyCode.MOVE_END_OF_LINE -> {
selection.updateAndNotify(selection.start, selectionEndMax)
KeyCode.ARROW_LEFT -> {
if (!selection.isSelectionMode && isManualSelectionMode) {
isManualSelectionModeStart = true
isManualSelectionModeEnd = false
}
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, meta(shift = isShiftPressed), count)
}
} else if (!selection.isSelectionMode && isManualSelectionMode) {
// No text is selected but manual selection mode is active, user wants to start a new
// selection. Must set manual selection direction.
when (code) {
KeyCode.ARROW_DOWN -> {}
KeyCode.ARROW_LEFT -> {
selection.updateAndNotify(
(selection.start - 1).coerceAtLeast(selectionStartMin),
selection.start
)
isManualSelectionModeLeft = true
isManualSelectionModeRight = false
}
KeyCode.ARROW_RIGHT -> {
selection.updateAndNotify(
selection.end,
(selection.end + 1).coerceAtMost(selectionEndMax)
)
isManualSelectionModeLeft = false
isManualSelectionModeRight = true
}
KeyCode.ARROW_UP -> {}
KeyCode.MOVE_START_OF_LINE -> {
selection.updateAndNotify(selectionStartMin, selection.start)
isManualSelectionModeLeft = true
isManualSelectionModeRight = false
}
KeyCode.MOVE_END_OF_LINE -> {
selection.updateAndNotify(selection.end, selectionEndMax)
isManualSelectionModeLeft = false
isManualSelectionModeRight = true
KeyCode.ARROW_RIGHT -> {
if (!selection.isSelectionMode && isManualSelectionMode) {
isManualSelectionModeStart = false
isManualSelectionModeEnd = true
}
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT, meta(shift = isShiftPressed), count)
}
} else {
// No selection and no manual selection mode -> move cursor around
when (code) {
KeyCode.ARROW_DOWN -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN)
KeyCode.ARROW_LEFT -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT)
KeyCode.ARROW_RIGHT -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT)
KeyCode.ARROW_UP -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_UP)
KeyCode.MOVE_START_OF_PAGE -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_UP)
KeyCode.MOVE_END_OF_PAGE -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_DOWN)
KeyCode.MOVE_START_OF_LINE -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_LEFT)
KeyCode.MOVE_END_OF_LINE -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_RIGHT)
KeyCode.ARROW_UP -> {
if (!selection.isSelectionMode && isManualSelectionMode) {
isManualSelectionModeStart = true
isManualSelectionModeEnd = false
}
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_UP, meta(shift = isShiftPressed), count)
}
KeyCode.MOVE_START_OF_PAGE -> {
if (!selection.isSelectionMode && isManualSelectionMode) {
isManualSelectionModeStart = true
isManualSelectionModeEnd = false
}
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_UP, meta(alt = true, shift = isShiftPressed), count)
}
KeyCode.MOVE_END_OF_PAGE -> {
if (!selection.isSelectionMode && isManualSelectionMode) {
isManualSelectionModeStart = false
isManualSelectionModeEnd = true
}
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, meta(alt = true, shift = isShiftPressed), count)
}
KeyCode.MOVE_START_OF_LINE -> {
if (!selection.isSelectionMode && isManualSelectionMode) {
isManualSelectionModeStart = true
isManualSelectionModeEnd = false
}
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, meta(alt = true, shift = isShiftPressed), count)
}
KeyCode.MOVE_END_OF_LINE -> {
if (!selection.isSelectionMode && isManualSelectionMode) {
isManualSelectionModeStart = false
isManualSelectionModeEnd = true
}
sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT, meta(alt = true, shift = isShiftPressed), count)
}
}
Unit
}
/**
@@ -678,7 +648,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
*/
private fun handleClipboardSelect() = activeEditorInstance.apply {
if (selection.isSelectionMode) {
if (isManualSelectionMode && isManualSelectionModeLeft) {
if (isManualSelectionMode && isManualSelectionModeStart) {
selection.updateAndNotify(selection.start, selection.start)
} else {
selection.updateAndNotify(selection.end, selection.end)
@@ -692,14 +662,28 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
}
/**
* Main logic point for sending a key press. Different actions may occur depending on the given
* [KeyData]. This method handles all key press send events, which are text based. For media
* input send events see MediaInputManager.
*
* @param keyData The [KeyData] object which should be sent.
* Adjusts a given key data for caps state and returns the correct reference.
*/
fun sendKeyPress(keyData: KeyData) {
when (keyData.code) {
private fun getAdjustedKeyData(keyData: KeyData): KeyData {
return if (caps && keyData is FlorisKeyData && keyData.shift != null) { keyData.shift!! } else { keyData }
}
override fun onInputKeyDown(ev: InputKeyEvent) {
val data = getAdjustedKeyData(ev.data)
when (data.code) {
KeyCode.INTERNAL_BATCH_EDIT -> {
florisboard.beginInternalBatchEdit()
return
}
KeyCode.SHIFT -> {
handleShiftDown(ev)
}
}
}
override fun onInputKeyUp(ev: InputKeyEvent) {
val data = getAdjustedKeyData(ev.data)
when (data.code) {
KeyCode.ARROW_DOWN,
KeyCode.ARROW_LEFT,
KeyCode.ARROW_RIGHT,
@@ -707,29 +691,33 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
KeyCode.MOVE_START_OF_PAGE,
KeyCode.MOVE_END_OF_PAGE,
KeyCode.MOVE_START_OF_LINE,
KeyCode.MOVE_END_OF_LINE -> handleArrow(keyData.code)
KeyCode.MOVE_END_OF_LINE -> if (ev.action == InputKeyEvent.Action.DOWN_UP || ev.action == InputKeyEvent.Action.REPEAT) {
handleArrow(data.code, ev.count)
} else {
handleArrow(data.code, 1)
}
KeyCode.CLIPBOARD_CUT -> activeEditorInstance.performClipboardCut()
KeyCode.CLIPBOARD_COPY -> activeEditorInstance.performClipboardCopy()
KeyCode.CLIPBOARD_PASTE -> {
activeEditorInstance.performClipboardPaste()
smartbarView?.resetClipboardSuggestion()
}
KeyCode.CLIPBOARD_PASTE -> activeEditorInstance.performClipboardPaste()
KeyCode.CLIPBOARD_SELECT -> handleClipboardSelect()
KeyCode.CLIPBOARD_SELECT_ALL -> activeEditorInstance.performClipboardSelectAll()
KeyCode.DELETE -> {
handleDelete()
smartbarView?.resetClipboardSuggestion()
}
KeyCode.ENTER -> {
handleEnter()
smartbarView?.resetClipboardSuggestion()
KeyCode.DELETE -> handleDelete()
KeyCode.DELETE_WORD -> handleDeleteWord()
KeyCode.ENTER -> handleEnter()
KeyCode.INTERNAL_BATCH_EDIT -> {
florisboard.endInternalBatchEdit()
return
}
KeyCode.LANGUAGE_SWITCH -> handleLanguageSwitch()
KeyCode.SETTINGS -> florisboard.launchSettings()
KeyCode.SHIFT -> handleShift()
KeyCode.SHIFT -> handleShiftUp()
KeyCode.SHIFT_LOCK -> handleShiftLock()
KeyCode.SHOW_INPUT_METHOD_PICKER -> florisboard.imeManager?.showInputMethodPicker()
KeyCode.SPACE -> handleSpace(ev)
KeyCode.SWITCH_TO_MEDIA_CONTEXT -> florisboard.setActiveInput(R.id.media_input)
KeyCode.SWITCH_TO_CLIPBOARD_CONTEXT -> florisboard.setActiveInput(R.id.clip_input)
KeyCode.SWITCH_TO_TEXT_CONTEXT -> florisboard.setActiveInput(R.id.text_input)
KeyCode.CLEAR_CLIPBOARD_HISTORY -> florisboard.florisClipboardManager?.clearHistoryWithAnimation()
KeyCode.TOGGLE_ONE_HANDED_MODE_LEFT -> florisboard.toggleOneHandedMode(isRight = false)
KeyCode.TOGGLE_ONE_HANDED_MODE_RIGHT -> florisboard.toggleOneHandedMode(isRight = true)
KeyCode.VIEW_CHARACTERS -> setActiveKeyboardMode(KeyboardMode.CHARACTERS)
@@ -744,49 +732,57 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
KeyboardMode.NUMERIC,
KeyboardMode.NUMERIC_ADVANCED,
KeyboardMode.PHONE,
KeyboardMode.PHONE2 -> when (keyData.type) {
KeyboardMode.PHONE2 -> when (data.type) {
KeyType.CHARACTER,
KeyType.NUMERIC -> {
val text = keyData.code.toChar().toString()
val text = data.code.toChar().toString()
activeEditorInstance.commitText(text)
}
else -> when (keyData.code) {
else -> when (data.code) {
KeyCode.PHONE_PAUSE,
KeyCode.PHONE_WAIT -> {
val text = keyData.code.toChar().toString()
val text = data.code.toChar().toString()
activeEditorInstance.commitText(text)
}
}
}
else -> when (keyData.type) {
KeyType.CHARACTER, KeyType.NUMERIC -> when (keyData.code) {
KeyCode.SPACE -> handleSpace()
else -> when (data.type) {
KeyType.CHARACTER, KeyType.NUMERIC -> when (data.code) {
KeyCode.URI_COMPONENT_TLD -> {
val tld = keyData.label.toLowerCase(Locale.ENGLISH)
val tld = data.label.toLowerCase(Locale.ENGLISH)
activeEditorInstance.commitText(tld)
}
else -> {
hasCapsRecentlyChanged = false
hasSpaceRecentlyPressed = false
var text = keyData.code.toChar().toString()
var text = data.code.toChar().toString()
val locale = if (florisboard.activeSubtype.locale.language == "el") { Locale.getDefault() } else { florisboard.activeSubtype.locale }
text = when (caps && activeKeyboardMode == KeyboardMode.CHARACTERS) {
true -> text.toUpperCase(florisboard.activeSubtype.locale)
true -> text.toUpperCase(locale)
false -> text
}
activeEditorInstance.commitText(text)
}
}
else -> {
Timber.e("sendKeyPress(keyData): Received unknown key: $keyData")
Timber.e("sendKeyPress(keyData): Received unknown key: $data")
}
}
}
smartbarView?.resetClipboardSuggestion()
}
}
if (keyData.code != KeyCode.SHIFT && !capsLock) {
if (data.code != KeyCode.SHIFT && !capsLock && !inputEventDispatcher.isPressed(KeyCode.SHIFT)) {
updateCapsState()
}
smartbarView?.updateSmartbarState()
}
override fun onInputKeyRepeat(ev: InputKeyEvent) {
onInputKeyUp(ev)
}
override fun onInputKeyCancel(ev: InputKeyEvent) {
val data = getAdjustedKeyData(ev.data)
when (data.code) {
KeyCode.SHIFT -> handleShiftCancel()
}
}
}

View File

@@ -30,6 +30,7 @@ import android.widget.Button
import androidx.appcompat.widget.AppCompatImageButton
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.InputKeyEvent
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyData
@@ -46,10 +47,25 @@ class EditingKeyView : AppCompatImageButton, ThemeManager.OnThemeUpdatedListener
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
private val themeManager: ThemeManager = ThemeManager.default()
private val data: KeyData
private val data: KeyData = when (id) {
R.id.arrow_down -> KeyData.ARROW_DOWN
R.id.arrow_left -> KeyData.ARROW_LEFT
R.id.arrow_right -> KeyData.ARROW_RIGHT
R.id.arrow_up -> KeyData.ARROW_UP
R.id.backspace -> KeyData.DELETE
R.id.clipboard_copy -> KeyData.CLIPBOARD_COPY
R.id.clipboard_cut -> KeyData.CLIPBOARD_CUT
R.id.clipboard_paste -> KeyData.CLIPBOARD_PASTE
R.id.move_start_of_line -> KeyData.MOVE_START_OF_LINE
R.id.move_end_of_line -> KeyData.MOVE_END_OF_LINE
R.id.select -> KeyData.CLIPBOARD_SELECT
R.id.select_all -> KeyData.CLIPBOARD_SELECT_ALL
else -> KeyData.UNSPECIFIED
}
private var isKeyPressed: Boolean = false
private val repeatedKeyPressHandler: Handler = Handler(context.mainLooper)
private val defaultTextSize: Float = Button(context).textSize
private var label: String? = null
private var labelPaint: Paint = Paint().apply {
alpha = 255
@@ -57,7 +73,7 @@ class EditingKeyView : AppCompatImageButton, ThemeManager.OnThemeUpdatedListener
isAntiAlias = true
isFakeBoldText = false
textAlign = Paint.Align.CENTER
textSize = Button(context).textSize
textSize = defaultTextSize
typeface = Typeface.DEFAULT
}
@@ -71,22 +87,6 @@ class EditingKeyView : AppCompatImageButton, ThemeManager.OnThemeUpdatedListener
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, R.style.TextEditingButton)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
val code = when (id) {
R.id.arrow_down -> KeyCode.ARROW_DOWN
R.id.arrow_left -> KeyCode.ARROW_LEFT
R.id.arrow_right -> KeyCode.ARROW_RIGHT
R.id.arrow_up -> KeyCode.ARROW_UP
R.id.backspace -> KeyCode.DELETE
R.id.clipboard_copy -> KeyCode.CLIPBOARD_COPY
R.id.clipboard_cut -> KeyCode.CLIPBOARD_CUT
R.id.clipboard_paste -> KeyCode.CLIPBOARD_PASTE
R.id.move_start_of_line -> KeyCode.MOVE_START_OF_LINE
R.id.move_end_of_line -> KeyCode.MOVE_END_OF_LINE
R.id.select -> KeyCode.CLIPBOARD_SELECT
R.id.select_all -> KeyCode.CLIPBOARD_SELECT_ALL
else -> 0
}
data = KeyData(code = code)
context.obtainStyledAttributes(attrs, R.styleable.EditingKeyView).apply {
label = getString(R.styleable.EditingKeyView_android_text)
recycle()
@@ -123,7 +123,7 @@ class EditingKeyView : AppCompatImageButton, ThemeManager.OnThemeUpdatedListener
val delayMillis = prefs.keyboard.longPressDelay.toLong()
repeatedKeyPressHandler.postAtScheduledRate(delayMillis, 25) {
if (isKeyPressed) {
florisboard?.textInputManager?.sendKeyPress(data)
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.downUp(data))
} else {
repeatedKeyPressHandler.cancelAll()
}
@@ -135,7 +135,7 @@ class EditingKeyView : AppCompatImageButton, ThemeManager.OnThemeUpdatedListener
isKeyPressed = false
repeatedKeyPressHandler.cancelAll()
if (event.actionMasked != MotionEvent.ACTION_CANCEL) {
florisboard?.textInputManager?.sendKeyPress(data)
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.downUp(data))
}
}
else -> return false
@@ -173,8 +173,10 @@ class EditingKeyView : AppCompatImageButton, ThemeManager.OnThemeUpdatedListener
}
val isPortrait =
resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
if (!isPortrait) {
labelPaint.textSize *= 0.9f
labelPaint.textSize = if (isPortrait) {
defaultTextSize
} else {
defaultTextSize * 0.9f
}
val centerX = measuredWidth / 2.0f
val centerY = measuredHeight / 2.0f + (labelPaint.textSize - labelPaint.descent()) / 2

View File

@@ -35,8 +35,6 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener,
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
private val themeManager: ThemeManager = ThemeManager.default()
private var arrowUpKey: EditingKeyView? = null
private var arrowDownKey: EditingKeyView? = null
private var selectKey: EditingKeyView? = null
private var selectAllKey: EditingKeyView? = null
private var cutKey: EditingKeyView? = null
@@ -52,8 +50,6 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener,
florisboard?.addEventListener(this)
themeManager.registerOnThemeUpdatedListener(this)
arrowUpKey = findViewById(R.id.arrow_up)
arrowDownKey = findViewById(R.id.arrow_down)
selectKey = findViewById(R.id.select)
selectAllKey = findViewById(R.id.select_all)
cutKey = findViewById(R.id.clipboard_cut)
@@ -74,8 +70,6 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener,
override fun onUpdateSelection() {
val isSelectionActive = florisboard?.activeEditorInstance?.selection?.isSelectionMode ?: false
val isSelectionMode = florisboard?.textInputManager?.isManualSelectionMode ?: false
arrowUpKey?.isEnabled = !(isSelectionActive || isSelectionMode)
arrowDownKey?.isEnabled = !(isSelectionActive || isSelectionMode)
selectKey?.isHighlighted = isSelectionActive || isSelectionMode
selectAllKey?.visibility = when {
isSelectionActive -> View.GONE
@@ -86,7 +80,11 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener,
else -> View.GONE
}
copyKey?.isEnabled = isSelectionActive
pasteKey?.isEnabled = florisboard?.clipboardManager?.hasPrimaryClip() ?: false
pasteKey?.isEnabled =
florisboard?.florisClipboardManager?.hasPrimaryClip() == true &&
florisboard.activeEditorInstance.contentMimeTypes?.any {
florisboard.florisClipboardManager!!.primaryClip?.mimeTypes?.contains(it) ?: false
} == true
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {

View File

@@ -40,6 +40,7 @@ enum class SwipeAction {
SHOW_INPUT_METHOD_PICKER,
SWITCH_TO_PREV_SUBTYPE,
SWITCH_TO_NEXT_SUBTYPE,
SWITCH_TO_CLIPBOARD_CONTEXT,
SWITCH_TO_PREV_KEYBOARD;
companion object {

View File

@@ -64,10 +64,7 @@ abstract class SwipeGesture {
* @property listener The listener to report detected swipes to.
*/
class Detector(private val context: Context, private val listener: Listener) {
private var firstMotionEvent: MotionEvent? = null
private var lastMotionEvent: MotionEvent? = null
private var absUnitCountX: Int = 0
private var absUnitCountY: Int = 0
private var pointerDataMap: MutableMap<Int, PointerData> = mutableMapOf()
private var thresholdWidth: Double = numericValue(context, DistanceThreshold.NORMAL)
private var unitWidth: Double = thresholdWidth / 4.0
@@ -92,67 +89,83 @@ abstract class SwipeGesture {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_POINTER_DOWN -> {
resetState()
firstMotionEvent = MotionEvent.obtainNoHistory(event)
lastMotionEvent = firstMotionEvent
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
resetState()
}
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
pointerDataMap[pointerId] = PointerData().apply {
firstX = event.getX(pointerIndex)
firstY = event.getY(pointerIndex)
lastX = firstX
lastY = firstY
}
}
MotionEvent.ACTION_MOVE -> {
val firstEvent = firstMotionEvent ?: return false
val absDiffX = event.x - firstEvent.x
val absDiffY = event.y - firstEvent.y
val lastEvent = lastMotionEvent ?: return false
val relDiffX = event.x - lastEvent.x
val relDiffY = event.y - lastEvent.y
return if (alwaysTriggerOnMove || abs(relDiffX) > (thresholdWidth / 2.0) || abs(relDiffY) > (thresholdWidth / 2.0)) {
lastMotionEvent = MotionEvent.obtainNoHistory(event)
val direction = detectDirection(relDiffX.toDouble(), relDiffY.toDouble())
val newAbsUnitCountX = (absDiffX / unitWidth).toInt()
val newAbsUnitCountY = (absDiffY / unitWidth).toInt()
val relUnitCountX = newAbsUnitCountX - absUnitCountX
val relUnitCountY = newAbsUnitCountY - absUnitCountY
absUnitCountX = newAbsUnitCountX
absUnitCountY = newAbsUnitCountY
listener.onSwipe(Event(
direction = direction,
type = Type.TOUCH_MOVE,
absUnitCountX,
absUnitCountY,
relUnitCountX,
relUnitCountY
))
} else {
false
for (pointerIndex in 0 until event.pointerCount) {
val pointerId = event.getPointerId(pointerIndex)
pointerDataMap[pointerId]?.apply {
val absDiffX = event.getX(pointerIndex) - firstX
val absDiffY = event.getY(pointerIndex) - firstY
val relDiffX = event.getX(pointerIndex) - lastX
val relDiffY = event.getY(pointerIndex) - lastY
return if (alwaysTriggerOnMove || abs(relDiffX) > (thresholdWidth / 2.0) || abs(relDiffY) > (thresholdWidth / 2.0)) {
lastX = event.getX(pointerIndex)
lastY = event.getY(pointerIndex)
val direction = detectDirection(relDiffX.toDouble(), relDiffY.toDouble())
val newAbsUnitCountX = (absDiffX / unitWidth).toInt()
val newAbsUnitCountY = (absDiffY / unitWidth).toInt()
val relUnitCountX = newAbsUnitCountX - absUnitCountX
val relUnitCountY = newAbsUnitCountY - absUnitCountY
absUnitCountX = newAbsUnitCountX
absUnitCountY = newAbsUnitCountY
listener.onSwipe(Event(
direction = direction,
type = Type.TOUCH_MOVE,
pointerId,
absUnitCountX,
absUnitCountY,
relUnitCountX,
relUnitCountY
))
} else {
false
}
}
}
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP -> {
val firstEvent = firstMotionEvent ?: return false
val absDiffX = event.x - firstEvent.x
val absDiffY = event.y - firstEvent.y
/*val velocityThresholdNV = numericValue(velocityThreshold)
val velocity =
((convertPixelsToDp(
sqrt(diffX.pow(2) + diffY.pow(2)),
context
) / event.downTime) * 10.0f.pow(8)).toInt()*/
// return if ((abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV) && velocity >= velocityThresholdNV) {
val ret = if ((abs(absDiffX) > thresholdWidth || abs(absDiffY) > thresholdWidth)) {
val direction = detectDirection(absDiffX.toDouble(), absDiffY.toDouble())
absUnitCountX = (absDiffX / unitWidth).toInt()
absUnitCountY = (absDiffY / unitWidth).toInt()
listener.onSwipe(Event(
direction = direction,
type = Type.TOUCH_UP,
absUnitCountX,
absUnitCountY,
absUnitCountX,
absUnitCountY
))
} else {
false
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
pointerDataMap.remove(pointerId)?.apply {
val absDiffX = event.getX(pointerIndex) - firstX
val absDiffY = event.getY(pointerIndex) - firstY
/*val velocityThresholdNV = numericValue(velocityThreshold)
val velocity =
((convertPixelsToDp(
sqrt(diffX.pow(2) + diffY.pow(2)),
context
) / event.downTime) * 10.0f.pow(8)).toInt()*/
// return if ((abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV) && velocity >= velocityThresholdNV) {
return if ((abs(absDiffX) > thresholdWidth || abs(absDiffY) > thresholdWidth)) {
val direction = detectDirection(absDiffX.toDouble(), absDiffY.toDouble())
absUnitCountX = (absDiffX / unitWidth).toInt()
absUnitCountY = (absDiffY / unitWidth).toInt()
listener.onSwipe(Event(
direction = direction,
type = Type.TOUCH_UP,
pointerId,
absUnitCountX,
absUnitCountY,
absUnitCountX,
absUnitCountY
))
} else {
false
}
}
resetState()
return ret
return false
}
MotionEvent.ACTION_CANCEL -> {
resetState()
@@ -160,7 +173,7 @@ abstract class SwipeGesture {
else -> return false
}
return false
} catch(e: Exception) {
} catch (e: Exception) {
return false
}
}
@@ -208,10 +221,16 @@ abstract class SwipeGesture {
* Resets the state.
*/
private fun resetState() {
firstMotionEvent = null
lastMotionEvent = null
absUnitCountX = 0
absUnitCountY = 0
pointerDataMap.clear()
}
class PointerData {
var firstX: Float = 0.0f
var firstY: Float = 0.0f
var lastX: Float = 0.0f
var lastY: Float = 0.0f
var absUnitCountX: Int = 0
var absUnitCountY: Int = 0
}
}
@@ -219,7 +238,7 @@ abstract class SwipeGesture {
* An interface which provides an abstract callback function, which will be called for any
* detected swipe event.
*/
fun interface Listener {
interface Listener {
fun onSwipe(event: Event): Boolean
}
@@ -231,6 +250,8 @@ abstract class SwipeGesture {
val direction: Direction,
/** The type of the swipe. */
val type: Type,
/** The pointer ID of this event, corresponds to the value reported by the original MotionEvent. */
val pointerId: Int,
/** The unit count on the x-axis, measured from the first event (ACTION_DOWN). */
val absUnitCountX: Int,
/** The unit count on the y-axis, measured from the first event (ACTION_DOWN). */

View File

@@ -50,7 +50,7 @@ object KeyCode {
const val CLEAR_INPUT = -13
const val VOICE_INPUT = -4
const val DISABLED = 0
const val UNSPECIFIED = 0
const val SPLIT_LAYOUT = -110
const val MERGE_LAYOUT = -111
@@ -65,9 +65,10 @@ object KeyCode {
const val CLIPBOARD_PASTE_POPUP = -133
const val CLIPBOARD_SELECT = -134
const val CLIPBOARD_SELECT_ALL = -135
const val CLEAR_CLIPBOARD_HISTORY = -136
const val UNDO = -136
const val REDO = -137
const val UNDO = -137
const val REDO = -138
const val PHONE_PAUSE = 44
const val PHONE_WAIT = 59
@@ -89,6 +90,8 @@ object KeyCode {
const val TOGGLE_ONE_HANDED_MODE_RIGHT =-216
const val URI_COMPONENT_TLD = -255
const val INTERNAL_BATCH_EDIT = -901
const val KESHIDA = 1600
const val HALF_SPACE = 8204
}

View File

@@ -34,7 +34,180 @@ open class KeyData(
var type: KeyType = KeyType.CHARACTER,
var code: Int = 0,
var label: String = ""
)
) {
companion object {
/** Predefined key data for [KeyCode.ARROW_DOWN] */
val ARROW_DOWN = KeyData(
type = KeyType.NAVIGATION,
code = KeyCode.ARROW_DOWN,
label = "arrow_down"
)
/** Predefined key data for [KeyCode.ARROW_LEFT] */
val ARROW_LEFT = KeyData(
type = KeyType.NAVIGATION,
code = KeyCode.ARROW_LEFT,
label = "arrow_left"
)
/** Predefined key data for [KeyCode.ARROW_RIGHT] */
val ARROW_RIGHT = KeyData(
type = KeyType.NAVIGATION,
code = KeyCode.ARROW_RIGHT,
label = "arrow_right"
)
/** Predefined key data for [KeyCode.ARROW_UP] */
val ARROW_UP = KeyData(
type = KeyType.NAVIGATION,
code = KeyCode.ARROW_UP,
label = "arrow_up"
)
/** Predefined key data for [KeyCode.CLIPBOARD_COPY] */
val CLIPBOARD_COPY = KeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.CLIPBOARD_COPY,
label = "clipboard_copy"
)
/** Predefined key data for [KeyCode.CLIPBOARD_CUT] */
val CLIPBOARD_CUT = KeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.CLIPBOARD_CUT,
label = "clipboard_cut"
)
/** Predefined key data for [KeyCode.CLIPBOARD_PASTE] */
val CLIPBOARD_PASTE = KeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.CLIPBOARD_PASTE,
label = "clipboard_paste"
)
/** Predefined key data for [KeyCode.CLIPBOARD_SELECT] */
val CLIPBOARD_SELECT = KeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.CLIPBOARD_SELECT,
label = "clipboard_select"
)
/** Predefined key data for [KeyCode.CLIPBOARD_SELECT_ALL] */
val CLIPBOARD_SELECT_ALL = KeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.CLIPBOARD_SELECT_ALL,
label = "clipboard_select_all"
)
/** Predefined key data for [KeyCode.DELETE] */
val DELETE = KeyData(
type = KeyType.ENTER_EDITING,
code = KeyCode.DELETE,
label = "delete"
)
/** Predefined key data for [KeyCode.DELETE_WORD] */
val DELETE_WORD = KeyData(
type = KeyType.ENTER_EDITING,
code = KeyCode.DELETE_WORD,
label = "delete_word"
)
/** Predefined key data for [KeyCode.INTERNAL_BATCH_EDIT] */
val INTERNAL_BATCH_EDIT = KeyData(
type = KeyType.FUNCTION,
code = KeyCode.INTERNAL_BATCH_EDIT,
label = "internal_batch_edit"
)
/** Predefined key data for [KeyCode.MOVE_START_OF_LINE] */
val MOVE_START_OF_LINE = KeyData(
type = KeyType.NAVIGATION,
code = KeyCode.MOVE_START_OF_LINE,
label = "move_start_of_line"
)
/** Predefined key data for [KeyCode.MOVE_END_OF_LINE] */
val MOVE_END_OF_LINE = KeyData(
type = KeyType.NAVIGATION,
code = KeyCode.MOVE_END_OF_LINE,
label = "move_end_of_line"
)
/** Predefined key data for [KeyCode.MOVE_START_OF_PAGE] */
val MOVE_START_OF_PAGE = KeyData(
type = KeyType.NAVIGATION,
code = KeyCode.MOVE_START_OF_PAGE,
label = "move_start_of_page"
)
/** Predefined key data for [KeyCode.MOVE_END_OF_PAGE] */
val MOVE_END_OF_PAGE = KeyData(
type = KeyType.NAVIGATION,
code = KeyCode.MOVE_END_OF_PAGE,
label = "move_end_of_page"
)
/** Predefined key data for [KeyCode.SHOW_INPUT_METHOD_PICKER] */
val SHOW_INPUT_METHOD_PICKER = KeyData(
type = KeyType.FUNCTION,
code = KeyCode.SHOW_INPUT_METHOD_PICKER,
label = "show_input_method_picker"
)
/** Predefined key data for [KeyCode.SWITCH_TO_TEXT_CONTEXT] */
val SWITCH_TO_TEXT_CONTEXT = KeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.SWITCH_TO_TEXT_CONTEXT,
label = "switch_to_text_context"
)
/** Predefined key data for [KeyCode.SWITCH_TO_CLIPBOARD_CONTEXT] */
val SWITCH_TO_CLIPBOARD_CONTEXT = KeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.SWITCH_TO_CLIPBOARD_CONTEXT,
label = "switch_to_clipboard_context"
)
/** Predefined key data for [KeyCode.SHIFT] */
val SHIFT = KeyData(
type = KeyType.MODIFIER,
code = KeyCode.SHIFT,
label = "shift"
)
/** Predefined key data for [KeyCode.SHIFT_LOCK] */
val SHIFT_LOCK = KeyData(
type = KeyType.MODIFIER,
code = KeyCode.SHIFT_LOCK,
label = "shift_lock"
)
/** Predefined key data for [KeyCode.SPACE] */
val SPACE = KeyData(
type = KeyType.CHARACTER,
code = KeyCode.SPACE,
label = "space"
)
/** Predefined key data for [KeyCode.UNSPECIFIED] */
val UNSPECIFIED = KeyData(
type = KeyType.UNSPECIFIED,
code = KeyCode.UNSPECIFIED,
label = "unspecified"
)
/** Predefined key data for [KeyCode.VIEW_CHARACTERS] */
val VIEW_CHARACTERS = KeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.VIEW_CHARACTERS,
label = "view_characters"
)
}
override fun toString(): String {
return "KeyData { type=$type code=$code label=\"$label\" }"
}
}
/**
* Data class which describes a single key and its attributes, while also providing additional
@@ -47,6 +220,10 @@ open class KeyData(
* or if the key should always be visible. Defaults to [KeyVariation.ALL].
* @property popup List of keys which will be accessible while long pressing the key. Defaults to
* an empty set (no extended popup).
* @property shift An alternative key to use when the keyboard caps state is true. Useful for layouts
* such as Colemak and Dvorak. Defaults to null (don't override base uppercase key). This override
* property should only be used to provide an uppercase variant of two else not related variants, but
* should not be used for providing an uppercase letter (e.g. 'a' -> 'A').
*/
class FlorisKeyData(
type: KeyType = KeyType.CHARACTER,
@@ -54,7 +231,8 @@ class FlorisKeyData(
label: String = "",
var groupId: Int = GROUP_DEFAULT,
var variation: KeyVariation = KeyVariation.ALL,
var popup: PopupSet<KeyData> = PopupSet()
var popup: PopupSet<KeyData> = PopupSet(),
var shift: KeyData? = null
) : KeyData(type, code, label) {
companion object {
/**

View File

@@ -18,7 +18,11 @@ package dev.patrickgold.florisboard.ime.text.key
import android.annotation.SuppressLint
import android.content.res.Configuration
import android.graphics.*
import android.graphics.Canvas
import android.graphics.Outline
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.graphics.drawable.PaintDrawable
import android.os.Handler
@@ -32,8 +36,10 @@ import com.google.android.flexbox.FlexboxLayout
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.ImeOptions
import dev.patrickgold.florisboard.ime.core.InputKeyEvent
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.popup.PopupManager
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.gestures.SwipeGesture
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
@@ -41,7 +47,9 @@ import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import dev.patrickgold.florisboard.ime.theme.ThemeValue
import dev.patrickgold.florisboard.util.*
import dev.patrickgold.florisboard.util.ViewLayoutUtils
import dev.patrickgold.florisboard.util.cancelAll
import dev.patrickgold.florisboard.util.postDelayed
import java.util.*
import kotlin.math.abs
@@ -69,7 +77,7 @@ class KeyView(
private var hasTriggeredGestureMove: Boolean = false
private var keyHintMode: KeyHintMode = KeyHintMode.DISABLED
private val longKeyPressHandler: Handler = Handler(context.mainLooper)
private val repeatedKeyPressHandler: Handler = Handler(context.mainLooper)
val popupManager = PopupManager<KeyboardView, KeyView>(keyboardView, florisboard?.popupLayerView)
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
private var shouldBlockNextKeyCode: Boolean = false
@@ -115,10 +123,10 @@ class KeyView(
val keyMarginH: Int
val keyMarginV: Int
if (keyboardView.isSmartbarKeyboardView){
if (keyboardView.isSmartbarKeyboardView) {
keyMarginH = resources.getDimension(R.dimen.key_marginH).toInt()
keyMarginV = resources.getDimension(R.dimen.key_marginV).toInt()
}else {
} else {
keyMarginV = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingVertical, context).toInt()
keyMarginH = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingHorizontal, context).toInt()
}
@@ -161,7 +169,7 @@ class KeyView(
setPadding(0, 0, 0, 0)
background = backgroundDrawable
elevation = if(themeValueCache.shouldShowBorder) 4.0f else 0.0f
elevation = if (themeValueCache.shouldShowBorder) 4.0f else 0.0f
if (prefs.keyboard.hintedNumberRowMode != KeyHintMode.DISABLED && data.popup.hint?.type == KeyType.NUMERIC) {
keyHintMode = prefs.keyboard.hintedNumberRowMode
@@ -186,17 +194,23 @@ class KeyView(
*/
fun getComputedLetter(
keyData: KeyData = data,
caps: Boolean = florisboard?.textInputManager?.caps ?: false && florisboard?.textInputManager?.getActiveKeyboardMode() == KeyboardMode.CHARACTERS,
caps: Boolean = florisboard?.textInputManager?.caps ?: false &&
florisboard?.textInputManager?.getActiveKeyboardMode() == KeyboardMode.CHARACTERS,
subtype: Subtype = florisboard?.activeSubtype ?: Subtype.DEFAULT
): String {
return when (data.code) {
KeyCode.URI_COMPONENT_TLD -> keyData.label.toLowerCase(Locale.ENGLISH)
else -> {
val labelText = (keyData.code.toChar()).toString()
if (caps) {
labelText.toUpperCase(subtype.locale)
} else {
labelText
return if (caps && keyData is FlorisKeyData && keyData.shift != null) {
(keyData.shift!!.code.toChar()).toString()
} else {
when (data.code) {
KeyCode.URI_COMPONENT_TLD -> keyData.label.toLowerCase(Locale.ENGLISH)
else -> {
val labelText = (keyData.code.toChar()).toString()
val locale = if (subtype.locale.language == "el") { Locale.getDefault() } else { subtype.locale }
if (caps) {
labelText.toUpperCase(locale)
} else {
labelText
}
}
}
}
@@ -228,78 +242,82 @@ class KeyView(
*/
fun onFlorisTouchEvent(event: MotionEvent?): Boolean {
if (event == null || !isEnabled) return false
val florisboard = florisboard ?: return false
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
val alwaysTriggerOnMove = (hasTriggeredGestureMove
&& (data.code == KeyCode.DELETE && prefs.gestures.deleteKeySwipeLeft == SwipeAction.DELETE_CHARACTERS_PRECISELY
|| data.code == KeyCode.SPACE))
&& (data.code == KeyCode.DELETE && prefs.gestures.deleteKeySwipeLeft == SwipeAction.DELETE_CHARACTERS_PRECISELY
|| data.code == KeyCode.SPACE))
if (swipeGestureDetector.onTouchEvent(event, alwaysTriggerOnMove)) {
isKeyPressed = false
if (florisboard.textInputManager.inputEventDispatcher.isPressed(data.code)) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
}
longKeyPressHandler.cancelAll()
repeatedKeyPressHandler.cancelAll()
keyboardView.popupManager.hide()
popupManager.hide()
return true
}
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
if (data.code == KeyCode.SHIFT) {
isKeyPressed = true
florisboard?.keyPressVibrate()
florisboard?.keyPressSound(data)
florisboard?.textInputManager?.sendKeyPress(data)
} else {
val delayMillis = prefs.keyboard.longPressDelay.toLong()
hasTriggeredGestureMove = false
shouldBlockNextKeyCode = false
florisboard?.prefs?.keyboard?.let {
if (it.popupEnabled){
keyboardView.popupManager.show(this, keyHintMode)
}
}
isKeyPressed = true
florisboard?.keyPressVibrate()
florisboard?.keyPressSound(data)
when (data.code) {
KeyCode.ARROW_DOWN,
KeyCode.ARROW_LEFT,
KeyCode.ARROW_RIGHT,
KeyCode.ARROW_UP,
KeyCode.DELETE -> {
repeatedKeyPressHandler.postAtScheduledRate((delayMillis * 2.0f).toLong(), 25) {
if (isKeyPressed) {
florisboard?.textInputManager?.sendKeyPress(data)
} else {
repeatedKeyPressHandler.cancelAll()
}
}
}
}
if (data.code == KeyCode.SPACE) {
initSelectionStart = florisboard?.activeEditorInstance?.selection?.start ?: 0
initSelectionEnd = florisboard?.activeEditorInstance?.selection?.end ?: 0
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.down(data))
isKeyPressed = true
val delayMillis = prefs.keyboard.longPressDelay.toLong()
hasTriggeredGestureMove = false
shouldBlockNextKeyCode = false
if (florisboard.prefs.keyboard.popupEnabled) {
popupManager.show(this, keyHintMode)
}
isKeyPressed = true
florisboard.keyPressVibrate()
florisboard.keyPressSound(data)
when (data.code) {
KeyCode.SPACE -> {
initSelectionStart = florisboard.activeEditorInstance.selection.start
initSelectionEnd = florisboard.activeEditorInstance.selection.end
longKeyPressHandler.postDelayed((delayMillis * 2.5f).toLong()) {
when (prefs.gestures.spaceBarLongPress) {
SwipeAction.NO_ACTION,
SwipeAction.INSERT_SPACE -> {}
SwipeAction.INSERT_SPACE -> {
}
else -> {
florisboard?.executeSwipeAction(prefs.gestures.spaceBarLongPress)
this.florisboard.executeSwipeAction(prefs.gestures.spaceBarLongPress)
shouldBlockNextKeyCode = true
}
}
}
} else {
}
KeyCode.SHIFT -> {
longKeyPressHandler.postDelayed((delayMillis * 2.5).toLong()) {
this.florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(KeyData.SHIFT_LOCK))
florisboard.keyPressVibrate()
florisboard.keyPressSound(data)
}
}
KeyCode.LANGUAGE_SWITCH -> {
longKeyPressHandler.postDelayed((delayMillis * 2.0).toLong()) {
shouldBlockNextKeyCode = true
this.florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(KeyData.SHOW_INPUT_METHOD_PICKER))
}
}
else -> {
longKeyPressHandler.postDelayed(delayMillis) {
if (data.popup.isNotEmpty()) {
keyboardView.popupManager.extend(this, keyHintMode)
popupManager.extend(this@KeyView, keyHintMode)
}
}
}
}
}
MotionEvent.ACTION_MOVE -> {
if (keyboardView.popupManager.isShowingExtendedPopup) {
if (popupManager.isShowingExtendedPopup) {
val isPointerWithinBounds =
keyboardView.popupManager.propagateMotionEvent(this, event)
popupManager.propagateMotionEvent(this, event)
if (!isPointerWithinBounds && !shouldBlockNextKeyCode) {
keyboardView.dismissActiveKeyViewReference()
keyboardView.dismissActiveKeyViewReference(pointerId)
}
} else {
val parent = parent as ViewGroup
@@ -309,31 +327,49 @@ class KeyView(
|| event.y > 1.35f * measuredHeight
) {
if (!shouldBlockNextKeyCode) {
keyboardView.dismissActiveKeyViewReference()
keyboardView.dismissActiveKeyViewReference(pointerId)
}
}
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
MotionEvent.ACTION_UP -> {
longKeyPressHandler.cancelAll()
repeatedKeyPressHandler.cancelAll()
if (data.code != KeyCode.SHIFT) {
if (hasTriggeredGestureMove && data.code == KeyCode.DELETE) {
florisboard?.activeEditorInstance?.apply {
if (selection.isSelectionMode) {
deleteBackwards()
if (hasTriggeredGestureMove && data.code == KeyCode.DELETE) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
florisboard.activeEditorInstance.apply {
if (selection.isSelectionMode) {
deleteBackwards()
}
}
} else {
val retData = popupManager.getActiveKeyData(this)
if (!shouldBlockNextKeyCode && retData != null) {
if (retData == data) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.up(data))
} else {
if (florisboard.textInputManager.inputEventDispatcher.isPressed(data.code)) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
}
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(retData))
}
} else {
val retData = keyboardView.popupManager.getActiveKeyData(this)
if (event.actionMasked != MotionEvent.ACTION_CANCEL && !shouldBlockNextKeyCode && retData != null) {
florisboard?.textInputManager?.sendKeyPress(retData)
} else {
shouldBlockNextKeyCode = false
if (florisboard.textInputManager.inputEventDispatcher.isPressed(data.code)) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
}
keyboardView.popupManager.hide()
}
popupManager.hide()
}
shouldBlockNextKeyCode = false
hasTriggeredGestureMove = false
isKeyPressed = false
}
MotionEvent.ACTION_CANCEL -> {
longKeyPressHandler.cancelAll()
if (data.code != KeyCode.SHIFT) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.cancel(data))
}
popupManager.hide()
shouldBlockNextKeyCode = false
hasTriggeredGestureMove = false
isKeyPressed = false
}
@@ -354,7 +390,7 @@ class KeyView(
SwipeAction.DELETE_CHARACTERS_PRECISELY -> {
florisboard.activeEditorInstance.apply {
selection.updateAndNotify(
(selection.end + event.absUnitCountX).coerceIn(0, selection.end),
(selection.end + event.absUnitCountX + 1).coerceIn(0, selection.end),
selection.end
)
}
@@ -398,12 +434,10 @@ class KeyView(
}
SwipeGesture.Direction.LEFT -> {
if (prefs.gestures.spaceBarSwipeLeft == SwipeAction.MOVE_CURSOR_LEFT) {
if (!florisboard.activeEditorInstance.isRawInputEditor) {
val s = (initSelectionEnd + event.absUnitCountX).coerceIn(0, florisboard.activeEditorInstance.cachedInput.expectedMaxLength)
florisboard.activeEditorInstance.selection.updateAndNotify(s, s)
} else {
for (n in 0 until abs(event.relUnitCountX)) {
florisboard.executeSwipeAction(prefs.gestures.spaceBarSwipeLeft)
abs(event.relUnitCountX).let {
val count = if (!hasTriggeredGestureMove) { it - 1 } else { it }
if (count > 0) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(KeyData.ARROW_LEFT, count))
}
}
} else {
@@ -415,12 +449,10 @@ class KeyView(
}
SwipeGesture.Direction.RIGHT -> {
if (prefs.gestures.spaceBarSwipeRight == SwipeAction.MOVE_CURSOR_RIGHT) {
if (!florisboard.activeEditorInstance.isRawInputEditor) {
val s = (initSelectionEnd + event.absUnitCountX).coerceIn(0, florisboard.activeEditorInstance.cachedInput.expectedMaxLength)
florisboard.activeEditorInstance.selection.updateAndNotify(s, s)
} else {
for (n in 0 until abs(event.relUnitCountX)) {
florisboard.executeSwipeAction(prefs.gestures.spaceBarSwipeRight)
abs(event.relUnitCountX).let {
val count = if (!hasTriggeredGestureMove) { it - 1 } else { it }
if (count > 0) {
florisboard.textInputManager.inputEventDispatcher.send(InputKeyEvent.downUp(KeyData.ARROW_RIGHT, count))
}
}
} else {
@@ -445,14 +477,13 @@ class KeyView(
* by Devunwired
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val keyMarginH: Int
val keyMarginV: Int
if (keyboardView.isSmartbarKeyboardView){
if (keyboardView.isSmartbarKeyboardView) {
keyMarginH = resources.getDimension(R.dimen.key_marginH).toInt()
keyMarginV = resources.getDimension(R.dimen.key_marginV).toInt()
}else {
} else {
keyMarginV = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingVertical, context).toInt()
keyMarginH = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingHorizontal, context).toInt()
}
@@ -476,11 +507,11 @@ class KeyView(
else -> when (data.code) {
KeyCode.SHIFT,
KeyCode.DELETE ->
if ((keyboardView.computedLayout?.arrangement?.get(2)?.size ?: 0) > 10) {
1.12f
} else {
1.56f
}
if ((keyboardView.computedLayout?.arrangement?.get(2)?.size ?: 0) > 10) {
1.12f
} else {
1.56f
}
KeyCode.VIEW_CHARACTERS,
KeyCode.VIEW_SYMBOLS,
KeyCode.VIEW_SYMBOLS2,
@@ -528,7 +559,7 @@ class KeyView(
}
drawablePaddingH = (0.2f * width).toInt()
drawablePaddingV = (0.2f * height).toInt()
drawablePaddingV = (0.2f * height * (1.0f / (florisboard?.inputView?.heightFactor ?: 1.0f)).coerceAtMost(1.0f)).toInt()
// MUST CALL THIS
setMeasuredDimension(width, height)
@@ -553,12 +584,25 @@ class KeyView(
KeyCode.CLIPBOARD_CUT -> (florisboard != null
&& florisboard.activeEditorInstance.selection.isSelectionMode
&& !florisboard.activeEditorInstance.isRawInputEditor)
KeyCode.CLIPBOARD_PASTE -> florisboard?.clipboardManager?.hasPrimaryClip() == true
KeyCode.CLIPBOARD_PASTE -> (
// such gore. checks
// 1. has a clipboard item
// 2. the clipboard item has any of the supported mime types of the editor OR is plain text.
florisboard?.florisClipboardManager?.canBePasted(florisboard.florisClipboardManager?.primaryClip)
) == true
KeyCode.CLIPBOARD_SELECT_ALL -> {
florisboard?.activeEditorInstance?.isRawInputEditor == false
}
KeyCode.SWITCH_TO_CLIPBOARD_CONTEXT -> {
visibility = when (prefs.clipboard.enableHistory ) {
true -> VISIBLE
false -> GONE
}
prefs.clipboard.enableHistory
}
else -> true
}
if (data.code == KeyCode.CLIPBOARD_PASTE)
if (!isEnabled) {
isKeyPressed = false
}
@@ -642,10 +686,10 @@ class KeyView(
val keyMarginH: Int
val keyMarginV: Int
if (keyboardView.isSmartbarKeyboardView){
if (keyboardView.isSmartbarKeyboardView) {
keyMarginH = resources.getDimension(R.dimen.key_marginH).toInt()
keyMarginV = resources.getDimension(R.dimen.key_marginV).toInt()
}else {
} else {
keyMarginV = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingVertical, context).toInt()
keyMarginH = ViewLayoutUtils.convertDpToPixel(prefs.keyboard.keySpacingHorizontal, context).toInt()
}
@@ -691,6 +735,12 @@ class KeyView(
}
}
}
KeyCode.SWITCH_TO_CLIPBOARD_CONTEXT -> {
when (prefs.clipboard.enableHistory) {
true -> VISIBLE
false -> GONE
}
}
KeyCode.LANGUAGE_SWITCH -> {
val tempUtilityKeyAction = when {
prefs.keyboard.utilityKeyEnabled -> prefs.keyboard.utilityKeyAction
@@ -711,7 +761,11 @@ class KeyView(
}
else -> if (data.variation != KeyVariation.ALL) {
val keyVariation = florisboard?.textInputManager?.keyVariation ?: KeyVariation.NORMAL
visibility = if (data.variation == keyVariation) { VISIBLE } else { GONE }
visibility = if (data.variation == keyVariation) {
VISIBLE
} else {
GONE
}
updateTouchHitBox()
}
}
@@ -827,12 +881,16 @@ class KeyView(
KeyboardMode.CHARACTERS -> {
label = florisboard?.activeSubtype?.locale?.displayName
}
else -> {}
else -> {
}
}
}
KeyCode.SWITCH_TO_MEDIA_CONTEXT -> {
drawable = getDrawable(context, R.drawable.ic_sentiment_satisfied)
}
KeyCode.SWITCH_TO_CLIPBOARD_CONTEXT -> {
drawable = getDrawable(context, R.drawable.ic_assignment)
}
KeyCode.SWITCH_TO_TEXT_CONTEXT,
KeyCode.VIEW_CHARACTERS -> {
label = resources.getString(R.string.key__view_characters)
@@ -907,7 +965,7 @@ class KeyView(
}
else -> when {
(data.type == KeyType.CHARACTER || data.type == KeyType.NUMERIC) &&
data.code != KeyCode.SPACE -> {
data.code != KeyCode.SPACE -> {
val cachedTextSize = setTextSizeFor(
labelPaint,
measuredWidth - (2.6f * drawablePaddingH),
@@ -926,7 +984,7 @@ class KeyView(
else -> 1.0f
}
)
keyboardView.popupManager.keyPopupTextSize = cachedTextSize
popupManager.keyPopupTextSize = cachedTextSize
}
else -> {
setTextSizeFor(
@@ -949,7 +1007,11 @@ class KeyView(
themeValueCache.keyForeground.toSolidColor().color
}
labelPaint.alpha = if (keyboardView.computedLayout?.mode == KeyboardMode.CHARACTERS &&
data.code == KeyCode.SPACE) { 120 } else { 255 }
data.code == KeyCode.SPACE) {
120
} else {
255
}
val centerX = measuredWidth / 2.0f
val centerY = measuredHeight / 2.0f + (labelPaint.textSize - labelPaint.descent()) / 2
if (label.contains("\n")) {

View File

@@ -22,16 +22,18 @@ import android.util.AttributeSet
import android.view.MotionEvent
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.core.view.children
import com.google.android.flexbox.FlexDirection
import com.google.android.flexbox.FlexboxLayout
import com.google.android.flexbox.JustifyContent
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.InputKeyEvent
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.popup.PopupManager
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.gestures.SwipeGesture
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyData
import dev.patrickgold.florisboard.ime.text.key.KeyView
import dev.patrickgold.florisboard.ime.text.layout.ComputedLayoutData
import dev.patrickgold.florisboard.ime.theme.Theme
@@ -47,12 +49,10 @@ import kotlin.math.roundToInt
*
* @property florisboard Reference to instance of core class [FlorisBoard].
*/
class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Listener,
class KeyboardView : FlexboxLayout, FlorisBoard.EventListener, SwipeGesture.Listener,
ThemeManager.OnThemeUpdatedListener {
private var activeKeyView: KeyView? = null
private var activePointerId: Int? = null
private var activeX: Float = 0.0f
private var activeY: Float = 0.0f
private var activeKeyViews: MutableMap<Int, KeyView> = mutableMapOf()
private var initialKeyCodes: MutableMap<Int, Int> = mutableMapOf()
var computedLayout: ComputedLayoutData? = null
set(v) {
@@ -62,11 +62,9 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
var desiredKeyWidth: Int = resources.getDimension(R.dimen.key_width).toInt()
var desiredKeyHeight: Int = resources.getDimension(R.dimen.key_height).toInt()
var florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
private var initialKeyCode: Int = 0
private val isPreviewMode: Boolean
val isSmartbarKeyboardView: Boolean
val isLoadingPlaceholderKeyboard: Boolean
var popupManager = PopupManager<KeyboardView, KeyView>(this, florisboard?.popupLayerView)
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
private val themeManager: ThemeManager = ThemeManager.default()
private val swipeGestureDetector = SwipeGesture.Detector(context, this)
@@ -80,7 +78,8 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
isLoadingPlaceholderKeyboard = getBoolean(R.styleable.KeyboardView_isLoadingPlaceholderKeyboard, false)
recycle()
}
orientation = VERTICAL
flexDirection = FlexDirection.COLUMN
justifyContent = JustifyContent.SPACE_BETWEEN
layoutParams = layoutParams ?: FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT
@@ -132,7 +131,6 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
* Dismisses all shown key popups when keyboard is detached from window.
*/
override fun onDetachedFromWindow() {
popupManager.dismissAllPopups()
if (!isPreviewMode) {
themeManager.unregisterOnThemeUpdatedListener(this)
}
@@ -171,92 +169,93 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
*/
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean {
event ?: return false
if (isPreviewMode || isLoadingPlaceholderKeyboard) {
return false
}
val eventFloris = MotionEvent.obtainNoHistory(event)
if (event == null || isPreviewMode || isLoadingPlaceholderKeyboard) return false
if (!isSmartbarKeyboardView && swipeGestureDetector.onTouchEvent(event)) {
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_CANCEL)
activeKeyView = null
activePointerId = null
for (pointerIndex in 0 until event.pointerCount) {
val pointerId = event.getPointerId(pointerIndex)
sendFlorisTouchEvent(event, pointerIndex, pointerId, MotionEvent.ACTION_CANCEL)
activeKeyViews.remove(pointerId)
}
if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) {
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.up(KeyData.INTERNAL_BATCH_EDIT))
}
return true
}
val pointerIndex = event.actionIndex
var pointerId = event.getPointerId(pointerIndex)
when (event.actionMasked) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_DOWN -> {
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.down(KeyData.INTERNAL_BATCH_EDIT))
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
searchForActiveKeyView(event, pointerIndex, pointerId)
initialKeyCodes[pointerId] = activeKeyViews[pointerId]?.data?.code ?: 0
sendFlorisTouchEvent(event, pointerIndex, pointerId, MotionEvent.ACTION_DOWN)
}
MotionEvent.ACTION_POINTER_DOWN -> {
if (activePointerId == null) {
activePointerId = pointerId
activeX = event.getX(pointerIndex)
activeY = event.getY(pointerIndex)
searchForActiveKeyView()
initialKeyCode = activeKeyView?.data?.code ?: 0
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_DOWN)
} else if (activePointerId != pointerId) {
// New pointer arrived. Send ACTION_UP to current active view and move on
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_UP)
activePointerId = pointerId
activeX = event.getX(pointerIndex)
activeY = event.getY(pointerIndex)
searchForActiveKeyView()
initialKeyCode = activeKeyView?.data?.code ?: 0
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_DOWN)
}
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
searchForActiveKeyView(event, pointerIndex, pointerId)
initialKeyCodes[pointerId] = activeKeyViews[pointerId]?.data?.code ?: 0
sendFlorisTouchEvent(event, pointerIndex, pointerId, MotionEvent.ACTION_DOWN)
}
MotionEvent.ACTION_MOVE -> {
for (index in 0 until event.pointerCount) {
pointerId = event.getPointerId(index)
if (activePointerId == pointerId) {
activeX = event.getX(index)
activeY = event.getY(index)
if (activeKeyView == null) {
searchForActiveKeyView()
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_DOWN)
} else {
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_MOVE)
}
for (pointerIndex in 0 until event.pointerCount) {
val pointerId = event.getPointerId(pointerIndex)
if (!activeKeyViews.containsKey(pointerId)) {
searchForActiveKeyView(event, pointerIndex, pointerId)
sendFlorisTouchEvent(event, pointerIndex, pointerId, MotionEvent.ACTION_DOWN)
} else {
sendFlorisTouchEvent(event, pointerIndex, pointerId, MotionEvent.ACTION_MOVE)
}
}
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL,
MotionEvent.ACTION_POINTER_UP -> {
if (activePointerId == pointerId) {
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_UP)
activeKeyView = null
activePointerId = null
}
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
sendFlorisTouchEvent(event, pointerIndex, pointerId, event.actionMasked)
activeKeyViews.remove(pointerId)
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL -> {
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
sendFlorisTouchEvent(event, pointerIndex, pointerId, event.actionMasked)
activeKeyViews.remove(pointerId)
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.up(KeyData.INTERNAL_BATCH_EDIT))
}
else -> return false
}
eventFloris.recycle()
return true
}
/**
* Sends a touch [event] to [activeKeyView] with action set to [actionParam]. Normalizes passed
* actions (ACTION_POINTER_* will be converted to ACTION_*). Translates the absolute coords of
* a passed [event] to relative ones so the [activeKeyView] can work with it.
* Sends a touch [event] to the active key view which is associated with given [pointerId]. The action of the
* event is set to [actionParam]. Normalizes passed actions (ACTION_POINTER_* will be converted to ACTION_*).
* Translates the absolute coords of a passed [event] to relative ones so the active key view can work with it.
*
* @param event The event to pass to [activeKeyView].
* @param event The event to pass to the active key view.
* @param pointerIndex The index of the pointer, used for getting coordinates.
* @param pointerId The unique ID of the pointer, used to reference the active key view.
* @param actionParam The action to set the [event] to.
*/
private fun sendFlorisTouchEvent(event: MotionEvent, actionParam: Int) {
val keyView = activeKeyView ?: return
val keyViewParent = keyView.parent as ViewGroup
keyView.onFlorisTouchEvent(event.apply {
action = when (actionParam) {
private fun sendFlorisTouchEvent(event: MotionEvent, pointerIndex: Int, pointerId: Int, actionParam: Int) {
val keyView = activeKeyViews[pointerId] ?: return
val keyViewParent = keyView.parent as? ViewGroup ?: return
val eventToSend = MotionEvent.obtain(
event.downTime,
event.eventTime,
when (actionParam) {
MotionEvent.ACTION_POINTER_DOWN -> MotionEvent.ACTION_DOWN
MotionEvent.ACTION_POINTER_UP -> MotionEvent.ACTION_UP
else -> actionParam
}
setLocation(
activeX - keyViewParent.x - keyView.x,
activeY - keyViewParent.y - keyView.y
)
})
},
event.getX(pointerIndex) - keyViewParent.x - keyView.x,
event.getY(pointerIndex) - keyViewParent.y - keyView.y,
0
)
keyView.onFlorisTouchEvent(eventToSend)
eventToSend.recycle()
}
/**
@@ -265,7 +264,7 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
*/
override fun onSwipe(event: SwipeGesture.Event): Boolean {
return when {
initialKeyCode == KeyCode.DELETE -> {
initialKeyCodes[event.pointerId] == KeyCode.DELETE -> {
if (event.type == SwipeGesture.Type.TOUCH_UP && event.direction == SwipeGesture.Direction.LEFT &&
prefs.gestures.deleteKeySwipeLeft == SwipeAction.DELETE_WORD) {
florisboard?.executeSwipeAction(prefs.gestures.deleteKeySwipeLeft)
@@ -274,7 +273,16 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
false
}
}
initialKeyCode > KeyCode.SPACE && !popupManager.isShowingExtendedPopup -> when {
initialKeyCodes[event.pointerId] == KeyCode.SHIFT && activeKeyViews[event.pointerId]?.data?.code != KeyCode.SHIFT &&
event.type == SwipeGesture.Type.TOUCH_UP -> {
activeKeyViews[event.pointerId]?.let {
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.up(it.popupManager.getActiveKeyData(it) ?: it.data))
florisboard?.textInputManager?.inputEventDispatcher?.send(InputKeyEvent.cancel(KeyData.SHIFT))
}
true
}
initialKeyCodes[event.pointerId] ?: 0 > KeyCode.SPACE &&
activeKeyViews[event.pointerId]?.popupManager?.isShowingExtendedPopup == false -> when {
!prefs.glide.enabled -> when (event.type) {
SwipeGesture.Type.TOUCH_UP -> {
val swipeAction = when (event.direction) {
@@ -300,15 +308,17 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
}
/**
* Searches for an active key view at [activeX]/[activeY].
* Searches for an active key view at the passed pointer location.
*/
private fun searchForActiveKeyView() {
private fun searchForActiveKeyView(event: MotionEvent, pointerIndex: Int, pointerId: Int) {
val activeX = event.getX(pointerIndex)
val activeY = event.getY(pointerIndex)
loop@ for (row in children) {
if (row is FlexboxLayout) {
for (keyView in row.children) {
if (keyView is KeyView) {
if (keyView.touchHitBox.contains(activeX.toInt(), activeY.toInt())) {
activeKeyView = keyView
activeKeyViews[pointerId] = keyView
break@loop
}
}
@@ -318,14 +328,12 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
}
/**
* Invalidates the current [activeKeyView] and sends a [MotionEvent.ACTION_CANCEL] to indicate
* the loss of focus.
* Invalidates the current active key view and sends a [MotionEvent.ACTION_CANCEL] to indicate the loss of focus.
*/
fun dismissActiveKeyViewReference() {
activeKeyView?.onFlorisTouchEvent(MotionEvent.obtain(
fun dismissActiveKeyViewReference(pointerId: Int) {
activeKeyViews.remove(pointerId)?.onFlorisTouchEvent(MotionEvent.obtain(
0, 0, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0
))
activeKeyView = null
}
/**
@@ -354,9 +362,17 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
} else {
(florisboard?.inputView?.desiredTextKeyboardViewHeight ?: MeasureSpec.getSize(heightMeasureSpec).toFloat())
} * if (isPreviewMode) { 0.90f } else { 1.00f }
val layoutSize = computedLayout?.arrangement?.size?.toFloat() ?: 4.0f
desiredKeyHeight = when {
isSmartbarKeyboardView -> desiredHeight - 1.5f * keyMarginV
else -> desiredHeight / (computedLayout?.arrangement?.size?.toFloat() ?: 4.0f) - 2.0f * keyMarginV
isSmartbarKeyboardView -> {
desiredHeight - 1.5f * keyMarginV
}
florisboard?.inputView?.shouldGiveAdditionalSpace == true -> {
desiredHeight / (layoutSize + 0.5f).coerceAtMost(5.0f) - 2.0f * keyMarginV
}
else -> {
desiredHeight / layoutSize - 2.0f * keyMarginV
}
}.roundToInt()
super.onMeasure(

View File

@@ -17,7 +17,6 @@
package dev.patrickgold.florisboard.ime.text.layout
import android.content.Context
import com.github.michaelbull.result.onSuccess
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dev.patrickgold.florisboard.ime.core.PrefHelper
@@ -78,10 +77,10 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
source = AssetSource.Assets,
path = PopupManager.POPUP_EXTENSION_PATH_REL + "/" + (subtype?.locale?.language ?: "\$default") + ".json"
)
assetManager.loadAsset(langTagRef, PopupExtension::class.java).onSuccess {
assetManager.loadAsset(langTagRef, PopupExtension::class).onSuccess {
return it
}
assetManager.loadAsset(langRef, PopupExtension::class.java).onSuccess {
assetManager.loadAsset(langRef, PopupExtension::class).onSuccess {
return it
}
return PopupExtension.empty()
@@ -210,7 +209,15 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
popupSet = extendedPopups.mapping[KeyVariation.ALL]?.get(label) ?:
extendedPopupsDefault.mapping[KeyVariation.ALL]?.get(label)
}
popupSet?.let { key.popup.merge(it) }
var keySpecificPopupSet: PopupSet<KeyData>? = null
if (label != key.label) {
keySpecificPopupSet = extendedPopups.mapping[KeyVariation.ALL]?.get(key.label) ?:
extendedPopupsDefault.mapping[KeyVariation.ALL]?.get(key.label)
}
key.popup.apply {
keySpecificPopupSet?.let { merge(it) }
popupSet?.let { merge(it) }
}
}
}
}

View File

@@ -0,0 +1,533 @@
/*
* 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.text.smartbar
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.Typeface
import android.text.TextPaint
import android.text.TextUtils
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.VelocityTracker
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.OverScroller
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.clip.FlorisClipboardManager
import dev.patrickgold.florisboard.ime.clip.provider.ClipboardItem
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import dev.patrickgold.florisboard.ime.theme.ThemeValue
import java.lang.ref.WeakReference
import java.util.*
import kotlin.collections.ArrayList
/**
* A candidate view allowing for easy of suggestions. Additionally it also features an integrated clipboard suggestion
* support, which works together with the normal suggestions provided by the NLP algorithm.
*/
class CandidateView : View, ThemeManager.OnThemeUpdatedListener {
private var themeManager: ThemeManager? = null
private var florisClipboardManager: FlorisClipboardManager? = null
private var eventListener: WeakReference<SmartbarView.EventListener?> = WeakReference(null)
private var displayMode: DisplayMode = DisplayMode.DYNAMIC_SCROLLABLE
private val candidates: ArrayList<String> = ArrayList()
private var clipboardItem: ClipboardItem? = null
private var clipboardItemTime: Long = 0
private var clipboardItemTimeout: Int = 60_000
private var computedCandidates: ArrayList<ComputedCandidate> = ArrayList()
private var computedCandidatesWidthPx: Int = 0
private var selectedIndex: Int = -1
private var backgroundPaint: Paint = Paint().apply { color = Color.BLACK }
private var candidateBackground: ThemeValue = ThemeValue.SolidColor.TRANSPARENT
private var candidateForeground: ThemeValue = ThemeValue.SolidColor.TRANSPARENT
private val candidateMarginH: Int = resources.getDimensionPixelOffset(R.dimen.smartbar_candidate_marginH)
private var dividerBackground: ThemeValue = ThemeValue.SolidColor.TRANSPARENT
private var dividerPaint: Paint = Paint().apply { color = Color.BLACK }
private var dividerWidth: Int = resources.getDimensionPixelSize(R.dimen.smartbar_divider_width)
private val pasteDrawable = ContextCompat.getDrawable(context, R.drawable.ic_content_paste)
private var lastX: Float = 0.0f
private val scroller: OverScroller = OverScroller(context, AccelerateDecelerateInterpolator())
private val textPaint: TextPaint = TextPaint().apply {
alpha = 255
color = Color.BLACK
isAntiAlias = true
isFakeBoldText = false
textAlign = Paint.Align.CENTER
textSize = resources.getDimension(R.dimen.smartbar_candidate_textSize)
typeface = Typeface.DEFAULT
}
private var velocityTracker: VelocityTracker? = null
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
init {
isHorizontalScrollBarEnabled = false
isVerticalScrollBarEnabled = false
scrollTo(0, 0)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
themeManager = ThemeManager.defaultOrNull()
themeManager?.registerOnThemeUpdatedListener(this)
florisClipboardManager = FlorisClipboardManager.getInstanceOrNull()
updateCandidates(candidates)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
themeManager?.unregisterOnThemeUpdatedListener(this)
themeManager = null
florisClipboardManager = null
candidates.clear()
velocityTracker?.recycle()
velocityTracker = null
}
fun updateCandidates(newCandidates: List<String>?) {
candidates.clear()
if (newCandidates != null) {
candidates.addAll(newCandidates)
}
recomputeCandidates()
}
fun updateClipboardItem(newClipboardCandidate: ClipboardItem) {
clipboardItem = newClipboardCandidate
clipboardItemTime = System.currentTimeMillis()
recomputeCandidates()
}
fun setEventListener(listener: SmartbarView.EventListener) {
eventListener = WeakReference(listener)
}
fun updateDisplaySettings(newDisplayMode: DisplayMode, newClipboardContentTimeout: Int) {
if (newClipboardContentTimeout != clipboardItemTimeout) {
clipboardItemTimeout = newClipboardContentTimeout
}
if (newDisplayMode != displayMode) {
displayMode = newDisplayMode
scroller.abortAnimation()
scrollTo(0, 0)
}
}
private fun recomputeCandidates() {
computedCandidates.clear()
if (clipboardItem != null && System.currentTimeMillis() - clipboardItemTime > clipboardItemTimeout) {
clipboardItem = null
}
val classicCandidateWidth = (measuredWidth - 2 * dividerWidth) / 3
val maxDynamicCandidateWidth = (measuredWidth * 0.7).toInt()
val clipItem = clipboardItem
val clipItemAvailable = clipItem != null && florisClipboardManager?.canBePasted(clipItem) == true
computedCandidatesWidthPx = 0
if (candidates.isEmpty()) {
if (clipItemAvailable) {
computedCandidates.add(ComputedCandidate.Clip(clipItem!!, Rect(
0,
0,
measuredWidth,
measuredHeight
)))
} else if (displayMode == DisplayMode.CLASSIC) {
for (n in 0 until 3) {
val left = (classicCandidateWidth + dividerWidth) * n
computedCandidates.add(ComputedCandidate.Empty(Rect(
left,
0,
left + classicCandidateWidth,
measuredHeight
)))
}
}
} else if (candidates.size == 1 && !clipItemAvailable) {
computedCandidates.add(ComputedCandidate.Word(candidates[0], Rect(
0,
0,
measuredWidth,
measuredHeight
)))
} else {
when (displayMode) {
DisplayMode.CLASSIC -> {
if (!clipItemAvailable) {
for (n in 0 until candidates.size.coerceAtMost(3)) {
val left = (classicCandidateWidth + dividerWidth) * n
computedCandidates.add(ComputedCandidate.Word(candidates[n], Rect(
left,
0,
left + classicCandidateWidth,
measuredHeight
)))
}
} else {
computedCandidates.add(ComputedCandidate.Clip(clipItem!!, Rect(
0,
0,
classicCandidateWidth,
measuredHeight
)))
for (n in 0 until candidates.size.coerceAtMost(2)) {
val left = (classicCandidateWidth + dividerWidth) * (n + 1)
computedCandidates.add(ComputedCandidate.Word(candidates[n], Rect(
left,
0,
left + classicCandidateWidth,
measuredHeight
)))
}
}
if (computedCandidates.size < 3) {
for (n in computedCandidates.size until 3) {
val left = (classicCandidateWidth + dividerWidth) * n
computedCandidates.add(ComputedCandidate.Empty(Rect(
left,
0,
left + classicCandidateWidth,
measuredHeight
)))
}
}
}
DisplayMode.DYNAMIC -> {
if (clipItemAvailable) {
val candidateWidth = (textPaint.measureText(clipItem!!.stringRepresentation()).toInt() + candidateMarginH + measuredHeight * 4 / 6).coerceAtMost(maxDynamicCandidateWidth)
computedCandidates.add(ComputedCandidate.Clip(clipItem, Rect(
0,
0,
candidateWidth,
measuredHeight
)))
computedCandidatesWidthPx += candidateWidth
}
for (n in candidates.indices) {
var tmpWidthPx = computedCandidatesWidthPx
if (tmpWidthPx > 0) {
tmpWidthPx += dividerWidth
}
val candidateWidth = (textPaint.measureText(candidates[n]).toInt() + 2 * candidateMarginH).coerceAtMost(maxDynamicCandidateWidth)
tmpWidthPx += candidateWidth
if (tmpWidthPx > measuredWidth) {
break
} else {
computedCandidates.add(ComputedCandidate.Word(candidates[n], Rect(
computedCandidatesWidthPx,
0,
computedCandidatesWidthPx + candidateWidth,
measuredHeight
)))
computedCandidatesWidthPx = tmpWidthPx
}
}
val widthToIncreasePerItem = (measuredWidth - computedCandidatesWidthPx) / computedCandidates.size
for (n in computedCandidates.indices) {
computedCandidates[n].geometry.let {
it.left += n * widthToIncreasePerItem
it.right += (n + 1) * widthToIncreasePerItem
}
}
}
DisplayMode.DYNAMIC_SCROLLABLE -> {
if (clipItemAvailable) {
val candidateWidth = (textPaint.measureText(clipItem!!.stringRepresentation()).toInt() + candidateMarginH + measuredHeight * 4 / 6).coerceAtMost(maxDynamicCandidateWidth)
computedCandidates.add(ComputedCandidate.Clip(clipItem, Rect(
0,
0,
candidateWidth,
measuredHeight
)))
computedCandidatesWidthPx += candidateWidth
}
for (n in candidates.indices) {
if (computedCandidatesWidthPx > 0) {
computedCandidatesWidthPx += dividerWidth
}
val candidateWidth = (textPaint.measureText(candidates[n]).toInt() + 2 * candidateMarginH).coerceAtMost(maxDynamicCandidateWidth)
computedCandidates.add(ComputedCandidate.Word(candidates[n], Rect(
computedCandidatesWidthPx,
0,
computedCandidatesWidthPx + candidateWidth,
measuredHeight
)))
computedCandidatesWidthPx += candidateWidth
}
}
}
}
selectedIndex = -1
scroller.abortAnimation()
scrollTo(0, 0)
invalidate()
}
override fun computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.currX, scroller.currY)
invalidate()
}
}
private fun determineSelectedIndex(relX: Int, relY: Int): Int {
val absX = relX + scrollX
val absY = relY + scrollY
var retIndex = -1
for ((n, computedCandidate) in computedCandidates.withIndex()) {
if (computedCandidate.geometry.contains(absX, absY)) {
retIndex = n
break
}
}
return retIndex
}
private fun onCandidateClick(index: Int) {
computedCandidates.getOrNull(index)?.let { candidate ->
when (candidate) {
is ComputedCandidate.Word -> {
eventListener.get()?.onSmartbarCandidatePressed(candidate.word)
}
is ComputedCandidate.Clip -> {
eventListener.get()?.onSmartbarClipboardCandidatePressed(candidate.clipboardItem)
}
is ComputedCandidate.Empty -> {
}
}
}
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean {
super.onTouchEvent(event)
event ?: return false
return when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
isPressed = true
selectedIndex = determineSelectedIndex(event.x.toInt(), event.y.toInt())
if (displayMode == DisplayMode.DYNAMIC_SCROLLABLE) {
velocityTracker?.recycle()
velocityTracker = VelocityTracker.obtain()
velocityTracker?.addMovement(event)
lastX = event.x
}
invalidate()
true
}
MotionEvent.ACTION_MOVE -> when (displayMode) {
DisplayMode.CLASSIC,
DisplayMode.DYNAMIC -> {
computedCandidates.getOrNull(selectedIndex)?.let { candidate ->
if (!candidate.geometry.contains(scrollX + event.x.toInt(), scrollY + event.y.toInt())) {
selectedIndex = -1
invalidate()
}
}
true
}
DisplayMode.DYNAMIC_SCROLLABLE -> {
velocityTracker?.addMovement(event)
selectedIndex = -1
if (computedCandidatesWidthPx > measuredWidth) {
scrollTo((scrollX + lastX - event.x).toInt().coerceIn(0, computedCandidatesWidthPx - measuredWidth), 0)
lastX = event.x
}
invalidate()
true
}
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL -> {
isPressed = false
if (event.actionMasked == MotionEvent.ACTION_UP) {
onCandidateClick(selectedIndex)
}
selectedIndex = -1
if (displayMode == DisplayMode.DYNAMIC_SCROLLABLE) {
velocityTracker?.let {
it.addMovement(event)
it.computeCurrentVelocity(1000)
if (computedCandidatesWidthPx > measuredWidth) {
scroller.fling(
scrollX, 0,
-it.xVelocity.toInt(), 0,
0, computedCandidatesWidthPx - measuredWidth,
0, 0,
0, 0
)
}
it.recycle()
velocityTracker = null
}
}
invalidate()
true
}
else -> false
}
}
override fun onThemeUpdated(theme: Theme) {
candidateBackground = theme.getAttr(Theme.Attr.WINDOW_SEMI_TRANSPARENT_COLOR)
candidateForeground = theme.getAttr(Theme.Attr.SMARTBAR_FOREGROUND)
dividerBackground = theme.getAttr(Theme.Attr.SMARTBAR_FOREGROUND_ALT)
invalidate()
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas ?: return
backgroundPaint.apply { color = candidateBackground.toSolidColor().color }
dividerPaint.apply { color = ColorUtils.setAlphaComponent(dividerBackground.toSolidColor().color, 64) }
textPaint.apply { color = candidateForeground.toSolidColor().color }
for ((n, computedCandidate) in computedCandidates.withIndex()) {
with(computedCandidate) {
if (n == selectedIndex) {
canvas.drawRect(geometry, backgroundPaint)
}
when (this) {
is ComputedCandidate.Word -> {
val ellipsizedWord = TextUtils.ellipsize(
word, textPaint, geometry.width().toFloat() - candidateMarginH, TextUtils.TruncateAt.MIDDLE
).toString()
canvas.drawText(
ellipsizedWord,
geometry.left + geometry.width() / 2.0f,
geometry.top + geometry.height() / 2.0f - (textPaint.descent() + textPaint.ascent()) / 2.0f,
textPaint
)
}
is ComputedCandidate.Clip -> {
pasteDrawable?.setTint(candidateForeground.toSolidColor().color)
pasteDrawable?.setBounds(
geometry.left + geometry.height() / 3,
geometry.height() / 3,
geometry.left + geometry.height() * 2 / 3,
geometry.height() * 2 / 3
)
pasteDrawable?.draw(canvas)
val pdWidth = geometry.height().toFloat()
val ellipsizedWord = TextUtils.ellipsize(
clipboardItem.stringRepresentation(), textPaint, geometry.width().toFloat() - pdWidth, TextUtils.TruncateAt.MIDDLE
).toString()
canvas.drawText(
ellipsizedWord,
geometry.left + geometry.width() / 2.0f + pdWidth / 2.0f - candidateMarginH,
geometry.top + geometry.height() / 2.0f - (textPaint.descent() + textPaint.ascent()) / 2.0f,
textPaint
)
}
is ComputedCandidate.Empty -> {
}
}
if (n + 1 < computedCandidates.size) {
canvas.drawRect(
geometry.right.toFloat(),
(geometry.height() / 4).toFloat(),
(geometry.right + dividerWidth).toFloat(),
(geometry.height() * 3 / 4).toFloat(),
dividerPaint
)
}
}
}
}
/**
* Data class describing a computed candidate item.
*
* @property geometry The geometry of the computed candidate, used to position and size the item correctly when
* being drawn on a canvas.
*/
private sealed class ComputedCandidate(val geometry: Rect) {
/**
* Computed word candidate, used for suggestions provided by the NLP algorithm.
*
* @property word The word this computed candidate item represents. Used in the callback to provide which word
* should be filled out.
*/
class Word(
val word: String,
geometry: Rect
) : ComputedCandidate(geometry) {
override fun toString(): String {
return "Word { word=\"$word\", geometry=$geometry }"
}
}
/**
* Computed word candidate, used for filling space when [DisplayMode.CLASSIC] is active. Does not hold any data
* and also does nothing when clicked on.
*/
class Empty(
geometry: Rect
) : ComputedCandidate(geometry) {
override fun toString(): String {
return "Empty { geometry=$geometry }"
}
}
/**
* Computed word candidate, used for clipboard paste suggestions.
*
* @property clipboardItem The clipboard item this computed candidate item represents. Used in the callback to
* provide which item should be pasted.
*/
class Clip(
val clipboardItem: ClipboardItem,
geometry: Rect
) : ComputedCandidate(geometry) {
override fun toString(): String {
return "Clip { clipboardItem=$clipboardItem, geometry=$geometry }"
}
}
}
/**
* Enum class defining the display mode for the candidate view.
*/
enum class DisplayMode {
CLASSIC,
DYNAMIC,
DYNAMIC_SCROLLABLE;
companion object {
fun fromString(string: String): DisplayMode {
return valueOf(string.toUpperCase(Locale.ENGLISH))
}
}
override fun toString(): String {
return super.toString().toLowerCase(Locale.ENGLISH)
}
}
}

View File

@@ -19,12 +19,12 @@ package dev.patrickgold.florisboard.ime.text.smartbar
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.Button
import androidx.annotation.IdRes
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.children
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.databinding.SmartbarBinding
import dev.patrickgold.florisboard.ime.clip.provider.ClipboardItem
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.core.Subtype
@@ -32,8 +32,6 @@ import dev.patrickgold.florisboard.ime.text.key.KeyVariation
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
import dev.patrickgold.florisboard.ime.theme.Theme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import dev.patrickgold.florisboard.util.setBackgroundTintColor2
import dev.patrickgold.florisboard.util.setDrawableTintColor2
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
@@ -51,7 +49,7 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
private val themeManager = ThemeManager.default()
private var eventListener: WeakReference<EventListener?>? = null
private var eventListener: WeakReference<EventListener?> = WeakReference(null)
private val mainScope = MainScope()
private var lastSuggestionInitDate: Long = 0
@@ -66,15 +64,12 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
binding.quickActionToggle.rotation = if (v) 180.0f else 0.0f
field = v
}
private var shouldSuggestClipboardContents: Boolean = false
private lateinit var binding: SmartbarBinding
private var indexedActionStartArea: MutableList<Int> = mutableListOf()
private var indexedMainArea: MutableList<Int> = mutableListOf()
private var indexedActionEndArea: MutableList<Int> = mutableListOf()
private var candidateViewList: MutableList<Button> = mutableListOf()
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
@@ -102,11 +97,9 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
indexedActionEndArea.add(view.id)
}
candidateViewList.add(binding.candidate0)
candidateViewList.add(binding.candidate1)
candidateViewList.add(binding.candidate2)
binding.backButton.setOnClickListener { eventListener.get()?.onSmartbarBackButtonPressed() }
binding.backButton.setOnClickListener { eventListener?.get()?.onSmartbarBackButtonPressed() }
binding.candidates.updateDisplaySettings(prefs.suggestion.displayMode, prefs.suggestion.clipboardContentTimeout * 1_000)
mainScope.launch(Dispatchers.Default) {
florisboard?.let {
@@ -122,28 +115,6 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
}
}
binding.candidate0.setOnClickListener {
if (it is Button) {
eventListener?.get()?.onSmartbarCandidatePressed(it.text.toString())
}
}
binding.candidate1.setOnClickListener {
if (it is Button) {
eventListener?.get()?.onSmartbarCandidatePressed(it.text.toString())
}
}
binding.candidate2.setOnClickListener {
if (it is Button) {
eventListener?.get()?.onSmartbarCandidatePressed(it.text.toString())
}
}
binding.clipboardSuggestion.setOnClickListener {
florisboard?.activeEditorInstance?.performClipboardPaste()
shouldSuggestClipboardContents = false
updateSmartbarState()
}
mainScope.launch(Dispatchers.Default) {
florisboard?.let {
val layout = it.textInputManager.layoutManager.fetchComputedLayoutAsync(
@@ -159,13 +130,13 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
}
binding.privateModeButton.setOnClickListener {
eventListener?.get()?.onSmartbarPrivateModeButtonClicked()
eventListener.get()?.onSmartbarPrivateModeButtonClicked()
}
for (quickAction in binding.quickActions.children) {
if (quickAction is SmartbarQuickActionButton) {
quickAction.id.let { quickActionId ->
quickAction.setOnClickListener { eventListener?.get()?.onSmartbarQuickActionPressed(quickActionId) }
quickAction.setOnClickListener { eventListener.get()?.onSmartbarQuickActionPressed(quickActionId) }
}
}
}
@@ -189,7 +160,6 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
}
override fun onDetachedFromWindow() {
eventListener = null
florisboard?.textInputManager?.unregisterSmartbarView(this)
themeManager.unregisterOnThemeUpdatedListener(this)
super.onDetachedFromWindow()
@@ -248,6 +218,7 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
*/
fun updateSmartbarState() {
binding.clipboardCursorRow.updateVisibility()
binding.candidates.updateDisplaySettings(prefs.suggestion.displayMode, prefs.suggestion.clipboardContentTimeout * 1_000)
when (florisboard) {
null -> configureFeatureVisibility(
actionStartAreaVisible = false,
@@ -275,9 +246,6 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
KeyboardMode.PHONE,
KeyboardMode.PHONE2 -> null
else -> when {
florisboard.activeEditorInstance.isComposingEnabled &&
shouldSuggestClipboardContents
-> R.id.clipboard_suggestion_row
florisboard.activeEditorInstance.isComposingEnabled &&
florisboard.activeEditorInstance.selection.isCursorMode
-> R.id.candidates
@@ -299,50 +267,21 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
}
fun onPrimaryClipChanged() {
if (prefs.suggestion.enabled && prefs.suggestion.suggestClipboardContent &&
florisboard?.activeEditorInstance?.isPrivateMode == false) {
shouldSuggestClipboardContents = true
val item = florisboard.clipboardManager?.primaryClip?.getItemAt(0)
when {
item?.text != null -> {
binding.clipboardSuggestion.text = item.text
}
item?.uri != null -> {
binding.clipboardSuggestion.text = "(Image) " + item.uri.toString()
}
else -> {
binding.clipboardSuggestion.text = item?.text ?: "(Error while retrieving clipboard data)"
}
}
if (prefs.suggestion.enabled && prefs.suggestion.clipboardContentEnabled && florisboard?.activeEditorInstance?.isPrivateMode == false ) {
florisboard.florisClipboardManager?.primaryClip?.let { binding.candidates.updateClipboardItem(it) }
updateSmartbarState()
}
}
fun resetClipboardSuggestion() {
shouldSuggestClipboardContents = false
updateSmartbarState()
}
fun setCandidateSuggestionWords(suggestionInitDate: Long, suggestions: List<String>) {
fun setCandidateSuggestionWords(suggestionInitDate: Long, suggestions: List<String>?) {
if (suggestionInitDate > lastSuggestionInitDate) {
lastSuggestionInitDate = suggestionInitDate
binding.candidate1.text = suggestions.getOrNull(0) ?: ""
binding.candidate0.text = suggestions.getOrNull(1) ?: ""
binding.candidate2.text = suggestions.getOrNull(2) ?: ""
binding.candidates.updateCandidates(suggestions)
}
}
fun updateCandidateSuggestionCapsState() {
val tim = florisboard?.textInputManager ?: return
if (tim.capsLock) {
binding.candidate0.text = binding.candidate0.text.toString().toUpperCase(florisboard.activeSubtype.locale)
binding.candidate1.text = binding.candidate1.text.toString().toUpperCase(florisboard.activeSubtype.locale)
binding.candidate2.text = binding.candidate2.text.toString().toUpperCase(florisboard.activeSubtype.locale)
} else {
binding.candidate0.text = binding.candidate0.text.toString().toLowerCase(florisboard.activeSubtype.locale)
binding.candidate1.text = binding.candidate1.text.toString().toLowerCase(florisboard.activeSubtype.locale)
binding.candidate2.text = binding.candidate2.text.toString().toLowerCase(florisboard.activeSubtype.locale)
}
//
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
@@ -368,17 +307,12 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
override fun onThemeUpdated(theme: Theme) {
setBackgroundColor(theme.getAttr(Theme.Attr.SMARTBAR_BACKGROUND).toSolidColor().color)
setBackgroundTintColor2(binding.clipboardSuggestion, theme.getAttr(Theme.Attr.SMARTBAR_BUTTON_BACKGROUND).toSolidColor().color)
setDrawableTintColor2(binding.clipboardSuggestion, theme.getAttr(Theme.Attr.SMARTBAR_BUTTON_FOREGROUND).toSolidColor().color)
binding.clipboardSuggestion.setTextColor(theme.getAttr(Theme.Attr.SMARTBAR_BUTTON_FOREGROUND).toSolidColor().color)
for (view in candidateViewList) {
view.setTextColor(theme.getAttr(Theme.Attr.SMARTBAR_FOREGROUND).toSolidColor().color)
}
invalidate()
}
fun setEventListener(listener: EventListener) {
eventListener = WeakReference(listener)
binding.candidates.setEventListener(listener)
}
/**
@@ -388,6 +322,7 @@ class SmartbarView : ConstraintLayout, ThemeManager.OnThemeUpdatedListener {
interface EventListener {
fun onSmartbarBackButtonPressed() {}
fun onSmartbarCandidatePressed(word: String) {}
fun onSmartbarClipboardCandidatePressed(clipboardItem: ClipboardItem) {}
//fun onSmartbarCandidateLongPressed() {}
fun onSmartbarPrivateModeButtonClicked() {}
fun onSmartbarQuickActionPressed(@IdRes quickActionId: Int) {}

View File

@@ -22,7 +22,7 @@ import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Color
import com.github.michaelbull.result.*
import android.net.Uri
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.extension.AssetManager
import dev.patrickgold.florisboard.ime.extension.AssetRef
@@ -58,10 +58,10 @@ class ThemeManager private constructor(
private set
companion object {
/**
* The static relative path where a theme is located, regardless of the [AssetSource].
*/
/** The static relative path where a theme is located, regardless of the [AssetSource]. */
const val THEME_PATH_REL: String = "ime/theme"
/** Maximum size in bytes a theme file may have when loaded. */
const val THEME_MAX_SIZE: Int = 512_000
private var defaultInstance: ThemeManager? = null
@@ -85,6 +85,8 @@ class ThemeManager private constructor(
)
}
}
fun defaultOrNull(): ThemeManager? = defaultInstance
}
init {
@@ -92,7 +94,7 @@ class ThemeManager private constructor(
}
/**
* Updates the current theme ref and loads the corresponding theme, as well as notfies all
* Updates the current theme ref and loads the corresponding theme, as well as notifies all
* callback receivers about the new theme.
*/
fun update() {
@@ -102,7 +104,7 @@ class ThemeManager private constructor(
activeTheme = AdaptiveThemeOverlay(this, if (ref == null) {
Theme.BASE_THEME
} else {
loadTheme(ref).getOr(Theme.BASE_THEME)
loadTheme(ref).getOrDefault(Theme.BASE_THEME)
})
Timber.i(activeTheme.label)
notifyCallbackReceivers()
@@ -247,23 +249,30 @@ class ThemeManager private constructor(
}
}
fun deleteTheme(ref: AssetRef): Result<Nothing?, Throwable> {
fun deleteTheme(ref: AssetRef): Result<Unit> {
return assetManager.deleteAsset(ref)
}
fun loadTheme(ref: AssetRef): Result<Theme, Throwable> {
assetManager.loadAsset(ref, ThemeJson::class.java).onSuccess { themeJson ->
val theme = themeJson.toTheme()
return Ok(theme)
}.onFailure {
fun loadTheme(ref: AssetRef): Result<Theme> {
val themeJson = assetManager.loadAsset(ref, ThemeJson::class).getOrElse {
Timber.e(it.toString())
return Err(it)
return Result.failure(it)
}
return Err(Exception("Unreachable code"))
val theme = themeJson.toTheme()
return Result.success(theme)
}
fun writeTheme(ref: AssetRef, theme: Theme): Result<Boolean, Throwable> {
return assetManager.writeAsset(ref, ThemeJson::class.java, ThemeJson.fromTheme(theme))
fun loadTheme(uri: Uri): Result<Theme> {
val themeJson = assetManager.loadAsset(uri, ThemeJson::class, THEME_MAX_SIZE).getOrElse {
Timber.e(it.toString())
return Result.failure(it)
}
val theme = themeJson.toTheme()
return Result.success(theme)
}
fun writeTheme(ref: AssetRef, theme: Theme): Result<Unit> {
return assetManager.writeAsset(ref, ThemeJson::class, ThemeJson.fromTheme(theme))
}
private fun evaluateActiveThemeRef(): AssetRef? {
@@ -300,7 +309,7 @@ class ThemeManager private constructor(
prefs.theme.dayThemeRef
}
}
}).onFailure { Timber.e(it) }.getOr(null)
}).onFailure { Timber.e(it) }.getOrDefault(null)
}
private fun indexThemeRefs() {
@@ -308,7 +317,7 @@ class ThemeManager private constructor(
indexedNightThemeRefs.clear()
assetManager.listAssets(
AssetRef(AssetSource.Assets, THEME_PATH_REL),
ThemeMetaOnly::class.java
ThemeMetaOnly::class
).onSuccess {
for ((ref, themeMetaOnly) in it) {
if (themeMetaOnly.isNightTheme) {
@@ -322,7 +331,7 @@ class ThemeManager private constructor(
}
assetManager.listAssets(
AssetRef(AssetSource.Internal, THEME_PATH_REL),
ThemeMetaOnly::class.java
ThemeMetaOnly::class
).onSuccess {
for ((ref, themeMetaOnly) in it) {
if (themeMetaOnly.isNightTheme) {

View File

@@ -17,7 +17,9 @@
package dev.patrickgold.florisboard.ime.theme
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import androidx.annotation.ColorInt
import dev.patrickgold.florisboard.R
/**
@@ -32,16 +34,22 @@ sealed class ThemeValue {
*/
data class Reference(val group: String, val attr: String) : ThemeValue() {
override fun toString(): String {
return super.toString()
return "@$group/$attr"
}
}
/**
* This holds a solid color as a color int.
*/
data class SolidColor(val color: Int) : ThemeValue() {
data class SolidColor(@ColorInt val color: Int) : ThemeValue() {
companion object {
val TRANSPARENT = SolidColor(Color.TRANSPARENT)
val BLACK = SolidColor(Color.BLACK)
val WHITE = SolidColor(Color.WHITE)
}
override fun toString(): String {
return super.toString()
return "#" + String.format("%08X", color)
}
fun complimentaryTextColor(isAlt: Boolean = false): SolidColor {
@@ -71,7 +79,7 @@ sealed class ThemeValue {
*/
data class LinearGradient(val dummy: Int) : ThemeValue() {
override fun toString(): String {
return super.toString()
return "--undefined--"
}
}
@@ -80,7 +88,7 @@ sealed class ThemeValue {
*/
data class RadialGradient(val dummy: Int) : ThemeValue() {
override fun toString(): String {
return super.toString()
return "--undefined--"
}
}
@@ -89,7 +97,7 @@ sealed class ThemeValue {
*/
data class OnOff(val state: Boolean) : ThemeValue() {
override fun toString(): String {
return super.toString()
return state.toString()
}
}
@@ -99,7 +107,7 @@ sealed class ThemeValue {
*/
data class Other(val rawValue: String) : ThemeValue() {
override fun toString(): String {
return super.toString()
return rawValue
}
}
@@ -172,32 +180,6 @@ sealed class ThemeValue {
}
}
/**
* Converts this theme value to a string representation.
*/
override fun toString(): String {
return when (this) {
is Reference -> {
"@$group/$attr"
}
is SolidColor -> {
"#" + String.format("%08X", color)
}
is LinearGradient -> {
"--undefined--"
}
is RadialGradient -> {
"--undefined--"
}
is OnOff -> {
state.toString()
}
is Other -> {
rawValue
}
}
}
/**
* Converts this theme value to a string representation which can be shown to the user.
*/

View File

@@ -16,6 +16,7 @@
package dev.patrickgold.florisboard.settings
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Bundle
@@ -32,6 +33,7 @@ class AboutActivity : AppCompatActivity() {
private lateinit var binding: AboutActivityBinding
private var licensesAlertDialog: AlertDialog? = null
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = AboutActivityBinding.inflate(layoutInflater)
@@ -41,7 +43,7 @@ class AboutActivity : AppCompatActivity() {
setSupportActionBar(toolbar)
// Set app version string
binding.appVersion.text = AppVersionUtils.getRawVersionName(this)
binding.appVersion.text = "v" + AppVersionUtils.getRawVersionName(this)
// Set onClickListeners for buttons
binding.privacyPolicyButton.setOnClickListener {

View File

@@ -28,10 +28,10 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.view.children
import androidx.core.view.forEach
import com.github.michaelbull.result.onSuccess
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.databinding.ThemeEditorActivityBinding
import dev.patrickgold.florisboard.databinding.ThemeEditorGroupViewBinding
import dev.patrickgold.florisboard.databinding.ThemeEditorMetaDialogBinding
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.extension.AssetRef
@@ -60,6 +60,12 @@ class ThemeEditorActivity : AppCompatActivity() {
private var editedThemeRef: AssetRef? = null
private var isSaved: Boolean = false
private var themeLabel: String = ""
set(v) {
field = v
binding.themeNameLabel.text = v
}
companion object {
/** Constant code for a theme saved activity result. */
const val RESULT_CODE_THEME_EDIT_SAVED: Int = 0xFBADC1
@@ -85,6 +91,8 @@ class ThemeEditorActivity : AppCompatActivity() {
}
}
binding.themeNameEditBtn.setOnClickListener { showMetaEditDialog() }
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
@@ -129,22 +137,13 @@ class ThemeEditorActivity : AppCompatActivity() {
R.id.add_group_btn -> addGroup()
R.id.theme_cancel_btn -> onBackPressed()
R.id.theme_save_btn -> {
val themeName = binding.themeNameValue.text.toString().trim()
if (Theme.validateField(Theme.ValidationField.THEME_LABEL, themeName)) {
val ref = editedThemeRef
if (ref != null) {
themeManager.writeTheme(
ref, editedTheme.copy(
label = themeName
)
)
isSaved = true
finish()
}
} else {
binding.themeNameLabel.error =
resources.getString(R.string.settings__theme_editor__error_theme_label_empty)
binding.themeNameLabel.isErrorEnabled = true
val ref = editedThemeRef
if (ref != null) {
themeManager.writeTheme(
ref, editedTheme.copy(label = themeLabel)
)
isSaved = true
finish()
}
}
}
@@ -305,7 +304,7 @@ class ThemeEditorActivity : AppCompatActivity() {
}
}
editedTheme = editedTheme.copy(
label = binding.themeNameValue.text.toString(),
label = themeLabel,
attributes = tempMap.toMap()
)
binding.keyboardPreview.onThemeUpdated(editedTheme)
@@ -315,7 +314,7 @@ class ThemeEditorActivity : AppCompatActivity() {
* Builds the Ui for the current [editedTheme]. Also sorts the groups afterwards.
*/
private fun buildUi() {
binding.themeNameValue.setText(editedTheme.label)
themeLabel = editedTheme.label
for ((groupName, groupAttrs) in editedTheme.attributes) {
val groupView = addGroup(groupName).root
for ((attrName, attrValue) in groupAttrs) {
@@ -330,4 +329,29 @@ class ThemeEditorActivity : AppCompatActivity() {
}
sortGroups()
}
private fun showMetaEditDialog() {
val dialogView = ThemeEditorMetaDialogBinding.inflate(layoutInflater)
dialogView.metaName.setText(editedTheme.label)
val dialog: AlertDialog
AlertDialog.Builder(this).apply {
setTitle(R.string.settings__theme_editor__edit_theme_name_dialog_title)
setCancelable(true)
setView(dialogView.root)
setPositiveButton(android.R.string.ok, null)
setNegativeButton(android.R.string.cancel, null)
create()
dialog = show()
dialog.getButton(android.app.AlertDialog.BUTTON_POSITIVE)?.setOnClickListener {
val tempThemeLabel = dialogView.metaName.text.toString().trim()
if (Theme.validateField(Theme.ValidationField.THEME_LABEL, tempThemeLabel)) {
themeLabel = tempThemeLabel
dialog.dismiss()
} else {
dialogView.metaNameLabel.error = resources.getString(R.string.settings__theme_editor__error_theme_label_empty)
dialogView.metaNameLabel.isErrorEnabled = true
}
}
}
}
}

View File

@@ -17,26 +17,28 @@
package dev.patrickgold.florisboard.settings
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.widget.LinearLayout
import android.widget.RadioButton
import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.IdRes
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.view.forEach
import com.github.michaelbull.result.getOr
import com.github.michaelbull.result.onFailure
import com.github.michaelbull.result.onSuccess
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.databinding.ThemeManagerActivityBinding
import dev.patrickgold.florisboard.ime.core.FlorisActivity
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.extension.AssetManager
import dev.patrickgold.florisboard.ime.extension.AssetRef
import dev.patrickgold.florisboard.ime.extension.AssetSource
import dev.patrickgold.florisboard.ime.extension.ExternalContentUtils
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
import dev.patrickgold.florisboard.ime.text.layout.LayoutManager
import dev.patrickgold.florisboard.ime.theme.Theme
@@ -47,11 +49,9 @@ import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import timber.log.Timber
class ThemeManagerActivity : AppCompatActivity() {
private lateinit var binding: ThemeManagerActivityBinding
class ThemeManagerActivity : FlorisActivity<ThemeManagerActivityBinding>() {
private lateinit var layoutManager: LayoutManager
private val mainScope = MainScope()
private lateinit var prefs: PrefHelper
private val themeManager: ThemeManager = ThemeManager.default()
private var key: String = ""
@@ -59,19 +59,70 @@ class ThemeManagerActivity : AppCompatActivity() {
private var selectedTheme: Theme = Theme.empty()
private var selectedRef: AssetRef? = null
companion object {
private const val EDITOR_REQ_CODE: Int = 0xFB01
private val themeEditor = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult? ->
if (result?.resultCode == ThemeEditorActivity.RESULT_CODE_THEME_EDIT_SAVED) {
themeManager.update()
buildUi()
}
}
private val importTheme = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
// If uri is null it indicates that the selection activity was cancelled (mostly by pressing the back button),
// so we don't display an error message here.
if (uri == null) return@registerForActivityResult
val toBeImportedTheme = themeManager.loadTheme(uri)
if (toBeImportedTheme.isSuccess) {
val newTheme = toBeImportedTheme.getOrNull()!!.copy(
name = toBeImportedTheme.getOrNull()!!.name + "_imported",
label = toBeImportedTheme.getOrNull()!!.label + " (Imported)"
)
val newAssetRef = AssetRef(
AssetSource.Internal,
ThemeManager.THEME_PATH_REL + "/" + newTheme.name + ".json"
)
themeManager.writeTheme(newAssetRef, newTheme).onSuccess {
themeManager.update()
selectedTheme = newTheme
selectedRef = newAssetRef
setThemeRefInPrefs(newAssetRef)
buildUi()
showMessage(R.string.settings__theme_manager__theme_import_success)
}.onFailure {
showError(it)
}
} else {
showError(toBeImportedTheme.exceptionOrNull()!!)
}
}
private val exportTheme = registerForActivityResult(ActivityResultContracts.CreateDocument()) { uri: Uri? ->
// If uri is null it indicates that the selection activity was cancelled (mostly by pressing the back button,
// so we don't display an error message here.
if (uri == null) return@registerForActivityResult
val selectedRef = selectedRef
if (selectedRef != null) {
AssetManager.default().loadAssetRaw(selectedRef).onSuccess {
Timber.i(it)
ExternalContentUtils.writeTextToUri(this, uri, it).onSuccess {
showMessage(R.string.settings__theme_manager__theme_export_success)
}.onFailure {
showError(it)
}
}.onFailure {
showError(it)
}
} else {
showError(NullPointerException("selectedRef is null!"))
}
}
companion object {
const val EXTRA_KEY: String = "key"
const val EXTRA_DEFAULT_VALUE: String = "default_value"
}
override fun onCreate(savedInstanceState: Bundle?) {
prefs = PrefHelper.getDefaultInstance(this)
super.onCreate(savedInstanceState)
binding = ThemeManagerActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
key = intent.getStringExtra(EXTRA_KEY) ?: ""
defaultValue = intent.getStringExtra(EXTRA_DEFAULT_VALUE) ?: ""
@@ -91,8 +142,10 @@ class ThemeManagerActivity : AppCompatActivity() {
binding.fabOptionCreateEmpty.setOnClickListener { onActionClicked(it) }
binding.fabOptionCreateFromSelected.setOnClickListener { onActionClicked(it) }
binding.fabOptionImport.setOnClickListener { onActionClicked(it) }
binding.themeDeleteBtn.setOnClickListener { onActionClicked(it) }
binding.themeEditBtn.setOnClickListener { onActionClicked(it) }
binding.themeExportBtn.setOnClickListener { onActionClicked(it) }
layoutManager = LayoutManager(this).apply {
preloadComputedLayout(KeyboardMode.CHARACTERS, Subtype.DEFAULT, prefs)
@@ -101,6 +154,10 @@ class ThemeManagerActivity : AppCompatActivity() {
buildUi()
}
override fun onCreateBinding(): ThemeManagerActivityBinding {
return ThemeManagerActivityBinding.inflate(layoutInflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
@@ -132,7 +189,7 @@ class ThemeManagerActivity : AppCompatActivity() {
PrefHelper.Theme.NIGHT_THEME_REF -> prefs.theme.nightThemeRef
else -> ""
}
).getOr(null)
).getOrDefault(null)
}
}
@@ -177,15 +234,6 @@ class ThemeManagerActivity : AppCompatActivity() {
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == EDITOR_REQ_CODE) {
themeManager.update()
buildUi()
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
fun onActionClicked(view: View) {
when (view.id) {
R.id.fab_option_create_empty -> {
@@ -202,9 +250,11 @@ class ThemeManagerActivity : AppCompatActivity() {
ThemeManager.THEME_PATH_REL + "/" + newTheme.name + ".json"
)
themeManager.writeTheme(newAssetRef, newTheme).onSuccess {
startActivityForResult(Intent(this, ThemeEditorActivity::class.java).apply {
putExtra(ThemeEditorActivity.EXTRA_THEME_REF, newAssetRef.toString())
}, EDITOR_REQ_CODE)
themeEditor.launch(
Intent(this, ThemeEditorActivity::class.java).apply {
putExtra(ThemeEditorActivity.EXTRA_THEME_REF, newAssetRef.toString())
}
)
}.onFailure {
Timber.e(it.toString())
}
@@ -227,16 +277,18 @@ class ThemeManagerActivity : AppCompatActivity() {
ThemeManager.THEME_PATH_REL + "/" + themeCopy.name + ".json"
)
themeManager.writeTheme(newAssetRef, themeCopy).onSuccess {
startActivityForResult(Intent(this, ThemeEditorActivity::class.java).apply {
putExtra(ThemeEditorActivity.EXTRA_THEME_REF, newAssetRef.toString())
}, EDITOR_REQ_CODE)
themeEditor.launch(
Intent(this, ThemeEditorActivity::class.java).apply {
putExtra(ThemeEditorActivity.EXTRA_THEME_REF, newAssetRef.toString())
}
)
}.onFailure {
Timber.e(it.toString())
}
}
/*R.id.fab_option_import -> {
Toast.makeText(this, "Import not yet implemented", Toast.LENGTH_SHORT).show()
}*/
R.id.fab_option_import -> {
importTheme.launch("*/*")
}
R.id.theme_delete_btn -> {
val deleteRef = selectedRef?.copy()
if (deleteRef?.source == AssetSource.Internal) {
@@ -273,9 +325,11 @@ class ThemeManagerActivity : AppCompatActivity() {
R.id.theme_edit_btn -> {
val editRef = selectedRef
if (editRef?.source == AssetSource.Internal) {
startActivityForResult(Intent(this, ThemeEditorActivity::class.java).apply {
putExtra(ThemeEditorActivity.EXTRA_THEME_REF, editRef.toString())
}, EDITOR_REQ_CODE)
themeEditor.launch(
Intent(this, ThemeEditorActivity::class.java).apply {
putExtra(ThemeEditorActivity.EXTRA_THEME_REF, editRef.toString())
}
)
} else {
// This toast normally should never show, though if the edit button is enabled
// even if it shouldn't, just show a toast so the user knows the app is
@@ -287,6 +341,9 @@ class ThemeManagerActivity : AppCompatActivity() {
).show()
}
}
R.id.theme_export_btn -> {
exportTheme.launch("${selectedTheme.name}.json")
}
}
}

View File

@@ -24,7 +24,6 @@ import android.util.AttributeSet
import androidx.preference.Preference
import androidx.preference.Preference.OnPreferenceClickListener
import androidx.preference.PreferenceManager
import com.github.michaelbull.result.onSuccess
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.extension.AssetRef

View File

@@ -23,11 +23,13 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
import dev.patrickgold.florisboard.settings.components.DialogSeekBarPreference
class KeyboardFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener {
private var heightFactorCustom: DialogSeekBarPreference? = null
private var oneHandedModeScaleFactor: DialogSeekBarPreference? = null
private var utilityKeyAction: ListPreference? = null
private var sharedPrefs: SharedPreferences? = null
@@ -36,8 +38,10 @@ class KeyboardFragment : PreferenceFragmentCompat(),
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context)
heightFactorCustom = findPreference(PrefHelper.Keyboard.HEIGHT_FACTOR_CUSTOM)
oneHandedModeScaleFactor = findPreference(PrefHelper.Keyboard.ONE_HANDED_MODE_SCALE_FACTOR)
utilityKeyAction = findPreference(PrefHelper.Keyboard.UTILITY_KEY_ACTION)
onSharedPreferenceChanged(null, PrefHelper.Keyboard.HEIGHT_FACTOR)
onSharedPreferenceChanged(null, PrefHelper.Keyboard.ONE_HANDED_MODE)
onSharedPreferenceChanged(null, PrefHelper.Keyboard.UTILITY_KEY_ENABLED)
}
@@ -52,10 +56,16 @@ class KeyboardFragment : PreferenceFragmentCompat(),
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (key == PrefHelper.Keyboard.HEIGHT_FACTOR) {
heightFactorCustom?.isVisible = sharedPrefs?.getString(key, "") == "custom"
} else if (key == PrefHelper.Keyboard.UTILITY_KEY_ENABLED) {
utilityKeyAction?.isVisible = sharedPrefs?.getBoolean(key, false) == true
when (key) {
PrefHelper.Keyboard.HEIGHT_FACTOR -> {
heightFactorCustom?.isVisible = sharedPrefs?.getString(key, "") == "custom"
}
PrefHelper.Keyboard.ONE_HANDED_MODE -> {
oneHandedModeScaleFactor?.isEnabled = sharedPrefs?.getString(key, "") != OneHandedMode.OFF
}
PrefHelper.Keyboard.UTILITY_KEY_ENABLED -> {
utilityKeyAction?.isVisible = sharedPrefs?.getBoolean(key, false) == true
}
}
}
}

View File

@@ -33,7 +33,7 @@ class EnableImeFragment : Fragment() {
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
): View {
binding = SetupFragmentEnableImeBinding.inflate(inflater, container, false)
binding.languageAndInputButton.setOnClickListener {
val intent = Intent()

View File

@@ -30,7 +30,7 @@ class FinishFragment : Fragment() {
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
): View {
binding = SetupFragmentFinishBinding.inflate(inflater, container, false)
// Set theme to floris_day

View File

@@ -68,12 +68,12 @@ data class VersionName(
if (list.size == 3) {
return VersionName(list[0], list[1], list[2])
}
} else if (raw.matches("""[0-9]+[.][0-9]+[.][0-9]+[.][0-9]+""".toRegex())) {
} else if (raw.matches("""[0-9]+[.][0-9]+[.][0-9]+[-][0-9]+""".toRegex())) {
val list = raw.split(".").map { it.toInt() }
if (list.size == 4) {
return VersionName(list[0], list[1], list[2], null, list[3])
}
} else if (raw.matches("""[0-9]+[.][0-9]+[.][0-9]+[.][a-zA-Z]+""".toRegex())) {
} else if (raw.matches("""[0-9]+[.][0-9]+[.][0-9]+[-][a-zA-Z]+""".toRegex())) {
val list = raw.split(".")
if (list.size == 4) {
return VersionName(
@@ -81,7 +81,7 @@ data class VersionName(
list[3], null
)
}
} else if (raw.matches("""[0-9]+[.][0-9]+[.][0-9]+[.][a-zA-Z]+[0-9]+""".toRegex())) {
} else if (raw.matches("""[0-9]+[.][0-9]+[.][0-9]+[-][a-zA-Z]+[0-9]+""".toRegex())) {
val list = raw.split(".")
if (list.size == 4) {
val extraName = list[3].split("""[0-9]+""".toRegex())[0]

View File

@@ -35,7 +35,7 @@ object LocaleUtils {
}
}
class JsonAdapter() {
class JsonAdapter {
@FromJson
fun fromJson(raw: String): Locale {
return stringToLocale(raw)

View File

@@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?android:attr/textColorPrimary" android:pathData="M7 15h7v2H7zm0-4h10v2H7zm0-4h10v2H7zm12-4h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5c-.14 0-.27.01-.4.04-.39.08-.74.28-1.01.55-.18.18-.33.4-.43.64-.1.23-.16.49-.16.77v14c0 .27.06.54.16.78s.25.45.43.64c.27.27.62.47 1.01.55.13.02.26.03.4.03h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7-.25c.41 0 .75.34.75.75s-.34.75-.75.75-.75-.34-.75-.75.34-.75.75-.75zM19 19H5V5h14v14z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?android:attr/textColorPrimary" android:pathData="M5 13h14v-2H5v2zm-2 4h14v-2H3v2zM7 7v2h14V7H7z"/>
</vector>

View File

@@ -0,0 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportHeight="24"
android:autoMirrored="true">
<path android:fillColor="?android:attr/textColorPrimary" android:pathData="M14,4v5c0,1.12,0.37,2.16,1,3H9c0.65-0.86,1-1.9,1-3V4H14 M17,2H7C6.45,2,6,2.45,6,3c0,0.55,0.45,1,1,1c0,0,0,0,0,0l1,0v5 c0,1.66-1.34,3-3,3v2h5.97v7l1,1l1-1v-7H19v-2c0,0,0,0,0,0c-1.66,0-3-1.34-3-3V4l1,0c0,0,0,0,0,0c0.55,0,1-0.45,1-1 C18,2.45,17.55,2,17,2L17,2z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?android:attr/textColorPrimary" android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
</vector>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Bottom 2dp Shadow -->
<item>
<shape android:shape="rectangle">
<solid android:color="@color/cardview_shadow_start_color" />
<corners android:radius="7dp" />
</shape>
</item>
<!-- White Top color -->
<item android:right="3px" android:bottom="5px">
<shape android:shape="rectangle">
<corners android:radius="7dp" />
</shape>
</item>
</layer-list>

View File

@@ -20,11 +20,12 @@
<ImageView
android:id="@+id/app_icon"
android:src="@mipmap/ic_launcher"
android:background="@mipmap/floris_app_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingBottom="8dp"
android:layout_marginTop="24dp"
android:layout_marginBottom="8dp"
android:elevation="8dp"
android:contentDescription="@string/about__app_icon_content_description"/>
<TextView

View File

@@ -0,0 +1,105 @@
<?xml version="1.0" encoding="utf-8"?>
<dev.patrickgold.florisboard.ime.clip.ClipboardPopupView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:id="@+id/pin_clip_item"
android:orientation="horizontal"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/pin_clip_item_icon"
android:layout_width="wrap_content"
android:scaleX="0.8"
android:scaleY="0.8"
android:layout_height="wrap_content"
android:src="@drawable/ic_pin"
android:contentDescription="@string/clip__pin_item" />
<Space
android:layout_width="10dp"
android:layout_height="0dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/clip__pin_item"
android:id="@+id/pin_clip_item_text"
tools:layout_editor_absoluteY="3dp" />
</LinearLayout>
<Space
android:layout_width="0dp"
android:layout_height="10dp"/>
<LinearLayout
android:layout_width="match_parent"
android:id="@+id/remove_from_history"
android:orientation="horizontal"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/remove_from_history_icon"
android:layout_width="wrap_content"
android:scaleX="0.8"
android:scaleY="0.8"
android:layout_height="wrap_content"
android:src="@drawable/ic_delete"
android:contentDescription="@string/clip__delete_item" />
<Space
android:layout_width="10dp"
android:layout_height="0dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/clip__delete_item"
android:id="@+id/remove_from_history_text"
tools:layout_editor_absoluteY="3dp" />
</LinearLayout>
<Space
android:id="@+id/paste_clip_item_space"
android:layout_width="0dp"
android:layout_height="10dp"/>
<LinearLayout
android:layout_width="match_parent"
android:id="@+id/paste_clip_item"
android:orientation="horizontal"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/paste_clip_item_icon"
android:layout_width="wrap_content"
android:scaleX="0.8"
android:scaleY="0.8"
android:layout_height="wrap_content"
android:src="@drawable/ic_content_paste"
android:contentDescription="@string/clip__paste_item" />
<Space
android:layout_width="10dp"
android:layout_height="0dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/clip__paste_item"
android:id="@+id/paste_clip_item_text"
tools:layout_editor_absoluteY="3dp" />
</LinearLayout>
</dev.patrickgold.florisboard.ime.clip.ClipboardPopupView>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<dev.patrickgold.florisboard.ime.clip.ClipboardHistoryItemView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:padding="0dp"
android:background="@drawable/shape_rect_rounded_3"
android:foreground="?selectableItemBackground"
>
<ImageView
android:id="@+id/clipboard_history_item_img"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:layout_marginEnd="0dp"
android:padding="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:background="@drawable/shape_rect_rounded_3"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/clipboard_pin"
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_marginEnd="5dp"
android:layout_marginTop="5dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:src="@drawable/ic_pin"
android:contentDescription="@string/clip__pin_item"/>
</dev.patrickgold.florisboard.ime.clip.ClipboardHistoryItemView>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<dev.patrickgold.florisboard.ime.clip.ClipboardHistoryItemView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:padding="5dp"
android:foreground="?selectableItemBackground"
android:background="@drawable/shape_rect_rounded_3"
>
<TextView
android:id="@+id/clipboard_history_item_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="25dp"
android:padding="6dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="A clipboard item would go here." />
<ImageView
android:id="@+id/clipboard_pin"
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_marginEnd="5dp"
android:layout_marginTop="5dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:src="@drawable/ic_pin"
android:contentDescription="@string/clip__pin_item"/>
</dev.patrickgold.florisboard.ime.clip.ClipboardHistoryItemView>

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<dev.patrickgold.florisboard.ime.clip.ClipboardHistoryView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/clip_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/clipboard_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageButton
android:id="@+id/back_to_keyboard_button"
android:layout_width="@dimen/clipboard_button_width"
android:layout_height="@dimen/clipboard_button_height"
android:layout_gravity="center_vertical"
android:background="@drawable/button_transparent_bg_on_press"
android:hapticFeedbackEnabled="false"
android:soundEffectsEnabled="false"
android:layout_weight="0"
android:src="@drawable/ic_arrow_back"
android:contentDescription="@string/clip__back_to_text_input" />
<TextView
android:id="@+id/clipboard_text"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:layout_weight="1"
android:text="@string/clip__context_header"
android:textSize="15sp" />
<ImageButton
android:id="@+id/clear_clipboard_history"
android:layout_width="@dimen/clipboard_button_width"
android:layout_height="@dimen/clipboard_button_height"
android:layout_gravity="center_vertical"
android:background="@drawable/button_transparent_bg_on_press"
android:hapticFeedbackEnabled="false"
android:soundEffectsEnabled="false"
android:layout_weight="0"
android:src="@drawable/ic_clear_all"
android:contentDescription="@string/clip__clear_history" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/clipboard_history_items"
android:paddingLeft="5dp"
android:paddingRight="5dp"
/>
</LinearLayout>
</dev.patrickgold.florisboard.ime.clip.ClipboardHistoryView>

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<dev.patrickgold.florisboard.ime.core.InputWindowView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/florisboard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -18,29 +19,32 @@
android:background="?inputView_bgColorFallback"
android:baselineAligned="false">
<LinearLayout
<dev.patrickgold.florisboard.ime.onehanded.OneHandedPanel
android:id="@+id/one_handed_ctrl_panel_start"
style="@style/OneHandedPanel"
android:visibility="gone">
android:layout_width="@dimen/one_handed_width"
android:layout_height="match_parent"
android:layout_weight="0"
android:visibility="gone"
app:panelSide="start">
<ImageButton
android:id="@+id/one_handed_ctrl_close_start"
style="@style/OneHandedPanelButton"
android:src="@drawable/ic_zoom_out_map"
android:contentDescription="@string/one_handed__close_btn_content_description"/>
<View
android:tag="one_handed_ctrl_close"
android:layout_width="match_parent"
android:layout_height="@dimen/one_handed_button_height"
android:visibility="invisible"/>
android:layout_marginBottom="@dimen/one_handed_button_height"
android:src="@drawable/ic_zoom_out_map"
android:background="@drawable/button_transparent_bg_on_press"
android:contentDescription="@string/one_handed__close_btn_content_description"/>
<ImageButton
android:id="@+id/one_handed_ctrl_move_start"
style="@style/OneHandedPanelButton"
android:tag="one_handed_ctrl_move"
android:layout_width="match_parent"
android:layout_height="@dimen/one_handed_button_height"
android:src="@drawable/ic_keyboard_arrow_left"
android:background="@drawable/button_transparent_bg_on_press"
android:contentDescription="@string/one_handed__move_start_btn_content_description"/>
</LinearLayout>
</dev.patrickgold.florisboard.ime.onehanded.OneHandedPanel>
<dev.patrickgold.florisboard.ime.core.FlorisViewFlipper
android:id="@+id/main_view_flipper"
@@ -53,31 +57,36 @@
<include layout="@layout/media_input_layout"/>
<include layout="@layout/clipboard_layout"/>
</dev.patrickgold.florisboard.ime.core.FlorisViewFlipper>
<LinearLayout
<dev.patrickgold.florisboard.ime.onehanded.OneHandedPanel
android:id="@+id/one_handed_ctrl_panel_end"
style="@style/OneHandedPanel"
android:visibility="gone">
android:layout_width="@dimen/one_handed_width"
android:layout_height="match_parent"
android:layout_weight="0"
android:visibility="gone"
app:panelSide="end">
<ImageButton
android:id="@+id/one_handed_ctrl_close_end"
style="@style/OneHandedPanelButton"
android:src="@drawable/ic_zoom_out_map"
android:contentDescription="@string/one_handed__close_btn_content_description"/>
<View
android:tag="one_handed_ctrl_close"
android:layout_width="match_parent"
android:layout_height="@dimen/one_handed_button_height"
android:visibility="invisible"/>
android:layout_marginBottom="@dimen/one_handed_button_height"
android:src="@drawable/ic_zoom_out_map"
android:background="@drawable/button_transparent_bg_on_press"
android:contentDescription="@string/one_handed__close_btn_content_description"/>
<ImageButton
android:id="@+id/one_handed_ctrl_move_end"
style="@style/OneHandedPanelButton"
android:tag="one_handed_ctrl_move"
android:layout_width="match_parent"
android:layout_height="@dimen/one_handed_button_height"
android:src="@drawable/ic_keyboard_arrow_right"
android:contentDescription="@string/one_handed__move_end_btn_content_description"/>
android:background="@drawable/button_transparent_bg_on_press"
android:contentDescription="@string/one_handed__move_start_btn_content_description"/>
</LinearLayout>
</dev.patrickgold.florisboard.ime.onehanded.OneHandedPanel>
</dev.patrickgold.florisboard.ime.core.InputView>

View File

@@ -39,38 +39,9 @@
app:layout_constraintEnd_toStartOf="@id/action_end_area"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
<dev.patrickgold.florisboard.ime.text.smartbar.CandidateView
android:id="@+id/candidates"
style="@style/SmartbarContainer">
<Button
android:id="@+id/candidate0"
style="@style/SmartbarCandidate"/>
<View style="@style/SmartbarDivider"/>
<Button
android:id="@+id/candidate1"
style="@style/SmartbarCandidate"/>
<View style="@style/SmartbarDivider"/>
<Button
android:id="@+id/candidate2"
style="@style/SmartbarCandidate"/>
</LinearLayout>
<LinearLayout
android:id="@+id/clipboard_suggestion_row"
style="@style/SmartbarContainer">
<Button
android:id="@+id/clipboard_suggestion"
android:drawableStart="@drawable/ic_content_paste_with_padding"
style="@style/SmartbarQuickAction.ClipboardSuggestion"/>
</LinearLayout>
style="@style/SmartbarContainer"/>
<LinearLayout
android:id="@+id/quick_actions"

View File

@@ -42,28 +42,31 @@
android:layout_margin="8dp"
app:isPreviewKeyboard="true"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/theme_name_label"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginHorizontal="8dp"
android:hint="@string/settings__theme_editor__name_label"
app:boxBackgroundMode="outline"
app:boxBackgroundColor="?android:windowBackground"
app:boxStrokeColor="?colorAccent"
app:boxStrokeErrorColor="?colorError"
app:boxStrokeWidth="1dp">
android:orientation="horizontal"
android:layout_margin="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/theme_name_value"
android:layout_width="match_parent"
<TextView
android:id="@+id/theme_name_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:importantForAutofill="no"
android:inputType="textFilter"
android:imeOptions="flagForceAscii|flagNoExtractUi"/>
android:layout_weight="1.0"
android:textColor="?android:textColorPrimary"
tools:text="GroupName"/>
</com.google.android.material.textfield.TextInputLayout>
<ImageButton
android:id="@+id/theme_name_edit_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_edit"
android:tint="?android:textColorPrimary"
android:contentDescription="@string/settings__theme_editor__edit_group_dialog_title"/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,31 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="20dp"
android:paddingTop="16dp"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/meta_name_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/settings__theme_editor__name_label"
app:boxBackgroundMode="outline"
app:boxBackgroundColor="?android:windowBackground"
app:boxStrokeColor="?colorAccent"
app:boxStrokeErrorColor="?colorError"
app:boxStrokeWidth="1dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/meta_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:importantForAutofill="no"
android:inputType="textFilter"
android:imeOptions="flagForceAscii|flagNoExtractUi"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View File

@@ -112,15 +112,30 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
android:orientation="horizontal">
<Button
android:id="@+id/theme_export_btn"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAllCaps="false"
android:text="@string/assets__action__export"
android:textColor="?android:textColorPrimary"
android:drawableStart="@drawable/ic_share"
android:drawablePadding="8dp"
android:drawableTint="?colorAccent"/>
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1.0"/>
<Button
android:id="@+id/theme_delete_btn"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:textAllCaps="false"
android:text="@string/assets__action__delete"
android:textColor="?android:textColorPrimary"
@@ -133,7 +148,6 @@
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:textAllCaps="false"
android:text="@string/assets__action__edit"
android:textColor="?android:textColorPrimary"
@@ -160,7 +174,8 @@
<com.nambimobile.widgets.efab.ExpandableFabLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
app:layout_dodgeInsetEdges="bottom">
<com.nambimobile.widgets.efab.Overlay
android:layout_width="match_parent"
@@ -189,13 +204,13 @@
app:fab_color="?colorPrimaryDark"
app:fab_icon="@drawable/ic_file"
app:label_text="@string/settings__theme_manager__create_from_selected"/>
<!--<com.nambimobile.widgets.efab.FabOption
<com.nambimobile.widgets.efab.FabOption
android:id="@+id/fab_option_import"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_color="?colorPrimaryDark"
app:fab_icon="@drawable/ic_input"
app:label_text="@string/assets__action__import"/>-->
app:label_text="@string/assets__action__import"/>
</com.nambimobile.widgets.efab.ExpandableFabLayout>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_app_icon_beta_background"/>
<foreground android:drawable="@mipmap/ic_app_icon_beta_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_app_icon_beta_background"/>
<foreground android:drawable="@mipmap/ic_app_icon_beta_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_app_icon_debug_background"/>
<foreground android:drawable="@mipmap/ic_app_icon_debug_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_app_icon_debug_background"/>
<foreground android:drawable="@mipmap/ic_app_icon_debug_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_app_icon_release_background"/>
<foreground android:drawable="@mipmap/ic_app_icon_release_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_app_icon_release_background"/>
<foreground android:drawable="@mipmap/ic_app_icon_release_foreground"/>
</adaptive-icon>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 B

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