mirror of
https://github.com/darkrenaissance/darkfi.git
synced 2026-01-06 21:34:00 -05:00
427 lines
16 KiB
Java
427 lines
16 KiB
Java
/* This file is part of DarkFi (https://dark.fi)
|
|
*
|
|
* Copyright (C) 2020-2026 Dyne.org foundation
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package autosuggest;
|
|
|
|
import android.content.Context;
|
|
import android.text.Editable;
|
|
import android.text.Selection;
|
|
import android.util.Log;
|
|
import android.view.KeyEvent;
|
|
import android.view.inputmethod.BaseInputConnection;
|
|
import android.view.inputmethod.EditorInfo;
|
|
import android.view.View;
|
|
import android.view.inputmethod.ExtractedText;
|
|
import android.view.inputmethod.ExtractedTextRequest;
|
|
import android.view.inputmethod.SurroundingText;
|
|
import android.view.inputmethod.InputMethodManager;
|
|
|
|
public class CustomInputConnection extends BaseInputConnection {
|
|
private static final boolean DEBUG = false;
|
|
private int id = -1;
|
|
|
|
private View mInternalView;
|
|
private Editable mEditable;
|
|
private boolean mSingleLine;
|
|
private int numBatchEdits;
|
|
private boolean shouldUpdateImeSelection;
|
|
|
|
native static void onCompose(int id, String text, int newCursorPos, boolean isCommit);
|
|
native static void onSetComposeRegion(int id, int start, int end);
|
|
native static void onFinishCompose(int id);
|
|
native static void onDeleteSurroundingText(int id, int left, int right);
|
|
|
|
public CustomInputConnection(int id, Editable editable, View view) {
|
|
super(view, true);
|
|
log("CustomInputConnection()");
|
|
this.id = id;
|
|
mEditable = editable;
|
|
mInternalView = view;
|
|
mSingleLine = false;
|
|
}
|
|
|
|
private void log(String fstr, Object... args) {
|
|
if (!DEBUG) return;
|
|
String text = "(" + id + "): " + String.format(fstr, args);
|
|
Log.d("darkfi", text);
|
|
}
|
|
|
|
/**
|
|
* Updates the AdapterInputConnection's internal representation of the text
|
|
* being edited and its selection and composition properties. The resulting
|
|
* Editable is accessible through the getEditable() method.
|
|
* If the text has not changed, this also calls updateSelection on the InputMethodManager.
|
|
* @param text The String contents of the field being edited
|
|
* @param selectionStart The character offset of the selection start, or the caret
|
|
* position if there is no selection
|
|
* @param selectionEnd The character offset of the selection end, or the caret
|
|
* position if there is no selection
|
|
* @param compositionStart The character offset of the composition start, or -1
|
|
* if there is no composition
|
|
* @param compositionEnd The character offset of the composition end, or -1
|
|
* if there is no selection
|
|
*/
|
|
public void setEditableText(String text, int selectionStart, int selectionEnd,
|
|
int compositionStart, int compositionEnd) {
|
|
log("setEditableText(%s, %d, %d, %d, %d)", text,
|
|
selectionStart, selectionEnd,
|
|
compositionStart, compositionEnd);
|
|
|
|
int prevSelectionStart = Selection.getSelectionStart(mEditable);
|
|
int prevSelectionEnd = Selection.getSelectionEnd(mEditable);
|
|
int prevEditableLength = mEditable.length();
|
|
int prevCompositionStart = getComposingSpanStart(mEditable);
|
|
int prevCompositionEnd = getComposingSpanEnd(mEditable);
|
|
String prevText = mEditable.toString();
|
|
|
|
selectionStart = Math.min(selectionStart, text.length());
|
|
selectionEnd = Math.min(selectionEnd, text.length());
|
|
compositionStart = Math.min(compositionStart, text.length());
|
|
compositionEnd = Math.min(compositionEnd, text.length());
|
|
|
|
boolean textUnchanged = prevText.equals(text);
|
|
|
|
if (textUnchanged
|
|
&& prevSelectionStart == selectionStart && prevSelectionEnd == selectionEnd
|
|
&& prevCompositionStart == compositionStart
|
|
&& prevCompositionEnd == compositionEnd) {
|
|
// Nothing has changed; don't need to do anything
|
|
return;
|
|
}
|
|
|
|
// When a programmatic change has been made to the editable field, both the start
|
|
// and end positions for the composition will equal zero. In this case we cancel the
|
|
// active composition in the editor as this no longer is relevant.
|
|
if (textUnchanged && compositionStart == 0 && compositionEnd == 0) {
|
|
cancelComposition();
|
|
}
|
|
|
|
if (!textUnchanged) {
|
|
log("replace mEditable with: %s", text);
|
|
mEditable.replace(0, mEditable.length(), text);
|
|
}
|
|
Selection.setSelection(mEditable, selectionStart, selectionEnd);
|
|
super.setComposingRegion(compositionStart, compositionEnd);
|
|
|
|
//log("textUnchanged=%s prevText=%s", textUnchanged, prevText);
|
|
//if (textUnchanged || prevText.equals("")) {
|
|
// log("setEditableText updating selection");
|
|
// updateSelection should be called when a manual selection change occurs.
|
|
// Should not be called if text is being entered else issues can occur
|
|
// e.g. backspace to undo autocorrection will not work with the default OSK.
|
|
getInputMethodManager().updateSelection(mInternalView,
|
|
selectionStart, selectionEnd, compositionStart, compositionEnd);
|
|
//}
|
|
}
|
|
|
|
@Override
|
|
public Editable getEditable() {
|
|
log("getEditable() -> %s", editableToXml(mEditable));
|
|
return mEditable;
|
|
}
|
|
|
|
@Override
|
|
public boolean setComposingText(CharSequence text, int newCursorPosition) {
|
|
log("setComposingText(%s, %d)", text, newCursorPosition);
|
|
super.setComposingText(text, newCursorPosition);
|
|
shouldUpdateImeSelection = true;
|
|
onCompose(id, text.toString(), newCursorPosition, false);
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean commitText(CharSequence text, int newCursorPosition) {
|
|
log("commitText(%s, %d)", text, newCursorPosition);
|
|
super.commitText(text, newCursorPosition);
|
|
shouldUpdateImeSelection = true;
|
|
onCompose(id, text.toString(), newCursorPosition, text.length() > 0);
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean performEditorAction(int actionCode) {
|
|
log("performEditorAction(%d)", actionCode);
|
|
switch (actionCode) {
|
|
case EditorInfo.IME_ACTION_NEXT:
|
|
cancelComposition();
|
|
// Send TAB key event
|
|
long timeStampMs = System.currentTimeMillis();
|
|
//mImeAdapter.sendSyntheticKeyEvent(
|
|
// sEventTypeRawKeyDown, timeStampMs, KeyEvent.KEYCODE_TAB, 0);
|
|
return true;
|
|
case EditorInfo.IME_ACTION_GO:
|
|
case EditorInfo.IME_ACTION_SEARCH:
|
|
//mImeAdapter.dismissInput(true);
|
|
break;
|
|
}
|
|
|
|
return super.performEditorAction(actionCode);
|
|
}
|
|
|
|
@Override
|
|
public boolean performContextMenuAction(int id) {
|
|
log("performContextMenuAction(%d)", id);
|
|
/*
|
|
switch (id) {
|
|
case android.R.id.selectAll:
|
|
return mImeAdapter.selectAll();
|
|
case android.R.id.cut:
|
|
return mImeAdapter.cut();
|
|
case android.R.id.copy:
|
|
return mImeAdapter.copy();
|
|
case android.R.id.paste:
|
|
return mImeAdapter.paste();
|
|
default:
|
|
return false;
|
|
}
|
|
*/
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public CharSequence getTextAfterCursor(int length, int flags) {
|
|
log("getTextAfterCursor(%d, %d)", length, flags);
|
|
return super.getTextAfterCursor(length, flags);
|
|
}
|
|
@Override
|
|
public CharSequence getTextBeforeCursor(int length, int flags) {
|
|
log("getTextBeforeCursor(%d, %d)", length, flags);
|
|
return super.getTextBeforeCursor(length, flags);
|
|
}
|
|
@Override
|
|
public SurroundingText getSurroundingText(int beforeLength, int afterLength, int flags) {
|
|
log("getSurroundingText(%d, %d, %d)", beforeLength, afterLength, flags);
|
|
return super.getSurroundingText(beforeLength, afterLength, flags);
|
|
}
|
|
@Override
|
|
public CharSequence getSelectedText(int flags) {
|
|
log("getSelectedText(%d)", flags);
|
|
return super.getSelectedText(flags);
|
|
}
|
|
|
|
@Override
|
|
public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
|
|
log("getExtractedText(...)");
|
|
ExtractedText et = new ExtractedText();
|
|
et.text = mEditable.toString();
|
|
et.partialEndOffset = mEditable.length();
|
|
et.selectionStart = Selection.getSelectionStart(mEditable);
|
|
et.selectionEnd = Selection.getSelectionEnd(mEditable);
|
|
et.flags = mSingleLine ? ExtractedText.FLAG_SINGLE_LINE : 0;
|
|
return et;
|
|
}
|
|
|
|
@Override
|
|
public boolean deleteSurroundingText(int leftLength, int rightLength) {
|
|
log("deleteSurroundingText(%d, %d)", leftLength, rightLength);
|
|
if (!super.deleteSurroundingText(leftLength, rightLength)) {
|
|
return false;
|
|
}
|
|
shouldUpdateImeSelection = true;
|
|
//return mImeAdapter.deleteSurroundingText(leftLength, rightLength);
|
|
onDeleteSurroundingText(id, leftLength, rightLength);
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean sendKeyEvent(KeyEvent event) {
|
|
int action = event.getAction();
|
|
int keycode = event.getKeyCode();
|
|
log("sendKeyEvent() [action=%d, keycode=%d]", action, keycode);
|
|
|
|
//mImeAdapter.mSelectionHandleController.hideAndDisallowAutomaticShowing();
|
|
//mImeAdapter.mInsertionHandleController.hideAndDisallowAutomaticShowing();
|
|
|
|
// If this is a key-up, and backspace/del or if the key has a character representation,
|
|
// need to update the underlying Editable (i.e. the local representation of the text
|
|
// being edited).
|
|
if (event.getAction() == KeyEvent.ACTION_UP) {
|
|
if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
|
|
super.deleteSurroundingText(1, 0);
|
|
} else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) {
|
|
super.deleteSurroundingText(0, 1);
|
|
} else {
|
|
int unicodeChar = event.getUnicodeChar();
|
|
if (unicodeChar != 0) {
|
|
Editable editable = getEditable();
|
|
int selectionStart = Selection.getSelectionStart(editable);
|
|
int selectionEnd = Selection.getSelectionEnd(editable);
|
|
if (selectionStart > selectionEnd) {
|
|
int temp = selectionStart;
|
|
selectionStart = selectionEnd;
|
|
selectionEnd = temp;
|
|
}
|
|
|
|
String inputChar = Character.toString((char)unicodeChar);
|
|
editable.replace(selectionStart, selectionEnd, inputChar);
|
|
onCompose(id, inputChar, selectionStart, true);
|
|
}
|
|
}
|
|
}
|
|
shouldUpdateImeSelection = true;
|
|
return super.sendKeyEvent(event);
|
|
}
|
|
|
|
@Override
|
|
public boolean finishComposingText() {
|
|
if (getComposingSpanStart(mEditable) == getComposingSpanEnd(mEditable)) {
|
|
log("finishComposingText() [DISABLED]");
|
|
return true;
|
|
}
|
|
log("finishComposingText()");
|
|
super.finishComposingText();
|
|
onFinishCompose(id);
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean setSelection(int start, int end) {
|
|
log("setSelection(%d, %d)", start, end);
|
|
if (start < 0 || end < 0) return true;
|
|
super.setSelection(start, end);
|
|
shouldUpdateImeSelection = true;
|
|
//return mImeAdapter.setEditableSelectionOffsets(start, end);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Informs the InputMethodManager and InputMethodSession (i.e. the IME) that there
|
|
* is no longer a current composition. Note this differs from finishComposingText, which
|
|
* is called by the IME when it wants to end a composition.
|
|
*/
|
|
void cancelComposition() {
|
|
log("cancelComposition()");
|
|
getInputMethodManager().restartInput(mInternalView);
|
|
}
|
|
|
|
@Override
|
|
public boolean setComposingRegion(int start, int end) {
|
|
log("setComposingRegion(%d, %d)", start, end);
|
|
int a = Math.min(start, end);
|
|
int b = Math.max(start, end);
|
|
super.setComposingRegion(a, b);
|
|
onSetComposeRegion(id, a, b);
|
|
return true;
|
|
}
|
|
|
|
boolean isActive() {
|
|
return getInputMethodManager().isActive();
|
|
}
|
|
|
|
private InputMethodManager getInputMethodManager() {
|
|
InputMethodManager imm = (InputMethodManager)mInternalView.getContext()
|
|
.getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
if (imm == null) {
|
|
Log.e("darkfi", "[IC]: InputMethodManager is NULL!");
|
|
}
|
|
return imm;
|
|
}
|
|
|
|
private void updateImeSelection() {
|
|
log("updateImeSelection()");
|
|
getInputMethodManager().updateSelection(
|
|
mInternalView,
|
|
Selection.getSelectionStart(mEditable),
|
|
Selection.getSelectionEnd(mEditable),
|
|
getComposingSpanStart(mEditable),
|
|
getComposingSpanEnd(mEditable)
|
|
);
|
|
log("updateImeSelection() DONE");
|
|
}
|
|
|
|
@Override
|
|
public boolean beginBatchEdit() {
|
|
log("beginBatchEdit");
|
|
++numBatchEdits;
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean endBatchEdit() {
|
|
log("endBatchEdit");
|
|
if (--numBatchEdits == 0 && shouldUpdateImeSelection) {
|
|
updateImeSelection();
|
|
shouldUpdateImeSelection = false;
|
|
}
|
|
log("endBatchEdit DONE");
|
|
return false;
|
|
}
|
|
|
|
public static String editableToXml(Editable editable) {
|
|
StringBuilder xmlBuilder = new StringBuilder();
|
|
int length = editable.length();
|
|
|
|
Object[] spans = editable.getSpans(0, editable.length(), Object.class);
|
|
|
|
for (int i = 0; i < length; i++) {
|
|
// Find spans starting at this position
|
|
for (Object span : spans) {
|
|
if (editable.getSpanStart(span) == i) {
|
|
xmlBuilder
|
|
.append("<")
|
|
.append(span.getClass().getSimpleName())
|
|
.append(">");
|
|
}
|
|
}
|
|
|
|
// Append the character
|
|
char c = editable.charAt(i);
|
|
xmlBuilder.append(c);
|
|
|
|
if (Character.isHighSurrogate(c)) {
|
|
if (i + 1 < editable.length() && Character.isLowSurrogate(editable.charAt(i + 1))) {
|
|
i += 1;
|
|
xmlBuilder.append(editable.charAt(i));
|
|
}
|
|
}
|
|
|
|
// Find spans ending at this position
|
|
for (Object span : spans) {
|
|
if (editable.getSpanEnd(span) == i) {
|
|
xmlBuilder
|
|
.append("</")
|
|
.append(span.getClass().getSimpleName())
|
|
.append(">");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find spans starting at this position
|
|
for (Object span : spans) {
|
|
if (editable.getSpanStart(span) == length) {
|
|
xmlBuilder
|
|
.append("<")
|
|
.append(span.getClass().getSimpleName())
|
|
.append(">");
|
|
}
|
|
}
|
|
// Find spans ending at this position
|
|
for (Object span : spans) {
|
|
if (editable.getSpanEnd(span) == length) {
|
|
xmlBuilder
|
|
.append("</")
|
|
.append(span.getClass().getSimpleName())
|
|
.append(">");
|
|
}
|
|
}
|
|
|
|
return xmlBuilder.toString();
|
|
}
|
|
}
|
|
|