keyboard: Add 'delete' OSK key action

This action will replace CLUTTER_KEY_Backspace emission for
the OSK backspace key. Following the available mockups, implement
different modes of operation:

- Single tap deletes a single character
- Long tap starts deleting characters one by one
- Longer tap switches to word-by-word deletion

This is made possible via the input method surrounding text,
inspecting the string to look the previous char/word position
backwards, and relies on IM focus providing enough context.

Since deleting text and getting surrounding text are both
async operations, we make one happen after the other, until
the button is released.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2278>
This commit is contained in:
Carlos Garnacho 2022-04-20 23:18:27 +02:00 committed by Florian Müllner
parent 482e62cb75
commit fce376939f
2 changed files with 102 additions and 0 deletions

View File

@ -311,4 +311,8 @@ var InputMethod = GObject.registerClass({
getSurroundingText() { getSurroundingText() {
return [this._surroundingText, this._surroundingTextCursor]; return [this._surroundingText, this._surroundingTextCursor];
} }
hasPreedit() {
return this._preeditVisible && this._preeditStr !== '' && this._preeditStr !== null;
}
}); });

View File

@ -25,6 +25,7 @@ const SHOW_KEYBOARD = 'screen-keyboard-enabled';
const KEY_SIZE = 2; const KEY_SIZE = 2;
const KEY_RELEASE_TIMEOUT = 50; const KEY_RELEASE_TIMEOUT = 50;
const BACKSPACE_WORD_DELETE_THRESHOLD = 50;
var AspectContainer = GObject.registerClass( var AspectContainer = GObject.registerClass(
class AspectContainer extends St.Widget { class AspectContainer extends St.Widget {
@ -1526,6 +1527,9 @@ var Keyboard = GObject.registerClass({
this._toggleEmoji(); this._toggleEmoji();
} else if (key.action === 'modifier') { } else if (key.action === 'modifier') {
this._toggleModifier(key.keyval); this._toggleModifier(key.keyval);
} else if (key.action === 'delete') {
this._toggleDelete(true);
this._toggleDelete(false);
} else if (!this._longPressed && key.action === 'levelSwitch') { } else if (!this._longPressed && key.action === 'levelSwitch') {
this._setActiveLayer(key.level); this._setActiveLayer(key.level);
this._setLatched( this._setLatched(
@ -1549,6 +1553,11 @@ var Keyboard = GObject.registerClass({
} }
} }
if (key.action === 'delete') {
button.connect('long-press',
() => this._toggleDelete(true));
}
if (key.action === 'modifier') { if (key.action === 'modifier') {
let modifierKeys = this._modifierKeys[key.keyval] || []; let modifierKeys = this._modifierKeys[key.keyval] || [];
modifierKeys.push(button); modifierKeys.push(button);
@ -1562,6 +1571,95 @@ var Keyboard = GObject.registerClass({
} }
} }
_previousWordPosition(text, cursor) {
/* Skip word prior to cursor */
let pos = Math.max(0, text.slice(0, cursor).search(/\s+\S+\s*$/));
if (pos < 0)
return 0;
/* Skip contiguous spaces */
for (; pos >= 0; pos--) {
if (text.charAt(pos) !== ' ')
return GLib.utf8_strlen(text.slice(0, pos + 1), -1);
}
return 0;
}
_toggleDelete(enabled) {
if (this._deleteEnabled === enabled)
return;
this._deleteEnabled = enabled;
this._timesDeleted = 0;
if (!Main.inputMethod.currentFocus || Main.inputMethod.hasPreedit()) {
/* If there is no IM focus or are in the middle of preedit,
* fallback to keypresses */
if (enabled)
this._keyboardController.keyvalPress(Clutter.KEY_BackSpace);
else
this._keyboardController.keyvalRelease(Clutter.KEY_BackSpace);
return;
}
if (enabled) {
let func = (text, cursor) => {
if (cursor === 0)
return;
let encoder = new TextEncoder();
let decoder = new TextDecoder();
/* Find cursor/anchor position in characters */
const cursorIdx = GLib.utf8_strlen(decoder.decode(encoder.encode(
text).slice(0, cursor)), -1);
const anchorIdx = this._timesDeleted < BACKSPACE_WORD_DELETE_THRESHOLD
? cursorIdx - 1
: this._previousWordPosition(text, cursor);
/* Now get offset from cursor */
const offset = anchorIdx - cursorIdx;
this._timesDeleted++;
Main.inputMethod.delete_surrounding(offset, Math.abs(offset));
};
this._surroundingUpdateId = Main.inputMethod.connect(
'surrounding-text-set', () => {
let [text, cursor] = Main.inputMethod.getSurroundingText();
if (this._timesDeleted === 0) {
func(text, cursor);
} else {
if (this._surroundingUpdateTimeoutId > 0) {
GLib.source_remove(this._surroundingUpdateTimeoutId);
this._surroundingUpdateTimeoutId = 0;
}
this._surroundingUpdateTimeoutId =
GLib.timeout_add(GLib.PRIORITY_DEFAULT, KEY_RELEASE_TIMEOUT, () => {
func(text, cursor);
this._surroundingUpdateTimeoutId = 0;
return GLib.SOURCE_REMOVE;
});
}
});
let [text, cursor] = Main.inputMethod.getSurroundingText();
if (text)
func(text, cursor);
else
Main.inputMethod.request_surrounding();
} else {
if (this._surroundingUpdateId > 0) {
Main.inputMethod.disconnect(this._surroundingUpdateId);
this._surroundingUpdateId = 0;
}
if (this._surroundingUpdateTimeoutId > 0) {
GLib.source_remove(this._surroundingUpdateTimeoutId);
this._surroundingUpdateTimeoutId = 0;
}
}
}
_setLatched(latched) { _setLatched(latched) {
this._latched = latched; this._latched = latched;
this._setCurrentLevelLatched(this._currentPage, this._latched); this._setCurrentLevelLatched(this._currentPage, this._latched);