Korean (#671)
* added Korean language * fokin context no more messing up everything in the InputModes * no more unnecessary textField and inputType passing in the InputModes * a single source of truth for the InputMode kind * ModePredictive -> ModeWords * no more db queries to increase the priority of emojis and special chars * Korean virtual keypad * more consistent displaying of the ABC string * sorted out the labels of 1-key and 0-key in numeric modes * documentation update
This commit is contained in:
parent
f3c701fd55
commit
5a108dcda9
107 changed files with 13010 additions and 609 deletions
|
|
@ -59,6 +59,7 @@ To support a new language one needs to:
|
|||
- `hasSpaceBetweenWords` _(optional)_ set to `no` when the language does not use spaces between words. For example: Thai, Chinese, Japanese, Korean, and so on. The default is `yes`.
|
||||
- `hasUpperCase` _(optional)_ set to `no` when the language has no upper- and lowercase letters. For example: Arabic, Hebrew, East Asian languages, and so on. The default is `yes`.
|
||||
- `name` _(optional)_ is automatically generated and equals the native name of the language (e.g. "English", "Deutsch", "Українська"). However, sometimes, the automatically selected name may be ambiguous. For example, both Portuguese in Portugal and Brazil will default to "Português", so assigning "Português brasileiro" would make it clear it's the language used in Brazil.
|
||||
- `sounds` _(mandatory for non-alphabetic languages)_ is an array of elements in the format: `[sound,digits]`. It is used for East Asian or other languages where there are thousands of different characters, that can not be described in the layout property. `sounds` contains all possible vowel and consonant sounds and their respective digit combinations. There must be no repeating sounds. If a single Latin letter stands for a sound, the letter must be capital. If more than one letter is necessary to represent the sound, the first letter must be capital, while the rest must be small. For example, "A", "P", "Wo", "Ei", "Dd". The sounds are then used in the dictionary format with phonetic transcriptions. See `Korean.yml` and the respective dictionary file for an example.
|
||||
|
||||
### Dictionary Formats
|
||||
|
||||
|
|
@ -102,6 +103,35 @@ fifth 3
|
|||
...
|
||||
```
|
||||
|
||||
#### CSV Containing Words and Phonetic Transcriptions
|
||||
The third accepted format is suitable for East Asian and other languages with many different characters. Each character or word has a phonetic representation using Latin letters. Frequencies are not applicable in this format.
|
||||
|
||||
Constraints:
|
||||
- No header.
|
||||
- The separator is `TAB`.
|
||||
- The first element is the language character or word
|
||||
- The second element is the phonetic representation with Latin letters. It must be a combination of the `sounds` in the respective YAML definition.
|
||||
|
||||
Example definition:
|
||||
```yaml
|
||||
# ...
|
||||
- sounds:
|
||||
- [We,123]
|
||||
- [Tt,221]
|
||||
- [Wo,48]
|
||||
```
|
||||
|
||||
Example dictionary:
|
||||
```csv
|
||||
다 WeWo
|
||||
줘 TtWoWe
|
||||
와 Wo
|
||||
```
|
||||
|
||||
Using the above example, when the user types "221-48-123", it will result in: "줘".
|
||||
|
||||
See `Korean.yml` and `ko-utf8.csv` for more examples.
|
||||
|
||||
## Translating the UI
|
||||
To translate Traditional T9 menus and messages in your language, add: `res/values-your-lang/strings.xml`. Then use the Android Studio translation editor. It is very handy.
|
||||
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ ext.convertDictionaries = { definitionsInputDir, dictionariesInputDir, dictionar
|
|||
int errorCount = 0
|
||||
|
||||
def errorStream = fileTree(dir: definitionsInputDir).getFiles().parallelStream().map { definition ->
|
||||
def (_, sounds, __, locale, dictionaryFile, langFileErrorCount, langFileErrorMsg) = parseLanguageDefintion(definition, dictionariesInputDir)
|
||||
def (_, sounds, noSyllables, locale, dictionaryFile, langFileErrorCount, langFileErrorMsg) = parseLanguageDefintion(definition, dictionariesInputDir)
|
||||
errorCount += langFileErrorCount
|
||||
if (!langFileErrorMsg.isEmpty()) {
|
||||
return langFileErrorMsg
|
||||
}
|
||||
|
||||
def (conversionErrorCount, conversionErrorMessages) = convertDictionary(definition, dictionaryFile, dictionariesOutputDir, dictionariesMetaDir, DICTIONARY_OUTPUT_EXTENSION, sounds, locale, MAX_ERRORS, CSV_DELIMITER)
|
||||
def (conversionErrorCount, conversionErrorMessages) = convertDictionary(definition, dictionaryFile, dictionariesOutputDir, dictionariesMetaDir, DICTIONARY_OUTPUT_EXTENSION, sounds, noSyllables, locale, MAX_ERRORS, CSV_DELIMITER)
|
||||
errorCount += conversionErrorCount
|
||||
if (!conversionErrorMessages.isEmpty()) {
|
||||
return conversionErrorMessages
|
||||
|
|
@ -31,7 +31,7 @@ ext.convertDictionaries = { definitionsInputDir, dictionariesInputDir, dictionar
|
|||
|
||||
|
||||
// this cannot be static, because DictionaryTools will not be visible
|
||||
def convertDictionary(File definition, File csvDictionary, String dictionariesOutputDir, String dictionariesMetaDir, String outputDictionaryExtension, HashMap<String, String> sounds, Locale locale, int maxErrors, String csvDelimiter) {
|
||||
def convertDictionary(File definition, File csvDictionary, String dictionariesOutputDir, String dictionariesMetaDir, String outputDictionaryExtension, HashMap<String, String> sounds, boolean noSyllables, Locale locale, int maxErrors, String csvDelimiter) {
|
||||
if (isDictionaryUpToDate(definition, csvDictionary, dictionariesMetaDir)) {
|
||||
return [0, ""]
|
||||
}
|
||||
|
|
@ -70,7 +70,7 @@ def convertDictionary(File definition, File csvDictionary, String dictionariesOu
|
|||
|
||||
outputDictionary = sortDictionary(outputDictionary)
|
||||
|
||||
def (assetError, zippedDictionary) = writeZippedDictionary(dictionariesOutputDir, csvDictionary, outputDictionary, outputDictionaryExtension)
|
||||
def (assetError, zippedDictionary) = writeZippedDictionary(dictionariesOutputDir, csvDictionary, outputDictionary, outputDictionaryExtension, noSyllables)
|
||||
if (assetError) {
|
||||
errorCount++
|
||||
errorMsg += assetError
|
||||
|
|
@ -88,12 +88,12 @@ def convertDictionary(File definition, File csvDictionary, String dictionariesOu
|
|||
|
||||
//////////////////// DICTIONARY PROCESSING ////////////////////
|
||||
|
||||
static byte[] compressDictionaryLine(String digitSequence, List<String> words) {
|
||||
static byte[] compressDictionaryLine(String digitSequence, List<String> words, boolean noSyllables) {
|
||||
if (words.isEmpty()) {
|
||||
throw new IllegalArgumentException("No words for digit sequence: ${digitSequence}")
|
||||
}
|
||||
|
||||
boolean shouldSeparateWords = false
|
||||
boolean shouldSeparateWords = !noSyllables
|
||||
|
||||
for (def i = 0; i < words.size(); i++) {
|
||||
if (words.get(i).length() != digitSequence.length()) {
|
||||
|
|
@ -104,7 +104,7 @@ static byte[] compressDictionaryLine(String digitSequence, List<String> words) {
|
|||
|
||||
return (
|
||||
digitSequence +
|
||||
(shouldSeparateWords ? ' ' : '') +
|
||||
(shouldSeparateWords && noSyllables ? ' ' : '') + // if the language definition has sounds (aka the characters are syllables), we separate the words for sure, so the initial hint is not needed
|
||||
words.join(shouldSeparateWords ? ' ' : null)
|
||||
).getBytes(StandardCharsets.UTF_8)
|
||||
}
|
||||
|
|
@ -166,7 +166,7 @@ static getZipDictionaryFile(dictionariesOutputDir, csvDictionary, outputDictiona
|
|||
/**
|
||||
* Zipping the text files results in a smaller APK in comparison to the uncompressed text files.
|
||||
*/
|
||||
static def writeZippedDictionary(dictionariesOutputDir, csvDictionaryFile, outputDictionary, outputDictionaryExtension) {
|
||||
static def writeZippedDictionary(dictionariesOutputDir, csvDictionaryFile, outputDictionary, outputDictionaryExtension, noSyllables) {
|
||||
def fileName = getDictionaryFileName(csvDictionaryFile)
|
||||
def outputFile = getZipDictionaryFile(dictionariesOutputDir, csvDictionaryFile, outputDictionaryExtension)
|
||||
|
||||
|
|
@ -174,7 +174,7 @@ static def writeZippedDictionary(dictionariesOutputDir, csvDictionaryFile, outpu
|
|||
def zipOutputStream = new ZipOutputStream(new FileOutputStream(outputFile))
|
||||
zipOutputStream.putNextEntry(new ZipEntry("${fileName}.txt"))
|
||||
outputDictionary.each { digitSequence, words ->
|
||||
zipOutputStream.write(compressDictionaryLine(digitSequence, words))
|
||||
zipOutputStream.write(compressDictionaryLine(digitSequence, words, noSyllables))
|
||||
}
|
||||
zipOutputStream.closeEntry()
|
||||
zipOutputStream.close()
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class Wrapper {
|
|||
|
||||
|
||||
static def wordToDigitSequence(Locale locale, String word, HashMap<String, String> sounds, boolean isTranscribed) {
|
||||
String sequence = ""
|
||||
def sequence = new StringBuilder()
|
||||
|
||||
final String normalizedWord = isTranscribed ? word : word.toUpperCase(locale)
|
||||
String currentSound = ""
|
||||
|
|
@ -41,7 +41,7 @@ class Wrapper {
|
|||
if (!sounds.containsKey(currentSound)) {
|
||||
throw new IllegalArgumentException("Sound or layout entry '${currentSound}' does not belong to the language sound list: ${sounds}.")
|
||||
} else {
|
||||
sequence += sounds.get(currentSound)
|
||||
sequence << sounds.get(currentSound)
|
||||
currentSound = ""
|
||||
}
|
||||
}
|
||||
|
|
@ -51,7 +51,7 @@ class Wrapper {
|
|||
throw new IllegalArgumentException("The word does not contain any valid sounds.")
|
||||
}
|
||||
|
||||
return sequence
|
||||
return sequence.toString()
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
72
app/languages/definitions/Korean.yml
Normal file
72
app/languages/definitions/Korean.yml
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
locale: ko-KR
|
||||
dictionaryFile: ko-utf8.csv
|
||||
hasUpperCase: no
|
||||
layout: # only used for the virtual key labels
|
||||
- [ㅇ,ㅁ,SPECIAL] # 0
|
||||
- [ㅣ,PUNCTUATION_KR] # 1
|
||||
- [ㆍ,:] # 2
|
||||
- [ㅡ] # 3
|
||||
- [ㄱ,ㅋ,ㄲ] # 4
|
||||
- [ㄴ,ㄹ] # 5
|
||||
- [ㄷ,ㅌ,ㄸ] # 6
|
||||
- [ㅂ,ㅍ,ㅃ] # 7
|
||||
- [ㅅ,ㅎ,ㅆ] # 8
|
||||
- [ㅈ,ㅊ,ㅉ] # 9
|
||||
sounds:
|
||||
########## initial consonants ##########
|
||||
- [G,4]
|
||||
- [K,44]
|
||||
- [Gg,444]
|
||||
- [N,5]
|
||||
- [L,55] # also: "R", but we need a unique identifier, so we use "L"
|
||||
- [D,6]
|
||||
- [T,66]
|
||||
- [Dd,666]
|
||||
- [B,7]
|
||||
- [P,77]
|
||||
- [Bb,777]
|
||||
- [S,8]
|
||||
- [H,88]
|
||||
- [Ss,888]
|
||||
- [J,9]
|
||||
- [C,99] # also transcribed as "Ch"
|
||||
- [Jj,999]
|
||||
- [Z,0] # "ㅇ" when zero initial consonant
|
||||
- [Ng,0]
|
||||
- [M,00]
|
||||
########## vowels ##########
|
||||
- [I,1]
|
||||
- [A,12]
|
||||
- [Ae,121]
|
||||
- [Ya,122]
|
||||
- [Yae,1221]
|
||||
- [Q,2] # "Q" is just some random unique identifier
|
||||
- [Qq, 22]
|
||||
- [Eo,21]
|
||||
- [E,211]
|
||||
- [Yeo,221]
|
||||
- [Ye,2211]
|
||||
- [Yo,223]
|
||||
- [O,23]
|
||||
- [Oe,231]
|
||||
- [Wa,2312]
|
||||
- [Wae,23121]
|
||||
- [Eu,3]
|
||||
- [Ui,31]
|
||||
- [U,32]
|
||||
- [Wi,321]
|
||||
- [Yu,322]
|
||||
- [Wo,3221]
|
||||
- [We,32211]
|
||||
########## final consonants ##########
|
||||
- [Gs,48]
|
||||
- [Lg,554]
|
||||
- [Lb,557]
|
||||
- [Ls,558]
|
||||
- [Lt,5566]
|
||||
- [Lp,5577]
|
||||
- [Lh,5588]
|
||||
- [Lm,5500]
|
||||
- [Nh,588]
|
||||
- [Nj,59]
|
||||
- [Bs,78]
|
||||
11248
app/languages/dictionaries/ko-utf8.csv
Normal file
11248
app/languages/dictionaries/ko-utf8.csv
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:tools="http://schemas.android.com/tools"
|
||||
android:versionCode="775"
|
||||
android:versionName="40.1"
|
||||
android:versionCode="799"
|
||||
android:versionName="40.25"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <!-- allows displaying notifications on Android >= 13 -->
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ public class DataStore {
|
|||
|
||||
private static void getWordsSync(ConsumerCompat<ArrayList<String>> dataHandler, Language language, String sequence, String filter, int minWords, int maxWords) {
|
||||
try {
|
||||
ArrayList<String> data = words.getSimilar(getWordsCancellationSignal, language, sequence, filter, minWords, maxWords);
|
||||
ArrayList<String> data = words.getMany(getWordsCancellationSignal, language, sequence, filter, minWords, maxWords);
|
||||
asyncReturn.post(() -> dataHandler.accept(data));
|
||||
} catch (Exception e) {
|
||||
Logger.e(LOG_TAG, "Error fetching words: " + e.getMessage());
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ public class CustomWordsImporter extends AbstractFileProcessor {
|
|||
Timer.start(getClass().getSimpleName());
|
||||
|
||||
sendStart(resources.getString(R.string.dictionary_import_running));
|
||||
if (isFileValid() && isThereRoomForMoreWords(activity) && insertWords(activity)) {
|
||||
if (isFileValid() && isThereRoomForMoreWords(activity) && insertWords()) {
|
||||
sendSuccess();
|
||||
Logger.i(getClass().getSimpleName(), "Imported " + file.getName() + " in " + Timer.get(getClass().getSimpleName()) + " ms");
|
||||
} else {
|
||||
|
|
@ -114,7 +114,7 @@ public class CustomWordsImporter extends AbstractFileProcessor {
|
|||
}
|
||||
|
||||
|
||||
private boolean insertWords(Context context) {
|
||||
private boolean insertWords() {
|
||||
ReadOps readOps = new ReadOps();
|
||||
int ignoredWords = 0;
|
||||
int lineCount = 1;
|
||||
|
|
@ -128,13 +128,13 @@ public class CustomWordsImporter extends AbstractFileProcessor {
|
|||
return false;
|
||||
}
|
||||
|
||||
CustomWord customWord = createCustomWord(context, line, lineCount);
|
||||
CustomWord customWord = createCustomWord(line, lineCount);
|
||||
if (customWord == null) {
|
||||
sqlite.failTransaction();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (readOps.exists(sqlite.getDb(), customWord.language, customWord.word)) {
|
||||
if (customWord.language == null || customWord.language.isSyllabary() || readOps.exists(sqlite.getDb(), customWord.language, customWord.word)) {
|
||||
ignoredWords++;
|
||||
} else {
|
||||
InsertOps.insertCustomWord(sqlite.getDb(), customWord.language, customWord.sequence, customWord.word);
|
||||
|
|
@ -154,7 +154,10 @@ public class CustomWordsImporter extends AbstractFileProcessor {
|
|||
}
|
||||
|
||||
if (ignoredWords > 0) {
|
||||
Logger.i(getClass().getSimpleName(), "Skipped " + ignoredWords + " word(s) that are already in the dictionary.");
|
||||
Logger.i(
|
||||
getClass().getSimpleName(),
|
||||
"Skipped " + ignoredWords + " word(s) that are already in the dictionary or do not belong to an alphabetic language."
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
@ -196,11 +199,11 @@ public class CustomWordsImporter extends AbstractFileProcessor {
|
|||
}
|
||||
|
||||
|
||||
private CustomWord createCustomWord(Context context, String line, int lineCount) {
|
||||
private CustomWord createCustomWord(String line, int lineCount) {
|
||||
try {
|
||||
return new CustomWord(
|
||||
CustomWordFile.getWord(line),
|
||||
CustomWordFile.getLanguage(context, line)
|
||||
CustomWordFile.getLanguage(line)
|
||||
);
|
||||
} catch (Exception e) {
|
||||
String linePreview = line.length() > 50 ? line.substring(0, 50) + "..." : line;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package io.github.sspanak.tt9.db.entities;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
|
@ -65,7 +64,7 @@ public class CustomWordFile {
|
|||
}
|
||||
|
||||
|
||||
public static NaturalLanguage getLanguage(@NonNull Context context, String line) {
|
||||
public static NaturalLanguage getLanguage(String line) {
|
||||
if (line == null) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -75,7 +74,7 @@ public class CustomWordFile {
|
|||
return null;
|
||||
}
|
||||
|
||||
return LanguageCollection.getLanguage(context, parts[1]);
|
||||
return LanguageCollection.getLanguage(parts[1]);
|
||||
}
|
||||
|
||||
@NonNull public static String getWord(String line) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package io.github.sspanak.tt9.db.entities;
|
|||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
|
@ -17,6 +19,7 @@ import java.util.zip.ZipEntry;
|
|||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import io.github.sspanak.tt9.R;
|
||||
import io.github.sspanak.tt9.languages.Language;
|
||||
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
|
||||
import io.github.sspanak.tt9.util.AssetFile;
|
||||
import io.github.sspanak.tt9.util.Logger;
|
||||
|
|
@ -25,6 +28,7 @@ public class WordFile extends AssetFile {
|
|||
private static final String LOG_TAG = WordFile.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
private final boolean hasSyllables;
|
||||
|
||||
private int lastCharCode;
|
||||
private BufferedReader reader;
|
||||
|
|
@ -36,9 +40,10 @@ public class WordFile extends AssetFile {
|
|||
private int sequences = -1;
|
||||
|
||||
|
||||
public WordFile(Context context, String path, AssetManager assets) {
|
||||
super(assets, path);
|
||||
public WordFile(@NonNull Context context, Language language, AssetManager assets) {
|
||||
super(assets, language != null ? language.getDictionaryFile() : "");
|
||||
this.context = context;
|
||||
hasSyllables = language != null && language.isSyllabary();
|
||||
|
||||
lastCharCode = 0;
|
||||
reader = null;
|
||||
|
|
@ -138,15 +143,6 @@ public class WordFile extends AssetFile {
|
|||
}
|
||||
|
||||
|
||||
public int getSequences() {
|
||||
if (sequences < 0) {
|
||||
loadProperties();
|
||||
}
|
||||
|
||||
return sequences;
|
||||
}
|
||||
|
||||
|
||||
private void setSequences(String rawProperty, String rawValue) {
|
||||
if (!rawProperty.equals("sequences")) {
|
||||
return;
|
||||
|
|
@ -278,7 +274,7 @@ public class WordFile extends AssetFile {
|
|||
return words;
|
||||
}
|
||||
|
||||
boolean areWordsSeparated = false;
|
||||
boolean areWordsSeparated = hasSyllables; // if the language chars are syllables, there is no leading space to hint word separation
|
||||
StringBuilder word = new StringBuilder();
|
||||
|
||||
// If the word string starts with a space, it means there are words longer than the sequence.
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import io.github.sspanak.tt9.db.entities.WordList;
|
|||
import io.github.sspanak.tt9.db.entities.WordPositionsStringBuilder;
|
||||
import io.github.sspanak.tt9.db.wordPairs.WordPair;
|
||||
import io.github.sspanak.tt9.db.words.SlowQueryStats;
|
||||
import io.github.sspanak.tt9.db.words.WordStore;
|
||||
import io.github.sspanak.tt9.languages.EmojiLanguage;
|
||||
import io.github.sspanak.tt9.languages.Language;
|
||||
import io.github.sspanak.tt9.util.Logger;
|
||||
|
|
@ -127,6 +128,9 @@ public class ReadOps {
|
|||
return new WordList();
|
||||
}
|
||||
|
||||
// EXACT_MATCHES concerns only the positions query
|
||||
filter = filter.equals(WordStore.FILTER_EXACT_MATCHES_ONLY) ? "" : filter;
|
||||
|
||||
String wordsQuery = getWordsQuery(language, positions, filter, maximumWords, fullOutput);
|
||||
if (wordsQuery.isEmpty() || (cancel != null && cancel.isCanceled())) {
|
||||
return new WordList();
|
||||
|
|
@ -151,11 +155,17 @@ public class ReadOps {
|
|||
|
||||
|
||||
public String getSimilarWordPositions(@NonNull SQLiteDatabase db, @NonNull CancellationSignal cancel, @NonNull Language language, @NonNull String sequence, String wordFilter, int minPositions) {
|
||||
int generations = switch (sequence.length()) {
|
||||
int generations;
|
||||
|
||||
if (wordFilter.equals(WordStore.FILTER_EXACT_MATCHES_ONLY)) {
|
||||
generations = 0;
|
||||
} else {
|
||||
generations = switch (sequence.length()) {
|
||||
case 2 -> wordFilter.isEmpty() ? 1 : 10;
|
||||
case 3, 4 -> wordFilter.isEmpty() ? 2 : 10;
|
||||
default -> 10;
|
||||
};
|
||||
}
|
||||
|
||||
return getWordPositions(db, cancel, language, sequence, generations, minPositions, wordFilter);
|
||||
}
|
||||
|
|
@ -215,26 +225,33 @@ public class ReadOps {
|
|||
}
|
||||
|
||||
|
||||
@NonNull private String getFactoryWordPositionsQuery(@NonNull Language language, @NonNull String sequence, int generations) {
|
||||
/**
|
||||
* Generates a query to search for positions in the dictionary words table. It supports sequences
|
||||
* that start with a "0" (searches them as strings).
|
||||
*/
|
||||
@NonNull
|
||||
private String getFactoryWordPositionsQuery(@NonNull Language language, @NonNull String sequence, int generations) {
|
||||
StringBuilder sql = new StringBuilder("SELECT `start`, `end` FROM ")
|
||||
.append(Tables.getWordPositions(language.getId()))
|
||||
.append(" WHERE ");
|
||||
|
||||
if (generations >= 0 && generations < 10) {
|
||||
sql.append(" sequence IN(").append(sequence);
|
||||
sql.append(" sequence IN('").append(sequence);
|
||||
|
||||
int lastChild = (int)Math.pow(10, generations) - 1;
|
||||
|
||||
for (int seqEnd = 1; seqEnd <= lastChild; seqEnd++) {
|
||||
if (seqEnd % 10 != 0) {
|
||||
sql.append(",").append(sequence).append(seqEnd);
|
||||
sql.append("','").append(sequence).append(seqEnd);
|
||||
}
|
||||
}
|
||||
|
||||
sql.append(")");
|
||||
sql.append("')");
|
||||
} else {
|
||||
String rangeEnd = generations == 10 ? "9" : "999999";
|
||||
sql.append(" sequence = ").append(sequence).append(" OR sequence BETWEEN ").append(sequence).append("1 AND ").append(sequence).append(rangeEnd);
|
||||
sql.append(" sequence = '")
|
||||
.append(sequence)
|
||||
.append("' OR sequence BETWEEN '").append(sequence).append("1' AND '").append(sequence).append(rangeEnd).append("'");
|
||||
sql.append(" ORDER BY `start` ");
|
||||
sql.append(" LIMIT 100");
|
||||
}
|
||||
|
|
@ -245,7 +262,12 @@ public class ReadOps {
|
|||
}
|
||||
|
||||
|
||||
@NonNull private String getCustomWordPositionsQuery(@NonNull Language language, @NonNull String sequence, int generations) {
|
||||
/**
|
||||
* Generates a query to search for custom word positions. This does NOT support sequences that
|
||||
* start with a "0" (searches them as integers).
|
||||
*/
|
||||
@NonNull
|
||||
private String getCustomWordPositionsQuery(@NonNull Language language, @NonNull String sequence, int generations) {
|
||||
String sql = "SELECT -id as `start`, -id as `end` FROM " + Tables.CUSTOM_WORDS +
|
||||
" WHERE langId = " + language.getId() +
|
||||
" AND (sequence = " + sequence;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ public class SQLiteOpener extends SQLiteOpenHelper {
|
|||
|
||||
private SQLiteOpener(Context context) {
|
||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
allLanguages = new ArrayList<>(LanguageCollection.getAll(context));
|
||||
allLanguages = new ArrayList<>(LanguageCollection.getAll());
|
||||
allLanguages.add(new EmojiLanguage());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -137,6 +137,11 @@ public class WordPairStore extends BaseSyncStore {
|
|||
|
||||
int totalPairs = 0;
|
||||
for (Language language : languages) {
|
||||
if (language.isSyllabary()) {
|
||||
Logger.d(LOG_TAG, "Not loading word pairs for syllabary language: " + language.getId());
|
||||
continue;
|
||||
}
|
||||
|
||||
HashMap<WordPair, WordPair> wordPairs = pairs.get(language.getId());
|
||||
if (wordPairs == null) {
|
||||
wordPairs = new HashMap<>();
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import io.github.sspanak.tt9.db.sqlite.SQLiteOpener;
|
|||
import io.github.sspanak.tt9.db.sqlite.Tables;
|
||||
import io.github.sspanak.tt9.languages.EmojiLanguage;
|
||||
import io.github.sspanak.tt9.languages.Language;
|
||||
import io.github.sspanak.tt9.languages.LanguageKind;
|
||||
import io.github.sspanak.tt9.languages.exceptions.InvalidLanguageCharactersException;
|
||||
import io.github.sspanak.tt9.languages.exceptions.InvalidLanguageException;
|
||||
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
|
||||
|
|
@ -108,7 +109,7 @@ public class DictionaryLoader {
|
|||
|
||||
public static void load(Context context, Language language) {
|
||||
DictionaryLoadingBar progressBar = DictionaryLoadingBar.getInstance(context);
|
||||
getInstance(context).setOnStatusChange(status -> progressBar.show(context, status));
|
||||
getInstance(context).setOnStatusChange(progressBar::show);
|
||||
self.load(context, new ArrayList<>() {{ add(language); }});
|
||||
}
|
||||
|
||||
|
|
@ -133,7 +134,7 @@ public class DictionaryLoader {
|
|||
load(context, language);
|
||||
}
|
||||
// or if the database is outdated, compared to the dictionary file, ask for confirmation and load
|
||||
else if (!hash.equals(new WordFile(context, language.getDictionaryFile(), self.assets).getHash())) {
|
||||
else if (!hash.equals(new WordFile(context, language, self.assets).getHash())) {
|
||||
new DictionaryUpdateNotification(context, language).show();
|
||||
}
|
||||
},
|
||||
|
|
@ -235,8 +236,12 @@ public class DictionaryLoader {
|
|||
|
||||
|
||||
private int importLetters(Language language) throws InvalidLanguageCharactersException {
|
||||
if (language.isSyllabary()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int lettersCount = 0;
|
||||
boolean isEnglish = language.getLocale().equals(Locale.ENGLISH);
|
||||
boolean isEnglish = LanguageKind.isEnglish(language);
|
||||
WordBatch letters = new WordBatch(language);
|
||||
|
||||
for (int key = 2; key <= 9; key++) {
|
||||
|
|
@ -254,7 +259,7 @@ public class DictionaryLoader {
|
|||
|
||||
|
||||
private void importWordFile(Context context, Language language, int positionShift, float minProgress, float maxProgress) throws Exception {
|
||||
WordFile wordFile = new WordFile(context, language.getDictionaryFile(), assets);
|
||||
WordFile wordFile = new WordFile(context, language, assets);
|
||||
WordBatch batch = new WordBatch(language, SettingsStore.DICTIONARY_IMPORT_BATCH_SIZE + 1);
|
||||
float progressRatio = (maxProgress - minProgress) / wordFile.getWords();
|
||||
int wordCount = 0;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import io.github.sspanak.tt9.util.Timer;
|
|||
|
||||
|
||||
public class WordStore extends BaseSyncStore {
|
||||
public static final String FILTER_EXACT_MATCHES_ONLY = "__exact__";
|
||||
private final String LOG_TAG = "sqlite.WordStore";
|
||||
private final ReadOps readOps;
|
||||
|
||||
|
|
@ -59,9 +60,10 @@ public class WordStore extends BaseSyncStore {
|
|||
/**
|
||||
* Loads words matching and similar to a given digit sequence
|
||||
* For example: "7655" -> "roll" (exact match), but also: "rolled", "roller", "rolling", ...
|
||||
* and other similar.
|
||||
* and other similar. When "wordFilter" is set to FILTER_EXACT_MATCHES_ONLY, the word list is
|
||||
* constrained only to the words with length equal to the digit sequence length (exact matches).
|
||||
*/
|
||||
public ArrayList<String> getSimilar(@NonNull CancellationSignal cancel, Language language, String sequence, String wordFilter, int minimumWords, int maximumWords) {
|
||||
public ArrayList<String> getMany(@NonNull CancellationSignal cancel, Language language, String sequence, String wordFilter, int minimumWords, int maximumWords) {
|
||||
if (!checkOrNotify()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
|
@ -89,7 +91,7 @@ public class WordStore extends BaseSyncStore {
|
|||
long wordsTime = Timer.stop("get_words");
|
||||
|
||||
printLoadingSummary(sequence, words, positionsTime, wordsTime);
|
||||
if (!cancel.isCanceled()) { // do not store empty results from aborted queries in the cache
|
||||
if (!cancel.isCanceled()) { // do not cache empty results from aborted queries
|
||||
SlowQueryStats.add(SlowQueryStats.generateKey(language, sequence, wordFilter, minWords), (int) (positionsTime + wordsTime), positions);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ public class DeviceInfo {
|
|||
return context.getResources().getDisplayMetrics().heightPixels;
|
||||
}
|
||||
|
||||
public static boolean noBackspaceKey(Context context) {
|
||||
public static boolean noBackspaceKey() {
|
||||
return !KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_DEL) && !KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_CLEAR);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import io.github.sspanak.tt9.R;
|
|||
import io.github.sspanak.tt9.db.DataStore;
|
||||
import io.github.sspanak.tt9.db.words.DictionaryLoader;
|
||||
import io.github.sspanak.tt9.ime.modes.InputMode;
|
||||
import io.github.sspanak.tt9.ime.modes.InputModeKind;
|
||||
import io.github.sspanak.tt9.languages.LanguageCollection;
|
||||
import io.github.sspanak.tt9.ui.UI;
|
||||
import io.github.sspanak.tt9.ui.dialogs.AddWordDialog;
|
||||
|
|
@ -88,6 +89,11 @@ abstract public class CommandHandler extends TextEditingHandler {
|
|||
return;
|
||||
}
|
||||
|
||||
if (mLanguage.isSyllabary()) {
|
||||
UI.toastShortSingle(this, R.string.function_add_word_not_available);
|
||||
return;
|
||||
}
|
||||
|
||||
if (DictionaryLoader.getInstance(this).isRunning()) {
|
||||
UI.toastShortSingle(this, R.string.dictionary_loading_please_wait);
|
||||
return;
|
||||
|
|
@ -134,10 +140,10 @@ abstract public class CommandHandler extends TextEditingHandler {
|
|||
|
||||
|
||||
protected void nextInputMode() {
|
||||
if (mInputMode.isPassthrough() || voiceInputOps.isListening()) {
|
||||
if (InputModeKind.isPassthrough(mInputMode) || voiceInputOps.isListening()) {
|
||||
return;
|
||||
} else if (allowedInputModes.size() == 1 && allowedInputModes.contains(InputMode.MODE_123)) {
|
||||
mInputMode = !mInputMode.is123() ? InputMode.getInstance(settings, mLanguage, inputType, textField, InputMode.MODE_123) : mInputMode;
|
||||
mInputMode = !InputModeKind.is123(mInputMode) ? InputMode.getInstance(settings, mLanguage, inputType, textField, InputMode.MODE_123) : mInputMode;
|
||||
} else {
|
||||
suggestionOps.cancelDelayedAccept();
|
||||
mInputMode.onAcceptSuggestion(suggestionOps.acceptIncomplete());
|
||||
|
|
@ -159,30 +165,34 @@ abstract public class CommandHandler extends TextEditingHandler {
|
|||
// select the next language
|
||||
int previous = mEnabledLanguages.indexOf(mLanguage.getId());
|
||||
int next = (previous + 1) % mEnabledLanguages.size();
|
||||
mLanguage = LanguageCollection.getLanguage(getApplicationContext(), mEnabledLanguages.get(next));
|
||||
mLanguage = LanguageCollection.getLanguage(mEnabledLanguages.get(next));
|
||||
|
||||
// validate and save it for the next time
|
||||
validateLanguages();
|
||||
}
|
||||
|
||||
|
||||
protected void nextTextCase() {
|
||||
protected boolean nextTextCase() {
|
||||
if (suggestionOps.isEmpty() || mInputMode.getSuggestions().isEmpty()) {
|
||||
// When there are no suggestions, there is no need to execute the code for
|
||||
// adjusting them below.
|
||||
if (mInputMode.nextTextCase()) {
|
||||
settings.saveTextCase(mInputMode.getTextCase());
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// When we are in AUTO mode and current dictionary word is in uppercase,
|
||||
// the mode would switch to UPPERCASE, but visually, the word would not change.
|
||||
// This is why we retry, until there is a visual change.
|
||||
boolean isChanged = false;
|
||||
String before = suggestionOps.get(0);
|
||||
for (int retries = 0; retries < 2 && mInputMode.nextTextCase(); retries++) {
|
||||
String after = mInputMode.getSuggestions().get(0);
|
||||
if (!after.equals(before)) {
|
||||
isChanged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -201,6 +211,8 @@ abstract public class CommandHandler extends TextEditingHandler {
|
|||
textField.setComposingText(suggestionOps.getCurrent());
|
||||
|
||||
settings.saveTextCase(mInputMode.getTextCase());
|
||||
|
||||
return isChanged;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ package io.github.sspanak.tt9.ime;
|
|||
|
||||
import android.view.KeyEvent;
|
||||
|
||||
import io.github.sspanak.tt9.R;
|
||||
import io.github.sspanak.tt9.db.words.DictionaryLoader;
|
||||
import io.github.sspanak.tt9.ime.helpers.TextField;
|
||||
import io.github.sspanak.tt9.ime.modes.ModePredictive;
|
||||
import io.github.sspanak.tt9.ime.modes.InputMode;
|
||||
import io.github.sspanak.tt9.ime.modes.InputModeKind;
|
||||
import io.github.sspanak.tt9.preferences.helpers.Hotkeys;
|
||||
import io.github.sspanak.tt9.ui.UI;
|
||||
import io.github.sspanak.tt9.util.Ternary;
|
||||
|
|
@ -103,7 +105,15 @@ public abstract class HotkeyHandler extends CommandHandler {
|
|||
}
|
||||
|
||||
if (keyCode == settings.getKeyShift()) {
|
||||
return onKeyNextTextCase(validateOnly);
|
||||
return
|
||||
onKeyNextTextCase(validateOnly)
|
||||
// when "Shift" and "Korean Space" share the same key, allow typing a space, when there
|
||||
// are no special characters to shift
|
||||
|| (keyCode == settings.getKeySpaceKorean() && onKeySpaceKorean(validateOnly));
|
||||
}
|
||||
|
||||
if (keyCode == settings.getKeySpaceKorean()) {
|
||||
return onText(" ", validateOnly);
|
||||
}
|
||||
|
||||
if (keyCode == settings.getKeyShowSettings()) {
|
||||
|
|
@ -176,7 +186,7 @@ public abstract class HotkeyHandler extends CommandHandler {
|
|||
|
||||
|
||||
public boolean onKeyFilterClear(boolean validateOnly) {
|
||||
if (suggestionOps.isEmpty()) {
|
||||
if (suggestionOps.isEmpty() || mLanguage.isSyllabary()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -205,6 +215,11 @@ public abstract class HotkeyHandler extends CommandHandler {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (mLanguage.isSyllabary()) {
|
||||
UI.toastShortSingle(this, R.string.function_filter_suggestions_not_available);
|
||||
return true; // prevent the default key action to acknowledge we have processed the event
|
||||
}
|
||||
|
||||
if (validateOnly) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -247,7 +262,7 @@ public abstract class HotkeyHandler extends CommandHandler {
|
|||
|
||||
|
||||
public boolean onKeyNextLanguage(boolean validateOnly) {
|
||||
if (mInputMode.isNumeric() || mEnabledLanguages.size() < 2) {
|
||||
if (InputModeKind.isNumeric(mInputMode) || mEnabledLanguages.size() < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -257,17 +272,21 @@ public abstract class HotkeyHandler extends CommandHandler {
|
|||
|
||||
suggestionOps.cancelDelayedAccept();
|
||||
nextLang();
|
||||
mInputMode.changeLanguage(mLanguage);
|
||||
mInputMode.clearWordStem();
|
||||
getSuggestions();
|
||||
|
||||
// for languages that do not have ABC or Predictive, make sure we remain in valid state
|
||||
if (!mInputMode.changeLanguage(mLanguage)) {
|
||||
mInputMode = InputMode.getInstance(settings, mLanguage, inputType, textField, determineInputModeId());
|
||||
}
|
||||
mInputMode.clearWordStem();
|
||||
|
||||
getSuggestions();
|
||||
statusBar.setText(mInputMode);
|
||||
mainView.render();
|
||||
if (!suggestionOps.isEmpty() || settings.isMainLayoutStealth()) {
|
||||
UI.toastShortSingle(this, mInputMode.getClass().getSimpleName(), mInputMode.toString());
|
||||
}
|
||||
|
||||
if (mInputMode instanceof ModePredictive) {
|
||||
if (InputModeKind.isPredictive(mInputMode)) {
|
||||
DictionaryLoader.autoLoad(this, mLanguage);
|
||||
}
|
||||
|
||||
|
|
@ -309,7 +328,9 @@ public abstract class HotkeyHandler extends CommandHandler {
|
|||
}
|
||||
|
||||
suggestionOps.scheduleDelayedAccept(mInputMode.getAutoAcceptTimeout()); // restart the timer
|
||||
nextTextCase();
|
||||
if (!nextTextCase()) {
|
||||
return false;
|
||||
}
|
||||
statusBar.setText(mInputMode);
|
||||
mainView.render();
|
||||
|
||||
|
|
@ -333,6 +354,7 @@ public abstract class HotkeyHandler extends CommandHandler {
|
|||
return true;
|
||||
}
|
||||
|
||||
|
||||
private boolean onKeyShowSettings(boolean validateOnly) {
|
||||
if (!isInputViewShown() || shouldBeOff()) {
|
||||
return false;
|
||||
|
|
@ -345,6 +367,26 @@ public abstract class HotkeyHandler extends CommandHandler {
|
|||
return true;
|
||||
}
|
||||
|
||||
|
||||
public boolean onKeySpaceKorean(boolean validateOnly) {
|
||||
if (shouldBeOff() || !InputModeKind.isCheonjiin(mInputMode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// type a space when there is nothing to accept
|
||||
if (suggestionOps.isEmpty() && !onText(" ", validateOnly)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// simulate accept with OK when there are suggestions
|
||||
if (!suggestionOps.isEmpty()) {
|
||||
onAcceptSuggestionManually(suggestionOps.acceptCurrent(), KeyEvent.KEYCODE_ENTER);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private boolean onKeyVoiceInput(boolean validateOnly) {
|
||||
if (!isInputViewShown() || shouldBeOff() || !voiceInputOps.isAvailable()) {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
package io.github.sspanak.tt9.ime;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import io.github.sspanak.tt9.ime.helpers.OrientationListener;
|
||||
import io.github.sspanak.tt9.ime.modes.ModeABC;
|
||||
import io.github.sspanak.tt9.ime.modes.InputModeKind;
|
||||
import io.github.sspanak.tt9.ime.voice.VoiceInputOps;
|
||||
import io.github.sspanak.tt9.languages.Language;
|
||||
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
|
||||
|
|
@ -35,32 +36,28 @@ abstract public class MainViewHandler extends HotkeyHandler {
|
|||
}
|
||||
}
|
||||
|
||||
public int getTextCase() {
|
||||
return mInputMode.getTextCase();
|
||||
}
|
||||
|
||||
public boolean isInputLimited() {
|
||||
return inputType.isLimited();
|
||||
}
|
||||
|
||||
public boolean isInputModeABC() {
|
||||
return mInputMode.getClass().equals(ModeABC.class);
|
||||
return InputModeKind.isABC(mInputMode);
|
||||
}
|
||||
|
||||
public boolean isInputModeNumeric() {
|
||||
return mInputMode.is123();
|
||||
return InputModeKind.isNumeric(mInputMode);
|
||||
}
|
||||
|
||||
public boolean isNumericModeStrict() {
|
||||
return mInputMode.is123() && inputType.isNumeric() && !inputType.isPhoneNumber();
|
||||
return InputModeKind.is123(mInputMode) && inputType.isNumeric() && !inputType.isPhoneNumber();
|
||||
}
|
||||
|
||||
public boolean isNumericModeSigned() {
|
||||
return mInputMode.is123() && inputType.isSignedNumber();
|
||||
return InputModeKind.is123(mInputMode) && inputType.isSignedNumber();
|
||||
}
|
||||
|
||||
public boolean isInputModePhone() {
|
||||
return mInputMode.is123() && inputType.isPhoneNumber();
|
||||
return InputModeKind.is123(mInputMode) && inputType.isPhoneNumber();
|
||||
}
|
||||
|
||||
public boolean isTextEditingActive() {
|
||||
|
|
@ -75,6 +72,29 @@ abstract public class MainViewHandler extends HotkeyHandler {
|
|||
return !(new VoiceInputOps(this, null, null, null)).isAvailable();
|
||||
}
|
||||
|
||||
public boolean notLanguageSyllabary() {
|
||||
return mLanguage == null || !mLanguage.isSyllabary();
|
||||
}
|
||||
|
||||
public String getABCString() {
|
||||
return mLanguage == null || mLanguage.isSyllabary() ? "ABC" : mLanguage.getAbcString().toUpperCase(mLanguage.getLocale());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getInputModeName() {
|
||||
if (InputModeKind.isPredictive(mInputMode)) {
|
||||
return "T9";
|
||||
} else if (InputModeKind.isNumeric(mInputMode)){
|
||||
return "123";
|
||||
} else {
|
||||
return getABCString();
|
||||
}
|
||||
}
|
||||
|
||||
public int getTextCase() {
|
||||
return mInputMode.getTextCase();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Language getLanguage() {
|
||||
return mLanguage;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package io.github.sspanak.tt9.ime;
|
|||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
|
||||
import io.github.sspanak.tt9.ime.modes.InputModeKind;
|
||||
import io.github.sspanak.tt9.languages.LanguageCollection;
|
||||
import io.github.sspanak.tt9.languages.LanguageKind;
|
||||
import io.github.sspanak.tt9.util.Clipboard;
|
||||
|
|
@ -14,7 +15,7 @@ abstract public class TextEditingHandler extends VoiceHandler {
|
|||
|
||||
@Override
|
||||
protected boolean onStart(InputConnection connection, EditorInfo field) {
|
||||
isSystemRTL = LanguageKind.isRTL(LanguageCollection.getDefault(this));
|
||||
isSystemRTL = LanguageKind.isRTL(LanguageCollection.getDefault());
|
||||
return super.onStart(connection, field);
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +43,7 @@ abstract public class TextEditingHandler extends VoiceHandler {
|
|||
private void onCommand(int key) {
|
||||
switch (key) {
|
||||
case 0:
|
||||
if (!mInputMode.isNumeric()) {
|
||||
if (!InputModeKind.isNumeric(mInputMode)) {
|
||||
onText(" ", false);
|
||||
}
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import androidx.annotation.NonNull;
|
|||
import io.github.sspanak.tt9.db.DataStore;
|
||||
import io.github.sspanak.tt9.db.words.DictionaryLoader;
|
||||
import io.github.sspanak.tt9.hacks.InputType;
|
||||
import io.github.sspanak.tt9.ime.modes.ModePredictive;
|
||||
import io.github.sspanak.tt9.ime.modes.InputModeKind;
|
||||
import io.github.sspanak.tt9.languages.LanguageCollection;
|
||||
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
|
||||
import io.github.sspanak.tt9.ui.UI;
|
||||
|
|
@ -119,6 +119,7 @@ public class TraditionalT9 extends MainViewHandler {
|
|||
isDead = false;
|
||||
settings.setDemoMode(false);
|
||||
Logger.setLevel(settings.getLogLevel());
|
||||
LanguageCollection.init(this);
|
||||
DataStore.init(this);
|
||||
super.onInit();
|
||||
}
|
||||
|
|
@ -137,7 +138,7 @@ public class TraditionalT9 extends MainViewHandler {
|
|||
|
||||
Logger.setLevel(settings.getLogLevel());
|
||||
|
||||
if (mInputMode.isPassthrough()) {
|
||||
if (InputModeKind.isPassthrough(mInputMode)) {
|
||||
onStop();
|
||||
} else {
|
||||
backgroundTasks.removeCallbacksAndMessages(null);
|
||||
|
|
@ -147,7 +148,7 @@ public class TraditionalT9 extends MainViewHandler {
|
|||
InputType newInputType = new InputType(connection, field);
|
||||
|
||||
if (newInputType.isText()) {
|
||||
DataStore.loadWordPairs(DictionaryLoader.getInstance(this), LanguageCollection.getAll(this, settings.getEnabledLanguageIds()));
|
||||
DataStore.loadWordPairs(DictionaryLoader.getInstance(this), LanguageCollection.getAll(settings.getEnabledLanguageIds()));
|
||||
}
|
||||
|
||||
if (newInputType.isNotUs(this)) {
|
||||
|
|
@ -238,7 +239,7 @@ public class TraditionalT9 extends MainViewHandler {
|
|||
|
||||
@Override
|
||||
protected boolean onNumber(int key, boolean hold, int repeat) {
|
||||
if (mInputMode instanceof ModePredictive && DictionaryLoader.autoLoad(this, mLanguage)) {
|
||||
if (InputModeKind.isPredictive(mInputMode) && DictionaryLoader.autoLoad(this, mLanguage)) {
|
||||
return true;
|
||||
}
|
||||
return super.onNumber(key, hold, repeat);
|
||||
|
|
|
|||
|
|
@ -17,9 +17,10 @@ import io.github.sspanak.tt9.ime.helpers.SuggestionOps;
|
|||
import io.github.sspanak.tt9.ime.helpers.TextField;
|
||||
import io.github.sspanak.tt9.ime.helpers.TextSelection;
|
||||
import io.github.sspanak.tt9.ime.modes.InputMode;
|
||||
import io.github.sspanak.tt9.ime.modes.ModePredictive;
|
||||
import io.github.sspanak.tt9.ime.modes.InputModeKind;
|
||||
import io.github.sspanak.tt9.languages.Language;
|
||||
import io.github.sspanak.tt9.languages.LanguageCollection;
|
||||
import io.github.sspanak.tt9.languages.LanguageKind;
|
||||
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
|
||||
import io.github.sspanak.tt9.ui.UI;
|
||||
import io.github.sspanak.tt9.util.Text;
|
||||
|
|
@ -48,7 +49,7 @@ public abstract class TypingHandler extends KeyPadHandler {
|
|||
|
||||
|
||||
protected boolean shouldBeOff() {
|
||||
return getCurrentInputConnection() == null || mInputMode.isPassthrough();
|
||||
return getCurrentInputConnection() == null || InputModeKind.isPassthrough(mInputMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -93,8 +94,8 @@ public abstract class TypingHandler extends KeyPadHandler {
|
|||
|
||||
|
||||
protected void validateLanguages() {
|
||||
mEnabledLanguages = InputModeValidator.validateEnabledLanguages(getApplicationContext(), mEnabledLanguages);
|
||||
mLanguage = InputModeValidator.validateLanguage(getApplicationContext(), mLanguage, mEnabledLanguages);
|
||||
mEnabledLanguages = InputModeValidator.validateEnabledLanguages(mEnabledLanguages);
|
||||
mLanguage = InputModeValidator.validateLanguage(mLanguage, mEnabledLanguages);
|
||||
settings.saveInputLanguage(mLanguage.getId());
|
||||
settings.saveEnabledLanguageIds(mEnabledLanguages);
|
||||
}
|
||||
|
|
@ -111,7 +112,7 @@ public abstract class TypingHandler extends KeyPadHandler {
|
|||
public boolean onBackspace(int repeat) {
|
||||
// Dialer fields seem to handle backspace on their own and we must ignore it,
|
||||
// otherwise, keyDown race condition occur for all keys.
|
||||
if (mInputMode.isPassthrough()) {
|
||||
if (InputModeKind.isPassthrough(mInputMode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -172,11 +173,17 @@ public abstract class TypingHandler extends KeyPadHandler {
|
|||
protected boolean onNumber(int key, boolean hold, int repeat) {
|
||||
suggestionOps.cancelDelayedAccept();
|
||||
|
||||
|
||||
// In Korean, the next char may "steal" components from the previous one, in which case,
|
||||
// we must replace the previous char with a one containing less strokes.
|
||||
if (mInputMode.shouldReplaceLastLetter(key, hold)) {
|
||||
mInputMode.replaceLastLetter();
|
||||
}
|
||||
// Automatically accept the previous word, when the next one is a space or punctuation,
|
||||
// instead of requiring "OK" before that.
|
||||
// First pass, analyze the incoming key press and decide whether it could be the start of
|
||||
// a new word.
|
||||
if (mInputMode.shouldAcceptPreviousSuggestion(key, hold)) {
|
||||
else if (mInputMode.shouldAcceptPreviousSuggestion(key, hold)) {
|
||||
String lastWord = suggestionOps.acceptIncomplete();
|
||||
mInputMode.onAcceptSuggestion(lastWord);
|
||||
autoCorrectSpace(lastWord, false, key);
|
||||
|
|
@ -230,15 +237,15 @@ public abstract class TypingHandler extends KeyPadHandler {
|
|||
|
||||
|
||||
private void autoCorrectSpace(String currentWord, boolean isWordAcceptedManually, int nextKey) {
|
||||
if (!inputType.isRustDesk() && mInputMode.shouldDeletePrecedingSpace(inputType, textField)) {
|
||||
if (!inputType.isRustDesk() && mInputMode.shouldDeletePrecedingSpace()) {
|
||||
textField.deletePrecedingSpace(currentWord);
|
||||
}
|
||||
|
||||
if (mInputMode.shouldAddPrecedingSpace(inputType, textField)) {
|
||||
if (mInputMode.shouldAddPrecedingSpace()) {
|
||||
textField.addPrecedingSpace(currentWord);
|
||||
}
|
||||
|
||||
if (mInputMode.shouldAddTrailingSpace(inputType, textField, isWordAcceptedManually, nextKey)) {
|
||||
if (mInputMode.shouldAddTrailingSpace(isWordAcceptedManually, nextKey)) {
|
||||
textField.setText(" ");
|
||||
}
|
||||
}
|
||||
|
|
@ -253,10 +260,10 @@ public abstract class TypingHandler extends KeyPadHandler {
|
|||
mEnabledLanguages = settings.getEnabledLanguageIds();
|
||||
|
||||
int oldLang = mLanguage != null ? mLanguage.getId() : -1;
|
||||
mLanguage = LanguageCollection.getLanguage(getApplicationContext(), settings.getInputLanguage());
|
||||
mLanguage = LanguageCollection.getLanguage(settings.getInputLanguage());
|
||||
validateLanguages();
|
||||
|
||||
Language appLanguage = textField.getLanguage(getApplicationContext(), mEnabledLanguages);
|
||||
Language appLanguage = textField.getLanguage(mEnabledLanguages);
|
||||
if (appLanguage != null) {
|
||||
mLanguage = appLanguage;
|
||||
}
|
||||
|
|
@ -270,7 +277,6 @@ public abstract class TypingHandler extends KeyPadHandler {
|
|||
* Restore the last used text case or auto-select a new one based on the input field properties.
|
||||
*/
|
||||
protected void determineTextCase() {
|
||||
mInputMode.setTextFieldCase(inputType.determineTextCase());
|
||||
InputModeValidator.validateTextCase(mInputMode, settings.getTextCase());
|
||||
}
|
||||
|
||||
|
|
@ -287,7 +293,11 @@ public abstract class TypingHandler extends KeyPadHandler {
|
|||
return InputMode.MODE_PASSTHROUGH;
|
||||
}
|
||||
|
||||
allowedInputModes = inputType.determineInputModes(this);
|
||||
allowedInputModes = new ArrayList<>(inputType.determineInputModes(getApplicationContext()));
|
||||
if (LanguageKind.isKorean(mLanguage) && allowedInputModes.contains(InputMode.MODE_ABC)) {
|
||||
allowedInputModes.remove(InputMode.MODE_ABC);
|
||||
}
|
||||
|
||||
return InputModeValidator.validateMode(settings.getInputMode(), allowedInputModes);
|
||||
}
|
||||
|
||||
|
|
@ -351,7 +361,7 @@ public abstract class TypingHandler extends KeyPadHandler {
|
|||
}
|
||||
|
||||
protected void getSuggestions() {
|
||||
if (mInputMode instanceof ModePredictive && DictionaryLoader.getInstance(this).isRunning()) {
|
||||
if (InputModeKind.isPredictive(mInputMode) && DictionaryLoader.getInstance(this).isRunning()) {
|
||||
mInputMode.reset();
|
||||
UI.toastShortSingle(this, R.string.dictionary_loading_please_wait);
|
||||
} else {
|
||||
|
|
@ -378,7 +388,7 @@ public abstract class TypingHandler extends KeyPadHandler {
|
|||
// but there are no words for the new language, we'll get only generated suggestions, consisting
|
||||
// of the last word of the previous language + endings from the new language. These words are invalid,
|
||||
// so we discard them.
|
||||
if (mInputMode instanceof ModePredictive && !mLanguage.isValidWord(suggestionOps.getCurrent()) && !Text.isGraphic(suggestionOps.getCurrent())) {
|
||||
if (InputModeKind.isPredictive(mInputMode) && !mLanguage.isValidWord(suggestionOps.getCurrent()) && !Text.isGraphic(suggestionOps.getCurrent())) {
|
||||
mInputMode.reset();
|
||||
suggestionOps.set(null);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import android.view.inputmethod.InputMethodManager;
|
|||
import io.github.sspanak.tt9.R;
|
||||
import io.github.sspanak.tt9.hacks.DeviceInfo;
|
||||
import io.github.sspanak.tt9.ime.modes.InputMode;
|
||||
import io.github.sspanak.tt9.ime.modes.InputModeKind;
|
||||
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
|
||||
import io.github.sspanak.tt9.ui.main.ResizableMainView;
|
||||
import io.github.sspanak.tt9.ui.tray.StatusBar;
|
||||
|
|
@ -56,7 +57,7 @@ abstract class UiHandler extends AbstractHandler {
|
|||
|
||||
|
||||
protected void setStatusIcon(InputMode mode) {
|
||||
if (!mode.isPassthrough() && settings.isStatusIconEnabled()) {
|
||||
if (!InputModeKind.isPassthrough(mode) && settings.isStatusIconEnabled()) {
|
||||
showStatusIcon(R.drawable.ic_status);
|
||||
} else {
|
||||
hideStatusIcon();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package io.github.sspanak.tt9.ime.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
|
|
@ -85,13 +84,13 @@ public class InputField {
|
|||
* it's a text field where the language doesn't matter, the function returns null.
|
||||
*/
|
||||
@Nullable
|
||||
public Language getLanguage(Context context, ArrayList<Integer> allowedLanguageIds) {
|
||||
public Language getLanguage(ArrayList<Integer> allowedLanguageIds) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (int i = 0; field.hintLocales != null && i < field.hintLocales.size(); i++) {
|
||||
Language lang = LanguageCollection.getByLanguageCode(context, field.hintLocales.get(i).getLanguage());
|
||||
Language lang = LanguageCollection.getByLanguageCode(field.hintLocales.get(i).getLanguage());
|
||||
if (lang != null && allowedLanguageIds.contains(lang.getId())) {
|
||||
return lang;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +1,36 @@
|
|||
package io.github.sspanak.tt9.ime.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import io.github.sspanak.tt9.util.Logger;
|
||||
import io.github.sspanak.tt9.ime.modes.InputMode;
|
||||
import io.github.sspanak.tt9.languages.Language;
|
||||
import io.github.sspanak.tt9.languages.LanguageCollection;
|
||||
import io.github.sspanak.tt9.util.Logger;
|
||||
|
||||
public class InputModeValidator {
|
||||
public static ArrayList<Integer> validateEnabledLanguages(Context context, ArrayList<Integer> enabledLanguageIds) {
|
||||
ArrayList<Language> validLanguages = LanguageCollection.getAll(context, enabledLanguageIds);
|
||||
public static ArrayList<Integer> validateEnabledLanguages(ArrayList<Integer> enabledLanguageIds) {
|
||||
ArrayList<Language> validLanguages = LanguageCollection.getAll(enabledLanguageIds);
|
||||
ArrayList<Integer> validLanguageIds = new ArrayList<>();
|
||||
for (Language lang : validLanguages) {
|
||||
validLanguageIds.add(lang.getId());
|
||||
}
|
||||
if (validLanguageIds.isEmpty()) {
|
||||
validLanguageIds.add(LanguageCollection.getDefault(context).getId());
|
||||
validLanguageIds.add(LanguageCollection.getDefault().getId());
|
||||
Logger.e("validateEnabledLanguages", "The language list seems to be corrupted. Resetting to first language only.");
|
||||
}
|
||||
|
||||
return validLanguageIds;
|
||||
}
|
||||
|
||||
public static Language validateLanguage(Context context, Language language, ArrayList<Integer> validLanguageIds) {
|
||||
public static Language validateLanguage(Language language, ArrayList<Integer> validLanguageIds) {
|
||||
if (language != null && validLanguageIds.contains(language.getId())) {
|
||||
return language;
|
||||
}
|
||||
|
||||
String error = language != null ? "Language: " + language.getId() + " is not enabled." : "Invalid language.";
|
||||
|
||||
Language validLanguage = LanguageCollection.getLanguage(context, validLanguageIds.get(0));
|
||||
validLanguage = validLanguage != null ? validLanguage : LanguageCollection.getDefault(context);
|
||||
Language validLanguage = LanguageCollection.getLanguage(validLanguageIds.get(0));
|
||||
validLanguage = validLanguage != null ? validLanguage : LanguageCollection.getDefault();
|
||||
|
||||
Logger.d("validateLanguage", error + " Enforcing language: " + validLanguage.getId());
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import android.text.InputType;
|
|||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
|
|
@ -130,14 +129,14 @@ abstract public class StandardInputType {
|
|||
* determineInputModes
|
||||
* Determine the typing mode based on the input field being edited. Returns an ArrayList of the allowed modes.
|
||||
*
|
||||
* @return ArrayList<SettingsStore.MODE_ABC | SettingsStore.MODE_123 | SettingsStore.MODE_PREDICTIVE>
|
||||
* @return Set<InputMode.MODE_PASSTHROUGH | InputMode.MODE_ABC | InputMode.MODE_123 | InputMode.MODE_PREDICTIVE>
|
||||
*/
|
||||
public ArrayList<Integer> determineInputModes(Context context) {
|
||||
public Set<Integer> determineInputModes(Context context) {
|
||||
Set<Integer> allowedModes = new HashSet<>();
|
||||
|
||||
if (field == null) {
|
||||
allowedModes.add(InputMode.MODE_PASSTHROUGH);
|
||||
return new ArrayList<>(allowedModes);
|
||||
return allowedModes;
|
||||
}
|
||||
|
||||
// Calculators (only 0-9 and math) and Dialer (0-9, "#" and "*") fields
|
||||
|
|
@ -145,7 +144,7 @@ abstract public class StandardInputType {
|
|||
// Note: A Dialer field is not a Phone number field.
|
||||
if (isSpecialNumeric(context)) {
|
||||
allowedModes.add(InputMode.MODE_PASSTHROUGH);
|
||||
return new ArrayList<>(allowedModes);
|
||||
return allowedModes;
|
||||
}
|
||||
|
||||
switch (field.inputType & InputType.TYPE_MASK_CLASS) {
|
||||
|
|
@ -155,7 +154,7 @@ abstract public class StandardInputType {
|
|||
// Numbers, dates and phone numbers default to the numeric keyboard,
|
||||
// with no extra features.
|
||||
allowedModes.add(InputMode.MODE_123);
|
||||
return new ArrayList<>(allowedModes);
|
||||
return allowedModes;
|
||||
|
||||
case InputType.TYPE_CLASS_TEXT:
|
||||
// This is general text editing. We will default to the
|
||||
|
|
@ -179,7 +178,7 @@ abstract public class StandardInputType {
|
|||
allowedModes.add(InputMode.MODE_123);
|
||||
allowedModes.add(InputMode.MODE_ABC);
|
||||
|
||||
return new ArrayList<>(allowedModes);
|
||||
return allowedModes;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import java.util.ArrayList;
|
|||
import io.github.sspanak.tt9.hacks.InputType;
|
||||
import io.github.sspanak.tt9.ime.helpers.TextField;
|
||||
import io.github.sspanak.tt9.languages.Language;
|
||||
import io.github.sspanak.tt9.languages.LanguageKind;
|
||||
import io.github.sspanak.tt9.languages.NaturalLanguage;
|
||||
import io.github.sspanak.tt9.languages.NullLanguage;
|
||||
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
|
||||
|
|
@ -28,11 +29,11 @@ abstract public class InputMode {
|
|||
public static final int CASE_DICTIONARY = 3; // do not force it, but use the dictionary word as-is
|
||||
protected final ArrayList<Integer> allowedTextCases = new ArrayList<>();
|
||||
protected int textCase = CASE_LOWER;
|
||||
protected int textFieldTextCase = CASE_UNDEFINED;
|
||||
|
||||
// data
|
||||
protected int autoAcceptTimeout = -1;
|
||||
@NonNull protected String digitSequence = "";
|
||||
protected final boolean isEmailMode;
|
||||
@NonNull protected Language language = new NullLanguage();
|
||||
protected final SettingsStore settings;
|
||||
@NonNull protected final ArrayList<String> suggestions = new ArrayList<>();
|
||||
|
|
@ -40,7 +41,8 @@ abstract public class InputMode {
|
|||
protected int specialCharSelectedGroup = 0;
|
||||
|
||||
|
||||
protected InputMode(SettingsStore settings) {
|
||||
protected InputMode(SettingsStore settings, InputType inputType) {
|
||||
isEmailMode = inputType != null && inputType.isEmail();
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
|
|
@ -48,15 +50,15 @@ abstract public class InputMode {
|
|||
public static InputMode getInstance(SettingsStore settings, @Nullable Language language, InputType inputType, TextField textField, int mode) {
|
||||
switch (mode) {
|
||||
case MODE_PREDICTIVE:
|
||||
return new ModePredictive(settings, inputType, textField, language);
|
||||
return (LanguageKind.isKorean(language) ? new ModeCheonjiin(settings, inputType, textField) : new ModeWords(settings, language, inputType, textField));
|
||||
case MODE_ABC:
|
||||
return new ModeABC(settings, inputType, language);
|
||||
return new ModeABC(settings, language, inputType);
|
||||
case MODE_PASSTHROUGH:
|
||||
return new ModePassthrough(settings);
|
||||
return new ModePassthrough(settings, inputType);
|
||||
default:
|
||||
Logger.w("InputMode", "Defaulting to mode: " + Mode123.class.getName() + " for unknown InputMode: " + mode);
|
||||
case MODE_123:
|
||||
return new Mode123(settings, inputType, language);
|
||||
return new Mode123(settings, language, inputType);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -92,11 +94,6 @@ abstract public class InputMode {
|
|||
return this;
|
||||
}
|
||||
|
||||
// Numeric mode identifiers. "instanceof" cannot be used in all cases, because they inherit each other.
|
||||
public boolean is123() { return false; }
|
||||
public boolean isPassthrough() { return false; }
|
||||
public boolean isNumeric() { return false; }
|
||||
|
||||
// Utility
|
||||
abstract public int getId();
|
||||
public boolean containsGeneratedSuggestions() { return false; }
|
||||
|
|
@ -105,19 +102,36 @@ abstract public class InputMode {
|
|||
public int getAutoAcceptTimeout() {
|
||||
return autoAcceptTimeout;
|
||||
}
|
||||
public void changeLanguage(@Nullable Language newLanguage) {
|
||||
|
||||
/**
|
||||
* Switches to a new language if the input mode supports it. If the InputMode return "false",
|
||||
* it does not support that language, so you must obtain a compatible alternative using the
|
||||
* getInstance() method and the same ID.
|
||||
* The default implementation is to switch to the new language (including NullLanguage) and
|
||||
* return "true".
|
||||
*/
|
||||
public boolean changeLanguage(@Nullable Language newLanguage) {
|
||||
setLanguage(newLanguage);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void setLanguage(@Nullable Language newLanguage) {
|
||||
language = newLanguage != null ? newLanguage : new NullLanguage();
|
||||
}
|
||||
|
||||
|
||||
// Interaction with the IME. Return "true" if it should perform the respective action.
|
||||
public boolean shouldAcceptPreviousSuggestion(String unacceptedText) { return false; }
|
||||
public boolean shouldAcceptPreviousSuggestion(int nextKey, boolean hold) { return false; }
|
||||
public boolean shouldAddTrailingSpace(InputType inputType, TextField textField, boolean isWordAcceptedManually, int nextKey) { return false; }
|
||||
public boolean shouldAddPrecedingSpace(InputType inputType, TextField textField) { return false; }
|
||||
public boolean shouldDeletePrecedingSpace(InputType inputType, TextField textField) { return false; }
|
||||
public boolean shouldAddTrailingSpace(boolean isWordAcceptedManually, int nextKey) { return false; }
|
||||
public boolean shouldAddPrecedingSpace() { return false; }
|
||||
public boolean shouldDeletePrecedingSpace() { return false; }
|
||||
public boolean shouldIgnoreText(String text) { return text == null || text.isEmpty(); }
|
||||
public boolean shouldReplaceLastLetter(int nextKey, boolean hold) { return false; }
|
||||
public boolean shouldSelectNextSuggestion() { return false; }
|
||||
|
||||
public boolean recompose(String word) { return false; }
|
||||
public void replaceLastLetter() {}
|
||||
|
||||
public void reset() {
|
||||
autoAcceptTimeout = -1;
|
||||
|
|
@ -137,10 +151,6 @@ abstract public class InputMode {
|
|||
return true;
|
||||
}
|
||||
|
||||
public void setTextFieldCase(int newTextCase) {
|
||||
textFieldTextCase = allowedTextCases.contains(newTextCase) ? newTextCase : CASE_UNDEFINED;
|
||||
}
|
||||
|
||||
public void defaultTextCase() {
|
||||
textCase = allowedTextCases.get(0);
|
||||
}
|
||||
|
|
@ -166,6 +176,11 @@ abstract public class InputMode {
|
|||
protected String adjustSuggestionTextCase(String word, int newTextCase) { return word; }
|
||||
|
||||
|
||||
protected boolean shouldSelectNextSpecialCharacters() {
|
||||
return !digitSequence.isEmpty();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This is used in nextTextCase() for switching to the next set of characters. Obviously,
|
||||
* special chars do not have a text case, but we use this trick to alternate the char groups.
|
||||
|
|
@ -175,16 +190,13 @@ abstract public class InputMode {
|
|||
specialCharSelectedGroup++;
|
||||
|
||||
return
|
||||
loadSpecialCharacters() // validates specialCharSelectedGroup
|
||||
shouldSelectNextSpecialCharacters() // check if the operation makes sense at all
|
||||
&& loadSpecialCharacters() // validates specialCharSelectedGroup and advances, if possible
|
||||
&& previousGroup != specialCharSelectedGroup; // verifies validation has passed
|
||||
}
|
||||
|
||||
|
||||
protected boolean loadSpecialCharacters() {
|
||||
if (digitSequence.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int key = digitSequence.charAt(0) - '0';
|
||||
ArrayList<String> chars = settings.getOrderedKeyChars(language, key, specialCharSelectedGroup);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
package io.github.sspanak.tt9.ime.modes;
|
||||
|
||||
public class InputModeKind {
|
||||
public static boolean isPassthrough(InputMode mode) {
|
||||
return mode != null && mode.getId() == InputMode.MODE_PASSTHROUGH;
|
||||
}
|
||||
|
||||
public static boolean is123(InputMode mode) {
|
||||
return mode != null && mode.getId() == InputMode.MODE_123;
|
||||
}
|
||||
|
||||
public static boolean isNumeric(InputMode mode) {
|
||||
return isPassthrough(mode) || is123(mode);
|
||||
}
|
||||
|
||||
public static boolean isABC(InputMode mode) {
|
||||
return mode != null && mode.getId() == InputMode.MODE_ABC;
|
||||
}
|
||||
|
||||
public static boolean isPredictive(InputMode mode) {
|
||||
return mode != null && mode.getId() == InputMode.MODE_PREDICTIVE;
|
||||
}
|
||||
|
||||
public static boolean isCheonjiin(InputMode mode) {
|
||||
return mode != null && mode.getClass().equals(ModeCheonjiin.class);
|
||||
}
|
||||
}
|
||||
|
|
@ -10,25 +10,20 @@ import io.github.sspanak.tt9.languages.NaturalLanguage;
|
|||
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
|
||||
import io.github.sspanak.tt9.util.Characters;
|
||||
|
||||
public class Mode123 extends ModePassthrough {
|
||||
class Mode123 extends ModePassthrough {
|
||||
@Override public int getId() { return MODE_123; }
|
||||
@Override @NonNull public String toString() { return "123"; }
|
||||
|
||||
@Override public final boolean is123() { return true; }
|
||||
@Override public boolean isPassthrough() { return false; }
|
||||
@Override public int getSequenceLength() { return digitSequence.length(); }
|
||||
@Override public boolean shouldAcceptPreviousSuggestion(int nextKey, boolean hold) { return true; }
|
||||
|
||||
private final ArrayList<ArrayList<String>> KEY_CHARACTERS = new ArrayList<>();
|
||||
private final boolean isEmailMode;
|
||||
|
||||
|
||||
public Mode123(SettingsStore settings, InputType inputType, Language language) {
|
||||
super(settings);
|
||||
protected Mode123(SettingsStore settings, Language language, InputType inputType) {
|
||||
super(settings, inputType);
|
||||
changeLanguage(language);
|
||||
|
||||
isEmailMode = inputType.isEmail();
|
||||
|
||||
if (inputType.isPhoneNumber()) {
|
||||
setSpecificSpecialCharacters(Characters.Phone, false);
|
||||
} else if (inputType.isNumeric()) {
|
||||
|
|
@ -59,8 +54,14 @@ public class Mode123 extends ModePassthrough {
|
|||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected boolean shouldSelectNextSpecialCharacters() {
|
||||
return !isEmailMode && digitSequence.equals(NaturalLanguage.SPECIAL_CHAR_KEY);
|
||||
}
|
||||
|
||||
|
||||
@Override protected boolean nextSpecialCharacters() {
|
||||
if (isEmailMode || !digitSequence.equals(NaturalLanguage.SPECIAL_CHAR_KEY) || !super.nextSpecialCharacters()) {
|
||||
if (!super.nextSpecialCharacters()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,18 +12,18 @@ import io.github.sspanak.tt9.languages.NaturalLanguage;
|
|||
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
|
||||
import io.github.sspanak.tt9.util.Characters;
|
||||
|
||||
public class ModeABC extends InputMode {
|
||||
class ModeABC extends InputMode {
|
||||
private final ArrayList<ArrayList<String>> KEY_CHARACTERS = new ArrayList<>();
|
||||
|
||||
private boolean shouldSelectNextLetter = false;
|
||||
|
||||
@Override public int getId() { return MODE_ABC; }
|
||||
|
||||
ModeABC(SettingsStore settings, InputType inputType, Language lang) {
|
||||
super(settings);
|
||||
protected ModeABC(SettingsStore settings, Language lang, InputType inputType) {
|
||||
super(settings, inputType);
|
||||
changeLanguage(lang);
|
||||
|
||||
if (inputType.isEmail()) {
|
||||
if (isEmailMode) {
|
||||
KEY_CHARACTERS.add(applyPunctuationOrder(Characters.Email.get(0), 0));
|
||||
KEY_CHARACTERS.add(applyPunctuationOrder(Characters.Email.get(1), 1));
|
||||
}
|
||||
|
|
@ -75,19 +75,28 @@ public class ModeABC extends InputMode {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldSelectNextSpecialCharacters() {
|
||||
return KEY_CHARACTERS.isEmpty() && digitSequence.equals(NaturalLanguage.SPECIAL_CHAR_KEY);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean nextSpecialCharacters() {
|
||||
if (KEY_CHARACTERS.isEmpty() && digitSequence.equals(NaturalLanguage.SPECIAL_CHAR_KEY) && super.nextSpecialCharacters()) {
|
||||
if (!super.nextSpecialCharacters()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
suggestions.add(language.getKeyNumber(digitSequence.charAt(0) - '0'));
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean changeLanguage(@Nullable Language newLanguage) {
|
||||
if (newLanguage != null && newLanguage.isSyllabary()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeLanguage(@Nullable Language newLanguage) {
|
||||
super.changeLanguage(newLanguage);
|
||||
setLanguage(newLanguage);
|
||||
|
||||
allowedTextCases.clear();
|
||||
allowedTextCases.add(CASE_LOWER);
|
||||
|
|
@ -97,6 +106,8 @@ public class ModeABC extends InputMode {
|
|||
|
||||
refreshSuggestions();
|
||||
shouldSelectNextLetter = true; // do not accept any previous suggestions after loading the new ones
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override public void onAcceptSuggestion(@NonNull String w) { reset(); }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,407 @@
|
|||
package io.github.sspanak.tt9.ime.modes;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import io.github.sspanak.tt9.hacks.InputType;
|
||||
import io.github.sspanak.tt9.ime.helpers.TextField;
|
||||
import io.github.sspanak.tt9.ime.modes.helpers.AutoSpace;
|
||||
import io.github.sspanak.tt9.ime.modes.helpers.Cheonjiin;
|
||||
import io.github.sspanak.tt9.ime.modes.predictions.Predictions;
|
||||
import io.github.sspanak.tt9.ime.modes.predictions.SyllablePredictions;
|
||||
import io.github.sspanak.tt9.languages.EmojiLanguage;
|
||||
import io.github.sspanak.tt9.languages.Language;
|
||||
import io.github.sspanak.tt9.languages.LanguageCollection;
|
||||
import io.github.sspanak.tt9.languages.LanguageKind;
|
||||
import io.github.sspanak.tt9.languages.NaturalLanguage;
|
||||
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
|
||||
import io.github.sspanak.tt9.util.Characters;
|
||||
|
||||
class ModeCheonjiin extends InputMode {
|
||||
// used when we want do display a different set of characters for a given key, for example
|
||||
// in email fields
|
||||
private final ArrayList<ArrayList<String>> KEY_CHARACTERS = new ArrayList<>();
|
||||
|
||||
// special chars and emojis
|
||||
private static String SPECIAL_CHAR_SEQUENCE_PREFIX;
|
||||
protected String CUSTOM_EMOJI_SEQUENCE;
|
||||
protected String EMOJI_SEQUENCE;
|
||||
protected String PUNCTUATION_SEQUENCE;
|
||||
protected String SPECIAL_CHAR_SEQUENCE;
|
||||
|
||||
// predictions
|
||||
protected boolean disablePredictions = false;
|
||||
protected Predictions predictions;
|
||||
@NonNull private String previousJamoSequence = "";
|
||||
|
||||
// text analysis
|
||||
protected final AutoSpace autoSpace;
|
||||
protected final InputType inputType;
|
||||
protected final TextField textField;
|
||||
|
||||
|
||||
protected ModeCheonjiin(SettingsStore settings, InputType inputType, TextField textField) {
|
||||
super(settings, inputType);
|
||||
|
||||
SPECIAL_CHAR_SEQUENCE_PREFIX = settings.holdForPunctuationInKorean() ? "11" : "1";
|
||||
|
||||
digitSequence = "";
|
||||
allowedTextCases.add(CASE_LOWER);
|
||||
this.inputType = inputType;
|
||||
this.textField = textField;
|
||||
|
||||
initPredictions();
|
||||
setLanguage(LanguageCollection.getLanguage(LanguageKind.KOREAN));
|
||||
setSpecialCharacterConstants();
|
||||
|
||||
if (isEmailMode) {
|
||||
// Note: applyPunctuationOrder() requires the language to be set
|
||||
KEY_CHARACTERS.add(applyPunctuationOrder(Characters.Email.get(0), 0));
|
||||
KEY_CHARACTERS.add(applyPunctuationOrder(Characters.Email.get(1), 1));
|
||||
} else {
|
||||
setCustomSpecialCharacters();
|
||||
}
|
||||
|
||||
autoSpace = new AutoSpace(settings).setLanguage(language);
|
||||
}
|
||||
|
||||
|
||||
protected void setCustomSpecialCharacters() {
|
||||
if (settings.holdForPunctuationInKorean()) {
|
||||
ArrayList<String> specialChars = new ArrayList<>(applyPunctuationOrder(Characters.Special, 0));
|
||||
specialChars.add(0, "0");
|
||||
KEY_CHARACTERS.add(specialChars);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected void setSpecialCharacterConstants() {
|
||||
CUSTOM_EMOJI_SEQUENCE = SPECIAL_CHAR_SEQUENCE_PREFIX + EmojiLanguage.CUSTOM_EMOJI_SEQUENCE;
|
||||
EMOJI_SEQUENCE = SPECIAL_CHAR_SEQUENCE_PREFIX + EmojiLanguage.EMOJI_SEQUENCE;
|
||||
PUNCTUATION_SEQUENCE = SPECIAL_CHAR_SEQUENCE_PREFIX + NaturalLanguage.PUNCTUATION_KEY;
|
||||
SPECIAL_CHAR_SEQUENCE = "000";
|
||||
}
|
||||
|
||||
|
||||
protected void initPredictions() {
|
||||
predictions = new SyllablePredictions(settings);
|
||||
predictions
|
||||
.setOnlyExactMatches(true)
|
||||
.setMinWords(0)
|
||||
.setWordsChangedHandler(this::onPredictions);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onBackspace() {
|
||||
if (settings.holdForPunctuationInKorean() && digitSequence.equals(PUNCTUATION_SEQUENCE)) {
|
||||
digitSequence = "";
|
||||
} else if (digitSequence.equals(SPECIAL_CHAR_SEQUENCE) || (!digitSequence.startsWith(PUNCTUATION_SEQUENCE) && Cheonjiin.isSingleJamo(digitSequence))) {
|
||||
digitSequence = "";
|
||||
} else if (!digitSequence.isEmpty()) {
|
||||
digitSequence = digitSequence.substring(0, digitSequence.length() - 1);
|
||||
}
|
||||
|
||||
return !digitSequence.isEmpty();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onNumber(int number, boolean hold, int repeat) {
|
||||
if (hold) {
|
||||
reset();
|
||||
digitSequence = String.valueOf(number);
|
||||
disablePredictions = true;
|
||||
onNumberHold(number);
|
||||
} else {
|
||||
basicReset();
|
||||
disablePredictions = false;
|
||||
onNumberPress(number);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
protected void onNumberHold(int number) {
|
||||
if (settings.holdForPunctuationInKorean() && number == 0) {
|
||||
disablePredictions = false;
|
||||
digitSequence = SPECIAL_CHAR_SEQUENCE;
|
||||
} else if (settings.holdForPunctuationInKorean() && number == 1) {
|
||||
disablePredictions = false;
|
||||
digitSequence = PUNCTUATION_SEQUENCE;
|
||||
} else {
|
||||
autoAcceptTimeout = 0;
|
||||
suggestions.add(language.getKeyNumber(number));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected void onNumberPress(int nextNumber) {
|
||||
int rewindAmount = shouldRewindRepeatingNumbers(nextNumber);
|
||||
if (rewindAmount > 0) {
|
||||
digitSequence = digitSequence.substring(0, digitSequence.length() - rewindAmount);
|
||||
}
|
||||
|
||||
if (digitSequence.startsWith(PUNCTUATION_SEQUENCE)) {
|
||||
digitSequence = SPECIAL_CHAR_SEQUENCE_PREFIX + EmojiLanguage.validateEmojiSequence(digitSequence.substring(SPECIAL_CHAR_SEQUENCE_PREFIX.length()), nextNumber);
|
||||
} else {
|
||||
digitSequence += String.valueOf(nextNumber);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private int shouldRewindRepeatingNumbers(int nextNumber) {
|
||||
final int nextChar = nextNumber + '0';
|
||||
final int repeatingDigits = digitSequence.length() > 1 && digitSequence.charAt(digitSequence.length() - 1) == nextChar ? Cheonjiin.getRepeatingEndingDigits(digitSequence) : 0;
|
||||
final int keyCharsCount = nextNumber == 0 ? 2 : language.getKeyCharacters(nextNumber).size();
|
||||
|
||||
if (!settings.holdForPunctuationInKorean() && SPECIAL_CHAR_SEQUENCE.equals(digitSequence + nextNumber)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (SPECIAL_CHAR_SEQUENCE.equals(digitSequence)) {
|
||||
return SPECIAL_CHAR_SEQUENCE.length();
|
||||
}
|
||||
|
||||
if (repeatingDigits == 0 || keyCharsCount < 2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return keyCharsCount < repeatingDigits + 1 ? repeatingDigits : 0;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean changeLanguage(@Nullable Language newLanguage) {
|
||||
return LanguageKind.isKorean(newLanguage);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
basicReset();
|
||||
digitSequence = "";
|
||||
previousJamoSequence = "";
|
||||
disablePredictions = false;
|
||||
}
|
||||
|
||||
|
||||
protected void basicReset() {
|
||||
super.reset();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void loadSuggestions(String ignored) {
|
||||
if (disablePredictions || loadSpecialCharacters() || loadEmojis()) {
|
||||
onSuggestionsUpdated.run();
|
||||
return;
|
||||
}
|
||||
|
||||
String seq = digitSequence;
|
||||
if (shouldDisplayCustomEmojis()) {
|
||||
seq = digitSequence.substring(SPECIAL_CHAR_SEQUENCE_PREFIX.length());
|
||||
} else if (!previousJamoSequence.isEmpty()) {
|
||||
seq = previousJamoSequence;
|
||||
}
|
||||
|
||||
predictions
|
||||
.setLanguage(shouldDisplayCustomEmojis() ? new EmojiLanguage() : language)
|
||||
.setDigitSequence(seq)
|
||||
.load();
|
||||
}
|
||||
|
||||
|
||||
protected boolean loadEmojis() {
|
||||
if (shouldDisplayEmojis()) {
|
||||
suggestions.clear();
|
||||
suggestions.addAll(new EmojiLanguage().getKeyCharacters(digitSequence.charAt(0) - '0', getEmojiGroup()));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
protected int getEmojiGroup() {
|
||||
return digitSequence.length() - EMOJI_SEQUENCE.length();
|
||||
}
|
||||
|
||||
|
||||
protected boolean shouldDisplayEmojis() {
|
||||
return !isEmailMode && digitSequence.startsWith(EMOJI_SEQUENCE) && !digitSequence.equals(CUSTOM_EMOJI_SEQUENCE);
|
||||
}
|
||||
|
||||
|
||||
protected boolean shouldDisplayCustomEmojis() {
|
||||
return !isEmailMode && digitSequence.equals(CUSTOM_EMOJI_SEQUENCE);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected boolean loadSpecialCharacters() {
|
||||
if (!shouldDisplaySpecialCharacters()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int number = digitSequence.isEmpty() ? Integer.MAX_VALUE : digitSequence.charAt(0) - '0';
|
||||
if (KEY_CHARACTERS.size() > number) {
|
||||
suggestions.clear();
|
||||
suggestions.addAll(KEY_CHARACTERS.get(number));
|
||||
return true;
|
||||
} else {
|
||||
return super.loadSpecialCharacters();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected boolean shouldDisplaySpecialCharacters() {
|
||||
return digitSequence.equals(PUNCTUATION_SEQUENCE) || digitSequence.equals(SPECIAL_CHAR_SEQUENCE);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* onPredictions
|
||||
* Gets the currently available Predictions and sends them over to the external caller.
|
||||
*/
|
||||
protected void onPredictions() {
|
||||
// in case the user hasn't added any custom emoji, do not allow advancing to the empty character group
|
||||
if (predictions.getList().isEmpty() && digitSequence.startsWith(EMOJI_SEQUENCE)) {
|
||||
digitSequence = EMOJI_SEQUENCE;
|
||||
return;
|
||||
}
|
||||
|
||||
suggestions.clear();
|
||||
suggestions.addAll(predictions.getList());
|
||||
|
||||
onSuggestionsUpdated.run();
|
||||
}
|
||||
|
||||
|
||||
private void onReplacementPredictions() {
|
||||
autoAcceptTimeout = 0;
|
||||
onPredictions();
|
||||
predictions.setWordsChangedHandler(this::onPredictions);
|
||||
|
||||
autoAcceptTimeout = -1;
|
||||
loadSuggestions(null);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean containsGeneratedSuggestions() {
|
||||
return predictions.containsGeneratedWords();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void replaceLastLetter() {
|
||||
previousJamoSequence = Cheonjiin.stripRepeatingEndingDigits(digitSequence);
|
||||
if (previousJamoSequence.isEmpty() || previousJamoSequence.length() == digitSequence.length()) {
|
||||
previousJamoSequence = "";
|
||||
return;
|
||||
}
|
||||
|
||||
digitSequence = digitSequence.substring(previousJamoSequence.length());
|
||||
|
||||
predictions.setWordsChangedHandler(this::onReplacementPredictions);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean shouldReplaceLastLetter(int nextKey, boolean hold) {
|
||||
return !hold && !shouldDisplayEmojis() && Cheonjiin.isThereMediaVowel(digitSequence) && Cheonjiin.isVowelDigit(nextKey);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* shouldAcceptPreviousSuggestion
|
||||
* Used for analysis before processing the incoming pressed key.
|
||||
*/
|
||||
@Override
|
||||
public boolean shouldAcceptPreviousSuggestion(int nextKey, boolean hold) {
|
||||
return
|
||||
(hold && !digitSequence.isEmpty())
|
||||
|| (digitSequence.equals(SPECIAL_CHAR_SEQUENCE) && nextKey != 0)
|
||||
|| (digitSequence.startsWith(PUNCTUATION_SEQUENCE) && nextKey != 1);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* shouldAcceptPreviousSuggestion
|
||||
* Used for analysis after loading the suggestions.
|
||||
*/
|
||||
@Override
|
||||
public boolean shouldAcceptPreviousSuggestion(String unacceptedText) {
|
||||
return
|
||||
!digitSequence.isEmpty()
|
||||
&& !disablePredictions && !shouldDisplayEmojis() && !shouldDisplaySpecialCharacters() && predictions.noDbWords()
|
||||
&& (Cheonjiin.endsWithDashVowel(digitSequence) || Cheonjiin.endsWithTwoConsonants(digitSequence));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onAcceptSuggestion(@NonNull String word, boolean preserveWordList) {
|
||||
if (shouldDisplaySpecialCharacters() || shouldDisplayEmojis()) {
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
|
||||
String digitSequenceStash = "";
|
||||
|
||||
boolean mustReload = false;
|
||||
if (predictions.noDbWords() && digitSequence.length() >= 2) {
|
||||
digitSequenceStash = digitSequence.substring(digitSequence.length() - 1);
|
||||
mustReload = true;
|
||||
} else if (!previousJamoSequence.isEmpty()) {
|
||||
digitSequenceStash = digitSequence;
|
||||
}
|
||||
|
||||
reset();
|
||||
|
||||
digitSequence = digitSequenceStash;
|
||||
if (mustReload) {
|
||||
loadSuggestions(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected boolean shouldSelectNextSpecialCharacters() {
|
||||
return digitSequence.equals(SPECIAL_CHAR_SEQUENCE);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean shouldAddTrailingSpace(boolean isWordAcceptedManually, int nextKey) {
|
||||
return autoSpace.shouldAddTrailingSpace(textField, inputType, isWordAcceptedManually, nextKey);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean shouldAddPrecedingSpace() {
|
||||
return autoSpace.shouldAddBeforePunctuation(inputType, textField);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean shouldDeletePrecedingSpace() {
|
||||
return autoSpace.shouldDeletePrecedingSpace(inputType, textField);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return MODE_PREDICTIVE;
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return language.getName();
|
||||
}
|
||||
}
|
||||
|
|
@ -2,12 +2,13 @@ package io.github.sspanak.tt9.ime.modes;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import io.github.sspanak.tt9.hacks.InputType;
|
||||
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
|
||||
|
||||
// see: InputType.isSpecialNumeric()
|
||||
public class ModePassthrough extends InputMode {
|
||||
ModePassthrough(SettingsStore settings) {
|
||||
super(settings);
|
||||
class ModePassthrough extends InputMode {
|
||||
protected ModePassthrough(SettingsStore settings, InputType inputType) {
|
||||
super(settings, inputType);
|
||||
reset();
|
||||
allowedTextCases.add(CASE_LOWER);
|
||||
}
|
||||
|
|
@ -16,9 +17,6 @@ public class ModePassthrough extends InputMode {
|
|||
@Override public int getSequenceLength() { return 0; }
|
||||
@Override @NonNull public String toString() { return "--"; }
|
||||
|
||||
@Override public boolean isNumeric() { return true; }
|
||||
@Override public boolean isPassthrough() { return true; }
|
||||
|
||||
@Override public boolean onNumber(int number, boolean hold, int repeat) { return false; }
|
||||
@Override public boolean shouldIgnoreText(String text) { return true; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,58 +7,58 @@ import java.util.ArrayList;
|
|||
|
||||
import io.github.sspanak.tt9.hacks.InputType;
|
||||
import io.github.sspanak.tt9.ime.helpers.TextField;
|
||||
import io.github.sspanak.tt9.ime.modes.helpers.AutoSpace;
|
||||
import io.github.sspanak.tt9.ime.modes.helpers.AutoTextCase;
|
||||
import io.github.sspanak.tt9.ime.modes.helpers.Predictions;
|
||||
import io.github.sspanak.tt9.ime.modes.predictions.WordPredictions;
|
||||
import io.github.sspanak.tt9.languages.EmojiLanguage;
|
||||
import io.github.sspanak.tt9.languages.Language;
|
||||
import io.github.sspanak.tt9.languages.LanguageKind;
|
||||
import io.github.sspanak.tt9.languages.NaturalLanguage;
|
||||
import io.github.sspanak.tt9.languages.exceptions.InvalidLanguageCharactersException;
|
||||
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
|
||||
import io.github.sspanak.tt9.util.Characters;
|
||||
import io.github.sspanak.tt9.util.Logger;
|
||||
import io.github.sspanak.tt9.util.Text;
|
||||
import io.github.sspanak.tt9.util.TextTools;
|
||||
|
||||
public class ModePredictive extends InputMode {
|
||||
class ModeWords extends ModeCheonjiin {
|
||||
private final String LOG_TAG = getClass().getSimpleName();
|
||||
|
||||
private final ArrayList<ArrayList<String>> KEY_CHARACTERS = new ArrayList<>();
|
||||
|
||||
public int getId() { return MODE_PREDICTIVE; }
|
||||
|
||||
private String lastAcceptedWord = "";
|
||||
|
||||
// stem filter
|
||||
private boolean isStemFuzzy = false;
|
||||
private String stem = "";
|
||||
|
||||
// async suggestion handling
|
||||
private boolean disablePredictions = false;
|
||||
|
||||
// text analysis tools
|
||||
private final AutoSpace autoSpace;
|
||||
private final AutoTextCase autoTextCase;
|
||||
private final Predictions predictions;
|
||||
private boolean isCursorDirectionForward = false;
|
||||
private int textFieldTextCase;
|
||||
|
||||
|
||||
ModePredictive(SettingsStore settings, InputType inputType, TextField textField, Language lang) {
|
||||
super(settings);
|
||||
protected ModeWords(SettingsStore settings, Language lang, InputType inputType, TextField textField) {
|
||||
super(settings, inputType, textField);
|
||||
|
||||
autoSpace = new AutoSpace(settings).setLanguage(lang);
|
||||
autoTextCase = new AutoTextCase(settings);
|
||||
digitSequence = "";
|
||||
predictions = new Predictions(settings, textField);
|
||||
|
||||
changeLanguage(lang);
|
||||
defaultTextCase();
|
||||
|
||||
if (inputType.isEmail()) {
|
||||
KEY_CHARACTERS.add(applyPunctuationOrder(Characters.Email.get(0), 0));
|
||||
KEY_CHARACTERS.add(applyPunctuationOrder(Characters.Email.get(1), 1));
|
||||
determineTextFieldTextCase();
|
||||
}
|
||||
|
||||
|
||||
@Override protected void setCustomSpecialCharacters() {} // we use the default ones
|
||||
|
||||
|
||||
protected void setSpecialCharacterConstants() {
|
||||
PUNCTUATION_SEQUENCE = NaturalLanguage.PUNCTUATION_KEY;
|
||||
EMOJI_SEQUENCE = EmojiLanguage.EMOJI_SEQUENCE;
|
||||
CUSTOM_EMOJI_SEQUENCE = EmojiLanguage.CUSTOM_EMOJI_SEQUENCE;
|
||||
SPECIAL_CHAR_SEQUENCE = NaturalLanguage.SPECIAL_CHAR_KEY;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void initPredictions() {
|
||||
predictions = new WordPredictions(settings, textField);
|
||||
predictions.setWordsChangedHandler(this::onPredictions);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -85,31 +85,34 @@ public class ModePredictive extends InputMode {
|
|||
@Override
|
||||
public boolean onNumber(int number, boolean hold, int repeat) {
|
||||
isCursorDirectionForward = true;
|
||||
return super.onNumber(number, hold, repeat);
|
||||
}
|
||||
|
||||
if (hold) {
|
||||
// hold to type any digit
|
||||
reset();
|
||||
|
||||
@Override
|
||||
protected void onNumberHold(int number) {
|
||||
autoAcceptTimeout = 0;
|
||||
disablePredictions = true;
|
||||
digitSequence = String.valueOf(number);
|
||||
suggestions.add(language.getKeyNumber(number));
|
||||
} else {
|
||||
super.reset();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onNumberPress(int number) {
|
||||
digitSequence = EmojiLanguage.validateEmojiSequence(digitSequence, number);
|
||||
disablePredictions = false;
|
||||
|
||||
if (digitSequence.equals(NaturalLanguage.PREFERRED_CHAR_SEQUENCE)) {
|
||||
autoAcceptTimeout = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void changeLanguage(@Nullable Language newLanguage) {
|
||||
super.changeLanguage(newLanguage);
|
||||
public boolean changeLanguage(@Nullable Language newLanguage) {
|
||||
if (newLanguage != null && newLanguage.isSyllabary()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
super.setLanguage(newLanguage);
|
||||
|
||||
autoSpace.setLanguage(language);
|
||||
|
||||
|
|
@ -119,12 +122,14 @@ public class ModePredictive extends InputMode {
|
|||
allowedTextCases.add(CASE_CAPITALIZE);
|
||||
allowedTextCases.add(CASE_UPPER);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean recompose(String word) {
|
||||
if (!language.hasSpaceBetweenWords()) {
|
||||
if (!language.hasSpaceBetweenWords() || language.isSyllabary()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -149,7 +154,7 @@ public class ModePredictive extends InputMode {
|
|||
|
||||
@Override
|
||||
public void reset() {
|
||||
super.reset();
|
||||
basicReset();
|
||||
digitSequence = "";
|
||||
disablePredictions = false;
|
||||
stem = "";
|
||||
|
|
@ -252,60 +257,33 @@ public class ModePredictive extends InputMode {
|
|||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean containsGeneratedSuggestions() {
|
||||
return predictions.containsGeneratedWords();
|
||||
}
|
||||
|
||||
/**
|
||||
* loadSuggestions
|
||||
* Loads the possible list of suggestions for the current digitSequence. "currentWord" is used
|
||||
* for generating suggestions when there are no results.
|
||||
* See: Predictions.generatePossibleCompletions()
|
||||
* See: WordPredictions.generatePossibleCompletions()
|
||||
*/
|
||||
@Override
|
||||
public void loadSuggestions(String currentWord) {
|
||||
if (disablePredictions) {
|
||||
super.loadSuggestions(currentWord);
|
||||
if (disablePredictions || loadPreferredChar() || loadSpecialCharacters() || loadEmojis()) {
|
||||
onSuggestionsUpdated.run();
|
||||
return;
|
||||
}
|
||||
|
||||
if (loadStaticSuggestions()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Language searchLanguage = digitSequence.equals(EmojiLanguage.CUSTOM_EMOJI_SEQUENCE) ? new EmojiLanguage() : language;
|
||||
|
||||
predictions
|
||||
.setDigitSequence(digitSequence)
|
||||
((WordPredictions) predictions)
|
||||
.setInputWord(currentWord.isEmpty() ? stem : currentWord)
|
||||
.setIsStemFuzzy(isStemFuzzy)
|
||||
.setStem(stem)
|
||||
.setLanguage(searchLanguage)
|
||||
.setInputWord(currentWord.isEmpty() ? stem : currentWord)
|
||||
.setWordsChangedHandler(this::onPredictions)
|
||||
.setDigitSequence(digitSequence)
|
||||
.setLanguage(shouldDisplayCustomEmojis() ? new EmojiLanguage() : language)
|
||||
.load();
|
||||
}
|
||||
|
||||
/**
|
||||
* loadStatic
|
||||
* Loads words that are not in the database and are supposed to be in the same order, such as
|
||||
* emoji or the preferred character for double "0". Returns "false", when there are no static
|
||||
* options for the current digitSequence.
|
||||
*/
|
||||
private boolean loadStaticSuggestions() {
|
||||
if (digitSequence.equals(NaturalLanguage.PUNCTUATION_KEY) || digitSequence.equals(NaturalLanguage.SPECIAL_CHAR_KEY)) {
|
||||
loadSpecialCharacters();
|
||||
onSuggestionsUpdated.run();
|
||||
return true;
|
||||
} else if (!digitSequence.equals(EmojiLanguage.CUSTOM_EMOJI_SEQUENCE) && digitSequence.startsWith(EmojiLanguage.EMOJI_SEQUENCE)) {
|
||||
suggestions.clear();
|
||||
suggestions.addAll(new EmojiLanguage().getKeyCharacters(digitSequence.charAt(0) - '0', digitSequence.length() - 2));
|
||||
onSuggestionsUpdated.run();
|
||||
return true;
|
||||
} else if (digitSequence.startsWith(NaturalLanguage.PREFERRED_CHAR_SEQUENCE)) {
|
||||
|
||||
private boolean loadPreferredChar() {
|
||||
if (digitSequence.startsWith(NaturalLanguage.PREFERRED_CHAR_SEQUENCE)) {
|
||||
suggestions.clear();
|
||||
suggestions.add(settings.getDoubleZeroChar());
|
||||
onSuggestionsUpdated.run();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -313,37 +291,6 @@ public class ModePredictive extends InputMode {
|
|||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected boolean loadSpecialCharacters() {
|
||||
int number = digitSequence.charAt(0) - '0';
|
||||
if (KEY_CHARACTERS.size() > number) {
|
||||
suggestions.clear();
|
||||
suggestions.addAll(KEY_CHARACTERS.get(number));
|
||||
return true;
|
||||
} else {
|
||||
return super.loadSpecialCharacters();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* onPredictions
|
||||
* Gets the currently available Predictions and sends them over to the external caller.
|
||||
*/
|
||||
private void onPredictions() {
|
||||
// in case the user hasn't added any custom emoji, do not allow advancing to the empty character group
|
||||
if (predictions.getList().isEmpty() && digitSequence.startsWith(EmojiLanguage.EMOJI_SEQUENCE)) {
|
||||
digitSequence = EmojiLanguage.EMOJI_SEQUENCE;
|
||||
return;
|
||||
}
|
||||
|
||||
suggestions.clear();
|
||||
suggestions.addAll(predictions.getList());
|
||||
|
||||
onSuggestionsUpdated.run();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* onAcceptSuggestion
|
||||
* Bring this word up in the suggestions list next time and if necessary preserves the suggestion list
|
||||
|
|
@ -365,26 +312,21 @@ public class ModePredictive extends InputMode {
|
|||
return;
|
||||
}
|
||||
|
||||
if (Characters.isStaticEmoji(currentWord)) {
|
||||
// emojis and special chars are not in the database, so there is no point in wasting resources
|
||||
// running queries on them
|
||||
if (!new Text(currentWord).isAlphabetic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// increment the frequency of the given word
|
||||
try {
|
||||
Language workingLanguage = TextTools.isGraphic(currentWord) ? new EmojiLanguage() : language;
|
||||
String sequence = workingLanguage.getDigitSequenceForWord(currentWord);
|
||||
|
||||
// punctuation and special chars are not in the database, so there is no point in
|
||||
// running queries that would update nothing
|
||||
if (!sequence.equals(NaturalLanguage.PUNCTUATION_KEY) && !sequence.startsWith(NaturalLanguage.SPECIAL_CHAR_KEY)) {
|
||||
predictions.onAccept(currentWord, sequence);
|
||||
}
|
||||
// increment the frequency of the given word
|
||||
predictions.onAccept(currentWord, language.getDigitSequenceForWord(currentWord));
|
||||
} catch (Exception e) {
|
||||
Logger.e(LOG_TAG, "Failed incrementing priority of word: '" + currentWord + "'. " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected String adjustSuggestionTextCase(String word, int newTextCase) {
|
||||
return autoTextCase.adjustSuggestionTextCase(new Text(language, word), newTextCase);
|
||||
|
|
@ -395,17 +337,17 @@ public class ModePredictive extends InputMode {
|
|||
textCase = autoTextCase.determineNextWordTextCase(textCase, textFieldTextCase, textBeforeCursor, digitSequence);
|
||||
}
|
||||
|
||||
private void determineTextFieldTextCase() {
|
||||
int fieldCase = inputType.determineTextCase();
|
||||
textFieldTextCase = allowedTextCases.contains(fieldCase) ? fieldCase : CASE_UNDEFINED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTextCase() {
|
||||
// Filter out the internally used text cases. They have no meaning outside this class.
|
||||
return (textCase == CASE_UPPER || textCase == CASE_LOWER) ? textCase : CASE_CAPITALIZE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean nextSpecialCharacters() {
|
||||
return digitSequence.equals(NaturalLanguage.SPECIAL_CHAR_KEY) && super.nextSpecialCharacters();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean nextTextCase() {
|
||||
int before = textCase;
|
||||
|
|
@ -424,6 +366,11 @@ public class ModePredictive extends InputMode {
|
|||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean shouldReplaceLastLetter(int n, boolean h) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* shouldAcceptPreviousSuggestion
|
||||
|
|
@ -436,7 +383,7 @@ public class ModePredictive extends InputMode {
|
|||
return true;
|
||||
}
|
||||
|
||||
final char SPECIAL_CHAR_KEY_CODE = NaturalLanguage.SPECIAL_CHAR_KEY.charAt(0);
|
||||
final char SPECIAL_CHAR_KEY_CODE = SPECIAL_CHAR_SEQUENCE.charAt(0);
|
||||
final int SPECIAL_CHAR_KEY = SPECIAL_CHAR_KEY_CODE - '0';
|
||||
|
||||
// Prevent typing the preferred character when the user has scrolled the special char suggestions.
|
||||
|
|
@ -456,7 +403,7 @@ public class ModePredictive extends InputMode {
|
|||
|
||||
/**
|
||||
* shouldAcceptPreviousSuggestion
|
||||
* Variant for post suggestion load analysis.
|
||||
* Used for analysis after loading the suggestions.
|
||||
*/
|
||||
@Override
|
||||
public boolean shouldAcceptPreviousSuggestion(String unacceptedText) {
|
||||
|
|
@ -473,8 +420,8 @@ public class ModePredictive extends InputMode {
|
|||
return
|
||||
!digitSequence.isEmpty()
|
||||
&& predictions.noDbWords()
|
||||
&& digitSequence.contains(NaturalLanguage.PUNCTUATION_KEY)
|
||||
&& !digitSequence.startsWith(EmojiLanguage.EMOJI_SEQUENCE)
|
||||
&& digitSequence.contains(PUNCTUATION_SEQUENCE)
|
||||
&& !digitSequence.startsWith(EMOJI_SEQUENCE)
|
||||
&& Text.containsOtherThan1(digitSequence);
|
||||
}
|
||||
|
||||
|
|
@ -498,24 +445,6 @@ public class ModePredictive extends InputMode {
|
|||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean shouldAddTrailingSpace(InputType inputType, TextField textField, boolean isWordAcceptedManually, int nextKey) {
|
||||
return autoSpace.shouldAddTrailingSpace(textField, inputType, isWordAcceptedManually, nextKey);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean shouldAddPrecedingSpace(InputType inputType, TextField textField) {
|
||||
return autoSpace.shouldAddBeforePunctuation(inputType, textField);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean shouldDeletePrecedingSpace(InputType inputType, TextField textField) {
|
||||
return autoSpace.shouldDeletePrecedingSpace(inputType, textField);
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
|
|
@ -21,11 +21,13 @@ public class AutoSpace {
|
|||
private final SettingsStore settings;
|
||||
|
||||
private boolean isLanguageFrench;
|
||||
private boolean isLanguageWithAlphabet;
|
||||
private boolean isLanguageWithSpaceBetweenWords;
|
||||
|
||||
|
||||
public AutoSpace(SettingsStore settingsStore) {
|
||||
settings = settingsStore;
|
||||
isLanguageWithAlphabet = false;
|
||||
isLanguageFrench = false;
|
||||
isLanguageWithSpaceBetweenWords = true;
|
||||
}
|
||||
|
|
@ -33,6 +35,7 @@ public class AutoSpace {
|
|||
|
||||
public AutoSpace setLanguage(Language language) {
|
||||
isLanguageFrench = LanguageKind.isFrench(language);
|
||||
isLanguageWithAlphabet = language != null && !language.isSyllabary();
|
||||
isLanguageWithSpaceBetweenWords = language != null && language.hasSpaceBetweenWords();
|
||||
return this;
|
||||
}
|
||||
|
|
@ -120,6 +123,7 @@ public class AutoSpace {
|
|||
private boolean shouldAddAfterWord(boolean isWordAcceptedManually, String previousChars, Text nextChars, int nextKey) {
|
||||
return
|
||||
isWordAcceptedManually // Do not add space when auto-accepting words, because it feels very confusing when typing.
|
||||
&& isLanguageWithAlphabet
|
||||
&& nextKey != 1
|
||||
&& nextChars.isEmpty()
|
||||
&& Text.previousIsLetter(previousChars);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
package io.github.sspanak.tt9.ime.modes.helpers;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class Cheonjiin {
|
||||
private static final Pattern MEDIAL_VOWEL = Pattern.compile("^[4-9|0]+[1-3]+[4-9|0]+$");
|
||||
|
||||
public static boolean isThereMediaVowel(@NonNull String digitSequence) {
|
||||
return !digitSequence.isEmpty() && MEDIAL_VOWEL.matcher(digitSequence).find();
|
||||
}
|
||||
|
||||
private static boolean isVowelDigit(char digit) {
|
||||
return digit == '1' || digit == '2' || digit == '3';
|
||||
}
|
||||
|
||||
public static boolean isSingleJamo(@NonNull String digitSequence) {
|
||||
int digits = digitSequence.length();
|
||||
|
||||
if (digits == 0 || digits > 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
char firstDigit = digitSequence.charAt(0);
|
||||
for (int i = 1; i < digits; i++) {
|
||||
if (digitSequence.charAt(i) != firstDigit) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isVowelDigit(int digit) {
|
||||
return digit == 1 || digit == 2 || digit == 3;
|
||||
}
|
||||
|
||||
public static boolean endsWithTwoConsonants(@NonNull String digitSequence) {
|
||||
if (digitSequence.length() < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
char consonant1 = digitSequence.charAt(digitSequence.length() - 1);
|
||||
for (int i = digitSequence.length() - 2; i >= 0; i--) {
|
||||
if (!isVowelDigit(digitSequence.charAt(i))) {
|
||||
return consonant1 != digitSequence.charAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean endsWithDashVowel(@NonNull String digitSequence) {
|
||||
int lastDigit = digitSequence.isEmpty() ? -1 : digitSequence.charAt(digitSequence.length() - 1) - '0';
|
||||
return lastDigit == 1 || lastDigit == 3;
|
||||
}
|
||||
|
||||
public static int getRepeatingEndingDigits(@NonNull String digitSequence) {
|
||||
int count = 0;
|
||||
for (int i = digitSequence.length() - 1; i >= 0; i--) {
|
||||
if (digitSequence.charAt(i) == digitSequence.charAt(digitSequence.length() - 1)) {
|
||||
count++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public static String stripRepeatingEndingDigits(@NonNull String digitSequence) {
|
||||
int end = digitSequence.length() - getRepeatingEndingDigits(digitSequence);
|
||||
return digitSequence.length() > 1 ? digitSequence.substring(0, end) : digitSequence;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
package io.github.sspanak.tt9.ime.modes.predictions;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import io.github.sspanak.tt9.db.DataStore;
|
||||
import io.github.sspanak.tt9.db.words.WordStore;
|
||||
import io.github.sspanak.tt9.languages.Language;
|
||||
import io.github.sspanak.tt9.languages.NullLanguage;
|
||||
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
|
||||
|
||||
abstract public class Predictions {
|
||||
protected final SettingsStore settings;
|
||||
|
||||
// settings
|
||||
@NonNull protected String digitSequence = "";
|
||||
@NonNull protected Language language = new NullLanguage();
|
||||
protected int minWords = SettingsStore.SUGGESTIONS_MIN;
|
||||
protected int maxWords = SettingsStore.SUGGESTIONS_MAX;
|
||||
protected boolean onlyExactMatches = false;
|
||||
@NonNull protected String stem = "";
|
||||
|
||||
// async operations
|
||||
protected Runnable onWordsChanged = () -> {};
|
||||
|
||||
// data
|
||||
protected boolean areThereDbWords = false;
|
||||
protected boolean containsGeneratedWords = false;
|
||||
@NonNull protected ArrayList<String> words = new ArrayList<>();
|
||||
|
||||
|
||||
public Predictions(SettingsStore settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
|
||||
public Predictions setDigitSequence(String digitSequence) {
|
||||
this.digitSequence = digitSequence;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public Predictions setLanguage(Language language) {
|
||||
this.language = language;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public Predictions setMinWords(int minWords) {
|
||||
this.minWords = minWords;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public Predictions setOnlyExactMatches(boolean onlyExactMatches) {
|
||||
this.onlyExactMatches = onlyExactMatches;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public void setWordsChangedHandler(Runnable handler) {
|
||||
onWordsChanged = handler;
|
||||
}
|
||||
|
||||
|
||||
public boolean containsGeneratedWords() {
|
||||
return containsGeneratedWords;
|
||||
}
|
||||
|
||||
|
||||
public ArrayList<String> getList() {
|
||||
return words;
|
||||
}
|
||||
|
||||
|
||||
public boolean noDbWords() {
|
||||
return !areThereDbWords;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* suggestMissingWords
|
||||
* Takes a list of words and appends them to the words list, if they are missing.
|
||||
*/
|
||||
protected void suggestMissingWords(ArrayList<String> newWords) {
|
||||
for (String newWord : newWords) {
|
||||
if (!words.contains(newWord) && !words.contains(newWord.toLowerCase(language.getLocale()))) {
|
||||
words.add(newWord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* load
|
||||
* Queries the dictionary database for a list of words matching the current language and sequence.
|
||||
*/
|
||||
public void load() {
|
||||
containsGeneratedWords = false;
|
||||
|
||||
if (digitSequence.isEmpty()) {
|
||||
words.clear();
|
||||
onWordsChanged.run();
|
||||
return;
|
||||
}
|
||||
|
||||
DataStore.getWords(
|
||||
(dbWords) -> onDbWords(dbWords, isRetryAllowed()),
|
||||
language,
|
||||
digitSequence,
|
||||
onlyExactMatches ? WordStore.FILTER_EXACT_MATCHES_ONLY : stem,
|
||||
minWords,
|
||||
maxWords
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
abstract public void onAccept(String word, String sequence);
|
||||
abstract protected boolean isRetryAllowed();
|
||||
abstract protected void onDbWords(ArrayList<String> dbWords, boolean retryAllowed);
|
||||
abstract protected ArrayList<String> generateWordVariations(String baseWord);
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
package io.github.sspanak.tt9.ime.modes.predictions;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import io.github.sspanak.tt9.ime.modes.helpers.Cheonjiin;
|
||||
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
|
||||
|
||||
public class SyllablePredictions extends Predictions {
|
||||
int defaultMinWords;
|
||||
int loadAttempts;
|
||||
String lastWord = "";
|
||||
String lastStableWord = "";
|
||||
int lastStableSequenceLength;
|
||||
|
||||
|
||||
public SyllablePredictions(SettingsStore settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Predictions setMinWords(int minWords) {
|
||||
defaultMinWords = minWords;
|
||||
return super.setMinWords(minWords);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected boolean isRetryAllowed() {
|
||||
return loadAttempts == 0;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
loadAttempts = 0;
|
||||
minWords = defaultMinWords;
|
||||
super.load();
|
||||
}
|
||||
|
||||
|
||||
private void loadSimilar() {
|
||||
loadAttempts++;
|
||||
minWords = defaultMinWords + 1;
|
||||
super.load();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onDbWords(ArrayList<String> dbWords, boolean retryAllowed) {
|
||||
areThereDbWords = !dbWords.isEmpty();
|
||||
|
||||
if (loadAttempts == 0) {
|
||||
words.clear();
|
||||
} else {
|
||||
onWordsChanged.run();
|
||||
return;
|
||||
}
|
||||
|
||||
if (digitSequence.length() < lastStableSequenceLength) {
|
||||
lastStableWord = "";
|
||||
lastStableSequenceLength = 0;
|
||||
}
|
||||
|
||||
if (areThereDbWords) {
|
||||
lastWord = dbWords.get(0);
|
||||
words.addAll(dbWords);
|
||||
} else {
|
||||
if (lastStableWord.isEmpty() && !lastWord.isEmpty()) {
|
||||
lastStableWord = lastWord;
|
||||
lastStableSequenceLength = digitSequence.length();
|
||||
}
|
||||
lastWord = "";
|
||||
words.addAll(generateWordVariations(lastStableWord));
|
||||
}
|
||||
|
||||
if (retryAllowed && !areThereDbWords) {
|
||||
loadSimilar();
|
||||
return;
|
||||
}
|
||||
|
||||
onWordsChanged.run();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected ArrayList<String> generateWordVariations(String baseWord) {
|
||||
baseWord = baseWord == null ? "" : baseWord;
|
||||
ArrayList<String> variants = new ArrayList<>();
|
||||
|
||||
try {
|
||||
int charIndex = Cheonjiin.getRepeatingEndingDigits(digitSequence) - 1;
|
||||
int key = digitSequence.charAt(digitSequence.length() - 1) - '0';
|
||||
String variant = baseWord + language.getKeyCharacters(key).get(charIndex);
|
||||
variants.add(variant);
|
||||
} catch (Exception ignored) {
|
||||
variants.add(baseWord);
|
||||
}
|
||||
|
||||
return variants;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onAccept(String word, String sequence) {
|
||||
lastWord = lastStableWord = "";
|
||||
lastStableSequenceLength = 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,109 +1,46 @@
|
|||
package io.github.sspanak.tt9.ime.modes.helpers;
|
||||
package io.github.sspanak.tt9.ime.modes.predictions;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import io.github.sspanak.tt9.db.DataStore;
|
||||
import io.github.sspanak.tt9.ime.helpers.TextField;
|
||||
import io.github.sspanak.tt9.languages.EmojiLanguage;
|
||||
import io.github.sspanak.tt9.languages.Language;
|
||||
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
|
||||
import io.github.sspanak.tt9.util.Characters;
|
||||
|
||||
public class Predictions {
|
||||
private final SettingsStore settings;
|
||||
public class WordPredictions extends Predictions {
|
||||
private final TextField textField;
|
||||
|
||||
private String digitSequence;
|
||||
private String inputWord;
|
||||
private boolean isStemFuzzy;
|
||||
private Language language;
|
||||
private String stem;
|
||||
|
||||
private String lastEnforcedTopWord = "";
|
||||
|
||||
// async operations
|
||||
private Runnable onWordsChanged = () -> {};
|
||||
|
||||
// data
|
||||
private boolean areThereDbWords = false;
|
||||
private boolean containsGeneratedWords = false;
|
||||
private ArrayList<String> words = new ArrayList<>();
|
||||
|
||||
public Predictions(SettingsStore settings, TextField textField) {
|
||||
this.settings = settings;
|
||||
public WordPredictions(SettingsStore settings, TextField textField) {
|
||||
super(settings);
|
||||
stem = "";
|
||||
this.textField = textField;
|
||||
}
|
||||
|
||||
|
||||
public Predictions setLanguage(Language language) {
|
||||
this.language = language;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Predictions setDigitSequence(String digitSequence) {
|
||||
this.digitSequence = digitSequence;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Predictions setIsStemFuzzy(boolean yes) {
|
||||
public WordPredictions setIsStemFuzzy(boolean yes) {
|
||||
this.isStemFuzzy = yes;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Predictions setStem(String stem) {
|
||||
|
||||
public WordPredictions setStem(String stem) {
|
||||
this.stem = stem;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Predictions setInputWord(String inputWord) {
|
||||
|
||||
public WordPredictions setInputWord(String inputWord) {
|
||||
this.inputWord = inputWord.toLowerCase(language.getLocale());
|
||||
return this;
|
||||
}
|
||||
|
||||
public Predictions setWordsChangedHandler(Runnable handler) {
|
||||
onWordsChanged = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean containsGeneratedWords() {
|
||||
return containsGeneratedWords;
|
||||
}
|
||||
|
||||
public ArrayList<String> getList() {
|
||||
return words;
|
||||
}
|
||||
|
||||
public boolean noDbWords() {
|
||||
return !areThereDbWords;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* load
|
||||
* Queries the dictionary database for a list of words matching the current language and
|
||||
* sequence or loads the static ones.
|
||||
*/
|
||||
public void load() {
|
||||
containsGeneratedWords = false;
|
||||
|
||||
if (digitSequence == null || digitSequence.isEmpty()) {
|
||||
words.clear();
|
||||
onWordsChanged.run();
|
||||
return;
|
||||
}
|
||||
|
||||
boolean retryAllowed = !digitSequence.equals(EmojiLanguage.CUSTOM_EMOJI_SEQUENCE);
|
||||
|
||||
DataStore.getWords(
|
||||
(dbWords) -> onDbWords(dbWords, retryAllowed),
|
||||
language,
|
||||
digitSequence,
|
||||
stem,
|
||||
SettingsStore.SUGGESTIONS_MIN,
|
||||
SettingsStore.SUGGESTIONS_MAX
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
private void loadWithoutLeadingPunctuation() {
|
||||
DataStore.getWords(
|
||||
|
|
@ -123,13 +60,18 @@ public class Predictions {
|
|||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected boolean isRetryAllowed() {
|
||||
return !EmojiLanguage.CUSTOM_EMOJI_SEQUENCE.equals(digitSequence);
|
||||
}
|
||||
|
||||
/**
|
||||
* dbWordsHandler
|
||||
* Callback for when the database has finished loading words. If there were no matches in the database,
|
||||
* they will be generated based on the "inputWord". After the word list is compiled, it notifies the
|
||||
* external handler it is now possible to use it with "getList()".
|
||||
*/
|
||||
private void onDbWords(ArrayList<String> dbWords, boolean isRetryAllowed) {
|
||||
protected void onDbWords(ArrayList<String> dbWords, boolean isRetryAllowed) {
|
||||
// only the first round matters, the second one is just for getting the letters for a given key
|
||||
areThereDbWords = !dbWords.isEmpty() && isRetryAllowed;
|
||||
|
||||
|
|
@ -167,29 +109,15 @@ public class Predictions {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* suggestMissingWords
|
||||
* Takes a list of words and appends them to the words list, if they are missing.
|
||||
*/
|
||||
private void suggestMissingWords(ArrayList<String> newWords) {
|
||||
for (String newWord : newWords) {
|
||||
if (!words.contains(newWord) && !words.contains(newWord.toLowerCase(language.getLocale()))) {
|
||||
words.add(newWord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* generateWordVariations
|
||||
* When there are no matching suggestions after the last key press, generate a list of possible
|
||||
* ones, so that the user can complete a missing word that is completely different from the ones
|
||||
* in the dictionary.
|
||||
*
|
||||
* For example, if the word is "missin_" and the last pressed key is "4", the results would be:
|
||||
* | missing | missinh | missini |
|
||||
*/
|
||||
private ArrayList<String> generateWordVariations(String baseWord) {
|
||||
protected ArrayList<String> generateWordVariations(String baseWord) {
|
||||
ArrayList<String> generatedWords = new ArrayList<>();
|
||||
|
||||
// This function is called from async context, so by the time it is executed, the digit sequence
|
||||
|
|
@ -265,9 +193,9 @@ public class Predictions {
|
|||
* Similar to generatePossibleCompletions(), but uses the current filter as a base word. This is
|
||||
* used to complement the database results with all possible variations for the next key, when
|
||||
* the stem filter is on.
|
||||
*
|
||||
* <p>
|
||||
* It will not generate anything if more than one key was pressed after filtering though.
|
||||
*
|
||||
* <p>
|
||||
* For example, if the filter is "extr", the current word is "extr_" and the user has pressed "1",
|
||||
* the database would have returned only "extra", but this function would also
|
||||
* generate: "extrb" and "extrc". This is useful for typing an unknown word, that is similar to
|
||||
|
|
@ -312,7 +240,7 @@ public class Predictions {
|
|||
!settings.getPredictWordPairs()
|
||||
// If the accepted word is longer than the sequence, it is some different word, not a textonym
|
||||
// of the fist suggestion. We don't need to store it.
|
||||
|| word == null || digitSequence == null
|
||||
|| word == null
|
||||
|| word.length() != digitSequence.length()
|
||||
// If the word is the first suggestion, we have already guessed it right, and it makes no
|
||||
// sense to store it as a popular pair.
|
||||
|
|
@ -16,6 +16,7 @@ abstract public class Language {
|
|||
protected String name;
|
||||
protected boolean hasSpaceBetweenWords = true;
|
||||
protected boolean hasUpperCase = true;
|
||||
protected boolean isSyllabary = false;
|
||||
|
||||
|
||||
public int getId() {
|
||||
|
|
@ -65,6 +66,10 @@ abstract public class Language {
|
|||
return hasUpperCase;
|
||||
}
|
||||
|
||||
final public boolean isSyllabary() {
|
||||
return isSyllabary;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
final public String toString() {
|
||||
|
|
|
|||
|
|
@ -30,19 +30,17 @@ public class LanguageCollection {
|
|||
}
|
||||
|
||||
|
||||
public static LanguageCollection getInstance(Context context) {
|
||||
public static void init(Context context) {
|
||||
if (self == null) {
|
||||
self = new LanguageCollection(context);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
@Nullable
|
||||
public static NaturalLanguage getLanguage(Context context, String langId) {
|
||||
public static NaturalLanguage getLanguage(String langId) {
|
||||
try {
|
||||
return getLanguage(context, Integer.parseInt(langId));
|
||||
return getLanguage(Integer.parseInt(langId));
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -50,23 +48,23 @@ public class LanguageCollection {
|
|||
|
||||
|
||||
@Nullable
|
||||
public static NaturalLanguage getLanguage(Context context, int langId) {
|
||||
if (getInstance(context).languages.containsKey(langId)) {
|
||||
return getInstance(context).languages.get(langId);
|
||||
public static NaturalLanguage getLanguage(int langId) {
|
||||
if (self.languages.containsKey(langId)) {
|
||||
return self.languages.get(langId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@NonNull public static Language getDefault(Context context) {
|
||||
Language language = getByLocale(context, SystemSettings.getLocale());
|
||||
language = language == null ? getByLocale(context, "en") : language;
|
||||
@NonNull public static Language getDefault() {
|
||||
Language language = getByLocale(SystemSettings.getLocale());
|
||||
language = language == null ? getByLocale("en") : language;
|
||||
return language == null ? new NullLanguage() : language;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static NaturalLanguage getByLanguageCode(Context context, String languageCode) {
|
||||
for (NaturalLanguage lang : getInstance(context).languages.values()) {
|
||||
public static NaturalLanguage getByLanguageCode(String languageCode) {
|
||||
for (NaturalLanguage lang : self.languages.values()) {
|
||||
if (lang.getLocale().getLanguage().equals(new Locale(languageCode).getLanguage())) {
|
||||
return lang;
|
||||
}
|
||||
|
|
@ -76,8 +74,8 @@ public class LanguageCollection {
|
|||
}
|
||||
|
||||
@Nullable
|
||||
public static NaturalLanguage getByLocale(Context context, String locale) {
|
||||
for (NaturalLanguage lang : getInstance(context).languages.values()) {
|
||||
public static NaturalLanguage getByLocale(String locale) {
|
||||
for (NaturalLanguage lang : self.languages.values()) {
|
||||
if (lang.getLocale().toString().equals(locale)) {
|
||||
return lang;
|
||||
}
|
||||
|
|
@ -86,14 +84,14 @@ public class LanguageCollection {
|
|||
return null;
|
||||
}
|
||||
|
||||
public static ArrayList<Language> getAll(Context context, ArrayList<Integer> languageIds, boolean sort) {
|
||||
public static ArrayList<Language> getAll(ArrayList<Integer> languageIds, boolean sort) {
|
||||
if (languageIds == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
ArrayList<NaturalLanguage> langList = new ArrayList<>();
|
||||
for (int languageId : languageIds) {
|
||||
NaturalLanguage lang = getLanguage(context, languageId);
|
||||
NaturalLanguage lang = getLanguage(languageId);
|
||||
if (lang != null) {
|
||||
langList.add(lang);
|
||||
}
|
||||
|
|
@ -106,12 +104,12 @@ public class LanguageCollection {
|
|||
return new ArrayList<>(langList);
|
||||
}
|
||||
|
||||
public static ArrayList<Language> getAll(Context context, ArrayList<Integer> languageIds) {
|
||||
return getAll(context, languageIds, false);
|
||||
public static ArrayList<Language> getAll(ArrayList<Integer> languageIds) {
|
||||
return getAll(languageIds, false);
|
||||
}
|
||||
|
||||
public static ArrayList<Language> getAll(Context context, boolean sort) {
|
||||
ArrayList<NaturalLanguage> langList = new ArrayList<>(getInstance(context).languages.values());
|
||||
public static ArrayList<Language> getAll(boolean sort) {
|
||||
ArrayList<NaturalLanguage> langList = new ArrayList<>(self.languages.values());
|
||||
|
||||
if (sort) {
|
||||
Collections.sort(langList);
|
||||
|
|
@ -120,8 +118,8 @@ public class LanguageCollection {
|
|||
return new ArrayList<>(langList);
|
||||
}
|
||||
|
||||
public static ArrayList<Language> getAll(Context context) {
|
||||
return getAll(context,false);
|
||||
public static ArrayList<Language> getAll() {
|
||||
return getAll(false);
|
||||
}
|
||||
|
||||
public static String toString(ArrayList<Language> list) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import io.github.sspanak.tt9.util.AssetFile;
|
|||
import io.github.sspanak.tt9.util.Logger;
|
||||
|
||||
public class LanguageDefinition extends AssetFile {
|
||||
private static final String LOG_TAG = LanguageDefinition.class.getSimpleName();
|
||||
|
||||
private static final String languagesDir = "languages";
|
||||
private static final String definitionsDir = languagesDir + "/definitions";
|
||||
|
||||
|
|
@ -22,6 +24,7 @@ public class LanguageDefinition extends AssetFile {
|
|||
public String dictionaryFile = "";
|
||||
public boolean hasSpaceBetweenWords = true;
|
||||
public boolean hasUpperCase = true;
|
||||
public boolean isSyllabary = false;
|
||||
public ArrayList<ArrayList<String>> layout = new ArrayList<>();
|
||||
public String locale = "";
|
||||
public String name = "";
|
||||
|
|
@ -40,9 +43,9 @@ public class LanguageDefinition extends AssetFile {
|
|||
ArrayList<String> files = new ArrayList<>();
|
||||
try {
|
||||
files.addAll(Arrays.asList(assets.list(definitionsDir)));
|
||||
Logger.d("LanguageDefinition", "Found: " + files.size() + " languages.");
|
||||
Logger.d(LOG_TAG, "Found: " + files.size() + " languages.");
|
||||
} catch (IOException | NullPointerException e) {
|
||||
Logger.e("tt9.LanguageDefinition", "Failed reading language definitions from: '" + definitionsDir + "'. " + e.getMessage());
|
||||
Logger.e(LOG_TAG, "Failed reading language definitions from: '" + definitionsDir + "'. " + e.getMessage());
|
||||
}
|
||||
|
||||
return files;
|
||||
|
|
@ -95,6 +98,7 @@ public class LanguageDefinition extends AssetFile {
|
|||
|
||||
hasSpaceBetweenWords = getPropertyFromYaml(yaml, "hasSpaceBetweenWords", hasSpaceBetweenWords);
|
||||
hasUpperCase = getPropertyFromYaml(yaml, "hasUpperCase", hasUpperCase);
|
||||
isSyllabary = hasYamlProperty(yaml, "sounds");
|
||||
layout = getLayoutFromYaml(yaml);
|
||||
locale = getPropertyFromYaml(yaml, "locale", locale);
|
||||
name = getPropertyFromYaml(yaml, "name", name);
|
||||
|
|
@ -126,6 +130,19 @@ public class LanguageDefinition extends AssetFile {
|
|||
}
|
||||
|
||||
|
||||
private boolean hasYamlProperty(ArrayList<String> yaml, String property) {
|
||||
final String yamlProperty = property + ":";
|
||||
|
||||
for (String line : yaml) {
|
||||
if (line.startsWith(yamlProperty)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The boolean variant of getPropertyFromYaml. It returns true if the property is found and is:
|
||||
* "true", "on", "yes" or "y".
|
||||
|
|
|
|||
|
|
@ -1,13 +1,19 @@
|
|||
package io.github.sspanak.tt9.languages;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class LanguageKind {
|
||||
public static final int KOREAN = 601579;
|
||||
|
||||
public static boolean isArabic(Language language) { return language != null && language.getId() == 502337; }
|
||||
public static boolean isBulgarian(Language language) { return language != null && language.getId() == 231650; }
|
||||
public static boolean isCyrillic(Language language) { return language != null && language.getKeyCharacters(2).contains("а"); }
|
||||
public static boolean isEnglish(Language language) { return language != null && language.getLocale().equals(Locale.ENGLISH); }
|
||||
public static boolean isFrench(Language language) { return language != null && language.getId() == 596550; }
|
||||
public static boolean isGreek(Language language) { return language != null && language.getId() == 597381; }
|
||||
public static boolean isHebrew(Language language) { return language != null && (language.getId() == 305450 || language.getId() == 403177); }
|
||||
public static boolean isHinglish(Language language) { return language != null && language.getId() == 468421; }
|
||||
public static boolean isKorean(Language language) { return language != null && language.getId() == KOREAN; }
|
||||
public static boolean isLatinBased(Language language) { return language != null && language.getKeyCharacters(2).contains("a"); }
|
||||
public static boolean isRTL(Language language) { return isArabic(language) || isHebrew(language); }
|
||||
public static boolean isUkrainian(Language language) { return language != null && language.getId() == 54645; }
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import java.util.Locale;
|
|||
import io.github.sspanak.tt9.languages.exceptions.InvalidLanguageCharactersException;
|
||||
import io.github.sspanak.tt9.util.Characters;
|
||||
import io.github.sspanak.tt9.util.Text;
|
||||
import io.github.sspanak.tt9.util.TextTools;
|
||||
|
||||
|
||||
public class NaturalLanguage extends Language implements Comparable<NaturalLanguage> {
|
||||
|
|
@ -31,6 +32,7 @@ public class NaturalLanguage extends Language implements Comparable<NaturalLangu
|
|||
lang.dictionaryFile = definition.getDictionaryFile();
|
||||
lang.hasSpaceBetweenWords = definition.hasSpaceBetweenWords;
|
||||
lang.hasUpperCase = definition.hasUpperCase;
|
||||
lang.isSyllabary = definition.isSyllabary;
|
||||
lang.name = definition.name.isEmpty() ? lang.name : definition.name;
|
||||
lang.setLocale(definition);
|
||||
lang.setLayout(definition);
|
||||
|
|
@ -78,6 +80,7 @@ public class NaturalLanguage extends Language implements Comparable<NaturalLangu
|
|||
final String FRENCH_PUNCTUATION_STYLE = PUNCTUATION_PLACEHOLDER + "_FR";
|
||||
final String GERMAN_PUNCTUATION_STYLE = PUNCTUATION_PLACEHOLDER + "_DE";
|
||||
final String GREEK_PUNCTUATION_STYLE = PUNCTUATION_PLACEHOLDER + "_GR";
|
||||
final String KOREAN_PUNCTUATION_STYLE = PUNCTUATION_PLACEHOLDER + "_KR";
|
||||
|
||||
ArrayList<String> keyChars = new ArrayList<>();
|
||||
for (String defChar : definitionChars) {
|
||||
|
|
@ -100,6 +103,9 @@ public class NaturalLanguage extends Language implements Comparable<NaturalLangu
|
|||
case GREEK_PUNCTUATION_STYLE:
|
||||
keyChars.addAll(Characters.PunctuationGreek);
|
||||
break;
|
||||
case KOREAN_PUNCTUATION_STYLE:
|
||||
keyChars.addAll(Characters.PunctuationKorean);
|
||||
break;
|
||||
default:
|
||||
keyChars.add(defChar);
|
||||
break;
|
||||
|
|
@ -263,7 +269,12 @@ public class NaturalLanguage extends Language implements Comparable<NaturalLangu
|
|||
|
||||
|
||||
public boolean isValidWord(String word) {
|
||||
if (word == null || word.isEmpty() || (word.length() == 1 && Character.isDigit(word.charAt(0)))) {
|
||||
if (
|
||||
word == null
|
||||
|| word.isEmpty()
|
||||
|| (isSyllabary && LanguageKind.isKorean(this) && TextTools.isHangul(word))
|
||||
|| (word.length() == 1 && Character.isDigit(word.charAt(0)))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import io.github.sspanak.tt9.R;
|
|||
import io.github.sspanak.tt9.db.DataStore;
|
||||
import io.github.sspanak.tt9.db.words.LegacyDb;
|
||||
import io.github.sspanak.tt9.ime.helpers.InputModeValidator;
|
||||
import io.github.sspanak.tt9.languages.LanguageCollection;
|
||||
import io.github.sspanak.tt9.preferences.helpers.Hotkeys;
|
||||
import io.github.sspanak.tt9.preferences.screens.BaseScreenFragment;
|
||||
import io.github.sspanak.tt9.preferences.screens.UsageStatsScreen;
|
||||
|
|
@ -43,10 +44,11 @@ public class PreferencesActivity extends ActivityWithNavigation implements Prefe
|
|||
applyTheme();
|
||||
Logger.setLevel(settings.getLogLevel());
|
||||
|
||||
LanguageCollection.init(this);
|
||||
try (LegacyDb db = new LegacyDb(this)) { db.clear(); }
|
||||
DataStore.init(this);
|
||||
|
||||
InputModeValidator.validateEnabledLanguages(this, settings.getEnabledLanguageIds());
|
||||
InputModeValidator.validateEnabledLanguages(settings.getEnabledLanguageIds());
|
||||
validateFunctionKeys();
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ public class Hotkeys {
|
|||
defaultKeys.put(SectionKeymap.ITEM_NEXT_LANGUAGE, -KeyEvent.KEYCODE_POUND); // negative means "hold"
|
||||
defaultKeys.put(SectionKeymap.ITEM_SELECT_KEYBOARD, KeyEvent.KEYCODE_UNKNOWN);
|
||||
defaultKeys.put(SectionKeymap.ITEM_SHIFT, KeyEvent.KEYCODE_STAR);
|
||||
defaultKeys.put(SectionKeymap.ITEM_SPACE_KOREAN, KeyEvent.KEYCODE_STAR);
|
||||
defaultKeys.put(SectionKeymap.ITEM_SHOW_SETTINGS, KeyEvent.KEYCODE_UNKNOWN);
|
||||
defaultKeys.put(SectionKeymap.ITEM_VOICE_INPUT, KeyEvent.KEYCODE_UNKNOWN);
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ public class UsageStatsScreen extends BaseScreenFragment {
|
|||
|
||||
private boolean deleteWordPairs(Preference ignored) {
|
||||
DataStore.deleteWordPairs(
|
||||
LanguageCollection.getAll(activity),
|
||||
LanguageCollection.getAll(),
|
||||
() -> UI.toastLongFromAsync(activity, "Word pairs deleted. You must reopen the screen manually.")
|
||||
);
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ public class PreferenceDeletableWord extends ScreenPreference {
|
|||
SettingsStore settings = new SettingsStore(getContext());
|
||||
DataStore.deleteCustomWord(
|
||||
this::onWordDeleted,
|
||||
LanguageCollection.getLanguage(getContext(), settings.getInputLanguage()),
|
||||
LanguageCollection.getLanguage(settings.getInputLanguage()),
|
||||
word
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ public class HotkeysScreen extends BaseScreenFragment {
|
|||
findPreference(SectionKeymap.ITEM_NEXT_LANGUAGE),
|
||||
findPreference(SectionKeymap.ITEM_SELECT_KEYBOARD),
|
||||
findPreference(SectionKeymap.ITEM_SHIFT),
|
||||
findPreference(SectionKeymap.ITEM_SPACE_KOREAN),
|
||||
findPreference(SectionKeymap.ITEM_SHOW_SETTINGS),
|
||||
findPreference(SectionKeymap.ITEM_VOICE_INPUT),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ public class SectionKeymap {
|
|||
public static final String ITEM_NEXT_LANGUAGE = "key_next_language";
|
||||
public static final String ITEM_SELECT_KEYBOARD = "key_select_keyboard";
|
||||
public static final String ITEM_SHIFT = "key_shift";
|
||||
public static final String ITEM_SPACE_KOREAN = "key_space_korean";
|
||||
public static final String ITEM_SHOW_SETTINGS = "key_show_settings";
|
||||
public static final String ITEM_VOICE_INPUT = "key_voice_input";
|
||||
|
||||
|
|
@ -155,7 +156,22 @@ public class SectionKeymap {
|
|||
}
|
||||
|
||||
for (DropDownPreference item : items) {
|
||||
if (item != null && !dropDown.getKey().equals(item.getKey()) && key.equals(item.getValue())) {
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// "Shift" and "Korean Space" can be the same key. It is properly handled in HotkeyHandler.
|
||||
if (
|
||||
(
|
||||
(dropDown.getKey().equals(ITEM_SHIFT) && item.getKey().equals(ITEM_SPACE_KOREAN))
|
||||
|| (dropDown.getKey().equals(ITEM_SPACE_KOREAN) && item.getKey().equals(ITEM_SHIFT))
|
||||
)
|
||||
&& key.equals(item.getValue())
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!dropDown.getKey().equals(item.getKey()) && key.equals(item.getValue())) {
|
||||
Logger.i("SectionKeymap.validateKey", "Key: '" + key + "' is already in use for function: " + item.getKey());
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ public class LanguageSelectionScreen extends BaseScreenFragment {
|
|||
return;
|
||||
}
|
||||
|
||||
ArrayList<Language> allLanguages = LanguageCollection.getAll(activity, true);
|
||||
ArrayList<Language> allLanguages = LanguageCollection.getAll(true);
|
||||
if (allLanguages.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ public class PreferenceSwitchLanguage extends SwitchPreferenceCompat {
|
|||
);
|
||||
|
||||
// word count
|
||||
WordFile wordFile = new WordFile(activity, language.getDictionaryFile(), activity.getAssets());
|
||||
WordFile wordFile = new WordFile(activity, language, activity.getAssets());
|
||||
summary
|
||||
.append(", ")
|
||||
.append(
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class ItemExportDictionary extends ItemExportAbstract {
|
|||
|
||||
protected boolean onStartProcessing() {
|
||||
return DictionaryExporter.getInstance()
|
||||
.setLanguages(LanguageCollection.getAll(activity, activity.getSettings().getEnabledLanguageIds()))
|
||||
.setLanguages(LanguageCollection.getAll(activity.getSettings().getEnabledLanguageIds()))
|
||||
.run(activity);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class ItemLoadDictionary extends ItemClickable {
|
|||
|
||||
|
||||
private void onLoadingStatusChange(Bundle status) {
|
||||
progressBar.show(activity, status);
|
||||
progressBar.show(status);
|
||||
item.setSummary(progressBar.getTitle() + " " + progressBar.getMessage());
|
||||
|
||||
if (progressBar.isCancelled()) {
|
||||
|
|
@ -67,7 +67,7 @@ class ItemLoadDictionary extends ItemClickable {
|
|||
|
||||
@Override
|
||||
protected boolean onClick(Preference p) {
|
||||
ArrayList<Language> languages = LanguageCollection.getAll(activity, activity.getSettings().getEnabledLanguageIds());
|
||||
ArrayList<Language> languages = LanguageCollection.getAll(activity.getSettings().getEnabledLanguageIds());
|
||||
|
||||
setLoadingStatus();
|
||||
if (!loader.load(activity, languages)) {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class ItemSelectLanguage {
|
|||
}
|
||||
|
||||
item.setSummary(
|
||||
LanguageCollection.toString(LanguageCollection.getAll(activity, activity.getSettings().getEnabledLanguageIds(), true))
|
||||
LanguageCollection.toString(LanguageCollection.getAll(activity.getSettings().getEnabledLanguageIds(), true))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class ItemTruncateAll extends ItemClickable {
|
|||
@Override
|
||||
protected boolean onClick(Preference p) {
|
||||
onStartDeleting();
|
||||
DataStore.deleteLanguages(this::onFinishDeleting, LanguageCollection.getAll(activity, false));
|
||||
DataStore.deleteLanguages(this::onFinishDeleting, LanguageCollection.getAll(false));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class ItemTruncateUnselected extends ItemTruncateAll {
|
|||
protected boolean onClick(Preference p) {
|
||||
ArrayList<Language> unselectedLanguages = new ArrayList<>();
|
||||
Set<Integer> selectedLanguageIds = new HashSet<>(activity.getSettings().getEnabledLanguageIds());
|
||||
for (Language lang : LanguageCollection.getAll(activity, false)) {
|
||||
for (Language lang : LanguageCollection.getAll(false)) {
|
||||
if (!selectedLanguageIds.contains(lang.getId())) {
|
||||
unselectedLanguages.add(lang);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class ItemPunctuationOrderLanguage extends ItemDropDown {
|
|||
}
|
||||
|
||||
LinkedHashMap<String, String> values = new LinkedHashMap<>();
|
||||
ArrayList<Language> languages = LanguageCollection.getAll(item.getContext(), settings.getEnabledLanguageIds(), true);
|
||||
ArrayList<Language> languages = LanguageCollection.getAll(settings.getEnabledLanguageIds(), true);
|
||||
if (languages.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ public class PunctuationScreen extends BaseScreenFragment {
|
|||
|
||||
restoreDefaults = new ItemRestoreDefaultPunctuation(activity.getSettings(), item, this::onLanguageChanged);
|
||||
restoreDefaults
|
||||
.setLanguage(LanguageCollection.getLanguage(activity, languageList.getValue()))
|
||||
.setLanguage(LanguageCollection.getLanguage(languageList.getValue()))
|
||||
.enableClickHandler();
|
||||
}
|
||||
|
||||
|
|
@ -97,7 +97,7 @@ public class PunctuationScreen extends BaseScreenFragment {
|
|||
|
||||
|
||||
private void onLanguageChanged(@Nullable String newLanguageId) {
|
||||
Language language = LanguageCollection.getLanguage(activity, newLanguageId);
|
||||
Language language = LanguageCollection.getLanguage(newLanguageId);
|
||||
|
||||
restoreDefaults.setLanguage(language);
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ class SettingsHacks extends BaseSettings {
|
|||
|
||||
/************* hack settings *************/
|
||||
|
||||
public boolean holdForPunctuationInKorean() {
|
||||
return prefs.getBoolean("pref_hold_for_punctuation_in_korean", true);
|
||||
}
|
||||
|
||||
public int getSuggestionScrollingDelay() {
|
||||
boolean defaultOn = DeviceInfo.noTouchScreen(context) && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q;
|
||||
return prefs.getBoolean("pref_alternative_suggestion_scrolling", defaultOn) ? 200 : 0;
|
||||
|
|
|
|||
|
|
@ -66,6 +66,9 @@ class SettingsHotkeys extends SettingsHacks {
|
|||
public int getKeyShift() {
|
||||
return getFunctionKey(SectionKeymap.ITEM_SHIFT);
|
||||
}
|
||||
public int getKeySpaceKorean() {
|
||||
return getFunctionKey(SectionKeymap.ITEM_SPACE_KOREAN);
|
||||
}
|
||||
public int getKeyShowSettings() {
|
||||
return getFunctionKey(SectionKeymap.ITEM_SHOW_SETTINGS);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class SettingsInput extends SettingsHotkeys {
|
|||
|
||||
public Set<String> getEnabledLanguagesIdsAsStrings() {
|
||||
Set<String> defaultLanguages = new HashSet<>(Collections.singletonList(
|
||||
String.valueOf(LanguageCollection.getDefault(context).getId())
|
||||
String.valueOf(LanguageCollection.getDefault().getId())
|
||||
));
|
||||
|
||||
return new HashSet<>(prefs.getStringSet("pref_languages", defaultLanguages));
|
||||
|
|
@ -51,7 +51,7 @@ class SettingsInput extends SettingsHotkeys {
|
|||
Set<String> validLanguageIds = new HashSet<>();
|
||||
|
||||
for (String langId : languageIds) {
|
||||
if (!Validators.validateInputLanguage(context, Integer.parseInt(langId), "saveEnabledLanguageIds")){
|
||||
if (!Validators.validateInputLanguage(Integer.parseInt(langId), "saveEnabledLanguageIds")){
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -69,12 +69,12 @@ class SettingsInput extends SettingsHotkeys {
|
|||
|
||||
|
||||
public int getInputLanguage() {
|
||||
return prefs.getInt("pref_input_language", LanguageCollection.getDefault(context).getId());
|
||||
return prefs.getInt("pref_input_language", LanguageCollection.getDefault().getId());
|
||||
}
|
||||
|
||||
|
||||
public void saveInputLanguage(int language) {
|
||||
if (Validators.validateInputLanguage(context, language, "saveInputLanguage")){
|
||||
if (Validators.validateInputLanguage(language, "saveInputLanguage")){
|
||||
prefsEditor.putInt("pref_input_language", language);
|
||||
prefsEditor.apply();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,6 +88,10 @@ class SettingsPunctuation extends SettingsInput {
|
|||
orderedChars = language.getKeyCharacters(number);
|
||||
}
|
||||
|
||||
if (number < 2) {
|
||||
orderedChars = removeLettersFromList(orderedChars);
|
||||
}
|
||||
|
||||
return orderedChars;
|
||||
}
|
||||
|
||||
|
|
@ -114,4 +118,16 @@ class SettingsPunctuation extends SettingsInput {
|
|||
|
||||
return charsList;
|
||||
}
|
||||
|
||||
|
||||
private ArrayList<String> removeLettersFromList(ArrayList<String> list) {
|
||||
ArrayList<String> cleanList = new ArrayList<>();
|
||||
for (String s : list) {
|
||||
if (!Character.isAlphabetic(s.codePointAt(0))) {
|
||||
cleanList.add(s);
|
||||
}
|
||||
}
|
||||
|
||||
return cleanList;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ public class SettingsUI extends SettingsTyping {
|
|||
|
||||
if (DeviceInfo.noKeyboard(context)) {
|
||||
DEFAULT_LAYOUT = LAYOUT_NUMPAD;
|
||||
} else if (DeviceInfo.noBackspaceKey(context) && !DeviceInfo.noTouchScreen(context)) {
|
||||
} else if (DeviceInfo.noBackspaceKey() && !DeviceInfo.noTouchScreen(context)) {
|
||||
DEFAULT_LAYOUT = LAYOUT_SMALL;
|
||||
} else {
|
||||
DEFAULT_LAYOUT = LAYOUT_TRAY;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
package io.github.sspanak.tt9.preferences.settings;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
|
|
@ -25,16 +23,16 @@ class Validators {
|
|||
InputMode.CASE_CAPITALIZE
|
||||
));
|
||||
|
||||
static boolean doesLanguageExist(Context context, int langId) {
|
||||
return LanguageCollection.getLanguage(context, langId) != null;
|
||||
static boolean doesLanguageExist(int langId) {
|
||||
return LanguageCollection.getLanguage(langId) != null;
|
||||
}
|
||||
|
||||
static boolean validateInputMode(int mode, String logTag, String logMsg) {
|
||||
return Validators.isIntInList(mode, validInputModes, logTag, logMsg);
|
||||
}
|
||||
|
||||
static boolean validateInputLanguage(Context context, int langId, String logTag) {
|
||||
if (!doesLanguageExist(context, langId)) {
|
||||
static boolean validateInputLanguage(int langId, String logTag) {
|
||||
if (!doesLanguageExist(langId)) {
|
||||
Logger.w(logTag, "Not saving invalid language with ID: " + langId);
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ public class AddWordDialog extends PopupDialog {
|
|||
word = intent.getStringExtra(PARAMETER_WORD);
|
||||
|
||||
int languageId = intent.getIntExtra(PARAMETER_LANGUAGE, -1);
|
||||
language = LanguageCollection.getLanguage(context, languageId);
|
||||
language = LanguageCollection.getLanguage(languageId);
|
||||
|
||||
if (language == null) {
|
||||
message = context.getString(R.string.add_word_invalid_language_x, languageId);
|
||||
|
|
|
|||
|
|
@ -20,13 +20,13 @@ public class AutoUpdateMonologue extends PopupDialog {
|
|||
|
||||
AutoUpdateMonologue(@NonNull Context context, @NonNull Intent intent, ConsumerCompat<String> activityFinisher) {
|
||||
super(context, activityFinisher);
|
||||
parseIntent(context, intent);
|
||||
parseIntent(intent);
|
||||
}
|
||||
|
||||
|
||||
private void parseIntent(@NonNull Context context, @NonNull Intent intent) {
|
||||
private void parseIntent(@NonNull Intent intent) {
|
||||
int languageId = intent.getIntExtra(PARAMETER_LANGUAGE, -1);
|
||||
language = LanguageCollection.getLanguage(context, languageId);
|
||||
language = LanguageCollection.getLanguage(languageId);
|
||||
|
||||
if (language == null) {
|
||||
Logger.e(getClass().getSimpleName(), "Auto-updating is not possible. Intent parameter '" + PARAMETER_LANGUAGE + "' is invalid: " + languageId);
|
||||
|
|
|
|||
|
|
@ -15,9 +15,12 @@ import java.util.Arrays;
|
|||
import io.github.sspanak.tt9.R;
|
||||
import io.github.sspanak.tt9.hacks.DeviceInfo;
|
||||
import io.github.sspanak.tt9.ime.TraditionalT9;
|
||||
import io.github.sspanak.tt9.languages.LanguageKind;
|
||||
import io.github.sspanak.tt9.ui.main.keys.SoftKey;
|
||||
import io.github.sspanak.tt9.ui.main.keys.SoftKeyFn;
|
||||
import io.github.sspanak.tt9.ui.main.keys.SoftKeyNumber;
|
||||
import io.github.sspanak.tt9.ui.main.keys.SoftKeyNumber0;
|
||||
import io.github.sspanak.tt9.ui.main.keys.SoftKeyNumber1;
|
||||
import io.github.sspanak.tt9.ui.main.keys.SoftKeyPunctuation;
|
||||
import io.github.sspanak.tt9.ui.main.keys.SoftKeySettings;
|
||||
|
||||
|
|
@ -89,13 +92,14 @@ class MainLayoutNumpad extends BaseMainLayout {
|
|||
@Override
|
||||
void showTextEditingPalette() {
|
||||
isTextEditingShown = true;
|
||||
boolean notKorean = tt9 != null && !LanguageKind.isKorean(tt9.getLanguage());
|
||||
|
||||
for (SoftKey key : getKeys()) {
|
||||
int keyId = key.getId();
|
||||
|
||||
if (keyId == R.id.soft_key_0) {
|
||||
key.setEnabled(tt9 != null && !tt9.isInputModeNumeric());
|
||||
} else if (key.getClass().equals(SoftKeyNumber.class)) {
|
||||
key.setEnabled(tt9 != null && !tt9.isInputModeNumeric() && notKorean);
|
||||
} else if (key.getClass().equals(SoftKeyNumber.class) || key instanceof SoftKeyNumber0 || key instanceof SoftKeyNumber1) {
|
||||
key.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
|
|
@ -115,7 +119,7 @@ class MainLayoutNumpad extends BaseMainLayout {
|
|||
keyId == R.id.soft_key_add_word
|
||||
|| keyId == R.id.soft_key_lf3
|
||||
|| keyId == R.id.soft_key_lf4
|
||||
|| keyId == R.id.soft_key_filter_suggestions
|
||||
|| (keyId == R.id.soft_key_filter_suggestions && notKorean)
|
||||
) {
|
||||
key.setEnabled(false);
|
||||
}
|
||||
|
|
@ -127,7 +131,7 @@ class MainLayoutNumpad extends BaseMainLayout {
|
|||
isTextEditingShown = false;
|
||||
|
||||
for (SoftKey key : getKeys()) {
|
||||
if (key.getClass().equals(SoftKeyNumber.class) || key.getClass().equals(SoftKeyPunctuation.class)) {
|
||||
if (key.getClass().equals(SoftKeyNumber.class) || key.getClass().equals(SoftKeyPunctuation.class) || key instanceof SoftKeyNumber0 || key instanceof SoftKeyNumber1) {
|
||||
key.setVisibility(View.VISIBLE);
|
||||
key.setEnabled(true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ public class SoftKeyAddWord extends SoftKey {
|
|||
public void render() {
|
||||
super.render();
|
||||
if (tt9 != null) {
|
||||
setEnabled(!tt9.isVoiceInputActive());
|
||||
setEnabled(!tt9.isVoiceInputActive() && tt9.notLanguageSyllabary());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package io.github.sspanak.tt9.ui.main.keys;
|
|||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import io.github.sspanak.tt9.languages.LanguageKind;
|
||||
import io.github.sspanak.tt9.ui.Vibration;
|
||||
|
||||
public class SoftKeyFilter extends SoftKey {
|
||||
|
|
@ -10,11 +11,30 @@ public class SoftKeyFilter extends SoftKey {
|
|||
public SoftKeyFilter(Context context, AttributeSet attrs) { super(context, attrs); }
|
||||
public SoftKeyFilter(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); }
|
||||
|
||||
@Override protected float getTitleRelativeSize() { return super.getTitleRelativeSize() / 0.85f; }
|
||||
@Override protected float getSubTitleRelativeSize() { return super.getSubTitleRelativeSize() / 0.85f; }
|
||||
@Override
|
||||
protected float getTitleRelativeSize() {
|
||||
return isKorean() ? 1.1f : super.getTitleRelativeSize() / 0.85f;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected float getSubTitleRelativeSize() {
|
||||
return super.getSubTitleRelativeSize() / 0.85f;
|
||||
}
|
||||
|
||||
|
||||
private boolean isKorean() {
|
||||
return tt9 != null && LanguageKind.isKorean(tt9.getLanguage());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void handleHold() {
|
||||
if (isKorean()) {
|
||||
handleRelease();
|
||||
return;
|
||||
}
|
||||
|
||||
preventRepeat();
|
||||
if (validateTT9Handler() && tt9.onKeyFilterClear(false)) {
|
||||
vibrate(Vibration.getHoldVibration());
|
||||
|
|
@ -22,21 +42,30 @@ public class SoftKeyFilter extends SoftKey {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected boolean handleRelease() {
|
||||
return
|
||||
validateTT9Handler()
|
||||
&& tt9.onKeyFilterSuggestions(false, getLastPressedKey() == getId());
|
||||
if (!validateTT9Handler()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isKorean()) {
|
||||
return tt9.onKeySpaceKorean(false);
|
||||
} else {
|
||||
return tt9.onKeyFilterSuggestions(false, getLastPressedKey() == getId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected String getTitle() {
|
||||
return "CLR";
|
||||
return isKorean() ? "␣" : "CLR";
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected String getSubTitle() {
|
||||
return "FLTR";
|
||||
return isKorean() ? null : "FLTR";
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -44,7 +73,12 @@ public class SoftKeyFilter extends SoftKey {
|
|||
public void render() {
|
||||
super.render();
|
||||
if (tt9 != null) {
|
||||
setEnabled(!tt9.isInputModeNumeric() && !tt9.isInputModeABC() && !tt9.isVoiceInputActive());
|
||||
setEnabled(
|
||||
!tt9.isInputModeNumeric()
|
||||
&& !tt9.isInputModeABC()
|
||||
&& !tt9.isVoiceInputActive()
|
||||
&& (LanguageKind.isKorean(tt9.getLanguage()) || tt9.notLanguageSyllabary())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,19 +55,7 @@ public class SoftKeyLF4 extends SwipeableKey {
|
|||
}
|
||||
|
||||
protected String getPressIcon() {
|
||||
if (tt9 == null || tt9.getLanguage() == null) {
|
||||
return getContext().getString(R.string.virtual_key_input_mode);
|
||||
}
|
||||
|
||||
if (tt9.isInputModeNumeric()) {
|
||||
return "123";
|
||||
}
|
||||
|
||||
if (tt9.isInputModeABC()) {
|
||||
return tt9.getLanguage().getAbcString().toUpperCase(tt9.getLanguage().getLocale());
|
||||
}
|
||||
|
||||
return "T9";
|
||||
return tt9 != null ? tt9.getInputModeName() : getContext().getString(R.string.virtual_key_input_mode);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -44,8 +44,6 @@ public class SoftKeyNumber extends SoftKey {
|
|||
put(9, 3);
|
||||
}};
|
||||
|
||||
private static final String PUNCTUATION_LABEL = ",:-)";
|
||||
|
||||
|
||||
public SoftKeyNumber(Context context) {
|
||||
super(context);
|
||||
|
|
@ -71,16 +69,6 @@ public class SoftKeyNumber extends SoftKey {
|
|||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected float getSubTitleRelativeSize() {
|
||||
if (tt9 != null && !tt9.isInputModeNumeric() && getNumber(getId()) == 0) {
|
||||
return 1.1f;
|
||||
}
|
||||
|
||||
return super.getSubTitleRelativeSize();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void handleHold() {
|
||||
preventRepeat();
|
||||
|
|
@ -140,30 +128,7 @@ public class SoftKeyNumber extends SoftKey {
|
|||
|
||||
@Override
|
||||
protected String getSubTitle() {
|
||||
if (tt9 == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int number = getNumber(getId());
|
||||
|
||||
return switch (number) {
|
||||
case 0 -> getSpecialCharList(tt9);
|
||||
case 1 -> tt9.isNumericModeStrict() ? null : PUNCTUATION_LABEL;
|
||||
default -> getKeyCharList(tt9, number);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private String getSpecialCharList(@NonNull TraditionalT9 tt9) {
|
||||
if (tt9.isNumericModeSigned()) {
|
||||
return "+/-";
|
||||
} else if (tt9.isNumericModeStrict()) {
|
||||
return null;
|
||||
} else if (tt9.isInputModeNumeric()) {
|
||||
return "+";
|
||||
} else {
|
||||
return "␣";
|
||||
}
|
||||
return tt9 == null ? null : getKeyCharList(tt9, getNumber(getId()));
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -234,14 +199,16 @@ public class SoftKeyNumber extends SoftKey {
|
|||
|
||||
/**
|
||||
* As suggested by the community, there is no need to display the accented letters.
|
||||
* People are used to seeing just "ABC", "DEF", etc.
|
||||
* People are used to seeing just "ABC", "DEF", etc. In the case of Korean, the keypad looks too
|
||||
* cluttered, so we skip the double consonants, like on phones with a physical keypad.
|
||||
*/
|
||||
private boolean shouldSkipAccents(char currentLetter, boolean isGreek, boolean isLatinBased) {
|
||||
return
|
||||
currentLetter == 'ѝ'
|
||||
|| currentLetter == 'ґ'
|
||||
|| currentLetter == 'ς'
|
||||
|| (currentLetter == 'ㄲ' || currentLetter == 'ㄸ' || currentLetter == 'ㅃ' || currentLetter == 'ㅆ' || currentLetter == 'ㅉ')
|
||||
|| (isLatinBased && currentLetter > 'z')
|
||||
|| currentLetter == 'ς'
|
||||
|| (isGreek && (currentLetter < 'α' || currentLetter > 'ω'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
package io.github.sspanak.tt9.ui.main.keys;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import io.github.sspanak.tt9.languages.LanguageKind;
|
||||
|
||||
public class SoftKeyNumber0 extends SoftKeyNumber {
|
||||
private static final String CHARS_NUMERIC_MODE = "+%$";
|
||||
|
||||
public SoftKeyNumber0(Context context) { super(context); }
|
||||
public SoftKeyNumber0(Context context, AttributeSet attrs) { super(context, attrs); }
|
||||
public SoftKeyNumber0(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); }
|
||||
|
||||
@Override
|
||||
protected String getTitle() {
|
||||
if (tt9 == null) {
|
||||
return super.getTitle();
|
||||
}
|
||||
|
||||
if (tt9.isNumericModeStrict()) {
|
||||
return "0";
|
||||
} if (tt9.isNumericModeSigned()) {
|
||||
return "+/-";
|
||||
} else if (tt9.isInputModePhone()) {
|
||||
return "+";
|
||||
} else if (tt9.isInputModeNumeric() || LanguageKind.isKorean(tt9.getLanguage())) {
|
||||
return CHARS_NUMERIC_MODE;
|
||||
}
|
||||
|
||||
return super.getTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getSubTitle() {
|
||||
if (tt9 == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (tt9.isNumericModeStrict()) {
|
||||
return null;
|
||||
} else if (tt9.isInputModeNumeric()) {
|
||||
return "0";
|
||||
} else if (LanguageKind.isKorean(tt9.getLanguage())) {
|
||||
return getKoreanCharList();
|
||||
} else {
|
||||
return "␣";
|
||||
}
|
||||
}
|
||||
|
||||
private String getKoreanCharList() {
|
||||
if (tt9 == null || tt9.getLanguage() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
StringBuilder list = new StringBuilder();
|
||||
for (String character : tt9.getLanguage().getKeyCharacters(0)) {
|
||||
if (Character.isAlphabetic(character.charAt(0))) {
|
||||
list.append(character);
|
||||
}
|
||||
}
|
||||
|
||||
return list.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected float getSubTitleRelativeSize() {
|
||||
if (tt9 != null && !tt9.isInputModeNumeric() && !LanguageKind.isKorean(tt9.getLanguage())) {
|
||||
return 1.1f;
|
||||
}
|
||||
|
||||
return super.getSubTitleRelativeSize();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package io.github.sspanak.tt9.ui.main.keys;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import io.github.sspanak.tt9.languages.LanguageKind;
|
||||
|
||||
public class SoftKeyNumber1 extends SoftKeyNumber {
|
||||
private static final String DEFAULT_LARGE_LABEL = ",:-)";
|
||||
private static final String KOREAN_SMALL_LABEL = "1 :-)";
|
||||
private static final String KOREAN_LARGE_LABEL = "ㅣ";
|
||||
|
||||
public SoftKeyNumber1(Context context) { super(context); }
|
||||
public SoftKeyNumber1(Context context, AttributeSet attrs) { super(context, attrs); }
|
||||
public SoftKeyNumber1(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); }
|
||||
|
||||
@Override
|
||||
protected String getTitle() {
|
||||
if (tt9 == null) {
|
||||
return super.getTitle();
|
||||
}
|
||||
|
||||
if (tt9.isInputModeNumeric() && !tt9.isNumericModeStrict()) {
|
||||
return DEFAULT_LARGE_LABEL;
|
||||
} else if (LanguageKind.isKorean(tt9.getLanguage())) {
|
||||
return KOREAN_SMALL_LABEL;
|
||||
} else {
|
||||
return "1";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getSubTitle() {
|
||||
if (tt9 == null || tt9.isNumericModeStrict()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (tt9.isNumericModeStrict()) {
|
||||
return null;
|
||||
} else if (tt9.isInputModeNumeric()) {
|
||||
return "1";
|
||||
} else if (LanguageKind.isKorean(tt9.getLanguage())) {
|
||||
return KOREAN_LARGE_LABEL;
|
||||
} else {
|
||||
return DEFAULT_LARGE_LABEL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -57,13 +57,7 @@ public class SoftKeyRF3 extends SoftKey {
|
|||
@Override
|
||||
protected String getTitle() {
|
||||
if (isTextEdtingActive()) {
|
||||
if (tt9 == null) {
|
||||
return "ABC";
|
||||
} else if (tt9.isInputModeNumeric()) {
|
||||
return "123";
|
||||
} else if (tt9.getLanguage() != null) {
|
||||
return tt9.getLanguage().getAbcString().toUpperCase(tt9.getLanguage().getLocale());
|
||||
}
|
||||
return tt9 == null ? "ABC" : tt9.getABCString();
|
||||
}
|
||||
|
||||
return isTextEditingMissing() && !isVoiceInputMissing() ? "🎤" : getContext().getString(R.string.virtual_key_text_editing).toUpperCase();
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ public class DictionaryLoadingBar extends DictionaryProgressNotification {
|
|||
}
|
||||
|
||||
|
||||
public void show(Context context, Bundle data) {
|
||||
public void show(Bundle data) {
|
||||
String error = data.getString("error", null);
|
||||
int fileCount = data.getInt("fileCount", -1);
|
||||
int progress = data.getInt("progress", -1);
|
||||
|
|
@ -72,7 +72,6 @@ public class DictionaryLoadingBar extends DictionaryProgressNotification {
|
|||
if (error != null) {
|
||||
hasFailed = true;
|
||||
showError(
|
||||
context,
|
||||
error,
|
||||
data.getInt("languageId", -1),
|
||||
data.getLong("fileLine", -1)
|
||||
|
|
@ -84,7 +83,6 @@ public class DictionaryLoadingBar extends DictionaryProgressNotification {
|
|||
}
|
||||
|
||||
showProgress(
|
||||
context,
|
||||
data.getLong("time", 0),
|
||||
data.getInt("currentFile", 0),
|
||||
data.getInt("progress", 0),
|
||||
|
|
@ -94,8 +92,8 @@ public class DictionaryLoadingBar extends DictionaryProgressNotification {
|
|||
}
|
||||
|
||||
|
||||
private String generateTitle(Context context, int languageId) {
|
||||
Language lang = LanguageCollection.getLanguage(context, languageId);
|
||||
private String generateTitle(int languageId) {
|
||||
Language lang = LanguageCollection.getLanguage(languageId);
|
||||
|
||||
if (lang != null) {
|
||||
return resources.getString(R.string.dictionary_loading, lang.getName());
|
||||
|
|
@ -105,7 +103,7 @@ public class DictionaryLoadingBar extends DictionaryProgressNotification {
|
|||
}
|
||||
|
||||
|
||||
private void showProgress(Context context, long time, int currentFile, int currentFileProgress, int languageId) {
|
||||
private void showProgress(long time, int currentFile, int currentFileProgress, int languageId) {
|
||||
if (currentFileProgress <= 0) {
|
||||
hide();
|
||||
isStopped = true;
|
||||
|
|
@ -119,12 +117,12 @@ public class DictionaryLoadingBar extends DictionaryProgressNotification {
|
|||
|
||||
if (progress >= maxProgress) {
|
||||
progress = maxProgress = 0;
|
||||
title = generateTitle(context, -1);
|
||||
title = generateTitle(-1);
|
||||
|
||||
String timeFormat = time > 60000 ? " (%1.0fs)" : " (%1.1fs)";
|
||||
message = resources.getString(R.string.completed) + String.format(Locale.ENGLISH, timeFormat, time / 1000.0);
|
||||
} else {
|
||||
title = generateTitle(context, languageId);
|
||||
title = generateTitle(languageId);
|
||||
message = currentFileProgress + "%";
|
||||
}
|
||||
|
||||
|
|
@ -132,8 +130,8 @@ public class DictionaryLoadingBar extends DictionaryProgressNotification {
|
|||
}
|
||||
|
||||
|
||||
private void showError(Context context, String errorType, int langId, long line) {
|
||||
Language lang = LanguageCollection.getLanguage(context, langId);
|
||||
private void showError(String errorType, int langId, long line) {
|
||||
Language lang = LanguageCollection.getLanguage(langId);
|
||||
|
||||
if (lang == null || errorType.equals(InvalidLanguageException.class.getSimpleName())) {
|
||||
message = resources.getString(R.string.add_word_invalid_language);
|
||||
|
|
@ -147,7 +145,7 @@ public class DictionaryLoadingBar extends DictionaryProgressNotification {
|
|||
message = resources.getString(R.string.dictionary_load_error, lang.getName(), errorType);
|
||||
}
|
||||
|
||||
title = generateTitle(context, -1);
|
||||
title = generateTitle(-1);
|
||||
progress = maxProgress = 0;
|
||||
|
||||
renderError();
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ public class Characters {
|
|||
",", ".", "-", "«", "»", "(", ")", "&", "~", "`", "'", "\"", "·", ":", "!", GR_QUESTION_MARK
|
||||
));
|
||||
|
||||
final public static ArrayList<String> PunctuationKorean = new ArrayList<>(Arrays.asList(
|
||||
",", ".", "~", "1", "(", ")", "&", "-", "`", ";", ":", "'", "\"", "!", "?"
|
||||
));
|
||||
|
||||
final public static ArrayList<String> Currency = new ArrayList<>(Arrays.asList(
|
||||
"$", "€", "₹", "₿", "₩", "¢", "¤", "₺", "₱", "¥", "₽", "£"
|
||||
));
|
||||
|
|
@ -107,15 +111,6 @@ public class Characters {
|
|||
))
|
||||
));
|
||||
|
||||
public static boolean isStaticEmoji(String emoji) {
|
||||
for (ArrayList<String> group : Emoji) {
|
||||
if (group.contains(emoji)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean isGraphic(char ch) {
|
||||
return !(ch < 256 || Character.isLetterOrDigit(ch) || Character.isAlphabetic(ch));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ public class TextTools {
|
|||
private static final Pattern containsOtherThan1 = Pattern.compile("[02-9]");
|
||||
private static final Pattern combiningString = Pattern.compile("^\\p{M}+$");
|
||||
private static final Pattern nextIsPunctuation = Pattern.compile("^\\p{Punct}");
|
||||
private static final Pattern isHangul = Pattern.compile("[\u1100-\u11FF\u302E-\u302F\u3131-\u318F\u3200-\u321F\u3260-\u327E\uA960-\uA97F\uAC00-\uD7FB\uFFA0-\uFFDF]+");
|
||||
private static final Pattern nextToWord = Pattern.compile("\\b$");
|
||||
private static final Pattern previousIsLetter = Pattern.compile("\\p{L}$");
|
||||
private static final Pattern startOfSentence = Pattern.compile("(?<!\\.)(^|[.?!؟¿¡])\\s+$");
|
||||
|
|
@ -40,6 +41,11 @@ public class TextTools {
|
|||
}
|
||||
|
||||
|
||||
public static boolean isHangul(String str) {
|
||||
return str != null && isHangul.matcher(str).find();
|
||||
}
|
||||
|
||||
|
||||
public static boolean isStartOfSentence(String str) {
|
||||
return str != null && startOfSentence.matcher(str).find();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<io.github.sspanak.tt9.ui.main.keys.SoftKeyNumber
|
||||
<io.github.sspanak.tt9.ui.main.keys.SoftKeyNumber1
|
||||
android:id="@+id/soft_key_1"
|
||||
style="@android:style/Widget.Holo.Button.Borderless"
|
||||
android:layout_width="0dp"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
android:layout_weight="1"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
|
||||
|
||||
<io.github.sspanak.tt9.ui.main.keys.SoftKeyNumber
|
||||
<io.github.sspanak.tt9.ui.main.keys.SoftKeyNumber0
|
||||
android:id="@+id/soft_key_0"
|
||||
style="@android:style/Widget.Holo.Button.Borderless"
|
||||
android:layout_width="0dp"
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@
|
|||
<string name="donate_title">Дарете</string>
|
||||
<string name="donate_summary">Ако харесвате %1$s, черпете една бира на: %2$s.</string>
|
||||
<string name="function_edit_text">Редактиране на текст</string>
|
||||
<string name="function_add_word_not_available">Добавянето на думи не е възможно на този език.</string>
|
||||
<string name="function_backspace">Триене на текст</string>
|
||||
<string name="dictionary_no_notifications">Речникови известия</string>
|
||||
<string name="dictionary_no_notifications_summary">Получавайте известия за обновления на речника и за прогреса при зареждане.</string>
|
||||
|
|
@ -81,6 +82,7 @@
|
|||
<string name="function_show_command_palette">Списък с команди</string>
|
||||
<string name="function_filter_clear">Изчистване на филтър</string>
|
||||
<string name="function_filter_suggestions">Филтриране на думи</string>
|
||||
<string name="function_filter_suggestions_not_available">Филтрирането не е възможно на този език.</string>
|
||||
<string name="function_previous_suggestion">Предишна дума</string>
|
||||
<string name="function_next_suggestion">Следваща дума</string>
|
||||
<string name="function_next_language">Следващ eзик</string>
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@
|
|||
<string name="pref_category_function_keys">Tastenkürzel</string>
|
||||
<string name="pref_category_keypad">Tastenfeld</string>
|
||||
<string name="char_space">Leerzeichen</string>
|
||||
<string name="function_add_word_not_available">Das Hinzufügen von Wörtern ist in dieser Sprache nicht möglich.</string>
|
||||
<string name="function_backspace">Rücktaste</string>
|
||||
<string name="setup_keyboard_status">Status</string>
|
||||
<string name="setup_default_keyboard">Standardtastatur auswählen</string>
|
||||
|
|
@ -174,4 +175,5 @@
|
|||
<string name="punctuation_order_save">Reihenfolge speichern</string>
|
||||
<string name="punctuation_order_forbidden_char">Verbotenes Zeichen:%1$s</string>
|
||||
<string name="punctuation_order_forbidden_chars">Verbotene Zeichen:%1$s</string>
|
||||
<string name="function_filter_suggestions_not_available">Das Filtern ist in dieser Sprache nicht möglich.</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@
|
|||
<string name="pref_dark_theme">Tema oscuro</string>
|
||||
<string name="char_space">Espacio</string>
|
||||
<string name="dictionary_truncating">Borrando…</string>
|
||||
<string name="function_add_word_not_available">No es posible agregar palabras en este idioma.</string>
|
||||
<string name="function_backspace">Retroceso</string>
|
||||
<string name="setup_keyboard_status">Estado</string>
|
||||
<string name="setup_default_keyboard">Selecciona teclado predeterminado</string>
|
||||
|
|
@ -81,6 +82,7 @@
|
|||
<string name="function_show_command_palette">Lista de comandos</string>
|
||||
<string name="function_filter_clear">Limpiar el filtro</string>
|
||||
<string name="function_filter_suggestions">Filtrar sugerencias</string>
|
||||
<string name="function_filter_suggestions_not_available">No es posible filtrar en este idioma.</string>
|
||||
<string name="function_previous_suggestion">Sugerencia previa</string>
|
||||
<string name="function_next_suggestion">Sugerencia siguiente</string>
|
||||
<string name="function_next_language">Idioma siguiente</string>
|
||||
|
|
|
|||
|
|
@ -67,12 +67,14 @@
|
|||
<string name="pref_auto_text_case_summary">Commencer automatiquement les phrases avec une majuscule.</string>
|
||||
<string name="pref_category_keypad">Clavier</string>
|
||||
<string name="char_space">Espace</string>
|
||||
<string name="function_add_word_not_available">L\'ajout de mots n\'est pas possible dans cette langue.</string>
|
||||
<string name="function_backspace">Retour arrière</string>
|
||||
<string name="dictionary_no_notifications">Notifications du dictionnaire</string>
|
||||
<string name="dictionary_no_notifications_summary">Recevoir des notifications sur les mises à jour du dictionnaire et sur la progression du chargement.</string>
|
||||
<string name="function_show_command_palette">Liste des commandes</string>
|
||||
<string name="function_filter_clear">Supprimer le filtre</string>
|
||||
<string name="function_filter_suggestions">Filtrer les mots</string>
|
||||
<string name="function_filter_suggestions_not_available">Le filtrage n\'est pas possible dans cette langue.</string>
|
||||
<string name="function_previous_suggestion">Mot précédent</string>
|
||||
<string name="function_next_suggestion">Mot suivant</string>
|
||||
<string name="function_next_language">Langue suivante</string>
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@
|
|||
<string name="dictionary_load_cancelled">Caricamento annullato.</string>
|
||||
<string name="pref_category_keypad">Tastiera</string>
|
||||
<string name="char_space">Spazio</string>
|
||||
<string name="function_add_word_not_available">Aggiungere parole non è possibile in questa lingua.</string>
|
||||
<string name="function_backspace">Backspace</string>
|
||||
<string name="setup_keyboard_status">Stato</string>
|
||||
<string name="setup_default_keyboard">Seleziona la tastiera predefinita</string>
|
||||
|
|
@ -174,5 +175,6 @@
|
|||
<string name="punctuation_order_save">Salvare l\'ordine</string>
|
||||
<string name="punctuation_order_forbidden_char">Carattere vietato:%1$s</string>
|
||||
<string name="punctuation_order_forbidden_chars">Caratteri vietati:%1$s</string>
|
||||
<string name="function_filter_suggestions_not_available">Il filtraggio non è possibile in questa lingua.</string>
|
||||
</resources>
|
||||
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@
|
|||
<string name="dictionary_truncated">המילון נמחק בהצלחה.</string>
|
||||
|
||||
<string name="dictionary_truncating">המחיקה מתבצעת…</string>
|
||||
<string name="function_add_word_not_available">אין אפשרות להוסיף מילים בשפה זו.</string>
|
||||
<string name="function_backspace">לחצן מחיקה</string>
|
||||
<string name="function_next_language">לחצן למעבר לשפה הבאה</string>
|
||||
<string name="function_next_mode">לחצן מצב קלט</string>
|
||||
|
|
@ -187,4 +188,5 @@
|
|||
<string name="punctuation_order_save">שמור את הסדר</string>
|
||||
<string name="punctuation_order_forbidden_char">תו אסור:%1$s</string>
|
||||
<string name="punctuation_order_forbidden_chars">תווים אסורים:%1$s</string>
|
||||
<string name="function_filter_suggestions_not_available">לא ניתן לסנן בשפה זו.</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -77,12 +77,14 @@
|
|||
<string name="dictionary_truncated">Žodynas sėkmingai ištrintas.</string>
|
||||
<string name="dictionary_truncating">Ištrinama…</string>
|
||||
|
||||
<string name="function_add_word_not_available">Žodžių pridėjimas šia kalba nėra galimas.</string>
|
||||
<string name="function_backspace">Trinti</string>
|
||||
<string name="dictionary_no_notifications">Žodyno pranešimai</string>
|
||||
<string name="dictionary_no_notifications_summary">Gaukite pranešimus apie žodynų atnaujinimus ir įkėlimo progresą.</string>
|
||||
<string name="function_show_command_palette">Rodyti komandų sąrašą</string>
|
||||
<string name="function_filter_clear">Panaikinti filtrą</string>
|
||||
<string name="function_filter_suggestions">Filtruoti pasiūlymus</string>
|
||||
<string name="function_filter_suggestions_not_available">Filtravimas šia kalba nėra galimas.</string>
|
||||
<string name="function_previous_suggestion">Ankstesnis pasiūlytas žodis</string>
|
||||
<string name="function_next_suggestion">Sekantis pasiūlytas žodis</string>
|
||||
<string name="function_next_language">Rašymo kalba</string>
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@
|
|||
<string name="pref_category_function_keys">Sneltoetsen</string>
|
||||
<string name="pref_category_keypad">Toetsenbord</string>
|
||||
<string name="char_space">Spatie</string>
|
||||
<string name="function_add_word_not_available">Het toevoegen van woorden is niet mogelijk in deze taal.</string>
|
||||
<string name="function_backspace">Backspace</string>
|
||||
<string name="setup_keyboard_status">Status</string>
|
||||
<string name="setup_default_keyboard">Standaardtoetsenbord selecteren</string>
|
||||
|
|
@ -173,4 +174,5 @@
|
|||
<string name="punctuation_order_save">Volgorde opslaan</string>
|
||||
<string name="punctuation_order_forbidden_char">Verboden teken:%1$s</string>
|
||||
<string name="punctuation_order_forbidden_chars">Verboden tekens:%1$s</string>
|
||||
<string name="function_filter_suggestions_not_available">Het filteren is niet mogelijk in deze taal.</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@
|
|||
<string name="dictionary_truncate_title">Limpar Dicionário</string>
|
||||
<string name="dictionary_truncated">Dicionário apagado com sucesso.</string>
|
||||
|
||||
<string name="function_add_word_not_available">Não é possível adicionar palavras neste idioma.</string>
|
||||
<string name="function_backspace">Backspace</string>
|
||||
<string name="function_next_language">Próximo Idioma</string>
|
||||
<string name="function_next_mode">Modo de Entrada</string>
|
||||
|
|
@ -187,4 +188,5 @@
|
|||
<string name="punctuation_order_save">Salvar ordem</string>
|
||||
<string name="punctuation_order_forbidden_char">Caractere proibido:%1$s</string>
|
||||
<string name="punctuation_order_forbidden_chars">Caracteres proibidos:%1$s</string>
|
||||
<string name="function_filter_suggestions_not_available">Não é possível filtrar neste idioma.</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -67,12 +67,14 @@
|
|||
<string name="pref_auto_text_case_summary">Автоматически начинать предложение с заглавной буквы.</string>
|
||||
<string name="pref_double_zero_char">Символ при двойном нажатии клавиши 0</string>
|
||||
<string name="dictionary_load_bad_char">Не удалось загрузить словарь. Проблема в слове в строке %1$d для языка «%2$s».</string>
|
||||
<string name="function_add_word_not_available">Добавление слов невозможно на этом языке.</string>
|
||||
<string name="function_backspace">Стереть</string>
|
||||
<string name="dictionary_no_notifications">Уведомления словаря</string>
|
||||
<string name="dictionary_no_notifications_summary">Получать уведомления о обновлениях словаря и о процессе загрузки.</string>
|
||||
<string name="function_show_command_palette">Список команд</string>
|
||||
<string name="function_filter_clear">Удалить фильтр</string>
|
||||
<string name="function_filter_suggestions">Фильтровать слова</string>
|
||||
<string name="function_filter_suggestions_not_available">Фильтрация невозможна на этом языке.</string>
|
||||
<string name="function_previous_suggestion">Предыдущее слово</string>
|
||||
<string name="function_next_suggestion">Следующее слово</string>
|
||||
<string name="function_next_language">Следующий язык</string>
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@
|
|||
<string name="pref_category_function_keys">Klavye Kısayolları</string>
|
||||
<string name="pref_category_keypad">Tuş Takımı</string>
|
||||
<string name="char_space">Boşluk</string>
|
||||
<string name="function_add_word_not_available">Bu dilde kelime eklemek mümkün değil.</string>
|
||||
<string name="function_backspace">Geri Tuşu</string>
|
||||
<string name="setup_keyboard_status">Durum</string>
|
||||
<string name="setup_default_keyboard">Varsayılan Klavyeyi Seçin</string>
|
||||
|
|
@ -136,6 +137,7 @@
|
|||
<string name="function_show_command_palette">Komut listesini göster</string>
|
||||
<string name="function_filter_clear">Filtre Temizle</string>
|
||||
<string name="function_filter_suggestions">Tahminleri Filtrele</string>
|
||||
<string name="function_filter_suggestions_not_available">Bu dilde filtreleme mümkün değil.</string>
|
||||
<string name="function_previous_suggestion">Önceki Tahmin</string>
|
||||
<string name="function_next_suggestion">Sonraki Tahmin</string>
|
||||
<string name="function_next_language">Sonraki Dil</string>
|
||||
|
|
|
|||
|
|
@ -110,12 +110,14 @@
|
|||
<string name="donate_title">Підтримати</string>
|
||||
<string name="donate_summary">Якщо вам подобається %1$s, ви можете пригостити мене пивом за адресою: %2$s.</string>
|
||||
|
||||
<string name="function_add_word_not_available">Додавання слів неможливе цією мовою.</string>
|
||||
<string name="function_backspace">Стерти</string>
|
||||
<string name="dictionary_no_notifications">Сповіщення словника</string>
|
||||
<string name="dictionary_no_notifications_summary">Отримувати повідомлення про оновлення словника та процес завантаження.</string>
|
||||
<string name="function_show_command_palette">Список команд</string>
|
||||
<string name="function_filter_clear">Очистити фільтр</string>
|
||||
<string name="function_filter_suggestions">Фільтрувати пропозиції</string>
|
||||
<string name="function_filter_suggestions_not_available">Фільтрація неможлива цією мовою.</string>
|
||||
<string name="function_previous_suggestion">Попередня пропозиція</string>
|
||||
<string name="function_next_suggestion">Наступна пропозиція</string>
|
||||
<string name="function_next_language">Наступна мова</string>
|
||||
|
|
|
|||
|
|
@ -144,11 +144,13 @@
|
|||
<string name="donate_url_short" translatable="false">www.buymeacoffee.com</string>
|
||||
|
||||
<string name="function_add_word">Add Word</string>
|
||||
<string name="function_add_word_not_available">Adding words is not possible in this language.</string>
|
||||
<string name="function_backspace">Backspace</string>
|
||||
<string name="function_show_command_palette">Show Command List</string>
|
||||
<string name="function_filter_clear">Clear Filter</string>
|
||||
<string name="function_edit_text">Edit Text</string>
|
||||
<string name="function_filter_suggestions">Filter Suggestions</string>
|
||||
<string name="function_filter_suggestions_not_available">Filtering is not possible in this language.</string>
|
||||
<string name="function_previous_suggestion">Previous Suggestion</string>
|
||||
<string name="function_next_suggestion">Next Suggestion</string>
|
||||
<string name="function_next_language">Next Language</string>
|
||||
|
|
@ -221,6 +223,7 @@
|
|||
<string name="virtual_key_input_mode" translatable="false">Mode</string>
|
||||
<string name="virtual_key_settings" translatable="false">Cfg</string>
|
||||
<string name="virtual_key_shift" translatable="false">Shift</string>
|
||||
<string name="virtual_key_space_korean">Space (Korean)</string>
|
||||
<string name="virtual_key_text_editing" translatable="false">Copy</string>
|
||||
|
||||
<string name="voice_input_listening">Speak</string>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,13 @@
|
|||
<DropDownPreference
|
||||
app:key="pref_input_handling_mode"
|
||||
app:title="Keypad Handling Mode" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
app:defaultValue="true"
|
||||
app:key="pref_hold_for_punctuation_in_korean"
|
||||
app:title="Hold to type special chars in Korean"
|
||||
app:summaryOff="Type special chars by multi-pressing 1-key or 0-key"
|
||||
app:summaryOn="Type special chars by holding 1-key or 0-key" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory app:title="Logging" app:singleLineTitle="true">
|
||||
|
|
|
|||
|
|
@ -50,6 +50,10 @@
|
|||
app:key="key_shift"
|
||||
app:title="@string/virtual_key_shift" />
|
||||
|
||||
<DropDownPreference
|
||||
app:key="key_space_korean"
|
||||
app:title="@string/virtual_key_space_korean" />
|
||||
|
||||
<DropDownPreference
|
||||
app:key="key_show_settings"
|
||||
app:title="@string/function_show_settings" />
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ ext.parseLanguageDefintion = { File languageFile, String dictionariesDir ->
|
|||
|
||||
alphabet = languageFile.name.contains("Catalan") ? '·' : alphabet
|
||||
alphabet = languageFile.name.contains("Hebrew") || languageFile.name.contains("Yiddish") ? '"' : alphabet
|
||||
alphabet = languageFile.name.contains("Korean") ? ':' : alphabet
|
||||
|
||||
for (String line : languageFile.readLines()) {
|
||||
if (
|
||||
|
|
@ -292,7 +293,7 @@ static def validateWord(String word, String validCharacters, boolean isAlphabeti
|
|||
errors += "${errorMsgPrefix}. Found numbers on line ${lineNumber}. Remove all numbers.\n"
|
||||
}
|
||||
|
||||
if (word.matches("^\\P{L}+\$")) {
|
||||
if (word.matches("^\\P{L}+\$") && !validCharacters.contains(word)) {
|
||||
errorCount++
|
||||
errors += "${errorMsgPrefix}. Found a garbage word: '${word}' on line ${lineNumber}.\n"
|
||||
}
|
||||
|
|
|
|||
6
docs/dictionaries/koWordlistReadme.txt
Normal file
6
docs/dictionaries/koWordlistReadme.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
Hangul characters table 1 from Wikipedia
|
||||
URL: https://en.wikipedia.org/wiki/Hangul_consonant_and_vowel_tables
|
||||
License: Creative Commons Attribution-ShareAlike 4.0 License
|
||||
|
||||
Hangul characters table 2 by Tammy Korean
|
||||
URL: https://learning-korean.com/pdf/
|
||||
|
|
@ -79,6 +79,10 @@ _**Hinweis 2:** Um Nachrichten mit OK in Nachrichtenanwendungen zu senden, müss
|
|||
- **Doppeldruck:** tippt das Zeichen, das in den Einstellungen für den Prädiktiven Modus zugewiesen wurde (Standard: „.“)
|
||||
- **Halten:** tippt „0“.
|
||||
- **Drücken „0“ und Drücken der Umschalt-Taste (Standard: Drücken „0“, „✱“):** tippt Währungszeichen
|
||||
- **Im Cheonjiin-Modus (Koreanisch):**
|
||||
- **Drücken:** Gibt "ㅇ" und "ㅁ" ein.
|
||||
- **Halten:** Gibt Leerzeichen, neue Zeilen, "0" oder Sonder-/Mathematikzeichen ein.
|
||||
- **Halten von "0" und Drücken von Shift (Standard: Halten von "0", Drücken von "✱"):** Gibt Währungszeichen ein.
|
||||
|
||||
#### 1-Taste:
|
||||
- **Im 123 Modus:**
|
||||
|
|
@ -92,7 +96,11 @@ _**Hinweis 2:** Um Nachrichten mit OK in Nachrichtenanwendungen zu senden, müss
|
|||
- **Mehrfaches Drücken:** tippt Emoji
|
||||
- **Drücken von 1-1-3:** tippt benutzerdefinierte Emojis (diese müssen zuvor über die [Wort hinzufügen-Taste](#wort-hinzufügen-taste) hinzugefügt worden sein)
|
||||
- **Halten:** tippt „1“.
|
||||
|
||||
- **Im Cheonjiin-Modus (Koreanisch):**
|
||||
- **Drücken:** tippt den Vokal "ㅣ".
|
||||
- **Halten:** tippt Satzzeichen.
|
||||
- **Halten, dann drücken:** tippt Emoji.
|
||||
- **Halten 1, 1 drücken, 3 drücken:** tippt benutzerdefinierte Emojis (diese müssen zuvor über die [Wort hinzufügen-Taste](#wort-hinzufügen-taste) hinzugefügt worden sein)
|
||||
|
||||
#### 2- bis 9-Taste:
|
||||
- **Im 123 Modus:** tippt die entsprechende Zahl
|
||||
|
|
|
|||
|
|
@ -69,33 +69,41 @@ _**Note 2:** To send messages with OK in messaging applications, you must enable
|
|||
- **In 123 mode:**
|
||||
- **Press:** type "0".
|
||||
- **Hold:** type special/math characters.
|
||||
- **Hold "0", then press Shift (Default: hold "0", press "✱"):** type currency characters
|
||||
- **Hold "0", then press Shift (Default: hold "0", press "✱"):** type currency characters.
|
||||
- **In ABC mode:**
|
||||
- **Press:** type space, newline, or special/math characters.
|
||||
- **Hold:** type "0".
|
||||
- **Press "0", then press Shift (Default: press "0", "✱"):** type currency characters
|
||||
- **Press "0", then press Shift (Default: press "0", "✱"):** type currency characters.
|
||||
- **In Predictive mode:**
|
||||
- **Press:** type space, newline, or special/math characters.
|
||||
- **Double press:** type the character assigned in Predictive mode settings. (Default: ".")
|
||||
- **Hold:** type "0".
|
||||
- **Press "0", then press Shift (Default: press "0", "✱"):** type currency characters
|
||||
- **Press "0", then press Shift (Default: press "0", "✱"):** type currency characters.
|
||||
- **In Cheonjiin mode (Korean):**
|
||||
- **Press:** type "ㅇ" and "ㅁ".
|
||||
- **Hold:** type space, newline, 0, or special/math characters.
|
||||
- **Hold "0", then press Shift (Default: hold "0", press "✱"):** type currency characters.
|
||||
|
||||
#### 1-key:
|
||||
- **In 123 mode:**
|
||||
- **Press:** type "1".
|
||||
- **Hold:** type sentence characters
|
||||
- **Hold:** type sentence characters.
|
||||
- **In ABC mode:**
|
||||
- **Press:** type sentence characters
|
||||
- **Press:** type sentence characters.
|
||||
- **Hold:** type "1".
|
||||
- **In Predictive mode:**
|
||||
- **Press:** type sentence characters
|
||||
- **Press multiple times:** type emoji
|
||||
- **Press 1-1-3:** type custom added emoji (you must have added some using [the Add Word key](#add-word-key))
|
||||
- **Press:** type punctuation and sentence characters.
|
||||
- **Press multiple times:** type emoji.
|
||||
- **Press 1-1-3:** type custom added emoji (you must have added some using [the Add Word key](#add-word-key)).
|
||||
- **Hold:** type "1".
|
||||
|
||||
- **In Cheonjiin mode (Korean):**
|
||||
- **Press:** type the "ㅣ" vowel.
|
||||
- **Hold:** type punctuation and sentence characters.
|
||||
- **Hold, then press:** type emoji.
|
||||
- **Hold 1, press 1, press 3:** type custom added emoji (you must have added some using [the Add Word key](#add-word-key)).
|
||||
|
||||
#### 2- to 9-key:
|
||||
- **In 123 mode:** type the respective number
|
||||
- **In 123 mode:** type the respective number.
|
||||
- **In ABC and Predictive mode:** type a letter or hold to type the respective number.
|
||||
|
||||
### Function Keys
|
||||
|
|
|
|||
|
|
@ -71,31 +71,40 @@ _**Nota 2:** Para enviar mensajes con OK en aplicaciones de mensajería, debes h
|
|||
- **Mantenga presionado:** escribir caracteres especiales/matemáticos.
|
||||
- **Mantenga presionado "0", luego presione Shift (Por Defecto: mantenga presionado "0", presione "✱")**: escribir caracteres de moneda.
|
||||
- **En modo ABC:**
|
||||
- **Presione:** escribir espacio, nueva línea o caracteres especiales/matemáticos.
|
||||
- **Presione:** escribir un espacio, nueva línea o caracteres especiales/matemáticos.
|
||||
- **Mantenga presionado:** escribir "0".
|
||||
- **Presione "0", luego presione Shift (Por Defecto: presione "0", "✱")**: escribir caracteres de moneda.
|
||||
- **En modo predictivo:**
|
||||
- **Presione:** escribir espacio, nueva línea o caracteres especiales/matemáticos.
|
||||
- **Doble presión:** escribir el carácter asignado en la configuración del modo predictivo. (Por Defecto: ".")
|
||||
- **En modo Predictivo:**
|
||||
- **Presione:** escribir un espacio, nueva línea o caracteres especiales/matemáticos.
|
||||
- **Presione dos veces:** escribir el carácter asignado en la configuración de modo predictivo. (Por Defecto: ".")
|
||||
- **Mantenga presionado:** escribir "0".
|
||||
- **Presione "0", luego presione Shift (Por Defecto: presione "0", "✱")**: escribir caracteres de moneda.
|
||||
- **En modo Cheonjiin (Coreano):**
|
||||
- **Presione:** escribir "ㅇ" y "ㅁ".
|
||||
- **Mantenga presionado:** escribir espacio, nueva línea, "0" o caracteres especiales/matemáticos.
|
||||
- **Mantenga presionado "0", luego presione Shift (Por Defecto: Mantenga presionado "0", presione "✱"):** escribir caracteres de moneda.
|
||||
|
||||
#### Tecla 1:
|
||||
- **En modo 123:**
|
||||
- **Presione:** escribir "1".
|
||||
- **Mantenga presionado:** escribir caracteres de oración.
|
||||
- **Mantenga presionado:** escribir caracteres de puntuación.
|
||||
- **En modo ABC:**
|
||||
- **Presione:** escribir caracteres de oración.
|
||||
- **Presione:** escribir caracteres de puntuación.
|
||||
- **Mantenga presionado:** escribir "1".
|
||||
- **En modo predictivo:**
|
||||
- **Presione:** escribir caracteres de oración.
|
||||
- **Presione varias veces:** escribir emoji.
|
||||
- **En modo Predictivo:**
|
||||
- **Presione:** escribir caracteres de puntuación.
|
||||
- **Presione varias veces:** escribir emojis.
|
||||
- **Presione 1-1-3:** escribir emoji personalizados agregados (debe haber agregado algunos usando la [Tecla de Agregar Palabra](#tecla-de-agregar-palabra)).
|
||||
- **Mantenga presionado:** escribir "1".
|
||||
- **En modo Cheonjiin (Coreano):**
|
||||
- **Presione:** escribir la vocal "ㅣ".
|
||||
- **Mantenga presionado:** escribir caracteres de puntuación.
|
||||
- **Mantene, luego presione:** escribir emojis.
|
||||
- **Mantenga presionado 1, presione 1, presione 3:** escribir emoji personalizados agregados (debe haber agregado algunos usando la [Tecla de Agregar Palabra](#tecla-de-agregar-palabra)).
|
||||
|
||||
#### Teclas del 2 al 9:
|
||||
- **En modo 123:** escribir el número respectivo.
|
||||
- **En modo ABC y Predictivo:** escribir una letra o mantener presionado para escribir el número respectivo.
|
||||
- **En modo 123:** escribir el número correspondiente.
|
||||
- **En modo ABC y Predictivo:** escribir una letra o mantener presionado para escribir el número correspondiente.
|
||||
|
||||
### Teclas de Función
|
||||
|
||||
|
|
|
|||
|
|
@ -75,23 +75,32 @@ _**Remarque 2** : Pour envoyer des messages avec OK dans les applications de mes
|
|||
- **Appui long** : tape "0".
|
||||
- **Appui sur "0", puis appui sur Maj (par défaut : appui sur "0", "✱") :** tape des symboles monétaires.
|
||||
- **En mode Prédictif :**
|
||||
- **Appui** : tape espace, nouvelle ligne, ou caractères spéciaux/mathématiques.
|
||||
- **Appui** : tape un espace, une nouvelle ligne, ou caractères spéciaux/mathématiques.
|
||||
- **Double appui** : tape le caractère attribué dans les paramètres du mode Prédictif (par défaut : ".").
|
||||
- **Appui long** : tape "0".
|
||||
- **Appui sur "0", puis appui sur Maj (par défaut : appui sur "0", "✱") :** tape des symboles monétaires.
|
||||
- **En mode Cheonjiin (Coréen) :**
|
||||
- **Appui :** tape "ㅇ" et "ㅁ".
|
||||
- **Appui long :** tape un espace, une nouvelle ligne, "0" ou des caractères spéciaux/mathématiques.
|
||||
- **Appui long sur "0", puis appui sur Maj (Par défaut : appui long sur "0", appuyer sur "✱") :** tape des symboles monétaires.
|
||||
|
||||
#### Touche 1 :
|
||||
- **En mode 123 :**
|
||||
- **Appui** : tape "1".
|
||||
- **Appui long** : tape des caractères de phrase.
|
||||
- **Appui long** : tape des caractères de ponctuation.
|
||||
- **En mode ABC :**
|
||||
- **Appui** : tape des caractères de phrase.
|
||||
- **Appui** : tape des caractères de ponctuation.
|
||||
- **Appui long** : tape "1".
|
||||
- **En mode Prédictif :**
|
||||
- **Appui** : tape des caractères de phrase.
|
||||
- **Appui** : tape des caractères de ponctuation.
|
||||
- **Appui multiple** : tape des émojis.
|
||||
- **Appui 1-1-3** : tape des émojis ajoutés (vous devez en ajouter en utilisant [la touche Ajouter un Mot](#touche-ajouter-un-mot)).
|
||||
- **Appui long** : tape "1".
|
||||
- **En mode Cheonjiin (Coréen) :**
|
||||
- **Appui :** tape la voyelle "ㅣ".
|
||||
- **Appui long :** tape des caractères de ponctuation.
|
||||
- **Appui long, puis appui court :** tape des emojis.
|
||||
- **Appui long sur 1, appui sur 1, puis sur 3 :** tape des emojis ajoutés (vous devez en ajouter en utilisant [la touche Ajouter un Mot](#touche-ajouter-un-mot)).
|
||||
|
||||
#### Touche 2 à 9 :
|
||||
- **En mode 123** : tape le chiffre correspondant.
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue