Allow changing toolbar icon and codes (#1110)

This commit is contained in:
Helium314 2024-09-23 20:45:23 +02:00 committed by GitHub
parent 7d1cf0dd63
commit 05523ecd30
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 433 additions and 89 deletions

View file

@ -0,0 +1,16 @@
package helium314.keyboard.latin
import android.content.Context
import android.content.SharedPreferences
import helium314.keyboard.latin.settings.Settings
import kotlinx.serialization.json.Json
fun customIconNames(prefs: SharedPreferences) = runCatching {
Json.decodeFromString<Map<String, String>>(prefs.getString(Settings.PREF_CUSTOM_ICON_NAMES, "")!!)
}.getOrElse { emptyMap() }
fun customIconIds(context: Context, prefs: SharedPreferences) = customIconNames(prefs)
.mapNotNull { entry ->
val id = runCatching { context.resources.getIdentifier(entry.value, "drawable", context.packageName) }.getOrNull()
id?.let { entry.key to it }
}

View file

@ -10,18 +10,37 @@ import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ScrollView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
import androidx.core.util.TypedValueCompat
import androidx.core.view.forEach
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.TwoStatePreference
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import helium314.keyboard.keyboard.KeyboardSwitcher
import helium314.keyboard.keyboard.KeyboardTheme
import helium314.keyboard.keyboard.internal.KeyboardIconsSet
import helium314.keyboard.latin.R
import helium314.keyboard.latin.common.FileUtils
import helium314.keyboard.latin.customIconNames
import helium314.keyboard.latin.databinding.ReorderDialogItemBinding
import helium314.keyboard.latin.utils.ResourceUtils
import helium314.keyboard.latin.utils.getStringResourceOrName
import helium314.keyboard.latin.utils.infoDialog
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.lang.Float.max
import java.lang.Float.min
import java.util.*
@ -73,6 +92,7 @@ class AppearanceSettingsFragment : SubScreenFragment() {
}
}
findPreference<Preference>("custom_background_image")?.setOnPreferenceClickListener { onClickLoadImage() }
findPreference<Preference>(Settings.PREF_CUSTOM_ICON_NAMES)?.setOnPreferenceClickListener { onClickCustomizeIcons() }
}
override fun onPause() {
@ -179,6 +199,111 @@ class AppearanceSettingsFragment : SubScreenFragment() {
userColorsPrefNight?.isVisible = dayNightPref?.isChecked == true && colorsNightPref?.value == KeyboardTheme.THEME_USER_NIGHT
}
// performance is not good, but not bad enough to justify work
private fun onClickCustomizeIcons(): Boolean {
val ctx = requireContext()
val padding = ResourceUtils.toPx(8, ctx.resources)
val ll = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setPadding(padding, 3 * padding, padding, padding)
}
val d = AlertDialog.Builder(ctx)
.setTitle(R.string.customize_icons)
.setView(ScrollView(context).apply { addView(ll) })
.setPositiveButton(R.string.dialog_close, null)
.create()
val cf = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(ContextCompat.getColor(ctx, R.color.foreground), BlendModeCompat.SRC_IN)
val icons = KeyboardIconsSet.getAllIcons(ctx)
icons.keys.forEach { iconName ->
val b = ReorderDialogItemBinding.inflate(LayoutInflater.from(ctx), ll, true)
b.reorderItemIcon.setImageDrawable(KeyboardIconsSet.instance.getNewDrawable(iconName, ctx))
b.reorderItemIcon.colorFilter = cf
b.reorderItemIcon.isVisible = true
b.reorderItemName.text = iconName.getStringResourceOrName("", ctx)
if (b.reorderItemName.text == iconName)
b.reorderItemName.text = iconName.getStringResourceOrName("label_", ctx)
b.root.setOnClickListener {
customizeIcon(iconName)
d.dismiss()
}
b.reorderItemSwitch.isGone = true
b.reorderItemDragIndicator.isGone = true
}
d.show()
return true
}
// todo: icon size is an important difference between holo and others, but really awful to work with
// scaling the intrinsic icon width may look awful depending on display density
private fun customizeIcon(iconName: String) {
val ctx = requireContext()
val rv = RecyclerView(ctx)
rv.layoutManager = GridLayoutManager(ctx, 6)
val padding = ResourceUtils.toPx(6, resources)
rv.setPadding(padding, 3 * padding, padding, padding)
val icons = KeyboardIconsSet.getAllIcons(ctx)
val iconsList = icons[iconName].orEmpty().toSet().toMutableList()
val iconsSet = icons.values.flatten().toMutableSet()
iconsSet.removeAll(iconsList)
iconsList.addAll(iconsSet)
val foregroundColor = ContextCompat.getColor(ctx, R.color.foreground)
val iconColorFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(foregroundColor, BlendModeCompat.SRC_IN)
var currentIconId = KeyboardIconsSet.instance.iconIds[iconName]
val adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val v = ImageView(ctx)
v.colorFilter = iconColorFilter
v.setPadding(padding, padding, padding, padding)
return object : RecyclerView.ViewHolder(v) { }
}
override fun getItemCount(): Int = iconsList.size
override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
val icon = ContextCompat.getDrawable(ctx, iconsList[position])?.mutate()
val imageView = viewHolder.itemView as? ImageView
imageView?.setImageDrawable(icon)
if (iconsList[position] == currentIconId) imageView?.setColorFilter(R.color.accent)
else imageView?.colorFilter = iconColorFilter
viewHolder.itemView.setOnClickListener { v ->
rv.forEach { (it as? ImageView)?.colorFilter = iconColorFilter }
(v as? ImageView)?.setColorFilter(R.color.accent)
currentIconId = iconsList[position]
}
}
}
rv.adapter = adapter
val title = iconName.getStringResourceOrName("", ctx).takeUnless { it == iconName }
?: iconName.getStringResourceOrName("label_", ctx)
val builder = AlertDialog.Builder(ctx)
.setTitle(title)
.setView(rv)
.setPositiveButton(android.R.string.ok) { _, _ ->
runCatching {
val icons2 = customIconNames(sharedPreferences).toMutableMap()
icons2[iconName] = currentIconId?.let { resources.getResourceEntryName(it) } ?: return@runCatching
sharedPreferences.edit().putString(Settings.PREF_CUSTOM_ICON_NAMES, Json.encodeToString(icons2)).apply()
KeyboardIconsSet.instance.loadIcons(ctx)
}
onClickCustomizeIcons()
}
.setNegativeButton(android.R.string.cancel) { _, _ -> onClickCustomizeIcons() }
if (customIconNames(sharedPreferences).contains(iconName))
builder.setNeutralButton(R.string.button_default) { _, _ ->
runCatching {
val icons2 = customIconNames(sharedPreferences).toMutableMap()
icons2.remove(iconName)
sharedPreferences.edit().putString(Settings.PREF_CUSTOM_ICON_NAMES, Json.encodeToString(icons2)).apply()
KeyboardIconsSet.instance.loadIcons(ctx)
}
onClickCustomizeIcons()
}
builder.show()
}
private fun onClickLoadImage(): Boolean {
if (Settings.readDayNightPref(sharedPreferences, resources)) {
AlertDialog.Builder(requireContext())

View file

@ -44,12 +44,15 @@ import helium314.keyboard.latin.utils.ResourceUtils;
import helium314.keyboard.latin.utils.RunInLocaleKt;
import helium314.keyboard.latin.utils.StatsUtils;
import helium314.keyboard.latin.utils.SubtypeSettingsKt;
import helium314.keyboard.latin.utils.ToolbarKey;
import helium314.keyboard.latin.utils.ToolbarUtilsKt;
import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
public final class Settings implements SharedPreferences.OnSharedPreferenceChangeListener {
@ -79,6 +82,9 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_COLOR_BACKGROUND_SUFFIX = "background";
public static final String PREF_AUTO_USER_COLOR_SUFFIX = "_auto";
public static final String PREF_ALL_COLORS_SUFFIX = "all_colors";
public static final String PREF_CUSTOM_ICON_NAMES = "custom_icon_names";
public static final String PREF_TOOLBAR_CUSTOM_KEY_CODES = "toolbar_custom_key_codes";
public static final String PREF_TOOLBAR_CUSTOM_LONGPRESS_CODES = "toolbar_custom_longpress_codes";
public static final String PREF_AUTO_CAP = "auto_cap";
public static final String PREF_VIBRATE_ON = "vibrate_on";
@ -187,6 +193,8 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
// static cache for background images to avoid potentially slow reload on every settings reload
private static Drawable sCachedBackgroundDay;
private static Drawable sCachedBackgroundNight;
private Map<String, Integer> mCustomToolbarKeyCodes = null;
private Map<String, Integer> mCustomToolbarLongpressCodes = null;
private static final Settings sInstance = new Settings();
@ -234,6 +242,8 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
Log.w(TAG, "onSharedPreferenceChanged called before loadSettings.");
return;
}
mCustomToolbarLongpressCodes = null;
mCustomToolbarKeyCodes = null;
loadSettings(mContext, mSettingsValues.mLocale, mSettingsValues.mInputAttributes);
StatsUtils.onLoadSettings(mSettingsValues);
} finally {
@ -706,4 +716,16 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public String readCustomCurrencyKey() {
return mPrefs.getString(PREF_CUSTOM_CURRENCY_KEY, "");
}
public Integer getCustomToolbarKeyCode(ToolbarKey key) {
if (mCustomToolbarKeyCodes == null)
mCustomToolbarKeyCodes = ToolbarUtilsKt.readCustomKeyCodes(mPrefs);
return mCustomToolbarKeyCodes.get(key.name());
}
public Integer getCustomToolbarLongpressCode(ToolbarKey key) {
if (mCustomToolbarLongpressCodes == null)
mCustomToolbarLongpressCodes = ToolbarUtilsKt.readCustomLongpressCodes(mPrefs);
return mCustomToolbarLongpressCodes.get(key.name());
}
}

View file

@ -10,6 +10,7 @@ import helium314.keyboard.latin.utils.defaultClipboardToolbarPref
import helium314.keyboard.latin.utils.defaultPinnedToolbarPref
import helium314.keyboard.latin.utils.defaultToolbarPref
import helium314.keyboard.latin.utils.reorderDialog
import helium314.keyboard.latin.utils.toolbarKeysCustomizer
class ToolbarSettingsFragment : SubScreenFragment() {
private var reloadKeyboard = false
@ -44,6 +45,11 @@ class ToolbarSettingsFragment : SubScreenFragment() {
) { iconsSet.getNewDrawable(it, requireContext()) }
true
}
findPreference<Preference>("customize_key_codes")?.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
toolbarKeysCustomizer(requireContext())
true
}
}
override fun onPause() {

View file

@ -182,7 +182,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
mMoreSuggestionsSlidingDetector = new GestureDetector(context, mMoreSuggestionsSlidingListener);
final KeyboardIconsSet iconsSet = KeyboardIconsSet.Companion.getInstance();
mIncognitoIcon = iconsSet.getNewDrawable(KeyboardIconsSet.NAME_INCOGNITO_KEY, context);
mIncognitoIcon = iconsSet.getNewDrawable(ToolbarKey.INCOGNITO.name(), context);
mToolbarArrowIcon = iconsSet.getNewDrawable(KeyboardIconsSet.NAME_TOOLBAR_KEY, context);
mBinIcon = iconsSet.getNewDrawable(KeyboardIconsSet.NAME_BIN, context);

View file

@ -1,16 +1,33 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.latin.utils
import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.content.SharedPreferences
import android.view.LayoutInflater
import android.widget.EditText
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ScrollView
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged
import helium314.keyboard.keyboard.internal.KeyboardIconsSet
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode.checkAndConvertCode
import helium314.keyboard.latin.R
import helium314.keyboard.latin.databinding.ReorderDialogItemBinding
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.utils.ToolbarKey.*
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.util.EnumMap
import java.util.Locale
@ -31,7 +48,7 @@ fun createToolbarKey(context: Context, iconsSet: KeyboardIconsSet, key: ToolbarK
return button
}
fun getCodeForToolbarKey(key: ToolbarKey) = when (key) {
fun getCodeForToolbarKey(key: ToolbarKey) = Settings.getInstance().getCustomToolbarKeyCode(key) ?: when (key) {
VOICE -> KeyCode.VOICE_INPUT
CLIPBOARD -> KeyCode.CLIPBOARD
NUMPAD -> KeyCode.NUMPAD
@ -63,7 +80,7 @@ fun getCodeForToolbarKey(key: ToolbarKey) = when (key) {
PAGE_END -> KeyCode.MOVE_END_OF_PAGE
}
fun getCodeForToolbarKeyLongClick(key: ToolbarKey) = when (key) {
fun getCodeForToolbarKeyLongClick(key: ToolbarKey) = Settings.getInstance().getCustomToolbarLongpressCode(key) ?: when (key) {
CLIPBOARD -> KeyCode.CLIPBOARD_PASTE
UNDO -> KeyCode.REDO
REDO -> KeyCode.UNDO
@ -175,3 +192,109 @@ private fun getEnabledToolbarKeys(prefs: SharedPreferences, pref: String, defaul
} else null
}
}
fun toolbarKeysCustomizer(context: Context) {
val padding = ResourceUtils.toPx(8, context.resources)
val ll = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setPadding(3 * padding, padding, padding, padding)
}
val dialog = AlertDialog.Builder(context)
.setTitle(R.string.customize_toolbar_key_codes)
.setView(ScrollView(context).apply { addView(ll) })
.setPositiveButton(R.string.dialog_close, null)
.create()
val cf = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(ContextCompat.getColor(context, R.color.foreground), BlendModeCompat.SRC_IN)
ToolbarKey.entries.forEach { key ->
val binding = ReorderDialogItemBinding.inflate(LayoutInflater.from(context), ll, true)
binding.reorderItemIcon.setImageDrawable(KeyboardIconsSet.instance.getNewDrawable(key.name, context))
binding.reorderItemIcon.colorFilter = cf
binding.reorderItemIcon.isVisible = true
binding.reorderItemName.text = key.name.lowercase().getStringResourceOrName("", context)
binding.root.setOnClickListener {
toolbarKeyCustomizer(context, key)
dialog.dismiss()
}
binding.reorderItemSwitch.isGone = true
binding.reorderItemDragIndicator.isGone = true
}
dialog.show()
}
@SuppressLint("SetTextI18n")
private fun toolbarKeyCustomizer(context: Context, key: ToolbarKey) {
val layout = LayoutInflater.from(context).inflate(R.layout.toolbar_key_customizer, null)
val prefs = DeviceProtectedUtils.getSharedPreferences(context)
var keyCode: String? = null
var longpressCode: String? = null
val builder = AlertDialog.Builder(context)
.setTitle(key.name.lowercase().getStringResourceOrName("", context))
.setView(ScrollView(context).apply { addView(layout) })
.setPositiveButton(android.R.string.ok) { _, _ ->
val newKeyCode = runCatching { keyCode?.toIntOrNull()?.checkAndConvertCode() }.getOrNull()?.takeIf { it < Char.MAX_VALUE.code }
val newLongpressCode = runCatching { longpressCode?.toIntOrNull()?.checkAndConvertCode() }.getOrNull()?.takeIf { it < Char.MAX_VALUE.code }
if (newKeyCode != null)
writeCustomKeyCodes(prefs, readCustomKeyCodes(prefs) + (key.name to newKeyCode))
if (newLongpressCode != null)
writeCustomLongpressCodes(prefs, readCustomLongpressCodes(prefs) + (key.name to newLongpressCode))
toolbarKeysCustomizer(context)
}
.setNegativeButton(android.R.string.cancel) { _, _ -> toolbarKeysCustomizer(context) }
if (readCustomKeyCodes(prefs).containsKey(key.name) || readCustomLongpressCodes(prefs).containsKey(key.name))
builder.setNeutralButton(R.string.button_default) { _, _ ->
val keys = readCustomKeyCodes(prefs).toMutableMap()
keys.remove(key.name)
prefs.edit().putString(Settings.PREF_TOOLBAR_CUSTOM_KEY_CODES, Json.encodeToString(keys)).apply()
val longpressKeys = readCustomLongpressCodes(prefs).toMutableMap()
longpressKeys.remove(key.name)
prefs.edit().putString(Settings.PREF_TOOLBAR_CUSTOM_LONGPRESS_CODES, Json.encodeToString(longpressKeys)).apply()
toolbarKeysCustomizer(context)
}
val dialog = builder.create()
fun checkOk() {
val keyOk = keyCode == null
|| runCatching { keyCode?.toIntOrNull()?.let { it.checkAndConvertCode() <= Char.MAX_VALUE.code } }.getOrNull() ?: false
val longPressOk = longpressCode == null
|| runCatching { longpressCode?.toIntOrNull()?.let { it.checkAndConvertCode() <= Char.MAX_VALUE.code } }.getOrNull() ?: false
dialog.getButton(DialogInterface.BUTTON_POSITIVE)?.isEnabled = keyOk && longPressOk
}
layout.findViewById<EditText>(R.id.toolbar_key_code)?.apply {
setText(getCodeForToolbarKey(key).toString())
doAfterTextChanged {
keyCode = it?.toString()
checkOk()
}
}
layout.findViewById<EditText>(R.id.toolbar_key_longpress_code)?.apply {
setText(getCodeForToolbarKeyLongClick(key).toString())
doAfterTextChanged {
longpressCode = it?.toString()
checkOk()
}
}
dialog.show()
}
fun readCustomKeyCodes(prefs: SharedPreferences) = prefs.getString(Settings.PREF_TOOLBAR_CUSTOM_KEY_CODES, "")!!
.split(";").associate {
val code = runCatching { it.substringAfter(",").toIntOrNull()?.checkAndConvertCode() }.getOrNull()
it.substringBefore(",") to code
}
fun readCustomLongpressCodes(prefs: SharedPreferences) = prefs.getString(Settings.PREF_TOOLBAR_CUSTOM_LONGPRESS_CODES, "")!!
.split(";").associate {
val code = runCatching { it.substringAfter(",").toIntOrNull()?.checkAndConvertCode() }.getOrNull()
it.substringBefore(",") to code
}
private fun writeCustomKeyCodes(prefs: SharedPreferences, codes: Map<String, Int?>) {
val string = codes.mapNotNull { entry -> entry.value?.let { "${entry.key},$it" } }.joinToString(";")
prefs.edit().putString(Settings.PREF_TOOLBAR_CUSTOM_KEY_CODES, string).apply()
}
private fun writeCustomLongpressCodes(prefs: SharedPreferences, codes: Map<String, Int?>) {
val string = codes.mapNotNull { entry -> entry.value?.let { "${entry.key},$it" } }.joinToString(";")
prefs.edit().putString(Settings.PREF_TOOLBAR_CUSTOM_LONGPRESS_CODES, string).apply()
}