Compare commits

..

11 Commits

Author SHA1 Message Date
Jules Aguillon
c5a6b8a3b2 Update docs 2024-05-26 00:35:55 +02:00
Jules Aguillon
0fadb4b9e6 Draw anticircle labels
They are drawn the same way as indication.
2024-05-26 00:35:11 +02:00
Jules Aguillon
04caf4309a Configure anticircle gesture per-key
This adds the new 'anticircle' attribute to layouts '<key>' elements
that configure the key to send when doing a anti-clockwise circle
gesture on it.
2024-05-26 00:30:36 +02:00
Jules Aguillon
bea2e6cd1f Update check_layout.output
Was outdated since previous commit
2024-05-26 00:20:36 +02:00
Jules Aguillon
abf36e5d56 Add missing characters to hang_dubeolsik_kr 2024-05-25 23:56:35 +02:00
JapanYoshi
c9b6380ed0 Add custom layout documentation in doc/ (#643) 2024-05-25 22:22:42 +02:00
Jules Aguillon
4906f8105f Circle and round trip gestures (#640)
This implements clockwise/anticlockwise circle and round trip gestures
inspired by Messagease.

The circle gestures start after a small threshold to avoid making the
regular swipe too hard to aim.

The gestures do:

- circle: The center symbol with Shift applied, with a fallback on Fn
- round trip: Same as the circle gesture but applied to a side symbol
- anticlockwise circle: Nothing currently. It is intended to be made
  configurable per-layout in the future.

The new Gesture class keeps track of what the pointer is doing while it moves
on a key. It replaces the 'selected_direction' integer.
2024-05-25 21:19:44 +02:00
NACAMURA Mitsuhiro
96fc4003f1 Update NixOS wiki link (#633) 2024-05-12 10:53:04 +02:00
Quinn Cypher
a91332a903 Pull the emoji list from unicode.org (#612)
- Removing unused information (names and descriptions) from the Emoji class
- Creating a Gradle task that generates a more efficient res/raw/emojis.txt file from the most recent Unicode standard
- Saving recently used emoji preferences as emoji values rather than names
- Migrating old user preferences to the new system
2024-05-08 13:02:19 +02:00
Spike
53e04d5784 Compass-point synonyms for edge keys in layouts (#628) 2024-05-08 12:51:11 +02:00
alotbsol555
c7d33356bc Add settings button to launcher app (#629) 2024-05-05 11:22:34 +02:00
21 changed files with 5299 additions and 3983 deletions

View File

@@ -14,7 +14,7 @@ Python 3 is required to update generated files but not to build the app.
For Android Studio users, no more setup is needed.
For Nix users, the right environment can be obtained with `nix-shell ./shell.nix`.
Instructions to install Nix are [here](https://nixos.wiki/wiki/Nix_Installation_Guide).
Instructions to install Nix are [here](https://wiki.nixos.org/wiki/Nix_Installation_Guide).
If you don't use Android Studio or Nix, you have to inform Gradle about the
location of your Android SDK by either:

View File

@@ -103,6 +103,14 @@ tasks.register('buildKeyboardFont') {
}
}
tasks.register('genEmojis') {
println "\nGenerating res/raw/emojis.txt"
exec {
workingDir = projectDir
commandLine "python", "gen_emoji.py"
}
}
tasks.withType(Test).configureEach {
dependsOn 'genLayoutsList'
dependsOn 'checkKeyboardLayouts'

View File

@@ -55,7 +55,8 @@ Duplicate keys: ટ, ડ
Layout doesn't define some important keys, missing: f11_placeholder, f12_placeholder
2 warnings
# hang_dubeolsik_kr
0 warnings
Layout doesn't define some important keys, missing: esc, f11_placeholder, f12_placeholder, tab
1 warnings
# hebr_1_il
Layout includes some ASCII punctuation but not all, missing: (, ), <, >, [, ], {, }
1 warnings

113
doc/Custom-layouts.md Normal file
View File

@@ -0,0 +1,113 @@
# Custom layouts
Unexpected Keyboard allows custom layouts to be defined, loaded, and used in the app. These layouts are defined in XML.
## Existing keyboard layouts
The XML data files for the keyboard layouts that come with the app can be seen [in `/srcs/layouts`](https://github.com/Julow/Unexpected-Keyboard/tree/master/srcs/layouts).
## Structure
A complete keyboard file with a single row containing a single Turkish "i" key is provided below:
<?xml version="1.0" encoding="utf-8"?>
<keyboard name="Keyboard Name" script="latin">
<modmap>
<shift a="i" b="İ" />
</modmap>
<row>
<key key0="i" />
</row>
</keyboard>
Shift assignments can be changed on a per-character basis.
### Crash course to XML
An XML document is made out of tags.
Paired tags start with `<` and end with `>`, and must be closed by another tag that starts with `</`. They can have other tags as children. Paired tags used in Unexpected Keyboard include `<row>`...`</row>` and `<keyboard>`...`</keyboard>`.
Auto-terminating tags start with `<` and end with `/>`, and can't have any children. Auto-terminating tags used in Unexpected Keyboard include `<key />` and `<shift />`.
An XML tag can have attributes, defined using an equals sign and a pair of ASCII double quotes.
If you do not like XML, you can also use [this third-party GUI editor](https://unexpected-keyboard-layout-editor.lixquid.com/) to create or edit a keyboard layout.
### XML declaration
Every keyboard XML starts with `<?xml version="1.0" encoding="utf-8"?>`.
### Keyboard metadata
The `<keyboard>` tag encloses the whole keyboard. The following properties may be used:
* `name`: The name of the keyboard as it appears in the settings menu. If not present, it will just be called “Custom layout”.
* `bottom_row`: Whether or not to show the common bottom row. Accepts `true` or `false`, and defaults to `true`.
* `script`: The (main) writing system that it supports. Possible values are `arabic`, `armenian`, `bengali`, `cyrillic`, `devanagari`, `gujarati`, `hangul`, `hebrew`, `latin`, `persian`, `shavian`, and `urdu`. Defaults to `latin`.
* `numpad_script`: The script to use for the numpad. This is useful for scripts where a different, non-ASCII set of numerals is used, like Devanagari and Arabic. Defaults to the same as `script`.
### Modmap
The `<modmap>` tag encloses custom mappings for the Shift and Fn keys behavior.
Each entry contains two mandatory properties: `a` for the base character before the modifier is applied, and `b` for the modified character.
For example, to make the `I` key behave as in Turkish:
<modmap>
<shift a="i" b="İ" />
</modmap>
There can be as many of these tags inside `<modmap>` as needed.
Shift and Fn modmaps also affect the clockwise circle and the roundtrip gestures.
### Row
The `<row>` tag encloses one row on the keyboard. It requires no properties, and supports the following:
* `height`: The height of the row. Defaults to 1, and accepts a positive floating point value.
The total height of the keyboard is defined in Settings as a percentage of the total height of the screen, which can be different between portrait and landscape. The height of a row is relative to the other ones, and are scaled to keep the height of the keyboard constant.
### Key
The `<key />` tag defines a key on the keyboard. It requires at least one of the following properties:
* `key0`: What the key should do when it is tapped.
* `nw`, `ne`, `sw`, `se`, `w`, `e`, `n`, `s`: What the key should do when it is swiped. They are based on cardinal directions, and uses the convention that North is up. These are the new set of keywords, and should not be used with the other set of keywords.
nw | n | ne
:-: | :--: | :-:
w | key0 | e
sw | s | se
* `key1` through `key8`: The older set of keywords for what the key should do when it is swiped. The directions are ordered as follows:
key1 | key7 | key2
:--: | :--: | :--:
key5 | key0 | key6
key3 | key8 | key4
The following properties are optionally supported:
* `width`: The width of the key relative to the normal width. Defaults to `1` and accepts a positive floating point value.
* `shift`: How much empty space to add to the left of this key. Defaults to `0` and accepts a non-negative floating point value.
* `indication`: An extra label to show under the main label, intended to be used as a legend for 2A typing (e.g. `<key key0="2" indication="ABC" />`). Caution: if you have `key8` defined, it overlaps!
* `slider`: If set to `true`, the keys `w` and `e` are sent repeatedly when the key is being slid on. Intended to be used on the space bar, and in fact used on the default space bar.
* `anticircle`: The key value to send when doing an anti-clockwise circle gesture on the key. The clockwise circle and round-trip gestures are not configurable that way.
## Possible key values
`key0` and `nw` through `se` (`key1` through `key8`) take arbitrary strings of characters, and if they don't match any of the special values, it is printed verbatim. (This is intended behavior.)
Special values for the keys are documented in [this page](Possible-key-values).
### `loc ` prefix
Keys prefixed with `loc ` do not appear by default, and are only visible when they are enabled through the "Add keys to keyboard" option in the settings menu, or the language installed on the device is detected to require it.
## Portrait vs. landscape
Unexpected Keyboard remembers *separately* which layout has last been used in portrait and landscape orientation. That is to say, you may have one custom layout for portrait orientation, but another custom layout for landscape orientation, and Unexpected Keyboard will switch between them without your intervention.

145
doc/Possible-key-values.md Normal file
View File

@@ -0,0 +1,145 @@
# Key values
This is an exhaustive list of special values accepted for the `key0` through `key8` or `nw` through `se` attributes on a key. Any string that does not exactly match these will be printed verbatim.
## Escape codes
Value | Escape code for
:---- | :------
`\?` | `?`
`\#` | `#`
`\@` | `@`
`\n` | Literal newline character. This is different from `enter` and `action` in certain apps.
`\t` | Literal tab character. This is different from `tab` in certain apps.
`\\` | `\`
XML escape codes also work, including:
Value | Escape code for
:------- | :------
`&amp;` | `&`
`&lt;` | `<`
`&gt;` | `>`
`&quot;` | `"`
## Modifiers
System modifiers are sent to the app, which is free to do whatever they want in response.
The other modifiers only exist within the keyboard.
Value | Meaning
:---------- | :------
`shift` | System modifier.
`ctrl` | System modifier.
`alt` | System modifier.
`meta` | System modifier. Equivalent to the Windows key.
`fn` | Activates Fn mode, which assigns letters and symbols to special characters. e.g. `fn` `!` = `¡`
`compose` | Compose key. Enables composing characters using Linux-like shortcuts; e.g. `Compose` `A` `single quote` types `Á` (A with acute accent).
`capslock` | Actives and locks Shift
## Special keys
These keys are sent to apps, which are free to ignore them. The keyboard do not perform editing in response to these keys.
`esc`, `enter`,
`up`, `right`,
`down`, `left`,
`page_up`, `page_down`,
`home`, `end`,
`backspace`, `delete`,
`insert`, `f1`-`f12`,
`tab`, `copy`,
`paste`, `cut`,
`selectAll`, `pasteAsPlainText`,
`undo`, `redo`
## Whitespace
Value | Meaning
:------ | :------
`space` | Space bar.
`nbsp` | No-break space. Note: To input the narrow no-break space recommended for French, use `\u202F`.
`zwj` | Zero-width joiner.
`zwnj` | Zero-width non-joiner.
## Keyboard editing actions
These keys perform editing on the text without sending keys that the app can interpret differently or ignore.
Value | Meaning
:----------------- | :------
`cursor_left` | Moves the cursor position to the left directly, without sending a `left` key event.
`cursor_right` | Moves the cursor position to the right directly, without sending a `right` key event.
## Other modifiers and diacritics
Value | Meaning
:------------------- | :------
`accent_aigu` | Acute accent. `á`
`accent_caron` | Háček. `č`
`accent_cedille` | Cedilla. `ç`
`accent_circonflexe` | Circumflex. `â`
`accent_grave` | Grave accent. `à`
`accent_macron` | Macron. `ā`
`accent_ring` | Ring accent. `å`
`accent_tilde` | Tilde. `ã`
`accent_trema` | Dieresis/umlaut. `ä`
`accent_ogonek` | Ogonek. `ą`
`accent_dot_above` | Dot accent. `ż` If applied to the lowercase `i`, removes the dot instead for Turkish. `ı`
`accent_double_aigu` | Double acute accent. `ő`
`accent_slash` | Slash through. `ø`
`accent_arrow_right` | Right arrow above, used to denote a vector. `a⃗`
`accent_breve` | Breve. `ă`
`accent_bar` | Bar/strikethrough. `ᵢ`
`accent_dot_below` | Dot below. `ạ`
`accent_horn` | Horn accent. `ơ`
`accent_hook_above` | Hook accent. `ả`
`superscript` | Superscript. `ᵃ`
`subscript` | Subscript. `ₐ`
`ordinal` | Turns `a` and `o` into `ª` and `º`.
`arrows` | Turns `1`-`4` and `6`-`9` into arrows.
`box` | Turns `1`-`9`, `0`, and `.` into single-line, thin box-drawing characters.
## Bidirectional
Value | Meaning
:------ | :------
`lrm` | Left-to-right mark.
`rlm` | Right-to-left mark.
`b(`, `b)`, `b[`, `b]`, `b{`, `b}`, `blt`, `bgt` | Sends the bracket characters, but with mirrored key legends for right-to-left languages. (`blt` and `bgt` print `<` and `>` respectively.)
## Hebrew
Keys ending in `_placeholder` are normally hidden unless the Fn key is pressed.
`qamats`, `patah`,
`sheva`, `dagesh`,
`hiriq`, `segol`,
`tsere`, `holam`,
`qubuts`, `hataf_patah`,
`hataf_qamats`, `hataf_segol`,
`shindot`, `shindot_placeholder`,
`sindot`, `sindot_placeholder`,
`geresh`, `gershayim`,
`maqaf`, `rafe`,
`ole`, `ole_placeholder`,
`meteg`, `meteg_placeholder`
## Unexpected Keyboard specific
Value | Meaning
:--------------------- | :------
`config` | Gear icon; opens Unexpected Keyboard settings.
`switch_text` | Switch to the text layer (main layer).
`switch_numeric` | Switch to the numeric layer.
`switch_emoji` | Switch to the emoji layer.
`switch_back_emoji` | Switch to the text layer from the emoji layer.
`switch_forward` | Change the keyboard layout, as long as Unexpected Keyboard has multiple keyboard layouts enabled in the settings.
`switch_backward` | Change the keyboard layout to the previous one in the list.
`switch_greekmath` | Switch to the Greek & Math Symbols layer.
`change_method` | Open the input method picker dialog.
`change_method_prev` | Switch to the previously used input method.
`action` | Performs a special context-sensitive operation related to the Enter key. For example, in the Twitter (X) app, `enter` adds a new line, while `action` posts.
`voice_typing` | Begin voice typing.
`voice_typing_chooser` | Shows a menu where you can choose which voice typing provider to use, then begins voice typing when you make a selection.
`shareText` | Emit a share Intent for the selected text. **Oddity:** This is in CamelCase.
## Unused
These keys are known to do nothing.
`replaceText`, `textAssist`,
`autofill`, `removed`
## Placeholders
These keys are normally hidden unless the Fn modifier is activated.
`f11_placeholder` | `f12_placeholder`

38
gen_emoji.py Normal file
View File

@@ -0,0 +1,38 @@
import urllib.request
import os.path
EMOJIS_PATH = 'res/raw/emojis.txt'
EMOJI_TEST_PATH = 'emoji-test.txt'
EMOJI_TEST_URL = 'https://unicode.org/Public/emoji/latest/emoji-test.txt'
def rawEmojiFromCodes(codes):
return ''.join([chr(int(c, 16)) for c in codes])
def getEmojiTestContents():
if os.path.exists(EMOJI_TEST_PATH):
print(f'Using existing {EMOJI_TEST_PATH}')
else:
print(f'Downloading {EMOJI_TEST_URL}')
urllib.request.urlretrieve(EMOJI_TEST_URL, EMOJI_TEST_PATH)
return open(EMOJI_TEST_PATH, mode='r', encoding='UTF-8').read()
emoji_list = []
group_indices = []
for line in getEmojiTestContents().splitlines():
if line.startswith('# group:'):
if len(group_indices) == 0 or len(emoji_list) > group_indices[-1]:
group_indices.append(len(emoji_list))
elif not line.startswith('#') and 'fully-qualified' in line:
codes = line.split(';')[0].split()
emoji_list.append(rawEmojiFromCodes(codes))
with open(EMOJIS_PATH, 'w', encoding='UTF-8') as emojis:
for e in emoji_list:
emojis.write(f'{e}\n')
emojis.write('\n')
emojis.write(' '.join([str(g) for g in group_indices]))
emojis.write('\n')
print(f'Parsed {len(emoji_list)} emojis in {len(group_indices)}')

View File

@@ -0,0 +1 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#999999" android:pathData="M12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10M10,22C9.75,22 9.54,21.82 9.5,21.58L9.13,18.93C8.5,18.68 7.96,18.34 7.44,17.94L4.95,18.95C4.73,19.03 4.46,18.95 4.34,18.73L2.34,15.27C2.21,15.05 2.27,14.78 2.46,14.63L4.57,12.97L4.5,12L4.57,11L2.46,9.37C2.27,9.22 2.21,8.95 2.34,8.73L4.34,5.27C4.46,5.05 4.73,4.96 4.95,5.05L7.44,6.05C7.96,5.66 8.5,5.32 9.13,5.07L9.5,2.42C9.54,2.18 9.75,2 10,2H14C14.25,2 14.46,2.18 14.5,2.42L14.87,5.07C15.5,5.32 16.04,5.66 16.56,6.05L19.05,5.05C19.27,4.96 19.54,5.05 19.66,5.27L21.66,8.73C21.79,8.95 21.73,9.22 21.54,9.37L19.43,11L19.5,12L19.43,13L21.54,14.63C21.73,14.78 21.79,15.05 21.66,15.27L19.66,18.73C19.54,18.95 19.27,19.04 19.05,18.95L16.56,17.95C16.04,18.34 15.5,18.68 14.87,18.93L14.5,21.58C14.46,21.82 14.25,22 14,22H10M11.25,4L10.88,6.61C9.68,6.86 8.62,7.5 7.85,8.39L5.44,7.35L4.69,8.65L6.8,10.2C6.4,11.37 6.4,12.64 6.8,13.8L4.68,15.36L5.43,16.66L7.86,15.62C8.63,16.5 9.68,17.14 10.87,17.38L11.24,20H12.76L13.13,17.39C14.32,17.14 15.37,16.5 16.14,15.62L18.57,16.66L19.32,15.36L17.2,13.81C17.6,12.64 17.6,11.37 17.2,10.2L19.31,8.65L18.56,7.35L16.15,8.39C15.38,7.5 14.32,6.86 13.12,6.62L12.75,4H11.25Z" /></vector>

View File

@@ -0,0 +1,10 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".LauncherActivity">
<item
android:id="@+id/btnLaunchSettingsActivity"
android:title=""
android:icon="@drawable/cog_outline"
android:showAsAction="always" />
</menu>

File diff suppressed because it is too large Load Diff

View File

@@ -7,26 +7,15 @@ 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 String _name;
private final KeyValue _kv;
private final String _desc;
private static HashMap<String, Emoji> emojis_by_name = new HashMap<String, Emoji>();
protected Emoji(String name, String bytecode, String desc)
protected Emoji(String bytecode)
{
_name = name;
_kv = new KeyValue(bytecode, KeyValue.Kind.String, 0, 0);
_desc = desc;
emojis_by_name.put(name, this);
}
public String name()
{
return _name;
this._kv = new KeyValue(bytecode, KeyValue.Kind.String, 0, 0);
}
public KeyValue kv()
@@ -34,54 +23,766 @@ public class Emoji
return _kv;
}
public String getDescription()
{
return (_desc);
}
public static int num_groups = 0;
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<>();
private static Emoji[][] emojis_by_group = new Emoji[][]{};
public static Emoji getEmojiByName(String name)
{
return emojis_by_name.get(name);
}
public static Emoji[] getEmojisByGroup(int group_id)
{
return (emojis_by_group[group_id]);
}
/* Read the list of emojis from a raw file. Will initialize only once. */
public static void init(Resources res)
{
if (num_groups > 0)
if (!_all.isEmpty())
return;
try
{
ArrayList<Emoji[]> groups = new ArrayList<Emoji[]>();
InputStream inputStream = res.openRawResource(R.raw.emojis);
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while (true)
// Read emoji (until empty line)
while (!(line = reader.readLine()).isEmpty())
{
line = reader.readLine();
if (line == null)
break;
int group_len = Integer.parseInt(line);
Emoji[] grp = new Emoji[group_len];
for (int i = 0; i < group_len; i++)
{
line = reader.readLine();
String[] f = line.split(" ", 3);
grp[i] = new Emoji(f[0], f[1], f[2]);
}
groups.add(grp);
Emoji e = new Emoji(line);
_all.add(e);
_stringMap.put(line, e);
}
// Read group indices
if ((line = reader.readLine()) != null)
{
String[] tokens = line.split(" ");
for (int i = 0; i < tokens.length-1; i++)
_groups.add(_all.subList(Integer.parseInt(tokens[i]), Integer.parseInt(tokens[i+1])));
}
num_groups = groups.size();
emojis_by_group = groups.toArray(new Emoji[0][]);
}
catch (IOException e) {}
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

@@ -10,10 +10,12 @@ import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.TextView;
import java.util.Arrays;
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
@@ -23,7 +25,7 @@ public class EmojiGridView extends GridView
private static final String LAST_USE_PREF = "emoji_last_use";
private Emoji[] _emojiArray;
private List<Emoji> _emojiArray;
private HashMap<Emoji, Integer> _lastUsed;
/*
@@ -34,6 +36,7 @@ public class EmojiGridView extends GridView
{
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);
@@ -48,26 +51,23 @@ public class EmojiGridView extends GridView
public void onItemClick(AdapterView<?> parent, View v, int pos, long id)
{
Config config = Config.globalConfig();
Integer used = _lastUsed.get(_emojiArray[pos]);
_lastUsed.put(_emojiArray[pos], (used == null) ? 1 : used.intValue() + 1);
config.handler.key_up(_emojiArray[pos].kv(), Pointers.Modifiers.EMPTY);
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 Emoji[] getLastEmojis()
private List<Emoji> getLastEmojis()
{
final HashMap<Emoji, Integer> map = _lastUsed;
Emoji[] array = new Emoji[map.size()];
map.keySet().toArray(array);
Arrays.sort(array, 0, array.length, new Comparator<Emoji>()
List<Emoji> list = new ArrayList<>(_lastUsed.keySet());
Collections.sort(list, new Comparator<Emoji>()
{
public int compare(Emoji a, Emoji b)
{
return (map.get(b).intValue() - map.get(a).intValue());
return _lastUsed.get(b) - _lastUsed.get(a);
}
});
return (array);
return list;
}
private void saveLastUsed()
@@ -77,7 +77,7 @@ public class EmojiGridView extends GridView
catch (Exception _e) { return; }
HashSet<String> set = new HashSet<String>();
for (Emoji emoji : _lastUsed.keySet())
set.add(String.valueOf(_lastUsed.get(emoji)) + "-" + emoji.name());
set.add(String.valueOf(_lastUsed.get(emoji)) + "-" + emoji.kv().getString());
edit.putStringSet(LAST_USE_PREF, set);
edit.apply();
}
@@ -98,7 +98,7 @@ public class EmojiGridView extends GridView
Emoji emoji;
if (data.length != 2)
continue ;
emoji = Emoji.getEmojiByName(data[1]);
emoji = Emoji.getEmojiByString(data[1]);
if (emoji == null)
continue ;
_lastUsed.put(emoji, Integer.valueOf(data[0]));
@@ -110,6 +110,37 @@ public class EmojiGridView extends GridView
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)
@@ -127,9 +158,9 @@ public class EmojiGridView extends GridView
{
Context _button_context;
Emoji[] _emojiArray;
List<Emoji> _emojiArray;
public EmojiViewAdpater(Context context, Emoji[] emojiArray)
public EmojiViewAdpater(Context context, List<Emoji> emojiArray)
{
_button_context = new ContextThemeWrapper(context, R.style.emojiGridButton);
_emojiArray = emojiArray;
@@ -139,12 +170,12 @@ public class EmojiGridView extends GridView
{
if (_emojiArray == null)
return (0);
return (_emojiArray.length);
return (_emojiArray.size());
}
public Object getItem(int pos)
{
return (_emojiArray[pos]);
return (_emojiArray.get(pos));
}
public long getItemId(int pos)
@@ -158,7 +189,7 @@ public class EmojiGridView extends GridView
if (view == null)
view = new EmojiView(_button_context);
view.setEmoji(_emojiArray[pos]);
view.setEmoji(_emojiArray.get(pos));
return view;
}
}

View File

@@ -19,9 +19,9 @@ public class EmojiGroupButtonsBar extends LinearLayout
super(context, attrs);
Emoji.init(context.getResources());
add_group(EmojiGridView.GROUP_LAST_USE, "\uD83D\uDD59");
for (int i = 0; i < Emoji.num_groups; i++)
for (int i = 0; i < Emoji.getNumGroups(); i++)
{
Emoji first = Emoji.getEmojisByGroup(i)[0];
Emoji first = Emoji.getEmojisByGroup(i).get(0);
add_group(i, first.kv().getString());
}
}

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) < ROTATION_THRESHOLD)
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

@@ -33,7 +33,6 @@ public final class KeyModifier
if (r == null)
{
r = k;
/* Order: Fn, Shift, accents */
for (int i = 0; i < n_mods; i++)
r = modify(r, mods.get(i));
ks.put(mods, r);
@@ -70,6 +69,7 @@ public final class KeyModifier
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_map_char(k, map_char_grave);
case AIGU: return apply_map_char(k, map_char_aigu);
@@ -139,11 +139,10 @@ public final class KeyModifier
case Char:
char kc = k.getChar();
String modified = map.apply(kc);
if (modified == null)
return k;
return KeyValue.makeStringKey(modified, k.getFlags());
default: return k;
if (modified != null)
return KeyValue.makeStringKey(modified, k.getFlags());
}
return k;
}
private static KeyValue apply_shift(KeyValue k)
@@ -482,6 +481,15 @@ public final class KeyModifier
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 shifted = apply_shift(k);
if (shifted == null || shifted.equals(k))
return apply_fn(k);
return shifted;
}
/* Lookup the cache entry for a key. Create it needed. */
private static HashMap<Pointers.Modifiers, KeyValue> cacheEntry(KeyValue k)
{

View File

@@ -27,6 +27,7 @@ public final class KeyValue implements Comparable<KeyValue>
public static enum Modifier
{
SHIFT,
GESTURE,
CTRL,
ALT,
META,
@@ -54,8 +55,8 @@ public final class KeyValue implements Comparable<KeyValue>
ARROW_RIGHT,
BREVE,
BAR,
FN, // Must be placed last to be applied first
}
FN,
} // Last is be applied first
public static enum Editing
{
@@ -404,6 +405,12 @@ public final class KeyValue implements Comparable<KeyValue>
return new KeyValue(str, Kind.String, 0, flags | FLAG_SMALLER_FONT);
}
/** Make a modifier key for passing to [KeyModifier]. */
public static KeyValue makeInternalModifier(Modifier mod)
{
return new KeyValue("", Kind.Modifier, mod.ordinal(), 0);
}
public static KeyValue getKeyByName(String name)
{
switch (name)

View File

@@ -268,8 +268,11 @@ public class Keyboard2View extends View
if (VERSION.SDK_INT >= 29)
{
// Disable the back-gesture on the keyboard area
Rect keyboard_area = new Rect(left,
top + (int)_config.marginTop, right, bottom);
Rect keyboard_area = new Rect(
left + (int)_config.horizontal_margin,
top + (int)_config.marginTop,
right - (int)_config.horizontal_margin,
bottom - (int)_config.margin_bottom);
setSystemGestureExclusionRects(Arrays.asList(keyboard_area));
}
}
@@ -319,10 +322,7 @@ public class Keyboard2View extends View
if (k.keys[i] != null)
drawSubLabel(canvas, k.keys[i], x, y, keyW, keyH, i, isKeyDown);
}
if (k.indication != null)
{
drawIndication(canvas, k.indication, keyW / 2f + x, y, keyH);
}
drawIndication(canvas, k, x, y, keyW, keyH);
x += _keyWidth * k.width;
}
y += row.height * _config.keyHeight;
@@ -440,15 +440,33 @@ public class Keyboard2View extends View
canvas.drawText(label, 0, label_len, x, y, p);
}
private void drawIndication(Canvas canvas, String indication, float x,
float y, float keyH)
private void drawIndication(Canvas canvas, KeyboardData.Key k, float x,
float y, float keyW, float keyH)
{
float textSize = keyH * _config.sublabelTextSize * _config.characterSize;
Paint p = _theme.indicationPaint();
boolean special_font = false;
String indic;
float text_size;
if (k.indication != null)
{
indic = k.indication;
text_size = keyH * _config.sublabelTextSize * _config.characterSize;
}
else if (k.anticircle != null)
{
indic = k.anticircle.getString();
special_font = k.anticircle.hasFlagsAny(KeyValue.FLAG_KEY_FONT);
text_size = scaleTextSize(k.anticircle, _config.sublabelTextSize, keyH);
}
else
{
return;
}
Paint p = _theme.indicationPaint(special_font);
p.setColor(_theme.subLabelColor);
p.setTextSize(textSize);
canvas.drawText(indication, x,
(keyH - p.ascent() - p.descent()) * 4/5 + y, p);
p.setTextSize(text_size);
// Limit indication length to 3 characters
canvas.drawText(indic, 0, Math.min(indic.length(), 3),
x + keyW / 2f, (keyH - p.ascent() - p.descent()) * 4/5 + y, p);
}
private float scaleTextSize(KeyValue k, float rel_size, float keyH)

View File

@@ -373,6 +373,8 @@ public final class KeyboardData
* 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. */
@@ -389,9 +391,10 @@ public final class KeyboardData
public static final int F_LOC = 1;
public static final int ALL_FLAGS = F_LOC;
protected Key(KeyValue[] ks, int f, float w, float s, boolean sl, String i)
protected Key(KeyValue[] ks, KeyValue antic, int f, float w, float s, boolean sl, String i)
{
keys = ks;
anticircle = antic;
keysflags = f;
width = w;
shift = s;
@@ -399,27 +402,48 @@ public final class KeyboardData
indication = i;
}
/** Write the parsed key into [ks] at [index]. Doesn't write if the
attribute is not present. Return flags that can be aggregated into the
value for [keysflags]. */
static int parse_key_attr(XmlPullParser parser, String attr, KeyValue[] ks,
/** 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
{
String name = parser.getAttributeValue(null, attr);
int flags = 0;
if (name == null)
if (key_val == null)
return 0;
String name_loc = stripPrefix(name, "loc ");
int flags = 0;
String name_loc = stripPrefix(key_val, "loc ");
if (name_loc != null)
{
flags |= F_LOC;
name = name_loc;
key_val = name_loc;
}
ks[index] = KeyValue.getKeyByName(name);
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;
@@ -429,22 +453,25 @@ public final class KeyboardData
{
KeyValue[] ks = new KeyValue[9];
int keysflags = 0;
keysflags |= parse_key_attr(parser, "key0", ks, 0);
keysflags |= parse_key_attr(parser, "key1", ks, 1);
keysflags |= parse_key_attr(parser, "key2", ks, 2);
keysflags |= parse_key_attr(parser, "key3", ks, 3);
keysflags |= parse_key_attr(parser, "key4", ks, 4);
keysflags |= parse_key_attr(parser, "key5", ks, 5);
keysflags |= parse_key_attr(parser, "key6", ks, 6);
keysflags |= parse_key_attr(parser, "key7", ks, 7);
keysflags |= parse_key_attr(parser, "key8", ks, 8);
keysflags |= parse_key_attr(parser, parser.getAttributeValue(null, "key0"), 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);
boolean slider = attribute_bool(parser, "slider", false);
String indication = parser.getAttributeValue(null, "indication");
while (parser.next() != XmlPullParser.END_TAG)
continue;
return new Key(ks, keysflags, width, shift, slider, indication);
return new Key(ks, anticircle, keysflags, width, shift, slider, indication);
}
/** Whether key at [index] as [flag]. */
@@ -456,7 +483,8 @@ public final class KeyboardData
/** New key with the width multiplied by 's'. */
public Key scaleWidth(float s)
{
return new Key(keys, keysflags, width * s, shift, slider, indication);
return new Key(keys, anticircle, keysflags, width * s, shift, slider,
indication);
}
public void getKeys(Map<KeyValue, KeyPos> dst, int row, int col)
@@ -477,12 +505,12 @@ public final class KeyboardData
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, flags, width, shift, slider, indication);
return new Key(ks, anticircle, flags, width, shift, slider, indication);
}
public Key withShift(float s)
{
return new Key(keys, keysflags, width, s, slider, indication);
return new Key(keys, anticircle, keysflags, width, s, slider, indication);
}
public boolean hasValue(KeyValue kv)
@@ -508,7 +536,7 @@ public final class KeyboardData
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.keysflags, k.width, k.shift, k.slider, k.indication);
return new Key(ks, k.anticircle, k.keysflags, k.width, k.shift, k.slider, k.indication);
}
}

