Residual scroll jank at 120 Hz (suspected Slint event-loop bug) needs per-frame + per-stage timing the logcat ring buffer can't hold. New cargo feature `perf` (default OFF — all sites are cfg-gated, zero overhead when off): - FR: per-frame app-side render cost + frame gap, from the rendering notifier (BeforeRendering/AfterRendering). - STG: event-loop stage costs (wake/timers/events/poll/lastrender) from timestamps instrumented into the pinned vendor copy of i-slint-backend-android-activity (stg.rs; Cargo.toml now depends on it directly + optional under `perf`). - DECODE: per-thumbnail-job cost on the workers. - All lines mirrored to /sdcard/Download/.gallery_perf.log (logcat rotates too fast at ~120 lines/s). Format + reading guide: docs/BUGS/scroll-jank-120hz.md.
732 lines
29 KiB
Java
732 lines
29 KiB
Java
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
|
|
|
// cSpell:ignore Spannable tbstart tbend
|
|
|
|
import java.util.concurrent.Callable;
|
|
import java.util.concurrent.FutureTask;
|
|
import android.view.ActionMode;
|
|
import android.view.Menu;
|
|
import android.view.MenuItem;
|
|
import android.view.MotionEvent;
|
|
import android.view.View;
|
|
import android.view.ViewTreeObserver;
|
|
import android.view.WindowInsets;
|
|
import android.view.WindowInsetsAnimation;
|
|
import android.view.WindowMetrics;
|
|
import android.view.inputmethod.EditorInfo;
|
|
import android.view.inputmethod.InputConnection;
|
|
import android.content.ClipData;
|
|
import android.content.ClipboardManager;
|
|
import android.content.Context;
|
|
import android.content.res.Configuration;
|
|
import android.content.res.TypedArray;
|
|
import android.graphics.BlendMode;
|
|
import android.graphics.BlendModeColorFilter;
|
|
import android.graphics.Insets;
|
|
import android.graphics.PorterDuff;
|
|
import android.graphics.Rect;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.text.Editable;
|
|
import android.text.Selection;
|
|
import android.text.SpannableStringBuilder;
|
|
import android.util.TypedValue;
|
|
import android.view.inputmethod.InputMethodManager;
|
|
import android.app.Activity;
|
|
import android.widget.FrameLayout;
|
|
import android.widget.ImageView;
|
|
import android.widget.PopupWindow;
|
|
import android.view.inputmethod.BaseInputConnection;
|
|
import android.os.Build;
|
|
import android.window.OnBackInvokedCallback;
|
|
import android.window.OnBackInvokedDispatcher;
|
|
|
|
class InputHandle extends ImageView {
|
|
private PopupWindow mPopupWindow;
|
|
private float mPressedX;
|
|
private float mPressedY;
|
|
private SlintInputView mRootView;
|
|
private int cursorX;
|
|
private int cursorY;
|
|
private int attr;
|
|
|
|
public InputHandle(SlintInputView rootView, int attr) {
|
|
super(rootView.getContext());
|
|
this.attr = attr;
|
|
mRootView = rootView;
|
|
Context ctx = rootView.getContext();
|
|
mPopupWindow = new PopupWindow(ctx, null, android.R.attr.textSelectHandleWindowStyle);
|
|
mPopupWindow.setSplitTouchEnabled(true);
|
|
mPopupWindow.setClippingEnabled(false);
|
|
int[] attrs = { attr };
|
|
Drawable drawable = ctx.getTheme().obtainStyledAttributes(attrs).getDrawable(0);
|
|
mPopupWindow.setWidth(drawable.getIntrinsicWidth());
|
|
mPopupWindow.setHeight(drawable.getIntrinsicHeight());
|
|
this.setImageDrawable(drawable);
|
|
mPopupWindow.setContentView(this);
|
|
}
|
|
|
|
@Override
|
|
public boolean onTouchEvent(MotionEvent ev) {
|
|
switch (ev.getActionMasked()) {
|
|
case MotionEvent.ACTION_DOWN: {
|
|
mPressedX = ev.getRawX() - cursorX;
|
|
mPressedY = ev.getRawY() - cursorY;
|
|
break;
|
|
}
|
|
|
|
case MotionEvent.ACTION_MOVE: {
|
|
mRootView.hideActionMenu(ActionMode.DEFAULT_HIDE_DURATION);
|
|
int id = attr == android.R.attr.textSelectHandleLeft ? 1
|
|
: attr == android.R.attr.textSelectHandleRight ? 2 : 0;
|
|
SlintAndroidJavaHelper.moveCursorHandle(id, Math.round(ev.getRawX() - mPressedX),
|
|
Math.round(ev.getRawY() - mPressedY));
|
|
break;
|
|
}
|
|
case MotionEvent.ACTION_UP:
|
|
case MotionEvent.ACTION_CANCEL:
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public void setPosition(int x, int y) {
|
|
cursorX = x;
|
|
cursorY = y;
|
|
|
|
if (attr == android.R.attr.textSelectHandleLeft) {
|
|
x -= 3 * mPopupWindow.getWidth() / 4;
|
|
} else if (attr == android.R.attr.textSelectHandleRight) {
|
|
x -= mPopupWindow.getWidth() / 4;
|
|
} else {
|
|
x -= mPopupWindow.getWidth() / 2;
|
|
}
|
|
|
|
mPopupWindow.showAtLocation(mRootView, 0, x, y);
|
|
mPopupWindow.update(x, y, -1, -1);
|
|
}
|
|
|
|
public void hide() {
|
|
mPopupWindow.dismiss();
|
|
}
|
|
|
|
public void setHandleColor(int color) {
|
|
Drawable drawable = getDrawable();
|
|
if (drawable != null) {
|
|
if (android.os.Build.VERSION.SDK_INT >= 29) {
|
|
drawable.setColorFilter(new BlendModeColorFilter(color, BlendMode.SRC_IN));
|
|
} else {
|
|
drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
|
|
}
|
|
setImageDrawable(drawable);
|
|
}
|
|
}
|
|
}
|
|
|
|
class SlintInputView extends View {
|
|
private String mText = "";
|
|
private int mCursorPosition = 0;
|
|
private int mAnchorPosition = 0;
|
|
private int mPreeditStart = 0;
|
|
private int mPreeditEnd = 0;
|
|
private int mInputType = EditorInfo.TYPE_CLASS_TEXT;
|
|
private int mInBatch = 0;
|
|
private boolean mPending = false;
|
|
private SlintEditable mEditable;
|
|
|
|
public class SlintEditable extends SpannableStringBuilder {
|
|
public SlintEditable() {
|
|
super(mText);
|
|
}
|
|
|
|
@Override
|
|
public SpannableStringBuilder replace(int start, int end, CharSequence tb, int tbstart, int tbend) {
|
|
super.replace(start, end, tb, tbstart, tbend);
|
|
setCursorPos(0, 0, 0, 0, 0, 0);
|
|
if (mInBatch == 0) {
|
|
update();
|
|
} else {
|
|
mPending = true;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
public void update() {
|
|
mPending = false;
|
|
mText = toString();
|
|
mCursorPosition = Selection.getSelectionStart(this);
|
|
mAnchorPosition = Selection.getSelectionEnd(this);
|
|
mPreeditStart = BaseInputConnection.getComposingSpanStart(this);
|
|
mPreeditEnd = BaseInputConnection.getComposingSpanEnd(this);
|
|
SlintAndroidJavaHelper.updateText(mText, mCursorPosition, mAnchorPosition, mPreeditStart, mPreeditEnd);
|
|
}
|
|
}
|
|
|
|
public SlintInputView(Context context) {
|
|
super(context);
|
|
setFocusable(true);
|
|
setFocusableInTouchMode(true);
|
|
mEditable = new SlintEditable();
|
|
}
|
|
|
|
@Override
|
|
public boolean onCheckIsTextEditor() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
|
|
outAttrs.inputType = mInputType;
|
|
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI;
|
|
outAttrs.initialSelStart = mCursorPosition;
|
|
outAttrs.initialSelEnd = mAnchorPosition;
|
|
return new BaseInputConnection(this, true) {
|
|
@Override
|
|
public Editable getEditable() {
|
|
return mEditable;
|
|
}
|
|
|
|
@Override
|
|
public boolean beginBatchEdit() {
|
|
mInBatch += 1;
|
|
return super.beginBatchEdit();
|
|
}
|
|
|
|
@Override
|
|
public boolean endBatchEdit() {
|
|
mInBatch -= 1;
|
|
if (mInBatch == 0 && mPending) {
|
|
mEditable.update();
|
|
}
|
|
return super.endBatchEdit();
|
|
}
|
|
};
|
|
}
|
|
|
|
public void setText(String text, int cursorPosition, int anchorPosition, int preeditStart, int preeditEnd,
|
|
int inputType) {
|
|
boolean typeChanged = mInputType != inputType;
|
|
boolean textChanged = !mText.equals(text);
|
|
boolean selectionChanged = mCursorPosition != cursorPosition || mAnchorPosition != anchorPosition;
|
|
|
|
mText = text;
|
|
mCursorPosition = cursorPosition;
|
|
mAnchorPosition = anchorPosition;
|
|
mPreeditStart = preeditStart;
|
|
mPreeditEnd = preeditEnd;
|
|
mInputType = inputType;
|
|
|
|
if (typeChanged) {
|
|
mEditable = new SlintEditable();
|
|
Selection.setSelection(mEditable, cursorPosition, anchorPosition);
|
|
InputMethodManager imm = (InputMethodManager) this.getContext()
|
|
.getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
imm.restartInput(this);
|
|
} else if (textChanged || selectionChanged) {
|
|
InputMethodManager imm = (InputMethodManager) this.getContext()
|
|
.getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
mInBatch += 1;
|
|
try {
|
|
if (textChanged) {
|
|
mEditable.replace(0, mEditable.length(), text);
|
|
}
|
|
if (Selection.getSelectionStart(mEditable) != cursorPosition
|
|
|| Selection.getSelectionEnd(mEditable) != anchorPosition) {
|
|
Selection.setSelection(mEditable, cursorPosition, anchorPosition);
|
|
}
|
|
} finally {
|
|
mInBatch -= 1;
|
|
mPending = false;
|
|
}
|
|
imm.updateSelection(this, cursorPosition, anchorPosition, preeditStart, preeditEnd);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onConfigurationChanged(Configuration newConfig) {
|
|
super.onConfigurationChanged(newConfig);
|
|
int currentNightMode = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
|
SlintAndroidJavaHelper.setNightMode(currentNightMode);
|
|
SlintAndroidJavaHelper.setFontScale(newConfig.fontScale);
|
|
}
|
|
|
|
private InputHandle mCursorHandle;
|
|
private InputHandle mLeftHandle;
|
|
private InputHandle mRightHandle;
|
|
public Rect selectionRect = new Rect();
|
|
|
|
// num_handles: 0=hidden, 1=cursor handle, 2=selection handles
|
|
public void setCursorPos(int left_x, int left_y, int right_x, int right_y, int cursor_height, int num_handles) {
|
|
int handleHeight = 0;
|
|
if (num_handles == 1) {
|
|
if (mLeftHandle != null) {
|
|
mLeftHandle.hide();
|
|
}
|
|
if (mRightHandle != null) {
|
|
mRightHandle.hide();
|
|
}
|
|
if (left_x != -1) {
|
|
if (mCursorHandle == null) {
|
|
mCursorHandle = new InputHandle(this, android.R.attr.textSelectHandle);
|
|
}
|
|
mCursorHandle.setPosition(left_x, left_y);
|
|
handleHeight = mCursorHandle.getHeight();
|
|
} else if (mCursorHandle != null) {
|
|
mCursorHandle.hide();
|
|
}
|
|
} else if (num_handles == 2) {
|
|
if (left_x != -1) {
|
|
if (mLeftHandle == null) {
|
|
mLeftHandle = new InputHandle(this, android.R.attr.textSelectHandleLeft);
|
|
}
|
|
mLeftHandle.setPosition(left_x, left_y);
|
|
handleHeight = mLeftHandle.getHeight();
|
|
} else {
|
|
if (mLeftHandle != null) {
|
|
mLeftHandle.hide();
|
|
}
|
|
}
|
|
if (right_x != -1) {
|
|
if (mRightHandle == null) {
|
|
mRightHandle = new InputHandle(this, android.R.attr.textSelectHandleRight);
|
|
}
|
|
mRightHandle.setPosition(right_x, right_y);
|
|
handleHeight = mRightHandle.getHeight();
|
|
} else {
|
|
if (mRightHandle != null) {
|
|
mRightHandle.hide();
|
|
}
|
|
}
|
|
if (mCursorHandle != null) {
|
|
mCursorHandle.hide();
|
|
}
|
|
showActionMenu();
|
|
} else {
|
|
if (mCursorHandle != null) {
|
|
handleHeight = mCursorHandle.getHeight();
|
|
mCursorHandle.hide();
|
|
}
|
|
if (mLeftHandle != null) {
|
|
mLeftHandle.hide();
|
|
}
|
|
if (mRightHandle != null) {
|
|
mRightHandle.hide();
|
|
}
|
|
hideActionMenu(-1);
|
|
}
|
|
|
|
selectionRect.set(Math.min(left_x, right_x), Math.min(left_y, right_y) - cursor_height,
|
|
Math.max(left_x, right_x), Math.max(left_y, right_y) + handleHeight);
|
|
if (mCurrentActionMode != null) {
|
|
mCurrentActionMode.invalidateContentRect();
|
|
}
|
|
}
|
|
|
|
public void setHandleColor(int color) {
|
|
if (mCursorHandle != null) {
|
|
mCursorHandle.setHandleColor(color);
|
|
}
|
|
if (mLeftHandle != null) {
|
|
mLeftHandle.setHandleColor(color);
|
|
}
|
|
if (mRightHandle != null) {
|
|
mRightHandle.setHandleColor(color);
|
|
}
|
|
}
|
|
|
|
private ActionMode mCurrentActionMode;
|
|
|
|
public void showActionMenu() {
|
|
if (mCurrentActionMode != null) {
|
|
mCurrentActionMode.hide(0);
|
|
return;
|
|
}
|
|
ActionMode.Callback2 action = new ActionMode.Callback2() {
|
|
@Override
|
|
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
|
mode.setTitle(null);
|
|
mode.setSubtitle(null);
|
|
mode.setTitleOptionalHint(true);
|
|
if (android.os.Build.VERSION.SDK_INT >= 28) {
|
|
menu.setGroupDividerEnabled(true);
|
|
}
|
|
|
|
final TypedArray a = getContext().obtainStyledAttributes(new int[] {
|
|
android.R.attr.actionModeCutDrawable,
|
|
android.R.attr.actionModeCopyDrawable,
|
|
android.R.attr.actionModePasteDrawable,
|
|
android.R.attr.actionModeSelectAllDrawable,
|
|
});
|
|
|
|
// Note: the ids are used in Java_SlintAndroidJavaHelper_popupMenuAction
|
|
menu.add(Menu.FIRST, 0, 0, android.R.string.cut)
|
|
.setAlphabeticShortcut('x')
|
|
.setIcon(a.getDrawable(0));
|
|
menu.add(Menu.FIRST, 1, 1, android.R.string.copy)
|
|
.setAlphabeticShortcut('c')
|
|
.setIcon(a.getDrawable(1));
|
|
menu.add(Menu.FIRST, 2, 2, android.R.string.paste)
|
|
.setAlphabeticShortcut('v')
|
|
.setIcon(a.getDrawable(2));
|
|
menu.add(Menu.FIRST, 3, 3, android.R.string.selectAll)
|
|
.setAlphabeticShortcut('a')
|
|
.setIcon(a.getDrawable(3));
|
|
|
|
a.recycle();
|
|
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
|
SlintAndroidJavaHelper.popupMenuAction(item.getItemId());
|
|
mode.finish();
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public void onDestroyActionMode(ActionMode action) {
|
|
}
|
|
|
|
// Introduced in API level 23
|
|
@Override
|
|
public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
|
|
outRect.set(selectionRect);
|
|
if (outRect.top < 0) {
|
|
// FIXME: I don't know why this is the case, but without that, the menu doesn't
|
|
// show at the right position when there is no room on top.
|
|
// Looks like the menu is always shown at outRect.top.
|
|
outRect.top = outRect.bottom;
|
|
}
|
|
}
|
|
|
|
};
|
|
mCurrentActionMode = startActionMode(action, ActionMode.TYPE_FLOATING);
|
|
|
|
}
|
|
|
|
public void hideActionMenu(int duration) {
|
|
if (mCurrentActionMode != null) {
|
|
if (duration < 0) {
|
|
mCurrentActionMode.finish();
|
|
mCurrentActionMode = null;
|
|
} else {
|
|
mCurrentActionMode.hide(duration);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public class SlintAndroidJavaHelper {
|
|
Activity mActivity;
|
|
SlintInputView mInputView;
|
|
private OnBackInvokedCallback mBackCallback;
|
|
|
|
public SlintAndroidJavaHelper(Activity activity) {
|
|
this.mActivity = activity;
|
|
this.mInputView = new SlintInputView(activity);
|
|
this.mActivity.runOnUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
|
|
FrameLayout.LayoutParams.MATCH_PARENT);
|
|
mActivity.addContentView(mInputView, params);
|
|
mInputView.setVisibility(View.VISIBLE);
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
mActivity.getWindow().getDecorView().getRootView()
|
|
.setOnApplyWindowInsetsListener((v, insets) -> dispatchInsets(insets));
|
|
// Attach the IME animation callback to the input view rather than the
|
|
// decor root: some OEM ROMs fail to render the IME surface when an
|
|
// animation callback is installed on the window's root view.
|
|
mInputView.setWindowInsetsAnimationCallback(
|
|
new WindowInsetsAnimation.Callback(
|
|
WindowInsetsAnimation.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
|
|
@Override
|
|
public WindowInsets onProgress(WindowInsets insets,
|
|
java.util.List<WindowInsetsAnimation> runningAnimations) {
|
|
return dispatchInsets(insets);
|
|
}
|
|
});
|
|
}
|
|
// On API 34+, Back arrives via OnBackInvokedDispatcher; forward
|
|
// it into Slint's key-event pipeline.
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && mBackCallback == null) {
|
|
mBackCallback = () -> SlintAndroidJavaHelper.onBackInvoked();
|
|
mActivity.getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
|
|
OnBackInvokedDispatcher.PRIORITY_DEFAULT, mBackCallback);
|
|
}
|
|
}
|
|
});
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
|
activity.getWindow().getDecorView().getRootView().getViewTreeObserver()
|
|
.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
|
@Override
|
|
public void onGlobalLayout() {
|
|
mActivity.runOnUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
Rect windowRect = get_view_rect();
|
|
Rect safeAreaRect = get_safe_area();
|
|
|
|
// This is only an approximation, because SDK level < 30 doesn't provide
|
|
// a way to get the keyboard area directly.
|
|
Rect visibleRect = new Rect();
|
|
mActivity.getWindow().getDecorView().getRootView()
|
|
.getWindowVisibleDisplayFrame(visibleRect);
|
|
int keyboardBottom = windowRect.bottom - visibleRect.bottom;
|
|
int keyboardLeft = windowRect.left - visibleRect.left;
|
|
int keyboardTop = windowRect.top - visibleRect.top;
|
|
int keyboardRight = windowRect.right - visibleRect.right;
|
|
int max = Math.max(keyboardBottom, Math.max(keyboardLeft,
|
|
Math.max(keyboardTop, keyboardRight)));
|
|
|
|
// only take the largest value (it's probably always going to be bottom)
|
|
if (max == keyboardBottom) {
|
|
keyboardTop = 0;
|
|
keyboardLeft = 0;
|
|
keyboardRight = 0;
|
|
} else if (max == keyboardLeft) {
|
|
keyboardTop = 0;
|
|
keyboardRight = 0;
|
|
keyboardBottom = 0;
|
|
} else if (max == keyboardTop) {
|
|
keyboardLeft = 0;
|
|
keyboardRight = 0;
|
|
keyboardBottom = 0;
|
|
} else {
|
|
keyboardTop = 0;
|
|
keyboardLeft = 0;
|
|
keyboardBottom = 0;
|
|
}
|
|
|
|
SlintAndroidJavaHelper.setInsets(
|
|
windowRect.top, windowRect.left,
|
|
windowRect.bottom, windowRect.right,
|
|
safeAreaRect.top, safeAreaRect.left,
|
|
safeAreaRect.bottom, safeAreaRect.right,
|
|
keyboardTop, keyboardLeft,
|
|
keyboardBottom, keyboardRight);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
private WindowInsets dispatchInsets(WindowInsets insets) {
|
|
// The listener-supplied `insets` reflects what reaches the decor view
|
|
// AFTER any ancestor has consumed insets, so in edge-to-edge mode the
|
|
// system bars and the display cutout often arrive as zero. Read those
|
|
// straight from the WindowManager, which always returns the unconsumed
|
|
// values — matching what get_safe_area() does. The IME inset still
|
|
// comes from the listener stream so keyboard show/hide animates.
|
|
Insets sysBars;
|
|
Insets cutout;
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
WindowInsets src = mActivity.getWindowManager().getCurrentWindowMetrics().getWindowInsets();
|
|
sysBars = src.getInsets(WindowInsets.Type.systemBars());
|
|
cutout = src.getInsets(WindowInsets.Type.displayCutout());
|
|
} else {
|
|
sysBars = insets.getInsets(WindowInsets.Type.systemBars());
|
|
cutout = insets.getInsets(WindowInsets.Type.displayCutout());
|
|
}
|
|
Insets safeAreaInsets = Insets.max(sysBars, cutout);
|
|
Insets keyboardAreaInsets = insets.getInsets(WindowInsets.Type.ime());
|
|
Rect windowRect = get_view_rect();
|
|
SlintAndroidJavaHelper.setInsets(
|
|
windowRect.top, windowRect.left,
|
|
windowRect.bottom, windowRect.right,
|
|
safeAreaInsets.top, safeAreaInsets.left,
|
|
safeAreaInsets.bottom, safeAreaInsets.right,
|
|
keyboardAreaInsets.top, keyboardAreaInsets.left,
|
|
keyboardAreaInsets.bottom, keyboardAreaInsets.right);
|
|
return insets;
|
|
}
|
|
|
|
public void show_keyboard() {
|
|
mActivity.runOnUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
mInputView.requestFocus();
|
|
InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
imm.showSoftInput(mInputView, 0);
|
|
}
|
|
});
|
|
}
|
|
|
|
public void hide_keyboard() {
|
|
mActivity.runOnUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
imm.hideSoftInputFromWindow(mInputView.getWindowToken(), 0);
|
|
mInputView.clearFocus();
|
|
mInputView.setCursorPos(0, 0, 0, 0, 0, 0);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Called from Rust when an OnBackInvokedCallback fires and Slint's key
|
|
// dispatch reports the Back key as unhandled — preserves the legacy
|
|
// Back-closes-the-activity default.
|
|
public void finish_activity() {
|
|
mActivity.runOnUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
mActivity.finish();
|
|
}
|
|
});
|
|
}
|
|
|
|
static public native void updateText(String text, int cursorPosition, int anchorPosition, int preeditStart,
|
|
int preeditOffset);
|
|
|
|
static public native void setNightMode(int nightMode);
|
|
|
|
static public native void setFontScale(float fontScale);
|
|
|
|
static public native void onBackInvoked();
|
|
|
|
static public native void moveCursorHandle(int id, int pos_x, int pos_y);
|
|
|
|
static public native void popupMenuAction(int id);
|
|
|
|
static public native void setInsets(int window_top, int window_left, int window_bottom, int window_right,
|
|
int safe_area_top, int safe_area_left, int safe_area_bottom, int safe_area_right,
|
|
int keyboard_top, int keyboard_left, int keyboard_bottom, int keyboard_right);
|
|
|
|
public void set_imm_data(String text, int cursor_position, int anchor_position, int preedit_start, int preedit_end,
|
|
int cur_x, int cur_y, int anchor_x, int anchor_y, int cursor_height, int input_type,
|
|
boolean show_cursor_handles) {
|
|
|
|
mActivity.runOnUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
int selStart = Math.min(cursor_position, anchor_position);
|
|
int selEnd = Math.max(cursor_position, anchor_position);
|
|
mInputView.setText(text, selStart, selEnd, preedit_start, preedit_end, input_type);
|
|
int num_handles = 0;
|
|
if (show_cursor_handles) {
|
|
num_handles = cursor_position == anchor_position ? 1 : 2;
|
|
}
|
|
if (cursor_position < anchor_position) {
|
|
mInputView.setCursorPos(cur_x, cur_y, anchor_x, anchor_y, cursor_height, num_handles);
|
|
} else {
|
|
mInputView.setCursorPos(anchor_x, anchor_y, cur_x, cur_y, cursor_height, num_handles);
|
|
}
|
|
|
|
}
|
|
});
|
|
}
|
|
|
|
public void set_handle_color(int color) {
|
|
mActivity.runOnUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
mInputView.setHandleColor(color);
|
|
}
|
|
});
|
|
}
|
|
|
|
public int color_scheme() {
|
|
int nightModeFlags = mActivity.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
|
return nightModeFlags;
|
|
}
|
|
|
|
public float font_scale() {
|
|
return mActivity.getResources().getConfiguration().fontScale;
|
|
}
|
|
|
|
public int accent_color() {
|
|
TypedValue typedValue = new TypedValue();
|
|
if (mActivity.getTheme().resolveAttribute(android.R.attr.colorAccent, typedValue, true)) {
|
|
return mActivity.getColor(typedValue.resourceId);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Get the size of the window
|
|
public Rect get_view_rect() {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
// On Android 11 and above, we can get the window bounds directly
|
|
WindowMetrics metrics = mActivity.getWindowManager().getCurrentWindowMetrics();
|
|
return metrics.getBounds();
|
|
} else {
|
|
View rootView = mActivity.getWindow().getDecorView().getRootView();
|
|
return new Rect(rootView.getLeft(), rootView.getTop(), rootView.getRight(), rootView.getBottom());
|
|
}
|
|
}
|
|
|
|
// On SDK level < 30, returns the inset for the safe area and the keyboard.
|
|
// On SDK level >= 30, returns the inset for the safe area only.
|
|
public Rect get_safe_area() {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
WindowMetrics metrics = mActivity.getWindowManager().getCurrentWindowMetrics();
|
|
WindowInsets insets = metrics.getWindowInsets();
|
|
Insets safeArea = Insets.max(
|
|
insets.getInsets(WindowInsets.Type.systemBars()),
|
|
insets.getInsets(WindowInsets.Type.displayCutout()));
|
|
return new Rect(safeArea.left, safeArea.top, safeArea.right, safeArea.bottom);
|
|
} else {
|
|
View decorView = mActivity.getWindow().getDecorView();
|
|
// Note: `View.getRootWindowInsets` requires API level 23 or above
|
|
WindowInsets insets = decorView.getRootView().getRootWindowInsets();
|
|
if (insets != null) {
|
|
return new Rect(
|
|
insets.getStableInsetLeft(),
|
|
insets.getStableInsetTop(),
|
|
insets.getStableInsetRight(),
|
|
insets.getStableInsetBottom());
|
|
}
|
|
return new Rect(0, 0, 0, 0);
|
|
}
|
|
}
|
|
|
|
public void show_action_menu() {
|
|
mActivity.runOnUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
mInputView.showActionMenu();
|
|
}
|
|
});
|
|
}
|
|
|
|
public String get_clipboard() {
|
|
FutureTask<String> future = new FutureTask<>(new Callable<String>() {
|
|
@Override
|
|
public String call() throws Exception {
|
|
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
|
if (clipboard.hasPrimaryClip()) {
|
|
ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0);
|
|
return item.getText().toString();
|
|
}
|
|
return "";
|
|
}
|
|
});
|
|
|
|
mActivity.runOnUiThread(future);
|
|
try {
|
|
return future.get(); // Wait for the result and return it
|
|
} catch (Exception e) {
|
|
e.printStackTrace();
|
|
return "";
|
|
}
|
|
}
|
|
|
|
public void set_clipboard(String text) {
|
|
mActivity.runOnUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
|
ClipData clip = ClipData.newPlainText(null, text);
|
|
clipboard.setPrimaryClip(clip);
|
|
}
|
|
});
|
|
}
|
|
}
|