-
-
Notifications
You must be signed in to change notification settings - Fork 316
Expand file tree
/
Copy pathKeyEventHandler.java
More file actions
581 lines (538 loc) · 18.2 KB
/
KeyEventHandler.java
File metadata and controls
581 lines (538 loc) · 18.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
package juloo.keyboard2;
import android.annotation.SuppressLint;
import android.os.Handler;
import android.os.Looper;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
import java.util.Iterator;
import juloo.keyboard2.suggestions.Suggestions;
public final class KeyEventHandler
implements Config.IKeyEventHandler,
ClipboardHistoryService.ClipboardPasteCallback,
CurrentlyTypedWord.Callback
{
IReceiver _recv;
Autocapitalisation _autocap;
Suggestions _suggestions;
CurrentlyTypedWord _typedword;
/** 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;
/** Whether the space bar automatically enters the best suggestion. */
boolean _space_bar_auto_complete = false;
public KeyEventHandler(IReceiver recv, Config config)
{
_recv = recv;
Handler handler = recv.getHandler();
_autocap = new Autocapitalisation(handler,
this.new Autocapitalisation_callback());
_mods = Pointers.Modifiers.EMPTY;
_suggestions = new Suggestions(recv, config);
_typedword = new CurrentlyTypedWord(handler, this);
}
/** Editing just started. */
public void started(Config conf)
{
InputConnection ic = _recv.getCurrentInputConnection();
_autocap.started(conf, ic);
_typedword.started(conf, ic);
_suggestions.started();
_move_cursor_force_fallback =
conf.editor_config.should_move_cursor_force_fallback;
_space_bar_auto_complete = conf.space_bar_auto_complete;
clear_space_bar_state();
}
/** Selection has been updated. */
public void selection_updated(int oldSelStart, int newSelStart, int newSelEnd)
{
_autocap.selection_updated(oldSelStart, newSelStart);
_typedword.selection_updated(oldSelStart, newSelStart, newSelEnd);
}
/** 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 suggestion_entered(String text)
{
String old = _typedword.get();
int cur_rel = _typedword.cursor_relative();
replace_surrounding_text(old.length() + cur_rel, -cur_rel, text + " ");
last_replaced_word = old;
last_replacement_word_len = text.length() + 1;
}
@Override
public void paste_from_clipboard_pane(String content)
{
send_text(content);
}
@Override
public void currently_typed_word(String word)
{
_suggestions.currently_typed_word(word);
}
public void ime_subtype_changed()
{
// Refresh the suggestions immediately after dictionary changed.
_suggestions.currently_typed_word(_typedword.get());
}
/** 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);
_typedword.event_sent(eventCode, metaState);
clear_space_bar_state();
}
}
void send_text(String text)
{
InputConnection conn = _recv.getCurrentInputConnection();
if (conn == null)
return;
_autocap.typed(text);
_typedword.typed(text);
conn.commitText(text, 1);
clear_space_bar_state();
}
void replace_surrounding_text(int remove_before, int remove_after,
String new_text)
{
InputConnection conn = _recv.getCurrentInputConnection();
if (conn == null)
return;
conn.beginBatchEdit();
conn.deleteSurroundingText(remove_before, remove_after);
conn.commitText(new_text, 1);
conn.endBatchEdit();
}
/** 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(_typedword.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(_typedword.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;
case SPACE_BAR: handle_space_bar(); break;
case BACKSPACE: handle_backspace(); 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_no_modmap(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);
}
/** The word that was replaced by a suggestion when the last action was to
enter a suggestion (with the space bar or the candidates view) or [null]
otherwise. */
String last_replaced_word = null;
/** Length of the text before the cursor that should be replaced by
backspace. */
int last_replacement_word_len = 0;
void handle_space_bar()
{
if (_space_bar_auto_complete && _suggestions.best_suggestion != null
&& !_typedword.is_selection_not_empty())
{
suggestion_entered(_suggestions.best_suggestion);
}
else
{
send_text(" ");
}
}
void handle_backspace()
{
if (last_replaced_word != null)
{
replace_surrounding_text(last_replacement_word_len, 0,
last_replaced_word + " ");
last_replaced_word = null;
}
else
{
send_key_down_up(KeyEvent.KEYCODE_DEL);
}
}
void clear_space_bar_state()
{
last_replaced_word = null;
}
public static interface IReceiver extends Suggestions.Callback
{
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);
}
}
}