allow setting a background image (separate for day and night)

todo: allow setting transparent background that should only apply to pressed keys with borders disabled?
This commit is contained in:
Helium314 2024-01-26 16:42:35 +01:00
parent fa04c393c8
commit 9d8a22880c
96 changed files with 266 additions and 115 deletions

View file

@ -17,6 +17,7 @@ import org.dslul.openboard.inputmethod.latin.common.DefaultColors
import org.dslul.openboard.inputmethod.latin.common.DynamicColors
import org.dslul.openboard.inputmethod.latin.settings.Settings
import org.dslul.openboard.inputmethod.latin.utils.DeviceProtectedUtils
import org.dslul.openboard.inputmethod.latin.utils.ResourceUtils
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) {
@ -100,6 +101,8 @@ private constructor(val themeId: Int, @JvmField val mStyleId: Int) {
@JvmStatic
fun getThemeColors(themeColors: String, themeStyle: String, context: Context, prefs: SharedPreferences): Colors {
val hasBorders = prefs.getBoolean(Settings.PREF_THEME_KEY_BORDERS, false)
val useNightImage = Settings.readDayNightPref(prefs, context.resources) && ResourceUtils.isNight(context.resources)
val backgroundImage = Settings.readUserBackgroundImage(context, useNightImage)
return when (themeColors) {
THEME_USER -> DefaultColors(
themeStyle,
@ -114,6 +117,7 @@ private constructor(val themeId: Int, @JvmField val mStyleId: Int) {
Settings.readUserColor(prefs, context, Settings.PREF_COLOR_SUGGESTION_TEXT_SUFFIX, false),
Settings.readUserColor(prefs, context, Settings.PREF_COLOR_SPACEBAR_TEXT_SUFFIX, false),
Settings.readUserColor(prefs, context, Settings.PREF_COLOR_GESTURE_SUFFIX, false),
keyboardBackground = backgroundImage
)
THEME_USER_NIGHT -> DefaultColors(
themeStyle,
@ -128,9 +132,10 @@ private constructor(val themeId: Int, @JvmField val mStyleId: Int) {
Settings.readUserColor(prefs, context, Settings.PREF_COLOR_SUGGESTION_TEXT_SUFFIX, true),
Settings.readUserColor(prefs, context, Settings.PREF_COLOR_SPACEBAR_TEXT_SUFFIX, true),
Settings.readUserColor(prefs, context, Settings.PREF_COLOR_GESTURE_SUFFIX, true),
keyboardBackground = backgroundImage
)
THEME_DYNAMIC -> {
if (Build.VERSION.SDK_INT >= VERSION_CODES.S) DynamicColors(context, themeStyle, hasBorders)
if (Build.VERSION.SDK_INT >= VERSION_CODES.S) DynamicColors(context, themeStyle, hasBorders, backgroundImage)
else getThemeColors(THEME_LIGHT, themeStyle, context, prefs)
}
THEME_DARK -> DefaultColors(
@ -143,6 +148,7 @@ private constructor(val themeId: Int, @JvmField val mStyleId: Int) {
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,
@ -156,6 +162,7 @@ private constructor(val themeId: Int, @JvmField val mStyleId: Int) {
Color.parseColor("#282828"),
Color.WHITE,
Color.parseColor("#80FFFFFF"),
keyboardBackground = backgroundImage
)
THEME_DARKER -> DefaultColors(
themeStyle,
@ -167,6 +174,7 @@ private constructor(val themeId: Int, @JvmField val mStyleId: Int) {
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,
@ -178,6 +186,7 @@ private constructor(val themeId: Int, @JvmField val mStyleId: Int) {
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,
@ -189,6 +198,7 @@ private constructor(val themeId: Int, @JvmField val mStyleId: Int) {
Color.WHITE,
Color.BLACK,
Color.BLACK,
keyboardBackground = backgroundImage
)
THEME_BROWN -> DefaultColors(
themeStyle,
@ -200,6 +210,7 @@ private constructor(val themeId: Int, @JvmField val mStyleId: Int) {
Color.WHITE,
Color.BLACK,
Color.BLACK,
keyboardBackground = backgroundImage
)
THEME_CHOCOLATE -> DefaultColors(
themeStyle,
@ -211,6 +222,7 @@ private constructor(val themeId: Int, @JvmField val mStyleId: Int) {
Color.rgb(193, 163, 146),
Color.WHITE,
Color.WHITE,
keyboardBackground = backgroundImage
)
THEME_CLOUDY -> DefaultColors(
themeStyle,
@ -222,6 +234,7 @@ private constructor(val themeId: Int, @JvmField val mStyleId: Int) {
Color.rgb(117, 128, 142),
Color.WHITE,
Color.WHITE,
keyboardBackground = backgroundImage
)
THEME_FOREST -> DefaultColors(
themeStyle,
@ -235,6 +248,7 @@ private constructor(val themeId: Int, @JvmField val mStyleId: Int) {
Color.rgb(0, 50, 0),
Color.rgb(0, 50, 0),
Color.rgb(0, 80, 0),
keyboardBackground = backgroundImage
)
THEME_INDIGO -> DefaultColors(
themeStyle,
@ -246,6 +260,7 @@ private constructor(val themeId: Int, @JvmField val mStyleId: Int) {
Color.WHITE,
Color.BLACK,
Color.BLACK,
keyboardBackground = backgroundImage
)
THEME_OCEAN -> DefaultColors(
themeStyle,
@ -257,6 +272,7 @@ private constructor(val themeId: Int, @JvmField val mStyleId: Int) {
Color.rgb(132, 157, 212),
Color.WHITE,
Color.WHITE,
keyboardBackground = backgroundImage
)
THEME_PINK -> DefaultColors(
themeStyle,
@ -268,6 +284,7 @@ private constructor(val themeId: Int, @JvmField val mStyleId: Int) {
Color.WHITE,
Color.BLACK,
Color.BLACK,
keyboardBackground = backgroundImage
)
THEME_SAND -> DefaultColors(
themeStyle,
@ -279,6 +296,7 @@ private constructor(val themeId: Int, @JvmField val mStyleId: Int) {
Color.WHITE,
Color.BLACK,
Color.BLACK,
keyboardBackground = backgroundImage
)
THEME_VIOLETTE -> DefaultColors(
themeStyle,
@ -290,6 +308,7 @@ private constructor(val themeId: Int, @JvmField val mStyleId: Int) {
Color.rgb(150, 150, 216),
Color.WHITE,
Color.WHITE,
keyboardBackground = backgroundImage
)
else /* THEME_LIGHT */ -> DefaultColors(
themeStyle,
@ -301,6 +320,7 @@ private constructor(val themeId: Int, @JvmField val mStyleId: Int) {
ContextCompat.getColor(context, R.color.key_background_normal_lxx_light_border),
ContextCompat.getColor(context, R.color.key_text_color_lxx_light),
ContextCompat.getColor(context, R.color.key_hint_letter_color_lxx_light),
keyboardBackground = backgroundImage
)
}
}

View file

@ -9,6 +9,7 @@ import android.content.res.TypedArray
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.PorterDuff
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.os.Build
@ -21,6 +22,7 @@ import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.graphics.drawable.toBitmap
import org.dslul.openboard.inputmethod.keyboard.KeyboardTheme.Companion.STYLE_HOLO
import org.dslul.openboard.inputmethod.keyboard.KeyboardTheme.Companion.STYLE_MATERIAL
import org.dslul.openboard.inputmethod.latin.common.ColorType.*
@ -60,7 +62,7 @@ interface Colors {
}
@RequiresApi(Build.VERSION_CODES.S)
class DynamicColors(context: Context, override val themeStyle: String, override val hasKeyBorders: Boolean) : Colors {
class DynamicColors(context: Context, override val themeStyle: String, override val hasKeyBorders: Boolean, private var keyboardBackground: Drawable? = null) : Colors {
private val isNight = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
@ -126,9 +128,6 @@ class DynamicColors(context: Context, override val themeStyle: String, override
private val stripBackgroundList: ColorStateList
private val toolbarKeyStateList = activatedStateList(keyText, darken(darken(keyText)))
/** custom drawable used for keyboard background */
private val keyboardBackground: Drawable?
/** darkened variant of [accent] because the accent color is always light for dynamic colors */
private val adjustedAccent: Int = darken(accent)
/** further darkened variant of [adjustedAccent] */
@ -143,17 +142,18 @@ class DynamicColors(context: Context, override val themeStyle: String, override
private val adjustedKeyBackground: Int = brighten(keyBackground)
/** further brightened variant of [adjustedKeyBackground] */
private val doubleAdjustedKeyBackground: Int = brighten(adjustedKeyBackground)
private var backgroundSetupDone = false
init {
accentColorFilter = colorFilter(doubleAdjustedAccent)
if (themeStyle == STYLE_HOLO) {
if (themeStyle == STYLE_HOLO && keyboardBackground == null) {
val darkerBackground = adjustLuminosityAndKeepAlpha(background, -0.2f)
navBar = darkerBackground
keyboardBackground = GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, intArrayOf(background, darkerBackground))
backgroundSetupDone = true
} else {
navBar = background
keyboardBackground = null
}
// todo (idea): make better use of the states?
@ -342,8 +342,15 @@ class DynamicColors(context: Context, override val themeStyle: String, override
else view.background.colorFilter = adjustedBackgroundFilter
KEYBOARD_BACKGROUND -> view.setBackgroundColor(Color.TRANSPARENT)
MAIN_BACKGROUND -> {
if (keyboardBackground != null) view.background = keyboardBackground
else view.background.colorFilter = backgroundFilter
if (keyboardBackground != null) {
if (!backgroundSetupDone) {
keyboardBackground = BitmapDrawable(view.context.resources, keyboardBackground!!.toBitmap(view.width, view.height))
backgroundSetupDone = true
}
view.background = keyboardBackground
} else {
view.background.colorFilter = backgroundFilter
}
}
else -> view.background.colorFilter = backgroundFilter
}
@ -363,6 +370,7 @@ class DefaultColors (
private val suggestionText: Int = keyText,
private val spaceBarText: Int = keyHintText,
private val gesture: Int = accent,
private var keyboardBackground: Drawable? = null,
) : Colors {
private val navBar: Int
/** brightened or darkened variant of [background], to be used if exact background color would be
@ -389,20 +397,18 @@ class DefaultColors (
private val adjustedBackgroundStateList: ColorStateList
private val stripBackgroundList: ColorStateList
private val toolbarKeyStateList = activatedStateList(suggestionText, darken(darken(suggestionText)))
/** custom drawable used for keyboard background */
private val keyboardBackground: Drawable?
private var backgroundSetupDone = false
override fun haveColorsChanged(context: Context) = false
init {
if (themeStyle == STYLE_HOLO) {
if (themeStyle == STYLE_HOLO && keyboardBackground == null) {
val darkerBackground = adjustLuminosityAndKeepAlpha(background, -0.2f)
navBar = darkerBackground
keyboardBackground = GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, intArrayOf(background, darkerBackground))
backgroundSetupDone = true
} else {
navBar = background
keyboardBackground = null
}
if (isDarkColor(background)) {
@ -513,8 +519,15 @@ class DefaultColors (
MORE_SUGGESTIONS_BACKGROUND -> view.background.colorFilter = backgroundFilter
KEYBOARD_BACKGROUND -> view.setBackgroundColor(Color.TRANSPARENT)
MAIN_BACKGROUND -> {
if (keyboardBackground != null) view.background = keyboardBackground
else view.background.colorFilter = backgroundFilter
if (keyboardBackground != null) {
if (!backgroundSetupDone) {
keyboardBackground = BitmapDrawable(view.context.resources, keyboardBackground!!.toBitmap(view.width, view.height))
backgroundSetupDone = true
}
view.background = keyboardBackground
} else {
view.background.colorFilter = backgroundFilter
}
}
else -> view.background.colorFilter = backgroundFilter
}

View file

@ -9,6 +9,7 @@ import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.os.Bundle
@ -61,8 +62,10 @@ class AdvancedSettingsFragment : SubScreenFragment() {
"userunigram.*/userunigram.*\\.(body|header)".toRegex(),
"UserHistoryDictionary.*/UserHistoryDictionary.*\\.(body|header)".toRegex(),
"spellcheck_userunigram.*/spellcheck_userunigram.*\\.(body|header)".toRegex(),
"custom_background_image.*".toRegex(),
) }
// is there any way to get additional information into the ActivityResult? would remove the need for 5 times the (almost) same code
private val libraryFilePicker = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode != Activity.RESULT_OK) return@registerForActivityResult
val uri = it.data?.data ?: return@registerForActivityResult
@ -81,6 +84,18 @@ class AdvancedSettingsFragment : SubScreenFragment() {
restore(uri)
}
private val dayImageFilePicker = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode != Activity.RESULT_OK) return@registerForActivityResult
val uri = it.data?.data ?: return@registerForActivityResult
loadImage(uri, false)
}
private val nightImageFilePicker = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode != Activity.RESULT_OK) return@registerForActivityResult
val uri = it.data?.data ?: return@registerForActivityResult
loadImage(uri, true)
}
override fun onCreate(icicle: Bundle?) {
super.onCreate(icicle)
addPreferencesFromResource(R.xml.prefs_screen_advanced)
@ -96,6 +111,7 @@ class AdvancedSettingsFragment : SubScreenFragment() {
setupKeyLongpressTimeoutSettings()
findPreference<Preference>("load_gesture_library")?.setOnPreferenceClickListener { onClickLoadLibrary() }
findPreference<Preference>("pref_backup_restore")?.setOnPreferenceClickListener { showBackupRestoreDialog() }
findPreference<Preference>("custom_background_image")?.setOnPreferenceClickListener { onClickLoadImage() }
findPreference<Preference>("custom_symbols_layout")?.setOnPreferenceClickListener {
val layoutName = Settings.readSymbolsLayoutName(context, context.resources.configuration.locale).takeIf { it.startsWith(CUSTOM_LAYOUT_PREFIX) }
@ -170,6 +186,55 @@ class AdvancedSettingsFragment : SubScreenFragment() {
}
}
private fun onClickLoadImage(): Boolean {
if (Settings.readDayNightPref(sharedPreferences, resources)) {
AlertDialog.Builder(requireContext())
.setMessage(R.string.day_or_night_image)
.setPositiveButton(R.string.day_or_night_day) { _, _ -> customImageDialog(false) }
.setNegativeButton(R.string.day_or_night_night) { _, _ -> customImageDialog(true) }
.setNeutralButton(android.R.string.cancel, null)
.show()
} else {
customImageDialog(false)
}
return true
}
private fun customImageDialog(night: Boolean) {
val imageFile = Settings.getCustomBackgroundFile(requireContext(), night)
val builder = AlertDialog.Builder(requireContext())
.setMessage(R.string.customize_background_image)
.setPositiveButton(R.string.button_load_custom) { _, _ ->
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType("image/*")
if (night) nightImageFilePicker.launch(intent)
else dayImageFilePicker.launch(intent)
}
.setNegativeButton(android.R.string.cancel, null)
if (imageFile.exists()) {
builder.setNeutralButton(R.string.delete) { _, _ ->
imageFile.delete()
Settings.clearCachedBackgroundImages()
KeyboardSwitcher.getInstance().forceUpdateKeyboardTheme(requireContext())
}
}
builder.show()
}
private fun loadImage(uri: Uri, night: Boolean) {
val imageFile = Settings.getCustomBackgroundFile(requireContext(), night)
FileUtils.copyStreamToNewFile(requireContext().contentResolver.openInputStream(uri), imageFile)
try {
BitmapFactory.decodeFile(imageFile.absolutePath)
} catch (_: Exception) {
infoDialog(requireContext(), R.string.file_read_error)
imageFile.delete()
}
Settings.clearCachedBackgroundImages()
KeyboardSwitcher.getInstance().forceUpdateKeyboardTheme(requireContext())
}
@SuppressLint("ApplySharedPref")
private fun renameToLibfileAndRestart(file: File, checksum: String) {
libfile.delete()

View file

@ -123,7 +123,7 @@ class LanguageSettingsDialog(
.setMessage(message)
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(R.string.button_copy_existing_layout) { _, _ -> copyLayout() }
.setPositiveButton(R.string.button_load_custom_layout) { _, _ -> fragment?.requestLayoutFile() }
.setPositiveButton(R.string.button_load_custom) { _, _ -> fragment?.requestLayoutFile() }
.create()
dialog.show()
(dialog.findViewById<View>(android.R.id.message) as? TextView)?.movementMethod = LinkMovementMethod.getInstance()
@ -201,7 +201,7 @@ class LanguageSettingsDialog(
reloadSetting()
}
if (isCustom) {
confirmDialog(context, context.getString(R.string.delete_layout, getLayoutDisplayName(layoutSetName!!)), context.getString(R.string.delete_dict)) { delete() }
confirmDialog(context, context.getString(R.string.delete_layout, getLayoutDisplayName(layoutSetName!!)), context.getString(R.string.delete)) { delete() }
} else {
delete()
}
@ -345,7 +345,7 @@ class LanguageSettingsDialog(
isVisible = true
setOnClickListener {
confirmDialog(context, context.getString(R.string.remove_dictionary_message, dictType), context.getString(
R.string.delete_dict)) {
R.string.delete)) {
val parent = dictFile.parentFile
dictFile.delete()
if (parent?.list()?.isEmpty() == true)

View file

@ -12,7 +12,10 @@ import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import org.dslul.openboard.inputmethod.latin.utils.CustomLayoutUtilsKt;
@ -23,6 +26,7 @@ import android.view.ContextThemeWrapper;
import android.view.Gravity;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import org.dslul.openboard.inputmethod.keyboard.KeyboardTheme;
@ -160,6 +164,10 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
private SettingsValues mSettingsValues;
private final ReentrantLock mSettingsValuesLock = new ReentrantLock();
// static cache for background images to avoid potentially slow reload on every settings reload
private static Drawable sCachedBackgroundDay;
private static Drawable sCachedBackgroundNight;
private static final Settings sInstance = new Settings();
// preferences that are not used in SettingsValues and thus should not trigger reload when changed
@ -532,6 +540,37 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
return "symbols_shifted";
}
@Nullable public static Drawable readUserBackgroundImage(final Context context, final boolean night) {
if (night && sCachedBackgroundNight != null) return sCachedBackgroundNight;
if (!night && sCachedBackgroundDay != null) return sCachedBackgroundDay;
final File image = getCustomBackgroundFile(context, night);
if (!image.isFile()) return null;
try {
if (night) {
sCachedBackgroundNight = new BitmapDrawable(context.getResources(), BitmapFactory.decodeFile(image.getAbsolutePath()));
return sCachedBackgroundNight;
} else {
sCachedBackgroundDay = new BitmapDrawable(context.getResources(), BitmapFactory.decodeFile(image.getAbsolutePath()));
return sCachedBackgroundDay;
}
} catch (Exception e) {
return null;
}
}
public static File getCustomBackgroundFile(final Context context, final boolean night) {
return new File(context.getFilesDir(), "custom_background_image" + (night ? "_night" : ""));
}
public static boolean readDayNightPref(final SharedPreferences prefs, final Resources res) {
return prefs.getBoolean(PREF_THEME_DAY_NIGHT, res.getBoolean(R.bool.day_night_default));
}
public static void clearCachedBackgroundImages() {
sCachedBackgroundDay = null;
sCachedBackgroundNight = null;
}
public static List<Locale> getSecondaryLocales(final SharedPreferences prefs, final String mainLocaleString) {
final String localesString = prefs.getString(PREF_SECONDARY_LOCALES_PREFIX + mainLocaleString.toLowerCase(Locale.ROOT), "");
@ -558,7 +597,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static Colors getColorsForCurrentTheme(final Context context, final SharedPreferences prefs) {
boolean isNight = ResourceUtils.isNight(context.getResources());
if (prefs.getBoolean(PREF_FORCE_OPPOSITE_THEME, false)) isNight = !isNight;
final String themeColors = (isNight && prefs.getBoolean(PREF_THEME_DAY_NIGHT, context.getResources().getBoolean(R.bool.day_night_default)))
final String themeColors = (isNight && readDayNightPref(prefs, context.getResources()))
? prefs.getString(Settings.PREF_THEME_COLORS_NIGHT, KeyboardTheme.THEME_DARKER)
: prefs.getString(Settings.PREF_THEME_COLORS, KeyboardTheme.THEME_LIGHT);
final String themeStyle = prefs.getString(Settings.PREF_THEME_STYLE, KeyboardTheme.STYLE_MATERIAL);

View file

@ -161,8 +161,8 @@ fun editCustomLayout(layoutName: String, context: Context, startContent: String?
if (isSymbols) {
val name = if (layoutName.contains("shift")) context.getString(R.string.shift_symbols) else context.getString(R.string.more_keys_symbols)
if (file.exists()) {
builder.setNeutralButton(R.string.delete_dict) { _, _ ->
confirmDialog(context, context.getString(R.string.delete_layout, name), context.getString(R.string.delete_dict)) {
builder.setNeutralButton(R.string.delete) { _, _ ->
confirmDialog(context, context.getString(R.string.delete_layout, name), context.getString(R.string.delete)) {
file.delete()
KeyboardSwitcher.getInstance().forceUpdateKeyboardTheme(context)
}