Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-21 15:13:05 +01:00
parent f2d952b743
commit 3ecd57d1b2
475 changed files with 37130 additions and 2 deletions

10
srcs/compose/README.md Normal file
View file

@ -0,0 +1,10 @@
# Compose sequences
The `compose.py` program parses the compose sequences found in this directory
and generates `srcs/juloo.keyboard2/ComposeKeyData.java`.
## `compose/en_US_UTF_8_Compose.pre`
This file is copied from the `xorg` project. Copyright applies.
## `compose/extra.json`

View file

@ -0,0 +1,60 @@
{
// latin
"a": "á",
"c": "ć",
"e": "é",
"g": "ǵ",
"i": "í",
"k": "ḱ",
"l": "ĺ",
"m": "ḿ",
"n": "ń",
"o": "ó",
"p": "ṕ",
"r": "ŕ",
"s": "ś",
"u": "ú",
"w": "ẃ",
"y": "ý",
"z": "ź",
// extended latin (multiple diacritics)
"â": "ấ",
"ă": "ắ",
"å": "ǻ",
"æ": "ǽ",
"ç": "ḉ",
"ê": "ế",
"ē": "ḗ",
"ï": "ḯ",
"ô": "ố",
"ơ": "ớ",
"õ": "ṍ",
"ō": "ṓ",
"ø": "ǿ",
"ṡ": "ṥ",
"ü": "ǘ",
"ư": "ứ",
"ũ": "ṹ",
// greek
"α": "ά",
"ε": "έ",
"η": "ή",
"ι": "ί",
"ο": "ό",
"υ": "ύ",
// cyrillic
"к": "ќ",
"г": "ѓ",
// combining character
"ą": "ą\u0301",
"j": "j\u0301",
"у": "у\u0301",
"е": "е\u0301",
"а": "а\u0301",
"о": "о\u0301",
"и": "и\u0301",
"ы": "ы\u0301",
"э": "э\u0301",
"ю": "ю\u0301",
"я": "я\u0301"
}

View file

@ -0,0 +1,13 @@
{
"0": "↔",
"1": "↙",
"2": "↓",
"3": "↘",
"4": "←",
"5": "↕",
"6": "→",
"7": "↖",
"8": "↑",
"9": "↗",
".": "↵"
}

View file

@ -0,0 +1,30 @@
{
// latin
"2": "ƻ",
"b": "ƀ",
"c": "ꞓ",
"d": "đ",
"f": "",
"g": "ǥ",
"h": "ħ",
"i": "ɨ",
"j": "ɉ",
"k": "ꝁ",
"l": "ƚ",
"o": "ɵ",
"p": "ᵽ",
"q": "ꝗ",
"r": "ɍ",
"t": "ŧ",
"u": "ʉ",
"y": "ɏ",
"z": "ƶ",
// extended latin
"ȷ": "ɟ",
// cyrillic
"о": "ө",
"ӧ": "ӫ",
"ү": "ұ",
"ь": "ҍ",
"х": "ӿ"
}

View file

@ -0,0 +1,13 @@
{
"1": "└",
"2": "┴",
"3": "┘",
"4": "├",
"5": "┼",
"6": "┤",
"7": "┌",
"8": "┬",
"9": "┐",
"0": "─",
".": "│"
}

View file

@ -0,0 +1,33 @@
{
// latin
"a": "ǎ",
"c": "č",
"d": "ď",
"e": "ě",
"g": "ǧ",
"h": "ȟ",
"i": "ǐ",
"j": "ǰ", // no uppercase
"k": "ǩ",
"l": "ľ",
"n": "ň",
"o": "ǒ",
"r": "ř",
"s": "š",
"t": "ť",
"u": "ǔ",
"z": "ž",
// extended latin
"ṡ": "ṧ",
"ü": "ǚ",
"ʒ": "ǯ",
// combining character
"в": "в\u030C",
"г": "г\u030C",
"ғ": "ғ\u030C",
"д": "д\u030C",
"з": "з\u030C",
"р": "р\u030C",
"т": "т\u030C",
"х": "х\u030C"
}

View file

@ -0,0 +1,17 @@
{
// latin
"c": "ç",
"d": "ḑ",
"e": "ȩ",
"g": "ģ",
"h": "ḩ",
"k": "ķ",
"l": "ļ",
"n": "ņ",
"r": "ŗ",
"s": "ş",
"t": "ţ",
// extended latin
"ć": "ḉ",
"ĕ": "ḝ"
}

View file

@ -0,0 +1,41 @@
{
"+": "⨣",
"≈": "⩯",
// latin
"a": "â",
"c": "ĉ",
"e": "ê",
"g": "ĝ",
"h": "ĥ",
"i": "î",
"j": "ĵ",
"o": "ô",
"ŝ": "ŝ",
"u": "û",
"w": "ŵ",
"x": "x̂",
"y": "ŷ",
"z": "ẑ",
// extended latin
"á": "ấ",
"à": "ầ",
"ã": "ẫ",
"ạ": "ậ",
"ả": "ẩ",
"é": "ế",
"è": "ề",
"ẽ": "ễ",
"ẹ": "ệ",
"ẻ": "ể",
"ó": "ố",
"ò": "ồ",
"ơ": "ổ",
"õ": "ỗ",
"ọ": "ộ",
// combining characters
"а": "а\u0302",
"е": "е\u0302",
"и": "и\u0302",
"о": "о\u0302",
"у": "у\u0302"
}

View file

@ -0,0 +1,56 @@
{
"a": "ȧ",
"b": "ḃ",
"c": "ċ",
"d": "ḋ",
"e": "ė",
"f": "ḟ",
"g": "ġ",
"h": "ḣ",
"m": "ṁ",
"n": "ṅ",
"o": "ȯ",
"p": "ṗ",
"r": "ṙ",
"s": "ṡ",
"t": "ṫ",
"w": "ẇ",
"x": "ẋ",
"y": "ẏ",
"z": "ż",
// remove dot since i and j already have one
"i": "ı",
"j": "ȷ",
// extended latin
"ā": "ǡ",
"ō": "ȱ",
"ś": "ṥ",
"ṣ": "ṩ",
"š": "ṧ",
"ſ": "ẛ",
// combining character
"k": "k\u0307",
"l": "l\u0307",
"q": "q\u0307",
"u": "u\u0307",
"v": "v\u0307",
"0": "0\u0307",
"1": "1\u0307",
"2": "2\u0307",
"3": "3\u0307",
"4": "4\u0307",
"5": "5\u0307",
"6": "6\u0307",
"7": "7\u0307",
"8": "8\u0307",
"9": "9\u0307",
// math
"∈": "⋵",
"": "⨰",
"∧": "⩑",
"": "⩒",
"≡": "⩧",
"~": "⩪",
"⊆": "⫃",
"⊇": "⫄"
}

View file

@ -0,0 +1,34 @@
{
// latin
"a": "ạ",
"b": "ḅ",
"d": "ḍ",
"e": "ẹ",
"h": "ḥ",
"i": "ị",
"k": "ḳ",
"l": "ḷ",
"m": "ṃ",
"n": "ṇ",
"o": "ọ",
"r": "ṛ",
"s": "ṣ",
"t": "ṭ",
"u": "ụ",
"v": "ṿ",
"w": "ẉ",
"y": "ỵ",
"z": "ẓ",
// extended latin
"ă": "ặ",
"â": "ậ",
"ê": "ệ",
"ô": "ộ",
"ơ": "ợ",
"ṡ": "ṩ",
"ư": "ự",
// math
"-": "⨪",
"+": "⨥",
"=": "⩦"
}

View file

@ -0,0 +1,14 @@
{
" ": "˝",
// latin
"o": "ő",
"u": "ű",
// cyrillic
"у": "ӳ",
// combining character
"a": "a\u030b",
"e": "e\u030b",
"i": "i\u030b",
"m": "m\u030b",
"y": "y\u030b"
}

View file

@ -0,0 +1,17 @@
{
// latin
"a": "ȁ",
"e": "ȅ",
"i": "ȉ",
"o": "ȍ",
"r": "ȑ",
"u": "ȕ",
//cyrillic
"ѵ": "ѷ",
"а": "а\u030f",
"е": "е\u030f",
"и": "и\u030f",
"о": "о\u030f",
"р": "р\u030f",
"у": "у\u030f"
}

View file

@ -0,0 +1,38 @@
{
// latin
"a": "à",
"e": "è",
"i": "ì",
"n": "ǹ",
"o": "ò",
"u": "ù",
"w": "ẁ",
"y": "ỳ",
// extended latin
"â": "ầ",
"ă": "ằ",
"ê": "ề",
"ē": "ḕ",
"ơ": "ờ",
"ô": "ồ",
"ō": "ṑ",
"ü": "ǜ",
"ư": "ừ",
// greek (technically not a grave, but a varia)
"α": "ὰ",
"ε": "ὲ",
"η": "ὴ",
"ι": "ὶ",
"ο": "ὸ",
"υ": "ὺ",
"ω": "ὼ",
// there is more like , , etc
// cyrillic
"е": "ѐ",
"и": "ѝ",
// combining character
"ɔ": "ɔ\u0300",
"s": "s\u0300",
"ʌ": "ʌ\u0300",
"z": "z\u0300"
}

View file

@ -0,0 +1,14 @@
{
"a": "ả",
"ă": "ẳ",
"â": "ẩ",
"e": "ẻ",
"ê": "ể",
"i": "ỉ",
"o": "ỏ",
"ô": "ổ",
"ơ": "ở",
"u": "ủ",
"ư": "ử",
"y": "ỷ"
}

View file

@ -0,0 +1,14 @@
{
"o": "ơ",
"ó": "ớ",
"ò": "ờ",
"ỏ": "ở",
"õ": "ỡ",
"ọ": "ợ",
"u": "ư",
"ú": "ứ",
"ù": "ừ",
"ủ": "ử",
"ũ": "ữ",
"ụ": "ự"
}

View file

@ -0,0 +1,35 @@
{
// latin
"a": "ā",
"e": "ē",
"g": "ḡ",
"i": "ī",
"o": "ō",
"u": "ū",
"y": "ȳ",
// extended latin
"æ": "ǣ",
"ä": "ǟ",
"ȧ": "ǡ",
"è": "ḕ",
"é": "ḗ",
"ḷ": "ḹ",
"ṛ": "ṝ",
"ö": "ȫ",
"ȯ": "ȱ",
"ǫ": "ǭ",
"õ": "ȭ",
"ò": "ṑ",
"ó": "ṓ",
"ü": "ǖ", // there is also
// cyrillic
"и": "ӣ",
"у": "ӯ",
// greek
"α": "ᾱ",
"ι": "ῑ",
"υ": "ῡ",
// combining characters
"l": "l\u0304",
"r": "r\u0304"
}

View file

@ -0,0 +1,10 @@
{
// latin
"a": "ą",
"e": "ę",
"i": "į",
"o": "ǫ",
"u": "ų",
// extended latin
"ō": "ǭ"
}

View file

@ -0,0 +1,14 @@
{
"a": "ª",
"o": "º",
"1": "ª",
"2": "º",
"3": "ⁿ",
"4": "ᵈ",
"5": "ᵉ",
"6": "ʳ",
"7": "ˢ",
"8": "ᵗ",
"9": "ʰ",
"*": "°"
}

View file

@ -0,0 +1,11 @@
{
// latin
"a": "å",
"u": "ů",
"w": "ẘ", // no uppercase
"y": "ẙ", // no uppercase
// extended latin
"á": "ǻ",
// extra
"~": "⸛"
}

View file

@ -0,0 +1,18 @@
{
"a": "ⱥ",
"b": "␢",
"c": "ȼ",
"e": "ɇ",
"g": "ꞡ",
"k": "ꝃ",
"l": "ł",
"n": "ꞥ",
"o": "ø",
"ó": "ǿ",
"ɔ": "ꬿ",
"r": "ꞧ",
"s": "ꞩ",
"t": "ⱦ",
"u": "ꞹ",
"v": "ꝟ"
}

View file

@ -0,0 +1,45 @@
{
// arabic numbers
"0": "₀",
"1": "₁",
"2": "₂",
"3": "₃",
"4": "₄",
"5": "₅",
"6": "₆",
"7": "₇",
"8": "₈",
"9": "₉",
// math operators
"+": "₊",
"-": "₋",
"=": "₌",
"(": "₍",
")": "₎",
// latin
"a": "ₐ",
"e": "ₑ",
"h": "ₕ",
"i": "ᵢ",
"j": "ⱼ",
"k": "ₖ",
"l": "ₗ",
"m": "ₘ",
"n": "ₙ",
"o": "ₒ",
"p": "ₚ",
"r": "ᵣ",
"s": "ₛ",
"t": "ₜ",
"u": "ᵤ",
"v": "ᵥ",
"x": "ₓ",
// extended latin
"ə": "ₔ",
// greek
"β": "ᵦ",
"γ": "ᵧ",
"ρ": "ᵨ",
"φ": "ᵩ",
"χ": "ᵪ"
}

View file

@ -0,0 +1,93 @@
{
// numbers
"0": "⁰",
"1": "¹",
"2": "²",
"3": "³",
"4": "⁴",
"5": "⁵",
"6": "⁶",
"7": "⁷",
"8": "⁸",
"9": "⁹",
// math operators
"+": "⁺",
"-": "⁻",
"=": "⁼",
"(": "⁽",
")": "⁾",
// latin
"n": "ⁿ",
// since there are no more "superscript" characters,
// we substitute with "modifier letter small"s which looks the same
// latin
"a": "ᵃ",
"b": "ᵇ",
"c": "ᶜ",
"d": "ᵈ",
"e": "ᵉ",
"f": "ᶠ",
"g": "ᵍ",
"h": "ʰ",
"i": "ⁱ",
"j": "ʲ",
"k": "ᵏ",
"l": "ˡ",
// see above for n
"m": "ᵐ",
"o": "ᵒ",
"p": "ᵖ",
"q": "ꟴ", // there is no proper lowercase superscript q
"r": "ʳ",
"s": "ˢ",
"t": "ᵗ",
"u": "ᵘ",
"v": "ᵛ",
"w": "ʷ",
"x": "ˣ",
"y": "ʸ",
"z": "ᶻ",
// extended latin
"ɐ": "ᵄ",
"ᴂ": "ᵆ",
"ɕ": "ᶝ",
"ə": "ᵊ",
"ɛ": "ᵋ",
"ɜ": "ᶟ", // turned open e, not the same
"ᴈ": "ᵌ", // reversed open e
"ɥ": "ᶣ",
"ɦ": "ʱ",
"ᴉ": "ᵎ",
"ɨ": "ᶤ",
"ɟ": "ᶡ",
"ɱ": "ᶬ",
"ɯ": "ᵚ",
"ɰ": "ᶭ",
"ŋ": "ᵑ",
"ᴝ": "ᵙ",
"ɵ": "ᶱ",
"œ": "ꟹ",
"ɔ": "ᵓ",
"ɹ": "ʴ",
"ɻ": "ʵ",
"ʁ": "ʶ",
"ʂ": "ᶳ",
"ʉ": "ᶶ",
"ʃ": "ᶴ",
"ʒ": "ᶾ",
"ʍ": "ꭩ",
// greek
"ɒ": "ᶛ",
"β": "ᵝ",
"ɣ": "ˠ",
"δ": "ᵟ",
"φ": "ᵠ",
"χ": "ᵡ",
"ι": "ᶥ",
"ʊ": "ᶷ",
"ʌ": "ᶺ",
"θ": "ᶿ",
// cyrillic
"ө": "ᶱ"
}

View file

@ -0,0 +1,21 @@
{
// latin
"a": "ã",
"e": "ẽ",
"i": "ĩ",
"n": "ñ",
"o": "õ",
"u": "ũ",
"v": "ṽ",
"y": "ỹ",
// extended latin
"ă": "ẵ",
"â": "ẫ",
"ê": "ễ",
"ơ": "ỡ",
"ō": "ȭ",
"ó": "ṍ",
"ö": "ṏ",
"ư": "ữ",
"ú": "ṹ"
}

View file

@ -0,0 +1,54 @@
{
// fun
"~": "⍨",
"*": "⍣",
"∇": "⍢",
"°": "⍤",
// latin
"a": "ä",
"e": "ë",
"h": "ḧ",
"i": "ï",
"o": "ö",
"t": "ẗ",
"u": "ü",
"w": "ẅ",
"x": "ẍ",
"y": "ÿ",
// extended latin
"ā": "ǟ",
"ō": "ȫ",
"õ": "ṏ",
"í": "ḯ",
"ū": "ǖ", // there is also
"ú": "ǘ",
"ù": "ǜ",
"ǔ": "ǚ",
// greek
"ι": "ϊ",
"υ": "ϋ",
"ὺ": "ῢ",
"ύ": "ΰ",
"ῦ": "ῧ",
"ϒ": "ϔ",
// cyrillic
"а": "ӓ",
"ә": "ӛ",
"ж": "ӝ",
"з": "ӟ",
"и": "ӥ",
"о": "ӧ",
"ө": "ӫ",
"э": "ӭ",
"у": "ӱ",
"ч": "ӵ",
"ы": "ӹ",
// combining character
"c": "c\u0308",
"j": "j\u0308",
"k": "k\u0308",
"l": "l\u0308",
"m": "m\u0308",
"n": "n\u0308",
"s": "s\u0308"
}

339
srcs/compose/compile.py Normal file
View file

@ -0,0 +1,339 @@
import textwrap, sys, re, string, json, os, string
from array import array
# Compile compose sequences from Xorg's format or from JSON files into an
# efficient state machine.
# See [ComposeKey.java] for the interpreter.
#
# Takes input files as arguments and generate a Java file.
# The initial state for each input is generated as a constant named after the
# input file.
# Parse symbol names from keysymdef.h. Many compose sequences in
# en_US_UTF_8_Compose.pre reference theses. For example, all the sequences on
# the Greek, Cyrillic and Hebrew scripts need these symbols.
def parse_keysymdef_h(fname):
with open(fname, "r") as inp:
keysym_re = re.compile(r'^#define XK_(\S+)\s+\S+\s*/\*.U\+([0-9a-fA-F]+)\s')
for line in inp:
m = re.match(keysym_re, line)
if m != None:
yield (m.group(1), chr(int(m.group(2), 16)))
dropped_sequences = 0
warning_count = 0
# [s] is a list of strings
def seq_to_str(s, result=None):
msg = "+".join(s)
return msg if result is None else msg + " = " + result
# Print a warning. If [seq] is passed, it is prepended to the message.
def warn(msg, seq=None, result=None):
global warning_count
if seq is not None:
msg = f"Sequence {seq_to_str(seq, result=result)} {msg}"
print(f"Warning: {msg}", file=sys.stderr)
warning_count += 1
# Parse XKB's Compose.pre files
def parse_sequences_file_xkb(fname, xkb_char_extra_names):
# Parse a line of the form:
# <Multi_key> <minus> <space> : "~" asciitilde # TILDE
# Sequences not starting with <Multi_key> are ignored.
line_re = re.compile(r'^((?:\s*<[^>]+>)+)\s*:\s*"((?:[^"\\]+|\\.)+)"\s*(\S+)?\s*(?:#.+)?$')
char_re = re.compile(r'\s*<(?:U([a-fA-F0-9]{4,6})|([^>]+))>')
def parse_seq_line(line):
global dropped_sequences
prefix = "<Multi_key>"
if not line.startswith(prefix):
return None
m = re.match(line_re, line[len(prefix):])
if m == None:
return None
def_ = m.group(1)
try:
def_ = parse_seq_chars(def_)
result = parse_seq_result(m.group(2))
except Exception as e:
# print(str(e) + ". Sequence dropped: " + line.strip(), file=sys.stderr)
dropped_sequences += 1
return None
return def_, result
char_names = { **xkb_char_extra_names }
# Interpret character names of the form "U0000" or using [char_names].
def parse_seq_char(sc):
uchar, named_char = sc
if uchar != "":
c = chr(int(uchar, 16))
elif len(named_char) == 1:
c = named_char
else:
if not named_char in char_names:
raise Exception("Unknown char: " + named_char)
c = char_names[named_char]
# The state machine can't represent sequence characters that do not fit
# in a 16-bit char.
if len(c) > 1 or ord(c[0]) > 65535:
raise Exception("Char out of range: " + r)
return c
# Interpret the left hand side of a sequence.
def parse_seq_chars(def_):
return list(map(parse_seq_char, re.findall(char_re, def_)))
# Interpret the result of a sequence, as outputed by [line_re].
def parse_seq_result(r):
if len(r) == 2 and r[0] == '\\':
return r[1]
return r
# Populate [char_names] with the information present in the file.
with open(fname, "r") as inp:
for line in inp:
m = re.match(line_re, line)
if m == None or m.group(3) == None:
continue
try:
char_names[m.group(3)] = parse_seq_result(m.group(2))
except Exception:
pass
# Parse the sequences
with open(fname, "r") as inp:
seqs = []
for line in inp:
s = parse_seq_line(line)
if s != None:
seqs.append(s)
return seqs
# Basic support for comments in json files. Reads a file
def strip_cstyle_comments(inp):
def strip_line(line):
i = line.find("//")
return line[:i] + "\n" if i >= 0 else line
return "".join(map(strip_line, inp))
# Parse from a json file containing a dictionary sequence → result string.
def parse_sequences_file_json(fname):
def tree_to_seqs(tree, prefix):
for c, r in tree.items():
if isinstance(r, str):
yield prefix + [c], r
else:
yield from tree_to_seqs(r, prefix + [c])
try:
with open(fname, "r") as inp:
tree = json.loads(strip_cstyle_comments(inp))
return list(tree_to_seqs(tree, []))
except Exception as e:
warn("Failed parsing '%s': %s" % (fname, str(e)))
# Format of the sequences file is determined by its extension
def parse_sequences_file(fname, xkb_char_extra_names={}):
if fname.endswith(".pre"):
return parse_sequences_file_xkb(fname, xkb_char_extra_names)
if fname.endswith(".json"):
return parse_sequences_file_json(fname)
raise Exception(fname + ": Unsupported format")
# A sequence directory can contain several sequence files as well as
# 'keysymdef.h'.
def parse_sequences_dir(dname):
compose_files = []
xkb_char_extra_names = {}
# Parse keysymdef.h first if present
for fbasename in os.listdir(dname):
fname = os.path.join(dname, fbasename)
if fbasename == "keysymdef.h":
xkb_char_extra_names = dict(parse_keysymdef_h(fname))
else:
compose_files.append(fname)
sequences = []
for fname in compose_files:
sequences.extend(parse_sequences_file(fname, xkb_char_extra_names))
return sequences
# Turn a list of sequences into a trie.
def add_sequences_to_trie(seqs, trie):
global dropped_sequences
def add_seq_to_trie(seq, result):
t_ = trie
for c in seq[:-1]:
t_ = t_.setdefault(c, {})
if isinstance(t_, str):
return False
c = seq[-1]
if c in t_:
return False
t_[c] = result
return True
def existing_sequence_to_str(seq): # Used in error message
i = 0
t_ = trie
while i < len(seq):
if seq[i] not in t_: break # No collision ?
t_ = t_[seq[i]]
i += 1
if isinstance(t_, str): break
return "".join(seq[:i]) + " = " + str(t_)
for seq, result in seqs:
if not add_seq_to_trie(seq, result):
dropped_sequences += 1
warn("Sequence collide: '%s' and '%s = %s'" % (
existing_sequence_to_str(seq),
"".join(seq), result))
# Compile the trie into a state machine.
def make_automata(tries):
previous_leafs = {} # Deduplicate leafs
states = []
def add_tree(t):
this_node_index = len(states)
# Index and size of the new node
i = len(states)
s = len(t.keys())
# Add node header
states.append(("\0", s + 1))
i += 1
# Reserve space for the current node in both arrays
for c in range(s):
states.append((None, None))
# Add nested nodes and fill the current node
for c in sorted(t.keys()):
states[i] = (c, add_node(t[c]))
i += 1
return this_node_index
def add_leaf(c):
if c in previous_leafs:
return previous_leafs[c]
this_node_index = len(states)
previous_leafs[c] = this_node_index
# There are two encoding for leafs: character final state for 15-bit
# characters and string final state for the rest.
if len(c) > 1 or ord(c[0]) > 32767: # String final state
# A ':' can be added to the result of a sequence to force a string
# final state. For example, to go through KeyValue lookup.
if c.startswith(":"): c = c[1:]
javachars = array('H', c.encode("UTF-16-LE"))
states.append((-1, len(javachars) + 1))
for c in javachars:
states.append((c, 0))
else: # Character final state
states.append((c, 1))
return this_node_index
def add_node(n):
if type(n) == str:
return add_leaf(n)
else:
return add_tree(n)
states.append((1, 1)) # Add an empty state at the beginning.
entry_states = { n: add_tree(root) for n, root in tries.items() }
return entry_states, states
# Debug
def print_automata(automata):
i = 0
for (s, e) in automata:
s = "%#06x" % s if isinstance(s, int) else '"%s"' % str(s)
print("%3d %8s %d" % (i, s, e), file=sys.stderr)
i += 1
# Report warnings about the compose sequences
def check_for_warnings(tries):
def get(seq):
t = tries
for c in seq:
if c not in t:
return None
t = t[c]
return t if type(t) == str else None
# Check that compose+Upper+Upper have an equivalent compose+Upper+Lower or compose+Lower+Lower
for c1 in string.ascii_uppercase:
for c2 in string.ascii_uppercase:
seq = [c1, c2]
seq_l = [c1, c2.lower()]
seq_ll = [c1.lower(), c2.lower()]
r = get(seq)
r_l = get(seq_l)
r_ll = get(seq_ll)
if r is not None:
ll_warning = f" (but {seq_to_str(seq_ll)} = {r_ll} exists)" if r_ll is not None else ""
if r_l is None:
if r != r_ll:
warn(f"has no lower case equivalent {seq_to_str(seq_l)}{ll_warning}", seq=seq, result=r)
elif r != r_l:
warn(f"is not the same as {seq_to_str(seq_l)} = {r_l}{ll_warning}", seq=seq, result=r)
def batched(ar, n):
i = 0
while i + n < len(ar):
yield ar[i:i+n]
i += n
if i < len(ar):
yield ar[i:]
# Print the state machine compiled by make_automata into java code that can be
# used by [ComposeKeyData.java].
def gen_java(entry_states, machine):
chars_map = {
# These characters cannot be used in unicode form as Java's parser
# unescape unicode sequences before parsing.
-1: "\\uFFFF",
"\"": "\\\"",
"\\": "\\\\",
"\n": "\\n",
"\r": "\\r",
ord("\""): "\\\"",
ord("\\"): "\\\\",
ord("\n"): "\\n",
ord("\r"): "\\r",
}
def char_repr(c):
if c in chars_map:
return chars_map[c]
if type(c) == int: # The edges array contains ints
return "\\u%04x" % c
if c in string.printable:
return c
return "\\u%04x" % ord(c)
def gen_array(array):
chars = list(map(char_repr, array))
return "\" +\n \"".join(map(lambda b: "".join(b), batched(chars, 72)))
def gen_entry_state(s):
name, state = s
return " public static final int %s = %d;" % (name, state)
print("""package juloo.keyboard2;
/** This file is generated, see [srcs/compose/compile.py]. */
public final class ComposeKeyData
{
public static final char[] states =
("%s").toCharArray();
public static final char[] edges =
("%s").toCharArray();
%s
}""" % (
# Break the edges array every few characters using string concatenation.
gen_array(map(lambda s: s[0], machine)),
gen_array(map(lambda s: s[1], machine)),
"\n".join(map(gen_entry_state, entry_states.items())),
))
total_sequences = 0
tries = {} # Orderred dict
for fname in sorted(sys.argv[1:]):
tname, _ = os.path.splitext(os.path.basename(fname))
if os.path.isdir(fname):
sequences = parse_sequences_dir(fname)
else:
sequences = parse_sequences_file(fname)
add_sequences_to_trie(sequences, tries.setdefault(tname, {}))
total_sequences += len(sequences)
check_for_warnings(tries["compose"])
entry_states, automata = make_automata(tries)
gen_java(entry_states, automata)
print("Compiled %d sequences into %d states. Dropped %d sequences. Generated %d warnings." % (total_sequences, len(automata), dropped_sequences, warning_count), file=sys.stderr)
# print_automata(automata)

View file

@ -0,0 +1,149 @@
{
"ا": {
"ا": "combining_alef_above",
"ع": "أ",
"و": "ۉ",
"ي": "ؽ",
"ی": "ؽ",
"۷": "combining_alef_below",
"٧": "combining_alef_below"
},
"ت": {
"د": "ط",
"ر": "ڑ",
"ش": "ث",
"ن": "ٹ"
},
"ج": {
"ش": "چ"
},
"ح": {
"ح": "combining_sukun"
},
"د": {
"ت": "ڈ",
"ز": "ذ",
"ت": "ڑ",
"۷": "ڕ"
},
"س": {
"ش": "ص"
},
"ش": {
"ت": "ث"
},
"ع": {
"ا": "إ",
"ه": "ۀ",
"و": "ؤ",
"ي": "ئ",
"ی": "ئ",
"۷": "combining_hamza_below",
"۸": "combining_hamza_above",
"٧": "combining_hamza_below",
"٨": "combining_hamza_above"
},
"غ": {
"ك": "گ",
"ک": "گ"
},
"ف": {
"و": "ڡ"
},
"ق": {
"و": "ۊ"
},
"ل": {
"ل": "combining_shaddah",
"۷": "ڵ",
"٧": "ڵ"
},
"ن": {
"ت": "ٹ",
"ه": "combining_fathatan",
"و": "combining_dammatan",
"ی": "combining_kasratan",
"ي": "combining_kasratan"
},
"ه": {
" ": "ە",
"ت": "ة",
"ع": "ۀ",
"ن": "combining_fathatan",
"ه": "combining_fatha",
"و": "ۆ",
"ي": "ێ",
"ی": "ێ"
},
"و": {
"ث": "ۋ",
"ع": "ؤ",
"ف": "ڡ",
"ن": "combining_dammatan",
"و": "combining_dammah",
"۷": "ۆ",
"۸": "ۉ",
"۸": "ۉ",
"٧": "ۆ",
"٨": "ۉ",
"٨": "ۉ"
},
"ي": {
" ": "ے",
"ا": "ى",
"ع": "ئ",
"ي": "combining_kasra",
"۷": "ێ",
"۸": "ؽ",
"ن": "combining_kasratan",
"٧": "ێ",
"٨": "ؽ"
},
"ی": {
" ": "ے",
"ا": "ى",
"ع": "ئ",
"ن": "combining_kasratan",
"ی": "combining_kasra",
"۷": "ێ",
"۸": "ؽ",
"٧": "ێ",
"٨": "ؽ"
},
"۷": {
"ا": "combining_alef_below",
"ر": "ڕ",
"ع": "combining_hamza_below",
"ل": "ڵ",
"و": "ۆ",
"ي": "ێ",
"ی": "ێ",
"۷": "combining_arabic_v"
},
"۸": {
"ع": "combining_hamza_above",
"و": "ۉ",
"و": "ۉ",
"ي": "ؽ",
"ی": "ؽ",
"۸": "combining_arabic_inverted_v"
},
"٧": {
"ا": "combining_alef_below",
"ر": "ڕ",
"ع": "combining_hamza_below",
"ل": "ڵ",
"و": "ۆ",
"ي": "ێ",
"٧": "combining_arabic_v",
"ی": "ێ"
},
"٨": {
"ع": "combining_hamza_above",
"و": "ۉ",
"و": "ۉ",
"ي": "ؽ",
"٨": "combining_arabic_inverted_v",
"ی": "ؽ"
}
}

View file

@ -0,0 +1,165 @@
{
",": {
"г": "ӻ",
"к": "ӄ",
"л": "ԓ",
"н": "ӈ",
"х": "ӽ",
"ѧ": "ӊ"
},
".": {
"г": "ӷ",
"ж": "җ",
"й": "ҋ",
"к": "қ",
"л": "ԯ",
"м": "ӎ",
"н": "ӊ",
"х": "ҳ",
"ч": "ҷ",
"і": "ї"
},
"а": {
"е": "ѣ",
"у": "ѡ",
"ч": "combining_aigu",
"ы": "ѣ",
"ь": "ꙙ",
"ꙋ": "ꙍ",
"ꙑ": "ѣ"
},
"б": {
"ч": "combining_slavonic_psili"
},
"г": {
",": "ӻ",
".": "ӷ",
"й": "ғ",
"к": "ґ",
"х": "ҁ",
"ј": "ғ"
},
"д": {
"е": "ꙉ",
"ж": "џ",
"з": "ꙃ",
"й": "ꙉ",
"ј": "ꙉ",
"ѥ": "ђ"
},
"е": {
"ч": "combining_trema"
},
"ж": {
".": "җ"
},
"з": {
"ф": "ҙ"
},
"и": {
"и": "ӣ",
"у": "ѵ"
},
"й": {
".": "ҋ",
"ч": "combining_breve"
},
"к": {
",": "ӄ",
".": "қ",
"г": "ґ",
"с": "ѯ",
"х": "ҁ",
"ш": "ѯ"
},
"л": {
",": "ԓ",
".": "ԯ",
"ь": "љ"
},
"м": {
".": "ӎ"
},
"н": {
",": "ӈ",
"·": "ԩ",
"ч": "combining_titlo",
"ь": "њ"
},
"о": {
"т": "ѿ",
"у": "ѹ",
"ч": "combining_inverted_breve"
},
"п": {
"с": "ѱ"
},
"т": {
"й": "ћ",
"ф": "ѳ",
"ј": "ћ"
},
"у": {
"и": "ѵ",
"й": "ў",
"у": "ӯ",
"ч": "combining_pokrytie",
"і": "ѵ",
"ј": "ў"
},
"х": {
",": "ӽ",
".": "ҳ",
"ч": "combining_slavonic_dasia"
},
"ч": {
".": "ҷ",
"а": "combining_aigu",
"б": "combining_slavonic_psili",
"е": "combining_trema",
"й": "combining_breve",
"н": "combining_titlo",
"о": "combining_inverted_breve",
"у": "combining_pokrytie",
"х": "combining_slavonic_dasia",
"ч": "combining_payerok",
"ч": "combining_payerok",
"ъ": "combining_vertical_tilde",
"ю": "combining_grave",
"ј": "combining_breve",
"ѧ": "combining_vzmet"
},
"ш": {
"т": "щ"
},
"ъ": {
"ч": "combining_vertical_tilde"
},
"ю": {
"а": "ꙓ",
"е": "ё",
"м": "ѭ",
"н": "ѩ",
"ч": "combining_grave"
},
"я": {
"ь": "ꙝ"
},
"і": {
"\"": "ї",
".": "ї",
"у": "ѵ",
"і": "ӣ"
},
"ј": {
"а": "ꙗ",
"ч": "combining_breve",
"ѣ": "ꙝ"
},
"ѡ": {
"т": "ѿ"
},
"ѧ": {
"ч": "combining_vzmet"
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,62 @@
{
"V": {
"s": "Š",
"c": "Č",
"z": "Ž"
},
"\\": {
"n": "\\n",
"t": "\\t"
},
"n": {
"g": {
"~": "n͠g"
}
},
"N": {
"g": {
"~": "N͠g"
},
"g": "Ŋ",
"n": ""
},
"g": {
"~": "g̃",
"u": "Ğ"
},
"A": {
"a": "Å",
"e": "Æ",
"t": "@"
},
"a": {
"E": "Æ"
},
"O": {
"e": "Œ"
},
"S": {
"s": "ẞ"
},
"I": {
"j": "IJ"
},
"D": {
"h": "Ð"
},
"E": {
"e": "Ə"
},
"Q": {
"q": ""
},
"R": {
"r": ""
},
"T": {
"h": "Þ"
},
"Z": {
"z": ""
}
}

File diff suppressed because it is too large Load diff

268
srcs/compose/fn.json Normal file
View file

@ -0,0 +1,268 @@
{
"1": "f1",
"2": "f2",
"3": "f3",
"4": "f4",
"5": "f5",
"6": "f6",
"7": "f7",
"8": "f8",
"9": "f9",
"0": "f10",
"<": "«",
">": "»",
"{": "",
"}": "",
"[": "",
"]": "",
"(": "“",
")": "”",
"'": "",
"\"": "„",
"-": "",
"_": "—",
"^": "¬",
"%": "‰",
"=": "≈",
"u": "µ",
"a": "æ",
"o": "œ",
"*": "°",
".": "…",
",": "·",
"!": "¡",
"?": "¿",
"|": "¦",
"§": "¶",
"†": "‡",
"×": "∙",
" ": "nbsp",
// arrows
"↖": "⇖",
"↑": "⇑",
"↗": "⇗",
"←": "⇐",
"→": "⇒",
"↙": "⇙",
"↓": "⇓",
"↘": "⇘",
"↔": "⇔",
"↕": "⇕",
// Currency symbols
"e": "€",
"l": "£",
"r": "₹",
"y": "¥",
"c": "¢",
"p": "₽",
"b": "₱",
"h": "₴",
"z": "₿",
// avoid showing these twice
"€": "removed",
"£": "removed",
// alternative greek letters
"π": "ϖ",
"θ": "ϑ",
"Θ": "ϴ",
"ε": "ϵ",
"β": "ϐ",
"ρ": "ϱ",
"σ": "ς",
"γ": "ɣ",
"φ": "ϕ",
"υ": "ϒ",
"κ": "ϰ",
// alternative math characters
"": "",
"∩": "⋂",
"∃": "∄",
"∫": "∮",
"Π": "∏",
"Σ": "∑",
"": "",
"∧": "⋀",
"⊷": "⊶",
"⊂": "⊆",
"⊃": "⊇",
"±": "∓",
// APL
"": "⍶",
"⍵": "⍹",
"⋄": "⌺",
"⍝": "⍧",
"∆": "⍙",
"∇": "⍢",
"": "⍡",
"⎕": "⍞",
// hebrew niqqud
"ק": "qamats", // kamatz
"ר": "hataf_qamats", // reduced kamatz
"ו": "holam",
"ם": "rafe",
"פ": "patah", // patach
"ש": "sheva",
"ד": "dagesh", // or mapiq
"ח": "hiriq",
"ף": "hataf_patah", // reduced patach
"ז": "qubuts", // kubuts
"ס": "segol",
"ב": "hataf_segol", // reduced segol
"צ": "tsere",
// Devanagari symbols
"ए": "ऍ",
"े": "ॅ",
"ऐ": "ऎ",
"ै": "ॆ",
"ऋ": "ॠ",
"ृ": "ॄ",
"ळ": "ऴ",
"र": "ऱ",
"क": "क़",
"ख": "ख़",
"ग": "ग़",
"घ": "ॻ",
"ढ": "ढ़",
"न": "ऩ",
"ड": "ड़",
"ट": "ॸ",
"ण": "ॾ",
"फ": "फ़",
"ऌ": "ॡ",
"ॢ": "ॣ",
"औ": "ॵ",
"ौ": "ॏ",
"ओ": "ऒ",
"ो": "ॊ",
"च": "ॼ",
"ज": "ज़",
"ब": "ॿ",
"व": "ॺ",
"य": "य़",
"अ": "ॲ",
"आ": "ऑ",
"ा": "ॉ",
"झ": "ॹ",
"ई": "ॴ",
"ी": "ऻ",
"इ": "ॳ",
"ि": "ऺ",
"उ": "ॶ",
"ऊ": "ॷ",
"ु": "ऄ",
"ष": "क्ष",
"थ": "त्र",
"द": "द्र",
"प": "प्र",
"श": "श्र",
"छ": "श्च",
"ँ": "ऀ",
"₹": "₨",
"ॖ": "ॗ",
"॓": "॔",
"॰": "ॱ",
"।": "॥",
"ं": "ॕ",
"़": "ॎ",
"ऽ": "",
// Persian numbers
"۱": "f1",
"۲": "f2",
"۳": "f3",
"۴": "f4",
"۵": "f5",
"۶": "f6",
"۷": "f7",
"۸": "f8",
"۹": "f9",
// Arabic numbers
"۰": "f10",
"١": "f1",
"٢": "f2",
"٣": "f3",
"٤": "f4",
"٥": "f5",
"٦": "f6",
"٧": "f7",
"٨": "f8",
"٩": "f9",
"٠": "f10",
// Cyrillic
"ꙑ": "ы",
"ы": "ꙑ",
"ш": "ѱ",
"з": "ꙁ",
"и": "і",
"і": "и",
"я": "ꙗ",
"е": "ѥ",
"ѡ": "ꙍ",
"о": "ѻ",
"а": "ѣ",
"э": "є",
"ъ": "ь",
"ь": "ъ",
"й": "ј",
"ꙉ": "ђ",
"ч": "ћ",
"ҁ": "қ",
"қ": "ҁ",
"џ": "ҷ",
"ҷ": "џ",
"ј": "й",
"у": "ꙋ",
"м": "ѫ",
"н": "ѧ",
"с": "ѕ",
"л": "ԯ",
"ԓ": "ԯ",
"\ua67d": "\u0483",
"\u0487": "\ua66f",
"ӈ": "ԩ",
// Arabic
":": "zwnj",
"ل": "ڵ",
"\u064F": "ۆ", // combining_dammah
"\u0650": "ێ", // combining_kasra
"ر": "ڕ",
"ب": "ٮ",
"ه": "ھ",
"ث": "پ",
"ز": "ژ",
"غ": "گ",
"ك": "ک",
"ا": "آ",
"ي": "ی",
"ک": "ك",
"ط": "ظ",
"ص": "ض",
"ی": "ي",
"ق": "غ",
"ع": "ء",
"ح": "ہ",
"ێ": "combining_kasra",
"ئ": "combining_hamza_above",
"ؽ": "combining_arabic_inverted_v",
"ۉ": "combining_arabic_inverted_v",
"ڡ": "combining_dammah",
"ة": "combining_fatha",
"إ": "combining_hamza_below",
"ۆ": "combining_arabic_v",
"س": "ـ",
"ف": "ڤ",
"ن": "ں",
// Tamil
"ய": ":௰",
"ஒ": ":ௐ",
"ள": ":௱",
"ச": ":௲",
"வ": ":௳"
}

View file

@ -0,0 +1,12 @@
{
"0": "",
"1": "১",
"2": "২",
"3": "৩",
"4": "",
"5": "৫",
"6": "৬",
"7": "",
"8": "৮",
"9": "৯"
}

View file

@ -0,0 +1,12 @@
{
"0": "",
"1": "१",
"2": "२",
"3": "३",
"4": "४",
"5": "५",
"6": "६",
"7": "७",
"8": "८",
"9": "९"
}

View file

@ -0,0 +1,12 @@
{
"0": "",
"1": "૧",
"2": "૨",
"3": "૩",
"4": "૪",
"5": "૫",
"6": "૬",
"7": "૭",
"8": "૮",
"9": "૯"
}

View file

@ -0,0 +1,14 @@
// Used with Arabic despite the name; called "Hindi numerals" in Arabic
// numpad_devanagari is used in Hindi
{
"0": "٠",
"1": "١",
"2": "٢",
"3": "٣",
"4": "٤",
"5": "٥",
"6": "٦",
"7": "٧",
"8": "٨",
"9": "٩"
}

View file

@ -0,0 +1,12 @@
{
"0": "",
"1": "೧",
"2": "೨",
"3": "೩",
"4": "೪",
"5": "೫",
"6": "೬",
"7": "೭",
"8": "೮",
"9": "೯"
}

View file

@ -0,0 +1,12 @@
{
"0": "۰",
"1": "۱",
"2": "۲",
"3": "۳",
"4": "۴",
"5": "۵",
"6": "۶",
"7": "۷",
"8": "۸",
"9": "۹"
}

View file

@ -0,0 +1,12 @@
{
"0": "",
"1": "௧",
"2": "௨",
"3": "௩",
"4": "௪",
"5": "௫",
"6": "௬",
"7": "௭",
"8": "௮",
"9": "௯"
}

138
srcs/compose/shift.json Normal file
View file

@ -0,0 +1,138 @@
{
"↙": "⇙",
"↓": "⇓",
"↘": "⇘",
"←": "⇐",
"→": "⇒",
"↖": "⇖",
"↑": "⇑",
"↗": "⇗",
"└": "╚",
"┴": "╩",
"┘": "╝",
"├": "╠",
"┼": "╬",
"┤": "╣",
"┌": "╔",
"┬": "╦",
"┐": "╗",
"─": "═",
"│": "║",
"∈": "∉",
"∋": "∌",
"⊂": "⊄",
"⊃": "⊅",
"⊆": "⊈",
"⊇": "⊉",
// superscript
"ᵃ": "ᴬ",
"ᵇ": "ᴮ",
"ᶜ": "ꟲ",
"ᵈ": "ᴰ",
"ᵉ": "ᴱ",
"ᶠ": "ꟳ",
"ᵍ": "ᴳ",
"ʰ": "ᴴ",
"ⁱ": "ᴵ",
"ʲ": "ᴶ",
"ᵏ": "ᴷ",
"ˡ": "ᴸ",
"ᵐ": "ᴹ",
"ⁿ": "ᴺ",
"ᵒ": "ᴼ",
"ᵖ": "ᴾ",
"ʳ": "ᴿ",
"ᵗ": "ᵀ",
"ᵘ": "ᵁ",
"ᵛ": "ⱽ",
"ʷ": "ᵂ",
"ᶾ": "ᴣ",
"ᵠ": "ᶲ",
// german eszett has an uppercase, but because it is uncommon, java doesn't know about it
"ß": "ẞ",
// these characters don't have a preapplied uppercase version, so we use combining characters
"ẗ": "T\u0308",
"ẘ": "W\u030A",
"ẙ": "Y\u030A",
"ǰ": "J\u030C",
"ȷ": "J\u0307",
// In Turkish, upper case of 'iı' is 'İI' but Java's toUpperCase will
// return 'II'. To make 'İ' accessible, make it the shift of 'ı'. This
// has the inconvenient of swapping i and ı on the keyboard.
"ı": "İ",
"₹": "₨",
// Gujarati alternate characters
"અ": "આ",
"ઇ": "ઈ",
"િ": "ી",
"ઉ": "ઊ",
"ુ": "ૂ",
"એ": "ઐ",
"ે": "ૈ",
"ઓ": "ઔ",
"ો": "ૌ",
"ક": "ખ",
"ગ": "ઘ",
"ચ": "છ",
"જ": "ઝ",
"ટ": "ઠ",
"ડ": "ઢ",
"ન": "ણ",
"ત": "થ",
"દ": "ધ",
"પ": "ફ",
"બ": "ભ",
"મ": "ં",
"લ": "ળ",
"સ": "શ",
"હ": "",
// Tamil alternate characters
"௹": "₨",
// Modern Hindi and Sanskrit
"अ": "आ",
"इ": "ई",
"ि": "ी",
"उ": "ऊ",
"ु": "ू",
"ए": "ऐ",
"े": "ै",
"ओ": "औ",
"ो": "ौ",
"क": "ख",
"ग": "घ",
"च": "छ",
"ज": "झ",
"ट": "ठ",
"ड": "ढ",
"न": "ण",
"त": "थ",
"द": "ध",
"ब": "भ",
"म": "ं",
"ल": "ळ",
"स": "श",
"ह": "",
"ऋ": "ॠ",
"ृ": "ॄ",
"ऌ": "ॡ",
"ॢ": "ॣ",
"॒": "॑",
"ॅ": "ॲ",
"ॉ": "ऑ",
// Mathematical symbols
"\uD835": {
"\uDD68": "𝕎", // 𝕨 𝕎
"\uDD69": "𝕏", // 𝕩 𝕏
"\uDD57": "𝔽", // 𝕗 𝔽
"\uDD58": "𝔾", // 𝕘 𝔾
"\uDD64": "𝕊" // 𝕤 𝕊
}
}

View file

@ -0,0 +1,203 @@
package juloo.keyboard2;
import android.os.Handler;
import android.text.InputType;
import android.text.TextUtils;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.KeyEvent;
public final class Autocapitalisation
{
boolean _enabled = false;
boolean _should_enable_shift = false;
boolean _should_disable_shift = false;
boolean _should_update_caps_mode = false;
Handler _handler;
InputConnection _ic;
Callback _callback;
int _caps_mode;
/** Keep track of the cursor to recognize cursor movements from typing. */
int _cursor;
static int SUPPORTED_CAPS_MODES =
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES |
InputType.TYPE_TEXT_FLAG_CAP_WORDS;
public Autocapitalisation(Handler h, Callback cb)
{
_handler = h;
_callback = cb;
}
/**
* The events are: started, typed, event sent, selection updated
* [started] does initialisation work and must be called before any other
* event.
*/
public void started(EditorInfo info, InputConnection ic)
{
_ic = ic;
_caps_mode = info.inputType & TextUtils.CAP_MODE_SENTENCES;
if (!Config.globalConfig().autocapitalisation || _caps_mode == 0)
{
_enabled = false;
return;
}
_enabled = true;
_should_enable_shift = (info.initialCapsMode != 0);
_should_update_caps_mode = started_should_update_state(info.inputType);
callback_now(true);
}
public void typed(CharSequence c)
{
for (int i = 0; i < c.length(); i++)
type_one_char(c.charAt(i));
callback(false);
}
public void event_sent(int code, int meta)
{
if (meta != 0)
{
_should_enable_shift = false;
_should_update_caps_mode = false;
return;
}
switch (code)
{
case KeyEvent.KEYCODE_DEL:
if (_cursor > 0) _cursor--;
_should_update_caps_mode = true;
break;
case KeyEvent.KEYCODE_ENTER:
_should_update_caps_mode = true;
break;
}
callback(true);
}
public void stop()
{
_should_enable_shift = false;
_should_update_caps_mode = false;
callback_now(true);
}
/** Pause auto capitalisation until [unpause()] is called. */
public boolean pause()
{
boolean was_enabled = _enabled;
stop();
_enabled = false;
return was_enabled;
}
/** Continue auto capitalisation after [pause()] was called. Argument is the
output of [pause()]. */
public void unpause(boolean was_enabled)
{
_enabled = was_enabled;
_should_update_caps_mode = true;
callback_now(true);
}
public static interface Callback
{
public void update_shift_state(boolean should_enable, boolean should_disable);
}
/** Returns [true] if shift might be disabled. */
public void selection_updated(int old_cursor, int new_cursor)
{
if (new_cursor == _cursor) // Just typing
return;
if (new_cursor == 0 && _ic != null)
{
// Detect whether the input box has been cleared
CharSequence t = _ic.getTextAfterCursor(1, 0);
if (t != null && t.equals(""))
_should_update_caps_mode = true;
}
_cursor = new_cursor;
_should_enable_shift = false;
callback(true);
}
Runnable delayed_callback = new Runnable()
{
public void run()
{
if (_should_update_caps_mode && _ic != null)
{
_should_enable_shift = _enabled && (_ic.getCursorCapsMode(_caps_mode) != 0);
_should_update_caps_mode = false;
}
_callback.update_shift_state(_should_enable_shift, _should_disable_shift);
}
};
/** Update the shift state if [_should_update_caps_mode] is true, then call
[_callback.update_shift_state]. This is done after a short delay to wait
for the editor to handle the events, as this might be called before the
corresponding event is sent. */
void callback(boolean might_disable)
{
_should_disable_shift = might_disable;
// The callback must be delayed because [getCursorCapsMode] would sometimes
// be called before the editor finished handling the previous event.
_handler.postDelayed(delayed_callback, 1);
}
/** Like [callback] but runs immediately. */
void callback_now(boolean might_disable)
{
_should_disable_shift = might_disable;
delayed_callback.run();
}
void type_one_char(char c)
{
_cursor++;
if (is_trigger_character(c))
_should_update_caps_mode = true;
else
_should_enable_shift = false;
}
boolean is_trigger_character(char c)
{
switch (c)
{
case ' ':
return true;
default:
return false;
}
}
/** Whether the caps state should be updated when input starts. [inputType]
is the field from the editor info object. */
boolean started_should_update_state(int inputType)
{
int class_ = inputType & InputType.TYPE_MASK_CLASS;
int variation = inputType & InputType.TYPE_MASK_VARIATION;
if (class_ != InputType.TYPE_CLASS_TEXT)
return false;
switch (variation)
{
case InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE:
case InputType.TYPE_TEXT_VARIATION_NORMAL:
case InputType.TYPE_TEXT_VARIATION_PERSON_NAME:
case InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE:
case InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT:
case InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT:
return true;
default:
return false;
}
}
}

View file

@ -0,0 +1,23 @@
package juloo.keyboard2;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.CheckBox;
import android.widget.CompoundButton;
final class ClipboardHistoryCheckBox extends CheckBox
implements CompoundButton.OnCheckedChangeListener
{
public ClipboardHistoryCheckBox(Context ctx, AttributeSet attrs)
{
super(ctx, attrs);
setChecked(Config.globalConfig().clipboard_history_enabled);
setOnCheckedChangeListener(this);
}
@Override
public void onCheckedChanged(CompoundButton _v, boolean isChecked)
{
ClipboardHistoryService.set_history_enabled(isChecked);
}
}

View file

@ -0,0 +1,182 @@
package juloo.keyboard2;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.os.Build.VERSION;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public final class ClipboardHistoryService
{
/** Start the service on startup and start listening to clipboard changes. */
public static void on_startup(Context ctx, ClipboardPasteCallback cb)
{
get_service(ctx);
_paste_callback = cb;
}
/** Start the service if it hasn't been started before. Returns [null] if the
feature is unsupported. */
public static ClipboardHistoryService get_service(Context ctx)
{
if (VERSION.SDK_INT <= 11)
return null;
if (_service == null)
_service = new ClipboardHistoryService(ctx);
return _service;
}
public static void set_history_enabled(boolean e)
{
Config.globalConfig().set_clipboard_history_enabled(e);
if (_service == null)
return;
if (e)
_service.add_current_clip();
else
_service.clear_history();
}
/** Send the given string to the editor. */
public static void paste(String clip)
{
if (_paste_callback != null)
_paste_callback.paste_from_clipboard_pane(clip);
}
/** The maximum size limits the amount of user data stored in memory but also
gives a sense to the user that the history is not persisted and can be
forgotten as soon as the app stops. */
public static final int MAX_HISTORY_SIZE = 6;
/** Time in ms until history entries expire. */
public static final long HISTORY_TTL_MS = 5 * 60 * 1000;
static ClipboardHistoryService _service = null;
static ClipboardPasteCallback _paste_callback = null;
ClipboardManager _cm;
List<HistoryEntry> _history;
OnClipboardHistoryChange _listener = null;
ClipboardHistoryService(Context ctx)
{
_history = new ArrayList<HistoryEntry>();
_cm = (ClipboardManager)ctx.getSystemService(Context.CLIPBOARD_SERVICE);
_cm.addPrimaryClipChangedListener(this.new SystemListener());
}
public List<String> clear_expired_and_get_history()
{
long now_ms = System.currentTimeMillis();
List<String> dst = new ArrayList<String>();
Iterator<HistoryEntry> it = _history.iterator();
while (it.hasNext())
{
HistoryEntry ent = it.next();
if (ent.expiry_timestamp <= now_ms)
it.remove();
else
dst.add(ent.content);
}
return dst;
}
/** This will call [on_clipboard_history_change]. */
public void remove_history_entry(String clip)
{
int last_pos = _history.size() - 1;
for (int pos = last_pos; pos >= 0; pos--)
{
if (!_history.get(pos).content.equals(clip))
continue;
// Removing the current clipboard, clear the system clipboard.
if (pos == last_pos)
{
if (VERSION.SDK_INT >= 28)
_cm.clearPrimaryClip();
else
_cm.setText("");
}
_history.remove(pos);
if (_listener != null)
_listener.on_clipboard_history_change();
}
}
/** Add clipboard entries to the history, skipping consecutive duplicates and
empty strings. */
public void add_clip(String clip)
{
if (!Config.globalConfig().clipboard_history_enabled)
return;
int size = _history.size();
if (clip.equals("") || (size > 0 && _history.get(size - 1).content.equals(clip)))
return;
if (size >= MAX_HISTORY_SIZE)
_history.remove(0);
_history.add(new HistoryEntry(clip));
if (_listener != null)
_listener.on_clipboard_history_change();
}
public void clear_history()
{
_history.clear();
if (_listener != null)
_listener.on_clipboard_history_change();
}
public void set_on_clipboard_history_change(OnClipboardHistoryChange l) { _listener = l; }
public static interface OnClipboardHistoryChange
{
public void on_clipboard_history_change();
}
/** Add what is currently in the system clipboard into the history. */
void add_current_clip()
{
ClipData clip = _cm.getPrimaryClip();
if (clip == null)
return;
int count = clip.getItemCount();
for (int i = 0; i < count; i++)
{
CharSequence text = clip.getItemAt(i).getText();
if (text != null)
add_clip(text.toString());
}
}
final class SystemListener implements ClipboardManager.OnPrimaryClipChangedListener
{
public SystemListener() {}
@Override
public void onPrimaryClipChanged()
{
add_current_clip();
}
}
static final class HistoryEntry
{
public final String content;
/** Time at which the entry expires. */
public final long expiry_timestamp;
public HistoryEntry(String c)
{
content = c;
expiry_timestamp = System.currentTimeMillis() + HISTORY_TTL_MS;
}
}
public interface ClipboardPasteCallback
{
public void paste_from_clipboard_pane(String content);
}
}

View file

@ -0,0 +1,125 @@
package juloo.keyboard2;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class ClipboardHistoryView extends NonScrollListView
implements ClipboardHistoryService.OnClipboardHistoryChange
{
List<String> _history;
ClipboardHistoryService _service;
ClipboardEntriesAdapter _adapter;
public ClipboardHistoryView(Context ctx, AttributeSet attrs)
{
super(ctx, attrs);
_history = Collections.EMPTY_LIST;
_adapter = this.new ClipboardEntriesAdapter();
_service = ClipboardHistoryService.get_service(ctx);
if (_service != null)
{
_service.set_on_clipboard_history_change(this);
_history = _service.clear_expired_and_get_history();
}
setAdapter(_adapter);
}
/** The history entry at index [pos] is removed from the history and added to
the list of pinned clipboards. */
public void pin_entry(int pos)
{
ClipboardPinView v = (ClipboardPinView)((ViewGroup)getParent().getParent()).findViewById(R.id.clipboard_pin_view);
String clip = _history.get(pos);
v.add_entry(clip);
_service.remove_history_entry(clip);
}
/** Send the specified entry to the editor. */
public void paste_entry(int pos)
{
ClipboardHistoryService.paste(_history.get(pos));
}
@Override
public void on_clipboard_history_change()
{
update_data();
}
@Override
protected void onWindowVisibilityChanged(int visibility)
{
if (visibility == View.VISIBLE)
update_data();
}
void update_data()
{
_history = _service.clear_expired_and_get_history();
_adapter.notifyDataSetChanged();
invalidate();
}
class ClipboardEntriesAdapter extends BaseAdapter
{
public ClipboardEntriesAdapter() {}
@Override
public int getCount() { return _history.size(); }
@Override
public Object getItem(int pos) { return _history.get(pos); }
@Override
public long getItemId(int pos) { return _history.get(pos).hashCode(); }
@Override
public View getView(final int pos, View v, ViewGroup _parent)
{
if (v == null)
v = View.inflate(getContext(), R.layout.clipboard_history_entry, null);
((TextView)v.findViewById(R.id.clipboard_entry_text))
.setText(_history.get(pos));
v.findViewById(R.id.clipboard_entry_addpin).setOnClickListener(
new View.OnClickListener()
{
@Override
public void onClick(View v) { pin_entry(pos); }
});
v.findViewById(R.id.clipboard_entry_paste).setOnClickListener(
new View.OnClickListener()
{
@Override
public void onClick(View v) { paste_entry(pos); }
});
// v.findViewById(R.id.clipboard_entry_removehist).setOnClickListener(
// new View.OnClickListener()
// {
// @Override
// public void onClick(View v)
// {
// AlertDialog d = new AlertDialog.Builder(getContext())
// .setTitle(R.string.clipboard_remove_confirm)
// .setPositiveButton(R.string.clipboard_remove_confirmed,
// new DialogInterface.OnClickListener(){
// public void onClick(DialogInterface _dialog, int _which)
// {
// _service.remove_history_entry(_history.get(pos));
// }
// })
// .setNegativeButton(android.R.string.cancel, null)
// .create();
// Utils.show_dialog_on_ime(d, v.getWindowToken());
// }
// });
return v;
}
}
}

View file

@ -0,0 +1,139 @@
package juloo.keyboard2;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.json.JSONArray;
import org.json.JSONException;
public final class ClipboardPinView extends NonScrollListView
{
/** Preference file name that store pinned clipboards. */
static final String PERSIST_FILE_NAME = "clipboards";
/** Preference name for pinned clipboards. */
static final String PERSIST_PREF = "pinned";
List<String> _entries;
ClipboardPinEntriesAdapter _adapter;
SharedPreferences _persist_store;
public ClipboardPinView(Context ctx, AttributeSet attrs)
{
super(ctx, attrs);
_entries = new ArrayList<String>();
_persist_store =
ctx.getSharedPreferences("pinned_clipboards", Context.MODE_PRIVATE);
load_from_prefs(_persist_store, _entries);
_adapter = this.new ClipboardPinEntriesAdapter();
setAdapter(_adapter);
}
/** Pin a clipboard and persist the change. */
public void add_entry(String text)
{
_entries.add(text);
_adapter.notifyDataSetChanged();
persist();
invalidate();
}
/** Remove the entry at index [pos] and persist the change. */
public void remove_entry(int pos)
{
if (pos < 0 || pos >= _entries.size())
return;
_entries.remove(pos);
_adapter.notifyDataSetChanged();
persist();
invalidate();
}
/** Send the specified entry to the editor. */
public void paste_entry(int pos)
{
ClipboardHistoryService.paste(_entries.get(pos));
}
void persist() { save_to_prefs(_persist_store, _entries); }
static void load_from_prefs(SharedPreferences store, List<String> dst)
{
String arr_s = store.getString(PERSIST_PREF, null);
if (arr_s == null)
return;
try
{
JSONArray arr = new JSONArray(arr_s);
for (int i = 0; i < arr.length(); i++)
dst.add(arr.getString(i));
}
catch (JSONException _e) {}
}
static void save_to_prefs(SharedPreferences store, List<String> entries)
{
JSONArray arr = new JSONArray();
for (int i = 0; i < entries.size(); i++)
arr.put(entries.get(i));
store.edit()
.putString(PERSIST_PREF, arr.toString())
.commit();
}
class ClipboardPinEntriesAdapter extends BaseAdapter
{
public ClipboardPinEntriesAdapter() {}
@Override
public int getCount() { return _entries.size(); }
@Override
public Object getItem(int pos) { return _entries.get(pos); }
@Override
public long getItemId(int pos) { return _entries.get(pos).hashCode(); }
@Override
public View getView(final int pos, View v, ViewGroup _parent)
{
if (v == null)
v = View.inflate(getContext(), R.layout.clipboard_pin_entry, null);
((TextView)v.findViewById(R.id.clipboard_pin_text))
.setText(_entries.get(pos));
v.findViewById(R.id.clipboard_pin_paste).setOnClickListener(
new View.OnClickListener()
{
@Override
public void onClick(View v) { paste_entry(pos); }
});
v.findViewById(R.id.clipboard_pin_remove).setOnClickListener(
new View.OnClickListener()
{
@Override
public void onClick(View v)
{
AlertDialog d = new AlertDialog.Builder(getContext())
.setTitle(R.string.clipboard_remove_confirm)
.setPositiveButton(R.string.clipboard_remove_confirmed,
new DialogInterface.OnClickListener(){
public void onClick(DialogInterface _dialog, int _which)
{
remove_entry(pos);
}
})
.setNegativeButton(android.R.string.cancel, null)
.create();
Utils.show_dialog_on_ime(d, v.getWindowToken());
}
});
return v;
}
}
}

View file

@ -0,0 +1,86 @@
package juloo.keyboard2;
import java.util.Arrays;
public final class ComposeKey
{
/** Apply the pending compose sequence to [kv]. Returns [null] if no sequence
matched. */
public static KeyValue apply(int state, KeyValue kv)
{
switch (kv.getKind())
{
case Char:
return apply(state, kv.getChar());
case String:
return apply(state, kv.getString());
}
return null;
}
/** Apply the pending compose sequence to char [c]. Returns [null] if no
sequence matched. */
public static KeyValue apply(int prev, char c)
{
char[] states = ComposeKeyData.states;
char[] edges = ComposeKeyData.edges;
int prev_length = edges[prev];
int next = Arrays.binarySearch(states, prev + 1, prev + prev_length, c);
if (next < 0)
return null;
next = edges[next];
int next_header = states[next];
if (next_header == 0) // Enter a new intermediate state.
return KeyValue.makeComposePending(String.valueOf(c), next, 0);
else if (next_header == 0xFFFF) // String final state
{
int next_length = edges[next];
return KeyValue.getKeyByName(
new String(states, next + 1, next_length - 1));
}
else // Character final state.
return KeyValue.makeCharKey((char)next_header);
}
/** Apply each char of a string to a sequence. Returns [null] if no sequence
matched. */
public static KeyValue apply(int prev, String s)
{
final int len = s.length();
int i = 0;
if (len == 0) return null;
while (true)
{
KeyValue k = apply(prev, s.charAt(i));
i++;
if (k == null) return null;
if (i >= len) return k;
if (k.getKind() != KeyValue.Kind.Compose_pending)
return null; // Found a final state before the end of [s].
prev = k.getPendingCompose();
}
}
/** The state machine is comprised of two arrays.
The [states] array represents the different states and the associated
transitions:
- The first cell is the header cell, [states[s]].
- If the header is equal to [0],
The remaining cells are the transitions characters, sorted
alphabetically.
- If the header is positive,
This is a final state, [states[s]] is the result of the sequence.
In this case, [edges[s]] must be equal to [1].
- If the header is equal to [-1],
This is a final state, the remaining cells represent the result string
which starts at index [s + 1] and has a length of [edges[s] - 1].
The [edges] array represents the transition state corresponding to each
accepted inputs.
- If [states[s]] is a header cell, [edges[s]] is the number of cells
occupied by the state [s], including the header cell.
- If [states[s]] is a transition, [edges[s]] is the index of the state to
jump into.
- If [states[s]] is a part of a final state, [edges[s]] is not used. */
}

View file

@ -0,0 +1,278 @@
package juloo.keyboard2;
/** This file is generated, see [srcs/compose/compile.py]. */
public final class ComposeKeyData
{
public static final char[] states =
("\u0001\u0000acegijklmnoprsuwyz\u00e2\u00e5\u00e6\u00e7\u00ea\u00ef\u00f4\u00f5\u00f8\u00fc\u0103\u0105\u0113\u014d\u0169\u01a1\u01b0\u03b1\u03b5\u03b7\u03b9\u03bf\u03c5\u0430\u0433\u0435\u0438\u043a\u043e\u0443\u044b\u044d\u044e\u044f\u1e61\u00e1\u0107\u00e9\u01f5\u00ed\uFFFF\u006a\u0301\u1e31\u013a\u1e3f\u0144\u00f3\u1e55\u0155\u015b\u00fa" +
"\u1e83\u00fd\u017a\u1ea5\u01fb\u01fd\u1e09\u1ebf\u1e2f\u1ed1\u1e4d\u01ff\u01d8\u1eaf\uFFFF\u0105\u0301\u1e17\u1e53\u1e79\u1edb\u1ee9\u03ac\u03ad\u03ae\u03af\u03cc\u03cd\uFFFF\u0430\u0301\u0453\uFFFF\u0435\u0301\uFFFF\u0438\u0301\u045c\uFFFF\u043e\u0301\uFFFF\u0443\u0301\uFFFF\u044b\u0301\uFFFF\u044d\u0301\uFFFF\u044e\u0301\uFFFF\u044f\u0301\u1e65\u0000.0123456789\u21b5\u2194" +
"\u2199\u2193\u2198\u2190\u2195\u2192\u2196\u2191\u2197\u00002bcdfghijklopqrtuyz\u0237\u043e\u0445\u044c\u04af\u04e7\u01bb\u0180\uFFFF\ua793\u0111\uFFFF\ua799\u01e5\u0127\u0268\u0249\uFFFF\ua741\u019a\u0275\u1d7d\uFFFF\ua757\u024d\u0167\u0289\u024f\u01b6\u025f\u04e9\u04ff\u048d\u04b1\u04eb\u0000.012345" +
"6789\u2502\u2500\u2514\u2534\u2518\u251c\u253c\u2524\u250c\u252c\u2510\u0000acdeghijklnorstuz\u00fc\u0292\u0432\u0433\u0434\u0437\u0440\u0442\u0445\u0493\u1e61\u01ce\u010d\u010f\u011b\u01e7\u021f\u01d0\u01f0\u01e9\u013e\u0148\u01d2\u0159\u0161\u0165\u01d4\u017e\u01da\u01ef\uFFFF\u0432\u030c\uFFFF\u0433\u030c\uFFFF\u0434\u030c" +
"\uFFFF\u0437\u030c\uFFFF\u0440\u030c\uFFFF\u0442\u030c\uFFFF\u0445\u030c\uFFFF\u0493\u030c\u1e67\u0000cdeghklnrst\u0107\u0115\u00e7\u1e11\u0229\u0123\u1e29\u0137\u013c\u0146\u0157\u015f\u0163\u1e1d\u0000+aceghijouwxyz\u00e0\u00e1\u00e3\u00e8\u00e9\u00f2\u00f3\u00f5\u015d\u01a1\u0430\u0435\u0438\u043e\u0443" +
"\u1ea1\u1ea3\u1eb9\u1ebb\u1ebd\u1ecd\u2248\u2a23\u00e2\u0109\u00ea\u011d\u0125\u00ee\u0135\u00f4\u00fb\u0175\uFFFF\u0078\u0302\u0177\u1e91\u1ea7\u1eab\u1ec1\u1ed3\u1ed7\u015d\u1ed5\uFFFF\u0430\u0302\uFFFF\u0435\u0302\uFFFF\u0438\u0302\uFFFF\u043e\u0302\uFFFF\u0443\u0302\u1ead\u1ea9\u1ec7\u1ec3\u1ec5\u1ed9\u2a6f\u00000123456789abcdefghi" +
"jklmnopqrstuvwxyz~\u0101\u014d\u015b\u0161\u017f\u1e63\u2208\u2227\u2228\u2261\u2286\u2287\u2a2f\uFFFF\u0030\u0307\uFFFF\u0031\u0307\uFFFF\u0032\u0307\uFFFF\u0033\u0307\uFFFF\u0034\u0307\uFFFF\u0035\u0307\uFFFF\u0036\u0307\uFFFF\u0037\u0307\uFFFF\u0038\u0307\uFFFF\u0039\u0307\u0227\u1e03\u010b\u1e0b\u0117\u1e1f\u0121\u1e23\u0131\u0237\uFFFF" +
"\u006b\u0307\uFFFF\u006c\u0307\u1e41\u1e45\u022f\u1e57\uFFFF\u0071\u0307\u1e59\u1e61\u1e6b\uFFFF\u0075\u0307\uFFFF\u0076\u0307\u1e87\u1e8b\u1e8f\u017c\u2a6a\u01e1\u0231\u1e9b\u1e69\u22f5\u2a51\u2a52\u2a67\u2ac3\u2ac4\u2a30\u0000+-=abdehiklmnorstuvwyz\u00e2\u00ea\u00f4\u0103\u01a1\u01b0\u1e61\u2a25\u2a2a\u2a66\u1ea1\u1e05" +
"\u1e0d\u1eb9\u1e25\u1ecb\u1e33\u1e37\u1e43\u1e47\u1ecd\u1e5b\u1e63\u1e6d\u1ee5\u1e7f\u1e89\u1ef5\u1e93\u1eb7\u1ee3\u1ef1\u0000 aeimouy\u0443\u02dd\uFFFF\u0061\u030b\uFFFF\u0065\u030b\uFFFF\u0069\u030b\uFFFF\u006d\u030b\u0151\u0171\uFFFF\u0079\u030b\u04f3\u0000aeioru\u0430\u0435\u0438\u043e\u0440\u0443\u0475\u0201\u0205\u0209\u020d\u0211\u0215\uFFFF\u0430\u030f" +
"\uFFFF\u0435\u030f\uFFFF\u0438\u030f\uFFFF\u043e\u030f\uFFFF\u0440\u030f\uFFFF\u0443\u030f\u0477\u0000aeinosuwyz\u00e2\u00ea\u00f4\u00fc\u0103\u0113\u014d\u01a1\u01b0\u0254\u028c\u03b1\u03b5\u03b7\u03b9\u03bf\u03c5\u03c9\u0435\u0438\u00e0\u00e8\u00ec\u01f9\u00f2\uFFFF\u0073\u0300\u00f9\u1e81\u1ef3\uFFFF\u007a\u0300\u01dc\u1eb1\u1e15\u1e51\u1edd\u1eeb\uFFFF\u0254\u0300\uFFFF\u028c" +
"\u0300\u1f70\u1f72\u1f74\u1f76\u1f78\u1f7a\u1f7c\u0450\u045d\u0000aeiouy\u00e2\u00ea\u00f4\u0103\u01a1\u01b0\u1ea3\u1ebb\u1ec9\u1ecf\u1ee7\u1ef7\u1eb3\u1edf\u1eed\u0000ou\u00f2\u00f3\u00f5\u00f9\u00fa\u0169\u1ecd\u1ecf\u1ee5\u1ee7\u01a1\u01b0\u1ee1\u1eef\u0000aegiloruy\u00e4\u00e6\u00e8\u00e9\u00f2\u00f3\u00f5\u00f6\u00fc\u01eb\u0227\u022f\u03b1" +
"\u03b9\u03c5\u0438\u0443\u1e37\u1e5b\u0101\u0113\u1e21\u012b\uFFFF\u006c\u0304\u014d\uFFFF\u0072\u0304\u016b\u0233\u01df\u01e3\u022d\u022b\u01d6\u01ed\u1fb1\u1fd1\u1fe1\u04e3\u04ef\u1e39\u1e5d\u0000aeiou\u014d\u0105\u0119\u012f\u01eb\u0173\u0000*123456789ao\u00b0\u00aa\u00ba\u207f\u1d48\u1d49\u02b3\u02e2\u1d57\u02b0\u0000auwy" +
"~\u00e1\u00e5\u016f\u1e98\u1e99\u2e1b\u0000abcegklnorstuv\u00f3\u0254\u2c65\u2422\u023c\u0247\uFFFF\ua7a1\uFFFF\ua743\u0142\uFFFF\ua7a5\u00f8\uFFFF\ua7a7\uFFFF\ua7a9\u2c66\uFFFF\ua7b9\uFFFF\ua75f\uFFFF\uab3f\u0000()+-0123456789=aehijklmn" +
"oprstuvx\u0259\u03b2\u03b3\u03c1\u03c6\u03c7\u208d\u208e\u208a\u208b\u2080\u2081\u2082\u2083\u2084\u2085\u2086\u2087\u2088\u2089\u208c\u2090\u2091\u2095\u1d62\u2c7c\u2096\u2097\u2098\u2099\u2092\u209a\u1d63\u209b\u209c\u1d64\u1d65\u2093\u2094\u1d66\u1d67\u1d68\u1d69\u1d6a\u0000()+-0123456789=abcd" +
"efghijklmnopqrstuvwxyz\u014b\u0153\u0250\u0252\u0254\u0255\u0259\u025b\u025c\u025f\u0263\u0265\u0266\u0268\u026f\u0270\u0271\u0275\u0279\u027b\u0281\u0282\u0283\u0289\u028a\u028c\u028d\u0292\u03b2\u03b4\u03b8\u03b9\u03c6\u03c7\u04e9\u1d02\u1d08\u1d09\u1d1d\u207d\u207e\u207a\u207b\u2070\u00b9\u00b2\u00b3\u2074\u2075\u2076" +
"\u2077\u2078\u2079\u207c\u1d43\u1d47\u1d9c\u1da0\u1d4d\u2071\u02b2\u1d4f\u02e1\u1d50\u1d52\u1d56\uFFFF\ua7f4\u1d58\u1d5b\u02b7\u02e3\u02b8\u1dbb\u1d51\uFFFF\ua7f9\u1d44\u1d9b\u1d53\u1d9d\u1d4a\u1d4b\u1d9f\u1da1\u02e0\u1da3\u02b1\u1da4\u1d5a\u1dad\u1dac\u1db1\u02b4\u02b5\u02b6\u1db3\u1db4\u1db6\u1db7\u1dba\uFFFF\uab69\u1dbe\u1d5d\u1d5f\u1dbf\u1da5\u1d60\u1d61\u1d46\u1d4c\u1d4e\u1d59\u0000aeinouv" +
"y\u00e2\u00ea\u00f3\u00f6\u00fa\u0103\u014d\u01a1\u01b0\u00e3\u1ebd\u0129\u00f1\u00f5\u0169\u1e7d\u1ef9\u1e4f\u1eb5\u0000*acehijklmnostuwxy~\u00b0\u00ed\u00f5\u00f9\u00fa\u0101\u014d\u016b\u01d4\u03b9\u03c5\u03cd\u03d2\u0430\u0436\u0437\u0438\u043e\u0443\u0447\u044b\u044d\u04d9\u04e9\u1f7a\u1fe6\u2207\u2363\u00e4\uFFFF\u0063\u0308" +
"\u00eb\u1e27\u00ef\uFFFF\u006a\u0308\uFFFF\u006b\u0308\uFFFF\u006c\u0308\uFFFF\u006d\u0308\uFFFF\u006e\u0308\u00f6\uFFFF\u0073\u0308\u1e97\u00fc\u1e85\u1e8d\u00ff\u2368\u2364\u03ca\u03cb\u03b0\u03d4\u04d3\u04dd\u04df\u04e5\u04e7\u04f1\u04f5\u04f9\u04ed\u04db\u1fe2\u1fe7\u2362\u0000 !\"#%'()*+,-./01234578:;<" +
"=>?ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnoprstuvwxyz{|~\u00a8\u00af\u00b4\u00b8\u00f7\u02d8\u0391\u0395\u0397" +
"\u0399\u039f\u03a5\u03a9\u03b1\u03b5\u03b7\u03b9\u03bf\u03c5\u03c9\u0415\u0417\u041d\u0421\u0430\u0431\u0433\u0434\u0435\u0436\u0437\u0438\u0439\u043a\u043b\u043c\u043d\u043e\u043f\u0442\u0443\u0445\u0447\u0448\u044a\u044e\u044f\u0456\u0458\u0461\u0467\u05b4\u05b7\u05b8\u05b9\u05bc\u05bf\u05c1\u05c2\u0627\u062a\u062c\u062d\u062f\u0633\u0634\u0639\u063a\u0641\u0642\u0644\u0646\u0647\u0648\u064a\u0653\u0654\u0655\u0667\u0668\u06cc" +
"\u06f7\u06f8\u093c\u09bc\u09c7\u0a3c\u0b3c\u0b47\u0bc6\u0bc7\u0bd7\u0c46\u0cbf\u0cc6\u0cca\u0d46\u0d47\u0dd9\u0ddc\u0f71\u0f90\u0f92\u0f9c\u0fa1\u0fa6\u0fab\u0fb2\u0fb3\u0fb5\u0fb7\u102e\u1100\u1102\u1103\u1105\u1106\u1107\u1108\u1109\u110a\u110b\u110c\u110e\u1111\u1112\u1121\u1132\u113c\u113e\u114e\u1150\u1161\u1163\u1165\u1167\u1169\u116a\u116d\u116e\u116f\u1172\u1173\u1174\u1175\u119e\u11a8\u11aa\u11ab\u11ae\u11af\u11b0\u11b1" +
"\u11b2\u11b3\u11b7\u11b8\u11ba\u11bc\u11c1\u11c2\u11ce\u11dd\u11ec\u11f0\u2190\u2191\u2192\u2193\u2203\u2206\u2207\u2208\u220a\u220b\u2218\u2223\u2225\u2227\u2228\u2229\u222a\u223c\u2243\u2248\u224d\u2260\u2261\u2264\u2265\u2272\u2273\u2276\u2277\u227a\u227b\u227c\u227d\u2282\u2283\u2286\u2287\u2291\u2292\u22a3\u22a4\u22a5\u22a8\u22a9\u22ab\u22b2\u22b3\u22b4\u22b5\u22c4\u2373\u2375\u237a\u2395\u25cb\u2add\u0000 (," +
"-.<>_\u00a0\u02d8\u00b8~\u2008\u02c7^\u00af\u0000!+?ABDEHIKLMNORSTUVWYZ^abdehiklmnorstuvwyz\u01a0\u01a1\u01af\u01b0\u00a1\u0000OUou\u1ee2\u1ef0\u203d\u1ea0\u1e04\u1e0c" +
"\u1eb8\u1e24\u1eca\u1e32\u1e36\u1e42\u1e46\u1ecc\u1e5a\u1e62\u1e6c\u1ee4\u1e7e\u1e88\u1ef4\u1e92\u00a6\u0000 \"',<>AEHIOUWXY_aehiotuwxy~\u00af\u00b4\u00d5\u00f5\u016a\u016b\u0399\u03a5\u03b9\u03c5\u03d2\u0406\u0410\u0415\u0416\u0417\u0418\u041e\u0423\u0427\u042b\u042d\u0430\u0435\u0436\u0437\u0438" +
"\u043e\u0443\u0447\u044b\u044d\u0456\u04d8\u04d9\u04e8\u04e9\u00a8\u0344\u201e\u201c\u201d\u00c4\u00cb\u1e26\u00cf\u00d6\u00dc\u1e84\u1e8c\u0178\u0000Uu\u1e7a\u1e7b\u0000Oo\u1e4e\u0000Uu\u03aa\u03ab\u0407\u04d2\u0401\u04dc\u04de\u04e4\u04e6\u04f0\u04f4\u04f8\u04ec\u0451\u0457\u04da\u04ea\u0000#ESbefq\u266f\u266b\u266c\u266d\u266a\u266e\u2669\u0000o\u2030\u0000" +
"\"'()+,/<>ACEGIJKLMNOPRSUWYZ^_abcegijklmnoprsuwyz~\u00af\u00b8\u00c2\u00c5\u00c6\u00c7\u00ca\u00cf\u00d4\u00d5\u00d8\u00dc\u00e2\u00e5\u00e6\u00e7\u00ea\u00ef\u00f4\u00f5\u00f8\u00fc\u0102" +
"\u0103\u0112\u0113\u014c\u014d\u0168\u0169\u01a0\u01a1\u01af\u01b0\u0391\u0395\u0397\u0399\u039f\u03a5\u03a9\u03b1\u03b5\u03b7\u03b9\u03bf\u03c5\u03c9\u0410\u0413\u0415\u0418\u041a\u041e\u0420\u0423\u042b\u042d\u042e\u042f\u0430\u0433\u0435\u0438\u043a\u043e\u0440\u0443\u044b\u044d\u044e\u044f\u2395\u0000 IUiu\u03b9\u03c5\u0385\u1e2e\u01d7\u0390\u00b4\u0000\u0391\u0395\u0397\u0399\u039f\u03a5\u03a9\u03b1" +
"\u03b5\u03b7\u03b9\u03bf\u03c5\u03c9\u1f0d\u1f1d\u1f2d\u1f3d\u1f4d\u1f5d\u1f6d\u1f05\u1f15\u1f25\u1f35\u1f45\u1f55\u1f65\u0000\u0391\u0395\u0397\u0399\u039f\u03a9\u03b1\u03b5\u03b7\u03b9\u03bf\u03c5\u03c9\u1f0c\u1f1c\u1f2c\u1f3c\u1f4c\u1f6c\u1f04\u1f14\u1f24\u1f34\u1f44\u1f54\u1f64\u0000OUou\u1eda\u1ee8\u201a\u0000Oo\u01fe\u2018\u2019\u00c1\u0106\u00c9\u01f4\u00cd\uFFFF\u004a\u0301\u1e30\u0139\u1e3e" +
"\u0143\u00d3\u1e54\u0154\u015a\u00da\u1e82\u00dd\u0179\u0000AEOaeo\u1ea4\u1ebe\u1ed0\u0000EOeo\u1e16\u1e52\u0000Aa\u1eae\u0000OUou\u1e4c\u1e78\u0000EOeo\u0000Cc\u1e08\u01fa\u01fc\u0386\u0388\u0389\u038a\u038c\u038e\u038f\u03ce\uFFFF\u0410\u0301\u0403\uFFFF\u0415\u0301\uFFFF\u0418\u0301\u040c\uFFFF\u041e\u0301\uFFFF\u0420" +
"\u0301\uFFFF\u0423\u0301\uFFFF\u042b\u0301\uFFFF\u042d\u0301\uFFFF\u042e\u0301\uFFFF\u042f\u0301\uFFFF\u0440\u0301\u235e\u0000 ()-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijk" +
"lmnopqrstuvwxyz\u0391\u0395\u0397\u0399\u039f\u03a1\u03a5\u03a9\u03b1\u03b5\u03b7\u03b9\u03bf\u03c1\u03c5\u03c9\u1100\u1102\u1103\u1105\u1106\u1107\u1109\u110b\u110c\u110e\u110f\u1110\u1111\u1112\u30a2\u30a4\u30a6\u30a8\u30aa\u30ab\u30ad\u30af\u30b1\u30b3\u30b5\u30b7\u30b9\u30bb\u30bd\u30bf\u30c1\u30c4\u30c6\u30c8\u30ca\u30cb\u30cc\u30cd\u30ce\u30cf\u30d2" +
"\u30d5\u30d8\u30db\u30de\u30df\u30e0\u30e1\u30e2\u30e4\u30e6\u30e8\u30e9\u30ea\u30eb\u30ec\u30ed\u30ef\u30f0\u30f1\u30f2\u4e00\u4e03\u4e09\u4e0a\u4e0b\u4e2d\u4e5d\u4e8c\u4e94\u4f01\u4f11\u512a\u516b\u516d\u5199\u52b4\u533b\u5341\u5354\u5370\u53f3\u540d\u56db\u571f\u591c\u5973\u5b66\u5b97\u5de6\u65e5\u6708\u6709\u6728\u682a\u6b63\u6c34\u6ce8\u706b\u7279\u7537\u76e3\u793e\u795d\u79d8\u8ca1\u8cc7\u9069\u91d1\u9805[\u0000)" +
"\uFFFF\ud83c\udd2f{\u0000)\u24ea\u0000)0123456789\u2460\u0000)\u2469\u0000)\u246a\u0000)\u246b\u0000)\u246c\u0000)\u246d\u0000)\u246e\u0000)\u246f\u0000)\u2470\u0000)\u2471\u0000)\u2472\u0000)0123456789\u2461\u0000)\u2473\u0000)\u3251\u0000)\u3252" +
"\u0000)\u3253\u0000)\u3254\u0000)\u3255\u0000)\u3256\u0000)\u3257\u0000)\u3258\u0000)\u3259\u0000)0123456789\u2462\u0000)\u325a\u0000)\u325b\u0000)\u325c\u0000)\u325d\u0000)\u325e\u0000)\u325f\u0000)\u32b1\u0000)\u32b2\u0000)\u32b3\u0000)\u32b4\u0000)012345" +
"6789\u2463\u0000)\u32b5\u0000)\u32b6\u0000)\u32b7\u0000)\u32b8\u0000)\u32b9\u0000)\u32ba\u0000)\u32bb\u0000)\u32bc\u0000)\u32bd\u0000)\u32be\u0000)0\u2464\u0000)\u32bf\u0000)\u2465\u0000)\u2466\u0000)\u2467\u0000)\u2468\u0000)\u24b6\u0000)\u24b7\u0000)\u24b8\u0000)\u24b9\u0000)\u24ba\u0000)\u24bb" +
"\u0000)\u24bc\u0000)\u24bd\u0000)\u24be\u0000)\u24bf\u0000)\u24c0\u0000)\u24c1\u0000)\u24c2\u0000)\u24c3\u0000)\u24c4\u0000)\u24c5\u0000)\u24c6\u0000)\u24c7\u0000)\u24c8\u0000)\u24c9\u0000)\u24ca\u0000)\u24cb\u0000)\u24cc\u0000)\u24cd\u0000)\u24ce\u0000)\u24cf\u0000)\u24d0\u0000)\u24d1\u0000)\u24d2\u0000)\u24d3" +
"\u0000)\u24d4\u0000)\u24d5\u0000)\u24d6\u0000)\u24d7\u0000)\u24d8\u0000)\u24d9\u0000)\u24da\u0000)\u24db\u0000)\u24dc\u0000)\u24dd\u0000)\u24de\u0000)\u24df\u0000)\u24e0\u0000)\u24e1\u0000)\u24e2\u0000)\u24e3\u0000)\u24e4\u0000)\u24e5\u0000)\u24e6\u0000)\u24e7\u0000)\u24e8\u0000)\u24e9\u1f09\u1f19\u1f29\u1f39\u1f49\u1fec" +
"\u1f59\u1f69\u1f01\u1f11\u1f21\u1f31\u1f41\u1fe5\u1f51\u1f61\u0000)\u1161\u3260\u0000)\u326e\u0000)\u1161\u3261\u0000)\u326f\u0000)\u1161\u3262\u0000)\u3270\u0000)\u1161\u3263\u0000)\u3271\u0000)\u1161\u3264\u0000)\u3272\u0000)\u1161\u3265\u0000)\u3273\u0000)\u1161\u3266\u0000)\u3274\u0000)\u1161\u3267\u0000)\u3275\u0000)\u1161\u3268\u0000)" +
"\u3276\u0000)\u1161\u3269\u0000)\u3277\u0000)\u1161\u326a\u0000)\u3278\u0000)\u1161\u326b\u0000)\u3279\u0000)\u1161\u326c\u0000)\u327a\u0000)\u1161\u326d\u0000)\u327b\u0000)\u32d0\u0000)\u32d1\u0000)\u32d2\u0000)\u32d3\u0000)\u32d4\u0000)\u32d5\u0000)\u32d6\u0000)\u32d7\u0000)\u32d8\u0000)\u32d9\u0000)\u32da\u0000)\u32db" +
"\u0000)\u32dc\u0000)\u32dd\u0000)\u32de\u0000)\u32df\u0000)\u32e0\u0000)\u32e1\u0000)\u32e2\u0000)\u32e3\u0000)\u32e4\u0000)\u32e5\u0000)\u32e6\u0000)\u32e7\u0000)\u32e8\u0000)\u32e9\u0000)\u32ea\u0000)\u32eb\u0000)\u32ec\u0000)\u32ed\u0000)\u32ee\u0000)\u32ef\u0000)\u32f0\u0000)\u32f1\u0000)\u32f2\u0000)\u32f3" +
"\u0000)\u32f4\u0000)\u32f5\u0000)\u32f6\u0000)\u32f7\u0000)\u32f8\u0000)\u32f9\u0000)\u32fa\u0000)\u32fb\u0000)\u32fc\u0000)\u32fd\u0000)\u32fe\u0000)\u3280\u0000)\u3286\u0000)\u3282\u0000)\u32a4\u0000)\u32a6\u0000)\u32a5\u0000)\u3288\u0000)\u3281\u0000)\u3284\u0000)\u32ad\u0000)\u32a1\u0000)\u329d\u0000)\u3287" +
"\u0000)\u3285\u0000)\u32a2\u0000)\u3298\u0000)\u32a9\u0000)\u3289\u0000)\u32af\u0000)\u329e\u0000)\u32a8\u0000)\u3294\u0000)\u3283\u0000)\u328f\u0000)\u32b0\u0000)\u329b\u0000)\u32ab\u0000)\u32aa\u0000)\u32a7\u0000)\u3290\u0000)\u328a\u0000)\u3292\u0000)\u328d\u0000)\u3291\u0000)\u32a3\u0000)\u328c\u0000)\u329f" +
"\u0000)\u328b\u0000)\u3295\u0000)\u329a\u0000)\u32ac\u0000)\u3293\u0000)\u3297\u0000)\u3299\u0000)\u3296\u0000)\u32ae\u0000)\u329c\u0000)\u328e\u0000)\u32a0\u0000)-\u0391\u0395\u0397\u0399\u039f\u03a9\u03b1\u03b5\u03b7\u03b9\u03bf\u03c1\u03c5\u03c9]}\u1f08\u1f18\u1f28\u1f38\u1f48\u1f68\u1f00\u1f10\u1f20\u1f30\u1f40\u1fe4\u1f50\u1f60\u0000'0" +
"AUau\u00a8\u25cb\u0000Aa\u00c5\u016e\u235f\u0000+-OUou#\u00b1\u01a0\u01af\u0000 \"',-ACDEGHIKLNORSTUacdeghiklnorstu\u0433\u043a\u043b\u043d\u0445\u0467\u00ac\u0104\u00c7\u1e10\u0118\u0122\u1e28" +
"\u012e\u0136\u013b\u0145\u01ea\u0156\u015e\u0162\u0172\u04fb\u04c4\u0513\u04c8\u04fd\u04ca\u0000 ()+,-./:>ADEIKLOU\\^_adeilou\u2191\u2193\u25cb\u0000 -.\u00ad\u2014\u2013\u0000Ee\uFFFF\u0116\u0304\uFFFF\u0117\u0304\u233f\u00f7\u0100\u0110\u0112\u012a\u20ad\u00a3\u014c" +
"\u016a\u2340\u2212\u234f\u2356\u2296\u0000 !'-.:<=>ABCDEFGHIMNOPRSTWXYZ^abcdefghimnoprstwxyz\u00b4\u015a\u015b\u0160\u0161\u017f\u0433\u0436\u0439\u043a\u043b\u043c\u043d\u0445\u0447" +
"\u0456\u1e62\u1e63\u25cb\u02d9\u0000Ss\u1e68\u0000Ss\u1e64\u00b7\u2026\u2235\u2039\u2022\u203a\u0226\u1e02\u010a\u1e0a\u0116\u1e1e\u0120\u1e22\u0130\u1e40\u1e44\u022e\u1e56\u1e58\u1e60\u1e6a\u1e86\u1e8a\u1e8e\u017b\u0000Ss\u1e66\u04f7\u0497\u048b\u049b\u052f\u04ce\u04b3\u04b7\u2299\u0000-/<=BCDGHILOTZ^bcdg" +
"hilmotuvz\u0294\u0413\u041a\u0433\u043a\u04ae\u04af\u2190\u2192\u2194\u2395\\\u2260\u0243\u20a1\u01e4\u0126\u0197\u0141\u00d8\u0166\u01b5|\u00a2\u20a5\u00b5\u221a\u02a1\u0492\u049e\u0493\u049f\u04b0\u219a\u219b\u21ae\u2341\u0000*3~\u2189\u236c\u0000123456789^\u00000\u2152\u00bd\u2153\u00bc\u2155\u2159\u2150" +
"\u215b\u2151\u000035^\u2154\u2156\u0000458^\u00be\u2157\u215c\u00005\u2158\u000068\u215a\u215d\u00008\u215e\u00008\u221e\u0000()-.\u2395\u2639\u263a\u2234\u2360\u0000 AEIOSTU_aeiostu\u02db\u0218\u021a\u236e\u0219\u021b\u0000 \"'-/3<=" +
">CDELNRSTZ_cdelnrstz\u2395\u226e\u2665\u00ab\u2264\u22c4\u010c\u010e\u011a\u013d\u0147\u0158\u0160\u0164\u017d\u2343\u0000/<>CELNOPRTUWY^_cdeopruvy\u0415\u0417\u0421\u0423\u0437\u0443\u2395\u21d0\u21d2\u20ac" +
"\u20a4\u20a6\u0150\u20bd\u20b9\u20ae\u0170\u20a9\u00a5\u21d1\u2261\u20ab\u21d3\u04f2\u2338\u0000 \"'/<=>AEIOU_aeiou\u00a8\u2395\u226f\u2265\u00bb\u00c2\u00ca\u00ce\u00d4\u00db\u2369\u2344\u0000!+?AEIOUY\\^abeiouy\u00c2\u00ca\u00d4\u00e2\u00ea\u00f4\u0102" +
"\u0103\u01a0\u01a1\u01af\u01b0\u2395\u2e18\u0000OUou\u1ede\u1eec\u00bf\u1ea2\u1eba\u1ec8\u1ece\u1ee6\u1ef6\u262d\u0000AEOaeo\u1ea8\u1ec2\u1ed4\u0000Aa\u1eb2\u2370\u0000\"'(*,-;>AET^_`aet~\u00a8\u00b4\u0102\u00c6@\u00c0\u00c3\u0000.|\u0e3f\u0000',./" +
"<=CEOr|\u2102\u20a0\u00a9\u20a2\u20b5\u0000,-.<Hh\u00d0\u0000\"',-.;<=>E^_`e\u00a8\u00b4\u018f\u00c8\u0000.Uilr\uFFFF\ud83d\udd95\uFFFF\ufb03\uFFFF\ufb04\u20a3\u0000(,.TU|\u02d8\u011e>\u20b2\u0000,\u0000\"',-." +
";>J^_`j~\u00a8\u00b4\u0132\u00cc\u0128\u0000'\u00b4\u0000,-\u0000',-/<=LTV\u0000A\u0000P\uFFFF\ud83d\udd96<\u0000.\u0000',<=GNOgno~\u014a\u2115\u2116\u00d1\u0000\"',-/;>ACERSXY^_" +
"`e~\u00a8\u00b4\u0152\u00ae\u00a7\u00a4\u262e\u00d2\u00d5\u0000!.=Pt\u00b6\u20a7\u0000Qq\u211a\u0000',<=ORrs\u211d\u20a8\u0000!',.;<MOSs|\u2120\u1e9e$\u0000,-./;<=HMh\u00de\u2122\u0000 !\"'*,-;" +
">AEGIOU^_`aegiou~\u00a8\u00b4\u00b8\u0391\u0399\u03a5\u03b1\u03b9\u03c5\u0410\u0415\u0416\u0418\u0423\u0430\u0435\u0436\u0438\u0443\u0000,\u0000Ee\u1e1c\u0000Aa\u1eb6\u0114\u012c\u014e\u016c\u00d9\u0103\u0115\u011f\u012d\u014f\u016d\u0168\u0000Ee\u1fb8\u1fd8\u1fe8\u1fb0\u1fd0\u1fe0\u04d0\u04d6\u04c1\u0419\u040e" +
"\u04d1\u04d7\u04c2\u0439\u045e\u0000Lcsz\u0000=^\u0174\u0000O\u0000\"'=^\u00a8\u00b4\u0176\u0000'.<Zz\u2124\u0000]\u2337\u0000-?not\u2395\u25cb\uFFFF\\\u006e\u0000/\uFFFF\ud83d\ude4c\uFFFF\\\u0074\u2342\u2349\u0000[\u0000!()+-./0123456" +
"789=ACEGHIJOSUWYZ_aceghijosuwyz|\u0410\u0415\u0418\u041e\u0420\u0423\u0430\u0435\u0438\u043e\u0440\u0443\u1eb8\u1eb9\u1ecc\u1ecd\u2212\u4e00\u4e01\u4e09\u4e0a\u4e0b\u4e19\u4e2d\u4e59\u4e8c\u4eba\u56db\u5730\u5929\u7532\u0000AEOaeo\u1eac\u1ec6" +
"\u1ed8\u0108\u011c\u0124\u0134\u015c\u1e90\u0000ahijlnorswxy\u0263\u0266\u0279\u027b\u0281\u0295\u02e4\uFFFF\u0410\u0302\uFFFF\u0415\u0302\uFFFF\u0418\u0302\uFFFF\u041e\u0302\uFFFF\u0420\u0302\uFFFF\u0423\u0302\uFFFF\u0440\u0302\u3192\u319c\u3194\u3196\u3198\u319b\u3197\u319a\u3193\u319f\u3195\u319e\u319d\u3199\u0000 !\"'()+-." +
"0123456789;<=>AEGIOUY^_aegiouy~\u00c4\u00c6\u00d5\u00d6\u00dc\u00e4\u00e6\u00f5\u00f6\u00fc\u0226\u0227\u0391\u0399\u03a5\u03b1\u03b9\u03c5\u0410\u0415\u0418\u041e\u0420\u0423\u0430\u0435\u0438\u043e\u0440\u0443\u1e36\u1e37\u1e5a\u1e5b\u2206\u220a\u2212\u2218\u2260\u2282\u2283" +
"\u22a5\u22c4\u2373\u2375\u237a\u25cb\u0000LRlr\u1e38\u1e5c\u0000AOUaou\u01de\u022a\u01d5\u2358\u0000AEOaeo\u01e0\u0230\u0000Oo\u01ec\u1e20\u0232\u0000Oo\u022c\u01e2\u1fb9\u1fd9\u1fe9\uFFFF\u0410\u0304\uFFFF\u0415\u0304\u04e2\uFFFF\u041e\u0304\uFFFF\u0420\u0304\u04ee\uFFFF\u0430\u0304\uFFFF\u0435\u0304\uFFFF\u043e\u0304\uFFFF\u0440" +
"\u0304\u2359\u2377\u235b\u2262\u2286\u2287\u234a\u235a\u2378\u2379\u2376\u235c\u0000\"()+AEINOUWY^_`abeinouwy\u00af\u00c2\u00ca\u00d4\u00dc\u00e2\u00ea\u00f4\u00fc\u0102\u0103\u0112\u0113\u014c\u014d\u01a0\u01a1\u01af\u01b0\u0391\u0395\u0397\u0399\u039f\u03a5\u03a9\u03b1\u03b5\u03b7\u03b9\u03bf\u03c5\u03c9\u0410" +
"\u0415\u0418\u041e\u0420\u0423\u0430\u0435\u0438\u043e\u0440\u0443\u0000Uu\u03b9\u03c5\u01db\u1fd2\u0000\u0391\u0395\u0397\u0399\u039f\u03a5\u03a9\u03b1\u03b5\u03b7\u03b9\u03bf\u03c5\u03c9\u1f0b\u1f1b\u1f2b\u1f3b\u1f4b\u1f5b\u1f6b\u1f03\u1f13\u1f23\u1f33\u1f43\u1f53\u1f63\u0000\u0391\u0395\u0397\u0399\u039f\u03a9\u03b1\u03b5\u03b7\u03b9\u03bf\u03c5\u03c9\u1f0a\u1f1a\u1f2a\u1f3a\u1f4a\u1f6a\u1f02\u1f12\u1f22\u1f32\u1f42" +
"\u1f52\u1f62\u0000OUou\u1edc\u1eea\u01f8\u1e80\u1ef2\u0000AEOaeo\u1ea6\u1ec0\u1ed2\u0000EOeo\u1e14\u1e50\u0000\u0410\u0415\u0418\u041e\u0420\u0423\u0430\u0435\u0438\u043e\u0440\u0443\uFFFF\u0410\u030f\uFFFF\u0415\u030f\uFFFF\u0418\u030f\uFFFF\u041e\u030f\uFFFF\u0420\u030f\uFFFF\u0423\u030f\u0000Aa\u1eb0\u0000EOeo\u1fba\u1fc8\u1fca" +
"\u1fda\u1ff8\u1fea\u1ffa\uFFFF\u0410\u0300\u0400\u040d\uFFFF\u041e\u0300\uFFFF\u0420\u0300\uFFFF\u0423\u0300\uFFFF\u0430\u0300\uFFFF\u043e\u0300\uFFFF\u0440\u0300\uFFFF\u0443\u0300\u0000\"'(*,-;>E^_`ae~\u00a8\u00b4\u00e6\u0000!,.AEGIOUaegiou\u00b8\u0391\u0399\u03a5\u03b1\u03b9\u03c5" +
"\u0410\u0415\u0416\u0418\u0423\u0430\u0435\u0436\u0438\u0443\u0000Aa\u0000Ee\u0000Ee\u0000\"',./<=ACDEGHIKLNORSTUZacdeghijklnorstuz|\u00dc\u00fc\u0000Uu\u01d9\u01cd\u01e6\u021e\u01cf\u01e8" +
"\u01d1\u01d3\u0000,-.<=hi\u00f0\u2300\u0000\"',-.;<=>^_`e\u00a8\u00b4\u0259\u0000.Sfils\u017f\uFFFF\ufb00\uFFFF\ufb01\uFFFF\ufb02\u0000(,.Utu~\u02d8\uFFFF\u0067\u0303\u0000,\u0000\"',-.;>^_`j~\u00a8\u00b4" +
"\u0133\u0000'\u00b4\u0000,k\u0138\u0000',-/<tv\u0000./u\u0000',<g~\u0000~\uFFFF\u006e\u0360\u0067\u0000\"',-/;>AU^_`aceorsuwxy~\u00a8\u00b4\u0153\u0000!.=o\u0000o\uFFFF\ud83d\udca9\u0000'," +
"<=\u0000!',.;<mos\u00b8\u00df\u0000,-./;<hm\u00fe\u0000\"'*,-/;>AEGIOU^_`aegiou~\u00a8\u00b4\u0000/ACDEGHIKNORSTUZacde" +
"ghijklnorstuz|\u0000^\u0000ox\u00d7\u0000\"'=^\u00a8\u00b4\u0000'.<\u0000}\u2205\u0000-=BCGS^cv~\u2190\u2192\u2206\u2207\u222a\u2282\u25cb\u2020\u2021\u236d\u2345\u2346\u234b\u2352\u2366\u2367\u233d\u0000\"()+0AEIN" +
"OUVY^abeinouvy|~\u00a8\u00c2\u00ca\u00d4\u00e2\u00ea\u00f4\u0102\u0103\u01a0\u01a1\u01af\u01b0\u03b1\u03b7\u03b9\u03c5\u03c9\u2207\u2227\u2228\u0000\u03b9\u03c5\u1fd7\u0000\u0391\u0397\u0399\u03a5\u03a9\u03b1\u03b7\u03b9\u03c5\u03c9\u1f0f\u1f2f\u1f3f\u1f5f\u1f6f\u1f07\u1f27\u1f37\u1f57\u1f67\u0000\u0391\u0397\u0399\u03a9\u03b1\u03b7\u03b9\u03c5\u03c9" +
"\u1f0e\u1f2e\u1f3e\u1f6e\u1f06\u1f26\u1f36\u1f56\u1f66\u0000OUou\u1ee0\u1eee\u1ebc\u1e7c\u1ef8\u0000AEOaeo\u1eaa\u1ec4\u1ed6\u0000Aa\u1eb4\u2248\u1fb6\u1fc6\u1fd6\u1fe6\u1ff6\u236b\u2372\u2371\u0000'*>AEIOUY`aeiouy~\u00b4\u2207\u2218\u22a4\u25cb\u1fed\u1fc1\u2361\u2365\u0000!\"" +
".;AEGIOUYaegiouy~\u00c4\u00c6\u00d5\u00d6\u00dc\u00e4\u00e6\u00f5\u00f6\u00fc\u0226\u0227\u0391\u0399\u03a5\u03b1\u03b9\u03c5\u0410\u0415\u0418\u041e\u0420\u0423\u0430\u0435\u0438\u043e\u0440\u0443\u1e36\u1e37\u1e5a\u1e5b\u22a4\u0000LRlr\u0000AOUaou\u0000AOao\u0000Oo" +
"\u0000Oo\u2351\u0000\"()+,/ACEGIJKLMNOPRSUWYZ^_abcegijklmnoprsuwyz~\u00af\u00b8\u00c2\u00c5\u00c6\u00c7\u00ca\u00cf\u00d4\u00d5\u00d8\u00dc\u00e2\u00e5\u00e6\u00e7\u00ea\u00ef\u00f4\u00f5\u00f8" +
"\u00fc\u0102\u0103\u0112\u0113\u014c\u014d\u0168\u0169\u01a0\u01a1\u01af\u01b0\u0391\u0395\u0397\u0399\u039f\u03a5\u03a9\u03b1\u03b5\u03b7\u03b9\u03bf\u03c5\u03c9\u0410\u0413\u0415\u0418\u041a\u041e\u0420\u0423\u042b\u042d\u042e\u042f\u0430\u0433\u0435\u0438\u043a\u043e\u0440\u0443\u044b\u044d\u044e\u044f\u0000IUiu\u03b9\u03c5\u0000\u0391\u0395\u0397\u0399\u039f\u03a5\u03a9\u03b1\u03b5\u03b7\u03b9\u03bf\u03c5" +
"\u03c9\u0000\u0391\u0395\u0397\u0399\u039f\u03a9\u03b1\u03b5\u03b7\u03b9\u03bf\u03c5\u03c9\u0000OUou\u0000Cc\u0000Oo\u0000AEOaeo\u0000EOeo\u0000Aa\u0000OUou\u0000EOeo\u0000Cc\u0000CDEGHKLNRSTcdeghk" +
"lnrst\u0228\u0000\u2395\u2339\u0000Gg\u0000'\u0000'\u0000'\u0000\"'\u0000'\u0000\"'\u0000'\u0000'\u0000'\u0000'\u0000\"'()`~\u00b4\u0391\u0397\u03a9\u03b1\u03b7\u03c9\u0000()\u03b1\u03b7\u03c9\u0000\u0391\u0397\u03a9\u03b1\u03b7\u03c9\u1f8d\u1f9d\u1fad\u1f85\u1f95\u1fa5\u0000\u0391\u0397\u03a9\u03b1" +
"\u03b7\u03c9\u1f8c\u1f9c\u1fac\u1f84\u1f94\u1fa4\u1fb4\u1fc4\u1ff4\u0000\u0391\u0397\u03a9\u03b1\u03b7\u03c9\u1f89\u1f99\u1fa9\u1f81\u1f91\u1fa1\u0000\u0391\u0397\u03a9\u03b1\u03b7\u03c9\u1f88\u1f98\u1fa8\u1f80\u1f90\u1fa0\u0000()\u03b1\u03b7\u03c9\u0000\u0391\u0397\u03a9\u03b1\u03b7\u03c9\u1f8b\u1f9b\u1fab\u1f83\u1f93\u1fa3\u0000\u0391\u0397\u03a9\u03b1\u03b7\u03c9\u1f8a\u1f9a\u1faa\u1f82\u1f92\u1fa2\u1fb2\u1fc2\u1ff2" +
"\u0000()\u03b1\u03b7\u03c9\u0000\u0391\u0397\u03a9\u03b1\u03b7\u03c9\u1f8f\u1f9f\u1faf\u1f87\u1f97\u1fa7\u0000\u0391\u0397\u03a9\u03b1\u03b7\u03c9\u1f8e\u1f9e\u1fae\u1f86\u1f96\u1fa6\u1fb7\u1fc7\u1ff7\u0000()\u03b1\u03b7\u03c9\u0000\u0391\u0397\u03a9\u03b1\u03b7\u03c9\u0000\u0391\u0397\u03a9\u03b1\u03b7\u03c9\u1fbc\u1fcc\u1ffc\u1fb3\u1fc3\u1ff3\u0000'\u0000\"'\u0000'\u0000=\u0000=" +
"\u0000\u041e\u043e\u0000=\u0000\u0435\u0443\u0447\u044b\u044c\ua64b\ua651\u0463\u0461\uFFFF\u0063\u006f\u006d\u0062\u0069\u006e\u0069\u006e\u0067\u005f\u0061\u0069\u0067\u0075\uFFFF\ua659\uFFFF\ua64d\u0000\u0447\uFFFF\u0063\u006f\u006d\u0062\u0069\u006e\u0069\u006e\u0067\u005f\u0073\u006c\u0061\u0076\u006f\u006e\u0069\u0063\u005f\u0070\u0073\u0069\u006c\u0069\u0000,.\u0439\u043a\u0445\u0458\u0491\u0481\u0000\u0435" +
"\u0436\u0437\u0439\u0458\u0465\uFFFF\ua649\u045f\uFFFF\ua643\u0452\u0000\u0447\uFFFF\u0063\u006f\u006d\u0062\u0069\u006e\u0069\u006e\u0067\u005f\u0074\u0072\u0065\u006d\u0061\u0000.\u0000=\u0444\u0499\u0000\u0438\u0443\u0475\u0000.\u0447\uFFFF\u0063\u006f\u006d\u0062\u0069\u006e\u0069\u006e\u0067\u005f\u0062\u0072\u0065\u0076\u0065\u0000,.\u0433\u0441\u0445\u0448\u046f\u0000,.\u044c\u0459\u0000" +
".\u0000,\u00b7\u0447\u044c\u0529\uFFFF\u0063\u006f\u006d\u0062\u0069\u006e\u0069\u006e\u0067\u005f\u0074\u0069\u0074\u006c\u006f\u045a\u0000\u0442\u0443\u0447\u047f\u0479\uFFFF\u0063\u006f\u006d\u0062\u0069\u006e\u0069\u006e\u0067\u005f\u0069\u006e\u0076\u0065\u0072\u0074\u0065\u0064\u005f\u0062\u0072\u0065\u0076\u0065\u0000\u0430\u0441\u0471\u0000\u0439\u0444\u0458\u045b\u0473\u0000\u0438\u0439\u0443\u0447\u0456\u0458" +
"\uFFFF\u0063\u006f\u006d\u0062\u0069\u006e\u0069\u006e\u0067\u005f\u0070\u006f\u006b\u0072\u0079\u0074\u0069\u0065\u0000,.\u0447\uFFFF\u0063\u006f\u006d\u0062\u0069\u006e\u0069\u006e\u0067\u005f\u0073\u006c\u0061\u0076\u006f\u006e\u0069\u0063\u005f\u0064\u0061\u0073\u0069\u0061\u0000.\u0430\u0431\u0435\u0439\u043d\u043e\u0443\u0445\u0447\u044a\u044e\u0458\u0467\uFFFF\u0063\u006f\u006d\u0062\u0069\u006e\u0069\u006e" +
"\u0067\u005f\u0070\u0061\u0079\u0065\u0072\u006f\u006b\uFFFF\u0063\u006f\u006d\u0062\u0069\u006e\u0069\u006e\u0067\u005f\u0076\u0065\u0072\u0074\u0069\u0063\u0061\u006c\u005f\u0074\u0069\u006c\u0064\u0065\uFFFF\u0063\u006f\u006d\u0062\u0069\u006e\u0069\u006e\u0067\u005f\u0067\u0072\u0061\u0076\u0065\uFFFF\u0063\u006f\u006d\u0062\u0069\u006e\u0069\u006e\u0067\u005f\u0076\u007a\u006d\u0065\u0074\u0000\u0442\u0449\u0000\u0447\u0000" +
"\u0430\u0435\u043c\u043d\u0447\uFFFF\ua653\u046d\u0469\u0000\u044c\uFFFF\ua65d\u0000\".\u0443\u0456\u0000\u0430\u0447\u0463\uFFFF\ua657\u0000\u0442\u0000\u0447\u0000\u05d9\uFFFF\ufb1d\u0000\u05d0\u05f2\uFFFF\ufb2e\uFFFF\ufb1f\u0000\u05d0\uFFFF\ufb2f\u0000\u05d5\uFFFF\ufb4b\u0000\u05d0\u05d1\u05d2\u05d3\u05d4\u05d5\u05d6\u05d8\u05d9\u05da\u05db\u05dc\u05de\u05e0\u05e1\u05e3\u05e4\u05e6\u05e7\u05e8\u05e9\u05ea\uFFFF\ufb30" +
"\uFFFF\ufb31\uFFFF\ufb32\uFFFF\ufb33\uFFFF\ufb34\uFFFF\ufb35\uFFFF\ufb36\uFFFF\ufb38\uFFFF\ufb39\uFFFF\ufb3a\uFFFF\ufb3b\uFFFF\ufb3c\uFFFF\ufb3e\uFFFF\ufb40\uFFFF\ufb41\uFFFF\ufb43\uFFFF\ufb44\uFFFF\ufb46\uFFFF\ufb47\uFFFF\ufb48\uFFFF\ufb49\uFFFF\ufb4a\u0000\u05d1\u05db\u05e4\uFFFF\ufb4c\uFFFF\ufb4d\uFFFF\ufb4e\u0000\u05bc\u05e9\ufb49\u0000\u05e9\uFFFF\ufb2c\uFFFF\ufb2a\u0000\u05bc\u05e9\ufb49\u0000\u05e9\uFFFF\ufb2d\uFFFF\ufb2b" +
"\u0000\u0627\u0639\u0648\u064a\u0667\u06cc\u06f7\uFFFF\u0063\u006f\u006d\u0062\u0069\u006e\u0069\u006e\u0067\u005f\u0061\u006c\u0065\u0066\u005f\u0061\u0062\u006f\u0076\u0065\u0623\u06c9\u063d\uFFFF\u0063\u006f\u006d\u0062\u0069\u006e\u0069\u006e\u0067\u005f\u0061\u006c\u0065\u0066\u005f\u0062\u0065\u006c\u006f\u0077\u0000\u062f\u0631\u0634\u0646\u0637\u0691\u062b\u0679\u0000\u0634\u0686\u0000\u062d\uFFFF\u0063\u006f\u006d\u0062" +
"\u0069\u006e\u0069\u006e\u0067\u005f\u0073\u0075\u006b\u0075\u006e\u0000\u062a\u0632\u06f7\u0630\u0695\u0000\u0634\u0635\u0000\u062a\u0000\u0627\u0647\u0648\u064a\u0667\u0668\u06cc\u06f7\u06f8\u0625\u06c0\u0624\u0626\uFFFF\u0063\u006f\u006d\u0062\u0069\u006e\u0069\u006e\u0067\u005f\u0068\u0061\u006d\u007a\u0061\u005f\u0062\u0065\u006c\u006f\u0077\uFFFF\u0063\u006f\u006d\u0062\u0069\u006e\u0069\u006e\u0067\u005f\u0068\u0061\u006d" +
"\u007a\u0061\u005f\u0061\u0062\u006f\u0076\u0065\u0000\u0643\u06a9\u06af\u0000\u0648\u06a1\u0000\u0648\u06ca\u0000\u0644\u0667\u06f7\uFFFF\u0063\u006f\u006d\u0062\u0069\u006e\u0069\u006e\u0067\u005f\u0073\u0068\u0061\u0064\u0064\u0061\u0068\u06b5\u0000\u062a\u0647\u0648\u064a\u06cc\uFFFF\u0063\u006f\u006d\u0062\u0069\u006e\u0069\u006e\u0067\u005f\u0066\u0061\u0074\u0068\u0061\u0074\u0061\u006e\uFFFF\u0063\u006f\u006d\u0062\u0069" +
"\u006e\u0069\u006e\u0067\u005f\u0064\u0061\u006d\u006d\u0061\u0074\u0061\u006e\uFFFF\u0063\u006f\u006d\u0062\u0069\u006e\u0069\u006e\u0067\u005f\u006b\u0061\u0073\u0072\u0061\u0074\u0061\u006e\u0000 \u062a\u0639\u0646\u0647\u0648\u064a\u06cc\u06d5\u0629\uFFFF\u0063\u006f\u006d\u0062\u0069\u006e\u0069\u006e\u0067\u005f\u0066\u0061\u0074\u0068\u0061\u06c6\u06ce\u0000\u062b\u0639\u0641\u0646\u0648\u0667\u0668\u06f7\u06f8\u06cb" +
"\uFFFF\u0063\u006f\u006d\u0062\u0069\u006e\u0069\u006e\u0067\u005f\u0064\u0061\u006d\u006d\u0061\u0068\u0000 \u0627\u0639\u0646\u064a\u0667\u0668\u06f7\u06f8\u06d2\u0649\uFFFF\u0063\u006f\u006d\u0062\u0069\u006e\u0069\u006e\u0067\u005f\u006b\u0061\u0073\u0072\u0061\u0000\u0627\u0622\u0000\u0627\u0648\u064a\u06c1\u06d2\u06d5\u06c2\u06d3\u0000\u0627\u0000\u0627\u0631\u0639\u0644\u0648\u064a\u0667\u06cc\uFFFF\u0063\u006f\u006d" +
"\u0062\u0069\u006e\u0069\u006e\u0067\u005f\u0061\u0072\u0061\u0062\u0069\u0063\u005f\u0076\u0000\u0639\u0648\u064a\u0668\u06cc\uFFFF\u0063\u006f\u006d\u0062\u0069\u006e\u0069\u006e\u0067\u005f\u0061\u0072\u0061\u0062\u0069\u0063\u005f\u0069\u006e\u0076\u0065\u0072\u0074\u0065\u0064\u005f\u0076\u0000 \u0627\u0639\u0646\u0667\u0668\u06cc\u06f7\u06f8\u0000\u0627\u0631\u0639\u0644\u0648\u064a\u06cc\u06f7\u0000\u0639\u0648\u064a" +
"\u06cc\u06f8\u0000\u0915\u0916\u0917\u091c\u0921\u0922\u0928\u092b\u092f\u0930\u0933\u0958\u0959\u095a\u095b\u095c\u095d\u0929\u095e\u095f\u0931\u0934\u0000\u09a1\u09a2\u09af\u09dc\u09dd\u09df\u0000\u09be\u09d7\u09cb\u09cc\u0000\u0a16\u0a17\u0a1c\u0a2b\u0a32\u0a38\u0a59\u0a5a\u0a5b\u0a5e\u0a33\u0a36\u0000\u0b21\u0b22\u0b5c\u0b5d\u0000\u0b3e\u0b56\u0b57\u0b4b\u0b48\u0b4c\u0000\u0bbe\u0bd7\u0bca\u0bcc\u0000\u0bbe\u0bcb\u0000\u0b92" +
"\u0b94\u0000\u0c56\u0c48\u0000\u0cd5\u0cc0\u0000\u0cc2\u0cd5\u0cd6\u0cca\u0cc7\u0cc8\u0000\u0cd5\u0ccb\u0000\u0d3e\u0d57\u0d4a\u0d4c\u0000\u0d3e\u0d4b\u0000\u0dca\u0dcf\u0ddf\u0dda\u0ddc\u0dde\u0000\u0dca\u0ddd\u0000\u0f72\u0f74\u0f80\u0f73\u0f75\u0f81\u0000\u0fb5\u0fb9\u0000\u0fb7\u0f93\u0000\u0fb7\u0f9d\u0000\u0fb7\u0fa2\u0000\u0fb7\u0fa7\u0000\u0fb7\u0fac\u0000\u0f80\u0f76\u0000\u0f80\u0f78\u0000\u0f40\u0f69\u0000\u0f42\u0f4c" +
"\u0f51\u0f56\u0f5b\u0f43\u0f4d\u0f52\u0f57\u0f5c\u0000\u1025\u1026\u0000\u1100\u1101\u0000\u1100\u1102\u1103\u1107\u1113\u1114\u1115\u1116\u0000\u1100\u1103\u1117\u1104\u0000\u1102\u1105\u110b\u1112\u1118\u1119\u111b\u111a\u0000\u1107\u110b\u111c\u111d\u0000\u1100\u1102\u1103\u1107\u1109\u110a\u110b\u110c\u110e\u1110\u1111\u112b\u112d\u112f\u1132\u1136\u111e\u111f\u1120\u1108\u1121\u1125\u112b\u1127\u1128\u1129\u112a\u112c\u1122" +
"\u1123\u1124\u1126\u0000\u110b\u0000\u1100\u1102\u1103\u1105\u1106\u1107\u1109\u110a\u110b\u110c\u110e\u110f\u1110\u1111\u1112\u111e\u112d\u112e\u112f\u1130\u1131\u1132\u110a\u1134\u1135\u1136\u1137\u1138\u1139\u113a\u113b\u1133\u0000\u1109\u0000\u1100\u1103\u1106\u1107\u1109\u110b\u110c\u110e\u1110\u1111\u1140\u1141\u1142\u1143\u1144\u1145\u1147\u1148\u1149\u114a\u114b\u1146\u0000\u110b\u110c\u114d\u110d\u0000\u110f\u1112\u1152" +
"\u1153\u0000\u1107\u110b\u1156\u1157\u0000\u1112\u1158\u0000\u1100\u1103\u1107\u1109\u110c\u0000\u1100\u0000\u113c\u113d\u0000\u113e\u113f\u0000\u114e\u114f\u0000\u1150\u1151\u0000\u1169\u116e\u1175\u1176\u1177\u1162\u0000\u1169\u116d\u1175\u1178\u1179\u1164\u0000\u1169\u116e\u1173\u1175\u117a\u117b\u117c\u1166\u0000\u1169\u116e\u1175\u117d\u117e\u1168\u0000\u1161\u1162\u1165\u1166\u1168\u1169\u116e\u1175\u116a\u116b\u117f\u1180" +
"\u1181\u1182\u1183\u116c\u0000\u1175\u0000\u1163\u1164\u1167\u1169\u1175\u1184\u1185\u1186\u1187\u1188\u0000\u1161\u1162\u1165\u1166\u1168\u116e\u1175\u117c\u1189\u118a\u116f\u1170\u118c\u118d\u1171\u118b\u0000\u1173\u1175\u0000\u1161\u1165\u1166\u1167\u1168\u116e\u1175\u118e\u118f\u1190\u1191\u1192\u1193\u1194\u0000\u116e\u1173\u1175\u1195\u1196\u1174\u0000\u116e\u1197\u0000\u1161\u1163\u1169\u116e\u1173\u119e\u1198\u1199\u119a" +
"\u119b\u119c\u119d\u0000\u1165\u116e\u1175\u119e\u119f\u11a0\u11a1\u11a2\u0000\u11a8\u11af\u11ba\u11e7\u11a9\u11c3\u11aa\u11c4\u0000\u11a8\u0000\u11a8\u11ae\u11ba\u11bd\u11c0\u11c2\u11eb\u11c5\u11c6\u11c7\u11ac\u11c9\u11ad\u11c8\u0000\u11a8\u11af\u11ca\u11cb\u0000\u11a8\u11aa\u11ab\u11ae\u11af\u11b7\u11b8\u11b9\u11ba\u11bb\u11bf\u11c0\u11c1\u11c2\u11da\u11dd\u11e5\u11e6\u11eb\u11f9\u11b0\u11cc\u11cd\u11ce\u11d0\u11b1\u11b2\u11d3" +
"\u11b3\u11d6\u11d8\u11b4\u11b5\u11b6\u11d1\u11d2\u11d4\u11d5\u11d7\u11d9\u0000\u11ba\u0000\u11a8\u11ba\u0000\u11ba\u11bc\u11c2\u0000\u11ba\u0000\u11a8\u11af\u11b8\u11ba\u11bb\u11bc\u11be\u11c2\u11eb\u11da\u11db\u11dc\u11dd\u11de\u11e2\u11e0\u11e1\u11df\u0000\u11af\u11ba\u11bc\u11c1\u11c2\u11e3\u11b9\u11e6\u11e4\u11e5\u0000\u11a8\u11ae\u11af\u11b8\u11ba\u11e7\u11e8\u11e9\u11ea\u11bb\u0000\u11a8\u11a9\u11bc\u11bf\u11ec\u11ed\u11ee" +
"\u11ef\u0000\u11b8\u11bc\u11f3\u11f4\u0000\u11ab\u11af\u11b7\u11b8\u11f5\u11f6\u11f7\u11f8\u0000\u11c2\u11cf\u0000\u11ba\u0000\u11a8\u0000\u11ba\u11eb\u11f1\u11f2\u0000|\u2395\u2347\u0000-\u2395\u2350\u0000|\u2395\u2348\u0000-\u2395\u2357\u0000/\u2204\u0000_|\u2395\u234d\u0000|~\u00a8\u2395\u2354\u0000/\u2209\u0000_\u0000/\u220c\u0000_\u00a8\u2229\u22a4\u22a5\u2395" +
"\u25cb\u235d\u2355\u234e\u233b\u233e\u0000/\u2224\u0000/\u2226\u0000~\u2228\u2395\u2353\u0000~\u2227\u2395\u234c\u0000\u2218\u0000|\u0000/\u2241\u0000/\u2244\u0000/\u2249\u0000/\u226d\u0000_\u2395\u236f\u0000/\u0000/\u2270\u0000/\u2271\u0000/\u2274\u0000/\u2275\u0000/\u2278\u0000/\u2279\u0000/\u2280\u0000/\u2281\u0000/\u22e0\u0000" +
"/\u22e1\u0000/_|\u2284\u0000/_\u2285\u0000/\u2288\u0000/\u2289\u0000/\u22e2\u0000/\u22e3\u0000/\u22ac\u0000\u00a8\u00af\u2218\u22a5\u2336\u0000_\u2218\u22a4\u0000/\u22ad\u0000/\u22ae\u0000/\u22af\u0000/\u22ea\u0000/\u22eb\u0000/\u22ec\u0000/\u22ed\u0000_\u2395\u233a\u0000_\u0000_\u0000_\u0000'/:<" +
"=>?\\\u00f7\u2190\u2191\u2192\u2193\u2206\u2207\u2218\u2227\u2228\u2260\u22c4\u25cb\u233c\u0000*-.\\_|\u00a8\u2218\u2395\u0000/\u2adc\u0000 !\"%'()*,-.0123456789:<=>?[]^_abcehlopru" +
"yz{|}\u00a3\u00a7\u00b1\u00d7\u0398\u03a0\u03a3\u03b2\u03b3\u03b5\u03b8\u03ba\u03c0\u03c1\u03c3\u03c5\u03c6\u0430\u0435\u0437\u0438\u0439\u043b\u043c\u043d\u043e\u0441\u0443\u0447\u0448\u044a\u044b\u044c\u044d\u044f\u0456\u0458\u045f\u0461\u0481\u0487\u049b\u04b7\u04c8\u0513\u05d1\u05d3\u05d5\u05d6\u05d7\u05dd\u05e1\u05e3\u05e4\u05e6\u05e7\u05e8\u05e9\u0625\u0626\u0627\u0628\u0629\u062b\u062d\u0631\u0632" +
"\u0633\u0635\u0637\u0639\u063a\u063d\u0641\u0642\u0643\u0644\u0646\u0647\u064a\u064f\u0650\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\u06a1\u06a9\u06c6\u06c9\u06cc\u06ce\u06f0\u06f1\u06f2\u06f3\u06f4\u06f5\u06f6\u06f7\u06f8\u06f9\u0901\u0902\u0905\u0906\u0907\u0908\u0909\u090a\u090b\u090c\u090f\u0910\u0913\u0914\u0915\u0916\u0917\u0918\u091a\u091b\u091c\u091d\u091f\u0921\u0922\u0923\u0925\u0926\u0928\u092a\u092b" +
"\u092c\u092f\u0930\u0933\u0935\u0936\u0937\u093c\u093d\u093e\u093f\u0940\u0941\u0943\u0947\u0948\u094b\u094c\u0953\u0956\u0962\u0964\u0970\u0b92\u0b9a\u0baf\u0bb3\u0bb5\u2020\u20ac\u20b9\u2190\u2191\u2192\u2193\u2194\u2195\u2196\u2197\u2198\u2199\u2203\u2206\u2207\u2227\u2228\u2229\u222a\u222b\u2282\u2283\u22a4\u22b7\u22c4\u235d\u2375\u237a\u2395\ua649\ua651\ua67d\uFFFF\u006e\u0062\u0073\u0070\uFFFF\u0066\u0031\u0030\uFFFF\u0066" +
"\u0031\uFFFF\u0066\u0032\uFFFF\u0066\u0033\uFFFF\u0066\u0034\uFFFF\u0066\u0035\uFFFF\u0066\u0036\uFFFF\u0066\u0037\uFFFF\u0066\u0038\uFFFF\u0066\u0039\uFFFF\u007a\u0077\u006e\u006a\u20b1\u20b4\u20bf\uFFFF\u0072\u0065\u006d\u006f\u0076\u0065\u0064\u2213\u2219\u03f4\u220f\u2211\u03d0\u0263\u03f5\u03d1\u03f0\u03d6\u03f1\u03c2\u03d2\u03d5\u0465\uFFFF\ua641\u0456\u0458\u046b\u0467\u047b\u0455\uFFFF\ua64b\u044c\uFFFF\ua651\u044a\u0454" +
"\u0438\uFFFF\ua66f\uFFFF\u0068\u0061\u0074\u0061\u0066\u005f\u0073\u0065\u0067\u006f\u006c\uFFFF\u0064\u0061\u0067\u0065\u0073\u0068\uFFFF\u0068\u006f\u006c\u0061\u006d\uFFFF\u0071\u0075\u0062\u0075\u0074\u0073\uFFFF\u0068\u0069\u0072\u0069\u0071\uFFFF\u0072\u0061\u0066\u0065\uFFFF\u0073\u0065\u0067\u006f\u006c\uFFFF\u0068\u0061\u0074\u0061\u0066\u005f\u0070\u0061\u0074\u0061\u0068\uFFFF\u0070\u0061\u0074\u0061\u0068\uFFFF\u0074" +
"\u0073\u0065\u0072\u0065\uFFFF\u0071\u0061\u006d\u0061\u0074\u0073\uFFFF\u0068\u0061\u0074\u0061\u0066\u005f\u0071\u0061\u006d\u0061\u0074\u0073\uFFFF\u0073\u0068\u0065\u0076\u0061\u066e\u067e\u06c1\u0698\u0640\u0636\u0638\u0621\u06a4\u063a\u06a9\u06ba\u06be\u06cc\u0643\u064a\u0900\u0955\u0972\u0911\u0973\u0974\u0976\u0977\u0960\u0961\u090d\u090e\u0912\u0975\u097b\u097c\uFFFF\u0936\u094d\u091a\u0979\u0978\u097e\uFFFF\u0924\u094d" +
"\u0930\uFFFF\u0926\u094d\u0930\uFFFF\u092a\u094d\u0930\u097f\u097a\uFFFF\u0936\u094d\u0930\uFFFF\u0915\u094d\u0937\u094e\u097d\u0949\u093a\u093b\u0904\u0944\u0945\u0946\u094a\u094f\u0954\u0957\u0963\u0965\u0971\uFFFF\u0bd0\uFFFF\u0bf2\uFFFF\u0bf0\uFFFF\u0bf1\uFFFF\u0bf3\u21d4\u21d5\u21d6\u21d7\u21d8\u21d9\u22c0\u22c1\u22c2\u22c3\u222e\u22b6\u044b\u0483\u00000123456789\u09e6\u09e7" +
"\u09e8\u09e9\u09ea\u09eb\u09ec\u09ed\u09ee\u09ef\u00000123456789\u0966\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f\u00000123456789\u0ae6\u0ae7\u0ae8\u0ae9\u0aea\u0aeb\u0aec\u0aed\u0aee\u0aef\u00000123456789\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\u0000" +
"0123456789\u0ce6\u0ce7\u0ce8\u0ce9\u0cea\u0ceb\u0cec\u0ced\u0cee\u0cef\u00000123456789\u06f0\u06f1\u06f2\u06f3\u06f4\u06f5\u06f6\u06f7\u06f8\u06f9\u00000123456789\u0be6\u0be7\u0be8\u0be9\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef\u0000\u00df\u0131\u01f0\u0237\u02b0\u02b2\u02b3\u02b7\u02e1" +
"\u0905\u0907\u0909\u090b\u090c\u090f\u0913\u0915\u0917\u091a\u091c\u091f\u0921\u0924\u0926\u0928\u092c\u092e\u0932\u0938\u0939\u093f\u0941\u0943\u0945\u0947\u0949\u094b\u0952\u0962\u0a85\u0a87\u0a89\u0a8f\u0a93\u0a95\u0a97\u0a9a\u0a9c\u0a9f\u0aa1\u0aa4\u0aa6\u0aa8\u0aaa\u0aac\u0aae\u0ab2\u0ab8\u0ab9\u0abf\u0ac1\u0ac7\u0acb\u0bf9\u1d43\u1d47\u1d48\u1d49\u1d4d\u1d4f\u1d50\u1d52\u1d56\u1d57\u1d58\u1d5b\u1d60\u1d9c\u1da0\u1dbe\u1e97" +
"\u1e98\u1e99\u2071\u207f\u20b9\u2190\u2191\u2192\u2193\u2196\u2197\u2198\u2199\u2208\u220b\u2282\u2283\u2286\u2287\u2500\u2502\u250c\u2510\u2514\u2518\u251c\u2524\u252c\u2534\u253c\ud835\uFFFF\u004a\u030c\uFFFF\u004a\u0307\u1d34\u1d36\u1d3f\u1d42\u1d38\u0906\u0908\u090a\u0910\u0914\u0916\u0918\u091b\u091d\u0920\u0922\u0925\u0927\u0923\u092d\u0902\u0933\u0936\u0903\u0940\u0942\u0948\u094c\u0951\u0a86\u0a88\u0a8a\u0a90\u0a94\u0a96" +
"\u0a98\u0a9b\u0a9d\u0aa0\u0aa2\u0aa5\u0aa7\u0aa3\u0aab\u0aad\u0a82\u0ab3\u0ab6\u0a83\u0ac0\u0ac2\u0ac8\u0acc\u1d2c\u1d2e\u1d30\u1d31\u1d33\u1d37\u1d39\u1d3c\u1d3e\u1d40\u1d41\u2c7d\u1db2\uFFFF\ua7f2\uFFFF\ua7f3\u1d23\uFFFF\u0054\u0308\uFFFF\u0057\u030a\uFFFF\u0059\u030a\u1d35\u1d3a\u2550\u2551\u2554\u2557\u255a\u255d\u2560\u2563\u2566\u2569\u256c\u0000\udd57\udd58\udd64\udd68\udd69\uFFFF\ud835\udd3d\uFFFF\ud835\udd3e\uFFFF\ud835" +
"\udd4a\uFFFF\ud835\udd4e\uFFFF\ud835\udd4f").toCharArray();
public static final char[] edges =
("\u0001\u0036\u0037\u0038\u0039\u003a\u003b\u003c\u003f\u0040\u0041\u0042\u0043\u0044\u0045\u0046\u0047\u0048\u0049\u004a\u004b\u004c\u004d\u004e\u004f\u0050\u0051\u0052\u0053\u0054\u0055\u0056\u0059\u005a\u005b\\\u005d\u005e\u005f\u0060\u0061\u0062\u0063\u0064\u0067\u0068\u006b\u006e\u006f\u0072\u0075\u0078\u007b\u007e\u0081\u0001\u0001\u0001\u0001\u0001\u0003\u0000\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001" +
"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0003\u0000\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0003\u0000\u0000\u0001\u0003\u0000\u0000\u0003\u0000\u0000\u0001\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0001\u000c\u008e\u008f\u0090\u0091\u0092\u0093\u0094\u0095\u0096\u0097\u0098\u0001\u0001" +
"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u001a\u00b3\u00b4\u00b5\u00b7\u00b8\u00ba\u00bb\u00bc\u00bd\u00be\u00c0\u00c1\u00c2\u00c3\u00c5\u00c6\u00c7\u00c8\u00c9\u00ca\u00cb\u00cc\u00cd\u00ce\u00cf\u0001\u0001\u0002\u0000\u0001\u0002\u0000\u0001\u0001\u0001\u0001\u0002\u0000\u0001\u0001\u0001\u0002\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u000c\u00dc\u00dd\u00de\u00df\u00e0\u00e1\u00e2" +
"\u00e3\u00e4\u00e5\u00e6\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u001d\u0104\u0105\u0106\u0107\u0108\u0109\u010a\u010b\u010c\u010d\u010e\u010f\u0110\u0111\u0112\u0113\u0114\u0115\u0116\u0117\u011a\u011d\u0120\u0123\u0126\u0129\u012c\u012f\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000" +
"\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0001\u000e\u013e\u013f\u0140\u0141\u0142\u0143\u0144\u0145\u0146\u0147\u0148\u004e\u0149\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0025\u016f\u0170\u0171\u0172\u0173\u0174\u0175\u0176\u0177\u0178\u0179\u017a\u017d\u017e\u017f\u004b\u0180\u0181\u004f\u0182\u0051\u0183\u0184\u0185\u0186\u0189\u018c\u018f\u0192" +
"\u0195\u0196\u0197\u0198\u0199\u019a\u019b\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0003\u0000\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0033\u01cf\u01d2\u01d5\u01d8\u01db\u01de\u01e1\u01e4\u01e7\u01ea\u01ed\u01ee\u01ef\u01f0\u01f1\u01f2\u01f3\u01f4\u01f5" +
"\u01f6\u01f7\u01fa\u01fd\u01fe\u01ff\u0200\u0201\u0204\u0205\u0206\u0207\u020a\u020d\u020e\u020f\u0210\u0211\u0212\u0213\u0081\u012f\u0214\u0215\u0216\u0217\u0218\u0219\u021a\u021b\u021c\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0003" +
"\u0000\u0000\u0003\u0000\u0000\u0001\u0001\u0001\u0001\u0003\u0000\u0000\u0001\u0001\u0001\u0003\u0000\u0000\u0003\u0000\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u001e\u023b\u023c\u023d\u023e\u023f\u0240\u0241\u0242\u0243\u0244\u0245\u0246\u0247\u0248\u0249\u024a\u024b\u024c\u024d\u024e\u024f\u0250\u0195\u0197\u019a\u0251\u0252\u0253\u0215\u0001\u0001\u0001\u0001\u0001" +
"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\n\u025e\u025f\u0262\u0265\u0268\u026b\u026c\u026d\u0270\u0001\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0001\u0001\u0003\u0000\u0000\u0001\u000e\u027f\u0280\u0281\u0282\u0283\u0284\u0285\u0288\u028b\u028e\u0291\u0294\u0297\u0001\u0001\u0001\u0001\u0001\u0001\u0003\u0000\u0000" +
"\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0001\u001f\u02b7\u02b8\u02b9\u02ba\u02bb\u02bc\u02bf\u02c0\u02c1\u02c2\u017f\u0181\u0182\u02c5\u02c6\u02c7\u02c8\u02c9\u02ca\u02cb\u02ce\u02d1\u02d2\u02d3\u02d4\u02d5\u02d6\u02d7\u02d8\u02d9\u0001\u0001\u0001\u0001\u0001\u0003\u0000\u0000\u0001\u0001\u0001\u0003\u0000\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0003\u0000\u0000\u0003\u0000" +
"\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\r\u02e7\u02e8\u02e9\u02ea\u02eb\u02ec\u0196\u0198\u0185\u02ed\u02ee\u02ef\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\r\u02fd\u02fe\u02c9\\\u02ff\u02ca\u005d\u0300\u0252\u02ee\u0253\u02ef\u0001\u0001\u0001\u0001\u001d\u031e\u031f\u0320\u0321\u0322\u0325\u0326\u0329\u032a\u032b\u032c\u02c7\u0059\u02c8\u005a\u032d\u032e\u032f\u0330\u0212\u0213\u0331" +
"\u0332\u0333\u0334\u0335\u0336\u0337\u0001\u0001\u0001\u0001\u0003\u0000\u0000\u0001\u0003\u0000\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0007\u033f\u0340\u0341\u0342\u0343\u0330\u0001\u0001\u0001\u0001\u0001\r\u0351\u0352\u0353\u0354\u0355\u0356\u0357\u0358\u0359\u035a\u0352\u0353\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0007\u0362\u0363\u0364\u0365" +
"\u0366\u004c\u0001\u0001\u0001\u0001\u0001\u0011\u0378\u0379\u037a\u037b\u037c\u037e\u0380\u0381\u0383\u0384\u0386\u0388\u0389\u038b\u0053\u038d\u0001\u0001\u0001\u0001\u0002\u0000\u0002\u0000\u0001\u0002\u0000\u0001\u0002\u0000\u0002\u0000\u0001\u0002\u0000\u0002\u0000\u0002\u0000\u0027\u03b6\u03b7\u03b8\u03b9\u03ba\u03bb\u03bc\u03bd\u03be\u03bf\u03c0\u03c1\u03c2\u03c3\u03c4\u03c5\u03c6\u03c7\u03c8\u03c9\u03ca\u03cb\u03cc\u03cd" +
"\u03ce\u03cf\u03d0\u03d1\u03d2\u03d3\u03d4\u03d5\u03d6\u03d7\u03d8\u03d9\u03da\u03db\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0051\u042d\u042e\u042f\u0430\u0431\u0432\u0433\u0434\u0435\u0436\u0437\u0438\u0439\u043a\u043b\u043c\u043d\u043e\u0355" +
"\u0356\u043f\u0440\u035a\u0441\u0442\u0443\u0444\u0445\u0354\u0446\u0447\u0448\u0357\u0358\u0359\u044a\u044b\u044c\u044d\u044e\u044f\u0450\u0451\u0453\u0454\u0455\u0456\u0457\u0458\u0459\u045a\u045b\u045c\u045d\u045e\u045f\u0460\u0461\u0462\u0463\u0464\u0465\u0466\u0467\u0468\u0469\u046a\u046b\u046d\u046e\u046f\u0470\u0471\u0472\u0473\u0462\u0474\u0475\u0476\u0477\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001" +
"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0002\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0002\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0002\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0012\u048a\u048b\u048c\u048d\u048e\u048f\u0490" +
"\u0491\u0180\u0199\u0052\u0492\u005b\u0493\u032d\u02ff\u0300\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u002f\u04c3\u04c4\u04c5\u04c8\u04c9\u04ca\u04cb\u04ce\u04d1\u04d4\u04d7\u04da\u04db\u04de\u04df\u04e0\u04e1\u04e2\u04e3\u04e4\u0050\u0492\u02c5\u0054\u032b\u032e\u032f\u0115\u04e5\u04e6\u04e7\u04e8\u04e9\u04ea\u04eb\u04ec\u04ed\u04ee\u04ef\u04f0\u04f1\u04f2\u00cf\u04f3\u04f4\u04f5\u0001\u0001\u0003\u0000\u0000" +
"\u0001\u0001\u0001\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0001\u0003\u0000\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0136\u062c\u063d\u0689\u06f5\u0704\u0707\u083c\u0bf4\u0c15\u0c24\u0c2f\u0c6f\u0cae\u0d24\u0d66\u0d6c\u0d82\u0d88\u0d90\u0d93\u0d98\u0d9b\u0d9e\u0da8\u0dbf" +
"\u0dec\u0e1f\u0e3e\u0e7d\u0e97\u0e9b\u0eac\u0eb4\u0ec7\u0ed5\u0ee0\u0ee2\u0ef5\u0ef8\u0efb\u0f0d\u0f0f\u0f1f\u0f3c\u0f44\u0f48\u0f53\u0f62\u0f6f\u0fc5\u0fca\u0fce\u0fd0\u0fd8\u0fdf\u0fe2\u0ff7\u0ff9\u108e\u1135\u121e\u1231\u125b\u1292\u129c\u12ad\u12bb\u12c7\u12c9\u12d9\u12dc\u12e0\u12e8\u12ec\u12f8\u1313\u131d\u1322\u132e\u1338\u1353\u1376\u1378\u137c\u1383\u1387\u138a\u13a6\u1422\u143d\u148c\u154e\u1566\u1569\u156c\u156e\u1570" +
"\u1572\u1575\u1577\u157a\u157c\u157e\u1580\u1582\u162d\u162f\u1632\u1634\u1636\u1638\u163b\u163d\u165a\u1675\u167e\u168b\u169d\u169f\u16a3\u16a7\u16ba\u16c2\u16c7\u16c9\u16e0\u16ff\u1703\u1709\u1723\u1740\u179a\u179d\u179f\u17a9\u17ad\u17b2\u17b8\u17ba\u17bc\u17c0\u17c7\u17cb\u17cf\u1812\u181c\u1826\u1830\u1865\u186e\u1871\u1883\u1889\u188c\u188e\u18c8\u18cc\u18cf\u18d2\u18e9\u1928\u1945\u1961\u197d\u1980\u1989\u198b\u19a7\u19c9" +
"\u19d3\u19dc\u19e2\u19f9\u1a00\u1a05\u1a12\u1a17\u1a1e\u1a23\u1a26\u1a29\u1a2c\u1a2f\u1a36\u1a39\u1a3e\u1a41\u1a48\u1a4b\u1a52\u1a55\u1a58\u1a5b\u1a5e\u1a61\u1a64\u1a67\u1a6a\u1a6d\u1a78\u1a7b\u1a7e\u1a87\u1a8c\u1a95\u1a9a\u1abb\u1abd\u1ade\u1ae0\u1af7\u1afc\u1b01\u1b06\u1b09\u1b0f\u1b11\u1b14\u1b17\u1b1a\u1b1d\u1b24\u1b2b\u1b34\u1b3b\u1b4c\u1b4e\u1b59\u1b6a\u1b6d\u1b7c\u1b83\u1b86\u1b93\u1b9c\u1ba5\u1ba7\u1bb6\u1bbb\u1be4\u1be6" +
"\u1be9\u1bed\u1bef\u1c02\u1c0d\u1c18\u1c21\u1c26\u1c2f\u1c32\u1c34\u1c36\u1c3b\u1c3f\u1c43\u1c47\u1c4b\u1c4e\u1c53\u1c59\u1c5c\u1c5e\u1c61\u1c6e\u1c71\u1c74\u1c79\u1c7e\u1c80\u1c82\u1c85\u1c88\u1c8b\u1c8e\u1c92\u1c94\u1c97\u1c9a\u1c9d\u1ca0\u1ca3\u1ca6\u1ca9\u1cac\u1caf\u1cb2\u1cb7\u1cbb\u1cbe\u1cc1\u1cc4\u1cc7\u1cca\u1cd0\u1cd4\u1cd7\u1cda\u1cdd\u1ce0\u1ce3\u1ce6\u1ce9\u1ced\u1cef\u1cf1\u1cf3\u1d0a\u1d14\u0009\u0635\u0636\u0637" +
"\u0638\u0639\u063a\u063b\u063c\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u002f\u066c\u066d\u0674\u0675\u0676\u0677\u0678\u0679\u067a\u067b\u067c\u067d\u067e\u067f\u0680\u0681\u0682\u0683\u0684\u0685\u0686\u0687\u0688\u023e\u023f\u0240\u0241\u0242\u0243\u0244\u0245\u0246\u0247\u0248\u0249\u024a\u024b\u024c\u024d\u024e\u024f\u0250\u0672\u0252\u0673\u0253\u0001\u0005\u0672\u0673\u0252\u0253\u0001\u0001\u0001\u0001\u0001\u0001" +
"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0041\u06ca\u06ca\u06cb\u06cc\u06cd\u06ce\u06cf\u06d0\u06d1\u06d2\u06d3\u06d4\u06d5\u06d6\u06d7\u06d8\u04c4\u04c8\u04c9\u04ca\u04da\u04de\u04df\u04e0\u04e1\u04e2\u06dd\u06e1\u06cb\u06e0\u0492\u06db\u06dc\u06e4\u06e5\u04e5\u04e6\u04e8\u06e6\u06e7\u06e8\u06e9\u06ea\u06eb\u06ec\u06ed\u06ee\u06ef\u06f0\u04e9\u06f1\u04ea\u04eb\u04ec" +
"\u04ed\u04ee\u04ef\u04f0\u04f1\u06f2\u06f3\u04f2\u06f4\u00cf\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0003\u06db\u06dc\u0001\u0001\u0003\u06e0\u0492\u0001\u0003\u06db\u06dc\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0008\u06fd\u06fe\u06ff\u0700\u0701\u0702\u0703\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0002\u0706\u0001\u007b" +
"\u0782\u078e\u078f\u07ac\u07c7\u07ce\u07cf\u07d3\u07d4\u07d5\u07d6\u07d7\u07d8\u07d9\u07da\u07dd\u07de\u07df\u07e0\u07e1\u07e2\u07e3\u07e4\u07e5\u07e6\u07e7\u07e8\u07e9\u07f3\u0037\u07fa\u0038\u0039\u003a\u003b\u003c\u003f\u0040\u0041\u0042\u0043\u0044\u0045\u0046\u0047\u0048\u0049\u004a\u07fe\u0805\u080a\u07f0\u080e\u080f\u080d\u07f1\u078b\u07f2\u0803\u07d2\u078c\u004b\u004c\u004d\u004e\u004f\u0050\u0051\u0052\u0053\u0054\u07fd" +
"\u0055\u07f8\u0059\u07f9\u005a\u0804\u005b\u07cc\\\u07cd\u005d\u0810\u0811\u0812\u0813\u0814\u0815\u0816\u005e\u005f\u0060\u0061\u0062\u0063\u0817\u0818\u081b\u081c\u081f\u0822\u0823\u0826\u0829\u082c\u082f\u0832\u0835\u0064\u0067\u0068\u006b\u006e\u006f\u0838\u0072\u0075\u0078\u007b\u007e\u083b\u0008\u078a\u078b\u078c\u0050\u0054\u078d\u04e7\u0001\u0001\u0001\u0001\u0001\u000f\u079e\u079f\u07a0\u07a1\u07a2\u07a3\u07a4\u07a5" +
"\u07a6\u07a7\u07a8\u07a9\u07aa\u07ab\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u000e\u07ba\u07bb\u07bc\u07bd\u07be\u07bf\u07c0\u07c1\u07c2\u07c3\u07c4\u07c5\u07c6\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0005\u07cc\u07cd\\\u005d\u0001\u0001\u0001\u0003\u07d2\u0053\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0003\u0000\u0000\u0001\u0001\u0001" +
"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0007\u07f0\u07f1\u07f2\u004b\u004f\u0051\u0001\u0001\u0001\u0005\u07f8\u07f9\u0059\u005a\u0001\u0001\u0003\u07fd\u0055\u0001\u0005\u0803\u0804\u0052\u005b\u0001\u0001\u0005\u07f8\u07f9\u0059\u005a\u0003\u080d\u004e\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0003\u0000\u0000\u0001\u0003\u0000\u0000\u0003\u0000\u0000\u0001\u0003\u0000\u0000\u0003\u0000" +
"\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0001\u00c1\u0636\u08fd\u08fe\u0903\u0904\u0907\u0932\u095d\u0988\u09b3\u09ba\u09bd\u09c0\u09c3\u09c6\u09c9\u09cc\u09cf\u09d2\u09d5\u09d8\u09db\u09de\u09e1\u09e4\u09e7\u09ea\u09ed\u09f0\u09f3\u09f6\u09f9\u09fc\u09ff\u0a02\u0a05\u0a08\u0a0b\u0a0e\u0a11\u0a14\u0a17\u0a1a\u0a1d\u0a20\u0a23\u0a26\u0a29\u0a2c\u0a2f\u0a32" +
"\u0a35\u0a38\u0a3b\u0a3e\u0a41\u0a44\u0a47\u0a4a\u0a4d\u0a50\u0a53\u0a56\u0a59\u0a5c\u0a5f\u0a62\u0a63\u0a64\u0a65\u0a66\u0a67\u0a68\u0a69\u0a6a\u0a6b\u0a6c\u0a6d\u0a6e\u0a6f\u0a70\u0a71\u0a72\u0a79\u0a80\u0a87\u0a8e\u0a95\u0a9c\u0aa3\u0aaa\u0ab1\u0ab8\u0abf\u0ac6\u0acd\u0ad4\u0ad7\u0ada\u0add\u0ae0\u0ae3\u0ae6\u0ae9\u0aec\u0aef\u0af2\u0af5\u0af8\u0afb\u0afe\u0b01\u0b04\u0b07\u0b0a\u0b0d\u0b10\u0b13\u0b16\u0b19\u0b1c\u0b1f\u0b22" +
"\u0b25\u0b28\u0b2b\u0b2e\u0b31\u0b34\u0b37\u0b3a\u0b3d\u0b40\u0b43\u0b46\u0b49\u0b4c\u0b4f\u0b52\u0b55\u0b58\u0b5b\u0b5e\u0b61\u0b64\u0b67\u0b6a\u0b6d\u0b70\u0b73\u0b76\u0b79\u0b7c\u0b7f\u0b82\u0b85\u0b88\u0b8b\u0b8e\u0b91\u0b94\u0b97\u0b9a\u0b9d\u0ba0\u0ba3\u0ba6\u0ba9\u0bac\u0baf\u0bb2\u0bb5\u0bb8\u0bbb\u0bbe\u0bc1\u0bc4\u0bc7\u0bca\u0bcd\u0bd0\u0bd3\u0bd6\u0bd9\u0bdc\u0bdf\u0be2\u0be5\u0be8\u0beb\u0bee\u0bf1\u0001\u0002\u0900" +
"\u0003\u0000\u0000\u0001\u0002\u0906\u0001\u000c\u0913\u0914\u0917\u091a\u091d\u0920\u0923\u0926\u0929\u092c\u092f\u0001\u0002\u0916\u0001\u0002\u0919\u0001\u0002\u091c\u0001\u0002\u091f\u0001\u0002\u0922\u0001\u0002\u0925\u0001\u0002\u0928\u0001\u0002\u092b\u0001\u0002\u092e\u0001\u0002\u0931\u0001\u000c\u093e\u093f\u0942\u0945\u0948\u094b\u094e\u0951\u0954\u0957\u095a\u0001\u0002\u0941\u0001\u0002\u0944\u0001\u0002\u0947\u0001" +
"\u0002\u094a\u0001\u0002\u094d\u0001\u0002\u0950\u0001\u0002\u0953\u0001\u0002\u0956\u0001\u0002\u0959\u0001\u0002\u095c\u0001\u000c\u0969\u096a\u096d\u0970\u0973\u0976\u0979\u097c\u097f\u0982\u0985\u0001\u0002\u096c\u0001\u0002\u096f\u0001\u0002\u0972\u0001\u0002\u0975\u0001\u0002\u0978\u0001\u0002\u097b\u0001\u0002\u097e\u0001\u0002\u0981\u0001\u0002\u0984\u0001\u0002\u0987\u0001\u000c\u0994\u0995\u0998\u099b\u099e\u09a1\u09a4" +
"\u09a7\u09aa\u09ad\u09b0\u0001\u0002\u0997\u0001\u0002\u099a\u0001\u0002\u099d\u0001\u0002\u09a0\u0001\u0002\u09a3\u0001\u0002\u09a6\u0001\u0002\u09a9\u0001\u0002\u09ac\u0001\u0002\u09af\u0001\u0002\u09b2\u0001\u0003\u09b6\u09b7\u0001\u0002\u09b9\u0001\u0002\u09bc\u0001\u0002\u09bf\u0001\u0002\u09c2\u0001\u0002\u09c5\u0001\u0002\u09c8\u0001\u0002\u09cb\u0001\u0002\u09ce\u0001\u0002\u09d1\u0001\u0002\u09d4\u0001\u0002\u09d7\u0001" +
"\u0002\u09da\u0001\u0002\u09dd\u0001\u0002\u09e0\u0001\u0002\u09e3\u0001\u0002\u09e6\u0001\u0002\u09e9\u0001\u0002\u09ec\u0001\u0002\u09ef\u0001\u0002\u09f2\u0001\u0002\u09f5\u0001\u0002\u09f8\u0001\u0002\u09fb\u0001\u0002\u09fe\u0001\u0002\u0a01\u0001\u0002\u0a04\u0001\u0002\u0a07\u0001\u0002\u0a0a\u0001\u0002\u0a0d\u0001\u0002\u0a10\u0001\u0002\u0a13\u0001\u0002\u0a16\u0001\u0002\u0a19\u0001\u0002\u0a1c\u0001\u0002\u0a1f\u0001" +
"\u0002\u0a22\u0001\u0002\u0a25\u0001\u0002\u0a28\u0001\u0002\u0a2b\u0001\u0002\u0a2e\u0001\u0002\u0a31\u0001\u0002\u0a34\u0001\u0002\u0a37\u0001\u0002\u0a3a\u0001\u0002\u0a3d\u0001\u0002\u0a40\u0001\u0002\u0a43\u0001\u0002\u0a46\u0001\u0002\u0a49\u0001\u0002\u0a4c\u0001\u0002\u0a4f\u0001\u0002\u0a52\u0001\u0002\u0a55\u0001\u0002\u0a58\u0001\u0002\u0a5b\u0001\u0002\u0a5e\u0001\u0002\u0a61\u0001\u0001\u0001\u0001\u0001\u0001\u0001" +
"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0003\u0a75\u0a76\u0001\u0002\u0a78\u0001\u0003\u0a7c\u0a7d\u0001\u0002\u0a7f\u0001\u0003\u0a83\u0a84\u0001\u0002\u0a86\u0001\u0003\u0a8a\u0a8b\u0001\u0002\u0a8d\u0001\u0003\u0a91\u0a92\u0001\u0002\u0a94\u0001\u0003\u0a98\u0a99\u0001\u0002\u0a9b\u0001\u0003\u0a9f\u0aa0\u0001\u0002\u0aa2\u0001\u0003\u0aa6\u0aa7\u0001\u0002\u0aa9\u0001\u0003\u0aad\u0aae\u0001\u0002\u0ab0" +
"\u0001\u0003\u0ab4\u0ab5\u0001\u0002\u0ab7\u0001\u0003\u0abb\u0abc\u0001\u0002\u0abe\u0001\u0003\u0ac2\u0ac3\u0001\u0002\u0ac5\u0001\u0003\u0ac9\u0aca\u0001\u0002\u0acc\u0001\u0003\u0ad0\u0ad1\u0001\u0002\u0ad3\u0001\u0002\u0ad6\u0001\u0002\u0ad9\u0001\u0002\u0adc\u0001\u0002\u0adf\u0001\u0002\u0ae2\u0001\u0002\u0ae5\u0001\u0002\u0ae8\u0001\u0002\u0aeb\u0001\u0002\u0aee\u0001\u0002\u0af1\u0001\u0002\u0af4\u0001\u0002\u0af7\u0001" +
"\u0002\u0afa\u0001\u0002\u0afd\u0001\u0002\u0b00\u0001\u0002\u0b03\u0001\u0002\u0b06\u0001\u0002\u0b09\u0001\u0002\u0b0c\u0001\u0002\u0b0f\u0001\u0002\u0b12\u0001\u0002\u0b15\u0001\u0002\u0b18\u0001\u0002\u0b1b\u0001\u0002\u0b1e\u0001\u0002\u0b21\u0001\u0002\u0b24\u0001\u0002\u0b27\u0001\u0002\u0b2a\u0001\u0002\u0b2d\u0001\u0002\u0b30\u0001\u0002\u0b33\u0001\u0002\u0b36\u0001\u0002\u0b39\u0001\u0002\u0b3c\u0001\u0002\u0b3f\u0001" +
"\u0002\u0b42\u0001\u0002\u0b45\u0001\u0002\u0b48\u0001\u0002\u0b4b\u0001\u0002\u0b4e\u0001\u0002\u0b51\u0001\u0002\u0b54\u0001\u0002\u0b57\u0001\u0002\u0b5a\u0001\u0002\u0b5d\u0001\u0002\u0b60\u0001\u0002\u0b63\u0001\u0002\u0b66\u0001\u0002\u0b69\u0001\u0002\u0b6c\u0001\u0002\u0b6f\u0001\u0002\u0b72\u0001\u0002\u0b75\u0001\u0002\u0b78\u0001\u0002\u0b7b\u0001\u0002\u0b7e\u0001\u0002\u0b81\u0001\u0002\u0b84\u0001\u0002\u0b87\u0001" +
"\u0002\u0b8a\u0001\u0002\u0b8d\u0001\u0002\u0b90\u0001\u0002\u0b93\u0001\u0002\u0b96\u0001\u0002\u0b99\u0001\u0002\u0b9c\u0001\u0002\u0b9f\u0001\u0002\u0ba2\u0001\u0002\u0ba5\u0001\u0002\u0ba8\u0001\u0002\u0bab\u0001\u0002\u0bae\u0001\u0002\u0bb1\u0001\u0002\u0bb4\u0001\u0002\u0bb7\u0001\u0002\u0bba\u0001\u0002\u0bbd\u0001\u0002\u0bc0\u0001\u0002\u0bc3\u0001\u0002\u0bc6\u0001\u0002\u0bc9\u0001\u0002\u0bcc\u0001\u0002\u0bcf\u0001" +
"\u0002\u0bd2\u0001\u0002\u0bd5\u0001\u0002\u0bd8\u0001\u0002\u0bdb\u0001\u0002\u0bde\u0001\u0002\u0be1\u0001\u0002\u0be4\u0001\u0002\u0be7\u0001\u0002\u0bea\u0001\u0002\u0bed\u0001\u0002\u0bf0\u0001\u0002\u0bf3\u0001\u0011\u0c05\u0c06\u0c07\u0c08\u0c09\u0c0a\u0c0b\u0c0c\u0c0d\u0c0e\u0c0f\u0c10\u0c11\u0c12\u0c13\u0c14\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0009\u0c1e\u0351" +
"\u0c21\u0c22\u0362\u0363\u04c3\u0c23\u0003\u080e\u004c\u0001\u0001\u0001\u0007\u0c2b\u0c2c\u0c2d\u0c2e\u02fd\u02fe\u0001\u0001\u0001\u0001\u002a\u0637\u06cc\u07ce\u0637\u0c59\u0c5a\u0c5b\u0c5c\u0c5d\u0c5e\u0c5f\u0c60\u0c61\u0c62\u0c63\u0c64\u0c65\u0c66\u0c67\u0c68\u033f\u013e\u013f\u0340\u0141\u0142\u0341\u0143\u0144\u0145\u0342\u0146\u0147\u0148\u0343\u0c69\u0c6a\u0c6b\u0c6c\u0c6d\u0c6e\u0001\u0001\u0001\u0001\u0001\u0001\u0001" +
"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0020\u0638\u0903\u0c06\u0c2c\u0c59\u0c8f\u0c96\u0c9f\u0ca0\u0095\u0ca1\u0ca2\u0ca3\u0ca4\u0ca5\u0ca6\u0ca7\u0ca8\u0ca9\u063c\u0caa\u031e\u00b7\u031f\u0321\u0ca6\u0325\u0329\u0cab\u0cac\u0cad\u0004\u0c93\u0c94\u0c95\u0001\u0001\u0001\u0003\u0c99\u0c9c\u0003\u0000\u0000\u0003\u0000\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001" +
"\u0001\u0001\u0001\u0001\u0001\u0001\u0046\u0cf4\u0cf5\u0cf9\u0cfd\u0cfe\u0cff\u0d00\u0d01\u0d02\u0d03\u0d04\u0d05\u0d06\u0d07\u0d08\u0d09\u0d0a\u0d0b\u0d0c\u0d0d\u0d0e\u0d0f\u0d10\u0d11\u0d12\u0d13\u0d14\u0d15\u0d16\u0cfd\u01ed\u01ee\u01ef\u01f0\u01f1\u01f2\u01f3\u01f4\u01f5\u01fd\u01fe\u01ff\u0200\u0204\u0205\u0206\u020d\u020e\u020f\u0210\u0d17\u0cfc\u0081\u0d1a\u012f\u0214\u0d1b\u0d1c\u0d1d\u0d1e\u0d1f\u0d20\u0c6e\u0d21\u0d22" +
"\u06f2\u0cf8\u0215\u0d23\u0001\u0003\u0cf8\u0215\u0001\u0003\u0cfc\u0081\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0003\u0cfc\u0081\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0028\u0c9f\u0d4c\u0d4c\u0d4d\u0d4e\u0d4f\u0ca2\u0d50\u0d51\u0d52\u0d53\u0d54\u0d55\u0d56\u0d57\u00b4\u0d58\u00b7\u00ba" +
"\u00bb\u00bc\u0380\u0d59\u0383\u00c6\u0d5a\u0d5b\u00c9\u0d5c\u0d5d\u0d5e\u0d5f\u0d60\u0d61\u00ce\u0d62\u0d63\u0d64\u0d65\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0004\u0351\u0d6a\u0d6b\u0001\u0001\u000b\u0d77\u0d7a\u0d7b\u0d7c\u0d7d\u0d7e\u0d7f\u0d80\u0d81\u0432\u0002\u0d79\u0001\u0001\u0001\u0001\u0001\u0001\u0001" +
"\u0001\u0001\u0004\u0d86\u0d87\u0433\u0001\u0001\u0005\u0d8d\u0d8e\u0d8f\u0434\u0001\u0001\u0001\u0002\u0d92\u0001\u0003\u0d96\u0d97\u0001\u0001\u0002\u0d9a\u0001\u0002\u0d9d\u0001\u0006\u0da4\u0da5\u0ca0\u0da6\u0da7\u0001\u0001\u0001\u0001\u0011\u0db9\u0c5a\u0c5d\u0c60\u0c64\u0dba\u0dbb\u0c68\u0dbc\u033f\u0340\u0341\u0342\u0dbd\u0dbe\u0343\u0001\u0001\u0001\u0001\u0001\u0001\u001e\u063a\u06cd\u07d3\u0093\u0ddd\u0dde\u0ddf\u0de0" +
"\u0de1\u0de2\u0de3\u0de4\u0de5\u0de6\u0de7\u0de8\u0de9\u0dea\u0de0\u0105\u0106\u0107\u010d\u010e\u0110\u0111\u0112\u0114\u0deb\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0021\u0d4d\u0e0d\u0e0e\u0e0f\u0e0f\u0e10\u0e11\u0e12\u0e13\u0e14\u0e15\u0e16\u0e17\u0e18\u0e19\u0e1a\u0e0f\u0e1b\u0e0f\u026b\u0e13\u0e14\u026c\u0e1c\u0e18\u0e0f\u0e13\u0e0f\u0e1d\u0e13\u0270\u0e1e\u0001\u0001\u0001" +
"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0015\u063b\u06ce\u07d4\u0e34\u0de1\u0e35\u0e36\u0e37\u0e38\u0e39\u0e3a\u0e3b\u0e35\u0170\u0172\u0175\u0177\u0178\u0e3c\u0e3d\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0020\u0e5e\u0e5f\u0e66\u0e67\u0e68\u0e69\u0e6a\u0e6b\u0e6c\u0e6d\u0e6e\u02e7\u0e78\u02e8\u02e9\u02ea\u02eb\u02ec\u0e75\u0e76\u0e77\u0196\u0198\u0185\u0e7b" +
"\u02ed\u0e64\u02ee\u0e65\u02ef\u0e7c\u0001\u0005\u0e64\u0e65\u02ee\u02ef\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0007\u0e75\u0e76\u0e77\u0196\u0198\u0185\u0001\u0001\u0001\u0003\u0e7b\u02ed\u0001\u0001\u0015\u06cf\u07d5\u0e92\u0c21\u0c5a\u0ca1\u0c5a\u0e37\u0c21\u0e93\u0e94\u0e37\u0ca1\u0e95\u0c21\u0e93\u0e94\u0e96\u06cf\u07d5\u0001\u0001\u0001\u0001\u0001\u0003\u0d04\u0e9a\u0001\u000c\u07d6\u0c5b\u0d05\u0d4f" +
"\u0de2\u0e0f\u0ea7\u0ea8\u0ea9\u0eaa\u0eab\u0001\u0001\u0001\u0001\u0001\u0007\u0c5c\u0ca2\u0d06\u0de3\u0eb3\u0eb3\u0001\u0011\u06d0\u07d7\u0c5d\u0ca3\u0d07\u0c5d\u0de4\u0e0f\u0e38\u0ec5\u0e38\u0ca3\u0ec6\u0ec5\u06d0\u07d7\u0001\u0001\u0006\u0d08\u0ecd\u0ed0\u0ed2\u0ed4\u0003\u0000\u0000\u0002\u0000\u0002\u0000\u0001\u0008\u0edd\u0c5e\u0d09\u0ede\u0edd\u0edf\u0edd\u0001\u0001\u0001\u0002\u0c5f\u0010\u06d2\u07d9\u0c60\u0ca4\u0d0b" +
"\u0c60\u0e39\u0ef2\u0e39\u0ca4\u0ef3\u0ef2\u0ef4\u06d2\u07d9\u0001\u0001\u0001\u0003\u07da\u07da\u0003\u0c61\u0ca5\n\u07de\u0c62\u0ca6\u0d53\u0de5\u0e10\u0f05\u0f0c\u0d57\u0002\u0f07\u0002\u0f09\u0003\u0000\u0000\u0001\u0002\u0d0c\u000c\u07e0\u0c63\u0de6\u0e11\u0f1b\u0f1c\u0f1d\u0f1b\u0f1c\u0f1d\u0f1e\u0001\u0001\u0001\u0001\u0016\u06d3\u07e1\u0c64\u0ca7\u0d54\u0c64\u0e3a\u09c8\u0ea9\u0f35\u0f36\u0f37\u0f38\u0f39\u0e3a\u0ca7" +
"\u0f3a\u0f35\u0f3b\u06d3\u07e1\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0006\u0f42\u0d0f\u0e13\u0f42\u0f43\u0001\u0001\u0003\u0f47\u0f47\u0001\u0009\u07e3\u0c65\u0de7\u0e14\u0f36\u0f51\u0f51\u0f52\u0001\u0001\u000c\u0f37\u07e4\u0c66\u0d11\u0dba\u0de8\u0f5f\u0f37\u0f60\u0f60\u0f61\u0001\u0001\u0001\u000b\u0c67\u0d55\u0d12\u0d55\u0dbb\u0de9\u0e15\u0f6d\u0f6e\u0f6d\u0001\u0001\u002d\u0f9c\u0fa2\u06d4\u07e5\u0c22\u0c68\u0ca8\u0c68" +
"\u0e3b\u0e92\u0fa6\u0edd\u0fa7\u0fa8\u0fa9\u0e3b\u0ca8\u0faa\u0fab\u0fac\u0fad\u0fae\u0faf\u0fb0\u0fb1\u06d4\u07e5\u0fb2\u0fb5\u0fb6\u0fb7\u0fb8\u0fb9\u0fba\u0fbb\u0fbc\u0fbd\u0fbe\u0fbf\u0fc0\u0fc1\u0fc2\u0fc3\u0fc4\u0002\u0f9e\u0003\u0fa1\u0149\u0001\u0003\u0fa5\u0251\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0003\u0fa1\u0149\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001" +
"\u0001\u0001\u0001\u0001\u0001\u0005\u0d57\u0de2\u0de8\u0dea\u0003\u0e17\u0fcd\u0001\u0002\u0f38\u0007\u06d7\u07e7\u0e18\u0fd7\u06d7\u07e7\u0001\u0006\u07e8\u0d16\u0dea\u0fde\u0fde\u0001\u0002\u0fe1\u0001\u0008\u0ca9\u0e6d\u0fea\u0fed\u0ff2\u0ff5\u0ff6\u0003\u0000\u0000\u0002\u0fef\u0003\u0000\u0000\u0003\u0000\u0000\u0001\u0001\u0002\u0fe1\u004e\u1047\u042d\u042e\u042f\u0430\u0cfd\u0d57\u0431\u0432\u0433\u0434\u0435\u0436\u0437" +
"\u0438\u0439\u043a\u043b\u0e37\u1051\u0e38\u1052\u1053\u0e39\u1054\u0e3a\u1055\u0e3b\u0fcd\u0fd7\u1056\u1057\u0170\u0171\u0172\u0173\u0174\u0175\u0176\u0177\u0184\u0178\u0179\u017d\u017e\u0097\u106b\u106e\u1071\u1074\u1077\u107a\u0186\u0189\u018c\u018f\u107d\u0192\u104f\u0197\u1050\u019a\u0430\u1080\u1081\u1082\u1083\u1084\u1085\u1086\u1087\u1088\u1089\u108a\u108b\u108c\u108d\u0007\u104e\u104f\u1050\u0195\u0197\u019a\u0001\u0001" +
"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0013\u0352\u035a\u0441\u0442\u0444\u0354\u0353\u0357\u0358\u044c\u044d\u044e\u045b\u045d\u0463\u0464\u0465\u106a\u0001\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0058\u063c\u10e6\u10ed\u10f7\u03b6\u03b7\u03b8\u03b9\u10f8" +
"\u03ba\u03bb\u03bc\u03bd\u03be\u03bf\u03c0\u03c1\u03c2\u03c3\u1101\u0de0\u03c4\u0e35\u0ca1\u0ca3\u1105\u0ca4\u0ca7\u0ca8\u1106\u063c\u063c\u031e\u031f\u0320\u0321\u0325\u0329\u032a\u1107\u10f4\u110b\u110a\u10f5\u10f6\u032b\u032c\u032d\u032e\u032f\u10ff\u0212\u110c\u110d\u110e\u0331\u0332\u0333\u110f\u1112\u1115\u1116\u1119\u111c\u111d\u1120\u0334\u1123\u1126\u0335\u10eb\u0336\u10ec\u0337\u1129\u112a\u03b9\u112b\u112c\u112d\u112e" +
"\u112f\u1130\u1131\u1132\u1133\u1134\u0005\u10eb\u10ec\u0336\u0337\u0001\u0001\u0007\u10f4\u10f5\u10f6\u032b\u032e\u032f\u0001\u0001\u0001\u0001\u0007\u10ff\u0c99\u1100\u0212\u0c9c\u0213\u0001\u0001\u0003\u1104\u0330\u0001\u0001\u0001\u0003\u110a\u032d\u0001\u0001\u0001\u0001\u0001\u0003\u0000\u0000\u0003\u0000\u0000\u0001\u0003\u0000\u0000\u0003\u0000\u0000\u0001\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000" +
"\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0046\u117b\u1182\u119f\u11ba\u0e95\u0ec6\u0ef3\u11c1\u0f3a\u0faa\u11c2\u11c3\u11c4\u11ce\u11d5\u02b7\u11f4\u02b8\u02b9\u02ba\u02bb\u02bf\u02c0\u02c1\u11f8\u11cb\u11cc\u11cd\u1180\u017f\u0181\u0182\u02c5\u11f7\u02c6\u11d3\u02c7\u11d4\u02c8\u11bf\u02c9\u11c0\u02ca\u11fd\u11fe\u11ff\u1200\u1201\u1202\u1203\u02d1\u02d2\u02d3\u02d4\u02d5\u02d6\u02d7\u1204" +
"\u1207\u1208\u1209\u120c\u120f\u1212\u02d8\u02d9\u1215\u1218\u121b\u0005\u1180\u02c5\u1181\u04f3\u0001\u0001\u000f\u1191\u1192\u1193\u1194\u1195\u1196\u1197\u1198\u1199\u119a\u119b\u119c\u119d\u119e\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u000e\u11ad\u11ae\u11af\u11b0\u11b1\u11b2\u11b3\u11b4\u11b5\u11b6\u11b7\u11b8\u11b9\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001" +
"\u0001\u0001\u0005\u11bf\u11c0\u02c9\u02ca\u0001\u0001\u0001\u0001\u0001\u0007\u11cb\u11cc\u11cd\u017f\u0181\u0182\u0001\u0001\u0001\u0005\u11d3\u11d4\u02c7\u02c8\u0001\u0001\r\u11e2\u11e5\u11e8\u11eb\u11ee\u11f1\u0285\u0288\u028b\u028e\u0291\u0294\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u11f7\u02c6\u0001\u0005\u11d3\u11d4\u02c7\u02c8\u0001\u0001\u0001" +
"\u0001\u0001\u0001\u0001\u0003\u0000\u0000\u0001\u0001\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0012\u04c4\u0037\u0fab\u0362\u033f\u031e\u033f\u0170\u0e93\u0170\u031e\u02b7\u0362\u1230\u048a\u04c4\u0037\u0001\u0021\u1252\u1255\u01ee\u0e92\u0fa6\u0edd\u0fa7\u0fa8\u0fa9\u0fab\u0fac\u0fad\u0fae\u0faf\u0fb0\u1258\u0fb5\u0fb6\u0fb7\u0fb8\u0fb9\u0fba" +
"\u0fbb\u0fbc\u0fbd\u0fbe\u0fbf\u0fc0\u0fc1\u0fc2\u0fc3\u0fc4\u0003\u0fa5\u0251\u0003\u0fa1\u0149\u0003\u0fa1\u0149\u002c\u1287\u0038\u013e\u01ef\u0d58\u0105\u0e0f\u128b\u0de2\u0de3\u0de4\u128c\u128d\u128e\u128f\u0de5\u0de6\u1290\u0de7\u0de8\u0de9\u1291\u0dea\u0104\u0105\u0106\u0107\u0108\u0109\u010a\u010b\u010c\u010d\u010e\u010f\u0110\u0111\u0112\u0113\u0114\u0d58\u128a\u0115\u0003\u128a\u0115\u0001\u0001\u0001\u0001\u0001\u0001" +
"\u0001\u0001\u0008\u013f\u00b7\u01f0\u0106\u0e1b\u129a\u129b\u0001\u0001\u0010\u04c8\u0039\u0340\u031f\u01f1\u0340\u0107\u0e0f\u0172\u0172\u031f\u02b8\u12ac\u04c8\u0039\u0001\u0007\u01f2\u12b4\u12b5\u12b7\u12b9\u12b4\u0001\u0002\u0000\u0002\u0000\u0002\u0000\u0009\u0fad\u0141\u01f3\u0fad\u0ede\u0edd\u12c4\u0fad\u0003\u0000\u0000\u0002\u0142\u000f\u04ca\u003b\u0341\u0321\u01f5\u0341\u0175\u0175\u0321\u02b9\u12d8\u048c\u04ca\u003b" +
"\u0001\u0003\u003c\u003c\u0003\u0143\u12df\u0001\u0008\u0040\u0144\u0ca6\u0380\u010d\u0f0c\u0d57\u0004\u01fd\u0d59\u0d5a\u0006\u0042\u0145\u010e\u12f2\u048d\u0002\u12f4\u0004\u0000\u0000\u0000\u001a\u04da\u0043\u0342\u0325\u0383\u0342\u0177\u0c21\u0c22\u0177\u0325\u02bb\u0362\u0ea9\u1312\u0351\u0f36\u0f37\u0363\u0364\u0f38\u0365\u048e\u04da\u0043\u0001\u0005\u0f42\u0200\u0e13\u1318\u0002\u131a\u0003\u0000\u0000\u0005\u0045\u0146" +
"\u0110\u0e14\u000b\u0f37\u0046\u0147\u0205\u0dbd\u0111\u0f5f\u0f37\u132d\u0147\u0001\u0009\u0148\u00c6\u0206\u00c6\u0dbe\u0112\u1337\u0f6e\u0001\u001b\u04df\u0047\u0363\u0343\u0329\u0d5a\u0343\u0178\u0e92\u0fa6\u0edd\u0fa7\u0fa8\u0fa9\u0178\u0329\u02bf\u0fab\u0fac\u0fad\u0fae\u0faf\u0fb0\u048f\u04df\u0047\u0023\u0d5b\u128b\u0de2\u0de3\u0de4\u128c\u128d\u128e\u128f\u0de6\u1290\u0de7\u0de8\u0de9\u1291\u0dea\u0104\u0105\u0106\u0107" +
"\u0108\u0109\u010a\u010b\u010c\u0d57\u010e\u010f\u0110\u0111\u0112\u0113\u0114\u0091\u0002\u0179\u0003\u0f38\u137b\u0001\u0007\u04e2\u0049\u0e18\u017d\u04e2\u0049\u0004\u004a\u0210\u0114\u0002\u1389\u0001\u0012\u139c\u139d\u0e9a\u0eab\u0edf\u0f61\u0097\u0d58\u0091\u139e\u139f\u13a0\u13a1\u13a2\u13a3\u13a4\u13a5\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u002f\u13d5\u13d9\u13ee\u1401\u0d6b\u0e96\u1408\u0ef4\u0f1e" +
"\u0f3b\u0fb1\u1409\u140a\u140b\u048a\u1415\u048b\u048c\u048d\u048e\u048f\u0490\u0491\u139e\u1419\u04e3\u1412\u1413\u1414\u0180\u0199\u0183\u1418\u0493\u1406\u02ff\u1407\u0300\u141a\u141b\u141c\u141d\u141e\u141f\u1420\u1421\u0003\u13d8\u04f4\u0001\u000b\u13e4\u13e5\u13e6\u13e7\u13e8\u13e9\u13ea\u13eb\u13ec\u13ed\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\n\u13f8\u13f9\u13fa\u13fb\u13fc\u13fd\u13fe\u13ff\u1400" +
"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0005\u1406\u1407\u02ff\u0300\u0001\u0001\u0001\u0001\u0001\u0007\u1412\u1413\u1414\u0180\u0199\u0183\u0001\u0001\u0001\u0003\u1418\u0493\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0017\u078a\u04c3\u0e3c\u06cf\u06d0\u06d2\u06d3\u06d4\u06d7\u1439\u04c4\u04c8\u04ca\u04da\u04df\u04e2\u143a\u078a\u04f5\u04e4\u143b\u143c\u0001\u0001\u0001\u0001\u0037\u1474\u1479" +
"\u1480\u1485\u0ca1\u0ca3\u1105\u0ca4\u0ca7\u0ca8\u1106\u031e\u031f\u0320\u0321\u0325\u0329\u032a\u1488\u10f4\u110b\u110a\u10f5\u10f6\u032b\u032c\u032d\u032e\u032f\u10ff\u0212\u110c\u110d\u110e\u0331\u0332\u0333\u110f\u1112\u1115\u1116\u1119\u111c\u111d\u1120\u0334\u1123\u1126\u0335\u10eb\u0336\u10ec\u0337\u148b\u0005\u10eb\u10ec\u0336\u0337\u0007\u10f4\u10f5\u10f6\u032b\u032e\u032f\u0005\u10ff\u1100\u0212\u0213\u0003\u1104\u0330" +
"\u0003\u110a\u032d\u0001\u0077\u1503\u150a\u1519\u1527\u152c\u152f\u07d5\u07d6\u07d7\u07d8\u07d9\u07da\u07dd\u07de\u07df\u07e0\u07e1\u07e2\u07e3\u07e4\u07e5\u07e6\u07e7\u07e8\u1532\u1539\u0037\u153e\u0038\u0039\u003a\u003b\u003c\u003f\u0040\u0041\u0042\u0043\u0044\u0045\u0046\u0047\u0048\u0049\u004a\u1541\u1546\u154b\u07f0\u080e\u080f\u080d\u07f1\u078b\u07f2\u0803\u07d2\u078c\u004b\u004c\u004d\u004e\u004f\u0050\u0051\u0052\u0053" +
"\u0054\u07fd\u0055\u07f8\u0059\u07f9\u005a\u0804\u005b\u07cc\\\u07cd\u005d\u0810\u0811\u0812\u0813\u0814\u0815\u0816\u005e\u005f\u0060\u0061\u0062\u0063\u0817\u0818\u081b\u081c\u081f\u0822\u0823\u0826\u0829\u082c\u082f\u0832\u0835\u0064\u0067\u0068\u006b\u006e\u006f\u0838\u0072\u0075\u0078\u007b\u007e\u0007\u078b\u078c\u0050\u0054\u078d\u04e7\u000f\u079e\u079f\u07a0\u07a1\u07a2\u07a3\u07a4\u07a5\u07a6\u07a7\u07a8\u07a9\u07aa" +
"\u07ab\u000e\u07ba\u07bb\u07bc\u07bd\u07be\u07bf\u07c0\u07c1\u07c2\u07c3\u07c4\u07c5\u07c6\u0005\u07cc\u07cd\\\u005d\u0003\u080d\u004e\u0003\u07d2\u0053\u0007\u07f0\u07f1\u07f2\u004b\u004f\u0051\u0005\u07f8\u07f9\u0059\u005a\u0003\u07fd\u0055\u0005\u0803\u0804\u0052\u005b\u0005\u07f8\u07f9\u0059\u005a\u0003\u080d\u004e\u0017\u0c5b\u0c5c\u1565\u0c5e\u0c5f\u0c61\u0c62\u0c63\u0c65\u0c66\u0c67\u013e\u013f\u0140\u0141\u0142\u0143" +
"\u0144\u0145\u0146\u0147\u0148\u0001\u0002\u1568\u0001\u0003\u0edd\u0fad\u0002\u0810\u0002\u0811\u0002\u0812\u0003\u06e4\u0813\u0002\u0814\u0003\u06e5\u0815\u0002\u0816\u0002\u005e\u0002\u005f\u0002\u0060\u000e\u04e5\u1590\u15b3\u15c0\u15cd\u15f0\u1613\u1627\u1628\u1629\u162a\u162b\u162c\u0006\u1596\u15a3\u15b0\u15b1\u15b2\u0007\u159d\u159e\u159f\u15a0\u15a1\u15a2\u0001\u0001\u0001\u0001\u0001\u0001\u0007\u15aa\u15ab\u15ac\u15ad" +
"\u15ae\u15af\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0007\u15ba\u15bb\u15bc\u15bd\u15be\u15bf\u0001\u0001\u0001\u0001\u0001\u0001\u0007\u15c7\u15c8\u15c9\u15ca\u15cb\u15cc\u0001\u0001\u0001\u0001\u0001\u0001\u0006\u15d3\u15e0\u15ed\u15ee\u15ef\u0007\u15da\u15db\u15dc\u15dd\u15de\u15df\u0001\u0001\u0001\u0001\u0001\u0001\u0007\u15e7\u15e8\u15e9\u15ea\u15eb\u15ec\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001" +
"\u0006\u15f6\u1603\u1610\u1611\u1612\u0007\u15fd\u15fe\u15ff\u1600\u1601\u1602\u0001\u0001\u0001\u0001\u0001\u0001\u0007\u160a\u160b\u160c\u160d\u160e\u160f\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0006\u1619\u1620\u15b0\u15b1\u15b2\u0007\u159d\u159e\u159f\u15a0\u15a1\u15a2\u0007\u15aa\u15ab\u15ac\u15ad\u15ae\u15af\u0001\u0001\u0001\u0001\u0001\u0001\u0002\u0062\u0003\u04e6\u0063\u0002\u0817\u0002\u0e0f\u0002\u0e13" +
"\u0003\u0f1d\u0f1d\u0002\u0e0f\u0008\u1645\u1646\u1647\u1645\u1656\u1658\u1645\u0001\u0001\u000f\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0002\u0000\u0002\u0000\u0002\u165c\u0019\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0007\u0c69\u0d1b\u0d5f\u167c\u167d\u0d5f\u0001\u0001\u0007\u1685" +
"\u1687\u1688\u1685\u1685\u168a\u0002\u0000\u0001\u0002\u0000\u0001\u0002\u168d\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0002\u0d1c\u0003\u0e13\u16a2\u0001\u0003\u0334\u16a6\u0001\u0003\u0d1d\u16aa\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0007\u0c6a\u0d1e\u167c\u16c1\u167d\u16c1\u0001\u0004\u0c6b\u0d1f\u16c6\u0001\u0002" +
"\u0d20\u0005\u0c6c\u16ce\u16cf\u16df\u0001\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0001\u0004\u16e4\u16e5\u16e6\u0001\u0001\u0019\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0003\u0f37\u1702\u0001\u0004\u1707\u1708\u1707\u0001\u0001\u0007\u16a6\u0fc4\u0335\u1710\u16a6\u0fc4" +
"\u0013\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0004\u0c6d\u0d21\u1727\u0019\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u000f\u0d22\u1647\u165c\u168d\u16aa\u16cf\u16e6\u1710\u1727\u174f\u1761\u177a\u16aa\u178a\u0012\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" +
"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0019\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0002\u179c\u0001\u0002\u1761\u0006" +
"\u17a5\u06f1\u17a7\u17a8\u177a\u0002\u0000\u0001\u0001\u0002\u17ab\u0002\u0000\u0005\u06f2\u06f2\u16a6\u0334\u0004\u17b6\u16aa\u17ab\u0002\u0000\u0002\u16e4\u0002\u178a\u0002\u17be\u0002\u0000\u0003\u17c3\u17c5\u0002\u0000\u0002\u0000\u0002\u17c9\u0002\u0000\u0002\u17cd\u0002\u0000\u0017\u17e6\u17e8\u17ea\u17ec\u17ee\u17f0\u17f2\u17f4\u17f6\u17f8\u17fa\u17fc\u17fe\u1800\u1802\u1804\u1806\u1808\u180a\u180c\u180e\u1810\u0002\u0000" +
"\u0002\u0000\u0002\u0000\u0002\u0000\u0002\u0000\u0002\u0000\u0002\u0000\u0002\u0000\u0002\u0000\u0002\u0000\u0002\u0000\u0002\u0000\u0002\u0000\u0002\u0000\u0002\u0000\u0002\u0000\u0002\u0000\u0002\u0000\u0002\u0000\u0002\u0000\u0002\u0000\u0002\u0000\u0004\u1816\u1818\u181a\u0002\u0000\u0002\u0000\u0002\u0000\u0004\u1820\u1824\u1822\u0002\u1822\u0002\u0000\u0002\u0000\u0004\u182a\u182e\u182c\u0002\u182c\u0002\u0000\u0002\u0000" +
"\u0008\u1838\u184d\u184e\u184f\u1850\u184f\u1850\u0015\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0001\u0001\u0001\u0015\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0005\u186a\u186b\u186c\u186d\u0001\u0001\u0001\u0001\u0002\u1870\u0001\u0002\u1873\u0010\u0000\u0000\u0000\u0000" +
"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0004\u186b\u1887\u1888\u0001\u0001\u0002\u188b\u0001\u0002\u186c\n\u1898\u1899\u189a\u189b\u189c\u18b2\u189b\u189c\u18b2\u0001\u0001\u0001\u0001\u0016\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0016\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" +
"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0003\u18cb\u18cb\u0001\u0002\u18ce\u0001\u0002\u18d1\u0001\u0004\u18d6\u18e8\u18e8\u0012\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0001\u0006\u186d\u18ef\u1902\u1915\u1915\u0013\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0013\u0000\u0000\u0000\u0000\u0000" +
"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0013\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0009\u1931\u1932\u1899\u18ef\u1933\u1943\u1944\u1944\u0001\u0001\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0001\u0001\n\u194f\u189a\u18ce\u1902\u1950\u1943\u184e\u1943\u184e\u0001" +
"\u0011\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\n\u196b\u196c\u189b\u1915\u196d\u1944\u184f\u1944\u184f\u0001\u0001\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0002\u197f\u0001\u0007\u184d\u189a\u189b\u1987\u1988\u1899\u0001\u0001\u0002\u1898\u0009\u1850\u1888\u189c\u18e8\u1943\u1944\u1994\u1944\u0013\u0000\u0000\u0000" +
"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0006\u18b2\u184e\u184f\u19ad\u184f\u001c\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\n\u196b\u196c\u189b\u1915\u1944\u184f\u196d\u1944\u184f\u0009\u1850\u1888\u189c\u18e8\u1943\u1944\u1944\u1994\u0006\u18b2\u184e\u184f" +
"\u184f\u19ad\u000c\u19ee\u19ef\u19f0\u19f1\u19f2\u19f3\u19f4\u19f5\u19f6\u19f7\u19f8\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0004\u19fd\u19fe\u19ff\u0001\u0001\u0001\u0003\u1a03\u1a04\u0001\u0001\u0007\u1a0c\u1a0d\u1a0e\u1a0f\u1a10\u1a11\u0001\u0001\u0001\u0001\u0001\u0001\u0003\u1a15\u1a16\u0001\u0001\u0004\u1a1b\u1a1c\u1a1d\u0001\u0001\u0001\u0003\u1a21\u1a22\u0001\u0001\u0002\u1a25\u0001\u0002\u1a28" +
"\u0001\u0002\u1a2b\u0001\u0002\u1a2e\u0001\u0004\u1a33\u1a34\u1a35\u0001\u0001\u0001\u0002\u1a38\u0001\u0003\u1a3c\u1a3d\u0001\u0001\u0002\u1a40\u0001\u0004\u1a45\u1a46\u1a47\u0001\u0001\u0001\u0002\u1a4a\u0001\u0004\u1a4f\u1a50\u1a51\u0001\u0001\u0001\u0002\u1a54\u0001\u0002\u1a57\u0001\u0002\u1a5a\u0001\u0002\u1a5d\u0001\u0002\u1a60\u0001\u0002\u1a63\u0001\u0002\u1a66\u0001\u0002\u1a69\u0001\u0002\u1a6c\u0001\u0006\u1a73\u1a74" +
"\u1a75\u1a76\u1a77\u0001\u0001\u0001\u0001\u0001\u0002\u1a7a\u0001\u0002\u1a7d\u0001\u0005\u1a83\u1a84\u1a85\u1a86\u0001\u0001\u0001\u0001\u0003\u1a8a\u1a8b\u0001\u0001\u0005\u1a91\u1a92\u1a93\u1a94\u0001\u0001\u0001\u0001\u0003\u1a98\u1a99\u0001\u0001\u0011\u1aab\u1aac\u1aad\u1aae\u1aaf\u1ab0\u1ab1\u1ab2\u1ab3\u1ab4\u1ab5\u1ab6\u1ab7\u1ab8\u1ab9\u1aba\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001" +
"\u0001\u0001\u0001\u0002\u1ab6\u0011\u1ace\u1acf\u1ad0\u1ad1\u1ad2\u1ad3\u1ad4\u1ad5\u1ad6\u1ad7\u1ad8\u1ad9\u1ada\u1adb\u1adc\u1add\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0002\u1ad5\u000c\u1aec\u1aed\u1aee\u1aef\u1af0\u1af1\u1af2\u1af3\u1af4\u1af5\u1af6\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0003\u1afa\u1afb\u0001\u0001\u0003\u1aff\u1b00\u0001" +
"\u0001\u0003\u1b04\u1b05\u0001\u0001\u0002\u1b08\u0001\u0006\u1ab7\u1ab8\u1ab9\u1ab0\u1aba\u0002\u1add\u0002\u1b13\u0001\u0002\u1b16\u0001\u0002\u1b19\u0001\u0002\u1b1c\u0001\u0004\u1b21\u1b22\u1b23\u0001\u0001\u0001\u0004\u1b28\u1b29\u1b2a\u0001\u0001\u0001\u0005\u1b30\u1b31\u1b32\u1b33\u0001\u0001\u0001\u0001\u0004\u1b38\u1b39\u1b3a\u0001\u0001\u0001\u0009\u1b44\u1b45\u1b46\u1b47\u1b48\u1b49\u1b4a\u1b4b\u0001\u0001\u0001\u0001" +
"\u0001\u0001\u0001\u0001\u0002\u1b45\u0006\u1b54\u1b55\u1b56\u1b57\u1b58\u0001\u0001\u0001\u0001\u0001\u0009\u1b62\u1b63\u1b64\u1b65\u1b66\u1b67\u1b68\u1b69\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0003\u1b69\u1b65\u0008\u1b75\u1b76\u1b77\u1b78\u1b79\u1b7a\u1b7b\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0004\u1b80\u1b81\u1b82\u0001\u0001\u0001\u0002\u1b85\u0001\u0007\u1b8d\u1b8e\u1b8f\u1b90\u1b91\u1b92\u0001\u0001\u0001" +
"\u0001\u0001\u0001\u0005\u1b98\u1b99\u1b9a\u1b9b\u0001\u0001\u0001\u0001\u0005\u1ba1\u1ba2\u1ba3\u1ba4\u0001\u0001\u0001\u0001\u0002\u1ba4\u0008\u1baf\u1bb0\u1bb1\u1bb2\u1bb3\u1bb4\u1bb5\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0003\u1bb9\u1bba\u0001\u0001\u0015\u1bd0\u1bd1\u1bd2\u1bd3\u1bd4\u1bd5\u1bd6\u1bd7\u1bd8\u1bd9\u1bda\u1bdb\u1bdc\u1bdd\u1bde\u1bdf\u1be0\u1be1\u1be2\u1be3\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001" +
"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0002\u1bd1\u0003\u1bde\u1bdf\u0004\u1bd7\u1be1\u1be0\u0002\u1bd9\n\u1bf9\u1bfa\u1bfb\u1bfc\u1bfd\u1bfe\u1bff\u1c00\u1c01\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0006\u1c08\u1c09\u1c0a\u1c0b\u1c0c\u0001\u0001\u0001\u0001\u0001\u0006\u1c13\u1c14\u1c15\u1c16\u1c17\u0001\u0001\u0001\u0001\u0001\u0005\u1c1d\u1c1e\u1c1f\u1c20\u0001\u0001\u0001" +
"\u0001\u0003\u1c24\u1c25\u0001\u0001\u0005\u1c2b\u1c2c\u1c2d\u1c2e\u0001\u0001\u0001\u0001\u0002\u1c31\u0001\u0002\u1bfd\u0002\u1c1e\u0003\u1c39\u1c3a\u0001\u0001\u0003\u139f\u1c3e\u0001\u0003\u0cab\u1c42\u0001\u0003\u13a0\u1c46\u0001\u0003\u0cac\u1c4a\u0001\u0002\u1c4d\u0001\u0004\u1129\u13a1\u1c52\u0001\u0005\u13a2\u141f\u04f5\u1c58\u0001\u0002\u1c5b\u0001\u0002\u112a\u0002\u1c60\u0001\u0008\u112b\u04e4\u1c69\u1c6a\u1c6b\u1c6c" +
"\u1c6d\u0001\u0001\u0001\u0001\u0001\u0002\u1c70\u0001\u0002\u1c73\u0001\u0004\u1420\u0de1\u1c78\u0001\u0004\u1421\u0de1\u1c7d\u0001\u0002\u1c69\u0002\u13a3\u0002\u1c84\u0001\u0002\u1c87\u0001\u0002\u1c8a\u0001\u0002\u1c8d\u0001\u0003\u112c\u1c91\u0001\u0002\u112c\u0002\u1c96\u0001\u0002\u1c99\u0001\u0002\u1c9c\u0001\u0002\u1c9f\u0001\u0002\u1ca2\u0001\u0002\u1ca5\u0001\u0002\u1ca8\u0001\u0002\u1cab\u0001\u0002\u1cae\u0001\u0002" +
"\u1cb1\u0001\u0004\u1cb6\u112d\u13a4\u0001\u0003\u1cba\u112e\u0001\u0002\u1cbd\u0001\u0002\u1cc0\u0001\u0002\u1cc3\u0001\u0002\u1cc6\u0001\u0002\u1cc9\u0001\u0005\u143b\u148b\u1c6a\u1ccf\u0001\u0004\u112f\u1c6b\u1ccf\u0002\u1cd6\u0001\u0002\u1cd9\u0001\u0002\u1cdc\u0001\u0002\u1cdf\u0001\u0002\u1ce2\u0001\u0002\u1ce5\u0001\u0002\u1ce8\u0001\u0003\u1130\u1cec\u0001\u0002\u1131\u0002\u1132\u0002\u1133\u0016\u083b\u0d65\u0da7\u0deb" +
"\u0e1e\u0e3d\u0e7c\u0ff5\u1568\u1c3e\u1c42\u1c46\u1c4a\u1c52\u1c58\u1c6c\u1c78\u1c7d\u1c91\u1cec\u1d09\u0001\n\u0c23\u0cad\u0d23\u0ff6\u1134\u13a5\u143c\u1c6d\u1d09\u0002\u1d16\u0001\u00f6\u1e0d\u066c\u06cc\u0706\u07ce\u06cd\u06ce\u0351\u0cfd\u0c95\u0cfe\u1e12\u1e16\u1e19\u1e1c\u1e1f\u1e22\u1e25\u1e28\u1e2b\u1e2e\u1e31\u0ddf\u1419\u0e36\u0e66\u07d3\u07d4\u0c59\u0c94\u1230\u1e36\u0d58\u0e0f\u1e37\u0ca6\u1312\u0e13\u0e14\u0d5a" +
"\u0e18\u1e38\u0d00\u0688\u0d02\u1e39\u0f42\u1e41\u1e42\u1e43\u1e44\u1e45\u1e46\u1e47\u1e48\u1e49\u1e4a\u1e4b\u1e4c\u1e4d\u1e4e\u1e4f\u1645\u1e50\u1e51\u1e53\u1e54\u0d1f\u1e55\u1e56\u1e57\u1e58\u1e59\u1707\u1702\u1e5b\u1e5c\u1e5e\u1e5f\u17b6\u1e60\u0fc3\u0d22\u1658\u0d1e\u1e61\u167d\u1687\u16ce\u0d1f\u1e63\u1e6f\u1e76\u1e7c\u1e83\u1e89\u1e8e\u1e94\u1ea0\u1ea6\u1eac\u1eb3\u1ec0\u189c\u18b2\u197f\u1ec6\u1933\u1ec7\u1ec8\u1888\u1ec9" +
"\u1eca\u1ecb\u1ecc\u1ecd\u18cb\u19ad\u1ece\u1ecf\u1ed0\u18e8\u1ed1\u1ed2\u1ed3\u1943\u1944\u1e12\u1e16\u1e19\u1e1c\u1e1f\u1e22\u1e25\u1e28\u1e2b\u1e2e\u1950\u1ed4\u1994\u19ad\u1ed5\u196d\u1e12\u1e16\u1e19\u1e1c\u1e1f\u1e22\u1e25\u1e28\u1e2b\u1e2e\u1ed6\u1ed7\u1ed8\u1ed9\u1eda\u1edb\u1edc\u1edd\u1ede\u1edf\u1ee0\u1ee1\u1ee2\u1ee3\u19ee\u19ef\u19f0\u1ee4\u1ee5\u1ee6\u19f1\u1eea\u1eeb\u19f2\u19f3\u1eec\u1eed\u1ef1\u19f4\u1ef5\u19f5" +
"\u1ef9\u19f6\u19f7\u19f8\u1efa\u1efb\u1eff\u1f03\u1f04\u1f05\u1f06\u1f07\u1f08\u1f09\u1f0a\u1f0b\u1f0c\u1f0d\u1f0e\u1f0f\u1f10\u1f11\u1f12\u1f13\u1f15\u1f17\u1f19\u1f1b\u139d\u1e39\u0f52\u0e0d\u0e19\u0e0e\u0e1c\u1f1d\u1f1e\u1f1f\u1f20\u1f21\u1f22\u1c4d\u1129\u04f5\u1f23\u1f24\u1f25\u1f26\u1f27\u112d\u112e\u143b\u1f28\u1cec\u13a4\u1132\u1133\u083b\u168a\u1f29\u1f2a\u0005\u0000\u0000\u0000\u0000\u0004\u0000\u0000\u0000\u0003\u0000" +
"\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0005\u0000\u0000\u0000\u0000\u0001\u0001\u0001\u0008\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0002\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0002\u0000\u0001\u0002\u0000\u0001\u0001" +
"\u0001\u0002\u0000\u000c\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0007\u0000\u0000\u0000\u0000\u0000\u0000\u0006\u0000\u0000\u0000\u0000\u0000\u0007\u0000\u0000\u0000\u0000\u0000\u0000\u0006\u0000\u0000\u0000\u0000\u0000\u0005\u0000\u0000\u0000\u0000\u0006\u0000\u0000\u0000\u0000\u0000\u000c\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0006\u0000\u0000\u0000\u0000\u0000\u0006\u0000" +
"\u0000\u0000\u0000\u0000\u0007\u0000\u0000\u0000\u0000\u0000\u0000\r\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0006\u0000\u0000\u0000\u0000\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0004\u0000\u0000\u0000\u0001\u0001\u0001\u0004\u0000\u0000" +
"\u0000\u0004\u0000\u0000\u0000\u0004\u0000\u0000\u0000\u0001\u0001\u0004\u0000\u0000\u0000\u0004\u0000\u0000\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0002\u0000\u0002\u0000\u0002\u0000\u0002\u0000\u0002\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u000b\u1f36\u1f37\u1f38\u1f39\u1f3a\u1f3b\u1f3c\u1f3d\u1f3e\u1f3f\u0001\u0001" +
"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u000b\u1f4b\u1f4c\u1f4d\u1f4e\u1f4f\u1f50\u1f51\u1f52\u1f53\u1f54\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u000b\u1f60\u1f61\u1f62\u1f63\u1f64\u1f65\u1f66\u1f67\u1f68\u1f69\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u000b\u1f75\u1f76\u1f77\u1f78\u1f79\u1f7a\u1f7b\u1f7c\u1f7d\u1f7e\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u000b" +
"\u1f8a\u1f8b\u1f8c\u1f8d\u1f8e\u1f8f\u1f90\u1f91\u1f92\u1f93\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u000b\u1f9f\u1fa0\u1fa1\u1fa2\u1fa3\u1fa4\u1fa5\u1fa6\u1fa7\u1fa8\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u000b\u1fb4\u1fb5\u1fb6\u1fb7\u1fb8\u1fb9\u1fba\u1fbb\u1fbc\u1fbd\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0071\u0f60\u0d0b\u202f\u2032\u2035\u2036\u2037\u2038\u2039" +
"\u203a\u203b\u203c\u1ede\u1edf\u203d\u203e\u203f\u2040\u2041\u2042\u2043\u2044\u2045\u2046\u2047\u2048\u2049\u204a\u204b\u204c\u204d\u204e\u1f09\u1ed8\u204f\u1ed9\u2050\u2051\u1f10\u2052\u2053\u2054\u2055\u2056\u2057\u2058\u2059\u205a\u205b\u205c\u205d\u205e\u205f\u2060\u2061\u2062\u2063\u2064\u2065\u2066\u2067\u2068\u2069\u0f52\u206a\u206b\u206c\u206d\u206e\u206f\u2070\u2071\u2072\u2073\u2074\u2075\u2076\u2077\u2079\u207b\u207c" +
"\u207f\u2082\u2085\u2086\u0f52\u0e0d\u0e19\u0e0e\u0e1c\u1f1f\u1f20\u1f21\u1f22\u1c5b\u1c60\u1cb6\u1cba\u1cbd\u1cc0\u2087\u2088\u2089\u208a\u208b\u208c\u208d\u208e\u208f\u2090\u2091\u2092\u0003\u0000\u0000\u0003\u0000\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001" +
"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0002\u0000\u0002\u0000\u0001\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0006\u2098\u209b\u209e\u20a1\u20a4\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000" +
"\u0000\u0003\u0000\u0000\u0003\u0000\u0000").toCharArray();
public static final int accent_aigu = 1;
public static final int accent_arrows = 130;
public static final int accent_bar = 153;
public static final int accent_box = 208;
public static final int accent_caron = 231;
public static final int accent_cedille = 304;
public static final int accent_circonflexe = 330;
public static final int accent_dot_above = 412;
public static final int accent_dot_below = 541;
public static final int accent_double_aigu = 596;
public static final int accent_double_grave = 625;
public static final int accent_grave = 664;
public static final int accent_hook_above = 730;
public static final int accent_horn = 752;
public static final int accent_macron = 769;
public static final int accent_ogonek = 824;
public static final int accent_ordinal = 836;
public static final int accent_ring = 859;
public static final int accent_slash = 871;
public static final int accent_subscript = 911;
public static final int accent_superscript = 988;
public static final int accent_tilde = 1144;
public static final int accent_trema = 1172;
public static final int compose = 1270;
public static final int fn = 7447;
public static final int numpad_bengali = 7979;
public static final int numpad_devanagari = 8000;
public static final int numpad_gujarati = 8021;
public static final int numpad_hindu = 8042;
public static final int numpad_kannada = 8063;
public static final int numpad_persian = 8084;
public static final int numpad_tamil = 8105;
public static final int shift = 8126;
}

View file

@ -0,0 +1,348 @@
package juloo.keyboard2;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import androidx.window.layout.WindowInfoTracker;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import juloo.keyboard2.prefs.CustomExtraKeysPreference;
import juloo.keyboard2.prefs.ExtraKeysPreference;
import juloo.keyboard2.prefs.LayoutsPreference;
public final class Config
{
private final SharedPreferences _prefs;
// From resources
public final float marginTop;
public final float keyPadding;
public final float labelTextSize;
public final float sublabelTextSize;
// From preferences
/** [null] represent the [system] layout. */
public List<KeyboardData> layouts;
public boolean show_numpad = false;
// From the 'numpad_layout' option, also apply to the numeric pane.
public boolean inverse_numpad = false;
public boolean add_number_row;
public boolean number_row_symbols;
public float swipe_dist_px;
public float slide_step_px;
// Let the system handle vibration when false.
public boolean vibrate_custom;
// Control the vibration if [vibrate_custom] is true.
public long vibrate_duration;
public long longPressTimeout;
public long longPressInterval;
public boolean keyrepeat_enabled;
public float margin_bottom;
public int keyboardHeightPercent;
public int screenHeightPixels;
public float horizontal_margin;
public float key_vertical_margin;
public float key_horizontal_margin;
public int labelBrightness; // 0 - 255
public int keyboardOpacity; // 0 - 255
public float customBorderRadius; // 0 - 1
public float customBorderLineWidth; // dp
public int keyOpacity; // 0 - 255
public int keyActivatedOpacity; // 0 - 255
public boolean double_tap_lock_shift;
public float characterSize; // Ratio
public int theme; // Values are R.style.*
public boolean autocapitalisation;
public boolean switch_input_immediate;
public NumberLayout selected_number_layout;
public boolean borderConfig;
public int circle_sensitivity;
public boolean clipboard_history_enabled;
// Dynamically set
public boolean shouldOfferVoiceTyping;
public String actionLabel; // Might be 'null'
public int actionId; // Meaningful only when 'actionLabel' isn't 'null'
public boolean swapEnterActionKey; // Swap the "enter" and "action" keys
public ExtraKeys extra_keys_subtype;
public Map<KeyValue, KeyboardData.PreferredPos> extra_keys_param;
public Map<KeyValue, KeyboardData.PreferredPos> extra_keys_custom;
public final IKeyEventHandler handler;
public boolean orientation_landscape = false;
public boolean foldable_unfolded = false;
/** Index in 'layouts' of the currently used layout. See
[get_current_layout()] and [set_current_layout()]. */
int current_layout_portrait;
int current_layout_landscape;
int current_layout_unfolded_portrait;
int current_layout_unfolded_landscape;
private Config(SharedPreferences prefs, Resources res, IKeyEventHandler h, Boolean foldableUnfolded)
{
_prefs = prefs;
// static values
marginTop = res.getDimension(R.dimen.margin_top);
keyPadding = res.getDimension(R.dimen.key_padding);
labelTextSize = 0.33f;
sublabelTextSize = 0.22f;
// from prefs
refresh(res, foldableUnfolded);
// initialized later
shouldOfferVoiceTyping = false;
actionLabel = null;
actionId = 0;
swapEnterActionKey = false;
extra_keys_subtype = null;
handler = h;
}
/*
** Reload prefs
*/
public void refresh(Resources res, Boolean foldableUnfolded)
{
DisplayMetrics dm = res.getDisplayMetrics();
orientation_landscape = res.getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
foldable_unfolded = foldableUnfolded;
float characterSizeScale = 1.f;
String show_numpad_s = _prefs.getString("show_numpad", "never");
show_numpad = "always".equals(show_numpad_s);
if (orientation_landscape)
{
if ("landscape".equals(show_numpad_s))
show_numpad = true;
keyboardHeightPercent = _prefs.getInt(foldable_unfolded ? "keyboard_height_landscape_unfolded" : "keyboard_height_landscape", 50);
characterSizeScale = 1.25f;
}
else
{
keyboardHeightPercent = _prefs.getInt(foldable_unfolded ? "keyboard_height_unfolded" : "keyboard_height", 35);
}
layouts = LayoutsPreference.load_from_preferences(res, _prefs);
inverse_numpad = _prefs.getString("numpad_layout", "default").equals("low_first");
String number_row = _prefs.getString("number_row", "no_number_row");
add_number_row = !number_row.equals("no_number_row");
number_row_symbols = number_row.equals("symbols");
// The baseline for the swipe distance correspond to approximately the
// width of a key in portrait mode, as most layouts have 10 columns.
// Multipled by the DPI ratio because most swipes are made in the diagonals.
// The option value uses an unnamed scale where the baseline is around 25.
float dpi_ratio = Math.max(dm.xdpi, dm.ydpi) / Math.min(dm.xdpi, dm.ydpi);
float swipe_scaling = Math.min(dm.widthPixels, dm.heightPixels) / 10.f * dpi_ratio;
float swipe_dist_value = Float.valueOf(_prefs.getString("swipe_dist", "15"));
swipe_dist_px = swipe_dist_value / 25.f * swipe_scaling;
slide_step_px = 0.4f * swipe_scaling;
vibrate_custom = _prefs.getBoolean("vibrate_custom", false);
vibrate_duration = _prefs.getInt("vibrate_duration", 20);
longPressTimeout = _prefs.getInt("longpress_timeout", 600);
longPressInterval = _prefs.getInt("longpress_interval", 65);
keyrepeat_enabled = _prefs.getBoolean("keyrepeat_enabled", true);
margin_bottom = get_dip_pref_oriented(dm, "margin_bottom", 7, 3);
key_vertical_margin = get_dip_pref(dm, "key_vertical_margin", 1.5f) / 100;
key_horizontal_margin = get_dip_pref(dm, "key_horizontal_margin", 2) / 100;
// Label brightness is used as the alpha channel
labelBrightness = _prefs.getInt("label_brightness", 100) * 255 / 100;
// Keyboard opacity
keyboardOpacity = _prefs.getInt("keyboard_opacity", 100) * 255 / 100;
keyOpacity = _prefs.getInt("key_opacity", 100) * 255 / 100;
keyActivatedOpacity = _prefs.getInt("key_activated_opacity", 100) * 255 / 100;
// keyboard border settings
borderConfig = _prefs.getBoolean("border_config", false);
customBorderRadius = _prefs.getInt("custom_border_radius", 0) / 100.f;
customBorderLineWidth = get_dip_pref(dm, "custom_border_line_width", 0);
screenHeightPixels = dm.heightPixels;
horizontal_margin =
get_dip_pref_oriented(dm, "horizontal_margin", 3, 28);
double_tap_lock_shift = _prefs.getBoolean("lock_double_tap", false);
characterSize =
_prefs.getFloat("character_size", 1.15f)
* characterSizeScale;
theme = getThemeId(res, _prefs.getString("theme", ""));
autocapitalisation = _prefs.getBoolean("autocapitalisation", true);
switch_input_immediate = _prefs.getBoolean("switch_input_immediate", false);
extra_keys_param = ExtraKeysPreference.get_extra_keys(_prefs);
extra_keys_custom = CustomExtraKeysPreference.get(_prefs);
selected_number_layout = NumberLayout.of_string(_prefs.getString("number_entry_layout", "pin"));
current_layout_portrait = _prefs.getInt("current_layout_portrait", 0);
current_layout_landscape = _prefs.getInt("current_layout_landscape", 0);
current_layout_unfolded_portrait = _prefs.getInt("current_layout_unfolded_portrait", 0);
current_layout_unfolded_landscape = _prefs.getInt("current_layout_unfolded_landscape", 0);
circle_sensitivity = Integer.valueOf(_prefs.getString("circle_sensitivity", "2"));
clipboard_history_enabled = _prefs.getBoolean("clipboard_history_enabled", false);
}
public int get_current_layout()
{
if (foldable_unfolded) {
return (orientation_landscape)
? current_layout_unfolded_landscape : current_layout_unfolded_portrait;
} else {
return (orientation_landscape)
? current_layout_landscape : current_layout_portrait;
}
}
public void set_current_layout(int l)
{
if (foldable_unfolded) {
if (orientation_landscape)
current_layout_unfolded_landscape = l;
else
current_layout_unfolded_portrait = l;
} else {
if (orientation_landscape)
current_layout_landscape = l;
else
current_layout_portrait = l;
}
SharedPreferences.Editor e = _prefs.edit();
e.putInt("current_layout_portrait", current_layout_portrait);
e.putInt("current_layout_landscape", current_layout_landscape);
e.putInt("current_layout_unfolded_portrait", current_layout_unfolded_portrait);
e.putInt("current_layout_unfolded_landscape", current_layout_unfolded_landscape);
e.apply();
}
public void set_clipboard_history_enabled(boolean e)
{
clipboard_history_enabled = e;
_prefs.edit().putBoolean("clipboard_history_enabled", e).commit();
}
private float get_dip_pref(DisplayMetrics dm, String pref_name, float def)
{
float value;
try { value = _prefs.getInt(pref_name, -1); }
catch (Exception e) { value = _prefs.getFloat(pref_name, -1f); }
if (value < 0f)
value = def;
return (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, dm));
}
/** [get_dip_pref] depending on orientation. */
float get_dip_pref_oriented(DisplayMetrics dm, String pref_base_name, float def_port, float def_land)
{
final String suffix;
if (foldable_unfolded) {
suffix = orientation_landscape ? "_landscape_unfolded" : "_portrait_unfolded";
} else {
suffix = orientation_landscape ? "_landscape" : "_portrait";
}
float def = orientation_landscape ? def_land : def_port;
return get_dip_pref(dm, pref_base_name + suffix, def);
}
private int getThemeId(Resources res, String theme_name)
{
int night_mode = res.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
switch (theme_name)
{
case "light": return R.style.Light;
case "black": return R.style.Black;
case "altblack": return R.style.AltBlack;
case "dark": return R.style.Dark;
case "white": return R.style.White;
case "epaper": return R.style.ePaper;
case "desert": return R.style.Desert;
case "jungle": return R.style.Jungle;
case "monetlight": return R.style.MonetLight;
case "monetdark": return R.style.MonetDark;
case "monet":
if ((night_mode & Configuration.UI_MODE_NIGHT_NO) != 0)
return R.style.MonetLight;
return R.style.MonetDark;
case "rosepine": return R.style.RosePine;
default:
case "system":
if ((night_mode & Configuration.UI_MODE_NIGHT_NO) != 0)
return R.style.Light;
return R.style.Dark;
}
}
private static Config _globalConfig = null;
public static void initGlobalConfig(SharedPreferences prefs, Resources res,
IKeyEventHandler handler, Boolean foldableUnfolded)
{
migrate(prefs);
_globalConfig = new Config(prefs, res, handler, foldableUnfolded);
LayoutModifier.init(_globalConfig, res);
}
public static Config globalConfig()
{
return _globalConfig;
}
public static SharedPreferences globalPrefs()
{
return _globalConfig._prefs;
}
public static interface IKeyEventHandler
{
public void key_down(KeyValue value, boolean is_swipe);
public void key_up(KeyValue value, Pointers.Modifiers mods);
public void mods_changed(Pointers.Modifiers mods);
}
/** Config migrations. */
private static int CONFIG_VERSION = 3;
public static void migrate(SharedPreferences prefs)
{
int saved_version = prefs.getInt("version", 0);
Logs.debug_config_migration(saved_version, CONFIG_VERSION);
if (saved_version == CONFIG_VERSION)
return;
SharedPreferences.Editor e = prefs.edit();
e.putInt("version", CONFIG_VERSION);
// Migrations might run on an empty [prefs] for new installs, in this case
// they set the default values of complex options.
switch (saved_version)
{
case 0:
// Primary, secondary and custom layout options are merged into the new
// Layouts option. This also sets the default value.
List<LayoutsPreference.Layout> l = new ArrayList<LayoutsPreference.Layout>();
l.add(migrate_layout(prefs.getString("layout", "system")));
String snd_layout = prefs.getString("second_layout", "none");
if (snd_layout != null && !snd_layout.equals("none"))
l.add(migrate_layout(snd_layout));
String custom_layout = prefs.getString("custom_layout", "");
if (custom_layout != null && !custom_layout.equals(""))
l.add(LayoutsPreference.CustomLayout.parse(custom_layout));
LayoutsPreference.save_to_preferences(e, l);
// Fallthrough
case 1:
boolean add_number_row = prefs.getBoolean("number_row", false);
e.putString("number_row", add_number_row ? "no_symbols" : "no_number_row");
// Fallthrough
case 2:
if (!prefs.contains("number_entry_layout")) {
e.putString("number_entry_layout", prefs.getBoolean("pin_entry_enabled", true) ? "pin" : "number");
}
// Fallthrough
case 3:
default: break;
}
e.apply();
}
private static LayoutsPreference.Layout migrate_layout(String name)
{
if (name == null || name.equals("system"))
return new LayoutsPreference.SystemLayout();
return new LayoutsPreference.NamedLayout(name);
}
}

View file

@ -0,0 +1,138 @@
package juloo.keyboard2;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Handler;
import android.text.InputType;
import android.text.Layout;
import android.widget.EditText;
public class CustomLayoutEditDialog
{
/** Dialog for specifying a custom layout. [initial_text] is the layout
description when modifying a layout. */
public static void show(Context ctx, String initial_text,
boolean allow_remove, final Callback callback)
{
final LayoutEntryEditText input = new LayoutEntryEditText(ctx);
input.setText(initial_text);
AlertDialog.Builder dialog = new AlertDialog.Builder(ctx)
.setView(input)
.setTitle(R.string.pref_custom_layout_title)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener(){
public void onClick(DialogInterface _dialog, int _which)
{
callback.select(input.getText().toString());
}
})
.setNegativeButton(android.R.string.cancel, null);
// Might be true when modifying an existing layout
if (allow_remove)
dialog.setNeutralButton(R.string.pref_layouts_remove_custom, new DialogInterface.OnClickListener(){
public void onClick(DialogInterface _dialog, int _which)
{
callback.select(null);
}
});
input.set_on_text_change(new LayoutEntryEditText.OnChangeListener()
{
public void on_change()
{
String error = callback.validate(input.getText().toString());
input.setError(error);
}
});
dialog.show();
}
public interface Callback
{
/** The entered text when the user clicks "OK", [null] when the user
cancels editing. */
public void select(String text);
/** Return a human readable error string if the [text] contains an error.
Return [null] otherwise. The error string will be displayed atop the
input box. This method is called everytime the text changes. */
public String validate(String text);
}
/** An editable text view that shows line numbers. */
static class LayoutEntryEditText extends EditText
{
/** Used to draw line numbers. */
Paint _ln_paint;
OnChangeListener _on_change_listener = null;
/** Delay validation to when user stops typing for a second. */
Handler _on_change_throttler;
Runnable _on_change_delayed = new Runnable()
{
public void run()
{
OnChangeListener l = LayoutEntryEditText.this._on_change_listener;
if (l != null)
l.on_change();
}
};
public LayoutEntryEditText(Context ctx)
{
super(ctx);
_ln_paint = new Paint(getPaint());
_ln_paint.setTextSize(_ln_paint.getTextSize() * 0.8f);
setHorizontallyScrolling(true);
setInputType(InputType.TYPE_CLASS_TEXT
| InputType.TYPE_TEXT_FLAG_MULTI_LINE);
_on_change_throttler = new Handler(ctx.getMainLooper());
}
public void set_on_text_change(OnChangeListener l)
{
_on_change_listener = l;
}
@Override
protected void onDraw(Canvas canvas)
{
float digit_width = _ln_paint.measureText("0");
int line_count = getLineCount();
// Extra '+ 1' serves as padding.
setPadding((int)(((int)Math.log10(line_count) + 1 + 1) * digit_width), 0, 0, 0);
super.onDraw(canvas);
_ln_paint.setColor(getPaint().getColor());
Rect clip_bounds = canvas.getClipBounds();
Layout layout = getLayout();
int offset = clip_bounds.left + (int)(digit_width / 2.f);
int line = layout.getLineForVertical(clip_bounds.top);
int skipped = line;
while (line < line_count)
{
int baseline = getLineBounds(line, null);
canvas.drawText(String.valueOf(line), offset, baseline, _ln_paint);
line++;
if (baseline >= clip_bounds.bottom)
break;
}
}
@Override
protected void onTextChanged(CharSequence text, int _start, int _lengthBefore, int _lengthAfter)
{
if (_on_change_throttler != null)
{
_on_change_throttler.removeCallbacks(_on_change_delayed);
_on_change_throttler.postDelayed(_on_change_delayed, 1000);
}
}
public static interface OnChangeListener
{
public void on_change();
}
}
}

View file

@ -0,0 +1,88 @@
package juloo.keyboard2;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build.VERSION;
import android.preference.PreferenceManager;
import java.util.Map;
import java.util.Set;
@TargetApi(24)
public final class DirectBootAwarePreferences
{
/* On API >= 24, preferences are read from the device protected storage. This
* storage is less protected than the default, no personnal or sensitive
* information is stored there (only the keyboard settings). This storage is
* accessible during boot and allow the keyboard to read its settings and
* allow typing the storage password. */
public static SharedPreferences get_shared_preferences(Context context)
{
if (VERSION.SDK_INT < 24)
return PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences prefs = get_protected_prefs(context);
check_need_migration(context, prefs);
return prefs;
}
/* Copy shared preferences to device protected storage. Not using
* [Context.moveSharedPreferencesFrom] because the settings activity still
* use [PreferenceActivity], which can't work on a non-default shared
* preference file. */
public static void copy_preferences_to_protected_storage(Context context,
SharedPreferences src)
{
if (VERSION.SDK_INT >= 24)
copy_shared_preferences(src, get_protected_prefs(context));
}
static SharedPreferences get_protected_prefs(Context context)
{
String pref_name =
PreferenceManager.getDefaultSharedPreferencesName(context);
return context.createDeviceProtectedStorageContext()
.getSharedPreferences(pref_name, Context.MODE_PRIVATE);
}
static void check_need_migration(Context app_context,
SharedPreferences protected_prefs)
{
if (!protected_prefs.getBoolean("need_migration", true))
return;
SharedPreferences prefs;
try
{
prefs = PreferenceManager.getDefaultSharedPreferences(app_context);
}
catch (Exception e)
{
// Device is locked, migrate later.
return;
}
prefs.edit().putBoolean("need_migration", false).apply();
copy_shared_preferences(prefs, protected_prefs);
}
static void copy_shared_preferences(SharedPreferences src, SharedPreferences dst)
{
SharedPreferences.Editor e = dst.edit();
Map<String, ?> entries = src.getAll();
for (String k : entries.keySet())
{
Object v = entries.get(k);
if (v instanceof Boolean)
e.putBoolean(k, (Boolean)v);
else if (v instanceof Float)
e.putFloat(k, (Float)v);
else if (v instanceof Integer)
e.putInt(k, (Integer)v);
else if (v instanceof Long)
e.putLong(k, (Long)v);
else if (v instanceof String)
e.putString(k, (String)v);
else if (v instanceof Set)
e.putStringSet(k, (Set<String>)v);
}
e.apply();
}
}

View file

@ -0,0 +1,794 @@
package juloo.keyboard2;
import android.content.res.Resources;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.BufferedReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class Emoji
{
private final KeyValue _kv;
protected Emoji(String bytecode)
{
this._kv = new KeyValue(bytecode, KeyValue.Kind.String, 0, 0);
}
public KeyValue kv()
{
return _kv;
}
private final static List<Emoji> _all = new ArrayList<>();
private final static List<List<Emoji>> _groups = new ArrayList<>();
private final static HashMap<String, Emoji> _stringMap = new HashMap<>();
public static void init(Resources res)
{
if (!_all.isEmpty())
return;
try
{
InputStream inputStream = res.openRawResource(R.raw.emojis);
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
// Read emoji (until empty line)
while (!(line = reader.readLine()).isEmpty())
{
Emoji e = new Emoji(line);
_all.add(e);
_stringMap.put(line, e);
}
// Read group indices
if ((line = reader.readLine()) != null)
{
String[] tokens = line.split(" ");
int last = 0;
for (int i = 1; i < tokens.length; i++)
{
int next = Integer.parseInt(tokens[i]);
_groups.add(_all.subList(last, next));
last = next;
}
_groups.add(_all.subList(last, _all.size()));
}
}
catch (IOException e) { Logs.exn("Emoji.init() failed", e); }
}
public static int getNumGroups()
{
return _groups.size();
}
public static List<Emoji> getEmojisByGroup(int groupIndex)
{
return _groups.get(groupIndex);
}
public static Emoji getEmojiByString(String value)
{
return _stringMap.get(value);
}
public static String mapOldNameToValue(String name) throws IllegalArgumentException
{
if (name.matches(":(u[a-fA-F0-9]{4,5})+:"))
{
StringBuilder sb = new StringBuilder();
for (String code : name.replace(":", "").substring(1).split("u"))
{
try
{
sb.append(Character.toChars(Integer.decode("0X" + code)));
}
catch (IllegalArgumentException e)
{
throw new IllegalArgumentException("Failed to parse codepoint '" + code + "' in name '" + name + "'", e);
}
}
return sb.toString();
}
switch (name)
{
case ":grinning:": return "😀";
case ":smiley:": return "😃";
case ":smile:": return "😄";
case ":grin:": return "😁";
case ":satisfied:": return "😆";
case ":sweat_smile:": return "😅";
case ":joy:": return "😂";
case ":wink:": return "😉";
case ":blush:": return "😊";
case ":innocent:": return "😇";
case ":heart_eyes:": return "😍";
case ":kissing_heart:": return "😘";
case ":kissing:": return "😗";
case ":kissing_closed_eyes:": return "😚";
case ":kissing_smiling_eyes:": return "😙";
case ":yum:": return "😋";
case ":stuck_out_tongue:": return "😛";
case ":stuck_out_tongue_winking_eye:": return "😜";
case ":stuck_out_tongue_closed_eyes:": return "😝";
case ":neutral_face:": return "😐";
case ":expressionless:": return "😑";
case ":no_mouth:": return "😶";
case ":smirk:": return "😏";
case ":unamused:": return "😒";
case ":grimacing:": return "😬";
case ":relieved:": return "😌";
case ":pensive:": return "😔";
case ":sleepy:": return "😪";
case ":sleeping:": return "😴";
case ":mask:": return "😷";
case ":dizzy_face:": return "😵";
case ":sunglasses:": return "😎";
case ":confused:": return "😕";
case ":worried:": return "😟";
case ":open_mouth:": return "😮";
case ":hushed:": return "😯";
case ":astonished:": return "😲";
case ":flushed:": return "😳";
case ":frowning:": return "😦";
case ":anguished:": return "😧";
case ":fearful:": return "😨";
case ":cold_sweat:": return "😰";
case ":disappointed_relieved:": return "😥";
case ":cry:": return "😢";
case ":sob:": return "😭";
case ":scream:": return "😱";
case ":confounded:": return "😖";
case ":persevere:": return "😣";
case ":disappointed:": return "😞";
case ":sweat:": return "😓";
case ":weary:": return "😩";
case ":tired_face:": return "😫";
case ":triumph:": return "😤";
case ":rage:": return "😡";
case ":angry:": return "😠";
case ":smiling_imp:": return "😈";
case ":imp:": return "👿";
case ":skull:": return "💀";
case ":shit:": return "💩";
case ":japanese_ogre:": return "👹";
case ":japanese_goblin:": return "👺";
case ":ghost:": return "👻";
case ":alien:": return "👽";
case ":space_invader:": return "👾";
case ":smiley_cat:": return "😺";
case ":smile_cat:": return "😸";
case ":joy_cat:": return "😹";
case ":heart_eyes_cat:": return "😻";
case ":smirk_cat:": return "😼";
case ":kissing_cat:": return "😽";
case ":scream_cat:": return "🙀";
case ":crying_cat_face:": return "😿";
case ":pouting_cat:": return "😾";
case ":see_no_evil:": return "🙈";
case ":hear_no_evil:": return "🙉";
case ":speak_no_evil:": return "🙊";
case ":kiss:": return "💋";
case ":love_letter:": return "💌";
case ":cupid:": return "💘";
case ":gift_heart:": return "💝";
case ":sparkling_heart:": return "💖";
case ":heartpulse:": return "💗";
case ":heartbeat:": return "💓";
case ":revolving_hearts:": return "💞";
case ":two_hearts:": return "💕";
case ":heart_decoration:": return "💟";
case ":broken_heart:": return "💔";
case ":yellow_heart:": return "💛";
case ":green_heart:": return "💚";
case ":blue_heart:": return "💙";
case ":purple_heart:": return "💜";
case ":100:": return "💯";
case ":anger:": return "💢";
case ":collision:": return "💥";
case ":dizzy:": return "💫";
case ":sweat_drops:": return "💦";
case ":dash:": return "💨";
case ":bomb:": return "💣";
case ":speech_balloon:": return "💬";
case ":thought_balloon:": return "💭";
case ":zzz:": return "💤";
case ":wave:": return "👋";
case ":ok_hand:": return "👌";
case ":point_left:": return "👈";
case ":point_right:": return "👉";
case ":point_up_2:": return "👆";
case ":point_down:": return "👇";
case ":thumbsup:": return "👍";
case ":thumbsdown:": return "👎";
case ":punch:": return "👊";
case ":clap:": return "👏";
case ":raised_hands:": return "🙌";
case ":open_hands:": return "👐";
case ":pray:": return "🙏";
case ":nail_care:": return "💅";
case ":muscle:": return "💪";
case ":ear:": return "👂";
case ":nose:": return "👃";
case ":eyes:": return "👀";
case ":tongue:": return "👅";
case ":lips:": return "👄";
case ":baby:": return "👶";
case ":boy:": return "👦";
case ":girl:": return "👧";
case ":person_with_blond_hair:": return "👱";
case ":man:": return "👨";
case ":woman:": return "👩";
case ":older_man:": return "👴";
case ":older_woman:": return "👵";
case ":person_frowning:": return "🙍";
case ":person_with_pouting_face:": return "🙎";
case ":no_good:": return "🙅";
case ":ok_woman:": return "🙆";
case ":information_desk_person:": return "💁";
case ":raising_hand:": return "🙋";
case ":bow:": return "🙇";
case ":cop:": return "👮";
case ":guardsman:": return "💂";
case ":construction_worker:": return "👷";
case ":princess:": return "👸";
case ":man_with_turban:": return "👳";
case ":man_with_gua_pi_mao:": return "👲";
case ":bride_with_veil:": return "👰";
case ":angel:": return "👼";
case ":santa:": return "🎅";
case ":massage:": return "💆";
case ":haircut:": return "💇";
case ":walking:": return "🚶";
case ":running:": return "🏃";
case ":dancer:": return "💃";
case ":dancers:": return "👯";
case ":horse_racing:": return "🏇";
case ":snowboarder:": return "🏂";
case ":surfer:": return "🏄";
case ":rowboat:": return "🚣";
case ":swimmer:": return "🏊";
case ":bicyclist:": return "🚴";
case ":mountain_bicyclist:": return "🚵";
case ":bath:": return "🛀";
case ":two_women_holding_hands:": return "👭";
case ":couple:": return "👫";
case ":two_men_holding_hands:": return "👬";
case ":couplekiss:": return "💏";
case ":couple_with_heart:": return "💑";
case ":family:": return "👪";
case ":bust_in_silhouette:": return "👤";
case ":busts_in_silhouette:": return "👥";
case ":footprints:": return "👣";
case ":monkey_face:": return "🐵";
case ":monkey:": return "🐒";
case ":dog:": return "🐶";
case ":dog2:": return "🐕";
case ":poodle:": return "🐩";
case ":wolf:": return "🐺";
case ":cat:": return "🐱";
case ":cat2:": return "🐈";
case ":tiger:": return "🐯";
case ":tiger2:": return "🐅";
case ":leopard:": return "🐆";
case ":horse:": return "🐴";
case ":racehorse:": return "🐎";
case ":cow:": return "🐮";
case ":ox:": return "🐂";
case ":water_buffalo:": return "🐃";
case ":cow2:": return "🐄";
case ":pig:": return "🐷";
case ":pig2:": return "🐖";
case ":boar:": return "🐗";
case ":pig_nose:": return "🐽";
case ":ram:": return "🐏";
case ":sheep:": return "🐑";
case ":goat:": return "🐐";
case ":dromedary_camel:": return "🐪";
case ":camel:": return "🐫";
case ":elephant:": return "🐘";
case ":mouse:": return "🐭";
case ":mouse2:": return "🐁";
case ":rat:": return "🐀";
case ":hamster:": return "🐹";
case ":rabbit:": return "🐰";
case ":rabbit2:": return "🐇";
case ":bear:": return "🐻";
case ":koala:": return "🐨";
case ":panda_face:": return "🐼";
case ":paw_prints:": return "🐾";
case ":chicken:": return "🐔";
case ":rooster:": return "🐓";
case ":hatching_chick:": return "🐣";
case ":baby_chick:": return "🐤";
case ":hatched_chick:": return "🐥";
case ":bird:": return "🐦";
case ":penguin:": return "🐧";
case ":frog:": return "🐸";
case ":crocodile:": return "🐊";
case ":turtle:": return "🐢";
case ":snake:": return "🐍";
case ":dragon_face:": return "🐲";
case ":dragon:": return "🐉";
case ":whale:": return "🐳";
case ":whale2:": return "🐋";
case ":flipper:": return "🐬";
case ":fish:": return "🐟";
case ":tropical_fish:": return "🐠";
case ":blowfish:": return "🐡";
case ":octopus:": return "🐙";
case ":shell:": return "🐚";
case ":snail:": return "🐌";
case ":bug:": return "🐛";
case ":ant:": return "🐜";
case ":honeybee:": return "🐝";
case ":beetle:": return "🐞";
case ":bouquet:": return "💐";
case ":cherry_blossom:": return "🌸";
case ":white_flower:": return "💮";
case ":rose:": return "🌹";
case ":hibiscus:": return "🌺";
case ":sunflower:": return "🌻";
case ":blossom:": return "🌼";
case ":tulip:": return "🌷";
case ":seedling:": return "🌱";
case ":evergreen_tree:": return "🌲";
case ":deciduous_tree:": return "🌳";
case ":palm_tree:": return "🌴";
case ":cactus:": return "🌵";
case ":ear_of_rice:": return "🌾";
case ":herb:": return "🌿";
case ":four_leaf_clover:": return "🍀";
case ":maple_leaf:": return "🍁";
case ":fallen_leaf:": return "🍂";
case ":leaves:": return "🍃";
case ":grapes:": return "🍇";
case ":melon:": return "🍈";
case ":watermelon:": return "🍉";
case ":tangerine:": return "🍊";
case ":lemon:": return "🍋";
case ":banana:": return "🍌";
case ":pineapple:": return "🍍";
case ":apple:": return "🍎";
case ":green_apple:": return "🍏";
case ":pear:": return "🍐";
case ":peach:": return "🍑";
case ":cherries:": return "🍒";
case ":strawberry:": return "🍓";
case ":tomato:": return "🍅";
case ":eggplant:": return "🍆";
case ":corn:": return "🌽";
case ":mushroom:": return "🍄";
case ":chestnut:": return "🌰";
case ":bread:": return "🍞";
case ":meat_on_bone:": return "🍖";
case ":poultry_leg:": return "🍗";
case ":hamburger:": return "🍔";
case ":fries:": return "🍟";
case ":pizza:": return "🍕";
case ":egg:": return "🍳";
case ":stew:": return "🍲";
case ":bento:": return "🍱";
case ":rice_cracker:": return "🍘";
case ":rice_ball:": return "🍙";
case ":rice:": return "🍚";
case ":curry:": return "🍛";
case ":ramen:": return "🍜";
case ":spaghetti:": return "🍝";
case ":sweet_potato:": return "🍠";
case ":oden:": return "🍢";
case ":sushi:": return "🍣";
case ":fried_shrimp:": return "🍤";
case ":fish_cake:": return "🍥";
case ":dango:": return "🍡";
case ":icecream:": return "🍦";
case ":shaved_ice:": return "🍧";
case ":ice_cream:": return "🍨";
case ":doughnut:": return "🍩";
case ":cookie:": return "🍪";
case ":birthday:": return "🎂";
case ":cake:": return "🍰";
case ":chocolate_bar:": return "🍫";
case ":candy:": return "🍬";
case ":lollipop:": return "🍭";
case ":custard:": return "🍮";
case ":honey_pot:": return "🍯";
case ":baby_bottle:": return "🍼";
case ":tea:": return "🍵";
case ":sake:": return "🍶";
case ":wine_glass:": return "🍷";
case ":cocktail:": return "🍸";
case ":tropical_drink:": return "🍹";
case ":beer:": return "🍺";
case ":beers:": return "🍻";
case ":fork_and_knife:": return "🍴";
case ":hocho:": return "🔪";
case ":earth_africa:": return "🌍";
case ":earth_americas:": return "🌎";
case ":earth_asia:": return "🌏";
case ":globe_with_meridians:": return "🌐";
case ":japan:": return "🗾";
case ":volcano:": return "🌋";
case ":mount_fuji:": return "🗻";
case ":house:": return "🏠";
case ":house_with_garden:": return "🏡";
case ":office:": return "🏢";
case ":post_office:": return "🏣";
case ":european_post_office:": return "🏤";
case ":hospital:": return "🏥";
case ":bank:": return "🏦";
case ":hotel:": return "🏨";
case ":love_hotel:": return "🏩";
case ":convenience_store:": return "🏪";
case ":school:": return "🏫";
case ":department_store:": return "🏬";
case ":factory:": return "🏭";
case ":japanese_castle:": return "🏯";
case ":european_castle:": return "🏰";
case ":wedding:": return "💒";
case ":tokyo_tower:": return "🗼";
case ":statue_of_liberty:": return "🗽";
case ":foggy:": return "🌁";
case ":stars:": return "🌃";
case ":sunrise_over_mountains:": return "🌄";
case ":sunrise:": return "🌅";
case ":city_sunset:": return "🌆";
case ":city_sunrise:": return "🌇";
case ":bridge_at_night:": return "🌉";
case ":carousel_horse:": return "🎠";
case ":ferris_wheel:": return "🎡";
case ":roller_coaster:": return "🎢";
case ":barber:": return "💈";
case ":circus_tent:": return "🎪";
case ":steam_locomotive:": return "🚂";
case ":train:": return "🚃";
case ":bullettrain_side:": return "🚄";
case ":bullettrain_front:": return "🚅";
case ":train2:": return "🚆";
case ":metro:": return "🚇";
case ":light_rail:": return "🚈";
case ":station:": return "🚉";
case ":tram:": return "🚊";
case ":monorail:": return "🚝";
case ":mountain_railway:": return "🚞";
case ":bus:": return "🚌";
case ":oncoming_bus:": return "🚍";
case ":trolleybus:": return "🚎";
case ":minibus:": return "🚐";
case ":ambulance:": return "🚑";
case ":fire_engine:": return "🚒";
case ":police_car:": return "🚓";
case ":oncoming_police_car:": return "🚔";
case ":taxi:": return "🚕";
case ":oncoming_taxi:": return "🚖";
case ":red_car:": return "🚗";
case ":oncoming_automobile:": return "🚘";
case ":blue_car:": return "🚙";
case ":truck:": return "🚚";
case ":articulated_lorry:": return "🚛";
case ":tractor:": return "🚜";
case ":bike:": return "🚲";
case ":busstop:": return "🚏";
case ":rotating_light:": return "🚨";
case ":traffic_light:": return "🚥";
case ":vertical_traffic_light:": return "🚦";
case ":construction:": return "🚧";
case ":speedboat:": return "🚤";
case ":ship:": return "🚢";
case ":seat:": return "💺";
case ":helicopter:": return "🚁";
case ":suspension_railway:": return "🚟";
case ":mountain_cableway:": return "🚠";
case ":aerial_tramway:": return "🚡";
case ":rocket:": return "🚀";
case ":clock12:": return "🕛";
case ":clock1230:": return "🕧";
case ":clock1:": return "🕐";
case ":clock130:": return "🕜";
case ":clock2:": return "🕑";
case ":clock230:": return "🕝";
case ":clock3:": return "🕒";
case ":clock330:": return "🕞";
case ":clock4:": return "🕓";
case ":clock430:": return "🕟";
case ":clock5:": return "🕔";
case ":clock530:": return "🕠";
case ":clock6:": return "🕕";
case ":clock630:": return "🕡";
case ":clock7:": return "🕖";
case ":clock730:": return "🕢";
case ":clock8:": return "🕗";
case ":clock830:": return "🕣";
case ":clock9:": return "🕘";
case ":clock930:": return "🕤";
case ":clock10:": return "🕙";
case ":clock1030:": return "🕥";
case ":clock11:": return "🕚";
case ":clock1130:": return "🕦";
case ":new_moon:": return "🌑";
case ":waxing_crescent_moon:": return "🌒";
case ":first_quarter_moon:": return "🌓";
case ":waxing_gibbous_moon:": return "🌔";
case ":full_moon:": return "🌕";
case ":waning_gibbous_moon:": return "🌖";
case ":last_quarter_moon:": return "🌗";
case ":waning_crescent_moon:": return "🌘";
case ":crescent_moon:": return "🌙";
case ":new_moon_with_face:": return "🌚";
case ":first_quarter_moon_with_face:": return "🌛";
case ":last_quarter_moon_with_face:": return "🌜";
case ":full_moon_with_face:": return "🌝";
case ":sun_with_face:": return "🌞";
case ":star2:": return "🌟";
case ":milky_way:": return "🌌";
case ":cyclone:": return "🌀";
case ":rainbow:": return "🌈";
case ":closed_umbrella:": return "🌂";
case ":fire:": return "🔥";
case ":droplet:": return "💧";
case ":ocean:": return "🌊";
case ":jack_o_lantern:": return "🎃";
case ":christmas_tree:": return "🎄";
case ":fireworks:": return "🎆";
case ":sparkler:": return "🎇";
case ":balloon:": return "🎈";
case ":tada:": return "🎉";
case ":confetti_ball:": return "🎊";
case ":tanabata_tree:": return "🎋";
case ":bamboo:": return "🎍";
case ":dolls:": return "🎎";
case ":flags:": return "🎏";
case ":wind_chime:": return "🎐";
case ":rice_scene:": return "🎑";
case ":ribbon:": return "🎀";
case ":gift:": return "🎁";
case ":ticket:": return "🎫";
case ":trophy:": return "🏆";
case ":basketball:": return "🏀";
case ":football:": return "🏈";
case ":rugby_football:": return "🏉";
case ":tennis:": return "🎾";
case ":bowling:": return "🎳";
case ":fishing_pole_and_fish:": return "🎣";
case ":running_shirt_with_sash:": return "🎽";
case ":ski:": return "🎿";
case ":dart:": return "🎯";
case ":8ball:": return "🎱";
case ":crystal_ball:": return "🔮";
case ":video_game:": return "🎮";
case ":slot_machine:": return "🎰";
case ":game_die:": return "🎲";
case ":black_joker:": return "🃏";
case ":mahjong:": return "🀄";
case ":flower_playing_cards:": return "🎴";
case ":performing_arts:": return "🎭";
case ":art:": return "🎨";
case ":eyeglasses:": return "👓";
case ":necktie:": return "👔";
case ":tshirt:": return "👕";
case ":jeans:": return "👖";
case ":dress:": return "👗";
case ":kimono:": return "👘";
case ":bikini:": return "👙";
case ":womans_clothes:": return "👚";
case ":purse:": return "👛";
case ":handbag:": return "👜";
case ":pouch:": return "👝";
case ":school_satchel:": return "🎒";
case ":shoe:": return "👞";
case ":athletic_shoe:": return "👟";
case ":high_heel:": return "👠";
case ":sandal:": return "👡";
case ":boot:": return "👢";
case ":crown:": return "👑";
case ":womans_hat:": return "👒";
case ":tophat:": return "🎩";
case ":mortar_board:": return "🎓";
case ":lipstick:": return "💄";
case ":ring:": return "💍";
case ":gem:": return "💎";
case ":mute:": return "🔇";
case ":sound:": return "🔉";
case ":speaker:": return "🔊";
case ":loudspeaker:": return "📢";
case ":mega:": return "📣";
case ":postal_horn:": return "📯";
case ":bell:": return "🔔";
case ":no_bell:": return "🔕";
case ":musical_score:": return "🎼";
case ":musical_note:": return "🎵";
case ":notes:": return "🎶";
case ":microphone:": return "🎤";
case ":headphones:": return "🎧";
case ":radio:": return "📻";
case ":saxophone:": return "🎷";
case ":guitar:": return "🎸";
case ":musical_keyboard:": return "🎹";
case ":trumpet:": return "🎺";
case ":violin:": return "🎻";
case ":iphone:": return "📱";
case ":calling:": return "📲";
case ":telephone_receiver:": return "📞";
case ":pager:": return "📟";
case ":fax:": return "📠";
case ":battery:": return "🔋";
case ":electric_plug:": return "🔌";
case ":computer:": return "💻";
case ":minidisc:": return "💽";
case ":floppy_disk:": return "💾";
case ":cd:": return "💿";
case ":dvd:": return "📀";
case ":movie_camera:": return "🎥";
case ":clapper:": return "🎬";
case ":tv:": return "📺";
case ":camera:": return "📷";
case ":video_camera:": return "📹";
case ":vhs:": return "📼";
case ":mag:": return "🔍";
case ":mag_right:": return "🔎";
case ":bulb:": return "💡";
case ":flashlight:": return "🔦";
case ":lantern:": return "🏮";
case ":notebook_with_decorative_cover:": return "📔";
case ":closed_book:": return "📕";
case ":open_book:": return "📖";
case ":green_book:": return "📗";
case ":blue_book:": return "📘";
case ":orange_book:": return "📙";
case ":books:": return "📚";
case ":notebook:": return "📓";
case ":ledger:": return "📒";
case ":page_with_curl:": return "📃";
case ":scroll:": return "📜";
case ":page_facing_up:": return "📄";
case ":newspaper:": return "📰";
case ":bookmark_tabs:": return "📑";
case ":bookmark:": return "🔖";
case ":moneybag:": return "💰";
case ":yen:": return "💴";
case ":dollar:": return "💵";
case ":euro:": return "💶";
case ":pound:": return "💷";
case ":money_with_wings:": return "💸";
case ":credit_card:": return "💳";
case ":chart:": return "💹";
case ":e-mail:": return "📧";
case ":incoming_envelope:": return "📨";
case ":envelope_with_arrow:": return "📩";
case ":outbox_tray:": return "📤";
case ":inbox_tray:": return "📥";
case ":package:": return "📦";
case ":mailbox:": return "📫";
case ":mailbox_closed:": return "📪";
case ":mailbox_with_mail:": return "📬";
case ":mailbox_with_no_mail:": return "📭";
case ":postbox:": return "📮";
case ":pencil:": return "📝";
case ":briefcase:": return "💼";
case ":file_folder:": return "📁";
case ":open_file_folder:": return "📂";
case ":date:": return "📅";
case ":calendar:": return "📆";
case ":card_index:": return "📇";
case ":chart_with_upwards_trend:": return "📈";
case ":chart_with_downwards_trend:": return "📉";
case ":bar_chart:": return "📊";
case ":clipboard:": return "📋";
case ":pushpin:": return "📌";
case ":round_pushpin:": return "📍";
case ":paperclip:": return "📎";
case ":straight_ruler:": return "📏";
case ":triangular_ruler:": return "📐";
case ":lock:": return "🔒";
case ":lock_with_ink_pen:": return "🔏";
case ":closed_lock_with_key:": return "🔐";
case ":key:": return "🔑";
case ":hammer:": return "🔨";
case ":gun:": return "🔫";
case ":wrench:": return "🔧";
case ":nut_and_bolt:": return "🔩";
case ":link:": return "🔗";
case ":microscope:": return "🔬";
case ":telescope:": return "🔭";
case ":satellite:": return "📡";
case ":syringe:": return "💉";
case ":pill:": return "💊";
case ":door:": return "🚪";
case ":toilet:": return "🚽";
case ":shower:": return "🚿";
case ":bathtub:": return "🛁";
case ":smoking:": return "🚬";
case ":moyai:": return "🗿";
case ":atm:": return "🏧";
case ":put_litter_in_its_place:": return "🚮";
case ":potable_water:": return "🚰";
case ":mens:": return "🚹";
case ":womens:": return "🚺";
case ":restroom:": return "🚻";
case ":baby_symbol:": return "🚼";
case ":wc:": return "🚾";
case ":passport_control:": return "🛂";
case ":customs:": return "🛃";
case ":baggage_claim:": return "🛄";
case ":left_luggage:": return "🛅";
case ":children_crossing:": return "🚸";
case ":no_entry_sign:": return "🚫";
case ":no_bicycles:": return "🚳";
case ":no_smoking:": return "🚭";
case ":do_not_litter:": return "🚯";
case ":non-potable_water:": return "🚱";
case ":no_pedestrians:": return "🚷";
case ":no_mobile_phones:": return "📵";
case ":underage:": return "🔞";
case ":arrows_clockwise:": return "🔃";
case ":arrows_counterclockwise:": return "🔄";
case ":back:": return "🔙";
case ":end:": return "🔚";
case ":on:": return "🔛";
case ":soon:": return "🔜";
case ":top:": return "🔝";
case ":six_pointed_star:": return "🔯";
case ":twisted_rightwards_arrows:": return "🔀";
case ":repeat:": return "🔁";
case ":repeat_one:": return "🔂";
case ":arrow_up_small:": return "🔼";
case ":arrow_down_small:": return "🔽";
case ":cinema:": return "🎦";
case ":low_brightness:": return "🔅";
case ":high_brightness:": return "🔆";
case ":signal_strength:": return "📶";
case ":vibration_mode:": return "📳";
case ":mobile_phone_off:": return "📴";
case ":currency_exchange:": return "💱";
case ":heavy_dollar_sign:": return "💲";
case ":trident:": return "🔱";
case ":name_badge:": return "📛";
case ":beginner:": return "🔰";
case ":keycap_ten:": return "🔟";
case ":capital_abcd:": return "🔠";
case ":abcd:": return "🔡";
case ":1234:": return "🔢";
case ":symbols:": return "🔣";
case ":abc:": return "🔤";
case ":ab:": return "🆎";
case ":cl:": return "🆑";
case ":cool:": return "🆒";
case ":free:": return "🆓";
case ":id:": return "🆔";
case ":new:": return "🆕";
case ":ng:": return "🆖";
case ":ok:": return "🆗";
case ":sos:": return "🆘";
case ":up:": return "🆙";
case ":vs:": return "🆚";
case ":koko:": return "🈁";
case ":ideograph_advantage:": return "🉐";
case ":accept:": return "🉑";
case ":red_circle:": return "🔴";
case ":large_blue_circle:": return "🔵";
case ":large_orange_diamond:": return "🔶";
case ":large_blue_diamond:": return "🔷";
case ":small_orange_diamond:": return "🔸";
case ":small_blue_diamond:": return "🔹";
case ":small_red_triangle:": return "🔺";
case ":small_red_triangle_down:": return "🔻";
case ":diamond_shape_with_a_dot_inside:": return "💠";
case ":radio_button:": return "🔘";
case ":white_square_button:": return "🔳";
case ":black_square_button:": return "🔲";
case ":checkered_flag:": return "🏁";
case ":triangular_flag_on_post:": return "🚩";
case ":crossed_flags:": return "🎌";
}
throw new IllegalArgumentException("'" + name + "' is not a valid name");
}
}

View file

@ -0,0 +1,196 @@
package juloo.keyboard2;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.AttributeSet;
import android.view.ContextThemeWrapper;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class EmojiGridView extends GridView
implements GridView.OnItemClickListener
{
public static final int GROUP_LAST_USE = -1;
private static final String LAST_USE_PREF = "emoji_last_use";
private List<Emoji> _emojiArray;
private HashMap<Emoji, Integer> _lastUsed;
/*
** TODO: adapt column width and emoji size
** TODO: use ArraySet instead of Emoji[]
*/
public EmojiGridView(Context context, AttributeSet attrs)
{
super(context, attrs);
Emoji.init(context.getResources());
migrateOldPrefs(); // TODO: Remove at some point in future
setOnItemClickListener(this);
loadLastUsed();
setEmojiGroup((_lastUsed.size() == 0) ? 0 : GROUP_LAST_USE);
}
public void setEmojiGroup(int group)
{
_emojiArray = (group == GROUP_LAST_USE) ? getLastEmojis() : Emoji.getEmojisByGroup(group);
setAdapter(new EmojiViewAdpater(getContext(), _emojiArray));
}
public void onItemClick(AdapterView<?> parent, View v, int pos, long id)
{
Config config = Config.globalConfig();
Integer used = _lastUsed.get(_emojiArray.get(pos));
_lastUsed.put(_emojiArray.get(pos), (used == null) ? 1 : used.intValue() + 1);
config.handler.key_up(_emojiArray.get(pos).kv(), Pointers.Modifiers.EMPTY);
saveLastUsed(); // TODO: opti
}
private List<Emoji> getLastEmojis()
{
List<Emoji> list = new ArrayList<>(_lastUsed.keySet());
Collections.sort(list, new Comparator<Emoji>()
{
public int compare(Emoji a, Emoji b)
{
return _lastUsed.get(b) - _lastUsed.get(a);
}
});
return list;
}
private void saveLastUsed()
{
SharedPreferences.Editor edit;
try { edit = emojiSharedPreferences().edit(); }
catch (Exception _e) { return; }
HashSet<String> set = new HashSet<String>();
for (Emoji emoji : _lastUsed.keySet())
set.add(String.valueOf(_lastUsed.get(emoji)) + "-" + emoji.kv().getString());
edit.putStringSet(LAST_USE_PREF, set);
edit.apply();
}
private void loadLastUsed()
{
_lastUsed = new HashMap<Emoji, Integer>();
SharedPreferences prefs;
// Storage might not be available (eg. the device is locked), avoid
// crashing.
try { prefs = emojiSharedPreferences(); }
catch (Exception _e) { return; }
Set<String> lastUseSet = prefs.getStringSet(LAST_USE_PREF, null);
if (lastUseSet != null)
for (String emojiData : lastUseSet)
{
String[] data = emojiData.split("-", 2);
Emoji emoji;
if (data.length != 2)
continue ;
emoji = Emoji.getEmojiByString(data[1]);
if (emoji == null)
continue ;
_lastUsed.put(emoji, Integer.valueOf(data[0]));
}
}
SharedPreferences emojiSharedPreferences()
{
return getContext().getSharedPreferences("emoji_last_use", Context.MODE_PRIVATE);
}
private void migrateOldPrefs()
{
final String MIGRATION_CHECK_KEY = "MIGRATION_COMPLETE";
SharedPreferences prefs;
try { prefs = emojiSharedPreferences(); }
catch (Exception e) { return; }
Set<String> lastUsed = prefs.getStringSet(LAST_USE_PREF, null);
if (lastUsed != null && !prefs.getBoolean(MIGRATION_CHECK_KEY, false))
{
SharedPreferences.Editor edit = prefs.edit();
edit.clear();
Set<String> lastUsedNew = new HashSet<>();
for (String entry : lastUsed)
{
String[] data = entry.split("-", 2);
try
{
lastUsedNew.add(Integer.parseInt(data[0]) + "-" + Emoji.mapOldNameToValue(data[1]));
}
catch (IllegalArgumentException ignored) {}
}
edit.putStringSet(LAST_USE_PREF, lastUsedNew);
edit.putBoolean(MIGRATION_CHECK_KEY, true);
edit.apply();
}
}
static class EmojiView extends TextView
{
public EmojiView(Context context)
{
super(context);
}
public void setEmoji(Emoji emoji)
{
setText(emoji.kv().getString());
}
}
static class EmojiViewAdpater extends BaseAdapter
{
Context _button_context;
List<Emoji> _emojiArray;
public EmojiViewAdpater(Context context, List<Emoji> emojiArray)
{
_button_context = new ContextThemeWrapper(context, R.style.emojiGridButton);
_emojiArray = emojiArray;
}
public int getCount()
{
if (_emojiArray == null)
return (0);
return (_emojiArray.size());
}
public Object getItem(int pos)
{
return (_emojiArray.get(pos));
}
public long getItemId(int pos)
{
return (pos);
}
public View getView(int pos, View convertView, ViewGroup parent)
{
EmojiView view = (EmojiView)convertView;
if (view == null)
view = new EmojiView(_button_context);
view.setEmoji(_emojiArray.get(pos));
return view;
}
}
}

View file

@ -0,0 +1,62 @@
package juloo.keyboard2;
import android.content.Context;
import android.util.AttributeSet;
import android.view.ContextThemeWrapper;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout.LayoutParams;
import android.widget.LinearLayout;
public class EmojiGroupButtonsBar extends LinearLayout
{
private EmojiGridView _emoji_grid = null;
public EmojiGroupButtonsBar(Context context, AttributeSet attrs)
{
super(context, attrs);
Emoji.init(context.getResources());
add_group(EmojiGridView.GROUP_LAST_USE, "\uD83D\uDD59");
for (int i = 0; i < Emoji.getNumGroups(); i++)
{
Emoji first = Emoji.getEmojisByGroup(i).get(0);
add_group(i, first.kv().getString());
}
}
void add_group(int id, String symbol)
{
addView(this.new EmojiGroupButton(getContext(), id, symbol),
new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT, 1.f));
}
EmojiGridView get_emoji_grid()
{
if (_emoji_grid == null)
_emoji_grid = (EmojiGridView)((ViewGroup)(getParent())).findViewById(R.id.emoji_grid);
return _emoji_grid;
}
class EmojiGroupButton extends Button implements View.OnTouchListener
{
int _group_id;
public EmojiGroupButton(Context context, int group_id, String symbol)
{
super(new ContextThemeWrapper(context, R.style.emojiTypeButton), null, 0);
_group_id = group_id;
setText(symbol);
setOnTouchListener(this);
}
public boolean onTouch(View view, MotionEvent event)
{
if (event.getAction() != MotionEvent.ACTION_DOWN)
return false;
get_emoji_grid().setEmojiGroup(_group_id);
return true;
}
}
}

View file

@ -0,0 +1,150 @@
package juloo.keyboard2;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public final class ExtraKeys
{
public static final ExtraKeys EMPTY = new ExtraKeys(Collections.EMPTY_LIST);
Collection<ExtraKey> _ks;
public ExtraKeys(Collection<ExtraKey> ks)
{
_ks = ks;
}
/** Add the keys that should be added to the keyboard into [dst]. Keys
already added to [dst] might have an impact, see [ExtraKey.compute]. */
public void compute(Map<KeyValue, KeyboardData.PreferredPos> dst, Query q)
{
for (ExtraKey k : _ks)
k.compute(dst, q);
}
public static ExtraKeys parse(String script, String str)
{
Collection<ExtraKey> dst = new ArrayList<ExtraKey>();
String[] ks = str.split("\\|");
for (int i = 0; i < ks.length; i++)
dst.add(ExtraKey.parse(ks[i], script));
return new ExtraKeys(dst);
}
/** Merge identical keys. This is required to decide whether to add
alternatives. Script is generalized (set to null) on any conflict. */
public static ExtraKeys merge(List<ExtraKeys> kss)
{
Map<KeyValue, ExtraKey> merged_keys = new HashMap<KeyValue, ExtraKey>();
for (ExtraKeys ks : kss)
for (ExtraKey k : ks._ks)
{
ExtraKey k2 = merged_keys.get(k.kv);
if (k2 != null)
k = k.merge_with(k2);
merged_keys.put(k.kv, k);
}
return new ExtraKeys(merged_keys.values());
}
final static class ExtraKey
{
/** The key to add. */
final KeyValue kv;
/** The key will be added to layouts of the same script. If null, might be
added to layouts of any script. */
final String script;
/** The key will not be added to layout that already contain all the
alternatives. */
final List<KeyValue> alternatives;
/** The key next to which to add. Might be [null]. */
final KeyValue next_to;
ExtraKey(KeyValue kv_, String script_, List<KeyValue> alts_, KeyValue next_to_)
{
kv = kv_;
script = script_;
alternatives = alts_;
next_to = next_to_;
}
/** Whether the key should be added to the keyboard. */
public void compute(Map<KeyValue, KeyboardData.PreferredPos> dst, Query q)
{
// Add the alternative if it's the only one. The list of alternatives is
// enforced to be complete by the merging step. The same [kv] will not
// appear again in the list of extra keys with a different list of
// alternatives.
// Selecting the dead key in the "Add key to the keyboard" option would
// disable this behavior for a key.
boolean use_alternative = (alternatives.size() == 1 && !dst.containsKey(kv));
if
((q.script == null || script == null || q.script.equals(script))
&& (alternatives.size() == 0 || !q.present.containsAll(alternatives)))
{
KeyValue kv_ = use_alternative ? alternatives.get(0) : kv;
KeyboardData.PreferredPos pos = KeyboardData.PreferredPos.DEFAULT;
if (next_to != null)
{
pos = new KeyboardData.PreferredPos(pos);
pos.next_to = next_to;
}
dst.put(kv_, pos);
}
}
/** Return a new key from two. [kv] are expected to be equal. [script] is
generalized to [null] on any conflict. [alternatives] are concatenated.
*/
public ExtraKey merge_with(ExtraKey k2)
{
String script_ = one_or_none(script, k2.script);
List<KeyValue> alts = new ArrayList<KeyValue>(alternatives);
KeyValue next_to_ = one_or_none(next_to, k2.next_to);
alts.addAll(k2.alternatives);
return new ExtraKey(kv, script_, alts, next_to_);
}
/** If one of [a] or [b] is null, return the other. If [a] and [b] are
equal, return [a]. Otherwise, return null. */
<E> E one_or_none(E a, E b)
{
return (a == null) ? b : (b == null || a.equals(b)) ? a : null;
}
/** Extra keys are of the form "key name" or "key name:alt1:alt2@next_to". */
public static ExtraKey parse(String str, String script)
{
String[] split_on_at = str.split("@", 2);
String[] key_names = split_on_at[0].split(":");
KeyValue kv = KeyValue.getKeyByName(key_names[0]);
KeyValue[] alts = new KeyValue[key_names.length-1];
for (int i = 1; i < key_names.length; i++)
alts[i-1] = KeyValue.getKeyByName(key_names[i]);
KeyValue next_to = null;
if (split_on_at.length > 1)
next_to = KeyValue.getKeyByName(split_on_at[1]);
return new ExtraKey(kv, script, Arrays.asList(alts), next_to);
}
}
public final static class Query
{
/** Script of the current layout. Might be null. */
final String script;
/** Keys present on the layout. */
final Set<KeyValue> present;
public Query(String script_, Set<KeyValue> present_)
{
script = script_;
present = present_;
}
}
}

View file

@ -0,0 +1,62 @@
package juloo.keyboard2;
import android.content.Context;
import android.content.pm.PackageManager;
import androidx.window.java.layout.WindowInfoTrackerCallbackAdapter;
import androidx.window.layout.DisplayFeature;
import androidx.window.layout.FoldingFeature;
import androidx.window.layout.WindowInfoTracker;
import androidx.window.layout.WindowLayoutInfo;
import androidx.core.util.Consumer;
public class FoldStateTracker {
private final Consumer<WindowLayoutInfo> _innerListener;
private final WindowInfoTrackerCallbackAdapter _windowInfoTracker;
private FoldingFeature _foldingFeature = null;
private Runnable _changedCallback = null;
public FoldStateTracker(Context context) {
_windowInfoTracker =
new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(context));
_innerListener = new LayoutStateChangeCallback();
_windowInfoTracker.addWindowLayoutInfoListener(context, Runnable::run, _innerListener);
}
public static boolean isFoldableDevice(Context context) {
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_SENSOR_HINGE_ANGLE);
}
public boolean isUnfolded() {
// FoldableFeature is only present when the device is unfolded. Otherwise, it's removed.
// A weird decision from Google, but that's how it works:
// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:window/window/src/main/java/androidx/window/layout/adapter/sidecar/SidecarAdapter.kt;l=187?q=SidecarAdapter
return _foldingFeature != null;
}
public void close() {
_windowInfoTracker.removeWindowLayoutInfoListener(_innerListener);
}
public void setChangedCallback(Runnable _changedCallback) {
this._changedCallback = _changedCallback;
}
class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
@Override
public void accept(WindowLayoutInfo newLayoutInfo) {
FoldingFeature old = _foldingFeature;
_foldingFeature = null;
for (DisplayFeature feature: newLayoutInfo.getDisplayFeatures()) {
if (feature instanceof FoldingFeature) {
_foldingFeature = (FoldingFeature) feature;
}
}
if (old != _foldingFeature && _changedCallback != null) {
_changedCallback.run();
}
}
}
}

View file

@ -0,0 +1,141 @@
package juloo.keyboard2;
public final class Gesture
{
/** The pointer direction that caused the last state change.
Integer from 0 to 15 (included). */
int current_dir;
State state;
public Gesture(int starting_direction)
{
current_dir = starting_direction;
state = State.Swiped;
}
enum State
{
Cancelled,
Swiped,
Rotating_clockwise,
Rotating_anticlockwise,
Ended_swipe,
Ended_center,
Ended_clockwise,
Ended_anticlockwise
}
enum Name
{
None,
Swipe,
Roundtrip,
Circle,
Anticircle
}
/** Angle to travel before a rotation gesture starts. A threshold too low
would be too easy to reach while doing back and forth gestures, as the
quadrants are very small. In the same unit as [current_dir] */
static final int ROTATION_THRESHOLD = 2;
/** Return the currently recognized gesture. Return [null] if no gesture is
recognized. Might change everytime [changed_direction] return [true]. */
public Name get_gesture()
{
switch (state)
{
case Cancelled:
return Name.None;
case Swiped:
case Ended_swipe:
return Name.Swipe;
case Ended_center:
return Name.Roundtrip;
case Rotating_clockwise:
case Ended_clockwise:
return Name.Circle;
case Rotating_anticlockwise:
case Ended_anticlockwise:
return Name.Anticircle;
}
return Name.None; // Unreachable
}
public boolean is_in_progress()
{
switch (state)
{
case Swiped:
case Rotating_clockwise:
case Rotating_anticlockwise:
return true;
}
return false;
}
public int current_direction() { return current_dir; }
/** The pointer changed direction. Return [true] if the gesture changed
state and [get_gesture] return a different value. */
public boolean changed_direction(int direction)
{
int d = dir_diff(current_dir, direction);
boolean clockwise = d > 0;
switch (state)
{
case Swiped:
if (Math.abs(d) < Config.globalConfig().circle_sensitivity)
return false;
// Start a rotation
state = (clockwise) ?
State.Rotating_clockwise : State.Rotating_anticlockwise;
current_dir = direction;
return true;
// Check that rotation is not reversing
case Rotating_clockwise:
case Rotating_anticlockwise:
current_dir = direction;
if ((state == State.Rotating_clockwise) == clockwise)
return false;
state = State.Cancelled;
return true;
}
return false;
}
/** Return [true] if [get_gesture] will return a different value. */
public boolean moved_to_center()
{
switch (state)
{
case Swiped: state = State.Ended_center; return true;
case Rotating_clockwise: state = State.Ended_clockwise; return false;
case Rotating_anticlockwise: state = State.Ended_anticlockwise; return false;
}
return false;
}
/** Will not change the gesture state. */
public void pointer_up()
{
switch (state)
{
case Swiped: state = State.Ended_swipe; break;
case Rotating_clockwise: state = State.Ended_clockwise; break;
case Rotating_anticlockwise: state = State.Ended_anticlockwise; break;
}
}
static int dir_diff(int d1, int d2)
{
final int n = 16;
// Shortest-path in modulo arithmetic
if (d1 == d2)
return 0;
int left = (d1 - d2 + n) % n;
int right = (d2 - d1 + n) % n;
return (left < right) ? -left : right;
}
}

View file

@ -0,0 +1,503 @@
package juloo.keyboard2;
import android.annotation.SuppressLint;
import android.os.Looper;
import android.os.Handler;
import android.text.InputType;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
import java.util.Iterator;
public final class KeyEventHandler
implements Config.IKeyEventHandler,
ClipboardHistoryService.ClipboardPasteCallback
{
IReceiver _recv;
Autocapitalisation _autocap;
/** State of the system modifiers. It is updated whether a modifier is down
or up and a corresponding key event is sent. */
Pointers.Modifiers _mods;
/** Consistent with [_mods]. This is a mutable state rather than computed
from [_mods] to ensure that the meta state is correct while up and down
events are sent for the modifier keys. */
int _meta_state = 0;
/** Whether to force sending arrow keys to move the cursor when
[setSelection] could be used instead. */
boolean _move_cursor_force_fallback = false;
public KeyEventHandler(IReceiver recv)
{
_recv = recv;
_autocap = new Autocapitalisation(recv.getHandler(),
this.new Autocapitalisation_callback());
_mods = Pointers.Modifiers.EMPTY;
}
/** Editing just started. */
public void started(EditorInfo info)
{
_autocap.started(info, _recv.getCurrentInputConnection());
_move_cursor_force_fallback = should_move_cursor_force_fallback(info);
}
/** Selection has been updated. */
public void selection_updated(int oldSelStart, int newSelStart)
{
_autocap.selection_updated(oldSelStart, newSelStart);
}
/** A key is being pressed. There will not necessarily be a corresponding
[key_up] event. */
@Override
public void key_down(KeyValue key, boolean isSwipe)
{
if (key == null)
return;
// Stop auto capitalisation when pressing some keys
switch (key.getKind())
{
case Modifier:
switch (key.getModifier())
{
case CTRL:
case ALT:
case META:
_autocap.stop();
break;
}
break;
case Compose_pending:
_autocap.stop();
break;
case Slider:
// Don't wait for the next key_up and move the cursor right away. This
// is called after the trigger distance have been travelled.
handle_slider(key.getSlider(), key.getSliderRepeat(), true);
break;
default: break;
}
}
/** A key has been released. */
@Override
public void key_up(KeyValue key, Pointers.Modifiers mods)
{
if (key == null)
return;
Pointers.Modifiers old_mods = _mods;
update_meta_state(mods);
switch (key.getKind())
{
case Char: send_text(String.valueOf(key.getChar())); break;
case String: send_text(key.getString()); break;
case Event: _recv.handle_event_key(key.getEvent()); break;
case Keyevent: send_key_down_up(key.getKeyevent()); break;
case Modifier: break;
case Editing: handle_editing_key(key.getEditing()); break;
case Compose_pending: _recv.set_compose_pending(true); break;
case Slider: handle_slider(key.getSlider(), key.getSliderRepeat(), false); break;
case Macro: evaluate_macro(key.getMacro()); break;
}
update_meta_state(old_mods);
}
@Override
public void mods_changed(Pointers.Modifiers mods)
{
update_meta_state(mods);
}
@Override
public void paste_from_clipboard_pane(String content)
{
send_text(content);
}
/** Update [_mods] to be consistent with the [mods], sending key events if
needed. */
void update_meta_state(Pointers.Modifiers mods)
{
// Released modifiers
Iterator<KeyValue> it = _mods.diff(mods);
while (it.hasNext())
sendMetaKeyForModifier(it.next(), false);
// Activated modifiers
it = mods.diff(_mods);
while (it.hasNext())
sendMetaKeyForModifier(it.next(), true);
_mods = mods;
}
// private void handleDelKey(int before, int after)
// {
// CharSequence selection = getCurrentInputConnection().getSelectedText(0);
// if (selection != null && selection.length() > 0)
// getCurrentInputConnection().commitText("", 1);
// else
// getCurrentInputConnection().deleteSurroundingText(before, after);
// }
void sendMetaKey(int eventCode, int meta_flags, boolean down)
{
if (down)
{
_meta_state = _meta_state | meta_flags;
send_keyevent(KeyEvent.ACTION_DOWN, eventCode, _meta_state);
}
else
{
send_keyevent(KeyEvent.ACTION_UP, eventCode, _meta_state);
_meta_state = _meta_state & ~meta_flags;
}
}
void sendMetaKeyForModifier(KeyValue kv, boolean down)
{
switch (kv.getKind())
{
case Modifier:
switch (kv.getModifier())
{
case CTRL:
sendMetaKey(KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.META_CTRL_LEFT_ON | KeyEvent.META_CTRL_ON, down);
break;
case ALT:
sendMetaKey(KeyEvent.KEYCODE_ALT_LEFT, KeyEvent.META_ALT_LEFT_ON | KeyEvent.META_ALT_ON, down);
break;
case SHIFT:
sendMetaKey(KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON, down);
break;
case META:
sendMetaKey(KeyEvent.KEYCODE_META_LEFT, KeyEvent.META_META_LEFT_ON | KeyEvent.META_META_ON, down);
break;
default:
break;
}
break;
}
}
void send_key_down_up(int keyCode)
{
send_key_down_up(keyCode, _meta_state);
}
/** Ignores currently pressed system modifiers. */
void send_key_down_up(int keyCode, int metaState)
{
send_keyevent(KeyEvent.ACTION_DOWN, keyCode, metaState);
send_keyevent(KeyEvent.ACTION_UP, keyCode, metaState);
}
void send_keyevent(int eventAction, int eventCode, int metaState)
{
InputConnection conn = _recv.getCurrentInputConnection();
if (conn == null)
return;
conn.sendKeyEvent(new KeyEvent(1, 1, eventAction, eventCode, 0,
metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
if (eventAction == KeyEvent.ACTION_UP)
_autocap.event_sent(eventCode, metaState);
}
void send_text(CharSequence text)
{
InputConnection conn = _recv.getCurrentInputConnection();
if (conn == null)
return;
conn.commitText(text, 1);
_autocap.typed(text);
}
/** See {!InputConnection.performContextMenuAction}. */
void send_context_menu_action(int id)
{
InputConnection conn = _recv.getCurrentInputConnection();
if (conn == null)
return;
conn.performContextMenuAction(id);
}
@SuppressLint("InlinedApi")
void handle_editing_key(KeyValue.Editing ev)
{
switch (ev)
{
case COPY: if(is_selection_not_empty()) send_context_menu_action(android.R.id.copy); break;
case PASTE: send_context_menu_action(android.R.id.paste); break;
case CUT: if(is_selection_not_empty()) send_context_menu_action(android.R.id.cut); break;
case SELECT_ALL: send_context_menu_action(android.R.id.selectAll); break;
case SHARE: send_context_menu_action(android.R.id.shareText); break;
case PASTE_PLAIN: send_context_menu_action(android.R.id.pasteAsPlainText); break;
case UNDO: send_context_menu_action(android.R.id.undo); break;
case REDO: send_context_menu_action(android.R.id.redo); break;
case REPLACE: send_context_menu_action(android.R.id.replaceText); break;
case ASSIST: send_context_menu_action(android.R.id.textAssist); break;
case AUTOFILL: send_context_menu_action(android.R.id.autofill); break;
case DELETE_WORD: send_key_down_up(KeyEvent.KEYCODE_DEL, KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON); break;
case FORWARD_DELETE_WORD: send_key_down_up(KeyEvent.KEYCODE_FORWARD_DEL, KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON); break;
case SELECTION_CANCEL: cancel_selection(); break;
}
}
static ExtractedTextRequest _move_cursor_req = null;
/** Query the cursor position. The extracted text is empty. Returns [null] if
the editor doesn't support this operation. */
ExtractedText get_cursor_pos(InputConnection conn)
{
if (_move_cursor_req == null)
{
_move_cursor_req = new ExtractedTextRequest();
_move_cursor_req.hintMaxChars = 0;
}
return conn.getExtractedText(_move_cursor_req, 0);
}
/** [r] might be negative, in which case the direction is reversed. */
void handle_slider(KeyValue.Slider s, int r, boolean key_down)
{
switch (s)
{
case Cursor_left: move_cursor(-r); break;
case Cursor_right: move_cursor(r); break;
case Cursor_up: move_cursor_vertical(-r); break;
case Cursor_down: move_cursor_vertical(r); break;
case Selection_cursor_left: move_cursor_sel(r, true, key_down); break;
case Selection_cursor_right: move_cursor_sel(r, false, key_down); break;
}
}
/** Move the cursor right or left, if possible without sending key events.
Unlike arrow keys, the selection is not removed even if shift is not on.
Falls back to sending arrow keys events if the editor do not support
moving the cursor or a modifier other than shift is pressed. */
void move_cursor(int d)
{
InputConnection conn = _recv.getCurrentInputConnection();
if (conn == null)
return;
ExtractedText et = get_cursor_pos(conn);
if (et != null && can_set_selection(conn))
{
int sel_start = et.selectionStart;
int sel_end = et.selectionEnd;
// Continue expanding the selection even if shift is not pressed
if (sel_end != sel_start)
{
sel_end += d;
if (sel_end == sel_start) // Avoid making the selection empty
sel_end += d;
}
else
{
sel_end += d;
// Leave 'sel_start' where it is if shift is pressed
if ((_meta_state & KeyEvent.META_SHIFT_ON) == 0)
sel_start = sel_end;
}
if (conn.setSelection(sel_start, sel_end))
return; // Fallback to sending key events if [setSelection] failed
}
move_cursor_fallback(d);
}
/** Move one of the two side of a selection. If [sel_left] is true, the left
position is moved, otherwise the right position is moved. */
void move_cursor_sel(int d, boolean sel_left, boolean key_down)
{
InputConnection conn = _recv.getCurrentInputConnection();
if (conn == null)
return;
ExtractedText et = get_cursor_pos(conn);
if (et != null && can_set_selection(conn))
{
int sel_start = et.selectionStart;
int sel_end = et.selectionEnd;
// Reorder the selection when the slider has just been pressed. The
// selection might have been reversed if one end crossed the other end
// with a previous slider.
if (key_down && sel_start > sel_end)
{
sel_start = et.selectionEnd;
sel_end = et.selectionStart;
}
do
{
if (sel_left)
sel_start += d;
else
sel_end += d;
// Move the cursor twice if moving it once would make the selection
// empty and stop selection mode.
} while (sel_start == sel_end);
if (conn.setSelection(sel_start, sel_end))
return; // Fallback to sending key events if [setSelection] failed
}
move_cursor_fallback(d);
}
/** Returns whether the selection can be set using [conn.setSelection()].
This can happen on Termux or when system modifiers are activated for
example. */
boolean can_set_selection(InputConnection conn)
{
final int system_mods =
KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON | KeyEvent.META_META_ON;
return !_move_cursor_force_fallback && (_meta_state & system_mods) == 0;
}
void move_cursor_fallback(int d)
{
if (d < 0)
send_key_down_up_repeat(KeyEvent.KEYCODE_DPAD_LEFT, -d);
else
send_key_down_up_repeat(KeyEvent.KEYCODE_DPAD_RIGHT, d);
}
/** Move the cursor up and down. This sends UP and DOWN key events that might
make the focus exit the text box. */
void move_cursor_vertical(int d)
{
if (d < 0)
send_key_down_up_repeat(KeyEvent.KEYCODE_DPAD_UP, -d);
else
send_key_down_up_repeat(KeyEvent.KEYCODE_DPAD_DOWN, d);
}
void evaluate_macro(KeyValue[] keys)
{
if (keys.length == 0)
return;
// Ignore modifiers that are activated at the time the macro is evaluated
mods_changed(Pointers.Modifiers.EMPTY);
evaluate_macro_loop(keys, 0, Pointers.Modifiers.EMPTY, _autocap.pause());
}
/** Evaluate the macro asynchronously to make sure event are processed in the
right order. */
void evaluate_macro_loop(final KeyValue[] keys, int i, Pointers.Modifiers mods, final boolean autocap_paused)
{
boolean should_delay = false;
KeyValue kv = KeyModifier.modify(keys[i], mods);
if (kv != null)
{
if (kv.hasFlagsAny(KeyValue.FLAG_LATCH))
{
// Non-special latchable keys clear latched modifiers
if (!kv.hasFlagsAny(KeyValue.FLAG_SPECIAL))
mods = Pointers.Modifiers.EMPTY;
mods = mods.with_extra_mod(kv);
}
else
{
key_down(kv, false);
key_up(kv, mods);
mods = Pointers.Modifiers.EMPTY;
}
should_delay = wait_after_macro_key(kv);
}
i++;
if (i >= keys.length) // Stop looping
{
_autocap.unpause(autocap_paused);
}
else if (should_delay)
{
// Add a delay before sending the next key to avoid race conditions
// causing keys to be handled in the wrong order. Notably, KeyEvent keys
// handling is scheduled differently than the other edit functions.
final int i_ = i;
final Pointers.Modifiers mods_ = mods;
_recv.getHandler().postDelayed(new Runnable() {
public void run()
{
evaluate_macro_loop(keys, i_, mods_, autocap_paused);
}
}, 1000/30);
}
else
evaluate_macro_loop(keys, i, mods, autocap_paused);
}
boolean wait_after_macro_key(KeyValue kv)
{
switch (kv.getKind())
{
case Keyevent:
case Editing:
case Event:
return true;
case Slider:
return _move_cursor_force_fallback;
default:
return false;
}
}
/** Repeat calls to [send_key_down_up]. */
void send_key_down_up_repeat(int event_code, int repeat)
{
while (repeat-- > 0)
send_key_down_up(event_code);
}
void cancel_selection()
{
InputConnection conn = _recv.getCurrentInputConnection();
if (conn == null)
return;
ExtractedText et = get_cursor_pos(conn);
if (et == null) return;
final int curs = et.selectionStart;
// Notify the receiver as Android's [onUpdateSelection] is not triggered.
if (conn.setSelection(curs, curs));
_recv.selection_state_changed(false);
}
boolean is_selection_not_empty()
{
InputConnection conn = _recv.getCurrentInputConnection();
if (conn == null) return false;
return (conn.getSelectedText(0) != null);
}
/** Workaround some apps which answers to [getExtractedText] but do not react
to [setSelection] while returning [true]. */
boolean should_move_cursor_force_fallback(EditorInfo info)
{
// This catch Acode: which sets several variations at once.
if ((info.inputType & InputType.TYPE_MASK_VARIATION & InputType.TYPE_TEXT_VARIATION_PASSWORD) != 0)
return true;
// Godot editor: Doesn't handle setSelection() but returns true.
return info.packageName.startsWith("org.godotengine.editor");
}
public static interface IReceiver
{
public void handle_event_key(KeyValue.Event ev);
public void set_shift_state(boolean state, boolean lock);
public void set_compose_pending(boolean pending);
public void selection_state_changed(boolean selection_is_ongoing);
public InputConnection getCurrentInputConnection();
public Handler getHandler();
}
class Autocapitalisation_callback implements Autocapitalisation.Callback
{
@Override
public void update_shift_state(boolean should_enable, boolean should_disable)
{
if (should_enable)
_recv.set_shift_state(true, false);
else if (should_disable)
_recv.set_shift_state(false, false);
}
}
}

View file

@ -0,0 +1,527 @@
package juloo.keyboard2;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import java.util.HashMap;
public final class KeyModifier
{
/** The optional modmap takes priority over modifiers usual behaviors. Set to
[null] to disable. */
private static Modmap _modmap = null;
public static void set_modmap(Modmap mm)
{
_modmap = mm;
}
/** Modify a key according to modifiers. */
public static KeyValue modify(KeyValue k, Pointers.Modifiers mods)
{
if (k == null)
return null;
int n_mods = mods.size();
KeyValue r = k;
for (int i = 0; i < n_mods; i++)
r = modify(r, mods.get(i));
/* Keys with an empty string are placeholder keys. */
if (r.getString().length() == 0)
return null;
return r;
}
public static KeyValue modify(KeyValue k, KeyValue mod)
{
switch (mod.getKind())
{
case Modifier:
return modify(k, mod.getModifier());
case Compose_pending:
return apply_compose_pending(mod.getPendingCompose(), k);
case Hangul_initial:
if (k.equals(mod)) // Allow typing the initial in letter form
return KeyValue.makeStringKey(k.getString(), KeyValue.FLAG_GREYED);
return combine_hangul_initial(k, mod.getHangulPrecomposed());
case Hangul_medial:
return combine_hangul_medial(k, mod.getHangulPrecomposed());
}
return k;
}
public static KeyValue modify(KeyValue k, KeyValue.Modifier mod)
{
switch (mod)
{
case CTRL: return apply_ctrl(k);
case ALT:
case META: return turn_into_keyevent(k);
case FN: return apply_fn(k);
case GESTURE: return apply_gesture(k);
case SHIFT: return apply_shift(k);
case GRAVE: return apply_compose_or_dead_char(k, ComposeKeyData.accent_grave, '\u02CB');
case AIGU: return apply_compose_or_dead_char(k, ComposeKeyData.accent_aigu, '\u00B4');
case CIRCONFLEXE: return apply_compose_or_dead_char(k, ComposeKeyData.accent_circonflexe, '\u02C6');
case TILDE: return apply_compose_or_dead_char(k, ComposeKeyData.accent_tilde, '\u02DC');
case CEDILLE: return apply_compose_or_dead_char(k, ComposeKeyData.accent_cedille, '\u00B8');
case TREMA: return apply_compose_or_dead_char(k, ComposeKeyData.accent_trema, '\u00A8');
case CARON: return apply_compose_or_dead_char(k, ComposeKeyData.accent_caron, '\u02C7');
case RING: return apply_compose_or_dead_char(k, ComposeKeyData.accent_ring, '\u02DA');
case MACRON: return apply_compose_or_dead_char(k, ComposeKeyData.accent_macron, '\u00AF');
case OGONEK: return apply_compose_or_dead_char(k, ComposeKeyData.accent_ogonek, '\u02DB');
case DOT_ABOVE: return apply_compose_or_dead_char(k, ComposeKeyData.accent_dot_above, '\u02D9');
case BREVE: return apply_dead_char(k, '\u02D8');
case DOUBLE_AIGU: return apply_compose(k, ComposeKeyData.accent_double_aigu);
case ORDINAL: return apply_compose(k, ComposeKeyData.accent_ordinal);
case SUPERSCRIPT: return apply_compose(k, ComposeKeyData.accent_superscript);
case SUBSCRIPT: return apply_compose(k, ComposeKeyData.accent_subscript);
case ARROWS: return apply_compose(k, ComposeKeyData.accent_arrows);
case BOX: return apply_compose(k, ComposeKeyData.accent_box);
case SLASH: return apply_compose(k, ComposeKeyData.accent_slash);
case BAR: return apply_compose(k, ComposeKeyData.accent_bar);
case DOT_BELOW: return apply_compose(k, ComposeKeyData.accent_dot_below);
case HORN: return apply_compose(k, ComposeKeyData.accent_horn);
case HOOK_ABOVE: return apply_compose(k, ComposeKeyData.accent_hook_above);
case DOUBLE_GRAVE: return apply_compose(k, ComposeKeyData.accent_double_grave);
case ARROW_RIGHT: return apply_combining_char(k, "\u20D7");
case SELECTION_MODE: return apply_selection_mode(k);
default: return k;
}
}
/** Modify a key after a long press. */
public static KeyValue modify_long_press(KeyValue k)
{
switch (k.getKind())
{
case Event:
switch (k.getEvent())
{
case CHANGE_METHOD_AUTO:
return KeyValue.getKeyByName("change_method");
case SWITCH_VOICE_TYPING:
return KeyValue.getKeyByName("voice_typing_chooser");
}
break;
}
return k;
}
/** Return the compose state that modifies the numpad script. */
public static int modify_numpad_script(String numpad_script)
{
if (numpad_script == null)
return -1;
switch (numpad_script)
{
case "hindu-arabic": return ComposeKeyData.numpad_hindu;
case "bengali": return ComposeKeyData.numpad_bengali;
case "devanagari": return ComposeKeyData.numpad_devanagari;
case "persian": return ComposeKeyData.numpad_persian;
case "gujarati": return ComposeKeyData.numpad_gujarati;
case "kannada": return ComposeKeyData.numpad_kannada;
case "tamil": return ComposeKeyData.numpad_tamil;
default: return -1;
}
}
/** Keys that do not match any sequence are greyed. */
private static KeyValue apply_compose_pending(int state, KeyValue kv)
{
switch (kv.getKind())
{
case Char:
case String:
KeyValue res = ComposeKey.apply(state, kv);
// Grey-out characters not part of any sequence.
if (res == null)
return kv.withFlags(kv.getFlags() | KeyValue.FLAG_GREYED);
return res;
/* Tapping compose again exits the pending sequence. */
case Compose_pending:
return KeyValue.getKeyByName("compose_cancel");
/* These keys are not greyed. */
case Event:
case Modifier:
return kv;
/* Other keys cannot be part of sequences. */
default:
return kv.withFlags(kv.getFlags() | KeyValue.FLAG_GREYED);
}
}
/** Apply the given compose state or fallback to the dead_char. */
private static KeyValue apply_compose_or_dead_char(KeyValue k, int state, char dead_char)
{
KeyValue r = ComposeKey.apply(state, k);
if (r != null)
return r;
return apply_dead_char(k, dead_char);
}
private static KeyValue apply_compose(KeyValue k, int state)
{
KeyValue r = ComposeKey.apply(state, k);
return (r != null) ? r : k;
}
private static KeyValue apply_dead_char(KeyValue k, char dead_char)
{
switch (k.getKind())
{
case Char:
char c = k.getChar();
char modified = (char)KeyCharacterMap.getDeadChar(dead_char, c);
if (modified != 0 && modified != c)
return KeyValue.makeStringKey(String.valueOf(modified));
}
return k;
}
private static KeyValue apply_combining_char(KeyValue k, String combining)
{
switch (k.getKind())
{
case Char:
return KeyValue.makeStringKey(k.getChar() + combining, k.getFlags());
}
return k;
}
private static KeyValue apply_shift(KeyValue k)
{
if (_modmap != null)
{
KeyValue mapped = _modmap.get(Modmap.M.Shift, k);
if (mapped != null)
return mapped;
}
KeyValue r = ComposeKey.apply(ComposeKeyData.shift, k);
if (r != null)
return r;
switch (k.getKind())
{
case Char:
char kc = k.getChar();
char c = Character.toUpperCase(kc);
return (kc == c) ? k : k.withChar(c);
case String:
String ks = k.getString();
String s = Utils.capitalize_string(ks);
return s.equals(ks) ? k : KeyValue.makeStringKey(s, k.getFlags());
default: return k;
}
}
private static KeyValue apply_fn(KeyValue k)
{
if (_modmap != null)
{
KeyValue mapped = _modmap.get(Modmap.M.Fn, k);
if (mapped != null)
return mapped;
}
String name = null;
switch (k.getKind())
{
case Char:
case String:
KeyValue r = ComposeKey.apply(ComposeKeyData.fn, k);
return (r != null) ? r : k;
case Keyevent: name = apply_fn_keyevent(k.getKeyevent()); break;
case Event: name = apply_fn_event(k.getEvent()); break;
case Placeholder: name = apply_fn_placeholder(k.getPlaceholder()); break;
case Editing: name = apply_fn_editing(k.getEditing()); break;
}
return (name == null) ? k : KeyValue.getKeyByName(name);
}
private static String apply_fn_keyevent(int code)
{
switch (code)
{
case KeyEvent.KEYCODE_DPAD_UP: return "page_up";
case KeyEvent.KEYCODE_DPAD_DOWN: return "page_down";
case KeyEvent.KEYCODE_DPAD_LEFT: return "home";
case KeyEvent.KEYCODE_DPAD_RIGHT: return "end";
case KeyEvent.KEYCODE_ESCAPE: return "insert";
case KeyEvent.KEYCODE_TAB: return "\\t";
case KeyEvent.KEYCODE_PAGE_UP:
case KeyEvent.KEYCODE_PAGE_DOWN:
case KeyEvent.KEYCODE_MOVE_HOME:
case KeyEvent.KEYCODE_MOVE_END: return "removed";
default: return null;
}
}
private static String apply_fn_event(KeyValue.Event ev)
{
switch (ev)
{
case SWITCH_NUMERIC: return "switch_greekmath";
default: return null;
}
}
private static String apply_fn_placeholder(KeyValue.Placeholder p)
{
switch (p)
{
case F11: return "f11";
case F12: return "f12";
case SHINDOT: return "shindot";
case SINDOT: return "sindot";
case OLE: return "ole";
case METEG: return "meteg";
default: return null;
}
}
private static String apply_fn_editing(KeyValue.Editing p)
{
switch (p)
{
case UNDO: return "redo";
case PASTE: return "pasteAsPlainText";
default: return null;
}
}
private static KeyValue apply_ctrl(KeyValue k)
{
if (_modmap != null)
{
KeyValue mapped = _modmap.get(Modmap.M.Ctrl, k);
// Do not return the modified character right away, first turn it into a
// key event.
if (mapped != null)
k = mapped;
}
return turn_into_keyevent(k);
}
private static KeyValue turn_into_keyevent(KeyValue k)
{
if (k.getKind() != KeyValue.Kind.Char)
return k;
int e;
switch (k.getChar())
{
case 'a': e = KeyEvent.KEYCODE_A; break;
case 'b': e = KeyEvent.KEYCODE_B; break;
case 'c': e = KeyEvent.KEYCODE_C; break;
case 'd': e = KeyEvent.KEYCODE_D; break;
case 'e': e = KeyEvent.KEYCODE_E; break;
case 'f': e = KeyEvent.KEYCODE_F; break;
case 'g': e = KeyEvent.KEYCODE_G; break;
case 'h': e = KeyEvent.KEYCODE_H; break;
case 'i': e = KeyEvent.KEYCODE_I; break;
case 'j': e = KeyEvent.KEYCODE_J; break;
case 'k': e = KeyEvent.KEYCODE_K; break;
case 'l': e = KeyEvent.KEYCODE_L; break;
case 'm': e = KeyEvent.KEYCODE_M; break;
case 'n': e = KeyEvent.KEYCODE_N; break;
case 'o': e = KeyEvent.KEYCODE_O; break;
case 'p': e = KeyEvent.KEYCODE_P; break;
case 'q': e = KeyEvent.KEYCODE_Q; break;
case 'r': e = KeyEvent.KEYCODE_R; break;
case 's': e = KeyEvent.KEYCODE_S; break;
case 't': e = KeyEvent.KEYCODE_T; break;
case 'u': e = KeyEvent.KEYCODE_U; break;
case 'v': e = KeyEvent.KEYCODE_V; break;
case 'w': e = KeyEvent.KEYCODE_W; break;
case 'x': e = KeyEvent.KEYCODE_X; break;
case 'y': e = KeyEvent.KEYCODE_Y; break;
case 'z': e = KeyEvent.KEYCODE_Z; break;
case '0': e = KeyEvent.KEYCODE_0; break;
case '1': e = KeyEvent.KEYCODE_1; break;
case '2': e = KeyEvent.KEYCODE_2; break;
case '3': e = KeyEvent.KEYCODE_3; break;
case '4': e = KeyEvent.KEYCODE_4; break;
case '5': e = KeyEvent.KEYCODE_5; break;
case '6': e = KeyEvent.KEYCODE_6; break;
case '7': e = KeyEvent.KEYCODE_7; break;
case '8': e = KeyEvent.KEYCODE_8; break;
case '9': e = KeyEvent.KEYCODE_9; break;
case '`': e = KeyEvent.KEYCODE_GRAVE; break;
case '-': e = KeyEvent.KEYCODE_MINUS; break;
case '=': e = KeyEvent.KEYCODE_EQUALS; break;
case '[': e = KeyEvent.KEYCODE_LEFT_BRACKET; break;
case ']': e = KeyEvent.KEYCODE_RIGHT_BRACKET; break;
case '\\': e = KeyEvent.KEYCODE_BACKSLASH; break;
case ';': e = KeyEvent.KEYCODE_SEMICOLON; break;
case '\'': e = KeyEvent.KEYCODE_APOSTROPHE; break;
case '/': e = KeyEvent.KEYCODE_SLASH; break;
case '@': e = KeyEvent.KEYCODE_AT; break;
case '+': e = KeyEvent.KEYCODE_PLUS; break;
case ',': e = KeyEvent.KEYCODE_COMMA; break;
case '.': e = KeyEvent.KEYCODE_PERIOD; break;
case '*': e = KeyEvent.KEYCODE_STAR; break;
case '#': e = KeyEvent.KEYCODE_POUND; break;
case '(': e = KeyEvent.KEYCODE_NUMPAD_LEFT_PAREN; break;
case ')': e = KeyEvent.KEYCODE_NUMPAD_RIGHT_PAREN; break;
case ' ': e = KeyEvent.KEYCODE_SPACE; break;
default: return k;
}
return k.withKeyevent(e);
}
/** Modify a key affected by a round-trip or a clockwise circle gesture. */
private static KeyValue apply_gesture(KeyValue k)
{
KeyValue modified = apply_shift(k);
if (modified != null && !modified.equals(k))
return modified;
modified = apply_fn(k);
if (modified != null && !modified.equals(k))
return modified;
String name = null;
switch (k.getKind())
{
case Modifier:
switch (k.getModifier())
{
case SHIFT: name = "capslock"; break;
}
break;
case Keyevent:
switch (k.getKeyevent())
{
case KeyEvent.KEYCODE_DEL: name = "delete_word"; break;
case KeyEvent.KEYCODE_FORWARD_DEL: name = "forward_delete_word"; break;
}
break;
}
return (name == null) ? k : KeyValue.getKeyByName(name);
}
private static KeyValue apply_selection_mode(KeyValue k)
{
String name = null;
switch (k.getKind())
{
case Char:
switch (k.getChar())
{
case ' ': name = "selection_cancel"; break;
}
break;
case Slider:
switch (k.getSlider())
{
case Cursor_left: name = "selection_cursor_left"; break;
case Cursor_right: name = "selection_cursor_right"; break;
}
break;
case Keyevent:
switch (k.getKeyevent())
{
case KeyEvent.KEYCODE_ESCAPE: name = "selection_cancel"; break;
}
break;
}
return (name == null) ? k : KeyValue.getKeyByName(name);
}
/** Compose the precomposed initial with the medial [kv]. */
private static KeyValue combine_hangul_initial(KeyValue kv, int precomposed)
{
switch (kv.getKind())
{
case Char:
return combine_hangul_initial(kv, kv.getChar(), precomposed);
case Hangul_initial:
// No initials are expected to compose, grey out
return kv.withFlags(kv.getFlags() | KeyValue.FLAG_GREYED);
default:
return kv;
}
}
private static KeyValue combine_hangul_initial(KeyValue kv, char medial,
int precomposed)
{
int medial_idx;
switch (medial)
{
// Vowels
case 'ㅏ': medial_idx = 0; break;
case 'ㅐ': medial_idx = 1; break;
case 'ㅑ': medial_idx = 2; break;
case 'ㅒ': medial_idx = 3; break;
case 'ㅓ': medial_idx = 4; break;
case 'ㅔ': medial_idx = 5; break;
case 'ㅕ': medial_idx = 6; break;
case 'ㅖ': medial_idx = 7; break;
case 'ㅗ': medial_idx = 8; break;
case 'ㅘ': medial_idx = 9; break;
case 'ㅙ': medial_idx = 10; break;
case 'ㅚ': medial_idx = 11; break;
case 'ㅛ': medial_idx = 12; break;
case 'ㅜ': medial_idx = 13; break;
case 'ㅝ': medial_idx = 14; break;
case 'ㅞ': medial_idx = 15; break;
case 'ㅟ': medial_idx = 16; break;
case 'ㅠ': medial_idx = 17; break;
case 'ㅡ': medial_idx = 18; break;
case 'ㅢ': medial_idx = 19; break;
case 'ㅣ': medial_idx = 20; break;
// Grey-out uncomposable characters
default: return kv.withFlags(kv.getFlags() | KeyValue.FLAG_GREYED);
}
return KeyValue.makeHangulMedial(precomposed, medial_idx);
}
/** Combine the precomposed medial with the final [kv]. */
private static KeyValue combine_hangul_medial(KeyValue kv, int precomposed)
{
switch (kv.getKind())
{
case Char:
return combine_hangul_medial(kv, kv.getChar(), precomposed);
case Hangul_initial:
// Finals that can also be initials have this kind.
return combine_hangul_medial(kv, kv.getString().charAt(0), precomposed);
default:
return kv;
}
}
private static KeyValue combine_hangul_medial(KeyValue kv, char c,
int precomposed)
{
int final_idx;
switch (c)
{
case ' ': final_idx = 0; break;
case 'ㄱ': final_idx = 1; break;
case 'ㄲ': final_idx = 2; break;
case 'ㄳ': final_idx = 3; break;
case 'ㄴ': final_idx = 4; break;
case 'ㄵ': final_idx = 5; break;
case 'ㄶ': final_idx = 6; break;
case 'ㄷ': final_idx = 7; break;
case 'ㄹ': final_idx = 8; break;
case 'ㄺ': final_idx = 9; break;
case 'ㄻ': final_idx = 10; break;
case 'ㄼ': final_idx = 11; break;
case 'ㄽ': final_idx = 12; break;
case 'ㄾ': final_idx = 13; break;
case 'ㄿ': final_idx = 14; break;
case 'ㅀ': final_idx = 15; break;
case 'ㅁ': final_idx = 16; break;
case 'ㅂ': final_idx = 17; break;
case 'ㅄ': final_idx = 18; break;
case 'ㅅ': final_idx = 19; break;
case 'ㅆ': final_idx = 20; break;
case 'ㅇ': final_idx = 21; break;
case 'ㅈ': final_idx = 22; break;
case 'ㅊ': final_idx = 23; break;
case 'ㅋ': final_idx = 24; break;
case 'ㅌ': final_idx = 25; break;
case 'ㅍ': final_idx = 26; break;
case 'ㅎ': final_idx = 27; break;
// Grey-out uncomposable characters
default: return kv.withFlags(kv.getFlags() | KeyValue.FLAG_GREYED);
}
return KeyValue.makeHangulFinal(precomposed, final_idx);
}
}

View file

@ -0,0 +1,866 @@
package juloo.keyboard2;
import android.view.KeyEvent;
import java.util.HashMap;
public final class KeyValue implements Comparable<KeyValue>
{
public static enum Event
{
CONFIG,
SWITCH_TEXT,
SWITCH_NUMERIC,
SWITCH_EMOJI,
SWITCH_BACK_EMOJI,
SWITCH_CLIPBOARD,
SWITCH_BACK_CLIPBOARD,
CHANGE_METHOD_PICKER,
CHANGE_METHOD_AUTO,
ACTION,
SWITCH_FORWARD,
SWITCH_BACKWARD,
SWITCH_GREEKMATH,
CAPS_LOCK,
SWITCH_VOICE_TYPING,
SWITCH_VOICE_TYPING_CHOOSER,
}
// Must be evaluated in the reverse order of their values.
public static enum Modifier
{
SHIFT,
GESTURE,
CTRL,
ALT,
META,
DOUBLE_AIGU,
DOT_ABOVE,
DOT_BELOW,
GRAVE,
AIGU,
CIRCONFLEXE,
TILDE,
CEDILLE,
TREMA,
HORN,
HOOK_ABOVE,
DOUBLE_GRAVE,
SUPERSCRIPT,
SUBSCRIPT,
RING,
CARON,
MACRON,
ORDINAL,
ARROWS,
BOX,
OGONEK,
SLASH,
ARROW_RIGHT,
BREVE,
BAR,
FN,
SELECTION_MODE,
} // Last is be applied first
public static enum Editing
{
COPY,
PASTE,
CUT,
SELECT_ALL,
PASTE_PLAIN,
UNDO,
REDO,
// Android context menu actions
REPLACE,
SHARE,
ASSIST,
AUTOFILL,
DELETE_WORD,
FORWARD_DELETE_WORD,
SELECTION_CANCEL,
}
public static enum Placeholder
{
REMOVED,
COMPOSE_CANCEL,
F11,
F12,
SHINDOT,
SINDOT,
OLE,
METEG
}
public static enum Kind
{
Char, Keyevent, Event, Compose_pending, Hangul_initial, Hangul_medial,
Modifier, Editing, Placeholder,
String, // [_payload] is also the string to output, value is unused.
Slider, // [_payload] is a [KeyValue.Slider], value is slider repeatition.
Macro, // [_payload] is a [KeyValue.Macro], value is unused.
}
private static final int FLAGS_OFFSET = 20;
private static final int KIND_OFFSET = 28;
// Key stay activated when pressed once.
public static final int FLAG_LATCH = (1 << FLAGS_OFFSET << 0);
// Key can be locked by typing twice when enabled in settings
public static final int FLAG_DOUBLE_TAP_LOCK = (1 << FLAGS_OFFSET << 1);
// Special keys are not repeated.
// Special latchable keys don't clear latched modifiers.
public static final int FLAG_SPECIAL = (1 << FLAGS_OFFSET << 2);
// Whether the symbol should be greyed out. For example, keys that are not
// part of the pending compose sequence.
public static final int FLAG_GREYED = (1 << FLAGS_OFFSET << 3);
// The special font is required to render this key.
public static final int FLAG_KEY_FONT = (1 << FLAGS_OFFSET << 4);
// 25% smaller symbols
public static final int FLAG_SMALLER_FONT = (1 << FLAGS_OFFSET << 5);
// Dimmer symbol
public static final int FLAG_SECONDARY = (1 << FLAGS_OFFSET << 6);
// Free: (1 << FLAGS_OFFSET << 7)
// Ranges for the different components
private static final int FLAGS_BITS = (0b11111111 << FLAGS_OFFSET); // 8 bits wide
private static final int KIND_BITS = (0b1111 << KIND_OFFSET); // 4 bits wide
private static final int VALUE_BITS = 0b11111111111111111111; // 20 bits wide
static
{
check((FLAGS_BITS & KIND_BITS) == 0); // No overlap with kind
check(~(FLAGS_BITS | KIND_BITS) == VALUE_BITS); // No overlap with value
check((FLAGS_BITS | KIND_BITS | VALUE_BITS) == ~0); // No holes
// No kind is out of range
check((((Kind.values().length - 1) << KIND_OFFSET) & ~KIND_BITS) == 0);
}
/** [_payload.toString()] is the symbol that is rendered on the keyboard. */
private final Comparable _payload;
/** This field encodes three things: Kind (KIND_BITS), flags (FLAGS_BITS) and
value (VALUE_BITS).
The meaning of the value depends on the kind. */
private final int _code;
public Kind getKind()
{
return Kind.values()[(_code & KIND_BITS) >>> KIND_OFFSET];
}
public int getFlags()
{
return (_code & FLAGS_BITS);
}
public boolean hasFlagsAny(int has)
{
return ((_code & has) != 0);
}
/** The string to render on the keyboard.
When [getKind() == Kind.String], also the string to send. */
public String getString()
{
return _payload.toString();
}
/** Defined only when [getKind() == Kind.Char]. */
public char getChar()
{
return (char)(_code & VALUE_BITS);
}
/** Defined only when [getKind() == Kind.Keyevent]. */
public int getKeyevent()
{
return (_code & VALUE_BITS);
}
/** Defined only when [getKind() == Kind.Event]. */
public Event getEvent()
{
return Event.values()[(_code & VALUE_BITS)];
}
/** Defined only when [getKind() == Kind.Modifier]. */
public Modifier getModifier()
{
return Modifier.values()[(_code & VALUE_BITS)];
}
/** Defined only when [getKind() == Kind.Editing]. */
public Editing getEditing()
{
return Editing.values()[(_code & VALUE_BITS)];
}
/** Defined only when [getKind() == Kind.Placeholder]. */
public Placeholder getPlaceholder()
{
return Placeholder.values()[(_code & VALUE_BITS)];
}
/** Defined only when [getKind() == Kind.Compose_pending]. */
public int getPendingCompose()
{
return (_code & VALUE_BITS);
}
/** Defined only when [getKind()] is [Kind.Hangul_initial] or
[Kind.Hangul_medial]. */
public int getHangulPrecomposed()
{
return (_code & VALUE_BITS);
}
/** Defined only when [getKind() == Kind.Slider]. */
public Slider getSlider()
{
return (Slider)_payload;
}
/** Defined only when [getKind() == Kind.Slider]. */
public int getSliderRepeat()
{
return ((int)(short)(_code & VALUE_BITS));
}
/** Defined only when [getKind() == Kind.Macro]. */
public KeyValue[] getMacro()
{
return ((Macro)_payload).keys;
}
/* Update the char and the symbol. */
public KeyValue withChar(char c)
{
return new KeyValue(String.valueOf(c), Kind.Char, c,
getFlags() & ~(FLAG_KEY_FONT | FLAG_SMALLER_FONT));
}
public KeyValue withKeyevent(int code)
{
return new KeyValue(getString(), Kind.Keyevent, code, getFlags());
}
public KeyValue withFlags(int f)
{
return new KeyValue(_payload, _code, _code, f);
}
public KeyValue withSymbol(String symbol)
{
int flags = getFlags() & ~(FLAG_KEY_FONT | FLAG_SMALLER_FONT);
switch (getKind())
{
case Char:
case Keyevent:
case Event:
case Compose_pending:
case Hangul_initial:
case Hangul_medial:
case Modifier:
case Editing:
case Placeholder:
if (symbol.length() > 1)
flags |= FLAG_SMALLER_FONT;
return new KeyValue(symbol, _code, _code, flags);
case Macro:
return makeMacro(symbol, getMacro(), flags);
default:
return makeMacro(symbol, new KeyValue[]{ this }, flags);
}
}
@Override
public boolean equals(Object obj)
{
return sameKey((KeyValue)obj);
}
@Override
public int compareTo(KeyValue snd)
{
// Compare the kind and value first, then the flags.
int d = (_code & ~FLAGS_BITS) - (snd._code & ~FLAGS_BITS);
if (d != 0)
return d;
d = _code - snd._code;
if (d != 0)
return d;
// Calls [compareTo] assuming that if [_code] matches, then [_payload] are
// of the same class.
return _payload.compareTo(snd._payload);
}
/** Type-safe alternative to [equals]. */
public boolean sameKey(KeyValue snd)
{
if (snd == null)
return false;
return _code == snd._code && _payload.compareTo(snd._payload) == 0;
}
@Override
public int hashCode()
{
return _payload.hashCode() + _code;
}
public String toString()
{
int value = _code & VALUE_BITS;
return "[KeyValue " + getKind().toString() + "+" + getFlags() + "+" + value + " \"" + getString() + "\"]";
}
private KeyValue(Comparable p, int kind, int value, int flags)
{
if (p == null)
throw new NullPointerException("KeyValue payload cannot be null");
_payload = p;
_code = (kind & KIND_BITS) | (flags & FLAGS_BITS) | (value & VALUE_BITS);
}
public KeyValue(Comparable p, Kind k, int v, int f)
{
this(p, (k.ordinal() << KIND_OFFSET), v, f);
}
private static KeyValue charKey(String symbol, char c, int flags)
{
return new KeyValue(symbol, Kind.Char, c, flags);
}
private static KeyValue charKey(int symbol, char c, int flags)
{
return charKey(String.valueOf((char)symbol), c, flags | FLAG_KEY_FONT);
}
private static KeyValue modifierKey(String symbol, Modifier m, int flags)
{
if (symbol.length() > 1)
flags |= FLAG_SMALLER_FONT;
return new KeyValue(symbol, Kind.Modifier, m.ordinal(),
FLAG_LATCH | FLAG_SPECIAL | FLAG_SECONDARY | flags);
}
private static KeyValue modifierKey(int symbol, Modifier m, int flags)
{
return modifierKey(String.valueOf((char)symbol), m, flags | FLAG_KEY_FONT);
}
private static KeyValue diacritic(int symbol, Modifier m)
{
return new KeyValue(String.valueOf((char)symbol), Kind.Modifier, m.ordinal(),
FLAG_LATCH | FLAG_SPECIAL | FLAG_KEY_FONT);
}
private static KeyValue eventKey(String symbol, Event e, int flags)
{
return new KeyValue(symbol, Kind.Event, e.ordinal(), flags | FLAG_SPECIAL | FLAG_SECONDARY);
}
private static KeyValue eventKey(int symbol, Event e, int flags)
{
return eventKey(String.valueOf((char)symbol), e, flags | FLAG_KEY_FONT);
}
public static KeyValue keyeventKey(String symbol, int code, int flags)
{
return new KeyValue(symbol, Kind.Keyevent, code, flags | FLAG_SECONDARY);
}
public static KeyValue keyeventKey(int symbol, int code, int flags)
{
return keyeventKey(String.valueOf((char)symbol), code, flags | FLAG_KEY_FONT);
}
private static KeyValue editingKey(String symbol, Editing action, int flags)
{
return new KeyValue(symbol, Kind.Editing, action.ordinal(),
flags | FLAG_SPECIAL | FLAG_SECONDARY);
}
private static KeyValue editingKey(String symbol, Editing action)
{
return editingKey(symbol, action, FLAG_SMALLER_FONT);
}
private static KeyValue editingKey(int symbol, Editing action)
{
return editingKey(String.valueOf((char)symbol), action, FLAG_KEY_FONT);
}
/** A key that slides the property specified by [s] by the amount specified
with [repeatition]. */
public static KeyValue sliderKey(Slider s, int repeatition)
{
// Casting to a short then back to a int to preserve the sign bit.
return new KeyValue(s, Kind.Slider, (short)repeatition & 0xFFFF,
FLAG_SPECIAL | FLAG_SECONDARY | FLAG_KEY_FONT);
}
/** A key that do nothing but has a unique ID. */
private static KeyValue placeholderKey(Placeholder id)
{
return new KeyValue("", Kind.Placeholder, id.ordinal(), 0);
}
private static KeyValue placeholderKey(int symbol, Placeholder id, int flags)
{
return new KeyValue(String.valueOf((char)symbol), Kind.Placeholder,
id.ordinal(), flags | FLAG_KEY_FONT);
}
public static KeyValue makeStringKey(String str)
{
return makeStringKey(str, 0);
}
public static KeyValue makeCharKey(char c)
{
return makeCharKey(c, null, 0);
}
public static KeyValue makeCharKey(char c, String symbol, int flags)
{
if (symbol == null)
symbol = String.valueOf(c);
return new KeyValue(symbol, Kind.Char, c, flags);
}
public static KeyValue makeCharKey(int symbol, char c, int flags)
{
return makeCharKey(c, String.valueOf((char)symbol), flags | FLAG_KEY_FONT);
}
public static KeyValue makeComposePending(String symbol, int state, int flags)
{
return new KeyValue(symbol, Kind.Compose_pending, state,
flags | FLAG_LATCH);
}
public static KeyValue makeComposePending(int symbol, int state, int flags)
{
return makeComposePending(String.valueOf((char)symbol), state,
flags | FLAG_KEY_FONT);
}
public static KeyValue makeHangulInitial(String symbol, int initial_idx)
{
return new KeyValue(symbol, Kind.Hangul_initial, initial_idx * 588 + 44032,
FLAG_LATCH);
}
public static KeyValue makeHangulMedial(int precomposed, int medial_idx)
{
precomposed += medial_idx * 28;
return new KeyValue(String.valueOf((char)precomposed), Kind.Hangul_medial,
precomposed, FLAG_LATCH);
}
public static KeyValue makeHangulFinal(int precomposed, int final_idx)
{
precomposed += final_idx;
return KeyValue.makeCharKey((char)precomposed);
}
public static KeyValue makeActionKey(String symbol)
{
return eventKey(symbol, Event.ACTION, FLAG_SMALLER_FONT);
}
/** Make a key that types a string. A char key is returned for a string of
length 1. */
public static KeyValue makeStringKey(String str, int flags)
{
if (str.length() == 1)
return new KeyValue(str, Kind.Char, str.charAt(0), flags);
else
return new KeyValue(str, Kind.String, 0, flags | FLAG_SMALLER_FONT);
}
public static KeyValue makeMacro(String symbol, KeyValue[] keys, int flags)
{
if (symbol.length() > 1)
flags |= FLAG_SMALLER_FONT;
return new KeyValue(new Macro(keys, symbol), Kind.Macro, 0, flags);
}
/** Make a modifier key for passing to [KeyModifier]. */
public static KeyValue makeInternalModifier(Modifier mod)
{
return new KeyValue("", Kind.Modifier, mod.ordinal(), 0);
}
/** Return a key by its name. If the given name doesn't correspond to any
special key, it is parsed with [KeyValueParser]. */
public static KeyValue getKeyByName(String name)
{
KeyValue k = getSpecialKeyByName(name);
if (k != null)
return k;
try
{
return KeyValueParser.parse(name);
}
catch (KeyValueParser.ParseError _e)
{
return makeStringKey(name);
}
}
public static KeyValue getSpecialKeyByName(String name)
{
switch (name)
{
/* These symbols have special meaning when in `srcs/layouts` and are
escaped in standard layouts. The backslash is not stripped when parsed
from the custom layout option. */
case "\\?": return makeStringKey("?");
case "\\#": return makeStringKey("#");
case "\\@": return makeStringKey("@");
case "\\\\": return makeStringKey("\\");
/* Modifiers and dead-keys */
case "shift": return modifierKey(0xE00A, Modifier.SHIFT, FLAG_DOUBLE_TAP_LOCK);
case "ctrl": return modifierKey("Ctrl", Modifier.CTRL, 0);
case "alt": return modifierKey("Alt", Modifier.ALT, 0);
case "accent_aigu": return diacritic(0xE050, Modifier.AIGU);
case "accent_caron": return diacritic(0xE051, Modifier.CARON);
case "accent_cedille": return diacritic(0xE052, Modifier.CEDILLE);
case "accent_circonflexe": return diacritic(0xE053, Modifier.CIRCONFLEXE);
case "accent_grave": return diacritic(0xE054, Modifier.GRAVE);
case "accent_macron": return diacritic(0xE055, Modifier.MACRON);
case "accent_ring": return diacritic(0xE056, Modifier.RING);
case "accent_tilde": return diacritic(0xE057, Modifier.TILDE);
case "accent_trema": return diacritic(0xE058, Modifier.TREMA);
case "accent_ogonek": return diacritic(0xE059, Modifier.OGONEK);
case "accent_dot_above": return diacritic(0xE05A, Modifier.DOT_ABOVE);
case "accent_double_aigu": return diacritic(0xE05B, Modifier.DOUBLE_AIGU);
case "accent_slash": return diacritic(0xE05C, Modifier.SLASH);
case "accent_arrow_right": return diacritic(0xE05D, Modifier.ARROW_RIGHT);
case "accent_breve": return diacritic(0xE05E, Modifier.BREVE);
case "accent_bar": return diacritic(0xE05F, Modifier.BAR);
case "accent_dot_below": return diacritic(0xE060, Modifier.DOT_BELOW);
case "accent_horn": return diacritic(0xE061, Modifier.HORN);
case "accent_hook_above": return diacritic(0xE062, Modifier.HOOK_ABOVE);
case "accent_double_grave": return diacritic(0xE063, Modifier.DOUBLE_GRAVE);
case "superscript": return modifierKey("Sup", Modifier.SUPERSCRIPT, 0);
case "subscript": return modifierKey("Sub", Modifier.SUBSCRIPT, 0);
case "ordinal": return modifierKey("Ord", Modifier.ORDINAL, 0);
case "arrows": return modifierKey("Arr", Modifier.ARROWS, 0);
case "box": return modifierKey("Box", Modifier.BOX, 0);
case "fn": return modifierKey("Fn", Modifier.FN, 0);
case "meta": return modifierKey("Meta", Modifier.META, 0);
/* Combining diacritics */
/* Glyphs is the corresponding dead-key + 0x0100. */
case "combining_dot_above": return makeCharKey(0xE15A, '\u0307', 0);
case "combining_double_aigu": return makeCharKey(0xE15B, '\u030B', 0);
case "combining_slash": return makeCharKey(0xE15C, '\u0337', 0);
case "combining_arrow_right": return makeCharKey(0xE15D, '\u20D7', 0);
case "combining_breve": return makeCharKey(0xE15E, '\u0306', 0);
case "combining_bar": return makeCharKey(0xE15F, '\u0335', 0);
case "combining_aigu": return makeCharKey(0xE150, '\u0301', 0);
case "combining_caron": return makeCharKey(0xE151, '\u030C', 0);
case "combining_cedille": return makeCharKey(0xE152, '\u0327', 0);
case "combining_circonflexe": return makeCharKey(0xE153, '\u0302', 0);
case "combining_grave": return makeCharKey(0xE154, '\u0300', 0);
case "combining_macron": return makeCharKey(0xE155, '\u0304', 0);
case "combining_ring": return makeCharKey(0xE156, '\u030A', 0);
case "combining_tilde": return makeCharKey(0xE157, '\u0303', 0);
case "combining_trema": return makeCharKey(0xE158, '\u0308', 0);
case "combining_ogonek": return makeCharKey(0xE159, '\u0328', 0);
case "combining_dot_below": return makeCharKey(0xE160, '\u0323', 0);
case "combining_horn": return makeCharKey(0xE161, '\u031B', 0);
case "combining_hook_above": return makeCharKey(0xE162, '\u0309', 0);
/* Combining diacritics that do not have a corresponding dead keys start
at 0xE200. */
case "combining_vertical_tilde": return makeCharKey(0xE200, '\u033E', 0);
case "combining_inverted_breve": return makeCharKey(0xE201, '\u0311', 0);
case "combining_pokrytie": return makeCharKey(0xE202, '\u0487', 0);
case "combining_slavonic_psili": return makeCharKey(0xE203, '\u0486', 0);
case "combining_slavonic_dasia": return makeCharKey(0xE204, '\u0485', 0);
case "combining_payerok": return makeCharKey(0xE205, '\uA67D', 0);
case "combining_titlo": return makeCharKey(0xE206, '\u0483', 0);
case "combining_vzmet": return makeCharKey(0xE207, '\uA66F', 0);
case "combining_arabic_v": return makeCharKey(0xE208, '\u065A', 0);
case "combining_arabic_inverted_v": return makeCharKey(0xE209, '\u065B', 0);
case "combining_shaddah": return makeCharKey(0xE210, '\u0651', 0);
case "combining_sukun": return makeCharKey(0xE211, '\u0652', 0);
case "combining_fatha": return makeCharKey(0xE212, '\u064E', 0);
case "combining_dammah": return makeCharKey(0xE213, '\u064F', 0);
case "combining_kasra": return makeCharKey(0xE214, '\u0650', 0);
case "combining_hamza_above": return makeCharKey(0xE215, '\u0654', 0);
case "combining_hamza_below": return makeCharKey(0xE216, '\u0655', 0);
case "combining_alef_above": return makeCharKey(0xE217, '\u0670', 0);
case "combining_fathatan": return makeCharKey(0xE218, '\u064B', 0);
case "combining_kasratan": return makeCharKey(0xE219, '\u064D', 0);
case "combining_dammatan": return makeCharKey(0xE220, '\u064C', 0);
case "combining_alef_below": return makeCharKey(0xE221, '\u0656', 0);
case "combining_kavyka": return makeCharKey(0xE222, '\uA67C', 0);
case "combining_palatalization": return makeCharKey(0xE223, '\u0484', 0);
/* Special event keys */
case "config": return eventKey(0xE004, Event.CONFIG, FLAG_SMALLER_FONT);
case "switch_text": return eventKey("ABC", Event.SWITCH_TEXT, FLAG_SMALLER_FONT);
case "switch_numeric": return eventKey("123+", Event.SWITCH_NUMERIC, FLAG_SMALLER_FONT);
case "switch_emoji": return eventKey(0xE001, Event.SWITCH_EMOJI, FLAG_SMALLER_FONT);
case "switch_back_emoji": return eventKey("ABC", Event.SWITCH_BACK_EMOJI, 0);
case "switch_clipboard": return eventKey(0xE017, Event.SWITCH_CLIPBOARD, 0);
case "switch_back_clipboard": return eventKey("ABC", Event.SWITCH_BACK_CLIPBOARD, 0);
case "switch_forward": return eventKey(0xE013, Event.SWITCH_FORWARD, FLAG_SMALLER_FONT);
case "switch_backward": return eventKey(0xE014, Event.SWITCH_BACKWARD, FLAG_SMALLER_FONT);
case "switch_greekmath": return eventKey("πλ∇¬", Event.SWITCH_GREEKMATH, FLAG_SMALLER_FONT);
case "change_method": return eventKey(0xE009, Event.CHANGE_METHOD_PICKER, FLAG_SMALLER_FONT);
case "change_method_prev": return eventKey(0xE009, Event.CHANGE_METHOD_AUTO, FLAG_SMALLER_FONT);
case "action": return eventKey("Action", Event.ACTION, FLAG_SMALLER_FONT); // Will always be replaced
case "capslock": return eventKey(0xE012, Event.CAPS_LOCK, 0);
case "voice_typing": return eventKey(0xE015, Event.SWITCH_VOICE_TYPING, FLAG_SMALLER_FONT);
case "voice_typing_chooser": return eventKey(0xE015, Event.SWITCH_VOICE_TYPING_CHOOSER, FLAG_SMALLER_FONT);
/* Key events */
case "esc": return keyeventKey("Esc", KeyEvent.KEYCODE_ESCAPE, FLAG_SMALLER_FONT);
case "enter": return keyeventKey(0xE00E, KeyEvent.KEYCODE_ENTER, 0);
case "up": return keyeventKey(0xE005, KeyEvent.KEYCODE_DPAD_UP, 0);
case "right": return keyeventKey(0xE006, KeyEvent.KEYCODE_DPAD_RIGHT, FLAG_SMALLER_FONT);
case "down": return keyeventKey(0xE007, KeyEvent.KEYCODE_DPAD_DOWN, 0);
case "left": return keyeventKey(0xE008, KeyEvent.KEYCODE_DPAD_LEFT, FLAG_SMALLER_FONT);
case "page_up": return keyeventKey(0xE002, KeyEvent.KEYCODE_PAGE_UP, 0);
case "page_down": return keyeventKey(0xE003, KeyEvent.KEYCODE_PAGE_DOWN, 0);
case "home": return keyeventKey(0xE00B, KeyEvent.KEYCODE_MOVE_HOME, FLAG_SMALLER_FONT);
case "end": return keyeventKey(0xE00C, KeyEvent.KEYCODE_MOVE_END, FLAG_SMALLER_FONT);
case "backspace": return keyeventKey(0xE011, KeyEvent.KEYCODE_DEL, 0);
case "delete": return keyeventKey(0xE010, KeyEvent.KEYCODE_FORWARD_DEL, 0);
case "insert": return keyeventKey("Ins", KeyEvent.KEYCODE_INSERT, FLAG_SMALLER_FONT);
case "f1": return keyeventKey("F1", KeyEvent.KEYCODE_F1, 0);
case "f2": return keyeventKey("F2", KeyEvent.KEYCODE_F2, 0);
case "f3": return keyeventKey("F3", KeyEvent.KEYCODE_F3, 0);
case "f4": return keyeventKey("F4", KeyEvent.KEYCODE_F4, 0);
case "f5": return keyeventKey("F5", KeyEvent.KEYCODE_F5, 0);
case "f6": return keyeventKey("F6", KeyEvent.KEYCODE_F6, 0);
case "f7": return keyeventKey("F7", KeyEvent.KEYCODE_F7, 0);
case "f8": return keyeventKey("F8", KeyEvent.KEYCODE_F8, 0);
case "f9": return keyeventKey("F9", KeyEvent.KEYCODE_F9, 0);
case "f10": return keyeventKey("F10", KeyEvent.KEYCODE_F10, 0);
case "f11": return keyeventKey("F11", KeyEvent.KEYCODE_F11, FLAG_SMALLER_FONT);
case "f12": return keyeventKey("F12", KeyEvent.KEYCODE_F12, FLAG_SMALLER_FONT);
case "tab": return keyeventKey(0xE00F, KeyEvent.KEYCODE_TAB, FLAG_SMALLER_FONT);
case "menu": return keyeventKey("Menu", KeyEvent.KEYCODE_MENU, FLAG_SMALLER_FONT);
case "scroll_lock": return keyeventKey("Scrl", KeyEvent.KEYCODE_SCROLL_LOCK, FLAG_SMALLER_FONT);
/* Spaces */
case "\\t": return charKey("\\t", '\t', 0); // Send the tab character
case "\\n": return charKey("\\n", '\n', 0); // Send the newline character
case "space": return charKey(0xE00D, ' ', FLAG_SMALLER_FONT | FLAG_GREYED);
case "nbsp": return charKey("\u237d", '\u00a0', FLAG_SMALLER_FONT);
case "nnbsp": return charKey("\u2423", '\u202F', FLAG_SMALLER_FONT);
/* bidi */
case "lrm": return charKey("", '\u200e', 0); // Send left-to-right mark
case "rlm": return charKey("", '\u200f', 0); // Send right-to-left mark
case "b(": return charKey("(", ')', 0);
case "b)": return charKey(")", '(', 0);
case "b[": return charKey("[", ']', 0);
case "b]": return charKey("]", '[', 0);
case "b{": return charKey("{", '}', 0);
case "b}": return charKey("}", '{', 0);
case "blt": return charKey("<", '>', 0);
case "bgt": return charKey(">", '<', 0);
/* hebrew niqqud */
case "qamats": return charKey("\u05E7\u05B8", '\u05B8', 0); // kamatz
case "patah": return charKey("\u05E4\u05B7", '\u05B7', 0); // patach
case "sheva": return charKey("\u05E9\u05B0", '\u05B0', 0);
case "dagesh": return charKey("\u05D3\u05BC", '\u05BC', 0); // or mapiq
case "hiriq": return charKey("\u05D7\u05B4", '\u05B4', 0);
case "segol": return charKey("\u05E1\u05B6", '\u05B6', 0);
case "tsere": return charKey("\u05E6\u05B5", '\u05B5', 0);
case "holam": return charKey("\u05D5\u05B9", '\u05B9', 0);
case "qubuts": return charKey("\u05E7\u05BB", '\u05BB', 0); // kubuts
case "hataf_patah": return charKey("\u05D7\u05B2\u05E4\u05B7", '\u05B2', 0); // reduced patach
case "hataf_qamats": return charKey("\u05D7\u05B3\u05E7\u05B8", '\u05B3', 0); // reduced kamatz
case "hataf_segol": return charKey("\u05D7\u05B1\u05E1\u05B6", '\u05B1', 0); // reduced segol
case "shindot": return charKey("\u05E9\u05C1", '\u05C1', 0);
case "shindot_placeholder": return placeholderKey(Placeholder.SHINDOT);
case "sindot": return charKey("\u05E9\u05C2", '\u05C2', 0);
case "sindot_placeholder": return placeholderKey(Placeholder.SINDOT);
/* hebrew punctuation */
case "geresh": return charKey("\u05F3", '\u05F3', 0);
case "gershayim": return charKey("\u05F4", '\u05F4', 0);
case "maqaf": return charKey("\u05BE", '\u05BE', 0);
/* hebrew biblical */
case "rafe": return charKey("\u05E4\u05BF", '\u05BF', 0);
case "ole": return charKey("\u05E2\u05AB", '\u05AB', 0);
case "ole_placeholder": return placeholderKey(Placeholder.OLE);
case "meteg": return charKey("\u05DE\u05BD", '\u05BD', 0); // or siluq or sof-pasuq
case "meteg_placeholder": return placeholderKey(Placeholder.METEG);
/* intending/preventing ligature - supported by many scripts*/
case "zwj": return charKey(0xE019, '\u200D', 0); // zero-width joiner (provides ligature)
case "zwnj":
case "halfspace": return charKey(0xE018, '\u200C', 0); // zero-width non joiner
/* Editing keys */
case "copy": return editingKey(0xE030, Editing.COPY);
case "paste": return editingKey(0xE032, Editing.PASTE);
case "cut": return editingKey(0xE031, Editing.CUT);
case "selectAll": return editingKey(0xE033, Editing.SELECT_ALL);
case "shareText": return editingKey(0xE034, Editing.SHARE);
case "pasteAsPlainText": return editingKey(0xE035, Editing.PASTE_PLAIN);
case "undo": return editingKey(0xE036, Editing.UNDO);
case "redo": return editingKey(0xE037, Editing.REDO);
case "delete_word": return editingKey(0xE01B, Editing.DELETE_WORD);
case "forward_delete_word": return editingKey(0xE01C, Editing.FORWARD_DELETE_WORD);
case "cursor_left": return sliderKey(Slider.Cursor_left, 1);
case "cursor_right": return sliderKey(Slider.Cursor_right, 1);
case "cursor_up": return sliderKey(Slider.Cursor_up, 1);
case "cursor_down": return sliderKey(Slider.Cursor_down, 1);
case "selection_cancel": return editingKey("Esc", Editing.SELECTION_CANCEL, FLAG_SMALLER_FONT);
case "selection_cursor_left": return sliderKey(Slider.Selection_cursor_left, -1); // Move the left side of the selection
case "selection_cursor_right": return sliderKey(Slider.Selection_cursor_right, 1);
// These keys are not used
case "replaceText": return editingKey("repl", Editing.REPLACE);
case "textAssist": return editingKey(0xE038, Editing.ASSIST);
case "autofill": return editingKey("auto", Editing.AUTOFILL);
/* The compose key */
case "compose": return makeComposePending(0xE016, ComposeKeyData.compose, FLAG_SECONDARY);
case "compose_cancel": return placeholderKey(0xE01A, Placeholder.COMPOSE_CANCEL, FLAG_SECONDARY);
/* Placeholder keys */
case "removed": return placeholderKey(Placeholder.REMOVED);
case "f11_placeholder": return placeholderKey(Placeholder.F11);
case "f12_placeholder": return placeholderKey(Placeholder.F12);
// Korean Hangul
case "": return makeHangulInitial("", 0);
case "": return makeHangulInitial("", 1);
case "": return makeHangulInitial("", 2);
case "": return makeHangulInitial("", 3);
case "": return makeHangulInitial("", 4);
case "": return makeHangulInitial("", 5);
case "": return makeHangulInitial("", 6);
case "": return makeHangulInitial("", 7);
case "": return makeHangulInitial("", 8);
case "": return makeHangulInitial("", 9);
case "": return makeHangulInitial("", 10);
case "": return makeHangulInitial("", 11);
case "": return makeHangulInitial("", 12);
case "": return makeHangulInitial("", 13);
case "": return makeHangulInitial("", 14);
case "": return makeHangulInitial("", 15);
case "": return makeHangulInitial("", 16);
case "": return makeHangulInitial("", 17);
case "": return makeHangulInitial("", 18);
/* Tamil letters should be smaller on the keyboard. */
case "": case "": case "": case "": case "": case "":
case "": case "": case "": case "": case "": case "":
case "": case "": case "": case "": case "": case "":
case "": case "": case "": case "": case "": case "":
case "": case "": case "": case "": case "": case "":
case "": case "": case "": case "": case "": case "":
case "": case "": case "": case "": case "": case "":
case "": case "": case "": case "": case "": case "":
case "": case "": case "": case "": case "": case "ி":
case "": case "": case "": case "": case "": case "":
case "": case "":
return makeStringKey(name, FLAG_SMALLER_FONT);
/* Sinhala letters to reduced size */
case "": case "": case "": case "": case "":
case "": case "": case "": case "": case "":
case "": case "": case "": case "": case "":
case "": case "": case "": case "": case "":
case "": case "": case "": case "": case "":
case "": case "": case "": case "": case "":
case "": case "": case "": case "": case "":
case "": case "": case "": case "": case "":
case "": case "": case "": case "": case "":
case "": case "": case "": case "": case "":
case "": case "": case "": case "": case "":
case "": case "": case "": case "":
/* Astrological numbers */
case "": case "": case "": case "": case "":
case "": case "": case "": case "": case "":
case "": case "":
/* Diacritics */
case "\u0d81": case "\u0d82": case "\u0d83": case "\u0dca":
case "\u0dcf": case "\u0dd0": case "\u0dd1": case "\u0dd2":
case "\u0dd3": case "\u0dd4": case "\u0dd6": case "\u0dd8":
case "\u0dd9": case "\u0dda": case "\u0ddb": case "\u0ddc":
case "\u0ddd": case "\u0dde": case "\u0ddf":
/* Archaic digits */
case "𑇡": case "𑇢": case "𑇣": case "𑇤": case "𑇥":
case "𑇦": case "𑇧": case "𑇨": case "𑇩": case "𑇪":
case "𑇫": case "𑇬": case "𑇭": case "𑇮": case "𑇯":
case "𑇰": case "𑇱": case "𑇲": case "𑇳": case "𑇴":
/* Exta */
case "": case "": // Rupee is not exclusively Sinhala sign
return makeStringKey(name, FLAG_SMALLER_FONT);
/* Internal keys */
case "selection_mode": return makeInternalModifier(Modifier.SELECTION_MODE);
default: return null;
}
}
// Substitute for [assert], which has no effect on Android.
private static void check(boolean b)
{
if (!b)
throw new RuntimeException("Assertion failure");
}
public static enum Slider
{
Cursor_left(0xE008),
Cursor_right(0xE006),
Cursor_up(0xE005),
Cursor_down(0xE007),
Selection_cursor_left(0xE008),
Selection_cursor_right(0xE006);
final String symbol;
Slider(int symbol_)
{
symbol = String.valueOf((char)symbol_);
}
@Override
public String toString() { return symbol; }
};
public static final class Macro implements Comparable<Macro>
{
public final KeyValue[] keys;
private final String _symbol;
public Macro(KeyValue[] keys_, String sym_)
{
keys = keys_;
_symbol = sym_;
}
public String toString() { return _symbol; }
@Override
public int compareTo(Macro snd)
{
int d = keys.length - snd.keys.length;
if (d != 0) return d;
for (int i = 0; i < keys.length; i++)
{
d = keys[i].compareTo(snd.keys[i]);
if (d != 0) return d;
}
return _symbol.compareTo(snd._symbol);
}
};
}

View file

@ -0,0 +1,289 @@
package juloo.keyboard2;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
Parse a key definition. The syntax for a key definition is:
- [(symbol):(key_action)]
- [:(kind) (attributes):(payload)].
- If [str] doesn't start with a [:] character, it is interpreted as an
arbitrary string key.
[key_action] is:
- ['Arbitrary string']
- [(key_action),(key_action),...]
- [keyevent:(code)]
- [(key_name)]
For the different kinds and attributes, see doc/Possible-key-values.md.
Examples:
- [:str flags=dim,small symbol='MyKey':'My arbitrary string'].
- [:str:'My arbitrary string'].
*/
public final class KeyValueParser
{
static Pattern KEYDEF_TOKEN;
static Pattern QUOTED_PAT;
static Pattern WORD_PAT;
static public KeyValue parse(String input) throws ParseError
{
int symbol_ends = 0;
final int input_len = input.length();
while (symbol_ends < input_len && input.charAt(symbol_ends) != ':')
symbol_ends++;
if (symbol_ends == 0) // Old syntax
return Starting_with_colon.parse(input);
if (symbol_ends == input_len) // String key
return KeyValue.makeStringKey(input);
String symbol = input.substring(0, symbol_ends);
init();
Matcher m = KEYDEF_TOKEN.matcher(input);
m.region(symbol_ends + 1, input_len);
KeyValue first_key = parse_key_def(m);
if (!parse_comma(m)) // Input is a single key def with a specified symbol
return first_key.withSymbol(symbol);
// Input is a macro
ArrayList<KeyValue> keydefs = new ArrayList<KeyValue>();
keydefs.add(first_key);
do { keydefs.add(parse_key_def(m)); }
while (parse_comma(m));
return KeyValue.makeMacro(symbol, keydefs.toArray(new KeyValue[]{}), 0);
}
static void init()
{
if (KEYDEF_TOKEN != null)
return;
KEYDEF_TOKEN = Pattern.compile("'|,|keyevent:|(?:[^\\\\',]+|\\\\.)+");
QUOTED_PAT = Pattern.compile("((?:[^'\\\\]+|\\\\')*)'");
WORD_PAT = Pattern.compile("[a-zA-Z0-9_]+|.");
}
static KeyValue key_by_name_or_str(String str)
{
KeyValue k = KeyValue.getSpecialKeyByName(str);
if (k != null)
return k;
return KeyValue.makeStringKey(str);
}
static KeyValue parse_key_def(Matcher m) throws ParseError
{
if (!match(m, KEYDEF_TOKEN))
parseError("Expected key definition", m);
String token = m.group(0);
switch (token)
{
case "'": return parse_string_keydef(m);
case ",": parseError("Unexpected comma", m); return null;
case "keyevent:": return parse_keyevent_keydef(m);
default: return key_by_name_or_str(remove_escaping(token));
}
}
static KeyValue parse_string_keydef(Matcher m) throws ParseError
{
if (!match(m, QUOTED_PAT))
parseError("Unterminated quoted string", m);
return KeyValue.makeStringKey(remove_escaping(m.group(1)));
}
static KeyValue parse_keyevent_keydef(Matcher m) throws ParseError
{
if (!match(m, WORD_PAT))
parseError("Expected keyevent code", m);
int eventcode = 0;
try { eventcode = Integer.parseInt(m.group(0)); }
catch (Exception _e)
{ parseError("Expected an integer payload", m); }
return KeyValue.keyeventKey("", eventcode, 0);
}
/** Returns [true] if the next token is a comma, [false] if it is the end of the input. Throws an error otherwise. */
static boolean parse_comma(Matcher m) throws ParseError
{
if (!match(m, KEYDEF_TOKEN))
return false;
String token = m.group(0);
if (!token.equals(","))
parseError("Expected comma instead of '"+ token + "'", m);
return true;
}
static String remove_escaping(String s)
{
if (!s.contains("\\"))
return s;
StringBuilder out = new StringBuilder(s.length());
final int len = s.length();
int prev = 0, i = 0;
for (; i < len; i++)
if (s.charAt(i) == '\\')
{
out.append(s, prev, i);
prev = i + 1;
}
out.append(s, prev, i);
return out.toString();
}
/**
Parse a key definition starting with a [:]. This is the old syntax and is
kept for compatibility.
*/
final static class Starting_with_colon
{
static Pattern START_PAT;
static Pattern ATTR_PAT;
static Pattern QUOTED_PAT;
static Pattern PAYLOAD_START_PAT;
static Pattern WORD_PAT;
static public KeyValue parse(String str) throws ParseError
{
String symbol = null;
int flags = 0;
init();
// Kind
Matcher m = START_PAT.matcher(str);
if (!m.lookingAt())
parseError("Expected kind, for example \":str ...\".", m);
String kind = m.group(1);
// Attributes
while (true)
{
if (!match(m, ATTR_PAT))
break;
String attr_name = m.group(1);
String attr_value = parseSingleQuotedString(m);
switch (attr_name)
{
case "flags":
flags = parseFlags(attr_value, m);
break;
case "symbol":
symbol = attr_value;
break;
default:
parseError("Unknown attribute "+attr_name, m);
}
}
// Payload
if (!match(m, PAYLOAD_START_PAT))
parseError("Unexpected character", m);
String payload;
switch (kind)
{
case "str":
payload = parseSingleQuotedString(m);
if (symbol == null)
return KeyValue.makeStringKey(payload, flags);
return KeyValue.makeStringKey(payload, flags).withSymbol(symbol);
case "char":
payload = parsePayloadWord(m);
if (payload.length() != 1)
parseError("Expected a single character payload", m);
return KeyValue.makeCharKey(payload.charAt(0), symbol, flags);
case "keyevent":
payload = parsePayloadWord(m);
int eventcode = 0;
try { eventcode = Integer.parseInt(payload); }
catch (Exception _e)
{ parseError("Expected an integer payload", m); }
if (symbol == null)
symbol = String.valueOf(eventcode);
return KeyValue.keyeventKey(symbol, eventcode, flags);
default: break;
}
parseError("Unknown kind '"+kind+"'", m, 1);
return null; // Unreachable
}
static String parseSingleQuotedString(Matcher m) throws ParseError
{
if (!match(m, QUOTED_PAT))
parseError("Expected quoted string", m);
return m.group(1).replace("\\'", "'");
}
static String parsePayloadWord(Matcher m) throws ParseError
{
if (!match(m, WORD_PAT))
parseError("Expected a word after ':' made of [a-zA-Z0-9_]", m);
return m.group(0);
}
static int parseFlags(String s, Matcher m) throws ParseError
{
int flags = 0;
for (String f : s.split(","))
{
switch (f)
{
case "dim": flags |= KeyValue.FLAG_SECONDARY; break;
case "small": flags |= KeyValue.FLAG_SMALLER_FONT; break;
default: parseError("Unknown flag "+f, m);
}
}
return flags;
}
static boolean match(Matcher m, Pattern pat)
{
try { m.region(m.end(), m.regionEnd()); } catch (Exception _e) {}
m.usePattern(pat);
return m.lookingAt();
}
static void init()
{
if (START_PAT != null)
return;
START_PAT = Pattern.compile(":(\\w+)");
ATTR_PAT = Pattern.compile("\\s*(\\w+)\\s*=");
QUOTED_PAT = Pattern.compile("'(([^'\\\\]+|\\\\')*)'");
PAYLOAD_START_PAT = Pattern.compile("\\s*:");
WORD_PAT = Pattern.compile("[a-zA-Z0-9_]*");
}
}
static boolean match(Matcher m, Pattern pat)
{
try { m.region(m.end(), m.regionEnd()); } catch (Exception _e) {}
m.usePattern(pat);
return m.lookingAt();
}
static void parseError(String msg, Matcher m) throws ParseError
{
parseError(msg, m, m.regionStart());
}
static void parseError(String msg, Matcher m, int i) throws ParseError
{
StringBuilder msg_ = new StringBuilder("Syntax error");
try
{
msg_.append(" at token '").append(m.group(0)).append("'");
} catch (IllegalStateException _e) {}
msg_.append(" at position ");
msg_.append(i);
msg_.append(": ");
msg_.append(msg);
throw new ParseError(msg_.toString());
}
public static class ParseError extends Exception
{
public ParseError(String msg) { super(msg); }
};
}

View file

@ -0,0 +1,522 @@
package juloo.keyboard2;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.inputmethodservice.InputMethodService;
import android.os.Build.VERSION;
import android.os.Handler;
import android.os.IBinder;
import android.text.InputType;
import android.util.Log;
import android.util.LogPrinter;
import android.view.*;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import juloo.keyboard2.prefs.LayoutsPreference;
public class Keyboard2 extends InputMethodService
implements SharedPreferences.OnSharedPreferenceChangeListener
{
private Keyboard2View _keyboardView;
private KeyEventHandler _keyeventhandler;
/** If not 'null', the layout to use instead of [_config.current_layout]. */
private KeyboardData _currentSpecialLayout;
/** Layout associated with the currently selected locale. Not 'null'. */
private KeyboardData _localeTextLayout;
private ViewGroup _emojiPane = null;
private ViewGroup _clipboard_pane = null;
public int actionId; // Action performed by the Action key.
private Handler _handler;
private Config _config;
private FoldStateTracker _foldStateTracker;
/** Layout currently visible before it has been modified. */
KeyboardData current_layout_unmodified()
{
if (_currentSpecialLayout != null)
return _currentSpecialLayout;
KeyboardData layout = null;
int layout_i = _config.get_current_layout();
if (layout_i >= _config.layouts.size())
layout_i = 0;
if (layout_i < _config.layouts.size())
layout = _config.layouts.get(layout_i);
if (layout == null)
layout = _localeTextLayout;
return layout;
}
/** Layout currently visible. */
KeyboardData current_layout()
{
if (_currentSpecialLayout != null)
return _currentSpecialLayout;
return LayoutModifier.modify_layout(current_layout_unmodified());
}
void setTextLayout(int l)
{
_config.set_current_layout(l);
_currentSpecialLayout = null;
_keyboardView.setKeyboard(current_layout());
}
void incrTextLayout(int delta)
{
int s = _config.layouts.size();
setTextLayout((_config.get_current_layout() + delta + s) % s);
}
void setSpecialLayout(KeyboardData l)
{
_currentSpecialLayout = l;
_keyboardView.setKeyboard(l);
}
KeyboardData loadLayout(int layout_id)
{
return KeyboardData.load(getResources(), layout_id);
}
/** Load a layout that contains a numpad. */
KeyboardData loadNumpad(int layout_id)
{
return LayoutModifier.modify_numpad(KeyboardData.load(getResources(), layout_id),
current_layout_unmodified());
}
KeyboardData loadPinentry(int layout_id)
{
return LayoutModifier.modify_pinentry(KeyboardData.load(getResources(), layout_id),
current_layout_unmodified());
}
@Override
public void onCreate()
{
super.onCreate();
SharedPreferences prefs = DirectBootAwarePreferences.get_shared_preferences(this);
_handler = new Handler(getMainLooper());
_keyeventhandler = new KeyEventHandler(this.new Receiver());
_foldStateTracker = new FoldStateTracker(this);
Config.initGlobalConfig(prefs, getResources(), _keyeventhandler, _foldStateTracker.isUnfolded());
prefs.registerOnSharedPreferenceChangeListener(this);
_config = Config.globalConfig();
_keyboardView = (Keyboard2View)inflate_view(R.layout.keyboard);
_keyboardView.reset();
Logs.set_debug_logs(getResources().getBoolean(R.bool.debug_logs));
ClipboardHistoryService.on_startup(this, _keyeventhandler);
_foldStateTracker.setChangedCallback(() -> { refresh_config(); });
}
@Override
public void onDestroy() {
super.onDestroy();
_foldStateTracker.close();
}
private List<InputMethodSubtype> getEnabledSubtypes(InputMethodManager imm)
{
String pkg = getPackageName();
for (InputMethodInfo imi : imm.getEnabledInputMethodList())
if (imi.getPackageName().equals(pkg))
return imm.getEnabledInputMethodSubtypeList(imi, true);
return Arrays.asList();
}
@TargetApi(12)
private ExtraKeys extra_keys_of_subtype(InputMethodSubtype subtype)
{
String extra_keys = subtype.getExtraValueOf("extra_keys");
String script = subtype.getExtraValueOf("script");
if (extra_keys != null)
return ExtraKeys.parse(script, extra_keys);
return ExtraKeys.EMPTY;
}
private void refreshAccentsOption(InputMethodManager imm, List<InputMethodSubtype> enabled_subtypes)
{
List<ExtraKeys> extra_keys = new ArrayList<ExtraKeys>();
for (InputMethodSubtype s : enabled_subtypes)
extra_keys.add(extra_keys_of_subtype(s));
_config.extra_keys_subtype = ExtraKeys.merge(extra_keys);
}
InputMethodManager get_imm()
{
return (InputMethodManager)getSystemService(INPUT_METHOD_SERVICE);
}
@TargetApi(12)
private InputMethodSubtype defaultSubtypes(InputMethodManager imm, List<InputMethodSubtype> enabled_subtypes)
{
if (VERSION.SDK_INT < 24)
return imm.getCurrentInputMethodSubtype();
// Android might return a random subtype, for example, the first in the
// list alphabetically.
InputMethodSubtype current_subtype = imm.getCurrentInputMethodSubtype();
if (current_subtype == null)
return null;
for (InputMethodSubtype s : enabled_subtypes)
if (s.getLanguageTag().equals(current_subtype.getLanguageTag()))
return s;
return null;
}
private void refreshSubtypeImm()
{
InputMethodManager imm = get_imm();
_config.shouldOfferVoiceTyping = true;
KeyboardData default_layout = null;
_config.extra_keys_subtype = null;
if (VERSION.SDK_INT >= 12)
{
List<InputMethodSubtype> enabled_subtypes = getEnabledSubtypes(imm);
InputMethodSubtype subtype = defaultSubtypes(imm, enabled_subtypes);
if (subtype != null)
{
String s = subtype.getExtraValueOf("default_layout");
if (s != null)
default_layout = LayoutsPreference.layout_of_string(getResources(), s);
refreshAccentsOption(imm, enabled_subtypes);
}
}
if (default_layout == null)
default_layout = loadLayout(R.xml.latn_qwerty_us);
_localeTextLayout = default_layout;
}
private String actionLabel_of_imeAction(int action)
{
int res;
switch (action)
{
case EditorInfo.IME_ACTION_NEXT: res = R.string.key_action_next; break;
case EditorInfo.IME_ACTION_DONE: res = R.string.key_action_done; break;
case EditorInfo.IME_ACTION_GO: res = R.string.key_action_go; break;
case EditorInfo.IME_ACTION_PREVIOUS: res = R.string.key_action_prev; break;
case EditorInfo.IME_ACTION_SEARCH: res = R.string.key_action_search; break;
case EditorInfo.IME_ACTION_SEND: res = R.string.key_action_send; break;
case EditorInfo.IME_ACTION_UNSPECIFIED:
case EditorInfo.IME_ACTION_NONE:
default: return null;
}
return getResources().getString(res);
}
private void refresh_action_label(EditorInfo info)
{
// First try to look at 'info.actionLabel', if it isn't set, look at
// 'imeOptions'.
if (info.actionLabel != null)
{
_config.actionLabel = info.actionLabel.toString();
actionId = info.actionId;
_config.swapEnterActionKey = false;
}
else
{
int action = info.imeOptions & EditorInfo.IME_MASK_ACTION;
_config.actionLabel = actionLabel_of_imeAction(action); // Might be null
actionId = action;
_config.swapEnterActionKey =
(info.imeOptions & EditorInfo.IME_FLAG_NO_ENTER_ACTION) == 0;
}
}
/** Might re-create the keyboard view. [_keyboardView.setKeyboard()] and
[setInputView()] must be called soon after. */
private void refresh_config()
{
int prev_theme = _config.theme;
_config.refresh(getResources(), _foldStateTracker.isUnfolded());
refreshSubtypeImm();
// Refreshing the theme config requires re-creating the views
if (prev_theme != _config.theme)
{
_keyboardView = (Keyboard2View)inflate_view(R.layout.keyboard);
_emojiPane = null;
_clipboard_pane = null;
setInputView(_keyboardView);
}
_keyboardView.reset();
}
private KeyboardData refresh_special_layout(EditorInfo info)
{
switch (info.inputType & InputType.TYPE_MASK_CLASS)
{
case InputType.TYPE_CLASS_NUMBER:
case InputType.TYPE_CLASS_PHONE:
case InputType.TYPE_CLASS_DATETIME:
if (_config.selected_number_layout == NumberLayout.PIN)
return loadPinentry(R.xml.pin);
else if (_config.selected_number_layout == NumberLayout.NUMBER)
return loadNumpad(R.xml.numeric);
default:
break;
}
return null;
}
@Override
public void onStartInputView(EditorInfo info, boolean restarting)
{
refresh_config();
refresh_action_label(info);
_currentSpecialLayout = refresh_special_layout(info);
_keyboardView.setKeyboard(current_layout());
_keyeventhandler.started(info);
setInputView(_keyboardView);
Logs.debug_startup_input_view(info, _config);
}
@Override
public void setInputView(View v)
{
ViewParent parent = v.getParent();
if (parent != null && parent instanceof ViewGroup)
((ViewGroup)parent).removeView(v);
super.setInputView(v);
updateSoftInputWindowLayoutParams();
}
@Override
public void updateFullscreenMode() {
super.updateFullscreenMode();
updateSoftInputWindowLayoutParams();
}
private void updateSoftInputWindowLayoutParams() {
final Window window = getWindow().getWindow();
// On API >= 35, Keyboard2View behaves as edge-to-edge
// APIs 30 to 34 have visual artifact when edge-to-edge is enabled
if (VERSION.SDK_INT >= 35)
{
WindowManager.LayoutParams wattrs = window.getAttributes();
wattrs.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
// Allow to draw behind system bars
wattrs.setFitInsetsTypes(0);
window.setDecorFitsSystemWindows(false);
}
updateLayoutHeightOf(window, ViewGroup.LayoutParams.MATCH_PARENT);
final View inputArea = window.findViewById(android.R.id.inputArea);
updateLayoutHeightOf(
(View) inputArea.getParent(),
isFullscreenMode()
? ViewGroup.LayoutParams.MATCH_PARENT
: ViewGroup.LayoutParams.WRAP_CONTENT);
updateLayoutGravityOf((View) inputArea.getParent(), Gravity.BOTTOM);
}
private static void updateLayoutHeightOf(final Window window, final int layoutHeight) {
final WindowManager.LayoutParams params = window.getAttributes();
if (params != null && params.height != layoutHeight) {
params.height = layoutHeight;
window.setAttributes(params);
}
}
private static void updateLayoutHeightOf(final View view, final int layoutHeight) {
final ViewGroup.LayoutParams params = view.getLayoutParams();
if (params != null && params.height != layoutHeight) {
params.height = layoutHeight;
view.setLayoutParams(params);
}
}
private static void updateLayoutGravityOf(final View view, final int layoutGravity) {
final ViewGroup.LayoutParams lp = view.getLayoutParams();
if (lp instanceof LinearLayout.LayoutParams) {
final LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) lp;
if (params.gravity != layoutGravity) {
params.gravity = layoutGravity;
view.setLayoutParams(params);
}
} else if (lp instanceof FrameLayout.LayoutParams) {
final FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) lp;
if (params.gravity != layoutGravity) {
params.gravity = layoutGravity;
view.setLayoutParams(params);
}
}
}
@Override
public void onCurrentInputMethodSubtypeChanged(InputMethodSubtype subtype)
{
refreshSubtypeImm();
_keyboardView.setKeyboard(current_layout());
}
@Override
public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, int candidatesStart, int candidatesEnd)
{
super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd);
_keyeventhandler.selection_updated(oldSelStart, newSelStart);
if ((oldSelStart == oldSelEnd) != (newSelStart == newSelEnd))
_keyboardView.set_selection_state(newSelStart != newSelEnd);
}
@Override
public void onFinishInputView(boolean finishingInput)
{
super.onFinishInputView(finishingInput);
_keyboardView.reset();
}
@Override
public void onSharedPreferenceChanged(SharedPreferences _prefs, String _key)
{
refresh_config();
_keyboardView.setKeyboard(current_layout());
}
@Override
public boolean onEvaluateFullscreenMode()
{
/* Entirely disable fullscreen mode. */
return false;
}
/** Not static */
public class Receiver implements KeyEventHandler.IReceiver
{
public void handle_event_key(KeyValue.Event ev)
{
switch (ev)
{
case CONFIG:
Intent intent = new Intent(Keyboard2.this, SettingsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
break;
case SWITCH_TEXT:
_currentSpecialLayout = null;
_keyboardView.setKeyboard(current_layout());
break;
case SWITCH_NUMERIC:
setSpecialLayout(loadNumpad(R.xml.numeric));
break;
case SWITCH_EMOJI:
if (_emojiPane == null)
_emojiPane = (ViewGroup)inflate_view(R.layout.emoji_pane);
setInputView(_emojiPane);
break;
case SWITCH_CLIPBOARD:
if (_clipboard_pane == null)
_clipboard_pane = (ViewGroup)inflate_view(R.layout.clipboard_pane);
setInputView(_clipboard_pane);
break;
case SWITCH_BACK_EMOJI:
case SWITCH_BACK_CLIPBOARD:
setInputView(_keyboardView);
break;
case CHANGE_METHOD_PICKER:
get_imm().showInputMethodPicker();
break;
case CHANGE_METHOD_AUTO:
if (VERSION.SDK_INT < 28)
get_imm().switchToLastInputMethod(getConnectionToken());
else
switchToNextInputMethod(false);
break;
case ACTION:
InputConnection conn = getCurrentInputConnection();
if (conn != null)
conn.performEditorAction(actionId);
break;
case SWITCH_FORWARD:
incrTextLayout(1);
break;
case SWITCH_BACKWARD:
incrTextLayout(-1);
break;
case SWITCH_GREEKMATH:
setSpecialLayout(loadNumpad(R.xml.greekmath));
break;
case CAPS_LOCK:
set_shift_state(true, true);
break;
case SWITCH_VOICE_TYPING:
if (!VoiceImeSwitcher.switch_to_voice_ime(Keyboard2.this, get_imm(),
Config.globalPrefs()))
_config.shouldOfferVoiceTyping = false;
break;
case SWITCH_VOICE_TYPING_CHOOSER:
VoiceImeSwitcher.choose_voice_ime(Keyboard2.this, get_imm(),
Config.globalPrefs());
break;
}
}
public void set_shift_state(boolean state, boolean lock)
{
_keyboardView.set_shift_state(state, lock);
}
public void set_compose_pending(boolean pending)
{
_keyboardView.set_compose_pending(pending);
}
public void selection_state_changed(boolean selection_is_ongoing)
{
_keyboardView.set_selection_state(selection_is_ongoing);
}
public InputConnection getCurrentInputConnection()
{
return Keyboard2.this.getCurrentInputConnection();
}
public Handler getHandler()
{
return _handler;
}
}
private IBinder getConnectionToken()
{
return getWindow().getWindow().getAttributes().token;
}
private View inflate_view(int layout)
{
return View.inflate(new ContextThemeWrapper(this, _config.theme), layout, null);
}
}

View file

@ -0,0 +1,485 @@
package juloo.keyboard2;
import android.content.Context;
import android.content.ContextWrapper;
import android.graphics.Canvas;
import android.graphics.Insets;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.inputmethodservice.InputMethodService;
import android.os.Build.VERSION;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
import java.util.Arrays;
public class Keyboard2View extends View
implements View.OnTouchListener, Pointers.IPointerEventHandler
{
private KeyboardData _keyboard;
/** The key holding the shift key is used to set shift state from
autocapitalisation. */
private KeyValue _shift_kv;
private KeyboardData.Key _shift_key;
/** Used to add fake pointers. */
private KeyValue _compose_kv;
private KeyboardData.Key _compose_key;
private Pointers _pointers;
private Pointers.Modifiers _mods;
private static int _currentWhat = 0;
private Config _config;
private float _keyWidth;
private float _mainLabelSize;
private float _subLabelSize;
private float _marginRight;
private float _marginLeft;
private float _marginBottom;
private int _insets_left = 0;
private int _insets_right = 0;
private int _insets_bottom = 0;
private Theme _theme;
private Theme.Computed _tc;
private static RectF _tmpRect = new RectF();
enum Vertical
{
TOP,
CENTER,
BOTTOM
}
public Keyboard2View(Context context, AttributeSet attrs)
{
super(context, attrs);
_theme = new Theme(getContext(), attrs);
_config = Config.globalConfig();
_pointers = new Pointers(this, _config);
refresh_navigation_bar(context);
setOnTouchListener(this);
int layout_id = (attrs == null) ? 0 :
attrs.getAttributeResourceValue(null, "layout", 0);
if (layout_id == 0)
reset();
else
setKeyboard(KeyboardData.load(getResources(), layout_id));
}
private Window getParentWindow(Context context)
{
if (context instanceof InputMethodService)
return ((InputMethodService)context).getWindow().getWindow();
if (context instanceof ContextWrapper)
return getParentWindow(((ContextWrapper)context).getBaseContext());
return null;
}
public void refresh_navigation_bar(Context context)
{
if (VERSION.SDK_INT < 21)
return;
// The intermediate Window is a [Dialog].
Window w = getParentWindow(context);
w.setNavigationBarColor(_theme.colorNavBar);
if (VERSION.SDK_INT < 26)
return;
int uiFlags = getSystemUiVisibility();
if (_theme.isLightNavBar)
uiFlags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
else
uiFlags &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
setSystemUiVisibility(uiFlags);
}
public void setKeyboard(KeyboardData kw)
{
_keyboard = kw;
_shift_kv = KeyValue.getKeyByName("shift");
_shift_key = _keyboard.findKeyWithValue(_shift_kv);
_compose_kv = KeyValue.getKeyByName("compose");
_compose_key = _keyboard.findKeyWithValue(_compose_kv);
KeyModifier.set_modmap(_keyboard.modmap);
reset();
}
public void reset()
{
_mods = Pointers.Modifiers.EMPTY;
_pointers.clear();
requestLayout();
invalidate();
}
void set_fake_ptr_latched(KeyboardData.Key key, KeyValue kv, boolean latched,
boolean lock)
{
if (_keyboard == null || key == null)
return;
_pointers.set_fake_pointer_state(key, kv, latched, lock);
}
/** Called by auto-capitalisation. */
public void set_shift_state(boolean latched, boolean lock)
{
set_fake_ptr_latched(_shift_key, _shift_kv, latched, lock);
}
/** Called from [KeyEventHandler]. */
public void set_compose_pending(boolean pending)
{
set_fake_ptr_latched(_compose_key, _compose_kv, pending, false);
}
/** Called from [Keybard2.onUpdateSelection]. */
public void set_selection_state(boolean selection_state)
{
set_fake_ptr_latched(KeyboardData.Key.EMPTY,
KeyValue.getKeyByName("selection_mode"), selection_state, true);
}
public KeyValue modifyKey(KeyValue k, Pointers.Modifiers mods)
{
return KeyModifier.modify(k, mods);
}
public void onPointerDown(KeyValue k, boolean isSwipe)
{
updateFlags();
_config.handler.key_down(k, isSwipe);
invalidate();
vibrate();
}
public void onPointerUp(KeyValue k, Pointers.Modifiers mods)
{
// [key_up] must be called before [updateFlags]. The latter might disable
// flags.
_config.handler.key_up(k, mods);
updateFlags();
invalidate();
}
public void onPointerHold(KeyValue k, Pointers.Modifiers mods)
{
_config.handler.key_up(k, mods);
updateFlags();
}
public void onPointerFlagsChanged(boolean shouldVibrate)
{
updateFlags();
invalidate();
if (shouldVibrate)
vibrate();
}
private void updateFlags()
{
_mods = _pointers.getModifiers();
_config.handler.mods_changed(_mods);
}
@Override
public boolean onTouch(View v, MotionEvent event)
{
int p;
switch (event.getActionMasked())
{
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
_pointers.onTouchUp(event.getPointerId(event.getActionIndex()));
break;
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
p = event.getActionIndex();
float tx = event.getX(p);
float ty = event.getY(p);
KeyboardData.Key key = getKeyAtPosition(tx, ty);
if (key != null)
_pointers.onTouchDown(tx, ty, event.getPointerId(p), key);
break;
case MotionEvent.ACTION_MOVE:
for (p = 0; p < event.getPointerCount(); p++)
_pointers.onTouchMove(event.getX(p), event.getY(p), event.getPointerId(p));
break;
case MotionEvent.ACTION_CANCEL:
_pointers.onTouchCancel();
break;
default:
return (false);
}
return (true);
}
private KeyboardData.Row getRowAtPosition(float ty)
{
float y = _config.marginTop;
if (ty < y)
return null;
for (KeyboardData.Row row : _keyboard.rows)
{
y += (row.shift + row.height) * _tc.row_height;
if (ty < y)
return row;
}
return null;
}
private KeyboardData.Key getKeyAtPosition(float tx, float ty)
{
KeyboardData.Row row = getRowAtPosition(ty);
float x = _marginLeft;
if (row == null || tx < x)
return null;
for (KeyboardData.Key key : row.keys)
{
float xLeft = x + key.shift * _keyWidth;
float xRight = xLeft + key.width * _keyWidth;
if (tx < xLeft)
return null;
if (tx < xRight)
return key;
x = xRight;
}
return null;
}
private void vibrate()
{
VibratorCompat.vibrate(this, _config);
}
@Override
public void onMeasure(int wSpec, int hSpec)
{
int width;
DisplayMetrics dm = getContext().getResources().getDisplayMetrics();
width = dm.widthPixels;
_marginLeft = Math.max(_config.horizontal_margin, _insets_left);
_marginRight = Math.max(_config.horizontal_margin, _insets_right);
_marginBottom = _config.margin_bottom + _insets_bottom;
_keyWidth = (width - _marginLeft - _marginRight) / _keyboard.keysWidth;
_tc = new Theme.Computed(_theme, _config, _keyWidth, _keyboard);
// Compute the size of labels based on the width or the height of keys. The
// margin around keys is taken into account. Keys normal aspect ratio is
// assumed to be 3/2. It's generally more, the width computation is useful
// when the keyboard is unusually high.
float labelBaseSize = Math.min(
_tc.row_height - _tc.vertical_margin,
_keyWidth * 3/2 - _tc.horizontal_margin
) * _config.characterSize;
_mainLabelSize = labelBaseSize * _config.labelTextSize;
_subLabelSize = labelBaseSize * _config.sublabelTextSize;
int height =
(int)(_tc.row_height * _keyboard.keysHeight
+ _config.marginTop + _marginBottom);
setMeasuredDimension(width, height);
}
@Override
public void onLayout(boolean changed, int left, int top, int right, int bottom)
{
if (!changed)
return;
if (VERSION.SDK_INT >= 29)
{
// Disable the back-gesture on the keyboard area
Rect keyboard_area = new Rect(
left + (int)_marginLeft,
top + (int)_config.marginTop,
right - (int)_marginRight,
bottom - (int)_marginBottom);
setSystemGestureExclusionRects(Arrays.asList(keyboard_area));
}
}
@Override
public WindowInsets onApplyWindowInsets(WindowInsets wi)
{
// LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS is set in [Keyboard2#updateSoftInputWindowLayoutParams] for SDK_INT >= 35.
if (VERSION.SDK_INT < 35)
return wi;
int insets_types =
WindowInsets.Type.systemBars()
| WindowInsets.Type.displayCutout();
Insets insets = wi.getInsets(insets_types);
_insets_left = insets.left;
_insets_right = insets.right;
_insets_bottom = insets.bottom;
return WindowInsets.CONSUMED;
}
/** Horizontal and vertical position of the 9 indexes. */
static final Paint.Align[] LABEL_POSITION_H = new Paint.Align[]{
Paint.Align.CENTER, Paint.Align.LEFT, Paint.Align.RIGHT, Paint.Align.LEFT,
Paint.Align.RIGHT, Paint.Align.LEFT, Paint.Align.RIGHT,
Paint.Align.CENTER, Paint.Align.CENTER
};
static final Vertical[] LABEL_POSITION_V = new Vertical[]{
Vertical.CENTER, Vertical.TOP, Vertical.TOP, Vertical.BOTTOM,
Vertical.BOTTOM, Vertical.CENTER, Vertical.CENTER, Vertical.TOP,
Vertical.BOTTOM
};
@Override
protected void onDraw(Canvas canvas)
{
// Set keyboard background opacity
getBackground().setAlpha(_config.keyboardOpacity);
float y = _tc.margin_top;
for (KeyboardData.Row row : _keyboard.rows)
{
y += row.shift * _tc.row_height;
float x = _marginLeft + _tc.margin_left;
float keyH = row.height * _tc.row_height - _tc.vertical_margin;
for (KeyboardData.Key k : row.keys)
{
x += k.shift * _keyWidth;
float keyW = _keyWidth * k.width - _tc.horizontal_margin;
boolean isKeyDown = _pointers.isKeyDown(k);
Theme.Computed.Key tc_key = isKeyDown ? _tc.key_activated : _tc.key;
drawKeyFrame(canvas, x, y, keyW, keyH, tc_key);
if (k.keys[0] != null)
drawLabel(canvas, k.keys[0], keyW / 2f + x, y, keyH, isKeyDown, tc_key);
for (int i = 1; i < 9; i++)
{
if (k.keys[i] != null)
drawSubLabel(canvas, k.keys[i], x, y, keyW, keyH, i, isKeyDown, tc_key);
}
drawIndication(canvas, k, x, y, keyW, keyH, _tc);
x += _keyWidth * k.width;
}
y += row.height * _tc.row_height;
}
}
@Override
public void onDetachedFromWindow()
{
super.onDetachedFromWindow();
}
/** Draw borders and background of the key. */
void drawKeyFrame(Canvas canvas, float x, float y, float keyW, float keyH,
Theme.Computed.Key tc)
{
float r = tc.border_radius;
float w = tc.border_width;
float padding = w / 2.f;
_tmpRect.set(x + padding, y + padding, x + keyW - padding, y + keyH - padding);
canvas.drawRoundRect(_tmpRect, r, r, tc.bg_paint);
if (w > 0.f)
{
float overlap = r - r * 0.85f + w; // sin(45°)
drawBorder(canvas, x, y, x + overlap, y + keyH, tc.border_left_paint, tc);
drawBorder(canvas, x + keyW - overlap, y, x + keyW, y + keyH, tc.border_right_paint, tc);
drawBorder(canvas, x, y, x + keyW, y + overlap, tc.border_top_paint, tc);
drawBorder(canvas, x, y + keyH - overlap, x + keyW, y + keyH, tc.border_bottom_paint, tc);
}
}
/** Clip to draw a border at a time. This allows to call [drawRoundRect]
several time with the same parameters but a different Paint. */
void drawBorder(Canvas canvas, float clipl, float clipt, float clipr,
float clipb, Paint paint, Theme.Computed.Key tc)
{
float r = tc.border_radius;
canvas.save();
canvas.clipRect(clipl, clipt, clipr, clipb);
canvas.drawRoundRect(_tmpRect, r, r, paint);
canvas.restore();
}
private int labelColor(KeyValue k, boolean isKeyDown, boolean sublabel)
{
if (isKeyDown)
{
int flags = _pointers.getKeyFlags(k);
if (flags != -1)
{
if ((flags & Pointers.FLAG_P_LOCKED) != 0)
return _theme.lockedColor;
return _theme.activatedColor;
}
}
if (k.hasFlagsAny(KeyValue.FLAG_SECONDARY | KeyValue.FLAG_GREYED))
{
if (k.hasFlagsAny(KeyValue.FLAG_GREYED))
return _theme.greyedLabelColor;
return _theme.secondaryLabelColor;
}
return sublabel ? _theme.subLabelColor : _theme.labelColor;
}
private void drawLabel(Canvas canvas, KeyValue kv, float x, float y,
float keyH, boolean isKeyDown, Theme.Computed.Key tc)
{
kv = modifyKey(kv, _mods);
if (kv == null)
return;
float textSize = scaleTextSize(kv, true);
Paint p = tc.label_paint(kv.hasFlagsAny(KeyValue.FLAG_KEY_FONT), labelColor(kv, isKeyDown, false), textSize);
canvas.drawText(kv.getString(), x, (keyH - p.ascent() - p.descent()) / 2f + y, p);
}
private void drawSubLabel(Canvas canvas, KeyValue kv, float x, float y,
float keyW, float keyH, int sub_index, boolean isKeyDown,
Theme.Computed.Key tc)
{
Paint.Align a = LABEL_POSITION_H[sub_index];
Vertical v = LABEL_POSITION_V[sub_index];
kv = modifyKey(kv, _mods);
if (kv == null)
return;
float textSize = scaleTextSize(kv, false);
Paint p = tc.sublabel_paint(kv.hasFlagsAny(KeyValue.FLAG_KEY_FONT), labelColor(kv, isKeyDown, true), textSize, a);
float subPadding = _config.keyPadding;
if (v == Vertical.CENTER)
y += (keyH - p.ascent() - p.descent()) / 2f;
else
y += (v == Vertical.TOP) ? subPadding - p.ascent() : keyH - subPadding - p.descent();
if (a == Paint.Align.CENTER)
x += keyW / 2f;
else
x += (a == Paint.Align.LEFT) ? subPadding : keyW - subPadding;
String label = kv.getString();
int label_len = label.length();
// Limit the label of string keys to 3 characters
if (label_len > 3 && kv.getKind() == KeyValue.Kind.String)
label_len = 3;
canvas.drawText(label, 0, label_len, x, y, p);
}
private void drawIndication(Canvas canvas, KeyboardData.Key k, float x,
float y, float keyW, float keyH, Theme.Computed tc)
{
if (k.indication == null || k.indication.equals(""))
return;
Paint p = tc.indication_paint;
p.setTextSize(_subLabelSize);
canvas.drawText(k.indication, 0, k.indication.length(),
x + keyW / 2f, (keyH - p.ascent() - p.descent()) * 4/5 + y, p);
}
private float scaleTextSize(KeyValue k, boolean main_label)
{
float smaller_font = k.hasFlagsAny(KeyValue.FLAG_SMALLER_FONT) ? 0.75f : 1.f;
float label_size = main_label ? _mainLabelSize : _subLabelSize;
return label_size * smaller_font;
}
}

View file

@ -0,0 +1,703 @@
package juloo.keyboard2;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.util.Xml;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import org.xmlpull.v1.XmlPullParser;
public final class KeyboardData
{
public final List<Row> rows;
/** Total width of the keyboard. */
public final float keysWidth;
/** Total height of the keyboard. */
public final float keysHeight;
/** Might be null. */
public final Modmap modmap;
/** Might be null. */
public final String script;
/** Might be different from [script]. Might be null. */
public final String numpad_script;
/** The [name] attribute. Might be null. */
public final String name;
/** Whether the bottom row should be added. */
public final boolean bottom_row;
/** Whether the number row is included in the layout and thus another one shouldn't be added. */
public final boolean embedded_number_row;
/** Whether extra keys from [method.xml] should be added to this layout. */
public final boolean locale_extra_keys;
/** Position of every keys on the layout, see [getKeys()]. */
private Map<KeyValue, KeyPos> _key_pos = null;
public KeyboardData mapKeys(MapKey f)
{
ArrayList<Row> rows_ = new ArrayList<Row>();
for (Row r : rows)
rows_.add(r.mapKeys(f));
return new KeyboardData(this, rows_);
}
/** Add keys from the given iterator into the keyboard. Preferred position is
specified via [PreferredPos]. */
public KeyboardData addExtraKeys(Iterator<Map.Entry<KeyValue, PreferredPos>> extra_keys)
{
/* Keys that couldn't be placed at their preferred position. */
ArrayList<KeyValue> unplaced_keys = new ArrayList<KeyValue>();
ArrayList<Row> rows = new ArrayList<Row>(this.rows);
while (extra_keys.hasNext())
{
Map.Entry<KeyValue, PreferredPos> kp = extra_keys.next();
if (!add_key_to_preferred_pos(rows, kp.getKey(), kp.getValue()))
unplaced_keys.add(kp.getKey());
}
for (KeyValue kv : unplaced_keys)
add_key_to_preferred_pos(rows, kv, PreferredPos.ANYWHERE);
return new KeyboardData(this, rows);
}
/** Place a key on the keyboard according to its preferred position. Mutates
[rows]. Returns [false] if it couldn't be placed. */
boolean add_key_to_preferred_pos(List<Row> rows, KeyValue kv, PreferredPos pos)
{
if (pos.next_to != null)
{
KeyPos next_to_pos = getKeys().get(pos.next_to);
// Use preferred direction if some preferred pos match
if (next_to_pos != null)
{
for (KeyPos p : pos.positions)
if ((p.row == -1 || p.row == next_to_pos.row)
&& (p.col == -1 || p.col == next_to_pos.col)
&& add_key_to_pos(rows, kv, next_to_pos.with_dir(p.dir)))
return true;
if (add_key_to_pos(rows, kv, next_to_pos.with_dir(-1)))
return true;
}
}
for (KeyPos p : pos.positions)
if (add_key_to_pos(rows, kv, p))
return true;
return false;
}
/** Place a key on the keyboard. A value of [-1] in one of the coordinate
means that the key can be placed anywhere in that coordinate, see
[PreferredPos]. Mutates [rows]. Returns [false] if it couldn't be placed.
*/
boolean add_key_to_pos(List<Row> rows, KeyValue kv, KeyPos p)
{
int i_row = p.row;
int i_row_end = Math.min(p.row, rows.size() - 1);
if (p.row == -1) { i_row = 0; i_row_end = rows.size() - 1; }
for (; i_row <= i_row_end; i_row++)
{
Row row = rows.get(i_row);
int i_col = p.col;
int i_col_end = Math.min(p.col, row.keys.size() - 1);
if (p.col == -1) { i_col = 0; i_col_end = row.keys.size() - 1; }
for (; i_col <= i_col_end; i_col++)
{
Key col = row.keys.get(i_col);
int i_dir = p.dir;
int i_dir_end = p.dir;
if (p.dir == -1) { i_dir = 1; i_dir_end = 4; }
for (; i_dir <= i_dir_end; i_dir++)
{
if (col.getKeyValue(i_dir) == null)
{
row.keys.set(i_col, col.withKeyValue(i_dir, kv));
return true;
}
}
}
}
return false;
}
public KeyboardData addNumPad(KeyboardData num_pad)
{
ArrayList<Row> extendedRows = new ArrayList<Row>();
Iterator<Row> iterNumPadRows = num_pad.rows.iterator();
for (Row row : rows)
{
ArrayList<KeyboardData.Key> keys = new ArrayList<Key>(row.keys);
if (iterNumPadRows.hasNext())
{
Row numPadRow = iterNumPadRows.next();
List<Key> nps = numPadRow.keys;
if (nps.size() > 0) {
float firstNumPadShift = 0.5f + keysWidth - row.keysWidth;
keys.add(nps.get(0).withShift(firstNumPadShift));
for (int i = 1; i < nps.size(); i++)
keys.add(nps.get(i));
}
}
extendedRows.add(new Row(keys, row.height, row.shift));
}
return new KeyboardData(this, extendedRows);
}
/** Insert the given row at the given indice. The row is scaled so that the
keys already on the keyboard don't change width. */
public KeyboardData insert_row(Row row, int i)
{
ArrayList<Row> rows_ = new ArrayList<Row>(this.rows);
rows_.add(i, row.updateWidth(keysWidth));
return new KeyboardData(this, rows_);
}
public Key findKeyWithValue(KeyValue kv)
{
KeyPos pos = getKeys().get(kv);
if (pos == null || pos.row >= rows.size())
return null;
return rows.get(pos.row).get_key_at_pos(pos);
}
/** This is computed once and cached. */
public Map<KeyValue, KeyPos> getKeys()
{
if (_key_pos == null)
{
_key_pos = new HashMap<KeyValue, KeyPos>();
for (int r = 0; r < rows.size(); r++)
rows.get(r).getKeys(_key_pos, r);
}
return _key_pos;
}
private static Map<Integer, KeyboardData> _layoutCache = new HashMap<Integer, KeyboardData>();
public static Row load_row(Resources res, int res_id) throws Exception
{
return parse_row(res.getXml(res_id));
}
public static KeyboardData load_num_pad(Resources res) throws Exception
{
return parse_keyboard(res.getXml(R.xml.numpad));
}
/** Load a layout from a resource ID. Returns [null] on error. */
public static KeyboardData load(Resources res, int id)
{
if (_layoutCache.containsKey(id))
return _layoutCache.get(id);
KeyboardData l = null;
XmlResourceParser parser = null;
try
{
parser = res.getXml(id);
l = parse_keyboard(parser);
}
catch (Exception e)
{
Logs.exn("Failed to load layout id " + id, e);
}
if (parser != null)
parser.close();
_layoutCache.put(id, l);
return l;
}
/** Load a layout from a string. Returns [null] on error. */
public static KeyboardData load_string(String src)
{
try
{
return load_string_exn(src);
}
catch (Exception e)
{
return null;
}
}
/** Like [load_string] but throws an exception on error and do not return
[null]. */
public static KeyboardData load_string_exn(String src) throws Exception
{
XmlPullParser parser = Xml.newPullParser();
parser.setInput(new StringReader(src));
return parse_keyboard(parser);
}
private static KeyboardData parse_keyboard(XmlPullParser parser) throws Exception
{
if (!expect_tag(parser, "keyboard"))
throw error(parser, "Expected tag <keyboard>");
boolean bottom_row = attribute_bool(parser, "bottom_row", true);
boolean embedded_number_row = attribute_bool(parser, "embedded_number_row", false);
boolean locale_extra_keys = attribute_bool(parser, "locale_extra_keys", true);
float specified_kw = attribute_float(parser, "width", 0f);
String script = parser.getAttributeValue(null, "script");
if (script != null && script.equals(""))
throw error(parser, "'script' attribute cannot be empty");
String numpad_script = parser.getAttributeValue(null, "numpad_script");
if (numpad_script == null)
numpad_script = script;
else if (numpad_script.equals(""))
throw error(parser, "'numpad_script' attribute cannot be empty");
String name = parser.getAttributeValue(null, "name");
ArrayList<Row> rows = new ArrayList<Row>();
Modmap modmap = null;
while (next_tag(parser))
{
switch (parser.getName())
{
case "row":
rows.add(Row.parse(parser));
break;
case "modmap":
if (modmap != null)
throw error(parser, "Multiple '<modmap>' are not allowed");
modmap = parse_modmap(parser);
break;
default:
throw error(parser, "Expecting tag <row>, got <" + parser.getName() + ">");
}
}
float kw = (specified_kw != 0f) ? specified_kw : compute_max_width(rows);
return new KeyboardData(rows, kw, modmap, script, numpad_script, name, bottom_row, embedded_number_row, locale_extra_keys);
}
private static float compute_max_width(List<Row> rows)
{
float w = 0.f;
for (Row r : rows)
w = Math.max(w, r.keysWidth);
return w;
}
private static Row parse_row(XmlPullParser parser) throws Exception
{
if (!expect_tag(parser, "row"))
throw error(parser, "Expected tag <row>");
return Row.parse(parser);
}
protected KeyboardData(List<Row> rows_, float kw, Modmap mm, String sc,
String npsc, String name_, boolean bottom_row_, boolean embedded_number_row_, boolean locale_extra_keys_)
{
float kh = 0.f;
for (Row r : rows_)
kh += r.height + r.shift;
rows = rows_;
modmap = mm;
script = sc;
numpad_script = npsc;
name = name_;
keysWidth = Math.max(kw, 1f);
keysHeight = kh;
bottom_row = bottom_row_;
embedded_number_row = embedded_number_row_;
locale_extra_keys = locale_extra_keys_;
}
/** Copies the fields of a keyboard, with rows changed. */
protected KeyboardData(KeyboardData src, List<Row> rows)
{
this(rows, compute_max_width(rows), src.modmap, src.script,
src.numpad_script, src.name, src.bottom_row, src.embedded_number_row, src.locale_extra_keys);
}
public static class Row
{
public final List<Key> keys;
/** Height of the row, without 'shift'. */
public final float height;
/** Extra empty space on the top. */
public final float shift;
/** Total width of the row. */
public final float keysWidth;
protected Row(List<Key> keys_, float h, float s)
{
float kw = 0.f;
for (Key k : keys_) kw += k.width + k.shift;
keys = keys_;
height = Math.max(h, 0.5f);
shift = Math.max(s, 0f);
keysWidth = kw;
}
public static Row parse(XmlPullParser parser) throws Exception
{
ArrayList<Key> keys = new ArrayList<Key>();
int status;
float h = attribute_float(parser, "height", 1f);
float shift = attribute_float(parser, "shift", 0f);
float scale = attribute_float(parser, "scale", 0f);
while (expect_tag(parser, "key"))
keys.add(Key.parse(parser));
Row row = new Row(keys, h, shift);
if (scale > 0f)
row = row.updateWidth(scale);
return row;
}
public Row copy()
{
return new Row(new ArrayList<Key>(keys), height, shift);
}
public void getKeys(Map<KeyValue, KeyPos> dst, int row)
{
for (int c = 0; c < keys.size(); c++)
keys.get(c).getKeys(dst, row, c);
}
public Map<KeyValue, KeyPos> getKeys(int row)
{
Map<KeyValue, KeyPos> dst = new HashMap<KeyValue, KeyPos>();
getKeys(dst, row);
return dst;
}
public Row mapKeys(MapKey f)
{
ArrayList<Key> keys_ = new ArrayList<Key>();
for (Key k : keys)
keys_.add(f.apply(k));
return new Row(keys_, height, shift);
}
/** Change the width of every keys so that the row is 's' units wide. */
public Row updateWidth(float newWidth)
{
final float s = newWidth / keysWidth;
return mapKeys(new MapKey(){
public Key apply(Key k) { return k.scaleWidth(s); }
});
}
public Key get_key_at_pos(KeyPos pos)
{
if (pos.col >= keys.size())
return null;
return keys.get(pos.col);
}
}
public static class Key
{
/**
* 1 7 2
* 5 0 6
* 3 8 4
*/
public final KeyValue[] keys;
/** Key accessed by the anti-clockwise circle gesture. */
public final KeyValue anticircle;
/** Pack flags for every key values. Flags are: [F_LOC]. */
private final int keysflags;
/** Key width in relative unit. */
public final float width;
/** Extra empty space on the left of the key. */
public final float shift;
/** String printed on the keys. It has no other effect. */
public final String indication;
/** Whether a key was declared with the 'loc' prefix. */
public static final int F_LOC = 1;
public static final int ALL_FLAGS = F_LOC;
protected Key(KeyValue[] ks, KeyValue antic, int f, float w, float s, String i)
{
keys = ks;
anticircle = antic;
keysflags = f;
width = Math.max(w, 0f);
shift = Math.max(s, 0f);
indication = i;
}
static final Key EMPTY = new Key(new KeyValue[9], null, 0, 1.f, 1.f, null);
/** Read a key value attribute that have a synonym. Having both synonyms
present at the same time is an error.
Returns [null] if the attributes are not present. */
static String get_key_attr(XmlPullParser parser, String syn1, String syn2)
throws Exception
{
String name1 = parser.getAttributeValue(null, syn1);
String name2 = parser.getAttributeValue(null, syn2);
if (name1 != null && name2 != null)
throw error(parser,
"'"+syn1+"' and '"+syn2+"' are synonyms and cannot be passed at the same time.");
return (name1 == null) ? name2 : name1;
}
/** Parse the key description [key_attr] and write into [ks] at [index].
Returns flags that can be aggregated into the value for [keysflags].
[key_attr] can be [null] for convenience. */
static int parse_key_attr(XmlPullParser parser, String key_val, KeyValue[] ks,
int index)
throws Exception
{
if (key_val == null)
return 0;
int flags = 0;
String name_loc = stripPrefix(key_val, "loc ");
if (name_loc != null)
{
flags |= F_LOC;
key_val = name_loc;
}
ks[index] = KeyValue.getKeyByName(key_val);
return (flags << index);
}
static KeyValue parse_nonloc_key_attr(XmlPullParser parser, String attr_name) throws Exception
{
String name = parser.getAttributeValue(null, attr_name);
if (name == null)
return null;
return KeyValue.getKeyByName(name);
}
static String stripPrefix(String s, String prefix)
{
return s.startsWith(prefix) ? s.substring(prefix.length()) : null;
}
public static Key parse(XmlPullParser parser) throws Exception
{
KeyValue[] ks = new KeyValue[9];
int keysflags = 0;
keysflags |= parse_key_attr(parser, get_key_attr(parser, "key0", "c"), ks, 0);
/* Swipe gestures (key1-key8 diagram above), with compass-point synonyms. */
keysflags |= parse_key_attr(parser, get_key_attr(parser, "key1", "nw"), ks, 1);
keysflags |= parse_key_attr(parser, get_key_attr(parser, "key2", "ne"), ks, 2);
keysflags |= parse_key_attr(parser, get_key_attr(parser, "key3", "sw"), ks, 3);
keysflags |= parse_key_attr(parser, get_key_attr(parser, "key4", "se"), ks, 4);
keysflags |= parse_key_attr(parser, get_key_attr(parser, "key5", "w"), ks, 5);
keysflags |= parse_key_attr(parser, get_key_attr(parser, "key6", "e"), ks, 6);
keysflags |= parse_key_attr(parser, get_key_attr(parser, "key7", "n"), ks, 7);
keysflags |= parse_key_attr(parser, get_key_attr(parser, "key8", "s"), ks, 8);
/* Other key attributes */
KeyValue anticircle = parse_nonloc_key_attr(parser, "anticircle");
float width = attribute_float(parser, "width", 1f);
float shift = attribute_float(parser, "shift", 0.f);
String indication = parser.getAttributeValue(null, "indication");
while (parser.next() != XmlPullParser.END_TAG)
continue;
return new Key(ks, anticircle, keysflags, width, shift, indication);
}
/** Whether key at [index] as [flag]. */
public boolean keyHasFlag(int index, int flag)
{
return (keysflags & (flag << index)) != 0;
}
/** New key with the width multiplied by 's'. */
public Key scaleWidth(float s)
{
return new Key(keys, anticircle, keysflags, width * s, shift, indication);
}
public void getKeys(Map<KeyValue, KeyPos> dst, int row, int col)
{
for (int i = 0; i < keys.length; i++)
if (keys[i] != null)
dst.put(keys[i], new KeyPos(row, col, i));
}
public KeyValue getKeyValue(int i)
{
return keys[i];
}
public Key withKeyValue(int i, KeyValue kv)
{
KeyValue[] ks = new KeyValue[keys.length];
for (int j = 0; j < keys.length; j++) ks[j] = keys[j];
ks[i] = kv;
int flags = (keysflags & ~(ALL_FLAGS << i));
return new Key(ks, anticircle, flags, width, shift, indication);
}
public Key withShift(float s)
{
return new Key(keys, anticircle, keysflags, width, s, indication);
}
public boolean hasValue(KeyValue kv)
{
for (int i = 0; i < keys.length; i++)
if (keys[i] != null && keys[i].equals(kv))
return true;
return false;
}
}
// Not using Function<KeyValue, KeyValue> to keep compatibility with Android 6.
public static abstract interface MapKey {
public Key apply(Key k);
}
public static abstract class MapKeyValues implements MapKey {
abstract public KeyValue apply(KeyValue c, boolean localized);
public Key apply(Key k)
{
KeyValue[] ks = new KeyValue[k.keys.length];
for (int i = 0; i < ks.length; i++)
if (k.keys[i] != null)
ks[i] = apply(k.keys[i], k.keyHasFlag(i, Key.F_LOC));
return new Key(ks, k.anticircle, k.keysflags, k.width, k.shift, k.indication);
}
}
public static Modmap parse_modmap(XmlPullParser parser) throws Exception
{
Modmap mm = new Modmap();
while (next_tag(parser))
{
Modmap.M m;
switch (parser.getName())
{
case "shift": m = Modmap.M.Shift; break;
case "fn": m = Modmap.M.Fn; break;
case "ctrl": m = Modmap.M.Ctrl; break;
default:
throw error(parser, "Expecting tag <shift> or <fn>, got <" +
parser.getName() + ">");
}
parse_modmap_mapping(parser, mm, m);
}
return mm;
}
private static void parse_modmap_mapping(XmlPullParser parser, Modmap mm,
Modmap.M m) throws Exception
{
KeyValue a = KeyValue.getKeyByName(parser.getAttributeValue(null, "a"));
KeyValue b = KeyValue.getKeyByName(parser.getAttributeValue(null, "b"));
while (parser.next() != XmlPullParser.END_TAG)
continue;
mm.add(m, a, b);
}
/** Position of a key on the layout. */
public final static class KeyPos
{
public final int row;
public final int col;
public final int dir;
public KeyPos(int r, int c, int d)
{
row = r;
col = c;
dir = d;
}
public KeyPos with_dir(int d)
{
return new KeyPos(row, col, d);
}
}
/** See [addExtraKeys()]. */
public final static class PreferredPos
{
/** Default position for extra keys. */
public static final PreferredPos DEFAULT;
public static final PreferredPos ANYWHERE;
/** Prefer the free position on the same keyboard key as the specified key.
Considered before [positions]. Might be [null]. */
public KeyValue next_to = null;
/** Array of positions to try in order. The special value [-1] as [row],
[col] or [dir] means that the field is unspecified. Every possible
values are tried for unspecified fields. Unspecified fields are
searched in this order: [dir], [col], [row]. */
public KeyPos[] positions = ANYWHERE_POSITIONS;
public PreferredPos() {}
public PreferredPos(KeyValue next_to_) { next_to = next_to_; }
public PreferredPos(KeyPos[] pos) { positions = pos; }
public PreferredPos(KeyValue next_to_, KeyPos[] pos) { next_to = next_to_; positions = pos; }
public PreferredPos(PreferredPos src)
{
next_to = src.next_to;
positions = src.positions;
}
static final KeyPos[] ANYWHERE_POSITIONS =
new KeyPos[]{ new KeyPos(-1, -1, -1) };
static
{
DEFAULT = new PreferredPos(new KeyPos[]{
new KeyPos(1, -1, 4),
new KeyPos(1, -1, 3),
new KeyPos(2, -1, 2),
new KeyPos(2, -1, 1)
});
ANYWHERE = new PreferredPos();
}
}
/** Parsing utils */
/** Returns [false] on [END_DOCUMENT] or [END_TAG], [true] otherwise. */
private static boolean next_tag(XmlPullParser parser) throws Exception
{
int status;
do
{
status = parser.next();
if (status == XmlPullParser.END_DOCUMENT || status == XmlPullParser.END_TAG)
return false;
}
while (status != XmlPullParser.START_TAG);
return true;
}
/** Returns [false] on [END_DOCUMENT] or [END_TAG], [true] otherwise. */
private static boolean expect_tag(XmlPullParser parser, String name) throws Exception
{
if (!next_tag(parser))
return false;
if (!parser.getName().equals(name))
throw error(parser, "Expecting tag <" + name + ">, got <" +
parser.getName() + ">");
return true;
}
private static boolean attribute_bool(XmlPullParser parser, String attr, boolean default_val)
{
String val = parser.getAttributeValue(null, attr);
if (val == null)
return default_val;
return val.equals("true");
}
private static float attribute_float(XmlPullParser parser, String attr, float default_val)
{
String val = parser.getAttributeValue(null, attr);
if (val == null)
return default_val;
return Float.parseFloat(val);
}
/** Construct a parsing error. */
private static Exception error(XmlPullParser parser, String message)
{
return new Exception(message + " " + parser.getPositionDescription());
}
}

View file

@ -0,0 +1,129 @@
package juloo.keyboard2;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Intent;
import android.graphics.drawable.Animatable;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Build.VERSION;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.provider.Settings;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
public class LauncherActivity extends Activity implements Handler.Callback
{
/** Text is replaced when receiving key events. */
TextView _tryhere_text;
EditText _tryhere_area;
/** Periodically restart the animations. */
List<Animatable> _animations;
Handler _handler;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.launcher_activity);
_tryhere_text = (TextView)findViewById(R.id.launcher_tryhere_text);
_tryhere_area = (EditText)findViewById(R.id.launcher_tryhere_area);
if (VERSION.SDK_INT >= 28)
_tryhere_area.addOnUnhandledKeyEventListener(
this.new Tryhere_OnUnhandledKeyEventListener());
_handler = new Handler(getMainLooper(), this);
}
@Override
public void onStart()
{
super.onStart();
_animations = new ArrayList<Animatable>();
_animations.add(find_anim(R.id.launcher_anim_swipe));
_animations.add(find_anim(R.id.launcher_anim_round_trip));
_animations.add(find_anim(R.id.launcher_anim_circle));
_handler.removeMessages(0);
_handler.sendEmptyMessageDelayed(0, 500);
}
@Override
public boolean handleMessage(Message _msg)
{
for (Animatable anim : _animations)
anim.start();
_handler.sendEmptyMessageDelayed(0, 3000);
return true;
}
@Override
public final boolean onCreateOptionsMenu(Menu menu)
{
getMenuInflater().inflate(R.menu.launcher_menu, menu);
return true;
}
@Override
public final boolean onOptionsItemSelected(MenuItem item)
{
if (item.getItemId() == R.id.btnLaunchSettingsActivity)
{
Intent intent = new Intent(LauncherActivity.this, SettingsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
return super.onOptionsItemSelected(item);
}
public void launch_imesettings(View _btn)
{
startActivity(new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS));
}
public void launch_imepicker(View v)
{
InputMethodManager imm =
(InputMethodManager)getSystemService(INPUT_METHOD_SERVICE);
imm.showInputMethodPicker();
}
Animatable find_anim(int id)
{
ImageView img = (ImageView)findViewById(id);
return (Animatable)img.getDrawable();
}
@TargetApi(28)
final class Tryhere_OnUnhandledKeyEventListener implements View.OnUnhandledKeyEventListener
{
public boolean onUnhandledKeyEvent(View v, KeyEvent ev)
{
// Don't handle the back key
if (ev.getKeyCode() == KeyEvent.KEYCODE_BACK)
return false;
// Key release of modifiers would erase interesting data
if (KeyEvent.isModifierKey(ev.getKeyCode()))
return false;
StringBuilder s = new StringBuilder();
if (ev.isAltPressed()) s.append("Alt+");
if (ev.isShiftPressed()) s.append("Shift+");
if (ev.isCtrlPressed()) s.append("Ctrl+");
if (ev.isMetaPressed()) s.append("Meta+");
// s.append(ev.getDisplayLabel());
String kc = KeyEvent.keyCodeToString(ev.getKeyCode());
s.append(kc.replaceFirst("^KEYCODE_", ""));
_tryhere_text.setText(s.toString());
return false;
}
}
}

View file

@ -0,0 +1,218 @@
package juloo.keyboard2;
import android.content.res.Resources;
import android.view.KeyEvent;
import java.util.TreeMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public final class LayoutModifier
{
static Config globalConfig;
static KeyboardData.Row bottom_row;
static KeyboardData.Row number_row_no_symbols;
static KeyboardData.Row number_row_symbols;
static KeyboardData num_pad;
/** Update the layout according to the configuration.
* - Remove the switching key if it isn't needed
* - Remove "localized" keys from other locales (not in 'extra_keys')
* - Replace the action key to show the right label
* - Swap the enter and action keys
* - Add the optional numpad and number row
* - Add the extra keys
*/
public static KeyboardData modify_layout(KeyboardData kw)
{
// Extra keys are removed from the set as they are encountered during the
// first iteration then automatically added.
final TreeMap<KeyValue, KeyboardData.PreferredPos> extra_keys = new TreeMap<KeyValue, KeyboardData.PreferredPos>();
final Set<KeyValue> remove_keys = new HashSet<KeyValue>();
// Make sure the config key is accessible to avoid being locked in a custom
// layout.
extra_keys.put(KeyValue.getKeyByName("config"), KeyboardData.PreferredPos.ANYWHERE);
extra_keys.putAll(globalConfig.extra_keys_param);
extra_keys.putAll(globalConfig.extra_keys_custom);
// Number row and numpads are added after the modification pass to allow
// removing the number keys from the main layout.
KeyboardData.Row added_number_row = null;
KeyboardData added_numpad = null;
if (globalConfig.show_numpad)
{
added_numpad = modify_numpad(num_pad, kw);
remove_keys.addAll(added_numpad.getKeys().keySet());
}
else if (globalConfig.add_number_row && !kw.embedded_number_row) // The numpad removes the number row
{
added_number_row = modify_number_row(globalConfig.number_row_symbols ? number_row_symbols : number_row_no_symbols, kw);
remove_keys.addAll(added_number_row.getKeys(0).keySet());
}
// Add the bottom row before computing the extra keys
if (kw.bottom_row)
kw = kw.insert_row(bottom_row, kw.rows.size());
// Compose keys to add to the layout
// 'extra_keys_keyset' reflects changes made to 'extra_keys'
Set<KeyValue> extra_keys_keyset = extra_keys.keySet();
// 'kw_keys' contains the keys present on the layout without any extra keys
Set<KeyValue> kw_keys = kw.getKeys().keySet();
if (globalConfig.extra_keys_subtype != null && kw.locale_extra_keys)
{
Set<KeyValue> present = new HashSet<KeyValue>(kw_keys);
present.addAll(extra_keys_keyset);
globalConfig.extra_keys_subtype.compute(extra_keys,
new ExtraKeys.Query(kw.script, present));
}
kw = kw.mapKeys(new KeyboardData.MapKeyValues() {
public KeyValue apply(KeyValue key, boolean localized)
{
if (localized && !extra_keys.containsKey(key))
return null;
if (remove_keys.contains(key))
return null;
return modify_key(key);
}
});
if (added_numpad != null)
kw = kw.addNumPad(added_numpad);
// Add extra keys that are not on the layout (including 'loc' keys)
extra_keys_keyset.removeAll(kw_keys);
if (extra_keys.size() > 0)
kw = kw.addExtraKeys(extra_keys.entrySet().iterator());
// Avoid adding extra keys to the number row
if (added_number_row != null)
kw = kw.insert_row(added_number_row, 0);
return kw;
}
/** Handle the numpad layout. The [main_kw] is used to adapt the numpad to
the main layout's script. */
public static KeyboardData modify_numpad(KeyboardData kw, KeyboardData main_kw)
{
final int map_digit = KeyModifier.modify_numpad_script(main_kw.numpad_script);
return kw.mapKeys(new KeyboardData.MapKeyValues() {
public KeyValue apply(KeyValue key, boolean localized)
{
switch (key.getKind())
{
case Char:
char prev_c = key.getChar();
char c = prev_c;
if (globalConfig.inverse_numpad)
c = inverse_numpad_char(c);
if (map_digit != -1)
{
KeyValue modified = ComposeKey.apply(map_digit, c);
if (modified != null) // Was modified by script
return modified;
}
if (prev_c != c) // Was inverted
return key.withChar(c);
return key; // Don't fallback into [modify_key]
}
return modify_key(key);
}
});
}
/** Modify the pin entry layout. [main_kw] is used to map the digits into the
same script. */
public static KeyboardData modify_pinentry(KeyboardData kw, KeyboardData main_kw)
{
KeyboardData.MapKeyValues m = numpad_script_map(main_kw.numpad_script);
return m == null ? kw : kw.mapKeys(m);
}
/** Modify the number row according to [main_kw]'s script. */
static KeyboardData.Row modify_number_row(KeyboardData.Row row,
KeyboardData main_kw)
{
KeyboardData.MapKeyValues m = numpad_script_map(main_kw.numpad_script);
return m == null ? row : row.mapKeys(m);
}
static KeyboardData.MapKeyValues numpad_script_map(String numpad_script)
{
final int map_digit = KeyModifier.modify_numpad_script(numpad_script);
if (map_digit == -1)
return null;
return new KeyboardData.MapKeyValues() {
public KeyValue apply(KeyValue key, boolean localized)
{
KeyValue modified = ComposeKey.apply(map_digit, key);
return (modified != null) ? modified : key;
}
};
}
/** Modify keys on the main layout and on the numpad according to the config.
*/
static KeyValue modify_key(KeyValue orig)
{
switch (orig.getKind())
{
case Event:
switch (orig.getEvent())
{
case CHANGE_METHOD_PICKER:
if (globalConfig.switch_input_immediate)
return KeyValue.getKeyByName("change_method_prev");
break;
case ACTION:
if (globalConfig.actionLabel == null)
return null; // Remove the action key
if (globalConfig.swapEnterActionKey)
return KeyValue.getKeyByName("enter");
return KeyValue.makeActionKey(globalConfig.actionLabel);
case SWITCH_FORWARD:
return (globalConfig.layouts.size() > 1) ? orig : null;
case SWITCH_BACKWARD:
return (globalConfig.layouts.size() > 2) ? orig : null;
case SWITCH_VOICE_TYPING:
case SWITCH_VOICE_TYPING_CHOOSER:
return globalConfig.shouldOfferVoiceTyping ? orig : null;
}
break;
case Keyevent:
switch (orig.getKeyevent())
{
case KeyEvent.KEYCODE_ENTER:
if (globalConfig.swapEnterActionKey && globalConfig.actionLabel != null)
return KeyValue.makeActionKey(globalConfig.actionLabel);
break;
}
break;
}
return orig;
}
static char inverse_numpad_char(char c)
{
switch (c)
{
case '7': return '1';
case '8': return '2';
case '9': return '3';
case '1': return '7';
case '2': return '8';
case '3': return '9';
default: return c;
}
}
public static void init(Config globalConfig_, Resources res)
{
globalConfig = globalConfig_;
try
{
number_row_no_symbols = KeyboardData.load_row(res, R.xml.number_row_no_symbols);
number_row_symbols = KeyboardData.load_row(res, R.xml.number_row);
bottom_row = KeyboardData.load_row(res, R.xml.bottom_row);
num_pad = KeyboardData.load_num_pad(res);
}
catch (Exception e)
{
throw new RuntimeException(e.getMessage()); // Not recoverable
}
}
}

View file

@ -0,0 +1,51 @@
package juloo.keyboard2;
import android.util.Log;
import android.util.LogPrinter;
import android.view.inputmethod.EditorInfo;
import org.json.JSONException;
public final class Logs
{
static final String TAG = "juloo.keyboard2";
static LogPrinter _debug_logs = null;
public static void set_debug_logs(boolean d)
{
_debug_logs = d ? new LogPrinter(Log.DEBUG, TAG) : null;
}
public static void debug_startup_input_view(EditorInfo info, Config conf)
{
if (_debug_logs == null)
return;
info.dump(_debug_logs, "");
if (info.extras != null)
_debug_logs.println("extras: "+info.extras.toString());
_debug_logs.println("swapEnterActionKey: "+conf.swapEnterActionKey);
_debug_logs.println("actionLabel: "+conf.actionLabel);
}
public static void debug_config_migration(int from_version, int to_version)
{
debug("Migrating config version from " + from_version + " to " + to_version);
}
public static void debug(String s)
{
if (_debug_logs != null)
_debug_logs.println(s);
}
public static void exn(String msg, Exception e)
{
Log.e(TAG, msg, e);
}
public static void trace()
{
if (_debug_logs != null)
_debug_logs.println(Log.getStackTraceString(new Exception()));
}
}

View file

@ -0,0 +1,33 @@
package juloo.keyboard2;
import java.lang.reflect.Array;
import java.util.Map;
import java.util.TreeMap;
/** Stores key combinations that are applied by [KeyModifier]. */
public final class Modmap
{
public enum M { Shift, Fn, Ctrl }
Map<KeyValue, KeyValue>[] _map;
public Modmap()
{
_map = (Map<KeyValue, KeyValue>[])Array.newInstance(TreeMap.class,
M.values().length);
}
public void add(M m, KeyValue a, KeyValue b)
{
int i = m.ordinal();
if (_map[i] == null)
_map[i] = new TreeMap<KeyValue, KeyValue>();
_map[i].put(a, b);
}
public KeyValue get(M m, KeyValue a)
{
Map<KeyValue, KeyValue> mm = _map[m.ordinal()];
return (mm == null) ? null : mm.get(a);
}
}

View file

@ -0,0 +1,38 @@
package juloo.keyboard2;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import android.widget.ListView;
/** A non-scrollable list view that can be embedded in a bigger ScrollView.
Credits to Dedaniya HirenKumar in
https://stackoverflow.com/questions/18813296/non-scrollable-listview-inside-scrollview */
public class NonScrollListView extends ListView
{
public NonScrollListView(Context context)
{
super(context);
}
public NonScrollListView(Context context, AttributeSet attrs)
{
super(context, attrs);
}
public NonScrollListView(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
}
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
int heightMeasureSpec_custom = MeasureSpec.makeMeasureSpec(
Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightMeasureSpec_custom);
ViewGroup.LayoutParams params = getLayoutParams();
params.height = getMeasuredHeight();
}
}

View file

@ -0,0 +1,17 @@
package juloo.keyboard2;
public enum NumberLayout {
PIN,
NUMBER,
NORMAL;
public static NumberLayout of_string(String name)
{
switch (name)
{
case "number": return NUMBER;
case "normal": return NORMAL;
case "pin": default: return PIN;
}
}
}

View file

@ -0,0 +1,816 @@
package juloo.keyboard2;
import android.os.Handler;
import android.os.Message;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* Manage pointers (fingers) on the screen and long presses.
* Call back to IPointerEventHandler.
*/
public final class Pointers implements Handler.Callback
{
public static final int FLAG_P_LATCHABLE = 1;
public static final int FLAG_P_LATCHED = (1 << 1);
public static final int FLAG_P_FAKE = (1 << 2);
public static final int FLAG_P_DOUBLE_TAP_LOCK = (1 << 3);
public static final int FLAG_P_LOCKED = (1 << 4);
public static final int FLAG_P_SLIDING = (1 << 5);
/** Clear latched (only if also FLAG_P_LATCHABLE set). */
public static final int FLAG_P_CLEAR_LATCHED = (1 << 6);
/** Can't be locked, even when long pressing. */
public static final int FLAG_P_CANT_LOCK = (1 << 7);
private Handler _longpress_handler;
private ArrayList<Pointer> _ptrs = new ArrayList<Pointer>();
private IPointerEventHandler _handler;
private Config _config;
public Pointers(IPointerEventHandler h, Config c)
{
_longpress_handler = new Handler(this);
_handler = h;
_config = c;
}
/** Return the list of modifiers currently activated. */
public Modifiers getModifiers()
{
return getModifiers(false);
}
/** When [skip_latched] is true, don't take flags of latched keys into account. */
private Modifiers getModifiers(boolean skip_latched)
{
int n_ptrs = _ptrs.size();
KeyValue[] mods = new KeyValue[n_ptrs];
int n_mods = 0;
for (int i = 0; i < n_ptrs; i++)
{
Pointer p = _ptrs.get(i);
if (p.value != null
&& !(skip_latched && p.hasFlagsAny(FLAG_P_LATCHED)
&& (p.flags & FLAG_P_LOCKED) == 0))
mods[n_mods++] = p.value;
}
return Modifiers.ofArray(mods, n_mods);
}
public void clear()
{
for (Pointer p : _ptrs)
stopLongPress(p);
_ptrs.clear();
}
public boolean isKeyDown(KeyboardData.Key k)
{
for (Pointer p : _ptrs)
if (p.key == k)
return true;
return false;
}
/** See [FLAG_P_*] flags. Returns [-1] if the key is not pressed. */
public int getKeyFlags(KeyValue kv)
{
for (Pointer p : _ptrs)
if (p.value != null && p.value.equals(kv))
return p.flags;
return -1;
}
/** The key must not be already latched . */
void add_fake_pointer(KeyboardData.Key key, KeyValue kv, boolean locked)
{
int flags = pointer_flags_of_kv(kv) | FLAG_P_FAKE | FLAG_P_LATCHED;
if (locked)
flags |= FLAG_P_LOCKED;
Pointer ptr = new Pointer(-1, key, kv, 0.f, 0.f, Modifiers.EMPTY, flags);
_ptrs.add(ptr);
_handler.onPointerFlagsChanged(false);
}
/** Set whether a key is latched or locked by adding a "fake" pointer, a
pointer that is not due to user interaction.
This is used by auto-capitalisation.
When [lock] is true, [latched] control whether the modifier is locked or disabled.
When [lock] is false, an existing locked pointer is not affected. */
public void set_fake_pointer_state(KeyboardData.Key key, KeyValue kv,
boolean latched, boolean lock)
{
Pointer ptr = getLatched(key, kv);
if (ptr == null)
{
// No existing pointer, latch the key.
if (latched)
{
add_fake_pointer(key, kv, lock);
_handler.onPointerFlagsChanged(false);
}
}
else if ((ptr.flags & FLAG_P_FAKE) == 0)
{} // Key already latched but not by a fake ptr, do nothing.
else if (lock)
{
// Acting on locked modifiers, replace the pointer each time.
removePtr(ptr);
if (latched)
add_fake_pointer(key, kv, lock);
_handler.onPointerFlagsChanged(false);
}
else if ((ptr.flags & FLAG_P_LOCKED) != 0)
{} // Existing ptr is locked but [lock] is false, do not continue.
else if (!latched)
{
// Key is latched by a fake ptr. Unlatch if requested.
removePtr(ptr);
_handler.onPointerFlagsChanged(false);
}
}
// Receiving events
public void onTouchUp(int pointerId)
{
Pointer ptr = getPtr(pointerId);
if (ptr == null)
return;
if (ptr.hasFlagsAny(FLAG_P_SLIDING))
{
clearLatched();
ptr.sliding.onTouchUp(ptr);
return;
}
stopLongPress(ptr);
KeyValue ptr_value = ptr.value;
if (ptr.gesture != null && ptr.gesture.is_in_progress())
{
// A gesture was in progress
ptr.gesture.pointer_up();
}
Pointer latched = getLatched(ptr);
if (latched != null) // Already latched
{
removePtr(ptr); // Remove dupplicate
// Toggle lockable key, except if it's a fake pointer
if ((latched.flags & (FLAG_P_FAKE | FLAG_P_DOUBLE_TAP_LOCK)) == FLAG_P_DOUBLE_TAP_LOCK)
lockPointer(latched, false);
else // Otherwise, unlatch
{
removePtr(latched);
_handler.onPointerUp(ptr_value, ptr.modifiers);
}
}
else if ((ptr.flags & FLAG_P_LATCHABLE) != 0)
{
// Latchable but non-special keys must clear latched.
if ((ptr.flags & FLAG_P_CLEAR_LATCHED) != 0)
clearLatched();
ptr.flags |= FLAG_P_LATCHED;
ptr.pointerId = -1;
_handler.onPointerFlagsChanged(false);
}
else
{
clearLatched();
removePtr(ptr);
_handler.onPointerUp(ptr_value, ptr.modifiers);
}
}
public void onTouchCancel()
{
clear();
_handler.onPointerFlagsChanged(true);
}
/* Whether an other pointer is down on a non-special key. */
private boolean isOtherPointerDown()
{
for (Pointer p : _ptrs)
if (!p.hasFlagsAny(FLAG_P_LATCHED) &&
(p.value == null || !p.value.hasFlagsAny(KeyValue.FLAG_SPECIAL)))
return true;
return false;
}
public void onTouchDown(float x, float y, int pointerId, KeyboardData.Key key)
{
// Ignore new presses while a sliding key is active. On some devices, ghost
// touch events can happen while the pointer travels on top of other keys.
if (isSliding())
return;
// Don't take latched modifiers into account if an other key is pressed.
// The other key already "own" the latched modifiers and will clear them.
Modifiers mods = getModifiers(isOtherPointerDown());
KeyValue value = _handler.modifyKey(key.keys[0], mods);
Pointer ptr = make_pointer(pointerId, key, value, x, y, mods);
_ptrs.add(ptr);
startLongPress(ptr);
_handler.onPointerDown(value, false);
}
static final int[] DIRECTION_TO_INDEX = new int[]{
7, 2, 2, 6, 6, 4, 4, 8, 8, 3, 3, 5, 5, 1, 1, 7
};
/**
* [direction] is an int between [0] and [15] that represent 16 sections of a
* circle, clockwise, starting at the top.
*/
static KeyValue getKeyAtDirection(KeyboardData.Key k, int direction)
{
return k.keys[DIRECTION_TO_INDEX[direction]];
}
/**
* Get the key nearest to [direction] that is not key0. Take care
* of applying [_handler.modifyKey] to the selected key in the same
* operation to be sure to treat removed keys correctly.
* Return [null] if no key could be found in the given direction or
* if the selected key didn't change.
*/
private KeyValue getNearestKeyAtDirection(Pointer ptr, int direction)
{
KeyValue k;
// [i] is [0, -1, +1, ..., -3, +3], scanning 43% of the circle's area,
// centered on the initial swipe direction.
for (int i = 0; i > -4; i = (~i>>31) - i)
{
int d = (direction + i + 16) % 16;
// Don't make the difference between a key that doesn't exist and a key
// that is removed by [_handler]. Triggers side effects.
k = _handler.modifyKey(getKeyAtDirection(ptr.key, d), ptr.modifiers);
if (k != null)
{
// When the nearest key is a slider, it is only selected if it's placed
// within 18% of the original swipe direction.
// This reduces accidental swipes on the slider and allow typing circle
// gestures without being interrupted by the slider.
if (k.getKind() == KeyValue.Kind.Slider && Math.abs(i) >= 2)
continue;
return k;
}
}
return null;
}
public void onTouchMove(float x, float y, int pointerId)
{
Pointer ptr = getPtr(pointerId);
if (ptr == null)
return;
if (ptr.hasFlagsAny(FLAG_P_SLIDING))
{
ptr.sliding.onTouchMove(ptr, x, y);
return;
}
// The position in a IME windows is clampled to view.
// For a better up swipe behaviour, set the y position to a negative value when clamped.
if (y == 0.0) y = -400;
float dx = x - ptr.downX;
float dy = y - ptr.downY;
float dist = Math.abs(dx) + Math.abs(dy);
if (dist < _config.swipe_dist_px)
{
// Pointer is still on the center.
if (ptr.gesture == null || !ptr.gesture.is_in_progress())
return;
// Gesture ended
ptr.gesture.moved_to_center();
ptr.value = apply_gesture(ptr, ptr.gesture.get_gesture());
ptr.flags = 0;
}
else
{ // Pointer is on a quadrant.
// See [getKeyAtDirection()] for the meaning. The starting point on the
// circle is the top direction.
double a = Math.atan2(dy, dx) + Math.PI;
// a is between 0 and 2pi, 0 is pointing to the left
// add 12 to align 0 to the top
int direction = ((int)(a * 8 / Math.PI) + 12) % 16;
if (ptr.gesture == null)
{ // Gesture starts
ptr.gesture = new Gesture(direction);
KeyValue new_value = getNearestKeyAtDirection(ptr, direction);
if (new_value != null)
{ // Pointer is swiping into a side key.
ptr.value = new_value;
ptr.flags = pointer_flags_of_kv(new_value);
// Start sliding mode
if (new_value.getKind() == KeyValue.Kind.Slider)
startSliding(ptr, x, y, dx, dy, new_value);
}
}
else if (ptr.gesture.changed_direction(direction))
{ // Gesture changed state
if (!ptr.gesture.is_in_progress())
{ // Gesture ended
_handler.onPointerFlagsChanged(true);
}
else
{
ptr.value = apply_gesture(ptr, ptr.gesture.get_gesture());
restartLongPress(ptr);
ptr.flags = 0; // Special behaviors are ignored during a gesture.
_handler.onPointerFlagsChanged(true); // Vibrate
}
}
}
}
// Pointers management
private Pointer getPtr(int pointerId)
{
for (Pointer p : _ptrs)
if (p.pointerId == pointerId)
return p;
return null;
}
private void removePtr(Pointer ptr)
{
_ptrs.remove(ptr);
}
private Pointer getLatched(Pointer target)
{
return getLatched(target.key, target.value);
}
private Pointer getLatched(KeyboardData.Key k, KeyValue v)
{
if (v == null)
return null;
for (Pointer p : _ptrs)
if (p.key == k && p.hasFlagsAny(FLAG_P_LATCHED)
&& p.value != null && p.value.equals(v))
return p;
return null;
}
private void clearLatched()
{
for (int i = _ptrs.size() - 1; i >= 0; i--)
{
Pointer ptr = _ptrs.get(i);
// Latched and not locked, remove
if (ptr.hasFlagsAny(FLAG_P_LATCHED) && (ptr.flags & FLAG_P_LOCKED) == 0)
_ptrs.remove(i);
// Not latched but pressed, don't latch once released and stop long press.
else if ((ptr.flags & FLAG_P_LATCHABLE) != 0)
ptr.flags &= ~FLAG_P_LATCHABLE;
}
}
/** Make a pointer into the locked state. */
private void lockPointer(Pointer ptr, boolean shouldVibrate)
{
ptr.flags = (ptr.flags & ~FLAG_P_DOUBLE_TAP_LOCK) | FLAG_P_LOCKED;
_handler.onPointerFlagsChanged(shouldVibrate);
}
boolean isSliding()
{
for (Pointer ptr : _ptrs)
if (ptr.hasFlagsAny(FLAG_P_SLIDING))
return true;
return false;
}
// Key repeat
/** Message from [_longpress_handler]. */
@Override
public boolean handleMessage(Message msg)
{
for (Pointer ptr : _ptrs)
{
if (ptr.timeoutWhat == msg.what)
{
handleLongPress(ptr);
return true;
}
}
return false;
}
private static int uniqueTimeoutWhat = 0;
private void startLongPress(Pointer ptr)
{
int what = (uniqueTimeoutWhat++);
ptr.timeoutWhat = what;
_longpress_handler.sendEmptyMessageDelayed(what, _config.longPressTimeout);
}
private void stopLongPress(Pointer ptr)
{
_longpress_handler.removeMessages(ptr.timeoutWhat);
}
private void restartLongPress(Pointer ptr)
{
stopLongPress(ptr);
startLongPress(ptr);
}
/** A pointer is long pressing. */
private void handleLongPress(Pointer ptr)
{
// Long press toggle lock on modifiers
if ((ptr.flags & FLAG_P_LATCHABLE) != 0)
{
if (!ptr.hasFlagsAny(FLAG_P_CANT_LOCK))
lockPointer(ptr, true);
return;
}
// Latched key, no key
if (ptr.hasFlagsAny(FLAG_P_LATCHED) || ptr.value == null)
return;
// Key is long-pressable
KeyValue kv = KeyModifier.modify_long_press(ptr.value);
if (!kv.equals(ptr.value))
{
ptr.value = kv;
_handler.onPointerDown(kv, true);
return;
}
// Special keys
if (kv.hasFlagsAny(KeyValue.FLAG_SPECIAL))
return;
// For every other keys, key-repeat
if (_config.keyrepeat_enabled)
{
_handler.onPointerHold(kv, ptr.modifiers);
_longpress_handler.sendEmptyMessageDelayed(ptr.timeoutWhat,
_config.longPressInterval);
}
}
// Sliding
/** When sliding is ongoing, key events are handled by the [Slider] class.
[kv] must be of kind [Slider]. */
void startSliding(Pointer ptr, float x, float y, float dx, float dy, KeyValue kv)
{
int r = kv.getSliderRepeat();
int dirx = dx < 0 ? -r : r;
int diry = dy < 0 ? -r : r;
stopLongPress(ptr);
ptr.flags |= FLAG_P_SLIDING;
ptr.sliding = new Sliding(x, y, dirx, diry, kv.getSlider());
_handler.onPointerDown(kv, true);
}
/** Return the [FLAG_P_*] flags that correspond to pressing [kv]. */
int pointer_flags_of_kv(KeyValue kv)
{
int flags = 0;
if (kv.hasFlagsAny(KeyValue.FLAG_LATCH))
{
// Non-special latchable key must clear modifiers and can't be locked
if (!kv.hasFlagsAny(KeyValue.FLAG_SPECIAL))
flags |= FLAG_P_CLEAR_LATCHED | FLAG_P_CANT_LOCK;
flags |= FLAG_P_LATCHABLE;
}
if (_config.double_tap_lock_shift &&
kv.hasFlagsAny(KeyValue.FLAG_DOUBLE_TAP_LOCK))
flags |= FLAG_P_DOUBLE_TAP_LOCK;
return flags;
}
// Gestures
/** Apply a gesture to the current key. */
KeyValue apply_gesture(Pointer ptr, Gesture.Name gesture)
{
switch (gesture)
{
case None:
return ptr.value;
case Swipe:
return ptr.value;
case Roundtrip:
return
modify_key_with_extra_modifier(
ptr,
getNearestKeyAtDirection(ptr, ptr.gesture.current_direction()),
KeyValue.Modifier.GESTURE);
case Circle:
return
modify_key_with_extra_modifier(ptr, ptr.key.keys[0],
KeyValue.Modifier.GESTURE);
case Anticircle:
return _handler.modifyKey(ptr.key.anticircle, ptr.modifiers);
}
return ptr.value; // Unreachable
}
KeyValue modify_key_with_extra_modifier(Pointer ptr, KeyValue kv,
KeyValue.Modifier extra_mod)
{
return
_handler.modifyKey(kv,
ptr.modifiers.with_extra_mod(KeyValue.makeInternalModifier(extra_mod)));
}
// Pointers
Pointer make_pointer(int p, KeyboardData.Key k, KeyValue v, float x, float y,
Modifiers m)
{
int flags = (v == null) ? 0 : pointer_flags_of_kv(v);
return new Pointer(p, k, v, x, y, m, flags);
}
private static final class Pointer
{
/** -1 when latched. */
public int pointerId;
/** The Key pressed by this Pointer */
public final KeyboardData.Key key;
/** Gesture state, see [Gesture]. [null] means the pointer has not moved out of the center region. */
public Gesture gesture;
/** Selected value with [modifiers] applied. */
public KeyValue value;
public float downX;
public float downY;
/** Modifier flags at the time the key was pressed. */
public Modifiers modifiers;
/** See [FLAG_P_*] flags. */
public int flags;
/** Identify timeout messages. */
public int timeoutWhat;
/** [null] when not in sliding mode. */
public Sliding sliding;
public Pointer(int p, KeyboardData.Key k, KeyValue v, float x, float y, Modifiers m, int f)
{
pointerId = p;
key = k;
gesture = null;
value = v;
downX = x;
downY = y;
modifiers = m;
flags = f;
timeoutWhat = -1;
sliding = null;
}
public boolean hasFlagsAny(int has)
{
return ((flags & has) != 0);
}
}
public final class Sliding
{
/** Accumulated distance since last event. */
float d = 0.f;
/** The slider speed changes depending on the pointer speed. */
float speed = 0.5f;
/** Coordinate of the last move. */
float last_x;
float last_y;
/** [System.currentTimeMillis()] at the time of the last move. Equals to
[-1] when the sliding hasn't started yet. */
long last_move_ms = -1;
/** The property which is being slided. */
KeyValue.Slider slider;
/** Direction of the initial movement, positive if sliding to the right and
negative if sliding to the left. */
int direction_x;
int direction_y;
public Sliding(float x, float y, int dirx, int diry, KeyValue.Slider s)
{
last_x = x;
last_y = y;
slider = s;
direction_x = dirx;
direction_y = diry;
}
static final float SPEED_SMOOTHING = 0.7f;
/** Avoid absurdly large values. */
static final float SPEED_MAX = 4.f;
/** Make vertical sliders slower. The intention is to make the up/down
slider slower, as we have less visibility and do smaller movements in
that direction. */
static final float SPEED_VERTICAL_MULT = 0.5f;
public void onTouchMove(Pointer ptr, float x, float y)
{
// Start sliding only after the pointer has travelled an other distance.
// This allows to trigger the slider movements only once with a short
// swipe.
float travelled = Math.abs(x - last_x) + Math.abs(y - last_y);
if (last_move_ms == -1)
{
if (travelled < _config.swipe_dist_px)
return;
last_move_ms = System.currentTimeMillis();
}
d += ((x - last_x) * speed * direction_x
+ (y - last_y) * speed * SPEED_VERTICAL_MULT * direction_y)
/ _config.slide_step_px;
update_speed(travelled, x, y);
// Send an event when [abs(d)] exceeds [1].
int d_ = (int)d;
if (d_ != 0)
{
d -= d_;
_handler.onPointerHold(KeyValue.sliderKey(slider, d_),
ptr.modifiers);
}
}
/** Handle a sliding pointer going up. Latched modifiers are not
cleared to allow easy adjustments to the cursors. The pointer is
cancelled. */
public void onTouchUp(Pointer ptr)
{
removePtr(ptr);
_handler.onPointerFlagsChanged(false);
}
/** [speed] is computed from the elapsed time and distance traveled
between two move events. Exponential smoothing is used to smooth out
the noise. Sets [last_move_ms] and [last_pos]. */
void update_speed(float travelled, float x, float y)
{
long now = System.currentTimeMillis();
float instant_speed = Math.min(SPEED_MAX,
travelled / (float)(now - last_move_ms) + 1.f);
speed = speed + (instant_speed - speed) * SPEED_SMOOTHING;
last_move_ms = now;
last_x = x;
last_y = y;
}
}
/** Represent modifiers currently activated.
Sorted in the order they should be evaluated. */
public static final class Modifiers
{
private final KeyValue[] _mods;
private final int _size;
private Modifiers(KeyValue[] m, int s)
{
_mods = m; _size = s;
}
public KeyValue get(int i) { return _mods[_size - 1 - i]; }
public int size() { return _size; }
public boolean has(KeyValue.Modifier m)
{
for (int i = 0; i < _size; i++)
{
KeyValue kv = _mods[i];
switch (kv.getKind())
{
case Modifier:
if (kv.getModifier().equals(m))
return true;
}
}
return false;
}
/** Return a copy of this object with an extra modifier added. */
public Modifiers with_extra_mod(KeyValue m)
{
KeyValue[] newmods = Arrays.copyOf(_mods, _size + 1);
newmods[_size] = m;
return ofArray(newmods, newmods.length);
}
/** Returns the activated modifiers that are not in [m2]. */
public Iterator<KeyValue> diff(Modifiers m2)
{
return new ModifiersDiffIterator(this, m2);
}
@Override
public int hashCode() { return Arrays.hashCode(_mods); }
@Override
public boolean equals(Object obj)
{
return Arrays.equals(_mods, ((Modifiers)obj)._mods);
}
public static final Modifiers EMPTY =
new Modifiers(new KeyValue[0], 0);
protected static Modifiers ofArray(KeyValue[] mods, int size)
{
// Sort and remove duplicates and nulls.
if (size > 1)
{
Arrays.sort(mods, 0, size);
int j = 0;
for (int i = 0; i < size; i++)
{
KeyValue m = mods[i];
if (m != null && (i + 1 >= size || m != mods[i + 1]))
{
mods[j] = m;
j++;
}
}
size = j;
}
return new Modifiers(mods, size);
}
/** Returns modifiers that are in [m1_] but not in [m2_]. */
static final class ModifiersDiffIterator
implements Iterator<KeyValue>
{
Modifiers m1;
int i1 = 0;
Modifiers m2;
int i2 = 0;
public ModifiersDiffIterator(Modifiers m1_, Modifiers m2_)
{
m1 = m1_;
m2 = m2_;
advance();
}
public boolean hasNext()
{
return i1 < m1._size;
}
public KeyValue next()
{
if (i1 >= m1._size)
throw new NoSuchElementException();
KeyValue m = m1._mods[i1];
i1++;
advance();
return m;
}
/** Advance to the next element if [i1] is not a valid element. The end
is reached when [i1 = m1.size()]. */
void advance()
{
while (i1 < m1.size())
{
KeyValue m = m1._mods[i1];
while (true)
{
if (i2 >= m2._size)
return;
int d = m.compareTo(m2._mods[i2]);
if (d < 0)
return;
i2++;
if (d == 0)
break;
}
i1++;
}
}
}
}
public interface IPointerEventHandler
{
/** Key can be modified or removed by returning [null]. */
public KeyValue modifyKey(KeyValue k, Modifiers mods);
/** A key is pressed. [getModifiers()] is uptodate. Might be called after a
press or a swipe to a different value. Down events are not paired with
up events. */
public void onPointerDown(KeyValue k, boolean isSwipe);
/** Key is released. [k] is the key that was returned by
[modifySelectedKey] or [modifySelectedKey]. */
public void onPointerUp(KeyValue k, Modifiers mods);
/** Flags changed because latched or locked keys or cancelled pointers. */
public void onPointerFlagsChanged(boolean shouldVibrate);
/** Key is repeating. */
public void onPointerHold(KeyValue k, Modifiers mods);
}
}

View file

@ -0,0 +1,49 @@
package juloo.keyboard2;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
public class SettingsActivity extends PreferenceActivity
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
// The preferences can't be read when in direct-boot mode. Avoid crashing
// and don't allow changing the settings.
// Run the config migration on this prefs as it might be different from the
// one used by the keyboard, which have been migrated.
try
{
Config.migrate(getPreferenceManager().getSharedPreferences());
}
catch (Exception _e) { fallbackEncrypted(); return; }
addPreferencesFromResource(R.xml.settings);
boolean foldableDevice = FoldStateTracker.isFoldableDevice(this);
findPreference("margin_bottom_portrait_unfolded").setEnabled(foldableDevice);
findPreference("margin_bottom_landscape_unfolded").setEnabled(foldableDevice);
findPreference("horizontal_margin_portrait_unfolded").setEnabled(foldableDevice);
findPreference("horizontal_margin_landscape_unfolded").setEnabled(foldableDevice);
findPreference("keyboard_height_unfolded").setEnabled(foldableDevice);
findPreference("keyboard_height_landscape_unfolded").setEnabled(foldableDevice);
}
void fallbackEncrypted()
{
// Can't communicate with the user here.
finish();
}
protected void onStop()
{
DirectBootAwarePreferences
.copy_preferences_to_protected_storage(this,
getPreferenceManager().getSharedPreferences());
super.onStop();
}
}

View file

@ -0,0 +1,202 @@
package juloo.keyboard2;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.util.AttributeSet;
public class Theme
{
// Key colors
public final int colorKey;
public final int colorKeyActivated;
// Label colors
public final int lockedColor;
public final int activatedColor;
public final int labelColor;
public final int subLabelColor;
public final int secondaryLabelColor;
public final int greyedLabelColor;
// Key borders
public final float keyBorderRadius;
public final float keyBorderWidth;
public final float keyBorderWidthActivated;
public final int keyBorderColorLeft;
public final int keyBorderColorTop;
public final int keyBorderColorRight;
public final int keyBorderColorBottom;
public final int colorNavBar;
public final boolean isLightNavBar;
public Theme(Context context, AttributeSet attrs)
{
getKeyFont(context); // _key_font will be accessed
TypedArray s = context.getTheme().obtainStyledAttributes(attrs, R.styleable.keyboard, 0, 0);
colorKey = s.getColor(R.styleable.keyboard_colorKey, 0);
colorKeyActivated = s.getColor(R.styleable.keyboard_colorKeyActivated, 0);
// colorKeyboard = s.getColor(R.styleable.keyboard_colorKeyboard, 0);
colorNavBar = s.getColor(R.styleable.keyboard_navigationBarColor, 0);
isLightNavBar = s.getBoolean(R.styleable.keyboard_windowLightNavigationBar, false);
labelColor = s.getColor(R.styleable.keyboard_colorLabel, 0);
activatedColor = s.getColor(R.styleable.keyboard_colorLabelActivated, 0);
lockedColor = s.getColor(R.styleable.keyboard_colorLabelLocked, 0);
subLabelColor = s.getColor(R.styleable.keyboard_colorSubLabel, 0);
secondaryLabelColor = adjustLight(labelColor,
s.getFloat(R.styleable.keyboard_secondaryDimming, 0.25f));
greyedLabelColor = adjustLight(labelColor,
s.getFloat(R.styleable.keyboard_greyedDimming, 0.5f));
keyBorderRadius = s.getDimension(R.styleable.keyboard_keyBorderRadius, 0);
keyBorderWidth = s.getDimension(R.styleable.keyboard_keyBorderWidth, 0);
keyBorderWidthActivated = s.getDimension(R.styleable.keyboard_keyBorderWidthActivated, 0);
keyBorderColorLeft = s.getColor(R.styleable.keyboard_keyBorderColorLeft, colorKey);
keyBorderColorTop = s.getColor(R.styleable.keyboard_keyBorderColorTop, colorKey);
keyBorderColorRight = s.getColor(R.styleable.keyboard_keyBorderColorRight, colorKey);
keyBorderColorBottom = s.getColor(R.styleable.keyboard_keyBorderColorBottom, colorKey);
s.recycle();
}
/** Interpolate the 'value' component toward its opposite by 'alpha'. */
int adjustLight(int color, float alpha)
{
float[] hsv = new float[3];
Color.colorToHSV(color, hsv);
float v = hsv[2];
hsv[2] = alpha - (2 * alpha - 1) * v;
return Color.HSVToColor(hsv);
}
Paint initIndicationPaint(Paint.Align align, Typeface font)
{
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setTextAlign(align);
if (font != null)
paint.setTypeface(font);
return (paint);
}
static Typeface _key_font = null;
static public Typeface getKeyFont(Context context)
{
if (_key_font == null)
_key_font = Typeface.createFromAsset(context.getAssets(), "special_font.ttf");
return _key_font;
}
public static final class Computed
{
public final float vertical_margin;
public final float horizontal_margin;
public final float margin_top;
public final float margin_left;
public final float row_height;
public final Paint indication_paint;
public final Key key;
public final Key key_activated;
public Computed(Theme theme, Config config, float keyWidth, KeyboardData layout)
{
// Rows height is proportional to the keyboard height, meaning it doesn't
// change for layouts with more or less rows. 3.95 is the usual height of
// a layout in KeyboardData unit. The keyboard will be higher if the
// layout has more rows and smaller if it has less because rows stay the
// same height.
row_height = Math.min(
config.screenHeightPixels * config.keyboardHeightPercent / 100 / 3.95f,
config.screenHeightPixels / layout.keysHeight);
vertical_margin = config.key_vertical_margin * row_height;
horizontal_margin = config.key_horizontal_margin * keyWidth;
// Add half of the key margin on the left and on the top as it's also
// added on the right and on the bottom of every keys.
margin_top = config.marginTop + vertical_margin / 2;
margin_left = horizontal_margin / 2;
key = new Key(theme, config, keyWidth, false);
key_activated = new Key(theme, config, keyWidth, true);
indication_paint = init_label_paint(config, null);
indication_paint.setColor(theme.subLabelColor);
}
public static final class Key
{
public final Paint bg_paint = new Paint();
public final Paint border_left_paint;
public final Paint border_top_paint;
public final Paint border_right_paint;
public final Paint border_bottom_paint;
public final float border_width;
public final float border_radius;
final Paint _label_paint;
final Paint _special_label_paint;
final Paint _sublabel_paint;
final Paint _special_sublabel_paint;
final int _label_alpha_bits;
public Key(Theme theme, Config config, float keyWidth, boolean activated)
{
bg_paint.setColor(activated ? theme.colorKeyActivated : theme.colorKey);
if (config.borderConfig)
{
border_radius = config.customBorderRadius * keyWidth;
border_width = config.customBorderLineWidth;
}
else
{
border_radius = theme.keyBorderRadius;
border_width = activated ? theme.keyBorderWidthActivated : theme.keyBorderWidth;
}
bg_paint.setAlpha(activated ? config.keyActivatedOpacity : config.keyOpacity);
border_left_paint = init_border_paint(config, border_width, theme.keyBorderColorLeft);
border_top_paint = init_border_paint(config, border_width, theme.keyBorderColorTop);
border_right_paint = init_border_paint(config, border_width, theme.keyBorderColorRight);
border_bottom_paint = init_border_paint(config, border_width, theme.keyBorderColorBottom);
_label_paint = init_label_paint(config, null);
_special_label_paint = init_label_paint(config, _key_font);
_sublabel_paint = init_label_paint(config, null);
_special_sublabel_paint = init_label_paint(config, _key_font);
_label_alpha_bits = (config.labelBrightness & 0xFF) << 24;
}
public Paint label_paint(boolean special_font, int color, float text_size)
{
Paint p = special_font ? _special_label_paint : _label_paint;
p.setColor((color & 0x00FFFFFF) | _label_alpha_bits);
p.setTextSize(text_size);
return p;
}
public Paint sublabel_paint(boolean special_font, int color, float text_size, Paint.Align align)
{
Paint p = special_font ? _special_sublabel_paint : _sublabel_paint;
p.setColor((color & 0x00FFFFFF) | _label_alpha_bits);
p.setTextSize(text_size);
p.setTextAlign(align);
return p;
}
}
static Paint init_border_paint(Config config, float border_width, int color)
{
Paint p = new Paint();
p.setAlpha(config.keyOpacity);
p.setStyle(Paint.Style.STROKE);
p.setStrokeWidth(border_width);
p.setColor(color);
return p;
}
static Paint init_label_paint(Config config, Typeface font)
{
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setTextAlign(Paint.Align.CENTER);
if (font != null)
p.setTypeface(font);
return p;
}
}
}

View file

@ -0,0 +1,52 @@
package juloo.keyboard2;
import android.app.AlertDialog;
import android.content.res.Resources;
import android.graphics.Insets;
import android.os.Build.VERSION;
import android.os.IBinder;
import android.view.View;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowManager;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Locale;
public final class Utils
{
/** Turn the first letter of a string uppercase. */
public static String capitalize_string(String s)
{
if (s.length() < 1)
return s;
// Make sure not to cut a code point in half
int i = s.offsetByCodePoints(0, 1);
return s.substring(0, i).toUpperCase(Locale.getDefault()) + s.substring(i);
}
/** Like [dialog.show()] but properly configure layout params when called
from an IME. [token] is the input view's [getWindowToken()]. */
public static void show_dialog_on_ime(AlertDialog dialog, IBinder token)
{
Window win = dialog.getWindow();
WindowManager.LayoutParams lp = win.getAttributes();
lp.token = token;
lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
win.setAttributes(lp);
win.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
dialog.show();
}
public static String read_all_utf8(InputStream inp) throws Exception
{
InputStreamReader reader = new InputStreamReader(inp, "UTF-8");
StringBuilder out = new StringBuilder();
int buff_length = 8000;
char[] buff = new char[buff_length];
int l;
while ((l = reader.read(buff, 0, buff_length)) != -1)
out.append(buff, 0, l);
return out.toString();
}
}

View file

@ -0,0 +1,46 @@
package juloo.keyboard2;
import android.content.Context;
import android.os.Vibrator;
import android.view.HapticFeedbackConstants;
import android.view.View;
public final class VibratorCompat
{
public static void vibrate(View v, Config config)
{
if (config.vibrate_custom)
{
if (config.vibrate_duration > 0)
vibrator_vibrate(v, config.vibrate_duration);
}
else
{
v.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP,
HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
}
}
/** Use the older [Vibrator] when the newer API is not available or the user
wants more control. */
static void vibrator_vibrate(View v, long duration)
{
try
{
get_vibrator(v).vibrate(duration);
}
catch (Exception e) {}
}
static Vibrator vibrator_service = null;
static Vibrator get_vibrator(View v)
{
if (vibrator_service == null)
{
vibrator_service =
(Vibrator)v.getContext().getSystemService(Context.VIBRATOR_SERVICE);
}
return vibrator_service;
}
}

View file

@ -0,0 +1,152 @@
package juloo.keyboard2;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.inputmethodservice.InputMethodService;
import android.os.Build.VERSION;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
import android.widget.ArrayAdapter;
import android.widget.Toast;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.List;
class VoiceImeSwitcher
{
static final String PREF_LAST_USED = "voice_ime_last_used";
static final String PREF_KNOWN_IMES = "voice_ime_known";
/** Switch to the voice ime. This might open a chooser popup. Preferences are
used to store the last selected voice ime and to detect whether the
chooser popup must be shown. Returns [false] if the detection failed and
is unlikely to succeed. */
public static boolean switch_to_voice_ime(InputMethodService ims,
InputMethodManager imm, SharedPreferences prefs)
{
List<IME> imes = get_voice_ime_list(imm);
String last_used = prefs.getString(PREF_LAST_USED, null);
String last_known_imes = prefs.getString(PREF_KNOWN_IMES, null);
IME last_used_ime = get_ime_by_id(imes, last_used);
if (imes.size() == 0)
return false;
if (last_used == null || last_known_imes == null || last_used_ime == null
|| !last_known_imes.equals(serialize_ime_ids(imes)))
choose_voice_ime_and_update_prefs(ims, prefs, imes);
else
switch_input_method(ims, last_used_ime);
return true;
}
public static boolean choose_voice_ime(InputMethodService ims,
InputMethodManager imm, SharedPreferences prefs)
{
List<IME> imes = get_voice_ime_list(imm);
choose_voice_ime_and_update_prefs(ims, prefs, imes);
return true;
}
/** Show the voice IME chooser popup and switch to the selected IME.
Preferences are updated so that future calls to [switch_to_voice_ime]
switch to the newly selected IME. */
static void choose_voice_ime_and_update_prefs(final InputMethodService ims,
final SharedPreferences prefs, final List<IME> imes)
{
List<String> ime_display_names = get_ime_display_names(ims, imes);
ArrayAdapter layouts = new ArrayAdapter(ims, android.R.layout.simple_list_item_1, ime_display_names);
AlertDialog dialog = new AlertDialog.Builder(ims)
.setAdapter(layouts, new DialogInterface.OnClickListener(){
public void onClick(DialogInterface _dialog, int which)
{
IME selected = imes.get(which);
prefs.edit()
.putString(PREF_LAST_USED, selected.get_id())
.putString(PREF_KNOWN_IMES, serialize_ime_ids(imes))
.apply();
switch_input_method(ims, selected);
}
})
.create();
if (ime_display_names.size() == 0)
dialog.setMessage(ims.getResources().getString(R.string.toast_no_voice_input));
Utils.show_dialog_on_ime(dialog, ims.getWindow().getWindow().getDecorView().getWindowToken());
}
static void switch_input_method(InputMethodService ims, IME ime)
{
if (VERSION.SDK_INT < 28)
ims.switchInputMethod(ime.get_id());
else
ims.switchInputMethod(ime.get_id(), ime.subtype);
}
static IME get_ime_by_id(List<IME> imes, String id)
{
if (id != null)
for (IME ime : imes)
if (ime.get_id().equals(id))
return ime;
return null;
}
static List<String> get_ime_display_names(InputMethodService ims, List<IME> imes)
{
List<String> names = new ArrayList<String>();
for (IME ime : imes)
names.add(ime.get_display_name(ims));
return names;
}
static List<IME> get_voice_ime_list(InputMethodManager imm)
{
List<IME> imes = new ArrayList<IME>();
for (InputMethodInfo im : imm.getEnabledInputMethodList())
for (InputMethodSubtype imst : imm.getEnabledInputMethodSubtypeList(im, true))
if (imst.getMode().equals("voice"))
imes.add(new IME(im, imst));
return imes;
}
/** The chooser popup is shown whether this string changes. */
static String serialize_ime_ids(List<IME> imes)
{
StringBuilder b = new StringBuilder();
for (IME ime : imes)
{
b.append(ime.get_id());
b.append(',');
}
return b.toString();
}
static class IME
{
public final InputMethodInfo im;
public final InputMethodSubtype subtype;
IME(InputMethodInfo im_, InputMethodSubtype st)
{
im = im_;
subtype = st;
}
String get_id() { return im.getId(); }
/** Localised display name. */
String get_display_name(Context ctx)
{
String subtype_name = "";
if (VERSION.SDK_INT >= 14)
{
subtype_name = subtype.getDisplayName(ctx, im.getPackageName(), null).toString();
if (!subtype_name.equals(""))
subtype_name = " - " + subtype_name;
}
return im.loadLabel(ctx.getPackageManager()).toString() + subtype_name;
}
}
}

View file

@ -0,0 +1,73 @@
package juloo.keyboard2.prefs;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.TextView;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import juloo.keyboard2.*;
import org.json.JSONArray;
import org.json.JSONException;
/** Allows to enter custom keys to be added to the keyboard. This shows up at
the top of the "Add keys to the keyboard" option. */
public class CustomExtraKeysPreference extends ListGroupPreference<String>
{
/** This pref stores a list of strings encoded as JSON. */
static final String KEY = "custom_extra_keys";
static final ListGroupPreference.Serializer<String> SERIALIZER =
new ListGroupPreference.StringSerializer();
public CustomExtraKeysPreference(Context context, AttributeSet attrs)
{
super(context, attrs);
setKey(KEY);
}
public static Map<KeyValue, KeyboardData.PreferredPos> get(SharedPreferences prefs)
{
Map<KeyValue, KeyboardData.PreferredPos> kvs =
new HashMap<KeyValue, KeyboardData.PreferredPos>();
List<String> key_names = load_from_preferences(KEY, prefs, null, SERIALIZER);
if (key_names != null)
{
for (String key_name : key_names)
kvs.put(KeyValue.getKeyByName(key_name), KeyboardData.PreferredPos.DEFAULT);
}
return kvs;
}
String label_of_value(String value, int i) { return value; }
@Override
void select(final SelectionCallback<String> callback, String old_value)
{
View content = View.inflate(getContext(), R.layout.dialog_edit_text, null);
((TextView)content.findViewById(R.id.text)).setText(old_value);
new AlertDialog.Builder(getContext())
.setView(content)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener(){
public void onClick(DialogInterface dialog, int which)
{
EditText input = (EditText)((AlertDialog)dialog).findViewById(R.id.text);
final String k = input.getText().toString();
if (!k.equals(""))
callback.select(k);
}
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
@Override
Serializer<String> get_serializer() { return SERIALIZER; }
}

View file

@ -0,0 +1,412 @@
package juloo.keyboard2.prefs;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.Build.VERSION;
import android.preference.CheckBoxPreference;
import android.preference.PreferenceCategory;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import juloo.keyboard2.*;
/** This class implements the "extra keys" preference but also defines the
possible extra keys. */
public class ExtraKeysPreference extends PreferenceCategory
{
/** Array of the keys that can be selected. */
public static String[] extra_keys = new String[]
{
"alt",
"meta",
"compose",
"voice_typing",
"switch_clipboard",
"accent_aigu",
"accent_grave",
"accent_double_aigu",
"accent_dot_above",
"accent_circonflexe",
"accent_tilde",
"accent_cedille",
"accent_trema",
"accent_ring",
"accent_caron",
"accent_macron",
"accent_ogonek",
"accent_breve",
"accent_slash",
"accent_bar",
"accent_dot_below",
"accent_hook_above",
"accent_horn",
"accent_double_grave",
"",
"ß",
"£",
"§",
"",
"ª",
"º",
"zwj",
"zwnj",
"nbsp",
"nnbsp",
"tab",
"esc",
"page_up",
"page_down",
"home",
"end",
"switch_greekmath",
"change_method",
"capslock",
"copy",
"paste",
"cut",
"selectAll",
"shareText",
"pasteAsPlainText",
"undo",
"redo",
"delete_word",
"forward_delete_word",
"superscript",
"subscript",
"f11_placeholder",
"f12_placeholder",
"menu",
"scroll_lock",
"combining_dot_above",
"combining_double_aigu",
"combining_slash",
"combining_arrow_right",
"combining_breve",
"combining_bar",
"combining_aigu",
"combining_caron",
"combining_cedille",
"combining_circonflexe",
"combining_grave",
"combining_macron",
"combining_ring",
"combining_tilde",
"combining_trema",
"combining_ogonek",
"combining_dot_below",
"combining_horn",
"combining_hook_above",
"combining_vertical_tilde",
"combining_inverted_breve",
"combining_pokrytie",
"combining_slavonic_psili",
"combining_slavonic_dasia",
"combining_payerok",
"combining_titlo",
"combining_vzmet",
"combining_arabic_v",
"combining_arabic_inverted_v",
"combining_shaddah",
"combining_sukun",
"combining_fatha",
"combining_dammah",
"combining_kasra",
"combining_hamza_above",
"combining_hamza_below",
"combining_alef_above",
"combining_fathatan",
"combining_kasratan",
"combining_dammatan",
"combining_alef_below",
"combining_kavyka",
"combining_palatalization",
};
/** Whether an extra key is enabled by default. */
public static boolean default_checked(String name)
{
switch (name)
{
case "voice_typing":
case "change_method":
case "switch_clipboard":
case "compose":
case "tab":
case "esc":
case "f11_placeholder":
case "f12_placeholder":
return true;
default:
return false;
}
}
/** Text that describe a key. Might be null. */
static String key_description(Resources res, String name)
{
int id = 0;
String additional_info = null;
switch (name)
{
case "capslock": id = R.string.key_descr_capslock; break;
case "change_method": id = R.string.key_descr_change_method; break;
case "compose": id = R.string.key_descr_compose; break;
case "copy": id = R.string.key_descr_copy; break;
case "cut": id = R.string.key_descr_cut; break;
case "end":
id = R.string.key_descr_end;
additional_info = format_key_combination(new String[]{"fn", "right"});
break;
case "home":
id = R.string.key_descr_home;
additional_info = format_key_combination(new String[]{"fn", "left"});
break;
case "page_down":
id = R.string.key_descr_page_down;
additional_info = format_key_combination(new String[]{"fn", "down"});
break;
case "page_up":
id = R.string.key_descr_page_up;
additional_info = format_key_combination(new String[]{"fn", "up"});
break;
case "paste": id = R.string.key_descr_paste; break;
case "pasteAsPlainText":
id = R.string.key_descr_pasteAsPlainText;
additional_info = format_key_combination(new String[]{"fn", "paste"});
break;
case "redo":
id = R.string.key_descr_redo;
additional_info = format_key_combination(new String[]{"fn", "undo"});
break;
case "delete_word":
id = R.string.key_descr_delete_word;
additional_info = format_key_combination_gesture(res, "backspace");
break;
case "forward_delete_word":
id = R.string.key_descr_forward_delete_word;
additional_info = format_key_combination_gesture(res, "forward_delete");
break;
case "selectAll": id = R.string.key_descr_selectAll; break;
case "subscript": id = R.string.key_descr_subscript; break;
case "superscript": id = R.string.key_descr_superscript; break;
case "switch_greekmath": id = R.string.key_descr_switch_greekmath; break;
case "undo": id = R.string.key_descr_undo; break;
case "voice_typing": id = R.string.key_descr_voice_typing; break;
case "ª": id = R.string.key_descr_ª; break;
case "º": id = R.string.key_descr_º; break;
case "switch_clipboard": id = R.string.key_descr_clipboard; break;
case "zwj": id = R.string.key_descr_zwj; break;
case "zwnj": id = R.string.key_descr_zwnj; break;
case "nbsp": id = R.string.key_descr_nbsp; break;
case "nnbsp": id = R.string.key_descr_nnbsp; break;
case "accent_aigu":
case "accent_grave":
case "accent_double_aigu":
case "accent_dot_above":
case "accent_circonflexe":
case "accent_tilde":
case "accent_cedille":
case "accent_trema":
case "accent_ring":
case "accent_caron":
case "accent_macron":
case "accent_ogonek":
case "accent_breve":
case "accent_slash":
case "accent_bar":
case "accent_dot_below":
case "accent_hook_above":
case "accent_horn":
case "accent_double_grave":
id = R.string.key_descr_dead_key;
break;
case "combining_dot_above":
case "combining_double_aigu":
case "combining_slash":
case "combining_arrow_right":
case "combining_breve":
case "combining_bar":
case "combining_aigu":
case "combining_caron":
case "combining_cedille":
case "combining_circonflexe":
case "combining_grave":
case "combining_macron":
case "combining_ring":
case "combining_tilde":
case "combining_trema":
case "combining_ogonek":
case "combining_dot_below":
case "combining_horn":
case "combining_hook_above":
case "combining_vertical_tilde":
case "combining_inverted_breve":
case "combining_pokrytie":
case "combining_slavonic_psili":
case "combining_slavonic_dasia":
case "combining_payerok":
case "combining_titlo":
case "combining_vzmet":
case "combining_arabic_v":
case "combining_arabic_inverted_v":
case "combining_shaddah":
case "combining_sukun":
case "combining_fatha":
case "combining_dammah":
case "combining_kasra":
case "combining_hamza_above":
case "combining_hamza_below":
case "combining_alef_above":
case "combining_fathatan":
case "combining_kasratan":
case "combining_dammatan":
case "combining_alef_below":
case "combining_kavyka":
case "combining_palatalization":
id = R.string.key_descr_combining;
break;
}
if (id == 0)
return additional_info;
String descr = res.getString(id);
if (additional_info != null)
descr += "" + additional_info;
return descr;
}
static String key_title(String key_name, KeyValue kv)
{
switch (key_name)
{
case "f11_placeholder": return "F11";
case "f12_placeholder": return "F12";
}
return kv.getString();
}
/** Format a key combination */
static String format_key_combination(String[] keys)
{
StringBuilder out = new StringBuilder();
for (int i = 0; i < keys.length; i++)
{
if (i > 0) out.append(" + ");
out.append(KeyValue.getKeyByName(keys[i]).getString());
}
return out.toString();
}
/** Explain a gesture on a key */
static String format_key_combination_gesture(Resources res, String key_name)
{
return res.getString(R.string.key_descr_gesture) + " + "
+ KeyValue.getKeyByName(key_name).getString();
}
/** Place an extra key next to the key specified by the first argument, on
bottom-right preferably or on the bottom-left. If the specified key is not
on the layout, place on the specified row and column. */
static KeyboardData.PreferredPos mk_preferred_pos(String next_to_key, int row, int col, boolean prefer_bottom_right)
{
KeyValue next_to = (next_to_key == null) ? null : KeyValue.getKeyByName(next_to_key);
int d1, d2; // Preferred direction and fallback direction
if (prefer_bottom_right) { d1 = 4; d2 = 3; } else { d1 = 3; d2 = 4; }
return new KeyboardData.PreferredPos(next_to,
new KeyboardData.KeyPos[]{
new KeyboardData.KeyPos(row, col, d1),
new KeyboardData.KeyPos(row, col, d2),
new KeyboardData.KeyPos(row, -1, d1),
new KeyboardData.KeyPos(row, -1, d2),
new KeyboardData.KeyPos(-1, -1, -1),
});
}
static KeyboardData.PreferredPos key_preferred_pos(String key_name)
{
switch (key_name)
{
case "cut": return mk_preferred_pos("x", 2, 2, true);
case "copy": return mk_preferred_pos("c", 2, 3, true);
case "paste": return mk_preferred_pos("v", 2, 4, true);
case "undo": return mk_preferred_pos("z", 2, 1, true);
case "selectAll": return mk_preferred_pos("a", 1, 0, true);
case "redo": return mk_preferred_pos("y", 0, 5, true);
case "f11_placeholder": return mk_preferred_pos("9", 0, 8, false);
case "f12_placeholder": return mk_preferred_pos("0", 0, 9, false);
case "delete_word": return mk_preferred_pos("backspace", -1, -1, false);
case "forward_delete_word": return mk_preferred_pos("backspace", -1, -1, true);
}
return KeyboardData.PreferredPos.DEFAULT;
}
/** Get the set of enabled extra keys. */
public static Map<KeyValue, KeyboardData.PreferredPos> get_extra_keys(SharedPreferences prefs)
{
Map<KeyValue, KeyboardData.PreferredPos> ks =
new HashMap<KeyValue, KeyboardData.PreferredPos>();
for (String key_name : extra_keys)
{
if (prefs.getBoolean(pref_key_of_key_name(key_name),
default_checked(key_name)))
ks.put(KeyValue.getKeyByName(key_name), key_preferred_pos(key_name));
}
return ks;
}
boolean _attached = false; /** Whether it has already been attached. */
public ExtraKeysPreference(Context context, AttributeSet attrs)
{
super(context, attrs);
setOrderingAsAdded(true);
}
@Override
protected void onAttachedToActivity()
{
if (_attached)
return;
_attached = true;
for (String key_name : extra_keys)
addPreference(new ExtraKeyCheckBoxPreference(getContext(), key_name,
default_checked(key_name)));
}
public static String pref_key_of_key_name(String key_name)
{
return "extra_key_" + key_name;
}
static class ExtraKeyCheckBoxPreference extends CheckBoxPreference
{
public ExtraKeyCheckBoxPreference(Context ctx, String key_name,
boolean default_checked)
{
super(ctx);
KeyValue kv = KeyValue.getKeyByName(key_name);
String title = key_title(key_name, kv);
String descr = key_description(ctx.getResources(), key_name);
if (descr != null)
title += " (" + descr + ")";
setKey(pref_key_of_key_name(key_name));
setDefaultValue(default_checked);
setTitle(title);
if (VERSION.SDK_INT >= 26)
setSingleLineTitle(false);
}
@Override
protected void onBindView(View view)
{
super.onBindView(view);
TextView title = (TextView)view.findViewById(android.R.id.title);
title.setTypeface(Theme.getKeyFont(getContext()));
}
}
}

View file

@ -0,0 +1,120 @@
package juloo.keyboard2.prefs;
import android.content.Context;
import android.content.res.TypedArray;
import android.preference.DialogPreference;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.SeekBar;
/*
** IntSlideBarPreference
** -
** Open a dialog showing a seekbar
** -
** xml attrs:
** android:defaultValue Default value (int)
** min min value (int)
** max max value (int)
** -
** Summary field allow to show the current value using %s flag
*/
public class IntSlideBarPreference extends DialogPreference
implements SeekBar.OnSeekBarChangeListener
{
private LinearLayout _layout;
private TextView _textView;
private SeekBar _seekBar;
private int _min;
private String _initialSummary;
public IntSlideBarPreference(Context context, AttributeSet attrs)
{
super(context, attrs);
_initialSummary = getSummary().toString();
_textView = new TextView(context);
_textView.setPadding(48, 40, 48, 40);
_seekBar = new SeekBar(context);
_seekBar.setOnSeekBarChangeListener(this);
_min = attrs.getAttributeIntValue(null, "min", 0);
int max = attrs.getAttributeIntValue(null, "max", 0);
_seekBar.setMax(max - _min);
_layout = new LinearLayout(getContext());
_layout.setOrientation(LinearLayout.VERTICAL);
_layout.addView(_textView);
_layout.addView(_seekBar);
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)
{
updateText();
}
@Override
public void onStartTrackingTouch(SeekBar seekBar)
{
}
@Override
public void onStopTrackingTouch(SeekBar seekBar)
{
}
@Override
protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue)
{
int value;
if (restorePersistedValue)
{
value = getPersistedInt(_min);
}
else
{
value = (Integer)defaultValue;
persistInt(value);
}
_seekBar.setProgress(value - _min);
updateText();
}
@Override
protected Object onGetDefaultValue(TypedArray a, int index)
{
return (a.getInt(index, _min));
}
@Override
protected void onDialogClosed(boolean positiveResult)
{
if (positiveResult)
persistInt(_seekBar.getProgress() + _min);
else
_seekBar.setProgress(getPersistedInt(_min) - _min);
updateText();
}
protected View onCreateDialogView()
{
ViewGroup parent = (ViewGroup)_layout.getParent();
if (parent != null)
parent.removeView(_layout);
return (_layout);
}
private void updateText()
{
String f = String.format(_initialSummary, _seekBar.getProgress() + _min);
_textView.setText(f);
setSummary(f);
}
}

View file

@ -0,0 +1,302 @@
package juloo.keyboard2.prefs;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ArrayAdapter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import juloo.keyboard2.*;
import org.json.JSONException;
import org.json.JSONObject;
public class LayoutsPreference extends ListGroupPreference<LayoutsPreference.Layout>
{
static final String KEY = "layouts";
static final List<Layout> DEFAULT =
Collections.singletonList((Layout)new SystemLayout());
static final ListGroupPreference.Serializer<Layout> SERIALIZER =
new Serializer();
/** Text displayed for each layout in the dialog list. */
String[] _layout_display_names;
public LayoutsPreference(Context ctx, AttributeSet attrs)
{
super(ctx, attrs);
setKey(KEY);
Resources res = ctx.getResources();
_layout_display_names = res.getStringArray(R.array.pref_layout_entries);
}
/** Obtained from [res/values/layouts.xml]. */
static List<String> _unsafe_layout_ids_str = null;
static TypedArray _unsafe_layout_ids_res = null;
/** Layout internal names. Contains "system" and "custom". */
public static List<String> get_layout_names(Resources res)
{
if (_unsafe_layout_ids_str == null)
_unsafe_layout_ids_str = Arrays.asList(
res.getStringArray(R.array.pref_layout_values));
return _unsafe_layout_ids_str;
}
/** Layout resource id for a layout name. [-1] if not found. */
public static int layout_id_of_name(Resources res, String name)
{
if (_unsafe_layout_ids_res == null)
_unsafe_layout_ids_res = res.obtainTypedArray(R.array.layout_ids);
int i = get_layout_names(res).indexOf(name);
if (i >= 0)
return _unsafe_layout_ids_res.getResourceId(i, 0);
return -1;
}
/** [null] for the "system" layout. */
public static List<KeyboardData> load_from_preferences(Resources res, SharedPreferences prefs)
{
List<KeyboardData> layouts = new ArrayList<KeyboardData>();
for (Layout l : load_from_preferences(KEY, prefs, DEFAULT, SERIALIZER))
{
if (l instanceof NamedLayout)
layouts.add(layout_of_string(res, ((NamedLayout)l).name));
else if (l instanceof CustomLayout)
layouts.add(((CustomLayout)l).parsed);
else // instanceof SystemLayout
layouts.add(null);
}
return layouts;
}
/** Does not call [prefs.commit()]. */
public static void save_to_preferences(SharedPreferences.Editor prefs, List<Layout> items)
{
save_to_preferences(KEY, prefs, items, SERIALIZER);
}
public static KeyboardData layout_of_string(Resources res, String name)
{
int id = layout_id_of_name(res, name);
if (id > 0)
return KeyboardData.load(res, id);
// Might happen when the app is downgraded, return the system layout.
return null;
}
@Override
protected void onSetInitialValue(boolean restoreValue, Object defaultValue)
{
super.onSetInitialValue(restoreValue, defaultValue);
if (_values.size() == 0)
set_values(new ArrayList<Layout>(DEFAULT), false);
}
String label_of_layout(Layout l)
{
if (l instanceof NamedLayout)
{
String lname = ((NamedLayout)l).name;
int value_i = get_layout_names(getContext().getResources()).indexOf(lname);
return value_i < 0 ? lname : _layout_display_names[value_i];
}
else if (l instanceof CustomLayout)
{
// Use the layout's name if possible
CustomLayout cl = (CustomLayout)l;
if (cl.parsed != null && cl.parsed.name != null
&& !cl.parsed.name.equals(""))
return cl.parsed.name;
else
return getContext().getString(R.string.pref_layout_e_custom);
}
else // instanceof SystemLayout
return getContext().getString(R.string.pref_layout_e_system);
}
@Override
String label_of_value(Layout value, int i)
{
return getContext().getString(R.string.pref_layouts_item, i + 1,
label_of_layout(value));
}
@Override
AddButton on_attach_add_button(AddButton prev_btn)
{
if (prev_btn == null)
return new LayoutsAddButton(getContext());
return prev_btn;
}
@Override
boolean should_allow_remove_item(Layout value)
{
return (_values.size() > 1 && !(value instanceof CustomLayout));
}
@Override
ListGroupPreference.Serializer<Layout> get_serializer() { return SERIALIZER; }
void select_dialog(final SelectionCallback callback)
{
ArrayAdapter layouts = new ArrayAdapter(getContext(), android.R.layout.simple_list_item_1, _layout_display_names);
new AlertDialog.Builder(getContext())
.setView(View.inflate(getContext(), R.layout.dialog_edit_text, null))
.setAdapter(layouts, new DialogInterface.OnClickListener(){
public void onClick(DialogInterface _dialog, int which)
{
String name = get_layout_names(getContext().getResources()).get(which);
switch (name)
{
case "system":
callback.select(new SystemLayout());
break;
case "custom":
select_custom(callback, read_initial_custom_layout());
break;
default:
callback.select(new NamedLayout(name));
break;
}
}
})
.show();
}
/** Dialog for specifying a custom layout. [initial_text] is the layout
description when modifying a layout. */
void select_custom(final SelectionCallback callback, String initial_text)
{
boolean allow_remove = callback.allow_remove() && _values.size() > 1;
CustomLayoutEditDialog.show(getContext(), initial_text, allow_remove,
new CustomLayoutEditDialog.Callback()
{
public void select(String text)
{
if (text == null)
callback.select(null);
else
callback.select(CustomLayout.parse(text));
}
public String validate(String text)
{
try
{
KeyboardData.load_string_exn(text);
return null; // Validation passed
}
catch (Exception e)
{
return e.getMessage();
}
}
});
}
/** Called when modifying a layout. Custom layouts behave differently. */
@Override
void select(final SelectionCallback callback, Layout prev_layout)
{
if (prev_layout != null && prev_layout instanceof CustomLayout)
select_custom(callback, ((CustomLayout)prev_layout).xml);
else
select_dialog(callback);
}
/** The initial text for the custom layout entry box. The qwerty_us layout is
a good default and contains a bit of documentation. */
String read_initial_custom_layout()
{
try
{
Resources res = getContext().getResources();
return Utils.read_all_utf8(res.openRawResource(R.raw.latn_qwerty_us));
}
catch (Exception _e)
{
return "";
}
}
class LayoutsAddButton extends AddButton
{
public LayoutsAddButton(Context ctx)
{
super(ctx);
setLayoutResource(R.layout.pref_layouts_add_btn);
}
}
/** A layout selected by the user. The only implementations are
[NamedLayout], [SystemLayout] and [CustomLayout]. */
public interface Layout {}
public static final class SystemLayout implements Layout
{
public SystemLayout() {}
}
/** The name of a layout defined in [srcs/layouts]. */
public static final class NamedLayout implements Layout
{
public final String name;
public NamedLayout(String n) { name = n; }
}
/** The XML description of a custom layout. */
public static final class CustomLayout implements Layout
{
public final String xml;
/** Might be null. */
public final KeyboardData parsed;
public CustomLayout(String xml_, KeyboardData k) { xml = xml_; parsed = k; }
public static CustomLayout parse(String xml)
{
KeyboardData parsed = null;
try { parsed = KeyboardData.load_string_exn(xml); }
catch (Exception e) {}
return new CustomLayout(xml, parsed);
}
}
/** Named layouts are serialized to strings and custom layouts to JSON
objects with a [kind] field. */
public static class Serializer implements ListGroupPreference.Serializer<Layout>
{
public Layout load_item(Object obj) throws JSONException
{
if (obj instanceof String)
{
String name = (String)obj;
if (name.equals("system"))
return new SystemLayout();
return new NamedLayout(name);
}
JSONObject obj_ = (JSONObject)obj;
switch (obj_.getString("kind"))
{
case "custom": return CustomLayout.parse(obj_.getString("xml"));
case "system": default: return new SystemLayout();
}
}
public Object save_item(Layout v) throws JSONException
{
if (v instanceof NamedLayout)
return ((NamedLayout)v).name;
if (v instanceof CustomLayout)
return new JSONObject().put("kind", "custom")
.put("xml", ((CustomLayout)v).xml);
return new JSONObject().put("kind", "system");
}
}
}

View file

@ -0,0 +1,289 @@
package juloo.keyboard2.prefs;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.Preference;
import android.preference.PreferenceGroup;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
import juloo.keyboard2.*;
import org.json.JSONArray;
import org.json.JSONException;
/** A list of preferences where the users can add items to the end and modify
and remove items. Backed by a string list. Implement user selection in
[select()]. */
public abstract class ListGroupPreference<E> extends PreferenceGroup
{
boolean _attached = false;
List<E> _values;
/** The "add" button currently displayed. */
AddButton _add_button = null;
public ListGroupPreference(Context context, AttributeSet attrs)
{
super(context, attrs);
setOrderingAsAdded(true);
setLayoutResource(R.layout.pref_listgroup_group);
_values = new ArrayList<E>();
}
/** Overrideable */
/** The label to display on the item for a given value. */
abstract String label_of_value(E value, int i);
/** Called every time the list changes and allows to change the "Add" button
appearance.
[prev_btn] is the previously attached button, might be null. */
AddButton on_attach_add_button(AddButton prev_btn)
{
if (prev_btn == null)
return new AddButton(getContext());
return prev_btn;
}
/** Called every time the list changes and allows to disable the "Remove"
buttons on every items. Might be used to enforce a minimum number of
items. */
boolean should_allow_remove_item(E _value)
{
return true;
}
/** Called when an item is added or modified. [old_value] is [null] if the
item is being added. */
abstract void select(SelectionCallback<E> callback, E old_value);
/** A separate class is used as the same serializer must be used in the
static context. See [Serializer] below. */
abstract Serializer<E> get_serializer();
/** Load/save utils */
/** Read a value saved by preference from a [SharedPreferences] object.
[serializer] must be the same that is returned by [get_serializer()].
Returns [null] on error. */
static <E> List<E> load_from_preferences(String key,
SharedPreferences prefs, List<E> def, Serializer<E> serializer)
{
String s = prefs.getString(key, null);
return (s != null) ? load_from_string(s, serializer) : def;
}
/** Save items into the preferences. Does not call [prefs.commit()]. */
static <E> void save_to_preferences(String key, SharedPreferences.Editor prefs, List<E> items, Serializer<E> serializer)
{
prefs.putString(key, save_to_string(items, serializer));
}
/** Decode a list of string previously encoded with [save_to_string]. Returns
[null] on error. */
static <E> List<E> load_from_string(String inp, Serializer<E> serializer)
{
try
{
List<E> l = new ArrayList<E>();
JSONArray arr = new JSONArray(inp);
for (int i = 0; i < arr.length(); i++)
l.add(serializer.load_item(arr.get(i)));
return l;
}
catch (JSONException e)
{
Logs.exn("load_from_string", e);
return null;
}
}
/** Encode a list of string so it can be passed to
[Preference.persistString()]. Decode with [load_from_string]. */
static <E> String save_to_string(List<E> items, Serializer<E> serializer)
{
List<Object> serialized_items = new ArrayList<Object>();
for (E it : items)
{
try
{
serialized_items.add(serializer.save_item(it));
}
catch (JSONException e)
{
Logs.exn("save_to_string", e);
}
}
return (new JSONArray(serialized_items)).toString();
}
/** Protected API */
/** Set the values. If [persist] is [true], persist into the store. */
void set_values(List<E> vs, boolean persist)
{
_values = vs;
reattach();
if (persist)
persistString(save_to_string(vs, get_serializer()));
}
void add_item(E v)
{
_values.add(v);
set_values(_values, true);
}
void change_item(int i, E v)
{
_values.set(i, v);
set_values(_values, true);
}
void remove_item(int i)
{
_values.remove(i);
set_values(_values, true);
}
/** Internal */
@Override
protected void onSetInitialValue(boolean restoreValue, Object defaultValue)
{
String input = (restoreValue) ? getPersistedString(null) : (String)defaultValue;
if (input != null)
{
List<E> values = load_from_string(input, get_serializer());
if (values != null)
set_values(values, false);
}
}
@Override
protected void onAttachedToActivity()
{
super.onAttachedToActivity();
if (_attached)
return;
_attached = true;
reattach();
}
void reattach()
{
if (!_attached)
return;
removeAll();
int i = 0;
for (E v : _values)
{
addPreference(this.new Item(getContext(), i, v));
i++;
}
_add_button = on_attach_add_button(_add_button);
_add_button.setOrder(Preference.DEFAULT_ORDER);
addPreference(_add_button);
}
class Item extends Preference
{
final E _value;
final int _index;
public Item(Context ctx, int index, E value)
{
super(ctx);
_value = value;
_index = index;
setPersistent(false);
setTitle(label_of_value(value, index));
if (should_allow_remove_item(value))
setWidgetLayoutResource(R.layout.pref_listgroup_item_widget);
}
@Override
protected View onCreateView(ViewGroup parent)
{
View v = super.onCreateView(parent);
View remove_btn = v.findViewById(R.id.pref_listgroup_remove_btn);
if (remove_btn != null)
remove_btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View _v)
{
remove_item(_index);
}
});
v.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View _v)
{
select(new SelectionCallback<E>() {
public void select(E value)
{
if (value == null)
remove_item(_index);
else
change_item(_index, value);
}
public boolean allow_remove() { return true; }
}, _value);
}
});
return v;
}
}
class AddButton extends Preference
{
public AddButton(Context ctx)
{
super(ctx);
setPersistent(false);
setLayoutResource(R.layout.pref_listgroup_add_btn);
}
@Override
protected void onClick()
{
select(new SelectionCallback<E>() {
public void select(E value)
{
add_item(value);
}
public boolean allow_remove() { return false; }
}, null);
}
}
public interface SelectionCallback<E>
{
public void select(E value);
/** If this method returns [true], [null] might be passed to [select] to
remove the item. */
public boolean allow_remove();
}
/** Methods for serializing and deserializing abstract items.
[StringSerializer] is an implementation. */
public interface Serializer<E>
{
/** [obj] is an object returned by [save_item()]. */
E load_item(Object obj) throws JSONException;
/** Serialize an item into JSON. Might return an object that can be inserted
in a [JSONArray]. */
Object save_item(E v) throws JSONException;
}
public static class StringSerializer implements Serializer<String>
{
public String load_item(Object obj) { return (String)obj; }
public Object save_item(String v) { return v; }
}
}

View file

@ -0,0 +1,131 @@
package juloo.keyboard2.prefs;
import android.content.Context;
import android.content.res.TypedArray;
import android.preference.DialogPreference;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.SeekBar;
/*
** SideBarPreference
** -
** Open a dialog showing a seekbar
** -
** xml attrs:
** android:defaultValue Default value (float)
** min min value (float)
** max max value (float)
** -
** Summary field allow to show the current value using %f or %s flag
*/
public class SlideBarPreference extends DialogPreference
implements SeekBar.OnSeekBarChangeListener
{
private static final int STEPS = 100;
private LinearLayout _layout;
private TextView _textView;
private SeekBar _seekBar;
private float _min;
private float _max;
private float _value;
private String _initialSummary;
public SlideBarPreference(Context context, AttributeSet attrs)
{
super(context, attrs);
_initialSummary = getSummary().toString();
_textView = new TextView(context);
_textView.setPadding(48, 40, 48, 40);
_seekBar = new SeekBar(context);
_seekBar.setOnSeekBarChangeListener(this);
_seekBar.setMax(STEPS);
_min = float_of_string(attrs.getAttributeValue(null, "min"));
_value = _min;
_max = Math.max(1f, float_of_string(attrs.getAttributeValue(null, "max")));
_layout = new LinearLayout(getContext());
_layout.setOrientation(LinearLayout.VERTICAL);
_layout.addView(_textView);
_layout.addView(_seekBar);
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)
{
_value = Math.round(progress * (_max - _min)) / (float)STEPS + _min;
updateText();
}
@Override
public void onStartTrackingTouch(SeekBar seekBar)
{
}
@Override
public void onStopTrackingTouch(SeekBar seekBar)
{
}
@Override
protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue)
{
if (restorePersistedValue)
{
_value = getPersistedFloat(_min);
}
else
{
_value = (Float)defaultValue;
persistFloat(_value);
}
_seekBar.setProgress((int)((_value - _min) * STEPS / (_max - _min)));
updateText();
}
@Override
protected Object onGetDefaultValue(TypedArray a, int index)
{
return (a.getFloat(index, _min));
}
@Override
protected void onDialogClosed(boolean positiveResult)
{
if (positiveResult)
persistFloat(_value);
else
_seekBar.setProgress((int)((getPersistedFloat(_min) - _min) * STEPS / (_max - _min)));
updateText();
}
protected View onCreateDialogView()
{
ViewGroup parent = (ViewGroup)_layout.getParent();
if (parent != null)
parent.removeView(_layout);
return (_layout);
}
private void updateText()
{
String f = String.format(_initialSummary, _value);
_textView.setText(f);
setSummary(f);
}
private static float float_of_string(String str)
{
if (str == null)
return (0f);
return (Float.parseFloat(str));
}
}

134
srcs/layouts/LICENSE Normal file
View file

@ -0,0 +1,134 @@
Layout definitions are licensed differently from the rest of the application
source code to allow use in other projects related or unrelated to Unexpected
Keyboard.
The following license applies to all the files in the srcs/layouts directory
whose name ends in .xml with the exception of:
- latn_neo2.xml
- latn_qwertz.xml
Files listed as exceptions are licensed under the same license as the rest of
the project and might contain copyright notices.
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.

42
srcs/layouts/arab_alt.xml Normal file
View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<keyboard name="Arabic Alt" script="arabic" numpad_script="hindu-arabic">
<row>
<key key0="ض" key2="١" key3="`" key4="loc esc"/>
<key key0="ص" key2="٢" key3="\@"/>
<key key0="ث" key2="٣" key3="\#"/>
<key key0="ق" key2="٤" key3="$"/>
<key key0="ف" key2="٥" key3="%"/>
<key key0="غ" key2="٦" key3="^"/>
<key key0="ع" key2="٧" key3="&amp;"/>
<key key0="ه" key2="٨" key3="*"/>
<key key0="خ" key2="٩" key3="("/>
<key key0="ح" key2="٠" key3=")"/>
<key key0="ج"/>
</row>
<row>
<key key0="ش" key4="loc tab"/>
<key key0="س"/>
<key key0="ي"/>
<key key0="ب"/>
<key key0="ل"/>
<key key0="ا" key1="أ"/>
<key key0="ت"/>
<key key0="ن"/>
<key key0="م"/>
<key key0="ك"/>
<key key0="ط"/>
</row>
<row>
<key key0="ذ"/>
<key key0="ء"/>
<key key0="ؤ" key1="{"/>
<key key0="ر" key1="}"/>
<key key0="ى" key1="ئ"/>
<key key0="ة"/>
<key key0="و" key3=","/>
<key key0="ز" key3="."/>
<key key0="ظ" key3="&#1567;"/>
<key key0="د"/>
<key width="1.0" key0="backspace" key2="delete"/>
</row>
</keyboard>

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<keyboard name="Talysh (تالشی همواج)" script="persian">
<row>
<key key0="ض" key2="۱"/>
<key key0="ص" key1="~" key2="۲" key3="\@"/>
<key key0="ث" key1="!" key2="۳" key3="\#"/>
<key key0="ق" key1="﷼" key2="۴" key3="$"/>
<key key0="ف" key2="۵" key3="٪"/>
<key key0="غ" key1="،" key2="۶" key3="^"/>
<key key0="ع" key2="۷" key3="&amp;"/>
<key key0="ه" key2="۸" key3="*"/>
<key key0="خ" key2="۹" key3="(" key4=")"/>
<key key0="ح" key2="۰"/>
<key key0="ج"/>
</row>
<row>
<key key0="ش" key1="َ" key2="loc tab"/>
<key key0="س" key1="ُ"/>
<key key0="ی" key4="ئ"/>
<key key0="ب" key1="ّ"/>
<key key0="ل" key3="ِ" key4="ﻻ"/>
<key key0="ا" key1="آ" key2="-" key3="إ" key4="أ"/>
<key key0="ت" key1="_" key2="+"/>
<key key0="ن" key4="ۨ"/>
<key key0="م"/>
<key key0="ک"/>
<key key0="گ" key1="ء"/>
</row>
<row>
<key key0="ظ"/>
<key key0="ط"/>
<key key0="ژ" key2="«"/>
<key key0="ز" key1="ْ" key2="»"/>
<key key0="ر" key2="."/>
<key key0="د" key2="؛" key3=":" key4="ذ"/>
<key key0="پ" key2="؟"/>
<key key0="و" key1="ۋ" key4="ۊ"/>
<key key0="چ"/>
<key key0="ٚ" key1="ٛ"/>
<key key0="backspace" key2="delete"/>
</row>
</keyboard>

44
srcs/layouts/arab_pc.xml Normal file
View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<keyboard name="Arabic PC" script="arabic">
<row>
<key key0="ض" key1="&#1614;" key2="1" key3="`" key4="loc esc"/>
<key key0="ص" key1="&#1611;" key2="2" key3="\@"/>
<key key0="ث" key1="&#1615;" key2="3" key3="\#" key4="loc €"/>
<key key0="ق" key1="&#1612;" key2="4" key3="$" key4="loc £"/>
<key key0="ف" key1="&#1604;&#1573;" key2="5" key3="%"/>
<key key0="غ" key1="&#1573;" key2="6" key3="^"/>
<key key0="ع" key1="&#8216;" key2="7" key3="&amp;"/>
<key key0="ه" key1="&#0247;" key2="8" key3="*"/>
<key key0="خ" key1="&#0215;" key2="9" key3="("/>
<key key0="ح" key1="&#1563;" key2="0" key3=")"/>
<key key0="ج" key1="&gt;" key2="-" key3="_"/>
<key key0="د" key1="&lt;" key2="=" key3="ذ"/>
<!-- <key key0="ذ" key1="&#1617;" key3="\\" key4="|"/> -->
</row>
<row>
<key shift="0.5" key0="ش" key1="&#1616;" key4="loc tab"/>
<key key0="س" key1="&#1613;"/>
<key key0="ي" key1="["/>
<key key0="ب" key1="]"/>
<key key0="ل" key1="&#1604;&#1571;"/>
<key key0="ا" key1="أ"/>
<key key0="ت" key1="ـ"/>
<key key0="ن" key1="&#1548;"/>
<key key0="م" key1="/"/>
<key key0="ك" key1=":"/>
<key key0="ط" key1="&quot;"/>
</row>
<row>
<key shift="0.5" key0="ئ" key1="~"/>
<key key0="ء" key1="&#1618;"/>
<key key0="ؤ" key1="{"/>
<key key0="ر" key1="}"/>
<key key0="لا" key1="&#1604;&#1570;"/>
<key key0="ى" key1="&#1570;"/>
<key key0="ة" key1="&#8217;"/>
<key key0="و" key1=","/>
<key key0="ز" key1="."/>
<key key0="ظ" key1="&#1567;"/>
<key width="1.5" key0="backspace" key2="delete"/>
</row>
</keyboard>

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<keyboard name="Kurdish (کوردی) QWERTY" script="arabic" numpad_script="hindu-arabic">
<row>
<key key0="ق" key1="halfspace" key2="١" key3="loc esc"/>
<key key0="و" key1="وو" key2="٢" key3="\@" key4="ڡ" />
<key key0="ە" key1="ة" key2="٣" key3="\#" key4="ۉ" />
<key key0="ر" key1="ڕ" key2="٤" key3="$"/>
<key key0="ت" key1="ط" key2="٥" key3="٪"/>
<key key0="ی" key1="ي" key2="٦" key3="^"/>
<key key0="ێ" key1="ؽ" key2="٧" key3="&amp;"/>
<key key0="ئ" key1="ء" key2="٨" key3="*"/>
<key key0="ۆ" key1="ۊ" key2="٩" key4=")" key3="("/>
<key key0="پ" key1="ث" key2="٠"/>
</row>
<row>
<key key0="ا" key1="آ" key2="loc tab"/>
<key key0="س" key1="ص"/>
<key key0="ش" key1="ض"/>
<key key0="د" key1="ذ" key4="ۮ"/>
<key key0="ف" key1="ڤ" key2="-" key3="_"/>
<key key2="ه" key0="ھ" key3="ہ"/>
<key key0="ژ" key1="ـ" key4="}" key3="{"/>
<key key0="ل" key1="ڵ" key4="]" key3="["/>
<key key0="ک" key2="ك" key3="\\"/>
<key key0="گ" key2="غ" key3="/"/>
</row>
<row>
<key key0="ز" key1="ظ"/>
<key key0="خ"/>
<key key0="ج"/>
<key key0="چ"/>
<key key0="ح" key2="&#1567;" key3="!"/>
<key key0="ع" key1="ٔ" key4="ٕ" />
<key key0="ب" key1="ٮ" />
<key key0="ن" key2="&#1548;" key3="&#1563;"/>
<key key0="م" key2="." key3=":" />
<key key0="backspace" key2="delete"/>
</row>
</keyboard>

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<keyboard name="Central Kurdish (سۆرانی) Persian Layout" script="arabic" numpad_script="hindu-arabic">
<row>
<key key0="،" key7="esc" key4="delete" />
<key key0="." key1="&lt;" key4="&gt;" />
<key key0="ە" key1="(" key4=")" />
<key key0="ق" key7=":" key8="*" />
<key key0="ف" key7="؟" key8="+" />
<key key0="ۆ" key7="!" key8="ۊ" />
<key key0="ع" key7="غ" key8="_" />
<key key0="ھ" key7="٪" key8="ه" />
<key key0="خ" key3="[" key2="]" />
<key key0="ح" key3="{" key2="}" />
<key key0="ج" key3="backspace" key7="halfspace" />
</row>
<row>
<key key0="ش" key7="١" key8="ض" />
<key key0="س" key7="٢" key8="ص" />
<key key0="ی" key7="٣" key8="ؽ" />
<key key0="ب" key7="٤" key8="ٮ" />
<key key0="ل" key7="٥" key8="ڵ" />
<key key0="ا" key7="٦" key8="آ" />
<key key0="ت" key7="٧" key8="ط" />
<key key0="ن" key7="٨" key8="-" />
<key key0="م" key7="٩" key8="=" />
<key key0="ک" key7="٠" key8="ك" />
<key key0="گ" key7="tab" />
</row>
<row>
<key key0="ڕ" key7="&#1563;" />
<key key0="ژ" key7="ـ" />
<key key0="ز" key7="ظ" />
<key key0="ر" key7="ٔ" />
<key key0="ڤ" key7="ڡ" />
<key key0="د" key7="ذ" />
<key key0="پ" key7="ث" />
<key key0="و" key7="ۉ" />
<key key0="ێ" key7="ٕ" />
<key key0="ئ" key7="ء" />
<key key0="چ" />
</row>
</keyboard>

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<keyboard name="Arabic PC (Hindu numerals)" script="arabic" numpad_script="hindu-arabic">
<row>
<key key0="ض" key1="&#1614;" key2="١" key3="`" key4="loc esc"/>
<key key0="ص" key1="&#1611;" key2="٢" key3="\@"/>
<key key0="ث" key1="&#1615;" key2="٣" key3="\#" key4="loc €"/>
<key key0="ق" key1="&#1612;" key2="٤" key3="$" key4="loc £"/>
<key key0="ف" key1="&#1604;&#1573;" key2="٥" key3="%"/>
<key key0="غ" key1="&#1573;" key2="٦" key3="^"/>
<key key0="ع" key1="&#8216;" key2="٧" key3="&amp;"/>
<key key0="ه" key1="&#0247;" key2="٨" key3="*"/>
<key key0="خ" key1="&#0215;" key2="٩" key3="("/>
<key key0="ح" key1="&#1563;" key2="٠" key3=")"/>
<key key0="ج" key1="&gt;" key2="-" key3="_"/>
<key key0="د" key1="&lt;" key2="=" key3="ذ"/>
<!-- <key key0="ذ" key1="&#1617;" key3="\\" key4="|"/> -->
</row>
<row>
<key shift="0.5" key0="ش" key1="&#1616;" key4="loc tab"/>
<key key0="س" key1="&#1613;"/>
<key key0="ي" key1="["/>
<key key0="ب" key1="]"/>
<key key0="ل" key1="&#1604;&#1571;"/>
<key key0="ا" key1="أ"/>
<key key0="ت" key1="ـ"/>
<key key0="ن" key1="&#1548;"/>
<key key0="م" key1="/"/>
<key key0="ك" key1=":"/>
<key key0="ط" key1="&quot;"/>
</row>
<row>
<key shift="0.5" key0="ئ" key1="~"/>
<key key0="ء" key1="&#1618;"/>
<key key0="ؤ" key1="{"/>
<key key0="ر" key1="}"/>
<key key0="لا" key1="&#1604;&#1570;"/>
<key key0="ى" key1="&#1570;"/>
<key key0="ة" key1="&#8217;"/>
<key key0="و" key1=","/>
<key key0="ز" key1="."/>
<key key0="ظ" key1="&#1567;"/>
<key width="1.5" key0="backspace" key2="delete"/>
</row>
</keyboard>

View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<keyboard name="Persian PC" script="persian">
<row>
<key key0="ض" key2="۱" key4="loc esc"/>
<key key0="ص" key2="۲" key1="~" key3="\@"/>
<key key0="ث" key2="۳" key1="!" key3="\#"/>
<key key0="ق" key2="۴" key1="﷼" key3="$"/>
<key key0="ف" key2="۵" key3="٪"/>
<key key0="غ" key2="۶" key1="،" key3="^"/>
<key key0="ع" key2="۷" key3="&amp;"/>
<key key0="ه" key2="۸" key3="*"/>
<key key0="خ" key2="۹" key3="(" key4=")"/>
<key key0="ح" key2="۰"/>
<key key0="ج"/>
</row>
<row>
<key key0="ش" key2="loc tab"/>
<key key0="س"/>
<key key0="ی" key2="ئ"/>
<key key0="ب"/>
<key key0="ل"/>
<key key0="ا" key1="آ" key4="ء" key2="-" key3="_"/>
<key key0="ت" key1="halfspace" key2="+"/>
<key key0="ن"/>
<key key0="م"/>
<key key0="ک"/>
<key key0="گ"/>
</row>
<row>
<key shift="0.5" key0="ظ"/>
<key key0="ط"/>
<key key0="ز" key1="«" key2="»"/>
<key key0="ر" key1="ژ" key2="."/>
<key key0="ذ" key2=":"/>
<key key0="د" key2="؛"/>
<key key0="پ" key2="&#1567;"/>
<key key0="و"/>
<key key0="چ"/>
<key width="1.5" key0="backspace" key2="delete"/>
</row>
</keyboard>

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<keyboard name="Armenian" script="armenian">
<row>
<key key0="է" key2="1"/>
<key key0="թ" key2="2" key1="~" key3="\@"/>
<key key0="փ" key2="3" key1="!" key3="\#"/>
<key key0="ձ" key2="4" key3="$"/>
<key key0="ջ" key2="5" key3="%"/>
<key key0="ր" key2="6" key3="^"/>
<key key0="չ" key2="7" key3="&amp;"/>
<key key0="ճ" key2="8" key3="*"/>
<key key0="ժ" key2="9" key3="(" key4=")"/>
<key key0="ծ" key2="0"/>
</row>
<row>
<key key0="ք" key4="loc esc"/>
<key key0="ո"/>
<key key0="ե" key1="և"/>
<key key0="ռ"/>
<key key0="տ"/>
<key key0="ը"/>
<key key0="ւ"/>
<key key0="ի"/>
<key key0="օ"/>
<key key0="պ"/>
</row>
<row>
<key key0="ա" key1="loc tab" key2="`"/>
<key key0="ս"/>
<key key0="դ"/>
<key key0="ֆ"/>
<key key0="գ"/>
<key key0="հ" key3="_" key2="-"/>
<key key0="յ" key3="+" key2="="/>
<key key0="կ" key3="{" key4="}"/>
<key key0="լ" key3="[" key4="]"/>
<key key0="խ" key3="\\" key2="|"/>
</row>
<row>
<key key0="shift" key2="loc capslock"/>
<key key0="զ"/>
<key key0="ղ"/>
<key key0="ց"/>
<key key0="վ" key4="." key2="&lt;"/>
<key key0="բ" key4="," key2="&gt;"/>
<key key0="ն" key4="/" key2="\?"/>
<key key0="մ" key4=";" key2=":"/>
<key key0="շ" key4="&apos;" key2="&quot;"/>
<key key0="backspace" key2="delete"/>
</row>
</keyboard>

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<keyboard name="বাংলা (জাতীয়)" script="bengali">
<row>
<key key0="ঙ" key2="ং" key3="loc esc" key4="১"/>
<key key0="য" key2="য়" key3="¶" key4="২"/>
<key key0="ড" key2="ঢ" key3="π" key4="৩"/>
<key key0="প" key2="ফ" key3="√" key4=""/>
<key key0="ট" key2="ঠ" key3="^" key4="৫"/>
<key key0="চ" key2="ছ" key3="÷" key4="৬"/>
<key key0="জ" key2="ঝ" key3="×" key4=""/>
<key key0="হ" key2="ঞ" key3="=" key4="৮"/>
<key key0="গ" key2="ঘ" key3="+" key4="৯"/>
<key key0="ড়" key2="ঢ়" key3="-" key4=""/>
</row>
<row>
<key shift="0.5" key0="ৃ" key1="ৠ" key2="ঋ" key3="loc tab" key4="র্"/>
<key key0="ু" key1="ঊ" key2="উ" key3="~" key4="ূ"/>
<key key0="ি" key1="ঈ" key2="ই" key3="•" key4="ী"/>
<key key0="া" key1="ৄ" key2="আ" key3="°" key4="অ"/>
<key key0="্" key1="ৗ" key2="ঁ" key3="\\" key4="/"/>
<key key0="ব" key1="`" key2="ভ" key3="&lt;" key4="&gt;"/>
<key key0="ক" key1="|" key2="খ" key3="[" key4="]"/>
<key key0="ত" key1="ৎ" key2="থ" key3="{" key4="}"/>
<key key0="দ" key1="_" key2="ধ" key3="(" key4=")"/>
</row>
<row>
<key width="1.4" key0="shift" key2="loc capslock"/>
<key shift="0.1" key0="্র" key2="্য" key3="\#" key4="*"/>
<key key0="ো" key1="ৌ" key2="ও" key3="ঔ" key4="\@"/>
<key key0="ে" key1="ৈ" key2="এ" key3="ঐ" key4="%"/>
<key key0="র" key1="ঃ" key2="ল" key3=":" key4="&amp;"/>
<key key0="ন" key1="৳" key2="ণ" key3=";" key4="."/>
<key key0="স" key1="&quot;" key2="ষ" key3="!" key4=","/>
<key key0="ম" key1="'" key2="শ" key3="\?" key4="।"/>
<key shift="0.1" width="1.4" key0="backspace" key2="delete"/>
</row>
</keyboard>

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8" ?>
<keyboard name="বাংলা (প্রভাত)" script="bengali">
<row>
<key key0="দ" key1="ধ" key2="১" key3="!" key5="loc esc" />
<key key0="ূ" key1="ঊ" key2="২" key3="\@" />
<key key0="ী" key1="ঈ" key2="৩" key3="\#" />
<key key0="র" key1="ড়" key2="" key3="৳" />
<key key0="ট" key1="ঠ" key2="৫" key3="%" />
<key key0="এ" key1="ঐ" key2="৬" key3="^" />
<key key0="ু" key1="উ" key2="" key3="ঞ" />
<key key0="ি" key1="ই" key2="৮" key3="ৎ" />
<key key0="ও" key1="ঔ" key2="৯" key3="(" key4=")" />
<key key0="প" key1="ফ" key2="" key3="zwj" key4="~" />
</row>
<row>
<key shift="0.5" key0="া" key1="অ" key5="loc tab" />
<key key0="স" key1="ষ" />
<key key0="ড" key1="ঢ" />
<key key0="ত" key1="থ" />
<key key0="গ" key1="ঘ" key2="-" key3="_" />
<key key0="হ" key1="ঃ" key2="=" key3="+" />
<key key0="জ" key1="ঝ" key3="ে" key4="ৈ" />
<key key0="ক" key1="খ" key3="ো" key4="ৌ" />
<key key0="ল" key1="ং" key2="॥" key3="halfspace" />
</row>
<row>
<key width="1.5" key0="shift" />
<key key0="য়" key1="য" />
<key key0="শ" key1="ঢ়" />
<key key0="চ" key1="ছ" key2="ৃ" key3="," />
<key key0="আ" key1="ঋ" key2="ঁ" key3="।" />
<key key0="ব" key1="ভ" key2="\?" key3="্" />
<key key0="ন" key1="ণ" key2=":" key3=";" />
<key key0="ম" key1="ঙ" key2="&quot;" key3="'" />
<key width="1.5" key0="backspace" key2="delete" />
</row>
</keyboard>

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<keyboard name="ФЦУЖЭН (Монгол)" script="cyrillic">
<row>
<key key0="ф" key2="1" key4="loc esc"/>
<key key0="ц" key1="~" key2="2" key3="\@"/>
<key key0="у" key1="!" key2="3" key3="\#"/>
<key key0="ж" key2="4" key3="$"/>
<key key0="э" key2="5" key3="%"/>
<key key0="н" key2="6" key3="^"/>
<key key0="г" key2="7" key3="&amp;"/>
<key key0="ш" key1="щ" key2="8" key3="*"/>
<key key0="ү" key2="9" key3="(" key4=")"/>
<key key0="з" key2="0"/>
<key key0="к"/>
</row>
<row>
<key key0="й" key1="loc tab" key2="`"/>
<key key0="ы"/>
<key key0="б"/>
<key key0="ө"/>
<key key0="а"/>
<key key0="х"/>
<key key0="р" key2="-" key3="_"/>
<key key0="о" key2="=" key3="+"/>
<key key0="л" key3="{" key4="}"/>
<key key0="д" key3="[" key4="]"/>
<key key0="п" key2="|" key3="\\"/>
</row>
<row>
<key key0="shift" key2="loc capslock"/>
<key key0="я"/>
<key key0="ч"/>
<key key0="ё" key1="е"/>
<key key0="с"/>
<key key0="м" key2="&lt;" key3="."/>
<key key0="и" key2=">" key3=","/>
<key key0="т" key1="₮" key2="\?" key3="/"/>
<key key0="ь" key1="ъ" key2=":" key3=";"/>
<key key0="в" key1="ю" key2="&quot;" key3="'"/>
<key key0="backspace" key2="delete"/>
</row>
</keyboard>

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<keyboard name="ЈЦУКЕН (Всисловѣнск)" script="cyrillic">
<row>
<key key0="ј" key1="" key2="1" key3="!" key4="combining_payerok" />
<key key0="ц" key1="ѕ" key2="2" key3="&quot;" key4="\@" />
<key key0="у" key1="ꙋ" key2="3" key4="combining_acute" />
<key key0="к" key1="ѯ" key2="4" key3="$" key4="combining_palatalization" />
<key key0="е" key1="є" key2="5" key3="%" key4="\#" />
<key key0="н" key1="њ" key2="6" key3="\?" />
<key key0="г" key1="ґ" key2="7" key3="(" key4="&amp;" />
<key key0="ш" key1="щ" key2="8" key3=")" key4="«" />
<key key0="і" key1="ї" key2="9" key3=":" key4="»" />
<key key0="з" key1="ꙁ" key2="0" key3="{" key4="}" />
<key key0="х" key1="ѹ" key2="ъ" key3="[" key4="]" />
</row>
<row>
<key key0="ф" key1="ѳ" key2="`" key3="ꙟ" key4="combining_vertical_tilde" />
<key key0="ы" key1="э" key2="ё" key3="ꙏ" key4="combining_slavonic_dasia" />
<key key0="в" key1="ѵ" key2="ѷ" key3="ў" key4="combining_slavonic_psili" />
<key key0="а" key1="ѻ" key2="ѐ" key3="ѥ" key4="combining_circonflexe" />
<key key0="п" key1="ѱ" key2="ѓ" key3="ꙓ" key4="combining_trema" />
<key key0="р" key1="ѽ" key2="ꙅ" key3="ꙑ" key4="combining_pokrytie" />
<key key0="о" key1="ѡ" key2="ꙍ" key3="ꙕ" key4="ѫ" />
<key key0="л" key1="љ" key3="ꙃ" key4="₽" />
<key key0="д" key1="ꙉ" key2="-" key3="_" key4="҂" />
<key key0="ж" key1="җ" key2="=" key3="+" key4="ҩ" />
<key key0="ѣ" key1="ԑ" key2="/" key3="\\" key4="|" />
</row>
<row>
<key width="1.18" key0="shift" key2="loc capslock" key3="loc tab" key4="loc esc"
/>
<key width="0.96" key0="я" key1="ꙗ" key2="ꙝ" key4="combining_titlo" />
<key width="0.96" key0="ч" key1="ћ" key2="ќ" key4="combining_grave" />
<key width="0.96" key0="с" key1="џ" key2="꙾" key3="ꙿ" key4="combining_kavyka" />
<key width="0.96" key0="м" key1="ꙙ" key2="ѭ" key3="*" key4="combining_vzmet" />
<key width="0.96" key0="и" key1="й" key2="ѝ" key3="." />
<key width="0.96" key0="т" key1="ѿ" key2=";" key3="," />
<key width="0.96" key0="ь" key1="ѧ" key2="&lt;" key3="⁙" />
<key width="0.96" key0="б" key1="ђ" key2="&gt;" key3="⁘" />
<key width="0.96" key0="ю" key1="ꙛ" key2="·" key3="⁖"/>
<key width="1.18" key0="backspace" key2="delete"/>
</row>
</keyboard>

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<keyboard name="ЙЦУКЕН (Қазақша)" script="cyrillic">
<row>
<key key0="ё"/>
<key key0="ә"/>
<key key0="і"/>
<key key0="ң"/>
<key key0="ғ"/>
<key key0="ү"/>
<key key0="ұ"/>
<key key0="қ"/>
<key key0="ө"/>
<key key0="һ"/>
<key key0="ъ"/>
</row>
<row>
<key key0="й" key2="1" key4="loc esc"/>
<key key0="ц" key1="loc ї" key2="2" key3="\@" key4="~"/>
<key key0="у" key1="loc ў" key2="3" key3="\#" key4="!"/>
<key key0="к" key2="4" key3="$"/>
<key key0="е" key2="5" key3="%"/>
<key key0="н" key1="loc є" key2="6" key3="^"/>
<key key0="г" key1="loc ґ" key2="7" key3="&amp;"/>
<key key0="ш" key2="8" key3="*"/>
<key key0="щ" key2="9" key3="(" key4=")"/>
<key key0="з" key2="0" key3="{" key4="}"/>
<key key0="х" key3="[" key4="]"/>
</row>
<row>
<key key0="ф" key1="loc tab" key2="`"/>
<key key0="ы"/>
<key key0="в"/>
<key key0="а"/>
<key key0="п"/>
<key key0="р"/>
<key key0="о"/>
<key key0="л" key1="₽"/>
<key key0="д" key2="-" key3="_"/>
<key key0="ж" key2="=" key3="+"/>
<key key0="э" key2="|" key3="\\"/>
</row>
<row>
<key key0="shift" key2="loc capslock"/>
<key key0="я"/>
<key key0="ч"/>
<key key0="с"/>
<key key0="м"/>
<key key0="и" key1="loc і" key2="&lt;" key3="."/>
<key key0="т" key2="&gt;" key3=","/>
<key key0="ь" key2="\?" key3="/"/>
<key key0="б" key2=":" key3=";"/>
<key key0="ю" key2="&quot;" key3="'"/>
<key key0="backspace" key2="delete"/>
</row>
</keyboard>

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<keyboard name="ЙЦУКЕН (Русский)" script="cyrillic">
<row>
<key key0="й" key2="1" key4="loc esc"/>
<key key0="ц" key1="loc ї" key2="2" key3="\@" key4="~"/>
<key key0="у" key1="loc ў" key2="3" key3="\#" key4="!"/>
<key key0="к" key2="4" key3="$"/>
<key key0="е" key1="ё" key2="5" key3="%"/>
<key key0="н" key1="loc є" key2="6" key3="^"/>
<key key0="г" key1="loc ґ" key2="7" key3="&amp;"/>
<key key0="ш" key2="8" key3="*"/>
<key key0="щ" key2="9" key3="(" key4=")"/>
<key key0="з" key2="0" key3="{" key4="}"/>
<key key0="х" key3="[" key4="]"/>
</row>
<row>
<key key0="ф" key1="loc tab" key2="`"/>
<key key0="ы"/>
<key key0="в"/>
<key key0="а"/>
<key key0="п"/>
<key key0="р"/>
<key key0="о"/>
<key key0="л" key1="₽"/>
<key key0="д" key2="-" key3="_"/>
<key key0="ж" key2="=" key3="+"/>
<key key0="э" key2="|" key3="\\"/>
</row>
<row scale="11">
<key width="1.22" key0="shift" key2="loc capslock"/>
<key key0="я"/>
<key key0="ч"/>
<key key0="с"/>
<key key0="м"/>
<key key0="и" key1="loc і" key2="&lt;" key3="."/>
<key key0="т" key2="&gt;" key3=","/>
<key key0="ь" key1="ъ" key2="\?" key3="/"/>
<key key0="б" key2=":" key3=";"/>
<key key0="ю" key2="&quot;" key3="'"/>
<key width="1.22" key0="backspace" key2="delete"/>
</row>
</keyboard>

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<keyboard name="ЙЦУКЕН (Українська)" script="cyrillic">
<row>
<key shift="0.1 " key0="й" key2="1" key4="loc esc"/>
<key key0="ц" key1="~" key2="2" key3="\@"/>
<key key0="у" key1="!" key2="3" key3="\#" key4="loc €"/>
<key key0="к" key2="4" key3="$"/>
<key key0="е" key2="5" key3="%"/>
<key key0="н" key2="6" key3="^"/>
<key key0="г" key1="ґ" key2="7" key3="&amp;"/>
<key key0="ш" key2="8" key3="*"/>
<key key0="щ" key2="9" key3="(" key4=")"/>
<key key0="з" key2="0"/>
<key key0="х"/>
</row>
<row>
<key shift="0.1" key0="ф" key1="loc tab" key2="`"/>
<key key0="і" key2="ї"/>
<key key0="в" />
<key key0="а" />
<key key0="п" />
<key key0="р" />
<key key0="о" key2="-" key3="_"/>
<key key0="л" key2="=" key3="+"/>
<key key0="д" key4="}" key3="{"/>
<key key0="ж" key3="[" key4="]"/>
<key key0="є" key2="|" key3="\\"/>
</row>
<row>
<key width="1.1" key0="shift" key2="loc capslock"/>
<key key0="я"/>
<key key0="ч" />
<key key0="с" />
<key key0="м" key2="&lt;" key3="."/>
<key key0="и" key2="&gt;" key3=","/>
<key key0="т" key2="\?" key3="/"/>
<key key0="ь" key2=":" key3=";"/>
<key key0="б" key2="&quot;" key3="'"/>
<key key0="ю" key1="«" key2="»"/>
<key width="1.1" key0="backspace" key2="delete"/>
</row>
</keyboard>

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<keyboard name="ЉЊЕРТЅ (Македонски)" script="cyrillic">
<row scale="11">
<key key0="љ" key2="1" key4="esc"/>
<key key0="њ" key1="~" key2="2" key3="\@"/>
<key key0="е" key1="!" key2="3" key3="\#" key4="ѐ"/>
<key key0="р" key2="4" key3="$"/>
<key key0="т" key2="5" key3="%"/>
<key key0="ѕ" key2="6" key3="^"/>
<key key0="у" key2="7" key3="&amp;"/>
<key key0="и" key2="8" key3="*" key4="ѝ"/>
<key key0="о" key2="9" key3="(" key4=")"/>
<key key0="п" key2="0" key3="[" key4="]"/>
<key key0="ш" key2="€" key3="{" key4="}"/>
</row>
<row scale="11">
<key key0="а" key1="tab"/>
<key key0="с"/>
<key key0="д"/>
<key key0="ф"/>
<key key0="г"/>
<key key0="х"/>
<key key0="ј"/>
<key key0="к"/>
<key key0="л" key2="|" key3="\\"/>
<key key0="ч" key2="-" key3="_"/>
<key key0="ќ" key2="=" key3="+"/>
</row>
<row scale="11">
<key width="1.5" key0="shift" key2="loc capslock"/>
<key key0="з"/>
<key key0="џ"/>
<key key0="ц"/>
<key key0="в" key2="&lt;" key3="."/>
<key key0="б" key2="&gt;" key3=","/>
<key key0="н" key2="\?" key3="/"/>
<key key0="м" key2=":" key3=";"/>
<key key0="ѓ" key2="`" key3="'"/>
<key key0="ж" key2="“" key3="„"/>
<key width="1.5" key0="backspace" key2="delete"/>
</row>
</keyboard>

View file

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<keyboard name="ЉЊЕРТЗ (Српски)" script="cyrillic">
<modmap>
<fn a="а" b="а̂" />
<fn a="е" b="е̂" />
<fn a="и" b="и̂" />
<fn a="о" b="о̂" />
<fn a="у" b="у̂" />
<fn a="cursor_left" b="home" />
<fn a="cursor_right" b="end" />
<ctrl a="љ" b="љ:q" />
<ctrl a="њ" b="њ:w" />
<ctrl a="е" b="е:e" />
<ctrl a="р" b="р:r" />
<ctrl a="т" b="т:t" />
<ctrl a="ж" b="ж:y" />
<ctrl a="у" b="у:u" />
<ctrl a="и" b="и:i" />
<ctrl a="о" b="о:o" />
<ctrl a="п" b="п:p" />
<ctrl a="а" b="а:a" />
<ctrl a="с" b="с:s" />
<ctrl a="д" b="д:d" />
<ctrl a="ф" b="ф:f" />
<ctrl a="г" b="г:g" />
<ctrl a="х" b="х:h" />
<ctrl a="ј" b="ј:j" />
<ctrl a="к" b="к:k" />
<ctrl a="л" b="л:l" />
<ctrl a="з" b="з:z" />
<ctrl a="џ" b="џ:x" />
<ctrl a="ц" b="ц:c" />
<ctrl a="в" b="в:v" />
<ctrl a="б" b="б:b" />
<ctrl a="н" b="н:n" />
<ctrl a="м" b="м:m" />
</modmap>
<row>
<key key0="љ" ne="1" se="loc esc"/>
<key key0="њ" nw="~" ne="2" sw="\@"/>
<key key0="е" ne="3" sw="\#" se="€"/>
<key key0="р" ne="4" sw="$" se="loc £"/>
<key key0="т" ne="5" sw="%"/>
<key key0="з" ne="6" sw="^"/>
<key key0="у" ne="7" sw="&amp;"/>
<key key0="и" ne="8" sw="*"/>
<key key0="о" ne="9" sw="(" se=")"/>
<key key0="п" ne="0" sw="[" se="]"/>
<key key0="ш"/>
</row>
<row>
<key key0="а" nw="loc tab" ne="loc selectAll"/>
<key key0="с" nw="loc §" ne="loc shareText"/>
<key key0="д"/>
<key key0="ф"/>
<key key0="г"/>
<key key0="х"/>
<key key0="ј" ne="loc accent_circonflexe" sw="{" se="}"/>
<key key0="к" ne="-" sw="_"/>
<key key0="л" ne="=" sw="+"/>
<key key0="ч" nw="'" ne="&quot;" sw="\\"/>
<key key0="ћ" ne="`" sw="|"/>
</row>
<row>
<key width="1.5" key0="shift" nw="loc superscript" ne="loc capslock" sw="loc subscript"/>
<key key0="ж" nw="loc undo" ne="loc redo" sw="&lt;" se="&gt;"/>
<key key0="џ" ne="loc cut"/>
<key key0="ц" ne="loc copy"/>
<key key0="в" ne="loc paste"/>
<key key0="б" ne="loc pasteAsPlainText"/>
<key key0="н" nw="!" ne="\?" sw="/"/>
<key key0="м" ne=";" sw=","/>
<key key0="ђ" ne=":" sw="."/>
<key width="1.5" key0="backspace" ne="delete"/>
</row>
</keyboard>

View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<keyboard name="УЕИШЩ (Български, БДС)" script="cyrillic">
<row>
<key key0="у" key2="1" key4="loc esc"/>
<key key0="е" key1="~" key2="2" key3="\@"/>
<key key0="и" key1="!" key2="3" key3="\#" key4="ѝ"/>
<key key0="ш" key2="4" key3="$"/>
<key key0="щ" key2="5" key3="%"/>
<key key0="к" key2="6" key3="^"/>
<key key0="с" key2="7" key3="&amp;" key4="§"/>
<key key0="д" key2="8" key3="*" key4="№"/>
<key key0="з" key2="9" key3="(" key4=")"/>
<key key0="ц" key2="0"/>
<key key0="б" key2="€"/>
</row>
<row>
<key key0="ь" key1="loc tab" key2="`"/>
<key key0="я"/>
<key key0="а"/>
<key key0="о" key2="-" key3="_"/>
<key key0="ж" key2="=" key3="+"/>
<key key0="г" key4="}" key3="{"/>
<key key0="т" key3="[" key4="]"/>
<key key0="н" key2="|" key3="\\"/>
<key key0="в"/>
<key key0="м"/>
<key key0="ч" key1="„" key2="“"/>
</row>
<row>
<key width="1.5" key0="shift" key2="loc capslock"/>
<key key0="ю"/>
<key key0="й"/>
<key key0="ъ" key1="loc accent_cedille" key2="&lt;" key4=">"/>
<key key0="ф" key2="\?" key3="/"/>
<key key0="х" key2=":" key3=";"/>
<key key0="п" key2="&quot;" key3="'"/>
<key key0="р" key3=","/>
<key key0="л" key3="."/>
<key width="1.5" key0="backspace" key2="delete"/>
</row>
</keyboard>

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<keyboard name="ЯВЕРТЪ" script="cyrillic">
<row>
<key key0="я" key2="1" key4="loc esc"/>
<key key0="в" key1="!" key2="2" key3="\@" key4="ч"/>
<key key0="е" key2="3" key3="\#" key4="№"/>
<key key0="р" key2="4" key3="$"/>
<key key0="т" key2="5" key3="%"/>
<key key0="ъ" key2="6" key3="^" key4="€"/>
<key key0="у" key2="7" key3="&amp;" key4="§"/>
<key key0="и" key2="8" key3="*"/>
<key key0="о" key2="9" key3="(" key4=")"/>
<key key0="п" key2="0"/>
</row>
<row>
<key shift="0.5" key0="а" key1="loc tab" key2="`"/>
<key key0="с"/>
<key key0="д"/>
<key key0="ф"/>
<key key0="г" key2="-" key3="_"/>
<key key0="х" key2="=" key3="+"/>
<key key0="й" key4="}" key3="{"/>
<key key0="к" key2="ш" key3="[" key4="]"/>
<key key0="л" key1="щ" key2="|" key3="\\" key4="ю"/>
</row>
<row>
<key width="1.5" key0="shift" key2="loc capslock"/>
<key key0="з"/>
<key key0="ь" key3="ѝ"/>
<key key0="ц" key2="&lt;" key3="."/>
<key key0="ж" key2="&gt;" key3=","/>
<key key0="б" key2="\?" key3="/"/>
<key key0="н" key2=":" key3=";"/>
<key key0="м" key2="&quot;" key3="'"/>
<key width="1.5" key0="backspace" key2="delete"/>
</row>
</keyboard>

Some files were not shown because too many files have changed in this diff Show more