Compare commits
161 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe6930fb76 | ||
|
|
6a10f0a01a | ||
|
|
30717eeb90 | ||
|
|
a664ab18c9 | ||
|
|
f50983d7ab | ||
|
|
be858802c5 | ||
|
|
1ba690e53a | ||
|
|
e16f81d350 | ||
|
|
0de2039d72 | ||
|
|
50b6a63468 | ||
|
|
8cb644b418 | ||
|
|
f138124670 | ||
|
|
0f76d7f9df | ||
|
|
27b9ec4628 | ||
|
|
ac733ed1dc | ||
|
|
6d15708f95 | ||
|
|
4377f3e41c | ||
|
|
1e690018d7 | ||
|
|
93bb5d2714 | ||
|
|
ad2b08a342 | ||
|
|
9e6508cee4 | ||
|
|
f735c138fb | ||
|
|
d663947fec | ||
|
|
c800617e26 | ||
|
|
f47c7abaf3 | ||
|
|
faf06ee234 | ||
|
|
07c41f9c27 | ||
|
|
80a0d9edab | ||
|
|
cd943a9d4a | ||
|
|
c3d3107b12 | ||
|
|
b91fac8e76 | ||
|
|
e2c784f4cf | ||
|
|
f83bdd8a28 | ||
|
|
dc10a459ca | ||
|
|
4bea68f151 | ||
|
|
daa8ce71ac | ||
|
|
f06f475e89 | ||
|
|
b784d0805c | ||
|
|
c245c6a37c | ||
|
|
264a287171 | ||
|
|
82d82466c6 | ||
|
|
0242d24cd1 | ||
|
|
76e683bfec | ||
|
|
ee1988d98e | ||
|
|
fe5f0d18ac | ||
|
|
41527e4f23 | ||
|
|
66fb1c5873 | ||
|
|
05103214dd | ||
|
|
bf9e2e4438 | ||
|
|
4209bdcfbe | ||
|
|
31db482bb4 | ||
|
|
e33499dab5 | ||
|
|
92b99ff34e | ||
|
|
f991c6479b | ||
|
|
5a45b1600a | ||
|
|
79f884b2a0 | ||
|
|
22330ad67b | ||
|
|
7f50a5aa77 | ||
|
|
de389918be | ||
|
|
4a57829105 | ||
|
|
bc6ca8c7fc | ||
|
|
0ffe0c915e | ||
|
|
392699f333 | ||
|
|
cf801c02fd | ||
|
|
665356f77b | ||
|
|
48c356a569 | ||
|
|
60eb92e92a | ||
|
|
602ffc2a93 | ||
|
|
dbacc0e466 | ||
|
|
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!
|
||||
-->
|
||||
32
.github/workflows/android.yml
vendored
Normal file
32
.github/workflows/android.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: FlorisBoard CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: set up JDK 1.8
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew clean assemble
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: app-debug.apk
|
||||
path: app/build/outputs/apk/debug/app-debug.apk
|
||||
@@ -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 [Crowdin](https://crowdin.florisboard.patrickgold.dev) 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.
|
||||
|
||||
155
README.md
155
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://github.com/florisboard/florisboard/releases) [](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,92 +31,142 @@ 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.
|
||||
|
||||
---
|
||||
|
||||

|
||||
<img align="right" height="256"
|
||||
src="https://patrickgold.dev/media/previews/florisboard-preview-day.png"
|
||||
alt="Preview image">
|
||||
|
||||
## Feature roadmap
|
||||
## Implemented features
|
||||
This list contains all implemented and fully functional features
|
||||
FlorisBoard currently has to offer. For planned features and its
|
||||
milestones, please refer to the [Feature roadmap](#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
|
||||
|
||||
### 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, Hungarian); more
|
||||
coming in future versions
|
||||
* [x] Non-latin character layouts (Arabic, 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
|
||||
* [x] Emoticon layout
|
||||
* [ ] Kaomoji layout
|
||||
|
||||
### 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
|
||||
* [x] Subtype selection (language/layout)
|
||||
* [x] Keyboard behaviour preferences
|
||||
* [ ] Text suggestion / Auto correct preferences
|
||||
* [ ] Gesture preferences
|
||||
|
||||
### Composing suggestions
|
||||
* [ ] Auto suggest words from precompiled dictionary
|
||||
* [ ] Auto suggest words from user dictionary
|
||||
* [ ] Auto suggest contacts
|
||||
* [ ] Multilingual typing
|
||||
* [x] Gesture preferences
|
||||
|
||||
### 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
|
||||
* [x] Gesture support
|
||||
* [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
|
||||
## Feature roadmap
|
||||
This section describes the features which are planned to be implemented
|
||||
in FlorisBoard for the next major versions, modularized into sections.
|
||||
Please note that the milestone due dates are only raw estimates and will
|
||||
most likely be delayed back, even though I'm eager to stick to these as
|
||||
close as possible.
|
||||
|
||||
## Used libraries and icons
|
||||
### [v0.4.0](https://github.com/florisboard/florisboard/milestone/4)
|
||||
- Module A: Smartbar rework (Implemented with #91)
|
||||
- Ability to enable/disable Smartbar (features below thus only work if
|
||||
Smartbar is enabled)
|
||||
- Dynamic switching between clipboard tools and word suggestions
|
||||
- Ability to show both the number row and word suggestions at once
|
||||
- Better icons in quick actions
|
||||
- Complete rework of the Smartbar code base and the Smartbar layout
|
||||
definition in XML
|
||||
|
||||
- Module B: Composing suggestions
|
||||
- Auto-suggestion of words based of precompiled dictionaries
|
||||
- Management of custom dictionary entries
|
||||
- Opt-in only: Learning of often typed word pais to better predict next
|
||||
words over time. Data collected here is stored locally and never leaves
|
||||
the user's device.
|
||||
|
||||
- Module C: Extension packs
|
||||
- Ability to load dictionaries (and later potentionally other cool
|
||||
features too) only if needed to keep the core APK size small
|
||||
- Currently unclear how exactly this will work, but this is definitely
|
||||
a must-have feature
|
||||
|
||||
- Module D: Glide typing
|
||||
- Swiping over the characters will automatically convert this to a word
|
||||
- Possibly also add improvements based on the Flow keyboard
|
||||
|
||||
### [v0.5.0](https://github.com/florisboard/florisboard/milestone/5)
|
||||
There's no exact roadmap yet but it is planned that the media part of
|
||||
FlorisBoard (emojis, emoticons, kaomoji) gets a rework. Also as an extension
|
||||
(requires v0.4.0/Module C) GIF support is planned.
|
||||
|
||||
### > v0.5.0
|
||||
This is completely open as of now and will gather planned features as time
|
||||
passes...
|
||||
|
||||
Backlog (currently not assigned to any milestone):
|
||||
|
||||
- Theme import/export
|
||||
- Floating keyboard
|
||||
|
||||
## 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 21
|
||||
versionName "0.3.2"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -21,9 +21,14 @@ android {
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
resValue "string", "app_name", "FlorisBoard Debug"
|
||||
}
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
resValue "string", "app_name", "FlorisBoard"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,18 +36,22 @@ 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'
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -12,176 +12,150 @@
|
||||
"swiss_german": "Swiss German (QWERTZ)",
|
||||
"swiss_french": "Swiss French (QWERTZ)",
|
||||
"swiss_italian": "Swiss Italian (QWERTZ)",
|
||||
"persian": "Persian"
|
||||
"hungarian": "Hungarian (QWERTZ)",
|
||||
"persian": "Persian",
|
||||
"arabic": "Arabic",
|
||||
"esperanto": "Esperanto",
|
||||
"esperanto_with_hx": "Esperanto with 'ĥ'",
|
||||
"colemak": "Colemak",
|
||||
"dvorak": "Dvorak",
|
||||
"canadian_french": "Canadian French (QWERTY)"
|
||||
},
|
||||
"defaultSubtypes": [
|
||||
{
|
||||
"id": 101,
|
||||
"languageTag": "en-US",
|
||||
"preferredLayout": "qwerty",
|
||||
"isAsciiCapable": true,
|
||||
"isEmojiCapable": true
|
||||
"preferredLayout": "qwerty"
|
||||
},
|
||||
{
|
||||
"id": 102,
|
||||
"languageTag": "en-UK",
|
||||
"preferredLayout": "qwerty",
|
||||
"isAsciiCapable": true,
|
||||
"isEmojiCapable": true
|
||||
"preferredLayout": "qwerty"
|
||||
},
|
||||
{
|
||||
"id": 103,
|
||||
"languageTag": "en-CA",
|
||||
"preferredLayout": "qwerty",
|
||||
"isAsciiCapable": true,
|
||||
"isEmojiCapable": true
|
||||
"preferredLayout": "qwerty"
|
||||
},
|
||||
{
|
||||
"id": 104,
|
||||
"languageTag": "en-AU",
|
||||
"preferredLayout": "qwerty",
|
||||
"isAsciiCapable": true,
|
||||
"isEmojiCapable": true
|
||||
"preferredLayout": "qwerty"
|
||||
},
|
||||
{
|
||||
"id": 201,
|
||||
"languageTag": "de-DE",
|
||||
"preferredLayout": "qwertz",
|
||||
"isAsciiCapable": true,
|
||||
"isEmojiCapable": true
|
||||
"preferredLayout": "qwertz"
|
||||
},
|
||||
{
|
||||
"id": 202,
|
||||
"languageTag": "de-AT",
|
||||
"preferredLayout": "qwertz",
|
||||
"isAsciiCapable": true,
|
||||
"isEmojiCapable": true
|
||||
"preferredLayout": "qwertz"
|
||||
},
|
||||
{
|
||||
"id": 203,
|
||||
"languageTag": "de-CH",
|
||||
"preferredLayout": "swiss_german",
|
||||
"isAsciiCapable": true,
|
||||
"isEmojiCapable": true
|
||||
"preferredLayout": "swiss_german"
|
||||
},
|
||||
{
|
||||
"id": 301,
|
||||
"languageTag": "fr-FR",
|
||||
"preferredLayout": "azerty",
|
||||
"isAsciiCapable": true,
|
||||
"isEmojiCapable": true
|
||||
"preferredLayout": "azerty"
|
||||
},
|
||||
{
|
||||
"id": 302,
|
||||
"languageTag": "fr-CA",
|
||||
"preferredLayout": "qwerty",
|
||||
"isAsciiCapable": true,
|
||||
"isEmojiCapable": true
|
||||
"preferredLayout": "canadian_french"
|
||||
},
|
||||
{
|
||||
"id": 303,
|
||||
"languageTag": "fr-CH",
|
||||
"preferredLayout": "swiss_french",
|
||||
"isAsciiCapable": true,
|
||||
"isEmojiCapable": true
|
||||
"preferredLayout": "swiss_french"
|
||||
},
|
||||
{
|
||||
"id": 401,
|
||||
"languageTag": "it-IT",
|
||||
"preferredLayout": "qwerty",
|
||||
"isAsciiCapable": true,
|
||||
"isEmojiCapable": true
|
||||
"preferredLayout": "qwerty"
|
||||
},
|
||||
{
|
||||
"id": 402,
|
||||
"languageTag": "it-CH",
|
||||
"preferredLayout": "swiss_italian",
|
||||
"isAsciiCapable": true,
|
||||
"isEmojiCapable": true
|
||||
"preferredLayout": "swiss_italian"
|
||||
},
|
||||
{
|
||||
"id": 501,
|
||||
"languageTag": "es-ES",
|
||||
"preferredLayout": "spanish",
|
||||
"isAsciiCapable": true,
|
||||
"isEmojiCapable": true
|
||||
"preferredLayout": "spanish"
|
||||
},
|
||||
{
|
||||
"id": 502,
|
||||
"languageTag": "es-US",
|
||||
"preferredLayout": "spanish",
|
||||
"isAsciiCapable": true,
|
||||
"isEmojiCapable": true
|
||||
"preferredLayout": "spanish"
|
||||
},
|
||||
{
|
||||
"id": 503,
|
||||
"languageTag": "es-419",
|
||||
"preferredLayout": "spanish",
|
||||
"isAsciiCapable": true,
|
||||
"isEmojiCapable": true
|
||||
"preferredLayout": "spanish"
|
||||
},
|
||||
{
|
||||
"id": 601,
|
||||
"languageTag": "pt-PT",
|
||||
"preferredLayout": "qwerty",
|
||||
"isAsciiCapable": true,
|
||||
"isEmojiCapable": true
|
||||
"preferredLayout": "qwerty"
|
||||
},
|
||||
{
|
||||
"id": 602,
|
||||
"languageTag": "pt-BR",
|
||||
"preferredLayout": "qwerty",
|
||||
"isAsciiCapable": true,
|
||||
"isEmojiCapable": true
|
||||
"preferredLayout": "qwerty"
|
||||
},
|
||||
{
|
||||
"id": 701,
|
||||
"languageTag": "nb-NO",
|
||||
"preferredLayout": "norwegian",
|
||||
"isAsciiCapable": true,
|
||||
"isEmojiCapable": true
|
||||
"preferredLayout": "norwegian"
|
||||
},
|
||||
{
|
||||
"id": 702,
|
||||
"languageTag": "nn-NO",
|
||||
"preferredLayout": "norwegian",
|
||||
"isAsciiCapable": true,
|
||||
"isEmojiCapable": true
|
||||
"preferredLayout": "norwegian"
|
||||
},
|
||||
{
|
||||
"id": 711,
|
||||
"languageTag": "sv-SE",
|
||||
"preferredLayout": "swedish_finnish",
|
||||
"isAsciiCapable": true,
|
||||
"isEmojiCapable": true
|
||||
"preferredLayout": "swedish_finnish"
|
||||
},
|
||||
{
|
||||
"id": 721,
|
||||
"languageTag": "fi-FI",
|
||||
"preferredLayout": "swedish_finnish",
|
||||
"isAsciiCapable": true,
|
||||
"isEmojiCapable": true
|
||||
"preferredLayout": "swedish_finnish"
|
||||
},
|
||||
{
|
||||
"id": 731,
|
||||
"languageTag": "da-DK",
|
||||
"preferredLayout": "danish",
|
||||
"isAsciiCapable": true,
|
||||
"isEmojiCapable": true
|
||||
"preferredLayout": "danish"
|
||||
},
|
||||
{
|
||||
"id": 741,
|
||||
"languageTag": "is-IS",
|
||||
"preferredLayout": "icelandic",
|
||||
"isAsciiCapable": true,
|
||||
"isEmojiCapable": true
|
||||
"preferredLayout": "icelandic"
|
||||
},
|
||||
{
|
||||
"id": 800,
|
||||
"id": 801,
|
||||
"languageTag": "fa-FA",
|
||||
"preferredLayout": "persian",
|
||||
"isAsciiCapable": true,
|
||||
"isEmojiCapable": true
|
||||
"preferredLayout": "persian"
|
||||
},
|
||||
{
|
||||
"id": 901,
|
||||
"languageTag": "ar",
|
||||
"preferredLayout": "arabic"
|
||||
},
|
||||
{
|
||||
"id": 1001,
|
||||
"languageTag": "hu",
|
||||
"preferredLayout": "hungarian"
|
||||
},
|
||||
{
|
||||
"id": 1101,
|
||||
"languageTag": "eo",
|
||||
"preferredLayout": "esperanto"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
46
app/src/main/assets/ime/text/characters/arabic.json
Normal file
46
app/src/main/assets/ime/text/characters/arabic.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "arabic",
|
||||
"direction": "rtl",
|
||||
"modifier": "arabic",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 1590, "label": "ض" },
|
||||
{ "code": 1589, "label": "ص" },
|
||||
{ "code": 1579, "label": "ث" },
|
||||
{ "code": 1602, "label": "ق" },
|
||||
{ "code": 1601, "label": "ف" },
|
||||
{ "code": 1594, "label": "غ" },
|
||||
{ "code": 1593, "label": "ع" },
|
||||
{ "code": 1607, "label": "ه" },
|
||||
{ "code": 1582, "label": "خ" },
|
||||
{ "code": 1581, "label": "ح" },
|
||||
{ "code": 1580, "label": "ج" }
|
||||
],
|
||||
[
|
||||
{ "code": 1588, "label": "ش" },
|
||||
{ "code": 1587, "label": "س" },
|
||||
{ "code": 1610, "label": "ي" },
|
||||
{ "code": 1576, "label": "ب" },
|
||||
{ "code": 1604, "label": "ل" },
|
||||
{ "code": 1575, "label": "ا" },
|
||||
{ "code": 1578, "label": "ت" },
|
||||
{ "code": 1606, "label": "ن" },
|
||||
{ "code": 1605, "label": "م" },
|
||||
{ "code": 1603, "label": "ك" },
|
||||
{ "code": 1591, "label": "ط" }
|
||||
],
|
||||
[
|
||||
{ "code": 1584, "label": "ذ" },
|
||||
{ "code": 1569, "label": "ء" },
|
||||
{ "code": 65157, "label": "ﺅ" },
|
||||
{ "code": 1585, "label": "ر" },
|
||||
{ "code": 1609, "label": "ى" },
|
||||
{ "code": 1577, "label": "ة" },
|
||||
{ "code": 1608, "label": "و" },
|
||||
{ "code": 1586, "label": "ز" },
|
||||
{ "code": 1592, "label": "ظ" },
|
||||
{ "code": 1583, "label": "د" }
|
||||
]
|
||||
]
|
||||
}
|
||||
40
app/src/main/assets/ime/text/characters/canadian_french.json
Normal file
40
app/src/main/assets/ime/text/characters/canadian_french.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "canadian_french",
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 113, "label": "q" },
|
||||
{ "code": 119, "label": "w" },
|
||||
{ "code": 101, "label": "e" },
|
||||
{ "code": 114, "label": "r" },
|
||||
{ "code": 116, "label": "t" },
|
||||
{ "code": 121, "label": "y" },
|
||||
{ "code": 117, "label": "u" },
|
||||
{ "code": 105, "label": "i" },
|
||||
{ "code": 111, "label": "o" },
|
||||
{ "code": 112, "label": "p" },
|
||||
{ "code": 232, "label": "è" }
|
||||
], [
|
||||
{ "code": 97, "label": "a" },
|
||||
{ "code": 115, "label": "s" },
|
||||
{ "code": 100, "label": "d" },
|
||||
{ "code": 102, "label": "f" },
|
||||
{ "code": 103, "label": "g" },
|
||||
{ "code": 104, "label": "h" },
|
||||
{ "code": 106, "label": "j" },
|
||||
{ "code": 107, "label": "k" },
|
||||
{ "code": 108, "label": "l" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 224, "label": "à" }
|
||||
], [
|
||||
{ "code": 122, "label": "z" },
|
||||
{ "code": 120, "label": "x" },
|
||||
{ "code": 99, "label": "c" },
|
||||
{ "code": 118, "label": "v" },
|
||||
{ "code": 98, "label": "b" },
|
||||
{ "code": 110, "label": "n" },
|
||||
{ "code": 109, "label": "m" }
|
||||
]
|
||||
]
|
||||
}
|
||||
40
app/src/main/assets/ime/text/characters/colemak.json
Normal file
40
app/src/main/assets/ime/text/characters/colemak.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "colemak",
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 113, "label": "q" },
|
||||
{ "code": 119, "label": "w" },
|
||||
{ "code": 102, "label": "f" },
|
||||
{ "code": 112, "label": "p" },
|
||||
{ "code": 103, "label": "g" },
|
||||
{ "code": 106, "label": "j" },
|
||||
{ "code": 108, "label": "l" },
|
||||
{ "code": 117, "label": "u" },
|
||||
{ "code": 121, "label": "y" },
|
||||
{ "code": 59, "label": ";", "popup": [
|
||||
{ "code": 58, "label": ":" }
|
||||
] }
|
||||
], [
|
||||
{ "code": 97, "label": "a" },
|
||||
{ "code": 114, "label": "r" },
|
||||
{ "code": 115, "label": "s" },
|
||||
{ "code": 116, "label": "t" },
|
||||
{ "code": 100, "label": "d" },
|
||||
{ "code": 104, "label": "h" },
|
||||
{ "code": 110, "label": "n" },
|
||||
{ "code": 101, "label": "e" },
|
||||
{ "code": 105, "label": "i" },
|
||||
{ "code": 111, "label": "o" }
|
||||
], [
|
||||
{ "code": 122, "label": "z" },
|
||||
{ "code": 120, "label": "x" },
|
||||
{ "code": 99, "label": "c" },
|
||||
{ "code": 118, "label": "v" },
|
||||
{ "code": 98, "label": "b" },
|
||||
{ "code": 107, "label": "k" },
|
||||
{ "code": 109, "label": "m" }
|
||||
]
|
||||
]
|
||||
}
|
||||
49
app/src/main/assets/ime/text/characters/dvorak.json
Normal file
49
app/src/main/assets/ime/text/characters/dvorak.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "dvorak",
|
||||
"direction": "ltr",
|
||||
"modifier": "dvorak",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 64, "label": "@", "variation": "email_address" },
|
||||
{ "code": 39, "label": "'", "variation": "normal", "popup": [
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 34, "label": "\"" }
|
||||
] },
|
||||
{ "code": 47, "label": "/", "variation": "uri" },
|
||||
{ "code": 44, "label": ",", "popup": [
|
||||
{ "code": 60, "label": "<" },
|
||||
{ "code": 63, "label": "?" }
|
||||
] },
|
||||
{ "code": 46, "label": ".", "popup": [
|
||||
{ "code": 62, "label": ">" }
|
||||
] },
|
||||
{ "code": 112, "label": "p" },
|
||||
{ "code": 121, "label": "y" },
|
||||
{ "code": 102, "label": "f" },
|
||||
{ "code": 103, "label": "g" },
|
||||
{ "code": 99, "label": "c" },
|
||||
{ "code": 114, "label": "r" },
|
||||
{ "code": 108, "label": "l" }
|
||||
], [
|
||||
{ "code": 97, "label": "a" },
|
||||
{ "code": 111, "label": "o" },
|
||||
{ "code": 101, "label": "e" },
|
||||
{ "code": 117, "label": "u" },
|
||||
{ "code": 105, "label": "i" },
|
||||
{ "code": 100, "label": "d" },
|
||||
{ "code": 104, "label": "h" },
|
||||
{ "code": 116, "label": "t" },
|
||||
{ "code": 110, "label": "n" },
|
||||
{ "code": 115, "label": "s" }
|
||||
], [
|
||||
{ "code": 106, "label": "j" },
|
||||
{ "code": 107, "label": "k" },
|
||||
{ "code": 120, "label": "x" },
|
||||
{ "code": 98, "label": "b" },
|
||||
{ "code": 109, "label": "m" },
|
||||
{ "code": 119, "label": "w" },
|
||||
{ "code": 118, "label": "v" }
|
||||
]
|
||||
]
|
||||
}
|
||||
38
app/src/main/assets/ime/text/characters/esperanto.json
Normal file
38
app/src/main/assets/ime/text/characters/esperanto.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "esperanto",
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 349, "label": "ŝ" },
|
||||
{ "code": 285, "label": "ĝ" },
|
||||
{ "code": 101, "label": "e" },
|
||||
{ "code": 114, "label": "r" },
|
||||
{ "code": 116, "label": "t" },
|
||||
{ "code": 365, "label": "ŭ" },
|
||||
{ "code": 117, "label": "u" },
|
||||
{ "code": 105, "label": "i" },
|
||||
{ "code": 111, "label": "o" },
|
||||
{ "code": 112, "label": "p" }
|
||||
], [
|
||||
{ "code": 97, "label": "a" },
|
||||
{ "code": 115, "label": "s" },
|
||||
{ "code": 100, "label": "d" },
|
||||
{ "code": 102, "label": "f" },
|
||||
{ "code": 103, "label": "g" },
|
||||
{ "code": 104, "label": "h" },
|
||||
{ "code": 106, "label": "j" },
|
||||
{ "code": 107, "label": "k" },
|
||||
{ "code": 108, "label": "l" },
|
||||
{ "code": 309, "label": "ĵ" }
|
||||
], [
|
||||
{ "code": 122, "label": "z" },
|
||||
{ "code": 265, "label": "ĉ" },
|
||||
{ "code": 99, "label": "c" },
|
||||
{ "code": 118, "label": "v" },
|
||||
{ "code": 98, "label": "b" },
|
||||
{ "code": 110, "label": "n" },
|
||||
{ "code": 109, "label": "m" }
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "esperanto_with_hx",
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 349, "label": "ŝ" },
|
||||
{ "code": 285, "label": "ĝ" },
|
||||
{ "code": 101, "label": "e" },
|
||||
{ "code": 114, "label": "r" },
|
||||
{ "code": 116, "label": "t" },
|
||||
{ "code": 365, "label": "ŭ" },
|
||||
{ "code": 117, "label": "u" },
|
||||
{ "code": 105, "label": "i" },
|
||||
{ "code": 111, "label": "o" },
|
||||
{ "code": 112, "label": "p" }
|
||||
], [
|
||||
{ "code": 97, "label": "a" },
|
||||
{ "code": 115, "label": "s" },
|
||||
{ "code": 100, "label": "d" },
|
||||
{ "code": 102, "label": "f" },
|
||||
{ "code": 103, "label": "g" },
|
||||
{ "code": 104, "label": "h" },
|
||||
{ "code": 106, "label": "j" },
|
||||
{ "code": 107, "label": "k" },
|
||||
{ "code": 108, "label": "l" },
|
||||
{ "code": 309, "label": "ĵ" }
|
||||
], [
|
||||
{ "code": 122, "label": "z" },
|
||||
{ "code": 265, "label": "ĉ" },
|
||||
{ "code": 99, "label": "c" },
|
||||
{ "code": 118, "label": "v" },
|
||||
{ "code": 98, "label": "b" },
|
||||
{ "code": 110, "label": "n" },
|
||||
{ "code": 109, "label": "m" },
|
||||
{ "code": 293, "label": "ĥ" }
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"ض": [
|
||||
{ "code": 1633, "label": "١" }
|
||||
],
|
||||
"ص": [
|
||||
{ "code": 1634, "label": "٢" }
|
||||
],
|
||||
"ث": [
|
||||
{ "code": 1635, "label": "٣" }
|
||||
],
|
||||
"ق": [
|
||||
{ "code": 1704, "label": "ڨ"},
|
||||
{ "code": 1636, "label": "٤" }
|
||||
],
|
||||
"ف": [
|
||||
{ "code": 1701, "label": "ڥ" },
|
||||
{ "code": 1700, "label": "ڤ" },
|
||||
{ "code": 1698, "label": "ڢ" },
|
||||
{ "code": 1637, "label": "٥" }
|
||||
],
|
||||
"غ": [
|
||||
{ "code": 1638, "label": "٦" }
|
||||
],
|
||||
"ع": [
|
||||
{ "code": 1639, "label": "٧" }
|
||||
],
|
||||
"ه": [
|
||||
{ "code": 1726, "label": "ھ" },
|
||||
{ "code": 1640, "label": "٨" }
|
||||
],
|
||||
"خ": [
|
||||
{ "code": 1641, "label": "٩" }
|
||||
],
|
||||
"ح": [
|
||||
{ "code": 1632, "label": "٠" }
|
||||
],
|
||||
"ج": [
|
||||
{ "code": 1670, "label": "چ" }
|
||||
],
|
||||
"ش": [
|
||||
{ "code": 1692, "label": "ڜ" }
|
||||
],
|
||||
"ي": [
|
||||
{ "code": 1574, "label": "ئ" },
|
||||
{ "code": 1609, "label": "ى" }
|
||||
],
|
||||
"ب": [
|
||||
{ "code": 1662, "label": "پ" }
|
||||
],
|
||||
"ل": [
|
||||
{ "code": 65275, "label": "لا" },
|
||||
{ "code": 65273, "label": "لإ" },
|
||||
{ "code": 65271, "label": "لأ" },
|
||||
{ "code": 65269, "label": "لآ" }
|
||||
],
|
||||
"ا": [
|
||||
{ "code": 1570, "label": "آ" },
|
||||
{ "code": 1569, "label": "ء" },
|
||||
{ "code": 1571, "label": "أ" },
|
||||
{ "code": 1573, "label": "إ" },
|
||||
{ "code": 1649, "label": "ٱ" }
|
||||
],
|
||||
"ك": [
|
||||
{ "code": 1705, "label": "ک"},
|
||||
{ "code": 1711, "label": "گ" }
|
||||
],
|
||||
"ى": [
|
||||
{ "code": 1574, "label": "ئ" }
|
||||
],
|
||||
"ز": [
|
||||
{ "code": 1688, "label": "ژ" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 1611, "label": "ً" },
|
||||
{ "code": 1622, "label": "ٖ" },
|
||||
{ "code": 1648, "label": "ٰ" },
|
||||
{ "code": 1619, "label": "ٓ" },
|
||||
{ "code": 1615, "label": "ُ" },
|
||||
{ "code": 1616, "label": "ِ" },
|
||||
{ "code": 1614, "label": "َ" },
|
||||
{ "code": 1600, "label": "ـ" },
|
||||
{ "code": 1621, "label": "ٕ" },
|
||||
{ "code": 1620, "label": "ٔ" },
|
||||
{ "code": 1617, "label": "ّ" },
|
||||
{ "code": 1612, "label": "ٌ" },
|
||||
{ "code": 1613, "label": "ٍ" },
|
||||
{ "code": 1618, "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" }
|
||||
]
|
||||
}
|
||||
@@ -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,57 @@
|
||||
{ "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": "ù" }
|
||||
],
|
||||
"ŝ": [
|
||||
{ "code": 113, "label": "q" }
|
||||
],
|
||||
"ĝ": [
|
||||
{ "code": 119, "label": "w" }
|
||||
],
|
||||
"ĉ": [
|
||||
{ "code": 120, "label": "x" }
|
||||
],
|
||||
"ŭ": [
|
||||
{ "code": 121, "label": "y" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
@@ -65,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" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"c": [
|
||||
{ "code": 265, "label": "ĉ" }
|
||||
],
|
||||
"g": [
|
||||
{ "code": 285, "label": "ĝ" }
|
||||
],
|
||||
"h": [
|
||||
{ "code": 293, "label": "ĥ" }
|
||||
],
|
||||
"j": [
|
||||
{ "code": 309, "label": "ĵ" }
|
||||
],
|
||||
"s": [
|
||||
{ "code": 349, "label": "ŝ" }
|
||||
],
|
||||
"u": [
|
||||
{ "code": 365, "label": "ŭ" }
|
||||
],
|
||||
"q": [
|
||||
{ "code": 349, "label": "ŝ" }
|
||||
],
|
||||
"w": [
|
||||
{ "code": 285, "label": "ĝ" }
|
||||
],
|
||||
"x": [
|
||||
{ "code": 265, "label": "ĉ" }
|
||||
],
|
||||
"y": [
|
||||
{ "code": 365, "label": "ŭ" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
{ "code": 34, "label": "\"" },
|
||||
{ "code": 45, "label": "-" },
|
||||
{ "code": 58, "label": ":" },
|
||||
{ "code": 39, "label": "'" },
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" },
|
||||
{ "code": 61, "label": "=" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "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" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"a": [
|
||||
{ "code": 225, "label": "á" }
|
||||
],
|
||||
"e": [
|
||||
{ "code": 233, "label": "é" }
|
||||
],
|
||||
"i": [
|
||||
{ "code": 237, "label": "í" }
|
||||
],
|
||||
"o": [
|
||||
{ "code": 243, "label": "ó" },
|
||||
{ "code": 246, "label": "ö" },
|
||||
{ "code": 337, "label": "ő" }
|
||||
],
|
||||
"ö": [
|
||||
{ "code": 337, "label": "ő" }
|
||||
],
|
||||
"u": [
|
||||
{ "code": 250, "label": "ú" },
|
||||
{ "code": 252, "label": "ü" },
|
||||
{ "code": 369, "label": "ű" }
|
||||
],
|
||||
"ü": [
|
||||
{ "code": 369, "label": "ű" }
|
||||
],
|
||||
".~normal": [
|
||||
{ "code": 44, "label": "," },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
{ "code": 34, "label": "\"" },
|
||||
{ "code": 45, "label": "-" },
|
||||
{ "code": 58, "label": ":" },
|
||||
{ "code": 39, "label": "'" },
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
],
|
||||
".~uri": [
|
||||
{ "code": -255, "label": ".com" },
|
||||
{ "code": -255, "label": ".hu" },
|
||||
{ "code": -255, "label": ".gov" },
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "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" }
|
||||
]
|
||||
}
|
||||
|
||||
41
app/src/main/assets/ime/text/characters/hungarian.json
Normal file
41
app/src/main/assets/ime/text/characters/hungarian.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"type": "characters",
|
||||
"name": "hungarian",
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 113, "label": "q" },
|
||||
{ "code": 119, "label": "w" },
|
||||
{ "code": 101, "label": "e" },
|
||||
{ "code": 114, "label": "r" },
|
||||
{ "code": 116, "label": "t" },
|
||||
{ "code": 122, "label": "z" },
|
||||
{ "code": 117, "label": "u" },
|
||||
{ "code": 105, "label": "i" },
|
||||
{ "code": 111, "label": "o" },
|
||||
{ "code": 112, "label": "p" },
|
||||
{ "code": 246, "label": "ö" }
|
||||
], [
|
||||
{ "code": 97, "label": "a" },
|
||||
{ "code": 115, "label": "s" },
|
||||
{ "code": 100, "label": "d" },
|
||||
{ "code": 102, "label": "f" },
|
||||
{ "code": 103, "label": "g" },
|
||||
{ "code": 104, "label": "h" },
|
||||
{ "code": 106, "label": "j" },
|
||||
{ "code": 107, "label": "k" },
|
||||
{ "code": 108, "label": "l" },
|
||||
{ "code": 233, "label": "é" },
|
||||
{ "code": 225, "label": "á" }
|
||||
], [
|
||||
{ "code": 121, "label": "y" },
|
||||
{ "code": 120, "label": "x" },
|
||||
{ "code": 99, "label": "c" },
|
||||
{ "code": 118, "label": "v" },
|
||||
{ "code": 98, "label": "b" },
|
||||
{ "code": 110, "label": "n" },
|
||||
{ "code": 109, "label": "m" },
|
||||
{ "code": 252, "label": "ü" }
|
||||
]
|
||||
]
|
||||
}
|
||||
32
app/src/main/assets/ime/text/characters/mod/arabic.json
Normal file
32
app/src/main/assets/ime/text/characters/mod/arabic.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"type": "characters/mod",
|
||||
"name": "arabic",
|
||||
"direction": "rtl",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": 0 },
|
||||
{ "code": -5, "label": "delete", "type": "enter_editing" }
|
||||
],
|
||||
[
|
||||
{ "code": -202, "label": "view_symbols", "type": "system_gui" },
|
||||
{ "code": 64, "label": "@", "variation": "email_address" },
|
||||
{ "code": 1548, "label": "،", "variation": "normal" },
|
||||
{ "code": 47, "label": "/", "variation": "uri" },
|
||||
{ "code": -210, "label": "language_switch", "type": "system_gui", "popup": [
|
||||
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui" },
|
||||
{ "code": -100, "label": "settings", "type": "system_gui" }
|
||||
] },
|
||||
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui", "popup": [
|
||||
{ "code": -100, "label": "settings", "type": "system_gui" }
|
||||
] },
|
||||
{ "code": 32, "label": " " },
|
||||
{ "code": 46, "label": ".", "variation": "email_address" },
|
||||
{ "code": 46, "label": ".", "variation": "normal" },
|
||||
{ "code": 46, "label": ".", "variation": "uri" },
|
||||
{ "code": 10, "label": "enter", "type": "enter_editing", "popup": [
|
||||
{ "code": -215, "label": "toggle_one_handed_mode", "type": "system_gui" },
|
||||
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui" }
|
||||
] }
|
||||
]
|
||||
]
|
||||
}
|
||||
30
app/src/main/assets/ime/text/characters/mod/dvorak.json
Normal file
30
app/src/main/assets/ime/text/characters/mod/dvorak.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"type": "characters/mod",
|
||||
"name": "dvorak",
|
||||
"direction": "ltr",
|
||||
"arrangement": [
|
||||
[
|
||||
{ "code": -1, "label": "shift", "type": "modifier" },
|
||||
{ "code": 0 },
|
||||
{ "code": -5, "label": "delete", "type": "enter_editing" }
|
||||
], [
|
||||
{ "code": -202, "label": "view_symbols", "type": "system_gui" },
|
||||
{ "code": 113, "label": "q" },
|
||||
{ "code": -210, "label": "language_switch", "type": "system_gui", "popup": [
|
||||
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui" },
|
||||
{ "code": -100, "label": "settings", "type": "system_gui" }
|
||||
] },
|
||||
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui", "popup": [
|
||||
{ "code": -100, "label": "settings", "type": "system_gui" }
|
||||
] },
|
||||
{ "code": 32, "label": " " },
|
||||
{ "code": 122, "label": "z", "variation": "email_address" },
|
||||
{ "code": 122, "label": "z", "variation": "normal" },
|
||||
{ "code": 122, "label": "z", "variation": "uri" },
|
||||
{ "code": 10, "label": "enter", "type": "enter_editing", "popup": [
|
||||
{ "code": -215, "label": "toggle_one_handed_mode", "type": "system_gui" },
|
||||
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui" }
|
||||
] }
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -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" }
|
||||
]
|
||||
]
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
{ "code": 163, "label": "£" },
|
||||
{ "code": 165, "label": "¥" }
|
||||
] },
|
||||
{ "code": 95, "label": "_", "popup": [] },
|
||||
{ "code": 37, "label": "%", "popup": [
|
||||
{ "code": 8240, "label": "‰" },
|
||||
{ "code": 8453, "label": "℅" }
|
||||
] },
|
||||
{ "code": 38, "label": "&", "popup": [] },
|
||||
{ "code": 45, "label": "-", "popup": [
|
||||
{ "code": 8212, "label": "—" },
|
||||
|
||||
@@ -54,10 +54,7 @@
|
||||
] },
|
||||
{ "code": 92, "label": "\\", "popup": [] }
|
||||
], [
|
||||
{ "code": 37, "label": "%", "popup": [
|
||||
{ "code": 8240, "label": "‰" },
|
||||
{ "code": 8453, "label": "℅" }
|
||||
] },
|
||||
{ "code": 95, "label": "_", "popup": [] },
|
||||
{ "code": 169, "label": "©", "popup": [] },
|
||||
{ "code": 174, "label": "®", "popup": [] },
|
||||
{ "code": 8482, "label": "™", "popup": [] },
|
||||
|
||||
64
app/src/main/assets/ime/theme/floris_day.json
Normal file
64
app/src/main/assets/ime/theme/floris_day.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"privateMode": {
|
||||
"bgColor": "#A000FF",
|
||||
"fgColor": "#FFFFFF"
|
||||
},
|
||||
"smartbar": {
|
||||
"bgColor": "transparent",
|
||||
"fgColor": "@window/textColor",
|
||||
"fgColorAlt": "#8A8A8A"
|
||||
},
|
||||
"smartbarButton": {
|
||||
"bgColor": "@key/bgColor",
|
||||
"fgColor": "@key/fgColor"
|
||||
}
|
||||
}
|
||||
}
|
||||
64
app/src/main/assets/ime/theme/floris_night.json
Normal file
64
app/src/main/assets/ime/theme/floris_night.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"privateMode": {
|
||||
"bgColor": "#A000FF",
|
||||
"fgColor": "#FFFFFF"
|
||||
},
|
||||
"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,401 @@
|
||||
/*
|
||||
* 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 timber.log.Timber
|
||||
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 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) {
|
||||
Timber.e(
|
||||
"install($context): Can't install crash handler with a null Context object, doing nothing!"
|
||||
)
|
||||
return false
|
||||
}
|
||||
val oldHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
if (oldHandler is UncaughtExceptionHandler) {
|
||||
Timber.i("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
|
||||
)
|
||||
)
|
||||
Timber.i(
|
||||
"install($context): Successfully installed crash handler for this application!"
|
||||
)
|
||||
} catch (e: SecurityException) {
|
||||
Timber.e(
|
||||
"install($context): Failed to install crash handler, probably due to missing runtime permission 'setDefaultUncaughtExceptionHandler':\n$e"
|
||||
)
|
||||
return false
|
||||
} catch (e: Exception) {
|
||||
Timber.e(
|
||||
"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)
|
||||
}
|
||||
Timber.i(
|
||||
"install($context): Successfully created crash handler notification channel!"
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(
|
||||
"install($context): Failed to create crash handler notification channel due to an unspecified error:\n$e"
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Timber.e(
|
||||
"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()
|
||||
Timber.i("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?) {
|
||||
Timber.e("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)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.BuildConfig
|
||||
import dev.patrickgold.florisboard.crashutility.CrashUtility
|
||||
import timber.log.Timber
|
||||
|
||||
class FlorisApplication : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
CrashUtility.install(this)
|
||||
if (BuildConfig.DEBUG)
|
||||
Timber.plant(Timber.DebugTree())
|
||||
}
|
||||
}
|
||||
@@ -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,22 @@ 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.ime.text.keyboard.KeyboardMode
|
||||
import dev.patrickgold.florisboard.settings.SettingsMainActivity
|
||||
import dev.patrickgold.florisboard.util.*
|
||||
import timber.log.Timber
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
* Variable which holds the current [FlorisBoard] instance. To get this instance from another
|
||||
@@ -53,21 +58,29 @@ 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
|
||||
private var isNumberRowVisible: Boolean = false
|
||||
|
||||
val textInputManager: TextInputManager
|
||||
val mediaInputManager: MediaInputManager
|
||||
@@ -81,14 +94,22 @@ class FlorisBoard : InputMethodService() {
|
||||
|
||||
companion object {
|
||||
private const val IME_ID: String = "dev.patrickgold.florisboard/.ime.core.FlorisBoard"
|
||||
private const val IME_ID_DEBUG: String = "dev.patrickgold.florisboard.debug/dev.patrickgold.florisboard.ime.core.FlorisBoard"
|
||||
|
||||
fun checkIfImeIsEnabled(context: Context): Boolean {
|
||||
val activeImeIds = Settings.Secure.getString(
|
||||
context.contentResolver,
|
||||
Settings.Secure.ENABLED_INPUT_METHODS
|
||||
)
|
||||
if (BuildConfig.DEBUG) Log.i(FlorisBoard::class.simpleName, "List of active IMEs: $activeImeIds")
|
||||
return activeImeIds.split(":").contains(IME_ID)
|
||||
Timber.i("List of active IMEs: $activeImeIds")
|
||||
return when {
|
||||
BuildConfig.DEBUG -> {
|
||||
activeImeIds.split(":").contains(IME_ID_DEBUG)
|
||||
}
|
||||
else -> {
|
||||
activeImeIds.split(":").contains(IME_ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun checkIfImeIsSelected(context: Context): Boolean {
|
||||
@@ -96,14 +117,33 @@ class FlorisBoard : InputMethodService() {
|
||||
context.contentResolver,
|
||||
Settings.Secure.DEFAULT_INPUT_METHOD
|
||||
)
|
||||
if (BuildConfig.DEBUG) Log.i(FlorisBoard::class.simpleName, "Selected IME: $selectedImeId")
|
||||
return selectedImeId == IME_ID
|
||||
Timber.i("Selected IME: $selectedImeId")
|
||||
return when {
|
||||
BuildConfig.DEBUG -> {
|
||||
selectedImeId == IME_ID_DEBUG
|
||||
}
|
||||
else -> {
|
||||
selectedImeId == IME_ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
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 +165,222 @@ class FlorisBoard : InputMethodService() {
|
||||
.build()
|
||||
)
|
||||
}
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onCreate()")
|
||||
Timber.i("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)
|
||||
isNumberRowVisible = prefs.keyboard.numberRow
|
||||
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()")
|
||||
Timber.i("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)")
|
||||
Timber.i("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()")
|
||||
Timber.i("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) {
|
||||
Timber.i("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)
|
||||
Timber.i("onStartInputView($info, $restarting)")
|
||||
Timber.i("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)
|
||||
Timber.i( "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() {
|
||||
Timber.i("onFinishInput()")
|
||||
|
||||
super.onFinishInput()
|
||||
currentInputConnection?.requestCursorUpdates(0)
|
||||
}
|
||||
|
||||
override fun onWindowShown() {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onWindowShown()")
|
||||
Timber.i("onWindowShown()")
|
||||
|
||||
prefs.sync()
|
||||
updateThemeIfNecessary()
|
||||
val newIsNumberRowVisible = prefs.keyboard.numberRow
|
||||
if (isNumberRowVisible != newIsNumberRowVisible) {
|
||||
textInputManager.layoutManager.clearLayoutCache(KeyboardMode.CHARACTERS)
|
||||
isNumberRowVisible = newIsNumberRowVisible
|
||||
}
|
||||
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()")
|
||||
Timber.i("onWindowHidden()")
|
||||
|
||||
super.onWindowHidden()
|
||||
textInputManager.onWindowHidden()
|
||||
mediaInputManager.onWindowHidden()
|
||||
eventListeners.toList().forEach { it?.get()?.onWindowHidden() }
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
Timber.i("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
|
||||
) {
|
||||
Timber.i("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)
|
||||
ColorStateList.valueOf(prefs.theme.oneHandedButtonFgColor).also {
|
||||
inputView?.oneHandedCtrlMoveStart?.imageTintList = it
|
||||
inputView?.oneHandedCtrlMoveEnd?.imageTintList = it
|
||||
inputView?.oneHandedCtrlCloseStart?.imageTintList = it
|
||||
inputView?.oneHandedCtrlCloseEnd?.imageTintList = it
|
||||
}
|
||||
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 +396,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 +406,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 +414,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 +438,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 +454,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 +486,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)
|
||||
@@ -422,39 +515,37 @@ class FlorisBoard : InputMethodService() {
|
||||
}
|
||||
|
||||
private fun initializeOneHandedEnvironment() {
|
||||
inputView?.findViewById<ImageButton>(R.id.one_handed_ctrl_move_start)
|
||||
?.setOnClickListener { v -> onOneHandedPanelButtonClick(v) }
|
||||
inputView?.findViewById<ImageButton>(R.id.one_handed_ctrl_move_end)
|
||||
?.setOnClickListener { v -> onOneHandedPanelButtonClick(v) }
|
||||
inputView?.findViewById<ImageButton>(R.id.one_handed_ctrl_close_start)
|
||||
?.setOnClickListener { v -> onOneHandedPanelButtonClick(v) }
|
||||
inputView?.findViewById<ImageButton>(R.id.one_handed_ctrl_close_end)
|
||||
?.setOnClickListener { v -> onOneHandedPanelButtonClick(v) }
|
||||
{ v:View -> onOneHandedPanelButtonClick(v) }.also {
|
||||
inputView?.oneHandedCtrlMoveStart?.setOnClickListener(it)
|
||||
inputView?.oneHandedCtrlMoveEnd?.setOnClickListener(it)
|
||||
inputView?.oneHandedCtrlCloseStart?.setOnClickListener(it)
|
||||
inputView?.oneHandedCtrlCloseEnd?.setOnClickListener(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onOneHandedPanelButtonClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.one_handed_ctrl_move_start -> {
|
||||
prefs.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 +556,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 +577,54 @@ 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.
|
||||
*
|
||||
* TODO: implement this function with a proper iterator
|
||||
*
|
||||
* @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,36 @@
|
||||
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.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ViewFlipper
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyVariation
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
import dev.patrickgold.florisboard.util.ViewLayoutUtils
|
||||
import timber.log.Timber
|
||||
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: Float = resources.getDimension(R.dimen.inputView_baseHeight)
|
||||
private set
|
||||
var desiredSmartbarHeight: Float = resources.getDimension(R.dimen.smartbar_baseHeight)
|
||||
private set
|
||||
var desiredTextKeyboardViewHeight: Float = resources.getDimension(R.dimen.textKeyboardView_baseHeight)
|
||||
private set
|
||||
var desiredMediaKeyboardViewHeight: Float = resources.getDimension(R.dimen.mediaKeyboardView_baseHeight)
|
||||
private set
|
||||
|
||||
var mainViewFlipper: ViewFlipper? = null
|
||||
private set
|
||||
@@ -38,20 +54,120 @@ class InputView : FrameLayout {
|
||||
private set
|
||||
var oneHandedCtrlPanelEnd: LinearLayout? = null
|
||||
private set
|
||||
var oneHandedCtrlMoveStart: ImageButton? = null
|
||||
private set
|
||||
var oneHandedCtrlMoveEnd: ImageButton? = null
|
||||
private set
|
||||
var oneHandedCtrlCloseStart: ImageButton? = null
|
||||
private set
|
||||
var oneHandedCtrlCloseEnd: ImageButton? = null
|
||||
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)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyleAttr
|
||||
)
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onAttachedToWindow()")
|
||||
Timber.i("onAttachedToWindow()")
|
||||
|
||||
super.onAttachedToWindow()
|
||||
|
||||
mainViewFlipper = findViewById(R.id.main_view_flipper)
|
||||
oneHandedCtrlPanelStart = findViewById(R.id.one_handed_ctrl_panel_start)
|
||||
oneHandedCtrlPanelEnd = findViewById(R.id.one_handed_ctrl_panel_end)
|
||||
oneHandedCtrlMoveStart = findViewById(R.id.one_handed_ctrl_move_start)
|
||||
oneHandedCtrlMoveEnd = findViewById(R.id.one_handed_ctrl_move_end)
|
||||
oneHandedCtrlCloseStart = findViewById(R.id.one_handed_ctrl_close_start)
|
||||
oneHandedCtrlCloseEnd = findViewById(R.id.one_handed_ctrl_close_end)
|
||||
|
||||
florisboard.registerInputView(this)
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val heightFactor = when (resources.configuration.orientation) {
|
||||
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
|
||||
"custom" -> prefs.keyboard.heightFactorCustom.toFloat() / 100.0f
|
||||
else -> 1.00f
|
||||
}
|
||||
var baseHeight = calcInputViewHeight() * heightFactor
|
||||
var baseSmartbarHeight = 0.16129f * baseHeight
|
||||
var baseTextInputHeight = baseHeight - baseSmartbarHeight
|
||||
val tim = florisboard.textInputManager
|
||||
val shouldGiveAdditionalSpace = prefs.keyboard.numberRow &&
|
||||
!(tim.getActiveKeyboardMode() == KeyboardMode.NUMERIC ||
|
||||
tim.getActiveKeyboardMode() == KeyboardMode.PHONE ||
|
||||
tim.getActiveKeyboardMode() == KeyboardMode.PHONE2)
|
||||
if (shouldGiveAdditionalSpace) {
|
||||
val additionalHeight = desiredTextKeyboardViewHeight * 0.18f
|
||||
baseHeight += additionalHeight
|
||||
baseTextInputHeight += additionalHeight
|
||||
}
|
||||
val smartbarDisabled = !prefs.smartbar.enabled ||
|
||||
tim.keyVariation == KeyVariation.PASSWORD && prefs.keyboard.numberRow ||
|
||||
tim.getActiveKeyboardMode() == KeyboardMode.NUMERIC ||
|
||||
tim.getActiveKeyboardMode() == KeyboardMode.PHONE ||
|
||||
tim.getActiveKeyboardMode() == KeyboardMode.PHONE2
|
||||
if (smartbarDisabled) {
|
||||
baseHeight = baseTextInputHeight
|
||||
baseSmartbarHeight = 0.0f
|
||||
}
|
||||
desiredInputViewHeight = baseHeight
|
||||
desiredSmartbarHeight = baseSmartbarHeight
|
||||
desiredTextKeyboardViewHeight = baseTextInputHeight
|
||||
desiredMediaKeyboardViewHeight = baseHeight
|
||||
// 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).
|
||||
baseHeight += ViewLayoutUtils.convertDpToPixel(
|
||||
florisboard.prefs.keyboard.bottomOffset.toFloat(),
|
||||
context
|
||||
)
|
||||
|
||||
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(baseHeight.roundToInt(), 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,10 @@ 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.ime.text.key.KeyHintMode
|
||||
import dev.patrickgold.florisboard.util.VersionName
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
@@ -37,9 +41,12 @@ 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 smartbar = Smartbar(this)
|
||||
val suggestion = Suggestion(this)
|
||||
val theme = Theme(this)
|
||||
|
||||
@@ -110,15 +117,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 +148,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
|
||||
|
||||
@@ -147,6 +167,7 @@ class PrefHelper(
|
||||
companion object {
|
||||
const val SETTINGS_THEME = "advanced__settings_theme"
|
||||
const val SHOW_APP_ICON = "advanced__show_app_icon"
|
||||
const val FORCE_PRIVATE_MODE = "advanced__force_private_mode"
|
||||
}
|
||||
|
||||
var settingsTheme: String = ""
|
||||
@@ -155,6 +176,9 @@ class PrefHelper(
|
||||
var showAppIcon: Boolean = false
|
||||
get() = prefHelper.getPref(SHOW_APP_ICON, true)
|
||||
private set
|
||||
var forcePrivateMode: Boolean
|
||||
get() = prefHelper.getPref(FORCE_PRIVATE_MODE, false)
|
||||
set(v) = prefHelper.setPref(FORCE_PRIVATE_MODE, v)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,37 +186,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 +307,121 @@ 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 FONT_SIZE_MULTIPLIER_PORTRAIT = "keyboard__font_size_multiplier_portrait"
|
||||
const val FONT_SIZE_MULTIPLIER_LANDSCAPE = "keyboard__font_size_multiplier_landscape"
|
||||
const val HEIGHT_FACTOR = "keyboard__height_factor"
|
||||
const val HEIGHT_FACTOR_CUSTOM = "keyboard__height_factor_custom"
|
||||
const val HINTED_NUMBER_ROW_MODE = "keyboard__hinted_number_row_mode"
|
||||
const val HINTED_SYMBOLS_MODE = "keyboard__hinted_symbols_mode"
|
||||
const val LONG_PRESS_DELAY = "keyboard__long_press_delay"
|
||||
const val NUMBER_ROW = "keyboard__number_row"
|
||||
const val ONE_HANDED_MODE = "keyboard__one_handed_mode"
|
||||
const val POPUP_ENABLED = "keyboard__popup_enabled"
|
||||
const val SOUND_ENABLED = "keyboard__sound_enabled"
|
||||
const val SOUND_VOLUME = "keyboard__sound_volume"
|
||||
const val 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 fontSizeMultiplierPortrait: Int
|
||||
get() = prefHelper.getPref(FONT_SIZE_MULTIPLIER_PORTRAIT, 100)
|
||||
set(v) = prefHelper.setPref(FONT_SIZE_MULTIPLIER_PORTRAIT, v)
|
||||
var fontSizeMultiplierLandscape: Int
|
||||
get() = prefHelper.getPref(FONT_SIZE_MULTIPLIER_LANDSCAPE, 100)
|
||||
set(v) = prefHelper.setPref(FONT_SIZE_MULTIPLIER_LANDSCAPE, v)
|
||||
var heightFactor: String = ""
|
||||
get() = prefHelper.getPref(HEIGHT_FACTOR, "normal")
|
||||
private set
|
||||
var heightFactorCustom: Int
|
||||
get() = prefHelper.getPref(HEIGHT_FACTOR_CUSTOM, 100)
|
||||
set(v) = prefHelper.setPref(HEIGHT_FACTOR_CUSTOM, v)
|
||||
var hintedNumberRowMode: KeyHintMode
|
||||
get() = KeyHintMode.fromString(prefHelper.getPref(HINTED_NUMBER_ROW_MODE, KeyHintMode.ENABLED_ACCENT_PRIORITY.toString()))
|
||||
set(v) = prefHelper.setPref(HINTED_NUMBER_ROW_MODE, v)
|
||||
var hintedSymbolsMode: KeyHintMode
|
||||
get() = KeyHintMode.fromString(prefHelper.getPref(HINTED_SYMBOLS_MODE, KeyHintMode.ENABLED_ACCENT_PRIORITY.toString()))
|
||||
set(v) = prefHelper.setPref(HINTED_SYMBOLS_MODE, v)
|
||||
var longPressDelay: Int = 0
|
||||
get() = prefHelper.getPref(LONG_PRESS_DELAY, 300)
|
||||
private set
|
||||
var numberRow: Boolean
|
||||
get() = prefHelper.getPref(NUMBER_ROW, false)
|
||||
set(v) = prefHelper.setPref(NUMBER_ROW, v)
|
||||
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 Smartbar preferences.
|
||||
*/
|
||||
class Smartbar(private val prefHelper: PrefHelper) {
|
||||
companion object {
|
||||
const val ENABLED = "smartbar__enabled"
|
||||
}
|
||||
|
||||
var enabled: Boolean
|
||||
get() = prefHelper.getPref(ENABLED, true)
|
||||
set(v) = prefHelper.setPref(ENABLED, 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 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 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 +429,127 @@ 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 PRIVATE_MODE_BG_COLOR = "theme__privateMode_bgColor"
|
||||
const val PRIVATE_MODE_FG_COLOR = "theme__privateMode_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 privateModeBgColor: Int
|
||||
get() = prefHelper.getPref(PRIVATE_MODE_BG_COLOR, 0)
|
||||
set(v) = prefHelper.setPref(PRIVATE_MODE_BG_COLOR, v)
|
||||
var privateModeFgColor: Int
|
||||
get() = prefHelper.getPref(PRIVATE_MODE_FG_COLOR, 0)
|
||||
set(v) = prefHelper.setPref(PRIVATE_MODE_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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,16 +87,10 @@ data class Subtype(
|
||||
* Must be a string which also exists in [FlorisBoard.ImeConfig.characterLayouts]. If the value is
|
||||
* not included within this list, no layout will be shown to the user if the user selects the
|
||||
* predefined layout value.
|
||||
* @property isAsciiCapable Legacy attribute for Android's InputMethodSubtype. Currently no real
|
||||
* use within this project.
|
||||
* @property isEmojiCapable Legacy attribute for Android's InputMethodSubtype. Currently no real
|
||||
* use within this project.
|
||||
*/
|
||||
data class DefaultSubtype(
|
||||
var id: Int,
|
||||
@Json(name = "languageTag")
|
||||
var locale: Locale,
|
||||
var preferredLayout: String,
|
||||
var isAsciiCapable: Boolean,
|
||||
var isEmojiCapable: Boolean
|
||||
var preferredLayout: String
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package dev.patrickgold.florisboard.ime.media
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Handler
|
||||
import android.util.Log
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
@@ -24,6 +25,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
|
||||
@@ -33,7 +35,10 @@ import dev.patrickgold.florisboard.ime.media.emoticon.EmoticonKeyboardView
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyData
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyType
|
||||
import dev.patrickgold.florisboard.util.cancelAll
|
||||
import dev.patrickgold.florisboard.util.postAtScheduledRate
|
||||
import kotlinx.coroutines.*
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@@ -50,10 +55,12 @@ 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
|
||||
private var osTimer: Timer? = null
|
||||
private var repeatedKeyPressHandler: Handler? = null
|
||||
private var tabLayout: TabLayout? = null
|
||||
private val tabViews = EnumMap<Tab, LinearLayout>(Tab::class.java)
|
||||
|
||||
@@ -71,6 +78,15 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
florisboard.addEventListener(this)
|
||||
}
|
||||
|
||||
override fun onCreateInputView() {
|
||||
super.onCreateInputView()
|
||||
repeatedKeyPressHandler = Handler(florisboard.context.mainLooper)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new input view has been registered. Used to initialize all media-relevant
|
||||
* views and layouts.
|
||||
@@ -78,7 +94,7 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onRegisterInputView(inputView: InputView) {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onRegisterInputView(inputView)")
|
||||
Timber.i("onRegisterInputView(inputView)")
|
||||
|
||||
launch(Dispatchers.Default) {
|
||||
mediaViewGroup = inputView.findViewById(R.id.media_input)
|
||||
@@ -104,15 +120,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))
|
||||
}
|
||||
}
|
||||
@@ -122,7 +135,7 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* Clean-up of resources and stopping all coroutines.
|
||||
*/
|
||||
override fun onDestroy() {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onDestroy()")
|
||||
Timber.i("onDestroy()")
|
||||
|
||||
cancel()
|
||||
instance = null
|
||||
@@ -147,17 +160,14 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
florisboard.keyPressVibrate()
|
||||
florisboard.keyPressSound(data)
|
||||
if (data?.code == KeyCode.DELETE && data.type == KeyType.ENTER_EDITING) {
|
||||
osTimer = Timer()
|
||||
osTimer?.scheduleAtFixedRate(object : TimerTask() {
|
||||
override fun run() {
|
||||
florisboard.textInputManager.sendKeyPress(data)
|
||||
}
|
||||
}, 500, 50)
|
||||
val delayMillis = florisboard.prefs.keyboard.longPressDelay.toLong()
|
||||
repeatedKeyPressHandler?.postAtScheduledRate(delayMillis, 25) {
|
||||
florisboard.textInputManager.sendKeyPress(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
osTimer?.cancel()
|
||||
osTimer = null
|
||||
repeatedKeyPressHandler?.cancelAll()
|
||||
if (event.actionMasked != MotionEvent.ACTION_CANCEL && data != null) {
|
||||
florisboard.textInputManager.sendKeyPress(data)
|
||||
}
|
||||
@@ -195,18 +205,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,68 @@
|
||||
/*
|
||||
* 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
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
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.0f
|
||||
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height.roundToInt(), 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import android.graphics.Paint
|
||||
import android.graphics.Typeface
|
||||
import android.util.Log
|
||||
import androidx.core.graphics.PaintCompat
|
||||
import timber.log.Timber
|
||||
import java.io.BufferedReader
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
@@ -171,13 +172,13 @@ fun parseRawEmojiSpecsFile(
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e("EmojiLayoutDataMap", "parseRawEmojiSpecsFile(): $e")
|
||||
Timber.e("parseRawEmojiSpecsFile(): $e")
|
||||
} finally {
|
||||
if (reader != null) {
|
||||
try {
|
||||
reader.close()
|
||||
} catch (e: IOException) {
|
||||
Log.e("EmojiLayoutDataMap", "parseRawEmojiSpecsFile(): $e")
|
||||
Timber.e("parseRawEmojiSpecsFile(): $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -18,15 +18,15 @@ package dev.patrickgold.florisboard.ime.popup
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.*
|
||||
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
|
||||
import dev.patrickgold.florisboard.databinding.KeyPopupExtendedViewBinding
|
||||
import dev.patrickgold.florisboard.databinding.KeyPopupViewBinding
|
||||
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyData
|
||||
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyView
|
||||
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyboardView
|
||||
@@ -34,7 +34,7 @@ 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
|
||||
import dev.patrickgold.florisboard.util.ViewLayoutUtils
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -56,9 +55,10 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
)
|
||||
private var keyPopupWidth: Int
|
||||
private var keyPopupHeight: Int
|
||||
var keyPopupTextSize: Float = keyboardView.resources.getDimension(R.dimen.key_popup_textSize)
|
||||
private var keyPopupDiffX: Int = 0
|
||||
private val popupView: LinearLayout
|
||||
private val popupViewExt: FlexboxLayout
|
||||
private val popupView: KeyPopupViewBinding
|
||||
private val popupViewExt: KeyPopupExtendedViewBinding
|
||||
private var row0count: Int = 0
|
||||
private var row1count: Int = 0
|
||||
private var window: PopupWindow
|
||||
@@ -66,25 +66,20 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
|
||||
/** Is true if the preview popup is visible to the user, else false */
|
||||
val isShowingPopup: Boolean
|
||||
get() = popupView.visibility == View.VISIBLE
|
||||
get() = popupView.root.visibility == View.VISIBLE
|
||||
/** Is true if the extended popup is visible to the user, else false */
|
||||
val isShowingExtendedPopup: Boolean
|
||||
get() = windowExt.isShowing
|
||||
|
||||
init {
|
||||
val inflater = LayoutInflater.from(keyboardView.context)
|
||||
keyPopupWidth = keyboardView.resources.getDimension(R.dimen.key_width).toInt()
|
||||
keyPopupHeight = keyboardView.resources.getDimension(R.dimen.key_height).toInt()
|
||||
popupView = View.inflate(
|
||||
keyboardView.context,
|
||||
R.layout.key_popup, null
|
||||
) as LinearLayout
|
||||
popupView.visibility = View.INVISIBLE
|
||||
popupViewExt = View.inflate(
|
||||
keyboardView.context,
|
||||
R.layout.key_popup_extended, null
|
||||
) as FlexboxLayout
|
||||
window = createPopupWindow(popupView)
|
||||
windowExt = createPopupWindow(popupViewExt)
|
||||
popupView = KeyPopupViewBinding.inflate(inflater, null, false)
|
||||
popupView.root.visibility = View.INVISIBLE
|
||||
popupViewExt = KeyPopupExtendedViewBinding.inflate(inflater, null, false)
|
||||
window = createPopupWindow(popupView.root)
|
||||
windowExt = createPopupWindow(popupViewExt.root)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,18 +97,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)
|
||||
val textSize = keyPopupTextSize
|
||||
if (keyView is KeyView) {
|
||||
when (keyView.data.popup[k].code) {
|
||||
when (keyView.dataPopupWithHint[k].code) {
|
||||
KeyCode.SETTINGS -> {
|
||||
textView.iconDrawable = getDrawable(
|
||||
keyView.context, R.drawable.ic_settings
|
||||
@@ -129,18 +120,18 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
}
|
||||
KeyCode.TOGGLE_ONE_HANDED_MODE -> {
|
||||
textView.iconDrawable = getDrawable(
|
||||
keyView.context, R.drawable.ic_keyboard_arrow_right
|
||||
keyView.context, R.drawable.ic_smartphone
|
||||
)
|
||||
}
|
||||
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) {
|
||||
@@ -156,11 +147,13 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
* @return A new [PopupWindow] already preconfigured and ready-to-go.
|
||||
*/
|
||||
private fun createPopupWindow(view: View): PopupWindow {
|
||||
return PopupWindow(keyboardView.context).apply {
|
||||
return PopupWindow(view.context).apply {
|
||||
animationStyle = 0
|
||||
contentView = view
|
||||
elevation = ViewLayoutUtils.convertDpToPixel(2.0f, view.context)
|
||||
enterTransition = null
|
||||
exitTransition = null
|
||||
isAttachedInDecor = false
|
||||
isClippingEnabled = false
|
||||
isFocusable = false
|
||||
isTouchable = false
|
||||
@@ -169,29 +162,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 +192,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) {
|
||||
@@ -214,19 +217,21 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
window.showAsDropDown(keyView, keyPopupX, keyPopupY, Gravity.NO_GRAVITY)
|
||||
}
|
||||
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
|
||||
popupView.symbol.layoutParams.height = (keyPopupHeight * 0.4f).toInt()
|
||||
popupView.symbol.setTextSize(TypedValue.COMPLEX_UNIT_PX, keyPopupTextSize)
|
||||
popupView.symbol.text = keyView.getComputedLetter()
|
||||
popupView.threedots.visibility = when {
|
||||
keyView.dataPopupWithHint.isEmpty() -> View.INVISIBLE
|
||||
else -> View.VISIBLE
|
||||
}
|
||||
} else if (keyView is EmojiKeyView) {
|
||||
popupView.findViewById<TextView>(R.id.key_popup_text)?.text = keyView.data.getCodePointsAsString()
|
||||
popupView.findViewById<ImageView>(R.id.key_popup_threedots)?.visibility = when {
|
||||
popupView.symbol.text = keyView.data.getCodePointsAsString()
|
||||
popupView.threedots.visibility = when {
|
||||
keyView.data.popup.isEmpty() -> View.INVISIBLE
|
||||
else -> View.VISIBLE
|
||||
}
|
||||
}
|
||||
popupView.visibility = View.VISIBLE
|
||||
popupView.root.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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
|
||||
}
|
||||
@@ -314,42 +318,54 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
}
|
||||
|
||||
// Build UI
|
||||
popupViewExt.removeAllViews()
|
||||
popupViewExt.root.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)
|
||||
popupViewExt.addView(
|
||||
val kk = when (keyView) {
|
||||
is KeyView -> when {
|
||||
isInitActive -> {
|
||||
hasShownFirst = true
|
||||
0
|
||||
}
|
||||
hasShownFirst -> k
|
||||
else -> k + 1
|
||||
}
|
||||
else -> k
|
||||
}
|
||||
popupViewExt.root.addView(
|
||||
createTextView(
|
||||
keyView, k, isInitActive, (row1count > 0) && (k - row1count == 0)
|
||||
keyView, kk, isInitActive, (row1count > 0) && (k - row1count == 0)
|
||||
)
|
||||
)
|
||||
if (isInitActive) {
|
||||
activeExtIndex = k
|
||||
}
|
||||
}
|
||||
popupView.findViewById<ImageView>(R.id.key_popup_threedots)?.visibility = View.INVISIBLE
|
||||
popupView.threedots.visibility = View.INVISIBLE
|
||||
|
||||
// Calculate layout params
|
||||
val extWidth = row0count * keyPopupWidth
|
||||
val extHeight = when {
|
||||
row1count > 0 -> keyView.measuredHeight * 2
|
||||
else -> keyView.measuredHeight
|
||||
}
|
||||
popupViewExt.justifyContent = if (anchorLeft) {
|
||||
row1count > 0 -> keyPopupHeight * 0.4f * 2.0f
|
||||
else -> keyPopupHeight * 0.4f
|
||||
}.toInt()
|
||||
popupViewExt.root.justifyContent = if (anchorLeft) {
|
||||
JustifyContent.FLEX_START
|
||||
} else {
|
||||
JustifyContent.FLEX_END
|
||||
}
|
||||
if (popupViewExt.layoutParams == null) {
|
||||
popupViewExt.layoutParams = ViewGroup.LayoutParams(extWidth, extHeight)
|
||||
if (popupViewExt.root.layoutParams == null) {
|
||||
popupViewExt.root.layoutParams = ViewGroup.LayoutParams(extWidth, extHeight)
|
||||
} else {
|
||||
popupViewExt.layoutParams.apply {
|
||||
popupViewExt.root.layoutParams.apply {
|
||||
width = extWidth
|
||||
height = extHeight
|
||||
}
|
||||
@@ -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,8 +458,8 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
}
|
||||
|
||||
if (keyView is KeyView) {
|
||||
for (k in keyView.data.popup.indices) {
|
||||
val view = popupViewExt.getChildAt(k)
|
||||
for (k in keyView.dataPopupWithHint.indices) {
|
||||
val view = popupViewExt.root.getChildAt(k)
|
||||
if (view != null) {
|
||||
val textView = view as KeyPopupExtendedSingleView
|
||||
textView.isActive = k == activeExtIndex
|
||||
@@ -451,7 +467,7 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
}
|
||||
} else if (keyView is EmojiKeyView) {
|
||||
for (k in keyView.data.popup.indices) {
|
||||
val view = popupViewExt.getChildAt(k)
|
||||
val view = popupViewExt.root.getChildAt(k)
|
||||
if (view != null) {
|
||||
val textView = view as KeyPopupExtendedSingleView
|
||||
textView.isActive = k == activeExtIndex
|
||||
@@ -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.root[activeExtIndex]
|
||||
if (singleView is KeyPopupExtendedSingleView) {
|
||||
keyView.dataPopupWithHint.getOrNull(singleView.adjustedIndex) ?: keyView.data
|
||||
} else {
|
||||
keyView.data
|
||||
}
|
||||
} else {
|
||||
keyView.data
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -498,7 +524,7 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
|
||||
* Hides the key preview popup as well as the extended popup.
|
||||
*/
|
||||
fun hide() {
|
||||
popupView.visibility = View.INVISIBLE
|
||||
popupView.root.visibility = View.INVISIBLE
|
||||
if (windowExt.isShowing) {
|
||||
windowExt.dismiss()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.databinding.KeyPopupViewBinding
|
||||
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 binding: KeyPopupViewBinding
|
||||
|
||||
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()
|
||||
binding = KeyPopupViewBinding.bind(this)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas?) {
|
||||
setBackgroundTintColor2(this, prefs.theme.keyPopupBgColor)
|
||||
binding.symbol.setTextColor(prefs.theme.keyPopupFgColor)
|
||||
setImageTintColor2(binding.threedots, prefs.theme.keyPopupFgColor)
|
||||
super.onDraw(canvas)
|
||||
}
|
||||
}
|
||||
@@ -18,20 +18,17 @@ 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.Toast
|
||||
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
|
||||
@@ -39,8 +36,9 @@ import dev.patrickgold.florisboard.ime.text.key.KeyVariation
|
||||
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.ime.text.smartbar.SmartbarManager
|
||||
import dev.patrickgold.florisboard.ime.text.smartbar.SmartbarView
|
||||
import kotlinx.coroutines.*
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@@ -51,41 +49,39 @@ import java.util.*
|
||||
* are separated from media-related UI. The core [FlorisBoard] will pass any event defined in
|
||||
* [FlorisBoard.EventListener] through to this class.
|
||||
*
|
||||
* TextInputManager also keeps track of the current composing word and syncs this value with the
|
||||
* Smartbar, which, depending on the mode and variation, may create candidates.
|
||||
* @see SmartbarManager.generateCandidatesFromComposing for more information.
|
||||
* TextInputManager is also the hub in the communication between the system, the active editor
|
||||
* instance and the Smartbar.
|
||||
*/
|
||||
class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
FlorisBoard.EventListener {
|
||||
FlorisBoard.EventListener, SmartbarView.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 var smartbarView: SmartbarView? = null
|
||||
|
||||
// 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 var instance: TextInputManager? = null
|
||||
@@ -99,34 +95,31 @@ 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()")
|
||||
Timber.i("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, florisboard.prefs)
|
||||
}
|
||||
}
|
||||
smartbarManager = SmartbarManager.getInstance()
|
||||
}
|
||||
|
||||
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()
|
||||
keyboardView.computedLayout = layoutManager.fetchComputedLayoutAsync(mode, florisboard.activeSubtype, florisboard.prefs).await()
|
||||
keyboardViews[mode] = keyboardView
|
||||
withContext(Dispatchers.Main) {
|
||||
textViewFlipper?.addView(keyboardView)
|
||||
@@ -137,11 +130,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)")
|
||||
Timber.i("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,91 +143,101 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun registerSmartbarView(view: SmartbarView) {
|
||||
smartbarView = view
|
||||
smartbarView?.setEventListener(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels all coroutines and cleans up.
|
||||
*/
|
||||
override fun onDestroy() {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onDestroy()")
|
||||
Timber.i("onDestroy()")
|
||||
|
||||
cancel()
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
layoutManager.onDestroy()
|
||||
smartbarManager.onDestroy()
|
||||
instance = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
KeyboardMode.NUMERIC,
|
||||
KeyboardMode.PHONE,
|
||||
KeyboardMode.PHONE2 -> false
|
||||
else -> keyVariation != KeyVariation.PASSWORD
|
||||
instance.apply {
|
||||
isComposingEnabled = when (keyboardMode) {
|
||||
KeyboardMode.NUMERIC,
|
||||
KeyboardMode.PHONE,
|
||||
KeyboardMode.PHONE2 -> false
|
||||
else -> keyVariation != KeyVariation.PASSWORD &&
|
||||
florisboard.prefs.suggestion.enabled// &&
|
||||
//!instance.inputAttributes.flagTextAutoComplete &&
|
||||
//!instance.inputAttributes.flagTextNoSuggestions
|
||||
}
|
||||
isPrivateMode = florisboard.prefs.advanced.forcePrivateMode ||
|
||||
imeOptions.flagNoPersonalizedLearning
|
||||
}
|
||||
if (!florisboard.prefs.correction.rememberCapsLockState) {
|
||||
capsLock = false
|
||||
}
|
||||
updateCapsState()
|
||||
resetComposingText()
|
||||
setActiveKeyboardMode(keyboardMode)
|
||||
smartbarManager.onStartInputView(keyboardMode, isComposingEnabled)
|
||||
smartbarView?.updateSmartbarState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle stuff when finishing to interact with a input editor.
|
||||
*/
|
||||
override fun onFinishInputView(finishingInput: Boolean) {
|
||||
smartbarManager.onFinishInputView()
|
||||
smartbarView?.updateSmartbarState()
|
||||
}
|
||||
|
||||
override fun onWindowShown() {
|
||||
keyboardViews[KeyboardMode.CHARACTERS]?.updateVisibility()
|
||||
smartbarManager.onWindowShown()
|
||||
smartbarView?.updateSmartbarState()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,212 +250,160 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets [activeKeyboardMode] and updates the [SmartbarManager.isQuickActionsVisible].
|
||||
* Sets [activeKeyboardMode] and updates the [SmartbarView.isQuickActionsVisible] state.
|
||||
*/
|
||||
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
|
||||
smartbarView?.isQuickActionsVisible = false
|
||||
smartbarView?.updateSmartbarState()
|
||||
}
|
||||
|
||||
override fun onSubtypeChanged(newSubtype: Subtype) {
|
||||
launch {
|
||||
val keyboardView = keyboardViews[KeyboardMode.CHARACTERS]
|
||||
keyboardView?.computedLayout = layoutManager.fetchComputedLayoutAsync(KeyboardMode.CHARACTERS, newSubtype).await()
|
||||
keyboardView?.computedLayout = layoutManager.fetchComputedLayoutAsync(KeyboardMode.CHARACTERS, newSubtype, florisboard.prefs).await()
|
||||
keyboardView?.updateVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* and passing this info on to the [SmartbarView] 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()
|
||||
smartbarView?.updateSmartbarState()
|
||||
}
|
||||
|
||||
override fun onPrimaryClipChanged() {
|
||||
smartbarView?.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 -> {}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSmartbarBackButtonPressed() {
|
||||
setActiveKeyboardMode(KeyboardMode.CHARACTERS)
|
||||
}
|
||||
|
||||
override fun onSmartbarPrivateModeButtonClicked() {
|
||||
Toast.makeText(florisboard.context, R.string.private_mode_dialog__title, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun onSmartbarQuickActionPressed(quickActionId: Int) {
|
||||
when (quickActionId) {
|
||||
R.id.quick_action_switch_to_editing_context -> {
|
||||
if (activeKeyboardMode == KeyboardMode.EDITING) {
|
||||
setActiveKeyboardMode(KeyboardMode.CHARACTERS)
|
||||
} else {
|
||||
setActiveKeyboardMode(KeyboardMode.EDITING)
|
||||
}
|
||||
}
|
||||
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()
|
||||
R.id.quick_action_undo -> {
|
||||
handleUndo()
|
||||
return
|
||||
}
|
||||
R.id.quick_action_redo -> {
|
||||
handleRedo()
|
||||
return
|
||||
}
|
||||
}
|
||||
smartbarView?.isQuickActionsVisible = false
|
||||
smartbarView?.updateSmartbarState()
|
||||
}
|
||||
|
||||
private fun handleUndo(){
|
||||
activeEditorInstance.performUndo()
|
||||
}
|
||||
|
||||
private fun handleRedo(){
|
||||
activeEditorInstance.performRedo()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 +431,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 +447,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 +598,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()
|
||||
smartbarView?.resetClipboardSuggestion()
|
||||
}
|
||||
KeyCode.CLIPBOARD_SELECT -> handleClipboardSelect()
|
||||
KeyCode.CLIPBOARD_SELECT_ALL -> handleClipboardSelectAll()
|
||||
KeyCode.DELETE -> {
|
||||
handleDelete()
|
||||
smartbarView?.resetClipboardSuggestion()
|
||||
}
|
||||
KeyCode.ENTER -> {
|
||||
handleEnter()
|
||||
smartbarView?.resetClipboardSuggestion()
|
||||
}
|
||||
KeyCode.LANGUAGE_SWITCH -> florisboard.switchToNextSubtype()
|
||||
KeyCode.SETTINGS -> florisboard.launchSettings()
|
||||
KeyCode.SHIFT -> handleShift()
|
||||
@@ -531,8 +640,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 +648,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 +666,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 +674,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"
|
||||
)
|
||||
Timber.e("sendKeyPress(keyData): Received unknown key: $keyData")
|
||||
}
|
||||
}
|
||||
}
|
||||
ic?.endBatchEdit()
|
||||
smartbarView?.resetClipboardSuggestion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class CapsMode {
|
||||
ALL,
|
||||
NONE,
|
||||
SENTENCES,
|
||||
WORDS;
|
||||
if (keyData.code != KeyCode.SHIFT && !capsLock) {
|
||||
updateCapsState()
|
||||
}
|
||||
smartbarView?.updateSmartbarState()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* 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.os.Handler
|
||||
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 dev.patrickgold.florisboard.util.cancelAll
|
||||
import dev.patrickgold.florisboard.util.postAtScheduledRate
|
||||
|
||||
/**
|
||||
* 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 val repeatedKeyPressHandler: Handler = Handler(context.mainLooper)
|
||||
|
||||
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 -> {
|
||||
val delayMillis = prefs.keyboard.longPressDelay.toLong()
|
||||
repeatedKeyPressHandler.postAtScheduledRate(delayMillis, 25) {
|
||||
if (isKeyPressed) {
|
||||
florisboard?.textInputManager?.sendKeyPress(data)
|
||||
} else {
|
||||
repeatedKeyPressHandler.cancelAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
isKeyPressed = false
|
||||
repeatedKeyPressHandler.cancelAll()
|
||||
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,106 @@
|
||||
/*
|
||||
* 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
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* 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).toFloat()
|
||||
val height = when (heightMode) {
|
||||
MeasureSpec.EXACTLY -> {
|
||||
// Must be this size
|
||||
heightSize
|
||||
}
|
||||
MeasureSpec.AT_MOST -> {
|
||||
// Can't be bigger than...
|
||||
(florisboard?.inputView?.desiredTextKeyboardViewHeight ?: 0.0f).coerceAtMost(heightSize)
|
||||
}
|
||||
else -> {
|
||||
// Be whatever you want
|
||||
florisboard?.inputView?.desiredTextKeyboardViewHeight ?: 0.0f
|
||||
}
|
||||
}
|
||||
|
||||
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height.roundToInt(), 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,196 @@
|
||||
/*
|
||||
* 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.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) / 4.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
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 java.util.*
|
||||
|
||||
/**
|
||||
* Enum for the key hint modes.
|
||||
*/
|
||||
enum class KeyHintMode {
|
||||
DISABLED,
|
||||
ENABLED_HINT_PRIORITY,
|
||||
ENABLED_ACCENT_PRIORITY,
|
||||
ENABLED_SMART_PRIORITY;
|
||||
|
||||
companion object {
|
||||
fun fromString(string: String): KeyHintMode {
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
@@ -25,7 +25,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 +32,16 @@ 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.cancelAll
|
||||
import dev.patrickgold.florisboard.util.postAtScheduledRate
|
||||
import dev.patrickgold.florisboard.util.postDelayed
|
||||
import dev.patrickgold.florisboard.util.setBackgroundTintColor2
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@@ -51,32 +56,49 @@ 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
|
||||
updateKeyPressedBackground()
|
||||
}
|
||||
private var osHandler: Handler? = null
|
||||
private var osTimer: Timer? = null
|
||||
private var hasTriggeredGestureMove: Boolean = false
|
||||
private val longKeyPressHandler: Handler = Handler(context.mainLooper)
|
||||
private val repeatedKeyPressHandler: Handler = Handler(context.mainLooper)
|
||||
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
|
||||
private var drawablePaddingH: Int = 0
|
||||
private var drawablePaddingV: Int = 0
|
||||
private var label: String? = null
|
||||
private var labelPaint: Paint = Paint().apply {
|
||||
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 +145,30 @@ class KeyView(
|
||||
background = getDrawable(context, R.drawable.shape_rect_rounded)
|
||||
elevation = 4.0f
|
||||
|
||||
var hintKeyData: KeyData? = null
|
||||
var hintKeyMode: KeyHintMode = KeyHintMode.DISABLED
|
||||
val hintedNumber = data.hintedNumber
|
||||
if (prefs.keyboard.hintedNumberRowMode != KeyHintMode.DISABLED && hintedNumber != null) {
|
||||
hintKeyData = hintedNumber
|
||||
hintKeyMode = prefs.keyboard.hintedNumberRowMode
|
||||
}
|
||||
val hintedSymbol = data.hintedSymbol
|
||||
if (prefs.keyboard.hintedSymbolsMode != KeyHintMode.DISABLED && hintedSymbol != null) {
|
||||
hintKeyData = hintedSymbol
|
||||
hintKeyMode = prefs.keyboard.hintedSymbolsMode
|
||||
}
|
||||
dataPopupWithHint = if (hintKeyData == null) {
|
||||
data.popup.toMutableList()
|
||||
} else {
|
||||
val popupList = data.popup.toMutableList()
|
||||
if (hintKeyMode == KeyHintMode.ENABLED_HINT_PRIORITY) {
|
||||
popupList.add(0, hintKeyData)
|
||||
} else {
|
||||
popupList.add(hintKeyData)
|
||||
}
|
||||
popupList
|
||||
}
|
||||
|
||||
updateKeyPressedBackground()
|
||||
}
|
||||
|
||||
@@ -171,31 +217,44 @@ 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
|
||||
longKeyPressHandler.cancelAll()
|
||||
repeatedKeyPressHandler.cancelAll()
|
||||
keyboardView.popupManager.hide()
|
||||
return true
|
||||
}
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
keyboardView.popupManager.show(this)
|
||||
val delayMillis = prefs.keyboard.longPressDelay.toLong()
|
||||
hasTriggeredGestureMove = false
|
||||
shouldBlockNextKeyCode = false
|
||||
florisboard?.prefs?.keyboard?.let {
|
||||
if (it.popupEnabled){
|
||||
keyboardView.popupManager.show(this)
|
||||
}
|
||||
}
|
||||
isKeyPressed = true
|
||||
florisboard?.keyPressVibrate()
|
||||
florisboard?.keyPressSound(data)
|
||||
if (data.code == KeyCode.DELETE && data.type == KeyType.ENTER_EDITING) {
|
||||
osTimer = Timer()
|
||||
osTimer?.scheduleAtFixedRate(object : TimerTask() {
|
||||
override fun run() {
|
||||
florisboard?.textInputManager?.sendKeyPress(data)
|
||||
if (!isKeyPressed) {
|
||||
osTimer?.cancel()
|
||||
osTimer = null
|
||||
when (data.code) {
|
||||
KeyCode.ARROW_DOWN,
|
||||
KeyCode.ARROW_LEFT,
|
||||
KeyCode.ARROW_RIGHT,
|
||||
KeyCode.ARROW_UP,
|
||||
KeyCode.DELETE -> {
|
||||
repeatedKeyPressHandler.postAtScheduledRate(delayMillis, 25) {
|
||||
if (isKeyPressed) {
|
||||
florisboard?.textInputManager?.sendKeyPress(data)
|
||||
} else {
|
||||
repeatedKeyPressHandler.cancelAll()
|
||||
}
|
||||
}
|
||||
}, 500, 50)
|
||||
}
|
||||
}
|
||||
val delayMillis = keyboardView.prefs.looknfeel.longPressDelay
|
||||
if (osHandler == null) {
|
||||
osHandler = Handler()
|
||||
}
|
||||
osHandler?.postDelayed({
|
||||
if (data.popup.isNotEmpty()) {
|
||||
longKeyPressHandler.postDelayed(delayMillis) {
|
||||
if (dataPopupWithHint.isNotEmpty()) {
|
||||
keyboardView.popupManager.extend(this)
|
||||
}
|
||||
if (data.code == KeyCode.SPACE) {
|
||||
@@ -207,7 +266,7 @@ class KeyView(
|
||||
)
|
||||
shouldBlockNextKeyCode = true
|
||||
}
|
||||
}, delayMillis.toLong())
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (keyboardView.popupManager.isShowingExtendedPopup) {
|
||||
@@ -231,16 +290,23 @@ class KeyView(
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
isKeyPressed = false
|
||||
osHandler?.removeCallbacksAndMessages(null)
|
||||
osTimer?.cancel()
|
||||
osTimer = null
|
||||
val retData = keyboardView.popupManager.getActiveKeyData(this)
|
||||
keyboardView.popupManager.hide()
|
||||
if (event.actionMasked != MotionEvent.ACTION_CANCEL && !shouldBlockNextKeyCode && retData != null) {
|
||||
florisboard?.textInputManager?.sendKeyPress(retData)
|
||||
performClick()
|
||||
longKeyPressHandler.cancelAll()
|
||||
repeatedKeyPressHandler.cancelAll()
|
||||
if (hasTriggeredGestureMove && data.code == KeyCode.DELETE) {
|
||||
hasTriggeredGestureMove = false
|
||||
florisboard?.activeEditorInstance?.apply {
|
||||
if (selection.isSelectionMode) {
|
||||
deleteBackwards()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
shouldBlockNextKeyCode = false
|
||||
val retData = keyboardView.popupManager.getActiveKeyData(this)
|
||||
keyboardView.popupManager.hide()
|
||||
if (event.actionMasked != MotionEvent.ACTION_CANCEL && !shouldBlockNextKeyCode && retData != null) {
|
||||
florisboard?.textInputManager?.sendKeyPress(retData)
|
||||
} else {
|
||||
shouldBlockNextKeyCode = false
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> return false
|
||||
@@ -248,6 +314,82 @@ 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
|
||||
)
|
||||
}
|
||||
hasTriggeredGestureMove = true
|
||||
shouldBlockNextKeyCode = true
|
||||
true
|
||||
}
|
||||
SwipeAction.DELETE_WORDS_PRECISELY -> {
|
||||
florisboard?.activeEditorInstance?.apply {
|
||||
leftAppendWordToSelection()
|
||||
}
|
||||
|
||||
hasTriggeredGestureMove = true
|
||||
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
|
||||
}
|
||||
|
||||
SwipeAction.DELETE_WORDS_PRECISELY -> {
|
||||
florisboard?.activeEditorInstance?.apply {
|
||||
leftPopWordFromSelection()
|
||||
}
|
||||
shouldBlockNextKeyCode = true
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
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 +397,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 +416,7 @@ class KeyView(
|
||||
else -> keyboardView.desiredKeyWidth
|
||||
}
|
||||
}
|
||||
val desiredHeight = keyboardView.desiredKeyHeight
|
||||
desiredHeight = keyboardView.desiredKeyHeight
|
||||
|
||||
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
|
||||
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
|
||||
@@ -313,7 +455,8 @@ class KeyView(
|
||||
}
|
||||
}
|
||||
|
||||
drawablePadding = (0.2f * height).toInt()
|
||||
drawablePaddingH = (0.2f * width).toInt()
|
||||
drawablePaddingV = (0.2f * height).toInt()
|
||||
|
||||
// MUST CALL THIS
|
||||
setMeasuredDimension(width, height)
|
||||
@@ -329,24 +472,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 +571,7 @@ class KeyView(
|
||||
* TextInputManager.
|
||||
*/
|
||||
fun updateVisibility() {
|
||||
updateEnabledState()
|
||||
when (data.code) {
|
||||
KeyCode.SWITCH_TO_TEXT_CONTEXT,
|
||||
KeyCode.SWITCH_TO_MEDIA_CONTEXT -> {
|
||||
@@ -415,6 +605,39 @@ 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, multiplier: Float = 1.0f): Float {
|
||||
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++
|
||||
}
|
||||
}
|
||||
textSize *= multiplier
|
||||
boxPaint.textSize = textSize
|
||||
return textSize
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the key label / drawable.
|
||||
*/
|
||||
@@ -426,50 +649,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.hintedNumberRowMode != KeyHintMode.DISABLED && hintedNumber != null) {
|
||||
hintedLabel = getComputedLetter(hintedNumber)
|
||||
}
|
||||
val hintedSymbol = data.hintedSymbol
|
||||
if (prefs.keyboard.hintedSymbolsMode != KeyHintMode.DISABLED && 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 +737,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 +747,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 +772,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) {
|
||||
@@ -529,11 +791,12 @@ class KeyView(
|
||||
} else {
|
||||
marginV = (measuredHeight - measuredWidth) / 2
|
||||
}
|
||||
// Note: using the vertical padding for horizontal as well on purpose here
|
||||
drawable.setBounds(
|
||||
marginH + drawablePadding,
|
||||
marginV + drawablePadding,
|
||||
measuredWidth - marginH - drawablePadding,
|
||||
measuredHeight - marginV - drawablePadding)
|
||||
marginH + drawablePaddingV,
|
||||
marginV + drawablePaddingV,
|
||||
measuredWidth - marginH - drawablePaddingV,
|
||||
measuredHeight - marginV - drawablePaddingV)
|
||||
drawable.colorFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(
|
||||
drawableColor,
|
||||
BlendModeCompat.SRC_ATOP
|
||||
@@ -544,20 +807,51 @@ 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.type == KeyType.NUMERIC) &&
|
||||
data.code != KeyCode.SPACE -> {
|
||||
val cachedTextSize = setTextSizeFor(
|
||||
labelPaint,
|
||||
desiredWidth - (2.6f * drawablePaddingH),
|
||||
desiredHeight - (3.4f * drawablePaddingV),
|
||||
// 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",
|
||||
when (resources.configuration.orientation) {
|
||||
Configuration.ORIENTATION_PORTRAIT -> {
|
||||
prefs.keyboard.fontSizeMultiplierPortrait.toFloat() / 100.0f
|
||||
}
|
||||
Configuration.ORIENTATION_LANDSCAPE -> {
|
||||
prefs.keyboard.fontSizeMultiplierLandscape.toFloat() / 100.0f
|
||||
}
|
||||
else -> 1.0f
|
||||
}
|
||||
)
|
||||
keyboardView.popupManager.keyPopupTextSize = cachedTextSize
|
||||
}
|
||||
else -> {
|
||||
setTextSizeFor(
|
||||
labelPaint,
|
||||
measuredWidth - (1.2f * drawablePaddingH),
|
||||
measuredHeight - (3.6f * drawablePaddingV),
|
||||
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 +863,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,35 @@ 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 = if (isSmartbarKeyboardView || isPreviewMode) {
|
||||
MeasureSpec.getSize(heightMeasureSpec).toFloat()
|
||||
} else {
|
||||
(florisboard?.inputView?.desiredTextKeyboardViewHeight ?: MeasureSpec.getSize(heightMeasureSpec).toFloat())
|
||||
} * if (isPreviewMode) { 0.90f } else { 1.00f }
|
||||
desiredKeyHeight = when {
|
||||
isSmartbarKeyboardView -> desiredHeight - 1.5f * keyMarginV
|
||||
else -> desiredHeight / (computedLayout?.arrangement?.size?.toFloat() ?: 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 +364,4 @@ class KeyboardView : LinearLayout {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas?) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
colorDrawable.color = getColorFromAttr(context, R.attr.keyboard_bgColor)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,9 @@ import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
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 +35,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,14 +107,15 @@ 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,
|
||||
modifier: LTN? = null,
|
||||
extension: LTN? = null
|
||||
extension: LTN? = null,
|
||||
prefs: PrefHelper
|
||||
): ComputedLayoutData {
|
||||
val computedArrangement: ComputedLayoutDataArrangement = mutableListOf()
|
||||
|
||||
@@ -174,26 +174,58 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
|
||||
for (computedRow in computedArrangement) {
|
||||
for (keyData in computedRow) {
|
||||
if (keyData.variation != KeyVariation.ALL) {
|
||||
if (keyData.variation == KeyVariation.NORMAL ||
|
||||
keyData.variation == KeyVariation.PASSWORD) {
|
||||
if (extendedPopups.containsKey(keyData.label + "~normal")) {
|
||||
keyData.popup.addAll(extendedPopups[keyData.label + "~normal"] ?: listOf())
|
||||
if (keyData.label == "." && modifierLayout?.name != "dvorak" ||
|
||||
keyData.label == "z" && modifierLayout?.name == "dvorak") {
|
||||
val label = "." // keyData.label
|
||||
if (keyData.variation == KeyVariation.NORMAL ||
|
||||
keyData.variation == KeyVariation.PASSWORD) {
|
||||
if (extendedPopups.containsKey("$label~normal")) {
|
||||
keyData.popup.addAll(extendedPopups["$label~normal"] ?: listOf())
|
||||
}
|
||||
}
|
||||
if (keyData.variation == KeyVariation.EMAIL_ADDRESS ||
|
||||
keyData.variation == KeyVariation.URI) {
|
||||
if (extendedPopups.containsKey("$label~uri")) {
|
||||
keyData.popup.addAll(extendedPopups["$label~uri"] ?: listOf())
|
||||
}
|
||||
}
|
||||
}
|
||||
if (keyData.variation == KeyVariation.EMAIL_ADDRESS ||
|
||||
keyData.variation == KeyVariation.URI) {
|
||||
if (extendedPopups.containsKey(keyData.label + "~uri")) {
|
||||
keyData.popup.addAll(extendedPopups[keyData.label + "~uri"] ?: listOf())
|
||||
}
|
||||
}
|
||||
}
|
||||
if (extendedPopups.containsKey(keyData.label)) {
|
||||
} else if (extendedPopups.containsKey(keyData.label)) {
|
||||
keyData.popup.addAll(extendedPopups[keyData.label] ?: listOf())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add hints to keys
|
||||
if (keyboardMode == KeyboardMode.CHARACTERS) {
|
||||
val symbolsComputedArrangement = fetchComputedLayoutAsync(KeyboardMode.SYMBOLS, subtype, prefs).await().arrangement
|
||||
val minRow = if (prefs.keyboard.numberRow) { 1 } else { 0 }
|
||||
for ((r, row) in computedArrangement.withIndex()) {
|
||||
if (r >= (3 + minRow) || r < minRow) {
|
||||
continue
|
||||
}
|
||||
var kOffset = 0
|
||||
val symbolRow = symbolsComputedArrangement.getOrNull(r - minRow)
|
||||
if (symbolRow != null) {
|
||||
for ((k, key) in row.withIndex()) {
|
||||
val lastKey = row.getOrNull(k - 1)
|
||||
if (key.variation != KeyVariation.ALL && lastKey != null && lastKey.variation != KeyVariation.ALL) {
|
||||
kOffset++
|
||||
}
|
||||
val symbol = symbolRow.getOrNull(k - kOffset)
|
||||
if (key.type == KeyType.CHARACTER && symbol?.type == KeyType.CHARACTER) {
|
||||
if (r == minRow) {
|
||||
key.hintedNumber = symbol
|
||||
} else if (r > minRow) {
|
||||
key.hintedSymbol = symbol
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ComputedLayoutData(
|
||||
keyboardMode,
|
||||
"computed",
|
||||
@@ -210,9 +242,10 @@ 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
|
||||
subtype: Subtype,
|
||||
prefs: PrefHelper
|
||||
): ComputedLayoutData {
|
||||
var main: LTN? = null
|
||||
var modifier: LTN? = null
|
||||
@@ -220,9 +253,15 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
|
||||
|
||||
when (keyboardMode) {
|
||||
KeyboardMode.CHARACTERS -> {
|
||||
if (prefs.keyboard.numberRow) {
|
||||
extension = LTN(LayoutType.EXTENSION, "number_row")
|
||||
}
|
||||
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")
|
||||
}
|
||||
@@ -236,17 +275,43 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
|
||||
main = LTN(LayoutType.PHONE2, "default")
|
||||
}
|
||||
KeyboardMode.SYMBOLS -> {
|
||||
extension = LTN(LayoutType.EXTENSION, "number_row")
|
||||
main = LTN(LayoutType.SYMBOLS, "western_default")
|
||||
modifier = LTN(LayoutType.SYMBOLS_MOD, "default")
|
||||
extension = LTN(LayoutType.EXTENSION, "number_row")
|
||||
}
|
||||
KeyboardMode.SYMBOLS2 -> {
|
||||
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, prefs)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the layout cache for the specified [keyboardMode].
|
||||
*
|
||||
* @param keyboardMode The keyboard mode for which the layout cache should be cleared. If null
|
||||
* is passed, the entire cache will be cleared. Defaults to null.
|
||||
*/
|
||||
fun clearLayoutCache(keyboardMode: KeyboardMode? = null) {
|
||||
if (keyboardMode == null) {
|
||||
computedLayoutCache.clear()
|
||||
} else {
|
||||
val it = computedLayoutCache.iterator()
|
||||
while (it.hasNext()) {
|
||||
val kms = it.next().key
|
||||
if (kms.first == keyboardMode) {
|
||||
it.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -260,17 +325,18 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
|
||||
@Synchronized
|
||||
fun fetchComputedLayoutAsync(
|
||||
keyboardMode: KeyboardMode,
|
||||
subtype: Subtype
|
||||
subtype: Subtype,
|
||||
prefs: PrefHelper
|
||||
): 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)
|
||||
computeLayoutFor(keyboardMode, subtype, prefs)
|
||||
}
|
||||
layoutCache[kms] = computedLayout
|
||||
computedLayoutCache[kms] = computedLayout
|
||||
computedLayout
|
||||
}
|
||||
}
|
||||
@@ -286,12 +352,13 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
|
||||
@Synchronized
|
||||
fun preloadComputedLayout(
|
||||
keyboardMode: KeyboardMode,
|
||||
subtype: Subtype
|
||||
subtype: Subtype,
|
||||
prefs: PrefHelper
|
||||
) {
|
||||
val kms = KMS(keyboardMode, subtype)
|
||||
if (layoutCache[kms] == null) {
|
||||
layoutCache[kms] = async(Dispatchers.IO) {
|
||||
computeLayoutFor(keyboardMode, subtype)
|
||||
if (computedLayoutCache[kms] == null) {
|
||||
computedLayoutCache[kms] = async(Dispatchers.IO) {
|
||||
computeLayoutFor(keyboardMode, subtype, prefs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
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.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.text.TextInputManager
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyData
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
|
||||
// TODO: Implement suggestion creation functionality
|
||||
// TODO: Cleanup and reorganize SmartbarManager
|
||||
class SmartbarManager private constructor() :
|
||||
SpellCheckerSession.SpellCheckerSessionListener, FlorisBoard.EventListener {
|
||||
|
||||
private val florisboard: FlorisBoard = FlorisBoard.getInstance()
|
||||
private var isComposingEnabled: Boolean = false
|
||||
private var spellCheckerSession: SpellCheckerSession? = null
|
||||
private val textInputManager: TextInputManager = TextInputManager.getInstance()
|
||||
var smartbarView: SmartbarView? = null
|
||||
private set
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
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 quickActionOnClickListener = View.OnClickListener { v ->
|
||||
isQuickActionsVisible = false
|
||||
when (v.id) {
|
||||
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
|
||||
}
|
||||
}
|
||||
private val quickActionToggleOnClickListener = View.OnClickListener {
|
||||
isQuickActionsVisible = !isQuickActionsVisible
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var instance: SmartbarManager? = null
|
||||
|
||||
@Synchronized
|
||||
fun getInstance(): SmartbarManager {
|
||||
if (instance == null) {
|
||||
instance = SmartbarManager()
|
||||
}
|
||||
return instance!!
|
||||
}
|
||||
}
|
||||
|
||||
fun registerSmartbarView(smartbarView: SmartbarView) {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "registerSmartbarView(smartbarView)")
|
||||
|
||||
this.smartbarView = smartbarView
|
||||
|
||||
smartbarView.quickActionToggle?.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)
|
||||
}
|
||||
}
|
||||
for (candidateView in smartbarView.candidateViewList) {
|
||||
candidateView.setOnClickListener(candidateViewOnClickListener)
|
||||
candidateView.setOnLongClickListener(candidateViewOnLongClickListener)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onWindowShown() {
|
||||
isQuickActionsVisible = false
|
||||
}
|
||||
|
||||
// TODO: clean up resources here
|
||||
override fun onDestroy() {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onDestroy()")
|
||||
|
||||
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
|
||||
when (keyboardMode) {
|
||||
KeyboardMode.NUMERIC, KeyboardMode.PHONE, KeyboardMode.PHONE2 -> {
|
||||
smartbarView?.visibility = View.GONE
|
||||
}
|
||||
else -> {
|
||||
smartbarView?.visibility = View.VISIBLE
|
||||
isQuickActionsVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onFinishInputView() {
|
||||
//spellCheckerSession?.close()
|
||||
}
|
||||
|
||||
fun deleteCandidateFromDictionary(candidate: String) {
|
||||
//
|
||||
}
|
||||
|
||||
fun resetCandidates() {
|
||||
//
|
||||
}
|
||||
|
||||
fun generateCandidatesFromComposing(composingText: String?) {
|
||||
val smartbarView = smartbarView ?: return
|
||||
|
||||
if (composingText == null) {
|
||||
smartbarView.candidateViewList[0].text = "candidate"
|
||||
smartbarView.candidateViewList[1].text = "suggestions"
|
||||
smartbarView.candidateViewList[2].text = "nyi"
|
||||
} else {
|
||||
smartbarView.candidateViewList[0].text = ""
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
private 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
|
||||
} 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
|
||||
}
|
||||
smartbarView.quickActionToggle?.rotation = 0.0f
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,18 +17,27 @@
|
||||
package dev.patrickgold.florisboard.ime.text.smartbar
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.util.AttributeSet
|
||||
import dev.patrickgold.florisboard.R
|
||||
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)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
init {
|
||||
updateTheme()
|
||||
}
|
||||
|
||||
/**
|
||||
* Override onMeasure() to automatically set the height of the button equal to the width of
|
||||
* the button. The height is MATCH_PARENT and the exact same calculated pixel size should be
|
||||
@@ -37,4 +46,19 @@ class SmartbarQuickActionButton : androidx.appcompat.widget.AppCompatImageButton
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
super.onMeasure(heightMeasureSpec, heightMeasureSpec)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas?) {
|
||||
updateTheme()
|
||||
super.onDraw(canvas)
|
||||
}
|
||||
|
||||
private fun updateTheme() {
|
||||
if (id == R.id.private_mode_button) {
|
||||
setBackgroundTintColor2(this, prefs.theme.privateModeBgColor)
|
||||
setColorFilter(prefs.theme.privateModeFgColor)
|
||||
} else {
|
||||
setBackgroundTintColor2(this, prefs.theme.smartbarButtonBgColor)
|
||||
setColorFilter(prefs.theme.smartbarButtonFgColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,61 +17,328 @@
|
||||
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.widget.Button
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.children
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.databinding.SmartbarBinding
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyVariation
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
|
||||
import dev.patrickgold.florisboard.util.setBackgroundTintColor2
|
||||
import dev.patrickgold.florisboard.util.setDrawableTintColor2
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* View class which keeps the references to important children and informs [SmartbarManager] that
|
||||
* it is now the active [SmartbarView] (useful when resetting the input view of FlorisBoard due to
|
||||
* a theme change).
|
||||
* View class which manages the state and the UI of the Smartbar, a key element in the usefulness
|
||||
* of FlorisBoard. The view automatically tries to get the current FlorisBoard instance, which it
|
||||
* needs to decide when a specific feature component is shown.
|
||||
*/
|
||||
class SmartbarView : LinearLayout {
|
||||
class SmartbarView : ConstraintLayout {
|
||||
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
private var eventListener: WeakReference<EventListener?>? = null
|
||||
private val mainScope = MainScope()
|
||||
|
||||
private val smartbarManager = SmartbarManager.getInstance()
|
||||
private var cachedActionStartAreaVisible: Boolean = false
|
||||
@IdRes private var cachedActionStartAreaId: Int? = null
|
||||
@IdRes private var cachedMainAreaId: Int? = null
|
||||
private var cachedActionEndAreaVisible: Boolean = false
|
||||
@IdRes private var cachedActionEndAreaId: Int? = null
|
||||
|
||||
var candidatesView: LinearLayout? = null
|
||||
private set
|
||||
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
|
||||
var isQuickActionsVisible: Boolean = false
|
||||
set(v) {
|
||||
binding.quickActionToggle.rotation = if (v) 180.0f else 0.0f
|
||||
field = v
|
||||
}
|
||||
private var shouldSuggestClipboardContents: Boolean = false
|
||||
|
||||
private lateinit var binding: SmartbarBinding
|
||||
private var indexedActionStartArea: MutableList<Int> = mutableListOf()
|
||||
private var indexedMainArea: MutableList<Int> = mutableListOf()
|
||||
private var indexedActionEndArea: MutableList<Int> = mutableListOf()
|
||||
|
||||
private var candidateViewList: MutableList<Button> = mutableListOf()
|
||||
|
||||
constructor(context: Context) : this(context, null)
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
/**
|
||||
* Called by Android when this view has been attached to a window. At this point we can be
|
||||
* certain that all children have been instantiated and that we can begin working with them.
|
||||
* After initializing all child views, this method registers the SmartbarView in the
|
||||
* TextInputManager, which then starts working together with this view.
|
||||
*/
|
||||
override fun onAttachedToWindow() {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onAttachedToWindow()")
|
||||
Timber.i("onAttachedToWindow()")
|
||||
|
||||
super.onAttachedToWindow()
|
||||
|
||||
candidatesView = findViewById(R.id.candidates)
|
||||
candidateViewList.add(findViewById(R.id.candidate0))
|
||||
candidateViewList.add(findViewById(R.id.candidate1))
|
||||
candidateViewList.add(findViewById(R.id.candidate2))
|
||||
binding = SmartbarBinding.bind(this)
|
||||
|
||||
numberRowView = findViewById(R.id.number_row)
|
||||
quickActionsView = findViewById(R.id.quick_actions)
|
||||
quickActionToggle = findViewById(R.id.quick_action_toggle)
|
||||
for (view in binding.actionStartArea.children) {
|
||||
indexedActionStartArea.add(view.id)
|
||||
}
|
||||
for (view in binding.mainArea.children) {
|
||||
indexedMainArea.add(view.id)
|
||||
}
|
||||
for (view in binding.actionEndArea.children) {
|
||||
indexedActionEndArea.add(view.id)
|
||||
}
|
||||
|
||||
smartbarManager.registerSmartbarView(this)
|
||||
candidateViewList.add(binding.candidate0)
|
||||
candidateViewList.add(binding.candidate1)
|
||||
candidateViewList.add(binding.candidate2)
|
||||
|
||||
binding.backButton.setOnClickListener { eventListener?.get()?.onSmartbarBackButtonPressed() }
|
||||
|
||||
binding.clipboardCursorRow.isSmartbarKeyboardView = true
|
||||
mainScope.launch(Dispatchers.Default) {
|
||||
florisboard?.let {
|
||||
val layout = florisboard.textInputManager.layoutManager.fetchComputedLayoutAsync(
|
||||
KeyboardMode.SMARTBAR_CLIPBOARD_CURSOR_ROW,
|
||||
Subtype.DEFAULT,
|
||||
prefs
|
||||
).await()
|
||||
launch(Dispatchers.Main) {
|
||||
binding.clipboardCursorRow.computedLayout = layout
|
||||
binding.clipboardCursorRow.updateVisibility()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.clipboardSuggestion.setOnClickListener {
|
||||
florisboard?.activeEditorInstance?.performClipboardPaste()
|
||||
shouldSuggestClipboardContents = false
|
||||
updateSmartbarState()
|
||||
}
|
||||
|
||||
binding.numberRow.isSmartbarKeyboardView = true
|
||||
mainScope.launch(Dispatchers.Default) {
|
||||
florisboard?.let {
|
||||
val layout = it.textInputManager.layoutManager.fetchComputedLayoutAsync(
|
||||
KeyboardMode.SMARTBAR_NUMBER_ROW,
|
||||
Subtype.DEFAULT,
|
||||
prefs
|
||||
).await()
|
||||
launch(Dispatchers.Main) {
|
||||
binding.numberRow.computedLayout = layout
|
||||
binding.numberRow.updateVisibility()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.privateModeButton.setOnClickListener {
|
||||
eventListener?.get()?.onSmartbarPrivateModeButtonClicked()
|
||||
}
|
||||
|
||||
for (quickAction in binding.quickActions.children) {
|
||||
if (quickAction is SmartbarQuickActionButton) {
|
||||
quickAction.setOnClickListener { eventListener?.get()?.onSmartbarQuickActionPressed(quickAction.id) }
|
||||
}
|
||||
}
|
||||
|
||||
binding.quickActionToggle.setOnClickListener {
|
||||
isQuickActionsVisible = !isQuickActionsVisible
|
||||
updateSmartbarState()
|
||||
}
|
||||
|
||||
configureFeatureVisibility(
|
||||
actionStartAreaVisible = false,
|
||||
actionStartAreaId = null,
|
||||
mainAreaId = null,
|
||||
actionEndAreaVisible = false,
|
||||
actionEndAreaId = null
|
||||
)
|
||||
|
||||
florisboard?.textInputManager?.registerSmartbarView(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiplies the default smartbar height with the given [factor] and sets it.
|
||||
* Updates the visibility of features based on the provided attributes.
|
||||
*
|
||||
* @param actionStartAreaVisible True if the action start area should be shown, else false.
|
||||
* @param actionStartAreaId The ID of the element to show within the action start area. Set to
|
||||
* null to leave this area blank.
|
||||
* @param mainAreaId The ID of the element to show within the main area. Set to null to leave
|
||||
* this area blank.
|
||||
* @param actionEndAreaVisible True if the action end area should be shown, else false.
|
||||
* @param actionEndAreaId The ID of the element to show within the action end area. Set to null
|
||||
* to leave this area blank.
|
||||
*/
|
||||
fun setHeightFactor(factor: Float) {
|
||||
val baseSize = resources.getDimension(R.dimen.smartbar_height)
|
||||
val size = (baseSize * factor).toInt()
|
||||
layoutParams?.height = size
|
||||
private fun configureFeatureVisibility(
|
||||
actionStartAreaVisible: Boolean = cachedActionStartAreaVisible,
|
||||
@IdRes actionStartAreaId: Int? = cachedActionStartAreaId,
|
||||
@IdRes mainAreaId: Int? = cachedMainAreaId,
|
||||
actionEndAreaVisible: Boolean = cachedActionEndAreaVisible,
|
||||
@IdRes actionEndAreaId: Int? = cachedActionEndAreaId
|
||||
) {
|
||||
binding.actionStartArea.visibility = when {
|
||||
actionStartAreaVisible && actionStartAreaId != null -> View.VISIBLE
|
||||
actionStartAreaVisible && actionStartAreaId == null -> View.INVISIBLE
|
||||
else -> View.GONE
|
||||
}
|
||||
if (actionStartAreaId != null) {
|
||||
binding.actionStartArea.displayedChild =
|
||||
indexedActionStartArea.indexOf(actionStartAreaId).coerceAtLeast(0)
|
||||
}
|
||||
binding.mainArea.visibility = when (mainAreaId) {
|
||||
null -> View.INVISIBLE
|
||||
else -> View.VISIBLE
|
||||
}
|
||||
if (mainAreaId != null) {
|
||||
binding.mainArea.displayedChild =
|
||||
indexedMainArea.indexOf(mainAreaId).coerceAtLeast(0)
|
||||
}
|
||||
binding.actionEndArea.visibility = when {
|
||||
actionEndAreaVisible && actionEndAreaId != null -> View.VISIBLE
|
||||
actionEndAreaVisible && actionEndAreaId == null -> View.INVISIBLE
|
||||
else -> View.GONE
|
||||
}
|
||||
if (actionEndAreaId != null) {
|
||||
binding.actionEndArea.displayedChild =
|
||||
indexedActionEndArea.indexOf(actionEndAreaId).coerceAtLeast(0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the Smartbar UI state by looking at the current keyboard mode, key variation, active
|
||||
* editor instance, etc. Passes the evaluated attributes to [configureFeatureVisibility].
|
||||
*/
|
||||
fun updateSmartbarState() {
|
||||
binding.clipboardCursorRow.updateVisibility()
|
||||
when (florisboard) {
|
||||
null -> configureFeatureVisibility(
|
||||
actionStartAreaVisible = false,
|
||||
actionStartAreaId = null,
|
||||
mainAreaId = null,
|
||||
actionEndAreaVisible = false,
|
||||
actionEndAreaId = null
|
||||
)
|
||||
else -> configureFeatureVisibility(
|
||||
actionStartAreaVisible = when (florisboard.textInputManager.keyVariation) {
|
||||
KeyVariation.PASSWORD -> false
|
||||
else -> true
|
||||
},
|
||||
actionStartAreaId = when (florisboard.textInputManager.getActiveKeyboardMode()) {
|
||||
KeyboardMode.EDITING -> R.id.back_button
|
||||
else -> R.id.quick_action_toggle
|
||||
},
|
||||
mainAreaId = when (florisboard.textInputManager.keyVariation) {
|
||||
KeyVariation.PASSWORD -> R.id.number_row
|
||||
else -> when (isQuickActionsVisible) {
|
||||
true -> R.id.quick_actions
|
||||
else -> when (florisboard.textInputManager.getActiveKeyboardMode()) {
|
||||
KeyboardMode.EDITING,
|
||||
KeyboardMode.NUMERIC,
|
||||
KeyboardMode.PHONE,
|
||||
KeyboardMode.PHONE2 -> null
|
||||
else -> when {
|
||||
florisboard.activeEditorInstance.isComposingEnabled &&
|
||||
shouldSuggestClipboardContents
|
||||
-> R.id.clipboard_suggestion_row
|
||||
florisboard.activeEditorInstance.isComposingEnabled &&
|
||||
florisboard.activeEditorInstance.selection.isCursorMode
|
||||
-> R.id.candidates
|
||||
else -> R.id.clipboard_cursor_row
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
actionEndAreaVisible = when (florisboard.textInputManager.keyVariation) {
|
||||
KeyVariation.PASSWORD -> false
|
||||
else -> true
|
||||
},
|
||||
actionEndAreaId = when {
|
||||
florisboard.activeEditorInstance.isPrivateMode -> R.id.private_mode_button
|
||||
else -> null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onPrimaryClipChanged() {
|
||||
if (prefs.suggestion.enabled && prefs.suggestion.suggestClipboardContent &&
|
||||
florisboard?.activeEditorInstance?.isPrivateMode == false) {
|
||||
shouldSuggestClipboardContents = true
|
||||
val item = florisboard.clipboardManager?.primaryClip?.getItemAt(0)
|
||||
when {
|
||||
item?.text != null -> {
|
||||
binding.clipboardSuggestion.text = item.text
|
||||
}
|
||||
item?.uri != null -> {
|
||||
binding.clipboardSuggestion.text = "(Image) " + item.uri.toString()
|
||||
}
|
||||
else -> {
|
||||
binding.clipboardSuggestion.text = item?.text ?: "(Error while retrieving clipboard data)"
|
||||
}
|
||||
}
|
||||
updateSmartbarState()
|
||||
}
|
||||
}
|
||||
|
||||
fun resetClipboardSuggestion() {
|
||||
shouldSuggestClipboardContents = false
|
||||
updateSmartbarState()
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
|
||||
val heightSize = MeasureSpec.getSize(heightMeasureSpec).toFloat()
|
||||
val height = when (heightMode) {
|
||||
MeasureSpec.EXACTLY -> {
|
||||
// Must be this size
|
||||
heightSize
|
||||
}
|
||||
MeasureSpec.AT_MOST -> {
|
||||
// Can't be bigger than...
|
||||
(florisboard?.inputView?.desiredSmartbarHeight ?: resources.getDimension(R.dimen.smartbar_baseHeight)).coerceAtMost(heightSize)
|
||||
}
|
||||
else -> {
|
||||
// Be whatever you want
|
||||
florisboard?.inputView?.desiredSmartbarHeight ?: resources.getDimension(R.dimen.smartbar_baseHeight)
|
||||
}
|
||||
}
|
||||
|
||||
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height.roundToInt(), MeasureSpec.EXACTLY))
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas?) {
|
||||
super.onDraw(canvas)
|
||||
setBackgroundColor(prefs.theme.smartbarBgColor)
|
||||
setBackgroundTintColor2(binding.clipboardSuggestion, prefs.theme.smartbarButtonBgColor)
|
||||
setDrawableTintColor2(binding.clipboardSuggestion, prefs.theme.smartbarButtonFgColor)
|
||||
binding.clipboardSuggestion.setTextColor(prefs.theme.smartbarButtonFgColor)
|
||||
for (view in candidateViewList) {
|
||||
view.setTextColor(prefs.theme.smartbarFgColor)
|
||||
}
|
||||
}
|
||||
|
||||
fun setEventListener(listener: EventListener) {
|
||||
eventListener = WeakReference(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener interface which can be used by other classes to receive updates when something
|
||||
* important happens in the Smartbar.
|
||||
*/
|
||||
interface EventListener {
|
||||
fun onSmartbarBackButtonPressed() {}
|
||||
//fun onSmartbarCandidatePressed() {}
|
||||
//fun onSmartbarCandidateLongPressed() {}
|
||||
fun onSmartbarPrivateModeButtonClicked() {}
|
||||
fun onSmartbarQuickActionPressed(@IdRes quickActionId: Int) {}
|
||||
}
|
||||
}
|
||||
|
||||
313
app/src/main/java/dev/patrickgold/florisboard/ime/theme/Theme.kt
Normal file
313
app/src/main/java/dev/patrickgold/florisboard/ime/theme/Theme.kt
Normal file
@@ -0,0 +1,313 @@
|
||||
/*
|
||||
* 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.privateModeBgColor = theme.getAttr("privateMode/bgColor", "#A000FF")
|
||||
prefs.theme.privateModeFgColor = theme.getAttr("privateMode/fgColor", "#FFFFFF")
|
||||
|
||||
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,55 @@
|
||||
/*
|
||||
* 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 androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.settings.components.DialogSeekBarPreference
|
||||
|
||||
class KeyboardFragment : PreferenceFragmentCompat(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private var heightFactorCustom: DialogSeekBarPreference? = null
|
||||
private var sharedPrefs: SharedPreferences? = null
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.prefs_keyboard)
|
||||
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
heightFactorCustom = findPreference(PrefHelper.Keyboard.HEIGHT_FACTOR_CUSTOM)
|
||||
onSharedPreferenceChanged(null, PrefHelper.Keyboard.HEIGHT_FACTOR)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
sharedPrefs?.registerOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
sharedPrefs?.unregisterOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
if (key == PrefHelper.Keyboard.HEIGHT_FACTOR) {
|
||||
heightFactorCustom?.isVisible = sharedPrefs?.getString(key, "") == "custom"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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, prefs).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)
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user