Skip to content

Make CandidatesView touchable #680

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 46 additions & 2 deletions app/src/main/cpp/androidfrontend/androidfrontend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ class AndroidInputContext : public InputContextV2 {
frontend_->updatePagedCandidate(paged);
}

bool selectCandidate(int idx) {
bool selectCandidateBulk(int idx) {
const auto &list = inputPanel().candidateList();
if (!list) {
return false;
Expand All @@ -150,6 +150,20 @@ class AndroidInputContext : public InputContextV2 {
return true;
}

bool selectCandidatePaged(int idx) {
const auto &list = inputPanel().candidateList();
if (!list) {
return false;
}
try {
list->candidate(idx).select(this);
} catch (const std::invalid_argument &e) {
FCITX_WARN() << "selectCandidate index out of range";
return false;
}
return true;
}

std::vector<std::string> getCandidates(const int offset, const int limit) {
std::vector<std::string> candidates;
const auto &list = inputPanel().candidateList();
Expand Down Expand Up @@ -237,6 +251,27 @@ class AndroidInputContext : public InputContextV2 {
}
}

void offsetCandidatePage(int delta) {
if (delta == 0) {
return;
}
const auto &list = inputPanel().candidateList();
if (!list) {
return;
}
const auto &pageable = list->toPageable();
if (!pageable) {
return;
}
if (delta > 0 && pageable->hasNext()) {
pageable->next();
updateUserInterface(UserInterfaceComponent::InputPanel);
} else if (delta < 0 && pageable->hasPrev()) {
pageable->prev();
updateUserInterface(UserInterfaceComponent::InputPanel);
}
}

private:
AndroidFrontend *frontend_;
int uid_;
Expand Down Expand Up @@ -328,7 +363,11 @@ void AndroidFrontend::releaseInputContext(const int uid) {

bool AndroidFrontend::selectCandidate(int idx) {
if (!activeIC_) return false;
return activeIC_->selectCandidate(idx);
if (pagingMode_) {
return activeIC_->selectCandidatePaged(idx);
} else {
return activeIC_->selectCandidateBulk(idx);
}
}

std::vector<CandidateAction> AndroidFrontend::getCandidateActions(const int idx) {
Expand Down Expand Up @@ -419,6 +458,11 @@ void AndroidFrontend::updatePagedCandidate(const PagedCandidateEntity &paged) {
pagedCandidateCallback(paged);
}

void AndroidFrontend::offsetCandidatePage(int delta) {
if (!activeIC_) return;
activeIC_->offsetCandidatePage(delta);
}

void AndroidFrontend::setCommitStringCallback(const CommitStringCallback &callback) {
commitStringCallback = callback;
}
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/cpp/androidfrontend/androidfrontend.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class AndroidFrontend : public AddonInstance {
void deleteSurrounding(const int before, const int after);
void showToast(const std::string &s);
void setCandidatePagingMode(const int mode);
void offsetCandidatePage(int delta);
void setCandidateListCallback(const CandidateListCallback &callback);
void setCommitStringCallback(const CommitStringCallback &callback);
void setPreeditCallback(const ClientPreeditCallback &callback);
Expand Down Expand Up @@ -74,6 +75,7 @@ class AndroidFrontend : public AddonInstance {
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, triggerCandidateAction);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, showToast);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setCandidatePagingMode);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, offsetCandidatePage);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setCandidateListCallback);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setCommitStringCallback);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setPreeditCallback);
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/cpp/androidfrontend/androidfrontend_public.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, showToast,
FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setCandidatePagingMode,
void(const int))

FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, offsetCandidatePage,
void(int))

FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setCandidateListCallback,
void(const CandidateListCallback &))

Expand Down
11 changes: 11 additions & 0 deletions app/src/main/cpp/native-lib.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,10 @@ class Fcitx {
return p_frontend->call<fcitx::IAndroidFrontend::setCandidatePagingMode>(mode);
}

void offsetCandidatePage(int delta) {
return p_frontend->call<fcitx::IAndroidFrontend::offsetCandidatePage>(delta);
}

void save() {
p_instance->save();
}
Expand Down Expand Up @@ -1068,6 +1072,13 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_setFcitxCandidatePagingMode(JNIEnv *env
Fcitx::Instance().setCandidatePagingMode(mode);
}

extern "C"
JNIEXPORT void JNICALL
Java_org_fcitx_fcitx5_android_core_Fcitx_offsetFcitxCandidatePage(JNIEnv *env, jclass clazz, jint delta) {
RETURN_IF_NOT_RUNNING
Fcitx::Instance().offsetCandidatePage(delta);
}

