Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1307f401cc | ||
|
|
ca6006767b | ||
|
|
2202db53ba | ||
|
|
321f19272e | ||
|
|
06a8a04020 | ||
|
|
2a1f7c3217 | ||
|
|
76952d55fe | ||
|
|
1f560f8b6b | ||
|
|
33bdc52354 | ||
|
|
97b795aed0 | ||
|
|
bb44362701 | ||
|
|
bab20c5baa | ||
|
|
a3000fe111 | ||
|
|
d4d2f52683 | ||
|
|
10ef340559 | ||
|
|
5b77262186 | ||
|
|
8ce56b1bf9 | ||
|
|
94667e8363 | ||
|
|
970b5eb82a | ||
|
|
a2ceed4521 | ||
|
|
6d7825e129 | ||
|
|
10c1a82995 | ||
|
|
267a39e870 | ||
|
|
f6fcbbcc34 | ||
|
|
f98b3cec4b | ||
|
|
e5a942be9f | ||
|
|
edc63aa680 | ||
|
|
23def145b2 | ||
|
|
3f7bd4f65d | ||
|
|
7b91d4f9d3 | ||
|
|
175369f7d7 | ||
|
|
79c5acc007 | ||
|
|
94d470dd96 | ||
|
|
ee9d61ad1e | ||
|
|
a3c7b538d0 | ||
|
|
ca4cd38bb2 | ||
|
|
7046c500ff | ||
|
|
0374a82f99 | ||
|
|
217acbd6f1 | ||
|
|
ef27d511be | ||
|
|
f9a4ffa5eb | ||
|
|
5533badd19 | ||
|
|
0f1b4b081d | ||
|
|
3feae09df0 | ||
|
|
34bb28d1fc | ||
|
|
551a294b05 | ||
|
|
671ff1d8b4 | ||
|
|
15caf66370 | ||
|
|
ae0a8e551b | ||
|
|
cb4bedfc2c | ||
|
|
7d63a6885c | ||
|
|
841d797b7c | ||
|
|
0c9ba5326a | ||
|
|
7c5a7dc148 | ||
|
|
37fc714729 | ||
|
|
ec7d65ebc0 | ||
|
|
5670af16d6 | ||
|
|
6b39a846e6 | ||
|
|
a25501d63c | ||
|
|
e9a5f2161c | ||
|
|
6f12f22937 | ||
|
|
25054ef679 | ||
|
|
3af17f99fe | ||
|
|
06664ff521 | ||
|
|
fde0749a3b | ||
|
|
c061e15263 | ||
|
|
7256c597c2 | ||
|
|
2f9d32027b | ||
|
|
538912edc2 | ||
|
|
ee5ff81ee8 | ||
|
|
d873dc54c5 | ||
|
|
1e967463de | ||
|
|
5c084a10dc | ||
|
|
f158a9deb3 | ||
|
|
66d328293c | ||
|
|
e33b652bb3 | ||
|
|
65d8c02b95 | ||
|
|
bd090132eb | ||
|
|
0eb5ca318b | ||
|
|
dfa9df6cd6 | ||
|
|
3f5dfbc852 | ||
|
|
59caafbf19 | ||
|
|
037a452baf | ||
|
|
ffa405f289 | ||
|
|
5d7091582f | ||
|
|
b4096f2cfb | ||
|
|
81c62f3e91 | ||
|
|
5c7db2b344 | ||
|
|
30bca99092 | ||
|
|
9a9445dab1 | ||
|
|
1fbfc32429 | ||
|
|
645b682451 |
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,34 +1,34 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help fix a bug
|
||||
about: Create a report to help FlorisBoard improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
#### Short description of bug
|
||||
A short but clear and concise description of what the bug is.
|
||||
<!--
|
||||
- Describe the bug in a short but concise way.
|
||||
- If you have a screenshot or screen recording of the bug, link them at
|
||||
the end of this issue.
|
||||
- Please search existing bug reports to avoid creating duplicates.
|
||||
- Thank you for your help in making FlorisBoard better!
|
||||
-->
|
||||
|
||||
#### Steps to reproduce
|
||||
**Environment information**
|
||||
- FlorisBoard Version: <!-- e.g. 0.1.0 -->
|
||||
- Install Source: <!-- Google PlayStore/F-Droid/GitHub/? -->
|
||||
- Device: <!-- e.g. OnePlus 7T -->
|
||||
- Android version, ROM: <!-- e.g. 10, Stock -->
|
||||
|
||||
**Steps to reproduce**
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
#### Expected behavior
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
#### What happened instead?
|
||||
A detailed description of what you expected to happen. If you have screenshots or a screen recording, add it here.
|
||||
|
||||
#### Additional info
|
||||
- Version: [e.g. 0.1.0]
|
||||
- Source: [e.g. Google PlayStore/F-Droid/GitHub/?]
|
||||
- Device: [e.g. OnePlus 7T]
|
||||
- Android version, ROM: [e.g. 10, Stock]
|
||||
|
||||
#### Log
|
||||
<!-- (remove this line if you paste a log)
|
||||
```
|
||||
If applicable, paste the captured debug log here.
|
||||
```
|
||||
(remove this line if you paste a log) -->
|
||||
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: General feedback
|
||||
url: https://github.com/florisboard/florisboard/blob/master/CONTRIBUTING.md
|
||||
about: Give general feedback about this project
|
||||
26
.github/ISSUE_TEMPLATE/feature_request.md
vendored
26
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,21 +1,19 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea or enhancement for this project
|
||||
name: Feature request / Suggestion
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: proposal
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
#### Short description of your idea
|
||||
A short but clear and concise description of your idea.
|
||||
|
||||
#### Detailed description of your idea
|
||||
A clear and concise description of what you want to be added or changed. If you also have
|
||||
an idea how to implement it, please describe it here.
|
||||
|
||||
#### Alternatives to your idea
|
||||
If you have considered an alternative solution for your idea, describe it here.
|
||||
|
||||
#### Additional context
|
||||
Add any other context or screenshots about the feature request here.
|
||||
<!--
|
||||
- Describe your idea in a short but concise way.
|
||||
- If you have multiple ideas which are not directly connected to each
|
||||
other, file an issue per idea. This makes it easy to implement one
|
||||
feature proposal at a time.
|
||||
- If you have any examples, e.g. screenshots or other keyboards which
|
||||
have the proposed feature implemented, link them here.
|
||||
- Please search existing proposals to avoid creating duplicates.
|
||||
- Thank you for your help in making FlorisBoard better!
|
||||
-->
|
||||
|
||||
16
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
16
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: Question
|
||||
about: Ask here if you have a question about FlorisBoard
|
||||
title: ''
|
||||
labels: question
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
- If you need assistance in using FlorisBoard, ask it here!
|
||||
- If you want to suggest an idea for this project, please use the
|
||||
Feature request template instead.
|
||||
- Please search existing questions to avoid creating duplicates.
|
||||
- Thank you for your help in making FlorisBoard better!
|
||||
-->
|
||||
@@ -2,62 +2,42 @@
|
||||
|
||||
First off, thanks for considering contributing to FlorisBoard!
|
||||
|
||||
There are several ways to contribute to FlorisBoard. This document provides some
|
||||
general guidelines for each type of contribution.
|
||||
There are several ways to contribute to FlorisBoard. This document
|
||||
provides some general guidelines for each type of contribution.
|
||||
|
||||
## Giving general feedback
|
||||
|
||||
Either use the review function within Google Play or email me at
|
||||
[florisboard@patrickgold.dev](mailto:florisboard@patrickgold.dev). I
|
||||
love to hear from you!
|
||||
|
||||
## Translations
|
||||
|
||||
To make FlorisBoard accessible in as many languages as possible, the
|
||||
platform  is used
|
||||
to crowdsource and manage translations. This is the only source of
|
||||
translations from now on - **PRs that add/update translations are no
|
||||
longer accepted.** The list of languages in Crowdin covers the top 20
|
||||
languages, but feel free to email me at
|
||||
[florisboard@patrickgold.dev](mailto:florisboard@patrickgold.dev) to
|
||||
request a language and I'll add it.
|
||||
|
||||
## Adding a new feature or making large changes
|
||||
|
||||
If you intend to add a new feature or to make large changes, please discuss this
|
||||
first through a proposal on GitHub. Discussing your idea enables both you and the
|
||||
dev team that we are on the same page before you start on working on your change.
|
||||
If you have any questions, feel free to ask for help at any time!
|
||||
If you intend to add a new feature or to make large changes, please
|
||||
discuss this first through a proposal on GitHub. Discussing your idea
|
||||
enables both you and the dev team that we are on the same page before
|
||||
you start on working on your change. If you have any questions, feel
|
||||
free to ask for help at any time!
|
||||
|
||||
## Adding a new keyboard layout / dictionary for locale
|
||||
|
||||
As FlorisBoard is currently in alpha stage, things might change drastically. This
|
||||
also includes the config scheme of keyboard layouts. To prevent incompatible
|
||||
configs because some features and structures may change, please do not add this
|
||||
kind of content yet. As FlorisBoard's state progresses and its core stabilizes,
|
||||
you will be able to add keyboard layouts.
|
||||
|
||||
## Translating FlorisBoard
|
||||
|
||||
Before starting to translate, when adding a new translation please file
|
||||
an issue stating that you want to translate FlorisBoard into a language.
|
||||
Once this gets approved you can start translating. When updating an
|
||||
already existing translation file you can just send a PR directly.
|
||||
|
||||
If you are not familiar with PRs, check out this guide:
|
||||
[https://www.gun.io/blog/how-to-github-fork-branch-and-pull-request](https://www.gun.io/blog/how-to-github-fork-branch-and-pull-request)
|
||||
|
||||
Notes for tips below:
|
||||
- Replace `<language>` with the language you want to add
|
||||
- Replace `<code>` with the ISO 639-1 code of the language you want to
|
||||
add
|
||||
([List of codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes))
|
||||
|
||||
### Tips when adding a new translation
|
||||
|
||||
- To add the new translation file, navigate to `app/src/main/res/values`
|
||||
and copy the file `strings.xml` into the folder
|
||||
`app/src/main/res/values-<code>` (you have to create this folder)
|
||||
- Translate only the phrases inside the brackets, leave the name
|
||||
attribute as it is
|
||||
E.g.: `<string name="hello_string">Hello World!</string>`
|
||||
`<string name="hello_string">Ciao mondo!</string>`
|
||||
- When finished translating, commit your changes locally, as the commit
|
||||
message use `Add <language> translation`
|
||||
- Push your change(s) and create the PR. When everything checks out, it
|
||||
will get accepted.
|
||||
|
||||
### Tips when updating a translation
|
||||
- To update a translation, check the `strings.xml` in
|
||||
`app/src/main/res/values` for newly added strings and add them to the
|
||||
translation file in `app/src/main/res/values-<code>`
|
||||
- When finished translating, commit your changes locally, as the commit
|
||||
message use `Update <language> translation`
|
||||
- Push your change(s) and create the PR. When everything checks out, it
|
||||
will get accepted.
|
||||
As FlorisBoard is currently in alpha stage, things might change
|
||||
drastically. This also includes the config scheme of keyboard layouts.
|
||||
To prevent incompatible configs because some features and structures may
|
||||
change, please do not add this kind of content yet. As FlorisBoard's
|
||||
state progresses and its core stabilizes, you will be able to add
|
||||
keyboard layouts.
|
||||
|
||||
## Bug reporting
|
||||
|
||||
@@ -68,6 +48,11 @@ use the premade [issue template](.github/ISSUE_TEMPLATE/bug_report.md)
|
||||
for bug reporting. This makes it easy for us to understand what the bug
|
||||
is and how to solve it.
|
||||
|
||||
### Capturing ADB debug logs
|
||||
### Capturing error logs
|
||||
|
||||
[[ TODO: create tutorial ]]
|
||||
Logs are captured by FlorisBoard's crash handler, which gives you the
|
||||
ability to copy it to the clipboard and paste it in GitHub. This is the
|
||||
preferred way to capture logs.
|
||||
|
||||
Alternatively, you can also use ADB (Android Debug Bridge) to capture
|
||||
the error log. This is recommended for experienced users only.
|
||||
|
||||
110
README.md
110
README.md
@@ -1,10 +1,23 @@
|
||||
# FlorisBoard
|
||||
<img align="left" width="80" height="80"
|
||||
src="fastlane/metadata/android/en-US/images/icon.png" alt="App icon">
|
||||
|
||||
An open-source keyboard for Android. Currently in alpha stage.
|
||||
# FlorisBoard  [](https://crowdin.florisboard.patrickgold.dev)
|
||||
|
||||
#### Public Alpha Test Programme
|
||||
Wanna try it out on your device? You can join the public alpha test
|
||||
programme on Google Play. To become a tester, follow these steps:
|
||||
**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.
|
||||
|
||||
## Public Alpha Test Programme
|
||||
Wanna try it out on your device? Use one of the following options:
|
||||
|
||||
_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)
|
||||
|
||||
_B. Google Play Public Alpha Test_:
|
||||
|
||||
You can join the public alpha test programme on Google Play. To become a
|
||||
tester, follow these steps:
|
||||
1. Join the
|
||||
[FlorisBoard Public Alpha Test](https://groups.google.com/g/florisboard-public-alpha-test)
|
||||
Google Group to be able to access the testing programme.
|
||||
@@ -18,65 +31,63 @@ programme on Google Play. To become a tester, follow these steps:
|
||||
4. Finished! You will receive future versions of FlorisBoard via Google
|
||||
Play.
|
||||
|
||||
##### Giving feedback
|
||||
If you want to give feedback to FlorisBoard, there are 2 ways to do so,
|
||||
as listed below:
|
||||
- *General feedback:* use the private feedback to developer section on
|
||||
the PlayStore listing.
|
||||
- *Bug reports or feature requests:* see the
|
||||
[contribution guidelines](CONTRIBUTING.md)
|
||||
_C. Use the APK provided in the release section of this repo_
|
||||
|
||||
Thank you for contributing to FlorisBoard!
|
||||
### 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).
|
||||
|
||||
##### Note on F-Droid release
|
||||
FlorisBoard is currently only available through Google Play, but it is
|
||||
planned to also release it via F-Droid later on. There is no exact
|
||||
timeline for this, but I aim for the 0.2.0 or 0.3.0 release.
|
||||
### Note on F-Droid release
|
||||
FlorisBoard is currently available through Google Play and IzzySoft's
|
||||
repo for F-Droid, but is in the inclusion process for the main F-Droid
|
||||
repo. Planned proper F-Droid release is version 0.3.0.
|
||||
|
||||
---
|
||||
|
||||

|
||||
<img align="right" height="256"
|
||||
src="https://patrickgold.dev/media/previews/florisboard-preview-day.png"
|
||||
alt="Preview image">
|
||||
|
||||
## Feature roadmap
|
||||
|
||||
### Basics
|
||||
* [x] Implementation of the keyboard core (InputMethodService)
|
||||
* [x] Own implementation of deprecated KeyboardView (base only)
|
||||
* [x] Custom implementation of deprecated KeyboardView (base only)
|
||||
* [x] Caps + Caps Lock
|
||||
* [x] Key popups
|
||||
* [x] Extended key popups (e.g. a -> á, à, ä, ...) (needs tweaks for
|
||||
emojis)
|
||||
* [x] Extended key popups (e.g. a -> á, à, ä, ...)
|
||||
* [x] Key press sound/vibration
|
||||
* [x] Portrait orientation support
|
||||
* [x] Landscape orientation support (needs tweaks)
|
||||
* [ ] Tablet screen support
|
||||
* [ ] Tablet screen support (0.4.0)
|
||||
|
||||
### Layouts
|
||||
* [x] Latin character layout (QWERTY)
|
||||
* [x] Other character layouts (both latin and non-latin) (Currently
|
||||
QWERTZ, AZERTY, swiss and spanish are supported besides QWERTY)
|
||||
* [x] Latin character layouts (QWERTY, QWERTZ, AZERTY, Swiss, Spanish,
|
||||
Norwegian, Swedish/Finnish, Icelandic, Danish); more coming in
|
||||
future versions
|
||||
* [x] Non-latin character layouts (Persian)
|
||||
* [x] Adapt to situation in app (password, url, text, etc. )
|
||||
* [x] Special character layout(s)
|
||||
* [x] Numeric layout
|
||||
* [x] Numeric layout (advanced)
|
||||
* [x] Phone number layout
|
||||
* [x] Emoji layout (popups buggy atm)
|
||||
* [x] Emoji layout (tweaks: 0.3.0)
|
||||
* [x] Emoticon layout
|
||||
* [ ] Kaomoji layout
|
||||
* [ ] Kaomoji layout (0.5.0)
|
||||
|
||||
### Preferences
|
||||
* [x] Setup wizard
|
||||
* [x] Preferences screen
|
||||
* [x] Customize look and behaviour of keyboard (currently only
|
||||
light/dark theme)
|
||||
* [ ] Theme customization
|
||||
* [ ] Theme import/export (?)
|
||||
* [x] Customize look and behaviour of keyboard
|
||||
* [x] Theme presets (currently only day/night theme)
|
||||
* [x] Theme customization
|
||||
* [ ] Theme import/export (0.4.0 or 0.5.0)
|
||||
* [x] Subtype selection (language/layout)
|
||||
* [x] Keyboard behaviour preferences
|
||||
* [ ] Text suggestion / Auto correct preferences
|
||||
* [ ] Gesture preferences
|
||||
* [ ] Text suggestion / Auto correct preferences (0.4.0 or 0.5.0)
|
||||
* [x] Gesture preferences (0.3.0)
|
||||
|
||||
### Composing suggestions
|
||||
### Composing suggestions (0.4.0 or 0.5.0)
|
||||
* [ ] Auto suggest words from precompiled dictionary
|
||||
* [ ] Auto suggest words from user dictionary
|
||||
* [ ] Auto suggest contacts
|
||||
@@ -84,26 +95,45 @@ timeline for this, but I aim for the 0.2.0 or 0.3.0 release.
|
||||
|
||||
### Other useful features
|
||||
* [x] One-handed mode
|
||||
* [ ] Clipboard manager (?)
|
||||
* [ ] Floating keyboard
|
||||
* [ ] Gesture support
|
||||
* [ ] Glide typing (?)
|
||||
* [x] Clipboard/cursor tools
|
||||
* [x] Integrated number row / symbols in character layouts (0.3.0)
|
||||
* [ ] Floating keyboard (0.4.0)
|
||||
* [x] Gesture support (0.3.0)
|
||||
* [ ] Glide typing (0.4.0)
|
||||
* [x] Full integration in IME service list of Android (xml/method)
|
||||
(integration is internal-only, because Android's default subtype
|
||||
implementation not really allows for dynamic language/layout
|
||||
pairs, only compile-time defined ones)
|
||||
* [ ] Description and settings reference in System Language & Input
|
||||
* [ ] (dev only) Generate well-structured documentation of code
|
||||
* [ ] ...
|
||||
|
||||
Note: (?) = not sure if it will be implemented
|
||||
Note:
|
||||
|
||||
## Used libraries and icons
|
||||
(?) = not sure if it will be implemented
|
||||
|
||||
(0.x.0) = planned version when feature will be implemented.
|
||||
|
||||
## Contributing
|
||||
Wanna contribute to FlorisBoard? That's great to hear! There are lots of
|
||||
different ways to help out. Bug reporting, making pull requests,
|
||||
translating FlorisBoard to make it more accessible, etc. For more
|
||||
information see the . Thank
|
||||
you for your help!
|
||||
|
||||
## List of permissions FlorisBoard requests
|
||||
Please refer to this [page](https://github.com/florisboard/florisboard/wiki/List-of-permissions-FlorisBoard-requests)
|
||||
to get more information on this topic.
|
||||
|
||||
## Used libraries, components and icons
|
||||
* [Google Flexbox Layout for Android](https://github.com/google/flexbox-layout)
|
||||
by [google](https://github.com/google)
|
||||
* [Google Material icons](https://github.com/google/material-design-icons) by
|
||||
[google](https://github.com/google)
|
||||
* [Moshi JSON library](https://github.com/square/moshi) by
|
||||
[square](https://github.com/square)
|
||||
* [ColorPicker preference](https://github.com/jaredrummler/ColorPicker) by
|
||||
[Jared Rummler](https://github.com/jaredrummler)
|
||||
|
||||
## License
|
||||
```
|
||||
|
||||
@@ -10,8 +10,8 @@ android {
|
||||
applicationId "dev.patrickgold.florisboard"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 29
|
||||
versionCode 10
|
||||
versionName "0.1.1"
|
||||
versionCode 18
|
||||
versionName "0.2.6"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -31,18 +31,21 @@ android {
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.core:core-ktx:1.3.0'
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'androidx.core:core-ktx:1.3.2'
|
||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'androidx.test:core:1.2.0'
|
||||
testImplementation 'androidx.test:core:1.3.0'
|
||||
testImplementation 'org.mockito:mockito-core:1.10.19'
|
||||
testImplementation 'org.mockito:mockito-inline:2.13.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
implementation 'com.google.android:flexbox:2.0.1'
|
||||
implementation "com.squareup.moshi:moshi-kotlin:1.9.2"
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.2.1'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7"
|
||||
implementation 'com.jaredrummler:colorpicker:1.1.0'
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
|
||||
<application
|
||||
android:name=".ime.core.FlorisApplication"
|
||||
android:allowBackup="false"
|
||||
android:extractNativeLibs="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
@@ -74,6 +75,14 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/SettingsTheme"/>
|
||||
|
||||
<!-- Advanced Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.settings.AdvancedActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/settings__advanced__title"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/SettingsTheme"/>
|
||||
|
||||
<!-- Setup Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.setup.SetupActivity"
|
||||
@@ -82,6 +91,13 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/SettingsTheme"/>
|
||||
|
||||
<!-- Crash Dialog Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.crashutility.CrashDialogActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/crash_dialog__title"
|
||||
android:theme="@style/CrashDialogTheme"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"a": [
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 226, "label": "â" },
|
||||
{ "code": 227, "label": "ã" },
|
||||
{ "code": 257, "label": "ā" },
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 230, "label": "æ" },
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 228, "label": "ä" }
|
||||
@@ -13,37 +13,37 @@
|
||||
{ "code": 240, "label": "ð" }
|
||||
],
|
||||
"e": [
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 281, "label": "ę" },
|
||||
{ "code": 279, "label": "ė" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 235, "label": "ë" },
|
||||
{ "code": 234, "label": "ê" }
|
||||
],
|
||||
"i": [
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 299, "label": "ī" },
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 303, "label": "į" },
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 239, "label": "ï" }
|
||||
],
|
||||
"l": [
|
||||
{ "code": 322, "label": "ł" }
|
||||
],
|
||||
"n": [
|
||||
{ "code": 324, "label": "ń" },
|
||||
{ "code": 241, "label": "ñ" }
|
||||
{ "code": 241, "label": "ñ" },
|
||||
{ "code": 324, "label": "ń" }
|
||||
],
|
||||
"o": [
|
||||
{ "code": 248, "label": "ø" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 339, "label": "œ" },
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 245, "label": "õ" },
|
||||
{ "code": 244, "label": "ô" },
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 248, "label": "ø" },
|
||||
{ "code": 246, "label": "ö" }
|
||||
],
|
||||
"s": [
|
||||
@@ -52,9 +52,9 @@
|
||||
{ "code": 353, "label": "š" }
|
||||
],
|
||||
"u": [
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 251, "label": "û" },
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 249, "label": "ù" }
|
||||
],
|
||||
@@ -69,6 +69,7 @@
|
||||
{ "code": 246, "label": "ö" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
@@ -83,14 +84,13 @@
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 63, "label": "?" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"a": [
|
||||
{ "code": 228, "label": "ä" },
|
||||
{ "code": 230, "label": "æ" },
|
||||
{ "code": 227, "label": "ã" },
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 257, "label": "ā" },
|
||||
{ "code": 228, "label": "ä" },
|
||||
{ "code": 226, "label": "â" },
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 225, "label": "á" }
|
||||
@@ -13,46 +13,47 @@
|
||||
{ "code": 231, "label": "ç" }
|
||||
],
|
||||
"e": [
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 234, "label": "ê" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 235, "label": "ë" }
|
||||
],
|
||||
"i": [
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 239, "label": "ï" },
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 299, "label": "ī" }
|
||||
],
|
||||
"n": [
|
||||
{ "code": 324, "label": "ń" },
|
||||
{ "code": 241, "label": "ñ" }
|
||||
{ "code": 241, "label": "ñ" },
|
||||
{ "code": 324, "label": "ń" }
|
||||
],
|
||||
"o": [
|
||||
{ "code": 246, "label": "ö" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 248, "label": "ø" },
|
||||
{ "code": 245, "label": "õ" },
|
||||
{ "code": 339, "label": "œ" },
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 246, "label": "ö" },
|
||||
{ "code": 244, "label": "ô" }
|
||||
],
|
||||
"s": [
|
||||
{ "code": 353, "label": "š" },
|
||||
{ "code": 223, "label": "ß" },
|
||||
{ "code": 353, "label": "š" },
|
||||
{ "code": 347, "label": "ś" }
|
||||
],
|
||||
"u": [
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 249, "label": "ù" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 251, "label": "û" },
|
||||
{ "code": 250, "label": "ú" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
@@ -67,14 +68,13 @@
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 63, "label": "?" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"a": [
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 230, "label": "æ" },
|
||||
{ "code": 227, "label": "ã" },
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 257, "label": "ā" },
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 226, "label": "â" },
|
||||
{ "code": 228, "label": "ä" }
|
||||
@@ -13,44 +13,45 @@
|
||||
{ "code": 231, "label": "ç" }
|
||||
],
|
||||
"e": [
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 234, "label": "ê" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 235, "label": "ë" }
|
||||
],
|
||||
"i": [
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 239, "label": "ï" },
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 299, "label": "ī" }
|
||||
],
|
||||
"n": [
|
||||
{ "code": 324, "label": "ń" },
|
||||
{ "code": 241, "label": "ñ" }
|
||||
{ "code": 241, "label": "ñ" },
|
||||
{ "code": 324, "label": "ń" }
|
||||
],
|
||||
"o": [
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 245, "label": "õ" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 339, "label": "œ" },
|
||||
{ "code": 248, "label": "ø" },
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 246, "label": "ö" },
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 244, "label": "ô" }
|
||||
],
|
||||
"s": [
|
||||
{ "code": 223, "label": "ß" }
|
||||
],
|
||||
"u": [
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 251, "label": "û" },
|
||||
{ "code": 249, "label": "ù" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
@@ -65,14 +66,13 @@
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 63, "label": "?" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,43 +1,44 @@
|
||||
{
|
||||
"a": [
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 261, "label": "ą" },
|
||||
{ "code": 230, "label": "æ" },
|
||||
{ "code": 257, "label": "ā" },
|
||||
{ "code": 170, "label": "ª" },
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 228, "label": "ä" },
|
||||
{ "code": 226, "label": "â" },
|
||||
{ "code": 227, "label": "ã" }
|
||||
],
|
||||
"c": [
|
||||
{ "code": 269, "label": "č" },
|
||||
{ "code": 231, "label": "ç" },
|
||||
{ "code": 269, "label": "č" },
|
||||
{ "code": 263, "label": "ć" }
|
||||
],
|
||||
"e": [
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 281, "label": "ę" },
|
||||
{ "code": 279, "label": "ė" },
|
||||
{ "code": 235, "label": "ë" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 234, "label": "ê" }
|
||||
],
|
||||
"i": [
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 299, "label": "ī" },
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 303, "label": "į" },
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 239, "label": "ï" }
|
||||
],
|
||||
"n": [
|
||||
{ "code": 324, "label": "ń" },
|
||||
{ "code": 241, "label": "ñ" }
|
||||
{ "code": 241, "label": "ñ" },
|
||||
{ "code": 324, "label": "ń" }
|
||||
],
|
||||
"o": [
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 186, "label": "º" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 248, "label": "ø" },
|
||||
@@ -45,20 +46,20 @@
|
||||
{ "code": 245, "label": "õ" },
|
||||
{ "code": 244, "label": "ô" },
|
||||
{ "code": 246, "label": "ö" },
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 242, "label": "ò" }
|
||||
],
|
||||
"s": [
|
||||
{ "code": 223, "label": "ß" }
|
||||
],
|
||||
"u": [
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 249, "label": "ù" },
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 251, "label": "û" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 58, "label": ":" },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 64, "label": "@" },
|
||||
@@ -73,14 +74,13 @@
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 63, "label": "?" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -34,9 +34,9 @@
|
||||
{ "code": 1610, "label": "ي" }
|
||||
],
|
||||
"ا": [
|
||||
{ "code": 1570, "label": "آ" },
|
||||
{ "code": 1649, "label": "ٱ" },
|
||||
{ "code": 1569, "label": "ء" },
|
||||
{ "code": 1570, "label": "آ" },
|
||||
{ "code": 1571, "label": "أ" },
|
||||
{ "code": 1573, "label": "إ" }
|
||||
],
|
||||
@@ -44,6 +44,7 @@
|
||||
{ "code": 1577, "label": "ة" }
|
||||
],
|
||||
"ک": [
|
||||
{ "code": 1706, "label": "ڪ"},
|
||||
{ "code": 1603, "label": "ك" }
|
||||
],
|
||||
"ز": [
|
||||
@@ -53,6 +54,7 @@
|
||||
{ "code": 1572, "label": "ؤ" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 1611, "label": "ً" },
|
||||
{ "code": 1622, "label": "ٖ" },
|
||||
{ "code": 1648, "label": "ٰ" },
|
||||
{ "code": 1619, "label": "ٓ" },
|
||||
@@ -65,15 +67,14 @@
|
||||
{ "code": 1617, "label": "ّ" },
|
||||
{ "code": 1612, "label": "ٌ" },
|
||||
{ "code": 1613, "label": "ٍ" },
|
||||
{ "code": 1611, "label": "ً" },
|
||||
{ "code": 1620, "label": "ٔ" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".ir"},
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".net" },
|
||||
{ "code": -255, "label": ".ir"}
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
{
|
||||
"a": [
|
||||
{ "code": 228, "label": "ä" },
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 226, "label": "â" },
|
||||
{ "code": 227, "label": "ã" },
|
||||
{ "code": 257, "label": "ā" },
|
||||
{ "code": 228, "label": "ä" },
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 230, "label": "æ" },
|
||||
{ "code": 224, "label": "à" }
|
||||
],
|
||||
"e": [
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 281, "label": "ę" },
|
||||
{ "code": 279, "label": "ė" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 235, "label": "ë" },
|
||||
{ "code": 234, "label": "ê" }
|
||||
],
|
||||
"i": [
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 299, "label": "ī" },
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 303, "label": "į" },
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 239, "label": "ï" }
|
||||
],
|
||||
"o": [
|
||||
{ "code": 246, "label": "ö" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 339, "label": "œ" },
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 245, "label": "õ" },
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 244, "label": "ô" },
|
||||
{ "code": 246, "label": "ö" },
|
||||
{ "code": 248, "label": "ø" }
|
||||
],
|
||||
"s": [
|
||||
@@ -42,15 +42,15 @@
|
||||
{ "code": 347, "label": "ś" }
|
||||
],
|
||||
"u": [
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 251, "label": "û" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 249, "label": "ù" }
|
||||
],
|
||||
"z": [
|
||||
{ "code": 380, "label": "ż" },
|
||||
{ "code": 382, "label": "ž" },
|
||||
{ "code": 380, "label": "ż" },
|
||||
{ "code": 378, "label": "ź" }
|
||||
],
|
||||
"ä": [
|
||||
@@ -60,6 +60,7 @@
|
||||
{ "code": 248, "label": "ø" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
@@ -74,14 +75,13 @@
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 63, "label": "?" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,42 +1,43 @@
|
||||
{
|
||||
"a": [
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 227, "label": "ã" },
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 257, "label": "ā" },
|
||||
{ "code": 170, "label": "ª" },
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 226, "label": "â" },
|
||||
{ "code": 230, "label": "æ" },
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 228, "label": "ä" }
|
||||
],
|
||||
"c": [
|
||||
{ "code": 269, "label": "č" },
|
||||
{ "code": 231, "label": "ç" },
|
||||
{ "code": 269, "label": "č" },
|
||||
{ "code": 263, "label": "ć" }
|
||||
],
|
||||
"e": [
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 281, "label": "ę" },
|
||||
{ "code": 279, "label": "ė" },
|
||||
{ "code": 234, "label": "ê" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 235, "label": "ë" }
|
||||
],
|
||||
"i": [
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 299, "label": "ī" },
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 303, "label": "į" },
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 239, "label": "ï" }
|
||||
],
|
||||
"n": [
|
||||
{ "code": 324, "label": "ń" },
|
||||
{ "code": 241, "label": "ñ" }
|
||||
{ "code": 241, "label": "ñ" },
|
||||
{ "code": 324, "label": "ń" }
|
||||
],
|
||||
"o": [
|
||||
{ "code": 244, "label": "ô" },
|
||||
{ "code": 186, "label": "º" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 245, "label": "õ" },
|
||||
@@ -44,18 +45,17 @@
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 246, "label": "ö" },
|
||||
{ "code": 244, "label": "ô" },
|
||||
{ "code": 339, "label": "œ" }
|
||||
],
|
||||
"s": [
|
||||
{ "code": 353, "label": "š" },
|
||||
{ "code": 223, "label": "ß" },
|
||||
{ "code": 353, "label": "š" },
|
||||
{ "code": 347, "label": "ś" }
|
||||
],
|
||||
"u": [
|
||||
{ "code": 249, "label": "ù" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 249, "label": "ù" },
|
||||
{ "code": 251, "label": "û" },
|
||||
{ "code": 250, "label": "ú" }
|
||||
],
|
||||
@@ -63,6 +63,7 @@
|
||||
{ "code": 255, "label": "ÿ" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
@@ -77,14 +78,13 @@
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 63, "label": "?" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"a": [
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 226, "label": "â" },
|
||||
{ "code": 227, "label": "ã" },
|
||||
{ "code": 257, "label": "ā" },
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 228, "label": "ä" },
|
||||
{ "code": 230, "label": "æ" },
|
||||
{ "code": 229, "label": "å" }
|
||||
@@ -13,39 +13,39 @@
|
||||
{ "code": 240, "label": "ð" }
|
||||
],
|
||||
"e": [
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 281, "label": "ę" },
|
||||
{ "code": 279, "label": "ė" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 235, "label": "ë" },
|
||||
{ "code": 234, "label": "ê" }
|
||||
],
|
||||
"i": [
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 299, "label": "ī" },
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 303, "label": "į" },
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 239, "label": "ï" }
|
||||
],
|
||||
"o": [
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 248, "label": "ø" },
|
||||
{ "code": 245, "label": "õ" },
|
||||
{ "code": 339, "label": "œ" },
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 244, "label": "ô" },
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 246, "label": "ö" }
|
||||
],
|
||||
"t": [
|
||||
{ "code": 254, "label": "þ" }
|
||||
],
|
||||
"u": [
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 251, "label": "û" },
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 249, "label": "ù" }
|
||||
],
|
||||
@@ -54,6 +54,7 @@
|
||||
{ "code": 255, "label": "ÿ" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
@@ -68,14 +69,13 @@
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 63, "label": "?" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,37 +1,38 @@
|
||||
{
|
||||
"a": [
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 227, "label": "ã" },
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 257, "label": "ā" },
|
||||
{ "code": 170, "label": "ª" },
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 226, "label": "â" },
|
||||
{ "code": 228, "label": "ä" },
|
||||
{ "code": 230, "label": "æ" }
|
||||
],
|
||||
"e": [
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 281, "label": "ę" },
|
||||
{ "code": 279, "label": "ė" },
|
||||
{ "code": 234, "label": "ê" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 235, "label": "ë" }
|
||||
],
|
||||
"i": [
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 299, "label": "ī" },
|
||||
{ "code": 239, "label": "ï" },
|
||||
{ "code": 303, "label": "į" },
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 237, "label": "í" }
|
||||
],
|
||||
"n": [
|
||||
{ "code": 324, "label": "ń" },
|
||||
{ "code": 241, "label": "ñ" }
|
||||
{ "code": 241, "label": "ñ" },
|
||||
{ "code": 324, "label": "ń" }
|
||||
],
|
||||
"o": [
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 186, "label": "º" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 339, "label": "œ" },
|
||||
@@ -39,17 +40,17 @@
|
||||
{ "code": 245, "label": "õ" },
|
||||
{ "code": 246, "label": "ö" },
|
||||
{ "code": 244, "label": "ô" },
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 243, "label": "ó" }
|
||||
],
|
||||
"u": [
|
||||
{ "code": 249, "label": "ù" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 251, "label": "û" },
|
||||
{ "code": 249, "label": "ù" },
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 252, "label": "ü" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
@@ -64,14 +65,13 @@
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 63, "label": "?" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"a": [
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 226, "label": "â" },
|
||||
{ "code": 227, "label": "ã" },
|
||||
{ "code": 257, "label": "ā" },
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 230, "label": "æ" },
|
||||
{ "code": 228, "label": "ä" },
|
||||
{ "code": 224, "label": "à" }
|
||||
@@ -13,28 +13,28 @@
|
||||
{ "code": 231, "label": "ç" }
|
||||
],
|
||||
"e": [
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 281, "label": "ę" },
|
||||
{ "code": 279, "label": "ė" },
|
||||
{ "code": 234, "label": "ê" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 235, "label": "ë" }
|
||||
],
|
||||
"o": [
|
||||
{ "code": 248, "label": "ø" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 339, "label": "œ" },
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 245, "label": "õ" },
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 244, "label": "ô" },
|
||||
{ "code": 248, "label": "ø" },
|
||||
{ "code": 246, "label": "ö" }
|
||||
],
|
||||
"u": [
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 249, "label": "ù" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 251, "label": "û" },
|
||||
{ "code": 250, "label": "ú" }
|
||||
],
|
||||
@@ -45,6 +45,7 @@
|
||||
{ "code": 246, "label": "ö" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
@@ -59,14 +60,13 @@
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 63, "label": "?" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"a": [
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 226, "label": "â" },
|
||||
{ "code": 227, "label": "ã" },
|
||||
{ "code": 257, "label": "ā" },
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 230, "label": "æ" },
|
||||
{ "code": 228, "label": "ä" },
|
||||
{ "code": 224, "label": "à" }
|
||||
@@ -13,11 +13,11 @@
|
||||
{ "code": 231, "label": "ç" }
|
||||
],
|
||||
"e": [
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 281, "label": "ę" },
|
||||
{ "code": 279, "label": "ė" },
|
||||
{ "code": 234, "label": "ê" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 235, "label": "ë" }
|
||||
],
|
||||
@@ -25,19 +25,19 @@
|
||||
{ "code": 236, "label": "ì" }
|
||||
],
|
||||
"o": [
|
||||
{ "code": 248, "label": "ø" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 339, "label": "œ" },
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 245, "label": "õ" },
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 244, "label": "ô" },
|
||||
{ "code": 248, "label": "ø" },
|
||||
{ "code": 246, "label": "ö" }
|
||||
],
|
||||
"u": [
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 249, "label": "ù" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 251, "label": "û" },
|
||||
{ "code": 250, "label": "ú" }
|
||||
],
|
||||
@@ -51,6 +51,7 @@
|
||||
{ "code": 246, "label": "ö" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
@@ -65,14 +66,13 @@
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 63, "label": "?" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,41 +1,42 @@
|
||||
{
|
||||
"a": [
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 228, "label": "ä" },
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 230, "label": "æ" },
|
||||
{ "code": 170, "label": "ª" },
|
||||
{ "code": 225, "label": "á" },
|
||||
{ "code": 227, "label": "ã" },
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 226, "label": "â" }
|
||||
],
|
||||
"c": [
|
||||
{ "code": 263, "label": "ć" },
|
||||
{ "code": 231, "label": "ç" },
|
||||
{ "code": 263, "label": "ć" },
|
||||
{ "code": 269, "label": "č" }
|
||||
],
|
||||
"e": [
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 235, "label": "ë" },
|
||||
{ "code": 279, "label": "ė" },
|
||||
{ "code": 275, "label": "ē" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 234, "label": "ê" },
|
||||
{ "code": 281, "label": "ę" }
|
||||
],
|
||||
"i": [
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 299, "label": "ī" },
|
||||
{ "code": 239, "label": "ï" },
|
||||
{ "code": 303, "label": "į" },
|
||||
{ "code": 236, "label": "ì" },
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 238, "label": "î" }
|
||||
],
|
||||
"n": [
|
||||
{ "code": 324, "label": "ń" },
|
||||
{ "code": 241, "label": "ñ" }
|
||||
{ "code": 241, "label": "ñ" },
|
||||
{ "code": 324, "label": "ń" }
|
||||
],
|
||||
"o": [
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 186, "label": "º" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 248, "label": "ø" },
|
||||
@@ -43,17 +44,17 @@
|
||||
{ "code": 246, "label": "ö" },
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 244, "label": "ô" },
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 245, "label": "õ" }
|
||||
],
|
||||
"u": [
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 249, "label": "ù" },
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 251, "label": "û" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
@@ -68,14 +69,13 @@
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 63, "label": "?" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"a": [
|
||||
{ "code": 228, "label": "ä" },
|
||||
{ "code": 224, "label": "à" },
|
||||
{ "code": 226, "label": "â" },
|
||||
{ "code": 261, "label": "ą" },
|
||||
{ "code": 227, "label": "ã" },
|
||||
{ "code": 228, "label": "ä" },
|
||||
{ "code": 229, "label": "å" },
|
||||
{ "code": 230, "label": "æ" },
|
||||
{ "code": 225, "label": "á" }
|
||||
],
|
||||
"c": [
|
||||
{ "code": 269, "label": "č" },
|
||||
{ "code": 231, "label": "ç" },
|
||||
{ "code": 269, "label": "č" },
|
||||
{ "code": 263, "label": "ć" }
|
||||
],
|
||||
"d": [
|
||||
@@ -19,36 +19,36 @@
|
||||
{ "code": 271, "label": "ď" }
|
||||
],
|
||||
"e": [
|
||||
{ "code": 234, "label": "ê" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 234, "label": "ê" },
|
||||
{ "code": 232, "label": "è" },
|
||||
{ "code": 235, "label": "ë" },
|
||||
{ "code": 281, "label": "ę" }
|
||||
],
|
||||
"i": [
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 239, "label": "ï" },
|
||||
{ "code": 299, "label": "ī" },
|
||||
{ "code": 303, "label": "į" },
|
||||
{ "code": 238, "label": "î" },
|
||||
{ "code": 237, "label": "í" },
|
||||
{ "code": 236, "label": "ì" }
|
||||
],
|
||||
"l": [
|
||||
{ "code": 322, "label": "ł" }
|
||||
],
|
||||
"n": [
|
||||
{ "code": 328, "label": "ň" },
|
||||
{ "code": 324, "label": "ń" },
|
||||
{ "code": 328, "label": "ň" },
|
||||
{ "code": 241, "label": "ñ" }
|
||||
],
|
||||
"o": [
|
||||
{ "code": 246, "label": "ö" },
|
||||
{ "code": 333, "label": "ō" },
|
||||
{ "code": 245, "label": "õ" },
|
||||
{ "code": 242, "label": "ò" },
|
||||
{ "code": 244, "label": "ô" },
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 339, "label": "œ" },
|
||||
{ "code": 246, "label": "ö" },
|
||||
{ "code": 248, "label": "ø" }
|
||||
],
|
||||
"r": [
|
||||
@@ -65,9 +65,9 @@
|
||||
{ "code": 254, "label": "þ" }
|
||||
],
|
||||
"u": [
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 363, "label": "ū" },
|
||||
{ "code": 249, "label": "ù" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 251, "label": "û" }
|
||||
],
|
||||
@@ -76,18 +76,19 @@
|
||||
{ "code": 255, "label": "ÿ" }
|
||||
],
|
||||
"z": [
|
||||
{ "code": 380, "label": "ż" },
|
||||
{ "code": 378, "label": "ź" },
|
||||
{ "code": 380, "label": "ż" },
|
||||
{ "code": 382, "label": "ž" }
|
||||
],
|
||||
"ä": [
|
||||
{ "code": 230, "label": "æ" }
|
||||
],
|
||||
"ö": [
|
||||
{ "code": 339, "label": "œ" },
|
||||
{ "code": 248, "label": "ø" }
|
||||
{ "code": 248, "label": "ø" },
|
||||
{ "code": 339, "label": "œ" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
@@ -102,14 +103,13 @@
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 63, "label": "?" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
] },
|
||||
{ "code": 32, "label": " " },
|
||||
{ "code": 8204, "label": "half_space", "variation": "normal" },
|
||||
{ "code": 1600, "label": "kashida", "variation": "normal" },
|
||||
{ "code": 46, "label": ".", "variation": "email_address" },
|
||||
{ "code": 46, "label": ".", "variation": "normal" },
|
||||
{ "code": 46, "label": ".", "variation": "uri" },
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "extension",
|
||||
"name": "clipboard_cursor_row",
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": -135, "label": "clipboard_select_all", "type": "enter_editing" },
|
||||
{ "code": -130, "label": "clipboard_copy", "type": "enter_editing" },
|
||||
{ "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" }
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -12,7 +12,9 @@
|
||||
{ "code": 44, "label": ",", "popup": [] },
|
||||
{ "code": -205, "label": "view_numeric_advanced", "type": "system_gui" },
|
||||
{ "code": 32, "label": " ", "popup": [] },
|
||||
{ "code": 46, "label": ".", "popup": [] },
|
||||
{ "code": 46, "label": ".", "popup": [
|
||||
{ "code": 8230, "label": "…" }
|
||||
] },
|
||||
{ "code": 10, "label": "enter", "type": "enter_editing" }
|
||||
]
|
||||
]
|
||||
|
||||
60
app/src/main/assets/ime/theme/floris_day.json
Normal file
60
app/src/main/assets/ime/theme/floris_day.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "floris_day",
|
||||
"displayName": "Floris Day",
|
||||
"author": "patrickgold",
|
||||
"isNightTheme": false,
|
||||
"attributes": {
|
||||
"window": {
|
||||
"colorPrimary": "#4CAF50",
|
||||
"colorPrimaryDark": "#388E3C",
|
||||
"colorAccent": "#FF9800",
|
||||
"navigationBarColor": "@keyboard/bgColor",
|
||||
"navigationBarLight": "true",
|
||||
"semiTransparentColor": "#20000000",
|
||||
"textColor": "#000000"
|
||||
},
|
||||
"keyboard": {
|
||||
"bgColor": "#E0E0E0"
|
||||
},
|
||||
"key": {
|
||||
"bgColor": "#FFFFFF",
|
||||
"bgColorPressed": "#F5F5F5",
|
||||
"fgColor": "@window/textColor"
|
||||
},
|
||||
"keyEnter": {
|
||||
"bgColor": "@window/colorPrimary",
|
||||
"bgColorPressed": "@window/colorPrimaryDark",
|
||||
"fgColor": "#FFFFFF"
|
||||
},
|
||||
"keyPopup": {
|
||||
"bgColor": "#EEEEEE",
|
||||
"bgColorActive": "#BDBDBD",
|
||||
"fgColor": "@window/textColor"
|
||||
},
|
||||
"keyShift": {
|
||||
"bgColor": "@key/bgColor",
|
||||
"bgColorPressed": "@key/bgColorPressed",
|
||||
"fgColor": "@window/textColor",
|
||||
"fgColorCapsLock": "@window/colorAccent"
|
||||
},
|
||||
"media": {
|
||||
"fgColor": "@window/textColor",
|
||||
"fgColorAlt": "#757575"
|
||||
},
|
||||
"oneHanded": {
|
||||
"bgColor": "#E8F5E9"
|
||||
},
|
||||
"oneHandedButton": {
|
||||
"fgColor": "#424242"
|
||||
},
|
||||
"smartbar": {
|
||||
"bgColor": "transparent",
|
||||
"fgColor": "@window/textColor",
|
||||
"fgColorAlt": "#8A8A8A"
|
||||
},
|
||||
"smartbarButton": {
|
||||
"bgColor": "@key/bgColor",
|
||||
"fgColor": "@key/fgColor"
|
||||
}
|
||||
}
|
||||
}
|
||||
60
app/src/main/assets/ime/theme/floris_night.json
Normal file
60
app/src/main/assets/ime/theme/floris_night.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "floris_night",
|
||||
"displayName": "Floris Night",
|
||||
"author": "patrickgold",
|
||||
"isNightTheme": true,
|
||||
"attributes": {
|
||||
"window": {
|
||||
"colorPrimary": "#4CAF50",
|
||||
"colorPrimaryDark": "#388E3C",
|
||||
"colorAccent": "#FF9800",
|
||||
"navigationBarColor": "@keyboard/bgColor",
|
||||
"navigationBarLight": "false",
|
||||
"semiTransparentColor": "#20FFFFFF",
|
||||
"textColor": "#FFFFFF"
|
||||
},
|
||||
"keyboard": {
|
||||
"bgColor": "#212121"
|
||||
},
|
||||
"key": {
|
||||
"bgColor": "#424242",
|
||||
"bgColorPressed": "#616161",
|
||||
"fgColor": "@window/textColor"
|
||||
},
|
||||
"keyEnter": {
|
||||
"bgColor": "@window/colorPrimary",
|
||||
"bgColorPressed": "@window/colorPrimaryDark",
|
||||
"fgColor": "#FFFFFF"
|
||||
},
|
||||
"keyPopup": {
|
||||
"bgColor": "#757575",
|
||||
"bgColorActive": "#BDBDBD",
|
||||
"fgColor": "@window/textColor"
|
||||
},
|
||||
"keyShift": {
|
||||
"bgColor": "@key/bgColor",
|
||||
"bgColorPressed": "@key/bgColorPressed",
|
||||
"fgColor": "@window/textColor",
|
||||
"fgColorCapsLock": "@window/colorAccent"
|
||||
},
|
||||
"media": {
|
||||
"fgColor": "@window/textColor",
|
||||
"fgColorAlt": "#BDBDBD"
|
||||
},
|
||||
"oneHanded": {
|
||||
"bgColor": "#1B5E20"
|
||||
},
|
||||
"oneHandedButton": {
|
||||
"fgColor": "#EEEEEE"
|
||||
},
|
||||
"smartbar": {
|
||||
"bgColor": "transparent",
|
||||
"fgColor": "@window/textColor",
|
||||
"fgColorAlt": "#73FFFFFF"
|
||||
},
|
||||
"smartbarButton": {
|
||||
"bgColor": "@key/bgColor",
|
||||
"fgColor": "@key/fgColor"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,24 @@ 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.
|
||||
</pre>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>ColorPicker preference</h3>
|
||||
<span>Copyright 2016 Jared Rummler / Copyright 2015 Daniel Nilsson</span>
|
||||
<pre>
|
||||
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.
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.crashutility
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.databinding.CrashDialogBinding
|
||||
|
||||
class CrashDialogActivity : AppCompatActivity() {
|
||||
private lateinit var binding: CrashDialogBinding
|
||||
private var stacktrace: String = ""
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = CrashDialogBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
stacktrace = CrashUtility.getUnhandledStacktrace(this)
|
||||
binding.stacktrace.text = stacktrace
|
||||
|
||||
binding.copyToClipboard.setOnClickListener {
|
||||
val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE)
|
||||
if (clipboardManager != null && clipboardManager is ClipboardManager) {
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText(stacktrace, stacktrace))
|
||||
}
|
||||
}
|
||||
|
||||
binding.openBugReportForm.setOnClickListener {
|
||||
val browserIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(resources.getString(R.string.florisboard__issue_tracker_new_issue_url))
|
||||
)
|
||||
startActivity(browserIntent)
|
||||
}
|
||||
|
||||
binding.close.setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.crashutility
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.*
|
||||
import android.app.Application.ActivityLifecycleCallbacks
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Process
|
||||
import android.util.Log
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import java.io.File
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
* Abstract class which holds several static methods used for handling unexpected errors.
|
||||
*
|
||||
* Parts of this class (especially the install() function and the uncaughtException() handler) have
|
||||
* been inspired by the great CustomActivityOnCrash library:
|
||||
* https://github.com/Ereza/CustomActivityOnCrash (licensed under Apache 2.0)
|
||||
* https://github.com/Ereza/CustomActivityOnCrash/blob/master/library/src/main/java/cat/ereza/customactivityoncrash/CustomActivityOnCrash.java
|
||||
*/
|
||||
abstract class CrashUtility private constructor() {
|
||||
companion object {
|
||||
private const val SHARED_PREFS_FILE = "crash_utility"
|
||||
private const val SHARED_PREFS_LAST_CRASH_TIMESTAMP = "last_crash_timestamp"
|
||||
|
||||
private const val NOTIFICATION_CHANNEL_ID = "dev.patrickgold.florisboard.crashutility"
|
||||
private const val NOTIFICATION_ID = 0xFBAD0100
|
||||
|
||||
private const val UNHANDLED_STACKTRACE_FILE_EXT = "stacktrace"
|
||||
private const val TAG = "CrashUtility"
|
||||
|
||||
private var lastActivityCreated: WeakReference<Activity?> = WeakReference(null)
|
||||
|
||||
/**
|
||||
* Installs the CrashUtility crash handler for the given package [context]. Also registers
|
||||
* a notification channel for devices with Android 8.0+.
|
||||
*
|
||||
* @param context The current package context. If null is supplied, this function does
|
||||
* nothing.
|
||||
* @return True if the installation was successful, false otherwise.
|
||||
*/
|
||||
fun install(context: Context?): Boolean {
|
||||
if (context == null) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"install($context): Can't install crash handler with a null Context object, doing nothing!"
|
||||
)
|
||||
return false
|
||||
}
|
||||
val oldHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
if (oldHandler is UncaughtExceptionHandler) {
|
||||
Log.i(TAG, "install($context): Crash handler is already installed, doing nothing!")
|
||||
} else {
|
||||
val application = context.applicationContext
|
||||
if (application != null && application is Application) {
|
||||
try {
|
||||
Thread.setDefaultUncaughtExceptionHandler(
|
||||
UncaughtExceptionHandler(
|
||||
WeakReference(application),
|
||||
WeakReference(oldHandler),
|
||||
application.filesDir.absolutePath
|
||||
)
|
||||
)
|
||||
Log.i(
|
||||
TAG,
|
||||
"install($context): Successfully installed crash handler for this application!"
|
||||
)
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"install($context): Failed to install crash handler, probably due to missing runtime permission 'setDefaultUncaughtExceptionHandler':\n$e"
|
||||
)
|
||||
return false
|
||||
} catch (e: Exception) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"install($context): Failed to install crash handler due to an unspecified error:\n$e"
|
||||
)
|
||||
return false
|
||||
}
|
||||
application.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
|
||||
override fun onActivityCreated(
|
||||
activity: Activity,
|
||||
savedInstanceState: Bundle?
|
||||
) {
|
||||
if (activity !is CrashDialogActivity) {
|
||||
lastActivityCreated = WeakReference(activity)
|
||||
}
|
||||
}
|
||||
override fun onActivityStarted(activity: Activity) {}
|
||||
override fun onActivityResumed(activity: Activity) {}
|
||||
override fun onActivityPaused(activity: Activity) {}
|
||||
override fun onActivityStopped(activity: Activity) {}
|
||||
override fun onActivitySaveInstanceState(
|
||||
activity: Activity,
|
||||
outState: Bundle
|
||||
) {}
|
||||
override fun onActivityDestroyed(activity: Activity) {}
|
||||
})
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
try {
|
||||
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE)
|
||||
if (notificationManager != null && notificationManager is NotificationManager) {
|
||||
val notificationChannel = NotificationChannel(
|
||||
NOTIFICATION_CHANNEL_ID,
|
||||
context.resources.getString(R.string.crash_notification_channel__title),
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
)
|
||||
notificationManager.createNotificationChannel(notificationChannel)
|
||||
}
|
||||
Log.i(
|
||||
TAG,
|
||||
"install($context): Successfully created crash handler notification channel!"
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"install($context): Failed to create crash handler notification channel due to an unspecified error:\n$e"
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.e(
|
||||
TAG,
|
||||
"install($context): Can't install crash handler with a null Application object, doing nothing!"
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and returns all unhandled stacktrace files.
|
||||
*
|
||||
* @param context The current package context. If null is supplied, this function returns
|
||||
* an empty string.
|
||||
* @return All unhandled stacktrace files or an empty string.
|
||||
*/
|
||||
fun getUnhandledStacktrace(context: Context?): String {
|
||||
context ?: return ""
|
||||
val retString: StringBuilder = StringBuilder()
|
||||
val ustDir = getUstDir(context)
|
||||
if (ustDir.isDirectory) {
|
||||
(ustDir.listFiles { pathname ->
|
||||
pathname.name.endsWith(".$UNHANDLED_STACKTRACE_FILE_EXT")
|
||||
})?.forEach { file ->
|
||||
val newLine = System.lineSeparator()
|
||||
Log.i(TAG, "Reading unhandled stacktrace: ${file.name}")
|
||||
retString.append("~~~ ${file.name} ~~~$newLine$newLine")
|
||||
retString.append(readFile(file))
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
return retString.toString()
|
||||
}
|
||||
|
||||
fun hasUnhandledStacktraceFiles(context: Context): Boolean {
|
||||
val ustDir = getUstDir(context)
|
||||
return if (ustDir.isDirectory) {
|
||||
(ustDir.listFiles { pathname ->
|
||||
pathname.name.endsWith(".$UNHANDLED_STACKTRACE_FILE_EXT")
|
||||
})?.isNotEmpty() ?: false
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last crash timestamp from the shared preferences.
|
||||
*
|
||||
* @param context The current package context. If null is supplied, this function returns
|
||||
* the default value for the timestamp (0).
|
||||
* @return The last time crash timestamp or 0.
|
||||
*/
|
||||
private fun getLastCrashTimestamp(context: Context?): Long {
|
||||
context ?: return 0
|
||||
return context.getSharedPreferences(SHARED_PREFS_FILE, Context.MODE_PRIVATE)
|
||||
.getLong(SHARED_PREFS_LAST_CRASH_TIMESTAMP, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the last crash timestamp in the shared preferences.
|
||||
*
|
||||
* @param context The current package context. If null is supplied, this function does
|
||||
* nothing.
|
||||
* @param value The timestamp of the current crash.
|
||||
*/
|
||||
@SuppressLint("ApplySharedPref")
|
||||
private fun setLastCrashTimestamp(context: Context?, value: Long) {
|
||||
context ?: return
|
||||
// Note: must use commit() instead of apply(), as the value must be immediately written
|
||||
// to be possibly instantly read again.
|
||||
context.getSharedPreferences(SHARED_PREFS_FILE, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putLong(SHARED_PREFS_LAST_CRASH_TIMESTAMP, value)
|
||||
.commit()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a reference to the current unhandled stacktrace directory.
|
||||
*
|
||||
* @param context The current package context.
|
||||
* @return The File object for the directory.
|
||||
*/
|
||||
private fun getUstDir(context: Context): File {
|
||||
val path = context.filesDir.absolutePath
|
||||
return File(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a reference to the stacktrace file for given [timestamp].
|
||||
*
|
||||
* @param context The current package context.
|
||||
* @param timestamp The timestamp of the stacktrace file to get.
|
||||
* @return The File object for the stacktrace file.
|
||||
*/
|
||||
private fun getUstFile(context: Context, timestamp: Long): File {
|
||||
val path = context.filesDir.absolutePath
|
||||
return File("$path/$timestamp.$UNHANDLED_STACKTRACE_FILE_EXT")
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a notification which opens [CrashDialogActivity] with given parameters.
|
||||
*
|
||||
* @param context The current package context. If null is supplied, this function does
|
||||
* nothing.
|
||||
* @param id The ID of the notification.
|
||||
* @param title The title of the notification.
|
||||
* @param body The body of the notification.
|
||||
*/
|
||||
private fun pushNotification(context: Context?, id: Int, title: String, body: String) {
|
||||
context ?: return
|
||||
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE)
|
||||
if (notificationManager != null && notificationManager is NotificationManager) {
|
||||
val notificationBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Notification.Builder(context.applicationContext, NOTIFICATION_CHANNEL_ID)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
Notification.Builder(context.applicationContext).apply {
|
||||
setPriority(Notification.PRIORITY_MAX)
|
||||
}
|
||||
}
|
||||
val crashDialogIntent = Intent(context, CrashDialogActivity::class.java)
|
||||
val notification = notificationBuilder.run {
|
||||
setContentTitle(title)
|
||||
style = Notification.BigTextStyle().bigText(body)
|
||||
setContentText(body)
|
||||
setSmallIcon(android.R.drawable.stat_notify_error)
|
||||
setContentIntent(PendingIntent.getActivity(context, 0, crashDialogIntent, 0)).setAutoCancel(
|
||||
true
|
||||
)
|
||||
build()
|
||||
}
|
||||
notificationManager.notify(id, notification)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a notification configured for a single crash.
|
||||
*
|
||||
* @param context The current package context. If null is supplied, this function does
|
||||
* nothing.
|
||||
*/
|
||||
private fun pushCrashOnceNotification(context: Context?) {
|
||||
context ?: return
|
||||
pushNotification(
|
||||
context,
|
||||
NOTIFICATION_ID.toInt(),
|
||||
context.resources.getString(R.string.crash_once_notification__title),
|
||||
context.resources.getString(R.string.crash_once_notification__body)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a notification configured for multiple crashes.
|
||||
*
|
||||
* @param context The current package context. If null is supplied, this function does
|
||||
* nothing.
|
||||
*/
|
||||
private fun pushCrashMultipleNotification(context: Context?) {
|
||||
context ?: return
|
||||
pushNotification(
|
||||
context,
|
||||
NOTIFICATION_ID.toInt(),
|
||||
context.resources.getString(R.string.crash_multiple_notification__title),
|
||||
context.resources.getString(R.string.crash_multiple_notification__body)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a given [file] and returns its content.
|
||||
*
|
||||
* @param file The file object.
|
||||
* @return The contents of the file or an empty string, if the file does not exist.
|
||||
*/
|
||||
private fun readFile(file: File): String {
|
||||
val retText = StringBuilder()
|
||||
if (file.exists()) {
|
||||
val newLine = System.lineSeparator()
|
||||
file.forEachLine {
|
||||
retText.append(it)
|
||||
retText.append(newLine)
|
||||
}
|
||||
}
|
||||
return retText.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes given [text] to given [file]. If the file already exists, its current content
|
||||
* will be overwritten.
|
||||
*
|
||||
* @param file The file object.
|
||||
* @param text The text to write to the file.
|
||||
* @return The contents of the file or an empty string, if the file does not exist.
|
||||
*/
|
||||
private fun writeToFile(file: File, text: String) {
|
||||
try {
|
||||
file.writeText(text)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom UncaughtExceptionHandler, which writes the captured stacktrace of the crash to the
|
||||
* internal storage, pushes a crash notification and kills the current process.
|
||||
*/
|
||||
class UncaughtExceptionHandler(
|
||||
private val application: WeakReference<Application>,
|
||||
private val oldHandler: WeakReference<Thread.UncaughtExceptionHandler?>,
|
||||
private val path: String
|
||||
) : Thread.UncaughtExceptionHandler {
|
||||
override fun uncaughtException(thread: Thread?, throwable: Throwable?) {
|
||||
Log.e(TAG, "Detected application crash, executing custom crash handler.")
|
||||
thread ?: return
|
||||
throwable ?: return
|
||||
val timestamp = System.currentTimeMillis()
|
||||
val stacktrace = Log.getStackTraceString(throwable)
|
||||
val ustFile = File("$path/$timestamp.$UNHANDLED_STACKTRACE_FILE_EXT")
|
||||
writeToFile(ustFile, stacktrace)
|
||||
val application = application.get()
|
||||
if (application != null) {
|
||||
val lastTimestamp = getLastCrashTimestamp(application)
|
||||
if (lastTimestamp > 0) {
|
||||
val lastFile = getUstFile(application, lastTimestamp)
|
||||
val lastStacktrace = readFile(lastFile)
|
||||
if (lastStacktrace == stacktrace) {
|
||||
// Delete last stacktrace if it matches previous unhandled one
|
||||
lastFile.delete()
|
||||
}
|
||||
}
|
||||
setLastCrashTimestamp(application, timestamp)
|
||||
if (timestamp - lastTimestamp < 5000) {
|
||||
pushCrashMultipleNotification(application)
|
||||
val florisboard = FlorisBoard.getInstanceOrNull()
|
||||
if (florisboard != null) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
florisboard.switchToPreviousInputMethod()
|
||||
} else {
|
||||
val imm = application.getSystemService(Context.INPUT_METHOD_SERVICE)
|
||||
if (imm != null && imm is InputMethodManager) {
|
||||
@Suppress("DEPRECATION")
|
||||
imm.switchToNextInputMethod(
|
||||
florisboard.window?.window?.attributes?.token,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pushCrashOnceNotification(application)
|
||||
}
|
||||
}
|
||||
val lastActivity = lastActivityCreated.get()
|
||||
if (lastActivity != null) {
|
||||
//oldHandler.get()?.uncaughtException(thread, throwable)
|
||||
lastActivity.finish()
|
||||
lastActivityCreated.clear()
|
||||
}
|
||||
Process.killProcess(Process.myPid())
|
||||
exitProcess(10)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,876 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.core
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipDescription
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.inputmethodservice.InputMethodService
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.text.InputType
|
||||
import android.view.KeyEvent
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.ExtractedTextRequest
|
||||
import android.view.inputmethod.InputContentInfo
|
||||
import androidx.annotation.RequiresApi
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
|
||||
// Constants for detectLastUnicodeCharacterLengthBeforeCursor method
|
||||
private const val LIGHT_SKIN_TONE = 0x1F3FB
|
||||
private const val MEDIUM_LIGHT_SKIN_TONE = 0x1F3FC
|
||||
private const val MEDIUM_SKIN_TONE = 0x1F3FD
|
||||
private const val MEDIUM_DARK_SKIN_TONE = 0x1F3FE
|
||||
private const val DARK_SKIN_TONE = 0x1F3FF
|
||||
private const val RED_HAIR = 0x1F9B0
|
||||
private const val CURLY_HAIR = 0x1F9B1
|
||||
private const val WHITE_HAIR = 0x1F9B2
|
||||
private const val BALD = 0x1F9B3
|
||||
private const val ZERO_WIDTH_JOINER = 0x200D
|
||||
private const val VARIATION_SELECTOR = 0xFE0F
|
||||
|
||||
// Array which holds all variations for easier checking (convenience only)
|
||||
private val emojiVariationArray: Array<Int> = arrayOf(
|
||||
LIGHT_SKIN_TONE,
|
||||
MEDIUM_LIGHT_SKIN_TONE,
|
||||
MEDIUM_SKIN_TONE,
|
||||
MEDIUM_DARK_SKIN_TONE,
|
||||
DARK_SKIN_TONE,
|
||||
RED_HAIR,
|
||||
CURLY_HAIR,
|
||||
WHITE_HAIR,
|
||||
BALD
|
||||
)
|
||||
|
||||
/**
|
||||
* Class which holds information relevant to an editor instance like the input [cachedText], [selection],
|
||||
* [inputAttributes], [imeOptions], etc. This class is thought to be an improved [EditorInfo]
|
||||
* object which also holds the state of the currently focused input editor.
|
||||
*/
|
||||
class EditorInstance private constructor(private val ims: InputMethodService?) {
|
||||
var contentMimeTypes: Array<out String?>? = null
|
||||
val cursorCapsMode: InputAttributes.CapsMode
|
||||
get() {
|
||||
val ic = ims?.currentInputConnection ?: return InputAttributes.CapsMode.NONE
|
||||
return InputAttributes.CapsMode.fromFlags(
|
||||
ic.getCursorCapsMode(inputAttributes.capsMode.toFlags())
|
||||
)
|
||||
}
|
||||
var currentWord: Region = Region(this)
|
||||
private set
|
||||
var imeOptions: ImeOptions = ImeOptions.fromImeOptionsInt(EditorInfo.IME_NULL)
|
||||
private set
|
||||
var inputAttributes: InputAttributes = InputAttributes.fromInputTypeInt(InputType.TYPE_NULL)
|
||||
private set
|
||||
var isComposingEnabled: Boolean = false
|
||||
set(v) {
|
||||
field = v
|
||||
reevaluateCurrentWord()
|
||||
if (v && !isRawInputEditor) {
|
||||
markComposingRegion(currentWord)
|
||||
} else {
|
||||
markComposingRegion(null)
|
||||
}
|
||||
}
|
||||
var isNewSelectionInBoundsOfOld: Boolean = false
|
||||
private set
|
||||
var isRawInputEditor: Boolean = true
|
||||
private set
|
||||
var packageName: String = "undefined"
|
||||
private set
|
||||
var selection: Selection = Selection(this)
|
||||
private set
|
||||
var cachedText: String = ""
|
||||
|
||||
private var clipboardManager: ClipboardManager? = null
|
||||
|
||||
init {
|
||||
val tmpClipboardManager = ims?.getSystemService(Context.CLIPBOARD_SERVICE)
|
||||
if (tmpClipboardManager != null && tmpClipboardManager is ClipboardManager) {
|
||||
clipboardManager = tmpClipboardManager
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun default(): EditorInstance {
|
||||
return EditorInstance(null)
|
||||
}
|
||||
|
||||
fun from(editorInfo: EditorInfo?, ims: InputMethodService?): EditorInstance {
|
||||
return if (editorInfo == null) { default() } else {
|
||||
EditorInstance(ims).apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
contentMimeTypes = editorInfo.contentMimeTypes
|
||||
}
|
||||
imeOptions = ImeOptions.fromImeOptionsInt(editorInfo.imeOptions)
|
||||
inputAttributes = InputAttributes.fromInputTypeInt(editorInfo.inputType)
|
||||
packageName = editorInfo.packageName
|
||||
/*selection = Selection(this).apply {
|
||||
start = editorInfo.initialSelStart
|
||||
end = editorInfo.initialSelEnd
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
updateEditorState()
|
||||
reevaluateCurrentWord()
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler which reacts to selection updates coming from the target app's editor.
|
||||
*/
|
||||
fun onUpdateSelection(
|
||||
oldSelStart: Int, oldSelEnd: Int,
|
||||
newSelStart: Int, newSelEnd: Int
|
||||
) {
|
||||
updateEditorState()
|
||||
isNewSelectionInBoundsOfOld =
|
||||
newSelStart >= (oldSelStart - 1) &&
|
||||
newSelStart <= (oldSelStart + 1) &&
|
||||
newSelEnd >= (oldSelEnd - 1) &&
|
||||
newSelEnd <= (oldSelEnd + 1)
|
||||
selection.apply {
|
||||
start = newSelStart
|
||||
end = newSelEnd
|
||||
}
|
||||
reevaluateCurrentWord()
|
||||
if (selection.isCursorMode && isComposingEnabled && !isRawInputEditor) {
|
||||
markComposingRegion(currentWord)
|
||||
} else {
|
||||
markComposingRegion(null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes the given [text] in the current composing region. Does nothing if the current
|
||||
* composing region is of zero length or null.
|
||||
*
|
||||
* @param text The text to complete in this editor's composing region.
|
||||
*
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun commitCompletion(text: String): Boolean {
|
||||
val ic = ims?.currentInputConnection ?: return false
|
||||
return if (isRawInputEditor) {
|
||||
false
|
||||
} else {
|
||||
ic.beginBatchEdit()
|
||||
ic.setComposingText(text, 1)
|
||||
markComposingRegion(null)
|
||||
updateEditorState()
|
||||
reevaluateCurrentWord()
|
||||
ic.endBatchEdit()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits the given [content] to this editor instance and adjusts both the cursor position and
|
||||
* composing region, if any.
|
||||
*
|
||||
* @param content The content to commit.
|
||||
*
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun commitContent(content: Uri, description: ClipDescription): Boolean {
|
||||
val ic = ims?.currentInputConnection ?: return false
|
||||
val contentMimeTypes = contentMimeTypes
|
||||
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1 || contentMimeTypes == null || contentMimeTypes.isEmpty()) {
|
||||
commitText(content.toString())
|
||||
} else {
|
||||
var mimeTypesDoMatch = false
|
||||
for (contentMimeType in contentMimeTypes) {
|
||||
if (description.hasMimeType(contentMimeType)) {
|
||||
mimeTypesDoMatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (mimeTypesDoMatch) {
|
||||
ic.beginBatchEdit()
|
||||
markComposingRegion(null)
|
||||
val ret = ic.commitContent(InputContentInfo(content, description), 0, null)
|
||||
ic.endBatchEdit()
|
||||
ret
|
||||
} else {
|
||||
commitText(content.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits the given [text] to this editor instance and adjusts both the cursor position and
|
||||
* composing region, if any.
|
||||
*
|
||||
* This method overwrites any selected text and replaces it with given [text]. If there is no
|
||||
* text selected (selection is in cursor mode), then this method will insert the [text] after
|
||||
* the cursor, then set the cursor position to the first character after the inserted text.
|
||||
*
|
||||
* @param text The text to commit.
|
||||
*
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun commitText(text: String): Boolean {
|
||||
val ic = ims?.currentInputConnection ?: return false
|
||||
return if (isRawInputEditor) {
|
||||
ic.commitText(text, 1)
|
||||
} else {
|
||||
ic.beginBatchEdit()
|
||||
markComposingRegion(null)
|
||||
ic.commitText(text, 1)
|
||||
updateEditorState()
|
||||
reevaluateCurrentWord()
|
||||
if (isComposingEnabled) {
|
||||
markComposingRegion(currentWord)
|
||||
}
|
||||
ic.endBatchEdit()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a backward delete on this editor's text. If a text selection is active, all
|
||||
* characters inside this selection will be removed, else only the left-most character from
|
||||
* the cursor's position.
|
||||
*
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun deleteBackwards(): Boolean {
|
||||
val ic = ims?.currentInputConnection ?: return false
|
||||
if (isRawInputEditor) {
|
||||
return sendSystemKeyEvent(KeyEvent.KEYCODE_DEL)
|
||||
} else {
|
||||
ic.beginBatchEdit()
|
||||
markComposingRegion(null)
|
||||
if (selection.isCursorMode && selection.start > 0) {
|
||||
val length = detectLastUnicodeCharacterLengthBeforeCursor()
|
||||
ic.deleteSurroundingText(length, 0)
|
||||
} else if (selection.isSelectionMode) {
|
||||
ic.commitText("", 1)
|
||||
}
|
||||
updateEditorState()
|
||||
reevaluateCurrentWord()
|
||||
if (isComposingEnabled) {
|
||||
markComposingRegion(currentWord)
|
||||
}
|
||||
ic.endBatchEdit()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes [n] words before the current cursor's position.
|
||||
* NOTE: this implementation does currently only delete currentWord. This is due to change in
|
||||
* future versions.
|
||||
*
|
||||
* @param n The number of words to delete before the cursor. Must be greater than 0 or this
|
||||
* method will fail.
|
||||
*
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun deleteWordsBeforeCursor(n: Int): Boolean {
|
||||
val ic = ims?.currentInputConnection ?: return false
|
||||
return if (n < 1 || isRawInputEditor) {
|
||||
false
|
||||
} else {
|
||||
ic.beginBatchEdit()
|
||||
markComposingRegion(null)
|
||||
if (currentWord.isValid) {
|
||||
ic.setSelection(currentWord.start, currentWord.end)
|
||||
ic.commitText("", 1)
|
||||
}
|
||||
reevaluateCurrentWord()
|
||||
ic.endBatchEdit()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets [n] characters after the cursor's current position. The resulting string may be any
|
||||
* length ranging from 0 to n.
|
||||
*
|
||||
* @param n The number of characters to get after the cursor. Must be greater than 0 or this
|
||||
* method will fail.
|
||||
*
|
||||
* @return [n] or less characters after the cursor.
|
||||
*/
|
||||
fun getTextAfterCursor(n: Int): String {
|
||||
if (!selection.isValid || n < 1 || isRawInputEditor) {
|
||||
return ""
|
||||
}
|
||||
val from = selection.end.coerceIn(0, cachedText.length)
|
||||
val to = (selection.end + n).coerceIn(0, cachedText.length)
|
||||
return cachedText.substring(from, to)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets [n] characters before the cursor's current position. The resulting string may be any
|
||||
* length ranging from 0 to n.
|
||||
*
|
||||
* @param n The number of characters to get before the cursor. Must be greater than 0 or this
|
||||
* method will fail.
|
||||
*
|
||||
* @return [n] or less characters after the cursor.
|
||||
*/
|
||||
fun getTextBeforeCursor(n: Int): String {
|
||||
if (!selection.isValid || n < 1 || isRawInputEditor) {
|
||||
return ""
|
||||
}
|
||||
val from = (selection.start - n).coerceIn(0, cachedText.length)
|
||||
val to = selection.start.coerceIn(0, cachedText.length)
|
||||
return cachedText.substring(from, to)
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a cut command on this editor instance and adjusts both the cursor position and
|
||||
* composing region, if any.
|
||||
*
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun performClipboardCut(): Boolean {
|
||||
return if (selection.isSelectionMode) {
|
||||
val clipData: ClipData = ClipData.newPlainText(selection.text, selection.text)
|
||||
clipboardManager?.setPrimaryClip(clipData)
|
||||
deleteBackwards()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a copy command on this editor instance and adjusts both the cursor position and
|
||||
* composing region, if any.
|
||||
*
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun performClipboardCopy(): Boolean {
|
||||
return if (selection.isSelectionMode) {
|
||||
val clipData: ClipData = ClipData.newPlainText(selection.text, selection.text)
|
||||
clipboardManager?.setPrimaryClip(clipData)
|
||||
setSelection(selection.end, selection.end)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a paste command on this editor instance and adjusts both the cursor position and
|
||||
* composing region, if any.
|
||||
*
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun performClipboardPaste(): Boolean {
|
||||
val clipData: ClipData? = clipboardManager?.primaryClip
|
||||
val item: ClipData.Item? = clipData?.getItemAt(0)
|
||||
return when {
|
||||
item?.text != null -> {
|
||||
commitText(item.text.toString())
|
||||
}
|
||||
item?.uri != null -> {
|
||||
commitContent(item.uri, clipData.description)
|
||||
}
|
||||
else -> {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an enter key press on the current input editor.
|
||||
*
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun performEnter(): Boolean {
|
||||
return if (isRawInputEditor) {
|
||||
sendSystemKeyEvent(KeyEvent.KEYCODE_ENTER)
|
||||
} else {
|
||||
commitText("\n")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a given [action] on the current input editor.
|
||||
*
|
||||
* @param action The action to be performed on this editor instance.
|
||||
*
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun performEnterAction(action: ImeOptions.Action): Boolean {
|
||||
val ic = ims?.currentInputConnection ?: return false
|
||||
return ic.performEditorAction(action.toInt())
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a given [keyCode] as a [KeyEvent.ACTION_DOWN].
|
||||
*
|
||||
* @param keyCode The key code to send, use a key code defined in Android's [KeyEvent], not in
|
||||
* [KeyCode] or this call may send a weird character, as this key codes do not match!!
|
||||
*
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun sendSystemKeyEvent(keyCode: Int): Boolean {
|
||||
val ic = ims?.currentInputConnection ?: return false
|
||||
return ic.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, keyCode))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a given [keyCode] as a [KeyEvent.ACTION_DOWN] with ALT pressed.
|
||||
*
|
||||
* @param keyCode The key code to send, use a key code defined in Android's [KeyEvent], not in
|
||||
* [KeyCode] or this call may send a weird character, as this key codes do not match!!
|
||||
*
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun sendSystemKeyEventAlt(keyCode: Int): Boolean {
|
||||
val ic = ims?.currentInputConnection ?: return false
|
||||
return ic.sendKeyEvent(
|
||||
KeyEvent(
|
||||
0,
|
||||
1,
|
||||
KeyEvent.ACTION_DOWN, keyCode,
|
||||
0,
|
||||
KeyEvent.META_ALT_LEFT_ON
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the selection region of this instance and notifies the input connection.
|
||||
*
|
||||
* @param from The start index of the selection in characters (inclusive).
|
||||
* @param to The end index of the selection in characters (exclusive).
|
||||
*
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun setSelection(from: Int, to: Int): Boolean {
|
||||
val ic = ims?.currentInputConnection ?: return false
|
||||
return if (isRawInputEditor) {
|
||||
selection.apply {
|
||||
start = -1
|
||||
end = -1
|
||||
}
|
||||
false
|
||||
} else {
|
||||
selection.apply {
|
||||
start = from
|
||||
end = to
|
||||
}
|
||||
ic.setSelection(from, to)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the length of the character before the cursor, as many Unicode characters nowadays
|
||||
* are longer than 1 Java char and thus the length has to be calculated in order to avoid
|
||||
* deleting only half of an emoji...
|
||||
* Is used primarily in [deleteBackwards].
|
||||
*
|
||||
* @return The length of the last Unicode character, in Java characters or 0 if the current
|
||||
* selection is invalid.
|
||||
*/
|
||||
private fun detectLastUnicodeCharacterLengthBeforeCursor(): Int {
|
||||
if (!selection.isValid) {
|
||||
return 0
|
||||
}
|
||||
var charIndex = 0
|
||||
var charLength = 0
|
||||
var charShouldGlue = false
|
||||
val textToSearch = cachedText.substring(0, selection.start.coerceAtMost(cachedText.length))
|
||||
var i = 0
|
||||
while (i < textToSearch.length) {
|
||||
val cp = textToSearch.codePointAt(i)
|
||||
val cpLength = Character.charCount(cp)
|
||||
when {
|
||||
charShouldGlue || cp == VARIATION_SELECTOR || emojiVariationArray.contains(cp) -> {
|
||||
charLength += cpLength
|
||||
charShouldGlue = false
|
||||
}
|
||||
cp == ZERO_WIDTH_JOINER -> {
|
||||
charLength += cpLength
|
||||
charShouldGlue = true
|
||||
}
|
||||
else -> {
|
||||
charIndex = i
|
||||
charLength = 0
|
||||
charShouldGlue = false
|
||||
}
|
||||
}
|
||||
i += cpLength
|
||||
}
|
||||
return textToSearch.length - charIndex
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a given [region] as composing and notifies the input connection.
|
||||
*
|
||||
* @param region The region which should be marked as composing.
|
||||
*
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
private fun markComposingRegion(region: Region?): Boolean {
|
||||
val ic = ims?.currentInputConnection ?: return false
|
||||
return when (region) {
|
||||
null -> ic.finishComposingText()
|
||||
else -> if (region.isValid) {
|
||||
ic.setComposingRegion(region.start, region.end)
|
||||
} else {
|
||||
ic.finishComposingText()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the current word in this editor instance based on the current cursor position and
|
||||
* given delimiter [regex].
|
||||
*
|
||||
* @param regex The delimiter regex which should be used to split up the content text and find
|
||||
* words. May differ from locale to locale.
|
||||
*
|
||||
* @return True on success, false if no current word could be found.
|
||||
*/
|
||||
private fun reevaluateCurrentWord(regex: Regex): Boolean {
|
||||
var foundValidWord = false
|
||||
if (selection.isValid && selection.isCursorMode) {
|
||||
val words = cachedText.split("((?<=$regex)|(?=$regex))".toRegex())
|
||||
var pos = 0
|
||||
for (word in words) {
|
||||
if (selection.start >= pos && selection.start <= pos + word.length &&
|
||||
word.isNotEmpty() && !word.matches(regex)) {
|
||||
currentWord.apply {
|
||||
start = pos
|
||||
end = pos + word.length
|
||||
}
|
||||
foundValidWord = true
|
||||
break
|
||||
} else {
|
||||
pos += word.length
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!foundValidWord) {
|
||||
currentWord.apply {
|
||||
start = -1
|
||||
end = -1
|
||||
}
|
||||
}
|
||||
return foundValidWord
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the current word with the correct delimiter regex for current subtype.
|
||||
* TODO: currently only supports en-US
|
||||
*/
|
||||
private fun reevaluateCurrentWord() {
|
||||
val regex = "[^\\p{L}]".toRegex()
|
||||
reevaluateCurrentWord(regex)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current text from the app's editor view.
|
||||
*
|
||||
* @return The target editor's content string.
|
||||
*/
|
||||
private fun updateEditorState() {
|
||||
val ic = ims?.currentInputConnection
|
||||
val et = ic?.getExtractedText(
|
||||
ExtractedTextRequest(), 0
|
||||
)
|
||||
val text = et?.text
|
||||
if (ic == null || et == null || text == null) {
|
||||
isRawInputEditor = true
|
||||
cachedText = ""
|
||||
selection.apply {
|
||||
start = -1
|
||||
end = -1
|
||||
}
|
||||
} else {
|
||||
isRawInputEditor = false
|
||||
cachedText = text.toString()
|
||||
selection.apply {
|
||||
start = et.selectionStart.coerceAtMost(cachedText.length)
|
||||
end = et.selectionEnd.coerceAtMost(cachedText.length)
|
||||
}
|
||||
}
|
||||
reevaluateCurrentWord()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class which holds the same information as an [EditorInfo.imeOptions] int but more accessible and
|
||||
* readable.
|
||||
*/
|
||||
class ImeOptions private constructor(imeOptions: Int) {
|
||||
val action: Action = Action.fromInt(imeOptions)
|
||||
val flagForceAscii: Boolean = imeOptions and EditorInfo.IME_FLAG_FORCE_ASCII > 0
|
||||
val flagNavigateNext: Boolean = imeOptions and EditorInfo.IME_FLAG_NAVIGATE_NEXT > 0
|
||||
val flagNavigatePrevious: Boolean = imeOptions and EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS > 0
|
||||
val flagNoAccessoryAction: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_ACCESSORY_ACTION > 0
|
||||
val flagNoEnterAction: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_ENTER_ACTION > 0
|
||||
val flagNoExtractUi: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_EXTRACT_UI > 0
|
||||
val flagNoFullscreen: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_FULLSCREEN > 0
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
val flagNoPersonalizedLearning: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING > 0
|
||||
|
||||
companion object {
|
||||
fun default(): ImeOptions {
|
||||
return fromImeOptionsInt(EditorInfo.IME_NULL)
|
||||
}
|
||||
|
||||
fun fromImeOptionsInt(imeOptions: Int): ImeOptions {
|
||||
return ImeOptions(imeOptions)
|
||||
}
|
||||
}
|
||||
|
||||
enum class Action {
|
||||
DONE,
|
||||
GO,
|
||||
NEXT,
|
||||
NONE,
|
||||
PREVIOUS,
|
||||
SEARCH,
|
||||
SEND,
|
||||
UNSPECIFIED;
|
||||
|
||||
companion object {
|
||||
fun fromInt(raw: Int): Action {
|
||||
return when (raw and EditorInfo.IME_MASK_ACTION) {
|
||||
EditorInfo.IME_ACTION_DONE -> DONE
|
||||
EditorInfo.IME_ACTION_GO -> GO
|
||||
EditorInfo.IME_ACTION_NEXT -> NEXT
|
||||
EditorInfo.IME_ACTION_NONE -> NONE
|
||||
EditorInfo.IME_ACTION_PREVIOUS -> PREVIOUS
|
||||
EditorInfo.IME_ACTION_SEARCH -> SEARCH
|
||||
EditorInfo.IME_ACTION_SEND -> SEND
|
||||
EditorInfo.IME_ACTION_UNSPECIFIED -> UNSPECIFIED
|
||||
else -> NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toInt(): Int {
|
||||
return when (this) {
|
||||
DONE -> EditorInfo.IME_ACTION_DONE
|
||||
GO -> EditorInfo.IME_ACTION_GO
|
||||
NEXT -> EditorInfo.IME_ACTION_NEXT
|
||||
NONE -> EditorInfo.IME_ACTION_NONE
|
||||
PREVIOUS -> EditorInfo.IME_ACTION_PREVIOUS
|
||||
SEARCH -> EditorInfo.IME_ACTION_SEARCH
|
||||
SEND -> EditorInfo.IME_ACTION_SEND
|
||||
UNSPECIFIED-> EditorInfo.IME_ACTION_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class which holds the same information as an [EditorInfo.inputType] int but more accessible and
|
||||
* readable.
|
||||
*/
|
||||
class InputAttributes private constructor(inputType: Int) {
|
||||
val type: Type
|
||||
val variation: Variation
|
||||
val capsMode: CapsMode
|
||||
var flagNumberDecimal: Boolean = false
|
||||
private set
|
||||
var flagNumberSigned: Boolean = false
|
||||
private set
|
||||
var flagTextAutoComplete: Boolean = false
|
||||
private set
|
||||
var flagTextAutoCorrect: Boolean = false
|
||||
private set
|
||||
var flagTextImeMultiLine: Boolean = false
|
||||
private set
|
||||
var flagTextMultiLine: Boolean = false
|
||||
private set
|
||||
var flagTextNoSuggestions: Boolean = false
|
||||
private set
|
||||
|
||||
init {
|
||||
when (inputType and InputType.TYPE_MASK_CLASS) {
|
||||
InputType.TYPE_CLASS_DATETIME -> {
|
||||
type = Type.DATETIME
|
||||
variation = when (inputType and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_DATETIME_VARIATION_DATE -> Variation.DATE
|
||||
InputType.TYPE_DATETIME_VARIATION_NORMAL -> Variation.NORMAL
|
||||
InputType.TYPE_DATETIME_VARIATION_TIME -> Variation.TIME
|
||||
else -> Variation.NORMAL
|
||||
}
|
||||
capsMode = CapsMode.NONE
|
||||
}
|
||||
InputType.TYPE_CLASS_NUMBER -> {
|
||||
type = Type.NUMBER
|
||||
variation = when (inputType and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_NUMBER_VARIATION_NORMAL -> Variation.NORMAL
|
||||
InputType.TYPE_NUMBER_VARIATION_PASSWORD -> Variation.PASSWORD
|
||||
else -> Variation.NORMAL
|
||||
}
|
||||
capsMode = CapsMode.NONE
|
||||
flagNumberDecimal = inputType and InputType.TYPE_NUMBER_FLAG_DECIMAL > 0
|
||||
flagNumberSigned = inputType and InputType.TYPE_NUMBER_FLAG_SIGNED > 0
|
||||
}
|
||||
InputType.TYPE_CLASS_PHONE -> {
|
||||
type = Type.PHONE
|
||||
variation = Variation.NORMAL
|
||||
capsMode = CapsMode.NONE
|
||||
}
|
||||
InputType.TYPE_CLASS_TEXT -> {
|
||||
type = Type.TEXT
|
||||
variation = when (inputType and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS -> Variation.EMAIL_ADDRESS
|
||||
InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT -> Variation.EMAIL_SUBJECT
|
||||
InputType.TYPE_TEXT_VARIATION_FILTER -> Variation.FILTER
|
||||
InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE -> Variation.LONG_MESSAGE
|
||||
InputType.TYPE_TEXT_VARIATION_NORMAL -> Variation.NORMAL
|
||||
InputType.TYPE_TEXT_VARIATION_PASSWORD -> Variation.PASSWORD
|
||||
InputType.TYPE_TEXT_VARIATION_PERSON_NAME -> Variation.PERSON_NAME
|
||||
InputType.TYPE_TEXT_VARIATION_PHONETIC -> Variation.PHONETIC
|
||||
InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS -> Variation.POSTAL_ADDRESS
|
||||
InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE -> Variation.SHORT_MESSAGE
|
||||
InputType.TYPE_TEXT_VARIATION_URI -> Variation.URI
|
||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD -> Variation.VISIBLE_PASSWORD
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT -> Variation.WEB_EDIT_TEXT
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS -> Variation.WEB_EMAIL_ADDRESS
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD -> Variation.WEB_PASSWORD
|
||||
else -> Variation.NORMAL
|
||||
}
|
||||
capsMode = CapsMode.fromFlags(inputType)
|
||||
flagTextAutoComplete = inputType and InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE > 0
|
||||
flagTextAutoCorrect = inputType and InputType.TYPE_TEXT_FLAG_AUTO_CORRECT > 0
|
||||
flagTextImeMultiLine = inputType and InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE > 0
|
||||
flagTextMultiLine = inputType and InputType.TYPE_TEXT_FLAG_MULTI_LINE > 0
|
||||
flagTextNoSuggestions = inputType and InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS > 0
|
||||
}
|
||||
else -> {
|
||||
type = Type.TEXT
|
||||
variation = Variation.NORMAL
|
||||
capsMode = CapsMode.NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromInputTypeInt(inputType: Int): InputAttributes {
|
||||
return InputAttributes(inputType)
|
||||
}
|
||||
}
|
||||
|
||||
enum class Type {
|
||||
DATETIME,
|
||||
NUMBER,
|
||||
PHONE,
|
||||
TEXT;
|
||||
}
|
||||
|
||||
enum class Variation {
|
||||
DATE,
|
||||
EMAIL_ADDRESS,
|
||||
EMAIL_SUBJECT,
|
||||
FILTER,
|
||||
LONG_MESSAGE,
|
||||
NORMAL,
|
||||
PASSWORD,
|
||||
PERSON_NAME,
|
||||
PHONETIC,
|
||||
POSTAL_ADDRESS,
|
||||
SHORT_MESSAGE,
|
||||
TIME,
|
||||
URI,
|
||||
VISIBLE_PASSWORD,
|
||||
WEB_EDIT_TEXT,
|
||||
WEB_EMAIL_ADDRESS,
|
||||
WEB_PASSWORD;
|
||||
}
|
||||
|
||||
enum class CapsMode {
|
||||
ALL,
|
||||
NONE,
|
||||
SENTENCES,
|
||||
WORDS;
|
||||
|
||||
companion object {
|
||||
fun fromFlags(flags: Int): CapsMode {
|
||||
return when {
|
||||
flags and InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS > 0 -> ALL
|
||||
flags and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES > 0 -> SENTENCES
|
||||
flags and InputType.TYPE_TEXT_FLAG_CAP_WORDS > 0 -> WORDS
|
||||
else -> NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toFlags(): Int {
|
||||
return when (this) {
|
||||
ALL -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
|
||||
SENTENCES -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
||||
WORDS -> InputType.TYPE_TEXT_FLAG_CAP_WORDS
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class which marks a region of the [text] in [editorInstance].
|
||||
*/
|
||||
open class Region(private val editorInstance: EditorInstance) {
|
||||
var start: Int = -1
|
||||
var end: Int = -1
|
||||
val isValid: Boolean
|
||||
get() = start >= 0 && end >= 0 && length >= 0
|
||||
val length: Int
|
||||
get() = end - start
|
||||
val text: String
|
||||
get() {
|
||||
val eiText = editorInstance.cachedText
|
||||
return if (!isValid || start >= eiText.length) {
|
||||
""
|
||||
} else {
|
||||
val end = if (end >= eiText.length) { eiText.length } else { end }
|
||||
editorInstance.cachedText.substring(start, end)
|
||||
}
|
||||
}
|
||||
|
||||
override operator fun equals(other: Any?): Boolean {
|
||||
return if (other is Region) {
|
||||
start == other.start && end == other.end
|
||||
} else {
|
||||
super.equals(other)
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = start
|
||||
result = 31 * result + end
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class which holds selection attributes and returns the correct text for set selection based on
|
||||
* the text in [editorInstance].
|
||||
*/
|
||||
class Selection(private val editorInstance: EditorInstance) : Region(editorInstance) {
|
||||
val isCursorMode: Boolean
|
||||
get() = length == 0 && isValid
|
||||
val isSelectionMode: Boolean
|
||||
get() = length != 0 && isValid
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.core
|
||||
|
||||
import android.app.Application
|
||||
import dev.patrickgold.florisboard.crashutility.CrashUtility
|
||||
|
||||
class FlorisApplication : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
CrashUtility.install(this)
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,12 @@
|
||||
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.InputMethodService
|
||||
import android.media.AudioManager
|
||||
import android.os.*
|
||||
@@ -28,20 +31,20 @@ import android.util.Log
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.CursorAnchorInfo
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputConnection
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import com.squareup.moshi.Json
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.media.MediaInputManager
|
||||
import dev.patrickgold.florisboard.ime.text.TextInputManager
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyData
|
||||
import dev.patrickgold.florisboard.settings.SettingsMainActivity
|
||||
import dev.patrickgold.florisboard.util.*
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
* Variable which holds the current [FlorisBoard] instance. To get this instance from another
|
||||
@@ -53,20 +56,27 @@ 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() {
|
||||
class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedListener {
|
||||
lateinit var prefs: PrefHelper
|
||||
private set
|
||||
|
||||
val context: Context
|
||||
get() = inputView?.context ?: this
|
||||
private var inputView: InputView? = null
|
||||
get() = inputWindowView?.context ?: this
|
||||
var inputView: InputView? = null
|
||||
private set
|
||||
private var inputWindowView: InputWindowView? = null
|
||||
private var eventListeners: MutableList<WeakReference<EventListener>> = mutableListOf()
|
||||
|
||||
private var audioManager: AudioManager? = null
|
||||
var clipboardManager: ClipboardManager? = null
|
||||
private var vibrator: Vibrator? = null
|
||||
private val osHandler = Handler()
|
||||
|
||||
var activeEditorInstance: EditorInstance = EditorInstance.default()
|
||||
|
||||
lateinit var subtypeManager: SubtypeManager
|
||||
lateinit var activeSubtype: Subtype
|
||||
private var currentThemeIsNight: Boolean = false
|
||||
private var currentThemeResId: Int = 0
|
||||
|
||||
val textInputManager: TextInputManager
|
||||
@@ -81,6 +91,7 @@ class FlorisBoard : InputMethodService() {
|
||||
|
||||
companion object {
|
||||
private const val IME_ID: String = "dev.patrickgold.florisboard/.ime.core.FlorisBoard"
|
||||
private val TAG: String? = FlorisBoard::class.simpleName
|
||||
|
||||
fun checkIfImeIsEnabled(context: Context): Boolean {
|
||||
val activeImeIds = Settings.Secure.getString(
|
||||
@@ -104,6 +115,18 @@ class FlorisBoard : InputMethodService() {
|
||||
fun getInstance(): FlorisBoard {
|
||||
return florisboardInstance!!
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun getInstanceOrNull(): FlorisBoard? {
|
||||
return florisboardInstance
|
||||
}
|
||||
|
||||
fun getDayNightBaseThemeId(isNightTheme: Boolean): Int {
|
||||
return when (isNightTheme) {
|
||||
true -> R.style.KeyboardThemeBase_Night
|
||||
else -> R.style.KeyboardThemeBase_Day
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
@@ -125,187 +148,218 @@ class FlorisBoard : InputMethodService() {
|
||||
.build()
|
||||
)
|
||||
}
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onCreate()")
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onCreate()")
|
||||
|
||||
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboardManager?.addPrimaryClipChangedListener(this)
|
||||
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
prefs = PrefHelper(this)
|
||||
prefs = PrefHelper.getDefaultInstance(this)
|
||||
prefs.initDefaultPreferences()
|
||||
prefs.sync()
|
||||
subtypeManager = SubtypeManager(this, prefs)
|
||||
activeSubtype = subtypeManager.getActiveSubtype() ?: Subtype.DEFAULT
|
||||
|
||||
currentThemeResId = prefs.theme.getSelectedThemeResId()
|
||||
currentThemeIsNight = prefs.internal.themeCurrentIsNight
|
||||
currentThemeResId = getDayNightBaseThemeId(currentThemeIsNight)
|
||||
setTheme(currentThemeResId)
|
||||
updateTheme()
|
||||
|
||||
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
|
||||
|
||||
super.onCreate()
|
||||
textInputManager.onCreate()
|
||||
mediaInputManager.onCreate()
|
||||
eventListeners.toList().forEach { it.get()?.onCreate() }
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
override fun onCreateInputView(): View? {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onCreateInputView()")
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onCreateInputView()")
|
||||
|
||||
baseContext.setTheme(currentThemeResId)
|
||||
|
||||
inputView = layoutInflater.inflate(R.layout.florisboard, null) as InputView
|
||||
inputWindowView = layoutInflater.inflate(R.layout.florisboard, null) as InputWindowView
|
||||
|
||||
textInputManager.onCreateInputView()
|
||||
mediaInputManager.onCreateInputView()
|
||||
eventListeners.toList().forEach { it.get()?.onCreateInputView() }
|
||||
|
||||
return inputView
|
||||
return inputWindowView
|
||||
}
|
||||
|
||||
fun registerInputView(inputView: InputView) {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "registerInputView(inputView)")
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "registerInputView($inputView)")
|
||||
|
||||
this.inputView = inputView
|
||||
initializeOneHandedEnvironment()
|
||||
updateTheme()
|
||||
updateSoftInputWindowLayoutParameters()
|
||||
updateOneHandedPanelVisibility()
|
||||
|
||||
textInputManager.onRegisterInputView(inputView)
|
||||
mediaInputManager.onRegisterInputView(inputView)
|
||||
eventListeners.toList().forEach { it.get()?.onRegisterInputView(inputView) }
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onDestroy()")
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onDestroy()")
|
||||
|
||||
clipboardManager?.removePrimaryClipChangedListener(this)
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
florisboardInstance = null
|
||||
|
||||
eventListeners.toList().forEach { it.get()?.onDestroy() }
|
||||
eventListeners.clear()
|
||||
super.onDestroy()
|
||||
textInputManager.onDestroy()
|
||||
mediaInputManager.onDestroy()
|
||||
}
|
||||
|
||||
override fun onStartInput(attribute: EditorInfo?, restarting: Boolean) {
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onStartInput($attribute, $restarting)")
|
||||
|
||||
super.onStartInput(attribute, restarting)
|
||||
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_IMMEDIATE)
|
||||
}
|
||||
|
||||
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
|
||||
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_MONITOR)
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onStartInputView($info, $restarting)")
|
||||
Log.i(TAG, "onStartInputView: " + info?.debugSummarize())
|
||||
|
||||
super.onStartInputView(info, restarting)
|
||||
textInputManager.onStartInputView(info, restarting)
|
||||
mediaInputManager.onStartInputView(info, restarting)
|
||||
activeEditorInstance = EditorInstance.from(info, this)
|
||||
eventListeners.toList().forEach {
|
||||
it.get()?.onStartInputView(activeEditorInstance, restarting)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinishInputView(finishingInput: Boolean) {
|
||||
currentInputConnection?.requestCursorUpdates(0)
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onFinishInputView($finishingInput)")
|
||||
|
||||
if (finishingInput) {
|
||||
activeEditorInstance = EditorInstance.default()
|
||||
}
|
||||
|
||||
super.onFinishInputView(finishingInput)
|
||||
textInputManager.onFinishInputView(finishingInput)
|
||||
mediaInputManager.onFinishInputView(finishingInput)
|
||||
eventListeners.toList().forEach { it.get()?.onFinishInputView(finishingInput) }
|
||||
}
|
||||
|
||||
override fun onFinishInput() {
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onFinishInput()")
|
||||
|
||||
super.onFinishInput()
|
||||
currentInputConnection?.requestCursorUpdates(0)
|
||||
}
|
||||
|
||||
override fun onWindowShown() {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onWindowShown()")
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onWindowShown()")
|
||||
|
||||
prefs.sync()
|
||||
updateThemeIfNecessary()
|
||||
updateTheme()
|
||||
updateOneHandedPanelVisibility()
|
||||
activeSubtype = subtypeManager.getActiveSubtype() ?: Subtype.DEFAULT
|
||||
onSubtypeChanged(activeSubtype)
|
||||
setActiveInput(R.id.text_input)
|
||||
|
||||
super.onWindowShown()
|
||||
textInputManager.onWindowShown()
|
||||
mediaInputManager.onWindowShown()
|
||||
eventListeners.toList().forEach { it.get()?.onWindowShown() }
|
||||
}
|
||||
|
||||
override fun onWindowHidden() {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onWindowHidden()")
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onWindowHidden()")
|
||||
|
||||
super.onWindowHidden()
|
||||
textInputManager.onWindowHidden()
|
||||
mediaInputManager.onWindowHidden()
|
||||
eventListeners.toList().forEach { it.get()?.onWindowHidden() }
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onConfigurationChanged($newConfig)")
|
||||
if (isInputViewShown) {
|
||||
updateOneHandedPanelVisibility()
|
||||
}
|
||||
|
||||
super.onConfigurationChanged(newConfig)
|
||||
textInputManager.onConfigurationChanged(newConfig)
|
||||
mediaInputManager.onConfigurationChanged(newConfig)
|
||||
}
|
||||
|
||||
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
|
||||
super.onUpdateCursorAnchorInfo(cursorAnchorInfo)
|
||||
textInputManager.onUpdateCursorAnchorInfo(cursorAnchorInfo)
|
||||
mediaInputManager.onUpdateCursorAnchorInfo(cursorAnchorInfo)
|
||||
}
|
||||
|
||||
override fun onUpdateSelection(
|
||||
oldSelStart: Int,
|
||||
oldSelEnd: Int,
|
||||
newSelStart: Int,
|
||||
newSelEnd: Int,
|
||||
candidatesStart: Int,
|
||||
candidatesEnd: Int
|
||||
oldSelStart: Int, oldSelEnd: Int,
|
||||
newSelStart: Int, newSelEnd: Int,
|
||||
candidatesStart: Int, candidatesEnd: Int
|
||||
) {
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onUpdateSelection($oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd)")
|
||||
|
||||
super.onUpdateSelection(
|
||||
oldSelStart,
|
||||
oldSelEnd,
|
||||
newSelStart,
|
||||
newSelEnd,
|
||||
candidatesStart,
|
||||
candidatesEnd
|
||||
oldSelStart, oldSelEnd,
|
||||
newSelStart, newSelEnd,
|
||||
candidatesStart, candidatesEnd
|
||||
)
|
||||
textInputManager.onUpdateSelection(
|
||||
oldSelStart,
|
||||
oldSelEnd,
|
||||
newSelStart,
|
||||
newSelEnd,
|
||||
candidatesStart,
|
||||
candidatesEnd
|
||||
)
|
||||
mediaInputManager.onUpdateSelection(
|
||||
oldSelStart,
|
||||
oldSelEnd,
|
||||
newSelStart,
|
||||
newSelEnd,
|
||||
candidatesStart,
|
||||
candidatesEnd
|
||||
activeEditorInstance.onUpdateSelection(
|
||||
oldSelStart, oldSelEnd,
|
||||
newSelStart, newSelEnd
|
||||
)
|
||||
eventListeners.toList().forEach { it.get()?.onUpdateSelection() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the preferences if the selected theme res id has changed and updates the theme only
|
||||
* then by rebuilding the UI and setting the navigation bar theme manually.
|
||||
* Updates the theme of the IME Window, status and navigation bar, as well as the InputView and
|
||||
* some of its components.
|
||||
*/
|
||||
private fun updateThemeIfNecessary() {
|
||||
val newThemeResId = prefs.theme.getSelectedThemeResId()
|
||||
if (newThemeResId != currentThemeResId) {
|
||||
currentThemeResId = newThemeResId
|
||||
private fun updateTheme() {
|
||||
// Rebuild the UI if the theme has changed from day to night or vice versa to prevent
|
||||
// theme glitches with scrollbars and hints of buttons in the media UI. If the UI must be
|
||||
// rebuild, quit this method, as it will be called again by the newly created UI.
|
||||
val newThemeIsNightMode = prefs.internal.themeCurrentIsNight
|
||||
if (currentThemeIsNight != newThemeIsNightMode) {
|
||||
currentThemeResId = getDayNightBaseThemeId(newThemeIsNightMode)
|
||||
currentThemeIsNight = newThemeIsNightMode
|
||||
setInputView(onCreateInputView())
|
||||
val w = window?.window ?: return
|
||||
w.navigationBarColor = getColorFromAttr(baseContext, android.R.attr.navigationBarColor)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
var flags = w.decorView.systemUiVisibility
|
||||
flags = if (getBooleanFromAttr(baseContext, android.R.attr.windowLightNavigationBar)) {
|
||||
flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
|
||||
} else {
|
||||
flags and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
|
||||
}
|
||||
w.decorView.systemUiVisibility = flags
|
||||
return
|
||||
}
|
||||
|
||||
// Get Window and the flags of the DecorView
|
||||
val w = window?.window ?: return
|
||||
var flags = w.decorView.systemUiVisibility
|
||||
|
||||
// Update navigation bar theme
|
||||
w.navigationBarColor = prefs.theme.navBarColor
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
flags = if (prefs.theme.navBarIsLight) {
|
||||
flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
|
||||
} else {
|
||||
flags and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
|
||||
}
|
||||
}
|
||||
|
||||
// Update status bar to be transparent
|
||||
// Done as starting with Android 11 the IME Window takes the primaryColorDark value and
|
||||
// colors the status bar, which isn't the desired behavior. (See issue #43)
|
||||
flags = flags or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
w.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
|
||||
w.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
|
||||
w.statusBarColor = Color.TRANSPARENT
|
||||
|
||||
// Apply the new flags to the DecorView
|
||||
w.decorView.systemUiVisibility = flags
|
||||
|
||||
// Update InputView theme
|
||||
inputView?.setBackgroundColor(prefs.theme.keyboardBgColor)
|
||||
inputView?.oneHandedCtrlPanelStart?.setBackgroundColor(prefs.theme.oneHandedBgColor)
|
||||
inputView?.oneHandedCtrlPanelEnd?.setBackgroundColor(prefs.theme.oneHandedBgColor)
|
||||
inputView?.findViewById<ImageButton>(R.id.one_handed_ctrl_move_start)
|
||||
?.imageTintList = ColorStateList.valueOf(prefs.theme.oneHandedButtonFgColor)
|
||||
inputView?.findViewById<ImageButton>(R.id.one_handed_ctrl_move_end)
|
||||
?.imageTintList = ColorStateList.valueOf(prefs.theme.oneHandedButtonFgColor)
|
||||
inputView?.findViewById<ImageButton>(R.id.one_handed_ctrl_close_start)
|
||||
?.imageTintList = ColorStateList.valueOf(prefs.theme.oneHandedButtonFgColor)
|
||||
inputView?.findViewById<ImageButton>(R.id.one_handed_ctrl_close_end)
|
||||
?.imageTintList = ColorStateList.valueOf(prefs.theme.oneHandedButtonFgColor)
|
||||
eventListeners.toList().forEach { it.get()?.onApplyThemeAttributes() }
|
||||
}
|
||||
|
||||
override fun onComputeInsets(outInsets: Insets?) {
|
||||
super.onComputeInsets(outInsets)
|
||||
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 = inputView.height
|
||||
outInsets?.visibleTopInsets = inputView.height
|
||||
outInsets?.contentTopInsets = inputWindowView.height
|
||||
outInsets?.visibleTopInsets = inputWindowView.height
|
||||
return
|
||||
}
|
||||
val innerInputViewContainer =
|
||||
inputView.findViewById<LinearLayout>(R.id.inner_input_view_container) ?: return
|
||||
val visibleTopY = inputView.height - innerInputViewContainer.measuredHeight
|
||||
val visibleTopY = inputWindowView.height - inputView.measuredHeight
|
||||
outInsets?.contentTopInsets = visibleTopY
|
||||
outInsets?.visibleTopInsets = visibleTopY
|
||||
}
|
||||
@@ -321,8 +375,8 @@ class FlorisBoard : InputMethodService() {
|
||||
private fun updateSoftInputWindowLayoutParameters() {
|
||||
val w = window?.window ?: return
|
||||
ViewLayoutUtils.updateLayoutHeightOf(w, WindowManager.LayoutParams.MATCH_PARENT)
|
||||
val inputView = this.inputView
|
||||
if (inputView != null) {
|
||||
val inputWindowView = this.inputWindowView
|
||||
if (inputWindowView != null) {
|
||||
val layoutHeight = if (isFullscreenMode) {
|
||||
WindowManager.LayoutParams.WRAP_CONTENT
|
||||
} else {
|
||||
@@ -331,7 +385,7 @@ class FlorisBoard : InputMethodService() {
|
||||
val inputArea = w.findViewById<View>(android.R.id.inputArea)
|
||||
ViewLayoutUtils.updateLayoutHeightOf(inputArea, layoutHeight)
|
||||
ViewLayoutUtils.updateLayoutGravityOf(inputArea, Gravity.BOTTOM)
|
||||
ViewLayoutUtils.updateLayoutHeightOf(inputView, layoutHeight)
|
||||
ViewLayoutUtils.updateLayoutHeightOf(inputWindowView, layoutHeight)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,9 +393,9 @@ class FlorisBoard : InputMethodService() {
|
||||
* Makes a key press vibration if the user has this feature enabled in the preferences.
|
||||
*/
|
||||
fun keyPressVibrate() {
|
||||
if (prefs.looknfeel.vibrationEnabled) {
|
||||
var vibrationStrength = prefs.looknfeel.vibrationStrength
|
||||
if (vibrationStrength == 0 && prefs.looknfeel.vibrationEnabledSystem) {
|
||||
if (prefs.keyboard.vibrationEnabled) {
|
||||
var vibrationStrength = prefs.keyboard.vibrationStrength
|
||||
if (vibrationStrength == -1 && prefs.keyboard.vibrationEnabledSystem) {
|
||||
vibrationStrength = 36
|
||||
}
|
||||
if (vibrationStrength > 0) {
|
||||
@@ -363,15 +417,15 @@ class FlorisBoard : InputMethodService() {
|
||||
* Makes a key press sound if the user has this feature enabled in the preferences.
|
||||
*/
|
||||
fun keyPressSound(keyData: KeyData? = null) {
|
||||
if (prefs.looknfeel.soundEnabled) {
|
||||
val soundVolume = prefs.looknfeel.soundVolume
|
||||
if (prefs.keyboard.soundEnabled) {
|
||||
val soundVolume = prefs.keyboard.soundVolume
|
||||
val effect = when (keyData?.code) {
|
||||
KeyCode.SPACE -> AudioManager.FX_KEYPRESS_SPACEBAR
|
||||
KeyCode.DELETE -> AudioManager.FX_KEYPRESS_DELETE
|
||||
KeyCode.ENTER -> AudioManager.FX_KEYPRESS_RETURN
|
||||
else -> AudioManager.FX_KEYPRESS_STANDARD
|
||||
}
|
||||
if (soundVolume == 0 && prefs.looknfeel.soundEnabledSystem) {
|
||||
if (soundVolume == -1 && prefs.keyboard.soundEnabledSystem) {
|
||||
audioManager!!.playSoundEffect(effect)
|
||||
} else if (soundVolume > 0) {
|
||||
audioManager!!.playSoundEffect(effect, soundVolume / 100f)
|
||||
@@ -379,6 +433,19 @@ class FlorisBoard : InputMethodService() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a given [SwipeAction]. Ignores any [SwipeAction] but the ones relevant for this
|
||||
* class.
|
||||
*/
|
||||
fun executeSwipeAction(swipeAction: SwipeAction) {
|
||||
when (swipeAction) {
|
||||
SwipeAction.HIDE_KEYBOARD -> requestHideSelf(0)
|
||||
SwipeAction.SWITCH_TO_PREV_SUBTYPE -> switchToPrevSubtype()
|
||||
SwipeAction.SWITCH_TO_NEXT_SUBTYPE -> switchToNextSubtype()
|
||||
else -> textInputManager.executeSwipeAction(swipeAction)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the IME and launches [SettingsMainActivity].
|
||||
*/
|
||||
@@ -398,6 +465,11 @@ class FlorisBoard : InputMethodService() {
|
||||
return subtypeManager.subtypes.size > 1
|
||||
}
|
||||
|
||||
fun switchToPrevSubtype() {
|
||||
activeSubtype = subtypeManager.switchToPrevSubtype() ?: Subtype.DEFAULT
|
||||
onSubtypeChanged(activeSubtype)
|
||||
}
|
||||
|
||||
fun switchToNextSubtype() {
|
||||
activeSubtype = subtypeManager.switchToNextSubtype() ?: Subtype.DEFAULT
|
||||
onSubtypeChanged(activeSubtype)
|
||||
@@ -435,26 +507,26 @@ class FlorisBoard : InputMethodService() {
|
||||
private fun onOneHandedPanelButtonClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.one_handed_ctrl_move_start -> {
|
||||
prefs.looknfeel.oneHandedMode = "start"
|
||||
prefs.keyboard.oneHandedMode = "start"
|
||||
}
|
||||
R.id.one_handed_ctrl_move_end -> {
|
||||
prefs.looknfeel.oneHandedMode = "end"
|
||||
prefs.keyboard.oneHandedMode = "end"
|
||||
}
|
||||
R.id.one_handed_ctrl_close_start,
|
||||
R.id.one_handed_ctrl_close_end -> {
|
||||
prefs.looknfeel.oneHandedMode = "off"
|
||||
prefs.keyboard.oneHandedMode = "off"
|
||||
}
|
||||
}
|
||||
updateOneHandedPanelVisibility()
|
||||
}
|
||||
|
||||
fun toggleOneHandedMode() {
|
||||
when (prefs.looknfeel.oneHandedMode) {
|
||||
when (prefs.keyboard.oneHandedMode) {
|
||||
"off" -> {
|
||||
prefs.looknfeel.oneHandedMode = "end"
|
||||
prefs.keyboard.oneHandedMode = "end"
|
||||
}
|
||||
else -> {
|
||||
prefs.looknfeel.oneHandedMode = "off"
|
||||
prefs.keyboard.oneHandedMode = "off"
|
||||
}
|
||||
}
|
||||
updateOneHandedPanelVisibility()
|
||||
@@ -465,7 +537,7 @@ class FlorisBoard : InputMethodService() {
|
||||
inputView?.oneHandedCtrlPanelStart?.visibility = View.GONE
|
||||
inputView?.oneHandedCtrlPanelEnd?.visibility = View.GONE
|
||||
} else {
|
||||
when (prefs.looknfeel.oneHandedMode) {
|
||||
when (prefs.keyboard.oneHandedMode) {
|
||||
"off" -> {
|
||||
inputView?.oneHandedCtrlPanelStart?.visibility = View.GONE
|
||||
inputView?.oneHandedCtrlPanelEnd?.visibility = View.GONE
|
||||
@@ -486,30 +558,52 @@ class FlorisBoard : InputMethodService() {
|
||||
}, 0)
|
||||
}
|
||||
|
||||
override fun onPrimaryClipChanged() {
|
||||
eventListeners.toList().forEach { it.get()?.onPrimaryClipChanged() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a given [listener] to the list which will receive FlorisBoard events.
|
||||
*
|
||||
* @param listener The listener object which receives the events.
|
||||
* @return True if the listener has been added successfully, false otherwise.
|
||||
*/
|
||||
fun addEventListener(listener: EventListener): Boolean {
|
||||
return eventListeners.add(WeakReference(listener))
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a given [listener] from the list which will receive FlorisBoard events.
|
||||
*
|
||||
* @param listener The same listener object which was used in [addEventListener].
|
||||
* @return True if the listener has been removed successfully, false otherwise. A false return
|
||||
* value may also indicate that the [listener] was not added previously.
|
||||
*/
|
||||
fun removeEventListener(listener: EventListener): Boolean {
|
||||
eventListeners.toList().forEach {
|
||||
if (it.get() == listener) {
|
||||
return eventListeners.remove(it)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
fun onCreate() {}
|
||||
fun onCreateInputView() {}
|
||||
fun onRegisterInputView(inputView: InputView) {}
|
||||
fun onDestroy() {}
|
||||
|
||||
fun onStartInputView(info: EditorInfo?, restarting: Boolean) {}
|
||||
fun onStartInputView(instance: EditorInstance, restarting: Boolean) {}
|
||||
fun onFinishInputView(finishingInput: Boolean) {}
|
||||
|
||||
fun onWindowShown() {}
|
||||
fun onWindowHidden() {}
|
||||
|
||||
fun onConfigurationChanged(newConfig: Configuration) {}
|
||||
|
||||
fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {}
|
||||
fun onUpdateSelection(
|
||||
oldSelStart: Int,
|
||||
oldSelEnd: Int,
|
||||
newSelStart: Int,
|
||||
newSelEnd: Int,
|
||||
candidatesStart: Int,
|
||||
candidatesEnd: Int
|
||||
) {}
|
||||
fun onUpdateSelection() {}
|
||||
|
||||
fun onApplyThemeAttributes() {}
|
||||
fun onPrimaryClipChanged() {}
|
||||
fun onSubtypeChanged(newSubtype: Subtype) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.core
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.ViewFlipper
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
/**
|
||||
* Custom ViewFlipper class used to prevent an unnecessary exception to be thrown when it is
|
||||
* detached from a window.
|
||||
*
|
||||
* Based on the solution of this SO answer: https://stackoverflow.com/a/8208874/6801193
|
||||
*/
|
||||
class FlorisViewFlipper : ViewFlipper {
|
||||
constructor(context: Context) : this(context, null)
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
try {
|
||||
super.onDetachedFromWindow()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
stopFlipping()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,20 +17,33 @@
|
||||
package dev.patrickgold.florisboard.ime.core
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.util.AttributeSet
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.Log
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ViewFlipper
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.util.ViewLayoutUtils
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
/**
|
||||
* Root view of the keyboard. Notifies [FlorisBoard] when it has been attached to a window.
|
||||
*/
|
||||
class InputView : FrameLayout {
|
||||
|
||||
class InputView : LinearLayout {
|
||||
private var florisboard: FlorisBoard = FlorisBoard.getInstance()
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
|
||||
var desiredInputViewHeight: Int = resources.getDimension(R.dimen.inputView_baseHeight).roundToInt()
|
||||
private set
|
||||
var desiredSmartbarHeight: Int = resources.getDimension(R.dimen.smartbar_baseHeight).roundToInt()
|
||||
private set
|
||||
var desiredTextKeyboardViewHeight: Int = resources.getDimension(R.dimen.textKeyboardView_baseHeight).roundToInt()
|
||||
private set
|
||||
var desiredMediaKeyboardViewHeight: Int = resources.getDimension(R.dimen.mediaKeyboardView_baseHeight).roundToInt()
|
||||
private set
|
||||
|
||||
var mainViewFlipper: ViewFlipper? = null
|
||||
private set
|
||||
@@ -41,7 +54,11 @@ class InputView : FrameLayout {
|
||||
|
||||
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)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyleAttr
|
||||
)
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onAttachedToWindow()")
|
||||
@@ -54,4 +71,66 @@ class InputView : FrameLayout {
|
||||
|
||||
florisboard.registerInputView(this)
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val heightFactor = when (resources.configuration.orientation) {
|
||||
Configuration.ORIENTATION_LANDSCAPE -> 1.0f
|
||||
else -> if (prefs.keyboard.oneHandedMode != "off") {
|
||||
0.9f
|
||||
} else {
|
||||
1.0f
|
||||
}
|
||||
} * when (prefs.keyboard.heightFactor) {
|
||||
"extra_short" -> 0.85f
|
||||
"short" -> 0.90f
|
||||
"mid_short" -> 0.95f
|
||||
"normal" -> 1.00f
|
||||
"mid_tall" -> 1.05f
|
||||
"tall" -> 1.10f
|
||||
"extra_tall" -> 1.15f
|
||||
else -> 1.00f
|
||||
}
|
||||
var height = (calcInputViewHeight() * heightFactor).roundToInt()
|
||||
desiredInputViewHeight = height
|
||||
desiredSmartbarHeight = (0.16129 * height).roundToInt()
|
||||
desiredTextKeyboardViewHeight = height - desiredSmartbarHeight
|
||||
desiredMediaKeyboardViewHeight = height
|
||||
// Add bottom offset for curved screens here. As the desired heights have already been set,
|
||||
// adding a value to the height now will result in a bottom padding (aka offset).
|
||||
height += ViewLayoutUtils.convertDpToPixel(
|
||||
florisboard.prefs.keyboard.bottomOffset.toFloat(),
|
||||
context
|
||||
).toInt()
|
||||
|
||||
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY))
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the input view height based on the current screen dimensions and the auto
|
||||
* selected dimension values.
|
||||
*
|
||||
* This method and the fraction values have been inspired by [OpenBoard](https://github.com/dslul/openboard)
|
||||
* but are not 1:1 the same. This implementation differs from the
|
||||
* [original](https://github.com/dslul/openboard/blob/90ae4c8aec034a8935e1fd02b441be25c7dba6ce/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/ResourceUtils.java)
|
||||
* by calculating the average of the min and max height values, then taking at least the input
|
||||
* view base height and return this resulting value.
|
||||
*/
|
||||
private fun calcInputViewHeight(): Float {
|
||||
val dm: DisplayMetrics = resources.displayMetrics
|
||||
val minBaseSize: Float = when (resources.configuration.orientation) {
|
||||
Configuration.ORIENTATION_LANDSCAPE -> resources.getFraction(
|
||||
R.fraction.inputView_minHeightFraction, dm.heightPixels, dm.heightPixels
|
||||
)
|
||||
else -> resources.getFraction(
|
||||
R.fraction.inputView_minHeightFraction, dm.widthPixels, dm.widthPixels
|
||||
)
|
||||
}
|
||||
val maxBaseSize: Float = resources.getFraction(
|
||||
R.fraction.inputView_maxHeightFraction, dm.heightPixels, dm.heightPixels
|
||||
)
|
||||
return ((minBaseSize + maxBaseSize) / 2.0f).coerceAtLeast(
|
||||
resources.getDimension(R.dimen.inputView_baseHeight)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.core
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ViewFlipper
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.R
|
||||
|
||||
/**
|
||||
* Root view of the keyboard.
|
||||
*/
|
||||
class InputWindowView : FrameLayout {
|
||||
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)
|
||||
}
|
||||
@@ -21,6 +21,9 @@ import android.content.SharedPreferences
|
||||
import android.provider.Settings
|
||||
import androidx.preference.PreferenceManager
|
||||
import dev.patrickgold.florisboard.R
|
||||
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.util.VersionName
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
@@ -37,9 +40,11 @@ class PrefHelper(
|
||||
|
||||
val advanced = Advanced(this)
|
||||
val correction = Correction(this)
|
||||
val gestures = Gestures(this)
|
||||
val glide = Glide(this)
|
||||
val internal = Internal(this)
|
||||
val keyboard = Keyboard(this)
|
||||
val looknfeel = Looknfeel(this)
|
||||
val localization = Localization(this)
|
||||
val suggestion = Suggestion(this)
|
||||
val theme = Theme(this)
|
||||
|
||||
@@ -110,15 +115,28 @@ class PrefHelper(
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var defaultInstance: PrefHelper? = null
|
||||
|
||||
@Synchronized
|
||||
fun getDefaultInstance(context: Context): PrefHelper {
|
||||
if (defaultInstance == null) {
|
||||
defaultInstance = PrefHelper(context)
|
||||
}
|
||||
return defaultInstance!!
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the [PreferenceManager] to set the defined preferences to their default values, if
|
||||
* they have not been initialized yet.
|
||||
*/
|
||||
fun initDefaultPreferences() {
|
||||
PreferenceManager.setDefaultValues(context, R.xml.prefs_advanced, true)
|
||||
PreferenceManager.setDefaultValues(context, R.xml.prefs_gestures, true)
|
||||
PreferenceManager.setDefaultValues(context, R.xml.prefs_keyboard, true)
|
||||
PreferenceManager.setDefaultValues(context, R.xml.prefs_looknfeel, true)
|
||||
PreferenceManager.setDefaultValues(context, R.xml.prefs_theme, true)
|
||||
PreferenceManager.setDefaultValues(context, R.xml.prefs_typing, true)
|
||||
//setPref(Keyboard.SUBTYPES, "")
|
||||
//setPref(Internal.IS_IME_SET_UP, false)
|
||||
}
|
||||
@@ -128,10 +146,10 @@ class PrefHelper(
|
||||
*/
|
||||
fun sync() {
|
||||
val contentResolver = context.contentResolver
|
||||
looknfeel.soundEnabledSystem = Settings.System.getInt(
|
||||
keyboard.soundEnabledSystem = Settings.System.getInt(
|
||||
contentResolver, Settings.System.SOUND_EFFECTS_ENABLED, 0
|
||||
) != 0
|
||||
looknfeel.vibrationEnabledSystem = Settings.System.getInt(
|
||||
keyboard.vibrationEnabledSystem = Settings.System.getInt(
|
||||
contentResolver, Settings.System.HAPTIC_FEEDBACK_ENABLED, 0
|
||||
) != 0
|
||||
|
||||
@@ -162,37 +180,120 @@ class PrefHelper(
|
||||
*/
|
||||
class Correction(private val prefHelper: PrefHelper) {
|
||||
companion object {
|
||||
const val DOUBLE_SPACE_PERIOD = "correction__double_space_period"
|
||||
const val AUTO_CAPITALIZATION = "correction__auto_capitalization"
|
||||
const val DOUBLE_SPACE_PERIOD = "correction__double_space_period"
|
||||
const val REMEMBER_CAPS_LOCK_STATE = "correction__remember_caps_lock_state"
|
||||
}
|
||||
|
||||
var doubleSpacePeriod: Boolean = false
|
||||
get() = prefHelper.getPref(DOUBLE_SPACE_PERIOD, true)
|
||||
private set
|
||||
var autoCapitalization: Boolean
|
||||
get() = prefHelper.getPref(AUTO_CAPITALIZATION, true)
|
||||
set(v) = prefHelper.setPref(AUTO_CAPITALIZATION, v)
|
||||
var doubleSpacePeriod: Boolean
|
||||
get() = prefHelper.getPref(DOUBLE_SPACE_PERIOD, true)
|
||||
set(v) = prefHelper.setPref(DOUBLE_SPACE_PERIOD, v)
|
||||
var rememberCapsLockState: Boolean
|
||||
get() = prefHelper.getPref(REMEMBER_CAPS_LOCK_STATE, false)
|
||||
set(v) = prefHelper.setPref(REMEMBER_CAPS_LOCK_STATE, v)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class for internal preferences.
|
||||
* Wrapper class for gestures preferences.
|
||||
*/
|
||||
class Gestures(private val prefHelper: PrefHelper) {
|
||||
companion object {
|
||||
const val SWIPE_UP = "gestures__swipe_up"
|
||||
const val SWIPE_DOWN = "gestures__swipe_down"
|
||||
const val SWIPE_LEFT = "gestures__swipe_left"
|
||||
const val SWIPE_RIGHT = "gestures__swipe_right"
|
||||
const val SPACE_BAR_SWIPE_LEFT = "gestures__space_bar_swipe_left"
|
||||
const val SPACE_BAR_SWIPE_RIGHT = "gestures__space_bar_swipe_right"
|
||||
const val DELETE_KEY_SWIPE_LEFT = "gestures__delete_key_swipe_left"
|
||||
const val SWIPE_VELOCITY_THRESHOLD = "gestures__swipe_velocity_threshold"
|
||||
const val SWIPE_DISTANCE_THRESHOLD = "gestures__swipe_distance_threshold"
|
||||
}
|
||||
|
||||
var swipeUp: SwipeAction
|
||||
get() = SwipeAction.fromString(prefHelper.getPref(SWIPE_UP, "no_action"))
|
||||
set(v) = prefHelper.setPref(SWIPE_UP, v)
|
||||
var swipeDown: SwipeAction
|
||||
get() = SwipeAction.fromString(prefHelper.getPref(SWIPE_DOWN, "no_action"))
|
||||
set(v) = prefHelper.setPref(SWIPE_DOWN, v)
|
||||
var swipeLeft: SwipeAction
|
||||
get() = SwipeAction.fromString(prefHelper.getPref(SWIPE_LEFT, "no_action"))
|
||||
set(v) = prefHelper.setPref(SWIPE_LEFT, v)
|
||||
var swipeRight: SwipeAction
|
||||
get() = SwipeAction.fromString(prefHelper.getPref(SWIPE_RIGHT, "no_action"))
|
||||
set(v) = prefHelper.setPref(SWIPE_RIGHT, v)
|
||||
var spaceBarSwipeLeft: SwipeAction
|
||||
get() = SwipeAction.fromString(prefHelper.getPref(SPACE_BAR_SWIPE_LEFT, "no_action"))
|
||||
set(v) = prefHelper.setPref(SPACE_BAR_SWIPE_LEFT, v)
|
||||
var spaceBarSwipeRight: SwipeAction
|
||||
get() = SwipeAction.fromString(prefHelper.getPref(SPACE_BAR_SWIPE_RIGHT, "no_action"))
|
||||
set(v) = prefHelper.setPref(SPACE_BAR_SWIPE_RIGHT, v)
|
||||
var deleteKeySwipeLeft: SwipeAction
|
||||
get() = SwipeAction.fromString(prefHelper.getPref(DELETE_KEY_SWIPE_LEFT, "no_action"))
|
||||
set(v) = prefHelper.setPref(DELETE_KEY_SWIPE_LEFT, v)
|
||||
var swipeVelocityThreshold: VelocityThreshold
|
||||
get() = VelocityThreshold.fromString(prefHelper.getPref(SWIPE_VELOCITY_THRESHOLD, "normal"))
|
||||
set(v) = prefHelper.setPref(SWIPE_VELOCITY_THRESHOLD, v)
|
||||
var swipeDistanceThreshold: DistanceThreshold
|
||||
get() = DistanceThreshold.fromString(prefHelper.getPref(SWIPE_DISTANCE_THRESHOLD, "normal"))
|
||||
set(v) = prefHelper.setPref(SWIPE_DISTANCE_THRESHOLD, v)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class for glide preferences.
|
||||
*/
|
||||
class Glide(private val prefHelper: PrefHelper) {
|
||||
companion object {
|
||||
const val ENABLED = "glide__enabled"
|
||||
const val SHOW_TRAIL = "glide__show_trail"
|
||||
}
|
||||
|
||||
var enabled: Boolean
|
||||
get() = prefHelper.getPref(ENABLED, false)
|
||||
set(v) = prefHelper.setPref(ENABLED, v)
|
||||
var showTrail: Boolean
|
||||
get() = prefHelper.getPref(SHOW_TRAIL, false)
|
||||
set(v) = prefHelper.setPref(SHOW_TRAIL, v)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class for internal preferences. A preference qualifies as an internal pref if the
|
||||
* user has no ability to control this preference's value directly (via a UI pref view).
|
||||
*/
|
||||
class Internal(private val prefHelper: PrefHelper) {
|
||||
companion object {
|
||||
const val IS_IME_SET_UP = "internal__is_ime_set_up"
|
||||
const val VERSION_ON_INSTALL = "internal__version_on_install"
|
||||
const val VERSION_LAST_USE = "internal__version_last_use"
|
||||
const val VERSION_LAST_CHANGELOG = "internal__version_last_changelog"
|
||||
const val IS_IME_SET_UP = "internal__is_ime_set_up"
|
||||
const val THEME_CURRENT_BASED_ON = "internal__theme_current_based_on"
|
||||
const val THEME_CURRENT_IS_MODIFIED = "internal__theme_current_is_modified"
|
||||
const val THEME_CURRENT_IS_NIGHT = "internal__theme_current_is_night"
|
||||
const val VERSION_ON_INSTALL = "internal__version_on_install"
|
||||
const val VERSION_LAST_USE = "internal__version_last_use"
|
||||
const val VERSION_LAST_CHANGELOG = "internal__version_last_changelog"
|
||||
}
|
||||
|
||||
var isImeSetUp: Boolean
|
||||
get() = prefHelper.getPref(IS_IME_SET_UP, false)
|
||||
set(value) = prefHelper.setPref(IS_IME_SET_UP, value)
|
||||
get() = prefHelper.getPref(IS_IME_SET_UP, false)
|
||||
set(v) = prefHelper.setPref(IS_IME_SET_UP, v)
|
||||
var themeCurrentBasedOn: String
|
||||
get() = prefHelper.getPref(THEME_CURRENT_BASED_ON, "undefined")
|
||||
set(v) = prefHelper.setPref(THEME_CURRENT_BASED_ON, v)
|
||||
var themeCurrentIsModified: Boolean
|
||||
get() = prefHelper.getPref(THEME_CURRENT_IS_MODIFIED, false)
|
||||
set(v) = prefHelper.setPref(THEME_CURRENT_IS_MODIFIED, v)
|
||||
var themeCurrentIsNight: Boolean
|
||||
get() = prefHelper.getPref(THEME_CURRENT_IS_NIGHT, false)
|
||||
set(v) = prefHelper.setPref(THEME_CURRENT_IS_NIGHT, v)
|
||||
var versionOnInstall: String
|
||||
get() = prefHelper.getPref(VERSION_ON_INSTALL, VersionName.DEFAULT_RAW)
|
||||
set(value) = prefHelper.setPref(VERSION_ON_INSTALL, value)
|
||||
get() = prefHelper.getPref(VERSION_ON_INSTALL, VersionName.DEFAULT_RAW)
|
||||
set(v) = prefHelper.setPref(VERSION_ON_INSTALL, v)
|
||||
var versionLastUse: String
|
||||
get() = prefHelper.getPref(VERSION_LAST_USE, VersionName.DEFAULT_RAW)
|
||||
set(value) = prefHelper.setPref(VERSION_LAST_USE, value)
|
||||
get() = prefHelper.getPref(VERSION_LAST_USE, VersionName.DEFAULT_RAW)
|
||||
set(v) = prefHelper.setPref(VERSION_LAST_USE, v)
|
||||
var versionLastChangelog: String
|
||||
get() = prefHelper.getPref(VERSION_LAST_CHANGELOG, VersionName.DEFAULT_RAW)
|
||||
set(value) = prefHelper.setPref(VERSION_LAST_CHANGELOG, value)
|
||||
get() = prefHelper.getPref(VERSION_LAST_CHANGELOG, VersionName.DEFAULT_RAW)
|
||||
set(v) = prefHelper.setPref(VERSION_LAST_CHANGELOG, v)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -200,72 +301,96 @@ class PrefHelper(
|
||||
*/
|
||||
class Keyboard(private val prefHelper: PrefHelper) {
|
||||
companion object {
|
||||
const val ACTIVE_SUBTYPE_ID = "keyboard__active_subtype_id"
|
||||
const val SUBTYPES = "keyboard__subtypes"
|
||||
}
|
||||
|
||||
var activeSubtypeId: Int
|
||||
get() = prefHelper.getPref(ACTIVE_SUBTYPE_ID, -1)
|
||||
set(v) = prefHelper.setPref(ACTIVE_SUBTYPE_ID, v)
|
||||
var subtypes: String
|
||||
get() = prefHelper.getPref(SUBTYPES, "")
|
||||
set(v) = prefHelper.setPref(SUBTYPES, v)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class for looknfeel preferences.
|
||||
*/
|
||||
class Looknfeel(private val prefHelper: PrefHelper) {
|
||||
companion object {
|
||||
const val HEIGHT_FACTOR = "looknfeel__height_factor"
|
||||
const val LONG_PRESS_DELAY = "looknfeel__long_press_delay"
|
||||
const val ONE_HANDED_MODE = "looknfeel__one_handed_mode"
|
||||
const val SOUND_ENABLED = "looknfeel__sound_enabled"
|
||||
const val SOUND_VOLUME = "looknfeel__sound_volume"
|
||||
const val VIBRATION_ENABLED = "looknfeel__vibration_enabled"
|
||||
const val VIBRATION_STRENGTH = "looknfeel__vibration_strength"
|
||||
const val BOTTOM_OFFSET = "keyboard__bottom_offset"
|
||||
const val HEIGHT_FACTOR = "keyboard__height_factor"
|
||||
const val HINTED_NUMBER_ROW = "keyboard__hinted_number_row"
|
||||
const val HINTED_SYMBOLS = "keyboard__hinted_symbols"
|
||||
const val LONG_PRESS_DELAY = "keyboard__long_press_delay"
|
||||
const val ONE_HANDED_MODE = "keyboard__one_handed_mode"
|
||||
const val POPUP_ENABLED = "keyboard__popup_enabled"
|
||||
const val SOUND_ENABLED = "keyboard__sound_enabled"
|
||||
const val SOUND_VOLUME = "keyboard__sound_volume"
|
||||
const val VIBRATION_ENABLED = "keyboard__vibration_enabled"
|
||||
const val VIBRATION_STRENGTH = "keyboard__vibration_strength"
|
||||
}
|
||||
|
||||
var bottomOffset: Int = 0
|
||||
get() = prefHelper.getPref(BOTTOM_OFFSET, 0)
|
||||
private set
|
||||
var heightFactor: String = ""
|
||||
get() = prefHelper.getPref(HEIGHT_FACTOR, "normal")
|
||||
private set
|
||||
var hintedNumberRow: Boolean
|
||||
get() = prefHelper.getPref(HINTED_NUMBER_ROW, true)
|
||||
set(v) = prefHelper.setPref(HINTED_NUMBER_ROW, v)
|
||||
var hintedSymbols: Boolean
|
||||
get() = prefHelper.getPref(HINTED_SYMBOLS, true)
|
||||
set(v) = prefHelper.setPref(HINTED_SYMBOLS, v)
|
||||
var longPressDelay: Int = 0
|
||||
get() = prefHelper.getPref(LONG_PRESS_DELAY, 300)
|
||||
private set
|
||||
var oneHandedMode: String
|
||||
get() = prefHelper.getPref(ONE_HANDED_MODE, "off")
|
||||
set(value) = prefHelper.setPref(ONE_HANDED_MODE, value)
|
||||
var popupEnabled: Boolean = false
|
||||
get() = prefHelper.getPref(POPUP_ENABLED, true)
|
||||
private set
|
||||
var soundEnabled: Boolean = false
|
||||
get() = prefHelper.getPref(SOUND_ENABLED, true)
|
||||
private set
|
||||
var soundEnabledSystem: Boolean = false
|
||||
var soundVolume: Int = 0
|
||||
get() = prefHelper.getPref(SOUND_VOLUME, 0)
|
||||
get() = prefHelper.getPref(SOUND_VOLUME, -1)
|
||||
private set
|
||||
var vibrationEnabled: Boolean = false
|
||||
get() = prefHelper.getPref(VIBRATION_ENABLED, true)
|
||||
private set
|
||||
var vibrationEnabledSystem: Boolean = false
|
||||
var vibrationStrength: Int = 0
|
||||
get() = prefHelper.getPref(VIBRATION_STRENGTH, 0)
|
||||
get() = prefHelper.getPref(VIBRATION_STRENGTH, -1)
|
||||
private set
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class for localization preferences.
|
||||
*/
|
||||
class Localization(private val prefHelper: PrefHelper) {
|
||||
companion object {
|
||||
const val ACTIVE_SUBTYPE_ID = "localization__active_subtype_id"
|
||||
const val SUBTYPES = "localization__subtypes"
|
||||
}
|
||||
|
||||
var activeSubtypeId: Int
|
||||
get() = prefHelper.getPref(ACTIVE_SUBTYPE_ID, Subtype.DEFAULT.id)
|
||||
set(v) = prefHelper.setPref(ACTIVE_SUBTYPE_ID, v)
|
||||
var subtypes: String
|
||||
get() = prefHelper.getPref(SUBTYPES, "")
|
||||
set(v) = prefHelper.setPref(SUBTYPES, v)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class for suggestion preferences.
|
||||
*/
|
||||
class Suggestion(private val prefHelper: PrefHelper) {
|
||||
companion object {
|
||||
const val ENABLED = "suggestion__enabled"
|
||||
const val USE_PREV_WORDS = "suggestion__use_prev_words"
|
||||
const val ENABLED = "suggestion__enabled"
|
||||
const val SHOW_INSTEAD = "suggestion__show_instead"
|
||||
const val SUGGEST_CLIPBOARD_CONTENT = "suggestion__suggest_clipboard_content"
|
||||
const val USE_PREV_WORDS = "suggestion__use_prev_words"
|
||||
}
|
||||
|
||||
var enabled: Boolean = false
|
||||
get() = prefHelper.getPref(ENABLED, true)
|
||||
private set
|
||||
var usePrevWords: Boolean = false
|
||||
get() = prefHelper.getPref(USE_PREV_WORDS, true)
|
||||
private set
|
||||
var enabled: Boolean
|
||||
get() = prefHelper.getPref(ENABLED, true)
|
||||
set(v) = prefHelper.setPref(ENABLED, v)
|
||||
var showInstead: String
|
||||
get() = prefHelper.getPref(SHOW_INSTEAD, "number_row")
|
||||
set(v) = prefHelper.setPref(SHOW_INSTEAD, 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)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -273,18 +398,119 @@ class PrefHelper(
|
||||
*/
|
||||
class Theme(private val prefHelper: PrefHelper) {
|
||||
companion object {
|
||||
const val NAME = "theme__name"
|
||||
const val COLOR_PRIMARY = "theme__colorPrimary"
|
||||
const val COLOR_PRIMARY_DARK = "theme__colorPrimaryDark"
|
||||
const val COLOR_ACCENT = "theme__colorAccent"
|
||||
const val NAV_BAR_COLOR = "theme__navBarColor"
|
||||
const val NAV_BAR_IS_LIGHT = "theme__navBarIsLight"
|
||||
const val KEYBOARD_BG_COLOR = "theme__keyboard_bgColor"
|
||||
const val KEY_BG_COLOR = "theme__key_bgColor"
|
||||
const val KEY_BG_COLOR_PRESSED = "theme__key_bgColorPressed"
|
||||
const val KEY_FG_COLOR = "theme__key_fgColor"
|
||||
const val KEY_ENTER_BG_COLOR = "theme__keyEnter_bgColor"
|
||||
const val KEY_ENTER_BG_COLOR_PRESSED = "theme__keyEnter_bgColorPressed"
|
||||
const val KEY_ENTER_FG_COLOR = "theme__keyEnter_fgColor"
|
||||
const val KEY_SHIFT_BG_COLOR = "theme__keyShift_bgColor"
|
||||
const val KEY_SHIFT_BG_COLOR_PRESSED = "theme__keyShift_bgColorPressed"
|
||||
const val KEY_SHIFT_FG_COLOR = "theme__keyShift_fgColor"
|
||||
const val KEY_SHIFT_FG_COLOR_CAPSLOCK = "theme__keyShift_fgColorCapsLock"
|
||||
const val KEY_POPUP_BG_COLOR = "theme__keyPopup_bgColor"
|
||||
const val KEY_POPUP_BG_COLOR_ACTIVE = "theme__keyPopup_bgColorActive"
|
||||
const val KEY_POPUP_FG_COLOR = "theme__keyPopup_fgColor"
|
||||
const val MEDIA_FG_COLOR = "theme__media_fgColor"
|
||||
const val MEDIA_FG_COLOR_ALT = "theme__media_fgColorAlt"
|
||||
const val ONE_HANDED_BG_COLOR = "theme__oneHanded_bgColor"
|
||||
const val ONE_HANDED_BUTTON_FG_COLOR = "theme__oneHandedButton_fgColor"
|
||||
const val SMARTBAR_BG_COLOR = "theme__smartbar_bgColor"
|
||||
const val SMARTBAR_FG_COLOR = "theme__smartbar_fgColor"
|
||||
const val SMARTBAR_FG_COLOR_ALT = "theme__smartbar_fgColorAlt"
|
||||
const val SMARTBAR_BUTTON_BG_COLOR = "theme__smartbarButton_bgColor"
|
||||
const val SMARTBAR_BUTTON_FG_COLOR = "theme__smartbarButton_fgColor"
|
||||
}
|
||||
|
||||
var name: String = ""
|
||||
get() = prefHelper.getPref(NAME, "floris_light")
|
||||
private set
|
||||
fun getSelectedThemeResId(): Int {
|
||||
return when (name) {
|
||||
"floris_light" -> R.style.KeyboardTheme_FlorisLight
|
||||
"floris_dark" -> R.style.KeyboardTheme_FlorisDark
|
||||
else -> R.style.KeyboardTheme_FlorisLight
|
||||
}
|
||||
}
|
||||
var colorPrimary: Int
|
||||
get() = prefHelper.getPref(COLOR_PRIMARY, 0)
|
||||
set(v) = prefHelper.setPref(COLOR_PRIMARY, v)
|
||||
var colorPrimaryDark: Int
|
||||
get() = prefHelper.getPref(COLOR_PRIMARY_DARK, 0)
|
||||
set(v) = prefHelper.setPref(COLOR_PRIMARY_DARK, v)
|
||||
var colorAccent: Int
|
||||
get() = prefHelper.getPref(COLOR_ACCENT, 0)
|
||||
set(v) = prefHelper.setPref(COLOR_ACCENT, v)
|
||||
var navBarColor: Int
|
||||
get() = prefHelper.getPref(NAV_BAR_COLOR, 0)
|
||||
set(v) = prefHelper.setPref(NAV_BAR_COLOR, v)
|
||||
var navBarIsLight: Boolean
|
||||
get() = prefHelper.getPref(NAV_BAR_IS_LIGHT, false)
|
||||
set(v) = prefHelper.setPref(NAV_BAR_IS_LIGHT, v)
|
||||
var keyboardBgColor: Int
|
||||
get() = prefHelper.getPref(KEYBOARD_BG_COLOR, 0)
|
||||
set(v) = prefHelper.setPref(KEYBOARD_BG_COLOR, v)
|
||||
var keyBgColor: Int
|
||||
get() = prefHelper.getPref(KEY_BG_COLOR, 0)
|
||||
set(v) = prefHelper.setPref(KEY_BG_COLOR, v)
|
||||
var keyBgColorPressed: Int
|
||||
get() = prefHelper.getPref(KEY_BG_COLOR_PRESSED, 0)
|
||||
set(v) = prefHelper.setPref(KEY_BG_COLOR_PRESSED, v)
|
||||
var keyFgColor: Int
|
||||
get() = prefHelper.getPref(KEY_FG_COLOR, 0)
|
||||
set(v) = prefHelper.setPref(KEY_FG_COLOR, v)
|
||||
var keyEnterBgColor: Int
|
||||
get() = prefHelper.getPref(KEY_ENTER_BG_COLOR, 0)
|
||||
set(v) = prefHelper.setPref(KEY_ENTER_BG_COLOR, v)
|
||||
var keyEnterBgColorPressed: Int
|
||||
get() = prefHelper.getPref(KEY_ENTER_BG_COLOR_PRESSED, 0)
|
||||
set(v) = prefHelper.setPref(KEY_ENTER_BG_COLOR_PRESSED, v)
|
||||
var keyEnterFgColor: Int
|
||||
get() = prefHelper.getPref(KEY_ENTER_FG_COLOR, 0)
|
||||
set(v) = prefHelper.setPref(KEY_ENTER_FG_COLOR, v)
|
||||
var keyShiftBgColor: Int
|
||||
get() = prefHelper.getPref(KEY_SHIFT_BG_COLOR, 0)
|
||||
set(v) = prefHelper.setPref(KEY_SHIFT_BG_COLOR, v)
|
||||
var keyShiftBgColorPressed: Int
|
||||
get() = prefHelper.getPref(KEY_SHIFT_BG_COLOR_PRESSED, 0)
|
||||
set(v) = prefHelper.setPref(KEY_SHIFT_BG_COLOR_PRESSED, v)
|
||||
var keyShiftFgColor: Int
|
||||
get() = prefHelper.getPref(KEY_SHIFT_FG_COLOR, 0)
|
||||
set(v) = prefHelper.setPref(KEY_SHIFT_FG_COLOR, v)
|
||||
var keyShiftFgColorCapsLock: Int
|
||||
get() = prefHelper.getPref(KEY_SHIFT_FG_COLOR_CAPSLOCK, 0)
|
||||
set(v) = prefHelper.setPref(KEY_SHIFT_FG_COLOR_CAPSLOCK, v)
|
||||
var keyPopupBgColor: Int
|
||||
get() = prefHelper.getPref(KEY_POPUP_BG_COLOR, 0)
|
||||
set(v) = prefHelper.setPref(KEY_POPUP_BG_COLOR, v)
|
||||
var keyPopupBgColorActive: Int
|
||||
get() = prefHelper.getPref(KEY_POPUP_BG_COLOR_ACTIVE, 0)
|
||||
set(v) = prefHelper.setPref(KEY_POPUP_BG_COLOR_ACTIVE, v)
|
||||
var keyPopupFgColor: Int
|
||||
get() = prefHelper.getPref(KEY_POPUP_FG_COLOR, 0)
|
||||
set(v) = prefHelper.setPref(KEY_POPUP_FG_COLOR, v)
|
||||
var mediaFgColor: Int
|
||||
get() = prefHelper.getPref(MEDIA_FG_COLOR, 0)
|
||||
set(v) = prefHelper.setPref(MEDIA_FG_COLOR, v)
|
||||
var mediaFgColorAlt: Int
|
||||
get() = prefHelper.getPref(MEDIA_FG_COLOR_ALT, 0)
|
||||
set(v) = prefHelper.setPref(MEDIA_FG_COLOR_ALT, v)
|
||||
var oneHandedBgColor: Int
|
||||
get() = prefHelper.getPref(ONE_HANDED_BG_COLOR, 0)
|
||||
set(v) = prefHelper.setPref(ONE_HANDED_BG_COLOR, v)
|
||||
var oneHandedButtonFgColor: Int
|
||||
get() = prefHelper.getPref(ONE_HANDED_BUTTON_FG_COLOR, 0)
|
||||
set(v) = prefHelper.setPref(ONE_HANDED_BUTTON_FG_COLOR, v)
|
||||
var smartbarBgColor: Int
|
||||
get() = prefHelper.getPref(SMARTBAR_BG_COLOR, 0)
|
||||
set(v) = prefHelper.setPref(SMARTBAR_BG_COLOR, v)
|
||||
var smartbarFgColor: Int
|
||||
get() = prefHelper.getPref(SMARTBAR_FG_COLOR, 0)
|
||||
set(v) = prefHelper.setPref(SMARTBAR_FG_COLOR, v)
|
||||
var smartbarFgColorAlt: Int
|
||||
get() = prefHelper.getPref(SMARTBAR_FG_COLOR_ALT, 0)
|
||||
set(v) = prefHelper.setPref(SMARTBAR_FG_COLOR_ALT, v)
|
||||
var smartbarButtonBgColor: Int
|
||||
get() = prefHelper.getPref(SMARTBAR_BUTTON_BG_COLOR, 0)
|
||||
set(v) = prefHelper.setPref(SMARTBAR_BUTTON_BG_COLOR, v)
|
||||
var smartbarButtonFgColor: Int
|
||||
get() = prefHelper.getPref(SMARTBAR_BUTTON_FG_COLOR, 0)
|
||||
set(v) = prefHelper.setPref(SMARTBAR_BUTTON_FG_COLOR, v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ class SubtypeManager(
|
||||
var imeConfig: FlorisBoard.ImeConfig = FlorisBoard.ImeConfig(context.packageName)
|
||||
var subtypes: List<Subtype>
|
||||
get() {
|
||||
val listRaw = prefs.keyboard.subtypes
|
||||
val listRaw = prefs.localization.subtypes
|
||||
return if (listRaw.isBlank()) {
|
||||
listOf()
|
||||
} else {
|
||||
@@ -58,7 +58,7 @@ class SubtypeManager(
|
||||
}
|
||||
}
|
||||
set(v) {
|
||||
prefs.keyboard.subtypes = v.joinToString(SUBTYPE_LIST_STR_DELIMITER)
|
||||
prefs.localization.subtypes = v.joinToString(SUBTYPE_LIST_STR_DELIMITER)
|
||||
}
|
||||
|
||||
init {
|
||||
@@ -71,7 +71,7 @@ class SubtypeManager(
|
||||
* Loads the [FlorisBoard.ImeConfig] from ime/config.json.
|
||||
*
|
||||
* @param path The path to to IME config file.
|
||||
* @returns The [FlorisBoard.ImeConfig] or a default config.
|
||||
* @return The [FlorisBoard.ImeConfig] or a default config.
|
||||
*/
|
||||
private fun loadImeConfig(path: String): FlorisBoard.ImeConfig {
|
||||
val rawJsonData: String = try {
|
||||
@@ -93,7 +93,7 @@ class SubtypeManager(
|
||||
* Adds a given [subtypeToAdd] to the subtype list, if it does not exist.
|
||||
*
|
||||
* @param subtypeToAdd The subtype which should be added.
|
||||
* @returns True if the subtype was added, false otherwise. A return value of false indicates
|
||||
* @return True if the subtype was added, false otherwise. A return value of false indicates
|
||||
* that the subtype already exists.
|
||||
*/
|
||||
private fun addSubtype(subtypeToAdd: Subtype): Boolean {
|
||||
@@ -112,7 +112,7 @@ class SubtypeManager(
|
||||
*
|
||||
* @param locale The locale of the subtype to be added.
|
||||
* @param layoutName The layout name of the subtype to be added.
|
||||
* @returns True if the subtype was added, false otherwise. A return value of false indicates
|
||||
* @return True if the subtype was added, false otherwise. A return value of false indicates
|
||||
* that the subtype already exists.
|
||||
*/
|
||||
fun addSubtype(locale: Locale, layoutName: String): Boolean {
|
||||
@@ -129,21 +129,21 @@ class SubtypeManager(
|
||||
* Gets the active subtype and returns it. If the activeSubtypeId points to a non-existent
|
||||
* subtype, this method tries to determine a new active subtype.
|
||||
*
|
||||
* @returns The active subtype or null, if the subtype list is empty or no new active subtype
|
||||
* @return The active subtype or null, if the subtype list is empty or no new active subtype
|
||||
* could be determined.
|
||||
*/
|
||||
fun getActiveSubtype(): Subtype? {
|
||||
for (subtype in subtypes) {
|
||||
if (subtype.id == prefs.keyboard.activeSubtypeId) {
|
||||
if (subtype.id == prefs.localization.activeSubtypeId) {
|
||||
return subtype
|
||||
}
|
||||
}
|
||||
val subtypeList = subtypes
|
||||
return if (subtypeList.isNotEmpty()) {
|
||||
prefs.keyboard.activeSubtypeId = subtypeList[0].id
|
||||
prefs.localization.activeSubtypeId = subtypeList[0].id
|
||||
subtypeList[0]
|
||||
} else {
|
||||
prefs.keyboard.activeSubtypeId = -1
|
||||
prefs.localization.activeSubtypeId = Subtype.DEFAULT.id
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -152,7 +152,7 @@ class SubtypeManager(
|
||||
* Gets a subtype by the given [id].
|
||||
*
|
||||
* @param id The id of the subtype you want to get.
|
||||
* @returns The subtype or null, if no matching subtype could be found.
|
||||
* @return The subtype or null, if no matching subtype could be found.
|
||||
*/
|
||||
fun getSubtypeById(id: Int): Subtype? {
|
||||
for (subtype in subtypes) {
|
||||
@@ -167,7 +167,7 @@ class SubtypeManager(
|
||||
* Gets the default system subtype for a given [locale].
|
||||
*
|
||||
* @param locale The locale of the default system subtype to get.
|
||||
* @returns The default system locale or null, if no matching default system subtype could be
|
||||
* @return The default system locale or null, if no matching default system subtype could be
|
||||
* found.
|
||||
*/
|
||||
fun getDefaultSubtypeForLocale(locale: Locale): DefaultSubtype? {
|
||||
@@ -212,15 +212,43 @@ class SubtypeManager(
|
||||
}
|
||||
}
|
||||
subtypes = subtypeList
|
||||
if (subtypeToRemove.id == prefs.keyboard.activeSubtypeId) {
|
||||
if (subtypeToRemove.id == prefs.localization.activeSubtypeId) {
|
||||
getActiveSubtype()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to the previous subtype in the subtype list if possible.
|
||||
*
|
||||
* @return The new active subtype or null if the determination process failed.
|
||||
*/
|
||||
fun switchToPrevSubtype(): Subtype? {
|
||||
val subtypeList = subtypes
|
||||
val activeSubtype = getActiveSubtype() ?: return null
|
||||
var triggerNextSubtype = false
|
||||
var newActiveSubtype: Subtype? = null
|
||||
for (subtype in subtypeList.reversed()) {
|
||||
if (triggerNextSubtype) {
|
||||
triggerNextSubtype = false
|
||||
newActiveSubtype = subtype
|
||||
} else if (subtype == activeSubtype) {
|
||||
triggerNextSubtype = true
|
||||
}
|
||||
}
|
||||
if (triggerNextSubtype) {
|
||||
newActiveSubtype = subtypeList.last()
|
||||
}
|
||||
prefs.localization.activeSubtypeId = when (newActiveSubtype) {
|
||||
null -> Subtype.DEFAULT.id
|
||||
else -> newActiveSubtype.id
|
||||
}
|
||||
return newActiveSubtype
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to the next subtype in the subtype list if possible.
|
||||
*
|
||||
* @returns The new active subtype or null if the determination process failed.
|
||||
* @return The new active subtype or null if the determination process failed.
|
||||
*/
|
||||
fun switchToNextSubtype(): Subtype? {
|
||||
val subtypeList = subtypes
|
||||
@@ -236,10 +264,10 @@ class SubtypeManager(
|
||||
}
|
||||
}
|
||||
if (triggerNextSubtype) {
|
||||
newActiveSubtype = subtypeList[0]
|
||||
newActiveSubtype = subtypeList.first()
|
||||
}
|
||||
prefs.keyboard.activeSubtypeId = when (newActiveSubtype) {
|
||||
null -> -1
|
||||
prefs.localization.activeSubtypeId = when (newActiveSubtype) {
|
||||
null -> Subtype.DEFAULT.id
|
||||
else -> newActiveSubtype.id
|
||||
}
|
||||
return newActiveSubtype
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.widget.*
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.EditorInstance
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.InputView
|
||||
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyData
|
||||
@@ -50,6 +51,8 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
FlorisBoard.EventListener {
|
||||
|
||||
private val florisboard = FlorisBoard.getInstance()
|
||||
private val activeEditorInstance: EditorInstance
|
||||
get() = florisboard.activeEditorInstance
|
||||
|
||||
private var activeTab: Tab? = null
|
||||
private var mediaViewFlipper: ViewFlipper? = null
|
||||
@@ -71,6 +74,10 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
florisboard.addEventListener(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new input view has been registered. Used to initialize all media-relevant
|
||||
* views and layouts.
|
||||
@@ -104,15 +111,12 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
override fun onTabReselected(tab: TabLayout.Tab) {}
|
||||
})
|
||||
|
||||
for (tab in Tab.values()) {
|
||||
val tabView = createTabViewFor(tab)
|
||||
tabViews[tab] = tabView
|
||||
withContext(Dispatchers.Main) {
|
||||
withContext(Dispatchers.Main) {
|
||||
for (tab in Tab.values()) {
|
||||
val tabView = createTabViewFor(tab)
|
||||
tabViews[tab] = tabView
|
||||
mediaViewFlipper?.addView(tabView)
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
tabLayout?.selectTab(tabLayout?.getTabAt(0))
|
||||
}
|
||||
}
|
||||
@@ -195,18 +199,14 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* Sends a given [emojiKeyData] to the current input editor.
|
||||
*/
|
||||
fun sendEmojiKeyPress(emojiKeyData: EmojiKeyData) {
|
||||
val ic = florisboard.currentInputConnection
|
||||
ic?.finishComposingText()
|
||||
ic?.commitText(emojiKeyData.getCodePointsAsString(), 1)
|
||||
activeEditorInstance.commitText(emojiKeyData.getCodePointsAsString())
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a given [emoticonKeyData] to the current input editor.
|
||||
*/
|
||||
fun sendEmoticonKeyPress(emoticonKeyData: EmoticonKeyData) {
|
||||
val ic = florisboard.currentInputConnection
|
||||
ic?.finishComposingText()
|
||||
ic?.commitText(emoticonKeyData.icon, 1)
|
||||
activeEditorInstance.commitText(emoticonKeyData.icon)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.media
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.util.AttributeSet
|
||||
import android.widget.Button
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
|
||||
class MediaInputView : LinearLayout, FlorisBoard.EventListener {
|
||||
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
|
||||
var tabLayout: TabLayout? = null
|
||||
private set
|
||||
var switchToTextInputButton: Button? = null
|
||||
private set
|
||||
var backspaceButton: ImageButton? = null
|
||||
private set
|
||||
|
||||
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) {
|
||||
florisboard?.addEventListener(this)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
tabLayout = findViewById(R.id.media_input_tabs)
|
||||
switchToTextInputButton = findViewById(R.id.media_input_switch_to_text_input_button)
|
||||
backspaceButton = findViewById(R.id.media_input_backspace_button)
|
||||
onApplyThemeAttributes()
|
||||
}
|
||||
|
||||
override fun onApplyThemeAttributes() {
|
||||
tabLayout?.setTabTextColors(prefs.theme.mediaFgColor, prefs.theme.mediaFgColor)
|
||||
tabLayout?.tabIconTint = ColorStateList.valueOf(prefs.theme.mediaFgColor)
|
||||
tabLayout?.setSelectedTabIndicatorColor(prefs.theme.colorPrimary)
|
||||
switchToTextInputButton?.setTextColor(prefs.theme.mediaFgColor)
|
||||
backspaceButton?.imageTintList = ColorStateList.valueOf(prefs.theme.mediaFgColor)
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val height = florisboard?.inputView?.desiredMediaKeyboardViewHeight ?: 0
|
||||
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY))
|
||||
}
|
||||
}
|
||||
@@ -23,12 +23,13 @@ import android.os.Handler
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
import android.view.MotionEvent
|
||||
import android.widget.HorizontalScrollView
|
||||
import android.widget.ScrollView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.BlendModeColorFilterCompat
|
||||
import androidx.core.graphics.BlendModeCompat
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.util.getColorFromAttr
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
|
||||
/**
|
||||
* View class for managing the rendering and the events of a single emoji keyboard key.
|
||||
@@ -40,10 +41,12 @@ import dev.patrickgold.florisboard.util.getColorFromAttr
|
||||
*/
|
||||
@SuppressLint("ViewConstructor")
|
||||
class EmojiKeyView(
|
||||
private val florisboard: FlorisBoard,
|
||||
private val emojiKeyboardView: EmojiKeyboardView,
|
||||
val data: EmojiKeyData
|
||||
) : androidx.appcompat.widget.AppCompatTextView(florisboard.context) {
|
||||
) : androidx.appcompat.widget.AppCompatTextView(emojiKeyboardView.context),
|
||||
FlorisBoard.EventListener {
|
||||
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
|
||||
private var isCancelled: Boolean = false
|
||||
private var osHandler: Handler? = null
|
||||
@@ -55,14 +58,16 @@ class EmojiKeyView(
|
||||
setPadding(0, 0, 0, 0)
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimension(R.dimen.emoji_key_textSize))
|
||||
|
||||
triangleDrawable = resources.getDrawable(
|
||||
R.drawable.triangle_bottom_right, context.theme
|
||||
)
|
||||
triangleDrawable?.colorFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(
|
||||
getColorFromAttr(context, R.attr.emoji_key_fgColor), BlendModeCompat.SRC_ATOP
|
||||
)
|
||||
triangleDrawable = ContextCompat.getDrawable(context, R.drawable.triangle_bottom_right)
|
||||
|
||||
text = data.getCodePointsAsString()
|
||||
|
||||
florisboard?.addEventListener(this)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
onApplyThemeAttributes()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,18 +84,18 @@ class EmojiKeyView(
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
isCancelled = false
|
||||
val delayMillis = florisboard.prefs.looknfeel.longPressDelay
|
||||
val delayMillis = prefs.keyboard.longPressDelay
|
||||
if (osHandler == null) {
|
||||
osHandler = Handler()
|
||||
}
|
||||
osHandler?.postDelayed({
|
||||
(parent.parent as HorizontalScrollView)
|
||||
(parent.parent as ScrollView)
|
||||
.requestDisallowInterceptTouchEvent(true)
|
||||
emojiKeyboardView.isScrollBlocked = true
|
||||
emojiKeyboardView.popupManager.show(this)
|
||||
emojiKeyboardView.popupManager.extend(this)
|
||||
florisboard.keyPressVibrate()
|
||||
florisboard.keyPressSound()
|
||||
florisboard?.keyPressVibrate()
|
||||
florisboard?.keyPressSound()
|
||||
}, delayMillis.toLong())
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
@@ -117,10 +122,10 @@ class EmojiKeyView(
|
||||
if (event.actionMasked != MotionEvent.ACTION_CANCEL &&
|
||||
retData != null && !isCancelled) {
|
||||
if (!emojiKeyboardView.isScrollBlocked) {
|
||||
florisboard.keyPressVibrate()
|
||||
florisboard.keyPressSound()
|
||||
florisboard?.keyPressVibrate()
|
||||
florisboard?.keyPressSound()
|
||||
}
|
||||
florisboard.mediaInputManager.sendEmojiKeyPress(retData)
|
||||
florisboard?.mediaInputManager?.sendEmojiKeyPress(retData)
|
||||
performClick()
|
||||
}
|
||||
if (event.actionMasked == MotionEvent.ACTION_CANCEL) {
|
||||
@@ -131,18 +136,29 @@ class EmojiKeyView(
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
triangleDrawable?.setBounds(
|
||||
(measuredWidth * 0.75f).toInt(),
|
||||
(measuredHeight * 0.75f).toInt(),
|
||||
(measuredWidth * 0.85f).toInt(),
|
||||
(measuredHeight * 0.85f).toInt()
|
||||
)
|
||||
}
|
||||
|
||||
override fun onApplyThemeAttributes() {
|
||||
triangleDrawable?.colorFilter =
|
||||
BlendModeColorFilterCompat.createBlendModeColorFilterCompat(
|
||||
prefs.theme.mediaFgColorAlt, BlendModeCompat.SRC_ATOP
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas?) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
canvas ?: return
|
||||
|
||||
if (data.popup.isNotEmpty()) {
|
||||
triangleDrawable?.setBounds(
|
||||
(measuredWidth * 0.75f).toInt(),
|
||||
(measuredHeight * 0.75f).toInt(),
|
||||
(measuredWidth * 0.85f).toInt(),
|
||||
(measuredHeight * 0.85f).toInt()
|
||||
)
|
||||
triangleDrawable?.draw(canvas)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,19 +18,20 @@ package dev.patrickgold.florisboard.ime.media.emoji
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.HorizontalScrollView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ViewFlipper
|
||||
import android.widget.*
|
||||
import com.google.android.flexbox.FlexDirection
|
||||
import com.google.android.flexbox.FlexWrap
|
||||
import com.google.android.flexbox.FlexboxLayout
|
||||
import com.google.android.flexbox.JustifyContent
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.popup.KeyPopupManager
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.*
|
||||
@@ -42,16 +43,18 @@ import java.util.*
|
||||
*
|
||||
* @property florisboard Reference to instance of core class [FlorisBoard].
|
||||
*/
|
||||
class EmojiKeyboardView : LinearLayout {
|
||||
class EmojiKeyboardView : LinearLayout, FlorisBoard.EventListener {
|
||||
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
|
||||
private var activeCategory: EmojiCategory = EmojiCategory.SMILEYS_EMOTION
|
||||
private var emojiViewFlipper: ViewFlipper
|
||||
private val emojiKeyWidth = resources.getDimension(R.dimen.emoji_key_width).toInt()
|
||||
private val emojiKeyHeight = resources.getDimension(R.dimen.emoji_key_height).toInt()
|
||||
private val florisboard: FlorisBoard = FlorisBoard.getInstance()
|
||||
private var layouts: Deferred<EmojiLayoutDataMap>
|
||||
private val mainScope = MainScope()
|
||||
private val uiLayouts = EnumMap<EmojiCategory, HorizontalScrollView>(EmojiCategory::class.java)
|
||||
private val tabLayout: TabLayout
|
||||
private val uiLayouts = EnumMap<EmojiCategory, ScrollView>(EmojiCategory::class.java)
|
||||
|
||||
var isScrollBlocked: Boolean = false
|
||||
var popupManager = KeyPopupManager<EmojiKeyboardView, EmojiKeyView>(this)
|
||||
@@ -62,18 +65,21 @@ class EmojiKeyboardView : LinearLayout {
|
||||
layouts = mainScope.async(Dispatchers.IO) {
|
||||
parseRawEmojiSpecsFile(context, "ime/media/emoji/emoji-test.txt")
|
||||
}
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
orientation = VERTICAL
|
||||
|
||||
emojiViewFlipper = ViewFlipper(context)
|
||||
emojiViewFlipper.layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
emojiViewFlipper.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, 0).apply {
|
||||
weight = 1.0f
|
||||
}
|
||||
emojiViewFlipper.measureAllChildren = false
|
||||
addView(emojiViewFlipper)
|
||||
|
||||
val tabs =
|
||||
tabLayout =
|
||||
ViewGroup.inflate(context, R.layout.media_input_emoji_tabs, null) as TabLayout
|
||||
tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||
setActiveCategory(when (tab?.position) {
|
||||
0 -> EmojiCategory.SMILEYS_EMOTION
|
||||
@@ -92,7 +98,8 @@ class EmojiKeyboardView : LinearLayout {
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) {}
|
||||
override fun onTabUnselected(tab: TabLayout.Tab?) {}
|
||||
})
|
||||
addView(tabs)
|
||||
addView(tabLayout)
|
||||
florisboard?.addEventListener(this)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
@@ -102,11 +109,7 @@ class EmojiKeyboardView : LinearLayout {
|
||||
buildLayout()
|
||||
setActiveCategory(EmojiCategory.SMILEYS_EMOTION)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
mainScope.cancel()
|
||||
onApplyThemeAttributes()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,10 +119,10 @@ class EmojiKeyboardView : LinearLayout {
|
||||
*/
|
||||
private suspend fun buildLayout() = withContext(Dispatchers.Default) {
|
||||
for (category in EmojiCategory.values()) {
|
||||
val hsv = buildLayoutForCategory(category)
|
||||
uiLayouts[category] = hsv
|
||||
val scrollView = buildLayoutForCategory(category)
|
||||
uiLayouts[category] = scrollView
|
||||
withContext(Dispatchers.Main) {
|
||||
emojiViewFlipper.addView(hsv)
|
||||
emojiViewFlipper.addView(scrollView)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,32 +132,52 @@ class EmojiKeyboardView : LinearLayout {
|
||||
* context and will not block the main UI thread.
|
||||
*
|
||||
* @param category The category for which a layout should be built.
|
||||
* @return The layout (top-most view is a [HorizontalScrollView]).
|
||||
* @return The layout (top-most view is a [ScrollView]).
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private suspend fun buildLayoutForCategory(
|
||||
category: EmojiCategory
|
||||
): HorizontalScrollView = withContext(Dispatchers.Default) {
|
||||
val hsv = HorizontalScrollView(context)
|
||||
hsv.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
||||
): ScrollView = withContext(Dispatchers.Default) {
|
||||
val scrollView = ScrollView(context)
|
||||
scrollView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||
val flexboxLayout = FlexboxLayout(context)
|
||||
flexboxLayout.layoutParams =
|
||||
LayoutParams(LayoutParams.WRAP_CONTENT, emojiKeyHeight * 3)
|
||||
flexboxLayout.flexDirection = FlexDirection.COLUMN
|
||||
LayoutParams(LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
flexboxLayout.flexDirection = FlexDirection.ROW
|
||||
flexboxLayout.justifyContent = JustifyContent.SPACE_BETWEEN
|
||||
flexboxLayout.flexWrap = FlexWrap.WRAP
|
||||
for (emojiKeyData in layouts.await()[category].orEmpty()) {
|
||||
val emojiKeyView =
|
||||
EmojiKeyView(florisboard, this@EmojiKeyboardView, emojiKeyData)
|
||||
EmojiKeyView(this@EmojiKeyboardView, emojiKeyData)
|
||||
emojiKeyView.layoutParams = FlexboxLayout.LayoutParams(
|
||||
emojiKeyWidth, emojiKeyHeight
|
||||
)
|
||||
flexboxLayout.addView(emojiKeyView)
|
||||
}
|
||||
hsv.setOnTouchListener { _, _ ->
|
||||
// Add empty placeholder emojis at the end so the grid view. Below is an illustration how
|
||||
// the UI looks with and without an placeholder (e = emoji):
|
||||
// Without placeholder With placeholder
|
||||
// e e e e e e e e e e e e e e
|
||||
// ............. .............
|
||||
// e e e e e e e e e e e e e e
|
||||
// e e e e e e e e
|
||||
//
|
||||
// Based on this SO's answer idea (by La Nube - Luis R. Díaz Muñiz):
|
||||
// https://stackoverflow.com/a/31478004/6801193
|
||||
//
|
||||
// 24 items are chosen here because that's probably the max items that will be shown per
|
||||
// row, even in landscape mode.
|
||||
for (n in 0 until 24) {
|
||||
val gridPlaceholderView = View(context).apply {
|
||||
layoutParams = LayoutParams(emojiKeyWidth, 0)
|
||||
}
|
||||
flexboxLayout.addView(gridPlaceholderView)
|
||||
}
|
||||
scrollView.setOnTouchListener { _, _ ->
|
||||
return@setOnTouchListener isScrollBlocked
|
||||
}
|
||||
hsv.addView(flexboxLayout)
|
||||
return@withContext hsv
|
||||
scrollView.addView(flexboxLayout)
|
||||
return@withContext scrollView
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -192,4 +215,9 @@ class EmojiKeyboardView : LinearLayout {
|
||||
))
|
||||
isScrollBlocked = true
|
||||
}
|
||||
|
||||
override fun onApplyThemeAttributes() {
|
||||
tabLayout.tabIconTint = ColorStateList.valueOf(prefs.theme.mediaFgColor)
|
||||
tabLayout.setSelectedTabIndicatorColor(prefs.theme.colorAccent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,20 +19,22 @@ package dev.patrickgold.florisboard.ime.popup
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.core.content.ContextCompat.getDrawable
|
||||
import androidx.core.graphics.BlendModeColorFilterCompat
|
||||
import androidx.core.graphics.BlendModeCompat
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.util.*
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
class KeyPopupExtendedSingleView(
|
||||
context: Context, var isActive: Boolean = false
|
||||
context: Context, val adjustedIndex: Int, var isActive: Boolean = false
|
||||
) : androidx.appcompat.widget.AppCompatTextView(
|
||||
context, null, 0
|
||||
) {
|
||||
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
var iconDrawable: Drawable? = null
|
||||
|
||||
init {
|
||||
@@ -40,15 +42,16 @@ class KeyPopupExtendedSingleView(
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas?) {
|
||||
setBackgroundTintColor2(this, when {
|
||||
isActive -> prefs.theme.keyPopupBgColorActive
|
||||
else -> Color.TRANSPARENT
|
||||
})
|
||||
setTextColor(prefs.theme.keyPopupFgColor)
|
||||
|
||||
super.onDraw(canvas)
|
||||
|
||||
canvas ?: return
|
||||
|
||||
setBackgroundTintColor(this, when {
|
||||
isActive -> R.attr.key_popup_extended_bgColorActive
|
||||
else -> R.attr.key_popup_extended_bgColor
|
||||
})
|
||||
|
||||
val drawable = iconDrawable
|
||||
val drawablePadding = (0.2f * measuredHeight).toInt()
|
||||
if (drawable != null) {
|
||||
@@ -65,7 +68,7 @@ class KeyPopupExtendedSingleView(
|
||||
measuredWidth - marginH - drawablePadding,
|
||||
measuredHeight - marginV - drawablePadding)
|
||||
drawable.colorFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(
|
||||
getColorFromAttr(context, R.attr.key_popup_fgColor),
|
||||
prefs.theme.keyPopupFgColor,
|
||||
BlendModeCompat.SRC_ATOP
|
||||
)
|
||||
drawable.draw(canvas)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.popup
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.util.AttributeSet
|
||||
import com.google.android.flexbox.FlexboxLayout
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.util.*
|
||||
|
||||
class KeyPopupExtendedView : FlexboxLayout {
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
|
||||
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 onDraw(canvas: Canvas?) {
|
||||
setBackgroundTintColor2(this, prefs.theme.keyPopupBgColor)
|
||||
super.onDraw(canvas)
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.core.content.ContextCompat.getDrawable
|
||||
import androidx.core.view.get
|
||||
import com.google.android.flexbox.FlexboxLayout
|
||||
import com.google.android.flexbox.JustifyContent
|
||||
import dev.patrickgold.florisboard.R
|
||||
@@ -34,7 +35,6 @@ 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.keyboard.KeyboardView
|
||||
import dev.patrickgold.florisboard.util.setTextTintColor
|
||||
|
||||
/**
|
||||
* Manages the creation and dismissal of key popups as well as the checks if the pointer moved
|
||||
@@ -43,7 +43,6 @@ import dev.patrickgold.florisboard.util.setTextTintColor
|
||||
* @property keyboardView Reference to the keyboard view to which this manager class belongs to.
|
||||
*/
|
||||
class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD) {
|
||||
|
||||
private var anchorLeft: Boolean = false
|
||||
private var anchorRight: Boolean = false
|
||||
private var anchorOffset: Int = 0
|
||||
@@ -102,18 +101,14 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
isInitActive: Boolean = false,
|
||||
isWrapBefore: Boolean = false
|
||||
): KeyPopupExtendedSingleView? {
|
||||
val textView = KeyPopupExtendedSingleView(keyView.context, isInitActive)
|
||||
val lp = FlexboxLayout.LayoutParams(keyPopupWidth, keyView.measuredHeight)
|
||||
val textView = KeyPopupExtendedSingleView(keyView.context, k, isInitActive)
|
||||
val lp = FlexboxLayout.LayoutParams(keyPopupWidth, (keyPopupHeight * 0.4f).toInt())
|
||||
lp.isWrapBefore = isWrapBefore
|
||||
textView.layoutParams = lp
|
||||
textView.gravity = Gravity.CENTER
|
||||
setTextTintColor(
|
||||
textView,
|
||||
R.attr.key_popup_fgColor
|
||||
)
|
||||
val textSize = keyboardView.resources.getDimension(R.dimen.key_popup_textSize)
|
||||
if (keyView is KeyView) {
|
||||
when (keyView.data.popup[k].code) {
|
||||
when (keyView.dataPopupWithHint[k].code) {
|
||||
KeyCode.SETTINGS -> {
|
||||
textView.iconDrawable = getDrawable(
|
||||
keyView.context, R.drawable.ic_settings
|
||||
@@ -134,13 +129,13 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
}
|
||||
else -> {
|
||||
textView.setTextSize(
|
||||
TypedValue.COMPLEX_UNIT_PX, when (keyView.data.popup[k].code) {
|
||||
TypedValue.COMPLEX_UNIT_PX, when (keyView.dataPopupWithHint[k].code) {
|
||||
KeyCode.URI_COMPONENT_TLD,
|
||||
KeyCode.SWITCH_TO_TEXT_CONTEXT -> textSize * 0.6f
|
||||
else -> textSize
|
||||
}
|
||||
)
|
||||
textView.text = keyView.getComputedLetter(keyView.data.popup[k])
|
||||
textView.text = keyView.getComputedLetter(keyView.dataPopupWithHint[k])
|
||||
}
|
||||
}
|
||||
} else if (keyView is EmojiKeyView) {
|
||||
@@ -169,29 +164,29 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a preview popup for the passed [keyView]. Ignores show requests for key views which
|
||||
* key code is equal to or less than [KeyCode.SPACE]. KeyViews with a code defined in
|
||||
* [exceptionsForKeyCodes] will only shadow-calculating the size of the key popup, as these
|
||||
* sizes are needed for the extended popup. No popup will be shown to the user in this case.
|
||||
*
|
||||
* @param keyView Reference to the keyView currently controlling the popup.
|
||||
* Calculates all attributes required by both the normal and the extended popup, regardless of
|
||||
* the passed [keyView]'s code.
|
||||
*/
|
||||
fun show(keyView: T_KV) {
|
||||
if (keyView is KeyView && keyView.data.code <= KeyCode.SPACE
|
||||
&& !exceptionsForKeyCodes.contains(keyView.data.code)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Update keyPopupWidth and keyPopupHeight
|
||||
private fun calc(keyView: T_KV) {
|
||||
if (keyboardView is KeyboardView) {
|
||||
when (keyboardView.resources.configuration.orientation) {
|
||||
Configuration.ORIENTATION_LANDSCAPE -> {
|
||||
keyPopupWidth = (keyboardView.desiredKeyWidth * 0.6f).toInt()
|
||||
keyPopupHeight = (keyboardView.desiredKeyHeight * 3.0f).toInt()
|
||||
if (keyboardView.isSmartbarKeyboardView) {
|
||||
keyPopupWidth = (keyView.measuredWidth * 0.6f).toInt()
|
||||
keyPopupHeight = (keyboardView.desiredKeyHeight * 3.0f * 1.2f).toInt()
|
||||
} else {
|
||||
keyPopupWidth = (keyboardView.desiredKeyWidth * 0.6f).toInt()
|
||||
keyPopupHeight = (keyboardView.desiredKeyHeight * 3.0f).toInt()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
keyPopupWidth = (keyboardView.desiredKeyWidth * 1.1f).toInt()
|
||||
keyPopupHeight = (keyboardView.desiredKeyHeight * 2.5f).toInt()
|
||||
if (keyboardView.isSmartbarKeyboardView) {
|
||||
keyPopupWidth = (keyView.measuredWidth * 1.1f).toInt()
|
||||
keyPopupHeight = (keyboardView.desiredKeyHeight * 2.5f * 1.2f).toInt()
|
||||
} else {
|
||||
keyPopupWidth = (keyboardView.desiredKeyWidth * 1.1f).toInt()
|
||||
keyPopupHeight = (keyboardView.desiredKeyHeight * 2.5f).toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (keyboardView is EmojiKeyboardView) {
|
||||
@@ -199,11 +194,21 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
keyPopupHeight = (keyView.measuredHeight * 2.5f).toInt()
|
||||
}
|
||||
keyPopupDiffX = (keyView.measuredWidth - keyPopupWidth) / 2
|
||||
// Calculating is done, so exit show() here if this key view is a special one.
|
||||
if (keyView is KeyView && exceptionsForKeyCodes.contains(keyView.data.code)) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a preview popup for the passed [keyView]. Ignores show requests for key views which
|
||||
* key code is equal to or less than [KeyCode.SPACE].
|
||||
*
|
||||
* @param keyView Reference to the keyView currently controlling the popup.
|
||||
*/
|
||||
fun show(keyView: T_KV) {
|
||||
if (keyView is KeyView && keyView.data.code <= KeyCode.SPACE) {
|
||||
return
|
||||
}
|
||||
|
||||
calc(keyView)
|
||||
|
||||
val keyPopupX = keyPopupDiffX
|
||||
val keyPopupY = -keyPopupHeight
|
||||
if (window.isShowing) {
|
||||
@@ -216,7 +221,7 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
if (keyView is KeyView) {
|
||||
popupView.findViewById<TextView>(R.id.key_popup_text)?.text = keyView.getComputedLetter()
|
||||
popupView.findViewById<ImageView>(R.id.key_popup_threedots)?.visibility = when {
|
||||
keyView.data.popup.isEmpty() -> View.INVISIBLE
|
||||
keyView.dataPopupWithHint.isEmpty() -> View.INVISIBLE
|
||||
else -> View.VISIBLE
|
||||
}
|
||||
} else if (keyView is EmojiKeyView) {
|
||||
@@ -256,18 +261,17 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
return
|
||||
}
|
||||
|
||||
// Anchor left if keyView is in left half of keyboardView, else anchor right
|
||||
if (keyView is KeyView) {
|
||||
anchorLeft = keyView.x < keyboardView.measuredWidth / 2
|
||||
} else if (keyView is EmojiKeyView) {
|
||||
val hsv = (keyView.parent.parent as HorizontalScrollView)
|
||||
anchorLeft = (keyView.x - hsv.scrollX) < keyboardView.measuredWidth / 2
|
||||
if (!isShowingPopup) {
|
||||
calc(keyView)
|
||||
}
|
||||
|
||||
// Anchor left if keyView is in left half of keyboardView, else anchor right
|
||||
anchorLeft = keyView.x < keyboardView.measuredWidth / 2
|
||||
anchorRight = !anchorLeft
|
||||
|
||||
// Determine key counts for each row
|
||||
val n = when (keyView) {
|
||||
is KeyView -> keyView.data.popup.size
|
||||
is KeyView -> keyView.dataPopupWithHint.size
|
||||
is EmojiKeyView -> keyView.data.popup.size
|
||||
else -> 0
|
||||
}
|
||||
@@ -316,17 +320,29 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
// Build UI
|
||||
popupViewExt.removeAllViews()
|
||||
val indices = when (keyView) {
|
||||
is KeyView -> keyView.data.popup.indices
|
||||
is KeyView -> keyView.dataPopupWithHint.indices
|
||||
is EmojiKeyView -> keyView.data.popup.indices
|
||||
else -> IntRange(0, 0)
|
||||
}
|
||||
var hasShownFirst = false
|
||||
for (k in indices) {
|
||||
val isInitActive =
|
||||
anchorLeft && (k - row1count == anchorOffset) ||
|
||||
anchorRight && (k - row1count == row0count - 1 - anchorOffset)
|
||||
val kk = when (keyView) {
|
||||
is KeyView -> when {
|
||||
isInitActive -> {
|
||||
hasShownFirst = true
|
||||
0
|
||||
}
|
||||
hasShownFirst -> k
|
||||
else -> k + 1
|
||||
}
|
||||
else -> k
|
||||
}
|
||||
popupViewExt.addView(
|
||||
createTextView(
|
||||
keyView, k, isInitActive, (row1count > 0) && (k - row1count == 0)
|
||||
keyView, kk, isInitActive, (row1count > 0) && (k - row1count == 0)
|
||||
)
|
||||
)
|
||||
if (isInitActive) {
|
||||
@@ -338,9 +354,9 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
// Calculate layout params
|
||||
val extWidth = row0count * keyPopupWidth
|
||||
val extHeight = when {
|
||||
row1count > 0 -> keyView.measuredHeight * 2
|
||||
else -> keyView.measuredHeight
|
||||
}
|
||||
row1count > 0 -> keyPopupHeight * 0.4f * 2.0f
|
||||
else -> keyPopupHeight * 0.4f
|
||||
}.toInt()
|
||||
popupViewExt.justifyContent = if (anchorLeft) {
|
||||
JustifyContent.FLEX_START
|
||||
} else {
|
||||
@@ -360,7 +376,7 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
else -> 0
|
||||
}
|
||||
val y = -keyPopupHeight - when {
|
||||
row1count > 0 -> keyView.measuredHeight
|
||||
row1count > 0 -> (keyPopupHeight * 0.4f).toInt()
|
||||
else -> 0
|
||||
}
|
||||
|
||||
@@ -442,7 +458,7 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
}
|
||||
|
||||
if (keyView is KeyView) {
|
||||
for (k in keyView.data.popup.indices) {
|
||||
for (k in keyView.dataPopupWithHint.indices) {
|
||||
val view = popupViewExt.getChildAt(k)
|
||||
if (view != null) {
|
||||
val textView = view as KeyPopupExtendedSingleView
|
||||
@@ -472,7 +488,17 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
*/
|
||||
fun getActiveKeyData(keyView: T_KV): KeyData? {
|
||||
return if (keyView is KeyView) {
|
||||
keyView.data.popup.getOrNull(activeExtIndex ?: -1) ?: keyView.data
|
||||
val activeExtIndex = activeExtIndex
|
||||
if (activeExtIndex != null) {
|
||||
val singleView = popupViewExt[activeExtIndex]
|
||||
if (singleView is KeyPopupExtendedSingleView) {
|
||||
keyView.dataPopupWithHint.getOrNull(singleView.adjustedIndex) ?: keyView.data
|
||||
} else {
|
||||
keyView.data
|
||||
}
|
||||
} else {
|
||||
keyView.data
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.popup
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.util.AttributeSet
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.util.*
|
||||
|
||||
class KeyPopupView : LinearLayout {
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
private lateinit var text: TextView
|
||||
private lateinit var threedots: ImageView
|
||||
|
||||
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()
|
||||
text = findViewById(R.id.key_popup_text)
|
||||
threedots = findViewById(R.id.key_popup_threedots)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas?) {
|
||||
setBackgroundTintColor2(this, prefs.theme.keyPopupBgColor)
|
||||
text.setTextColor(prefs.theme.keyPopupFgColor)
|
||||
setImageTintColor2(threedots, prefs.theme.keyPopupFgColor)
|
||||
super.onDraw(canvas)
|
||||
}
|
||||
}
|
||||
@@ -18,20 +18,16 @@ package dev.patrickgold.florisboard.ime.text
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.text.InputType
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.inputmethod.CursorAnchorInfo
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.ExtractedTextRequest
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.view.inputmethod.*
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ViewFlipper
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.InputView
|
||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||
import dev.patrickgold.florisboard.ime.core.*
|
||||
import dev.patrickgold.florisboard.ime.text.editing.EditingKeyboardView
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyData
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyType
|
||||
@@ -59,35 +55,35 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
FlorisBoard.EventListener {
|
||||
|
||||
private val florisboard = FlorisBoard.getInstance()
|
||||
private val activeEditorInstance: EditorInstance
|
||||
get() = florisboard.activeEditorInstance
|
||||
|
||||
private var activeKeyboardMode: KeyboardMode? = null
|
||||
private val keyboardViews = EnumMap<KeyboardMode, KeyboardView>(KeyboardMode::class.java)
|
||||
private var editingKeyboardView: EditingKeyboardView? = null
|
||||
private val osHandler = Handler()
|
||||
private var textViewFlipper: ViewFlipper? = null
|
||||
var textViewGroup: LinearLayout? = null
|
||||
|
||||
var keyVariation: KeyVariation = KeyVariation.NORMAL
|
||||
private val layoutManager = LayoutManager(florisboard)
|
||||
lateinit var smartbarManager: SmartbarManager
|
||||
val layoutManager = LayoutManager(florisboard)
|
||||
private lateinit var smartbarManager: SmartbarManager
|
||||
|
||||
// Caps/Space related properties
|
||||
var caps: Boolean = false
|
||||
private set
|
||||
var capsLock: Boolean = false
|
||||
private set
|
||||
private var cursorCapsMode: CapsMode = CapsMode.NONE
|
||||
private var editorCapsMode: CapsMode = CapsMode.NONE
|
||||
private var hasCapsRecentlyChanged: Boolean = false
|
||||
private var hasSpaceRecentlyPressed: Boolean = false
|
||||
|
||||
// Composing text related properties
|
||||
private var composingText: String? = null
|
||||
private var composingTextStart: Int? = null
|
||||
private var cursorPos: Int = 0
|
||||
private var isComposingEnabled: Boolean = false
|
||||
private var isTextSelected: Boolean = false
|
||||
var isManualSelectionMode: Boolean = false
|
||||
private var isManualSelectionModeLeft: Boolean = false
|
||||
private var isManualSelectionModeRight: Boolean = false
|
||||
|
||||
companion object {
|
||||
private val TAG: String? = TextInputManager::class.simpleName
|
||||
private var instance: TextInputManager? = null
|
||||
|
||||
@Synchronized
|
||||
@@ -99,24 +95,24 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
florisboard.addEventListener(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-UI-related setup + preloading of all required computed layouts (asynchronous in the
|
||||
* background).
|
||||
*/
|
||||
override fun onCreate() {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onCreate()")
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onCreate()")
|
||||
|
||||
for (mode in KeyboardMode.values()) {
|
||||
if (mode == KeyboardMode.CHARACTERS) {
|
||||
var subtypes = florisboard.subtypeManager.subtypes
|
||||
if (subtypes.isEmpty()) {
|
||||
subtypes = listOf(Subtype.DEFAULT)
|
||||
}
|
||||
for (subtype in subtypes) {
|
||||
layoutManager.preloadComputedLayout(mode, subtype)
|
||||
}
|
||||
} else {
|
||||
layoutManager.preloadComputedLayout(mode, florisboard.activeSubtype)
|
||||
var subtypes = florisboard.subtypeManager.subtypes
|
||||
if (subtypes.isEmpty()) {
|
||||
subtypes = listOf(Subtype.DEFAULT)
|
||||
}
|
||||
for (subtype in subtypes) {
|
||||
for (mode in KeyboardMode.values()) {
|
||||
layoutManager.preloadComputedLayout(mode, subtype)
|
||||
}
|
||||
}
|
||||
smartbarManager = SmartbarManager.getInstance()
|
||||
@@ -124,8 +120,6 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
|
||||
private suspend fun addKeyboardView(mode: KeyboardMode) {
|
||||
val keyboardView = KeyboardView(florisboard.context)
|
||||
keyboardView.florisboard = florisboard
|
||||
keyboardView.prefs = florisboard.prefs
|
||||
keyboardView.computedLayout = layoutManager.fetchComputedLayoutAsync(mode, florisboard.activeSubtype).await()
|
||||
keyboardViews[mode] = keyboardView
|
||||
withContext(Dispatchers.Main) {
|
||||
@@ -137,11 +131,12 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* Sets up the newly registered input view.
|
||||
*/
|
||||
override fun onRegisterInputView(inputView: InputView) {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onRegisterInputView(inputView)")
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onRegisterInputView(inputView)")
|
||||
|
||||
launch(Dispatchers.Default) {
|
||||
textViewGroup = inputView.findViewById(R.id.text_input)
|
||||
textViewFlipper = inputView.findViewById(R.id.text_input_view_flipper)
|
||||
editingKeyboardView = inputView.findViewById(R.id.editing)
|
||||
|
||||
val activeKeyboardMode = getActiveKeyboardMode()
|
||||
addKeyboardView(activeKeyboardMode)
|
||||
@@ -149,7 +144,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
setActiveKeyboardMode(activeKeyboardMode)
|
||||
}
|
||||
for (mode in KeyboardMode.values()) {
|
||||
if (mode != activeKeyboardMode) {
|
||||
if (mode != activeKeyboardMode && mode != KeyboardMode.SMARTBAR_NUMBER_ROW) {
|
||||
addKeyboardView(mode)
|
||||
}
|
||||
}
|
||||
@@ -160,7 +155,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* Cancels all coroutines and cleans up.
|
||||
*/
|
||||
override fun onDestroy() {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onDestroy()")
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onDestroy()")
|
||||
|
||||
cancel()
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
@@ -170,58 +165,60 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the [activeKeyboardMode], [keyVariation] and [isComposingEnabled] property values
|
||||
* when starting to interact with a input editor. Also resets the composing texts and sets the
|
||||
* initial caps mode accordingly.
|
||||
* Evaluates the [activeKeyboardMode], [keyVariation] and [EditorInstance.isComposingEnabled]
|
||||
* property values when starting to interact with a input editor. Also resets the composing
|
||||
* texts and sets the initial caps mode accordingly.
|
||||
*/
|
||||
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
|
||||
val keyboardMode = when (info) {
|
||||
null -> KeyboardMode.CHARACTERS
|
||||
else -> when (info.inputType and InputType.TYPE_MASK_CLASS) {
|
||||
InputType.TYPE_CLASS_NUMBER -> {
|
||||
keyVariation = KeyVariation.NORMAL
|
||||
KeyboardMode.NUMERIC
|
||||
}
|
||||
InputType.TYPE_CLASS_PHONE -> {
|
||||
keyVariation = KeyVariation.NORMAL
|
||||
KeyboardMode.PHONE
|
||||
}
|
||||
InputType.TYPE_CLASS_TEXT -> {
|
||||
keyVariation = when (info.inputType and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS -> {
|
||||
KeyVariation.EMAIL_ADDRESS
|
||||
}
|
||||
InputType.TYPE_TEXT_VARIATION_PASSWORD,
|
||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD -> {
|
||||
KeyVariation.PASSWORD
|
||||
}
|
||||
InputType.TYPE_TEXT_VARIATION_URI -> {
|
||||
KeyVariation.URI
|
||||
}
|
||||
else -> {
|
||||
KeyVariation.NORMAL
|
||||
}
|
||||
override fun onStartInputView(instance: EditorInstance, restarting: Boolean) {
|
||||
val keyboardMode = when (instance.inputAttributes.type) {
|
||||
InputAttributes.Type.NUMBER -> {
|
||||
keyVariation = KeyVariation.NORMAL
|
||||
KeyboardMode.NUMERIC
|
||||
}
|
||||
InputAttributes.Type.PHONE -> {
|
||||
keyVariation = KeyVariation.NORMAL
|
||||
KeyboardMode.PHONE
|
||||
}
|
||||
InputAttributes.Type.TEXT -> {
|
||||
keyVariation = when (instance.inputAttributes.variation) {
|
||||
InputAttributes.Variation.EMAIL_ADDRESS,
|
||||
InputAttributes.Variation.WEB_EMAIL_ADDRESS -> {
|
||||
KeyVariation.EMAIL_ADDRESS
|
||||
}
|
||||
InputAttributes.Variation.PASSWORD,
|
||||
InputAttributes.Variation.VISIBLE_PASSWORD,
|
||||
InputAttributes.Variation.WEB_PASSWORD -> {
|
||||
KeyVariation.PASSWORD
|
||||
}
|
||||
InputAttributes.Variation.URI -> {
|
||||
KeyVariation.URI
|
||||
}
|
||||
else -> {
|
||||
KeyVariation.NORMAL
|
||||
}
|
||||
KeyboardMode.CHARACTERS
|
||||
}
|
||||
else -> {
|
||||
keyVariation = KeyVariation.NORMAL
|
||||
KeyboardMode.CHARACTERS
|
||||
}
|
||||
KeyboardMode.CHARACTERS
|
||||
}
|
||||
else -> {
|
||||
keyVariation = KeyVariation.NORMAL
|
||||
KeyboardMode.CHARACTERS
|
||||
}
|
||||
}
|
||||
isComposingEnabled = when (keyboardMode) {
|
||||
instance.isComposingEnabled = when (keyboardMode) {
|
||||
KeyboardMode.NUMERIC,
|
||||
KeyboardMode.PHONE,
|
||||
KeyboardMode.PHONE2 -> false
|
||||
else -> keyVariation != KeyVariation.PASSWORD
|
||||
else -> keyVariation != KeyVariation.PASSWORD &&
|
||||
florisboard.prefs.suggestion.enabled// &&
|
||||
//!instance.inputAttributes.flagTextAutoComplete &&
|
||||
//!instance.inputAttributes.flagTextNoSuggestions
|
||||
}
|
||||
if (!florisboard.prefs.correction.rememberCapsLockState) {
|
||||
capsLock = false
|
||||
}
|
||||
updateCapsState()
|
||||
resetComposingText()
|
||||
setActiveKeyboardMode(keyboardMode)
|
||||
smartbarManager.onStartInputView(keyboardMode, isComposingEnabled)
|
||||
smartbarManager.onStartInputView(keyboardMode)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -248,14 +245,19 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
/**
|
||||
* Sets [activeKeyboardMode] and updates the [SmartbarManager.isQuickActionsVisible].
|
||||
*/
|
||||
private fun setActiveKeyboardMode(mode: KeyboardMode) {
|
||||
textViewFlipper?.displayedChild =
|
||||
textViewFlipper?.indexOfChild(keyboardViews[mode]) ?: 0
|
||||
fun setActiveKeyboardMode(mode: KeyboardMode) {
|
||||
textViewFlipper?.displayedChild = textViewFlipper?.indexOfChild(when (mode) {
|
||||
KeyboardMode.EDITING -> editingKeyboardView
|
||||
else -> keyboardViews[mode]
|
||||
}) ?: 0
|
||||
keyboardViews[mode]?.updateVisibility()
|
||||
keyboardViews[mode]?.requestLayout()
|
||||
keyboardViews[mode]?.requestLayoutAllKeys()
|
||||
activeKeyboardMode = mode
|
||||
smartbarManager.isQuickActionsVisible = false
|
||||
isManualSelectionMode = false
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = false
|
||||
}
|
||||
|
||||
override fun onSubtypeChanged(newSubtype: Subtype) {
|
||||
@@ -270,133 +272,47 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* Main logic point for processing cursor updates as well as parsing the current composing word
|
||||
* and passing this info on to the [SmartbarManager] to turn it into candidate suggestions.
|
||||
*/
|
||||
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
|
||||
cursorAnchorInfo ?: return
|
||||
|
||||
val ic = florisboard.currentInputConnection
|
||||
|
||||
if (isComposingEnabled) {
|
||||
if (cursorAnchorInfo.selectionEnd - cursorAnchorInfo.selectionStart == 0) {
|
||||
val newCursorPos = cursorAnchorInfo.selectionStart
|
||||
val prevComposingText = (cursorAnchorInfo.composingText ?: "").toString()
|
||||
val inputText =
|
||||
(ic?.getExtractedText(ExtractedTextRequest(), 0)?.text ?: "").toString()
|
||||
setComposingTextBasedOnInput(inputText, newCursorPos)
|
||||
if ((newCursorPos == cursorPos) && (composingText == prevComposingText)) {
|
||||
// Ignore this, as nothing has changed
|
||||
} else {
|
||||
cursorPos = newCursorPos
|
||||
if (composingText != null && composingTextStart != null) {
|
||||
ic?.setComposingRegion(
|
||||
composingTextStart!!,
|
||||
composingTextStart!! + composingText!!.length
|
||||
)
|
||||
} else {
|
||||
resetComposingText()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resetComposingText()
|
||||
}
|
||||
smartbarManager.generateCandidatesFromComposing(composingText)
|
||||
override fun onUpdateSelection() {
|
||||
if (!activeEditorInstance.isNewSelectionInBoundsOfOld) {
|
||||
isManualSelectionMode = false
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = false
|
||||
}
|
||||
isTextSelected = cursorAnchorInfo.selectionEnd - cursorAnchorInfo.selectionStart != 0
|
||||
updateCapsState()
|
||||
smartbarManager.onUpdateSelection()
|
||||
}
|
||||
|
||||
override fun onPrimaryClipChanged() {
|
||||
smartbarManager.onPrimaryClipChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the [composingText] and [composingTextStart] properties. Does NOT sync with
|
||||
* [SmartbarManager]!
|
||||
*
|
||||
* @param notifyInputConnection If the current input connection should be notified.
|
||||
*/
|
||||
private fun resetComposingText(notifyInputConnection: Boolean = true) {
|
||||
if (notifyInputConnection) {
|
||||
val ic = florisboard.currentInputConnection
|
||||
ic?.finishComposingText()
|
||||
}
|
||||
composingText = null
|
||||
composingTextStart = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to parse the [composingText] from a given [inputCursorPos] within [inputText].
|
||||
* Sets both [composingText] and [composingTextStart] to null if it fails, else to its
|
||||
* parsed values.
|
||||
*
|
||||
* @param inputText The input text to search in.
|
||||
* @param inputCursorPos The position where to search in [inputText].
|
||||
*/
|
||||
private fun setComposingTextBasedOnInput(inputText: String, inputCursorPos: Int) {
|
||||
val words = inputText.split("[^\\p{L}]".toRegex())
|
||||
var pos = 0
|
||||
resetComposingText(false)
|
||||
for (word in words) {
|
||||
if (inputCursorPos >= pos && inputCursorPos <= pos + word.length && word.isNotEmpty()) {
|
||||
composingText = word
|
||||
composingTextStart = pos
|
||||
break
|
||||
} else {
|
||||
pos += word.length + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should primarily pe used by [SmartbarManager.candidateViewOnClickListener] to commit
|
||||
* a candidate if a user has pressed on it.
|
||||
*/
|
||||
fun commitCandidate(candidateText: String) {
|
||||
val ic = florisboard.currentInputConnection
|
||||
ic?.setComposingText(candidateText, 1)
|
||||
ic?.finishComposingText()
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the [CapsMode] out of the given [flags].
|
||||
*
|
||||
* @param flags The input flags.
|
||||
* @return A [CapsMode] value.
|
||||
*/
|
||||
private fun parseCapsModeFromFlags(flags: Int): CapsMode {
|
||||
return when {
|
||||
flags and InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS > 0 -> {
|
||||
CapsMode.ALL
|
||||
}
|
||||
flags and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES > 0 -> {
|
||||
CapsMode.SENTENCES
|
||||
}
|
||||
flags and InputType.TYPE_TEXT_FLAG_CAP_WORDS > 0 -> {
|
||||
CapsMode.WORDS
|
||||
}
|
||||
else -> {
|
||||
CapsMode.NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the current cursor caps mode from the current input connection.
|
||||
*
|
||||
* @return The [CapsMode] according to the returned flags by the current input connection.
|
||||
*/
|
||||
private fun fetchCurrentCursorCapsMode(): CapsMode {
|
||||
val ic = florisboard.currentInputConnection
|
||||
val info = florisboard.currentInputEditorInfo
|
||||
val capsFlags = ic?.getCursorCapsMode(info.inputType) ?: 0
|
||||
return parseCapsModeFromFlags(capsFlags)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current caps state according to the [cursorCapsMode], while respecting
|
||||
* [capsLock] property.
|
||||
* Updates the current caps state according to the [EditorInstance.cursorCapsMode], while
|
||||
* respecting [capsLock] property and the correction.autoCapitalization preference.
|
||||
*/
|
||||
private fun updateCapsState() {
|
||||
cursorCapsMode = fetchCurrentCursorCapsMode()
|
||||
editorCapsMode = parseCapsModeFromFlags(florisboard.currentInputEditorInfo.inputType)
|
||||
if (!capsLock) {
|
||||
caps = cursorCapsMode != CapsMode.NONE
|
||||
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
|
||||
caps = florisboard.prefs.correction.autoCapitalization &&
|
||||
activeEditorInstance.cursorCapsMode != InputAttributes.CapsMode.NONE
|
||||
launch(Dispatchers.Main) {
|
||||
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a given [SwipeAction]. Ignores any [SwipeAction] but the ones relevant for this
|
||||
* class.
|
||||
*/
|
||||
fun executeSwipeAction(swipeAction: SwipeAction) {
|
||||
when (swipeAction) {
|
||||
SwipeAction.DELETE_WORD -> handleDeleteWord()
|
||||
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.SHIFT -> handleShift()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,54 +320,41 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* Handles a [KeyCode.DELETE] event.
|
||||
*/
|
||||
private fun handleDelete() {
|
||||
val ic = florisboard.currentInputConnection
|
||||
ic?.beginBatchEdit()
|
||||
resetComposingText()
|
||||
ic?.sendKeyEvent(
|
||||
KeyEvent(
|
||||
KeyEvent.ACTION_DOWN,
|
||||
KeyEvent.KEYCODE_DEL
|
||||
)
|
||||
)
|
||||
ic?.endBatchEdit()
|
||||
isManualSelectionMode = false
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = false
|
||||
activeEditorInstance.deleteBackwards()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.DELETE_WORD] event.
|
||||
*/
|
||||
private fun handleDeleteWord() {
|
||||
isManualSelectionMode = false
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = false
|
||||
activeEditorInstance.deleteWordsBeforeCursor(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.ENTER] event.
|
||||
*/
|
||||
private fun handleEnter() {
|
||||
val ic = florisboard.currentInputConnection
|
||||
ic?.beginBatchEdit()
|
||||
resetComposingText()
|
||||
val action = florisboard.currentInputEditorInfo?.imeOptions ?: 0
|
||||
if (action and EditorInfo.IME_FLAG_NO_ENTER_ACTION > 0) {
|
||||
ic?.sendKeyEvent(
|
||||
KeyEvent(
|
||||
KeyEvent.ACTION_DOWN,
|
||||
KeyEvent.KEYCODE_ENTER
|
||||
)
|
||||
)
|
||||
if (activeEditorInstance.imeOptions.flagNoEnterAction) {
|
||||
activeEditorInstance.performEnter()
|
||||
} else {
|
||||
when (action and EditorInfo.IME_MASK_ACTION) {
|
||||
EditorInfo.IME_ACTION_DONE,
|
||||
EditorInfo.IME_ACTION_GO,
|
||||
EditorInfo.IME_ACTION_NEXT,
|
||||
EditorInfo.IME_ACTION_PREVIOUS,
|
||||
EditorInfo.IME_ACTION_SEARCH,
|
||||
EditorInfo.IME_ACTION_SEND -> {
|
||||
ic?.performEditorAction(action)
|
||||
}
|
||||
else -> {
|
||||
ic?.sendKeyEvent(
|
||||
KeyEvent(
|
||||
KeyEvent.ACTION_DOWN,
|
||||
KeyEvent.KEYCODE_ENTER
|
||||
)
|
||||
)
|
||||
when (activeEditorInstance.imeOptions.action) {
|
||||
ImeOptions.Action.DONE,
|
||||
ImeOptions.Action.GO,
|
||||
ImeOptions.Action.NEXT,
|
||||
ImeOptions.Action.PREVIOUS,
|
||||
ImeOptions.Action.SEARCH,
|
||||
ImeOptions.Action.SEND -> {
|
||||
activeEditorInstance.performEnterAction(activeEditorInstance.imeOptions.action)
|
||||
}
|
||||
else -> activeEditorInstance.performEnter()
|
||||
}
|
||||
}
|
||||
ic?.endBatchEdit()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -479,14 +382,13 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* enabled by the user.
|
||||
*/
|
||||
private fun handleSpace() {
|
||||
val ic = florisboard.currentInputConnection
|
||||
if (florisboard.prefs.correction.doubleSpacePeriod) {
|
||||
if (hasSpaceRecentlyPressed) {
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
val text = ic?.getTextBeforeCursor(2, 0) ?: ""
|
||||
val text = activeEditorInstance.getTextBeforeCursor(2)
|
||||
if (text.length == 2 && !text.matches("""[.!?‽\s][\s]""".toRegex())) {
|
||||
ic?.deleteSurroundingText(1, 0)
|
||||
ic?.commitText(".", 1)
|
||||
activeEditorInstance.deleteBackwards()
|
||||
activeEditorInstance.commitText(".")
|
||||
}
|
||||
hasSpaceRecentlyPressed = false
|
||||
} else {
|
||||
@@ -496,7 +398,147 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
ic?.commitText(KeyCode.SPACE.toChar().toString(), 1)
|
||||
activeEditorInstance.commitText(KeyCode.SPACE.toChar().toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles [KeyCode] arrow and move events, behaves differently depending on text selection.
|
||||
*/
|
||||
private fun handleArrow(code: Int) = activeEditorInstance.apply {
|
||||
val selectionStartMin = 0
|
||||
val selectionEndMax = cachedText.length
|
||||
if (selection.isSelectionMode && isManualSelectionMode) {
|
||||
// Text is selected and it is manual selection -> Expand selection depending on started
|
||||
// direction.
|
||||
when (code) {
|
||||
KeyCode.ARROW_DOWN -> {}
|
||||
KeyCode.ARROW_LEFT -> {
|
||||
if (isManualSelectionModeLeft) {
|
||||
setSelection(
|
||||
(selection.start - 1).coerceAtLeast(selectionStartMin),
|
||||
selection.end
|
||||
)
|
||||
} else {
|
||||
setSelection(selection.start, selection.end - 1)
|
||||
}
|
||||
}
|
||||
KeyCode.ARROW_RIGHT -> {
|
||||
if (isManualSelectionModeRight) {
|
||||
setSelection(
|
||||
selection.start,
|
||||
(selection.end + 1).coerceAtMost(selectionEndMax)
|
||||
)
|
||||
} else {
|
||||
setSelection(selection.start + 1, selection.end)
|
||||
}
|
||||
}
|
||||
KeyCode.ARROW_UP -> {}
|
||||
KeyCode.MOVE_HOME -> {
|
||||
if (isManualSelectionModeLeft) {
|
||||
setSelection(selectionStartMin, selection.end)
|
||||
} else {
|
||||
setSelection(selectionStartMin, selection.start)
|
||||
}
|
||||
}
|
||||
KeyCode.MOVE_END -> {
|
||||
if (isManualSelectionModeRight) {
|
||||
setSelection(selection.start, selectionEndMax)
|
||||
} else {
|
||||
setSelection(selection.end, selectionEndMax)
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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 -> {
|
||||
setSelection(selection.start, selection.end - 1)
|
||||
}
|
||||
KeyCode.ARROW_RIGHT -> {
|
||||
setSelection(
|
||||
selection.start,
|
||||
(selection.end + 1).coerceAtMost(selectionEndMax)
|
||||
)
|
||||
}
|
||||
KeyCode.ARROW_UP -> {}
|
||||
KeyCode.MOVE_HOME -> {
|
||||
setSelection(selectionStartMin, selection.start)
|
||||
}
|
||||
KeyCode.MOVE_END -> {
|
||||
setSelection(selection.start, selectionEndMax)
|
||||
}
|
||||
}
|
||||
} 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 -> {
|
||||
setSelection(
|
||||
(selection.start - 1).coerceAtLeast(selectionStartMin),
|
||||
selection.start
|
||||
)
|
||||
isManualSelectionModeLeft = true
|
||||
isManualSelectionModeRight = false
|
||||
}
|
||||
KeyCode.ARROW_RIGHT -> {
|
||||
setSelection(
|
||||
selection.end,
|
||||
(selection.end + 1).coerceAtMost(selectionEndMax)
|
||||
)
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = true
|
||||
}
|
||||
KeyCode.ARROW_UP -> {}
|
||||
KeyCode.MOVE_HOME -> {
|
||||
setSelection(selectionStartMin, selection.start)
|
||||
isManualSelectionModeLeft = true
|
||||
isManualSelectionModeRight = false
|
||||
}
|
||||
KeyCode.MOVE_END -> {
|
||||
setSelection(selection.end, selectionEndMax)
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = true
|
||||
}
|
||||
}
|
||||
} 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_HOME -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_UP)
|
||||
KeyCode.MOVE_END -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_DOWN)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.CLIPBOARD_SELECT] event.
|
||||
*/
|
||||
private fun handleClipboardSelect() = activeEditorInstance.apply {
|
||||
if (selection.isSelectionMode) {
|
||||
if (isManualSelectionMode && isManualSelectionModeLeft) {
|
||||
setSelection(selection.start, selection.start)
|
||||
} else {
|
||||
setSelection(selection.end, selection.end)
|
||||
}
|
||||
isManualSelectionMode = false
|
||||
} else {
|
||||
isManualSelectionMode = !isManualSelectionMode
|
||||
// Must call to update UI properly
|
||||
editingKeyboardView?.onUpdateSelection()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.CLIPBOARD_SELECT_ALL] event.
|
||||
*/
|
||||
private fun handleClipboardSelectAll() {
|
||||
activeEditorInstance.setSelection(0, activeEditorInstance.cachedText.length)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -507,11 +549,29 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* @param keyData The [KeyData] object which should be sent.
|
||||
*/
|
||||
fun sendKeyPress(keyData: KeyData) {
|
||||
val ic = florisboard.currentInputConnection
|
||||
|
||||
when (keyData.code) {
|
||||
KeyCode.DELETE -> handleDelete()
|
||||
KeyCode.ENTER -> handleEnter()
|
||||
KeyCode.ARROW_DOWN,
|
||||
KeyCode.ARROW_LEFT,
|
||||
KeyCode.ARROW_RIGHT,
|
||||
KeyCode.ARROW_UP,
|
||||
KeyCode.MOVE_HOME,
|
||||
KeyCode.MOVE_END -> handleArrow(keyData.code)
|
||||
KeyCode.CLIPBOARD_CUT -> activeEditorInstance.performClipboardCut()
|
||||
KeyCode.CLIPBOARD_COPY -> activeEditorInstance.performClipboardCopy()
|
||||
KeyCode.CLIPBOARD_PASTE -> {
|
||||
activeEditorInstance.performClipboardPaste()
|
||||
smartbarManager.resetClipboardSuggestion()
|
||||
}
|
||||
KeyCode.CLIPBOARD_SELECT -> handleClipboardSelect()
|
||||
KeyCode.CLIPBOARD_SELECT_ALL -> handleClipboardSelectAll()
|
||||
KeyCode.DELETE -> {
|
||||
handleDelete()
|
||||
smartbarManager.resetClipboardSuggestion()
|
||||
}
|
||||
KeyCode.ENTER -> {
|
||||
handleEnter()
|
||||
smartbarManager.resetClipboardSuggestion()
|
||||
}
|
||||
KeyCode.LANGUAGE_SWITCH -> florisboard.switchToNextSubtype()
|
||||
KeyCode.SETTINGS -> florisboard.launchSettings()
|
||||
KeyCode.SHIFT -> handleShift()
|
||||
@@ -531,8 +591,6 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
KeyCode.VIEW_SYMBOLS -> setActiveKeyboardMode(KeyboardMode.SYMBOLS)
|
||||
KeyCode.VIEW_SYMBOLS2 -> setActiveKeyboardMode(KeyboardMode.SYMBOLS2)
|
||||
else -> {
|
||||
ic?.beginBatchEdit()
|
||||
resetComposingText()
|
||||
when (activeKeyboardMode) {
|
||||
KeyboardMode.NUMERIC,
|
||||
KeyboardMode.NUMERIC_ADVANCED,
|
||||
@@ -541,13 +599,13 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
KeyType.CHARACTER,
|
||||
KeyType.NUMERIC -> {
|
||||
val text = keyData.code.toChar().toString()
|
||||
ic?.commitText(text, 1)
|
||||
activeEditorInstance.commitText(text)
|
||||
}
|
||||
else -> when (keyData.code) {
|
||||
KeyCode.PHONE_PAUSE,
|
||||
KeyCode.PHONE_WAIT -> {
|
||||
val text = keyData.code.toChar().toString()
|
||||
ic?.commitText(text, 1)
|
||||
activeEditorInstance.commitText(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -559,7 +617,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
true -> keyData.label.toUpperCase(Locale.getDefault())
|
||||
false -> keyData.label.toLowerCase(Locale.getDefault())
|
||||
}
|
||||
ic?.commitText(tld, 1)
|
||||
activeEditorInstance.commitText(tld)
|
||||
}
|
||||
else -> {
|
||||
var text = keyData.code.toChar().toString()
|
||||
@@ -567,26 +625,20 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
true -> text.toUpperCase(Locale.getDefault())
|
||||
false -> text.toLowerCase(Locale.getDefault())
|
||||
}
|
||||
ic?.commitText(text, 1)
|
||||
activeEditorInstance.commitText(text)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Log.e(
|
||||
this::class.simpleName,
|
||||
"sendKeyPress(keyData): Received unknown key: $keyData"
|
||||
)
|
||||
Log.e(TAG,"sendKeyPress(keyData): Received unknown key: $keyData")
|
||||
}
|
||||
}
|
||||
}
|
||||
ic?.endBatchEdit()
|
||||
smartbarManager.resetClipboardSuggestion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class CapsMode {
|
||||
ALL,
|
||||
NONE,
|
||||
SENTENCES,
|
||||
WORDS;
|
||||
if (keyData.code != KeyCode.SHIFT && !capsLock) {
|
||||
updateCapsState()
|
||||
}
|
||||
smartbarManager.updateActiveContainerVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.text.editing
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Typeface
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
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.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyData
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* View class for managing and rendering an editing key.
|
||||
*/
|
||||
class EditingKeyView : AppCompatImageButton {
|
||||
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
private val data: KeyData
|
||||
private var isKeyPressed: Boolean = false
|
||||
private var osTimer: Timer? = null
|
||||
|
||||
private var label: String? = null
|
||||
private var labelPaint: Paint = Paint().apply {
|
||||
alpha = 255
|
||||
color = 0
|
||||
isAntiAlias = true
|
||||
isFakeBoldText = false
|
||||
textAlign = Paint.Align.CENTER
|
||||
textSize = Button(context).textSize
|
||||
typeface = Typeface.DEFAULT
|
||||
}
|
||||
|
||||
var isHighlighted: Boolean = false
|
||||
set(value) { field = value; invalidate() }
|
||||
|
||||
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_home -> KeyCode.MOVE_HOME
|
||||
R.id.move_end -> KeyCode.MOVE_END
|
||||
R.id.select -> KeyCode.CLIPBOARD_SELECT
|
||||
R.id.select_all -> KeyCode.CLIPBOARD_SELECT_ALL
|
||||
else -> 0
|
||||
}
|
||||
data = KeyData(code)
|
||||
context.obtainStyledAttributes(attrs, R.styleable.EditingKeyView).apply {
|
||||
label = getString(R.styleable.EditingKeyView_android_text)
|
||||
recycle()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||
if (!isEnabled || event == null) {
|
||||
return false
|
||||
}
|
||||
super.onTouchEvent(event)
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
isKeyPressed = true
|
||||
florisboard?.keyPressVibrate()
|
||||
florisboard?.keyPressSound(data)
|
||||
when (data.code) {
|
||||
KeyCode.ARROW_DOWN,
|
||||
KeyCode.ARROW_LEFT,
|
||||
KeyCode.ARROW_RIGHT,
|
||||
KeyCode.ARROW_UP,
|
||||
KeyCode.DELETE -> {
|
||||
osTimer = Timer()
|
||||
osTimer?.scheduleAtFixedRate(object : TimerTask() {
|
||||
override fun run() {
|
||||
florisboard?.textInputManager?.sendKeyPress(data)
|
||||
if (!isKeyPressed) {
|
||||
osTimer?.cancel()
|
||||
osTimer = null
|
||||
}
|
||||
}
|
||||
}, 500, 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
isKeyPressed = false
|
||||
osTimer?.cancel()
|
||||
osTimer = null
|
||||
if (event.actionMasked != MotionEvent.ACTION_CANCEL) {
|
||||
florisboard?.textInputManager?.sendKeyPress(data)
|
||||
}
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the key label / drawable.
|
||||
*/
|
||||
override fun onDraw(canvas: Canvas?) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
canvas ?: return
|
||||
|
||||
imageTintList = ColorStateList.valueOf(when {
|
||||
isEnabled -> prefs.theme.smartbarFgColor
|
||||
else -> prefs.theme.smartbarFgColorAlt
|
||||
})
|
||||
|
||||
// Draw label
|
||||
val label = label
|
||||
if (label != null) {
|
||||
labelPaint.color = if (isHighlighted && isEnabled) {
|
||||
prefs.theme.colorPrimary
|
||||
} else if (!isEnabled) {
|
||||
prefs.theme.smartbarFgColorAlt
|
||||
} else {
|
||||
prefs.theme.smartbarFgColor
|
||||
}
|
||||
val isPortrait =
|
||||
resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
|
||||
if (!isPortrait) {
|
||||
labelPaint.textSize *= 0.9f
|
||||
}
|
||||
val centerX = measuredWidth / 2.0f
|
||||
val centerY = measuredHeight / 2.0f + (labelPaint.textSize - labelPaint.descent()) / 2
|
||||
if (label.contains("\n")) {
|
||||
// Even if more lines may be existing only the first 2 are shown
|
||||
val labelLines = label.split("\n")
|
||||
canvas.drawText(labelLines[0], centerX, centerY * 0.70f, labelPaint)
|
||||
canvas.drawText(labelLines[1], centerX, centerY * 1.30f, labelPaint)
|
||||
} else {
|
||||
canvas.drawText(label, centerX, centerY, labelPaint)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.text.editing
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.util.setBackgroundTintColor2
|
||||
|
||||
/**
|
||||
* View class for updating the key views depending on the current selection and clipboard state.
|
||||
*/
|
||||
class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener {
|
||||
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
|
||||
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
|
||||
private var copyKey: EditingKeyView? = null
|
||||
private var pasteKey: EditingKeyView? = 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) {
|
||||
florisboard?.addEventListener(this)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
|
||||
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)
|
||||
copyKey = findViewById(R.id.clipboard_copy)
|
||||
pasteKey = findViewById(R.id.clipboard_paste)
|
||||
}
|
||||
|
||||
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
|
||||
else -> View.VISIBLE
|
||||
}
|
||||
cutKey?.visibility = when {
|
||||
isSelectionActive -> View.VISIBLE
|
||||
else -> View.GONE
|
||||
}
|
||||
copyKey?.isEnabled = isSelectionActive
|
||||
pasteKey?.isEnabled = florisboard?.clipboardManager?.hasPrimaryClip() ?: false
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
|
||||
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
|
||||
val height = when (heightMode) {
|
||||
MeasureSpec.EXACTLY -> {
|
||||
// Must be this size
|
||||
heightSize
|
||||
}
|
||||
MeasureSpec.AT_MOST -> {
|
||||
// Can't be bigger than...
|
||||
(florisboard?.inputView?.desiredTextKeyboardViewHeight ?: 0).coerceAtMost(heightSize)
|
||||
}
|
||||
else -> {
|
||||
// Be whatever you want
|
||||
florisboard?.inputView?.desiredTextKeyboardViewHeight ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY))
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas?) {
|
||||
super.onDraw(canvas)
|
||||
setBackgroundTintColor2(this, prefs.theme.smartbarBgColor)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.text.gestures
|
||||
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Enum for declaring the distance thresholds for swipe gestures.
|
||||
*/
|
||||
enum class DistanceThreshold {
|
||||
VERY_SHORT,
|
||||
SHORT,
|
||||
NORMAL,
|
||||
LONG,
|
||||
VERY_LONG;
|
||||
|
||||
companion object {
|
||||
fun fromString(string: String): DistanceThreshold {
|
||||
return valueOf(string.toUpperCase(Locale.ROOT))
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return super.toString().toLowerCase(Locale.ROOT)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.text.gestures
|
||||
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Enum for declaring the possible actions for swipe gestures.
|
||||
*/
|
||||
enum class SwipeAction {
|
||||
NO_ACTION,
|
||||
DELETE_CHARACTERS_PRECISELY,
|
||||
DELETE_WORD,
|
||||
DELETE_WORDS_PRECISELY,
|
||||
HIDE_KEYBOARD,
|
||||
MOVE_CURSOR_UP,
|
||||
MOVE_CURSOR_DOWN,
|
||||
MOVE_CURSOR_LEFT,
|
||||
MOVE_CURSOR_RIGHT,
|
||||
SHIFT,
|
||||
SWITCH_TO_PREV_SUBTYPE,
|
||||
SWITCH_TO_NEXT_SUBTYPE;
|
||||
|
||||
companion object {
|
||||
fun fromString(string: String): SwipeAction {
|
||||
return valueOf(string.toUpperCase(Locale.ROOT))
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return super.toString().toLowerCase(Locale.ROOT)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.text.gestures
|
||||
|
||||
import android.content.Context
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.MotionEvent
|
||||
import dev.patrickgold.florisboard.R
|
||||
import java.lang.Exception
|
||||
import kotlin.math.*
|
||||
|
||||
/**
|
||||
* Wrapper class which holds all enums, interfaces and classes for detecting a swipe gesture.
|
||||
*/
|
||||
abstract class SwipeGesture {
|
||||
/**
|
||||
* Class which detects swipes based on given [MotionEvent]s. Only supports single-finger swipes
|
||||
* and ignores additional pointers provided, if any.
|
||||
*
|
||||
* @property listener The listener to report detected swipes to.
|
||||
*/
|
||||
class Detector(private val context: Context, private val listener: Listener) {
|
||||
private val eventList: MutableList<MotionEvent> = mutableListOf()
|
||||
private var indexFirst: Int = 0
|
||||
private var indexLastMoveRecognized: Int = 0
|
||||
|
||||
var distanceThreshold: DistanceThreshold = DistanceThreshold.NORMAL
|
||||
var velocityThreshold: VelocityThreshold = VelocityThreshold.NORMAL
|
||||
|
||||
fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
try {
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN,
|
||||
MotionEvent.ACTION_POINTER_DOWN -> {
|
||||
clearEventList()
|
||||
eventList.add(MotionEvent.obtainNoHistory(event))
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
eventList.add(MotionEvent.obtainNoHistory(event))
|
||||
val lastEvent = eventList[indexLastMoveRecognized]
|
||||
val diffX = event.x - lastEvent.x
|
||||
val diffY = event.y - lastEvent.y
|
||||
val distanceThresholdNV = numericValue(distanceThreshold) / 2.0f
|
||||
return if (abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV) {
|
||||
indexLastMoveRecognized = eventList.size - 1
|
||||
val direction = detectDirection(diffX.toDouble(), diffY.toDouble())
|
||||
listener.onSwipe(direction, Type.TOUCH_MOVE)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP,
|
||||
MotionEvent.ACTION_POINTER_UP -> {
|
||||
val firstEvent = eventList[indexFirst]
|
||||
val diffX = event.x - firstEvent.x
|
||||
val diffY = event.y - firstEvent.y
|
||||
val distanceThresholdNV = numericValue(distanceThreshold)
|
||||
/*val velocityThresholdNV = numericValue(velocityThreshold)
|
||||
val velocity =
|
||||
((convertPixelsToDp(
|
||||
sqrt(diffX.pow(2) + diffY.pow(2)),
|
||||
context
|
||||
) / event.downTime) * 10.0f.pow(8)).toInt()*/
|
||||
clearEventList()
|
||||
// return if ((abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV) && velocity >= velocityThresholdNV) {
|
||||
return if ((abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV)) {
|
||||
val direction = detectDirection(diffX.toDouble(), diffY.toDouble())
|
||||
listener.onSwipe(direction, Type.TOUCH_UP)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
clearEventList()
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
return false
|
||||
} catch(e: Exception) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the angle based on the given x any y lengths. The returned angle is in degree
|
||||
* and goes clockwise, beginning with 0° at +x, 90° at +y, 180° at -y and 270° at -y.
|
||||
*
|
||||
* Coordinate system (based on the Android display coordinate system):
|
||||
* -y
|
||||
* -x 00 +x
|
||||
* +y
|
||||
*/
|
||||
private fun angle(diffX: Double, diffY: Double): Double {
|
||||
val tmpAngle = abs(360 * atan(diffY / diffX) / (2 * PI))
|
||||
return if (diffX < 0 && diffY >= 0) {
|
||||
180.0f - tmpAngle
|
||||
} else if (diffX < 0 && diffY < 0) {
|
||||
180.0f + tmpAngle
|
||||
} else if (diffX >= 0 && diffY < 0) {
|
||||
360.0f - tmpAngle
|
||||
} else {
|
||||
tmpAngle
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the direction of a finger swipe by two given events.
|
||||
*/
|
||||
private fun detectDirection(diffX: Double, diffY: Double): Direction {
|
||||
val diffAngle = angle(diffX, diffY) / 360
|
||||
return when {
|
||||
diffAngle >= (1/16.0f) && diffAngle < (3/16.0f) -> Direction.DOWN_RIGHT
|
||||
diffAngle >= (3/16.0f) && diffAngle < (5/16.0f) -> Direction.DOWN
|
||||
diffAngle >= (5/16.0f) && diffAngle < (7/16.0f) -> Direction.DOWN_LEFT
|
||||
diffAngle >= (7/16.0f) && diffAngle < (9/16.0f) -> Direction.LEFT
|
||||
diffAngle >= (9/16.0f) && diffAngle < (11/16.0f) -> Direction.UP_LEFT
|
||||
diffAngle >= (11/16.0f) && diffAngle < (13/16.0f) -> Direction.UP
|
||||
diffAngle >= (13/16.0f) && diffAngle < (15/16.0f) -> Direction.UP_RIGHT
|
||||
else -> Direction.RIGHT
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up and clears the event list.
|
||||
*/
|
||||
private fun clearEventList() {
|
||||
for (event in eventList) {
|
||||
event.recycle()
|
||||
}
|
||||
eventList.clear()
|
||||
indexFirst = 0
|
||||
indexLastMoveRecognized = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a numeric value for a given [DistanceThreshold], based on the values defined in
|
||||
* the resources dimens.xml file.
|
||||
*/
|
||||
private fun numericValue(of: DistanceThreshold): Double {
|
||||
return when (of) {
|
||||
DistanceThreshold.VERY_SHORT -> context.resources.getDimension(R.dimen.gesture_distance_threshold_very_short)
|
||||
DistanceThreshold.SHORT -> context.resources.getDimension(R.dimen.gesture_distance_threshold_short)
|
||||
DistanceThreshold.NORMAL -> context.resources.getDimension(R.dimen.gesture_distance_threshold_normal)
|
||||
DistanceThreshold.LONG -> context.resources.getDimension(R.dimen.gesture_distance_threshold_long)
|
||||
DistanceThreshold.VERY_LONG -> context.resources.getDimension(R.dimen.gesture_distance_threshold_very_long)
|
||||
}.toDouble()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a numeric value for a given [VelocityThreshold], based on the values defined in
|
||||
* the resources dimens.xml file.
|
||||
*/
|
||||
private fun numericValue(of: VelocityThreshold): Double {
|
||||
return when (of) {
|
||||
VelocityThreshold.VERY_SLOW -> context.resources.getInteger(R.integer.gesture_velocity_threshold_very_slow)
|
||||
VelocityThreshold.SLOW -> context.resources.getInteger(R.integer.gesture_velocity_threshold_slow)
|
||||
VelocityThreshold.NORMAL -> context.resources.getInteger(R.integer.gesture_velocity_threshold_normal)
|
||||
VelocityThreshold.FAST -> context.resources.getInteger(R.integer.gesture_velocity_threshold_fast)
|
||||
VelocityThreshold.VERY_FAST -> context.resources.getInteger(R.integer.gesture_velocity_threshold_very_fast)
|
||||
}.toDouble()
|
||||
}
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun onSwipe(direction: Direction, type: Type): Boolean
|
||||
}
|
||||
|
||||
enum class Direction {
|
||||
UP_LEFT,
|
||||
UP,
|
||||
UP_RIGHT,
|
||||
RIGHT,
|
||||
DOWN_RIGHT,
|
||||
DOWN,
|
||||
DOWN_LEFT,
|
||||
LEFT,
|
||||
}
|
||||
|
||||
enum class Type {
|
||||
TOUCH_UP,
|
||||
TOUCH_MOVE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.text.gestures
|
||||
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Enum for declaring the velocity thresholds for swipe gestures.
|
||||
*/
|
||||
enum class VelocityThreshold {
|
||||
VERY_SLOW,
|
||||
SLOW,
|
||||
NORMAL,
|
||||
FAST,
|
||||
VERY_FAST;
|
||||
|
||||
companion object {
|
||||
fun fromString(string: String): VelocityThreshold {
|
||||
return valueOf(string.toUpperCase(Locale.ROOT))
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return super.toString().toLowerCase(Locale.ROOT)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.text.key
|
||||
|
||||
object KeyCode {
|
||||
@@ -70,5 +86,6 @@ object KeyCode {
|
||||
const val TOGGLE_ONE_HANDED_MODE = -215
|
||||
const val URI_COMPONENT_TLD = -255
|
||||
|
||||
const val KESHIDA = 1600
|
||||
const val HALF_SPACE = 8204
|
||||
}
|
||||
|
||||
@@ -1,8 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.text.key
|
||||
|
||||
/**
|
||||
* Data class which describes a single key and its variants.
|
||||
*
|
||||
* @property code The UTF-8 encoded code of the character. The code defined here is used as the
|
||||
* data passed to the system.
|
||||
* @property label The string used to display the key in the UI. Is not used for the actual data
|
||||
* passed to the system. Should normally be the exact same as the [code]. Defaults to an empty
|
||||
* string.
|
||||
* @property hintedNumber The hinted number which will be dynamically inserted into the long-press
|
||||
* [popup]. Leave null to disable the hinted popup for this key. The visibility of the hinted number
|
||||
* is controlled by the preferences. Defaults to null.
|
||||
* @property hintedSymbol The hinted symbol which will be dynamically inserted into the long-press
|
||||
* [popup]. Leave null to disable the hinted popup for this key. The visibility of the hinted symbol
|
||||
* is controlled by the preferences. Defaults to null.
|
||||
* @property popup List of keys which will be accessible while long pressing the key. Defaults to
|
||||
* an empty list (no extended popup).
|
||||
* @property type The type of the key. Some actions require both [code] and [type] to match in order
|
||||
* to be successfully executed. Defaults to [KeyType.CHARACTER].
|
||||
* @property variation Controls if the key should only be shown in some contexts (e.g.: url input)
|
||||
* or if the key should always be visible. Defaults to [KeyVariation.ALL].
|
||||
*/
|
||||
data class KeyData(
|
||||
var code: Int,
|
||||
var label: String = "",
|
||||
var hintedNumber: KeyData? = null,
|
||||
var hintedSymbol: KeyData? = null,
|
||||
var popup: MutableList<KeyData> = mutableListOf(),
|
||||
var type: KeyType = KeyType.CHARACTER,
|
||||
var variation: KeyVariation = KeyVariation.ALL
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.text.key
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.text.key
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package dev.patrickgold.florisboard.ime.text.key
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.*
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Handler
|
||||
@@ -25,7 +24,6 @@ import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewOutlineProvider
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.core.content.ContextCompat.getDrawable
|
||||
import androidx.core.graphics.BlendModeColorFilterCompat
|
||||
import androidx.core.graphics.BlendModeCompat
|
||||
@@ -33,10 +31,13 @@ import androidx.core.view.children
|
||||
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.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeGesture
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
|
||||
import dev.patrickgold.florisboard.util.getColorFromAttr
|
||||
import dev.patrickgold.florisboard.util.setBackgroundTintColor
|
||||
import dev.patrickgold.florisboard.util.setBackgroundTintColor2
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@@ -51,8 +52,8 @@ import java.util.*
|
||||
class KeyView(
|
||||
private val keyboardView: KeyboardView,
|
||||
val data: KeyData
|
||||
) : View(keyboardView.context) {
|
||||
|
||||
) : View(keyboardView.context), SwipeGesture.Listener {
|
||||
val dataPopupWithHint: MutableList<KeyData>
|
||||
private var isKeyPressed: Boolean = false
|
||||
set(value) {
|
||||
field = value
|
||||
@@ -60,8 +61,11 @@ class KeyView(
|
||||
}
|
||||
private var osHandler: Handler? = null
|
||||
private var osTimer: Timer? = null
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
private var shouldBlockNextKeyCode: Boolean = false
|
||||
|
||||
private var desiredWidth: Int = 0
|
||||
private var desiredHeight: Int = 0
|
||||
private var drawable: Drawable? = null
|
||||
private var drawableColor: Int = 0
|
||||
private var drawablePadding: Int = 0
|
||||
@@ -70,13 +74,25 @@ class KeyView(
|
||||
alpha = 255
|
||||
color = 0
|
||||
isAntiAlias = true
|
||||
isFakeBoldText = true
|
||||
isFakeBoldText = false
|
||||
textAlign = Paint.Align.CENTER
|
||||
textSize = resources.getDimension(R.dimen.key_textSize)
|
||||
typeface = Typeface.DEFAULT
|
||||
}
|
||||
private var hintedLabel: String? = null
|
||||
private var hintedLabelPaint: Paint = Paint().apply {
|
||||
alpha = 120
|
||||
color = 0
|
||||
isAntiAlias = true
|
||||
isFakeBoldText = false
|
||||
textAlign = Paint.Align.CENTER
|
||||
textSize = resources.getDimension(R.dimen.key_textHintSize)
|
||||
typeface = Typeface.DEFAULT
|
||||
}
|
||||
private val tempRect: Rect = Rect()
|
||||
|
||||
var florisboard: FlorisBoard? = null
|
||||
private val swipeGestureDetector = SwipeGesture.Detector(context, this)
|
||||
var touchHitBox: Rect = Rect(-1, -1, -1, -1)
|
||||
|
||||
init {
|
||||
@@ -123,6 +139,23 @@ class KeyView(
|
||||
background = getDrawable(context, R.drawable.shape_rect_rounded)
|
||||
elevation = 4.0f
|
||||
|
||||
var hintKeyData: KeyData? = null
|
||||
val hintedNumber = data.hintedNumber
|
||||
if (prefs.keyboard.hintedNumberRow && hintedNumber != null) {
|
||||
hintKeyData = hintedNumber
|
||||
}
|
||||
val hintedSymbol = data.hintedSymbol
|
||||
if (prefs.keyboard.hintedSymbols && hintedSymbol != null) {
|
||||
hintKeyData = hintedSymbol
|
||||
}
|
||||
dataPopupWithHint = if (hintKeyData == null) {
|
||||
data.popup.toMutableList()
|
||||
} else {
|
||||
val popupList = data.popup.toMutableList()
|
||||
popupList.add(hintKeyData)
|
||||
popupList
|
||||
}
|
||||
|
||||
updateKeyPressedBackground()
|
||||
}
|
||||
|
||||
@@ -171,10 +204,23 @@ class KeyView(
|
||||
* go look at which child the pointer is actually above.
|
||||
*/
|
||||
fun onFlorisTouchEvent(event: MotionEvent?): Boolean {
|
||||
event ?: return false
|
||||
if (event == null || !isEnabled) return false
|
||||
if (swipeGestureDetector.onTouchEvent(event)) {
|
||||
isKeyPressed = false
|
||||
osHandler?.removeCallbacksAndMessages(null)
|
||||
osTimer?.cancel()
|
||||
osTimer = null
|
||||
keyboardView.popupManager.hide()
|
||||
return true
|
||||
}
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
keyboardView.popupManager.show(this)
|
||||
shouldBlockNextKeyCode = false
|
||||
florisboard?.prefs?.keyboard?.let {
|
||||
if (it.popupEnabled){
|
||||
keyboardView.popupManager.show(this)
|
||||
}
|
||||
}
|
||||
isKeyPressed = true
|
||||
florisboard?.keyPressVibrate()
|
||||
florisboard?.keyPressSound(data)
|
||||
@@ -190,12 +236,12 @@ class KeyView(
|
||||
}
|
||||
}, 500, 50)
|
||||
}
|
||||
val delayMillis = keyboardView.prefs.looknfeel.longPressDelay
|
||||
val delayMillis = prefs.keyboard.longPressDelay
|
||||
if (osHandler == null) {
|
||||
osHandler = Handler()
|
||||
}
|
||||
osHandler?.postDelayed({
|
||||
if (data.popup.isNotEmpty()) {
|
||||
if (dataPopupWithHint.isNotEmpty()) {
|
||||
keyboardView.popupManager.extend(this)
|
||||
}
|
||||
if (data.code == KeyCode.SPACE) {
|
||||
@@ -248,6 +294,74 @@ class KeyView(
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Swipe event handler. Listens to touch_move left/right swipes and triggers the swipe action
|
||||
* defined in the prefs.
|
||||
*/
|
||||
override fun onSwipe(direction: SwipeGesture.Direction, type: SwipeGesture.Type): Boolean {
|
||||
return when (data.code) {
|
||||
KeyCode.DELETE -> when (type) {
|
||||
SwipeGesture.Type.TOUCH_MOVE -> when (direction) {
|
||||
SwipeGesture.Direction.LEFT -> when (prefs.gestures.deleteKeySwipeLeft) {
|
||||
SwipeAction.DELETE_CHARACTERS_PRECISELY -> {
|
||||
florisboard?.activeEditorInstance?.apply {
|
||||
setSelection(
|
||||
if (selection.start > 0) { selection.start - 1 } else { selection.start },
|
||||
selection.end
|
||||
)
|
||||
}
|
||||
shouldBlockNextKeyCode = true
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
SwipeGesture.Direction.RIGHT -> when (prefs.gestures.deleteKeySwipeLeft) {
|
||||
SwipeAction.DELETE_CHARACTERS_PRECISELY -> {
|
||||
florisboard?.activeEditorInstance?.apply {
|
||||
setSelection(
|
||||
if (selection.start < selection.end) { selection.start + 1 } else { selection.start },
|
||||
selection.end
|
||||
)
|
||||
}
|
||||
shouldBlockNextKeyCode = true
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
SwipeGesture.Type.TOUCH_UP -> when (prefs.gestures.deleteKeySwipeLeft) {
|
||||
SwipeAction.DELETE_CHARACTERS_PRECISELY -> {
|
||||
florisboard?.activeEditorInstance?.apply {
|
||||
if (selection.isSelectionMode) {
|
||||
deleteBackwards()
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
KeyCode.SPACE -> when (type) {
|
||||
SwipeGesture.Type.TOUCH_MOVE -> when (direction) {
|
||||
SwipeGesture.Direction.LEFT -> {
|
||||
florisboard?.executeSwipeAction(prefs.gestures.spaceBarSwipeLeft)
|
||||
shouldBlockNextKeyCode = true
|
||||
true
|
||||
}
|
||||
SwipeGesture.Direction.RIGHT -> {
|
||||
florisboard?.executeSwipeAction(prefs.gestures.spaceBarSwipeRight)
|
||||
shouldBlockNextKeyCode = true
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Solution base from this great StackOverflow answer which explained and helped a lot
|
||||
* for handling onMeasure():
|
||||
@@ -255,7 +369,7 @@ class KeyView(
|
||||
* by Devunwired
|
||||
*/
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val desiredWidth = when (keyboardView.computedLayout?.mode) {
|
||||
desiredWidth = when (keyboardView.computedLayout?.mode) {
|
||||
KeyboardMode.NUMERIC,
|
||||
KeyboardMode.PHONE,
|
||||
KeyboardMode.PHONE2 -> (keyboardView.desiredKeyWidth * 2.68f).toInt()
|
||||
@@ -274,7 +388,7 @@ class KeyView(
|
||||
else -> keyboardView.desiredKeyWidth
|
||||
}
|
||||
}
|
||||
val desiredHeight = keyboardView.desiredKeyHeight
|
||||
desiredHeight = keyboardView.desiredKeyHeight
|
||||
|
||||
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
|
||||
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
|
||||
@@ -329,24 +443,70 @@ class KeyView(
|
||||
outlineProvider = KeyViewOutline(w, h)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the enabled state of a key depending on the [data] and its parameters.
|
||||
*/
|
||||
private fun updateEnabledState() {
|
||||
isEnabled = when (data.code) {
|
||||
KeyCode.CLIPBOARD_COPY,
|
||||
KeyCode.CLIPBOARD_CUT -> {
|
||||
florisboard?.activeEditorInstance?.selection?.isSelectionMode == true &&
|
||||
florisboard?.activeEditorInstance?.isRawInputEditor == false
|
||||
}
|
||||
KeyCode.CLIPBOARD_PASTE -> florisboard?.clipboardManager?.hasPrimaryClip() == true
|
||||
KeyCode.CLIPBOARD_SELECT_ALL -> {
|
||||
florisboard?.activeEditorInstance?.isRawInputEditor == false
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
if (!isEnabled) {
|
||||
isKeyPressed = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the background depending on [isKeyPressed] and [data].
|
||||
*/
|
||||
private fun updateKeyPressedBackground() {
|
||||
if (data.code == KeyCode.ENTER) {
|
||||
setBackgroundTintColor(
|
||||
this, when {
|
||||
isKeyPressed -> R.attr.colorPrimaryDark
|
||||
else -> R.attr.colorPrimary
|
||||
when {
|
||||
keyboardView.isSmartbarKeyboardView -> {
|
||||
elevation = 0.0f
|
||||
setBackgroundTintColor2(
|
||||
this, when {
|
||||
isKeyPressed && isEnabled -> prefs.theme.smartbarButtonBgColor
|
||||
else -> prefs.theme.smartbarBgColor
|
||||
}
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
elevation = 4.0f
|
||||
when (data.code) {
|
||||
KeyCode.ENTER -> {
|
||||
setBackgroundTintColor2(
|
||||
this, when {
|
||||
isKeyPressed && isEnabled -> prefs.theme.keyEnterBgColorPressed
|
||||
else -> prefs.theme.keyEnterBgColor
|
||||
}
|
||||
)
|
||||
}
|
||||
KeyCode.SHIFT -> {
|
||||
setBackgroundTintColor2(
|
||||
this, when {
|
||||
isKeyPressed && isEnabled -> prefs.theme.keyShiftBgColorPressed
|
||||
else -> prefs.theme.keyShiftBgColor
|
||||
}
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
setBackgroundTintColor2(
|
||||
this, when {
|
||||
isKeyPressed && isEnabled -> prefs.theme.keyBgColorPressed
|
||||
else -> prefs.theme.keyBgColor
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
setBackgroundTintColor(
|
||||
this, when {
|
||||
isKeyPressed -> R.attr.key_bgColorPressed
|
||||
else -> R.attr.key_bgColor
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,6 +542,7 @@ class KeyView(
|
||||
* TextInputManager.
|
||||
*/
|
||||
fun updateVisibility() {
|
||||
updateEnabledState()
|
||||
when (data.code) {
|
||||
KeyCode.SWITCH_TO_TEXT_CONTEXT,
|
||||
KeyCode.SWITCH_TO_MEDIA_CONTEXT -> {
|
||||
@@ -415,6 +576,37 @@ class KeyView(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically sets the text size of [boxPaint] for given [text] so it fits within the given
|
||||
* bounds.
|
||||
*
|
||||
* Implementation based on this blog post by Lucas (SketchingDev), written on Aug 20, 2015
|
||||
* https://sketchingdev.co.uk/blog/resizing-text-to-fit-into-a-container-on-android.html
|
||||
*
|
||||
* @param boxPaint The [Paint] object which the text size should be applied to.
|
||||
* @param boxWidth The max width for the surrounding box of [text].
|
||||
* @param boxHeight The max height for the surrounding box of [text].
|
||||
* @param text The text for which the size should be calculated.
|
||||
*/
|
||||
private fun setTextSizeFor(boxPaint: Paint, boxWidth: Float, boxHeight: Float, text: String) {
|
||||
var stage = 1
|
||||
var textSize = 0.0f
|
||||
while (stage < 3) {
|
||||
if (stage == 1) {
|
||||
textSize += 10.0f
|
||||
} else if (stage == 2) {
|
||||
textSize -= 1.0f
|
||||
}
|
||||
boxPaint.textSize = textSize
|
||||
boxPaint.getTextBounds(text, 0, text.length, tempRect)
|
||||
val fits = tempRect.width() < boxWidth && tempRect.height() < boxHeight
|
||||
if (stage == 1 && !fits || stage == 2 && fits) {
|
||||
stage++
|
||||
}
|
||||
}
|
||||
boxPaint.textSize = textSize
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the key label / drawable.
|
||||
*/
|
||||
@@ -426,50 +618,83 @@ class KeyView(
|
||||
updateKeyPressedBackground()
|
||||
|
||||
if (data.type == KeyType.CHARACTER && data.code != KeyCode.SPACE
|
||||
&& data.code != KeyCode.HALF_SPACE || data.type == KeyType.NUMERIC
|
||||
&& data.code != KeyCode.HALF_SPACE && data.code != KeyCode.KESHIDA || data.type == KeyType.NUMERIC
|
||||
) {
|
||||
label = getComputedLetter()
|
||||
val hintedNumber = data.hintedNumber
|
||||
if (prefs.keyboard.hintedNumberRow && hintedNumber != null) {
|
||||
hintedLabel = getComputedLetter(hintedNumber)
|
||||
}
|
||||
val hintedSymbol = data.hintedSymbol
|
||||
if (prefs.keyboard.hintedSymbols && hintedSymbol != null) {
|
||||
hintedLabel = getComputedLetter(hintedSymbol)
|
||||
}
|
||||
|
||||
} else {
|
||||
when (data.code) {
|
||||
KeyCode.ARROW_LEFT -> {
|
||||
drawable = getDrawable(context, R.drawable.ic_keyboard_arrow_left)
|
||||
drawableColor = prefs.theme.keyFgColor
|
||||
}
|
||||
KeyCode.ARROW_RIGHT -> {
|
||||
drawable = getDrawable(context, R.drawable.ic_keyboard_arrow_right)
|
||||
drawableColor = prefs.theme.keyFgColor
|
||||
}
|
||||
KeyCode.CLIPBOARD_COPY -> {
|
||||
drawable = getDrawable(context, R.drawable.ic_content_copy)
|
||||
drawableColor = prefs.theme.keyFgColor
|
||||
}
|
||||
KeyCode.CLIPBOARD_CUT -> {
|
||||
drawable = getDrawable(context, R.drawable.ic_content_cut)
|
||||
drawableColor = prefs.theme.keyFgColor
|
||||
}
|
||||
KeyCode.CLIPBOARD_PASTE -> {
|
||||
drawable = getDrawable(context, R.drawable.ic_content_paste)
|
||||
drawableColor = prefs.theme.keyFgColor
|
||||
}
|
||||
KeyCode.CLIPBOARD_SELECT_ALL -> {
|
||||
drawable = getDrawable(context, R.drawable.ic_select_all)
|
||||
drawableColor = prefs.theme.keyFgColor
|
||||
}
|
||||
KeyCode.DELETE -> {
|
||||
drawable = getDrawable(context, R.drawable.ic_backspace)
|
||||
drawableColor = getColorFromAttr(context, R.attr.key_fgColor)
|
||||
drawableColor = prefs.theme.keyFgColor
|
||||
}
|
||||
KeyCode.ENTER -> {
|
||||
val action = florisboard?.currentInputEditorInfo?.imeOptions ?: 0
|
||||
drawable = getDrawable(context, when (action and EditorInfo.IME_MASK_ACTION) {
|
||||
EditorInfo.IME_ACTION_DONE -> R.drawable.ic_done
|
||||
EditorInfo.IME_ACTION_GO -> R.drawable.ic_arrow_right_alt
|
||||
EditorInfo.IME_ACTION_NEXT -> R.drawable.ic_arrow_right_alt
|
||||
EditorInfo.IME_ACTION_NONE -> R.drawable.ic_keyboard_return
|
||||
EditorInfo.IME_ACTION_PREVIOUS -> R.drawable.ic_arrow_right_alt
|
||||
EditorInfo.IME_ACTION_SEARCH -> R.drawable.ic_search
|
||||
EditorInfo.IME_ACTION_SEND -> R.drawable.ic_send
|
||||
else -> R.drawable.ic_arrow_right_alt
|
||||
val imeOptions = florisboard?.activeEditorInstance?.imeOptions ?: ImeOptions.default()
|
||||
drawable = getDrawable(context, when (imeOptions.action) {
|
||||
ImeOptions.Action.DONE -> R.drawable.ic_done
|
||||
ImeOptions.Action.GO -> R.drawable.ic_arrow_right_alt
|
||||
ImeOptions.Action.NEXT -> R.drawable.ic_arrow_right_alt
|
||||
ImeOptions.Action.NONE -> R.drawable.ic_keyboard_return
|
||||
ImeOptions.Action.PREVIOUS -> R.drawable.ic_arrow_right_alt
|
||||
ImeOptions.Action.SEARCH -> R.drawable.ic_search
|
||||
ImeOptions.Action.SEND -> R.drawable.ic_send
|
||||
ImeOptions.Action.UNSPECIFIED -> R.drawable.ic_keyboard_return
|
||||
})
|
||||
drawableColor = getColorFromAttr(context, R.attr.key_enter_fgColor)
|
||||
if (action and EditorInfo.IME_FLAG_NO_ENTER_ACTION > 0) {
|
||||
drawableColor = prefs.theme.keyEnterFgColor
|
||||
if (imeOptions.flagNoEnterAction) {
|
||||
drawable = getDrawable(context, R.drawable.ic_keyboard_return)
|
||||
}
|
||||
}
|
||||
KeyCode.LANGUAGE_SWITCH -> {
|
||||
drawable = getDrawable(context, R.drawable.ic_language)
|
||||
drawableColor = getColorFromAttr(context, R.attr.key_fgColor)
|
||||
drawableColor = prefs.theme.keyFgColor
|
||||
}
|
||||
KeyCode.PHONE_PAUSE -> label = resources.getString(R.string.key__phone_pause)
|
||||
KeyCode.PHONE_WAIT -> label = resources.getString(R.string.key__phone_wait)
|
||||
KeyCode.SHIFT -> {
|
||||
drawable = getDrawable(context, when {
|
||||
florisboard?.textInputManager?.caps ?: false && florisboard?.textInputManager?.capsLock ?: false -> {
|
||||
drawableColor = getColorFromAttr(context, R.attr.colorAccent)
|
||||
drawableColor = prefs.theme.keyShiftFgColorCapsLock
|
||||
R.drawable.ic_keyboard_capslock
|
||||
}
|
||||
florisboard?.textInputManager?.caps ?: false && !(florisboard?.textInputManager?.capsLock ?: false) -> {
|
||||
drawableColor = getColorFromAttr(context, R.attr.key_fgColor)
|
||||
drawableColor = prefs.theme.keyShiftFgColor
|
||||
R.drawable.ic_keyboard_capslock
|
||||
}
|
||||
else -> {
|
||||
drawableColor = getColorFromAttr(context, R.attr.key_fgColor)
|
||||
drawableColor = prefs.theme.keyShiftFgColor
|
||||
R.drawable.ic_keyboard_arrow_up
|
||||
}
|
||||
})
|
||||
@@ -481,7 +706,7 @@ class KeyView(
|
||||
KeyboardMode.PHONE,
|
||||
KeyboardMode.PHONE2 -> {
|
||||
drawable = getDrawable(context, R.drawable.ic_space_bar)
|
||||
drawableColor = getColorFromAttr(context, R.attr.key_fgColor)
|
||||
drawableColor = prefs.theme.keyFgColor
|
||||
}
|
||||
KeyboardMode.CHARACTERS -> {
|
||||
label = florisboard?.activeSubtype?.locale?.displayName
|
||||
@@ -491,7 +716,7 @@ class KeyView(
|
||||
}
|
||||
KeyCode.SWITCH_TO_MEDIA_CONTEXT -> {
|
||||
drawable = getDrawable(context, R.drawable.ic_sentiment_satisfied)
|
||||
drawableColor = getColorFromAttr(context, R.attr.key_fgColor)
|
||||
drawableColor = prefs.theme.keyFgColor
|
||||
}
|
||||
KeyCode.SWITCH_TO_TEXT_CONTEXT,
|
||||
KeyCode.VIEW_CHARACTERS -> {
|
||||
@@ -516,12 +741,18 @@ class KeyView(
|
||||
KeyCode.HALF_SPACE -> {
|
||||
label = resources.getString(R.string.key__view_half_space)
|
||||
}
|
||||
KeyCode.KESHIDA -> {
|
||||
label = resources.getString(R.string.key__view_keshida)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw drawable
|
||||
val drawable = drawable
|
||||
if (drawable != null) {
|
||||
if (keyboardView.isSmartbarKeyboardView && !isEnabled) {
|
||||
drawableColor = prefs.theme.smartbarFgColorAlt
|
||||
}
|
||||
var marginV = 0
|
||||
var marginH = 0
|
||||
if (measuredWidth > measuredHeight) {
|
||||
@@ -544,20 +775,40 @@ class KeyView(
|
||||
// Draw label
|
||||
val label = label
|
||||
if (label != null) {
|
||||
if (data.code == KeyCode.VIEW_NUMERIC || data.code == KeyCode.VIEW_NUMERIC_ADVANCED
|
||||
|| data.code == KeyCode.SPACE) {
|
||||
labelPaint.textSize = resources.getDimension(R.dimen.key_numeric_textSize)
|
||||
} else {
|
||||
labelPaint.textSize = resources.getDimension(R.dimen.key_textSize)
|
||||
when (data.code) {
|
||||
KeyCode.VIEW_NUMERIC, KeyCode.VIEW_NUMERIC_ADVANCED -> {
|
||||
labelPaint.textSize = resources.getDimension(R.dimen.key_numeric_textSize)
|
||||
}
|
||||
else -> when {
|
||||
data.type == KeyType.CHARACTER && data.code != KeyCode.SPACE -> {
|
||||
setTextSizeFor(
|
||||
labelPaint,
|
||||
desiredWidth - (2.2f * drawablePadding),
|
||||
desiredHeight - (3.0f * drawablePadding),
|
||||
// Note: taking a "X" here because it is one of the biggest letters and
|
||||
// the keys must have the same base character for calculation, else
|
||||
// they will all look different and weird...
|
||||
"X"
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
setTextSizeFor(
|
||||
labelPaint,
|
||||
measuredWidth - (2.6f * drawablePadding),
|
||||
measuredHeight - (3.6f * drawablePadding),
|
||||
when (data.code) {
|
||||
KeyCode.VIEW_CHARACTERS, KeyCode.VIEW_SYMBOLS, KeyCode.VIEW_SYMBOLS2 -> {
|
||||
resources.getString(R.string.key__view_symbols)
|
||||
}
|
||||
else -> label
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
labelPaint.color = getColorFromAttr(context, R.attr.key_fgColor)
|
||||
labelPaint.color = prefs.theme.keyFgColor
|
||||
labelPaint.alpha = if (keyboardView.computedLayout?.mode == KeyboardMode.CHARACTERS &&
|
||||
data.code == KeyCode.SPACE) { 120 } else { 255 }
|
||||
val isPortrait =
|
||||
resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
|
||||
if (keyboardView.prefs.looknfeel.oneHandedMode != "off" && isPortrait) {
|
||||
labelPaint.textSize *= 0.9f
|
||||
}
|
||||
val centerX = measuredWidth / 2.0f
|
||||
val centerY = measuredHeight / 2.0f + (labelPaint.textSize - labelPaint.descent()) / 2
|
||||
if (label.contains("\n")) {
|
||||
@@ -569,6 +820,25 @@ class KeyView(
|
||||
canvas.drawText(label, centerX, centerY, labelPaint)
|
||||
}
|
||||
}
|
||||
|
||||
// Draw hinted label
|
||||
val hintedLabel = hintedLabel
|
||||
if (hintedLabel != null) {
|
||||
setTextSizeFor(
|
||||
hintedLabelPaint,
|
||||
desiredWidth * 1.0f / 6.0f,
|
||||
desiredHeight * 1.0f / 6.0f,
|
||||
// Note: taking a "X" here because it is one of the biggest letters and
|
||||
// the keys must have the same base character for calculation, else
|
||||
// they will all look different and weird...
|
||||
"X"
|
||||
)
|
||||
hintedLabelPaint.color = prefs.theme.keyFgColor
|
||||
hintedLabelPaint.alpha = 120
|
||||
val centerX = measuredWidth * 5.0f / 6.0f
|
||||
val centerY = measuredHeight * 1.0f / 6.0f + (hintedLabelPaint.textSize - hintedLabelPaint.descent()) / 2
|
||||
canvas.drawText(hintedLabel, centerX, centerY, hintedLabelPaint)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,10 +18,13 @@ package dev.patrickgold.florisboard.ime.text.keyboard
|
||||
|
||||
enum class KeyboardMode {
|
||||
CHARACTERS,
|
||||
EDITING,
|
||||
SYMBOLS,
|
||||
SYMBOLS2,
|
||||
NUMERIC,
|
||||
NUMERIC_ADVANCED,
|
||||
PHONE,
|
||||
PHONE2
|
||||
PHONE2,
|
||||
SMARTBAR_CLIPBOARD_CURSOR_ROW,
|
||||
SMARTBAR_NUMBER_ROW
|
||||
}
|
||||
|
||||
@@ -18,9 +18,6 @@ package dev.patrickgold.florisboard.ime.text.keyboard
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewGroup
|
||||
@@ -32,25 +29,27 @@ import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.popup.KeyPopupManager
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyView
|
||||
import dev.patrickgold.florisboard.ime.text.layout.ComputedLayoutData
|
||||
import dev.patrickgold.florisboard.util.getColorFromAttr
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeGesture
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Manages the layout of the keyboard, key measurement, key selection and all touch events.
|
||||
* Supports multi touch events.
|
||||
*
|
||||
* TODO: Implement swipe gesture support
|
||||
* Supports multi touch events. Note that the keyboard's background is transparent. The 'real'
|
||||
* background of this keyboard is the background of the underlying mainViewFlipper. This prevents
|
||||
* rendering issues when a keyboard is being loaded for the first time.
|
||||
*
|
||||
* @property florisboard Reference to instance of core class [FlorisBoard].
|
||||
*/
|
||||
class KeyboardView : LinearLayout {
|
||||
class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Listener {
|
||||
private var activeKeyView: KeyView? = null
|
||||
private var activePointerId: Int? = null
|
||||
private var activeX: Float = 0.0f
|
||||
private var activeY: Float = 0.0f
|
||||
|
||||
private var colorDrawable: ColorDrawable
|
||||
var computedLayout: ComputedLayoutData? = null
|
||||
set(v) {
|
||||
field = v
|
||||
@@ -58,21 +57,24 @@ class KeyboardView : LinearLayout {
|
||||
}
|
||||
var desiredKeyWidth: Int = resources.getDimension(R.dimen.key_width).toInt()
|
||||
var desiredKeyHeight: Int = resources.getDimension(R.dimen.key_height).toInt()
|
||||
var florisboard: FlorisBoard? = null
|
||||
var florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
|
||||
private var initialKeyCode: Int = 0
|
||||
var isPreviewMode: Boolean = false
|
||||
var isSmartbarKeyboardView: Boolean = false
|
||||
var popupManager = KeyPopupManager<KeyboardView, KeyView>(this)
|
||||
lateinit var prefs: PrefHelper
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
private val swipeGestureDetector = SwipeGesture.Detector(context, this)
|
||||
|
||||
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) {
|
||||
colorDrawable = ColorDrawable(getColorFromAttr(context, R.attr.keyboard_bgColor))
|
||||
background = colorDrawable
|
||||
orientation = VERTICAL
|
||||
layoutParams = layoutParams ?: FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
florisboard?.addEventListener(this)
|
||||
onWindowShown()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,6 +109,13 @@ class KeyboardView : LinearLayout {
|
||||
popupManager.dismissAllPopups()
|
||||
}
|
||||
|
||||
override fun onWindowShown() {
|
||||
swipeGestureDetector.apply {
|
||||
distanceThreshold = prefs.gestures.swipeDistanceThreshold
|
||||
velocityThreshold = prefs.gestures.swipeVelocityThreshold
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Catch all events which are designated for child views.
|
||||
*/
|
||||
@@ -124,6 +133,12 @@ class KeyboardView : LinearLayout {
|
||||
return false
|
||||
}
|
||||
val eventFloris = MotionEvent.obtainNoHistory(event)
|
||||
if (!isSmartbarKeyboardView && swipeGestureDetector.onTouchEvent(event)) {
|
||||
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_CANCEL)
|
||||
activeKeyView = null
|
||||
activePointerId = null
|
||||
return true
|
||||
}
|
||||
val pointerIndex = event.actionIndex
|
||||
var pointerId = event.getPointerId(pointerIndex)
|
||||
when (event.actionMasked) {
|
||||
@@ -134,6 +149,7 @@ class KeyboardView : LinearLayout {
|
||||
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
|
||||
@@ -142,6 +158,7 @@ class KeyboardView : LinearLayout {
|
||||
activeX = event.getX(pointerIndex)
|
||||
activeY = event.getY(pointerIndex)
|
||||
searchForActiveKeyView()
|
||||
initialKeyCode = activeKeyView?.data?.code ?: 0
|
||||
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_DOWN)
|
||||
}
|
||||
}
|
||||
@@ -199,6 +216,46 @@ class KeyboardView : LinearLayout {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Swipe event handler. Listens to touch_up swipes and executes the swipe action defined for it
|
||||
* in the prefs.
|
||||
*/
|
||||
override fun onSwipe(direction: SwipeGesture.Direction, type: SwipeGesture.Type): Boolean {
|
||||
return when {
|
||||
initialKeyCode == KeyCode.DELETE -> {
|
||||
if (type == SwipeGesture.Type.TOUCH_UP && direction == SwipeGesture.Direction.LEFT &&
|
||||
prefs.gestures.deleteKeySwipeLeft == SwipeAction.DELETE_WORD) {
|
||||
florisboard?.executeSwipeAction(prefs.gestures.deleteKeySwipeLeft)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
initialKeyCode > KeyCode.SPACE && !popupManager.isShowingExtendedPopup -> when {
|
||||
!prefs.glide.enabled -> when (type) {
|
||||
SwipeGesture.Type.TOUCH_UP -> {
|
||||
val swipeAction = when (direction) {
|
||||
SwipeGesture.Direction.UP -> prefs.gestures.swipeUp
|
||||
SwipeGesture.Direction.DOWN -> prefs.gestures.swipeDown
|
||||
SwipeGesture.Direction.LEFT -> prefs.gestures.swipeLeft
|
||||
SwipeGesture.Direction.RIGHT -> prefs.gestures.swipeRight
|
||||
else -> SwipeAction.NO_ACTION
|
||||
}
|
||||
if (swipeAction != SwipeAction.NO_ACTION) {
|
||||
florisboard?.executeSwipeAction(swipeAction)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for an active key view at [activeX]/[activeY].
|
||||
*/
|
||||
@@ -232,37 +289,31 @@ class KeyboardView : LinearLayout {
|
||||
* The desired key heights/widths are being calculated here.
|
||||
*/
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
|
||||
|
||||
val keyMarginH = resources.getDimension((R.dimen.key_marginH)).toInt()
|
||||
desiredKeyWidth = (widthSize / 10) - (2 * keyMarginH)
|
||||
val keyMarginV = resources.getDimension((R.dimen.key_marginV)).toInt()
|
||||
|
||||
val factor = prefs.looknfeel.heightFactor
|
||||
val keyHeightFactor = when (resources.configuration.orientation) {
|
||||
Configuration.ORIENTATION_LANDSCAPE -> 0.85f
|
||||
else -> if (prefs.looknfeel.oneHandedMode == "start" ||
|
||||
prefs.looknfeel.oneHandedMode == "end") {
|
||||
0.9f
|
||||
} else {
|
||||
1.0f
|
||||
}
|
||||
} * when (factor) {
|
||||
"extra_short" -> 0.85f
|
||||
"short" -> 0.90f
|
||||
"mid_short" -> 0.95f
|
||||
"normal" -> 1.00f
|
||||
"mid_tall" -> 1.05f
|
||||
"tall" -> 1.10f
|
||||
"extra_tall" -> 1.15f
|
||||
else -> 1.00f
|
||||
} * when (isPreviewMode) {
|
||||
true -> 0.90f
|
||||
else -> 1.00f
|
||||
val desiredWidth = MeasureSpec.getSize(widthMeasureSpec).toFloat()
|
||||
desiredKeyWidth = if (isSmartbarKeyboardView) {
|
||||
(desiredWidth / 6.0f - 2.0f * keyMarginH).roundToInt()
|
||||
} else {
|
||||
(desiredWidth / 10.0f - 2.0f * keyMarginH).roundToInt()
|
||||
}
|
||||
desiredKeyHeight = (resources.getDimension(R.dimen.key_height) * keyHeightFactor).toInt()
|
||||
florisboard?.textInputManager?.smartbarManager?.smartbarView?.setHeightFactor(keyHeightFactor)
|
||||
val desiredHeight = MeasureSpec.getSize(heightMeasureSpec) * if (isPreviewMode) { 0.90f } else { 1.00f }
|
||||
desiredKeyHeight = when {
|
||||
isSmartbarKeyboardView -> desiredHeight - 1.5f * keyMarginV
|
||||
else -> desiredHeight / 4.0f - 2.0f * keyMarginV
|
||||
}.roundToInt()
|
||||
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
super.onMeasure(
|
||||
MeasureSpec.makeMeasureSpec(desiredWidth.roundToInt(), MeasureSpec.EXACTLY),
|
||||
MeasureSpec.makeMeasureSpec(desiredHeight.roundToInt(), MeasureSpec.EXACTLY)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onApplyThemeAttributes() {
|
||||
if (isPreviewMode) {
|
||||
setBackgroundColor(prefs.theme.keyboardBgColor)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -309,10 +360,4 @@ class KeyboardView : LinearLayout {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas?) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
colorDrawable.color = getColorFromAttr(context, R.attr.keyboard_bgColor)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,10 +22,7 @@ import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyData
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyTypeAdapter
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyVariation
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyVariationAdapter
|
||||
import dev.patrickgold.florisboard.ime.text.key.*
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.*
|
||||
@@ -37,18 +34,19 @@ private typealias KMS = Pair<KeyboardMode, Subtype>
|
||||
* Class which manages layout loading and caching.
|
||||
*/
|
||||
class LayoutManager(private val context: Context) : CoroutineScope by MainScope() {
|
||||
private val layoutCache: HashMap<KMS, Deferred<ComputedLayoutData>> = hashMapOf()
|
||||
private val computedLayoutCache: HashMap<KMS, Deferred<ComputedLayoutData>> = hashMapOf()
|
||||
|
||||
/**
|
||||
* Loads the layout for the specified type and name.
|
||||
*
|
||||
* @returns the [LayoutData] or null.
|
||||
* @return the [LayoutData] or null.
|
||||
*/
|
||||
private fun loadLayout(ltn: LTN?) = loadLayout(ltn?.first, ltn?.second)
|
||||
private fun loadLayout(type: LayoutType?, name: String?): LayoutData? {
|
||||
if (type == null || name == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val rawJsonData: String = try {
|
||||
context.assets.open("ime/text/$type/$name.json").bufferedReader().use { it.readText() }
|
||||
} catch (e: Exception) {
|
||||
@@ -108,9 +106,9 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
|
||||
* @param main The main layout type and name.
|
||||
* @param modifier The modifier (mod) layout type and name.
|
||||
* @param extension The extension layout type and name.
|
||||
* @returns a [ComputedLayoutData] object, regardless of the specified LTNs or errors.
|
||||
* @return a [ComputedLayoutData] object, regardless of the specified LTNs or errors.
|
||||
*/
|
||||
private fun mergeLayouts(
|
||||
private suspend fun mergeLayoutsAsync(
|
||||
keyboardMode: KeyboardMode,
|
||||
subtype: Subtype,
|
||||
main: LTN? = null,
|
||||
@@ -194,6 +192,28 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
|
||||
}
|
||||
}
|
||||
|
||||
// Add hints to keys
|
||||
if (keyboardMode == KeyboardMode.CHARACTERS) {
|
||||
val symbolsComputedArrangement = fetchComputedLayoutAsync(KeyboardMode.SYMBOLS, subtype).await().arrangement
|
||||
for ((r, row) in computedArrangement.withIndex()) {
|
||||
if (r >= 3) {
|
||||
break
|
||||
}
|
||||
if (symbolsComputedArrangement.getOrNull(r) != null) {
|
||||
for ((k, key) in row.withIndex()) {
|
||||
if (key.type == KeyType.CHARACTER) {
|
||||
val symbol = symbolsComputedArrangement[r].getOrNull(k)
|
||||
if (r == 0) {
|
||||
key.hintedNumber = symbol
|
||||
} else {
|
||||
key.hintedSymbol = symbol
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ComputedLayoutData(
|
||||
keyboardMode,
|
||||
"computed",
|
||||
@@ -210,7 +230,7 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
|
||||
* @param keyboardMode The keyboard mode for which the layout should be computed.
|
||||
* @param subtype The subtype which localizes the computed layout.
|
||||
*/
|
||||
private fun computeLayoutFor(
|
||||
private suspend fun computeLayoutFor(
|
||||
keyboardMode: KeyboardMode,
|
||||
subtype: Subtype
|
||||
): ComputedLayoutData {
|
||||
@@ -223,6 +243,9 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
|
||||
main = LTN(LayoutType.CHARACTERS, subtype.layout)
|
||||
modifier = LTN(LayoutType.CHARACTERS_MOD, "default")
|
||||
}
|
||||
KeyboardMode.EDITING -> {
|
||||
// Layout for this mode is defined in custom layout xml file.
|
||||
}
|
||||
KeyboardMode.NUMERIC -> {
|
||||
main = LTN(LayoutType.NUMERIC, "default")
|
||||
}
|
||||
@@ -244,9 +267,15 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
|
||||
main = LTN(LayoutType.SYMBOLS2, "western_default")
|
||||
modifier = LTN(LayoutType.SYMBOLS2_MOD, "default")
|
||||
}
|
||||
KeyboardMode.SMARTBAR_CLIPBOARD_CURSOR_ROW -> {
|
||||
extension = LTN(LayoutType.EXTENSION, "clipboard_cursor_row")
|
||||
}
|
||||
KeyboardMode.SMARTBAR_NUMBER_ROW -> {
|
||||
extension = LTN(LayoutType.EXTENSION, "number_row")
|
||||
}
|
||||
}
|
||||
|
||||
return mergeLayouts(keyboardMode, subtype, main, modifier, extension)
|
||||
return mergeLayoutsAsync(keyboardMode, subtype, main, modifier, extension)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -263,14 +292,14 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
|
||||
subtype: Subtype
|
||||
): Deferred<ComputedLayoutData> {
|
||||
val kms = KMS(keyboardMode, subtype)
|
||||
val cachedComputedLayout = layoutCache[kms]
|
||||
val cachedComputedLayout = computedLayoutCache[kms]
|
||||
return if (cachedComputedLayout != null) {
|
||||
cachedComputedLayout
|
||||
} else {
|
||||
val computedLayout = async(Dispatchers.IO) {
|
||||
computeLayoutFor(keyboardMode, subtype)
|
||||
}
|
||||
layoutCache[kms] = computedLayout
|
||||
computedLayoutCache[kms] = computedLayout
|
||||
computedLayout
|
||||
}
|
||||
}
|
||||
@@ -289,8 +318,8 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
|
||||
subtype: Subtype
|
||||
) {
|
||||
val kms = KMS(keyboardMode, subtype)
|
||||
if (layoutCache[kms] == null) {
|
||||
layoutCache[kms] = async(Dispatchers.IO) {
|
||||
if (computedLayoutCache[kms] == null) {
|
||||
computedLayoutCache[kms] = async(Dispatchers.IO) {
|
||||
computeLayoutFor(keyboardMode, subtype)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +1,83 @@
|
||||
package dev.patrickgold.florisboard.ime.text.smartbar
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.textservice.SentenceSuggestionsInfo
|
||||
import android.view.textservice.SpellCheckerSession
|
||||
import android.view.textservice.SuggestionsInfo
|
||||
import android.view.textservice.TextServicesManager
|
||||
import android.widget.Button
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.view.children
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.EditorInstance
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||
import dev.patrickgold.florisboard.ime.text.TextInputManager
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyData
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
// TODO: Implement suggestion creation functionality
|
||||
// TODO: Cleanup and reorganize SmartbarManager
|
||||
class SmartbarManager private constructor() :
|
||||
SpellCheckerSession.SpellCheckerSessionListener, FlorisBoard.EventListener {
|
||||
class SmartbarManager private constructor() : CoroutineScope by MainScope(),
|
||||
FlorisBoard.EventListener {
|
||||
|
||||
private val florisboard: FlorisBoard = FlorisBoard.getInstance()
|
||||
private var isComposingEnabled: Boolean = false
|
||||
private var spellCheckerSession: SpellCheckerSession? = null
|
||||
private val activeEditorInstance: EditorInstance
|
||||
get() = florisboard.activeEditorInstance
|
||||
private val prefs: PrefHelper
|
||||
get() = florisboard.prefs
|
||||
|
||||
private val textInputManager: TextInputManager = TextInputManager.getInstance()
|
||||
var smartbarView: SmartbarView? = null
|
||||
private set
|
||||
private var shouldSuggestClipboardContents: Boolean = false
|
||||
private var smartbarView: SmartbarView? = null
|
||||
|
||||
var isQuickActionsVisible: Boolean = false
|
||||
set(value) { field = value; updateActiveContainerVisibility() }
|
||||
|
||||
private val candidateViewOnClickListener = View.OnClickListener { v ->
|
||||
val view = v as Button
|
||||
val text = view.text.toString()
|
||||
if (text.isNotEmpty()) {
|
||||
textInputManager.commitCandidate(text)
|
||||
florisboard.activeEditorInstance.commitCompletion(text)
|
||||
}
|
||||
}
|
||||
private val candidateViewOnLongClickListener = View.OnLongClickListener { v ->
|
||||
true
|
||||
}
|
||||
private val numberRowButtonOnClickListener = View.OnClickListener { v ->
|
||||
val keyData = when (v.id) {
|
||||
R.id.number_row_0 -> KeyData(48, "0")
|
||||
R.id.number_row_1 -> KeyData(49, "1")
|
||||
R.id.number_row_2 -> KeyData(50, "2")
|
||||
R.id.number_row_3 -> KeyData(51, "3")
|
||||
R.id.number_row_4 -> KeyData(52, "4")
|
||||
R.id.number_row_5 -> KeyData(53, "5")
|
||||
R.id.number_row_6 -> KeyData(54, "6")
|
||||
R.id.number_row_7 -> KeyData(55, "7")
|
||||
R.id.number_row_8 -> KeyData(56, "8")
|
||||
R.id.number_row_9 -> KeyData(57, "9")
|
||||
else -> KeyData(0)
|
||||
}
|
||||
florisboard.textInputManager.sendKeyPress(keyData)
|
||||
private val clipboardSuggestionViewOnClickListener = View.OnClickListener {
|
||||
activeEditorInstance.performClipboardPaste()
|
||||
shouldSuggestClipboardContents = false
|
||||
updateActiveContainerVisibility()
|
||||
}
|
||||
private val quickActionOnClickListener = View.OnClickListener { v ->
|
||||
isQuickActionsVisible = false
|
||||
when (v.id) {
|
||||
R.id.back_button -> {
|
||||
florisboard.textInputManager.setActiveKeyboardMode(KeyboardMode.CHARACTERS)
|
||||
smartbarView?.setActiveVariant(R.id.smartbar_variant_default)
|
||||
}
|
||||
R.id.quick_action_switch_to_editing_context -> {
|
||||
if (florisboard.textInputManager.getActiveKeyboardMode() == KeyboardMode.EDITING) {
|
||||
florisboard.textInputManager.setActiveKeyboardMode(KeyboardMode.CHARACTERS)
|
||||
smartbarView?.setActiveVariant(R.id.smartbar_variant_default)
|
||||
} else {
|
||||
florisboard.textInputManager.setActiveKeyboardMode(KeyboardMode.EDITING)
|
||||
smartbarView?.setActiveVariant(R.id.smartbar_variant_back_only)
|
||||
}
|
||||
}
|
||||
R.id.quick_action_switch_to_media_context -> florisboard.setActiveInput(R.id.media_input)
|
||||
R.id.quick_action_open_settings -> florisboard.launchSettings()
|
||||
R.id.quick_action_one_handed_toggle -> florisboard.toggleOneHandedMode()
|
||||
else -> return@OnClickListener
|
||||
}
|
||||
isQuickActionsVisible = false
|
||||
updateSmartbarUI()
|
||||
}
|
||||
private val quickActionToggleOnClickListener = View.OnClickListener {
|
||||
isQuickActionsVisible = !isQuickActionsVisible
|
||||
updateSmartbarUI()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -89,27 +97,46 @@ class SmartbarManager private constructor() :
|
||||
|
||||
this.smartbarView = smartbarView
|
||||
|
||||
smartbarView.quickActionToggle?.setOnClickListener(quickActionToggleOnClickListener)
|
||||
smartbarView.findViewById<View>(R.id.quick_action_toggle)?.setOnClickListener(quickActionToggleOnClickListener)
|
||||
val quickActions = smartbarView.findViewById<LinearLayout>(R.id.quick_actions)
|
||||
for (quickAction in quickActions.children) {
|
||||
if (quickAction is ImageButton) {
|
||||
quickAction.setOnClickListener(quickActionOnClickListener)
|
||||
}
|
||||
}
|
||||
val numberRow = smartbarView.findViewById<LinearLayout>(R.id.number_row)
|
||||
for (numberRowButton in numberRow.children) {
|
||||
if (numberRowButton is Button) {
|
||||
numberRowButton.setOnClickListener(numberRowButtonOnClickListener)
|
||||
launch(Dispatchers.Default) {
|
||||
val numberRow = smartbarView.findViewById<KeyboardView>(R.id.smartbar_variant_number_row)
|
||||
numberRow.isSmartbarKeyboardView = true
|
||||
val layout = textInputManager.layoutManager.fetchComputedLayoutAsync(KeyboardMode.SMARTBAR_NUMBER_ROW, Subtype.DEFAULT).await()
|
||||
launch(Dispatchers.Main) {
|
||||
numberRow.computedLayout = layout
|
||||
numberRow.updateVisibility()
|
||||
}
|
||||
}
|
||||
val clipboardSuggestion = smartbarView.findViewById<Button>(R.id.clipboard_suggestion)
|
||||
clipboardSuggestion.setOnClickListener(clipboardSuggestionViewOnClickListener)
|
||||
launch(Dispatchers.Default) {
|
||||
val ccRow = smartbarView.findViewById<KeyboardView>(R.id.clipboard_cursor_row)
|
||||
ccRow.isSmartbarKeyboardView = true
|
||||
val layout = textInputManager.layoutManager.fetchComputedLayoutAsync(KeyboardMode.SMARTBAR_CLIPBOARD_CURSOR_ROW, Subtype.DEFAULT).await()
|
||||
launch(Dispatchers.Main) {
|
||||
ccRow.computedLayout = layout
|
||||
ccRow.updateVisibility()
|
||||
}
|
||||
}
|
||||
val backButton = smartbarView.findViewById<View>(R.id.back_button)
|
||||
backButton.setOnClickListener(quickActionOnClickListener)
|
||||
for (candidateView in smartbarView.candidateViewList) {
|
||||
candidateView.setOnClickListener(candidateViewOnClickListener)
|
||||
candidateView.setOnLongClickListener(candidateViewOnLongClickListener)
|
||||
}
|
||||
|
||||
updateSmartbarUI()
|
||||
}
|
||||
|
||||
override fun onWindowShown() {
|
||||
isQuickActionsVisible = false
|
||||
updateActiveContainerVisibility()
|
||||
}
|
||||
|
||||
// TODO: clean up resources here
|
||||
@@ -119,61 +146,31 @@ class SmartbarManager private constructor() :
|
||||
instance = null
|
||||
}
|
||||
|
||||
override fun onGetSuggestions(arr: Array<out SuggestionsInfo>?) {
|
||||
if (arr == null || arr.isEmpty()) {
|
||||
return
|
||||
}
|
||||
/*val suggestions = arr[0]
|
||||
for (i in 0 until suggestions.suggestionsCount) {
|
||||
candidateViewList[i].text = suggestions.getSuggestionAt(i)
|
||||
if (i == 2) {
|
||||
break
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
override fun onGetSentenceSuggestions(arr: Array<out SentenceSuggestionsInfo>?) {
|
||||
if (arr == null || arr.isEmpty()) {
|
||||
return
|
||||
}
|
||||
/*val suggestions = arr[0].getSuggestionsInfoAt(0)
|
||||
for (i in 0 until suggestions.suggestionsCount) {
|
||||
candidateViewList[i].text = suggestions.getSuggestionAt(i)
|
||||
if (i == 2) {
|
||||
break
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
fun onStartInputView(keyboardMode: KeyboardMode, isComposingEnabled: Boolean) {
|
||||
this.isComposingEnabled = isComposingEnabled
|
||||
fun onStartInputView(keyboardMode: KeyboardMode) {
|
||||
when (keyboardMode) {
|
||||
KeyboardMode.NUMERIC, KeyboardMode.PHONE, KeyboardMode.PHONE2 -> {
|
||||
smartbarView?.visibility = View.GONE
|
||||
smartbarView?.setActiveVariant(null)
|
||||
}
|
||||
else -> {
|
||||
smartbarView?.visibility = View.VISIBLE
|
||||
smartbarView?.setActiveVariant(R.id.smartbar_variant_default)
|
||||
isQuickActionsVisible = false
|
||||
}
|
||||
}
|
||||
updateSmartbarUI()
|
||||
}
|
||||
|
||||
fun onFinishInputView() {
|
||||
//spellCheckerSession?.close()
|
||||
}
|
||||
|
||||
fun deleteCandidateFromDictionary(candidate: String) {
|
||||
//
|
||||
override fun onUpdateSelection() {
|
||||
updateSmartbarUI()
|
||||
}
|
||||
|
||||
fun resetCandidates() {
|
||||
//
|
||||
}
|
||||
|
||||
fun generateCandidatesFromComposing(composingText: String?) {
|
||||
fun generateCandidatesFromComposing(composingText: String) {
|
||||
val smartbarView = smartbarView ?: return
|
||||
|
||||
if (composingText == null) {
|
||||
if (composingText == "") {
|
||||
smartbarView.candidateViewList[0].text = "candidate"
|
||||
smartbarView.candidateViewList[1].text = "suggestions"
|
||||
smartbarView.candidateViewList[2].text = "nyi"
|
||||
@@ -182,59 +179,89 @@ class SmartbarManager private constructor() :
|
||||
smartbarView.candidateViewList[1].text = composingText + "test"
|
||||
smartbarView.candidateViewList[2].text = ""
|
||||
}
|
||||
//spellCheckerSession?.getSentenceSuggestions(arrayOf(TextInfo(composing)), 3)
|
||||
//android.util.Log.i("SPELL", "GEN")
|
||||
/*val dic: Uri = UserDictionary.Words.CONTENT_URI
|
||||
val resolver: ContentResolver = florisboard.contentResolver
|
||||
val cursor: Cursor = resolver.query(dic, null, null, null, null) ?: return
|
||||
var count = 0
|
||||
while (cursor.moveToNext()) {
|
||||
val word = cursor.getString(cursor.getColumnIndex(UserDictionary.Words.WORD))
|
||||
candidateViewList[count].text = word
|
||||
if (count++ > 2) {
|
||||
break
|
||||
}
|
||||
}
|
||||
cursor.close()*/
|
||||
}
|
||||
|
||||
fun writeCandidate(candidate: String) {
|
||||
//
|
||||
}
|
||||
|
||||
fun getPreferredContainerId(): Int {
|
||||
return when {
|
||||
!isComposingEnabled -> when(textInputManager.getActiveKeyboardMode()) {
|
||||
KeyboardMode.CHARACTERS -> R.id.number_row
|
||||
else -> 0
|
||||
}
|
||||
else -> R.id.candidates
|
||||
override fun onPrimaryClipChanged() {
|
||||
if (prefs.suggestion.enabled && prefs.suggestion.suggestClipboardContent) {
|
||||
shouldSuggestClipboardContents = true
|
||||
updateActiveContainerVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateActiveContainerVisibility() {
|
||||
fun resetClipboardSuggestion() {
|
||||
if (prefs.suggestion.enabled && prefs.suggestion.suggestClipboardContent) {
|
||||
shouldSuggestClipboardContents = false
|
||||
updateActiveContainerVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSmartbarUI() {
|
||||
val ei = activeEditorInstance
|
||||
if (ei.selection.isCursorMode && ei.isComposingEnabled) {
|
||||
generateCandidatesFromComposing(ei.currentWord.text)
|
||||
}
|
||||
updateActiveContainerVisibility()
|
||||
val ccRow = smartbarView?.findViewById<KeyboardView>(R.id.clipboard_cursor_row)
|
||||
ccRow?.updateVisibility()
|
||||
}
|
||||
|
||||
fun updateActiveContainerVisibility() {
|
||||
val smartbarView = smartbarView ?: return
|
||||
|
||||
if (isQuickActionsVisible) {
|
||||
smartbarView.candidatesView?.visibility = View.GONE
|
||||
smartbarView.numberRowView?.visibility = View.GONE
|
||||
smartbarView.quickActionsView?.visibility = View.VISIBLE
|
||||
smartbarView.quickActionToggle?.rotation = -180.0f
|
||||
smartbarView.setActiveVariant(R.id.smartbar_variant_default)
|
||||
smartbarView.setActiveContainer(R.id.quick_actions)
|
||||
smartbarView.findViewById<View>(R.id.quick_action_toggle)?.rotation = -180.0f
|
||||
} else {
|
||||
if (florisboard.prefs.suggestion.enabled) {
|
||||
smartbarView.candidatesView?.visibility = View.VISIBLE
|
||||
smartbarView.numberRowView?.visibility = View.GONE
|
||||
smartbarView.quickActionsView?.visibility = View.GONE
|
||||
} else if (textInputManager.getActiveKeyboardMode() == KeyboardMode.CHARACTERS) {
|
||||
smartbarView.candidatesView?.visibility = View.GONE
|
||||
smartbarView.numberRowView?.visibility = View.VISIBLE
|
||||
smartbarView.quickActionsView?.visibility = View.GONE
|
||||
} else {
|
||||
smartbarView.candidatesView?.visibility = View.GONE
|
||||
smartbarView.numberRowView?.visibility = View.GONE
|
||||
smartbarView.quickActionsView?.visibility = View.GONE
|
||||
when {
|
||||
textInputManager.getActiveKeyboardMode() == KeyboardMode.EDITING -> {
|
||||
smartbarView.setActiveVariant(R.id.smartbar_variant_back_only)
|
||||
smartbarView.setActiveContainer(null)
|
||||
}
|
||||
activeEditorInstance.isComposingEnabled -> {
|
||||
smartbarView.setActiveVariant(R.id.smartbar_variant_default)
|
||||
val containerId = if (shouldSuggestClipboardContents && florisboard.clipboardManager?.hasPrimaryClip() == true) {
|
||||
val clipboardSuggestion = smartbarView.findViewById<Button>(R.id.clipboard_suggestion)
|
||||
val item = florisboard.clipboardManager?.primaryClip?.getItemAt(0)
|
||||
when {
|
||||
item?.text != null -> {
|
||||
clipboardSuggestion?.text = item.text
|
||||
}
|
||||
item?.uri != null -> {
|
||||
clipboardSuggestion?.text = "(Image) " + item.uri.toString()
|
||||
}
|
||||
else -> {
|
||||
clipboardSuggestion?.text = item?.text ?: "(Error while retrieving clipboard data)"
|
||||
}
|
||||
}
|
||||
R.id.clipboard_suggestion_row
|
||||
} else {
|
||||
R.id.candidates
|
||||
}
|
||||
smartbarView.setActiveContainer(containerId)
|
||||
}
|
||||
textInputManager.getActiveKeyboardMode() == KeyboardMode.CHARACTERS -> {
|
||||
when (prefs.suggestion.showInstead) {
|
||||
"number_row" -> {
|
||||
smartbarView.setActiveVariant(R.id.smartbar_variant_number_row)
|
||||
smartbarView.setActiveContainer(null)
|
||||
}
|
||||
"clipboard_cursor_tools" -> {
|
||||
smartbarView.setActiveVariant(R.id.smartbar_variant_default)
|
||||
smartbarView.setActiveContainer(R.id.clipboard_cursor_row)
|
||||
}
|
||||
else -> {
|
||||
smartbarView.setActiveVariant(null)
|
||||
smartbarView.setActiveContainer(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
smartbarView.setActiveVariant(null)
|
||||
smartbarView.setActiveContainer(null)
|
||||
}
|
||||
}
|
||||
smartbarView.quickActionToggle?.rotation = 0.0f
|
||||
smartbarView.findViewById<View>(R.id.quick_action_toggle)?.rotation = 0.0f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,17 @@
|
||||
package dev.patrickgold.florisboard.ime.text.smartbar
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.util.AttributeSet
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.util.setBackgroundTintColor2
|
||||
|
||||
/**
|
||||
* Basically the same as an ImageButton.
|
||||
* @see [onMeasure] why this view class exists.
|
||||
*/
|
||||
class SmartbarQuickActionButton : androidx.appcompat.widget.AppCompatImageButton {
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
|
||||
constructor(context: Context) : this(context, null)
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
@@ -37,4 +41,10 @@ class SmartbarQuickActionButton : androidx.appcompat.widget.AppCompatImageButton
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
super.onMeasure(heightMeasureSpec, heightMeasureSpec)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas?) {
|
||||
super.onDraw(canvas)
|
||||
setBackgroundTintColor2(this, prefs.theme.smartbarButtonBgColor)
|
||||
setColorFilter(prefs.theme.smartbarButtonFgColor)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,21 @@
|
||||
package dev.patrickgold.florisboard.ime.text.smartbar
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.core.view.children
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.util.setBackgroundTintColor2
|
||||
import dev.patrickgold.florisboard.util.setDrawableTintColor2
|
||||
|
||||
/**
|
||||
* View class which keeps the references to important children and informs [SmartbarManager] that
|
||||
@@ -31,19 +39,15 @@ import dev.patrickgold.florisboard.R
|
||||
* a theme change).
|
||||
*/
|
||||
class SmartbarView : LinearLayout {
|
||||
|
||||
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
private val smartbarManager = SmartbarManager.getInstance()
|
||||
|
||||
var candidatesView: LinearLayout? = null
|
||||
private set
|
||||
private var variants: MutableList<ViewGroup> = mutableListOf()
|
||||
private var containers: MutableList<ViewGroup> = mutableListOf()
|
||||
|
||||
var candidateViewList: MutableList<Button> = mutableListOf()
|
||||
private set
|
||||
var numberRowView: LinearLayout? = null
|
||||
private set
|
||||
var quickActionsView: LinearLayout? = null
|
||||
private set
|
||||
var quickActionToggle: ImageButton? = null
|
||||
private set
|
||||
|
||||
constructor(context: Context) : this(context, null)
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
@@ -54,24 +58,95 @@ class SmartbarView : LinearLayout {
|
||||
|
||||
super.onAttachedToWindow()
|
||||
|
||||
candidatesView = findViewById(R.id.candidates)
|
||||
variants.add(findViewById(R.id.smartbar_variant_default))
|
||||
variants.add(findViewById(R.id.smartbar_variant_back_only))
|
||||
variants.add(findViewById(R.id.smartbar_variant_number_row))
|
||||
|
||||
containers.add(findViewById(R.id.candidates))
|
||||
containers.add(findViewById(R.id.clipboard_suggestion_row))
|
||||
containers.add(findViewById(R.id.clipboard_cursor_row))
|
||||
containers.add(findViewById(R.id.quick_actions))
|
||||
|
||||
candidateViewList.add(findViewById(R.id.candidate0))
|
||||
candidateViewList.add(findViewById(R.id.candidate1))
|
||||
candidateViewList.add(findViewById(R.id.candidate2))
|
||||
|
||||
numberRowView = findViewById(R.id.number_row)
|
||||
quickActionsView = findViewById(R.id.quick_actions)
|
||||
quickActionToggle = findViewById(R.id.quick_action_toggle)
|
||||
|
||||
smartbarManager.registerSmartbarView(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiplies the default smartbar height with the given [factor] and sets it.
|
||||
* Sets the active Smartbar variant based on the given id. Pass null to hide all variants and
|
||||
* show an empty Smartbar.
|
||||
*
|
||||
* @param which Which variant to show. Pass null to hide all.
|
||||
*/
|
||||
fun setHeightFactor(factor: Float) {
|
||||
val baseSize = resources.getDimension(R.dimen.smartbar_height)
|
||||
val size = (baseSize * factor).toInt()
|
||||
layoutParams?.height = size
|
||||
fun setActiveVariant(@IdRes which: Int?) {
|
||||
for (variant in variants) {
|
||||
if (variant.id == which) {
|
||||
variant.visibility = View.VISIBLE
|
||||
} else {
|
||||
variant.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the active Smartbar container based on the given id. Does only work if the currently
|
||||
* shown Smartbar variant is [R.id.smartbar_variant_default]. Pass null to hide all containers
|
||||
* and show only the quick action toggle.
|
||||
*
|
||||
* @param which Which container to show. Pass null to hide all.
|
||||
*/
|
||||
fun setActiveContainer(@IdRes which: Int?) {
|
||||
for (container in containers) {
|
||||
if (container.id == which) {
|
||||
container.visibility = View.VISIBLE
|
||||
} else {
|
||||
container.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
|
||||
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
|
||||
val height = when (heightMode) {
|
||||
MeasureSpec.EXACTLY -> {
|
||||
// Must be this size
|
||||
heightSize
|
||||
}
|
||||
MeasureSpec.AT_MOST -> {
|
||||
// Can't be bigger than...
|
||||
(florisboard?.inputView?.desiredSmartbarHeight ?: 0).coerceAtMost(heightSize)
|
||||
}
|
||||
else -> {
|
||||
// Be whatever you want
|
||||
florisboard?.inputView?.desiredSmartbarHeight ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY))
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas?) {
|
||||
super.onDraw(canvas)
|
||||
setBackgroundColor(prefs.theme.smartbarBgColor)
|
||||
for (container in containers + variants) {
|
||||
when (container.id) {
|
||||
R.id.clipboard_suggestion_row -> {
|
||||
val clipboardSuggestion = findViewById<Button>(R.id.clipboard_suggestion)
|
||||
setBackgroundTintColor2(clipboardSuggestion, prefs.theme.smartbarButtonBgColor)
|
||||
setDrawableTintColor2(clipboardSuggestion, prefs.theme.smartbarButtonFgColor)
|
||||
clipboardSuggestion.setTextColor(prefs.theme.smartbarButtonFgColor)
|
||||
}
|
||||
R.id.candidates -> {
|
||||
for (view in container.children) {
|
||||
if (view is Button) {
|
||||
view.setTextColor(prefs.theme.smartbarFgColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
310
app/src/main/java/dev/patrickgold/florisboard/ime/theme/Theme.kt
Normal file
310
app/src/main/java/dev/patrickgold/florisboard/ime/theme/Theme.kt
Normal file
@@ -0,0 +1,310 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.theme
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
|
||||
/**
|
||||
* Data class which holds a parsed theme json file. Used for loading a theme
|
||||
* preset in Settings.
|
||||
* Note: this implementation is generic and allows for any group/attr names.
|
||||
* FlorisBoard itself expects certain groups and attrs to be able to
|
||||
* color the controls accordingly. See 'ime/themes/floris_day.json'
|
||||
* for a good example of which attributes FlorisBoard needs!
|
||||
*
|
||||
* @property name A unique id/name for this theme. Must only contain certain
|
||||
* characters: upper/lower case letters, numbers (not at the beginning!) or
|
||||
* an underline (_).
|
||||
* @property displayName The name of this theme when shown to the user. Can
|
||||
* contain any valid Unicode character.
|
||||
* @property author The name of the author of this theme. Should be your
|
||||
* username on GitHub/GitLab/BitBucket/... or your full name.
|
||||
* @property isNightTheme If this theme is meant for display at day (false)
|
||||
* or night (true). This property is only used to auto-assign this theme to
|
||||
* either the day or night theme list in Settings, which is used when the
|
||||
* user wants to auto-set his theme based on the current time.
|
||||
* @property rawAttrs Map which holds the raw attributes of this theme. Note
|
||||
* that the name of this property is 'attributes' within the json file!
|
||||
* Attributes are always grouped together. This ensures a better structure
|
||||
* and easier storage. The group- as well as the attr-name has the same
|
||||
* limitations as the theme [name].
|
||||
* Attribute values can be of different format:
|
||||
* 1. A color
|
||||
* Either #RRGGBB or #AARRGGBB (case-insensitive) -> e.g. #A034FF23
|
||||
* 2. A static word
|
||||
* - transparent (=0x00000000)
|
||||
* - true (=0x1)
|
||||
* - false (=0x0)
|
||||
* 3. A reference to another attribute within the SAME theme, as follows:
|
||||
* @group/attrName -> e.g. @window/textColor
|
||||
* Note that referencing attributes has its limitations:
|
||||
* a. Recursive references will cause an exception.
|
||||
* b. Referencing an previously defined attribute is fine.
|
||||
* c. Referencing an attribute not-yet defined is also ok, as long as
|
||||
* the reference can be resolved at the next iteration.
|
||||
* d. If the next iteration cannot resolve a value, an exception is
|
||||
* thrown.
|
||||
* 4. If the value is of any other format, an exception will be thrown.
|
||||
*
|
||||
* @throws IllegalArgumentException either at an invalid value or when a
|
||||
* reference cannot be resolved.
|
||||
*/
|
||||
data class Theme(
|
||||
val name: String,
|
||||
val displayName: String,
|
||||
val author: String,
|
||||
val isNightTheme: Boolean = false,
|
||||
@Json(name = "attributes")
|
||||
private val rawAttrs: Map<String, Map<String, String>>
|
||||
) {
|
||||
/**
|
||||
* Holds the parsed attributes after init.
|
||||
*/
|
||||
val parsedAttrs: MutableMap<String, MutableMap<String, Int>> = mutableMapOf()
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Loads a theme from the specified [path].
|
||||
*
|
||||
* @param context A reference to the current [Context]. Used to request
|
||||
* asset file.
|
||||
* @param path The path to the json theme file in the asset folder.
|
||||
* @return A parsed [Theme] or null. A null value may indicate that
|
||||
* the file does not exist or that an error during the reading
|
||||
* of the file occurred.
|
||||
*/
|
||||
fun fromJsonFile(context: Context, path: String): Theme? {
|
||||
val rawJsonData: String = try {
|
||||
context.assets.open(path).bufferedReader().use { it.readText() }
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
} ?: return null
|
||||
return fromJsonString(rawJsonData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a theme from the given [rawData].
|
||||
*
|
||||
* @param rawData The raw json theme file as a string.
|
||||
* @return A parsed [Theme] or null. A null value may indicate that an error
|
||||
* during the reading of the [rawData] occurred.
|
||||
*/
|
||||
fun fromJsonString(rawData: String): Theme? {
|
||||
val moshi = Moshi.Builder()
|
||||
.add(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
val layoutAdapter = moshi.adapter(Theme::class.java)
|
||||
return layoutAdapter.fromJson(rawData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a given [theme] to the [prefs]. The default color values are based off the
|
||||
* Floris Day theme and are not intended to be modified. Instead, themes should be defined
|
||||
* in assets/ime/theme/<theme_id>.json
|
||||
*
|
||||
* @param theme The theme data.
|
||||
* @param prefs The preference object to write the theme to.
|
||||
*/
|
||||
fun writeThemeToPrefs(prefs: PrefHelper, theme: Theme) {
|
||||
// Internal prefs part I
|
||||
prefs.internal.themeCurrentBasedOn = theme.name
|
||||
prefs.internal.themeCurrentIsNight = theme.isNightTheme
|
||||
|
||||
// Theme attributes
|
||||
prefs.theme.colorPrimary = theme.getAttr("window/colorPrimary", "#4CAF50")
|
||||
prefs.theme.colorPrimaryDark = theme.getAttr("window/colorPrimaryDark", "#388E3C")
|
||||
prefs.theme.colorAccent = theme.getAttr("window/colorAccent", "#FF9800")
|
||||
prefs.theme.navBarColor = theme.getAttr("window/navigationBarColor", "#E0E0E0")
|
||||
prefs.theme.navBarIsLight = (theme.getAttrOrNull("window/navigationBarLight") ?: 0) > 0
|
||||
|
||||
prefs.theme.keyboardBgColor = theme.getAttr("keyboard/bgColor", "#E0E0E0")
|
||||
|
||||
prefs.theme.keyBgColor = theme.getAttr("key/bgColor", "#FFFFFF")
|
||||
prefs.theme.keyBgColorPressed = theme.getAttr("key/bgColorPressed", "#F5F5F5")
|
||||
prefs.theme.keyFgColor = theme.getAttr("key/fgColor", "#000000")
|
||||
|
||||
prefs.theme.keyEnterBgColor = theme.getAttr("keyEnter/bgColor", "#4CAF50")
|
||||
prefs.theme.keyEnterBgColorPressed = theme.getAttr("keyEnter/bgColorPressed", "#388E3C")
|
||||
prefs.theme.keyEnterFgColor = theme.getAttr("keyEnter/fgColor", "#FFFFFF")
|
||||
|
||||
prefs.theme.keyPopupBgColor = theme.getAttr("keyPopup/bgColor", "#EEEEEE")
|
||||
prefs.theme.keyPopupBgColorActive = theme.getAttr("keyPopup/bgColorActive", "#BDBDBD")
|
||||
prefs.theme.keyPopupFgColor = theme.getAttr("keyPopup/fgColor", "#000000")
|
||||
|
||||
prefs.theme.keyShiftBgColor = theme.getAttr("keyShift/bgColor", "#FFFFFF")
|
||||
prefs.theme.keyShiftBgColorPressed = theme.getAttr("keyShift/bgColorPressed", "#F5F5F5")
|
||||
prefs.theme.keyShiftFgColor = theme.getAttr("keyShift/fgColor", "#000000")
|
||||
prefs.theme.keyShiftFgColorCapsLock = theme.getAttr("keyShift/fgColorCapsLock", "#FF9800")
|
||||
|
||||
prefs.theme.mediaFgColor = theme.getAttr("media/fgColor", "#000000")
|
||||
prefs.theme.mediaFgColorAlt = theme.getAttr("media/fgColorAlt", "#757575")
|
||||
|
||||
prefs.theme.oneHandedBgColor = theme.getAttr("oneHanded/bgColor", "#E8F5E9")
|
||||
|
||||
prefs.theme.oneHandedButtonFgColor = theme.getAttr("oneHandedButton/fgColor", "#424242")
|
||||
|
||||
prefs.theme.smartbarBgColor = theme.getAttr("smartbar/bgColor", "#E0E0E0")
|
||||
prefs.theme.smartbarFgColor = theme.getAttr("smartbar/fgColor", "#000000")
|
||||
prefs.theme.smartbarFgColorAlt = theme.getAttr("smartbar/fgColorAlt", "#4A000000")
|
||||
|
||||
prefs.theme.smartbarButtonBgColor = theme.getAttr("smartbarButton/bgColor", "#FFFFFF")
|
||||
prefs.theme.smartbarButtonFgColor = theme.getAttr("smartbarButton/fgColor", "#000000")
|
||||
|
||||
// Internal prefs part II (must be written at the end!!)
|
||||
prefs.internal.themeCurrentIsModified = false
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val listOfAttrsToReevaluate = mutableListOf<Triple<String, String, String>>()
|
||||
for (group in rawAttrs) {
|
||||
val groupMap = mutableMapOf<String, Int>()
|
||||
parsedAttrs[group.key] = groupMap
|
||||
for (attr in group.value) {
|
||||
val colorRegex = """[#]([0-9a-fA-F]{8}|[0-9a-fA-F]{6})""".toRegex()
|
||||
val refRegex = """[@]([a-zA-Z_][a-zA-Z0-9_]*)[/]([a-zA-Z_][a-zA-Z0-9_]*)""".toRegex()
|
||||
when {
|
||||
attr.value.matches(colorRegex) -> {
|
||||
groupMap[attr.key] = Color.parseColor(attr.value)
|
||||
}
|
||||
attr.value == "transparent" -> {
|
||||
groupMap[attr.key] = Color.TRANSPARENT
|
||||
}
|
||||
attr.value == "true" -> {
|
||||
groupMap[attr.key] = 0x1
|
||||
}
|
||||
attr.value == "false" -> {
|
||||
groupMap[attr.key] = 0x0
|
||||
}
|
||||
attr.value.matches(refRegex) -> {
|
||||
val attrValue = getAttrOrNull(attr.value.substring(1))
|
||||
if (attrValue != null) {
|
||||
groupMap[attr.key] = attrValue
|
||||
} else {
|
||||
listOfAttrsToReevaluate.add(Triple(group.key, attr.key, attr.value))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
throw IllegalArgumentException("The specified attr '${attr.key}' = '${attr.value}' is not valid!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (attrToReevaluate in listOfAttrsToReevaluate) {
|
||||
val attrValue = getAttrOrNull(attrToReevaluate.third.substring(1))
|
||||
if (attrValue != null) {
|
||||
parsedAttrs[attrToReevaluate.first]?.put(attrToReevaluate.second, attrValue)
|
||||
} else {
|
||||
throw IllegalArgumentException("The specified attr '${attrToReevaluate.second}' = '${attrToReevaluate.third}' is not valid!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getAttr(key: String, defaultColor: String): Int {
|
||||
return getAttrOrNull(key) ?: Color.parseColor(defaultColor)
|
||||
}
|
||||
fun getAttr(group: String, attr: String, defaultColor: String): Int {
|
||||
return getAttrOrNull(group, attr) ?: Color.parseColor(defaultColor)
|
||||
}
|
||||
|
||||
fun getAttrOrNull(key: String): Int? {
|
||||
val regex = """([a-zA-Z_][a-zA-Z0-9_]*)[/]([a-zA-Z_][a-zA-Z0-9_]*)""".toRegex()
|
||||
return if (key.matches(regex)) {
|
||||
val split = key.split("/")
|
||||
getAttrOrNull(split[0], split[1])
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
fun getAttrOrNull(group: String, attr: String): Int? {
|
||||
return parsedAttrs[group]?.get(attr)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class which is used to quickly parse only the relevant meta data to
|
||||
* display a theme in a selection list.
|
||||
*
|
||||
* @see [Theme] for details regarding the attributes and the theme structure.
|
||||
*/
|
||||
data class ThemeMetaOnly(
|
||||
val name: String,
|
||||
val displayName: String,
|
||||
val author: String,
|
||||
val isNightTheme: Boolean = false
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
* Loads the theme meta data from the specified [path].
|
||||
*
|
||||
* @param context A reference to the current [Context]. Used to request
|
||||
* asset file.
|
||||
* @param path The path to the json theme file in the asset folder.
|
||||
* @return [ThemeMetaOnly] or null. A null value may indicate that
|
||||
* the file does not exist or that an error during the reading
|
||||
* of the file occurred.
|
||||
*/
|
||||
fun loadFromJsonFile(context: Context, path: String): ThemeMetaOnly? {
|
||||
val rawJsonData: String = try {
|
||||
context.assets.open(path).bufferedReader().use { it.readText() }
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
} ?: return null
|
||||
val moshi = Moshi.Builder()
|
||||
.add(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
val layoutAdapter = moshi.adapter(ThemeMetaOnly::class.java)
|
||||
return layoutAdapter.fromJson(rawJsonData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all theme meta data from the specified [path].
|
||||
*
|
||||
* @param context A reference to the current [Context]. Used to request
|
||||
* asset file.
|
||||
* @param path The path to the dir in the asset folder.
|
||||
* @return [ThemeMetaOnly] or null. A null value may indicate that
|
||||
* the file does not exist or that an error during the reading
|
||||
* of the file occurred.
|
||||
*/
|
||||
fun loadAllFromDir(context: Context, path: String): List<ThemeMetaOnly> {
|
||||
val ret = mutableListOf<ThemeMetaOnly>()
|
||||
try {
|
||||
val list = context.assets.list(path)
|
||||
if (list != null && list.isNotEmpty()) {
|
||||
// Is a folder
|
||||
for (file in list) {
|
||||
val subList = context.assets.list("$path/$file")
|
||||
if (subList?.isEmpty() == true) {
|
||||
// Is file
|
||||
val metaData = loadFromJsonFile(context, "$path/$file")
|
||||
if (metaData != null) {
|
||||
ret.add(metaData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: java.lang.Exception) {}
|
||||
return ret
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.settings
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.databinding.AdvancedActivityBinding
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.util.PackageManagerUtils
|
||||
|
||||
class AdvancedActivity : AppCompatActivity(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private lateinit var binding: AdvancedActivityBinding
|
||||
private lateinit var prefs: PrefHelper
|
||||
|
||||
companion object {
|
||||
const val RESULT_APPLY_THEME = 0x322D
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
prefs = PrefHelper.getDefaultInstance(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = AdvancedActivityBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
supportActionBar?.setTitle(R.string.settings__advanced__title)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
onBackPressed()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sp: SharedPreferences?, key: String?) {
|
||||
prefs.sync()
|
||||
if (key == PrefHelper.Advanced.SETTINGS_THEME) {
|
||||
setResult(RESULT_APPLY_THEME)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
prefs.shared.registerOnSharedPreferenceChangeListener(this)
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
prefs.shared.unregisterOnSharedPreferenceChangeListener(this)
|
||||
updateLauncherIconStatus()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
private fun updateLauncherIconStatus() {
|
||||
// Set LauncherAlias enabled/disabled state just before destroying/pausing this activity
|
||||
if (prefs.advanced.showAppIcon) {
|
||||
PackageManagerUtils.showAppIcon(this)
|
||||
} else {
|
||||
PackageManagerUtils.hideAppIcon(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.databinding.SettingsFragmentAdvancedBinding
|
||||
|
||||
class AdvancedFragment : SettingsMainActivity.SettingsFragment() {
|
||||
private lateinit var binding: SettingsFragmentAdvancedBinding
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
binding = SettingsFragmentAdvancedBinding.inflate(inflater, container, false)
|
||||
|
||||
val transaction = childFragmentManager.beginTransaction()
|
||||
transaction.replace(
|
||||
binding.prefsAdvancedFrame.id,
|
||||
SettingsMainActivity.PrefFragment.createFromResource(R.xml.prefs_advanced)
|
||||
)
|
||||
transaction.commit()
|
||||
|
||||
return binding.root
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.ContextThemeWrapper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.databinding.SettingsFragmentLooknfeelBinding
|
||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
|
||||
import dev.patrickgold.florisboard.ime.text.layout.LayoutManager
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
class LooknfeelFragment : SettingsMainActivity.SettingsFragment(), CoroutineScope by MainScope() {
|
||||
private lateinit var binding: SettingsFragmentLooknfeelBinding
|
||||
private lateinit var keyboardView: KeyboardView
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
binding = SettingsFragmentLooknfeelBinding.inflate(inflater, container, false)
|
||||
|
||||
launch(Dispatchers.Default) {
|
||||
val themeContext = ContextThemeWrapper(context, prefs.theme.getSelectedThemeResId())
|
||||
val layoutManager = LayoutManager(themeContext)
|
||||
keyboardView = KeyboardView(themeContext)
|
||||
keyboardView.prefs = prefs
|
||||
keyboardView.isPreviewMode = true
|
||||
keyboardView.computedLayout = layoutManager.fetchComputedLayoutAsync(KeyboardMode.CHARACTERS, Subtype.DEFAULT).await()
|
||||
keyboardView.updateVisibility()
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.themeLinearLayout.addView(keyboardView, 0)
|
||||
}
|
||||
}
|
||||
|
||||
val transaction = childFragmentManager.beginTransaction()
|
||||
transaction.replace(
|
||||
binding.prefsLooknfeelFrame.id,
|
||||
SettingsMainActivity.PrefFragment.createFromResource(R.xml.prefs_looknfeel)
|
||||
)
|
||||
transaction.replace(
|
||||
binding.prefsThemeFrame.id,
|
||||
SettingsMainActivity.PrefFragment.createFromResource(R.xml.prefs_theme)
|
||||
)
|
||||
transaction.commit()
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -28,32 +28,33 @@ import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.databinding.SettingsActivityBinding
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.core.SubtypeManager
|
||||
import dev.patrickgold.florisboard.settings.fragments.*
|
||||
import dev.patrickgold.florisboard.util.AppVersionUtils
|
||||
import dev.patrickgold.florisboard.util.PackageManagerUtils
|
||||
|
||||
private const val FRAGMENT_TAG = "FRAGMENT_TAG"
|
||||
internal const val FRAGMENT_TAG = "FRAGMENT_TAG"
|
||||
private const val PREF_RES_ID = "PREF_RES_ID"
|
||||
private const val SELECTED_ITEM_ID = "SELECTED_ITEM_ID"
|
||||
private const val ADVANCED_REQ_CODE = 0x145F
|
||||
|
||||
class SettingsMainActivity : AppCompatActivity(),
|
||||
BottomNavigationView.OnNavigationItemSelectedListener,
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
lateinit var binding: SettingsActivityBinding
|
||||
lateinit var prefs: PrefHelper
|
||||
private lateinit var prefs: PrefHelper
|
||||
lateinit var subtypeManager: SubtypeManager
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
prefs = PrefHelper(this, PreferenceManager.getDefaultSharedPreferences(this))
|
||||
prefs = PrefHelper.getDefaultInstance(this)
|
||||
prefs.initDefaultPreferences()
|
||||
subtypeManager =
|
||||
SubtypeManager(this, prefs)
|
||||
prefs.sync()
|
||||
subtypeManager = SubtypeManager(this, prefs)
|
||||
|
||||
val mode = when (prefs.advanced.settingsTheme) {
|
||||
"light" -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
@@ -99,9 +100,14 @@ class SettingsMainActivity : AppCompatActivity(),
|
||||
loadFragment(KeyboardFragment())
|
||||
true
|
||||
}
|
||||
R.id.settings__navigation__looknfeel -> {
|
||||
supportActionBar?.setTitle(R.string.settings__looknfeel__title)
|
||||
loadFragment(LooknfeelFragment())
|
||||
R.id.settings__navigation__typing -> {
|
||||
supportActionBar?.setTitle(R.string.settings__typing__title)
|
||||
loadFragment(TypingFragment())
|
||||
true
|
||||
}
|
||||
R.id.settings__navigation__theme -> {
|
||||
supportActionBar?.setTitle(R.string.settings__theme__title)
|
||||
loadFragment(ThemeFragment())
|
||||
true
|
||||
}
|
||||
R.id.settings__navigation__gestures -> {
|
||||
@@ -109,20 +115,15 @@ class SettingsMainActivity : AppCompatActivity(),
|
||||
loadFragment(GesturesFragment())
|
||||
true
|
||||
}
|
||||
R.id.settings__navigation__advanced -> {
|
||||
supportActionBar?.setTitle(R.string.settings__advanced__title)
|
||||
loadFragment(AdvancedFragment())
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadFragment(fragment: Fragment) {
|
||||
val transaction = supportFragmentManager.beginTransaction()
|
||||
transaction.replace(binding.pageFrame.id, fragment, FRAGMENT_TAG)
|
||||
//transaction.addToBackStack(null)
|
||||
transaction.commit()
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(binding.pageFrame.id, fragment, FRAGMENT_TAG)
|
||||
.commit()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
@@ -130,6 +131,14 @@ class SettingsMainActivity : AppCompatActivity(),
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (binding.bottomNavigation.selectedItemId != R.id.settings__navigation__home) {
|
||||
binding.bottomNavigation.selectedItemId = R.id.settings__navigation__home
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
@@ -144,6 +153,10 @@ class SettingsMainActivity : AppCompatActivity(),
|
||||
startActivity(browserIntent)
|
||||
true
|
||||
}
|
||||
R.id.settings__menu_advanced -> {
|
||||
startActivityForResult(Intent(this, AdvancedActivity::class.java), ADVANCED_REQ_CODE)
|
||||
true
|
||||
}
|
||||
R.id.settings__menu_about -> {
|
||||
startActivity(Intent(this, AboutActivity::class.java))
|
||||
true
|
||||
@@ -152,22 +165,18 @@ class SettingsMainActivity : AppCompatActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sp: SharedPreferences?, key: String?) {
|
||||
if (key == PrefHelper.Advanced.SETTINGS_THEME) {
|
||||
recreate()
|
||||
}
|
||||
val fragment = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG)
|
||||
if (fragment != null && fragment.isVisible) {
|
||||
if (fragment is LooknfeelFragment) {
|
||||
if (key == PrefHelper.Theme.NAME) {
|
||||
// TODO: recreate() is only a lazy solution, better would be to only recreate
|
||||
// the keyboard view
|
||||
recreate()
|
||||
}
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == ADVANCED_REQ_CODE) {
|
||||
if (resultCode == AdvancedActivity.RESULT_APPLY_THEME) {
|
||||
recreate()
|
||||
}
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sp: SharedPreferences?, key: String?) {}
|
||||
|
||||
private fun updateLauncherIconStatus() {
|
||||
// Set LauncherAlias enabled/disabled state just before destroying/pausing this activity
|
||||
if (prefs.advanced.showAppIcon) {
|
||||
@@ -195,7 +204,6 @@ class SettingsMainActivity : AppCompatActivity(),
|
||||
}
|
||||
|
||||
abstract class SettingsFragment : Fragment() {
|
||||
protected lateinit var prefs: PrefHelper
|
||||
protected lateinit var settingsMainActivity: SettingsMainActivity
|
||||
protected lateinit var subtypeManager: SubtypeManager
|
||||
|
||||
@@ -203,7 +211,6 @@ class SettingsMainActivity : AppCompatActivity(),
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
settingsMainActivity = activity as SettingsMainActivity
|
||||
prefs = settingsMainActivity.prefs
|
||||
subtypeManager = settingsMainActivity.subtypeManager
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.settings.components
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.SeekBar
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceManager
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.databinding.SeekBarDialogBinding
|
||||
|
||||
/**
|
||||
* Custom preference which represents a seek bar which shows the current value in the summary. The
|
||||
* value can be changed by clicking on the preference, which brings up a dialog which a seek bar.
|
||||
* This implementation also allows for a min / max step value, while being backwards compatible.
|
||||
*
|
||||
* @see R.styleable.DialogSeekBarPreferenceAttrs for which xml attributes this preference accepts
|
||||
* besides the default Preference attributes.
|
||||
*
|
||||
* @property defaultValue The default value of this preference.
|
||||
* @property systemDefaultValue At this exact value [systemDefaultValueText] should be shown instead
|
||||
* of the actual value.
|
||||
* @property systemDefaultValueText The text to show if this preference's value or seek bar is
|
||||
* [systemDefaultValue]. Set to null to disable the system default text feature.
|
||||
* @property min The minimum value of the seek bar. Must not be greater or equal than [max].
|
||||
* @property max The maximum value of the seek bar. Must not be lesser or equal than [min].
|
||||
* @property step The step in which the seek bar increases per move. If the provided value is less
|
||||
* than 1, 1 will be used as step. Note that the xml attribute's name for this property is
|
||||
* [R.styleable.DialogSeekBarPreferenceAttrs_seekBarIncrement].
|
||||
* @property unit The unit to show after the value. Set to an empty string to disable this feature.
|
||||
*/
|
||||
class DialogSeekBarPreference : Preference {
|
||||
private var defaultValue: Int = 0
|
||||
private var systemDefaultValue: Int = -1
|
||||
private var systemDefaultValueText: String? = null
|
||||
private var min: Int = 0
|
||||
private var max: Int = 100
|
||||
private var step: Int = 1
|
||||
private var unit: String = ""
|
||||
|
||||
@Suppress("unused")
|
||||
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) {
|
||||
layoutResource = R.layout.list_item
|
||||
context.obtainStyledAttributes(attrs, R.styleable.DialogSeekBarPreferenceAttrs).apply {
|
||||
min = getInt(R.styleable.DialogSeekBarPreferenceAttrs_min, min)
|
||||
max = getInt(R.styleable.DialogSeekBarPreferenceAttrs_max, max)
|
||||
step = getInt(R.styleable.DialogSeekBarPreferenceAttrs_seekBarIncrement, step)
|
||||
if (step < 1) {
|
||||
step = 1
|
||||
}
|
||||
defaultValue = getInt(R.styleable.DialogSeekBarPreferenceAttrs_android_defaultValue, defaultValue)
|
||||
systemDefaultValue = getInt(R.styleable.DialogSeekBarPreferenceAttrs_systemDefaultValue, min - 1)
|
||||
systemDefaultValueText = getString(R.styleable.DialogSeekBarPreferenceAttrs_systemDefaultValueText)
|
||||
unit = getString(R.styleable.DialogSeekBarPreferenceAttrs_unit) ?: unit
|
||||
recycle()
|
||||
}
|
||||
onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
|
||||
summary = getTextForValue(newValue.toString())
|
||||
true
|
||||
}
|
||||
onPreferenceClickListener = OnPreferenceClickListener {
|
||||
showSeekBarDialog()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToHierarchy(preferenceManager: PreferenceManager?) {
|
||||
super.onAttachedToHierarchy(preferenceManager)
|
||||
summary = getTextForValue(sharedPreferences.getInt(key, defaultValue))
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the text for the given [value] and adds the defined [unit] at the end.
|
||||
* If [systemDefaultValueText] is not null this method tries to match the given [value] with
|
||||
* [systemDefaultValue] and returns [systemDefaultValueText] upon matching.
|
||||
*/
|
||||
private fun getTextForValue(value: Any): String {
|
||||
if (value !is Int) {
|
||||
return "??$unit"
|
||||
}
|
||||
val systemDefValText = systemDefaultValueText
|
||||
return if (value == systemDefaultValue && systemDefValText != null) {
|
||||
systemDefValText
|
||||
} else {
|
||||
value.toString() + unit
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the seek bar dialog.
|
||||
*/
|
||||
private fun showSeekBarDialog() {
|
||||
val inflater =
|
||||
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val dialogView = SeekBarDialogBinding.inflate(inflater)
|
||||
val initValue = sharedPreferences.getInt(key, defaultValue)
|
||||
dialogView.seekBar.max = actualValueToSeekBarProgress(max)
|
||||
dialogView.seekBar.progress = actualValueToSeekBarProgress(initValue)
|
||||
dialogView.seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||
dialogView.seekBarValue.text = getTextForValue(seekBarProgressToActualValue(progress))
|
||||
}
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
|
||||
})
|
||||
dialogView.seekBarValue.text = getTextForValue(initValue)
|
||||
AlertDialog.Builder(context).apply {
|
||||
setTitle(this@DialogSeekBarPreference.title)
|
||||
setCancelable(true)
|
||||
setView(dialogView.root)
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val actualValue = seekBarProgressToActualValue(dialogView.seekBar.progress)
|
||||
sharedPreferences.edit().putInt(key, actualValue).apply()
|
||||
}
|
||||
setNeutralButton(R.string.settings__default) { _, _ ->
|
||||
sharedPreferences.edit().putInt(key, defaultValue).apply()
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel, null)
|
||||
setOnDismissListener { summary = getTextForValue(sharedPreferences.getInt(key, defaultValue)) }
|
||||
create()
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the actual value to a progress value which the Android SeekBar implementation can
|
||||
* handle. (Android's SeekBar step is fixed at 1 and min at 0)
|
||||
*
|
||||
* @param actual The actual value.
|
||||
* @return the internal value which is used to allow different min and step values.
|
||||
*/
|
||||
private fun actualValueToSeekBarProgress(actual: Int): Int {
|
||||
return (actual - min) / step
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the Android SeekBar value to the actual value.
|
||||
*
|
||||
* @param progress The progress value of the SeekBar.
|
||||
* @return the actual value which is ready to use.
|
||||
*/
|
||||
private fun seekBarProgressToActualValue(progress: Int): Int {
|
||||
return (progress * step) + min
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.settings.components
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceManager
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.databinding.ThemeSelectorDialogBinding
|
||||
import dev.patrickgold.florisboard.databinding.ThemeSelectorListItemBinding
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.theme.Theme
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeMetaOnly
|
||||
|
||||
/**
|
||||
* Custom preference which handles the theme preset selection dialog and shows a summary in the
|
||||
* list.
|
||||
*/
|
||||
class ThemePresetSelectorPreference : Preference, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private var dialog: AlertDialog? = null
|
||||
private val metaDataCache: MutableMap<String, ThemeMetaOnly> = mutableMapOf()
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
|
||||
@Suppress("unused")
|
||||
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) {
|
||||
layoutResource = R.layout.list_item
|
||||
onPreferenceClickListener = OnPreferenceClickListener {
|
||||
showThemeSelectorDialog()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToHierarchy(preferenceManager: PreferenceManager?) {
|
||||
super.onAttachedToHierarchy(preferenceManager)
|
||||
summary = generateSummaryText()
|
||||
prefs.shared.registerOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
override fun onDetached() {
|
||||
if (dialog?.isShowing == true) {
|
||||
dialog?.dismiss()
|
||||
}
|
||||
prefs.shared.unregisterOnSharedPreferenceChangeListener(this)
|
||||
super.onDetached()
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sp: SharedPreferences?, key: String?) {
|
||||
if (key == PrefHelper.Internal.THEME_CURRENT_IS_MODIFIED) {
|
||||
summary = generateSummaryText()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the summary text to display and returns it. Based on the prefs.internal.theme*
|
||||
* values and the theme meta cache.
|
||||
*/
|
||||
private fun generateSummaryText(): String {
|
||||
val themeKey = prefs.internal.themeCurrentBasedOn
|
||||
val isModified = prefs.internal.themeCurrentIsModified
|
||||
var metaOnly: ThemeMetaOnly? = metaDataCache[themeKey]
|
||||
if (metaOnly == null) {
|
||||
try {
|
||||
metaOnly = ThemeMetaOnly.loadFromJsonFile(context, "ime/theme/$themeKey.json")
|
||||
} catch (e: Exception) {
|
||||
return context.resources.getString(R.string.settings__theme__undefined)
|
||||
}
|
||||
}
|
||||
metaOnly ?: return context.resources.getString(R.string.settings__theme__undefined)
|
||||
return if (isModified) {
|
||||
String.format(context.resources.getString(R.string.settings__theme__preset_summary), metaOnly.displayName)
|
||||
} else {
|
||||
metaOnly.displayName
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the theme selector dialog.
|
||||
*/
|
||||
private fun showThemeSelectorDialog() {
|
||||
val inflater =
|
||||
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val dialogView = ThemeSelectorDialogBinding.inflate(inflater)
|
||||
val selectedThemeView = ThemeSelectorListItemBinding.inflate(inflater)
|
||||
selectedThemeView.title.text = generateSummaryText()
|
||||
dialogView.content.addView(selectedThemeView.root, 1)
|
||||
metaDataCache.clear()
|
||||
ThemeMetaOnly.loadAllFromDir(context, "ime/theme").forEach { metaData ->
|
||||
metaDataCache[metaData.name] = metaData
|
||||
}
|
||||
for ((themeKey, metaData) in metaDataCache) {
|
||||
if (themeKey == prefs.internal.themeCurrentBasedOn && !prefs.internal.themeCurrentIsModified) {
|
||||
continue
|
||||
}
|
||||
val availableThemeView = ThemeSelectorListItemBinding.inflate(inflater)
|
||||
availableThemeView.title.text = metaData.displayName
|
||||
availableThemeView.root.setOnClickListener {
|
||||
applyThemePreset(metaData.name)
|
||||
dialog?.dismiss()
|
||||
}
|
||||
dialogView.content.addView(availableThemeView.root)
|
||||
}
|
||||
AlertDialog.Builder(context).apply {
|
||||
setTitle(this@ThemePresetSelectorPreference.title)
|
||||
setCancelable(true)
|
||||
setView(dialogView.root)
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
//
|
||||
}
|
||||
setNeutralButton(R.string.settings__default) { _, _ ->
|
||||
//
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel, null)
|
||||
setOnDismissListener { summary = generateSummaryText() }
|
||||
create()
|
||||
dialog = show()
|
||||
dialog?.getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the Theme for given [themeKey] to the preferences. Overrides any custom user-defined
|
||||
* theme in the shared prefs, if existent.
|
||||
*
|
||||
* @param themeKey The key of the Theme preset to be applied.
|
||||
*/
|
||||
private fun applyThemePreset(themeKey: String) {
|
||||
val theme = Theme.fromJsonFile(context, "ime/theme/$themeKey.json") ?: return
|
||||
Theme.writeThemeToPrefs(prefs, theme)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.settings.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import dev.patrickgold.florisboard.R
|
||||
|
||||
class AdvancedFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.prefs_advanced)
|
||||
}
|
||||
}
|
||||
@@ -14,25 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.settings
|
||||
package dev.patrickgold.florisboard.settings.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.databinding.SettingsFragmentGesturesBinding
|
||||
|
||||
class GesturesFragment : SettingsMainActivity.SettingsFragment() {
|
||||
private lateinit var binding: SettingsFragmentGesturesBinding
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
binding = SettingsFragmentGesturesBinding.inflate(inflater, container, false)
|
||||
|
||||
return binding.root
|
||||
class GesturesFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.prefs_gestures)
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.settings
|
||||
package dev.patrickgold.florisboard.settings.fragments
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
@@ -25,6 +25,7 @@ import android.view.ViewGroup
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.databinding.SettingsFragmentHomeBinding
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.settings.SettingsMainActivity
|
||||
import dev.patrickgold.florisboard.setup.SetupActivity
|
||||
|
||||
class HomeFragment : SettingsMainActivity.SettingsFragment() {
|
||||
@@ -56,6 +57,12 @@ class HomeFragment : SettingsMainActivity.SettingsFragment() {
|
||||
startActivity(this)
|
||||
}
|
||||
}
|
||||
binding.localizationCard.setOnClickListener {
|
||||
settingsMainActivity.binding.bottomNavigation.selectedItemId = R.id.settings__navigation__typing
|
||||
}
|
||||
binding.themeCard.setOnClickListener {
|
||||
settingsMainActivity.binding.bottomNavigation.selectedItemId = R.id.settings__navigation__theme
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.settings.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import dev.patrickgold.florisboard.R
|
||||
|
||||
class KeyboardFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.prefs_keyboard)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.settings.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import dev.patrickgold.florisboard.R
|
||||
|
||||
class ThemeCustomizeFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.prefs_theme)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.settings.fragments
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.ContextThemeWrapper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.databinding.SettingsFragmentThemeBinding
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
|
||||
import dev.patrickgold.florisboard.ime.text.layout.LayoutManager
|
||||
import dev.patrickgold.florisboard.settings.SettingsMainActivity
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class ThemeFragment : SettingsMainActivity.SettingsFragment(), CoroutineScope by MainScope(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private lateinit var binding: SettingsFragmentThemeBinding
|
||||
private lateinit var keyboardView: KeyboardView
|
||||
private lateinit var prefs: PrefHelper
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
prefs = PrefHelper.getDefaultInstance(requireContext())
|
||||
binding = SettingsFragmentThemeBinding.inflate(inflater, container, false)
|
||||
|
||||
launch(Dispatchers.Default) {
|
||||
val themeContext = ContextThemeWrapper(context, FlorisBoard.getDayNightBaseThemeId(prefs.internal.themeCurrentIsNight))
|
||||
val layoutManager = LayoutManager(themeContext)
|
||||
keyboardView = KeyboardView(themeContext)
|
||||
keyboardView.layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
resources.getDimension(R.dimen.textKeyboardView_baseHeight).roundToInt()
|
||||
).apply {
|
||||
val m = resources.getDimension(R.dimen.keyboard_preview_margin).toInt()
|
||||
setMargins(m, m, m, m)
|
||||
}
|
||||
prefs.sync()
|
||||
keyboardView.isPreviewMode = true
|
||||
val subtype = subtypeManager.getActiveSubtype() ?: Subtype.DEFAULT
|
||||
keyboardView.computedLayout = layoutManager.fetchComputedLayoutAsync(KeyboardMode.CHARACTERS, subtype).await()
|
||||
keyboardView.updateVisibility()
|
||||
keyboardView.onApplyThemeAttributes()
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.root.addView(keyboardView, 0)
|
||||
}
|
||||
}
|
||||
|
||||
loadThemePrefFragment()
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun loadThemePrefFragment() {
|
||||
childFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(
|
||||
binding.prefsFrame.id,
|
||||
SettingsMainActivity.PrefFragment.createFromResource(R.xml.prefs_theme)
|
||||
)
|
||||
.commit()
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sp: SharedPreferences?, key: String?) {
|
||||
prefs.sync()
|
||||
key ?: return
|
||||
if (key == PrefHelper.Internal.THEME_CURRENT_BASED_ON ||
|
||||
key == PrefHelper.Internal.THEME_CURRENT_IS_MODIFIED && !prefs.internal.themeCurrentIsModified) {
|
||||
loadThemePrefFragment()
|
||||
}
|
||||
if (key.startsWith("theme__")) {
|
||||
prefs.internal.themeCurrentIsModified = true
|
||||
keyboardView.onApplyThemeAttributes()
|
||||
keyboardView.invalidate()
|
||||
keyboardView.invalidateAllKeys()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
prefs.shared.registerOnSharedPreferenceChangeListener(this)
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
prefs.shared.unregisterOnSharedPreferenceChangeListener(this)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.settings
|
||||
package dev.patrickgold.florisboard.settings.fragments
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.os.Bundle
|
||||
@@ -23,16 +23,15 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.FrameLayout
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.databinding.SettingsFragmentKeyboardBinding
|
||||
import dev.patrickgold.florisboard.databinding.SettingsFragmentKeyboardSubtypeDialogBinding
|
||||
import dev.patrickgold.florisboard.databinding.SettingsFragmentKeyboardSubtypeListItemBinding
|
||||
import dev.patrickgold.florisboard.databinding.ListItemBinding
|
||||
import dev.patrickgold.florisboard.databinding.SettingsFragmentTypingBinding
|
||||
import dev.patrickgold.florisboard.databinding.SettingsFragmentTypingSubtypeDialogBinding
|
||||
import dev.patrickgold.florisboard.settings.SettingsMainActivity
|
||||
import dev.patrickgold.florisboard.util.LocaleUtils
|
||||
|
||||
class KeyboardFragment : SettingsMainActivity.SettingsFragment() {
|
||||
private lateinit var binding: SettingsFragmentKeyboardBinding
|
||||
class TypingFragment : SettingsMainActivity.SettingsFragment() {
|
||||
private lateinit var binding: SettingsFragmentTypingBinding
|
||||
/**
|
||||
* Must always have a reference to the open AlertDialog to dismiss the AlertDialog in the event
|
||||
* of onDestroy(), if this is not done a memory leak will most likely happen!
|
||||
@@ -44,17 +43,18 @@ class KeyboardFragment : SettingsMainActivity.SettingsFragment() {
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
binding = SettingsFragmentKeyboardBinding.inflate(inflater, container, false)
|
||||
binding = SettingsFragmentTypingBinding.inflate(inflater, container, false)
|
||||
binding.subtypeAddBtn.setOnClickListener { showAddSubtypeDialog() }
|
||||
|
||||
updateSubtypeListView()
|
||||
|
||||
val transaction = childFragmentManager.beginTransaction()
|
||||
transaction.replace(
|
||||
binding.prefsKeyboardFrame.id,
|
||||
SettingsMainActivity.PrefFragment.createFromResource(R.xml.prefs_keyboard)
|
||||
)
|
||||
transaction.commit()
|
||||
childFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(
|
||||
binding.prefsFrame.id,
|
||||
SettingsMainActivity.PrefFragment.createFromResource(R.xml.prefs_typing)
|
||||
)
|
||||
.commit()
|
||||
|
||||
return binding.root
|
||||
}
|
||||
@@ -66,7 +66,7 @@ class KeyboardFragment : SettingsMainActivity.SettingsFragment() {
|
||||
|
||||
private fun showAddSubtypeDialog() {
|
||||
val dialogView =
|
||||
SettingsFragmentKeyboardSubtypeDialogBinding.inflate(layoutInflater)
|
||||
SettingsFragmentTypingSubtypeDialogBinding.inflate(layoutInflater)
|
||||
val languageAdapter: ArrayAdapter<String> = ArrayAdapter(
|
||||
requireContext(),
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
@@ -95,11 +95,11 @@ class KeyboardFragment : SettingsMainActivity.SettingsFragment() {
|
||||
)
|
||||
dialogView.layoutSpinner.adapter = layoutAdapter
|
||||
AlertDialog.Builder(context).apply {
|
||||
setTitle(R.string.settings__keyboard__subtype_add_title)
|
||||
setTitle(R.string.settings__localization__subtype_add_title)
|
||||
setCancelable(true)
|
||||
setView(dialogView.root)
|
||||
setPositiveButton(R.string.settings__keyboard__subtype_add, null)
|
||||
setNegativeButton(R.string.settings__keyboard__subtype_cancel) { _, _ -> }
|
||||
setPositiveButton(R.string.settings__localization__subtype_add, null)
|
||||
setNegativeButton(R.string.settings__localization__subtype_cancel) { _, _ -> }
|
||||
setOnDismissListener { activeDialogWindow = null }
|
||||
create()
|
||||
activeDialogWindow = show()
|
||||
@@ -110,7 +110,7 @@ class KeyboardFragment : SettingsMainActivity.SettingsFragment() {
|
||||
val layoutName = subtypeManager.imeConfig.characterLayouts.keys.toList()[dialogView.layoutSpinner.selectedItemPosition]
|
||||
val success = subtypeManager.addSubtype(LocaleUtils.stringToLocale(languageCode), layoutName)
|
||||
if (!success) {
|
||||
dialogView.errorBox.setText(R.string.settings__keyboard__subtype_error_already_exists)
|
||||
dialogView.errorBox.setText(R.string.settings__localization__subtype_error_already_exists)
|
||||
dialogView.errorBox.visibility = View.VISIBLE
|
||||
} else {
|
||||
updateSubtypeListView()
|
||||
@@ -123,7 +123,7 @@ class KeyboardFragment : SettingsMainActivity.SettingsFragment() {
|
||||
private fun showEditSubtypeDialog(id: Int) {
|
||||
val subtype = subtypeManager.getSubtypeById(id) ?: return
|
||||
val dialogView =
|
||||
SettingsFragmentKeyboardSubtypeDialogBinding.inflate(layoutInflater)
|
||||
SettingsFragmentTypingSubtypeDialogBinding.inflate(layoutInflater)
|
||||
val languageAdapter: ArrayAdapter<String> = ArrayAdapter(
|
||||
requireContext(),
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
@@ -143,10 +143,10 @@ class KeyboardFragment : SettingsMainActivity.SettingsFragment() {
|
||||
subtypeManager.imeConfig.characterLayouts.keys.toList().indexOf(subtype.layout)
|
||||
)
|
||||
AlertDialog.Builder(context).apply {
|
||||
setTitle(R.string.settings__keyboard__subtype_edit_title)
|
||||
setTitle(R.string.settings__localization__subtype_edit_title)
|
||||
setCancelable(true)
|
||||
setView(dialogView.root)
|
||||
setPositiveButton(R.string.settings__keyboard__subtype_apply) { _, _ ->
|
||||
setPositiveButton(R.string.settings__localization__subtype_apply) { _, _ ->
|
||||
val languageCode = subtypeManager.imeConfig.defaultSubtypesLanguageCodes[dialogView.languageSpinner.selectedItemPosition]
|
||||
val layoutName = subtypeManager.imeConfig.characterLayouts.keys.toList()[dialogView.layoutSpinner.selectedItemPosition]
|
||||
subtype.locale = LocaleUtils.stringToLocale(languageCode)
|
||||
@@ -154,11 +154,11 @@ class KeyboardFragment : SettingsMainActivity.SettingsFragment() {
|
||||
subtypeManager.modifySubtypeWithSameId(subtype)
|
||||
updateSubtypeListView()
|
||||
}
|
||||
setNeutralButton(R.string.settings__keyboard__subtype_delete) { _, _ ->
|
||||
setNeutralButton(R.string.settings__localization__subtype_delete) { _, _ ->
|
||||
subtypeManager.removeSubtype(subtype)
|
||||
updateSubtypeListView()
|
||||
}
|
||||
setNegativeButton(R.string.settings__keyboard__subtype_cancel) { _, _ -> }
|
||||
setNegativeButton(R.string.settings__localization__subtype_cancel) { _, _ -> }
|
||||
setOnDismissListener { activeDialogWindow = null }
|
||||
create()
|
||||
activeDialogWindow = show()
|
||||
@@ -174,9 +174,9 @@ class KeyboardFragment : SettingsMainActivity.SettingsFragment() {
|
||||
binding.subtypeNotConfWarning.visibility = View.GONE
|
||||
for (subtype in subtypes) {
|
||||
val itemView =
|
||||
SettingsFragmentKeyboardSubtypeListItemBinding.inflate(layoutInflater)
|
||||
ListItemBinding.inflate(layoutInflater)
|
||||
itemView.title.text = subtype.locale.displayName
|
||||
itemView.caption.text = subtypeManager.imeConfig.characterLayouts[subtype.layout]
|
||||
itemView.summary.text = subtypeManager.imeConfig.characterLayouts[subtype.layout]
|
||||
itemView.root.setOnClickListener { showEditSubtypeDialog(subtype.id) }
|
||||
binding.subtypeListView.addView(itemView.root)
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import dev.patrickgold.florisboard.databinding.SetupFragmentFinishBinding
|
||||
import dev.patrickgold.florisboard.ime.theme.Theme
|
||||
|
||||
class FinishFragment : Fragment() {
|
||||
private lateinit var binding: SetupFragmentFinishBinding
|
||||
@@ -33,6 +34,12 @@ class FinishFragment : Fragment() {
|
||||
): View? {
|
||||
binding = SetupFragmentFinishBinding.inflate(inflater, container, false)
|
||||
|
||||
// Set theme to floris_day
|
||||
Theme.writeThemeToPrefs(
|
||||
(activity as SetupActivity).prefs,
|
||||
Theme.fromJsonFile(requireContext(), "ime/theme/floris_day.json")!!
|
||||
)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package dev.patrickgold.florisboard.setup
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -26,6 +27,7 @@ import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
|
||||
class MakeDefaultFragment : Fragment(), SetupActivity.EventListener {
|
||||
private lateinit var binding: SetupFragmentMakeDefaultBinding
|
||||
private var osHandler: Handler? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@@ -60,6 +62,11 @@ class MakeDefaultFragment : Fragment(), SetupActivity.EventListener {
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
if (hasFocus && context != null) {
|
||||
updateState()
|
||||
if (osHandler == null) {
|
||||
osHandler = Handler()
|
||||
}
|
||||
osHandler?.postDelayed({ updateState() }, 250)
|
||||
osHandler?.postDelayed({ updateState() }, 500)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class SetupActivity : AppCompatActivity() {
|
||||
private lateinit var adapter: ViewPagerAdapter
|
||||
private lateinit var binding: SetupActivityBinding
|
||||
lateinit var imm: InputMethodManager
|
||||
private lateinit var prefs: PrefHelper
|
||||
lateinit var prefs: PrefHelper
|
||||
private var shouldFinish: Boolean = false
|
||||
private var shouldLaunchSettings: Boolean = true
|
||||
|
||||
|
||||
@@ -16,11 +16,14 @@
|
||||
|
||||
package dev.patrickgold.florisboard.util
|
||||
|
||||
import android.content.Context
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
|
||||
|
||||
/**
|
||||
* This file has been taken from the Android LatinIME project. Following modifications were done to
|
||||
* the original source code:
|
||||
@@ -71,4 +74,30 @@ object ViewLayoutUtils {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method converts dp unit to equivalent pixels, depending on device density.
|
||||
*
|
||||
* Source: https://stackoverflow.com/a/9563438/6801193 (by Muhammad Nabeel Arif)
|
||||
*
|
||||
* @param dp A value in dp (density independent pixels) unit. Which we need to convert into pixels
|
||||
* @param context Context to get resources and device specific display metrics
|
||||
* @return A float value to represent px equivalent to dp depending on device density
|
||||
*/
|
||||
fun convertDpToPixel(dp: Float, context: Context): Float {
|
||||
return dp * (context.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method converts device specific pixels to density independent pixels.
|
||||
*
|
||||
* Source: https://stackoverflow.com/a/9563438/6801193 (by Muhammad Nabeel Arif)
|
||||
*
|
||||
* @param px A value in px (pixels) unit. Which we need to convert into db
|
||||
* @param context Context to get resources and device specific display metrics
|
||||
* @return A float value to represent dp equivalent to px value
|
||||
*/
|
||||
fun convertPixelsToDp(px: Float, context: Context): Float {
|
||||
return px / (context.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.util
|
||||
|
||||
import android.text.InputType
|
||||
import android.text.TextUtils
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
fun EditorInfo.debugSummarize(): String {
|
||||
var summary = this::class.qualifiedName + "\r\n"
|
||||
summary += "imeOptions: " + this.imeOptions.debugSummarize(EditorInfo::class) + "\r\n"
|
||||
summary += "initialCapsMode: " + this.initialCapsMode.debugSummarize(TextUtils::class) + "\r\n"
|
||||
summary += "initialSelStart: " + this.initialSelStart + "\r\n"
|
||||
summary += "initialSelEnd: " + this.initialSelEnd + "\r\n"
|
||||
summary += "inputType: " + this.inputType.debugSummarize(InputType::class) + "\r\n"
|
||||
summary += "packageName: " + this.packageName
|
||||
return summary
|
||||
}
|
||||
|
||||
fun <T: Any> Int.debugSummarize(type: KClass<T>): String {
|
||||
var summary = ""
|
||||
when (type) {
|
||||
EditorInfo::class -> {
|
||||
when (this) {
|
||||
EditorInfo.IME_NULL -> {
|
||||
summary += "IME_NULL"
|
||||
}
|
||||
else -> {
|
||||
val tAction = when (this and EditorInfo.IME_MASK_ACTION) {
|
||||
EditorInfo.IME_ACTION_DONE -> "IME_ACTION_DONE"
|
||||
EditorInfo.IME_ACTION_GO -> "IME_ACTION_GO"
|
||||
EditorInfo.IME_ACTION_NEXT -> "IME_ACTION_NEXT"
|
||||
EditorInfo.IME_ACTION_NONE -> "IME_ACTION_NONE"
|
||||
EditorInfo.IME_ACTION_PREVIOUS -> "IME_ACTION_PREVIOUS"
|
||||
EditorInfo.IME_ACTION_SEARCH -> "IME_ACTION_SEARCH"
|
||||
EditorInfo.IME_ACTION_SEND -> "IME_ACTION_SEND"
|
||||
EditorInfo.IME_ACTION_UNSPECIFIED -> "IME_ACTION_UNSPECIFIED"
|
||||
else -> String.format("0x%08x", this and EditorInfo.IME_MASK_ACTION)
|
||||
}
|
||||
var tFlags = ""
|
||||
if (this and EditorInfo.IME_FLAG_FORCE_ASCII > 0) {
|
||||
tFlags += "IME_FLAG_FORCE_ASCII|"
|
||||
}
|
||||
if (this and EditorInfo.IME_FLAG_NAVIGATE_NEXT > 0) {
|
||||
tFlags += "IME_FLAG_NAVIGATE_NEXT|"
|
||||
}
|
||||
if (this and EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS > 0) {
|
||||
tFlags += "IME_FLAG_NAVIGATE_PREVIOUS|"
|
||||
}
|
||||
if (this and EditorInfo.IME_FLAG_NO_ACCESSORY_ACTION > 0) {
|
||||
tFlags += "IME_FLAG_NO_ACCESSORY_ACTION|"
|
||||
}
|
||||
if (this and EditorInfo.IME_FLAG_NO_ENTER_ACTION > 0) {
|
||||
tFlags += "IME_FLAG_NO_ENTER_ACTION|"
|
||||
}
|
||||
if (this and EditorInfo.IME_FLAG_NO_EXTRACT_UI > 0) {
|
||||
tFlags += "IME_FLAG_NO_EXTRACT_UI|"
|
||||
}
|
||||
if (this and EditorInfo.IME_FLAG_NO_FULLSCREEN > 0) {
|
||||
tFlags += "IME_FLAG_NO_FULLSCREEN|"
|
||||
}
|
||||
if (this and EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING > 0) {
|
||||
tFlags += "IME_FLAG_NO_PERSONALIZED_LEARNING|"
|
||||
}
|
||||
if (tFlags.isEmpty()) {
|
||||
tFlags = "(none)"
|
||||
}
|
||||
if (tFlags.endsWith("|")) {
|
||||
tFlags = tFlags.substring(0, tFlags.length - 1)
|
||||
}
|
||||
summary += "action=$tAction flags=$tFlags"
|
||||
}
|
||||
}
|
||||
}
|
||||
InputType::class -> {
|
||||
when (this) {
|
||||
InputType.TYPE_NULL -> {
|
||||
summary += "TYPE_NULL"
|
||||
}
|
||||
else -> {
|
||||
val tClass: String
|
||||
val tVariation: String
|
||||
var tFlags = ""
|
||||
when (this and InputType.TYPE_MASK_CLASS) {
|
||||
InputType.TYPE_CLASS_DATETIME -> {
|
||||
tClass = "TYPE_CLASS_DATETIME"
|
||||
tVariation = when (this and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_DATETIME_VARIATION_DATE -> "TYPE_DATETIME_VARIATION_DATE"
|
||||
InputType.TYPE_DATETIME_VARIATION_NORMAL -> "TYPE_DATETIME_VARIATION_NORMAL"
|
||||
InputType.TYPE_DATETIME_VARIATION_TIME -> "TYPE_DATETIME_VARIATION_TIME"
|
||||
else -> String.format("0x%08x", this and InputType.TYPE_MASK_VARIATION)
|
||||
}
|
||||
}
|
||||
InputType.TYPE_CLASS_NUMBER -> {
|
||||
tClass = "TYPE_CLASS_NUMBER"
|
||||
tVariation = when (this and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_NUMBER_VARIATION_NORMAL -> "TYPE_NUMBER_VARIATION_NORMAL"
|
||||
InputType.TYPE_NUMBER_VARIATION_PASSWORD -> "TYPE_NUMBER_VARIATION_PASSWORD"
|
||||
else -> String.format("0x%08x", this and InputType.TYPE_MASK_VARIATION)
|
||||
}
|
||||
if (this and InputType.TYPE_NUMBER_FLAG_DECIMAL > 0) {
|
||||
tFlags += "TYPE_NUMBER_FLAG_DECIMAL|"
|
||||
}
|
||||
if (this and InputType.TYPE_NUMBER_FLAG_SIGNED > 0) {
|
||||
tFlags += "TYPE_NUMBER_FLAG_SIGNED|"
|
||||
}
|
||||
}
|
||||
InputType.TYPE_CLASS_PHONE -> {
|
||||
tClass = "TYPE_CLASS_PHONE"
|
||||
tVariation = String.format("0x%08x", this and InputType.TYPE_MASK_VARIATION)
|
||||
}
|
||||
InputType.TYPE_CLASS_TEXT -> {
|
||||
tClass = "TYPE_CLASS_TEXT"
|
||||
tVariation = when (this and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS -> "TYPE_TEXT_VARIATION_EMAIL_ADDRESS"
|
||||
InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT -> "TYPE_TEXT_VARIATION_EMAIL_SUBJECT"
|
||||
InputType.TYPE_TEXT_VARIATION_FILTER -> "TYPE_TEXT_VARIATION_FILTER"
|
||||
InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE -> "TYPE_TEXT_VARIATION_LONG_MESSAGE"
|
||||
InputType.TYPE_TEXT_VARIATION_NORMAL -> "TYPE_TEXT_VARIATION_NORMAL"
|
||||
InputType.TYPE_TEXT_VARIATION_PASSWORD -> "TYPE_TEXT_VARIATION_PASSWORD"
|
||||
InputType.TYPE_TEXT_VARIATION_PERSON_NAME -> "TYPE_TEXT_VARIATION_PERSON_NAME"
|
||||
InputType.TYPE_TEXT_VARIATION_PHONETIC -> "TYPE_TEXT_VARIATION_PHONETIC"
|
||||
InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS -> "TYPE_TEXT_VARIATION_POSTAL_ADDRESS"
|
||||
InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE -> "TYPE_TEXT_VARIATION_SHORT_MESSAGE"
|
||||
InputType.TYPE_TEXT_VARIATION_URI -> "TYPE_TEXT_VARIATION_URI"
|
||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD -> "TYPE_TEXT_VARIATION_VISIBLE_PASSWORD"
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT -> "TYPE_TEXT_VARIATION_WEB_EDIT_TEXT"
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS -> "TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS"
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD -> "TYPE_TEXT_VARIATION_WEB_PASSWORD"
|
||||
else -> String.format("0x%08x", this and InputType.TYPE_MASK_VARIATION)
|
||||
}
|
||||
if (this and InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE > 0) {
|
||||
tFlags += "TYPE_TEXT_FLAG_AUTO_COMPLETE|"
|
||||
}
|
||||
if (this and InputType.TYPE_TEXT_FLAG_AUTO_CORRECT > 0) {
|
||||
tFlags += "TYPE_TEXT_FLAG_AUTO_CORRECT|"
|
||||
}
|
||||
if (this and InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS > 0) {
|
||||
tFlags += "TYPE_TEXT_FLAG_CAP_CHARACTERS|"
|
||||
}
|
||||
if (this and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES > 0) {
|
||||
tFlags += "TYPE_TEXT_FLAG_CAP_SENTENCES|"
|
||||
}
|
||||
if (this and InputType.TYPE_TEXT_FLAG_CAP_WORDS > 0) {
|
||||
tFlags += "TYPE_TEXT_FLAG_CAP_WORDS|"
|
||||
}
|
||||
if (this and InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE > 0) {
|
||||
tFlags += "TYPE_TEXT_FLAG_IME_MULTI_LINE|"
|
||||
}
|
||||
if (this and InputType.TYPE_TEXT_FLAG_MULTI_LINE > 0) {
|
||||
tFlags += "TYPE_TEXT_FLAG_MULTI_LINE|"
|
||||
}
|
||||
if (this and InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS > 0) {
|
||||
tFlags += "TYPE_TEXT_FLAG_NO_SUGGESTIONS|"
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
tClass = String.format("0x%08x", this and InputType.TYPE_MASK_CLASS)
|
||||
tVariation = String.format("0x%08x", this and InputType.TYPE_MASK_VARIATION)
|
||||
}
|
||||
}
|
||||
if (tFlags.isEmpty()) {
|
||||
tFlags = "(none)"
|
||||
}
|
||||
if (tFlags.endsWith("|")) {
|
||||
tFlags = tFlags.substring(0, tFlags.length - 1)
|
||||
}
|
||||
summary += "class=$tClass variation=$tVariation flags=$tFlags"
|
||||
}
|
||||
}
|
||||
}
|
||||
TextUtils::class -> {
|
||||
var tFlags = ""
|
||||
if (this and TextUtils.CAP_MODE_CHARACTERS > 0) {
|
||||
tFlags += "CAP_MODE_CHARACTERS|"
|
||||
}
|
||||
if (this and TextUtils.CAP_MODE_SENTENCES > 0) {
|
||||
tFlags += "CAP_MODE_SENTENCES|"
|
||||
}
|
||||
if (this and TextUtils.CAP_MODE_WORDS > 0) {
|
||||
tFlags += "CAP_MODE_WORDS|"
|
||||
}
|
||||
if (this and TextUtils.SAFE_STRING_FLAG_FIRST_LINE > 0) {
|
||||
tFlags += "SAFE_STRING_FLAG_FIRST_LINE|"
|
||||
}
|
||||
if (this and TextUtils.SAFE_STRING_FLAG_SINGLE_LINE > 0) {
|
||||
tFlags += "SAFE_STRING_FLAG_SINGLE_LINE|"
|
||||
}
|
||||
if (this and TextUtils.SAFE_STRING_FLAG_TRIM > 0) {
|
||||
tFlags += "SAFE_STRING_FLAG_TRIM|"
|
||||
}
|
||||
if (tFlags.isEmpty()) {
|
||||
tFlags = "(none)"
|
||||
}
|
||||
if (tFlags.endsWith("|")) {
|
||||
tFlags = tFlags.substring(0, tFlags.length - 1)
|
||||
}
|
||||
summary += "flags=$tFlags"
|
||||
}
|
||||
}
|
||||
return summary
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import androidx.core.view.children
|
||||
|
||||
fun getColorFromAttr(
|
||||
@@ -33,16 +34,28 @@ fun setBackgroundTintColor(view: View, colorId: Int) {
|
||||
getColorFromAttr(view.context, colorId)
|
||||
)
|
||||
}
|
||||
fun setBackgroundTintColor2(view: View, colorInt: Int) {
|
||||
view.backgroundTintList = ColorStateList.valueOf(colorInt)
|
||||
}
|
||||
fun setDrawableTintColor(view: Button, colorId: Int) {
|
||||
view.compoundDrawableTintList = ColorStateList.valueOf(
|
||||
getColorFromAttr(view.context, colorId)
|
||||
)
|
||||
}
|
||||
fun setDrawableTintColor2(view: Button, colorInt: Int) {
|
||||
view.compoundDrawableTintList = ColorStateList.valueOf(colorInt)
|
||||
}
|
||||
fun setImageTintColor2(view: ImageView, colorInt: Int) {
|
||||
view.imageTintList = ColorStateList.valueOf(colorInt)
|
||||
}
|
||||
fun setTextTintColor(view: View, colorId: Int) {
|
||||
view.foregroundTintList = ColorStateList.valueOf(
|
||||
getColorFromAttr(view.context, colorId)
|
||||
)
|
||||
}
|
||||
fun setTextTintColor2(view: View, colorInt: Int) {
|
||||
view.foregroundTintList = ColorStateList.valueOf(colorInt)
|
||||
}
|
||||
|
||||
fun refreshLayoutOf(view: View?) {
|
||||
if (view is ViewGroup) {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_pressed="true">
|
||||
<shape>
|
||||
<solid android:color="?semiTransparentColor"/>
|
||||
<stroke android:width="0.5dp" android:color="?semiTransparentColor"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item android:state_focused="true">
|
||||
<shape>
|
||||
<solid android:color="?semiTransparentColor"/>
|
||||
<stroke android:width="0.5dp" android:color="?semiTransparentColor"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape>
|
||||
<solid android:color="@android:color/transparent"/>
|
||||
<stroke android:width="0.5dp" android:color="?semiTransparentColor"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
5
app/src/main/res/drawable/ic_arrow_back.xml
Normal file
5
app/src/main/res/drawable/ic_arrow_back.xml
Normal 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="#FF000000" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_content_copy.xml
Normal file
5
app/src/main/res/drawable/ic_content_copy.xml
Normal 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="#FF000000" android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_content_cut.xml
Normal file
5
app/src/main/res/drawable/ic_content_cut.xml
Normal 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="#FF000000" android:pathData="M9.64,7.64c0.23,-0.5 0.36,-1.05 0.36,-1.64 0,-2.21 -1.79,-4 -4,-4S2,3.79 2,6s1.79,4 4,4c0.59,0 1.14,-0.13 1.64,-0.36L10,12l-2.36,2.36C7.14,14.13 6.59,14 6,14c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4c0,-0.59 -0.13,-1.14 -0.36,-1.64L12,14l7,7h3v-1L9.64,7.64zM6,8c-1.1,0 -2,-0.89 -2,-2s0.9,-2 2,-2 2,0.89 2,2 -0.9,2 -2,2zM6,20c-1.1,0 -2,-0.89 -2,-2s0.9,-2 2,-2 2,0.89 2,2 -0.9,2 -2,2zM12,12.5c-0.28,0 -0.5,-0.22 -0.5,-0.5s0.22,-0.5 0.5,-0.5 0.5,0.22 0.5,0.5 -0.22,0.5 -0.5,0.5zM19,3l-6,6 2,2 7,-7L22,3z"/>
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_content_paste.xml
Normal file
5
app/src/main/res/drawable/ic_content_paste.xml
Normal 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="#FF000000" android:pathData="M19,2h-4.18C14.4,0.84 13.3,0 12,0c-1.3,0 -2.4,0.84 -2.82,2L5,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,4c0,-1.1 -0.9,-2 -2,-2zM12,2c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM19,20L5,20L5,4h2v3h10L17,4h2v16z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<item
|
||||
android:left="8dp"
|
||||
android:right="8dp"
|
||||
android:drawable="@drawable/ic_content_paste"/>
|
||||
</layer-list>
|
||||
5
app/src/main/res/drawable/ic_first_page.xml
Normal file
5
app/src/main/res/drawable/ic_first_page.xml
Normal 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="#FF000000" android:pathData="M18.41,16.59L13.82,12l4.59,-4.59L17,6l-6,6 6,6zM6,6h2v12H6z"/>
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_format_italic.xml
Normal file
5
app/src/main/res/drawable/ic_format_italic.xml
Normal 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="#FF000000" android:pathData="M10,4v3h2.21l-3.42,8H6v3h8v-3h-2.21l3.42,-8H18V4z"/>
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_last_page.xml
Normal file
5
app/src/main/res/drawable/ic_last_page.xml
Normal 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="#FF000000" android:pathData="M5.59,7.41L10.18,12l-4.59,4.59L7,18l6,-6 -6,-6zM16,6h2v12h-2z"/>
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_select_all.xml
Normal file
5
app/src/main/res/drawable/ic_select_all.xml
Normal 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="#FF000000" android:pathData="M3,5h2L5,3c-1.1,0 -2,0.9 -2,2zM3,13h2v-2L3,11v2zM7,21h2v-2L7,19v2zM3,9h2L5,7L3,7v2zM13,3h-2v2h2L13,3zM19,3v2h2c0,-1.1 -0.9,-2 -2,-2zM5,21v-2L3,19c0,1.1 0.9,2 2,2zM3,17h2v-2L3,15v2zM9,3L7,3v2h2L9,3zM11,21h2v-2h-2v2zM19,13h2v-2h-2v2zM19,21c1.1,0 2,-0.9 2,-2h-2v2zM19,9h2L21,7h-2v2zM19,17h2v-2h-2v2zM15,21h2v-2h-2v2zM15,5h2L17,3h-2v2zM7,17h10L17,7L7,7v10zM9,9h6v6L9,15L9,9z"/>
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_spellcheck.xml
Normal file
5
app/src/main/res/drawable/ic_spellcheck.xml
Normal 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="#FF000000" android:pathData="M12.45,16h2.09L9.43,3L7.57,3L2.46,16h2.09l1.12,-3h5.64l1.14,3zM6.43,11L8.5,5.48 10.57,11L6.43,11zM21.59,11.59l-8.09,8.09L9.83,16l-1.41,1.41 5.09,5.09L23,13l-1.41,-1.41z"/>
|
||||
</vector>
|
||||
@@ -2,16 +2,16 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape>
|
||||
<padding android:top="1dp" android:right="1dp" android:bottom="1dp" android:left="1dp" />
|
||||
<solid android:color="?key_popup_extended_shadowColor" />
|
||||
<corners android:radius="@dimen/key_borderRadius" />
|
||||
<padding android:top="1dp" android:right="1dp" android:bottom="1dp" android:left="1dp"/>
|
||||
<solid android:color="#CDAFAFAF"/>
|
||||
<corners android:radius="@dimen/key_borderRadius"/>
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="?key_popup_bgColor" />
|
||||
<corners android:radius="@dimen/key_borderRadius" />
|
||||
<solid android:color="@android:color/white"/>
|
||||
<corners android:radius="@dimen/key_borderRadius"/>
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
||||
6
app/src/main/res/drawable/shape_rect_rounded_2.xml
Normal file
6
app/src/main/res/drawable/shape_rect_rounded_2.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@android:color/black" />
|
||||
<corners android:radius="@dimen/smartbar_radius" />
|
||||
</shape>
|
||||
16
app/src/main/res/layout/advanced_activity.xml
Normal file
16
app/src/main/res/layout/advanced_activity.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include layout="@layout/toolbar"/>
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/advanced_fragment_frame"
|
||||
android:name="dev.patrickgold.florisboard.settings.fragments.AdvancedFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
</LinearLayout>
|
||||
49
app/src/main/res/layout/crash_dialog.xml
Normal file
49
app/src/main/res/layout/crash_dialog.xml
Normal file
@@ -0,0 +1,49 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp"
|
||||
android:theme="@style/CrashDialogTheme">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"
|
||||
android:text="@string/crash_dialog__description"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/copy_to_clipboard"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"
|
||||
android:text="@string/crash_dialog__copy_to_clipboard"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/open_bug_report_form"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"
|
||||
android:text="@string/crash_dialog__open_bug_report_form"/>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/stacktrace"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"/>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<Button
|
||||
android:id="@+id/close"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"
|
||||
android:text="@string/crash_dialog__close"/>
|
||||
|
||||
</LinearLayout>
|
||||
138
app/src/main/res/layout/editing_layout.xml
Normal file
138
app/src/main/res/layout/editing_layout.xml
Normal file
@@ -0,0 +1,138 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyboardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/editing"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/white"
|
||||
android:backgroundTintMode="multiply">
|
||||
|
||||
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
|
||||
android:id="@+id/arrow_left"
|
||||
style="@style/TextEditingButton"
|
||||
app:layout_constraintWidth_percent="0.2"
|
||||
app:layout_constraintHeight_percent="0.75"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/move_home"
|
||||
android:src="@drawable/ic_keyboard_arrow_left"/>
|
||||
|
||||
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
|
||||
android:id="@+id/arrow_up"
|
||||
style="@style/TextEditingButton"
|
||||
app:layout_constraintWidth_percent="0.3"
|
||||
app:layout_constraintHeight_percent="0.25"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@+id/arrow_left"
|
||||
android:src="@drawable/ic_keyboard_arrow_up"/>
|
||||
|
||||
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
|
||||
android:id="@+id/select"
|
||||
style="@style/TextEditingButton"
|
||||
app:layout_constraintWidth_percent="0.3"
|
||||
app:layout_constraintHeight_percent="0.25"
|
||||
app:layout_constraintTop_toBottomOf="@+id/arrow_up"
|
||||
app:layout_constraintLeft_toRightOf="@+id/arrow_left"
|
||||
android:text="@android:string/selectTextMode"/>
|
||||
|
||||
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
|
||||
android:id="@+id/arrow_down"
|
||||
style="@style/TextEditingButton"
|
||||
app:layout_constraintWidth_percent="0.3"
|
||||
app:layout_constraintHeight_percent="0.25"
|
||||
app:layout_constraintTop_toBottomOf="@+id/select"
|
||||
app:layout_constraintLeft_toRightOf="@+id/arrow_left"
|
||||
android:src="@drawable/ic_keyboard_arrow_down"/>
|
||||
|
||||
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
|
||||
android:id="@+id/arrow_right"
|
||||
style="@style/TextEditingButton"
|
||||
app:layout_constraintWidth_percent="0.2"
|
||||
app:layout_constraintHeight_percent="0.75"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/move_end"
|
||||
app:layout_constraintLeft_toRightOf="@+id/select"
|
||||
android:src="@drawable/ic_keyboard_arrow_right"/>
|
||||
|
||||
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
|
||||
android:id="@+id/move_home"
|
||||
style="@style/TextEditingButton"
|
||||
app:layout_constraintWidth_percent="0.35"
|
||||
app:layout_constraintHeight_percent="0.25"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:src="@drawable/ic_first_page"/>
|
||||
|
||||
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
|
||||
android:id="@+id/move_end"
|
||||
style="@style/TextEditingButton"
|
||||
app:layout_constraintWidth_percent="0.35"
|
||||
app:layout_constraintHeight_percent="0.25"
|
||||
app:layout_constraintLeft_toRightOf="@+id/move_home"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:src="@drawable/ic_last_page"/>
|
||||
|
||||
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
|
||||
android:id="@+id/select_all"
|
||||
style="@style/TextEditingButton"
|
||||
app:layout_constraintWidth_percent="0.3"
|
||||
app:layout_constraintHeight_percent="0.25"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@+id/arrow_right"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:text="@android:string/selectAll"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
|
||||
android:id="@+id/clipboard_cut"
|
||||
style="@style/TextEditingButton"
|
||||
app:layout_constraintWidth_percent="0.3"
|
||||
app:layout_constraintHeight_percent="0.25"
|
||||
app:layout_constraintTop_toTopOf="@+id/select_all"
|
||||
app:layout_constraintLeft_toRightOf="@+id/arrow_right"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:text="@android:string/cut"/>
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="select_all,clipboard_cut"/>
|
||||
|
||||
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
|
||||
android:id="@+id/clipboard_copy"
|
||||
style="@style/TextEditingButton"
|
||||
app:layout_constraintWidth_percent="0.3"
|
||||
app:layout_constraintHeight_percent="0.25"
|
||||
app:layout_constraintTop_toBottomOf="@+id/barrier1"
|
||||
app:layout_constraintBottom_toTopOf="@+id/clipboard_paste"
|
||||
app:layout_constraintLeft_toRightOf="@+id/arrow_right"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:text="@android:string/copy"/>
|
||||
|
||||
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
|
||||
android:id="@+id/clipboard_paste"
|
||||
style="@style/TextEditingButton"
|
||||
app:layout_constraintWidth_percent="0.3"
|
||||
app:layout_constraintHeight_percent="0.25"
|
||||
app:layout_constraintTop_toBottomOf="@+id/clipboard_copy"
|
||||
app:layout_constraintBottom_toTopOf="@+id/backspace"
|
||||
app:layout_constraintLeft_toRightOf="@+id/arrow_right"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:text="@android:string/paste"/>
|
||||
|
||||
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
|
||||
android:id="@+id/backspace"
|
||||
style="@style/TextEditingButton"
|
||||
app:layout_constraintWidth_percent="0.3"
|
||||
app:layout_constraintHeight_percent="0.25"
|
||||
app:layout_constraintTop_toBottomOf="@+id/clipboard_paste"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@+id/move_end"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:src="@drawable/ic_backspace"/>
|
||||
|
||||
</dev.patrickgold.florisboard.ime.text.editing.EditingKeyboardView>
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<dev.patrickgold.florisboard.ime.core.InputView
|
||||
<dev.patrickgold.florisboard.ime.core.InputWindowView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/florisboard"
|
||||
android:layout_width="match_parent"
|
||||
@@ -8,13 +8,13 @@
|
||||
android:gravity="bottom"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
<dev.patrickgold.florisboard.ime.core.InputView
|
||||
android:id="@+id/inner_input_view_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:orientation="horizontal"
|
||||
android:background="?keyboard_bgColor"
|
||||
android:background="?inputView_bgColorFallback"
|
||||
android:baselineAligned="false">
|
||||
|
||||
<LinearLayout
|
||||
@@ -25,32 +25,34 @@
|
||||
<ImageButton
|
||||
android:id="@+id/one_handed_ctrl_close_start"
|
||||
style="@style/OneHandedPanelButton"
|
||||
android:src="@drawable/ic_zoom_out_map"/>
|
||||
android:src="@drawable/ic_zoom_out_map"
|
||||
android:contentDescription="@string/one_handed__close_btn_content_description"/>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/one_handed_button_height"
|
||||
android:visibility="invisible" />
|
||||
android:visibility="invisible"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/one_handed_ctrl_move_start"
|
||||
style="@style/OneHandedPanelButton"
|
||||
android:src="@drawable/ic_keyboard_arrow_left"/>
|
||||
android:src="@drawable/ic_keyboard_arrow_left"
|
||||
android:contentDescription="@string/one_handed__move_start_btn_content_description"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ViewFlipper
|
||||
<dev.patrickgold.florisboard.ime.core.FlorisViewFlipper
|
||||
android:id="@+id/main_view_flipper"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:measureAllChildren="false">
|
||||
|
||||
<include layout="@layout/text_input_layout" />
|
||||
<include layout="@layout/text_input_layout"/>
|
||||
|
||||
<include layout="@layout/media_input_layout" />
|
||||
<include layout="@layout/media_input_layout"/>
|
||||
|
||||
</ViewFlipper>
|
||||
</dev.patrickgold.florisboard.ime.core.FlorisViewFlipper>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/one_handed_ctrl_panel_end"
|
||||
@@ -60,20 +62,22 @@
|
||||
<ImageButton
|
||||
android:id="@+id/one_handed_ctrl_close_end"
|
||||
style="@style/OneHandedPanelButton"
|
||||
android:src="@drawable/ic_zoom_out_map"/>
|
||||
android:src="@drawable/ic_zoom_out_map"
|
||||
android:contentDescription="@string/one_handed__close_btn_content_description"/>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/one_handed_button_height"
|
||||
android:visibility="invisible" />
|
||||
android:visibility="invisible"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/one_handed_ctrl_move_end"
|
||||
style="@style/OneHandedPanelButton"
|
||||
android:src="@drawable/ic_keyboard_arrow_right"/>
|
||||
android:src="@drawable/ic_keyboard_arrow_right"
|
||||
android:contentDescription="@string/one_handed__move_end_btn_content_description"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</dev.patrickgold.florisboard.ime.core.InputView>
|
||||
|
||||
</dev.patrickgold.florisboard.ime.core.InputView>
|
||||
</dev.patrickgold.florisboard.ime.core.InputWindowView>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user