diff --git a/README.md b/README.md index 3d7fdb014..bbb7d3a90 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Does not use internet permission, and thus is 100% offline.
  • Backup and restore your settings and learned word / history data
  • -For more information about the app and features, please visit the [wiki](https://github.com/Helium314/HeliBoard/wiki) +For FAQ and more information about the app and features, please visit the [wiki](https://github.com/Helium314/HeliBoard/wiki) # Contributing ❤ diff --git a/app/src/main/java/helium314/keyboard/keyboard/Key.java b/app/src/main/java/helium314/keyboard/keyboard/Key.java index da9144b61..f27d8c3c2 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/Key.java +++ b/app/src/main/java/helium314/keyboard/keyboard/Key.java @@ -938,11 +938,12 @@ public final boolean isAccentColored() { final String iconName = getIconName(); if (iconName == null) return false; // todo: other way of identifying the color? - // if yes, NAME_CLIPBOARD_ACTION_KEY and NAME_CLIPBOARD_NORMAL_KEY could be merged + // this should be done differently, as users can set any icon now + // how is the background drawable selected? can we use the same way? return iconName.equals(KeyboardIconsSet.NAME_NEXT_KEY) || iconName.equals(KeyboardIconsSet.NAME_PREVIOUS_KEY) - || iconName.equals(KeyboardIconsSet.NAME_CLIPBOARD_ACTION_KEY) - || iconName.equals(KeyboardIconsSet.NAME_EMOJI_ACTION_KEY); + || iconName.equals("clipboard_action_key") + || iconName.equals("emoji_action_key"); } public boolean hasFunctionalBackground() { diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/KeyboardIconsSet.kt b/app/src/main/java/helium314/keyboard/keyboard/internal/KeyboardIconsSet.kt index dd4852675..c688de383 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/internal/KeyboardIconsSet.kt +++ b/app/src/main/java/helium314/keyboard/keyboard/internal/KeyboardIconsSet.kt @@ -6,6 +6,7 @@ import android.graphics.drawable.Drawable import androidx.core.content.ContextCompat import helium314.keyboard.keyboard.KeyboardTheme import helium314.keyboard.latin.R +import helium314.keyboard.latin.customIconIds import helium314.keyboard.latin.settings.Settings import helium314.keyboard.latin.utils.DeviceProtectedUtils import helium314.keyboard.latin.utils.Log @@ -13,17 +14,20 @@ import helium314.keyboard.latin.utils.ToolbarKey import java.util.Locale class KeyboardIconsSet private constructor() { - private var iconIds = emptyMap() + var iconIds = emptyMap() + private set private val iconsByName = HashMap(80) fun loadIcons(context: Context) { val prefs = DeviceProtectedUtils.getSharedPreferences(context) val iconStyle = prefs.getString(Settings.PREF_ICON_STYLE, KeyboardTheme.STYLE_MATERIAL) - val ids = when (iconStyle) { + val defaultIds = when (iconStyle) { KeyboardTheme.STYLE_HOLO -> keyboardIconsHolo KeyboardTheme.STYLE_ROUNDED -> keyboardIconsRounded else -> keyboardIconsMaterial } + val overrideIds = customIconIds(context, prefs) + val ids = if (overrideIds.isEmpty()) defaultIds else defaultIds + overrideIds if (ids == iconIds) return iconIds = ids iconsByName.clear() @@ -38,13 +42,15 @@ class KeyboardIconsSet private constructor() { } } - fun getIconDrawable(name: String?): Drawable? = iconsByName[name?.lowercase(Locale.US)] + fun getIconDrawable(name: String?): Drawable? = name?.lowercase(Locale.US)?.let { + iconsByName[it] ?: iconsByName[alternativeNames[it]] + } + /** gets drawable from resources, with mutate (might be necessary to avoid coloring issues...) */ - fun getNewDrawable(name: String?, context: Context): Drawable? = - iconIds[name?.lowercase(Locale.US)]?.let { ContextCompat.getDrawable(context, it)?.mutate() } + fun getNewDrawable(name: String?, context: Context): Drawable? = name?.lowercase(Locale.US)?.let { name -> + (iconIds[name] ?: iconIds[alternativeNames[name]])?.let { ContextCompat.getDrawable(context, it)?.mutate() } + } - // sometimes there are 2 names for the same icon for historic reasons, - // and removing needs to be handled with care to not break custom themes companion object { private val TAG = KeyboardIconsSet::class.simpleName const val PREFIX_ICON = "!icon/" @@ -53,7 +59,6 @@ class KeyboardIconsSet private constructor() { const val NAME_SHIFT_KEY_SHIFTED = "shift_key_shifted" const val NAME_SHIFT_KEY_LOCKED = "shift_key_locked" const val NAME_DELETE_KEY = "delete_key" - const val NAME_SETTINGS_KEY = "settings_key" const val NAME_SPACE_KEY = "space_key" const val NAME_SPACE_KEY_FOR_NUMBER_LAYOUT = "space_key_for_number_layout" const val NAME_ENTER_KEY = "enter_key" @@ -64,64 +69,37 @@ class KeyboardIconsSet private constructor() { const val NAME_DONE_KEY = "done_key" const val NAME_PREVIOUS_KEY = "previous_key" const val NAME_TAB_KEY = "tab_key" - const val NAME_SHORTCUT_KEY = "shortcut_key" - const val NAME_INCOGNITO_KEY = "incognito_key" const val NAME_SHORTCUT_KEY_DISABLED = "shortcut_key_disabled" const val NAME_LANGUAGE_SWITCH_KEY = "language_switch_key" const val NAME_ZWNJ_KEY = "zwnj_key" const val NAME_ZWJ_KEY = "zwj_key" - const val NAME_EMOJI_ACTION_KEY = "emoji_action_key" - const val NAME_EMOJI_NORMAL_KEY = "emoji_normal_key" - const val NAME_CLIPBOARD_ACTION_KEY = "clipboard_action_key" - const val NAME_CLIPBOARD_NORMAL_KEY = "clipboard_normal_key" - const val NAME_CLEAR_CLIPBOARD_KEY = "clear_clipboard_key" - const val NAME_CUT_KEY = "cut_key" - const val NAME_START_ONEHANDED_KEY = "start_onehanded_mode_key" const val NAME_STOP_ONEHANDED_KEY = "stop_onehanded_mode_key" const val NAME_SWITCH_ONEHANDED_KEY = "switch_onehanded_key" const val NAME_RESIZE_ONEHANDED_KEY = "resize_onehanded_key" const val NAME_TOOLBAR_KEY = "toolbar_key" const val NAME_BIN = "bin" -/* - private val styleableIdByName = hashMapOf( - NAME_SHIFT_KEY to R.styleable.Keyboard_iconShiftKey, - NAME_DELETE_KEY to R.styleable.Keyboard_iconDeleteKey, - NAME_SETTINGS_KEY to R.styleable.Keyboard_iconSettingsKey, - NAME_SPACE_KEY to R.styleable.Keyboard_iconSpaceKey, - NAME_ENTER_KEY to R.styleable.Keyboard_iconEnterKey, - NAME_GO_KEY to R.styleable.Keyboard_iconGoKey, - NAME_SEARCH_KEY to R.styleable.Keyboard_iconSearchKey, - NAME_SEND_KEY to R.styleable.Keyboard_iconSendKey, - NAME_NEXT_KEY to R.styleable.Keyboard_iconNextKey, - NAME_DONE_KEY to R.styleable.Keyboard_iconDoneKey, - NAME_PREVIOUS_KEY to R.styleable.Keyboard_iconPreviousKey, - NAME_TAB_KEY to R.styleable.Keyboard_iconTabKey, - NAME_SHORTCUT_KEY to R.styleable.Keyboard_iconShortcutKey, - NAME_INCOGNITO_KEY to R.styleable.Keyboard_iconIncognitoKey, - NAME_SPACE_KEY_FOR_NUMBER_LAYOUT to R.styleable.Keyboard_iconSpaceKeyForNumberLayout, - NAME_SHIFT_KEY_SHIFTED to R.styleable.Keyboard_iconShiftKeyShifted, - NAME_SHIFT_KEY_LOCKED to R.styleable.Keyboard_iconShiftKeyLocked, - NAME_SHORTCUT_KEY_DISABLED to R.styleable.Keyboard_iconShortcutKeyDisabled, - NAME_LANGUAGE_SWITCH_KEY to R.styleable.Keyboard_iconLanguageSwitchKey, - NAME_ZWNJ_KEY to R.styleable.Keyboard_iconZwnjKey, - NAME_ZWJ_KEY to R.styleable.Keyboard_iconZwjKey, - NAME_EMOJI_ACTION_KEY to R.styleable.Keyboard_iconEmojiActionKey, - NAME_EMOJI_NORMAL_KEY to R.styleable.Keyboard_iconEmojiNormalKey, - NAME_CLIPBOARD_ACTION_KEY to R.styleable.Keyboard_iconClipboardActionKey, - NAME_CLIPBOARD_NORMAL_KEY to R.styleable.Keyboard_iconClipboardNormalKey, - NAME_CLEAR_CLIPBOARD_KEY to R.styleable.Keyboard_iconClearClipboardKey, - NAME_CUT_KEY to R.styleable.Keyboard_iconCutKey, - NAME_START_ONEHANDED_KEY to R.styleable.Keyboard_iconStartOneHandedMode, - NAME_STOP_ONEHANDED_KEY to R.styleable.Keyboard_iconStopOneHandedMode, - NAME_SWITCH_ONEHANDED_KEY to R.styleable.Keyboard_iconSwitchOneHandedMode, - ).apply { ToolbarKey.entries.forEach { put(it.name.lowercase(Locale.US), getStyleableIconId(it)) } } -*/ + + // names used in the past, and we can't just delete them because they might still be in use in some layouts + // (also some of them are in use for internal layouts, but there we could just remove them...) + private val alternativeNames = hashMapOf( + "clear_clipboard_key" to ToolbarKey.CLEAR_CLIPBOARD.name.lowercase(Locale.US), + "shortcut_key" to ToolbarKey.VOICE.name.lowercase(Locale.US), + "emoji_action_key" to ToolbarKey.EMOJI.name.lowercase(Locale.US), + "emoji_normal_key" to ToolbarKey.EMOJI.name.lowercase(Locale.US), + "clipboard_action_key" to ToolbarKey.CLIPBOARD.name.lowercase(Locale.US), + "clipboard_normal_key" to ToolbarKey.CLIPBOARD.name.lowercase(Locale.US), + "cut_key" to ToolbarKey.CUT.name.lowercase(Locale.US), + "incognito_key" to ToolbarKey.INCOGNITO.name.lowercase(Locale.US), + "settings_key" to ToolbarKey.SETTINGS.name.lowercase(Locale.US), + "start_onehanded_mode_key" to ToolbarKey.ONE_HANDED.name.lowercase(Locale.US), + ) + + // todo: incognito and force incognito should not be the same? or not the same as toolbar key? private val keyboardIconsHolo by lazy { hashMapOf( NAME_SHIFT_KEY to R.drawable.sym_keyboard_shift_holo, NAME_SHIFT_KEY_SHIFTED to R.drawable.sym_keyboard_shifted_holo, NAME_SHIFT_KEY_LOCKED to R.drawable.sym_keyboard_shift_lock_holo, NAME_DELETE_KEY to R.drawable.sym_keyboard_delete_holo, - NAME_SETTINGS_KEY to R.drawable.sym_keyboard_settings_holo, // NAME_SPACE_KEY to null, NAME_ENTER_KEY to R.drawable.sym_keyboard_return_holo, // NAME_GO_KEY to null, @@ -131,20 +109,11 @@ class KeyboardIconsSet private constructor() { // NAME_NEXT_KEY to null, // NAME_PREVIOUS_KEY to null, NAME_TAB_KEY to R.drawable.sym_keyboard_tab_holo, - NAME_INCOGNITO_KEY to R.drawable.sym_keyboard_incognito_holo, NAME_SPACE_KEY_FOR_NUMBER_LAYOUT to R.drawable.sym_keyboard_space_holo, - NAME_SHORTCUT_KEY to R.drawable.sym_keyboard_voice_holo, NAME_SHORTCUT_KEY_DISABLED to R.drawable.sym_keyboard_voice_off_holo, NAME_LANGUAGE_SWITCH_KEY to R.drawable.sym_keyboard_language_switch, NAME_ZWNJ_KEY to R.drawable.sym_keyboard_zwnj_holo, NAME_ZWJ_KEY to R.drawable.sym_keyboard_zwj_holo, - NAME_EMOJI_ACTION_KEY to R.drawable.sym_keyboard_smiley_holo, - NAME_EMOJI_NORMAL_KEY to R.drawable.sym_keyboard_smiley_holo, - NAME_CLIPBOARD_ACTION_KEY to R.drawable.sym_keyboard_clipboard_holo, - NAME_CLIPBOARD_NORMAL_KEY to R.drawable.sym_keyboard_clipboard_holo, - NAME_CLEAR_CLIPBOARD_KEY to R.drawable.sym_keyboard_clear_clipboard_holo, - NAME_CUT_KEY to R.drawable.sym_keyboard_cut, - NAME_START_ONEHANDED_KEY to R.drawable.sym_keyboard_start_onehanded_holo, NAME_STOP_ONEHANDED_KEY to R.drawable.sym_keyboard_stop_onehanded_holo, NAME_SWITCH_ONEHANDED_KEY to R.drawable.ic_arrow_left, NAME_RESIZE_ONEHANDED_KEY to R.drawable.ic_arrow_horizontal, @@ -191,7 +160,6 @@ class KeyboardIconsSet private constructor() { NAME_SHIFT_KEY_SHIFTED to R.drawable.sym_keyboard_shift_lxx, NAME_SHIFT_KEY_LOCKED to R.drawable.sym_keyboard_shift_lock_lxx, NAME_DELETE_KEY to R.drawable.sym_keyboard_delete_lxx, - NAME_SETTINGS_KEY to R.drawable.sym_keyboard_settings_lxx, // NAME_SPACE_KEY to null, NAME_ENTER_KEY to R.drawable.sym_keyboard_return_lxx, NAME_GO_KEY to R.drawable.sym_keyboard_go_lxx, @@ -201,20 +169,11 @@ class KeyboardIconsSet private constructor() { NAME_NEXT_KEY to R.drawable.ic_arrow_right, NAME_PREVIOUS_KEY to R.drawable.ic_arrow_left, NAME_TAB_KEY to R.drawable.sym_keyboard_tab_lxx, - NAME_INCOGNITO_KEY to R.drawable.sym_keyboard_incognito_lxx, NAME_SPACE_KEY_FOR_NUMBER_LAYOUT to R.drawable.sym_keyboard_space_lxx, - NAME_SHORTCUT_KEY to R.drawable.sym_keyboard_voice_lxx, NAME_SHORTCUT_KEY_DISABLED to R.drawable.sym_keyboard_voice_off_lxx, NAME_LANGUAGE_SWITCH_KEY to R.drawable.sym_keyboard_language_switch_lxx, NAME_ZWNJ_KEY to R.drawable.sym_keyboard_zwnj_lxx, NAME_ZWJ_KEY to R.drawable.sym_keyboard_zwj_lxx, - NAME_EMOJI_ACTION_KEY to R.drawable.sym_keyboard_smiley_lxx, - NAME_EMOJI_NORMAL_KEY to R.drawable.sym_keyboard_smiley_lxx, - NAME_CLIPBOARD_ACTION_KEY to R.drawable.sym_keyboard_clipboard_lxx, - NAME_CLIPBOARD_NORMAL_KEY to R.drawable.sym_keyboard_clipboard_lxx, - NAME_CLEAR_CLIPBOARD_KEY to R.drawable.sym_keyboard_clear_clipboard_lxx, - NAME_CUT_KEY to R.drawable.sym_keyboard_cut, - NAME_START_ONEHANDED_KEY to R.drawable.sym_keyboard_start_onehanded_lxx, NAME_STOP_ONEHANDED_KEY to R.drawable.sym_keyboard_stop_onehanded_lxx, NAME_SWITCH_ONEHANDED_KEY to R.drawable.ic_arrow_left, NAME_RESIZE_ONEHANDED_KEY to R.drawable.ic_arrow_horizontal, @@ -261,7 +220,6 @@ class KeyboardIconsSet private constructor() { NAME_SHIFT_KEY_SHIFTED to R.drawable.sym_keyboard_shift_rounded, NAME_SHIFT_KEY_LOCKED to R.drawable.sym_keyboard_shift_lock_rounded, NAME_DELETE_KEY to R.drawable.sym_keyboard_delete_rounded, - NAME_SETTINGS_KEY to R.drawable.sym_keyboard_settings_rounded, // NAME_SPACE_KEY to null, NAME_ENTER_KEY to R.drawable.sym_keyboard_return_rounded, NAME_GO_KEY to R.drawable.sym_keyboard_go_rounded, @@ -271,20 +229,11 @@ class KeyboardIconsSet private constructor() { NAME_NEXT_KEY to R.drawable.ic_arrow_right_rounded, NAME_PREVIOUS_KEY to R.drawable.ic_arrow_left_rounded, NAME_TAB_KEY to R.drawable.sym_keyboard_tab_rounded, - NAME_INCOGNITO_KEY to R.drawable.sym_keyboard_incognito_lxx, NAME_SPACE_KEY_FOR_NUMBER_LAYOUT to R.drawable.sym_keyboard_space_rounded, - NAME_SHORTCUT_KEY to R.drawable.sym_keyboard_voice_rounded, NAME_SHORTCUT_KEY_DISABLED to R.drawable.sym_keyboard_voice_off_rounded, NAME_LANGUAGE_SWITCH_KEY to R.drawable.sym_keyboard_language_switch_lxx, NAME_ZWNJ_KEY to R.drawable.sym_keyboard_zwnj_lxx, NAME_ZWJ_KEY to R.drawable.sym_keyboard_zwj_lxx, - NAME_EMOJI_ACTION_KEY to R.drawable.sym_keyboard_smiley_rounded, - NAME_EMOJI_NORMAL_KEY to R.drawable.sym_keyboard_smiley_rounded, - NAME_CLIPBOARD_ACTION_KEY to R.drawable.sym_keyboard_clipboard_rounded, - NAME_CLIPBOARD_NORMAL_KEY to R.drawable.sym_keyboard_clipboard_rounded, - NAME_CLEAR_CLIPBOARD_KEY to R.drawable.sym_keyboard_clear_clipboard_rounded, - NAME_CUT_KEY to R.drawable.sym_keyboard_cut_rounded, - NAME_START_ONEHANDED_KEY to R.drawable.sym_keyboard_start_onehanded_rounded, NAME_STOP_ONEHANDED_KEY to R.drawable.sym_keyboard_stop_onehanded_rounded, NAME_SWITCH_ONEHANDED_KEY to R.drawable.ic_arrow_left_rounded, NAME_RESIZE_ONEHANDED_KEY to R.drawable.ic_arrow_horizontal_rounded, @@ -326,6 +275,19 @@ class KeyboardIconsSet private constructor() { } } } + fun getAllIcons(context: Context): Map> { + // currently active style first + val prefs = DeviceProtectedUtils.getSharedPreferences(context) + val iconStyle = prefs.getString(Settings.PREF_ICON_STYLE, KeyboardTheme.STYLE_MATERIAL) + return keyboardIconsMaterial.entries.associate { (name, id) -> + name to when (iconStyle) { + KeyboardTheme.STYLE_HOLO -> listOfNotNull(keyboardIconsHolo[name], keyboardIconsRounded[name], id) + KeyboardTheme.STYLE_ROUNDED -> listOfNotNull(keyboardIconsRounded[name], id, keyboardIconsHolo[name]) + else -> listOfNotNull(id, keyboardIconsRounded[name], keyboardIconsHolo[name]) + } + } + } + val instance = KeyboardIconsSet() } } diff --git a/app/src/main/java/helium314/keyboard/latin/Settings.kt b/app/src/main/java/helium314/keyboard/latin/Settings.kt new file mode 100644 index 000000000..4d5ea2977 --- /dev/null +++ b/app/src/main/java/helium314/keyboard/latin/Settings.kt @@ -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>(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 } + } diff --git a/app/src/main/java/helium314/keyboard/latin/settings/AppearanceSettingsFragment.kt b/app/src/main/java/helium314/keyboard/latin/settings/AppearanceSettingsFragment.kt index 5bf2525e2..f2dda95d1 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/AppearanceSettingsFragment.kt +++ b/app/src/main/java/helium314/keyboard/latin/settings/AppearanceSettingsFragment.kt @@ -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("custom_background_image")?.setOnPreferenceClickListener { onClickLoadImage() } + findPreference(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() { + 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()) diff --git a/app/src/main/java/helium314/keyboard/latin/settings/Settings.java b/app/src/main/java/helium314/keyboard/latin/settings/Settings.java index df1435353..25ffa31e7 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Settings.java +++ b/app/src/main/java/helium314/keyboard/latin/settings/Settings.java @@ -44,12 +44,15 @@ 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 mCustomToolbarKeyCodes = null; + private Map mCustomToolbarLongpressCodes = null; private static final Settings sInstance = new Settings(); @@ -234,6 +242,8 @@ public void onSharedPreferenceChanged(final SharedPreferences prefs, final Strin 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 String getInLocale(@StringRes final int resId, final Locale locale) { 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()); + } } diff --git a/app/src/main/java/helium314/keyboard/latin/settings/ToolbarSettingsFragment.kt b/app/src/main/java/helium314/keyboard/latin/settings/ToolbarSettingsFragment.kt index e7519b1ce..2b8a49d2b 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/ToolbarSettingsFragment.kt +++ b/app/src/main/java/helium314/keyboard/latin/settings/ToolbarSettingsFragment.kt @@ -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("customize_key_codes")?.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + toolbarKeysCustomizer(requireContext()) + true + } } override fun onPause() { diff --git a/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripView.java b/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripView.java index e105eade6..679865578 100644 --- a/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripView.java +++ b/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripView.java @@ -182,7 +182,7 @@ public SuggestionStripView(final Context context, final AttributeSet attrs, fina 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); diff --git a/app/src/main/java/helium314/keyboard/latin/utils/ToolbarUtils.kt b/app/src/main/java/helium314/keyboard/latin/utils/ToolbarUtils.kt index c1e431009..5420a7b8c 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/ToolbarUtils.kt +++ b/app/src/main/java/helium314/keyboard/latin/utils/ToolbarUtils.kt @@ -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(R.id.toolbar_key_code)?.apply { + setText(getCodeForToolbarKey(key).toString()) + doAfterTextChanged { + keyCode = it?.toString() + checkOk() + } + } + layout.findViewById(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) { + 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) { + val string = codes.mapNotNull { entry -> entry.value?.let { "${entry.key},$it" } }.joinToString(";") + prefs.edit().putString(Settings.PREF_TOOLBAR_CUSTOM_LONGPRESS_CODES, string).apply() +} diff --git a/app/src/main/res/layout/reorder_dialog_item.xml b/app/src/main/res/layout/reorder_dialog_item.xml index 557689e5d..8aad852e9 100644 --- a/app/src/main/res/layout/reorder_dialog_item.xml +++ b/app/src/main/res/layout/reorder_dialog_item.xml @@ -31,6 +31,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" /> + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f3dbc1e0d..5196b8306 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -881,6 +881,36 @@ New dictionary: Pause Wait + + Enter + + Tab + + Delete + + Shift + + Shift (shifted) + + Caps lock + + Space (number layout) + + End one-handed mode + + Resize one-handed mode + + Switch one-handed mode side + + Voice input disabled + + Show / hide toolbar + + @string/show_language_switch_key + + Zero-width joiner + + Zero-width non-joiner Horizontal spacebar swipe gesture @@ -895,6 +925,12 @@ New dictionary: Variable toolbar direction Reverse direction when a right-to-left keyboard subtype is selected + + Customize toolbar key codes + + Key code + + Long press code Auto show toolbar @@ -905,4 +941,6 @@ New dictionary: Hide the toolbar when suggestions become available Content copied + + Customize icons diff --git a/app/src/main/res/xml/prefs_screen_appearance.xml b/app/src/main/res/xml/prefs_screen_appearance.xml index 603132cbc..a4e519448 100644 --- a/app/src/main/res/xml/prefs_screen_appearance.xml +++ b/app/src/main/res/xml/prefs_screen_appearance.xml @@ -25,6 +25,10 @@ android:defaultValue="Material" latin:singleLineTitle="false" /> + + + +