Compare commits

..

7 Commits

Author SHA1 Message Date
Jules Aguillon
3d1383adfa Add a delay after a Keyevent key in a macro
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.
2025-03-08 13:52:56 +01:00
Jules Aguillon
80bc21c4af Construct a single handler 2025-03-08 13:47:55 +01:00
Spike
5e77fa84cf doc: Massage section on "Escape codes" (#912)
Some checks failed
Make Apk CI / Build-Apk (push) Has been cancelled
Check translations / check-translations (push) Has been cancelled
Check layouts / check_layout.output (push) Has been cancelled
Check layouts / Generated files (push) Has been cancelled
* Add introductory text to the tables

And mention characters that don't have escapes

* Turn tables around

Tables should be structured by what the user wants, not by what the code does.

* Address Julow review #1

- Merge tables, no matter which rule requires escaping; "in the usual way for XML" applies to both
- 3 escapes not mandatory removed from table to new ¶ below
- Found one more symbol → legend

* doc: Clarify escaping of comma and colon per #915
2025-03-08 12:11:54 +01:00
Jules Aguillon
06fbc83c9c sync_translations.py: Handle '\n' in store descriptions
These are added by Weblate.
2025-03-08 12:08:57 +01:00
Jules Aguillon
a123810247 Change indentation of strings.xml files to match Weblate
This runs sync_translations.py, which also remove uneeded comments.
2025-03-08 12:05:35 +01:00
Jules Aguillon
906755b787 prefs: Add 'delete_word' and 'forward_delete_word' to extra keys
Some checks failed
Make Apk CI / Build-Apk (push) Has been cancelled
Check translations / check-translations (push) Has been cancelled
Check layouts / check_layout.output (push) Has been cancelled
Check layouts / Generated files (push) Has been cancelled
The gesture combination is mentioned. Preferred position are added.
2025-03-01 16:52:46 +01:00
Jules Aguillon
80c52460c7 Add 'delete_word' and 'forward_delete_word' keys
These keys are the equivalent of ctrl+backspace and ctrl+delete,
respectively.

They can be reached with Gesture+backspace and Gesture+delete
respectively.
2025-03-01 16:52:46 +01:00
23 changed files with 2429 additions and 2376 deletions

View File

@@ -25,31 +25,30 @@ Key values can be any of the following:
+ `⏯:keyevent:85` A play/pause key (which has no effect in most apps). + `⏯: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 + `my@:'my.email@domain.com'` A key that sends an arbitrary string
- A macro, `symbol:key_def1,key_def2,...`. - A macro, `legend:key_def1,key_def2,...`.
This results in a key that behaves as if the sequence of `key_def` had been pressed in order. This results in a key with legend `legend` that behaves as if the sequence of `key_def` had been pressed in order.
Examples: Examples:
+ `CA:ctrl,a,ctrl,c` A key with legend CA that sends the sequence `ctrl+a`, `ctrl+c`. + `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`. + `Cd:ctrl,backspace` A key with legend Cd that sends the shortcut `ctrl+backspace`.
## Escape codes ### Escape codes
Value | Escape code for
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
:---- | :------ :---- | :------
`\?` | `?` 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. `<` | `&lt;`
`\\` | `\` `>` | `&gt;`
`"` | `&quot;`
XML escape codes also work, including: The characters `?`, `#`, and `@` do not need to be escaped when writing custom layouts. Internally, they can be escaped by prepending backslash (by typing `\?`, `\#`, and `\@`).
Value | Escape code for 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.
:------- | :------
`&amp;` | `&`
`&lt;` | `<`
`&gt;` | `>`
`&quot;` | `"`
## Modifiers ## Modifiers
System modifiers are sent to the app, which can take app-specific action. System modifiers are sent to the app, which can take app-specific action.

View File

@@ -1,7 +1,6 @@
package juloo.keyboard2; package juloo.keyboard2;
import android.os.Handler; import android.os.Handler;
import android.os.Looper;
import android.text.InputType; import android.text.InputType;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.inputmethod.EditorInfo; import android.view.inputmethod.EditorInfo;
@@ -27,9 +26,9 @@ public final class Autocapitalisation
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES |
InputType.TYPE_TEXT_FLAG_CAP_WORDS; InputType.TYPE_TEXT_FLAG_CAP_WORDS;
public Autocapitalisation(Looper looper, Callback cb) public Autocapitalisation(Handler h, Callback cb)
{ {
_handler = new Handler(looper); _handler = h;
_callback = cb; _callback = cb;
} }

View File

@@ -2,6 +2,7 @@ package juloo.keyboard2;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.os.Looper; import android.os.Looper;
import android.os.Handler;
import android.text.InputType; import android.text.InputType;
import android.view.KeyCharacterMap; import android.view.KeyCharacterMap;
import android.view.KeyEvent; import android.view.KeyEvent;
@@ -28,10 +29,10 @@ public final class KeyEventHandler
[setSelection] could be used instead. */ [setSelection] could be used instead. */
boolean _move_cursor_force_fallback = false; boolean _move_cursor_force_fallback = false;
public KeyEventHandler(Looper looper, IReceiver recv) public KeyEventHandler(IReceiver recv)
{ {
_recv = recv; _recv = recv;
_autocap = new Autocapitalisation(looper, _autocap = new Autocapitalisation(recv.getHandler(),
this.new Autocapitalisation_callback()); this.new Autocapitalisation_callback());
_mods = Pointers.Modifiers.EMPTY; _mods = Pointers.Modifiers.EMPTY;
} }
@@ -324,32 +325,73 @@ public final class KeyEventHandler
void evaluate_macro(KeyValue[] keys) void evaluate_macro(KeyValue[] keys)
{ {
final Pointers.Modifiers empty = Pointers.Modifiers.EMPTY; if (keys.length == 0)
return;
// Ignore modifiers that are activated at the time the macro is evaluated // Ignore modifiers that are activated at the time the macro is evaluated
mods_changed(empty); mods_changed(Pointers.Modifiers.EMPTY);
Pointers.Modifiers mods = empty; evaluate_macro_loop(keys, 0, Pointers.Modifiers.EMPTY, _autocap.pause());
final boolean autocap_paused = _autocap.pause(); }
for (KeyValue kv : keys)
/** 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)
{ {
kv = KeyModifier.modify(kv, mods);
if (kv == null)
continue;
if (kv.hasFlagsAny(KeyValue.FLAG_LATCH)) if (kv.hasFlagsAny(KeyValue.FLAG_LATCH))
{ {
// Non-special latchable keys clear latched modifiers // Non-special latchable keys clear latched modifiers
if (!kv.hasFlagsAny(KeyValue.FLAG_SPECIAL)) if (!kv.hasFlagsAny(KeyValue.FLAG_SPECIAL))
mods = empty; mods = Pointers.Modifiers.EMPTY;
mods = mods.with_extra_mod(kv); mods = mods.with_extra_mod(kv);
} }
else else
{ {
key_down(kv, false); key_down(kv, false);
key_up(kv, mods); key_up(kv, mods);
mods = empty; mods = Pointers.Modifiers.EMPTY;
} }
should_delay = wait_after_macro_key(kv);
} }
i++;
if (i >= keys.length) // Stop looping
{
_autocap.unpause(autocap_paused); _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]. */ /** Repeat calls to [send_key_down_up]. */
void send_key_down_up_repeat(int event_code, int repeat) void send_key_down_up_repeat(int event_code, int repeat)
@@ -364,6 +406,7 @@ public final class KeyEventHandler
public void set_shift_state(boolean state, boolean lock); public void set_shift_state(boolean state, boolean lock);
public void set_compose_pending(boolean pending); public void set_compose_pending(boolean pending);
public InputConnection getCurrentInputConnection(); public InputConnection getCurrentInputConnection();
public Handler getHandler();
} }
class Autocapitalisation_callback implements Autocapitalisation.Callback class Autocapitalisation_callback implements Autocapitalisation.Callback