View File

@@ -10,6 +10,8 @@ import android.os.Build.VERSION;
import android.os.Bundle;
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;
@@ -37,7 +39,21 @@ public class LauncherActivity extends Activity
this.new Tryhere_OnUnhandledKeyEventListener());
setup_intro_video(_intro_video);
}
@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));

View File

@@ -143,6 +143,12 @@ public final class Pointers implements Handler.Callback
return;
}
stopKeyRepeat(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
{
@@ -152,7 +158,7 @@ public final class Pointers implements Handler.Callback
else // Otherwise, unlatch
{
removePtr(latched);
_handler.onPointerUp(ptr.value, ptr.modifiers);
_handler.onPointerUp(ptr_value, ptr.modifiers);
}
}
else if ((ptr.flags & FLAG_P_LATCHABLE) != 0)
@@ -168,7 +174,7 @@ public final class Pointers implements Handler.Callback
{
clearLatched();
removePtr(ptr);
_handler.onPointerUp(ptr.value, ptr.modifiers);
_handler.onPointerUp(ptr_value, ptr.modifiers);
}
}
@@ -217,18 +223,15 @@ public final class Pointers implements Handler.Callback
return k.keys[DIRECTION_TO_INDEX[direction]];
}
/*
* Get the KeyValue at the given direction. In case of swipe (direction !=
* null), get the nearest KeyValue that is not key0.
* Take care of applying [_handler.onPointerSwipe] to the selected key, this
* must be done at the same time 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.
/**
* 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, Integer direction)
private KeyValue getNearestKeyAtDirection(Pointer ptr, int direction)
{
if (direction == null)
return _handler.modifyKey(ptr.key.keys[0], ptr.modifiers);
KeyValue k;
// [i] is [0, -1, 1, -2, 2, ...]
for (int i = 0; i > -4; i = (~i>>31) - i)
@@ -261,37 +264,59 @@ public final class Pointers implements Handler.Callback
float dy = y - ptr.downY;
float dist = Math.abs(dx) + Math.abs(dy);
Integer direction;
if (dist < _config.swipe_dist_px)
{
direction = null;
// 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
direction = ((int)(a * 8 / Math.PI) + 12) % 16;
}
int direction = ((int)(a * 8 / Math.PI) + 12) % 16;
if (ptr.gesture == null)
{ // Gesture starts
if (direction != ptr.selected_direction)
{
ptr.selected_direction = direction;
KeyValue newValue = getNearestKeyAtDirection(ptr, direction);
if (newValue != null && !newValue.equals(ptr.value))
{
ptr.value = newValue;
ptr.flags = pointer_flags_of_kv(newValue);
// Sliding mode is entered when key5 or key6 is down on a slider key.
if (ptr.key.slider &&
(newValue.equals(ptr.key.getKeyValue(5))
|| newValue.equals(ptr.key.getKeyValue(6))))
{
startSliding(ptr, x);
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);
// Sliding mode is entered when key5 or key6 is down on a slider key.
if (ptr.key.slider &&
(new_value.equals(ptr.key.getKeyValue(5))
|| new_value.equals(ptr.key.getKeyValue(6))))
{
startSliding(ptr, x);
}
_handler.onPointerDown(new_value, true);
}
}
else if (ptr.gesture.changed_direction(direction))
{ // Gesture changed state
if (!ptr.gesture.is_in_progress())
{ // Gesture ended
stopKeyRepeat(ptr);
_handler.onPointerFlagsChanged(true);
}
else
{
ptr.value = apply_gesture(ptr, ptr.gesture.get_gesture());
restartKeyRepeat(ptr);
ptr.flags = 0; // Special behaviors are ignored during a gesture.
}
_handler.onPointerDown(newValue, true);
}
}
}
@@ -395,6 +420,12 @@ public final class Pointers implements Handler.Callback
}
}
private void restartKeyRepeat(Pointer ptr)
{
stopKeyRepeat(ptr);
startKeyRepeat(ptr);
}
/** A pointer is repeating. Returns [true] if repeat should continue. */
private boolean handleKeyRepeat(Pointer ptr)
{
@@ -447,14 +478,51 @@ public final class Pointers implements Handler.Callback
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
private static final class Pointer
{
/** -1 when latched. */
public int pointerId;
/** The Key pressed by this Pointer */
public final KeyboardData.Key key;
/** Current direction. [null] means not swiping. */
public Integer selected_direction;
/** 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;
@@ -472,7 +540,7 @@ public final class Pointers implements Handler.Callback
{
pointerId = p;
key = k;
selected_direction = null;
gesture = null;
value = v;
downX = x;
downY = y;
@@ -602,6 +670,14 @@ public final class Pointers implements Handler.Callback
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)
{

View File

@@ -35,6 +35,7 @@ public class Theme
private final Paint _keySubLabelPaint;
private final Paint _specialKeySubLabelPaint;
private final Paint _indicationPaint;
private final Paint _specialIndicationPaint;
public Theme(Context context, AttributeSet attrs)
{
@@ -68,6 +69,7 @@ public class Theme
_specialKeyLabelPaint = initLabelPaint(Paint.Align.CENTER, specialKeyFont);
_specialKeySubLabelPaint = initLabelPaint(Paint.Align.LEFT, specialKeyFont);
_indicationPaint = initLabelPaint(Paint.Align.CENTER, null);
_specialIndicationPaint = initLabelPaint(Paint.Align.CENTER, specialKeyFont);
}
public Paint labelPaint(boolean special_font)
@@ -83,9 +85,9 @@ public class Theme
return p;
}
public Paint indicationPaint()
public Paint indicationPaint(boolean special_font)
{
return _indicationPaint;
return special_font ? _specialIndicationPaint : _indicationPaint;
}
/** Interpolate the 'value' component toward its opposite by 'alpha'. */

View File

@@ -1,48 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<keyboard name="두벌식 (Korean)" script="hangul">
<modmap>
<!-- The hangul character don't have shifted variants, this is specific to
the layout. -->
<shift a="ㅂ" b="ㅃ"/>
<shift a="ㅈ" b="ㅉ"/>
<shift a="ㄷ" b="ㄸ"/>
<shift a="ㄱ" b="ㄲ"/>
<shift a="ㅅ" b="ㅆ"/>
<shift a="ㅐ" b="ㅒ"/>
<shift a="ㅔ" b="ㅖ"/>
</modmap>
<row>
<key key0="ㅂ" key2="1" key4="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="ㅑ" key2="8" key3="*"/>
<key key0="ㅐ" key2="9" key3="(" key4=")"/>
<key key0="ㅔ" key2="0" key3="f11_placeholder" key4="f12_placeholder"/>
<key key0="ㅂ" key2="1" key3="ㅄ" key4=""/>
<key key0="ㅈ" key1="~" key2="2" key3="ㄵ" key4="ㅉ"/>
<key key0="ㄷ" key1="!" key2="3" key4=""/>
<key key0="ㄱ" key1="\@" key2="4" key3="ㄺ" key4="ㄲ"/>
<key key0="ㅅ" key1="\#" key2="5" key3="ㄳ" key4="ㅆ"/>
<key key0="ㅛ" key1="$" key2="6"/>
<key key0="ㅕ" key1="%" key2="7" key3=""/>
<key key0="ㅑ" key1="^" key2="8"/>
<key key0="ㅐ" key1="&amp;" key2="9" key3=""/>
<key key0="ㅔ" key1="*" key2="0" key3=""/>
</row>
<row>
<key shift="0.5" key0="ㅁ" key1="tab" key2="`"/>
<key key0="ㄴ"/>
<key key0="ㅇ"/>
<key key0="ㄹ"/>
<key key0="ㅎ" key2="-" key3="_"/>
<key key0="ㅗ" key2="=" key3="+"/>
<key key0="ㅓ" key4="}" key3="{"/>
<key key0="ㅏ" key3="[" key4="]"/>
<key shift="0.5" key0="ㅁ" key2="`"/>
<key key0="ㄴ" key3="ㄼ"/>
<key key0="ㅇ" key3="ㄻ"/>
<key key0="ㄹ" key1="-" key2="_" key3="ㄽ"/>
<key key0="ㅎ" key1="=" key2="+" key3="ㅀ" key4="ㄶ"/>
<key key0="ㅗ" key1="{" key2="}" key3=""/>
<key key0="ㅓ" key1="[" key2="]" key3=""/>
<key key0="ㅏ" key1="(" key2=")"/>
<key key0="ㅣ" key2="|" key3="\\"/>
</row>
<row>
<key width="1.5" key0="shift" key2="loc capslock"/>
<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="ㄾ" key3="&lt;" key4="&gt;"/>
<key key0="ㅊ" key3="."/>
<key key0="ㅍ" key1="" key3=","/>
<key key0="ㅠ" key1="ㅞ" key2="" key3="/" key4="\?"/>
<key key0="ㅜ" key2="" key3=";" key4=":"/>
<key key0="ㅡ" key2="" key3="'" key4="&quot;"/>
<key width="1.5" key0="backspace" key2="delete"/>
</row>
</keyboard>