HeliBoard/app/src/main/java/helium314/keyboard/keyboard/KeyboardTheme.kt
2025-02-11 23:21:34 +01:00

497 lines
24 KiB
Kotlin

/*
* Copyright (C) 2014 The Android Open Source Project
* modified
* SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
*/
package helium314.keyboard.keyboard
import android.content.Context
import android.content.SharedPreferences
import android.graphics.Color
import android.os.Build
import android.util.TypedValue
import android.view.ContextThemeWrapper
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import helium314.keyboard.latin.R
import helium314.keyboard.latin.common.AllColors
import helium314.keyboard.latin.common.ColorType
import helium314.keyboard.latin.common.Colors
import helium314.keyboard.latin.common.DefaultColors
import helium314.keyboard.latin.common.DynamicColors
import helium314.keyboard.latin.settings.Defaults
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.utils.brightenOrDarken
import helium314.keyboard.latin.utils.isBrightColor
import helium314.keyboard.latin.utils.isGoodContrast
import helium314.keyboard.latin.utils.prefs
import helium314.keyboard.settings.keyboardNeedsReload
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.util.EnumMap
class KeyboardTheme // Note: The themeId should be aligned with "themeId" attribute of Keyboard style in values/themes-<style>.xml.
private constructor(val themeId: Int, @JvmField val mStyleId: Int) {
override fun equals(other: Any?) = if (other === this) true
else (other as? KeyboardTheme)?.themeId == themeId
override fun hashCode(): Int {
return themeId
}
companion object {
// old themes, now called styles
const val STYLE_MATERIAL = "Material"
const val STYLE_HOLO = "Holo"
const val STYLE_ROUNDED = "Rounded"
// new themes that are just colors
const val THEME_LIGHT = "light"
const val THEME_HOLO_WHITE = "holo_white"
const val THEME_DARK = "dark"
const val THEME_DARKER = "darker"
const val THEME_BLACK = "black"
const val THEME_DYNAMIC = "dynamic"
const val THEME_BLUE_GRAY = "blue_gray"
const val THEME_BROWN = "brown"
const val THEME_CHOCOLATE = "chocolate"
const val THEME_CLOUDY = "cloudy"
const val THEME_FOREST = "forest"
const val THEME_INDIGO = "indigo"
const val THEME_OCEAN = "ocean"
const val THEME_PINK = "pink"
const val THEME_SAND = "sand"
const val THEME_VIOLETTE = "violette"
fun getAvailableDefaultColors(prefs: SharedPreferences, isNight: Boolean) = listOfNotNull(
if (!isNight) THEME_LIGHT else null, THEME_DARK,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) THEME_DYNAMIC else null,
if (prefs.getString(Settings.PREF_THEME_STYLE, Defaults.PREF_THEME_STYLE) == STYLE_HOLO) THEME_HOLO_WHITE else null,
THEME_DARKER,
THEME_BLACK,
if (!isNight) THEME_BLUE_GRAY else null,
if (!isNight) THEME_BROWN else null,
THEME_CHOCOLATE,
THEME_CLOUDY,
THEME_FOREST,
if (!isNight) THEME_INDIGO else null,
if (!isNight) THEME_PINK else null,
THEME_OCEAN,
if (!isNight) THEME_SAND else null,
THEME_VIOLETTE
)
val STYLES = arrayOf(STYLE_MATERIAL, STYLE_HOLO, STYLE_ROUNDED)
// These should be aligned with Keyboard.themeId and Keyboard.Case.keyboardTheme
// attributes' values in attrs.xml.
private const val THEME_ID_HOLO_BASE = 0
private const val THEME_ID_LXX_BASE = 1
private const val THEME_ID_LXX_BASE_BORDER = 2
private const val THEME_ID_ROUNDED_BASE = 3
private const val THEME_ID_ROUNDED_BASE_BORDER = 4
private const val DEFAULT_THEME_ID = THEME_ID_LXX_BASE
private val KEYBOARD_THEMES = arrayOf(
KeyboardTheme(THEME_ID_HOLO_BASE, R.style.KeyboardTheme_HoloBase),
KeyboardTheme(THEME_ID_LXX_BASE, R.style.KeyboardTheme_LXX_Base),
KeyboardTheme(THEME_ID_LXX_BASE_BORDER, R.style.KeyboardTheme_LXX_Base_Border),
KeyboardTheme(THEME_ID_ROUNDED_BASE, R.style.KeyboardTheme_Rounded_Base),
KeyboardTheme(THEME_ID_ROUNDED_BASE_BORDER, R.style.KeyboardTheme_Rounded_Base_Border)
)
// named colors, with names from old settings
const val COLOR_ACCENT = "accent"
const val COLOR_GESTURE = "gesture"
const val COLOR_SUGGESTION_TEXT = "suggestion_text"
const val COLOR_TEXT = "text"
const val COLOR_HINT_TEXT = "hint_text"
const val COLOR_KEYS = "keys"
const val COLOR_FUNCTIONAL_KEYS = "functional_keys"
const val COLOR_SPACEBAR = "spacebar"
const val COLOR_SPACEBAR_TEXT = "spacebar_text"
const val COLOR_BACKGROUND = "background"
@JvmStatic
fun getKeyboardTheme(context: Context): KeyboardTheme {
val prefs = context.prefs()
val style = prefs.getString(Settings.PREF_THEME_STYLE, Defaults.PREF_THEME_STYLE)
val borders = prefs.getBoolean(Settings.PREF_THEME_KEY_BORDERS, Defaults.PREF_THEME_KEY_BORDERS)
val matchingId = when (style) {
STYLE_HOLO -> THEME_ID_HOLO_BASE
STYLE_ROUNDED -> if (borders) THEME_ID_ROUNDED_BASE_BORDER else THEME_ID_ROUNDED_BASE
else -> if (borders) THEME_ID_LXX_BASE_BORDER else THEME_ID_LXX_BASE
}
return KEYBOARD_THEMES.firstOrNull { it.themeId == matchingId } ?: KEYBOARD_THEMES[DEFAULT_THEME_ID]
}
fun getThemeActionAndEmojiKeyLabelFlags(themeId: Int): Int {
return if (themeId == THEME_ID_LXX_BASE || themeId == THEME_ID_ROUNDED_BASE) Key.LABEL_FLAGS_KEEP_BACKGROUND_ASPECT_RATIO else 0
}
@JvmStatic
fun getThemeColors(themeName: String, themeStyle: String, context: Context, prefs: SharedPreferences, isNight: Boolean): Colors {
val hasBorders = prefs.getBoolean(Settings.PREF_THEME_KEY_BORDERS, Defaults.PREF_THEME_KEY_BORDERS)
val backgroundImage = Settings.readUserBackgroundImage(context, isNight)
return when (themeName) {
THEME_DYNAMIC -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) DynamicColors(context, themeStyle, hasBorders, backgroundImage)
else getThemeColors(THEME_LIGHT, themeStyle, context, prefs, isNight)
}
THEME_DARK -> DefaultColors(
themeStyle,
hasBorders,
ContextCompat.getColor(context, R.color.gesture_trail_color_lxx_dark),
Color.parseColor("#263238"),
Color.parseColor("#364248"),
Color.parseColor("#2d393f"),
Color.parseColor("#364248"),
ContextCompat.getColor(context, R.color.key_text_color_lxx_dark),
ContextCompat.getColor(context, R.color.key_hint_letter_color_lxx_dark),
keyboardBackground = backgroundImage
)
THEME_HOLO_WHITE -> DefaultColors(
themeStyle,
hasBorders,
Color.WHITE,
Color.parseColor("#282828"),
Color.WHITE, // drawable is transparent
Color.parseColor("#444444"), // should be 222222, but the key drawable is already grey
Color.WHITE,
Color.WHITE,
Color.parseColor("#282828"),
Color.WHITE,
Color.parseColor("#80FFFFFF"),
keyboardBackground = backgroundImage
)
THEME_DARKER -> DefaultColors(
themeStyle,
hasBorders,
ContextCompat.getColor(context, R.color.gesture_trail_color_lxx_dark),
ContextCompat.getColor(context, R.color.keyboard_background_lxx_dark_border),
ContextCompat.getColor(context, R.color.key_background_normal_lxx_dark_border),
ContextCompat.getColor(context, R.color.key_background_functional_lxx_dark_border),
ContextCompat.getColor(context, R.color.key_background_normal_lxx_dark_border),
ContextCompat.getColor(context, R.color.key_text_color_lxx_dark),
ContextCompat.getColor(context, R.color.key_hint_letter_color_lxx_dark),
keyboardBackground = backgroundImage
)
THEME_BLACK -> DefaultColors(
themeStyle,
hasBorders,
ContextCompat.getColor(context, R.color.gesture_trail_color_lxx_dark),
ContextCompat.getColor(context, R.color.background_amoled_black),
ContextCompat.getColor(context, R.color.background_amoled_dark),
ContextCompat.getColor(context, R.color.background_amoled_dark),
ContextCompat.getColor(context, R.color.background_amoled_dark),
ContextCompat.getColor(context, R.color.key_text_color_lxx_dark),
ContextCompat.getColor(context, R.color.key_hint_letter_color_lxx_dark),
keyboardBackground = backgroundImage
)
THEME_BLUE_GRAY -> DefaultColors(
themeStyle,
hasBorders,
Color.rgb(120, 144, 156),
Color.rgb(236, 239, 241),
Color.WHITE,
Color.rgb(207, 216, 220),
Color.WHITE,
Color.BLACK,
Color.BLACK,
keyboardBackground = backgroundImage
)
THEME_BROWN -> DefaultColors(
themeStyle,
hasBorders,
Color.rgb(141, 110, 99),
Color.rgb(239, 235, 233),
Color.WHITE,
Color.rgb(215, 204, 200),
Color.WHITE,
Color.BLACK,
Color.BLACK,
keyboardBackground = backgroundImage
)
THEME_CHOCOLATE -> DefaultColors(
themeStyle,
hasBorders,
Color.rgb(80, 128, 255),
Color.rgb(140, 112, 94),
Color.rgb(193, 163, 146),
Color.rgb(168, 127, 103),
Color.rgb(193, 163, 146),
Color.WHITE,
Color.WHITE,
keyboardBackground = backgroundImage
)
THEME_CLOUDY -> DefaultColors(
themeStyle,
hasBorders,
Color.rgb(255, 113, 129),
Color.rgb(81, 97, 113),
Color.rgb(117, 128, 142),
Color.rgb(99, 109, 121),
Color.rgb(117, 128, 142),
Color.WHITE,
Color.WHITE,
keyboardBackground = backgroundImage
)
THEME_FOREST -> DefaultColors(
themeStyle,
hasBorders,
Color.rgb(75, 110, 75),
Color.rgb(181, 125, 88),
Color.rgb(228, 212, 191),
Color.rgb(212, 186, 153),
Color.rgb(228, 212, 191),
Color.rgb(0, 50, 0),
Color.rgb(0, 50, 0),
Color.rgb(0, 50, 0),
Color.rgb(0, 80, 0),
keyboardBackground = backgroundImage
)
THEME_INDIGO -> DefaultColors(
themeStyle,
hasBorders,
Color.rgb(92, 107, 192),
Color.rgb(232, 234, 246),
Color.WHITE,
Color.rgb(197, 202, 233),
Color.WHITE,
Color.BLACK,
Color.BLACK,
keyboardBackground = backgroundImage
)
THEME_OCEAN -> DefaultColors(
themeStyle,
hasBorders,
Color.rgb(255, 124, 0),
Color.rgb(89, 109, 155),
Color.rgb(132, 157, 212),
Color.rgb(81, 116, 194),
Color.rgb(132, 157, 212),
Color.WHITE,
Color.WHITE,
keyboardBackground = backgroundImage
)
THEME_PINK -> DefaultColors(
themeStyle,
hasBorders,
Color.rgb(236, 64, 122),
Color.rgb(252, 228, 236),
Color.WHITE,
Color.rgb(248, 187, 208),
Color.WHITE,
Color.BLACK,
Color.BLACK,
keyboardBackground = backgroundImage
)
THEME_SAND -> DefaultColors(
themeStyle,
hasBorders,
Color.rgb(110, 155, 255),
Color.rgb(242, 232, 218),
Color.WHITE,
Color.rgb(234, 211, 185),
Color.WHITE,
Color.BLACK,
Color.BLACK,
keyboardBackground = backgroundImage
)
THEME_VIOLETTE -> DefaultColors(
themeStyle,
hasBorders,
Color.rgb(255, 96, 255),
Color.rgb(112, 112, 174),
Color.rgb(150, 150, 216),
Color.rgb(123, 123, 206),
Color.rgb(150, 150, 216),
Color.WHITE,
Color.WHITE,
keyboardBackground = backgroundImage
)
else -> { // user-defined theme
if (readUserMoreColors(prefs, themeName) == 2)
AllColors(readUserAllColors(prefs, themeName), themeStyle, hasBorders, backgroundImage)
else {
val colors = readUserColors(prefs, themeName)
DefaultColors(
themeStyle,
hasBorders,
determineUserColor(colors, context, COLOR_ACCENT, false),
determineUserColor(colors, context, COLOR_BACKGROUND, false),
determineUserColor(colors, context, COLOR_KEYS, false),
determineUserColor(colors, context, COLOR_FUNCTIONAL_KEYS, false),
determineUserColor(colors, context, COLOR_SPACEBAR, false),
determineUserColor(colors, context, COLOR_TEXT, false),
determineUserColor(colors, context, COLOR_HINT_TEXT, false),
determineUserColor(colors, context, COLOR_SUGGESTION_TEXT, false),
determineUserColor(colors, context, COLOR_SPACEBAR_TEXT, false),
determineUserColor(colors, context, COLOR_GESTURE, false),
backgroundImage,
)
}
}
}
}
fun writeUserColors(prefs: SharedPreferences, themeName: String, colors: List<ColorSetting>) {
val key = Settings.PREF_USER_COLORS_PREFIX + themeName
val value = Json.encodeToString(colors.filter { it.color != null || it.auto == false })
prefs.edit().putString(key, value).apply()
keyboardNeedsReload = true
}
fun readUserColors(prefs: SharedPreferences, themeName: String): List<ColorSetting> {
val key = Settings.PREF_USER_COLORS_PREFIX + themeName
return Json.decodeFromString(prefs.getString(key, Defaults.PREF_USER_COLORS)!!)
}
fun writeUserMoreColors(prefs: SharedPreferences, themeName: String, value: Int) {
val key = Settings.PREF_USER_MORE_COLORS_PREFIX + themeName
prefs.edit().putInt(key, value).apply()
keyboardNeedsReload = true
}
fun readUserMoreColors(prefs: SharedPreferences, themeName: String): Int {
val key = Settings.PREF_USER_MORE_COLORS_PREFIX + themeName
return prefs.getInt(key, Defaults.PREF_USER_MORE_COLORS)
}
fun writeUserAllColors(prefs: SharedPreferences, themeName: String, colorMap: EnumMap<ColorType, Int>) {
val key = Settings.PREF_USER_ALL_COLORS_PREFIX + themeName
prefs.edit().putString(key, colorMap.map { "${it.key},${it.value}" }.joinToString(";")).apply()
keyboardNeedsReload = true
}
fun readUserAllColors(prefs: SharedPreferences, themeName: String): EnumMap<ColorType, Int> {
val key = Settings.PREF_USER_ALL_COLORS_PREFIX + themeName
val colorsString = prefs.getString(key, Defaults.PREF_USER_ALL_COLORS)!!
val colorMap = EnumMap<ColorType, Int>(ColorType::class.java)
colorsString.split(";").forEach {
val ct = try {
ColorType.valueOf(it.substringBefore(",").uppercase())
} catch (_: Exception) { // todo: which one?
return@forEach
}
val i = it.substringAfter(",").toIntOrNull() ?: return@forEach
colorMap[ct] = i
}
return colorMap
}
fun getUnusedThemeName(initialName: String, prefs: SharedPreferences): String {
val existingNames = prefs.all.keys.mapNotNull {
when {
it.startsWith(Settings.PREF_USER_COLORS_PREFIX) -> it.substringAfter(Settings.PREF_USER_COLORS_PREFIX)
it.startsWith(Settings.PREF_USER_ALL_COLORS_PREFIX) -> it.substringAfter(Settings.PREF_USER_ALL_COLORS_PREFIX)
it.startsWith(Settings.PREF_USER_MORE_COLORS_PREFIX) -> it.substringAfter(Settings.PREF_USER_MORE_COLORS_PREFIX)
else -> null
}
}.toSortedSet()
if (initialName !in existingNames) return initialName
var i = 1
while ("$initialName$i" in existingNames)
i++
return "$initialName$i"
}
// returns false if not renamed due to invalid name or collision
fun renameUserColors(from: String, to: String, prefs: SharedPreferences): Boolean {
if (to.isBlank()) return false // don't want that
if (to == from) return true // nothing to do
val existingNames = prefs.all.keys.mapNotNull {
when {
it.startsWith(Settings.PREF_USER_COLORS_PREFIX) -> it.substringAfter(Settings.PREF_USER_COLORS_PREFIX)
it.startsWith(Settings.PREF_USER_ALL_COLORS_PREFIX) -> it.substringAfter(Settings.PREF_USER_ALL_COLORS_PREFIX)
it.startsWith(Settings.PREF_USER_MORE_COLORS_PREFIX) -> it.substringAfter(Settings.PREF_USER_MORE_COLORS_PREFIX)
else -> null
}
}.toSortedSet()
if (to in existingNames) return false
// all good, now rename
prefs.edit {
if (prefs.contains(Settings.PREF_USER_COLORS_PREFIX + from)) {
putString(Settings.PREF_USER_COLORS_PREFIX + to, prefs.getString(Settings.PREF_USER_COLORS_PREFIX + from, ""))
remove(Settings.PREF_USER_COLORS_PREFIX + from)
}
if (prefs.contains(Settings.PREF_USER_ALL_COLORS_PREFIX + from)) {
putString(Settings.PREF_USER_ALL_COLORS_PREFIX + to, prefs.getString(Settings.PREF_USER_ALL_COLORS_PREFIX + from, ""))
remove(Settings.PREF_USER_ALL_COLORS_PREFIX + from)
}
if (prefs.contains(Settings.PREF_USER_MORE_COLORS_PREFIX + from)) {
putInt(Settings.PREF_USER_MORE_COLORS_PREFIX + to, prefs.getInt(Settings.PREF_USER_MORE_COLORS_PREFIX + from, 0))
remove(Settings.PREF_USER_MORE_COLORS_PREFIX + from)
}
if (prefs.getString(Settings.PREF_THEME_COLORS, Defaults.PREF_THEME_COLORS) == from)
putString(Settings.PREF_THEME_COLORS, to)
if (prefs.getString(Settings.PREF_THEME_COLORS_NIGHT, Defaults.PREF_THEME_COLORS_NIGHT) == from)
putString(Settings.PREF_THEME_COLORS_NIGHT, to)
}
return true
}
fun determineUserColor(colors: List<ColorSetting>, context: Context, colorName: String, isNight: Boolean): Int {
val c = colors.firstOrNull { it.name == colorName }
val color = c?.color
val auto = c?.auto ?: true
return if (auto || color == null)
determineAutoColor(colors, colorName, isNight, context)
else color
}
private fun determineAutoColor(colors: List<ColorSetting>, colorName: String, isNight: Boolean, context: Context): Int {
when (colorName) {
COLOR_ACCENT -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
// try determining accent color on Android 10 & 11, accent is not available in resources
val wrapper: Context = ContextThemeWrapper(context, android.R.style.Theme_DeviceDefault)
val value = TypedValue()
if (wrapper.theme.resolveAttribute(android.R.attr.colorAccent, value, true)) return value.data
}
return ContextCompat.getColor(Settings.getDayNightContext(context, isNight), R.color.accent)
}
COLOR_GESTURE -> return determineUserColor(colors, context, COLOR_ACCENT, isNight)
COLOR_SUGGESTION_TEXT ->
return determineUserColor(colors, context, COLOR_TEXT, isNight)
COLOR_TEXT -> {
// base it on background color, and not key, because it's also used for suggestions
val background = determineUserColor(colors, context, COLOR_BACKGROUND, isNight)
return if (isBrightColor(background)) {
// but if key borders are enabled, we still want reasonable contrast
if (!context.prefs().getBoolean(Settings.PREF_THEME_KEY_BORDERS, Defaults.PREF_THEME_KEY_BORDERS)
|| isGoodContrast(Color.BLACK, determineUserColor(colors, context, COLOR_KEYS, isNight))
) Color.BLACK
else Color.GRAY
} else Color.WHITE
}
COLOR_HINT_TEXT -> {
return if (isBrightColor(determineUserColor(colors, context, COLOR_KEYS, isNight))) Color.DKGRAY
else determineUserColor(colors, context, COLOR_TEXT, isNight)
}
COLOR_KEYS ->
return brightenOrDarken(determineUserColor(colors, context, COLOR_BACKGROUND, isNight), isNight)
COLOR_FUNCTIONAL_KEYS ->
return brightenOrDarken(determineUserColor(colors, context, COLOR_KEYS, isNight), true)
COLOR_SPACEBAR -> return determineUserColor(colors, context, COLOR_KEYS, isNight)
COLOR_SPACEBAR_TEXT -> {
val spacebar = determineUserColor(colors, context, COLOR_SPACEBAR, isNight)
val hintText = determineUserColor(colors, context, COLOR_HINT_TEXT, isNight)
if (isGoodContrast(hintText, spacebar)) return hintText and -0x7f000001 // add some transparency
val text = determineUserColor(colors, context, COLOR_TEXT, isNight)
if (isGoodContrast(text, spacebar)) return text and -0x7f000001
return if (isBrightColor(spacebar)) Color.BLACK and -0x7f000001
else Color.WHITE and -0x7f000001
}
COLOR_BACKGROUND -> return ContextCompat.getColor(
Settings.getDayNightContext(context, isNight),
R.color.keyboard_background
)
else -> return ContextCompat.getColor(Settings.getDayNightContext(context, isNight), R.color.keyboard_background)
}
}
}
}
@Serializable
data class ColorSetting(val name: String, val auto: Boolean?, val color: Int?) {
var displayName = name
}