View File

@@ -6,6 +6,7 @@ import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.inputmethodservice.InputMethodService; import android.inputmethodservice.InputMethodService;
import android.os.Build.VERSION; import android.os.Build.VERSION;
import android.os.Handler;
import android.os.IBinder; import android.os.IBinder;
import android.text.InputType; import android.text.InputType;
import android.util.Log; import android.util.Log;
@@ -38,6 +39,7 @@ public class Keyboard2 extends InputMethodService
private ViewGroup _emojiPane = null; private ViewGroup _emojiPane = null;
private ViewGroup _clipboard_pane = null; private ViewGroup _clipboard_pane = null;
public int actionId; // Action performed by the Action key. public int actionId; // Action performed by the Action key.
private Handler _handler;
private Config _config; private Config _config;
@@ -107,7 +109,8 @@ public class Keyboard2 extends InputMethodService
{ {
super.onCreate(); super.onCreate();
SharedPreferences prefs = DirectBootAwarePreferences.get_shared_preferences(this); SharedPreferences prefs = DirectBootAwarePreferences.get_shared_preferences(this);
_keyeventhandler = new KeyEventHandler(getMainLooper(), this.new Receiver()); _handler = new Handler(getMainLooper());
_keyeventhandler = new KeyEventHandler(this.new Receiver());
Config.initGlobalConfig(prefs, getResources(), _keyeventhandler); Config.initGlobalConfig(prefs, getResources(), _keyeventhandler);
prefs.registerOnSharedPreferenceChangeListener(this); prefs.registerOnSharedPreferenceChangeListener(this);
_config = Config.globalConfig(); _config = Config.globalConfig();
@@ -481,6 +484,11 @@ public class Keyboard2 extends InputMethodService
{ {
return Keyboard2.this.getCurrentInputConnection(); return Keyboard2.this.getCurrentInputConnection();
} }
public Handler getHandler()
{
return _handler;
}
} }
private IBinder getConnectionToken() private IBinder getConnectionToken()

View File

@@ -74,7 +74,11 @@ def sync_metadata(value_dir, strings):
os.makedirs(meta_dir) os.makedirs(meta_dir)
txt_file = os.path.join(meta_dir, fname) txt_file = os.path.join(meta_dir, fname)
with open(txt_file, "w", encoding="utf-8") as out: with open(txt_file, "w", encoding="utf-8") as out:
out.write(string.text.removeprefix('"').removesuffix('"')) out.write(string.text
.replace("\\n", "\n")
.replace("\\'", "'")
.removeprefix('"')
.removesuffix('"'))
out.write("\n") out.write("\n")
sync_meta_file("title.txt", ("app_name_release", None)) sync_meta_file("title.txt", ("app_name_release", None))
sync_meta_file("short_description.txt", ("short_description", None)) sync_meta_file("short_description.txt", ("short_description", None))