mirror of
https://github.com/Helium314/HeliBoard.git
synced 2025-04-20 14:19:08 +00:00
Make layouts customizable (#361)
now layouts can be added from files, or default layouts can be copied and edited
This commit is contained in:
parent
450338c080
commit
54abf44628
21 changed files with 522 additions and 129 deletions
|
@ -22,6 +22,7 @@ Might end up on F-Droid...
|
||||||
* Number pad
|
* Number pad
|
||||||
* Show all available extra characters on long pressing a key
|
* Show all available extra characters on long pressing a key
|
||||||
* Backup your learned word / history data
|
* Backup your learned word / history data
|
||||||
|
* Add custom keyboard layouts
|
||||||
* Adjustable bottom padding
|
* Adjustable bottom padding
|
||||||
|
|
||||||
## Hidden functionality
|
## Hidden functionality
|
||||||
|
|
|
@ -438,4 +438,13 @@ public final class KeyboardLayoutSet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// used for testing keyboard layout files without actually creating a keyboard
|
||||||
|
public static KeyboardId getFakeKeyboardId(final int elementId) {
|
||||||
|
final Params params = new Params();
|
||||||
|
params.mEditorInfo = new EditorInfo();
|
||||||
|
params.mSubtype = RichInputMethodSubtype.getEmojiSubtype();
|
||||||
|
params.mSubtype.getKeyboardLayoutSetName();
|
||||||
|
return new KeyboardId(elementId, params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -300,7 +300,12 @@ public final class MoreKeysKeyboard extends Keyboard {
|
||||||
dividerWidth = 0;
|
dividerWidth = 0;
|
||||||
}
|
}
|
||||||
final MoreKeySpec[] moreKeys = key.getMoreKeys();
|
final MoreKeySpec[] moreKeys = key.getMoreKeys();
|
||||||
mParams.setParameters(moreKeys.length, key.getMoreKeysColumnNumber(), keyWidth,
|
final int defaultColumns = key.getMoreKeysColumnNumber();
|
||||||
|
final int spaceForKeys = keyboard.mId.mWidth / keyWidth;
|
||||||
|
final int finalNumColumns = spaceForKeys >= Math.min(moreKeys.length, defaultColumns)
|
||||||
|
? defaultColumns
|
||||||
|
: (spaceForKeys > 0 ? spaceForKeys : defaultColumns); // in last case setParameters will throw an exception
|
||||||
|
mParams.setParameters(moreKeys.length, finalNumColumns, keyWidth,
|
||||||
rowHeight, key.getX() + key.getWidth() / 2, keyboard.mId.mWidth,
|
rowHeight, key.getX() + key.getWidth() / 2, keyboard.mId.mWidth,
|
||||||
key.isMoreKeysFixedColumn(), key.isMoreKeysFixedOrder(), dividerWidth);
|
key.isMoreKeysFixedColumn(), key.isMoreKeysFixedOrder(), dividerWidth);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import androidx.appcompat.widget.AppCompatTextView;
|
||||||
|
|
||||||
import org.dslul.openboard.inputmethod.keyboard.Key;
|
import org.dslul.openboard.inputmethod.keyboard.Key;
|
||||||
import org.dslul.openboard.inputmethod.latin.R;
|
import org.dslul.openboard.inputmethod.latin.R;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.common.StringUtilsKt;
|
||||||
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
|
||||||
|
@ -66,6 +67,10 @@ public class KeyPreviewView extends AppCompatTextView {
|
||||||
if (sNoScaleXTextSet.contains(text)) {
|
if (sNoScaleXTextSet.contains(text)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (StringUtilsKt.isEmoji(text)) {
|
||||||
|
sNoScaleXTextSet.add(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// TODO: Override {@link #setBackground(Drawable)} that is supported from API 16 and
|
// TODO: Override {@link #setBackground(Drawable)} that is supported from API 16 and
|
||||||
// calculate maximum text width.
|
// calculate maximum text width.
|
||||||
final Drawable background = getBackground();
|
final Drawable background = getBackground();
|
||||||
|
|
|
@ -8,6 +8,7 @@ package org.dslul.openboard.inputmethod.keyboard.internal;
|
||||||
|
|
||||||
import org.dslul.openboard.inputmethod.latin.common.Constants;
|
import org.dslul.openboard.inputmethod.latin.common.Constants;
|
||||||
import org.dslul.openboard.inputmethod.latin.common.StringUtils;
|
import org.dslul.openboard.inputmethod.latin.common.StringUtils;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.define.DebugFlags;
|
||||||
|
|
||||||
import static org.dslul.openboard.inputmethod.latin.common.Constants.CODE_OUTPUT_TEXT;
|
import static org.dslul.openboard.inputmethod.latin.common.Constants.CODE_OUTPUT_TEXT;
|
||||||
import static org.dslul.openboard.inputmethod.latin.common.Constants.CODE_UNSPECIFIED;
|
import static org.dslul.openboard.inputmethod.latin.common.Constants.CODE_UNSPECIFIED;
|
||||||
|
@ -88,7 +89,9 @@ public final class KeySpecParser {
|
||||||
// Treat a sole vertical bar as a special case of key label.
|
// Treat a sole vertical bar as a special case of key label.
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
throw new KeySpecParserError("Empty label");
|
if (DebugFlags.DEBUG_ENABLED)
|
||||||
|
throw new KeySpecParserError("Empty label");
|
||||||
|
else return -1;
|
||||||
}
|
}
|
||||||
return labelEnd;
|
return labelEnd;
|
||||||
}
|
}
|
||||||
|
@ -118,7 +121,8 @@ public final class KeySpecParser {
|
||||||
if (indexOfLabelEnd(getAfterLabelEnd(keySpec, labelEnd)) < 0) {
|
if (indexOfLabelEnd(getAfterLabelEnd(keySpec, labelEnd)) < 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + keySpec);
|
if (DebugFlags.DEBUG_ENABLED)
|
||||||
|
throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + keySpec);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
@ -132,7 +136,7 @@ public final class KeySpecParser {
|
||||||
}
|
}
|
||||||
final int labelEnd = indexOfLabelEnd(keySpec);
|
final int labelEnd = indexOfLabelEnd(keySpec);
|
||||||
final String label = parseEscape(getBeforeLabelEnd(keySpec, labelEnd));
|
final String label = parseEscape(getBeforeLabelEnd(keySpec, labelEnd));
|
||||||
if (label.isEmpty()) {
|
if (label.isEmpty() && DebugFlags.DEBUG_ENABLED) {
|
||||||
throw new KeySpecParserError("Empty label: " + keySpec);
|
throw new KeySpecParserError("Empty label: " + keySpec);
|
||||||
}
|
}
|
||||||
return label;
|
return label;
|
||||||
|
@ -164,13 +168,13 @@ public final class KeySpecParser {
|
||||||
// See {@link #getCode(Resources, String)}.
|
// See {@link #getCode(Resources, String)}.
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (outputText.isEmpty()) {
|
if (outputText.isEmpty() && DebugFlags.DEBUG_ENABLED) {
|
||||||
throw new KeySpecParserError("Empty outputText: " + keySpec);
|
throw new KeySpecParserError("Empty outputText: " + keySpec);
|
||||||
}
|
}
|
||||||
return outputText;
|
return outputText;
|
||||||
}
|
}
|
||||||
final String label = getLabel(keySpec);
|
final String label = getLabel(keySpec);
|
||||||
if (label == null) {
|
if (label == null && DebugFlags.DEBUG_ENABLED) {
|
||||||
throw new KeySpecParserError("Empty label: " + keySpec);
|
throw new KeySpecParserError("Empty label: " + keySpec);
|
||||||
}
|
}
|
||||||
// Code is automatically generated for one letter label. See {@link getCode()}.
|
// Code is automatically generated for one letter label. See {@link getCode()}.
|
||||||
|
@ -198,7 +202,9 @@ public final class KeySpecParser {
|
||||||
}
|
}
|
||||||
final String label = getLabel(keySpec);
|
final String label = getLabel(keySpec);
|
||||||
if (label == null) {
|
if (label == null) {
|
||||||
throw new KeySpecParserError("Empty label: " + keySpec);
|
if (DebugFlags.DEBUG_ENABLED)
|
||||||
|
throw new KeySpecParserError("Empty label: " + keySpec);
|
||||||
|
else return CODE_OUTPUT_TEXT;
|
||||||
}
|
}
|
||||||
// Code is automatically generated for one letter label.
|
// Code is automatically generated for one letter label.
|
||||||
return (StringUtils.codePointCount(label) == 1) ? label.codePointAt(0) : CODE_OUTPUT_TEXT;
|
return (StringUtils.codePointCount(label) == 1) ? label.codePointAt(0) : CODE_OUTPUT_TEXT;
|
||||||
|
|
|
@ -47,66 +47,6 @@ open class KeyboardBuilder<KP : KeyboardParams>(protected val mContext: Context,
|
||||||
mParams.mAllowRedundantMoreKeys = enabled
|
mParams.mAllowRedundantMoreKeys = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: further plan
|
|
||||||
// after the old parser is removed
|
|
||||||
// maybe the language -> layout thing could be moved to assets? and maybe even here the extra keys could be defined...
|
|
||||||
// should be either both in method.xml, or both in assets (actually method might be more suitable)
|
|
||||||
// go through a lot of todos in parsers, key, keyboardlayoutset, ... as a lot of things should only change after old parser is removed
|
|
||||||
// also remove the keyboard_layout_set files?
|
|
||||||
// allow users to define their own layouts (maybe do everything else first?)
|
|
||||||
// need to solve the scaling issue with number row and 5 row keyboards
|
|
||||||
// write up how things work for users, also regarding language more keys
|
|
||||||
// readme, maybe also some "help" button in a dialog
|
|
||||||
// some sort of proper UI, or simply text input?
|
|
||||||
// better text import for the start because of much work
|
|
||||||
// ui follows later (consider that users need to be able to start from existing layouts!)
|
|
||||||
// some warning if more than 2 or 3 characters on a single label
|
|
||||||
// currently can't resize keys, but could set autoXScale (does only decrease size, never increase)
|
|
||||||
// careful about moreKeys: if moreKeys don't fit on screen, parser throws an exception!
|
|
||||||
// need to somehow test for this
|
|
||||||
// is that autoColumnOrder thing a workaround for that?
|
|
||||||
// still would crash for a single huge label
|
|
||||||
// potential keyspec parsing issues:
|
|
||||||
// MoreKeySpec constructor does things like KeySpecParser.getLabel and others
|
|
||||||
// these work with special characters like | and \ doing things depending on their position
|
|
||||||
// if used wrongly, things can crash
|
|
||||||
// -> maybe disable this style of parsing when creating MoreKeySpec of a user-provided layout
|
|
||||||
// or also for the simple layouts, because there is no need to have it in layouts
|
|
||||||
// does the same issue apply to normal key labels?
|
|
||||||
// popup and (single key) long press preview rescale the label on x only, which may deform emojis
|
|
||||||
// does glide typing work with multiple letters on one key? if not, users should be notified
|
|
||||||
// maybe allow users to define their own symbol and shift-symbol layouts
|
|
||||||
// allow users to import layouts, which essentially just fills the text from a file
|
|
||||||
|
|
||||||
// labelFlags should be set correctly
|
|
||||||
// alignHintLabelToBottom: on lxx and rounded themes, but did not find what it actually does...
|
|
||||||
// alignIconToBottom: space_key_for_number_layout
|
|
||||||
// alignLabelOffCenter: number keys in phone layout
|
|
||||||
// fontNormal: turkish (rows 1 and 2 only), .com, emojis, numModeKeyStyle, a bunch of non-latin languages
|
|
||||||
// -> switches to normal typeface, only relevant for holo which has bold
|
|
||||||
// fontMonoSpace: unused
|
|
||||||
// fontDefault: keyExclamationQuestion, a bunch of "normal" keys in fontNormal layouts like thai
|
|
||||||
// -> switches to default defined typeface, useful e.g. if row has fontNormal
|
|
||||||
// followKeyLargeLetterRatio: number keys in number/phone/numpad layouts
|
|
||||||
// followKeyLetterRatio: mode keys in number layouts, some keys in some non-latin layouts
|
|
||||||
// followKeyLabelRatio: enter key, some keys in phone layout (same as followKeyLetterRatio + followKeyLargeLetterRatio)
|
|
||||||
// followKeyHintLabelRatio: unused directly (but includes some others)
|
|
||||||
// hasPopupHint: basically the long-pressable functional keys
|
|
||||||
// hasShiftedLetterHint: period key and some keys on pcqwerty
|
|
||||||
// hasHintLabel: number keys in number layouts
|
|
||||||
// autoXScale: com key, action keys, some on phone layout, some non-latin languages
|
|
||||||
// autoScale: only one single letter in khmer layout (includes autoXScale)
|
|
||||||
// preserveCase: action key + more keys, com key, shift keys
|
|
||||||
// shiftedLetterActivated: period and some keys on pcqwerty, tablet only (wtf, when enabled can't open moreKeys -> remove? or what would be the use?)
|
|
||||||
// fromCustomActionLabel: action key with customLabelActionKeyStyle -> check parser where to get this info
|
|
||||||
// followFunctionalTextColor: number mode keys, action key
|
|
||||||
// keepBackgroundAspectRatio: lxx and rounded action more keys, lxx no-border action and emoji, moreKeys keyboard view
|
|
||||||
// disableKeyHintLabel: keys in pcqwerty row 1 and number row
|
|
||||||
// disableAdditionalMoreKeys: only keys in pcqwerty row 1 so there is no number row -> not necessary with the new layouts, just remove it completely
|
|
||||||
// maybe remove some of the flags? or keep supporting them?
|
|
||||||
// for pcqwerty: hasShiftedLetterHint -> hasShiftedLetterHint|shiftedLetterActivated when shift is enabled, need to consider if the flag is used
|
|
||||||
// actually period key also has shifted letter hint
|
|
||||||
|
|
||||||
fun load(xmlId: Int, id: KeyboardId): KeyboardBuilder<KP> {
|
fun load(xmlId: Int, id: KeyboardId): KeyboardBuilder<KP> {
|
||||||
mParams.mId = id
|
mParams.mId = id
|
||||||
if (id.isEmojiKeyboard) {
|
if (id.isEmojiKeyboard) {
|
||||||
|
@ -120,7 +60,10 @@ open class KeyboardBuilder<KP : KeyboardParams>(protected val mContext: Context,
|
||||||
mParams.mMoreKeyTypes.addAll(sv.mMoreKeyTypes)
|
mParams.mMoreKeyTypes.addAll(sv.mMoreKeyTypes)
|
||||||
// add label source only if moreKey type enabled
|
// add label source only if moreKey type enabled
|
||||||
sv.mMoreKeyLabelSources.forEach { if (it in sv.mMoreKeyTypes) mParams.mMoreKeyLabelSources.add(it) }
|
sv.mMoreKeyLabelSources.forEach { if (it in sv.mMoreKeyTypes) mParams.mMoreKeyLabelSources.add(it) }
|
||||||
keysInRows = KeyboardParser.parseFromAssets(mParams, mContext)
|
keysInRows = if (mParams.mId.isAlphabetKeyboard && mParams.mId.mSubtype.isCustom)
|
||||||
|
KeyboardParser.parseCustom(mParams, mContext)
|
||||||
|
else
|
||||||
|
KeyboardParser.parseFromAssets(mParams, mContext)
|
||||||
determineAbsoluteValues()
|
determineAbsoluteValues()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "error parsing layout $id ${id.mElementId}", e)
|
Log.e(TAG, "error parsing layout $id ${id.mElementId}", e)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
package org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser
|
package org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -6,7 +7,6 @@ import android.content.res.Resources
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.Log
|
import org.dslul.openboard.inputmethod.latin.utils.Log
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import org.dslul.openboard.inputmethod.keyboard.Key
|
import org.dslul.openboard.inputmethod.keyboard.Key
|
||||||
import org.dslul.openboard.inputmethod.keyboard.Key.KeyParams
|
import org.dslul.openboard.inputmethod.keyboard.Key.KeyParams
|
||||||
|
@ -19,16 +19,19 @@ import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.
|
||||||
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.SimplePopups
|
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.floris.SimplePopups
|
||||||
import org.dslul.openboard.inputmethod.latin.R
|
import org.dslul.openboard.inputmethod.latin.R
|
||||||
import org.dslul.openboard.inputmethod.latin.common.Constants
|
import org.dslul.openboard.inputmethod.latin.common.Constants
|
||||||
|
import org.dslul.openboard.inputmethod.latin.common.isEmoji
|
||||||
import org.dslul.openboard.inputmethod.latin.common.splitOnWhitespace
|
import org.dslul.openboard.inputmethod.latin.common.splitOnWhitespace
|
||||||
import org.dslul.openboard.inputmethod.latin.define.DebugFlags
|
import org.dslul.openboard.inputmethod.latin.define.DebugFlags
|
||||||
import org.dslul.openboard.inputmethod.latin.settings.Settings
|
import org.dslul.openboard.inputmethod.latin.settings.Settings
|
||||||
import org.dslul.openboard.inputmethod.latin.spellcheck.AndroidSpellCheckerService
|
import org.dslul.openboard.inputmethod.latin.spellcheck.AndroidSpellCheckerService
|
||||||
|
import org.dslul.openboard.inputmethod.latin.utils.CUSTOM_LAYOUT_PREFIX
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.InputTypeUtils
|
import org.dslul.openboard.inputmethod.latin.utils.InputTypeUtils
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.MORE_KEYS_LAYOUT
|
import org.dslul.openboard.inputmethod.latin.utils.MORE_KEYS_LAYOUT
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.MORE_KEYS_NUMBER
|
import org.dslul.openboard.inputmethod.latin.utils.MORE_KEYS_NUMBER
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.RunInLocale
|
import org.dslul.openboard.inputmethod.latin.utils.RunInLocale
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.ScriptUtils
|
import org.dslul.openboard.inputmethod.latin.utils.ScriptUtils
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.sumOf
|
import org.dslul.openboard.inputmethod.latin.utils.sumOf
|
||||||
|
import java.io.File
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -111,10 +114,8 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
|
||||||
// setting the correct moreKeys is handled in PopupSet
|
// setting the correct moreKeys is handled in PopupSet
|
||||||
// not for korean/lao/thai layouts, todo: should be decided in the layout / layoutInfos, not in the parser
|
// not for korean/lao/thai layouts, todo: should be decided in the layout / layoutInfos, not in the parser
|
||||||
baseKeys.first().take(10).forEachIndexed { index, keyData -> keyData.popup.numberIndex = index }
|
baseKeys.first().take(10).forEachIndexed { index, keyData -> keyData.popup.numberIndex = index }
|
||||||
if (DebugFlags.DEBUG_ENABLED && baseKeys.first().size < 10) {
|
if (baseKeys.first().size < 10) {
|
||||||
val message = "first row only has ${baseKeys.first().size} keys: ${baseKeys.first().map { it.label }}"
|
Log.w(TAG, "first row only has ${baseKeys.first().size} keys: ${baseKeys.first().map { it.label }}")
|
||||||
Log.w(TAG, message)
|
|
||||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,9 +189,10 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
|
||||||
}
|
}
|
||||||
|
|
||||||
for (key in row) {
|
for (key in row) {
|
||||||
// todo: maybe autoScale / autoXScale if label has more than 2 characters (exception for emojis?)
|
val extraFlags = if (key.label.length > 2 && key.label.codePointCount(0, key.label.length) > 2 && !isEmoji(key.label))
|
||||||
// but that could also be determined in toKeyParams
|
Key.LABEL_FLAGS_AUTO_X_SCALE
|
||||||
val keyParams = key.compute(params).toKeyParams(params, keyWidth, defaultLabelFlags)
|
else 0
|
||||||
|
val keyParams = key.compute(params).toKeyParams(params, keyWidth, defaultLabelFlags or extraFlags)
|
||||||
paramsRow.add(keyParams)
|
paramsRow.add(keyParams)
|
||||||
if (DebugFlags.DEBUG_ENABLED)
|
if (DebugFlags.DEBUG_ENABLED)
|
||||||
Log.d(TAG, "adding key ${keyParams.mLabel}, ${keyParams.mCode}")
|
Log.d(TAG, "adding key ${keyParams.mLabel}, ${keyParams.mCode}")
|
||||||
|
@ -239,6 +241,8 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
|
||||||
val rowAboveLastNormalRowKeyWidth = rowAboveLastNormalRow.first { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }.mRelativeWidth
|
val rowAboveLastNormalRowKeyWidth = rowAboveLastNormalRow.first { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }.mRelativeWidth
|
||||||
if (lastNormalRowKeyWidth <= rowAboveLastNormalRowKeyWidth + 0.0001f)
|
if (lastNormalRowKeyWidth <= rowAboveLastNormalRowKeyWidth + 0.0001f)
|
||||||
return // no need
|
return // no need
|
||||||
|
if (lastNormalRowKeyWidth / rowAboveLastNormalRowKeyWidth > 1.1f)
|
||||||
|
return // don't resize on large size difference
|
||||||
if (lastNormalRow.any { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL && it.mRelativeWidth != lastNormalRowKeyWidth })
|
if (lastNormalRow.any { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL && it.mRelativeWidth != lastNormalRowKeyWidth })
|
||||||
return // normal keys have different width, don't deal with this
|
return // normal keys have different width, don't deal with this
|
||||||
val numberOfNormalKeys = lastNormalRow.count { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }
|
val numberOfNormalKeys = lastNormalRow.count { it.mBackgroundType == Key.BACKGROUND_TYPE_NORMAL }
|
||||||
|
@ -667,10 +671,7 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
|
||||||
return this
|
return this
|
||||||
val id = context.resources.getIdentifier("label_$this", "string", context.packageName)
|
val id = context.resources.getIdentifier("label_$this", "string", context.packageName)
|
||||||
if (id == 0) {
|
if (id == 0) {
|
||||||
val message = "no resource for label $this in ${params.mId}"
|
Log.w(TAG, "no resource for label $this in ${params.mId}")
|
||||||
Log.w(TAG, message)
|
|
||||||
if (DebugFlags.DEBUG_ENABLED)
|
|
||||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
return getInLocale(id)
|
return getInLocale(id)
|
||||||
|
@ -782,6 +783,15 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = KeyboardParser::class.simpleName
|
private val TAG = KeyboardParser::class.simpleName
|
||||||
|
|
||||||
|
fun parseCustom(params: KeyboardParams, context: Context): ArrayList<ArrayList<KeyParams>> {
|
||||||
|
val layoutName = params.mId.mSubtype.keyboardLayoutSetName
|
||||||
|
val f = File(context.filesDir, "layouts${File.separator}$layoutName")
|
||||||
|
return if (layoutName.endsWith(".json"))
|
||||||
|
JsonKeyboardParser(params, context).parseLayoutString(f.readText())
|
||||||
|
else
|
||||||
|
SimpleKeyboardParser(params, context).parseLayoutString(f.readText())
|
||||||
|
}
|
||||||
|
|
||||||
fun parseFromAssets(params: KeyboardParams, context: Context): ArrayList<ArrayList<KeyParams>> {
|
fun parseFromAssets(params: KeyboardParams, context: Context): ArrayList<ArrayList<KeyParams>> {
|
||||||
val id = params.mId
|
val id = params.mId
|
||||||
val layoutName = params.mId.mSubtype.keyboardLayoutSetName.substringBefore("+")
|
val layoutName = params.mId.mSubtype.keyboardLayoutSetName.substringBefore("+")
|
||||||
|
@ -812,21 +822,20 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
|
||||||
// touchPositionCorrectionData needs to be loaded, currently always holo is applied in readAttributes
|
// touchPositionCorrectionData needs to be loaded, currently always holo is applied in readAttributes
|
||||||
private fun layoutInfos(params: KeyboardParams): LayoutInfos {
|
private fun layoutInfos(params: KeyboardParams): LayoutInfos {
|
||||||
val name = params.mId.mSubtype.keyboardLayoutSetName
|
val name = params.mId.mSubtype.keyboardLayoutSetName
|
||||||
val labelFlags = if (!params.mId.isAlphabetKeyboard) 0 else when (name) {
|
val language = params.mId.locale.language
|
||||||
"armenian_phonetic", "arabic", "arabic_pc", "bengali", "bengali_akkhor", "bengali_unijoy",
|
val labelFlags = if (!params.mId.isAlphabetKeyboard) 0 else when (language) {
|
||||||
"farsi", "hindi", "hindi_compact", "lao", "marathi", "nepali_romanized", "nepali_traditional",
|
"hy", "ar", "be", "fa", "hi", "lo", "mr", "ne", "th", "ur" -> Key.LABEL_FLAGS_FONT_NORMAL
|
||||||
"thai", "urdu" -> Key.LABEL_FLAGS_FONT_NORMAL
|
"kn", "km", "ml", "si", "ta", "te" -> Key.LABEL_FLAGS_FONT_NORMAL or Key.LABEL_FLAGS_AUTO_X_SCALE
|
||||||
"kannada", "khmer", "malayalam", "sinhala", "tamil", "telugu" -> Key.LABEL_FLAGS_FONT_NORMAL or Key.LABEL_FLAGS_AUTO_X_SCALE
|
|
||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
// only for alphabet, but some exceptions for shift layouts
|
// only for alphabet, but some exceptions for shift layouts
|
||||||
val enableProximityCharsCorrection = params.mId.isAlphabetKeyboard && when (name) {
|
val enableProximityCharsCorrection = params.mId.isAlphabetKeyboard && when (name) {
|
||||||
// todo: test effect on correction (just add qwerty to the list for testing)
|
// todo: test effect on correction (just add qwerty to the list for testing)
|
||||||
"akkhor", "georgian", "hindi", "lao", "nepali_romanized", "nepali_traditional", "sinhala", "thai" ->
|
"bengali_akkhor", "georgian", "hindi", "lao", "nepali_romanized", "nepali_traditional", "sinhala", "thai" ->
|
||||||
params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET
|
params.mId.mElementId == KeyboardId.ELEMENT_ALPHABET
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
val allowRedundantMoreKeys = name != "nordic" && name != "serbian_qwertz" && params.mId.mElementId != KeyboardId.ELEMENT_SYMBOLS
|
val allowRedundantMoreKeys = params.mId.mElementId != KeyboardId.ELEMENT_SYMBOLS // todo: always set to false?
|
||||||
// essentially this is default for 4 row and non-alphabet layouts, maybe this could be determined automatically instead of using a list
|
// essentially this is default for 4 row and non-alphabet layouts, maybe this could be determined automatically instead of using a list
|
||||||
// todo: check the difference between default (i.e. none) and holo (test behavior on keyboard)
|
// todo: check the difference between default (i.e. none) and holo (test behavior on keyboard)
|
||||||
// todo: null for MoreKeysKeyboard only
|
// todo: null for MoreKeysKeyboard only
|
||||||
|
@ -834,6 +843,7 @@ abstract class KeyboardParser(private val params: KeyboardParams, private val co
|
||||||
R.array.touch_position_correction_data_default
|
R.array.touch_position_correction_data_default
|
||||||
else R.array.touch_position_correction_data_holo
|
else R.array.touch_position_correction_data_holo
|
||||||
val hasZwnjKey = params.mId.locale.language in listOf("fa", "ne", "kn", "te") // determine from language, user might have custom layout
|
val hasZwnjKey = params.mId.locale.language in listOf("fa", "ne", "kn", "te") // determine from language, user might have custom layout
|
||||||
|
// custom non-json layout for non-uppercase language should not have shift key
|
||||||
val hasShiftKey = !params.mId.isAlphabetKeyboard || name !in listOf("hindi_compact", "bengali", "arabic", "arabic_pc", "hebrew", "kannada", "malayalam", "marathi", "farsi", "tamil", "telugu")
|
val hasShiftKey = !params.mId.isAlphabetKeyboard || name !in listOf("hindi_compact", "bengali", "arabic", "arabic_pc", "hebrew", "kannada", "malayalam", "marathi", "farsi", "tamil", "telugu")
|
||||||
return LayoutInfos(labelFlags, enableProximityCharsCorrection, allowRedundantMoreKeys, touchPositionCorrectionData, hasZwnjKey, hasShiftKey)
|
return LayoutInfos(labelFlags, enableProximityCharsCorrection, allowRedundantMoreKeys, touchPositionCorrectionData, hasZwnjKey, hasShiftKey)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ class SimpleKeyboardParser(private val params: KeyboardParams, private val conte
|
||||||
context.assets.open("layouts/$layoutName.txt").reader().readText()
|
context.assets.open("layouts/$layoutName.txt").reader().readText()
|
||||||
|
|
||||||
override fun parseCoreLayout(layoutContent: String): MutableList<List<KeyData>> {
|
override fun parseCoreLayout(layoutContent: String): MutableList<List<KeyData>> {
|
||||||
val rowStrings = layoutContent.replace("\r\n", "\n").split("\n\n")
|
val rowStrings = layoutContent.replace("\r\n", "\n").split("\\n\\s*\\n".toRegex())
|
||||||
return rowStrings.mapIndexedNotNullTo(mutableListOf()) { i, row ->
|
return rowStrings.mapIndexedNotNullTo(mutableListOf()) { i, row ->
|
||||||
if (row.isBlank()) return@mapIndexedNotNullTo null
|
if (row.isBlank()) return@mapIndexedNotNullTo null
|
||||||
if (addExtraKeys)
|
if (addExtraKeys)
|
||||||
|
|
|
@ -7,12 +7,13 @@
|
||||||
package org.dslul.openboard.inputmethod.latin;
|
package org.dslul.openboard.inputmethod.latin;
|
||||||
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.Log;
|
|
||||||
import android.view.inputmethod.InputMethodSubtype;
|
import android.view.inputmethod.InputMethodSubtype;
|
||||||
|
|
||||||
import org.dslul.openboard.inputmethod.compat.InputMethodSubtypeCompatUtils;
|
import org.dslul.openboard.inputmethod.compat.InputMethodSubtypeCompatUtils;
|
||||||
import org.dslul.openboard.inputmethod.latin.common.Constants;
|
import org.dslul.openboard.inputmethod.latin.common.Constants;
|
||||||
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils;
|
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.utils.CustomLayoutUtilsKt;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.utils.Log;
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.SubtypeLocaleUtils;
|
import org.dslul.openboard.inputmethod.latin.utils.SubtypeLocaleUtils;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -72,6 +73,10 @@ public class RichInputMethodSubtype {
|
||||||
return SubtypeLocaleUtils.NO_LANGUAGE.equals(mSubtype.getLocale());
|
return SubtypeLocaleUtils.NO_LANGUAGE.equals(mSubtype.getLocale());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isCustom() {
|
||||||
|
return getKeyboardLayoutSetName().startsWith(CustomLayoutUtilsKt.CUSTOM_LAYOUT_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
public String getNameForLogging() {
|
public String getNameForLogging() {
|
||||||
return toString();
|
return toString();
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -53,6 +53,7 @@ class AdvancedSettingsFragment : SubScreenFragment() {
|
||||||
private var libfile: File? = null
|
private var libfile: File? = null
|
||||||
private val backupFilePatterns by lazy { listOf(
|
private val backupFilePatterns by lazy { listOf(
|
||||||
"blacklists/.*\\.txt".toRegex(),
|
"blacklists/.*\\.txt".toRegex(),
|
||||||
|
"layouts/.*.(txt|json)".toRegex(),
|
||||||
"dicts/.*/.*user\\.dict".toRegex(),
|
"dicts/.*/.*user\\.dict".toRegex(),
|
||||||
"userunigram.*/userunigram.*\\.(body|header)".toRegex(),
|
"userunigram.*/userunigram.*\\.(body|header)".toRegex(),
|
||||||
"UserHistoryDictionary.*/UserHistoryDictionary.*\\.(body|header)".toRegex(),
|
"UserHistoryDictionary.*/UserHistoryDictionary.*\\.(body|header)".toRegex(),
|
||||||
|
|
|
@ -4,10 +4,12 @@ package org.dslul.openboard.inputmethod.latin.settings
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.text.InputType
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.widget.EditText
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.ScrollView
|
import android.widget.ScrollView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
@ -17,10 +19,13 @@ import androidx.core.view.get
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.size
|
import androidx.core.view.size
|
||||||
|
import androidx.core.widget.doAfterTextChanged
|
||||||
import org.dslul.openboard.inputmethod.dictionarypack.DictionaryPackConstants
|
import org.dslul.openboard.inputmethod.dictionarypack.DictionaryPackConstants
|
||||||
import org.dslul.openboard.inputmethod.keyboard.KeyboardLayoutSet
|
import org.dslul.openboard.inputmethod.keyboard.KeyboardLayoutSet
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.KeyboardSwitcher
|
||||||
import org.dslul.openboard.inputmethod.latin.BinaryDictionaryGetter
|
import org.dslul.openboard.inputmethod.latin.BinaryDictionaryGetter
|
||||||
import org.dslul.openboard.inputmethod.latin.R
|
import org.dslul.openboard.inputmethod.latin.R
|
||||||
|
import org.dslul.openboard.inputmethod.latin.common.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET
|
||||||
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils
|
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils
|
||||||
import org.dslul.openboard.inputmethod.latin.databinding.LanguageListItemBinding
|
import org.dslul.openboard.inputmethod.latin.databinding.LanguageListItemBinding
|
||||||
import org.dslul.openboard.inputmethod.latin.databinding.LocaleSettingsDialogBinding
|
import org.dslul.openboard.inputmethod.latin.databinding.LocaleSettingsDialogBinding
|
||||||
|
@ -69,7 +74,7 @@ class LanguageSettingsDialog(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fillSubtypesView() {
|
private fun fillSubtypesView() {
|
||||||
if (infos.any { it.subtype.isAsciiCapable }) { // currently can only add subtypes for latin keyboards
|
if (infos.first().subtype.isAsciiCapable) {
|
||||||
binding.addSubtype.setOnClickListener {
|
binding.addSubtype.setOnClickListener {
|
||||||
val layouts = context.resources.getStringArray(R.array.predefined_layouts)
|
val layouts = context.resources.getStringArray(R.array.predefined_layouts)
|
||||||
.filterNot { layoutName -> infos.any { SubtypeLocaleUtils.getKeyboardLayoutSetName(it.subtype) == layoutName } }
|
.filterNot { layoutName -> infos.any { SubtypeLocaleUtils.getKeyboardLayoutSetName(it.subtype) == layoutName } }
|
||||||
|
@ -78,19 +83,14 @@ class LanguageSettingsDialog(
|
||||||
.setTitle(R.string.keyboard_layout_set)
|
.setTitle(R.string.keyboard_layout_set)
|
||||||
.setItems(displayNames.toTypedArray()) { di, i ->
|
.setItems(displayNames.toTypedArray()) { di, i ->
|
||||||
di.dismiss()
|
di.dismiss()
|
||||||
val newSubtype = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(mainLocaleString, layouts[i])
|
addSubtype(layouts[i])
|
||||||
val newSubtypeInfo = newSubtype.toSubtypeInfo(mainLocale, context, true, infos.first().hasDictionary) // enabled by default, because why else add them
|
|
||||||
addAdditionalSubtype(prefs, context.resources, newSubtype)
|
|
||||||
addEnabledSubtype(prefs, newSubtype)
|
|
||||||
addSubtypeToView(newSubtypeInfo)
|
|
||||||
infos.add(newSubtypeInfo)
|
|
||||||
reloadSetting()
|
|
||||||
}
|
}
|
||||||
|
.setNeutralButton(R.string.button_title_add_custom_layout) { _, _ -> onClickAddCustomSubtype() }
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
} else
|
} else
|
||||||
binding.addSubtype.isGone = true
|
binding.addSubtype.setOnClickListener { onClickAddCustomSubtype() }
|
||||||
|
|
||||||
// add subtypes
|
// add subtypes
|
||||||
infos.sortedBy { it.displayName }.forEach {
|
infos.sortedBy { it.displayName }.forEach {
|
||||||
|
@ -98,12 +98,80 @@ class LanguageSettingsDialog(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun addSubtype(name: String) {
|
||||||
|
val newSubtype = AdditionalSubtypeUtils.createEmojiCapableAdditionalSubtype(mainLocaleString, name, infos.first().subtype.isAsciiCapable)
|
||||||
|
val newSubtypeInfo = newSubtype.toSubtypeInfo(mainLocale, context, true, infos.first().hasDictionary) // enabled by default
|
||||||
|
val displayName = SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(newSubtype)
|
||||||
|
val old = infos.firstOrNull { isAdditionalSubtype(it.subtype) && displayName == SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(it.subtype) }
|
||||||
|
if (old != null) {
|
||||||
|
KeyboardSwitcher.getInstance().forceUpdateKeyboardTheme(context)
|
||||||
|
reloadSetting()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
addAdditionalSubtype(prefs, context.resources, newSubtype)
|
||||||
|
addEnabledSubtype(prefs, newSubtype)
|
||||||
|
addSubtypeToView(newSubtypeInfo)
|
||||||
|
KeyboardLayoutSet.onKeyboardThemeChanged()
|
||||||
|
infos.add(newSubtypeInfo)
|
||||||
|
reloadSetting()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onClickAddCustomSubtype() {
|
||||||
|
val link = "<a href='$LAYOUT_FORMAT_URL'>" + context.getString(R.string.dictionary_link_text) + "</a>"
|
||||||
|
val message = SpannableStringUtils.fromHtml(context.getString(R.string.message_add_custom_layout, link))
|
||||||
|
val dialog = Builder(context)
|
||||||
|
.setTitle(R.string.button_title_add_custom_layout)
|
||||||
|
.setMessage(message)
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.setNeutralButton(R.string.button_copy_existing_layout) { _, _ -> copyLayout() }
|
||||||
|
.setPositiveButton(R.string.button_load_custom_layout) { _, _ -> fragment?.requestLayoutFile() }
|
||||||
|
.create()
|
||||||
|
dialog.show()
|
||||||
|
(dialog.findViewById<View>(android.R.id.message) as? TextView)?.movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyLayout() {
|
||||||
|
val layouts = mutableListOf<String>()
|
||||||
|
val displayNames = mutableListOf<String>()
|
||||||
|
if (infos.first().subtype.isAsciiCapable) {
|
||||||
|
layouts.addAll(context.resources.getStringArray(R.array.predefined_layouts))
|
||||||
|
layouts.forEach { displayNames.add(SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(it) ?: it) }
|
||||||
|
}
|
||||||
|
infos.forEach {
|
||||||
|
val layoutSetName = it.subtype.getExtraValueOf(KEYBOARD_LAYOUT_SET)
|
||||||
|
if (layoutSetName?.startsWith(CUSTOM_LAYOUT_PREFIX) == false) { // don't allow copying custom layout (at least for now)
|
||||||
|
layouts.add(layoutSetName)
|
||||||
|
displayNames.add(SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(it.subtype))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Builder(context)
|
||||||
|
.setTitle(R.string.keyboard_layout_set)
|
||||||
|
.setItems(displayNames.toTypedArray()) { di, i ->
|
||||||
|
di.dismiss()
|
||||||
|
val fileName = context.assets.list("layouts")!!.firstOrNull { it.startsWith(layouts[i]) } ?: return@setItems
|
||||||
|
loadCustomLayout(context.assets.open("layouts${File.separator}$fileName").reader().readText(), layouts[i], mainLocaleString, context) { addSubtype(it) }
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewLayoutFile(uri: Uri?) {
|
||||||
|
loadCustomLayout(uri, mainLocaleString, context) { addSubtype(it) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun addSubtypeToView(subtype: SubtypeInfo) {
|
private fun addSubtypeToView(subtype: SubtypeInfo) {
|
||||||
val row = LayoutInflater.from(context).inflate(R.layout.language_list_item, listView)
|
val row = LayoutInflater.from(context).inflate(R.layout.language_list_item, listView)
|
||||||
|
val layoutSetName: String? = subtype.subtype.getExtraValueOf(KEYBOARD_LAYOUT_SET)
|
||||||
row.findViewById<TextView>(R.id.language_name).text =
|
row.findViewById<TextView>(R.id.language_name).text =
|
||||||
SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(subtype.subtype)
|
SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(subtype.subtype)
|
||||||
?: SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype.subtype)
|
?: SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype.subtype)
|
||||||
row.findViewById<View>(R.id.language_details).isGone = true
|
if (layoutSetName?.startsWith(CUSTOM_LAYOUT_PREFIX) == true) {
|
||||||
|
row.findViewById<TextView>(R.id.language_details).setText(R.string.edit_layout)
|
||||||
|
row.findViewById<View>(R.id.language_text).setOnClickListener { editCustomLayout(layoutSetName, context) }
|
||||||
|
} else {
|
||||||
|
row.findViewById<View>(R.id.language_details).isGone = true
|
||||||
|
}
|
||||||
row.findViewById<SwitchCompat>(R.id.language_switch).apply {
|
row.findViewById<SwitchCompat>(R.id.language_switch).apply {
|
||||||
isChecked = subtype.isEnabled
|
isChecked = subtype.isEnabled
|
||||||
isEnabled = !onlySystemLocales
|
isEnabled = !onlySystemLocales
|
||||||
|
@ -127,6 +195,8 @@ class LanguageSettingsDialog(
|
||||||
// can be re-added easily, no need for confirmation dialog
|
// can be re-added easily, no need for confirmation dialog
|
||||||
binding.subtypes.removeView(row)
|
binding.subtypes.removeView(row)
|
||||||
infos.remove(subtype)
|
infos.remove(subtype)
|
||||||
|
if (layoutSetName?.startsWith(CUSTOM_LAYOUT_PREFIX) == true)
|
||||||
|
removeCustomLayoutFile(layoutSetName, context)
|
||||||
|
|
||||||
removeAdditionalSubtype(prefs, context.resources, subtype.subtype)
|
removeAdditionalSubtype(prefs, context.resources, subtype.subtype)
|
||||||
removeEnabledSubtype(prefs, subtype.subtype)
|
removeEnabledSubtype(prefs, subtype.subtype)
|
||||||
|
@ -353,3 +423,5 @@ private fun getAvailableSecondaryLocales(context: Context, mainLocaleString: Str
|
||||||
}
|
}
|
||||||
return locales
|
return locales
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val LAYOUT_FORMAT_URL = "https://github.com/Helium314/openboard/blob/new/layouts.md"
|
||||||
|
|
|
@ -42,6 +42,12 @@ class LanguageSettingsFragment : Fragment(R.layout.language_settings) {
|
||||||
listener?.onNewDictionary(uri)
|
listener?.onNewDictionary(uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val layoutFilePicker = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
if (it.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
||||||
|
val uri = it.data?.data ?: return@registerForActivityResult
|
||||||
|
listener?.onNewLayoutFile(uri)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
sharedPreferences = DeviceProtectedUtils.getSharedPreferences(requireContext())
|
sharedPreferences = DeviceProtectedUtils.getSharedPreferences(requireContext())
|
||||||
|
@ -180,6 +186,7 @@ class LanguageSettingsFragment : Fragment(R.layout.language_settings) {
|
||||||
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
fun onNewDictionary(uri: Uri?)
|
fun onNewDictionary(uri: Uri?)
|
||||||
|
fun onNewLayoutFile(uri: Uri?)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var listener: Listener? = null
|
private var listener: Listener? = null
|
||||||
|
@ -195,6 +202,14 @@ class LanguageSettingsFragment : Fragment(R.layout.language_settings) {
|
||||||
dictionaryFilePicker.launch(intent)
|
dictionaryFilePicker.launch(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun requestLayoutFile() {
|
||||||
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||||
|
.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
// todo: any working way to allow only json and text files?
|
||||||
|
.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("text/*", "application/octet-stream")) // doesn't allow opening json files with "application/json"
|
||||||
|
.setType("*/*")
|
||||||
|
layoutFilePicker.launch(intent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SubtypeInfo(val displayName: String, val subtype: InputMethodSubtype, var isEnabled: Boolean, var hasDictionary: Boolean) {
|
class SubtypeInfo(val displayName: String, val subtype: InputMethodSubtype, var isEnabled: Boolean, var hasDictionary: Boolean) {
|
||||||
|
|
|
@ -16,9 +16,12 @@ import org.dslul.openboard.inputmethod.latin.R
|
||||||
import org.dslul.openboard.inputmethod.latin.RichInputMethodManager
|
import org.dslul.openboard.inputmethod.latin.RichInputMethodManager
|
||||||
import org.dslul.openboard.inputmethod.latin.define.DebugFlags
|
import org.dslul.openboard.inputmethod.latin.define.DebugFlags
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.AdditionalSubtypeUtils
|
import org.dslul.openboard.inputmethod.latin.utils.AdditionalSubtypeUtils
|
||||||
|
import org.dslul.openboard.inputmethod.latin.utils.CUSTOM_LAYOUT_PREFIX
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.DeviceProtectedUtils
|
import org.dslul.openboard.inputmethod.latin.utils.DeviceProtectedUtils
|
||||||
|
import org.dslul.openboard.inputmethod.latin.utils.Log
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.SubtypeLocaleUtils
|
import org.dslul.openboard.inputmethod.latin.utils.SubtypeLocaleUtils
|
||||||
import org.xmlpull.v1.XmlPullParser
|
import org.xmlpull.v1.XmlPullParser
|
||||||
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
import kotlin.collections.LinkedHashMap
|
import kotlin.collections.LinkedHashMap
|
||||||
|
@ -39,15 +42,15 @@ fun getAllAvailableSubtypes(): List<InputMethodSubtype> {
|
||||||
return resourceSubtypesByLocale.values.flatten() + additionalSubtypes
|
return resourceSubtypesByLocale.values.flatten() + additionalSubtypes
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addEnabledSubtype(prefs: SharedPreferences, subtype: InputMethodSubtype) {
|
fun addEnabledSubtype(prefs: SharedPreferences, newSubtype: InputMethodSubtype) {
|
||||||
require(initialized)
|
require(initialized)
|
||||||
val subtypeString = subtype.prefString()
|
val subtypeString = newSubtype.prefString()
|
||||||
val oldSubtypeStrings = prefs.getString(Settings.PREF_ENABLED_INPUT_STYLES, "")!!.split(SUBTYPE_SEPARATOR)
|
val oldSubtypeStrings = prefs.getString(Settings.PREF_ENABLED_INPUT_STYLES, "")!!.split(SUBTYPE_SEPARATOR)
|
||||||
val newString = (oldSubtypeStrings + subtypeString).filter { it.isNotBlank() }.toSortedSet().joinToString(SUBTYPE_SEPARATOR)
|
val newString = (oldSubtypeStrings + subtypeString).filter { it.isNotBlank() }.toSortedSet().joinToString(SUBTYPE_SEPARATOR)
|
||||||
prefs.edit { putString(Settings.PREF_ENABLED_INPUT_STYLES, newString) }
|
prefs.edit { putString(Settings.PREF_ENABLED_INPUT_STYLES, newString) }
|
||||||
|
|
||||||
if (subtype !in enabledSubtypes) {
|
if (newSubtype !in enabledSubtypes) {
|
||||||
enabledSubtypes.add(subtype)
|
enabledSubtypes.add(newSubtype)
|
||||||
enabledSubtypes.sortBy { it.locale() } // for consistent order
|
enabledSubtypes.sortBy { it.locale() } // for consistent order
|
||||||
RichInputMethodManager.getInstance().refreshSubtypeCaches()
|
RichInputMethodManager.getInstance().refreshSubtypeCaches()
|
||||||
}
|
}
|
||||||
|
@ -63,8 +66,9 @@ fun removeEnabledSubtype(prefs: SharedPreferences, subtype: InputMethodSubtype)
|
||||||
|
|
||||||
fun addAdditionalSubtype(prefs: SharedPreferences, resources: Resources, subtype: InputMethodSubtype) {
|
fun addAdditionalSubtype(prefs: SharedPreferences, resources: Resources, subtype: InputMethodSubtype) {
|
||||||
val oldAdditionalSubtypesString = Settings.readPrefAdditionalSubtypes(prefs, resources)
|
val oldAdditionalSubtypesString = Settings.readPrefAdditionalSubtypes(prefs, resources)
|
||||||
val oldAdditionalSubtypes = AdditionalSubtypeUtils.createAdditionalSubtypesArray(oldAdditionalSubtypesString).toSet()
|
val additionalSubtypes = AdditionalSubtypeUtils.createAdditionalSubtypesArray(oldAdditionalSubtypesString).toMutableSet()
|
||||||
val newAdditionalSubtypesString = AdditionalSubtypeUtils.createPrefSubtypes((oldAdditionalSubtypes + subtype).toTypedArray())
|
additionalSubtypes.add(subtype)
|
||||||
|
val newAdditionalSubtypesString = AdditionalSubtypeUtils.createPrefSubtypes(additionalSubtypes.toTypedArray())
|
||||||
Settings.writePrefAdditionalSubtypes(prefs, newAdditionalSubtypesString)
|
Settings.writePrefAdditionalSubtypes(prefs, newAdditionalSubtypesString)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,6 +147,7 @@ fun init(context: Context) {
|
||||||
reloadSystemLocales(context)
|
reloadSystemLocales(context)
|
||||||
|
|
||||||
loadResourceSubtypes(context.resources)
|
loadResourceSubtypes(context.resources)
|
||||||
|
removeInvalidCustomSubtypes(context)
|
||||||
loadAdditionalSubtypes(context)
|
loadAdditionalSubtypes(context)
|
||||||
loadEnabledSubtypes(context)
|
loadEnabledSubtypes(context)
|
||||||
initialized = true
|
initialized = true
|
||||||
|
@ -214,6 +219,23 @@ private fun loadAdditionalSubtypes(context: Context) {
|
||||||
additionalSubtypes.addAll(subtypes)
|
additionalSubtypes.addAll(subtypes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remove custom subtypes without a layout file
|
||||||
|
private fun removeInvalidCustomSubtypes(context: Context) {
|
||||||
|
val prefs = DeviceProtectedUtils.getSharedPreferences(context)
|
||||||
|
val additionalSubtypes = Settings.readPrefAdditionalSubtypes(prefs, context.resources).split(";")
|
||||||
|
val customSubtypeFiles by lazy { File(context.filesDir, "layouts").list() }
|
||||||
|
val subtypesToRemove = mutableListOf<String>()
|
||||||
|
additionalSubtypes.forEach {
|
||||||
|
val name = it.substringAfter(":").substringBefore(":")
|
||||||
|
if (!name.startsWith(CUSTOM_LAYOUT_PREFIX)) return@forEach
|
||||||
|
if (name !in customSubtypeFiles)
|
||||||
|
subtypesToRemove.add(it)
|
||||||
|
}
|
||||||
|
if (subtypesToRemove.isEmpty()) return
|
||||||
|
Log.w(TAG, "removing custom subtypes without files: $subtypesToRemove")
|
||||||
|
Settings.writePrefAdditionalSubtypes(prefs, additionalSubtypes.filterNot { it in subtypesToRemove }.joinToString(";"))
|
||||||
|
}
|
||||||
|
|
||||||
// requires loadResourceSubtypes to be called before
|
// requires loadResourceSubtypes to be called before
|
||||||
private fun loadEnabledSubtypes(context: Context) {
|
private fun loadEnabledSubtypes(context: Context) {
|
||||||
val prefs = DeviceProtectedUtils.getSharedPreferences(context)
|
val prefs = DeviceProtectedUtils.getSharedPreferences(context)
|
||||||
|
@ -231,8 +253,10 @@ private fun loadEnabledSubtypes(context: Context) {
|
||||||
val subtype = subtypesForLocale.firstOrNull { SubtypeLocaleUtils.getKeyboardLayoutSetName(it) == localeAndLayout.last() }
|
val subtype = subtypesForLocale.firstOrNull { SubtypeLocaleUtils.getKeyboardLayoutSetName(it) == localeAndLayout.last() }
|
||||||
?: additionalSubtypes.firstOrNull { it.locale() == localeAndLayout.first() && SubtypeLocaleUtils.getKeyboardLayoutSetName(it) == localeAndLayout.last() }
|
?: additionalSubtypes.firstOrNull { it.locale() == localeAndLayout.first() && SubtypeLocaleUtils.getKeyboardLayoutSetName(it) == localeAndLayout.last() }
|
||||||
if (subtype == null) {
|
if (subtype == null) {
|
||||||
|
val message = "subtype $localeAndLayout could not be loaded"
|
||||||
|
Log.w(TAG, message)
|
||||||
if (DebugFlags.DEBUG_ENABLED)
|
if (DebugFlags.DEBUG_ENABLED)
|
||||||
Toast.makeText(context, "subtype $localeAndLayout could not be loaded", Toast.LENGTH_LONG).show()
|
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
||||||
else // don't remove in debug mode
|
else // don't remove in debug mode
|
||||||
removeEnabledSubtype(prefs, localeAndLayout.joinToString(LOCALE_LAYOUT_SEPARATOR))
|
removeEnabledSubtype(prefs, localeAndLayout.joinToString(LOCALE_LAYOUT_SEPARATOR))
|
||||||
continue
|
continue
|
||||||
|
@ -268,6 +292,7 @@ private val systemSubtypes = mutableListOf<InputMethodSubtype>()
|
||||||
|
|
||||||
private const val SUBTYPE_SEPARATOR = ";"
|
private const val SUBTYPE_SEPARATOR = ";"
|
||||||
private const val LOCALE_LAYOUT_SEPARATOR = ":"
|
private const val LOCALE_LAYOUT_SEPARATOR = ":"
|
||||||
|
private const val TAG = "SubtypeSettings"
|
||||||
|
|
||||||
@Suppress("deprecation") // it's deprecated, but no replacement for API < 24
|
@Suppress("deprecation") // it's deprecated, but no replacement for API < 24
|
||||||
// todo: subtypes should now have language tags -> use them for api >= 24
|
// todo: subtypes should now have language tags -> use them for api >= 24
|
||||||
|
|
|
@ -8,7 +8,6 @@ package org.dslul.openboard.inputmethod.latin.utils;
|
||||||
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.Log;
|
|
||||||
import android.view.inputmethod.InputMethodSubtype;
|
import android.view.inputmethod.InputMethodSubtype;
|
||||||
|
|
||||||
import org.dslul.openboard.inputmethod.annotations.UsedForTesting;
|
import org.dslul.openboard.inputmethod.annotations.UsedForTesting;
|
||||||
|
@ -73,14 +72,12 @@ public final class AdditionalSubtypeUtils {
|
||||||
|
|
||||||
public static InputMethodSubtype createDummyAdditionalSubtype(
|
public static InputMethodSubtype createDummyAdditionalSubtype(
|
||||||
final String localeString, final String keyboardLayoutSetName) {
|
final String localeString, final String keyboardLayoutSetName) {
|
||||||
return createAdditionalSubtypeInternal(localeString, keyboardLayoutSetName,
|
return createAdditionalSubtypeInternal(localeString, keyboardLayoutSetName, false, false);
|
||||||
false /* isAsciiCapable */, false /* isEmojiCapable */);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InputMethodSubtype createAsciiEmojiCapableAdditionalSubtype(
|
public static InputMethodSubtype createEmojiCapableAdditionalSubtype(
|
||||||
final String localeString, final String keyboardLayoutSetName) {
|
final String localeString, final String keyboardLayoutSetName, final boolean asciiCapable) {
|
||||||
return createAdditionalSubtypeInternal(localeString, keyboardLayoutSetName,
|
return createAdditionalSubtypeInternal(localeString, keyboardLayoutSetName, asciiCapable, true);
|
||||||
true /* isAsciiCapable */, true /* isEmojiCapable */);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getPrefSubtype(final InputMethodSubtype subtype) {
|
public static String getPrefSubtype(final InputMethodSubtype subtype) {
|
||||||
|
@ -120,11 +117,10 @@ public final class AdditionalSubtypeUtils {
|
||||||
}
|
}
|
||||||
final String localeString = elems[INDEX_OF_LOCALE];
|
final String localeString = elems[INDEX_OF_LOCALE];
|
||||||
final String keyboardLayoutSetName = elems[INDEX_OF_KEYBOARD_LAYOUT];
|
final String keyboardLayoutSetName = elems[INDEX_OF_KEYBOARD_LAYOUT];
|
||||||
// Here we assume that all the additional subtypes have AsciiCapable and EmojiCapable.
|
final boolean asciiCapable = ScriptUtils.getScriptFromSpellCheckerLocale(LocaleUtils.constructLocaleFromString(localeString)) == ScriptUtils.SCRIPT_LATIN;
|
||||||
// This is actually what the setting dialog for additional subtype is doing.
|
// Here we assume that all the additional subtypes are EmojiCapable
|
||||||
final InputMethodSubtype subtype = createAsciiEmojiCapableAdditionalSubtype(
|
final InputMethodSubtype subtype = createEmojiCapableAdditionalSubtype(localeString, keyboardLayoutSetName, asciiCapable);
|
||||||
localeString, keyboardLayoutSetName);
|
if (subtype.getNameResId() == SubtypeLocaleUtils.UNKNOWN_KEYBOARD_LAYOUT && !keyboardLayoutSetName.startsWith(CustomLayoutUtilsKt.CUSTOM_LAYOUT_PREFIX)) {
|
||||||
if (subtype.getNameResId() == SubtypeLocaleUtils.UNKNOWN_KEYBOARD_LAYOUT) {
|
|
||||||
// Skip unknown keyboard layout subtype. This may happen when predefined keyboard
|
// Skip unknown keyboard layout subtype. This may happen when predefined keyboard
|
||||||
// layout has been removed.
|
// layout has been removed.
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -0,0 +1,169 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
package org.dslul.openboard.inputmethod.latin.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.OpenableColumns
|
||||||
|
import android.text.InputType
|
||||||
|
import android.widget.EditText
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.widget.doAfterTextChanged
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.Key
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.KeyboardId
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.KeyboardLayoutSet
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.KeyboardSwitcher
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.JsonKeyboardParser
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.MORE_KEYS_NORMAL
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.SimpleKeyboardParser
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.addLocaleKeyTextsToParams
|
||||||
|
import org.dslul.openboard.inputmethod.latin.R
|
||||||
|
import org.dslul.openboard.inputmethod.latin.settings.infoDialog
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.math.BigInteger
|
||||||
|
|
||||||
|
fun loadCustomLayout(uri: Uri?, localeString: String, context: Context, onAdded: (String) -> Unit) {
|
||||||
|
if (uri == null)
|
||||||
|
return infoDialog(context, context.getString(R.string.layout_error, "layout file not found"))
|
||||||
|
val layoutContent: String
|
||||||
|
try {
|
||||||
|
val i = context.contentResolver.openInputStream(uri)
|
||||||
|
layoutContent = i?.use { it.reader().readText() } ?: throw IOException()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
return infoDialog(context, context.getString(R.string.layout_error, "cannot read layout file"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = ""
|
||||||
|
context.contentResolver.query(uri, null, null, null, null).use {
|
||||||
|
if (it != null && it.moveToFirst()) {
|
||||||
|
val idx = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||||
|
if (idx >= 0)
|
||||||
|
name = it.getString(idx).substringBeforeLast(".")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadCustomLayout(layoutContent, name, localeString, context, onAdded)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadCustomLayout(layoutContent: String, layoutName: String, localeString: String, context: Context, onAdded: (String) -> Unit) {
|
||||||
|
var name = layoutName
|
||||||
|
val isJson = checkLayout(layoutContent, context)
|
||||||
|
?: return infoDialog(context, context.getString(R.string.layout_error, "invalid layout file, ${Log.getLog().lastOrNull { it.tag == TAG }?.message}"))
|
||||||
|
|
||||||
|
AlertDialog.Builder(context)
|
||||||
|
.setTitle(R.string.title_layout_name_select)
|
||||||
|
.setView(EditText(context).apply {
|
||||||
|
setText(name)
|
||||||
|
doAfterTextChanged { name = it.toString() }
|
||||||
|
setPadding(30, 10, 30, 10)
|
||||||
|
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_NORMAL
|
||||||
|
})
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
// name must be encoded to avoid issues with validity of subtype extra string or file name
|
||||||
|
name = "$CUSTOM_LAYOUT_PREFIX${localeString}.${encodeBase36(name)}.${if (isJson) "json" else "txt"}"
|
||||||
|
val file = getFile(name, context)
|
||||||
|
if (file.exists())
|
||||||
|
file.delete()
|
||||||
|
file.parentFile?.mkdir()
|
||||||
|
file.writeText(layoutContent)
|
||||||
|
onAdded(name)
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkLayout(layoutContent: String, context: Context): Boolean? {
|
||||||
|
val params = KeyboardParams()
|
||||||
|
params.mId = KeyboardLayoutSet.getFakeKeyboardId(KeyboardId.ELEMENT_ALPHABET)
|
||||||
|
params.mMoreKeyTypes.add(MORE_KEYS_LAYOUT)
|
||||||
|
addLocaleKeyTextsToParams(context, params, MORE_KEYS_NORMAL)
|
||||||
|
try {
|
||||||
|
val keys = JsonKeyboardParser(params, context).parseLayoutString(layoutContent)
|
||||||
|
if (!checkKeys(keys))
|
||||||
|
return null
|
||||||
|
return true
|
||||||
|
} catch (e: Exception) { Log.w(TAG, "error parsing custom json layout", e) }
|
||||||
|
try {
|
||||||
|
val keys = SimpleKeyboardParser(params, context).parseLayoutString(layoutContent)
|
||||||
|
if (!checkKeys(keys))
|
||||||
|
return null
|
||||||
|
return false
|
||||||
|
} catch (e: Exception) { Log.w(TAG, "error parsing custom simple layout", e) }
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkKeys(keys: List<List<Key.KeyParams>>): Boolean {
|
||||||
|
if (keys.isEmpty() || keys.any { it.isEmpty() }) {
|
||||||
|
Log.w(TAG, "empty rows")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (keys.size > 8) {
|
||||||
|
Log.w(TAG, "too many rows")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (keys.any { it.size > 20 }) {
|
||||||
|
Log.w(TAG, "too many keys in one row")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (keys.any { it.any { ((it.mLabel?.length ?: 0) > 6) } }) {
|
||||||
|
Log.w(TAG, "too long text on key")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (keys.any { it.any { (it.mMoreKeys?.size ?: 0) > 20 } }) {
|
||||||
|
Log.w(TAG, "too many popup keys on a key")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (keys.any { it.any { it.mMoreKeys?.any { (it.mLabel?.length ?: 0) > 10 } == true } }) {
|
||||||
|
Log.w(TAG, "too long text on popup key")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFile(layoutName: String, context: Context) =
|
||||||
|
File(context.filesDir, "layouts${File.separator}$layoutName")
|
||||||
|
|
||||||
|
// undo the name changes in loadCustomLayout when clicking ok
|
||||||
|
fun getLayoutDisplayName(layoutName: String) =
|
||||||
|
try {
|
||||||
|
decodeBase36(layoutName.substringAfter(CUSTOM_LAYOUT_PREFIX).substringAfter(".").substringBeforeLast("."))
|
||||||
|
} catch (_: NumberFormatException) {
|
||||||
|
layoutName
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeCustomLayoutFile(layoutName: String, context: Context) {
|
||||||
|
getFile(layoutName, context).delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun editCustomLayout(layoutName: String, context: Context, startContent: String? = null) {
|
||||||
|
val file = getFile(layoutName, context)
|
||||||
|
val editText = EditText(context).apply {
|
||||||
|
setText(startContent ?: file.readText())
|
||||||
|
}
|
||||||
|
AlertDialog.Builder(context)
|
||||||
|
.setTitle(getLayoutDisplayName(layoutName))
|
||||||
|
.setView(editText)
|
||||||
|
.setPositiveButton(R.string.save) { _, _ ->
|
||||||
|
val content = editText.text.toString()
|
||||||
|
val isJson = checkLayout(content, context)
|
||||||
|
if (isJson == null) {
|
||||||
|
editCustomLayout(layoutName, context, content)
|
||||||
|
infoDialog(context, context.getString(R.string.layout_error, Log.getLog().lastOrNull { it.tag == TAG }?.message))
|
||||||
|
} else {
|
||||||
|
val wasJson = file.name.substringAfterLast(".") == "json"
|
||||||
|
file.writeText(content)
|
||||||
|
if (isJson != wasJson) // unlikely to be needed, but better be safe
|
||||||
|
file.renameTo(File(file.absolutePath.substringBeforeLast(".") + if (isJson) "json" else "txt"))
|
||||||
|
KeyboardSwitcher.getInstance().forceUpdateKeyboardTheme(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun encodeBase36(string: String): String = BigInteger(string.toByteArray()).toString(36)
|
||||||
|
|
||||||
|
private fun decodeBase36(string: String) = BigInteger(string, 36).toByteArray().decodeToString()
|
||||||
|
|
||||||
|
// this goes into prefs and file names, so do not change!
|
||||||
|
const val CUSTOM_LAYOUT_PREFIX = "custom."
|
||||||
|
private const val TAG = "CustomLayoutUtils"
|
|
@ -73,7 +73,6 @@ public class ScriptUtils {
|
||||||
mLanguageCodeToScriptCode.put("ko", SCRIPT_HANGUL);
|
mLanguageCodeToScriptCode.put("ko", SCRIPT_HANGUL);
|
||||||
mLanguageCodeToScriptCode.put("hi", SCRIPT_DEVANAGARI);
|
mLanguageCodeToScriptCode.put("hi", SCRIPT_DEVANAGARI);
|
||||||
mLanguageCodeToScriptCode.put("kn", SCRIPT_KANNADA);
|
mLanguageCodeToScriptCode.put("kn", SCRIPT_KANNADA);
|
||||||
mLanguageCodeToScriptCode.put("kh", SCRIPT_KHMER);
|
|
||||||
mLanguageCodeToScriptCode.put("mr", SCRIPT_DEVANAGARI);
|
mLanguageCodeToScriptCode.put("mr", SCRIPT_DEVANAGARI);
|
||||||
mLanguageCodeToScriptCode.put("mn", SCRIPT_CYRILLIC);
|
mLanguageCodeToScriptCode.put("mn", SCRIPT_CYRILLIC);
|
||||||
mLanguageCodeToScriptCode.put("be", SCRIPT_CYRILLIC);
|
mLanguageCodeToScriptCode.put("be", SCRIPT_CYRILLIC);
|
||||||
|
|
|
@ -283,6 +283,8 @@ public final class SubtypeLocaleUtils {
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public static String getKeyboardLayoutSetDisplayName(@NonNull final String layoutName) {
|
public static String getKeyboardLayoutSetDisplayName(@NonNull final String layoutName) {
|
||||||
|
if (layoutName.startsWith(CustomLayoutUtilsKt.CUSTOM_LAYOUT_PREFIX))
|
||||||
|
return CustomLayoutUtilsKt.getLayoutDisplayName(layoutName);
|
||||||
return sKeyboardLayoutToDisplayNameMap.get(layoutName);
|
return sKeyboardLayoutToDisplayNameMap.get(layoutName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -421,8 +421,22 @@ disposition rather than other common dispositions for Latin languages. [CHAR LIM
|
||||||
<string name="save">Save</string>
|
<string name="save">Save</string>
|
||||||
<!-- Title of the spinner for choosing a language of custom style in the settings dialog [CHAR LIMIT=15] -->
|
<!-- Title of the spinner for choosing a language of custom style in the settings dialog [CHAR LIMIT=15] -->
|
||||||
<string name="subtype_locale">Language</string>
|
<string name="subtype_locale">Language</string>
|
||||||
<!-- Title of the spinner for choosing a keyboard layout of custom style in the settings dialog [CHAR LIMIT=15] -->
|
<!-- Title of the section for choosing a keyboard layout language settings dialog -->
|
||||||
<string name="keyboard_layout_set">Layout</string>
|
<string name="keyboard_layout_set">Layout</string>
|
||||||
|
<!-- Button text and title for adding a custom layout -->
|
||||||
|
<string name="button_title_add_custom_layout">Add custom layout</string>
|
||||||
|
<!-- Message text for adding a custom layout, interpreted as HTML -->
|
||||||
|
<string name="message_add_custom_layout">Select a file in a compatible format. Information about the formats is available %s.</string>
|
||||||
|
<!-- Button text for loading a custom layout file -->
|
||||||
|
<string name="button_load_custom_layout">Load file</string>
|
||||||
|
<!-- Button text for copying an existing layout -->
|
||||||
|
<string name="button_copy_existing_layout">Copy existing layout</string>
|
||||||
|
<!-- Title text for choosing a layout name -->
|
||||||
|
<string name="title_layout_name_select">Set layout name</string>
|
||||||
|
<!-- Message on layout error -->
|
||||||
|
<string name="layout_error">Layout error: %s</string>
|
||||||
|
<!-- Text hint for editing layout -->
|
||||||
|
<string name="edit_layout">Tap to edit raw layout</string>
|
||||||
<!-- Title of the settings for keypress vibration duration [CHAR LIMIT=35] -->
|
<!-- Title of the settings for keypress vibration duration [CHAR LIMIT=35] -->
|
||||||
<string name="prefs_keypress_vibration_duration_settings">Keypress vibration duration</string>
|
<string name="prefs_keypress_vibration_duration_settings">Keypress vibration duration</string>
|
||||||
<!-- Title of the settings for keypress sound volume [CHAR LIMIT=35] -->
|
<!-- Title of the settings for keypress sound volume [CHAR LIMIT=35] -->
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
package org.dslul.openboard.inputmethod.latin
|
||||||
|
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.KeyboardId
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.KeyboardLayoutSet
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.MORE_KEYS_NORMAL
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.SimpleKeyboardParser
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.internal.keyboard_parser.addLocaleKeyTextsToParams
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.Robolectric
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
import org.robolectric.shadows.ShadowLog
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@Config(shadows = [
|
||||||
|
ShadowInputMethodManager2::class,
|
||||||
|
])
|
||||||
|
class ParserTest {
|
||||||
|
private lateinit var latinIME: LatinIME
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
latinIME = Robolectric.setupService(LatinIME::class.java)
|
||||||
|
ShadowLog.setupLogging()
|
||||||
|
ShadowLog.stream = System.out
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun simpleParser() {
|
||||||
|
val params = KeyboardParams()
|
||||||
|
params.mId = KeyboardLayoutSet.getFakeKeyboardId(KeyboardId.ELEMENT_ALPHABET)
|
||||||
|
addLocaleKeyTextsToParams(latinIME, params, MORE_KEYS_NORMAL)
|
||||||
|
val layoutStrings = listOf(
|
||||||
|
"""
|
||||||
|
a
|
||||||
|
b
|
||||||
|
c
|
||||||
|
|
||||||
|
d
|
||||||
|
e
|
||||||
|
f
|
||||||
|
""", // normal
|
||||||
|
"""
|
||||||
|
a
|
||||||
|
b
|
||||||
|
c
|
||||||
|
|
||||||
|
d
|
||||||
|
e
|
||||||
|
f
|
||||||
|
""", // spaces in the empty line
|
||||||
|
"""
|
||||||
|
a
|
||||||
|
b
|
||||||
|
c
|
||||||
|
|
||||||
|
d
|
||||||
|
e
|
||||||
|
f
|
||||||
|
""".replace("\n", "\r\n"), // windows file endings
|
||||||
|
"""
|
||||||
|
a
|
||||||
|
b
|
||||||
|
c
|
||||||
|
|
||||||
|
|
||||||
|
d
|
||||||
|
e
|
||||||
|
f
|
||||||
|
|
||||||
|
""", // too many newlines
|
||||||
|
"""
|
||||||
|
a
|
||||||
|
b x
|
||||||
|
c v
|
||||||
|
|
||||||
|
d
|
||||||
|
e
|
||||||
|
f
|
||||||
|
""", // spaces in the end
|
||||||
|
"""
|
||||||
|
a
|
||||||
|
b
|
||||||
|
c
|
||||||
|
|
||||||
|
d
|
||||||
|
e
|
||||||
|
f""", // no newline at the end
|
||||||
|
)
|
||||||
|
val wantedKeyLabels = listOf(listOf("a", "b", "c"), listOf("d", "e", "f"))
|
||||||
|
layoutStrings.forEachIndexed { i, layout ->
|
||||||
|
println(i)
|
||||||
|
val keyLabels = SimpleKeyboardParser(params, latinIME).parseCoreLayout(layout).map { it.map { it.label } }
|
||||||
|
assertEquals(wantedKeyLabels, keyLabels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
layouts.md
32
layouts.md
|
@ -1,12 +1,26 @@
|
||||||
# Layouts
|
A compilation of information about the layout formats usable in this app.
|
||||||
|
|
||||||
(WIP) information about the layout format
|
There are two distinct formats:
|
||||||
|
* the _simple_ format is a text file with one key per line, and two consecutive line breaks indicating a switch to the next row, [example](app/src/main/assets/layouts/qwerty.txt)
|
||||||
|
* the _json_ format taken from [FlorisBoard](https://github.com/florisboard/florisboard/blob/master/CONTRIBUTING.md#adding-the-layout), but only "normal" keys are supported (i.e. no action keys and similar), [example](app/src/main/assets/layouts/azerty.json)
|
||||||
|
|
||||||
## simple
|
## General notes
|
||||||
One key per line, two consecutive newlines mark a row end.
|
Adding too many keys or too long texts will make the keyboard look awkward or broken, and even crash the app under some specific conditions.
|
||||||
Key format: [label] [moreKeys], all separated by space, e.g. `a 0 + *` will create a key with text a, and the keys `0`, `+`, and `*` on long press. Some characters currently require escape using `\` (todo: add the list, or better add them in code instead of requiring it in the layouts).
|
There are some sanity checks when adding a layout to avoid such issues, but they do not cover all possible cases.
|
||||||
Special symbols: `%` (only for language-dependent moreKeys, not user defined, also better use sth like `%%%`) acts as placeholder for normal moreKeys. `$$$` will be replaced by currency (or default to `$`).
|
Further there is no check whether the layout actually contains characters of the selected language.
|
||||||
Language-dependent moreKeys should never contain "special" moreKeys, i.e. those starting with `!` (exception for `punctuation`)
|
|
||||||
|
|
||||||
## json
|
If you use an external glide typing library, you likely will have issues if your layout contains duplicate keys, or keys with text longer than a single letter.
|
||||||
Character layouts from FlorisBoard, but missing code or label will be determined automatically. And not everything supported...
|
|
||||||
|
There are special key labels that are intended for internal use only, but can (currently) be set on custom layouts too. An example is `!icon/previous_key|!code/key_action_previous`, so it's unlikely you will stumble upon issues here when not intentionally provoking it.
|
||||||
|
One special label that might be wanted though is `$$$`, which will be replaced by the local currency.
|
||||||
|
|
||||||
|
## Simple format
|
||||||
|
* One key per line
|
||||||
|
* Key format: [label] [popup keys], all separated by space, e.g. `a 0 + *` will create a key with text `a`, and the keys `0`, `+`, and `*` on long press
|
||||||
|
* Two consecutive newlines mark beginning of a new row
|
||||||
|
|
||||||
|
## Json format
|
||||||
|
* You can use character layouts from FlorisBoard
|
||||||
|
* There is no need for specifying a code, it will be determined from the label automatically
|
||||||
|
* Specify it if you want key label and code to be different
|
||||||
|
* You can add a _labelFlag_ to a key for some specific effects, see [here](app/src/main/res/values/attrs.xml) in the section _keyLabelFlags_ for names and numeric values
|
||||||
|
|
Loading…
Add table
Reference in a new issue