Compare commits

...

127 Commits

Author SHA1 Message Date
Patrick Goldinger
fe6930fb76 Release v0.3.2 2020-12-27 21:48:48 +01:00
Patrick Goldinger
6a10f0a01a Swap underscore and percentage sign in symbols layout (#101) 2020-12-27 21:39:51 +01:00
Patrick Goldinger
30717eeb90 Merge pull request #115 from yashx/cleanUp
InputView Code Cleanup
2020-12-27 21:26:09 +01:00
Patrick Goldinger
a664ab18c9 Merge pull request #114 from florisboard/feat-toggle-ext-popup-priority
Add hint priority mode setting
2020-12-27 21:00:11 +01:00
yashx
f50983d7ab InputView Code Cleanup 2020-12-28 01:02:56 +05:30
Patrick Goldinger
be858802c5 Fix old hint strings not removed from translated files 2020-12-27 20:03:29 +01:00
Patrick Goldinger
1ba690e53a Add hint priority mode setting (#39) 2020-12-27 19:44:24 +01:00
Patrick Goldinger
e16f81d350 Merge pull request #111 from yashx/timber
Switch to Timber for Logging
2020-12-27 18:27:10 +01:00
Patrick Goldinger
0de2039d72 Merge pull request #110 from florisboard/feat-private-mode
Add private mode (aka incognito mode) base
2020-12-27 16:23:02 +01:00
Patrick Goldinger
50b6a63468 Add private mode theme attributes 2020-12-27 16:16:53 +01:00
yashx
8cb644b418 Switch to Timber for Logging 2020-12-27 11:33:09 +05:30
Patrick Goldinger
f138124670 Add private mode (aka incognito mode) base (#106) 2020-12-26 23:24:27 +01:00
Patrick Goldinger
0f76d7f9df Merge pull request #107 from yashx/undoRedo
Added Undo Redo Buttons to Quick Actions in Smart Bar
2020-12-26 21:20:35 +01:00
Patrick Goldinger
27b9ec4628 Merge pull request #108 from florisboard/fix-typing-ux
Improve input UX and performance
2020-12-26 21:13:51 +01:00
Patrick Goldinger
ac733ed1dc Further improve input UX 2020-12-26 21:09:11 +01:00
yashx
6d15708f95 Switch to emulating hardware key press to paste 2020-12-26 22:03:54 +05:30
Patrick Goldinger
4377f3e41c Improve input performance by avoiding object allocation 2020-12-26 16:26:58 +01:00
Patrick Goldinger
1e690018d7 Merge pull request #100 from yashx/hardwareDelete
Switch to emulating hardware key press to delete
2020-12-26 11:37:32 +01:00
yashx
93bb5d2714 Added Undo Redo Buttons to Quick Actions in Smart Bar 2020-12-26 15:23:38 +05:30
Patrick Goldinger
ad2b08a342 Merge pull request #102 from The-Quantum-Alpha/patch-1
Create canadian_french.json
2020-12-26 03:55:21 +01:00
The Quantum Alpha
9e6508cee4 yeah, whatever with the config.json
🇨🇦fr
2020-12-25 21:49:12 -05:00
The Quantum Alpha
f735c138fb Create canadian_french.json
qwerty, but with éàè
2020-12-25 20:56:49 -05:00
yashx
d663947fec Switch to emulating hardware key press to delete 2020-12-26 01:31:22 +05:30
Patrick Goldinger
c800617e26 Merge pull request #99 from yashx/arrowsFix
Fix left and right arrow in clipboard cursor row
2020-12-25 20:57:54 +01:00
yashx
f47c7abaf3 Fix left and right arrow in clipboard cursor row 2020-12-26 00:27:40 +05:30
Patrick Goldinger
faf06ee234 Merge pull request #97 from yashx/deleteGesture
Delete Key gesture Improvents
2020-12-25 19:39:33 +01:00
Patrick Goldinger
07c41f9c27 Merge pull request #98 from florisboard/fix-key-delete-crash-on-hold
Fix key delete crash on holding down
2020-12-25 19:34:10 +01:00
Patrick Goldinger
80a0d9edab Fix scheduled timer crash in media and editing as well 2020-12-25 19:20:12 +01:00
Patrick Goldinger
cd943a9d4a Fix key delete crash on holding down
Fix key delete crash on holding down 2
2020-12-25 19:12:06 +01:00
yashx
c3d3107b12 Added Delete Words Precisely 2020-12-25 20:34:36 +05:30
yashx
b91fac8e76 Fix Delete current word 2020-12-25 18:11:30 +05:30
Patrick Goldinger
e2c784f4cf Merge pull request #92 from Surendrajat/ci 2020-12-24 16:07:57 +01:00
Surendrajat
f83bdd8a28 Enable automatic build CI workflows
fix executable permission

add badge in README too

upload artifact

fix name
2020-12-24 15:55:23 +01:00
Patrick Goldinger
dc10a459ca Release v0.3.1 2020-12-23 00:42:22 +01:00
Patrick Goldinger
4bea68f151 Update translations from Crowdin 2020-12-23 00:25:21 +01:00
Patrick Goldinger
daa8ce71ac Remove unused legacy subtype attributes
isAsciiCapable and isEmojiCapable have no real use in FlorisBoard,
and as the Android InputMethodSubtype class will never be used,
there's no reason to keep these in. Removing them lets the config
look more clean.
2020-12-22 20:51:51 +01:00
Patrick Goldinger
f06f475e89 Merge pull request #90 from jeremiah-miller/esperanto_layout
Added Esperanto keyboard layout
2020-12-22 20:21:27 +01:00
Jeremiah Miller
b784d0805c Merge branch 'master' into esperanto_layout 2020-12-22 11:39:47 -07:00
bbgun7
c245c6a37c Added popups to en.json so that all english characters can be accessed from the esperanto layout 2020-12-22 11:38:33 -07:00
bbgun7
264a287171 Fixed popups for esperanto (eo) layout, and added eo layout variant 2020-12-22 11:37:29 -07:00
Patrick Goldinger
82d82466c6 Add Dvorak keyboard layout (#72) 2020-12-21 23:30:32 +01:00
Patrick Goldinger
0242d24cd1 Add Colemak keyboard layout (#72) 2020-12-21 22:05:32 +01:00
Patrick Goldinger
76e683bfec Fix event listener NullPointerException (#73, #81) 2020-12-21 20:02:28 +01:00
Patrick Goldinger
ee1988d98e Merge pull request #91 from florisboard/feat-smartbar-rework
Smartbar rework (Milestone v0.4.0 / Module A)
2020-12-21 18:55:28 +01:00
Patrick Goldinger
fe5f0d18ac Update README.md feature roadmap 2020-12-21 18:50:09 +01:00
Patrick Goldinger
41527e4f23 Reimplement clipboard suggestions 2020-12-21 18:02:10 +01:00
Patrick Goldinger
66fb1c5873 Improve Smartbar display logic
- Smartbar now doesn't show in number, phone and phone2 layouts.
- Remove "show instead" preference as it does not do anything anymore.
- Change one-handed icon to a smartphone, which should improve clarity.
2020-12-21 00:25:54 +01:00
Patrick Goldinger
05103214dd Add debug specific build.gradle settings
- This allows to have both a debug and release version of FlorisBoard
  on a single device.
2020-12-20 21:58:34 +01:00
Patrick Goldinger
bf9e2e4438 Add number row as character layout extension
- Number row is now not part of the Smartbar anymore, but is an
  extension of the character layout, meaning that it is possible to
  show both a number row and the Smartbar.
- The Smartbar can now be disabled in the preferences.
- Adjust height calculation when number row is shown.
- Fix Smartbar not applying calculated height correctly.
2020-12-20 19:58:23 +01:00
Patrick Goldinger
4209bdcfbe Fix syntax error in Hungarian extended popup list 2020-12-20 19:55:12 +01:00
bbgun7
31db482bb4 Added extended popups for esperanto layout 2020-12-19 21:06:44 -07:00
bbgun7
e33499dab5 Added Esperanto keyboard layout 2020-12-19 13:50:30 -07:00
Patrick Goldinger
92b99ff34e Rework Smartbar code base and layout XML
- The Smartbar XML layout has been completely changed and is now
  pretty solid.
- SmartbarManager's tasks have been split up: UI related things
  and the management of the state are now managed within the
  SmartbarView, setting the values and listening to events is now done within TextInputManager. Removing SmartbarManager was an important
  step because the code and logic was just a pure mess.
- SmartbarView is now responsible to manage the state, show and hide
  features based on various parameters from the keyboard core.
2020-12-17 23:09:09 +01:00
Patrick Goldinger
f991c6479b Add feature roadmap to README.md 2020-12-13 23:58:51 +01:00
Patrick Goldinger
5a45b1600a Merge pull request #75 from zoli111/master
Add Hungarian layout
2020-12-13 23:17:39 +01:00
zoli111
79f884b2a0 Fix Hungarian layout 2020-12-10 18:46:59 +01:00
zoli111
22330ad67b Add Hungarian layout 2020-12-08 22:32:34 +01:00
Patrick Goldinger
7f50a5aa77 Update CONTRIBUTING.md
Remove "!" preceding Crowdin link as it was treated as image.
2020-12-08 02:06:06 +01:00
Patrick Goldinger
de389918be Release v0.3.0 2020-12-06 23:48:59 +01:00
Patrick Goldinger
4a57829105 Update translations from Crowdin 2020-12-06 23:29:30 +01:00
Patrick Goldinger
bc6ca8c7fc Improve precise character delete swipe (#25)
- Lowered distance threshold for move swipes
- Fix delete swipe not recognized when only one character was selected
2020-12-05 20:41:36 +01:00
Patrick Goldinger
0ffe0c915e Fix symbol hint not accounting for missing shift (#68)
- The symbols are now correctly taken from the symbol layout, without
  the switch to symbol2 and delete key.
2020-12-04 18:56:38 +01:00
Patrick Goldinger
392699f333 Fix keyboard UI not displaying correctly for rtl languages (#69) 2020-12-04 18:38:23 +01:00
Patrick Goldinger
cf801c02fd Merge pull request #66 from HeiWiper/master
Added an Arabic keyboard and mod, and changed persian ID to 801
2020-12-04 18:04:33 +01:00
Patrick Goldinger
665356f77b Major improvements in auto sizing(#48, #50, #61)
- Keyboard height can - besides of the preset values - be set between
  50% and 150%
- Key font size range has been extended to 50%-150%
- Key font size multiplier now affects the popup as well
- Key popup size scales with the keyboard height value
- Fix key size algorithm not working on xxhdpi screens
- Improve key popup manager backend
2020-12-03 23:43:18 +01:00
Hei Wiper
48c356a569 Added an Arabic keyboard and mod, and changed persian ID to 801 2020-12-03 23:11:52 +01:00
Patrick Goldinger
60eb92e92a Fix bottom offset not applying correctly (#58) 2020-12-02 19:57:19 +01:00
Patrick Goldinger
602ffc2a93 Add option to adjust font size multiplier (#48)
- Also improve default key font size calculation parameters.
2020-12-02 18:27:59 +01:00
Patrick Goldinger
dbacc0e466 Fix release badge in README.md not pointing to releases 2020-12-01 20:53:05 +01:00
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
162 changed files with 8963 additions and 1922 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,11 +1,19 @@
---
name: Feature request
about: Suggest an idea or enhancement for this project
name: Feature request / Suggestion
about: Suggest an idea for this project
title: ''
labels: proposal
assignees: ''
---
Describe your idea in a short but concise way. If you have multiple ideas which are not directly connected to each other, file an issue per idea. This makes it easy to implement one feature proposal at a time. If you have any examples, e.g. screenshots or other keyboards which have the proposed feature implemented, link them here.
Thank you for your help in making FlorisBoard better!
<!--
- Describe your idea in a short but concise way.
- If you have multiple ideas which are not directly connected to each
other, file an issue per idea. This makes it easy to implement one
feature proposal at a time.
- If you have any examples, e.g. screenshots or other keyboards which
have the proposed feature implemented, link them here.
- Please search existing proposals to avoid creating duplicates.
- Thank you for your help in making FlorisBoard better!
-->

16
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@@ -0,0 +1,16 @@
---
name: Question
about: Ask here if you have a question about FlorisBoard
title: ''
labels: question
assignees: ''
---
<!--
- If you need assistance in using FlorisBoard, ask it here!
- If you want to suggest an idea for this project, please use the
Feature request template instead.
- Please search existing questions to avoid creating duplicates.
- Thank you for your help in making FlorisBoard better!
-->

32
.github/workflows/android.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: FlorisBoard CI
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Build with Gradle
run: ./gradlew clean assemble
- uses: actions/upload-artifact@v2
with:
name: app-debug.apk
path: app/build/outputs/apk/debug/app-debug.apk

View File

@@ -2,62 +2,42 @@
First off, thanks for considering contributing to FlorisBoard!
There are several ways to contribute to FlorisBoard. This document provides some
general guidelines for each type of contribution.
There are several ways to contribute to FlorisBoard. This document
provides some general guidelines for each type of contribution.
## Giving general feedback
Either use the review function within Google Play or email me at
[florisboard@patrickgold.dev](mailto:florisboard@patrickgold.dev). I
love to hear from you!
## Translations
To make FlorisBoard accessible in as many languages as possible, the
platform [Crowdin](https://crowdin.florisboard.patrickgold.dev) is used
to crowdsource and manage translations. This is the only source of
translations from now on - **PRs that add/update translations are no
longer accepted.** The list of languages in Crowdin covers the top 20
languages, but feel free to email me at
[florisboard@patrickgold.dev](mailto:florisboard@patrickgold.dev) to
request a language and I'll add it.
## Adding a new feature or making large changes
If you intend to add a new feature or to make large changes, please discuss this
first through a proposal on GitHub. Discussing your idea enables both you and the
dev team that we are on the same page before you start on working on your change.
If you have any questions, feel free to ask for help at any time!
If you intend to add a new feature or to make large changes, please
discuss this first through a proposal on GitHub. Discussing your idea
enables both you and the dev team that we are on the same page before
you start on working on your change. If you have any questions, feel
free to ask for help at any time!
## Adding a new keyboard layout / dictionary for locale
As FlorisBoard is currently in alpha stage, things might change drastically. This
also includes the config scheme of keyboard layouts. To prevent incompatible
configs because some features and structures may change, please do not add this
kind of content yet. As FlorisBoard's state progresses and its core stabilizes,
you will be able to add keyboard layouts.
## Translating FlorisBoard
Before starting to translate, when adding a new translation please file
an issue stating that you want to translate FlorisBoard into a language.
Once this gets approved you can start translating. When updating an
already existing translation file you can just send a PR directly.
If you are not familiar with PRs, check out this guide:
[https://www.gun.io/blog/how-to-github-fork-branch-and-pull-request](https://www.gun.io/blog/how-to-github-fork-branch-and-pull-request)
Notes for tips below:
- Replace `<language>` with the language you want to add
- Replace `<code>` with the ISO 639-1 code of the language you want to
add
([List of codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes))
### Tips when adding a new translation
- To add the new translation file, navigate to `app/src/main/res/values`
and copy the file `strings.xml` into the folder
`app/src/main/res/values-<code>` (you have to create this folder)
- Translate only the phrases inside the brackets, leave the name
attribute as it is
E.g.: `<string name="hello_string">Hello World!</string>`
`<string name="hello_string">Ciao mondo!</string>`
- When finished translating, commit your changes locally, as the commit
message use `Add <language> translation`
- Push your change(s) and create the PR. When everything checks out, it
will get accepted.
### Tips when updating a translation
- To update a translation, check the `strings.xml` in
`app/src/main/res/values` for newly added strings and add them to the
translation file in `app/src/main/res/values-<code>`
- When finished translating, commit your changes locally, as the commit
message use `Update <language> translation`
- Push your change(s) and create the PR. When everything checks out, it
will get accepted.
As FlorisBoard is currently in alpha stage, things might change
drastically. This also includes the config scheme of keyboard layouts.
To prevent incompatible configs because some features and structures may
change, please do not add this kind of content yet. As FlorisBoard's
state progresses and its core stabilizes, you will be able to add
keyboard layouts.
## Bug reporting
@@ -68,6 +48,11 @@ use the premade [issue template](.github/ISSUE_TEMPLATE/bug_report.md)
for bug reporting. This makes it easy for us to understand what the bug
is and how to solve it.
### Capturing ADB debug logs
### Capturing error logs
[[ TODO: create tutorial ]]
Logs are captured by FlorisBoard's crash handler, which gives you the
ability to copy it to the clipboard and paste it in GitHub. This is the
preferred way to capture logs.
Alternatively, you can also use ADB (Android Debug Bridge) to capture
the error log. This is recommended for experienced users only.

145
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)](https://github.com/florisboard/florisboard/releases) [![Crowdin](https://badges.crowdin.net/florisboard/localized.svg)](https://crowdin.florisboard.patrickgold.dev) ![FlorisBoard CI](https://github.com/florisboard/florisboard/workflows/FlorisBoard%20CI/badge.svg?event=push)
#### 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,84 +31,132 @@ 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.
---
![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
## Implemented features
This list contains all implemented and fully functional features
FlorisBoard currently has to offer. For planned features and its
milestones, please refer to the [Feature roadmap](#feature-roadmap).
### Basics
* [x] Implementation of the keyboard core (InputMethodService)
* [x] Own implementation of deprecated KeyboardView (base only)
* [x] Custom implementation of deprecated KeyboardView (base only)
* [x] Caps + Caps Lock
* [x] Key popups
* [x] Extended key popups (e.g. a -> á, à, ä, ...) (needs tweaks for
emojis)
* [x] Extended key popups (e.g. a -> á, à, ä, ...)
* [x] Key press sound/vibration
* [x] Portrait orientation support
* [x] Landscape orientation support (needs tweaks)
* [ ] Tablet screen support
### Layouts
* [x] Latin character layouts (QWERTY, QWERTZ, AZERTY, Swiss, Spanish,
Norwegian, Swedish/Finnish, Icelandic, Danish)
* [x] Non-latin character layouts (Persian)
Norwegian, Swedish/Finnish, Icelandic, Danish, Hungarian); more
coming in future versions
* [x] Non-latin character layouts (Arabic, Persian)
* [x] Adapt to situation in app (password, url, text, etc. )
* [x] Special character layout(s)
* [x] Numeric layout
* [x] Numeric layout (advanced)
* [x] Phone number layout
* [x] Emoji layout (popups buggy atm)
* [x] Emoji layout
* [x] Emoticon layout
* [ ] Kaomoji layout
### Preferences
* [x] Setup wizard
* [x] Preferences screen
* [x] Customize look and behaviour of keyboard (currently only
light/dark theme)
* [x] Customize look and behaviour of keyboard
* [x] Theme presets (currently only day/night theme)
* [x] Theme customization
* [ ] Theme import/export (?)
* [x] Subtype selection (language/layout)
* [x] Keyboard behaviour preferences
* [ ] Text suggestion / Auto correct preferences
* [ ] Gesture preferences
### Composing suggestions
* [ ] Auto suggest words from precompiled dictionary
* [ ] Auto suggest words from user dictionary
* [ ] Auto suggest contacts
* [ ] Multilingual typing
* [x] Gesture preferences
### Other useful features
* [x] One-handed mode
* [x] Clipboard/cursor tools
* [ ] Floating keyboard
* [ ] Gesture support
* [ ] Glide typing (?)
* [x] Integrated number row / symbols in character layouts
* [x] Gesture support
* [x] Full integration in IME service list of Android (xml/method)
(integration is internal-only, because Android's default subtype
implementation not really allows for dynamic language/layout
pairs, only compile-time defined ones)
* [ ] Description and settings reference in System Language & Input
* [ ] (dev only) Generate well-structured documentation of code
* [ ] ...
Note: (?) = not sure if it will be implemented
## Feature roadmap
This section describes the features which are planned to be implemented
in FlorisBoard for the next major versions, modularized into sections.
Please note that the milestone due dates are only raw estimates and will
most likely be delayed back, even though I'm eager to stick to these as
close as possible.
### [v0.4.0](https://github.com/florisboard/florisboard/milestone/4)
- Module A: Smartbar rework (Implemented with #91)
- Ability to enable/disable Smartbar (features below thus only work if
Smartbar is enabled)
- Dynamic switching between clipboard tools and word suggestions
- Ability to show both the number row and word suggestions at once
- Better icons in quick actions
- Complete rework of the Smartbar code base and the Smartbar layout
definition in XML
- Module B: Composing suggestions
- Auto-suggestion of words based of precompiled dictionaries
- Management of custom dictionary entries
- Opt-in only: Learning of often typed word pais to better predict next
words over time. Data collected here is stored locally and never leaves
the user's device.
- Module C: Extension packs
- Ability to load dictionaries (and later potentionally other cool
features too) only if needed to keep the core APK size small
- Currently unclear how exactly this will work, but this is definitely
a must-have feature
- Module D: Glide typing
- Swiping over the characters will automatically convert this to a word
- Possibly also add improvements based on the Flow keyboard
### [v0.5.0](https://github.com/florisboard/florisboard/milestone/5)
There's no exact roadmap yet but it is planned that the media part of
FlorisBoard (emojis, emoticons, kaomoji) gets a rework. Also as an extension
(requires v0.4.0/Module C) GIF support is planned.
### > v0.5.0
This is completely open as of now and will gather planned features as time
passes...
Backlog (currently not assigned to any milestone):
- Theme import/export
- Floating keyboard
## Contributing
Wanna contribute to FlorisBoard? That's great to hear! There are lots of
different ways to help out. Bug reporting, making pull requests,
translating FlorisBoard to make it more accessible, etc. For more
information see the ![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)

View File

@@ -10,8 +10,8 @@ android {
applicationId "dev.patrickgold.florisboard"
minSdkVersion 23
targetSdkVersion 29
versionCode 12
versionName "0.2.0"
versionCode 21
versionName "0.3.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -21,9 +21,14 @@ android {
}
buildTypes {
debug {
applicationIdSuffix ".debug"
resValue "string", "app_name", "FlorisBoard Debug"
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
resValue "string", "app_name", "FlorisBoard"
}
}
}
@@ -33,19 +38,20 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.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.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7"
implementation 'com.jaredrummler:colorpicker:1.1.0'
implementation 'com.jakewharton.timber:timber:4.7.1'
}

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"
@@ -90,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

@@ -12,176 +12,150 @@
"swiss_german": "Swiss German (QWERTZ)",
"swiss_french": "Swiss French (QWERTZ)",
"swiss_italian": "Swiss Italian (QWERTZ)",
"persian": "Persian"
"hungarian": "Hungarian (QWERTZ)",
"persian": "Persian",
"arabic": "Arabic",
"esperanto": "Esperanto",
"esperanto_with_hx": "Esperanto with 'ĥ'",
"colemak": "Colemak",
"dvorak": "Dvorak",
"canadian_french": "Canadian French (QWERTY)"
},
"defaultSubtypes": [
{
"id": 101,
"languageTag": "en-US",
"preferredLayout": "qwerty",
"isAsciiCapable": true,
"isEmojiCapable": true
"preferredLayout": "qwerty"
},
{
"id": 102,
"languageTag": "en-UK",
"preferredLayout": "qwerty",
"isAsciiCapable": true,
"isEmojiCapable": true
"preferredLayout": "qwerty"
},
{
"id": 103,
"languageTag": "en-CA",
"preferredLayout": "qwerty",
"isAsciiCapable": true,
"isEmojiCapable": true
"preferredLayout": "qwerty"
},
{
"id": 104,
"languageTag": "en-AU",
"preferredLayout": "qwerty",
"isAsciiCapable": true,
"isEmojiCapable": true
"preferredLayout": "qwerty"
},
{
"id": 201,
"languageTag": "de-DE",
"preferredLayout": "qwertz",
"isAsciiCapable": true,
"isEmojiCapable": true
"preferredLayout": "qwertz"
},
{
"id": 202,
"languageTag": "de-AT",
"preferredLayout": "qwertz",
"isAsciiCapable": true,
"isEmojiCapable": true
"preferredLayout": "qwertz"
},
{
"id": 203,
"languageTag": "de-CH",
"preferredLayout": "swiss_german",
"isAsciiCapable": true,
"isEmojiCapable": true
"preferredLayout": "swiss_german"
},
{
"id": 301,
"languageTag": "fr-FR",
"preferredLayout": "azerty",
"isAsciiCapable": true,
"isEmojiCapable": true
"preferredLayout": "azerty"
},
{
"id": 302,
"languageTag": "fr-CA",
"preferredLayout": "qwerty",
"isAsciiCapable": true,
"isEmojiCapable": true
"preferredLayout": "canadian_french"
},
{
"id": 303,
"languageTag": "fr-CH",
"preferredLayout": "swiss_french",
"isAsciiCapable": true,
"isEmojiCapable": true
"preferredLayout": "swiss_french"
},
{
"id": 401,
"languageTag": "it-IT",
"preferredLayout": "qwerty",
"isAsciiCapable": true,
"isEmojiCapable": true
"preferredLayout": "qwerty"
},
{
"id": 402,
"languageTag": "it-CH",
"preferredLayout": "swiss_italian",
"isAsciiCapable": true,
"isEmojiCapable": true
"preferredLayout": "swiss_italian"
},
{
"id": 501,
"languageTag": "es-ES",
"preferredLayout": "spanish",
"isAsciiCapable": true,
"isEmojiCapable": true
"preferredLayout": "spanish"
},
{
"id": 502,
"languageTag": "es-US",
"preferredLayout": "spanish",
"isAsciiCapable": true,
"isEmojiCapable": true
"preferredLayout": "spanish"
},
{
"id": 503,
"languageTag": "es-419",
"preferredLayout": "spanish",
"isAsciiCapable": true,
"isEmojiCapable": true
"preferredLayout": "spanish"
},
{
"id": 601,
"languageTag": "pt-PT",
"preferredLayout": "qwerty",
"isAsciiCapable": true,
"isEmojiCapable": true
"preferredLayout": "qwerty"
},
{
"id": 602,
"languageTag": "pt-BR",
"preferredLayout": "qwerty",
"isAsciiCapable": true,
"isEmojiCapable": true
"preferredLayout": "qwerty"
},
{
"id": 701,
"languageTag": "nb-NO",
"preferredLayout": "norwegian",
"isAsciiCapable": true,
"isEmojiCapable": true
"preferredLayout": "norwegian"
},
{
"id": 702,
"languageTag": "nn-NO",
"preferredLayout": "norwegian",
"isAsciiCapable": true,
"isEmojiCapable": true
"preferredLayout": "norwegian"
},
{
"id": 711,
"languageTag": "sv-SE",
"preferredLayout": "swedish_finnish",
"isAsciiCapable": true,
"isEmojiCapable": true
"preferredLayout": "swedish_finnish"
},
{
"id": 721,
"languageTag": "fi-FI",
"preferredLayout": "swedish_finnish",
"isAsciiCapable": true,
"isEmojiCapable": true
"preferredLayout": "swedish_finnish"
},
{
"id": 731,
"languageTag": "da-DK",
"preferredLayout": "danish",
"isAsciiCapable": true,
"isEmojiCapable": true
"preferredLayout": "danish"
},
{
"id": 741,
"languageTag": "is-IS",
"preferredLayout": "icelandic",
"isAsciiCapable": true,
"isEmojiCapable": true
"preferredLayout": "icelandic"
},
{
"id": 800,
"id": 801,
"languageTag": "fa-FA",
"preferredLayout": "persian",
"isAsciiCapable": true,
"isEmojiCapable": true
"preferredLayout": "persian"
},
{
"id": 901,
"languageTag": "ar",
"preferredLayout": "arabic"
},
{
"id": 1001,
"languageTag": "hu",
"preferredLayout": "hungarian"
},
{
"id": 1101,
"languageTag": "eo",
"preferredLayout": "esperanto"
}
]
}

View File

@@ -0,0 +1,46 @@
{
"type": "characters",
"name": "arabic",
"direction": "rtl",
"modifier": "arabic",
"arrangement": [
[
{ "code": 1590, "label": "ض" },
{ "code": 1589, "label": "ص" },
{ "code": 1579, "label": "ث" },
{ "code": 1602, "label": "ق" },
{ "code": 1601, "label": "ف" },
{ "code": 1594, "label": "غ" },
{ "code": 1593, "label": "ع" },
{ "code": 1607, "label": "ه" },
{ "code": 1582, "label": "خ" },
{ "code": 1581, "label": "ح" },
{ "code": 1580, "label": "ج" }
],
[
{ "code": 1588, "label": "ش" },
{ "code": 1587, "label": "س" },
{ "code": 1610, "label": "ي" },
{ "code": 1576, "label": "ب" },
{ "code": 1604, "label": "ل" },
{ "code": 1575, "label": "ا" },
{ "code": 1578, "label": "ت" },
{ "code": 1606, "label": "ن" },
{ "code": 1605, "label": "م" },
{ "code": 1603, "label": "ك" },
{ "code": 1591, "label": "ط" }
],
[
{ "code": 1584, "label": "ذ" },
{ "code": 1569, "label": "ء" },
{ "code": 65157, "label": "ﺅ" },
{ "code": 1585, "label": "ر" },
{ "code": 1609, "label": "ى" },
{ "code": 1577, "label": "ة" },
{ "code": 1608, "label": "و" },
{ "code": 1586, "label": "ز" },
{ "code": 1592, "label": "ظ" },
{ "code": 1583, "label": "د" }
]
]
}

View File

@@ -0,0 +1,40 @@
{
"type": "characters",
"name": "canadian_french",
"direction": "ltr",
"arrangement": [
[
{ "code": 113, "label": "q" },
{ "code": 119, "label": "w" },
{ "code": 101, "label": "e" },
{ "code": 114, "label": "r" },
{ "code": 116, "label": "t" },
{ "code": 121, "label": "y" },
{ "code": 117, "label": "u" },
{ "code": 105, "label": "i" },
{ "code": 111, "label": "o" },
{ "code": 112, "label": "p" },
{ "code": 232, "label": "è" }
], [
{ "code": 97, "label": "a" },
{ "code": 115, "label": "s" },
{ "code": 100, "label": "d" },
{ "code": 102, "label": "f" },
{ "code": 103, "label": "g" },
{ "code": 104, "label": "h" },
{ "code": 106, "label": "j" },
{ "code": 107, "label": "k" },
{ "code": 108, "label": "l" },
{ "code": 233, "label": "é" },
{ "code": 224, "label": "à" }
], [
{ "code": 122, "label": "z" },
{ "code": 120, "label": "x" },
{ "code": 99, "label": "c" },
{ "code": 118, "label": "v" },
{ "code": 98, "label": "b" },
{ "code": 110, "label": "n" },
{ "code": 109, "label": "m" }
]
]
}

View File

@@ -0,0 +1,40 @@
{
"type": "characters",
"name": "colemak",
"direction": "ltr",
"arrangement": [
[
{ "code": 113, "label": "q" },
{ "code": 119, "label": "w" },
{ "code": 102, "label": "f" },
{ "code": 112, "label": "p" },
{ "code": 103, "label": "g" },
{ "code": 106, "label": "j" },
{ "code": 108, "label": "l" },
{ "code": 117, "label": "u" },
{ "code": 121, "label": "y" },
{ "code": 59, "label": ";", "popup": [
{ "code": 58, "label": ":" }
] }
], [
{ "code": 97, "label": "a" },
{ "code": 114, "label": "r" },
{ "code": 115, "label": "s" },
{ "code": 116, "label": "t" },
{ "code": 100, "label": "d" },
{ "code": 104, "label": "h" },
{ "code": 110, "label": "n" },
{ "code": 101, "label": "e" },
{ "code": 105, "label": "i" },
{ "code": 111, "label": "o" }
], [
{ "code": 122, "label": "z" },
{ "code": 120, "label": "x" },
{ "code": 99, "label": "c" },
{ "code": 118, "label": "v" },
{ "code": 98, "label": "b" },
{ "code": 107, "label": "k" },
{ "code": 109, "label": "m" }
]
]
}

View File

@@ -0,0 +1,49 @@
{
"type": "characters",
"name": "dvorak",
"direction": "ltr",
"modifier": "dvorak",
"arrangement": [
[
{ "code": 64, "label": "@", "variation": "email_address" },
{ "code": 39, "label": "'", "variation": "normal", "popup": [
{ "code": 33, "label": "!" },
{ "code": 34, "label": "\"" }
] },
{ "code": 47, "label": "/", "variation": "uri" },
{ "code": 44, "label": ",", "popup": [
{ "code": 60, "label": "<" },
{ "code": 63, "label": "?" }
] },
{ "code": 46, "label": ".", "popup": [
{ "code": 62, "label": ">" }
] },
{ "code": 112, "label": "p" },
{ "code": 121, "label": "y" },
{ "code": 102, "label": "f" },
{ "code": 103, "label": "g" },
{ "code": 99, "label": "c" },
{ "code": 114, "label": "r" },
{ "code": 108, "label": "l" }
], [
{ "code": 97, "label": "a" },
{ "code": 111, "label": "o" },
{ "code": 101, "label": "e" },
{ "code": 117, "label": "u" },
{ "code": 105, "label": "i" },
{ "code": 100, "label": "d" },
{ "code": 104, "label": "h" },
{ "code": 116, "label": "t" },
{ "code": 110, "label": "n" },
{ "code": 115, "label": "s" }
], [
{ "code": 106, "label": "j" },
{ "code": 107, "label": "k" },
{ "code": 120, "label": "x" },
{ "code": 98, "label": "b" },
{ "code": 109, "label": "m" },
{ "code": 119, "label": "w" },
{ "code": 118, "label": "v" }
]
]
}

View File

@@ -0,0 +1,38 @@
{
"type": "characters",
"name": "esperanto",
"direction": "ltr",
"arrangement": [
[
{ "code": 349, "label": "ŝ" },
{ "code": 285, "label": "ĝ" },
{ "code": 101, "label": "e" },
{ "code": 114, "label": "r" },
{ "code": 116, "label": "t" },
{ "code": 365, "label": "ŭ" },
{ "code": 117, "label": "u" },
{ "code": 105, "label": "i" },
{ "code": 111, "label": "o" },
{ "code": 112, "label": "p" }
], [
{ "code": 97, "label": "a" },
{ "code": 115, "label": "s" },
{ "code": 100, "label": "d" },
{ "code": 102, "label": "f" },
{ "code": 103, "label": "g" },
{ "code": 104, "label": "h" },
{ "code": 106, "label": "j" },
{ "code": 107, "label": "k" },
{ "code": 108, "label": "l" },
{ "code": 309, "label": "ĵ" }
], [
{ "code": 122, "label": "z" },
{ "code": 265, "label": "ĉ" },
{ "code": 99, "label": "c" },
{ "code": 118, "label": "v" },
{ "code": 98, "label": "b" },
{ "code": 110, "label": "n" },
{ "code": 109, "label": "m" }
]
]
}

View File

@@ -0,0 +1,39 @@
{
"type": "characters",
"name": "esperanto_with_hx",
"direction": "ltr",
"arrangement": [
[
{ "code": 349, "label": "ŝ" },
{ "code": 285, "label": "ĝ" },
{ "code": 101, "label": "e" },
{ "code": 114, "label": "r" },
{ "code": 116, "label": "t" },
{ "code": 365, "label": "ŭ" },
{ "code": 117, "label": "u" },
{ "code": 105, "label": "i" },
{ "code": 111, "label": "o" },
{ "code": 112, "label": "p" }
], [
{ "code": 97, "label": "a" },
{ "code": 115, "label": "s" },
{ "code": 100, "label": "d" },
{ "code": 102, "label": "f" },
{ "code": 103, "label": "g" },
{ "code": 104, "label": "h" },
{ "code": 106, "label": "j" },
{ "code": 107, "label": "k" },
{ "code": 108, "label": "l" },
{ "code": 309, "label": "ĵ" }
], [
{ "code": 122, "label": "z" },
{ "code": 265, "label": "ĉ" },
{ "code": 99, "label": "c" },
{ "code": 118, "label": "v" },
{ "code": 98, "label": "b" },
{ "code": 110, "label": "n" },
{ "code": 109, "label": "m" },
{ "code": 293, "label": "ĥ" }
]
]
}

View File

@@ -0,0 +1,97 @@
{
"ض": [
{ "code": 1633, "label": "١" }
],
"ص": [
{ "code": 1634, "label": "٢" }
],
"ث": [
{ "code": 1635, "label": "٣" }
],
"ق": [
{ "code": 1704, "label": "ڨ"},
{ "code": 1636, "label": "٤" }
],
"ف": [
{ "code": 1701, "label": "ڥ" },
{ "code": 1700, "label": "ڤ" },
{ "code": 1698, "label": "ڢ" },
{ "code": 1637, "label": "٥" }
],
"غ": [
{ "code": 1638, "label": "٦" }
],
"ع": [
{ "code": 1639, "label": "٧" }
],
"ه": [
{ "code": 1726, "label": "ھ" },
{ "code": 1640, "label": "٨" }
],
"خ": [
{ "code": 1641, "label": "٩" }
],
"ح": [
{ "code": 1632, "label": "٠" }
],
"ج": [
{ "code": 1670, "label": "چ" }
],
"ش": [
{ "code": 1692, "label": "ڜ" }
],
"ي": [
{ "code": 1574, "label": "ئ" },
{ "code": 1609, "label": "ى" }
],
"ب": [
{ "code": 1662, "label": "پ" }
],
"ل": [
{ "code": 65275, "label": "لا" },
{ "code": 65273, "label": "لإ" },
{ "code": 65271, "label": "لأ" },
{ "code": 65269, "label": "لآ" }
],
"ا": [
{ "code": 1570, "label": "آ" },
{ "code": 1569, "label": "ء" },
{ "code": 1571, "label": "أ" },
{ "code": 1573, "label": "إ" },
{ "code": 1649, "label": "ٱ" }
],
"ك": [
{ "code": 1705, "label": "ک"},
{ "code": 1711, "label": "گ" }
],
"ى": [
{ "code": 1574, "label": "ئ" }
],
"ز": [
{ "code": 1688, "label": "ژ" }
],
".~normal": [
{ "code": 1611, "label": "ً" },
{ "code": 1622, "label": "ٖ" },
{ "code": 1648, "label": "ٰ" },
{ "code": 1619, "label": "ٓ" },
{ "code": 1615, "label": "ُ" },
{ "code": 1616, "label": "ِ" },
{ "code": 1614, "label": "َ" },
{ "code": 1600, "label": "ـ" },
{ "code": 1621, "label": "ٕ" },
{ "code": 1620, "label": "ٔ" },
{ "code": 1617, "label": "ّ" },
{ "code": 1612, "label": "ٌ" },
{ "code": 1613, "label": "ٍ" },
{ "code": 1618, "label": "ْ" }
],
".~uri": [
{ "code": -255, "label": ".ir"},
{ "code": -255, "label": ".gov" },
{ "code": -255, "label": ".edu" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".com" },
{ "code": -255, "label": ".net" }
]
}

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,57 @@
{ "code": 231, "label": "ç" }
],
"e": [
{ "code": 233, "label": "é" },
{ "code": 275, "label": "ē" },
{ "code": 234, "label": "ê" },
{ "code": 233, "label": "é" },
{ "code": 232, "label": "è" },
{ "code": 235, "label": "ë" }
],
"i": [
{ "code": 237, "label": "í" },
{ "code": 236, "label": "ì" },
{ "code": 239, "label": "ï" },
{ "code": 237, "label": "í" },
{ "code": 238, "label": "î" },
{ "code": 299, "label": "ī" }
],
"n": [
{ "code": 324, "label": "ń" },
{ "code": 241, "label": "ñ" }
{ "code": 241, "label": "ñ" },
{ "code": 324, "label": "ń" }
],
"o": [
{ "code": 243, "label": "ó" },
{ "code": 245, "label": "õ" },
{ "code": 333, "label": "ō" },
{ "code": 339, "label": "œ" },
{ "code": 248, "label": "ø" },
{ "code": 242, "label": "ò" },
{ "code": 246, "label": "ö" },
{ "code": 243, "label": "ó" },
{ "code": 244, "label": "ô" }
],
"s": [
{ "code": 223, "label": "ß" }
],
"u": [
{ "code": 250, "label": "ú" },
{ "code": 363, "label": "ū" },
{ "code": 252, "label": "ü" },
{ "code": 250, "label": "ú" },
{ "code": 251, "label": "û" },
{ "code": 249, "label": "ù" }
],
"ŝ": [
{ "code": 113, "label": "q" }
],
"ĝ": [
{ "code": 119, "label": "w" }
],
"ĉ": [
{ "code": 120, "label": "x" }
],
"ŭ": [
{ "code": 121, "label": "y" }
],
".~normal": [
{ "code": 44, "label": "," },
{ "code": 38, "label": "&" },
{ "code": 37, "label": "%" },
{ "code": 43, "label": "+" },
@@ -65,14 +78,13 @@
{ "code": 41, "label": ")" },
{ "code": 35, "label": "#" },
{ "code": 33, "label": "!" },
{ "code": 44, "label": "," },
{ "code": 63, "label": "?" }
],
".~uri": [
{ "code": -255, "label": ".com" },
{ "code": -255, "label": ".gov" },
{ "code": -255, "label": ".edu" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".com" },
{ "code": -255, "label": ".net" }
]
}

View File

@@ -0,0 +1,58 @@
{
"c": [
{ "code": 265, "label": "ĉ" }
],
"g": [
{ "code": 285, "label": "ĝ" }
],
"h": [
{ "code": 293, "label": "ĥ" }
],
"j": [
{ "code": 309, "label": "ĵ" }
],
"s": [
{ "code": 349, "label": "ŝ" }
],
"u": [
{ "code": 365, "label": "ŭ" }
],
"q": [
{ "code": 349, "label": "ŝ" }
],
"w": [
{ "code": 285, "label": "ĝ" }
],
"x": [
{ "code": 265, "label": "ĉ" }
],
"y": [
{ "code": 365, "label": "ŭ" }
],
".~normal": [
{ "code": 44, "label": "," },
{ "code": 38, "label": "&" },
{ "code": 37, "label": "%" },
{ "code": 43, "label": "+" },
{ "code": 34, "label": "\"" },
{ "code": 45, "label": "-" },
{ "code": 58, "label": ":" },
{ "code": 39, "label": "'" },
{ "code": 64, "label": "@" },
{ "code": 59, "label": ";" },
{ "code": 47, "label": "/" },
{ "code": 40, "label": "(" },
{ "code": 41, "label": ")" },
{ "code": 35, "label": "#" },
{ "code": 33, "label": "!" },
{ "code": 63, "label": "?" },
{ "code": 61, "label": "=" }
],
".~uri": [
{ "code": -255, "label": ".com" },
{ "code": -255, "label": ".gov" },
{ "code": -255, "label": ".edu" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".net" }
]
}

View File

@@ -1,43 +1,44 @@
{
"a": [
{ "code": 225, "label": "á" },
{ "code": 229, "label": "å" },
{ "code": 261, "label": "ą" },
{ "code": 230, "label": "æ" },
{ "code": 257, "label": "ā" },
{ "code": 170, "label": "ª" },
{ "code": 225, "label": "á" },
{ "code": 224, "label": "à" },
{ "code": 228, "label": "ä" },
{ "code": 226, "label": "â" },
{ "code": 227, "label": "ã" }
],
"c": [
{ "code": 269, "label": "č" },
{ "code": 231, "label": "ç" },
{ "code": 269, "label": "č" },
{ "code": 263, "label": "ć" }
],
"e": [
{ "code": 233, "label": "é" },
{ "code": 275, "label": "ē" },
{ "code": 281, "label": "ę" },
{ "code": 279, "label": "ė" },
{ "code": 235, "label": "ë" },
{ "code": 233, "label": "é" },
{ "code": 232, "label": "è" },
{ "code": 234, "label": "ê" }
],
"i": [
{ "code": 237, "label": "í" },
{ "code": 299, "label": "ī" },
{ "code": 238, "label": "î" },
{ "code": 303, "label": "į" },
{ "code": 236, "label": "ì" },
{ "code": 237, "label": "í" },
{ "code": 239, "label": "ï" }
],
"n": [
{ "code": 324, "label": "ń" },
{ "code": 241, "label": "ñ" }
{ "code": 241, "label": "ñ" },
{ "code": 324, "label": "ń" }
],
"o": [
{ "code": 243, "label": "ó" },
{ "code": 186, "label": "º" },
{ "code": 333, "label": "ō" },
{ "code": 248, "label": "ø" },
@@ -45,20 +46,20 @@
{ "code": 245, "label": "õ" },
{ "code": 244, "label": "ô" },
{ "code": 246, "label": "ö" },
{ "code": 243, "label": "ó" },
{ "code": 242, "label": "ò" }
],
"s": [
{ "code": 223, "label": "ß" }
],
"u": [
{ "code": 250, "label": "ú" },
{ "code": 363, "label": "ū" },
{ "code": 249, "label": "ù" },
{ "code": 250, "label": "ú" },
{ "code": 252, "label": "ü" },
{ "code": 251, "label": "û" }
],
".~normal": [
{ "code": 44, "label": "," },
{ "code": 58, "label": ":" },
{ "code": 38, "label": "&" },
{ "code": 64, "label": "@" },
@@ -73,14 +74,13 @@
{ "code": 41, "label": ")" },
{ "code": 35, "label": "#" },
{ "code": 33, "label": "!" },
{ "code": 44, "label": "," },
{ "code": 63, "label": "?" }
],
".~uri": [
{ "code": -255, "label": ".com" },
{ "code": -255, "label": ".gov" },
{ "code": -255, "label": ".edu" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".com" },
{ "code": -255, "label": ".net" }
]
}

View File

@@ -34,9 +34,9 @@
{ "code": 1610, "label": "ي" }
],
"ا": [
{ "code": 1570, "label": "آ" },
{ "code": 1649, "label": "ٱ" },
{ "code": 1569, "label": "ء" },
{ "code": 1570, "label": "آ" },
{ "code": 1571, "label": "أ" },
{ "code": 1573, "label": "إ" }
],
@@ -44,8 +44,8 @@
{ "code": 1577, "label": "ة" }
],
"ک": [
{ "code": 1603, "label": "ك" },
{ "code": 1706, "label": "ڪ"}
{ "code": 1706, "label": "ڪ"},
{ "code": 1603, "label": "ك" }
],
"ز": [
{ "code": 1688, "label": "ژ" }
@@ -54,6 +54,7 @@
{ "code": 1572, "label": "ؤ" }
],
".~normal": [
{ "code": 1611, "label": "ً" },
{ "code": 1622, "label": "ٖ" },
{ "code": 1648, "label": "ٰ" },
{ "code": 1619, "label": "ٓ" },
@@ -66,15 +67,14 @@
{ "code": 1617, "label": "ّ" },
{ "code": 1612, "label": "ٌ" },
{ "code": 1613, "label": "ٍ" },
{ "code": 1611, "label": "ً" },
{ "code": 1620, "label": "ٔ" }
],
".~uri": [
{ "code": -255, "label": ".ir"},
{ "code": -255, "label": ".gov" },
{ "code": -255, "label": ".edu" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".com" },
{ "code": -255, "label": ".net" },
{ "code": -255, "label": ".ir"}
{ "code": -255, "label": ".net" }
]
}

View File

@@ -1,39 +1,39 @@
{
"a": [
{ "code": 228, "label": "ä" },
{ "code": 225, "label": "á" },
{ "code": 226, "label": "â" },
{ "code": 227, "label": "ã" },
{ "code": 257, "label": "ā" },
{ "code": 228, "label": "ä" },
{ "code": 229, "label": "å" },
{ "code": 230, "label": "æ" },
{ "code": 224, "label": "à" }
],
"e": [
{ "code": 233, "label": "é" },
{ "code": 275, "label": "ē" },
{ "code": 281, "label": "ę" },
{ "code": 279, "label": "ė" },
{ "code": 232, "label": "è" },
{ "code": 233, "label": "é" },
{ "code": 235, "label": "ë" },
{ "code": 234, "label": "ê" }
],
"i": [
{ "code": 237, "label": "í" },
{ "code": 299, "label": "ī" },
{ "code": 236, "label": "ì" },
{ "code": 303, "label": "į" },
{ "code": 238, "label": "î" },
{ "code": 237, "label": "í" },
{ "code": 239, "label": "ï" }
],
"o": [
{ "code": 246, "label": "ö" },
{ "code": 333, "label": "ō" },
{ "code": 339, "label": "œ" },
{ "code": 243, "label": "ó" },
{ "code": 245, "label": "õ" },
{ "code": 242, "label": "ò" },
{ "code": 244, "label": "ô" },
{ "code": 246, "label": "ö" },
{ "code": 248, "label": "ø" }
],
"s": [
@@ -42,15 +42,15 @@
{ "code": 347, "label": "ś" }
],
"u": [
{ "code": 252, "label": "ü" },
{ "code": 363, "label": "ū" },
{ "code": 251, "label": "û" },
{ "code": 252, "label": "ü" },
{ "code": 250, "label": "ú" },
{ "code": 249, "label": "ù" }
],
"z": [
{ "code": 380, "label": "ż" },
{ "code": 382, "label": "ž" },
{ "code": 380, "label": "ż" },
{ "code": 378, "label": "ź" }
],
"ä": [
@@ -60,6 +60,7 @@
{ "code": 248, "label": "ø" }
],
".~normal": [
{ "code": 44, "label": "," },
{ "code": 38, "label": "&" },
{ "code": 37, "label": "%" },
{ "code": 43, "label": "+" },
@@ -74,14 +75,13 @@
{ "code": 41, "label": ")" },
{ "code": 35, "label": "#" },
{ "code": 33, "label": "!" },
{ "code": 44, "label": "," },
{ "code": 63, "label": "?" }
],
".~uri": [
{ "code": -255, "label": ".com" },
{ "code": -255, "label": ".gov" },
{ "code": -255, "label": ".edu" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".com" },
{ "code": -255, "label": ".net" }
]
}

View File

@@ -1,42 +1,43 @@
{
"a": [
{ "code": 224, "label": "à" },
{ "code": 227, "label": "ã" },
{ "code": 229, "label": "å" },
{ "code": 257, "label": "ā" },
{ "code": 170, "label": "ª" },
{ "code": 224, "label": "à" },
{ "code": 226, "label": "â" },
{ "code": 230, "label": "æ" },
{ "code": 225, "label": "á" },
{ "code": 228, "label": "ä" }
],
"c": [
{ "code": 269, "label": "č" },
{ "code": 231, "label": "ç" },
{ "code": 269, "label": "č" },
{ "code": 263, "label": "ć" }
],
"e": [
{ "code": 233, "label": "é" },
{ "code": 275, "label": "ē" },
{ "code": 281, "label": "ę" },
{ "code": 279, "label": "ė" },
{ "code": 234, "label": "ê" },
{ "code": 233, "label": "é" },
{ "code": 232, "label": "è" },
{ "code": 235, "label": "ë" }
],
"i": [
{ "code": 238, "label": "î" },
{ "code": 299, "label": "ī" },
{ "code": 237, "label": "í" },
{ "code": 303, "label": "į" },
{ "code": 236, "label": "ì" },
{ "code": 238, "label": "î" },
{ "code": 239, "label": "ï" }
],
"n": [
{ "code": 324, "label": "ń" },
{ "code": 241, "label": "ñ" }
{ "code": 241, "label": "ñ" },
{ "code": 324, "label": "ń" }
],
"o": [
{ "code": 244, "label": "ô" },
{ "code": 186, "label": "º" },
{ "code": 333, "label": "ō" },
{ "code": 245, "label": "õ" },
@@ -44,18 +45,17 @@
{ "code": 243, "label": "ó" },
{ "code": 242, "label": "ò" },
{ "code": 246, "label": "ö" },
{ "code": 244, "label": "ô" },
{ "code": 339, "label": "œ" }
],
"s": [
{ "code": 353, "label": "š" },
{ "code": 223, "label": "ß" },
{ "code": 353, "label": "š" },
{ "code": 347, "label": "ś" }
],
"u": [
{ "code": 249, "label": "ù" },
{ "code": 363, "label": "ū" },
{ "code": 252, "label": "ü" },
{ "code": 249, "label": "ù" },
{ "code": 251, "label": "û" },
{ "code": 250, "label": "ú" }
],
@@ -63,6 +63,7 @@
{ "code": 255, "label": "ÿ" }
],
".~normal": [
{ "code": 44, "label": "," },
{ "code": 38, "label": "&" },
{ "code": 37, "label": "%" },
{ "code": 43, "label": "+" },
@@ -77,14 +78,13 @@
{ "code": 41, "label": ")" },
{ "code": 35, "label": "#" },
{ "code": 33, "label": "!" },
{ "code": 44, "label": "," },
{ "code": 63, "label": "?" }
],
".~uri": [
{ "code": -255, "label": ".com" },
{ "code": -255, "label": ".gov" },
{ "code": -255, "label": ".edu" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".com" },
{ "code": -255, "label": ".net" }
]
}

View File

@@ -0,0 +1,53 @@
{
"a": [
{ "code": 225, "label": "á" }
],
"e": [
{ "code": 233, "label": "é" }
],
"i": [
{ "code": 237, "label": "í" }
],
"o": [
{ "code": 243, "label": "ó" },
{ "code": 246, "label": "ö" },
{ "code": 337, "label": "ő" }
],
"ö": [
{ "code": 337, "label": "ő" }
],
"u": [
{ "code": 250, "label": "ú" },
{ "code": 252, "label": "ü" },
{ "code": 369, "label": "ű" }
],
"ü": [
{ "code": 369, "label": "ű" }
],
".~normal": [
{ "code": 44, "label": "," },
{ "code": 38, "label": "&" },
{ "code": 37, "label": "%" },
{ "code": 43, "label": "+" },
{ "code": 34, "label": "\"" },
{ "code": 45, "label": "-" },
{ "code": 58, "label": ":" },
{ "code": 39, "label": "'" },
{ "code": 64, "label": "@" },
{ "code": 59, "label": ";" },
{ "code": 47, "label": "/" },
{ "code": 40, "label": "(" },
{ "code": 41, "label": ")" },
{ "code": 35, "label": "#" },
{ "code": 33, "label": "!" },
{ "code": 63, "label": "?" }
],
".~uri": [
{ "code": -255, "label": ".com" },
{ "code": -255, "label": ".hu" },
{ "code": -255, "label": ".gov" },
{ "code": -255, "label": ".edu" },
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".net" }
]
}

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,41 @@
{
"type": "characters",
"name": "hungarian",
"direction": "ltr",
"arrangement": [
[
{ "code": 113, "label": "q" },
{ "code": 119, "label": "w" },
{ "code": 101, "label": "e" },
{ "code": 114, "label": "r" },
{ "code": 116, "label": "t" },
{ "code": 122, "label": "z" },
{ "code": 117, "label": "u" },
{ "code": 105, "label": "i" },
{ "code": 111, "label": "o" },
{ "code": 112, "label": "p" },
{ "code": 246, "label": "ö" }
], [
{ "code": 97, "label": "a" },
{ "code": 115, "label": "s" },
{ "code": 100, "label": "d" },
{ "code": 102, "label": "f" },
{ "code": 103, "label": "g" },
{ "code": 104, "label": "h" },
{ "code": 106, "label": "j" },
{ "code": 107, "label": "k" },
{ "code": 108, "label": "l" },
{ "code": 233, "label": "é" },
{ "code": 225, "label": "á" }
], [
{ "code": 121, "label": "y" },
{ "code": 120, "label": "x" },
{ "code": 99, "label": "c" },
{ "code": 118, "label": "v" },
{ "code": 98, "label": "b" },
{ "code": 110, "label": "n" },
{ "code": 109, "label": "m" },
{ "code": 252, "label": "ü" }
]
]
}

View File

@@ -0,0 +1,32 @@
{
"type": "characters/mod",
"name": "arabic",
"direction": "rtl",
"arrangement": [
[
{ "code": 0 },
{ "code": -5, "label": "delete", "type": "enter_editing" }
],
[
{ "code": -202, "label": "view_symbols", "type": "system_gui" },
{ "code": 64, "label": "@", "variation": "email_address" },
{ "code": 1548, "label": "،", "variation": "normal" },
{ "code": 47, "label": "/", "variation": "uri" },
{ "code": -210, "label": "language_switch", "type": "system_gui", "popup": [
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui" },
{ "code": -100, "label": "settings", "type": "system_gui" }
] },
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui", "popup": [
{ "code": -100, "label": "settings", "type": "system_gui" }
] },
{ "code": 32, "label": " " },
{ "code": 46, "label": ".", "variation": "email_address" },
{ "code": 46, "label": ".", "variation": "normal" },
{ "code": 46, "label": ".", "variation": "uri" },
{ "code": 10, "label": "enter", "type": "enter_editing", "popup": [
{ "code": -215, "label": "toggle_one_handed_mode", "type": "system_gui" },
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui" }
] }
]
]
}

View File

@@ -0,0 +1,30 @@
{
"type": "characters/mod",
"name": "dvorak",
"direction": "ltr",
"arrangement": [
[
{ "code": -1, "label": "shift", "type": "modifier" },
{ "code": 0 },
{ "code": -5, "label": "delete", "type": "enter_editing" }
], [
{ "code": -202, "label": "view_symbols", "type": "system_gui" },
{ "code": 113, "label": "q" },
{ "code": -210, "label": "language_switch", "type": "system_gui", "popup": [
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui" },
{ "code": -100, "label": "settings", "type": "system_gui" }
] },
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui", "popup": [
{ "code": -100, "label": "settings", "type": "system_gui" }
] },
{ "code": 32, "label": " " },
{ "code": 122, "label": "z", "variation": "email_address" },
{ "code": 122, "label": "z", "variation": "normal" },
{ "code": 122, "label": "z", "variation": "uri" },
{ "code": 10, "label": "enter", "type": "enter_editing", "popup": [
{ "code": -215, "label": "toggle_one_handed_mode", "type": "system_gui" },
{ "code": -213, "label": "switch_to_media_context", "type": "system_gui" }
] }
]
]
}

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

