Compare commits

...

147 Commits

Author SHA1 Message Date
Patrick Goldinger
65595076ee Release v0.5.0-alpha02 2025-05-09 00:42:58 +02:00
florisboard-bot
bf1563ae78 Update translations from Crowdin 2025-05-09 00:33:24 +02:00
Patrick Goldinger
d111771e06 Upgrade jetpref and compose-tooltip 2025-05-09 00:29:24 +02:00
Patrick Goldinger
00ce01e102 Fix inline autofill chip style (#2868) 2025-05-07 00:59:10 +02:00
Patrick Goldinger
15b7022204 Improve Select files screen UX (#2859, #2860)
Co-authored-by: Lars Mühlbauer <lm41@lm41.xyz>
2025-05-07 00:27:00 +02:00
Lars Mühlbauer
759b7f0033 Add language switch option to smartbar (#2816)
* Add language switch option to smartbar.

* Only enable switch language button when there are multiple subtypes
2025-05-06 23:31:23 +02:00
Lars Mühlbauer
73c3777529 Add transparent navigation bar to android xml themes (#2867) 2025-05-06 22:54:28 +02:00
Patrick Goldinger
a365e76383 Release v0.5.0-alpha01 2025-05-04 03:58:50 +02:00
florisboard-bot
7c4747d451 Update translations from Crowdin 2025-05-04 03:49:59 +02:00
Patrick Goldinger
12be43e100 Merge pull request #2855 from florisboard/feat/snyggv2
Theme rework part II (Snygg v2)
2025-05-04 03:41:36 +02:00
klaurence
207cf244e6 Change "Material you" to "Material You" (#2827) 2025-05-04 03:36:47 +02:00
lm41
6f45cf8456 Remove println 2025-05-04 03:34:42 +02:00
Patrick Goldinger
5b73de5700 Add and improve theme translations
Co-authored-by: Lars Mühlbauer <lm41@lm41.xyz>
2025-05-04 03:06:30 +02:00
Patrick Goldinger
25c94e8270 Fix SnyggBox image sizing and SnyggText clipping
Co-authored-by: Lars Mühlbauer <lm41@lm41.xyz>
2025-05-04 01:48:43 +02:00
Patrick Goldinger
0cd703427a Fix custom rule ordering and other issues in theme editor
Co-authored-by: Lars Mühlbauer <lm41@lm41.xyz>
2025-05-04 00:00:41 +02:00
Patrick Goldinger
3871978abc Add custom sorting of element names in theme editor 2025-05-03 22:26:57 +02:00
lm41
a9502be471 Improve import addons UX for themes 2025-05-03 17:41:45 +02:00
lm41
a5f2844488 Rework floris_pure_night and floris_pure_night_borderless stylesheets 2025-05-03 16:53:01 +02:00
lm41
dd67497154 Rework floris_night and floris_night_borderless stylesheets 2025-05-03 16:38:44 +02:00
lm41
01c7512c01 Improve floris_day_borderless theme 2025-05-03 15:56:13 +02:00
Patrick Goldinger
fe37ea914c Revert "Enable compose strong skipping mode"
This reverts commit fa768c0e2a.
2025-05-03 15:39:30 +02:00
Patrick Goldinger
fa768c0e2a Enable compose strong skipping mode 2025-05-03 15:01:12 +02:00
Patrick Goldinger
bcd3d1719b Add TODO for nav bar color issue 2025-05-02 04:57:15 +02:00
Patrick Goldinger
55f881d8a0 Fix inline suggestions UI style bundle not being returned 2025-05-02 04:23:55 +02:00
Patrick Goldinger
cd817dec93 Continue adapting base style and IME UI
Co-authored-by: Lars Mühlbauer <lm41@lm41.xyz>
2025-05-02 04:17:56 +02:00
lm41
cdf8bc2825 Move all colors to the @defines block in floris_day 2025-05-01 23:39:45 +02:00
lm41
62d07b0142 Re-introduce SnyggIconButton
This commit reverts some parts of the previous commit
as it massively broke the layout.
2025-05-01 16:19:33 +02:00
lm41
f50cd2de58 Add and improve docs for the Snygg ui components
Co-authored-by: Patrick Goldinger <patrick@patrickgold.dev>
2025-05-01 02:32:00 +02:00
lm41
396a47cde9 Improve docs for SnyggUi 2025-04-30 19:11:12 +02:00
lm41
51e67c7293 Add docs for SnyggIcon and SnyggText 2025-04-30 19:10:39 +02:00
Patrick Goldinger
0bc64c6253 Rework SnyggButton and adapt clipboard clear all dialog style 2025-04-30 02:32:58 +02:00
Patrick Goldinger
f571013fe5 Add custom font name dropdown in theme editor 2025-04-30 01:31:06 +02:00
Patrick Goldinger
5f7ca53458 Fix Snygg rule ordering issue with selectors 2025-04-30 00:14:47 +02:00
Patrick Goldinger
0cacb9a671 Enforce presence of correct schema reference in Snygg stylesheet 2025-04-29 23:52:13 +02:00
lm41
30d36b7541 Improve extension archive file manager screen 2025-04-29 23:08:28 +02:00
Patrick Goldinger
3ece15f516 Continue adaption of base style to Snygg v2 2025-04-28 05:18:41 +02:00
Patrick Goldinger
603fab1831 Add Snygg serialization exceptions and detailed error explanation for missing schema 2025-04-28 04:26:36 +02:00
Patrick Goldinger
90838012ad Add error handling and display for stylesheet load failures 2025-04-28 03:46:53 +02:00
Patrick Goldinger
e366c94d55 Rework SnyggStylesheet serialization and add lenient config and pretty print 2025-04-28 03:06:10 +02:00
Patrick Goldinger
a0ffa3feb1 Rename line-clamp to text-max-lines and reorder properties 2025-04-28 00:59:45 +02:00
Patrick Goldinger
ecbf5c4e66 Add preview icon for most new values in theme editor 2025-04-28 00:34:18 +02:00
Patrick Goldinger
023645646c Finish implementing multiple property sets editor UI in theme editor 2025-04-28 00:10:47 +02:00
lm41
553251d3c1 WIP: Add propertySet editing for SnyggMultiplePropertySetsEditor values 2025-04-27 19:20:03 +02:00
Patrick Goldinger
92ce04d5d9 Implement font-style and font-weight into Snygg font rule 2025-04-27 17:09:54 +02:00
Patrick Goldinger
9930eb1bca Implement required properties into Snygg spec and serialization 2025-04-27 17:01:56 +02:00
Patrick Goldinger
47b2d81f72 Implement Snygg multiple property sets functionality into spec 2025-04-27 16:18:22 +02:00
Patrick Goldinger
e6d6260d72 Implement file selector for Snygg URI values in theme editor
Co-authored-by: Lars Mühlbauer <lm41@lm41.xyz>
2025-04-27 02:32:55 +02:00
Patrick Goldinger
dd02948a00 Improve Snygg image support and rename object-fit to content-scale 2025-04-27 01:22:22 +02:00
Patrick Goldinger
4a12b3ded0 Improve SnyggAssetResolver interface and param type 2025-04-26 04:51:02 +02:00
Patrick Goldinger
6acfa7f349 Implement basic ext editor file management
Co-authored-by: Lars Mühlbauer <lm41@lm41.xyz>
2025-04-26 04:27:16 +02:00
Patrick Goldinger
d3e2dda165 Improve Snygg asset handling and robustness 2025-04-26 04:26:49 +02:00
Patrick Goldinger
a5ec6a10b3 Fix issues with meta / rule decl in Snygg 2025-04-25 22:12:57 +02:00
Patrick Goldinger
0a8260b7fa Add support for title/description in Snygg spec & Upgrade to 2020-12 schema version 2025-04-25 17:40:02 +02:00
Patrick Goldinger
4cddc9b305 Add background image support for IME surface
Co-authored-by: lm41 <lm41@lm41.xyz>
2025-04-25 03:31:58 +02:00
lm41
b4835b869a Add padding value input in theme editor
Co-authored-by: Patrick Goldinger <patrick@patrickgold.dev>
2025-04-25 00:16:10 +02:00
Patrick Goldinger
11c6dc8a0d Continue adaption of base styles & Fix inherit behavior bug in Snygg 2025-04-24 18:07:53 +02:00
Patrick Goldinger
71a2baf402 Adapt app module to new SnyggRule attribute behavior 2025-04-24 16:49:59 +02:00
Patrick Goldinger
94bfd39a9b Change SnyggRule attributes to use strings as the base type 2025-04-24 16:35:07 +02:00
Patrick Goldinger
d473343ca3 Rework Snygg UI element name and adapt TextKeyboardLayout.kt 2025-04-24 02:43:15 +02:00
Patrick Goldinger
5101db7169 Implement line clamp and uri value in the theme editor 2025-04-24 01:52:59 +02:00
Patrick Goldinger
08e10639d1 Implement new Snygg enum like properties in the theme editor 2025-04-24 00:40:25 +02:00
Patrick Goldinger
741dd82003 Adapt the theme editor to use new color representation 2025-04-23 21:28:22 +02:00
Patrick Goldinger
4c2bfc5dab Add new Snygg names translations 2025-04-23 20:53:54 +02:00
lm41
ce3cb4427a Migrate ThemeEditor to Material 3 2025-04-23 16:57:26 +02:00
Patrick Goldinger
463e3a3588 Move Snygg attributes to SnyggAttributes data class 2025-04-23 16:49:03 +02:00
Patrick Goldinger
da611d3077 Document and test SnyggRule 2025-04-23 16:12:49 +02:00
Patrick Goldinger
d0afe023a6 Basic adaption of default theme files
Co-authored-by: lm41 <lm41@lm41.xyz>
2025-04-23 03:38:07 +02:00
Patrick Goldinger
00f01b59db Fix ThemeManager new Snygg v2 integration
Co-authored-by: Lars Mühlbauer <lm41@lm41.xyz>
2025-04-23 03:06:54 +02:00
Patrick Goldinger
7a1cc211f3 Adapt EditPropertyDialog to Snygg v2
Co-authored-by: lm41 <lm41@lm41.xyz>
2025-04-23 02:26:06 +02:00
Patrick Goldinger
1e29204d9c Adapt EditRuleDialog in theme editor to Snygg v2
Co-authored-by: Lars Mühlbauer <lm41@lm41.xyz>
2025-04-23 00:52:14 +02:00
Patrick Goldinger
d541eed655 Fix tooltip showing on all usages of quick button 2025-04-10 17:31:31 +02:00
lm41
aaf7e62ed9 Switch to new Tooltip composable 2025-04-10 16:04:37 +02:00
Patrick Goldinger
29cbc4f373 Remove icon button with padding 2025-04-06 17:02:15 +02:00
Patrick Goldinger
791f3d2a69 Basic fix for emoji palette view 2025-04-06 16:46:58 +02:00
Patrick Goldinger
6595aa4d1a Fix clipboard input layout style 2025-04-06 16:28:10 +02:00
Patrick Goldinger
815ea0845a Remove SnyggSurface & Fix text input style 2025-04-06 14:45:56 +02:00
Patrick Goldinger
37c5256769 Fix and adapt base style for actions and actions editor 2025-04-06 13:55:14 +02:00
Patrick Goldinger
b85703db36 Clean up Snygg UI helpers and fix clip issues 2025-04-06 12:56:47 +02:00
Patrick Goldinger
bc1bc8fece Add Snygg line-clamp property 2025-04-05 18:12:56 +02:00
Patrick Goldinger
b3c6589326 WIP: IME UI draws again with base style 2025-04-05 17:37:07 +02:00
Patrick Goldinger
8f44d04a6f WIP: app module compiles again 2025-04-05 15:55:55 +02:00
Patrick Goldinger
349758991a Add Snygg anonymous elements and further adaptions in app 2025-04-05 14:47:42 +02:00
Patrick Goldinger
fed979dec9 Add Snygg selector inheritance and merging 2025-04-04 16:16:00 +02:00
Patrick Goldinger
d6f8555bdf WIP: continue adaption of Snygg v2 2025-04-04 15:53:46 +02:00
Patrick Goldinger
5afd6bd722 Improve SnyggStylesheet and SnyggRule tests 2025-04-03 01:02:57 +02:00
Patrick Goldinger
0716420697 WIP: Further adaption of Snygg 2025-04-02 14:33:05 +02:00
Patrick Goldinger
b923a5bf65 WIP: Add SnyggIcon, SnyggIconButton, SnyggSpacer and continue adaption 2025-04-02 13:10:52 +02:00
Patrick Goldinger
3c2b2ae473 WIP: Begin adapting app module to new Snygg v2
Co-authored-by: lm41 <lm41@lm41.xyz>
2025-04-02 02:59:57 +02:00
Patrick Goldinger
1e1881e6ea Raise minimum API to 26 (Android 8)
Upgrade Kotlin to 2.1.20 and also upgrade other dependencies

Co-authored-by: lm41 <lm41@lm41.xyz>
2025-04-02 02:08:59 +02:00
Patrick Goldinger
3acda3745a Add Snygg font-family to Text
Co-authored-by: lm41 <lm41@lm41.xyz>
2025-04-02 01:52:43 +02:00
Patrick Goldinger
2a8c67c4f4 Fully rework SnyggSpec and parsing behavior
Co-authored-by: lm41 <lm41@lm41.xyz>
2025-04-02 01:46:00 +02:00
Patrick Goldinger
0d572fc1ae Add preliminary support for Snygg custom fonts and images
Co-authored-by: lm41 <lm41@lm41.xyz>
2025-03-28 03:20:42 +01:00
Patrick Goldinger
7810d8034c Update Snygg stylesheet.schema.json 2025-03-26 23:24:19 +01:00
Patrick Goldinger
6b9fc555f0 Add multiple text relevant Snygg properties 2025-03-26 23:15:09 +01:00
Patrick Goldinger
f352c768f4 Automate Snygg stylesheet schema generation
Additionally, rework the value spec declaration so it is serializable.
2025-03-26 19:48:41 +01:00
Patrick Goldinger
24d6667b52 Rework Snygg selectors to enum
Co-authored-by: lm41 <lm41@lm41.xyz>
2025-03-26 03:25:07 +01:00
Patrick Goldinger
d29cfff7d4 Add SnyggButton and SnyggSurface
Co-authored-by: lm41 <lm41@lm41.xyz>
2025-03-26 02:32:21 +01:00
Patrick Goldinger
babf58bf53 Clean up Snygg UI helpers and add row/column
Co-authored-by: lm41 <lm41@lm41.xyz>
2025-03-26 01:23:27 +01:00
Patrick Goldinger
701088c842 Implement Snygg inherit behavior
Co-authored-by: lm41 <lm41@lm41.xyz>
2025-03-26 00:51:35 +01:00
Patrick Goldinger
4fdbb7c235 Add Snygg font-weight and refactor appearance code 2025-03-25 22:50:44 +01:00
Patrick Goldinger
5831bd84ad Rework SnyggSpec and serialization 2025-03-25 12:14:44 +01:00
Patrick Goldinger
dd97ff09b6 Add support for margin and padding in Snygg 2025-03-25 02:56:06 +01:00
Patrick Goldinger
a5407502f3 Add support for shadowColor in Snygg 2025-03-25 02:02:09 +01:00
Patrick Goldinger
def05ef3a7 Add first preview of SnyggText 2025-03-25 01:48:50 +01:00
Patrick Goldinger
8ec03c09cc Add further SnyggTheme tests 2025-03-25 00:54:07 +01:00
Patrick Goldinger
a298687f6e Add snygg dynamic color and compilation tests
Co-authored-by: lm41 <lm41@lm41.xyz>
2025-03-24 23:38:53 +01:00
Patrick Goldinger
2488614d22 Set up Snygg UI package for v2 2025-03-24 22:22:02 +01:00
Patrick Goldinger
ca8ca65533 Full rework of snygg dynamic color handling 2025-03-24 22:14:11 +01:00
lm41
8e134cc15f Move ColorPalette to :lib:color and simplify MaterialYouColor 2025-03-24 20:22:35 +01:00
lm41
17860815b6 Add SnyggShapeValueTest 2025-03-24 18:01:58 +01:00
lm41
bb502b5f8c Improve tests and code coverage in SnyggRule 2025-03-23 20:18:30 +01:00
Patrick Goldinger
0e51f25bb0 Add basic stylesheet compilation 2025-03-23 20:16:07 +01:00
Patrick Goldinger
c1e50c9684 Add SnyggRule sorting 2025-03-23 14:09:07 +01:00
Patrick Goldinger
c377cd9ed7 Add tests for SnyggSolidColorValue 2025-03-22 23:26:55 +01:00
Patrick Goldinger
1fc3b536d9 Remove kotest and sub-dependencies 2025-03-22 22:17:16 +01:00
Patrick Goldinger
ae645fa327 Improve snygg testing build config and usage 2025-03-22 22:09:27 +01:00
Patrick Goldinger
bfd20055fc Improve SnyggStylesheetEditor and add tests 2025-03-22 21:18:06 +01:00
Patrick Goldinger
15aa6a6bb3 Improve SnyggStylesheet testing 2025-03-22 20:35:09 +01:00
Patrick Goldinger
e4ef14e684 Switch lib.snygg to kotlinx.kover
(and regain my sanity, thanks Lars)
2025-03-22 18:32:16 +01:00
Patrick Goldinger
39d7ea4fc0 Add JaCoCo to lib.snygg 2025-03-22 18:17:24 +01:00
Patrick Goldinger
6d330f807a Improve snygg stylesheet testing and move size test to lib.snygg 2025-03-22 15:53:17 +01:00
Patrick Goldinger
e6bc0657f6 Rework stylesheet serialization and ass basic test 2025-03-22 15:21:59 +01:00
Patrick Goldinger
4686234ba2 Re-implement SnyggRule and WIP stylesheet/theme
Co-authored-by: lm41 <lm41@lm41.xyz>
2025-03-22 04:54:00 +01:00
lm41
57e4765ce7 Remove custom implementation of SnyggIdToValueMap
The custom List<Pair<String, Any>> implementation of SnyggIdToValueMap
is removed in favor of the typealias for MutableMap<String, Any>. This
way we can work with normal map functions and have a more optimized
data-structure for our use-case.
The SnyggIdToValueMap.new() functions are replaced by a single
snyggIdToValueMapOf() function which allocates a new MutableMap.
The getOrThrow and getOrNull functions are modified to work with the
new underling map data-structure. The add function is a direct alias
to the putAll function.
2025-03-21 18:25:05 +01:00
Patrick Goldinger
aaf26a7a4b Add TODO comments and plan new Snygg v2 API 2025-03-21 02:40:31 +01:00
Patrick Goldinger
f8f6756b84 Initial Snygg v2 draft 2025-03-21 01:25:17 +01:00
Lars Mühlbauer
35fd70ce6d Improve crash screen (#2804)
* Improve crash screen

The whole crash screen content is now scrollable
and the buttons to report or close the activity
were moved to a footer. To distinguish between
the log content and the general explanation, a
horizontal divider was added.

* Do not restart activity on orientation change
2025-03-12 00:26:12 +01:00
Lars Mühlbauer
a0ebd62f9a Add toggle for keyboard label language (#2811) 2025-03-12 00:25:58 +01:00
Lars Mühlbauer
3dacd3c2d0 Rework one-handed mode. (#2801)
* Rework one-handed mode.

The one-handed mode now remembers the position
where it was disabled and opens to this position
the next time it is activated.

* Add seperade `TOGGLE_COMPACT_LAYOUT` keycode

* Update AppPrefs.kt

* Update KeyboardScreen.kt

* Update ComputingEvaluator.kt
2025-03-11 23:02:51 +01:00
Lars Mühlbauer
c73b8dda0b Do not ignore flagTextNoSuggestions. (#2807)
Fixes: #2806
2025-03-11 20:38:01 +01:00
Lars Mühlbauer
5353a01226 Refactor Settings (#2780)
* Remove old migration rules

* Rename advanced settings to other and move incognito settings to typing

* Move clipboard suggestions to clipboard screen

* Move other to correct alphabetical location

* Optimize++
2025-03-05 00:08:24 +01:00
florisboard-bot
34f8fec2f6 Update translations from Crowdin 2025-03-04 22:39:56 +01:00
npnpatidar
7199fcdf12 Devanagari Script and Hindi Language Layout Added (#2723)
* test commit

* hi-IN popupmappings id added

* basic layout

* heart replaces with space

* code corrected

* lots of other keys added

* all characters have been added

* popmappings rearranged

* some positional change

* * and \ won't popup due to क्ष and  ज्ञ

* ओ औ code correced

* some vovels reordered

* Hindi does not capitalization concept
2025-03-04 22:27:16 +01:00
Lars Mühlbauer
8319f563b9 Add support for switching the subtype on space bar long press (#2781)
* Add initial support for switching the subtype on space bar long press

* Fix keyboard presses behind the header panel

* Fix taps behind sheet

* Extract string ressource and refactor code

* Instant select subtype

* Persist suptype change in prefs
2025-03-04 22:23:24 +01:00
Lars Mühlbauer
45efe52159 Add new screenshot generator and update fastlane images (#2787)
* Add new screenshot generator and update fastlane images

* Update featureGraphic.png to match g-play requirements
2025-03-04 21:46:54 +01:00
Lars Mühlbauer
7a286b932b Update snygg dynamic color scheme on wallpaper change (#2778)
* Update snygg dynamic color scheme on wallpaper change

* Move broadcast receiver handling to FlorisImeService and properly unregister the receiver

* Set systembar color on stylesheet change
2025-03-04 21:08:45 +01:00
Lars Mühlbauer
df3f84e96d Fix text being rendered behind the top app bar (#2758)
* Fix text being rendered behind the top app bar

This commit fixes an issue where the text was rendered
behind the top app bar in the crash dialog activity by
removing the top app bar in the style and adding a top
app bar in the layout XML.

* Fix toolbar rendering messed up in crash dialog screen

---------

Co-authored-by: Patrick Goldinger <patrick@patrickgold.dev>
2025-02-14 13:13:44 +01:00
Lars Mühlbauer
4cc2216940 Fix switching back from voice ime (#2684) (#2759)
This commit fixes an issue where the voice ime was
not possible to switch back to FlorisBoard, because
the keyboard subtype was not specified.
2025-02-14 12:23:46 +01:00
Lars Mühlbauer
21fdd31c88 Fix BottomSheetContent drawing behind system nav bar (#2757)
This commit fixes an issue where the BottomSheetContent
was drawn behind the system navigation bar. To fix this
issue, I applied the saveDrawingPadding Modifier to the
AnimatedVisibility. There was a color problem on the
system navigation bar, so I set the background color to
SheetOutOfBoundsBgColorInactive. Now the system nav bar
is colored the right way.
2025-02-14 12:20:13 +01:00
Lars Mühlbauer
53c6cb52ca Fix crash when opening clipboard history (#2756)
This Commit fixes a crash when opening the clipboard history
with a theme which does not provide a sp size for the header
or the clipboard item. The sp value is now saveTimes multiplied
with 1f.
2025-02-14 12:15:39 +01:00
Patrick Goldinger
9b12213b3e Possible fixes for all keys invisible bug (#2743)
* Fix possible race condition in extension manager init

* Fix deprecation warnings in Kotlin contracts

* Fix race condition in KeyboardManagerResources

* Add debug glide.enabled toggle
2025-02-02 22:35:25 +01:00
Patrick Goldinger
659a5062ab Merge pull request #2750 from florisboard/feat/implement-custom-material-you-color
Implement custom material you colors
2025-02-02 20:17:29 +01:00
lm41
086e5f7782 Add new JetPrefColorPicker for the keyboard accent color.
This commit introduces the new color schemes for spoofed
material you themes. The material you themes can now be
used on any android version that is supported by FlorisBoard
and not just on android 12+.
2025-02-02 19:40:04 +01:00
lm41
ae73e64cd2 Add new JetPrefColorPicker for the settings accent color. 2025-02-02 19:35:16 +01:00
lm41
9f5e8cddd5 Add :lib:color package
This package includes a set of material 3 color schemes
and a map of the corresponding colors to the color schemes.
Please visit the packages README.md for information
on how the schemes were created.
2025-02-02 19:30:39 +01:00
Lars Mühlbauer
3724495d4f Remove code duplication in setup screen (#2742)
* Remove code duplication in setup screen

* fixup! Remove code duplication in setup screen
2025-01-16 22:52:25 +01:00
Patrick Goldinger
15b5dd9e3e Fix issues with all keys invisible detection (#2737) 2025-01-13 02:07:05 +01:00
266 changed files with 18977 additions and 7105 deletions

View File

@@ -14,8 +14,8 @@
* limitations under the License.
*/
import org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag
import java.io.ByteArrayOutputStream
import org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag
plugins {
alias(libs.plugins.agp.application)
@@ -23,7 +23,6 @@ plugins {
alias(libs.plugins.kotlin.plugin.compose)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.mannodermaus.android.junit5)
alias(libs.plugins.mikepenz.aboutlibraries)
}
@@ -52,6 +51,7 @@ android {
freeCompilerArgs = listOf(
"-opt-in=kotlin.contracts.ExperimentalContracts",
"-Xjvm-default=all-compatibility",
"-Xwhen-guards",
)
}
@@ -202,16 +202,11 @@ dependencies {
implementation(libs.patrickgold.jetpref.material.ui)
implementation(project(":lib:android"))
implementation(project(":lib:color"))
implementation(project(":lib:kotlin"))
implementation(project(":lib:native"))
implementation(project(":lib:snygg"))
testImplementation(libs.equalsverifier)
testImplementation(libs.kotest.assertions.core)
testImplementation(libs.kotest.extensions.roboelectric)
testImplementation(libs.kotest.property)
testImplementation(libs.kotest.runner.junit5)
androidTestImplementation(libs.androidx.test.ext)
androidTestImplementation(libs.androidx.test.espresso.core)
}

View File

@@ -114,6 +114,7 @@
android:name="dev.patrickgold.florisboard.lib.crashutility.CrashDialogActivity"
android:icon="@mipmap/floris_app_icon"
android:label="@string/crash_dialog__title"
android:configChanges="orientation|screenSize"
android:theme="@style/CrashDialogTheme"/>
<!-- Copy to Clipboard Activity -->

View File

@@ -49,6 +49,12 @@
"authors": [ "iamrasel" ],
"direction": "ltr"
},
{
"id": "hindi_in",
"label": "हिंदी",
"authors": [ "npnpatidar" ],
"direction": "ltr"
},
{
"id": "bepo",
"label": "BÉPO",

View File

@@ -0,0 +1,42 @@
[
[
{ "$": "case_selector", "lower": { "code": 2335, "label": "ट" }, "upper": { "code": 2336, "label": "ठ" } },
{ "$": "case_selector", "lower": { "code": 2337, "label": "ड" }, "upper": { "code": 2338, "label": "ढ" } },
{ "$": "case_selector", "lower": { "code": 2375, "label": "े" }, "upper": { "code": 2376, "label": "ै" } },
{ "$": "case_selector", "lower": { "code": 2352, "label": "र" }, "upper": { "code": 2371, "label": "ृ" } },
{ "$": "case_selector", "lower": { "code": 2340, "label": "त" }, "upper": { "code": 2341, "label": "थ" } },
{ "$": "case_selector", "lower": { "code": 2351, "label": "य" }, "upper": { "code": 2399, "label": "य़" } },
{ "$": "case_selector", "lower": { "code": 2369, "label": "ु" }, "upper": { "code": 2370, "label": "ू" } },
{ "$": "case_selector", "lower": { "code": 2367, "label": "ि" }, "upper": { "code": 2368, "label": "ी" } },
{ "$": "case_selector", "lower": { "code": 2379, "label": "ो" }, "upper": { "code": 2380, "label": "ौ" } },
{ "$": "case_selector", "lower": { "code": 2346, "label": "प" }, "upper": { "code": 2398, "label": "फ़" } }
],
[
{ "$": "case_selector", "lower": { "code": 2366, "label": "ा" }, "upper": { "code": 2309, "label": "अ" } },
{ "$": "case_selector", "lower": { "code": 2360, "label": "स" }, "upper": { "code": 2358, "label": "श" } },
{ "$": "case_selector", "lower": { "code": 2342, "label": "द" }, "upper": { "code": 2343, "label": "ध" } },
{ "$": "case_selector", "lower": { "code": 2347, "label": "फ" }, "upper": { "code": 2364, "label": " ़" } },
{ "$": "case_selector", "lower": { "code": 2327, "label": "ग" }, "upper": { "code": 2328, "label": "घ" } },
{ "$": "case_selector", "lower": { "code": 2361, "label": "ह" }, "upper": { "code": 2307, "label": "" } },
{ "$": "case_selector", "lower": { "code": 2332, "label": "ज" }, "upper": { "code": 2333, "label": "झ" } },
{ "$": "case_selector", "lower": { "code": 2325, "label": "क" }, "upper": { "code": 2326, "label": "ख" } },
{ "$": "case_selector", "lower": { "code": 2354, "label": "ल" }, "upper": { "code": 2355, "label": "ळ" } }
],
[
{
"$": "case_selector",
"lower": { "$": "multi_text_key", "codePoints": [2332, 2381, 2334], "label": "ज्ञ" },
"upper": { "code": 2395, "label": "ज़" }
},
{
"$": "case_selector",
"lower": { "$": "multi_text_key", "codePoints": [2325, 2381, 2359], "label": "क्ष" },
"upper": { "code": 2359, "label": "ष" }
},
{ "$": "case_selector", "lower": { "code": 2330, "label": "च" }, "upper": { "code": 2331, "label": "छ" } },
{ "$": "case_selector", "lower": { "code": 2357, "label": "व" }, "upper": { "code": 2381, "label": "्" } },
{ "$": "case_selector", "lower": { "code": 2348, "label": "ब" }, "upper": { "code": 2349, "label": "भ" } },
{ "$": "case_selector", "lower": { "code": 2344, "label": "न" }, "upper": { "code": 2339, "label": "ण" } },
{ "$": "case_selector", "lower": { "code": 2350, "label": "म" }, "upper": { "code": 2306, "label": "ं" } }
]
]

View File

@@ -48,6 +48,7 @@
{ "id": "fi", "authors": [ "patrickgold" ] },
{ "id": "fo", "authors": [ "BinFlush" ] },
{ "id": "fr", "authors": [ "patrickgold" ] },
{ "id": "hi-IN", "authors": [ "npnpatidar" ] },
{ "id": "hr", "authors": [ "hedidnothingwrong" ] },
{ "id": "hu", "authors": [ "zoli111, gabik65" ] },
{ "id": "hy", "authors": [ "PJTSearch" ] },
@@ -695,6 +696,16 @@
"numericAdvanced": "org.florisboard.layouts:bengali"
}
},
{
"languageTag": "hi-IN",
"composer": "org.florisboard.composers:appender",
"currencySet": "org.florisboard.currencysets:indian_rupee",
"popupMapping": "org.florisboard.localization:hi-IN",
"preferred": {
"characters": "org.florisboard.layouts:hindi_in",
"numericRow": "org.florisboard.layouts:devanagari"
}
},
{
"languageTag": "ast",
"composer": "org.florisboard.composers:appender",

View File

@@ -0,0 +1,173 @@
{
"all": {
"क": {
"main": { "$": "auto_text_key", "code": 2392, "label": "क़" },
"relevant": [{ "$": "auto_text_key", "code": 2393, "label": "ख़" }]
},
"ग": {
"main": { "$": "auto_text_key", "code": 2394, "label": "ग़" },
"relevant": [{ "$": "auto_text_key", "code": 2427, "label": "ॻ" }]
},
"च": {
"relevant": [
{ "$": "auto_text_key", "code": 2385, "label": " ॑" },
{ "$": "auto_text_key", "code": 2386, "label": " ॒" }
]
},
"ज": {
"main": { "$": "auto_text_key", "code": 2395, "label": "ज़" },
"relevant": [
{ "$": "auto_text_key", "code": 2428, "label": "ॼ" },
{ "$": "auto_text_key", "code": 2425, "label": "ॹ" }
]
},
"ड": {
"main": { "$": "auto_text_key", "code": 2396, "label": "ड़" },
"relevant": [{ "$": "auto_text_key", "code": 2397, "label": "ढ़" }]
},
"त": {
"main": { "$": "multi_text_key", "codePoints": [2340, 2381, 2352], "label": "त्र" }
},
"द": {
"main": { "$": "auto_text_key", "code": 2396, "label": "ड़" },
"relevant": [
{ "$": "auto_text_key", "code": 2430, "label": "ॾ" },
{ "$": "auto_text_key", "code": 2397, "label": "ढ़" },
{ "$": "auto_text_key", "code": 2424, "label": "ॸ" }
]
},
"न": {
"main": { "$": "auto_text_key", "code": 2329, "label": "ङ" },
"relevant": [
{ "$": "auto_text_key", "code": 2345, "label": "ऩ" },
{ "$": "auto_text_key", "code": 2334, "label": "ञ" }
]
},
"फ": {
"main": { "$": "auto_text_key", "code": 2398, "label": "फ़" }
},
"ब": {
"relevant": [
{ "$": "auto_text_key", "code": 2431, "label": "ॿ" },
{ "$": "auto_text_key", "code": 2365, "label": "ऽ" },
{ "$": "auto_text_key", "code": 2416, "label": "॰" }
]
},
"म": {
"main": { "$": "auto_text_key", "code": 2305, "label": "ँ" },
"relevant": [{ "$": "auto_text_key", "code": 2304, "label": "ऀ" }]
},
"य": {
"main": { "$": "auto_text_key", "code": 2426, "label": "ॺ" }
},
"र": {
"main": { "$": "auto_text_key", "code": 2315, "label": "ऋ" },
"relevant": [
{ "$": "auto_text_key", "code": 2400, "label": "ॠ" },
{ "$": "auto_text_key", "code": 2372, "label": "ॄ" },
{ "$": "auto_text_key", "code": 2353, "label": "ऱ" }
]
},
"ल": {
"relevant": [
{ "$": "auto_text_key", "code": 2402, "label": "ॢ" },
{ "$": "auto_text_key", "code": 2403, "label": "ॣ" },
{ "$": "auto_text_key", "code": 2316, "label": "ऌ" },
{ "$": "auto_text_key", "code": 2401, "label": "ॡ" },
{ "$": "auto_text_key", "code": 2356, "label": "ऴ" }
]
},
"व": {
"relevant": [
{ "$": "auto_text_key", "code": 2387, "label": " ॓" },
{ "$": "auto_text_key", "code": 2388, "label": " ॔" }
]
},
"स": {
"main": { "$": "multi_text_key", "codePoints": [2358, 2381, 2352], "label": "श्र" },
"relevant": [{ "$": "auto_text_key", "code": 2359, "label": "ष" }]
},
"ा": {
"main": { "$": "auto_text_key", "code": 2310, "label": "आ" },
"relevant": [
{ "$": "auto_text_key", "code": 2373, "label": "ॅ" },
{ "$": "auto_text_key", "code": 2418, "label": "ॲ" },
{ "$": "auto_text_key", "code": 2308, "label": "ऄ" }
]
},
"ि": {
"main": { "$": "auto_text_key", "code": 2311, "label": "इ" },
"relevant": [{ "$": "auto_text_key", "code": 2312, "label": "ई" }]
},
"ु": {
"main": { "$": "auto_text_key", "code": 2313, "label": "उ" },
"relevant": [
{ "$": "auto_text_key", "code": 2422, "label": "ॶ" },
{ "$": "auto_text_key", "code": 2423, "label": "ॷ" },
{ "$": "auto_text_key", "code": 2390, "label": " ॖ" },
{ "$": "auto_text_key", "code": 2314, "label": "ऊ" },
{ "$": "auto_text_key", "code": 2391, "label": " ॗ" }
]
},
"े": {
"main": { "$": "auto_text_key", "code": 2319, "label": "ए" },
"relevant": [
{ "$": "auto_text_key", "code": 2317, "label": "ऍ" },
{ "$": "auto_text_key", "code": 2318, "label": "ऎ" },
{ "$": "auto_text_key", "code": 2374, "label": " ॆ" },
{ "$": "auto_text_key", "code": 2320, "label": "ऐ" },
{ "$": "auto_text_key", "code": 2382, "label": " ॎ" },
{ "$": "auto_text_key", "code": 2389, "label": " ॕ" }
]
},
"ो": {
"main": { "$": "auto_text_key", "code": 2323, "label": "ओ" },
"relevant": [
{ "$": "auto_text_key", "code": 2322, "label": "ऒ" },
{ "$": "auto_text_key", "code": 2378, "label": " ॊ" },
{ "$": "auto_text_key", "code": 2383, "label": " ॏ" },
{ "$": "auto_text_key", "code": 2421, "label": "ॵ" },
{ "$": "auto_text_key", "code": 2384, "label": "ॐ" },
{ "$": "auto_text_key", "code": 2377, "label": "ॉ" },
{ "$": "auto_text_key", "code": 2419, "label": "ॳ" },
{ "$": "auto_text_key", "code": 2420, "label": "ॴ" },
{ "$": "auto_text_key", "code": 2362, "label": " ऺ" },
{ "$": "auto_text_key", "code": 2363, "label": " ऻ" },
{ "$": "auto_text_key", "code": 2324, "label": "औ" },
{ "$": "auto_text_key", "code": 2321, "label": "ऑ" }
]
},
"~right": {
"main": { "code": 2404, "label": "।" },
"relevant": [
{ "code": 37, "label": "%" },
{ "code": 43, "label": "+" },
{ "code": 45, "label": "-" },
{ "code": 58, "label": ":" },
{ "code": 59, "label": ";" },
{ "code": 47, "label": "/" },
{ "$": "layout_direction_selector", "ltr": { "code": 40, "label": "(" }, "rtl": { "code": 41, "label": "(" } },
{ "$": "layout_direction_selector", "ltr": { "code": 41, "label": ")" }, "rtl": { "code": 40, "label": ")" } },
{ "code": 8205, "label": ">⁞<" },
{ "code": 8204, "label": "<⁞>" },
{ "code": 2417, "label": "ॱ" },
{ "code": 2429, "label": "" },
{ "code": 33, "label": "!" },
{ "code": 63, "label": "?" },
{ "code": 2405, "label": "॥" }
]
}
},
"uri": {
"~right": {
"main": { "code": -255, "label": ".com" },
"relevant": [
{ "code": -255, "label": ".org" },
{ "code": -255, "label": ".net" },
{ "code": -255, "label": ".in" },
{ "code": -255, "label": ".co.in" }
]
}
}
}

View File

@@ -1,65 +0,0 @@
{
"$type": "dev.patrickgold.florisboard.ime.theme.Theme",
"name": "gboard_day",
"label": "Gboard Day",
"authors": [ "patrickgold", "itskareem" ],
"isNightTheme": false,
"attributes": {
"window": {
"colorPrimary": "#0479ed",
"colorPrimaryDark": "#0467c9",
"colorAccent": "#FF9800",
"navigationBarColor": "@keyboard/background",
"navigationBarLight": "true",
"semiTransparentColor": "#20000000",
"textColor": "#000000"
},
"keyboard": {
"background": "#D1D6DC"
},
"key": {
"background": "#FCFFFF",
"backgroundPressed": "#F5F5F5",
"foreground": "@window/textColor",
"foregroundPressed": "@window/textColor",
"showBorder": "true"
},
"key:enter": {
"background": "@window/colorPrimary",
"backgroundPressed": "@window/colorPrimaryDark",
"foreground": "#FFFFFF",
"foregroundPressed": "#FFFFFF"
},
"key:shift:capslock": {
"foreground": "@window/colorAccent",
"foregroundPressed": "@window/colorAccent"
},
"media": {
"foreground": "@window/textColor",
"foregroundAlt": "#757575"
},
"oneHanded": {
"background": "@keyboard/background",
"foreground": "#424242"
},
"popup": {
"background": "#EEEEEE",
"backgroundActive": "#BDBDBD",
"foreground": "@window/textColor"
},
"privateMode": {
"background": "#A000FF",
"foreground": "#FFFFFF"
},
"smartbar": {
"background": "@keyboard/background",
"foreground": "@window/textColor",
"foregroundAlt": "#8A8A8A"
},
"smartbarButton": {
"background": "@key/background",
"foreground": "@key/foreground"
},
"glideTrail": {"foreground": "#200479ed"}
}
}

View File

@@ -1,65 +0,0 @@
{
"$type": "dev.patrickgold.florisboard.ime.theme.Theme",
"name": "gboard_night",
"label": "Gboard Night",
"authors": [ "Netscaping" ],
"isNightTheme": true,
"attributes": {
"window": {
"colorPrimary": "#5e97f6",
"colorPrimaryDark": "#4285f4",
"colorAccent": "#FF9800",
"navigationBarColor": "@keyboard/background",
"navigationBarLight": "false",
"semiTransparentColor": "#20FFFFFF",
"textColor": "#FFFFFF"
},
"keyboard": {
"background": "#292e33"
},
"key": {
"background": "#484c4f",
"backgroundPressed": "#5e5e60",
"foreground": "@window/textColor",
"foregroundPressed": "@window/textColor",
"showBorder": "true"
},
"key:enter": {
"background": "@window/colorPrimary",
"backgroundPressed": "@window/colorPrimaryDark",
"foreground": "#FFFFFF",
"foregroundPressed": "#FFFFFF"
},
"key:shift:capslock": {
"foreground": "@window/colorAccent",
"foregroundPressed": "@window/colorAccent"
},
"media": {
"foreground": "@window/textColor",
"foregroundAlt": "#BDBDBD"
},
"oneHanded": {
"background": "#373c41",
"foreground": "#9b9da0"
},
"popup": {
"background": "#373c41",
"backgroundActive": "#5a5e60",
"foreground": "@window/textColor"
},
"privateMode": {
"background": "#A000FF",
"foreground": "#FFFFFF"
},
"smartbar": {
"background": "transparent",
"foreground": "#d4d5d6",
"foregroundAlt": "#73FFFFFF"
},
"smartbarButton": {
"background": "#FFFFFF",
"foreground": "#686868"
},
"glideTrail": {"foreground": "#205e97f6"}
}
}

View File

@@ -2,10 +2,10 @@
"$": "ime.extension.theme",
"meta": {
"id": "org.florisboard.themes",
"version": "0.1.0",
"version": "0.2.0",
"title": "FlorisBoard default themes",
"description": "Default themes (both day and night) for the keyboard UI",
"maintainers": [ "patrickgold <patrick@patrickgold.dev>" ],
"maintainers": [ "patrickgold <patrick@patrickgold.dev>", "lm41 <lm41@lm41.xyz>" ],
"license": "apache-2.0"
},
"themes": [
@@ -13,49 +13,37 @@
"id": "floris_day",
"label": "Floris Day",
"authors": [ "patrickgold" ],
"isNight": false,
"isBorderless": false,
"isMaterialYouAware": false
"isNight": false
},
{
"id": "floris_day_borderless",
"label": "Floris Day (Borderless)",
"authors": [ "patrickgold" ],
"isNight": false,
"isBorderless": true,
"isMaterialYouAware": false
"isNight": false
},
{
"id": "floris_night",
"label": "Floris Night",
"authors": [ "patrickgold" ],
"isNight": true,
"isBorderless": false,
"isMaterialYouAware": false
"isNight": true
},
{
"id": "floris_night_borderless",
"label": "Floris Night (Borderless)",
"authors": [ "patrickgold" ],
"isNight": true,
"isBorderless": true,
"isMaterialYouAware": false
"isNight": true
},
{
"id": "floris_pure_night",
"label": "Floris Pure Night",
"authors": [ "serebit" ],
"isNight": true,
"isBorderless": false,
"isMaterialYouAware": false
"isNight": true
},
{
"id": "floris_pure_night_borderless",
"label": "Floris Pure Night (Borderless)",
"authors": [ "serebit" ],
"isNight": true,
"isBorderless": true,
"isMaterialYouAware": false
"isNight": true
}
]
}

View File

@@ -1,4 +1,5 @@
{
"$schema": "https://schemas.florisboard.org/snygg/v2/stylesheet",
"@defines": {
"--primary": "#4caf50",
"--primary-variant": "#388e3c",
@@ -8,7 +9,20 @@
"--surface": "#ffffff",
"--surface-variant": "#f5f5f5",
"--popup-surface": "#eeeeee",
"--focused-popup-surface": "#bdbdbd",
"--drag-marker": "rgb(255,0,0)",
"--spacer-color": "rgba(0 ,0, 0, 0.25)",
"--one-hand-background": "#e8f5e9",
"--one-hand-foreground": "#424242",
"--incognito-icon-color": "#00000011",
"--on-primary": "#f0f0f0",
"--on-background": "#121212",
"--on-background-disabled": "rgb(175,175,175)",
"--on-surface": "#000000",
"--on-surface-variant": "#5f5f5f",
@@ -16,8 +30,10 @@
"--shape-variant": "rounded-corner(12dp, 12dp, 12dp, 12dp)"
},
"keyboard": {
"background": "var(--background)"
"window": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"clip": "no"
},
"key": {
@@ -25,48 +41,69 @@
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
"shadow-elevation": "2dp",
"text-max-lines": "1"
},
"key:pressed": {
"background": "var(--surface-variant)",
"foreground": "var(--on-surface)"
},
"key[code={c:enter}]": {
"key[code=10]": {
"background": "var(--primary)",
"foreground": "var(--on-surface)"
},
"key[code={c:enter}]:pressed": {
"key[code=10]:pressed": {
"background": "var(--primary-variant)",
"foreground": "var(--on-surface)"
},
"key[code={c:shift}][shiftstate={sh:caps_lock}]": {
"foreground": "var(--secondary)"
},
"key[code={c:space}]": {
"key[code=32]": {
"background": "var(--surface)",
"foreground": "var(--on-surface-variant)",
"font-size": "12sp",
"text-overflow": "ellipsis"
},
"key[code=-201,-202,-203]": {
"font-size": "18sp"
},
"key[code=-204,-205]": {
"font-size": "12sp"
},
"key[code=-205]": {
"text-max-lines": "2"
},
"key[code=-11][shiftstate=`caps_lock`]": {
"foreground": "var(--secondary)"
},
"key-hint": {
"background": "transparent",
"foreground": "var(--on-surface-variant)",
"font-size": "12sp"
"font-family": "monospace",
"font-size": "12sp",
"padding": "0dp 1dp 1dp 0dp",
"text-max-lines": "1"
},
"key-popup": {
"background": "#eeeeee",
"key-popup-box": {
"background": "var(--popup-surface)",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"key-popup:focus": {
"background": "#bdbdbd",
"foreground": "var(--on-surface)"
"key-popup-element:focus": {
"background": "var(--focused-popup-surface)",
"shape": "var(--shape)"
},
"key-popup-extended-indicator": {
"font-size": "16sp"
},
"smartbar": {
"font-size": "18sp"
},
"smartbar-shared-actions-toggle": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"margin": "6dp",
"shape": "circle()",
"shadow-elevation": "2dp"
},
@@ -78,117 +115,180 @@
"smartbar-action-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "18sp",
"shape": "var(--shape)"
},
"smartbar-action-key:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"smartbar-action-key:disabled": {
"background": "transparent",
"foreground": "#12121248"
"foreground": "var(--on-background-disabled)"
},
"smartbar-action-tile": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "18sp",
"shape": "var(--shape)"
},
"smartbar-action-tile:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
"font-size": "14sp",
"shape": "var(--shape)",
"text-max-lines": "2",
"text-align": "center",
"text-overflow": "ellipsis"
},
"smartbar-action-tile:disabled": {
"background": "transparent",
"foreground": "#12121248"
"foreground": "var(--on-background-disabled)"
},
"smartbar-actions-overflow-customize-button": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"shape": "circle()",
"shadow-elevation": "2dp"
"shape": "circle()"
},
"smartbar-actions-editor": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
},
"smartbar-actions-editor-header": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "16sp"
"font-size": "16sp",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-header-button": {
"margin": "4dp",
"shape": "circle()"
},
"smartbar-actions-editor-subheader": {
"foreground": "var(--on-background)",
"font-size": "16sp"
"foreground": "var(--secondary)",
"font-size": "16sp",
"font-weight": "bold",
"padding": "12dp 16dp 12dp 8dp",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-tile-grid": {
"margin": "4dp 0dp"
},
"smartbar-actions-editor-tile": {
"margin": "4dp",
"padding": "8dp",
"text-align": "center",
"text-max-lines": "2",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-tile[code=-999]": {
"foreground": "var(--on-background-disabled)"
},
"smartbar-actions-editor-tile[code=-991]": {
"foreground": "var(--drag-marker)"
},
"smartbar-candidate-word": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"shape": "rectangle()"
"margin": "4dp",
"padding": "8dp 0dp",
"shape": "rectangle()",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-candidate-word:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-background)"
"foreground": "var(--on-surface)"
},
"smartbar-candidate-clip": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"shape": "rounded-corner(8%, 8%, 8%, 8%)"
"margin": "4dp",
"padding": "8dp 0dp",
"shape": "rounded-corner(8%, 8%, 8%, 8%)",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-candidate-clip:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-background)"
"foreground": "var(--on-surface)"
},
"smartbar-candidate-spacer": {
"foreground": "var(--surface)"
"foreground": "var(--spacer-color)"
},
"clipboard-header": {
"background": "transparent",
"foreground": "var(--on-surface)",
"foreground": "var(--on-background)",
"font-size": "16sp"
},
"clipboard-header-button": {
"margin": "4dp",
"shape": "circle()"
},
"clipboard-header-button:disabled": {
"foreground": "var(--on-background-disabled)"
},
"clipboard-header-text": {
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"clipboard-subheader": {
"font-size": "14sp",
"margin": "6dp"
},
"clipboard-content": {
"padding": "10dp"
},
"clipboard-item": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"margin": "4dp",
"padding": "12dp 8dp",
"shape": "var(--shape-variant)",
"shadow-elevation": "2dp"
},
"clipboard-item-popup": {
"background": "var(--surface-variant)",
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"margin": "4dp",
"shape": "var(--shape-variant)",
"shadow-elevation": "2dp"
},
"emoji-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "22sp",
"shape": "var(--shape)"
"clipboard-item-popup-action": {
"font-size": "16sp",
"padding": "12dp"
},
"emoji-key:pressed": {
"clipboard-clear-all-dialog": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"emoji-key-popup": {
"background": "#eeeeee",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
"shape": "var(--shape-variant)",
"shadow-elevation": "1dp"
},
"emoji-tab": {
"foreground": "var(--on-background)"
"clipboard-clear-all-dialog-message": {
"padding": "16dp"
},
"emoji-tab:focus": {
"foreground": "var(--primary)"
"clipboard-clear-all-dialog-buttons": {
"padding": "4dp"
},
"clipboard-clear-all-dialog-button": {
"background": "transparent",
"foreground": "var(--on-surface)",
"shape": "var(--shape-variant)"
},
"clipboard-history-disabled-title": {
"font-weight": "bold"
},
"clipboard-history-disabled-message": {
"padding": "0dp 4dp 0dp 8dp"
},
"clipboard-history-disabled-button": {
"background": "var(--primary)",
"foreground": "var(--on-primary)",
"shape": "rounded-corner(24dp,24dp,24dp,24dp)"
},
"clipboard-history-locked-title": {
"font-weight": "bold",
"text-align": "center"
},
"clipboard-history-locked-message": {
"padding": "0dp 4dp 0dp 0dp",
"text-align": "center"
},
"extracted-landscape-input-layout": {
@@ -213,15 +313,70 @@
},
"incognito-mode-indicator": {
"foreground": "#00000011"
"foreground": "var(--incognito-icon-color)"
},
"inline-autofill-chip": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"media-emoji-subheader": {
"font-weight": "bold",
"margin": "4dp"
},
"media-emoji-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "22sp",
"shape": "var(--shape)"
},
"media-emoji-key:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"media-emoji-key-popup-box": {
"background": "var(--popup-surface)",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"media-emoji-key-popup-element:focus": {
"background": "var(--focused-popup-surface)",
"shape": "var(--shape)"
},
"media-emoji-tab": {
"foreground": "var(--on-background)"
},
"media-emoji-tab:focus": {
"foreground": "var(--primary)"
},
"media-bottom-row-button": {
"padding": "16dp 0dp",
"shape": "var(--shape)"
},
"media-emoji-key-popup-extended-indicator": {
"foreground": "inherit"
},
"one-handed-panel": {
"background": "#e8f5e9",
"foreground": "#424242"
"background": "var(--one-hand-background)",
"foreground": "var(--one-hand-foreground)"
},
"system-nav-bar": {
"background": "var(--background)"
"subtype-panel": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
},
"subtype-panel-header": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "18sp",
"padding": "12dp",
"text-align": "center",
"text-max-lines": "1",
"text-overflow": "ellipsis"
}
}

View File

@@ -1,4 +1,5 @@
{
"$schema": "https://schemas.florisboard.org/snygg/v2/stylesheet",
"@defines": {
"--primary": "#4caf50",
"--primary-variant": "#388e3c",
@@ -8,7 +9,20 @@
"--surface": "#f0f0f0",
"--surface-variant": "#ffffff",
"--popup-surface": "#eeeeee",
"--focused-popup-surface": "#bdbdbd",
"--drag-marker": "rgb(255,0,0)",
"--spacer-color": "rgba(0 ,0, 0, 0.25)",
"--one-hand-background": "#e8f5e9",
"--one-hand-foreground": "#424242",
"--incognito-icon-color": "#00000011",
"--on-primary": "#f0f0f0",
"--on-background": "#121212",
"--on-background-disabled": "#12121248",
"--on-surface": "#000000",
"--on-surface-variant": "#5f5f5f",
@@ -16,56 +30,82 @@
"--shape-variant": "rounded-corner(12dp, 12dp, 12dp, 12dp)"
},
"keyboard": {
"background": "var(--background)"
"window": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"clip": "no"
},
"key": {
"background": "transparent",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)"
"shape": "var(--shape)",
"text-max-lines": "1"
},
"key:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"key[code={c:enter}]": {
"key[code=10]": {
"background": "var(--primary)",
"foreground": "var(--on-surface)"
"foreground": "var(--on-surface)",
"margin": "0dp 6dp"
},
"key[code={c:enter}]:pressed": {
"key[code=10]:pressed": {
"background": "var(--primary-variant)",
"foreground": "var(--on-surface)"
},
"key[code={c:shift}][shiftstate={sh:caps_lock}]": {
"foreground": "var(--secondary)"
},
"key[code={c:space}]": {
"key[code=32]": {
"background": "var(--surface)",
"foreground": "var(--on-surface-variant)",
"font-size": "12sp",
"margin": "0dp 6dp",
"text-overflow": "ellipsis"
},
"key[code=-201,-202,-203]": {
"font-size": "18sp"
},
"key[code=-204,-205]": {
"font-size": "12sp"
},
"key[code=-205]": {
"text-max-lines": "2"
},
"key[code=-11][shiftstate=`caps_lock`]": {
"foreground": "var(--secondary)"
},
"key-hint": {
"background": "transparent",
"foreground": "var(--on-surface-variant)",
"font-size": "12sp"
"font-size": "12sp",
"font-family": "monospace",
"padding": "0dp 1dp 1dp 0dp",
"text-max-lines": "1"
},
"key-popup": {
"background": "#eeeeee",
"key-popup-box": {
"background": "var(--popup-surface)",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"key-popup:focus": {
"background": "#bdbdbd",
"foreground": "var(--on-surface)"
"key-popup-element:focus": {
"background": "var(--focused-popup-surface)",
"foreground": "var(--on-surface)",
"shape": "var(--shape)"
},
"key-popup-extended-indicator": {
"font-size": "16sp"
},
"smartbar": {
"font-size": "18sp"
},
"smartbar-shared-actions-toggle": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"margin": "6dp",
"shape": "circle()",
"shadow-elevation": "2dp"
},
@@ -77,7 +117,6 @@
"smartbar-action-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "18sp",
"shape": "var(--shape)"
},
"smartbar-action-key:pressed": {
@@ -85,48 +124,80 @@
"foreground": "var(--on-surface)"
},
"smartbar-action-key:disabled": {
"background": "transparent",
"foreground": "#12121248"
"foreground": "var(--on-background-disabled)"
},
"smartbar-action-tile": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "18sp",
"shape": "var(--shape)"
"font-size": "14sp",
"shape": "var(--shape)",
"text-max-lines": "2",
"text-align": "center",
"text-overflow": "ellipsis"
},
"smartbar-action-tile:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"smartbar-action-tile:disabled": {
"background": "transparent",
"foreground": "#12121248"
"foreground": "var(--on-background-disabled)"
},
"smartbar-actions-overflow-customize-button": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"shape": "circle()",
"shadow-elevation": "2dp"
"shape": "circle()"
},
"smartbar-actions-editor": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
},
"smartbar-actions-editor-header": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "16sp"
"font-size": "16sp",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-header-button": {
"margin": "4dp",
"shape": "circle()"
},
"smartbar-actions-editor-subheader": {
"foreground": "var(--on-background)",
"font-size": "16sp"
"font-size": "16sp",
"font-weight": "bold",
"padding": "12dp 16dp 12dp 8dp",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-tile-grid": {
"margin": "4dp 0dp"
},
"smartbar-actions-editor-tile": {
"margin": "4dp",
"padding": "8dp",
"text-align": "center",
"text-max-lines": "2",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-tile[code=-999]": {
"foreground": "var(--on-background-disabled)"
},
"smartbar-actions-editor-tile[code=-991]": {
"foreground": "var(--drag-marker)"
},
"smartbar-candidate-word": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"shape": "rectangle()"
"margin": "4dp",
"padding": "8dp 0dp",
"shape": "rectangle()",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-candidate-word:pressed": {
"background": "var(--surface)",
@@ -136,7 +207,11 @@
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"shape": "rounded-corner(8%, 8%, 8%, 8%)"
"margin": "4dp",
"padding": "8dp 0dp",
"shape": "rounded-corner(8%, 8%, 8%, 8%)",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-candidate-clip:pressed": {
"background": "var(--surface)",
@@ -151,43 +226,80 @@
"foreground": "var(--on-surface)",
"font-size": "16sp"
},
"clipboard-header-button": {
"margin": "4dp",
"shape": "circle()"
},
"clipboard-header-button:disabled": {
"foreground": "var(--on-background-disabled)"
},
"clipboard-header-text": {
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"clipboard-subheader": {
"font-size": "14sp",
"margin": "6dp"
},
"clipboard-content": {
"padding": "10dp"
},
"clipboard-item": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"margin": "4dp",
"padding": "12dp 8dp",
"shape": "var(--shape-variant)",
"shadow-elevation": "2dp"
},
"clipboard-item-popup": {
"background": "var(--surface-variant)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"margin": "4dp",
"padding": "12dp 8dp",
"shape": "var(--shape-variant)",
"shadow-elevation": "2dp"
},
"emoji-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "22sp",
"shape": "var(--shape)"
"clipboard-item-popup-action": {
"font-size": "16sp",
"padding": "12dp"
},
"emoji-key:pressed": {
"clipboard-clear-all-dialog": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"emoji-key-popup": {
"background": "#eeeeee",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
"shape": "var(--shape-variant)",
"shadow-elevation": "1dp"
},
"emoji-tab": {
"foreground": "var(--on-background)"
"clipboard-clear-all-dialog-message": {
"padding": "16dp"
},
"emoji-tab:focus": {
"foreground": "var(--primary)"
"clipboard-clear-all-dialog-buttons": {
"padding": "4dp"
},
"clipboard-clear-all-dialog-button": {
"background": "transparent",
"foreground": "var(--on-surface)",
"shape": "var(--shape-variant)"
},
"clipboard-history-disabled-title": {
"font-weight": "bold"
},
"clipboard-history-disabled-message": {
"padding": "0dp 4dp 0dp 8dp"
},
"clipboard-history-disabled-button": {
"background": "var(--primary)",
"foreground": "var(--on-primary)",
"shape": "rounded-corner(24dp,24dp,24dp,24dp)"
},
"clipboard-history-locked-title": {
"font-weight": "bold",
"text-align": "center"
},
"clipboard-history-locked-message": {
"padding": "0dp 4dp 0dp 0dp",
"text-align": "center"
},
"extracted-landscape-input-layout": {
@@ -212,15 +324,71 @@
},
"incognito-mode-indicator": {
"foreground": "#00000011"
"foreground": "var(--incognito-icon-color)"
},
"inline-autofill-chip": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"media-emoji-subheader": {
"font-weight": "bold",
"margin": "4dp"
},
"media-emoji-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "22sp",
"shape": "var(--shape)"
},
"media-emoji-key:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"media-emoji-key-popup-box": {
"background": "var(--popup-surface)",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"media-emoji-key-popup-element:focus": {
"background": "var(--focused-popup-surface)",
"shape": "var(--shape)"
},
"media-emoji-tab": {
"foreground": "var(--on-background)"
},
"media-emoji-tab:focus": {
"foreground": "var(--primary)"
},
"media-bottom-row-button": {
"padding": "16dp 0dp",
"shape": "var(--shape)"
},
"media-emoji-key-popup-extended-indicator": {
"foreground": "inherit"
},
"one-handed-panel": {
"background": "#e8f5e9",
"foreground": "#424242"
"background": "var(--one-hand-background)",
"foreground": "var(--one-hand-foreground)"
},
"system-nav-bar": {
"background": "var(--background)"
"subtype-panel": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
},
"subtype-panel-header": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "18sp",
"padding": "12dp",
"text-align": "center",
"text-max-lines": "1",
"text-overflow": "ellipsis"
}
}

View File

@@ -1,4 +1,5 @@
{
"$schema": "https://schemas.florisboard.org/snygg/v2/stylesheet",
"@defines": {
"--primary": "#4caf50",
"--primary-variant": "#388e3c",
@@ -7,8 +8,16 @@
"--background": "#212121",
"--surface": "#424242",
"--surface-variant": "#616161",
"--popup-surface": "#757575",
"--focused-popup-surface": "#bdbdbd",
"--drag-marker": "rgb(255,0,0)",
"--spacer-color": "rgba(255, 255, 255, 0.25)",
"--one-hand-background": "#1b5e20",
"--one-hand-foreground": "#eeeeee",
"--incognito-icon-color": "#ffffff11",
"--on-primary": "#f0f0f0",
"--on-background": "#dcdcdc",
"--on-background-disabled": "#dcdcdc48",
"--on-surface": "#ffffff",
"--on-surface-variant": "#a0a0a0",
@@ -16,8 +25,10 @@
"--shape-variant": "rounded-corner(12dp, 12dp, 12dp, 12dp)"
},
"keyboard": {
"background": "var(--background)"
"window": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"clip": "no"
},
"key": {
@@ -25,48 +36,68 @@
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
"shadow-elevation": "2dp",
"text-max-lines": "1"
},
"key:pressed": {
"background": "var(--surface-variant)",
"foreground": "var(--on-surface)"
},
"key[code={c:enter}]": {
"key[code=10]": {
"background": "var(--primary)",
"foreground": "var(--on-surface)"
},
"key[code={c:enter}]:pressed": {
"key[code=10]:pressed": {
"background": "var(--primary-variant)",
"foreground": "var(--on-surface)"
},
"key[code={c:shift}][shiftstate={sh:caps_lock}]": {
"foreground": "var(--secondary)"
},
"key[code={c:space}]": {
"key[code=32]": {
"background": "var(--surface)",
"foreground": "var(--on-surface-variant)",
"font-size": "12sp",
"text-overflow": "ellipsis"
},
"key[code=-201,-202,-203]": {
"font-size": "18sp"
},
"key[code=-204,-205]": {
"font-size": "12sp"
},
"key[code=-205]": {
"text-max-lines": "2"
},
"key[code=-11][shiftstate=`caps_lock`]": {
"foreground": "var(--secondary)"
},
"key-hint": {
"background": "transparent",
"foreground": "var(--on-surface-variant)",
"font-size": "12sp"
"font-family": "monospace",
"font-size": "12sp",
"padding": "0dp 1dp 1dp 0dp",
"text-max-lines": "1"
},
"key-popup": {
"background": "#757575",
"key-popup-box": {
"background": "var(--popup-surface)",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"key-popup:focus": {
"background": "#bdbdbd",
"foreground": "var(--on-surface)"
"key-popup-element:focus": {
"background": "var(--focused-popup-surface)",
"shape": "var(--shape)"
},
"key-popup-extended-indicator": {
"font-size": "16sp"
},
"smartbar": {
"font-size": "18sp"
},
"smartbar-shared-actions-toggle": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"margin": "6dp",
"shape": "circle()",
"shadow-elevation": "2dp"
},
@@ -78,117 +109,180 @@
"smartbar-action-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "18sp",
"shape": "var(--shape)"
},
"smartbar-action-key:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"smartbar-action-key:disabled": {
"background": "transparent",
"foreground": "#dcdcdc48"
"foreground": "var(--on-background-disabled)"
},
"smartbar-action-tile": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "18sp",
"shape": "var(--shape)"
},
"smartbar-action-tile:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
"font-size": "14sp",
"shape": "var(--shape)",
"text-max-lines": "2",
"text-align": "center",
"text-overflow": "ellipsis"
},
"smartbar-action-tile:disabled": {
"background": "transparent",
"foreground": "#dcdcdc48"
"foreground": "var(--on-background-disabled)"
},
"smartbar-actions-overflow-customize-button": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"shape": "circle()",
"shadow-elevation": "2dp"
"shape": "circle()"
},
"smartbar-actions-editor": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
},
"smartbar-actions-editor-header": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "16sp"
"font-size": "16sp",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-header-button": {
"margin": "4dp",
"shape": "circle()"
},
"smartbar-actions-editor-subheader": {
"foreground": "var(--on-background)",
"font-size": "16sp"
"foreground": "var(--secondary)",
"font-size": "16sp",
"font-weight": "bold",
"padding": "12dp 16dp 12dp 8dp",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-tile-grid": {
"margin": "4dp 0dp"
},
"smartbar-actions-editor-tile": {
"margin": "4dp",
"padding": "8dp",
"text-align": "center",
"text-max-lines": "2",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-tile[code=-999]": {
"foreground": "var(--on-background-disabled)"
},
"smartbar-actions-editor-tile[code=-991]": {
"foreground": "var(--drag-marker)"
},
"smartbar-candidate-word": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"shape": "rectangle()"
"margin": "4dp",
"padding": "8dp 0dp",
"shape": "rectangle()",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-candidate-word:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-background)"
"foreground": "var(--on-surface)"
},
"smartbar-candidate-clip": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"shape": "rounded-corner(8%, 8%, 8%, 8%)"
"margin": "4dp",
"padding": "8dp 0dp",
"shape": "rounded-corner(8%, 8%, 8%, 8%)",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-candidate-clip:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-background)"
"foreground": "var(--on-surface)"
},
"smartbar-candidate-spacer": {
"foreground": "var(--surface)"
"foreground": "var(--spacer-color)"
},
"clipboard-header": {
"background": "transparent",
"foreground": "var(--on-surface)",
"foreground": "var(--on-background)",
"font-size": "16sp"
},
"clipboard-header-button": {
"margin": "4dp",
"shape": "circle()"
},
"clipboard-header-button:disabled": {
"foreground": "var(--on-background-disabled)"
},
"clipboard-header-text": {
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"clipboard-subheader": {
"font-size": "14sp",
"margin": "6dp"
},
"clipboard-content": {
"padding": "10dp"
},
"clipboard-item": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"margin": "4dp",
"padding": "12dp 8dp",
"shape": "var(--shape-variant)",
"shadow-elevation": "2dp"
},
"clipboard-item-popup": {
"background": "var(--surface-variant)",
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"margin": "4dp",
"shape": "var(--shape-variant)",
"shadow-elevation": "2dp"
},
"emoji-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "22sp",
"shape": "var(--shape)"
"clipboard-item-popup-action": {
"font-size": "16sp",
"padding": "12dp"
},
"emoji-key:pressed": {
"clipboard-clear-all-dialog": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"emoji-key-popup": {
"background": "#757575",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
"shape": "var(--shape-variant)",
"shadow-elevation": "1dp"
},
"emoji-tab": {
"foreground": "var(--on-background)"
"clipboard-clear-all-dialog-message": {
"padding": "16dp"
},
"emoji-tab:focus": {
"foreground": "var(--primary)"
"clipboard-clear-all-dialog-buttons": {
"padding": "4dp"
},
"clipboard-clear-all-dialog-button": {
"background": "transparent",
"foreground": "var(--on-surface)",
"shape": "var(--shape-variant)"
},
"clipboard-history-disabled-title": {
"font-weight": "bold"
},
"clipboard-history-disabled-message": {
"padding": "0dp 4dp 0dp 8dp"
},
"clipboard-history-disabled-button": {
"background": "var(--primary)",
"foreground": "var(--on-primary)",
"shape": "rounded-corner(24dp,24dp,24dp,24dp)"
},
"clipboard-history-locked-title": {
"font-weight": "bold",
"text-align": "center"
},
"clipboard-history-locked-message": {
"padding": "0dp 4dp 0dp 0dp",
"text-align": "center"
},
"extracted-landscape-input-layout": {
@@ -213,15 +307,69 @@
},
"incognito-mode-indicator": {
"foreground": "#ffffff11"
"foreground": "var(--incognito-icon-color)"
},
"inline-autofill-chip": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"media-emoji-subheader": {
"font-weight": "bold",
"margin": "4dp"
},
"media-emoji-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "22sp",
"shape": "var(--shape)"
},
"media-emoji-key:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"media-emoji-key-popup-box": {
"background": "var(--popup-surface)",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"media-emoji-key-popup-element:focus": {
"background": "var(--focused-popup-surface)",
"shape": "var(--shape)"
},
"media-emoji-tab": {
"foreground": "var(--on-background)"
},
"media-emoji-tab:focus": {
"foreground": "var(--primary)"
},
"media-bottom-row-button": {
"padding": "16dp 0dp",
"shape": "var(--shape)"
},
"media-emoji-key-popup-extended-indicator": {
"foreground": "inherit"
},
"one-handed-panel": {
"background": "#1b5e20",
"foreground": "#eeeeee"
"background": "var(--one-hand-background)",
"foreground": "var(--one-hand-foreground)"
},
"system-nav-bar": {
"background": "var(--background)"
"subtype-panel": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
},
"subtype-panel-header": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "18sp",
"padding": "12dp",
"text-align": "center",
"text-max-lines": "1",
"text-overflow": "ellipsis"
}
}

View File

@@ -1,4 +1,5 @@
{
"$schema": "https://schemas.florisboard.org/snygg/v2/stylesheet",
"@defines": {
"--primary": "#4caf50",
"--primary-variant": "#388e3c",
@@ -7,8 +8,16 @@
"--background": "#212121",
"--surface": "#424242",
"--surface-variant": "#616161",
"--popup-surface": "#757575",
"--focused-popup-surface": "#bdbdbd",
"--drag-marker": "rgb(255,0,0)",
"--spacer-color": "rgba(255, 255, 255, 0.25)",
"--one-hand-background": "#1b5e20",
"--one-hand-foreground": "#eeeeee",
"--incognito-icon-color": "#ffffff11",
"--on-primary": "#f0f0f0",
"--on-background": "#dcdcdc",
"--on-background-disabled": "#dcdcdc48",
"--on-surface": "#ffffff",
"--on-surface-variant": "#a0a0a0",
@@ -16,56 +25,81 @@
"--shape-variant": "rounded-corner(12dp, 12dp, 12dp, 12dp)"
},
"keyboard": {
"background": "var(--background)"
"window": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"clip": "no"
},
"key": {
"background": "transparent",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)"
"shape": "var(--shape)",
"text-max-lines": "1"
},
"key:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"key[code={c:enter}]": {
"key[code=10]": {
"background": "var(--primary)",
"foreground": "var(--on-surface)"
"foreground": "var(--on-surface)",
"margin": "0dp 6dp"
},
"key[code={c:enter}]:pressed": {
"key[code=10]:pressed": {
"background": "var(--primary-variant)",
"foreground": "var(--on-surface)"
},
"key[code={c:shift}][shiftstate={sh:caps_lock}]": {
"foreground": "var(--secondary)"
},
"key[code={c:space}]": {
"key[code=32]": {
"background": "var(--surface)",
"foreground": "var(--on-surface-variant)",
"font-size": "12sp",
"margin": "0dp 6dp",
"text-overflow": "ellipsis"
},
"key[code=-201,-202,-203]": {
"font-size": "18sp"
},
"key[code=-204,-205]": {
"font-size": "12sp"
},
"key[code=-205]": {
"text-max-lines": "2"
},
"key[code=-11][shiftstate=`caps_lock`]": {
"foreground": "var(--secondary)"
},
"key-hint": {
"background": "transparent",
"foreground": "var(--on-surface-variant)",
"font-size": "12sp"
"font-size": "12sp",
"font-family": "monospace",
"padding": "0dp 1dp 1dp 0dp",
"text-max-lines": "1"
},
"key-popup": {
"background": "#757575",
"key-popup-box": {
"background": "var(--popup-surface)",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"key-popup:focus": {
"background": "#bdbdbd",
"foreground": "var(--on-surface)"
"key-popup-element:focus": {
"background": "var(--focused-popup-surface)",
"foreground": "var(--on-surface)",
"shape": "var(--shape)"
},
"key-popup-extended-indicator": {
"font-size": "16sp"
},
"smartbar": {
"font-size": "18sp"
},
"smartbar-shared-actions-toggle": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"margin": "6dp",
"shape": "circle()",
"shadow-elevation": "2dp"
},
@@ -77,7 +111,6 @@
"smartbar-action-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "18sp",
"shape": "var(--shape)"
},
"smartbar-action-key:pressed": {
@@ -85,48 +118,80 @@
"foreground": "var(--on-surface)"
},
"smartbar-action-key:disabled": {
"background": "transparent",
"foreground": "#dcdcdc48"
"foreground": "var(--on-background-disabled)"
},
"smartbar-action-tile": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "18sp",
"shape": "var(--shape)"
"font-size": "14sp",
"shape": "var(--shape)",
"text-max-lines": "2",
"text-align": "center",
"text-overflow": "ellipsis"
},
"smartbar-action-tile:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"smartbar-action-tile:disabled": {
"background": "transparent",
"foreground": "#dcdcdc48"
"foreground": "var(--on-background-disabled)"
},
"smartbar-actions-overflow-customize-button": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"shape": "circle()",
"shadow-elevation": "2dp"
"shape": "circle()"
},
"smartbar-actions-editor": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
},
"smartbar-actions-editor-header": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "16sp"
"font-size": "16sp",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-header-button": {
"margin": "4dp",
"shape": "circle()"
},
"smartbar-actions-editor-subheader": {
"foreground": "var(--on-background)",
"font-size": "16sp"
"font-size": "16sp",
"font-weight": "bold",
"padding": "12dp 16dp 12dp 8dp",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-tile-grid": {
"margin": "4dp 0dp"
},
"smartbar-actions-editor-tile": {
"margin": "4dp",
"padding": "8dp",
"text-align": "center",
"text-max-lines": "2",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-tile[code=-999]": {
"foreground": "var(--on-background-disabled)"
},
"smartbar-actions-editor-tile[code=-991]": {
"foreground": "var(--drag-marker)"
},
"smartbar-candidate-word": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"shape": "rectangle()"
"margin": "4dp",
"padding": "8dp 0dp",
"shape": "rectangle()",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-candidate-word:pressed": {
"background": "var(--surface)",
@@ -136,7 +201,11 @@
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"shape": "rounded-corner(8%, 8%, 8%, 8%)"
"margin": "4dp",
"padding": "8dp 0dp",
"shape": "rounded-corner(8%, 8%, 8%, 8%)",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-candidate-clip:pressed": {
"background": "var(--surface)",
@@ -151,43 +220,80 @@
"foreground": "var(--on-surface)",
"font-size": "16sp"
},
"clipboard-header-button": {
"margin": "4dp",
"shape": "circle()"
},
"clipboard-header-button:disabled": {
"foreground": "var(--on-background-disabled)"
},
"clipboard-header-text": {
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"clipboard-subheader": {
"font-size": "14sp",
"margin": "6dp"
},
"clipboard-content": {
"padding": "10dp"
},
"clipboard-item": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"margin": "4dp",
"padding": "12dp 8dp",
"shape": "var(--shape-variant)",
"shadow-elevation": "2dp"
},
"clipboard-item-popup": {
"background": "var(--surface-variant)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"margin": "4dp",
"padding": "12dp 8dp",
"shape": "var(--shape-variant)",
"shadow-elevation": "2dp"
},
"emoji-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "22sp",
"shape": "var(--shape)"
"clipboard-item-popup-action": {
"font-size": "16sp",
"padding": "12dp"
},
"emoji-key:pressed": {
"clipboard-clear-all-dialog": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"emoji-key-popup": {
"background": "#757575",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
"shape": "var(--shape-variant)",
"shadow-elevation": "1dp"
},
"emoji-tab": {
"foreground": "var(--on-background)"
"clipboard-clear-all-dialog-message": {
"padding": "16dp"
},
"emoji-tab:focus": {
"foreground": "var(--primary)"
"clipboard-clear-all-dialog-buttons": {
"padding": "4dp"
},
"clipboard-clear-all-dialog-button": {
"background": "transparent",
"foreground": "var(--on-surface)",
"shape": "var(--shape-variant)"
},
"clipboard-history-disabled-title": {
"font-weight": "bold"
},
"clipboard-history-disabled-message": {
"padding": "0dp 4dp 0dp 8dp"
},
"clipboard-history-disabled-button": {
"background": "var(--primary)",
"foreground": "var(--on-primary)",
"shape": "rounded-corner(24dp,24dp,24dp,24dp)"
},
"clipboard-history-locked-title": {
"font-weight": "bold",
"text-align": "center"
},
"clipboard-history-locked-message": {
"padding": "0dp 4dp 0dp 0dp",
"text-align": "center"
},
"extracted-landscape-input-layout": {
@@ -212,15 +318,69 @@
},
"incognito-mode-indicator": {
"foreground": "#ffffff11"
"foreground": "var(--incognito-icon-color)"
},
"inline-autofill-chip": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"media-emoji-subheader": {
"font-weight": "bold",
"margin": "4dp"
},
"media-emoji-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "22sp",
"shape": "var(--shape)"
},
"media-emoji-key:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"media-emoji-key-popup-box": {
"background": "var(--popup-surface)",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"media-emoji-key-popup-element:focus": {
"background": "var(--focused-popup-surface)",
"shape": "var(--shape)"
},
"media-emoji-tab": {
"foreground": "var(--on-background)"
},
"media-emoji-tab:focus": {
"foreground": "var(--primary)"
},
"media-bottom-row-button": {
"padding": "16dp 0dp",
"shape": "var(--shape)"
},
"media-emoji-key-popup-extended-indicator": {
"foreground": "inherit"
},
"one-handed-panel": {
"background": "#1b5e20",
"foreground": "#eeeeee"
"background": "var(--one-hand-background)",
"foreground": "var(--one-hand-foreground)"
},
"system-nav-bar": {
"background": "var(--background)"
"subtype-panel": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
},
"subtype-panel-header": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "18sp",
"padding": "12dp",
"text-align": "center",
"text-max-lines": "1",
"text-overflow": "ellipsis"
}
}

View File

@@ -1,4 +1,5 @@
{
"$schema": "https://schemas.florisboard.org/snygg/v2/stylesheet",
"@defines": {
"--primary": "#388e3c",
"--primary-variant": "#306d32",
@@ -7,7 +8,15 @@
"--background": "#000000",
"--surface": "#212121",
"--surface-variant": "#3d3d3d",
"--popup-surface": "#424242",
"--focused-popup-surface": "#707070",
"--drag-marker": "rgb(255,0,0)",
"--spacer-color": "rgba(255, 255, 255, 0.25)",
"--one-hand-background": "#1b5e20",
"--one-hand-foreground": "#eeeeee",
"--incognito-icon-color": "#ffffff11",
"--on-primary": "#f0f0f0",
"--on-background-disabled": "#dcdcdc48",
"--on-background": "#eeeeee",
"--on-surface": "#eeeeee",
"--on-surface-variant": "#ffffff73",
@@ -16,8 +25,10 @@
"--shape-variant": "rounded-corner(12dp, 12dp, 12dp, 12dp)"
},
"keyboard": {
"background": "var(--background)"
"window": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"clip": "no"
},
"key": {
@@ -25,48 +36,68 @@
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
"shadow-elevation": "2dp",
"text-max-lines": "1"
},
"key:pressed": {
"background": "var(--surface-variant)",
"foreground": "var(--on-surface)"
},
"key[code={c:enter}]": {
"key[code=10]": {
"background": "var(--primary)",
"foreground": "var(--on-surface)"
},
"key[code={c:enter}]:pressed": {
"key[code=10]:pressed": {
"background": "var(--primary-variant)",
"foreground": "var(--on-surface)"
},
"key[code={c:shift}][shiftstate={sh:caps_lock}]": {
"foreground": "var(--secondary)"
},
"key[code={c:space}]": {
"key[code=32]": {
"background": "var(--surface)",
"foreground": "var(--on-surface-variant)",
"font-size": "12sp",
"text-overflow": "ellipsis"
},
"key[code=-201,-202,-203]": {
"font-size": "18sp"
},
"key[code=-204,-205]": {
"font-size": "12sp"
},
"key[code=-205]": {
"text-max-lines": "2"
},
"key[code=-11][shiftstate=`caps_lock`]": {
"foreground": "var(--secondary)"
},
"key-hint": {
"background": "transparent",
"foreground": "var(--on-surface-variant)",
"font-size": "12sp"
"font-family": "monospace",
"font-size": "12sp",
"padding": "0dp 1dp 1dp 0dp",
"text-max-lines": "1"
},
"key-popup": {
"background": "#424242",
"key-popup-box": {
"background": "var(--popup-surface)",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"key-popup:focus": {
"background": "#707070",
"foreground": "var(--on-surface)"
"key-popup-element:focus": {
"background": "var(--focused-popup-surface)",
"shape": "var(--shape)"
},
"key-popup-extended-indicator": {
"font-size": "16sp"
},
"smartbar": {
"font-size": "18sp"
},
"smartbar-shared-actions-toggle": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"margin": "6dp",
"shape": "circle()",
"shadow-elevation": "2dp"
},
@@ -78,7 +109,6 @@
"smartbar-action-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "18sp",
"shape": "var(--shape)"
},
"smartbar-action-key:pressed": {
@@ -86,109 +116,177 @@
"foreground": "var(--on-surface)"
},
"smartbar-action-key:disabled": {
"background": "transparent",
"foreground": "#dcdcdc48"
"foreground": "var(--on-background-disabled)"
},
"smartbar-action-tile": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "18sp",
"shape": "var(--shape)"
},
"smartbar-action-tile:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
"font-size": "14sp",
"shape": "var(--shape)",
"text-max-lines": "2",
"text-align": "center",
"text-overflow": "ellipsis"
},
"smartbar-action-tile:disabled": {
"background": "transparent",
"foreground": "#dcdcdc48"
"foreground": "var(--on-background-disabled)"
},
"smartbar-actions-overflow-customize-button": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"shape": "circle()",
"shadow-elevation": "2dp"
"shape": "circle()"
},
"smartbar-actions-editor": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
},
"smartbar-actions-editor-header": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "16sp"
"font-size": "16sp",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-header-button": {
"margin": "4dp",
"shape": "circle()"
},
"smartbar-actions-editor-subheader": {
"foreground": "var(--on-background)",
"font-size": "16sp"
"foreground": "var(--secondary)",
"font-size": "16sp",
"font-weight": "bold",
"padding": "12dp 16dp 12dp 8dp",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-tile-grid": {
"margin": "4dp 0dp"
},
"smartbar-actions-editor-tile": {
"margin": "4dp",
"padding": "8dp",
"text-align": "center",
"text-max-lines": "2",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-tile[code=-999]": {
"foreground": "var(--on-background-disabled)"
},
"smartbar-actions-editor-tile[code=-991]": {
"foreground": "var(--drag-marker)"
},
"smartbar-candidate-word": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"shape": "rectangle()"
"margin": "4dp",
"padding": "8dp 0dp",
"shape": "rectangle()",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-candidate-word:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-background)"
"foreground": "var(--on-surface)"
},
"smartbar-candidate-clip": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"shape": "rounded-corner(8%, 8%, 8%, 8%)"
"margin": "4dp",
"padding": "8dp 0dp",
"shape": "rounded-corner(8%, 8%, 8%, 8%)",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-candidate-clip:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-background)"
"foreground": "var(--on-surface)"
},
"smartbar-candidate-spacer": {
"foreground": "var(--surface)"
"foreground": "var(--spacer-color)"
},
"clipboard-header": {
"background": "transparent",
"foreground": "var(--on-surface)",
"foreground": "var(--on-background)",
"font-size": "16sp"
},
"clipboard-header-button": {
"margin": "4dp",
"shape": "circle()"
},
"clipboard-header-button:disabled": {
"foreground": "var(--on-background-disabled)"
},
"clipboard-header-text": {
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"clipboard-subheader": {
"font-size": "14sp",
"margin": "6dp"
},
"clipboard-content": {
"padding": "10dp"
},
"clipboard-item": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"margin": "4dp",
"padding": "12dp 8dp",
"shape": "var(--shape-variant)",
"shadow-elevation": "2dp"
},
"clipboard-item-popup": {
"background": "var(--surface-variant)",
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"margin": "4dp",
"shape": "var(--shape-variant)",
"shadow-elevation": "2dp"
},
"emoji-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "22sp",
"shape": "var(--shape)"
"clipboard-item-popup-action": {
"font-size": "16sp",
"padding": "12dp"
},
"emoji-key:pressed": {
"clipboard-clear-all-dialog": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"emoji-key-popup": {
"background": "#757575",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
"shape": "var(--shape-variant)",
"shadow-elevation": "1dp"
},
"emoji-tab": {
"foreground": "var(--on-background)"
"clipboard-clear-all-dialog-message": {
"padding": "16dp"
},
"emoji-tab:focus": {
"foreground": "var(--primary)"
"clipboard-clear-all-dialog-buttons": {
"padding": "4dp"
},
"clipboard-clear-all-dialog-button": {
"background": "transparent",
"foreground": "var(--on-surface)",
"shape": "var(--shape-variant)"
},
"clipboard-history-disabled-title": {
"font-weight": "bold"
},
"clipboard-history-disabled-message": {
"padding": "0dp 4dp 0dp 8dp"
},
"clipboard-history-disabled-button": {
"background": "var(--primary)",
"foreground": "var(--on-primary)",
"shape": "rounded-corner(24dp,24dp,24dp,24dp)"
},
"clipboard-history-locked-title": {
"font-weight": "bold",
"text-align": "center"
},
"clipboard-history-locked-message": {
"padding": "0dp 4dp 0dp 0dp",
"text-align": "center"
},
"extracted-landscape-input-layout": {
@@ -199,29 +297,83 @@
"foreground": "var(--on-background)",
"font-size": "16sp",
"shape": "rounded-corner(12dp, 12dp, 12dp, 12dp)",
"border-color": "var(--secondary-variant)",
"border-width": "1dp"
"border-color": "var(--secondary)",
"border-width": "2dp"
},
"extracted-landscape-input-action": {
"background": "var(--surface)",
"background": "var(--primary)",
"foreground": "var(--on-surface)",
"shape": "rounded-corner(4dp, 4dp, 4dp, 4dp)"
},
"glide-trail": {
"foreground": "var(--primary-variant)"
"foreground": "var(--primary)"
},
"incognito-mode-indicator": {
"foreground": "#ffffff11"
"foreground": "var(--incognito-icon-color)"
},
"inline-autofill-chip": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"media-emoji-subheader": {
"font-weight": "bold",
"margin": "4dp"
},
"media-emoji-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "22sp",
"shape": "var(--shape)"
},
"media-emoji-key:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"media-emoji-key-popup-box": {
"background": "var(--popup-surface)",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"media-emoji-key-popup-element:focus": {
"background": "var(--focused-popup-surface)",
"shape": "var(--shape)"
},
"media-emoji-tab": {
"foreground": "var(--on-background)"
},
"media-emoji-tab:focus": {
"foreground": "var(--primary)"
},
"media-bottom-row-button": {
"padding": "16dp 0dp",
"shape": "var(--shape)"
},
"media-emoji-key-popup-extended-indicator": {
"foreground": "inherit"
},
"one-handed-panel": {
"background": "#000000",
"foreground": "#eeeeee"
"background": "var(--one-hand-background)",
"foreground": "var(--one-hand-foreground)"
},
"system-nav-bar": {
"background": "var(--background)"
"subtype-panel": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
},
"subtype-panel-header": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "18sp",
"padding": "12dp",
"text-align": "center",
"text-max-lines": "1",
"text-overflow": "ellipsis"
}
}

View File

@@ -1,4 +1,5 @@
{
"$schema": "https://schemas.florisboard.org/snygg/v2/stylesheet",
"@defines": {
"--primary": "#388e3c",
"--primary-variant": "#306d32",
@@ -7,7 +8,15 @@
"--background": "#000000",
"--surface": "#212121",
"--surface-variant": "#3d3d3d",
"--popup-surface": "#424242",
"--focused-popup-surface": "#707070",
"--drag-marker": "rgb(255,0,0)",
"--spacer-color": "rgba(255, 255, 255, 0.25)",
"--one-hand-background": "#1b5e20",
"--one-hand-foreground": "#eeeeee",
"--incognito-icon-color": "#ffffff11",
"--on-primary": "#f0f0f0",
"--on-background-disabled": "#dcdcdc48",
"--on-background": "#eeeeee",
"--on-surface": "#eeeeee",
"--on-surface-variant": "#ffffff73",
@@ -16,56 +25,81 @@
"--shape-variant": "rounded-corner(12dp, 12dp, 12dp, 12dp)"
},
"keyboard": {
"background": "var(--background)"
"window": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"clip": "no"
},
"key": {
"background": "transparent",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)"
"shape": "var(--shape)",
"text-max-lines": "1"
},
"key:pressed": {
"background": "#6161617f",
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"key[code={c:enter}]": {
"key[code=10]": {
"background": "var(--primary)",
"foreground": "var(--on-surface)"
"foreground": "var(--on-surface)",
"margin": "0dp 6dp"
},
"key[code={c:enter}]:pressed": {
"key[code=10]:pressed": {
"background": "var(--primary-variant)",
"foreground": "var(--on-surface)"
},
"key[code={c:shift}][shiftstate={sh:caps_lock}]": {
"foreground": "var(--secondary)"
},
"key[code={c:space}]": {
"background": "#61616146",
"key[code=32]": {
"background": "var(--surface)",
"foreground": "var(--on-surface-variant)",
"font-size": "12sp",
"margin": "0dp 6dp",
"text-overflow": "ellipsis"
},
"key[code=-201,-202,-203]": {
"font-size": "18sp"
},
"key[code=-204,-205]": {
"font-size": "12sp"
},
"key[code=-205]": {
"text-max-lines": "2"
},
"key[code=-11][shiftstate=`caps_lock`]": {
"foreground": "var(--secondary)"
},
"key-hint": {
"background": "transparent",
"foreground": "var(--on-surface-variant)",
"font-size": "12sp"
"font-size": "12sp",
"font-family": "monospace",
"padding": "0dp 1dp 1dp 0dp",
"text-max-lines": "1"
},
"key-popup": {
"background": "#363636",
"key-popup-box": {
"background": "var(--popup-surface)",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"key-popup:focus": {
"background": "#5F5F5F",
"foreground": "var(--on-surface)"
"key-popup-element:focus": {
"background": "var(--focused-popup-surface)",
"foreground": "var(--on-surface)",
"shape": "var(--shape)"
},
"key-popup-extended-indicator": {
"font-size": "16sp"
},
"smartbar": {
"font-size": "18sp"
},
"smartbar-shared-actions-toggle": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"margin": "6dp",
"shape": "circle()",
"shadow-elevation": "2dp"
},
@@ -77,7 +111,6 @@
"smartbar-action-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "18sp",
"shape": "var(--shape)"
},
"smartbar-action-key:pressed": {
@@ -85,48 +118,80 @@
"foreground": "var(--on-surface)"
},
"smartbar-action-key:disabled": {
"background": "transparent",
"foreground": "#dcdcdc48"
"foreground": "var(--on-background-disabled)"
},
"smartbar-action-tile": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "18sp",
"shape": "var(--shape)"
"font-size": "14sp",
"shape": "var(--shape)",
"text-max-lines": "2",
"text-align": "center",
"text-overflow": "ellipsis"
},
"smartbar-action-tile:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"smartbar-action-tile:disabled": {
"background": "transparent",
"foreground": "#dcdcdc48"
"foreground": "var(--on-background-disabled)"
},
"smartbar-actions-overflow-customize-button": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"shape": "circle()",
"shadow-elevation": "2dp"
"shape": "circle()"
},
"smartbar-actions-editor": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
},
"smartbar-actions-editor-header": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "16sp"
"font-size": "16sp",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-header-button": {
"margin": "4dp",
"shape": "circle()"
},
"smartbar-actions-editor-subheader": {
"foreground": "var(--on-background)",
"font-size": "16sp"
"font-size": "16sp",
"font-weight": "bold",
"padding": "12dp 16dp 12dp 8dp",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-tile-grid": {
"margin": "4dp 0dp"
},
"smartbar-actions-editor-tile": {
"margin": "4dp",
"padding": "8dp",
"text-align": "center",
"text-max-lines": "2",
"text-overflow": "ellipsis"
},
"smartbar-actions-editor-tile[code=-999]": {
"foreground": "var(--on-background-disabled)"
},
"smartbar-actions-editor-tile[code=-991]": {
"foreground": "var(--drag-marker)"
},
"smartbar-candidate-word": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"shape": "rectangle()"
"margin": "4dp",
"padding": "8dp 0dp",
"shape": "rectangle()",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-candidate-word:pressed": {
"background": "var(--surface)",
@@ -136,7 +201,11 @@
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "14sp",
"shape": "rounded-corner(8%, 8%, 8%, 8%)"
"margin": "4dp",
"padding": "8dp 0dp",
"shape": "rounded-corner(8%, 8%, 8%, 8%)",
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"smartbar-candidate-clip:pressed": {
"background": "var(--surface)",
@@ -151,43 +220,80 @@
"foreground": "var(--on-surface)",
"font-size": "16sp"
},
"clipboard-header-button": {
"margin": "4dp",
"shape": "circle()"
},
"clipboard-header-button:disabled": {
"foreground": "var(--on-background-disabled)"
},
"clipboard-header-text": {
"text-max-lines": "1",
"text-overflow": "ellipsis"
},
"clipboard-subheader": {
"font-size": "14sp",
"margin": "6dp"
},
"clipboard-content": {
"padding": "10dp"
},
"clipboard-item": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"margin": "4dp",
"padding": "12dp 8dp",
"shape": "var(--shape-variant)",
"shadow-elevation": "2dp"
},
"clipboard-item-popup": {
"background": "var(--surface-variant)",
"foreground": "var(--on-surface)",
"font-size": "14sp",
"margin": "4dp",
"padding": "12dp 8dp",
"shape": "var(--shape-variant)",
"shadow-elevation": "2dp"
},
"emoji-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "22sp",
"shape": "var(--shape)"
"clipboard-item-popup-action": {
"font-size": "16sp",
"padding": "12dp"
},
"emoji-key:pressed": {
"clipboard-clear-all-dialog": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"emoji-key-popup": {
"background": "#757575",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
"shape": "var(--shape-variant)",
"shadow-elevation": "1dp"
},
"emoji-tab": {
"foreground": "var(--on-background)"
"clipboard-clear-all-dialog-message": {
"padding": "16dp"
},
"emoji-tab:focus": {
"foreground": "var(--primary)"
"clipboard-clear-all-dialog-buttons": {
"padding": "4dp"
},
"clipboard-clear-all-dialog-button": {
"background": "transparent",
"foreground": "var(--on-surface)",
"shape": "var(--shape-variant)"
},
"clipboard-history-disabled-title": {
"font-weight": "bold"
},
"clipboard-history-disabled-message": {
"padding": "0dp 4dp 0dp 8dp"
},
"clipboard-history-disabled-button": {
"background": "var(--primary)",
"foreground": "var(--on-primary)",
"shape": "rounded-corner(24dp,24dp,24dp,24dp)"
},
"clipboard-history-locked-title": {
"font-weight": "bold",
"text-align": "center"
},
"clipboard-history-locked-message": {
"padding": "0dp 4dp 0dp 0dp",
"text-align": "center"
},
"extracted-landscape-input-layout": {
@@ -198,29 +304,83 @@
"foreground": "var(--on-background)",
"font-size": "16sp",
"shape": "rounded-corner(12dp, 12dp, 12dp, 12dp)",
"border-color": "var(--secondary-variant)",
"border-width": "1dp"
"border-color": "var(--secondary)",
"border-width": "2dp"
},
"extracted-landscape-input-action": {
"background": "var(--surface)",
"background": "var(--primary)",
"foreground": "var(--on-surface)",
"shape": "rounded-corner(4dp, 4dp, 4dp, 4dp)"
},
"glide-trail": {
"foreground": "var(--primary-variant)"
"foreground": "var(--primary)"
},
"incognito-mode-indicator": {
"foreground": "#ffffff11"
"foreground": "var(--incognito-icon-color)"
},
"inline-autofill-chip": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"media-emoji-subheader": {
"font-weight": "bold",
"margin": "4dp"
},
"media-emoji-key": {
"background": "transparent",
"foreground": "var(--on-background)",
"font-size": "22sp",
"shape": "var(--shape)"
},
"media-emoji-key:pressed": {
"background": "var(--surface)",
"foreground": "var(--on-surface)"
},
"media-emoji-key-popup-box": {
"background": "var(--popup-surface)",
"foreground": "var(--on-surface)",
"font-size": "22sp",
"shape": "var(--shape)",
"shadow-elevation": "2dp"
},
"media-emoji-key-popup-element:focus": {
"background": "var(--focused-popup-surface)",
"shape": "var(--shape)"
},
"media-emoji-tab": {
"foreground": "var(--on-background)"
},
"media-emoji-tab:focus": {
"foreground": "var(--primary)"
},
"media-bottom-row-button": {
"padding": "16dp 0dp",
"shape": "var(--shape)"
},
"media-emoji-key-popup-extended-indicator": {
"foreground": "inherit"
},
"one-handed-panel": {
"background": "#000000",
"foreground": "#eeeeee"
"background": "var(--one-hand-background)",
"foreground": "var(--one-hand-foreground)"
},
"system-nav-bar": {
"background": "var(--background)"
"subtype-panel": {
"background": "var(--background)",
"foreground": "var(--on-background)",
"shape": "rounded-corner(24dp, 24dp, 0dp, 0dp)"
},
"subtype-panel-header": {
"background": "var(--surface)",
"foreground": "var(--on-surface)",
"font-size": "18sp",
"padding": "12dp",
"text-align": "center",
"text-max-lines": "1",
"text-overflow": "ellipsis"
}
}

View File

@@ -33,7 +33,6 @@ import dev.patrickgold.florisboard.ime.keyboard.KeyboardManager
import dev.patrickgold.florisboard.ime.media.emoji.FlorisEmojiCompat
import dev.patrickgold.florisboard.ime.nlp.NlpManager
import dev.patrickgold.florisboard.ime.text.gestures.GlideTypingManager
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import dev.patrickgold.florisboard.lib.cache.CacheManager
import dev.patrickgold.florisboard.lib.crashutility.CrashUtility
@@ -61,7 +60,6 @@ class FlorisApplication : Application() {
System.loadLibrary("fl_native")
} catch (_: Exception) {
}
FlorisImeTheme.init()
}
}

View File

@@ -18,6 +18,7 @@ package dev.patrickgold.florisboard
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.res.Configuration
import android.inputmethodservice.ExtractEditText
import android.os.Build
@@ -48,10 +49,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.ButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -79,6 +79,8 @@ import dev.patrickgold.florisboard.app.devtools.DevtoolsOverlay
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.ime.ImeUiMode
import dev.patrickgold.florisboard.ime.clipboard.ClipboardInputLayout
import dev.patrickgold.florisboard.ime.core.SelectSubtypePanel
import dev.patrickgold.florisboard.ime.core.isSubtypeSelectionShowing
import dev.patrickgold.florisboard.ime.editor.EditorRange
import dev.patrickgold.florisboard.ime.editor.FlorisEditorInfo
import dev.patrickgold.florisboard.ime.input.InputFeedbackController
@@ -99,7 +101,7 @@ import dev.patrickgold.florisboard.ime.smartbar.quickaction.QuickActionsEditorPa
import dev.patrickgold.florisboard.ime.text.TextInputLayout
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.lib.compose.FlorisButton
import dev.patrickgold.florisboard.ime.theme.WallpaperChangeReceiver
import dev.patrickgold.florisboard.lib.compose.ProvideLocalizedResources
import dev.patrickgold.florisboard.lib.compose.SystemUiIme
import dev.patrickgold.florisboard.lib.devtools.LogTopic
@@ -111,6 +113,7 @@ import dev.patrickgold.florisboard.lib.util.ViewUtils
import dev.patrickgold.florisboard.lib.util.debugSummarize
import dev.patrickgold.florisboard.lib.util.launchActivity
import dev.patrickgold.jetpref.datastore.model.observeAsState
import java.lang.ref.WeakReference
import org.florisboard.lib.android.AndroidInternalR
import org.florisboard.lib.android.AndroidVersion
import org.florisboard.lib.android.isOrientationLandscape
@@ -118,14 +121,11 @@ import org.florisboard.lib.android.isOrientationPortrait
import org.florisboard.lib.android.showShortToast
import org.florisboard.lib.android.systemServiceOrNull
import org.florisboard.lib.kotlin.collectLatestIn
import org.florisboard.lib.snygg.ui.SnyggSurface
import org.florisboard.lib.snygg.ui.shape
import org.florisboard.lib.snygg.ui.snyggBackground
import org.florisboard.lib.snygg.ui.snyggBorder
import org.florisboard.lib.snygg.ui.snyggShadow
import org.florisboard.lib.snygg.ui.solidColor
import org.florisboard.lib.snygg.ui.spSize
import java.lang.ref.WeakReference
import org.florisboard.lib.snygg.ui.SnyggBox
import org.florisboard.lib.snygg.ui.SnyggButton
import org.florisboard.lib.snygg.ui.SnyggRow
import org.florisboard.lib.snygg.ui.SnyggText
import org.florisboard.lib.snygg.ui.rememberSnyggThemeQuery
/**
* Global weak reference for the [FlorisImeService] class. This is needed as certain actions (request hide, switch to
@@ -232,10 +232,10 @@ class FlorisImeService : LifecycleInputMethodService() {
val imm = ims.systemServiceOrNull(InputMethodManager::class) ?: return false
val list: List<InputMethodInfo> = imm.enabledInputMethodList
for (el in list) {
for (i in 0 until el.subtypeCount){
for (i in 0 until el.subtypeCount) {
if (el.getSubtypeAt(i).mode != "voice") continue
if (AndroidVersion.ATLEAST_API28_P) {
ims.switchInputMethod(el.id)
ims.switchInputMethod(el.id, el.getSubtypeAt(i))
return true
} else {
ims.window.window?.let { window ->
@@ -267,6 +267,8 @@ class FlorisImeService : LifecycleInputMethodService() {
private var isExtractUiShown by mutableStateOf(false)
private var resourcesContext by mutableStateOf(this as Context)
private val wallpaperChangeReceiver = WallpaperChangeReceiver()
init {
setTheme(R.style.FlorisImeTheme)
}
@@ -277,9 +279,20 @@ class FlorisImeService : LifecycleInputMethodService() {
WindowCompat.setDecorFitsSystemWindows(window.window!!, false)
subtypeManager.activeSubtypeFlow.collectLatestIn(lifecycleScope) { subtype ->
val config = Configuration(resources.configuration)
config.setLocale(subtype.primaryLocale.base)
if (prefs.localization.displayKeyboardLabelsInSubtypeLanguage.get()) {
config.setLocale(subtype.primaryLocale.base)
}
resourcesContext = createConfigurationContext(config)
}
prefs.localization.displayKeyboardLabelsInSubtypeLanguage.observeForever { shouldSync ->
val config = Configuration(resources.configuration)
if (shouldSync) {
config.setLocale(subtypeManager.activeSubtype.primaryLocale.base)
}
resourcesContext = createConfigurationContext(config)
}
@Suppress("DEPRECATION") // We do not retrieve the wallpaper but only listen to changes
registerReceiver(wallpaperChangeReceiver, IntentFilter(Intent.ACTION_WALLPAPER_CHANGED))
}
override fun onCreateInputView(): View {
@@ -318,6 +331,7 @@ class FlorisImeService : LifecycleInputMethodService() {
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(wallpaperChangeReceiver)
FlorisImeServiceReference = WeakReference(null)
inputWindowView = null
}
@@ -448,6 +462,10 @@ class FlorisImeService : LifecycleInputMethodService() {
flogInfo(LogTopic.IMS_EVENTS) { "Creating inline suggestions request" }
val stylesBundle = themeManager.createInlineSuggestionUiStyleBundle(this)
if (stylesBundle == null) {
flogWarning(LogTopic.IMS_EVENTS) { "Failed to retrieve inline suggestions style bundle" }
return null
}
val spec = InlinePresentationSpec.Builder(
InlineSuggestionUiSmallestSize,
InlineSuggestionUiBiggestSize,
@@ -495,7 +513,9 @@ class FlorisImeService : LifecycleInputMethodService() {
outInsets.visibleTopInsets = visibleTopY
outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_REGION
val left = 0
val top = if (keyboardManager.activeState.isBottomSheetShowing()) { 0 } else {
val top = if (keyboardManager.activeState.isBottomSheetShowing() || keyboardManager.activeState.isSubtypeSelectionShowing()) {
0
} else {
visibleTopY - if (needAdditionalOverlay) FlorisImeSizing.Static.smartbarHeightPx else 0
}
val right = inputViewSize.width
@@ -508,7 +528,8 @@ class FlorisImeService : LifecycleInputMethodService() {
*/
private fun updateSoftInputWindowLayoutParameters() {
val w = window?.window ?: return
WindowCompat.setDecorFitsSystemWindows(w, true)
// TODO: Verify that this doesn't give us a padding problem
WindowCompat.setDecorFitsSystemWindows(w, false)
ViewUtils.updateLayoutHeightOf(w, WindowManager.LayoutParams.MATCH_PARENT)
val layoutHeight = if (isFullscreenUiMode) {
WindowManager.LayoutParams.WRAP_CONTENT
@@ -545,19 +566,18 @@ class FlorisImeService : LifecycleInputMethodService() {
ProvideKeyboardRowBaseHeight {
CompositionLocalProvider(LocalInputFeedbackController provides inputFeedbackController) {
FlorisImeTheme {
// Do not apply system bar padding here yet, we want to draw it ourselves
Column(modifier = Modifier.fillMaxWidth()) {
if (!(isFullscreenUiMode && isExtractUiShown)) {
Box(
DevtoolsOverlay(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
) {
DevtoolsOverlay(modifier = Modifier.fillMaxSize())
}
)
}
ImeUi()
SystemUiIme()
}
SystemUiIme()
}
}
}
@@ -568,25 +588,22 @@ class FlorisImeService : LifecycleInputMethodService() {
@Composable
private fun ImeUi() {
val state by keyboardManager.activeState.collectAsState()
val keyboardStyle = FlorisImeTheme.style.get(
element = FlorisImeUi.Keyboard,
mode = state.inputShiftState.value,
)
val layoutDirection = LocalLayoutDirection.current
SideEffect {
if (keyboardManager.activeState.layoutDirection != layoutDirection) {
keyboardManager.activeState.layoutDirection = layoutDirection
}
LaunchedEffect(layoutDirection) {
keyboardManager.activeState.layoutDirection = layoutDirection
}
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
SnyggSurface(
SnyggBox(
elementName = FlorisImeUi.Window.elementName,
attributes = mapOf(FlorisImeUi.Attr.ShiftState to state.inputShiftState.attrName()),
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.onGloballyPositioned { coords -> inputViewSize = coords.size }
.onGloballyPositioned { coords -> inputViewSize = coords.size },
clickAndSemanticsModifier = Modifier
// Do not remove below line or touch input may get stuck
.pointerInteropFilter { false },
style = keyboardStyle,
supportsBackgroundImage = true,
) {
val configuration = LocalConfiguration.current
val bottomOffset by if (configuration.isOrientationPortrait()) {
@@ -598,18 +615,18 @@ class FlorisImeService : LifecycleInputMethodService() {
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
// Apply system bars padding here (we already drew our keyboard background)
.safeDrawingPadding()
// FIXME: removing this fixes the Smartbar sizing but breaks one-handed-mode
//.height(IntrinsicSize.Min)
.padding(bottom = bottomOffset),
) {
val oneHandedMode by prefs.keyboard.oneHandedMode.observeAsState()
val oneHandedModeEnabled by prefs.keyboard.oneHandedModeEnabled.observeAsState()
val oneHandedModeScaleFactor by prefs.keyboard.oneHandedModeScaleFactor.observeAsState()
val keyboardWeight = when {
oneHandedMode == OneHandedMode.OFF || configuration.isOrientationLandscape() -> 1f
!oneHandedModeEnabled || configuration.isOrientationLandscape() -> 1f
else -> oneHandedModeScaleFactor / 100f
}
if (oneHandedMode == OneHandedMode.END && configuration.isOrientationPortrait()) {
if (oneHandedModeEnabled && oneHandedMode == OneHandedMode.END && configuration.isOrientationPortrait()) {
OneHandedPanel(
panelSide = OneHandedMode.START,
weight = 1f - keyboardWeight,
@@ -628,7 +645,7 @@ class FlorisImeService : LifecycleInputMethodService() {
}
}
}
if (oneHandedMode == OneHandedMode.START && configuration.isOrientationPortrait()) {
if (oneHandedModeEnabled && oneHandedMode == OneHandedMode.START && configuration.isOrientationPortrait()) {
OneHandedPanel(
panelSide = OneHandedMode.END,
weight = 1f - keyboardWeight,
@@ -680,12 +697,22 @@ class FlorisImeService : LifecycleInputMethodService() {
ProvideLocalizedResources(resourcesContext, forceLayoutDirection = LayoutDirection.Ltr) {
FlorisImeTheme {
BottomSheetHostUi(
isShowing = state.isBottomSheetShowing(),
isShowing = state.isBottomSheetShowing() || state.isSubtypeSelectionShowing(),
onHide = {
keyboardManager.activeState.isActionsEditorVisible = false
if (state.isBottomSheetShowing()) {
keyboardManager.activeState.isActionsEditorVisible = false
}
if (state.isSubtypeSelectionShowing()) {
keyboardManager.activeState.isSubtypeSelectionVisible = false
}
},
) {
QuickActionsEditorPanel()
if (state.isBottomSheetShowing()) {
QuickActionsEditorPanel()
}
if (state.isSubtypeSelectionShowing()) {
SelectSubtypePanel()
}
}
}
}
@@ -719,44 +746,38 @@ class FlorisImeService : LifecycleInputMethodService() {
@Composable
fun Content() {
val context = LocalContext.current
ProvideLocalizedResources(resourcesContext, forceLayoutDirection = LayoutDirection.Ltr) {
FlorisImeTheme {
val layoutStyle = FlorisImeTheme.style.get(FlorisImeUi.ExtractedLandscapeInputLayout)
val fieldStyle = FlorisImeTheme.style.get(FlorisImeUi.ExtractedLandscapeInputField)
val actionStyle = FlorisImeTheme.style.get(FlorisImeUi.ExtractedLandscapeInputAction)
val activeEditorInfo by editorInstance.activeInfoFlow.collectAsState()
Box(
modifier = Modifier
.snyggBackground(context, layoutStyle, FlorisImeTheme.fallbackSurfaceColor()),
) {
Row(
SnyggBox(FlorisImeUi.ExtractedLandscapeInputLayout.elementName) {
SnyggRow(
modifier = Modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically,
) {
val fieldColor = fieldStyle.foreground.solidColor(context, FlorisImeTheme.fallbackContentColor())
AndroidView(
SnyggBox(FlorisImeUi.ExtractedLandscapeInputLayout.elementName,
modifier = Modifier
.padding(8.dp)
.fillMaxHeight()
.weight(1f)
.snyggShadow(fieldStyle)
.snyggBorder(context, fieldStyle)
.snyggBackground(context, fieldStyle),
factory = { extractEditText },
update = { view ->
view.background = null
view.backgroundTintList = null
view.foregroundTintList = null
view.setTextColor(fieldColor.toArgb())
view.setHintTextColor(fieldColor.copy(fieldColor.alpha * 0.6f).toArgb())
view.setTextSize(
TypedValue.COMPLEX_UNIT_SP,
fieldStyle.fontSize.spSize(default = 16.sp).value,
)
},
)
FlorisButton(
.weight(1f),
) {
val fieldStyle = rememberSnyggThemeQuery(FlorisImeUi.ExtractedLandscapeInputField.elementName)
val foreground = fieldStyle.foreground()
AndroidView(
factory = { extractEditText },
update = { view ->
view.background = null
view.backgroundTintList = null
view.foregroundTintList = null
view.setTextColor(foreground.toArgb())
view.setHintTextColor(foreground.copy(foreground.alpha * 0.6f).toArgb())
view.setTextSize(
TypedValue.COMPLEX_UNIT_SP,
fieldStyle.fontSize(default = 16.sp).value,
)
},
)
}
SnyggButton(
FlorisImeUi.ExtractedLandscapeInputAction.elementName,
onClick = {
if (activeEditorInfo.extractedActionId != 0) {
currentInputConnection?.performEditorAction(activeEditorInfo.extractedActionId)
@@ -765,15 +786,13 @@ class FlorisImeService : LifecycleInputMethodService() {
}
},
modifier = Modifier.padding(horizontal = 8.dp),
text = activeEditorInfo.extractedActionLabel
?: getTextForImeAction(activeEditorInfo.imeOptions.action.toInt())
?: "ACTION",
shape = actionStyle.shape.shape(),
colors = ButtonDefaults.buttonColors(
containerColor = actionStyle.background.solidColor(context, FlorisImeTheme.fallbackContentColor()),
contentColor = actionStyle.foreground.solidColor(context, FlorisImeTheme.fallbackSurfaceColor()),
),
)
) {
SnyggText(
text = activeEditorInfo.extractedActionLabel
?: getTextForImeAction(activeEditorInfo.imeOptions.action.toInt())
?: "ACTION",
)
}
}
}
}

View File

@@ -18,9 +18,10 @@ package dev.patrickgold.florisboard.app
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import dev.patrickgold.florisboard.app.settings.theme.DisplayColorsAs
import dev.patrickgold.florisboard.app.settings.theme.DisplayKbdAfterDialogs
import dev.patrickgold.florisboard.app.settings.theme.SnyggLevel
import dev.patrickgold.florisboard.app.setup.NotificationPermissionState
import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn
import dev.patrickgold.florisboard.ime.core.Subtype
@@ -40,58 +41,34 @@ import dev.patrickgold.florisboard.ime.smartbar.CandidatesDisplayMode
import dev.patrickgold.florisboard.ime.smartbar.ExtendedActionsPlacement
import dev.patrickgold.florisboard.ime.smartbar.IncognitoDisplayMode
import dev.patrickgold.florisboard.ime.smartbar.SmartbarLayout
import dev.patrickgold.florisboard.ime.smartbar.quickaction.QuickAction
import dev.patrickgold.florisboard.ime.smartbar.quickaction.QuickActionArrangement
import dev.patrickgold.florisboard.ime.smartbar.quickaction.QuickActionJsonConfig
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyHintConfiguration
import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
import dev.patrickgold.florisboard.ime.text.key.UtilityKeyAction
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
import dev.patrickgold.florisboard.ime.theme.ThemeMode
import dev.patrickgold.florisboard.ime.theme.extCoreTheme
import dev.patrickgold.florisboard.lib.compose.ColorPreferenceSerializer
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
import dev.patrickgold.florisboard.lib.observeAsTransformingState
import dev.patrickgold.florisboard.lib.util.VersionName
import dev.patrickgold.jetpref.datastore.JetPref
import dev.patrickgold.jetpref.datastore.model.PreferenceMigrationEntry
import dev.patrickgold.jetpref.datastore.model.PreferenceModel
import dev.patrickgold.jetpref.datastore.model.PreferenceType
import dev.patrickgold.jetpref.datastore.model.observeAsState
import kotlinx.serialization.encodeToString
import dev.patrickgold.jetpref.material.ui.ColorRepresentation
import kotlinx.serialization.json.Json
import org.florisboard.lib.android.AndroidVersion
import org.florisboard.lib.android.isOrientationPortrait
import org.florisboard.lib.snygg.SnyggLevel
import org.florisboard.lib.color.DEFAULT_GREEN
fun florisPreferenceModel() = JetPref.getOrCreatePreferenceModel(AppPrefs::class, ::AppPrefs)
class AppPrefs : PreferenceModel("florisboard-app-prefs") {
val advanced = Advanced()
inner class Advanced {
val settingsTheme = enum(
key = "advanced__settings_theme",
default = AppTheme.AUTO,
)
val useMaterialYou = boolean(
key = "advanced__use_material_you",
default = true,
)
val settingsLanguage = string(
key = "advanced__settings_language",
default = "auto",
)
val showAppIcon = boolean(
key = "advanced__show_app_icon",
default = true,
)
val incognitoMode = enum(
key = "advanced__incognito_mode",
default = IncognitoMode.DYNAMIC_ON_OFF,
)
// Internal pref
val forceIncognitoModeFromDynamic = boolean(
key = "advanced__force_incognito_mode_from_dynamic",
default = false,
)
}
val clipboard = Clipboard()
inner class Clipboard {
val useInternalClipboard = boolean(
@@ -138,6 +115,14 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
key = "clipboard__clear_primary_clip_deletes_last_item",
default = true,
)
val suggestionEnabled = boolean(
key = "clipboard__suggestion_enabled",
default = true,
)
val suggestionTimeout = int(
key = "clipboard__suggestion_timeout",
default = 60,
)
}
val correction = Correction()
@@ -166,10 +151,6 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
key = "devtools__enabled",
default = false,
)
val showLastLayoutComputation = boolean(
key = "devtools__show_last_layout_computation",
default = false,
)
val showPrimaryClip = boolean(
key = "devtools__show_primary_clip",
default = false,
@@ -502,7 +483,11 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
)
val oneHandedMode = enum(
key = "keyboard__one_handed_mode",
default = OneHandedMode.OFF,
default = OneHandedMode.END,
)
val oneHandedModeEnabled = boolean(
key = "keyboard__one_handed_mode_enabled",
default = false,
)
val oneHandedModeScaleFactor = int(
key = "keyboard__one_handed_mode_scale_factor",
@@ -574,14 +559,14 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
@Composable
fun fontSizeMultiplier(): Float {
val configuration = LocalConfiguration.current
val oneHandedMode by oneHandedMode.observeAsState()
val oneHandedModeEnabled by oneHandedModeEnabled.observeAsState()
val oneHandedModeFactor by oneHandedModeScaleFactor.observeAsTransformingState { it / 100.0f }
val fontSizeMultiplierBase by if (configuration.isOrientationPortrait()) {
fontSizeMultiplierPortrait
} else {
fontSizeMultiplierLandscape
}.observeAsTransformingState { it / 100.0f }
val fontSizeMultiplier = fontSizeMultiplierBase * if (oneHandedMode != OneHandedMode.OFF && configuration.isOrientationPortrait()) {
val fontSizeMultiplier = fontSizeMultiplierBase * if (oneHandedModeEnabled && configuration.isOrientationPortrait()) {
oneHandedModeFactor
} else {
1.0f
@@ -596,6 +581,10 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
key = "localization__display_language_names_in",
default = DisplayLanguageNamesIn.SYSTEM_LOCALE,
)
val displayKeyboardLabelsInSubtypeLanguage = boolean(
key = "localization__display_keyboard_labels_in_subtype_language",
default = false,
)
val activeSubtypeId = long(
key = "localization__active_subtype_id",
default = Subtype.DEFAULT.id,
@@ -606,6 +595,30 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
)
}
val other = Other()
inner class Other {
val settingsTheme = enum(
key = "other__settings_theme",
default = AppTheme.AUTO,
)
val accentColor = custom(
key = "other__accent_color",
default = when (AndroidVersion.ATLEAST_API31_S) {
true -> Color.Unspecified
false -> DEFAULT_GREEN
},
serializer = ColorPreferenceSerializer,
)
val settingsLanguage = string(
key = "other__settings_language",
default = "auto",
)
val showAppIcon = boolean(
key = "other__show_app_icon",
default = true,
)
}
val smartbar = Smartbar()
inner class Smartbar {
val enabled = boolean(
@@ -682,13 +695,14 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
key = "suggestion__block_possibly_offensive",
default = true,
)
val clipboardContentEnabled = boolean(
key = "suggestion__clipboard_content_enabled",
default = true,
val incognitoMode = enum(
key = "suggestion__incognito_mode",
default = IncognitoMode.DYNAMIC_ON_OFF,
)
val clipboardContentTimeout = int(
key = "suggestion__clipboard_content_timeout",
default = 60,
// Internal pref
val forceIncognitoModeFromDynamic = boolean(
key = "suggestion__force_incognito_mode_from_dynamic",
default = false,
)
}
@@ -708,6 +722,14 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
default = extCoreTheme("floris_night"),
serializer = ExtensionComponentName.Serializer,
)
val accentColor = custom(
key = "theme__accent_color",
default = when (AndroidVersion.ATLEAST_API31_S) {
true -> Color.Unspecified
false -> DEFAULT_GREEN
},
serializer = ColorPreferenceSerializer,
)
//val sunriseTime = localTime(
// key = "theme__sunrise_time",
// default = LocalTime.of(6, 0),
@@ -716,9 +738,9 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
// key = "theme__sunset_time",
// default = LocalTime.of(18, 0),
//)
val editorDisplayColorsAs = enum(
key = "theme__editor_display_colors_as",
default = DisplayColorsAs.HEX8,
val editorColorRepresentation = enum(
key = "theme__editor_color_representation",
default = ColorRepresentation.HEX,
)
val editorDisplayKbdAfterDialogs = enum(
key = "theme__editor_display_kbd_after_dialogs",
@@ -732,34 +754,6 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
override fun migrate(entry: PreferenceMigrationEntry): PreferenceMigrationEntry {
return when (entry.key) {
// Migrate enums from their lowercase to uppercase representation
// Keep migration rule until: 0.5 dev cycle
"advanced__settings_theme", "gestures__swipe_up", "gestures__swipe_down", "gestures__swipe_left",
"gestures__swipe_right", "gestures__space_bar_swipe_up", "gestures__space_bar_swipe_left",
"gestures__space_bar_swipe_right", "gestures__space_bar_long_press", "gestures__delete_key_swipe_left",
"gestures__delete_key_long_press", "keyboard__hinted_number_row_mode", "keyboard__hinted_symbols_mode",
"keyboard__utility_key_action", "keyboard__one_handed_mode", "keyboard__landscape_input_ui_mode",
"localization__display_language_names_in", "smartbar__primary_actions_row_type",
"smartbar__secondary_actions_placement", "smartbar__secondary_actions_row_type", "spelling__language_mode",
"suggestion__display_mode", "theme__mode", "theme__editor_display_colors_as",
"theme__editor_display_kbd_after_dialogs", "theme__editor_level",
-> {
entry.transform(rawValue = entry.rawValue.uppercase())
}
// Migrate old private mode force flag as this is a sensitive preference
// Keep migration rule until: 0.5 dev cycle
"advanced__force_private_mode" -> {
if (entry.rawValue.toBoolean()) {
entry.transform(
type = PreferenceType.string(),
key = "advanced__incognito_mode",
rawValue = IncognitoMode.FORCE_ON.toString(),
)
} else {
entry.reset()
}
}
// Migrate media prefs to emoji prefs
// Keep migration rule until: 0.6 dev cycle
@@ -774,19 +768,80 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
"media__emoji_recently_used_max_size" -> {
entry.transform(key = "emoji__history_recent_max_size")
}
"media__emoji_preferred_skin_tone" -> {
// Migrate advanced prefs to other prefs
// Keep migration rules until: 0.7 dev cycle
"advanced__settings_theme" -> {
entry.transform(key = "other__settings_theme")
}
"advanced__accent_color" -> {
entry.transform(key = "other__accent_color")
}
"advanced__settings_language" -> {
entry.transform(key = "other__settings_language")
}
"advanced__show_app_icon" -> {
entry.transform(key = "other__show_app_icon")
}
"advanced__incognito_mode" -> {
entry.transform(key = "suggestion__incognito_mode")
}
"advanced__force_incognito_mode_from_dynamic" -> {
entry.transform(key = "suggestion__force_incognito_mode_from_dynamic")
}
// Migrate clipboard suggestion prefs to clipboard
// Keep migration rules until: 0.7 dev cycle
"suggestion__clipboard_content_enabled" -> {
entry.transform(key = "clipboard__suggestion_enabled")
}
"suggestion__clipboard_content_timeout" -> {
entry.transform(key = "clipboard__suggestion_timeout")
}
//Migrate one hand mode prefs keep until: 0.7 dev cycle
"keyboard__one_handed_mode" -> {
if (entry.rawValue != "OFF") {
val prefs by florisPreferenceModel()
prefs.keyboard.oneHandedModeEnabled.set(true)
entry.keepAsIs()
} else {
entry.reset()
}
}
"smartbar__action_arrangement" -> {
val arrangement = QuickActionJsonConfig.decodeFromString<QuickActionArrangement>(entry.rawValue)
var newArrangement = arrangement.copy(
dynamicActions = arrangement.dynamicActions.map { action ->
if (action is QuickAction.InsertKey && action.data.code == KeyCode.COMPACT_LAYOUT_TO_RIGHT) {
action.copy(TextKeyData.TOGGLE_COMPACT_LAYOUT)
} else {
action
}
}
)
if (QuickAction.InsertKey(TextKeyData.LANGUAGE_SWITCH) !in newArrangement) {
newArrangement = newArrangement.copy(
dynamicActions = newArrangement.dynamicActions.plus(QuickAction.InsertKey(TextKeyData.LANGUAGE_SWITCH))
)
}
val json = QuickActionJsonConfig.encodeToString(newArrangement)
entry.transform(rawValue = json)
}
// Migrate theme editor fine-tuning
// Keep migration rule until: 0.6 dev cycle
"theme__editor_display_colors_as" -> {
val colorRepresentation = when (entry.rawValue) {
"RGBA" -> ColorRepresentation.RGB
else -> ColorRepresentation.HEX
}
entry.transform(
key = "emoji__preferred_skin_tone",
rawValue = entry.rawValue.uppercase(), // keep until: 0.5 dev cycle
)
}
"media__emoji_preferred_hair_style" -> {
entry.transform(
key = "emoji__preferred_hair_style",
rawValue = entry.rawValue.uppercase(), // keep until: 0.5 dev cycle
key = "theme__editor_color_representation",
rawValue = colorRepresentation.name,
)
}
// Default: keep entry
else -> entry.keepAsIs()
}

View File

@@ -18,8 +18,8 @@ package dev.patrickgold.florisboard.app
import androidx.compose.runtime.Composable
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.settings.theme.DisplayColorsAs
import dev.patrickgold.florisboard.app.settings.theme.DisplayKbdAfterDialogs
import dev.patrickgold.florisboard.app.settings.theme.SnyggLevel
import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn
import dev.patrickgold.florisboard.ime.input.CapitalizationBehavior
import dev.patrickgold.florisboard.ime.input.HapticVibrationMode
@@ -41,9 +41,9 @@ import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
import dev.patrickgold.florisboard.ime.text.key.UtilityKeyAction
import dev.patrickgold.florisboard.ime.theme.ThemeMode
import dev.patrickgold.florisboard.lib.compose.stringRes
import org.florisboard.lib.snygg.SnyggLevel
import dev.patrickgold.jetpref.datastore.ui.ListPreferenceEntry
import dev.patrickgold.jetpref.datastore.ui.listPrefEntries
import dev.patrickgold.jetpref.material.ui.ColorRepresentation
import org.florisboard.lib.kotlin.curlyFormat
import kotlin.reflect.KClass
@@ -58,19 +58,19 @@ private val ENUM_DISPLAY_ENTRIES = mapOf<Pair<KClass<*>, String>, @Composable ()
)
entry(
key = AppTheme.AUTO_AMOLED,
label = stringRes(R.string.pref__advanced__settings_theme__auto_amoled),
label = stringRes(R.string.pref__other__settings_theme__auto_amoled),
)
entry(
key = AppTheme.LIGHT,
label = stringRes(R.string.pref__advanced__settings_theme__light),
label = stringRes(R.string.pref__other__settings_theme__light),
)
entry(
key = AppTheme.DARK,
label = stringRes(R.string.pref__advanced__settings_theme__dark),
label = stringRes(R.string.pref__other__settings_theme__dark),
)
entry(
key = AppTheme.AMOLED_DARK,
label = stringRes(R.string.pref__advanced__settings_theme__amoled_dark),
label = stringRes(R.string.pref__other__settings_theme__amoled_dark),
)
}
},
@@ -102,18 +102,24 @@ private val ENUM_DISPLAY_ENTRIES = mapOf<Pair<KClass<*>, String>, @Composable ()
)
}
},
DisplayColorsAs::class to DEFAULT to {
ColorRepresentation::class to DEFAULT to {
listPrefEntries {
entry(
key = DisplayColorsAs.HEX8,
label = stringRes(R.string.enum__display_colors_as__hex8),
key = ColorRepresentation.HEX,
label = stringRes(R.string.enum__color_representation__hex),
description = stringRes(R.string.general__example_given).curlyFormat("example" to "#4caf50ff"),
showDescriptionOnlyIfSelected = true,
)
entry(
key = DisplayColorsAs.RGBA,
label = stringRes(R.string.enum__display_colors_as__rgba),
description = stringRes(R.string.general__example_given).curlyFormat("example" to "rgba(76,175,80,1.0)"),
key = ColorRepresentation.RGB,
label = stringRes(R.string.enum__color_representation__rgb),
description = stringRes(R.string.general__example_given).curlyFormat("example" to "rgba(76, 175, 80, 1.0)"),
showDescriptionOnlyIfSelected = true,
)
entry(
key = ColorRepresentation.HSV,
label = stringRes(R.string.enum__color_representation__hsv),
description = stringRes(R.string.general__example_given).curlyFormat("example" to "hsva(122, 56, 68, 1.0)"),
showDescriptionOnlyIfSelected = true,
)
}
@@ -376,10 +382,6 @@ private val ENUM_DISPLAY_ENTRIES = mapOf<Pair<KClass<*>, String>, @Composable ()
},
OneHandedMode::class to DEFAULT to {
listPrefEntries {
entry(
key = OneHandedMode.OFF,
label = stringRes(R.string.enum__one_handed_mode__off),
)
entry(
key = OneHandedMode.START,
label = stringRes(R.string.enum__one_handed_mode__start),
@@ -542,6 +544,10 @@ private val ENUM_DISPLAY_ENTRIES = mapOf<Pair<KClass<*>, String>, @Composable ()
key = SwipeAction.SHOW_INPUT_METHOD_PICKER,
label = stringRes(R.string.enum__swipe_action__show_input_method_picker),
)
entry(
key = SwipeAction.SHOW_SUBTYPE_PICKER,
label = "Show subtype picker"
)
entry(
key = SwipeAction.SWITCH_TO_PREV_SUBTYPE,
label = stringRes(R.string.enum__swipe_action__switch_to_prev_subtype),

View File

@@ -88,17 +88,17 @@ class FlorisAppActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
prefs.advanced.settingsTheme.observe(this) {
prefs.other.settingsTheme.observe(this) {
appTheme = it
}
prefs.advanced.settingsLanguage.observe(this) {
prefs.other.settingsLanguage.observe(this) {
val config = Configuration(resources.configuration)
val locale = if (it == "auto") FlorisLocale.default() else FlorisLocale.fromTag(it)
config.setLocale(locale.base)
resourcesContext = createConfigurationContext(config)
}
if (AndroidVersion.ATMOST_API28_P) {
prefs.advanced.showAppIcon.observe(this) {
prefs.other.showAppIcon.observe(this) {
showAppIcon = it
}
}
@@ -117,8 +117,7 @@ class FlorisAppActivity : ComponentActivity() {
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
setContent {
ProvideLocalizedResources(resourcesContext) {
val useMaterialYou by prefs.advanced.useMaterialYou.observeAsState()
FlorisAppTheme(theme = appTheme, isMaterialYouAware = useMaterialYou) {
FlorisAppTheme(theme = appTheme) {
Surface(color = MaterialTheme.colorScheme.background) {
AppContent()
}

View File

@@ -47,7 +47,7 @@ import dev.patrickgold.florisboard.app.settings.HomeScreen
import dev.patrickgold.florisboard.app.settings.about.AboutScreen
import dev.patrickgold.florisboard.app.settings.about.ProjectLicenseScreen
import dev.patrickgold.florisboard.app.settings.about.ThirdPartyLicensesScreen
import dev.patrickgold.florisboard.app.settings.advanced.AdvancedScreen
import dev.patrickgold.florisboard.app.settings.advanced.OtherScreen
import dev.patrickgold.florisboard.app.settings.advanced.BackupScreen
import dev.patrickgold.florisboard.app.settings.advanced.RestoreScreen
import dev.patrickgold.florisboard.app.settings.clipboard.ClipboardScreen
@@ -110,9 +110,9 @@ object Routes {
const val Media = "settings/media"
const val Advanced = "settings/advanced"
const val Backup = "settings/advanced/backup"
const val Restore = "settings/advanced/restore"
const val Other = "settings/other"
const val Backup = "settings/other/backup"
const val Restore = "settings/other/restore"
const val About = "settings/about"
const val ProjectLicense = "settings/about/project-license"
@@ -239,7 +239,7 @@ object Routes {
composableWithDeepLink(Settings.Media) { MediaScreen() }
composableWithDeepLink(Settings.Advanced) { AdvancedScreen() }
composableWithDeepLink(Settings.Other) { OtherScreen() }
composableWithDeepLink(Settings.Backup) { BackupScreen() }
composableWithDeepLink(Settings.Restore) { RestoreScreen() }

View File

@@ -17,19 +17,22 @@
package dev.patrickgold.florisboard.app.apptheme
import android.app.Activity
import android.content.Context
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
import dev.patrickgold.florisboard.app.AppTheme
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.jetpref.datastore.model.observeAsState
import org.florisboard.lib.android.AndroidVersion
import org.florisboard.lib.color.ColorMappings
/*private val AmoledDarkColorPalette = darkColorScheme(
primary = Green500,
@@ -70,170 +73,66 @@ private val LightColorPalette = lightColorScheme(
*/
)*/
private val lightScheme = lightColorScheme(
primary = primaryLight,
onPrimary = onPrimaryLight,
primaryContainer = primaryContainerLight,
onPrimaryContainer = onPrimaryContainerLight,
secondary = secondaryLight,
onSecondary = onSecondaryLight,
secondaryContainer = secondaryContainerLight,
onSecondaryContainer = onSecondaryContainerLight,
tertiary = tertiaryLight,
onTertiary = onTertiaryLight,
tertiaryContainer = tertiaryContainerLight,
onTertiaryContainer = onTertiaryContainerLight,
error = errorLight,
onError = onErrorLight,
errorContainer = errorContainerLight,
onErrorContainer = onErrorContainerLight,
background = backgroundLight,
onBackground = onBackgroundLight,
surface = surfaceLight,
onSurface = onSurfaceLight,
surfaceVariant = surfaceVariantLight,
onSurfaceVariant = onSurfaceVariantLight,
outline = outlineLight,
outlineVariant = outlineVariantLight,
scrim = scrimLight,
inverseSurface = inverseSurfaceLight,
inverseOnSurface = inverseOnSurfaceLight,
inversePrimary = inversePrimaryLight,
surfaceDim = surfaceDimLight,
surfaceBright = surfaceBrightLight,
surfaceContainerLowest = surfaceContainerLowestLight,
surfaceContainerLow = surfaceContainerLowLight,
surfaceContainer = surfaceContainerLight,
surfaceContainerHigh = surfaceContainerHighLight,
surfaceContainerHighest = surfaceContainerHighestLight,
)
private val darkScheme = darkColorScheme(
primary = primaryDark,
onPrimary = onPrimaryDark,
primaryContainer = primaryContainerDark,
onPrimaryContainer = onPrimaryContainerDark,
secondary = secondaryDark,
onSecondary = onSecondaryDark,
secondaryContainer = secondaryContainerDark,
onSecondaryContainer = onSecondaryContainerDark,
tertiary = tertiaryDark,
onTertiary = onTertiaryDark,
tertiaryContainer = tertiaryContainerDark,
onTertiaryContainer = onTertiaryContainerDark,
error = errorDark,
onError = onErrorDark,
errorContainer = errorContainerDark,
onErrorContainer = onErrorContainerDark,
background = backgroundDark,
onBackground = onBackgroundDark,
surface = surfaceDark,
onSurface = onSurfaceDark,
surfaceVariant = surfaceVariantDark,
onSurfaceVariant = onSurfaceVariantDark,
outline = outlineDark,
outlineVariant = outlineVariantDark,
scrim = scrimDark,
inverseSurface = inverseSurfaceDark,
inverseOnSurface = inverseOnSurfaceDark,
inversePrimary = inversePrimaryDark,
surfaceDim = surfaceDimDark,
surfaceBright = surfaceBrightDark,
surfaceContainerLowest = surfaceContainerLowestDark,
surfaceContainerLow = surfaceContainerLowDark,
surfaceContainer = surfaceContainerDark,
surfaceContainerHigh = surfaceContainerHighDark,
surfaceContainerHighest = surfaceContainerHighestDark,
)
@Composable
fun getColorScheme(
context: Context,
theme: AppTheme,
): ColorScheme {
val prefs by florisPreferenceModel()
val accentColor by prefs.other.accentColor.observeAsState()
val isDark = isSystemInDarkTheme()
private val amoledScheme = darkScheme.copy(
background = amoledDark,
surface = amoledDark
)
return when (theme) {
AppTheme.AUTO -> {
if (isDark) {
ColorMappings.dynamicDarkColorScheme(context, accentColor)
} else {
ColorMappings.dynamicLightColorScheme(context, accentColor)
}
}
AppTheme.DARK -> {
ColorMappings.dynamicDarkColorScheme(context, accentColor)
}
AppTheme.LIGHT -> {
ColorMappings.dynamicLightColorScheme(context, accentColor)
}
AppTheme.AMOLED_DARK -> {
ColorMappings.dynamicDarkColorScheme(context, accentColor).amoled()
}
AppTheme.AUTO_AMOLED -> {
if (isDark) {
ColorMappings.dynamicDarkColorScheme(context, accentColor).amoled()
} else {
ColorMappings.dynamicLightColorScheme(context, accentColor)
}
}
}
}
fun ColorScheme.amoled(): ColorScheme {
return this.copy(background = Color.Black, surface = Color.Black)
}
@Composable
fun FlorisAppTheme(
theme: AppTheme,
isMaterialYouAware: Boolean,
content: @Composable () -> Unit
content: @Composable () -> Unit,
) {
val colors = if (AndroidVersion.ATLEAST_API31_S) {
when (theme) {
AppTheme.AUTO -> when {
isMaterialYouAware -> when {
isSystemInDarkTheme() -> dynamicDarkColorScheme(LocalContext.current)
else -> dynamicLightColorScheme(LocalContext.current)
}
else -> {
when {
isSystemInDarkTheme() -> darkScheme
else -> lightScheme
}
}
}
AppTheme.AUTO_AMOLED -> when {
isMaterialYouAware -> when {
isSystemInDarkTheme() -> dynamicDarkColorScheme(LocalContext.current).copy(
background = amoledDark,
surface = amoledDark,
)
else -> dynamicLightColorScheme(LocalContext.current)
}
else -> {
when {
isSystemInDarkTheme() -> amoledScheme
else -> lightScheme
}
}
}
AppTheme.LIGHT -> when {
isMaterialYouAware -> dynamicLightColorScheme(LocalContext.current)
else -> lightScheme
}
AppTheme.DARK -> when {
isMaterialYouAware -> dynamicDarkColorScheme(LocalContext.current)
else -> darkScheme
}
AppTheme.AMOLED_DARK -> when {
isMaterialYouAware -> dynamicDarkColorScheme(LocalContext.current).copy(
background = amoledDark,
surface = amoledDark,
)
else -> amoledScheme
}
}
} else {
when (theme) {
AppTheme.AUTO -> when {
isSystemInDarkTheme() -> darkScheme
else -> lightScheme
}
AppTheme.AUTO_AMOLED -> when {
isSystemInDarkTheme() -> darkScheme
else -> lightScheme
}
AppTheme.LIGHT -> lightScheme
AppTheme.DARK -> darkScheme
AppTheme.AMOLED_DARK -> amoledScheme
}
}
val colors = getColorScheme(
context = LocalContext.current,
theme = theme,
)
val darkTheme =
theme == AppTheme.DARK
|| theme == AppTheme.AMOLED_DARK
|| (theme == AppTheme.AUTO && isSystemInDarkTheme())
|| (theme == AppTheme.AUTO_AMOLED && isSystemInDarkTheme())
|| theme == AppTheme.AMOLED_DARK
|| (theme == AppTheme.AUTO && isSystemInDarkTheme())
|| (theme == AppTheme.AUTO_AMOLED && isSystemInDarkTheme())
val view = LocalView.current
if (!view.isInEditMode) {

View File

@@ -29,6 +29,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -43,13 +44,17 @@ import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.clipboardManager
import dev.patrickgold.florisboard.editorInstance
import dev.patrickgold.florisboard.ime.keyboard.CachedLayout
import dev.patrickgold.florisboard.ime.keyboard.DebugLayoutComputationResult
import dev.patrickgold.florisboard.ime.nlp.NlpInlineAutofill
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import dev.patrickgold.florisboard.keyboardManager
import dev.patrickgold.florisboard.lib.FlorisLocale
import dev.patrickgold.florisboard.lib.observeAsNonNullState
import dev.patrickgold.florisboard.nlpManager
import dev.patrickgold.florisboard.themeManager
import dev.patrickgold.jetpref.datastore.model.observeAsState
import org.florisboard.lib.android.AndroidVersion
import org.florisboard.lib.snygg.SnyggMissingSchemaException
import java.text.SimpleDateFormat
import java.util.*
@@ -61,15 +66,16 @@ fun DevtoolsOverlay(modifier: Modifier = Modifier) {
val context = LocalContext.current
val prefs by florisPreferenceModel()
val keyboardManager by context.keyboardManager()
val themeManager by context.themeManager()
val devtoolsEnabled by prefs.devtools.enabled.observeAsState()
val showPrimaryClip by prefs.devtools.showPrimaryClip.observeAsState()
val showInputStateOverlay by prefs.devtools.showInputStateOverlay.observeAsState()
val showLastLayoutComputation by prefs.devtools.showLastLayoutComputation.observeAsState()
val showSpellingOverlay by prefs.devtools.showSpellingOverlay.observeAsState()
val showInlineAutofillOverlay by prefs.devtools.showInlineAutofillOverlay.observeAsState()
val debugLayoutResult by keyboardManager.layoutManager.debugLayoutComputationResultFlow.collectAsState()
val themeInfo by themeManager.activeThemeInfo.observeAsState()
CompositionLocalProvider(
LocalContentColor provides Color.White,
@@ -82,8 +88,8 @@ fun DevtoolsOverlay(modifier: Modifier = Modifier) {
if (devtoolsEnabled && showInputStateOverlay) {
DevtoolsInputStateOverlay()
}
if (devtoolsEnabled && showLastLayoutComputation || debugLayoutResult?.allLayoutsSuccess() == false) {
DevtoolsLastLayoutComputationOverlay()
if (debugLayoutResult?.allLayoutsSuccess() == false) {
DevtoolsLastLayoutComputationOverlay(debugLayoutResult)
}
if (devtoolsEnabled && showSpellingOverlay) {
DevtoolsSpellingOverlay()
@@ -91,6 +97,10 @@ fun DevtoolsOverlay(modifier: Modifier = Modifier) {
if (devtoolsEnabled && showInlineAutofillOverlay && AndroidVersion.ATLEAST_API30_R) {
DevtoolsInlineAutofillOverlay()
}
val loadFailure = themeInfo?.loadFailure
if (loadFailure != null) {
DevtoolsStylesheetFailedToLoadOverlay(loadFailure)
}
}
}
}
@@ -137,13 +147,9 @@ private fun DevtoolsInputStateOverlay() {
}
@Composable
private fun DevtoolsLastLayoutComputationOverlay() {
val context = LocalContext.current
val keyboardManager by context.keyboardManager()
val debugLayoutResult by keyboardManager.layoutManager.debugLayoutComputationResultFlow.collectAsState()
private fun DevtoolsLastLayoutComputationOverlay(debugLayoutResult: DebugLayoutComputationResult?) {
@Composable
fun PrintResult(result: Result<CachedLayout>) {
fun PrintResult(result: Result<CachedLayout?>) {
if (result.isSuccess) {
DevtoolsText(text = "loaded: ${result.getOrNull()?.name}")
} else {
@@ -229,6 +235,39 @@ private fun DevtoolsInlineAutofillOverlay() {
}
}
@Composable
private fun DevtoolsStylesheetFailedToLoadOverlay(loadFailure: ThemeManager.LoadFailure) {
DevtoolsOverlayBox(title = "Failed to load stylesheet, fell back to base style") {
DevtoolsSubGroup(title = "Extension") {
DevtoolsText(text = "id: ${loadFailure.extension.id}")
DevtoolsText(text = "title: ${loadFailure.extension.title}")
DevtoolsText(text = "version: ${loadFailure.extension.version}")
}
DevtoolsSubGroup(title = "Component") {
DevtoolsText(text = "id: ${loadFailure.component.id}")
DevtoolsText(text = "label: ${loadFailure.component.label}")
DevtoolsText(text = "path: ${loadFailure.component.stylesheetPath()}")
}
val cause = loadFailure.cause
DevtoolsSubGroup(title = "Cause") {
DevtoolsText(text = "${cause.message}")
}
if (cause is SnyggMissingSchemaException) {
DevtoolsSubGroup(title = "Explanation") {
DevtoolsText(
text = """
It appears youre trying to load a theme designed for FlorisBoard v0.4 (Snygg v1), which isnt compatible with the latest release using Snygg v2.
If you are the theme author, please update your theme to support Snygg v2.
If youre a user, please update your theme via the Addons Store. If an updated version isnt available yet, please select one of the built-in themes during this transition period.
""".trimIndent()
)
}
}
}
}
@Composable
private fun DevtoolsOverlayBox(
title: String,

View File

@@ -73,12 +73,6 @@ fun DevtoolsScreen() = FlorisScreen {
summary = stringRes(R.string.devtools__show_input_state_overlay__summary),
enabledIf = { prefs.devtools.enabled isEqualTo true },
)
SwitchPreference(
prefs.devtools.showLastLayoutComputation,
title = "Show last layout computation",
summary = "Show the last layout computation in a dialog",
enabledIf = { prefs.devtools.enabled isEqualTo true },
)
SwitchPreference(
prefs.devtools.showSpellingOverlay,
title = stringRes(R.string.devtools__show_spelling_overlay__label),
@@ -128,6 +122,13 @@ fun DevtoolsScreen() = FlorisScreen {
onClick = { navController.navigate(Routes.Devtools.ExportDebugLog) },
enabledIf = { prefs.devtools.enabled isEqualTo true },
)
SwitchPreference(
prefs.glide.enabled,
title = "prefs.glide.enabled (debug)",
summaryOn = "This impacts your performance and may trigger the all keys invisible bug!",
summaryOff = "Recommended to keep this off!",
enabledIf = { prefs.devtools.enabled isEqualTo true },
)
}
PreferenceGroup(title = stringRes(R.string.devtools__group_android__title)) {

View File

@@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Input
import androidx.compose.material.icons.filled.Shop
import androidx.compose.material.icons.outlined.FileDownload
import androidx.compose.material3.MaterialTheme
@@ -29,6 +30,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.Routes
@@ -41,6 +44,41 @@ import dev.patrickgold.florisboard.lib.ext.generateUpdateUrl
import dev.patrickgold.florisboard.lib.util.launchUrl
import org.florisboard.lib.kotlin.curlyFormat
@Composable
fun ImportExtensionBox(navController: NavController) {
val context = LocalContext.current
FlorisOutlinedBox(
modifier = Modifier.defaultFlorisOutlinedBox(),
) {
Text(
modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 4.dp),
text = stringRes(id = R.string.ext__home__info),
style = MaterialTheme.typography.bodySmall,
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 6.dp),
) {
FlorisTextButton(
onClick = {
context.launchUrl("https://${BuildConfig.FLADDONS_STORE_URL}/")
},
icon = Icons.Default.Shop,
text = stringRes(id = R.string.ext__home__visit_store),
)
Spacer(modifier = Modifier.weight(1f))
FlorisTextButton(
onClick = {
navController.navigate(Routes.Ext.Import(ExtensionImportScreenType.EXT_ANY, null))
},
icon = Icons.AutoMirrored.Filled.Input,
text = stringRes(R.string.action__import),
)
}
}
}
@Composable
fun UpdateBox(extensionIndex: List<Extension>) {
val context = LocalContext.current

View File

@@ -81,14 +81,11 @@ fun ExtensionComponentView(
when (component) {
is ThemeExtensionComponent -> {
val text = remember(
component.authors, component.isNightTheme, component.isBorderless,
component.isMaterialYouAware, component.stylesheetPath(),
component.authors, component.isNightTheme, component.stylesheetPath(),
) {
buildString {
appendLine("authors = ${component.authors}")
appendLine("isNightTheme = ${component.isNightTheme}")
appendLine("isBorderless = ${component.isBorderless}")
appendLine("isMaterialYouAware = ${component.isMaterialYouAware}")
append("stylesheetPath = ${component.stylesheetPath()}")
}
}

View File

@@ -0,0 +1,291 @@
/*
* Copyright (C) 2025 The FlorisBoard Contributors
*
* 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.app.ext
import android.provider.OpenableColumns
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Photo
import androidx.compose.material.icons.filled.TextFields
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.core.content.MimeTypeFilter
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.lib.cache.CacheManager
import dev.patrickgold.florisboard.lib.compose.FlorisIconButton
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.jetpref.datastore.ui.Preference
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
import dev.patrickgold.jetpref.material.ui.JetPrefTextField
import java.io.File
import java.util.*
import org.florisboard.lib.android.query
import org.florisboard.lib.android.readToFile
import org.florisboard.lib.android.showLongToast
import org.florisboard.lib.android.showShortToast
import org.florisboard.lib.kotlin.io.parentDir
import org.florisboard.lib.kotlin.io.subDir
import org.florisboard.lib.kotlin.io.subFile
const val FONTS = "fonts"
const val IMAGES = "images"
val MIME_TYPES = mapOf(
FONTS to listOf(
// Source: https://www.alienfactory.co.uk/articles/mime-types-for-web-fonts-in-bedsheet#mimeTypes
"font/*",
"application/vnd.ms-fontobject", // .eot
"application/font-woff", // .woff
"application/x-font-truetype", // .ttf
"application/x-font-opentype", // .otf
),
IMAGES to listOf(
"image/*",
),
)
@Composable
fun ExtensionEditFilesScreen(workspace: CacheManager.ExtEditorWorkspace<*>) = FlorisScreen {
title = stringRes(R.string.ext__editor__files__title)
fun handleBackPress() {
workspace.currentAction = null
}
navigationIcon {
FlorisIconButton(
onClick = { handleBackPress() },
icon = Icons.Default.Close,
)
}
content {
val context = LocalContext.current
var version by rememberSaveable { mutableIntStateOf(0) }
val fontFiles = remember(version) {
workspace.extDir.subDir(FONTS).listFiles { it.isFile }.orEmpty().asList()
}
val imageFiles = remember(version) {
workspace.extDir.subDir(IMAGES).listFiles { it.isFile }.orEmpty().asList()
}
var currentImportDest by remember { mutableStateOf<String?>(null) }
var currentImportResult by remember { mutableStateOf<Result<Pair<File, String>>?>(null) }
val importLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent(),
onResult = { uri ->
currentImportResult = runCatching {
checkNotNull(uri) { "" }
val tempFile = context.cacheDir.subFile("temp_${UUID.randomUUID()}")
context.contentResolver.readToFile(uri, tempFile)
val mimeType = context.contentResolver.getType(uri)
val types = MIME_TYPES[currentImportDest!!]!!
checkNotNull(MimeTypeFilter.matches(mimeType, types.toTypedArray())) {
"Given file mime type was '$mimeType', expected one of $types"
}
val fileName = context.contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME)).use { cursor ->
if (cursor == null || !cursor.moveToFirst()) return@use null
val name = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.getString(name)
}
tempFile to fileName.orEmpty()
}
},
)
LaunchedEffect(currentImportResult) {
val message = currentImportResult?.exceptionOrNull()?.message
if (!message.isNullOrBlank()) {
context.showLongToast(message)
}
}
BackHandler {
handleBackPress()
}
@Composable
fun FileList(title: String, icon: ImageVector, files: List<File>, onAdd: () -> Unit) {
var dialogFile by remember { mutableStateOf<File?>(null) }
ListItem(
headlineContent = {
Text(
text = title,
color = MaterialTheme.colorScheme.secondary,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
leadingContent = {
Spacer(modifier = Modifier.width(24.dp))
},
trailingContent = {
IconButton(
onClick = onAdd,
) {
Icon(Icons.Default.Add, null)
}
},
)
for (file in files) {
Preference(
onClick = {
dialogFile = file
},
icon = icon,
title = file.name,
)
}
dialogFile?.let { file ->
var fileNameInput by rememberSaveable { mutableStateOf(file.name) }
JetPrefAlertDialog(
title = stringRes(R.string.general__properties),
confirmLabel = stringRes(R.string.action__apply),
dismissLabel = stringRes(R.string.action__cancel),
neutralLabel = stringRes(R.string.action__delete),
allowOutsideDismissal = true,
onNeutral = {
if (file.delete()) {
context.showShortToast("Successfully deleted")
} else {
context.showShortToast("Failed to delete")
}
dialogFile = null
version++
},
onConfirm = {
val newFile = file.parentFile!!.subFile(fileNameInput).canonicalFile
if (newFile.parentFile != file.canonicalFile.parentFile) {
context.showLongToast("Invalid file name!")
return@JetPrefAlertDialog
}
if (newFile.exists()) {
context.showShortToast("Filename already exists.")
return@JetPrefAlertDialog
}
val success = file.renameTo(newFile)
if (success) {
context.showShortToast("Successfully renamed")
} else {
context.showShortToast("Failed to rename the file.")
}
dialogFile = null
version++
},
onDismiss = {
dialogFile = null
},
) {
JetPrefTextField(
labelText = stringRes(R.string.general__file_name),
value = fileNameInput,
onValueChange = { fileNameInput = it },
singleLine = true,
)
}
}
}
FileList(
title = stringRes(R.string.ext__editor__files__type_fonts),
icon = Icons.Default.TextFields,
files = fontFiles,
) {
currentImportDest = FONTS
importLauncher.launch("*/*")
}
FileList(
title = stringRes(R.string.ext__editor__files__type_images),
icon = Icons.Default.Photo,
files = imageFiles,
) {
currentImportDest = IMAGES
importLauncher.launch("*/*")
}
val dest = currentImportDest
val result = currentImportResult?.getOrNull()
if (dest != null && result != null) {
var fileNameInput by rememberSaveable { mutableStateOf(result.second) }
JetPrefAlertDialog(
title = stringRes(R.string.action__import_file),
confirmLabel = stringRes(R.string.action__add),
onConfirm = {
val fileName = fileNameInput.trim()
val dir = workspace.extDir.subDir(dest)
dir.mkdirs()
val file = dir.subFile(fileName)
if (file.parentDir != workspace.extDir.subDir(dest)) {
context.showShortToast("Invalid file name")
} else if (file.exists()) {
context.showShortToast("File already exists")
} else {
val tempFile = result.first
if (!tempFile.renameTo(file)) {
context.showShortToast("Failed to rename file")
tempFile.delete()
}
currentImportDest = null
currentImportResult = null
version++
}
},
dismissLabel = stringRes(R.string.action__cancel),
onDismiss = {
val tempFile = result.first
tempFile.delete()
currentImportDest = null
currentImportResult = null
},
) {
JetPrefTextField(
value = fileNameInput,
onValueChange = { fileNameInput = it },
singleLine = true,
)
}
}
}
}

View File

@@ -34,7 +34,6 @@ import androidx.compose.material.icons.automirrored.outlined.LibraryBooks
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Code
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -49,6 +48,7 @@ import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.settings.advanced.RadioListItem
import dev.patrickgold.florisboard.app.settings.theme.DialogProperty
import dev.patrickgold.florisboard.app.settings.theme.PrettyPrintConfig
import dev.patrickgold.florisboard.app.settings.theme.ThemeEditorScreen
import dev.patrickgold.florisboard.cacheManager
import dev.patrickgold.florisboard.extensionManager
@@ -64,9 +64,9 @@ import dev.patrickgold.florisboard.lib.compose.FlorisButtonBar
import dev.patrickgold.florisboard.lib.compose.FlorisIconButton
import dev.patrickgold.florisboard.lib.compose.FlorisInfoCard
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedBox
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedTextField
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.FlorisUnsavedChangesDialog
import dev.patrickgold.florisboard.lib.compose.Validation
import dev.patrickgold.florisboard.lib.compose.defaultFlorisOutlinedBox
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.ext.Extension
@@ -83,15 +83,17 @@ import dev.patrickgold.florisboard.lib.ext.validate
import dev.patrickgold.florisboard.lib.io.FlorisRef
import dev.patrickgold.florisboard.lib.io.ZipUtils
import dev.patrickgold.florisboard.lib.rememberValidationResult
import org.florisboard.lib.snygg.SnyggStylesheetJsonConfig
import dev.patrickgold.florisboard.themeManager
import dev.patrickgold.jetpref.datastore.ui.Preference
import dev.patrickgold.jetpref.datastore.ui.vectorResource
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
import dev.patrickgold.jetpref.material.ui.JetPrefTextField
import java.util.*
import org.florisboard.lib.android.showLongToast
import org.florisboard.lib.kotlin.io.deleteContentsRecursively
import org.florisboard.lib.kotlin.io.subDir
import org.florisboard.lib.kotlin.io.subFile
import org.florisboard.lib.kotlin.io.writeJson
import java.util.UUID
import kotlin.reflect.KClass
private val TextFieldVerticalPadding = 8.dp
@@ -197,7 +199,7 @@ private fun ExtensionEditScreenSheetSwitcher(
ManageDependenciesScreen(workspace)
}
is EditorAction.ManageFiles -> {
ManageFilesScreen(workspace)
ExtensionEditFilesScreen(workspace)
}
is EditorAction.CreateComponent<*> -> {
CreateComponentScreen(workspace, action.type)
@@ -261,17 +263,33 @@ private fun EditScreen(
return
}
val manifest = extEditor.build()
workspace.saverDir.deleteContentsRecursively()
val manifestFile = workspace.saverDir.subFile(ExtensionDefaults.MANIFEST_FILE_NAME)
manifestFile.writeJson(manifest, ExtensionJsonConfig)
when (extEditor) {
is ThemeExtensionEditor -> {
// TODO: this is hacky
val fonts = workspace.extDir.subDir("fonts")
if (fonts.exists()) {
fonts.copyRecursively(workspace.saverDir.subDir("fonts"), overwrite = true)
}
val images = workspace.extDir.subDir("images")
if (images.exists()) {
images.copyRecursively(workspace.saverDir.subDir("images"), overwrite = true)
}
for (theme in extEditor.themes) {
val stylesheetFile = workspace.saverDir.subFile(theme.stylesheetPath())
stylesheetFile.parentFile?.mkdirs()
val stylesheetEditor = theme.stylesheetEditor
if (stylesheetEditor != null) {
val stylesheet = stylesheetEditor.build()
stylesheetFile.writeJson(stylesheet, SnyggStylesheetJsonConfig)
runCatching {
val stylesheet = stylesheetEditor.build().toJson(PrettyPrintConfig).getOrThrow()
stylesheetFile.writeText(stylesheet)
}.onFailure {
// TODO: better error handling
context.showLongToast(it.message.toString())
return
}
} else {
val unmodifiedStylesheetFile = workspace.extDir.subFile(theme.stylesheetPath())
if (unmodifiedStylesheetFile.exists()) {
@@ -582,35 +600,6 @@ private fun ManageDependenciesScreen(workspace: CacheManager.ExtEditorWorkspace<
}
}
@Composable
private fun ManageFilesScreen(workspace: CacheManager.ExtEditorWorkspace<*>) = FlorisScreen {
title = stringRes(R.string.ext__editor__files__title)
fun handleBackPress() {
workspace.currentAction = null
}
navigationIcon {
FlorisIconButton(
onClick = { handleBackPress() },
icon = Icons.Default.Close,
)
}
content {
BackHandler {
handleBackPress()
}
FlorisInfoCard(
modifier = Modifier.padding(all = 8.dp),
text = """
Managing archive files is currently not supported.
""".trimIndent().replace('\n', ' '),
)
}
}
private enum class CreateFrom {
EMPTY,
EXISTING;
@@ -703,15 +692,14 @@ private fun <T : ExtensionComponent> CreateComponentScreen(
val component = editor.themes.find { it.id == componentName.componentId } ?: return
val componentEditor = component.let { c ->
ThemeExtensionComponentEditor(
componentId, c.label, c.authors, c.isNightTheme, c.isBorderless,
c.isMaterialYouAware, stylesheetPath = "",
componentId, c.label, c.authors, c.isNightTheme, stylesheetPath = "",
).also { it.stylesheetEditor = c.stylesheetEditor }
}
if (componentEditor.stylesheetEditor != null) {
val stylesheet = componentEditor.stylesheetEditor!!.build()
val stylesheetFile = workspace.extDir.subFile(componentEditor.stylesheetPath())
stylesheetFile.parentFile?.mkdirs()
stylesheetFile.writeJson(stylesheet, SnyggStylesheetJsonConfig)
val stylesheet = componentEditor.stylesheetEditor!!.build().toJson(PrettyPrintConfig).getOrThrow()
stylesheetFile.writeText(stylesheet)
componentEditor.stylesheetEditor = null
} else {
val srcStylesheetFile = workspace.extDir.subFile(component.stylesheetPath())
@@ -813,36 +801,37 @@ private fun <T : ExtensionComponent> CreateComponentScreen(
modifier = Modifier.padding(horizontal = 16.dp),
text = stringRes(R.string.ext__meta__id),
) {
FlorisOutlinedTextField(
JetPrefTextField(
modifier = Modifier.fillMaxWidth(),
value = newId,
onValueChange = { newId = it },
singleLine = true,
showValidationError = showValidationErrors,
validationResult = newIdValidation,
)
Validation(showValidationErrors, newIdValidation)
}
DialogProperty(
modifier = Modifier.padding(horizontal = 16.dp),
text = stringRes(R.string.ext__meta__label),
) {
FlorisOutlinedTextField(
JetPrefTextField(
modifier = Modifier.fillMaxWidth(),
value = newLabel,
onValueChange = { newLabel = it },
singleLine = true,
showValidationError = showValidationErrors,
validationResult = newLabelValidation,
)
Validation(showValidationErrors, newLabelValidation)
}
DialogProperty(
modifier = Modifier.padding(horizontal = 16.dp),
text = stringRes(R.string.ext__meta__authors),
) {
FlorisOutlinedTextField(
JetPrefTextField(
modifier = Modifier.fillMaxWidth(),
value = newAuthors,
onValueChange = { newAuthors = it },
showValidationError = showValidationErrors,
validationResult = newAuthorsValidation,
)
Validation(showValidationErrors, newAuthorsValidation)
}
}
}
@@ -860,7 +849,6 @@ private fun EditorSheetTextField(
showValidationError: Boolean = false,
validationResult: ValidationResult? = null,
) {
val borderColor = MaterialTheme.colorScheme.outline
Column(modifier = Modifier.padding(vertical = TextFieldVerticalPadding)) {
Row(
modifier = Modifier
@@ -880,18 +868,13 @@ private fun EditorSheetTextField(
)
}
}
FlorisOutlinedTextField(
JetPrefTextField(
modifier = modifier.fillMaxWidth(),
enabled = enabled,
value = value,
onValueChange = onValueChange,
singleLine = singleLine,
showValidationError = showValidationError,
validationResult = validationResult,
colors = OutlinedTextFieldDefaults.colors(
unfocusedBorderColor = borderColor,
disabledBorderColor = borderColor,
)
)
Validation(showValidationError, validationResult)
}
}

View File

@@ -16,33 +16,18 @@
package dev.patrickgold.florisboard.app.ext
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Input
import androidx.compose.material.icons.filled.Keyboard
import androidx.compose.material.icons.filled.Language
import androidx.compose.material.icons.filled.Palette
import androidx.compose.material.icons.filled.Shop
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.Routes
import dev.patrickgold.florisboard.extensionManager
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedBox
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.FlorisTextButton
import dev.patrickgold.florisboard.lib.compose.defaultFlorisOutlinedBox
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.util.launchUrl
import dev.patrickgold.jetpref.datastore.ui.Preference
@Composable
@@ -56,36 +41,7 @@ fun ExtensionHomeScreen() = FlorisScreen {
val extensionIndex = extensionManager.combinedExtensionList()
content {
FlorisOutlinedBox(
modifier = Modifier.defaultFlorisOutlinedBox(),
) {
Text(
modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 4.dp),
text = stringRes(id = R.string.ext__home__info),
style = MaterialTheme.typography.bodySmall,
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 6.dp),
) {
FlorisTextButton(
onClick = {
context.launchUrl("https://${BuildConfig.FLADDONS_STORE_URL}/")
},
icon = Icons.Default.Shop,
text = stringRes(id = R.string.ext__home__visit_store),
)
Spacer(modifier = Modifier.weight(1f))
FlorisTextButton(
onClick = {
navController.navigate(Routes.Ext.Import(ExtensionImportScreenType.EXT_ANY, null))
},
icon = Icons.AutoMirrored.Filled.Input,
text = stringRes(R.string.action__import),
)
}
}
ImportExtensionBox(navController)
UpdateBox(extensionIndex = extensionIndex)

View File

@@ -113,6 +113,9 @@ fun ExtensionListScreen(type: ExtensionListScreenType, showUpdate: Boolean) = Fl
contentPadding = PaddingValues(bottom = fabHeightDp),
) {
if (showUpdate) {
item {
ImportExtensionBox(navController)
}
item {
UpdateBox(extensionIndex = extensionIndex)
}

View File

@@ -19,6 +19,7 @@ package dev.patrickgold.florisboard.app.settings
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Assignment
import androidx.compose.material.icons.filled.Adb
import androidx.compose.material.icons.filled.Extension
import androidx.compose.material.icons.filled.Gesture
import androidx.compose.material.icons.filled.Language
@@ -152,8 +153,8 @@ fun HomeScreen() = FlorisScreen {
)
Preference(
icon = Icons.Outlined.Build,
title = stringRes(R.string.settings__advanced__title),
onClick = { navController.navigate(Routes.Settings.Advanced) },
title = stringRes(R.string.settings__other__title),
onClick = { navController.navigate(Routes.Settings.Other) },
)
Preference(
icon = Icons.Outlined.Info,

View File

@@ -19,58 +19,71 @@ package dev.patrickgold.florisboard.app.settings.advanced
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Adb
import androidx.compose.material.icons.filled.Archive
import androidx.compose.material.icons.filled.FormatPaint
import androidx.compose.material.icons.filled.FormatColorFill
import androidx.compose.material.icons.filled.Language
import androidx.compose.material.icons.filled.Palette
import androidx.compose.material.icons.filled.Preview
import androidx.compose.material.icons.filled.SettingsBackupRestore
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.AppTheme
import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.Routes
import dev.patrickgold.florisboard.app.enumDisplayEntriesOf
import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn
import dev.patrickgold.florisboard.ime.keyboard.IncognitoMode
import dev.patrickgold.florisboard.lib.FlorisLocale
import org.florisboard.lib.android.AndroidVersion
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.jetpref.datastore.model.observeAsState
import dev.patrickgold.jetpref.datastore.ui.ColorPickerPreference
import dev.patrickgold.jetpref.datastore.ui.ListPreference
import dev.patrickgold.jetpref.datastore.ui.Preference
import dev.patrickgold.jetpref.datastore.ui.PreferenceGroup
import dev.patrickgold.jetpref.datastore.ui.SwitchPreference
import dev.patrickgold.jetpref.datastore.ui.isMaterialYou
import dev.patrickgold.jetpref.datastore.ui.listPrefEntries
import dev.patrickgold.jetpref.datastore.ui.vectorResource
import org.florisboard.lib.android.AndroidVersion
import org.florisboard.lib.color.ColorMappings
@Composable
fun AdvancedScreen() = FlorisScreen {
title = stringRes(R.string.settings__advanced__title)
fun OtherScreen() = FlorisScreen {
title = stringRes(R.string.settings__other__title)
previewFieldVisible = false
val navController = LocalNavController.current
val context = LocalContext.current
content {
ListPreference(
prefs.advanced.settingsTheme,
prefs.other.settingsTheme,
icon = Icons.Default.Palette,
title = stringRes(R.string.pref__advanced__settings_theme__label),
title = stringRes(R.string.pref__other__settings_theme__label),
entries = enumDisplayEntriesOf(AppTheme::class),
)
SwitchPreference(
pref = prefs.advanced.useMaterialYou,
icon = Icons.Default.FormatPaint,
title = stringRes(R.string.pref__advanced__settings_material_you__label),
visibleIf = {
AndroidVersion.ATLEAST_API31_S
},
ColorPickerPreference(
pref = prefs.other.accentColor,
title = stringRes(R.string.pref__other__settings_accent_color__label),
defaultValueLabel = stringRes(R.string.action__default),
icon = Icons.Default.FormatColorFill,
defaultColors = ColorMappings.colors,
showAlphaSlider = false,
enableAdvancedLayout = false,
colorOverride = {
if (it.isMaterialYou(context)) {
Color.Unspecified
} else {
it
}
}
)
ListPreference(
prefs.advanced.settingsLanguage,
prefs.other.settingsLanguage,
icon = Icons.Default.Language,
title = stringRes(R.string.pref__advanced__settings_language__label),
title = stringRes(R.string.pref__other__settings_language__label),
entries = listPrefEntries {
listOf(
"auto",
@@ -132,21 +145,15 @@ fun AdvancedScreen() = FlorisScreen {
}
)
SwitchPreference(
prefs.advanced.showAppIcon,
prefs.other.showAppIcon,
icon = Icons.Default.Preview,
title = stringRes(R.string.pref__advanced__show_app_icon__label),
title = stringRes(R.string.pref__other__show_app_icon__label),
summary = when {
AndroidVersion.ATLEAST_API29_Q -> stringRes(R.string.pref__advanced__show_app_icon__summary_atleast_q)
AndroidVersion.ATLEAST_API29_Q -> stringRes(R.string.pref__other__show_app_icon__summary_atleast_q)
else -> null
},
enabledIf = { AndroidVersion.ATMOST_API28_P },
)
ListPreference(
prefs.advanced.incognitoMode,
icon = vectorResource(id = R.drawable.ic_incognito),
title = stringRes(R.string.pref__advanced__incognito_mode__label),
entries = enumDisplayEntriesOf(IncognitoMode::class),
)
Preference(
icon = Icons.Default.Adb,
title = stringRes(R.string.devtools__title),

View File

@@ -52,6 +52,23 @@ fun ClipboardScreen() = FlorisScreen {
enabledIf = { prefs.clipboard.useInternalClipboard isEqualTo true },
)
PreferenceGroup(title = stringRes(R.string.pref__clipboard__group_clipboard_suggestion__label)) {
SwitchPreference(
prefs.clipboard.suggestionEnabled,
title = stringRes(R.string.pref__clipboard__suggestion_enabled__label),
summary = stringRes(R.string.pref__clipboard__suggestion_enabled__summary),
)
DialogSliderPreference(
prefs.clipboard.suggestionTimeout,
title = stringRes(R.string.pref__clipboard__suggestion_timeout__label),
valueLabel = { stringRes(R.string.pref__clipboard__suggestion_timeout__summary, "v" to it) },
min = 30,
max = 300,
stepIncrement = 5,
enabledIf = { prefs.clipboard.suggestionEnabled isEqualTo true },
)
}
PreferenceGroup(title = stringRes(R.string.pref__clipboard__group_clipboard_history__label)) {
SwitchPreference(
prefs.clipboard.historyEnabled,

View File

@@ -56,14 +56,15 @@ import dev.patrickgold.florisboard.ime.dictionary.UserDictionaryEntry
import dev.patrickgold.florisboard.ime.dictionary.UserDictionaryValidation
import dev.patrickgold.florisboard.lib.FlorisLocale
import dev.patrickgold.florisboard.lib.compose.FlorisIconButton
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedTextField
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.Validation
import dev.patrickgold.florisboard.lib.compose.rippleClickable
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.rememberValidationResult
import dev.patrickgold.florisboard.lib.util.launchActivity
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
import dev.patrickgold.jetpref.material.ui.JetPrefListItem
import dev.patrickgold.jetpref.material.ui.JetPrefTextField
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.florisboard.lib.android.showLongToast
@@ -366,39 +367,35 @@ fun UserDictionaryScreen(type: UserDictionaryType) = FlorisScreen {
) {
Column {
DialogProperty(text = stringRes(R.string.settings__udm__dialog__word_label)) {
FlorisOutlinedTextField(
JetPrefTextField(
value = word,
onValueChange = { word = it },
showValidationError = showValidationErrors,
validationResult = wordValidation,
)
Validation(showValidationErrors, wordValidation)
}
DialogProperty(text = stringRes(
R.string.settings__udm__dialog__freq_label,
"f_min" to FREQUENCY_MIN, "f_max" to FREQUENCY_MAX,
)) {
FlorisOutlinedTextField(
JetPrefTextField(
value = freq,
onValueChange = { freq = it },
showValidationError = showValidationErrors,
validationResult = freqValidation,
)
Validation(showValidationErrors, freqValidation)
}
DialogProperty(text = stringRes(R.string.settings__udm__dialog__shortcut_label)) {
FlorisOutlinedTextField(
JetPrefTextField(
value = shortcut,
onValueChange = { shortcut = it },
showValidationError = showValidationErrors,
validationResult = shortcutValidation,
)
Validation(showValidationErrors, shortcutValidation)
}
DialogProperty(text = stringRes(R.string.settings__udm__dialog__locale_label)) {
FlorisOutlinedTextField(
JetPrefTextField(
value = locale,
onValueChange = { locale = it },
showValidationError = showValidationErrors,
validationResult = localeValidation,
)
Validation(showValidationErrors, localeValidation)
}
}
}

View File

@@ -107,8 +107,10 @@ fun KeyboardScreen() = FlorisScreen {
PreferenceGroup(title = stringRes(R.string.pref__keyboard__group_layout__label)) {
ListPreference(
prefs.keyboard.oneHandedMode,
prefs.keyboard.oneHandedModeEnabled,
title = stringRes(R.string.pref__keyboard__one_handed_mode__label),
entries = enumDisplayEntriesOf(OneHandedMode::class),
summarySwitchDisabled = stringRes(R.string.state__disabled),
)
DialogSliderPreference(
prefs.keyboard.oneHandedModeScaleFactor,
@@ -117,7 +119,7 @@ fun KeyboardScreen() = FlorisScreen {
min = 70,
max = 90,
stepIncrement = 1,
enabledIf = { prefs.keyboard.oneHandedMode isNotEqualTo OneHandedMode.OFF },
enabledIf = { prefs.keyboard.oneHandedModeEnabled.isTrue() },
)
ListPreference(
prefs.keyboard.landscapeInputUiMode,

View File

@@ -53,6 +53,7 @@ import dev.patrickgold.jetpref.datastore.model.observeAsState
import dev.patrickgold.jetpref.datastore.ui.ListPreference
import dev.patrickgold.jetpref.datastore.ui.Preference
import dev.patrickgold.jetpref.datastore.ui.PreferenceGroup
import dev.patrickgold.jetpref.datastore.ui.SwitchPreference
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@@ -103,6 +104,10 @@ fun LocalizationScreen() = FlorisScreen {
title = stringRes(R.string.settings__localization__display_language_names_in__label),
entries = enumDisplayEntriesOf(DisplayLanguageNamesIn::class),
)
SwitchPreference(
prefs.localization.displayKeyboardLabelsInSubtypeLanguage,
title = stringRes(R.string.settings__localization__display_keyboard_labels_in_subtype_language),
)
Preference(
title = stringRes(R.string.settings__localization__language_pack_title),
summary = stringRes(R.string.settings__localization__language_pack_summary),

View File

@@ -48,8 +48,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
@@ -59,7 +59,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
@@ -82,30 +81,33 @@ import dev.patrickgold.florisboard.ime.keyboard.computeImageVector
import dev.patrickgold.florisboard.ime.keyboard.computeLabel
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
import dev.patrickgold.florisboard.ime.theme.FlorisImeUiSpec
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.keyboardManager
import dev.patrickgold.florisboard.lib.NATIVE_NULLPTR
import org.florisboard.lib.android.showShortToast
import org.florisboard.lib.android.stringRes
import dev.patrickgold.florisboard.lib.compose.FlorisChip
import dev.patrickgold.florisboard.lib.compose.FlorisDropdownMenu
import dev.patrickgold.florisboard.lib.compose.FlorisHyperlinkText
import dev.patrickgold.florisboard.lib.compose.FlorisIconButton
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedTextField
import dev.patrickgold.florisboard.lib.compose.florisHorizontalScroll
import dev.patrickgold.florisboard.lib.compose.stringRes
import org.florisboard.lib.snygg.SnyggLevel
import org.florisboard.lib.snygg.SnyggRule
import dev.patrickgold.florisboard.lib.util.InputMethodUtils
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
import dev.patrickgold.jetpref.material.ui.JetPrefDropdown
import dev.patrickgold.jetpref.material.ui.JetPrefTextField
import dev.patrickgold.jetpref.material.ui.JetPrefTextFieldDefaults
import org.florisboard.lib.android.showShortToast
import org.florisboard.lib.android.stringRes
import org.florisboard.lib.kotlin.curlyFormat
import org.florisboard.lib.kotlin.getKeyByValue
import org.florisboard.lib.snygg.SnyggAnnotationRule
import org.florisboard.lib.snygg.SnyggElementRule
import org.florisboard.lib.snygg.SnyggRule
import org.florisboard.lib.snygg.SnyggSelector
import org.florisboard.lib.snygg.ui.NonNullSaver
private val TransparentTextSelectionColors = TextSelectionColors(
handleColor = Color.Transparent,
backgroundColor = Color.Transparent,
)
internal val SnyggEmptyRuleForAdding = SnyggRule(element = "- select -")
internal val SnyggEmptyRuleForAdding = SnyggElementRule(elementName = "--select--")
@OptIn(ExperimentalLayoutApi::class)
@Composable
@@ -116,69 +118,69 @@ internal fun EditRuleDialog(
onDeleteRule: (rule: SnyggRule) -> Unit,
onDismiss: () -> Unit,
) {
val context = LocalContext.current
val isAddRuleDialog = initRule == SnyggEmptyRuleForAdding
var showSelectAsError by rememberSaveable { mutableStateOf(false) }
var showAlreadyExistsError by rememberSaveable { mutableStateOf(false) }
val possibleElementNames = remember {
listOf(SnyggEmptyRuleForAdding.element) + FlorisImeUiSpec.elements.keys
val possibleRuleTemplates = remember {
buildList {
add(SnyggEmptyRuleForAdding)
add(SnyggAnnotationRule.Font(fontName = ""))
FlorisImeUi.elementNames.forEach { name ->
add(SnyggElementRule(name))
}
}
}
val possibleRuleLabels = possibleRuleTemplates.map { rule ->
val elementName = when (rule) {
is SnyggElementRule -> rule.elementName
else -> rule.decl().name
}
context.translateElementName(elementName, level) ?: rule
}
val possibleElementLabels = possibleElementNames.map { translateElementName(it, level) ?: it }
var elementsExpanded by remember { mutableStateOf(false) }
var elementsSelectedIndex by rememberSaveable {
val index = possibleElementNames.indexOf(initRule.element).coerceIn(possibleElementNames.indices)
val index = possibleRuleTemplates
.indexOfFirst { rule ->
val elementName = when (rule) {
is SnyggElementRule -> rule.elementName
else -> rule.decl().name
}
val initElementName = when (initRule) {
is SnyggElementRule -> initRule.elementName
else -> initRule.decl().name
}
elementName == initElementName
}
.coerceIn(possibleRuleTemplates.indices)
mutableIntStateOf(index)
}
val codes = rememberSaveable(saver = IntListSaver) { initRule.codes.toMutableStateList() }
var editCodeDialogValue by rememberSaveable { mutableStateOf<Int?>(null) }
val groups = rememberSaveable(saver = IntListSaver) { initRule.groups.toMutableStateList() }
var shiftStateUnshifted by rememberSaveable {
mutableStateOf(initRule.shiftStates.contains(InputShiftState.UNSHIFTED.value))
var currentRule by rememberSaveable(elementsSelectedIndex, stateSaver = SnyggRule.NonNullSaver) {
mutableStateOf(
if (isAddRuleDialog) possibleRuleTemplates[elementsSelectedIndex] else initRule
)
}
var shiftStateShiftedManual by rememberSaveable {
mutableStateOf(initRule.shiftStates.contains(InputShiftState.SHIFTED_MANUAL.value))
}
var shiftStateShiftedAutomatic by rememberSaveable {
mutableStateOf(initRule.shiftStates.contains(InputShiftState.SHIFTED_AUTOMATIC.value))
}
var shiftStateCapsLock by rememberSaveable {
mutableStateOf(initRule.shiftStates.contains(InputShiftState.CAPS_LOCK.value))
}
var pressedSelector by rememberSaveable { mutableStateOf(initRule.pressedSelector) }
var focusSelector by rememberSaveable { mutableStateOf(initRule.focusSelector) }
var disabledSelector by rememberSaveable { mutableStateOf(initRule.disabledSelector) }
JetPrefAlertDialog(
title = stringRes(if (isAddRuleDialog) {
R.string.settings__theme_editor__add_rule
} else {
R.string.settings__theme_editor__edit_rule
}),
confirmLabel = stringRes(if (isAddRuleDialog) {
R.string.action__add
} else {
R.string.action__apply
}),
title = stringRes(
if (isAddRuleDialog) {
R.string.settings__theme_editor__add_rule
} else {
R.string.settings__theme_editor__edit_rule
}
),
confirmLabel = stringRes(
if (isAddRuleDialog) {
R.string.action__add
} else {
R.string.action__apply
}
),
onConfirm = {
if (isAddRuleDialog && elementsSelectedIndex == 0) {
showSelectAsError = true
} else {
val newRule = SnyggRule(
element = possibleElementNames[elementsSelectedIndex],
codes = codes.toList(),
groups = groups.toList(),
shiftStates = buildList {
if (shiftStateUnshifted) { add(InputShiftState.UNSHIFTED.value) }
if (shiftStateShiftedManual) { add(InputShiftState.SHIFTED_MANUAL.value) }
if (shiftStateShiftedAutomatic) { add(InputShiftState.SHIFTED_AUTOMATIC.value) }
if (shiftStateCapsLock) { add(InputShiftState.CAPS_LOCK.value) }
},
pressedSelector = pressedSelector,
focusSelector = focusSelector,
disabledSelector = disabledSelector,
)
if (!onConfirmRule(initRule, newRule)) {
if (!onConfirmRule(initRule, currentRule)) {
showAlreadyExistsError = true
}
}
@@ -202,154 +204,228 @@ internal fun EditRuleDialog(
)
}
DialogProperty(text = stringRes(R.string.settings__theme_editor__rule_element)) {
FlorisDropdownMenu(
items = possibleElementLabels,
expanded = elementsExpanded,
DialogProperty(text = stringRes(R.string.settings__theme_editor__rule_name)) {
JetPrefDropdown(
options = possibleRuleLabels,
selectedOptionIndex = elementsSelectedIndex,
onSelectOption = { elementsSelectedIndex = it },
enabled = isAddRuleDialog,
selectedIndex = elementsSelectedIndex,
isError = showSelectAsError && elementsSelectedIndex == 0,
onSelectItem = { elementsSelectedIndex = it },
onExpandRequest = { elementsExpanded = true },
onDismissRequest = { elementsExpanded = false },
)
}
DialogProperty(text = stringRes(R.string.settings__theme_editor__rule_selectors)) {
Row(modifier = Modifier.florisHorizontalScroll()) {
FlorisChip(
onClick = { pressedSelector = !pressedSelector },
modifier = Modifier.padding(end = 4.dp),
text = when (level) {
SnyggLevel.DEVELOPER -> SnyggRule.PRESSED_SELECTOR
else -> stringRes(R.string.snygg__rule_selector__pressed)
(currentRule as? SnyggAnnotationRule.Font)?.apply {
DialogProperty(text = stringRes(R.string.snygg__rule_annotation__font_name)) {
JetPrefTextField(
modifier = Modifier,
value = fontName,
onValueChange = {
currentRule = copy(fontName = it)
},
selected = pressedSelector,
)
FlorisChip(
onClick = { focusSelector = !focusSelector },
modifier = Modifier.padding( end = 4.dp),
text = when (level) {
SnyggLevel.DEVELOPER -> SnyggRule.FOCUS_SELECTOR
else -> stringRes(R.string.snygg__rule_selector__focus)
},
selected = focusSelector,
)
FlorisChip(
onClick = { disabledSelector = !disabledSelector },
text = when (level) {
SnyggLevel.DEVELOPER -> SnyggRule.DISABLED_SELECTOR
else -> stringRes(R.string.snygg__rule_selector__disabled)
},
selected = disabledSelector,
singleLine = true,
)
}
}
DialogProperty(
text = stringRes(R.string.settings__theme_editor__rule_codes),
trailingIconTitle = {
FlorisIconButton(
onClick = { editCodeDialogValue = NATIVE_NULLPTR.toInt() },
modifier = Modifier.offset(x = 12.dp),
icon = Icons.Default.Add,
)
},
) {
Text(
modifier = Modifier.padding(vertical = 4.dp),
text = stringRes(if (codes.isEmpty()) {
R.string.settings__theme_editor__no_codes_defined
// TODO: Move to toplevel @Composable function
(currentRule as? SnyggElementRule)?.apply {
if (elementName == SnyggEmptyRuleForAdding.elementName) {
return@apply
}
fun updateCurrentRule(newSelector: SnyggSelector) {
currentRule = if (selector == newSelector) {
copy(selector = SnyggSelector.NONE)
} else {
R.string.settings__theme_editor__codes_defined
}),
fontStyle = FontStyle.Italic,
)
FlowRow {
for (code in codes) {
copy(selector = newSelector)
}
}
DialogProperty(text = stringRes(R.string.settings__theme_editor__rule_selectors)) {
Row(modifier = Modifier.florisHorizontalScroll()) {
//TODO: LazyRow
FlorisChip(
onClick = { editCodeDialogValue = code },
text = code.toString(),
selected = editCodeDialogValue == code,
shape = MaterialTheme.shapes.medium,
onClick = { updateCurrentRule(SnyggSelector.PRESSED) },
modifier = Modifier.padding(end = 4.dp),
text = when (level) {
SnyggLevel.DEVELOPER -> SnyggSelector.PRESSED.id
else -> stringRes(R.string.snygg__rule_selector__pressed)
},
selected = selector == SnyggSelector.PRESSED,
)
FlorisChip(
onClick = { updateCurrentRule(SnyggSelector.FOCUS) },
modifier = Modifier.padding( end = 4.dp),
text = when (level) {
SnyggLevel.DEVELOPER -> SnyggSelector.FOCUS.id
else -> stringRes(R.string.snygg__rule_selector__focus)
},
selected = selector == SnyggSelector.FOCUS,
)
FlorisChip(
onClick = { updateCurrentRule(SnyggSelector.HOVER) },
modifier = Modifier.padding( end = 4.dp),
text = when (level) {
SnyggLevel.DEVELOPER -> SnyggSelector.HOVER.id
else -> stringRes(R.string.snygg__rule_selector__hover)
},
selected = selector == SnyggSelector.HOVER,
)
FlorisChip(
onClick = { updateCurrentRule(SnyggSelector.DISABLED) },
text = when (level) {
SnyggLevel.DEVELOPER -> SnyggSelector.DISABLED.id
else -> stringRes(R.string.snygg__rule_selector__disabled)
},
selected = selector == SnyggSelector.DISABLED,
)
}
}
val codes = remember(currentRule) {
attributes[FlorisImeUi.Attr.Code] ?: emptyList()
}
var editCodeDialogValue by rememberSaveable { mutableStateOf<String?>(null) }
val initCodeValue = editCodeDialogValue
if (initCodeValue != null) {
EditCodeValueDialog(
codeValue = initCodeValue,
checkExisting = { codes.contains(it) },
onAdd = {
currentRule = copy(
attributes = attributes.including(FlorisImeUi.Attr.Code to it)
)
},
onDelete = {
currentRule = copy(
attributes = attributes.excluding(FlorisImeUi.Attr.Code to it)
)
},
onDismiss = { editCodeDialogValue = null },
)
}
DialogProperty(
text = stringRes(R.string.settings__theme_editor__rule_codes),
trailingIconTitle = {
FlorisIconButton(
onClick = { editCodeDialogValue = KeyCode.UNSPECIFIED.toString() },
modifier = Modifier.offset(x = 12.dp),
icon = Icons.Default.Add,
)
},
) {
Text(
modifier = Modifier.padding(vertical = 4.dp),
text = stringRes(
if (codes.isEmpty()) {
R.string.settings__theme_editor__no_codes_defined
} else {
R.string.settings__theme_editor__codes_defined
}
),
fontStyle = FontStyle.Italic,
)
FlowRow {
for (code in codes) {
FlorisChip(
onClick = { editCodeDialogValue = code },
text = code.toString(),
selected = editCodeDialogValue == code,
shape = MaterialTheme.shapes.medium,
)
}
}
}
val shiftStateUnshifted = remember(currentRule) {
attributes[FlorisImeUi.Attr.ShiftState]?.contains(InputShiftState.UNSHIFTED.attrName()) == true
}
val shiftStateShiftedManual = remember(currentRule) {
attributes[FlorisImeUi.Attr.ShiftState]?.contains(InputShiftState.SHIFTED_MANUAL.attrName()) == true
}
val shiftStateShiftedAutomatic = remember(currentRule) {
attributes[FlorisImeUi.Attr.ShiftState]?.contains(InputShiftState.SHIFTED_AUTOMATIC.attrName()) == true
}
val shiftStateCapsLock = remember(currentRule) {
attributes[FlorisImeUi.Attr.ShiftState]?.contains(InputShiftState.CAPS_LOCK.attrName()) == true
}
DialogProperty(text = stringRes(R.string.settings__theme_editor__rule_shift_states)) {
FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
FlorisChip(
onClick = {
currentRule = copy(
attributes = attributes.toggling(
FlorisImeUi.Attr.ShiftState to InputShiftState.UNSHIFTED.attrName()
)
)
},
text = when (level) {
SnyggLevel.DEVELOPER -> InputShiftState.UNSHIFTED.attrName()
else -> stringRes(R.string.enum__input_shift_state__unshifted)
},
selected = shiftStateUnshifted,
)
FlorisChip(
onClick = {
currentRule = copy(
attributes = attributes.toggling(
FlorisImeUi.Attr.ShiftState to InputShiftState.SHIFTED_MANUAL.attrName()
)
)
},
text = when (level) {
SnyggLevel.DEVELOPER -> InputShiftState.SHIFTED_MANUAL.attrName()
else -> stringRes(R.string.enum__input_shift_state__shifted_manual)
},
selected = shiftStateShiftedManual,
)
FlorisChip(
onClick = {
currentRule = copy(
attributes = attributes.toggling(
FlorisImeUi.Attr.ShiftState to InputShiftState.SHIFTED_AUTOMATIC.attrName()
)
)
},
text = when (level) {
SnyggLevel.DEVELOPER -> InputShiftState.SHIFTED_AUTOMATIC.attrName()
else -> stringRes(R.string.enum__input_shift_state__shifted_automatic)
},
selected = shiftStateShiftedAutomatic,
)
FlorisChip(
onClick = {
currentRule = copy(
attributes = attributes.toggling(
FlorisImeUi.Attr.ShiftState to InputShiftState.CAPS_LOCK.attrName()
)
)
},
text = when (level) {
SnyggLevel.DEVELOPER -> InputShiftState.CAPS_LOCK.attrName()
else -> stringRes(R.string.enum__input_shift_state__caps_lock)
},
selected = shiftStateCapsLock,
)
}
}
}
DialogProperty(text = stringRes(R.string.settings__theme_editor__rule_shift_states)) {
FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
FlorisChip(
onClick = { shiftStateUnshifted = !shiftStateUnshifted },
text = when (level) {
SnyggLevel.DEVELOPER -> remember {
SnyggRule.Placeholders.getKeyByValue(InputShiftState.UNSHIFTED.value)
}
else -> stringRes(R.string.enum__input_shift_state__unshifted)
},
selected = shiftStateUnshifted,
)
FlorisChip(
onClick = { shiftStateShiftedManual = !shiftStateShiftedManual },
text = when (level) {
SnyggLevel.DEVELOPER -> remember {
SnyggRule.Placeholders.getKeyByValue(InputShiftState.SHIFTED_MANUAL.value)
}
else -> stringRes(R.string.enum__input_shift_state__shifted_manual)
},
selected = shiftStateShiftedManual,
)
FlorisChip(
onClick = { shiftStateShiftedAutomatic = !shiftStateShiftedAutomatic },
text = when (level) {
SnyggLevel.DEVELOPER -> remember {
SnyggRule.Placeholders.getKeyByValue(InputShiftState.SHIFTED_AUTOMATIC.value)
}
else -> stringRes(R.string.enum__input_shift_state__shifted_automatic)
},
selected = shiftStateShiftedAutomatic,
)
FlorisChip(
onClick = { shiftStateCapsLock = !shiftStateCapsLock },
text = when (level) {
SnyggLevel.DEVELOPER -> remember {
SnyggRule.Placeholders.getKeyByValue(InputShiftState.CAPS_LOCK.value)
}
else -> stringRes(R.string.enum__input_shift_state__caps_lock)
},
selected = shiftStateCapsLock,
)
}
}
}
}
val initCodeValue = editCodeDialogValue
if (initCodeValue != null) {
EditCodeValueDialog(
codeValue = initCodeValue,
checkExisting = { codes.contains(it) },
onAdd = { codes.add(it) },
onDelete = { codes.remove(it) },
onDismiss = { editCodeDialogValue = null },
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun EditCodeValueDialog(
codeValue: Int,
checkExisting: (Int) -> Boolean,
onAdd: (Int) -> Unit,
onDelete: (Int) -> Unit,
codeValue: String,
checkExisting: (String) -> Boolean,
onAdd: (String) -> Unit,
onDelete: (String) -> Unit,
onDismiss: () -> Unit,
) {
val context = LocalContext.current
val keyboardManager by context.keyboardManager()
var inputCodeString by rememberSaveable(codeValue) {
val str = if (codeValue == 0) "" else codeValue.toString()
val str = if (codeValue == KeyCode.UNSPECIFIED.toString()) "" else codeValue.toString()
mutableStateOf(str)
}
val textKeyData = remember(inputCodeString) {
@@ -406,6 +482,7 @@ private fun EditCodeValueDialog(
inputCodeString = data.code.toString()
isRecordingKey = false
}
override fun onInputKeyRepeat(data: KeyData) = Unit
override fun onInputKeyCancel(data: KeyData) = Unit
}
@@ -423,16 +500,20 @@ private fun EditCodeValueDialog(
}
JetPrefAlertDialog(
title = stringRes(if (codeValue == NATIVE_NULLPTR.toInt()) {
R.string.settings__theme_editor__add_code
} else {
R.string.settings__theme_editor__edit_code
}),
confirmLabel = stringRes(if (codeValue == NATIVE_NULLPTR.toInt()) {
R.string.action__add
} else {
R.string.action__apply
}),
title = stringRes(
if (codeValue == KeyCode.UNSPECIFIED.toString()) {
R.string.settings__theme_editor__add_code
} else {
R.string.settings__theme_editor__edit_code
}
),
confirmLabel = stringRes(
if (codeValue == KeyCode.UNSPECIFIED.toString()) {
R.string.action__add
} else {
R.string.action__apply
}
),
onConfirm = {
val code = inputCodeString.trim().toIntOrNull(radix = 10)
when {
@@ -440,25 +521,28 @@ private fun EditCodeValueDialog(
errorId = R.string.settings__theme_editor__code_invalid
showError = true
}
code == codeValue -> {
code.toString() == codeValue -> {
onDismiss()
}
checkExisting(code) -> {
checkExisting(code.toString()) -> {
errorId = R.string.settings__theme_editor__code_already_exists
showError = true
}
else -> {
if (codeValue != NATIVE_NULLPTR.toInt()) {
if (codeValue != KeyCode.UNSPECIFIED.toString()) {
onDelete(codeValue)
}
onAdd(code)
onAdd(code.toString())
onDismiss()
}
}
},
dismissLabel = stringRes(R.string.action__cancel),
onDismiss = onDismiss,
neutralLabel = if (codeValue != NATIVE_NULLPTR.toInt()) {
neutralLabel = if (codeValue != KeyCode.UNSPECIFIED.toString()) {
stringRes(R.string.action__delete)
} else {
null
@@ -505,7 +589,7 @@ private fun EditCodeValueDialog(
LocalTextSelectionColors.current
}
CompositionLocalProvider(LocalTextSelectionColors provides textSelectionColors) {
FlorisOutlinedTextField(
JetPrefTextField(
modifier = Modifier
.focusRequester(focusRequester)
.weight(1f),
@@ -514,7 +598,7 @@ private fun EditCodeValueDialog(
inputCodeString = v
showError = false
},
placeholder = when {
placeholderText = when {
isRecordingKey -> {
stringRes(R.string.settings__theme_editor__code_recording_placeholder)
}
@@ -527,21 +611,25 @@ private fun EditCodeValueDialog(
},
isError = showError,
singleLine = true,
colors = if (isRecordingKey) {
OutlinedTextFieldDefaults.colors(
focusedTextColor = Color.Transparent,
cursorColor = Color.Transparent,
appearance = JetPrefTextFieldDefaults.filled(
colors = if (isRecordingKey) {
TextFieldDefaults.colors(
focusedTextColor = Color.Transparent,
cursorColor = Color.Transparent,
)
} else {
TextFieldDefaults.colors()
}
),
trailingIcon = {
FlorisIconButton(
onClick = { requestStartRecording() },
icon = Icons.Default.Pageview,
iconColor = recordingKeyColor,
)
} else {
OutlinedTextFieldDefaults.colors()
},
}
)
}
FlorisIconButton(
onClick = { requestStartRecording() },
icon = Icons.Default.Pageview,
iconColor = recordingKeyColor,
)
}
AnimatedVisibility(visible = showError) {
Text(
@@ -573,9 +661,12 @@ private fun TextKeyDataPreviewBox(
override val mode = KeyboardMode.NUMERIC_ADVANCED
override fun getKeyForPos(pointerX: Float, pointerY: Float) = error("not implemented")
override fun keys() = error("not implemented")
override fun layout(keyboardWidth: Float, keyboardHeight: Float, desiredKey: Key,
extendTouchBoundariesDownwards: Boolean) = error("not implemented")
override fun layout(
keyboardWidth: Float, keyboardHeight: Float, desiredKey: Key,
extendTouchBoundariesDownwards: Boolean,
) = error("not implemented")
}
override fun context() = context
}
}
@@ -603,20 +694,19 @@ private fun TextKeyDataPreviewBox(
.align(Alignment.CenterVertically),
contentAlignment = Alignment.Center,
) {
if (label != null) {
Text(
text = label,
fontSize = 16.sp,
maxLines = 1,
softWrap = false,
)
}
if (icon != null) {
Icon(
modifier = Modifier.requiredSize(24.dp),
imageVector = icon,
contentDescription = null,
)
} else if (label != null) {
Text(
text = label,
fontSize = 16.sp,
maxLines = 1,
softWrap = false,
)
}
}
Column(modifier = Modifier.weight(1f)) {

View File

@@ -23,9 +23,9 @@ import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.enumDisplayEntriesOf
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.lib.compose.stringRes
import org.florisboard.lib.snygg.SnyggLevel
import dev.patrickgold.jetpref.datastore.ui.ListPreference
import dev.patrickgold.jetpref.datastore.ui.PreferenceLayout
import dev.patrickgold.jetpref.material.ui.ColorRepresentation
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
private val FineTuneContentPadding = PaddingValues(horizontal = 8.dp)
@@ -44,9 +44,9 @@ fun FineTuneDialog(onDismiss: () -> Unit) {
entries = enumDisplayEntriesOf(SnyggLevel::class),
)
ListPreference(
listPref = prefs.theme.editorDisplayColorsAs,
title = stringRes(R.string.settings__theme_editor__fine_tune__display_colors_as),
entries = enumDisplayEntriesOf(DisplayColorsAs::class),
listPref = prefs.theme.editorColorRepresentation,
title = stringRes(R.string.settings__theme_editor__fine_tune__color_representation),
entries = enumDisplayEntriesOf(ColorRepresentation::class),
)
ListPreference(
listPref = prefs.theme.editorDisplayKbdAfterDialogs,

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.florisboard.lib.snygg
package dev.patrickgold.florisboard.app.settings.theme
/**
* SnyggLevel indicates if a rule property is intended to be edited by all users (BASIC) or only by advanced users

View File

@@ -26,29 +26,64 @@ import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.FormatAlignLeft
import androidx.compose.material.icons.automirrored.filled.FormatAlignRight
import androidx.compose.material.icons.automirrored.filled.WrapText
import androidx.compose.material.icons.filled.AttachFile
import androidx.compose.material.icons.filled.CheckBox
import androidx.compose.material.icons.filled.CheckBoxOutlineBlank
import androidx.compose.material.icons.filled.FontDownload
import androidx.compose.material.icons.filled.FormatAlignCenter
import androidx.compose.material.icons.filled.FormatAlignJustify
import androidx.compose.material.icons.filled.FormatBold
import androidx.compose.material.icons.filled.FormatItalic
import androidx.compose.material.icons.filled.FormatSize
import androidx.compose.material.icons.filled.FormatStrikethrough
import androidx.compose.material.icons.filled.FormatUnderlined
import androidx.compose.material.icons.filled.Link
import androidx.compose.material.icons.filled.OpenInFull
import androidx.compose.material.icons.filled.Padding
import androidx.compose.material.icons.filled.Straighten
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.jetpref.datastore.model.observeAsState
import org.florisboard.lib.snygg.value.SnyggCutCornerDpShapeValue
import org.florisboard.lib.snygg.value.SnyggDefinedVarValue
import org.florisboard.lib.snygg.value.SnyggDpSizeValue
import org.florisboard.lib.snygg.value.SnyggMaterialYouValue
import org.florisboard.lib.snygg.value.SnyggRoundedCornerDpShapeValue
import org.florisboard.lib.snygg.value.SnyggShapeValue
import org.florisboard.lib.snygg.value.SnyggSolidColorValue
import org.florisboard.lib.snygg.value.SnyggStaticColorValue
import org.florisboard.lib.snygg.value.SnyggSpSizeValue
import org.florisboard.lib.snygg.value.SnyggValue
import dev.patrickgold.jetpref.material.ui.checkeredBackground
import org.florisboard.lib.color.ColorMappings
import org.florisboard.lib.color.getColor
import org.florisboard.lib.snygg.value.SnyggContentScaleValue
import org.florisboard.lib.snygg.value.SnyggCustomFontFamilyValue
import org.florisboard.lib.snygg.value.SnyggDynamicDarkColorValue
import org.florisboard.lib.snygg.value.SnyggDynamicLightColorValue
import org.florisboard.lib.snygg.value.SnyggFontStyleValue
import org.florisboard.lib.snygg.value.SnyggFontWeightValue
import org.florisboard.lib.snygg.value.SnyggGenericFontFamilyValue
import org.florisboard.lib.snygg.value.SnyggNoValue
import org.florisboard.lib.snygg.value.SnyggPaddingValue
import org.florisboard.lib.snygg.value.SnyggTextAlignValue
import org.florisboard.lib.snygg.value.SnyggTextDecorationLineValue
import org.florisboard.lib.snygg.value.SnyggTextOverflowValue
import org.florisboard.lib.snygg.value.SnyggUriValue
import org.florisboard.lib.snygg.value.SnyggYesValue
object SnyggValueIcon {
interface Spec {
@@ -86,13 +121,51 @@ internal fun SnyggValueIcon(
modifier: Modifier = Modifier,
spec: SnyggValueIcon.Spec = SnyggValueIcon.Normal,
) {
val prefs by florisPreferenceModel()
val context = LocalContext.current
val accentColor by prefs.theme.accentColor.observeAsState()
when (value) {
is SnyggSolidColorValue -> {
is SnyggStaticColorValue -> {
SnyggValueColorBox(modifier = modifier, spec = spec, backgroundColor = value.color)
}
is SnyggDynamicLightColorValue -> {
val colorScheme = ColorMappings.dynamicLightColorScheme(context, accentColor)
SnyggValueColorBox(modifier = modifier, spec = spec, backgroundColor = colorScheme.getColor(value.colorName))
}
is SnyggDynamicDarkColorValue -> {
val colorScheme = ColorMappings.dynamicDarkColorScheme(context, accentColor)
SnyggValueColorBox(modifier = modifier, spec = spec, backgroundColor = colorScheme.getColor(value.colorName))
}
is SnyggMaterialYouValue -> {
SnyggValueColorBox(modifier = modifier, spec = spec, backgroundColor = value.loadColor(LocalContext.current))
is SnyggGenericFontFamilyValue, is SnyggCustomFontFamilyValue -> {
Icon(
modifier = modifier.requiredSize(spec.iconSize),
imageVector = Icons.Default.FontDownload,
contentDescription = null,
)
}
is SnyggFontStyleValue -> {
Icon(
modifier = modifier.requiredSize(spec.iconSize),
imageVector = Icons.Default.FormatItalic,
contentDescription = null,
)
}
is SnyggFontWeightValue -> {
Icon(
modifier = modifier.requiredSize(spec.iconSize),
imageVector = Icons.Default.FormatBold,
contentDescription = null,
)
}
is SnyggPaddingValue -> {
Icon(
modifier = modifier.requiredSize(spec.iconSize),
imageVector = Icons.Default.Padding,
contentDescription = null,
)
}
is SnyggShapeValue -> {
@@ -102,6 +175,7 @@ internal fun SnyggValueIcon(
.border(spec.borderWith, MaterialTheme.colorScheme.onBackground, value.alwaysPercentShape())
)
}
is SnyggDpSizeValue -> {
Icon(
modifier = modifier.requiredSize(spec.iconSize),
@@ -116,6 +190,37 @@ internal fun SnyggValueIcon(
contentDescription = null,
)
}
is SnyggTextAlignValue -> {
Icon(
modifier = modifier.requiredSize(spec.iconSize),
imageVector = when (value.textAlign) {
TextAlign.Left, TextAlign.Start -> Icons.AutoMirrored.Default.FormatAlignLeft
TextAlign.Right, TextAlign.End -> Icons.AutoMirrored.Default.FormatAlignRight
TextAlign.Justify -> Icons.Default.FormatAlignJustify
else -> Icons.Default.FormatAlignCenter
},
contentDescription = null,
)
}
is SnyggTextDecorationLineValue -> {
Icon(
modifier = modifier.requiredSize(spec.iconSize),
imageVector = when (value.textDecoration) {
TextDecoration.LineThrough -> Icons.Default.FormatStrikethrough
else -> Icons.Default.FormatUnderlined
},
contentDescription = null,
)
}
is SnyggTextOverflowValue -> {
Icon(
modifier = modifier.requiredSize(spec.iconSize),
imageVector = Icons.AutoMirrored.Default.WrapText,
contentDescription = null,
)
}
is SnyggDefinedVarValue -> {
val realValue = definedVariables[value.key]
if (realValue == null) {
@@ -150,6 +255,37 @@ internal fun SnyggValueIcon(
}
}
}
is SnyggUriValue -> {
Icon(
modifier = modifier.requiredSize(spec.iconSize),
imageVector = Icons.Default.AttachFile,
contentDescription = null,
)
}
is SnyggContentScaleValue -> {
Icon(
modifier = modifier.requiredSize(spec.iconSize),
imageVector = Icons.Default.OpenInFull,
contentDescription = null,
)
}
is SnyggYesValue -> {
Icon(
modifier = modifier.requiredSize(spec.iconSize),
imageVector = Icons.Default.FormatBold,
contentDescription = null,
)
}
is SnyggNoValue -> {
Icon(
modifier = modifier.requiredSize(spec.iconSize),
imageVector = Icons.Default.CheckBoxOutlineBlank,
contentDescription = null,
)
}
else -> {
// Render nothing
}

View File

@@ -36,11 +36,16 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material.icons.filled.Tune
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
@@ -53,11 +58,8 @@ import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -71,17 +73,18 @@ import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.apptheme.Shapes
import dev.patrickgold.florisboard.app.ext.ExtensionComponentView
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.ime.theme.FlorisImeUiSpec
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponent
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponentEditor
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionEditor
import dev.patrickgold.florisboard.ime.theme.ThemeManager
import dev.patrickgold.florisboard.ime.theme.extPreviewTheme
import dev.patrickgold.florisboard.lib.cache.CacheManager
import dev.patrickgold.florisboard.lib.compose.FlorisIconButton
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedBox
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedTextField
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.PreviewKeyboardField
import dev.patrickgold.florisboard.lib.compose.Validation
import dev.patrickgold.florisboard.lib.compose.defaultFlorisOutlinedBox
import dev.patrickgold.florisboard.lib.compose.florisVerticalScroll
import dev.patrickgold.florisboard.lib.compose.rememberPreviewFieldController
@@ -93,26 +96,46 @@ import dev.patrickgold.florisboard.themeManager
import dev.patrickgold.jetpref.datastore.model.observeAsState
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
import dev.patrickgold.jetpref.material.ui.JetPrefListItem
import dev.patrickgold.jetpref.material.ui.JetPrefTextField
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.florisboard.lib.android.showLongToast
import org.florisboard.lib.kotlin.io.readJson
import org.florisboard.lib.kotlin.io.subFile
import org.florisboard.lib.snygg.SnyggLevel
import org.florisboard.lib.snygg.SnyggPropertySetEditor
import org.florisboard.lib.snygg.SnyggPropertySetSpec
import org.florisboard.lib.snygg.SnyggAnnotationRule
import org.florisboard.lib.snygg.SnyggElementRule
import org.florisboard.lib.snygg.SnyggJsonConfiguration
import org.florisboard.lib.snygg.SnyggMultiplePropertySetsEditor
import org.florisboard.lib.snygg.SnyggRule
import org.florisboard.lib.snygg.SnyggSelector
import org.florisboard.lib.snygg.SnyggSinglePropertySetEditor
import org.florisboard.lib.snygg.SnyggSpec
import org.florisboard.lib.snygg.SnyggSpecDecl
import org.florisboard.lib.snygg.SnyggStylesheet
import org.florisboard.lib.snygg.SnyggStylesheetEditor
import org.florisboard.lib.snygg.SnyggStylesheetJsonConfig
import org.florisboard.lib.snygg.definedVariablesRule
import org.florisboard.lib.snygg.isDefinedVariablesRule
import org.florisboard.lib.snygg.ui.Saver
import kotlin.Boolean
import kotlin.String
internal val IntListSaver = Saver<SnapshotStateList<Int>, ArrayList<Int>>(
save = { ArrayList(it) },
restore = { it.toMutableStateList() },
internal val PrettyPrintConfig = SnyggJsonConfiguration.of(
prettyPrint = true,
prettyPrintIndent = " ",
)
private val LenientConfig = SnyggJsonConfiguration.of(
ignoreMissingSchema = true,
ignoreInvalidSchema = true,
ignoreUnsupportedSchema = true,
ignoreInvalidRules = true,
ignoreInvalidProperties = true,
ignoreInvalidValues = true,
)
private enum class StylesheetLoadingStrategy {
TRY_LOAD_OR_ASK_ON_CONFLICT, // default state
TRY_LOAD_OR_EMPTY, // user chose to not auto-fix errors
TRY_LOAD_OR_PARSE_LENIENT; // user chose to auto-fix errors
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun ThemeEditorScreen(
@@ -130,35 +153,66 @@ fun ThemeEditorScreen(
val scope = rememberCoroutineScope()
val previewFieldController = rememberPreviewFieldController().also { it.isVisible = true }
val stylesheetEditor = remember {
var stylesheetLoadingStrategy by rememberSaveable {
mutableStateOf(StylesheetLoadingStrategy.TRY_LOAD_OR_ASK_ON_CONFLICT)
}
var stylesheetEditorFailure by remember { mutableStateOf<Throwable?>(null) }
val stylesheetEditor = remember(stylesheetLoadingStrategy) {
editor.stylesheetEditor ?: run {
stylesheetEditorFailure = null
val stylesheetPath = editor.stylesheetPath()
editor.stylesheetPathOnLoad = stylesheetPath
val stylesheetFile = workspace.extDir.subFile(stylesheetPath)
val stylesheetEditor = if (stylesheetFile.exists()) {
try {
stylesheetFile.readJson<SnyggStylesheet>(SnyggStylesheetJsonConfig).edit()
} catch (e: Throwable) {
SnyggStylesheetEditor()
val stylesheetJson = stylesheetFile.readText()
val config = when (stylesheetLoadingStrategy) {
StylesheetLoadingStrategy.TRY_LOAD_OR_PARSE_LENIENT -> LenientConfig
else -> PrettyPrintConfig
}
SnyggStylesheet.fromJson(stylesheetJson, config).getOrThrow().edit(CustomRuleComparator)
} catch (error: Throwable) {
stylesheetEditorFailure = when (stylesheetLoadingStrategy) {
StylesheetLoadingStrategy.TRY_LOAD_OR_ASK_ON_CONFLICT -> error
else -> null
}
SnyggStylesheetEditor(SnyggStylesheet.SCHEMA_V2, comparator = CustomRuleComparator)
}
} else {
SnyggStylesheetEditor()
}
if (stylesheetEditor.rules.none { (rule, _) -> rule.isDefinedVariablesRule() }) {
stylesheetEditor.rules[SnyggRule.definedVariablesRule()] = SnyggPropertySetEditor()
SnyggStylesheetEditor(SnyggStylesheet.SCHEMA_V2, comparator = CustomRuleComparator)
}
stylesheetEditor.rules.putIfAbsent(SnyggAnnotationRule.Defines, SnyggSinglePropertySetEditor())
stylesheetEditor
}.also { editor.stylesheetEditor = it }
}
val definedVariables = remember(stylesheetEditor.rules) {
stylesheetEditor.rules.firstNotNullOfOrNull { (rule, propertySet) ->
if (rule is SnyggAnnotationRule.Defines && propertySet is SnyggSinglePropertySetEditor) {
propertySet.properties
} else {
null
}
} ?: emptyMap()
}
val fontNames = remember(stylesheetEditor.rules) {
stylesheetEditor.rules.mapNotNull { (rule, _) ->
if (rule is SnyggAnnotationRule.Font) {
rule.fontName
} else {
null
}
}
}
val snyggLevel by prefs.theme.editorLevel.observeAsState()
val displayColorsAs by prefs.theme.editorDisplayColorsAs.observeAsState()
val colorRepresentation by prefs.theme.editorColorRepresentation.observeAsState()
val displayKbdAfterDialogs by prefs.theme.editorDisplayKbdAfterDialogs.observeAsState()
var oldFocusState by remember { mutableStateOf(false) }
var snyggRuleToEdit by rememberSaveable(stateSaver = SnyggRule.Saver) { mutableStateOf(null) }
var snyggPropertyToEdit by remember { mutableStateOf<PropertyInfo?>(null) }
var snyggPropertySetForEditing = remember<SnyggPropertySetEditor?> { null }
var snyggPropertySetSpecForEditing = remember<SnyggPropertySetSpec?> { null }
var snyggPropertySetForEditing = remember<SnyggSinglePropertySetEditor?> { null }
var showEditComponentMetaDialog by rememberSaveable { mutableStateOf(false) }
var showFineTuneDialog by rememberSaveable { mutableStateOf(false) }
@@ -198,6 +252,33 @@ fun ThemeEditorScreen(
}
content {
stylesheetEditorFailure?.let { failure ->
JetPrefAlertDialog(
title = stringRes(R.string.settings__theme_editor__stylesheet_error_title),
confirmLabel = stringRes(R.string.action__yes),
onConfirm = {
editor.stylesheetEditor = null
stylesheetLoadingStrategy = StylesheetLoadingStrategy.TRY_LOAD_OR_PARSE_LENIENT
},
dismissLabel = stringRes(R.string.action__no),
onDismiss = {
editor.stylesheetEditor = null
stylesheetLoadingStrategy = StylesheetLoadingStrategy.TRY_LOAD_OR_EMPTY
},
) {
Column {
Text(
modifier = Modifier.padding(bottom = 8.dp),
text = failure.message.toString(),
fontStyle = FontStyle.Italic,
)
Text(
text = stringRes(R.string.settings__theme_editor__stylesheet_error_description),
)
}
}
}
BackHandler {
handleBackPress()
}
@@ -229,23 +310,15 @@ fun ThemeEditorScreen(
DisposableEffect(workspace.version) {
themeManager.previewThemeInfo = ThemeManager.ThemeInfo.DEFAULT.copy(
stylesheet = stylesheetEditor.build().compileToFullyQualified(FlorisImeUiSpec),
name = extPreviewTheme(System.currentTimeMillis().toString()),
stylesheet = stylesheetEditor.build(),
loadedDir = workspace.extDir,
)
onDispose {
themeManager.previewThemeInfo = null
}
}
val definedVariables = remember(stylesheetEditor.rules) {
stylesheetEditor.rules.firstNotNullOfOrNull { (rule, propertySet) ->
if (rule.isDefinedVariablesRule()) {
propertySet.properties
} else {
null
}
} ?: emptyMap()
}
// TODO: (priority = low)
// Floris scrollbar does not like lazy lists with non-constant item heights.
// Consider building a custom scrollbar tailored for this list specifically.
@@ -263,7 +336,7 @@ fun ThemeEditorScreen(
onEditBtnClick = { showEditComponentMetaDialog = true },
)
if (stylesheetEditor.rules.isEmpty() ||
(stylesheetEditor.rules.size == 1 && stylesheetEditor.rules.keys.all { it.isDefinedVariablesRule() })
(stylesheetEditor.rules.size == 1 && stylesheetEditor.rules.all { (rule, _) -> rule == SnyggAnnotationRule.Defines })
) {
Text(
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
@@ -274,9 +347,9 @@ fun ThemeEditorScreen(
}
}
items(stylesheetEditor.rules.entries.toList()) { (rule, propertySet) -> key(rule) {
val isVariablesRule = rule.isDefinedVariablesRule()
val propertySetSpec = FlorisImeUiSpec.propertySetSpec(rule.element)
items(stylesheetEditor.rules.toList()) { (rule, propertySet) -> key(rule) {
val propertySetSpec = SnyggSpec.propertySetSpecOf(rule)
val isVariablesRule = rule == SnyggAnnotationRule.Defines
FlorisOutlinedBox(
modifier = Modifier
.padding(vertical = 8.dp, horizontal = 16.dp)
@@ -291,33 +364,129 @@ fun ThemeEditorScreen(
snyggRuleToEdit = rule
},
onAddPropertyBtnClick = {
snyggPropertySetForEditing = propertySet
snyggPropertySetSpecForEditing = propertySetSpec
snyggPropertyToEdit = SnyggEmptyPropertyInfoForAdding
when(propertySet) {
is SnyggMultiplePropertySetsEditor -> {
workspace.update {
propertySet.sets.add(SnyggSinglePropertySetEditor())
}
}
is SnyggSinglePropertySetEditor -> {
snyggPropertySetForEditing = propertySet
snyggPropertyToEdit = SnyggEmptyPropertyInfoForAdding.copy(
rule = rule,
)
}
}
},
)
if (isVariablesRule) {
Text(
modifier = Modifier.padding(bottom = 8.dp, start = 16.dp, end = 16.dp),
text = stringRes(R.string.snygg__rule_element__defines_description),
text = stringRes(R.string.snygg__rule_annotation__defines_description),
style = MaterialTheme.typography.bodyMedium,
fontStyle = FontStyle.Italic,
)
}
for ((propertyName, propertyValue) in propertySet.properties) {
val propertySpec = propertySetSpec?.propertySpec(propertyName)
if (propertySpec != null && propertySpec.level <= snyggLevel || isVariablesRule) {
JetPrefListItem(
modifier = Modifier.rippleClickable {
snyggPropertySetForEditing = propertySet
snyggPropertySetSpecForEditing = propertySetSpec
snyggPropertyToEdit = PropertyInfo(propertyName, propertyValue)
},
text = translatePropertyName(propertyName, snyggLevel),
secondaryText = translatePropertyValue(propertyValue, snyggLevel, displayColorsAs),
singleLineSecondaryText = true,
trailing = { SnyggValueIcon(propertyValue, definedVariables) },
)
@Composable
fun SinglePropertySetEditor(
propertySet: SnyggSinglePropertySetEditor,
) {
for ((propertyName, propertySpec) in propertySetSpec?.properties.orEmpty()) {
if (propertySpec.required && !propertySet.properties.containsKey(propertyName)) {
FlorisOutlinedBox(title = "Errors", modifier = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp)) {
Text(
modifier = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp),
text = "Required property '$propertyName' does not exist",
color = MaterialTheme.colorScheme.error,
)
}
}
}
for ((propertyName, propertyValue) in propertySet.properties) {
if (true /*propertySpec != null && propertySpec.level <= snyggLevel*/ || isVariablesRule) {
JetPrefListItem(
modifier = Modifier.rippleClickable {
snyggPropertySetForEditing = propertySet
snyggPropertyToEdit = PropertyInfo(rule, propertyName, propertyValue)
},
text = context.translatePropertyName(propertyName, snyggLevel),
secondaryText = context.translatePropertyValue(propertyValue, snyggLevel, colorRepresentation),
singleLineSecondaryText = true,
trailing = { SnyggValueIcon(propertyValue, definedVariables) },
)
}
}
}
when (propertySet) {
is SnyggSinglePropertySetEditor -> {
SinglePropertySetEditor(propertySet)
}
is SnyggMultiplePropertySetsEditor -> {
val sets = propertySet.sets
sets.forEachIndexed { propertySetIndex, propertySet ->
key(propertySet.uuid) {
FlorisOutlinedBox(Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp)) {
Row {
Text("Source set", Modifier
.padding(start = 16.dp)
.align(Alignment.CenterVertically))
Spacer(Modifier.weight(1f))
FlorisIconButton(
onClick = {
workspace.update {
if (propertySetIndex > 0) {
val set = sets.removeAt(propertySetIndex)
sets.add(propertySetIndex - 1, set)
}
}
},
icon = Icons.Default.KeyboardArrowUp,
iconColor = MaterialTheme.colorScheme.primary,
iconModifier = Modifier.size(ButtonDefaults.IconSize),
enabled = propertySetIndex > 0,
)
FlorisIconButton(
onClick = {
workspace.update {
if (propertySetIndex + 1 < sets.size) {
val set = sets.removeAt(propertySetIndex)
sets.add(propertySetIndex + 1, set)
}
}
},
icon = Icons.Default.KeyboardArrowDown,
iconColor = MaterialTheme.colorScheme.primary,
iconModifier = Modifier.size(ButtonDefaults.IconSize),
enabled = propertySetIndex + 1 < sets.size,
)
FlorisIconButton(
onClick = {
workspace.update {
sets.removeAt(propertySetIndex)
}
},
icon = Icons.Default.Delete,
iconColor = MaterialTheme.colorScheme.primary,
iconModifier = Modifier.size(ButtonDefaults.IconSize),
)
FlorisIconButton(
onClick = {
snyggPropertySetForEditing = propertySet
snyggPropertyToEdit = SnyggEmptyPropertyInfoForAdding.copy(
rule = rule,
)
},
icon = Icons.Default.Add,
iconColor = MaterialTheme.colorScheme.primary,
iconModifier = Modifier.size(ButtonDefaults.IconSize),
)
}
SinglePropertySetEditor(propertySet)
}
}
}
}
}
}
@@ -369,7 +538,14 @@ fun ThemeEditorScreen(
true
}
oldRule == SnyggEmptyRuleForAdding -> {
rules[newRule] = SnyggPropertySetEditor()
when (SnyggSpec.propertySetSpecOf(newRule)!!.type) {
SnyggSpecDecl.PropertySet.Type.SINGLE_SET -> {
rules[newRule] = SnyggSinglePropertySetEditor()
}
SnyggSpecDecl.PropertySet.Type.MULTIPLE_SETS -> {
rules[newRule] = SnyggMultiplePropertySetsEditor()
}
}
snyggRuleToEdit = null
scope.launch {
lazyListState.animateScrollToItem(index = rules.keys.indexOf(newRule))
@@ -396,11 +572,12 @@ fun ThemeEditorScreen(
val propertyToEdit = snyggPropertyToEdit
if (propertyToEdit != null) {
EditPropertyDialog(
propertySetSpec = snyggPropertySetSpecForEditing,
initProperty = propertyToEdit,
level = snyggLevel,
displayColorsAs = displayColorsAs,
colorRepresentation = colorRepresentation,
definedVariables = definedVariables,
fontNames = fontNames,
workspace = workspace,
onConfirmNewValue = { name, value ->
val properties = snyggPropertySetForEditing?.properties ?: return@EditPropertyDialog false
if (propertyToEdit == SnyggEmptyPropertyInfoForAdding && properties.containsKey(name)) {
@@ -441,8 +618,6 @@ private fun ComponentMetaEditorDialog(
var authors by rememberSaveable { mutableStateOf(editor.authors.joinToString("\n")) }
val authorsValidation = rememberValidationResult(ExtensionValidation.ComponentAuthors, authors)
var isNightTheme by rememberSaveable { mutableStateOf(editor.isNightTheme) }
var isBorderless by rememberSaveable { mutableStateOf(editor.isBorderless) }
val isMaterialYouAware by rememberSaveable { mutableStateOf(editor.isMaterialYouAware) }
var stylesheetPath by rememberSaveable { mutableStateOf(editor.stylesheetPath) }
val stylesheetPathValidation = rememberValidationResult(ExtensionValidation.ThemeComponentStylesheetPath, stylesheetPath)
@@ -464,8 +639,6 @@ private fun ComponentMetaEditorDialog(
editor.label = label.trim()
editor.authors = authors.lines().map { it.trim() }.filter { it.isNotBlank() }
editor.isNightTheme = isNightTheme
editor.isBorderless = isBorderless
editor.isMaterialYouAware = isMaterialYouAware
editor.stylesheetPath = stylesheetPath.trim()
}
onConfirm()
@@ -477,31 +650,28 @@ private fun ComponentMetaEditorDialog(
) {
Column {
DialogProperty(text = stringRes(R.string.ext__meta__id)) {
FlorisOutlinedTextField(
JetPrefTextField(
value = id,
onValueChange = { id = it },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
singleLine = true,
showValidationError = showValidationErrors,
validationResult = idValidation,
)
Validation(showValidationErrors, idValidation)
}
DialogProperty(text = stringRes(R.string.ext__meta__label)) {
FlorisOutlinedTextField(
JetPrefTextField(
value = label,
onValueChange = { label = it },
singleLine = true,
showValidationError = showValidationErrors,
validationResult = labelValidation,
)
Validation(showValidationErrors, labelValidation)
}
DialogProperty(text = stringRes(R.string.ext__meta__authors)) {
FlorisOutlinedTextField(
JetPrefTextField(
value = authors,
onValueChange = { authors = it },
showValidationError = showValidationErrors,
validationResult = authorsValidation,
)
Validation(showValidationErrors, authorsValidation)
}
JetPrefListItem(
modifier = Modifier.toggleable(isNightTheme) { isNightTheme = it },
@@ -509,28 +679,21 @@ private fun ComponentMetaEditorDialog(
trailing = {
Switch(checked = isNightTheme, onCheckedChange = null)
},
)
JetPrefListItem(
modifier = Modifier.toggleable(isBorderless) { isBorderless = it },
text = stringRes(R.string.settings__theme_editor__component_meta_is_borderless),
trailing = {
Switch(checked = isBorderless, onCheckedChange = null)
},
colors = ListItemDefaults.colors(containerColor = AlertDialogDefaults.containerColor)
)
DialogProperty(text = stringRes(R.string.settings__theme_editor__component_meta_stylesheet_path)) {
FlorisOutlinedTextField(
JetPrefTextField(
value = stylesheetPath,
onValueChange = { stylesheetPath = it },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
singleLine = true,
placeholder = if (stylesheetPath.isEmpty()) {
placeholderText = if (stylesheetPath.isEmpty()) {
ThemeExtensionComponent.defaultStylesheetPath(id.trim())
} else {
null
},
showValidationError = showValidationErrors,
validationResult = stylesheetPathValidation,
)
Validation(showValidationErrors, stylesheetPathValidation)
}
}
}
@@ -544,6 +707,8 @@ private fun SnyggRuleRow(
onEditRuleBtnClick: () -> Unit,
onAddPropertyBtnClick: () -> Unit,
) {
val context = LocalContext.current
@Composable
fun Selector(text: String) {
Text(
@@ -580,38 +745,53 @@ private fun SnyggRuleRow(
.weight(1f)
.padding(vertical = 8.dp, horizontal = 10.dp),
) {
Text(
text = translateElementName(rule, level),
style = MaterialTheme.typography.bodyMedium,
fontFamily = FontFamily.Monospace,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Row(modifier = Modifier.fillMaxWidth()) {
if (rule.pressedSelector) {
Selector(text = when (level) {
SnyggLevel.DEVELOPER -> SnyggRule.PRESSED_SELECTOR
else -> stringRes(R.string.snygg__rule_selector__pressed)
})
if (rule is SnyggElementRule) {
Text(
text = context.translateElementName(rule, level),
style = MaterialTheme.typography.bodyMedium,
fontFamily = FontFamily.Monospace,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Row(modifier = Modifier.fillMaxWidth()) {
if (rule.selector == SnyggSelector.PRESSED) {
Selector(
text = when (level) {
SnyggLevel.DEVELOPER -> SnyggSelector.PRESSED.id
else -> stringRes(R.string.snygg__rule_selector__pressed)
}
)
}
if (rule.selector == SnyggSelector.FOCUS) {
Selector(
text = when (level) {
SnyggLevel.DEVELOPER -> SnyggSelector.FOCUS.id
else -> stringRes(R.string.snygg__rule_selector__focus)
}
)
}
if (rule.selector == SnyggSelector.HOVER) {
Selector(
text = when (level) {
SnyggLevel.DEVELOPER -> SnyggSelector.HOVER.id
else -> stringRes(R.string.snygg__rule_selector__hover)
}
)
}
if (rule.selector == SnyggSelector.DISABLED) {
Selector(
text = when (level) {
SnyggLevel.DEVELOPER -> SnyggSelector.DISABLED.id
else -> stringRes(R.string.snygg__rule_selector__disabled)
}
)
}
}
if (rule.focusSelector) {
Selector(text = when (level) {
SnyggLevel.DEVELOPER -> SnyggRule.FOCUS_SELECTOR
else -> stringRes(R.string.snygg__rule_selector__focus)
})
for ((attrKey, attrValue) in rule.attributes) {
AttributesList(text = attrKey, list = attrValue.toString())
}
if (rule.disabledSelector) {
Selector(text = when (level) {
SnyggLevel.DEVELOPER -> SnyggRule.DISABLED_SELECTOR
else -> stringRes(R.string.snygg__rule_selector__disabled)
})
}
}
if (rule.codes.isNotEmpty()) {
AttributesList(text = "code", list = remember(rule.codes) { rule.codes.toString() })
}
if (rule.shiftStates.isNotEmpty()) {
AttributesList(text = "shiftstate", list = remember(rule.shiftStates) { rule.shiftStates.toString() })
} else {
Text(text = rule.toString())
}
}
if (showEditBtn) {
@@ -645,10 +825,31 @@ internal fun DialogProperty(
.weight(1f)
.padding(vertical = 8.dp),
text = text,
style = MaterialTheme.typography.titleSmall,
style = MaterialTheme.typography.titleMedium,
)
trailingIconTitle()
}
content()
}
}
private object CustomRuleComparator : Comparator<SnyggRule> {
@Suppress("IfThenToElvis")
override fun compare(a: SnyggRule, b: SnyggRule): Int {
return if (a !is SnyggElementRule || b !is SnyggElementRule || a.elementName == b.elementName) {
a.compareTo(b)
} else {
val aOrdinal = FlorisImeUi.elementNamesToOrdinals[a.elementName]
val bOrdinal = FlorisImeUi.elementNamesToOrdinals[b.elementName]
if (aOrdinal == null && bOrdinal == null) {
a.elementName.compareTo(b.elementName)
} else if (bOrdinal == null) {
-1
} else if (aOrdinal == null) {
1
} else {
aOrdinal.compareTo(bOrdinal)
}
}
}
}

View File

@@ -19,12 +19,14 @@ package dev.patrickgold.florisboard.app.settings.theme
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BrightnessAuto
import androidx.compose.material.icons.filled.ColorLens
import androidx.compose.material.icons.filled.DarkMode
import androidx.compose.material.icons.filled.LightMode
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.R
@@ -41,8 +43,11 @@ import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
import dev.patrickgold.florisboard.themeManager
import dev.patrickgold.jetpref.datastore.model.observeAsState
import dev.patrickgold.jetpref.datastore.ui.ColorPickerPreference
import dev.patrickgold.jetpref.datastore.ui.ListPreference
import dev.patrickgold.jetpref.datastore.ui.Preference
import dev.patrickgold.jetpref.datastore.ui.isMaterialYou
import org.florisboard.lib.color.ColorMappings
@Composable
fun ThemeScreen() = FlorisScreen {
@@ -108,6 +113,22 @@ fun ThemeScreen() = FlorisScreen {
navController.navigate(Routes.Settings.ThemeManager(ThemeManagerScreenAction.SELECT_NIGHT))
},
)
ColorPickerPreference(
pref = prefs.theme.accentColor,
title = stringRes(R.string.pref__theme__theme_accent_color__label),
defaultValueLabel = stringRes(R.string.action__default),
icon = Icons.Default.ColorLens,
defaultColors = ColorMappings.colors,
showAlphaSlider = false,
enableAdvancedLayout = false,
colorOverride = {
if (it.isMaterialYou(context)) {
Color.Unspecified
} else {
it
}
}
)
AddonManagementReferenceBox(type = ExtensionListScreenType.EXT_THEME)
}

View File

@@ -16,134 +16,108 @@
package dev.patrickgold.florisboard.app.settings.theme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import android.content.Context
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.lib.UnicodeCtrlChar
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.jetpref.material.ui.ColorRepresentation
import org.florisboard.lib.kotlin.simpleNameOrEnclosing
import org.florisboard.lib.snygg.Snygg
import org.florisboard.lib.snygg.SnyggLevel
import org.florisboard.lib.snygg.SnyggRule
import org.florisboard.lib.snygg.value.RgbaColor
import org.florisboard.lib.snygg.SnyggElementRule
import org.florisboard.lib.snygg.value.SnyggCircleShapeValue
import org.florisboard.lib.snygg.value.SnyggCustomFontFamilyValue
import org.florisboard.lib.snygg.value.SnyggCutCornerDpShapeValue
import org.florisboard.lib.snygg.value.SnyggCutCornerPercentShapeValue
import org.florisboard.lib.snygg.value.SnyggDefinedVarValue
import org.florisboard.lib.snygg.value.SnyggDpSizeValue
import org.florisboard.lib.snygg.value.SnyggExplicitInheritValue
import org.florisboard.lib.snygg.value.SnyggImplicitInheritValue
import org.florisboard.lib.snygg.value.SnyggMaterialYouDarkColorValue
import org.florisboard.lib.snygg.value.SnyggMaterialYouLightColorValue
import org.florisboard.lib.snygg.value.SnyggDynamicDarkColorValue
import org.florisboard.lib.snygg.value.SnyggDynamicLightColorValue
import org.florisboard.lib.snygg.value.SnyggFontStyleValue
import org.florisboard.lib.snygg.value.SnyggFontWeightValue
import org.florisboard.lib.snygg.value.SnyggGenericFontFamilyValue
import org.florisboard.lib.snygg.value.SnyggInheritValue
import org.florisboard.lib.snygg.value.SnyggNoValue
import org.florisboard.lib.snygg.value.SnyggContentScaleValue
import org.florisboard.lib.snygg.value.SnyggPaddingValue
import org.florisboard.lib.snygg.value.SnyggPercentageSizeValue
import org.florisboard.lib.snygg.value.SnyggRectangleShapeValue
import org.florisboard.lib.snygg.value.SnyggRoundedCornerDpShapeValue
import org.florisboard.lib.snygg.value.SnyggRoundedCornerPercentShapeValue
import org.florisboard.lib.snygg.value.SnyggSolidColorValue
import org.florisboard.lib.snygg.value.SnyggSpSizeValue
import org.florisboard.lib.snygg.value.SnyggStaticColorValue
import org.florisboard.lib.snygg.value.SnyggTextAlignValue
import org.florisboard.lib.snygg.value.SnyggTextDecorationLineValue
import org.florisboard.lib.snygg.value.SnyggTextMaxLinesValue
import org.florisboard.lib.snygg.value.SnyggTextOverflowValue
import org.florisboard.lib.snygg.value.SnyggUndefinedValue
import org.florisboard.lib.snygg.value.SnyggUriValue
import org.florisboard.lib.snygg.value.SnyggValue
import org.florisboard.lib.snygg.value.SnyggValueEncoder
import kotlin.math.roundToInt
import org.florisboard.lib.snygg.value.SnyggYesValue
@Composable
internal fun translateElementName(rule: SnyggRule, level: SnyggLevel): String {
return translateElementName(rule.element, level) ?: remember {
buildString {
if (rule.isAnnotation) {
append(SnyggRule.ANNOTATION_MARKER)
}
append(rule.element)
}
internal fun Context.translateElementName(rule: SnyggElementRule, level: SnyggLevel): String {
return translateElementName(rule.elementName, level) ?: rule.elementName
}
internal fun Context.translateElementName(element: String, level: SnyggLevel): String? {
return when (level) {
SnyggLevel.DEVELOPER -> null
else -> FlorisImeUi.elementNamesToTranslation[element]?.let { getString(it) }
}
}
@Composable
internal fun translateElementName(element: String, level: SnyggLevel): String? {
return when (level) {
SnyggLevel.DEVELOPER -> null
else -> when (element) {
"defines" -> R.string.snygg__rule_element__defines
FlorisImeUi.Keyboard -> R.string.snygg__rule_element__keyboard
FlorisImeUi.Key -> R.string.snygg__rule_element__key
FlorisImeUi.KeyHint -> R.string.snygg__rule_element__key_hint
FlorisImeUi.KeyPopup -> R.string.snygg__rule_element__key_popup
FlorisImeUi.ClipboardHeader -> R.string.snygg__rule_element__clipboard_header
FlorisImeUi.ClipboardItem -> R.string.snygg__rule_element__clipboard_item
FlorisImeUi.ClipboardItemPopup -> R.string.snygg__rule_element__clipboard_item_popup
FlorisImeUi.EmojiKey -> R.string.snygg__rule_element__emoji_key
FlorisImeUi.EmojiKeyPopup -> R.string.snygg__rule_element__emoji_key_popup
FlorisImeUi.EmojiTab -> R.string.snygg__rule_element__emoji_key_tab
FlorisImeUi.ExtractedLandscapeInputLayout -> R.string.snygg__rule_element__extracted_landscape_input_layout
FlorisImeUi.ExtractedLandscapeInputField -> R.string.snygg__rule_element__extracted_landscape_input_field
FlorisImeUi.ExtractedLandscapeInputAction -> R.string.snygg__rule_element__extracted_landscape_input_action
FlorisImeUi.GlideTrail -> R.string.snygg__rule_element__glide_trail
FlorisImeUi.IncognitoModeIndicator -> R.string.snygg__rule_element__incognito_mode_indicator
FlorisImeUi.OneHandedPanel -> R.string.snygg__rule_element__one_handed_panel
FlorisImeUi.Smartbar -> R.string.snygg__rule_element__smartbar
FlorisImeUi.SmartbarSharedActionsRow -> R.string.snygg__rule_element__smartbar_shared_actions_row
FlorisImeUi.SmartbarSharedActionsToggle -> R.string.snygg__rule_element__smartbar_shared_actions_toggle
FlorisImeUi.SmartbarExtendedActionsRow -> R.string.snygg__rule_element__smartbar_extended_actions_row
FlorisImeUi.SmartbarExtendedActionsToggle -> R.string.snygg__rule_element__smartbar_extended_actions_toggle
FlorisImeUi.SmartbarActionKey -> R.string.snygg__rule_element__smartbar_action_key
FlorisImeUi.SmartbarActionTile -> R.string.snygg__rule_element__smartbar_action_tile
FlorisImeUi.SmartbarActionsOverflow -> R.string.snygg__rule_element__smartbar_actions_overflow
FlorisImeUi.SmartbarActionsOverflowCustomizeButton -> R.string.snygg__rule_element__smartbar_actions_overflow_customize_button
FlorisImeUi.SmartbarActionsEditor -> R.string.snygg__rule_element__smartbar_actions_editor
FlorisImeUi.SmartbarActionsEditorHeader -> R.string.snygg__rule_element__smartbar_actions_editor_header
FlorisImeUi.SmartbarActionsEditorSubheader -> R.string.snygg__rule_element__smartbar_actions_editor_subheader
FlorisImeUi.SmartbarCandidatesRow -> R.string.snygg__rule_element__smartbar_candidates_row
FlorisImeUi.SmartbarCandidateWord -> R.string.snygg__rule_element__smartbar_candidate_word
FlorisImeUi.SmartbarCandidateClip -> R.string.snygg__rule_element__smartbar_candidate_clip
FlorisImeUi.SmartbarCandidateSpacer -> R.string.snygg__rule_element__smartbar_candidate_spacer
FlorisImeUi.SystemNavBar -> R.string.snygg__rule_element__system_nav_bar
else -> null
}
}.let { if (it != null) { stringRes(it) } else { null } }
}
private val PropertyNameMap = mapOf(
Snygg.Background to R.string.snygg__property_name__background,
Snygg.Foreground to R.string.snygg__property_name__foreground,
Snygg.BackgroundImage to R.string.snygg__property_name__background_image,
Snygg.ContentScale to R.string.snygg__property_name__content_scale,
Snygg.BorderColor to R.string.snygg__property_name__border_color,
Snygg.BorderStyle to R.string.snygg__property_name__border_style,
Snygg.BorderWidth to R.string.snygg__property_name__border_width,
Snygg.FontFamily to R.string.snygg__property_name__font_family,
Snygg.FontSize to R.string.snygg__property_name__font_size,
Snygg.FontStyle to R.string.snygg__property_name__font_style,
Snygg.FontWeight to R.string.snygg__property_name__font_weight,
Snygg.LetterSpacing to R.string.snygg__property_name__letter_spacing,
Snygg.LineHeight to R.string.snygg__property_name__line_height,
Snygg.Margin to R.string.snygg__property_name__margin,
Snygg.Padding to R.string.snygg__property_name__padding,
Snygg.ShadowColor to R.string.snygg__property_name__shadow_color,
Snygg.ShadowElevation to R.string.snygg__property_name__shadow_elevation,
Snygg.Shape to R.string.snygg__property_name__shape,
Snygg.Clip to R.string.snygg__property_name__clip,
Snygg.Src to R.string.snygg__property_name__src,
Snygg.TextAlign to R.string.snygg__property_name__text_align,
Snygg.TextDecorationLine to R.string.snygg__property_name__text_decoration_line,
Snygg.TextMaxLines to R.string.snygg__property_name__text_max_lines,
Snygg.TextOverflow to R.string.snygg__property_name__text_overflow,
"--primary" to R.string.snygg__property_name__var_primary,
"--primary-variant" to R.string.snygg__property_name__var_primary_variant,
"--secondary" to R.string.snygg__property_name__var_secondary,
"--secondary-variant" to R.string.snygg__property_name__var_secondary_variant,
"--background" to R.string.snygg__property_name__var_background,
"--surface" to R.string.snygg__property_name__var_surface,
"--surface-variant" to R.string.snygg__property_name__var_surface_variant,
"--on-primary" to R.string.snygg__property_name__var_on_primary,
"--on-secondary" to R.string.snygg__property_name__var_on_secondary,
"--on-background" to R.string.snygg__property_name__var_on_background,
"--on-surface" to R.string.snygg__property_name__var_on_surface,
"--on-surface-variant" to R.string.snygg__property_name__var_on_surface_variant,
"--shape" to R.string.snygg__property_name__var_shape,
"--shape-variant" to R.string.snygg__property_name__var_shape_variant
)
@Composable
internal fun translatePropertyName(propertyName: String, level: SnyggLevel): String {
internal fun Context.translatePropertyName(propertyName: String, level: SnyggLevel): String {
return when (level) {
SnyggLevel.DEVELOPER -> null
else -> when (propertyName) {
Snygg.Width -> R.string.snygg__property_name__width
Snygg.Height -> R.string.snygg__property_name__height
Snygg.Background -> R.string.snygg__property_name__background
Snygg.Foreground -> R.string.snygg__property_name__foreground
Snygg.BorderColor -> R.string.snygg__property_name__border_color
Snygg.BorderStyle -> R.string.snygg__property_name__border_style
Snygg.BorderWidth -> R.string.snygg__property_name__border_width
Snygg.FontFamily -> R.string.snygg__property_name__font_family
Snygg.FontSize -> R.string.snygg__property_name__font_size
Snygg.FontStyle -> R.string.snygg__property_name__font_style
Snygg.FontVariant -> R.string.snygg__property_name__font_variant
Snygg.FontWeight -> R.string.snygg__property_name__font_weight
Snygg.ShadowElevation -> R.string.snygg__property_name__shadow_elevation
Snygg.Shape -> R.string.snygg__property_name__shape
"--primary" -> R.string.snygg__property_name__var_primary
"--primary-variant" -> R.string.snygg__property_name__var_primary_variant
"--secondary" -> R.string.snygg__property_name__var_secondary
"--secondary-variant" -> R.string.snygg__property_name__var_secondary_variant
"--background" -> R.string.snygg__property_name__var_background
"--surface" -> R.string.snygg__property_name__var_surface
"--surface-variant" -> R.string.snygg__property_name__var_surface_variant
"--on-primary" -> R.string.snygg__property_name__var_on_primary
"--on-secondary" -> R.string.snygg__property_name__var_on_secondary
"--on-background" -> R.string.snygg__property_name__var_on_background
"--on-surface" -> R.string.snygg__property_name__var_on_surface
"--on-surface-variant" -> R.string.snygg__property_name__var_on_surface_variant
"--shape" -> R.string.snygg__property_name__var_shape
"--shape-variant" -> R.string.snygg__property_name__var_shape_variant
else -> null
}
else -> PropertyNameMap[propertyName]
}.let { resId ->
when {
resId != null -> {
stringRes(resId)
getString(resId)
}
propertyName.isBlank() -> {
stringRes(R.string.general__select_dropdown_value_placeholder)
getString(R.string.general__select_dropdown_value_placeholder)
}
else -> {
propertyName
@@ -152,15 +126,14 @@ internal fun translatePropertyName(propertyName: String, level: SnyggLevel): Str
}
}
@Composable
internal fun translatePropertyValue(
internal fun Context.translatePropertyValue(
propertyValue: SnyggValue,
level: SnyggLevel,
displayColorsAs: DisplayColorsAs,
colorRepresentation: ColorRepresentation,
): String {
return when (propertyValue) {
is SnyggSolidColorValue -> remember(propertyValue.color, displayColorsAs) {
buildColorString(propertyValue.color, displayColorsAs)
is SnyggStaticColorValue -> {
colorRepresentation.formatColor(propertyValue.color, withAlpha = true)
}
else -> when (level) {
SnyggLevel.DEVELOPER -> null
@@ -176,51 +149,38 @@ internal fun translatePropertyValue(
}
}
internal fun buildColorString(color: Color, displayColorsAs: DisplayColorsAs): String {
return when (displayColorsAs) {
DisplayColorsAs.HEX8 -> buildString {
append(UnicodeCtrlChar.LeftToRightIsolate)
append("#")
append((color.red * RgbaColor.RedMax).roundToInt().toString(16).padStart(2, '0'))
append((color.green * RgbaColor.GreenMax).roundToInt().toString(16).padStart(2, '0'))
append((color.blue * RgbaColor.BlueMax).roundToInt().toString(16).padStart(2, '0'))
append((color.alpha * 0xFF).roundToInt().toString(16).padStart(2, '0'))
append(UnicodeCtrlChar.PopDirectionalIsolate)
}
DisplayColorsAs.RGBA -> buildString {
append(UnicodeCtrlChar.LeftToRightIsolate)
append("rgba(")
append((color.red * RgbaColor.RedMax).roundToInt())
append(",")
append((color.green * RgbaColor.GreenMax).roundToInt())
append(",")
append((color.blue * RgbaColor.BlueMax).roundToInt())
append(",")
append(color.alpha)
append(")")
append(UnicodeCtrlChar.PopDirectionalIsolate)
}
}
}
private val PropertyValueEncoderNameMap = mapOf(
SnyggUndefinedValue to R.string.general__select_dropdown_value_placeholder,
SnyggInheritValue to R.string.snygg__property_value__explicit_inherit,
SnyggDefinedVarValue to R.string.snygg__property_value__defined_var,
SnyggYesValue to R.string.snygg__property_value__yes,
SnyggNoValue to R.string.snygg__property_value__no,
SnyggStaticColorValue to R.string.snygg__property_value__solid_color,
SnyggDynamicLightColorValue to R.string.snygg__property_value__material_you_light_color,
SnyggDynamicDarkColorValue to R.string.snygg__property_value__material_you_dark_color,
SnyggGenericFontFamilyValue to R.string.snygg__property_value__font_family_generic,
SnyggCustomFontFamilyValue to R.string.snygg__property_value__font_family_custom,
SnyggFontStyleValue to R.string.snygg__property_value__font_style,
SnyggFontWeightValue to R.string.snygg__property_value__font_weight,
SnyggPaddingValue to R.string.snygg__property_value__padding,
SnyggRectangleShapeValue to R.string.snygg__property_value__rectangle_shape,
SnyggCircleShapeValue to R.string.snygg__property_value__circle_shape,
SnyggCutCornerDpShapeValue to R.string.snygg__property_value__cut_corner_shape_dp,
SnyggCutCornerPercentShapeValue to R.string.snygg__property_value__cut_corner_shape_percent,
SnyggRoundedCornerDpShapeValue to R.string.snygg__property_value__rounded_corner_shape_dp,
SnyggRoundedCornerPercentShapeValue to R.string.snygg__property_value__rounded_corner_shape_percent,
SnyggDpSizeValue to R.string.snygg__property_value__dp_size,
SnyggSpSizeValue to R.string.snygg__property_value__sp_size,
SnyggPercentageSizeValue to R.string.snygg__property_value__percentage_size,
SnyggContentScaleValue to R.string.snygg__property_value__content_scale,
SnyggTextAlignValue to R.string.snygg__property_value__text_align,
SnyggTextDecorationLineValue to R.string.snygg__property_value__text_decoration_line,
SnyggTextMaxLinesValue to R.string.snygg__property_value__text_max_lines,
SnyggTextOverflowValue to R.string.snygg__property_value__text_overflow,
SnyggUriValue to R.string.snygg__property_value__uri,
)
@Composable
internal fun translatePropertyValueEncoderName(encoder: SnyggValueEncoder): String {
return when (encoder) {
SnyggImplicitInheritValue -> R.string.general__select_dropdown_value_placeholder
SnyggExplicitInheritValue -> R.string.snygg__property_value__explicit_inherit
SnyggSolidColorValue -> R.string.snygg__property_value__solid_color
SnyggMaterialYouLightColorValue -> R.string.snygg__property_value__material_you_light_color
SnyggMaterialYouDarkColorValue -> R.string.snygg__property_value__material_you_dark_color
SnyggRectangleShapeValue -> R.string.snygg__property_value__rectangle_shape
SnyggCircleShapeValue -> R.string.snygg__property_value__circle_shape
SnyggCutCornerDpShapeValue -> R.string.snygg__property_value__cut_corner_shape_dp
SnyggCutCornerPercentShapeValue -> R.string.snygg__property_value__cut_corner_shape_percent
SnyggRoundedCornerDpShapeValue -> R.string.snygg__property_value__rounded_corner_shape_dp
SnyggRoundedCornerPercentShapeValue -> R.string.snygg__property_value__rounded_corner_shape_percent
SnyggDpSizeValue -> R.string.snygg__property_value__dp_size
SnyggSpSizeValue -> R.string.snygg__property_value__sp_size
SnyggPercentageSizeValue -> R.string.snygg__property_value__percentage_size
SnyggDefinedVarValue -> R.string.snygg__property_value__defined_var
else -> null
}.let { if (it != null) { stringRes(it) } else { encoder::class.simpleName ?: "" } }.toString()
internal fun Context.translatePropertyValueEncoderName(encoder: SnyggValueEncoder): String {
return PropertyValueEncoderNameMap[encoder]?.let { getString(it) }
?: encoder::class.simpleNameOrEnclosing().orEmpty()
}

View File

@@ -35,6 +35,7 @@ import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.Routes
import dev.patrickgold.florisboard.app.enumDisplayEntriesOf
import dev.patrickgold.florisboard.ime.keyboard.IncognitoMode
import dev.patrickgold.florisboard.ime.nlp.SpellingLanguageMode
import dev.patrickgold.florisboard.lib.compose.FlorisErrorCard
import dev.patrickgold.florisboard.lib.compose.FlorisHyperlinkText
@@ -47,6 +48,7 @@ import dev.patrickgold.jetpref.datastore.ui.ListPreference
import dev.patrickgold.jetpref.datastore.ui.Preference
import dev.patrickgold.jetpref.datastore.ui.PreferenceGroup
import dev.patrickgold.jetpref.datastore.ui.SwitchPreference
import dev.patrickgold.jetpref.datastore.ui.vectorResource
import org.florisboard.lib.android.AndroidVersion
@OptIn(ExperimentalJetPrefDatastoreUi::class)
@@ -79,28 +81,18 @@ fun TypingScreen() = FlorisScreen {
summary = stringRes(R.string.pref__suggestion__block_possibly_offensive__summary),
enabledIf = { prefs.suggestion.enabled isEqualTo true },
)
SwitchPreference(
prefs.suggestion.clipboardContentEnabled,
title = stringRes(R.string.pref__suggestion__clipboard_content_enabled__label),
summary = stringRes(R.string.pref__suggestion__clipboard_content_enabled__summary),
enabledIf = { prefs.suggestion.enabled isEqualTo true },
)
DialogSliderPreference(
prefs.suggestion.clipboardContentTimeout,
title = stringRes(R.string.pref__suggestion__clipboard_content_timeout__label),
valueLabel = { stringRes(R.string.pref__suggestion__clipboard_content_timeout__summary, "v" to it) },
min = 30,
max = 300,
stepIncrement = 5,
enabledIf = { prefs.suggestion.enabled isEqualTo true },
visibleIf = { prefs.suggestion.clipboardContentEnabled isEqualTo true },
)
SwitchPreference(
prefs.suggestion.api30InlineSuggestionsEnabled,
title = stringRes(R.string.pref__suggestion__api30_inline_suggestions_enabled__label),
summary = stringRes(R.string.pref__suggestion__api30_inline_suggestions_enabled__summary),
visibleIf = { AndroidVersion.ATLEAST_API30_R },
)
ListPreference(
prefs.suggestion.incognitoMode,
icon = vectorResource(id = R.drawable.ic_incognito),
title = stringRes(R.string.pref__suggestion__incognito_mode__label),
entries = enumDisplayEntriesOf(IncognitoMode::class),
)
}
PreferenceGroup(title = stringRes(R.string.pref__correction__title)) {

View File

@@ -45,9 +45,6 @@ import dev.patrickgold.florisboard.app.FlorisAppActivity
import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.Routes
import dev.patrickgold.florisboard.app.florisPreferenceModel
import org.florisboard.lib.android.AndroidVersion
import dev.patrickgold.florisboard.lib.util.launchActivity
import dev.patrickgold.florisboard.lib.util.launchUrl
import dev.patrickgold.florisboard.lib.compose.FlorisBulletSpacer
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.FlorisScreenScope
@@ -56,9 +53,12 @@ import dev.patrickgold.florisboard.lib.compose.FlorisStepLayout
import dev.patrickgold.florisboard.lib.compose.FlorisStepState
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.util.InputMethodUtils
import dev.patrickgold.florisboard.lib.util.launchActivity
import dev.patrickgold.florisboard.lib.util.launchUrl
import dev.patrickgold.jetpref.datastore.model.observeAsState
import dev.patrickgold.jetpref.datastore.ui.PreferenceUiScope
import kotlinx.coroutines.delay
import org.florisboard.lib.android.AndroidVersion
@Composable
@@ -105,128 +105,65 @@ private fun FlorisScreenScope.content(
hasNotificationPermission: NotificationPermissionState,
) {
// Show screen without notification permission if the android version is below android 13.
if (AndroidVersion.ATMOST_API32_S_V2) {
val stepState = rememberSaveable(saver = FlorisStepState.Saver) {
val initStep = when {
!isFlorisBoardEnabled -> Steps.EnableIme.id
!isFlorisBoardSelected -> Steps.SelectIme.id
hasNotificationPermission == NotificationPermissionState.NOT_SET && AndroidVersion.ATLEAST_API33_T -> Steps.SelectNotification.id
else -> Steps.FinishUp.id
}
FlorisStepState.new(init = initStep)
}
val stepState = rememberSaveable(saver = FlorisStepState.Saver) {
val initStep = when {
!isFlorisBoardEnabled -> Steps.WithoutNotifications.EnableIme.id
!isFlorisBoardSelected -> Steps.WithoutNotifications.SelectIme.id
else -> Steps.WithoutNotifications.FinishUp.id
}
FlorisStepState.new(init = initStep)
content {
LaunchedEffect(isFlorisBoardEnabled, isFlorisBoardSelected, hasNotificationPermission) {
stepState.setCurrentAuto(
when {
!isFlorisBoardEnabled -> Steps.EnableIme.id
!isFlorisBoardSelected -> Steps.SelectIme.id
hasNotificationPermission == NotificationPermissionState.NOT_SET && AndroidVersion.ATLEAST_API33_T -> Steps.SelectNotification.id
else -> Steps.FinishUp.id
}
)
}
content {
LaunchedEffect(isFlorisBoardEnabled, isFlorisBoardSelected) {
stepState.setCurrentAuto(
when {
!isFlorisBoardEnabled -> Steps.WithoutNotifications.EnableIme.id
!isFlorisBoardSelected -> Steps.WithoutNotifications.SelectIme.id
else -> Steps.WithoutNotifications.FinishUp.id
}
)
}
// Below block allows to return from the system IME enabler activity
// as soon as it gets selected.
LaunchedEffect(Unit) {
while (true) {
delay(200L)
val isEnabled = InputMethodUtils.isFlorisboardEnabled(context)
if (stepState.getCurrentAuto().value == Steps.WithoutNotifications.EnableIme.id &&
stepState.getCurrentManual().value == -1 &&
!isFlorisBoardEnabled &&
!isFlorisBoardSelected &&
isEnabled
) {
context.launchActivity(FlorisAppActivity::class) {
it.flags = (Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
or Intent.FLAG_ACTIVITY_SINGLE_TOP
or Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
// Below block allows to return from the system IME enabler activity
// as soon as it gets selected.
LaunchedEffect(Unit) {
while (true) {
delay(200L)
val isEnabled = InputMethodUtils.isFlorisboardEnabled(context)
if (stepState.getCurrentAuto().value == Steps.EnableIme.id &&
stepState.getCurrentManual().value == -1 &&
!isFlorisBoardEnabled &&
!isFlorisBoardSelected &&
hasNotificationPermission == NotificationPermissionState.NOT_SET &&
isEnabled
) {
context.launchActivity(FlorisAppActivity::class) {
it.flags = (Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
or Intent.FLAG_ACTIVITY_SINGLE_TOP
or Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
}
}
FlorisStepLayout(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
stepState = stepState,
header = {
StepText(stringRes(R.string.setup__intro_message))
Spacer(modifier = Modifier.height(16.dp))
},
steps = steps(
context, navController, requestNotification
),
footer = {
footer(context)
},
)
}
// Show the screen with notification permission on android 13+
} else {
val stepState = rememberSaveable(saver = FlorisStepState.Saver) {
val initStep = when {
!isFlorisBoardEnabled -> Steps.WithNotifications.EnableIme.id
!isFlorisBoardSelected -> Steps.WithNotifications.SelectIme.id
hasNotificationPermission == NotificationPermissionState.NOT_SET -> Steps.WithNotifications.SelectNotification.id
else -> Steps.WithNotifications.FinishUp.id
}
FlorisStepState.new(init = initStep)
}
content {
LaunchedEffect(isFlorisBoardEnabled, isFlorisBoardSelected, hasNotificationPermission) {
stepState.setCurrentAuto(
when {
!isFlorisBoardEnabled -> Steps.WithNotifications.EnableIme.id
!isFlorisBoardSelected -> Steps.WithNotifications.SelectIme.id
hasNotificationPermission == NotificationPermissionState.NOT_SET -> Steps.WithNotifications.SelectNotification.id
else -> Steps.WithNotifications.FinishUp.id
}
)
}
// Below block allows to return from the system IME enabler activity
// as soon as it gets selected.
LaunchedEffect(Unit) {
while (true) {
delay(200L)
val isEnabled = InputMethodUtils.isFlorisboardEnabled(context)
if (stepState.getCurrentAuto().value == Steps.WithNotifications.EnableIme.id &&
stepState.getCurrentManual().value == -1 &&
!isFlorisBoardEnabled &&
!isFlorisBoardSelected &&
hasNotificationPermission == NotificationPermissionState.NOT_SET &&
isEnabled
) {
context.launchActivity(FlorisAppActivity::class) {
it.flags = (Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
or Intent.FLAG_ACTIVITY_SINGLE_TOP
or Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
}
}
}
FlorisStepLayout(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
stepState = stepState,
header = {
StepText(stringRes(R.string.setup__intro_message))
Spacer(modifier = Modifier.height(16.dp))
},
steps = steps(
context, navController, requestNotification
),
footer = {
footer(context)
},
)
}
FlorisStepLayout(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
stepState = stepState,
header = {
StepText(stringRes(R.string.setup__intro_message))
Spacer(modifier = Modifier.height(16.dp))
},
steps = steps(
context, navController, requestNotification
),
footer = {
footer(context)
},
)
}
}
@@ -255,105 +192,60 @@ private fun footer(context: Context) {
private fun PreferenceUiScope<AppPrefs>.steps(
context: Context,
navController: NavController,
requestNotification: ManagedActivityResultLauncher<String, Boolean>
requestNotification: ManagedActivityResultLauncher<String, Boolean>,
): List<FlorisStep> {
return if (AndroidVersion.ATMOST_API32_S_V2) {
listOf(
return listOfNotNull(
FlorisStep(
id = Steps.EnableIme.id,
title = stringRes(R.string.setup__enable_ime__title),
) {
StepText(stringRes(R.string.setup__enable_ime__description))
StepButton(label = stringRes(R.string.setup__enable_ime__open_settings_btn)) {
InputMethodUtils.showImeEnablerActivity(context)
}
},
FlorisStep(
id = Steps.SelectIme.id,
title = stringRes(R.string.setup__select_ime__title),
) {
StepText(stringRes(R.string.setup__select_ime__description))
StepButton(label = stringRes(R.string.setup__select_ime__switch_keyboard_btn)) {
InputMethodUtils.showImePicker(context)
}
},
if (AndroidVersion.ATLEAST_API33_T) {
FlorisStep(
id = Steps.WithoutNotifications.EnableIme.id,
title = stringRes(R.string.setup__enable_ime__title),
) {
StepText(stringRes(R.string.setup__enable_ime__description))
StepButton(label = stringRes(R.string.setup__enable_ime__open_settings_btn)) {
InputMethodUtils.showImeEnablerActivity(context)
}
},
FlorisStep(
id = Steps.WithoutNotifications.SelectIme.id,
title = stringRes(R.string.setup__select_ime__title),
) {
StepText(stringRes(R.string.setup__select_ime__description))
StepButton(label = stringRes(R.string.setup__select_ime__switch_keyboard_btn)) {
InputMethodUtils.showImePicker(context)
}
},
FlorisStep(
id = Steps.WithoutNotifications.FinishUp.id,
title = stringRes(R.string.setup__finish_up__title),
) {
StepText(stringRes(R.string.setup__finish_up__description_p1))
StepText(stringRes(R.string.setup__finish_up__description_p2))
StepButton(label = stringRes(R.string.setup__finish_up__finish_btn)) {
this@steps.prefs.internal.isImeSetUp.set(true)
navController.navigate(Routes.Settings.Home) {
popUpTo(Routes.Setup.Screen) {
inclusive = true
}
}
}
},
)
} else {
listOf(
FlorisStep(
id = Steps.WithNotifications.EnableIme.id,
title = stringRes(R.string.setup__enable_ime__title),
) {
StepText(stringRes(R.string.setup__enable_ime__description))
StepButton(label = stringRes(R.string.setup__enable_ime__open_settings_btn)) {
InputMethodUtils.showImeEnablerActivity(context)
}
},
FlorisStep(
id = Steps.WithNotifications.SelectIme.id,
title = stringRes(R.string.setup__select_ime__title),
) {
StepText(stringRes(R.string.setup__select_ime__description))
StepButton(label = stringRes(R.string.setup__select_ime__switch_keyboard_btn)) {
InputMethodUtils.showImePicker(context)
}
},
FlorisStep(
id = Steps.WithNotifications.SelectNotification.id,
title = stringRes(R.string.setup__grant_notification_permission__title)
id = Steps.SelectNotification.id,
title = stringRes(R.string.setup__grant_notification_permission__title),
) {
StepText(stringRes(R.string.setup__grant_notification_permission__description))
StepButton(stringRes(R.string.setup__grant_notification_permission__btn)) {
if (AndroidVersion.ATLEAST_API33_T) {
requestNotification.launch(android.Manifest.permission.POST_NOTIFICATIONS)
requestNotification.launch(android.Manifest.permission.POST_NOTIFICATIONS)
}
}
} else null,
FlorisStep(
id = Steps.FinishUp.id,
title = stringRes(R.string.setup__finish_up__title),
) {
StepText(stringRes(R.string.setup__finish_up__description_p1))
StepText(stringRes(R.string.setup__finish_up__description_p2))
StepButton(label = stringRes(R.string.setup__finish_up__finish_btn)) {
this@steps.prefs.internal.isImeSetUp.set(true)
navController.navigate(Routes.Settings.Home) {
popUpTo(Routes.Setup.Screen) {
inclusive = true
}
}
},
FlorisStep(
id = Steps.WithNotifications.FinishUp.id,
title = stringRes(R.string.setup__finish_up__title),
) {
StepText(stringRes(R.string.setup__finish_up__description_p1))
StepText(stringRes(R.string.setup__finish_up__description_p2))
StepButton(label = stringRes(R.string.setup__finish_up__finish_btn)) {
this@steps.prefs.internal.isImeSetUp.set(true)
navController.navigate(Routes.Settings.Home) {
popUpTo(Routes.Setup.Screen) {
inclusive = true
}
}
}
},
)
}
}
}
)
}
private sealed class Steps(val id: Int) {
sealed class WithoutNotifications(id: Int) : Steps(id) {
data object EnableIme : WithoutNotifications(id = 1)
data object SelectIme : WithoutNotifications(id = 2)
data object FinishUp : WithoutNotifications(id = 3)
}
sealed class WithNotifications(id: Int) : Steps(id) {
data object EnableIme : WithNotifications(id = 1)
data object SelectIme : WithNotifications(id = 2)
data object SelectNotification : WithNotifications(id = 3)
data object FinishUp : WithNotifications(id = 4)
}
data object EnableIme : Steps(id = 1)
data object SelectIme : Steps(id = 2)
data object SelectNotification : Steps(id = 3)
data object FinishUp : Steps(id = 4)
}

View File

@@ -31,20 +31,16 @@ import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
@@ -54,9 +50,7 @@ import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.ToggleOff
import androidx.compose.material.icons.filled.ToggleOn
import androidx.compose.material.icons.filled.Videocam
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -73,12 +67,6 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.florisPreferenceModel
@@ -90,16 +78,12 @@ import dev.patrickgold.florisboard.ime.clipboard.provider.ItemType
import dev.patrickgold.florisboard.ime.keyboard.FlorisImeSizing
import dev.patrickgold.florisboard.ime.media.KeyboardLikeButton
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.keyboardManager
import dev.patrickgold.florisboard.lib.compose.FlorisIconButtonWithInnerPadding
import dev.patrickgold.florisboard.lib.compose.FlorisStaggeredVerticalGrid
import dev.patrickgold.florisboard.lib.compose.FlorisTextButton
import dev.patrickgold.florisboard.lib.compose.autoMirrorForRtl
import dev.patrickgold.florisboard.lib.compose.florisVerticalScroll
import dev.patrickgold.florisboard.lib.compose.rippleClickable
import dev.patrickgold.florisboard.lib.compose.safeTimes
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.observeAsNonNullState
import dev.patrickgold.florisboard.lib.util.NetworkUtils
@@ -108,20 +92,14 @@ import org.florisboard.lib.android.AndroidKeyguardManager
import org.florisboard.lib.android.AndroidVersion
import org.florisboard.lib.android.showShortToast
import org.florisboard.lib.android.systemService
import org.florisboard.lib.snygg.SnyggPropertySet
import org.florisboard.lib.snygg.ui.SnyggBox
import org.florisboard.lib.snygg.ui.SnyggButton
import org.florisboard.lib.snygg.ui.SnyggSurface
import org.florisboard.lib.snygg.ui.snyggBackground
import org.florisboard.lib.snygg.ui.snyggBorder
import org.florisboard.lib.snygg.ui.snyggClip
import org.florisboard.lib.snygg.ui.snyggShadow
import org.florisboard.lib.snygg.ui.solidColor
import org.florisboard.lib.snygg.ui.spSize
import org.florisboard.lib.snygg.ui.SnyggColumn
import org.florisboard.lib.snygg.ui.SnyggIcon
import org.florisboard.lib.snygg.ui.SnyggIconButton
import org.florisboard.lib.snygg.ui.SnyggRow
import org.florisboard.lib.snygg.ui.SnyggText
private val ContentPadding = PaddingValues(horizontal = 4.dp)
private val ItemMargin = PaddingValues(all = 6.dp)
private val ItemPadding = PaddingValues(vertical = 8.dp, horizontal = 12.dp)
private val DescriptionPadding = PaddingValues(top = 4.dp, start = 12.dp, end = 12.dp)
private val ItemWidth = 200.dp
private val DialogWidth = 240.dp
@@ -139,69 +117,79 @@ fun ClipboardInputLayout(
val historyEnabled by prefs.clipboard.historyEnabled.observeAsState()
val history by clipboardManager.history.observeAsNonNullState()
val innerHeight = FlorisImeSizing.imeUiHeight() - FlorisImeSizing.smartbarHeight
var popupItem by remember(history) { mutableStateOf<ClipboardItem?>(null) }
var showClearAllHistory by remember { mutableStateOf(false) }
val headerStyle = FlorisImeTheme.style.get(FlorisImeUi.ClipboardHeader)
val itemStyle = FlorisImeTheme.style.get(FlorisImeUi.ClipboardItem)
val popupStyle = FlorisImeTheme.style.get(FlorisImeUi.ClipboardItemPopup)
val enableHistoryButtonStyle = FlorisImeTheme.style.get(FlorisImeUi.ClipboardEnableHistoryButton)
fun isPopupSurfaceActive() = popupItem != null || showClearAllHistory
@Composable
fun HeaderRow() {
Row(
SnyggRow(FlorisImeUi.ClipboardHeader.elementName,
modifier = Modifier
.fillMaxWidth()
.height(FlorisImeSizing.smartbarHeight)
.snyggBackground(context, headerStyle),
.height(FlorisImeSizing.smartbarHeight),
verticalAlignment = Alignment.CenterVertically,
) {
FlorisIconButtonWithInnerPadding(
val sizeModifier = Modifier
.sizeIn(maxHeight = FlorisImeSizing.smartbarHeight)
.aspectRatio(1f)
SnyggIconButton(
elementName = FlorisImeUi.ClipboardHeaderButton.elementName,
onClick = { keyboardManager.activeState.imeUiMode = ImeUiMode.TEXT },
icon = Icons.AutoMirrored.Filled.ArrowBack,
iconColor = headerStyle.foreground.solidColor(context),
)
Text(
modifier = sizeModifier,
) {
SnyggIcon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
)
}
SnyggText(
elementName = FlorisImeUi.ClipboardHeaderText.elementName,
modifier = Modifier.weight(1f),
text = stringRes(R.string.clipboard__header_title),
color = headerStyle.foreground.solidColor(context),
fontSize = headerStyle.fontSize.spSize(),
)
FlorisIconButtonWithInnerPadding(
SnyggIconButton(
elementName = FlorisImeUi.ClipboardHeaderButton.elementName,
onClick = { prefs.clipboard.historyEnabled.set(!historyEnabled) },
modifier = Modifier.autoMirrorForRtl(),
icon = if (historyEnabled) {
Icons.Default.ToggleOn
} else {
Icons.Default.ToggleOff
},
iconColor = headerStyle.foreground.solidColor(context),
modifier = sizeModifier.autoMirrorForRtl(),
enabled = !deviceLocked && !isPopupSurfaceActive(),
)
FlorisIconButtonWithInnerPadding(
) {
SnyggIcon(
imageVector = if (historyEnabled) {
Icons.Default.ToggleOn
} else {
Icons.Default.ToggleOff
},
)
}
SnyggIconButton(
elementName = FlorisImeUi.ClipboardHeaderButton.elementName,
onClick = { showClearAllHistory = true },
modifier = Modifier.autoMirrorForRtl(),
icon = Icons.Default.ClearAll,
iconColor = headerStyle.foreground.solidColor(context),
modifier = sizeModifier.autoMirrorForRtl(),
enabled = !deviceLocked && historyEnabled && history.all.isNotEmpty() && !isPopupSurfaceActive(),
)
FlorisIconButtonWithInnerPadding(
) {
SnyggIcon(
imageVector = Icons.Default.ClearAll,
)
}
SnyggIconButton(
elementName = FlorisImeUi.ClipboardHeaderButton.elementName,
onClick = {
context.showShortToast("TODO: implement inline clip item editing")
},
icon = Icons.Default.Edit,
iconColor = headerStyle.foreground.solidColor(context),
modifier = sizeModifier,
enabled = !deviceLocked && historyEnabled && !isPopupSurfaceActive(),
)
) {
SnyggIcon(
imageVector = Icons.Default.Edit,
)
}
KeyboardLikeButton(
modifier = sizeModifier,
inputEventDispatcher = keyboardManager.inputEventDispatcher,
keyData = TextKeyData.DELETE,
element = FlorisImeUi.ClipboardHeader,
elementName = FlorisImeUi.ClipboardHeaderButton.elementName,
) {
Icon(Icons.AutoMirrored.Outlined.Backspace, null)
SnyggIcon(imageVector = Icons.AutoMirrored.Outlined.Backspace)
}
}
}
@@ -210,16 +198,11 @@ fun ClipboardInputLayout(
@Composable
fun ClipItemView(
item: ClipboardItem,
style: SnyggPropertySet,
contentScrollInsteadOfClip: Boolean,
modifier: Modifier = Modifier,
) {
SnyggSurface(
modifier = modifier
.fillMaxWidth()
.padding(ItemMargin),
style = style,
clip = true,
SnyggBox(FlorisImeUi.ClipboardItem.elementName,
modifier = modifier.fillMaxWidth(),
clickAndSemanticsModifier = Modifier.combinedClickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(),
@@ -251,14 +234,9 @@ fun ClipboardInputLayout(
contentScale = ContentScale.FillWidth,
)
} else {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(ItemPadding),
SnyggText(
modifier = Modifier.fillMaxWidth(),
text = bitmap.exceptionOrNull()?.message ?: "Unknown error",
style = TextStyle(textDirection = TextDirection.Ltr),
color = Color.Red,
fontSize = style.fontSize.spSize(),
)
}
} else if (item.type == ItemType.VIDEO) {
@@ -298,31 +276,20 @@ fun ClipboardInputLayout(
tint = Color.Black,
)
} else {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(ItemPadding),
SnyggText(
modifier = Modifier.fillMaxWidth(),
text = bitmap.exceptionOrNull()?.message ?: "Unknown error",
style = TextStyle(textDirection = TextDirection.Ltr),
color = Color.Red,
fontSize = style.fontSize.spSize(),
)
}
} else {
val text = item.stringRepresentation()
Column {
ClipTextItemDescription(text, style)
Text(
ClipTextItemDescription(text)
SnyggText(
modifier = Modifier
.fillMaxWidth()
.run { if (contentScrollInsteadOfClip) this.florisVerticalScroll() else this }
.padding(ItemPadding),
.run { if (contentScrollInsteadOfClip) this.florisVerticalScroll() else this },
text = item.displayText(),
style = TextStyle(textDirection = TextDirection.ContentOrLtr),
color = style.foreground.solidColor(context),
fontSize = style.fontSize.spSize(),
maxLines = if (contentScrollInsteadOfClip) Int.MAX_VALUE else 5,
overflow = TextOverflow.Ellipsis,
)
}
}
@@ -331,15 +298,12 @@ fun ClipboardInputLayout(
@Composable
fun HistoryMainView() {
Box(
modifier = Modifier
.fillMaxWidth()
.height(innerHeight),
SnyggBox(FlorisImeUi.ClipboardContent.elementName,
modifier = Modifier.fillMaxSize(),
) {
val historyAlpha by animateFloatAsState(targetValue = if (isPopupSurfaceActive()) 0.12f else 1f)
Column(
SnyggColumn(
modifier = Modifier
.padding(ContentPadding)
.fillMaxSize()
.alpha(historyAlpha)
.florisVerticalScroll(),
@@ -347,42 +311,38 @@ fun ClipboardInputLayout(
if (history.pinned.isNotEmpty()) {
ClipCategoryTitle(
text = stringRes(R.string.clipboard__group_pinned),
style = itemStyle,
)
FlorisStaggeredVerticalGrid(maxColumnWidth = ItemWidth) {
for (item in history.pinned) {
ClipItemView(item, itemStyle, contentScrollInsteadOfClip = false)
ClipItemView(item, contentScrollInsteadOfClip = false)
}
}
}
if (history.recent.isNotEmpty()) {
ClipCategoryTitle(
text = stringRes(R.string.clipboard__group_recent),
style = itemStyle,
)
FlorisStaggeredVerticalGrid(maxColumnWidth = ItemWidth) {
for (item in history.recent) {
ClipItemView(item, itemStyle, contentScrollInsteadOfClip = false)
ClipItemView(item, contentScrollInsteadOfClip = false)
}
}
}
if (history.other.isNotEmpty()) {
ClipCategoryTitle(
text = stringRes(R.string.clipboard__group_other),
style = itemStyle,
)
FlorisStaggeredVerticalGrid(maxColumnWidth = ItemWidth) {
for (item in history.other) {
ClipItemView(item, itemStyle, contentScrollInsteadOfClip = false)
ClipItemView(item, contentScrollInsteadOfClip = false)
}
}
}
}
if (popupItem != null) {
Row(
SnyggRow(
modifier = Modifier
.fillMaxSize()
.padding(ContentPadding)
.pointerInput(Unit) {
detectTapGestures { popupItem = null }
},
@@ -392,17 +352,9 @@ fun ClipboardInputLayout(
ClipItemView(
modifier = Modifier.widthIn(max = ItemWidth),
item = popupItem!!,
style = itemStyle,
contentScrollInsteadOfClip = true,
)
Column(
modifier = Modifier
.padding(ItemMargin)
.snyggShadow(popupStyle)
.snyggBorder(context, popupStyle)
.snyggBackground(context, popupStyle)
.snyggClip(popupStyle),
) {
SnyggColumn(FlorisImeUi.ClipboardItemPopup.elementName) {
PopupAction(
iconId = R.drawable.ic_pin,
text = stringRes(if (popupItem!!.isPinned) {
@@ -410,7 +362,6 @@ fun ClipboardInputLayout(
} else {
R.string.clip__pin_item
}),
style = popupStyle,
) {
if (popupItem!!.isPinned) {
clipboardManager.unpinClip(popupItem!!)
@@ -422,7 +373,6 @@ fun ClipboardInputLayout(
PopupAction(
iconId = R.drawable.ic_delete,
text = stringRes(R.string.clip__delete_item),
style = popupStyle,
) {
clipboardManager.deleteClip(popupItem!!)
popupItem = null
@@ -430,7 +380,6 @@ fun ClipboardInputLayout(
PopupAction(
iconId = R.drawable.ic_content_paste,
text = stringRes(R.string.clip__paste_item),
style = popupStyle,
) {
clipboardManager.pasteItem(popupItem!!)
popupItem = null
@@ -439,51 +388,53 @@ fun ClipboardInputLayout(
}
}
if (showClearAllHistory) {
Row(
SnyggRow(
modifier = Modifier
.fillMaxSize()
.padding(ContentPadding)
.pointerInput(Unit) {
detectTapGestures { showClearAllHistory = false }
},
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceAround,
) {
Column(
SnyggColumn(
elementName = FlorisImeUi.ClipboardClearAllDialog.elementName,
modifier = Modifier
.width(DialogWidth)
.snyggShadow(popupStyle)
.snyggBorder(context, popupStyle)
.snyggBackground(context, popupStyle)
.snyggClip(popupStyle)
.pointerInput(Unit) {
detectTapGestures { /* Do nothing */ }
},
) {
Text(
modifier = Modifier.padding(all = 16.dp),
SnyggText(
elementName = FlorisImeUi.ClipboardClearAllDialogMessage.elementName,
text = stringRes(R.string.clipboard__confirm_clear_history__message),
color = popupStyle.foreground.solidColor(context),
)
Row(modifier = Modifier.padding(horizontal = 8.dp)) {
SnyggRow(FlorisImeUi.ClipboardClearAllDialogButtons.elementName) {
Spacer(modifier = Modifier.weight(1f))
FlorisTextButton(
SnyggButton(
elementName = FlorisImeUi.ClipboardClearAllDialogButton.elementName,
attributes = mapOf("action" to "no"),
onClick = {
showClearAllHistory = false
},
modifier = Modifier.padding(end = 8.dp),
text = stringRes(R.string.action__no),
colors = ButtonDefaults.textButtonColors(contentColor = popupStyle.foreground.solidColor(context)),
)
FlorisTextButton(
) {
SnyggText(
text = stringRes(R.string.action__no),
)
}
SnyggButton(
elementName = FlorisImeUi.ClipboardClearAllDialogButton.elementName,
attributes = mapOf("action" to "yes"),
onClick = {
clipboardManager.clearHistory()
context.showShortToast(R.string.clipboard__cleared_history)
showClearAllHistory = false
},
text = stringRes(R.string.action__yes),
colors = ButtonDefaults.textButtonColors(contentColor = popupStyle.foreground.solidColor(context)),
)
) {
SnyggText(
text = stringRes(R.string.action__yes),
)
}
}
}
}
@@ -493,98 +444,62 @@ fun ClipboardInputLayout(
@Composable
fun HistoryEmptyView() {
Column(
modifier = Modifier
.fillMaxWidth()
.height(innerHeight)
.padding(ContentPadding),
SnyggColumn(FlorisImeUi.ClipboardContent.elementName,
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.padding(top = 24.dp, bottom = 8.dp),
SnyggText(
text = stringRes(R.string.clipboard__empty__title),
color = itemStyle.foreground.solidColor(context),
fontSize = itemStyle.fontSize.spSize() safeTimes 1.1f,
fontWeight = FontWeight.Bold,
)
Text(
SnyggText(
text = stringRes(R.string.clipboard__empty__message),
color = itemStyle.foreground.solidColor(context),
fontSize = itemStyle.fontSize.spSize(),
textAlign = TextAlign.Center,
)
}
}
@Composable
fun HistoryDisabledView() {
Box(
modifier = Modifier
.fillMaxWidth()
.height(innerHeight)
.padding(ContentPadding),
contentAlignment = Alignment.TopCenter,
SnyggColumn(FlorisImeUi.ClipboardContent.elementName,
modifier = Modifier.fillMaxSize(),
) {
SnyggSurface(
modifier = Modifier
.padding(ItemMargin)
.fillMaxWidth()
.wrapContentHeight(),
style = itemStyle,
contentPadding = ItemPadding,
SnyggText(
elementName = FlorisImeUi.ClipboardHistoryDisabledTitle.elementName,
modifier = Modifier.padding(bottom = 8.dp),
text = stringRes(R.string.clipboard__disabled__title),
)
SnyggText(
elementName = FlorisImeUi.ClipboardHistoryDisabledMessage.elementName,
text = stringRes(R.string.clipboard__disabled__message),
)
SnyggButton(FlorisImeUi.ClipboardHistoryDisabledButton.elementName,
onClick = { prefs.clipboard.historyEnabled.set(true) },
modifier = Modifier.align(Alignment.End),
) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(
modifier = Modifier.padding(bottom = 8.dp),
text = stringRes(R.string.clipboard__disabled__title),
color = itemStyle.foreground.solidColor(context),
fontSize = itemStyle.fontSize.spSize() safeTimes 1.1f,
fontWeight = FontWeight.Bold,
)
Text(
text = stringRes(R.string.clipboard__disabled__message),
color = itemStyle.foreground.solidColor(context),
fontSize = itemStyle.fontSize.spSize(),
)
SnyggButton(
modifier = Modifier
.padding(top = 8.dp)
.align(Alignment.End),
onClick = { prefs.clipboard.historyEnabled.set(true) },
style = enableHistoryButtonStyle,
text = stringRes(R.string.clipboard__disabled__enable_button)
)
}
SnyggText(
text = stringRes(R.string.clipboard__disabled__enable_button),
)
}
}
}
@Composable
fun HistoryLockedView() {
Column(
modifier = Modifier
.fillMaxWidth()
.height(innerHeight)
.padding(ContentPadding),
SnyggColumn(FlorisImeUi.ClipboardContent.elementName,
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.padding(top = 24.dp, bottom = 8.dp),
SnyggText(
elementName = FlorisImeUi.ClipboardHistoryLockedTitle.elementName,
text = stringRes(R.string.clipboard__locked__title),
color = itemStyle.foreground.solidColor(context),
fontSize = itemStyle.fontSize.spSize() safeTimes 1.1f,
fontWeight = FontWeight.Bold,
)
Text(
SnyggText(
elementName = FlorisImeUi.ClipboardHistoryLockedMessage.elementName,
text = stringRes(R.string.clipboard__locked__message),
color = itemStyle.foreground.solidColor(context),
fontSize = itemStyle.fontSize.spSize(),
textAlign = TextAlign.Center,
)
}
}
Column(
SnyggColumn(
modifier = modifier
.fillMaxWidth()
.height(FlorisImeSizing.imeUiHeight()),
@@ -609,29 +524,19 @@ fun ClipboardInputLayout(
@Composable
private fun ClipCategoryTitle(
text: String,
style: SnyggPropertySet,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
Text(
modifier = modifier
.padding(ItemMargin)
.padding(top = 8.dp)
.fillMaxWidth(),
SnyggText(FlorisImeUi.ClipboardSubheader.elementName,
modifier = modifier.fillMaxWidth(),
text = text.uppercase(),
color = style.foreground.solidColor(context),
fontWeight = FontWeight.Bold,
fontSize = style.fontSize.spSize() safeTimes 0.8f,
)
}
@Composable
private fun ClipTextItemDescription(
text: String,
style: SnyggPropertySet,
modifier: Modifier = Modifier,
): Unit = with(LocalDensity.current) {
val context = LocalContext.current
val iconId: Int?
val description: String?
when {
@@ -653,27 +558,16 @@ private fun ClipTextItemDescription(
}
}
if (iconId != null && description != null) {
Row(
modifier = modifier
.padding(DescriptionPadding)
.offset(y = DescriptionPadding.calculateTopPadding()),
SnyggRow(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
val fontSize = style.fontSize.spSize()
Icon(
modifier = Modifier
.padding(end = 8.dp)
.requiredSize(fontSize.toDp()),
SnyggIcon(
painter = painterResource(id = iconId),
contentDescription = null,
tint = style.foreground.solidColor(context),
)
Text(
SnyggText(
modifier = Modifier.weight(1f),
text = description,
color = style.foreground.solidColor(context),
fontSize = fontSize safeTimes 0.8f,
fontStyle = FontStyle.Italic,
)
}
}
@@ -683,29 +577,19 @@ private fun ClipTextItemDescription(
private fun PopupAction(
@DrawableRes iconId: Int,
text: String,
style: SnyggPropertySet,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
val context = LocalContext.current
Row(
modifier = modifier
.width(ItemWidth)
.rippleClickable(onClick = onClick)
.padding(all = 8.dp),
SnyggRow(FlorisImeUi.ClipboardItemPopupAction.elementName,
modifier = modifier.rippleClickable(onClick = onClick),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
modifier = Modifier.padding(end = 8.dp),
SnyggIcon(FlorisImeUi.ClipboardItemPopupActionIcon.elementName,
painter = painterResource(iconId),
contentDescription = null,
tint = style.foreground.solidColor(context),
)
Text(
SnyggText(FlorisImeUi.ClipboardItemPopupActionText.elementName,
modifier = Modifier.weight(1f),
text = text,
color = style.foreground.solidColor(context),
fontSize = style.fontSize.spSize(),
)
}
}

View File

@@ -120,9 +120,8 @@ class FlorisCopyToClipboardActivity : ComponentActivity() {
setContent {
ProvideLocalizedResources(this, forceLayoutDirection = LayoutDirection.Ltr) {
val theme by prefs.advanced.settingsTheme.observeAsState()
val isMaterialYouAware by prefs.advanced.useMaterialYou.observeAsState()
FlorisAppTheme(theme, isMaterialYouAware) {
val theme by prefs.other.settingsTheme.observeAsState()
FlorisAppTheme(theme) {
BottomSheet {
Row {
Text(
@@ -162,7 +161,9 @@ class FlorisCopyToClipboardActivity : ComponentActivity() {
Column {
content()
Button(
modifier = Modifier.align(Alignment.End).padding(16.dp),
modifier = Modifier
.align(Alignment.End)
.padding(16.dp),
onClick = { finish() },
colors = ButtonDefaults.textButtonColors(
//containerColor = buttonContainer.background.solidColor(context = context),

View File

@@ -0,0 +1,109 @@
/*
* Copyright (C) 2025 The FlorisBoard Contributors
*
* 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 androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.RadioButtonChecked
import androidx.compose.material.icons.filled.RadioButtonUnchecked
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItemDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.keyboard.KeyboardState
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.keyboardManager
import dev.patrickgold.florisboard.lib.compose.rippleClickable
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.subtypeManager
import dev.patrickgold.jetpref.material.ui.JetPrefListItem
import org.florisboard.lib.snygg.ui.SnyggColumn
import org.florisboard.lib.snygg.ui.SnyggRow
import org.florisboard.lib.snygg.ui.SnyggText
@Composable
fun SelectSubtypePanel(modifier: Modifier = Modifier) {
val context = LocalContext.current
val keyboardManager by context.keyboardManager()
val subtypeManager by context.subtypeManager()
val listState = rememberLazyListState()
val subtypes by subtypeManager.subtypesFlow.collectAsState()
val currentlySelected = subtypeManager.activeSubtype.id
SnyggColumn(FlorisImeUi.SubtypePanel.elementName, modifier = modifier.safeDrawingPadding()) {
SnyggRow(
elementName = FlorisImeUi.SubtypePanelHeader.elementName,
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
SnyggText(
modifier = Modifier
.fillMaxWidth()
.clickable(false) {},
text = stringRes(R.string.select_subtype_panel__header),
)
}
Box {
LazyColumn(
state = listState,
) {
items(
subtypes,
key = {
it.id
}
) {
JetPrefListItem(
modifier = Modifier
.fillMaxWidth()
.rippleClickable {
subtypeManager.switchToSubtypeById(it.id)
keyboardManager.activeState.isSubtypeSelectionVisible = false
},
icon = {
if (currentlySelected == it.id) {
Icon(Icons.Default.RadioButtonChecked, null)
} else {
Icon(Icons.Default.RadioButtonUnchecked, null)
}
},
text = it.primaryLocale.displayName(),
colors = ListItemDefaults.colors(),
)
}
}
}
}
}
fun KeyboardState.isSubtypeSelectionShowing(): Boolean {
return isSubtypeSelectionVisible
}

View File

@@ -24,7 +24,6 @@ import dev.patrickgold.florisboard.lib.FlorisLocale
import dev.patrickgold.florisboard.lib.devtools.flogDebug
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@@ -227,4 +226,11 @@ class SubtypeManager(context: Context) {
prefs.localization.activeSubtypeId.set(newActiveSubtype.id)
activeSubtype = newActiveSubtype
}
fun switchToSubtypeById(id: Long) {
if (subtypes.any { it.id == id }) {
activeSubtype = getSubtypeById(id)!!
prefs.localization.activeSubtypeId.set(id)
}
}
}

View File

@@ -281,7 +281,7 @@ abstract class AbstractEditorInstance(context: Context) {
abstract fun determineComposer(composerName: ExtensionComponentName): Composer
protected open fun shouldDetermineComposingRegion(editorInfo: FlorisEditorInfo): Boolean {
return editorInfo.isRichInputEditor
return editorInfo.isRichInputEditor && !editorInfo.inputAttributes.flagTextNoSuggestions
}
private suspend fun determineLocalComposing(

View File

@@ -119,11 +119,11 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
//!instance.inputAttributes.flagTextAutoComplete &&
//!instance.inputAttributes.flagTextNoSuggestions
}
activeState.isIncognitoMode = when (prefs.advanced.incognitoMode.get()) {
activeState.isIncognitoMode = when (prefs.suggestion.incognitoMode.get()) {
IncognitoMode.FORCE_OFF -> false
IncognitoMode.FORCE_ON -> true
IncognitoMode.DYNAMIC_ON_OFF -> {
editorInfo.imeOptions.flagNoPersonalizedLearning || prefs.advanced.forceIncognitoModeFromDynamic.get()
editorInfo.imeOptions.flagNoPersonalizedLearning || prefs.suggestion.forceIncognitoModeFromDynamic.get()
}
}
}

View File

@@ -43,5 +43,7 @@ enum class InputShiftState(val value: Int) {
fun fromInt(int: Int) = entries.firstOrNull { it.value == int } ?: UNSHIFTED
}
fun attrName() = name.lowercase()
fun toInt() = value
}

View File

@@ -206,7 +206,8 @@ fun ComputingEvaluator.computeImageVector(data: KeyData): ImageVector? {
Icons.Default.DeleteSweep
}
KeyCode.COMPACT_LAYOUT_TO_LEFT,
KeyCode.COMPACT_LAYOUT_TO_RIGHT -> {
KeyCode.COMPACT_LAYOUT_TO_RIGHT,
KeyCode.TOGGLE_COMPACT_LAYOUT -> {
context()?.vectorResource(id = R.drawable.ic_accessibility_one_handed)
}
KeyCode.VOICE_INPUT -> {

View File

@@ -119,7 +119,7 @@ fun ProvideKeyboardRowBaseHeight(content: @Composable () -> Unit) {
val heightFactorPortrait by prefs.keyboard.heightFactorPortrait.observeAsTransformingState { it.toFloat() / 100f }
val heightFactorLandscape by prefs.keyboard.heightFactorLandscape.observeAsTransformingState { it.toFloat() / 100f }
val oneHandedMode by prefs.keyboard.oneHandedMode.observeAsState()
val oneHandedMode by prefs.keyboard.oneHandedModeEnabled.observeAsState()
val oneHandedModeScaleFactor by prefs.keyboard.oneHandedModeScaleFactor.observeAsTransformingState { it.toFloat() / 100f }
// Only set systemBarHeights on api 35 or later because https://developer.android.com/about/versions/15/behavior-changes-15#stable-configuration
@@ -134,7 +134,7 @@ fun ProvideKeyboardRowBaseHeight(content: @Composable () -> Unit) {
) {
calcInputViewHeight(resources, systemBarHeights) * when {
configuration.isOrientationLandscape() -> heightFactorLandscape
else -> heightFactorPortrait * (if (oneHandedMode != OneHandedMode.OFF) oneHandedModeScaleFactor else 1f)
else -> heightFactorPortrait * (if (oneHandedMode) oneHandedModeScaleFactor else 1f)
}
}
val smartbarHeight = baseRowHeight * 0.753f

View File

@@ -55,10 +55,6 @@ import dev.patrickgold.florisboard.ime.text.key.KeyType
import dev.patrickgold.florisboard.ime.text.key.UtilityKeyAction
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyboardCache
import org.florisboard.lib.android.AndroidKeyguardManager
import org.florisboard.lib.android.showLongToast
import org.florisboard.lib.android.showShortToast
import org.florisboard.lib.android.systemService
import dev.patrickgold.florisboard.lib.devtools.LogTopic
import dev.patrickgold.florisboard.lib.devtools.flogError
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
@@ -67,6 +63,7 @@ import dev.patrickgold.florisboard.lib.uppercase
import dev.patrickgold.florisboard.lib.util.InputMethodUtils
import dev.patrickgold.florisboard.nlpManager
import dev.patrickgold.florisboard.subtypeManager
import java.lang.ref.WeakReference
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -75,9 +72,12 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.florisboard.lib.android.AndroidKeyguardManager
import org.florisboard.lib.android.showLongToast
import org.florisboard.lib.android.showShortToast
import org.florisboard.lib.android.systemService
import org.florisboard.lib.kotlin.collectIn
import org.florisboard.lib.kotlin.collectLatestIn
import java.lang.ref.WeakReference
private val DoubleSpacePeriodMatcher = """([^.!?‽\s]\s)""".toRegex()
@@ -189,7 +189,7 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver {
keyboardMode = mode,
subtype = subtype,
).await()
}.await()
}
val computingEvaluator = ComputingEvaluatorImpl(
version = activeEvaluatorVersion++,
keyboard = computedKeyboard,
@@ -236,11 +236,8 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver {
return subtypeManager.subtypes.size > 1
}
fun toggleOneHandedMode(isRight: Boolean) {
prefs.keyboard.oneHandedMode.set(when (prefs.keyboard.oneHandedMode.get()) {
OneHandedMode.OFF -> if (isRight) { OneHandedMode.END } else { OneHandedMode.START }
else -> OneHandedMode.OFF
})
fun toggleOneHandedMode() {
prefs.keyboard.oneHandedModeEnabled.set(!prefs.keyboard.oneHandedModeEnabled.get())
}
fun executeSwipeAction(swipeAction: SwipeAction) {
@@ -272,6 +269,7 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver {
SwipeAction.REDO -> TextKeyData.REDO
SwipeAction.UNDO -> TextKeyData.UNDO
SwipeAction.SHOW_INPUT_METHOD_PICKER -> TextKeyData.SYSTEM_INPUT_METHOD_PICKER
SwipeAction.SHOW_SUBTYPE_PICKER -> TextKeyData.SHOW_SUBTYPE_PICKER
SwipeAction.SWITCH_TO_CLIPBOARD_CONTEXT -> TextKeyData.IME_UI_MODE_CLIPBOARD
SwipeAction.SWITCH_TO_PREV_SUBTYPE -> TextKeyData.IME_PREV_SUBTYPE
SwipeAction.SWITCH_TO_NEXT_SUBTYPE -> TextKeyData.IME_NEXT_SUBTYPE
@@ -582,7 +580,7 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver {
* Handles a [KeyCode.TOGGLE_INCOGNITO_MODE] event.
*/
private fun handleToggleIncognitoMode() {
prefs.advanced.forceIncognitoModeFromDynamic.set(!prefs.advanced.forceIncognitoModeFromDynamic.get())
prefs.suggestion.forceIncognitoModeFromDynamic.set(!prefs.suggestion.forceIncognitoModeFromDynamic.get())
val newState = !activeState.isIncognitoMode
activeState.isIncognitoMode = newState
lastToastReference.get()?.cancel()
@@ -719,8 +717,15 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver {
clipboardManager.updatePrimaryClip(null)
appContext.showShortToast(R.string.clipboard__cleared_primary_clip)
}
KeyCode.COMPACT_LAYOUT_TO_LEFT -> toggleOneHandedMode(isRight = false)
KeyCode.COMPACT_LAYOUT_TO_RIGHT -> toggleOneHandedMode(isRight = true)
KeyCode.TOGGLE_COMPACT_LAYOUT -> toggleOneHandedMode()
KeyCode.COMPACT_LAYOUT_TO_LEFT -> {
prefs.keyboard.oneHandedMode.set(OneHandedMode.START)
toggleOneHandedMode()
}
KeyCode.COMPACT_LAYOUT_TO_RIGHT -> {
prefs.keyboard.oneHandedMode.set(OneHandedMode.END)
toggleOneHandedMode()
}
KeyCode.DELETE -> handleDelete()
KeyCode.DELETE_WORD -> handleDeleteWord()
KeyCode.ENTER -> handleEnter()
@@ -742,6 +747,9 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver {
KeyCode.SHIFT -> handleShiftUp(data)
KeyCode.SPACE -> handleSpace(data)
KeyCode.SYSTEM_INPUT_METHOD_PICKER -> InputMethodUtils.showImePicker(appContext)
KeyCode.SHOW_SUBTYPE_PICKER -> {
appContext.keyboardManager.value.activeState.isSubtypeSelectionVisible = true
}
KeyCode.SYSTEM_PREV_INPUT_METHOD -> FlorisImeService.switchToPrevInputMethod()
KeyCode.SYSTEM_NEXT_INPUT_METHOD -> FlorisImeService.switchToNextInputMethod()
KeyCode.TOGGLE_SMARTBAR_VISIBILITY -> {
@@ -867,17 +875,22 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver {
val punctuationRules = MutableLiveData<Map<ExtensionComponentName, PunctuationRule>>(emptyMap())
val subtypePresets = MutableLiveData<List<SubtypePreset>>(emptyList())
private val anyChangedGuard = Mutex(locked = false)
val anyChanged = MutableLiveData(Unit)
init {
scope.launch(Dispatchers.Main.immediate) {
extensionManager.keyboardExtensions.observeForever { keyboardExtensions ->
parseKeyboardExtensions(keyboardExtensions)
scope.launch {
anyChangedGuard.withLock {
parseKeyboardExtensions(keyboardExtensions)
}
}
}
}
}
private fun parseKeyboardExtensions(keyboardExtensions: List<KeyboardExtension>) = scope.launch {
private fun parseKeyboardExtensions(keyboardExtensions: List<KeyboardExtension>) {
val localComposers = mutableMapOf<ExtensionComponentName, Composer>()
val localCurrencySets = mutableMapOf<ExtensionComponentName, CurrencySet>()
val localLayouts = mutableMapOf<LayoutType, MutableMap<ExtensionComponentName, LayoutArrangementComponent>>()
@@ -956,10 +969,13 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver {
KeyCode.CLIPBOARD_SELECT_ALL -> {
editorInfo.isRichInputEditor
}
KeyCode.TOGGLE_INCOGNITO_MODE -> when (prefs.advanced.incognitoMode.get()) {
KeyCode.TOGGLE_INCOGNITO_MODE -> when (prefs.suggestion.incognitoMode.get()) {
IncognitoMode.FORCE_OFF, IncognitoMode.FORCE_ON -> false
IncognitoMode.DYNAMIC_ON_OFF -> !editorInfo.imeOptions.flagNoPersonalizedLearning
}
KeyCode.LANGUAGE_SWITCH -> {
subtypeManager.subtypes.size > 1
}
else -> true
}
}

View File

@@ -34,5 +34,7 @@ enum class KeyboardMode(val value: Int) {
fun fromInt(int: Int) = entries.firstOrNull { it.value == int } ?: CHARACTERS
}
fun attrName() = name.lowercase()
fun toInt() = value
}

View File

@@ -57,6 +57,7 @@ import kotlin.properties.Delegates
*
* <Byte 7> | <Byte 6> | <Byte 5> | <Byte 4> | Description
* ---------|----------|----------|----------|---------------------------------
* | | | 1 | Subtype selection dialog visible
* 1 | | | | Devtools: Show drag&drop helpers
*
* The resulting structure is only relevant during a runtime lifespan and
@@ -91,6 +92,8 @@ open class KeyboardState protected constructor(open var rawValue: ULong) {
const val F_IS_RTL_LAYOUT_DIRECTION: ULong = 0x08000000u
const val F_IS_SUBTYPE_SELECTION_VISIBLE: ULong = 0x1_0000_0000u
const val F_DEBUG_SHOW_DRAG_AND_DROP_HELPERS = 0x01_00_00_00_00_00_00_00uL
const val STATE_ALL_ZERO: ULong = 0uL
@@ -188,6 +191,10 @@ open class KeyboardState protected constructor(open var rawValue: ULong) {
get() = getFlag(F_IS_ACTIONS_EDITOR_VISIBLE)
set(v) { setFlag(F_IS_ACTIONS_EDITOR_VISIBLE, v) }
var isSubtypeSelectionVisible: Boolean
get() = getFlag(F_IS_SUBTYPE_SELECTION_VISIBLE)
set(v) { setFlag(F_IS_SUBTYPE_SELECTION_VISIBLE, v) }
var isComposingEnabled: Boolean
get() = getFlag(F_IS_COMPOSING_ENABLED)
set(v) { setFlag(F_IS_COMPOSING_ENABLED, v) }

View File

@@ -65,9 +65,9 @@ private data class CachedPopupMapping(
)
data class DebugLayoutComputationResult(
val main: Result<CachedLayout>,
val mod: Result<CachedLayout>,
val ext: Result<CachedLayout>,
val main: Result<CachedLayout?>,
val mod: Result<CachedLayout?>,
val ext: Result<CachedLayout?>,
) {
fun allLayoutsSuccess(): Boolean {
return main.isSuccess && mod.isSuccess && ext.isSuccess
@@ -96,8 +96,13 @@ class LayoutManager(context: Context) {
*
* @return A deferred result for a layout.
*/
private fun loadLayoutAsync(ltn: LTN?) = ioScope.runCatchingAsync {
require(ltn != null) { "Invalid argument value for 'ltn': null" }
private fun loadLayoutAsync(ltn: LTN?, allowNullLTN: Boolean) = ioScope.runCatchingAsync {
if (!allowNullLTN) {
requireNotNull(ltn) { "Invalid argument value for 'ltn': null" }
}
if (ltn == null) {
return@runCatchingAsync null
}
layoutCacheGuard.withLock {
val cached = layoutCache[ltn]
if (cached != null) {
@@ -176,7 +181,7 @@ class LayoutManager(context: Context) {
val extendedPopupsDefault = loadPopupMappingAsync()
val extendedPopups = loadPopupMappingAsync(subtype)
val mainLayoutResult = loadLayoutAsync(main).await()
val mainLayoutResult = loadLayoutAsync(main, allowNullLTN = false).await()
val mainLayout = mainLayoutResult.onFailure {
flogWarning { "$keyboardMode - main - $it" }
}.getOrNull()
@@ -196,11 +201,11 @@ class LayoutManager(context: Context) {
} else {
modifier
}
val modifierLayoutResult = loadLayoutAsync(modifierToLoad).await()
val modifierLayoutResult = loadLayoutAsync(modifierToLoad, allowNullLTN = true).await()
val modifierLayout = modifierLayoutResult.onFailure {
flogWarning { "$keyboardMode - mod - $it" }
}.getOrNull()
val extensionLayoutResult = loadLayoutAsync(extension).await()
val extensionLayoutResult = loadLayoutAsync(extension, allowNullLTN = true).await()
val extensionLayout = extensionLayoutResult.onFailure {
flogWarning { "$keyboardMode - ext - $it" }
}.getOrNull()

View File

@@ -20,18 +20,19 @@ import android.annotation.SuppressLint
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.waitForUpOrCancellation
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Backspace
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
@@ -46,7 +47,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.ime.input.InputEventDispatcher
import dev.patrickgold.florisboard.ime.input.LocalInputFeedbackController
import dev.patrickgold.florisboard.ime.keyboard.FlorisImeSizing
@@ -54,10 +54,12 @@ import dev.patrickgold.florisboard.ime.keyboard.KeyData
import dev.patrickgold.florisboard.ime.media.emoji.EmojiData
import dev.patrickgold.florisboard.ime.media.emoji.EmojiPaletteView
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.keyboardManager
import org.florisboard.lib.snygg.ui.SnyggSurface
import org.florisboard.lib.snygg.SnyggSelector
import org.florisboard.lib.snygg.ui.SnyggBox
import org.florisboard.lib.snygg.ui.SnyggColumn
import org.florisboard.lib.snygg.ui.SnyggRow
@SuppressLint("MutableCollectionMutableState")
@Composable
@@ -73,7 +75,8 @@ fun MediaInputLayout(
}
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
Column(
SnyggColumn(
elementName = FlorisImeUi.Media.elementName,
modifier = modifier
.fillMaxWidth()
.height(FlorisImeSizing.imeUiHeight()),
@@ -82,14 +85,17 @@ fun MediaInputLayout(
modifier = Modifier.weight(1f),
fullEmojiMappings = emojiLayoutDataMap,
)
Row(
SnyggRow(
elementName = FlorisImeUi.MediaBottomRow.elementName,
modifier = Modifier
.fillMaxWidth()
.height(FlorisImeSizing.keyboardRowBaseHeight * 0.8f),
) {
KeyboardLikeButton(
elementName = FlorisImeUi.MediaBottomRowButton.elementName,
inputEventDispatcher = keyboardManager.inputEventDispatcher,
keyData = TextKeyData.IME_UI_MODE_TEXT,
modifier = Modifier.fillMaxHeight(),
) {
Text(
text = "ABC",
@@ -98,8 +104,10 @@ fun MediaInputLayout(
}
Spacer(modifier = Modifier.weight(1f))
KeyboardLikeButton(
elementName = FlorisImeUi.MediaBottomRowButton.elementName,
inputEventDispatcher = keyboardManager.inputEventDispatcher,
keyData = TextKeyData.DELETE,
modifier = Modifier.fillMaxHeight(),
) {
Icon(imageVector = Icons.AutoMirrored.Outlined.Backspace, contentDescription = null)
}
@@ -113,42 +121,45 @@ internal fun KeyboardLikeButton(
modifier: Modifier = Modifier,
inputEventDispatcher: InputEventDispatcher,
keyData: KeyData,
element: String = FlorisImeUi.EmojiKey,
content: @Composable RowScope.() -> Unit,
elementName: String = FlorisImeUi.MediaEmojiKey.elementName,
content: @Composable () -> Unit,
) {
val inputFeedbackController = LocalInputFeedbackController.current
var isPressed by remember { mutableStateOf(false) }
val keyStyle = FlorisImeTheme.style.get(
element = element,
code = keyData.code,
isPressed = isPressed,
)
SnyggSurface(
modifier = modifier.pointerInput(Unit) {
awaitEachGesture {
awaitFirstDown(requireUnconsumed = false).also {
if (it.pressed != it.previousPressed) it.consume()
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
val selector = if (isPressed) {
SnyggSelector.PRESSED
} else {
SnyggSelector.NONE
}
SnyggBox(
elementName = elementName,
attributes = mapOf(FlorisImeUi.Attr.Code to keyData.code),
selector = selector,
clickAndSemanticsModifier = modifier
.indication(interactionSource, ripple())
.pointerInput(Unit) {
awaitEachGesture {
val down = awaitFirstDown(requireUnconsumed = false).also {
if (it.pressed != it.previousPressed) it.consume()
}
val press = PressInteraction.Press(down.position)
interactionSource.tryEmit(press)
inputEventDispatcher.sendDown(keyData)
inputFeedbackController.keyPress(keyData)
val up = waitForUpOrCancellation()
if (up != null) {
interactionSource.tryEmit(PressInteraction.Release(press))
inputEventDispatcher.sendUp(keyData)
} else {
interactionSource.tryEmit(PressInteraction.Cancel(press))
inputEventDispatcher.sendCancel(keyData)
}
}
isPressed = true
inputEventDispatcher.sendDown(keyData)
inputFeedbackController.keyPress(keyData)
val up = waitForUpOrCancellation()
isPressed = false
if (up != null) {
inputEventDispatcher.sendUp(keyData)
} else {
inputEventDispatcher.sendCancel(keyData)
}
}
},
style = keyStyle,
},
contentAlignment = Alignment.Center,
) {
Row(
modifier = Modifier
.fillMaxHeight()
.padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically,
content = content,
)
content()
}
}

View File

@@ -47,7 +47,7 @@ import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.PushPin
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.TabRowDefaults
@@ -88,23 +88,22 @@ import dev.patrickgold.florisboard.editorInstance
import dev.patrickgold.florisboard.ime.input.LocalInputFeedbackController
import dev.patrickgold.florisboard.ime.keyboard.FlorisImeSizing
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.keyboardManager
import dev.patrickgold.florisboard.lib.compose.florisScrollbar
import dev.patrickgold.florisboard.lib.compose.header
import dev.patrickgold.florisboard.lib.compose.safeTimes
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.jetpref.datastore.model.observeAsState
import kotlinx.coroutines.launch
import org.florisboard.lib.android.AndroidKeyguardManager
import org.florisboard.lib.android.showShortToast
import org.florisboard.lib.android.systemService
import org.florisboard.lib.snygg.ui.snyggBackground
import org.florisboard.lib.snygg.ui.snyggBorder
import org.florisboard.lib.snygg.ui.snyggShadow
import org.florisboard.lib.snygg.ui.solidColor
import org.florisboard.lib.snygg.ui.spSize
import org.florisboard.lib.snygg.SnyggSelector
import org.florisboard.lib.snygg.ui.SnyggBox
import org.florisboard.lib.snygg.ui.SnyggIcon
import org.florisboard.lib.snygg.ui.SnyggRow
import org.florisboard.lib.snygg.ui.SnyggText
import org.florisboard.lib.snygg.ui.rememberSnyggThemeQuery
import kotlin.math.ceil
private val EmojiCategoryValues = EmojiCategory.entries
@@ -164,10 +163,6 @@ fun EmojiPaletteView(
val preferredSkinTone by prefs.emoji.preferredSkinTone.observeAsState()
val emojiHistoryEnabled by prefs.emoji.historyEnabled.observeAsState()
val fontSizeMultiplier = prefs.keyboard.fontSizeMultiplier()
val emojiKeyStyle = FlorisImeTheme.style.get(element = FlorisImeUi.EmojiKey)
val emojiKeyFontSize = emojiKeyStyle.fontSize.spSize(default = EmojiDefaultFontSize) safeTimes fontSizeMultiplier
val contentColor = emojiKeyStyle.foreground.solidColor(context, default = FlorisImeTheme.fallbackContentColor())
var activeCategory by remember(emojiHistoryEnabled) {
if (emojiHistoryEnabled) {
@@ -182,11 +177,9 @@ fun EmojiPaletteView(
@Composable
fun GridHeader(text: String) {
Text(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
SnyggText(
elementName = FlorisImeUi.MediaEmojiSubheader.elementName,
text = text,
style = MaterialTheme.typography.titleMedium,
color = contentColor,
)
}
@@ -202,9 +195,6 @@ fun EmojiPaletteView(
preferredSkinTone = preferredSkinTone,
isPinned = isPinned,
isRecent = isRecent,
contentColor = contentColor,
fontSize = emojiKeyFontSize,
fontSizeMultiplier = fontSizeMultiplier,
onEmojiInput = { emoji ->
keyboardManager.inputEventDispatcher.sendDownUp(emoji)
scope.launch {
@@ -259,7 +249,6 @@ fun EmojiPaletteView(
) {
Text(
text = stringRes(R.string.emoji__history__phone_locked_message),
color = contentColor,
)
}
} else if (activeCategory == EmojiCategory.RECENTLY_USED && isEmojiHistoryEmpty) {
@@ -270,12 +259,10 @@ fun EmojiPaletteView(
) {
Text(
text = stringRes(R.string.emoji__history__empty_message),
color = contentColor,
)
Text(
modifier = Modifier.padding(top = 8.dp),
text = stringRes(R.string.emoji__history__usage_tip),
color = contentColor,
fontStyle = FontStyle.Italic,
)
}
@@ -284,7 +271,7 @@ fun EmojiPaletteView(
LazyVerticalGrid(
modifier = Modifier
.fillMaxSize()
.florisScrollbar(lazyListState, color = contentColor.copy(alpha = 0.28f)),
.florisScrollbar(lazyListState),
columns = GridCells.Adaptive(minSize = EmojiBaseWidth),
state = lazyListState,
) {
@@ -322,30 +309,29 @@ private fun EmojiCategoriesTabRow(
onCategoryChange: (EmojiCategory) -> Unit,
emojiHistoryEnabled: Boolean,
) {
val context = LocalContext.current
val inputFeedbackController = LocalInputFeedbackController.current
val tabStyle = FlorisImeTheme.style.get(element = FlorisImeUi.EmojiTab)
val tabStyleFocused = FlorisImeTheme.style.get(element = FlorisImeUi.EmojiTab, isFocus = true)
val unselectedContentColor = tabStyle.foreground.solidColor(context, default = FlorisImeTheme.fallbackContentColor())
val selectedContentColor = tabStyleFocused.foreground.solidColor(context, default = FlorisImeTheme.fallbackContentColor())
val selectedTabIndex = if (emojiHistoryEnabled) {
EmojiCategoryValues.indexOf(activeCategory)
} else {
EmojiCategoryValues.indexOf(activeCategory) - 1
}
val style = rememberSnyggThemeQuery(FlorisImeUi.MediaEmojiTab.elementName)
TabRow(
modifier = Modifier
.fillMaxWidth()
.height(FlorisImeSizing.smartbarHeight),
selectedTabIndex = selectedTabIndex,
containerColor = Color.Transparent,
contentColor = selectedContentColor,
contentColor = style.foreground(),
indicator = { tabPositions ->
val style = rememberSnyggThemeQuery(
elementName = FlorisImeUi.MediaEmojiTab.elementName,
selector = SnyggSelector.FOCUS,
)
TabRowDefaults.PrimaryIndicator(
Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex]),
color = selectedContentColor,
height = 4.dp
height = 4.dp,
color = style.foreground(),
)
},
) {
@@ -359,13 +345,12 @@ private fun EmojiCategoriesTabRow(
onCategoryChange(category)
},
selected = activeCategory == category,
icon = { Icon(
icon = { SnyggIcon(
elementName = FlorisImeUi.MediaEmojiTab.elementName,
selector = if (activeCategory == category) SnyggSelector.FOCUS else SnyggSelector.NONE,
modifier = Modifier.size(ButtonDefaults.IconSize),
imageVector = category.icon(),
contentDescription = null,
) },
unselectedContentColor = unselectedContentColor,
selectedContentColor = selectedContentColor,
)
}
}
@@ -378,9 +363,6 @@ private fun EmojiKey(
preferredSkinTone: EmojiSkinTone,
isPinned: Boolean,
isRecent: Boolean,
contentColor: Color,
fontSize: TextUnit,
fontSizeMultiplier: Float,
onEmojiInput: (Emoji) -> Unit,
onHistoryAction: () -> Unit,
) {
@@ -389,7 +371,7 @@ private fun EmojiKey(
val variations = emojiSet.variations(withoutSkinTone = preferredSkinTone)
var showVariantsBox by remember { mutableStateOf(false) }
Box(
SnyggBox(FlorisImeUi.MediaEmojiKey.elementName,
modifier = Modifier
.aspectRatio(1f)
.pointerInput(Unit) {
@@ -413,10 +395,9 @@ private fun EmojiKey(
modifier = Modifier.align(Alignment.Center),
text = base.value,
emojiCompatInstance = emojiCompatInstance,
color = contentColor,
fontSize = fontSize,
)
if (variations.isNotEmpty() || isPinned || isRecent) {
val style = rememberSnyggThemeQuery(FlorisImeUi.MediaEmojiKeyPopupExtendedIndicator.elementName)
val shape = when (LocalLayoutDirection.current) {
LayoutDirection.Ltr -> VariantsTriangleShapeLtr
LayoutDirection.Rtl -> VariantsTriangleShapeRtl
@@ -426,7 +407,7 @@ private fun EmojiKey(
.align(Alignment.BottomEnd)
.offset(x = (-4).dp, y = (-4).dp)
.size(4.dp)
.background(contentColor, shape),
.background(style.foreground(), shape),
)
}
@@ -448,7 +429,6 @@ private fun EmojiKey(
variations = variations,
visible = showVariantsBox,
emojiCompatInstance = emojiCompatInstance,
fontSizeMultiplier = fontSizeMultiplier,
onEmojiTap = { emoji ->
onEmojiInput(emoji)
showVariantsBox = false
@@ -467,13 +447,10 @@ private fun EmojiVariationsPopup(
variations: List<Emoji>,
visible: Boolean,
emojiCompatInstance: EmojiCompat?,
fontSizeMultiplier: Float,
onEmojiTap: (Emoji) -> Unit,
onDismiss: () -> Unit,
) {
val popupStyle = FlorisImeTheme.style.get(element = FlorisImeUi.EmojiKeyPopup)
val emojiKeyHeight = FlorisImeSizing.smartbarHeight
val context = LocalContext.current
if (visible) {
Popup(
@@ -484,29 +461,25 @@ private fun EmojiVariationsPopup(
},
onDismissRequest = onDismiss,
) {
FlowRow(
SnyggRow(
elementName = FlorisImeUi.MediaEmojiKeyPopupBox.elementName,
modifier = Modifier
.widthIn(max = EmojiBaseWidth * 6)
.snyggShadow(popupStyle)
.snyggBorder(context, popupStyle)
.snyggBackground(context, popupStyle, fallbackColor = FlorisImeTheme.fallbackSurfaceColor()),
.widthIn(max = EmojiBaseWidth * 6),
) {
for (emoji in variations) {
Box(
SnyggBox(
elementName = FlorisImeUi.MediaEmojiKeyPopupElement.elementName,
modifier = Modifier
.pointerInput(Unit) {
detectTapGestures { onEmojiTap(emoji) }
}
.width(EmojiBaseWidth)
.height(emojiKeyHeight)
.padding(all = 4.dp),
.height(emojiKeyHeight),
) {
EmojiText(
modifier = Modifier.align(Alignment.Center),
text = emoji.value,
emojiCompatInstance = emojiCompatInstance,
color = popupStyle.foreground.solidColor(context, default = FlorisImeTheme.fallbackContentColor()),
fontSize = popupStyle.fontSize.spSize(default = EmojiDefaultFontSize) safeTimes fontSizeMultiplier,
)
}
}
@@ -526,7 +499,6 @@ private fun EmojiHistoryPopup(
) {
val prefs by florisPreferenceModel()
val scope = rememberCoroutineScope()
val popupStyle = FlorisImeTheme.style.get(element = FlorisImeUi.EmojiKeyPopup)
val emojiKeyHeight = FlorisImeSizing.smartbarHeight
val context = LocalContext.current
val pinnedUS by prefs.emoji.historyPinnedUpdateStrategy.observeAsState()
@@ -536,7 +508,8 @@ private fun EmojiHistoryPopup(
@Composable
fun Action(icon: ImageVector, action: suspend () -> Unit) {
Box(
SnyggBox(
elementName = FlorisImeUi.MediaEmojiKeyPopupElement.elementName,
modifier = Modifier
.pointerInput(Unit) {
detectTapGestures {
@@ -547,14 +520,11 @@ private fun EmojiHistoryPopup(
}
}
.width(EmojiBaseWidth)
.height(emojiKeyHeight)
.padding(all = 4.dp),
.height(emojiKeyHeight),
) {
Icon(
SnyggIcon(
modifier = Modifier.align(Alignment.Center),
imageVector = icon,
contentDescription = null,
tint = popupStyle.foreground.solidColor(context, default = FlorisImeTheme.fallbackContentColor()),
)
}
}
@@ -569,12 +539,10 @@ private fun EmojiHistoryPopup(
},
onDismissRequest = onDismiss,
) {
FlowRow(
SnyggRow(
elementName = FlorisImeUi.MediaEmojiKeyPopupBox.elementName,
modifier = Modifier
.widthIn(max = EmojiBaseWidth * 6)
.snyggShadow(popupStyle)
.snyggBorder(context, popupStyle)
.snyggBackground(context, popupStyle, fallbackColor = FlorisImeTheme.fallbackSurfaceColor()),
.widthIn(max = EmojiBaseWidth * 6),
) {
if (isCurrentlyPinned) {
Action(

View File

@@ -21,8 +21,6 @@ import dev.patrickgold.florisboard.lib.FlorisLocale
import io.github.reactivecircus.cache4k.Cache
import org.florisboard.lib.kotlin.GuardedByLock
import org.florisboard.lib.kotlin.guardedByLock
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
open class BreakIteratorGroup {
private val charInstances = Cache.Builder().build<FlorisLocale, GuardedByLock<BreakIterator>>()
@@ -32,19 +30,13 @@ open class BreakIteratorGroup {
private val sentenceInstances = Cache.Builder().build<FlorisLocale, GuardedByLock<BreakIterator>>()
suspend fun <R> character(locale: FlorisLocale, action: (BreakIterator) -> R): R {
contract {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
val instance = charInstances.get(locale) {
guardedByLock { BreakIterator.getCharacterInstance(locale.base) }
}
return instance.withLock { action(it) }
return instance.withLock(null, action)
}
suspend fun <R> word(locale: FlorisLocale, action: (BreakIterator) -> R): R {
contract {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
val instance = wordInstances.get(locale) {
guardedByLock { BreakIterator.getWordInstance(locale.base) }
}
@@ -52,9 +44,6 @@ open class BreakIteratorGroup {
}
suspend fun <R> sentence(locale: FlorisLocale, action: (BreakIterator) -> R): R {
contract {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
val instance = sentenceInstances.get(locale) {
guardedByLock { BreakIterator.getSentenceInstance(locale.base) }
}

View File

@@ -96,7 +96,7 @@ class NlpManager(context: Context) {
prefs.suggestion.enabled.observeForever {
assembleCandidates()
}
prefs.suggestion.clipboardContentEnabled.observeForever {
prefs.clipboard.suggestionEnabled.observeForever {
assembleCandidates()
}
prefs.emoji.suggestionEnabled.observeForever {
@@ -370,14 +370,14 @@ class NlpManager(context: Context) {
isPrivateSession: Boolean,
): List<SuggestionCandidate> {
// Check if enabled
if (!prefs.suggestion.clipboardContentEnabled.get()) return emptyList()
if (!prefs.clipboard.suggestionEnabled.get()) return emptyList()
val currentItem = validateClipboardItem(clipboardManager.primaryClip, lastClipboardItemId, content.text)
?: return emptyList()
return buildList {
val now = System.currentTimeMillis()
if ((now - currentItem.creationTimestampMs) < prefs.suggestion.clipboardContentTimeout.get() * 1000) {
if ((now - currentItem.creationTimestampMs) < prefs.clipboard.suggestionTimeout.get() * 1000) {
add(ClipboardSuggestionCandidate(currentItem, sourceProvider = this@ClipboardSuggestionProvider, context = context))
if (currentItem.isSensitive) {
return@buildList

View File

@@ -20,7 +20,6 @@ package dev.patrickgold.florisboard.ime.onehanded
* Static object which contains all possible one-handed mode strings.
*/
enum class OneHandedMode {
OFF,
START,
END;
}

View File

@@ -17,29 +17,26 @@
package dev.patrickgold.florisboard.ime.onehanded
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.ZoomOutMap
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.ime.input.LocalInputFeedbackController
import dev.patrickgold.florisboard.ime.keyboard.FlorisImeSizing
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.lib.compose.stringRes
import org.florisboard.lib.snygg.ui.snyggBackground
import org.florisboard.lib.snygg.ui.solidColor
import org.florisboard.lib.snygg.ui.SnyggColumn
import org.florisboard.lib.snygg.ui.SnyggIcon
import org.florisboard.lib.snygg.ui.SnyggIconButton
@Composable
fun RowScope.OneHandedPanel(
@@ -49,38 +46,43 @@ fun RowScope.OneHandedPanel(
) {
val prefs by florisPreferenceModel()
val inputFeedbackController = LocalInputFeedbackController.current
val oneHandedPanelStyle = FlorisImeTheme.style.get(FlorisImeUi.OneHandedPanel)
val context = LocalContext.current
Column(
SnyggColumn(
FlorisImeUi.OneHandedPanel.elementName,
modifier = modifier
.weight(weight)
.snyggBackground(context, oneHandedPanelStyle)
.height(FlorisImeSizing.imeUiHeight()),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceEvenly,
) {
IconButton(
SnyggIconButton(
FlorisImeUi.OneHandedPanelButton.elementName,
onClick = {
inputFeedbackController.keyPress()
prefs.keyboard.oneHandedMode.set(OneHandedMode.OFF)
prefs.keyboard.oneHandedModeEnabled.set(false)
},
modifier = Modifier.fillMaxWidth()
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f),
) {
Icon(
SnyggIcon(
modifier = Modifier.fillMaxWidth(),
imageVector = Icons.Default.ZoomOutMap,
contentDescription = stringRes(R.string.one_handed__close_btn_content_description),
tint = oneHandedPanelStyle.foreground.solidColor(context),
)
}
IconButton(
SnyggIconButton(
FlorisImeUi.OneHandedPanelButton.elementName,
onClick = {
inputFeedbackController.keyPress()
prefs.keyboard.oneHandedMode.set(panelSide)
},
modifier = Modifier.weight(1f).fillMaxWidth(),
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
) {
Icon(
SnyggIcon(
modifier = Modifier.fillMaxWidth(),
imageVector = if (panelSide == OneHandedMode.START) {
Icons.AutoMirrored.Filled.KeyboardArrowLeft
} else {
@@ -93,7 +95,6 @@ fun RowScope.OneHandedPanel(
R.string.one_handed__move_end_btn_content_description
}
),
tint = oneHandedPanelStyle.foreground.solidColor(context),
)
}
}

View File

@@ -17,79 +17,55 @@
package dev.patrickgold.florisboard.ime.popup
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreHoriz
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import dev.patrickgold.florisboard.ime.keyboard.Key
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.lib.compose.safeTimes
import org.florisboard.lib.snygg.ui.SnyggSurface
import org.florisboard.lib.snygg.ui.snyggBackground
import org.florisboard.lib.snygg.ui.snyggBorder
import org.florisboard.lib.snygg.ui.snyggShadow
import org.florisboard.lib.snygg.ui.solidColor
import org.florisboard.lib.snygg.ui.spSize
import org.florisboard.lib.snygg.SnyggSelector
import org.florisboard.lib.snygg.ui.SnyggBox
import org.florisboard.lib.snygg.ui.SnyggColumn
import org.florisboard.lib.snygg.ui.SnyggIcon
import org.florisboard.lib.snygg.ui.SnyggRow
import org.florisboard.lib.snygg.ui.SnyggText
@Composable
fun PopupBaseBox(
modifier: Modifier = Modifier,
key: Key,
fontSizeMultiplier: Float,
shouldIndicateExtendedPopups: Boolean,
): Unit = with(LocalDensity.current) {
val context = LocalContext.current
val popupStyle = FlorisImeTheme.style.get(
element = FlorisImeUi.KeyPopup,
)
val fontSize = popupStyle.fontSize.spSize() safeTimes fontSizeMultiplier
SnyggSurface(
SnyggBox(
elementName = FlorisImeUi.KeyPopupBox.elementName,
modifier = modifier,
style = popupStyle,
clip = true,
) {
key.label?.let { label ->
Box(
SnyggBox(
modifier = Modifier
.fillMaxWidth()
.height(key.visibleBounds.height.toDp())
.align(Alignment.TopCenter),
) {
Text(
SnyggText(
modifier = Modifier.align(Alignment.Center),
text = label,
color = popupStyle.foreground.solidColor(context),
fontSize = fontSize,
maxLines = 1,
softWrap = false,
)
}
}
if (shouldIndicateExtendedPopups) {
Icon(
modifier = Modifier
.requiredSize(fontSize.toDp() * 0.65f)
.align(Alignment.CenterEnd),
SnyggIcon(
elementName = FlorisImeUi.KeyPopupExtendedIndicator.elementName,
modifier = Modifier.align(Alignment.CenterEnd),
imageVector = Icons.Default.MoreHoriz,
contentDescription = null,
tint = popupStyle.foreground.solidColor(context),
)
}
}
@@ -99,71 +75,40 @@ fun PopupBaseBox(
fun PopupExtBox(
modifier: Modifier = Modifier,
elements: List<List<PopupUiController.Element>>,
fontSizeMultiplier: Float,
elemArrangement: Arrangement.Horizontal,
elemWidth: Dp,
elemHeight: Dp,
activeElementIndex: Int,
): Unit = with(LocalDensity.current) {
val context = LocalContext.current
val popupStyle = FlorisImeTheme.style.get(
element = FlorisImeUi.KeyPopup,
isFocus = false,
)
Column(
modifier = modifier
.snyggShadow(popupStyle)
.snyggBorder(context, popupStyle)
.snyggBackground(context, popupStyle),
) {
SnyggColumn(FlorisImeUi.KeyPopupBox.elementName, modifier = modifier) {
for (row in elements.asReversed()) {
Row(
SnyggRow(
modifier = Modifier
.fillMaxWidth()
.requiredHeight(elemHeight),
horizontalArrangement = elemArrangement,
) {
for (element in row) {
val elemStyle = if (activeElementIndex == element.orderedIndex) {
FlorisImeTheme.style.get(
element = FlorisImeUi.KeyPopup,
isFocus = true,
)
val selector = if (activeElementIndex == element.orderedIndex) {
SnyggSelector.FOCUS
} else {
popupStyle
null
}
val elemFontSize = elemStyle.fontSize.spSize() safeTimes fontSizeMultiplier safeTimes
if (element.data.code == KeyCode.URI_COMPONENT_TLD) { 0.6f } else { 1.0f }
Box(
modifier = Modifier
.size(elemWidth, elemHeight)
.run {
if (activeElementIndex == element.orderedIndex) {
snyggBackground(context, elemStyle)
} else {
this
}
},
SnyggBox(
elementName = FlorisImeUi.KeyPopupElement.elementName,
selector = selector,
modifier = Modifier.size(elemWidth, elemHeight),
) {
element.label?.let { label ->
Text(
SnyggText(
modifier = Modifier.align(Alignment.Center),
text = label,
color = elemStyle.foreground.solidColor(context),
fontSize = elemFontSize,
maxLines = 1,
softWrap = false,
)
}
element.icon?.let { icon ->
Icon(
modifier = Modifier
.requiredSize(elemFontSize.toDp() * 1.1f)
.align(Alignment.Center),
SnyggIcon(
modifier = Modifier.align(Alignment.Center),
imageVector = icon,
contentDescription = null,
tint = elemStyle.foreground.solidColor(context),
)
}
}

View File

@@ -454,7 +454,6 @@ class PopupUiController(
.requiredSize(renderInfo.bounds.size.toDpSize())
.absoluteOffset { renderInfo.bounds.topLeft.toIntOffset() },
key = renderInfo.key,
fontSizeMultiplier = fontSizeMultiplier,
shouldIndicateExtendedPopups = renderInfo.shouldIndicateExtendedPopups && extRenderInfo == null,
)
}
@@ -467,7 +466,6 @@ class PopupUiController(
.requiredSize(renderInfo.bounds.size.toDpSize())
.absoluteOffset { renderInfo.bounds.topLeft.toIntOffset() },
elements = renderInfo.elements,
fontSizeMultiplier = fontSizeMultiplier,
elemArrangement = if (renderInfo.anchorLeft) {
Arrangement.Start
} else {

View File

@@ -48,7 +48,6 @@ fun BottomSheetHostUi(
val bgColorOutOfBounds by animateColorAsState(
if (isShowing) SheetOutOfBoundsBgColorActive else SheetOutOfBoundsBgColorInactive
)
Column(Modifier.background(bgColorOutOfBounds)) {
Box(
modifier = Modifier

View File

@@ -21,17 +21,11 @@ import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.waitForUpOrCancellation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -45,25 +39,23 @@ import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.ime.nlp.ClipboardSuggestionCandidate
import dev.patrickgold.florisboard.ime.nlp.SuggestionCandidate
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.keyboardManager
import dev.patrickgold.florisboard.lib.compose.conditional
import dev.patrickgold.florisboard.lib.compose.florisHorizontalScroll
import dev.patrickgold.florisboard.lib.compose.safeTimes
import dev.patrickgold.florisboard.nlpManager
import dev.patrickgold.florisboard.subtypeManager
import dev.patrickgold.jetpref.datastore.model.observeAsState
import org.florisboard.lib.snygg.ui.snyggBackground
import org.florisboard.lib.snygg.ui.solidColor
import org.florisboard.lib.snygg.ui.spSize
import org.florisboard.lib.snygg.SnyggSelector
import org.florisboard.lib.snygg.ui.SnyggColumn
import org.florisboard.lib.snygg.ui.SnyggIcon
import org.florisboard.lib.snygg.ui.SnyggRow
import org.florisboard.lib.snygg.ui.SnyggSpacer
import org.florisboard.lib.snygg.ui.SnyggText
val CandidatesRowScrollbarHeight = 2.dp
@@ -78,13 +70,10 @@ fun CandidatesRow(modifier: Modifier = Modifier) {
val displayMode by prefs.suggestion.displayMode.observeAsState()
val candidates by nlpManager.activeCandidatesFlow.collectAsState()
val rowStyle = FlorisImeTheme.style.get(FlorisImeUi.SmartbarCandidatesRow)
val spacerStyle = FlorisImeTheme.style.get(FlorisImeUi.SmartbarCandidateSpacer)
Row(
SnyggRow(
elementName = FlorisImeUi.SmartbarCandidatesRow.elementName,
modifier = modifier
.fillMaxSize()
.snyggBackground(context, rowStyle)
.conditional(displayMode == CandidatesDisplayMode.DYNAMIC_SCROLLABLE && candidates.size > 1) {
florisHorizontalScroll(scrollbarHeight = CandidatesRowScrollbarHeight)
},
@@ -115,12 +104,12 @@ fun CandidatesRow(modifier: Modifier = Modifier) {
}
for ((n, candidate) in list.withIndex()) {
if (n > 0) {
Spacer(
SnyggSpacer(
elementName = FlorisImeUi.SmartbarCandidateSpacer.elementName,
modifier = Modifier
.width(1.dp)
.fillMaxHeight(0.6f)
.align(Alignment.CenterVertically)
.snyggBackground(context, spacerStyle),
.align(Alignment.CenterVertically),
)
}
CandidateItem(
@@ -156,24 +145,18 @@ private fun CandidateItem(
onLongPress: () -> Boolean = { false },
longPressDelay: Long,
) = with(LocalDensity.current) {
val context = LocalContext.current
var isPressed by remember { mutableStateOf(false) }
val style = if (candidate is ClipboardSuggestionCandidate) {
FlorisImeTheme.style.get(
element = FlorisImeUi.SmartbarCandidateClip,
isPressed = isPressed,
)
val elementName = if (candidate is ClipboardSuggestionCandidate) {
FlorisImeUi.SmartbarCandidateClip
} else {
FlorisImeTheme.style.get(
element = FlorisImeUi.SmartbarCandidateWord,
isPressed = isPressed,
)
}
FlorisImeUi.SmartbarCandidateWord
}.elementName
Row(
SnyggRow(
elementName = elementName,
selector = if (isPressed) SnyggSelector.PRESSED else SnyggSelector.NONE,
modifier = modifier
.snyggBackground(context, style)
.pointerInput(Unit) {
awaitEachGesture {
val down = awaitFirstDown()
@@ -197,47 +180,27 @@ private fun CandidateItem(
}
isPressed = false
}
}
.padding(horizontal = 12.dp),
},
verticalAlignment = Alignment.CenterVertically,
) {
if (candidate.icon != null) {
Icon(
modifier = Modifier
.requiredSize(
style.fontSize
.spSize()
.toDp() * 1.5f
)
.padding(end = 4.dp),
imageVector = candidate.icon!!,
contentDescription = null,
tint = style.foreground.solidColor(context),
)
SnyggIcon(imageVector = candidate.icon!!)
}
Column(
SnyggColumn(
modifier = if (displayMode == CandidatesDisplayMode.CLASSIC) Modifier.weight(1f) else Modifier,
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
SnyggText(
elementName = null,
attributes = mapOf("auto-commit" to if (candidate.isEligibleForAutoCommit) 1 else 0),
text = candidate.text.toString(),
color = style.foreground.solidColor(context),
fontSize = style.fontSize.spSize(),
fontWeight = if (candidate.isEligibleForAutoCommit) FontWeight.Bold else FontWeight.Normal,
textAlign = TextAlign.Center,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (candidate.secondaryText != null) {
Text(
SnyggText(
elementName = null,
attributes = mapOf("auto-commit" to if (candidate.isEligibleForAutoCommit) 1 else 0),
text = candidate.secondaryText!!.toString(),
color = style.foreground.solidColor(context),
fontSize = style.fontSize.spSize() safeTimes 0.6,
fontWeight = if (candidate.isEligibleForAutoCommit) FontWeight.Bold else FontWeight.Normal,
textAlign = TextAlign.Center,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}

View File

@@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -32,8 +33,22 @@ import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.viewinterop.AndroidView
import dev.patrickgold.florisboard.ime.nlp.NlpInlineAutofillSuggestion
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.lib.compose.florisHorizontalScroll
import dev.patrickgold.florisboard.lib.toIntOffset
import org.florisboard.lib.snygg.SnyggPropertySet
import org.florisboard.lib.snygg.SnyggSinglePropertySet
import org.florisboard.lib.snygg.ui.rememberSnyggThemeQuery
var CachedInlineSuggestionsChipStyleSet: SnyggSinglePropertySet? = null
@Composable
fun InlineSuggestionsStyleCache() {
val chipStyleSet = rememberSnyggThemeQuery(FlorisImeUi.InlineAutofillChip.elementName)
LaunchedEffect(chipStyleSet) {
CachedInlineSuggestionsChipStyleSet = chipStyleSet
}
}
@RequiresApi(Build.VERSION_CODES.R)
@Composable

View File

@@ -25,22 +25,19 @@ import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.UnfoldLess
import androidx.compose.material.icons.filled.UnfoldMore
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
@@ -61,7 +58,6 @@ import dev.patrickgold.florisboard.ime.nlp.NlpInlineAutofill
import dev.patrickgold.florisboard.ime.smartbar.quickaction.QuickActionButton
import dev.patrickgold.florisboard.ime.smartbar.quickaction.QuickActionsRow
import dev.patrickgold.florisboard.ime.smartbar.quickaction.ToggleOverflowPanelAction
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.keyboardManager
import dev.patrickgold.florisboard.lib.compose.horizontalTween
@@ -70,10 +66,10 @@ import dev.patrickgold.florisboard.nlpManager
import dev.patrickgold.jetpref.datastore.model.observeAsState
import dev.patrickgold.jetpref.datastore.ui.vectorResource
import org.florisboard.lib.android.AndroidVersion
import org.florisboard.lib.snygg.ui.snyggBackground
import org.florisboard.lib.snygg.ui.snyggBorder
import org.florisboard.lib.snygg.ui.snyggShadow
import org.florisboard.lib.snygg.ui.solidColor
import org.florisboard.lib.snygg.ui.SnyggIcon
import org.florisboard.lib.snygg.ui.SnyggIconButton
import org.florisboard.lib.snygg.ui.SnyggRow
import org.florisboard.lib.snygg.ui.rememberSnyggThemeQuery
private const val AnimationDuration = 200
@@ -157,65 +153,47 @@ private fun SmartbarMainRow(modifier: Modifier = Modifier) {
val shouldAnimate by prefs.smartbar.sharedActionsExpandWithAnimation.observeAsState()
val smartbarStyle = FlorisImeTheme.style.get(FlorisImeUi.Smartbar)
val primaryActionsToggleStyle = FlorisImeTheme.style.get(FlorisImeUi.SmartbarSharedActionsToggle)
val secondaryActionsToggleStyle = FlorisImeTheme.style.get(FlorisImeUi.SmartbarExtendedActionsToggle)
@Composable
fun SharedActionsToggle() {
IconButton(
SnyggIconButton(
elementName = FlorisImeUi.SmartbarSharedActionsToggle.elementName,
onClick = {
if (/* was */ sharedActionsExpanded) {
keyboardManager.activeState.isActionsOverflowVisible = false
}
prefs.smartbar.sharedActionsExpanded.set(!sharedActionsExpanded)
},
modifier = Modifier.sizeIn(maxHeight = FlorisImeSizing.smartbarHeight).aspectRatio(1f)
) {
Box(
modifier = Modifier
.padding(4.dp)
.fillMaxHeight()
.aspectRatio(1f)
.snyggShadow(primaryActionsToggleStyle)
.snyggBorder(context, primaryActionsToggleStyle)
.snyggBackground(context, primaryActionsToggleStyle),
contentAlignment = Alignment.Center,
val transition = updateTransition(sharedActionsExpanded, label = "sharedActionsExpandedToggleBtn")
val rotation by transition.animateFloat(
transitionSpec = {
if (shouldAnimate) AnimationTween else NoAnimationTween
},
label = "rotation",
) {
val transition = updateTransition(sharedActionsExpanded, label = "sharedActionsExpandedToggleBtn")
val rotation by transition.animateFloat(
transitionSpec = {
if (shouldAnimate) AnimationTween else NoAnimationTween
},
label = "rotation",
) {
if (it) 180f else 0f
}
val arrowIcon = if (flipToggles) {
Icons.AutoMirrored.Default.KeyboardArrowLeft
} else {
Icons.AutoMirrored.Default.KeyboardArrowRight
}
val incognitoIcon = vectorResource(id = R.drawable.ic_incognito)
val incognitoDisplayMode = prefs.keyboard.incognitoDisplayMode.observeAsState()
val isIncognitoMode = keyboardManager.activeState.isIncognitoMode
val icon = if (isIncognitoMode) {
when (incognitoDisplayMode.value) {
IncognitoDisplayMode.REPLACE_SHARED_ACTIONS_TOGGLE -> incognitoIcon!!
IncognitoDisplayMode.DISPLAY_BEHIND_KEYBOARD -> arrowIcon
}
} else {
arrowIcon
}
Icon(
modifier = Modifier.rotate(if (incognitoDisplayMode.value == IncognitoDisplayMode.DISPLAY_BEHIND_KEYBOARD) rotation else 0f),
imageVector = icon,
contentDescription = null,
tint = primaryActionsToggleStyle.foreground.solidColor(
context,
default = FlorisImeTheme.fallbackContentColor()
),
)
if (it) 180f else 0f
}
val arrowIcon = if (flipToggles) {
Icons.AutoMirrored.Default.KeyboardArrowLeft
} else {
Icons.AutoMirrored.Default.KeyboardArrowRight
}
val incognitoIcon = vectorResource(id = R.drawable.ic_incognito)
val incognitoDisplayMode = prefs.keyboard.incognitoDisplayMode.observeAsState()
val isIncognitoMode = keyboardManager.activeState.isIncognitoMode
val icon = if (isIncognitoMode) {
when (incognitoDisplayMode.value) {
IncognitoDisplayMode.REPLACE_SHARED_ACTIONS_TOGGLE -> incognitoIcon!!
IncognitoDisplayMode.DISPLAY_BEHIND_KEYBOARD -> arrowIcon
}
} else {
arrowIcon
}
SnyggIcon(
modifier = Modifier.rotate(if (incognitoDisplayMode.value == IncognitoDisplayMode.DISPLAY_BEHIND_KEYBOARD) rotation else 0f),
imageVector = icon,
)
}
}
@@ -246,10 +224,10 @@ private fun SmartbarMainRow(modifier: Modifier = Modifier) {
exit = exitTransition,
) {
QuickActionsRow(
FlorisImeUi.SmartbarSharedActionsRow.elementName,
modifier = modifier
.fillMaxWidth()
.height(FlorisImeSizing.smartbarHeight),
elementName = FlorisImeUi.SmartbarSharedActionsRow,
)
}
}
@@ -257,7 +235,8 @@ private fun SmartbarMainRow(modifier: Modifier = Modifier) {
@Composable
fun ExtendedActionsToggle() {
IconButton(
SnyggIconButton(
FlorisImeUi.SmartbarExtendedActionsToggle.elementName,
onClick = {
if (/* was */ extendedActionsExpanded) {
keyboardManager.activeState.isActionsOverflowVisible = false
@@ -265,38 +244,25 @@ private fun SmartbarMainRow(modifier: Modifier = Modifier) {
prefs.smartbar.extendedActionsExpanded.set(!extendedActionsExpanded)
},
) {
Box(
val transition = updateTransition(extendedActionsExpanded, label = "smartbarSecondaryRowToggleBtn")
val alpha by transition.animateFloat(label = "alpha") { if (it) 1f else 0f }
val rotation by transition.animateFloat(label = "rotation") { if (it) 180f else 0f }
// Expanded icon
SnyggIcon(
FlorisImeUi.SmartbarExtendedActionsToggle.elementName,
modifier = Modifier
.padding(4.dp)
.fillMaxHeight()
.aspectRatio(1f)
.snyggShadow(secondaryActionsToggleStyle)
.snyggBorder(context, secondaryActionsToggleStyle)
.snyggBackground(context, secondaryActionsToggleStyle),
contentAlignment = Alignment.Center,
) {
val transition = updateTransition(extendedActionsExpanded, label = "smartbarSecondaryRowToggleBtn")
val alpha by transition.animateFloat(label = "alpha") { if (it) 1f else 0f }
val rotation by transition.animateFloat(label = "rotation") { if (it) 180f else 0f }
// Expanded icon
Icon(
modifier = Modifier
.alpha(alpha)
.rotate(rotation),
imageVector = Icons.Default.UnfoldLess,
contentDescription = null,
tint = secondaryActionsToggleStyle.foreground.solidColor(context),
)
// Not expanded icon
Icon(
modifier = Modifier
.alpha(1f - alpha)
.rotate(rotation - 180f),
imageVector = Icons.Default.UnfoldMore,
contentDescription = null,
tint = secondaryActionsToggleStyle.foreground.solidColor(context),
)
}
.alpha(alpha)
.rotate(rotation),
imageVector = Icons.Default.UnfoldLess,
)
// Not expanded icon
SnyggIcon(
FlorisImeUi.SmartbarExtendedActionsToggle.elementName,
modifier = Modifier
.alpha(1f - alpha)
.rotate(rotation - 180f),
imageVector = Icons.Default.UnfoldMore,
)
}
}
@@ -338,11 +304,11 @@ private fun SmartbarMainRow(modifier: Modifier = Modifier) {
}
}
Row(
SnyggRow(
FlorisImeUi.Smartbar.elementName,
modifier = modifier
.fillMaxWidth()
.height(FlorisImeSizing.smartbarHeight)
.snyggBackground(context, smartbarStyle),
.height(FlorisImeSizing.smartbarHeight),
) {
when (smartbarLayout) {
SmartbarLayout.SUGGESTIONS_ONLY -> {
@@ -357,7 +323,7 @@ private fun SmartbarMainRow(modifier: Modifier = Modifier) {
if (shouldShowInlineSuggestionsUi) {
InlineSuggestionsUi(inlineSuggestions)
} else {
QuickActionsRow(elementName = FlorisImeUi.SmartbarSharedActionsRow)
QuickActionsRow(FlorisImeUi.SmartbarSharedActionsRow.elementName)
}
}
@@ -390,16 +356,16 @@ private fun SmartbarMainRow(modifier: Modifier = Modifier) {
@Composable
private fun SmartbarSecondaryRow(modifier: Modifier = Modifier) {
val context = LocalContext.current
val prefs by florisPreferenceModel()
val smartbarLayout by prefs.smartbar.layout.observeAsState()
val secondaryRowStyle = FlorisImeTheme.style.get(FlorisImeUi.SmartbarExtendedActionsRow)
val secondaryRowStyle = rememberSnyggThemeQuery(FlorisImeUi.SmartbarExtendedActionsRow.elementName)
val windowStyle = rememberSnyggThemeQuery(FlorisImeUi.Window.elementName)
val extendedActionsExpanded by prefs.smartbar.extendedActionsExpanded.observeAsState()
val extendedActionsPlacement by prefs.smartbar.extendedActionsPlacement.observeAsState()
val background = secondaryRowStyle.background.solidColor(context).let { color ->
val background = secondaryRowStyle.background().let { color ->
if (extendedActionsPlacement == ExtendedActionsPlacement.OVERLAY_APP_UI) {
if (color.isUnspecified || color.alpha == 0f) {
FlorisImeTheme.style.get(FlorisImeUi.Keyboard).background.solidColor(context, default = Color.Black)
windowStyle.background(default = Color.Black)
} else {
color
}
@@ -414,11 +380,11 @@ private fun SmartbarSecondaryRow(modifier: Modifier = Modifier) {
exit = VerticalExitTransition,
) {
QuickActionsRow(
FlorisImeUi.SmartbarExtendedActionsRow.elementName,
modifier = modifier
.fillMaxWidth()
.height(FlorisImeSizing.smartbarHeight)
.background(background),
elementName = FlorisImeUi.SmartbarExtendedActionsRow,
)
}
}

View File

@@ -89,6 +89,7 @@ fun QuickAction.computeDisplayName(evaluator: ComputingEvaluator): String {
KeyCode.CLIPBOARD_SELECT_ALL -> R.string.quick_action__clipboard_select_all
KeyCode.IME_UI_MODE_CLIPBOARD -> R.string.quick_action__ime_ui_mode_clipboard
KeyCode.IME_UI_MODE_MEDIA -> R.string.quick_action__ime_ui_mode_media
KeyCode.LANGUAGE_SWITCH -> R.string.quick_action__language_switch
KeyCode.SETTINGS -> R.string.quick_action__settings
KeyCode.UNDO -> R.string.quick_action__undo
KeyCode.REDO -> R.string.quick_action__redo
@@ -97,7 +98,7 @@ fun QuickAction.computeDisplayName(evaluator: ComputingEvaluator): String {
KeyCode.TOGGLE_AUTOCORRECT -> R.string.quick_action__toggle_autocorrect
KeyCode.VOICE_INPUT -> R.string.quick_action__voice_input
// TODO: In the future this will be merged into the resize keyboard panel, for now it is a separate action
KeyCode.COMPACT_LAYOUT_TO_RIGHT -> R.string.quick_action__one_handed_mode
KeyCode.TOGGLE_COMPACT_LAYOUT -> R.string.quick_action__one_handed_mode
KeyCode.DRAG_MARKER -> if (evaluator.state.debugShowDragAndDropHelpers) {
R.string.quick_action__drag_marker
} else {
@@ -125,6 +126,7 @@ fun QuickAction.computeTooltip(evaluator: ComputingEvaluator): String {
KeyCode.CLIPBOARD_SELECT_ALL -> R.string.quick_action__clipboard_select_all__tooltip
KeyCode.IME_UI_MODE_CLIPBOARD -> R.string.quick_action__ime_ui_mode_clipboard__tooltip
KeyCode.IME_UI_MODE_MEDIA -> R.string.quick_action__ime_ui_mode_media__tooltip
KeyCode.LANGUAGE_SWITCH -> R.string.quick_action__language_switch__tooltip
KeyCode.SETTINGS -> R.string.quick_action__settings__tooltip
KeyCode.UNDO -> R.string.quick_action__undo__tooltip
KeyCode.REDO -> R.string.quick_action__redo__tooltip
@@ -133,7 +135,7 @@ fun QuickAction.computeTooltip(evaluator: ComputingEvaluator): String {
KeyCode.TOGGLE_AUTOCORRECT -> R.string.quick_action__toggle_autocorrect__tooltip
KeyCode.VOICE_INPUT -> R.string.quick_action__voice_input__tooltip
// TODO: In the future this will be merged into the resize keyboard panel, for now it is a separate action
KeyCode.COMPACT_LAYOUT_TO_RIGHT -> R.string.quick_action__one_handed_mode__tooltip
KeyCode.TOGGLE_COMPACT_LAYOUT -> R.string.quick_action__one_handed_mode__tooltip
KeyCode.DRAG_MARKER -> if (evaluator.state.debugShowDragAndDropHelpers) {
R.string.quick_action__drag_marker__tooltip
} else {

View File

@@ -26,7 +26,7 @@ import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.plus
import kotlinx.serialization.modules.polymorphic
private val QuickActionJsonConfig = Json(DefaultJsonConfig) {
val QuickActionJsonConfig = Json(DefaultJsonConfig) {
classDiscriminator = "$"
encodeDefaults = false
ignoreUnknownKeys = true
@@ -47,7 +47,7 @@ data class QuickActionArrangement(
val dynamicActions: List<QuickAction>,
val hiddenActions: List<QuickAction>,
) {
fun contains(action: QuickAction): Boolean {
operator fun contains(action: QuickAction): Boolean {
return stickyAction == action || dynamicActions.contains(action) || hiddenActions.contains(action)
}
@@ -61,7 +61,7 @@ data class QuickActionArrangement(
QuickAction.InsertKey(TextKeyData.TOGGLE_INCOGNITO_MODE),
QuickAction.InsertKey(TextKeyData.IME_UI_MODE_CLIPBOARD),
QuickAction.InsertKey(TextKeyData.IME_UI_MODE_MEDIA),
QuickAction.InsertKey(TextKeyData.COMPACT_LAYOUT_TO_RIGHT),
QuickAction.InsertKey(TextKeyData.TOGGLE_COMPACT_LAYOUT),
QuickAction.InsertKey(TextKeyData.TOGGLE_AUTOCORRECT),
QuickAction.InsertKey(TextKeyData.ARROW_UP),
QuickAction.InsertKey(TextKeyData.ARROW_DOWN),
@@ -72,6 +72,7 @@ data class QuickActionArrangement(
QuickAction.InsertKey(TextKeyData.CLIPBOARD_CUT),
QuickAction.InsertKey(TextKeyData.CLIPBOARD_PASTE),
QuickAction.InsertKey(TextKeyData.CLIPBOARD_SELECT_ALL),
QuickAction.InsertKey(TextKeyData.LANGUAGE_SWITCH),
),
hiddenActions = listOf(
),

View File

@@ -16,11 +16,7 @@
package dev.patrickgold.florisboard.ime.smartbar.quickaction
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.waitForUpOrCancellation
@@ -30,50 +26,33 @@ import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import dev.patrickgold.compose.tooltip.tooltip
import dev.patrickgold.compose.tooltip.PlainTooltip
import dev.patrickgold.florisboard.FlorisImeService
import dev.patrickgold.florisboard.ime.keyboard.ComputingEvaluator
import dev.patrickgold.florisboard.ime.keyboard.FlorisImeSizing
import dev.patrickgold.florisboard.ime.keyboard.computeImageVector
import dev.patrickgold.florisboard.ime.keyboard.computeLabel
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import org.florisboard.lib.snygg.ui.shape
import org.florisboard.lib.snygg.ui.snyggBorder
import org.florisboard.lib.snygg.ui.snyggClip
import org.florisboard.lib.snygg.ui.snyggShadow
import org.florisboard.lib.snygg.ui.solidColor
private val BackgroundAnimationSpec = tween<Color>(durationMillis = 150, easing = FastOutSlowInEasing)
private val DebugHelperColor = Color.Red.copy(alpha = 0.5f)
import org.florisboard.lib.snygg.SnyggSelector
import org.florisboard.lib.snygg.ui.SnyggBox
import org.florisboard.lib.snygg.ui.SnyggIcon
import org.florisboard.lib.snygg.ui.SnyggText
enum class QuickActionBarType {
INTERACTIVE_BUTTON,
INTERACTIVE_TILE,
STATIC_TILE;
EDITOR_TILE;
}
@Composable
@@ -88,47 +67,17 @@ fun QuickActionButton(
val inputFeedbackController = FlorisImeService.inputFeedbackController()
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
val isEnabled = type == QuickActionBarType.STATIC_TILE || evaluator.evaluateEnabled(action.keyData())
val element = when (type) {
val isEnabled = type == QuickActionBarType.EDITOR_TILE || evaluator.evaluateEnabled(action.keyData())
val elementName = when (type) {
QuickActionBarType.INTERACTIVE_BUTTON -> FlorisImeUi.SmartbarActionKey
else -> FlorisImeUi.SmartbarActionTile
QuickActionBarType.INTERACTIVE_TILE -> FlorisImeUi.SmartbarActionTile
QuickActionBarType.EDITOR_TILE -> FlorisImeUi.SmartbarActionsEditorTile
}.elementName
val selector = when {
isPressed -> SnyggSelector.PRESSED
!isEnabled -> SnyggSelector.DISABLED
else -> null
}
// We always need to know both state's styles to animate smoothly
val actionStyleNotPressed = FlorisImeTheme.style.get(
element = element,
code = action.keyData().code,
isPressed = false,
isDisabled = !isEnabled,
)
val actionStylePressed = FlorisImeTheme.style.get(
element = element,
code = action.keyData().code,
isPressed = true,
isDisabled = !isEnabled,
)
val actionStyle = if (isPressed) actionStylePressed else actionStyleNotPressed
val bgColor by animateColorAsState(
targetValue = if (isPressed) {
actionStylePressed.background.solidColor(context)
} else {
if (actionStyleNotPressed.background.solidColor(context).alpha == 0f) {
actionStylePressed.background.solidColor(context).copy(0f)
} else {
actionStyleNotPressed.background.solidColor(context)
}
},
animationSpec = BackgroundAnimationSpec, label = "bgColor",
)
val fgColor = when (action.keyData().code) {
KeyCode.DRAG_MARKER -> {
DebugHelperColor
}
else -> {
actionStyle.foreground.solidColor(context, default = FlorisImeTheme.fallbackContentColor())
}
}
val fgAlpha = if (action.keyData().code == KeyCode.NOOP) 0.5f else 1f
// Need to manually cancel an action if this composable suddenly leaves the composition to prevent the key from
// being stuck in the pressed state
@@ -140,92 +89,70 @@ fun QuickActionButton(
}
}
val tooltipModifier = if (type == QuickActionBarType.INTERACTIVE_BUTTON) {
Modifier.tooltip(action.computeTooltip(evaluator))
} else {
Modifier
}
Box(
modifier = modifier
.aspectRatio(1f)
.alpha(fgAlpha)
.snyggShadow(actionStyle)
.snyggBorder(context, actionStyle)
.background(bgColor, actionStyle.shape.shape())
.snyggClip(actionStyle)
.indication(interactionSource, LocalIndication.current)
.pointerInput(action, isEnabled) {
awaitEachGesture {
val down = awaitFirstDown()
down.consume()
if (isEnabled && type != QuickActionBarType.STATIC_TILE) {
val press = PressInteraction.Press(down.position)
inputFeedbackController?.keyPress(TextKeyData.UNSPECIFIED)
interactionSource.tryEmit(press)
action.onPointerDown(context)
val up = waitForUpOrCancellation()
if (up != null) {
up.consume()
interactionSource.tryEmit(PressInteraction.Release(press))
action.onPointerUp(context)
} else {
interactionSource.tryEmit(PressInteraction.Cancel(press))
action.onPointerCancel(context)
PlainTooltip(action.computeTooltip(evaluator), enabled = type == QuickActionBarType.INTERACTIVE_BUTTON) {
SnyggBox(
elementName = elementName,
attributes = mapOf(FlorisImeUi.Attr.Code to action.keyData().code),
selector = selector,
modifier = modifier,
clickAndSemanticsModifier = Modifier
.aspectRatio(1f)
.indication(interactionSource, LocalIndication.current)
.pointerInput(action, isEnabled) {
awaitEachGesture {
val down = awaitFirstDown()
down.consume()
if (isEnabled && type != QuickActionBarType.EDITOR_TILE) {
val press = PressInteraction.Press(down.position)
inputFeedbackController?.keyPress(TextKeyData.UNSPECIFIED)
interactionSource.tryEmit(press)
action.onPointerDown(context)
val up = waitForUpOrCancellation()
if (up != null) {
up.consume()
interactionSource.tryEmit(PressInteraction.Release(press))
action.onPointerUp(context)
} else {
interactionSource.tryEmit(PressInteraction.Cancel(press))
action.onPointerCancel(context)
}
}
}
}
}
.then(tooltipModifier)
.padding(8.dp),
contentAlignment = Alignment.Center,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
// Render foreground
Box(
modifier = Modifier.size(FlorisImeSizing.smartbarHeight * 0.5f),
contentAlignment = Alignment.Center,
) {
when (action) {
is QuickAction.InsertKey -> {
val (imageVector, label) = remember(action, evaluator) {
evaluator.computeImageVector(action.data) to evaluator.computeLabel(action.data)
},
contentAlignment = Alignment.Center,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
// Render foreground
Box(
modifier = Modifier.size(FlorisImeSizing.smartbarHeight * 0.5f),
contentAlignment = Alignment.Center,
) {
when (action) {
is QuickAction.InsertKey -> {
val (imageVector, label) = remember(action, evaluator) {
evaluator.computeImageVector(action.data) to evaluator.computeLabel(action.data)
}
if (imageVector != null) {
SnyggIcon(imageVector = imageVector)
} else if (label != null) {
SnyggText(text = label)
}
}
if (imageVector != null) {
Icon(
imageVector = imageVector,
contentDescription = null,
tint = fgColor,
)
} else if (label != null) {
Text(
text = label,
color = fgColor,
is QuickAction.InsertText -> {
SnyggText(
text = action.data.firstOrNull().toString().ifBlank { "?" },
)
}
}
is QuickAction.InsertText -> {
Text(
text = action.data.firstOrNull().toString().ifBlank { "?" },
color = fgColor,
fontSize = 16.sp,
)
}
}
}
// Render additional info if this is a tile
if (type != QuickActionBarType.INTERACTIVE_BUTTON) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = action.computeDisplayName(evaluator = evaluator),
color = fgColor,
fontSize = if (type == QuickActionBarType.STATIC_TILE) 10.sp else 13.sp,
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
// Render additional info if this is a tile
if (type != QuickActionBarType.INTERACTIVE_BUTTON) {
SnyggText(
text = action.computeDisplayName(evaluator = evaluator),
)
}
}
}
}

View File

@@ -18,13 +18,14 @@ package dev.patrickgold.florisboard.ime.smartbar.quickaction
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
@@ -33,7 +34,6 @@ import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
@@ -47,8 +47,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
@@ -58,25 +56,24 @@ import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.ime.keyboard.FlorisImeSizing
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.keyboardManager
import dev.patrickgold.florisboard.lib.compose.FlorisIconButton
import dev.patrickgold.florisboard.lib.compose.safeTimes
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.toIntOffset
import org.florisboard.lib.snygg.SnyggPropertySet
import org.florisboard.lib.snygg.ui.snyggBackground
import org.florisboard.lib.snygg.ui.snyggClip
import org.florisboard.lib.snygg.ui.solidColor
import org.florisboard.lib.snygg.ui.spSize
import org.florisboard.lib.snygg.ui.SnyggBox
import org.florisboard.lib.snygg.ui.SnyggButton
import org.florisboard.lib.snygg.ui.SnyggColumn
import org.florisboard.lib.snygg.ui.SnyggIcon
import org.florisboard.lib.snygg.ui.SnyggIconButton
import org.florisboard.lib.snygg.ui.SnyggRow
import org.florisboard.lib.snygg.ui.SnyggText
private const val ItemNotFound = -1
private val NoopAction = QuickAction.InsertKey(TextKeyData(code = KeyCode.NOOP))
private val DragMarkerAction = QuickAction.InsertKey(TextKeyData(code = KeyCode.DRAG_MARKER))
@Composable
fun QuickActionsEditorPanel(modifier: Modifier = Modifier) {
fun QuickActionsEditorPanel() {
val prefs by florisPreferenceModel()
val context = LocalContext.current
val keyboardManager by context.keyboardManager()
@@ -99,10 +96,6 @@ fun QuickActionsEditorPanel(modifier: Modifier = Modifier) {
var activeDragPosition by remember { mutableStateOf(IntOffset.Zero) }
var activeDragSize by remember { mutableStateOf(IntSize.Zero) }
val panelStyle = FlorisImeTheme.style.get(FlorisImeUi.SmartbarActionsEditor)
val headerStyle = FlorisImeTheme.style.get(FlorisImeUi.SmartbarActionsEditorHeader)
val subheaderStyle = FlorisImeTheme.style.get(FlorisImeUi.SmartbarActionsEditorSubheader)
fun findItemForOffsetOrClosestInRow(offset: IntOffset): LazyGridItemInfo? {
var closestItemInRow: LazyGridItemInfo? = null
// Using manual for loop with indices instead of firstOrNull() because this method gets
@@ -249,35 +242,34 @@ fun QuickActionsEditorPanel(modifier: Modifier = Modifier) {
}
}
Column(
modifier = modifier
.snyggBackground(context, panelStyle, fallbackColor = FlorisImeTheme.fallbackSurfaceColor())
.snyggClip(panelStyle),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.snyggBackground(context, headerStyle),
SnyggColumn(FlorisImeUi.SmartbarActionsEditor.elementName, modifier = Modifier.safeDrawingPadding()) {
SnyggRow(
elementName = FlorisImeUi.SmartbarActionsEditorHeader.elementName,
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
FlorisIconButton(
onClick = {
keyboardManager.activeState.isActionsEditorVisible = false
},
icon = Icons.AutoMirrored.Filled.KeyboardArrowLeft,
iconColor = headerStyle.foreground.solidColor(context, default = FlorisImeTheme.fallbackContentColor()),
)
Text(
// Extra box wrapper is needed to enforce size constraint but still allow for Snygg margin to be used
Box(modifier = Modifier.size(48.dp)) {
SnyggIconButton(
elementName = FlorisImeUi.SmartbarActionsEditorHeaderButton.elementName,
modifier = Modifier.fillMaxHeight().aspectRatio(1f),
onClick = {
keyboardManager.activeState.isActionsEditorVisible = false
},
) {
SnyggIcon(
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowLeft,
)
}
}
SnyggText(
modifier = Modifier.weight(1f),
text = stringRes(R.string.quick_actions_editor__header),
color = headerStyle.foreground.solidColor(context, default = FlorisImeTheme.fallbackContentColor()),
fontSize = headerStyle.fontSize.spSize(),
textAlign = TextAlign.Center,
)
Spacer(Modifier.size(48.dp))
}
Box {
SnyggBox(FlorisImeUi.SmartbarActionsEditorTileGrid.elementName) {
LazyVerticalGrid(
modifier = Modifier
.pointerInput(Unit) {
@@ -295,7 +287,6 @@ fun QuickActionsEditorPanel(modifier: Modifier = Modifier) {
val n = if (stickyAction != NoopAction) 1 else 0
Subheader(
text = stringRes(R.string.quick_actions_editor__subheader_sticky_action, "n" to n),
style = subheaderStyle,
)
}
item(key = keyOf(stickyAction)) {
@@ -303,14 +294,13 @@ fun QuickActionsEditorPanel(modifier: Modifier = Modifier) {
modifier = Modifier.animateItem(),
action = stickyAction,
evaluator = evaluator,
type = QuickActionBarType.STATIC_TILE,
type = QuickActionBarType.EDITOR_TILE,
)
}
item(span = { GridItemSpan(maxLineSpan) }) {
val n = dynamicActions.count { it != NoopAction }
Subheader(
text = stringRes(R.string.quick_actions_editor__subheader_dynamic_actions, "n" to n),
style = subheaderStyle,
)
}
itemsIndexed(dynamicActions, key = { i, a -> keyOf(a) ?: i }) { _, action ->
@@ -318,14 +308,13 @@ fun QuickActionsEditorPanel(modifier: Modifier = Modifier) {
modifier = Modifier.animateItem(),
action = action,
evaluator = evaluator,
type = QuickActionBarType.STATIC_TILE,
type = QuickActionBarType.EDITOR_TILE,
)
}
item(span = { GridItemSpan(maxLineSpan) }) {
val n = hiddenActions.count { it != NoopAction }
Subheader(
text = stringRes(R.string.quick_actions_editor__subheader_hidden_actions, "n" to n),
style = subheaderStyle,
)
}
itemsIndexed(hiddenActions, key = { i, a -> keyOf(a) ?: i }) { _, action ->
@@ -333,7 +322,7 @@ fun QuickActionsEditorPanel(modifier: Modifier = Modifier) {
modifier = Modifier.animateItem(),
action = action,
evaluator = evaluator,
type = QuickActionBarType.STATIC_TILE,
type = QuickActionBarType.EDITOR_TILE,
)
}
}
@@ -348,7 +337,7 @@ fun QuickActionsEditorPanel(modifier: Modifier = Modifier) {
.offset(-size.width / 2, -size.height / 2),
action = activeDragAction!!,
evaluator = evaluator,
type = QuickActionBarType.STATIC_TILE,
type = QuickActionBarType.EDITOR_TILE,
)
}
}
@@ -358,17 +347,11 @@ fun QuickActionsEditorPanel(modifier: Modifier = Modifier) {
@Composable
private fun Subheader(
text: String,
style: SnyggPropertySet,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
Text(
modifier = modifier
.fillMaxWidth()
.padding(top = 16.dp, bottom = 8.dp, start = 16.dp, end = 16.dp),
SnyggText(
elementName = FlorisImeUi.SmartbarActionsEditorSubheader.elementName,
modifier = modifier.fillMaxWidth(),
text = text,
color = style.foreground.solidColor(context, default = FlorisImeTheme.fallbackContentColor()),
fontWeight = FontWeight.Bold,
fontSize = style.fontSize.spSize() safeTimes 0.8f,
)
}

View File

@@ -16,10 +16,8 @@
package dev.patrickgold.florisboard.ime.smartbar.quickaction
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
@@ -31,17 +29,16 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.ime.keyboard.FlorisImeSizing
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.keyboardManager
import dev.patrickgold.florisboard.lib.compose.stringRes
import org.florisboard.lib.snygg.ui.SnyggButton
import org.florisboard.lib.snygg.ui.snyggBackground
import dev.patrickgold.jetpref.datastore.model.observeAsState
import org.florisboard.lib.snygg.ui.SnyggBox
import org.florisboard.lib.snygg.ui.SnyggText
@Composable
fun QuickActionsOverflowPanel() {
@@ -63,19 +60,15 @@ fun QuickActionsOverflowPanel() {
actionArrangement.dynamicActions.takeLast(dynamicActionsCountToShow)
}
val panelStyle = FlorisImeTheme.style.get(FlorisImeUi.SmartbarActionsOverflow)
val buttonStyle = FlorisImeTheme.style.get(FlorisImeUi.SmartbarActionsOverflowCustomizeButton)
Box(
SnyggBox(
elementName = FlorisImeUi.SmartbarActionsOverflow.elementName,
modifier = Modifier
.fillMaxWidth()
.height(FlorisImeSizing.keyboardUiHeight())
.snyggBackground(context, panelStyle),
.height(FlorisImeSizing.keyboardUiHeight()),
) {
LazyVerticalGrid(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp),
.fillMaxWidth(),
columns = GridCells.Adaptive(FlorisImeSizing.smartbarHeight * 2.2f),
) {
items(visibleActions) { action ->
@@ -87,13 +80,15 @@ fun QuickActionsOverflowPanel() {
}
item(span = { GridItemSpan(maxLineSpan) }) {
SnyggButton(
elementName = FlorisImeUi.SmartbarActionsOverflowCustomizeButton.elementName,
onClick = { keyboardManager.activeState.isActionsEditorVisible = true },
modifier = Modifier
.wrapContentWidth()
.padding(vertical = 8.dp),
text = stringRes(R.string.quick_actions_overflow__customize_actions_button),
style = buttonStyle,
)
.wrapContentWidth(),
) {
SnyggText(
text = stringRes(R.string.quick_actions_overflow__customize_actions_button),
)
}
}
}
}

View File

@@ -18,7 +18,6 @@ package dev.patrickgold.florisboard.ime.smartbar.quickaction
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
@@ -32,10 +31,9 @@ import androidx.compose.ui.platform.LocalDensity
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.ime.smartbar.SmartbarLayout
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
import dev.patrickgold.florisboard.keyboardManager
import org.florisboard.lib.snygg.ui.snyggBackground
import dev.patrickgold.jetpref.datastore.model.observeAsState
import org.florisboard.lib.snygg.ui.SnyggRow
internal val ToggleOverflowPanelAction = QuickAction.InsertKey(TextKeyData.TOGGLE_ACTIONS_OVERFLOW)
@@ -67,8 +65,6 @@ fun QuickActionsRow(
val showOverflowAction = actionArrangement.stickyAction != null ||
smartbarLayout != SmartbarLayout.SUGGESTIONS_ACTIONS_SHARED || !sharedActionsExpanded
val rowStyle = FlorisImeTheme.style.get(elementName)
BoxWithConstraints(modifier = modifier.fillMaxSize()) {
val width = constraints.maxWidth.toDp()
val height = constraints.maxHeight.toDp()
@@ -85,10 +81,9 @@ fun QuickActionsRow(
}.coerceAtLeast(0)
}
Row(
modifier = Modifier
.fillMaxSize()
.snyggBackground(context, rowStyle),
SnyggRow(
elementName = elementName,
modifier = Modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly,
) {

View File

@@ -1,93 +0,0 @@
/*
* Copyright (C) 2025 The FlorisBoard Contributors
*
* 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
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.extensionManager
import dev.patrickgold.florisboard.ime.keyboard.FlorisImeSizing
import dev.patrickgold.florisboard.keyboardManager
import dev.patrickgold.florisboard.lib.util.launchUrl
import org.florisboard.lib.snygg.SnyggPropertySet
import org.florisboard.lib.snygg.ui.SnyggButton
import org.florisboard.lib.snygg.ui.SnyggSurface
import org.florisboard.lib.snygg.ui.solidColor
import org.florisboard.lib.snygg.value.SnyggRoundedCornerDpShapeValue
import org.florisboard.lib.snygg.value.SnyggSolidColorValue
@Composable
fun HowDidWeGetHere() {
val context = LocalContext.current
val extensionManager by context.extensionManager()
val keyboardManager by context.keyboardManager()
val style = SnyggPropertySet(mapOf(
"background" to SnyggSolidColorValue(Color.Yellow),
"foreground" to SnyggSolidColorValue(Color.Black),
"shape" to SnyggRoundedCornerDpShapeValue(16.dp, 16.dp, 16.dp, 16.dp, RoundedCornerShape(16.dp)),
))
@Composable
fun ColoredText(text: String) {
Text(
text = text,
color = style.foreground.solidColor(context),
)
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(FlorisImeSizing.keyboardUiHeight())
.padding(8.dp),
) {
SnyggSurface(style = style) {
Column(modifier = Modifier.padding(8.dp)) {
ColoredText(text = "Challenge Complete! - How did we get here?\n")
ColoredText(text = "You landed in a state which shouldn't be reachable, possibly related to the \"All keys invisible\" bug. Please report this bug and the steps to reproduce to the devs using the button below. Thanks!")
Row {
SnyggButton(
onClick = {
keyboardManager.activeState.rawValue = 0u
extensionManager.init()
},
text = "Try reset keyboard",
style = style,
)
SnyggButton(
onClick = {
context.launchUrl("https://github.com/florisboard/florisboard/issues/2362")
},
text = "Report bug to devs",
style = style,
)
}
}
}
}
}

View File

@@ -20,7 +20,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
@@ -33,16 +32,17 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.LayoutDirection
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.ime.keyboard.KeyboardMode
import dev.patrickgold.florisboard.ime.smartbar.IncognitoDisplayMode
import dev.patrickgold.florisboard.ime.smartbar.InlineSuggestionsStyleCache
import dev.patrickgold.florisboard.ime.smartbar.Smartbar
import dev.patrickgold.florisboard.ime.smartbar.quickaction.QuickActionsOverflowPanel
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyboardLayout
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.keyboardManager
import dev.patrickgold.jetpref.datastore.model.observeAsState
import org.florisboard.lib.snygg.ui.solidColor
import org.florisboard.lib.snygg.ui.SnyggBox
import org.florisboard.lib.snygg.ui.SnyggColumn
import org.florisboard.lib.snygg.ui.SnyggIcon
@Composable
fun TextInputLayout(
@@ -56,6 +56,8 @@ fun TextInputLayout(
val state by keyboardManager.activeState.collectAsState()
val evaluator by keyboardManager.activeEvaluator.collectAsState()
InlineSuggestionsStyleCache()
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
Column(
modifier = modifier
@@ -67,31 +69,19 @@ fun TextInputLayout(
QuickActionsOverflowPanel()
} else {
Box {
val showIncognitoIcon = evaluator.state.isIncognitoMode && prefs.keyboard.incognitoDisplayMode.observeAsState().value == IncognitoDisplayMode.DISPLAY_BEHIND_KEYBOARD
val incognitoDisplayMode by prefs.keyboard.incognitoDisplayMode.observeAsState()
val showIncognitoIcon = evaluator.state.isIncognitoMode &&
incognitoDisplayMode == IncognitoDisplayMode.DISPLAY_BEHIND_KEYBOARD
if (showIncognitoIcon) {
val indicatorStyle = FlorisImeTheme.style.get(FlorisImeUi.IncognitoModeIndicator)
Icon(
SnyggIcon(
FlorisImeUi.IncognitoModeIndicator.elementName,
modifier = Modifier
.matchParentSize()
.align(Alignment.Center),
painter = painterResource(R.drawable.ic_incognito),
contentDescription = null,
tint = indicatorStyle.foreground.solidColor(
context, default = FlorisImeTheme.fallbackContentColor().copy(alpha = 0.067f),
),
)
}
val debugLayoutResult by keyboardManager.layoutManager
.debugLayoutComputationResultFlow.collectAsState()
if (state.keyboardMode != KeyboardMode.EDITING) {
if (debugLayoutResult?.allLayoutsSuccess() == true) {
TextKeyboardLayout(evaluator = evaluator)
} else {
HowDidWeGetHere()
}
} else {
HowDidWeGetHere()
}
TextKeyboardLayout(evaluator = evaluator)
}
}
}

View File

@@ -42,6 +42,7 @@ enum class SwipeAction {
SELECT_WORDS_PRECISELY,
SHIFT,
SHOW_INPUT_METHOD_PICKER,
SHOW_SUBTYPE_PICKER,
SWITCH_TO_PREV_SUBTYPE,
SWITCH_TO_NEXT_SUBTYPE,
SWITCH_TO_CLIPBOARD_CONTEXT,

View File

@@ -68,6 +68,7 @@ object KeyCode {
const val CLIPBOARD_CLEAR_FULL_HISTORY = -37
const val CLIPBOARD_CLEAR_PRIMARY_CLIP = -38
const val TOGGLE_COMPACT_LAYOUT = -110
const val COMPACT_LAYOUT_TO_LEFT = -111
const val COMPACT_LAYOUT_TO_RIGHT = -112
const val SPLIT_LAYOUT = -113
@@ -95,6 +96,7 @@ object KeyCode {
const val IME_PREV_SUBTYPE = -225
const val IME_NEXT_SUBTYPE = -226
const val LANGUAGE_SWITCH = -227
const val SHOW_SUBTYPE_PICKER = -228
const val IME_SHOW_UI = -231
const val IME_HIDE_UI = -232

View File

@@ -121,6 +121,7 @@ class TextKeyData(
CLIPBOARD_CLEAR_HISTORY,
CLIPBOARD_CLEAR_FULL_HISTORY,
CLIPBOARD_CLEAR_PRIMARY_CLIP,
TOGGLE_COMPACT_LAYOUT,
COMPACT_LAYOUT_TO_LEFT,
COMPACT_LAYOUT_TO_RIGHT,
UNDO,
@@ -336,6 +337,12 @@ class TextKeyData(
label = "clipboard_clear_primary_clip",
)
/** Predefined key data for [KeyCode.TOGGLE_COMPACT_LAYOUT] */
val TOGGLE_COMPACT_LAYOUT = TextKeyData(
type = KeyType.SYSTEM_GUI,
code = KeyCode.TOGGLE_COMPACT_LAYOUT,
label = "toggle_compact_layout",
)
/** Predefined key data for [KeyCode.COMPACT_LAYOUT_TO_LEFT] */
val COMPACT_LAYOUT_TO_LEFT = TextKeyData(
type = KeyType.SYSTEM_GUI,
@@ -412,6 +419,12 @@ class TextKeyData(
code = KeyCode.SYSTEM_INPUT_METHOD_PICKER,
label = "system_input_method_picker",
)
/** Predefined key data for [KeyCode.SHOW_SUBTYPE_PICKER] */
val SHOW_SUBTYPE_PICKER = TextKeyData(
type = KeyType.FUNCTION,
code = KeyCode.SHOW_SUBTYPE_PICKER,
label = "subtype_picker",
)
/** Predefined key data for [KeyCode.SYSTEM_PREV_INPUT_METHOD] */
val SYSTEM_PREV_INPUT_METHOD = TextKeyData(
type = KeyType.FUNCTION,

View File

@@ -28,6 +28,7 @@ import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import java.util.*
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
@@ -42,7 +43,7 @@ import kotlin.contracts.contract
*/
@Suppress("MemberVisibilityCanBePrivate")
class TextKeyboardCache(ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {
private val cache: EnumMap<KeyboardMode, SparseArrayCompat<Deferred<TextKeyboard>>> = EnumMap(KeyboardMode::class.java)
private val cache: EnumMap<KeyboardMode, SparseArrayCompat<TextKeyboard>> = EnumMap(KeyboardMode::class.java)
private val scope: CoroutineScope = CoroutineScope(ioDispatcher + SupervisorJob())
init {
@@ -105,7 +106,7 @@ class TextKeyboardCache(ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {
* @return The deferred computed keyboard or null if the cache does not have an entry associated with the given
* params.
*/
fun getAsync(mode: KeyboardMode, subtype: Subtype): Deferred<TextKeyboard>? {
fun getAsync(mode: KeyboardMode, subtype: Subtype): TextKeyboard? {
return cache[mode]!![subtype.hashCode()].also {
flogDebug(LogTopic.TEXT_KEYBOARD_VIEW) { "Get keyboard '$mode ${subtype.toShortString()}'" }
}
@@ -122,7 +123,7 @@ class TextKeyboardCache(ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {
*
* @return The deferred computed keyboard either from the cache or from [block].
*/
fun getOrElseAsync(mode: KeyboardMode, subtype: Subtype, block: suspend () -> TextKeyboard): Deferred<TextKeyboard> {
fun getOrElseAsync(mode: KeyboardMode, subtype: Subtype, block: suspend () -> TextKeyboard): TextKeyboard {
contract {
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
}
@@ -130,7 +131,7 @@ class TextKeyboardCache(ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {
return if (cachedKeyboard != null) {
cachedKeyboard
} else {
val keyboard = scope.async { block() }
val keyboard = runBlocking { block() }
set(mode, subtype, keyboard)
keyboard
}
@@ -143,7 +144,7 @@ class TextKeyboardCache(ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {
* @param subtype The subtype of the computed keyboard to set.
* @param keyboard The deferred computed keyboard to set for the given params.
*/
fun set(mode: KeyboardMode, subtype: Subtype, keyboard: Deferred<TextKeyboard>) {
fun set(mode: KeyboardMode, subtype: Subtype, keyboard: TextKeyboard) {
flogDebug(LogTopic.TEXT_KEYBOARD_VIEW) { "Set keyboard '$mode ${subtype.toShortString()}'" }
cache[mode]!![subtype.hashCode()] = keyboard
}

View File

@@ -17,14 +17,16 @@
package dev.patrickgold.florisboard.ime.text.keyboard
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.view.MotionEvent
import android.view.animation.AccelerateInterpolator
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@@ -55,7 +57,6 @@ import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -78,7 +79,6 @@ import dev.patrickgold.florisboard.ime.text.gestures.SwipeGesture
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyType
import dev.patrickgold.florisboard.ime.text.key.KeyVariation
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.keyboardManager
import dev.patrickgold.florisboard.lib.FlorisRect
@@ -95,13 +95,15 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.onFailure
import kotlinx.coroutines.isActive
import org.florisboard.lib.android.isOrientationLandscape
import org.florisboard.lib.snygg.ui.SnyggSurface
import org.florisboard.lib.snygg.ui.snyggBackground
import org.florisboard.lib.snygg.ui.solidColor
import org.florisboard.lib.snygg.ui.spSize
import org.florisboard.lib.snygg.SnyggSelector
import org.florisboard.lib.snygg.ui.SnyggBox
import org.florisboard.lib.snygg.ui.SnyggIcon
import org.florisboard.lib.snygg.ui.SnyggText
import org.florisboard.lib.snygg.ui.rememberSnyggThemeQuery
import kotlin.math.abs
import kotlin.math.sqrt
@SuppressLint("UnusedBoxWithConstraintsScope")
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun TextKeyboardLayout(
@@ -119,8 +121,8 @@ fun TextKeyboardLayout(
val glideEnabled = glideEnabledInternal && evaluator.editorInfo.isRichInputEditor &&
evaluator.state.keyVariation != KeyVariation.PASSWORD
val glideShowTrail by prefs.glide.showTrail.observeAsState()
val glideTrailColor = FlorisImeTheme.style.get(element = FlorisImeUi.GlideTrail)
.foreground.solidColor(context, default = Color.Green)
val glideTrailStyle = rememberSnyggThemeQuery(FlorisImeUi.GlideTrail.elementName)
val glideTrailColor = glideTrailStyle.foreground(default = Color.Green)
val controller = remember { TextKeyboardLayoutController(context) }.also {
it.keyboard = keyboard
@@ -295,7 +297,7 @@ fun TextKeyboardLayout(
val debugShowTouchBoundaries by prefs.devtools.showKeyTouchBoundaries.observeAsState()
for (textKey in keyboard.keys()) {
TextKeyButton(
textKey, evaluator, fontSizeMultiplier,
textKey, evaluator,
debugShowTouchBoundaries,
)
}
@@ -316,49 +318,29 @@ fun TextKeyboardLayout(
private fun TextKeyButton(
key: TextKey,
evaluator: ComputingEvaluator,
fontSizeMultiplier: Float,
debugShowTouchBoundaries: Boolean,
) = with(LocalDensity.current) {
val context = LocalContext.current
val keyStyle = FlorisImeTheme.style.get(
element = FlorisImeUi.Key,
code = key.computedData.code,
mode = evaluator.state.inputShiftState.value,
isPressed = key.isPressed && key.isEnabled,
isDisabled = !key.isEnabled,
evaluator.keyboard.mode
val attributes = mapOf(
FlorisImeUi.Attr.Code to key.computedData.code,
FlorisImeUi.Attr.Mode to evaluator.keyboard.mode.attrName(),
FlorisImeUi.Attr.ShiftState to evaluator.state.inputShiftState.attrName(),
)
val fontSize = keyStyle.fontSize.spSize() safeTimes fontSizeMultiplier safeTimes when (key.computedData.code) {
KeyCode.VIEW_CHARACTERS,
KeyCode.VIEW_SYMBOLS,
KeyCode.VIEW_SYMBOLS2 -> 0.80f
KeyCode.VIEW_NUMERIC,
KeyCode.VIEW_NUMERIC_ADVANCED -> 0.55f
else -> 1.0f
val selector = when {
!key.isEnabled -> SnyggSelector.DISABLED
key.isPressed -> SnyggSelector.PRESSED
else -> SnyggSelector.NONE
}
val size = key.visibleBounds.size.toDpSize()
Box(
SnyggBox(
FlorisImeUi.Key.elementName,
attributes = attributes,
selector = selector,
modifier = Modifier
.requiredSize(size)
.absoluteOffset { key.visibleBounds.topLeft.toIntOffset() },
) {
// TODO: maybe make this customizable through a size property for keyStyle
val isReducedHeight = key.computedData.let { it.code == KeyCode.ENTER || it.code == KeyCode.SPACE }
SnyggSurface(
modifier = Modifier
.fillMaxWidth()
.run {
if (isReducedHeight && FlorisImeTheme.config.isBorderless) {
this.padding(vertical = size.height * 0.15f)
} else {
this
}
}
.fillMaxHeight(),
style = keyStyle,
clip = false,
) { }
val isTelpadKey = key.computedData.type == KeyType.NUMERIC && evaluator.keyboard.mode == KeyboardMode.PHONE
val isTelPadKey = key.computedData.type == KeyType.NUMERIC && evaluator.keyboard.mode == KeyboardMode.PHONE
key.label?.let { label ->
var customLabel = label
if (key.computedData.code == KeyCode.SPACE) {
@@ -370,52 +352,29 @@ private fun TextKeyButton(
SpaceBarMode.SPACE_BAR_KEY -> customLabel = ""
}
}
Text(
SnyggText(
modifier = Modifier
.wrapContentSize()
.align(if (isTelpadKey) BiasAlignment(-0.5f, 0f) else Alignment.Center),
.align(if (isTelPadKey) BiasAlignment(-0.5f, 0f) else Alignment.Center),
text = customLabel,
color = keyStyle.foreground.solidColor(context),
fontSize = fontSize,
maxLines = if (key.computedData.code == KeyCode.VIEW_NUMERIC_ADVANCED) 2 else 1,
softWrap = key.computedData.code == KeyCode.VIEW_NUMERIC_ADVANCED,
overflow = when (key.computedData.code) {
KeyCode.SPACE -> TextOverflow.Ellipsis
else -> TextOverflow.Visible
},
)
}
key.hintedLabel?.let { hintedLabel ->
val keyHintStyle = FlorisImeTheme.style.get(
element = FlorisImeUi.KeyHint,
code = key.computedHintData.code,
mode = evaluator.state.inputShiftState.value,
isPressed = key.isPressed,
)
val hintFontSize = keyHintStyle.fontSize.spSize() safeTimes fontSizeMultiplier
Text(
SnyggText(
elementName = FlorisImeUi.KeyHint.elementName,
attributes = attributes,
selector = selector,
modifier = Modifier
.wrapContentSize()
.align(if (isTelpadKey) BiasAlignment(0.5f, 0f) else Alignment.TopEnd)
.snyggBackground(context, keyHintStyle)
.padding(horizontal = (key.visibleBounds.width / 12f).toDp()),
.align(if (isTelPadKey) BiasAlignment(0.5f, 0f) else Alignment.TopEnd),
text = hintedLabel,
color = keyHintStyle.foreground.solidColor(context),
fontFamily = FontFamily.Monospace,
fontSize = hintFontSize,
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Visible,
)
}
key.foregroundImageVector?.let { imageVector ->
Icon(
modifier = Modifier
.requiredSize(fontSize.toDp() * 1.1f)
.align(Alignment.Center),
SnyggIcon(
modifier = Modifier.align(Alignment.Center),
imageVector = imageVector,
contentDescription = null,
tint = keyStyle.foreground.solidColor(context),
)
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (C) 2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.theme
import android.content.Context
import dev.patrickgold.florisboard.lib.devtools.flogError
import org.florisboard.lib.kotlin.io.subFile
import org.florisboard.lib.snygg.value.SnyggAssetResolver
import java.net.URI
class FlorisAssetResolver(val context: Context, val themeInfo: ThemeManager.ThemeInfo) : SnyggAssetResolver {
override fun resolveAbsolutePath(uri: String) = runCatching {
val uri = URI.create(uri)
require(uri.scheme == "flex")
require(uri.authority.isNullOrEmpty())
val baseDir = checkNotNull(themeInfo.loadedDir) { "Loaded directory was null" }
val basePath = baseDir.canonicalPath
val canonicalFile = baseDir.subFile(uri.path).canonicalFile
val canonicalPath = canonicalFile.path
check(canonicalPath.startsWith(basePath)) {
"Calculated path '$canonicalPath' does not start with base path '$basePath'"
}
check(canonicalFile.exists()) {
"Calculated path '$canonicalPath' does not exist"
}
check(canonicalFile.isFile()) {
"Calculated path '$canonicalPath' is not a file"
}
canonicalPath
}.onFailure { exception ->
flogError { "FlorisAssetResolver failed to resolve URI '$uri'\n error: ${exception.message}\n with: $themeInfo" }
}
}

View File

@@ -18,93 +18,28 @@ package dev.patrickgold.florisboard.ime.theme
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import dev.patrickgold.florisboard.ime.input.InputShiftState
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.lib.observeAsNonNullState
import dev.patrickgold.florisboard.themeManager
import org.florisboard.lib.android.AndroidVersion
import org.florisboard.lib.snygg.Snygg
import org.florisboard.lib.snygg.SnyggStylesheet
import org.florisboard.lib.snygg.ui.ProvideSnyggUiDefaults
import org.florisboard.lib.snygg.ui.SnyggUiDefaults
import dev.patrickgold.jetpref.datastore.model.observeAsState
import org.florisboard.lib.snygg.ui.ProvideSnyggTheme
import org.florisboard.lib.snygg.ui.rememberSnyggTheme
private val LocalConfig = staticCompositionLocalOf<ThemeExtensionComponent> { error("not init") }
private val LocalStyle = staticCompositionLocalOf<SnyggStylesheet> { error("not init") }
private val MaterialDarkFallbackPalette = darkColorScheme()
private val MaterialLightFallbackPalette = lightColorScheme()
object FlorisImeTheme {
val config: ThemeExtensionComponent
@Composable
@ReadOnlyComposable
get() = LocalConfig.current
val style: SnyggStylesheet
@Composable
@ReadOnlyComposable
get() = LocalStyle.current
@Composable
fun fallbackSurfaceColor(): Color {
return if (config.isNightTheme) Color.Black else Color.White
}
@Composable
fun fallbackContentColor(): Color {
return if (config.isNightTheme) Color.White else Color.Black
}
fun init() {
Snygg.init(
stylesheetSpec = FlorisImeUiSpec,
rulePreferredElementSorting = listOf(
FlorisImeUi.Keyboard,
FlorisImeUi.Key,
FlorisImeUi.KeyHint,
FlorisImeUi.KeyPopup,
FlorisImeUi.Smartbar,
FlorisImeUi.SmartbarSharedActionsRow,
FlorisImeUi.SmartbarSharedActionsToggle,
FlorisImeUi.SmartbarExtendedActionsRow,
FlorisImeUi.SmartbarExtendedActionsToggle,
FlorisImeUi.SmartbarActionKey,
FlorisImeUi.SmartbarActionTile,
FlorisImeUi.SmartbarActionsOverflow,
FlorisImeUi.SmartbarActionsOverflowCustomizeButton,
FlorisImeUi.SmartbarActionsEditor,
FlorisImeUi.SmartbarActionsEditorHeader,
FlorisImeUi.SmartbarActionsEditorSubheader,
FlorisImeUi.SmartbarCandidatesRow,
FlorisImeUi.SmartbarCandidateWord,
FlorisImeUi.SmartbarCandidateClip,
FlorisImeUi.SmartbarCandidateSpacer,
),
rulePlaceholders = mapOf(
"c:delete" to KeyCode.DELETE,
"c:enter" to KeyCode.ENTER,
"c:shift" to KeyCode.SHIFT,
"c:space" to KeyCode.SPACE,
"sh:unshifted" to InputShiftState.UNSHIFTED.value,
"sh:shifted_manual" to InputShiftState.SHIFTED_MANUAL.value,
"sh:shifted_automatic" to InputShiftState.SHIFTED_AUTOMATIC.value,
"sh:caps_lock" to InputShiftState.CAPS_LOCK.value,
),
)
}
}
@Composable
@@ -112,34 +47,29 @@ fun FlorisImeTheme(content: @Composable () -> Unit) {
val context = LocalContext.current
val themeManager by context.themeManager()
val prefs by florisPreferenceModel()
val accentColor by prefs.theme.accentColor.observeAsState()
val activeThemeInfo by themeManager.activeThemeInfo.observeAsNonNullState()
val activeConfig = remember(activeThemeInfo) { activeThemeInfo.config }
val activeStyle = remember(activeThemeInfo) { activeThemeInfo.stylesheet }
val materialColors = if (activeConfig.isNightTheme) {
if (AndroidVersion.ATLEAST_API31_S) {
dynamicDarkColorScheme(context)
} else {
MaterialDarkFallbackPalette
}
} else {
if (AndroidVersion.ATLEAST_API31_S) {
dynamicLightColorScheme(context)
} else {
MaterialLightFallbackPalette
}
val assetResolver = remember(activeThemeInfo) {
FlorisAssetResolver(context, activeThemeInfo)
}
MaterialTheme(materialColors) {
val snyggTheme = rememberSnyggTheme(activeStyle, assetResolver)
MaterialTheme {
CompositionLocalProvider(
LocalConfig provides activeConfig,
LocalStyle provides activeStyle,
LocalTextStyle provides TextStyle.Default,
) {
val fallbackContentColor = FlorisImeTheme.fallbackContentColor()
val fallbackSurfaceColor = FlorisImeTheme.fallbackSurfaceColor()
val snyggUiDefaults = remember(fallbackContentColor, fallbackSurfaceColor) {
SnyggUiDefaults(fallbackContentColor, fallbackSurfaceColor)
}
ProvideSnyggUiDefaults(snyggUiDefaults, content)
ProvideSnyggTheme(
snyggTheme = snyggTheme,
dynamicAccentColor = accentColor,
assetResolver = assetResolver,
content = content,
)
}
}
}

View File

@@ -16,182 +16,296 @@
package dev.patrickgold.florisboard.ime.theme
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import dev.patrickgold.florisboard.ime.input.InputShiftState
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import org.florisboard.lib.snygg.SnyggSelector
import org.florisboard.lib.snygg.SnyggStylesheet
val FlorisImeThemeBaseStyle = SnyggStylesheet {
val FlorisImeThemeBaseStyle = SnyggStylesheet.v2 {
defines {
"primary" to rgbaColor(76, 175, 80)
"primaryVariant" to rgbaColor(56, 142, 60)
"secondary" to rgbaColor(245, 124, 0)
"secondaryVariant" to rgbaColor(230, 81, 0)
"background" to rgbaColor(33, 33, 33)
"surface" to rgbaColor(66, 66, 66)
"surfaceVariant" to rgbaColor(97, 97, 97)
"--primary" to rgbaColor(76, 175, 80)
"--primary-variant" to rgbaColor(56, 142, 60)
"--secondary" to rgbaColor(245, 124, 0)
"--secondary-variant" to rgbaColor(230, 81, 0)
"--background" to rgbaColor(33, 33, 33)
"--surface" to rgbaColor(66, 66, 66)
"--surface-variant" to rgbaColor(97, 97, 97)
"onBackground" to rgbaColor(255, 255, 255)
"onSurface" to rgbaColor(255, 255, 255)
"--on-primary" to rgbaColor(240, 240, 240)
"--on-background" to rgbaColor(255, 255, 255)
"--on-background-disabled" to rgbaColor(80, 80, 80)
"--on-surface" to rgbaColor(255, 255, 255)
"--shape" to roundedCornerShape(8.dp)
"--shape-variant" to roundedCornerShape(12.dp)
}
FlorisImeUi.Keyboard {
background = `var`("background")
FlorisImeUi.Window.elementName {
background = `var`("--background")
foreground = `var`("--on-background")
}
FlorisImeUi.Key {
background = `var`("surface")
foreground = `var`("onSurface")
fontSize = size(22.sp)
FlorisImeUi.Key.elementName {
background = `var`("--surface")
foreground = `var`("--on-surface")
fontSize = fontSize(22.sp)
shadowElevation = size(2.dp)
shape = roundedCornerShape(20)
shape = `var`("--shape")
textMaxLines = textMaxLines(1)
}
FlorisImeUi.Key(pressedSelector = true) {
background = `var`("surfaceVariant")
foreground = `var`("onSurface")
FlorisImeUi.Key.elementName(selector = SnyggSelector.PRESSED) {
background = `var`("--surface-variant")
foreground = `var`("--on-surface")
}
FlorisImeUi.Key(codes = listOf(KeyCode.ENTER)) {
background = `var`("primary")
foreground = `var`("onSurface")
FlorisImeUi.Key.elementName(FlorisImeUi.Attr.Code to listOf(KeyCode.ENTER)) {
background = `var`("--primary")
foreground = `var`("--on-surface")
}
FlorisImeUi.Key(codes = listOf(KeyCode.ENTER), pressedSelector = true) {
background = `var`("primaryVariant")
foreground = `var`("onSurface")
FlorisImeUi.Key.elementName(FlorisImeUi.Attr.Code to listOf(KeyCode.ENTER), selector = SnyggSelector.PRESSED) {
background = `var`("--primary-variant")
foreground = `var`("--on-surface")
}
FlorisImeUi.Key(
codes = listOf(KeyCode.SHIFT),
modes = listOf(InputShiftState.CAPS_LOCK.value),
FlorisImeUi.Key.elementName(FlorisImeUi.Attr.Code to listOf(KeyCode.SPACE)) {
background = `var`("--surface")
foreground = `var`("--on-surface")
fontSize = fontSize(12.sp)
textOverflow = textOverflow(TextOverflow.Ellipsis)
}
FlorisImeUi.Key.elementName(FlorisImeUi.Attr.Code to listOf(
KeyCode.VIEW_CHARACTERS,
KeyCode.VIEW_SYMBOLS,
KeyCode.VIEW_SYMBOLS2,
)) {
fontSize = fontSize(18.sp)
}
FlorisImeUi.Key.elementName(FlorisImeUi.Attr.Code to listOf(
KeyCode.VIEW_NUMERIC,
KeyCode.VIEW_NUMERIC_ADVANCED,
)) {
fontSize = fontSize(12.sp)
}
FlorisImeUi.Key.elementName(FlorisImeUi.Attr.Code to listOf(KeyCode.VIEW_NUMERIC_ADVANCED)) {
textMaxLines = textMaxLines(2)
}
FlorisImeUi.Key.elementName(
FlorisImeUi.Attr.Code to listOf(KeyCode.SHIFT),
FlorisImeUi.Attr.ShiftState to listOf(InputShiftState.CAPS_LOCK.attrName()),
) {
foreground = rgbaColor(255, 152, 0)
}
FlorisImeUi.Key(codes = listOf(KeyCode.SPACE)) {
background = `var`("surface")
foreground = rgbaColor(144, 144, 144)
fontSize = size(12.sp)
}
FlorisImeUi.KeyHint {
FlorisImeUi.KeyHint.elementName {
background = rgbaColor(0, 0, 0, 0f)
foreground = rgbaColor(184, 184, 184)
fontSize = size(12.sp)
foreground = `var`("--on-surface-variant")
fontFamily = genericFontFamily(FontFamily.Monospace)
fontSize = fontSize(12.sp)
padding = padding(0.dp, 1.dp, 1.dp, 0.dp)
textMaxLines = textMaxLines(1)
}
FlorisImeUi.KeyPopup {
FlorisImeUi.KeyPopupBox.elementName {
background = rgbaColor(117, 117, 117)
foreground = `var`("onSurface")
fontSize = size(22.sp)
shape = roundedCornerShape(20)
foreground = `var`("--on-surface")
fontSize = fontSize(22.sp)
shape = `var`("--shape")
shadowElevation = size(2.dp)
}
FlorisImeUi.KeyPopup(focusSelector = true) {
FlorisImeUi.KeyPopupElement.elementName(selector = SnyggSelector.FOCUS) {
background = rgbaColor(189, 189, 189)
foreground = `var`("onSurface")
fontSize = size(22.sp)
shape = roundedCornerShape(20)
shape = `var`("--shape")
}
FlorisImeUi.KeyPopupExtendedIndicator.elementName {
fontSize = fontSize(16.sp)
}
FlorisImeUi.ClipboardHeader {
background = rgbaColor(0, 0, 0, 0f)
foreground = `var`("onSurface")
fontSize = size(16.sp)
FlorisImeUi.Smartbar.elementName {
fontSize = fontSize(18.sp)
}
FlorisImeUi.ClipboardItem {
background = `var`("surface")
foreground = `var`("onSurface")
fontSize = size(14.sp)
shape = roundedCornerShape(12.dp)
}
FlorisImeUi.ClipboardItemPopup {
background = rgbaColor(117, 117, 117)
foreground = `var`("onSurface")
fontSize = size(14.sp)
shape = roundedCornerShape(12.dp)
}
FlorisImeUi.ClipboardEnableHistoryButton {
background = `var`("primary")
foreground = rgbaColor(0, 0, 0)
shape = roundedCornerShape(12.dp)
}
FlorisImeUi.EmojiKey {
background = rgbaColor(0, 0, 0, 0f)
foreground = `var`("onBackground")
fontSize = size(22.sp)
shape = roundedCornerShape(20)
}
FlorisImeUi.EmojiKey(pressedSelector = true) {
background = `var`("surface")
foreground = `var`("onSurface")
}
FlorisImeUi.GlideTrail {
foreground = `var`("primary")
}
FlorisImeUi.IncognitoModeIndicator {
foreground = rgbaColor(255, 255, 255, 0.067f)
}
FlorisImeUi.OneHandedPanel {
background = rgbaColor(27, 94, 32)
foreground = rgbaColor(238, 238, 238)
}
FlorisImeUi.Smartbar {
FlorisImeUi.SmartbarSharedActionsRow.elementName {
background = rgbaColor(0, 0, 0, 0f)
}
FlorisImeUi.SmartbarSharedActionsRow {
background = rgbaColor(0, 0, 0, 0f)
}
FlorisImeUi.SmartbarSharedActionsToggle {
background = `var`("surface")
foreground = `var`("onSurface")
FlorisImeUi.SmartbarSharedActionsToggle.elementName {
background = `var`("--surface")
foreground = `var`("--on-surface")
shape = circleShape()
}
FlorisImeUi.SmartbarExtendedActionsRow {
FlorisImeUi.SmartbarExtendedActionsRow.elementName {
background = rgbaColor(0, 0, 0, 0f)
}
FlorisImeUi.SmartbarExtendedActionsToggle {
FlorisImeUi.SmartbarExtendedActionsToggle.elementName {
background = rgbaColor(0, 0, 0, 0f)
foreground = rgbaColor(144, 144, 144)
shape = circleShape()
}
FlorisImeUi.SmartbarActionKey {
FlorisImeUi.SmartbarActionKey.elementName {
background = rgbaColor(0, 0, 0, 0f)
foreground = rgbaColor(220, 220, 220)
fontSize = size(18.sp)
shape = `var`("--shape")
clip = yes()
}
FlorisImeUi.SmartbarActionKey.elementName(selector = SnyggSelector.DISABLED) {
foreground = `var`("--on-background-disabled")
}
FlorisImeUi.SmartbarActionTile.elementName {
background = rgbaColor(0, 0, 0, 0f)
foreground = `var`("--on-background")
margin = padding(4.dp)
shape = roundedCornerShape(20)
clip = yes()
textMaxLines = textMaxLines(2)
textOverflow = textOverflow(TextOverflow.Ellipsis)
}
FlorisImeUi.SmartbarActionKey(pressedSelector = true) {
background = `var`("surface")
foreground = rgbaColor(220, 220, 220)
FlorisImeUi.SmartbarActionTile.elementName(selector = SnyggSelector.DISABLED) {
foreground = `var`("--on-background-disabled")
}
FlorisImeUi.SmartbarActionKey(disabledSelector = true) {
FlorisImeUi.SmartbarActionsOverflowCustomizeButton.elementName {
background = `var`("--primary")
foreground = `var`("--on-primary")
shape = roundedCornerShape(24.dp)
}
FlorisImeUi.SmartbarActionsEditor.elementName {
background = `var`("--background")
foreground = `var`("--on-background")
shape = roundedCornerShape(24.dp, 24.dp, 0.dp, 0.dp)
clip = yes()
}
FlorisImeUi.SmartbarActionsEditorHeader.elementName {
background = `var`("--surface")
foreground = `var`("--on-surface")
fontSize = fontSize(16.sp)
textMaxLines = textMaxLines(1)
textOverflow = textOverflow(TextOverflow.Ellipsis)
}
FlorisImeUi.SmartbarActionsEditorSubheader.elementName {
foreground = `var`("--secondary")
fontSize = fontSize(16.sp)
fontWeight = fontWeight(FontWeight.Bold)
padding = padding(12.dp, 16.dp, 12.dp, 8.dp)
textMaxLines = textMaxLines(1)
textOverflow = textOverflow(TextOverflow.Ellipsis)
}
FlorisImeUi.SmartbarActionsEditorTileGrid.elementName {
margin = padding(4.dp, 0.dp)
}
FlorisImeUi.SmartbarActionsEditorTile.elementName {
margin = padding(4.dp)
padding = padding(8.dp)
textAlign = textAlign(TextAlign.Center)
textMaxLines = textMaxLines(2)
textOverflow = textOverflow(TextOverflow.Ellipsis)
}
FlorisImeUi.SmartbarActionsEditorTile.elementName(FlorisImeUi.Attr.Code to listOf(KeyCode.NOOP)) {
foreground = `var`("--on-background-disabled")
}
FlorisImeUi.SmartbarActionsEditorTile.elementName(FlorisImeUi.Attr.Code to listOf(KeyCode.DRAG_MARKER)) {
foreground = rgbaColor(255, 0, 0)
}
FlorisImeUi.SmartbarCandidateWord.elementName {
background = rgbaColor(0, 0, 0, 0f)
foreground = `var`("surface")
}
FlorisImeUi.SmartbarCandidateWord {
background = rgbaColor(0, 0, 0, 0f)
foreground = rgbaColor(220, 220, 220)
fontSize = size(14.sp)
foreground = `var`("--on-background")
fontSize = fontSize(14.sp)
margin = padding(4.dp)
padding = padding(8.dp, 0.dp)
shape = rectangleShape()
textMaxLines = textMaxLines(1)
textOverflow = textOverflow(TextOverflow.Ellipsis)
}
FlorisImeUi.SmartbarCandidateWord(pressedSelector = true) {
background = `var`("surface")
foreground = rgbaColor(220, 220, 220)
FlorisImeUi.SmartbarCandidateWord.elementName(selector = SnyggSelector.PRESSED) {
background = `var`("--surface")
foreground = `var`("--on-surface")
}
FlorisImeUi.SmartbarCandidateClip {
FlorisImeUi.SmartbarCandidateClip.elementName {
background = rgbaColor(0, 0, 0, 0f)
foreground = rgbaColor(220, 220, 220)
fontSize = size(14.sp)
fontSize = fontSize(14.sp)
margin = padding(4.dp)
padding = padding(8.dp, 0.dp)
shape = roundedCornerShape(8)
textMaxLines = textMaxLines(1)
textOverflow = textOverflow(TextOverflow.Ellipsis)
}
FlorisImeUi.SmartbarCandidateClip(pressedSelector = true) {
background = `var`("surface")
foreground = rgbaColor(220, 220, 220)
FlorisImeUi.SmartbarCandidateClip.elementName(selector = SnyggSelector.PRESSED) {
background = `var`("--surface")
foreground = `var`("--on-surface")
}
FlorisImeUi.SmartbarCandidateSpacer {
FlorisImeUi.SmartbarCandidateSpacer.elementName {
foreground = rgbaColor(255, 255, 255, 0.25f)
}
FlorisImeUi.SystemNavBar {
background = `var`("background")
FlorisImeUi.ClipboardHeader.elementName {
foreground = `var`("--on-background")
fontSize = fontSize(16.sp)
}
FlorisImeUi.ClipboardSubheader.elementName {
fontSize = fontSize(14.sp)
margin = padding(6.dp)
}
FlorisImeUi.ClipboardContent.elementName {
padding = padding(10.dp)
}
FlorisImeUi.ClipboardItem.elementName {
background = `var`("--surface")
foreground = `var`("--on-surface")
fontSize = fontSize(14.sp)
margin = padding(4.dp)
padding = padding(12.dp, 8.dp)
shape = `var`("--shape-variant")
clip = yes()
shadowElevation = size(2.dp)
}
FlorisImeUi.ClipboardItemPopup.elementName {
background = `var`("--surface")
foreground = `var`("--on-surface")
margin = padding(4.dp)
shape = `var`("--shape-variant")
clip = yes()
shadowElevation = size(2.dp)
}
FlorisImeUi.ClipboardItemPopupAction.elementName {
fontSize = fontSize(16.sp)
padding = padding(12.dp)
}
FlorisImeUi.ClipboardItemPopupActionText.elementName {
margin = padding(8.dp, 0.dp, 0.dp, 0.dp)
}
FlorisImeUi.ClipboardHistoryDisabledButton.elementName {
background = `var`("--primary")
foreground = `var`("--on-primary")
shape = roundedCornerShape(24.dp)
}
FlorisImeUi.MediaEmojiKey.elementName {
background = rgbaColor(0, 0, 0, 0f)
foreground = `var`("--on-background")
fontSize = fontSize(22.sp)
shape = `var`("--shape")
}
FlorisImeUi.MediaEmojiKey.elementName(selector = SnyggSelector.PRESSED) {
background = `var`("--surface")
foreground = `var`("--on-surface")
}
FlorisImeUi.GlideTrail.elementName {
foreground = `var`("--primary")
}
FlorisImeUi.InlineAutofillChip.elementName {
background = `var`("--surface")
foreground = `var`("--on-surface")
}
FlorisImeUi.IncognitoModeIndicator.elementName {
foreground = rgbaColor(255, 255, 255, 0.067f)
}
FlorisImeUi.OneHandedPanel.elementName {
background = rgbaColor(27, 94, 32)
foreground = rgbaColor(238, 238, 238)
}
}

View File

@@ -16,47 +16,307 @@
package dev.patrickgold.florisboard.ime.theme
object FlorisImeUi {
const val Keyboard = "keyboard"
const val Key = "key"
const val KeyHint = "key-hint"
const val KeyPopup = "key-popup"
import dev.patrickgold.florisboard.R
const val ClipboardHeader = "clipboard-header"
const val ClipboardItem = "clipboard-item"
const val ClipboardItemPopup = "clipboard-item-popup"
const val ClipboardEnableHistoryButton = "clipboard-enable-history-button"
enum class FlorisImeUi(val elementName: String, val resId: Int?) {
Root(
elementName = "root",
resId = R.string.snygg__rule_element__root,
),
Window(
elementName = "window",
resId = R.string.snygg__rule_element__window,
),
const val EmojiKey = "emoji-key"
const val EmojiKeyPopup = "emoji-key-popup"
const val EmojiTab = "emoji-tab"
Key(
elementName = "key",
resId = R.string.snygg__rule_element__key,
),
KeyHint(
elementName = "key-hint",
resId = R.string.snygg__rule_element__key_hint,
),
KeyPopupBox(
elementName = "key-popup-box",
resId = R.string.snygg__rule_element__key_popup_box,
),
KeyPopupElement(
elementName = "key-popup-element",
resId = R.string.snygg__rule_element__key_popup_element,
),
KeyPopupExtendedIndicator(
elementName = "key-popup-extended-indicator",
resId = R.string.snygg__rule_element__key_popup_extended_indicator,
),
const val ExtractedLandscapeInputLayout = "extracted-landscape-input-layout"
const val ExtractedLandscapeInputField = "extracted-landscape-input-field"
const val ExtractedLandscapeInputAction = "extracted-landscape-input-action"
ClipboardHeader(
elementName = "clipboard-header",
resId = R.string.snygg__rule_element__clipboard_header,
),
ClipboardHeaderButton(
elementName = "clipboard-header-button",
resId = R.string.snygg__rule_element__clipboard_header_button,
),
ClipboardHeaderText(
elementName = "clipboard-header-text",
resId = R.string.snygg__rule_element__clipboard_header_text,
),
ClipboardSubheader(
elementName = "clipboard-subheader",
resId = R.string.snygg__rule_element__clipboard_subheader,
),
ClipboardContent(
elementName = "clipboard-content",
resId = R.string.snygg__rule_element__clipboard_content,
),
ClipboardItem(
elementName = "clipboard-item",
resId = R.string.snygg__rule_element__clipboard_item,
),
ClipboardItemPopup(
elementName = "clipboard-item-popup",
resId = R.string.snygg__rule_element__clipboard_item_popup,
),
ClipboardItemPopupAction(
elementName = "clipboard-item-popup-action",
resId = R.string.snygg__rule_element__clipboard_item_popup_action,
),
ClipboardItemPopupActionIcon(
elementName = "clipboard-item-popup-action-icon",
resId = R.string.snygg__rule_element__clipboard_item_popup_action_icon,
),
ClipboardItemPopupActionText(
elementName = "clipboard-item-popup-action-text",
resId = R.string.snygg__rule_element__clipboard_item_popup_action_text,
),
ClipboardClearAllDialog(
elementName = "clipboard-clear-all-dialog",
resId = R.string.snygg__rule_element__clipboard_clear_all_dialog,
),
ClipboardClearAllDialogMessage(
elementName = "clipboard-clear-all-dialog-message",
resId = R.string.snygg__rule_element__clipboard_clear_all_dialog_message,
),
ClipboardClearAllDialogButtons(
elementName = "clipboard-clear-all-dialog-buttons",
resId = R.string.snygg__rule_element__clipboard_clear_all_dialog_buttons,
),
ClipboardClearAllDialogButton(
elementName = "clipboard-clear-all-dialog-button",
resId = R.string.snygg__rule_element__clipboard_clear_all_dialog_button,
),
ClipboardHistoryDisabledTitle(
elementName = "clipboard-history-disabled-title",
resId = R.string.snygg__rule_element__clipboard_history_disabled_title,
),
ClipboardHistoryDisabledMessage(
elementName = "clipboard-history-disabled-message",
resId = R.string.snygg__rule_element__clipboard_history_disabled_message,
),
ClipboardHistoryDisabledButton(
elementName = "clipboard-history-disabled-button",
resId = R.string.snygg__rule_element__clipboard_history_disabled_button,
),
ClipboardHistoryLockedTitle(
elementName = "clipboard-history-locked-title",
resId = R.string.snygg__rule_element__clipboard_history_locked_title,
),
ClipboardHistoryLockedMessage(
elementName = "clipboard-history-locked-message",
resId = R.string.snygg__rule_element__clipboard_history_locked_message,
),
const val GlideTrail = "glide-trail"
ExtractedLandscapeInputLayout(
elementName = "extracted-landscape-input-layout",
resId = R.string.snygg__rule_element__extracted_landscape_input_layout,
),
ExtractedLandscapeInputField(
elementName = "extracted-landscape-input-field",
resId = R.string.snygg__rule_element__extracted_landscape_input_field,
),
ExtractedLandscapeInputAction(
elementName = "extracted-landscape-input-action",
resId = R.string.snygg__rule_element__extracted_landscape_input_action,
),
const val IncognitoModeIndicator = "incognito-mode-indicator"
GlideTrail(
elementName = "glide-trail",
resId = R.string.snygg__rule_element__glide_trail,
),
const val OneHandedPanel = "one-handed-panel"
IncognitoModeIndicator(
elementName = "incognito-mode-indicator",
resId = R.string.snygg__rule_element__incognito_mode_indicator,
),
const val Smartbar = "smartbar"
const val SmartbarSharedActionsRow = "smartbar-shared-actions-row"
const val SmartbarSharedActionsToggle = "smartbar-shared-actions-toggle"
const val SmartbarExtendedActionsRow = "smartbar-extended-actions-row"
const val SmartbarExtendedActionsToggle = "smartbar-extended-actions-toggle"
const val SmartbarActionKey = "smartbar-action-key"
const val SmartbarActionTile = "smartbar-action-tile"
const val SmartbarActionsOverflow = "smartbar-actions-overflow"
const val SmartbarActionsOverflowCustomizeButton = "smartbar-actions-overflow-customize-button"
const val SmartbarActionsEditor = "smartbar-actions-editor"
const val SmartbarActionsEditorHeader = "smartbar-actions-editor-header"
const val SmartbarActionsEditorSubheader = "smartbar-actions-editor-subheader"
const val SmartbarCandidatesRow = "smartbar-candidates-row"
const val SmartbarCandidateWord = "smartbar-candidate-word"
const val SmartbarCandidateClip = "smartbar-candidate-clip"
const val SmartbarCandidateSpacer = "smartbar-candidate-spacer"
InlineAutofillChip(
elementName = "inline-autofill-chip",
resId = R.string.snygg__rule_element__inline_autofill_chip,
),
const val SystemNavBar = "system-nav-bar"
Media(
elementName = "media",
resId = R.string.snygg__rule_element__media,
),
MediaEmojiSubheader(
elementName = "media-emoji-subheader",
resId = R.string.snygg__rule_element__media_emoji_subheader,
),
MediaEmojiKey(
elementName = "media-emoji-key",
resId = R.string.snygg__rule_element__media_emoji_key,
),
MediaEmojiKeyPopupBox(
elementName = "media-emoji-key-popup-box",
resId = R.string.snygg__rule_element__media_emoji_key_popup_box,
),
MediaEmojiKeyPopupElement(
elementName = "media-emoji-key-popup-element",
resId = R.string.snygg__rule_element__media_emoji_key_popup_element,
),
MediaEmojiKeyPopupExtendedIndicator(
elementName = "media-emoji-key-popup-extended-indicator",
resId = R.string.snygg__rule_element__media_emoji_key_popup_extended_indicator,
),
MediaEmojiTab(
elementName = "media-emoji-tab",
resId = R.string.snygg__rule_element__media_emoji_tab,
),
MediaBottomRow(
elementName = "media-bottom-row",
resId = R.string.snygg__rule_element__media_bottom_row,
),
MediaBottomRowButton(
elementName = "media-bottom-row-button",
resId = R.string.snygg__rule_element__media_bottom_row_button,
),
OneHandedPanel(
elementName = "one-handed-panel",
resId = R.string.snygg__rule_element__one_handed_panel,
),
OneHandedPanelButton(
elementName = "one-handed-panel-button",
resId = R.string.snygg__rule_element__one_handed_panel_button,
),
Smartbar(
elementName = "smartbar",
resId = R.string.snygg__rule_element__smartbar,
),
SmartbarSharedActionsRow(
elementName = "smartbar-shared-actions-row",
resId = R.string.snygg__rule_element__smartbar_shared_actions_row,
),
SmartbarSharedActionsToggle(
elementName = "smartbar-shared-actions-toggle",
resId = R.string.snygg__rule_element__smartbar_shared_actions_toggle,
),
SmartbarExtendedActionsRow(
elementName = "smartbar-extended-actions-row",
resId = R.string.snygg__rule_element__smartbar_extended_actions_row,
),
SmartbarExtendedActionsToggle(
elementName = "smartbar-extended-actions-toggle",
resId = R.string.snygg__rule_element__smartbar_extended_actions_toggle,
),
SmartbarActionKey(
elementName = "smartbar-action-key",
resId = R.string.snygg__rule_element__smartbar_action_key,
),
SmartbarActionTile(
elementName = "smartbar-action-tile",
resId = R.string.snygg__rule_element__smartbar_action_tile,
),
SmartbarActionsOverflow(
elementName = "smartbar-actions-overflow",
resId = R.string.snygg__rule_element__smartbar_actions_overflow,
),
SmartbarActionsOverflowCustomizeButton(
elementName = "smartbar-actions-overflow-customize-button",
resId = R.string.snygg__rule_element__smartbar_actions_overflow_customize_button,
),
SmartbarActionsEditor(
elementName = "smartbar-actions-editor",
resId = R.string.snygg__rule_element__smartbar_actions_editor,
),
SmartbarActionsEditorHeader(
elementName = "smartbar-actions-editor-header",
resId = R.string.snygg__rule_element__smartbar_actions_editor_header,
),
SmartbarActionsEditorHeaderButton(
elementName = "smartbar-actions-editor-header-button",
resId = R.string.snygg__rule_element__smartbar_actions_editor_header_button,
),
SmartbarActionsEditorSubheader(
elementName = "smartbar-actions-editor-subheader",
resId = R.string.snygg__rule_element__smartbar_actions_editor_subheader,
),
SmartbarActionsEditorTileGrid(
elementName = "smartbar-actions-editor-tile-grid",
resId = R.string.snygg__rule_element__smartbar_actions_editor_tile_grid,
),
SmartbarActionsEditorTile(
elementName = "smartbar-actions-editor-tile",
resId = R.string.snygg__rule_element__smartbar_actions_editor_tile,
),
SmartbarCandidatesRow(
elementName = "smartbar-candidates-row",
resId = R.string.snygg__rule_element__smartbar_candidates_row,
),
SmartbarCandidateWord(
elementName = "smartbar-candidate-word",
resId = R.string.snygg__rule_element__smartbar_candidate_word,
),
SmartbarCandidateClip(
elementName = "smartbar-candidate-clip",
resId = R.string.snygg__rule_element__smartbar_candidate_clip,
),
SmartbarCandidateSpacer(
elementName = "smartbar-candidate-spacer",
resId = R.string.snygg__rule_element__smartbar_candidate_spacer,
),
SubtypePanel(
elementName = "subtype-panel",
resId = R.string.snygg__rule_element__subtype_panel,
),
SubtypePanelHeader(
elementName = "subtype-panel-header",
resId = R.string.snygg__rule_element__subtype_panel_header,
);
companion object {
val elementNames by lazy { entries.map { it.elementName } }
val elementNamesToOrdinals by lazy {
val enumEntries = entries
buildMap {
enumEntries.forEach { entry ->
put(entry.elementName, entry.ordinal)
}
}
}
val elementNamesToTranslation by lazy {
val enumEntries = entries
buildMap {
put("defines", R.string.snygg__rule_annotation__defines)
put("font", R.string.snygg__rule_annotation__font)
enumEntries.forEach { entry ->
put(entry.elementName, entry.resId)
}
}
}
}
object Attr {
const val Code = "code"
const val Mode = "mode"
const val ShiftState = "shiftstate"
}
}

View File

@@ -16,41 +16,42 @@
package dev.patrickgold.florisboard.ime.theme
/*
import org.florisboard.lib.snygg.Snygg
import org.florisboard.lib.snygg.SnyggLevel
import org.florisboard.lib.snygg.SnyggPropertySetSpecBuilder
import org.florisboard.lib.snygg.SnyggSpec
import org.florisboard.lib.snygg.SnyggPropertySetSpecDeclBuilder
import org.florisboard.lib.snygg.SnyggSpecDecl
import org.florisboard.lib.snygg.value.SnyggCircleShapeValue
import org.florisboard.lib.snygg.value.SnyggCutCornerDpShapeValue
import org.florisboard.lib.snygg.value.SnyggCutCornerPercentShapeValue
import org.florisboard.lib.snygg.value.SnyggDpSizeValue
import org.florisboard.lib.snygg.value.SnyggMaterialYouDarkColorValue
import org.florisboard.lib.snygg.value.SnyggMaterialYouLightColorValue
import org.florisboard.lib.snygg.value.SnyggDynamicColorDarkColorValue
import org.florisboard.lib.snygg.value.SnyggDynamicColorLightColorValue
import org.florisboard.lib.snygg.value.SnyggRectangleShapeValue
import org.florisboard.lib.snygg.value.SnyggRoundedCornerDpShapeValue
import org.florisboard.lib.snygg.value.SnyggRoundedCornerPercentShapeValue
import org.florisboard.lib.snygg.value.SnyggSolidColorValue
import org.florisboard.lib.snygg.value.SnyggStaticColorValue
import org.florisboard.lib.snygg.value.SnyggSpSizeValue
fun SnyggPropertySetSpecBuilder.background() {
fun SnyggPropertySetSpecDeclBuilder.background() {
property(
name = Snygg.Background,
level = SnyggLevel.BASIC,
supportedValues(SnyggSolidColorValue, SnyggMaterialYouLightColorValue, SnyggMaterialYouDarkColorValue),
supportedValues(SnyggStaticColorValue, SnyggDynamicColorLightColorValue, SnyggDynamicColorDarkColorValue),
)
}
fun SnyggPropertySetSpecBuilder.foreground() {
fun SnyggPropertySetSpecDeclBuilder.foreground() {
property(
name = Snygg.Foreground,
level = SnyggLevel.BASIC,
supportedValues(SnyggSolidColorValue, SnyggMaterialYouLightColorValue, SnyggMaterialYouDarkColorValue),
supportedValues(SnyggStaticColorValue, SnyggDynamicColorLightColorValue, SnyggDynamicColorDarkColorValue),
)
}
fun SnyggPropertySetSpecBuilder.border() {
fun SnyggPropertySetSpecDeclBuilder.border() {
property(
name = Snygg.BorderColor,
level = SnyggLevel.ADVANCED,
supportedValues(SnyggSolidColorValue, SnyggMaterialYouLightColorValue, SnyggMaterialYouDarkColorValue),
supportedValues(SnyggStaticColorValue, SnyggDynamicColorLightColorValue, SnyggDynamicColorDarkColorValue),
)
property(
name = Snygg.BorderWidth,
@@ -58,21 +59,21 @@ fun SnyggPropertySetSpecBuilder.border() {
supportedValues(SnyggDpSizeValue),
)
}
fun SnyggPropertySetSpecBuilder.font() {
fun SnyggPropertySetSpecDeclBuilder.font() {
property(
name = Snygg.FontSize,
level = SnyggLevel.ADVANCED,
supportedValues(SnyggSpSizeValue),
)
}
fun SnyggPropertySetSpecBuilder.shadow() {
fun SnyggPropertySetSpecDeclBuilder.shadow() {
property(
name = Snygg.ShadowElevation,
level = SnyggLevel.ADVANCED,
supportedValues(SnyggDpSizeValue),
)
}
fun SnyggPropertySetSpecBuilder.shape() {
fun SnyggPropertySetSpecDeclBuilder.shape() {
property(
name = Snygg.Shape,
level = SnyggLevel.ADVANCED,
@@ -87,7 +88,7 @@ fun SnyggPropertySetSpecBuilder.shape() {
)
}
object FlorisImeUiSpec : SnyggSpec({
object FlorisImeUiSpec : SnyggSpecDecl({
element(FlorisImeUi.Keyboard) {
background()
}
@@ -276,3 +277,40 @@ object FlorisImeUiSpec : SnyggSpec({
background()
}
})
Snygg.init(
stylesheetSpec = FlorisImeUiSpec,
rulePreferredElementSorting = listOf(
FlorisImeUi.Keyboard,
FlorisImeUi.Key,
FlorisImeUi.KeyHint,
FlorisImeUi.KeyPopup,
FlorisImeUi.Smartbar,
FlorisImeUi.SmartbarSharedActionsRow,
FlorisImeUi.SmartbarSharedActionsToggle,
FlorisImeUi.SmartbarExtendedActionsRow,
FlorisImeUi.SmartbarExtendedActionsToggle,
FlorisImeUi.SmartbarActionKey,
FlorisImeUi.SmartbarActionTile,
FlorisImeUi.SmartbarActionsOverflow,
FlorisImeUi.SmartbarActionsOverflowCustomizeButton,
FlorisImeUi.SmartbarActionsEditor,
FlorisImeUi.SmartbarActionsEditorHeader,
FlorisImeUi.SmartbarActionsEditorSubheader,
FlorisImeUi.SmartbarCandidatesRow,
FlorisImeUi.SmartbarCandidateWord,
FlorisImeUi.SmartbarCandidateClip,
FlorisImeUi.SmartbarCandidateSpacer,
),
rulePlaceholders = mapOf(
"c:delete" to KeyCode.DELETE,
"c:enter" to KeyCode.ENTER,
"c:shift" to KeyCode.SHIFT,
"c:space" to KeyCode.SPACE,
"sh:unshifted" to InputShiftState.UNSHIFTED.value,
"sh:shifted_manual" to InputShiftState.SHIFTED_MANUAL.value,
"sh:shifted_automatic" to InputShiftState.SHIFTED_AUTOMATIC.value,
"sh:caps_lock" to InputShiftState.CAPS_LOCK.value,
),
)
*/

View File

@@ -28,6 +28,12 @@ inline fun extCoreTheme(id: String) = ExtensionComponentName(
componentId = id,
)
@Suppress("NOTHING_TO_INLINE")
inline fun extPreviewTheme(id: String) = ExtensionComponentName(
extensionId = "local.themes.preview",
componentId = id,
)
interface ThemeExtensionComponent : ExtensionComponent {
companion object {
fun defaultStylesheetPath(id: String): String {
@@ -39,8 +45,6 @@ interface ThemeExtensionComponent : ExtensionComponent {
override val label: String
override val authors: List<String>
val isNightTheme: Boolean
val isBorderless: Boolean
val isMaterialYouAware: Boolean
val stylesheetPath: String?
fun stylesheetPath(): String = stylesheetPath.takeUnless { it.isNullOrBlank() } ?: defaultStylesheetPath(id)
@@ -53,14 +57,12 @@ data class ThemeExtensionComponentImpl(
override val authors: List<String>,
@SerialName("isNight")
override val isNightTheme: Boolean = true,
override val isBorderless: Boolean = false,
override val isMaterialYouAware: Boolean = false,
@SerialName("stylesheet")
override val stylesheetPath: String? = null,
) : ThemeExtensionComponent {
fun edit() = ThemeExtensionComponentEditor(
id, label, authors, isNightTheme, isBorderless, isMaterialYouAware, stylesheetPath ?: "",
id, label, authors, isNightTheme, stylesheetPath ?: "",
)
}
@@ -69,8 +71,6 @@ class ThemeExtensionComponentEditor(
override var label: String = "",
override var authors: List<String> = emptyList(),
override var isNightTheme: Boolean = true,
override var isBorderless: Boolean = false,
override var isMaterialYouAware: Boolean = false,
override var stylesheetPath: String = "",
) : ThemeExtensionComponent {
@@ -83,8 +83,6 @@ class ThemeExtensionComponentEditor(
label = label.trim(),
authors = authors.filterNot { it.isBlank() },
isNightTheme = isNightTheme,
isBorderless = isBorderless,
isMaterialYouAware = isMaterialYouAware,
stylesheetPath = stylesheetPath.takeUnless { it.isBlank() },
)
check(id.isNotBlank()) { "Theme component ID cannot be blank" }

View File

@@ -45,7 +45,10 @@ import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.appContext
import dev.patrickgold.florisboard.extensionManager
import dev.patrickgold.florisboard.ime.smartbar.CachedInlineSuggestionsChipStyleSet
import dev.patrickgold.florisboard.lib.devtools.flogInfo
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
import dev.patrickgold.florisboard.lib.ext.ExtensionMeta
import dev.patrickgold.florisboard.lib.io.ZipUtils
import dev.patrickgold.florisboard.lib.util.ViewUtils
import kotlinx.coroutines.CoroutineScope
@@ -54,11 +57,13 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.florisboard.lib.kotlin.io.FsDir
import org.florisboard.lib.kotlin.io.deleteContentsRecursively
import org.florisboard.lib.kotlin.io.subDir
import org.florisboard.lib.kotlin.io.subFile
import org.florisboard.lib.snygg.SnyggStylesheet
import org.florisboard.lib.snygg.SnyggStylesheetJsonConfig
import org.florisboard.lib.snygg.ui.solidColor
import org.florisboard.lib.snygg.value.MaterialYouColor
import org.florisboard.lib.snygg.value.SnyggSolidColorValue
import org.florisboard.lib.snygg.value.SnyggStaticColorValue
import java.util.UUID
import kotlin.properties.Delegates
/**
@@ -119,7 +124,6 @@ class ThemeManager(context: Context) {
*/
fun updateActiveTheme(action: () -> Unit = { }) = scope.launch {
activeThemeGuard.withLock {
MaterialYouColor.resetColorSchemeCache()
action()
previewThemeInfo?.let { previewThemeInfo ->
_activeThemeInfo.postValue(previewThemeInfo)
@@ -129,25 +133,40 @@ class ThemeManager(context: Context) {
val cachedInfo = cachedThemeInfos.find { it.name == activeName }
if (cachedInfo != null) {
_activeThemeInfo.postValue(cachedInfo)
} else {
val themeExt = extensionManager.getExtensionById(activeName.extensionId) as? ThemeExtension
val themeExtRef = themeExt?.sourceRef
if (themeExtRef != null) {
val themeConfig = themeExt.themes.find { it.id == activeName.componentId }
if (themeConfig != null) {
val newStylesheet = ZipUtils.readFileFromArchive(
appContext, themeExtRef, themeConfig.stylesheetPath(),
).getOrNull()?.let { raw -> SnyggStylesheetJsonConfig.decodeFromString<SnyggStylesheet>(raw) }
if (newStylesheet != null) {
val newCompiledStylesheet = (newStylesheet)
.compileToFullyQualified(FlorisImeUiSpec)
val newInfo = ThemeInfo(activeName, themeConfig, newCompiledStylesheet)
cachedThemeInfos.add(newInfo)
_activeThemeInfo.postValue(newInfo)
}
}
}
return@withLock
}
val themeExt = extensionManager.getExtensionById(activeName.extensionId) as? ThemeExtension
val themeExtRef = themeExt?.sourceRef
if (themeExtRef == null) {
return@withLock
}
val themeConfig = themeExt.themes.find { it.id == activeName.componentId }
if (themeConfig == null) {
return@withLock
}
// TODO: loaded dir is implemented already...
// TODO: this leaks the loaded dir, but at least the state is not kaputt from compose viewpoint
val loadedDir = appContext.cacheDir.subDir("loaded").subDir(UUID.randomUUID().toString())
runCatching {
loadedDir.mkdirs()
loadedDir.deleteContentsRecursively()
ZipUtils.unzip(appContext, themeExtRef, loadedDir).getOrThrow()
flogInfo { "Loaded extension ${themeExt.meta.id} into $loadedDir" }
val stylesheetFile = loadedDir.subFile(themeConfig.stylesheetPath())
val stylesheetJson = stylesheetFile.readText()
SnyggStylesheet.fromJson(stylesheetJson).getOrThrow()
}.fold(
onSuccess = { newStylesheet ->
val newInfo = ThemeInfo(activeName, themeConfig, newStylesheet, loadedDir, null)
cachedThemeInfos.add(newInfo)
_activeThemeInfo.postValue(newInfo)
},
onFailure = { cause ->
_activeThemeInfo.postValue(ThemeInfo.DEFAULT.copy(
loadFailure = LoadFailure(themeExt.meta, themeConfig, cause)
))
},
)
}
}
@@ -188,20 +207,16 @@ class ThemeManager(context: Context) {
* Creates a new inline suggestion UI bundle based on the attributes of the given [style].
*
* @param context The context of the parent view/controller.
* @param style The theme from which the color attributes should be fetched. Defaults to
* [FlorisImeThemeBaseStyle].
* @param style The style set which is responsible for styling the chips.
*
* @return A bundle containing all necessary attributes for the inline suggestion views to properly display.
*/
@SuppressLint("RestrictedApi")
@RequiresApi(Build.VERSION_CODES.R)
fun createInlineSuggestionUiStyleBundle(
context: Context,
style: SnyggStylesheet = activeThemeInfo.value?.stylesheet ?: FlorisImeThemeBaseStyle,
): Bundle {
val snyggStyle = style.getStatic(FlorisImeUi.SmartbarSharedActionsToggle)
val bgColor = snyggStyle.background.solidColor(context)
val fgColor = snyggStyle.foreground.solidColor(context)
fun createInlineSuggestionUiStyleBundle(context: Context): Bundle? {
val styleSet = CachedInlineSuggestionsChipStyleSet ?: return null
val bgColor = styleSet.background(default = Color.White)
val fgColor = styleSet.foreground(default = Color.Black)
val bgDrawableId = androidx.autofill.R.drawable.autofill_inline_suggestion_chip_background
val bgDrawable = Icon.createWithResource(context, bgDrawableId).apply {
@@ -276,21 +291,35 @@ class ThemeManager(context: Context) {
val name: ExtensionComponentName,
val config: ThemeExtensionComponent,
val stylesheet: SnyggStylesheet,
val loadedDir: FsDir?,
val loadFailure: LoadFailure?,
) {
override fun toString(): String {
return "ThemeInfo(name=$name, config=$config, loadedDir=$loadedDir)"
}
companion object {
val DEFAULT = ThemeInfo(
name = extCoreTheme("base"),
config = ThemeExtensionComponentImpl(id = "base", label = "Base", authors = listOf()),
stylesheet = FlorisImeThemeBaseStyle.compileToFullyQualified(FlorisImeUiSpec),
stylesheet = FlorisImeThemeBaseStyle,
loadedDir = null,
loadFailure = null,
)
}
}
data class LoadFailure(
val extension: ExtensionMeta,
val component: ThemeExtensionComponent,
val cause: Throwable,
)
data class RemoteColors(
val packageName: String,
val colorPrimary: SnyggSolidColorValue?,
val colorPrimaryVariant: SnyggSolidColorValue?,
val colorSecondary: SnyggSolidColorValue?,
val colorPrimary: SnyggStaticColorValue?,
val colorPrimaryVariant: SnyggStaticColorValue?,
val colorSecondary: SnyggStaticColorValue?,
) {
companion object {
val DEFAULT = RemoteColors("undefined", null, null, null)

View File

@@ -0,0 +1,35 @@
/*
* Copyright (C) 2025 The FlorisBoard Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.theme
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import dev.patrickgold.florisboard.lib.devtools.flogDebug
import dev.patrickgold.florisboard.themeManager
class WallpaperChangeReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent == null) return
if (context == null) return
@Suppress("DEPRECATION") // We do not retrieve the wallpaper but only listen to changes
if (intent.action == Intent.ACTION_WALLPAPER_CHANGED) {
flogDebug { "Wallpaper changed" }
context.themeManager().value.updateActiveTheme()
}
}
}

View File

@@ -217,7 +217,7 @@ class FlorisLocale private constructor(val base: Locale) {
*/
val supportsCapitalization: Boolean
get() = when (language) {
"zh", "ko", "th", "bn" -> false
"zh", "ko", "th", "bn", "hi" -> false
else -> true
}

View File

@@ -51,6 +51,7 @@ import org.florisboard.lib.kotlin.io.readJson
import org.florisboard.lib.kotlin.io.subDir
import org.florisboard.lib.kotlin.io.subFile
import java.io.Closeable
import java.io.File
import java.util.UUID
class CacheManager(context: Context) {
@@ -62,6 +63,8 @@ class CacheManager(context: Context) {
private const val ExporterDirName = "exporter"
private const val EditorDirName = "editor"
private const val BackupAndRestoreDirName = "backup-and-restore"
const val LoadedDirName = "loaded"
}
private val appContext by context.appContext()

View File

@@ -0,0 +1,30 @@
/*
* Copyright (C) 2025 The FlorisBoard Contributors
*
* 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.lib.compose
import androidx.compose.ui.graphics.Color
import dev.patrickgold.jetpref.datastore.model.PreferenceSerializer
object ColorPreferenceSerializer : PreferenceSerializer<Color> {
@OptIn(ExperimentalStdlibApi::class)
override fun deserialize(value: String): Color {
return Color(value.hexToULong())
}
@OptIn(ExperimentalStdlibApi::class)
override fun serialize(value: Color): String = value.value.toHexString()
}

View File

@@ -16,10 +16,7 @@
package dev.patrickgold.florisboard.lib.compose
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
@@ -34,14 +31,11 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
@Composable
fun FlorisButton(
@@ -166,38 +160,3 @@ fun FlorisIconButton(
}
}
}
@Composable
fun FlorisIconButtonWithInnerPadding(
onClick: () -> Unit,
modifier: Modifier = Modifier,
icon: ImageVector,
enabled: Boolean = true,
iconModifier: Modifier = Modifier,
iconColor: Color = Color.Unspecified,
) {
IconButton(
modifier = modifier,
enabled = enabled,
onClick = onClick,
) {
val contentAlpha = if (enabled) 1f else 0.14f
CompositionLocalProvider(
LocalContentColor provides iconColor,
) {
Box(
modifier = iconModifier
.padding(4.dp)
.fillMaxHeight()
.aspectRatio(1f),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = modifier.alpha(contentAlpha),
imageVector = icon,
contentDescription = null,
)
}
}
}
}

View File

@@ -41,6 +41,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
//TODO: Use JetPrefDropDownMenu instead
@Composable
fun <T : Any> FlorisDropdownMenu(
items: List<T>,

View File

@@ -1,228 +0,0 @@
/*
* Copyright (C) 2021-2025 The FlorisBoard Contributors
*
* 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.lib.compose
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.lib.ValidationResult
@Composable
fun FlorisOutlinedTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
textStyle: TextStyle = TextStyle.Default,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
singleLine: Boolean = false,
maxLines: Int = Int.MAX_VALUE,
placeholder: String? = null,
isError: Boolean = false,
showValidationHint: Boolean = true,
showValidationError: Boolean = false,
validationResult: ValidationResult? = null,
visualTransformation: VisualTransformation = VisualTransformation.None,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = MaterialTheme.shapes.small,
colors: TextFieldColors = OutlinedTextFieldDefaults.colors(),
) {
var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = value)) }
val textFieldValue = textFieldValueState.copy(text = value)
FlorisOutlinedTextField(
value = textFieldValue,
onValueChange = {
textFieldValueState = it
if (value != it.text) {
onValueChange(it.text)
}
},
modifier = modifier,
enabled = enabled,
readOnly = readOnly,
textStyle = textStyle,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
singleLine = singleLine,
maxLines = maxLines,
placeholder = placeholder,
isError = isError,
showValidationHint = showValidationHint,
showValidationError = showValidationError,
validationResult = validationResult,
visualTransformation = visualTransformation,
interactionSource = interactionSource,
shape = shape,
colors = colors,
)
}
@Composable
fun FlorisOutlinedTextField(
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
textStyle: TextStyle = TextStyle.Default,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
singleLine: Boolean = false,
maxLines: Int = Int.MAX_VALUE,
placeholder: String? = null,
isError: Boolean = false,
showValidationHint: Boolean = true,
showValidationError: Boolean = false,
validationResult: ValidationResult? = null,
visualTransformation: VisualTransformation = VisualTransformation.None,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = MaterialTheme.shapes.small,
colors: TextFieldColors = OutlinedTextFieldDefaults.colors(
unfocusedBorderColor = MaterialTheme.colorScheme.outline,
disabledBorderColor = MaterialTheme.colorScheme.outline,
),
) {
val textColor = textStyle.color.takeOrElse {
if (!enabled) {
colors.disabledTextColor
} else {
colors.unfocusedTextColor
}
}
val mergedTextStyle = textStyle.copy(color = textColor, textDirection = TextDirection.Content)
val isFocused by interactionSource.collectIsFocusedAsState()
val isErrorState = isError || (showValidationError && validationResult?.isInvalid() == true)
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
BasicTextField(
modifier = modifier.padding(vertical = 4.dp),
value = value,
onValueChange = onValueChange,
enabled = enabled,
readOnly = readOnly,
textStyle = mergedTextStyle,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
singleLine = singleLine,
maxLines = maxLines,
visualTransformation = visualTransformation,
cursorBrush = SolidColor(
if (isErrorState) {
colors.errorCursorColor
} else {
colors.cursorColor
}
),
decorationBox = { innerTextField ->
Surface(
modifier = modifier.fillMaxWidth(),
color = if (enabled) {
if (isErrorState) {
colors.errorContainerColor
} else if (isFocused) {
colors.focusedContainerColor
} else {
colors.unfocusedContainerColor
}
} else {
colors.disabledContainerColor
},
border = if (isErrorState && enabled) {
BorderStroke(ButtonDefaults.outlinedButtonBorder.width, MaterialTheme.colorScheme.error)
} else if (isFocused) {
BorderStroke(ButtonDefaults.outlinedButtonBorder.width, MaterialTheme.colorScheme.primary)
} else {
ButtonDefaults.outlinedButtonBorder
},
shape = shape,
) {
Box(
modifier = Modifier
.defaultMinSize(
minWidth = ButtonDefaults.MinWidth,
minHeight = 40.dp,
)
.padding(ButtonDefaults.ContentPadding),
contentAlignment = Alignment.CenterStart,
) {
ProvideTextStyle(value = mergedTextStyle) {
innerTextField()
}
if (!placeholder.isNullOrBlank()) {
Text(
text = placeholder,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.56f),
)
}
}
}
},
)
}
if (showValidationHint && validationResult?.isValid() == true && validationResult.hasHintMessage()) {
Text(
text = validationResult.hintMessage(),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.56f),
)
}
if (showValidationError && validationResult?.isInvalid() == true && validationResult.hasErrorMessage()) {
Text(
text = validationResult.errorMessage(),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error,
)
}
}

View File

@@ -22,31 +22,23 @@ import android.content.ContextWrapper
import android.inputmethodservice.InputMethodService
import android.view.Window
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowInsetsControllerCompat
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import org.florisboard.lib.android.AndroidVersion
import org.florisboard.lib.snygg.ui.solidColor
@Composable
fun SystemUiIme() {
val useDarkIcons = !FlorisImeTheme.config.isNightTheme
val context = LocalContext.current
val view = LocalView.current
val backgroundColor = FlorisImeTheme.style.get(FlorisImeUi.SystemNavBar).background.solidColor(context)
val window = view.context.findWindow()!!
val windowInsetsController = WindowInsetsControllerCompat(window, view)
SideEffect {
if (AndroidVersion.ATLEAST_API26_O) {
window.navigationBarColor = backgroundColor.toArgb()
windowInsetsController.isAppearanceLightNavigationBars = useDarkIcons
if (AndroidVersion.ATLEAST_API29_Q) window.isNavigationBarContrastEnforced = true
LaunchedEffect(useDarkIcons) {
windowInsetsController.isAppearanceLightNavigationBars = useDarkIcons
if (AndroidVersion.ATLEAST_API29_Q) {
window.isNavigationBarContrastEnforced = true
}
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright (C) 2025 The FlorisBoard Contributors
*
* 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.lib.compose
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import dev.patrickgold.florisboard.lib.ValidationResult
@Composable
fun Validation(
showValidationErrors: Boolean,
validationResult: ValidationResult?,
) {
if (showValidationErrors) {
if (validationResult is ValidationResult.Valid && validationResult.hasHintMessage()) {
Text(
text = validationResult.hintMessage(),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.56f),
)
}
if (validationResult is ValidationResult.Invalid && validationResult.hasErrorMessage()) {
Text(
text = validationResult.errorMessage(),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error,
)
}
}
}

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