Compare commits

...

107 Commits

Author SHA1 Message Date
Patrick Goldinger
1307f401cc Release v0.2.6 2020-12-01 20:46:24 +01:00
Patrick Goldinger
ca6006767b Improve key font sizing (#48)
- Key font size is now generated with a better algorithm.
- Key font size in general is now bigger and the letter/white space
  ratio has been improved.
2020-12-01 19:57:58 +01:00
Patrick Goldinger
2202db53ba Add reference to permission list to README.md 2020-12-01 16:46:08 +01:00
Patrick Goldinger
321f19272e Fix Smartbar number row disappearing incorrectly (#52) 2020-11-30 22:24:06 +01:00
Patrick Goldinger
06a8a04020 Improve keyboard height calculation (#50) 2020-11-30 22:03:33 +01:00
Patrick Goldinger
2a1f7c3217 Add Horizontal Ellipsis (Three-dots) character to symbols (#51) 2020-11-30 18:18:02 +01:00
Patrick Goldinger
76952d55fe Release v0.2.5 2020-11-29 23:30:19 +01:00
florisboard-bot
1f560f8b6b Update translations from Crowdin (#49) 2020-11-29 23:11:26 +01:00
Patrick Goldinger
33bdc52354 Add precise delete key gesture for characters (#25) 2020-11-29 22:46:10 +01:00
Patrick Goldinger
97b795aed0 Fix status bar incorrectly drawn in Android 11 (#43) 2020-11-29 18:33:52 +01:00
Patrick Goldinger
bb44362701 Fix EmojiKeyboardView init crash in Android 6.0 (#41) 2020-11-28 19:11:18 +01:00
Patrick Goldinger
bab20c5baa Add comments to strings.xml to help translators
- This is done to help translators in Crowdin better understanding
  in which context a string is used.
2020-11-27 19:45:11 +01:00
Patrick Goldinger
a3000fe111 Update README.md and CONTRIBUTING.md
- Now includes links to the Crowdin project.
- Add Crowdin badge.
- Update some paragraphs and the layout.
2020-11-26 19:56:15 +01:00
florisboard-bot
d4d2f52683 Update Crowdin configuration file 2020-11-26 00:28:39 +01:00
florisboard-bot
10ef340559 Update Crowdin configuration file 2020-11-26 00:09:38 +01:00
Patrick Goldinger
5b77262186 Prepare string resource files for Crowdin 2020-11-25 21:47:53 +01:00
Patrick Goldinger
8ce56b1bf9 Fix error log output omitting line separator characters 2020-11-24 19:26:20 +01:00
Patrick Goldinger
94667e8363 Fix keyboard crashing when long pressing delete key (#40) 2020-11-24 18:33:27 +01:00
Patrick Goldinger
970b5eb82a Release v0.2.4 2020-11-22 21:46:36 +01:00
Patrick Goldinger
a2ceed4521 Improve Smartbar layout / Add clipboard content suggestions (#38)
- This commit adds clipboard content suggestions. These suggestions do
  only show if suggestions in general are turned on.
- The suggestions show for both text and images in the clipboard, but
  do currently only work for text.
- Clipboard/Cursor row is now a proper KeyboardView, which gets rid of
  the hardcoded keys for the arrows / clipboard commands.
- Fix errors in doc strings.
- Fix other logic errors in TextInputManager and EditorInstance.
2020-11-22 21:25:35 +01:00
Patrick Goldinger
6d7825e129 Add crash handler and error detail form
- This crash handler catches nearly all uncaught errors and notifies
  the user about it. If an uncaught error occurs in the FlorisBoard
  service initialization, the handler detects this and switches to
  another installed keyboard.
- The error detail form contains the captured stacktrace and adds
  a copy to clipboard functionality as well as a button to the
  GitHub issue tracker.
2020-11-19 23:59:23 +01:00
Patrick Goldinger
10c1a82995 Rework core to fix potential crashes when entering text 2020-11-17 18:34:57 +01:00
Patrick Goldinger
267a39e870 Add basic clipboard text suggestion to Smartbar (#38) 2020-11-16 23:52:51 +01:00
Patrick Goldinger
f6fcbbcc34 Update project dependencies and build.gradle 2020-11-16 18:32:55 +01:00
Patrick Goldinger
f98b3cec4b Improve layout and behavior of number row in Smartbar (#31)
- Number row is now a proper keyboard instead of a LinearLayout with
  hardcoded keys.
- Number row takes whole Smartbar width by hiding the Smartbar arrow
  (improves size per number key, which allows it to be more easily
  touchable).
2020-11-15 23:43:16 +01:00
Patrick Goldinger
e5a942be9f Add support for raw text editors (e.g. terminals, ...)
- FlorisBoard is now able to perform input on raw input editors (editors
  which either have an incomplete, faulty or purposely simple
  implementation).
- Especially targeted at terminal apps, as these apps do not manage the
  state of the input but only forward it.
2020-11-13 20:56:41 +01:00
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
Patrick Goldinger
0eb5ca318b Release v0.1.2 2020-08-16 22:41:06 +02:00
Patrick Goldinger
dfa9df6cd6 Merge pull request #13 from florisboard/feat-clipboard-cursor-tools
Add clipboard cursor related tools
2020-08-16 22:13:19 +02:00
Patrick Goldinger
3f5dfbc852 Add number row and clipboard cursor tools to Smartbar (#9, #3)
- Smartbar now supports showing a number row or a clipboard/cursor toolbar
  if candidate suggestions are disabled. What toolbar will be shown is
  controlled by prefs.suggestion.showInstead
- Smartbar now shows a back button when the active keyboard mode is
  KeyboardMode.EDITING
- Improvements in backend of Smartbar
2020-08-16 17:07:52 +02:00
Patrick Goldinger
59caafbf19 Implement cursor movement and clipboard functionality
- Add backend handling for editing layout.
- Improve FlorisBoard event listener implementation, allow different
  objects than Text-/MediaInputManager to receive events.
- Add function to send a key event to the system (allows to write this
  in a single line, which is more readable)
2020-08-15 20:26:06 +02:00
Patrick Goldinger
037a452baf Add SmartbarQuickAction to access editing layout 2020-08-14 19:17:13 +02:00
Patrick Goldinger
ffa405f289 Add clipboard and cursor editing layout (UI) 2020-08-14 18:54:35 +02:00
Patrick Goldinger
5d7091582f Modify prefs_looknfeel.xml to use new seek bar pref 2020-08-11 23:12:15 +02:00
Patrick Goldinger
b4096f2cfb Add DialogSeekBarPreference in .settings.components
- This SeekBar implementation allows for better control of min/max/step.
- The current value of the preference is shown in the summary, to change it
  the user has to click on it, where a dialog window with a SeekBar opens.
2020-08-11 23:06:51 +02:00
Patrick Goldinger
81c62f3e91 Improve list item layout
- List item now uses Android's predefined ids for title and summary
- Needed for custom preference implementation
- Layout font size in list_item.xml is now the same as the other list items
2020-08-11 22:58:49 +02:00
Patrick Goldinger
5c7db2b344 Simplify feature_request.md issue template 2020-08-10 23:26:17 +02:00
Patrick Goldinger
30bca99092 Fix extended key popup not aligning correctly
when FlorisBoard initializes with prefs.popup.enabled=false. This was
due to the fact that the show() method of KeyPopupManager did some required
calculations which were not done if the popups were disabled.
2020-08-10 22:59:06 +02:00
Patrick Goldinger
9a9445dab1 Merge pull request #8 from hamedsj/master
Add Disable Checkbox for "Preview Popup" feature
2020-08-10 21:54:45 +02:00
hamedsj
1fbfc32429 Add checkbox for disable "Preview PopUp" in keyboard settings 2020-08-10 23:45:17 +04:30
hamedsj
645b682451 Add Keshida character to untranslatable strings/keys 2020-08-10 22:56:42 +04:30
Patrick Goldinger
63ed46ccf4 Release v0.1.1 2020-08-09 20:43:01 +02:00
Patrick Goldinger
236f682622 Fix LayoutManager incorrectly merging main and mod layout
- Related to issue mentioned in #6
2020-08-09 20:23:27 +02:00
Patrick Goldinger
bef69b3187 Merge pull request #6 from PHELAT/master
Add support for persian keyboard layout
2020-08-09 20:14:03 +02:00
Mahdi Nouri
235224aefd Move the delete key back to the modifier layout 2020-08-09 16:54:09 +04:30
qw123wh
2489872589 Add Italian PlayStore translation by Qw123wh (#5)
- Update full_description.txt
- Add files via upload
- Update full_description.txt
2020-08-09 09:35:53 +02:00
Mahdi Nouri
33a9504707 Add support for persian layout 2020-08-08 18:46:45 +04:30
Mahdi Nouri
0fb7bbb034 Add support for half-space key with an appropriate icon 2020-08-08 17:53:37 +04:30
Mahdi Nouri
beef54941f Add the ability to load a customized layout modifier for each layout 2020-08-08 16:58:49 +04:30
Patrick Goldinger
634ff4972d Show number row in Smartbar if composing word suggestions is disabled 2020-08-08 11:27:22 +02:00
Patrick Goldinger
52f3477e24 Merge branch 'master' of https://github.com/florisboard/florisboard 2020-08-07 21:24:29 +02:00
Patrick Goldinger
1086464b09 Add string resources for preferences items / Fix apostrophe issues 2020-08-07 21:24:10 +02:00
Patrick Goldinger
2e4267aad4 Update full_description.txt
- Add supported keyboard layouts to the list
2020-08-07 19:07:48 +02:00
Patrick Goldinger
b14fe8ad03 Merge pull request #4 from qw123wh/qw123wh--italian-translate
Add Italian translation
2020-08-07 17:42:00 +02:00
qw123wh
82d6141fb2 Italian translation
Add Italian translation
2020-08-07 14:48:41 +02:00
Patrick Goldinger
e0e7bcc08a Split strings.xml into translatable and non-translatable file
- Done to easily identify which strings should be localized and which not
- Add translating guide in CONTRIBUTING.md
2020-08-06 23:08:27 +02:00
181 changed files with 8508 additions and 1936 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
<!-- (remove this line if you paste a log)
```
If applicable, paste the captured debug log here.
```
(remove this line if you paste a log) -->

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
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,21 +1,19 @@
---
name: Feature request
about: Suggest an idea or enhancement for this project
name: Feature request / Suggestion
about: Suggest an idea for this project
title: ''
labels: proposal
assignees: ''
---
#### Short description of your idea
A short but clear and concise description of your idea.
#### Detailed description of your idea
A clear and concise description of what you want to be added or changed. If you also have
an idea how to implement it, please describe it here.
#### Alternatives to your idea
If you have considered an alternative solution for your idea, describe it here.
#### Additional context
Add any other context or screenshots about the feature request here.
<!--
- Describe your idea in a short but concise way.
- If you have multiple ideas which are not directly connected to each
other, file an issue per idea. This makes it easy to implement one
feature proposal at a time.
- If you have any examples, e.g. screenshots or other keyboards which
have the proposed feature implemented, link them here.
- Please search existing proposals to avoid creating duplicates.
- Thank you for your help in making FlorisBoard better!
-->

16
.github/ISSUE_TEMPLATE/question.md vendored Normal file
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

@@ -2,32 +2,57 @@
First off, thanks for considering contributing to FlorisBoard!
There are several ways to contribute to FlorisBoard. This document provides some
general guidelines for each type of contribution.
There are several ways to contribute to FlorisBoard. This document
provides some general guidelines for each type of contribution.
## Giving general feedback
Either use the review function within Google Play or email me at
[florisboard@patrickgold.dev](mailto:florisboard@patrickgold.dev). I
love to hear from you!
## Translations
To make FlorisBoard accessible in as many languages as possible, the
platform ![Crowdin](https://crowdin.florisboard.patrickgold.dev) is used
to crowdsource and manage translations. This is the only source of
translations from now on - **PRs that add/update translations are no
longer accepted.** The list of languages in Crowdin covers the top 20
languages, but feel free to email me at
[florisboard@patrickgold.dev](mailto:florisboard@patrickgold.dev) to
request a language and I'll add it.
## Adding a new feature or making large changes
If you intend to add a new feature or to make large changes, please discuss this
first through a proposal on GitHub. Discussing your idea enables both you and the
dev team that we are on the same page before you start on working on your change.
If you have any questions, feel free to ask for help at any time!
If you intend to add a new feature or to make large changes, please
discuss this first through a proposal on GitHub. Discussing your idea
enables both you and the dev team that we are on the same page before
you start on working on your change. If you have any questions, feel
free to ask for help at any time!
## Adding a new keyboard layout / dictionary for locale
As FlorisBoard is currently in alpha stage, things might change drastically. This
also includes the config scheme of keyboard layouts. To prevent incompatible
configs because some features and structures may change, please do not add this
kind of content yet. As FlorisBoard's state progresses and its core stabilizes,
you will be able to add keyboard layouts.
As FlorisBoard is currently in alpha stage, things might change
drastically. This also includes the config scheme of keyboard layouts.
To prevent incompatible configs because some features and structures may
change, please do not add this kind of content yet. As FlorisBoard's
state progresses and its core stabilizes, you will be able to add
keyboard layouts.
## Bug reporting
This kind of contribution is the most important, as it tells where FlorisBoard
has flaws and thus should be imroved to maximize stability and user experience.
To make this process as smooth as possible, please use the premade [issue
template](.github/ISSUE_TEMPLATE/bug_report.md) for bug reporting. This makes it
easy for us to understand what the bug is and how to solve it.
This kind of contribution is the most important, as it tells where
FlorisBoard has flaws and thus should be improved to maximize stability
and user experience. To make this process as smooth as possible, please
use the premade [issue template](.github/ISSUE_TEMPLATE/bug_report.md)
for bug reporting. This makes it easy for us to understand what the bug
is and how to solve it.
### Capturing ADB debug logs
### Capturing error logs
[[ TODO: create tutorial ]]
Logs are captured by FlorisBoard's crash handler, which gives you the
ability to copy it to the clipboard and paste it in GitHub. This is the
preferred way to capture logs.
Alternatively, you can also use ADB (Android Debug Bridge) to capture
the error log. This is recommended for experienced users only.

110
README.md
View File

@@ -1,10 +1,23 @@
# FlorisBoard
<img align="left" width="80" height="80"
src="fastlane/metadata/android/en-US/images/icon.png" alt="App icon">
An open-source keyboard for Android. Currently in alpha stage.
# FlorisBoard ![Release](https://img.shields.io/github/v/release/florisboard/florisboard) [![Crowdin](https://badges.crowdin.net/florisboard/localized.svg)](https://crowdin.florisboard.patrickgold.dev)
#### Public Alpha Test Programme
Wanna try it out on your device? You can join the public alpha test
programme on Google Play. To become a tester, follow these steps:
**FlorisBoard** is a free and open-source keyboard for Android 6.0+
devices. It aims at being modern, user-friendly and customizable while
fully respecting your privacy. Currently in alpha/early-beta state.
## Public Alpha Test Programme
Wanna try it out on your device? Use one of the following options:
_A. IzzySoft's repo for F-Droid_:
[<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" height="64" alt="IzzySoft repo badge">](https://apt.izzysoft.de/fdroid/index/apk/dev.patrickgold.florisboard)
_B. Google Play Public Alpha Test_:
You can join the public alpha test programme on Google Play. To become a
tester, follow these steps:
1. Join the
[FlorisBoard Public Alpha Test](https://groups.google.com/g/florisboard-public-alpha-test)
Google Group to be able to access the testing programme.
@@ -18,65 +31,63 @@ programme on Google Play. To become a tester, follow these steps:
4. Finished! You will receive future versions of FlorisBoard via Google
Play.
##### Giving feedback
If you want to give feedback to FlorisBoard, there are 2 ways to do so,
as listed below:
- *General feedback:* use the private feedback to developer section on
the PlayStore listing.
- *Bug reports or feature requests:* see the
[contribution guidelines](CONTRIBUTING.md)
_C. Use the APK provided in the release section of this repo_
Thank you for contributing to FlorisBoard!
### Giving feedback
If you want to give feedback to FlorisBoard, there are several ways to
do so, as listed [here](CONTRIBUTING.md#giving-general-feedback).
##### Note on F-Droid release
FlorisBoard is currently only available through Google Play, but it is
planned to also release it via F-Droid later on. There is no exact
timeline for this, but I aim for the 0.2.0 or 0.3.0 release.
### Note on F-Droid release
FlorisBoard is currently available through Google Play and IzzySoft's
repo for F-Droid, but is in the inclusion process for the main F-Droid
repo. Planned proper F-Droid release is version 0.3.0.
---
![Preview image](https://patrickgold.dev/media/previews/florisboard.png)
<img align="right" height="256"
src="https://patrickgold.dev/media/previews/florisboard-preview-day.png"
alt="Preview image">
## Feature roadmap
### Basics
* [x] Implementation of the keyboard core (InputMethodService)
* [x] Own implementation of deprecated KeyboardView (base only)
* [x] Custom implementation of deprecated KeyboardView (base only)
* [x] Caps + Caps Lock
* [x] Key popups
* [x] Extended key popups (e.g. a -> á, à, ä, ...) (needs tweaks for
emojis)
* [x] Extended key popups (e.g. a -> á, à, ä, ...)
* [x] Key press sound/vibration
* [x] Portrait orientation support
* [x] Landscape orientation support (needs tweaks)
* [ ] Tablet screen support
* [ ] Tablet screen support (0.4.0)
### Layouts
* [x] Latin character layout (QWERTY)
* [x] Other character layouts (both latin and non-latin) (Currently
QWERTZ, AZERTY, swiss and spanish are supported besides QWERTY)
* [x] Latin character layouts (QWERTY, QWERTZ, AZERTY, Swiss, Spanish,
Norwegian, Swedish/Finnish, Icelandic, Danish); more coming in
future versions
* [x] Non-latin character layouts (Persian)
* [x] Adapt to situation in app (password, url, text, etc. )
* [x] Special character layout(s)
* [x] Numeric layout
* [x] Numeric layout (advanced)
* [x] Phone number layout
* [x] Emoji layout (popups buggy atm)
* [x] Emoji layout (tweaks: 0.3.0)
* [x] Emoticon layout
* [ ] Kaomoji layout
* [ ] Kaomoji layout (0.5.0)
### Preferences
* [x] Setup wizard
* [x] Preferences screen
* [x] Customize look and behaviour of keyboard (currently only
light/dark theme)
* [ ] Theme customization
* [ ] Theme import/export (?)
* [x] Customize look and behaviour of keyboard
* [x] Theme presets (currently only day/night theme)
* [x] Theme customization
* [ ] Theme import/export (0.4.0 or 0.5.0)
* [x] Subtype selection (language/layout)
* [x] Keyboard behaviour preferences
* [ ] Text suggestion / Auto correct preferences
* [ ] Gesture preferences
* [ ] Text suggestion / Auto correct preferences (0.4.0 or 0.5.0)
* [x] Gesture preferences (0.3.0)
### Composing suggestions
### Composing suggestions (0.4.0 or 0.5.0)
* [ ] Auto suggest words from precompiled dictionary
* [ ] Auto suggest words from user dictionary
* [ ] Auto suggest contacts
@@ -84,26 +95,45 @@ timeline for this, but I aim for the 0.2.0 or 0.3.0 release.
### Other useful features
* [x] One-handed mode
* [ ] Clipboard manager (?)
* [ ] Floating keyboard
* [ ] Gesture support
* [ ] Glide typing (?)
* [x] Clipboard/cursor tools
* [x] Integrated number row / symbols in character layouts (0.3.0)
* [ ] Floating keyboard (0.4.0)
* [x] Gesture support (0.3.0)
* [ ] Glide typing (0.4.0)
* [x] Full integration in IME service list of Android (xml/method)
(integration is internal-only, because Android's default subtype
implementation not really allows for dynamic language/layout
pairs, only compile-time defined ones)
* [ ] Description and settings reference in System Language & Input
* [ ] (dev only) Generate well-structured documentation of code
* [ ] ...
Note: (?) = not sure if it will be implemented
Note:
## Used libraries and icons
(?) = not sure if it will be implemented
(0.x.0) = planned version when feature will be implemented.
## Contributing
Wanna contribute to FlorisBoard? That's great to hear! There are lots of
different ways to help out. Bug reporting, making pull requests,
translating FlorisBoard to make it more accessible, etc. For more
information see the ![contributing guidelines](CONTRIBUTING.md). Thank
you for your help!
## List of permissions FlorisBoard requests
Please refer to this [page](https://github.com/florisboard/florisboard/wiki/List-of-permissions-FlorisBoard-requests)
to get more information on this topic.
## Used libraries, components and icons
* [Google Flexbox Layout for Android](https://github.com/google/flexbox-layout)
by [google](https://github.com/google)
* [Google Material icons](https://github.com/google/material-design-icons) by
[google](https://github.com/google)
* [Moshi JSON library](https://github.com/square/moshi) by
[square](https://github.com/square)
* [ColorPicker preference](https://github.com/jaredrummler/ColorPicker) by
[Jared Rummler](https://github.com/jaredrummler)
## License
```

View File

@@ -10,8 +10,8 @@ android {
applicationId "dev.patrickgold.florisboard"
minSdkVersion 23
targetSdkVersion 29
versionCode 9
versionName "0.1.0"
versionCode 18
versionName "0.2.6"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -31,18 +31,21 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.3.0'
implementation 'androidx.preference:preference:1.1.1'
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.12'
testImplementation 'androidx.test:core:1.2.0'
testImplementation 'androidx.test:core:1.3.0'
testImplementation 'org.mockito:mockito-core:1.10.19'
testImplementation 'org.mockito:mockito-inline:2.13.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation 'com.google.android:flexbox:2.0.1'
implementation "com.squareup.moshi:moshi-kotlin:1.9.2"
implementation 'com.google.android.material:material:1.1.0'
implementation 'com.google.android.material:material:1.2.1'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7"
implementation 'com.jaredrummler:colorpicker:1.1.0'
}

View File

@@ -21,6 +21,7 @@
<uses-permission android:name="android.permission.VIBRATE"/>
<application
android:name=".ime.core.FlorisApplication"
android:allowBackup="false"
android:extractNativeLibs="false"
android:icon="@mipmap/ic_launcher"
@@ -74,6 +75,14 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/SettingsTheme"/>
<!-- Advanced Activity -->
<activity
android:name="dev.patrickgold.florisboard.settings.AdvancedActivity"
android:icon="@mipmap/ic_launcher"
android:label="@string/settings__advanced__title"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/SettingsTheme"/>
<!-- Setup Activity -->
<activity
android:name="dev.patrickgold.florisboard.setup.SetupActivity"
@@ -82,6 +91,13 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/SettingsTheme"/>
<!-- Crash Dialog Activity -->
<activity
android:name="dev.patrickgold.florisboard.crashutility.CrashDialogActivity"
android:icon="@mipmap/ic_launcher"
android:label="@string/crash_dialog__title"
android:theme="@style/CrashDialogTheme"/>
</application>
</manifest>

View File

@@ -11,7 +11,8 @@
"icelandic": "Icelandic (QWERTY)",
"swiss_german": "Swiss German (QWERTZ)",
"swiss_french": "Swiss French (QWERTZ)",
"swiss_italian": "Swiss Italian (QWERTZ)"
"swiss_italian": "Swiss Italian (QWERTZ)",
"persian": "Persian"
},
"defaultSubtypes": [
{
@@ -174,6 +175,13 @@
"preferredLayout": "icelandic",
"isAsciiCapable": true,
"isEmojiCapable": true
},
{
"id": 800,
"languageTag": "fa-FA",
"preferredLayout": "persian",
"isAsciiCapable": true,
"isEmojiCapable": true
}
]
}

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

@@ -0,0 +1,80 @@
{
"ض": [
{ "code": 1777, "label": "۱" }
],
"ص": [
{ "code": 1778, "label": "۲" }
],
"ث": [
{ "code": 1779, "label": "۳" }
],
"ق": [
{ "code": 1780, "label": "۴" }
],
"ف": [
{ "code": 1781, "label": "۵" }
],
"غ": [
{ "code": 1782, "label": "۶" }
],
"ع": [
{ "code": 1783, "label": "۷" }
],
"ه": [
{ "code": 1784, "label": "۸" }
],
"خ": [
{ "code": 1785, "label": "۹" }
],
"ح": [
{ "code": 1776, "label": "۰" }
],
"ی": [
{ "code": 1574, "label": "ئ" },
{ "code": 1610, "label": "ي" }
],
"ا": [
{ "code": 1570, "label": "آ" },
{ "code": 1649, "label": "ٱ" },
{ "code": 1569, "label": "ء" },
{ "code": 1571, "label": "أ" },
{ "code": 1573, "label": "إ" }
],
"ت": [
{ "code": 1577, "label": "ة" }
],
"ک": [
{ "code": 1706, "label": "ڪ"},
{ "code": 1603, "label": "ك" }
],
"ز": [
{ "code": 1688, "label": "ژ" }
],
"و": [
{ "code": 1572, "label": "ؤ" }
],
".~normal": [
{ "code": 1611, "label": "ً" },
{ "code": 1622, "label": "ٖ" },
{ "code": 1648, "label": "ٰ" },
{ "code": 1619, "label": "ٓ" },
{ "code": 1615, "label": "ُ" },
{ "code": 1616, "label": "ِ" },
{ "code": 1614, "label": "َ" },
{ "code": 1600, "label": "ـ" },
{ "code": 1621, "label": "ٕ" },
{ "code": 1618, "label": "ْ" },
{ "code": 1617, "label": "ّ" },
{ "code": 1612, "label": "ٌ" },
{ "code": 1613, "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" }
]
}

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,34 @@
{
"type": "characters/mod",
"name": "persian",
"direction": "rtl",
"arrangement": [
[
{ "code": 0 },
{ "code": -5, "label": "delete", "type": "enter_editing" }
],
[
{ "code": -202, "label": "view_symbols", "type": "system_gui" },
{ "code": 64, "label": "@", "variation": "email_address" },
{ "code": 1548, "label": "،", "variation": "normal" },
{ "code": 47, "label": "/", "variation": "uri" },
{ "code": -210, "label": "language_switch", "type": "system_gui", "popup": [
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui" },
{ "code": -100, "label": "settings", "type": "system_gui" }
] },
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui", "popup": [
{ "code": -100, "label": "settings", "type": "system_gui" }
] },
{ "code": 32, "label": " " },
{ "code": 8204, "label": "half_space", "variation": "normal" },
{ "code": 1600, "label": "kashida", "variation": "normal" },
{ "code": 46, "label": ".", "variation": "email_address" },
{ "code": 46, "label": ".", "variation": "normal" },
{ "code": 46, "label": ".", "variation": "uri" },
{ "code": 10, "label": "enter", "type": "enter_editing", "popup": [
{ "code": -215, "label": "toggle_one_handed_mode", "type": "system_gui" },
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui" }
] }
]
]
}

View File

@@ -0,0 +1,45 @@
{
"type": "characters",
"name": "persian",
"direction": "rtl",
"modifier": "persian",
"arrangement": [
[
{ "code": 1590, "label": "ض" },
{ "code": 1589, "label": "ص" },
{ "code": 1579, "label": "ث" },
{ "code": 1602, "label": "ق" },
{ "code": 1601, "label": "ف" },
{ "code": 1594, "label": "غ" },
{ "code": 1593, "label": "ع" },
{ "code": 1607, "label": "ه" },
{ "code": 1582, "label": "خ" },
{ "code": 1581, "label": "ح" },
{ "code": 1580, "label": "ج" }
],
[
{ "code": 1588, "label": "ش" },
{ "code": 1587, "label": "س" },
{ "code": 1740, "label": "ی" },
{ "code": 1576, "label": "ب" },
{ "code": 1604, "label": "ل" },
{ "code": 1575, "label": "ا" },
{ "code": 1578, "label": "ت" },
{ "code": 1606, "label": "ن" },
{ "code": 1605, "label": "م" },
{ "code": 1705, "label": "ک" },
{ "code": 1711, "label": "گ" }
],
[
{ "code": 1592, "label": "ظ" },
{ "code": 1591, "label": "ط" },
{ "code": 1586, "label": "ز" },
{ "code": 1585, "label": "ر" },
{ "code": 1584, "label": "ذ" },
{ "code": 1583, "label": "د" },
{ "code": 1662, "label": "پ" },
{ "code": 1608, "label": "و" },
{ "code": 1670, "label": "چ" }
]
]
}

View File

@@ -0,0 +1,15 @@
{
"type": "extension",
"name": "clipboard_cursor_row",
"direction": "ltr",
"arrangement": [
[
{ "code": -135, "label": "clipboard_select_all", "type": "enter_editing" },
{ "code": -130, "label": "clipboard_copy", "type": "enter_editing" },
{ "code": -20, "label": "arrow_left", "type": "navigation" },
{ "code": -21, "label": "arrow_right", "type": "navigation" },
{ "code": -131, "label": "clipboard_cut", "type": "enter_editing" },
{ "code": -132, "label": "clipboard_paste", "type": "enter_editing" }
]
]
}

View File

@@ -12,7 +12,9 @@
{ "code": 44, "label": ",", "popup": [] },
{ "code": -205, "label": "view_numeric_advanced", "type": "system_gui" },
{ "code": 32, "label": " ", "popup": [] },
{ "code": 46, "label": ".", "popup": [] },
{ "code": 46, "label": ".", "popup": [
{ "code": 8230, "label": "…" }
] },
{ "code": 10, "label": "enter", "type": "enter_editing" }
]
]

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,60 @@
/*
* Copyright (C) 2020 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.crashutility
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.databinding.CrashDialogBinding
class CrashDialogActivity : AppCompatActivity() {
private lateinit var binding: CrashDialogBinding
private var stacktrace: String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = CrashDialogBinding.inflate(layoutInflater)
setContentView(binding.root)
stacktrace = CrashUtility.getUnhandledStacktrace(this)
binding.stacktrace.text = stacktrace
binding.copyToClipboard.setOnClickListener {
val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE)
if (clipboardManager != null && clipboardManager is ClipboardManager) {
clipboardManager.setPrimaryClip(ClipData.newPlainText(stacktrace, stacktrace))
}
}
binding.openBugReportForm.setOnClickListener {
val browserIntent = Intent(
Intent.ACTION_VIEW,
Uri.parse(resources.getString(R.string.florisboard__issue_tracker_new_issue_url))
)
startActivity(browserIntent)
}
binding.close.setOnClickListener {
finish()
}
}
}

View File

@@ -0,0 +1,408 @@
/*
* Copyright (C) 2020 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.crashutility
import android.annotation.SuppressLint
import android.app.*
import android.app.Application.ActivityLifecycleCallbacks
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Process
import android.util.Log
import android.view.inputmethod.InputMethodManager
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import java.io.File
import java.lang.ref.WeakReference
import kotlin.system.exitProcess
/**
* Abstract class which holds several static methods used for handling unexpected errors.
*
* Parts of this class (especially the install() function and the uncaughtException() handler) have
* been inspired by the great CustomActivityOnCrash library:
* https://github.com/Ereza/CustomActivityOnCrash (licensed under Apache 2.0)
* https://github.com/Ereza/CustomActivityOnCrash/blob/master/library/src/main/java/cat/ereza/customactivityoncrash/CustomActivityOnCrash.java
*/
abstract class CrashUtility private constructor() {
companion object {
private const val SHARED_PREFS_FILE = "crash_utility"
private const val SHARED_PREFS_LAST_CRASH_TIMESTAMP = "last_crash_timestamp"
private const val NOTIFICATION_CHANNEL_ID = "dev.patrickgold.florisboard.crashutility"
private const val NOTIFICATION_ID = 0xFBAD0100
private const val UNHANDLED_STACKTRACE_FILE_EXT = "stacktrace"
private const val TAG = "CrashUtility"
private var lastActivityCreated: WeakReference<Activity?> = WeakReference(null)
/**
* Installs the CrashUtility crash handler for the given package [context]. Also registers
* a notification channel for devices with Android 8.0+.
*
* @param context The current package context. If null is supplied, this function does
* nothing.
* @return True if the installation was successful, false otherwise.
*/
fun install(context: Context?): Boolean {
if (context == null) {
Log.e(
TAG,
"install($context): Can't install crash handler with a null Context object, doing nothing!"
)
return false
}
val oldHandler = Thread.getDefaultUncaughtExceptionHandler()
if (oldHandler is UncaughtExceptionHandler) {
Log.i(TAG, "install($context): Crash handler is already installed, doing nothing!")
} else {
val application = context.applicationContext
if (application != null && application is Application) {
try {
Thread.setDefaultUncaughtExceptionHandler(
UncaughtExceptionHandler(
WeakReference(application),
WeakReference(oldHandler),
application.filesDir.absolutePath
)
)
Log.i(
TAG,
"install($context): Successfully installed crash handler for this application!"
)
} catch (e: SecurityException) {
Log.e(
TAG,
"install($context): Failed to install crash handler, probably due to missing runtime permission 'setDefaultUncaughtExceptionHandler':\n$e"
)
return false
} catch (e: Exception) {
Log.e(
TAG,
"install($context): Failed to install crash handler due to an unspecified error:\n$e"
)
return false
}
application.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
override fun onActivityCreated(
activity: Activity,
savedInstanceState: Bundle?
) {
if (activity !is CrashDialogActivity) {
lastActivityCreated = WeakReference(activity)
}
}
override fun onActivityStarted(activity: Activity) {}
override fun onActivityResumed(activity: Activity) {}
override fun onActivityPaused(activity: Activity) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivitySaveInstanceState(
activity: Activity,
outState: Bundle
) {}
override fun onActivityDestroyed(activity: Activity) {}
})
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE)
if (notificationManager != null && notificationManager is NotificationManager) {
val notificationChannel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
context.resources.getString(R.string.crash_notification_channel__title),
NotificationManager.IMPORTANCE_HIGH
)
notificationManager.createNotificationChannel(notificationChannel)
}
Log.i(
TAG,
"install($context): Successfully created crash handler notification channel!"
)
} catch (e: Exception) {
Log.e(
TAG,
"install($context): Failed to create crash handler notification channel due to an unspecified error:\n$e"
)
}
}
} else {
Log.e(
TAG,
"install($context): Can't install crash handler with a null Application object, doing nothing!"
)
return false
}
}
return true
}
/**
* Reads and returns all unhandled stacktrace files.
*
* @param context The current package context. If null is supplied, this function returns
* an empty string.
* @return All unhandled stacktrace files or an empty string.
*/
fun getUnhandledStacktrace(context: Context?): String {
context ?: return ""
val retString: StringBuilder = StringBuilder()
val ustDir = getUstDir(context)
if (ustDir.isDirectory) {
(ustDir.listFiles { pathname ->
pathname.name.endsWith(".$UNHANDLED_STACKTRACE_FILE_EXT")
})?.forEach { file ->
val newLine = System.lineSeparator()
Log.i(TAG, "Reading unhandled stacktrace: ${file.name}")
retString.append("~~~ ${file.name} ~~~$newLine$newLine")
retString.append(readFile(file))
file.delete()
}
}
return retString.toString()
}
fun hasUnhandledStacktraceFiles(context: Context): Boolean {
val ustDir = getUstDir(context)
return if (ustDir.isDirectory) {
(ustDir.listFiles { pathname ->
pathname.name.endsWith(".$UNHANDLED_STACKTRACE_FILE_EXT")
})?.isNotEmpty() ?: false
} else {
false
}
}
/**
* Gets the last crash timestamp from the shared preferences.
*
* @param context The current package context. If null is supplied, this function returns
* the default value for the timestamp (0).
* @return The last time crash timestamp or 0.
*/
private fun getLastCrashTimestamp(context: Context?): Long {
context ?: return 0
return context.getSharedPreferences(SHARED_PREFS_FILE, Context.MODE_PRIVATE)
.getLong(SHARED_PREFS_LAST_CRASH_TIMESTAMP, 0)
}
/**
* Sets the last crash timestamp in the shared preferences.
*
* @param context The current package context. If null is supplied, this function does
* nothing.
* @param value The timestamp of the current crash.
*/
@SuppressLint("ApplySharedPref")
private fun setLastCrashTimestamp(context: Context?, value: Long) {
context ?: return
// Note: must use commit() instead of apply(), as the value must be immediately written
// to be possibly instantly read again.
context.getSharedPreferences(SHARED_PREFS_FILE, Context.MODE_PRIVATE)
.edit()
.putLong(SHARED_PREFS_LAST_CRASH_TIMESTAMP, value)
.commit()
}
/**
* Gets a reference to the current unhandled stacktrace directory.
*
* @param context The current package context.
* @return The File object for the directory.
*/
private fun getUstDir(context: Context): File {
val path = context.filesDir.absolutePath
return File(path)
}
/**
* Gets a reference to the stacktrace file for given [timestamp].
*
* @param context The current package context.
* @param timestamp The timestamp of the stacktrace file to get.
* @return The File object for the stacktrace file.
*/
private fun getUstFile(context: Context, timestamp: Long): File {
val path = context.filesDir.absolutePath
return File("$path/$timestamp.$UNHANDLED_STACKTRACE_FILE_EXT")
}
/**
* Push a notification which opens [CrashDialogActivity] with given parameters.
*
* @param context The current package context. If null is supplied, this function does
* nothing.
* @param id The ID of the notification.
* @param title The title of the notification.
* @param body The body of the notification.
*/
private fun pushNotification(context: Context?, id: Int, title: String, body: String) {
context ?: return
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE)
if (notificationManager != null && notificationManager is NotificationManager) {
val notificationBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Notification.Builder(context.applicationContext, NOTIFICATION_CHANNEL_ID)
} else {
@Suppress("DEPRECATION")
Notification.Builder(context.applicationContext).apply {
setPriority(Notification.PRIORITY_MAX)
}
}
val crashDialogIntent = Intent(context, CrashDialogActivity::class.java)
val notification = notificationBuilder.run {
setContentTitle(title)
style = Notification.BigTextStyle().bigText(body)
setContentText(body)
setSmallIcon(android.R.drawable.stat_notify_error)
setContentIntent(PendingIntent.getActivity(context, 0, crashDialogIntent, 0)).setAutoCancel(
true
)
build()
}
notificationManager.notify(id, notification)
}
}
/**
* Push a notification configured for a single crash.
*
* @param context The current package context. If null is supplied, this function does
* nothing.
*/
private fun pushCrashOnceNotification(context: Context?) {
context ?: return
pushNotification(
context,
NOTIFICATION_ID.toInt(),
context.resources.getString(R.string.crash_once_notification__title),
context.resources.getString(R.string.crash_once_notification__body)
)
}
/**
* Push a notification configured for multiple crashes.
*
* @param context The current package context. If null is supplied, this function does
* nothing.
*/
private fun pushCrashMultipleNotification(context: Context?) {
context ?: return
pushNotification(
context,
NOTIFICATION_ID.toInt(),
context.resources.getString(R.string.crash_multiple_notification__title),
context.resources.getString(R.string.crash_multiple_notification__body)
)
}
/**
* Reads a given [file] and returns its content.
*
* @param file The file object.
* @return The contents of the file or an empty string, if the file does not exist.
*/
private fun readFile(file: File): String {
val retText = StringBuilder()
if (file.exists()) {
val newLine = System.lineSeparator()
file.forEachLine {
retText.append(it)
retText.append(newLine)
}
}
return retText.toString()
}
/**
* Writes given [text] to given [file]. If the file already exists, its current content
* will be overwritten.
*
* @param file The file object.
* @param text The text to write to the file.
* @return The contents of the file or an empty string, if the file does not exist.
*/
private fun writeToFile(file: File, text: String) {
try {
file.writeText(text)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
/**
* Custom UncaughtExceptionHandler, which writes the captured stacktrace of the crash to the
* internal storage, pushes a crash notification and kills the current process.
*/
class UncaughtExceptionHandler(
private val application: WeakReference<Application>,
private val oldHandler: WeakReference<Thread.UncaughtExceptionHandler?>,
private val path: String
) : Thread.UncaughtExceptionHandler {
override fun uncaughtException(thread: Thread?, throwable: Throwable?) {
Log.e(TAG, "Detected application crash, executing custom crash handler.")
thread ?: return
throwable ?: return
val timestamp = System.currentTimeMillis()
val stacktrace = Log.getStackTraceString(throwable)
val ustFile = File("$path/$timestamp.$UNHANDLED_STACKTRACE_FILE_EXT")
writeToFile(ustFile, stacktrace)
val application = application.get()
if (application != null) {
val lastTimestamp = getLastCrashTimestamp(application)
if (lastTimestamp > 0) {
val lastFile = getUstFile(application, lastTimestamp)
val lastStacktrace = readFile(lastFile)
if (lastStacktrace == stacktrace) {
// Delete last stacktrace if it matches previous unhandled one
lastFile.delete()
}
}
setLastCrashTimestamp(application, timestamp)
if (timestamp - lastTimestamp < 5000) {
pushCrashMultipleNotification(application)
val florisboard = FlorisBoard.getInstanceOrNull()
if (florisboard != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
florisboard.switchToPreviousInputMethod()
} else {
val imm = application.getSystemService(Context.INPUT_METHOD_SERVICE)
if (imm != null && imm is InputMethodManager) {
@Suppress("DEPRECATION")
imm.switchToNextInputMethod(
florisboard.window?.window?.attributes?.token,
false
)
}
}
}
} else {
pushCrashOnceNotification(application)
}
}
val lastActivity = lastActivityCreated.get()
if (lastActivity != null) {
//oldHandler.get()?.uncaughtException(thread, throwable)
lastActivity.finish()
lastActivityCreated.clear()
}
Process.killProcess(Process.myPid())
exitProcess(10)
}
}
}

View File

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

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.ime.core
import android.app.Application
import dev.patrickgold.florisboard.crashutility.CrashUtility
class FlorisApplication : Application() {
override fun onCreate() {
super.onCreate()
CrashUtility.install(this)
}
}

View File

@@ -17,9 +17,12 @@
package dev.patrickgold.florisboard.ime.core
import android.annotation.SuppressLint
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.graphics.Color
import android.inputmethodservice.InputMethodService
import android.media.AudioManager
import android.os.*
@@ -28,20 +31,20 @@ import android.util.Log
import android.view.Gravity
import android.view.View
import android.view.WindowManager
import android.view.inputmethod.CursorAnchorInfo
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import android.widget.ImageButton
import android.widget.LinearLayout
import com.squareup.moshi.Json
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.media.MediaInputManager
import dev.patrickgold.florisboard.ime.text.TextInputManager
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyData
import dev.patrickgold.florisboard.settings.SettingsMainActivity
import dev.patrickgold.florisboard.util.*
import java.lang.ref.WeakReference
/**
* Variable which holds the current [FlorisBoard] instance. To get this instance from another
@@ -53,20 +56,27 @@ private var florisboardInstance: FlorisBoard? = null
* Core class responsible to link together both the text and media input managers as well as
* managing the one-handed UI.
*/
class FlorisBoard : InputMethodService() {
class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedListener {
lateinit var prefs: PrefHelper
private set
val context: Context
get() = inputView?.context ?: this
private var inputView: InputView? = null
get() = inputWindowView?.context ?: this
var inputView: InputView? = null
private set
private var inputWindowView: InputWindowView? = null
private var eventListeners: MutableList<WeakReference<EventListener>> = mutableListOf()
private var audioManager: AudioManager? = null
var clipboardManager: ClipboardManager? = null
private var vibrator: Vibrator? = null
private val osHandler = Handler()
var activeEditorInstance: EditorInstance = EditorInstance.default()
lateinit var subtypeManager: SubtypeManager
lateinit var activeSubtype: Subtype
private var currentThemeIsNight: Boolean = false
private var currentThemeResId: Int = 0
val textInputManager: TextInputManager
@@ -81,6 +91,7 @@ class FlorisBoard : InputMethodService() {
companion object {
private const val IME_ID: String = "dev.patrickgold.florisboard/.ime.core.FlorisBoard"
private val TAG: String? = FlorisBoard::class.simpleName
fun checkIfImeIsEnabled(context: Context): Boolean {
val activeImeIds = Settings.Secure.getString(
@@ -104,6 +115,18 @@ class FlorisBoard : InputMethodService() {
fun getInstance(): FlorisBoard {
return florisboardInstance!!
}
@Synchronized
fun getInstanceOrNull(): FlorisBoard? {
return florisboardInstance
}
fun getDayNightBaseThemeId(isNightTheme: Boolean): Int {
return when (isNightTheme) {
true -> R.style.KeyboardThemeBase_Night
else -> R.style.KeyboardThemeBase_Day
}
}
}
override fun onCreate() {
@@ -125,187 +148,218 @@ class FlorisBoard : InputMethodService() {
.build()
)
}
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onCreate()")
if (BuildConfig.DEBUG) Log.i(TAG, "onCreate()")
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboardManager?.addPrimaryClipChangedListener(this)
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
prefs = PrefHelper(this)
prefs = PrefHelper.getDefaultInstance(this)
prefs.initDefaultPreferences()
prefs.sync()
subtypeManager = SubtypeManager(this, prefs)
activeSubtype = subtypeManager.getActiveSubtype() ?: Subtype.DEFAULT
currentThemeResId = prefs.theme.getSelectedThemeResId()
currentThemeIsNight = prefs.internal.themeCurrentIsNight
currentThemeResId = getDayNightBaseThemeId(currentThemeIsNight)
setTheme(currentThemeResId)
updateTheme()
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
super.onCreate()
textInputManager.onCreate()
mediaInputManager.onCreate()
eventListeners.toList().forEach { it.get()?.onCreate() }
}
@SuppressLint("InflateParams")
override fun onCreateInputView(): View? {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onCreateInputView()")
if (BuildConfig.DEBUG) Log.i(TAG, "onCreateInputView()")
baseContext.setTheme(currentThemeResId)
inputView = layoutInflater.inflate(R.layout.florisboard, null) as InputView
inputWindowView = layoutInflater.inflate(R.layout.florisboard, null) as InputWindowView
textInputManager.onCreateInputView()
mediaInputManager.onCreateInputView()
eventListeners.toList().forEach { it.get()?.onCreateInputView() }
return inputView
return inputWindowView
}
fun registerInputView(inputView: InputView) {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "registerInputView(inputView)")
if (BuildConfig.DEBUG) Log.i(TAG, "registerInputView($inputView)")
this.inputView = inputView
initializeOneHandedEnvironment()
updateTheme()
updateSoftInputWindowLayoutParameters()
updateOneHandedPanelVisibility()
textInputManager.onRegisterInputView(inputView)
mediaInputManager.onRegisterInputView(inputView)
eventListeners.toList().forEach { it.get()?.onRegisterInputView(inputView) }
}
override fun onDestroy() {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onDestroy()")
if (BuildConfig.DEBUG) Log.i(TAG, "onDestroy()")
clipboardManager?.removePrimaryClipChangedListener(this)
osHandler.removeCallbacksAndMessages(null)
florisboardInstance = null
eventListeners.toList().forEach { it.get()?.onDestroy() }
eventListeners.clear()
super.onDestroy()
textInputManager.onDestroy()
mediaInputManager.onDestroy()
}
override fun onStartInput(attribute: EditorInfo?, restarting: Boolean) {
if (BuildConfig.DEBUG) Log.i(TAG, "onStartInput($attribute, $restarting)")
super.onStartInput(attribute, restarting)
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_IMMEDIATE)
}
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_MONITOR)
if (BuildConfig.DEBUG) Log.i(TAG, "onStartInputView($info, $restarting)")
Log.i(TAG, "onStartInputView: " + info?.debugSummarize())
super.onStartInputView(info, restarting)
textInputManager.onStartInputView(info, restarting)
mediaInputManager.onStartInputView(info, restarting)
activeEditorInstance = EditorInstance.from(info, this)
eventListeners.toList().forEach {
it.get()?.onStartInputView(activeEditorInstance, restarting)
}
}
override fun onFinishInputView(finishingInput: Boolean) {
currentInputConnection?.requestCursorUpdates(0)
if (BuildConfig.DEBUG) Log.i(TAG, "onFinishInputView($finishingInput)")
if (finishingInput) {
activeEditorInstance = EditorInstance.default()
}
super.onFinishInputView(finishingInput)
textInputManager.onFinishInputView(finishingInput)
mediaInputManager.onFinishInputView(finishingInput)
eventListeners.toList().forEach { it.get()?.onFinishInputView(finishingInput) }
}
override fun onFinishInput() {
if (BuildConfig.DEBUG) Log.i(TAG, "onFinishInput()")
super.onFinishInput()
currentInputConnection?.requestCursorUpdates(0)
}
override fun onWindowShown() {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onWindowShown()")
if (BuildConfig.DEBUG) Log.i(TAG, "onWindowShown()")
prefs.sync()
updateThemeIfNecessary()
updateTheme()
updateOneHandedPanelVisibility()
activeSubtype = subtypeManager.getActiveSubtype() ?: Subtype.DEFAULT
onSubtypeChanged(activeSubtype)
setActiveInput(R.id.text_input)
super.onWindowShown()
textInputManager.onWindowShown()
mediaInputManager.onWindowShown()
eventListeners.toList().forEach { it.get()?.onWindowShown() }
}
override fun onWindowHidden() {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onWindowHidden()")
if (BuildConfig.DEBUG) Log.i(TAG, "onWindowHidden()")
super.onWindowHidden()
textInputManager.onWindowHidden()
mediaInputManager.onWindowHidden()
eventListeners.toList().forEach { it.get()?.onWindowHidden() }
}
override fun onConfigurationChanged(newConfig: Configuration) {
if (BuildConfig.DEBUG) Log.i(TAG, "onConfigurationChanged($newConfig)")
if (isInputViewShown) {
updateOneHandedPanelVisibility()
}
super.onConfigurationChanged(newConfig)
textInputManager.onConfigurationChanged(newConfig)
mediaInputManager.onConfigurationChanged(newConfig)
}
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
super.onUpdateCursorAnchorInfo(cursorAnchorInfo)
textInputManager.onUpdateCursorAnchorInfo(cursorAnchorInfo)
mediaInputManager.onUpdateCursorAnchorInfo(cursorAnchorInfo)
}
override fun onUpdateSelection(
oldSelStart: Int,
oldSelEnd: Int,
newSelStart: Int,
newSelEnd: Int,
candidatesStart: Int,
candidatesEnd: Int
oldSelStart: Int, oldSelEnd: Int,
newSelStart: Int, newSelEnd: Int,
candidatesStart: Int, candidatesEnd: Int
) {
if (BuildConfig.DEBUG) Log.i(TAG, "onUpdateSelection($oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd)")
super.onUpdateSelection(
oldSelStart,
oldSelEnd,
newSelStart,
newSelEnd,
candidatesStart,
candidatesEnd
oldSelStart, oldSelEnd,
newSelStart, newSelEnd,
candidatesStart, candidatesEnd
)
textInputManager.onUpdateSelection(
oldSelStart,
oldSelEnd,
newSelStart,
newSelEnd,
candidatesStart,
candidatesEnd
)
mediaInputManager.onUpdateSelection(
oldSelStart,
oldSelEnd,
newSelStart,
newSelEnd,
candidatesStart,
candidatesEnd
activeEditorInstance.onUpdateSelection(
oldSelStart, oldSelEnd,
newSelStart, newSelEnd
)
eventListeners.toList().forEach { it.get()?.onUpdateSelection() }
}
/**
* Checks the preferences if the selected theme res id has changed and updates the theme only
* then by rebuilding the UI and setting the navigation bar theme manually.
* Updates the theme of the IME Window, status and navigation bar, as well as the InputView and
* some of its components.
*/
private fun updateThemeIfNecessary() {
val newThemeResId = prefs.theme.getSelectedThemeResId()
if (newThemeResId != currentThemeResId) {
currentThemeResId = newThemeResId
private fun updateTheme() {
// Rebuild the UI if the theme has changed from day to night or vice versa to prevent
// theme glitches with scrollbars and hints of buttons in the media UI. If the UI must be
// rebuild, quit this method, as it will be called again by the newly created UI.
val newThemeIsNightMode = prefs.internal.themeCurrentIsNight
if (currentThemeIsNight != newThemeIsNightMode) {
currentThemeResId = getDayNightBaseThemeId(newThemeIsNightMode)
currentThemeIsNight = newThemeIsNightMode
setInputView(onCreateInputView())
val w = window?.window ?: return
w.navigationBarColor = getColorFromAttr(baseContext, android.R.attr.navigationBarColor)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
var flags = w.decorView.systemUiVisibility
flags = if (getBooleanFromAttr(baseContext, android.R.attr.windowLightNavigationBar)) {
flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
} else {
flags and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
}
w.decorView.systemUiVisibility = flags
return
}
// Get Window and the flags of the DecorView
val w = window?.window ?: return
var flags = w.decorView.systemUiVisibility
// Update navigation bar theme
w.navigationBarColor = prefs.theme.navBarColor
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
flags = if (prefs.theme.navBarIsLight) {
flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
} else {
flags and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
}
}
// Update status bar to be transparent
// Done as starting with Android 11 the IME Window takes the primaryColorDark value and
// colors the status bar, which isn't the desired behavior. (See issue #43)
flags = flags or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
w.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
w.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
w.statusBarColor = Color.TRANSPARENT
// Apply the new flags to the DecorView
w.decorView.systemUiVisibility = flags
// Update InputView theme
inputView?.setBackgroundColor(prefs.theme.keyboardBgColor)
inputView?.oneHandedCtrlPanelStart?.setBackgroundColor(prefs.theme.oneHandedBgColor)
inputView?.oneHandedCtrlPanelEnd?.setBackgroundColor(prefs.theme.oneHandedBgColor)
inputView?.findViewById<ImageButton>(R.id.one_handed_ctrl_move_start)
?.imageTintList = ColorStateList.valueOf(prefs.theme.oneHandedButtonFgColor)
inputView?.findViewById<ImageButton>(R.id.one_handed_ctrl_move_end)
?.imageTintList = ColorStateList.valueOf(prefs.theme.oneHandedButtonFgColor)
inputView?.findViewById<ImageButton>(R.id.one_handed_ctrl_close_start)
?.imageTintList = ColorStateList.valueOf(prefs.theme.oneHandedButtonFgColor)
inputView?.findViewById<ImageButton>(R.id.one_handed_ctrl_close_end)
?.imageTintList = ColorStateList.valueOf(prefs.theme.oneHandedButtonFgColor)
eventListeners.toList().forEach { it.get()?.onApplyThemeAttributes() }
}
override fun onComputeInsets(outInsets: Insets?) {
super.onComputeInsets(outInsets)
val inputView = this.inputView ?: return
val inputWindowView = this.inputWindowView ?: return
// TODO: Check also if the keyboard is currently suppressed by a hardware keyboard
if (!isInputViewShown) {
outInsets?.contentTopInsets = inputView.height
outInsets?.visibleTopInsets = inputView.height
outInsets?.contentTopInsets = inputWindowView.height
outInsets?.visibleTopInsets = inputWindowView.height
return
}
val innerInputViewContainer =
inputView.findViewById<LinearLayout>(R.id.inner_input_view_container) ?: return
val visibleTopY = inputView.height - innerInputViewContainer.measuredHeight
val visibleTopY = inputWindowView.height - inputView.measuredHeight
outInsets?.contentTopInsets = visibleTopY
outInsets?.visibleTopInsets = visibleTopY
}
@@ -321,8 +375,8 @@ class FlorisBoard : InputMethodService() {
private fun updateSoftInputWindowLayoutParameters() {
val w = window?.window ?: return
ViewLayoutUtils.updateLayoutHeightOf(w, WindowManager.LayoutParams.MATCH_PARENT)
val inputView = this.inputView
if (inputView != null) {
val inputWindowView = this.inputWindowView
if (inputWindowView != null) {
val layoutHeight = if (isFullscreenMode) {
WindowManager.LayoutParams.WRAP_CONTENT
} else {
@@ -331,7 +385,7 @@ class FlorisBoard : InputMethodService() {
val inputArea = w.findViewById<View>(android.R.id.inputArea)
ViewLayoutUtils.updateLayoutHeightOf(inputArea, layoutHeight)
ViewLayoutUtils.updateLayoutGravityOf(inputArea, Gravity.BOTTOM)
ViewLayoutUtils.updateLayoutHeightOf(inputView, layoutHeight)
ViewLayoutUtils.updateLayoutHeightOf(inputWindowView, layoutHeight)
}
}
@@ -339,9 +393,9 @@ class FlorisBoard : InputMethodService() {
* Makes a key press vibration if the user has this feature enabled in the preferences.
*/
fun keyPressVibrate() {
if (prefs.looknfeel.vibrationEnabled) {
var vibrationStrength = prefs.looknfeel.vibrationStrength
if (vibrationStrength == 0 && prefs.looknfeel.vibrationEnabledSystem) {
if (prefs.keyboard.vibrationEnabled) {
var vibrationStrength = prefs.keyboard.vibrationStrength
if (vibrationStrength == -1 && prefs.keyboard.vibrationEnabledSystem) {
vibrationStrength = 36
}
if (vibrationStrength > 0) {
@@ -363,15 +417,15 @@ class FlorisBoard : InputMethodService() {
* Makes a key press sound if the user has this feature enabled in the preferences.
*/
fun keyPressSound(keyData: KeyData? = null) {
if (prefs.looknfeel.soundEnabled) {
val soundVolume = prefs.looknfeel.soundVolume
if (prefs.keyboard.soundEnabled) {
val soundVolume = prefs.keyboard.soundVolume
val effect = when (keyData?.code) {
KeyCode.SPACE -> AudioManager.FX_KEYPRESS_SPACEBAR
KeyCode.DELETE -> AudioManager.FX_KEYPRESS_DELETE
KeyCode.ENTER -> AudioManager.FX_KEYPRESS_RETURN
else -> AudioManager.FX_KEYPRESS_STANDARD
}
if (soundVolume == 0 && prefs.looknfeel.soundEnabledSystem) {
if (soundVolume == -1 && prefs.keyboard.soundEnabledSystem) {
audioManager!!.playSoundEffect(effect)
} else if (soundVolume > 0) {
audioManager!!.playSoundEffect(effect, soundVolume / 100f)
@@ -379,6 +433,19 @@ class FlorisBoard : InputMethodService() {
}
}
/**
* Executes a given [SwipeAction]. Ignores any [SwipeAction] but the ones relevant for this
* class.
*/
fun executeSwipeAction(swipeAction: SwipeAction) {
when (swipeAction) {
SwipeAction.HIDE_KEYBOARD -> requestHideSelf(0)
SwipeAction.SWITCH_TO_PREV_SUBTYPE -> switchToPrevSubtype()
SwipeAction.SWITCH_TO_NEXT_SUBTYPE -> switchToNextSubtype()
else -> textInputManager.executeSwipeAction(swipeAction)
}
}
/**
* Hides the IME and launches [SettingsMainActivity].
*/
@@ -398,6 +465,11 @@ class FlorisBoard : InputMethodService() {
return subtypeManager.subtypes.size > 1
}
fun switchToPrevSubtype() {
activeSubtype = subtypeManager.switchToPrevSubtype() ?: Subtype.DEFAULT
onSubtypeChanged(activeSubtype)
}
fun switchToNextSubtype() {
activeSubtype = subtypeManager.switchToNextSubtype() ?: Subtype.DEFAULT
onSubtypeChanged(activeSubtype)
@@ -435,26 +507,26 @@ class FlorisBoard : InputMethodService() {
private fun onOneHandedPanelButtonClick(v: View) {
when (v.id) {
R.id.one_handed_ctrl_move_start -> {
prefs.looknfeel.oneHandedMode = "start"
prefs.keyboard.oneHandedMode = "start"
}
R.id.one_handed_ctrl_move_end -> {
prefs.looknfeel.oneHandedMode = "end"
prefs.keyboard.oneHandedMode = "end"
}
R.id.one_handed_ctrl_close_start,
R.id.one_handed_ctrl_close_end -> {
prefs.looknfeel.oneHandedMode = "off"
prefs.keyboard.oneHandedMode = "off"
}
}
updateOneHandedPanelVisibility()
}
fun toggleOneHandedMode() {
when (prefs.looknfeel.oneHandedMode) {
when (prefs.keyboard.oneHandedMode) {
"off" -> {
prefs.looknfeel.oneHandedMode = "end"
prefs.keyboard.oneHandedMode = "end"
}
else -> {
prefs.looknfeel.oneHandedMode = "off"
prefs.keyboard.oneHandedMode = "off"
}
}
updateOneHandedPanelVisibility()
@@ -465,7 +537,7 @@ class FlorisBoard : InputMethodService() {
inputView?.oneHandedCtrlPanelStart?.visibility = View.GONE
inputView?.oneHandedCtrlPanelEnd?.visibility = View.GONE
} else {
when (prefs.looknfeel.oneHandedMode) {
when (prefs.keyboard.oneHandedMode) {
"off" -> {
inputView?.oneHandedCtrlPanelStart?.visibility = View.GONE
inputView?.oneHandedCtrlPanelEnd?.visibility = View.GONE
@@ -486,30 +558,52 @@ class FlorisBoard : InputMethodService() {
}, 0)
}
override fun onPrimaryClipChanged() {
eventListeners.toList().forEach { it.get()?.onPrimaryClipChanged() }
}
/**
* Adds a given [listener] to the list which will receive FlorisBoard events.
*
* @param listener The listener object which receives the events.
* @return True if the listener has been added successfully, false otherwise.
*/
fun addEventListener(listener: EventListener): Boolean {
return eventListeners.add(WeakReference(listener))
}
/**
* Removes a given [listener] from the list which will receive FlorisBoard events.
*
* @param listener The same listener object which was used in [addEventListener].
* @return True if the listener has been removed successfully, false otherwise. A false return
* value may also indicate that the [listener] was not added previously.
*/
fun removeEventListener(listener: EventListener): Boolean {
eventListeners.toList().forEach {
if (it.get() == listener) {
return eventListeners.remove(it)
}
}
return false
}
interface EventListener {
fun onCreate() {}
fun onCreateInputView() {}
fun onRegisterInputView(inputView: InputView) {}
fun onDestroy() {}
fun onStartInputView(info: EditorInfo?, restarting: Boolean) {}
fun onStartInputView(instance: EditorInstance, restarting: Boolean) {}
fun onFinishInputView(finishingInput: Boolean) {}
fun onWindowShown() {}
fun onWindowHidden() {}
fun onConfigurationChanged(newConfig: Configuration) {}
fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {}
fun onUpdateSelection(
oldSelStart: Int,
oldSelEnd: Int,
newSelStart: Int,
newSelEnd: Int,
candidatesStart: Int,
candidatesEnd: Int
) {}
fun onUpdateSelection() {}
fun onApplyThemeAttributes() {}
fun onPrimaryClipChanged() {}
fun onSubtypeChanged(newSubtype: Subtype) {}
}

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,33 @@
package dev.patrickgold.florisboard.ime.core
import android.content.Context
import android.content.res.Configuration
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.util.Log
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.ViewFlipper
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.util.ViewLayoutUtils
import kotlin.math.roundToInt
/**
* Root view of the keyboard. Notifies [FlorisBoard] when it has been attached to a window.
*/
class InputView : FrameLayout {
class InputView : LinearLayout {
private var florisboard: FlorisBoard = FlorisBoard.getInstance()
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
var desiredInputViewHeight: Int = resources.getDimension(R.dimen.inputView_baseHeight).roundToInt()
private set
var desiredSmartbarHeight: Int = resources.getDimension(R.dimen.smartbar_baseHeight).roundToInt()
private set
var desiredTextKeyboardViewHeight: Int = resources.getDimension(R.dimen.textKeyboardView_baseHeight).roundToInt()
private set
var desiredMediaKeyboardViewHeight: Int = resources.getDimension(R.dimen.mediaKeyboardView_baseHeight).roundToInt()
private set
var mainViewFlipper: ViewFlipper? = null
private set
@@ -41,7 +54,11 @@ class InputView : FrameLayout {
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
override fun onAttachedToWindow() {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onAttachedToWindow()")
@@ -54,4 +71,66 @@ class InputView : FrameLayout {
florisboard.registerInputView(this)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val heightFactor = when (resources.configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> 1.0f
else -> if (prefs.keyboard.oneHandedMode != "off") {
0.9f
} else {
1.0f
}
} * when (prefs.keyboard.heightFactor) {
"extra_short" -> 0.85f
"short" -> 0.90f
"mid_short" -> 0.95f
"normal" -> 1.00f
"mid_tall" -> 1.05f
"tall" -> 1.10f
"extra_tall" -> 1.15f
else -> 1.00f
}
var height = (calcInputViewHeight() * heightFactor).roundToInt()
desiredInputViewHeight = height
desiredSmartbarHeight = (0.16129 * height).roundToInt()
desiredTextKeyboardViewHeight = height - desiredSmartbarHeight
desiredMediaKeyboardViewHeight = height
// Add bottom offset for curved screens here. As the desired heights have already been set,
// adding a value to the height now will result in a bottom padding (aka offset).
height += ViewLayoutUtils.convertDpToPixel(
florisboard.prefs.keyboard.bottomOffset.toFloat(),
context
).toInt()
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY))
}
/**
* Calculates the input view height based on the current screen dimensions and the auto
* selected dimension values.
*
* This method and the fraction values have been inspired by [OpenBoard](https://github.com/dslul/openboard)
* but are not 1:1 the same. This implementation differs from the
* [original](https://github.com/dslul/openboard/blob/90ae4c8aec034a8935e1fd02b441be25c7dba6ce/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/ResourceUtils.java)
* by calculating the average of the min and max height values, then taking at least the input
* view base height and return this resulting value.
*/
private fun calcInputViewHeight(): Float {
val dm: DisplayMetrics = resources.displayMetrics
val minBaseSize: Float = when (resources.configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> resources.getFraction(
R.fraction.inputView_minHeightFraction, dm.heightPixels, dm.heightPixels
)
else -> resources.getFraction(
R.fraction.inputView_minHeightFraction, dm.widthPixels, dm.widthPixels
)
}
val maxBaseSize: Float = resources.getFraction(
R.fraction.inputView_maxHeightFraction, dm.heightPixels, dm.heightPixels
)
return ((minBaseSize + maxBaseSize) / 2.0f).coerceAtLeast(
resources.getDimension(R.dimen.inputView_baseHeight)
)
}
}

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

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 {
@@ -71,7 +71,7 @@ class SubtypeManager(
* Loads the [FlorisBoard.ImeConfig] from ime/config.json.
*
* @param path The path to to IME config file.
* @returns The [FlorisBoard.ImeConfig] or a default config.
* @return The [FlorisBoard.ImeConfig] or a default config.
*/
private fun loadImeConfig(path: String): FlorisBoard.ImeConfig {
val rawJsonData: String = try {
@@ -93,7 +93,7 @@ class SubtypeManager(
* Adds a given [subtypeToAdd] to the subtype list, if it does not exist.
*
* @param subtypeToAdd The subtype which should be added.
* @returns True if the subtype was added, false otherwise. A return value of false indicates
* @return True if the subtype was added, false otherwise. A return value of false indicates
* that the subtype already exists.
*/
private fun addSubtype(subtypeToAdd: Subtype): Boolean {
@@ -112,7 +112,7 @@ class SubtypeManager(
*
* @param locale The locale of the subtype to be added.
* @param layoutName The layout name of the subtype to be added.
* @returns True if the subtype was added, false otherwise. A return value of false indicates
* @return True if the subtype was added, false otherwise. A return value of false indicates
* that the subtype already exists.
*/
fun addSubtype(locale: Locale, layoutName: String): Boolean {
@@ -129,21 +129,21 @@ class SubtypeManager(
* Gets the active subtype and returns it. If the activeSubtypeId points to a non-existent
* subtype, this method tries to determine a new active subtype.
*
* @returns The active subtype or null, if the subtype list is empty or no new active subtype
* @return The active subtype or null, if the subtype list is empty or no new active subtype
* could be determined.
*/
fun getActiveSubtype(): Subtype? {
for (subtype in subtypes) {
if (subtype.id == prefs.keyboard.activeSubtypeId) {
if (subtype.id == prefs.localization.activeSubtypeId) {
return subtype
}
}
val subtypeList = subtypes
return if (subtypeList.isNotEmpty()) {
prefs.keyboard.activeSubtypeId = subtypeList[0].id
prefs.localization.activeSubtypeId = subtypeList[0].id
subtypeList[0]
} else {
prefs.keyboard.activeSubtypeId = -1
prefs.localization.activeSubtypeId = Subtype.DEFAULT.id
null
}
}
@@ -152,7 +152,7 @@ class SubtypeManager(
* Gets a subtype by the given [id].
*
* @param id The id of the subtype you want to get.
* @returns The subtype or null, if no matching subtype could be found.
* @return The subtype or null, if no matching subtype could be found.
*/
fun getSubtypeById(id: Int): Subtype? {
for (subtype in subtypes) {
@@ -167,7 +167,7 @@ class SubtypeManager(
* Gets the default system subtype for a given [locale].
*
* @param locale The locale of the default system subtype to get.
* @returns The default system locale or null, if no matching default system subtype could be
* @return The default system locale or null, if no matching default system subtype could be
* found.
*/
fun getDefaultSubtypeForLocale(locale: Locale): DefaultSubtype? {
@@ -212,15 +212,43 @@ class SubtypeManager(
}
}
subtypes = subtypeList
if (subtypeToRemove.id == prefs.keyboard.activeSubtypeId) {
if (subtypeToRemove.id == prefs.localization.activeSubtypeId) {
getActiveSubtype()
}
}
/**
* Switch to the previous subtype in the subtype list if possible.
*
* @return The new active subtype or null if the determination process failed.
*/
fun switchToPrevSubtype(): Subtype? {
val subtypeList = subtypes
val activeSubtype = getActiveSubtype() ?: return null
var triggerNextSubtype = false
var newActiveSubtype: Subtype? = null
for (subtype in subtypeList.reversed()) {
if (triggerNextSubtype) {
triggerNextSubtype = false
newActiveSubtype = subtype
} else if (subtype == activeSubtype) {
triggerNextSubtype = true
}
}
if (triggerNextSubtype) {
newActiveSubtype = subtypeList.last()
}
prefs.localization.activeSubtypeId = when (newActiveSubtype) {
null -> Subtype.DEFAULT.id
else -> newActiveSubtype.id
}
return newActiveSubtype
}
/**
* Switch to the next subtype in the subtype list if possible.
*
* @returns The new active subtype or null if the determination process failed.
* @return The new active subtype or null if the determination process failed.
*/
fun switchToNextSubtype(): Subtype? {
val subtypeList = subtypes
@@ -236,10 +264,10 @@ class SubtypeManager(
}
}
if (triggerNextSubtype) {
newActiveSubtype = subtypeList[0]
newActiveSubtype = subtypeList.first()
}
prefs.keyboard.activeSubtypeId = when (newActiveSubtype) {
null -> -1
prefs.localization.activeSubtypeId = when (newActiveSubtype) {
null -> Subtype.DEFAULT.id
else -> newActiveSubtype.id
}
return newActiveSubtype

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
@@ -71,6 +74,10 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
}
}
init {
florisboard.addEventListener(this)
}
/**
* Called when a new input view has been registered. Used to initialize all media-relevant
* views and layouts.
@@ -104,15 +111,12 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
override fun onTabReselected(tab: TabLayout.Tab) {}
})
for (tab in Tab.values()) {
val tabView = createTabViewFor(tab)
tabViews[tab] = tabView
withContext(Dispatchers.Main) {
withContext(Dispatchers.Main) {
for (tab in Tab.values()) {
val tabView = createTabViewFor(tab)
tabViews[tab] = tabView
mediaViewFlipper?.addView(tabView)
}
}
withContext(Dispatchers.Main) {
tabLayout?.selectTab(tabLayout?.getTabAt(0))
}
}
@@ -195,18 +199,14 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
* Sends a given [emojiKeyData] to the current input editor.
*/
fun sendEmojiKeyPress(emojiKeyData: EmojiKeyData) {
val ic = florisboard.currentInputConnection
ic?.finishComposingText()
ic?.commitText(emojiKeyData.getCodePointsAsString(), 1)
activeEditorInstance.commitText(emojiKeyData.getCodePointsAsString())
}
/**
* Sends a given [emoticonKeyData] to the current input editor.
*/
fun sendEmoticonKeyPress(emoticonKeyData: EmoticonKeyData) {
val ic = florisboard.currentInputConnection
ic?.finishComposingText()
ic?.commitText(emoticonKeyData.icon, 1)
activeEditorInstance.commitText(emoticonKeyData.icon)
}
/**

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 lp = FlexboxLayout.LayoutParams(keyPopupWidth, keyView.measuredHeight)
val textView = KeyPopupExtendedSingleView(keyView.context, k, isInitActive)
val lp = FlexboxLayout.LayoutParams(keyPopupWidth, (keyPopupHeight * 0.4f).toInt())
lp.isWrapBefore = isWrapBefore
textView.layoutParams = lp
textView.gravity = Gravity.CENTER
setTextTintColor(
textView,
R.attr.key_popup_fgColor
)
val textSize = keyboardView.resources.getDimension(R.dimen.key_popup_textSize)
if (keyView is KeyView) {
when (keyView.data.popup[k].code) {
when (keyView.dataPopupWithHint[k].code) {
KeyCode.SETTINGS -> {
textView.iconDrawable = getDrawable(
keyView.context, R.drawable.ic_settings
@@ -134,13 +129,13 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
}
else -> {
textView.setTextSize(
TypedValue.COMPLEX_UNIT_PX, when (keyView.data.popup[k].code) {
TypedValue.COMPLEX_UNIT_PX, when (keyView.dataPopupWithHint[k].code) {
KeyCode.URI_COMPONENT_TLD,
KeyCode.SWITCH_TO_TEXT_CONTEXT -> textSize * 0.6f
else -> textSize
}
)
textView.text = keyView.getComputedLetter(keyView.data.popup[k])
textView.text = keyView.getComputedLetter(keyView.dataPopupWithHint[k])
}
}
} else if (keyView is EmojiKeyView) {
@@ -169,29 +164,29 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
}
/**
* Shows a preview popup for the passed [keyView]. Ignores show requests for key views which
* key code is equal to or less than [KeyCode.SPACE]. KeyViews with a code defined in
* [exceptionsForKeyCodes] will only shadow-calculating the size of the key popup, as these
* sizes are needed for the extended popup. No popup will be shown to the user in this case.
*
* @param keyView Reference to the keyView currently controlling the popup.
* Calculates all attributes required by both the normal and the extended popup, regardless of
* the passed [keyView]'s code.
*/
fun show(keyView: T_KV) {
if (keyView is KeyView && keyView.data.code <= KeyCode.SPACE
&& !exceptionsForKeyCodes.contains(keyView.data.code)) {
return
}
// Update keyPopupWidth and keyPopupHeight
private fun calc(keyView: T_KV) {
if (keyboardView is KeyboardView) {
when (keyboardView.resources.configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> {
keyPopupWidth = (keyboardView.desiredKeyWidth * 0.6f).toInt()
keyPopupHeight = (keyboardView.desiredKeyHeight * 3.0f).toInt()
if (keyboardView.isSmartbarKeyboardView) {
keyPopupWidth = (keyView.measuredWidth * 0.6f).toInt()
keyPopupHeight = (keyboardView.desiredKeyHeight * 3.0f * 1.2f).toInt()
} else {
keyPopupWidth = (keyboardView.desiredKeyWidth * 0.6f).toInt()
keyPopupHeight = (keyboardView.desiredKeyHeight * 3.0f).toInt()
}
}
else -> {
keyPopupWidth = (keyboardView.desiredKeyWidth * 1.1f).toInt()
keyPopupHeight = (keyboardView.desiredKeyHeight * 2.5f).toInt()
if (keyboardView.isSmartbarKeyboardView) {
keyPopupWidth = (keyView.measuredWidth * 1.1f).toInt()
keyPopupHeight = (keyboardView.desiredKeyHeight * 2.5f * 1.2f).toInt()
} else {
keyPopupWidth = (keyboardView.desiredKeyWidth * 1.1f).toInt()
keyPopupHeight = (keyboardView.desiredKeyHeight * 2.5f).toInt()
}
}
}
} else if (keyboardView is EmojiKeyboardView) {
@@ -199,11 +194,21 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
keyPopupHeight = (keyView.measuredHeight * 2.5f).toInt()
}
keyPopupDiffX = (keyView.measuredWidth - keyPopupWidth) / 2
// Calculating is done, so exit show() here if this key view is a special one.
if (keyView is KeyView && exceptionsForKeyCodes.contains(keyView.data.code)) {
}
/**
* Shows a preview popup for the passed [keyView]. Ignores show requests for key views which
* key code is equal to or less than [KeyCode.SPACE].
*
* @param keyView Reference to the keyView currently controlling the popup.
*/
fun show(keyView: T_KV) {
if (keyView is KeyView && keyView.data.code <= KeyCode.SPACE) {
return
}
calc(keyView)
val keyPopupX = keyPopupDiffX
val keyPopupY = -keyPopupHeight
if (window.isShowing) {
@@ -216,7 +221,7 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
if (keyView is KeyView) {
popupView.findViewById<TextView>(R.id.key_popup_text)?.text = keyView.getComputedLetter()
popupView.findViewById<ImageView>(R.id.key_popup_threedots)?.visibility = when {
keyView.data.popup.isEmpty() -> View.INVISIBLE
keyView.dataPopupWithHint.isEmpty() -> View.INVISIBLE
else -> View.VISIBLE
}
} else if (keyView is EmojiKeyView) {
@@ -256,18 +261,17 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
return
}
// Anchor left if keyView is in left half of keyboardView, else anchor right
if (keyView is KeyView) {
anchorLeft = keyView.x < keyboardView.measuredWidth / 2
} else if (keyView is EmojiKeyView) {
val hsv = (keyView.parent.parent as HorizontalScrollView)
anchorLeft = (keyView.x - hsv.scrollX) < keyboardView.measuredWidth / 2
if (!isShowingPopup) {
calc(keyView)
}
// Anchor left if keyView is in left half of keyboardView, else anchor right
anchorLeft = keyView.x < keyboardView.measuredWidth / 2
anchorRight = !anchorLeft
// Determine key counts for each row
val n = when (keyView) {
is KeyView -> keyView.data.popup.size
is KeyView -> keyView.dataPopupWithHint.size
is EmojiKeyView -> keyView.data.popup.size
else -> 0
}
@@ -316,17 +320,29 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
// Build UI
popupViewExt.removeAllViews()
val indices = when (keyView) {
is KeyView -> keyView.data.popup.indices
is KeyView -> keyView.dataPopupWithHint.indices
is EmojiKeyView -> keyView.data.popup.indices
else -> IntRange(0, 0)
}
var hasShownFirst = false
for (k in indices) {
val isInitActive =
anchorLeft && (k - row1count == anchorOffset) ||
anchorRight && (k - row1count == row0count - 1 - anchorOffset)
val kk = when (keyView) {
is KeyView -> when {
isInitActive -> {
hasShownFirst = true
0
}
hasShownFirst -> k
else -> k + 1
}
else -> k
}
popupViewExt.addView(
createTextView(
keyView, k, isInitActive, (row1count > 0) && (k - row1count == 0)
keyView, kk, isInitActive, (row1count > 0) && (k - row1count == 0)
)
)
if (isInitActive) {
@@ -338,9 +354,9 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
// Calculate layout params
val extWidth = row0count * keyPopupWidth
val extHeight = when {
row1count > 0 -> keyView.measuredHeight * 2
else -> keyView.measuredHeight
}
row1count > 0 -> keyPopupHeight * 0.4f * 2.0f
else -> keyPopupHeight * 0.4f
}.toInt()
popupViewExt.justifyContent = if (anchorLeft) {
JustifyContent.FLEX_START
} else {
@@ -360,7 +376,7 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
else -> 0
}
val y = -keyPopupHeight - when {
row1count > 0 -> keyView.measuredHeight
row1count > 0 -> (keyPopupHeight * 0.4f).toInt()
else -> 0
}
@@ -442,7 +458,7 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
}
if (keyView is KeyView) {
for (k in keyView.data.popup.indices) {
for (k in keyView.dataPopupWithHint.indices) {
val view = popupViewExt.getChildAt(k)
if (view != null) {
val textView = view as KeyPopupExtendedSingleView
@@ -472,7 +488,17 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
*/
fun getActiveKeyData(keyView: T_KV): KeyData? {
return if (keyView is KeyView) {
keyView.data.popup.getOrNull(activeExtIndex ?: -1) ?: keyView.data
val activeExtIndex = activeExtIndex
if (activeExtIndex != null) {
val singleView = popupViewExt[activeExtIndex]
if (singleView is KeyPopupExtendedSingleView) {
keyView.dataPopupWithHint.getOrNull(singleView.adjustedIndex) ?: keyView.data
} else {
keyView.data
}
} else {
keyView.data
}
} else {
null
}

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

@@ -18,20 +18,16 @@ package dev.patrickgold.florisboard.ime.text
import android.content.Context
import android.os.Handler
import android.text.InputType
import android.util.Log
import android.view.KeyEvent
import android.view.inputmethod.CursorAnchorInfo
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.ExtractedTextRequest
import android.view.inputmethod.InputMethodManager
import android.view.inputmethod.*
import android.widget.LinearLayout
import android.widget.ViewFlipper
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.InputView
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.core.*
import dev.patrickgold.florisboard.ime.text.editing.EditingKeyboardView
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyData
import dev.patrickgold.florisboard.ime.text.key.KeyType
@@ -59,35 +55,35 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
FlorisBoard.EventListener {
private val florisboard = FlorisBoard.getInstance()
private val activeEditorInstance: EditorInstance
get() = florisboard.activeEditorInstance
private var activeKeyboardMode: KeyboardMode? = null
private val keyboardViews = EnumMap<KeyboardMode, KeyboardView>(KeyboardMode::class.java)
private var editingKeyboardView: EditingKeyboardView? = null
private val osHandler = Handler()
private var textViewFlipper: ViewFlipper? = null
var textViewGroup: LinearLayout? = null
var keyVariation: KeyVariation = KeyVariation.NORMAL
private val layoutManager = LayoutManager(florisboard)
lateinit var smartbarManager: SmartbarManager
val layoutManager = LayoutManager(florisboard)
private lateinit var smartbarManager: SmartbarManager
// Caps/Space related properties
var caps: Boolean = false
private set
var capsLock: Boolean = false
private set
private var cursorCapsMode: CapsMode = CapsMode.NONE
private var editorCapsMode: CapsMode = CapsMode.NONE
private var hasCapsRecentlyChanged: Boolean = false
private var hasSpaceRecentlyPressed: Boolean = false
// Composing text related properties
private var composingText: String? = null
private var composingTextStart: Int? = null
private var cursorPos: Int = 0
private var isComposingEnabled: Boolean = false
private var isTextSelected: Boolean = false
var isManualSelectionMode: Boolean = false
private var isManualSelectionModeLeft: Boolean = false
private var isManualSelectionModeRight: Boolean = false
companion object {
private val TAG: String? = TextInputManager::class.simpleName
private var instance: TextInputManager? = null
@Synchronized
@@ -99,24 +95,24 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
}
}
init {
florisboard.addEventListener(this)
}
/**
* Non-UI-related setup + preloading of all required computed layouts (asynchronous in the
* background).
*/
override fun onCreate() {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onCreate()")
if (BuildConfig.DEBUG) Log.i(TAG, "onCreate()")
for (mode in KeyboardMode.values()) {
if (mode == KeyboardMode.CHARACTERS) {
var subtypes = florisboard.subtypeManager.subtypes
if (subtypes.isEmpty()) {
subtypes = listOf(Subtype.DEFAULT)
}
for (subtype in subtypes) {
layoutManager.preloadComputedLayout(mode, subtype)
}
} else {
layoutManager.preloadComputedLayout(mode, florisboard.activeSubtype)
var subtypes = florisboard.subtypeManager.subtypes
if (subtypes.isEmpty()) {
subtypes = listOf(Subtype.DEFAULT)
}
for (subtype in subtypes) {
for (mode in KeyboardMode.values()) {
layoutManager.preloadComputedLayout(mode, subtype)
}
}
smartbarManager = SmartbarManager.getInstance()
@@ -124,8 +120,6 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
private suspend fun addKeyboardView(mode: KeyboardMode) {
val keyboardView = KeyboardView(florisboard.context)
keyboardView.florisboard = florisboard
keyboardView.prefs = florisboard.prefs
keyboardView.computedLayout = layoutManager.fetchComputedLayoutAsync(mode, florisboard.activeSubtype).await()
keyboardViews[mode] = keyboardView
withContext(Dispatchers.Main) {
@@ -137,11 +131,12 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* Sets up the newly registered input view.
*/
override fun onRegisterInputView(inputView: InputView) {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onRegisterInputView(inputView)")
if (BuildConfig.DEBUG) Log.i(TAG, "onRegisterInputView(inputView)")
launch(Dispatchers.Default) {
textViewGroup = inputView.findViewById(R.id.text_input)
textViewFlipper = inputView.findViewById(R.id.text_input_view_flipper)
editingKeyboardView = inputView.findViewById(R.id.editing)
val activeKeyboardMode = getActiveKeyboardMode()
addKeyboardView(activeKeyboardMode)
@@ -149,7 +144,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
setActiveKeyboardMode(activeKeyboardMode)
}
for (mode in KeyboardMode.values()) {
if (mode != activeKeyboardMode) {
if (mode != activeKeyboardMode && mode != KeyboardMode.SMARTBAR_NUMBER_ROW) {
addKeyboardView(mode)
}
}
@@ -160,7 +155,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* Cancels all coroutines and cleans up.
*/
override fun onDestroy() {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onDestroy()")
if (BuildConfig.DEBUG) Log.i(TAG, "onDestroy()")
cancel()
osHandler.removeCallbacksAndMessages(null)
@@ -170,58 +165,60 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
}
/**
* Evaluates the [activeKeyboardMode], [keyVariation] and [isComposingEnabled] property values
* when starting to interact with a input editor. Also resets the composing texts and sets the
* initial caps mode accordingly.
* Evaluates the [activeKeyboardMode], [keyVariation] and [EditorInstance.isComposingEnabled]
* property values when starting to interact with a input editor. Also resets the composing
* texts and sets the initial caps mode accordingly.
*/
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
val keyboardMode = when (info) {
null -> KeyboardMode.CHARACTERS
else -> when (info.inputType and InputType.TYPE_MASK_CLASS) {
InputType.TYPE_CLASS_NUMBER -> {
keyVariation = KeyVariation.NORMAL
KeyboardMode.NUMERIC
}
InputType.TYPE_CLASS_PHONE -> {
keyVariation = KeyVariation.NORMAL
KeyboardMode.PHONE
}
InputType.TYPE_CLASS_TEXT -> {
keyVariation = when (info.inputType and InputType.TYPE_MASK_VARIATION) {
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS -> {
KeyVariation.EMAIL_ADDRESS
}
InputType.TYPE_TEXT_VARIATION_PASSWORD,
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD -> {
KeyVariation.PASSWORD
}
InputType.TYPE_TEXT_VARIATION_URI -> {
KeyVariation.URI
}
else -> {
KeyVariation.NORMAL
}
override fun onStartInputView(instance: EditorInstance, restarting: Boolean) {
val keyboardMode = when (instance.inputAttributes.type) {
InputAttributes.Type.NUMBER -> {
keyVariation = KeyVariation.NORMAL
KeyboardMode.NUMERIC
}
InputAttributes.Type.PHONE -> {
keyVariation = KeyVariation.NORMAL
KeyboardMode.PHONE
}
InputAttributes.Type.TEXT -> {
keyVariation = when (instance.inputAttributes.variation) {
InputAttributes.Variation.EMAIL_ADDRESS,
InputAttributes.Variation.WEB_EMAIL_ADDRESS -> {
KeyVariation.EMAIL_ADDRESS
}
InputAttributes.Variation.PASSWORD,
InputAttributes.Variation.VISIBLE_PASSWORD,
InputAttributes.Variation.WEB_PASSWORD -> {
KeyVariation.PASSWORD
}
InputAttributes.Variation.URI -> {
KeyVariation.URI
}
else -> {
KeyVariation.NORMAL
}
KeyboardMode.CHARACTERS
}
else -> {
keyVariation = KeyVariation.NORMAL
KeyboardMode.CHARACTERS
}
KeyboardMode.CHARACTERS
}
else -> {
keyVariation = KeyVariation.NORMAL
KeyboardMode.CHARACTERS
}
}
isComposingEnabled = when (keyboardMode) {
instance.isComposingEnabled = when (keyboardMode) {
KeyboardMode.NUMERIC,
KeyboardMode.PHONE,
KeyboardMode.PHONE2 -> false
else -> keyVariation != KeyVariation.PASSWORD
else -> keyVariation != KeyVariation.PASSWORD &&
florisboard.prefs.suggestion.enabled// &&
//!instance.inputAttributes.flagTextAutoComplete &&
//!instance.inputAttributes.flagTextNoSuggestions
}
if (!florisboard.prefs.correction.rememberCapsLockState) {
capsLock = false
}
updateCapsState()
resetComposingText()
setActiveKeyboardMode(keyboardMode)
smartbarManager.onStartInputView(keyboardMode, isComposingEnabled)
smartbarManager.onStartInputView(keyboardMode)
}
/**
@@ -233,6 +230,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
override fun onWindowShown() {
keyboardViews[KeyboardMode.CHARACTERS]?.updateVisibility()
smartbarManager.onWindowShown()
}
/**
@@ -245,16 +243,21 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
}
/**
* Sets [activeKeyboardMode] and updates the [SmartbarManager.activeContainerId].
* Sets [activeKeyboardMode] and updates the [SmartbarManager.isQuickActionsVisible].
*/
private fun setActiveKeyboardMode(mode: KeyboardMode) {
textViewFlipper?.displayedChild =
textViewFlipper?.indexOfChild(keyboardViews[mode]) ?: 0
fun setActiveKeyboardMode(mode: KeyboardMode) {
textViewFlipper?.displayedChild = textViewFlipper?.indexOfChild(when (mode) {
KeyboardMode.EDITING -> editingKeyboardView
else -> keyboardViews[mode]
}) ?: 0
keyboardViews[mode]?.updateVisibility()
keyboardViews[mode]?.requestLayout()
keyboardViews[mode]?.requestLayoutAllKeys()
activeKeyboardMode = mode
smartbarManager.activeContainerId = smartbarManager.getPreferredContainerId()
smartbarManager.isQuickActionsVisible = false
isManualSelectionMode = false
isManualSelectionModeLeft = false
isManualSelectionModeRight = false
}
override fun onSubtypeChanged(newSubtype: Subtype) {
@@ -269,133 +272,47 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* Main logic point for processing cursor updates as well as parsing the current composing word
* and passing this info on to the [SmartbarManager] to turn it into candidate suggestions.
*/
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
cursorAnchorInfo ?: return
val ic = florisboard.currentInputConnection
if (isComposingEnabled) {
if (cursorAnchorInfo.selectionEnd - cursorAnchorInfo.selectionStart == 0) {
val newCursorPos = cursorAnchorInfo.selectionStart
val prevComposingText = (cursorAnchorInfo.composingText ?: "").toString()
val inputText =
(ic?.getExtractedText(ExtractedTextRequest(), 0)?.text ?: "").toString()
setComposingTextBasedOnInput(inputText, newCursorPos)
if ((newCursorPos == cursorPos) && (composingText == prevComposingText)) {
// Ignore this, as nothing has changed
} else {
cursorPos = newCursorPos
if (composingText != null && composingTextStart != null) {
ic?.setComposingRegion(
composingTextStart!!,
composingTextStart!! + composingText!!.length
)
} else {
resetComposingText()
}
}
} else {
resetComposingText()
}
smartbarManager.generateCandidatesFromComposing(composingText)
override fun onUpdateSelection() {
if (!activeEditorInstance.isNewSelectionInBoundsOfOld) {
isManualSelectionMode = false
isManualSelectionModeLeft = false
isManualSelectionModeRight = false
}
isTextSelected = cursorAnchorInfo.selectionEnd - cursorAnchorInfo.selectionStart != 0
updateCapsState()
smartbarManager.onUpdateSelection()
}
override fun onPrimaryClipChanged() {
smartbarManager.onPrimaryClipChanged()
}
/**
* Resets the [composingText] and [composingTextStart] properties. Does NOT sync with
* [SmartbarManager]!
*
* @param notifyInputConnection If the current input connection should be notified.
*/
private fun resetComposingText(notifyInputConnection: Boolean = true) {
if (notifyInputConnection) {
val ic = florisboard.currentInputConnection
ic?.finishComposingText()
}
composingText = null
composingTextStart = null
}
/**
* Tries to parse the [composingText] from a given [inputCursorPos] within [inputText].
* Sets both [composingText] and [composingTextStart] to null if it fails, else to its
* parsed values.
*
* @param inputText The input text to search in.
* @param inputCursorPos The position where to search in [inputText].
*/
private fun setComposingTextBasedOnInput(inputText: String, inputCursorPos: Int) {
val words = inputText.split("[^\\p{L}]".toRegex())
var pos = 0
resetComposingText(false)
for (word in words) {
if (inputCursorPos >= pos && inputCursorPos <= pos + word.length && word.isNotEmpty()) {
composingText = word
composingTextStart = pos
break
} else {
pos += word.length + 1
}
}
}
/**
* Should primarily pe used by [SmartbarManager.candidateViewOnClickListener] to commit
* a candidate if a user has pressed on it.
*/
fun commitCandidate(candidateText: String) {
val ic = florisboard.currentInputConnection
ic?.setComposingText(candidateText, 1)
ic?.finishComposingText()
}
/**
* Parses the [CapsMode] out of the given [flags].
*
* @param flags The input flags.
* @return A [CapsMode] value.
*/
private fun parseCapsModeFromFlags(flags: Int): CapsMode {
return when {
flags and InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS > 0 -> {
CapsMode.ALL
}
flags and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES > 0 -> {
CapsMode.SENTENCES
}
flags and InputType.TYPE_TEXT_FLAG_CAP_WORDS > 0 -> {
CapsMode.WORDS
}
else -> {
CapsMode.NONE
}
}
}
/**
* Fetches the current cursor caps mode from the current input connection.
*
* @return The [CapsMode] according to the returned flags by the current input connection.
*/
private fun fetchCurrentCursorCapsMode(): CapsMode {
val ic = florisboard.currentInputConnection
val info = florisboard.currentInputEditorInfo
val capsFlags = ic?.getCursorCapsMode(info.inputType) ?: 0
return parseCapsModeFromFlags(capsFlags)
}
/**
* Updates the current caps state according to the [cursorCapsMode], while respecting
* [capsLock] property.
* Updates the current caps state according to the [EditorInstance.cursorCapsMode], while
* respecting [capsLock] property and the correction.autoCapitalization preference.
*/
private fun updateCapsState() {
cursorCapsMode = fetchCurrentCursorCapsMode()
editorCapsMode = parseCapsModeFromFlags(florisboard.currentInputEditorInfo.inputType)
if (!capsLock) {
caps = cursorCapsMode != CapsMode.NONE
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
caps = florisboard.prefs.correction.autoCapitalization &&
activeEditorInstance.cursorCapsMode != InputAttributes.CapsMode.NONE
launch(Dispatchers.Main) {
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
}
}
}
/**
* Executes a given [SwipeAction]. Ignores any [SwipeAction] but the ones relevant for this
* class.
*/
fun executeSwipeAction(swipeAction: SwipeAction) {
when (swipeAction) {
SwipeAction.DELETE_WORD -> handleDeleteWord()
SwipeAction.MOVE_CURSOR_DOWN -> handleArrow(KeyCode.ARROW_DOWN)
SwipeAction.MOVE_CURSOR_UP -> handleArrow(KeyCode.ARROW_UP)
SwipeAction.MOVE_CURSOR_LEFT -> handleArrow(KeyCode.ARROW_LEFT)
SwipeAction.MOVE_CURSOR_RIGHT -> handleArrow(KeyCode.ARROW_RIGHT)
SwipeAction.SHIFT -> handleShift()
else -> {}
}
}
@@ -403,54 +320,41 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* Handles a [KeyCode.DELETE] event.
*/
private fun handleDelete() {
val ic = florisboard.currentInputConnection
ic?.beginBatchEdit()
resetComposingText()
ic?.sendKeyEvent(
KeyEvent(
KeyEvent.ACTION_DOWN,
KeyEvent.KEYCODE_DEL
)
)
ic?.endBatchEdit()
isManualSelectionMode = false
isManualSelectionModeLeft = false
isManualSelectionModeRight = false
activeEditorInstance.deleteBackwards()
}
/**
* Handles a [KeyCode.DELETE_WORD] event.
*/
private fun handleDeleteWord() {
isManualSelectionMode = false
isManualSelectionModeLeft = false
isManualSelectionModeRight = false
activeEditorInstance.deleteWordsBeforeCursor(1)
}
/**
* Handles a [KeyCode.ENTER] event.
*/
private fun handleEnter() {
val ic = florisboard.currentInputConnection
ic?.beginBatchEdit()
resetComposingText()
val action = florisboard.currentInputEditorInfo?.imeOptions ?: 0
if (action and EditorInfo.IME_FLAG_NO_ENTER_ACTION > 0) {
ic?.sendKeyEvent(
KeyEvent(
KeyEvent.ACTION_DOWN,
KeyEvent.KEYCODE_ENTER
)
)
if (activeEditorInstance.imeOptions.flagNoEnterAction) {
activeEditorInstance.performEnter()
} else {
when (action and EditorInfo.IME_MASK_ACTION) {
EditorInfo.IME_ACTION_DONE,
EditorInfo.IME_ACTION_GO,
EditorInfo.IME_ACTION_NEXT,
EditorInfo.IME_ACTION_PREVIOUS,
EditorInfo.IME_ACTION_SEARCH,
EditorInfo.IME_ACTION_SEND -> {
ic?.performEditorAction(action)
}
else -> {
ic?.sendKeyEvent(
KeyEvent(
KeyEvent.ACTION_DOWN,
KeyEvent.KEYCODE_ENTER
)
)
when (activeEditorInstance.imeOptions.action) {
ImeOptions.Action.DONE,
ImeOptions.Action.GO,
ImeOptions.Action.NEXT,
ImeOptions.Action.PREVIOUS,
ImeOptions.Action.SEARCH,
ImeOptions.Action.SEND -> {
activeEditorInstance.performEnterAction(activeEditorInstance.imeOptions.action)
}
else -> activeEditorInstance.performEnter()
}
}
ic?.endBatchEdit()
}
/**
@@ -478,14 +382,13 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* enabled by the user.
*/
private fun handleSpace() {
val ic = florisboard.currentInputConnection
if (florisboard.prefs.correction.doubleSpacePeriod) {
if (hasSpaceRecentlyPressed) {
osHandler.removeCallbacksAndMessages(null)
val text = ic?.getTextBeforeCursor(2, 0) ?: ""
val text = activeEditorInstance.getTextBeforeCursor(2)
if (text.length == 2 && !text.matches("""[.!?‽\s][\s]""".toRegex())) {
ic?.deleteSurroundingText(1, 0)
ic?.commitText(".", 1)
activeEditorInstance.deleteBackwards()
activeEditorInstance.commitText(".")
}
hasSpaceRecentlyPressed = false
} else {
@@ -495,7 +398,147 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
}, 300)
}
}
ic?.commitText(KeyCode.SPACE.toChar().toString(), 1)
activeEditorInstance.commitText(KeyCode.SPACE.toChar().toString())
}
/**
* Handles [KeyCode] arrow and move events, behaves differently depending on text selection.
*/
private fun handleArrow(code: Int) = activeEditorInstance.apply {
val selectionStartMin = 0
val selectionEndMax = cachedText.length
if (selection.isSelectionMode && isManualSelectionMode) {
// Text is selected and it is manual selection -> Expand selection depending on started
// direction.
when (code) {
KeyCode.ARROW_DOWN -> {}
KeyCode.ARROW_LEFT -> {
if (isManualSelectionModeLeft) {
setSelection(
(selection.start - 1).coerceAtLeast(selectionStartMin),
selection.end
)
} else {
setSelection(selection.start, selection.end - 1)
}
}
KeyCode.ARROW_RIGHT -> {
if (isManualSelectionModeRight) {
setSelection(
selection.start,
(selection.end + 1).coerceAtMost(selectionEndMax)
)
} else {
setSelection(selection.start + 1, selection.end)
}
}
KeyCode.ARROW_UP -> {}
KeyCode.MOVE_HOME -> {
if (isManualSelectionModeLeft) {
setSelection(selectionStartMin, selection.end)
} else {
setSelection(selectionStartMin, selection.start)
}
}
KeyCode.MOVE_END -> {
if (isManualSelectionModeRight) {
setSelection(selection.start, selectionEndMax)
} else {
setSelection(selection.end, selectionEndMax)
}
}
}
} else if (selection.isSelectionMode && !isManualSelectionMode) {
// Text is selected but no manual selection mode -> arrows behave as if selection was
// started in manual left mode
when (code) {
KeyCode.ARROW_DOWN -> {}
KeyCode.ARROW_LEFT -> {
setSelection(selection.start, selection.end - 1)
}
KeyCode.ARROW_RIGHT -> {
setSelection(
selection.start,
(selection.end + 1).coerceAtMost(selectionEndMax)
)
}
KeyCode.ARROW_UP -> {}
KeyCode.MOVE_HOME -> {
setSelection(selectionStartMin, selection.start)
}
KeyCode.MOVE_END -> {
setSelection(selection.start, selectionEndMax)
}
}
} else if (!selection.isSelectionMode && isManualSelectionMode) {
// No text is selected but manual selection mode is active, user wants to start a new
// selection. Must set manual selection direction.
when (code) {
KeyCode.ARROW_DOWN -> {}
KeyCode.ARROW_LEFT -> {
setSelection(
(selection.start - 1).coerceAtLeast(selectionStartMin),
selection.start
)
isManualSelectionModeLeft = true
isManualSelectionModeRight = false
}
KeyCode.ARROW_RIGHT -> {
setSelection(
selection.end,
(selection.end + 1).coerceAtMost(selectionEndMax)
)
isManualSelectionModeLeft = false
isManualSelectionModeRight = true
}
KeyCode.ARROW_UP -> {}
KeyCode.MOVE_HOME -> {
setSelection(selectionStartMin, selection.start)
isManualSelectionModeLeft = true
isManualSelectionModeRight = false
}
KeyCode.MOVE_END -> {
setSelection(selection.end, selectionEndMax)
isManualSelectionModeLeft = false
isManualSelectionModeRight = true
}
}
} else {
// No selection and no manual selection mode -> move cursor around
when (code) {
KeyCode.ARROW_DOWN -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN)
KeyCode.ARROW_LEFT -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT)
KeyCode.ARROW_RIGHT -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT)
KeyCode.ARROW_UP -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_UP)
KeyCode.MOVE_HOME -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_UP)
KeyCode.MOVE_END -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_DOWN)
}
}
}
/**
* Handles a [KeyCode.CLIPBOARD_SELECT] event.
*/
private fun handleClipboardSelect() = activeEditorInstance.apply {
if (selection.isSelectionMode) {
if (isManualSelectionMode && isManualSelectionModeLeft) {
setSelection(selection.start, selection.start)
} else {
setSelection(selection.end, selection.end)
}
isManualSelectionMode = false
} else {
isManualSelectionMode = !isManualSelectionMode
// Must call to update UI properly
editingKeyboardView?.onUpdateSelection()
}
}
/**
* Handles a [KeyCode.CLIPBOARD_SELECT_ALL] event.
*/
private fun handleClipboardSelectAll() {
activeEditorInstance.setSelection(0, activeEditorInstance.cachedText.length)
}
/**
@@ -506,11 +549,29 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* @param keyData The [KeyData] object which should be sent.
*/
fun sendKeyPress(keyData: KeyData) {
val ic = florisboard.currentInputConnection
when (keyData.code) {
KeyCode.DELETE -> handleDelete()
KeyCode.ENTER -> handleEnter()
KeyCode.ARROW_DOWN,
KeyCode.ARROW_LEFT,
KeyCode.ARROW_RIGHT,
KeyCode.ARROW_UP,
KeyCode.MOVE_HOME,
KeyCode.MOVE_END -> handleArrow(keyData.code)
KeyCode.CLIPBOARD_CUT -> activeEditorInstance.performClipboardCut()
KeyCode.CLIPBOARD_COPY -> activeEditorInstance.performClipboardCopy()
KeyCode.CLIPBOARD_PASTE -> {
activeEditorInstance.performClipboardPaste()
smartbarManager.resetClipboardSuggestion()
}
KeyCode.CLIPBOARD_SELECT -> handleClipboardSelect()
KeyCode.CLIPBOARD_SELECT_ALL -> handleClipboardSelectAll()
KeyCode.DELETE -> {
handleDelete()
smartbarManager.resetClipboardSuggestion()
}
KeyCode.ENTER -> {
handleEnter()
smartbarManager.resetClipboardSuggestion()
}
KeyCode.LANGUAGE_SWITCH -> florisboard.switchToNextSubtype()
KeyCode.SETTINGS -> florisboard.launchSettings()
KeyCode.SHIFT -> handleShift()
@@ -530,8 +591,6 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
KeyCode.VIEW_SYMBOLS -> setActiveKeyboardMode(KeyboardMode.SYMBOLS)
KeyCode.VIEW_SYMBOLS2 -> setActiveKeyboardMode(KeyboardMode.SYMBOLS2)
else -> {
ic?.beginBatchEdit()
resetComposingText()
when (activeKeyboardMode) {
KeyboardMode.NUMERIC,
KeyboardMode.NUMERIC_ADVANCED,
@@ -540,13 +599,13 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
KeyType.CHARACTER,
KeyType.NUMERIC -> {
val text = keyData.code.toChar().toString()
ic?.commitText(text, 1)
activeEditorInstance.commitText(text)
}
else -> when (keyData.code) {
KeyCode.PHONE_PAUSE,
KeyCode.PHONE_WAIT -> {
val text = keyData.code.toChar().toString()
ic?.commitText(text, 1)
activeEditorInstance.commitText(text)
}
}
}
@@ -558,7 +617,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
true -> keyData.label.toUpperCase(Locale.getDefault())
false -> keyData.label.toLowerCase(Locale.getDefault())
}
ic?.commitText(tld, 1)
activeEditorInstance.commitText(tld)
}
else -> {
var text = keyData.code.toChar().toString()
@@ -566,26 +625,20 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
true -> text.toUpperCase(Locale.getDefault())
false -> text.toLowerCase(Locale.getDefault())
}
ic?.commitText(text, 1)
activeEditorInstance.commitText(text)
}
}
else -> {
Log.e(
this::class.simpleName,
"sendKeyPress(keyData): Received unknown key: $keyData"
)
Log.e(TAG,"sendKeyPress(keyData): Received unknown key: $keyData")
}
}
}
ic?.endBatchEdit()
smartbarManager.resetClipboardSuggestion()
}
}
}
enum class CapsMode {
ALL,
NONE,
SENTENCES,
WORDS;
if (keyData.code != KeyCode.SHIFT && !capsLock) {
updateCapsState()
}
smartbarManager.updateActiveContainerVisibility()
}
}

View File

@@ -0,0 +1,169 @@
/*
* Copyright (C) 2020 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.text.editing
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Typeface
import android.util.AttributeSet
import android.view.MotionEvent
import android.widget.Button
import androidx.appcompat.widget.AppCompatImageButton
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyData
import java.util.*
/**
* View class for managing and rendering an editing key.
*/
class EditingKeyView : AppCompatImageButton {
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
private val data: KeyData
private var isKeyPressed: Boolean = false
private var osTimer: Timer? = null
private var label: String? = null
private var labelPaint: Paint = Paint().apply {
alpha = 255
color = 0
isAntiAlias = true
isFakeBoldText = false
textAlign = Paint.Align.CENTER
textSize = Button(context).textSize
typeface = Typeface.DEFAULT
}
var isHighlighted: Boolean = false
set(value) { field = value; invalidate() }
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, R.style.TextEditingButton)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
val code = when (id) {
R.id.arrow_down -> KeyCode.ARROW_DOWN
R.id.arrow_left -> KeyCode.ARROW_LEFT
R.id.arrow_right -> KeyCode.ARROW_RIGHT
R.id.arrow_up -> KeyCode.ARROW_UP
R.id.backspace -> KeyCode.DELETE
R.id.clipboard_copy -> KeyCode.CLIPBOARD_COPY
R.id.clipboard_cut -> KeyCode.CLIPBOARD_CUT
R.id.clipboard_paste -> KeyCode.CLIPBOARD_PASTE
R.id.move_home -> KeyCode.MOVE_HOME
R.id.move_end -> KeyCode.MOVE_END
R.id.select -> KeyCode.CLIPBOARD_SELECT
R.id.select_all -> KeyCode.CLIPBOARD_SELECT_ALL
else -> 0
}
data = KeyData(code)
context.obtainStyledAttributes(attrs, R.styleable.EditingKeyView).apply {
label = getString(R.styleable.EditingKeyView_android_text)
recycle()
}
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (!isEnabled || event == null) {
return false
}
super.onTouchEvent(event)
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
isKeyPressed = true
florisboard?.keyPressVibrate()
florisboard?.keyPressSound(data)
when (data.code) {
KeyCode.ARROW_DOWN,
KeyCode.ARROW_LEFT,
KeyCode.ARROW_RIGHT,
KeyCode.ARROW_UP,
KeyCode.DELETE -> {
osTimer = Timer()
osTimer?.scheduleAtFixedRate(object : TimerTask() {
override fun run() {
florisboard?.textInputManager?.sendKeyPress(data)
if (!isKeyPressed) {
osTimer?.cancel()
osTimer = null
}
}
}, 500, 50)
}
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
isKeyPressed = false
osTimer?.cancel()
osTimer = null
if (event.actionMasked != MotionEvent.ACTION_CANCEL) {
florisboard?.textInputManager?.sendKeyPress(data)
}
}
else -> return false
}
return true
}
/**
* Draw the key label / drawable.
*/
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas ?: return
imageTintList = ColorStateList.valueOf(when {
isEnabled -> prefs.theme.smartbarFgColor
else -> prefs.theme.smartbarFgColorAlt
})
// Draw label
val label = label
if (label != null) {
labelPaint.color = if (isHighlighted && isEnabled) {
prefs.theme.colorPrimary
} else if (!isEnabled) {
prefs.theme.smartbarFgColorAlt
} else {
prefs.theme.smartbarFgColor
}
val isPortrait =
resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
if (!isPortrait) {
labelPaint.textSize *= 0.9f
}
val centerX = measuredWidth / 2.0f
val centerY = measuredHeight / 2.0f + (labelPaint.textSize - labelPaint.descent()) / 2
if (label.contains("\n")) {
// Even if more lines may be existing only the first 2 are shown
val labelLines = label.split("\n")
canvas.drawText(labelLines[0], centerX, centerY * 0.70f, labelPaint)
canvas.drawText(labelLines[1], centerX, centerY * 1.30f, labelPaint)
} else {
canvas.drawText(label, centerX, centerY, labelPaint)
}
}
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright (C) 2020 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.text.editing
import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.util.setBackgroundTintColor2
/**
* View class for updating the key views depending on the current selection and clipboard state.
*/
class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener {
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
private var arrowUpKey: EditingKeyView? = null
private var arrowDownKey: EditingKeyView? = null
private var selectKey: EditingKeyView? = null
private var selectAllKey: EditingKeyView? = null
private var cutKey: EditingKeyView? = null
private var copyKey: EditingKeyView? = null
private var pasteKey: EditingKeyView? = null
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
florisboard?.addEventListener(this)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
arrowUpKey = findViewById(R.id.arrow_up)
arrowDownKey = findViewById(R.id.arrow_down)
selectKey = findViewById(R.id.select)
selectAllKey = findViewById(R.id.select_all)
cutKey = findViewById(R.id.clipboard_cut)
copyKey = findViewById(R.id.clipboard_copy)
pasteKey = findViewById(R.id.clipboard_paste)
}
override fun onUpdateSelection() {
val isSelectionActive = florisboard?.activeEditorInstance?.selection?.isSelectionMode ?: false
val isSelectionMode = florisboard?.textInputManager?.isManualSelectionMode ?: false
arrowUpKey?.isEnabled = !(isSelectionActive || isSelectionMode)
arrowDownKey?.isEnabled = !(isSelectionActive || isSelectionMode)
selectKey?.isHighlighted = isSelectionActive || isSelectionMode
selectAllKey?.visibility = when {
isSelectionActive -> View.GONE
else -> View.VISIBLE
}
cutKey?.visibility = when {
isSelectionActive -> View.VISIBLE
else -> View.GONE
}
copyKey?.isEnabled = isSelectionActive
pasteKey?.isEnabled = florisboard?.clipboardManager?.hasPrimaryClip() ?: false
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
val height = when (heightMode) {
MeasureSpec.EXACTLY -> {
// Must be this size
heightSize
}
MeasureSpec.AT_MOST -> {
// Can't be bigger than...
(florisboard?.inputView?.desiredTextKeyboardViewHeight ?: 0).coerceAtMost(heightSize)
}
else -> {
// Be whatever you want
florisboard?.inputView?.desiredTextKeyboardViewHeight ?: 0
}
}
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY))
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
setBackgroundTintColor2(this, prefs.theme.smartbarBgColor)
}
}

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,47 @@
/*
* Copyright (C) 2020 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.text.gestures
import java.util.*
/**
* Enum for declaring the possible actions for swipe gestures.
*/
enum class SwipeAction {
NO_ACTION,
DELETE_CHARACTERS_PRECISELY,
DELETE_WORD,
DELETE_WORDS_PRECISELY,
HIDE_KEYBOARD,
MOVE_CURSOR_UP,
MOVE_CURSOR_DOWN,
MOVE_CURSOR_LEFT,
MOVE_CURSOR_RIGHT,
SHIFT,
SWITCH_TO_PREV_SUBTYPE,
SWITCH_TO_NEXT_SUBTYPE;
companion object {
fun fromString(string: String): SwipeAction {
return valueOf(string.toUpperCase(Locale.ROOT))
}
}
override fun toString(): String {
return super.toString().toLowerCase(Locale.ROOT)
}
}

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 {
@@ -69,4 +85,7 @@ object KeyCode {
const val SWITCH_TO_CLIPBOARD_CONTEXT = -214
const val TOGGLE_ONE_HANDED_MODE = -215
const val URI_COMPONENT_TLD = -255
const val KESHIDA = 1600
const val HALF_SPACE = 8204
}

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
@@ -25,7 +24,6 @@ import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.ViewOutlineProvider
import android.view.inputmethod.EditorInfo
import androidx.core.content.ContextCompat.getDrawable
import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
@@ -33,10 +31,13 @@ import androidx.core.view.children
import com.google.android.flexbox.FlexboxLayout
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.ImeOptions
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.gestures.SwipeGesture
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
import dev.patrickgold.florisboard.util.getColorFromAttr
import dev.patrickgold.florisboard.util.setBackgroundTintColor
import dev.patrickgold.florisboard.util.setBackgroundTintColor2
import java.util.*
/**
@@ -51,8 +52,8 @@ import java.util.*
class KeyView(
private val keyboardView: KeyboardView,
val data: KeyData
) : View(keyboardView.context) {
) : View(keyboardView.context), SwipeGesture.Listener {
val dataPopupWithHint: MutableList<KeyData>
private var isKeyPressed: Boolean = false
set(value) {
field = value
@@ -60,8 +61,11 @@ class KeyView(
}
private var osHandler: Handler? = null
private var osTimer: Timer? = null
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
private var shouldBlockNextKeyCode: Boolean = false
private var desiredWidth: Int = 0
private var desiredHeight: Int = 0
private var drawable: Drawable? = null
private var drawableColor: Int = 0
private var drawablePadding: Int = 0
@@ -70,13 +74,25 @@ class KeyView(
alpha = 255
color = 0
isAntiAlias = true
isFakeBoldText = true
isFakeBoldText = false
textAlign = Paint.Align.CENTER
textSize = resources.getDimension(R.dimen.key_textSize)
typeface = Typeface.DEFAULT
}
private var hintedLabel: String? = null
private var hintedLabelPaint: Paint = Paint().apply {
alpha = 120
color = 0
isAntiAlias = true
isFakeBoldText = false
textAlign = Paint.Align.CENTER
textSize = resources.getDimension(R.dimen.key_textHintSize)
typeface = Typeface.DEFAULT
}
private val tempRect: Rect = Rect()
var florisboard: FlorisBoard? = null
private val swipeGestureDetector = SwipeGesture.Detector(context, this)
var touchHitBox: Rect = Rect(-1, -1, -1, -1)
init {
@@ -123,6 +139,23 @@ class KeyView(
background = getDrawable(context, R.drawable.shape_rect_rounded)
elevation = 4.0f
var hintKeyData: KeyData? = null
val hintedNumber = data.hintedNumber
if (prefs.keyboard.hintedNumberRow && hintedNumber != null) {
hintKeyData = hintedNumber
}
val hintedSymbol = data.hintedSymbol
if (prefs.keyboard.hintedSymbols && hintedSymbol != null) {
hintKeyData = hintedSymbol
}
dataPopupWithHint = if (hintKeyData == null) {
data.popup.toMutableList()
} else {
val popupList = data.popup.toMutableList()
popupList.add(hintKeyData)
popupList
}
updateKeyPressedBackground()
}
@@ -171,10 +204,23 @@ class KeyView(
* go look at which child the pointer is actually above.
*/
fun onFlorisTouchEvent(event: MotionEvent?): Boolean {
event ?: return false
if (event == null || !isEnabled) return false
if (swipeGestureDetector.onTouchEvent(event)) {
isKeyPressed = false
osHandler?.removeCallbacksAndMessages(null)
osTimer?.cancel()
osTimer = null
keyboardView.popupManager.hide()
return true
}
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
keyboardView.popupManager.show(this)
shouldBlockNextKeyCode = false
florisboard?.prefs?.keyboard?.let {
if (it.popupEnabled){
keyboardView.popupManager.show(this)
}
}
isKeyPressed = true
florisboard?.keyPressVibrate()
florisboard?.keyPressSound(data)
@@ -190,12 +236,12 @@ class KeyView(
}
}, 500, 50)
}
val delayMillis = keyboardView.prefs.looknfeel.longPressDelay
val delayMillis = prefs.keyboard.longPressDelay
if (osHandler == null) {
osHandler = Handler()
}
osHandler?.postDelayed({
if (data.popup.isNotEmpty()) {
if (dataPopupWithHint.isNotEmpty()) {
keyboardView.popupManager.extend(this)
}
if (data.code == KeyCode.SPACE) {
@@ -248,6 +294,74 @@ class KeyView(
return true
}
/**
* Swipe event handler. Listens to touch_move left/right swipes and triggers the swipe action
* defined in the prefs.
*/
override fun onSwipe(direction: SwipeGesture.Direction, type: SwipeGesture.Type): Boolean {
return when (data.code) {
KeyCode.DELETE -> when (type) {
SwipeGesture.Type.TOUCH_MOVE -> when (direction) {
SwipeGesture.Direction.LEFT -> when (prefs.gestures.deleteKeySwipeLeft) {
SwipeAction.DELETE_CHARACTERS_PRECISELY -> {
florisboard?.activeEditorInstance?.apply {
setSelection(
if (selection.start > 0) { selection.start - 1 } else { selection.start },
selection.end
)
}
shouldBlockNextKeyCode = true
true
}
else -> false
}
SwipeGesture.Direction.RIGHT -> when (prefs.gestures.deleteKeySwipeLeft) {
SwipeAction.DELETE_CHARACTERS_PRECISELY -> {
florisboard?.activeEditorInstance?.apply {
setSelection(
if (selection.start < selection.end) { selection.start + 1 } else { selection.start },
selection.end
)
}
shouldBlockNextKeyCode = true
true
}
else -> false
}
else -> false
}
SwipeGesture.Type.TOUCH_UP -> when (prefs.gestures.deleteKeySwipeLeft) {
SwipeAction.DELETE_CHARACTERS_PRECISELY -> {
florisboard?.activeEditorInstance?.apply {
if (selection.isSelectionMode) {
deleteBackwards()
}
}
true
}
else -> false
}
}
KeyCode.SPACE -> when (type) {
SwipeGesture.Type.TOUCH_MOVE -> when (direction) {
SwipeGesture.Direction.LEFT -> {
florisboard?.executeSwipeAction(prefs.gestures.spaceBarSwipeLeft)
shouldBlockNextKeyCode = true
true
}
SwipeGesture.Direction.RIGHT -> {
florisboard?.executeSwipeAction(prefs.gestures.spaceBarSwipeRight)
shouldBlockNextKeyCode = true
true
}
else -> false
}
else -> false
}
else -> false
}
}
/**
* Solution base from this great StackOverflow answer which explained and helped a lot
* for handling onMeasure():
@@ -255,7 +369,7 @@ class KeyView(
* by Devunwired
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val desiredWidth = when (keyboardView.computedLayout?.mode) {
desiredWidth = when (keyboardView.computedLayout?.mode) {
KeyboardMode.NUMERIC,
KeyboardMode.PHONE,
KeyboardMode.PHONE2 -> (keyboardView.desiredKeyWidth * 2.68f).toInt()
@@ -274,7 +388,7 @@ class KeyView(
else -> keyboardView.desiredKeyWidth
}
}
val desiredHeight = keyboardView.desiredKeyHeight
desiredHeight = keyboardView.desiredKeyHeight
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
@@ -329,24 +443,70 @@ class KeyView(
outlineProvider = KeyViewOutline(w, h)
}
/**
* Updates the enabled state of a key depending on the [data] and its parameters.
*/
private fun updateEnabledState() {
isEnabled = when (data.code) {
KeyCode.CLIPBOARD_COPY,
KeyCode.CLIPBOARD_CUT -> {
florisboard?.activeEditorInstance?.selection?.isSelectionMode == true &&
florisboard?.activeEditorInstance?.isRawInputEditor == false
}
KeyCode.CLIPBOARD_PASTE -> florisboard?.clipboardManager?.hasPrimaryClip() == true
KeyCode.CLIPBOARD_SELECT_ALL -> {
florisboard?.activeEditorInstance?.isRawInputEditor == false
}
else -> true
}
if (!isEnabled) {
isKeyPressed = false
}
}
/**
* Updates the background depending on [isKeyPressed] and [data].
*/
private fun updateKeyPressedBackground() {
if (data.code == KeyCode.ENTER) {
setBackgroundTintColor(
this, when {
isKeyPressed -> R.attr.colorPrimaryDark
else -> R.attr.colorPrimary
when {
keyboardView.isSmartbarKeyboardView -> {
elevation = 0.0f
setBackgroundTintColor2(
this, when {
isKeyPressed && isEnabled -> prefs.theme.smartbarButtonBgColor
else -> prefs.theme.smartbarBgColor
}
)
}
else -> {
elevation = 4.0f
when (data.code) {
KeyCode.ENTER -> {
setBackgroundTintColor2(
this, when {
isKeyPressed && isEnabled -> prefs.theme.keyEnterBgColorPressed
else -> prefs.theme.keyEnterBgColor
}
)
}
KeyCode.SHIFT -> {
setBackgroundTintColor2(
this, when {
isKeyPressed && isEnabled -> prefs.theme.keyShiftBgColorPressed
else -> prefs.theme.keyShiftBgColor
}
)
}
else -> {
setBackgroundTintColor2(
this, when {
isKeyPressed && isEnabled -> prefs.theme.keyBgColorPressed
else -> prefs.theme.keyBgColor
}
)
}
}
)
} else {
setBackgroundTintColor(
this, when {
isKeyPressed -> R.attr.key_bgColorPressed
else -> R.attr.key_bgColor
}
)
}
}
}
@@ -382,6 +542,7 @@ class KeyView(
* TextInputManager.
*/
fun updateVisibility() {
updateEnabledState()
when (data.code) {
KeyCode.SWITCH_TO_TEXT_CONTEXT,
KeyCode.SWITCH_TO_MEDIA_CONTEXT -> {
@@ -415,6 +576,37 @@ class KeyView(
}
}
/**
* Automatically sets the text size of [boxPaint] for given [text] so it fits within the given
* bounds.
*
* Implementation based on this blog post by Lucas (SketchingDev), written on Aug 20, 2015
* https://sketchingdev.co.uk/blog/resizing-text-to-fit-into-a-container-on-android.html
*
* @param boxPaint The [Paint] object which the text size should be applied to.
* @param boxWidth The max width for the surrounding box of [text].
* @param boxHeight The max height for the surrounding box of [text].
* @param text The text for which the size should be calculated.
*/
private fun setTextSizeFor(boxPaint: Paint, boxWidth: Float, boxHeight: Float, text: String) {
var stage = 1
var textSize = 0.0f
while (stage < 3) {
if (stage == 1) {
textSize += 10.0f
} else if (stage == 2) {
textSize -= 1.0f
}
boxPaint.textSize = textSize
boxPaint.getTextBounds(text, 0, text.length, tempRect)
val fits = tempRect.width() < boxWidth && tempRect.height() < boxHeight
if (stage == 1 && !fits || stage == 2 && fits) {
stage++
}
}
boxPaint.textSize = textSize
}
/**
* Draw the key label / drawable.
*/
@@ -426,50 +618,83 @@ class KeyView(
updateKeyPressedBackground()
if (data.type == KeyType.CHARACTER && data.code != KeyCode.SPACE
|| data.type == KeyType.NUMERIC
&& data.code != KeyCode.HALF_SPACE && data.code != KeyCode.KESHIDA || data.type == KeyType.NUMERIC
) {
label = getComputedLetter()
val hintedNumber = data.hintedNumber
if (prefs.keyboard.hintedNumberRow && hintedNumber != null) {
hintedLabel = getComputedLetter(hintedNumber)
}
val hintedSymbol = data.hintedSymbol
if (prefs.keyboard.hintedSymbols && hintedSymbol != null) {
hintedLabel = getComputedLetter(hintedSymbol)
}
} else {
when (data.code) {
KeyCode.ARROW_LEFT -> {
drawable = getDrawable(context, R.drawable.ic_keyboard_arrow_left)
drawableColor = prefs.theme.keyFgColor
}
KeyCode.ARROW_RIGHT -> {
drawable = getDrawable(context, R.drawable.ic_keyboard_arrow_right)
drawableColor = prefs.theme.keyFgColor
}
KeyCode.CLIPBOARD_COPY -> {
drawable = getDrawable(context, R.drawable.ic_content_copy)
drawableColor = prefs.theme.keyFgColor
}
KeyCode.CLIPBOARD_CUT -> {
drawable = getDrawable(context, R.drawable.ic_content_cut)
drawableColor = prefs.theme.keyFgColor
}
KeyCode.CLIPBOARD_PASTE -> {
drawable = getDrawable(context, R.drawable.ic_content_paste)
drawableColor = prefs.theme.keyFgColor
}
KeyCode.CLIPBOARD_SELECT_ALL -> {
drawable = getDrawable(context, R.drawable.ic_select_all)
drawableColor = prefs.theme.keyFgColor
}
KeyCode.DELETE -> {
drawable = getDrawable(context, R.drawable.ic_backspace)
drawableColor = getColorFromAttr(context, R.attr.key_fgColor)
drawableColor = prefs.theme.keyFgColor
}
KeyCode.ENTER -> {
val action = florisboard?.currentInputEditorInfo?.imeOptions ?: 0
drawable = getDrawable(context, when (action and EditorInfo.IME_MASK_ACTION) {
EditorInfo.IME_ACTION_DONE -> R.drawable.ic_done
EditorInfo.IME_ACTION_GO -> R.drawable.ic_arrow_right_alt
EditorInfo.IME_ACTION_NEXT -> R.drawable.ic_arrow_right_alt
EditorInfo.IME_ACTION_NONE -> R.drawable.ic_keyboard_return
EditorInfo.IME_ACTION_PREVIOUS -> R.drawable.ic_arrow_right_alt
EditorInfo.IME_ACTION_SEARCH -> R.drawable.ic_search
EditorInfo.IME_ACTION_SEND -> R.drawable.ic_send
else -> R.drawable.ic_arrow_right_alt
val imeOptions = florisboard?.activeEditorInstance?.imeOptions ?: ImeOptions.default()
drawable = getDrawable(context, when (imeOptions.action) {
ImeOptions.Action.DONE -> R.drawable.ic_done
ImeOptions.Action.GO -> R.drawable.ic_arrow_right_alt
ImeOptions.Action.NEXT -> R.drawable.ic_arrow_right_alt
ImeOptions.Action.NONE -> R.drawable.ic_keyboard_return
ImeOptions.Action.PREVIOUS -> R.drawable.ic_arrow_right_alt
ImeOptions.Action.SEARCH -> R.drawable.ic_search
ImeOptions.Action.SEND -> R.drawable.ic_send
ImeOptions.Action.UNSPECIFIED -> R.drawable.ic_keyboard_return
})
drawableColor = getColorFromAttr(context, R.attr.key_enter_fgColor)
if (action and EditorInfo.IME_FLAG_NO_ENTER_ACTION > 0) {
drawableColor = prefs.theme.keyEnterFgColor
if (imeOptions.flagNoEnterAction) {
drawable = getDrawable(context, R.drawable.ic_keyboard_return)
}
}
KeyCode.LANGUAGE_SWITCH -> {
drawable = getDrawable(context, R.drawable.ic_language)
drawableColor = getColorFromAttr(context, R.attr.key_fgColor)
drawableColor = prefs.theme.keyFgColor
}
KeyCode.PHONE_PAUSE -> label = resources.getString(R.string.key__phone_pause)
KeyCode.PHONE_WAIT -> label = resources.getString(R.string.key__phone_wait)
KeyCode.SHIFT -> {
drawable = getDrawable(context, when {
florisboard?.textInputManager?.caps ?: false && florisboard?.textInputManager?.capsLock ?: false -> {
drawableColor = getColorFromAttr(context, R.attr.colorAccent)
drawableColor = prefs.theme.keyShiftFgColorCapsLock
R.drawable.ic_keyboard_capslock
}
florisboard?.textInputManager?.caps ?: false && !(florisboard?.textInputManager?.capsLock ?: false) -> {
drawableColor = getColorFromAttr(context, R.attr.key_fgColor)
drawableColor = prefs.theme.keyShiftFgColor
R.drawable.ic_keyboard_capslock
}
else -> {
drawableColor = getColorFromAttr(context, R.attr.key_fgColor)
drawableColor = prefs.theme.keyShiftFgColor
R.drawable.ic_keyboard_arrow_up
}
})
@@ -481,7 +706,7 @@ class KeyView(
KeyboardMode.PHONE,
KeyboardMode.PHONE2 -> {
drawable = getDrawable(context, R.drawable.ic_space_bar)
drawableColor = getColorFromAttr(context, R.attr.key_fgColor)
drawableColor = prefs.theme.keyFgColor
}
KeyboardMode.CHARACTERS -> {
label = florisboard?.activeSubtype?.locale?.displayName
@@ -491,7 +716,7 @@ class KeyView(
}
KeyCode.SWITCH_TO_MEDIA_CONTEXT -> {
drawable = getDrawable(context, R.drawable.ic_sentiment_satisfied)
drawableColor = getColorFromAttr(context, R.attr.key_fgColor)
drawableColor = prefs.theme.keyFgColor
}
KeyCode.SWITCH_TO_TEXT_CONTEXT,
KeyCode.VIEW_CHARACTERS -> {
@@ -513,12 +738,21 @@ class KeyView(
KeyCode.VIEW_SYMBOLS2 -> {
label = resources.getString(R.string.key__view_symbols2)
}
KeyCode.HALF_SPACE -> {
label = resources.getString(R.string.key__view_half_space)
}
KeyCode.KESHIDA -> {
label = resources.getString(R.string.key__view_keshida)
}
}
}
// Draw drawable
val drawable = drawable
if (drawable != null) {
if (keyboardView.isSmartbarKeyboardView && !isEnabled) {
drawableColor = prefs.theme.smartbarFgColorAlt
}
var marginV = 0
var marginH = 0
if (measuredWidth > measuredHeight) {
@@ -541,20 +775,40 @@ class KeyView(
// Draw label
val label = label
if (label != null) {
if (data.code == KeyCode.VIEW_NUMERIC || data.code == KeyCode.VIEW_NUMERIC_ADVANCED
|| data.code == KeyCode.SPACE) {
labelPaint.textSize = resources.getDimension(R.dimen.key_numeric_textSize)
} else {
labelPaint.textSize = resources.getDimension(R.dimen.key_textSize)
when (data.code) {
KeyCode.VIEW_NUMERIC, KeyCode.VIEW_NUMERIC_ADVANCED -> {
labelPaint.textSize = resources.getDimension(R.dimen.key_numeric_textSize)
}
else -> when {
data.type == KeyType.CHARACTER && data.code != KeyCode.SPACE -> {
setTextSizeFor(
labelPaint,
desiredWidth - (2.2f * drawablePadding),
desiredHeight - (3.0f * drawablePadding),
// Note: taking a "X" here because it is one of the biggest letters and
// the keys must have the same base character for calculation, else
// they will all look different and weird...
"X"
)
}
else -> {
setTextSizeFor(
labelPaint,
measuredWidth - (2.6f * drawablePadding),
measuredHeight - (3.6f * drawablePadding),
when (data.code) {
KeyCode.VIEW_CHARACTERS, KeyCode.VIEW_SYMBOLS, KeyCode.VIEW_SYMBOLS2 -> {
resources.getString(R.string.key__view_symbols)
}
else -> label
}
)
}
}
}
labelPaint.color = getColorFromAttr(context, R.attr.key_fgColor)
labelPaint.color = prefs.theme.keyFgColor
labelPaint.alpha = if (keyboardView.computedLayout?.mode == KeyboardMode.CHARACTERS &&
data.code == KeyCode.SPACE) { 120 } else { 255 }
val isPortrait =
resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
if (keyboardView.prefs.looknfeel.oneHandedMode != "off" && isPortrait) {
labelPaint.textSize *= 0.9f
}
val centerX = measuredWidth / 2.0f
val centerY = measuredHeight / 2.0f + (labelPaint.textSize - labelPaint.descent()) / 2
if (label.contains("\n")) {
@@ -566,6 +820,25 @@ class KeyView(
canvas.drawText(label, centerX, centerY, labelPaint)
}
}
// Draw hinted label
val hintedLabel = hintedLabel
if (hintedLabel != null) {
setTextSizeFor(
hintedLabelPaint,
desiredWidth * 1.0f / 6.0f,
desiredHeight * 1.0f / 6.0f,
// Note: taking a "X" here because it is one of the biggest letters and
// the keys must have the same base character for calculation, else
// they will all look different and weird...
"X"
)
hintedLabelPaint.color = prefs.theme.keyFgColor
hintedLabelPaint.alpha = 120
val centerX = measuredWidth * 5.0f / 6.0f
val centerY = measuredHeight * 1.0f / 6.0f + (hintedLabelPaint.textSize - hintedLabelPaint.descent()) / 2
canvas.drawText(hintedLabel, centerX, centerY, hintedLabelPaint)
}
}
/**

View File

@@ -18,10 +18,13 @@ package dev.patrickgold.florisboard.ime.text.keyboard
enum class KeyboardMode {
CHARACTERS,
EDITING,
SYMBOLS,
SYMBOLS2,
NUMERIC,
NUMERIC_ADVANCED,
PHONE,
PHONE2
PHONE2,
SMARTBAR_CLIPBOARD_CURSOR_ROW,
SMARTBAR_NUMBER_ROW
}

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,24 @@ class KeyboardView : LinearLayout {
}
var desiredKeyWidth: Int = resources.getDimension(R.dimen.key_width).toInt()
var desiredKeyHeight: Int = resources.getDimension(R.dimen.key_height).toInt()
var florisboard: FlorisBoard? = null
var florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
private var initialKeyCode: Int = 0
var isPreviewMode: Boolean = false
var isSmartbarKeyboardView: Boolean = false
var popupManager = KeyPopupManager<KeyboardView, KeyView>(this)
lateinit var prefs: PrefHelper
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
private val swipeGestureDetector = SwipeGesture.Detector(context, this)
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
colorDrawable = ColorDrawable(getColorFromAttr(context, R.attr.keyboard_bgColor))
background = colorDrawable
orientation = VERTICAL
layoutParams = layoutParams ?: FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT
)
florisboard?.addEventListener(this)
onWindowShown()
}
/**
@@ -107,6 +109,13 @@ class KeyboardView : LinearLayout {
popupManager.dismissAllPopups()
}
override fun onWindowShown() {
swipeGestureDetector.apply {
distanceThreshold = prefs.gestures.swipeDistanceThreshold
velocityThreshold = prefs.gestures.swipeVelocityThreshold
}
}
/**
* Catch all events which are designated for child views.
*/
@@ -124,6 +133,12 @@ class KeyboardView : LinearLayout {
return false
}
val eventFloris = MotionEvent.obtainNoHistory(event)
if (!isSmartbarKeyboardView && swipeGestureDetector.onTouchEvent(event)) {
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_CANCEL)
activeKeyView = null
activePointerId = null
return true
}
val pointerIndex = event.actionIndex
var pointerId = event.getPointerId(pointerIndex)
when (event.actionMasked) {
@@ -134,6 +149,7 @@ class KeyboardView : LinearLayout {
activeX = event.getX(pointerIndex)
activeY = event.getY(pointerIndex)
searchForActiveKeyView()
initialKeyCode = activeKeyView?.data?.code ?: 0
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_DOWN)
} else if (activePointerId != pointerId) {
// New pointer arrived. Send ACTION_UP to current active view and move on
@@ -142,6 +158,7 @@ class KeyboardView : LinearLayout {
activeX = event.getX(pointerIndex)
activeY = event.getY(pointerIndex)
searchForActiveKeyView()
initialKeyCode = activeKeyView?.data?.code ?: 0
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_DOWN)
}
}
@@ -199,6 +216,46 @@ class KeyboardView : LinearLayout {
})
}
/**
* Swipe event handler. Listens to touch_up swipes and executes the swipe action defined for it
* in the prefs.
*/
override fun onSwipe(direction: SwipeGesture.Direction, type: SwipeGesture.Type): Boolean {
return when {
initialKeyCode == KeyCode.DELETE -> {
if (type == SwipeGesture.Type.TOUCH_UP && direction == SwipeGesture.Direction.LEFT &&
prefs.gestures.deleteKeySwipeLeft == SwipeAction.DELETE_WORD) {
florisboard?.executeSwipeAction(prefs.gestures.deleteKeySwipeLeft)
true
} else {
false
}
}
initialKeyCode > KeyCode.SPACE && !popupManager.isShowingExtendedPopup -> when {
!prefs.glide.enabled -> when (type) {
SwipeGesture.Type.TOUCH_UP -> {
val swipeAction = when (direction) {
SwipeGesture.Direction.UP -> prefs.gestures.swipeUp
SwipeGesture.Direction.DOWN -> prefs.gestures.swipeDown
SwipeGesture.Direction.LEFT -> prefs.gestures.swipeLeft
SwipeGesture.Direction.RIGHT -> prefs.gestures.swipeRight
else -> SwipeAction.NO_ACTION
}
if (swipeAction != SwipeAction.NO_ACTION) {
florisboard?.executeSwipeAction(swipeAction)
true
} else {
false
}
}
else -> false
}
else -> false
}
else -> false
}
}
/**
* Searches for an active key view at [activeX]/[activeY].
*/
@@ -232,37 +289,31 @@ class KeyboardView : LinearLayout {
* The desired key heights/widths are being calculated here.
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val keyMarginH = resources.getDimension((R.dimen.key_marginH)).toInt()
desiredKeyWidth = (widthSize / 10) - (2 * keyMarginH)
val keyMarginV = resources.getDimension((R.dimen.key_marginV)).toInt()
val factor = prefs.looknfeel.heightFactor
val keyHeightFactor = when (resources.configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> 0.85f
else -> if (prefs.looknfeel.oneHandedMode == "start" ||
prefs.looknfeel.oneHandedMode == "end") {
0.9f
} else {
1.0f
}
} * when (factor) {
"extra_short" -> 0.85f
"short" -> 0.90f
"mid_short" -> 0.95f
"normal" -> 1.00f
"mid_tall" -> 1.05f
"tall" -> 1.10f
"extra_tall" -> 1.15f
else -> 1.00f
} * when (isPreviewMode) {
true -> 0.90f
else -> 1.00f
val desiredWidth = MeasureSpec.getSize(widthMeasureSpec).toFloat()
desiredKeyWidth = if (isSmartbarKeyboardView) {
(desiredWidth / 6.0f - 2.0f * keyMarginH).roundToInt()
} else {
(desiredWidth / 10.0f - 2.0f * keyMarginH).roundToInt()
}
desiredKeyHeight = (resources.getDimension(R.dimen.key_height) * keyHeightFactor).toInt()
florisboard?.textInputManager?.smartbarManager?.smartbarView?.setHeightFactor(keyHeightFactor)
val desiredHeight = MeasureSpec.getSize(heightMeasureSpec) * if (isPreviewMode) { 0.90f } else { 1.00f }
desiredKeyHeight = when {
isSmartbarKeyboardView -> desiredHeight - 1.5f * keyMarginV
else -> desiredHeight / 4.0f - 2.0f * keyMarginV
}.roundToInt()
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
super.onMeasure(
MeasureSpec.makeMeasureSpec(desiredWidth.roundToInt(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(desiredHeight.roundToInt(), MeasureSpec.EXACTLY)
)
}
override fun onApplyThemeAttributes() {
if (isPreviewMode) {
setBackgroundColor(prefs.theme.keyboardBgColor)
}
}
/**
@@ -309,10 +360,4 @@ class KeyboardView : LinearLayout {
}
}
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
colorDrawable.color = getColorFromAttr(context, R.attr.keyboard_bgColor)
}
}

View File

@@ -24,6 +24,7 @@ data class LayoutData(
val type: LayoutType,
val name: String,
val direction: String,
val modifier: String?,
val arrangement: LayoutDataArrangement = listOf()
) {
private fun getComputedLayoutDataArrangement(): ComputedLayoutDataArrangement {

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,18 +34,19 @@ private typealias KMS = Pair<KeyboardMode, Subtype>
* Class which manages layout loading and caching.
*/
class LayoutManager(private val context: Context) : CoroutineScope by MainScope() {
private val layoutCache: HashMap<KMS, Deferred<ComputedLayoutData>> = hashMapOf()
private val computedLayoutCache: HashMap<KMS, Deferred<ComputedLayoutData>> = hashMapOf()
/**
* Loads the layout for the specified type and name.
*
* @returns the [LayoutData] or null.
* @return the [LayoutData] or null.
*/
private fun loadLayout(ltn: LTN?) = loadLayout(ltn?.first, ltn?.second)
private fun loadLayout(type: LayoutType?, name: String?): LayoutData? {
if (type == null || name == null) {
return null
}
val rawJsonData: String = try {
context.assets.open("ime/text/$type/$name.json").bufferedReader().use { it.readText() }
} catch (e: Exception) {
@@ -108,9 +106,9 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
* @param main The main layout type and name.
* @param modifier The modifier (mod) layout type and name.
* @param extension The extension layout type and name.
* @returns a [ComputedLayoutData] object, regardless of the specified LTNs or errors.
* @return a [ComputedLayoutData] object, regardless of the specified LTNs or errors.
*/
private fun mergeLayouts(
private suspend fun mergeLayoutsAsync(
keyboardMode: KeyboardMode,
subtype: Subtype,
main: LTN? = null,
@@ -120,7 +118,12 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
val computedArrangement: ComputedLayoutDataArrangement = mutableListOf()
val mainLayout = loadLayout(main)
val modifierLayout = loadLayout(modifier)
val modifierToLoad = if (mainLayout?.modifier != null) {
LTN(LayoutType.CHARACTERS_MOD, mainLayout.modifier)
} else {
modifier
}
val modifierLayout = loadLayout(modifierToLoad)
val extensionLayout = loadLayout(extension)
if (extensionLayout != null) {
@@ -139,14 +142,12 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
// merge main and mod here
val mergedRow = mutableListOf<KeyData>()
val firstModRow = modifierLayout.arrangement.firstOrNull()
val firstModKey = firstModRow?.firstOrNull()
if (firstModKey != null) {
mergedRow.add(firstModKey)
}
mergedRow.addAll(mainRow)
val lastModKey = firstModRow?.lastOrNull()
if (lastModKey != null && firstModKey != lastModKey) {
mergedRow.add(lastModKey)
for (modKey in (firstModRow ?: listOf())) {
if (modKey.code == 0) {
mergedRow.addAll(mainRow)
} else {
mergedRow.add(modKey)
}
}
computedArrangement.add(mergedRow)
}
@@ -191,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",
@@ -207,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 {
@@ -220,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")
}
@@ -241,9 +267,15 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
main = LTN(LayoutType.SYMBOLS2, "western_default")
modifier = LTN(LayoutType.SYMBOLS2_MOD, "default")
}
KeyboardMode.SMARTBAR_CLIPBOARD_CURSOR_ROW -> {
extension = LTN(LayoutType.EXTENSION, "clipboard_cursor_row")
}
KeyboardMode.SMARTBAR_NUMBER_ROW -> {
extension = LTN(LayoutType.EXTENSION, "number_row")
}
}
return mergeLayouts(keyboardMode, subtype, main, modifier, extension)
return mergeLayoutsAsync(keyboardMode, subtype, main, modifier, extension)
}
/**
@@ -260,14 +292,14 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
subtype: Subtype
): Deferred<ComputedLayoutData> {
val kms = KMS(keyboardMode, subtype)
val cachedComputedLayout = layoutCache[kms]
val cachedComputedLayout = computedLayoutCache[kms]
return if (cachedComputedLayout != null) {
cachedComputedLayout
} else {
val computedLayout = async(Dispatchers.IO) {
computeLayoutFor(keyboardMode, subtype)
}
layoutCache[kms] = computedLayout
computedLayoutCache[kms] = computedLayout
computedLayout
}
}
@@ -286,8 +318,8 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
subtype: Subtype
) {
val kms = KMS(keyboardMode, subtype)
if (layoutCache[kms] == null) {
layoutCache[kms] = async(Dispatchers.IO) {
if (computedLayoutCache[kms] == null) {
computedLayoutCache[kms] = async(Dispatchers.IO) {
computeLayoutFor(keyboardMode, subtype)
}
}

View File

@@ -1,80 +1,83 @@
package dev.patrickgold.florisboard.ime.text.smartbar
import android.content.Context
import android.util.Log
import android.view.View
import android.view.textservice.SentenceSuggestionsInfo
import android.view.textservice.SpellCheckerSession
import android.view.textservice.SuggestionsInfo
import android.view.textservice.TextServicesManager
import android.widget.Button
import android.widget.ImageButton
import android.widget.LinearLayout
import androidx.core.view.children
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.EditorInstance
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.text.TextInputManager
import dev.patrickgold.florisboard.ime.text.key.KeyData
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
// TODO: Implement suggestion creation functionality
// TODO: Cleanup and reorganize SmartbarManager
class SmartbarManager private constructor() :
SpellCheckerSession.SpellCheckerSessionListener, FlorisBoard.EventListener {
class SmartbarManager private constructor() : CoroutineScope by MainScope(),
FlorisBoard.EventListener {
private val florisboard: FlorisBoard = FlorisBoard.getInstance()
private var isComposingEnabled: Boolean = false
private var spellCheckerSession: SpellCheckerSession? = null
private val textInputManager: TextInputManager = TextInputManager.getInstance()
var smartbarView: SmartbarView? = null
private set
private val activeEditorInstance: EditorInstance
get() = florisboard.activeEditorInstance
private val prefs: PrefHelper
get() = florisboard.prefs
var activeContainerId: Int = R.id.candidates
set(value) { field = value; updateActiveContainerVisibility() }
private val textInputManager: TextInputManager = TextInputManager.getInstance()
private var shouldSuggestClipboardContents: Boolean = false
private var smartbarView: SmartbarView? = null
var isQuickActionsVisible: Boolean = false
private val candidateViewOnClickListener = View.OnClickListener { v ->
val view = v as Button
val text = view.text.toString()
if (text.isNotEmpty()) {
textInputManager.commitCandidate(text)
florisboard.activeEditorInstance.commitCompletion(text)
}
}
private val candidateViewOnLongClickListener = View.OnLongClickListener { v ->
true
}
private val numberRowButtonOnClickListener = View.OnClickListener { v ->
val keyData = when (v.id) {
R.id.number_row_0 -> KeyData(48, "0")
R.id.number_row_1 -> KeyData(49, "1")
R.id.number_row_2 -> KeyData(50, "2")
R.id.number_row_3 -> KeyData(51, "3")
R.id.number_row_4 -> KeyData(52, "4")
R.id.number_row_5 -> KeyData(53, "5")
R.id.number_row_6 -> KeyData(54, "6")
R.id.number_row_7 -> KeyData(55, "7")
R.id.number_row_8 -> KeyData(56, "8")
R.id.number_row_9 -> KeyData(57, "9")
else -> KeyData(0)
}
florisboard.textInputManager.sendKeyPress(keyData)
private val clipboardSuggestionViewOnClickListener = View.OnClickListener {
activeEditorInstance.performClipboardPaste()
shouldSuggestClipboardContents = false
updateActiveContainerVisibility()
}
private val quickActionOnClickListener = View.OnClickListener { v ->
when (v.id) {
R.id.quick_action_switch_to_media_context -> {
activeContainerId = getPreferredContainerId()
florisboard.setActiveInput(R.id.media_input)
R.id.back_button -> {
florisboard.textInputManager.setActiveKeyboardMode(KeyboardMode.CHARACTERS)
smartbarView?.setActiveVariant(R.id.smartbar_variant_default)
}
R.id.quick_action_switch_to_editing_context -> {
if (florisboard.textInputManager.getActiveKeyboardMode() == KeyboardMode.EDITING) {
florisboard.textInputManager.setActiveKeyboardMode(KeyboardMode.CHARACTERS)
smartbarView?.setActiveVariant(R.id.smartbar_variant_default)
} else {
florisboard.textInputManager.setActiveKeyboardMode(KeyboardMode.EDITING)
smartbarView?.setActiveVariant(R.id.smartbar_variant_back_only)
}
}
R.id.quick_action_switch_to_media_context -> florisboard.setActiveInput(R.id.media_input)
R.id.quick_action_open_settings -> florisboard.launchSettings()
R.id.quick_action_one_handed_toggle -> florisboard.toggleOneHandedMode()
else -> return@OnClickListener
}
isQuickActionsVisible = false
updateSmartbarUI()
}
private val quickActionToggleOnClickListener = View.OnClickListener {
activeContainerId = when (activeContainerId) {
R.id.quick_actions -> getPreferredContainerId()
else -> R.id.quick_actions
}
isQuickActionsVisible = !isQuickActionsVisible
updateSmartbarUI()
}
companion object {
@@ -94,23 +97,46 @@ class SmartbarManager private constructor() :
this.smartbarView = smartbarView
smartbarView.quickActionToggle?.setOnClickListener(quickActionToggleOnClickListener)
smartbarView.findViewById<View>(R.id.quick_action_toggle)?.setOnClickListener(quickActionToggleOnClickListener)
val quickActions = smartbarView.findViewById<LinearLayout>(R.id.quick_actions)
for (quickAction in quickActions.children) {
if (quickAction is ImageButton) {
quickAction.setOnClickListener(quickActionOnClickListener)
}
}
val numberRow = smartbarView.findViewById<LinearLayout>(R.id.number_row)
for (numberRowButton in numberRow.children) {
if (numberRowButton is Button) {
numberRowButton.setOnClickListener(numberRowButtonOnClickListener)
launch(Dispatchers.Default) {
val numberRow = smartbarView.findViewById<KeyboardView>(R.id.smartbar_variant_number_row)
numberRow.isSmartbarKeyboardView = true
val layout = textInputManager.layoutManager.fetchComputedLayoutAsync(KeyboardMode.SMARTBAR_NUMBER_ROW, Subtype.DEFAULT).await()
launch(Dispatchers.Main) {
numberRow.computedLayout = layout
numberRow.updateVisibility()
}
}
val clipboardSuggestion = smartbarView.findViewById<Button>(R.id.clipboard_suggestion)
clipboardSuggestion.setOnClickListener(clipboardSuggestionViewOnClickListener)
launch(Dispatchers.Default) {
val ccRow = smartbarView.findViewById<KeyboardView>(R.id.clipboard_cursor_row)
ccRow.isSmartbarKeyboardView = true
val layout = textInputManager.layoutManager.fetchComputedLayoutAsync(KeyboardMode.SMARTBAR_CLIPBOARD_CURSOR_ROW, Subtype.DEFAULT).await()
launch(Dispatchers.Main) {
ccRow.computedLayout = layout
ccRow.updateVisibility()
}
}
val backButton = smartbarView.findViewById<View>(R.id.back_button)
backButton.setOnClickListener(quickActionOnClickListener)
for (candidateView in smartbarView.candidateViewList) {
candidateView.setOnClickListener(candidateViewOnClickListener)
candidateView.setOnLongClickListener(candidateViewOnLongClickListener)
}
updateSmartbarUI()
}
override fun onWindowShown() {
isQuickActionsVisible = false
updateActiveContainerVisibility()
}
// TODO: clean up resources here
@@ -120,137 +146,122 @@ class SmartbarManager private constructor() :
instance = null
}
override fun onGetSuggestions(arr: Array<out SuggestionsInfo>?) {
if (arr == null || arr.isEmpty()) {
return
}
/*val suggestions = arr[0]
for (i in 0 until suggestions.suggestionsCount) {
candidateViewList[i].text = suggestions.getSuggestionAt(i)
if (i == 2) {
break
}
}*/
}
override fun onGetSentenceSuggestions(arr: Array<out SentenceSuggestionsInfo>?) {
if (arr == null || arr.isEmpty()) {
return
}
/*val suggestions = arr[0].getSuggestionsInfoAt(0)
for (i in 0 until suggestions.suggestionsCount) {
candidateViewList[i].text = suggestions.getSuggestionAt(i)
if (i == 2) {
break
}
}*/
}
fun onStartInputView(keyboardMode: KeyboardMode, isComposingEnabled: Boolean) {
this.isComposingEnabled = isComposingEnabled
when {
keyboardMode == KeyboardMode.NUMERIC ||
keyboardMode == KeyboardMode.PHONE ||
keyboardMode == KeyboardMode.PHONE2 -> {
smartbarView?.visibility = View.GONE
}
!isComposingEnabled -> {
smartbarView?.visibility = View.VISIBLE
activeContainerId = R.id.number_row
fun onStartInputView(keyboardMode: KeyboardMode) {
when (keyboardMode) {
KeyboardMode.NUMERIC, KeyboardMode.PHONE, KeyboardMode.PHONE2 -> {
smartbarView?.setActiveVariant(null)
}
else -> {
smartbarView?.visibility = View.VISIBLE
activeContainerId = R.id.candidates
//val tsm = florisboard.getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE) as TextServicesManager
//spellCheckerSession = tsm.newSpellCheckerSession(null, null, this, true)
smartbarView?.setActiveVariant(R.id.smartbar_variant_default)
isQuickActionsVisible = false
}
}
updateSmartbarUI()
}
fun onFinishInputView() {
//spellCheckerSession?.close()
}
fun deleteCandidateFromDictionary(candidate: String) {
//
override fun onUpdateSelection() {
updateSmartbarUI()
}
fun resetCandidates() {
//
}
fun generateCandidatesFromComposing(composingText: String?) {
fun generateCandidatesFromComposing(composingText: String) {
val smartbarView = smartbarView ?: return
if (composingText == null) {
if (composingText == "") {
smartbarView.candidateViewList[0].text = "candidate"
smartbarView.candidateViewList[1].text = "suggestions"
smartbarView.candidateViewList[2].text = "nyi"
} else {
activeContainerId = R.id.candidates
updateActiveContainerVisibility()
smartbarView.candidateViewList[0].text = ""
smartbarView.candidateViewList[1].text = composingText + "test"
smartbarView.candidateViewList[2].text = ""
}
//spellCheckerSession?.getSentenceSuggestions(arrayOf(TextInfo(composing)), 3)
//android.util.Log.i("SPELL", "GEN")
/*val dic: Uri = UserDictionary.Words.CONTENT_URI
val resolver: ContentResolver = florisboard.contentResolver
val cursor: Cursor = resolver.query(dic, null, null, null, null) ?: return
var count = 0
while (cursor.moveToNext()) {
val word = cursor.getString(cursor.getColumnIndex(UserDictionary.Words.WORD))
candidateViewList[count].text = word
if (count++ > 2) {
break
}
}
cursor.close()*/
}
fun writeCandidate(candidate: String) {
//
}
fun getPreferredContainerId(): Int {
return when {
!isComposingEnabled -> when(textInputManager.getActiveKeyboardMode()) {
KeyboardMode.CHARACTERS -> R.id.number_row
else -> 0
}
else -> R.id.candidates
override fun onPrimaryClipChanged() {
if (prefs.suggestion.enabled && prefs.suggestion.suggestClipboardContent) {
shouldSuggestClipboardContents = true
updateActiveContainerVisibility()
}
}
private fun updateActiveContainerVisibility() {
fun resetClipboardSuggestion() {
if (prefs.suggestion.enabled && prefs.suggestion.suggestClipboardContent) {
shouldSuggestClipboardContents = false
updateActiveContainerVisibility()
}
}
private fun updateSmartbarUI() {
val ei = activeEditorInstance
if (ei.selection.isCursorMode && ei.isComposingEnabled) {
generateCandidatesFromComposing(ei.currentWord.text)
}
updateActiveContainerVisibility()
val ccRow = smartbarView?.findViewById<KeyboardView>(R.id.clipboard_cursor_row)
ccRow?.updateVisibility()
}
fun updateActiveContainerVisibility() {
val smartbarView = smartbarView ?: return
when (activeContainerId) {
R.id.quick_actions -> {
smartbarView.candidatesView?.visibility = View.GONE
smartbarView.numberRowView?.visibility = View.GONE
smartbarView.quickActionsView?.visibility = View.VISIBLE
smartbarView.quickActionToggle?.rotation = -180.0f
}
R.id.number_row -> {
smartbarView.candidatesView?.visibility = View.GONE
smartbarView.numberRowView?.visibility = View.VISIBLE
smartbarView.quickActionsView?.visibility = View.GONE
smartbarView.quickActionToggle?.rotation = 0.0f
}
R.id.candidates -> {
smartbarView.candidatesView?.visibility = View.VISIBLE
smartbarView.numberRowView?.visibility = View.GONE
smartbarView.quickActionsView?.visibility = View.GONE
smartbarView.quickActionToggle?.rotation = 0.0f
}
else -> {
smartbarView.candidatesView?.visibility = View.GONE
smartbarView.numberRowView?.visibility = View.GONE
smartbarView.quickActionsView?.visibility = View.GONE
smartbarView.quickActionToggle?.rotation = 0.0f
if (isQuickActionsVisible) {
smartbarView.setActiveVariant(R.id.smartbar_variant_default)
smartbarView.setActiveContainer(R.id.quick_actions)
smartbarView.findViewById<View>(R.id.quick_action_toggle)?.rotation = -180.0f
} else {
when {
textInputManager.getActiveKeyboardMode() == KeyboardMode.EDITING -> {
smartbarView.setActiveVariant(R.id.smartbar_variant_back_only)
smartbarView.setActiveContainer(null)
}
activeEditorInstance.isComposingEnabled -> {
smartbarView.setActiveVariant(R.id.smartbar_variant_default)
val containerId = if (shouldSuggestClipboardContents && florisboard.clipboardManager?.hasPrimaryClip() == true) {
val clipboardSuggestion = smartbarView.findViewById<Button>(R.id.clipboard_suggestion)
val item = florisboard.clipboardManager?.primaryClip?.getItemAt(0)
when {
item?.text != null -> {
clipboardSuggestion?.text = item.text
}
item?.uri != null -> {
clipboardSuggestion?.text = "(Image) " + item.uri.toString()
}
else -> {
clipboardSuggestion?.text = item?.text ?: "(Error while retrieving clipboard data)"
}
}
R.id.clipboard_suggestion_row
} else {
R.id.candidates
}
smartbarView.setActiveContainer(containerId)
}
textInputManager.getActiveKeyboardMode() == KeyboardMode.CHARACTERS -> {
when (prefs.suggestion.showInstead) {
"number_row" -> {
smartbarView.setActiveVariant(R.id.smartbar_variant_number_row)
smartbarView.setActiveContainer(null)
}
"clipboard_cursor_tools" -> {
smartbarView.setActiveVariant(R.id.smartbar_variant_default)
smartbarView.setActiveContainer(R.id.clipboard_cursor_row)
}
else -> {
smartbarView.setActiveVariant(null)
smartbarView.setActiveContainer(null)
}
}
}
else -> {
smartbarView.setActiveVariant(null)
smartbarView.setActiveContainer(null)
}
}
smartbarView.findViewById<View>(R.id.quick_action_toggle)?.rotation = 0.0f
}
}
}

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,13 +17,21 @@
package dev.patrickgold.florisboard.ime.text.smartbar
import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageButton
import android.widget.LinearLayout
import androidx.annotation.IdRes
import androidx.core.view.children
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.util.setBackgroundTintColor2
import dev.patrickgold.florisboard.util.setDrawableTintColor2
/**
* View class which keeps the references to important children and informs [SmartbarManager] that
@@ -31,19 +39,15 @@ import dev.patrickgold.florisboard.R
* a theme change).
*/
class SmartbarView : LinearLayout {
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
private val smartbarManager = SmartbarManager.getInstance()
var candidatesView: LinearLayout? = null
private set
private var variants: MutableList<ViewGroup> = mutableListOf()
private var containers: MutableList<ViewGroup> = mutableListOf()
var candidateViewList: MutableList<Button> = mutableListOf()
private set
var numberRowView: LinearLayout? = null
private set
var quickActionsView: LinearLayout? = null
private set
var quickActionToggle: ImageButton? = null
private set
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
@@ -54,24 +58,95 @@ class SmartbarView : LinearLayout {
super.onAttachedToWindow()
candidatesView = findViewById(R.id.candidates)
variants.add(findViewById(R.id.smartbar_variant_default))
variants.add(findViewById(R.id.smartbar_variant_back_only))
variants.add(findViewById(R.id.smartbar_variant_number_row))
containers.add(findViewById(R.id.candidates))
containers.add(findViewById(R.id.clipboard_suggestion_row))
containers.add(findViewById(R.id.clipboard_cursor_row))
containers.add(findViewById(R.id.quick_actions))
candidateViewList.add(findViewById(R.id.candidate0))
candidateViewList.add(findViewById(R.id.candidate1))
candidateViewList.add(findViewById(R.id.candidate2))
numberRowView = findViewById(R.id.number_row)
quickActionsView = findViewById(R.id.quick_actions)
quickActionToggle = findViewById(R.id.quick_action_toggle)
smartbarManager.registerSmartbarView(this)
}
/**
* Multiplies the default smartbar height with the given [factor] and sets it.
* Sets the active Smartbar variant based on the given id. Pass null to hide all variants and
* show an empty Smartbar.
*
* @param which Which variant to show. Pass null to hide all.
*/
fun setHeightFactor(factor: Float) {
val baseSize = resources.getDimension(R.dimen.smartbar_height)
val size = (baseSize * factor).toInt()
layoutParams?.height = size
fun setActiveVariant(@IdRes which: Int?) {
for (variant in variants) {
if (variant.id == which) {
variant.visibility = View.VISIBLE
} else {
variant.visibility = View.GONE
}
}
}
/**
* Sets the active Smartbar container based on the given id. Does only work if the currently
* shown Smartbar variant is [R.id.smartbar_variant_default]. Pass null to hide all containers
* and show only the quick action toggle.
*
* @param which Which container to show. Pass null to hide all.
*/
fun setActiveContainer(@IdRes which: Int?) {
for (container in containers) {
if (container.id == which) {
container.visibility = View.VISIBLE
} else {
container.visibility = View.GONE
}
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
val height = when (heightMode) {
MeasureSpec.EXACTLY -> {
// Must be this size
heightSize
}
MeasureSpec.AT_MOST -> {
// Can't be bigger than...
(florisboard?.inputView?.desiredSmartbarHeight ?: 0).coerceAtMost(heightSize)
}
else -> {
// Be whatever you want
florisboard?.inputView?.desiredSmartbarHeight ?: 0
}
}
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY))
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
setBackgroundColor(prefs.theme.smartbarBgColor)
for (container in containers + variants) {
when (container.id) {
R.id.clipboard_suggestion_row -> {
val clipboardSuggestion = findViewById<Button>(R.id.clipboard_suggestion)
setBackgroundTintColor2(clipboardSuggestion, prefs.theme.smartbarButtonBgColor)
setDrawableTintColor2(clipboardSuggestion, prefs.theme.smartbarButtonFgColor)
clipboardSuggestion.setTextColor(prefs.theme.smartbarButtonFgColor)
}
R.id.candidates -> {
for (view in container.children) {
if (view is Button) {
view.setTextColor(prefs.theme.smartbarFgColor)
}
}
}
}
}
}
}

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.
* @return A parsed [Theme] or null. A null value may indicate that
* the file does not exist or that an error during the reading
* of the file occurred.
*/
fun fromJsonFile(context: Context, path: String): Theme? {
val rawJsonData: String = try {
context.assets.open(path).bufferedReader().use { it.readText() }
} catch (e: Exception) {
null
} ?: return null
return fromJsonString(rawJsonData)
}
/**
* Loads a theme from the given [rawData].
*
* @param rawData The raw json theme file as a string.
* @return A parsed [Theme] or null. A null value may indicate that an error
* during the reading of the [rawData] occurred.
*/
fun fromJsonString(rawData: String): Theme? {
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
val layoutAdapter = moshi.adapter(Theme::class.java)
return layoutAdapter.fromJson(rawData)
}
/**
* Writes a given [theme] to the [prefs]. The default color values are based off the
* Floris Day theme and are not intended to be modified. Instead, themes should be defined
* in assets/ime/theme/<theme_id>.json
*
* @param theme The theme data.
* @param prefs The preference object to write the theme to.
*/
fun writeThemeToPrefs(prefs: PrefHelper, theme: Theme) {
// Internal prefs part I
prefs.internal.themeCurrentBasedOn = theme.name
prefs.internal.themeCurrentIsNight = theme.isNightTheme
// Theme attributes
prefs.theme.colorPrimary = theme.getAttr("window/colorPrimary", "#4CAF50")
prefs.theme.colorPrimaryDark = theme.getAttr("window/colorPrimaryDark", "#388E3C")
prefs.theme.colorAccent = theme.getAttr("window/colorAccent", "#FF9800")
prefs.theme.navBarColor = theme.getAttr("window/navigationBarColor", "#E0E0E0")
prefs.theme.navBarIsLight = (theme.getAttrOrNull("window/navigationBarLight") ?: 0) > 0
prefs.theme.keyboardBgColor = theme.getAttr("keyboard/bgColor", "#E0E0E0")
prefs.theme.keyBgColor = theme.getAttr("key/bgColor", "#FFFFFF")
prefs.theme.keyBgColorPressed = theme.getAttr("key/bgColorPressed", "#F5F5F5")
prefs.theme.keyFgColor = theme.getAttr("key/fgColor", "#000000")
prefs.theme.keyEnterBgColor = theme.getAttr("keyEnter/bgColor", "#4CAF50")
prefs.theme.keyEnterBgColorPressed = theme.getAttr("keyEnter/bgColorPressed", "#388E3C")
prefs.theme.keyEnterFgColor = theme.getAttr("keyEnter/fgColor", "#FFFFFF")
prefs.theme.keyPopupBgColor = theme.getAttr("keyPopup/bgColor", "#EEEEEE")
prefs.theme.keyPopupBgColorActive = theme.getAttr("keyPopup/bgColorActive", "#BDBDBD")
prefs.theme.keyPopupFgColor = theme.getAttr("keyPopup/fgColor", "#000000")
prefs.theme.keyShiftBgColor = theme.getAttr("keyShift/bgColor", "#FFFFFF")
prefs.theme.keyShiftBgColorPressed = theme.getAttr("keyShift/bgColorPressed", "#F5F5F5")
prefs.theme.keyShiftFgColor = theme.getAttr("keyShift/fgColor", "#000000")
prefs.theme.keyShiftFgColorCapsLock = theme.getAttr("keyShift/fgColorCapsLock", "#FF9800")
prefs.theme.mediaFgColor = theme.getAttr("media/fgColor", "#000000")
prefs.theme.mediaFgColorAlt = theme.getAttr("media/fgColorAlt", "#757575")
prefs.theme.oneHandedBgColor = theme.getAttr("oneHanded/bgColor", "#E8F5E9")
prefs.theme.oneHandedButtonFgColor = theme.getAttr("oneHandedButton/fgColor", "#424242")
prefs.theme.smartbarBgColor = theme.getAttr("smartbar/bgColor", "#E0E0E0")
prefs.theme.smartbarFgColor = theme.getAttr("smartbar/fgColor", "#000000")
prefs.theme.smartbarFgColorAlt = theme.getAttr("smartbar/fgColorAlt", "#4A000000")
prefs.theme.smartbarButtonBgColor = theme.getAttr("smartbarButton/bgColor", "#FFFFFF")
prefs.theme.smartbarButtonFgColor = theme.getAttr("smartbarButton/fgColor", "#000000")
// Internal prefs part II (must be written at the end!!)
prefs.internal.themeCurrentIsModified = false
}
}
init {
val listOfAttrsToReevaluate = mutableListOf<Triple<String, String, String>>()
for (group in rawAttrs) {
val groupMap = mutableMapOf<String, Int>()
parsedAttrs[group.key] = groupMap
for (attr in group.value) {
val colorRegex = """[#]([0-9a-fA-F]{8}|[0-9a-fA-F]{6})""".toRegex()
val refRegex = """[@]([a-zA-Z_][a-zA-Z0-9_]*)[/]([a-zA-Z_][a-zA-Z0-9_]*)""".toRegex()
when {
attr.value.matches(colorRegex) -> {
groupMap[attr.key] = Color.parseColor(attr.value)
}
attr.value == "transparent" -> {
groupMap[attr.key] = Color.TRANSPARENT
}
attr.value == "true" -> {
groupMap[attr.key] = 0x1
}
attr.value == "false" -> {
groupMap[attr.key] = 0x0
}
attr.value.matches(refRegex) -> {
val attrValue = getAttrOrNull(attr.value.substring(1))
if (attrValue != null) {
groupMap[attr.key] = attrValue
} else {
listOfAttrsToReevaluate.add(Triple(group.key, attr.key, attr.value))
}
}
else -> {
throw IllegalArgumentException("The specified attr '${attr.key}' = '${attr.value}' is not valid!")
}
}
}
}
for (attrToReevaluate in listOfAttrsToReevaluate) {
val attrValue = getAttrOrNull(attrToReevaluate.third.substring(1))
if (attrValue != null) {
parsedAttrs[attrToReevaluate.first]?.put(attrToReevaluate.second, attrValue)
} else {
throw IllegalArgumentException("The specified attr '${attrToReevaluate.second}' = '${attrToReevaluate.third}' is not valid!")
}
}
}
fun getAttr(key: String, defaultColor: String): Int {
return getAttrOrNull(key) ?: Color.parseColor(defaultColor)
}
fun getAttr(group: String, attr: String, defaultColor: String): Int {
return getAttrOrNull(group, attr) ?: Color.parseColor(defaultColor)
}
fun getAttrOrNull(key: String): Int? {
val regex = """([a-zA-Z_][a-zA-Z0-9_]*)[/]([a-zA-Z_][a-zA-Z0-9_]*)""".toRegex()
return if (key.matches(regex)) {
val split = key.split("/")
getAttrOrNull(split[0], split[1])
} else {
null
}
}
fun getAttrOrNull(group: String, attr: String): Int? {
return parsedAttrs[group]?.get(attr)
}
}
/**
* Data class which is used to quickly parse only the relevant meta data to
* display a theme in a selection list.
*
* @see [Theme] for details regarding the attributes and the theme structure.
*/
data class ThemeMetaOnly(
val name: String,
val displayName: String,
val author: String,
val isNightTheme: Boolean = false
) {
companion object {
/**
* Loads the theme meta data from the specified [path].
*
* @param context A reference to the current [Context]. Used to request
* asset file.
* @param path The path to the json theme file in the asset folder.
* @return [ThemeMetaOnly] or null. A null value may indicate that
* the file does not exist or that an error during the reading
* of the file occurred.
*/
fun loadFromJsonFile(context: Context, path: String): ThemeMetaOnly? {
val rawJsonData: String = try {
context.assets.open(path).bufferedReader().use { it.readText() }
} catch (e: Exception) {
null
} ?: return null
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
val layoutAdapter = moshi.adapter(ThemeMetaOnly::class.java)
return layoutAdapter.fromJson(rawJsonData)
}
/**
* Loads all theme meta data from the specified [path].
*
* @param context A reference to the current [Context]. Used to request
* asset file.
* @param path The path to the dir in the asset folder.
* @return [ThemeMetaOnly] or null. A null value may indicate that
* the file does not exist or that an error during the reading
* of the file occurred.
*/
fun loadAllFromDir(context: Context, path: String): List<ThemeMetaOnly> {
val ret = mutableListOf<ThemeMetaOnly>()
try {
val list = context.assets.list(path)
if (list != null && list.isNotEmpty()) {
// Is a folder
for (file in list) {
val subList = context.assets.list("$path/$file")
if (subList?.isEmpty() == true) {
// Is file
val metaData = loadFromJsonFile(context, "$path/$file")
if (metaData != null) {
ret.add(metaData)
}
}
}
}
} catch (e: java.lang.Exception) {}
return ret
}
}
}

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,164 @@
/*
* Copyright (C) 2020 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.settings.components
import android.app.AlertDialog
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.SeekBar
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.databinding.SeekBarDialogBinding
/**
* Custom preference which represents a seek bar which shows the current value in the summary. The
* value can be changed by clicking on the preference, which brings up a dialog which a seek bar.
* This implementation also allows for a min / max step value, while being backwards compatible.
*
* @see R.styleable.DialogSeekBarPreferenceAttrs for which xml attributes this preference accepts
* besides the default Preference attributes.
*
* @property defaultValue The default value of this preference.
* @property systemDefaultValue At this exact value [systemDefaultValueText] should be shown instead
* of the actual value.
* @property systemDefaultValueText The text to show if this preference's value or seek bar is
* [systemDefaultValue]. Set to null to disable the system default text feature.
* @property min The minimum value of the seek bar. Must not be greater or equal than [max].
* @property max The maximum value of the seek bar. Must not be lesser or equal than [min].
* @property step The step in which the seek bar increases per move. If the provided value is less
* than 1, 1 will be used as step. Note that the xml attribute's name for this property is
* [R.styleable.DialogSeekBarPreferenceAttrs_seekBarIncrement].
* @property unit The unit to show after the value. Set to an empty string to disable this feature.
*/
class DialogSeekBarPreference : Preference {
private var defaultValue: Int = 0
private var systemDefaultValue: Int = -1
private var systemDefaultValueText: String? = null
private var min: Int = 0
private var max: Int = 100
private var step: Int = 1
private var unit: String = ""
@Suppress("unused")
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
layoutResource = R.layout.list_item
context.obtainStyledAttributes(attrs, R.styleable.DialogSeekBarPreferenceAttrs).apply {
min = getInt(R.styleable.DialogSeekBarPreferenceAttrs_min, min)
max = getInt(R.styleable.DialogSeekBarPreferenceAttrs_max, max)
step = getInt(R.styleable.DialogSeekBarPreferenceAttrs_seekBarIncrement, step)
if (step < 1) {
step = 1
}
defaultValue = getInt(R.styleable.DialogSeekBarPreferenceAttrs_android_defaultValue, defaultValue)
systemDefaultValue = getInt(R.styleable.DialogSeekBarPreferenceAttrs_systemDefaultValue, min - 1)
systemDefaultValueText = getString(R.styleable.DialogSeekBarPreferenceAttrs_systemDefaultValueText)
unit = getString(R.styleable.DialogSeekBarPreferenceAttrs_unit) ?: unit
recycle()
}
onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
summary = getTextForValue(newValue.toString())
true
}
onPreferenceClickListener = OnPreferenceClickListener {
showSeekBarDialog()
true
}
}
override fun onAttachedToHierarchy(preferenceManager: PreferenceManager?) {
super.onAttachedToHierarchy(preferenceManager)
summary = getTextForValue(sharedPreferences.getInt(key, defaultValue))
}
/**
* Generates the text for the given [value] and adds the defined [unit] at the end.
* If [systemDefaultValueText] is not null this method tries to match the given [value] with
* [systemDefaultValue] and returns [systemDefaultValueText] upon matching.
*/
private fun getTextForValue(value: Any): String {
if (value !is Int) {
return "??$unit"
}
val systemDefValText = systemDefaultValueText
return if (value == systemDefaultValue && systemDefValText != null) {
systemDefValText
} else {
value.toString() + unit
}
}
/**
* Shows the seek bar dialog.
*/
private fun showSeekBarDialog() {
val inflater =
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val dialogView = SeekBarDialogBinding.inflate(inflater)
val initValue = sharedPreferences.getInt(key, defaultValue)
dialogView.seekBar.max = actualValueToSeekBarProgress(max)
dialogView.seekBar.progress = actualValueToSeekBarProgress(initValue)
dialogView.seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
dialogView.seekBarValue.text = getTextForValue(seekBarProgressToActualValue(progress))
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
})
dialogView.seekBarValue.text = getTextForValue(initValue)
AlertDialog.Builder(context).apply {
setTitle(this@DialogSeekBarPreference.title)
setCancelable(true)
setView(dialogView.root)
setPositiveButton(android.R.string.ok) { _, _ ->
val actualValue = seekBarProgressToActualValue(dialogView.seekBar.progress)
sharedPreferences.edit().putInt(key, actualValue).apply()
}
setNeutralButton(R.string.settings__default) { _, _ ->
sharedPreferences.edit().putInt(key, defaultValue).apply()
}
setNegativeButton(android.R.string.cancel, null)
setOnDismissListener { summary = getTextForValue(sharedPreferences.getInt(key, defaultValue)) }
create()
show()
}
}
/**
* Converts the actual value to a progress value which the Android SeekBar implementation can
* handle. (Android's SeekBar step is fixed at 1 and min at 0)
*
* @param actual The actual value.
* @return the internal value which is used to allow different min and step values.
*/
private fun actualValueToSeekBarProgress(actual: Int): Int {
return (actual - min) / step
}
/**
* Converts the Android SeekBar value to the actual value.
*
* @param progress The progress value of the SeekBar.
* @return the actual value which is ready to use.
*/
private fun seekBarProgressToActualValue(progress: Int): Int {
return (progress * step) + min
}
}

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,118 @@
/*
* Copyright (C) 2020 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.settings.fragments
import android.content.SharedPreferences
import android.os.Bundle
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.databinding.SettingsFragmentThemeBinding
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
import dev.patrickgold.florisboard.ime.text.layout.LayoutManager
import dev.patrickgold.florisboard.settings.SettingsMainActivity
import kotlinx.coroutines.*
import kotlin.math.roundToInt
class ThemeFragment : SettingsMainActivity.SettingsFragment(), CoroutineScope by MainScope(),
SharedPreferences.OnSharedPreferenceChangeListener {
private lateinit var binding: SettingsFragmentThemeBinding
private lateinit var keyboardView: KeyboardView
private lateinit var prefs: PrefHelper
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
prefs = PrefHelper.getDefaultInstance(requireContext())
binding = SettingsFragmentThemeBinding.inflate(inflater, container, false)
launch(Dispatchers.Default) {
val themeContext = ContextThemeWrapper(context, FlorisBoard.getDayNightBaseThemeId(prefs.internal.themeCurrentIsNight))
val layoutManager = LayoutManager(themeContext)
keyboardView = KeyboardView(themeContext)
keyboardView.layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
resources.getDimension(R.dimen.textKeyboardView_baseHeight).roundToInt()
).apply {
val m = resources.getDimension(R.dimen.keyboard_preview_margin).toInt()
setMargins(m, m, m, m)
}
prefs.sync()
keyboardView.isPreviewMode = true
val subtype = subtypeManager.getActiveSubtype() ?: Subtype.DEFAULT
keyboardView.computedLayout = layoutManager.fetchComputedLayoutAsync(KeyboardMode.CHARACTERS, subtype).await()
keyboardView.updateVisibility()
keyboardView.onApplyThemeAttributes()
withContext(Dispatchers.Main) {
binding.root.addView(keyboardView, 0)
}
}
loadThemePrefFragment()
return binding.root
}
private fun loadThemePrefFragment() {
childFragmentManager
.beginTransaction()
.replace(
binding.prefsFrame.id,
SettingsMainActivity.PrefFragment.createFromResource(R.xml.prefs_theme)
)
.commit()
}
override fun onSharedPreferenceChanged(sp: SharedPreferences?, key: String?) {
prefs.sync()
key ?: return
if (key == PrefHelper.Internal.THEME_CURRENT_BASED_ON ||
key == PrefHelper.Internal.THEME_CURRENT_IS_MODIFIED && !prefs.internal.themeCurrentIsModified) {
loadThemePrefFragment()
}
if (key.startsWith("theme__")) {
prefs.internal.themeCurrentIsModified = true
keyboardView.onApplyThemeAttributes()
keyboardView.invalidate()
keyboardView.invalidateAllKeys()
}
}
override fun onResume() {
prefs.shared.registerOnSharedPreferenceChangeListener(this)
super.onResume()
}
override fun onPause() {
prefs.shared.unregisterOnSharedPreferenceChangeListener(this)
super.onPause()
}
override fun onDestroy() {
cancel()
super.onDestroy()
}
}

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
@@ -23,16 +23,15 @@ import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.FrameLayout
import com.google.android.material.snackbar.Snackbar
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.databinding.SettingsFragmentKeyboardBinding
import dev.patrickgold.florisboard.databinding.SettingsFragmentKeyboardSubtypeDialogBinding
import dev.patrickgold.florisboard.databinding.SettingsFragmentKeyboardSubtypeListItemBinding
import dev.patrickgold.florisboard.databinding.ListItemBinding
import dev.patrickgold.florisboard.databinding.SettingsFragmentTypingBinding
import dev.patrickgold.florisboard.databinding.SettingsFragmentTypingSubtypeDialogBinding
import dev.patrickgold.florisboard.settings.SettingsMainActivity
import dev.patrickgold.florisboard.util.LocaleUtils
class KeyboardFragment : SettingsMainActivity.SettingsFragment() {
private lateinit var binding: SettingsFragmentKeyboardBinding
class TypingFragment : SettingsMainActivity.SettingsFragment() {
private lateinit var binding: SettingsFragmentTypingBinding
/**
* Must always have a reference to the open AlertDialog to dismiss the AlertDialog in the event
* of onDestroy(), if this is not done a memory leak will most likely happen!
@@ -44,17 +43,18 @@ class KeyboardFragment : SettingsMainActivity.SettingsFragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = SettingsFragmentKeyboardBinding.inflate(inflater, container, false)
binding = SettingsFragmentTypingBinding.inflate(inflater, container, false)
binding.subtypeAddBtn.setOnClickListener { showAddSubtypeDialog() }
updateSubtypeListView()
val transaction = childFragmentManager.beginTransaction()
transaction.replace(
binding.prefsKeyboardFrame.id,
SettingsMainActivity.PrefFragment.createFromResource(R.xml.prefs_keyboard)
)
transaction.commit()
childFragmentManager
.beginTransaction()
.replace(
binding.prefsFrame.id,
SettingsMainActivity.PrefFragment.createFromResource(R.xml.prefs_typing)
)
.commit()
return binding.root
}
@@ -66,7 +66,7 @@ class KeyboardFragment : SettingsMainActivity.SettingsFragment() {
private fun showAddSubtypeDialog() {
val dialogView =
SettingsFragmentKeyboardSubtypeDialogBinding.inflate(layoutInflater)
SettingsFragmentTypingSubtypeDialogBinding.inflate(layoutInflater)
val languageAdapter: ArrayAdapter<String> = ArrayAdapter(
requireContext(),
android.R.layout.simple_spinner_dropdown_item,
@@ -95,11 +95,11 @@ class KeyboardFragment : SettingsMainActivity.SettingsFragment() {
)
dialogView.layoutSpinner.adapter = layoutAdapter
AlertDialog.Builder(context).apply {
setTitle(R.string.settings__keyboard__subtype_add_title)
setTitle(R.string.settings__localization__subtype_add_title)
setCancelable(true)
setView(dialogView.root)
setPositiveButton(R.string.settings__keyboard__subtype_add, null)
setNegativeButton(R.string.settings__keyboard__subtype_cancel) { _, _ -> }
setPositiveButton(R.string.settings__localization__subtype_add, null)
setNegativeButton(R.string.settings__localization__subtype_cancel) { _, _ -> }
setOnDismissListener { activeDialogWindow = null }
create()
activeDialogWindow = show()
@@ -110,7 +110,7 @@ class KeyboardFragment : SettingsMainActivity.SettingsFragment() {
val layoutName = subtypeManager.imeConfig.characterLayouts.keys.toList()[dialogView.layoutSpinner.selectedItemPosition]
val success = subtypeManager.addSubtype(LocaleUtils.stringToLocale(languageCode), layoutName)
if (!success) {
dialogView.errorBox.setText(R.string.settings__keyboard__subtype_error_already_exists)
dialogView.errorBox.setText(R.string.settings__localization__subtype_error_already_exists)
dialogView.errorBox.visibility = View.VISIBLE
} else {
updateSubtypeListView()
@@ -123,7 +123,7 @@ class KeyboardFragment : SettingsMainActivity.SettingsFragment() {
private fun showEditSubtypeDialog(id: Int) {
val subtype = subtypeManager.getSubtypeById(id) ?: return
val dialogView =
SettingsFragmentKeyboardSubtypeDialogBinding.inflate(layoutInflater)
SettingsFragmentTypingSubtypeDialogBinding.inflate(layoutInflater)
val languageAdapter: ArrayAdapter<String> = ArrayAdapter(
requireContext(),
android.R.layout.simple_spinner_dropdown_item,
@@ -143,10 +143,10 @@ class KeyboardFragment : SettingsMainActivity.SettingsFragment() {
subtypeManager.imeConfig.characterLayouts.keys.toList().indexOf(subtype.layout)
)
AlertDialog.Builder(context).apply {
setTitle(R.string.settings__keyboard__subtype_edit_title)
setTitle(R.string.settings__localization__subtype_edit_title)
setCancelable(true)
setView(dialogView.root)
setPositiveButton(R.string.settings__keyboard__subtype_apply) { _, _ ->
setPositiveButton(R.string.settings__localization__subtype_apply) { _, _ ->
val languageCode = subtypeManager.imeConfig.defaultSubtypesLanguageCodes[dialogView.languageSpinner.selectedItemPosition]
val layoutName = subtypeManager.imeConfig.characterLayouts.keys.toList()[dialogView.layoutSpinner.selectedItemPosition]
subtype.locale = LocaleUtils.stringToLocale(languageCode)
@@ -154,11 +154,11 @@ class KeyboardFragment : SettingsMainActivity.SettingsFragment() {
subtypeManager.modifySubtypeWithSameId(subtype)
updateSubtypeListView()
}
setNeutralButton(R.string.settings__keyboard__subtype_delete) { _, _ ->
setNeutralButton(R.string.settings__localization__subtype_delete) { _, _ ->
subtypeManager.removeSubtype(subtype)
updateSubtypeListView()
}
setNegativeButton(R.string.settings__keyboard__subtype_cancel) { _, _ -> }
setNegativeButton(R.string.settings__localization__subtype_cancel) { _, _ -> }
setOnDismissListener { activeDialogWindow = null }
create()
activeDialogWindow = show()
@@ -174,9 +174,9 @@ class KeyboardFragment : SettingsMainActivity.SettingsFragment() {
binding.subtypeNotConfWarning.visibility = View.GONE
for (subtype in subtypes) {
val itemView =
SettingsFragmentKeyboardSubtypeListItemBinding.inflate(layoutInflater)
ListItemBinding.inflate(layoutInflater)
itemView.title.text = subtype.locale.displayName
itemView.caption.text = subtypeManager.imeConfig.characterLayouts[subtype.layout]
itemView.summary.text = subtypeManager.imeConfig.characterLayouts[subtype.layout]
itemView.root.setOnClickListener { showEditSubtypeDialog(subtype.id) }
binding.subtypeListView.addView(itemView.root)
}

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

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape>
<solid android:color="?semiTransparentColor"/>
<stroke android:width="0.5dp" android:color="?semiTransparentColor"/>
</shape>
</item>
<item android:state_focused="true">
<shape>
<solid android:color="?semiTransparentColor"/>
<stroke android:width="0.5dp" android:color="?semiTransparentColor"/>
</shape>
</item>
<item>
<shape>
<solid android:color="@android:color/transparent"/>
<stroke android:width="0.5dp" android:color="?semiTransparentColor"/>
</shape>
</item>
</selector>

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="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
</vector>

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="M9.64,7.64c0.23,-0.5 0.36,-1.05 0.36,-1.64 0,-2.21 -1.79,-4 -4,-4S2,3.79 2,6s1.79,4 4,4c0.59,0 1.14,-0.13 1.64,-0.36L10,12l-2.36,2.36C7.14,14.13 6.59,14 6,14c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4c0,-0.59 -0.13,-1.14 -0.36,-1.64L12,14l7,7h3v-1L9.64,7.64zM6,8c-1.1,0 -2,-0.89 -2,-2s0.9,-2 2,-2 2,0.89 2,2 -0.9,2 -2,2zM6,20c-1.1,0 -2,-0.89 -2,-2s0.9,-2 2,-2 2,0.89 2,2 -0.9,2 -2,2zM12,12.5c-0.28,0 -0.5,-0.22 -0.5,-0.5s0.22,-0.5 0.5,-0.5 0.5,0.22 0.5,0.5 -0.22,0.5 -0.5,0.5zM19,3l-6,6 2,2 7,-7L22,3z"/>
</vector>

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="M19,2h-4.18C14.4,0.84 13.3,0 12,0c-1.3,0 -2.4,0.84 -2.82,2L5,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,4c0,-1.1 -0.9,-2 -2,-2zM12,2c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM19,20L5,20L5,4h2v3h10L17,4h2v16z"/>
</vector>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:left="8dp"
android:right="8dp"
android:drawable="@drawable/ic_content_paste"/>
</layer-list>

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="M18.41,16.59L13.82,12l4.59,-4.59L17,6l-6,6 6,6zM6,6h2v12H6z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M10,4v3h2.21l-3.42,8H6v3h8v-3h-2.21l3.42,-8H18V4z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M5.59,7.41L10.18,12l-4.59,4.59L7,18l6,-6 -6,-6zM16,6h2v12h-2z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M3,5h2L5,3c-1.1,0 -2,0.9 -2,2zM3,13h2v-2L3,11v2zM7,21h2v-2L7,19v2zM3,9h2L5,7L3,7v2zM13,3h-2v2h2L13,3zM19,3v2h2c0,-1.1 -0.9,-2 -2,-2zM5,21v-2L3,19c0,1.1 0.9,2 2,2zM3,17h2v-2L3,15v2zM9,3L7,3v2h2L9,3zM11,21h2v-2h-2v2zM19,13h2v-2h-2v2zM19,21c1.1,0 2,-0.9 2,-2h-2v2zM19,9h2L21,7h-2v2zM19,17h2v-2h-2v2zM15,21h2v-2h-2v2zM15,5h2L17,3h-2v2zM7,17h10L17,7L7,7v10zM9,9h6v6L9,15L9,9z"/>
</vector>

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,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/black" />
<corners android:radius="@dimen/smartbar_radius" />
</shape>

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>

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