Compare commits

...

52 Commits

Author SHA1 Message Date
Patrick Goldinger
edc63aa680 Release v0.2.3 2020-11-11 23:08:55 +01:00
Patrick Goldinger
23def145b2 Finish reworking core (#35 #33) 2020-11-11 22:59:27 +01:00
Patrick Goldinger
3f7bd4f65d Fix delete key not working for emojis / Fix several other bugs 2020-11-10 23:44:07 +01:00
Patrick Goldinger
7b91d4f9d3 Add EditorInstance object to better manage state of input
- EditorInstance is an improved EditorInfo object which also holds the
  current state of the input like text, selection, ...
- Should help in cleaning up TextInputManager and resolve issues around
  non-updating caps states, etc.
2020-11-08 22:34:05 +01:00
Patrick Goldinger
175369f7d7 Improve onStartInputView behaviour 2020-11-05 19:41:09 +01:00
Patrick Goldinger
79c5acc007 Improve debugging inspection output
- Needed for inspection why FlorisBoard behaves strangely in some apps
2020-11-04 21:24:06 +01:00
Patrick Goldinger
94d470dd96 Fix font sizing bug in KeyView
- Calculation may require 2 iterations until the correct size is found
  because both width and height can be <=0 or >=0
2020-11-03 18:56:11 +01:00
Patrick Goldinger
ee9d61ad1e Add auto font sizing for text input keys (#32)
- Font of keys is now adjusted accordingly to the keyboard height
  preference.
- Affects hinted symbols / numbers too.
2020-11-01 22:22:14 +01:00
Patrick Goldinger
a3c7b538d0 Add option to remember caps lock state (#30)
- Located in Settings > Typing > Remember caps lock state
- Defaults to false (do not remember state)
2020-10-30 16:49:47 +01:00
Patrick Goldinger
ca4cd38bb2 Release v0.2.2 2020-10-28 23:38:27 +01:00
Patrick Goldinger
7046c500ff Add number and symbol hint for character layout
- If enabled, the first row of the character layout now has a number row
  integrated.
- If enabled, row 2 & 3 of the character layout will have the symbol of
  the corresponding position in the symbol layout.
- In the top-right corner of each key with a hinted character the number
  or symbol will be visible.
- Also: change order of popup keys in the json definition files. The
  first popup of each key is now the most important and will always be
  focused. Then the following popup keys will be filled from left to
  right and from top to bottom.
- Change layout manager to accommodate new hint feature.
- Document KeyData class
- Add license to several files in ime.text.key package.
- Change how layouts are loaded in TextInputManager: all layouts are now
  loaded for all layout types, this is done a) to help with the new hint
  feature. b) to implement subtype-dependent symbol layouts (nyi;
  future plan).
2020-10-28 23:16:06 +01:00
Patrick Goldinger
0374a82f99 Fix UI not updating correctly in clipboard/cursor row
- UI is now queued for redrawing after cursor status has changed
2020-10-26 17:08:19 +01:00
Patrick Goldinger
217acbd6f1 Improve emoji input view layout
- Emoji input layout now fits automatically to the keyboard's height
- Scroll orientation has been changed to vertical which fits the new
  layout better.
2020-10-26 16:50:09 +01:00
Patrick Goldinger
ef27d511be Add NYI tag notice to settings home screen 2020-10-26 15:22:28 +01:00
Patrick Goldinger
f9a4ffa5eb Add bottom offset option to accommodate for curved screens (#20)
This option will default to 0dp (disabled) but can expand up to 24dp.
Located in Settings > Keyboard.
2020-10-23 17:49:05 +02:00
Patrick Goldinger
5533badd19 Add option to turn off auto-capitalization (Fix #21) 2020-10-23 15:52:44 +02:00
Patrick Goldinger
0f1b4b081d Disable swipe velocity threshold preference
Currently the swipe velocity is calculated based on the path and
time of the swipe. The length unit is completely dependent on the
phone's screen and causes different values on different devices.
If a device-independent solution is found this preference will be
enabled again.
2020-10-22 18:50:25 +02:00
Patrick Goldinger
3feae09df0 Add feedback option to CONTRIBUTING.md as mailto links do not work within config.yml 2020-10-21 23:18:28 +02:00
Patrick Goldinger
34bb28d1fc Fix issue config.yml syntax 2020-10-21 23:16:03 +02:00
Patrick Goldinger
551a294b05 Add feedback option to issue creation process
See #22
2020-10-21 22:47:49 +02:00
Patrick Goldinger
671ff1d8b4 Add question issue template / Improve issue creation process 2020-10-19 20:29:08 +02:00
Patrick Goldinger
15caf66370 Release v0.2.1 2020-10-18 18:36:37 +02:00
Patrick Goldinger
ae0a8e551b Merge pull request #19 from florisboard/feat-gestures
Add gestures & scrolling space bar
2020-10-18 18:27:49 +02:00
Patrick Goldinger
cb4bedfc2c Add delete word and switch to prev subtype swipe action / Fix bugs
- Remove NYI tag from gesture preferences
- Adjust velocity threshold values
2020-10-18 18:16:46 +02:00
Patrick Goldinger
7d63a6885c Add velocity threshold / Fix bugs in gesture detection 2020-10-18 16:14:26 +02:00
Patrick Goldinger
841d797b7c Add custom gesture detector and listener interface 2020-10-17 21:04:09 +02:00
Patrick Goldinger
0c9ba5326a Add basic gesture support (up, down, left, right)
Using the Android GestureDetector. Will probably be replaced by custom
implementation.
2020-10-15 19:10:08 +02:00
Patrick Goldinger
7c5a7dc148 Add gestures and glide typing preferences
Also add backbone access in PrefHelper, base for gestures implementation.
2020-10-09 16:59:15 +02:00
Patrick Goldinger
37fc714729 Update feature roadmap / Add link to IzzySoft's repo 2020-10-08 19:39:49 +02:00
Patrick Goldinger
ec7d65ebc0 Add changelogs beginning with version 0.2.0 (12)
Based on suggestion of @IzzySoft in #1.
2020-10-08 16:14:45 +02:00
Patrick Goldinger
5670af16d6 Merge pull request #18 from IzzySoft/master
formatting full_description.txt
2020-10-08 16:09:17 +02:00
Izzy
6b39a846e6 formatting full_description.txt 2020-10-07 23:45:01 +02:00
Patrick Goldinger
a25501d63c Release v0.2.0 2020-10-05 22:33:15 +02:00
Patrick Goldinger
e9a5f2161c Fix bugs in setup and settings / Change values for suggestion prefs 2020-10-05 22:25:17 +02:00
Patrick Goldinger
6f12f22937 Fix #17
Fix bug where the action defined for the enter key did not behave as
intended when the supplied action has flags set. Some apps ignored these
and worked flawlessly (Reddit, Chrome, Firefox, ...) while other
apps didn't behave that well (Twitter, F-Droid, ...).
2020-10-05 21:05:28 +02:00
Patrick Goldinger
25054ef679 Improve layout measurement and height factor calculation
- Height factor is now being used in the root InputView rather than
  repeatedly recalculating it on a per KeyboardView basis.
- Keyboard views had an empty space when in one-handed mode.
- Icon size in Smartbar is still messed up, but will be resolved
  when overhauling the Smartbar.
- Media layout may contain empty spaces, will be addressed when
  overhauling the media context UI.
2020-09-22 19:37:51 +02:00
Patrick Goldinger
3af17f99fe Add back Advanced Settings / Fix back button logic in Settings 2020-09-17 20:10:49 +02:00
Patrick Goldinger
06664ff521 Merge pull request #15 from florisboard/feat-theme-customization
Add theme customization feature
2020-09-16 20:04:30 +02:00
Patrick Goldinger
fde0749a3b Fix unhandled exceptions / Improve core layout view
- When detaching an InputView from the window, it will throw an
  exception when it is flipping while being detached. This has been
  fixed by extending the class and catching the exception.
- The core layout now has InputWindowView as the root and InputView
  as the real input view. While technically nothing has changed here,
  due to the better naming scheme it is more clearer now.
- Fix theme colors in both Floris Day and Night.
- Move ime.editing package into ime.text
2020-09-16 19:40:01 +02:00
Patrick Goldinger
c061e15263 Change used color picker pref / Fix bugs 2020-09-13 19:15:04 +02:00
Patrick Goldinger
7256c597c2 Add theme preset selector dialog / Add Floris Night theme
- Theme preset can now be selected and will be applied.
- Floris Night theme re-added (was previously defined in
  res/values/theme.xml)
- Various bug fixes / feature enhancement regarding themes
  and preferences.
2020-09-11 20:23:04 +02:00
Patrick Goldinger
2f9d32027b Add own fragment for theme prefs
- Advanced settings fragment is currently not accessible, will change
  at a later stage
- Partial support for one-handed colors
- Add method to write a Theme class to prefs
- Base theme now only sets the absolute minimum values and fallbacks
- Base Theme comes in Day and Night variant, is dependent on
  isNightTheme flag in <theme>.json
2020-09-09 21:01:24 +02:00
Patrick Goldinger
538912edc2 Add media context theme support / Fix various bugs 2020-09-04 19:45:54 +02:00
Patrick Goldinger
ee5ff81ee8 Improve applying pref color to View 2020-09-02 18:38:45 +02:00
Patrick Goldinger
d873dc54c5 Add theme prefs for KeyPopup, Smartbar, EditingLayout and NavBar 2020-09-01 20:10:44 +02:00
Patrick Goldinger
1e967463de Add theme prefs for core KeyboardView and KeyView 2020-08-31 21:09:24 +02:00
Patrick Goldinger
5c084a10dc Add Theme core class and sample theme file
- Theme class is responsible for parsing / packing a theme.
- The sample theme file will be the keyboards default and fallback theme
  in the future, with slight modifications.
2020-08-31 18:22:01 +02:00
Patrick Goldinger
f158a9deb3 Update Kotlin to 1.4.0 / Update other packages as well 2020-08-27 23:24:11 +02:00
Patrick Goldinger
66d328293c Merge pull request #14 from florisboard/feat-new-settings-screen
Revamp settings screen
2020-08-27 22:23:59 +02:00
Patrick Goldinger
e33b652bb3 Add localization and theme prefs to fragments / Improve layout
- Improve layout of list_item.xml (adheres more to predefined standard
  values now)
- Add localization card to typing fragment.
- Add theme card to keyboard fragment (will get revision and own sub-fragment)
  with next feature (theme customization).
2020-08-27 21:51:47 +02:00
Patrick Goldinger
65d8c02b95 Restructure settings naming scheme and structure (frontend)
- Home, Localization and Theme need a better UI, only temporary for now.
2020-08-26 23:49:10 +02:00
Patrick Goldinger
bd090132eb Restructure settings naming scheme and structure (backend)
- No setting added or removed, only renamed and possibly moved within the
  settings structure.
- Strings have also been updated (en, it-IT)
2020-08-26 22:47:43 +02:00
124 changed files with 5227 additions and 1647 deletions

View File

@@ -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
<!--
```
If applicable, paste the captured debug log here.
```
-->

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View 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

View File

@@ -1,11 +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: ''
---
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.
Thank you for your help in making FlorisBoard better!
<!--
- 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
View 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!
-->

View File

@@ -5,6 +5,12 @@ 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.
## 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!
## Adding a new feature or making large changes
If you intend to add a new feature or to make large changes, please discuss this
@@ -51,6 +57,7 @@ Notes for tips below:
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>`

View File

@@ -3,8 +3,16 @@
An open-source keyboard for Android. Currently in alpha stage.
#### 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:
Wanna try it out on your device? Use one of the following options:
_A. IzzySoft's repo for F-Droid_:
[<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" height="64" alt="IzzySoft repo badge">](https://apt.izzysoft.de/fdroid/index/apk/dev.patrickgold.florisboard)
_B. Google Play Public Alpha Test_:
You can join the public alpha test programme on Google Play. To become a
tester, follow these steps:
1. Join the
[FlorisBoard Public Alpha Test](https://groups.google.com/g/florisboard-public-alpha-test)
Google Group to be able to access the testing programme.
@@ -18,65 +26,64 @@ programme on Google Play. To become a tester, follow these steps:
4. Finished! You will receive future versions of FlorisBoard via Google
Play.
_C. Use the APK provided in the release section of this repo_
##### 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)
If you want to give feedback to FlorisBoard, there are several ways to
do so, as listed in the [contribution guidelines](CONTRIBUTING.md).
Thank you for contributing to FlorisBoard!
##### 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.
FlorisBoard is currently available through Google Play and IzzySoft's
repo for F-Droid, but is currently in the inclusion process for the main
F-Droid repo. Planned proper F-Droid release is version 0.3.0.
---
![Preview image](https://patrickgold.dev/media/previews/florisboard.png)
<img src="https://patrickgold.dev/media/previews/florisboard.png"
height="256" alt="Preview Image">
## Feature roadmap
### Basics
* [x] Implementation of the keyboard core (InputMethodService)
* [x] Own implementation of deprecated KeyboardView (base only)
* [x] Custom implementation of deprecated KeyboardView (base only)
* [x] Caps + Caps Lock
* [x] Key popups
* [x] Extended key popups (e.g. a -> á, à, ä, ...) (needs tweaks for
emojis)
* [x] Extended key popups (e.g. a -> á, à, ä, ...)
* [x] Key press sound/vibration
* [x] Portrait orientation support
* [x] Landscape orientation support (needs tweaks)
* [ ] Tablet screen support
* [ ] Tablet screen support (0.4.0)
### Layouts
* [x] Latin character layouts (QWERTY, QWERTZ, AZERTY, Swiss, Spanish,
Norwegian, Swedish/Finnish, Icelandic, Danish)
Norwegian, Swedish/Finnish, Icelandic, Danish); more coming in
future versions
* [x] Non-latin character layouts (Persian)
* [x] Adapt to situation in app (password, url, text, etc. )
* [x] Special character layout(s)
* [x] Numeric layout
* [x] Numeric layout (advanced)
* [x] Phone number layout
* [x] Emoji layout (popups buggy atm)
* [x] Emoji layout (tweaks: 0.3.0)
* [x] Emoticon layout
* [ ] Kaomoji layout
* [ ] Kaomoji layout (0.3.0)
### Preferences
* [x] Setup wizard
* [x] Preferences screen
* [x] Customize look and behaviour of keyboard (currently only
light/dark theme)
* [ ] Theme customization
* [ ] Theme import/export (?)
* [x] Customize look and behaviour of keyboard
* [x] Theme presets (currently only day/night theme)
* [x] Theme customization
* [ ] Theme import/export (0.4.0 or 0.5.0)
* [x] Subtype selection (language/layout)
* [x] Keyboard behaviour preferences
* [ ] Text suggestion / Auto correct preferences
* [ ] Gesture preferences
* [ ] Text suggestion / Auto correct preferences (0.4.0 or 0.5.0)
* [x] Gesture preferences (0.3.0)
### Composing suggestions
### Composing suggestions (0.4.0 or 0.5.0)
* [ ] Auto suggest words from precompiled dictionary
* [ ] Auto suggest words from user dictionary
* [ ] Auto suggest contacts
@@ -85,25 +92,33 @@ timeline for this, but I aim for the 0.2.0 or 0.3.0 release.
### Other useful features
* [x] One-handed mode
* [x] Clipboard/cursor tools
* [ ] Floating keyboard
* [ ] Gesture support
* [ ] Glide typing (?)
* [x] Integrated number row / symbols in character layouts (0.3.0)
* [ ] Floating keyboard (0.4.0)
* [x] Gesture support (0.3.0)
* [ ] Glide typing (0.4.0)
* [x] Full integration in IME service list of Android (xml/method)
(integration is internal-only, because Android's default subtype
implementation not really allows for dynamic language/layout
pairs, only compile-time defined ones)
* [ ] Description and settings reference in System Language & Input
* [ ] (dev only) Generate well-structured documentation of code
* [ ] ...
Note: (?) = not sure if it will be implemented
Note:
## Used libraries and icons
(?) = not sure if it will be implemented
(0.x.0) = planned version when feature will be implemented.
## 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
```

View File

@@ -10,8 +10,8 @@ android {
applicationId "dev.patrickgold.florisboard"
minSdkVersion 23
targetSdkVersion 29
versionCode 11
versionName "0.1.2"
versionCode 15
versionName "0.2.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -31,10 +31,11 @@ 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 "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0'
testImplementation 'junit:junit:4.12'
testImplementation 'androidx.test:core:1.2.0'
testImplementation 'org.mockito:mockito-core:1.10.19'
@@ -43,7 +44,8 @@ dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.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.0'
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'
}

View File

@@ -74,6 +74,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"

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
{
"a": [
{ "code": 224, "label": "à" },
{ "code": 230, "label": "æ" },
{ "code": 227, "label": "ã" },
{ "code": 229, "label": "å" },
{ "code": 257, "label": "ā" },
{ "code": 224, "label": "à" },
{ "code": 225, "label": "á" },
{ "code": 226, "label": "â" },
{ "code": 228, "label": "ä" }
@@ -13,44 +13,45 @@
{ "code": 231, "label": "ç" }
],
"e": [
{ "code": 233, "label": "é" },
{ "code": 275, "label": "ē" },
{ "code": 234, "label": "ê" },
{ "code": 233, "label": "é" },
{ "code": 232, "label": "è" },
{ "code": 235, "label": "ë" }
],
"i": [
{ "code": 237, "label": "í" },
{ "code": 236, "label": "ì" },
{ "code": 239, "label": "ï" },
{ "code": 237, "label": "í" },
{ "code": 238, "label": "î" },
{ "code": 299, "label": "ī" }
],
"n": [
{ "code": 324, "label": "ń" },
{ "code": 241, "label": "ñ" }
{ "code": 241, "label": "ñ" },
{ "code": 324, "label": "ń" }
],
"o": [
{ "code": 243, "label": "ó" },
{ "code": 245, "label": "õ" },
{ "code": 333, "label": "ō" },
{ "code": 339, "label": "œ" },
{ "code": 248, "label": "ø" },
{ "code": 242, "label": "ò" },
{ "code": 246, "label": "ö" },
{ "code": 243, "label": "ó" },
{ "code": 244, "label": "ô" }
],
"s": [
{ "code": 223, "label": "ß" }
],
"u": [
{ "code": 250, "label": "ú" },
{ "code": 363, "label": "ū" },
{ "code": 252, "label": "ü" },
{ "code": 250, "label": "ú" },
{ "code": 251, "label": "û" },
{ "code": 249, "label": "ù" }
],
".~normal": [
{ "code": 44, "label": "," },
{ "code": 38, "label": "&" },
{ "code": 37, "label": "%" },
{ "code": 43, "label": "+" },
@@ -65,14 +66,13 @@
{ "code": 41, "label": ")" },
{ "code": 35, "label": "#" },
{ "code": 33, "label": "!" },
{ "code": 44, "label": "," },
{ "code": 63, "label": "?" }
],
".~uri": [
{ "code": -255, "label": ".com" },
{ "code": -255, "label": ".gov" },
{ "code": -255, "label": ".edu" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".com" },
{ "code": -255, "label": ".net" }
]
}

View File

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

View File

@@ -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,8 +44,8 @@
{ "code": 1577, "label": "ة" }
],
"ک": [
{ "code": 1603, "label": "ك" },
{ "code": 1706, "label": "ڪ"}
{ "code": 1706, "label": "ڪ"},
{ "code": 1603, "label": "ك" }
],
"ز": [
{ "code": 1688, "label": "ژ" }
@@ -54,6 +54,7 @@
{ "code": 1572, "label": "ؤ" }
],
".~normal": [
{ "code": 1611, "label": "ً" },
{ "code": 1622, "label": "ٖ" },
{ "code": 1648, "label": "ٰ" },
{ "code": 1619, "label": "ٓ" },
@@ -66,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" }
]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,60 @@
{
"name": "floris_day",
"displayName": "Floris Day",
"author": "patrickgold",
"isNightTheme": false,
"attributes": {
"window": {
"colorPrimary": "#4CAF50",
"colorPrimaryDark": "#388E3C",
"colorAccent": "#FF9800",
"navigationBarColor": "@keyboard/bgColor",
"navigationBarLight": "true",
"semiTransparentColor": "#20000000",
"textColor": "#000000"
},
"keyboard": {
"bgColor": "#E0E0E0"
},
"key": {
"bgColor": "#FFFFFF",
"bgColorPressed": "#F5F5F5",
"fgColor": "@window/textColor"
},
"keyEnter": {
"bgColor": "@window/colorPrimary",
"bgColorPressed": "@window/colorPrimaryDark",
"fgColor": "#FFFFFF"
},
"keyPopup": {
"bgColor": "#EEEEEE",
"bgColorActive": "#BDBDBD",
"fgColor": "@window/textColor"
},
"keyShift": {
"bgColor": "@key/bgColor",
"bgColorPressed": "@key/bgColorPressed",
"fgColor": "@window/textColor",
"fgColorCapsLock": "@window/colorAccent"
},
"media": {
"fgColor": "@window/textColor",
"fgColorAlt": "#757575"
},
"oneHanded": {
"bgColor": "#E8F5E9"
},
"oneHandedButton": {
"fgColor": "#424242"
},
"smartbar": {
"bgColor": "transparent",
"fgColor": "@window/textColor",
"fgColorAlt": "#8A8A8A"
},
"smartbarButton": {
"bgColor": "@key/bgColor",
"fgColor": "@key/fgColor"
}
}
}

View File

@@ -0,0 +1,60 @@
{
"name": "floris_night",
"displayName": "Floris Night",
"author": "patrickgold",
"isNightTheme": true,
"attributes": {
"window": {
"colorPrimary": "#4CAF50",
"colorPrimaryDark": "#388E3C",
"colorAccent": "#FF9800",
"navigationBarColor": "@keyboard/bgColor",
"navigationBarLight": "false",
"semiTransparentColor": "#20FFFFFF",
"textColor": "#FFFFFF"
},
"keyboard": {
"bgColor": "#212121"
},
"key": {
"bgColor": "#424242",
"bgColorPressed": "#616161",
"fgColor": "@window/textColor"
},
"keyEnter": {
"bgColor": "@window/colorPrimary",
"bgColorPressed": "@window/colorPrimaryDark",
"fgColor": "#FFFFFF"
},
"keyPopup": {
"bgColor": "#757575",
"bgColorActive": "#BDBDBD",
"fgColor": "@window/textColor"
},
"keyShift": {
"bgColor": "@key/bgColor",
"bgColorPressed": "@key/bgColorPressed",
"fgColor": "@window/textColor",
"fgColorCapsLock": "@window/colorAccent"
},
"media": {
"fgColor": "@window/textColor",
"fgColorAlt": "#BDBDBD"
},
"oneHanded": {
"bgColor": "#1B5E20"
},
"oneHandedButton": {
"fgColor": "#EEEEEE"
},
"smartbar": {
"bgColor": "transparent",
"fgColor": "@window/textColor",
"fgColorAlt": "#73FFFFFF"
},
"smartbarButton": {
"bgColor": "@key/bgColor",
"fgColor": "@key/fgColor"
}
}
}

View File

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

View File

@@ -0,0 +1,739 @@
/*
* 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.inputmethodservice.InputMethodService
import android.os.Build
import android.text.InputType
import android.view.KeyEvent
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.ExtractedTextRequest
import androidx.annotation.RequiresApi
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import java.lang.StringBuilder
// Constants for detectLastUnicodeCharacterLengthBeforeCursor method
private const val LIGHT_SKIN_TONE = 0x1F3FB
private const val MEDIUM_LIGHT_SKIN_TONE = 0x1F3FC
private const val MEDIUM_SKIN_TONE = 0x1F3FD
private const val MEDIUM_DARK_SKIN_TONE = 0x1F3FE
private const val DARK_SKIN_TONE = 0x1F3FF
private const val RED_HAIR = 0x1F9B0
private const val CURLY_HAIR = 0x1F9B1
private const val WHITE_HAIR = 0x1F9B2
private const val BALD = 0x1F9B3
private const val ZERO_WIDTH_JOINER = 0x200D
private const val VARIATION_SELECTOR = 0xFE0F
// Array which holds all variations for easier checking (convenience only)
private val emojiVariationArray: Array<Int> = arrayOf(
LIGHT_SKIN_TONE,
MEDIUM_LIGHT_SKIN_TONE,
MEDIUM_SKIN_TONE,
MEDIUM_DARK_SKIN_TONE,
DARK_SKIN_TONE,
RED_HAIR,
CURLY_HAIR,
WHITE_HAIR,
BALD
)
/**
* Class which holds information relevant to an editor instance like the input [cachedText], [selection],
* [inputAttributes], [imeOptions], etc. This class is thought to be an improved [EditorInfo]
* object which also holds the state of the currently focused input editor.
*/
class EditorInstance private constructor(private val ims: InputMethodService?) {
val cursorCapsMode: InputAttributes.CapsMode
get() {
val ic = ims?.currentInputConnection ?: return InputAttributes.CapsMode.NONE
return InputAttributes.CapsMode.fromFlags(
ic.getCursorCapsMode(inputAttributes.capsMode.toFlags())
)
}
var currentWord: Region = Region(this)
private set
var imeOptions: ImeOptions = ImeOptions.fromImeOptionsInt(EditorInfo.IME_NULL)
private set
var inputAttributes: InputAttributes = InputAttributes.fromInputTypeInt(InputType.TYPE_NULL)
private set
var isComposingEnabled: Boolean = false
set(v) {
field = v
reevaluate()
if (v) {
markComposingRegion(currentWord)
} else {
markComposingRegion(null)
}
}
var isNewSelectionInBoundsOfOld: Boolean = false
private set
var packageName: String = "undefined"
private set
var selection: Selection = Selection(this)
private set
val cachedText: String
get() = cachedTextInternal.toString()
private var cachedTextInternal: StringBuilder = StringBuilder("")
companion object {
fun default(): EditorInstance {
return EditorInstance(null)
}
fun from(editorInfo: EditorInfo?, ims: InputMethodService?): EditorInstance {
return if (editorInfo == null) { default() } else {
EditorInstance(ims).apply {
imeOptions = ImeOptions.fromImeOptionsInt(editorInfo.imeOptions)
inputAttributes = InputAttributes.fromInputTypeInt(editorInfo.inputType)
packageName = editorInfo.packageName
/*selection = Selection(this).apply {
start = editorInfo.initialSelStart
end = editorInfo.initialSelEnd
}*/
}
}
}
}
init {
fetchExtractedTextFromInputConnection()
reevaluate()
}
/**
* Event handler which reacts to selection updates coming from the target app's editor.
*/
fun onUpdateSelection(
oldSelStart: Int, oldSelEnd: Int,
newSelStart: Int, newSelEnd: Int
) {
fetchExtractedTextFromInputConnection()
isNewSelectionInBoundsOfOld =
newSelStart >= (oldSelStart - 1) &&
newSelStart <= (oldSelStart + 1) &&
newSelEnd >= (oldSelEnd - 1) &&
newSelEnd <= (oldSelEnd + 1)
selection.apply {
start = newSelStart
end = newSelEnd
}
reevaluate()
if (selection.isCursorMode && isComposingEnabled) {
markComposingRegion(currentWord)
} else {
markComposingRegion(null)
}
}
/**
* Completes the given [text] in the current composing region. Does nothing if the current
* composing region is of zero length or null.
*
* @param text The text to complete in this editor's composing region.
*
* @returns True on success, false if an error occurred or the input connection is invalid.
*/
fun commitCompletion(text: String): Boolean {
return false
}
/**
* Commits the given [text] to this editor instance and adjusts both the cursor position and
* composing region, if any.
*
* This method overwrites any selected text and replaces it with given [text]. If there is no
* text selected (selection is in cursor mode), then this method will insert the [text] after
* the cursor, then set the cursor position to the first character after the inserted text.
*
* @param text The text to commit.
*
* @returns True on success, false if an error occurred or the input connection is invalid.
*/
fun commitText(text: String): Boolean {
val ic = ims?.currentInputConnection ?: return false
ic.beginBatchEdit()
markComposingRegion(null)
if (selection.isCursorMode) {
cachedTextInternal.insert(selection.start, text)
} else if (selection.isSelectionMode) {
cachedTextInternal.replace(selection.start, selection.end, text)
}
selection.apply {
start += text.length
end = start
}
reevaluate()
ic.commitText(text, 1)
if (isComposingEnabled) {
markComposingRegion(currentWord)
}
ic.setSelection(selection.start, selection.end)
ic.endBatchEdit()
return true
}
/**
* Executes a backward delete on this editor's text. If a text selection is active, all
* characters inside this selection will be removed, else only the left-most character from
* the cursor's position.
*
* @returns True on success, false if an error occurred or the input connection is invalid.
*/
fun deleteBackwards(): Boolean {
val ic = ims?.currentInputConnection ?: return false
ic.beginBatchEdit()
markComposingRegion(null)
if (selection.isCursorMode && selection.start > 0) {
val length = detectLastUnicodeCharacterLengthBeforeCursor()
cachedTextInternal.replace(selection.start - length, selection.start, "")
selection.apply {
start -= length
end = start
}
ic.deleteSurroundingText(length, 0)
} else if (selection.isSelectionMode) {
cachedTextInternal.replace(selection.start, selection.end, "")
selection.apply {
end = start
}
ic.commitText("", 1)
}
reevaluate()
if (isComposingEnabled) {
markComposingRegion(currentWord)
}
ic.setSelection(selection.start, selection.end)
ic.endBatchEdit()
return true
}
/**
* Deletes [n] words before the current cursor's position.
* NOTE: this implementation does currently only delete currentWord. This is due to change in
* future versions.
*
* @param n The number of words to delete before the cursor. Must be greater than 0 or this
* method will fail.
*
* @returns True on success, false if an error occurred or the input connection is invalid.
*/
fun deleteWordsBeforeCursor(n: Int): Boolean {
val ic = ims?.currentInputConnection ?: return false
if (n < 1) {
return false
}
ic.beginBatchEdit()
markComposingRegion(null)
if (currentWord.isValid) {
cachedTextInternal.replace(currentWord.start, currentWord.end, "")
selection.apply {
start = currentWord.start
end = start
}
ic.setSelection(currentWord.start, currentWord.end)
ic.commitText("", 1)
}
reevaluate()
ic.setSelection(selection.start, selection.end)
ic.endBatchEdit()
return true
}
/**
* Gets [n] characters after the cursor's current position. The resulting string may be any
* length ranging from 0 to n.
*
* @param n The number of characters to get after the cursor. Must be greater than 0 or this
* method will fail.
*
* @returns [n] or less characters after the cursor.
*/
fun getTextAfterCursor(n: Int): String {
if (!selection.isValid || n < 1) {
return ""
}
val from = selection.end
val to = (selection.end + n).coerceAtMost(cachedTextInternal.length)
return cachedTextInternal.substring(from, to)
}
/**
* Gets [n] characters before the cursor's current position. The resulting string may be any
* length ranging from 0 to n.
*
* @param n The number of characters to get before the cursor. Must be greater than 0 or this
* method will fail.
*
* @returns [n] or less characters after the cursor.
*/
fun getTextBeforeCursor(n: Int): String {
if (!selection.isValid || n < 1) {
return ""
}
val from = (selection.start - n).coerceAtLeast(0)
val to = selection.start
return cachedTextInternal.substring(from, to)
}
/**
* Performs a given [action] on the current input editor.
*
* @param action The action to be performed on this editor instance.
*
* @returns True on success, false if an error occurred or the input connection is invalid.
*/
fun performEnterAction(action: ImeOptions.Action): Boolean {
val ic = ims?.currentInputConnection ?: return false
return ic.performEditorAction(action.toInt())
}
/**
* Sends a given [keyCode] as a [KeyEvent.ACTION_DOWN].
*
* @param ic The input connection on which this operation should be performed.
* @param keyCode The key code to send, use a key code defined in Android's [KeyEvent], not in
* [KeyCode] or this call may send a weird character, as this key codes do not match!!
*
* @returns True on success, false if an error occurred or the input connection is invalid.
*/
fun sendSystemKeyEvent(keyCode: Int): Boolean {
val ic = ims?.currentInputConnection ?: return false
return ic.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, keyCode))
}
/**
* Sends a given [keyCode] as a [KeyEvent.ACTION_DOWN] with ALT pressed.
*
* @param ic The input connection on which this operation should be performed.
* @param keyCode The key code to send, use a key code defined in Android's [KeyEvent], not in
* [KeyCode] or this call may send a weird character, as this key codes do not match!!
*
* @returns True on success, false if an error occurred or the input connection is invalid.
*/
fun sendSystemKeyEventAlt(keyCode: Int): Boolean {
val ic = ims?.currentInputConnection ?: return false
return ic.sendKeyEvent(
KeyEvent(
0,
1,
KeyEvent.ACTION_DOWN, keyCode,
0,
KeyEvent.META_ALT_LEFT_ON
)
)
}
/**
* Sets the selection region of this instance and notifies the input connection.
*
* @param from The start index of the selection in characters (inclusive).
* @param to The end index of the selection in characters (exclusive).
*
* @returns True on success, false if an error occurred or the input connection is invalid.
*/
fun setSelection(from: Int, to: Int): Boolean {
val ic = ims?.currentInputConnection ?: return false
selection.apply {
start = from
end = to
}
return ic.setSelection(from, to)
}
/**
* Detects the length of the character before the cursor, as many Unicode characters nowadays
* are longer than 1 Java char and thus the length has to be calculated in order to avoid
* deleting only half of an emoji...
* Is used primarily in [deleteBackwards].
*
* @returns The length of the last Unicode character, in Java characters or 0 if the current
* selection is invalid.
*/
private fun detectLastUnicodeCharacterLengthBeforeCursor(): Int {
if (!selection.isValid) {
return 0
}
var charIndex = 0
var charLength = 0
var charShouldGlue = false
val textToSearch = cachedTextInternal.substring(0, selection.start.coerceAtMost(cachedTextInternal.length))
var i = 0
while (i < textToSearch.length) {
val cp = textToSearch.codePointAt(i)
val cpLength = Character.charCount(cp)
when {
charShouldGlue || cp == VARIATION_SELECTOR || emojiVariationArray.contains(cp) -> {
charLength += cpLength
charShouldGlue = false
}
cp == ZERO_WIDTH_JOINER -> {
charLength += cpLength
charShouldGlue = true
}
else -> {
charIndex = i
charLength = 0
charShouldGlue = false
}
}
i += cpLength
}
return textToSearch.length - charIndex
}
/**
* Gets the current text from the app's editor view.
*
* @returns The target editor's content string.
*/
private fun fetchExtractedTextFromInputConnection() {
val ic = ims?.currentInputConnection ?: return
val et = ic.getExtractedText(
ExtractedTextRequest(), 0
) ?: return
val text = et.text ?: ""
cachedTextInternal.setLength(0)
cachedTextInternal.append(text)
selection.apply {
start = et.selectionStart.coerceAtMost(cachedTextInternal.length)
end = et.selectionEnd.coerceAtMost(cachedTextInternal.length)
}
}
/**
* Marks a given [region] as composing and notifies the input connection.
*
* @param region The region which should be marked as composing.
*
* @returns True on success, false if an error occurred or the input connection is invalid.
*/
private fun markComposingRegion(region: Region?): Boolean {
val ic = ims?.currentInputConnection ?: return false
return when (region) {
null -> ic.finishComposingText()
else -> if (region.isValid) {
ic.setComposingRegion(region.start, region.end)
} else {
ic.finishComposingText()
}
}
}
/**
* Evaluates the current word in this editor instance based on the current cursor position and
* given delimiter [regex].
*
* @param regex The delimiter regex which should be used to split up the content text and find
* words. May differ from locale to locale.
*
* @returns True on success, false if no current word could be found.
*/
private fun reevaluateCurrentWord(regex: Regex): Boolean {
var foundValidWord = false
if (selection.isValid && selection.isCursorMode) {
val words = cachedText.split("((?<=$regex)|(?=$regex))".toRegex())
var pos = 0
for (word in words) {
if (selection.start >= pos && selection.start <= pos + word.length &&
word.isNotEmpty() && !word.matches(regex)) {
currentWord.apply {
start = pos
end = pos + word.length
}
foundValidWord = true
break
} else {
pos += word.length
}
}
}
if (!foundValidWord) {
currentWord.apply {
start = -1
end = -1
}
}
return foundValidWord
}
/**
* Triggers all reevaluation processes.
*/
private fun reevaluate() {
val regex = "[^\\p{L}]".toRegex()
reevaluateCurrentWord(regex)
}
}
/**
* Class which holds the same information as an [EditorInfo.imeOptions] int but more accessible and
* readable.
*/
class ImeOptions private constructor(imeOptions: Int) {
val action: Action = Action.fromInt(imeOptions)
val flagForceAscii: Boolean = imeOptions and EditorInfo.IME_FLAG_FORCE_ASCII > 0
val flagNavigateNext: Boolean = imeOptions and EditorInfo.IME_FLAG_NAVIGATE_NEXT > 0
val flagNavigatePrevious: Boolean = imeOptions and EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS > 0
val flagNoAccessoryAction: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_ACCESSORY_ACTION > 0
val flagNoEnterAction: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_ENTER_ACTION > 0
val flagNoExtractUi: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_EXTRACT_UI > 0
val flagNoFullscreen: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_FULLSCREEN > 0
@RequiresApi(Build.VERSION_CODES.O)
val flagNoPersonalizedLearning: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING > 0
companion object {
fun fromImeOptionsInt(imeOptions: Int): ImeOptions {
return ImeOptions(imeOptions)
}
}
enum class Action {
DONE,
GO,
NEXT,
NONE,
PREVIOUS,
SEARCH,
SEND,
UNSPECIFIED;
companion object {
fun fromInt(raw: Int): Action {
return when (raw and EditorInfo.IME_MASK_ACTION) {
EditorInfo.IME_ACTION_DONE -> DONE
EditorInfo.IME_ACTION_GO -> GO
EditorInfo.IME_ACTION_NEXT -> NEXT
EditorInfo.IME_ACTION_NONE -> NONE
EditorInfo.IME_ACTION_PREVIOUS -> PREVIOUS
EditorInfo.IME_ACTION_SEARCH -> SEARCH
EditorInfo.IME_ACTION_SEND -> SEND
EditorInfo.IME_ACTION_UNSPECIFIED -> UNSPECIFIED
else -> NONE
}
}
}
fun toInt(): Int {
return when (this) {
DONE -> EditorInfo.IME_ACTION_DONE
GO -> EditorInfo.IME_ACTION_GO
NEXT -> EditorInfo.IME_ACTION_NEXT
NONE -> EditorInfo.IME_ACTION_NONE
PREVIOUS -> EditorInfo.IME_ACTION_PREVIOUS
SEARCH -> EditorInfo.IME_ACTION_SEARCH
SEND -> EditorInfo.IME_ACTION_SEND
UNSPECIFIED-> EditorInfo.IME_ACTION_UNSPECIFIED
}
}
}
}
/**
* Class which holds the same information as an [EditorInfo.inputType] int but more accessible and
* readable.
*/
class InputAttributes private constructor(inputType: Int) {
val type: Type
val variation: Variation
val capsMode: CapsMode
var flagNumberDecimal: Boolean = false
private set
var flagNumberSigned: Boolean = false
private set
var flagTextAutoComplete: Boolean = false
private set
var flagTextAutoCorrect: Boolean = false
private set
var flagTextImeMultiLine: Boolean = false
private set
var flagTextMultiLine: Boolean = false
private set
var flagTextNoSuggestions: Boolean = false
private set
init {
when (inputType and InputType.TYPE_MASK_CLASS) {
InputType.TYPE_CLASS_DATETIME -> {
type = Type.DATETIME
variation = when (inputType and InputType.TYPE_MASK_VARIATION) {
InputType.TYPE_DATETIME_VARIATION_DATE -> Variation.DATE
InputType.TYPE_DATETIME_VARIATION_NORMAL -> Variation.NORMAL
InputType.TYPE_DATETIME_VARIATION_TIME -> Variation.TIME
else -> Variation.NORMAL
}
capsMode = CapsMode.NONE
}
InputType.TYPE_CLASS_NUMBER -> {
type = Type.NUMBER
variation = when (inputType and InputType.TYPE_MASK_VARIATION) {
InputType.TYPE_NUMBER_VARIATION_NORMAL -> Variation.NORMAL
InputType.TYPE_NUMBER_VARIATION_PASSWORD -> Variation.PASSWORD
else -> Variation.NORMAL
}
capsMode = CapsMode.NONE
flagNumberDecimal = inputType and InputType.TYPE_NUMBER_FLAG_DECIMAL > 0
flagNumberSigned = inputType and InputType.TYPE_NUMBER_FLAG_SIGNED > 0
}
InputType.TYPE_CLASS_PHONE -> {
type = Type.PHONE
variation = Variation.NORMAL
capsMode = CapsMode.NONE
}
InputType.TYPE_CLASS_TEXT -> {
type = Type.TEXT
variation = when (inputType and InputType.TYPE_MASK_VARIATION) {
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS -> Variation.EMAIL_ADDRESS
InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT -> Variation.EMAIL_SUBJECT
InputType.TYPE_TEXT_VARIATION_FILTER -> Variation.FILTER
InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE -> Variation.LONG_MESSAGE
InputType.TYPE_TEXT_VARIATION_NORMAL -> Variation.NORMAL
InputType.TYPE_TEXT_VARIATION_PASSWORD -> Variation.PASSWORD
InputType.TYPE_TEXT_VARIATION_PERSON_NAME -> Variation.PERSON_NAME
InputType.TYPE_TEXT_VARIATION_PHONETIC -> Variation.PHONETIC
InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS -> Variation.POSTAL_ADDRESS
InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE -> Variation.SHORT_MESSAGE
InputType.TYPE_TEXT_VARIATION_URI -> Variation.URI
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD -> Variation.VISIBLE_PASSWORD
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT -> Variation.WEB_EDIT_TEXT
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS -> Variation.WEB_EMAIL_ADDRESS
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD -> Variation.WEB_PASSWORD
else -> Variation.NORMAL
}
capsMode = CapsMode.fromFlags(inputType)
flagTextAutoComplete = inputType and InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE > 0
flagTextAutoCorrect = inputType and InputType.TYPE_TEXT_FLAG_AUTO_CORRECT > 0
flagTextImeMultiLine = inputType and InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE > 0
flagTextMultiLine = inputType and InputType.TYPE_TEXT_FLAG_MULTI_LINE > 0
flagTextNoSuggestions = inputType and InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS > 0
}
else -> {
type = Type.TEXT
variation = Variation.NORMAL
capsMode = CapsMode.NONE
}
}
}
companion object {
fun fromInputTypeInt(inputType: Int): InputAttributes {
return InputAttributes(inputType)
}
}
enum class Type {
DATETIME,
NUMBER,
PHONE,
TEXT;
}
enum class Variation {
DATE,
EMAIL_ADDRESS,
EMAIL_SUBJECT,
FILTER,
LONG_MESSAGE,
NORMAL,
PASSWORD,
PERSON_NAME,
PHONETIC,
POSTAL_ADDRESS,
SHORT_MESSAGE,
TIME,
URI,
VISIBLE_PASSWORD,
WEB_EDIT_TEXT,
WEB_EMAIL_ADDRESS,
WEB_PASSWORD;
}
enum class CapsMode {
ALL,
NONE,
SENTENCES,
WORDS;
companion object {
fun fromFlags(flags: Int): CapsMode {
return when {
flags and InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS > 0 -> ALL
flags and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES > 0 -> SENTENCES
flags and InputType.TYPE_TEXT_FLAG_CAP_WORDS > 0 -> WORDS
else -> NONE
}
}
}
fun toFlags(): Int {
return when (this) {
ALL -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
SENTENCES -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
WORDS -> InputType.TYPE_TEXT_FLAG_CAP_WORDS
else -> 0
}
}
}
}
/**
* Class which marks a region of the [text] in [editorInstance].
*/
open class Region(private val editorInstance: EditorInstance) {
var start: Int = -1
var end: Int = -1
val isValid: Boolean
get() = start >= 0 && end >= 0 && length >= 0
val length: Int
get() = end - start
val text: String
get() {
val eiText = editorInstance.cachedText
return if (!isValid || start >= eiText.length) {
""
} else {
val end = if (end >= eiText.length) { eiText.length } else { end }
editorInstance.cachedText.substring(start, end)
}
}
override operator fun equals(other: Any?): Boolean {
return if (other is Region) {
start == other.start && end == other.end
} else {
super.equals(other)
}
}
override fun hashCode(): Int {
var result = start
result = 31 * result + end
return result
}
}
/**
* Class which holds selection attributes and returns the correct text for set selection based on
* the text in [editorInstance].
*/
class Selection(private val editorInstance: EditorInstance) : Region(editorInstance) {
val isCursorMode: Boolean
get() = length == 0 && isValid
val isSelectionMode: Boolean
get() = length != 0 && isValid
}

