mirror of
https://github.com/Julow/Unexpected-Keyboard.git
synced 2025-08-18 00:10:00 +02:00
Compare commits
2 Commits
selection-
...
delete-wor
Author | SHA1 | Date | |
---|---|---|---|
|
e60304b30d | ||
|
a6f9c72eb3 |
@@ -25,30 +25,31 @@ Key values can be any of the following:
|
||||
+ `⏯:keyevent:85` A play/pause key (which has no effect in most apps).
|
||||
+ `my@:'my.email@domain.com'` A key that sends an arbitrary string
|
||||
|
||||
- A macro, `legend:key_def1,key_def2,...`.
|
||||
This results in a key with legend `legend` that behaves as if the sequence of `key_def` had been pressed in order.
|
||||
- A macro, `symbol:key_def1,key_def2,...`.
|
||||
This results in a key that behaves as if the sequence of `key_def` had been pressed in order.
|
||||
|
||||
Examples:
|
||||
+ `CA:ctrl,a,ctrl,c` A key with legend CA that sends the sequence `ctrl+a`, `ctrl+c`.
|
||||
+ `Cd:ctrl,backspace` A key with legend Cd that sends the shortcut `ctrl+backspace`.
|
||||
|
||||
### Escape codes
|
||||
|
||||
When defining a key value, several characters have special effects. If you want a character not to have its usual effect but to be taken literally, you should "escape" it in the usual way for XML:
|
||||
|
||||
To get this character... | ...you can type
|
||||
## Escape codes
|
||||
Value | Escape code for
|
||||
:---- | :------
|
||||
A literal newline character, which is different from `enter` and `action` in certain apps. | `\n`
|
||||
A literal tab character, which is different from `tab` in certain apps. | `\t`
|
||||
`\` | `\\`
|
||||
`&` | `&`
|
||||
`<` | `<`
|
||||
`>` | `>`
|
||||
`"` | `"`
|
||||
`\?` | `?`
|
||||
`\#` | `#`
|
||||
`\@` | `@`
|
||||
`\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.
|
||||
`\\` | `\`
|
||||
|
||||
The characters `?`, `#`, and `@` do not need to be escaped when writing custom layouts. Internally, they can be escaped by prepending backslash (by typing `\?`, `\#`, and `\@`).
|
||||
XML escape codes also work, including:
|
||||
|
||||
The characters `,` and `:` can be escaped in a key value, using single quotes. For example, this macro defines a key with legend `http` that sends a string containing `:`: `<key c="http:home,'https://'" />` For simplicity, `,` and `:` cannot be escaped in the key legend.
|
||||
Value | Escape code for
|
||||
:------- | :------
|
||||
`&` | `&`
|
||||
`<` | `<`
|
||||
`>` | `>`
|
||||
`"` | `"`
|
||||
|
||||
## Modifiers
|
||||
System modifiers are sent to the app, which can take app-specific action.
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package juloo.keyboard2;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
@@ -26,9 +27,9 @@ public final class Autocapitalisation
|
||||
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES |
|
||||
InputType.TYPE_TEXT_FLAG_CAP_WORDS;
|
||||
|
||||
public Autocapitalisation(Handler h, Callback cb)
|
||||
public Autocapitalisation(Looper looper, Callback cb)
|
||||
{
|
||||
_handler = h;
|
||||
_handler = new Handler(looper);
|
||||
_callback = cb;
|
||||
}
|
||||
|
||||
|
@@ -2,7 +2,6 @@ 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;
|
||||
@@ -29,10 +28,10 @@ public final class KeyEventHandler
|
||||
[setSelection] could be used instead. */
|
||||
boolean _move_cursor_force_fallback = false;
|
||||
|
||||
public KeyEventHandler(IReceiver recv)
|
||||
public KeyEventHandler(Looper looper, IReceiver recv)
|
||||
{
|
||||
_recv = recv;
|
||||
_autocap = new Autocapitalisation(recv.getHandler(),
|
||||
_autocap = new Autocapitalisation(looper,
|
||||
this.new Autocapitalisation_callback());
|
||||
_mods = Pointers.Modifiers.EMPTY;
|
||||
}
|
||||
@@ -242,7 +241,6 @@ public final class KeyEventHandler
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,17 +258,15 @@ public final class KeyEventHandler
|
||||
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)
|
||||
/** [repeatition] might be negative, in which case the direction is reversed. */
|
||||
void handle_slider(KeyValue.Slider s, int repeatition)
|
||||
{
|
||||
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); break;
|
||||
case Selection_cursor_right: move_cursor_sel(r, false); break;
|
||||
case Cursor_left: move_cursor(-repeatition); break;
|
||||
case Cursor_right: move_cursor(repeatition); break;
|
||||
case Cursor_up: move_cursor_vertical(-repeatition); break;
|
||||
case Cursor_down: move_cursor_vertical(repeatition); break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,7 +280,12 @@ public final class KeyEventHandler
|
||||
if (conn == null)
|
||||
return;
|
||||
ExtractedText et = get_cursor_pos(conn);
|
||||
if (et != null && can_set_selection(conn))
|
||||
int system_mods =
|
||||
KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON | KeyEvent.META_META_ON;
|
||||
// Fallback to sending key events if system modifiers are activated or
|
||||
// ExtractedText is not supported, for example on Termux.
|
||||
if (!_move_cursor_force_fallback && et != null
|
||||
&& (_meta_state & system_mods) == 0)
|
||||
{
|
||||
int sel_start = et.selectionStart;
|
||||
int sel_end = et.selectionEnd;
|
||||
@@ -303,45 +304,8 @@ public final class KeyEventHandler
|
||||
sel_start = sel_end;
|
||||
}
|
||||
if (conn.setSelection(sel_start, sel_end))
|
||||
return; // Fallback to sending key events if [setSelection] failed
|
||||
return; // [setSelection] succeeded, don't fallback to key events
|
||||
}
|
||||
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)
|
||||
{
|
||||
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;
|
||||
if (sel_left == (sel_start <= sel_end))
|
||||
sel_start += d;
|
||||
else
|
||||
sel_end += d;
|
||||
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
|
||||
@@ -360,73 +324,32 @@ public final class KeyEventHandler
|
||||
|
||||
void evaluate_macro(KeyValue[] keys)
|
||||
{
|
||||
if (keys.length == 0)
|
||||
return;
|
||||
final Pointers.Modifiers empty = Pointers.Modifiers.EMPTY;
|
||||
// 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)
|
||||
mods_changed(empty);
|
||||
Pointers.Modifiers mods = empty;
|
||||
final boolean autocap_paused = _autocap.pause();
|
||||
for (KeyValue kv : keys)
|
||||
{
|
||||
kv = KeyModifier.modify(kv, mods);
|
||||
if (kv == null)
|
||||
continue;
|
||||
if (kv.hasFlagsAny(KeyValue.FLAG_LATCH))
|
||||
{
|
||||
// Non-special latchable keys clear latched modifiers
|
||||
if (!kv.hasFlagsAny(KeyValue.FLAG_SPECIAL))
|
||||
mods = Pointers.Modifiers.EMPTY;
|
||||
mods = empty;
|
||||
mods = mods.with_extra_mod(kv);
|
||||
}
|
||||
else
|
||||
{
|
||||
key_down(kv, false);
|
||||
key_up(kv, mods);
|
||||
mods = Pointers.Modifiers.EMPTY;
|
||||
mods = 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)
|
||||
@@ -435,27 +358,12 @@ public final class KeyEventHandler
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
|
@@ -82,7 +82,6 @@ public final class KeyModifier
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -393,34 +392,6 @@ public final class KeyModifier
|
||||
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)
|
||||
{
|
||||
|
@@ -59,7 +59,6 @@ public final class KeyValue implements Comparable<KeyValue>
|
||||
BREVE,
|
||||
BAR,
|
||||
FN,
|
||||
SELECTION_MODE,
|
||||
} // Last is be applied first
|
||||
|
||||
public static enum Editing
|
||||
@@ -78,7 +77,6 @@ public final class KeyValue implements Comparable<KeyValue>
|
||||
AUTOFILL,
|
||||
DELETE_WORD,
|
||||
FORWARD_DELETE_WORD,
|
||||
SELECTION_CANCEL,
|
||||
}
|
||||
|
||||
public static enum Placeholder
|
||||
@@ -717,9 +715,6 @@ public final class KeyValue implements Comparable<KeyValue>
|
||||
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);
|
||||
@@ -769,9 +764,6 @@ public final class KeyValue implements Comparable<KeyValue>
|
||||
case "௲": case "௳":
|
||||
return makeStringKey(name, FLAG_SMALLER_FONT);
|
||||
|
||||
/* Internal keys */
|
||||
case "selection_mode": return makeInternalModifier(Modifier.SELECTION_MODE);
|
||||
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
@@ -788,9 +780,7 @@ public final class KeyValue implements Comparable<KeyValue>
|
||||
Cursor_left(0xE008),
|
||||
Cursor_right(0xE006),
|
||||
Cursor_up(0xE005),
|
||||
Cursor_down(0xE007),
|
||||
Selection_cursor_left(0xE008),
|
||||
Selection_cursor_right(0xE006);
|
||||
Cursor_down(0xE007);
|
||||
|
||||
final String symbol;
|
||||
|
||||
|
@@ -6,7 +6,6 @@ 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;
|
||||
@@ -39,7 +38,6 @@ public class Keyboard2 extends InputMethodService
|
||||
private ViewGroup _emojiPane = null;
|
||||
private ViewGroup _clipboard_pane = null;
|
||||
public int actionId; // Action performed by the Action key.
|
||||
private Handler _handler;
|
||||
|
||||
private Config _config;
|
||||
|
||||
@@ -109,8 +107,7 @@ public class Keyboard2 extends InputMethodService
|
||||
{
|
||||
super.onCreate();
|
||||
SharedPreferences prefs = DirectBootAwarePreferences.get_shared_preferences(this);
|
||||
_handler = new Handler(getMainLooper());
|
||||
_keyeventhandler = new KeyEventHandler(this.new Receiver());
|
||||
_keyeventhandler = new KeyEventHandler(getMainLooper(), this.new Receiver());
|
||||
Config.initGlobalConfig(prefs, getResources(), _keyeventhandler);
|
||||
prefs.registerOnSharedPreferenceChangeListener(this);
|
||||
_config = Config.globalConfig();
|
||||
@@ -362,8 +359,6 @@ public class Keyboard2 extends InputMethodService
|
||||
{
|
||||
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
|
||||
@@ -482,20 +477,10 @@ public class Keyboard2 extends InputMethodService
|
||||
_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()
|
||||
|
@@ -139,13 +139,6 @@ public class Keyboard2View extends View
|
||||
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);
|
||||
|
@@ -422,8 +422,6 @@ public final class KeyboardData
|
||||
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. */
|
||||
|
@@ -74,11 +74,7 @@ def sync_metadata(value_dir, strings):
|
||||
os.makedirs(meta_dir)
|
||||
txt_file = os.path.join(meta_dir, fname)
|
||||
with open(txt_file, "w", encoding="utf-8") as out:
|
||||
out.write(string.text
|
||||
.replace("\\n", "\n")
|
||||
.replace("\\'", "'")
|
||||
.removeprefix('"')
|
||||
.removesuffix('"'))
|
||||
out.write(string.text.removeprefix('"').removesuffix('"'))
|
||||
out.write("\n")
|
||||
sync_meta_file("title.txt", ("app_name_release", None))
|
||||
sync_meta_file("short_description.txt", ("short_description", None))
|
||||
|
Reference in New Issue
Block a user