diff --git a/shell/platform/ohos/flutter_embedding/flutter/src/main/ets/embedding/engine/systemchannels/TextInputChannel.ets b/shell/platform/ohos/flutter_embedding/flutter/src/main/ets/embedding/engine/systemchannels/TextInputChannel.ets index c02c3ad005371a98e50fed4d23ca2b3852c89cd6..315a433f8491dd40e4f533287dfa5de49a49302f 100644 --- a/shell/platform/ohos/flutter_embedding/flutter/src/main/ets/embedding/engine/systemchannels/TextInputChannel.ets +++ b/shell/platform/ohos/flutter_embedding/flutter/src/main/ets/embedding/engine/systemchannels/TextInputChannel.ets @@ -20,6 +20,8 @@ import TextInputPlugin from '../../../plugin/editing/TextInputPlugin'; import Log from '../../../util/Log'; import DartExecutor from '../dart/DartExecutor'; import inputMethod from '@ohos.inputMethod'; +import ArrayList from '@ohos.util.ArrayList'; +import { TextEditingDelta, TextEditingDeltaJson } from '../../../plugin/editing/TextEditingDelta'; const TAG = "TextInputChannel"; @@ -56,6 +58,18 @@ export default class TextInputChannel { return state; } + createEditingDeltaJSON(batchDeltas: ArrayList): EditingDelta { + let deltas: Array = []; + batchDeltas.forEach((val, idx, array) => { + deltas.push(val.toJSON()); + }) + + let state: EditingDelta = { + deltas: deltas, + }; + return state; + } + /** * Instructs Flutter to update its text input editing state to reflect the given configuration. */ @@ -72,6 +86,12 @@ export default class TextInputChannel { this.channel.invokeMethod('TextInputClient.updateEditingState', [inputClientId, state]); } + updateEditingStateWithDeltas(inputClientId: number, batchDeltas: ArrayList): void { + Log.d(TAG, "updateEditingStateWithDeltas:" + "batchDeltas length: " + batchDeltas.length); + const state: ESObject = this.createEditingDeltaJSON(batchDeltas); + this.channel.invokeMethod('TextInputClient.updateEditingStateWithDeltas', [inputClientId, state]); + } + newline(inputClientId: number): void { Log.d(TAG, "Sending 'newline' message."); this.channel.invokeMethod("TextInputClient.performAction", [inputClientId, "TextInputAction.newline"]); @@ -124,7 +144,6 @@ export default class TextInputChannel { performPrivateCommand(inputClientId: number, action: string, data: ESObject) { } - } interface EditingState { @@ -135,6 +154,12 @@ interface EditingState { composingExtent: number; } + +interface EditingDelta { + deltas: Array; +} + + export interface TextInputMethodHandler { show(): void; @@ -175,12 +200,12 @@ export class Configuration { enableSuggestions: boolean, enableIMEPersonalizedLearning: boolean, enableDeltaModel: boolean, - textCapitalization:TextCapitalization, + textCapitalization: TextCapitalization, inputType: InputType, inputAction: Number, actionLabel: String, - autofill :boolean, - contentListString:[], + autofill: boolean, + contentListString: [], fields: Configuration[] ) { this.obscureText = obscureText; @@ -197,8 +222,9 @@ export class Configuration { this.fields = fields } + static getTextCapitalizationFromValue(encodedName: string): TextCapitalization { - let textKeys= ["CHARACTERS", "WORDS","SENTENCES", "NONE"]; + let textKeys = ["CHARACTERS", "WORDS", "SENTENCES", "NONE"]; for (let textKey of textKeys) { if (TextCapitalization[textKey] === encodedName) { return textKey as TextCapitalization; @@ -206,8 +232,9 @@ export class Configuration { } throw new Error("No such TextCapitalization: " + encodedName); } - private static inputActionFromTextInputAction(inputActionName:string):number{ - switch(inputActionName){ + + private static inputActionFromTextInputAction(inputActionName: string): number { + switch (inputActionName) { case "TextInputAction.previous": return inputMethod.EnterKeyType.PREVIOUS case "TextInputAction.unspecified": @@ -228,23 +255,24 @@ export class Configuration { } } - static fromJson(json:ESObject) { - const inputActionName:string = json.inputAction; + + static fromJson(json: ESObject) { + const inputActionName: string = json.inputAction; if (!inputActionName) { throw new Error("Configuration JSON missing 'inputAction' property."); } - let fields:Array = new Array(); + let fields: Array = new Array(); if (json.fields !== null && json.fields !== undefined) { - fields = json.fields.map((field:ESObject):ESObject => Configuration.fromJson(field)); + fields = json.fields.map((field: ESObject): ESObject => Configuration.fromJson(field)); } - const inputAction:number = Configuration.inputActionFromTextInputAction(inputActionName); + const inputAction: number = Configuration.inputActionFromTextInputAction(inputActionName); // Build list of content commit mime types from the data in the JSON list. - const contentList:Array = []; + const contentList: Array = []; if (json.contentCommitMimeTypes !== null && json.contentCommitMimeTypes !== undefined) { - json.contentCommitMimeTypes.forEach((type:ESObject) => { + json.contentCommitMimeTypes.forEach((type: ESObject) => { contentList.push(type); }); } @@ -263,7 +291,6 @@ export class Configuration { fields ); } - } enum TextCapitalization { @@ -304,7 +331,7 @@ export class InputType { } static getTextInputTypeFromValue(encodedName: string): TextInputType { - let textKeys = [ "TEXT", "DATETIME","NAME" , "POSTAL_ADDRESS", "NUMBER", "PHONE", "MULTILINE", "EMAIL_ADDRESS", "URL", "VISIBLE_PASSWORD", "NONE" ,]; + let textKeys = ["TEXT", "DATETIME", "NAME", "POSTAL_ADDRESS", "NUMBER", "PHONE", "MULTILINE", "EMAIL_ADDRESS", "URL", "VISIBLE_PASSWORD", "NONE",]; for (let textKey of textKeys) { if (TextInputType[textKey] === encodedName) { return textKey as TextInputType; @@ -403,7 +430,7 @@ class TextInputCallback implements MethodCallHandler { break; case "TextInput.setClient": const textInputClientId: number = args[0] as number; - const jsonConfiguration :string = args[1] ; + const jsonConfiguration: string = args[1]; const config: Configuration | null = Configuration.fromJson(jsonConfiguration); this.textInputMethodHandler.setClient(textInputClientId, config); @@ -442,5 +469,4 @@ class TextInputCallback implements MethodCallHandler { break; } } - } diff --git a/shell/platform/ohos/flutter_embedding/flutter/src/main/ets/plugin/editing/ListenableEditingState.ets b/shell/platform/ohos/flutter_embedding/flutter/src/main/ets/plugin/editing/ListenableEditingState.ets index 953f7cad09b96e97f5bbc45ee519cf70358d2eed..0726ab0fb43398a1d60062f472484c3f6b3601d5 100644 --- a/shell/platform/ohos/flutter_embedding/flutter/src/main/ets/plugin/editing/ListenableEditingState.ets +++ b/shell/platform/ohos/flutter_embedding/flutter/src/main/ets/plugin/editing/ListenableEditingState.ets @@ -19,10 +19,11 @@ import inputMethod from '@ohos.inputMethod'; import ArrayList from '@ohos.util.ArrayList'; import { TextEditingDelta } from './TextEditingDelta'; import TextInputChannel from '../../embedding/engine/systemchannels/TextInputChannel'; -const TAG = "ListenableEditingState"; -export class ListenableEditingState { - private TextInputChannel: TextInputChannel | null = null ; +const TAG = "ListenableEditingState"; + +export class ListenableEditingState { + private TextInputChannel: TextInputChannel | null = null; private client: number = 0 //Cache used to storage software keyboard input action private mStringCache: string; @@ -56,6 +57,34 @@ export class ListenableEditingState { this.mComposingEndCache = -1; } + extractBatchTextEditingDeltas(): ArrayList { + let currentBatchDeltas = new ArrayList(); + this.mBatchTextEditingDeltas.forEach((data) => { + currentBatchDeltas.add(data); + }) + this.mBatchTextEditingDeltas.clear(); + return currentBatchDeltas; + } + + clearBatchDeltas(): void { + this.mBatchTextEditingDeltas.clear(); + } + + replace(start: number, end: number, tb: String, tbStart: number, tbEnd: number): void { + const placeIndex = this.mSelectionStartCache < this.mSelectionEndCache ? this.mSelectionStartCache : this.mSelectionEndCache; + + this.mBatchTextEditingDeltas.add( + new TextEditingDelta( + this.mStringCache.toString(), + placeIndex + tbEnd, + placeIndex + tbEnd, + this.getComposingStart(), + this.getComposingEnd(), + start, + end + tbStart, + tb.toString() + )); + } getSelectionStart(): number { return this.mSelectionStartCache; @@ -116,20 +145,26 @@ export class ListenableEditingState { } handleInsertTextEvent(text: string): void { - if(this.mStringCache.length == this.mSelectionStartCache) { + let start = this.mSelectionStartCache < this.mSelectionEndCache ? this.mSelectionStartCache : this.mSelectionEndCache; + let end = this.mSelectionStartCache > this.mSelectionEndCache ? this.mSelectionStartCache : this.mSelectionEndCache; + const length = text.length; + this.replace(start, end, text, 0, length); + + if (this.mStringCache.length == this.mSelectionStartCache) { //Insert text one by one - this.mStringCache += text; + let tempStr: string = this.mStringCache.substring(0, start) + text + this.mStringCache.substring(end); + this.mStringCache = tempStr; this.setSelectionStart(this.mStringCache.length); this.setSelectionEnd(this.mStringCache.length); - } else if(this.mStringCache.length > this.mSelectionStartCache) { + } else if (this.mStringCache.length > this.mSelectionStartCache) { //Insert text in the middle of string - let tempStr: string = this.mStringCache.substring(0, this.mSelectionStartCache) + text + this.mStringCache.substring(this.mSelectionStartCache); + let tempStr: string = this.mStringCache.substring(0, start) + text + this.mStringCache.substring(end); this.mStringCache = tempStr; - this.mSelectionStartCache += text.length; + this.mSelectionStartCache = start + text.length; this.mSelectionEndCache = this.mSelectionStartCache; } - if(this.mListeners == null) { + if (this.mListeners == null) { Log.e(TAG, "mListeners is null"); return; } @@ -139,7 +174,7 @@ export class ListenableEditingState { updateTextInputState(state: TextEditState): void { this.beginBatchEdit(); this.setStringCache(state.text); - if(state.hasSelection()) { + if (state.hasSelection()) { this.setSelectionStart(state.selectionStart); this.setSelectionEnd(state.selectionEnd); } else { @@ -217,22 +252,30 @@ export class ListenableEditingState { } handleDeleteEvent(leftOrRight: boolean, length: number): void { - if(leftOrRight == false) { + let start = this.mSelectionStartCache < this.mSelectionEndCache ? this.mSelectionStartCache : this.mSelectionEndCache; + let end = this.mSelectionStartCache > this.mSelectionEndCache ? this.mSelectionStartCache : this.mSelectionEndCache; + + if (leftOrRight == false) { //delete left - if(this.mSelectionStartCache == 0) { + if (start == 0 && end == 0) { return; } - this.mSelectionStartCache -= length; - let tempStr: string = this.mStringCache.slice(0, this.mSelectionStartCache) + this.mStringCache.slice(this.mSelectionStartCache + length); + const startData = start == end ? start - 1 : start; + const tbEnd = start == end ? -1 : 0; + this.replace(startData, end, "", 0, tbEnd); + this.mSelectionStartCache = startData; + let tempStr: string = this.mStringCache.slice(0, startData) + this.mStringCache.slice(end); this.mStringCache = tempStr; this.mSelectionEndCache = this.mSelectionStartCache; - } else if(leftOrRight == true) { + } else if (leftOrRight == true) { //delete right - if(this.mSelectionStartCache == this.mStringCache.length) { + if (start == this.mStringCache.length) { return; } - this.mSelectionEndCache += length; - let tempStr: string = this.mStringCache.slice(0,this.mSelectionStartCache) + this.mStringCache.slice(this.mSelectionEndCache); + const tbStart = start == end ? 1 : 0; + this.replace(start, end, "", tbStart, 0); + this.mSelectionEndCache = start; + let tempStr: string = this.mStringCache.slice(0, start) + (end + 1 >= this.mStringCache.length ? "" : this.mStringCache.slice(end + 1)); this.mStringCache = tempStr; this.mSelectionStartCache = this.mSelectionEndCache; } diff --git a/shell/platform/ohos/flutter_embedding/flutter/src/main/ets/plugin/editing/TextEditingDelta.ets b/shell/platform/ohos/flutter_embedding/flutter/src/main/ets/plugin/editing/TextEditingDelta.ets index 1aa1b2d87245f029c1b8a630fecf02bceb41ea8d..495c4a51821d5b9279c56dc134138e5ccb17b41f 100644 --- a/shell/platform/ohos/flutter_embedding/flutter/src/main/ets/plugin/editing/TextEditingDelta.ets +++ b/shell/platform/ohos/flutter_embedding/flutter/src/main/ets/plugin/editing/TextEditingDelta.ets @@ -38,11 +38,11 @@ export class TextEditingDelta { this.newSelectionEnd = selectionEnd; this.newComposingStart = composingStart; this.newComposingEnd = composingEnd; - if(replacementDestinationStart === undefined || + if (replacementDestinationStart === undefined || replacementDestinationEnd === undefined || replacementSource === undefined) { this.setDeltas(oldEditable, "", -1, -1); - } else { + } else { this.setDeltas( oldEditable, replacementSource, @@ -58,4 +58,30 @@ export class TextEditingDelta { this.deltaStart = newStart; this.deltaEnd = newExtent; } + + toJSON(): TextEditingDeltaJson { + let state: TextEditingDeltaJson = { + oldText: this.oldText.toString(), + deltaText: this.deltaText.toString(), + deltaStart: this.deltaStart, + deltaEnd: this.deltaEnd, + selectionBase: this.newSelectionStart, + selectionExtent: this.newSelectionEnd, + composingBase: this.newComposingStart, + composingExtent: this.newComposingEnd, + }; + return state; + } +} + + +export interface TextEditingDeltaJson { + oldText: string; + deltaText: string; + deltaStart: number; + deltaEnd: number; + selectionBase: number; + selectionExtent: number; + composingBase: number; + composingExtent: number; } diff --git a/shell/platform/ohos/flutter_embedding/flutter/src/main/ets/plugin/editing/TextInputPlugin.ets b/shell/platform/ohos/flutter_embedding/flutter/src/main/ets/plugin/editing/TextInputPlugin.ets index ca7f22a339dd13c6b6fcf4c06a1a1b4f40604e16..9252bf0b0242a9e09e579d883b4546f25e36bf6d 100644 --- a/shell/platform/ohos/flutter_embedding/flutter/src/main/ets/plugin/editing/TextInputPlugin.ets +++ b/shell/platform/ohos/flutter_embedding/flutter/src/main/ets/plugin/editing/TextInputPlugin.ets @@ -13,14 +13,17 @@ * limitations under the License. */ -import TextInputChannel, { Configuration, TextEditState, +import TextInputChannel, { + Configuration, + TextEditState, TextInputMethodHandler, - TextInputType } from '../../embedding/engine/systemchannels/TextInputChannel'; + TextInputType +} from '../../embedding/engine/systemchannels/TextInputChannel'; import inputMethod from '@ohos.inputMethod'; import Log from '../../util/Log'; import { EditingStateWatcher, ListenableEditingState } from './ListenableEditingState'; -export default class TextInputPlugin implements EditingStateWatcher{ +export default class TextInputPlugin implements EditingStateWatcher { private static TAG = "TextInputPlugin"; private textInputChannel: TextInputChannel; private mTextInputHandler: TextInputMethodHandlerImpl; @@ -42,19 +45,25 @@ export default class TextInputPlugin implements EditingStateWatcher{ didChangeEditingState(textChanged: boolean, selectionChanged: boolean, composingRegionChanged: boolean): void { let editable = this.mTextInputHandler.mEditable; let inputTarget = this.mTextInputHandler.inputTarget; - this.textInputChannel.updateEditingState(inputTarget.id, editable.getStringCache(), - editable.getSelectionStart(), editable.getSelectionEnd(), - editable.getComposingStart(), editable.getComposingEnd()) + let configuration = this.mTextInputHandler.configuration; + if (configuration!= null && configuration.enableDeltaModel) { + this.textInputChannel.updateEditingStateWithDeltas(inputTarget.id,editable.extractBatchTextEditingDeltas()); + editable.clearBatchDeltas(); + } else { + this.textInputChannel.updateEditingState(inputTarget.id, editable.getStringCache(), + editable.getSelectionStart(), editable.getSelectionEnd(), + editable.getComposingStart(), editable.getComposingEnd()) + } + } detach(): void { this.mTextInputHandler.inputMethodController.detach((err) => { - if(err) { + if (err) { Log.e(TextInputPlugin.TAG, "Failed to detach: " + JSON.stringify(err)); } }) } - } class TextInputMethodHandlerImpl implements TextInputMethodHandler { @@ -62,7 +71,7 @@ class TextInputMethodHandlerImpl implements TextInputMethodHandler { private textConfig: inputMethod.TextConfig; inputMethodController: inputMethod.InputMethodController; inputTarget: InputTarget; - private configuration: Configuration | null = null; + public configuration: Configuration | null = null; mEditable: ListenableEditingState; private mRestartInputPending: boolean = false; private plugin: EditingStateWatcher | ESObject; @@ -74,7 +83,7 @@ class TextInputMethodHandlerImpl implements TextInputMethodHandler { inputAttribute: { textInputType: 0, enterKeyType: 1 - }}; + } }; this.plugin = plugin; this.mEditable = new ListenableEditingState(null, 0); this.inputMethodController = inputMethod.getController(); @@ -82,7 +91,7 @@ class TextInputMethodHandlerImpl implements TextInputMethodHandler { } show(): void { - if(this.keyboardStatus == KeyboardStatus.SHOW){ + if (this.keyboardStatus == KeyboardStatus.SHOW) { return; } this.showTextInput(); @@ -103,7 +112,7 @@ class TextInputMethodHandlerImpl implements TextInputMethodHandler { } setClient(textInputClientId: number, configuration: Configuration | null): void { - Log.d(TextInputMethodHandlerImpl.TAG,"textInputClientId: " + textInputClientId); + Log.d(TextInputMethodHandlerImpl.TAG, "textInputClientId: " + textInputClientId); this.setTextInputClient(textInputClientId, configuration); } @@ -116,7 +125,7 @@ class TextInputMethodHandlerImpl implements TextInputMethodHandler { } setEditingState(editingState: TextEditState): void { - Log.d(TextInputMethodHandlerImpl.TAG, "text:" + editingState.text +" selectionStart:" + editingState.selectionStart + " selectionEnd:" + Log.d(TextInputMethodHandlerImpl.TAG, "text:" + editingState.text + " selectionStart:" + editingState.selectionStart + " selectionEnd:" + editingState.selectionEnd + " composingStart:" + editingState.composingStart + " composingEnd" + editingState.composingEnd); this.mEditable.updateTextInputState(editingState); } @@ -127,10 +136,10 @@ class TextInputMethodHandlerImpl implements TextInputMethodHandler { private async showTextInput(): Promise { await this.attach(true); - if(this.imcFlag != true) { + if (this.imcFlag != true) { this.listenKeyBoardEvent(); } - this.inputMethodController.showTextInput().then(()=> { + this.inputMethodController.showTextInput().then(() => { Log.d(TextInputMethodHandlerImpl.TAG, "Succeeded in showing softKeyboard"); }).catch((err: ESObject) => { Log.e(TextInputMethodHandlerImpl.TAG, "Failed to show softKeyboard:" + JSON.stringify(err)); @@ -145,7 +154,7 @@ class TextInputMethodHandlerImpl implements TextInputMethodHandler { }) } - async attach(showKeyboard: boolean): Promise { + async attach(showKeyboard: boolean): Promise { try { await this.inputMethodController.attach(showKeyboard, this.textConfig); } catch (err) { @@ -154,17 +163,15 @@ class TextInputMethodHandlerImpl implements TextInputMethodHandler { } setTextInputClient(client: number, configuration: Configuration | null): void { - const INPUT_TYPE_NAME = ['NONE', 'TEXT', 'MULTILINE', 'NUMBER', 'PHONE','DATETIME','EMAIL_ADDRESS','URL','VISIBLE_PASSWORD'] - if(configuration) - { + const INPUT_TYPE_NAME = ['NONE', 'TEXT', 'MULTILINE', 'NUMBER', 'PHONE', 'DATETIME', 'EMAIL_ADDRESS', 'URL', 'VISIBLE_PASSWORD'] + if (configuration) { this.configuration = configuration; - if(configuration.inputType) - { - this.textConfig.inputAttribute.textInputType =INPUT_TYPE_NAME.indexOf(configuration.inputType.type) + if (configuration.inputType) { + this.textConfig.inputAttribute.textInputType = INPUT_TYPE_NAME.indexOf(configuration.inputType.type) this.textConfig.inputAttribute.enterKeyType = configuration.inputAction as ESObject; } } - if(this.canShowTextInput()) { + if (this.canShowTextInput()) { this.inputTarget = new InputTarget(Type.FRAMEWORK_CLIENT, client); } else { this.inputTarget = new InputTarget(Type.NO_TARGET, client); @@ -178,7 +185,7 @@ class TextInputMethodHandlerImpl implements TextInputMethodHandler { } canShowTextInput(): boolean { - if(this.configuration == null || this.configuration.inputType == null) { + if (this.configuration == null || this.configuration.inputType == null) { return true; } return this.configuration.inputType.type != TextInputType.NONE; @@ -228,7 +235,7 @@ class TextInputMethodHandlerImpl implements TextInputMethodHandler { try { this.inputMethodController.on('sendKeyboardStatus', (state) => { - if(state == KeyboardStatus.HIDE) { + if (state == KeyboardStatus.HIDE) { this.keyboardStatus = KeyboardStatus.HIDE; this.plugin.textInputChannel.onConnectionClosed(this.inputTarget.id); } @@ -261,7 +268,7 @@ class TextInputMethodHandlerImpl implements TextInputMethodHandler { } public clearTextInputClient(): void { - if(this.inputTarget.type == Type.VIRTUAL_DISPLAY_PLATFORM_VIEW) { + if (this.inputTarget.type == Type.VIRTUAL_DISPLAY_PLATFORM_VIEW) { return; } this.mEditable.removeEditingStateListener(this.plugin); @@ -295,5 +302,4 @@ export class InputTarget { this.type = type; this.id = id; } - } \ No newline at end of file diff --git a/shell/platform/ohos/flutter_embedding/flutter/src/main/ets/util/Log.ets b/shell/platform/ohos/flutter_embedding/flutter/src/main/ets/util/Log.ets index 437b52f18049b9ba77e47350033482a06368c5c4..33796b25fe8d344ceb1f83eeefc36b210f5f5711 100644 --- a/shell/platform/ohos/flutter_embedding/flutter/src/main/ets/util/Log.ets +++ b/shell/platform/ohos/flutter_embedding/flutter/src/main/ets/util/Log.ets @@ -91,6 +91,12 @@ export default class Log { */ static e(tag: string, format: string, ...args: ESObject[]) { if (Log.isLoggable(HiLog.LogLevel.ERROR)) { + args.forEach((item: ESObject, index: number) => { + if (item instanceof Error) { + args[index] = item.message + item.stack; + } + format+="%{public}s"; + }) HiLog.error(DOMAIN, TAG, tag + SYMBOL + format, args); } }