@@ -15,7 +15,10 @@
{ "code": 163, "label": "£" },
{ "code": 165, "label": "¥" }
] },
{ "code": 95, "label": "_", "popup": [] },
{ "code": 37, "label": "%", "popup": [
{ "code": 8240, "label": "‰" },
{ "code": 8453, "label": "℅" }
] },
{ "code": 38, "label": "&", "popup": [] },
{ "code": 45, "label": "-", "popup": [
{ "code": 8212, "label": "—" },

View File

@@ -54,10 +54,7 @@
] },
{ "code": 92, "label": "\\", "popup": [] }
], [
{ "code": 37, "label": "%", "popup": [
{ "code": 8240, "label": "‰" },
{ "code": 8453, "label": "℅" }
] },
{ "code": 95, "label": "_", "popup": [] },
{ "code": 169, "label": "©", "popup": [] },
{ "code": 174, "label": "®", "popup": [] },
{ "code": 8482, "label": "™", "popup": [] },

View File

@@ -47,6 +47,10 @@
"oneHandedButton": {
"fgColor": "#424242"
},
"privateMode": {
"bgColor": "#A000FF",
"fgColor": "#FFFFFF"
},
"smartbar": {
"bgColor": "transparent",
"fgColor": "@window/textColor",

View File

@@ -47,6 +47,10 @@
"oneHandedButton": {
"fgColor": "#EEEEEE"
},
"privateMode": {
"bgColor": "#A000FF",
"fgColor": "#FFFFFF"
},
"smartbar": {
"bgColor": "transparent",
"fgColor": "@window/textColor",

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
/*
* Copyright (C) 2020 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.core
import android.app.Application
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.crashutility.CrashUtility
import timber.log.Timber
class FlorisApplication : Application() {
override fun onCreate() {
super.onCreate()
CrashUtility.install(this)
if (BuildConfig.DEBUG)
Timber.plant(Timber.DebugTree())
}
}

View File

@@ -22,6 +22,7 @@ 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.*
@@ -30,20 +31,22 @@ import android.util.Log
import android.view.Gravity
import android.view.View
import android.view.WindowManager
import android.view.inputmethod.CursorAnchorInfo
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import android.widget.ImageButton
import android.widget.LinearLayout
import com.squareup.moshi.Json
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.media.MediaInputManager
import dev.patrickgold.florisboard.ime.text.TextInputManager
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyData
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
import dev.patrickgold.florisboard.settings.SettingsMainActivity
import dev.patrickgold.florisboard.util.*
import timber.log.Timber
import java.lang.ref.WeakReference
/**
* Variable which holds the current [FlorisBoard] instance. To get this instance from another
@@ -55,7 +58,7 @@ 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
@@ -64,17 +67,20 @@ class FlorisBoard : InputMethodService() {
var inputView: InputView? = null
private set
private var inputWindowView: InputWindowView? = null
private var eventListeners: MutableList<EventListener> = mutableListOf()
private var eventListeners: MutableList<WeakReference<EventListener?>?> = mutableListOf()
private var audioManager: AudioManager? = null
var clipboardManager: ClipboardManager? = null
private var vibrator: Vibrator? = null
private val osHandler = Handler()
var activeEditorInstance: EditorInstance = EditorInstance.default()
lateinit var subtypeManager: SubtypeManager
lateinit var activeSubtype: Subtype
private var currentThemeIsNight: Boolean = false
private var currentThemeResId: Int = 0
private var isNumberRowVisible: Boolean = false
val textInputManager: TextInputManager
val mediaInputManager: MediaInputManager
@@ -88,14 +94,22 @@ class FlorisBoard : InputMethodService() {
companion object {
private const val IME_ID: String = "dev.patrickgold.florisboard/.ime.core.FlorisBoard"
private const val IME_ID_DEBUG: String = "dev.patrickgold.florisboard.debug/dev.patrickgold.florisboard.ime.core.FlorisBoard"
fun checkIfImeIsEnabled(context: Context): Boolean {
val activeImeIds = Settings.Secure.getString(
context.contentResolver,
Settings.Secure.ENABLED_INPUT_METHODS
)
if (BuildConfig.DEBUG) Log.i(FlorisBoard::class.simpleName, "List of active IMEs: $activeImeIds")
return activeImeIds.split(":").contains(IME_ID)
Timber.i("List of active IMEs: $activeImeIds")
return when {
BuildConfig.DEBUG -> {
activeImeIds.split(":").contains(IME_ID_DEBUG)
}
else -> {
activeImeIds.split(":").contains(IME_ID)
}
}
}
fun checkIfImeIsSelected(context: Context): Boolean {
@@ -103,8 +117,15 @@ class FlorisBoard : InputMethodService() {
context.contentResolver,
Settings.Secure.DEFAULT_INPUT_METHOD
)
if (BuildConfig.DEBUG) Log.i(FlorisBoard::class.simpleName, "Selected IME: $selectedImeId")
return selectedImeId == IME_ID
Timber.i("Selected IME: $selectedImeId")
return when {
BuildConfig.DEBUG -> {
selectedImeId == IME_ID_DEBUG
}
else -> {
selectedImeId == IME_ID
}
}
}
@Synchronized
@@ -144,10 +165,11 @@ class FlorisBoard : InputMethodService() {
.build()
)
}
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onCreate()")
Timber.i("onCreate()")
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboardManager?.addPrimaryClipChangedListener(this)
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
prefs = PrefHelper.getDefaultInstance(this)
prefs.initDefaultPreferences()
@@ -157,30 +179,31 @@ class FlorisBoard : InputMethodService() {
currentThemeIsNight = prefs.internal.themeCurrentIsNight
currentThemeResId = getDayNightBaseThemeId(currentThemeIsNight)
isNumberRowVisible = prefs.keyboard.numberRow
setTheme(currentThemeResId)
updateTheme()
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
super.onCreate()
eventListeners.toList().forEach { it.onCreate() }
eventListeners.toList().forEach { it?.get()?.onCreate() }
}
@SuppressLint("InflateParams")
override fun onCreateInputView(): View? {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onCreateInputView()")
Timber.i("onCreateInputView()")
baseContext.setTheme(currentThemeResId)
inputWindowView = layoutInflater.inflate(R.layout.florisboard, null) as InputWindowView
eventListeners.toList().forEach { it.onCreateInputView() }
eventListeners.toList().forEach { it?.get()?.onCreateInputView() }
return inputWindowView
}
fun registerInputView(inputView: InputView) {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "registerInputView(inputView)")
Timber.i("registerInputView($inputView)")
this.inputView = inputView
initializeOneHandedEnvironment()
@@ -188,38 +211,66 @@ class FlorisBoard : InputMethodService() {
updateSoftInputWindowLayoutParameters()
updateOneHandedPanelVisibility()
eventListeners.toList().forEach { it.onRegisterInputView(inputView) }
eventListeners.toList().forEach { it?.get()?.onRegisterInputView(inputView) }
}
override fun onDestroy() {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onDestroy()")
Timber.i("onDestroy()")
clipboardManager?.removePrimaryClipChangedListener(this)
osHandler.removeCallbacksAndMessages(null)
florisboardInstance = null
eventListeners.toList().forEach { it.onDestroy() }
eventListeners.toList().forEach { it?.get()?.onDestroy() }
eventListeners.clear()
super.onDestroy()
}
override fun onStartInput(attribute: EditorInfo?, restarting: Boolean) {
Timber.i("onStartInput($attribute, $restarting)")
super.onStartInput(attribute, restarting)
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_IMMEDIATE)
}
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_MONITOR)
Timber.i("onStartInputView($info, $restarting)")
Timber.i("onStartInputView: ${info?.debugSummarize()}")
super.onStartInputView(info, restarting)
eventListeners.toList().forEach { it.onStartInputView(info, restarting) }
activeEditorInstance = EditorInstance.from(info, this)
eventListeners.toList().forEach {
it?.get()?.onStartInputView(activeEditorInstance, restarting)
}
}
override fun onFinishInputView(finishingInput: Boolean) {
currentInputConnection?.requestCursorUpdates(0)
Timber.i( "onFinishInputView($finishingInput)")
if (finishingInput) {
activeEditorInstance = EditorInstance.default()
}
super.onFinishInputView(finishingInput)
eventListeners.toList().forEach { it.onFinishInputView(finishingInput) }
eventListeners.toList().forEach { it?.get()?.onFinishInputView(finishingInput) }
}
override fun onFinishInput() {
Timber.i("onFinishInput()")
super.onFinishInput()
currentInputConnection?.requestCursorUpdates(0)
}
override fun onWindowShown() {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onWindowShown()")
Timber.i("onWindowShown()")
prefs.sync()
val newIsNumberRowVisible = prefs.keyboard.numberRow
if (isNumberRowVisible != newIsNumberRowVisible) {
textInputManager.layoutManager.clearLayoutCache(KeyboardMode.CHARACTERS)
isNumberRowVisible = newIsNumberRowVisible
}
updateTheme()
updateOneHandedPanelVisibility()
activeSubtype = subtypeManager.getActiveSubtype() ?: Subtype.DEFAULT
@@ -227,17 +278,18 @@ class FlorisBoard : InputMethodService() {
setActiveInput(R.id.text_input)
super.onWindowShown()
eventListeners.toList().forEach { it.onWindowShown() }
eventListeners.toList().forEach { it?.get()?.onWindowShown() }
}
override fun onWindowHidden() {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onWindowHidden()")
Timber.i("onWindowHidden()")
super.onWindowHidden()
eventListeners.toList().forEach { it.onWindowHidden() }
eventListeners.toList().forEach { it?.get()?.onWindowHidden() }
}
override fun onConfigurationChanged(newConfig: Configuration) {
Timber.i("onConfigurationChanged($newConfig)")
if (isInputViewShown) {
updateOneHandedPanelVisibility()
}
@@ -245,43 +297,33 @@ class FlorisBoard : InputMethodService() {
super.onConfigurationChanged(newConfig)
}
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
super.onUpdateCursorAnchorInfo(cursorAnchorInfo)
eventListeners.toList().forEach { it.onUpdateCursorAnchorInfo(cursorAnchorInfo) }
}
override fun onUpdateSelection(
oldSelStart: Int,
oldSelEnd: Int,
newSelStart: Int,
newSelEnd: Int,
candidatesStart: Int,
candidatesEnd: Int
oldSelStart: Int, oldSelEnd: Int,
newSelStart: Int, newSelEnd: Int,
candidatesStart: Int, candidatesEnd: Int
) {
Timber.i("onUpdateSelection($oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd)")
super.onUpdateSelection(
oldSelStart,
oldSelEnd,
newSelStart,
newSelEnd,
candidatesStart,
candidatesEnd
oldSelStart, oldSelEnd,
newSelStart, newSelEnd,
candidatesStart, candidatesEnd
)
eventListeners.toList().forEach {
it.onUpdateSelection(
oldSelStart,
oldSelEnd,
newSelStart,
newSelEnd,
candidatesStart,
candidatesEnd
)
}
activeEditorInstance.onUpdateSelection(
oldSelStart, oldSelEnd,
newSelStart, newSelEnd
)
eventListeners.toList().forEach { it?.get()?.onUpdateSelection() }
}
/**
* Reapplies the supplies colors and settings from prefs to navigation bar.
* Updates the theme of the IME Window, status and navigation bar, as well as the InputView and
* some of its components.
*/
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)
@@ -289,29 +331,43 @@ class FlorisBoard : InputMethodService() {
setInputView(onCreateInputView())
return
}
// Get Window and the flags of the DecorView
val w = window?.window ?: return
inputView?.setBackgroundColor(prefs.theme.keyboardBgColor)
var flags = w.decorView.systemUiVisibility
// Update navigation bar theme
w.navigationBarColor = prefs.theme.navBarColor
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
var flags = w.decorView.systemUiVisibility
flags = if (prefs.theme.navBarIsLight) {
flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
} else {
flags and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
}
w.decorView.systemUiVisibility = flags
}
// 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.onApplyThemeAttributes() }
ColorStateList.valueOf(prefs.theme.oneHandedButtonFgColor).also {
inputView?.oneHandedCtrlMoveStart?.imageTintList = it
inputView?.oneHandedCtrlMoveEnd?.imageTintList = it
inputView?.oneHandedCtrlCloseStart?.imageTintList = it
inputView?.oneHandedCtrlCloseEnd?.imageTintList = it
}
eventListeners.toList().forEach { it?.get()?.onApplyThemeAttributes() }
}
override fun onComputeInsets(outInsets: Insets?) {
@@ -398,6 +454,19 @@ class FlorisBoard : InputMethodService() {
}
}
/**
* Executes a given [SwipeAction]. Ignores any [SwipeAction] but the ones relevant for this
* class.
*/
fun executeSwipeAction(swipeAction: SwipeAction) {
when (swipeAction) {
SwipeAction.HIDE_KEYBOARD -> requestHideSelf(0)
SwipeAction.SWITCH_TO_PREV_SUBTYPE -> switchToPrevSubtype()
SwipeAction.SWITCH_TO_NEXT_SUBTYPE -> switchToNextSubtype()
else -> textInputManager.executeSwipeAction(swipeAction)
}
}
/**
* Hides the IME and launches [SettingsMainActivity].
*/
@@ -417,6 +486,11 @@ class FlorisBoard : InputMethodService() {
return subtypeManager.subtypes.size > 1
}
fun switchToPrevSubtype() {
activeSubtype = subtypeManager.switchToPrevSubtype() ?: Subtype.DEFAULT
onSubtypeChanged(activeSubtype)
}
fun switchToNextSubtype() {
activeSubtype = subtypeManager.switchToNextSubtype() ?: Subtype.DEFAULT
onSubtypeChanged(activeSubtype)
@@ -441,14 +515,12 @@ class FlorisBoard : InputMethodService() {
}
private fun initializeOneHandedEnvironment() {
inputView?.findViewById<ImageButton>(R.id.one_handed_ctrl_move_start)
?.setOnClickListener { v -> onOneHandedPanelButtonClick(v) }
inputView?.findViewById<ImageButton>(R.id.one_handed_ctrl_move_end)
?.setOnClickListener { v -> onOneHandedPanelButtonClick(v) }
inputView?.findViewById<ImageButton>(R.id.one_handed_ctrl_close_start)
?.setOnClickListener { v -> onOneHandedPanelButtonClick(v) }
inputView?.findViewById<ImageButton>(R.id.one_handed_ctrl_close_end)
?.setOnClickListener { v -> onOneHandedPanelButtonClick(v) }
{ v:View -> onOneHandedPanelButtonClick(v) }.also {
inputView?.oneHandedCtrlMoveStart?.setOnClickListener(it)
inputView?.oneHandedCtrlMoveEnd?.setOnClickListener(it)
inputView?.oneHandedCtrlCloseStart?.setOnClickListener(it)
inputView?.oneHandedCtrlCloseEnd?.setOnClickListener(it)
}
}
private fun onOneHandedPanelButtonClick(v: View) {
@@ -505,25 +577,36 @@ 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.
* @returns True if the listener has been added successfully, false otherwise.
* @return True if the listener has been added successfully, false otherwise.
*/
fun addEventListener(listener: EventListener): Boolean {
return eventListeners.add(listener)
return eventListeners.add(WeakReference(listener))
}
/**
* Removes a given [listener] from the list which will receive FlorisBoard events.
*
* TODO: implement this function with a proper iterator
*
* @param listener The same listener object which was used in [addEventListener].
* @returns True if the listener has been removed successfully, false otherwise. A false return
* @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 {
return eventListeners.remove(listener)
eventListeners.toList().forEach {
if (it?.get() == listener) {
return eventListeners.remove(it)
}
}
return false
}
interface EventListener {
@@ -532,23 +615,16 @@ class FlorisBoard : InputMethodService() {
fun onRegisterInputView(inputView: InputView) {}
fun onDestroy() {}
fun onStartInputView(info: EditorInfo?, restarting: Boolean) {}
fun onStartInputView(instance: EditorInstance, restarting: Boolean) {}
fun onFinishInputView(finishingInput: Boolean) {}
fun onWindowShown() {}
fun onWindowHidden() {}
fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {}
fun onUpdateSelection(
oldSelStart: Int,
oldSelEnd: Int,
newSelStart: Int,
newSelEnd: Int,
candidatesStart: Int,
candidatesEnd: Int
) {}
fun onUpdateSelection() {}
fun onApplyThemeAttributes() {}
fun onPrimaryClipChanged() {}
fun onSubtypeChanged(newSubtype: Subtype) {}
}

View File

@@ -19,11 +19,17 @@ 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.ImageButton
import android.widget.LinearLayout
import android.widget.ViewFlipper
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.text.key.KeyVariation
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
import dev.patrickgold.florisboard.util.ViewLayoutUtils
import timber.log.Timber
import kotlin.math.roundToInt
/**
@@ -33,13 +39,13 @@ 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()
var desiredInputViewHeight: Float = resources.getDimension(R.dimen.inputView_baseHeight)
private set
var desiredSmartbarHeight: Int = resources.getDimension(R.dimen.smartbar_baseHeight).roundToInt()
var desiredSmartbarHeight: Float = resources.getDimension(R.dimen.smartbar_baseHeight)
private set
var desiredTextKeyboardViewHeight: Int = resources.getDimension(R.dimen.textKeyboardView_baseHeight).roundToInt()
var desiredTextKeyboardViewHeight: Float = resources.getDimension(R.dimen.textKeyboardView_baseHeight)
private set
var desiredMediaKeyboardViewHeight: Int = resources.getDimension(R.dimen.mediaKeyboardView_baseHeight).roundToInt()
var desiredMediaKeyboardViewHeight: Float = resources.getDimension(R.dimen.mediaKeyboardView_baseHeight)
private set
var mainViewFlipper: ViewFlipper? = null
@@ -48,26 +54,42 @@ class InputView : LinearLayout {
private set
var oneHandedCtrlPanelEnd: LinearLayout? = null
private set
var oneHandedCtrlMoveStart: ImageButton? = null
private set
var oneHandedCtrlMoveEnd: ImageButton? = null
private set
var oneHandedCtrlCloseStart: ImageButton? = null
private set
var oneHandedCtrlCloseEnd: ImageButton? = null
private set
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
override fun onAttachedToWindow() {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onAttachedToWindow()")
Timber.i("onAttachedToWindow()")
super.onAttachedToWindow()
mainViewFlipper = findViewById(R.id.main_view_flipper)
oneHandedCtrlPanelStart = findViewById(R.id.one_handed_ctrl_panel_start)
oneHandedCtrlPanelEnd = findViewById(R.id.one_handed_ctrl_panel_end)
oneHandedCtrlMoveStart = findViewById(R.id.one_handed_ctrl_move_start)
oneHandedCtrlMoveEnd = findViewById(R.id.one_handed_ctrl_move_end)
oneHandedCtrlCloseStart = findViewById(R.id.one_handed_ctrl_close_start)
oneHandedCtrlCloseEnd = findViewById(R.id.one_handed_ctrl_close_end)
florisboard.registerInputView(this)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val heightFactor = when (resources.configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> 0.85f
Configuration.ORIENTATION_LANDSCAPE -> 1.0f
else -> if (prefs.keyboard.oneHandedMode != "off") {
0.9f
} else {
@@ -81,14 +103,71 @@ class InputView : LinearLayout {
"mid_tall" -> 1.05f
"tall" -> 1.10f
"extra_tall" -> 1.15f
"custom" -> prefs.keyboard.heightFactorCustom.toFloat() / 100.0f
else -> 1.00f
}
val height = (resources.getDimension(R.dimen.inputView_baseHeight) * heightFactor).roundToInt()
desiredInputViewHeight = height
desiredSmartbarHeight = (0.16129 * height).roundToInt()
desiredTextKeyboardViewHeight = height - desiredSmartbarHeight
desiredMediaKeyboardViewHeight = height
var baseHeight = calcInputViewHeight() * heightFactor
var baseSmartbarHeight = 0.16129f * baseHeight
var baseTextInputHeight = baseHeight - baseSmartbarHeight
val tim = florisboard.textInputManager
val shouldGiveAdditionalSpace = prefs.keyboard.numberRow &&
!(tim.getActiveKeyboardMode() == KeyboardMode.NUMERIC ||
tim.getActiveKeyboardMode() == KeyboardMode.PHONE ||
tim.getActiveKeyboardMode() == KeyboardMode.PHONE2)
if (shouldGiveAdditionalSpace) {
val additionalHeight = desiredTextKeyboardViewHeight * 0.18f
baseHeight += additionalHeight
baseTextInputHeight += additionalHeight
}
val smartbarDisabled = !prefs.smartbar.enabled ||
tim.keyVariation == KeyVariation.PASSWORD && prefs.keyboard.numberRow ||
tim.getActiveKeyboardMode() == KeyboardMode.NUMERIC ||
tim.getActiveKeyboardMode() == KeyboardMode.PHONE ||
tim.getActiveKeyboardMode() == KeyboardMode.PHONE2
if (smartbarDisabled) {
baseHeight = baseTextInputHeight
baseSmartbarHeight = 0.0f
}
desiredInputViewHeight = baseHeight
desiredSmartbarHeight = baseSmartbarHeight
desiredTextKeyboardViewHeight = baseTextInputHeight
desiredMediaKeyboardViewHeight = baseHeight
// Add bottom offset for curved screens here. As the desired heights have already been set,
// adding a value to the height now will result in a bottom padding (aka offset).
baseHeight += ViewLayoutUtils.convertDpToPixel(
florisboard.prefs.keyboard.bottomOffset.toFloat(),
context
)
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY))
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(baseHeight.roundToInt(), MeasureSpec.EXACTLY))
}
/**
* Calculates the input view height based on the current screen dimensions and the auto
* selected dimension values.
*
* This method and the fraction values have been inspired by [OpenBoard](https://github.com/dslul/openboard)
* but are not 1:1 the same. This implementation differs from the
* [original](https://github.com/dslul/openboard/blob/90ae4c8aec034a8935e1fd02b441be25c7dba6ce/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/ResourceUtils.java)
* by calculating the average of the min and max height values, then taking at least the input
* view base height and return this resulting value.
*/
private fun calcInputViewHeight(): Float {
val dm: DisplayMetrics = resources.displayMetrics
val minBaseSize: Float = when (resources.configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> resources.getFraction(
R.fraction.inputView_minHeightFraction, dm.heightPixels, dm.heightPixels
)
else -> resources.getFraction(
R.fraction.inputView_minHeightFraction, dm.widthPixels, dm.widthPixels
)
}
val maxBaseSize: Float = resources.getFraction(
R.fraction.inputView_maxHeightFraction, dm.heightPixels, dm.heightPixels
)
return ((minBaseSize + maxBaseSize) / 2.0f).coerceAtLeast(
resources.getDimension(R.dimen.inputView_baseHeight)
)
}
}

View File

@@ -21,6 +21,10 @@ import android.content.SharedPreferences
import android.provider.Settings
import androidx.preference.PreferenceManager
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.text.gestures.DistanceThreshold
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.gestures.VelocityThreshold
import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
import dev.patrickgold.florisboard.util.VersionName
import kotlin.collections.HashMap
@@ -37,9 +41,12 @@ class PrefHelper(
val advanced = Advanced(this)
val correction = Correction(this)
val gestures = Gestures(this)
val glide = Glide(this)
val internal = Internal(this)
val keyboard = Keyboard(this)
val localization = Localization(this)
val smartbar = Smartbar(this)
val suggestion = Suggestion(this)
val theme = Theme(this)
@@ -160,6 +167,7 @@ class PrefHelper(
companion object {
const val SETTINGS_THEME = "advanced__settings_theme"
const val SHOW_APP_ICON = "advanced__show_app_icon"
const val FORCE_PRIVATE_MODE = "advanced__force_private_mode"
}
var settingsTheme: String = ""
@@ -168,6 +176,9 @@ class PrefHelper(
var showAppIcon: Boolean = false
get() = prefHelper.getPref(SHOW_APP_ICON, true)
private set
var forcePrivateMode: Boolean
get() = prefHelper.getPref(FORCE_PRIVATE_MODE, false)
set(v) = prefHelper.setPref(FORCE_PRIVATE_MODE, v)
}
/**
@@ -175,12 +186,82 @@ 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 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)
}
/**
@@ -226,22 +307,50 @@ class PrefHelper(
*/
class Keyboard(private val prefHelper: PrefHelper) {
companion object {
const val HEIGHT_FACTOR = "keyboard__height_factor"
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"
const val BOTTOM_OFFSET = "keyboard__bottom_offset"
const val FONT_SIZE_MULTIPLIER_PORTRAIT = "keyboard__font_size_multiplier_portrait"
const val FONT_SIZE_MULTIPLIER_LANDSCAPE = "keyboard__font_size_multiplier_landscape"
const val HEIGHT_FACTOR = "keyboard__height_factor"
const val HEIGHT_FACTOR_CUSTOM = "keyboard__height_factor_custom"
const val HINTED_NUMBER_ROW_MODE = "keyboard__hinted_number_row_mode"
const val HINTED_SYMBOLS_MODE = "keyboard__hinted_symbols_mode"
const val LONG_PRESS_DELAY = "keyboard__long_press_delay"
const val NUMBER_ROW = "keyboard__number_row"
const val ONE_HANDED_MODE = "keyboard__one_handed_mode"
const val POPUP_ENABLED = "keyboard__popup_enabled"
const val SOUND_ENABLED = "keyboard__sound_enabled"
const val SOUND_VOLUME = "keyboard__sound_volume"
const val VIBRATION_ENABLED = "keyboard__vibration_enabled"
const val VIBRATION_STRENGTH = "keyboard__vibration_strength"
}
var bottomOffset: Int = 0
get() = prefHelper.getPref(BOTTOM_OFFSET, 0)
private set
var fontSizeMultiplierPortrait: Int
get() = prefHelper.getPref(FONT_SIZE_MULTIPLIER_PORTRAIT, 100)
set(v) = prefHelper.setPref(FONT_SIZE_MULTIPLIER_PORTRAIT, v)
var fontSizeMultiplierLandscape: Int
get() = prefHelper.getPref(FONT_SIZE_MULTIPLIER_LANDSCAPE, 100)
set(v) = prefHelper.setPref(FONT_SIZE_MULTIPLIER_LANDSCAPE, v)
var heightFactor: String = ""
get() = prefHelper.getPref(HEIGHT_FACTOR, "normal")
private set
var heightFactorCustom: Int
get() = prefHelper.getPref(HEIGHT_FACTOR_CUSTOM, 100)
set(v) = prefHelper.setPref(HEIGHT_FACTOR_CUSTOM, v)
var hintedNumberRowMode: KeyHintMode
get() = KeyHintMode.fromString(prefHelper.getPref(HINTED_NUMBER_ROW_MODE, KeyHintMode.ENABLED_ACCENT_PRIORITY.toString()))
set(v) = prefHelper.setPref(HINTED_NUMBER_ROW_MODE, v)
var hintedSymbolsMode: KeyHintMode
get() = KeyHintMode.fromString(prefHelper.getPref(HINTED_SYMBOLS_MODE, KeyHintMode.ENABLED_ACCENT_PRIORITY.toString()))
set(v) = prefHelper.setPref(HINTED_SYMBOLS_MODE, v)
var longPressDelay: Int = 0
get() = prefHelper.getPref(LONG_PRESS_DELAY, 300)
private set
var numberRow: Boolean
get() = prefHelper.getPref(NUMBER_ROW, false)
set(v) = prefHelper.setPref(NUMBER_ROW, v)
var oneHandedMode: String
get() = prefHelper.getPref(ONE_HANDED_MODE, "off")
set(value) = prefHelper.setPref(ONE_HANDED_MODE, value)
@@ -274,32 +383,45 @@ class PrefHelper(
}
var activeSubtypeId: Int
get() = prefHelper.getPref(ACTIVE_SUBTYPE_ID, Subtype.DEFAULT.id)
get() = prefHelper.getPref(ACTIVE_SUBTYPE_ID, Subtype.DEFAULT.id)
set(v) = prefHelper.setPref(ACTIVE_SUBTYPE_ID, v)
var subtypes: String
get() = prefHelper.getPref(SUBTYPES, "")
get() = prefHelper.getPref(SUBTYPES, "")
set(v) = prefHelper.setPref(SUBTYPES, v)
}
/**
* Wrapper class for Smartbar preferences.
*/
class Smartbar(private val prefHelper: PrefHelper) {
companion object {
const val ENABLED = "smartbar__enabled"
}
var enabled: Boolean
get() = prefHelper.getPref(ENABLED, true)
set(v) = prefHelper.setPref(ENABLED, v)
}
/**
* Wrapper class for suggestion preferences.
*/
class Suggestion(private val prefHelper: PrefHelper) {
companion object {
const val ENABLED = "suggestion__enabled"
const val SHOW_INSTEAD = "suggestion__show_instead"
const val USE_PREV_WORDS = "suggestion__use_prev_words"
const val ENABLED = "suggestion__enabled"
const val SUGGEST_CLIPBOARD_CONTENT = "suggestion__suggest_clipboard_content"
const val USE_PREV_WORDS = "suggestion__use_prev_words"
}
var enabled: Boolean = false
get() = prefHelper.getPref(ENABLED, true)
private set
var showInstead: String = ""
get() = prefHelper.getPref(SHOW_INSTEAD, "number_row")
private set
var usePrevWords: Boolean = false
get() = prefHelper.getPref(USE_PREV_WORDS, true)
private set
var enabled: Boolean
get() = prefHelper.getPref(ENABLED, true)
set(v) = prefHelper.setPref(ENABLED, v)
var suggestClipboardContent: Boolean
get() = prefHelper.getPref(SUGGEST_CLIPBOARD_CONTENT, false)
set(v) = prefHelper.setPref(SUGGEST_CLIPBOARD_CONTENT, v)
var usePrevWords: Boolean
get() = prefHelper.getPref(USE_PREV_WORDS, true)
set(v) = prefHelper.setPref(USE_PREV_WORDS, v)
}
/**
@@ -330,6 +452,8 @@ class PrefHelper(
const val MEDIA_FG_COLOR_ALT = "theme__media_fgColorAlt"
const val ONE_HANDED_BG_COLOR = "theme__oneHanded_bgColor"
const val ONE_HANDED_BUTTON_FG_COLOR = "theme__oneHandedButton_fgColor"
const val PRIVATE_MODE_BG_COLOR = "theme__privateMode_bgColor"
const val PRIVATE_MODE_FG_COLOR = "theme__privateMode_fgColor"
const val SMARTBAR_BG_COLOR = "theme__smartbar_bgColor"
const val SMARTBAR_FG_COLOR = "theme__smartbar_fgColor"
const val SMARTBAR_FG_COLOR_ALT = "theme__smartbar_fgColorAlt"
@@ -406,6 +530,12 @@ class PrefHelper(
var oneHandedButtonFgColor: Int
get() = prefHelper.getPref(ONE_HANDED_BUTTON_FG_COLOR, 0)
set(v) = prefHelper.setPref(ONE_HANDED_BUTTON_FG_COLOR, v)
var privateModeBgColor: Int
get() = prefHelper.getPref(PRIVATE_MODE_BG_COLOR, 0)
set(v) = prefHelper.setPref(PRIVATE_MODE_BG_COLOR, v)
var privateModeFgColor: Int
get() = prefHelper.getPref(PRIVATE_MODE_FG_COLOR, 0)
set(v) = prefHelper.setPref(PRIVATE_MODE_FG_COLOR, v)
var smartbarBgColor: Int
get() = prefHelper.getPref(SMARTBAR_BG_COLOR, 0)
set(v) = prefHelper.setPref(SMARTBAR_BG_COLOR, v)

View File

@@ -87,16 +87,10 @@ data class Subtype(
* Must be a string which also exists in [FlorisBoard.ImeConfig.characterLayouts]. If the value is
* not included within this list, no layout will be shown to the user if the user selects the
* predefined layout value.
* @property isAsciiCapable Legacy attribute for Android's InputMethodSubtype. Currently no real
* use within this project.
* @property isEmojiCapable Legacy attribute for Android's InputMethodSubtype. Currently no real
* use within this project.
*/
data class DefaultSubtype(
var id: Int,
@Json(name = "languageTag")
var locale: Locale,
var preferredLayout: String,
var isAsciiCapable: Boolean,
var isEmojiCapable: Boolean
var preferredLayout: String
)

View File

@@ -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,7 +129,7 @@ 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? {
@@ -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? {
@@ -217,10 +217,38 @@ class SubtypeManager(
}
}
/**
* 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.localization.activeSubtypeId = when (newActiveSubtype) {
null -> -1
null -> Subtype.DEFAULT.id
else -> newActiveSubtype.id
}
return newActiveSubtype

View File

@@ -17,6 +17,7 @@
package dev.patrickgold.florisboard.ime.media
import android.annotation.SuppressLint
import android.os.Handler
import android.util.Log
import android.view.MotionEvent
import android.view.View
@@ -24,6 +25,7 @@ import android.widget.*
import com.google.android.material.tabs.TabLayout
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.EditorInstance
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.InputView
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyData
@@ -33,7 +35,10 @@ import dev.patrickgold.florisboard.ime.media.emoticon.EmoticonKeyboardView
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyData
import dev.patrickgold.florisboard.ime.text.key.KeyType
import dev.patrickgold.florisboard.util.cancelAll
import dev.patrickgold.florisboard.util.postAtScheduledRate
import kotlinx.coroutines.*
import timber.log.Timber
import java.util.*
/**
@@ -50,10 +55,12 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
FlorisBoard.EventListener {
private val florisboard = FlorisBoard.getInstance()
private val activeEditorInstance: EditorInstance
get() = florisboard.activeEditorInstance
private var activeTab: Tab? = null
private var mediaViewFlipper: ViewFlipper? = null
private var osTimer: Timer? = null
private var repeatedKeyPressHandler: Handler? = null
private var tabLayout: TabLayout? = null
private val tabViews = EnumMap<Tab, LinearLayout>(Tab::class.java)
@@ -75,6 +82,11 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
florisboard.addEventListener(this)
}
override fun onCreateInputView() {
super.onCreateInputView()
repeatedKeyPressHandler = Handler(florisboard.context.mainLooper)
}
/**
* Called when a new input view has been registered. Used to initialize all media-relevant
* views and layouts.
@@ -82,7 +94,7 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
*/
@SuppressLint("ClickableViewAccessibility")
override fun onRegisterInputView(inputView: InputView) {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onRegisterInputView(inputView)")
Timber.i("onRegisterInputView(inputView)")
launch(Dispatchers.Default) {
mediaViewGroup = inputView.findViewById(R.id.media_input)
@@ -108,15 +120,12 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
override fun onTabReselected(tab: TabLayout.Tab) {}
})
for (tab in Tab.values()) {
val tabView = createTabViewFor(tab)
tabViews[tab] = tabView
withContext(Dispatchers.Main) {
withContext(Dispatchers.Main) {
for (tab in Tab.values()) {
val tabView = createTabViewFor(tab)
tabViews[tab] = tabView
mediaViewFlipper?.addView(tabView)
}
}
withContext(Dispatchers.Main) {
tabLayout?.selectTab(tabLayout?.getTabAt(0))
}
}
@@ -126,7 +135,7 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
* Clean-up of resources and stopping all coroutines.
*/
override fun onDestroy() {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onDestroy()")
Timber.i("onDestroy()")
cancel()
instance = null
@@ -151,17 +160,14 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
florisboard.keyPressVibrate()
florisboard.keyPressSound(data)
if (data?.code == KeyCode.DELETE && data.type == KeyType.ENTER_EDITING) {
osTimer = Timer()
osTimer?.scheduleAtFixedRate(object : TimerTask() {
override fun run() {
florisboard.textInputManager.sendKeyPress(data)
}
}, 500, 50)
val delayMillis = florisboard.prefs.keyboard.longPressDelay.toLong()
repeatedKeyPressHandler?.postAtScheduledRate(delayMillis, 25) {
florisboard.textInputManager.sendKeyPress(data)
}
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
osTimer?.cancel()
osTimer = null
repeatedKeyPressHandler?.cancelAll()
if (event.actionMasked != MotionEvent.ACTION_CANCEL && data != null) {
florisboard.textInputManager.sendKeyPress(data)
}
@@ -199,18 +205,14 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
* Sends a given [emojiKeyData] to the current input editor.
*/
fun sendEmojiKeyPress(emojiKeyData: EmojiKeyData) {
val ic = florisboard.currentInputConnection
ic?.finishComposingText()
ic?.commitText(emojiKeyData.getCodePointsAsString(), 1)
activeEditorInstance.commitText(emojiKeyData.getCodePointsAsString())
}
/**
* Sends a given [emoticonKeyData] to the current input editor.
*/
fun sendEmoticonKeyPress(emoticonKeyData: EmoticonKeyData) {
val ic = florisboard.currentInputConnection
ic?.finishComposingText()
ic?.commitText(emoticonKeyData.icon, 1)
activeEditorInstance.commitText(emoticonKeyData.icon)
}
/**

View File

@@ -26,6 +26,7 @@ import com.google.android.material.tabs.TabLayout
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.PrefHelper
import kotlin.math.roundToInt
class MediaInputView : LinearLayout, FlorisBoard.EventListener {
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
@@ -61,7 +62,7 @@ class MediaInputView : LinearLayout, FlorisBoard.EventListener {
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val height = florisboard?.inputView?.desiredInputViewHeight ?: 0
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY))
val height = florisboard?.inputView?.desiredMediaKeyboardViewHeight ?: 0.0f
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height.roundToInt(), MeasureSpec.EXACTLY))
}
}

View File

@@ -18,14 +18,12 @@ package dev.patrickgold.florisboard.ime.media.emoji
import android.annotation.SuppressLint
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.drawable.Drawable
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
@@ -91,7 +89,7 @@ class EmojiKeyView(
osHandler = Handler()
}
osHandler?.postDelayed({
(parent.parent as HorizontalScrollView)
(parent.parent as ScrollView)
.requestDisallowInterceptTouchEvent(true)
emojiKeyboardView.isScrollBlocked = true
emojiKeyboardView.popupManager.show(this)

View File

@@ -21,14 +21,13 @@ 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
@@ -55,7 +54,7 @@ class EmojiKeyboardView : LinearLayout, FlorisBoard.EventListener {
private var layouts: Deferred<EmojiLayoutDataMap>
private val mainScope = MainScope()
private val tabLayout: TabLayout
private val uiLayouts = EnumMap<EmojiCategory, HorizontalScrollView>(EmojiCategory::class.java)
private val uiLayouts = EnumMap<EmojiCategory, ScrollView>(EmojiCategory::class.java)
var isScrollBlocked: Boolean = false
var popupManager = KeyPopupManager<EmojiKeyboardView, EmojiKeyView>(this)
@@ -66,12 +65,15 @@ class EmojiKeyboardView : LinearLayout, FlorisBoard.EventListener {
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)
@@ -117,10 +119,10 @@ class EmojiKeyboardView : LinearLayout, FlorisBoard.EventListener {
*/
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)
}
}
}
@@ -130,18 +132,19 @@ class EmojiKeyboardView : LinearLayout, FlorisBoard.EventListener {
* 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 =
@@ -151,11 +154,30 @@ class EmojiKeyboardView : LinearLayout, FlorisBoard.EventListener {
)
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
}
/**

View File

@@ -21,6 +21,7 @@ import android.graphics.Paint
import android.graphics.Typeface
import android.util.Log
import androidx.core.graphics.PaintCompat
import timber.log.Timber
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
@@ -171,13 +172,13 @@ fun parseRawEmojiSpecsFile(
}
}
} catch (e: IOException) {
Log.e("EmojiLayoutDataMap", "parseRawEmojiSpecsFile(): $e")
Timber.e("parseRawEmojiSpecsFile(): $e")
} finally {
if (reader != null) {
try {
reader.close()
} catch (e: IOException) {
Log.e("EmojiLayoutDataMap", "parseRawEmojiSpecsFile(): $e")
Timber.e("parseRawEmojiSpecsFile(): $e")
}
}
}

View File

@@ -30,7 +30,7 @@ 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
) {

View File

@@ -18,15 +18,15 @@ package dev.patrickgold.florisboard.ime.popup
import android.content.res.Configuration
import android.util.TypedValue
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.*
import android.widget.*
import androidx.core.content.ContextCompat.getDrawable
import androidx.core.view.get
import com.google.android.flexbox.FlexboxLayout
import com.google.android.flexbox.JustifyContent
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.databinding.KeyPopupExtendedViewBinding
import dev.patrickgold.florisboard.databinding.KeyPopupViewBinding
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyData
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyView
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyboardView
@@ -34,6 +34,7 @@ import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyData
import dev.patrickgold.florisboard.ime.text.key.KeyView
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
import dev.patrickgold.florisboard.util.ViewLayoutUtils
/**
* Manages the creation and dismissal of key popups as well as the checks if the pointer moved
@@ -42,7 +43,6 @@ import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
* @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
@@ -55,9 +55,10 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
)
private var keyPopupWidth: Int
private var keyPopupHeight: Int
var keyPopupTextSize: Float = keyboardView.resources.getDimension(R.dimen.key_popup_textSize)
private var keyPopupDiffX: Int = 0
private val popupView: LinearLayout
private val popupViewExt: FlexboxLayout
private val popupView: KeyPopupViewBinding
private val popupViewExt: KeyPopupExtendedViewBinding
private var row0count: Int = 0
private var row1count: Int = 0
private var window: PopupWindow
@@ -65,25 +66,20 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
/** Is true if the preview popup is visible to the user, else false */
val isShowingPopup: Boolean
get() = popupView.visibility == View.VISIBLE
get() = popupView.root.visibility == View.VISIBLE
/** Is true if the extended popup is visible to the user, else false */
val isShowingExtendedPopup: Boolean
get() = windowExt.isShowing
init {
val inflater = LayoutInflater.from(keyboardView.context)
keyPopupWidth = keyboardView.resources.getDimension(R.dimen.key_width).toInt()
keyPopupHeight = keyboardView.resources.getDimension(R.dimen.key_height).toInt()
popupView = View.inflate(
keyboardView.context,
R.layout.key_popup, null
) as LinearLayout
popupView.visibility = View.INVISIBLE
popupViewExt = View.inflate(
keyboardView.context,
R.layout.key_popup_extended, null
) as FlexboxLayout
window = createPopupWindow(popupView)
windowExt = createPopupWindow(popupViewExt)
popupView = KeyPopupViewBinding.inflate(inflater, null, false)
popupView.root.visibility = View.INVISIBLE
popupViewExt = KeyPopupExtendedViewBinding.inflate(inflater, null, false)
window = createPopupWindow(popupView.root)
windowExt = createPopupWindow(popupViewExt.root)
}
/**
@@ -101,14 +97,14 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
isInitActive: Boolean = false,
isWrapBefore: Boolean = false
): KeyPopupExtendedSingleView? {
val textView = KeyPopupExtendedSingleView(keyView.context, isInitActive)
val lp = FlexboxLayout.LayoutParams(keyPopupWidth, keyView.measuredHeight)
val textView = KeyPopupExtendedSingleView(keyView.context, k, isInitActive)
val lp = FlexboxLayout.LayoutParams(keyPopupWidth, (keyPopupHeight * 0.4f).toInt())
lp.isWrapBefore = isWrapBefore
textView.layoutParams = lp
textView.gravity = Gravity.CENTER
val textSize = keyboardView.resources.getDimension(R.dimen.key_popup_textSize)
val textSize = keyPopupTextSize
if (keyView is KeyView) {
when (keyView.data.popup[k].code) {
when (keyView.dataPopupWithHint[k].code) {
KeyCode.SETTINGS -> {
textView.iconDrawable = getDrawable(
keyView.context, R.drawable.ic_settings
@@ -124,18 +120,18 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
}
KeyCode.TOGGLE_ONE_HANDED_MODE -> {
textView.iconDrawable = getDrawable(
keyView.context, R.drawable.ic_keyboard_arrow_right
keyView.context, R.drawable.ic_smartphone
)
}
else -> {
textView.setTextSize(
TypedValue.COMPLEX_UNIT_PX, when (keyView.data.popup[k].code) {
TypedValue.COMPLEX_UNIT_PX, when (keyView.dataPopupWithHint[k].code) {
KeyCode.URI_COMPONENT_TLD,
KeyCode.SWITCH_TO_TEXT_CONTEXT -> textSize * 0.6f
else -> textSize
}
)
textView.text = keyView.getComputedLetter(keyView.data.popup[k])
textView.text = keyView.getComputedLetter(keyView.dataPopupWithHint[k])
}
}
} else if (keyView is EmojiKeyView) {
@@ -151,11 +147,13 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
* @return A new [PopupWindow] already preconfigured and ready-to-go.
*/
private fun createPopupWindow(view: View): PopupWindow {
return PopupWindow(keyboardView.context).apply {
return PopupWindow(view.context).apply {
animationStyle = 0
contentView = view
elevation = ViewLayoutUtils.convertDpToPixel(2.0f, view.context)
enterTransition = null
exitTransition = null
isAttachedInDecor = false
isClippingEnabled = false
isFocusable = false
isTouchable = false
@@ -171,12 +169,22 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
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) {
@@ -209,19 +217,21 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
window.showAsDropDown(keyView, keyPopupX, keyPopupY, Gravity.NO_GRAVITY)
}
if (keyView is KeyView) {
popupView.findViewById<TextView>(R.id.key_popup_text)?.text = keyView.getComputedLetter()
popupView.findViewById<ImageView>(R.id.key_popup_threedots)?.visibility = when {
keyView.data.popup.isEmpty() -> View.INVISIBLE
popupView.symbol.layoutParams.height = (keyPopupHeight * 0.4f).toInt()
popupView.symbol.setTextSize(TypedValue.COMPLEX_UNIT_PX, keyPopupTextSize)
popupView.symbol.text = keyView.getComputedLetter()
popupView.threedots.visibility = when {
keyView.dataPopupWithHint.isEmpty() -> View.INVISIBLE
else -> View.VISIBLE
}
} else if (keyView is EmojiKeyView) {
popupView.findViewById<TextView>(R.id.key_popup_text)?.text = keyView.data.getCodePointsAsString()
popupView.findViewById<ImageView>(R.id.key_popup_threedots)?.visibility = when {
popupView.symbol.text = keyView.data.getCodePointsAsString()
popupView.threedots.visibility = when {
keyView.data.popup.isEmpty() -> View.INVISIBLE
else -> View.VISIBLE
}
}
popupView.visibility = View.VISIBLE
popupView.root.visibility = View.VISIBLE
}
/**
@@ -256,17 +266,12 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
}
// Anchor left if keyView is in left half of keyboardView, else anchor right
if (keyView is KeyView) {
anchorLeft = keyView.x < keyboardView.measuredWidth / 2
} else if (keyView is EmojiKeyView) {
val hsv = (keyView.parent.parent as HorizontalScrollView)
anchorLeft = (keyView.x - hsv.scrollX) < keyboardView.measuredWidth / 2
}
anchorLeft = keyView.x < keyboardView.measuredWidth / 2
anchorRight = !anchorLeft
// Determine key counts for each row
val n = when (keyView) {
is KeyView -> keyView.data.popup.size
is KeyView -> keyView.dataPopupWithHint.size
is EmojiKeyView -> keyView.data.popup.size
else -> 0
}
@@ -313,42 +318,54 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
}
// Build UI
popupViewExt.removeAllViews()
popupViewExt.root.removeAllViews()
val indices = when (keyView) {
is KeyView -> keyView.data.popup.indices
is KeyView -> keyView.dataPopupWithHint.indices
is EmojiKeyView -> keyView.data.popup.indices
else -> IntRange(0, 0)
}
var hasShownFirst = false
for (k in indices) {
val isInitActive =
anchorLeft && (k - row1count == anchorOffset) ||
anchorRight && (k - row1count == row0count - 1 - anchorOffset)
popupViewExt.addView(
val kk = when (keyView) {
is KeyView -> when {
isInitActive -> {
hasShownFirst = true
0
}
hasShownFirst -> k
else -> k + 1
}
else -> k
}
popupViewExt.root.addView(
createTextView(
keyView, k, isInitActive, (row1count > 0) && (k - row1count == 0)
keyView, kk, isInitActive, (row1count > 0) && (k - row1count == 0)
)
)
if (isInitActive) {
activeExtIndex = k
}
}
popupView.findViewById<ImageView>(R.id.key_popup_threedots)?.visibility = View.INVISIBLE
popupView.threedots.visibility = View.INVISIBLE
// Calculate layout params
val extWidth = row0count * keyPopupWidth
val extHeight = when {
row1count > 0 -> keyView.measuredHeight * 2
else -> keyView.measuredHeight
}
popupViewExt.justifyContent = if (anchorLeft) {
row1count > 0 -> keyPopupHeight * 0.4f * 2.0f
else -> keyPopupHeight * 0.4f
}.toInt()
popupViewExt.root.justifyContent = if (anchorLeft) {
JustifyContent.FLEX_START
} else {
JustifyContent.FLEX_END
}
if (popupViewExt.layoutParams == null) {
popupViewExt.layoutParams = ViewGroup.LayoutParams(extWidth, extHeight)
if (popupViewExt.root.layoutParams == null) {
popupViewExt.root.layoutParams = ViewGroup.LayoutParams(extWidth, extHeight)
} else {
popupViewExt.layoutParams.apply {
popupViewExt.root.layoutParams.apply {
width = extWidth
height = extHeight
}
@@ -359,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
}
@@ -441,8 +458,8 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
}
if (keyView is KeyView) {
for (k in keyView.data.popup.indices) {
val view = popupViewExt.getChildAt(k)
for (k in keyView.dataPopupWithHint.indices) {
val view = popupViewExt.root.getChildAt(k)
if (view != null) {
val textView = view as KeyPopupExtendedSingleView
textView.isActive = k == activeExtIndex
@@ -450,7 +467,7 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
}
} else if (keyView is EmojiKeyView) {
for (k in keyView.data.popup.indices) {
val view = popupViewExt.getChildAt(k)
val view = popupViewExt.root.getChildAt(k)
if (view != null) {
val textView = view as KeyPopupExtendedSingleView
textView.isActive = k == activeExtIndex
@@ -471,7 +488,17 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
*/
fun getActiveKeyData(keyView: T_KV): KeyData? {
return if (keyView is KeyView) {
keyView.data.popup.getOrNull(activeExtIndex ?: -1) ?: keyView.data
val activeExtIndex = activeExtIndex
if (activeExtIndex != null) {
val singleView = popupViewExt.root[activeExtIndex]
if (singleView is KeyPopupExtendedSingleView) {
keyView.dataPopupWithHint.getOrNull(singleView.adjustedIndex) ?: keyView.data
} else {
keyView.data
}
} else {
keyView.data
}
} else {
null
}
@@ -497,7 +524,7 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
* Hides the key preview popup as well as the extended popup.
*/
fun hide() {
popupView.visibility = View.INVISIBLE
popupView.root.visibility = View.INVISIBLE
if (windowExt.isShowing) {
windowExt.dismiss()
}

View File

@@ -23,13 +23,13 @@ import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.databinding.KeyPopupViewBinding
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.util.*
class KeyPopupView : LinearLayout {
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
private lateinit var text: TextView
private lateinit var threedots: ImageView
private lateinit var binding: KeyPopupViewBinding
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
@@ -37,14 +37,13 @@ class KeyPopupView : LinearLayout {
override fun onAttachedToWindow() {
super.onAttachedToWindow()
text = findViewById(R.id.key_popup_text)
threedots = findViewById(R.id.key_popup_threedots)
binding = KeyPopupViewBinding.bind(this)
}
override fun onDraw(canvas: Canvas?) {
setBackgroundTintColor2(this, prefs.theme.keyPopupBgColor)
text.setTextColor(prefs.theme.keyPopupFgColor)
setImageTintColor2(threedots, prefs.theme.keyPopupFgColor)
binding.symbol.setTextColor(prefs.theme.keyPopupFgColor)
setImageTintColor2(binding.threedots, prefs.theme.keyPopupFgColor)
super.onDraw(canvas)
}
}

View File

@@ -16,21 +16,19 @@
package dev.patrickgold.florisboard.ime.text
import android.content.ClipData
import android.content.Context
import android.os.Handler
import android.text.InputType
import android.util.Log
import android.view.KeyEvent
import android.view.inputmethod.*
import android.widget.LinearLayout
import android.widget.Toast
import android.widget.ViewFlipper
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.InputView
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.core.*
import dev.patrickgold.florisboard.ime.text.editing.EditingKeyboardView
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyData
import dev.patrickgold.florisboard.ime.text.key.KeyType
@@ -38,8 +36,9 @@ import dev.patrickgold.florisboard.ime.text.key.KeyVariation
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
import dev.patrickgold.florisboard.ime.text.layout.LayoutManager
import dev.patrickgold.florisboard.ime.text.smartbar.SmartbarManager
import dev.patrickgold.florisboard.ime.text.smartbar.SmartbarView
import kotlinx.coroutines.*
import timber.log.Timber
import java.util.*
/**
@@ -50,14 +49,15 @@ import java.util.*
* are separated from media-related UI. The core [FlorisBoard] will pass any event defined in
* [FlorisBoard.EventListener] through to this class.
*
* TextInputManager also keeps track of the current composing word and syncs this value with the
* Smartbar, which, depending on the mode and variation, may create candidates.
* @see SmartbarManager.generateCandidatesFromComposing for more information.
* TextInputManager is also the hub in the communication between the system, the active editor
* instance and the Smartbar.
*/
class TextInputManager private constructor() : CoroutineScope by MainScope(),
FlorisBoard.EventListener {
FlorisBoard.EventListener, SmartbarView.EventListener {
private val florisboard = FlorisBoard.getInstance()
private val activeEditorInstance: EditorInstance
get() = florisboard.activeEditorInstance
private var activeKeyboardMode: KeyboardMode? = null
private val keyboardViews = EnumMap<KeyboardMode, KeyboardView>(KeyboardMode::class.java)
@@ -67,34 +67,21 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
var textViewGroup: LinearLayout? = null
var keyVariation: KeyVariation = KeyVariation.NORMAL
private val layoutManager = LayoutManager(florisboard)
lateinit var smartbarManager: SmartbarManager
val layoutManager = LayoutManager(florisboard)
private var smartbarView: SmartbarView? = null
// Caps/Space related properties
var caps: Boolean = false
private set
var capsLock: Boolean = false
private set
private var cursorCapsMode: CapsMode = CapsMode.NONE
private var editorCapsMode: CapsMode = CapsMode.NONE
private var hasCapsRecentlyChanged: Boolean = false
private var hasSpaceRecentlyPressed: Boolean = false
// Composing text related properties
private var composingText: String? = null
private var composingTextStart: Int? = null
private var cursorPos: Int = 0
private var isComposingEnabled: Boolean = false
var isManualSelectionMode: Boolean = false
private var isManualSelectionModeLeft: Boolean = false
private var isManualSelectionModeRight: Boolean = false
val isTextSelected: Boolean
get() = selectionEnd - selectionStart != 0
private var lastCursorAnchorInfo: CursorAnchorInfo? = null
private var selectionStart: Int = 0
private val selectionStartMin: Int = 0
private var selectionEnd: Int = 0
private var selectionEndMax: Int = 0
companion object {
private var instance: TextInputManager? = null
@@ -117,27 +104,22 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* background).
*/
override fun onCreate() {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onCreate()")
Timber.i("onCreate()")
for (mode in KeyboardMode.values()) {
if (mode == KeyboardMode.CHARACTERS) {
var subtypes = florisboard.subtypeManager.subtypes
if (subtypes.isEmpty()) {
subtypes = listOf(Subtype.DEFAULT)
}
for (subtype in subtypes) {
layoutManager.preloadComputedLayout(mode, subtype)
}
} else {
layoutManager.preloadComputedLayout(mode, florisboard.activeSubtype)
var subtypes = florisboard.subtypeManager.subtypes
if (subtypes.isEmpty()) {
subtypes = listOf(Subtype.DEFAULT)
}
for (subtype in subtypes) {
for (mode in KeyboardMode.values()) {
layoutManager.preloadComputedLayout(mode, subtype, florisboard.prefs)
}
}
smartbarManager = SmartbarManager.getInstance()
}
private suspend fun addKeyboardView(mode: KeyboardMode) {
val keyboardView = KeyboardView(florisboard.context)
keyboardView.computedLayout = layoutManager.fetchComputedLayoutAsync(mode, florisboard.activeSubtype).await()
keyboardView.computedLayout = layoutManager.fetchComputedLayoutAsync(mode, florisboard.activeSubtype, florisboard.prefs).await()
keyboardViews[mode] = keyboardView
withContext(Dispatchers.Main) {
textViewFlipper?.addView(keyboardView)
@@ -148,7 +130,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* Sets up the newly registered input view.
*/
override fun onRegisterInputView(inputView: InputView) {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onRegisterInputView(inputView)")
Timber.i("onRegisterInputView(inputView)")
launch(Dispatchers.Default) {
textViewGroup = inputView.findViewById(R.id.text_input)
@@ -161,91 +143,101 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
setActiveKeyboardMode(activeKeyboardMode)
}
for (mode in KeyboardMode.values()) {
if (mode != activeKeyboardMode) {
if (mode != activeKeyboardMode && mode != KeyboardMode.SMARTBAR_NUMBER_ROW) {
addKeyboardView(mode)
}
}
}
}
fun registerSmartbarView(view: SmartbarView) {
smartbarView = view
smartbarView?.setEventListener(this)
}
/**
* Cancels all coroutines and cleans up.
*/
override fun onDestroy() {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onDestroy()")
Timber.i("onDestroy()")
cancel()
osHandler.removeCallbacksAndMessages(null)
layoutManager.onDestroy()
smartbarManager.onDestroy()
instance = null
}
/**
* Evaluates the [activeKeyboardMode], [keyVariation] and [isComposingEnabled] property values
* when starting to interact with a input editor. Also resets the composing texts and sets the
* initial caps mode accordingly.
* Evaluates the [activeKeyboardMode], [keyVariation] and [EditorInstance.isComposingEnabled]
* property values when starting to interact with a input editor. Also resets the composing
* texts and sets the initial caps mode accordingly.
*/
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
val keyboardMode = when (info) {
null -> KeyboardMode.CHARACTERS
else -> when (info.inputType and InputType.TYPE_MASK_CLASS) {
InputType.TYPE_CLASS_NUMBER -> {
keyVariation = KeyVariation.NORMAL
KeyboardMode.NUMERIC
}
InputType.TYPE_CLASS_PHONE -> {
keyVariation = KeyVariation.NORMAL
KeyboardMode.PHONE
}
InputType.TYPE_CLASS_TEXT -> {
keyVariation = when (info.inputType and InputType.TYPE_MASK_VARIATION) {
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS -> {
KeyVariation.EMAIL_ADDRESS
}
InputType.TYPE_TEXT_VARIATION_PASSWORD,
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD -> {
KeyVariation.PASSWORD
}
InputType.TYPE_TEXT_VARIATION_URI -> {
KeyVariation.URI
}
else -> {
KeyVariation.NORMAL
}
override fun onStartInputView(instance: EditorInstance, restarting: Boolean) {
val keyboardMode = when (instance.inputAttributes.type) {
InputAttributes.Type.NUMBER -> {
keyVariation = KeyVariation.NORMAL
KeyboardMode.NUMERIC
}
InputAttributes.Type.PHONE -> {
keyVariation = KeyVariation.NORMAL
KeyboardMode.PHONE
}
InputAttributes.Type.TEXT -> {
keyVariation = when (instance.inputAttributes.variation) {
InputAttributes.Variation.EMAIL_ADDRESS,
InputAttributes.Variation.WEB_EMAIL_ADDRESS -> {
KeyVariation.EMAIL_ADDRESS
}
InputAttributes.Variation.PASSWORD,
InputAttributes.Variation.VISIBLE_PASSWORD,
InputAttributes.Variation.WEB_PASSWORD -> {
KeyVariation.PASSWORD
}
InputAttributes.Variation.URI -> {
KeyVariation.URI
}
else -> {
KeyVariation.NORMAL
}
KeyboardMode.CHARACTERS
}
else -> {
keyVariation = KeyVariation.NORMAL
KeyboardMode.CHARACTERS
}
KeyboardMode.CHARACTERS
}
else -> {
keyVariation = KeyVariation.NORMAL
KeyboardMode.CHARACTERS
}
}
isComposingEnabled = when (keyboardMode) {
KeyboardMode.NUMERIC,
KeyboardMode.PHONE,
KeyboardMode.PHONE2 -> false
else -> keyVariation != KeyVariation.PASSWORD && florisboard.prefs.suggestion.enabled
instance.apply {
isComposingEnabled = when (keyboardMode) {
KeyboardMode.NUMERIC,
KeyboardMode.PHONE,
KeyboardMode.PHONE2 -> false
else -> keyVariation != KeyVariation.PASSWORD &&
florisboard.prefs.suggestion.enabled// &&
//!instance.inputAttributes.flagTextAutoComplete &&
//!instance.inputAttributes.flagTextNoSuggestions
}
isPrivateMode = florisboard.prefs.advanced.forcePrivateMode ||
imeOptions.flagNoPersonalizedLearning
}
if (!florisboard.prefs.correction.rememberCapsLockState) {
capsLock = false
}
updateCapsState()
resetComposingText()
setActiveKeyboardMode(keyboardMode)
smartbarManager.onStartInputView(keyboardMode, isComposingEnabled)
smartbarView?.updateSmartbarState()
}
/**
* Handle stuff when finishing to interact with a input editor.
*/
override fun onFinishInputView(finishingInput: Boolean) {
smartbarManager.onFinishInputView()
smartbarView?.updateSmartbarState()
}
override fun onWindowShown() {
keyboardViews[KeyboardMode.CHARACTERS]?.updateVisibility()
smartbarManager.onWindowShown()
smartbarView?.updateSmartbarState()
}
/**
@@ -258,7 +250,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
}
/**
* Sets [activeKeyboardMode] and updates the [SmartbarManager.isQuickActionsVisible].
* Sets [activeKeyboardMode] and updates the [SmartbarView.isQuickActionsVisible] state.
*/
fun setActiveKeyboardMode(mode: KeyboardMode) {
textViewFlipper?.displayedChild = textViewFlipper?.indexOfChild(when (mode) {
@@ -269,233 +261,147 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
keyboardViews[mode]?.requestLayout()
keyboardViews[mode]?.requestLayoutAllKeys()
activeKeyboardMode = mode
smartbarManager.isQuickActionsVisible = false
isManualSelectionMode = false
isManualSelectionModeLeft = false
isManualSelectionModeRight = false
smartbarView?.isQuickActionsVisible = false
smartbarView?.updateSmartbarState()
}
override fun onSubtypeChanged(newSubtype: Subtype) {
launch {
val keyboardView = keyboardViews[KeyboardMode.CHARACTERS]
keyboardView?.computedLayout = layoutManager.fetchComputedLayoutAsync(KeyboardMode.CHARACTERS, newSubtype).await()
keyboardView?.computedLayout = layoutManager.fetchComputedLayoutAsync(KeyboardMode.CHARACTERS, newSubtype, florisboard.prefs).await()
keyboardView?.updateVisibility()
}
}
/**
* Main logic point for processing cursor updates as well as parsing the current composing word
* and passing this info on to the [SmartbarManager] to turn it into candidate suggestions.
* and passing this info on to the [SmartbarView] to turn it into candidate suggestions.
*/
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
cursorAnchorInfo ?: return
lastCursorAnchorInfo = cursorAnchorInfo
val ic = florisboard.currentInputConnection
val isNewSelectionInBoundsOfOld =
cursorAnchorInfo.selectionStart >= (selectionStart - 1) &&
cursorAnchorInfo.selectionStart <= (selectionStart + 1) &&
cursorAnchorInfo.selectionEnd >= (selectionEnd - 1) &&
cursorAnchorInfo.selectionEnd <= (selectionEnd + 1)
selectionStart = cursorAnchorInfo.selectionStart
selectionEnd = cursorAnchorInfo.selectionEnd
val inputText =
(ic?.getExtractedText(ExtractedTextRequest(), 0)?.text ?: "").toString()
selectionEndMax = inputText.length
if (isComposingEnabled) {
if (!isTextSelected) {
val newCursorPos = cursorAnchorInfo.selectionStart
val prevComposingText = (cursorAnchorInfo.composingText ?: "").toString()
setComposingTextBasedOnInput(inputText, newCursorPos)
if ((newCursorPos == cursorPos) && (composingText == prevComposingText)) {
// Ignore this, as nothing has changed
} else {
cursorPos = newCursorPos
if (composingText != null && composingTextStart != null) {
ic?.setComposingRegion(
composingTextStart!!,
composingTextStart!! + composingText!!.length
)
} else {
resetComposingText()
}
}
} else {
resetComposingText()
}
smartbarManager.generateCandidatesFromComposing(composingText)
}
if (!isNewSelectionInBoundsOfOld) {
override fun onUpdateSelection() {
if (!activeEditorInstance.isNewSelectionInBoundsOfOld) {
isManualSelectionMode = false
isManualSelectionModeLeft = false
isManualSelectionModeRight = false
}
updateCapsState()
smartbarManager.onUpdateCursorAnchorInfo(cursorAnchorInfo)
smartbarView?.updateSmartbarState()
}
override fun onPrimaryClipChanged() {
smartbarView?.onPrimaryClipChanged()
}
/**
* Resets the [composingText] and [composingTextStart] properties. Does NOT sync with
* [SmartbarManager]!
*
* @param notifyInputConnection If the current input connection should be notified.
*/
private fun resetComposingText(notifyInputConnection: Boolean = true) {
if (notifyInputConnection) {
val ic = florisboard.currentInputConnection
ic?.finishComposingText()
}
composingText = null
composingTextStart = null
}
/**
* Tries to parse the [composingText] from a given [inputCursorPos] within [inputText].
* Sets both [composingText] and [composingTextStart] to null if it fails, else to its
* parsed values.
*
* @param inputText The input text to search in.
* @param inputCursorPos The position where to search in [inputText].
*/
private fun setComposingTextBasedOnInput(inputText: String, inputCursorPos: Int) {
val words = inputText.split("[^\\p{L}]".toRegex())
var pos = 0
resetComposingText(false)
for (word in words) {
if (inputCursorPos >= pos && inputCursorPos <= pos + word.length && word.isNotEmpty()) {
composingText = word
composingTextStart = pos
break
} else {
pos += word.length + 1
}
}
}
/**
* Should primarily pe used by [SmartbarManager.candidateViewOnClickListener] to commit
* a candidate if a user has pressed on it.
*/
fun commitCandidate(candidateText: String) {
val ic = florisboard.currentInputConnection
ic?.setComposingText(candidateText, 1)
ic?.finishComposingText()
}
/**
* Parses the [CapsMode] out of the given [flags].
*
* @param flags The input flags.
* @return A [CapsMode] value.
*/
private fun parseCapsModeFromFlags(flags: Int): CapsMode {
return when {
flags and InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS > 0 -> {
CapsMode.ALL
}
flags and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES > 0 -> {
CapsMode.SENTENCES
}
flags and InputType.TYPE_TEXT_FLAG_CAP_WORDS > 0 -> {
CapsMode.WORDS
}
else -> {
CapsMode.NONE
}
}
}
/**
* Fetches the current cursor caps mode from the current input connection.
*
* @return The [CapsMode] according to the returned flags by the current input connection.
*/
private fun fetchCurrentCursorCapsMode(): CapsMode {
val ic = florisboard.currentInputConnection
val info = florisboard.currentInputEditorInfo
val capsFlags = ic?.getCursorCapsMode(info.inputType) ?: 0
return parseCapsModeFromFlags(capsFlags)
}
/**
* Updates the current caps state according to the [cursorCapsMode], while respecting
* [capsLock] property.
* Updates the current caps state according to the [EditorInstance.cursorCapsMode], while
* respecting [capsLock] property and the correction.autoCapitalization preference.
*/
private fun updateCapsState() {
cursorCapsMode = fetchCurrentCursorCapsMode()
editorCapsMode = parseCapsModeFromFlags(florisboard.currentInputEditorInfo.inputType)
if (!capsLock) {
caps = cursorCapsMode != CapsMode.NONE
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
caps = florisboard.prefs.correction.autoCapitalization &&
activeEditorInstance.cursorCapsMode != InputAttributes.CapsMode.NONE
launch(Dispatchers.Main) {
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
}
}
}
/**
* Sends a given [keyCode] as a [KeyEvent.ACTION_DOWN].
*
* @param ic The input connection on which this operation should be performed.
* @param keyCode The key code to send, use a key code defined in Android's [KeyEvent], not in
* [KeyCode] or this call may send a weird character, as this key codes do not match!!
* Executes a given [SwipeAction]. Ignores any [SwipeAction] but the ones relevant for this
* class.
*/
private fun sendSystemKeyEvent(ic: InputConnection?, keyCode: Int) {
ic?.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, keyCode))
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 -> {}
}
}
/**
* Sends a given [keyCode] as a [KeyEvent.ACTION_DOWN] with ALT pressed.
*
* @param ic The input connection on which this operation should be performed.
* @param keyCode The key code to send, use a key code defined in Android's [KeyEvent], not in
* [KeyCode] or this call may send a weird character, as this key codes do not match!!
*/
private fun sendSystemKeyEventAlt(ic: InputConnection?, keyCode: Int) {
ic?.sendKeyEvent(
KeyEvent(
0,
1,
KeyEvent.ACTION_DOWN, keyCode,
0,
KeyEvent.META_ALT_LEFT_ON
)
)
override fun onSmartbarBackButtonPressed() {
setActiveKeyboardMode(KeyboardMode.CHARACTERS)
}
override fun onSmartbarPrivateModeButtonClicked() {
Toast.makeText(florisboard.context, R.string.private_mode_dialog__title, Toast.LENGTH_LONG).show()
}
override fun onSmartbarQuickActionPressed(quickActionId: Int) {
when (quickActionId) {
R.id.quick_action_switch_to_editing_context -> {
if (activeKeyboardMode == KeyboardMode.EDITING) {
setActiveKeyboardMode(KeyboardMode.CHARACTERS)
} else {
setActiveKeyboardMode(KeyboardMode.EDITING)
}
}
R.id.quick_action_switch_to_media_context -> florisboard.setActiveInput(R.id.media_input)
R.id.quick_action_open_settings -> florisboard.launchSettings()
R.id.quick_action_one_handed_toggle -> florisboard.toggleOneHandedMode()
R.id.quick_action_undo -> {
handleUndo()
return
}
R.id.quick_action_redo -> {
handleRedo()
return
}
}
smartbarView?.isQuickActionsVisible = false
smartbarView?.updateSmartbarState()
}
private fun handleUndo(){
activeEditorInstance.performUndo()
}
private fun handleRedo(){
activeEditorInstance.performRedo()
}
/**
* Handles a [KeyCode.DELETE] event.
*/
private fun handleDelete() {
val ic = florisboard.currentInputConnection
ic?.beginBatchEdit()
resetComposingText()
isManualSelectionMode = false
isManualSelectionModeLeft = false
isManualSelectionModeRight = false
sendSystemKeyEvent(ic, KeyEvent.KEYCODE_DEL)
ic?.endBatchEdit()
activeEditorInstance.deleteBackwards()
}
/**
* Handles a [KeyCode.DELETE_WORD] event.
*/
private fun handleDeleteWord() {
isManualSelectionMode = false
isManualSelectionModeLeft = false
isManualSelectionModeRight = false
activeEditorInstance.deleteWordsBeforeCursor(1)
}
/**
* Handles a [KeyCode.ENTER] event.
*/
private fun handleEnter() {
val ic = florisboard.currentInputConnection
resetComposingText()
val action = florisboard.currentInputEditorInfo?.imeOptions ?: 0
val actionMasked = action and EditorInfo.IME_MASK_ACTION
if (action and EditorInfo.IME_FLAG_NO_ENTER_ACTION > 0) {
sendSystemKeyEvent(ic, KeyEvent.KEYCODE_ENTER)
if (activeEditorInstance.imeOptions.flagNoEnterAction) {
activeEditorInstance.performEnter()
} else {
when (actionMasked) {
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(actionMasked)
when (activeEditorInstance.imeOptions.action) {
ImeOptions.Action.DONE,
ImeOptions.Action.GO,
ImeOptions.Action.NEXT,
ImeOptions.Action.PREVIOUS,
ImeOptions.Action.SEARCH,
ImeOptions.Action.SEND -> {
activeEditorInstance.performEnterAction(activeEditorInstance.imeOptions.action)
}
else -> sendSystemKeyEvent(ic, KeyEvent.KEYCODE_ENTER)
else -> activeEditorInstance.performEnter()
}
}
}
@@ -525,14 +431,13 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* enabled by the user.
*/
private fun handleSpace() {
val ic = florisboard.currentInputConnection
if (florisboard.prefs.correction.doubleSpacePeriod) {
if (hasSpaceRecentlyPressed) {
osHandler.removeCallbacksAndMessages(null)
val text = ic?.getTextBeforeCursor(2, 0) ?: ""
val text = activeEditorInstance.getTextBeforeCursor(2)
if (text.length == 2 && !text.matches("""[.!?‽\s][\s]""".toRegex())) {
ic?.deleteSurroundingText(1, 0)
ic?.commitText(".", 1)
activeEditorInstance.deleteBackwards()
activeEditorInstance.commitText(".")
}
hasSpaceRecentlyPressed = false
} else {
@@ -542,107 +447,107 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
}, 300)
}
}
ic?.commitText(KeyCode.SPACE.toChar().toString(), 1)
activeEditorInstance.commitText(KeyCode.SPACE.toChar().toString())
}
/**
* Handles [KeyCode] arrow and move events, behaves differently depending on text selection.
*/
private fun handleArrow(code: Int) {
val ic = florisboard.currentInputConnection
resetComposingText()
if (isTextSelected && isManualSelectionMode) {
private fun handleArrow(code: Int) = activeEditorInstance.apply {
val selectionStartMin = 0
val selectionEndMax = cachedText.length
if (selection.isSelectionMode && isManualSelectionMode) {
// Text is selected and it is manual selection -> Expand selection depending on started
// direction.
when (code) {
KeyCode.ARROW_DOWN -> {}
KeyCode.ARROW_LEFT -> {
if (isManualSelectionModeLeft) {
ic?.setSelection(
(selectionStart - 1).coerceAtLeast(selectionStartMin),
selectionEnd
setSelection(
(selection.start - 1).coerceAtLeast(selectionStartMin),
selection.end
)
} else {
ic?.setSelection(selectionStart, selectionEnd - 1)
setSelection(selection.start, selection.end - 1)
}
}
KeyCode.ARROW_RIGHT -> {
if (isManualSelectionModeRight) {
ic?.setSelection(
selectionStart,
(selectionEnd + 1).coerceAtMost(selectionEndMax)
setSelection(
selection.start,
(selection.end + 1).coerceAtMost(selectionEndMax)
)
} else {
ic?.setSelection(selectionStart + 1, selectionEnd)
setSelection(selection.start + 1, selection.end)
}
}
KeyCode.ARROW_UP -> {}
KeyCode.MOVE_HOME -> {
if (isManualSelectionModeLeft) {
ic?.setSelection(selectionStartMin, selectionEnd)
setSelection(selectionStartMin, selection.end)
} else {
ic?.setSelection(selectionStartMin, selectionStart)
setSelection(selectionStartMin, selection.start)
}
}
KeyCode.MOVE_END -> {
if (isManualSelectionModeRight) {
ic?.setSelection(selectionStart, selectionEndMax)
setSelection(selection.start, selectionEndMax)
} else {
ic?.setSelection(selectionEnd, selectionEndMax)
setSelection(selection.end, selectionEndMax)
}
}
}
} else if (isTextSelected && !isManualSelectionMode) {
} else if (selection.isSelectionMode && !isManualSelectionMode) {
// Text is selected but no manual selection mode -> arrows behave as if selection was
// started in manual left mode
when (code) {
KeyCode.ARROW_DOWN -> {}
KeyCode.ARROW_LEFT -> {
ic?.setSelection(selectionStart, selectionEnd - 1)
setSelection(selection.start, selection.end - 1)
}
KeyCode.ARROW_RIGHT -> {
ic?.setSelection(
selectionStart,
(selectionEnd + 1).coerceAtMost(selectionEndMax)
setSelection(
selection.start,
(selection.end + 1).coerceAtMost(selectionEndMax)
)
}
KeyCode.ARROW_UP -> {}
KeyCode.MOVE_HOME -> {
ic?.setSelection(selectionStartMin, selectionStart)
setSelection(selectionStartMin, selection.start)
}
KeyCode.MOVE_END -> {
ic?.setSelection(selectionStart, selectionEndMax)
setSelection(selection.start, selectionEndMax)
}
}
} else if (!isTextSelected && isManualSelectionMode) {
} else if (!selection.isSelectionMode && isManualSelectionMode) {
// No text is selected but manual selection mode is active, user wants to start a new
// selection. Must set manual selection direction.
when (code) {
KeyCode.ARROW_DOWN -> {}
KeyCode.ARROW_LEFT -> {
ic?.setSelection(
(selectionStart - 1).coerceAtLeast(selectionStartMin),
selectionStart
setSelection(
(selection.start - 1).coerceAtLeast(selectionStartMin),
selection.start
)
isManualSelectionModeLeft = true
isManualSelectionModeRight = false
}
KeyCode.ARROW_RIGHT -> {
ic?.setSelection(
selectionEnd,
(selectionEnd + 1).coerceAtMost(selectionEndMax)
setSelection(
selection.end,
(selection.end + 1).coerceAtMost(selectionEndMax)
)
isManualSelectionModeLeft = false
isManualSelectionModeRight = true
}
KeyCode.ARROW_UP -> {}
KeyCode.MOVE_HOME -> {
ic?.setSelection(selectionStartMin, selectionStart)
setSelection(selectionStartMin, selection.start)
isManualSelectionModeLeft = true
isManualSelectionModeRight = false
}
KeyCode.MOVE_END -> {
ic?.setSelection(selectionEnd, selectionEndMax)
setSelection(selection.end, selectionEndMax)
isManualSelectionModeLeft = false
isManualSelectionModeRight = true
}
@@ -650,87 +555,39 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
} else {
// No selection and no manual selection mode -> move cursor around
when (code) {
KeyCode.ARROW_DOWN -> sendSystemKeyEvent(ic, KeyEvent.KEYCODE_DPAD_DOWN)
KeyCode.ARROW_LEFT -> sendSystemKeyEvent(ic, KeyEvent.KEYCODE_DPAD_LEFT)
KeyCode.ARROW_RIGHT -> sendSystemKeyEvent(ic, KeyEvent.KEYCODE_DPAD_RIGHT)
KeyCode.ARROW_UP -> sendSystemKeyEvent(ic, KeyEvent.KEYCODE_DPAD_UP)
KeyCode.MOVE_HOME -> sendSystemKeyEventAlt(ic, KeyEvent.KEYCODE_DPAD_UP)
KeyCode.MOVE_END -> sendSystemKeyEventAlt(ic, KeyEvent.KEYCODE_DPAD_DOWN)
KeyCode.ARROW_DOWN -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN)
KeyCode.ARROW_LEFT -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT)
KeyCode.ARROW_RIGHT -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT)
KeyCode.ARROW_UP -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_UP)
KeyCode.MOVE_HOME -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_UP)
KeyCode.MOVE_END -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_DOWN)
}
}
}
/**
* Handles a [KeyCode.CLIPBOARD_CUT] event.
* TODO: handle other data than text too, e.g. Uri, Intent, ...
*/
private fun handleClipboardCut() {
val ic = florisboard.currentInputConnection
val selectedText = ic?.getSelectedText(0)
if (selectedText != null) {
florisboard.clipboardManager
?.setPrimaryClip(ClipData.newPlainText(selectedText, selectedText))
}
resetComposingText()
ic?.commitText("", 1)
}
/**
* Handles a [KeyCode.CLIPBOARD_COPY] event.
* TODO: handle other data than text too, e.g. Uri, Intent, ...
*/
private fun handleClipboardCopy() {
val ic = florisboard.currentInputConnection
val selectedText = ic?.getSelectedText(0)
if (selectedText != null) {
florisboard.clipboardManager
?.setPrimaryClip(ClipData.newPlainText(selectedText, selectedText))
}
resetComposingText()
ic?.setSelection(selectionEnd, selectionEnd)
}
/**
* Handles a [KeyCode.CLIPBOARD_PASTE] event.
* TODO: handle other data than text too, e.g. Uri, Intent, ...
*/
private fun handleClipboardPaste() {
val ic = florisboard.currentInputConnection
val item = florisboard.clipboardManager?.primaryClip?.getItemAt(0)
val pasteText = item?.text
if (pasteText != null) {
resetComposingText()
ic?.commitText(pasteText, 1)
}
}
/**
* Handles a [KeyCode.CLIPBOARD_SELECT] event.
*/
private fun handleClipboardSelect() {
val ic = florisboard.currentInputConnection
resetComposingText()
if (isTextSelected) {
private fun handleClipboardSelect() = activeEditorInstance.apply {
if (selection.isSelectionMode) {
if (isManualSelectionMode && isManualSelectionModeLeft) {
ic?.setSelection(selectionStart, selectionStart)
setSelection(selection.start, selection.start)
} else {
ic?.setSelection(selectionEnd, selectionEnd)
setSelection(selection.end, selection.end)
}
isManualSelectionMode = false
} else {
isManualSelectionMode = !isManualSelectionMode
// Must recall to update UI properly
florisboard.onUpdateCursorAnchorInfo(lastCursorAnchorInfo)
// Must call to update UI properly
editingKeyboardView?.onUpdateSelection()
}
}
}
/**
* Handles a [KeyCode.CLIPBOARD_SELECT_ALL] event.
*/
private fun handleClipboardSelectAll() {
val ic = florisboard.currentInputConnection
resetComposingText()
ic?.setSelection(selectionStartMin, selectionEndMax)
activeEditorInstance.setSelection(0, activeEditorInstance.cachedText.length)
}
/**
@@ -741,8 +598,6 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* @param keyData The [KeyData] object which should be sent.
*/
fun sendKeyPress(keyData: KeyData) {
val ic = florisboard.currentInputConnection
when (keyData.code) {
KeyCode.ARROW_DOWN,
KeyCode.ARROW_LEFT,
@@ -750,13 +605,22 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
KeyCode.ARROW_UP,
KeyCode.MOVE_HOME,
KeyCode.MOVE_END -> handleArrow(keyData.code)
KeyCode.CLIPBOARD_CUT -> handleClipboardCut()
KeyCode.CLIPBOARD_COPY -> handleClipboardCopy()
KeyCode.CLIPBOARD_PASTE -> handleClipboardPaste()
KeyCode.CLIPBOARD_CUT -> activeEditorInstance.performClipboardCut()
KeyCode.CLIPBOARD_COPY -> activeEditorInstance.performClipboardCopy()
KeyCode.CLIPBOARD_PASTE -> {
activeEditorInstance.performClipboardPaste()
smartbarView?.resetClipboardSuggestion()
}
KeyCode.CLIPBOARD_SELECT -> handleClipboardSelect()
KeyCode.CLIPBOARD_SELECT_ALL -> handleClipboardSelectAll()
KeyCode.DELETE -> handleDelete()
KeyCode.ENTER -> handleEnter()
KeyCode.DELETE -> {
handleDelete()
smartbarView?.resetClipboardSuggestion()
}
KeyCode.ENTER -> {
handleEnter()
smartbarView?.resetClipboardSuggestion()
}
KeyCode.LANGUAGE_SWITCH -> florisboard.switchToNextSubtype()
KeyCode.SETTINGS -> florisboard.launchSettings()
KeyCode.SHIFT -> handleShift()
@@ -776,8 +640,6 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
KeyCode.VIEW_SYMBOLS -> setActiveKeyboardMode(KeyboardMode.SYMBOLS)
KeyCode.VIEW_SYMBOLS2 -> setActiveKeyboardMode(KeyboardMode.SYMBOLS2)
else -> {
ic?.beginBatchEdit()
resetComposingText()
when (activeKeyboardMode) {
KeyboardMode.NUMERIC,
KeyboardMode.NUMERIC_ADVANCED,
@@ -786,13 +648,13 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
KeyType.CHARACTER,
KeyType.NUMERIC -> {
val text = keyData.code.toChar().toString()
ic?.commitText(text, 1)
activeEditorInstance.commitText(text)
}
else -> when (keyData.code) {
KeyCode.PHONE_PAUSE,
KeyCode.PHONE_WAIT -> {
val text = keyData.code.toChar().toString()
ic?.commitText(text, 1)
activeEditorInstance.commitText(text)
}
}
}
@@ -804,7 +666,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
true -> keyData.label.toUpperCase(Locale.getDefault())
false -> keyData.label.toLowerCase(Locale.getDefault())
}
ic?.commitText(tld, 1)
activeEditorInstance.commitText(tld)
}
else -> {
var text = keyData.code.toChar().toString()
@@ -812,26 +674,20 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
true -> text.toUpperCase(Locale.getDefault())
false -> text.toLowerCase(Locale.getDefault())
}
ic?.commitText(text, 1)
activeEditorInstance.commitText(text)
}
}
else -> {
Log.e(
this::class.simpleName,
"sendKeyPress(keyData): Received unknown key: $keyData"
)
Timber.e("sendKeyPress(keyData): Received unknown key: $keyData")
}
}
}
ic?.endBatchEdit()
smartbarView?.resetClipboardSuggestion()
}
}
}
enum class CapsMode {
ALL,
NONE,
SENTENCES,
WORDS;
if (keyData.code != KeyCode.SHIFT && !capsLock) {
updateCapsState()
}
smartbarView?.updateSmartbarState()
}
}

View File

@@ -23,6 +23,7 @@ import android.content.res.Configuration
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Typeface
import android.os.Handler
import android.util.AttributeSet
import android.view.MotionEvent
import android.widget.Button
@@ -32,7 +33,8 @@ 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.*
import dev.patrickgold.florisboard.util.cancelAll
import dev.patrickgold.florisboard.util.postAtScheduledRate
/**
* View class for managing and rendering an editing key.
@@ -42,7 +44,7 @@ class EditingKeyView : AppCompatImageButton {
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
private val data: KeyData
private var isKeyPressed: Boolean = false
private var osTimer: Timer? = null
private val repeatedKeyPressHandler: Handler = Handler(context.mainLooper)
private var label: String? = null
private var labelPaint: Paint = Paint().apply {
@@ -100,23 +102,20 @@ class EditingKeyView : AppCompatImageButton {
KeyCode.ARROW_RIGHT,
KeyCode.ARROW_UP,
KeyCode.DELETE -> {
osTimer = Timer()
osTimer?.scheduleAtFixedRate(object : TimerTask() {
override fun run() {
val delayMillis = prefs.keyboard.longPressDelay.toLong()
repeatedKeyPressHandler.postAtScheduledRate(delayMillis, 25) {
if (isKeyPressed) {
florisboard?.textInputManager?.sendKeyPress(data)
if (!isKeyPressed) {
osTimer?.cancel()
osTimer = null
}
} else {
repeatedKeyPressHandler.cancelAll()
}
}, 500, 50)
}
}
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
isKeyPressed = false
osTimer?.cancel()
osTimer = null
repeatedKeyPressHandler.cancelAll()
if (event.actionMasked != MotionEvent.ACTION_CANCEL) {
florisboard?.textInputManager?.sendKeyPress(data)
}

View File

@@ -20,12 +20,12 @@ import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import android.view.View
import android.view.inputmethod.CursorAnchorInfo
import androidx.constraintlayout.widget.ConstraintLayout
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.util.setBackgroundTintColor2
import kotlin.math.roundToInt
/**
* View class for updating the key views depending on the current selection and clipboard state.
@@ -60,8 +60,8 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener {
pasteKey = findViewById(R.id.clipboard_paste)
}
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
val isSelectionActive = florisboard?.textInputManager?.isTextSelected ?: false
override fun onUpdateSelection() {
val isSelectionActive = florisboard?.activeEditorInstance?.selection?.isSelectionMode ?: false
val isSelectionMode = florisboard?.textInputManager?.isManualSelectionMode ?: false
arrowUpKey?.isEnabled = !(isSelectionActive || isSelectionMode)
arrowDownKey?.isEnabled = !(isSelectionActive || isSelectionMode)
@@ -80,7 +80,7 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec).toFloat()
val height = when (heightMode) {
MeasureSpec.EXACTLY -> {
// Must be this size
@@ -88,15 +88,15 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener {
}
MeasureSpec.AT_MOST -> {
// Can't be bigger than...
(florisboard?.inputView?.desiredTextKeyboardViewHeight ?: 0).coerceAtMost(heightSize)
(florisboard?.inputView?.desiredTextKeyboardViewHeight ?: 0.0f).coerceAtMost(heightSize)
}
else -> {
// Be whatever you want
florisboard?.inputView?.desiredTextKeyboardViewHeight ?: 0
florisboard?.inputView?.desiredTextKeyboardViewHeight ?: 0.0f
}
}
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY))
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height.roundToInt(), MeasureSpec.EXACTLY))
}
override fun onDraw(canvas: Canvas?) {

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

View File

@@ -0,0 +1,40 @@
/*
* Copyright (C) 2020 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.text.gestures
import java.util.*
/**
* Enum for declaring the velocity thresholds for swipe gestures.
*/
enum class VelocityThreshold {
VERY_SLOW,
SLOW,
NORMAL,
FAST,
VERY_FAST;
companion object {
fun fromString(string: String): VelocityThreshold {
return valueOf(string.toUpperCase(Locale.ROOT))
}
}
override fun toString(): String {
return super.toString().toLowerCase(Locale.ROOT)
}
}

View File

@@ -1,3 +1,19 @@
/*
* Copyright (C) 2020 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.text.key
object KeyCode {

View File

@@ -1,8 +1,47 @@
/*
* Copyright (C) 2020 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.text.key
/**
* Data class which describes a single key and its variants.
*
* @property code The UTF-8 encoded code of the character. The code defined here is used as the
* data passed to the system.
* @property label The string used to display the key in the UI. Is not used for the actual data
* passed to the system. Should normally be the exact same as the [code]. Defaults to an empty
* string.
* @property hintedNumber The hinted number which will be dynamically inserted into the long-press
* [popup]. Leave null to disable the hinted popup for this key. The visibility of the hinted number
* is controlled by the preferences. Defaults to null.
* @property hintedSymbol The hinted symbol which will be dynamically inserted into the long-press
* [popup]. Leave null to disable the hinted popup for this key. The visibility of the hinted symbol
* is controlled by the preferences. Defaults to null.
* @property popup List of keys which will be accessible while long pressing the key. Defaults to
* an empty list (no extended popup).
* @property type The type of the key. Some actions require both [code] and [type] to match in order
* to be successfully executed. Defaults to [KeyType.CHARACTER].
* @property variation Controls if the key should only be shown in some contexts (e.g.: url input)
* or if the key should always be visible. Defaults to [KeyVariation.ALL].
*/
data class KeyData(
var code: Int,
var label: String = "",
var hintedNumber: KeyData? = null,
var hintedSymbol: KeyData? = null,
var popup: MutableList<KeyData> = mutableListOf(),
var type: KeyType = KeyType.CHARACTER,
var variation: KeyVariation = KeyVariation.ALL

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2020 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.text.key
import java.util.*
/**
* Enum for the key hint modes.
*/
enum class KeyHintMode {
DISABLED,
ENABLED_HINT_PRIORITY,
ENABLED_ACCENT_PRIORITY,
ENABLED_SMART_PRIORITY;
companion object {
fun fromString(string: String): KeyHintMode {
return valueOf(string.toUpperCase(Locale.ROOT))
}
}
override fun toString(): String {
return super.toString().toLowerCase(Locale.ROOT)
}
}

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

@@ -25,7 +25,6 @@ import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.ViewOutlineProvider
import android.view.inputmethod.EditorInfo
import androidx.core.content.ContextCompat.getDrawable
import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
@@ -33,9 +32,15 @@ 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.cancelAll
import dev.patrickgold.florisboard.util.postAtScheduledRate
import dev.patrickgold.florisboard.util.postDelayed
import dev.patrickgold.florisboard.util.setBackgroundTintColor2
import java.util.*
@@ -51,33 +56,49 @@ import java.util.*
class KeyView(
private val keyboardView: KeyboardView,
val data: KeyData
) : View(keyboardView.context) {
) : View(keyboardView.context), SwipeGesture.Listener {
val dataPopupWithHint: MutableList<KeyData>
private var isKeyPressed: Boolean = false
set(value) {
field = value
updateKeyPressedBackground()
}
private var osHandler: Handler? = null
private var osTimer: Timer? = null
private var hasTriggeredGestureMove: Boolean = false
private val longKeyPressHandler: Handler = Handler(context.mainLooper)
private val repeatedKeyPressHandler: Handler = Handler(context.mainLooper)
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
private var shouldBlockNextKeyCode: Boolean = false
private var desiredWidth: Int = 0
private var desiredHeight: Int = 0
private var drawable: Drawable? = null
private var drawableColor: Int = 0
private var drawablePadding: Int = 0
private var drawablePaddingH: Int = 0
private var drawablePaddingV: Int = 0
private var label: String? = null
private var labelPaint: Paint = Paint().apply {
alpha = 255
color = 0
isAntiAlias = true
isFakeBoldText = true
isFakeBoldText = false
textAlign = Paint.Align.CENTER
textSize = resources.getDimension(R.dimen.key_textSize)
typeface = Typeface.DEFAULT
}
private var hintedLabel: String? = null
private var hintedLabelPaint: Paint = Paint().apply {
alpha = 120
color = 0
isAntiAlias = true
isFakeBoldText = false
textAlign = Paint.Align.CENTER
textSize = resources.getDimension(R.dimen.key_textHintSize)
typeface = Typeface.DEFAULT
}
private val tempRect: Rect = Rect()
var florisboard: FlorisBoard? = null
private val swipeGestureDetector = SwipeGesture.Detector(context, this)
var touchHitBox: Rect = Rect(-1, -1, -1, -1)
init {
@@ -124,6 +145,30 @@ class KeyView(
background = getDrawable(context, R.drawable.shape_rect_rounded)
elevation = 4.0f
var hintKeyData: KeyData? = null
var hintKeyMode: KeyHintMode = KeyHintMode.DISABLED
val hintedNumber = data.hintedNumber
if (prefs.keyboard.hintedNumberRowMode != KeyHintMode.DISABLED && hintedNumber != null) {
hintKeyData = hintedNumber
hintKeyMode = prefs.keyboard.hintedNumberRowMode
}
val hintedSymbol = data.hintedSymbol
if (prefs.keyboard.hintedSymbolsMode != KeyHintMode.DISABLED && hintedSymbol != null) {
hintKeyData = hintedSymbol
hintKeyMode = prefs.keyboard.hintedSymbolsMode
}
dataPopupWithHint = if (hintKeyData == null) {
data.popup.toMutableList()
} else {
val popupList = data.popup.toMutableList()
if (hintKeyMode == KeyHintMode.ENABLED_HINT_PRIORITY) {
popupList.add(0, hintKeyData)
} else {
popupList.add(hintKeyData)
}
popupList
}
updateKeyPressedBackground()
}
@@ -172,9 +217,19 @@ class KeyView(
* go look at which child the pointer is actually above.
*/
fun onFlorisTouchEvent(event: MotionEvent?): Boolean {
event ?: return false
if (event == null || !isEnabled) return false
if (swipeGestureDetector.onTouchEvent(event)) {
isKeyPressed = false
longKeyPressHandler.cancelAll()
repeatedKeyPressHandler.cancelAll()
keyboardView.popupManager.hide()
return true
}
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
val delayMillis = prefs.keyboard.longPressDelay.toLong()
hasTriggeredGestureMove = false
shouldBlockNextKeyCode = false
florisboard?.prefs?.keyboard?.let {
if (it.popupEnabled){
keyboardView.popupManager.show(this)
@@ -183,24 +238,23 @@ class KeyView(
isKeyPressed = true
florisboard?.keyPressVibrate()
florisboard?.keyPressSound(data)
if (data.code == KeyCode.DELETE && data.type == KeyType.ENTER_EDITING) {
osTimer = Timer()
osTimer?.scheduleAtFixedRate(object : TimerTask() {
override fun run() {
florisboard?.textInputManager?.sendKeyPress(data)
if (!isKeyPressed) {
osTimer?.cancel()
osTimer = null
when (data.code) {
KeyCode.ARROW_DOWN,
KeyCode.ARROW_LEFT,
KeyCode.ARROW_RIGHT,
KeyCode.ARROW_UP,
KeyCode.DELETE -> {
repeatedKeyPressHandler.postAtScheduledRate(delayMillis, 25) {
if (isKeyPressed) {
florisboard?.textInputManager?.sendKeyPress(data)
} else {
repeatedKeyPressHandler.cancelAll()
}
}
}, 500, 50)
}
}
val delayMillis = prefs.keyboard.longPressDelay
if (osHandler == null) {
osHandler = Handler()
}
osHandler?.postDelayed({
if (data.popup.isNotEmpty()) {
longKeyPressHandler.postDelayed(delayMillis) {
if (dataPopupWithHint.isNotEmpty()) {
keyboardView.popupManager.extend(this)
}
if (data.code == KeyCode.SPACE) {
@@ -212,7 +266,7 @@ class KeyView(
)
shouldBlockNextKeyCode = true
}
}, delayMillis.toLong())
}
}
MotionEvent.ACTION_MOVE -> {
if (keyboardView.popupManager.isShowingExtendedPopup) {
@@ -236,16 +290,23 @@ class KeyView(
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
isKeyPressed = false
osHandler?.removeCallbacksAndMessages(null)
osTimer?.cancel()
osTimer = null
val retData = keyboardView.popupManager.getActiveKeyData(this)
keyboardView.popupManager.hide()
if (event.actionMasked != MotionEvent.ACTION_CANCEL && !shouldBlockNextKeyCode && retData != null) {
florisboard?.textInputManager?.sendKeyPress(retData)
performClick()
longKeyPressHandler.cancelAll()
repeatedKeyPressHandler.cancelAll()
if (hasTriggeredGestureMove && data.code == KeyCode.DELETE) {
hasTriggeredGestureMove = false
florisboard?.activeEditorInstance?.apply {
if (selection.isSelectionMode) {
deleteBackwards()
}
}
} else {
shouldBlockNextKeyCode = false
val retData = keyboardView.popupManager.getActiveKeyData(this)
keyboardView.popupManager.hide()
if (event.actionMasked != MotionEvent.ACTION_CANCEL && !shouldBlockNextKeyCode && retData != null) {
florisboard?.textInputManager?.sendKeyPress(retData)
} else {
shouldBlockNextKeyCode = false
}
}
}
else -> return false
@@ -253,6 +314,82 @@ class KeyView(
return true
}
/**
* Swipe event handler. Listens to touch_move left/right swipes and triggers the swipe action
* defined in the prefs.
*/
override fun onSwipe(direction: SwipeGesture.Direction, type: SwipeGesture.Type): Boolean {
return when (data.code) {
KeyCode.DELETE -> when (type) {
SwipeGesture.Type.TOUCH_MOVE -> when (direction) {
SwipeGesture.Direction.LEFT -> when (prefs.gestures.deleteKeySwipeLeft) {
SwipeAction.DELETE_CHARACTERS_PRECISELY -> {
florisboard?.activeEditorInstance?.apply {
setSelection(
if (selection.start > 0) { selection.start - 1 } else { selection.start },
selection.end
)
}
hasTriggeredGestureMove = true
shouldBlockNextKeyCode = true
true
}
SwipeAction.DELETE_WORDS_PRECISELY -> {
florisboard?.activeEditorInstance?.apply {
leftAppendWordToSelection()
}
hasTriggeredGestureMove = true
shouldBlockNextKeyCode = true
true
}
else -> false
}
SwipeGesture.Direction.RIGHT -> when (prefs.gestures.deleteKeySwipeLeft) {
SwipeAction.DELETE_CHARACTERS_PRECISELY -> {
florisboard?.activeEditorInstance?.apply {
setSelection(
if (selection.start < selection.end) { selection.start + 1 } else { selection.start },
selection.end
)
}
shouldBlockNextKeyCode = true
true
}
SwipeAction.DELETE_WORDS_PRECISELY -> {
florisboard?.activeEditorInstance?.apply {
leftPopWordFromSelection()
}
shouldBlockNextKeyCode = true
true
}
else -> false
}
else -> false
}
else -> false
}
KeyCode.SPACE -> when (type) {
SwipeGesture.Type.TOUCH_MOVE -> when (direction) {
SwipeGesture.Direction.LEFT -> {
florisboard?.executeSwipeAction(prefs.gestures.spaceBarSwipeLeft)
shouldBlockNextKeyCode = true
true
}
SwipeGesture.Direction.RIGHT -> {
florisboard?.executeSwipeAction(prefs.gestures.spaceBarSwipeRight)
shouldBlockNextKeyCode = true
true
}
else -> false
}
else -> false
}
else -> false
}
}
/**
* Solution base from this great StackOverflow answer which explained and helped a lot
* for handling onMeasure():
@@ -260,7 +397,7 @@ class KeyView(
* by Devunwired
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val desiredWidth = when (keyboardView.computedLayout?.mode) {
desiredWidth = when (keyboardView.computedLayout?.mode) {
KeyboardMode.NUMERIC,
KeyboardMode.PHONE,
KeyboardMode.PHONE2 -> (keyboardView.desiredKeyWidth * 2.68f).toInt()
@@ -279,7 +416,7 @@ class KeyView(
else -> keyboardView.desiredKeyWidth
}
}
val desiredHeight = keyboardView.desiredKeyHeight
desiredHeight = keyboardView.desiredKeyHeight
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
@@ -318,7 +455,8 @@ class KeyView(
}
}
drawablePadding = (0.2f * height).toInt()
drawablePaddingH = (0.2f * width).toInt()
drawablePaddingV = (0.2f * height).toInt()
// MUST CALL THIS
setMeasuredDimension(width, height)
@@ -334,34 +472,69 @@ 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() {
when (data.code) {
KeyCode.ENTER -> {
when {
keyboardView.isSmartbarKeyboardView -> {
elevation = 0.0f
setBackgroundTintColor2(
this, when {
isKeyPressed -> prefs.theme.keyEnterBgColorPressed
else -> prefs.theme.keyEnterBgColor
}
)
}
KeyCode.SHIFT -> {
setBackgroundTintColor2(
this, when {
isKeyPressed -> prefs.theme.keyShiftBgColorPressed
else -> prefs.theme.keyShiftBgColor
isKeyPressed && isEnabled -> prefs.theme.smartbarButtonBgColor
else -> prefs.theme.smartbarBgColor
}
)
}
else -> {
setBackgroundTintColor2(
this, when {
isKeyPressed -> prefs.theme.keyBgColorPressed
else -> prefs.theme.keyBgColor
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
}
)
}
}
}
}
}
@@ -398,6 +571,7 @@ class KeyView(
* TextInputManager.
*/
fun updateVisibility() {
updateEnabledState()
when (data.code) {
KeyCode.SWITCH_TO_TEXT_CONTEXT,
KeyCode.SWITCH_TO_MEDIA_CONTEXT -> {
@@ -431,6 +605,39 @@ class KeyView(
}
}
/**
* Automatically sets the text size of [boxPaint] for given [text] so it fits within the given
* bounds.
*
* Implementation based on this blog post by Lucas (SketchingDev), written on Aug 20, 2015
* https://sketchingdev.co.uk/blog/resizing-text-to-fit-into-a-container-on-android.html
*
* @param boxPaint The [Paint] object which the text size should be applied to.
* @param boxWidth The max width for the surrounding box of [text].
* @param boxHeight The max height for the surrounding box of [text].
* @param text The text for which the size should be calculated.
*/
private fun setTextSizeFor(boxPaint: Paint, boxWidth: Float, boxHeight: Float, text: String, multiplier: Float = 1.0f): Float {
var stage = 1
var textSize = 0.0f
while (stage < 3) {
if (stage == 1) {
textSize += 10.0f
} else if (stage == 2) {
textSize -= 1.0f
}
boxPaint.textSize = textSize
boxPaint.getTextBounds(text, 0, text.length, tempRect)
val fits = tempRect.width() < boxWidth && tempRect.height() < boxHeight
if (stage == 1 && !fits || stage == 2 && fits) {
stage++
}
}
textSize *= multiplier
boxPaint.textSize = textSize
return textSize
}
/**
* Draw the key label / drawable.
*/
@@ -445,26 +652,59 @@ class KeyView(
&& data.code != KeyCode.HALF_SPACE && data.code != KeyCode.KESHIDA || data.type == KeyType.NUMERIC
) {
label = getComputedLetter()
val hintedNumber = data.hintedNumber
if (prefs.keyboard.hintedNumberRowMode != KeyHintMode.DISABLED && hintedNumber != null) {
hintedLabel = getComputedLetter(hintedNumber)
}
val hintedSymbol = data.hintedSymbol
if (prefs.keyboard.hintedSymbolsMode != KeyHintMode.DISABLED && hintedSymbol != null) {
hintedLabel = getComputedLetter(hintedSymbol)
}
} else {
when (data.code) {
KeyCode.ARROW_LEFT -> {
drawable = getDrawable(context, R.drawable.ic_keyboard_arrow_left)
drawableColor = prefs.theme.keyFgColor
}
KeyCode.ARROW_RIGHT -> {
drawable = getDrawable(context, R.drawable.ic_keyboard_arrow_right)
drawableColor = prefs.theme.keyFgColor
}
KeyCode.CLIPBOARD_COPY -> {
drawable = getDrawable(context, R.drawable.ic_content_copy)
drawableColor = prefs.theme.keyFgColor
}
KeyCode.CLIPBOARD_CUT -> {
drawable = getDrawable(context, R.drawable.ic_content_cut)
drawableColor = prefs.theme.keyFgColor
}
KeyCode.CLIPBOARD_PASTE -> {
drawable = getDrawable(context, R.drawable.ic_content_paste)
drawableColor = prefs.theme.keyFgColor
}
KeyCode.CLIPBOARD_SELECT_ALL -> {
drawable = getDrawable(context, R.drawable.ic_select_all)
drawableColor = prefs.theme.keyFgColor
}
KeyCode.DELETE -> {
drawable = getDrawable(context, R.drawable.ic_backspace)
drawableColor = 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 = prefs.theme.keyEnterFgColor
if (action and EditorInfo.IME_FLAG_NO_ENTER_ACTION > 0) {
if (imeOptions.flagNoEnterAction) {
drawable = getDrawable(context, R.drawable.ic_keyboard_return)
}
}
@@ -541,6 +781,9 @@ class KeyView(
// 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) {
@@ -548,11 +791,12 @@ class KeyView(
} else {
marginV = (measuredHeight - measuredWidth) / 2
}
// Note: using the vertical padding for horizontal as well on purpose here
drawable.setBounds(
marginH + drawablePadding,
marginV + drawablePadding,
measuredWidth - marginH - drawablePadding,
measuredHeight - marginV - drawablePadding)
marginH + drawablePaddingV,
marginV + drawablePaddingV,
measuredWidth - marginH - drawablePaddingV,
measuredHeight - marginV - drawablePaddingV)
drawable.colorFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(
drawableColor,
BlendModeCompat.SRC_ATOP
@@ -563,20 +807,51 @@ class KeyView(
// Draw label
val label = label
if (label != null) {
if (data.code == KeyCode.VIEW_NUMERIC || data.code == KeyCode.VIEW_NUMERIC_ADVANCED
|| data.code == KeyCode.SPACE) {
labelPaint.textSize = resources.getDimension(R.dimen.key_numeric_textSize)
} else {
labelPaint.textSize = resources.getDimension(R.dimen.key_textSize)
when (data.code) {
KeyCode.VIEW_NUMERIC, KeyCode.VIEW_NUMERIC_ADVANCED -> {
labelPaint.textSize = resources.getDimension(R.dimen.key_numeric_textSize)
}
else -> when {
(data.type == KeyType.CHARACTER || data.type == KeyType.NUMERIC) &&
data.code != KeyCode.SPACE -> {
val cachedTextSize = setTextSizeFor(
labelPaint,
desiredWidth - (2.6f * drawablePaddingH),
desiredHeight - (3.4f * drawablePaddingV),
// Note: taking a "X" here because it is one of the biggest letters and
// the keys must have the same base character for calculation, else
// they will all look different and weird...
"X",
when (resources.configuration.orientation) {
Configuration.ORIENTATION_PORTRAIT -> {
prefs.keyboard.fontSizeMultiplierPortrait.toFloat() / 100.0f
}
Configuration.ORIENTATION_LANDSCAPE -> {
prefs.keyboard.fontSizeMultiplierLandscape.toFloat() / 100.0f
}
else -> 1.0f
}
)
keyboardView.popupManager.keyPopupTextSize = cachedTextSize
}
else -> {
setTextSizeFor(
labelPaint,
measuredWidth - (1.2f * drawablePaddingH),
measuredHeight - (3.6f * drawablePaddingV),
when (data.code) {
KeyCode.VIEW_CHARACTERS, KeyCode.VIEW_SYMBOLS, KeyCode.VIEW_SYMBOLS2 -> {
resources.getString(R.string.key__view_symbols)
}
else -> label
}
)
}
}
}
labelPaint.color = 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 (prefs.keyboard.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")) {
@@ -588,6 +863,25 @@ class KeyView(
canvas.drawText(label, centerX, centerY, labelPaint)
}
}
// Draw hinted label
val hintedLabel = hintedLabel
if (hintedLabel != null) {
setTextSizeFor(
hintedLabelPaint,
desiredWidth * 1.0f / 6.0f,
desiredHeight * 1.0f / 6.0f,
// Note: taking a "X" here because it is one of the biggest letters and
// the keys must have the same base character for calculation, else
// they will all look different and weird...
"X"
)
hintedLabelPaint.color = prefs.theme.keyFgColor
hintedLabelPaint.alpha = 120
val centerX = measuredWidth * 5.0f / 6.0f
val centerY = measuredHeight * 1.0f / 6.0f + (hintedLabelPaint.textSize - hintedLabelPaint.descent()) / 2
canvas.drawText(hintedLabel, centerX, centerY, hintedLabelPaint)
}
}
/**

View File

@@ -24,5 +24,7 @@ enum class KeyboardMode {
NUMERIC,
NUMERIC_ADVANCED,
PHONE,
PHONE2
PHONE2,
SMARTBAR_CLIPBOARD_CURSOR_ROW,
SMARTBAR_NUMBER_ROW
}

View File

@@ -29,8 +29,11 @@ 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.ime.text.gestures.SwipeGesture
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import kotlin.math.roundToInt
/**
@@ -39,11 +42,9 @@ import kotlin.math.roundToInt
* 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.
*
* TODO: Implement swipe gesture support
*
* @property florisboard Reference to instance of core class [FlorisBoard].
*/
class KeyboardView : LinearLayout, FlorisBoard.EventListener {
class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Listener {
private var activeKeyView: KeyView? = null
private var activePointerId: Int? = null
private var activeX: Float = 0.0f
@@ -57,9 +58,12 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener {
var desiredKeyWidth: Int = resources.getDimension(R.dimen.key_width).toInt()
var desiredKeyHeight: Int = resources.getDimension(R.dimen.key_height).toInt()
var florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
private var initialKeyCode: Int = 0
var isPreviewMode: Boolean = false
var isSmartbarKeyboardView: Boolean = false
var popupManager = KeyPopupManager<KeyboardView, KeyView>(this)
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)
@@ -70,6 +74,7 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener {
FrameLayout.LayoutParams.WRAP_CONTENT
)
florisboard?.addEventListener(this)
onWindowShown()
}
/**
@@ -104,6 +109,13 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener {
popupManager.dismissAllPopups()
}
override fun onWindowShown() {
swipeGestureDetector.apply {
distanceThreshold = prefs.gestures.swipeDistanceThreshold
velocityThreshold = prefs.gestures.swipeVelocityThreshold
}
}
/**
* Catch all events which are designated for child views.
*/
@@ -121,6 +133,12 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener {
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) {
@@ -131,6 +149,7 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener {
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
@@ -139,6 +158,7 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener {
activeX = event.getX(pointerIndex)
activeY = event.getY(pointerIndex)
searchForActiveKeyView()
initialKeyCode = activeKeyView?.data?.code ?: 0
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_DOWN)
}
}
@@ -196,6 +216,46 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener {
})
}
/**
* 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].
*/
@@ -229,20 +289,29 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener {
* 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 keyHeightFactor = when (isPreviewMode) {
true -> 0.90f
else -> 1.00f
}
val desiredHeight = keyHeightFactor * (florisboard?.inputView?.desiredTextKeyboardViewHeight ?: resources.getDimension(R.dimen.textKeyboardView_baseHeight).toInt())
desiredKeyHeight = (desiredHeight / 4 - 2 * keyMarginV).roundToInt()
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(desiredHeight.roundToInt(), MeasureSpec.EXACTLY))
val desiredWidth = MeasureSpec.getSize(widthMeasureSpec).toFloat()
desiredKeyWidth = if (isSmartbarKeyboardView) {
(desiredWidth / 6.0f - 2.0f * keyMarginH).roundToInt()
} else {
(desiredWidth / 10.0f - 2.0f * keyMarginH).roundToInt()
}
val desiredHeight = if (isSmartbarKeyboardView || isPreviewMode) {
MeasureSpec.getSize(heightMeasureSpec).toFloat()
} else {
(florisboard?.inputView?.desiredTextKeyboardViewHeight ?: MeasureSpec.getSize(heightMeasureSpec).toFloat())
} * if (isPreviewMode) { 0.90f } else { 1.00f }
desiredKeyHeight = when {
isSmartbarKeyboardView -> desiredHeight - 1.5f * keyMarginV
else -> desiredHeight / (computedLayout?.arrangement?.size?.toFloat() ?: 4.0f) - 2.0f * keyMarginV
}.roundToInt()
super.onMeasure(
MeasureSpec.makeMeasureSpec(desiredWidth.roundToInt(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(desiredHeight.roundToInt(), MeasureSpec.EXACTLY)
)
}
override fun onApplyThemeAttributes() {

View File

@@ -21,11 +21,9 @@ import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.text.key.KeyData
import dev.patrickgold.florisboard.ime.text.key.KeyTypeAdapter
import dev.patrickgold.florisboard.ime.text.key.KeyVariation
import dev.patrickgold.florisboard.ime.text.key.KeyVariationAdapter
import dev.patrickgold.florisboard.ime.text.key.*
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
import kotlinx.coroutines.*
import java.util.*
@@ -37,18 +35,19 @@ private typealias KMS = Pair<KeyboardMode, Subtype>
* Class which manages layout loading and caching.
*/
class LayoutManager(private val context: Context) : CoroutineScope by MainScope() {
private val layoutCache: HashMap<KMS, Deferred<ComputedLayoutData>> = hashMapOf()
private val computedLayoutCache: HashMap<KMS, Deferred<ComputedLayoutData>> = hashMapOf()
/**
* Loads the layout for the specified type and name.
*
* @returns the [LayoutData] or null.
* @return the [LayoutData] or null.
*/
private fun loadLayout(ltn: LTN?) = loadLayout(ltn?.first, ltn?.second)
private fun loadLayout(type: LayoutType?, name: String?): LayoutData? {
if (type == null || name == null) {
return null
}
val rawJsonData: String = try {
context.assets.open("ime/text/$type/$name.json").bufferedReader().use { it.readText() }
} catch (e: Exception) {
@@ -108,14 +107,15 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
* @param main The main layout type and name.
* @param modifier The modifier (mod) layout type and name.
* @param extension The extension layout type and name.
* @returns a [ComputedLayoutData] object, regardless of the specified LTNs or errors.
* @return a [ComputedLayoutData] object, regardless of the specified LTNs or errors.
*/
private fun mergeLayouts(
private suspend fun mergeLayoutsAsync(
keyboardMode: KeyboardMode,
subtype: Subtype,
main: LTN? = null,
modifier: LTN? = null,
extension: LTN? = null
extension: LTN? = null,
prefs: PrefHelper
): ComputedLayoutData {
val computedArrangement: ComputedLayoutDataArrangement = mutableListOf()
@@ -174,26 +174,58 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
for (computedRow in computedArrangement) {
for (keyData in computedRow) {
if (keyData.variation != KeyVariation.ALL) {
if (keyData.variation == KeyVariation.NORMAL ||
keyData.variation == KeyVariation.PASSWORD) {
if (extendedPopups.containsKey(keyData.label + "~normal")) {
keyData.popup.addAll(extendedPopups[keyData.label + "~normal"] ?: listOf())
if (keyData.label == "." && modifierLayout?.name != "dvorak" ||
keyData.label == "z" && modifierLayout?.name == "dvorak") {
val label = "." // keyData.label
if (keyData.variation == KeyVariation.NORMAL ||
keyData.variation == KeyVariation.PASSWORD) {
if (extendedPopups.containsKey("$label~normal")) {
keyData.popup.addAll(extendedPopups["$label~normal"] ?: listOf())
}
}
if (keyData.variation == KeyVariation.EMAIL_ADDRESS ||
keyData.variation == KeyVariation.URI) {
if (extendedPopups.containsKey("$label~uri")) {
keyData.popup.addAll(extendedPopups["$label~uri"] ?: listOf())
}
}
}
if (keyData.variation == KeyVariation.EMAIL_ADDRESS ||
keyData.variation == KeyVariation.URI) {
if (extendedPopups.containsKey(keyData.label + "~uri")) {
keyData.popup.addAll(extendedPopups[keyData.label + "~uri"] ?: listOf())
}
}
}
if (extendedPopups.containsKey(keyData.label)) {
} else if (extendedPopups.containsKey(keyData.label)) {
keyData.popup.addAll(extendedPopups[keyData.label] ?: listOf())
}
}
}
}
// Add hints to keys
if (keyboardMode == KeyboardMode.CHARACTERS) {
val symbolsComputedArrangement = fetchComputedLayoutAsync(KeyboardMode.SYMBOLS, subtype, prefs).await().arrangement
val minRow = if (prefs.keyboard.numberRow) { 1 } else { 0 }
for ((r, row) in computedArrangement.withIndex()) {
if (r >= (3 + minRow) || r < minRow) {
continue
}
var kOffset = 0
val symbolRow = symbolsComputedArrangement.getOrNull(r - minRow)
if (symbolRow != null) {
for ((k, key) in row.withIndex()) {
val lastKey = row.getOrNull(k - 1)
if (key.variation != KeyVariation.ALL && lastKey != null && lastKey.variation != KeyVariation.ALL) {
kOffset++
}
val symbol = symbolRow.getOrNull(k - kOffset)
if (key.type == KeyType.CHARACTER && symbol?.type == KeyType.CHARACTER) {
if (r == minRow) {
key.hintedNumber = symbol
} else if (r > minRow) {
key.hintedSymbol = symbol
}
}
}
}
}
}
return ComputedLayoutData(
keyboardMode,
"computed",
@@ -210,9 +242,10 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
* @param keyboardMode The keyboard mode for which the layout should be computed.
* @param subtype The subtype which localizes the computed layout.
*/
private fun computeLayoutFor(
private suspend fun computeLayoutFor(
keyboardMode: KeyboardMode,
subtype: Subtype
subtype: Subtype,
prefs: PrefHelper
): ComputedLayoutData {
var main: LTN? = null
var modifier: LTN? = null
@@ -220,9 +253,15 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
when (keyboardMode) {
KeyboardMode.CHARACTERS -> {
if (prefs.keyboard.numberRow) {
extension = LTN(LayoutType.EXTENSION, "number_row")
}
main = LTN(LayoutType.CHARACTERS, subtype.layout)
modifier = LTN(LayoutType.CHARACTERS_MOD, "default")
}
KeyboardMode.EDITING -> {
// Layout for this mode is defined in custom layout xml file.
}
KeyboardMode.NUMERIC -> {
main = LTN(LayoutType.NUMERIC, "default")
}
@@ -236,17 +275,43 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
main = LTN(LayoutType.PHONE2, "default")
}
KeyboardMode.SYMBOLS -> {
extension = LTN(LayoutType.EXTENSION, "number_row")
main = LTN(LayoutType.SYMBOLS, "western_default")
modifier = LTN(LayoutType.SYMBOLS_MOD, "default")
extension = LTN(LayoutType.EXTENSION, "number_row")
}
KeyboardMode.SYMBOLS2 -> {
main = LTN(LayoutType.SYMBOLS2, "western_default")
modifier = LTN(LayoutType.SYMBOLS2_MOD, "default")
}
KeyboardMode.SMARTBAR_CLIPBOARD_CURSOR_ROW -> {
extension = LTN(LayoutType.EXTENSION, "clipboard_cursor_row")
}
KeyboardMode.SMARTBAR_NUMBER_ROW -> {
extension = LTN(LayoutType.EXTENSION, "number_row")
}
}
return mergeLayouts(keyboardMode, subtype, main, modifier, extension)
return mergeLayoutsAsync(keyboardMode, subtype, main, modifier, extension, prefs)
}
/**
* Clears the layout cache for the specified [keyboardMode].
*
* @param keyboardMode The keyboard mode for which the layout cache should be cleared. If null
* is passed, the entire cache will be cleared. Defaults to null.
*/
fun clearLayoutCache(keyboardMode: KeyboardMode? = null) {
if (keyboardMode == null) {
computedLayoutCache.clear()
} else {
val it = computedLayoutCache.iterator()
while (it.hasNext()) {
val kms = it.next().key
if (kms.first == keyboardMode) {
it.remove()
}
}
}
}
/**
@@ -260,17 +325,18 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
@Synchronized
fun fetchComputedLayoutAsync(
keyboardMode: KeyboardMode,
subtype: Subtype
subtype: Subtype,
prefs: PrefHelper
): Deferred<ComputedLayoutData> {
val kms = KMS(keyboardMode, subtype)
val cachedComputedLayout = layoutCache[kms]
val cachedComputedLayout = computedLayoutCache[kms]
return if (cachedComputedLayout != null) {
cachedComputedLayout
} else {
val computedLayout = async(Dispatchers.IO) {
computeLayoutFor(keyboardMode, subtype)
computeLayoutFor(keyboardMode, subtype, prefs)
}
layoutCache[kms] = computedLayout
computedLayoutCache[kms] = computedLayout
computedLayout
}
}
@@ -286,12 +352,13 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
@Synchronized
fun preloadComputedLayout(
keyboardMode: KeyboardMode,
subtype: Subtype
subtype: Subtype,
prefs: PrefHelper
) {
val kms = KMS(keyboardMode, subtype)
if (layoutCache[kms] == null) {
layoutCache[kms] = async(Dispatchers.IO) {
computeLayoutFor(keyboardMode, subtype)
if (computedLayoutCache[kms] == null) {
computedLayoutCache[kms] = async(Dispatchers.IO) {
computeLayoutFor(keyboardMode, subtype, prefs)
}
}
}

View File

@@ -1,233 +0,0 @@
package dev.patrickgold.florisboard.ime.text.smartbar
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.CursorAnchorInfo
import android.widget.Button
import android.widget.ImageButton
import android.widget.LinearLayout
import androidx.core.view.children
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.text.TextInputManager
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyData
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
// TODO: Implement suggestion creation functionality
// TODO: Cleanup and reorganize SmartbarManager
class SmartbarManager private constructor() : FlorisBoard.EventListener {
private val florisboard: FlorisBoard = FlorisBoard.getInstance()
private var isComposingEnabled: Boolean = false
private val textInputManager: TextInputManager = TextInputManager.getInstance()
var smartbarView: SmartbarView? = null
private set
var isQuickActionsVisible: Boolean = false
set(value) { field = value; updateActiveContainerVisibility() }
private val candidateViewOnClickListener = View.OnClickListener { v ->
val view = v as Button
val text = view.text.toString()
if (text.isNotEmpty()) {
textInputManager.commitCandidate(text)
}
}
private val candidateViewOnLongClickListener = View.OnLongClickListener { v ->
true
}
private val keyButtonOnClickListener = 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")
R.id.cc_select_all -> KeyData(KeyCode.CLIPBOARD_SELECT_ALL)
R.id.cc_copy -> KeyData(KeyCode.CLIPBOARD_COPY)
R.id.cc_arrow_left -> KeyData(KeyCode.ARROW_LEFT)
R.id.cc_arrow_right -> KeyData(KeyCode.ARROW_RIGHT)
R.id.cc_cut -> KeyData(KeyCode.CLIPBOARD_CUT)
R.id.cc_paste -> KeyData(KeyCode.CLIPBOARD_PASTE)
else -> KeyData(0)
}
florisboard.textInputManager.sendKeyPress(keyData)
}
private val quickActionOnClickListener = View.OnClickListener { v ->
when (v.id) {
R.id.back_button -> {
florisboard.textInputManager.setActiveKeyboardMode(KeyboardMode.CHARACTERS)
smartbarView?.setActiveVariant(R.id.smartbar_variant_default)
}
R.id.quick_action_switch_to_editing_context -> {
if (florisboard.textInputManager.getActiveKeyboardMode() == KeyboardMode.EDITING) {
florisboard.textInputManager.setActiveKeyboardMode(KeyboardMode.CHARACTERS)
smartbarView?.setActiveVariant(R.id.smartbar_variant_default)
} else {
florisboard.textInputManager.setActiveKeyboardMode(KeyboardMode.EDITING)
smartbarView?.setActiveVariant(R.id.smartbar_variant_back_only)
}
}
R.id.quick_action_switch_to_media_context -> florisboard.setActiveInput(R.id.media_input)
R.id.quick_action_open_settings -> florisboard.launchSettings()
R.id.quick_action_one_handed_toggle -> florisboard.toggleOneHandedMode()
else -> return@OnClickListener
}
isQuickActionsVisible = false
}
private val quickActionToggleOnClickListener = View.OnClickListener {
isQuickActionsVisible = !isQuickActionsVisible
}
companion object {
private var instance: SmartbarManager? = null
@Synchronized
fun getInstance(): SmartbarManager {
if (instance == null) {
instance = SmartbarManager()
}
return instance!!
}
}
fun registerSmartbarView(smartbarView: SmartbarView) {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "registerSmartbarView(smartbarView)")
this.smartbarView = smartbarView
smartbarView.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(keyButtonOnClickListener)
}
}
val clipboardCursorRow = smartbarView.findViewById<ViewGroup>(R.id.clipboard_cursor_row)
for (clipboardCursorRowButton in clipboardCursorRow.children) {
if (clipboardCursorRowButton is ImageButton) {
clipboardCursorRowButton.setOnClickListener(keyButtonOnClickListener)
}
}
val backButton = smartbarView.findViewById<View>(R.id.back_button)
backButton.setOnClickListener(quickActionOnClickListener)
for (candidateView in smartbarView.candidateViewList) {
candidateView.setOnClickListener(candidateViewOnClickListener)
candidateView.setOnLongClickListener(candidateViewOnLongClickListener)
}
smartbarView.setActiveVariant(R.id.smartbar_variant_default)
}
override fun onWindowShown() {
isQuickActionsVisible = false
}
// TODO: clean up resources here
override fun onDestroy() {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onDestroy()")
instance = null
}
fun onStartInputView(keyboardMode: KeyboardMode, isComposingEnabled: Boolean) {
this.isComposingEnabled = isComposingEnabled
when (keyboardMode) {
KeyboardMode.NUMERIC, KeyboardMode.PHONE, KeyboardMode.PHONE2 -> {
smartbarView?.setActiveVariant(null)
}
else -> {
smartbarView?.setActiveVariant(R.id.smartbar_variant_default)
isQuickActionsVisible = false
}
}
}
fun onFinishInputView() {
//spellCheckerSession?.close()
}
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
val isSelectionActive = florisboard.textInputManager.isTextSelected
smartbarView?.findViewById<View>(R.id.cc_cut)?.isEnabled = isSelectionActive
smartbarView?.findViewById<View>(R.id.cc_copy)?.isEnabled = isSelectionActive
smartbarView?.findViewById<View>(R.id.cc_paste)?.isEnabled =
florisboard.clipboardManager?.hasPrimaryClip() ?: false
}
fun deleteCandidateFromDictionary(candidate: String) {
//
}
fun resetCandidates() {
//
}
fun generateCandidatesFromComposing(composingText: String?) {
val smartbarView = smartbarView ?: return
if (composingText == null) {
smartbarView.candidateViewList[0].text = "candidate"
smartbarView.candidateViewList[1].text = "suggestions"
smartbarView.candidateViewList[2].text = "nyi"
} else {
smartbarView.candidateViewList[0].text = ""
smartbarView.candidateViewList[1].text = composingText + "test"
smartbarView.candidateViewList[2].text = ""
}
//spellCheckerSession?.getSentenceSuggestions(arrayOf(TextInfo(composing)), 3)
//android.util.Log.i("SPELL", "GEN")
/*val dic: Uri = UserDictionary.Words.CONTENT_URI
val resolver: ContentResolver = florisboard.contentResolver
val cursor: Cursor = resolver.query(dic, null, null, null, null) ?: return
var count = 0
while (cursor.moveToNext()) {
val word = cursor.getString(cursor.getColumnIndex(UserDictionary.Words.WORD))
candidateViewList[count].text = word
if (count++ > 2) {
break
}
}
cursor.close()*/
}
fun writeCandidate(candidate: String) {
//
}
private fun updateActiveContainerVisibility() {
val smartbarView = smartbarView ?: return
if (isQuickActionsVisible) {
smartbarView.setActiveContainer(R.id.quick_actions)
smartbarView.findViewById<View>(R.id.quick_action_toggle)?.rotation = -180.0f
} else {
if (isComposingEnabled) {
smartbarView.setActiveContainer(R.id.candidates)
} else if (textInputManager.getActiveKeyboardMode() == KeyboardMode.CHARACTERS) {
smartbarView.setActiveContainer(when (florisboard.prefs.suggestion.showInstead) {
"number_row" -> R.id.number_row
"clipboard_cursor_tools" -> R.id.clipboard_cursor_row
else -> null
})
} else {
smartbarView.setActiveContainer(null)
}
smartbarView.findViewById<View>(R.id.quick_action_toggle)?.rotation = 0.0f
}
}
}

View File

@@ -19,6 +19,7 @@ package dev.patrickgold.florisboard.ime.text.smartbar
import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.util.setBackgroundTintColor2
@@ -33,6 +34,10 @@ class SmartbarQuickActionButton : androidx.appcompat.widget.AppCompatImageButton
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
init {
updateTheme()
}
/**
* Override onMeasure() to automatically set the height of the button equal to the width of
* the button. The height is MATCH_PARENT and the exact same calculated pixel size should be
@@ -43,8 +48,17 @@ class SmartbarQuickActionButton : androidx.appcompat.widget.AppCompatImageButton
}
override fun onDraw(canvas: Canvas?) {
updateTheme()
super.onDraw(canvas)
setBackgroundTintColor2(this, prefs.theme.smartbarButtonBgColor)
setColorFilter(prefs.theme.smartbarButtonFgColor)
}
private fun updateTheme() {
if (id == R.id.private_mode_button) {
setBackgroundTintColor2(this, prefs.theme.privateModeBgColor)
setColorFilter(prefs.theme.privateModeFgColor)
} else {
setBackgroundTintColor2(this, prefs.theme.smartbarButtonBgColor)
setColorFilter(prefs.theme.smartbarButtonFgColor)
}
}
}

View File

@@ -19,97 +19,284 @@ 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.constraintlayout.widget.ConstraintLayout
import androidx.core.view.children
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.databinding.SmartbarBinding
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.util.setImageTintColor2
import kotlinx.android.synthetic.main.florisboard.view.*
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.text.key.KeyVariation
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
import dev.patrickgold.florisboard.util.setBackgroundTintColor2
import dev.patrickgold.florisboard.util.setDrawableTintColor2
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.lang.ref.WeakReference
import kotlin.math.roundToInt
/**
* View class which keeps the references to important children and informs [SmartbarManager] that
* it is now the active [SmartbarView] (useful when resetting the input view of FlorisBoard due to
* a theme change).
* View class which manages the state and the UI of the Smartbar, a key element in the usefulness
* of FlorisBoard. The view automatically tries to get the current FlorisBoard instance, which it
* needs to decide when a specific feature component is shown.
*/
class SmartbarView : LinearLayout {
class SmartbarView : ConstraintLayout {
private val florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
private val smartbarManager = SmartbarManager.getInstance()
private var eventListener: WeakReference<EventListener?>? = null
private val mainScope = MainScope()
private var variants: MutableList<ViewGroup> = mutableListOf()
private var containers: MutableList<ViewGroup> = mutableListOf()
private var cachedActionStartAreaVisible: Boolean = false
@IdRes private var cachedActionStartAreaId: Int? = null
@IdRes private var cachedMainAreaId: Int? = null
private var cachedActionEndAreaVisible: Boolean = false
@IdRes private var cachedActionEndAreaId: Int? = null
var candidateViewList: MutableList<Button> = mutableListOf()
private set
var isQuickActionsVisible: Boolean = false
set(v) {
binding.quickActionToggle.rotation = if (v) 180.0f else 0.0f
field = v
}
private var shouldSuggestClipboardContents: Boolean = false
private lateinit var binding: SmartbarBinding
private var indexedActionStartArea: MutableList<Int> = mutableListOf()
private var indexedMainArea: MutableList<Int> = mutableListOf()
private var indexedActionEndArea: MutableList<Int> = mutableListOf()
private var candidateViewList: MutableList<Button> = mutableListOf()
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
/**
* Called by Android when this view has been attached to a window. At this point we can be
* certain that all children have been instantiated and that we can begin working with them.
* After initializing all child views, this method registers the SmartbarView in the
* TextInputManager, which then starts working together with this view.
*/
override fun onAttachedToWindow() {
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onAttachedToWindow()")
Timber.i("onAttachedToWindow()")
super.onAttachedToWindow()
variants.add(findViewById(R.id.smartbar_variant_default))
variants.add(findViewById(R.id.smartbar_variant_back_only))
binding = SmartbarBinding.bind(this)
containers.add(findViewById(R.id.candidates))
containers.add(findViewById(R.id.clipboard_cursor_row))
containers.add(findViewById(R.id.number_row))
containers.add(findViewById(R.id.quick_actions))
for (view in binding.actionStartArea.children) {
indexedActionStartArea.add(view.id)
}
for (view in binding.mainArea.children) {
indexedMainArea.add(view.id)
}
for (view in binding.actionEndArea.children) {
indexedActionEndArea.add(view.id)
}
candidateViewList.add(findViewById(R.id.candidate0))
candidateViewList.add(findViewById(R.id.candidate1))
candidateViewList.add(findViewById(R.id.candidate2))
candidateViewList.add(binding.candidate0)
candidateViewList.add(binding.candidate1)
candidateViewList.add(binding.candidate2)
smartbarManager.registerSmartbarView(this)
binding.backButton.setOnClickListener { eventListener?.get()?.onSmartbarBackButtonPressed() }
binding.clipboardCursorRow.isSmartbarKeyboardView = true
mainScope.launch(Dispatchers.Default) {
florisboard?.let {
val layout = florisboard.textInputManager.layoutManager.fetchComputedLayoutAsync(
KeyboardMode.SMARTBAR_CLIPBOARD_CURSOR_ROW,
Subtype.DEFAULT,
prefs
).await()
launch(Dispatchers.Main) {
binding.clipboardCursorRow.computedLayout = layout
binding.clipboardCursorRow.updateVisibility()
}
}
}
binding.clipboardSuggestion.setOnClickListener {
florisboard?.activeEditorInstance?.performClipboardPaste()
shouldSuggestClipboardContents = false
updateSmartbarState()
}
binding.numberRow.isSmartbarKeyboardView = true
mainScope.launch(Dispatchers.Default) {
florisboard?.let {
val layout = it.textInputManager.layoutManager.fetchComputedLayoutAsync(
KeyboardMode.SMARTBAR_NUMBER_ROW,
Subtype.DEFAULT,
prefs
).await()
launch(Dispatchers.Main) {
binding.numberRow.computedLayout = layout
binding.numberRow.updateVisibility()
}
}
}
binding.privateModeButton.setOnClickListener {
eventListener?.get()?.onSmartbarPrivateModeButtonClicked()
}
for (quickAction in binding.quickActions.children) {
if (quickAction is SmartbarQuickActionButton) {
quickAction.setOnClickListener { eventListener?.get()?.onSmartbarQuickActionPressed(quickAction.id) }
}
}
binding.quickActionToggle.setOnClickListener {
isQuickActionsVisible = !isQuickActionsVisible
updateSmartbarState()
}
configureFeatureVisibility(
actionStartAreaVisible = false,
actionStartAreaId = null,
mainAreaId = null,
actionEndAreaVisible = false,
actionEndAreaId = null
)
florisboard?.textInputManager?.registerSmartbarView(this)
}
/**
* Sets the active Smartbar variant based on the given id. Pass null to hide all variants and
* show an empty Smartbar.
* Updates the visibility of features based on the provided attributes.
*
* @param which Which variant to show. Pass null to hide all.
* @param actionStartAreaVisible True if the action start area should be shown, else false.
* @param actionStartAreaId The ID of the element to show within the action start area. Set to
* null to leave this area blank.
* @param mainAreaId The ID of the element to show within the main area. Set to null to leave
* this area blank.
* @param actionEndAreaVisible True if the action end area should be shown, else false.
* @param actionEndAreaId The ID of the element to show within the action end area. Set to null
* to leave this area blank.
*/
fun setActiveVariant(@IdRes which: Int?) {
for (variant in variants) {
if (variant.id == which) {
variant.visibility = View.VISIBLE
} else {
variant.visibility = View.GONE
}
private fun configureFeatureVisibility(
actionStartAreaVisible: Boolean = cachedActionStartAreaVisible,
@IdRes actionStartAreaId: Int? = cachedActionStartAreaId,
@IdRes mainAreaId: Int? = cachedMainAreaId,
actionEndAreaVisible: Boolean = cachedActionEndAreaVisible,
@IdRes actionEndAreaId: Int? = cachedActionEndAreaId
) {
binding.actionStartArea.visibility = when {
actionStartAreaVisible && actionStartAreaId != null -> View.VISIBLE
actionStartAreaVisible && actionStartAreaId == null -> View.INVISIBLE
else -> View.GONE
}
if (actionStartAreaId != null) {
binding.actionStartArea.displayedChild =
indexedActionStartArea.indexOf(actionStartAreaId).coerceAtLeast(0)
}
binding.mainArea.visibility = when (mainAreaId) {
null -> View.INVISIBLE
else -> View.VISIBLE
}
if (mainAreaId != null) {
binding.mainArea.displayedChild =
indexedMainArea.indexOf(mainAreaId).coerceAtLeast(0)
}
binding.actionEndArea.visibility = when {
actionEndAreaVisible && actionEndAreaId != null -> View.VISIBLE
actionEndAreaVisible && actionEndAreaId == null -> View.INVISIBLE
else -> View.GONE
}
if (actionEndAreaId != null) {
binding.actionEndArea.displayedChild =
indexedActionEndArea.indexOf(actionEndAreaId).coerceAtLeast(0)
}
}
/**
* 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.
* Updates the Smartbar UI state by looking at the current keyboard mode, key variation, active
* editor instance, etc. Passes the evaluated attributes to [configureFeatureVisibility].
*/
fun setActiveContainer(@IdRes which: Int?) {
for (container in containers) {
if (container.id == which) {
container.visibility = View.VISIBLE
} else {
container.visibility = View.GONE
}
fun updateSmartbarState() {
binding.clipboardCursorRow.updateVisibility()
when (florisboard) {
null -> configureFeatureVisibility(
actionStartAreaVisible = false,
actionStartAreaId = null,
mainAreaId = null,
actionEndAreaVisible = false,
actionEndAreaId = null
)
else -> configureFeatureVisibility(
actionStartAreaVisible = when (florisboard.textInputManager.keyVariation) {
KeyVariation.PASSWORD -> false
else -> true
},
actionStartAreaId = when (florisboard.textInputManager.getActiveKeyboardMode()) {
KeyboardMode.EDITING -> R.id.back_button
else -> R.id.quick_action_toggle
},
mainAreaId = when (florisboard.textInputManager.keyVariation) {
KeyVariation.PASSWORD -> R.id.number_row
else -> when (isQuickActionsVisible) {
true -> R.id.quick_actions
else -> when (florisboard.textInputManager.getActiveKeyboardMode()) {
KeyboardMode.EDITING,
KeyboardMode.NUMERIC,
KeyboardMode.PHONE,
KeyboardMode.PHONE2 -> null
else -> when {
florisboard.activeEditorInstance.isComposingEnabled &&
shouldSuggestClipboardContents
-> R.id.clipboard_suggestion_row
florisboard.activeEditorInstance.isComposingEnabled &&
florisboard.activeEditorInstance.selection.isCursorMode
-> R.id.candidates
else -> R.id.clipboard_cursor_row
}
}
}
},
actionEndAreaVisible = when (florisboard.textInputManager.keyVariation) {
KeyVariation.PASSWORD -> false
else -> true
},
actionEndAreaId = when {
florisboard.activeEditorInstance.isPrivateMode -> R.id.private_mode_button
else -> null
}
)
}
}
fun onPrimaryClipChanged() {
if (prefs.suggestion.enabled && prefs.suggestion.suggestClipboardContent &&
florisboard?.activeEditorInstance?.isPrivateMode == false) {
shouldSuggestClipboardContents = true
val item = florisboard.clipboardManager?.primaryClip?.getItemAt(0)
when {
item?.text != null -> {
binding.clipboardSuggestion.text = item.text
}
item?.uri != null -> {
binding.clipboardSuggestion.text = "(Image) " + item.uri.toString()
}
else -> {
binding.clipboardSuggestion.text = item?.text ?: "(Error while retrieving clipboard data)"
}
}
updateSmartbarState()
}
}
fun resetClipboardSuggestion() {
shouldSuggestClipboardContents = false
updateSmartbarState()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec).toFloat()
val height = when (heightMode) {
MeasureSpec.EXACTLY -> {
// Must be this size
@@ -117,48 +304,41 @@ class SmartbarView : LinearLayout {
}
MeasureSpec.AT_MOST -> {
// Can't be bigger than...
(florisboard?.inputView?.desiredSmartbarHeight ?: 0).coerceAtMost(heightSize)
(florisboard?.inputView?.desiredSmartbarHeight ?: resources.getDimension(R.dimen.smartbar_baseHeight)).coerceAtMost(heightSize)
}
else -> {
// Be whatever you want
florisboard?.inputView?.desiredSmartbarHeight ?: 0
florisboard?.inputView?.desiredSmartbarHeight ?: resources.getDimension(R.dimen.smartbar_baseHeight)
}
}
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY))
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height.roundToInt(), MeasureSpec.EXACTLY))
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
setBackgroundColor(prefs.theme.smartbarBgColor)
for (container in containers) {
when (container.id) {
R.id.number_row -> {
for (button in container.children) {
if (button is Button) {
button.setTextColor(prefs.theme.smartbarFgColor)
}
}
}
R.id.clipboard_cursor_row -> {
for (button in container.children) {
if (button is ImageButton) {
if (button.isEnabled) {
setImageTintColor2(button, prefs.theme.smartbarFgColor)
} else {
setImageTintColor2(button, prefs.theme.smartbarFgColorAlt)
}
}
}
}
R.id.candidates -> {
for (view in container.children) {
if (view is Button) {
view.setTextColor(prefs.theme.smartbarFgColor)
}
}
}
}
setBackgroundTintColor2(binding.clipboardSuggestion, prefs.theme.smartbarButtonBgColor)
setDrawableTintColor2(binding.clipboardSuggestion, prefs.theme.smartbarButtonFgColor)
binding.clipboardSuggestion.setTextColor(prefs.theme.smartbarButtonFgColor)
for (view in candidateViewList) {
view.setTextColor(prefs.theme.smartbarFgColor)
}
}
fun setEventListener(listener: EventListener) {
eventListener = WeakReference(listener)
}
/**
* Event listener interface which can be used by other classes to receive updates when something
* important happens in the Smartbar.
*/
interface EventListener {
fun onSmartbarBackButtonPressed() {}
//fun onSmartbarCandidatePressed() {}
//fun onSmartbarCandidateLongPressed() {}
fun onSmartbarPrivateModeButtonClicked() {}
fun onSmartbarQuickActionPressed(@IdRes quickActionId: Int) {}
}
}

View File

@@ -88,7 +88,7 @@ data class Theme(
* @param context A reference to the current [Context]. Used to request
* asset file.
* @param path The path to the json theme file in the asset folder.
* @returns A parsed [Theme] or null. A null value may indicate that
* @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.
*/
@@ -105,7 +105,7 @@ data class Theme(
* Loads a theme from the given [rawData].
*
* @param rawData The raw json theme file as a string.
* @returns A parsed [Theme] or null. A null value may indicate that an error
* @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? {
@@ -162,6 +162,9 @@ data class Theme(
prefs.theme.oneHandedButtonFgColor = theme.getAttr("oneHandedButton/fgColor", "#424242")
prefs.theme.privateModeBgColor = theme.getAttr("privateMode/bgColor", "#A000FF")
prefs.theme.privateModeFgColor = theme.getAttr("privateMode/fgColor", "#FFFFFF")
prefs.theme.smartbarBgColor = theme.getAttr("smartbar/bgColor", "#E0E0E0")
prefs.theme.smartbarFgColor = theme.getAttr("smartbar/fgColor", "#000000")
prefs.theme.smartbarFgColorAlt = theme.getAttr("smartbar/fgColorAlt", "#4A000000")
@@ -259,7 +262,7 @@ data class ThemeMetaOnly(
* @param context A reference to the current [Context]. Used to request
* asset file.
* @param path The path to the json theme file in the asset folder.
* @returns [ThemeMetaOnly] or null. A null value may indicate that
* @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.
*/
@@ -282,7 +285,7 @@ data class ThemeMetaOnly(
* @param context A reference to the current [Context]. Used to request
* asset file.
* @param path The path to the dir in the asset folder.
* @returns [ThemeMetaOnly] or null. A null value may indicate that
* @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.
*/

View File

@@ -146,7 +146,7 @@ class DialogSeekBarPreference : Preference {
* handle. (Android's SeekBar step is fixed at 1 and min at 0)
*
* @param actual The actual value.
* @returns the internal value which is used to allow different min and step values.
* @return the internal value which is used to allow different min and step values.
*/
private fun actualValueToSeekBarProgress(actual: Int): Int {
return (actual - min) / step
@@ -156,7 +156,7 @@ class DialogSeekBarPreference : Preference {
* Converts the Android SeekBar value to the actual value.
*
* @param progress The progress value of the SeekBar.
* @returns the actual value which is ready to use.
* @return the actual value which is ready to use.
*/
private fun seekBarProgressToActualValue(progress: Int): Int {
return (progress * step) + min

View File

@@ -16,12 +16,40 @@
package dev.patrickgold.florisboard.settings.fragments
import android.content.SharedPreferences
import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.settings.components.DialogSeekBarPreference
class KeyboardFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener {
private var heightFactorCustom: DialogSeekBarPreference? = null
private var sharedPrefs: SharedPreferences? = null
class KeyboardFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.prefs_keyboard)
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context)
heightFactorCustom = findPreference(PrefHelper.Keyboard.HEIGHT_FACTOR_CUSTOM)
onSharedPreferenceChanged(null, PrefHelper.Keyboard.HEIGHT_FACTOR)
}
override fun onResume() {
super.onResume()
sharedPrefs?.registerOnSharedPreferenceChangeListener(this)
}
override fun onPause() {
super.onPause()
sharedPrefs?.unregisterOnSharedPreferenceChangeListener(this)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (key == PrefHelper.Keyboard.HEIGHT_FACTOR) {
heightFactorCustom?.isVisible = sharedPrefs?.getString(key, "") == "custom"
}
}
}

View File

@@ -33,6 +33,7 @@ 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 {
@@ -54,7 +55,7 @@ class ThemeFragment : SettingsMainActivity.SettingsFragment(), CoroutineScope by
keyboardView = KeyboardView(themeContext)
keyboardView.layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
resources.getDimension(R.dimen.textKeyboardView_baseHeight).roundToInt()
).apply {
val m = resources.getDimension(R.dimen.keyboard_preview_margin).toInt()
setMargins(m, m, m, m)
@@ -62,7 +63,7 @@ class ThemeFragment : SettingsMainActivity.SettingsFragment(), CoroutineScope by
prefs.sync()
keyboardView.isPreviewMode = true
val subtype = subtypeManager.getActiveSubtype() ?: Subtype.DEFAULT
keyboardView.computedLayout = layoutManager.fetchComputedLayoutAsync(KeyboardMode.CHARACTERS, subtype).await()
keyboardView.computedLayout = layoutManager.fetchComputedLayoutAsync(KeyboardMode.CHARACTERS, subtype, prefs).await()
keyboardView.updateVisibility()
keyboardView.onApplyThemeAttributes()
withContext(Dispatchers.Main) {

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,21 @@
package dev.patrickgold.florisboard.util
import android.os.Handler
fun Handler.postAtScheduledRate(delayMillis: Long, periodMillis: Long, r: Runnable) {
val internalRunnable = object : Runnable {
override fun run() {
this@postAtScheduledRate.postDelayed(this, periodMillis)
r.run()
}
}
this.postDelayed(internalRunnable, delayMillis)
}
fun Handler.postDelayed(delayMillis: Long, r: Runnable) {
this.postDelayed(r, delayMillis)
}
fun Handler.cancelAll() {
this.removeCallbacksAndMessages(null)
}

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

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

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,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M18.4,10.6C16.55,8.99 14.15,8 11.5,8c-4.65,0 -8.58,3.03 -9.96,7.22L3.9,16c1.05,-3.19 4.05,-5.5 7.6,-5.5 1.95,0 3.73,0.72 5.12,1.88L13,16h9V7l-3.6,3.6z"/>
</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,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12L21,5l-9,-4zM12,11.99h7c-0.53,4.12 -3.28,7.79 -7,8.94L12,12L5,12L5,6.3l7,-3.11v8.8z"/>
</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="M17,1.01L7,1c-1.1,0 -2,0.9 -2,2v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V3c0,-1.1 -0.9,-1.99 -2,-1.99zM17,19H7V5h10v14z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12.5,8c-2.65,0 -5.05,0.99 -6.9,2.6L2,7v9h9l-3.62,-3.62c1.39,-1.16 3.16,-1.88 5.12,-1.88 3.54,0 6.55,2.31 7.6,5.5l2.37,-0.78C21.08,11.03 17.15,8 12.5,8z"/>
</vector>

View File

@@ -1,13 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<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="#CDAFAFAF"/>
<corners android:radius="@dimen/key_borderRadius"/>
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="@android:color/white"/>

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,49 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="8dp"
android:theme="@style/CrashDialogTheme">
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="@string/crash_dialog__description"/>
<Button
android:id="@+id/copy_to_clipboard"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="@string/crash_dialog__copy_to_clipboard"/>
<Button
android:id="@+id/open_bug_report_form"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="@string/crash_dialog__open_bug_report_form"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<TextView
android:id="@+id/stacktrace"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"/>
</ScrollView>
<Button
android:id="@+id/close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="@string/crash_dialog__close"/>
</LinearLayout>

View File

@@ -6,7 +6,8 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:gravity="bottom"
android:orientation="vertical">
android:orientation="vertical"
android:layoutDirection="ltr">
<dev.patrickgold.florisboard.ime.core.InputView
android:id="@+id/inner_input_view_container"

View File

@@ -9,14 +9,13 @@
android:backgroundTintMode="multiply">
<TextView
android:id="@+id/key_popup_text"
android:id="@+id/symbol"
android:layout_width="match_parent"
android:layout_height="@dimen/key_height"
android:gravity="center"
android:textSize="@dimen/key_popup_textSize"/>
android:gravity="center"/>
<ImageView
android:id="@+id/key_popup_threedots"
android:id="@+id/threedots"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_margin="0dp"

View File

@@ -39,6 +39,16 @@
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
style="@style/SettingsCardView">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Note: Preferences tagged with [NYI] are not yet implemented and thus won\'t do anything or do some basic placeholder stuff only. Please do not file a bug report for these."/>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/repo_url_card"
style="@style/SettingsCardView.Clickable">

View File

@@ -1,52 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<dev.patrickgold.florisboard.ime.text.smartbar.SmartbarView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/smartbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent">
android:orientation="horizontal"
android:layoutDirection="locale">
<LinearLayout
android:id="@+id/smartbar_variant_default"
android:layout_width="match_parent"
<dev.patrickgold.florisboard.ime.core.FlorisViewFlipper
android:id="@+id/action_start_area"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:visibility="gone">
app:layout_constrainedWidth="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<dev.patrickgold.florisboard.ime.text.smartbar.SmartbarQuickActionButton
android:id="@+id/quick_action_toggle"
style="@style/SmartbarQuickAction.Toggle"
android:contentDescription="@string/smartbar__quick_action_toggle__alt"
android:src="@drawable/ic_keyboard_arrow_right" />
android:src="@drawable/ic_keyboard_arrow_right"/>
<dev.patrickgold.florisboard.ime.text.smartbar.SmartbarQuickActionButton
android:id="@+id/back_button"
style="@style/SmartbarQuickAction"
android:contentDescription="@string/smartbar__quick_action__exit_editing"
android:src="@drawable/ic_arrow_back"/>
</dev.patrickgold.florisboard.ime.core.FlorisViewFlipper>
<dev.patrickgold.florisboard.ime.core.FlorisViewFlipper
android:id="@+id/main_area"
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:layout_constrainedWidth="true"
app:layout_constraintStart_toEndOf="@id/action_start_area"
app:layout_constraintEnd_toStartOf="@id/action_end_area"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:id="@+id/candidates"
style="@style/SmartbarContainer"
android:visibility="gone">
style="@style/SmartbarContainer">
<Button
android:id="@+id/candidate0"
style="@style/SmartbarCandidate"/>
style="@style/SmartbarCandidate"
android:text="suggestions"/>
<View style="@style/SmartbarDivider"/>
<Button
android:id="@+id/candidate1"
style="@style/SmartbarCandidate"/>
style="@style/SmartbarCandidate"
android:text="not yet"/>
<View style="@style/SmartbarDivider"/>
<Button
android:id="@+id/candidate2"
style="@style/SmartbarCandidate"/>
style="@style/SmartbarCandidate"
android:text="implemented"/>
</LinearLayout>
<LinearLayout
android:id="@+id/clipboard_suggestion_row"
style="@style/SmartbarContainer">
<Button
android:id="@+id/clipboard_suggestion"
android:drawableStart="@drawable/ic_content_paste_with_padding"
style="@style/SmartbarQuickAction.ClipboardSuggestion"/>
</LinearLayout>
<LinearLayout
android:id="@+id/quick_actions"
style="@style/SmartbarContainer"
android:visibility="gone">
style="@style/SmartbarContainer">
<dev.patrickgold.florisboard.ime.text.smartbar.SmartbarQuickActionButton
android:id="@+id/quick_action_undo"
style="@style/SmartbarQuickAction"
android:contentDescription="@string/smartbar__quick_action__undo"
android:src="@drawable/ic_undo"/>
<dev.patrickgold.florisboard.ime.text.smartbar.SmartbarQuickActionButton
android:id="@+id/quick_action_redo"
style="@style/SmartbarQuickAction"
android:contentDescription="@string/smartbar__quick_action__redo"
android:src="@drawable/ic_redo"/>
<dev.patrickgold.florisboard.ime.text.smartbar.SmartbarQuickActionButton
android:id="@+id/quick_action_switch_to_media_context"
@@ -65,7 +108,7 @@
android:id="@+id/quick_action_one_handed_toggle"
style="@style/SmartbarQuickAction"
android:contentDescription="@string/smartbar__quick_action__one_handed_mode"
android:src="@drawable/ic_keyboard_arrow_right"/>
android:src="@drawable/ic_smartphone"/>
<!-- TODO: find better icon for editing -->
<dev.patrickgold.florisboard.ime.text.smartbar.SmartbarQuickActionButton
@@ -76,133 +119,33 @@
</LinearLayout>
<!-- TODO: integrate a KeyboardView instead of hardcoding these buttons -->
<LinearLayout
android:id="@+id/number_row"
style="@style/SmartbarContainer"
android:visibility="gone"
tools:ignore="HardcodedText">
<Button
android:id="@+id/number_row_1"
style="@style/SmartbarCandidate"
android:text="1"/>
<Button
android:id="@+id/number_row_2"
style="@style/SmartbarCandidate"
android:text="2"/>
<Button
android:id="@+id/number_row_3"
style="@style/SmartbarCandidate"
android:text="3"/>
<Button
android:id="@+id/number_row_4"
style="@style/SmartbarCandidate"
android:text="4"/>
<Button
android:id="@+id/number_row_5"
style="@style/SmartbarCandidate"
android:text="5"/>
<Button
android:id="@+id/number_row_6"
style="@style/SmartbarCandidate"
android:text="6"/>
<Button
android:id="@+id/number_row_7"
style="@style/SmartbarCandidate"
android:text="7"/>
<Button
android:id="@+id/number_row_8"
style="@style/SmartbarCandidate"
android:text="8"/>
<Button
android:id="@+id/number_row_9"
style="@style/SmartbarCandidate"
android:text="9"/>
<Button
android:id="@+id/number_row_0"
style="@style/SmartbarCandidate"
android:text="0"/>
</LinearLayout>
<!-- TODO: integrate a KeyboardView instead of hardcoding these buttons -->
<LinearLayout
<dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
android:id="@+id/clipboard_cursor_row"
style="@style/SmartbarContainer"
android:visibility="gone"
tools:ignore="HardcodedText">
android:layoutDirection="ltr"/>
<ImageButton
android:id="@+id/cc_select_all"
style="@style/SmartbarCandidate"
android:src="@drawable/ic_select_all"
android:tint="@drawable/button_key_enable_color_selector"/>
<ImageButton
android:id="@+id/cc_copy"
style="@style/SmartbarCandidate"
android:src="@drawable/ic_content_copy"
android:tint="@drawable/button_key_enable_color_selector"/>
<ImageButton
android:id="@+id/cc_arrow_left"
style="@style/SmartbarCandidate"
android:src="@drawable/ic_keyboard_arrow_left"
android:tint="@drawable/button_key_enable_color_selector"/>
<ImageButton
android:id="@+id/cc_arrow_right"
style="@style/SmartbarCandidate"
android:src="@drawable/ic_keyboard_arrow_right"
android:tint="@drawable/button_key_enable_color_selector"/>
<ImageButton
android:id="@+id/cc_cut"
style="@style/SmartbarCandidate"
android:src="@drawable/ic_content_cut"
android:tint="@drawable/button_key_enable_color_selector"/>
<ImageButton
android:id="@+id/cc_paste"
style="@style/SmartbarCandidate"
android:src="@drawable/ic_content_paste"
android:tint="@drawable/button_key_enable_color_selector"/>
</LinearLayout>
<!-- Placeholder on the right which reserves the space for a second button -->
<dev.patrickgold.florisboard.ime.text.smartbar.SmartbarQuickActionButton
android:layout_width="wrap_content"
<dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
android:id="@+id/number_row"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/smartbar_button_margin"
android:clickable="false"
android:visibility="invisible"/>
android:layoutDirection="ltr"/>
</LinearLayout>
</dev.patrickgold.florisboard.ime.core.FlorisViewFlipper>
<LinearLayout
android:id="@+id/smartbar_variant_back_only"
android:layout_width="match_parent"
<dev.patrickgold.florisboard.ime.core.FlorisViewFlipper
android:id="@+id/action_end_area"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:visibility="gone">
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<dev.patrickgold.florisboard.ime.text.smartbar.SmartbarQuickActionButton
android:id="@+id/back_button"
style="@style/SmartbarQuickAction"
android:contentDescription="@string/smartbar__quick_action__exit_editing"
android:src="@drawable/ic_arrow_back"/>
android:id="@+id/private_mode_button"
style="@style/SmartbarQuickAction.PrivateModeButton"
android:contentDescription="@string/smartbar__quick_action__private_mode"
android:src="@drawable/ic_security"/>
</LinearLayout>
</dev.patrickgold.florisboard.ime.core.FlorisViewFlipper>
</dev.patrickgold.florisboard.ime.text.smartbar.SmartbarView>

View File

@@ -0,0 +1,222 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="key__phone_pause" comment="Label for the Pause key in the telephone keyboard layout">إيقاف مؤقت</string>
<string name="key__phone_wait" comment="Label for the Wait key in the telephone keyboard layout">انتظار</string>
<string name="key_popup__threedots_alt" comment="Content description for the three-dots icon in a key popup">أيقونة ثلاث نقاط. إذا كانت ظاهرة ، تشير إلى أنه يمكن استخدام المزيد من الأحرف إذا تم الضغط عليها لفترة أطول.</string>
<!-- One-handed strings -->
<string name="one_handed__close_btn_content_description" comment="Content description for the one-handed close button">غلق وضع اليد الواحدة.</string>
<string name="one_handed__move_start_btn_content_description" comment="Content description for the one-handed move to left button">نقل لوحة المفاتيح إلى اليسار.</string>
<string name="one_handed__move_end_btn_content_description" comment="Content description for the one-handed move to right button">نقل لوحة المفاتيح إلى اليمين.</string>
<!-- Media strings -->
<string name="media__tab__emojis" comment="Tab description for emojis in the media UI">ايموجي</string>
<string name="media__tab__emoticons" comment="Tab description for emoticons in the media UI">رموز تعبيرية</string>
<string name="media__tab__kaomoji" comment="Tab description for kaomoji in the media UI">كاوموجي</string>
<!-- Emoji strings -->
<string name="emoji__category__smileys_emotion" comment="Emoji category name">وجوه تعبيرية و عواطف</string>
<string name="emoji__category__people_body" comment="Emoji category name">أشخاص و أجسام</string>
<string name="emoji__category__animals_nature" comment="Emoji category name">حيوانات و طبيعة</string>
<string name="emoji__category__food_drink" comment="Emoji category name">مأكولات و مشروبات</string>
<string name="emoji__category__travel_places" comment="Emoji category name">سفر و أماكن</string>
<string name="emoji__category__activities" comment="Emoji category name">أنشطة</string>
<string name="emoji__category__objects" comment="Emoji category name">أشياء</string>
<string name="emoji__category__symbols" comment="Emoji category name">رموز</string>
<string name="emoji__category__flags" comment="Emoji category name">أعلام</string>
<!-- Smartbar strings -->
<string name="smartbar__quick_action_toggle__alt" comment="Content description for the quick action toggle button in Smartbar">زر اﻹجراءات السريعة. عند الضغط عليه ، يمكنك التبديل بين اقتراحات الكلمات و أزرار الإجراءات السريعة.</string>
<string name="smartbar__quick_action__exit_editing" comment="Content-description for the exit editing layout button in Smartbar">الخروج من لوحة التعديل النصي.</string>
<string name="smartbar__quick_action__one_handed_mode" comment="Content-description for the one-handed quick action in Smartbar">تغيير حالة وضع اليد الواحدة.</string>
<string name="smartbar__quick_action__open_settings" comment="Content-description for the settings quick action in Smartbar">فتح الإعدادات.</string>
<string name="smartbar__quick_action__switch_to_editing_context" comment="Content-description for the editing quick action in Smartbar">الإنتقال إلى لوحة التعديل النصي.</string>
<string name="smartbar__quick_action__switch_to_media_context" comment="Content-description for the media quick action in Smartbar">الإنتقال إلى لوحة إدخال الوسائط.</string>
<!-- Settings UI strings -->
<string name="settings__title" comment="Title of Settings">الاعدادات</string>
<string name="settings__menu" comment="Hint of top-right three-dot icon in Settings">المزيد من الخيارات</string>
<string name="settings__menu_help" comment="Three-dot menu entry for Help and Feedback web link">المساعدة والملاحظات</string>
<string name="settings__navigation__home" comment="Long-press hint of bottom nav item Home in Settings">الصفحة الرئيسية</string>
<string name="settings__navigation__keyboard" comment="Long-press hint of bottom nav item Keyboard in Settings">لوحة المفاتيح</string>
<string name="settings__navigation__typing" comment="Long-press hint of bottom nav item Typing in Settings">الكتابة</string>
<string name="settings__navigation__theme" comment="Long-press hint of bottom nav item Theme in Settings">المظهر</string>
<string name="settings__navigation__gestures" comment="Long-press hint of bottom nav item Gestures in Settings">الإيماءات</string>
<string name="settings__default" comment="General string which is used when a preference has the default value set">الإفتراضي</string>
<string name="settings__system_default" comment="General string which is used when a preference has the system default value set">الإعداد الافتراضي</string>
<string name="settings__home__title" comment="Title of the Home fragment">مرحبا بكم في %s</string>
<string name="settings__home__ime_not_enabled" comment="Error message shown in Home fragment when FlorisBoard is not enabled in the system">لم يتم تمكين FlorisBoard في النظام وبالتالي لن يكون متاحًا كطريقة إدخال في منتقي الإدخال. انقر هنا لحل هذه المشكلة.</string>
<string name="settings__home__ime_not_selected" comment="Warning message shown in Home fragment when FlorisBoard is not selected as the default keyboard">لم يتم اختيار FlorisBoard كطريقة الإدخال الافتراضية. انقر هنا لحل هذه المشكلة.</string>
<string name="settings__home__contribute" comment="Contributing message shown in Home fragment">شكرا على تجربة FlorisBoard! هذا المشروع لا يزال في المرحلة ألفا وبالتالي يفتقد الميزات. إذا وجدت أي أخطاء أو تريد تقديم اقتراح ، فالرجاء مراجعة المخزن على GitHub وطرح مشكلة. هذا يساعد في جعل FlorisBoard أفضل. شكرا جزيلا!</string>
<string name="settings__localization__title" comment="Title of languages and layout box in the Typing fragment">اللغات وتخطيطات لوحة المفاتيح</string>
<string name="settings__localization__subtype_no_subtypes_configured_warning" comment="Warning message that no subtype has been defined in the Typing fragment">يبدو أنك لم تقم بإختيار أية أنواع فرعية. كبديل، سيتم استخدام النوع الفرعي English / QWERTY!</string>
<string name="settings__localization__subtype_add" comment="Subtype dialog add button">إضافة</string>
<string name="settings__localization__subtype_add_title" comment="Title of subtype dialog when adding a new subtype">إضافة نوع فرعي</string>
<string name="settings__localization__subtype_apply" comment="Subtype dialog apply button">تطبيق</string>
<string name="settings__localization__subtype_cancel" comment="Subtype dialog cancel button">إلغاء</string>
<string name="settings__localization__subtype_delete" comment="Subtype dialog delete button">حذف</string>
<string name="settings__localization__subtype_edit_title" comment="Title of subtype dialog when editing an existing subtype">تعديل نوع فرعي</string>
<string name="settings__localization__subtype_locale" comment="Label for locale dropdown in subtype dialog">اللغة</string>
<string name="settings__localization__subtype_layout" comment="Label for keyboard layout dropdown in subtype dialog">تخطيط لوحة المفاتيح</string>
<string name="settings__localization__subtype_error_already_exists" comment="Error message shown in subtype dialog when a subtype to add already exists">هذا النوع الفرعي موجود مسبقا!</string>
<string name="settings__theme__title" comment="Title of the Theme fragment">مظهر لوحة المفاتيح</string>
<string name="settings__theme__undefined" comment="General string for an undefined preference value">غير محدد</string>
<string name="settings__theme__preset_title" comment="Label of the theme preset preference">المظهر</string>
<string name="settings__theme__preset_summary" comment="Summary of the theme preset preference">مخصص (بناء على %s)</string>
<string name="settings__theme__preset_dialog_selected_theme" comment="Label of the selected themes list">المظهر المحدد:</string>
<string name="settings__theme__preset_dialog_available_themes" comment="Label of the available themes list">المظاهر المتاحة:</string>
<string name="settings__theme__preset_dialog_alt_arrow_right" comment="Content description of the theme selection button in theme dialog">سهم لليمين</string>
<string name="settings__theme__background" comment="General label for a background preference">لون الخلفية</string>
<string name="settings__theme__background_active" comment="General label for an active background preference">لون الخلفية عند التنشيط</string>
<string name="settings__theme__background_pressed" comment="General label for a pressed background preference">لون الخلفية عند الضغط</string>
<string name="settings__theme__foreground" comment="General label for a foreground preference">لون الواجهة</string>
<string name="settings__theme__foreground_alt" comment="General label for an alternate foreground preference">لون الواجهة (البديل)</string>
<string name="settings__theme__foreground_capslock" comment="General label for a capslock foreground preference">لون الواجهة (وضع الحروف الكبيرة)</string>
<string name="settings__theme__dialog_title" comment="Title of the color selection dialog for a single theme preference">اختر اللون</string>
<string name="settings__theme__group_window" comment="Theme group label">النافذة والنظام</string>
<string name="settings__theme__group_keyboard" comment="Theme group label">لوحة المفاتيح</string>
<string name="settings__theme__group_key" comment="Theme group label">المفتاح</string>
<string name="settings__theme__group_key_enter" comment="Theme group label">مفتاح الإدخال</string>
<string name="settings__theme__group_key_popup" comment="Theme group label">نافذة المفتاح المنبثقة</string>
<string name="settings__theme__group_key_shift" comment="Theme group label">مفتاح التحويل</string>
<string name="settings__theme__group_media" comment="Theme group label">سياق الوسائط</string>
<string name="settings__theme__group_one_handed" comment="Theme group label">اليد الواحدة</string>
<string name="settings__theme__group_one_handed_button" comment="Theme group label">زر اليد الواحدة</string>
<string name="settings__theme__group_smartbar" comment="Theme group label">الشريط الذكـي</string>
<string name="settings__theme__group_smartbar_button" comment="Theme group label">زر الشريط الذكي</string>
<string name="pref__theme__colorPrimary_title" comment="Title of Color primary theme preference">اللون الأساسي</string>
<string name="pref__theme__colorPrimary_summary" comment="Summary of Color primary theme preference">يتم تطبيقه على تموج علامة تبويب الوسائط الرئيسية وإبراز الاختيار</string>
<string name="pref__theme__colorPrimaryDark_title" comment="Title of Color primary dark theme preference">اللون الأساسي (داكن)</string>
<string name="pref__theme__colorPrimaryDark_summary" comment="Summary of Color primary dark theme preference">غير مستخدم حاليا ، محجوز للتنفيذ المستقبلي</string>
<string name="pref__theme__colorAccent_title" comment="Title of Color accent theme preference">لون التمييز</string>
<string name="pref__theme__colorAccent_summary" comment="Summary of Color accent theme preference">يتم تطبيقه على علامة تبويب الإيموجي</string>
<string name="pref__theme__navBarColor_title" comment="Title of Nav bar color theme preference">لون شريط التصفّح</string>
<string name="pref__theme__navBarColor_summary" comment="Summary of Nav bar color theme preference">خلفية شريط التصفّح.</string>
<string name="pref__theme__navBarIsLight_title" comment="Title of Nav bar is light theme preference">واجهة شريط التصفّح</string>
<string name="pref__theme__navBarIsLight_summary" comment="Summary of Nav bar is light theme preference">ضبط على التشغيل للواجهة الداكنة أو على إيقاف التشغيل للواجهة الفاتحة.</string>
<string name="settings__keyboard__title" comment="Title of Keyboard preferences fragment">تفضيلات لوحة المفاتيح</string>
<string name="pref__keyboard__group_keys__label" comment="Preference group title">المفاتيح</string>
<string name="pref__keyboard__number_row__label" comment="Preference title">صف الأعداد</string>
<string name="pref__keyboard__number_row__summary" comment="Preference summary">إظهار صف الأعداد فوق تخطيط الحروف</string>
<string name="pref__keyboard__font_size_multiplier_portrait__label" comment="Preference title">مضاعف حجم الخط (عمودي)</string>
<string name="pref__keyboard__font_size_multiplier_landscape__label" comment="Preference title">مضاعف حجم الخط (أفقي)</string>
<string name="pref__keyboard__group_layout__label" comment="Preference group title">نظام التخطيط</string>
<string name="pref__keyboard__one_handed_mode__label" comment="Preference value">وضع اليد الواحدة</string>
<string name="pref__keyboard__one_handed_mode__off" comment="Preference value">إيقاف</string>
<string name="pref__keyboard__one_handed_mode__right" comment="Preference value">وضع اليد اليمنى</string>
<string name="pref__keyboard__one_handed_mode__left" comment="Preference value">وضع اليد اليسرى</string>
<string name="pref__keyboard__height_factor__label" comment="Preference title">ارتفاع لوحة المفاتيح</string>
<string name="pref__keyboard__height_factor__extra_short" comment="Preference value">قصير جدا</string>
<string name="pref__keyboard__height_factor__short" comment="Preference value">قصير</string>
<string name="pref__keyboard__height_factor__mid_short" comment="Preference value">قصير قليلا</string>
<string name="pref__keyboard__height_factor__normal" comment="Preference value">عادي</string>
<string name="pref__keyboard__height_factor__mid_tall" comment="Preference value">طويل قليلا</string>
<string name="pref__keyboard__height_factor__tall" comment="Preference value">طويل</string>
<string name="pref__keyboard__height_factor__extra_tall" comment="Preference value">طويل جدا</string>
<string name="pref__keyboard__height_factor__custom" comment="Preference value">مخصص</string>
<string name="pref__keyboard__height_factor_custom__label" comment="Preference title">قيمة ارتفاع لوحة المفاتيح المخصصة</string>
<string name="pref__keyboard__bottom_offset__label" comment="Preference title">الإزاحة السفلية (للشاشات المقوسة)</string>
<string name="pref__keyboard__group_keypress__label" comment="Preference group title">ضغط المفتاح</string>
<string name="pref__keyboard__sound_enabled__label" comment="Preference title">الصوت عند ضغط المفتاح</string>
<string name="pref__keyboard__sound_volume__label" comment="Preference title">حجم الصوت عند ضغط المفتاح</string>
<string name="pref__keyboard__vibration_enabled__label" comment="Preference title">إهتزاز عند ضغط المفتاح</string>
<string name="pref__keyboard__vibration_strength__label" comment="Preference title">شدة الإهتزاز عند ضغط المفتاح</string>
<string name="pref__keyboard__popup_visible__label" comment="Preference title">رؤية النافذة المنبثقة</string>
<string name="pref__keyboard__popup_visible__summary" comment="Preference summary">إظهار النافذة المنبثقة عندما تضغط على مفتاح</string>
<string name="pref__keyboard__long_press_delay__label" comment="Preference title">مدة الضغط المطوّل على المفتاح</string>
<string name="settings__typing__title" comment="Title of Typing experience fragment">تجربة الكتابة</string>
<string name="pref__smartbar__enabled__label" comment="Preference title">تفعيل الشريط الذكي</string>
<string name="pref__smartbar__enabled__summary" comment="Preference summary">سوف يظهر أعلى لوحة المفاتيح</string>
<string name="pref__suggestion__title" comment="Preference group title">الإقتراحات</string>
<string name="pref__suggestion__enabled__label" comment="Preference title">[NYI] إظهار الإقتراحات عند الكتابة</string>
<string name="pref__suggestion__enabled__summary" comment="Preference summary">سوف يظهر الشريط الذكي</string>
<string name="pref__suggestion__suggest_clipboard_content__label" comment="Preference title">إقتراحات محتوى الحافظة</string>
<string name="pref__suggestion__suggest_clipboard_content__summary" comment="Preference summary">إقتراح لصق محتوى الحافظة إذا تم نسخه مسبقًا</string>
<string name="pref__suggestion__use_pref_words__label" comment="Preference title">[NYI] إقتراحات الكلمة التالية</string>
<string name="pref__suggestion__use_pref_words__summary" comment="Preference summary">إستخدام الكلمات السابقة لتحسين الاقتراحات</string>
<string name="pref__correction__title" comment="Preference group title">الإصلاحات</string>
<string name="pref__correction__auto_capitalization__label" comment="Preference title">استخدام الأحرف الكبيرة تلقائيًا</string>
<string name="pref__correction__auto_capitalization__summary" comment="Preference summary">استخدام الأحرف الكبيرة في الكلمات على حسب سياق نص الإدخال الحالي</string>
<string name="pref__correction__remember_caps_lock_state__label" comment="Preference title">تذكر حالة زر الأحرف الكبيرة</string>
<string name="pref__correction__remember_caps_lock_state__summary" comment="Preference summary">يضل وضع الأحرف الكبيرة قيد التشغيل عند الإنتقال إلى حقل نصي آخر</string>
<string name="pref__correction__double_space_period__label" comment="Preference title">نقطة المسافة المزدوجة</string>
<string name="pref__correction__double_space_period__summary" comment="Preference summary">الضغط مرتين على مفتاح المسافة يضيف نقطة متبوعة بمسافة</string>
<string name="settings__gestures__title" comment="Title of Gestures fragment">الإيماءات و الكتابة بالتمرير</string>
<string name="pref__glide__title" comment="Preference group title">الكتابة بالتمرير</string>
<string name="pref__glide__enabled__label" comment="Preference title">[NYI] تفعيل الكتابة بالتمرير</string>
<string name="pref__glide__enabled__summary" comment="Preference summary">أكتب كلمة بتمرير إصبعك عبر حروفها</string>
<string name="pref__glide__show_trail__label" comment="Preference title">[NYI] إظهار آثار التمرير</string>
<string name="pref__glide__show_trail__summary" comment="Preference summary">سوف يختفي بعد كل كلمة</string>
<string name="pref__gestures__title" comment="Preference group title">الإيماءات</string>
<string name="pref__gestures__swipe_action__no_action" comment="Preference value for swipe action">بدون إجراء</string>
<string name="pref__gestures__swipe_action__delete_characters_precisely" comment="Preference value for swipe action">حذف الحروف بدقة</string>
<string name="pref__gestures__swipe_action__delete_word" comment="Preference value for swipe action">حذف الكلمة الحالية</string>
<string name="pref__gestures__swipe_action__delete_words_precisely" comment="Preference value for swipe action">حذف الكلمات بدقة</string>
<string name="pref__gestures__swipe_action__hide_keyboard" comment="Preference value for swipe action">إخفاء لوحة المفاتيح</string>
<string name="pref__gestures__swipe_action__move_cursor_up" comment="Preference value for swipe action">تحريك المؤشر إلى الأعلى</string>
<string name="pref__gestures__swipe_action__move_cursor_down" comment="Preference value for swipe action">تحريك المؤشر إلى الأسفل</string>
<string name="pref__gestures__swipe_action__move_cursor_left" comment="Preference value for swipe action">تحريك المؤشر إلى اليسار</string>
<string name="pref__gestures__swipe_action__move_cursor_right" comment="Preference value for swipe action">تحريك المؤشر إلى اليمين</string>
<string name="pref__gestures__swipe_action__shift" comment="Preference value for swipe action">مفتاح التحويل</string>
<string name="pref__gestures__swipe_action__switch_to_prev_subtype" comment="Preference value for swipe action">التبديل إلى النوع الفرعي السابق</string>
<string name="pref__gestures__swipe_action__switch_to_next_subtype" comment="Preference value for swipe action">التبديل إلى النوع الفرعي التالي</string>
<string name="pref__gestures__swipe_up__label" comment="Preference title">السحب للأعلى</string>
<string name="pref__gestures__swipe_down__label" comment="Preference title">السحب للأسفل</string>
<string name="pref__gestures__swipe_left__label" comment="Preference title">السحب لليسار</string>
<string name="pref__gestures__swipe_right__label" comment="Preference title">السحب لليمين</string>
<string name="pref__gestures__space_bar_swipe_left__label" comment="Preference title">السحب لليسار من خلال مفتاح المسافة</string>
<string name="pref__gestures__space_bar_swipe_right__label" comment="Preference title">السحب لليمين من خلال مفتاح المسافة</string>
<string name="pref__gestures__delete_key_swipe_left__label" comment="Preference title">السحب لليسار من خلال مفتاح الحذف</string>
<string name="pref__gestures__swipe_velocity_threshold__label" comment="Preference title">عتبة سرعة السحب</string>
<string name="pref__gestures__swipe_velocity_threshold__very_slow" comment="Preference value for swipe velocity threshold">بطيئة جداً</string>
<string name="pref__gestures__swipe_velocity_threshold__slow" comment="Preference value for swipe velocity threshold">بطيئة</string>
<string name="pref__gestures__swipe_velocity_threshold__normal" comment="Preference value for swipe velocity threshold">عادية</string>
<string name="pref__gestures__swipe_velocity_threshold__fast" comment="Preference value for swipe velocity threshold">سريعة</string>
<string name="pref__gestures__swipe_velocity_threshold__very_fast" comment="Preference value for swipe velocity threshold">سريعة جداً</string>
<string name="pref__gestures__swipe_distance_threshold__label" comment="Preference title">عتبة مسافة السحب</string>
<string name="pref__gestures__swipe_distance_threshold__very_short" comment="Preference value for swipe distance threshold">قصيرة جداً</string>
<string name="pref__gestures__swipe_distance_threshold__short" comment="Preference value for swipe distance threshold">قصيرة</string>
<string name="pref__gestures__swipe_distance_threshold__normal" comment="Preference value for swipe distance threshold">عادية</string>
<string name="pref__gestures__swipe_distance_threshold__long" comment="Preference value for swipe distance threshold">طويلة</string>
<string name="pref__gestures__swipe_distance_threshold__very_long" comment="Preference value for swipe distance threshold">طويلة جداً</string>
<string name="settings__advanced__title" comment="Title of Advanced settings activity">إعدادات متقدمة</string>
<string name="pref__advanced__settings_theme__label" comment="Label of Settings theme preference in Advanced">إعدادات المظهر</string>
<string name="pref__advanced__settings_theme__light" comment="Possible value of Settings theme preference in Advanced">فاتح</string>
<string name="pref__advanced__settings_theme__dark" comment="Possible value of Settings theme preference in Advanced">داكن</string>
<string name="pref__advanced__show_app_icon__label" comment="Label of Show app icon preference in Advanced">إظهار أيقونة البرنامج في صفحة الهاتف الرئيسية</string>
<!-- About UI strings -->
<string name="about__title" comment="Title of About activity">حول التطبيق</string>
<string name="about__app_icon_content_description" comment="Content description of app icon in About">أيقونة التطبيق FlorisBoard</string>
<string name="about__view_licenses" comment="Label of View licenses button in About">تراخيص البرامج مفتوحة المصدر</string>
<string name="about__view_privacy_policy" comment="Label of View privacy policy button in About">سياسة الخصوصية</string>
<string name="about__view_source_code" comment="Label of View source code button in About">مصدر التعليمات البرمجية</string>
<string name="about__license__title" comment="Title of Open-source licenses dialog">تراخيص البرامج مفتوحة المصدر</string>
<!-- Setup UI strings -->
<string name="setup__title" comment="Title of Setup">الإعداد</string>
<string name="setup__prev_button" comment="Label of Previous button in Setup (try to find a short translation due to limited space in UI)">السابق</string>
<string name="setup__cancel_button" comment="Label of Cancel button in Setup">إلغاء</string>
<string name="setup__next_button" comment="Label of Next button in Setup (try to find a short translation due to limited space in UI)">التالي</string>
<string name="setup__finish_button" comment="Label of Finish button in Setup">إنهاء</string>
<string name="setup__ok_button" comment="Label of OK button in Setup">حسنًا</string>
<string name="setup__welcome__title" comment="Title of Welcome fragment in Setup">مرحبا!</string>
<string name="setup__welcome__intro" comment="Paragraph in Welcome fragment in Setup">شكرا على تجربة FlorisBoard! قبل أن تتمكن من البدء في استخدامه ، يتعين علينا القيام بالأشياء المعتادة وذلك بتمكينه في إعدادات النظام ، وإعداد لغتك / تخطيطك المفضل ، إلخ... ولكن لا تقلق - سيرشدك معالج الإعداد خلال هذا!</string>
<string name="setup__welcome__privacy" comment="Paragraph in Welcome fragment in Setup">يحترم FlorisBoard خصوصيتك تمامًا ولا يجمع أي بيانات مستخدم. لمزيد من المعلومات انظر هنا:</string>
<string name="setup__welcome__trust" comment="Paragraph in Welcome fragment in Setup">مصدر التعليمات البرمجية لـ FlorisBoard متاح للجميع ، لذا يمكنك بسهولة مراجعة ما يفعله FlorisBoard في الخلفية. يرجى مراجعة الرابط الخاص بالمستودع أدناه.</string>
<string name="setup__welcome__contribute" comment="Paragraph in Welcome fragment in Setup">شيء أخير قبل بدء الإعداد - إذا واجهت أي أخطاء / أعطال / مشكلات في FlorisBoard أو كان لديك طلب ميزة - توجه إلى مستودع GitHub الموجود في الرابط أدناه واطرح مشكلة. هذا يساعد في تحسين التجربة لجميع المستخدمين!</string>
<string name="setup__welcome__outro" comment="Paragraph in Welcome fragment in Setup">لبدء الإعداد إضغط على <i>التالي</i>.</string>
<string name="setup__enable_ime__title" comment="Title of Enable IME fragment in Setup">تفعيل FlorisBoard</string>
<string name="setup__enable_ime__text_before_enabled" comment="Description of state in Enable IME fragment before user enabled">يتطلب Android تمكين كل لوحة مفاتيح مخصصة يدويًا قبل أن تتمكن من استخدامها. إضغط على الزر أدناه للتوجه إلى <i>اللغات و الإدخال</i> الإعدادات, ثم تأكد من إختيار \'<i>FlorisBoard</i>\'.</string>
<string name="setup__enable_ime__text_after_enabled" comment="Description of state in Enable IME fragment after user enabled">تم تفعيل FlorisBoard بنجاح. للمواصلة إضغط على <i>التالي</i>!</string>
<string name="setup__enable_ime__text_button_language_and_input" comment="Label of language and input button in Enable IME fragment">فتح إعدادات اللغات و الإدخال</string>
<string name="setup__make_default__title" comment="Title of Make IME default fragment in Setup">جعل FlorisBoard افتراضيًا</string>
<string name="setup__make_default__text_before_switch" comment="Description of state in Make IME default fragment before user switched">تم تمكين FlorisBoard الآن في نظامك. لاستخدامه بشكل دائم ، قم بالتبديل إلى FlorisBoard عن طريق تحديده في مربع حوار محدد الإدخال!</string>
<string name="setup__make_default__text_after_switch" comment="Description of state in Make IME default fragment after user switched">تم تبديل لوحة المفاتيح الإفتراضية إلى FlorisBoard بنجاح!</string>
<string name="setup__make_default__text_switch_button" comment="Label of switch button in Make IME default fragment">تبديل لوحة المفاتيح</string>
<string name="setup__finish__title" comment="Title of Setup finished fragment in Setup">تم إنهاء الاعداد!</string>
<!-- Crash Dialog strings -->
<string name="crash_dialog__title" comment="Title of crash dialog">تقرير حول خطأ في FlorisBoard</string>
<string name="crash_dialog__description" comment="Description of crash dialog">نأسف للإزعاج ، ولكن FlorisBoard تعطل بسبب خطأ غير متوقع.\n\nإذا كنت ترغب في الإبلاغ عن هذا الخطأ ، انقر على \"نسخ إلى الحافظة\" ، ثم على الزر \"فتح تقرير الخطأ\". املأ تقرير الخطأ والصق السجل. هذا يساعد في جعل FlorisBoard أفضل وأكثر استقرارًا للجميع. شكرا جزيلا!</string>
<string name="crash_dialog__copy_to_clipboard" comment="Label of Copy to clipboard button in crash dialog">نسخ إلى الحافظة</string>
<string name="crash_dialog__open_bug_report_form" comment="Label of Open bug report button in crash dialog">فتح نموذج تقرير الخطأ (github.com)</string>
<string name="crash_dialog__close" comment="Label of Close button in crash dialog">إغلاق</string>
<string name="crash_notification_channel__title" comment="Title of crash notification channel">تقارير حول خطأ في FlorisBoard</string>
<string name="crash_once_notification__title" comment="Title of the notification for a single crash">توقف FlorisBoard عن العمل…</string>
<string name="crash_once_notification__body" comment="Body of the notification for a single crash">انقر لعرض تفاصيل الخطأ</string>
<string name="crash_multiple_notification__title" comment="Title of the notification for consecutive crashes">يبدو أن FlorisBoard يوقف عن العمل بشكل متكرر…</string>
<string name="crash_multiple_notification__body" comment="Body of the notification for consecutive crashes">العودة إلى لوحة المفاتيح السابقة لإيقاف حلقة التعطل الغير منتهية. انقر لعرض تفاصيل الخطأ</string>
</resources>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- One-handed strings -->
<!-- Media strings -->
<!-- Emoji strings -->
<!-- Smartbar strings -->
<!-- Settings UI strings -->
<!-- About UI strings -->
<!-- Setup UI strings -->
<!-- Crash Dialog strings -->
</resources>

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