View File

@@ -20,6 +20,7 @@ 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.inputmethodservice.InputMethodService
import android.media.AudioManager
@@ -29,16 +30,15 @@ import android.util.Log
import android.view.Gravity
import android.view.View
import android.view.WindowManager
import android.view.inputmethod.CursorAnchorInfo
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import android.widget.ImageButton
import android.widget.LinearLayout
import com.squareup.moshi.Json
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.media.MediaInputManager
import dev.patrickgold.florisboard.ime.text.TextInputManager
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyData
import dev.patrickgold.florisboard.settings.SettingsMainActivity
@@ -59,8 +59,10 @@ class FlorisBoard : InputMethodService() {
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<EventListener> = mutableListOf()
private var audioManager: AudioManager? = null
@@ -68,8 +70,11 @@ class FlorisBoard : InputMethodService() {
private var vibrator: Vibrator? = null
private val osHandler = Handler()
var activeEditorInstance: EditorInstance = EditorInstance.default()
lateinit var subtypeManager: SubtypeManager
lateinit var activeSubtype: Subtype
private var currentThemeIsNight: Boolean = false
private var currentThemeResId: Int = 0
val textInputManager: TextInputManager
@@ -84,6 +89,7 @@ class FlorisBoard : InputMethodService() {
companion object {
private const val IME_ID: String = "dev.patrickgold.florisboard/.ime.core.FlorisBoard"
private val TAG: String? = FlorisBoard::class.simpleName
fun checkIfImeIsEnabled(context: Context): Boolean {
val activeImeIds = Settings.Secure.getString(
@@ -112,6 +118,13 @@ class FlorisBoard : InputMethodService() {
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() {
@@ -133,96 +146,123 @@ class FlorisBoard : InputMethodService() {
.build()
)
}
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onCreate()")
if (BuildConfig.DEBUG) Log.i(TAG, "onCreate()")
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
prefs = PrefHelper(this)
prefs = PrefHelper.getDefaultInstance(this)
prefs.initDefaultPreferences()
prefs.sync()
subtypeManager = SubtypeManager(this, prefs)
activeSubtype = subtypeManager.getActiveSubtype() ?: Subtype.DEFAULT
currentThemeResId = prefs.theme.getSelectedThemeResId()
currentThemeIsNight = prefs.internal.themeCurrentIsNight
currentThemeResId = getDayNightBaseThemeId(currentThemeIsNight)
setTheme(currentThemeResId)
updateTheme()
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
super.onCreate()
eventListeners.forEach { it.onCreate() }
eventListeners.toList().forEach { it.onCreate() }
}
@SuppressLint("InflateParams")
override fun onCreateInputView(): View? {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onCreateInputView()")
if (BuildConfig.DEBUG) Log.i(TAG, "onCreateInputView()")
baseContext.setTheme(currentThemeResId)
inputView = layoutInflater.inflate(R.layout.florisboard, null) as InputView
inputWindowView = layoutInflater.inflate(R.layout.florisboard, null) as InputWindowView
eventListeners.forEach { it.onCreateInputView() }
eventListeners.toList().forEach { it.onCreateInputView() }
return inputView
return inputWindowView
}
fun registerInputView(inputView: InputView) {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "registerInputView(inputView)")
if (BuildConfig.DEBUG) Log.i(TAG, "registerInputView($inputView)")
this.inputView = inputView
initializeOneHandedEnvironment()
updateTheme()
updateSoftInputWindowLayoutParameters()
updateOneHandedPanelVisibility()
eventListeners.forEach { it.onRegisterInputView(inputView) }
eventListeners.toList().forEach { it.onRegisterInputView(inputView) }
}
override fun onDestroy() {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onDestroy()")
if (BuildConfig.DEBUG) Log.i(TAG, "onDestroy()")
osHandler.removeCallbacksAndMessages(null)
florisboardInstance = null
eventListeners.toList().forEach { it.onDestroy() }
eventListeners.clear()
super.onDestroy()
eventListeners.forEach { it.onDestroy() }
}
override fun onStartInput(attribute: EditorInfo?, restarting: Boolean) {
if (BuildConfig.DEBUG) Log.i(TAG, "onStartInput($attribute, $restarting)")
super.onStartInput(attribute, restarting)
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_IMMEDIATE)
}
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_MONITOR)
if (BuildConfig.DEBUG) Log.i(TAG, "onStartInputView($info, $restarting)")
Log.i(TAG, "onStartInputView: " + info?.debugSummarize())
super.onStartInputView(info, restarting)
eventListeners.forEach { it.onStartInputView(info, restarting) }
activeEditorInstance = EditorInstance.from(info, this)
eventListeners.toList().forEach {
it.onStartInputView(activeEditorInstance, restarting)
}
}
override fun onFinishInputView(finishingInput: Boolean) {
currentInputConnection?.requestCursorUpdates(0)
if (BuildConfig.DEBUG) Log.i(TAG, "onFinishInputView($finishingInput)")
if (finishingInput) {
activeEditorInstance = EditorInstance.default()
}
super.onFinishInputView(finishingInput)
eventListeners.forEach { it.onFinishInputView(finishingInput) }
eventListeners.toList().forEach { it.onFinishInputView(finishingInput) }
}
override fun onFinishInput() {
if (BuildConfig.DEBUG) Log.i(TAG, "onFinishInput()")
super.onFinishInput()
currentInputConnection?.requestCursorUpdates(0)
}
override fun onWindowShown() {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onWindowShown()")
if (BuildConfig.DEBUG) Log.i(TAG, "onWindowShown()")
prefs.sync()
updateThemeIfNecessary()
updateTheme()
updateOneHandedPanelVisibility()
activeSubtype = subtypeManager.getActiveSubtype() ?: Subtype.DEFAULT
onSubtypeChanged(activeSubtype)
setActiveInput(R.id.text_input)
super.onWindowShown()
eventListeners.forEach { it.onWindowShown() }
eventListeners.toList().forEach { it.onWindowShown() }
}
override fun onWindowHidden() {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onWindowHidden()")
if (BuildConfig.DEBUG) Log.i(TAG, "onWindowHidden()")
super.onWindowHidden()
eventListeners.forEach { it.onWindowHidden() }
eventListeners.toList().forEach { it.onWindowHidden() }
}
override fun onConfigurationChanged(newConfig: Configuration) {
if (BuildConfig.DEBUG) Log.i(TAG, "onConfigurationChanged($newConfig)")
if (isInputViewShown) {
updateOneHandedPanelVisibility()
}
@@ -230,74 +270,72 @@ class FlorisBoard : InputMethodService() {
super.onConfigurationChanged(newConfig)
}
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
super.onUpdateCursorAnchorInfo(cursorAnchorInfo)
eventListeners.forEach { it.onUpdateCursorAnchorInfo(cursorAnchorInfo) }
}
override fun onUpdateSelection(
oldSelStart: Int,
oldSelEnd: Int,
newSelStart: Int,
newSelEnd: Int,
candidatesStart: Int,
candidatesEnd: Int
oldSelStart: Int, oldSelEnd: Int,
newSelStart: Int, newSelEnd: Int,
candidatesStart: Int, candidatesEnd: Int
) {
if (BuildConfig.DEBUG) Log.i(TAG, "onUpdateSelection($oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd)")
super.onUpdateSelection(
oldSelStart,
oldSelEnd,
newSelStart,
newSelEnd,
candidatesStart,
candidatesEnd
oldSelStart, oldSelEnd,
newSelStart, newSelEnd,
candidatesStart, candidatesEnd
)
eventListeners.forEach {
it.onUpdateSelection(
oldSelStart,
oldSelEnd,
newSelStart,
newSelEnd,
candidatesStart,
candidatesEnd
)
}
activeEditorInstance.onUpdateSelection(
oldSelStart, oldSelEnd,
newSelStart, newSelEnd
)
eventListeners.toList().forEach { it.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.
* Reapplies the supplies colors and settings from prefs to navigation bar.
*/
private fun updateThemeIfNecessary() {
val newThemeResId = prefs.theme.getSelectedThemeResId()
if (newThemeResId != currentThemeResId) {
currentThemeResId = newThemeResId
private fun updateTheme() {
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
}
val w = window?.window ?: return
inputView?.setBackgroundColor(prefs.theme.keyboardBgColor)
w.navigationBarColor = prefs.theme.navBarColor
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
var flags = w.decorView.systemUiVisibility
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()
}
w.decorView.systemUiVisibility = flags
}
inputView?.oneHandedCtrlPanelStart?.setBackgroundColor(prefs.theme.oneHandedBgColor)
inputView?.oneHandedCtrlPanelEnd?.setBackgroundColor(prefs.theme.oneHandedBgColor)
inputView?.findViewById<ImageButton>(R.id.one_handed_ctrl_move_start)
?.imageTintList = ColorStateList.valueOf(prefs.theme.oneHandedButtonFgColor)
inputView?.findViewById<ImageButton>(R.id.one_handed_ctrl_move_end)
?.imageTintList = ColorStateList.valueOf(prefs.theme.oneHandedButtonFgColor)
inputView?.findViewById<ImageButton>(R.id.one_handed_ctrl_close_start)
?.imageTintList = ColorStateList.valueOf(prefs.theme.oneHandedButtonFgColor)
inputView?.findViewById<ImageButton>(R.id.one_handed_ctrl_close_end)
?.imageTintList = ColorStateList.valueOf(prefs.theme.oneHandedButtonFgColor)
eventListeners.toList().forEach { it.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
}
@@ -313,8 +351,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 {
@@ -323,7 +361,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)
}
}
@@ -331,9 +369,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 == -1 && prefs.looknfeel.vibrationEnabledSystem) {
if (prefs.keyboard.vibrationEnabled) {
var vibrationStrength = prefs.keyboard.vibrationStrength
if (vibrationStrength == -1 && prefs.keyboard.vibrationEnabledSystem) {
vibrationStrength = 36
}
if (vibrationStrength > 0) {
@@ -355,15 +393,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 == -1 && prefs.looknfeel.soundEnabledSystem) {
if (soundVolume == -1 && prefs.keyboard.soundEnabledSystem) {
audioManager!!.playSoundEffect(effect)
} else if (soundVolume > 0) {
audioManager!!.playSoundEffect(effect, soundVolume / 100f)
@@ -371,6 +409,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].
*/
@@ -390,6 +441,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)
@@ -427,26 +483,26 @@ class FlorisBoard : InputMethodService() {
private fun onOneHandedPanelButtonClick(v: View) {
when (v.id) {
R.id.one_handed_ctrl_move_start -> {
prefs.looknfeel.oneHandedMode = "start"
prefs.keyboard.oneHandedMode = "start"
}
R.id.one_handed_ctrl_move_end -> {
prefs.looknfeel.oneHandedMode = "end"
prefs.keyboard.oneHandedMode = "end"
}
R.id.one_handed_ctrl_close_start,
R.id.one_handed_ctrl_close_end -> {
prefs.looknfeel.oneHandedMode = "off"
prefs.keyboard.oneHandedMode = "off"
}
}
updateOneHandedPanelVisibility()
}
fun toggleOneHandedMode() {
when (prefs.looknfeel.oneHandedMode) {
when (prefs.keyboard.oneHandedMode) {
"off" -> {
prefs.looknfeel.oneHandedMode = "end"
prefs.keyboard.oneHandedMode = "end"
}
else -> {
prefs.looknfeel.oneHandedMode = "off"
prefs.keyboard.oneHandedMode = "off"
}
}
updateOneHandedPanelVisibility()
@@ -457,7 +513,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
@@ -505,22 +561,15 @@ class FlorisBoard : InputMethodService() {
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 onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {}
fun onUpdateSelection(
oldSelStart: Int,
oldSelEnd: Int,
newSelStart: Int,
newSelEnd: Int,
candidatesStart: Int,
candidatesEnd: Int
) {}
fun onUpdateSelection() {}
fun onApplyThemeAttributes() {}
fun onSubtypeChanged(newSubtype: Subtype) {}
}

View File

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

View File

@@ -17,20 +17,31 @@
package dev.patrickgold.florisboard.ime.core
import android.content.Context
import android.content.res.Configuration
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
import dev.patrickgold.florisboard.util.ViewLayoutUtils
import kotlin.math.roundToInt
/**
* Root view of the keyboard. Notifies [FlorisBoard] when it has been attached to a window.
*/
class InputView : FrameLayout {
class InputView : LinearLayout {
private var florisboard: FlorisBoard = FlorisBoard.getInstance()
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
var desiredInputViewHeight: Int = resources.getDimension(R.dimen.inputView_baseHeight).roundToInt()
private set
var desiredSmartbarHeight: Int = resources.getDimension(R.dimen.smartbar_baseHeight).roundToInt()
private set
var desiredTextKeyboardViewHeight: Int = resources.getDimension(R.dimen.textKeyboardView_baseHeight).roundToInt()
private set
var desiredMediaKeyboardViewHeight: Int = resources.getDimension(R.dimen.mediaKeyboardView_baseHeight).roundToInt()
private set
var mainViewFlipper: ViewFlipper? = null
private set
@@ -54,4 +65,34 @@ class InputView : FrameLayout {
florisboard.registerInputView(this)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val heightFactor = when (resources.configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> 0.85f
else -> if (prefs.keyboard.oneHandedMode != "off") {
0.9f
} else {
1.0f
}
} * when (prefs.keyboard.heightFactor) {
"extra_short" -> 0.85f
"short" -> 0.90f
"mid_short" -> 0.95f
"normal" -> 1.00f
"mid_tall" -> 1.05f
"tall" -> 1.10f
"extra_tall" -> 1.15f
else -> 1.00f
}
var height = (resources.getDimension(R.dimen.inputView_baseHeight) * heightFactor).roundToInt()
desiredInputViewHeight = height
desiredSmartbarHeight = (0.16129 * height).roundToInt()
desiredTextKeyboardViewHeight = height - desiredSmartbarHeight
desiredMediaKeyboardViewHeight = height
// Add bottom offset for curved screens here. As the desired heights have already been set,
// adding a value to the height now will result in a bottom padding (aka offset).
height += ViewLayoutUtils.convertDpToPixel(florisboard.prefs.keyboard.bottomOffset.toFloat(), context).toInt()
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY))
}
}

View File

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

View File

@@ -21,6 +21,9 @@ import android.content.SharedPreferences
import android.provider.Settings
import androidx.preference.PreferenceManager
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.text.gestures.DistanceThreshold
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.gestures.VelocityThreshold
import dev.patrickgold.florisboard.util.VersionName
import kotlin.collections.HashMap
@@ -37,11 +40,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 suggestion = Suggestion(this)
val popup = PopUp(this)
val theme = Theme(this)
/**
@@ -111,15 +115,28 @@ class PrefHelper(
}
}
companion object {
private var defaultInstance: PrefHelper? = null
@Synchronized
fun getDefaultInstance(context: Context): PrefHelper {
if (defaultInstance == null) {
defaultInstance = PrefHelper(context)
}
return defaultInstance!!
}
}
/**
* Tells the [PreferenceManager] to set the defined preferences to their default values, if
* they have not been initialized yet.
*/
fun initDefaultPreferences() {
PreferenceManager.setDefaultValues(context, R.xml.prefs_advanced, true)
PreferenceManager.setDefaultValues(context, R.xml.prefs_gestures, true)
PreferenceManager.setDefaultValues(context, R.xml.prefs_keyboard, true)
PreferenceManager.setDefaultValues(context, R.xml.prefs_looknfeel, true)
PreferenceManager.setDefaultValues(context, R.xml.prefs_theme, true)
PreferenceManager.setDefaultValues(context, R.xml.prefs_typing, true)
//setPref(Keyboard.SUBTYPES, "")
//setPref(Internal.IS_IME_SET_UP, false)
}
@@ -129,10 +146,10 @@ class PrefHelper(
*/
fun sync() {
val contentResolver = context.contentResolver
looknfeel.soundEnabledSystem = Settings.System.getInt(
keyboard.soundEnabledSystem = Settings.System.getInt(
contentResolver, Settings.System.SOUND_EFFECTS_ENABLED, 0
) != 0
looknfeel.vibrationEnabledSystem = Settings.System.getInt(
keyboard.vibrationEnabledSystem = Settings.System.getInt(
contentResolver, Settings.System.HAPTIC_FEEDBACK_ENABLED, 0
) != 0
@@ -163,37 +180,120 @@ class PrefHelper(
*/
class Correction(private val prefHelper: PrefHelper) {
companion object {
const val DOUBLE_SPACE_PERIOD = "correction__double_space_period"
const val AUTO_CAPITALIZATION = "correction__auto_capitalization"
const val DOUBLE_SPACE_PERIOD = "correction__double_space_period"
const val REMEMBER_CAPS_LOCK_STATE = "correction__remember_caps_lock_state"
}
var doubleSpacePeriod: Boolean = false
get() = prefHelper.getPref(DOUBLE_SPACE_PERIOD, true)
private set
var autoCapitalization: Boolean
get() = prefHelper.getPref(AUTO_CAPITALIZATION, true)
set(v) = prefHelper.setPref(AUTO_CAPITALIZATION, v)
var doubleSpacePeriod: Boolean
get() = prefHelper.getPref(DOUBLE_SPACE_PERIOD, true)
set(v) = prefHelper.setPref(DOUBLE_SPACE_PERIOD, v)
var rememberCapsLockState: Boolean
get() = prefHelper.getPref(REMEMBER_CAPS_LOCK_STATE, false)
set(v) = prefHelper.setPref(REMEMBER_CAPS_LOCK_STATE, v)
}
/**
* Wrapper class for internal preferences.
* Wrapper class for gestures preferences.
*/
class Gestures(private val prefHelper: PrefHelper) {
companion object {
const val SWIPE_UP = "gestures__swipe_up"
const val SWIPE_DOWN = "gestures__swipe_down"
const val SWIPE_LEFT = "gestures__swipe_left"
const val SWIPE_RIGHT = "gestures__swipe_right"
const val SPACE_BAR_SWIPE_LEFT = "gestures__space_bar_swipe_left"
const val SPACE_BAR_SWIPE_RIGHT = "gestures__space_bar_swipe_right"
const val DELETE_KEY_SWIPE_LEFT = "gestures__delete_key_swipe_left"
const val SWIPE_VELOCITY_THRESHOLD = "gestures__swipe_velocity_threshold"
const val SWIPE_DISTANCE_THRESHOLD = "gestures__swipe_distance_threshold"
}
var swipeUp: SwipeAction
get() = SwipeAction.fromString(prefHelper.getPref(SWIPE_UP, "no_action"))
set(v) = prefHelper.setPref(SWIPE_UP, v)
var swipeDown: SwipeAction
get() = SwipeAction.fromString(prefHelper.getPref(SWIPE_DOWN, "no_action"))
set(v) = prefHelper.setPref(SWIPE_DOWN, v)
var swipeLeft: SwipeAction
get() = SwipeAction.fromString(prefHelper.getPref(SWIPE_LEFT, "no_action"))
set(v) = prefHelper.setPref(SWIPE_LEFT, v)
var swipeRight: SwipeAction
get() = SwipeAction.fromString(prefHelper.getPref(SWIPE_RIGHT, "no_action"))
set(v) = prefHelper.setPref(SWIPE_RIGHT, v)
var spaceBarSwipeLeft: SwipeAction
get() = SwipeAction.fromString(prefHelper.getPref(SPACE_BAR_SWIPE_LEFT, "no_action"))
set(v) = prefHelper.setPref(SPACE_BAR_SWIPE_LEFT, v)
var spaceBarSwipeRight: SwipeAction
get() = SwipeAction.fromString(prefHelper.getPref(SPACE_BAR_SWIPE_RIGHT, "no_action"))
set(v) = prefHelper.setPref(SPACE_BAR_SWIPE_RIGHT, v)
var deleteKeySwipeLeft: SwipeAction
get() = SwipeAction.fromString(prefHelper.getPref(DELETE_KEY_SWIPE_LEFT, "no_action"))
set(v) = prefHelper.setPref(DELETE_KEY_SWIPE_LEFT, v)
var swipeVelocityThreshold: VelocityThreshold
get() = VelocityThreshold.fromString(prefHelper.getPref(SWIPE_VELOCITY_THRESHOLD, "normal"))
set(v) = prefHelper.setPref(SWIPE_VELOCITY_THRESHOLD, v)
var swipeDistanceThreshold: DistanceThreshold
get() = DistanceThreshold.fromString(prefHelper.getPref(SWIPE_DISTANCE_THRESHOLD, "normal"))
set(v) = prefHelper.setPref(SWIPE_DISTANCE_THRESHOLD, v)
}
/**
* Wrapper class for glide preferences.
*/
class Glide(private val prefHelper: PrefHelper) {
companion object {
const val ENABLED = "glide__enabled"
const val SHOW_TRAIL = "glide__show_trail"
}
var enabled: Boolean
get() = prefHelper.getPref(ENABLED, false)
set(v) = prefHelper.setPref(ENABLED, v)
var showTrail: Boolean
get() = prefHelper.getPref(SHOW_TRAIL, false)
set(v) = prefHelper.setPref(SHOW_TRAIL, v)
}
/**
* Wrapper class for internal preferences. A preference qualifies as an internal pref if the
* user has no ability to control this preference's value directly (via a UI pref view).
*/
class Internal(private val prefHelper: PrefHelper) {
companion object {
const val IS_IME_SET_UP = "internal__is_ime_set_up"
const val VERSION_ON_INSTALL = "internal__version_on_install"
const val VERSION_LAST_USE = "internal__version_last_use"
const val VERSION_LAST_CHANGELOG = "internal__version_last_changelog"
const val IS_IME_SET_UP = "internal__is_ime_set_up"
const val THEME_CURRENT_BASED_ON = "internal__theme_current_based_on"
const val THEME_CURRENT_IS_MODIFIED = "internal__theme_current_is_modified"
const val THEME_CURRENT_IS_NIGHT = "internal__theme_current_is_night"
const val VERSION_ON_INSTALL = "internal__version_on_install"
const val VERSION_LAST_USE = "internal__version_last_use"
const val VERSION_LAST_CHANGELOG = "internal__version_last_changelog"
}
var isImeSetUp: Boolean
get() = prefHelper.getPref(IS_IME_SET_UP, false)
set(value) = prefHelper.setPref(IS_IME_SET_UP, value)
get() = prefHelper.getPref(IS_IME_SET_UP, false)
set(v) = prefHelper.setPref(IS_IME_SET_UP, v)
var themeCurrentBasedOn: String
get() = prefHelper.getPref(THEME_CURRENT_BASED_ON, "undefined")
set(v) = prefHelper.setPref(THEME_CURRENT_BASED_ON, v)
var themeCurrentIsModified: Boolean
get() = prefHelper.getPref(THEME_CURRENT_IS_MODIFIED, false)
set(v) = prefHelper.setPref(THEME_CURRENT_IS_MODIFIED, v)
var themeCurrentIsNight: Boolean
get() = prefHelper.getPref(THEME_CURRENT_IS_NIGHT, false)
set(v) = prefHelper.setPref(THEME_CURRENT_IS_NIGHT, v)
var versionOnInstall: String
get() = prefHelper.getPref(VERSION_ON_INSTALL, VersionName.DEFAULT_RAW)
set(value) = prefHelper.setPref(VERSION_ON_INSTALL, value)
get() = prefHelper.getPref(VERSION_ON_INSTALL, VersionName.DEFAULT_RAW)
set(v) = prefHelper.setPref(VERSION_ON_INSTALL, v)
var versionLastUse: String
get() = prefHelper.getPref(VERSION_LAST_USE, VersionName.DEFAULT_RAW)
set(value) = prefHelper.setPref(VERSION_LAST_USE, value)
get() = prefHelper.getPref(VERSION_LAST_USE, VersionName.DEFAULT_RAW)
set(v) = prefHelper.setPref(VERSION_LAST_USE, v)
var versionLastChangelog: String
get() = prefHelper.getPref(VERSION_LAST_CHANGELOG, VersionName.DEFAULT_RAW)
set(value) = prefHelper.setPref(VERSION_LAST_CHANGELOG, value)
get() = prefHelper.getPref(VERSION_LAST_CHANGELOG, VersionName.DEFAULT_RAW)
set(v) = prefHelper.setPref(VERSION_LAST_CHANGELOG, v)
}
/**
@@ -201,41 +301,40 @@ class PrefHelper(
*/
class Keyboard(private val prefHelper: PrefHelper) {
companion object {
const val ACTIVE_SUBTYPE_ID = "keyboard__active_subtype_id"
const val SUBTYPES = "keyboard__subtypes"
}
var activeSubtypeId: Int
get() = prefHelper.getPref(ACTIVE_SUBTYPE_ID, -1)
set(v) = prefHelper.setPref(ACTIVE_SUBTYPE_ID, v)
var subtypes: String
get() = prefHelper.getPref(SUBTYPES, "")
set(v) = prefHelper.setPref(SUBTYPES, v)
}
/**
* Wrapper class for looknfeel preferences.
*/
class Looknfeel(private val prefHelper: PrefHelper) {
companion object {
const val HEIGHT_FACTOR = "looknfeel__height_factor"
const val LONG_PRESS_DELAY = "looknfeel__long_press_delay"
const val ONE_HANDED_MODE = "looknfeel__one_handed_mode"
const val SOUND_ENABLED = "looknfeel__sound_enabled"
const val SOUND_VOLUME = "looknfeel__sound_volume"
const val VIBRATION_ENABLED = "looknfeel__vibration_enabled"
const val VIBRATION_STRENGTH = "looknfeel__vibration_strength"
const val BOTTOM_OFFSET = "keyboard__bottom_offset"
const val HEIGHT_FACTOR = "keyboard__height_factor"
const val HINTED_NUMBER_ROW = "keyboard__hinted_number_row"
const val HINTED_SYMBOLS = "keyboard__hinted_symbols"
const val LONG_PRESS_DELAY = "keyboard__long_press_delay"
const val ONE_HANDED_MODE = "keyboard__one_handed_mode"
const val POPUP_ENABLED = "keyboard__popup_enabled"
const val SOUND_ENABLED = "keyboard__sound_enabled"
const val SOUND_VOLUME = "keyboard__sound_volume"
const val VIBRATION_ENABLED = "keyboard__vibration_enabled"
const val VIBRATION_STRENGTH = "keyboard__vibration_strength"
}
var bottomOffset: Int = 0
get() = prefHelper.getPref(BOTTOM_OFFSET, 0)
private set
var heightFactor: String = ""
get() = prefHelper.getPref(HEIGHT_FACTOR, "normal")
private set
var hintedNumberRow: Boolean
get() = prefHelper.getPref(HINTED_NUMBER_ROW, true)
set(v) = prefHelper.setPref(HINTED_NUMBER_ROW, v)
var hintedSymbols: Boolean
get() = prefHelper.getPref(HINTED_SYMBOLS, true)
set(v) = prefHelper.setPref(HINTED_SYMBOLS, v)
var longPressDelay: Int = 0
get() = prefHelper.getPref(LONG_PRESS_DELAY, 300)
private set
var oneHandedMode: String
get() = prefHelper.getPref(ONE_HANDED_MODE, "off")
set(value) = prefHelper.setPref(ONE_HANDED_MODE, value)
var popupEnabled: Boolean = false
get() = prefHelper.getPref(POPUP_ENABLED, true)
private set
var soundEnabled: Boolean = false
get() = prefHelper.getPref(SOUND_ENABLED, true)
private set
@@ -252,6 +351,23 @@ class PrefHelper(
private set
}
/**
* Wrapper class for localization preferences.
*/
class Localization(private val prefHelper: PrefHelper) {
companion object {
const val ACTIVE_SUBTYPE_ID = "localization__active_subtype_id"
const val SUBTYPES = "localization__subtypes"
}
var activeSubtypeId: Int
get() = prefHelper.getPref(ACTIVE_SUBTYPE_ID, Subtype.DEFAULT.id)
set(v) = prefHelper.setPref(ACTIVE_SUBTYPE_ID, v)
var subtypes: String
get() = prefHelper.getPref(SUBTYPES, "")
set(v) = prefHelper.setPref(SUBTYPES, v)
}
/**
* Wrapper class for suggestion preferences.
*/
@@ -273,36 +389,124 @@ class PrefHelper(
private set
}
/**
* Wrapper class for popup preferences.
*/
class PopUp(private val prefHelper: PrefHelper) {
companion object {
const val ENABLED = "popup__enabled"
}
var enabled: Boolean = false
get() = prefHelper.getPref(ENABLED, true)
private set
}
/**
* Wrapper class for theme preferences.
*/
class Theme(private val prefHelper: PrefHelper) {
companion object {
const val NAME = "theme__name"
const val COLOR_PRIMARY = "theme__colorPrimary"
const val COLOR_PRIMARY_DARK = "theme__colorPrimaryDark"
const val COLOR_ACCENT = "theme__colorAccent"
const val NAV_BAR_COLOR = "theme__navBarColor"
const val NAV_BAR_IS_LIGHT = "theme__navBarIsLight"
const val KEYBOARD_BG_COLOR = "theme__keyboard_bgColor"
const val KEY_BG_COLOR = "theme__key_bgColor"
const val KEY_BG_COLOR_PRESSED = "theme__key_bgColorPressed"
const val KEY_FG_COLOR = "theme__key_fgColor"
const val KEY_ENTER_BG_COLOR = "theme__keyEnter_bgColor"
const val KEY_ENTER_BG_COLOR_PRESSED = "theme__keyEnter_bgColorPressed"
const val KEY_ENTER_FG_COLOR = "theme__keyEnter_fgColor"
const val KEY_SHIFT_BG_COLOR = "theme__keyShift_bgColor"
const val KEY_SHIFT_BG_COLOR_PRESSED = "theme__keyShift_bgColorPressed"
const val KEY_SHIFT_FG_COLOR = "theme__keyShift_fgColor"
const val KEY_SHIFT_FG_COLOR_CAPSLOCK = "theme__keyShift_fgColorCapsLock"
const val KEY_POPUP_BG_COLOR = "theme__keyPopup_bgColor"
const val KEY_POPUP_BG_COLOR_ACTIVE = "theme__keyPopup_bgColorActive"
const val KEY_POPUP_FG_COLOR = "theme__keyPopup_fgColor"
const val MEDIA_FG_COLOR = "theme__media_fgColor"
const val MEDIA_FG_COLOR_ALT = "theme__media_fgColorAlt"
const val ONE_HANDED_BG_COLOR = "theme__oneHanded_bgColor"
const val ONE_HANDED_BUTTON_FG_COLOR = "theme__oneHandedButton_fgColor"
const val SMARTBAR_BG_COLOR = "theme__smartbar_bgColor"
const val SMARTBAR_FG_COLOR = "theme__smartbar_fgColor"
const val SMARTBAR_FG_COLOR_ALT = "theme__smartbar_fgColorAlt"
const val SMARTBAR_BUTTON_BG_COLOR = "theme__smartbarButton_bgColor"
const val SMARTBAR_BUTTON_FG_COLOR = "theme__smartbarButton_fgColor"
}
var name: String = ""
get() = prefHelper.getPref(NAME, "floris_light")
private set
fun getSelectedThemeResId(): Int {
return when (name) {
"floris_light" -> R.style.KeyboardTheme_FlorisLight
"floris_dark" -> R.style.KeyboardTheme_FlorisDark
else -> R.style.KeyboardTheme_FlorisLight
}
}
var colorPrimary: Int
get() = prefHelper.getPref(COLOR_PRIMARY, 0)
set(v) = prefHelper.setPref(COLOR_PRIMARY, v)
var colorPrimaryDark: Int
get() = prefHelper.getPref(COLOR_PRIMARY_DARK, 0)
set(v) = prefHelper.setPref(COLOR_PRIMARY_DARK, v)
var colorAccent: Int
get() = prefHelper.getPref(COLOR_ACCENT, 0)
set(v) = prefHelper.setPref(COLOR_ACCENT, v)
var navBarColor: Int
get() = prefHelper.getPref(NAV_BAR_COLOR, 0)
set(v) = prefHelper.setPref(NAV_BAR_COLOR, v)
var navBarIsLight: Boolean
get() = prefHelper.getPref(NAV_BAR_IS_LIGHT, false)
set(v) = prefHelper.setPref(NAV_BAR_IS_LIGHT, v)
var keyboardBgColor: Int
get() = prefHelper.getPref(KEYBOARD_BG_COLOR, 0)
set(v) = prefHelper.setPref(KEYBOARD_BG_COLOR, v)
var keyBgColor: Int
get() = prefHelper.getPref(KEY_BG_COLOR, 0)
set(v) = prefHelper.setPref(KEY_BG_COLOR, v)
var keyBgColorPressed: Int
get() = prefHelper.getPref(KEY_BG_COLOR_PRESSED, 0)
set(v) = prefHelper.setPref(KEY_BG_COLOR_PRESSED, v)
var keyFgColor: Int
get() = prefHelper.getPref(KEY_FG_COLOR, 0)
set(v) = prefHelper.setPref(KEY_FG_COLOR, v)
var keyEnterBgColor: Int
get() = prefHelper.getPref(KEY_ENTER_BG_COLOR, 0)
set(v) = prefHelper.setPref(KEY_ENTER_BG_COLOR, v)
var keyEnterBgColorPressed: Int
get() = prefHelper.getPref(KEY_ENTER_BG_COLOR_PRESSED, 0)
set(v) = prefHelper.setPref(KEY_ENTER_BG_COLOR_PRESSED, v)
var keyEnterFgColor: Int
get() = prefHelper.getPref(KEY_ENTER_FG_COLOR, 0)
set(v) = prefHelper.setPref(KEY_ENTER_FG_COLOR, v)
var keyShiftBgColor: Int
get() = prefHelper.getPref(KEY_SHIFT_BG_COLOR, 0)
set(v) = prefHelper.setPref(KEY_SHIFT_BG_COLOR, v)
var keyShiftBgColorPressed: Int
get() = prefHelper.getPref(KEY_SHIFT_BG_COLOR_PRESSED, 0)
set(v) = prefHelper.setPref(KEY_SHIFT_BG_COLOR_PRESSED, v)
var keyShiftFgColor: Int
get() = prefHelper.getPref(KEY_SHIFT_FG_COLOR, 0)
set(v) = prefHelper.setPref(KEY_SHIFT_FG_COLOR, v)
var keyShiftFgColorCapsLock: Int
get() = prefHelper.getPref(KEY_SHIFT_FG_COLOR_CAPSLOCK, 0)
set(v) = prefHelper.setPref(KEY_SHIFT_FG_COLOR_CAPSLOCK, v)
var keyPopupBgColor: Int
get() = prefHelper.getPref(KEY_POPUP_BG_COLOR, 0)
set(v) = prefHelper.setPref(KEY_POPUP_BG_COLOR, v)
var keyPopupBgColorActive: Int
get() = prefHelper.getPref(KEY_POPUP_BG_COLOR_ACTIVE, 0)
set(v) = prefHelper.setPref(KEY_POPUP_BG_COLOR_ACTIVE, v)
var keyPopupFgColor: Int
get() = prefHelper.getPref(KEY_POPUP_FG_COLOR, 0)
set(v) = prefHelper.setPref(KEY_POPUP_FG_COLOR, v)
var mediaFgColor: Int
get() = prefHelper.getPref(MEDIA_FG_COLOR, 0)
set(v) = prefHelper.setPref(MEDIA_FG_COLOR, v)
var mediaFgColorAlt: Int
get() = prefHelper.getPref(MEDIA_FG_COLOR_ALT, 0)
set(v) = prefHelper.setPref(MEDIA_FG_COLOR_ALT, v)
var oneHandedBgColor: Int
get() = prefHelper.getPref(ONE_HANDED_BG_COLOR, 0)
set(v) = prefHelper.setPref(ONE_HANDED_BG_COLOR, v)
var oneHandedButtonFgColor: Int
get() = prefHelper.getPref(ONE_HANDED_BUTTON_FG_COLOR, 0)
set(v) = prefHelper.setPref(ONE_HANDED_BUTTON_FG_COLOR, v)
var smartbarBgColor: Int
get() = prefHelper.getPref(SMARTBAR_BG_COLOR, 0)
set(v) = prefHelper.setPref(SMARTBAR_BG_COLOR, v)
var smartbarFgColor: Int
get() = prefHelper.getPref(SMARTBAR_FG_COLOR, 0)
set(v) = prefHelper.setPref(SMARTBAR_FG_COLOR, v)
var smartbarFgColorAlt: Int
get() = prefHelper.getPref(SMARTBAR_FG_COLOR_ALT, 0)
set(v) = prefHelper.setPref(SMARTBAR_FG_COLOR_ALT, v)
var smartbarButtonBgColor: Int
get() = prefHelper.getPref(SMARTBAR_BUTTON_BG_COLOR, 0)
set(v) = prefHelper.setPref(SMARTBAR_BUTTON_BG_COLOR, v)
var smartbarButtonFgColor: Int
get() = prefHelper.getPref(SMARTBAR_BUTTON_FG_COLOR, 0)
set(v) = prefHelper.setPref(SMARTBAR_BUTTON_FG_COLOR, v)
}
}

View File

@@ -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 {
@@ -134,16 +134,16 @@ class SubtypeManager(
*/
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
}
}
@@ -212,11 +212,39 @@ 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.
*
* @returns 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.
*
@@ -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

View File

@@ -24,6 +24,7 @@ import android.widget.*
import com.google.android.material.tabs.TabLayout
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.EditorInstance
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.InputView
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyData
@@ -50,6 +51,8 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
FlorisBoard.EventListener {
private val florisboard = FlorisBoard.getInstance()
private val activeEditorInstance: EditorInstance
get() = florisboard.activeEditorInstance
private var activeTab: Tab? = null
private var mediaViewFlipper: ViewFlipper? = null
@@ -129,7 +132,6 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onDestroy()")
cancel()
florisboard.removeEventListener(this)
instance = null
}
@@ -200,18 +202,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)
}
/**

View File

@@ -0,0 +1,67 @@
/*
* Copyright (C) 2020 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.media
import android.content.Context
import android.content.res.ColorStateList
import android.util.AttributeSet
import android.widget.Button
import android.widget.ImageButton
import android.widget.LinearLayout
import com.google.android.material.tabs.TabLayout
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.PrefHelper
class MediaInputView : LinearLayout, FlorisBoard.EventListener {
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
var tabLayout: TabLayout? = null
private set
var switchToTextInputButton: Button? = null
private set
var backspaceButton: ImageButton? = null
private set
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
florisboard?.addEventListener(this)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
tabLayout = findViewById(R.id.media_input_tabs)
switchToTextInputButton = findViewById(R.id.media_input_switch_to_text_input_button)
backspaceButton = findViewById(R.id.media_input_backspace_button)
onApplyThemeAttributes()
}
override fun onApplyThemeAttributes() {
tabLayout?.setTabTextColors(prefs.theme.mediaFgColor, prefs.theme.mediaFgColor)
tabLayout?.tabIconTint = ColorStateList.valueOf(prefs.theme.mediaFgColor)
tabLayout?.setSelectedTabIndicatorColor(prefs.theme.colorPrimary)
switchToTextInputButton?.setTextColor(prefs.theme.mediaFgColor)
backspaceButton?.imageTintList = ColorStateList.valueOf(prefs.theme.mediaFgColor)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val height = florisboard?.inputView?.desiredMediaKeyboardViewHeight ?: 0
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY))
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,6 +24,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.core.content.ContextCompat.getDrawable
import androidx.core.view.get
import com.google.android.flexbox.FlexboxLayout
import com.google.android.flexbox.JustifyContent
import dev.patrickgold.florisboard.R
@@ -34,7 +35,6 @@ import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyData
import dev.patrickgold.florisboard.ime.text.key.KeyView
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
import dev.patrickgold.florisboard.util.setTextTintColor
/**
* Manages the creation and dismissal of key popups as well as the checks if the pointer moved
@@ -43,7 +43,6 @@ import dev.patrickgold.florisboard.util.setTextTintColor
* @property keyboardView Reference to the keyboard view to which this manager class belongs to.
*/
class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD) {
private var anchorLeft: Boolean = false
private var anchorRight: Boolean = false
private var anchorOffset: Int = 0
@@ -102,18 +101,14 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
isInitActive: Boolean = false,
isWrapBefore: Boolean = false
): KeyPopupExtendedSingleView? {
val textView = KeyPopupExtendedSingleView(keyView.context, isInitActive)
val textView = KeyPopupExtendedSingleView(keyView.context, k, isInitActive)
val lp = FlexboxLayout.LayoutParams(keyPopupWidth, keyView.measuredHeight)
lp.isWrapBefore = isWrapBefore
textView.layoutParams = lp
textView.gravity = Gravity.CENTER
setTextTintColor(
textView,
R.attr.key_popup_fgColor
)
val textSize = keyboardView.resources.getDimension(R.dimen.key_popup_textSize)
if (keyView is KeyView) {
when (keyView.data.popup[k].code) {
when (keyView.dataPopupWithHint[k].code) {
KeyCode.SETTINGS -> {
textView.iconDrawable = getDrawable(
keyView.context, R.drawable.ic_settings
@@ -134,13 +129,13 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
}
else -> {
textView.setTextSize(
TypedValue.COMPLEX_UNIT_PX, when (keyView.data.popup[k].code) {
TypedValue.COMPLEX_UNIT_PX, when (keyView.dataPopupWithHint[k].code) {
KeyCode.URI_COMPONENT_TLD,
KeyCode.SWITCH_TO_TEXT_CONTEXT -> textSize * 0.6f
else -> textSize
}
)
textView.text = keyView.getComputedLetter(keyView.data.popup[k])
textView.text = keyView.getComputedLetter(keyView.dataPopupWithHint[k])
}
}
} else if (keyView is EmojiKeyView) {
@@ -216,7 +211,7 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
if (keyView is KeyView) {
popupView.findViewById<TextView>(R.id.key_popup_text)?.text = keyView.getComputedLetter()
popupView.findViewById<ImageView>(R.id.key_popup_threedots)?.visibility = when {
keyView.data.popup.isEmpty() -> View.INVISIBLE
keyView.dataPopupWithHint.isEmpty() -> View.INVISIBLE
else -> View.VISIBLE
}
} else if (keyView is EmojiKeyView) {
@@ -261,17 +256,12 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
}
// 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
}
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
}
@@ -320,17 +310,29 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
// Build UI
popupViewExt.removeAllViews()
val indices = when (keyView) {
is KeyView -> keyView.data.popup.indices
is KeyView -> keyView.dataPopupWithHint.indices
is EmojiKeyView -> keyView.data.popup.indices
else -> IntRange(0, 0)
}
var hasShownFirst = false
for (k in indices) {
val isInitActive =
anchorLeft && (k - row1count == anchorOffset) ||
anchorRight && (k - row1count == row0count - 1 - anchorOffset)
val kk = when (keyView) {
is KeyView -> when {
isInitActive -> {
hasShownFirst = true
0
}
hasShownFirst -> k
else -> k + 1
}
else -> k
}
popupViewExt.addView(
createTextView(
keyView, k, isInitActive, (row1count > 0) && (k - row1count == 0)
keyView, kk, isInitActive, (row1count > 0) && (k - row1count == 0)
)
)
if (isInitActive) {
@@ -446,7 +448,7 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
}
if (keyView is KeyView) {
for (k in keyView.data.popup.indices) {
for (k in keyView.dataPopupWithHint.indices) {
val view = popupViewExt.getChildAt(k)
if (view != null) {
val textView = view as KeyPopupExtendedSingleView
@@ -476,7 +478,13 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
*/
fun getActiveKeyData(keyView: T_KV): KeyData? {
return if (keyView is KeyView) {
keyView.data.popup.getOrNull(activeExtIndex ?: -1) ?: keyView.data
val activeExtIndex = activeExtIndex
if (activeExtIndex != null) {
val singleView = popupViewExt[activeExtIndex] as KeyPopupExtendedSingleView
keyView.dataPopupWithHint.getOrNull(singleView.adjustedIndex) ?: keyView.data
} else {
keyView.data
}
} else {
null
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright (C) 2020 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.popup
import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.util.*
class KeyPopupView : LinearLayout {
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
private lateinit var text: TextView
private lateinit var threedots: ImageView
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
override fun onAttachedToWindow() {
super.onAttachedToWindow()
text = findViewById(R.id.key_popup_text)
threedots = findViewById(R.id.key_popup_threedots)
}
override fun onDraw(canvas: Canvas?) {
setBackgroundTintColor2(this, prefs.theme.keyPopupBgColor)
text.setTextColor(prefs.theme.keyPopupFgColor)
setImageTintColor2(threedots, prefs.theme.keyPopupFgColor)
super.onDraw(canvas)
}
}

View File

@@ -19,7 +19,6 @@ package dev.patrickgold.florisboard.ime.text
import android.content.ClipData
import android.content.Context
import android.os.Handler
import android.text.InputType
import android.util.Log
import android.view.KeyEvent
import android.view.inputmethod.*
@@ -27,10 +26,9 @@ import android.widget.LinearLayout
import android.widget.ViewFlipper
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.InputView
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.editing.EditingKeyboardView
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
@@ -58,6 +56,8 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
FlorisBoard.EventListener {
private val florisboard = FlorisBoard.getInstance()
private val activeEditorInstance: EditorInstance
get() = florisboard.activeEditorInstance
private var activeKeyboardMode: KeyboardMode? = null
private val keyboardViews = EnumMap<KeyboardMode, KeyboardView>(KeyboardMode::class.java)
@@ -68,35 +68,23 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
var keyVariation: KeyVariation = KeyVariation.NORMAL
private val layoutManager = LayoutManager(florisboard)
lateinit var smartbarManager: SmartbarManager
private lateinit var smartbarManager: SmartbarManager
// Caps/Space related properties
var caps: Boolean = false
private set
var capsLock: Boolean = false
private set
private var cursorCapsMode: CapsMode = CapsMode.NONE
private var editorCapsMode: CapsMode = CapsMode.NONE
private var hasCapsRecentlyChanged: Boolean = false
private var hasSpaceRecentlyPressed: Boolean = false
// Composing text related properties
private var composingText: String? = null
private var composingTextStart: Int? = null
private var cursorPos: Int = 0
private var isComposingEnabled: Boolean = false
var isManualSelectionMode: Boolean = false
private var isManualSelectionModeLeft: Boolean = false
private var isManualSelectionModeRight: Boolean = false
val isTextSelected: Boolean
get() = selectionEnd - selectionStart != 0
private var lastCursorAnchorInfo: CursorAnchorInfo? = null
private var selectionStart: Int = 0
private val selectionStartMin: Int = 0
private var selectionEnd: Int = 0
private var selectionEndMax: Int = 0
companion object {
private val TAG: String? = TextInputManager::class.simpleName
private var instance: TextInputManager? = null
@Synchronized
@@ -117,19 +105,15 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* background).
*/
override fun onCreate() {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onCreate()")
if (BuildConfig.DEBUG) Log.i(TAG, "onCreate()")
for (mode in KeyboardMode.values()) {
if (mode == KeyboardMode.CHARACTERS) {
var subtypes = florisboard.subtypeManager.subtypes
if (subtypes.isEmpty()) {
subtypes = listOf(Subtype.DEFAULT)
}
for (subtype in subtypes) {
layoutManager.preloadComputedLayout(mode, subtype)
}
} else {
layoutManager.preloadComputedLayout(mode, florisboard.activeSubtype)
var subtypes = florisboard.subtypeManager.subtypes
if (subtypes.isEmpty()) {
subtypes = listOf(Subtype.DEFAULT)
}
for (subtype in subtypes) {
for (mode in KeyboardMode.values()) {
layoutManager.preloadComputedLayout(mode, subtype)
}
}
smartbarManager = SmartbarManager.getInstance()
@@ -137,8 +121,6 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
private suspend fun addKeyboardView(mode: KeyboardMode) {
val keyboardView = KeyboardView(florisboard.context)
keyboardView.florisboard = florisboard
keyboardView.prefs = florisboard.prefs
keyboardView.computedLayout = layoutManager.fetchComputedLayoutAsync(mode, florisboard.activeSubtype).await()
keyboardViews[mode] = keyboardView
withContext(Dispatchers.Main) {
@@ -150,7 +132,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* Sets up the newly registered input view.
*/
override fun onRegisterInputView(inputView: InputView) {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onRegisterInputView(inputView)")
if (BuildConfig.DEBUG) Log.i(TAG, "onRegisterInputView(inputView)")
launch(Dispatchers.Default) {
textViewGroup = inputView.findViewById(R.id.text_input)
@@ -174,69 +156,70 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* Cancels all coroutines and cleans up.
*/
override fun onDestroy() {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onDestroy()")
if (BuildConfig.DEBUG) Log.i(TAG, "onDestroy()")
cancel()
osHandler.removeCallbacksAndMessages(null)
layoutManager.onDestroy()
smartbarManager.onDestroy()
florisboard.removeEventListener(this)
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) {
instance.isComposingEnabled = when (keyboardMode) {
KeyboardMode.NUMERIC,
KeyboardMode.PHONE,
KeyboardMode.PHONE2 -> false
else -> keyVariation != KeyVariation.PASSWORD && florisboard.prefs.suggestion.enabled
else -> keyVariation != KeyVariation.PASSWORD &&
florisboard.prefs.suggestion.enabled &&
//!instance.inputAttributes.flagTextAutoComplete &&
!instance.inputAttributes.flagTextNoSuggestions
}
if (!florisboard.prefs.correction.rememberCapsLockState) {
capsLock = false
}
updateCapsState()
resetComposingText()
setActiveKeyboardMode(keyboardMode)
smartbarManager.onStartInputView(keyboardMode, isComposingEnabled)
smartbarManager.onStartInputView(keyboardMode, instance.isComposingEnabled)
}
/**
@@ -290,218 +273,86 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* Main logic point for processing cursor updates as well as parsing the current composing word
* and passing this info on to the [SmartbarManager] to turn it into candidate suggestions.
*/
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
cursorAnchorInfo ?: return
lastCursorAnchorInfo = cursorAnchorInfo
val ic = florisboard.currentInputConnection
val isNewSelectionInBoundsOfOld =
cursorAnchorInfo.selectionStart >= (selectionStart - 1) &&
cursorAnchorInfo.selectionStart <= (selectionStart + 1) &&
cursorAnchorInfo.selectionEnd >= (selectionEnd - 1) &&
cursorAnchorInfo.selectionEnd <= (selectionEnd + 1)
selectionStart = cursorAnchorInfo.selectionStart
selectionEnd = cursorAnchorInfo.selectionEnd
val inputText =
(ic?.getExtractedText(ExtractedTextRequest(), 0)?.text ?: "").toString()
selectionEndMax = inputText.length
if (isComposingEnabled) {
if (!isTextSelected) {
val newCursorPos = cursorAnchorInfo.selectionStart
val prevComposingText = (cursorAnchorInfo.composingText ?: "").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.selection.isCursorMode) {
smartbarManager.generateCandidatesFromComposing(activeEditorInstance.currentWord.text)
}
if (!isNewSelectionInBoundsOfOld) {
if (!activeEditorInstance.isNewSelectionInBoundsOfOld) {
isManualSelectionMode = false
isManualSelectionModeLeft = false
isManualSelectionModeRight = false
}
updateCapsState()
smartbarManager.onUpdateCursorAnchorInfo(cursorAnchorInfo)
smartbarManager.onUpdateSelection()
}
/**
* 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
caps = florisboard.prefs.correction.autoCapitalization &&
activeEditorInstance.cursorCapsMode != InputAttributes.CapsMode.NONE
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
}
}
/**
* Sends a given [keyCode] as a [KeyEvent.ACTION_DOWN].
*
* @param ic The input connection on which this operation should be performed.
* @param keyCode The key code to send, use a key code defined in Android's [KeyEvent], not in
* [KeyCode] or this call may send a weird character, as this key codes do not match!!
* Executes a given [SwipeAction]. Ignores any [SwipeAction] but the ones relevant for this
* class.
*/
private fun sendSystemKeyEvent(ic: InputConnection?, keyCode: Int) {
ic?.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, keyCode))
}
/**
* Sends a given [keyCode] as a [KeyEvent.ACTION_DOWN] with ALT pressed.
*
* @param ic The input connection on which this operation should be performed.
* @param keyCode The key code to send, use a key code defined in Android's [KeyEvent], not in
* [KeyCode] or this call may send a weird character, as this key codes do not match!!
*/
private fun sendSystemKeyEventAlt(ic: InputConnection?, keyCode: Int) {
ic?.sendKeyEvent(
KeyEvent(
0,
1,
KeyEvent.ACTION_DOWN, keyCode,
0,
KeyEvent.META_ALT_LEFT_ON
)
)
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 -> {}
}
}
/**
* Handles a [KeyCode.DELETE] event.
*/
private fun handleDelete() {
val ic = florisboard.currentInputConnection
ic?.beginBatchEdit()
resetComposingText()
isManualSelectionMode = false
isManualSelectionModeLeft = false
isManualSelectionModeRight = false
sendSystemKeyEvent(ic, KeyEvent.KEYCODE_DEL)
ic?.endBatchEdit()
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) {
sendSystemKeyEvent(ic, KeyEvent.KEYCODE_ENTER)
if (activeEditorInstance.imeOptions.flagNoEnterAction) {
activeEditorInstance.commitText("\n")
} 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)
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 -> sendSystemKeyEvent(ic, KeyEvent.KEYCODE_ENTER)
else -> activeEditorInstance.commitText("\n")
}
}
ic?.endBatchEdit()
}
/**
@@ -529,14 +380,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 {
@@ -546,107 +396,107 @@ 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) {
val ic = florisboard.currentInputConnection
resetComposingText()
if (isTextSelected && isManualSelectionMode) {
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) {
ic?.setSelection(
(selectionStart - 1).coerceAtLeast(selectionStartMin),
selectionEnd
setSelection(
(selection.start - 1).coerceAtLeast(selectionStartMin),
selection.end
)
} else {
ic?.setSelection(selectionStart, selectionEnd - 1)
setSelection(selection.start, selection.end - 1)
}
}
KeyCode.ARROW_RIGHT -> {
if (isManualSelectionModeRight) {
ic?.setSelection(
selectionStart,
(selectionEnd + 1).coerceAtMost(selectionEndMax)
setSelection(
selection.start,
(selection.end + 1).coerceAtMost(selectionEndMax)
)
} else {
ic?.setSelection(selectionStart + 1, selectionEnd)
setSelection(selection.start + 1, selection.end)
}
}
KeyCode.ARROW_UP -> {}
KeyCode.MOVE_HOME -> {
if (isManualSelectionModeLeft) {
ic?.setSelection(selectionStartMin, selectionEnd)
setSelection(selectionStartMin, selection.end)
} else {
ic?.setSelection(selectionStartMin, selectionStart)
setSelection(selectionStartMin, selection.start)
}
}
KeyCode.MOVE_END -> {
if (isManualSelectionModeRight) {
ic?.setSelection(selectionStart, selectionEndMax)
setSelection(selection.start, selectionEndMax)
} else {
ic?.setSelection(selectionEnd, selectionEndMax)
setSelection(selection.end, selectionEndMax)
}
}
}
} else if (isTextSelected && !isManualSelectionMode) {
} 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 -> {
ic?.setSelection(selectionStart, selectionEnd - 1)
setSelection(selection.start, selection.end - 1)
}
KeyCode.ARROW_RIGHT -> {
ic?.setSelection(
selectionStart,
(selectionEnd + 1).coerceAtMost(selectionEndMax)
setSelection(
selection.start,
(selection.end + 1).coerceAtMost(selectionEndMax)
)
}
KeyCode.ARROW_UP -> {}
KeyCode.MOVE_HOME -> {
ic?.setSelection(selectionStartMin, selectionStart)
setSelection(selectionStartMin, selection.start)
}
KeyCode.MOVE_END -> {
ic?.setSelection(selectionStart, selectionEndMax)
setSelection(selection.start, selectionEndMax)
}
}
} else if (!isTextSelected && isManualSelectionMode) {
} 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 -> {
ic?.setSelection(
(selectionStart - 1).coerceAtLeast(selectionStartMin),
selectionStart
setSelection(
(selection.start - 1).coerceAtLeast(selectionStartMin),
selection.start
)
isManualSelectionModeLeft = true
isManualSelectionModeRight = false
}
KeyCode.ARROW_RIGHT -> {
ic?.setSelection(
selectionEnd,
(selectionEnd + 1).coerceAtMost(selectionEndMax)
setSelection(
selection.end,
(selection.end + 1).coerceAtMost(selectionEndMax)
)
isManualSelectionModeLeft = false
isManualSelectionModeRight = true
}
KeyCode.ARROW_UP -> {}
KeyCode.MOVE_HOME -> {
ic?.setSelection(selectionStartMin, selectionStart)
setSelection(selectionStartMin, selection.start)
isManualSelectionModeLeft = true
isManualSelectionModeRight = false
}
KeyCode.MOVE_END -> {
ic?.setSelection(selectionEnd, selectionEndMax)
setSelection(selection.end, selectionEndMax)
isManualSelectionModeLeft = false
isManualSelectionModeRight = true
}
@@ -654,12 +504,12 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
} else {
// No selection and no manual selection mode -> move cursor around
when (code) {
KeyCode.ARROW_DOWN -> sendSystemKeyEvent(ic, KeyEvent.KEYCODE_DPAD_DOWN)
KeyCode.ARROW_LEFT -> sendSystemKeyEvent(ic, KeyEvent.KEYCODE_DPAD_LEFT)
KeyCode.ARROW_RIGHT -> sendSystemKeyEvent(ic, KeyEvent.KEYCODE_DPAD_RIGHT)
KeyCode.ARROW_UP -> sendSystemKeyEvent(ic, KeyEvent.KEYCODE_DPAD_UP)
KeyCode.MOVE_HOME -> sendSystemKeyEventAlt(ic, KeyEvent.KEYCODE_DPAD_UP)
KeyCode.MOVE_END -> sendSystemKeyEventAlt(ic, KeyEvent.KEYCODE_DPAD_DOWN)
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)
}
}
}
@@ -669,14 +519,10 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* TODO: handle other data than text too, e.g. Uri, Intent, ...
*/
private fun handleClipboardCut() {
val ic = florisboard.currentInputConnection
val selectedText = ic?.getSelectedText(0)
if (selectedText != null) {
florisboard.clipboardManager
?.setPrimaryClip(ClipData.newPlainText(selectedText, selectedText))
}
resetComposingText()
ic?.commitText("", 1)
val selectedText = activeEditorInstance.selection.text
florisboard.clipboardManager
?.setPrimaryClip(ClipData.newPlainText(selectedText, selectedText))
activeEditorInstance.commitText("")
}
/**
@@ -684,14 +530,10 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* TODO: handle other data than text too, e.g. Uri, Intent, ...
*/
private fun handleClipboardCopy() {
val ic = florisboard.currentInputConnection
val selectedText = ic?.getSelectedText(0)
if (selectedText != null) {
florisboard.clipboardManager
?.setPrimaryClip(ClipData.newPlainText(selectedText, selectedText))
}
resetComposingText()
ic?.setSelection(selectionEnd, selectionEnd)
val selectedText = activeEditorInstance.selection.text
florisboard.clipboardManager
?.setPrimaryClip(ClipData.newPlainText(selectedText, selectedText))
activeEditorInstance.apply { setSelection(selection.end, selection.end) }
}
/**
@@ -699,42 +541,36 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* TODO: handle other data than text too, e.g. Uri, Intent, ...
*/
private fun handleClipboardPaste() {
val ic = florisboard.currentInputConnection
val item = florisboard.clipboardManager?.primaryClip?.getItemAt(0)
val pasteText = item?.text
if (pasteText != null) {
resetComposingText()
ic?.commitText(pasteText, 1)
activeEditorInstance.commitText(pasteText.toString())
}
}
/**
* Handles a [KeyCode.CLIPBOARD_SELECT] event.
*/
private fun handleClipboardSelect() {
val ic = florisboard.currentInputConnection
resetComposingText()
if (isTextSelected) {
private fun handleClipboardSelect() = activeEditorInstance.apply {
if (selection.isSelectionMode) {
if (isManualSelectionMode && isManualSelectionModeLeft) {
ic?.setSelection(selectionStart, selectionStart)
setSelection(selection.start, selection.start)
} else {
ic?.setSelection(selectionEnd, selectionEnd)
setSelection(selection.end, selection.end)
}
isManualSelectionMode = false
} else {
isManualSelectionMode = !isManualSelectionMode
// Must recall to update UI properly
florisboard.onUpdateCursorAnchorInfo(lastCursorAnchorInfo)
// Must call to update UI properly
editingKeyboardView?.onUpdateSelection()
}
}
}
/**
* Handles a [KeyCode.CLIPBOARD_SELECT_ALL] event.
*/
private fun handleClipboardSelectAll() {
val ic = florisboard.currentInputConnection
resetComposingText()
ic?.setSelection(selectionStartMin, selectionEndMax)
activeEditorInstance.setSelection(0, activeEditorInstance.cachedText.length)
}
/**
@@ -745,8 +581,6 @@ 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.ARROW_DOWN,
KeyCode.ARROW_LEFT,
@@ -780,8 +614,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,
@@ -790,13 +622,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)
}
}
}
@@ -808,7 +640,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()
@@ -816,26 +648,15 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
true -> text.toUpperCase(Locale.getDefault())
false -> text.toLowerCase(Locale.getDefault())
}
ic?.commitText(text, 1)
activeEditorInstance.commitText(text)
}
}
else -> {
Log.e(
this::class.simpleName,
"sendKeyPress(keyData): Received unknown key: $keyData"
)
Log.e(TAG,"sendKeyPress(keyData): Received unknown key: $keyData")
}
}
}
ic?.endBatchEdit()
}
}
}
enum class CapsMode {
ALL,
NONE,
SENTENCES,
WORDS;
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.editing
package dev.patrickgold.florisboard.ime.text.editing
import android.annotation.SuppressLint
import android.content.Context
@@ -29,9 +29,9 @@ 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.getColorFromAttr
import java.util.*
/**
@@ -39,6 +39,7 @@ import java.util.*
*/
class EditingKeyView : AppCompatImageButton {
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
private val data: KeyData
private var isKeyPressed: Boolean = false
private var osTimer: Timer? = null
@@ -133,20 +134,20 @@ class EditingKeyView : AppCompatImageButton {
canvas ?: return
imageTintList = ColorStateList.valueOf(getColorFromAttr(context, when {
isEnabled -> R.attr.key_fgColor
else -> android.R.attr.colorButtonNormal
}))
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) {
getColorFromAttr(context, R.attr.colorPrimary)
prefs.theme.colorPrimary
} else if (!isEnabled) {
getColorFromAttr(context, android.R.attr.colorButtonNormal)
prefs.theme.smartbarFgColorAlt
} else {
getColorFromAttr(context, R.attr.key_fgColor)
prefs.theme.smartbarFgColor
}
val isPortrait =
resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT

View File

@@ -14,21 +14,24 @@
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.editing
package dev.patrickgold.florisboard.ime.text.editing
import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import android.view.View
import android.view.inputmethod.CursorAnchorInfo
import androidx.constraintlayout.widget.ConstraintLayout
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.util.setBackgroundTintColor2
/**
* View class for updating the key views depending on the current selection and clipboard state.
*/
class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener {
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
private var arrowUpKey: EditingKeyView? = null
private var arrowDownKey: EditingKeyView? = null
@@ -56,14 +59,8 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener {
pasteKey = findViewById(R.id.clipboard_paste)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
florisboard?.removeEventListener(this)
}
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
val isSelectionActive = florisboard?.textInputManager?.isTextSelected ?: false
override fun onUpdateSelection() {
val isSelectionActive = florisboard?.activeEditorInstance?.selection?.isSelectionMode ?: false
val isSelectionMode = florisboard?.textInputManager?.isManualSelectionMode ?: false
arrowUpKey?.isEnabled = !(isSelectionActive || isSelectionMode)
arrowDownKey?.isEnabled = !(isSelectionActive || isSelectionMode)
@@ -79,4 +76,30 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener {
copyKey?.isEnabled = isSelectionActive
pasteKey?.isEnabled = florisboard?.clipboardManager?.hasPrimaryClip() ?: false
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
val height = when (heightMode) {
MeasureSpec.EXACTLY -> {
// Must be this size
heightSize
}
MeasureSpec.AT_MOST -> {
// Can't be bigger than...
(florisboard?.inputView?.desiredTextKeyboardViewHeight ?: 0).coerceAtMost(heightSize)
}
else -> {
// Be whatever you want
florisboard?.inputView?.desiredTextKeyboardViewHeight ?: 0
}
}
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY))
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
setBackgroundTintColor2(this, prefs.theme.smartbarBgColor)
}
}

View File

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

View File

@@ -0,0 +1,45 @@
/*
* 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_WORD,
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)
}
}

View File

@@ -0,0 +1,197 @@
/*
* Copyright (C) 2020 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.text.gestures
import android.content.Context
import android.util.DisplayMetrics
import android.view.MotionEvent
import dev.patrickgold.florisboard.R
import java.lang.Exception
import kotlin.math.*
/**
* Wrapper class which holds all enums, interfaces and classes for detecting a swipe gesture.
*/
abstract class SwipeGesture {
/**
* Class which detects swipes based on given [MotionEvent]s. Only supports single-finger swipes
* and ignores additional pointers provided, if any.
*
* @property listener The listener to report detected swipes to.
*/
class Detector(private val context: Context, private val listener: Listener) {
private val eventList: MutableList<MotionEvent> = mutableListOf()
private var indexFirst: Int = 0
private var indexLastMoveRecognized: Int = 0
var distanceThreshold: DistanceThreshold = DistanceThreshold.NORMAL
var velocityThreshold: VelocityThreshold = VelocityThreshold.NORMAL
fun onTouchEvent(event: MotionEvent): Boolean {
try {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_POINTER_DOWN -> {
clearEventList()
eventList.add(MotionEvent.obtainNoHistory(event))
}
MotionEvent.ACTION_MOVE -> {
eventList.add(MotionEvent.obtainNoHistory(event))
val lastEvent = eventList[indexLastMoveRecognized]
val diffX = event.x - lastEvent.x
val diffY = event.y - lastEvent.y
val distanceThresholdNV = numericValue(distanceThreshold) / 2.0f
return if (abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV) {
indexLastMoveRecognized = eventList.size - 1
val direction = detectDirection(diffX.toDouble(), diffY.toDouble())
listener.onSwipe(direction, Type.TOUCH_MOVE)
} else {
false
}
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP -> {
val firstEvent = eventList[indexFirst]
val diffX = event.x - firstEvent.x
val diffY = event.y - firstEvent.y
val distanceThresholdNV = numericValue(distanceThreshold)
/*val velocityThresholdNV = numericValue(velocityThreshold)
val velocity =
((convertPixelsToDp(
sqrt(diffX.pow(2) + diffY.pow(2)),
context
) / event.downTime) * 10.0f.pow(8)).toInt()*/
clearEventList()
// return if ((abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV) && velocity >= velocityThresholdNV) {
return if ((abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV)) {
val direction = detectDirection(diffX.toDouble(), diffY.toDouble())
listener.onSwipe(direction, Type.TOUCH_UP)
} else {
false
}
}
MotionEvent.ACTION_CANCEL -> {
clearEventList()
}
else -> return false
}
return false
} catch(e: Exception) {
return false
}
}
/**
* Calculates the angle based on the given x any y lengths. The returned angle is in degree
* and goes clockwise, beginning with 0° at +x, 90° at +y, 180° at -y and 270° at -y.
*
* Coordinate system (based on the Android display coordinate system):
* -y
* -x 00 +x
* +y
*/
private fun angle(diffX: Double, diffY: Double): Double {
val tmpAngle = abs(360 * atan(diffY / diffX) / (2 * PI))
return if (diffX < 0 && diffY >= 0) {
180.0f - tmpAngle
} else if (diffX < 0 && diffY < 0) {
180.0f + tmpAngle
} else if (diffX >= 0 && diffY < 0) {
360.0f - tmpAngle
} else {
tmpAngle
}
}
/**
* Detects the direction of a finger swipe by two given events.
*/
private fun detectDirection(diffX: Double, diffY: Double): Direction {
val diffAngle = angle(diffX, diffY) / 360
return when {
diffAngle >= (1/16.0f) && diffAngle < (3/16.0f) -> Direction.DOWN_RIGHT
diffAngle >= (3/16.0f) && diffAngle < (5/16.0f) -> Direction.DOWN
diffAngle >= (5/16.0f) && diffAngle < (7/16.0f) -> Direction.DOWN_LEFT
diffAngle >= (7/16.0f) && diffAngle < (9/16.0f) -> Direction.LEFT
diffAngle >= (9/16.0f) && diffAngle < (11/16.0f) -> Direction.UP_LEFT
diffAngle >= (11/16.0f) && diffAngle < (13/16.0f) -> Direction.UP
diffAngle >= (13/16.0f) && diffAngle < (15/16.0f) -> Direction.UP_RIGHT
else -> Direction.RIGHT
}
}
/**
* Cleans up and clears the event list.
*/
private fun clearEventList() {
for (event in eventList) {
event.recycle()
}
eventList.clear()
indexFirst = 0
indexLastMoveRecognized = 0
}
/**
* Returns a numeric value for a given [DistanceThreshold], based on the values defined in
* the resources dimens.xml file.
*/
private fun numericValue(of: DistanceThreshold): Double {
return when (of) {
DistanceThreshold.VERY_SHORT -> context.resources.getDimension(R.dimen.gesture_distance_threshold_very_short)
DistanceThreshold.SHORT -> context.resources.getDimension(R.dimen.gesture_distance_threshold_short)
DistanceThreshold.NORMAL -> context.resources.getDimension(R.dimen.gesture_distance_threshold_normal)
DistanceThreshold.LONG -> context.resources.getDimension(R.dimen.gesture_distance_threshold_long)
DistanceThreshold.VERY_LONG -> context.resources.getDimension(R.dimen.gesture_distance_threshold_very_long)
}.toDouble()
}
/**
* Returns a numeric value for a given [VelocityThreshold], based on the values defined in
* the resources dimens.xml file.
*/
private fun numericValue(of: VelocityThreshold): Double {
return when (of) {
VelocityThreshold.VERY_SLOW -> context.resources.getInteger(R.integer.gesture_velocity_threshold_very_slow)
VelocityThreshold.SLOW -> context.resources.getInteger(R.integer.gesture_velocity_threshold_slow)
VelocityThreshold.NORMAL -> context.resources.getInteger(R.integer.gesture_velocity_threshold_normal)
VelocityThreshold.FAST -> context.resources.getInteger(R.integer.gesture_velocity_threshold_fast)
VelocityThreshold.VERY_FAST -> context.resources.getInteger(R.integer.gesture_velocity_threshold_very_fast)
}.toDouble()
}
}
interface Listener {
fun onSwipe(direction: Direction, type: Type): Boolean
}
enum class Direction {
UP_LEFT,
UP,
UP_RIGHT,
RIGHT,
DOWN_RIGHT,
DOWN,
DOWN_LEFT,
LEFT,
}
enum class Type {
TOUCH_UP,
TOUCH_MOVE;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,6 @@
package dev.patrickgold.florisboard.ime.text.key
import android.annotation.SuppressLint
import android.content.res.Configuration
import android.graphics.*
import android.graphics.drawable.Drawable
import android.os.Handler
@@ -33,11 +32,13 @@ import androidx.core.view.children
import com.google.android.flexbox.FlexboxLayout
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.text.gestures.SwipeGesture
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
import dev.patrickgold.florisboard.util.getColorFromAttr
import dev.patrickgold.florisboard.util.setBackgroundTintColor
import dev.patrickgold.florisboard.util.setBackgroundTintColor2
import java.util.*
import kotlin.math.abs
/**
* View class for managing the rendering and the events of a single keyboard key.
@@ -51,8 +52,8 @@ import java.util.*
class KeyView(
private val keyboardView: KeyboardView,
val data: KeyData
) : View(keyboardView.context) {
) : View(keyboardView.context), SwipeGesture.Listener {
val dataPopupWithHint: MutableList<KeyData>
private var isKeyPressed: Boolean = false
set(value) {
field = value
@@ -60,8 +61,11 @@ class KeyView(
}
private var osHandler: Handler? = null
private var osTimer: Timer? = null
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
private var shouldBlockNextKeyCode: Boolean = false
private var desiredWidth: Int = 0
private var desiredHeight: Int = 0
private var drawable: Drawable? = null
private var drawableColor: Int = 0
private var drawablePadding: Int = 0
@@ -75,8 +79,20 @@ class KeyView(
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 = true
textAlign = Paint.Align.CENTER
textSize = resources.getDimension(R.dimen.key_textHintSize)
typeface = Typeface.DEFAULT
}
private val tempRect: Rect = Rect()
var florisboard: FlorisBoard? = null
private val swipeGestureDetector = SwipeGesture.Detector(context, this)
var touchHitBox: Rect = Rect(-1, -1, -1, -1)
init {
@@ -123,6 +139,23 @@ class KeyView(
background = getDrawable(context, R.drawable.shape_rect_rounded)
elevation = 4.0f
var hintKeyData: KeyData? = null
val hintedNumber = data.hintedNumber
if (prefs.keyboard.hintedNumberRow && hintedNumber != null) {
hintKeyData = hintedNumber
}
val hintedSymbol = data.hintedSymbol
if (prefs.keyboard.hintedSymbols && hintedSymbol != null) {
hintKeyData = hintedSymbol
}
dataPopupWithHint = if (hintKeyData == null) {
data.popup.toMutableList()
} else {
val popupList = data.popup.toMutableList()
popupList.add(hintKeyData)
popupList
}
updateKeyPressedBackground()
}
@@ -172,10 +205,19 @@ class KeyView(
*/
fun onFlorisTouchEvent(event: MotionEvent?): Boolean {
event ?: return false
if (swipeGestureDetector.onTouchEvent(event)) {
isKeyPressed = false
osHandler?.removeCallbacksAndMessages(null)
osTimer?.cancel()
osTimer = null
keyboardView.popupManager.hide()
return true
}
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
florisboard?.prefs?.popup?.let {
if (it.enabled){
shouldBlockNextKeyCode = false
florisboard?.prefs?.keyboard?.let {
if (it.popupEnabled){
keyboardView.popupManager.show(this)
}
}
@@ -194,12 +236,12 @@ class KeyView(
}
}, 500, 50)
}
val delayMillis = keyboardView.prefs.looknfeel.longPressDelay
val delayMillis = prefs.keyboard.longPressDelay
if (osHandler == null) {
osHandler = Handler()
}
osHandler?.postDelayed({
if (data.popup.isNotEmpty()) {
if (dataPopupWithHint.isNotEmpty()) {
keyboardView.popupManager.extend(this)
}
if (data.code == KeyCode.SPACE) {
@@ -252,6 +294,34 @@ 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.SPACE -> when (type) {
SwipeGesture.Type.TOUCH_MOVE -> when (direction) {
SwipeGesture.Direction.LEFT -> {
florisboard?.executeSwipeAction(prefs.gestures.spaceBarSwipeLeft)
osHandler?.removeCallbacksAndMessages(null)
shouldBlockNextKeyCode = true
true
}
SwipeGesture.Direction.RIGHT -> {
florisboard?.executeSwipeAction(prefs.gestures.spaceBarSwipeRight)
osHandler?.removeCallbacksAndMessages(null)
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():
@@ -259,7 +329,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()
@@ -278,7 +348,7 @@ class KeyView(
else -> keyboardView.desiredKeyWidth
}
}
val desiredHeight = keyboardView.desiredKeyHeight
desiredHeight = keyboardView.desiredKeyHeight
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
@@ -337,20 +407,31 @@ class KeyView(
* 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
}
)
} else {
setBackgroundTintColor(
this, when {
isKeyPressed -> R.attr.key_bgColorPressed
else -> R.attr.key_bgColor
}
)
when (data.code) {
KeyCode.ENTER -> {
setBackgroundTintColor2(
this, when {
isKeyPressed -> prefs.theme.keyEnterBgColorPressed
else -> prefs.theme.keyEnterBgColor
}
)
}
KeyCode.SHIFT -> {
setBackgroundTintColor2(
this, when {
isKeyPressed -> prefs.theme.keyShiftBgColorPressed
else -> prefs.theme.keyShiftBgColor
}
)
}
else -> {
setBackgroundTintColor2(
this, when {
isKeyPressed -> prefs.theme.keyBgColorPressed
else -> prefs.theme.keyBgColor
}
)
}
}
}
@@ -419,6 +500,45 @@ class KeyView(
}
}
/**
* Automatically sets the text size of [boxPaint] for given [text] so it fits within the given
* bounds.
*
* Implementation based on this SO answer by Michael Scheper, but has been modified to
* incorporate the height as well: https://stackoverflow.com/a/21895626/6801193
*
* @param boxPaint The [Paint] object which the text size should be applied to.
* @param boxWidth The max width for the surrounding box of [text].
* @param boxHeight The max height for the surrounding box of [text].
* @param text The text for which the size should be calculated.
*/
private fun setTextSizeFor(boxPaint: Paint, boxWidth: Float, boxHeight: Float, text: String) {
var textSize = 64.0f
// Must loop twice as there can be bot with and height which are too big, which requires
// 2 iterations to adjust
for (n in 0..1) {
boxPaint.textSize = textSize
boxPaint.getTextBounds(text, 0, text.length, tempRect)
val diffWidth = tempRect.width() - boxWidth
val diffHeight = tempRect.height() - boxHeight
val factor = if (diffWidth < 0 && diffHeight < 0) {
// Text box is smaller as given box, text size must be increased
if (abs(diffWidth) < abs(diffHeight)) {
boxWidth / tempRect.width()
} else {
boxHeight / tempRect.height()
}
} else if (diffWidth > diffHeight) {
// Text box is larger on minimum one side than given box, text size must be decreased
boxWidth / tempRect.width()
} else {
boxHeight / tempRect.height()
}
textSize *= factor
}
boxPaint.textSize = textSize
}
/**
* Draw the key label / drawable.
*/
@@ -433,11 +553,20 @@ class KeyView(
&& data.code != KeyCode.HALF_SPACE && data.code != KeyCode.KESHIDA || data.type == KeyType.NUMERIC
) {
label = getComputedLetter()
val hintedNumber = data.hintedNumber
if (prefs.keyboard.hintedNumberRow && hintedNumber != null) {
hintedLabel = getComputedLetter(hintedNumber)
}
val hintedSymbol = data.hintedSymbol
if (prefs.keyboard.hintedSymbols && hintedSymbol != null) {
hintedLabel = getComputedLetter(hintedSymbol)
}
} else {
when (data.code) {
KeyCode.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
@@ -451,29 +580,29 @@ class KeyView(
EditorInfo.IME_ACTION_SEND -> R.drawable.ic_send
else -> R.drawable.ic_arrow_right_alt
})
drawableColor = getColorFromAttr(context, R.attr.key_enter_fgColor)
drawableColor = prefs.theme.keyEnterFgColor
if (action and EditorInfo.IME_FLAG_NO_ENTER_ACTION > 0) {
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
}
})
@@ -485,7 +614,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
@@ -495,7 +624,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 -> {
@@ -551,20 +680,40 @@ class KeyView(
// Draw label
val label = label
if (label != null) {
if (data.code == KeyCode.VIEW_NUMERIC || data.code == KeyCode.VIEW_NUMERIC_ADVANCED
|| data.code == KeyCode.SPACE) {
labelPaint.textSize = resources.getDimension(R.dimen.key_numeric_textSize)
} else {
labelPaint.textSize = resources.getDimension(R.dimen.key_textSize)
when (data.code) {
KeyCode.VIEW_NUMERIC, KeyCode.VIEW_NUMERIC_ADVANCED -> {
labelPaint.textSize = resources.getDimension(R.dimen.key_numeric_textSize)
}
else -> when {
data.type == KeyType.CHARACTER && data.code != KeyCode.SPACE -> {
setTextSizeFor(
labelPaint,
desiredWidth - (2.6f * drawablePadding),
desiredHeight - (3.6f * drawablePadding),
// Note: taking a "X" here because it is one of the biggest letters and
// the keys must have the same base character for calculation, else
// they will all look different and weird...
"X"
)
}
else -> {
setTextSizeFor(
labelPaint,
measuredWidth - (2.6f * drawablePadding),
measuredHeight - (3.6f * drawablePadding),
when (data.code) {
KeyCode.VIEW_CHARACTERS, KeyCode.VIEW_SYMBOLS, KeyCode.VIEW_SYMBOLS2 -> {
resources.getString(R.string.key__view_symbols)
}
else -> label
}
)
}
}
}
labelPaint.color = getColorFromAttr(context, R.attr.key_fgColor)
labelPaint.color = prefs.theme.keyFgColor
labelPaint.alpha = if (keyboardView.computedLayout?.mode == KeyboardMode.CHARACTERS &&
data.code == KeyCode.SPACE) { 120 } else { 255 }
val isPortrait =
resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
if (keyboardView.prefs.looknfeel.oneHandedMode != "off" && isPortrait) {
labelPaint.textSize *= 0.9f
}
val centerX = measuredWidth / 2.0f
val centerY = measuredHeight / 2.0f + (labelPaint.textSize - labelPaint.descent()) / 2
if (label.contains("\n")) {
@@ -576,6 +725,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)
}
}
/**

View File

@@ -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,23 @@ 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 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 +108,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 +132,12 @@ class KeyboardView : LinearLayout {
return false
}
val eventFloris = MotionEvent.obtainNoHistory(event)
if (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 +148,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 +157,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 +215,45 @@ 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) {
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].
*/
@@ -237,32 +292,21 @@ class KeyboardView : LinearLayout {
val keyMarginH = resources.getDimension((R.dimen.key_marginH)).toInt()
desiredKeyWidth = (widthSize / 10) - (2 * keyMarginH)
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) {
val keyMarginV = resources.getDimension((R.dimen.key_marginV)).toInt()
val keyHeightFactor = when (isPreviewMode) {
true -> 0.90f
else -> 1.00f
}
desiredKeyHeight = (resources.getDimension(R.dimen.key_height) * keyHeightFactor).toInt()
florisboard?.textInputManager?.smartbarManager?.smartbarView?.setHeightFactor(keyHeightFactor)
val desiredHeight = keyHeightFactor * (florisboard?.inputView?.desiredTextKeyboardViewHeight ?: resources.getDimension(R.dimen.textKeyboardView_baseHeight).toInt())
desiredKeyHeight = (desiredHeight / 4 - 2 * keyMarginV).roundToInt()
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(desiredHeight.roundToInt(), MeasureSpec.EXACTLY))
}
override fun onApplyThemeAttributes() {
if (isPreviewMode) {
setBackgroundColor(prefs.theme.keyboardBgColor)
}
}
/**
@@ -309,10 +353,4 @@ class KeyboardView : LinearLayout {
}
}
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
colorDrawable.color = getColorFromAttr(context, R.attr.keyboard_bgColor)
}
}

View File

@@ -22,10 +22,7 @@ import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.text.key.KeyData
import dev.patrickgold.florisboard.ime.text.key.KeyTypeAdapter
import dev.patrickgold.florisboard.ime.text.key.KeyVariation
import dev.patrickgold.florisboard.ime.text.key.KeyVariationAdapter
import dev.patrickgold.florisboard.ime.text.key.*
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
import kotlinx.coroutines.*
import java.util.*
@@ -37,7 +34,7 @@ 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.
@@ -49,6 +46,7 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
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) {
@@ -110,7 +108,7 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
* @param extension The extension layout type and name.
* @returns a [ComputedLayoutData] object, regardless of the specified LTNs or errors.
*/
private fun mergeLayouts(
private suspend fun mergeLayoutsAsync(
keyboardMode: KeyboardMode,
subtype: Subtype,
main: LTN? = null,
@@ -194,6 +192,28 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
}
}
// Add hints to keys
if (keyboardMode == KeyboardMode.CHARACTERS) {
val symbolsComputedArrangement = fetchComputedLayoutAsync(KeyboardMode.SYMBOLS, subtype).await().arrangement
for ((r, row) in computedArrangement.withIndex()) {
if (r >= 3) {
break
}
if (symbolsComputedArrangement.getOrNull(r) != null) {
for ((k, key) in row.withIndex()) {
if (key.type == KeyType.CHARACTER) {
val symbol = symbolsComputedArrangement[r].getOrNull(k)
if (r == 0) {
key.hintedNumber = symbol
} else {
key.hintedSymbol = symbol
}
}
}
}
}
}
return ComputedLayoutData(
keyboardMode,
"computed",
@@ -210,7 +230,7 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
* @param keyboardMode The keyboard mode for which the layout should be computed.
* @param subtype The subtype which localizes the computed layout.
*/
private fun computeLayoutFor(
private suspend fun computeLayoutFor(
keyboardMode: KeyboardMode,
subtype: Subtype
): ComputedLayoutData {
@@ -223,6 +243,9 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
main = LTN(LayoutType.CHARACTERS, subtype.layout)
modifier = LTN(LayoutType.CHARACTERS_MOD, "default")
}
KeyboardMode.EDITING -> {
// Layout for this mode is defined in custom layout xml file.
}
KeyboardMode.NUMERIC -> {
main = LTN(LayoutType.NUMERIC, "default")
}
@@ -246,7 +269,7 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
}
}
return mergeLayouts(keyboardMode, subtype, main, modifier, extension)
return mergeLayoutsAsync(keyboardMode, subtype, main, modifier, extension)
}
/**
@@ -263,14 +286,14 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
subtype: Subtype
): Deferred<ComputedLayoutData> {
val kms = KMS(keyboardMode, subtype)
val cachedComputedLayout = layoutCache[kms]
val cachedComputedLayout = computedLayoutCache[kms]
return if (cachedComputedLayout != null) {
cachedComputedLayout
} else {
val computedLayout = async(Dispatchers.IO) {
computeLayoutFor(keyboardMode, subtype)
}
layoutCache[kms] = computedLayout
computedLayoutCache[kms] = computedLayout
computedLayout
}
}
@@ -289,8 +312,8 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
subtype: Subtype
) {
val kms = KMS(keyboardMode, subtype)
if (layoutCache[kms] == null) {
layoutCache[kms] = async(Dispatchers.IO) {
if (computedLayoutCache[kms] == null) {
computedLayoutCache[kms] = async(Dispatchers.IO) {
computeLayoutFor(keyboardMode, subtype)
}
}

View File

@@ -33,7 +33,7 @@ class SmartbarManager private constructor() : FlorisBoard.EventListener {
val view = v as Button
val text = view.text.toString()
if (text.isNotEmpty()) {
textInputManager.commitCandidate(text)
florisboard.activeEditorInstance.commitCompletion(text)
}
}
private val candidateViewOnLongClickListener = View.OnLongClickListener { v ->
@@ -161,12 +161,13 @@ class SmartbarManager private constructor() : FlorisBoard.EventListener {
//spellCheckerSession?.close()
}
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
val isSelectionActive = florisboard.textInputManager.isTextSelected
override fun onUpdateSelection() {
val isSelectionActive = florisboard.activeEditorInstance.selection.isSelectionMode
smartbarView?.findViewById<View>(R.id.cc_cut)?.isEnabled = isSelectionActive
smartbarView?.findViewById<View>(R.id.cc_copy)?.isEnabled = isSelectionActive
smartbarView?.findViewById<View>(R.id.cc_paste)?.isEnabled =
florisboard.clipboardManager?.hasPrimaryClip() ?: false
smartbarView?.invalidate()
}
fun deleteCandidateFromDictionary(candidate: String) {
@@ -177,10 +178,10 @@ class SmartbarManager private constructor() : FlorisBoard.EventListener {
//
}
fun generateCandidatesFromComposing(composingText: String?) {
fun generateCandidatesFromComposing(composingText: String) {
val smartbarView = smartbarView ?: return
if (composingText == null) {
if (composingText == "") {
smartbarView.candidateViewList[0].text = "candidate"
smartbarView.candidateViewList[1].text = "suggestions"
smartbarView.candidateViewList[2].text = "nyi"

View File

@@ -17,13 +17,17 @@
package dev.patrickgold.florisboard.ime.text.smartbar
import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.util.setBackgroundTintColor2
/**
* Basically the same as an ImageButton.
* @see [onMeasure] why this view class exists.
*/
class SmartbarQuickActionButton : androidx.appcompat.widget.AppCompatImageButton {
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
@@ -37,4 +41,10 @@ class SmartbarQuickActionButton : androidx.appcompat.widget.AppCompatImageButton
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(heightMeasureSpec, heightMeasureSpec)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
setBackgroundTintColor2(this, prefs.theme.smartbarButtonBgColor)
setColorFilter(prefs.theme.smartbarButtonFgColor)
}
}

View File

@@ -17,6 +17,7 @@
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
@@ -25,8 +26,13 @@ import android.widget.Button
import android.widget.ImageButton
import android.widget.LinearLayout
import androidx.annotation.IdRes
import androidx.core.view.children
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.util.setImageTintColor2
import kotlinx.android.synthetic.main.florisboard.view.*
/**
* View class which keeps the references to important children and informs [SmartbarManager] that
@@ -34,7 +40,8 @@ import dev.patrickgold.florisboard.R
* a theme change).
*/
class SmartbarView : LinearLayout {
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
private val smartbarManager = SmartbarManager.getInstance()
private var variants: MutableList<ViewGroup> = mutableListOf()
@@ -100,12 +107,58 @@ class SmartbarView : LinearLayout {
}
}
/**
* Multiplies the default smartbar height with the given [factor] and sets it.
*/
fun setHeightFactor(factor: Float) {
val baseSize = resources.getDimension(R.dimen.smartbar_height)
val size = (baseSize * factor).toInt()
layoutParams?.height = size
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
val height = when (heightMode) {
MeasureSpec.EXACTLY -> {
// Must be this size
heightSize
}
MeasureSpec.AT_MOST -> {
// Can't be bigger than...
(florisboard?.inputView?.desiredSmartbarHeight ?: 0).coerceAtMost(heightSize)
}
else -> {
// Be whatever you want
florisboard?.inputView?.desiredSmartbarHeight ?: 0
}
}
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY))
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
setBackgroundColor(prefs.theme.smartbarBgColor)
for (container in containers) {
when (container.id) {
R.id.number_row -> {
for (button in container.children) {
if (button is Button) {
button.setTextColor(prefs.theme.smartbarFgColor)
}
}
}
R.id.clipboard_cursor_row -> {
for (button in container.children) {
if (button is ImageButton) {
if (button.isEnabled) {
setImageTintColor2(button, prefs.theme.smartbarFgColor)
} else {
setImageTintColor2(button, prefs.theme.smartbarFgColorAlt)
}
}
}
}
R.id.candidates -> {
for (view in container.children) {
if (view is Button) {
view.setTextColor(prefs.theme.smartbarFgColor)
}
}
}
}
}
}
}

View File

@@ -0,0 +1,310 @@
/*
* Copyright (C) 2020 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.theme
import android.content.Context
import android.graphics.Color
import com.squareup.moshi.Json
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dev.patrickgold.florisboard.ime.core.PrefHelper
/**
* Data class which holds a parsed theme json file. Used for loading a theme
* preset in Settings.
* Note: this implementation is generic and allows for any group/attr names.
* FlorisBoard itself expects certain groups and attrs to be able to
* color the controls accordingly. See 'ime/themes/floris_day.json'
* for a good example of which attributes FlorisBoard needs!
*
* @property name A unique id/name for this theme. Must only contain certain
* characters: upper/lower case letters, numbers (not at the beginning!) or
* an underline (_).
* @property displayName The name of this theme when shown to the user. Can
* contain any valid Unicode character.
* @property author The name of the author of this theme. Should be your
* username on GitHub/GitLab/BitBucket/... or your full name.
* @property isNightTheme If this theme is meant for display at day (false)
* or night (true). This property is only used to auto-assign this theme to
* either the day or night theme list in Settings, which is used when the
* user wants to auto-set his theme based on the current time.
* @property rawAttrs Map which holds the raw attributes of this theme. Note
* that the name of this property is 'attributes' within the json file!
* Attributes are always grouped together. This ensures a better structure
* and easier storage. The group- as well as the attr-name has the same
* limitations as the theme [name].
* Attribute values can be of different format:
* 1. A color
* Either #RRGGBB or #AARRGGBB (case-insensitive) -> e.g. #A034FF23
* 2. A static word
* - transparent (=0x00000000)
* - true (=0x1)
* - false (=0x0)
* 3. A reference to another attribute within the SAME theme, as follows:
* @group/attrName -> e.g. @window/textColor
* Note that referencing attributes has its limitations:
* a. Recursive references will cause an exception.
* b. Referencing an previously defined attribute is fine.
* c. Referencing an attribute not-yet defined is also ok, as long as
* the reference can be resolved at the next iteration.
* d. If the next iteration cannot resolve a value, an exception is
* thrown.
* 4. If the value is of any other format, an exception will be thrown.
*
* @throws IllegalArgumentException either at an invalid value or when a
* reference cannot be resolved.
*/
data class Theme(
val name: String,
val displayName: String,
val author: String,
val isNightTheme: Boolean = false,
@Json(name = "attributes")
private val rawAttrs: Map<String, Map<String, String>>
) {
/**
* Holds the parsed attributes after init.
*/
val parsedAttrs: MutableMap<String, MutableMap<String, Int>> = mutableMapOf()
companion object {
/**
* Loads a theme from the specified [path].
*
* @param context A reference to the current [Context]. Used to request
* asset file.
* @param path The path to the json theme file in the asset folder.
* @returns 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.
* @returns A parsed [Theme] or null. A null value may indicate that an error
* during the reading of the [rawData] occurred.
*/
fun fromJsonString(rawData: String): Theme? {
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
val layoutAdapter = moshi.adapter(Theme::class.java)
return layoutAdapter.fromJson(rawData)
}
/**
* Writes a given [theme] to the [prefs]. The default color values are based off the
* Floris Day theme and are not intended to be modified. Instead, themes should be defined
* in assets/ime/theme/<theme_id>.json
*
* @param theme The theme data.
* @param prefs The preference object to write the theme to.
*/
fun writeThemeToPrefs(prefs: PrefHelper, theme: Theme) {
// Internal prefs part I
prefs.internal.themeCurrentBasedOn = theme.name
prefs.internal.themeCurrentIsNight = theme.isNightTheme
// Theme attributes
prefs.theme.colorPrimary = theme.getAttr("window/colorPrimary", "#4CAF50")
prefs.theme.colorPrimaryDark = theme.getAttr("window/colorPrimaryDark", "#388E3C")
prefs.theme.colorAccent = theme.getAttr("window/colorAccent", "#FF9800")
prefs.theme.navBarColor = theme.getAttr("window/navigationBarColor", "#E0E0E0")
prefs.theme.navBarIsLight = (theme.getAttrOrNull("window/navigationBarLight") ?: 0) > 0
prefs.theme.keyboardBgColor = theme.getAttr("keyboard/bgColor", "#E0E0E0")
prefs.theme.keyBgColor = theme.getAttr("key/bgColor", "#FFFFFF")
prefs.theme.keyBgColorPressed = theme.getAttr("key/bgColorPressed", "#F5F5F5")
prefs.theme.keyFgColor = theme.getAttr("key/fgColor", "#000000")
prefs.theme.keyEnterBgColor = theme.getAttr("keyEnter/bgColor", "#4CAF50")
prefs.theme.keyEnterBgColorPressed = theme.getAttr("keyEnter/bgColorPressed", "#388E3C")
prefs.theme.keyEnterFgColor = theme.getAttr("keyEnter/fgColor", "#FFFFFF")
prefs.theme.keyPopupBgColor = theme.getAttr("keyPopup/bgColor", "#EEEEEE")
prefs.theme.keyPopupBgColorActive = theme.getAttr("keyPopup/bgColorActive", "#BDBDBD")
prefs.theme.keyPopupFgColor = theme.getAttr("keyPopup/fgColor", "#000000")
prefs.theme.keyShiftBgColor = theme.getAttr("keyShift/bgColor", "#FFFFFF")
prefs.theme.keyShiftBgColorPressed = theme.getAttr("keyShift/bgColorPressed", "#F5F5F5")
prefs.theme.keyShiftFgColor = theme.getAttr("keyShift/fgColor", "#000000")
prefs.theme.keyShiftFgColorCapsLock = theme.getAttr("keyShift/fgColorCapsLock", "#FF9800")
prefs.theme.mediaFgColor = theme.getAttr("media/fgColor", "#000000")
prefs.theme.mediaFgColorAlt = theme.getAttr("media/fgColorAlt", "#757575")
prefs.theme.oneHandedBgColor = theme.getAttr("oneHanded/bgColor", "#E8F5E9")
prefs.theme.oneHandedButtonFgColor = theme.getAttr("oneHandedButton/fgColor", "#424242")
prefs.theme.smartbarBgColor = theme.getAttr("smartbar/bgColor", "#E0E0E0")
prefs.theme.smartbarFgColor = theme.getAttr("smartbar/fgColor", "#000000")
prefs.theme.smartbarFgColorAlt = theme.getAttr("smartbar/fgColorAlt", "#4A000000")
prefs.theme.smartbarButtonBgColor = theme.getAttr("smartbarButton/bgColor", "#FFFFFF")
prefs.theme.smartbarButtonFgColor = theme.getAttr("smartbarButton/fgColor", "#000000")
// Internal prefs part II (must be written at the end!!)
prefs.internal.themeCurrentIsModified = false
}
}
init {
val listOfAttrsToReevaluate = mutableListOf<Triple<String, String, String>>()
for (group in rawAttrs) {
val groupMap = mutableMapOf<String, Int>()
parsedAttrs[group.key] = groupMap
for (attr in group.value) {
val colorRegex = """[#]([0-9a-fA-F]{8}|[0-9a-fA-F]{6})""".toRegex()
val refRegex = """[@]([a-zA-Z_][a-zA-Z0-9_]*)[/]([a-zA-Z_][a-zA-Z0-9_]*)""".toRegex()
when {
attr.value.matches(colorRegex) -> {
groupMap[attr.key] = Color.parseColor(attr.value)
}
attr.value == "transparent" -> {
groupMap[attr.key] = Color.TRANSPARENT
}
attr.value == "true" -> {
groupMap[attr.key] = 0x1
}
attr.value == "false" -> {
groupMap[attr.key] = 0x0
}
attr.value.matches(refRegex) -> {
val attrValue = getAttrOrNull(attr.value.substring(1))
if (attrValue != null) {
groupMap[attr.key] = attrValue
} else {
listOfAttrsToReevaluate.add(Triple(group.key, attr.key, attr.value))
}
}
else -> {
throw IllegalArgumentException("The specified attr '${attr.key}' = '${attr.value}' is not valid!")
}
}
}
}
for (attrToReevaluate in listOfAttrsToReevaluate) {
val attrValue = getAttrOrNull(attrToReevaluate.third.substring(1))
if (attrValue != null) {
parsedAttrs[attrToReevaluate.first]?.put(attrToReevaluate.second, attrValue)
} else {
throw IllegalArgumentException("The specified attr '${attrToReevaluate.second}' = '${attrToReevaluate.third}' is not valid!")
}
}
}
fun getAttr(key: String, defaultColor: String): Int {
return getAttrOrNull(key) ?: Color.parseColor(defaultColor)
}
fun getAttr(group: String, attr: String, defaultColor: String): Int {
return getAttrOrNull(group, attr) ?: Color.parseColor(defaultColor)
}
fun getAttrOrNull(key: String): Int? {
val regex = """([a-zA-Z_][a-zA-Z0-9_]*)[/]([a-zA-Z_][a-zA-Z0-9_]*)""".toRegex()
return if (key.matches(regex)) {
val split = key.split("/")
getAttrOrNull(split[0], split[1])
} else {
null
}
}
fun getAttrOrNull(group: String, attr: String): Int? {
return parsedAttrs[group]?.get(attr)
}
}
/**
* Data class which is used to quickly parse only the relevant meta data to
* display a theme in a selection list.
*
* @see [Theme] for details regarding the attributes and the theme structure.
*/
data class ThemeMetaOnly(
val name: String,
val displayName: String,
val author: String,
val isNightTheme: Boolean = false
) {
companion object {
/**
* Loads the theme meta data from the specified [path].
*
* @param context A reference to the current [Context]. Used to request
* asset file.
* @param path The path to the json theme file in the asset folder.
* @returns [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.
* @returns [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
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
/*
* Copyright (C) 2020 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.settings.fragments
import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat
import dev.patrickgold.florisboard.R
class KeyboardFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.prefs_keyboard)
}
}

View File

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

View File

@@ -0,0 +1,117 @@
/*
* 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.*
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,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
val m = resources.getDimension(R.dimen.keyboard_preview_margin).toInt()
setMargins(m, m, m, m)
}
prefs.sync()
keyboardView.isPreviewMode = true
val subtype = subtypeManager.getActiveSubtype() ?: Subtype.DEFAULT
keyboardView.computedLayout = layoutManager.fetchComputedLayoutAsync(KeyboardMode.CHARACTERS, subtype).await()
keyboardView.updateVisibility()
keyboardView.onApplyThemeAttributes()
withContext(Dispatchers.Main) {
binding.root.addView(keyboardView, 0)
}
}
loadThemePrefFragment()
return binding.root
}
private fun loadThemePrefFragment() {
childFragmentManager
.beginTransaction()
.replace(
binding.prefsFrame.id,
SettingsMainActivity.PrefFragment.createFromResource(R.xml.prefs_theme)
)
.commit()
}
override fun onSharedPreferenceChanged(sp: SharedPreferences?, key: String?) {
prefs.sync()
key ?: return
if (key == PrefHelper.Internal.THEME_CURRENT_BASED_ON ||
key == PrefHelper.Internal.THEME_CURRENT_IS_MODIFIED && !prefs.internal.themeCurrentIsModified) {
loadThemePrefFragment()
}
if (key.startsWith("theme__")) {
prefs.internal.themeCurrentIsModified = true
keyboardView.onApplyThemeAttributes()
keyboardView.invalidate()
keyboardView.invalidateAllKeys()
}
}
override fun onResume() {
prefs.shared.registerOnSharedPreferenceChangeListener(this)
super.onResume()
}
override fun onPause() {
prefs.shared.unregisterOnSharedPreferenceChangeListener(this)
super.onPause()
}
override fun onDestroy() {
cancel()
super.onDestroy()
}
}

View File

@@ -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
@@ -25,12 +25,13 @@ import android.widget.AdapterView
import android.widget.ArrayAdapter
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.databinding.ListItemBinding
import dev.patrickgold.florisboard.databinding.SettingsFragmentKeyboardBinding
import dev.patrickgold.florisboard.databinding.SettingsFragmentKeyboardSubtypeDialogBinding
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!
@@ -42,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
}
@@ -64,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,
@@ -93,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()
@@ -108,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()
@@ -121,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,
@@ -141,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)
@@ -152,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()

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,217 @@
/*
* Copyright (C) 2020 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.util
import android.text.InputType
import android.text.TextUtils
import android.view.inputmethod.EditorInfo
import kotlin.reflect.KClass
fun EditorInfo.debugSummarize(): String {
var summary = this::class.qualifiedName + "\r\n"
summary += "imeOptions: " + this.imeOptions.debugSummarize(EditorInfo::class) + "\r\n"
summary += "initialCapsMode: " + this.initialCapsMode.debugSummarize(TextUtils::class) + "\r\n"
summary += "initialSelStart: " + this.initialSelStart + "\r\n"
summary += "initialSelEnd: " + this.initialSelEnd + "\r\n"
summary += "inputType: " + this.inputType.debugSummarize(InputType::class) + "\r\n"
summary += "packageName: " + this.packageName
return summary
}
fun <T: Any> Int.debugSummarize(type: KClass<T>): String {
var summary = ""
when (type) {
EditorInfo::class -> {
when (this) {
EditorInfo.IME_NULL -> {
summary += "IME_NULL"
}
else -> {
val tAction = when (this and EditorInfo.IME_MASK_ACTION) {
EditorInfo.IME_ACTION_DONE -> "IME_ACTION_DONE"
EditorInfo.IME_ACTION_GO -> "IME_ACTION_GO"
EditorInfo.IME_ACTION_NEXT -> "IME_ACTION_NEXT"
EditorInfo.IME_ACTION_NONE -> "IME_ACTION_NONE"
EditorInfo.IME_ACTION_PREVIOUS -> "IME_ACTION_PREVIOUS"
EditorInfo.IME_ACTION_SEARCH -> "IME_ACTION_SEARCH"
EditorInfo.IME_ACTION_SEND -> "IME_ACTION_SEND"
EditorInfo.IME_ACTION_UNSPECIFIED -> "IME_ACTION_UNSPECIFIED"
else -> String.format("0x%08x", this and EditorInfo.IME_MASK_ACTION)
}
var tFlags = ""
if (this and EditorInfo.IME_FLAG_FORCE_ASCII > 0) {
tFlags += "IME_FLAG_FORCE_ASCII|"
}
if (this and EditorInfo.IME_FLAG_NAVIGATE_NEXT > 0) {
tFlags += "IME_FLAG_NAVIGATE_NEXT|"
}
if (this and EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS > 0) {
tFlags += "IME_FLAG_NAVIGATE_PREVIOUS|"
}
if (this and EditorInfo.IME_FLAG_NO_ACCESSORY_ACTION > 0) {
tFlags += "IME_FLAG_NO_ACCESSORY_ACTION|"
}
if (this and EditorInfo.IME_FLAG_NO_ENTER_ACTION > 0) {
tFlags += "IME_FLAG_NO_ENTER_ACTION|"
}
if (this and EditorInfo.IME_FLAG_NO_EXTRACT_UI > 0) {
tFlags += "IME_FLAG_NO_EXTRACT_UI|"
}
if (this and EditorInfo.IME_FLAG_NO_FULLSCREEN > 0) {
tFlags += "IME_FLAG_NO_FULLSCREEN|"
}
if (this and EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING > 0) {
tFlags += "IME_FLAG_NO_PERSONALIZED_LEARNING|"
}
if (tFlags.isEmpty()) {
tFlags = "(none)"
}
if (tFlags.endsWith("|")) {
tFlags = tFlags.substring(0, tFlags.length - 1)
}
summary += "action=$tAction flags=$tFlags"
}
}
}
InputType::class -> {
when (this) {
InputType.TYPE_NULL -> {
summary += "TYPE_NULL"
}
else -> {
val tClass: String
val tVariation: String
var tFlags = ""
when (this and InputType.TYPE_MASK_CLASS) {
InputType.TYPE_CLASS_DATETIME -> {
tClass = "TYPE_CLASS_DATETIME"
tVariation = when (this and InputType.TYPE_MASK_VARIATION) {
InputType.TYPE_DATETIME_VARIATION_DATE -> "TYPE_DATETIME_VARIATION_DATE"
InputType.TYPE_DATETIME_VARIATION_NORMAL -> "TYPE_DATETIME_VARIATION_NORMAL"
InputType.TYPE_DATETIME_VARIATION_TIME -> "TYPE_DATETIME_VARIATION_TIME"
else -> String.format("0x%08x", this and InputType.TYPE_MASK_VARIATION)
}
}
InputType.TYPE_CLASS_NUMBER -> {
tClass = "TYPE_CLASS_NUMBER"
tVariation = when (this and InputType.TYPE_MASK_VARIATION) {
InputType.TYPE_NUMBER_VARIATION_NORMAL -> "TYPE_NUMBER_VARIATION_NORMAL"
InputType.TYPE_NUMBER_VARIATION_PASSWORD -> "TYPE_NUMBER_VARIATION_PASSWORD"
else -> String.format("0x%08x", this and InputType.TYPE_MASK_VARIATION)
}
if (this and InputType.TYPE_NUMBER_FLAG_DECIMAL > 0) {
tFlags += "TYPE_NUMBER_FLAG_DECIMAL|"
}
if (this and InputType.TYPE_NUMBER_FLAG_SIGNED > 0) {
tFlags += "TYPE_NUMBER_FLAG_SIGNED|"
}
}
InputType.TYPE_CLASS_PHONE -> {
tClass = "TYPE_CLASS_PHONE"
tVariation = String.format("0x%08x", this and InputType.TYPE_MASK_VARIATION)
}
InputType.TYPE_CLASS_TEXT -> {
tClass = "TYPE_CLASS_TEXT"
tVariation = when (this and InputType.TYPE_MASK_VARIATION) {
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS -> "TYPE_TEXT_VARIATION_EMAIL_ADDRESS"
InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT -> "TYPE_TEXT_VARIATION_EMAIL_SUBJECT"
InputType.TYPE_TEXT_VARIATION_FILTER -> "TYPE_TEXT_VARIATION_FILTER"
InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE -> "TYPE_TEXT_VARIATION_LONG_MESSAGE"
InputType.TYPE_TEXT_VARIATION_NORMAL -> "TYPE_TEXT_VARIATION_NORMAL"
InputType.TYPE_TEXT_VARIATION_PASSWORD -> "TYPE_TEXT_VARIATION_PASSWORD"
InputType.TYPE_TEXT_VARIATION_PERSON_NAME -> "TYPE_TEXT_VARIATION_PERSON_NAME"
InputType.TYPE_TEXT_VARIATION_PHONETIC -> "TYPE_TEXT_VARIATION_PHONETIC"
InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS -> "TYPE_TEXT_VARIATION_POSTAL_ADDRESS"
InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE -> "TYPE_TEXT_VARIATION_SHORT_MESSAGE"
InputType.TYPE_TEXT_VARIATION_URI -> "TYPE_TEXT_VARIATION_URI"
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD -> "TYPE_TEXT_VARIATION_VISIBLE_PASSWORD"
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT -> "TYPE_TEXT_VARIATION_WEB_EDIT_TEXT"
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS -> "TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS"
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD -> "TYPE_TEXT_VARIATION_WEB_PASSWORD"
else -> String.format("0x%08x", this and InputType.TYPE_MASK_VARIATION)
}
if (this and InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE > 0) {
tFlags += "TYPE_TEXT_FLAG_AUTO_COMPLETE|"
}
if (this and InputType.TYPE_TEXT_FLAG_AUTO_CORRECT > 0) {
tFlags += "TYPE_TEXT_FLAG_AUTO_CORRECT|"
}
if (this and InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS > 0) {
tFlags += "TYPE_TEXT_FLAG_CAP_CHARACTERS|"
}
if (this and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES > 0) {
tFlags += "TYPE_TEXT_FLAG_CAP_SENTENCES|"
}
if (this and InputType.TYPE_TEXT_FLAG_CAP_WORDS > 0) {
tFlags += "TYPE_TEXT_FLAG_CAP_WORDS|"
}
if (this and InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE > 0) {
tFlags += "TYPE_TEXT_FLAG_IME_MULTI_LINE|"
}
if (this and InputType.TYPE_TEXT_FLAG_MULTI_LINE > 0) {
tFlags += "TYPE_TEXT_FLAG_MULTI_LINE|"
}
if (this and InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS > 0) {
tFlags += "TYPE_TEXT_FLAG_NO_SUGGESTIONS|"
}
}
else -> {
tClass = String.format("0x%08x", this and InputType.TYPE_MASK_CLASS)
tVariation = String.format("0x%08x", this and InputType.TYPE_MASK_VARIATION)
}
}
if (tFlags.isEmpty()) {
tFlags = "(none)"
}
if (tFlags.endsWith("|")) {
tFlags = tFlags.substring(0, tFlags.length - 1)
}
summary += "class=$tClass variation=$tVariation flags=$tFlags"
}
}
}
TextUtils::class -> {
var tFlags = ""
if (this and TextUtils.CAP_MODE_CHARACTERS > 0) {
tFlags += "CAP_MODE_CHARACTERS|"
}
if (this and TextUtils.CAP_MODE_SENTENCES > 0) {
tFlags += "CAP_MODE_SENTENCES|"
}
if (this and TextUtils.CAP_MODE_WORDS > 0) {
tFlags += "CAP_MODE_WORDS|"
}
if (this and TextUtils.SAFE_STRING_FLAG_FIRST_LINE > 0) {
tFlags += "SAFE_STRING_FLAG_FIRST_LINE|"
}
if (this and TextUtils.SAFE_STRING_FLAG_SINGLE_LINE > 0) {
tFlags += "SAFE_STRING_FLAG_SINGLE_LINE|"
}
if (this and TextUtils.SAFE_STRING_FLAG_TRIM > 0) {
tFlags += "SAFE_STRING_FLAG_TRIM|"
}
if (tFlags.isEmpty()) {
tFlags = "(none)"
}
if (tFlags.endsWith("|")) {
tFlags = tFlags.substring(0, tFlags.length - 1)
}
summary += "flags=$tFlags"
}
}
return summary
}

View File

@@ -6,6 +6,7 @@ import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import androidx.core.view.children
fun getColorFromAttr(
@@ -33,16 +34,28 @@ fun setBackgroundTintColor(view: View, colorId: Int) {
getColorFromAttr(view.context, colorId)
)
}
fun setBackgroundTintColor2(view: View, colorInt: Int) {
view.backgroundTintList = ColorStateList.valueOf(colorInt)
}
fun setDrawableTintColor(view: Button, colorId: Int) {
view.compoundDrawableTintList = ColorStateList.valueOf(
getColorFromAttr(view.context, colorId)
)
}
fun setDrawableTintColor2(view: Button, colorInt: Int) {
view.compoundDrawableTintList = ColorStateList.valueOf(colorInt)
}
fun setImageTintColor2(view: ImageView, colorInt: Int) {
view.imageTintList = ColorStateList.valueOf(colorInt)
}
fun setTextTintColor(view: View, colorId: Int) {
view.foregroundTintList = ColorStateList.valueOf(
getColorFromAttr(view.context, colorId)
)
}
fun setTextTintColor2(view: View, colorInt: Int) {
view.foregroundTintList = ColorStateList.valueOf(colorInt)
}
fun refreshLayoutOf(view: View?) {
if (view is ViewGroup) {

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false" android:color="?android:colorButtonNormal"/>
<item android:color="?smartbar_button_fgColor"/>
<item android:color="#FFFFFF"/>
</selector>

View File

@@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12.45,16h2.09L9.43,3L7.57,3L2.46,16h2.09l1.12,-3h5.64l1.14,3zM6.43,11L8.5,5.48 10.57,11L6.43,11zM21.59,11.59l-8.09,8.09L9.83,16l-1.41,1.41 5.09,5.09L23,13l-1.41,-1.41z"/>
</vector>

View File

@@ -2,16 +2,16 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<padding android:top="1dp" android:right="1dp" android:bottom="1dp" android:left="1dp" />
<solid android:color="?key_popup_extended_shadowColor" />
<corners android:radius="@dimen/key_borderRadius" />
<padding android:top="1dp" android:right="1dp" android:bottom="1dp" android:left="1dp"/>
<solid android:color="#CDAFAFAF"/>
<corners android:radius="@dimen/key_borderRadius"/>
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="?key_popup_bgColor" />
<corners android:radius="@dimen/key_borderRadius" />
<solid android:color="@android:color/white"/>
<corners android:radius="@dimen/key_borderRadius"/>
</shape>
</item>
</layer-list>

View File

@@ -0,0 +1,16 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/toolbar"/>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/advanced_fragment_frame"
android:name="dev.patrickgold.florisboard.settings.fragments.AdvancedFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>

View File

@@ -1,12 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<dev.patrickgold.florisboard.ime.editing.EditingKeyboardView
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyboardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/editing"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:background="@android:color/white"
android:backgroundTintMode="multiply">
<dev.patrickgold.florisboard.ime.editing.EditingKeyView
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
android:id="@+id/arrow_left"
style="@style/TextEditingButton"
app:layout_constraintWidth_percent="0.2"
@@ -16,7 +18,7 @@
app:layout_constraintBottom_toTopOf="@+id/move_home"
android:src="@drawable/ic_keyboard_arrow_left"/>
<dev.patrickgold.florisboard.ime.editing.EditingKeyView
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
android:id="@+id/arrow_up"
style="@style/TextEditingButton"
app:layout_constraintWidth_percent="0.3"
@@ -25,7 +27,7 @@
app:layout_constraintLeft_toRightOf="@+id/arrow_left"
android:src="@drawable/ic_keyboard_arrow_up"/>
<dev.patrickgold.florisboard.ime.editing.EditingKeyView
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
android:id="@+id/select"
style="@style/TextEditingButton"
app:layout_constraintWidth_percent="0.3"
@@ -34,7 +36,7 @@
app:layout_constraintLeft_toRightOf="@+id/arrow_left"
android:text="@android:string/selectTextMode"/>
<dev.patrickgold.florisboard.ime.editing.EditingKeyView
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
android:id="@+id/arrow_down"
style="@style/TextEditingButton"
app:layout_constraintWidth_percent="0.3"
@@ -43,7 +45,7 @@
app:layout_constraintLeft_toRightOf="@+id/arrow_left"
android:src="@drawable/ic_keyboard_arrow_down"/>
<dev.patrickgold.florisboard.ime.editing.EditingKeyView
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
android:id="@+id/arrow_right"
style="@style/TextEditingButton"
app:layout_constraintWidth_percent="0.2"
@@ -53,7 +55,7 @@
app:layout_constraintLeft_toRightOf="@+id/select"
android:src="@drawable/ic_keyboard_arrow_right"/>
<dev.patrickgold.florisboard.ime.editing.EditingKeyView
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
android:id="@+id/move_home"
style="@style/TextEditingButton"
app:layout_constraintWidth_percent="0.35"
@@ -62,7 +64,7 @@
app:layout_constraintBottom_toBottomOf="parent"
android:src="@drawable/ic_first_page"/>
<dev.patrickgold.florisboard.ime.editing.EditingKeyView
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
android:id="@+id/move_end"
style="@style/TextEditingButton"
app:layout_constraintWidth_percent="0.35"
@@ -71,7 +73,7 @@
app:layout_constraintBottom_toBottomOf="parent"
android:src="@drawable/ic_last_page"/>
<dev.patrickgold.florisboard.ime.editing.EditingKeyView
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
android:id="@+id/select_all"
style="@style/TextEditingButton"
app:layout_constraintWidth_percent="0.3"
@@ -82,7 +84,7 @@
android:text="@android:string/selectAll"
android:visibility="gone"/>
<dev.patrickgold.florisboard.ime.editing.EditingKeyView
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
android:id="@+id/clipboard_cut"
style="@style/TextEditingButton"
app:layout_constraintWidth_percent="0.3"
@@ -100,7 +102,7 @@
app:barrierDirection="bottom"
app:constraint_referenced_ids="select_all,clipboard_cut"/>
<dev.patrickgold.florisboard.ime.editing.EditingKeyView
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
android:id="@+id/clipboard_copy"
style="@style/TextEditingButton"
app:layout_constraintWidth_percent="0.3"
@@ -111,7 +113,7 @@
app:layout_constraintRight_toRightOf="parent"
android:text="@android:string/copy"/>
<dev.patrickgold.florisboard.ime.editing.EditingKeyView
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
android:id="@+id/clipboard_paste"
style="@style/TextEditingButton"
app:layout_constraintWidth_percent="0.3"
@@ -122,7 +124,7 @@
app:layout_constraintRight_toRightOf="parent"
android:text="@android:string/paste"/>
<dev.patrickgold.florisboard.ime.editing.EditingKeyView
<dev.patrickgold.florisboard.ime.text.editing.EditingKeyView
android:id="@+id/backspace"
style="@style/TextEditingButton"
app:layout_constraintWidth_percent="0.3"
@@ -133,4 +135,4 @@
app:layout_constraintRight_toRightOf="parent"
android:src="@drawable/ic_backspace"/>
</dev.patrickgold.florisboard.ime.editing.EditingKeyboardView>
</dev.patrickgold.florisboard.ime.text.editing.EditingKeyboardView>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<dev.patrickgold.florisboard.ime.core.InputView
<dev.patrickgold.florisboard.ime.core.InputWindowView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/florisboard"
android:layout_width="match_parent"
@@ -8,13 +8,13 @@
android:gravity="bottom"
android:orientation="vertical">
<LinearLayout
<dev.patrickgold.florisboard.ime.core.InputView
android:id="@+id/inner_input_view_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:orientation="horizontal"
android:background="?keyboard_bgColor"
android:background="?inputView_bgColorFallback"
android:baselineAligned="false">
<LinearLayout
@@ -25,32 +25,34 @@
<ImageButton
android:id="@+id/one_handed_ctrl_close_start"
style="@style/OneHandedPanelButton"
android:src="@drawable/ic_zoom_out_map"/>
android:src="@drawable/ic_zoom_out_map"
android:contentDescription="@string/one_handed__close_btn_content_description"/>
<View
android:layout_width="match_parent"
android:layout_height="@dimen/one_handed_button_height"
android:visibility="invisible" />
android:visibility="invisible"/>
<ImageButton
android:id="@+id/one_handed_ctrl_move_start"
style="@style/OneHandedPanelButton"
android:src="@drawable/ic_keyboard_arrow_left"/>
android:src="@drawable/ic_keyboard_arrow_left"
android:contentDescription="@string/one_handed__move_start_btn_content_description"/>
</LinearLayout>
<ViewFlipper
<dev.patrickgold.florisboard.ime.core.FlorisViewFlipper
android:id="@+id/main_view_flipper"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:measureAllChildren="false">
<include layout="@layout/text_input_layout" />
<include layout="@layout/text_input_layout"/>
<include layout="@layout/media_input_layout" />
<include layout="@layout/media_input_layout"/>
</ViewFlipper>
</dev.patrickgold.florisboard.ime.core.FlorisViewFlipper>
<LinearLayout
android:id="@+id/one_handed_ctrl_panel_end"
@@ -60,20 +62,22 @@
<ImageButton
android:id="@+id/one_handed_ctrl_close_end"
style="@style/OneHandedPanelButton"
android:src="@drawable/ic_zoom_out_map"/>
android:src="@drawable/ic_zoom_out_map"
android:contentDescription="@string/one_handed__close_btn_content_description"/>
<View
android:layout_width="match_parent"
android:layout_height="@dimen/one_handed_button_height"
android:visibility="invisible" />
android:visibility="invisible"/>
<ImageButton
android:id="@+id/one_handed_ctrl_move_end"
style="@style/OneHandedPanelButton"
android:src="@drawable/ic_keyboard_arrow_right"/>
android:src="@drawable/ic_keyboard_arrow_right"
android:contentDescription="@string/one_handed__move_end_btn_content_description"/>
</LinearLayout>
</LinearLayout>
</dev.patrickgold.florisboard.ime.core.InputView>
</dev.patrickgold.florisboard.ime.core.InputView>
</dev.patrickgold.florisboard.ime.core.InputWindowView>

View File

@@ -1,17 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<dev.patrickgold.florisboard.ime.popup.KeyPopupView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/key_popup"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@drawable/key_popup_bgshape">
android:background="@drawable/key_popup_bgshape"
android:backgroundTintMode="multiply">
<TextView
android:id="@+id/key_popup_text"
android:layout_width="match_parent"
android:layout_height="@dimen/key_height"
android:gravity="center"
android:textColor="?attr/key_popup_fgColor"
android:textSize="@dimen/key_popup_textSize"/>
<ImageView
@@ -23,7 +24,6 @@
android:layout_gravity="end"
android:padding="0dp"
android:src="@drawable/ic_more_horiz"
android:tint="?attr/key_popup_fgColor"
android:visibility="visible" />
android:visibility="visible"/>
</LinearLayout>
</dev.patrickgold.florisboard.ime.popup.KeyPopupView>

View File

@@ -1,13 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.flexbox.FlexboxLayout
<dev.patrickgold.florisboard.ime.popup.KeyPopupExtendedView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/key_popup_extended"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/key_popup_bgshape"
android:backgroundTintMode="multiply"
android:padding="0dp"
android:textSize="@dimen/key_popup_textSize"
app:flexDirection="row"
app:flexWrap="wrap"
app:showDivider="none" />
app:showDivider="none"/>

View File

@@ -3,15 +3,20 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:listPreferredItemHeightSmall"
android:orientation="vertical"
android:padding="16dp"
android:gravity="center_vertical"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd"
android:clickable="true"
android:focusable="true"
android:background="?selectableItemBackground">
<TextView
android:id="@android:id/title"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceListItem"
@@ -22,7 +27,7 @@
<TextView
android:id="@android:id/summary"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
android:textColor="?android:attr/textColorSecondary"

View File

@@ -1,20 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<dev.patrickgold.florisboard.ime.media.MediaInputView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/media_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ViewFlipper
<dev.patrickgold.florisboard.ime.core.FlorisViewFlipper
android:id="@+id/media_input_view_flipper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:measureAllChildren="true"/>
android:layout_height="0dp"
android:layout_weight="1.0"
android:measureAllChildren="false"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0.0"
android:orientation="horizontal">
<Button
@@ -87,4 +90,4 @@
</LinearLayout>
</LinearLayout>
</dev.patrickgold.florisboard.ime.media.MediaInputView>

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="GESTURES | not yet implemented..."/>
</LinearLayout>

View File

@@ -39,6 +39,16 @@
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
style="@style/SettingsCardView">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Note: Preferences tagged with [NYI] are not yet implemented and thus won\'t do anything or do some basic placeholder stuff only. Please do not file a bug report for these."/>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/repo_url_card"
style="@style/SettingsCardView.Clickable">
@@ -50,4 +60,28 @@
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/localization_card"
style="@style/SettingsCardView.Clickable">
<!-- TODO: create lang preview and make nice feature graphic for this card -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Localization prefs -->"/>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/theme_card"
style="@style/SettingsCardView.Clickable">
<!-- TODO: create theme preview and make nice feature graphic for this card -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Theme prefs -->"/>
</androidx.cardview.widget.CardView>
</LinearLayout>

View File

@@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.cardview.widget.CardView
style="@style/SettingsCardView"
android:layout_marginTop="16dp"
android:layout_marginBottom="0dp">
<LinearLayout
android:id="@+id/themeLinearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- KeyboardView preview will be inserted here programmatically -->
<!-- Must be RelativeLayout, because it blocks RecyclerView to create a second scrollbar -->
<RelativeLayout
android:id="@+id/prefs_theme_frame"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Must be RelativeLayout, because it blocks RecyclerView to create a second scrollbar -->
<RelativeLayout
android:id="@+id/prefs_looknfeel_frame"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>

View File

@@ -4,9 +4,11 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- KeyboardView preview will be inserted here programmatically -->
<!-- Must be RelativeLayout, because it blocks RecyclerView to create a second scrollbar -->
<RelativeLayout
android:id="@+id/prefs_advanced_frame"
android:id="@+id/prefs_frame"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

View File

@@ -12,20 +12,31 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:orientation="vertical"
android:theme="@style/PreferenceThemeOverlay">
<TextView
android:id="@+id/subtype_heading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="0dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:padding="8dp"
android:text="@string/settings__localization__title"
android:textAppearance="?preferenceCategoryTitleTextAppearance"
android:textColor="?colorAccent"/>
<TextView
android:id="@+id/subtype_not_conf_warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="16dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:layout_margin="8dp"
android:padding="8dp"
android:background="@drawable/shape_rect_rounded"
android:backgroundTint="?colorWarning"
android:text="@string/settings__keyboard__subtype_no_subtypes_configured_warning"
android:text="@string/settings__localization__subtype_no_subtypes_configured_warning"
android:textColor="?textColorWarning"/>
<LinearLayout
@@ -40,7 +51,7 @@
android:layout_height="wrap_content"
android:layout_gravity="end"
android:textAllCaps="false"
android:text="@string/settings__keyboard__subtype_add_title"
android:text="@string/settings__localization__subtype_add_title"
android:drawableStart="@drawable/ic_add"
android:drawablePadding="8dp"
android:drawableTint="?colorAccent"/>
@@ -51,7 +62,7 @@
<!-- Must be RelativeLayout, because it blocks RecyclerView to create a second scrollbar -->
<RelativeLayout
android:id="@+id/prefs_keyboard_frame"
android:id="@+id/prefs_frame"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

View File

@@ -12,7 +12,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorError"
tools:text="@string/settings__keyboard__subtype_error_already_exists"
tools:text="@string/settings__localization__subtype_error_already_exists"
android:textColor="@color/textColorError"
android:layout_marginBottom="8dp"
android:padding="8dp"
@@ -27,7 +27,7 @@
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/settings__keyboard__subtype_locale"
android:text="@string/settings__localization__subtype_locale"
android:paddingHorizontal="8dp"/>
<androidx.appcompat.widget.AppCompatSpinner
@@ -46,7 +46,7 @@
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/settings__keyboard__subtype_layout"
android:text="@string/settings__localization__subtype_layout"
android:paddingHorizontal="8dp"/>
<androidx.appcompat.widget.AppCompatSpinner

View File

@@ -4,8 +4,8 @@
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/smartbar"
android:layout_width="match_parent"
android:layout_height="@dimen/smartbar_height"
android:background="?smartbar_bgColor">
android:layout_height="wrap_content"
android:background="@android:color/transparent">
<LinearLayout
android:id="@+id/smartbar_variant_default"

View File

@@ -6,10 +6,10 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<include layout="@layout/smartbar" />
<include layout="@layout/smartbar"/>
<!-- KeyboardViews will be inserted in ViewFlipper below dynamically -->
<ViewFlipper
<dev.patrickgold.florisboard.ime.core.FlorisViewFlipper
android:id="@+id/text_input_view_flipper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -33,6 +33,6 @@
<include layout="@layout/editing_layout"/>
</ViewFlipper>
</dev.patrickgold.florisboard.ime.core.FlorisViewFlipper>
</LinearLayout>

View File

@@ -0,0 +1,29 @@
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<LinearLayout
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/settings__theme__preset_dialog_selected_theme"
android:textColor="?android:textColorPrimary"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/settings__theme__preset_dialog_available_themes"
android:textColor="?android:textColorPrimary"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@@ -0,0 +1,36 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd"
android:clickable="true"
android:focusable="true"
android:background="?selectableItemBackground">
<TextView
android:id="@android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
tools:text="Theme Name"
android:textColor="?android:textColorSecondary"/>
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1.0"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
android:src="@drawable/ic_keyboard_arrow_right"
android:contentDescription="@string/settings__theme__preset_dialog_alt_arrow_right"
app:tint="?android:textColorSecondary"/>
</LinearLayout>

View File

@@ -6,17 +6,24 @@
android:icon="@drawable/ic_more_vert"
android:title="@string/settings__menu"
app:showAsAction="always">
<menu>
<item
android:id="@+id/settings__menu_help"
android:orderInCategory="1"
android:title="@string/settings__menu_help" />
android:title="@string/settings__menu_help"/>
<item
android:id="@+id/settings__menu_advanced"
android:orderInCategory="2"
android:title="@string/settings__menu_advanced"/>
<item
android:id="@+id/settings__menu_about"
android:orderInCategory="2"
android:title="@string/settings__menu_about" />
android:orderInCategory="3"
android:title="@string/settings__menu_about"/>
</menu>
</item>
</menu>

View File

@@ -4,26 +4,26 @@
<item
android:id="@+id/settings__navigation__home"
android:icon="@drawable/ic_home"
android:title="@string/settings__navigation__home" />
android:title="@string/settings__navigation__home"/>
<item
android:id="@+id/settings__navigation__keyboard"
android:icon="@drawable/ic_keyboard"
android:title="@string/settings__navigation__keyboard" />
android:title="@string/settings__navigation__keyboard"/>
<item
android:id="@+id/settings__navigation__looknfeel"
android:id="@+id/settings__navigation__typing"
android:icon="@drawable/ic_spellcheck"
android:title="@string/settings__navigation__typing"/>
<item
android:id="@+id/settings__navigation__theme"
android:icon="@drawable/ic_palette"
android:title="@string/settings__navigation__looknfeel" />
android:title="@string/settings__navigation__theme"/>
<item
android:id="@+id/settings__navigation__gestures"
android:icon="@drawable/ic_gesture"
android:title="@string/settings__navigation__gestures" />
<item
android:id="@+id/settings__navigation__advanced"
android:icon="@drawable/ic_more_vert"
android:title="@string/settings__navigation__advanced" />
android:title="@string/settings__navigation__gestures"/>
</menu>

View File

@@ -32,51 +32,54 @@
<string name="settings__menu_help">Aiuto &amp; feedback</string>
<string name="settings__navigation__home">Home</string>
<string name="settings__navigation__keyboard">Tastiera</string>
<string name="settings__navigation__looknfeel">Aspetto &amp; funzionalità</string>
<string name="settings__navigation__typing">Digitazione</string>
<string name="settings__navigation__theme">Tema</string>
<string name="settings__navigation__gestures">Gesti</string>
<string name="settings__navigation__advanced">Avanzate</string>
<string name="settings__home__title">Benvenuto in %s</string>
<string name="settings__home__ime_not_enabled">FlorisBoard non è abilitato nel sistema e quindi non sarà disponibile come metodo di immissione.Clicca quì per risolvere questo problema.</string>
<string name="settings__home__ime_not_selected">FlorisBoard non è la tastiera predefinita. Clicca quì per risolvere questo problema.</string>
<string name="settings__home__contribute">Grazie per aver provato FlorisBoard! Questo progetto è ancora in fase alfa e quindi manca di alcune funzionalità. Se trovate qualche bug o volete dare un suggerimento, date un\'occhiata al repo su GitHub e segnalate un problema. Questo aiuta a rendere FlorisBoard migliore. Grazie!</string>
<string name="settings__keyboard__title">Tastiera &amp; Correzione del testo</string>
<string name="settings__keyboard__subtype_no_subtypes_configured_warning">Sembra che tu non abbia configurato nessuno stile di input personalizzato. Come ripiego verrà utilizzato lo stile input English/QWERTY!</string>
<string name="settings__keyboard__subtype_add">Aggiungi</string>
<string name="settings__keyboard__subtype_add_title">Aggiungi stile input</string>
<string name="settings__keyboard__subtype_apply">Applica</string>
<string name="settings__keyboard__subtype_cancel">Annulla</string>
<string name="settings__keyboard__subtype_delete">Elimina</string>
<string name="settings__keyboard__subtype_edit_title">Modifica stile di input</string>
<string name="settings__keyboard__subtype_locale">Locale</string>
<string name="settings__keyboard__subtype_layout">Layout della tastiera</string>
<string name="settings__keyboard__subtype_error_already_exists">Questo stile di input esiste già !</string>
<string name="settings__localization__title">Lingue &amp; Layout della tastiera</string>
<string name="settings__localization__subtype_no_subtypes_configured_warning">Sembra che tu non abbia configurato nessuno stile di input personalizzato. Come ripiego verrà utilizzato lo stile input English/QWERTY!</string>
<string name="settings__localization__subtype_add">Aggiungi</string>
<string name="settings__localization__subtype_add_title">Aggiungi stile input</string>
<string name="settings__localization__subtype_apply">Applica</string>
<string name="settings__localization__subtype_cancel">Annulla</string>
<string name="settings__localization__subtype_delete">Elimina</string>
<string name="settings__localization__subtype_edit_title">Modifica stile di input</string>
<string name="settings__localization__subtype_locale">Locale</string>
<string name="settings__localization__subtype_layout">Layout della tastiera</string>
<string name="settings__localization__subtype_error_already_exists">Questo stile di input esiste già !</string>
<string name="settings__theme__title">Tema tastiera</string>
<string name="pref__theme__name__label">Tema tastiera</string>
<string name="settings__keyboard__title">Tastiera preferenze</string>
<string name="pref__keyboard__group_layout__label">Layout</string>
<string name="pref__keyboard__one_handed_mode__label">Modalità ad una mano</string>
<string name="pref__keyboard__height_factor__label">Altezza tastiera</string>
<string name="pref__keyboard__group_keypress__label">Pressione tasti</string>
<string name="pref__keyboard__sound_enabled__label">Suono pressione tasti</string>
<string name="pref__keyboard__sound_volume__label">Volume del suono alla pressione dei tasti</string>
<string name="pref__keyboard__vibration_enabled__label">Vibrazione alla pressione dei tasti</string>
<string name="pref__keyboard__vibration_strength__label">Intensità della vibrazione alla pressione dei tasti</string>
<string name="pref__keyboard__popup_visible__label">Visibilità Popup</string>
<string name="pref__keyboard__popup_visible__summary">Mostra popup quando si preme un tasto</string>
<string name="pref__keyboard__long_press_delay__label">Ritardo lunga pressione tasti</string>
<string name="settings__typing__title">Esperienza di digitazione</string>
<string name="pref__suggestion__title">Suggerimenti</string>
<string name="pref__suggestion__enabled__label">Visualizza suggerimenti mentre digiti</string>
<string name="pref__suggestion__enabled__summary">Verrà visualizzato nella parte superiore della tastiera</string>
<string name="pref__suggestion__use_pref_words__label">Suggerimenti per la parola successiva</string>
<string name="pref__suggestion__use_pref_words__summary">Utilizzare le parole precedenti per generare suggerimenti</string>
<string name="pref__correction__title">Correzioni</string>
<string name="pref__popup__title">PopUp</string>
<string name="pref__correction__double_space_period__label">Doppio tocco barra spaziatrice</string>
<string name="pref__correction__double_space_period__summary">Doppio tocco su barra spaziatrice per mettere il punto (.) seguito da uno spazio</string>
<string name="pref__popup__visible__label">Visibilità Popup</string>
<string name="pref__popup__visible__summary">Mostra popup quando si preme un tasto</string>
<string name="settings__looknfeel__title">Aspetto &amp; funzionalità</string>
<string name="pref__looknfeel__group_layout__label">Layout</string>
<string name="pref__looknfeel__height_factor__label">Altezza tastiera</string>
<string name="pref__looknfeel__one_handed_mode__label">Modalità ad una mano</string>
<string name="pref__looknfeel__group_keypress__label">Pressione tasti</string>
<string name="pref__looknfeel__long_press_delay__label">Ritardo lunga pressione tasti</string>
<string name="pref__looknfeel__sound_enabled__label">Suono pressione tasti</string>
<string name="pref__looknfeel__sound_volume__label">Volume del suono alla pressione dei tasti</string>
<string name="pref__looknfeel__vibration_enabled__label">Vibrazione alla pressione dei tasti</string>
<string name="pref__looknfeel__vibration_strength__label">Intensità della vibrazione alla pressione dei tasti</string>
<string name="pref__theme__name__label">Tema tastiera</string>
<string name="settings__gestures__title">Gesti</string>
<string name="settings__gestures__title">Gesti &amp; Digitazione a scorrimento</string>
<string name="settings__advanced__title">Avanzate</string>
<string name="pref__advanced__settings_theme__label">Impostazioni tema</string>
@@ -100,7 +103,7 @@
<string name="setup__ok_button">OK</string>
<string name="setup__welcome__title">Benvenuto!</string>
<string name="setup__welcome__intro">TGrazie per aver provato FlorisBoard! Prima che possiate iniziare ad usarlo, dobbiamo fare le solite cose e abilitarlo nelle impostazioni di sistema, impostare la vostra lingua/ il layout preferito, ecc... Ma non preoccuparti: segui questa procedura guidata </string>
<string name="setup__welcome__intro">Grazie per aver provato FlorisBoard! Prima che possiate iniziare ad usarlo, dobbiamo fare le solite cose e abilitarlo nelle impostazioni di sistema, impostare la vostra lingua/ il layout preferito, ecc... Ma non preoccuparti: segui questa procedura guidata </string>
<string name="setup__welcome__privacy">[[ TODO: inserisci quì la descrizione della privacy ]]</string>
<string name="setup__welcome__trust">Il codice sorgente di FlorisBoard è accessibile pubblicamente a chiunque, quindi puoi facilmente rivedere cosa fa FlorisBoard in background. Controlla il link nel repository in basso.</string>
<string name="setup__welcome__contribute">Un\'ultima cosa prima di iniziare l\'installazione - se riscontri errori / arresti anomali / problemi con FlorisBoard o hai una richiesta di funzionalità - vai al repository GitHub collegato di seguito e presenta un problema. Questo aiuta a migliorare l\'esperienza per tutti gli utenti!</string>

View File

@@ -1,15 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="pref__looknfeel__height_factor__entries">
<item>@string/pref__looknfeel__height_factor__extra_short</item>
<item>@string/pref__looknfeel__height_factor__short</item>
<item>@string/pref__looknfeel__height_factor__mid_short</item>
<item>@string/pref__looknfeel__height_factor__normal</item>
<item>@string/pref__looknfeel__height_factor__mid_tall</item>
<item>@string/pref__looknfeel__height_factor__tall</item>
<item>@string/pref__looknfeel__height_factor__extra_tall</item>
<string-array name="pref__keyboard__height_factor__entries">
<item>@string/pref__keyboard__height_factor__extra_short</item>
<item>@string/pref__keyboard__height_factor__short</item>
<item>@string/pref__keyboard__height_factor__mid_short</item>
<item>@string/pref__keyboard__height_factor__normal</item>
<item>@string/pref__keyboard__height_factor__mid_tall</item>
<item>@string/pref__keyboard__height_factor__tall</item>
<item>@string/pref__keyboard__height_factor__extra_tall</item>
</string-array>
<string-array name="pref__looknfeel__height_factor__values">
<string-array name="pref__keyboard__height_factor__values">
<item>extra_short</item>
<item>short</item>
<item>mid_short</item>
@@ -19,12 +19,12 @@
<item>extra_tall</item>
</string-array>
<string-array name="pref__looknfeel__one_handed_mode__entries">
<item>@string/pref__looknfeel__one_handed_mode__off</item>
<item>@string/pref__looknfeel__one_handed_mode__right</item>
<item>@string/pref__looknfeel__one_handed_mode__left</item>
<string-array name="pref__keyboard__one_handed_mode__entries">
<item>@string/pref__keyboard__one_handed_mode__off</item>
<item>@string/pref__keyboard__one_handed_mode__right</item>
<item>@string/pref__keyboard__one_handed_mode__left</item>
</string-array>
<string-array name="pref__looknfeel__one_handed_mode__values">
<string-array name="pref__keyboard__one_handed_mode__values">
<item>off</item>
<item>end</item>
<item>start</item>
@@ -50,12 +50,58 @@
<item>clipboard_cursor_tools</item>
</string-array>
<string-array name="pref__theme__name__entries">
<item>Floris Light</item>
<item>Floris Dark</item>
<string-array name="pref__gestures__swipe_action__entries">
<item>@string/pref__gestures__swipe_action__no_action</item>
<item>@string/pref__gestures__swipe_action__delete_word</item>
<item>@string/pref__gestures__swipe_action__hide_keyboard</item>
<item>@string/pref__gestures__swipe_action__move_cursor_up</item>
<item>@string/pref__gestures__swipe_action__move_cursor_down</item>
<item>@string/pref__gestures__swipe_action__move_cursor_left</item>
<item>@string/pref__gestures__swipe_action__move_cursor_right</item>
<item>@string/pref__gestures__swipe_action__shift</item>
<item>@string/pref__gestures__swipe_action__switch_to_prev_subtype</item>
<item>@string/pref__gestures__swipe_action__switch_to_next_subtype</item>
</string-array>
<string-array name="pref__theme__name__values">
<item>floris_light</item>
<item>floris_dark</item>
<string-array name="pref__gestures__swipe_action__values">
<item>no_action</item>
<item>delete_word</item>
<item>hide_keyboard</item>
<item>move_cursor_up</item>
<item>move_cursor_down</item>
<item>move_cursor_left</item>
<item>move_cursor_right</item>
<item>shift</item>
<item>switch_to_prev_subtype</item>
<item>switch_to_next_subtype</item>
</string-array>
<string-array name="pref__gestures__swipe_velocity_threshold__entries">
<item>@string/pref__gestures__swipe_velocity_threshold__very_slow</item>
<item>@string/pref__gestures__swipe_velocity_threshold__slow</item>
<item>@string/pref__gestures__swipe_velocity_threshold__normal</item>
<item>@string/pref__gestures__swipe_velocity_threshold__fast</item>
<item>@string/pref__gestures__swipe_velocity_threshold__very_fast</item>
</string-array>
<string-array name="pref__gestures__swipe_velocity_threshold__values">
<item>very_slow</item>
<item>slow</item>
<item>normal</item>
<item>fast</item>
<item>very_fast</item>
</string-array>
<string-array name="pref__gestures__swipe_distance_threshold__entries">
<item>@string/pref__gestures__swipe_distance_threshold__very_short</item>
<item>@string/pref__gestures__swipe_distance_threshold__short</item>
<item>@string/pref__gestures__swipe_distance_threshold__normal</item>
<item>@string/pref__gestures__swipe_distance_threshold__long</item>
<item>@string/pref__gestures__swipe_distance_threshold__very_long</item>
</string-array>
<string-array name="pref__gestures__swipe_distance_threshold__values">
<item>very_short</item>
<item>short</item>
<item>normal</item>
<item>long</item>
<item>very_long</item>
</string-array>
</resources>

View File

@@ -14,39 +14,12 @@
<attr name="android:text" format="string|reference"/>
</declare-styleable>
<declare-styleable name="KeyboardTheme">
<attr name="keyboardViewStyle" format="reference" />
<attr name="keyboardRowViewStyle" format="reference" />
<declare-styleable name="KeyboardThemeBase">
<attr name="keyboardViewStyle" format="reference"/>
<attr name="keyboardRowViewStyle" format="reference"/>
<attr name="semiTransparentColor" format="color" />
<attr name="key_bgColor" format="color" />
<attr name="key_bgColorPressed" format="color" />
<attr name="key_fgColor" format="color" />
<attr name="key_enter_fgColor" format="color" />
<attr name="key_popup_bgColor" format="color" />
<attr name="key_popup_fgColor" format="color" />
<attr name="key_popup_extended_bgColor" format="color" />
<attr name="key_popup_extended_bgColorActive" format="color" />
<attr name="key_popup_extended_shadowColor" format="color" />
<attr name="keyboard_bgColor" format="color" />
<attr name="emoji_key_bgColor" format="color" />
<attr name="emoji_key_bgColorPressed" format="color" />
<attr name="emoji_key_fgColor" format="color" />
<attr name="one_handed_bgColor" format="color" />
<attr name="one_handed_button_fgColor" format="color" />
<attr name="smartbar_bgColor" format="color" />
<attr name="smartbar_fgColor" format="color" />
<attr name="smartbar_button_bgColor" format="color" />
<attr name="smartbar_button_bgColorPressed" format="color" />
<attr name="smartbar_button_fgColor" format="color" />
<attr name="smartbar_candidate_fgColor" format="color" />
<attr name="semiTransparentColor" format="color"/>
<attr name="inputView_bgColorFallback" format="color"/>
</declare-styleable>
<declare-styleable name="SettingsTheme">

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