extern "C"
JNIEXPORT void JNICALL
Java_org_fcitx_fcitx5_android_core_Fcitx_loopOnce(JNIEnv *env, jclass clazz) {
Expand Down
30 changes: 27 additions & 3 deletions app/src/main/java/org/fcitx/fcitx5/android/core/Fcitx.kt
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,13 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner {
override suspend fun save() = withFcitxContext { saveFcitxState() }
override suspend fun reloadConfig() = withFcitxContext { reloadFcitxConfig() }

override suspend fun sendKey(key: String, states: UInt, code: Int, up: Boolean, timestamp: Int) =
override suspend fun sendKey(
key: String,
states: UInt,
code: Int,
up: Boolean,
timestamp: Int
) =
withFcitxContext { sendKeyToFcitxString(key, states.toInt(), code, up, timestamp) }

override suspend fun sendKey(c: Char, states: UInt, code: Int, up: Boolean, timestamp: Int) =
Expand All @@ -82,7 +88,13 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner {
override suspend fun sendKey(sym: Int, states: UInt, code: Int, up: Boolean, timestamp: Int) =
withFcitxContext { sendKeySymToFcitx(sym, states.toInt(), code, up, timestamp) }

override suspend fun sendKey(sym: KeySym, states: KeyStates, code: Int, up: Boolean, timestamp: Int) =
override suspend fun sendKey(
sym: KeySym,
states: KeyStates,
code: Int,
up: Boolean,
timestamp: Int
) =
withFcitxContext { sendKeySymToFcitx(sym.sym, states.toInt(), code, up, timestamp) }

override suspend fun select(idx: Int): Boolean = withFcitxContext { selectCandidate(idx) }
Expand Down Expand Up @@ -172,6 +184,9 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner {
override suspend fun setCandidatePagingMode(mode: Int) =
withFcitxContext { setFcitxCandidatePagingMode(mode) }

override suspend fun offsetCandidatePage(delta: Int) =
withFcitxContext { offsetFcitxCandidatePage(delta) }

init {
if (lifecycle.currentState != FcitxLifecycle.State.STOPPED)
throw IllegalAccessException("Fcitx5 has already been created!")
Expand Down Expand Up @@ -233,7 +248,13 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner {
external fun reloadFcitxConfig()

@JvmStatic
external fun sendKeyToFcitxString(key: String, state: Int, code: Int, up: Boolean, timestamp: Int)
external fun sendKeyToFcitxString(
key: String,
state: Int,
code: Int,
up: Boolean,
timestamp: Int
)

@JvmStatic
external fun sendKeyToFcitxChar(c: Char, state: Int, code: Int, up: Boolean, timestamp: Int)
Expand Down Expand Up @@ -343,6 +364,9 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner {
@JvmStatic
external fun setFcitxCandidatePagingMode(mode: Int)

@JvmStatic
external fun offsetFcitxCandidatePage(delta: Int)

@JvmStatic
external fun loopOnce()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,5 +103,6 @@ interface FcitxAPI {
suspend fun triggerCandidateAction(idx: Int, actionIdx: Int)

suspend fun setCandidatePagingMode(mode: Int)
suspend fun offsetCandidatePage(delta: Int)

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import androidx.annotation.Size
import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.core.FcitxEvent
import org.fcitx.fcitx5.android.daemon.FcitxConnection
import org.fcitx.fcitx5.android.daemon.launchOnReady
import org.fcitx.fcitx5.android.data.prefs.AppPrefs
import org.fcitx.fcitx5.android.data.theme.Theme
import org.fcitx.fcitx5.android.input.candidates.floating.PagedCandidatesUi
Expand Down Expand Up @@ -73,6 +74,8 @@ class CandidatesView(
true
}

private val touchEventReceiverWindow = TouchEventReceiverWindow(this)

private val setupTextView: TextView.() -> Unit = {
textSize = fontSize.toFloat()
val v = dp(itemPaddingVertical)
Expand All @@ -82,7 +85,11 @@ class CandidatesView(

private val preeditUi = PreeditUi(ctx, theme, setupTextView)

private val candidatesUi = PagedCandidatesUi(ctx, theme, setupTextView).apply {
private val candidatesUi = PagedCandidatesUi(ctx, theme, setupTextView,
onCandidateClick = { index -> fcitx.launchOnReady { it.select(index) } },
onPrevPage = { fcitx.launchOnReady { it.offsetCandidatePage(-1) } },
onNextPage = { fcitx.launchOnReady { it.offsetCandidatePage(1) } }
).apply {
root.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
}

Expand Down Expand Up @@ -116,6 +123,7 @@ class CandidatesView(
candidatesUi.update(paged, orientation)
visibility = VISIBLE
} else {
touchEventReceiverWindow.dismiss()
visibility = GONE
}
}
Expand All @@ -141,6 +149,8 @@ class CandidatesView(
}
translationY =
if (bottom + selfHeight > parentHeight - bottomInsets) top - selfHeight else bottom
// update touchEventReceiverWindow's position after CandidatesView's
touchEventReceiverWindow.showup()
shouldUpdatePosition = false
}

Expand Down Expand Up @@ -193,6 +203,7 @@ class CandidatesView(
override fun onDetachedFromWindow() {
viewTreeObserver.removeOnPreDrawListener(preDrawListener)
candidatesUi.root.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
touchEventReceiverWindow.dismiss()
super.onDetachedFromWindow()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2025 Fcitx5 for Android Contributors
*/

package org.fcitx.fcitx5.android.input

import android.annotation.SuppressLint
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.widget.PopupWindow

class TouchEventReceiverWindow(
private val contentView: View
) {
private val ctx = contentView.context

private val window = PopupWindow(object : View(ctx) {
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean {
return contentView.dispatchTouchEvent(event)
}
}).apply {
// disable animation
animationStyle = 0
}

private var isWindowShowing = false

private val cachedLocation = intArrayOf(0, 0)

fun showup() {
isWindowShowing = true
val (x, y) = cachedLocation.also { contentView.getLocationInWindow(it) }
val width = contentView.width
val height = contentView.height
if (window.isShowing) {
window.update(x, y, width, height)
} else {
window.width = width
window.height = height
window.showAtLocation(contentView, Gravity.NO_GRAVITY, x, y)
}
}

fun dismiss() {
if (isWindowShowing) {
isWindowShowing = false
window.dismiss()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package org.fcitx.fcitx5.android.input.candidates.floating

import android.annotation.SuppressLint
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.view.updateLayoutParams
Expand All @@ -24,7 +25,10 @@ import splitties.views.dsl.recyclerview.recyclerView
class PagedCandidatesUi(
override val ctx: Context,
val theme: Theme,
private val setupTextView: TextView.() -> Unit
private val setupTextView: TextView.() -> Unit,
private val onCandidateClick: (Int) -> Unit,
private val onPrevPage: () -> Unit,
private val onNextPage: () -> Unit
) : Ui {

private var data = FcitxEvent.PagedCandidateEvent.Data.Empty
Expand All @@ -50,6 +54,12 @@ class PagedCandidatesUi(
ui.root.layoutParams = FlexboxLayoutManager.LayoutParams(wrap, wrap).apply {
flexGrow = 1f
}
ui.prevIcon.setOnClickListener {
onPrevPage.invoke()
}
ui.nextIcon.setOnClickListener {
onNextPage.invoke()
}
}
}
}
Expand All @@ -59,6 +69,9 @@ class PagedCandidatesUi(
is UiHolder.Candidate -> {
val candidate = data.candidates[position]
holder.ui.update(candidate, active = position == data.cursorIndex)
holder.ui.root.setOnClickListener {
onCandidateClick.invoke(position)
}
}
is UiHolder.Pagination -> {
holder.ui.update(data)
Expand All @@ -79,6 +92,7 @@ class PagedCandidatesUi(
isFocusable = false
adapter = candidatesAdapter
layoutManager = candidatesLayoutManager
overScrollMode = View.OVER_SCROLL_NEVER
}

@SuppressLint("NotifyDataSetChanged")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ class PaginationUi(override val ctx: Context, val theme: Theme) : Ui {
imageTintList = ColorStateList.valueOf(theme.keyTextColor)
imageDrawable = drawable(icon)
scaleType = ImageView.ScaleType.CENTER_CROP
isClickable = true
}

private val prevIcon = createIcon(R.drawable.ic_baseline_arrow_prev_24)
private val nextIcon = createIcon(R.drawable.ic_baseline_arrow_next_24)
val prevIcon = createIcon(R.drawable.ic_baseline_arrow_prev_24)
val nextIcon = createIcon(R.drawable.ic_baseline_arrow_next_24)

private val disabledAlpha = styledFloat(android.R.attr.disabledAlpha)

Expand Down
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy