offer opening dictionary repository if no local dictionary is found when enabling a subtype

This commit is contained in:
Helium314 2023-08-30 11:45:30 +02:00
parent b3764239b8
commit 3758cfe403
6 changed files with 118 additions and 53 deletions

View file

@ -134,6 +134,8 @@ class LanguageAdapter(list: List<MutableList<SubtypeInfo>> = listOf(), context:
setOnCheckedChangeListener { _, b -> setOnCheckedChangeListener { _, b ->
if (b) { if (b) {
if (infos.size == 1) { if (infos.size == 1) {
if (!infos.first().hasDictionary)
showMissingDictionaryDialog(context, infos.first().subtype.locale.toLocale())
addEnabledSubtype(prefs, infos.first().subtype) addEnabledSubtype(prefs, infos.first().subtype)
infos.single().isEnabled = true infos.single().isEnabled = true
} else { } else {

View file

@ -12,6 +12,7 @@ import android.view.View
import android.widget.* import android.widget.*
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.size
import org.dslul.openboard.inputmethod.dictionarypack.DictionaryPackConstants import org.dslul.openboard.inputmethod.dictionarypack.DictionaryPackConstants
import org.dslul.openboard.inputmethod.latin.BinaryDictionaryGetter import org.dslul.openboard.inputmethod.latin.BinaryDictionaryGetter
import org.dslul.openboard.inputmethod.latin.R import org.dslul.openboard.inputmethod.latin.R
@ -19,12 +20,11 @@ import org.dslul.openboard.inputmethod.latin.common.LocaleUtils
import org.dslul.openboard.inputmethod.latin.utils.* import org.dslul.openboard.inputmethod.latin.utils.*
import java.io.File import java.io.File
import java.util.* import java.util.*
import kotlin.collections.HashSet
@Suppress("deprecation") @Suppress("deprecation")
class LanguageSettingsDialog( class LanguageSettingsDialog(
context: Context, context: Context,
private val subtypes: MutableList<SubtypeInfo>, private val infos: MutableList<SubtypeInfo>,
private val fragment: LanguageSettingsFragment?, private val fragment: LanguageSettingsFragment?,
private val onlySystemLocales: Boolean, private val onlySystemLocales: Boolean,
private val onSubtypesChanged: () -> Unit private val onSubtypesChanged: () -> Unit
@ -32,11 +32,11 @@ class LanguageSettingsDialog(
private val context = ContextThemeWrapper(context, R.style.platformDialogTheme) private val context = ContextThemeWrapper(context, R.style.platformDialogTheme)
private val prefs = DeviceProtectedUtils.getSharedPreferences(context)!! private val prefs = DeviceProtectedUtils.getSharedPreferences(context)!!
private val view = LayoutInflater.from(context).inflate(R.layout.locale_settings_dialog, null) private val view = LayoutInflater.from(context).inflate(R.layout.locale_settings_dialog, null)
private val mainLocaleString = subtypes.first().subtype.locale private val mainLocaleString = infos.first().subtype.locale
private val mainLocale = mainLocaleString.toLocale() private val mainLocale = mainLocaleString.toLocale()
init { init {
setTitle(subtypes.first().displayName) setTitle(infos.first().displayName)
setView(ScrollView(context).apply { addView(view) }) setView(ScrollView(context).apply { addView(view) })
setButton(BUTTON_NEGATIVE, context.getString(R.string.dialog_close)) { _, _ -> setButton(BUTTON_NEGATIVE, context.getString(R.string.dialog_close)) { _, _ ->
dismiss() dismiss()
@ -61,21 +61,21 @@ class LanguageSettingsDialog(
} }
private fun fillSubtypesView(subtypesView: LinearLayout) { private fun fillSubtypesView(subtypesView: LinearLayout) {
if (subtypes.any { it.subtype.isAsciiCapable }) { // currently can only add subtypes for latin keyboards if (infos.any { it.subtype.isAsciiCapable }) { // currently can only add subtypes for latin keyboards
subtypesView.findViewById<ImageView>(R.id.add_subtype).setOnClickListener { subtypesView.findViewById<ImageView>(R.id.add_subtype).setOnClickListener {
val layouts = context.resources.getStringArray(R.array.predefined_layouts) val layouts = context.resources.getStringArray(R.array.predefined_layouts)
.filterNot { layoutName -> subtypes.any { SubtypeLocaleUtils.getKeyboardLayoutSetName(it.subtype) == layoutName } } .filterNot { layoutName -> infos.any { SubtypeLocaleUtils.getKeyboardLayoutSetName(it.subtype) == layoutName } }
val displayNames = layouts.map { SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(it) } val displayNames = layouts.map { SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(it) }
Builder(context) Builder(context)
.setTitle(R.string.keyboard_layout_set) .setTitle(R.string.keyboard_layout_set)
.setItems(displayNames.toTypedArray()) { di, i -> .setItems(displayNames.toTypedArray()) { di, i ->
di.dismiss() di.dismiss()
val newSubtype = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(mainLocaleString, layouts[i]) val newSubtype = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(mainLocaleString, layouts[i])
val newSubtypeInfo = newSubtype.toSubtypeInfo(mainLocale, context, true) // enabled by default, because why else add them val newSubtypeInfo = newSubtype.toSubtypeInfo(mainLocale, context, true, infos.first().hasDictionary) // enabled by default, because why else add them
addAdditionalSubtype(prefs, context.resources, newSubtype) addAdditionalSubtype(prefs, context.resources, newSubtype)
addEnabledSubtype(prefs, newSubtype) addEnabledSubtype(prefs, newSubtype)
addSubtypeToView(newSubtypeInfo, subtypesView) addSubtypeToView(newSubtypeInfo, subtypesView)
subtypes.add(newSubtypeInfo) infos.add(newSubtypeInfo)
onSubtypesChanged() onSubtypesChanged()
} }
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
@ -85,7 +85,7 @@ class LanguageSettingsDialog(
subtypesView.findViewById<View>(R.id.add_subtype).isGone = true subtypesView.findViewById<View>(R.id.add_subtype).isGone = true
// add subtypes // add subtypes
subtypes.sortedBy { it.displayName }.forEach { infos.sortedBy { it.displayName }.forEach {
addSubtypeToView(it, subtypesView) addSubtypeToView(it, subtypesView)
} }
} }
@ -100,8 +100,11 @@ class LanguageSettingsDialog(
isChecked = subtype.isEnabled isChecked = subtype.isEnabled
isEnabled = !onlySystemLocales isEnabled = !onlySystemLocales
setOnCheckedChangeListener { _, b -> setOnCheckedChangeListener { _, b ->
if (b) if (b) {
if (!infos.first().hasDictionary)
showMissingDictionaryDialog(context, mainLocale)
addEnabledSubtype(prefs, subtype.subtype) addEnabledSubtype(prefs, subtype.subtype)
}
else else
removeEnabledSubtype(prefs, subtype.subtype) removeEnabledSubtype(prefs, subtype.subtype)
subtype.isEnabled = b subtype.isEnabled = b
@ -115,7 +118,7 @@ class LanguageSettingsDialog(
setOnClickListener { setOnClickListener {
// can be re-added easily, no need for confirmation dialog // can be re-added easily, no need for confirmation dialog
subtypesView.removeView(row) subtypesView.removeView(row)
subtypes.remove(subtype) infos.remove(subtype)
removeAdditionalSubtype(prefs, context.resources, subtype.subtype) removeAdditionalSubtype(prefs, context.resources, subtype.subtype)
removeEnabledSubtype(prefs, subtype.subtype) removeEnabledSubtype(prefs, subtype.subtype)
@ -128,10 +131,10 @@ class LanguageSettingsDialog(
private fun fillSecondaryLocaleView(secondaryLocalesView: LinearLayout) { private fun fillSecondaryLocaleView(secondaryLocalesView: LinearLayout) {
// can only use multilingual typing if there is more than one dictionary available // can only use multilingual typing if there is more than one dictionary available
val availableSecondaryLocales = getAvailableDictionaryLocales( val availableSecondaryLocales = getAvailableSecondaryLocales(
context, context,
mainLocaleString, mainLocaleString,
subtypes.first().subtype.isAsciiCapable infos.first().subtype.isAsciiCapable
) )
val selectedSecondaryLocales = Settings.getSecondaryLocales(prefs, mainLocaleString) val selectedSecondaryLocales = Settings.getSecondaryLocales(prefs, mainLocaleString)
selectedSecondaryLocales.forEach { selectedSecondaryLocales.forEach {
@ -210,6 +213,9 @@ class LanguageSettingsDialog(
} }
private fun addDictionaryToView(dictFile: File, dictionariesView: LinearLayout) { private fun addDictionaryToView(dictFile: File, dictionariesView: LinearLayout) {
if (!infos.first().hasDictionary) {
infos.forEach { it.hasDictionary = true }
}
val dictType = dictFile.name.substringBefore("_${USER_DICTIONARY_SUFFIX}") val dictType = dictFile.name.substringBefore("_${USER_DICTIONARY_SUFFIX}")
val row = LayoutInflater.from(context).inflate(R.layout.language_list_item, listView) val row = LayoutInflater.from(context).inflate(R.layout.language_list_item, listView)
row.findViewById<TextView>(R.id.language_name).text = dictType row.findViewById<TextView>(R.id.language_name).text = dictType
@ -235,6 +241,10 @@ class LanguageSettingsDialog(
val newDictBroadcast = Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION) val newDictBroadcast = Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION)
fragment?.activity?.sendBroadcast(newDictBroadcast) fragment?.activity?.sendBroadcast(newDictBroadcast)
dictionariesView.removeView(row) dictionariesView.removeView(row)
if (dictionariesView.size < 2) { // first view is "Dictionaries"
infos.forEach { it.hasDictionary = false }
}
} }
} }
} }
@ -276,47 +286,19 @@ fun getUserAndInternalDictionaries(context: Context, locale: String): Pair<List<
} }
// get locales with same script as main locale, but different language // get locales with same script as main locale, but different language
private fun getAvailableDictionaryLocales(context: Context, mainLocaleString: String, asciiCapable: Boolean): Set<Locale> { private fun getAvailableSecondaryLocales(context: Context, mainLocaleString: String, asciiCapable: Boolean): Set<Locale> {
val mainLocale = mainLocaleString.toLocale() val mainLocale = mainLocaleString.toLocale()
val locales = HashSet<Locale>() val locales = getDictionaryLocales(context)
val mainScript = if (asciiCapable) ScriptUtils.SCRIPT_LATIN val mainScript = if (asciiCapable) ScriptUtils.SCRIPT_LATIN
else ScriptUtils.getScriptFromSpellCheckerLocale(mainLocale) else ScriptUtils.getScriptFromSpellCheckerLocale(mainLocale)
// ScriptUtils.getScriptFromSpellCheckerLocale may return latin when it should not // ScriptUtils.getScriptFromSpellCheckerLocale may return latin when it should not
// e.g. for persian or chinese // e.g. for persian or chinese
// workaround: don't allow secondary locales for these locales // workaround: don't allow secondary locales for these locales
if (!asciiCapable && mainScript == ScriptUtils.SCRIPT_LATIN) return locales if (!asciiCapable && mainScript == ScriptUtils.SCRIPT_LATIN) return emptySet()
// get cached dictionaries: extracted or user-added dictionaries locales.removeAll {
val cachedDirectoryList = DictionaryInfoUtils.getCachedDirectoryList(context) it.language == mainLocale.language
if (cachedDirectoryList != null) { || ScriptUtils.getScriptFromSpellCheckerLocale(it) != mainScript
for (directory in cachedDirectoryList) {
if (!directory.isDirectory) continue
if (directory.list()?.isNotEmpty() != true) continue
val dirLocale = DictionaryInfoUtils.getWordListIdFromFileName(directory.name)
if (dirLocale == mainLocaleString) continue
val locale = dirLocale.toLocale()
if (locale.language == mainLocale.language) continue
val localeScript = ScriptUtils.getScriptFromSpellCheckerLocale(locale)
if (localeScript != mainScript) continue
locales.add(locale)
}
}
// get assets dictionaries
val assetsDictionaryList = BinaryDictionaryGetter.getAssetsDictionaryList(context)
if (assetsDictionaryList != null) {
for (dictionary in assetsDictionaryList) {
val dictLocale =
BinaryDictionaryGetter.extractLocaleFromAssetsDictionaryFile(dictionary)
?: continue
if (dictLocale == mainLocaleString) continue
val locale = dictLocale.toLocale()
if (locale.language == mainLocale.language) continue
val localeScript = ScriptUtils.getScriptFromSpellCheckerLocale(locale)
if (localeScript != mainScript) continue
locales.add(locale)
}
} }
return locales return locales
} }
private const val DICTIONARY_URL = "https://codeberg.org/Helium314/aosp-dictionaries"

View file

@ -12,6 +12,7 @@ import org.dslul.openboard.inputmethod.latin.R
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils import org.dslul.openboard.inputmethod.latin.common.LocaleUtils
import org.dslul.openboard.inputmethod.latin.utils.DictionaryInfoUtils import org.dslul.openboard.inputmethod.latin.utils.DictionaryInfoUtils
import org.dslul.openboard.inputmethod.latin.utils.SubtypeLocaleUtils import org.dslul.openboard.inputmethod.latin.utils.SubtypeLocaleUtils
import org.dslul.openboard.inputmethod.latin.utils.getDictionaryLocales
import java.util.Locale import java.util.Locale
@ -22,6 +23,7 @@ class LanguageSettingsFragment : SubScreenFragment() {
private val enabledSubtypes = mutableListOf<InputMethodSubtype>() private val enabledSubtypes = mutableListOf<InputMethodSubtype>()
private val systemLocales = mutableListOf<Locale>() private val systemLocales = mutableListOf<Locale>()
private val languageFilterListPreference by lazy { findPreference("pref_language_filter") as LanguageFilterListPreference } private val languageFilterListPreference by lazy { findPreference("pref_language_filter") as LanguageFilterListPreference }
private val dictionaryLocales by lazy { getDictionaryLocales(activity) }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -137,7 +139,7 @@ class LanguageSettingsFragment : SubScreenFragment() {
} }
private fun InputMethodSubtype.toSubtypeInfo(locale: Locale, isEnabled: Boolean = false) = private fun InputMethodSubtype.toSubtypeInfo(locale: Locale, isEnabled: Boolean = false) =
toSubtypeInfo(locale, activity, isEnabled) toSubtypeInfo(locale, activity, isEnabled, dictionaryLocales.contains(locale))
private fun List<SubtypeInfo>.addToSortedSubtypes() { private fun List<SubtypeInfo>.addToSortedSubtypes() {
forEach { forEach {
@ -169,7 +171,7 @@ class LanguageSettingsFragment : SubScreenFragment() {
} }
class SubtypeInfo(val displayName: String, val subtype: InputMethodSubtype, var isEnabled: Boolean) { class SubtypeInfo(val displayName: String, val subtype: InputMethodSubtype, var isEnabled: Boolean, var hasDictionary: Boolean) {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (other !is SubtypeInfo) return false if (other !is SubtypeInfo) return false
return subtype == other.subtype return subtype == other.subtype
@ -180,8 +182,8 @@ class SubtypeInfo(val displayName: String, val subtype: InputMethodSubtype, var
} }
} }
fun InputMethodSubtype.toSubtypeInfo(locale: Locale, context: Context, isEnabled: Boolean): SubtypeInfo = fun InputMethodSubtype.toSubtypeInfo(locale: Locale, context: Context, isEnabled: Boolean, hasDictionary: Boolean): SubtypeInfo =
SubtypeInfo(LocaleUtils.getLocaleDisplayNameInSystemLocale(locale, context), this, isEnabled) SubtypeInfo(LocaleUtils.getLocaleDisplayNameInSystemLocale(locale, context), this, isEnabled, hasDictionary)
private const val DICTIONARY_REQUEST_CODE = 96834 private const val DICTIONARY_REQUEST_CODE = 96834
const val USER_DICTIONARY_SUFFIX = "user.dict" const val USER_DICTIONARY_SUFFIX = "user.dict"

View file

@ -152,6 +152,8 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_SELECTED_INPUT_STYLE = "pref_selected_input_style"; public static final String PREF_SELECTED_INPUT_STYLE = "pref_selected_input_style";
public static final String PREF_USE_SYSTEM_LOCALES = "pref_use_system_locales"; public static final String PREF_USE_SYSTEM_LOCALES = "pref_use_system_locales";
public static final String PREF_DONT_SHOW_MISSING_DICTIONARY_DIALOG = "pref_dont_show_missing_dict_dialog";
// This preference key is deprecated. Use {@link #PREF_SHOW_LANGUAGE_SWITCH_KEY} instead. // This preference key is deprecated. Use {@link #PREF_SHOW_LANGUAGE_SWITCH_KEY} instead.
// This is being used only for the backward compatibility. // This is being used only for the backward compatibility.
private static final String PREF_SUPPRESS_LANGUAGE_SWITCH_KEY = private static final String PREF_SUPPRESS_LANGUAGE_SWITCH_KEY =

View file

@ -0,0 +1,70 @@
package org.dslul.openboard.inputmethod.latin.utils
import android.app.AlertDialog
import android.content.Context
import android.text.Html
import android.text.method.LinkMovementMethod
import android.view.View
import android.widget.TextView
import androidx.core.content.edit
import org.dslul.openboard.inputmethod.latin.BinaryDictionaryGetter
import org.dslul.openboard.inputmethod.latin.R
import org.dslul.openboard.inputmethod.latin.settings.Settings
import java.util.*
import kotlin.collections.HashSet
fun getDictionaryLocales(context: Context): MutableSet<Locale> {
val locales = HashSet<Locale>()
// get cached dictionaries: extracted or user-added dictionaries
val cachedDirectoryList = DictionaryInfoUtils.getCachedDirectoryList(context)
if (cachedDirectoryList != null) {
for (directory in cachedDirectoryList) {
if (!directory.isDirectory) continue
if (directory.list()?.isNotEmpty() != true) continue
val dirLocale = DictionaryInfoUtils.getWordListIdFromFileName(directory.name)
val locale = dirLocale.toLocale()
locales.add(locale)
}
}
// get assets dictionaries
val assetsDictionaryList = BinaryDictionaryGetter.getAssetsDictionaryList(context)
if (assetsDictionaryList != null) {
for (dictionary in assetsDictionaryList) {
val dictLocale =
BinaryDictionaryGetter.extractLocaleFromAssetsDictionaryFile(dictionary)
?: continue
val locale = dictLocale.toLocale()
locales.add(locale)
}
}
return locales
}
fun showMissingDictionaryDialog(context: Context, locale: Locale) {
val prefs = DeviceProtectedUtils.getSharedPreferences(context)
if (prefs.getBoolean(Settings.PREF_DONT_SHOW_MISSING_DICTIONARY_DIALOG, false) || locale.toString() == "zz")
return
val repositoryLink = "<a href='$DICTIONARY_URL'>" + context.getString(R.string.dictionary_link_text) + "</a>"
val dictionaryLink = "<a href='$DICTIONARY_URL/src/branch/main/dictionaries/main_$locale.dict'>" + context.getString(
R.string.dictionary_link_text) + "</a>"
val message = Html.fromHtml(context.getString(
R.string.no_dictionary_message,
repositoryLink,
locale.toString(),
dictionaryLink,
))
val dialog = AlertDialog.Builder(context)
.setTitle(R.string.no_dictionaries_available)
.setMessage(message)
.setNegativeButton(R.string.dialog_close, null)
.setNeutralButton(R.string.no_dictionary_dont_show_again_button) { _, _ ->
prefs.edit { putBoolean(Settings.PREF_DONT_SHOW_MISSING_DICTIONARY_DIALOG, true) }
}
.create()
dialog.show()
(dialog.findViewById<View>(android.R.id.message) as? TextView)?.movementMethod = LinkMovementMethod.getInstance()
}
const val DICTIONARY_URL = "https://codeberg.org/Helium314/aosp-dictionaries"

View file

@ -499,9 +499,16 @@ disposition rather than other common dispositions for Latin languages. [CHAR LIM
<string name="replace_dictionary">"Replace dictionary"</string> <string name="replace_dictionary">"Replace dictionary"</string>
<!-- Message for user dictionary remove dialog --> <!-- Message for user dictionary remove dialog -->
<string name="remove_dictionary_message">"Really remove user-added dictionary \"%s\"?"</string> <string name="remove_dictionary_message">"Really remove user-added dictionary \"%s\"?"</string>
<!-- Message for the user dictionary selection dialog. This string will be interpreted as HTML --> <!-- Message when no dictionary is available for the selected language, with no_dictionaries_available as title.
%1$s will be replaced by dictionary_link_text, %2$s by the language code, %3$s by dictionary_link_text again.
This string will be interpreted as HTML -->
<string name="no_dictionary_message">"Without a dictionary, you will only get suggestions for text you entered before.&lt;br&gt;
You can download dictionaries %1$s, or check whether a dictionary for \"%2$s\" can be downloaded directly %3$s."</string>
<!-- Button to never show the no_dictionary_message when enabling a language that has no dictionary -->
<string name="no_dictionary_dont_show_again_button">"Don't show again"</string>
<!-- Message for the user dictionary selection dialog, %s will be replaced by dictionary_link_text. This string will be interpreted as HTML -->
<string name="add_dictionary">"Select a dictionary to add. Dictionaries in .dict format can be downloaded %s."</string> <string name="add_dictionary">"Select a dictionary to add. Dictionaries in .dict format can be downloaded %s."</string>
<!-- Title of the link to the download page inserted into selection message (above) --> <!-- Title of the link to the download page inserted into messages (add_dictionary and no_dictionary_message) -->
<string name="dictionary_link_text">"here"</string> <string name="dictionary_link_text">"here"</string>
<!-- Text shown when dictionary file could not be read --> <!-- Text shown when dictionary file could not be read -->
<string name="dictionary_file_error">"Error: Selected file is not a valid dictionary file"</string> <string name="dictionary_file_error">"Error: Selected file is not a valid dictionary file"</string>