From e2fc8a6c6520f1b51db338c165e0aef7c1798e60 Mon Sep 17 00:00:00 2001 From: Potato Hatsue <1793913507@qq.com> Date: Fri, 3 Feb 2023 20:56:51 -0500 Subject: [PATCH 001/624] Fix bugs in disabling addons --- .../org/fcitx/fcitx5/android/core/Fcitx.kt | 8 ++++- .../main/settings/addon/AddonListFragment.kt | 2 ++ .../fcitx5/android/utils/ImmutableGraph.kt | 29 ++++++++++++------- flake.lock | 12 ++++---- flake.nix | 4 +-- 5 files changed, 35 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/Fcitx.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/Fcitx.kt index 5fb1fcc12..ccb981292 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/core/Fcitx.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/core/Fcitx.kt @@ -51,7 +51,13 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner { override fun getAddonReverseDependencies(addon: String) = (addonGraph ?: run { computeAddonGraph().also { addonGraph = it } }).let { graph -> addonReverseDependencies.computeIfAbsent(addon) - { graph.bfs(it) } + { + graph.bfs(it) { level, _, dep -> + // stop when the direct child is an optional dependency + dep == FcitxAPI.AddonDep.Required + || (level == 1 && dep == FcitxAPI.AddonDep.Optional) + } + } } override fun translate(str: String, domain: String) = getFcitxTranslation(domain, str) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/addon/AddonListFragment.kt b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/addon/AddonListFragment.kt index 4b88c0ce0..f86dfb7cc 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/addon/AddonListFragment.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/addon/AddonListFragment.kt @@ -90,6 +90,8 @@ class AddonListFragment : ProgressFragment(), OnItemChangedListener { } else { ui.updateItem(ui.indexItem(entry), entry.copy(enabled = false)) } + } else { + ui.updateItem(ui.indexItem(entry), entry.copy(enabled = false)) } } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/utils/ImmutableGraph.kt b/app/src/main/java/org/fcitx/fcitx5/android/utils/ImmutableGraph.kt index 512578323..b311aaecd 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/utils/ImmutableGraph.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/utils/ImmutableGraph.kt @@ -1,6 +1,7 @@ package org.fcitx.fcitx5.android.utils -import java.util.* +import java.util.LinkedList +import java.util.Queue class ImmutableGraph( edges: List> @@ -32,24 +33,30 @@ class ImmutableGraph( } } - fun bfs(vertex: V): List> { + /** + * @param predicate: whether to continue searching after this node + */ + fun bfs(vertex: V, predicate: (Int, V, L) -> Boolean = { _, _, _ -> true }): List> { val start = vertices.indexOf(vertex).takeIf { it != -1 } ?: return emptyList() val visited = BooleanArray(vertices.size) - val queue: Queue> = LinkedList() + val queue: Queue> = LinkedList() val result = mutableListOf>() + var level = 0 visited[start] = true - queue.add(start to -1) + queue.add(Triple(start, -1, true)) while (queue.isNotEmpty()) { - val (x, v) = queue.remove() + val (x, v, cont) = queue.remove() if (start != x) result.add(x to v) - visited.indices.forEach { i -> - val l = adjacencyMatrix[x][i].takeIf { it != -1 } - if (l != null && !visited[i]) { - queue.add(i to l) - visited[i] = true + if (cont) + visited.indices.forEach { i -> + val l = adjacencyMatrix[x][i].takeIf { it != -1 } + if (l != null && !visited[i]) { + queue.add(Triple(i, l, predicate(level, vertices[i], labels[l]))) + visited[i] = true + } } - } + level++ } return result.map { (v, l) -> vertices[v] to labels[l] } } diff --git a/flake.lock b/flake.lock index 908397cc3..29e0c7216 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1650374568, - "narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=", + "lastModified": 1673956053, + "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", "owner": "edolstra", "repo": "flake-compat", - "rev": "b4a34015c698c7793d592d66adbab377907a2be8", + "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", "type": "github" }, "original": { @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1667811565, - "narHash": "sha256-HYml7RdQPQ7X13VNe2CoDMqmifsXbt4ACTKxHRKQE3Q=", + "lastModified": 1675183161, + "narHash": "sha256-Zq8sNgAxDckpn7tJo7V1afRSk2eoVbu3OjI1QklGLNg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "667e5581d16745bcda791300ae7e2d73f49fff25", + "rev": "e1e1b192c1a5aab2960bf0a0bd53a2e8124fa18e", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 0feec20c1..e91ab0cec 100644 --- a/flake.nix +++ b/flake.nix @@ -45,9 +45,9 @@ fcitx5-android-sdk = rec { cmakeVersion = "3.22.1"; buildToolsVersion = "33.0.0"; - platformToolsVersion = "33.0.2"; + platformToolsVersion = "33.0.3"; platformVersion = "33"; - ndkVersion = "24.0.8215888"; + ndkVersion = "25.0.8775105"; abiVersions = [ "arm64-v8a" "armeabi-v7a" ]; androidComposition = prev.androidenv.composeAndroidPackages { inherit platformToolsVersion ndkVersion; From 872eb9ac333539e7d04fd9203e51c73285b75cd3 Mon Sep 17 00:00:00 2001 From: Rocka Date: Mon, 6 Feb 2023 12:34:34 +0800 Subject: [PATCH 002/624] Fix DynamicList multi select when click checkbox --- .../android/ui/common/DynamicListAdapter.kt | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/ui/common/DynamicListAdapter.kt b/app/src/main/java/org/fcitx/fcitx5/android/ui/common/DynamicListAdapter.kt index c20493633..8519c9b61 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/ui/common/DynamicListAdapter.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/ui/common/DynamicListAdapter.kt @@ -94,28 +94,31 @@ abstract class DynamicListAdapter( multiselectCheckBox.isChecked = item in selected if (enableAddAndDelete && removable(item)) { + multiselectCheckBox.setOnCheckedChangeListener { _, isChecked -> + select(item, isChecked) + } nameText.setOnLongClickListener { itemTouchHelper?.startDrag(holder) true } nameText.setOnClickListener { - select(item, multiselectCheckBox) + multiselectCheckBox.toggle() } } else { multiselectCheckBox.isEnabled = false + multiselectCheckBox.setOnCheckedChangeListener(null) + nameText.setOnClickListener(null) } } } - private fun select(item: T, checkBox: CheckBox) { + private fun select(item: T, shouldSelect: Boolean) { if (!enableAddAndDelete || !multiselect) return - if (item in selected) { - selected.remove(item) - checkBox.isChecked = false + if (shouldSelect) { + if (selected.indexOf(item) == -1) selected.add(item) } else { - selected.add(item) - checkBox.isChecked = true + selected.remove(item) } } From 650dede9c012a18486e974c9c203f1b4fb41a375 Mon Sep 17 00:00:00 2001 From: Potato Hatsue <1793913507@qq.com> Date: Sat, 11 Feb 2023 16:16:01 -0500 Subject: [PATCH 003/624] Refactor popup with PopupAction --- .../android/input/keyboard/BaseKeyboard.kt | 141 +++++++++++------- .../android/input/keyboard/KeyboardWindow.kt | 12 +- .../android/input/keyboard/TextKeyboard.kt | 32 ++-- .../android/input/picker/PickerPageUi.kt | 64 ++++---- .../input/picker/PickerPagesAdapter.kt | 10 +- .../android/input/picker/PickerWindow.kt | 45 +++--- .../fcitx5/android/input/popup/PopupAction.kt | 50 +++++++ .../input/popup/PopupActionListener.kt | 7 + .../android/input/popup/PopupComponent.kt | 57 +++---- .../android/input/popup/PopupListener.kt | 15 -- 10 files changed, 248 insertions(+), 185 deletions(-) create mode 100644 app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupAction.kt create mode 100644 app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupActionListener.kt delete mode 100644 app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupListener.kt diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/BaseKeyboard.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/BaseKeyboard.kt index c5a5747b5..6ba97b757 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/BaseKeyboard.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/BaseKeyboard.kt @@ -10,15 +10,29 @@ import androidx.annotation.DrawableRes import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.children import org.fcitx.fcitx5.android.R -import org.fcitx.fcitx5.android.core.* +import org.fcitx.fcitx5.android.core.FcitxEvent +import org.fcitx.fcitx5.android.core.FcitxKeyMapping +import org.fcitx.fcitx5.android.core.InputMethodEntry +import org.fcitx.fcitx5.android.core.KeyStates +import org.fcitx.fcitx5.android.core.KeySym import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.data.theme.Theme import org.fcitx.fcitx5.android.input.keyboard.CustomGestureView.GestureType import org.fcitx.fcitx5.android.input.keyboard.CustomGestureView.OnGestureListener -import org.fcitx.fcitx5.android.input.popup.PopupListener +import org.fcitx.fcitx5.android.input.popup.PopupAction +import org.fcitx.fcitx5.android.input.popup.PopupActionListener import splitties.bitflags.hasFlag import splitties.dimensions.dp -import splitties.views.dsl.constraintlayout.* +import splitties.views.dsl.constraintlayout.above +import splitties.views.dsl.constraintlayout.after +import splitties.views.dsl.constraintlayout.before +import splitties.views.dsl.constraintlayout.below +import splitties.views.dsl.constraintlayout.bottomOfParent +import splitties.views.dsl.constraintlayout.constraintLayout +import splitties.views.dsl.constraintlayout.endOfParent +import splitties.views.dsl.constraintlayout.lParams +import splitties.views.dsl.constraintlayout.startOfParent +import splitties.views.dsl.constraintlayout.topOfParent import splitties.views.dsl.core.add import splitties.views.imageResource import timber.log.Timber @@ -38,7 +52,7 @@ abstract class BaseKeyboard( private val vivoKeypressWorkaround by AppPrefs.getInstance().advanced.vivoKeypressWorkaround - var keyPopupListener: PopupListener? = null + var popupActionListener: PopupActionListener? = null private val selectionSwipeThreshold = dp(10f) private val inputSwipeThreshold = dp(36f) @@ -109,6 +123,7 @@ abstract class BaseKeyboard( true } } + else -> false } } @@ -125,10 +140,12 @@ abstract class BaseKeyboard( true } else false } + GestureType.Up -> { onAction(KeyAction.DeleteSelectionAction(event.totalX)) false } + else -> false } } @@ -140,18 +157,21 @@ abstract class BaseKeyboard( onAction(it.action) } } + is KeyDef.Behavior.LongPress -> { setOnLongClickListener { _ -> onAction(it.action) true } } + is KeyDef.Behavior.Repeat -> { repeatEnabled = true onRepeatListener = { _ -> onAction(it.action) } } + is KeyDef.Behavior.Swipe -> { swipeEnabled = true swipeThresholdY = inputSwipeThreshold @@ -166,10 +186,12 @@ abstract class BaseKeyboard( false } } + else -> false } || oldOnGestureListener.onGesture(view, event) } } + is KeyDef.Behavior.DoubleTap -> { doubleTapEnabled = true onDoubleTapListener = { _ -> @@ -184,7 +206,7 @@ abstract class BaseKeyboard( is KeyDef.Popup.Menu -> { setOnLongClickListener { view -> view as KeyView - onPopupMenu(view.id, it, view.bounds) + onPopupAction(PopupAction.ShowMenuAction(view.id, it, view.bounds)) // do not consume this LongClick gesture false } @@ -196,17 +218,20 @@ abstract class BaseKeyboard( GestureType.Move -> { onPopupChangeFocus(view.id, event.x, event.y) } + GestureType.Up -> { onPopupTrigger(view.id) } + else -> false } || oldOnGestureListener.onGesture(view, event) } } + is KeyDef.Popup.Keyboard -> { setOnLongClickListener { view -> view as KeyView - onPopupKeyboard(view.id, it, view.bounds) + onPopupAction(PopupAction.ShowKeyboardAction(view.id, it, view.bounds)) // do not consume this LongClick gesture false } @@ -218,46 +243,60 @@ abstract class BaseKeyboard( GestureType.Move -> { onPopupChangeFocus(view.id, event.x, event.y) } + GestureType.Up -> { onPopupTrigger(view.id) } + else -> false } || oldOnGestureListener.onGesture(view, event) } } + is KeyDef.Popup.AltPreview -> { val oldOnGestureListener = onGestureListener ?: OnGestureListener.Empty onGestureListener = OnGestureListener { view, event -> view as KeyView - when (event.type) { - GestureType.Down -> { - onPopupPreview(view.id, it.content, view.bounds) - } - GestureType.Move -> { - val triggered = swipeSymbolDirection.checkY(event.totalY) - val text = if (triggered) it.alternative else it.content - onPopupPreviewUpdate(view.id, text) - } - GestureType.Up -> { - onPopupDismiss(view.id) + if (popupOnKeyPress) { + when (event.type) { + GestureType.Down -> onPopupAction( + PopupAction.PreviewAction(view.id, it.content, view.bounds) + ) + + GestureType.Move -> { + val triggered = swipeSymbolDirection.checkY(event.totalY) + val text = if (triggered) it.alternative else it.content + onPopupAction( + PopupAction.PreviewUpdateAction(view.id, text) + ) + } + + GestureType.Up -> { + onPopupAction(PopupAction.DismissAction(view.id)) + } } } // never consume gesture in preview popup oldOnGestureListener.onGesture(view, event) } } + is KeyDef.Popup.Preview -> { val oldOnGestureListener = onGestureListener ?: OnGestureListener.Empty onGestureListener = OnGestureListener { view, event -> view as KeyView - when (event.type) { - GestureType.Down -> { - onPopupPreview(view.id, it.content, view.bounds) - } - GestureType.Up -> { - onPopupDismiss(view.id) + if (popupOnKeyPress) { + when (event.type) { + GestureType.Down -> onPopupAction( + PopupAction.PreviewAction(view.id, it.content, view.bounds) + ) + + GestureType.Up -> { + onPopupAction(PopupAction.DismissAction(view.id)) + } + + else -> {} } - else -> {} } // never consume gesture in preview popup oldOnGestureListener.onGesture(view, event) @@ -354,6 +393,7 @@ abstract class BaseKeyboard( ) return true } + MotionEvent.ACTION_POINTER_DOWN -> { val i = event.actionIndex val target = findTargetChild(event.getX(i), event.getY(i)) ?: return false @@ -363,6 +403,7 @@ abstract class BaseKeyboard( ) return true } + MotionEvent.ACTION_MOVE -> { for (i in 0 until event.pointerCount) { val target = touchTarget[event.getPointerId(i)] ?: continue @@ -372,6 +413,7 @@ abstract class BaseKeyboard( } return true } + MotionEvent.ACTION_UP -> { val i = event.actionIndex val pid = event.getPointerId(i) @@ -382,6 +424,7 @@ abstract class BaseKeyboard( touchTarget.remove(pid) return true } + MotionEvent.ACTION_POINTER_UP -> { val i = event.actionIndex val pid = event.getPointerId(i) @@ -392,6 +435,7 @@ abstract class BaseKeyboard( touchTarget.remove(pid) return true } + MotionEvent.ACTION_CANCEL -> { val i = event.actionIndex val pid = event.getPointerId(i) @@ -408,7 +452,7 @@ abstract class BaseKeyboard( } @CallSuper - open fun onAction( + protected open fun onAction( action: KeyAction, source: KeyActionListener.Source = KeyActionListener.Source.Keyboard ) { @@ -416,42 +460,27 @@ abstract class BaseKeyboard( } @CallSuper - open fun onPopupPreview(viewId: Int, content: String, bounds: Rect) { - if (!popupOnKeyPress) return - keyPopupListener?.onPreview(viewId, content, bounds) + protected open fun onPopupAction(action: PopupAction) { + popupActionListener?.onPopupAction(action) } - @CallSuper - open fun onPopupPreviewUpdate(viewId: Int, content: String) { - if (!popupOnKeyPress) return - keyPopupListener?.onPreviewUpdate(viewId, content) + private fun onPopupChangeFocus(viewId: Int, x: Float, y: Float): Boolean { + var result = false + popupActionListener?.onPopupAction(PopupAction.ChangeFocusAction(viewId, x, y) { + result = it + }) + return result } - @CallSuper - open fun onPopupDismiss(viewId: Int) { - keyPopupListener?.onDismiss(viewId) - } - - @CallSuper - open fun onPopupKeyboard(viewId: Int, keyboard: KeyDef.Popup.Keyboard, bounds: Rect) { - keyPopupListener?.onShowKeyboard(viewId, keyboard, bounds) - } - - open fun onPopupMenu(viewId: Int, menu: KeyDef.Popup.Menu, bounds: Rect) { - keyPopupListener?.onShowMenu(viewId, menu, bounds) - } - - @CallSuper - open fun onPopupChangeFocus(viewId: Int, x: Float, y: Float): Boolean { - return keyPopupListener?.onChangeFocus(viewId, x, y) ?: false - } - - @CallSuper - fun onPopupTrigger(viewId: Int): Boolean { + private fun onPopupTrigger(viewId: Int): Boolean { + var action: KeyAction? = null // ask popup keyboard whether there's a pending KeyAction - val action = keyPopupListener?.onTrigger(viewId) ?: return false - onAction(action, KeyActionListener.Source.Popup) - onPopupDismiss(viewId) + onPopupAction(PopupAction.TriggerAction(viewId) { + action = it + }) + if (action == null) return false + onAction(action!!, KeyActionListener.Source.Popup) + onPopupAction(PopupAction.DismissAction(viewId)) return true } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt index 7c298acb9..4d87e8db3 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt @@ -17,8 +17,8 @@ import org.fcitx.fcitx5.android.input.dependency.fcitx import org.fcitx.fcitx5.android.input.dependency.inputMethodService import org.fcitx.fcitx5.android.input.dependency.theme import org.fcitx.fcitx5.android.input.picker.PickerWindow +import org.fcitx.fcitx5.android.input.popup.PopupActionListener import org.fcitx.fcitx5.android.input.popup.PopupComponent -import org.fcitx.fcitx5.android.input.popup.PopupListener import org.fcitx.fcitx5.android.input.wm.EssentialWindow import org.fcitx.fcitx5.android.input.wm.InputWindow import org.fcitx.fcitx5.android.input.wm.InputWindowManager @@ -77,7 +77,7 @@ class KeyboardWindow : InputWindow.SimpleInputWindow(), Essentia } } - private val popupListener: PopupListener by lazy { + private val popupActionListener: PopupActionListener by lazy { popup.listener } @@ -93,7 +93,7 @@ class KeyboardWindow : InputWindow.SimpleInputWindow(), Essentia it.onDetach() keyboardView.removeView(it) it.keyActionListener = null - it.keyPopupListener = null + it.popupActionListener = null } } @@ -101,7 +101,7 @@ class KeyboardWindow : InputWindow.SimpleInputWindow(), Essentia currentKeyboardName = target currentKeyboard?.let { it.keyActionListener = keyActionListener - it.keyPopupListener = popupListener + it.popupActionListener = popupActionListener keyboardView.apply { add(it, lParams(matchParent, matchParent)) } it.onAttach(service.editorInfo) it.onInputMethodChange(fcitx.runImmediately { inputMethodEntryCached }) @@ -154,14 +154,14 @@ class KeyboardWindow : InputWindow.SimpleInputWindow(), Essentia override fun onAttached() { currentKeyboard?.let { it.keyActionListener = keyActionListener - it.keyPopupListener = popupListener + it.popupActionListener = popupActionListener } } override fun onDetached() { currentKeyboard?.let { it.keyActionListener = null - it.keyPopupListener = null + it.popupActionListener = null } popup.dismissAll() } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt index e418534d3..fc09bca5b 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt @@ -2,7 +2,6 @@ package org.fcitx.fcitx5.android.input.keyboard import android.annotation.SuppressLint import android.content.Context -import android.graphics.Rect import android.view.inputmethod.EditorInfo import androidx.core.view.allViews import org.fcitx.fcitx5.android.R @@ -11,6 +10,7 @@ import org.fcitx.fcitx5.android.core.InputMethodEntry import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.data.prefs.ManagedPreference import org.fcitx.fcitx5.android.data.theme.Theme +import org.fcitx.fcitx5.android.input.popup.PopupAction import splitties.resources.drawable import splitties.views.imageDrawable import splitties.views.imageResource @@ -116,8 +116,7 @@ class TextKeyboard( transformKeyAction(action) } is KeyAction.CapsAction -> switchCapsState(action.lock) - else -> { - } + else -> {} } super.onAction(action, source) } @@ -156,20 +155,19 @@ class TextKeyboard( } } - override fun onPopupPreview(viewId: Int, content: String, bounds: Rect) { - super.onPopupPreview(viewId, transformInputString(content), bounds) - } - - override fun onPopupPreviewUpdate(viewId: Int, content: String) { - super.onPopupPreviewUpdate(viewId, transformInputString(content)) - } - - override fun onPopupKeyboard(viewId: Int, keyboard: KeyDef.Popup.Keyboard, bounds: Rect) { - val label = keyboard.label - val k = if (label.length == 1 && label[0].isLetter()) - KeyDef.Popup.Keyboard(transformAlphabet(label)) - else keyboard - super.onPopupKeyboard(viewId, k, bounds) + override fun onPopupAction(action: PopupAction) { + val newAction = when (action) { + is PopupAction.PreviewAction -> action.copy(content = transformInputString(action.content)) + is PopupAction.PreviewUpdateAction -> action.copy(content = transformInputString(action.content)) + is PopupAction.ShowKeyboardAction -> { + val label = action.keyboard.label + if (label.length == 1 && label[0].isLetter()) + action.copy(keyboard = KeyDef.Popup.Keyboard(transformAlphabet(label))) + else action + } + else -> action + } + super.onPopupAction(newAction) } private fun switchCapsState(lock: Boolean = false) { diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerPageUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerPageUi.kt index 391ff8536..7f23d440d 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerPageUi.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerPageUi.kt @@ -1,7 +1,6 @@ package org.fcitx.fcitx5.android.input.picker import android.content.Context -import android.graphics.Rect import android.util.TypedValue import android.view.ViewGroup import androidx.core.view.updateLayoutParams @@ -18,7 +17,8 @@ import org.fcitx.fcitx5.android.input.keyboard.KeyActionListener.Source import org.fcitx.fcitx5.android.input.keyboard.KeyDef.Appearance import org.fcitx.fcitx5.android.input.keyboard.KeyDef.Appearance.Border import org.fcitx.fcitx5.android.input.keyboard.KeyDef.Appearance.Variant -import org.fcitx.fcitx5.android.input.popup.PopupListener +import org.fcitx.fcitx5.android.input.popup.PopupAction +import org.fcitx.fcitx5.android.input.popup.PopupActionListener import splitties.views.dsl.constraintlayout.* import splitties.views.dsl.core.Ui import splitties.views.dsl.core.add @@ -26,7 +26,7 @@ import splitties.views.dsl.core.matchParent import splitties.views.gravityCenter import splitties.views.lines -class PickerPageUi(override val ctx: Context, val theme: Theme, val density: Density) : Ui { +class PickerPageUi(override val ctx: Context, val theme: Theme, private val density: Density) : Ui { enum class Density( val pageSize: Int, @@ -59,7 +59,7 @@ class PickerPageUi(override val ctx: Context, val theme: Theme, val density: Den } var keyActionListener: KeyActionListener? = null - var popupListener: PopupListener? = null + var popupActionListener: PopupActionListener? = null private val keyAppearance = Appearance.Text( displayText = "", @@ -137,6 +137,7 @@ class PickerPageUi(override val ctx: Context, val theme: Theme, val density: Den }) } } + Density.Medium, Density.Low -> { keyViews.forEachIndexed { i, keyView -> val row = i / columnCount @@ -212,7 +213,14 @@ class PickerPageUi(override val ctx: Context, val theme: Theme, val density: Den // the actual bounds on press. see [^1] as well view.updateBounds() } - onPopupKeyboard(view.id, text, view.bounds) + // TODO: maybe popup keyboard should just accept String as label? + onPopupAction( + PopupAction.ShowKeyboardAction( + view.id, + KeyDef.Popup.Keyboard(text), + bounds + ) + ) false } swipeEnabled = true @@ -226,16 +234,20 @@ class PickerPageUi(override val ctx: Context, val theme: Theme, val density: Den // eg. it's inside the next page of ViewPager // so update bounds when it's pressed view.updateBounds() - onPopupPreview(view.id, text, view.bounds) + onPopupAction( + PopupAction.PreviewAction(view.id, text, view.bounds) + ) } false } + CustomGestureView.GestureType.Move -> { - onPopupKeyboardChangeFocus(view.id, event.x, event.y) + onPopupChangeFocus(view.id, event.x, event.y) } + CustomGestureView.GestureType.Up -> { - onPopupKeyboardTrigger(view.id).also { - onPopupDismiss(view.id) + onPopupTrigger(view.id).also { + onPopupAction(PopupAction.DismissAction(view.id)) } } } @@ -245,28 +257,28 @@ class PickerPageUi(override val ctx: Context, val theme: Theme, val density: Den } } - private fun onPopupPreview(viewId: Int, content: String, bounds: Rect) { - popupListener?.onPreview(viewId, content, bounds) - } - - private fun onPopupDismiss(viewId: Int) { - popupListener?.onDismiss(viewId) - } - - private fun onPopupKeyboard(viewId: Int, label: String, bounds: Rect) { - // TODO: maybe popup keyboard should just accept String as label? - popupListener?.onShowKeyboard(viewId, KeyDef.Popup.Keyboard(label), bounds) + private fun onPopupAction(action: PopupAction) { + popupActionListener?.onPopupAction(action) } - private fun onPopupKeyboardChangeFocus(viewId: Int, x: Float, y: Float): Boolean { - return popupListener?.onChangeFocus(viewId, x, y) ?: false + private fun onPopupChangeFocus(viewId: Int, x: Float, y: Float): Boolean { + var result = false + popupActionListener?.onPopupAction(PopupAction.ChangeFocusAction(viewId, x, y) { + result = it + }) + return result } - private fun onPopupKeyboardTrigger(viewId: Int): Boolean { + private fun onPopupTrigger(viewId: Int): Boolean { + var action: FcitxKeyAction? = null // TODO: maybe popup keyboard should just yield String value? - val action = popupListener?.onTrigger(viewId) as? FcitxKeyAction ?: return false - onSymbolClick(action.act) - onPopupDismiss(viewId) + onPopupAction(PopupAction.TriggerAction(viewId) { + action = it as? FcitxKeyAction + }) + if (action == null) return false + onSymbolClick(action!!.act) + onPopupAction(PopupAction.DismissAction(viewId)) return true } + } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerPagesAdapter.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerPagesAdapter.kt index d453138d7..6f39e6b52 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerPagesAdapter.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerPagesAdapter.kt @@ -5,12 +5,12 @@ import androidx.recyclerview.widget.RecyclerView import org.fcitx.fcitx5.android.data.RecentlyUsed import org.fcitx.fcitx5.android.data.theme.Theme import org.fcitx.fcitx5.android.input.keyboard.KeyActionListener -import org.fcitx.fcitx5.android.input.popup.PopupListener +import org.fcitx.fcitx5.android.input.popup.PopupActionListener class PickerPagesAdapter( val theme: Theme, private val keyActionListener: KeyActionListener, - private val popupListener: PopupListener, + private val popupActionListener: PopupActionListener, data: List>>, val density: PickerPageUi.Density, recentlyUsedFileName: String @@ -117,18 +117,18 @@ class PickerPagesAdapter( holder.ui.keyActionListener = keyActionListener if (holder.bindingAdapterPosition == 0) { // prevent popup on RecentlyUsed page - holder.ui.popupListener = null + holder.ui.popupActionListener = null // update RecentlyUsed when it's page attached updateRecent() holder.ui.setItems(pages[0]) } else { - holder.ui.popupListener = popupListener + holder.ui.popupActionListener = popupActionListener } } override fun onViewDetachedFromWindow(holder: ViewHolder) { holder.ui.keyActionListener = null - holder.ui.popupListener = null + holder.ui.popupActionListener = null } companion object { diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerWindow.kt index 8dd0d82f2..b1eebd72d 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerWindow.kt @@ -1,6 +1,5 @@ package org.fcitx.fcitx5.android.input.picker -import android.graphics.Rect import android.view.Gravity import androidx.core.content.ContextCompat import androidx.transition.Slide @@ -9,8 +8,9 @@ import androidx.viewpager2.widget.ViewPager2 import org.fcitx.fcitx5.android.input.broadcast.InputBroadcastReceiver import org.fcitx.fcitx5.android.input.dependency.theme import org.fcitx.fcitx5.android.input.keyboard.* +import org.fcitx.fcitx5.android.input.popup.PopupAction +import org.fcitx.fcitx5.android.input.popup.PopupActionListener import org.fcitx.fcitx5.android.input.popup.PopupComponent -import org.fcitx.fcitx5.android.input.popup.PopupListener import org.fcitx.fcitx5.android.input.wm.EssentialWindow import org.fcitx.fcitx5.android.input.wm.InputWindow import org.fcitx.fcitx5.android.input.wm.InputWindowManager @@ -64,11 +64,13 @@ class PickerWindow( windowManager.attachWindow(KeyboardWindow) } } + is KeyAction.FcitxKeyAction -> { // we want the behavior of CommitAction (commit the character as-is), // but don't want to include it in recently used list commonKeyActionListener.listener.onKeyAction(KeyAction.CommitAction(it.act), source) } + else -> { if (it is KeyAction.CommitAction) { pickerPagesAdapter.insertRecent(it.text) @@ -78,35 +80,30 @@ class PickerWindow( } } - private val popupListener: PopupListener by lazy { - object : PopupListener by popup.listener { - override fun onPreview(viewId: Int, content: String, bounds: Rect) { - if (!popupPreview) return - popup.listener.onPreview(viewId, content, bounds) - } - - override fun onShowKeyboard( - viewId: Int, - keyboard: KeyDef.Popup.Keyboard, - bounds: Rect - ) { - // prevent ViewPager from consuming swipe gesture when popup keyboard shown - pickerLayout.pager.isUserInputEnabled = false - popup.listener.onShowKeyboard(viewId, keyboard, bounds) - } - - override fun onDismiss(viewId: Int) { - popup.listener.onDismiss(viewId) - // restore ViewPager scrolling - pickerLayout.pager.isUserInputEnabled = true + private val popupActionListener: PopupActionListener by lazy { + PopupActionListener { + when (it) { + is PopupAction.PreviewAction -> { + if (!popupPreview) return@PopupActionListener + } + is PopupAction.ShowKeyboardAction -> { + // prevent ViewPager from consuming swipe gesture when popup keyboard shown + pickerLayout.pager.isUserInputEnabled = false + } + is PopupAction.DismissAction -> { + // restore ViewPager scrolling + pickerLayout.pager.isUserInputEnabled = true + } + else -> {} } + popup.listener.onPopupAction(it) } } override fun onCreateView() = PickerLayout(context, theme, switchKey).apply { pickerLayout = this pickerPagesAdapter = PickerPagesAdapter( - theme, keyActionListener, popupListener, data, density, key.name + theme, keyActionListener, popupActionListener, data, density, key.name ) tabsUi.apply { setTabs(pickerPagesAdapter.categories) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupAction.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupAction.kt new file mode 100644 index 000000000..c023d7d81 --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupAction.kt @@ -0,0 +1,50 @@ +package org.fcitx.fcitx5.android.input.popup + +import android.graphics.Rect +import org.fcitx.fcitx5.android.input.keyboard.KeyAction +import org.fcitx.fcitx5.android.input.keyboard.KeyDef + +sealed class PopupAction { + + abstract val viewId: Int + + data class PreviewAction( + override val viewId: Int, + val content: String, + val bounds: Rect + ) : PopupAction() + + data class PreviewUpdateAction( + override val viewId: Int, + val content: String, + ) : PopupAction() + + data class DismissAction( + override val viewId: Int + ) : PopupAction() + + data class ShowKeyboardAction( + override val viewId: Int, + val keyboard: KeyDef.Popup.Keyboard, + val bounds: Rect + ) : PopupAction() + + data class ShowMenuAction( + override val viewId: Int, + val menu: KeyDef.Popup.Menu, + val bounds: Rect + ) : PopupAction() + + data class ChangeFocusAction( + override val viewId: Int, + val x: Float, + val y: Float, + val cont: ((Boolean) -> Unit) = {} + ) : PopupAction() + + data class TriggerAction( + override val viewId: Int, + val cont: ((KeyAction?) -> Unit) = {} + ) : PopupAction() +} + diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupActionListener.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupActionListener.kt new file mode 100644 index 000000000..a60ed162d --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupActionListener.kt @@ -0,0 +1,7 @@ +package org.fcitx.fcitx5.android.input.popup + +fun interface PopupActionListener { + + fun onPopupAction(action: PopupAction) + +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupComponent.kt index 69aff3e9a..a29dc0589 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupComponent.kt @@ -21,7 +21,7 @@ import splitties.dimensions.dp import splitties.views.dsl.core.add import splitties.views.dsl.core.frameLayout import splitties.views.dsl.core.lParams -import java.util.* +import java.util.LinkedList class PopupComponent : UniqueComponent(), Dependent, ManagedHandler by managedHandler() { @@ -61,7 +61,7 @@ class PopupComponent : } } - fun showPopup(viewId: Int, content: String, bounds: Rect) { + private fun showPopup(viewId: Int, content: String, bounds: Rect) { showingEntryUi[viewId]?.apply { dismissJobs[viewId]?.also { dismissJobs.remove(viewId)?.cancel() @@ -85,11 +85,11 @@ class PopupComponent : showingEntryUi[viewId] = popup } - fun updatePopup(viewId: Int, content: String) { + private fun updatePopup(viewId: Int, content: String) { showingEntryUi[viewId]?.setText(content) } - fun showKeyboard(viewId: Int, keyboard: KeyDef.Popup.Keyboard, bounds: Rect) { + private fun showKeyboard(viewId: Int, keyboard: KeyDef.Popup.Keyboard, bounds: Rect) { val keys = PopupPreset[keyboard.label] ?: return // clear popup preview text OR create empty popup preview showingEntryUi[viewId]?.setText("") ?: showPopup(viewId, "", bounds) @@ -122,7 +122,7 @@ class PopupComponent : showingContainerUi[viewId] = keyboardUi } - fun showMenu(viewId: Int, menu: KeyDef.Popup.Menu, bounds: Rect) { + private fun showMenu(viewId: Int, menu: KeyDef.Popup.Menu, bounds: Rect) { showingEntryUi[viewId]?.let { dismissPopupEntry(viewId, it) } @@ -142,15 +142,15 @@ class PopupComponent : showingContainerUi[viewId] = menuUi } - fun changeFocus(viewId: Int, x: Float, y: Float): Boolean { + private fun changeFocus(viewId: Int, x: Float, y: Float): Boolean { return showingContainerUi[viewId]?.changeFocus(x, y) ?: false } - fun triggerFocused(viewId: Int): KeyAction? { + private fun triggerFocused(viewId: Int): KeyAction? { return showingContainerUi[viewId]?.onTrigger() } - fun dismissPopup(viewId: Int) { + private fun dismissPopup(viewId: Int) { dismissPopupContainer(viewId) showingEntryUi[viewId]?.also { val timeLeft = it.lastShowTime + hideThreshold - System.currentTimeMillis() @@ -198,33 +198,18 @@ class PopupComponent : showingEntryUi.clear() } - val listener: PopupListener = object : PopupListener { - override fun onPreview(viewId: Int, content: String, bounds: Rect) { - showPopup(viewId, content, bounds) - } - - override fun onPreviewUpdate(viewId: Int, content: String) { - updatePopup(viewId, content) - } - - override fun onDismiss(viewId: Int) { - dismissPopup(viewId) - } - - override fun onShowKeyboard(viewId: Int, keyboard: KeyDef.Popup.Keyboard, bounds: Rect) { - showKeyboard(viewId, keyboard, bounds) - } - - override fun onShowMenu(viewId: Int, menu: KeyDef.Popup.Menu, bounds: Rect) { - showMenu(viewId, menu, bounds) - } - - override fun onChangeFocus(viewId: Int, x: Float, y: Float): Boolean { - return changeFocus(viewId, x, y) - } - - override fun onTrigger(viewId: Int): KeyAction? { - return triggerFocused(viewId) + val listener: PopupActionListener = + PopupActionListener { action -> + with(action) { + when (this) { + is PopupAction.ChangeFocusAction -> cont(changeFocus(viewId, x, y)) + is PopupAction.DismissAction -> dismissPopup(viewId) + is PopupAction.PreviewAction -> showPopup(viewId, content, bounds) + is PopupAction.PreviewUpdateAction -> updatePopup(viewId, content) + is PopupAction.ShowKeyboardAction -> showKeyboard(viewId, keyboard, bounds) + is PopupAction.ShowMenuAction -> showMenu(viewId, menu, bounds) + is PopupAction.TriggerAction -> cont(triggerFocused(viewId)) + } + } } - } } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupListener.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupListener.kt deleted file mode 100644 index 648285dd1..000000000 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupListener.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.fcitx.fcitx5.android.input.popup - -import android.graphics.Rect -import org.fcitx.fcitx5.android.input.keyboard.KeyAction -import org.fcitx.fcitx5.android.input.keyboard.KeyDef - -interface PopupListener { - fun onPreview(viewId: Int, content: String, bounds: Rect) - fun onPreviewUpdate(viewId: Int, content: String) - fun onDismiss(viewId: Int) - fun onShowKeyboard(viewId: Int, keyboard: KeyDef.Popup.Keyboard, bounds: Rect) - fun onShowMenu(viewId: Int, menu: KeyDef.Popup.Menu, bounds: Rect) - fun onChangeFocus(viewId: Int, x: Float, y: Float): Boolean - fun onTrigger(viewId: Int): KeyAction? -} From 0a0cb67f4c396eee0adee2713ba8995c461a3179 Mon Sep 17 00:00:00 2001 From: Potato Hatsue <1793913507@qq.com> Date: Fri, 10 Feb 2023 23:52:11 -0500 Subject: [PATCH 004/624] Show number row on bar when inputing password --- .../android/input/bar/KawaiiBarComponent.kt | 60 +++++++++++++++++-- .../input/bar/KawaiiBarStateMachine.kt | 22 +++++-- .../fcitx5/android/input/bar/KawaiiBarUi.kt | 57 +++++++++++++++++- 3 files changed, 128 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt index 309c61358..3b2369e1d 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt @@ -2,6 +2,7 @@ package org.fcitx.fcitx5.android.input.bar import android.graphics.Color import android.os.Build +import android.text.InputType import android.view.KeyEvent import android.view.View import android.view.inputmethod.EditorInfo @@ -17,9 +18,23 @@ import org.fcitx.fcitx5.android.data.clipboard.ClipboardManager import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.data.prefs.ManagedPreference import org.fcitx.fcitx5.android.data.theme.ThemeManager -import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.State.* -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.* -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.* +import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.State.ClickToAttachWindow +import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.State.ClickToDetachWindow +import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.State.Hidden +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.ClipboardUpdatedEmpty +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.ClipboardUpdatedNonEmpty +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.KawaiiBarShown +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.MenuButtonClicked +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.Pasted +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.Timeout +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.CandidateUpdateNonEmpty +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.CapFlagsUpdatedNoPassword +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.CapFlagsUpdatedPassword +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.ExtendedWindowAttached +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.PreeditUpdatedEmpty +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.PreeditUpdatedNonEmpty +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.WindowDetachedWithCandidatesEmpty +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.WindowDetachedWithCandidatesNonEmpty import org.fcitx.fcitx5.android.input.broadcast.InputBroadcastReceiver import org.fcitx.fcitx5.android.input.candidates.HorizontalCandidateComponent import org.fcitx.fcitx5.android.input.candidates.expanded.ExpandedCandidateStyle @@ -31,7 +46,9 @@ import org.fcitx.fcitx5.android.input.dependency.context import org.fcitx.fcitx5.android.input.dependency.inputMethodService import org.fcitx.fcitx5.android.input.dependency.theme import org.fcitx.fcitx5.android.input.editing.TextEditingWindow +import org.fcitx.fcitx5.android.input.keyboard.CommonKeyActionListener import org.fcitx.fcitx5.android.input.keyboard.KeyboardWindow +import org.fcitx.fcitx5.android.input.popup.PopupComponent import org.fcitx.fcitx5.android.input.status.StatusAreaWindow import org.fcitx.fcitx5.android.input.wm.InputWindow import org.fcitx.fcitx5.android.input.wm.InputWindowManager @@ -54,6 +71,8 @@ class KawaiiBarComponent : UniqueViewComponent( private val windowManager: InputWindowManager by manager.must() private val service by manager.inputMethodService() private val horizontalCandidate: HorizontalCandidateComponent by manager.must() + private val commonKeyActionListener: CommonKeyActionListener by manager.must() + private val popup: PopupComponent by manager.must() private val clipboardSuggestion = AppPrefs.getInstance().clipboard.clipboardSuggestion private val clipboardItemTimeout = AppPrefs.getInstance().clipboard.clipboardItemTimeout @@ -93,6 +112,7 @@ class KawaiiBarComponent : UniqueViewComponent( // renew timeout when clipboard suggestion is present launchClipboardTimeoutJob() } + else -> {} } } @@ -102,6 +122,10 @@ class KawaiiBarComponent : UniqueViewComponent( idleUiStateMachine = IdleUiStateMachine.new(it, idleUiStateMachine) } + private val popupActionListener by lazy { + popup.listener + } + init { ClipboardManager.addOnUpdateListener(onClipboardUpdateListener) clipboardSuggestion.registerOnChangeListener(onClipboardSuggestionUpdateListener) @@ -172,6 +196,8 @@ class KawaiiBarComponent : UniqueViewComponent( private val titleUi by lazy { KawaiiBarUi.Title(context, theme) } + private val numberRowUi by lazy { KawaiiBarUi.NumberRowUi(context, theme) } + private val barStateMachine = KawaiiBarStateMachine.new { switchUiByState(it) } @@ -182,10 +208,12 @@ class KawaiiBarComponent : UniqueViewComponent( setExpandButtonToAttach() setExpandButtonEnabled(true) } + ClickToDetachWindow -> { setExpandButtonToDetach() setExpandButtonEnabled(true) } + Hidden -> { setExpandButtonEnabled(false) } @@ -239,11 +267,20 @@ class KawaiiBarComponent : UniqueViewComponent( if (view.displayedChild == index) return Timber.d("Switch bar to $state") - if (view.getChildAt(index) != titleUi.root) { + val new = view.getChildAt(index) + if (new != titleUi.root) { titleUi.setReturnButtonOnClickListener { } titleUi.setTitle("") titleUi.removeExtension() } + if (new == numberRowUi.root) { + numberRowUi.root.keyActionListener = commonKeyActionListener.listener + numberRowUi.root.popupActionListener = popupActionListener + } else { + numberRowUi.root.keyActionListener = null + numberRowUi.root.popupActionListener = null + popup.dismissAll() + } view.displayedChild = index } @@ -255,6 +292,7 @@ class KawaiiBarComponent : UniqueViewComponent( add(idleUi.root, lParams(matchParent, matchParent)) add(candidateUi.root, lParams(matchParent, matchParent)) add(titleUi.root, lParams(matchParent, matchParent)) + add(numberRowUi.root, lParams(matchParent, matchParent)) } } @@ -292,6 +330,7 @@ class KawaiiBarComponent : UniqueViewComponent( } barStateMachine.push(ExtendedWindowAttached) } + is InputWindow.SimpleInputWindow<*> -> { } } @@ -305,4 +344,17 @@ class KawaiiBarComponent : UniqueViewComponent( WindowDetachedWithCandidatesNonEmpty ) } + + override fun onEditorInfoUpdate(info: EditorInfo?) { + info?.inputType?.and(InputType.TYPE_MASK_VARIATION)?.run { + if (equals(InputType.TYPE_TEXT_VARIATION_PASSWORD) || + equals(InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) || + equals(InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) + ) + barStateMachine.push(CapFlagsUpdatedPassword) + else + barStateMachine.push(CapFlagsUpdatedNoPassword) + + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarStateMachine.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarStateMachine.kt index 0198ab479..e9d55f2e5 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarStateMachine.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarStateMachine.kt @@ -1,12 +1,22 @@ package org.fcitx.fcitx5.android.input.bar -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.State.* -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.* +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.State.Candidate +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.State.Idle +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.State.NumberRow +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.State.Title +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.CandidateUpdateNonEmpty +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.CapFlagsUpdatedNoPassword +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.CapFlagsUpdatedPassword +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.ExtendedWindowAttached +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.PreeditUpdatedEmpty +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.PreeditUpdatedNonEmpty +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.WindowDetachedWithCandidatesEmpty +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.WindowDetachedWithCandidatesNonEmpty import org.fcitx.fcitx5.android.utils.eventStateMachine object KawaiiBarStateMachine { enum class State { - Idle, Candidate, Title + Idle, Candidate, Title, NumberRow } enum class TransitionEvent { @@ -15,7 +25,9 @@ object KawaiiBarStateMachine { CandidateUpdateNonEmpty, ExtendedWindowAttached, WindowDetachedWithCandidatesEmpty, - WindowDetachedWithCandidatesNonEmpty + WindowDetachedWithCandidatesNonEmpty, + CapFlagsUpdatedPassword, + CapFlagsUpdatedNoPassword, } fun new(block: (State) -> Unit) = eventStateMachine(Idle) { @@ -26,6 +38,8 @@ object KawaiiBarStateMachine { from(Title) transitTo Candidate on WindowDetachedWithCandidatesNonEmpty from(Candidate) transitTo Idle on PreeditUpdatedEmpty from(Candidate) transitTo Title on ExtendedWindowAttached + from(Idle) transitTo NumberRow on CapFlagsUpdatedPassword + from(NumberRow) transitTo Idle on CapFlagsUpdatedNoPassword onNewState(block) } } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarUi.kt index e06d483cf..4cf8c337b 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarUi.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarUi.kt @@ -1,5 +1,6 @@ package org.fcitx.fcitx5.android.input.bar +import android.annotation.SuppressLint import android.content.Context import android.graphics.PorterDuff import android.graphics.PorterDuffColorFilter @@ -15,14 +16,40 @@ import androidx.annotation.DrawableRes import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible import org.fcitx.fcitx5.android.R +import org.fcitx.fcitx5.android.core.KeySym import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.data.theme.Theme -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.State.* +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.State.Clipboard +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.State.ClipboardTimedOut +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.State.Empty +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.State.Toolbar +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.State.ToolbarWithClip +import org.fcitx.fcitx5.android.input.keyboard.BaseKeyboard import org.fcitx.fcitx5.android.input.keyboard.CustomGestureView +import org.fcitx.fcitx5.android.input.keyboard.KeyAction +import org.fcitx.fcitx5.android.input.keyboard.KeyDef import org.fcitx.fcitx5.android.utils.rippleDrawable import splitties.dimensions.dp -import splitties.views.dsl.constraintlayout.* -import splitties.views.dsl.core.* +import splitties.views.dsl.constraintlayout.after +import splitties.views.dsl.constraintlayout.before +import splitties.views.dsl.constraintlayout.bottomOfParent +import splitties.views.dsl.constraintlayout.centerHorizontally +import splitties.views.dsl.constraintlayout.centerVertically +import splitties.views.dsl.constraintlayout.constraintLayout +import splitties.views.dsl.constraintlayout.endOfParent +import splitties.views.dsl.constraintlayout.lParams +import splitties.views.dsl.constraintlayout.matchConstraints +import splitties.views.dsl.constraintlayout.startOfParent +import splitties.views.dsl.constraintlayout.topOfParent +import splitties.views.dsl.core.Ui +import splitties.views.dsl.core.add +import splitties.views.dsl.core.horizontalLayout +import splitties.views.dsl.core.imageView +import splitties.views.dsl.core.lParams +import splitties.views.dsl.core.matchParent +import splitties.views.dsl.core.textView +import splitties.views.dsl.core.verticalMargin +import splitties.views.dsl.core.wrapContent import splitties.views.gravityCenter import splitties.views.gravityVerticalCenter import splitties.views.imageResource @@ -224,19 +251,23 @@ sealed class KawaiiBarUi(override val ctx: Context, protected val theme: Theme) transitionToClipboardBar() enableClipboardItem() } + Toolbar -> { transitionToButtonsBar() disableClipboardItem() } + Empty -> { // empty and clipboard share the same view transitionToClipboardBar() disableClipboardItem() setClipboardItemText("") } + ToolbarWithClip -> { transitionToButtonsBar() } + ClipboardTimedOut -> { transitionToClipboardBar() } @@ -320,4 +351,24 @@ sealed class KawaiiBarUi(override val ctx: Context, protected val theme: Theme) } } + class NumberRowUi(ctx: Context, theme: Theme) : KawaiiBarUi(ctx, theme) { + @SuppressLint("ViewConstructor") + class Keyboard(ctx: Context, theme: Theme) : BaseKeyboard( + ctx, + theme, + listOf(listOf("1", "2", "3", "4", "5", "6", "7", "8", "9", "0").map(::NumKey)) + ) { + class NumKey(digit: String) : KeyDef( + Appearance.Text( + displayText = digit, + textSize = 21f + ), + setOf(Behavior.Press(KeyAction.SymAction(KeySym(digit.codePointAt(0))))), + arrayOf(Popup.Preview(digit)) + ) + } + + override val root = Keyboard(ctx, theme) + + } } \ No newline at end of file From 3b8e309303208fa1ced93e4450986094bc8cf456 Mon Sep 17 00:00:00 2001 From: Potato Hatsue <1793913507@qq.com> Date: Sat, 11 Feb 2023 23:58:11 -0500 Subject: [PATCH 005/624] Save CapabilityFlags in service and prioritize titleUi --- .../fcitx5/android/core/CapabilityFlag.kt | 6 ++- .../android/input/FcitxInputMethodService.kt | 13 +++++- .../fcitx/fcitx5/android/input/InputView.kt | 16 ++++++- .../android/input/bar/KawaiiBarComponent.kt | 46 +++++++------------ .../input/bar/KawaiiBarStateMachine.kt | 10 ++-- .../fcitx5/android/input/bar/KawaiiBarUi.kt | 28 ++--------- .../input/broadcast/InputBroadcastReceiver.kt | 3 +- .../input/broadcast/InputBroadcaster.kt | 6 ++- .../input/editorinfo/EditorInfoWindow.kt | 8 ++-- .../android/input/keyboard/BaseKeyboard.kt | 19 ++------ .../android/input/keyboard/KeyboardWindow.kt | 17 ++++--- .../android/input/keyboard/TextKeyboard.kt | 3 +- .../org/fcitx/fcitx5/android/utils/Utils.kt | 6 ++- 13 files changed, 86 insertions(+), 95 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/CapabilityFlag.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/CapabilityFlag.kt index 5397663eb..8f26be8c7 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/core/CapabilityFlag.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/core/CapabilityFlag.kt @@ -73,7 +73,7 @@ value class CapabilityFlags constructor(val flags: ULong) { fun mergeFlags(arr: Array): ULong = arr.fold(CapabilityFlag.NoFlag.flag) { acc, it -> acc or it.flag } - private val DefaultFlags = CapabilityFlags( + val DefaultFlags = CapabilityFlags( CapabilityFlag.Preedit.flag or CapabilityFlag.ClientUnfocusCommit.flag or CapabilityFlag.ClientSideInputPanel.flag @@ -164,5 +164,9 @@ value class CapabilityFlags constructor(val flags: ULong) { constructor(vararg flags: CapabilityFlag) : this(mergeFlags(flags)) + fun has(flag: ULong) = flags.hasFlag(flag) + + fun has(flag: CapabilityFlag) = flags.hasFlag(flag.flag) + fun toLong() = flags.toLong() } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt index afc5f2f5c..e3d3cb105 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt @@ -45,6 +45,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() { private var inputView: InputView? = null var editorInfo: EditorInfo? = null + var capabilityFlags = CapabilityFlags.DefaultFlags val selection = CursorTracker() val composing = CursorRange() @@ -92,6 +93,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() { is FcitxEvent.CommitStringEvent -> { commitText(event.data) } + is FcitxEvent.KeyEvent -> event.data.let event@{ if (it.states.virtual) { // KeyEvent from virtual keyboard @@ -127,9 +129,11 @@ class FcitxInputMethodService : LifecycleInputMethodService() { } } } + is FcitxEvent.PreeditEvent -> { updateComposingText(event.data.clientPreedit) } + else -> { } } @@ -378,21 +382,26 @@ class FcitxInputMethodService : LifecycleInputMethodService() { } override fun onStartInput(attribute: EditorInfo, restarting: Boolean) { + val flags = CapabilityFlags.fromEditorInfo(attribute) // update selection as soon as possible selection.resetTo(attribute.initialSelStart, attribute.initialSelEnd) composing.clear() composingText = FormattedText() editorInfo = attribute + capabilityFlags = flags Timber.d("onStartInput: initialSel=${selection.current}, restarting=$restarting") if (restarting) return lifecycleScope.launchOnFcitxReady(fcitx) { it.setCapFlags(CapabilityFlags.fromEditorInfo(attribute)) } + inputView?.onEditorInfoUpdate(attribute, flags) } override fun onStartInputView(info: EditorInfo, restarting: Boolean) { Timber.d("onStartInputView: restarting=$restarting") + val flags = CapabilityFlags.fromEditorInfo(info) editorInfo = info + capabilityFlags = flags lifecycleScope.launchOnFcitxReady(fcitx) { if (restarting) { // when input restarts in the same editor, focus out to clear previous state @@ -402,10 +411,11 @@ class FcitxInputMethodService : LifecycleInputMethodService() { } // EditorInfo can be different in onStartInput and onStartInputView, // especially in browsers - it.setCapFlags(CapabilityFlags.fromEditorInfo(info)) + it.setCapFlags(flags) it.focus(true) } inputView?.onShow() + inputView?.onEditorInfoUpdate(info, flags) } override fun onUpdateSelection( @@ -544,6 +554,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() { override fun onFinishInput() { Timber.d("onFinishInput") editorInfo = null + capabilityFlags = CapabilityFlags.DefaultFlags } override fun onUnbindInput() { diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt index c6999f1d6..034450f5b 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt @@ -7,6 +7,7 @@ import android.graphics.Color import android.os.Build import android.view.View import android.view.WindowManager +import android.view.inputmethod.EditorInfo import android.widget.ImageView import android.widget.Space import androidx.constraintlayout.widget.ConstraintLayout @@ -15,6 +16,7 @@ import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.fcitx.fcitx5.android.R +import org.fcitx.fcitx5.android.core.CapabilityFlags import org.fcitx.fcitx5.android.core.FcitxEvent import org.fcitx.fcitx5.android.daemon.FcitxConnection import org.fcitx.fcitx5.android.data.prefs.AppPrefs @@ -29,8 +31,8 @@ import org.fcitx.fcitx5.android.input.candidates.HorizontalCandidateComponent import org.fcitx.fcitx5.android.input.keyboard.CommonKeyActionListener import org.fcitx.fcitx5.android.input.keyboard.KeyboardWindow import org.fcitx.fcitx5.android.input.picker.emojiPicker -import org.fcitx.fcitx5.android.input.picker.symbolPicker import org.fcitx.fcitx5.android.input.picker.emoticonPicker +import org.fcitx.fcitx5.android.input.picker.symbolPicker import org.fcitx.fcitx5.android.input.popup.PopupComponent import org.fcitx.fcitx5.android.input.preedit.PreeditComponent import org.fcitx.fcitx5.android.input.punctuation.PunctuationComponent @@ -176,6 +178,7 @@ class InputView( it.isNavigationBarContrastEnforced = true } } + NavbarBackground.ColorOnly -> { shouldUpdateNavbarForeground = true shouldUpdateNavbarBackground = true @@ -184,6 +187,7 @@ class InputView( it.isNavigationBarContrastEnforced = false } } + NavbarBackground.Full -> { shouldUpdateNavbarForeground = true // allow draw behind navigation bar @@ -283,7 +287,6 @@ class InputView( // We cannot use the key for keyboard window, // as this is the only place where the window manager gets keyboard window instance windowManager.attachWindow(keyboardWindow) - broadcaster.onEditorInfoUpdate(service.editorInfo) } fun onHide() { @@ -295,18 +298,23 @@ class InputView( is FcitxEvent.CandidateListEvent -> { broadcaster.onCandidateUpdate(it.data) } + is FcitxEvent.PreeditEvent -> { broadcaster.onPreeditUpdate(it.data) } + is FcitxEvent.InputPanelAuxEvent -> { broadcaster.onInputPanelAuxUpdate(it.data) } + is FcitxEvent.IMChangeEvent -> { broadcaster.onImeUpdate(it.data) } + is FcitxEvent.StatusAreaEvent -> { broadcaster.onStatusAreaUpdate(it.data) } + else -> { } } @@ -316,6 +324,10 @@ class InputView( broadcaster.onSelectionUpdate(start, end) } + fun onEditorInfoUpdate(info: EditorInfo, capFlags: CapabilityFlags) { + broadcaster.onEditorInfoUpdate(info, capFlags) + } + private var showingDialog: Dialog? = null fun showDialog(dialog: Dialog) { diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt index 3b2369e1d..db50caaf8 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt @@ -2,7 +2,6 @@ package org.fcitx.fcitx5.android.input.bar import android.graphics.Color import android.os.Build -import android.text.InputType import android.view.KeyEvent import android.view.View import android.view.inputmethod.EditorInfo @@ -13,28 +12,16 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.fcitx.fcitx5.android.R +import org.fcitx.fcitx5.android.core.CapabilityFlag +import org.fcitx.fcitx5.android.core.CapabilityFlags import org.fcitx.fcitx5.android.core.FcitxEvent import org.fcitx.fcitx5.android.data.clipboard.ClipboardManager import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.data.prefs.ManagedPreference import org.fcitx.fcitx5.android.data.theme.ThemeManager -import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.State.ClickToAttachWindow -import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.State.ClickToDetachWindow -import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.State.Hidden -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.ClipboardUpdatedEmpty -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.ClipboardUpdatedNonEmpty -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.KawaiiBarShown -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.MenuButtonClicked -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.Pasted -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.Timeout -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.CandidateUpdateNonEmpty -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.CapFlagsUpdatedNoPassword -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.CapFlagsUpdatedPassword -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.ExtendedWindowAttached -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.PreeditUpdatedEmpty -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.PreeditUpdatedNonEmpty -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.WindowDetachedWithCandidatesEmpty -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.WindowDetachedWithCandidatesNonEmpty +import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.State.* +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.* +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.* import org.fcitx.fcitx5.android.input.broadcast.InputBroadcastReceiver import org.fcitx.fcitx5.android.input.candidates.HorizontalCandidateComponent import org.fcitx.fcitx5.android.input.candidates.expanded.ExpandedCandidateStyle @@ -339,22 +326,21 @@ class KawaiiBarComponent : UniqueViewComponent( override fun onWindowDetached(window: InputWindow) { barStateMachine.push( if (horizontalCandidate.adapter.candidates.isEmpty()) - WindowDetachedWithCandidatesEmpty + if (service.capabilityFlags.has(CapabilityFlag.Password)) + WindowDetachedWithCapFlagsPassword + else + WindowDetachedWithCapFlagsNoPassword else WindowDetachedWithCandidatesNonEmpty ) } - override fun onEditorInfoUpdate(info: EditorInfo?) { - info?.inputType?.and(InputType.TYPE_MASK_VARIATION)?.run { - if (equals(InputType.TYPE_TEXT_VARIATION_PASSWORD) || - equals(InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) || - equals(InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) - ) - barStateMachine.push(CapFlagsUpdatedPassword) + override fun onEditorInfoUpdate(info: EditorInfo, capFlags: CapabilityFlags) { + barStateMachine.push( + if (capFlags.has(CapabilityFlag.Password)) + CapFlagsUpdatedPassword else - barStateMachine.push(CapFlagsUpdatedNoPassword) - - } + CapFlagsUpdatedNoPassword + ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarStateMachine.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarStateMachine.kt index e9d55f2e5..9016b506e 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarStateMachine.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarStateMachine.kt @@ -10,8 +10,9 @@ import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent. import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.ExtendedWindowAttached import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.PreeditUpdatedEmpty import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.PreeditUpdatedNonEmpty -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.WindowDetachedWithCandidatesEmpty import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.WindowDetachedWithCandidatesNonEmpty +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.WindowDetachedWithCapFlagsNoPassword +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.WindowDetachedWithCapFlagsPassword import org.fcitx.fcitx5.android.utils.eventStateMachine object KawaiiBarStateMachine { @@ -24,7 +25,8 @@ object KawaiiBarStateMachine { PreeditUpdatedNonEmpty, CandidateUpdateNonEmpty, ExtendedWindowAttached, - WindowDetachedWithCandidatesEmpty, + WindowDetachedWithCapFlagsPassword, + WindowDetachedWithCapFlagsNoPassword, WindowDetachedWithCandidatesNonEmpty, CapFlagsUpdatedPassword, CapFlagsUpdatedNoPassword, @@ -34,12 +36,14 @@ object KawaiiBarStateMachine { from(Idle) transitTo Title on ExtendedWindowAttached from(Idle) transitTo Candidate on PreeditUpdatedNonEmpty from(Idle) transitTo Candidate on CandidateUpdateNonEmpty - from(Title) transitTo Idle on WindowDetachedWithCandidatesEmpty + from(Title) transitTo Idle on WindowDetachedWithCapFlagsNoPassword + from(Title) transitTo NumberRow on WindowDetachedWithCapFlagsPassword from(Title) transitTo Candidate on WindowDetachedWithCandidatesNonEmpty from(Candidate) transitTo Idle on PreeditUpdatedEmpty from(Candidate) transitTo Title on ExtendedWindowAttached from(Idle) transitTo NumberRow on CapFlagsUpdatedPassword from(NumberRow) transitTo Idle on CapFlagsUpdatedNoPassword + from(NumberRow) transitTo Title on ExtendedWindowAttached onNewState(block) } } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarUi.kt index 4cf8c337b..389272c96 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarUi.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarUi.kt @@ -19,37 +19,15 @@ import org.fcitx.fcitx5.android.R import org.fcitx.fcitx5.android.core.KeySym import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.data.theme.Theme -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.State.Clipboard -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.State.ClipboardTimedOut -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.State.Empty -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.State.Toolbar -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.State.ToolbarWithClip +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.State.* import org.fcitx.fcitx5.android.input.keyboard.BaseKeyboard import org.fcitx.fcitx5.android.input.keyboard.CustomGestureView import org.fcitx.fcitx5.android.input.keyboard.KeyAction import org.fcitx.fcitx5.android.input.keyboard.KeyDef import org.fcitx.fcitx5.android.utils.rippleDrawable import splitties.dimensions.dp -import splitties.views.dsl.constraintlayout.after -import splitties.views.dsl.constraintlayout.before -import splitties.views.dsl.constraintlayout.bottomOfParent -import splitties.views.dsl.constraintlayout.centerHorizontally -import splitties.views.dsl.constraintlayout.centerVertically -import splitties.views.dsl.constraintlayout.constraintLayout -import splitties.views.dsl.constraintlayout.endOfParent -import splitties.views.dsl.constraintlayout.lParams -import splitties.views.dsl.constraintlayout.matchConstraints -import splitties.views.dsl.constraintlayout.startOfParent -import splitties.views.dsl.constraintlayout.topOfParent -import splitties.views.dsl.core.Ui -import splitties.views.dsl.core.add -import splitties.views.dsl.core.horizontalLayout -import splitties.views.dsl.core.imageView -import splitties.views.dsl.core.lParams -import splitties.views.dsl.core.matchParent -import splitties.views.dsl.core.textView -import splitties.views.dsl.core.verticalMargin -import splitties.views.dsl.core.wrapContent +import splitties.views.dsl.constraintlayout.* +import splitties.views.dsl.core.* import splitties.views.gravityCenter import splitties.views.gravityVerticalCenter import splitties.views.imageResource diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcastReceiver.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcastReceiver.kt index 83f2056d7..543447b4b 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcastReceiver.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcastReceiver.kt @@ -2,6 +2,7 @@ package org.fcitx.fcitx5.android.input.broadcast import android.view.inputmethod.EditorInfo import org.fcitx.fcitx5.android.core.Action +import org.fcitx.fcitx5.android.core.CapabilityFlags import org.fcitx.fcitx5.android.core.FcitxEvent.InputPanelAuxEvent import org.fcitx.fcitx5.android.core.FcitxEvent.PreeditEvent import org.fcitx.fcitx5.android.core.InputMethodEntry @@ -12,7 +13,7 @@ interface InputBroadcastReceiver { fun onScopeSetupFinished(scope: DynamicScope) {} - fun onEditorInfoUpdate(info: EditorInfo?) {} + fun onEditorInfoUpdate(info: EditorInfo, capFlags: CapabilityFlags) {} fun onPreeditUpdate(data: PreeditEvent.Data) {} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcaster.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcaster.kt index 9fc409811..98a802465 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcaster.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcaster.kt @@ -2,6 +2,7 @@ package org.fcitx.fcitx5.android.input.broadcast import android.view.inputmethod.EditorInfo import org.fcitx.fcitx5.android.core.Action +import org.fcitx.fcitx5.android.core.CapabilityFlags import org.fcitx.fcitx5.android.core.FcitxEvent.InputPanelAuxEvent import org.fcitx.fcitx5.android.core.FcitxEvent.PreeditEvent import org.fcitx.fcitx5.android.core.InputMethodEntry @@ -23,6 +24,7 @@ class InputBroadcaster : UniqueComponent(), Dependent, InputBr receivers.add(scopeEvent.dependency as InputBroadcastReceiver) } } + is ScopeEvent.DependencyLeftEvent -> { if (scopeEvent.dependency is InputBroadcastReceiver && scopeEvent.dependency !is InputBroadcaster) { receivers.remove(scopeEvent.dependency as InputBroadcastReceiver) @@ -39,8 +41,8 @@ class InputBroadcaster : UniqueComponent(), Dependent, InputBr receivers.forEach { it.onInputPanelAuxUpdate(data) } } - override fun onEditorInfoUpdate(info: EditorInfo?) { - receivers.forEach { it.onEditorInfoUpdate(info) } + override fun onEditorInfoUpdate(info: EditorInfo, capFlags: CapabilityFlags) { + receivers.forEach { it.onEditorInfoUpdate(info, capFlags) } } override fun onImeUpdate(ime: InputMethodEntry) { diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/editorinfo/EditorInfoWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/editorinfo/EditorInfoWindow.kt index 45ca86417..e9dd514db 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/editorinfo/EditorInfoWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/editorinfo/EditorInfoWindow.kt @@ -2,6 +2,7 @@ package org.fcitx.fcitx5.android.input.editorinfo import android.view.inputmethod.EditorInfo import org.fcitx.fcitx5.android.R +import org.fcitx.fcitx5.android.core.CapabilityFlags import org.fcitx.fcitx5.android.input.FcitxInputMethodService import org.fcitx.fcitx5.android.input.broadcast.InputBroadcastReceiver import org.fcitx.fcitx5.android.input.dependency.inputMethodService @@ -25,11 +26,12 @@ class EditorInfoWindow : InputWindow.ExtendedInputWindow(), override fun onCreateView() = ui.root override fun onAttached() { - onEditorInfoUpdate(service.editorInfo) + service.editorInfo?.let { + ui.setValues(EditorInfoParser.parse(it)) + } } - override fun onEditorInfoUpdate(info: EditorInfo?) { - if (info == null) return + override fun onEditorInfoUpdate(info: EditorInfo, capFlags: CapabilityFlags) { ui.setValues(EditorInfoParser.parse(info)) } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/BaseKeyboard.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/BaseKeyboard.kt index 6ba97b757..9942f8a89 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/BaseKeyboard.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/BaseKeyboard.kt @@ -10,11 +10,7 @@ import androidx.annotation.DrawableRes import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.children import org.fcitx.fcitx5.android.R -import org.fcitx.fcitx5.android.core.FcitxEvent -import org.fcitx.fcitx5.android.core.FcitxKeyMapping -import org.fcitx.fcitx5.android.core.InputMethodEntry -import org.fcitx.fcitx5.android.core.KeyStates -import org.fcitx.fcitx5.android.core.KeySym +import org.fcitx.fcitx5.android.core.* import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.data.theme.Theme import org.fcitx.fcitx5.android.input.keyboard.CustomGestureView.GestureType @@ -23,16 +19,7 @@ import org.fcitx.fcitx5.android.input.popup.PopupAction import org.fcitx.fcitx5.android.input.popup.PopupActionListener import splitties.bitflags.hasFlag import splitties.dimensions.dp -import splitties.views.dsl.constraintlayout.above -import splitties.views.dsl.constraintlayout.after -import splitties.views.dsl.constraintlayout.before -import splitties.views.dsl.constraintlayout.below -import splitties.views.dsl.constraintlayout.bottomOfParent -import splitties.views.dsl.constraintlayout.constraintLayout -import splitties.views.dsl.constraintlayout.endOfParent -import splitties.views.dsl.constraintlayout.lParams -import splitties.views.dsl.constraintlayout.startOfParent -import splitties.views.dsl.constraintlayout.topOfParent +import splitties.views.dsl.constraintlayout.* import splitties.views.dsl.core.add import splitties.views.imageResource import timber.log.Timber @@ -488,7 +475,7 @@ abstract class BaseKeyboard( // do nothing by default } - open fun onEditorInfoChange(info: EditorInfo?) { + open fun onEditorInfoChange(info: EditorInfo, capFlags: CapabilityFlags) { // do nothing by default } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt index 4d87e8db3..4ca9a0f96 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt @@ -8,6 +8,7 @@ import android.widget.FrameLayout import androidx.core.content.ContextCompat import androidx.transition.Slide import org.fcitx.fcitx5.android.R +import org.fcitx.fcitx5.android.core.CapabilityFlags import org.fcitx.fcitx5.android.core.FcitxEvent import org.fcitx.fcitx5.android.core.InputMethodEntry import org.fcitx.fcitx5.android.data.prefs.AppPrefs @@ -127,16 +128,14 @@ class KeyboardWindow : InputWindow.SimpleInputWindow(), Essentia } } - override fun onEditorInfoUpdate(info: EditorInfo?) { - val targetLayout = service.editorInfo?.let { - when (it.inputType and InputType.TYPE_MASK_CLASS) { - InputType.TYPE_CLASS_NUMBER -> NumberKeyboard.Name - InputType.TYPE_CLASS_PHONE -> NumberKeyboard.Name - else -> TextKeyboard.Name - } + override fun onEditorInfoUpdate(info: EditorInfo, capFlags: CapabilityFlags) { + val targetLayout = when (info.inputType and InputType.TYPE_MASK_CLASS) { + InputType.TYPE_CLASS_NUMBER -> NumberKeyboard.Name + InputType.TYPE_CLASS_PHONE -> NumberKeyboard.Name + else -> TextKeyboard.Name } - switchLayout(targetLayout ?: TextKeyboard.Name, remember = false) - currentKeyboard?.onEditorInfoChange(info) + switchLayout(targetLayout, remember = false) + currentKeyboard?.onEditorInfoChange(info, capFlags) } override fun onImeUpdate(ime: InputMethodEntry) { diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt index fc09bca5b..58d970fc7 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt @@ -5,6 +5,7 @@ import android.content.Context import android.view.inputmethod.EditorInfo import androidx.core.view.allViews import org.fcitx.fcitx5.android.R +import org.fcitx.fcitx5.android.core.CapabilityFlags import org.fcitx.fcitx5.android.core.FcitxEvent import org.fcitx.fcitx5.android.core.InputMethodEntry import org.fcitx.fcitx5.android.data.prefs.AppPrefs @@ -135,7 +136,7 @@ class TextKeyboard( `return`.img.imageResource = drawableForReturn(info) } - override fun onEditorInfoChange(info: EditorInfo?) { + override fun onEditorInfoChange(info: EditorInfo, capFlags: CapabilityFlags) { `return`.img.imageResource = drawableForReturn(info) } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/utils/Utils.kt b/app/src/main/java/org/fcitx/fcitx5/android/utils/Utils.kt index 90a944a23..893dc79a8 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/utils/Utils.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/utils/Utils.kt @@ -269,4 +269,8 @@ inline fun withTempDir(block: (File) -> T): T { } finally { dir.delete() } -} \ No newline at end of file +} + +@Suppress("NOTHING_TO_INLINE") +inline infix fun T1?.notNullTo(y: T2?): Pair? = + this?.let { a -> y?.let { b -> a to b } } \ No newline at end of file From a53028bc67bbcc34d4139e32b8d5b6beff862352 Mon Sep 17 00:00:00 2001 From: Rocka Date: Sun, 12 Feb 2023 17:17:50 +0800 Subject: [PATCH 006/624] Disable key border and margin in password number row --- .../fcitx5/android/input/bar/KawaiiBarUi.kt | 4 +++- .../fcitx5/android/input/keyboard/KeyDef.kt | 10 +++++++--- .../fcitx5/android/input/keyboard/KeyView.kt | 19 ++++++++++++++----- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarUi.kt index 389272c96..d9c7f8ec2 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarUi.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarUi.kt @@ -339,7 +339,9 @@ sealed class KawaiiBarUi(override val ctx: Context, protected val theme: Theme) class NumKey(digit: String) : KeyDef( Appearance.Text( displayText = digit, - textSize = 21f + textSize = 21f, + border = Appearance.Border.Off, + margin = false ), setOf(Behavior.Press(KeyAction.SymAction(KeySym(digit.codePointAt(0))))), arrayOf(Popup.Preview(digit)) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDef.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDef.kt index c48819b04..f2667e134 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDef.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDef.kt @@ -12,6 +12,7 @@ open class KeyDef( val percentWidth: Float, val variant: Variant, val border: Border, + val margin: Boolean, val viewId: Int, ) { enum class Variant { @@ -33,8 +34,9 @@ open class KeyDef( percentWidth: Float = 0.1f, variant: Variant = Variant.Normal, border: Border = Border.Default, + margin: Boolean = true, viewId: Int = -1 - ) : Appearance(percentWidth, variant, border, viewId) + ) : Appearance(percentWidth, variant, border, margin, viewId) class AltText( displayText: String, @@ -48,8 +50,9 @@ open class KeyDef( percentWidth: Float = 0.1f, variant: Variant = Variant.Normal, border: Border = Border.Default, + margin: Boolean = true, viewId: Int = -1 - ) : Text(displayText, textSize, textStyle, percentWidth, variant, border, viewId) + ) : Text(displayText, textSize, textStyle, percentWidth, variant, border, margin, viewId) class Image( @DrawableRes @@ -57,8 +60,9 @@ open class KeyDef( percentWidth: Float = 0.1f, variant: Variant = Variant.Normal, border: Border = Border.Default, + margin: Boolean = true, viewId: Int = -1 - ) : Appearance(percentWidth, variant, border, viewId) + ) : Appearance(percentWidth, variant, border, margin, viewId) } sealed class Behavior { diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyView.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyView.kt index 3ccc8fe6f..1e3db9912 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyView.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyView.kt @@ -32,11 +32,20 @@ import kotlin.math.min abstract class KeyView(ctx: Context, val theme: Theme, val def: KeyDef.Appearance) : CustomGestureView(ctx) { - val bordered = ThemeManager.prefs.keyBorder.getValue() - val rippled = ThemeManager.prefs.keyRippleEffect.getValue() - val radius = dp(ThemeManager.prefs.keyRadius.getValue().toFloat()) - val hMargin = dp(ThemeManager.prefs.keyHorizontalMargin.getValue()) - val vMargin = dp(ThemeManager.prefs.keyVerticalMargin.getValue()) + val bordered: Boolean + val rippled: Boolean + val radius: Float + val hMargin: Int + val vMargin: Int + + init { + val prefs = ThemeManager.prefs + bordered = prefs.keyBorder.getValue() + rippled = prefs.keyRippleEffect.getValue() + radius = dp(prefs.keyRadius.getValue().toFloat()) + hMargin = if (def.margin) dp(prefs.keyHorizontalMargin.getValue()) else 0 + vMargin = if (def.margin) dp(prefs.keyVerticalMargin.getValue()) else 0 + } private val cachedLocation = intArrayOf(0, 0) private val cachedBounds = Rect() From b1ad6bd8759ea5f48d041cd0169c2d8d87c37127 Mon Sep 17 00:00:00 2001 From: Rocka Date: Sun, 12 Feb 2023 21:11:22 +0800 Subject: [PATCH 007/624] Reset composing range when user moves cursor out --- .../fcitx/fcitx5/android/input/FcitxInputMethodService.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt index e3d3cb105..0dd7788f7 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt @@ -382,11 +382,13 @@ class FcitxInputMethodService : LifecycleInputMethodService() { } override fun onStartInput(attribute: EditorInfo, restarting: Boolean) { - val flags = CapabilityFlags.fromEditorInfo(attribute) // update selection as soon as possible + // FIXME: onSelectionUpdate may happen before onStartInput when restarting=true, + // resulting outdated initialSel{Start,End}. selection.resetTo(attribute.initialSelStart, attribute.initialSelEnd) composing.clear() composingText = FormattedText() + val flags = CapabilityFlags.fromEditorInfo(attribute) editorInfo = attribute capabilityFlags = flags Timber.d("onStartInput: initialSel=${selection.current}, restarting=$restarting") @@ -474,6 +476,8 @@ class FcitxInputMethodService : LifecycleInputMethodService() { } } else { Timber.d("handleCursorUpdate: focus out/in") + composing.clear() + composingText = FormattedText() // cursor outside composing range, finish composing as-is inputConnection?.finishComposingText() // `fcitx.reset()` here would commit preedit after new cursor position From 929ed19450813fb48a0d1be563e6914c80e144dc Mon Sep 17 00:00:00 2001 From: Rocka Date: Sun, 12 Feb 2023 21:36:01 +0800 Subject: [PATCH 008/624] Ignore initial selection when restarting input --- .../fcitx/fcitx5/android/input/FcitxInputMethodService.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt index 0dd7788f7..ba9220357 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt @@ -383,9 +383,10 @@ class FcitxInputMethodService : LifecycleInputMethodService() { override fun onStartInput(attribute: EditorInfo, restarting: Boolean) { // update selection as soon as possible - // FIXME: onSelectionUpdate may happen before onStartInput when restarting=true, - // resulting outdated initialSel{Start,End}. - selection.resetTo(attribute.initialSelStart, attribute.initialSelEnd) + if (!restarting) { + // initialSel{Start,End} is usually outdated when restarting input + selection.resetTo(attribute.initialSelStart, attribute.initialSelEnd) + } composing.clear() composingText = FormattedText() val flags = CapabilityFlags.fromEditorInfo(attribute) From 4a106b633d838369d34549f5dabce38320ce9acb Mon Sep 17 00:00:00 2001 From: rocka Date: Mon, 13 Feb 2023 23:50:33 +0800 Subject: [PATCH 009/624] Merge InputView#onShow and onEditorInfoUpdate (#195) Since EditorInfo can only be ontained in onStartInput{,View}, there is no reason to split onShow and onEditorInfoUpdate callbacks. --- .../android/input/FcitxInputMethodService.kt | 8 +-- .../fcitx/fcitx5/android/input/InputView.kt | 68 ++++++++++--------- .../android/input/bar/KawaiiBarComponent.kt | 30 ++++---- .../input/broadcast/InputBroadcastReceiver.kt | 2 +- .../input/broadcast/InputBroadcaster.kt | 4 +- .../input/editorinfo/EditorInfoWindow.kt | 10 +-- .../android/input/keyboard/KeyboardWindow.kt | 2 +- .../android/input/wm/InputWindowManager.kt | 8 ++- 8 files changed, 63 insertions(+), 69 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt index ba9220357..57e02b180 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt @@ -397,7 +397,6 @@ class FcitxInputMethodService : LifecycleInputMethodService() { lifecycleScope.launchOnFcitxReady(fcitx) { it.setCapFlags(CapabilityFlags.fromEditorInfo(attribute)) } - inputView?.onEditorInfoUpdate(attribute, flags) } override fun onStartInputView(info: EditorInfo, restarting: Boolean) { @@ -417,8 +416,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() { it.setCapFlags(flags) it.focus(true) } - inputView?.onShow() - inputView?.onEditorInfoUpdate(info, flags) + inputView?.startInput(info, flags, restarting) } override fun onUpdateSelection( @@ -434,7 +432,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() { cursorUpdateIndex += 1 Timber.d("onUpdateSelection: old=[$oldSelStart,$oldSelEnd] new=[$newSelStart,$newSelEnd] cand=[$candidatesStart,$candidatesEnd]") handleCursorUpdate(newSelStart, newSelEnd, cursorUpdateIndex) - inputView?.onSelectionUpdate(newSelStart, newSelEnd) + inputView?.updateSelection(newSelStart, newSelEnd) } override fun onUpdateCursorAnchorInfo(info: CursorAnchorInfo) { @@ -553,7 +551,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() { lifecycleScope.launchOnFcitxReady(fcitx) { it.focus(false) } - inputView?.onHide() + inputView?.finishInput() } override fun onFinishInput() { diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt index 034450f5b..83cb0ef09 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt @@ -164,6 +164,8 @@ class InputView( it.registerOnChangeListener(onKeyboardSizeChangeListener) } + // make sure KeyboardWindow's view has been created before it receives any broadcast + windowManager.addEssentialWindow(keyboardWindow, createView = true) windowManager.addEssentialWindow(symbolPicker) windowManager.addEssentialWindow(emojiPicker) windowManager.addEssentialWindow(emoticonPicker) @@ -260,37 +262,28 @@ class InputView( windowManager.view.setPadding(sidePadding, 0, sidePadding, 0) } - override fun onDetachedFromWindow() { - keyboardSizePrefs.forEach { - it.unregisterOnChangeListener(onKeyboardSizeChangeListener) - } - ViewCompat.setOnApplyWindowInsetsListener(this, null) - super.onDetachedFromWindow() - } - - fun onShow() { - if (shouldUpdateNavbarForeground || shouldUpdateNavbarBackground) { - service.window.window!!.also { - if (shouldUpdateNavbarForeground) { - WindowCompat.getInsetsController(it, it.decorView) - .isAppearanceLightNavigationBars = !theme.isDark - } - if (shouldUpdateNavbarBackground) { - it.navigationBarColor = when (theme) { - is Theme.Builtin -> if (keyBorder) theme.backgroundColor else theme.keyboardColor - is Theme.Custom -> theme.backgroundColor + /** + * called when [InputView] is about to show, or restart + */ + fun startInput(info: EditorInfo, capFlags: CapabilityFlags, restarting: Boolean = false) { + if (!restarting) { + if (shouldUpdateNavbarForeground || shouldUpdateNavbarBackground) { + service.window.window!!.also { + if (shouldUpdateNavbarForeground) { + WindowCompat.getInsetsController(it, it.decorView) + .isAppearanceLightNavigationBars = !theme.isDark + } + if (shouldUpdateNavbarBackground) { + it.navigationBarColor = when (theme) { + is Theme.Builtin -> if (keyBorder) theme.backgroundColor else theme.keyboardColor + is Theme.Custom -> theme.backgroundColor + } } } } } - kawaiiBar.onShow() - // We cannot use the key for keyboard window, - // as this is the only place where the window manager gets keyboard window instance - windowManager.attachWindow(keyboardWindow) - } - - fun onHide() { - showingDialog?.dismiss() + broadcaster.onStartInput(info, capFlags) + windowManager.attachWindow(KeyboardWindow) } private fun handleFcitxEvent(it: FcitxEvent<*>) { @@ -320,14 +313,10 @@ class InputView( } } - fun onSelectionUpdate(start: Int, end: Int) { + fun updateSelection(start: Int, end: Int) { broadcaster.onSelectionUpdate(start, end) } - fun onEditorInfoUpdate(info: EditorInfo, capFlags: CapabilityFlags) { - broadcaster.onEditorInfoUpdate(info, capFlags) - } - private var showingDialog: Dialog? = null fun showDialog(dialog: Dialog) { @@ -350,6 +339,21 @@ class InputView( } } + /** + * called when [InputView] is being hidden + */ + fun finishInput() { + showingDialog?.dismiss() + } + + override fun onDetachedFromWindow() { + keyboardSizePrefs.forEach { + it.unregisterOnChangeListener(onKeyboardSizeChangeListener) + } + ViewCompat.setOnApplyWindowInsetsListener(this, null) + super.onDetachedFromWindow() + } + fun onDestroy() { showingDialog?.dismiss() eventHandlerJob.cancel() diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt index db50caaf8..e965753c4 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt @@ -240,15 +240,6 @@ class KawaiiBarComponent : UniqueViewComponent( candidateUi.expandButton.visibility = View.INVISIBLE } - fun onShow() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - idleUi.privateMode( - service.editorInfo?.imeOptions?.hasFlag(EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING) == true - ) - } - idleUiStateMachine.push(KawaiiBarShown) - } - private fun switchUiByState(state: KawaiiBarStateMachine.State) { val index = state.ordinal if (view.displayedChild == index) @@ -293,6 +284,19 @@ class KawaiiBarComponent : UniqueViewComponent( } } + override fun onStartInput(info: EditorInfo, capFlags: CapabilityFlags) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + idleUi.privateMode(info.imeOptions.hasFlag(EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING)) + } + idleUiStateMachine.push(KawaiiBarShown) + barStateMachine.push( + if (capFlags.has(CapabilityFlag.Password)) + CapFlagsUpdatedPassword + else + CapFlagsUpdatedNoPassword + ) + } + override fun onPreeditUpdate(data: FcitxEvent.PreeditEvent.Data) { barStateMachine.push( if (data.preedit.isEmpty() && data.clientPreedit.isEmpty()) @@ -335,12 +339,4 @@ class KawaiiBarComponent : UniqueViewComponent( ) } - override fun onEditorInfoUpdate(info: EditorInfo, capFlags: CapabilityFlags) { - barStateMachine.push( - if (capFlags.has(CapabilityFlag.Password)) - CapFlagsUpdatedPassword - else - CapFlagsUpdatedNoPassword - ) - } } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcastReceiver.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcastReceiver.kt index 543447b4b..907633525 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcastReceiver.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcastReceiver.kt @@ -13,7 +13,7 @@ interface InputBroadcastReceiver { fun onScopeSetupFinished(scope: DynamicScope) {} - fun onEditorInfoUpdate(info: EditorInfo, capFlags: CapabilityFlags) {} + fun onStartInput(info: EditorInfo, capFlags: CapabilityFlags) {} fun onPreeditUpdate(data: PreeditEvent.Data) {} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcaster.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcaster.kt index 98a802465..4e1793a9f 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcaster.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcaster.kt @@ -41,8 +41,8 @@ class InputBroadcaster : UniqueComponent(), Dependent, InputBr receivers.forEach { it.onInputPanelAuxUpdate(data) } } - override fun onEditorInfoUpdate(info: EditorInfo, capFlags: CapabilityFlags) { - receivers.forEach { it.onEditorInfoUpdate(info, capFlags) } + override fun onStartInput(info: EditorInfo, capFlags: CapabilityFlags) { + receivers.forEach { it.onStartInput(info, capFlags) } } override fun onImeUpdate(ime: InputMethodEntry) { diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/editorinfo/EditorInfoWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/editorinfo/EditorInfoWindow.kt index e9dd514db..3eaf1c253 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/editorinfo/EditorInfoWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/editorinfo/EditorInfoWindow.kt @@ -1,16 +1,12 @@ package org.fcitx.fcitx5.android.input.editorinfo -import android.view.inputmethod.EditorInfo import org.fcitx.fcitx5.android.R -import org.fcitx.fcitx5.android.core.CapabilityFlags import org.fcitx.fcitx5.android.input.FcitxInputMethodService -import org.fcitx.fcitx5.android.input.broadcast.InputBroadcastReceiver import org.fcitx.fcitx5.android.input.dependency.inputMethodService import org.fcitx.fcitx5.android.input.dependency.theme import org.fcitx.fcitx5.android.input.wm.InputWindow -class EditorInfoWindow : InputWindow.ExtendedInputWindow(), - InputBroadcastReceiver { +class EditorInfoWindow : InputWindow.ExtendedInputWindow() { private val service: FcitxInputMethodService by manager.inputMethodService() private val theme by manager.theme() @@ -31,9 +27,5 @@ class EditorInfoWindow : InputWindow.ExtendedInputWindow(), } } - override fun onEditorInfoUpdate(info: EditorInfo, capFlags: CapabilityFlags) { - ui.setValues(EditorInfoParser.parse(info)) - } - override fun onDetached() {} } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt index 4ca9a0f96..35a7ce048 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt @@ -128,7 +128,7 @@ class KeyboardWindow : InputWindow.SimpleInputWindow(), Essentia } } - override fun onEditorInfoUpdate(info: EditorInfo, capFlags: CapabilityFlags) { + override fun onStartInput(info: EditorInfo, capFlags: CapabilityFlags) { val targetLayout = when (info.inputType and InputType.TYPE_MASK_CLASS) { InputType.TYPE_CLASS_NUMBER -> NumberKeyboard.Name InputType.TYPE_CLASS_PHONE -> NumberKeyboard.Name diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/wm/InputWindowManager.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/wm/InputWindowManager.kt index 737110d41..004784097 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/wm/InputWindowManager.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/wm/InputWindowManager.kt @@ -58,7 +58,10 @@ class InputWindowManager : UniqueViewComponent( * This function does not create any view nor set up the scope */ @Suppress("BOUNDS_NOT_ALLOWED_IF_BOUNDED_BY_TYPE_PARAMETER") - fun addEssentialWindow(window: R) where R : W, R : E { + fun addEssentialWindow( + window: R, + createView: Boolean = false + ) where R : W, R : E { ensureThread() if (window.key in essentialWindows) { if (essentialWindows[window.key] === window) @@ -66,7 +69,8 @@ class InputWindowManager : UniqueViewComponent( else throw IllegalStateException("${window.key} is already occupied") } - essentialWindows[window.key] = window to null + val view = if (createView) window.onCreateView() else null + essentialWindows[window.key] = window to view } fun getEssentialWindow(windowKey: EssentialWindow.Key) = From 27f0645bc884d296f51ef9f2761aadc3f875e17a Mon Sep 17 00:00:00 2001 From: Rocka Date: Tue, 14 Feb 2023 15:05:13 +0800 Subject: [PATCH 010/624] Make cached EditorInfo not-null with default value --- .../fcitx5/android/core/CapabilityFlag.kt | 6 +- .../android/input/FcitxInputMethodService.kt | 58 +++++++++---------- .../input/editorinfo/EditorInfoWindow.kt | 4 +- .../android/input/keyboard/BaseKeyboard.kt | 12 ++-- .../android/input/keyboard/NumberKeyboard.kt | 2 +- .../android/input/keyboard/TextKeyboard.kt | 4 +- 6 files changed, 41 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/CapabilityFlag.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/CapabilityFlag.kt index 8f26be8c7..0f2f82386 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/core/CapabilityFlag.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/core/CapabilityFlag.kt @@ -79,9 +79,9 @@ value class CapabilityFlags constructor(val flags: ULong) { CapabilityFlag.ClientSideInputPanel.flag ) - fun fromEditorInfo(info: EditorInfo?): CapabilityFlags { + fun fromEditorInfo(info: EditorInfo): CapabilityFlags { var flags = DefaultFlags.flags - info?.imeOptions?.let { + info.imeOptions.let { if (it.hasFlag(EditorInfo.IME_FLAG_FORCE_ASCII)) { flags += CapabilityFlag.Alpha } @@ -89,7 +89,7 @@ value class CapabilityFlags constructor(val flags: ULong) { flags += CapabilityFlag.Sensitive } } - info?.inputType?.let { + info.inputType.let { when (it and InputType.TYPE_MASK_CLASS) { InputType.TYPE_NULL -> { flags -= CapabilityFlag.Preedit diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt index 57e02b180..7003d8f38 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt @@ -44,12 +44,12 @@ class FcitxInputMethodService : LifecycleInputMethodService() { private var inputView: InputView? = null - var editorInfo: EditorInfo? = null + var editorInfo = EmptyEditorInfo var capabilityFlags = CapabilityFlags.DefaultFlags val selection = CursorTracker() val composing = CursorRange() - private var composingText = FormattedText() + private var composingText = EmptyFormattedText private var cursorUpdateIndex: Int = 0 @@ -146,36 +146,32 @@ class FcitxInputMethodService : LifecycleInputMethodService() { } else if (lastSelection.start > 0) { selection.predictOffset(-1) } - editorInfo?.apply { - // In practice nobody (apart form us) would set `privateImeOptions` to our - // `DeleteSurroundingFlag`, leading to a behavior of simulating backspace key pressing - // in almost every EditText. - if (privateImeOptions != DeleteSurroundingFlag || - inputType and InputType.TYPE_MASK_CLASS == InputType.TYPE_NULL - ) { + // In practice nobody (apart form us) would set `privateImeOptions` to our + // `DeleteSurroundingFlag`, leading to a behavior of simulating backspace key pressing + // in almost every EditText. + if (editorInfo.privateImeOptions != DeleteSurroundingFlag || + editorInfo.inputType and InputType.TYPE_MASK_CLASS == InputType.TYPE_NULL + ) { + sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL) + return + } + if (lastSelection.isEmpty()) { + if (lastSelection.start <= 0) { sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL) return } - inputConnection?.apply { - if (lastSelection.isEmpty()) { - if (lastSelection.start <= 0) { - sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL) - return - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - deleteSurroundingTextInCodePoints(1, 0) - } else { - deleteSurroundingText(1, 0) - } - } else { - commitText("", 0) - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + inputConnection?.deleteSurroundingTextInCodePoints(1, 0) + } else { + inputConnection?.deleteSurroundingText(1, 0) } - } ?: sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL) + } else { + inputConnection?.commitText("", 0) + } } private fun handleReturnKey() { - editorInfo?.apply { + editorInfo.run { if (inputType and InputType.TYPE_MASK_CLASS == InputType.TYPE_NULL) { sendDownUpKeyEvents(KeyEvent.KEYCODE_ENTER) return @@ -193,7 +189,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() { EditorInfo.IME_ACTION_NONE -> commitText("\n") else -> inputConnection?.performEditorAction(action) } - } ?: sendDownUpKeyEvents(KeyEvent.KEYCODE_ENTER) + } } fun commitText(text: String) { @@ -203,7 +199,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() { selection.predict(start + text.length) // clear composing range composing.clear() - composingText = FormattedText() + composingText = EmptyFormattedText inputConnection?.commitText(text, 1) } @@ -388,7 +384,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() { selection.resetTo(attribute.initialSelStart, attribute.initialSelEnd) } composing.clear() - composingText = FormattedText() + composingText = EmptyFormattedText val flags = CapabilityFlags.fromEditorInfo(attribute) editorInfo = attribute capabilityFlags = flags @@ -476,7 +472,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() { } else { Timber.d("handleCursorUpdate: focus out/in") composing.clear() - composingText = FormattedText() + composingText = EmptyFormattedText // cursor outside composing range, finish composing as-is inputConnection?.finishComposingText() // `fcitx.reset()` here would commit preedit after new cursor position @@ -556,7 +552,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() { override fun onFinishInput() { Timber.d("onFinishInput") - editorInfo = null + editorInfo = EmptyEditorInfo capabilityFlags = CapabilityFlags.DefaultFlags } @@ -587,6 +583,8 @@ class FcitxInputMethodService : LifecycleInputMethodService() { } companion object { + val EmptyEditorInfo = EditorInfo() + val EmptyFormattedText = FormattedText() const val DeleteSurroundingFlag = "org.fcitx.fcitx5.android.DELETE_SURROUNDING" } } \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/editorinfo/EditorInfoWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/editorinfo/EditorInfoWindow.kt index 3eaf1c253..67c73770d 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/editorinfo/EditorInfoWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/editorinfo/EditorInfoWindow.kt @@ -22,9 +22,7 @@ class EditorInfoWindow : InputWindow.ExtendedInputWindow() { override fun onCreateView() = ui.root override fun onAttached() { - service.editorInfo?.let { - ui.setValues(EditorInfoParser.parse(it)) - } + ui.setValues(EditorInfoParser.parse(service.editorInfo)) } override fun onDetached() {} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/BaseKeyboard.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/BaseKeyboard.kt index 9942f8a89..c38f21f8f 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/BaseKeyboard.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/BaseKeyboard.kt @@ -295,11 +295,11 @@ abstract class BaseKeyboard( } @DrawableRes - protected fun drawableForReturn(info: EditorInfo?): Int { - if (info?.imeOptions?.hasFlag(EditorInfo.IME_FLAG_NO_ENTER_ACTION) == true) { + protected fun drawableForReturn(info: EditorInfo): Int { + if (info.imeOptions.hasFlag(EditorInfo.IME_FLAG_NO_ENTER_ACTION)) { return R.drawable.ic_baseline_keyboard_return_24 } - return when (info?.imeOptions?.and(EditorInfo.IME_MASK_ACTION)) { + return when (info.imeOptions and EditorInfo.IME_MASK_ACTION) { EditorInfo.IME_ACTION_GO -> R.drawable.ic_baseline_arrow_forward_24 EditorInfo.IME_ACTION_SEARCH -> R.drawable.ic_baseline_search_24 EditorInfo.IME_ACTION_SEND -> R.drawable.ic_baseline_send_24 @@ -313,7 +313,7 @@ abstract class BaseKeyboard( // FIXME: need some new API to know exactly whether next enter would be captured by fcitx protected fun updateReturnButton( `return`: ImageKeyView, - info: EditorInfo?, + info: EditorInfo, preedit: FcitxEvent.PreeditEvent.Data // aux: FcitxEvent.InputPanelAuxEvent.Data ) { @@ -471,7 +471,7 @@ abstract class BaseKeyboard( return true } - open fun onAttach(info: EditorInfo? = null) { + open fun onAttach(info: EditorInfo) { // do nothing by default } @@ -479,7 +479,7 @@ abstract class BaseKeyboard( // do nothing by default } - open fun onPreeditChange(info: EditorInfo?, data: FcitxEvent.PreeditEvent.Data) { + open fun onPreeditChange(info: EditorInfo, data: FcitxEvent.PreeditEvent.Data) { // do nothing by default } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/NumberKeyboard.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/NumberKeyboard.kt index 216eed1b7..fc46cdf88 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/NumberKeyboard.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/NumberKeyboard.kt @@ -55,7 +55,7 @@ class NumberKeyboard( val space: TextKeyView by lazy { findViewById(R.id.button_mini_space) } val `return`: ImageKeyView by lazy { findViewById(R.id.button_return) } - override fun onAttach(info: EditorInfo?) { + override fun onAttach(info: EditorInfo) { `return`.img.imageResource = drawableForReturn(info) } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt index 58d970fc7..8792db58a 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt @@ -130,7 +130,7 @@ class TextKeyboard( if (capsState == CapsState.Once) switchCapsState() } - override fun onAttach(info: EditorInfo?) { + override fun onAttach(info: EditorInfo) { updateCapsButtonIcon() updateAlphabetKeys() `return`.img.imageResource = drawableForReturn(info) @@ -140,7 +140,7 @@ class TextKeyboard( `return`.img.imageResource = drawableForReturn(info) } - override fun onPreeditChange(info: EditorInfo?, data: FcitxEvent.PreeditEvent.Data) { + override fun onPreeditChange(info: EditorInfo, data: FcitxEvent.PreeditEvent.Data) { updateReturnButton(`return`, info, data) } From ae0f189c0454708b4d6c4b2e30ac5b057dff77e4 Mon Sep 17 00:00:00 2001 From: Rocka Date: Tue, 14 Feb 2023 15:51:27 +0800 Subject: [PATCH 011/624] Remove workaround for restarting input --- .../android/input/FcitxInputMethodService.kt | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt index 7003d8f38..a76d8edbd 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt @@ -379,27 +379,16 @@ class FcitxInputMethodService : LifecycleInputMethodService() { override fun onStartInput(attribute: EditorInfo, restarting: Boolean) { // update selection as soon as possible - if (!restarting) { - // initialSel{Start,End} is usually outdated when restarting input - selection.resetTo(attribute.initialSelStart, attribute.initialSelEnd) - } + // sometimes when restarting input, onUpdateSelection happens before onStartInput, and + // initialSel{Start,End} is outdated. but it's the client app's responsibility to send + // right cursor position, try to workaround this would simply introduce more bugs. + selection.resetTo(attribute.initialSelStart, attribute.initialSelEnd) composing.clear() composingText = EmptyFormattedText val flags = CapabilityFlags.fromEditorInfo(attribute) editorInfo = attribute capabilityFlags = flags Timber.d("onStartInput: initialSel=${selection.current}, restarting=$restarting") - if (restarting) return - lifecycleScope.launchOnFcitxReady(fcitx) { - it.setCapFlags(CapabilityFlags.fromEditorInfo(attribute)) - } - } - - override fun onStartInputView(info: EditorInfo, restarting: Boolean) { - Timber.d("onStartInputView: restarting=$restarting") - val flags = CapabilityFlags.fromEditorInfo(info) - editorInfo = info - capabilityFlags = flags lifecycleScope.launchOnFcitxReady(fcitx) { if (restarting) { // when input restarts in the same editor, focus out to clear previous state @@ -412,7 +401,16 @@ class FcitxInputMethodService : LifecycleInputMethodService() { it.setCapFlags(flags) it.focus(true) } - inputView?.startInput(info, flags, restarting) + } + + override fun onStartInputView(info: EditorInfo, restarting: Boolean) { + Timber.d("onStartInputView: restarting=$restarting") + lifecycleScope.launchOnFcitxReady(fcitx) { + it.focus(true) + } + // because onStartInputView will always be called after onStartInput, + // editorInfo and capFlags should be up-to-date + inputView?.startInput(editorInfo, capabilityFlags, restarting) } override fun onUpdateSelection( From 6b5140960a6ffa70e557e9f5b40d414dfbf353ec Mon Sep 17 00:00:00 2001 From: Rocka Date: Tue, 14 Feb 2023 19:12:07 +0800 Subject: [PATCH 012/624] Treat VISIBLE_PASSWORD as Sensitive, not Password --- .../main/java/org/fcitx/fcitx5/android/core/CapabilityFlag.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/CapabilityFlag.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/CapabilityFlag.kt index 0f2f82386..b68596713 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/core/CapabilityFlag.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/core/CapabilityFlag.kt @@ -127,11 +127,13 @@ value class CapabilityFlags constructor(val flags: ULong) { flags += CapabilityFlag.Email } if (equals(InputType.TYPE_TEXT_VARIATION_PASSWORD) || - equals(InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) || equals(InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) ) { flags += CapabilityFlag.Password } + if (equals(InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD)) { + flags += CapabilityFlag.Sensitive + } if (equals(InputType.TYPE_TEXT_VARIATION_URI)) { flags += CapabilityFlag.Url } From 79ddbc935d19282f29e2328db5912bfe004f6ca5 Mon Sep 17 00:00:00 2001 From: Potato Hatsue <1793913507@qq.com> Date: Tue, 14 Feb 2023 22:29:30 -0500 Subject: [PATCH 013/624] Keep bar in idle state when switched to number keyboard --- .../fcitx/fcitx5/android/input/InputView.kt | 4 --- .../android/input/bar/KawaiiBarComponent.kt | 26 +++++++++++++++---- .../input/bar/KawaiiBarStateMachine.kt | 8 +++++- .../android/input/keyboard/KeyboardWindow.kt | 18 +++++++++++++ .../android/input/wm/InputWindowManager.kt | 10 ++++--- .../org/fcitx/fcitx5/android/utils/Utils.kt | 7 ++--- 6 files changed, 57 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt index 83cb0ef09..7bb1e8797 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt @@ -99,10 +99,6 @@ class InputView( scope += windowManager scope += kawaiiBar scope += horizontalCandidate - scope += keyboardWindow - scope += symbolPicker - scope += emojiPicker - scope += emoticonPicker broadcaster.onScopeSetupFinished(scope) } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt index e965753c4..d1a8390e7 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt @@ -19,9 +19,24 @@ import org.fcitx.fcitx5.android.data.clipboard.ClipboardManager import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.data.prefs.ManagedPreference import org.fcitx.fcitx5.android.data.theme.ThemeManager -import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.State.* -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.* -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.* +import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.State.ClickToAttachWindow +import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.State.ClickToDetachWindow +import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.State.Hidden +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.ClipboardUpdatedEmpty +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.ClipboardUpdatedNonEmpty +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.KawaiiBarShown +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.MenuButtonClicked +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.Pasted +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.Timeout +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.CandidateUpdateNonEmpty +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.CapFlagsUpdatedNoPassword +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.CapFlagsUpdatedPassword +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.ExtendedWindowAttached +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.PreeditUpdatedEmpty +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.PreeditUpdatedNonEmpty +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.WindowDetachedWithCandidatesNonEmpty +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.WindowDetachedWithCapFlagsNoPassword +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.WindowDetachedWithCapFlagsPassword import org.fcitx.fcitx5.android.input.broadcast.InputBroadcastReceiver import org.fcitx.fcitx5.android.input.candidates.HorizontalCandidateComponent import org.fcitx.fcitx5.android.input.candidates.expanded.ExpandedCandidateStyle @@ -40,6 +55,7 @@ import org.fcitx.fcitx5.android.input.status.StatusAreaWindow import org.fcitx.fcitx5.android.input.wm.InputWindow import org.fcitx.fcitx5.android.input.wm.InputWindowManager import org.fcitx.fcitx5.android.utils.AppUtil +import org.fcitx.fcitx5.android.utils.isInPasswordMode import org.mechdancer.dependency.DynamicScope import org.mechdancer.dependency.manager.must import splitties.bitflags.hasFlag @@ -185,7 +201,7 @@ class KawaiiBarComponent : UniqueViewComponent( private val numberRowUi by lazy { KawaiiBarUi.NumberRowUi(context, theme) } - private val barStateMachine = KawaiiBarStateMachine.new { + val barStateMachine = KawaiiBarStateMachine.new { switchUiByState(it) } @@ -330,7 +346,7 @@ class KawaiiBarComponent : UniqueViewComponent( override fun onWindowDetached(window: InputWindow) { barStateMachine.push( if (horizontalCandidate.adapter.candidates.isEmpty()) - if (service.capabilityFlags.has(CapabilityFlag.Password)) + if (service.isInPasswordMode) WindowDetachedWithCapFlagsPassword else WindowDetachedWithCapFlagsNoPassword diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarStateMachine.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarStateMachine.kt index 9016b506e..21e0d4546 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarStateMachine.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarStateMachine.kt @@ -8,6 +8,8 @@ import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent. import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.CapFlagsUpdatedNoPassword import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.CapFlagsUpdatedPassword import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.ExtendedWindowAttached +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.KeyboardSwitchedOutNumberWithCapFlagsPassword +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.KeyboardSwitchedToNumber import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.PreeditUpdatedEmpty import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.PreeditUpdatedNonEmpty import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.WindowDetachedWithCandidatesNonEmpty @@ -30,20 +32,24 @@ object KawaiiBarStateMachine { WindowDetachedWithCandidatesNonEmpty, CapFlagsUpdatedPassword, CapFlagsUpdatedNoPassword, + KeyboardSwitchedToNumber, + KeyboardSwitchedOutNumberWithCapFlagsPassword } fun new(block: (State) -> Unit) = eventStateMachine(Idle) { from(Idle) transitTo Title on ExtendedWindowAttached from(Idle) transitTo Candidate on PreeditUpdatedNonEmpty from(Idle) transitTo Candidate on CandidateUpdateNonEmpty + from(Idle) transitTo NumberRow on CapFlagsUpdatedPassword + from(Idle) transitTo NumberRow on KeyboardSwitchedOutNumberWithCapFlagsPassword from(Title) transitTo Idle on WindowDetachedWithCapFlagsNoPassword from(Title) transitTo NumberRow on WindowDetachedWithCapFlagsPassword from(Title) transitTo Candidate on WindowDetachedWithCandidatesNonEmpty from(Candidate) transitTo Idle on PreeditUpdatedEmpty from(Candidate) transitTo Title on ExtendedWindowAttached - from(Idle) transitTo NumberRow on CapFlagsUpdatedPassword from(NumberRow) transitTo Idle on CapFlagsUpdatedNoPassword from(NumberRow) transitTo Title on ExtendedWindowAttached + from(NumberRow) transitTo Idle on KeyboardSwitchedToNumber onNewState(block) } } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt index 35a7ce048..77c213290 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt @@ -13,6 +13,9 @@ import org.fcitx.fcitx5.android.core.FcitxEvent import org.fcitx.fcitx5.android.core.InputMethodEntry import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.input.FcitxInputMethodService +import org.fcitx.fcitx5.android.input.bar.KawaiiBarComponent +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.KeyboardSwitchedOutNumberWithCapFlagsPassword +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.KeyboardSwitchedToNumber import org.fcitx.fcitx5.android.input.broadcast.InputBroadcastReceiver import org.fcitx.fcitx5.android.input.dependency.fcitx import org.fcitx.fcitx5.android.input.dependency.inputMethodService @@ -23,6 +26,7 @@ import org.fcitx.fcitx5.android.input.popup.PopupComponent import org.fcitx.fcitx5.android.input.wm.EssentialWindow import org.fcitx.fcitx5.android.input.wm.InputWindow import org.fcitx.fcitx5.android.input.wm.InputWindowManager +import org.fcitx.fcitx5.android.utils.isInPasswordMode import org.mechdancer.dependency.manager.must import splitties.views.dsl.core.add import splitties.views.dsl.core.frameLayout @@ -38,6 +42,7 @@ class KeyboardWindow : InputWindow.SimpleInputWindow(), Essentia private val commonKeyActionListener: CommonKeyActionListener by manager.must() private val windowManager: InputWindowManager by manager.must() private val popup: PopupComponent by manager.must() + private val bar: KawaiiBarComponent by manager.must() companion object : EssentialWindow.Key @@ -119,6 +124,8 @@ class KeyboardWindow : InputWindow.SimpleInputWindow(), Essentia } detachCurrentLayout() attachLayout(target) + if (windowManager.isAttached(this)) + notifyBarLayoutChanged() } else { if (remember) { lastSymbolType = PickerWindow.Key.Symbol.name @@ -155,6 +162,7 @@ class KeyboardWindow : InputWindow.SimpleInputWindow(), Essentia it.keyActionListener = keyActionListener it.popupActionListener = popupActionListener } + notifyBarLayoutChanged() } override fun onDetached() { @@ -164,4 +172,14 @@ class KeyboardWindow : InputWindow.SimpleInputWindow(), Essentia } popup.dismissAll() } + + // Call this when + // 1) the keyboard window was newly attached + // 2) currently keyboard window is attached and switchLayout was used + private fun notifyBarLayoutChanged() { + if (currentKeyboardName == NumberKeyboard.Name) + bar.barStateMachine.push(KeyboardSwitchedToNumber) + else if (service.isInPasswordMode) + bar.barStateMachine.push(KeyboardSwitchedOutNumberWithCapFlagsPassword) + } } \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/wm/InputWindowManager.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/wm/InputWindowManager.kt index 004784097..86c67b4ec 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/wm/InputWindowManager.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/wm/InputWindowManager.kt @@ -54,8 +54,9 @@ class InputWindowManager : UniqueViewComponent( } /** - * Associate essential window with its key - * This function does not create any view nor set up the scope + * Associate essential window with its key and add it to scope + * If [createView] is `true`, the view will be created immediately. + * Otherwise, it will be created on first attach */ @Suppress("BOUNDS_NOT_ALLOWED_IF_BOUNDED_BY_TYPE_PARAMETER") fun addEssentialWindow( @@ -64,11 +65,12 @@ class InputWindowManager : UniqueViewComponent( ) where R : W, R : E { ensureThread() if (window.key in essentialWindows) { - if (essentialWindows[window.key] === window) + if (essentialWindows[window.key]!!.first === window) Timber.d("Skip adding essential window $window") else throw IllegalStateException("${window.key} is already occupied") } + scope += window val view = if (createView) window.onCreateView() else null essentialWindows[window.key] = window to view } @@ -166,4 +168,6 @@ class InputWindowManager : UniqueViewComponent( if (!isUiThread()) throw IllegalThreadStateException("Window manager must be operated in main thread!") } + + fun isAttached(window: InputWindow) = currentWindow === window } \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/utils/Utils.kt b/app/src/main/java/org/fcitx/fcitx5/android/utils/Utils.kt index 893dc79a8..943e6e570 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/utils/Utils.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/utils/Utils.kt @@ -44,6 +44,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.fcitx.fcitx5.android.FcitxApplication import org.fcitx.fcitx5.android.R +import org.fcitx.fcitx5.android.core.CapabilityFlag +import org.fcitx.fcitx5.android.input.FcitxInputMethodService import splitties.experimental.InternalSplittiesApi import splitties.resources.withResolvedThemeAttribute import splitties.views.bottomPadding @@ -271,6 +273,5 @@ inline fun withTempDir(block: (File) -> T): T { } } -@Suppress("NOTHING_TO_INLINE") -inline infix fun T1?.notNullTo(y: T2?): Pair? = - this?.let { a -> y?.let { b -> a to b } } \ No newline at end of file +val FcitxInputMethodService.isInPasswordMode + get() = capabilityFlags.has(CapabilityFlag.Password) \ No newline at end of file From 51eaa40324462068c58106688f2c823f41efbe60 Mon Sep 17 00:00:00 2001 From: Potato Hatsue <1793913507@qq.com> Date: Fri, 17 Feb 2023 08:39:19 -0500 Subject: [PATCH 014/624] Refactor event state machine (#196) Co-authored-by: Rocka --- .../input/bar/ExpandButtonStateMachine.kt | 48 ++--- .../android/input/bar/IdleUiStateMachine.kt | 92 ++++++---- .../android/input/bar/KawaiiBarComponent.kt | 57 +++--- .../input/bar/KawaiiBarStateMachine.kt | 85 ++++----- .../HorizontalCandidateComponent.kt | 10 +- .../window/BaseExpandedCandidateWindow.kt | 10 +- .../input/clipboard/ClipboardStateMachine.kt | 54 +++--- .../input/clipboard/ClipboardWindow.kt | 19 +- .../android/input/keyboard/KeyboardWindow.kt | 7 +- .../fcitx5/android/utils/EventStateMachine.kt | 164 +++++++++++------- .../fcitx5/android/utils/ImmutableGraph.kt | 8 - 11 files changed, 311 insertions(+), 243 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ExpandButtonStateMachine.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ExpandButtonStateMachine.kt index ad9843687..a03679508 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ExpandButtonStateMachine.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ExpandButtonStateMachine.kt @@ -1,9 +1,12 @@ package org.fcitx.fcitx5.android.input.bar -import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.State.* -import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.TransitionEvent.* +import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.BooleanKey.CandidatesEmpty +import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.State.ClickToAttachWindow +import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.State.ClickToDetachWindow +import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.State.Hidden +import org.fcitx.fcitx5.android.utils.BuildTransitionEvent import org.fcitx.fcitx5.android.utils.EventStateMachine -import org.fcitx.fcitx5.android.utils.eventStateMachine +import org.fcitx.fcitx5.android.utils.TransitionBuildBlock object ExpandButtonStateMachine { @@ -13,23 +16,28 @@ object ExpandButtonStateMachine { Hidden } - enum class TransitionEvent { - ExpandedCandidatesUpdatedEmpty, - ExpandedCandidatesUpdatedNonEmpty, - ExpandedCandidatesAttached, - ExpandedCandidatesDetachedWithCandidatesEmpty, - ExpandedCandidatesDetachedWithCandidatesNonEmpty, + enum class BooleanKey : EventStateMachine.BooleanStateKey { + CandidatesEmpty } - fun new(block: (State) -> Unit): EventStateMachine = - eventStateMachine( - Hidden - ) { - from(Hidden) transitTo ClickToAttachWindow on ExpandedCandidatesUpdatedNonEmpty - from(ClickToAttachWindow) transitTo Hidden on ExpandedCandidatesUpdatedEmpty - from(ClickToAttachWindow) transitTo ClickToDetachWindow on ExpandedCandidatesAttached - from(ClickToDetachWindow) transitTo ClickToAttachWindow on ExpandedCandidatesDetachedWithCandidatesNonEmpty - from(ClickToDetachWindow) transitTo Hidden on ExpandedCandidatesDetachedWithCandidatesEmpty - onNewState(block) + enum class TransitionEvent(val builder: TransitionBuildBlock) : + EventStateMachine.TransitionEvent by BuildTransitionEvent(builder) { + ExpandedCandidatesUpdated({ + from(Hidden) transitTo ClickToAttachWindow on (CandidatesEmpty to false) + from(ClickToAttachWindow) transitTo Hidden on (CandidatesEmpty to true) + }), + ExpandedCandidatesAttached({ + from(ClickToAttachWindow) transitTo ClickToDetachWindow + }), + ExpandedCandidatesDetached({ + from(ClickToDetachWindow) transitTo Hidden on (CandidatesEmpty to true) + from(ClickToDetachWindow) transitTo ClickToAttachWindow on (CandidatesEmpty to false) + }); + } + + fun new(block: (State) -> Unit) = + EventStateMachine(Hidden).apply { + onNewStateListener = block } -} \ No newline at end of file +} + diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/IdleUiStateMachine.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/IdleUiStateMachine.kt index 277c8921e..161d7f7b2 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/IdleUiStateMachine.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/IdleUiStateMachine.kt @@ -1,49 +1,69 @@ package org.fcitx.fcitx5.android.input.bar -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.State.* -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.* +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.BooleanKey.ClipboardEmpty +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.State.Clipboard +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.State.ClipboardTimedOut +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.State.Empty +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.State.Toolbar +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.State.ToolbarWithClip +import org.fcitx.fcitx5.android.utils.BuildTransitionEvent import org.fcitx.fcitx5.android.utils.EventStateMachine -import org.fcitx.fcitx5.android.utils.eventStateMachine +import org.fcitx.fcitx5.android.utils.TransitionBuildBlock object IdleUiStateMachine { enum class State { Clipboard, Toolbar, Empty, ToolbarWithClip, ClipboardTimedOut } - enum class TransitionEvent { - Timeout, - Pasted, - MenuButtonClicked, - ClipboardUpdatedEmpty, - ClipboardUpdatedNonEmpty, - KawaiiBarShown, + enum class BooleanKey : EventStateMachine.BooleanStateKey { + ClipboardEmpty } - fun new( - toolbarByDefault: Boolean, - old: EventStateMachine? = null, - block: ((State) -> Unit)? = null - ): EventStateMachine { - val initialState = if (toolbarByDefault) Toolbar else Empty - return eventStateMachine(old?.currentState ?: initialState) { - from(Toolbar) transitTo Clipboard on ClipboardUpdatedNonEmpty - from(Toolbar) transitTo Empty on MenuButtonClicked - from(ToolbarWithClip) transitTo Toolbar on Timeout - from(ToolbarWithClip) transitTo Toolbar on ClipboardUpdatedEmpty - from(ToolbarWithClip) transitTo Clipboard on MenuButtonClicked - from(ToolbarWithClip) transitTo Clipboard on ClipboardUpdatedNonEmpty - from(Clipboard) transitTo ToolbarWithClip on MenuButtonClicked - from(Clipboard) transitTo ClipboardTimedOut on Timeout - from(Clipboard) transitTo initialState on Pasted - from(Clipboard) transitTo initialState on ClipboardUpdatedEmpty - from(ClipboardTimedOut) transitTo Toolbar on MenuButtonClicked - from(ClipboardTimedOut) transitTo initialState on KawaiiBarShown - from(ClipboardTimedOut) transitTo initialState on Pasted - from(ClipboardTimedOut) transitTo initialState on ClipboardUpdatedEmpty - from(ClipboardTimedOut) transitTo Clipboard on ClipboardUpdatedNonEmpty - from(Empty) transitTo Toolbar on MenuButtonClicked - from(Empty) transitTo Clipboard on ClipboardUpdatedNonEmpty - onNewState(old?.onNewStateListener ?: block ?: throw IllegalArgumentException()) - } + enum class TransitionEvent(val builder: TransitionBuildBlock) : + EventStateMachine.TransitionEvent by BuildTransitionEvent(builder) { + Timeout({ + from(ToolbarWithClip) transitTo Toolbar + from(Clipboard) transitTo ClipboardTimedOut + }), + Pasted({ + accept { initial, current, _ -> + when (current) { + Clipboard -> initial + ClipboardTimedOut -> initial + else -> current + } + } + }), + MenuButtonClicked({ + from(Toolbar) transitTo Empty + from(ToolbarWithClip) transitTo Clipboard + from(Clipboard) transitTo ToolbarWithClip + from(ClipboardTimedOut) transitTo Toolbar + from(Empty) transitTo Toolbar + }), + ClipboardUpdated({ + accept { initial, current, bool -> + val clipboardEmpty = bool(ClipboardEmpty) == true + when (current) { + ToolbarWithClip -> if (clipboardEmpty) Toolbar else Clipboard + Clipboard -> if (clipboardEmpty) initial else current + ClipboardTimedOut -> if (clipboardEmpty) initial else Clipboard + Toolbar -> if (!clipboardEmpty) Clipboard else current + Empty -> if (!clipboardEmpty) Clipboard else current + } + } + }), + KawaiiBarShown({ + accept { initial, current, _ -> + if (current == ClipboardTimedOut) initial + else current + } + }) } + + fun new(toolbarByDefault: Boolean, block: (State) -> Unit) = + EventStateMachine(if (toolbarByDefault) Toolbar else Empty).apply { + onNewStateListener = block + } + } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt index d1a8390e7..5ddbab313 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt @@ -22,21 +22,20 @@ import org.fcitx.fcitx5.android.data.theme.ThemeManager import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.State.ClickToAttachWindow import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.State.ClickToDetachWindow import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.State.Hidden -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.ClipboardUpdatedEmpty -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.ClipboardUpdatedNonEmpty +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.BooleanKey.ClipboardEmpty +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.ClipboardUpdated import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.KawaiiBarShown import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.MenuButtonClicked import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.Pasted import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.Timeout -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.CandidateUpdateNonEmpty -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.CapFlagsUpdatedNoPassword -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.CapFlagsUpdatedPassword +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.BooleanKey.CandidateEmpty +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.BooleanKey.CapFlagsPassword +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.BooleanKey.PreeditEmpty +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.CandidatesUpdated +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.CapFlagsUpdated import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.ExtendedWindowAttached -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.PreeditUpdatedEmpty -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.PreeditUpdatedNonEmpty -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.WindowDetachedWithCandidatesNonEmpty -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.WindowDetachedWithCapFlagsNoPassword -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.WindowDetachedWithCapFlagsPassword +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.PreeditUpdated +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.WindowDetached import org.fcitx.fcitx5.android.input.broadcast.InputBroadcastReceiver import org.fcitx.fcitx5.android.input.candidates.HorizontalCandidateComponent import org.fcitx.fcitx5.android.input.candidates.expanded.ExpandedCandidateStyle @@ -55,7 +54,6 @@ import org.fcitx.fcitx5.android.input.status.StatusAreaWindow import org.fcitx.fcitx5.android.input.wm.InputWindow import org.fcitx.fcitx5.android.input.wm.InputWindowManager import org.fcitx.fcitx5.android.utils.AppUtil -import org.fcitx.fcitx5.android.utils.isInPasswordMode import org.mechdancer.dependency.DynamicScope import org.mechdancer.dependency.manager.must import splitties.bitflags.hasFlag @@ -89,10 +87,10 @@ class KawaiiBarComponent : UniqueViewComponent( if (!clipboardSuggestion.getValue()) return@OnClipboardUpdateListener service.lifecycleScope.launch { if (it.text.isEmpty()) { - idleUiStateMachine.push(ClipboardUpdatedEmpty) + idleUiStateMachine.push(ClipboardUpdated, ClipboardEmpty to true) } else { idleUi.setClipboardItemText(it.text.take(42)) - idleUiStateMachine.push(ClipboardUpdatedNonEmpty) + idleUiStateMachine.push(ClipboardUpdated, ClipboardEmpty to false) launchClipboardTimeoutJob() } } @@ -101,7 +99,7 @@ class KawaiiBarComponent : UniqueViewComponent( private val onClipboardSuggestionUpdateListener = ManagedPreference.OnChangeListener { _, it -> if (!it) { - idleUiStateMachine.push(ClipboardUpdatedEmpty) + idleUiStateMachine.push(ClipboardUpdated, ClipboardEmpty to true) clipboardTimeoutJob?.cancel() clipboardTimeoutJob = null } @@ -122,7 +120,9 @@ class KawaiiBarComponent : UniqueViewComponent( private val onExpandToolbarByDefaultUpdateListener = ManagedPreference.OnChangeListener { _, it -> - idleUiStateMachine = IdleUiStateMachine.new(it, idleUiStateMachine) + idleUiStateMachine = IdleUiStateMachine.new(it) { + idleUi.switchUiByState(it) + } } private val popupActionListener by lazy { @@ -305,26 +305,19 @@ class KawaiiBarComponent : UniqueViewComponent( idleUi.privateMode(info.imeOptions.hasFlag(EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING)) } idleUiStateMachine.push(KawaiiBarShown) - barStateMachine.push( - if (capFlags.has(CapabilityFlag.Password)) - CapFlagsUpdatedPassword - else - CapFlagsUpdatedNoPassword - ) + barStateMachine.setBooleanState(CapFlagsPassword, capFlags.has(CapabilityFlag.Password)) + barStateMachine.push(CapFlagsUpdated) } override fun onPreeditUpdate(data: FcitxEvent.PreeditEvent.Data) { barStateMachine.push( - if (data.preedit.isEmpty() && data.clientPreedit.isEmpty()) - PreeditUpdatedEmpty - else - PreeditUpdatedNonEmpty + PreeditUpdated, + PreeditEmpty to (data.preedit.isEmpty() && data.clientPreedit.isEmpty()) ) } override fun onCandidateUpdate(data: Array) { - if (data.isNotEmpty()) - barStateMachine.push(CandidateUpdateNonEmpty) + barStateMachine.push(CandidatesUpdated, CandidateEmpty to data.isEmpty()) } override fun onWindowAttached(window: InputWindow) { @@ -344,15 +337,7 @@ class KawaiiBarComponent : UniqueViewComponent( } override fun onWindowDetached(window: InputWindow) { - barStateMachine.push( - if (horizontalCandidate.adapter.candidates.isEmpty()) - if (service.isInPasswordMode) - WindowDetachedWithCapFlagsPassword - else - WindowDetachedWithCapFlagsNoPassword - else - WindowDetachedWithCandidatesNonEmpty - ) + barStateMachine.push(WindowDetached) } } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarStateMachine.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarStateMachine.kt index 21e0d4546..a6ee5d380 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarStateMachine.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarStateMachine.kt @@ -1,56 +1,63 @@ package org.fcitx.fcitx5.android.input.bar +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.BooleanKey.CandidateEmpty +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.BooleanKey.CapFlagsPassword +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.BooleanKey.PreeditEmpty import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.State.Candidate import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.State.Idle import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.State.NumberRow import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.State.Title -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.CandidateUpdateNonEmpty -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.CapFlagsUpdatedNoPassword -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.CapFlagsUpdatedPassword -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.ExtendedWindowAttached -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.KeyboardSwitchedOutNumberWithCapFlagsPassword -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.KeyboardSwitchedToNumber -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.PreeditUpdatedEmpty -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.PreeditUpdatedNonEmpty -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.WindowDetachedWithCandidatesNonEmpty -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.WindowDetachedWithCapFlagsNoPassword -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.WindowDetachedWithCapFlagsPassword -import org.fcitx.fcitx5.android.utils.eventStateMachine +import org.fcitx.fcitx5.android.utils.BuildTransitionEvent +import org.fcitx.fcitx5.android.utils.EventStateMachine +import org.fcitx.fcitx5.android.utils.TransitionBuildBlock object KawaiiBarStateMachine { enum class State { Idle, Candidate, Title, NumberRow } - enum class TransitionEvent { - PreeditUpdatedEmpty, - PreeditUpdatedNonEmpty, - CandidateUpdateNonEmpty, - ExtendedWindowAttached, - WindowDetachedWithCapFlagsPassword, - WindowDetachedWithCapFlagsNoPassword, - WindowDetachedWithCandidatesNonEmpty, - CapFlagsUpdatedPassword, - CapFlagsUpdatedNoPassword, - KeyboardSwitchedToNumber, - KeyboardSwitchedOutNumberWithCapFlagsPassword + enum class BooleanKey : EventStateMachine.BooleanStateKey { + PreeditEmpty, CandidateEmpty, CapFlagsPassword } - fun new(block: (State) -> Unit) = eventStateMachine(Idle) { - from(Idle) transitTo Title on ExtendedWindowAttached - from(Idle) transitTo Candidate on PreeditUpdatedNonEmpty - from(Idle) transitTo Candidate on CandidateUpdateNonEmpty - from(Idle) transitTo NumberRow on CapFlagsUpdatedPassword - from(Idle) transitTo NumberRow on KeyboardSwitchedOutNumberWithCapFlagsPassword - from(Title) transitTo Idle on WindowDetachedWithCapFlagsNoPassword - from(Title) transitTo NumberRow on WindowDetachedWithCapFlagsPassword - from(Title) transitTo Candidate on WindowDetachedWithCandidatesNonEmpty - from(Candidate) transitTo Idle on PreeditUpdatedEmpty - from(Candidate) transitTo Title on ExtendedWindowAttached - from(NumberRow) transitTo Idle on CapFlagsUpdatedNoPassword - from(NumberRow) transitTo Title on ExtendedWindowAttached - from(NumberRow) transitTo Idle on KeyboardSwitchedToNumber - onNewState(block) + enum class TransitionEvent(val builder: TransitionBuildBlock) : + EventStateMachine.TransitionEvent by BuildTransitionEvent(builder) { + PreeditUpdated({ + from(Candidate) transitTo Idle on (PreeditEmpty to true) + from(Idle) transitTo Candidate on (PreeditEmpty to false) + }), + CandidatesUpdated({ + from(Idle) transitTo Candidate on (CandidateEmpty to false) + }), + ExtendedWindowAttached({ + from(Idle) transitTo Title + from(Candidate) transitTo Title + from(NumberRow) transitTo Title + }), + CapFlagsUpdated({ + from(Idle) transitTo NumberRow on (CapFlagsPassword to true) + from(NumberRow) transitTo Idle on (CapFlagsPassword to false) + }), + WindowDetached({ + // candidate state has higher priority so here it goes first + from(Title) transitTo Candidate on (CandidateEmpty to false) + from(Title) transitTo Idle on (CapFlagsPassword to false) + from(Title) transitTo NumberRow on (CapFlagsPassword to true) + }), + KeyboardSwitchedOutNumber({ + from(Idle) transitTo NumberRow on (CapFlagsPassword to true) + }), + KeyboardSwitchedToNumber({ + from(NumberRow) transitTo Idle + }) } + + fun new(block: (State) -> Unit) = + EventStateMachine( + Idle + ).apply { + onNewStateListener = block + } + } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/HorizontalCandidateComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/HorizontalCandidateComponent.kt index fef6f06c4..5f43cf192 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/HorizontalCandidateComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/HorizontalCandidateComponent.kt @@ -10,8 +10,8 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.runBlocking import org.fcitx.fcitx5.android.R import org.fcitx.fcitx5.android.data.prefs.AppPrefs -import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.TransitionEvent.ExpandedCandidatesUpdatedEmpty -import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.TransitionEvent.ExpandedCandidatesUpdatedNonEmpty +import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.BooleanKey.CandidatesEmpty +import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.TransitionEvent.ExpandedCandidatesUpdated import org.fcitx.fcitx5.android.input.bar.KawaiiBarComponent import org.fcitx.fcitx5.android.input.broadcast.InputBroadcastReceiver import org.fcitx.fcitx5.android.input.candidates.expanded.decoration.FlexboxVerticalDecoration @@ -63,10 +63,8 @@ class HorizontalCandidateComponent : super.onLayoutCompleted(state) refreshExpanded() bar.expandButtonStateMachine.push( - if (adapter!!.itemCount - childCount > 0) - ExpandedCandidatesUpdatedNonEmpty - else - ExpandedCandidatesUpdatedEmpty + ExpandedCandidatesUpdated, + CandidatesEmpty to (adapter!!.itemCount - childCount <= 0) ) } } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/BaseExpandedCandidateWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/BaseExpandedCandidateWindow.kt index 7be81d486..3c63e949f 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/BaseExpandedCandidateWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/BaseExpandedCandidateWindow.kt @@ -10,7 +10,9 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.fcitx.fcitx5.android.core.FcitxEvent import org.fcitx.fcitx5.android.data.prefs.AppPrefs -import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.TransitionEvent.* +import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.BooleanKey.CandidatesEmpty +import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.TransitionEvent.ExpandedCandidatesAttached +import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.TransitionEvent.ExpandedCandidatesDetached import org.fcitx.fcitx5.android.input.bar.KawaiiBarComponent import org.fcitx.fcitx5.android.input.broadcast.InputBroadcastReceiver import org.fcitx.fcitx5.android.input.candidates.CandidateViewBuilder @@ -88,10 +90,8 @@ abstract class BaseExpandedCandidateWindow> : override fun onDetached() { bar.expandButtonStateMachine.push( - if (adapter.candidates.size > adapter.offset) - ExpandedCandidatesDetachedWithCandidatesNonEmpty - else - ExpandedCandidatesDetachedWithCandidatesEmpty + ExpandedCandidatesDetached, + CandidatesEmpty to (adapter.candidates.size <= adapter.offset) ) offsetJob?.cancel() offsetJob = null diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardStateMachine.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardStateMachine.kt index 48e57ece6..56aeadd9a 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardStateMachine.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardStateMachine.kt @@ -1,9 +1,13 @@ package org.fcitx.fcitx5.android.input.clipboard -import org.fcitx.fcitx5.android.input.clipboard.ClipboardStateMachine.State.* -import org.fcitx.fcitx5.android.input.clipboard.ClipboardStateMachine.TransitionEvent.* +import org.fcitx.fcitx5.android.input.clipboard.ClipboardStateMachine.BooleanKey.ClipboardDbEmpty +import org.fcitx.fcitx5.android.input.clipboard.ClipboardStateMachine.BooleanKey.ClipboardListeningEnabled +import org.fcitx.fcitx5.android.input.clipboard.ClipboardStateMachine.State.AddMore +import org.fcitx.fcitx5.android.input.clipboard.ClipboardStateMachine.State.EnableListening +import org.fcitx.fcitx5.android.input.clipboard.ClipboardStateMachine.State.Normal +import org.fcitx.fcitx5.android.utils.BuildTransitionEvent import org.fcitx.fcitx5.android.utils.EventStateMachine -import org.fcitx.fcitx5.android.utils.eventStateMachine +import org.fcitx.fcitx5.android.utils.TransitionBuildBlock object ClipboardStateMachine { @@ -11,26 +15,32 @@ object ClipboardStateMachine { Normal, AddMore, EnableListening } - enum class TransitionEvent { - ClipboardDbUpdatedEmpty, - ClipboardDbUpdatedNonEmpty, - ClipboardListeningDisabled, - ClipboardListeningEnabledWithDbNonEmpty, - ClipboardListeningEnabledWithDbEmpty + enum class BooleanKey : EventStateMachine.BooleanStateKey { + ClipboardDbEmpty, + ClipboardListeningEnabled } - fun new( - initialState: State, - block: (State) -> Unit - ): EventStateMachine = eventStateMachine( - initialState - ) { - from(Normal) transitTo AddMore on ClipboardDbUpdatedEmpty - from(Normal) transitTo EnableListening on ClipboardListeningDisabled - from(EnableListening) transitTo Normal on ClipboardListeningEnabledWithDbNonEmpty - from(EnableListening) transitTo AddMore on ClipboardListeningEnabledWithDbEmpty - from(AddMore) transitTo Normal on ClipboardDbUpdatedNonEmpty - from(AddMore) transitTo EnableListening on ClipboardListeningDisabled - onNewState(block) + enum class TransitionEvent(val builder: TransitionBuildBlock) : + EventStateMachine.TransitionEvent by BuildTransitionEvent(builder) { + ClipboardDbUpdated({ + from(Normal) transitTo AddMore on (ClipboardDbEmpty to true) + from(AddMore) transitTo Normal on (ClipboardDbEmpty to false) + }), + ClipboardListeningUpdated({ + from(Normal) transitTo EnableListening on (ClipboardListeningEnabled to false) + from(EnableListening) transitTo Normal onF { + it(ClipboardListeningEnabled) == true && it(ClipboardDbEmpty) == false + } + from(EnableListening) transitTo AddMore onF { + it(ClipboardListeningEnabled) == true && it(ClipboardDbEmpty) == true + } + from(AddMore) transitTo EnableListening on (ClipboardListeningEnabled to false) + }) } + + fun new(initialState: State, block: (State) -> Unit) = + EventStateMachine(initialState).apply { + onNewStateListener = block + } + } \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardWindow.kt index cf83572e7..03f32dcf6 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardWindow.kt @@ -14,8 +14,13 @@ import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.data.prefs.ManagedPreference import org.fcitx.fcitx5.android.data.theme.Theme import org.fcitx.fcitx5.android.input.FcitxInputMethodService -import org.fcitx.fcitx5.android.input.clipboard.ClipboardStateMachine.State.* -import org.fcitx.fcitx5.android.input.clipboard.ClipboardStateMachine.TransitionEvent.* +import org.fcitx.fcitx5.android.input.clipboard.ClipboardStateMachine.BooleanKey.ClipboardDbEmpty +import org.fcitx.fcitx5.android.input.clipboard.ClipboardStateMachine.BooleanKey.ClipboardListeningEnabled +import org.fcitx.fcitx5.android.input.clipboard.ClipboardStateMachine.State.AddMore +import org.fcitx.fcitx5.android.input.clipboard.ClipboardStateMachine.State.EnableListening +import org.fcitx.fcitx5.android.input.clipboard.ClipboardStateMachine.State.Normal +import org.fcitx.fcitx5.android.input.clipboard.ClipboardStateMachine.TransitionEvent.ClipboardDbUpdated +import org.fcitx.fcitx5.android.input.clipboard.ClipboardStateMachine.TransitionEvent.ClipboardListeningUpdated import org.fcitx.fcitx5.android.input.dependency.inputMethodService import org.fcitx.fcitx5.android.input.dependency.theme import org.fcitx.fcitx5.android.input.wm.InputWindow @@ -29,21 +34,17 @@ class ClipboardWindow : InputWindow.ExtendedInputWindow() { private val service: FcitxInputMethodService by manager.inputMethodService() private val theme by manager.theme() - private lateinit var stateMachine: EventStateMachine + private lateinit var stateMachine: EventStateMachine private var isClipboardDbEmpty by Delegates.observable(ClipboardManager.itemCount == 0) { _, _, new -> stateMachine.push( - if (new) ClipboardDbUpdatedEmpty - else ClipboardDbUpdatedNonEmpty + ClipboardDbUpdated, ClipboardDbEmpty to new ) } private val clipboardEnabledListener = ManagedPreference.OnChangeListener { _, it -> stateMachine.push( - if (it) - if (isClipboardDbEmpty) ClipboardListeningEnabledWithDbEmpty - else ClipboardListeningEnabledWithDbNonEmpty - else ClipboardListeningDisabled + ClipboardListeningUpdated, ClipboardListeningEnabled to it ) } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt index 77c213290..875efc400 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt @@ -14,7 +14,7 @@ import org.fcitx.fcitx5.android.core.InputMethodEntry import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.input.FcitxInputMethodService import org.fcitx.fcitx5.android.input.bar.KawaiiBarComponent -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.KeyboardSwitchedOutNumberWithCapFlagsPassword +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.KeyboardSwitchedOutNumber import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.KeyboardSwitchedToNumber import org.fcitx.fcitx5.android.input.broadcast.InputBroadcastReceiver import org.fcitx.fcitx5.android.input.dependency.fcitx @@ -26,7 +26,6 @@ import org.fcitx.fcitx5.android.input.popup.PopupComponent import org.fcitx.fcitx5.android.input.wm.EssentialWindow import org.fcitx.fcitx5.android.input.wm.InputWindow import org.fcitx.fcitx5.android.input.wm.InputWindowManager -import org.fcitx.fcitx5.android.utils.isInPasswordMode import org.mechdancer.dependency.manager.must import splitties.views.dsl.core.add import splitties.views.dsl.core.frameLayout @@ -179,7 +178,7 @@ class KeyboardWindow : InputWindow.SimpleInputWindow(), Essentia private fun notifyBarLayoutChanged() { if (currentKeyboardName == NumberKeyboard.Name) bar.barStateMachine.push(KeyboardSwitchedToNumber) - else if (service.isInPasswordMode) - bar.barStateMachine.push(KeyboardSwitchedOutNumberWithCapFlagsPassword) + else + bar.barStateMachine.push(KeyboardSwitchedOutNumber) } } \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/utils/EventStateMachine.kt b/app/src/main/java/org/fcitx/fcitx5/android/utils/EventStateMachine.kt index 59334aafb..ece48abd6 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/utils/EventStateMachine.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/utils/EventStateMachine.kt @@ -3,95 +3,143 @@ package org.fcitx.fcitx5.android.utils import org.fcitx.fcitx5.android.data.prefs.AppPrefs import timber.log.Timber -class EventStateMachine( - initialState: State, - private val stateGraph: ImmutableGraph> +class EventStateMachine, B : EventStateMachine.BooleanStateKey>( + private val initialState: State, + private val externalBooleanStates: MutableMap = mutableMapOf() ) { - private var currentStateIx = stateGraph.vertices.indexOf(initialState) + interface BooleanStateKey { + val name: String + } + + interface TransitionEvent { + /** + * INVARIANT: No side effects + * @return the next state + */ + fun accept(initialState: State, currentState: State, useBoolean: (B) -> Boolean?): State + + } var onNewStateListener: ((State) -> Unit)? = null - val currentState - get() = stateGraph.vertices[currentStateIx] + var currentState = initialState + private set private val enableDebugLog: Boolean by AppPrefs.getInstance().internal.verboseLog - private val knownEvents by lazy { stateGraph.labels.flatten() } - /** * Push an event that may trigger a transition of state */ fun push(event: Event) { - if (event !in knownEvents) - throw IllegalArgumentException("$event is an unknown event") - val transitions = stateGraph.getEdgesOfVertexWithIndex(currentState) - val filtered = transitions.filter { event in it.second.label } - when (filtered.size) { - 0 -> { - // do nothing - if (enableDebugLog) - Timber.d("At $currentState ignored $event. All transitions ${transitions.map { it.second.label.joinToString() }} did not match") - } - 1 -> { - if (enableDebugLog) - Timber.d("At $currentState transited to ${filtered.first().second.vertex2}. Transition $event was matched") - currentStateIx = filtered.first().first.first - onNewStateListener?.invoke(filtered.first().second.vertex2) - } - else -> throw IllegalStateException("More than one transitions are found given $event on $currentState") + val newState = event.accept(initialState, currentState) { externalBooleanStates[it] } + if (newState == currentState) { + if (enableDebugLog) + Timber.d("At $currentState, $event didn't change the state") + return } + if (enableDebugLog) + Timber.d("At $currentState transited to $newState by $event") + currentState = newState + onNewStateListener?.invoke(newState) } -} + /** + * Update boolean states and push an event + */ + fun push(event: Event, vararg booleanStates: Pair) { + booleanStates.forEach { + setBooleanState(it.first, it.second) + } + push(event) + } + fun setBooleanState(key: B, value: Boolean) { + externalBooleanStates[key] = value + } -// DSL + fun getBooleanState(key: B) = + externalBooleanStates[key] -fun eventStateMachine( - initialState: State, - builder: EventStateMachineBuilder.() -> Unit -) = - EventStateMachineBuilder(initialState).apply(builder).build() + fun unsafeJump(state: State) { + currentState = state + onNewStateListener?.invoke(state) + } +} -class EventStateMachineBuilder( - private val initialState: State -) { - private val map = mutableMapOf, MutableList>() +// DSL +class TransitionEventBuilder { + + private val enableDebugLog: Boolean by AppPrefs.getInstance().internal.verboseLog - private var listener: ((State) -> Unit)? = null + private var raw: ((State, State, (B) -> Boolean?) -> State)? = null - inner class EventTransitionBuilder(val startState: State) { - lateinit var endState: State - lateinit var event: Event + inner class Builder(val source: State) { + lateinit var target: State + var pred: ((B) -> Boolean?) -> Boolean = { _ -> true } + } + infix fun Builder.transitTo(state: State) = apply { + target = state } - infix fun EventTransitionBuilder.transitTo(state: State) = apply { - endState = state + infix fun Builder.on(expected: Pair) = apply { + this.pred = { it(expected.first) == expected.second } } - infix fun EventTransitionBuilder.on(event: Event) = run { - this.event = event - if (startState to endState !in map) - map[startState to endState] = mutableListOf(event) - else - map.getValue(startState to endState) += event + infix fun Builder.onF(pred: ((B) -> Boolean?) -> Boolean) = apply { + this.pred = pred } - fun from(state: State) = EventTransitionBuilder(state) + val builders = mutableListOf() + + fun from(state: State) = Builder(state).also { builders += it } - fun onNewState(block: (State) -> Unit) { - listener = block + /** + * Use either [from] or [accept] to build the transition event + */ + fun accept(block: ((State, State, (B) -> Boolean?) -> State)) = apply { + raw = block } - fun build() = EventStateMachine( - initialState, - ImmutableGraph(map.map { (k, v) -> - ImmutableGraph.Edge(k.first, k.second, v) - }) - ).apply { - listener?.let { onNewStateListener = it } + fun build() = + object : EventStateMachine.TransitionEvent { + override fun accept( + initialState: State, + currentState: State, + useBoolean: (B) -> Boolean? + ): State { + if (raw != null) + return raw!!(initialState, currentState, useBoolean) + val filtered = builders.filter { it.source == currentState && it.pred(useBoolean) } + return when (filtered.size) { + 0 -> currentState + 1 -> filtered[0].target + else -> { + val first = filtered[0].target + if (enableDebugLog) + Timber.d("More than one target states at $currentState: ${filtered.joinToString()}. Take the first one: $first") + first + } + } + } + } +} + +typealias TransitionBuildBlock = TransitionEventBuilder.() -> Unit + +class BuildTransitionEvent(block: TransitionBuildBlock) : + EventStateMachine.TransitionEvent { + private val delegate: EventStateMachine.TransitionEvent by lazy { + TransitionEventBuilder().also(block).build() } + + override fun accept( + initialState: State, + currentState: State, + useBoolean: (B) -> Boolean? + ): State = + delegate.accept(initialState, currentState, useBoolean) + } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/utils/ImmutableGraph.kt b/app/src/main/java/org/fcitx/fcitx5/android/utils/ImmutableGraph.kt index b311aaecd..957f813fe 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/utils/ImmutableGraph.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/utils/ImmutableGraph.kt @@ -25,14 +25,6 @@ class ImmutableGraph( } } - fun getEdgesOfVertexWithIndex(vertex: V) = adjacencyMatrix[vertices.indexOf(vertex)] - .asIterable() - .mapIndexedNotNull { v2Idx, labelIdx -> - labelIdx.takeIf { it != -1 }?.run { - (v2Idx to labelIdx) to Edge(vertex, vertices[v2Idx], labels[labelIdx]) - } - } - /** * @param predicate: whether to continue searching after this node */ From f25cb153c161dc2d6696fc103341954a704e15ac Mon Sep 17 00:00:00 2001 From: Potato Hatsue <1793913507@qq.com> Date: Fri, 17 Feb 2023 23:35:17 -0500 Subject: [PATCH 015/624] Fix potential crash in fragments --- .../ui/main/settings/PunctuationEditorFragment.kt | 9 +++++++-- .../android/ui/main/settings/QuickPhraseEditFragment.kt | 9 +++++++-- .../ui/main/settings/im/InputMethodListFragment.kt | 3 ++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/PunctuationEditorFragment.kt b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/PunctuationEditorFragment.kt index 1d6c57588..11d2fe4b0 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/PunctuationEditorFragment.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/PunctuationEditorFragment.kt @@ -14,7 +14,11 @@ import org.fcitx.fcitx5.android.ui.common.BaseDynamicListUi import org.fcitx.fcitx5.android.ui.common.OnItemChangedListener import org.fcitx.fcitx5.android.utils.NaiveDustman import org.fcitx.fcitx5.android.utils.str -import splitties.views.dsl.core.* +import splitties.views.dsl.core.add +import splitties.views.dsl.core.lParams +import splitties.views.dsl.core.matchParent +import splitties.views.dsl.core.verticalLayout +import splitties.views.dsl.core.view import splitties.views.dsl.material.addInput import splitties.views.setPaddingDp @@ -167,7 +171,8 @@ class PunctuationEditorFragment : ProgressFragment(), OnItemChangedListener Date: Sat, 18 Feb 2023 22:14:27 +0800 Subject: [PATCH 016/624] Update prebuilt and enable OpenCC --- app/src/main/assets/usr/share/opencc | 1 + app/src/main/cpp/CMakeLists.txt | 11 ++++++++++- app/src/main/cpp/prebuilt | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) create mode 120000 app/src/main/assets/usr/share/opencc diff --git a/app/src/main/assets/usr/share/opencc b/app/src/main/assets/usr/share/opencc new file mode 120000 index 000000000..7acee9112 --- /dev/null +++ b/app/src/main/assets/usr/share/opencc @@ -0,0 +1 @@ +../../../cpp/prebuilt/opencc/data \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 37c4c808f..2a1b29de1 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -35,6 +35,11 @@ find_package(LibIntl) # prebuilt libevent set(Libevent_DIR "${PREBUILT_DIR}/libevent/${ANDROID_ABI}/lib/cmake/libevent") set(LIBEVENT_STATIC_LINK ON) +# LibeventTargets-static.cmake uses find_path and find_library, but NDK's android{,-legacy}.toolchain.cmake +# set CMAKE_SYSROOT and CMAKE_FIND_ROOT_PATH cause find_* commands to re-root input paths. +# turn off re-root behavior because we want to find libevent in prebuilt instead of NDK sysroot +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY NEVER) find_package(Libevent) option(ENABLE_TEST "" OFF) @@ -79,12 +84,16 @@ include("${PREBUILT_DIR}/lua/${ANDROID_ABI}/lib/cmake/LuaConfig.cmake") option(USE_DLOPEN "" OFF) add_subdirectory(fcitx5-lua) +set(OpenCC_DIR "${PREBUILT_DIR}/opencc/${ANDROID_ABI}/lib/cmake/opencc") +find_package(OpenCC) + option(ENABLE_TEST "" OFF) -option(ENABLE_OPENCC "" OFF) option(ENABLE_GUI "" OFF) option(ENABLE_BROWSER "" OFF) option(USE_WEBKIT "" OFF) option(ENABLE_CLOUDPINYIN "" OFF) +# prefer OpenCC_DIR rather than fcitx5-chinese-addons/cmake/FindOpenCC.cmake +set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON) add_subdirectory(fcitx5-chinese-addons) # rename to include executable in apk set_target_properties(scel2org5 PROPERTIES OUTPUT_NAME libscel2org5.so) diff --git a/app/src/main/cpp/prebuilt b/app/src/main/cpp/prebuilt index fb5695039..95021a13e 160000 --- a/app/src/main/cpp/prebuilt +++ b/app/src/main/cpp/prebuilt @@ -1 +1 @@ -Subproject commit fb569503911087b00a052e8a87507bd56a8a2adf +Subproject commit 95021a13ed23dec90ce42de6e6076c544df8515f From e6e00e10dc8802b2593f72bf7cb9b52064c214ab Mon Sep 17 00:00:00 2001 From: Rocka Date: Sat, 18 Feb 2023 22:35:00 +0800 Subject: [PATCH 017/624] Update fcitx5 submodules --- app/src/main/cpp/fcitx5 | 2 +- app/src/main/cpp/fcitx5-chinese-addons | 2 +- app/src/main/cpp/fcitx5-unikey | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/cpp/fcitx5 b/app/src/main/cpp/fcitx5 index c8518ff74..3a46edf0c 160000 --- a/app/src/main/cpp/fcitx5 +++ b/app/src/main/cpp/fcitx5 @@ -1 +1 @@ -Subproject commit c8518ff74cb55424a64f666e935b83b0c9a2d199 +Subproject commit 3a46edf0c83c40285e5fd8dc76f9b95990024e47 diff --git a/app/src/main/cpp/fcitx5-chinese-addons b/app/src/main/cpp/fcitx5-chinese-addons index df605315d..1d541b2fd 160000 --- a/app/src/main/cpp/fcitx5-chinese-addons +++ b/app/src/main/cpp/fcitx5-chinese-addons @@ -1 +1 @@ -Subproject commit df605315d305f2713797bca6d9d48b7c038ac94b +Subproject commit 1d541b2fd65edb2b737a6f5e219bd2e3c2aaf7e1 diff --git a/app/src/main/cpp/fcitx5-unikey b/app/src/main/cpp/fcitx5-unikey index 3155d51c7..d7dac40c6 160000 --- a/app/src/main/cpp/fcitx5-unikey +++ b/app/src/main/cpp/fcitx5-unikey @@ -1 +1 @@ -Subproject commit 3155d51c7e8924e4e756ebd0688b40966a3221f0 +Subproject commit d7dac40c6dbb8d4168e86c6c417232178698b5dc From a13e8542050bd692de491009e47c50d727c095a8 Mon Sep 17 00:00:00 2001 From: Rocka Date: Fri, 17 Feb 2023 22:12:10 +0800 Subject: [PATCH 018/624] Split object-conversion.h from native-lib.cpp --- app/src/main/cpp/helper-types.h | 32 +++++ app/src/main/cpp/jni-utils.h | 5 +- app/src/main/cpp/native-lib.cpp | 181 +++------------------------ app/src/main/cpp/object-conversion.h | 160 +++++++++++++++++++++++ 4 files changed, 208 insertions(+), 170 deletions(-) create mode 100644 app/src/main/cpp/object-conversion.h diff --git a/app/src/main/cpp/helper-types.h b/app/src/main/cpp/helper-types.h index 0850d94c1..fa2e9453e 100644 --- a/app/src/main/cpp/helper-types.h +++ b/app/src/main/cpp/helper-types.h @@ -5,6 +5,38 @@ #include #include +class InputMethodStatus { +public: + const fcitx::InputMethodEntry *entry; + std::string subMode; + std::string subModeLabel; + std::string subModeIcon; + + InputMethodStatus(const fcitx::InputMethodEntry *entry, + fcitx::InputMethodEngine *engine, + fcitx::InputContext *ic) + : entry(entry) { + if (engine) { + subMode = engine->subMode(*entry, *ic); + subModeLabel = engine->subModeLabel(*entry, *ic); + subModeIcon = engine->subModeIcon(*entry, *ic); + } + } + + InputMethodStatus(const fcitx::InputMethodEntry *entry) + : entry(entry) {} +}; + +class AddonStatus { +public: + const fcitx::AddonInfo *info; + bool enabled; + + AddonStatus(const fcitx::AddonInfo *info, bool enabled) : + info(info), + enabled(enabled) {} +}; + class ActionEntity { public: int id; diff --git a/app/src/main/cpp/jni-utils.h b/app/src/main/cpp/jni-utils.h index 6d8dcecef..5ec574728 100644 --- a/app/src/main/cpp/jni-utils.h +++ b/app/src/main/cpp/jni-utils.h @@ -98,9 +98,6 @@ class GlobalRefSingleton { jclass Integer; jmethodID IntegerInit; - jclass Long; - jmethodID LongInit; - jclass Boolean; jmethodID BooleanInit; @@ -174,4 +171,6 @@ class GlobalRefSingleton { const JEnv AttachEnv() const { return JEnv(jvm); } }; +extern GlobalRefSingleton *GlobalRef; + #endif //FCITX5_ANDROID_JNI_UTILS_H diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp index b0e2d42c2..2fa1aa12b 100644 --- a/app/src/main/cpp/native-lib.cpp +++ b/app/src/main/cpp/native-lib.cpp @@ -28,6 +28,7 @@ #include "jni-utils.h" #include "nativestreambuf.h" #include "helper-types.h" +#include "object-conversion.h" class Fcitx { @@ -134,21 +135,14 @@ class Fcitx { return std::move(entries); } - typedef std::tuple> IMStatus; - - IMStatus inputMethodStatus() { + InputMethodStatus inputMethodStatus() { auto *ic = p_frontend->call(); auto *engine = p_instance->inputMethodEngine(ic); const auto *entry = p_instance->inputMethodEntry(ic); if (engine) { - auto subMode = engine->subMode(*entry, *ic); - auto subModeLabel = engine->subModeLabel(*entry, *ic); - auto subModeIcon = engine->subModeIcon(*entry, *ic); - return std::make_tuple(entry, std::vector{subMode, subModeLabel, subModeIcon}); - } else if (entry) { - return std::make_tuple(entry, std::vector{}); + return {entry, engine, ic}; } - return std::make_tuple(nullptr, std::vector{}); + return {entry}; } void setInputMethod(const std::string &ime) { @@ -273,14 +267,14 @@ class Fcitx { engine->setConfigForInputMethod(*entry, config); } - std::map getAddons() { + std::vector getAddons() { auto &globalConfig = p_instance->globalConfig(); auto &addonManager = p_instance->addonManager(); const auto &enabledAddons = globalConfig.enabledAddons(); std::unordered_set enabledSet(enabledAddons.begin(), enabledAddons.end()); const auto &disabledAddons = globalConfig.disabledAddons(); std::unordered_set disabledSet(disabledAddons.begin(), disabledAddons.end()); - std::map addons; + std::vector addons; for (const auto category: {fcitx::AddonCategory::InputMethod, fcitx::AddonCategory::Frontend, fcitx::AddonCategory::Loader, @@ -298,7 +292,7 @@ class Fcitx { } else if (enabledSet.count(info->uniqueName())) { enabled = true; } - addons.insert({info, enabled}); + addons.emplace_back(AddonStatus(info, enabled)); } } return addons; @@ -337,7 +331,7 @@ class Fcitx { void triggerQuickPhrase() { if (!p_quickphrase) return; - auto *ic = p_instance->inputContextManager().lastFocusedInputContext(); + auto *ic = p_frontend->call(); if (!ic) return; p_quickphrase->call( ic, "", "", "", "", fcitx::Key{FcitxKey_None} @@ -346,7 +340,7 @@ class Fcitx { void triggerUnicode() { if (!p_unicode) return; - auto *ic = p_instance->inputContextManager().lastFocusedInputContext(); + auto *ic = p_frontend->call(); if (!ic) return; p_unicode->call(ic); } @@ -437,7 +431,6 @@ class Fcitx { } }; - #define DO_IF_NOT_RUNNING(expr) \ if (!Fcitx::Instance().isRunning()) { \ FCITX_WARN() << "Fcitx is not running!"; \ @@ -446,7 +439,7 @@ class Fcitx { #define RETURN_IF_NOT_RUNNING DO_IF_NOT_RUNNING(return) #define RETURN_VALUE_IF_NOT_RUNNING(v) DO_IF_NOT_RUNNING(return (v)) -static GlobalRefSingleton *GlobalRef; +GlobalRefSingleton *GlobalRef; JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void * /* reserved */) { @@ -463,52 +456,6 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_setupLogStream(JNIEnv *env, jclass claz Fcitx::setLogStream(stream, verbose); } -jobject fcitxInputMethodEntryWithSubModeToJObject(JNIEnv *env, const fcitx::InputMethodEntry *entry, const std::vector &subMode); - -jobject fcitxActionToJObject(JNIEnv *env, const ActionEntity &act) { - jobjectArray menu = nullptr; - if (act.menu) { - const int size = static_cast(act.menu->size()); - menu = env->NewObjectArray(size, GlobalRef->Action, nullptr); - for (int i = 0; i < size; i++) { - env->SetObjectArrayElement(menu, i, fcitxActionToJObject(env, act.menu->at(i))); - } - } - auto obj = env->NewObject(GlobalRef->Action, GlobalRef->ActionInit, - act.id, - act.isSeparator, - act.isCheckable, - act.isChecked, - *JString(env, act.name), - *JString(env, act.icon), - *JString(env, act.shortText), - *JString(env, act.longText), - menu - ); - if (menu) { - env->DeleteLocalRef(menu); - } - return obj; -} - -jobject fcitxTextToJObject(JNIEnv *env, const fcitx::Text &text) { - const int size = static_cast(text.size()); - auto str = JRef(env, env->NewObjectArray(size, GlobalRef->String, nullptr)); - auto fmt = JRef(env, env->NewIntArray(size)); - int flag = static_cast(fcitx::TextFormatFlag::NoFlag); - for (int i = 0; i < size; i++) { - env->SetObjectArrayElement(str, i, *JString(env, text.stringAt(i))); - flag = text.formatAt(i).toInteger(); - env->SetIntArrayRegion(fmt, i, 1, &flag); - } - auto obj = env->CallStaticObjectMethod(GlobalRef->FormattedText, GlobalRef->FormattedTextFromByteCursor, - *str, - *fmt, - text.cursor() - ); - return obj; -} - extern "C" JNIEXPORT void JNICALL Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(JNIEnv *env, jclass clazz, jstring locale, jstring appData, jstring appLib, jstring extData, jstring extCache) { @@ -628,7 +575,7 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(JNIEnv *env, jclass clazz, auto env = GlobalRef->AttachEnv(); auto vararg = JRef(env, env->NewObjectArray(1, GlobalRef->Object, nullptr)); const auto status = Fcitx::Instance().inputMethodStatus(); - auto obj = JRef(env, fcitxInputMethodEntryWithSubModeToJObject(env, std::get<0>(status), std::get<1>(status))); + auto obj = JRef(env, fcitxInputMethodStatusToJObject(env, status)); env->SetObjectArrayElement(vararg, 0, obj); env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 6, *vararg); }; @@ -756,28 +703,6 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_nextInputMethod(JNIEnv *env, jclass cla Fcitx::Instance().nextInputMethod(forward == JNI_TRUE); } -jobject fcitxInputMethodEntryToJObject(JNIEnv *env, const fcitx::InputMethodEntry *entry) { - return env->NewObject(GlobalRef->InputMethodEntry, GlobalRef->InputMethodEntryInit, - *JString(env, entry->uniqueName()), - *JString(env, entry->name()), - *JString(env, entry->icon()), - *JString(env, entry->nativeName()), - *JString(env, entry->label()), - *JString(env, entry->languageCode()), - entry->isConfigurable() - ); -} - -jobjectArray fcitxInputMethodEntriesToJObjectArray(JNIEnv *env, const std::vector &entries) { - jobjectArray array = env->NewObjectArray(static_cast(entries.size()), GlobalRef->InputMethodEntry, nullptr); - int i = 0; - for (const auto &entry: entries) { - auto obj = JRef(env, fcitxInputMethodEntryToJObject(env, entry)); - env->SetObjectArrayElement(array, i++, obj); - } - return array; -} - extern "C" JNIEXPORT jobjectArray JNICALL Java_org_fcitx_fcitx5_android_core_Fcitx_listInputMethods(JNIEnv *env, jclass clazz) { @@ -786,29 +711,12 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_listInputMethods(JNIEnv *env, jclass cl return fcitxInputMethodEntriesToJObjectArray(env, entries); } -jobject fcitxInputMethodEntryWithSubModeToJObject(JNIEnv *env, const fcitx::InputMethodEntry *entry, const std::vector &subMode) { - if (!entry) return nullptr; - if (subMode.empty()) return fcitxInputMethodEntryToJObject(env, entry); - return env->NewObject(GlobalRef->InputMethodEntry, GlobalRef->InputMethodEntryInitWithSubMode, - *JString(env, entry->uniqueName()), - *JString(env, entry->name()), - *JString(env, entry->icon()), - *JString(env, entry->nativeName()), - *JString(env, entry->label()), - *JString(env, entry->languageCode()), - entry->isConfigurable(), - *JString(env, subMode[0]), - *JString(env, subMode[1]), - *JString(env, subMode[2]) - ); -} - extern "C" JNIEXPORT jobject JNICALL Java_org_fcitx_fcitx5_android_core_Fcitx_inputMethodStatus(JNIEnv *env, jclass clazz) { RETURN_VALUE_IF_NOT_RUNNING(nullptr) const auto &status = Fcitx::Instance().inputMethodStatus(); - return fcitxInputMethodEntryWithSubModeToJObject(env, std::get<0>(status), std::get<1>(status)); + return fcitxInputMethodStatusToJObject(env, status); } extern "C" @@ -839,25 +747,6 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_setEnabledInputMethods(JNIEnv *env, jcl Fcitx::Instance().setEnabledInputMethods(entries); } -jobject fcitxRawConfigToJObject(JNIEnv *env, const fcitx::RawConfig &cfg) { - jobject obj = env->NewObject(GlobalRef->RawConfig, GlobalRef->RawConfigInit, - *JString(env, cfg.name()), - *JString(env, cfg.comment()), - *JString(env, cfg.value()), - nullptr); - if (!cfg.hasSubItems()) { - return obj; - } - auto array = JRef(env, env->NewObjectArray(static_cast(cfg.subItemsSize()), GlobalRef->RawConfig, nullptr)); - int i = 0; - for (const auto &item: cfg.subItems()) { - auto jItem = JRef(env, fcitxRawConfigToJObject(env, *cfg.get(item))); - env->SetObjectArrayElement(array, i++, jItem); - } - env->CallVoidMethod(obj, GlobalRef->RawConfigSetSubItems, *array); - return obj; -} - extern "C" JNIEXPORT jobject JNICALL Java_org_fcitx_fcitx5_android_core_Fcitx_getFcitxGlobalConfig(JNIEnv *env, jclass clazz) { @@ -890,28 +779,6 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_getFcitxInputMethodConfig(JNIEnv *env, return result ? fcitxRawConfigToJObject(env, *result) : nullptr; } -void jobjectFillRawConfig(JNIEnv *env, jobject jConfig, fcitx::RawConfig &config) { - auto subItems = JRef(env, env->GetObjectField(jConfig, GlobalRef->RawConfigSubItems)); - if (*subItems == nullptr) { - auto value = JRef(env, env->GetObjectField(jConfig, GlobalRef->RawConfigValue)); - config = CString(env, value); - } else { - int size = env->GetArrayLength(subItems); - for (int i = 0; i < size; i++) { - auto item = JRef(env, env->GetObjectArrayElement(subItems, i)); - auto name = JRef(env, env->GetObjectField(item, GlobalRef->RawConfigName)); - auto subConfig = config.get(CString(env, name), true); - jobjectFillRawConfig(env, item, *subConfig); - } - } -} - -fcitx::RawConfig jobjectToRawConfig(JNIEnv *env, jobject jConfig) { - fcitx::RawConfig config; - jobjectFillRawConfig(env, jConfig, config); - return config; -} - extern "C" JNIEXPORT void JNICALL Java_org_fcitx_fcitx5_android_core_Fcitx_setFcitxGlobalConfig(JNIEnv *env, jclass clazz, jobject config) { @@ -944,15 +811,6 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_setFcitxInputMethodConfig(JNIEnv *env, Fcitx::Instance().setInputMethodConfig(CString(env, im), rawConfig); } -jobjectArray stringVectorToJStringArray(JNIEnv *env, const std::vector &strings) { - jobjectArray array = env->NewObjectArray(static_cast(strings.size()), GlobalRef->String, nullptr); - int i = 0; - for (const auto &s: strings) { - env->SetObjectArrayElement(array, i++, JString(env, s)); - } - return array; -} - extern "C" JNIEXPORT jobjectArray JNICALL Java_org_fcitx_fcitx5_android_core_Fcitx_getFcitxAddons(JNIEnv *env, jclass clazz) { @@ -961,19 +819,7 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_getFcitxAddons(JNIEnv *env, jclass claz jobjectArray array = env->NewObjectArray(static_cast(addons.size()), GlobalRef->AddonInfo, nullptr); int i = 0; for (const auto addon: addons) { - const auto *info = addon.first; - auto obj = JRef(env, env->NewObject(GlobalRef->AddonInfo, GlobalRef->AddonInfoInit, - *JString(env, info->uniqueName()), - *JString(env, info->name().match()), - *JString(env, info->comment().match()), - static_cast(info->category()), - info->isConfigurable(), - addon.second, - info->isDefaultEnabled(), - info->onDemand(), - stringVectorToJStringArray(env, info->dependencies()), - stringVectorToJStringArray(env, info->optionalDependencies()) - )); + auto obj = JRef(env, fcitxAddonStatusToJObject(env, addon)); env->SetObjectArrayElement(array, i++, obj); } return array; @@ -1002,6 +848,7 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_setFcitxAddonState(JNIEnv *env, jclass extern "C" JNIEXPORT void JNICALL Java_org_fcitx_fcitx5_android_core_Fcitx_triggerQuickPhraseInput(JNIEnv *env, jclass clazz) { + RETURN_IF_NOT_RUNNING Fcitx::Instance().triggerQuickPhrase(); } diff --git a/app/src/main/cpp/object-conversion.h b/app/src/main/cpp/object-conversion.h new file mode 100644 index 000000000..a84840fde --- /dev/null +++ b/app/src/main/cpp/object-conversion.h @@ -0,0 +1,160 @@ +#ifndef FCITX5_ANDROID_OBJECT_CONVERSION_H +#define FCITX5_ANDROID_OBJECT_CONVERSION_H + +#include + +#include + +#include "jni-utils.h" +#include "helper-types.h" + +jobject fcitxInputMethodEntryToJObject(JNIEnv *env, const fcitx::InputMethodEntry *entry) { + return env->NewObject(GlobalRef->InputMethodEntry, GlobalRef->InputMethodEntryInit, + *JString(env, entry->uniqueName()), + *JString(env, entry->name()), + *JString(env, entry->icon()), + *JString(env, entry->nativeName()), + *JString(env, entry->label()), + *JString(env, entry->languageCode()), + entry->isConfigurable() + ); +} + +jobjectArray fcitxInputMethodEntriesToJObjectArray(JNIEnv *env, const std::vector &entries) { + jobjectArray array = env->NewObjectArray(static_cast(entries.size()), GlobalRef->InputMethodEntry, nullptr); + int i = 0; + for (const auto &entry: entries) { + auto obj = JRef(env, fcitxInputMethodEntryToJObject(env, entry)); + env->SetObjectArrayElement(array, i++, obj); + } + return array; +} + +jobject fcitxInputMethodStatusToJObject(JNIEnv *env, const InputMethodStatus &status) { + const auto entry = status.entry; + if (status.subMode.empty()) return fcitxInputMethodEntryToJObject(env, entry); + return env->NewObject(GlobalRef->InputMethodEntry, GlobalRef->InputMethodEntryInitWithSubMode, + *JString(env, entry->uniqueName()), + *JString(env, entry->name()), + *JString(env, entry->icon()), + *JString(env, entry->nativeName()), + *JString(env, entry->label()), + *JString(env, entry->languageCode()), + entry->isConfigurable(), + *JString(env, status.subMode), + *JString(env, status.subModeLabel), + *JString(env, status.subModeIcon) + ); +} + +jobject fcitxRawConfigToJObject(JNIEnv *env, const fcitx::RawConfig &cfg) { + jobject obj = env->NewObject(GlobalRef->RawConfig, GlobalRef->RawConfigInit, + *JString(env, cfg.name()), + *JString(env, cfg.comment()), + *JString(env, cfg.value()), + nullptr); + if (!cfg.hasSubItems()) { + return obj; + } + auto array = JRef(env, env->NewObjectArray(static_cast(cfg.subItemsSize()), GlobalRef->RawConfig, nullptr)); + int i = 0; + for (const auto &item: cfg.subItems()) { + auto jItem = JRef(env, fcitxRawConfigToJObject(env, *cfg.get(item))); + env->SetObjectArrayElement(array, i++, jItem); + } + env->CallVoidMethod(obj, GlobalRef->RawConfigSetSubItems, *array); + return obj; +} + +void jobjectFillRawConfig(JNIEnv *env, jobject jConfig, fcitx::RawConfig &config) { + auto subItems = JRef(env, env->GetObjectField(jConfig, GlobalRef->RawConfigSubItems)); + if (*subItems == nullptr) { + auto value = JRef(env, env->GetObjectField(jConfig, GlobalRef->RawConfigValue)); + config = CString(env, value); + } else { + int size = env->GetArrayLength(subItems); + for (int i = 0; i < size; i++) { + auto item = JRef(env, env->GetObjectArrayElement(subItems, i)); + auto name = JRef(env, env->GetObjectField(item, GlobalRef->RawConfigName)); + auto subConfig = config.get(CString(env, name), true); + jobjectFillRawConfig(env, item, *subConfig); + } + } +} + +fcitx::RawConfig jobjectToRawConfig(JNIEnv *env, jobject jConfig) { + fcitx::RawConfig config; + jobjectFillRawConfig(env, jConfig, config); + return config; +} + +jobjectArray stringVectorToJStringArray(JNIEnv *env, const std::vector &strings) { + jobjectArray array = env->NewObjectArray(static_cast(strings.size()), GlobalRef->String, nullptr); + int i = 0; + for (const auto &s: strings) { + env->SetObjectArrayElement(array, i++, JString(env, s)); + } + return array; +} + +jobject fcitxAddonStatusToJObject(JNIEnv *env, const AddonStatus &status) { + const auto info = status.info; + return env->NewObject(GlobalRef->AddonInfo, GlobalRef->AddonInfoInit, + *JString(env, info->uniqueName()), + *JString(env, info->name().match()), + *JString(env, info->comment().match()), + static_cast(info->category()), + info->isConfigurable(), + status.enabled, + info->isDefaultEnabled(), + info->onDemand(), + stringVectorToJStringArray(env, info->dependencies()), + stringVectorToJStringArray(env, info->optionalDependencies()) + ); +} + +jobject fcitxActionToJObject(JNIEnv *env, const ActionEntity &act) { + jobjectArray menu = nullptr; + if (act.menu) { + const int size = static_cast(act.menu->size()); + menu = env->NewObjectArray(size, GlobalRef->Action, nullptr); + for (int i = 0; i < size; i++) { + env->SetObjectArrayElement(menu, i, fcitxActionToJObject(env, act.menu->at(i))); + } + } + auto obj = env->NewObject(GlobalRef->Action, GlobalRef->ActionInit, + act.id, + act.isSeparator, + act.isCheckable, + act.isChecked, + *JString(env, act.name), + *JString(env, act.icon), + *JString(env, act.shortText), + *JString(env, act.longText), + menu + ); + if (menu) { + env->DeleteLocalRef(menu); + } + return obj; +} + +jobject fcitxTextToJObject(JNIEnv *env, const fcitx::Text &text) { + const int size = static_cast(text.size()); + auto str = JRef(env, env->NewObjectArray(size, GlobalRef->String, nullptr)); + auto fmt = JRef(env, env->NewIntArray(size)); + int flag = static_cast(fcitx::TextFormatFlag::NoFlag); + for (int i = 0; i < size; i++) { + env->SetObjectArrayElement(str, i, *JString(env, text.stringAt(i))); + flag = text.formatAt(i).toInteger(); + env->SetIntArrayRegion(fmt, i, 1, &flag); + } + auto obj = env->CallStaticObjectMethod(GlobalRef->FormattedText, GlobalRef->FormattedTextFromByteCursor, + *str, + *fmt, + text.cursor() + ); + return obj; +} + +#endif //FCITX5_ANDROID_OBJECT_CONVERSION_H From af967f746c1b54fbaef422b296c2bb4073c112fd Mon Sep 17 00:00:00 2001 From: Rocka Date: Sat, 18 Feb 2023 14:18:03 +0800 Subject: [PATCH 019/624] Refactor Preedit and InputPanel update events --- .../org/fcitx/fcitx5/android/FcitxTest.kt | 4 +- .../cpp/androidfrontend/androidfrontend.cpp | 62 ++++++++++++++----- .../cpp/androidfrontend/androidfrontend.h | 12 ++-- .../androidfrontend/androidfrontend_public.h | 8 +-- app/src/main/cpp/native-lib.cpp | 16 ++--- .../fcitx5/android/core/CapabilityFlag.kt | 5 +- .../org/fcitx/fcitx5/android/core/Fcitx.kt | 8 +-- .../org/fcitx/fcitx5/android/core/FcitxAPI.kt | 4 +- .../fcitx/fcitx5/android/core/FcitxEvent.kt | 48 +++++++------- .../fcitx5/android/core/FormattedText.kt | 3 + .../android/input/FcitxInputMethodService.kt | 13 ++-- .../fcitx/fcitx5/android/input/InputView.kt | 8 +-- .../android/input/bar/KawaiiBarComponent.kt | 41 ++++++------ .../input/broadcast/InputBroadcastReceiver.kt | 8 +-- .../input/broadcast/InputBroadcaster.kt | 12 ++-- .../window/BaseExpandedCandidateWindow.kt | 17 ++++- .../android/input/keyboard/BaseKeyboard.kt | 52 +++------------- .../input/keyboard/CommonKeyActionListener.kt | 7 +-- .../android/input/keyboard/KeyboardWindow.kt | 31 +++++++--- .../android/input/keyboard/NumberKeyboard.kt | 5 +- .../input/keyboard/ReturnKeyDrawable.kt | 33 ++++++++++ .../android/input/keyboard/TextKeyboard.kt | 16 ++--- .../android/input/preedit/PreeditComponent.kt | 17 +---- .../fcitx5/android/input/preedit/PreeditUi.kt | 34 +++++----- 24 files changed, 245 insertions(+), 219 deletions(-) create mode 100644 app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/ReturnKeyDrawable.kt diff --git a/app/src/androidTest/java/org/fcitx/fcitx5/android/FcitxTest.kt b/app/src/androidTest/java/org/fcitx/fcitx5/android/FcitxTest.kt index 6988562d0..2defe76f7 100644 --- a/app/src/androidTest/java/org/fcitx/fcitx5/android/FcitxTest.kt +++ b/app/src/androidTest/java/org/fcitx/fcitx5/android/FcitxTest.kt @@ -70,10 +70,10 @@ class FcitxTest { private suspend fun receiveFirstCommitString() = receiveFirst() - private suspend fun receiveFirstPreedit() = receiveFirst() + private suspend fun receiveFirstPreedit() = receiveFirst() private suspend fun receiveFirstInputPanelAux() = - receiveFirst() + receiveFirst() } diff --git a/app/src/main/cpp/androidfrontend/androidfrontend.cpp b/app/src/main/cpp/androidfrontend/androidfrontend.cpp index 4cfc31bf4..654af2603 100644 --- a/app/src/main/cpp/androidfrontend/androidfrontend.cpp +++ b/app/src/main/cpp/androidfrontend/androidfrontend.cpp @@ -42,14 +42,25 @@ class AndroidInputContext : public InputContext { } void updatePreeditImpl() override { - // if PreeditInApplication is disabled, this function is not called - // moved to `updateClientSideUIImpl` + checkClientPreeditUpdate(); } - void updateClientSideUIImpl() override { + void updateInputPanel() { + // Normally input method engine should check CapabilityFlag::Preedit before update clientPreedit, + // and fcitx5 won't trigger UpdatePreeditEvent when that flag is not present, in which case + // InputContext::updatePreeditImpl() won't be called. + // However on Android, androidkeyboard uses clientPreedit unconditionally in order to provide + // a more integrated experience, so we need to check clientPreedit update manually even if + // clientPreedit is not enabled. + if (!isPreeditEnabled()) { + checkClientPreeditUpdate(); + } InputPanel &ip = inputPanel(); - frontend_->updatePreedit(filterText(ip.preedit()), filterText(ip.clientPreedit())); - frontend_->updateInputPanelAux(filterText(ip.auxUp()), filterText(ip.auxDown())); + frontend_->updateInputPanel( + filterText(ip.preedit()), + filterText(ip.auxUp()), + filterText(ip.auxDown()) + ); std::vector candidates; const auto &list = ip.candidateList(); if (list) { @@ -95,11 +106,22 @@ class AndroidInputContext : public InputContext { AndroidFrontend *frontend_; int uid_; - Text filterText(const Text &orig) { + bool clientPreeditEmpty_ = true; + + void checkClientPreeditUpdate() { + const auto &clientPreedit = filterText(inputPanel().clientPreedit()); + bool empty = clientPreedit.empty(); + // skip update if new and old clientPreedit are both empty + if (empty && clientPreeditEmpty_) return; + clientPreeditEmpty_ = empty; + frontend_->updateClientPreedit(clientPreedit); + } + + inline Text filterText(const Text &orig) { return frontend_->instance()->outputFilter(this, orig); } - std::string filterString(const Text &orig) { + inline std::string filterString(const Text &orig) { return filterText(orig).toString(); } }; @@ -122,8 +144,16 @@ AndroidFrontend::AndroidFrontend(Instance *instance) EventWatcherPhase::Default, [this](Event &event) { auto &e = static_cast(event); - if (e.component() == UserInterfaceComponent::StatusArea) { - handleStatusAreaUpdate(); + switch (e.component()) { + case UserInterfaceComponent::InputPanel: { + auto *ic = dynamic_cast(activeIC_); + if (ic) ic->updateInputPanel(); + break; + } + case UserInterfaceComponent::StatusArea: { + handleStatusAreaUpdate(); + break; + } } } )); @@ -153,12 +183,12 @@ void AndroidFrontend::updateCandidateList(const std::vector &candid candidateListCallback(candidates); } -void AndroidFrontend::updatePreedit(const Text &preedit, const Text &clientPreedit) { - preeditCallback(preedit, clientPreedit); +void AndroidFrontend::updateClientPreedit(const Text &clientPreedit) { + preeditCallback(clientPreedit); } -void AndroidFrontend::updateInputPanelAux(const Text &auxUp, const Text &auxDown) { - inputPanelAuxCallback(auxUp, auxDown); +void AndroidFrontend::updateInputPanel(const Text &preedit, const Text &auxUp, const Text &auxDown) { + inputPanelAuxCallback(preedit, auxUp, auxDown); } void AndroidFrontend::releaseInputContext(const int uid) { @@ -238,11 +268,11 @@ void AndroidFrontend::setCommitStringCallback(const CommitStringCallback &callba commitStringCallback = callback; } -void AndroidFrontend::setPreeditCallback(const PreeditCallback &callback) { +void AndroidFrontend::setPreeditCallback(const ClientPreeditCallback &callback) { preeditCallback = callback; } -void AndroidFrontend::setInputPanelAuxCallback(const InputPanelAuxCallback &callback) { +void AndroidFrontend::setInputPanelAuxCallback(const InputPanelCallback &callback) { inputPanelAuxCallback = callback; } @@ -265,7 +295,7 @@ void AndroidFrontend::handleStatusAreaUpdate() { statusAreaUpdateCallback(); statusAreaUpdated_ = false; statusAreaDefer_ = nullptr; - return false; + return true; }); } diff --git a/app/src/main/cpp/androidfrontend/androidfrontend.h b/app/src/main/cpp/androidfrontend/androidfrontend.h index 147b55572..2c1bd1406 100644 --- a/app/src/main/cpp/androidfrontend/androidfrontend.h +++ b/app/src/main/cpp/androidfrontend/androidfrontend.h @@ -19,8 +19,8 @@ class AndroidFrontend : public AddonInstance { void updateCandidateList(const std::vector &candidates); void commitString(const std::string &str); - void updatePreedit(const Text &preedit, const Text &clientPreedit); - void updateInputPanelAux(const Text &auxUp, const Text &auxDown); + void updateClientPreedit(const Text &clientPreedit); + void updateInputPanel(const Text &preedit, const Text &auxUp, const Text &auxDown); void releaseInputContext(const int uid); void keyEvent(const Key &key, bool isRelease, const int timestamp); @@ -36,8 +36,8 @@ class AndroidFrontend : public AddonInstance { void setCapabilityFlags(uint64_t flag); void setCandidateListCallback(const CandidateListCallback &callback); void setCommitStringCallback(const CommitStringCallback &callback); - void setPreeditCallback(const PreeditCallback &callback); - void setInputPanelAuxCallback(const InputPanelAuxCallback &callback); + void setPreeditCallback(const ClientPreeditCallback &callback); + void setInputPanelAuxCallback(const InputPanelCallback &callback); void setKeyEventCallback(const KeyEventCallback &callback); void setInputMethodChangeCallback(const InputMethodChangeCallback &callback); void setStatusAreaUpdateCallback(const StatusAreaUpdateCallback &callback); @@ -73,8 +73,8 @@ class AndroidFrontend : public AddonInstance { CandidateListCallback candidateListCallback = [](const std::vector &) {}; CommitStringCallback commitStringCallback = [](const std::string &) {}; - PreeditCallback preeditCallback = [](const Text &, const Text &) {}; - InputPanelAuxCallback inputPanelAuxCallback = [](const fcitx::Text &, const fcitx::Text &) {}; + ClientPreeditCallback preeditCallback = [](const Text &) {}; + InputPanelCallback inputPanelAuxCallback = [](const fcitx::Text &, const fcitx::Text &, const Text &) {}; KeyEventCallback keyEventCallback = [](const int, const uint32_t, const uint32_t, const bool, const int) {}; InputMethodChangeCallback imChangeCallback = [] {}; StatusAreaUpdateCallback statusAreaUpdateCallback = [] {}; diff --git a/app/src/main/cpp/androidfrontend/androidfrontend_public.h b/app/src/main/cpp/androidfrontend/androidfrontend_public.h index 3ca994934..45b8d5550 100644 --- a/app/src/main/cpp/androidfrontend/androidfrontend_public.h +++ b/app/src/main/cpp/androidfrontend/androidfrontend_public.h @@ -6,8 +6,8 @@ typedef std::function &)> CandidateListCallback; typedef std::function CommitStringCallback; -typedef std::function PreeditCallback; -typedef std::function InputPanelAuxCallback; +typedef std::function ClientPreeditCallback; +typedef std::function InputPanelCallback; typedef std::function KeyEventCallback; typedef std::function InputMethodChangeCallback; typedef std::function StatusAreaUpdateCallback; @@ -49,10 +49,10 @@ FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setCommitStringCallback, void(const CommitStringCallback &)) FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setPreeditCallback, - void(const PreeditCallback &)) + void(const ClientPreeditCallback &)) FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setInputPanelAuxCallback, - void(const InputPanelAuxCallback &)) + void(const InputPanelCallback &)) FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setKeyEventCallback, void(const KeyEventCallback &)) diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp index 2fa1aa12b..49df7b4ac 100644 --- a/app/src/main/cpp/native-lib.cpp +++ b/app/src/main/cpp/native-lib.cpp @@ -542,18 +542,18 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(JNIEnv *env, jclass clazz, env->SetObjectArrayElement(vararg, 0, JString(env, str)); env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 1, *vararg); }; - auto preeditCallback = [](const fcitx::Text &preedit, const fcitx::Text &clientPreedit) { + auto preeditCallback = [](const fcitx::Text &clientPreedit) { auto env = GlobalRef->AttachEnv(); - auto vararg = JRef(env, env->NewObjectArray(2, GlobalRef->FormattedText, nullptr)); - env->SetObjectArrayElement(vararg, 0, fcitxTextToJObject(env, preedit)); - env->SetObjectArrayElement(vararg, 1, fcitxTextToJObject(env, clientPreedit)); + auto vararg = JRef(env, env->NewObjectArray(1, GlobalRef->FormattedText, nullptr)); + env->SetObjectArrayElement(vararg, 0, fcitxTextToJObject(env, clientPreedit)); env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 2, *vararg); }; - auto inputPanelAuxCallback = [](const fcitx::Text &auxUp, const fcitx::Text &auxDown) { + auto inputPanelAuxCallback = [](const fcitx::Text &preedit, const fcitx::Text &auxUp, const fcitx::Text &auxDown) { auto env = GlobalRef->AttachEnv(); - auto vararg = JRef(env, env->NewObjectArray(2, GlobalRef->FormattedText, nullptr)); - env->SetObjectArrayElement(vararg, 0, fcitxTextToJObject(env, auxUp)); - env->SetObjectArrayElement(vararg, 1, fcitxTextToJObject(env, auxDown)); + auto vararg = JRef(env, env->NewObjectArray(3, GlobalRef->FormattedText, nullptr)); + env->SetObjectArrayElement(vararg, 0, fcitxTextToJObject(env, preedit)); + env->SetObjectArrayElement(vararg, 1, fcitxTextToJObject(env, auxUp)); + env->SetObjectArrayElement(vararg, 2, fcitxTextToJObject(env, auxDown)); env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 3, *vararg); }; auto readyCallback = []() { diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/CapabilityFlag.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/CapabilityFlag.kt index b68596713..6f58477a9 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/core/CapabilityFlag.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/core/CapabilityFlag.kt @@ -74,9 +74,8 @@ value class CapabilityFlags constructor(val flags: ULong) { arr.fold(CapabilityFlag.NoFlag.flag) { acc, it -> acc or it.flag } val DefaultFlags = CapabilityFlags( - CapabilityFlag.Preedit.flag or - CapabilityFlag.ClientUnfocusCommit.flag or - CapabilityFlag.ClientSideInputPanel.flag + CapabilityFlag.Preedit, + CapabilityFlag.ClientUnfocusCommit ) fun fromEditorInfo(info: EditorInfo): CapabilityFlags { diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/Fcitx.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/Fcitx.kt index ccb981292..eacb35142 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/core/Fcitx.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/core/Fcitx.kt @@ -36,10 +36,10 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner { override var statusAreaActionsCached: Array = arrayOf() private set - override var preeditCached = FcitxEvent.PreeditEvent.Data() + override var clientPreeditCached = FormattedText.Empty private set - override var panelAuxCached = FcitxEvent.InputPanelAuxEvent.Data() + override var inputPanelCached = FcitxEvent.InputPanelEvent.Data() private set // the computation is delayed to the first call of [getAddonReverseDependencies] @@ -403,8 +403,8 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner { is FcitxEvent.ReadyEvent -> lifecycleRegistry.postEvent(FcitxLifecycle.Event.ON_READY) is FcitxEvent.IMChangeEvent -> inputMethodEntryCached = it.data is FcitxEvent.StatusAreaEvent -> statusAreaActionsCached = it.data - is FcitxEvent.PreeditEvent -> preeditCached = it.data - is FcitxEvent.InputPanelAuxEvent -> panelAuxCached = it.data + is FcitxEvent.ClientPreeditEvent -> clientPreeditCached = it.data + is FcitxEvent.InputPanelEvent -> inputPanelCached = it.data else -> {} } }.launchIn(lifeCycleScope) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxAPI.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxAPI.kt index 2345f0a2e..abf15e8be 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxAPI.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxAPI.kt @@ -27,9 +27,9 @@ interface FcitxAPI { val statusAreaActionsCached: Array - val preeditCached: FcitxEvent.PreeditEvent.Data + val clientPreeditCached: FormattedText - val panelAuxCached: FcitxEvent.InputPanelAuxEvent.Data + val inputPanelCached: FcitxEvent.InputPanelEvent.Data fun getAddonReverseDependencies(addon: String): List> diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxEvent.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxEvent.kt index d4056fe3f..a7073dcb7 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxEvent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxEvent.kt @@ -24,9 +24,7 @@ sealed class FcitxEvent(open val data: T) { return data.contentHashCode() } - override fun toString(): String = "CandidateListEvent(data=[${ - data.take(5).joinToString() - }${if (data.size > 5) ", ..." else ""}])" + override fun toString(): String = "CandidateListEvent(${data.joinToString(limit = 5)})" } data class CommitStringEvent(override val data: String) : @@ -35,27 +33,25 @@ sealed class FcitxEvent(open val data: T) { get() = EventType.Commit } - data class PreeditEvent(override val data: Data) : - FcitxEvent(data) { + data class ClientPreeditEvent(override val data: FormattedText) : + FcitxEvent(data) { override val eventType: EventType - get() = EventType.Preedit + get() = EventType.ClientPreedit - data class Data(val preedit: FormattedText, val clientPreedit: FormattedText) { - constructor() : this(FormattedText(), FormattedText()) - } - - override fun toString(): String { - return "PreeditEvent(preedit=[${data.preedit}, ${data.preedit.cursor}], clientPreedit=[${data.clientPreedit}, ${data.clientPreedit.cursor}])" - } + override fun toString(): String = "ClientPreeditEvent('$data', ${data.cursor})" } - data class InputPanelAuxEvent(override val data: Data) : - FcitxEvent(data) { + data class InputPanelEvent(override val data: Data) : + FcitxEvent(data) { override val eventType: EventType - get() = EventType.Aux + get() = EventType.InputPanel - data class Data(val auxUp: FormattedText, val auxDown: FormattedText) { - constructor() : this(FormattedText(), FormattedText()) + data class Data( + val preedit: FormattedText, + val auxUp: FormattedText, + val auxDown: FormattedText + ) { + constructor() : this(FormattedText.Empty, FormattedText.Empty, FormattedText.Empty) } } @@ -131,8 +127,8 @@ sealed class FcitxEvent(open val data: T) { enum class EventType { Candidate, Commit, - Preedit, - Aux, + ClientPreedit, + InputPanel, Ready, Key, Change, @@ -149,11 +145,13 @@ sealed class FcitxEvent(open val data: T) { when (Types[type]) { EventType.Candidate -> CandidateListEvent(params as Array) EventType.Commit -> CommitStringEvent(params[0] as String) - EventType.Preedit -> PreeditEvent( - PreeditEvent.Data(params[0] as FormattedText, params[1] as FormattedText) - ) - EventType.Aux -> InputPanelAuxEvent( - InputPanelAuxEvent.Data(params[0] as FormattedText, params[1] as FormattedText) + EventType.ClientPreedit -> ClientPreeditEvent(params[0] as FormattedText) + EventType.InputPanel -> InputPanelEvent( + InputPanelEvent.Data( + params[0] as FormattedText, + params[1] as FormattedText, + params[2] as FormattedText + ) ) EventType.Ready -> ReadyEvent() EventType.Key -> KeyEvent( diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/FormattedText.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/FormattedText.kt index 437eff84a..90f662675 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/core/FormattedText.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/core/FormattedText.kt @@ -38,6 +38,9 @@ data class FormattedText( constructor() : this(arrayOf(), intArrayOf(), -1) companion object { + @JvmStatic + val Empty = FormattedText() + @JvmStatic @Suppress("UNUSED") // called from JNI fun fromByteCursor( diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt index a76d8edbd..0a796ac27 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt @@ -49,7 +49,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() { val selection = CursorTracker() val composing = CursorRange() - private var composingText = EmptyFormattedText + private var composingText = FormattedText.Empty private var cursorUpdateIndex: Int = 0 @@ -130,8 +130,8 @@ class FcitxInputMethodService : LifecycleInputMethodService() { } } - is FcitxEvent.PreeditEvent -> { - updateComposingText(event.data.clientPreedit) + is FcitxEvent.ClientPreeditEvent -> { + updateComposingText(event.data) } else -> { @@ -199,7 +199,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() { selection.predict(start + text.length) // clear composing range composing.clear() - composingText = EmptyFormattedText + composingText = FormattedText.Empty inputConnection?.commitText(text, 1) } @@ -384,7 +384,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() { // right cursor position, try to workaround this would simply introduce more bugs. selection.resetTo(attribute.initialSelStart, attribute.initialSelEnd) composing.clear() - composingText = EmptyFormattedText + composingText = FormattedText.Empty val flags = CapabilityFlags.fromEditorInfo(attribute) editorInfo = attribute capabilityFlags = flags @@ -470,7 +470,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() { } else { Timber.d("handleCursorUpdate: focus out/in") composing.clear() - composingText = EmptyFormattedText + composingText = FormattedText.Empty // cursor outside composing range, finish composing as-is inputConnection?.finishComposingText() // `fcitx.reset()` here would commit preedit after new cursor position @@ -582,7 +582,6 @@ class FcitxInputMethodService : LifecycleInputMethodService() { companion object { val EmptyEditorInfo = EditorInfo() - val EmptyFormattedText = FormattedText() const val DeleteSurroundingFlag = "org.fcitx.fcitx5.android.DELETE_SURROUNDING" } } \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt index 7bb1e8797..9e6585a11 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt @@ -288,12 +288,12 @@ class InputView( broadcaster.onCandidateUpdate(it.data) } - is FcitxEvent.PreeditEvent -> { - broadcaster.onPreeditUpdate(it.data) + is FcitxEvent.ClientPreeditEvent -> { + broadcaster.onClientPreeditUpdate(it.data) } - is FcitxEvent.InputPanelAuxEvent -> { - broadcaster.onInputPanelAuxUpdate(it.data) + is FcitxEvent.InputPanelEvent -> { + broadcaster.onInputPanelUpdate(it.data) } is FcitxEvent.IMChangeEvent -> { diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt index 5ddbab313..dcd740a48 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt @@ -15,37 +15,23 @@ import org.fcitx.fcitx5.android.R import org.fcitx.fcitx5.android.core.CapabilityFlag import org.fcitx.fcitx5.android.core.CapabilityFlags import org.fcitx.fcitx5.android.core.FcitxEvent +import org.fcitx.fcitx5.android.core.FormattedText import org.fcitx.fcitx5.android.data.clipboard.ClipboardManager import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.data.prefs.ManagedPreference import org.fcitx.fcitx5.android.data.theme.ThemeManager -import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.State.ClickToAttachWindow -import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.State.ClickToDetachWindow -import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.State.Hidden +import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.State.* import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.BooleanKey.ClipboardEmpty -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.ClipboardUpdated -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.KawaiiBarShown -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.MenuButtonClicked -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.Pasted -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.Timeout -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.BooleanKey.CandidateEmpty -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.BooleanKey.CapFlagsPassword -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.BooleanKey.PreeditEmpty -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.CandidatesUpdated -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.CapFlagsUpdated -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.ExtendedWindowAttached -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.PreeditUpdated -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.WindowDetached +import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.* +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.BooleanKey.* +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.* import org.fcitx.fcitx5.android.input.broadcast.InputBroadcastReceiver import org.fcitx.fcitx5.android.input.candidates.HorizontalCandidateComponent import org.fcitx.fcitx5.android.input.candidates.expanded.ExpandedCandidateStyle import org.fcitx.fcitx5.android.input.candidates.expanded.window.FlexboxExpandedCandidateWindow import org.fcitx.fcitx5.android.input.candidates.expanded.window.GridExpandedCandidateWindow import org.fcitx.fcitx5.android.input.clipboard.ClipboardWindow -import org.fcitx.fcitx5.android.input.dependency.UniqueViewComponent -import org.fcitx.fcitx5.android.input.dependency.context -import org.fcitx.fcitx5.android.input.dependency.inputMethodService -import org.fcitx.fcitx5.android.input.dependency.theme +import org.fcitx.fcitx5.android.input.dependency.* import org.fcitx.fcitx5.android.input.editing.TextEditingWindow import org.fcitx.fcitx5.android.input.keyboard.CommonKeyActionListener import org.fcitx.fcitx5.android.input.keyboard.KeyboardWindow @@ -67,6 +53,7 @@ import timber.log.Timber class KawaiiBarComponent : UniqueViewComponent(), InputBroadcastReceiver { + private val fcitx by manager.fcitx() private val context by manager.context() private val theme by manager.theme() private val windowManager: InputWindowManager by manager.must() @@ -309,13 +296,23 @@ class KawaiiBarComponent : UniqueViewComponent( barStateMachine.push(CapFlagsUpdated) } - override fun onPreeditUpdate(data: FcitxEvent.PreeditEvent.Data) { + private fun pushPreeditUpdateEvent(preedit: FormattedText, clientPreedit: FormattedText) { barStateMachine.push( PreeditUpdated, - PreeditEmpty to (data.preedit.isEmpty() && data.clientPreedit.isEmpty()) + PreeditEmpty to (preedit.isEmpty() && clientPreedit.isEmpty()) ) } + override fun onClientPreeditUpdate(data: FormattedText) { + val preedit = fcitx.runImmediately { inputPanelCached.preedit } + pushPreeditUpdateEvent(preedit, data) + } + + override fun onInputPanelUpdate(data: FcitxEvent.InputPanelEvent.Data) { + val clientPreedit = fcitx.runImmediately { clientPreeditCached } + pushPreeditUpdateEvent(data.preedit, clientPreedit) + } + override fun onCandidateUpdate(data: Array) { barStateMachine.push(CandidatesUpdated, CandidateEmpty to data.isEmpty()) } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcastReceiver.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcastReceiver.kt index 907633525..d6736f5a2 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcastReceiver.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcastReceiver.kt @@ -3,8 +3,8 @@ package org.fcitx.fcitx5.android.input.broadcast import android.view.inputmethod.EditorInfo import org.fcitx.fcitx5.android.core.Action import org.fcitx.fcitx5.android.core.CapabilityFlags -import org.fcitx.fcitx5.android.core.FcitxEvent.InputPanelAuxEvent -import org.fcitx.fcitx5.android.core.FcitxEvent.PreeditEvent +import org.fcitx.fcitx5.android.core.FcitxEvent.InputPanelEvent +import org.fcitx.fcitx5.android.core.FormattedText import org.fcitx.fcitx5.android.core.InputMethodEntry import org.fcitx.fcitx5.android.input.wm.InputWindow import org.mechdancer.dependency.DynamicScope @@ -15,9 +15,9 @@ interface InputBroadcastReceiver { fun onStartInput(info: EditorInfo, capFlags: CapabilityFlags) {} - fun onPreeditUpdate(data: PreeditEvent.Data) {} + fun onClientPreeditUpdate(data: FormattedText) {} - fun onInputPanelAuxUpdate(data: InputPanelAuxEvent.Data) {} + fun onInputPanelUpdate(data: InputPanelEvent.Data) {} fun onImeUpdate(ime: InputMethodEntry) {} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcaster.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcaster.kt index 4e1793a9f..3178cb0ae 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcaster.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcaster.kt @@ -3,8 +3,8 @@ package org.fcitx.fcitx5.android.input.broadcast import android.view.inputmethod.EditorInfo import org.fcitx.fcitx5.android.core.Action import org.fcitx.fcitx5.android.core.CapabilityFlags -import org.fcitx.fcitx5.android.core.FcitxEvent.InputPanelAuxEvent -import org.fcitx.fcitx5.android.core.FcitxEvent.PreeditEvent +import org.fcitx.fcitx5.android.core.FcitxEvent.InputPanelEvent +import org.fcitx.fcitx5.android.core.FormattedText import org.fcitx.fcitx5.android.core.InputMethodEntry import org.fcitx.fcitx5.android.input.wm.InputWindow import org.mechdancer.dependency.Dependent @@ -33,12 +33,12 @@ class InputBroadcaster : UniqueComponent(), Dependent, InputBr } } - override fun onPreeditUpdate(data: PreeditEvent.Data) { - receivers.forEach { it.onPreeditUpdate(data) } + override fun onClientPreeditUpdate(data: FormattedText) { + receivers.forEach { it.onClientPreeditUpdate(data) } } - override fun onInputPanelAuxUpdate(data: InputPanelAuxEvent.Data) { - receivers.forEach { it.onInputPanelAuxUpdate(data) } + override fun onInputPanelUpdate(data: InputPanelEvent.Data) { + receivers.forEach { it.onInputPanelUpdate(data) } } override fun onStartInput(info: EditorInfo, capFlags: CapabilityFlags) { diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/BaseExpandedCandidateWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/BaseExpandedCandidateWindow.kt index 3c63e949f..1dfd6c0ee 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/BaseExpandedCandidateWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/BaseExpandedCandidateWindow.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.fcitx.fcitx5.android.core.FcitxEvent +import org.fcitx.fcitx5.android.core.FormattedText import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.BooleanKey.CandidatesEmpty import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.TransitionEvent.ExpandedCandidatesAttached @@ -19,6 +20,7 @@ import org.fcitx.fcitx5.android.input.candidates.CandidateViewBuilder import org.fcitx.fcitx5.android.input.candidates.HorizontalCandidateComponent import org.fcitx.fcitx5.android.input.candidates.adapter.BaseCandidateViewAdapter import org.fcitx.fcitx5.android.input.candidates.expanded.ExpandedCandidateLayout +import org.fcitx.fcitx5.android.input.dependency.fcitx import org.fcitx.fcitx5.android.input.dependency.theme import org.fcitx.fcitx5.android.input.keyboard.CommonKeyActionListener import org.fcitx.fcitx5.android.input.keyboard.KeyAction @@ -33,6 +35,7 @@ abstract class BaseExpandedCandidateWindow> : protected val builder: CandidateViewBuilder by manager.must() protected val theme by manager.theme() + private val fcitx by manager.fcitx() private val commonKeyActionListener: CommonKeyActionListener by manager.must() private val bar: KawaiiBarComponent by manager.must() private val horizontalCandidate: HorizontalCandidateComponent by manager.must() @@ -98,9 +101,19 @@ abstract class BaseExpandedCandidateWindow> : candidateLayout.embeddedKeyboard.keyActionListener = null } - override fun onPreeditUpdate(data: FcitxEvent.PreeditEvent.Data) { - if (data.preedit.isEmpty() && data.clientPreedit.isEmpty()) { + private fun evaluateShouldDetach(preedit: FormattedText, clientPreedit: FormattedText) { + if (preedit.isEmpty() && clientPreedit.isEmpty()) { windowManager.attachWindow(KeyboardWindow) } } + + override fun onClientPreeditUpdate(data: FormattedText) { + val preedit = fcitx.runImmediately { inputPanelCached.preedit } + evaluateShouldDetach(preedit, data) + } + + override fun onInputPanelUpdate(data: FcitxEvent.InputPanelEvent.Data) { + val clientPreedit = fcitx.runImmediately { clientPreeditCached } + evaluateShouldDetach(data.preedit, clientPreedit) + } } \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/BaseKeyboard.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/BaseKeyboard.kt index c38f21f8f..28071f453 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/BaseKeyboard.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/BaseKeyboard.kt @@ -4,24 +4,23 @@ import android.content.Context import android.graphics.Rect import android.view.MotionEvent import android.view.View -import android.view.inputmethod.EditorInfo import androidx.annotation.CallSuper import androidx.annotation.DrawableRes import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.children -import org.fcitx.fcitx5.android.R -import org.fcitx.fcitx5.android.core.* +import org.fcitx.fcitx5.android.core.FcitxKeyMapping +import org.fcitx.fcitx5.android.core.InputMethodEntry +import org.fcitx.fcitx5.android.core.KeyStates +import org.fcitx.fcitx5.android.core.KeySym import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.data.theme.Theme import org.fcitx.fcitx5.android.input.keyboard.CustomGestureView.GestureType import org.fcitx.fcitx5.android.input.keyboard.CustomGestureView.OnGestureListener import org.fcitx.fcitx5.android.input.popup.PopupAction import org.fcitx.fcitx5.android.input.popup.PopupActionListener -import splitties.bitflags.hasFlag import splitties.dimensions.dp import splitties.views.dsl.constraintlayout.* import splitties.views.dsl.core.add -import splitties.views.imageResource import timber.log.Timber import kotlin.math.absoluteValue import kotlin.math.roundToInt @@ -294,39 +293,6 @@ abstract class BaseKeyboard( } } - @DrawableRes - protected fun drawableForReturn(info: EditorInfo): Int { - if (info.imeOptions.hasFlag(EditorInfo.IME_FLAG_NO_ENTER_ACTION)) { - return R.drawable.ic_baseline_keyboard_return_24 - } - return when (info.imeOptions and EditorInfo.IME_MASK_ACTION) { - EditorInfo.IME_ACTION_GO -> R.drawable.ic_baseline_arrow_forward_24 - EditorInfo.IME_ACTION_SEARCH -> R.drawable.ic_baseline_search_24 - EditorInfo.IME_ACTION_SEND -> R.drawable.ic_baseline_send_24 - EditorInfo.IME_ACTION_NEXT -> R.drawable.ic_baseline_keyboard_tab_24 - EditorInfo.IME_ACTION_DONE -> R.drawable.ic_baseline_done_24 - EditorInfo.IME_ACTION_PREVIOUS -> R.drawable.ic_baseline_keyboard_tab_reverse_24 - else -> R.drawable.ic_baseline_keyboard_return_24 - } - } - - // FIXME: need some new API to know exactly whether next enter would be captured by fcitx - protected fun updateReturnButton( - `return`: ImageKeyView, - info: EditorInfo, - preedit: FcitxEvent.PreeditEvent.Data - // aux: FcitxEvent.InputPanelAuxEvent.Data - ) { - val hasPreedit = preedit.preedit.isNotEmpty() || preedit.clientPreedit.isNotEmpty() - // `auxUp` is not empty when switching input methods, ignore it to reduce flicker - // || aux.auxUp.isNotEmpty() - `return`.img.imageResource = if (hasPreedit) { - R.drawable.ic_baseline_keyboard_return_24 - } else { - drawableForReturn(info) - } - } - override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { val (x, y) = intArrayOf(0, 0).also { getLocationInWindow(it) } bounds.set(x, y, x + width, y + height) @@ -471,15 +437,11 @@ abstract class BaseKeyboard( return true } - open fun onAttach(info: EditorInfo) { - // do nothing by default - } - - open fun onEditorInfoChange(info: EditorInfo, capFlags: CapabilityFlags) { + open fun onAttach() { // do nothing by default } - open fun onPreeditChange(info: EditorInfo, data: FcitxEvent.PreeditEvent.Data) { + open fun onReturnDrawableUpdate(@DrawableRes returnDrawable: Int) { // do nothing by default } @@ -487,7 +449,7 @@ abstract class BaseKeyboard( // do nothing by default } - open fun onInputMethodChange(ime: InputMethodEntry) { + open fun onInputMethodUpdate(ime: InputMethodEntry) { // do nothing by default } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/CommonKeyActionListener.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/CommonKeyActionListener.kt index 434b507aa..e5cf8575b 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/CommonKeyActionListener.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/CommonKeyActionListener.kt @@ -41,7 +41,7 @@ class CommonKeyActionListener : private var backspaceSwipeState = Stopped private suspend fun FcitxAPI.commitAndReset() { - if (preeditCached.run { preedit.isEmpty() && clientPreedit.isEmpty() }) { + if (clientPreeditCached.isEmpty() && inputPanelCached.preedit.isEmpty()) { // preedit is empty, there can be prediction candidates reset() } else if (inputMethodEntryCached.uniqueName.let { it == "keyboard-us" || it == "unikey" }) { @@ -86,15 +86,14 @@ class CommonKeyActionListener : ) } is MoveSelectionAction -> when (backspaceSwipeState) { - Stopped -> backspaceSwipeState = it.preeditCached.let { p -> - if (p.preedit.isEmpty() && p.clientPreedit.isEmpty()) { + Stopped -> backspaceSwipeState = + if (it.clientPreeditCached.isEmpty() && it.inputPanelCached.preedit.isEmpty()) { // update state to `Selection` and apply first offset service.applySelectionOffset(action.start, action.end) Selection } else { Reset } - } Selection -> { service.applySelectionOffset(action.start, action.end) } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt index 875efc400..fb4456b99 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt @@ -5,14 +5,15 @@ import android.view.Gravity import android.view.View import android.view.inputmethod.EditorInfo import android.widget.FrameLayout +import androidx.annotation.DrawableRes import androidx.core.content.ContextCompat import androidx.transition.Slide import org.fcitx.fcitx5.android.R import org.fcitx.fcitx5.android.core.CapabilityFlags import org.fcitx.fcitx5.android.core.FcitxEvent +import org.fcitx.fcitx5.android.core.FormattedText import org.fcitx.fcitx5.android.core.InputMethodEntry import org.fcitx.fcitx5.android.data.prefs.AppPrefs -import org.fcitx.fcitx5.android.input.FcitxInputMethodService import org.fcitx.fcitx5.android.input.bar.KawaiiBarComponent import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.KeyboardSwitchedOutNumber import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.KeyboardSwitchedToNumber @@ -35,7 +36,7 @@ import splitties.views.dsl.core.matchParent class KeyboardWindow : InputWindow.SimpleInputWindow(), EssentialWindow, InputBroadcastReceiver { - private val service: FcitxInputMethodService by manager.inputMethodService() + private val service by manager.inputMethodService() private val fcitx by manager.fcitx() private val theme by manager.theme() private val commonKeyActionListener: CommonKeyActionListener by manager.must() @@ -86,6 +87,16 @@ class KeyboardWindow : InputWindow.SimpleInputWindow(), Essentia popup.listener } + @DrawableRes + private var returnDrawable = R.drawable.ic_baseline_keyboard_return_24 + + private fun updateReturnDrawable() { + val newDrawable = ReturnKeyDrawable.from(fcitx, service.editorInfo) + if (returnDrawable == newDrawable) return + returnDrawable = newDrawable + currentKeyboard?.onReturnDrawableUpdate(returnDrawable) + } + // This will be called EXACTLY ONCE override fun onCreateView(): View { keyboardView = context.frameLayout(R.id.keyboard_view) @@ -108,8 +119,9 @@ class KeyboardWindow : InputWindow.SimpleInputWindow(), Essentia it.keyActionListener = keyActionListener it.popupActionListener = popupActionListener keyboardView.apply { add(it, lParams(matchParent, matchParent)) } - it.onAttach(service.editorInfo) - it.onInputMethodChange(fcitx.runImmediately { inputMethodEntryCached }) + it.onAttach() + it.onReturnDrawableUpdate(returnDrawable) + it.onInputMethodUpdate(fcitx.runImmediately { inputMethodEntryCached }) } } @@ -141,15 +153,18 @@ class KeyboardWindow : InputWindow.SimpleInputWindow(), Essentia else -> TextKeyboard.Name } switchLayout(targetLayout, remember = false) - currentKeyboard?.onEditorInfoChange(info, capFlags) } override fun onImeUpdate(ime: InputMethodEntry) { - currentKeyboard?.onInputMethodChange(ime) + currentKeyboard?.onInputMethodUpdate(ime) + } + + override fun onClientPreeditUpdate(data: FormattedText) { + updateReturnDrawable() } - override fun onPreeditUpdate(data: FcitxEvent.PreeditEvent.Data) { - currentKeyboard?.onPreeditChange(service.editorInfo, data) + override fun onInputPanelUpdate(data: FcitxEvent.InputPanelEvent.Data) { + updateReturnDrawable() } override fun onPunctuationUpdate(mapping: Map) { diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/NumberKeyboard.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/NumberKeyboard.kt index fc46cdf88..468910ea9 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/NumberKeyboard.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/NumberKeyboard.kt @@ -2,7 +2,6 @@ package org.fcitx.fcitx5.android.input.keyboard import android.annotation.SuppressLint import android.content.Context -import android.view.inputmethod.EditorInfo import org.fcitx.fcitx5.android.R import org.fcitx.fcitx5.android.data.theme.Theme import org.fcitx.fcitx5.android.input.picker.PickerWindow @@ -55,8 +54,8 @@ class NumberKeyboard( val space: TextKeyView by lazy { findViewById(R.id.button_mini_space) } val `return`: ImageKeyView by lazy { findViewById(R.id.button_return) } - override fun onAttach(info: EditorInfo) { - `return`.img.imageResource = drawableForReturn(info) + override fun onReturnDrawableUpdate(returnDrawable: Int) { + `return`.img.imageResource = returnDrawable } } \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/ReturnKeyDrawable.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/ReturnKeyDrawable.kt new file mode 100644 index 000000000..fe032da81 --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/ReturnKeyDrawable.kt @@ -0,0 +1,33 @@ +package org.fcitx.fcitx5.android.input.keyboard + +import android.view.inputmethod.EditorInfo +import org.fcitx.fcitx5.android.R +import org.fcitx.fcitx5.android.daemon.FcitxConnection +import splitties.bitflags.hasFlag + +object ReturnKeyDrawable { + fun fromEditorInfo(info: EditorInfo): Int { + if (info.imeOptions.hasFlag(EditorInfo.IME_FLAG_NO_ENTER_ACTION)) { + return R.drawable.ic_baseline_keyboard_return_24 + } + return when (info.imeOptions and EditorInfo.IME_MASK_ACTION) { + EditorInfo.IME_ACTION_GO -> R.drawable.ic_baseline_arrow_forward_24 + EditorInfo.IME_ACTION_SEARCH -> R.drawable.ic_baseline_search_24 + EditorInfo.IME_ACTION_SEND -> R.drawable.ic_baseline_send_24 + EditorInfo.IME_ACTION_NEXT -> R.drawable.ic_baseline_keyboard_tab_24 + EditorInfo.IME_ACTION_DONE -> R.drawable.ic_baseline_done_24 + EditorInfo.IME_ACTION_PREVIOUS -> R.drawable.ic_baseline_keyboard_tab_reverse_24 + else -> R.drawable.ic_baseline_keyboard_return_24 + } + } + + fun from(fcitx: FcitxConnection, info: EditorInfo): Int { + val preedit = fcitx.runImmediately { inputPanelCached.preedit } + val clientPreedit = fcitx.runImmediately { clientPreeditCached } + return if (preedit.isEmpty() && clientPreedit.isEmpty()) { + R.drawable.ic_baseline_keyboard_return_24 + } else { + fromEditorInfo(info) + } + } +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt index 8792db58a..d5e58ac8e 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt @@ -2,11 +2,8 @@ package org.fcitx.fcitx5.android.input.keyboard import android.annotation.SuppressLint import android.content.Context -import android.view.inputmethod.EditorInfo import androidx.core.view.allViews import org.fcitx.fcitx5.android.R -import org.fcitx.fcitx5.android.core.CapabilityFlags -import org.fcitx.fcitx5.android.core.FcitxEvent import org.fcitx.fcitx5.android.core.InputMethodEntry import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.data.prefs.ManagedPreference @@ -130,18 +127,13 @@ class TextKeyboard( if (capsState == CapsState.Once) switchCapsState() } - override fun onAttach(info: EditorInfo) { + override fun onAttach() { updateCapsButtonIcon() updateAlphabetKeys() - `return`.img.imageResource = drawableForReturn(info) } - override fun onEditorInfoChange(info: EditorInfo, capFlags: CapabilityFlags) { - `return`.img.imageResource = drawableForReturn(info) - } - - override fun onPreeditChange(info: EditorInfo, data: FcitxEvent.PreeditEvent.Data) { - updateReturnButton(`return`, info, data) + override fun onReturnDrawableUpdate(returnDrawable: Int) { + `return`.img.imageResource = returnDrawable } override fun onPunctuationUpdate(mapping: Map) { @@ -149,7 +141,7 @@ class TextKeyboard( updatePunctuationKeys() } - override fun onInputMethodChange(ime: InputMethodEntry) { + override fun onInputMethodUpdate(ime: InputMethodEntry) { space.mainText.text = buildString { append(ime.displayName) ime.subMode.run { label.ifEmpty { name.ifEmpty { null } } }?.let { append(" ($it)") } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/preedit/PreeditComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/preedit/PreeditComponent.kt index 7b4e102f0..f1786efd3 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/preedit/PreeditComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/preedit/PreeditComponent.kt @@ -16,23 +16,10 @@ class PreeditComponent : UniqueComponent(), Dependent, InputBr private val context by manager.context() private val theme by manager.theme() - private var preedit = FcitxEvent.PreeditEvent.Data() - private var aux = FcitxEvent.InputPanelAuxEvent.Data() - val ui by lazy { PreeditUi(context, theme) } - override fun onPreeditUpdate(data: FcitxEvent.PreeditEvent.Data) { - preedit = data - updateView() - } - - override fun onInputPanelAuxUpdate(data: FcitxEvent.InputPanelAuxEvent.Data) { - aux = data - updateView() - } - - private fun updateView() { - ui.update(preedit, aux) + override fun onInputPanelUpdate(data: FcitxEvent.InputPanelEvent.Data) { + ui.update(data) ui.root.visibility = if (ui.visible) View.VISIBLE else View.INVISIBLE } } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/preedit/PreeditUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/preedit/PreeditUi.kt index 6ad08743f..901bcdd41 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/preedit/PreeditUi.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/preedit/PreeditUi.kt @@ -73,37 +73,37 @@ class PreeditUi(override val ctx: Context, private val theme: Theme) : Ui { } } - fun update(preedit: FcitxEvent.PreeditEvent.Data, aux: FcitxEvent.InputPanelAuxEvent.Data) { + fun update(inputPanel: FcitxEvent.InputPanelEvent.Data) { val bkgColor = theme.genericActiveBackgroundColor - val upText: SpannedString + val upString: SpannedString val upCursor: Int - if (aux.auxUp.isEmpty()) { - upText = preedit.preedit.toSpannedString(bkgColor) - upCursor = preedit.preedit.cursor + if (inputPanel.auxUp.isEmpty()) { + upString = inputPanel.preedit.toSpannedString(bkgColor) + upCursor = inputPanel.preedit.cursor } else { - upText = buildSpannedString { - append(aux.auxUp.toSpannedString(bkgColor)) - append(preedit.preedit.toSpannedString(bkgColor)) + upString = buildSpannedString { + append(inputPanel.auxUp.toSpannedString(bkgColor)) + append(inputPanel.preedit.toSpannedString(bkgColor)) } - upCursor = preedit.preedit.cursor.let { + upCursor = inputPanel.preedit.cursor.let { if (it < 0) it - else aux.auxUp.length + it + else inputPanel.auxUp.length + it } } - val downString = aux.auxDown.toSpannedString(bkgColor) - val hasUp = upText.isNotEmpty() + val downString = inputPanel.auxDown.toSpannedString(bkgColor) + val hasUp = upString.isNotEmpty() val hasDown = downString.isNotEmpty() visible = hasUp || hasDown if (!visible) return - val upString = if (upCursor < 0 || upCursor == upText.length) { - upText + val upStringWithCursor = if (upCursor < 0 || upCursor == upString.length) { + upString } else buildSpannedString { - if (upCursor > 0) append(upText, 0, upCursor) + if (upCursor > 0) append(upString, 0, upCursor) append('|') setSpan(cursorSpan, upCursor, upCursor + 1, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) - append(upText, upCursor, upText.length) + append(upString, upCursor, upString.length) } - updateTextView(upView, upString, hasUp) + updateTextView(upView, upStringWithCursor, hasUp) updateTextView(downView, downString, hasDown) } } From 7cd221b67c26fbfccb7eabb4ca4ad7283ce77497 Mon Sep 17 00:00:00 2001 From: Rocka Date: Mon, 20 Feb 2023 23:19:47 +0800 Subject: [PATCH 020/624] Fix bar transition between Idle and Candidate --- .../fcitx5/android/input/bar/KawaiiBarStateMachine.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarStateMachine.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarStateMachine.kt index a6ee5d380..ef30a9a34 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarStateMachine.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarStateMachine.kt @@ -24,10 +24,15 @@ object KawaiiBarStateMachine { EventStateMachine.TransitionEvent by BuildTransitionEvent(builder) { PreeditUpdated({ from(Candidate) transitTo Idle on (PreeditEmpty to true) - from(Idle) transitTo Candidate on (PreeditEmpty to false) + from(Idle) transitTo Candidate onF { + it(PreeditEmpty) == false && it(CandidateEmpty) == false + } }), CandidatesUpdated({ from(Idle) transitTo Candidate on (CandidateEmpty to false) + from(Candidate) transitTo Idle onF { + it(PreeditEmpty) == true && it(CandidateEmpty) == true + } }), ExtendedWindowAttached({ from(Idle) transitTo Title From f734f8da11c15d5e1685d641930f44dbcf84d2c8 Mon Sep 17 00:00:00 2001 From: Rocka Date: Mon, 20 Feb 2023 23:32:14 +0800 Subject: [PATCH 021/624] Extract common broadcast handlers PreeditEmptyState, Punctuation, ReturnKeyDrawable --- .../fcitx/fcitx5/android/input/InputView.kt | 12 +++- .../android/input/bar/KawaiiBarComponent.kt | 19 +----- .../input/broadcast/InputBroadcastReceiver.kt | 5 ++ .../input/broadcast/InputBroadcaster.kt | 8 +++ .../broadcast/PreeditEmptyStateComponent.kt | 32 ++++++++++ .../PunctuationComponent.kt | 24 +++----- .../broadcast/ReturnKeyDrawableComponent.kt | 59 +++++++++++++++++++ .../expanded/ExpandedCandidateLayout.kt | 14 ++--- .../window/BaseExpandedCandidateWindow.kt | 22 +++---- .../android/input/keyboard/KeyboardWindow.kt | 29 +++------ .../input/keyboard/ReturnKeyDrawable.kt | 33 ----------- .../android/input/picker/PickerLayout.kt | 6 +- .../android/input/picker/PickerWindow.kt | 10 +++- .../android/input/popup/PopupComponent.kt | 2 +- 14 files changed, 160 insertions(+), 115 deletions(-) create mode 100644 app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/PreeditEmptyStateComponent.kt rename app/src/main/java/org/fcitx/fcitx5/android/input/{punctuation => broadcast}/PunctuationComponent.kt (68%) create mode 100644 app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/ReturnKeyDrawableComponent.kt delete mode 100644 app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/ReturnKeyDrawable.kt diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt index 9e6585a11..fc4a0fbf3 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt @@ -26,6 +26,9 @@ import org.fcitx.fcitx5.android.data.theme.ThemeManager import org.fcitx.fcitx5.android.data.theme.ThemeManager.Prefs.NavbarBackground import org.fcitx.fcitx5.android.input.bar.KawaiiBarComponent import org.fcitx.fcitx5.android.input.broadcast.InputBroadcaster +import org.fcitx.fcitx5.android.input.broadcast.PreeditEmptyStateComponent +import org.fcitx.fcitx5.android.input.broadcast.PunctuationComponent +import org.fcitx.fcitx5.android.input.broadcast.ReturnKeyDrawableComponent import org.fcitx.fcitx5.android.input.candidates.CandidateViewBuilder import org.fcitx.fcitx5.android.input.candidates.HorizontalCandidateComponent import org.fcitx.fcitx5.android.input.keyboard.CommonKeyActionListener @@ -35,7 +38,6 @@ import org.fcitx.fcitx5.android.input.picker.emoticonPicker import org.fcitx.fcitx5.android.input.picker.symbolPicker import org.fcitx.fcitx5.android.input.popup.PopupComponent import org.fcitx.fcitx5.android.input.preedit.PreeditComponent -import org.fcitx.fcitx5.android.input.punctuation.PunctuationComponent import org.fcitx.fcitx5.android.input.wm.InputWindowManager import org.fcitx.fcitx5.android.utils.styledFloat import org.mechdancer.dependency.DynamicScope @@ -73,6 +75,8 @@ class InputView( private val broadcaster = InputBroadcaster() private val popup = PopupComponent() private val punctuation = PunctuationComponent() + private val returnKeyDrawable = ReturnKeyDrawableComponent() + private val preeditEmptyState = PreeditEmptyStateComponent() private val preedit = PreeditComponent() private val commonKeyActionListener = CommonKeyActionListener() private val candidateViewBuilder = CandidateViewBuilder() @@ -93,6 +97,8 @@ class InputView( scope += broadcaster scope += popup scope += punctuation + scope += returnKeyDrawable + scope += preeditEmptyState scope += preedit scope += commonKeyActionListener scope += candidateViewBuilder @@ -279,6 +285,7 @@ class InputView( } } broadcaster.onStartInput(info, capFlags) + returnKeyDrawable.updateDrawableOnEditorInfo(info) windowManager.attachWindow(KeyboardWindow) } @@ -289,10 +296,12 @@ class InputView( } is FcitxEvent.ClientPreeditEvent -> { + preeditEmptyState.updatePreeditEmptyState(clientPreedit = it.data) broadcaster.onClientPreeditUpdate(it.data) } is FcitxEvent.InputPanelEvent -> { + preeditEmptyState.updatePreeditEmptyState(preedit = it.data.preedit) broadcaster.onInputPanelUpdate(it.data) } @@ -301,6 +310,7 @@ class InputView( } is FcitxEvent.StatusAreaEvent -> { + punctuation.updatePunctuationMapping(it.data) broadcaster.onStatusAreaUpdate(it.data) } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt index dcd740a48..8daae7f50 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt @@ -14,8 +14,6 @@ import kotlinx.coroutines.launch import org.fcitx.fcitx5.android.R import org.fcitx.fcitx5.android.core.CapabilityFlag import org.fcitx.fcitx5.android.core.CapabilityFlags -import org.fcitx.fcitx5.android.core.FcitxEvent -import org.fcitx.fcitx5.android.core.FormattedText import org.fcitx.fcitx5.android.data.clipboard.ClipboardManager import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.data.prefs.ManagedPreference @@ -296,21 +294,8 @@ class KawaiiBarComponent : UniqueViewComponent( barStateMachine.push(CapFlagsUpdated) } - private fun pushPreeditUpdateEvent(preedit: FormattedText, clientPreedit: FormattedText) { - barStateMachine.push( - PreeditUpdated, - PreeditEmpty to (preedit.isEmpty() && clientPreedit.isEmpty()) - ) - } - - override fun onClientPreeditUpdate(data: FormattedText) { - val preedit = fcitx.runImmediately { inputPanelCached.preedit } - pushPreeditUpdateEvent(preedit, data) - } - - override fun onInputPanelUpdate(data: FcitxEvent.InputPanelEvent.Data) { - val clientPreedit = fcitx.runImmediately { clientPreeditCached } - pushPreeditUpdateEvent(data.preedit, clientPreedit) + override fun onPreeditEmptyStateUpdate(empty: Boolean) { + barStateMachine.push(PreeditUpdated, PreeditEmpty to empty) } override fun onCandidateUpdate(data: Array) { diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcastReceiver.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcastReceiver.kt index d6736f5a2..7dcdca068 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcastReceiver.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcastReceiver.kt @@ -1,6 +1,7 @@ package org.fcitx.fcitx5.android.input.broadcast import android.view.inputmethod.EditorInfo +import androidx.annotation.DrawableRes import org.fcitx.fcitx5.android.core.Action import org.fcitx.fcitx5.android.core.CapabilityFlags import org.fcitx.fcitx5.android.core.FcitxEvent.InputPanelEvent @@ -33,4 +34,8 @@ interface InputBroadcastReceiver { fun onPunctuationUpdate(mapping: Map) {} + fun onPreeditEmptyStateUpdate(empty: Boolean) {} + + fun onReturnKeyDrawableUpdate(@DrawableRes resourceId: Int) {} + } \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcaster.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcaster.kt index 3178cb0ae..7f0835273 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcaster.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/InputBroadcaster.kt @@ -77,4 +77,12 @@ class InputBroadcaster : UniqueComponent(), Dependent, InputBr receivers.forEach { it.onPunctuationUpdate(mapping) } } + override fun onPreeditEmptyStateUpdate(empty: Boolean) { + receivers.forEach { it.onPreeditEmptyStateUpdate(empty) } + } + + override fun onReturnKeyDrawableUpdate(resourceId: Int) { + receivers.forEach { it.onReturnKeyDrawableUpdate(resourceId) } + } + } \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/PreeditEmptyStateComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/PreeditEmptyStateComponent.kt new file mode 100644 index 000000000..14e6257ca --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/PreeditEmptyStateComponent.kt @@ -0,0 +1,32 @@ +package org.fcitx.fcitx5.android.input.broadcast + +import org.fcitx.fcitx5.android.core.FormattedText +import org.fcitx.fcitx5.android.input.dependency.fcitx +import org.mechdancer.dependency.Dependent +import org.mechdancer.dependency.UniqueComponent +import org.mechdancer.dependency.manager.ManagedHandler +import org.mechdancer.dependency.manager.managedHandler +import org.mechdancer.dependency.manager.must + +class PreeditEmptyStateComponent : + UniqueComponent(), Dependent, ManagedHandler by managedHandler() { + + private val fcitx by manager.fcitx() + private val broadcaster: InputBroadcaster by manager.must() + private val returnKeyDrawable: ReturnKeyDrawableComponent by manager.must() + + var isEmpty: Boolean = true + private set + + fun updatePreeditEmptyState( + clientPreedit: FormattedText = fcitx.runImmediately { clientPreeditCached }, + preedit: FormattedText = fcitx.runImmediately { inputPanelCached.preedit } + ) { + val empty = clientPreedit.isEmpty() && preedit.isEmpty() + if (isEmpty == empty) return + isEmpty = empty + broadcaster.onPreeditEmptyStateUpdate(isEmpty) + returnKeyDrawable.updateDrawableOnPreedit(isEmpty) + } + +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/punctuation/PunctuationComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/PunctuationComponent.kt similarity index 68% rename from app/src/main/java/org/fcitx/fcitx5/android/input/punctuation/PunctuationComponent.kt rename to app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/PunctuationComponent.kt index ad80af288..867e41fd8 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/punctuation/PunctuationComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/PunctuationComponent.kt @@ -1,11 +1,9 @@ -package org.fcitx.fcitx5.android.input.punctuation +package org.fcitx.fcitx5.android.input.broadcast import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.launch import org.fcitx.fcitx5.android.core.Action import org.fcitx.fcitx5.android.data.punctuation.PunctuationManager -import org.fcitx.fcitx5.android.input.broadcast.InputBroadcastReceiver -import org.fcitx.fcitx5.android.input.broadcast.InputBroadcaster import org.fcitx.fcitx5.android.input.dependency.fcitx import org.fcitx.fcitx5.android.input.dependency.inputMethodService import org.mechdancer.dependency.Dependent @@ -14,37 +12,33 @@ import org.mechdancer.dependency.manager.ManagedHandler import org.mechdancer.dependency.manager.managedHandler import org.mechdancer.dependency.manager.must -class PunctuationComponent : InputBroadcastReceiver, +class PunctuationComponent : UniqueComponent(), Dependent, ManagedHandler by managedHandler() { private val fcitx by manager.fcitx() private val service by manager.inputMethodService() private val broadcaster: InputBroadcaster by manager.must() - private var mapping: Map = mapOf() + private var mapping: Map = emptyMap() var enabled: Boolean = false private set fun transform(p: String) = mapping.getOrDefault(p, p) - private fun updateMapping(lang: String? = null) { + fun updatePunctuationMapping(actions: Array) { + enabled = actions.any { + // TODO: A better way to check if punctuation mapping is enabled + it.name == "punctuation" && it.icon == "fcitx-punc-active" + } service.lifecycleScope.launch { mapping = if (enabled) { fcitx.runOnReady { - PunctuationManager.load(this, lang ?: inputMethodEntryCached.languageCode) + PunctuationManager.load(this, inputMethodEntryCached.languageCode) .associate { it.key to it.mapping } } } else emptyMap() broadcaster.onPunctuationUpdate(mapping) } } - - override fun onStatusAreaUpdate(actions: Array) { - enabled = actions.any { - // TODO A better way to check if punctuation mapping is enabled - it.name == "punctuation" && it.icon == "fcitx-punc-active" - } - updateMapping() - } } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/ReturnKeyDrawableComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/ReturnKeyDrawableComponent.kt new file mode 100644 index 000000000..dcb0142f9 --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/broadcast/ReturnKeyDrawableComponent.kt @@ -0,0 +1,59 @@ +package org.fcitx.fcitx5.android.input.broadcast + +import android.view.inputmethod.EditorInfo +import androidx.annotation.DrawableRes +import org.fcitx.fcitx5.android.R +import org.mechdancer.dependency.Dependent +import org.mechdancer.dependency.UniqueComponent +import org.mechdancer.dependency.manager.ManagedHandler +import org.mechdancer.dependency.manager.managedHandler +import org.mechdancer.dependency.manager.must +import splitties.bitflags.hasFlag + +class ReturnKeyDrawableComponent : + UniqueComponent(), Dependent, ManagedHandler by managedHandler() { + + companion object { + @DrawableRes + const val DEFAULT_DRAWABLE = R.drawable.ic_baseline_keyboard_return_24 + } + + private val broadcaster: InputBroadcaster by manager.must() + + @DrawableRes + var resourceId: Int = DEFAULT_DRAWABLE + private set + + @DrawableRes + private var actionDrawable: Int = DEFAULT_DRAWABLE + + @DrawableRes + private fun drawableFromEditorInfo(info: EditorInfo): Int { + if (info.imeOptions.hasFlag(EditorInfo.IME_FLAG_NO_ENTER_ACTION)) { + return R.drawable.ic_baseline_keyboard_return_24 + } + return when (info.imeOptions and EditorInfo.IME_MASK_ACTION) { + EditorInfo.IME_ACTION_GO -> R.drawable.ic_baseline_arrow_forward_24 + EditorInfo.IME_ACTION_SEARCH -> R.drawable.ic_baseline_search_24 + EditorInfo.IME_ACTION_SEND -> R.drawable.ic_baseline_send_24 + EditorInfo.IME_ACTION_NEXT -> R.drawable.ic_baseline_keyboard_tab_24 + EditorInfo.IME_ACTION_DONE -> R.drawable.ic_baseline_done_24 + EditorInfo.IME_ACTION_PREVIOUS -> R.drawable.ic_baseline_keyboard_tab_reverse_24 + else -> R.drawable.ic_baseline_keyboard_return_24 + } + } + + fun updateDrawableOnEditorInfo(info: EditorInfo) { + actionDrawable = drawableFromEditorInfo(info) + if (resourceId == actionDrawable) return + resourceId = actionDrawable + broadcaster.onReturnKeyDrawableUpdate(resourceId) + } + + fun updateDrawableOnPreedit(preeditEmpty: Boolean) { + val newResId = if (preeditEmpty) actionDrawable else DEFAULT_DRAWABLE + if (resourceId == newResId) return + resourceId = newResId + broadcaster.onReturnKeyDrawableUpdate(resourceId) + } +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/ExpandedCandidateLayout.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/ExpandedCandidateLayout.kt index ef654b8a4..15b3cf860 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/ExpandedCandidateLayout.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/ExpandedCandidateLayout.kt @@ -3,7 +3,6 @@ package org.fcitx.fcitx5.android.input.candidates.expanded import android.annotation.SuppressLint import android.content.Context import androidx.constraintlayout.widget.ConstraintLayout -import androidx.recyclerview.widget.RecyclerView import org.fcitx.fcitx5.android.R import org.fcitx.fcitx5.android.data.theme.Theme import org.fcitx.fcitx5.android.data.theme.ThemeManager @@ -12,13 +11,10 @@ import splitties.views.backgroundColor import splitties.views.dsl.constraintlayout.* import splitties.views.dsl.core.add import splitties.views.dsl.recyclerview.recyclerView +import splitties.views.imageResource @SuppressLint("ViewConstructor") -class ExpandedCandidateLayout( - context: Context, - theme: Theme, - initRecyclerView: RecyclerView.() -> Unit = {} -) : ConstraintLayout(context) { +class ExpandedCandidateLayout(context: Context, theme: Theme) : ConstraintLayout(context) { class Keyboard(context: Context, theme: Theme) : BaseKeyboard(context, theme, Layout) { companion object { @@ -56,6 +52,10 @@ class ExpandedCandidateLayout( val pageDnBtn: ImageKeyView by lazy { findViewById(DownBtnId) } val backspace: ImageKeyView by lazy { findViewById(R.id.button_backspace) } val `return`: ImageKeyView by lazy { findViewById(R.id.button_return) } + + override fun onReturnDrawableUpdate(returnDrawable: Int) { + `return`.img.imageResource = returnDrawable + } } private val keyBorder by ThemeManager.prefs.keyBorder @@ -91,8 +91,6 @@ class ExpandedCandidateLayout( endOfParent() bottomOfParent() }) - - initRecyclerView(recyclerView) } fun resetPosition() { diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/BaseExpandedCandidateWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/BaseExpandedCandidateWindow.kt index 1dfd6c0ee..be4c39222 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/BaseExpandedCandidateWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/BaseExpandedCandidateWindow.kt @@ -8,14 +8,13 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import org.fcitx.fcitx5.android.core.FcitxEvent -import org.fcitx.fcitx5.android.core.FormattedText import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.BooleanKey.CandidatesEmpty import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.TransitionEvent.ExpandedCandidatesAttached import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.TransitionEvent.ExpandedCandidatesDetached import org.fcitx.fcitx5.android.input.bar.KawaiiBarComponent import org.fcitx.fcitx5.android.input.broadcast.InputBroadcastReceiver +import org.fcitx.fcitx5.android.input.broadcast.ReturnKeyDrawableComponent import org.fcitx.fcitx5.android.input.candidates.CandidateViewBuilder import org.fcitx.fcitx5.android.input.candidates.HorizontalCandidateComponent import org.fcitx.fcitx5.android.input.candidates.adapter.BaseCandidateViewAdapter @@ -40,6 +39,7 @@ abstract class BaseExpandedCandidateWindow> : private val bar: KawaiiBarComponent by manager.must() private val horizontalCandidate: HorizontalCandidateComponent by manager.must() private val windowManager: InputWindowManager by manager.must() + private val returnKeyDrawable: ReturnKeyDrawableComponent by manager.must() protected val disableAnimation by AppPrefs.getInstance().advanced.disableAnimation @@ -73,7 +73,10 @@ abstract class BaseExpandedCandidateWindow> : override fun onAttached() { lifecycleCoroutineScope = candidateLayout.findViewTreeLifecycleOwner()!!.lifecycleScope bar.expandButtonStateMachine.push(ExpandedCandidatesAttached) - candidateLayout.embeddedKeyboard.keyActionListener = keyActionListener + candidateLayout.embeddedKeyboard.also { + it.onReturnDrawableUpdate(returnKeyDrawable.resourceId) + it.keyActionListener = keyActionListener + } offsetJob = horizontalCandidate.expandedCandidateOffset .onEach(this::updateCandidatesWithOffset) .launchIn(lifecycleCoroutineScope) @@ -101,19 +104,10 @@ abstract class BaseExpandedCandidateWindow> : candidateLayout.embeddedKeyboard.keyActionListener = null } - private fun evaluateShouldDetach(preedit: FormattedText, clientPreedit: FormattedText) { - if (preedit.isEmpty() && clientPreedit.isEmpty()) { + override fun onPreeditEmptyStateUpdate(empty: Boolean) { + if (empty) { windowManager.attachWindow(KeyboardWindow) } } - override fun onClientPreeditUpdate(data: FormattedText) { - val preedit = fcitx.runImmediately { inputPanelCached.preedit } - evaluateShouldDetach(preedit, data) - } - - override fun onInputPanelUpdate(data: FcitxEvent.InputPanelEvent.Data) { - val clientPreedit = fcitx.runImmediately { clientPreeditCached } - evaluateShouldDetach(data.preedit, clientPreedit) - } } \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt index fb4456b99..93e762176 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt @@ -5,19 +5,17 @@ import android.view.Gravity import android.view.View import android.view.inputmethod.EditorInfo import android.widget.FrameLayout -import androidx.annotation.DrawableRes import androidx.core.content.ContextCompat import androidx.transition.Slide import org.fcitx.fcitx5.android.R import org.fcitx.fcitx5.android.core.CapabilityFlags -import org.fcitx.fcitx5.android.core.FcitxEvent -import org.fcitx.fcitx5.android.core.FormattedText import org.fcitx.fcitx5.android.core.InputMethodEntry import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.input.bar.KawaiiBarComponent import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.KeyboardSwitchedOutNumber import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.KeyboardSwitchedToNumber import org.fcitx.fcitx5.android.input.broadcast.InputBroadcastReceiver +import org.fcitx.fcitx5.android.input.broadcast.ReturnKeyDrawableComponent import org.fcitx.fcitx5.android.input.dependency.fcitx import org.fcitx.fcitx5.android.input.dependency.inputMethodService import org.fcitx.fcitx5.android.input.dependency.theme @@ -43,6 +41,7 @@ class KeyboardWindow : InputWindow.SimpleInputWindow(), Essentia private val windowManager: InputWindowManager by manager.must() private val popup: PopupComponent by manager.must() private val bar: KawaiiBarComponent by manager.must() + private val returnKeyDrawable: ReturnKeyDrawableComponent by manager.must() companion object : EssentialWindow.Key @@ -87,16 +86,6 @@ class KeyboardWindow : InputWindow.SimpleInputWindow(), Essentia popup.listener } - @DrawableRes - private var returnDrawable = R.drawable.ic_baseline_keyboard_return_24 - - private fun updateReturnDrawable() { - val newDrawable = ReturnKeyDrawable.from(fcitx, service.editorInfo) - if (returnDrawable == newDrawable) return - returnDrawable = newDrawable - currentKeyboard?.onReturnDrawableUpdate(returnDrawable) - } - // This will be called EXACTLY ONCE override fun onCreateView(): View { keyboardView = context.frameLayout(R.id.keyboard_view) @@ -120,7 +109,7 @@ class KeyboardWindow : InputWindow.SimpleInputWindow(), Essentia it.popupActionListener = popupActionListener keyboardView.apply { add(it, lParams(matchParent, matchParent)) } it.onAttach() - it.onReturnDrawableUpdate(returnDrawable) + it.onReturnDrawableUpdate(returnKeyDrawable.resourceId) it.onInputMethodUpdate(fcitx.runImmediately { inputMethodEntryCached }) } } @@ -159,18 +148,14 @@ class KeyboardWindow : InputWindow.SimpleInputWindow(), Essentia currentKeyboard?.onInputMethodUpdate(ime) } - override fun onClientPreeditUpdate(data: FormattedText) { - updateReturnDrawable() - } - - override fun onInputPanelUpdate(data: FcitxEvent.InputPanelEvent.Data) { - updateReturnDrawable() - } - override fun onPunctuationUpdate(mapping: Map) { currentKeyboard?.onPunctuationUpdate(mapping) } + override fun onReturnKeyDrawableUpdate(resourceId: Int) { + currentKeyboard?.onReturnDrawableUpdate(resourceId) + } + override fun onAttached() { currentKeyboard?.let { it.keyActionListener = keyActionListener diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/ReturnKeyDrawable.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/ReturnKeyDrawable.kt deleted file mode 100644 index fe032da81..000000000 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/ReturnKeyDrawable.kt +++ /dev/null @@ -1,33 +0,0 @@ -package org.fcitx.fcitx5.android.input.keyboard - -import android.view.inputmethod.EditorInfo -import org.fcitx.fcitx5.android.R -import org.fcitx.fcitx5.android.daemon.FcitxConnection -import splitties.bitflags.hasFlag - -object ReturnKeyDrawable { - fun fromEditorInfo(info: EditorInfo): Int { - if (info.imeOptions.hasFlag(EditorInfo.IME_FLAG_NO_ENTER_ACTION)) { - return R.drawable.ic_baseline_keyboard_return_24 - } - return when (info.imeOptions and EditorInfo.IME_MASK_ACTION) { - EditorInfo.IME_ACTION_GO -> R.drawable.ic_baseline_arrow_forward_24 - EditorInfo.IME_ACTION_SEARCH -> R.drawable.ic_baseline_search_24 - EditorInfo.IME_ACTION_SEND -> R.drawable.ic_baseline_send_24 - EditorInfo.IME_ACTION_NEXT -> R.drawable.ic_baseline_keyboard_tab_24 - EditorInfo.IME_ACTION_DONE -> R.drawable.ic_baseline_done_24 - EditorInfo.IME_ACTION_PREVIOUS -> R.drawable.ic_baseline_keyboard_tab_reverse_24 - else -> R.drawable.ic_baseline_keyboard_return_24 - } - } - - fun from(fcitx: FcitxConnection, info: EditorInfo): Int { - val preedit = fcitx.runImmediately { inputPanelCached.preedit } - val clientPreedit = fcitx.runImmediately { clientPreeditCached } - return if (preedit.isEmpty() && clientPreedit.isEmpty()) { - R.drawable.ic_baseline_keyboard_return_24 - } else { - fromEditorInfo(info) - } - } -} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerLayout.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerLayout.kt index f838aae8c..5fa81b204 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerLayout.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerLayout.kt @@ -11,6 +11,7 @@ import splitties.dimensions.dp import splitties.views.dsl.constraintlayout.* import splitties.views.dsl.core.add import splitties.views.dsl.core.view +import splitties.views.imageResource @SuppressLint("ViewConstructor") class PickerLayout(context: Context, theme: Theme, switchKey: KeyDef) : @@ -41,8 +42,11 @@ class PickerLayout(context: Context, theme: Theme, switchKey: KeyDef) : ) ) - val backspace: ImageKeyView by lazy { findViewById(R.id.button_backspace) } val `return`: ImageKeyView by lazy { findViewById(R.id.button_return) } + + override fun onReturnDrawableUpdate(returnDrawable: Int) { + `return`.img.imageResource = returnDrawable + } } val embeddedKeyboard = Keyboard(context, theme, switchKey) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerWindow.kt index b1eebd72d..32e818f21 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/picker/PickerWindow.kt @@ -5,7 +5,7 @@ import androidx.core.content.ContextCompat import androidx.transition.Slide import androidx.transition.Transition import androidx.viewpager2.widget.ViewPager2 -import org.fcitx.fcitx5.android.input.broadcast.InputBroadcastReceiver +import org.fcitx.fcitx5.android.input.broadcast.ReturnKeyDrawableComponent import org.fcitx.fcitx5.android.input.dependency.theme import org.fcitx.fcitx5.android.input.keyboard.* import org.fcitx.fcitx5.android.input.popup.PopupAction @@ -22,7 +22,7 @@ class PickerWindow( val density: PickerPageUi.Density, private val switchKey: KeyDef, val popupPreview: Boolean = true -) : InputWindow.ExtendedInputWindow(), EssentialWindow, InputBroadcastReceiver { +) : InputWindow.ExtendedInputWindow(), EssentialWindow { enum class Key : EssentialWindow.Key { Symbol, @@ -34,6 +34,7 @@ class PickerWindow( private val windowManager: InputWindowManager by manager.must() private val commonKeyActionListener: CommonKeyActionListener by manager.must() private val popup: PopupComponent by manager.must() + private val returnKeyDrawable: ReturnKeyDrawableComponent by manager.must() private lateinit var pickerLayout: PickerLayout private lateinit var pickerPagesAdapter: PickerPagesAdapter @@ -147,7 +148,10 @@ class PickerWindow( override fun onCreateBarExtension() = pickerLayout.tabsUi.root override fun onAttached() { - pickerLayout.embeddedKeyboard.keyActionListener = keyActionListener + pickerLayout.embeddedKeyboard.also { + it.onReturnDrawableUpdate(returnKeyDrawable.resourceId) + it.keyActionListener = keyActionListener + } } override fun onDetached() { diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupComponent.kt index a29dc0589..a8241ed48 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/popup/PopupComponent.kt @@ -6,12 +6,12 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.fcitx.fcitx5.android.data.theme.ThemeManager +import org.fcitx.fcitx5.android.input.broadcast.PunctuationComponent import org.fcitx.fcitx5.android.input.dependency.context import org.fcitx.fcitx5.android.input.dependency.inputMethodService import org.fcitx.fcitx5.android.input.dependency.theme import org.fcitx.fcitx5.android.input.keyboard.KeyAction import org.fcitx.fcitx5.android.input.keyboard.KeyDef -import org.fcitx.fcitx5.android.input.punctuation.PunctuationComponent import org.mechdancer.dependency.Dependent import org.mechdancer.dependency.UniqueComponent import org.mechdancer.dependency.manager.ManagedHandler From 44e09f0dd748ab97ddfd52e620a195fff6cd7481 Mon Sep 17 00:00:00 2001 From: linsui <36977733+linsui@users.noreply.github.com> Date: Tue, 21 Feb 2023 11:08:48 +0000 Subject: [PATCH 022/624] Add icon to fastlane metadata (#199) Co-authored-by: linsui --- fastlane/metadata/android/en-US/images/icon.png | 1 + 1 file changed, 1 insertion(+) create mode 120000 fastlane/metadata/android/en-US/images/icon.png diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png new file mode 120000 index 000000000..915bc00c6 --- /dev/null +++ b/fastlane/metadata/android/en-US/images/icon.png @@ -0,0 +1 @@ +../../../../../app/src/main/res/mipmap-xxxhdpi/ic_launcher.png \ No newline at end of file From 9be4fdc02acc20dca5a15bf4853d82be24652a35 Mon Sep 17 00:00:00 2001 From: linsui <36977733+linsui@users.noreply.github.com> Date: Tue, 21 Feb 2023 17:00:04 +0000 Subject: [PATCH 023/624] Add F-Droid badge (#200) Co-authored-by: linsui Co-authored-by: Rocka --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6ad710e44..0dc94e35e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,19 @@ An attempt to run fcitx5 on Android. -[![Jenkins Build Status](https://img.shields.io/jenkins/s/https/jenkins.fcitx-im.org/job/android/job/fcitx5-android.svg)](https://jenkins.fcitx-im.org/job/android/job/fcitx5-android/) +## Download + +### Latest CI builds + +Jenkins: [![build status](https://img.shields.io/jenkins/build.svg?jobUrl=https://jenkins.fcitx-im.org/job/android/job/fcitx5-android/)](https://jenkins.fcitx-im.org/job/android/job/fcitx5-android/) + +### Tagged releases + +GitHub: [![release version](https://img.shields.io/github/v/release/fcitx5-android/fcitx5-android)](https://github.com/fcitx5-android/fcitx5-android/releases) + +[Get it on F-Droid](https://f-droid.org/packages/org.fcitx.fcitx5.android) ## Project status From ab53a12d79788cfd999388b2b691427563667cd9 Mon Sep 17 00:00:00 2001 From: Rocka Date: Wed, 22 Feb 2023 01:01:52 +0800 Subject: [PATCH 024/624] Refactor HorizontalCandidateComponent - drop horizontal_candidate_growth prefs, span count is controlled by expanded_candidate_grid_span_count - don't stretch candidates when recycler view can display all of them --- .../fcitx5/android/data/prefs/AppPrefs.kt | 4 +- .../android/data/prefs/ManagedPreference.kt | 2 +- .../input/candidates/CandidateViewBuilder.kt | 59 ++++++---- .../HorizontalCandidateComponent.kt | 101 +++++++++++++----- .../adapter/SimpleCandidateViewAdapter.kt | 20 ---- .../window/BaseExpandedCandidateWindow.kt | 5 +- .../window/FlexboxExpandedCandidateWindow.kt | 27 +++-- .../window/GridExpandedCandidateWindow.kt | 11 +- 8 files changed, 137 insertions(+), 92 deletions(-) delete mode 100644 app/src/main/java/org/fcitx/fcitx5/android/input/candidates/adapter/SimpleCandidateViewAdapter.kt diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt index 40615797a..2fb15b96b 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt @@ -175,8 +175,6 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) { keyboardBottomPaddingLandscape = secondary } - val horizontalCandidateGrowth = - switch(R.string.horizontal_candidate_growth, "horizontal_candidate_growth", true) val expandedCandidateStyle = list( R.string.expanded_candidate_style, "expanded_candidate_style", @@ -206,7 +204,7 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) { 8, 4, 12, - ) { expandedCandidateStyle.getValue() == ExpandedCandidateStyle.Grid } + ) expandedCandidateGridSpanCount = primary expandedCandidateGridSpanCountLandscape = secondary } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreference.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreference.kt index 7032f1062..a7dea0f0f 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreference.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreference.kt @@ -44,7 +44,7 @@ abstract class ManagedPreference( } fun fireChange() { - listeners.forEach { with(it) { onChange(key, getValue()) } } + listeners.forEach { it.onChange(key, getValue()) } } class PBool(sharedPreferences: SharedPreferences, key: String, defaultValue: Boolean) : diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/CandidateViewBuilder.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/CandidateViewBuilder.kt index f7d7aa493..df207d8bf 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/CandidateViewBuilder.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/CandidateViewBuilder.kt @@ -2,14 +2,13 @@ package org.fcitx.fcitx5.android.input.candidates import android.graphics.drawable.ShapeDrawable import android.graphics.drawable.shapes.RectShape -import android.view.ContextThemeWrapper +import android.view.View +import android.view.ViewGroup import androidx.lifecycle.lifecycleScope -import org.fcitx.fcitx5.android.daemon.FcitxConnection +import com.google.android.flexbox.FlexboxLayoutManager import org.fcitx.fcitx5.android.daemon.launchOnFcitxReady -import org.fcitx.fcitx5.android.data.theme.Theme -import org.fcitx.fcitx5.android.input.FcitxInputMethodService +import org.fcitx.fcitx5.android.input.candidates.adapter.BaseCandidateViewAdapter import org.fcitx.fcitx5.android.input.candidates.adapter.GridCandidateViewAdapter -import org.fcitx.fcitx5.android.input.candidates.adapter.SimpleCandidateViewAdapter import org.fcitx.fcitx5.android.input.dependency.context import org.fcitx.fcitx5.android.input.dependency.fcitx import org.fcitx.fcitx5.android.input.dependency.inputMethodService @@ -19,36 +18,52 @@ import org.mechdancer.dependency.UniqueComponent import org.mechdancer.dependency.manager.ManagedHandler import org.mechdancer.dependency.manager.managedHandler import splitties.dimensions.dp +import splitties.views.setPaddingDp +import java.lang.Integer.max -class CandidateViewBuilder : UniqueComponent(), Dependent, - ManagedHandler by managedHandler() { +class CandidateViewBuilder : + UniqueComponent(), Dependent, ManagedHandler by managedHandler() { - private val service: FcitxInputMethodService by manager.inputMethodService() - private val context: ContextThemeWrapper by manager.context() - private val fcitx: FcitxConnection by manager.fcitx() + private val service by manager.inputMethodService() + private val context by manager.context() + private val fcitx by manager.fcitx() private val theme by manager.theme() fun gridAdapter() = object : GridCandidateViewAdapter() { + override val theme = this@CandidateViewBuilder.theme override fun onSelect(idx: Int) { service.lifecycleScope.launchOnFcitxReady(fcitx) { it.select(idx) } } - - override val theme: Theme - get() = this@CandidateViewBuilder.theme } - fun simpleAdapter() = object : SimpleCandidateViewAdapter() { - override fun onSelect(idx: Int) { - service.lifecycleScope.launchOnFcitxReady(fcitx) { it.select(idx) } - } + fun flexAdapter(initLayoutParams: View.(Int) -> FlexboxLayoutManager.LayoutParams) = + object : BaseCandidateViewAdapter() { + override val theme = this@CandidateViewBuilder.theme + override fun onSelect(idx: Int) { + service.lifecycleScope.launchOnFcitxReady(fcitx) { it.select(idx) } + } - override val theme: Theme - get() = this@CandidateViewBuilder.theme - } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return super.onCreateViewHolder(parent, viewType).apply { + ui.root.apply { + setPaddingDp(8, 0, 8, 0) + minimumWidth = dp(40) + } + } + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + super.onBindViewHolder(holder, position) + // set ViewHolder's layoutParams to `FlexboxLayoutManager.LayoutParams`, + // so layoutManager won't bother `generate{,Default}LayoutParams` + holder.ui.root.layoutParams = initLayoutParams(holder.ui.root, position) + } + } fun dividerDrawable() = ShapeDrawable(RectShape()).apply { - intrinsicWidth = context.dp(1) - intrinsicHeight = context.dp(1) + val intrinsicSize = max(1, context.dp(1)) + intrinsicWidth = intrinsicSize + intrinsicHeight = intrinsicSize paint.color = theme.dividerColor } } \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/HorizontalCandidateComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/HorizontalCandidateComponent.kt index 5f43cf192..8a407706a 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/HorizontalCandidateComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/HorizontalCandidateComponent.kt @@ -1,7 +1,7 @@ package org.fcitx.fcitx5.android.input.candidates -import android.content.Context -import android.view.ViewGroup +import android.content.res.Configuration +import androidx.core.view.updateLayoutParams import androidx.recyclerview.widget.RecyclerView import com.google.android.flexbox.FlexboxLayoutManager import kotlinx.coroutines.channels.BufferOverflow @@ -14,23 +14,40 @@ import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.BooleanKey.Ca import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.TransitionEvent.ExpandedCandidatesUpdated import org.fcitx.fcitx5.android.input.bar.KawaiiBarComponent import org.fcitx.fcitx5.android.input.broadcast.InputBroadcastReceiver +import org.fcitx.fcitx5.android.input.candidates.adapter.BaseCandidateViewAdapter import org.fcitx.fcitx5.android.input.candidates.expanded.decoration.FlexboxVerticalDecoration import org.fcitx.fcitx5.android.input.dependency.UniqueViewComponent import org.fcitx.fcitx5.android.input.dependency.context import org.mechdancer.dependency.manager.must +import splitties.views.dsl.core.wrapContent import splitties.views.dsl.recyclerview.recyclerView class HorizontalCandidateComponent : UniqueViewComponent(), InputBroadcastReceiver { + private val context by manager.context() private val builder: CandidateViewBuilder by manager.must() - private val context: Context by manager.context() private val bar: KawaiiBarComponent by manager.must() - val adapter by lazy { - builder.simpleAdapter() + private val maxSpanCount by lazy { + AppPrefs.getInstance().keyboard.run { + if (context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) + expandedCandidateGridSpanCount + else + expandedCandidateGridSpanCountLandscape + }.getValue() } + private var layoutFlexGrow = 1f + + /** + * Second layout pass is needed when total candidates count < [maxSpanCount], but the + * RecyclerView cannot display all of them. In that case, displayed candidates should be + * stretched evenly (by setting flexGrow to 1.0f). + */ + private var secondLayoutPassNeeded = false + private var secondLayoutPassDone = false + // Since expanded candidate window is created once the expand button was clicked, // we need to replay the last offset private val _expandedCandidateOffset = MutableSharedFlow( @@ -40,39 +57,73 @@ class HorizontalCandidateComponent : val expandedCandidateOffset = _expandedCandidateOffset.asSharedFlow() - val horizontalCandidateGrowth by AppPrefs.getInstance().keyboard.horizontalCandidateGrowth - private fun refreshExpanded() { runBlocking { _expandedCandidateOffset.emit(view.childCount) } } - override val view by lazy { - context.recyclerView(R.id.candidate_view) { - adapter = this@HorizontalCandidateComponent.adapter - layoutManager = object : FlexboxLayoutManager(context) { - override fun canScrollVertically(): Boolean = false - override fun canScrollHorizontally(): Boolean = false - override fun generateLayoutParams(lp: ViewGroup.LayoutParams?) = - LayoutParams(lp).apply { - flexGrow = if (horizontalCandidateGrowth) 1f else 0f - } + val adapter: BaseCandidateViewAdapter by lazy { + builder.flexAdapter { + val rv = this@HorizontalCandidateComponent.view + FlexboxLayoutManager.LayoutParams(wrapContent, rv.height).apply { + minWidth = (rv.width - dividerDrawable.intrinsicWidth * maxSpanCount) / maxSpanCount + flexGrow = layoutFlexGrow + } + } + } - override fun onLayoutCompleted(state: RecyclerView.State?) { - super.onLayoutCompleted(state) - refreshExpanded() - bar.expandButtonStateMachine.push( - ExpandedCandidatesUpdated, - CandidatesEmpty to (adapter!!.itemCount - childCount <= 0) - ) + val layoutManager by lazy { + object : FlexboxLayoutManager(context) { + override fun canScrollVertically() = false + override fun canScrollHorizontally() = false + override fun onLayoutCompleted(state: RecyclerView.State?) { + super.onLayoutCompleted(state) + if (secondLayoutPassNeeded) { + if (childCount >= adapter.candidates.size) { + // second layout pass is not actually need, + // because RecyclerView can display all the candidates + secondLayoutPassNeeded = false + } else { + // second layout pass would trigger onLayoutCompleted as well, + // just skip second onLayoutCompleted + if (secondLayoutPassDone) return + secondLayoutPassDone = true + for (i in 0 until childCount) { + getChildAt(i)!!.updateLayoutParams { + flexGrow = 1f + } + } + } } + refreshExpanded() + bar.expandButtonStateMachine.push( + ExpandedCandidatesUpdated, + CandidatesEmpty to (adapter.candidates.size - childCount <= 0) + ) } - addItemDecoration(FlexboxVerticalDecoration(builder.dividerDrawable())) + // no need to override `generate{,Default}LayoutParams`, because builder.flexAdapter() + // guarantees ViewHolder's layoutParams to be `FlexboxLayoutManager.LayoutParams` + } + } + + private val dividerDrawable by lazy { + builder.dividerDrawable() + } + + override val view by lazy { + context.recyclerView(R.id.candidate_view) { + adapter = this@HorizontalCandidateComponent.adapter + layoutManager = this@HorizontalCandidateComponent.layoutManager + addItemDecoration(FlexboxVerticalDecoration(dividerDrawable)) } } override fun onCandidateUpdate(data: Array) { + layoutFlexGrow = if (data.size < maxSpanCount) 0f else 1f + // second layout pass maybe needed when total candidates count < maxSpanCount + secondLayoutPassNeeded = data.size < maxSpanCount + secondLayoutPassDone = false adapter.updateCandidates(data) } } \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/adapter/SimpleCandidateViewAdapter.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/adapter/SimpleCandidateViewAdapter.kt deleted file mode 100644 index d1f3fbcb6..000000000 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/adapter/SimpleCandidateViewAdapter.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.fcitx.fcitx5.android.input.candidates.adapter - -import android.view.ViewGroup -import androidx.recyclerview.widget.GridLayoutManager -import splitties.dimensions.dp -import splitties.views.dsl.core.wrapContent -import splitties.views.setPaddingDp - -abstract class SimpleCandidateViewAdapter : BaseCandidateViewAdapter() { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - return super.onCreateViewHolder(parent, viewType).apply { - ui.root.apply { - setPaddingDp(8, 0, 8, 0) - minimumWidth = dp(40) - layoutParams = GridLayoutManager.LayoutParams(wrapContent, dp(40)) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/BaseExpandedCandidateWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/BaseExpandedCandidateWindow.kt index be4c39222..bbf6e6ea9 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/BaseExpandedCandidateWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/BaseExpandedCandidateWindow.kt @@ -3,6 +3,7 @@ package org.fcitx.fcitx5.android.input.candidates.expanded.window import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn @@ -44,8 +45,7 @@ abstract class BaseExpandedCandidateWindow> : protected val disableAnimation by AppPrefs.getInstance().advanced.disableAnimation private lateinit var lifecycleCoroutineScope: LifecycleCoroutineScope - protected lateinit var candidateLayout: ExpandedCandidateLayout - private set + private lateinit var candidateLayout: ExpandedCandidateLayout abstract fun onCreateCandidateLayout(): ExpandedCandidateLayout @@ -63,6 +63,7 @@ abstract class BaseExpandedCandidateWindow> : } abstract val adapter: BaseCandidateViewAdapter + abstract val layoutManager: RecyclerView.LayoutManager private var offsetJob: Job? = null diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/FlexboxExpandedCandidateWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/FlexboxExpandedCandidateWindow.kt index 3d218b6eb..a10f00834 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/FlexboxExpandedCandidateWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/FlexboxExpandedCandidateWindow.kt @@ -1,7 +1,6 @@ package org.fcitx.fcitx5.android.input.candidates.expanded.window import android.util.DisplayMetrics -import android.view.ViewGroup import androidx.recyclerview.widget.LinearSmoothScroller import androidx.recyclerview.widget.RecyclerView import com.google.android.flexbox.AlignItems @@ -9,34 +8,34 @@ import com.google.android.flexbox.FlexboxLayoutManager import com.google.android.flexbox.JustifyContent import org.fcitx.fcitx5.android.input.candidates.expanded.ExpandedCandidateLayout import org.fcitx.fcitx5.android.input.candidates.expanded.decoration.FlexboxHorizontalDecoration +import splitties.dimensions.dp +import splitties.views.dsl.core.wrapContent class FlexboxExpandedCandidateWindow : BaseExpandedCandidateWindow() { override val adapter by lazy { - builder.simpleAdapter() + builder.flexAdapter { + FlexboxLayoutManager.LayoutParams(wrapContent, dp(40)).apply { flexGrow = 1f } + } } - val layoutManager: FlexboxLayoutManager - get() = candidateLayout.recyclerView.layoutManager as FlexboxLayoutManager + override val layoutManager by lazy { + FlexboxLayoutManager(context).apply { + justifyContent = JustifyContent.SPACE_AROUND + alignItems = AlignItems.FLEX_START + } + } override fun onCreateCandidateLayout(): ExpandedCandidateLayout = ExpandedCandidateLayout(context, theme).apply { recyclerView.apply { - layoutManager = object : FlexboxLayoutManager(context) { - init { - justifyContent = JustifyContent.SPACE_AROUND - alignItems = AlignItems.FLEX_START - } - - override fun generateLayoutParams(lp: ViewGroup.LayoutParams?) = - LayoutParams(lp).apply { flexGrow = 1f } - } adapter = this@FlexboxExpandedCandidateWindow.adapter + layoutManager = this@FlexboxExpandedCandidateWindow.layoutManager addItemDecoration(FlexboxHorizontalDecoration(builder.dividerDrawable())) addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - (recyclerView.layoutManager as FlexboxLayoutManager).apply { + this@FlexboxExpandedCandidateWindow.layoutManager.apply { pageUpBtn.isEnabled = findFirstCompletelyVisibleItemPosition() != 0 pageDnBtn.isEnabled = findLastCompletelyVisibleItemPosition() != itemCount - 1 diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/GridExpandedCandidateWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/GridExpandedCandidateWindow.kt index 062c759c2..fc2cd4108 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/GridExpandedCandidateWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/expanded/window/GridExpandedCandidateWindow.kt @@ -26,16 +26,17 @@ class GridExpandedCandidateWindow : builder.gridAdapter() } - val layoutManager: GridLayoutManager - get() = candidateLayout.recyclerView.layoutManager as GridLayoutManager + override val layoutManager by lazy { + GridLayoutManager(context, gridSpanCount).apply { + spanSizeLookup = SpanHelper(adapter, this) + } + } override fun onCreateCandidateLayout(): ExpandedCandidateLayout = ExpandedCandidateLayout(context, theme).apply { recyclerView.apply { - layoutManager = GridLayoutManager(context, gridSpanCount).apply { - spanSizeLookup = SpanHelper(this@GridExpandedCandidateWindow.adapter, this) - } adapter = this@GridExpandedCandidateWindow.adapter + layoutManager = this@GridExpandedCandidateWindow.layoutManager addItemDecoration(GridDecoration(builder.dividerDrawable())) addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { From 8ae331e1be17539bb0700548bc64ec24d802962e Mon Sep 17 00:00:00 2001 From: Rocka Date: Wed, 22 Feb 2023 19:15:26 +0800 Subject: [PATCH 025/624] Allow removing English input method --- .../android/ui/main/settings/im/InputMethodListFragment.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/im/InputMethodListFragment.kt b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/im/InputMethodListFragment.kt index 34796d21f..5b27af983 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/im/InputMethodListFragment.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/im/InputMethodListFragment.kt @@ -49,8 +49,6 @@ class InputMethodListFragment : ProgressFragment(), OnItemChangedListener Date: Wed, 22 Feb 2023 19:19:22 +0800 Subject: [PATCH 026/624] Bring back horizontal_candidate_growth prefs Also slightly increase candidate item padding --- .../fcitx5/android/data/prefs/AppPrefs.kt | 2 ++ .../input/candidates/CandidateViewBuilder.kt | 2 +- .../HorizontalCandidateComponent.kt | 33 +++++++++++++------ 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt index 2fb15b96b..6500e839c 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt @@ -175,6 +175,8 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) { keyboardBottomPaddingLandscape = secondary } + val horizontalCandidateGrowth = + switch(R.string.horizontal_candidate_growth, "horizontal_candidate_growth", true) val expandedCandidateStyle = list( R.string.expanded_candidate_style, "expanded_candidate_style", diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/CandidateViewBuilder.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/CandidateViewBuilder.kt index df207d8bf..9fa196e72 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/CandidateViewBuilder.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/CandidateViewBuilder.kt @@ -46,7 +46,7 @@ class CandidateViewBuilder : override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return super.onCreateViewHolder(parent, viewType).apply { ui.root.apply { - setPaddingDp(8, 0, 8, 0) + setPaddingDp(10, 0, 10, 0) minimumWidth = dp(40) } } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/HorizontalCandidateComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/HorizontalCandidateComponent.kt index 8a407706a..2577941c6 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/HorizontalCandidateComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/HorizontalCandidateComponent.kt @@ -20,7 +20,6 @@ import org.fcitx.fcitx5.android.input.dependency.UniqueViewComponent import org.fcitx.fcitx5.android.input.dependency.context import org.mechdancer.dependency.manager.must import splitties.views.dsl.core.wrapContent -import splitties.views.dsl.recyclerview.recyclerView class HorizontalCandidateComponent : UniqueViewComponent(), InputBroadcastReceiver { @@ -29,6 +28,7 @@ class HorizontalCandidateComponent : private val builder: CandidateViewBuilder by manager.must() private val bar: KawaiiBarComponent by manager.must() + private val candidateGrowth by AppPrefs.getInstance().keyboard.horizontalCandidateGrowth private val maxSpanCount by lazy { AppPrefs.getInstance().keyboard.run { if (context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) @@ -38,6 +38,8 @@ class HorizontalCandidateComponent : }.getValue() } + private var layoutViewWidth = 0 + private var layoutViewHeight = 0 private var layoutFlexGrow = 1f /** @@ -65,11 +67,11 @@ class HorizontalCandidateComponent : val adapter: BaseCandidateViewAdapter by lazy { builder.flexAdapter { - val rv = this@HorizontalCandidateComponent.view - FlexboxLayoutManager.LayoutParams(wrapContent, rv.height).apply { - minWidth = (rv.width - dividerDrawable.intrinsicWidth * maxSpanCount) / maxSpanCount + val lp = FlexboxLayoutManager.LayoutParams(wrapContent, layoutViewHeight) + if (candidateGrowth) lp.apply { + minWidth = layoutViewWidth flexGrow = layoutFlexGrow - } + } else lp } } @@ -112,7 +114,14 @@ class HorizontalCandidateComponent : } override val view by lazy { - context.recyclerView(R.id.candidate_view) { + object : RecyclerView(context) { + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + layoutViewWidth = w / maxSpanCount - dividerDrawable.intrinsicWidth + layoutViewHeight = h + } + }.apply { + id = R.id.candidate_view adapter = this@HorizontalCandidateComponent.adapter layoutManager = this@HorizontalCandidateComponent.layoutManager addItemDecoration(FlexboxVerticalDecoration(dividerDrawable)) @@ -120,10 +129,14 @@ class HorizontalCandidateComponent : } override fun onCandidateUpdate(data: Array) { - layoutFlexGrow = if (data.size < maxSpanCount) 0f else 1f - // second layout pass maybe needed when total candidates count < maxSpanCount - secondLayoutPassNeeded = data.size < maxSpanCount - secondLayoutPassDone = false + if (candidateGrowth) { + // second layout pass maybe needed when total candidates count < maxSpanCount + secondLayoutPassNeeded = data.size < maxSpanCount + secondLayoutPassDone = false + layoutFlexGrow = if (secondLayoutPassNeeded) 0f else 1f + } else { + secondLayoutPassNeeded = false + } adapter.updateCandidates(data) } } \ No newline at end of file From 6d6798a1bc20dac16886a24f8136147129e8870c Mon Sep 17 00:00:00 2001 From: Rocka Date: Wed, 22 Feb 2023 20:13:51 +0800 Subject: [PATCH 027/624] Fix EditorInfo inputType variation detection --- .../input/editorinfo/EditorInfoParser.kt | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/editorinfo/EditorInfoParser.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/editorinfo/EditorInfoParser.kt index 92a3a9459..c14b43c36 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/editorinfo/EditorInfoParser.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/editorinfo/EditorInfoParser.kt @@ -7,17 +7,20 @@ import android.text.TextUtils import android.view.inputmethod.EditorInfo import android.view.inputmethod.SurroundingText import splitties.bitflags.hasFlag +import java.lang.reflect.Field import java.lang.reflect.Modifier object EditorInfoParser { + private fun Field.isStatic() = Modifier.isStatic(modifiers) + private val EDITOR_INFO_MEMBER = EditorInfo::class.java.declaredFields - .filter { !Modifier.isStatic(it.modifiers) } + .filter { !it.isStatic() } ////////// EditorInfo constants private val EDITOR_INFO_STATIC = EditorInfo::class.java.declaredFields - .filter { Modifier.isStatic(it.modifiers) } + .filter { it.isStatic() } private val IME_ACTION = EDITOR_INFO_STATIC .filter { it.name.startsWith("IME_ACTION_") } @@ -28,21 +31,21 @@ object EditorInfoParser { ////////// InputType constants private val INPUT_TYPE_STATIC = InputType::class.java.declaredFields - .filter { Modifier.isStatic(it.modifiers) } + .filter { it.isStatic() } private val TYPE_CLASS = INPUT_TYPE_STATIC - .filter { it.name.run { startsWith("TYPE_") && contains("_CLASS_") } } + .filter { it.name.contains("_CLASS_") || it.name == "TYPE_NULL" } private val TYPE_FLAGS = INPUT_TYPE_STATIC - .filter { it.name.run { startsWith("TYPE_") && contains("_FLAG_") } } + .filter { it.name.contains("_FLAG_") } private val TYPE_VARIATION = INPUT_TYPE_STATIC - .filter { it.name.run { startsWith("TYPE_") && contains("_VARIATION_") } } + .filter { it.name.contains("_VARIATION_") } ////////// TextUtils constants private val CAP_MODE = TextUtils::class.java.declaredFields - .filter { Modifier.isStatic(it.modifiers) && it.name.startsWith("CAP_MODE_") } + .filter { it.isStatic() && it.name.startsWith("CAP_MODE_") } private fun parseImeOptions(imeOptions: Int): String { val action = imeOptions and EditorInfo.IME_MASK_ACTION @@ -61,8 +64,9 @@ object EditorInfoParser { .filter { flags.hasFlag(it.getInt(null)) } .joinToString("\n ") { it.name } val variation = inputType and InputType.TYPE_MASK_VARIATION + val variationPrefix = classString.replace("_CLASS", "") val variationString = TYPE_VARIATION - .filter { variation.hasFlag(it.getInt(null)) } + .filter { variation == it.getInt(null) && it.name.startsWith(variationPrefix) } .joinToString("\n ") { it.name } return "class=$classString\nflags=$flagsString\nvariation=$variationString" } From 8ca3fe59167e8c10637e13a99e948e3dc42b8d06 Mon Sep 17 00:00:00 2001 From: Rocka Date: Wed, 22 Feb 2023 20:24:22 +0800 Subject: [PATCH 028/624] Show more spell candidates rather than only 1 page --- app/src/main/cpp/androidkeyboard/androidkeyboard.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/cpp/androidkeyboard/androidkeyboard.cpp b/app/src/main/cpp/androidkeyboard/androidkeyboard.cpp index 5d41ee197..f9c491865 100644 --- a/app/src/main/cpp/androidkeyboard/androidkeyboard.cpp +++ b/app/src/main/cpp/androidkeyboard/androidkeyboard.cpp @@ -250,7 +250,7 @@ void AndroidKeyboardEngine::updateCandidate(const InputMethodEntry &entry, Input results = spell()->call(entry.languageCode(), SpellProvider::Default, state->buffer_.userInput(), - *config_.pageSize); + 20); } auto candidateList = std::make_unique(); for (const auto &result: results) { From b225aebbd32efdbf7268a42e4d0848971800884e Mon Sep 17 00:00:00 2001 From: Rocka Date: Wed, 22 Feb 2023 20:25:35 +0800 Subject: [PATCH 029/624] Add option for showing toolbar number row --- .../org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt | 2 ++ .../fcitx5/android/input/bar/KawaiiBarComponent.kt | 12 +++++++++--- app/src/main/res/values/strings.xml | 1 + 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt index 6500e839c..edd62f7d2 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt @@ -83,6 +83,8 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) { switch(R.string.system_touch_sounds, "system_touch_sounds", true) val expandToolbarByDefault = switch(R.string.expand_toolbar_by_default, "expand_toolbar_by_default", false) + val toolbarNumRowOnPassword = + switch(R.string.toolbar_num_row_on_password, "toolbar_num_row_on_password", true) val popupOnKeyPress = switch(R.string.popup_on_key_press, "popup_on_key_press", true) val keepLettersUppercase = switch( R.string.keep_keyboard_letters_uppercase, diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt index 8daae7f50..c81fc6036 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt @@ -29,7 +29,10 @@ import org.fcitx.fcitx5.android.input.candidates.expanded.ExpandedCandidateStyle import org.fcitx.fcitx5.android.input.candidates.expanded.window.FlexboxExpandedCandidateWindow import org.fcitx.fcitx5.android.input.candidates.expanded.window.GridExpandedCandidateWindow import org.fcitx.fcitx5.android.input.clipboard.ClipboardWindow -import org.fcitx.fcitx5.android.input.dependency.* +import org.fcitx.fcitx5.android.input.dependency.UniqueViewComponent +import org.fcitx.fcitx5.android.input.dependency.context +import org.fcitx.fcitx5.android.input.dependency.inputMethodService +import org.fcitx.fcitx5.android.input.dependency.theme import org.fcitx.fcitx5.android.input.editing.TextEditingWindow import org.fcitx.fcitx5.android.input.keyboard.CommonKeyActionListener import org.fcitx.fcitx5.android.input.keyboard.KeyboardWindow @@ -51,7 +54,6 @@ import timber.log.Timber class KawaiiBarComponent : UniqueViewComponent(), InputBroadcastReceiver { - private val fcitx by manager.fcitx() private val context by manager.context() private val theme by manager.theme() private val windowManager: InputWindowManager by manager.must() @@ -64,6 +66,7 @@ class KawaiiBarComponent : UniqueViewComponent( private val clipboardItemTimeout = AppPrefs.getInstance().clipboard.clipboardItemTimeout private val expandedCandidateStyle by AppPrefs.getInstance().keyboard.expandedCandidateStyle private val expandToolbarByDefault = AppPrefs.getInstance().keyboard.expandToolbarByDefault + private val toolbarNumRowOnPassword by AppPrefs.getInstance().keyboard.toolbarNumRowOnPassword private var clipboardTimeoutJob: Job? = null @@ -290,7 +293,10 @@ class KawaiiBarComponent : UniqueViewComponent( idleUi.privateMode(info.imeOptions.hasFlag(EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING)) } idleUiStateMachine.push(KawaiiBarShown) - barStateMachine.setBooleanState(CapFlagsPassword, capFlags.has(CapabilityFlag.Password)) + barStateMachine.setBooleanState( + CapFlagsPassword, + toolbarNumRowOnPassword && capFlags.has(CapabilityFlag.Password) + ) barStateMachine.push(CapFlagsUpdated) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ca6279ee9..b25341cd0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -196,4 +196,5 @@ Please select both of above files before importing Without the permission to post notifications, we are not able to notify you when some time-consuming operations are done. No Notification Permission + Show number row on toolbar when input password \ No newline at end of file From c16882918481b21100aabb6363cbe381746dcbc5 Mon Sep 17 00:00:00 2001 From: Rocka Date: Wed, 22 Feb 2023 21:06:19 +0800 Subject: [PATCH 030/624] Add lang switch and space key customizations - space_long_press_behavior: none by default - show_lang_switch_key: true by default --- .../fcitx5/android/data/prefs/AppPrefs.kt | 21 +++++++++++++++++++ ...erDialog.kt => InputMethodPickerDialog.kt} | 2 +- .../input/keyboard/CommonKeyActionListener.kt | 18 +++++++++++----- .../android/input/keyboard/KeyAction.kt | 6 ++++-- .../android/input/keyboard/KeyDefPreset.kt | 6 +++--- .../input/keyboard/SpaceLongPressBehavior.kt | 14 +++++++++++++ .../android/input/keyboard/TextKeyboard.kt | 12 +++++++++++ app/src/main/res/values/strings.xml | 6 ++++++ 8 files changed, 74 insertions(+), 11 deletions(-) rename app/src/main/java/org/fcitx/fcitx5/android/input/dialog/{InputMethodSwitcherDialog.kt => InputMethodPickerDialog.kt} (98%) create mode 100644 app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/SpaceLongPressBehavior.kt diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt index edd62f7d2..5a135667a 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt @@ -4,6 +4,7 @@ import android.content.SharedPreferences import android.os.Build import org.fcitx.fcitx5.android.R import org.fcitx.fcitx5.android.input.candidates.expanded.ExpandedCandidateStyle +import org.fcitx.fcitx5.android.input.keyboard.SpaceLongPressBehavior import org.fcitx.fcitx5.android.input.keyboard.SwipeSymbolDirection import org.fcitx.fcitx5.android.input.picker.PickerWindow import org.fcitx.fcitx5.android.utils.getSystemProperty @@ -116,6 +117,26 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) { "ms", 10 ) + val spaceKeyLongPressBehavior = list( + R.string.space_long_press_behavior, + "space_long_press_behavior", + SpaceLongPressBehavior.None, + SpaceLongPressBehavior, + listOf( + SpaceLongPressBehavior.None, + SpaceLongPressBehavior.Enumerate, + SpaceLongPressBehavior.ToggleActivate, + SpaceLongPressBehavior.ShowPicker + ), + listOf( + R.string.space_behavior_none, + R.string.space_behavior_enumerate, + R.string.space_behavior_activate, + R.string.space_behavior_picker + ) + ) + val showLangSwitchKey = + switch(R.string.show_lang_switch_key, "show_lang_switch_key", true) val keyboardHeightPercent: ManagedPreference.PInt val keyboardHeightPercentLandscape: ManagedPreference.PInt diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/dialog/InputMethodSwitcherDialog.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/dialog/InputMethodPickerDialog.kt similarity index 98% rename from app/src/main/java/org/fcitx/fcitx5/android/input/dialog/InputMethodSwitcherDialog.kt rename to app/src/main/java/org/fcitx/fcitx5/android/input/dialog/InputMethodPickerDialog.kt index b6776c4ff..cd8e93d91 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/dialog/InputMethodSwitcherDialog.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/dialog/InputMethodPickerDialog.kt @@ -17,7 +17,7 @@ import splitties.views.dsl.recyclerview.recyclerView import splitties.views.recyclerview.verticalLayoutManager import splitties.views.topPadding -object InputMethodSwitcherDialog { +object InputMethodPickerDialog { suspend fun build( fcitx: FcitxAPI, service: FcitxInputMethodService, diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/CommonKeyActionListener.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/CommonKeyActionListener.kt index e5cf8575b..d49bf1441 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/CommonKeyActionListener.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/CommonKeyActionListener.kt @@ -11,7 +11,7 @@ import org.fcitx.fcitx5.android.input.dependency.fcitx import org.fcitx.fcitx5.android.input.dependency.inputMethodService import org.fcitx.fcitx5.android.input.dependency.inputView import org.fcitx.fcitx5.android.input.dialog.AddMoreInputMethodsPrompt -import org.fcitx.fcitx5.android.input.dialog.InputMethodSwitcherDialog +import org.fcitx.fcitx5.android.input.dialog.InputMethodPickerDialog import org.fcitx.fcitx5.android.input.keyboard.CommonKeyActionListener.BackspaceSwipeState.* import org.fcitx.fcitx5.android.input.keyboard.KeyAction.* import org.fcitx.fcitx5.android.input.picker.PickerWindow @@ -74,15 +74,13 @@ class CommonKeyActionListener : is LangSwitchAction -> { if (it.enabledIme().size < 2) { inputView.showDialog(AddMoreInputMethodsPrompt.build(context)) - } else if (action.enumerate) { - it.enumerateIme() } else { it.toggleIme() } } - is InputMethodSwitchAction -> { + is ShowInputMethodPickerAction -> { inputView.showDialog( - InputMethodSwitcherDialog.build(it, service, context) + InputMethodPickerDialog.build(it, service, context) ) } is MoveSelectionAction -> when (backspaceSwipeState) { @@ -118,6 +116,16 @@ class CommonKeyActionListener : windowManager.attachWindow(key) } } + is SpaceLongPressAction -> { + when (AppPrefs.getInstance().keyboard.spaceKeyLongPressBehavior.getValue()) { + SpaceLongPressBehavior.None -> {} + SpaceLongPressBehavior.Enumerate -> it.enumerateIme() + SpaceLongPressBehavior.ToggleActivate -> it.toggleIme() + SpaceLongPressBehavior.ShowPicker -> inputView.showDialog( + InputMethodPickerDialog.build(it, service, context) + ) + } + } else -> { } } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyAction.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyAction.kt index 071ec94bb..936df4827 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyAction.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyAction.kt @@ -23,9 +23,9 @@ sealed class KeyAction { object UnicodeAction : KeyAction() - class LangSwitchAction(val enumerate: Boolean = true) : KeyAction() + object LangSwitchAction : KeyAction() - object InputMethodSwitchAction : KeyAction() + object ShowInputMethodPickerAction : KeyAction() data class LayoutSwitchAction(val act: String = "") : KeyAction() @@ -34,4 +34,6 @@ sealed class KeyAction { data class DeleteSelectionAction(val totalCnt: Int = 0) : KeyAction() data class PickerSwitchAction(val key: PickerWindow.Key? = null) : KeyAction() + + object SpaceLongPressAction: KeyAction() } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDefPreset.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDefPreset.kt index 1d35d5980..ae46ea864 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDefPreset.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyDefPreset.kt @@ -192,8 +192,8 @@ class LanguageKey : KeyDef( viewId = R.id.button_lang ), setOf( - Behavior.Press(KeyAction.LangSwitchAction()), - Behavior.LongPress(KeyAction.InputMethodSwitchAction) + Behavior.Press(KeyAction.LangSwitchAction), + Behavior.LongPress(KeyAction.ShowInputMethodPickerAction) ) ) @@ -207,7 +207,7 @@ class SpaceKey : KeyDef( ), setOf( Behavior.Press(KeyAction.SymAction(KeySym(FcitxKeyMapping.FcitxKey_space))), - Behavior.LongPress(KeyAction.LangSwitchAction(enumerate = false)) + Behavior.LongPress(KeyAction.SpaceLongPressAction) ) ) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/SpaceLongPressBehavior.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/SpaceLongPressBehavior.kt new file mode 100644 index 000000000..2bb42109b --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/SpaceLongPressBehavior.kt @@ -0,0 +1,14 @@ +package org.fcitx.fcitx5.android.input.keyboard + +import org.fcitx.fcitx5.android.data.prefs.ManagedPreference + +enum class SpaceLongPressBehavior { + None, + Enumerate, + ToggleActivate, + ShowPicker; + + companion object : ManagedPreference.StringLikeCodec { + override fun decode(raw: String): SpaceLongPressBehavior = valueOf(raw) + } +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt index d5e58ac8e..a38340ec5 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt @@ -2,6 +2,7 @@ package org.fcitx.fcitx5.android.input.keyboard import android.annotation.SuppressLint import android.content.Context +import android.view.View import androidx.core.view.allViews import org.fcitx.fcitx5.android.R import org.fcitx.fcitx5.android.core.InputMethodEntry @@ -77,12 +78,19 @@ class TextKeyboard( val space: TextKeyView by lazy { findViewById(R.id.button_space) } val `return`: ImageKeyView by lazy { findViewById(R.id.button_return) } + private val showLangSwitchKey = AppPrefs.getInstance().keyboard.showLangSwitchKey + private val showLangSwitchKeyListener = ManagedPreference.OnChangeListener { _, v -> + updateLangSwitchKey(v) + } + private val keepLettersUppercase = AppPrefs.getInstance().keyboard.keepLettersUppercase private val keepLettersUppercaseListener = ManagedPreference.OnChangeListener { _, _ -> updateAlphabetKeys() } init { + updateLangSwitchKey(showLangSwitchKey.getValue()) + showLangSwitchKey.registerOnChangeListener(showLangSwitchKeyListener) keepLettersUppercase.registerOnChangeListener(keepLettersUppercaseListener) } @@ -187,6 +195,10 @@ class TextKeyboard( } } + private fun updateLangSwitchKey(visible: Boolean) { + lang.visibility = if (visible) View.VISIBLE else View.GONE + } + private fun updateAlphabetKeys() { textKeys.forEach { if (it.def !is KeyDef.Appearance.AltText) return diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b25341cd0..5e3d280c7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -197,4 +197,10 @@ Without the permission to post notifications, we are not able to notify you when some time-consuming operations are done. No Notification Permission Show number row on toolbar when input password + Space key long press behavior + None + Enumerate input methods + Toggle input method activate state + Show input method picker dialog + Show language switch key \ No newline at end of file From fe2a33c1140d64b00450aab5d41ccb96ca5f5494 Mon Sep 17 00:00:00 2001 From: Rocka Date: Wed, 22 Feb 2023 21:07:31 +0800 Subject: [PATCH 031/624] Reset caps lock state on TextKeyboard attach --- .../java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt index a38340ec5..e7a216392 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt @@ -136,6 +136,7 @@ class TextKeyboard( } override fun onAttach() { + capsState = CapsState.None; updateCapsButtonIcon() updateAlphabetKeys() } From dc57ffde412e85cd1c1c334c553dfd28420ca561 Mon Sep 17 00:00:00 2001 From: Rocka Date: Wed, 22 Feb 2023 21:09:18 +0800 Subject: [PATCH 032/624] Update fcitx5 submodules --- app/src/main/cpp/fcitx5-chinese-addons | 2 +- app/src/main/cpp/libime | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/cpp/fcitx5-chinese-addons b/app/src/main/cpp/fcitx5-chinese-addons index 1d541b2fd..2e4c7c9f6 160000 --- a/app/src/main/cpp/fcitx5-chinese-addons +++ b/app/src/main/cpp/fcitx5-chinese-addons @@ -1 +1 @@ -Subproject commit 1d541b2fd65edb2b737a6f5e219bd2e3c2aaf7e1 +Subproject commit 2e4c7c9f60cb78323ac29725f1ae2d67c6ced11a diff --git a/app/src/main/cpp/libime b/app/src/main/cpp/libime index 803b35595..1160455a9 160000 --- a/app/src/main/cpp/libime +++ b/app/src/main/cpp/libime @@ -1 +1 @@ -Subproject commit 803b355954a08325db06b34bc9832f2f1fa235f8 +Subproject commit 1160455a9078aa77c0d8b52db67a20eed3e16196 From 42bd8da29e8769c5a8de3dcc5c39c2e4fff0eb22 Mon Sep 17 00:00:00 2001 From: Rocka Date: Wed, 22 Feb 2023 23:58:52 +0800 Subject: [PATCH 033/624] Minor bug fixes - enumerate input methods on lang switch key press - notify BaseKeyboard on KeyboardWindow attach/detach --- .../fcitx5/android/input/keyboard/CommonKeyActionListener.kt | 2 +- .../org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/CommonKeyActionListener.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/CommonKeyActionListener.kt index d49bf1441..a99820957 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/CommonKeyActionListener.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/CommonKeyActionListener.kt @@ -75,7 +75,7 @@ class CommonKeyActionListener : if (it.enabledIme().size < 2) { inputView.showDialog(AddMoreInputMethodsPrompt.build(context)) } else { - it.toggleIme() + it.enumerateIme() } } is ShowInputMethodPickerAction -> { diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt index 93e762176..c1647588b 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt @@ -160,12 +160,14 @@ class KeyboardWindow : InputWindow.SimpleInputWindow(), Essentia currentKeyboard?.let { it.keyActionListener = keyActionListener it.popupActionListener = popupActionListener + it.onAttach() } notifyBarLayoutChanged() } override fun onDetached() { currentKeyboard?.let { + it.onDetach() it.keyActionListener = null it.popupActionListener = null } From b83bfc12b937e51697bc14b0dc938733489bed2c Mon Sep 17 00:00:00 2001 From: Rocka Date: Wed, 22 Feb 2023 23:59:11 +0800 Subject: [PATCH 034/624] Update fcitx5 submodules --- app/src/main/cpp/fcitx5-chinese-addons | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/cpp/fcitx5-chinese-addons b/app/src/main/cpp/fcitx5-chinese-addons index 2e4c7c9f6..61a662c35 160000 --- a/app/src/main/cpp/fcitx5-chinese-addons +++ b/app/src/main/cpp/fcitx5-chinese-addons @@ -1 +1 @@ -Subproject commit 2e4c7c9f60cb78323ac29725f1ae2d67c6ced11a +Subproject commit 61a662c35050bc7aca85c684d7659ec1b3f3eff8 From 1a5a38f37c481d8ed2269981e214ecb2fb9bdd56 Mon Sep 17 00:00:00 2001 From: Rocka Date: Thu, 23 Feb 2023 19:44:19 +0800 Subject: [PATCH 035/624] Add horizontal_candidate_style prefs - limit max candidates count shown in horizontal recycler view - 3 styles: never fill width, auto (fill width when can fit in max limit), always fill width --- .../fcitx5/android/data/prefs/AppPrefs.kt | 19 ++++- .../HorizontalCandidateComponent.kt | 76 +++++++++++-------- .../candidates/HorizontalCandidateMode.kt | 13 ++++ .../adapter/BaseCandidateViewAdapter.kt | 14 +++- app/src/main/res/values/strings.xml | 4 + 5 files changed, 92 insertions(+), 34 deletions(-) create mode 100644 app/src/main/java/org/fcitx/fcitx5/android/input/candidates/HorizontalCandidateMode.kt diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt index 5a135667a..dc7158f58 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt @@ -3,6 +3,7 @@ package org.fcitx.fcitx5.android.data.prefs import android.content.SharedPreferences import android.os.Build import org.fcitx.fcitx5.android.R +import org.fcitx.fcitx5.android.input.candidates.HorizontalCandidateMode import org.fcitx.fcitx5.android.input.candidates.expanded.ExpandedCandidateStyle import org.fcitx.fcitx5.android.input.keyboard.SpaceLongPressBehavior import org.fcitx.fcitx5.android.input.keyboard.SwipeSymbolDirection @@ -198,8 +199,22 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) { keyboardBottomPaddingLandscape = secondary } - val horizontalCandidateGrowth = - switch(R.string.horizontal_candidate_growth, "horizontal_candidate_growth", true) + val horizontalCandidateStyle = list( + R.string.horizontal_candidate_style, + "horizontal_candidate_style", + HorizontalCandidateMode.AutoFillWidth, + HorizontalCandidateMode, + listOf( + HorizontalCandidateMode.NeverFillWidth, + HorizontalCandidateMode.AutoFillWidth, + HorizontalCandidateMode.AlwaysFillWidth, + ), + listOf( + R.string.horizontal_candidate_never_fill, + R.string.horizontal_candidate_auto_fill, + R.string.horizontal_candidate_always_fill + ) + ) val expandedCandidateStyle = list( R.string.expanded_candidate_style, "expanded_candidate_style", diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/HorizontalCandidateComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/HorizontalCandidateComponent.kt index 2577941c6..713479af1 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/HorizontalCandidateComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/HorizontalCandidateComponent.kt @@ -14,11 +14,13 @@ import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.BooleanKey.Ca import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.TransitionEvent.ExpandedCandidatesUpdated import org.fcitx.fcitx5.android.input.bar.KawaiiBarComponent import org.fcitx.fcitx5.android.input.broadcast.InputBroadcastReceiver +import org.fcitx.fcitx5.android.input.candidates.HorizontalCandidateMode.* import org.fcitx.fcitx5.android.input.candidates.adapter.BaseCandidateViewAdapter import org.fcitx.fcitx5.android.input.candidates.expanded.decoration.FlexboxVerticalDecoration import org.fcitx.fcitx5.android.input.dependency.UniqueViewComponent import org.fcitx.fcitx5.android.input.dependency.context import org.mechdancer.dependency.manager.must +import splitties.views.dsl.core.matchParent import splitties.views.dsl.core.wrapContent class HorizontalCandidateComponent : @@ -28,24 +30,24 @@ class HorizontalCandidateComponent : private val builder: CandidateViewBuilder by manager.must() private val bar: KawaiiBarComponent by manager.must() - private val candidateGrowth by AppPrefs.getInstance().keyboard.horizontalCandidateGrowth - private val maxSpanCount by lazy { + private val fillStyle by AppPrefs.getInstance().keyboard.horizontalCandidateStyle + private val maxSpanCountPref by lazy { AppPrefs.getInstance().keyboard.run { if (context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) expandedCandidateGridSpanCount else expandedCandidateGridSpanCountLandscape - }.getValue() + } } - private var layoutViewWidth = 0 - private var layoutViewHeight = 0 + private var layoutMinWidth = 0 private var layoutFlexGrow = 1f /** - * Second layout pass is needed when total candidates count < [maxSpanCount], but the - * RecyclerView cannot display all of them. In that case, displayed candidates should be - * stretched evenly (by setting flexGrow to 1.0f). + * (for [HorizontalCandidateMode.AutoFillWidth] only) + * Second layout pass is needed when: + * [^1] total candidates count < maxSpanCount && [^2] RecyclerView cannot display all of them + * In that case, displayed candidates should be stretched evenly (by setting flexGrow to 1.0f). */ private var secondLayoutPassNeeded = false private var secondLayoutPassDone = false @@ -67,28 +69,24 @@ class HorizontalCandidateComponent : val adapter: BaseCandidateViewAdapter by lazy { builder.flexAdapter { - val lp = FlexboxLayoutManager.LayoutParams(wrapContent, layoutViewHeight) - if (candidateGrowth) lp.apply { - minWidth = layoutViewWidth + FlexboxLayoutManager.LayoutParams(wrapContent, matchParent).apply { + minWidth = layoutMinWidth flexGrow = layoutFlexGrow - } else lp + } } } - val layoutManager by lazy { + val layoutManager: FlexboxLayoutManager by lazy { object : FlexboxLayoutManager(context) { override fun canScrollVertically() = false override fun canScrollHorizontally() = false - override fun onLayoutCompleted(state: RecyclerView.State?) { + override fun onLayoutCompleted(state: RecyclerView.State) { super.onLayoutCompleted(state) if (secondLayoutPassNeeded) { - if (childCount >= adapter.candidates.size) { - // second layout pass is not actually need, - // because RecyclerView can display all the candidates - secondLayoutPassNeeded = false - } else { - // second layout pass would trigger onLayoutCompleted as well, - // just skip second onLayoutCompleted + if (childCount < adapter.candidates.size) { + // [^2] RecyclerView can't display all candidates + // update LayoutParams in onLayoutCompleted would trigger another + // onLayoutCompleted, skip the second one to avoid infinite loop if (secondLayoutPassDone) return secondLayoutPassDone = true for (i in 0 until childCount) { @@ -96,6 +94,8 @@ class HorizontalCandidateComponent : flexGrow = 1f } } + } else { + secondLayoutPassNeeded = false } } refreshExpanded() @@ -117,8 +117,10 @@ class HorizontalCandidateComponent : object : RecyclerView(context) { override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) - layoutViewWidth = w / maxSpanCount - dividerDrawable.intrinsicWidth - layoutViewHeight = h + if (fillStyle == AutoFillWidth) { + val maxSpanCount = maxSpanCountPref.getValue() + layoutMinWidth = w / maxSpanCount - dividerDrawable.intrinsicWidth + } } }.apply { id = R.id.candidate_view @@ -129,14 +131,26 @@ class HorizontalCandidateComponent : } override fun onCandidateUpdate(data: Array) { - if (candidateGrowth) { - // second layout pass maybe needed when total candidates count < maxSpanCount - secondLayoutPassNeeded = data.size < maxSpanCount - secondLayoutPassDone = false - layoutFlexGrow = if (secondLayoutPassNeeded) 0f else 1f - } else { - secondLayoutPassNeeded = false + val maxSpanCount = maxSpanCountPref.getValue() + when (fillStyle) { + NeverFillWidth -> { + layoutMinWidth = 0 + layoutFlexGrow = 0f + secondLayoutPassNeeded = false + } + AutoFillWidth -> { + layoutMinWidth = view.width / maxSpanCount - dividerDrawable.intrinsicWidth + layoutFlexGrow = if (secondLayoutPassNeeded) 0f else 1f + // [^1] total candidates count < maxSpanCount + secondLayoutPassNeeded = data.size < maxSpanCount + secondLayoutPassDone = false + } + AlwaysFillWidth -> { + layoutMinWidth = 0 + layoutFlexGrow = 1f + secondLayoutPassNeeded = false + } } - adapter.updateCandidates(data) + adapter.updateCandidatesWithLimit(data, maxSpanCount) } } \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/HorizontalCandidateMode.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/HorizontalCandidateMode.kt new file mode 100644 index 000000000..700bdfa8c --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/HorizontalCandidateMode.kt @@ -0,0 +1,13 @@ +package org.fcitx.fcitx5.android.input.candidates + +import org.fcitx.fcitx5.android.data.prefs.ManagedPreference + +enum class HorizontalCandidateMode { + NeverFillWidth, + AutoFillWidth, + AlwaysFillWidth; + + companion object : ManagedPreference.StringLikeCodec { + override fun decode(raw: String): HorizontalCandidateMode = valueOf(raw) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/adapter/BaseCandidateViewAdapter.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/adapter/BaseCandidateViewAdapter.kt index a955ce095..2f5f1b49f 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/adapter/BaseCandidateViewAdapter.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/adapter/BaseCandidateViewAdapter.kt @@ -19,6 +19,9 @@ abstract class BaseCandidateViewAdapter : var offset = 0 private set + var limit = -1 + private set + fun getCandidateAt(position: Int) = candidates[offset + position] @SuppressLint("NotifyDataSetChanged") @@ -32,6 +35,11 @@ abstract class BaseCandidateViewAdapter : updateCandidates(data) } + fun updateCandidatesWithLimit(data: Array, limit: Int) { + this.limit = limit + updateCandidates(data) + } + @CallSuper override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder(CandidateItemUi(parent.context, theme)).apply { @@ -47,7 +55,11 @@ abstract class BaseCandidateViewAdapter : abstract val theme: Theme - override fun getItemCount() = candidates.size - offset + override fun getItemCount(): Int { + val offsetSize = candidates.size - offset + if (limit < 0) return offsetSize + return if (limit > offsetSize) offsetSize else limit + } abstract fun onSelect(idx: Int) } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5e3d280c7..27de9da20 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -203,4 +203,8 @@ Toggle input method activate state Show input method picker dialog Show language switch key + Horizontal candidate style + Never fill width + Fill width on demand + Always fill width \ No newline at end of file From f5262de7e70393fd37ef8ff906e9847f61202837 Mon Sep 17 00:00:00 2001 From: Rocka Date: Thu, 23 Feb 2023 22:03:12 +0800 Subject: [PATCH 036/624] Fix libevent's cmake config --- app/src/main/cpp/CMakeLists.txt | 5 ----- app/src/main/cpp/prebuilt | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 2a1b29de1..7708bf165 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -35,11 +35,6 @@ find_package(LibIntl) # prebuilt libevent set(Libevent_DIR "${PREBUILT_DIR}/libevent/${ANDROID_ABI}/lib/cmake/libevent") set(LIBEVENT_STATIC_LINK ON) -# LibeventTargets-static.cmake uses find_path and find_library, but NDK's android{,-legacy}.toolchain.cmake -# set CMAKE_SYSROOT and CMAKE_FIND_ROOT_PATH cause find_* commands to re-root input paths. -# turn off re-root behavior because we want to find libevent in prebuilt instead of NDK sysroot -set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE NEVER) -set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY NEVER) find_package(Libevent) option(ENABLE_TEST "" OFF) diff --git a/app/src/main/cpp/prebuilt b/app/src/main/cpp/prebuilt index 95021a13e..f895c6853 160000 --- a/app/src/main/cpp/prebuilt +++ b/app/src/main/cpp/prebuilt @@ -1 +1 @@ -Subproject commit 95021a13ed23dec90ce42de6e6076c544df8515f +Subproject commit f895c6853d235e309bad6e69c3384edd31f616b7 From e85e5df0ff5e44eee4d526e709974154e545897d Mon Sep 17 00:00:00 2001 From: Rocka Date: Fri, 24 Feb 2023 09:23:22 +0800 Subject: [PATCH 037/624] Update fcitx5 submodules --- app/src/main/cpp/fcitx5-chinese-addons | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/cpp/fcitx5-chinese-addons b/app/src/main/cpp/fcitx5-chinese-addons index 61a662c35..3d0b57870 160000 --- a/app/src/main/cpp/fcitx5-chinese-addons +++ b/app/src/main/cpp/fcitx5-chinese-addons @@ -1 +1 @@ -Subproject commit 61a662c35050bc7aca85c684d7659ec1b3f3eff8 +Subproject commit 3d0b5787095043ad49aaa49a90c3430b7b641879 From 9a64ff86508bc210db2e8d2103db930afbec2eff Mon Sep 17 00:00:00 2001 From: Rocka Date: Fri, 24 Feb 2023 09:32:53 +0800 Subject: [PATCH 038/624] Suppress clang invalid-source-encoding warning for unikey --- app/src/main/cpp/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 7708bf165..352f644b6 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -96,6 +96,8 @@ set_target_properties(scel2org5 PROPERTIES OUTPUT_NAME libscel2org5.so) option(ENABLE_TEST "" OFF) option(ENABLE_QT "" OFF) add_subdirectory(fcitx5-unikey) +# suppress "illegal character encoding in character literal" warning in unikey/data.cpp +target_compile_options(unikey-lib PRIVATE "-Wno-invalid-source-encoding") add_library(native-lib SHARED native-lib.cpp) target_link_libraries(native-lib From 5dc2998e42aeb2ca06e00915b864231be7b8e3fb Mon Sep 17 00:00:00 2001 From: Rocka Date: Fri, 24 Feb 2023 15:36:48 +0800 Subject: [PATCH 039/624] Fix horizontal candidate would always fill width in AutoFillWidth style --- .../android/input/candidates/HorizontalCandidateComponent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/HorizontalCandidateComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/HorizontalCandidateComponent.kt index 713479af1..eddbbf5b7 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/HorizontalCandidateComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/HorizontalCandidateComponent.kt @@ -140,7 +140,7 @@ class HorizontalCandidateComponent : } AutoFillWidth -> { layoutMinWidth = view.width / maxSpanCount - dividerDrawable.intrinsicWidth - layoutFlexGrow = if (secondLayoutPassNeeded) 0f else 1f + layoutFlexGrow = if (data.size < maxSpanCount) 0f else 1f // [^1] total candidates count < maxSpanCount secondLayoutPassNeeded = data.size < maxSpanCount secondLayoutPassDone = false From 767870ef5ffc057dfa2dd7a2939bef7f1c8521e8 Mon Sep 17 00:00:00 2001 From: Rocka Date: Fri, 24 Feb 2023 15:40:07 +0800 Subject: [PATCH 040/624] Merge native libraries, remove JNA --- app/build.gradle.kts | 1 - app/src/main/cpp/CMakeLists.txt | 13 ++--- app/src/main/cpp/native-lib.cpp | 53 +++++++++++++++++++ app/src/main/cpp/pinyindictionaryutils.cpp | 23 -------- app/src/main/cpp/tabledictionaryutils.cpp | 38 ------------- app/src/main/cpp/utf8utils.cpp | 6 --- .../data/clipboard/ClipboardManager.kt | 3 +- .../android/data/pinyin/PinyinDictManager.kt | 3 -- .../fcitx5/android/data/table/TableManager.kt | 4 -- .../fcitx/fcitx5/android/utils/UTF8Utils.kt | 13 ----- .../org/fcitx/fcitx5/android/utils/Utils.kt | 24 --------- 11 files changed, 57 insertions(+), 124 deletions(-) delete mode 100644 app/src/main/cpp/pinyindictionaryutils.cpp delete mode 100644 app/src/main/cpp/tabledictionaryutils.cpp delete mode 100644 app/src/main/cpp/utf8utils.cpp delete mode 100644 app/src/main/java/org/fcitx/fcitx5/android/utils/UTF8Utils.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8abe71d59..78039c039 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -311,7 +311,6 @@ dependencies { implementation("androidx.room:room-runtime:$roomVersion") ksp("androidx.room:room-compiler:$roomVersion") implementation("androidx.room:room-ktx:$roomVersion") - implementation("net.java.dev.jna:jna:5.13.0@aar") implementation("com.jakewharton.timber:timber:5.0.1") implementation("androidx.core:core-ktx:1.9.0") implementation("androidx.appcompat:appcompat:1.6.0") diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 352f644b6..e26f3fc42 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -108,13 +108,6 @@ target_link_libraries(native-lib Fcitx5::Core Fcitx5::Module::QuickPhrase Fcitx5::Module::Unicode - Fcitx5::Module::Clipboard) - -add_library(pinyindictionaryutils SHARED pinyindictionaryutils.cpp) -target_link_libraries(pinyindictionaryutils LibIME::Pinyin) - -add_library(utf8utils SHARED utf8utils.cpp) -target_link_libraries(utf8utils Fcitx5::Utils) - -add_library(tabledictionaryutils SHARED tabledictionaryutils.cpp) -target_link_libraries(tabledictionaryutils LibIME::Table) + Fcitx5::Module::Clipboard + LibIME::Pinyin + LibIME::Table) diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp index 49df7b4ac..90fc77a35 100644 --- a/app/src/main/cpp/native-lib.cpp +++ b/app/src/main/cpp/native-lib.cpp @@ -2,6 +2,7 @@ #include #include +#include #include @@ -24,6 +25,9 @@ #include #include +#include +#include + #include "androidfrontend/androidfrontend_public.h" #include "jni-utils.h" #include "nativestreambuf.h" @@ -954,3 +958,52 @@ Java_org_fcitx_fcitx5_android_core_Key_create(JNIEnv *env, jclass clazz, jint sy *JString(env, key.toString(fcitx::KeyStringFormat::Localized)) ); } + +extern "C" +JNIEXPORT void JNICALL +Java_org_fcitx_fcitx5_android_data_pinyin_PinyinDictManager_pinyinDictConv(JNIEnv *env, jclass clazz, jstring src, jstring dest, jboolean mode) { + using namespace libime; + PinyinDictionary dict; + try { + dict.load(PinyinDictionary::SystemDict, *CString(env, src), + mode == JNI_TRUE ? PinyinDictFormat::Binary : PinyinDictFormat::Text); + std::ofstream out; + out.open(*CString(env, dest), std::ios::out | std::ios::binary); + dict.save(PinyinDictionary::SystemDict, out, + mode == JNI_TRUE ? PinyinDictFormat::Text : PinyinDictFormat::Binary); + } catch (const std::exception &e) { + throwJavaException(env, e.what()); + } +} + +extern "C" +JNIEXPORT void JNICALL +Java_org_fcitx_fcitx5_android_data_table_TableManager_tableDictConv(JNIEnv *env, jclass clazz, jstring src, jstring dest, jboolean mode) { + using namespace libime; + TableBasedDictionary dict; + try { + dict.load(*CString(env, src), mode == JNI_TRUE ? TableFormat::Binary : TableFormat::Text); + std::ofstream out; + out.open(*CString(env, dest), std::ios::out | std::ios::binary); + dict.save(out, mode == JNI_TRUE ? TableFormat::Text : TableFormat::Binary); + } catch (const std::exception &e) { + throwJavaException(env, e.what()); + } +} + +extern "C" +JNIEXPORT jboolean JNICALL +Java_org_fcitx_fcitx5_android_data_table_TableManager_checkTableDictFormat(JNIEnv *env, jclass clazz, jstring src, jboolean user) { + using namespace libime; + TableBasedDictionary dict; + try { + if (user == JNI_TRUE) { + dict.loadUser(CString(env, src), TableFormat::Binary); + } else { + dict.load(*CString(env, src), TableFormat::Binary); + } + } catch (const std::exception &e) { + throwJavaException(env, e.what()); + } + return JNI_TRUE; +} diff --git a/app/src/main/cpp/pinyindictionaryutils.cpp b/app/src/main/cpp/pinyindictionaryutils.cpp deleted file mode 100644 index b2beb5109..000000000 --- a/app/src/main/cpp/pinyindictionaryutils.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include -#include - -#include "jni-utils.h" - -#include "libime/pinyin/pinyindictionary.h" - -extern "C" -JNIEXPORT void JNICALL -Java_org_fcitx_fcitx5_android_data_pinyin_PinyinDictManager_pinyinDictConv(JNIEnv *env, jclass clazz, jstring src, jstring dest, jboolean mode) { - using namespace libime; - PinyinDictionary dict; - try { - dict.load(PinyinDictionary::SystemDict, *CString(env, src), - mode == JNI_TRUE ? PinyinDictFormat::Binary : PinyinDictFormat::Text); - std::ofstream out; - out.open(*CString(env, dest), std::ios::out | std::ios::binary); - dict.save(PinyinDictionary::SystemDict, out, - mode == JNI_TRUE ? PinyinDictFormat::Text : PinyinDictFormat::Binary); - } catch (const std::exception &e) { - throwJavaException(env, e.what()); - } -} \ No newline at end of file diff --git a/app/src/main/cpp/tabledictionaryutils.cpp b/app/src/main/cpp/tabledictionaryutils.cpp deleted file mode 100644 index ed2be1095..000000000 --- a/app/src/main/cpp/tabledictionaryutils.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include -#include - -#include "jni-utils.h" - -#include "libime/table/tablebaseddictionary.h" - -extern "C" -JNIEXPORT void JNICALL -Java_org_fcitx_fcitx5_android_data_table_TableManager_tableDictConv(JNIEnv *env, jclass clazz, jstring src, jstring dest, jboolean mode) { - using namespace libime; - TableBasedDictionary dict; - try { - dict.load(*CString(env, src), mode == JNI_TRUE ? TableFormat::Binary : TableFormat::Text); - std::ofstream out; - out.open(*CString(env, dest), std::ios::out | std::ios::binary); - dict.save(out, mode == JNI_TRUE ? TableFormat::Text : TableFormat::Binary); - } catch (const std::exception &e) { - throwJavaException(env, e.what()); - } -} - -extern "C" -JNIEXPORT jboolean JNICALL -Java_org_fcitx_fcitx5_android_data_table_TableManager_checkTableDictFormat(JNIEnv *env, jclass clazz, jstring src, jboolean user) { - using namespace libime; - TableBasedDictionary dict; - try { - if (user == JNI_TRUE) { - dict.loadUser(CString(env, src), TableFormat::Binary); - } else { - dict.load(*CString(env, src), TableFormat::Binary); - } - } catch (const std::exception &e) { - throwJavaException(env, e.what()); - } - return JNI_TRUE; -} diff --git a/app/src/main/cpp/utf8utils.cpp b/app/src/main/cpp/utf8utils.cpp deleted file mode 100644 index 596c57b98..000000000 --- a/app/src/main/cpp/utf8utils.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#include "fcitx-utils/utf8.h" - -extern "C" bool validateUTF8(const char *s) { - std::string str = s; - return fcitx::utf8::validate(str); -} \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/ClipboardManager.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/ClipboardManager.kt index 0b9077349..df81846f1 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/ClipboardManager.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/ClipboardManager.kt @@ -11,7 +11,6 @@ import org.fcitx.fcitx5.android.data.clipboard.db.ClipboardDatabase import org.fcitx.fcitx5.android.data.clipboard.db.ClipboardEntry import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.data.prefs.ManagedPreference -import org.fcitx.fcitx5.android.utils.UTF8Utils import org.fcitx.fcitx5.android.utils.WeakHashSet import splitties.systemservices.clipboardManager @@ -114,7 +113,7 @@ object ClipboardManager : ClipboardManager.OnPrimaryClipChangedListener, override fun onPrimaryClipChanged() { clipboardManager.primaryClip ?.let { ClipboardEntry.fromClipData(it) } - ?.takeIf { it.text.isNotBlank() && UTF8Utils.instance.validateUTF8(it.text) } + ?.takeIf { it.text.isNotBlank() } ?.let { e -> launch { mutex.withLock { diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/pinyin/PinyinDictManager.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/pinyin/PinyinDictManager.kt index a4011f013..18e56243b 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/data/pinyin/PinyinDictManager.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/pinyin/PinyinDictManager.kt @@ -11,9 +11,6 @@ import java.io.IOException import java.io.InputStream object PinyinDictManager { - init { - System.loadLibrary("pinyindictionaryutils") - } private val pinyinDicDir = File( appContext.getExternalFilesDir(null)!!, "data/pinyin/dictionaries" diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/table/TableManager.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/table/TableManager.kt index a12626036..5bba55b6b 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/data/table/TableManager.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/table/TableManager.kt @@ -13,10 +13,6 @@ import java.util.zip.ZipInputStream object TableManager { - init { - System.loadLibrary("tabledictionaryutils") - } - private val inputMethodDir = File( appContext.getExternalFilesDir(null)!!, "data/inputmethod" ).also { it.mkdirs() } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/utils/UTF8Utils.kt b/app/src/main/java/org/fcitx/fcitx5/android/utils/UTF8Utils.kt deleted file mode 100644 index 3cb59de68..000000000 --- a/app/src/main/java/org/fcitx/fcitx5/android/utils/UTF8Utils.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.fcitx.fcitx5.android.utils - -import com.sun.jna.Library - -interface UTF8Utils : Library { - - fun validateUTF8(str: String): Boolean - - companion object { - val instance: UTF8Utils by nativeLib("utf8utils") - } - -} \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/utils/Utils.kt b/app/src/main/java/org/fcitx/fcitx5/android/utils/Utils.kt index 943e6e570..a704d4ba9 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/utils/Utils.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/utils/Utils.kt @@ -4,7 +4,6 @@ import android.annotation.SuppressLint import android.app.Activity import android.content.ContentResolver import android.content.Context -import android.content.ContextWrapper import android.content.res.Configuration import android.graphics.Color import android.graphics.ColorFilter @@ -26,7 +25,6 @@ import android.widget.Toast import androidx.annotation.AttrRes import androidx.annotation.IdRes import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.graphics.ColorUtils import androidx.core.view.ViewCompat @@ -38,14 +36,10 @@ import androidx.navigation.NavController import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 import arrow.core.toOption -import com.sun.jna.Library -import com.sun.jna.Native import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.fcitx.fcitx5.android.FcitxApplication import org.fcitx.fcitx5.android.R -import org.fcitx.fcitx5.android.core.CapabilityFlag -import org.fcitx.fcitx5.android.input.FcitxInputMethodService import splitties.experimental.InternalSplittiesApi import splitties.resources.withResolvedThemeAttribute import splitties.views.bottomPadding @@ -80,10 +74,6 @@ fun Uri.queryFileName(contentResolver: ContentResolver) = it.getString(index) }.toOption() -inline fun nativeLib(name: String): Lazy = lazy { - Native.load(name, T::class.java) -} - val EditText.str: String get() = editableText.toString() @OptIn(InternalSplittiesApi::class) @@ -252,17 +242,6 @@ fun ZipInputStream.extract(destDir: File): List { return extracted } -fun Context.getHostActivity(): AppCompatActivity? { - var context = this - while (context is ContextWrapper) { - if (context is AppCompatActivity) - return context - else - context = context.baseContext - } - return null -} - inline fun withTempDir(block: (File) -> T): T { // creates /data/user///cache/ val dir = File(createTempDirectory().pathString) @@ -272,6 +251,3 @@ inline fun withTempDir(block: (File) -> T): T { dir.delete() } } - -val FcitxInputMethodService.isInPasswordMode - get() = capabilityFlags.has(CapabilityFlag.Password) \ No newline at end of file From 9c8e234ce41ead46f5e24c450a5c2fe32db0dc65 Mon Sep 17 00:00:00 2001 From: Rocka Date: Fri, 24 Feb 2023 15:48:47 +0800 Subject: [PATCH 041/624] Migrate to mikepenz/AboutLibraries & update dependencies --- app/build.gradle.kts | 20 +- app/licenses.yml | 585 ------------------ .../org/fcitx/fcitx5/android/data/Licenses.kt | 53 -- .../android/ui/main/LicensesFragment.kt | 39 +- build.gradle.kts | 3 +- 5 files changed, 41 insertions(+), 659 deletions(-) delete mode 100644 app/licenses.yml delete mode 100644 app/src/main/java/org/fcitx/fcitx5/android/data/Licenses.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 78039c039..9df05ad92 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,10 +20,10 @@ fun exec(cmd: String): String = ByteArrayOutputStream().use { plugins { id("com.android.application") kotlin("android") - id("com.google.devtools.ksp") version "1.8.0-1.0.8" - id("com.cookpad.android.plugin.license-tools") version "1.2.8" - kotlin("plugin.serialization") version "1.8.0" kotlin("plugin.parcelize") + kotlin("plugin.serialization") version "1.8.0" + id("com.google.devtools.ksp") version "1.8.0-1.0.8" + id("com.mikepenz.aboutlibraries.plugin") } val dataDescriptorName = "descriptor.json" @@ -163,6 +163,13 @@ kotlin { } } +aboutLibraries { + excludeFields = arrayOf("metadata", "funding", "description", "scm", "website", "developers", "name") + fetchRemoteLicense = false + fetchRemoteFunding = false + includePlatform = false +} + kotlin.sourceSets.all { languageSettings.optIn("kotlin.RequiresOptIn") } @@ -195,7 +202,6 @@ val generateDataDescriptor by tasks.register("generateDataDe inputDir.set(file("src/main/assets")) outputFile.set(file("src/main/assets/${dataDescriptorName}")) dependsOn(installFcitxComponent) - dependsOn(tasks.findByName("generateLicenseJson")) } /** * Note *Graph* @@ -289,7 +295,6 @@ tasks.register("cleanGeneratedAssets") { } }) delete(file("src/main/assets/${dataDescriptorName}")) - delete(file("src/main/assets/licenses.json")) }.also { tasks.clean.dependsOn(it) } tasks.register("cleanCxxIntermediates") { @@ -299,7 +304,7 @@ tasks.register("cleanCxxIntermediates") { dependencies { implementation("org.ini4j:ini4j:0.5.4") ksp(project(":codegen")) - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.0") + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.2") implementation("io.arrow-kt:arrow-core:1.1.5") implementation("androidx.activity:activity-ktx:1.6.1") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1") @@ -313,7 +318,7 @@ dependencies { implementation("androidx.room:room-ktx:$roomVersion") implementation("com.jakewharton.timber:timber:5.0.1") implementation("androidx.core:core-ktx:1.9.0") - implementation("androidx.appcompat:appcompat:1.6.0") + implementation("androidx.appcompat:appcompat:1.6.1") implementation("androidx.constraintlayout:constraintlayout:2.1.4") val lifecycleVersion = "2.5.1" implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion") @@ -334,6 +339,7 @@ dependencies { implementation("com.louiscad.splitties:splitties-views-dsl-recyclerview:$splittiesVersion") implementation("com.louiscad.splitties:splitties-views-recyclerview:$splittiesVersion") implementation("com.louiscad.splitties:splitties-views-dsl-material:$splittiesVersion") + implementation("com.mikepenz:aboutlibraries-core:10.6.1") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test:runner:1.5.2") androidTestImplementation("androidx.test:rules:1.5.0") diff --git a/app/licenses.yml b/app/licenses.yml deleted file mode 100644 index 65f19729b..000000000 --- a/app/licenses.yml +++ /dev/null @@ -1,585 +0,0 @@ -- artifact: androidx.activity:activity-ktx:+ - name: activity-ktx - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/activity#1.5.1 -- artifact: androidx.activity:activity:+ - name: activity - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/activity#1.5.1 -- artifact: androidx.annotation:annotation-experimental:+ - name: annotation-experimental - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/annotation#1.1.0 -- artifact: androidx.annotation:annotation:+ - name: annotation - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html -- artifact: androidx.appcompat:appcompat-resources:+ - name: appcompat-resources - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/appcompat#1.4.2 -- artifact: androidx.appcompat:appcompat:+ - name: appcompat - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/appcompat#1.4.2 -- artifact: androidx.arch.core:core-common:+ - name: core-common - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/topic/libraries/architecture/index.html -- artifact: androidx.arch.core:core-runtime:+ - name: core-runtime - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/topic/libraries/architecture/index.html -- artifact: androidx.cardview:cardview:+ - name: cardview - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html -- artifact: androidx.collection:collection-ktx:+ - name: collection-ktx - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html -- artifact: androidx.collection:collection:+ - name: collection - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html -- artifact: androidx.concurrent:concurrent-futures:+ - name: concurrent-futures - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/topic/libraries/architecture/index.html -- artifact: androidx.constraintlayout:constraintlayout-core:+ - name: constraintlayout-core - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://tools.android.com -- artifact: androidx.constraintlayout:constraintlayout:+ - name: constraintlayout - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://tools.android.com -- artifact: androidx.coordinatorlayout:coordinatorlayout:+ - name: coordinatorlayout - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx -- artifact: androidx.core:core-ktx:+ - name: core-ktx - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/core#1.8.0 -- artifact: androidx.core:core:+ - name: core - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/core#1.8.0 -- artifact: androidx.cursoradapter:cursoradapter:+ - name: cursoradapter - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html -- artifact: androidx.customview:customview:+ - name: customview - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx -- artifact: androidx.databinding:viewbinding:+ - name: viewbinding - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt -- artifact: androidx.documentfile:documentfile:+ - name: documentfile - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html -- artifact: androidx.drawerlayout:drawerlayout:+ - name: drawerlayout - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx -- artifact: androidx.dynamicanimation:dynamicanimation:+ - name: dynamicanimation - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html -- artifact: androidx.emoji2:emoji2-views-helper:+ - name: emoji2-views-helper - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/emoji2#1.0.0 -- artifact: androidx.emoji2:emoji2:+ - name: emoji2 - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/emoji2#1.0.0 -- artifact: androidx.exifinterface:exifinterface:+ - name: exifinterface - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/exifinterface#1.3.3 -- artifact: androidx.fragment:fragment-ktx:+ - name: fragment-ktx - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/fragment#1.5.1 -- artifact: androidx.fragment:fragment:+ - name: fragment - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/fragment#1.5.1 -- artifact: androidx.interpolator:interpolator:+ - name: interpolator - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html -- artifact: androidx.legacy:legacy-support-core-utils:+ - name: legacy-support-core-utils - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html -- artifact: androidx.lifecycle:lifecycle-common-java8:+ - name: lifecycle-common-java8 - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/lifecycle#2.5.1 -- artifact: androidx.lifecycle:lifecycle-common:+ - name: lifecycle-common - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/lifecycle#2.5.1 -- artifact: androidx.lifecycle:lifecycle-livedata-core-ktx:+ - name: lifecycle-livedata-core-ktx - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/lifecycle#2.5.1 -- artifact: androidx.lifecycle:lifecycle-livedata-core:+ - name: lifecycle-livedata-core - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/lifecycle#2.5.1 -- artifact: androidx.lifecycle:lifecycle-livedata:+ - name: lifecycle-livedata - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/topic/libraries/architecture/index.html -- artifact: androidx.lifecycle:lifecycle-process:+ - name: lifecycle-process - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/lifecycle#2.4.0 -- artifact: androidx.lifecycle:lifecycle-runtime-ktx:+ - name: lifecycle-runtime-ktx - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/lifecycle#2.5.1 -- artifact: androidx.lifecycle:lifecycle-runtime:+ - name: lifecycle-runtime - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/lifecycle#2.5.1 -- artifact: androidx.lifecycle:lifecycle-service:+ - name: lifecycle-service - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/lifecycle#2.5.1 -- artifact: androidx.lifecycle:lifecycle-viewmodel-ktx:+ - name: lifecycle-viewmodel-ktx - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/lifecycle#2.5.1 -- artifact: androidx.lifecycle:lifecycle-viewmodel-savedstate:+ - name: lifecycle-viewmodel-savedstate - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/lifecycle#2.5.1 -- artifact: androidx.lifecycle:lifecycle-viewmodel:+ - name: lifecycle-viewmodel - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/lifecycle#2.5.1 -- artifact: androidx.loader:loader:+ - name: loader - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html -- artifact: androidx.localbroadcastmanager:localbroadcastmanager:+ - name: localbroadcastmanager - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html -- artifact: androidx.navigation:navigation-common-ktx:+ - name: navigation-common-ktx - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/navigation#2.5.1 -- artifact: androidx.navigation:navigation-common:+ - name: navigation-common - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/navigation#2.5.1 -- artifact: androidx.navigation:navigation-fragment-ktx:+ - name: navigation-fragment-ktx - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/navigation#2.5.1 -- artifact: androidx.navigation:navigation-fragment:+ - name: navigation-fragment - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/navigation#2.5.1 -- artifact: androidx.navigation:navigation-runtime-ktx:+ - name: navigation-runtime-ktx - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/navigation#2.5.1 -- artifact: androidx.navigation:navigation-runtime:+ - name: navigation-runtime - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/navigation#2.5.1 -- artifact: androidx.navigation:navigation-ui-ktx:+ - name: navigation-ui-ktx - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/navigation#2.5.1 -- artifact: androidx.navigation:navigation-ui:+ - name: navigation-ui - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/navigation#2.5.1 -- artifact: androidx.preference:preference-ktx:+ - name: preference-ktx - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/preference#1.2.0 -- artifact: androidx.preference:preference:+ - name: preference - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/preference#1.2.0 -- artifact: androidx.print:print:+ - name: print - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html -- artifact: androidx.recyclerview:recyclerview:+ - name: recyclerview - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/recyclerview#1.2.1 -- artifact: androidx.resourceinspection:resourceinspection-annotation:+ - name: resourceinspection-annotation - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/resourceinspection#1.0.0 -- artifact: androidx.room:room-common:+ - name: room-common - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/room#2.4.3 -- artifact: androidx.room:room-ktx:+ - name: room-ktx - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/room#2.4.3 -- artifact: androidx.room:room-runtime:+ - name: room-runtime - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/room#2.4.3 -- artifact: androidx.savedstate:savedstate-ktx:+ - name: savedstate-ktx - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/savedstate#1.2.0 -- artifact: androidx.savedstate:savedstate:+ - name: savedstate - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/savedstate#1.2.0 -- artifact: androidx.slidingpanelayout:slidingpanelayout:+ - name: slidingpanelayout - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/slidingpanelayout#1.2.0 -- artifact: androidx.sqlite:sqlite-framework:+ - name: sqlite-framework - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/sqlite#2.2.0 -- artifact: androidx.sqlite:sqlite:+ - name: sqlite - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/sqlite#2.2.0 -- artifact: androidx.startup:startup-runtime:+ - name: startup-runtime - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/startup#1.0.0 -- artifact: androidx.tracing:tracing:+ - name: tracing - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/tracing#1.0.0 -- artifact: androidx.transition:transition:+ - name: transition - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/transition#1.4.1 -- artifact: androidx.vectordrawable:vectordrawable-animated:+ - name: vectordrawable-animated - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx -- artifact: androidx.vectordrawable:vectordrawable:+ - name: vectordrawable - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx -- artifact: androidx.versionedparcelable:versionedparcelable:+ - name: versionedparcelable - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html -- artifact: androidx.viewpager2:viewpager2:+ - name: viewpager2 - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx -- artifact: androidx.viewpager:viewpager:+ - name: viewpager - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://developer.android.com/tools/extras/support-library.html -- artifact: androidx.window:window:+ - name: window - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/jetpack/androidx/releases/window#1.0.0 -- artifact: cat.ereza:customactivityoncrash:+ - name: customactivityoncrash - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://github.com/Ereza/CustomActivityOnCrash -- artifact: com.github.CanHub:Android-Image-Cropper:+ - name: Android-Image-Cropper - copyrightHolder: #COPYRIGHT_HOLDER# - license: Apache License 2.0 - licenseUrl: https://api.github.com/licenses/apache-2.0 - url: https://canhub.github.io/ -- artifact: com.google.android.flexbox:flexbox:+ - name: flexbox - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt -- artifact: com.google.android.material:material:+ - name: material - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://github.com/material-components/material-components-android -- artifact: com.google.guava:listenablefuture:+ - name: listenablefuture - copyrightHolder: #COPYRIGHT_HOLDER# - license: #LICENSE# -- artifact: com.jakewharton.timber:timber:+ - name: timber - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://github.com/JakeWharton/timber -- artifact: com.louiscad.splitties:splitties-bitflags-jvm:+ - name: splitties-bitflags-jvm - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: https://www.apache.org/licenses/LICENSE-2.0.txt - url: https://github.com/LouisCAD/Splitties -- artifact: io.arrow-kt:arrow-annotations-jvm:+ - name: arrow-annotations-jvm - copyrightHolder: #COPYRIGHT_HOLDER# - license: https://www.apache.org/licenses/LICENSE-2.0.txt - url: https://github.com/arrow-kt/arrow/ -- artifact: io.arrow-kt:arrow-continuations-jvm:+ - name: arrow-continuations-jvm - copyrightHolder: #COPYRIGHT_HOLDER# - license: https://www.apache.org/licenses/LICENSE-2.0.txt - url: https://github.com/arrow-kt/arrow/ -- artifact: io.arrow-kt:arrow-core-jvm:+ - name: arrow-core-jvm - copyrightHolder: #COPYRIGHT_HOLDER# - license: https://www.apache.org/licenses/LICENSE-2.0.txt - url: https://github.com/arrow-kt/arrow/ -- artifact: net.java.dev.jna:jna:+ - name: jna - copyrightHolder: #COPYRIGHT_HOLDER# - license: LGPL-2.1-or-later - licenseUrl: https://www.gnu.org/licenses/old-licenses/lgpl-2.1 - url: https://github.com/java-native-access/jna -- artifact: org.codehaus.mojo:animal-sniffer-annotations:+ - name: animal-sniffer-annotations - copyrightHolder: #COPYRIGHT_HOLDER# - license: #LICENSE# -- artifact: org.jetbrains.kotlin:kotlin-android-extensions-runtime:+ - name: kotlin-android-extensions-runtime - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://kotlinlang.org/ -- artifact: org.jetbrains.kotlin:kotlin-parcelize-runtime:+ - name: kotlin-parcelize-runtime - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://kotlinlang.org/ -- artifact: org.jetbrains.kotlin:kotlin-reflect:+ - name: kotlin-reflect - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://kotlinlang.org/ -- artifact: org.jetbrains.kotlin:kotlin-stdlib-common:+ - name: kotlin-stdlib-common - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://kotlinlang.org/ -- artifact: org.jetbrains.kotlin:kotlin-stdlib-jdk7:+ - name: kotlin-stdlib-jdk7 - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://kotlinlang.org/ -- artifact: org.jetbrains.kotlin:kotlin-stdlib-jdk8:+ - name: kotlin-stdlib-jdk8 - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://kotlinlang.org/ -- artifact: org.jetbrains.kotlin:kotlin-stdlib:+ - name: kotlin-stdlib - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://kotlinlang.org/ -- artifact: org.jetbrains.kotlinx:kotlinx-coroutines-android:+ - name: kotlinx-coroutines-android - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: https://www.apache.org/licenses/LICENSE-2.0.txt - url: https://github.com/Kotlin/kotlinx.coroutines -- artifact: org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:+ - name: kotlinx-coroutines-core-jvm - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: https://www.apache.org/licenses/LICENSE-2.0.txt - url: https://github.com/Kotlin/kotlinx.coroutines -- artifact: org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:+ - name: kotlinx-serialization-core-jvm - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: https://www.apache.org/licenses/LICENSE-2.0.txt - url: https://github.com/Kotlin/kotlinx.serialization -- artifact: org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:+ - name: kotlinx-serialization-json-jvm - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: https://www.apache.org/licenses/LICENSE-2.0.txt - url: https://github.com/Kotlin/kotlinx.serialization -- artifact: org.jetbrains:annotations:+ - name: annotations - copyrightHolder: #COPYRIGHT_HOLDER# - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://www.jetbrains.org -- artifact: org.mechdancer:dependency:+ - name: dependency - copyrightHolder: #COPYRIGHT_HOLDER# - license: WTFPL - licenseUrl: http://www.wtfpl.net/txt/copying/ - url: https://github.com/MechDancer/dependency \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/Licenses.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/Licenses.kt deleted file mode 100644 index ee549ee45..000000000 --- a/app/src/main/java/org/fcitx/fcitx5/android/data/Licenses.kt +++ /dev/null @@ -1,53 +0,0 @@ -package org.fcitx.fcitx5.android.data - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.serialization.Serializable -import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.builtins.MapSerializer -import kotlinx.serialization.builtins.serializer -import kotlinx.serialization.json.Json -import org.fcitx.fcitx5.android.utils.appContext - -object Licenses { - - @Serializable - data class LibraryLicense( - val artifactId: LibraryArtifactID, - val license: String? = null, - val licenseUrl: String? = null, - val normalizedLicense: String? = null, - val url: String? = null, - val libraryName: String, - ) - - @Serializable - data class LibraryArtifactID( - val name: String, - val group: String, - val version: String - ) - - private var parsed: List? = null - - suspend fun getAll(): Result> = runCatching { - parsed?.let { return@runCatching it } - withContext(Dispatchers.IO) { - val content = - appContext.assets.open(licensesJSON).bufferedReader().use { x -> x.readText() } - val list = Json.decodeFromString( - MapSerializer( - String.serializer(), - ListSerializer(LibraryLicense.serializer()) - ), - content - )["libraries"]!! - .filter { !it.licenseUrl.isNullOrEmpty() } - .sortedBy { it.libraryName } - parsed = list - list - } - } - - private const val licensesJSON = "licenses.json" -} \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/LicensesFragment.kt b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/LicensesFragment.kt index 45acb8c6e..b7bce28c7 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/LicensesFragment.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/LicensesFragment.kt @@ -1,14 +1,14 @@ package org.fcitx.fcitx5.android.ui.main -import android.content.Intent -import android.net.Uri +import android.app.AlertDialog import android.os.Bundle import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import androidx.preference.Preference +import com.mikepenz.aboutlibraries.Libs +import com.mikepenz.aboutlibraries.entity.License import kotlinx.coroutines.launch import org.fcitx.fcitx5.android.R -import org.fcitx.fcitx5.android.data.Licenses import org.fcitx.fcitx5.android.ui.common.PaddingPreferenceFragment class LicensesFragment : PaddingPreferenceFragment() { @@ -23,23 +23,36 @@ class LicensesFragment : PaddingPreferenceFragment() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { lifecycleScope.launch { - Licenses.getAll().onSuccess { licenses -> - val context = preferenceManager.context - val screen = preferenceManager.createPreferenceScreen(context) - licenses.forEach { license -> + val context = preferenceManager.context + val screen = preferenceManager.createPreferenceScreen(context) + val jsonString = resources.openRawResource(R.raw.aboutlibraries) + .bufferedReader() + .use { it.readText() } + Libs.Builder() + .withJson(jsonString) + .build() + .libraries.forEach { screen.addPreference(Preference(context).apply { isIconSpaceReserved = false - title = license.libraryName - summary = license.artifactId.group + title = "${it.uniqueId}:${it.artifactVersion}" + val license = it.licenses.firstOrNull() ?: return@forEach + summary = license.spdxId ?: license.name setOnPreferenceClickListener { - startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(license.licenseUrl))) - true + showLicenseDialog(license) } }) } - preferenceScreen = screen - } + preferenceScreen = screen } } + private fun showLicenseDialog(license: License): Boolean { + AlertDialog.Builder(context) + .setTitle(license.name) + .setMessage(license.licenseContent) + .setPositiveButton(android.R.string.ok, null) + .show() + return true + } + } \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index c99804ec4..2504954bb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,7 @@ plugins { - id("com.android.application") version "7.4.0" apply false + id("com.android.application") version "7.4.1" apply false kotlin("android") version "1.8.0" apply false + id("com.mikepenz.aboutlibraries.plugin") version "10.6.1" apply false } tasks.register("clean", Delete::class) { From cc530d9437779f37f8641fb24eb27dbcca023517 Mon Sep 17 00:00:00 2001 From: Rocka Date: Fri, 24 Feb 2023 15:49:40 +0800 Subject: [PATCH 042/624] Update fcitx5 submodules --- app/src/main/cpp/libime | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/cpp/libime b/app/src/main/cpp/libime index 1160455a9..a02733f20 160000 --- a/app/src/main/cpp/libime +++ b/app/src/main/cpp/libime @@ -1 +1 @@ -Subproject commit 1160455a9078aa77c0d8b52db67a20eed3e16196 +Subproject commit a02733f20c1a41ddb5991da339ffb7985b14b624 From cf29b0b93e963f6d4166e8453366c77def44a330 Mon Sep 17 00:00:00 2001 From: Rocka Date: Fri, 24 Feb 2023 19:43:34 +0800 Subject: [PATCH 043/624] Add license for native dependency & fix license display --- app/build.gradle.kts | 3 +- app/licenses/libraries/boost.json | 11 +++++ .../libraries/fcitx5-chinese-addons.json | 12 ++++++ app/licenses/libraries/fcitx5-lua.json | 11 +++++ app/licenses/libraries/fcitx5-unikey.json | 12 ++++++ app/licenses/libraries/fcitx5.json | 11 +++++ app/licenses/libraries/fmt.json | 11 +++++ app/licenses/libraries/libevent.json | 11 +++++ app/licenses/libraries/libime.json | 11 +++++ app/licenses/libraries/libintl-lite.json | 11 +++++ app/licenses/libraries/lua.json | 11 +++++ app/licenses/libraries/opencc.json | 11 +++++ .../fcitx5/android/ui/main/AboutFragment.kt | 4 +- .../android/ui/main/LicensesFragment.kt | 43 ++++++++++++++----- .../org/fcitx/fcitx5/android/utils/Const.kt | 4 +- 15 files changed, 161 insertions(+), 16 deletions(-) create mode 100644 app/licenses/libraries/boost.json create mode 100644 app/licenses/libraries/fcitx5-chinese-addons.json create mode 100644 app/licenses/libraries/fcitx5-lua.json create mode 100644 app/licenses/libraries/fcitx5-unikey.json create mode 100644 app/licenses/libraries/fcitx5.json create mode 100644 app/licenses/libraries/fmt.json create mode 100644 app/licenses/libraries/libevent.json create mode 100644 app/licenses/libraries/libime.json create mode 100644 app/licenses/libraries/libintl-lite.json create mode 100644 app/licenses/libraries/lua.json create mode 100644 app/licenses/libraries/opencc.json diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9df05ad92..1620483e9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -164,7 +164,8 @@ kotlin { } aboutLibraries { - excludeFields = arrayOf("metadata", "funding", "description", "scm", "website", "developers", "name") + configPath = "app/licenses" + excludeFields = arrayOf("generated", "developers", "organization", "scm", "funding", "content") fetchRemoteLicense = false fetchRemoteFunding = false includePlatform = false diff --git a/app/licenses/libraries/boost.json b/app/licenses/libraries/boost.json new file mode 100644 index 000000000..7848e8ef2 --- /dev/null +++ b/app/licenses/libraries/boost.json @@ -0,0 +1,11 @@ +{ + "uniqueId": "boostorg/boost", + "artifactVersion": "1.80.0", + "description": "Free peer-reviewed portable C++ source libraries", + "name": "boostorg/boost", + "website": "https://www.boost.org/", + "tag": "native", + "licenses": [ + "BSL-1.0" + ] +} diff --git a/app/licenses/libraries/fcitx5-chinese-addons.json b/app/licenses/libraries/fcitx5-chinese-addons.json new file mode 100644 index 000000000..5fd84fb80 --- /dev/null +++ b/app/licenses/libraries/fcitx5-chinese-addons.json @@ -0,0 +1,12 @@ +{ + "uniqueId": "fcitx/fcitx5-chinese-addons", + "artifactVersion": "5.0.16", + "description": "Chinese related addon for fcitx5", + "name": "fcitx/fcitx5-chinese-addons", + "website": "https://github.com/fcitx/fcitx5-chinese-addons", + "tag": "native", + "licenses": [ + "GPL-2.0-or-later", + "LGPL-2.1-or-later" + ] +} diff --git a/app/licenses/libraries/fcitx5-lua.json b/app/licenses/libraries/fcitx5-lua.json new file mode 100644 index 000000000..36e42980a --- /dev/null +++ b/app/licenses/libraries/fcitx5-lua.json @@ -0,0 +1,11 @@ +{ + "uniqueId": "fcitx/fcitx5-lua", + "artifactVersion": "5.0.10", + "description": "Lua support for fcitx5", + "name": "fcitx/fcitx5-lua", + "website": "https://github.com/fcitx/fcitx5-lua", + "tag": "native", + "licenses": [ + "LGPL-2.1-or-later" + ] +} diff --git a/app/licenses/libraries/fcitx5-unikey.json b/app/licenses/libraries/fcitx5-unikey.json new file mode 100644 index 000000000..de37d9ede --- /dev/null +++ b/app/licenses/libraries/fcitx5-unikey.json @@ -0,0 +1,12 @@ +{ + "uniqueId": "fcitx/fcitx5-unikey", + "artifactVersion": "5.0.12", + "description": "Unikey (Vietnamese Input Method) engine support for Fcitx", + "name": "fcitx/fcitx5-unikey", + "website": "https://github.com/fcitx/fcitx5-unikey", + "tag": "native", + "licenses": [ + "GPL-2.0-or-later", + "LGPL-2.1-or-later" + ] +} diff --git a/app/licenses/libraries/fcitx5.json b/app/licenses/libraries/fcitx5.json new file mode 100644 index 000000000..e3f5b562b --- /dev/null +++ b/app/licenses/libraries/fcitx5.json @@ -0,0 +1,11 @@ +{ + "uniqueId": "fcitx/fcitx5", + "artifactVersion": "5.0.21", + "description": "Next generation of fcitx", + "name": "fcitx/fcitx5", + "website": "https://github.com/fcitx/fcitx5", + "tag": "native", + "licenses": [ + "LGPL-2.1-or-later" + ] +} diff --git a/app/licenses/libraries/fmt.json b/app/licenses/libraries/fmt.json new file mode 100644 index 000000000..ec190743e --- /dev/null +++ b/app/licenses/libraries/fmt.json @@ -0,0 +1,11 @@ +{ + "uniqueId": "fmtlib/fmt", + "artifactVersion": "8.1.1", + "description": "Open-source formatting library for C++", + "name": "fmtlib/fmt", + "website": "https://fmt.dev", + "tag": "native", + "licenses": [ + "MIT" + ] +} diff --git a/app/licenses/libraries/libevent.json b/app/licenses/libraries/libevent.json new file mode 100644 index 000000000..b7ed73770 --- /dev/null +++ b/app/licenses/libraries/libevent.json @@ -0,0 +1,11 @@ +{ + "uniqueId": "libevent/libevent", + "artifactVersion": "release-2.1.12-stable", + "description": "Event notification library", + "name": "libevent/libevent", + "website": "https://libevent.org/", + "tag": "native", + "licenses": [ + "BSD-3-Clause" + ] +} diff --git a/app/licenses/libraries/libime.json b/app/licenses/libraries/libime.json new file mode 100644 index 000000000..13a980f09 --- /dev/null +++ b/app/licenses/libraries/libime.json @@ -0,0 +1,11 @@ +{ + "uniqueId": "fcitx/libime", + "artifactVersion": "1.0.16", + "description": "library to support generic input method implementation", + "name": "fcitx/libime", + "website": "https://github.com/fcitx/libime", + "tag": "native", + "licenses": [ + "LGPL-2.1-or-later" + ] +} diff --git a/app/licenses/libraries/libintl-lite.json b/app/licenses/libraries/libintl-lite.json new file mode 100644 index 000000000..86daf4cce --- /dev/null +++ b/app/licenses/libraries/libintl-lite.json @@ -0,0 +1,11 @@ +{ + "uniqueId": "j-jorge/libintl-lite", + "artifactVersion": "5750d92", + "description": "simple (but less powerful) GNU gettext libintl replacement", + "name": "j-jorge/libintl-lite", + "website": "https://github.com/j-jorge/libintl-lite", + "tag": "native", + "licenses": [ + "BSL-1.0" + ] +} diff --git a/app/licenses/libraries/lua.json b/app/licenses/libraries/lua.json new file mode 100644 index 000000000..e75a093c1 --- /dev/null +++ b/app/licenses/libraries/lua.json @@ -0,0 +1,11 @@ +{ + "uniqueId": "lua/lua", + "artifactVersion": "5.4.4", + "description": "Powerful lightweight programming language designed for extending applications", + "name": "lua/lua", + "website": "https://www.lua.org/", + "tag": "native", + "licenses": [ + "MIT" + ] +} diff --git a/app/licenses/libraries/opencc.json b/app/licenses/libraries/opencc.json new file mode 100644 index 000000000..d1d58905d --- /dev/null +++ b/app/licenses/libraries/opencc.json @@ -0,0 +1,11 @@ +{ + "uniqueId": "BYVoid/OpenCC", + "artifactVersion": "1.1.6", + "description": "opensource project for conversions between Traditional Chinese, Simplified Chinese and Japanese Kanji (Shinjitai).", + "name": "BYVoid/OpenCC", + "website": "https://opencc.byvoid.com/", + "tag": "native", + "licenses": [ + "Apache-2.0" + ] +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/AboutFragment.kt b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/AboutFragment.kt index 1727608e6..c9b55cf0e 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/AboutFragment.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/AboutFragment.kt @@ -60,11 +60,11 @@ class AboutFragment : PaddingPreferenceFragment() { screen.addPreference(Preference(context).apply { setTitle(R.string.license) - summary = Const.lgpl + summary = Const.licenseSpdxId isIconSpaceReserved = false isSingleLineTitle = false setOnPreferenceClickListener { - startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(Const.lgplLicenseUrl))) + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(Const.licenseUrl))) true } }) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/LicensesFragment.kt b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/LicensesFragment.kt index b7bce28c7..7887f5bd2 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/LicensesFragment.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/LicensesFragment.kt @@ -1,6 +1,8 @@ package org.fcitx.fcitx5.android.ui.main import android.app.AlertDialog +import android.content.Intent +import android.net.Uri import android.os.Bundle import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope @@ -31,14 +33,17 @@ class LicensesFragment : PaddingPreferenceFragment() { Libs.Builder() .withJson(jsonString) .build() - .libraries.forEach { + .libraries + .sortedBy { + if (it.tag == "native") it.uniqueId.uppercase() else it.uniqueId.lowercase() + } + .forEach { screen.addPreference(Preference(context).apply { isIconSpaceReserved = false title = "${it.uniqueId}:${it.artifactVersion}" - val license = it.licenses.firstOrNull() ?: return@forEach - summary = license.spdxId ?: license.name - setOnPreferenceClickListener { - showLicenseDialog(license) + summary = it.licenses.joinToString { l -> l.spdxId ?: l.name } + setOnPreferenceClickListener { _ -> + showLicenseDialog(it.uniqueId, it.licenses) } }) } @@ -46,13 +51,29 @@ class LicensesFragment : PaddingPreferenceFragment() { } } - private fun showLicenseDialog(license: License): Boolean { - AlertDialog.Builder(context) - .setTitle(license.name) - .setMessage(license.licenseContent) - .setPositiveButton(android.R.string.ok, null) - .show() + private fun showLicenseDialog(uniqueId: String, licenses: Set): Boolean { + when (licenses.size) { + 0 -> {} + 1 -> showLicenseContent(licenses.first()) + else -> { + val licenseArray = licenses.toTypedArray() + val licenseNames = licenseArray.map { it.spdxId ?: it.name }.toTypedArray() + AlertDialog.Builder(context) + .setTitle(uniqueId) + .setItems(licenseNames) { _, idx -> + showLicenseContent(licenseArray[idx]) + } + .setPositiveButton(android.R.string.cancel, null) + .show() + } + } return true } + private fun showLicenseContent(license: License) { + if (license.url?.isNotBlank() == true) { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(license.url))) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/utils/Const.kt b/app/src/main/java/org/fcitx/fcitx5/android/utils/Const.kt index d9e14c564..47a4949ac 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/utils/Const.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/utils/Const.kt @@ -8,8 +8,8 @@ object Const { const val versionName = "${BuildConfig.VERSION_NAME}-${BuildConfig.BUILD_TYPE}" const val dataDescriptorName = BuildConfig.DATA_DESCRIPTOR_NAME const val githubRepo = "https://github.com/fcitx5-android/fcitx5-android" - const val lgpl = "LGPL-2.1-or-later" - const val lgplLicenseUrl = "https://www.gnu.org/licenses/old-licenses/lgpl-2.1" + const val licenseSpdxId = "LGPL-2.1-or-later" + const val licenseUrl = "https://www.gnu.org/licenses/old-licenses/lgpl-2.1" const val privacyPolicyUrl = "https://fcitx5-android.github.io/privacy/" const val faqUrl = "https://fcitx5-android.github.io/faq/" } \ No newline at end of file From 5ac719c3547165a3e77fe265c471a5a211580320 Mon Sep 17 00:00:00 2001 From: rocka Date: Mon, 27 Feb 2023 18:37:07 +0800 Subject: [PATCH 044/624] Custom UncaughtExceptionHandler (#202) Co-authored-by: Potato Hatsue <1793913507@qq.com> --- app/build.gradle.kts | 1 - .../fcitx/fcitx5/android/FcitxApplication.kt | 26 +++++++++--- .../fcitx5/android/ui/main/LogActivity.kt | 40 ++++++++++--------- .../fcitx5/android/ui/main/log/LogAdapter.kt | 5 +-- .../fcitx5/android/ui/main/log/LogView.kt | 20 +++++----- flake.lock | 6 +-- flake.nix | 1 - 7 files changed, 58 insertions(+), 41 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1620483e9..cf2746b15 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -310,7 +310,6 @@ dependencies { implementation("androidx.activity:activity-ktx:1.6.1") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1") implementation("com.github.CanHub:Android-Image-Cropper:4.2.1") - implementation("cat.ereza:customactivityoncrash:2.4.0") implementation("com.google.android.flexbox:flexbox:3.0.0") implementation("org.mechdancer:dependency:0.1.2") val roomVersion = "2.5.0" diff --git a/app/src/main/java/org/fcitx/fcitx5/android/FcitxApplication.kt b/app/src/main/java/org/fcitx/fcitx5/android/FcitxApplication.kt index d1d4d4d83..1cdeb873e 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/FcitxApplication.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/FcitxApplication.kt @@ -1,11 +1,11 @@ package org.fcitx.fcitx5.android import android.app.Application +import android.content.Intent import android.content.res.Configuration import android.os.Process import android.util.Log import androidx.preference.PreferenceManager -import cat.ereza.customactivityoncrash.config.CaocConfig import org.fcitx.fcitx5.android.data.clipboard.ClipboardManager import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.data.theme.ThemeManager @@ -13,14 +13,29 @@ import org.fcitx.fcitx5.android.ui.main.LogActivity import org.fcitx.fcitx5.android.utils.Locales import org.fcitx.fcitx5.android.utils.isDarkMode import timber.log.Timber +import kotlin.system.exitProcess class FcitxApplication : Application() { override fun onCreate() { super.onCreate() - CaocConfig.Builder.create() - .enabled(!BuildConfig.DEBUG) - .errorActivity(LogActivity::class.java) - .apply() + if (!BuildConfig.DEBUG) { + Thread.setDefaultUncaughtExceptionHandler { _, e -> + startActivity(Intent(applicationContext, LogActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + putExtra(LogActivity.FROM_CRASH, true) + // avoid transaction overflow + val truncated = e.stackTraceToString().let { + if (it.length > MAX_STACKTRACE_SIZE) + it.take(MAX_STACKTRACE_SIZE) + "" + else + it + } + putExtra(LogActivity.CRASH_STACK_TRACE, truncated) + }) + exitProcess(10) + } + } + instance = this // we don't have AppPrefs available yet val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(applicationContext) @@ -65,5 +80,6 @@ class FcitxApplication : Application() { instance ?: throw IllegalStateException("Fcitx application is not created!") fun getLastPid() = lastPid + private const val MAX_STACKTRACE_SIZE = 128000 } } \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/LogActivity.kt b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/LogActivity.kt index f98110943..39a165e7f 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/LogActivity.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/LogActivity.kt @@ -11,7 +11,6 @@ import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updateLayoutParams import androidx.lifecycle.lifecycleScope -import cat.ereza.customactivityoncrash.CustomActivityOnCrash import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.launch @@ -63,24 +62,24 @@ class LogActivity : AppCompatActivity() { with(binding) { setSupportActionBar(toolbar) this@LogActivity.logView = logView - logView.setLogcat( - if (CustomActivityOnCrash.getConfigFromIntent(intent) == null) { - supportActionBar!!.apply { - setDisplayHomeAsUpEnabled(true) - setTitle(R.string.real_time_logs) - } - Logcat() - } else { - supportActionBar!!.setTitle(R.string.crash_logs) - clearButton.visibility = View.GONE - AlertDialog.Builder(this@LogActivity) - .setTitle(R.string.app_crash) - .setMessage(R.string.app_crash_message) - .setPositiveButton(android.R.string.ok) { _, _ -> } - .show() - Logcat(FcitxApplication.getLastPid()) + if (intent.hasExtra(FROM_CRASH)) { + supportActionBar!!.setTitle(R.string.crash_logs) + clearButton.visibility = View.GONE + AlertDialog.Builder(this@LogActivity) + .setTitle(R.string.app_crash) + .setMessage(R.string.app_crash_message) + .setPositiveButton(android.R.string.ok, null) + .show() + logView.append("--------- Crash stacktrace") + logView.append(intent.getStringExtra(CRASH_STACK_TRACE) ?: "") + logView.setLogcat(Logcat(FcitxApplication.getLastPid())) + } else { + supportActionBar!!.apply { + setDisplayHomeAsUpEnabled(true) + setTitle(R.string.real_time_logs) } - ) + logView.setLogcat(Logcat()) + } clearButton.setOnClickListener { logView.clear() } @@ -90,4 +89,9 @@ class LogActivity : AppCompatActivity() { } registerLauncher() } + + companion object { + const val FROM_CRASH = "from_crash" + const val CRASH_STACK_TRACE = "crash_stack_trace" + } } \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/log/LogAdapter.kt b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/log/LogAdapter.kt index c403f5f11..cfa33c077 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/log/LogAdapter.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/log/LogAdapter.kt @@ -2,7 +2,6 @@ package org.fcitx.fcitx5.android.ui.main.log import android.graphics.Typeface import android.os.Build -import android.text.SpannableString import android.view.ViewGroup import android.view.ViewGroup.MarginLayoutParams import android.view.textclassifier.TextClassifier @@ -14,11 +13,11 @@ import splitties.views.dsl.core.startMargin import splitties.views.dsl.core.textView import splitties.views.dsl.core.wrapContent -class LogAdapter(private val entries: ArrayList = ArrayList()) : +class LogAdapter(private val entries: ArrayList = ArrayList()) : RecyclerView.Adapter() { inner class Holder(val textView: TextView) : RecyclerView.ViewHolder(textView) - fun append(line: SpannableString) { + fun append(line: CharSequence) { val size = entries.size entries.add(line) notifyItemInserted(size) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/log/LogView.kt b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/log/LogView.kt index 06abf4e8f..8695393e6 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/log/LogView.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/log/LogView.kt @@ -1,11 +1,10 @@ package org.fcitx.fcitx5.android.ui.main.log import android.content.Context -import android.text.Spannable -import android.text.SpannableString -import android.text.style.ForegroundColorSpan import android.util.AttributeSet import android.widget.HorizontalScrollView +import androidx.core.text.buildSpannedString +import androidx.core.text.color import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.flow.launchIn @@ -41,6 +40,12 @@ class LogView @JvmOverloads constructor(context: Context, attributeSet: Attribut super.onDetachedFromWindow() } + fun append(content: String) { + logAdapter.append(buildSpannedString { + color(styledColor(android.R.attr.colorForeground)) { append(content) } + }) + } + fun setLogcat(logcat: Logcat) { this.logcat = logcat logcat.initLogFlow() @@ -56,13 +61,8 @@ class LogView @JvmOverloads constructor(context: Context, attributeSet: Attribut else -> android.R.attr.colorForeground } ) - logAdapter.append(SpannableString(it).apply { - setSpan( - ForegroundColorSpan(color), - 0, - it.length, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) + logAdapter.append(buildSpannedString { + color(color) { append(it) } }) }.launchIn(findViewTreeLifecycleOwner()!!.lifecycleScope) } diff --git a/flake.lock b/flake.lock index 29e0c7216..1d57a29ab 100644 --- a/flake.lock +++ b/flake.lock @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1675183161, - "narHash": "sha256-Zq8sNgAxDckpn7tJo7V1afRSk2eoVbu3OjI1QklGLNg=", + "lastModified": 1677342105, + "narHash": "sha256-kv1fpkfCJGb0M+LZaCHFUuIS9kRIwyVgupHu86Y28nc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e1e1b192c1a5aab2960bf0a0bd53a2e8124fa18e", + "rev": "b1f87ca164a9684404c8829b851c3586c4d9f089", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index e91ab0cec..35bd19740 100644 --- a/flake.nix +++ b/flake.nix @@ -22,7 +22,6 @@ buildInputs = [ androidComposition.androidsdk extra-cmake-modules - glibc gettext python39 icu From 0c427f4518d9adc32b6bc9e243ab0fd3190a8418 Mon Sep 17 00:00:00 2001 From: Rocka Date: Mon, 27 Feb 2023 19:43:28 +0800 Subject: [PATCH 045/624] Optimize when commit text equals to composing Requires the engine call commitString before reset inputPanel --- .../cpp/androidkeyboard/androidkeyboard.cpp | 2 +- .../fcitx5/android/core/FormattedText.kt | 2 +- .../android/input/FcitxInputMethodService.kt | 22 +++++++++++++------ 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/app/src/main/cpp/androidkeyboard/androidkeyboard.cpp b/app/src/main/cpp/androidkeyboard/androidkeyboard.cpp index f9c491865..946146209 100644 --- a/app/src/main/cpp/androidkeyboard/androidkeyboard.cpp +++ b/app/src/main/cpp/androidkeyboard/androidkeyboard.cpp @@ -23,10 +23,10 @@ class AndroidKeyboardCandidateWord : public CandidateWord { commit_(std::move(commit)) {} void select(InputContext *inputContext) const override { + inputContext->commitString(commit_); inputContext->inputPanel().reset(); inputContext->updatePreedit(); inputContext->updateUserInterface(UserInterfaceComponent::InputPanel); - inputContext->commitString(commit_); engine_->resetState(inputContext, true); } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/FormattedText.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/FormattedText.kt index 90f662675..a0745cbdb 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/core/FormattedText.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/core/FormattedText.kt @@ -54,7 +54,7 @@ data class FormattedText( StringBuilder().apply { var byteSize = 0 strings.forEach { - val bytes = it.encodeToByteArray() + val bytes = it.toByteArray() val total = byteSize + bytes.size if (total < byteCursor) { append(it) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt index 0a796ac27..919391706 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt @@ -51,6 +51,11 @@ class FcitxInputMethodService : LifecycleInputMethodService() { val composing = CursorRange() private var composingText = FormattedText.Empty + private fun resetComposingState() { + composing.clear() + composingText = FormattedText.Empty + } + private var cursorUpdateIndex: Int = 0 private var highlightColor: Int = 0x66008577 // material_deep_teal_500 with alpha 0.4 @@ -193,13 +198,18 @@ class FcitxInputMethodService : LifecycleInputMethodService() { } fun commitText(text: String) { + if (composing.isNotEmpty() && composingText.toString() == text) { + // when composing text equals commit content, finish composing text as-is + selection.predict(composing.end) + resetComposingState() + inputConnection?.finishComposingText() + return + } // committed text should replace composing (if any), replace selected range (if any), // or simply prepend before cursor val start = if (composing.isEmpty()) selection.latest.start else composing.start selection.predict(start + text.length) - // clear composing range - composing.clear() - composingText = FormattedText.Empty + resetComposingState() inputConnection?.commitText(text, 1) } @@ -383,8 +393,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() { // initialSel{Start,End} is outdated. but it's the client app's responsibility to send // right cursor position, try to workaround this would simply introduce more bugs. selection.resetTo(attribute.initialSelStart, attribute.initialSelEnd) - composing.clear() - composingText = FormattedText.Empty + resetComposingState() val flags = CapabilityFlags.fromEditorInfo(attribute) editorInfo = attribute capabilityFlags = flags @@ -469,8 +478,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() { } } else { Timber.d("handleCursorUpdate: focus out/in") - composing.clear() - composingText = FormattedText.Empty + resetComposingState() // cursor outside composing range, finish composing as-is inputConnection?.finishComposingText() // `fcitx.reset()` here would commit preedit after new cursor position From 43732d10095e570b6197a33420a8fbd4918c2fcd Mon Sep 17 00:00:00 2001 From: Rocka Date: Tue, 28 Feb 2023 00:38:55 +0800 Subject: [PATCH 046/624] Attach bar's clipboard listener after scope setup Fix crash when clipboard updates before scope setup finish --- .../fcitx5/android/input/bar/KawaiiBarComponent.kt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt index c81fc6036..0a6dff48c 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt @@ -117,13 +117,6 @@ class KawaiiBarComponent : UniqueViewComponent( popup.listener } - init { - ClipboardManager.addOnUpdateListener(onClipboardUpdateListener) - clipboardSuggestion.registerOnChangeListener(onClipboardSuggestionUpdateListener) - clipboardItemTimeout.registerOnChangeListener(onClipboardTimeoutUpdateListener) - expandToolbarByDefault.registerOnChangeListener(onExpandToolbarByDefaultUpdateListener) - } - private fun launchClipboardTimeoutJob() { clipboardTimeoutJob?.cancel() val timeout = clipboardItemTimeout.getValue() * 1000L @@ -286,6 +279,10 @@ class KawaiiBarComponent : UniqueViewComponent( onClipboardUpdateListener.onUpdate(it) } } + ClipboardManager.addOnUpdateListener(onClipboardUpdateListener) + clipboardSuggestion.registerOnChangeListener(onClipboardSuggestionUpdateListener) + clipboardItemTimeout.registerOnChangeListener(onClipboardTimeoutUpdateListener) + expandToolbarByDefault.registerOnChangeListener(onExpandToolbarByDefaultUpdateListener) } override fun onStartInput(info: EditorInfo, capFlags: CapabilityFlags) { From 01c09b8b22820032d51ca5b62d3de984e6873fc9 Mon Sep 17 00:00:00 2001 From: Rocka Date: Thu, 2 Mar 2023 01:59:06 +0800 Subject: [PATCH 047/624] Refactor ClipboardAdapter with androidx.paging --- app/build.gradle.kts | 2 + .../data/clipboard/ClipboardManager.kt | 6 +- .../android/data/clipboard/db/ClipboardDao.kt | 11 ++- .../input/clipboard/ClipboardAdapter.kt | 83 +++++-------------- .../input/clipboard/ClipboardWindow.kt | 74 +++++++---------- 5 files changed, 64 insertions(+), 112 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cf2746b15..6ed4a677f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -314,6 +314,7 @@ dependencies { implementation("org.mechdancer:dependency:0.1.2") val roomVersion = "2.5.0" implementation("androidx.room:room-runtime:$roomVersion") + implementation("androidx.room:room-paging:$roomVersion") ksp("androidx.room:room-compiler:$roomVersion") implementation("androidx.room:room-ktx:$roomVersion") implementation("com.jakewharton.timber:timber:5.0.1") @@ -328,6 +329,7 @@ dependencies { implementation("androidx.preference:preference-ktx:1.2.0") implementation("androidx.recyclerview:recyclerview:1.2.1") implementation("androidx.viewpager2:viewpager2:1.0.0") + implementation("androidx.paging:paging-runtime-ktx:3.1.1") val navVersion = "2.5.3" implementation("androidx.navigation:navigation-fragment-ktx:$navVersion") implementation("androidx.navigation:navigation-ui-ktx:$navVersion") diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/ClipboardManager.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/ClipboardManager.kt index df81846f1..cbb6f1063 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/ClipboardManager.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/ClipboardManager.kt @@ -77,7 +77,9 @@ object ClipboardManager : ClipboardManager.OnPrimaryClipChangedListener, suspend fun get(id: Int) = clbDao.get(id) - suspend fun getAll() = clbDao.getAll() + suspend fun haveUnpinned() = clbDao.haveUnpinned() + + fun allEntries() = clbDao.allEntries() suspend fun pin(id: Int) = clbDao.updatePinStatus(id, true) @@ -134,7 +136,7 @@ object ClipboardManager : ClipboardManager.OnPrimaryClipChangedListener, private suspend fun removeOutdated() { val limit = limitPref.getValue() - val unpinned = clbDao.getAll().filter { !it.pinned } + val unpinned = clbDao.getAllUnpinned() if (unpinned.size > limit) { // the last one we will keep val last = unpinned diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardDao.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardDao.kt index 04d6861eb..098047d14 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardDao.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardDao.kt @@ -1,5 +1,6 @@ package org.fcitx.fcitx5.android.data.clipboard.db +import androidx.paging.PagingSource import androidx.room.Dao import androidx.room.Insert import androidx.room.Query @@ -27,8 +28,14 @@ interface ClipboardDao { @Query("SELECT * FROM ${ClipboardEntry.TABLE_NAME} WHERE rowId=:rowId LIMIT 1") suspend fun get(rowId: Long): ClipboardEntry? - @Query("SELECT * FROM ${ClipboardEntry.TABLE_NAME}") - suspend fun getAll(): List + @Query("SELECT EXISTS(SELECT 1 FROM ${ClipboardEntry.TABLE_NAME} WHERE pinned=0)") + suspend fun haveUnpinned(): Boolean + + @Query("SELECT * FROM ${ClipboardEntry.TABLE_NAME} WHERE pinned=0") + suspend fun getAllUnpinned(): List + + @Query("SELECT * FROM ${ClipboardEntry.TABLE_NAME} ORDER BY pinned DESC, timestamp DESC") + fun allEntries(): PagingSource @Query("SELECT * FROM ${ClipboardEntry.TABLE_NAME} WHERE text=:text LIMIT 1") suspend fun find(text: String): ClipboardEntry? diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardAdapter.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardAdapter.kt index 43499c211..d12e23d33 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardAdapter.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardAdapter.kt @@ -10,6 +10,7 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.lifecycleScope +import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.launch @@ -18,13 +19,28 @@ import org.fcitx.fcitx5.android.data.clipboard.db.ClipboardEntry import org.fcitx.fcitx5.android.data.theme.Theme import splitties.resources.drawable import splitties.resources.styledColor -import kotlin.collections.set import kotlin.math.min abstract class ClipboardAdapter : - RecyclerView.Adapter() { + PagingDataAdapter(diffCallback) { companion object { + private val diffCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: ClipboardEntry, + newItem: ClipboardEntry + ): Boolean { + return true + } + + override fun areContentsTheSame( + oldItem: ClipboardEntry, + newItem: ClipboardEntry + ): Boolean { + return oldItem == newItem + } + } + /** * excerpt text to show on ClipboardEntryUi, to reduce render time of very long text * @param str text to excerpt @@ -49,19 +65,6 @@ abstract class ClipboardAdapter : } } - private val _entries = mutableListOf() - - // maps entry id to list index - // since we don't have much data, we are not using sparse int array here - private val _entriesId = mutableMapOf() - - val entries: List - get() = _entries - - fun getPositionById(id: Int) = _entriesId.getValue(id) - - fun getEntryById(id: Int) = entries[getPositionById(id)] - private var popupMenu: PopupMenu? = null inner class ViewHolder(val entryUi: ClipboardEntryUi) : @@ -72,11 +75,11 @@ abstract class ClipboardAdapter : override fun onBindViewHolder(holder: ViewHolder, position: Int) { with(holder.entryUi) { - val entry = _entries[position] + val entry = getItem(position) ?: return text.text = excerptText(entry.text) pin.visibility = if (entry.pinned) View.VISIBLE else View.INVISIBLE root.setOnClickListener { - onPaste(entry.id) + onPaste(entry) } root.setOnLongClickListener { popupMenu?.dismiss() @@ -95,17 +98,13 @@ abstract class ClipboardAdapter : } if (entry.pinned) menuItem(R.string.unpin, R.drawable.ic_outline_push_pin_24) { onUnpin(entry.id) - setPinStatus(entry.id, false) } else menuItem(R.string.pin, R.drawable.ic_baseline_push_pin_24) { onPin(entry.id) - setPinStatus(entry.id, true) } menuItem(R.string.edit, R.drawable.ic_baseline_edit_24) { onEdit(entry.id) } menuItem(R.string.delete, R.drawable.ic_baseline_delete_24) { - delete(entry.id) - // make `onDelete` access entries after delete onDelete(entry.id) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { @@ -121,53 +120,13 @@ abstract class ClipboardAdapter : } } - private fun delete(id: Int) { - val position = _entriesId.getValue(id) - _entries.removeAt(position) - _entriesId.remove(id) - // Update indices after the removed item - for (i in position until _entries.size) { - _entriesId[_entries[i].id] = i - } - notifyItemRemoved(position) - } - - private fun setPinStatus(id: Int, pinned: Boolean) { - val position = _entriesId.getValue(id) - _entries[position] = _entries[position].copy(pinned = pinned) - notifyItemChanged(position) - // pin will cause a change of order - updateEntries(_entries) - } - - fun updateEntries(entries: List) { - val sorted = entries.sortedWith { o1, o2 -> - when { - o1.pinned && !o2.pinned -> -1 - !o1.pinned && o2.pinned -> 1 - else -> o2.timestamp.compareTo(o1.timestamp) - } - } - val callback = ClipboardEntryDiffCallback(_entries, sorted) - val diff = DiffUtil.calculateDiff(callback) - _entries.clear() - _entries.addAll(sorted) - _entriesId.clear() - _entries.forEachIndexed { index, clipboardEntry -> - _entriesId[clipboardEntry.id] = index - } - diff.dispatchUpdatesTo(this) - } - fun onDetached() { popupMenu?.dismiss() } abstract val theme: Theme - override fun getItemCount(): Int = _entries.size - - abstract fun onPaste(id: Int) + abstract fun onPaste(entry: ClipboardEntry) abstract suspend fun onPin(id: Int) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardWindow.kt index 03f32dcf6..678ec1a21 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardWindow.kt @@ -6,19 +6,22 @@ import androidx.core.text.bold import androidx.core.text.buildSpannedString import androidx.core.text.color import androidx.lifecycle.lifecycleScope +import androidx.paging.Pager +import androidx.paging.PagingConfig import androidx.recyclerview.widget.StaggeredGridLayoutManager +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.fcitx.fcitx5.android.R import org.fcitx.fcitx5.android.data.clipboard.ClipboardManager +import org.fcitx.fcitx5.android.data.clipboard.db.ClipboardEntry import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.data.prefs.ManagedPreference -import org.fcitx.fcitx5.android.data.theme.Theme import org.fcitx.fcitx5.android.input.FcitxInputMethodService import org.fcitx.fcitx5.android.input.clipboard.ClipboardStateMachine.BooleanKey.ClipboardDbEmpty import org.fcitx.fcitx5.android.input.clipboard.ClipboardStateMachine.BooleanKey.ClipboardListeningEnabled -import org.fcitx.fcitx5.android.input.clipboard.ClipboardStateMachine.State.AddMore -import org.fcitx.fcitx5.android.input.clipboard.ClipboardStateMachine.State.EnableListening -import org.fcitx.fcitx5.android.input.clipboard.ClipboardStateMachine.State.Normal +import org.fcitx.fcitx5.android.input.clipboard.ClipboardStateMachine.State.* import org.fcitx.fcitx5.android.input.clipboard.ClipboardStateMachine.TransitionEvent.ClipboardDbUpdated import org.fcitx.fcitx5.android.input.clipboard.ClipboardStateMachine.TransitionEvent.ClipboardListeningUpdated import org.fcitx.fcitx5.android.input.dependency.inputMethodService @@ -50,51 +53,25 @@ class ClipboardWindow : InputWindow.ExtendedInputWindow() { private val clipboardEnabledPref = AppPrefs.getInstance().clipboard.clipboardListening - private fun updateClipboardEntries() { - service.lifecycleScope.launch { - ClipboardManager.getAll().also { - isClipboardDbEmpty = it.isEmpty() - adapter.updateEntries(it) - } - } + private val clipboardEntriesPager by lazy { + Pager(PagingConfig(pageSize = 10)) { ClipboardManager.allEntries() } } + private var adapterSubmitJob: Job? = null - private fun deleteAllEntries(skipPinned: Boolean = true) { + private fun deleteAllEntries(skipPinned: Boolean) { service.lifecycleScope.launch { ClipboardManager.deleteAll(skipPinned) - if (skipPinned) { - ClipboardManager.getAll().also { - isClipboardDbEmpty = it.isEmpty() - adapter.updateEntries(it) - } - } else { - // manually set entries to empty - adapter.updateEntries(emptyList()) - isClipboardDbEmpty = true - } } } - private val onClipboardUpdateListener = ClipboardManager.OnClipboardUpdateListener { - updateClipboardEntries() - } - private val adapter: ClipboardAdapter by lazy { object : ClipboardAdapter() { + override val theme = this@ClipboardWindow.theme override suspend fun onPin(id: Int) = ClipboardManager.pin(id) override suspend fun onUnpin(id: Int) = ClipboardManager.unpin(id) override fun onEdit(id: Int) = AppUtil.launchClipboardEdit(context, id) - override suspend fun onDelete(id: Int) { - ClipboardManager.delete(id) - isClipboardDbEmpty = entries.isEmpty() - } - - override fun onPaste(id: Int) { - service.commitText(getEntryById(id).text) - } - - override val theme: Theme - get() = this@ClipboardWindow.theme + override suspend fun onDelete(id: Int) = ClipboardManager.delete(id) + override fun onPaste(entry: ClipboardEntry) = service.commitText(entry.text) } } @@ -108,10 +85,12 @@ class ClipboardWindow : InputWindow.ExtendedInputWindow() { clipboardEnabledPref.setValue(true) } deleteAllButton.setOnClickListener { - if (adapter.entries.any { !it.pinned }) { - deleteAllEntries() - } else { - promptDeleteAllPinned() + service.lifecycleScope.launch { + if (ClipboardManager.haveUnpinned()) { + deleteAllEntries(skipPinned = true) + } else { + promptDeleteAllPinned() + } } } } @@ -139,7 +118,7 @@ class ClipboardWindow : InputWindow.ExtendedInputWindow() { } add(android.R.string.ok).apply { setOnMenuItemClickListener { - deleteAllEntries(false) + deleteAllEntries(skipPinned = false) true } } @@ -162,16 +141,19 @@ class ClipboardWindow : InputWindow.ExtendedInputWindow() { } // manually switch to initial ui ui.switchUiByState(initialState) - // manually sync clipboard entries form db - updateClipboardEntries() + adapter.addLoadStateListener { + isClipboardDbEmpty = it.append.endOfPaginationReached && adapter.itemCount < 1 + } + adapterSubmitJob = clipboardEntriesPager.flow + .onEach { adapter.submitData(it) } + .launchIn(service.lifecycleScope) clipboardEnabledPref.registerOnChangeListener(clipboardEnabledListener) - ClipboardManager.addOnUpdateListener(onClipboardUpdateListener) } override fun onDetached() { clipboardEnabledPref.unregisterOnChangeListener(clipboardEnabledListener) - ClipboardManager.removeOnUpdateListener(onClipboardUpdateListener) adapter.onDetached() + adapterSubmitJob?.cancel() promptMenu?.dismiss() } From a7cf87220419215c1ad0a1de646dbbc7413cfc54 Mon Sep 17 00:00:00 2001 From: Rocka Date: Thu, 2 Mar 2023 22:05:29 +0800 Subject: [PATCH 048/624] Swipe left/right to delete clipboard item --- .../input/clipboard/ClipboardAdapter.kt | 16 +++--- .../input/clipboard/ClipboardWindow.kt | 49 +++++++++++++++++-- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardAdapter.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardAdapter.kt index d12e23d33..2d526514e 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardAdapter.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardAdapter.kt @@ -8,12 +8,9 @@ import android.view.ViewGroup import android.widget.PopupMenu import androidx.annotation.DrawableRes import androidx.annotation.StringRes -import androidx.lifecycle.findViewTreeLifecycleOwner -import androidx.lifecycle.lifecycleScope import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import kotlinx.coroutines.launch import org.fcitx.fcitx5.android.R import org.fcitx.fcitx5.android.data.clipboard.db.ClipboardEntry import org.fcitx.fcitx5.android.data.theme.Theme @@ -86,12 +83,11 @@ abstract class ClipboardAdapter : val iconColor = ctx.styledColor(android.R.attr.colorControlNormal) val iconColorFilter = PorterDuffColorFilter(iconColor, PorterDuff.Mode.SRC_IN) val popup = PopupMenu(root.context, root) - val scope = root.findViewTreeLifecycleOwner()!!.lifecycleScope - fun menuItem(@StringRes title: Int, @DrawableRes ic: Int, cb: suspend () -> Unit) { + fun menuItem(@StringRes title: Int, @DrawableRes ic: Int, callback: () -> Unit) { popup.menu.add(title).apply { icon = ctx.drawable(ic)?.apply { colorFilter = iconColorFilter } setOnMenuItemClickListener { - scope.launch { cb() } + callback() true } } @@ -120,6 +116,8 @@ abstract class ClipboardAdapter : } } + fun getEntryAt(position: Int) = getItem(position) + fun onDetached() { popupMenu?.dismiss() } @@ -128,12 +126,12 @@ abstract class ClipboardAdapter : abstract fun onPaste(entry: ClipboardEntry) - abstract suspend fun onPin(id: Int) + abstract fun onPin(id: Int) - abstract suspend fun onUnpin(id: Int) + abstract fun onUnpin(id: Int) abstract fun onEdit(id: Int) - abstract suspend fun onDelete(id: Int) + abstract fun onDelete(id: Int) } \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardWindow.kt index 678ec1a21..ca78dc9bd 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardWindow.kt @@ -8,6 +8,8 @@ import androidx.core.text.color import androidx.lifecycle.lifecycleScope import androidx.paging.Pager import androidx.paging.PagingConfig +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.StaggeredGridLayoutManager import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn @@ -67,11 +69,25 @@ class ClipboardWindow : InputWindow.ExtendedInputWindow() { private val adapter: ClipboardAdapter by lazy { object : ClipboardAdapter() { override val theme = this@ClipboardWindow.theme - override suspend fun onPin(id: Int) = ClipboardManager.pin(id) - override suspend fun onUnpin(id: Int) = ClipboardManager.unpin(id) - override fun onEdit(id: Int) = AppUtil.launchClipboardEdit(context, id) - override suspend fun onDelete(id: Int) = ClipboardManager.delete(id) - override fun onPaste(entry: ClipboardEntry) = service.commitText(entry.text) + override fun onPin(id: Int) { + service.lifecycleScope.launch { ClipboardManager.pin(id) } + } + + override fun onUnpin(id: Int) { + service.lifecycleScope.launch { ClipboardManager.unpin(id) } + } + + override fun onEdit(id: Int) { + AppUtil.launchClipboardEdit(context, id) + } + + override fun onDelete(id: Int) { + service.lifecycleScope.launch { ClipboardManager.delete(id) } + } + + override fun onPaste(entry: ClipboardEntry) { + service.commitText(entry.text) + } } } @@ -81,6 +97,29 @@ class ClipboardWindow : InputWindow.ExtendedInputWindow() { layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL) adapter = this@ClipboardWindow.adapter } + ItemTouchHelper(object : ItemTouchHelper.Callback() { + override fun getMovementFlags( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ): Int { + return makeMovementFlags(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) + } + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + return false + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + val entry = adapter.getEntryAt(viewHolder.bindingAdapterPosition) ?: return + service.lifecycleScope.launch { + ClipboardManager.delete(entry.id) + } + } + }).attachToRecyclerView(recyclerView) enableUi.enableButton.setOnClickListener { clipboardEnabledPref.setValue(true) } From e5fa2166ab16cf110abc5afa3286549b7e96f826 Mon Sep 17 00:00:00 2001 From: Rocka Date: Thu, 2 Mar 2023 22:12:30 +0800 Subject: [PATCH 049/624] Remove assets/licenses.json in gitignore --- .gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitignore b/.gitignore index 41db9b36b..49ae2eb54 100644 --- a/.gitignore +++ b/.gitignore @@ -10,8 +10,6 @@ /app/src/main/assets/usr/share/locale # Generated asset descriptor /app/src/main/assets/descriptor.json -# Generated license json -/app/src/main/assets/licenses.json # Intellij .idea/* From 0e92f725b6eb4035d1ff170d2699f3110c70f745 Mon Sep 17 00:00:00 2001 From: Rocka Date: Thu, 2 Mar 2023 22:14:28 +0800 Subject: [PATCH 050/624] Always put cursor after committed text --- .../fcitx5/android/input/FcitxInputMethodService.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt index 919391706..8da71fc86 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt @@ -200,9 +200,15 @@ class FcitxInputMethodService : LifecycleInputMethodService() { fun commitText(text: String) { if (composing.isNotEmpty() && composingText.toString() == text) { // when composing text equals commit content, finish composing text as-is - selection.predict(composing.end) + val cursor = composing.end + selection.predict(cursor) resetComposingState() - inputConnection?.finishComposingText() + inputConnection?.apply { + beginBatchEdit() + finishComposingText() + setSelection(cursor, cursor) + endBatchEdit() + } return } // committed text should replace composing (if any), replace selected range (if any), From 4d9e8a25a7ddc300676553f994f7de5236648362 Mon Sep 17 00:00:00 2001 From: Potato Hatsue <1793913507@qq.com> Date: Fri, 3 Mar 2023 21:52:43 -0500 Subject: [PATCH 051/624] Add play badge --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 0dc94e35e..30e82190c 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,10 @@ GitHub: [![release version](https://img.shields.io/github/v/release/fcitx5-andro alt="Get it on F-Droid" width="207" height="80">](https://f-droid.org/packages/org.fcitx.fcitx5.android) +[Get it on Google Play](https://play.google.com/store/apps/details?id=org.fcitx.fcitx5.android&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1) + ## Project status ### Implemented From eda70637db4b12fbf0ddd9f64f3a9be91466aefe Mon Sep 17 00:00:00 2001 From: Potato Hatsue <1793913507@qq.com> Date: Fri, 3 Mar 2023 21:53:08 -0500 Subject: [PATCH 052/624] Fix zip path traversal vulnerability --- app/src/main/java/org/fcitx/fcitx5/android/utils/Utils.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/utils/Utils.kt b/app/src/main/java/org/fcitx/fcitx5/android/utils/Utils.kt index a704d4ba9..239a16937 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/utils/Utils.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/utils/Utils.kt @@ -233,8 +233,11 @@ fun getSystemProperty(key: String): String { fun ZipInputStream.extract(destDir: File): List { val extracted = mutableListOf() var entry = nextEntry + val canonicalDest = destDir.canonicalPath while (entry != null && !entry.isDirectory) { val file = File(destDir, entry.name) + if (!file.canonicalPath.startsWith(canonicalDest)) + throw SecurityException() copyTo(file.outputStream()) extracted.add(file) entry = nextEntry From e1968f1bb8ee440930621c3f130656322821fbc3 Mon Sep 17 00:00:00 2001 From: Potato Hatsue <1793913507@qq.com> Date: Sat, 4 Mar 2023 00:14:31 -0500 Subject: [PATCH 053/624] Support inline suggestions Co-authored-by: Rocka --- app/build.gradle.kts | 1 + .../fcitx5/android/data/prefs/AppPrefs.kt | 2 + .../android/input/FcitxInputMethodService.kt | 83 ++++++ .../fcitx/fcitx5/android/input/InputView.kt | 9 +- .../android/input/bar/IdleUiStateMachine.kt | 69 ----- .../android/input/bar/KawaiiBarComponent.kt | 180 +++++++++---- .../input/bar/KawaiiBarStateMachine.kt | 24 +- .../fcitx5/android/input/bar/KawaiiBarUi.kt | 248 ++++++++++++------ .../adapter/BaseCandidateViewAdapter.kt | 2 +- .../input/clipboard/ClipboardAdapter.kt | 10 +- .../clipboard/ClipboardEntryDiffCallback.kt | 19 -- .../android/input/keyboard/KeyboardWindow.kt | 7 +- .../drawable/bkg_inline_suggestion_dark.xml | 19 ++ .../drawable/bkg_inline_suggestion_light.xml | 19 ++ app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml-v30/input_method.xml | 12 + flake.lock | 6 +- flake.nix | 2 +- 18 files changed, 459 insertions(+), 254 deletions(-) delete mode 100644 app/src/main/java/org/fcitx/fcitx5/android/input/bar/IdleUiStateMachine.kt delete mode 100644 app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardEntryDiffCallback.kt create mode 100644 app/src/main/res/drawable/bkg_inline_suggestion_dark.xml create mode 100644 app/src/main/res/drawable/bkg_inline_suggestion_light.xml create mode 100644 app/src/main/res/xml-v30/input_method.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6ed4a677f..0af81514e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -303,6 +303,7 @@ tasks.register("cleanCxxIntermediates") { }.also { tasks.clean.dependsOn(it) } dependencies { + implementation("androidx.autofill:autofill:1.1.0") implementation("org.ini4j:ini4j:0.5.4") ksp(project(":codegen")) coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.2") diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt index dc7158f58..b4c0ee0ba 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt @@ -142,6 +142,8 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) { val keyboardHeightPercent: ManagedPreference.PInt val keyboardHeightPercentLandscape: ManagedPreference.PInt + val inlineSuggestions = switch(R.string.inline_suggestions, "inline_suggestions", true) + init { val (primary, secondary) = twinInt( R.string.keyboard_height, diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt index 8da71fc86..b34b96d5d 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/FcitxInputMethodService.kt @@ -1,20 +1,35 @@ package org.fcitx.fcitx5.android.input import android.annotation.SuppressLint +import android.content.res.ColorStateList import android.content.res.Configuration +import android.graphics.Color +import android.graphics.drawable.Icon import android.os.Build +import android.os.Bundle import android.os.SystemClock import android.text.InputType import android.util.LruCache +import android.util.Size import android.view.* import android.view.inputmethod.CursorAnchorInfo import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InlineSuggestionsRequest +import android.view.inputmethod.InlineSuggestionsResponse import android.widget.FrameLayout +import android.widget.inline.InlinePresentationSpec +import androidx.annotation.RequiresApi +import androidx.autofill.inline.UiVersions +import androidx.autofill.inline.common.ImageViewStyle +import androidx.autofill.inline.common.TextViewStyle +import androidx.autofill.inline.common.ViewStyle +import androidx.autofill.inline.v1.InlineSuggestionUi import androidx.core.view.updateLayoutParams import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import org.fcitx.fcitx5.android.R import org.fcitx.fcitx5.android.core.* import org.fcitx.fcitx5.android.daemon.FcitxConnection import org.fcitx.fcitx5.android.daemon.FcitxDaemon @@ -28,10 +43,12 @@ import org.fcitx.fcitx5.android.input.cursor.CursorTracker import org.fcitx.fcitx5.android.utils.alpha import org.fcitx.fcitx5.android.utils.inputConnection import splitties.bitflags.hasFlag +import splitties.dimensions.dp import splitties.resources.styledColor import timber.log.Timber import kotlin.math.max + class FcitxInputMethodService : LifecycleInputMethodService() { private lateinit var fcitx: FcitxConnection @@ -61,6 +78,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() { private var highlightColor: Int = 0x66008577 // material_deep_teal_500 with alpha 0.4 private val ignoreSystemCursor by AppPrefs.getInstance().advanced.ignoreSystemCursor + private val inlineSuggestions by AppPrefs.getInstance().keyboard.inlineSuggestions private val recreateInputViewListener = ManagedPreference.OnChangeListener { _, _ -> recreateInputView(ThemeManager.getActiveTheme()) @@ -192,6 +210,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() { when (val action = imeOptions and EditorInfo.IME_MASK_ACTION) { EditorInfo.IME_ACTION_UNSPECIFIED, EditorInfo.IME_ACTION_NONE -> commitText("\n") + else -> inputConnection?.performEditorAction(action) } } @@ -553,6 +572,68 @@ class FcitxInputMethodService : LifecycleInputMethodService() { ic.endBatchEdit() } + @SuppressLint("RestrictedApi") + @RequiresApi(Build.VERSION_CODES.R) + override fun onCreateInlineSuggestionsRequest(uiExtras: Bundle): InlineSuggestionsRequest? { + if (!inlineSuggestions) return null + val theme = inputView?.theme ?: return null + val chipDrawable = if (theme.isDark) R.drawable.bkg_inline_suggestion_dark else R.drawable.bkg_inline_suggestion_light + val chipBg = Icon.createWithResource(this, chipDrawable).setTint(theme.keyTextColor) + val style = InlineSuggestionUi.newStyleBuilder() + .setSingleIconChipStyle( + ViewStyle.Builder() + .setBackgroundColor(Color.TRANSPARENT) + .setPadding(0, 0, 0, 0) + .build() + ) + .setChipStyle( + ViewStyle.Builder() + .setBackground(chipBg) + .setPadding(dp(10), 0, dp(10), 0) + .build() + ) + .setTitleStyle( + TextViewStyle.Builder() + .setLayoutMargin(dp(4), 0, dp(4), 0) + .setTextColor(theme.keyTextColor) + .setTextSize(14f) + .build() + ) + .setSubtitleStyle( + TextViewStyle.Builder() + .setTextColor(theme.altKeyTextColor) + .setTextSize(12f) + .build() + ) + .setStartIconStyle( + ImageViewStyle.Builder() + .setTintList(ColorStateList.valueOf(theme.altKeyTextColor)) + .build() + ) + .setEndIconStyle( + ImageViewStyle.Builder() + .setTintList(ColorStateList.valueOf(theme.altKeyTextColor)) + .build() + ) + .build() + val styleBundle = UiVersions.newStylesBuilder() + .addStyle(style) + .build() + val spec = InlinePresentationSpec + .Builder(InlinePresentationSpecMinSize, InlinePresentationSpecMaxSize) + .setStyle(styleBundle) + .build() + return InlineSuggestionsRequest.Builder(listOf(spec)) + .setMaxSuggestionCount(InlineSuggestionsRequest.SUGGESTION_COUNT_UNLIMITED) + .build() + } + + @RequiresApi(Build.VERSION_CODES.R) + override fun onInlineSuggestionsResponse(response: InlineSuggestionsResponse): Boolean { + if (!inlineSuggestions) return false + return inputView?.handleInlineSuggestions(response) ?: false + } + override fun onFinishInputView(finishingInput: Boolean) { Timber.d("onFinishInputView: finishingInput=$finishingInput") inputConnection?.finishComposingText() @@ -597,5 +678,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() { companion object { val EmptyEditorInfo = EditorInfo() const val DeleteSurroundingFlag = "org.fcitx.fcitx5.android.DELETE_SURROUNDING" + private val InlinePresentationSpecMinSize = Size(0, 0) + private val InlinePresentationSpecMaxSize = Size(Int.MAX_VALUE, Int.MAX_VALUE) } } \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt index fc4a0fbf3..9dde40624 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt @@ -8,8 +8,10 @@ import android.os.Build import android.view.View import android.view.WindowManager import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InlineSuggestionsResponse import android.widget.ImageView import android.widget.Space +import androidx.annotation.RequiresApi import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.* import androidx.lifecycle.lifecycleScope @@ -221,7 +223,7 @@ class InputView( centerVertically() centerHorizontally() }) - add(kawaiiBar.view, lParams(matchParent, dp(40)) { + add(kawaiiBar.view, lParams(matchParent, dp(KawaiiBarComponent.HEIGHT)) { topOfParent() centerHorizontally() }) @@ -352,6 +354,11 @@ class InputView( showingDialog?.dismiss() } + @RequiresApi(Build.VERSION_CODES.R) + fun handleInlineSuggestions(response: InlineSuggestionsResponse): Boolean { + return kawaiiBar.handleInlineSuggestions(response) + } + override fun onDetachedFromWindow() { keyboardSizePrefs.forEach { it.unregisterOnChangeListener(onKeyboardSizeChangeListener) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/IdleUiStateMachine.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/IdleUiStateMachine.kt deleted file mode 100644 index 161d7f7b2..000000000 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/IdleUiStateMachine.kt +++ /dev/null @@ -1,69 +0,0 @@ -package org.fcitx.fcitx5.android.input.bar - -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.BooleanKey.ClipboardEmpty -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.State.Clipboard -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.State.ClipboardTimedOut -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.State.Empty -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.State.Toolbar -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.State.ToolbarWithClip -import org.fcitx.fcitx5.android.utils.BuildTransitionEvent -import org.fcitx.fcitx5.android.utils.EventStateMachine -import org.fcitx.fcitx5.android.utils.TransitionBuildBlock - -object IdleUiStateMachine { - enum class State { - Clipboard, Toolbar, Empty, ToolbarWithClip, ClipboardTimedOut - } - - enum class BooleanKey : EventStateMachine.BooleanStateKey { - ClipboardEmpty - } - - enum class TransitionEvent(val builder: TransitionBuildBlock) : - EventStateMachine.TransitionEvent by BuildTransitionEvent(builder) { - Timeout({ - from(ToolbarWithClip) transitTo Toolbar - from(Clipboard) transitTo ClipboardTimedOut - }), - Pasted({ - accept { initial, current, _ -> - when (current) { - Clipboard -> initial - ClipboardTimedOut -> initial - else -> current - } - } - }), - MenuButtonClicked({ - from(Toolbar) transitTo Empty - from(ToolbarWithClip) transitTo Clipboard - from(Clipboard) transitTo ToolbarWithClip - from(ClipboardTimedOut) transitTo Toolbar - from(Empty) transitTo Toolbar - }), - ClipboardUpdated({ - accept { initial, current, bool -> - val clipboardEmpty = bool(ClipboardEmpty) == true - when (current) { - ToolbarWithClip -> if (clipboardEmpty) Toolbar else Clipboard - Clipboard -> if (clipboardEmpty) initial else current - ClipboardTimedOut -> if (clipboardEmpty) initial else Clipboard - Toolbar -> if (!clipboardEmpty) Clipboard else current - Empty -> if (!clipboardEmpty) Clipboard else current - } - } - }), - KawaiiBarShown({ - accept { initial, current, _ -> - if (current == ClipboardTimedOut) initial - else current - } - }) - } - - fun new(toolbarByDefault: Boolean, block: (State) -> Unit) = - EventStateMachine(if (toolbarByDefault) Toolbar else Empty).apply { - onNewStateListener = block - } - -} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt index 0a6dff48c..83babff4c 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt @@ -1,16 +1,21 @@ package org.fcitx.fcitx5.android.input.bar +import android.annotation.SuppressLint import android.graphics.Color import android.os.Build +import android.util.Size import android.view.KeyEvent import android.view.View +import android.view.ViewGroup import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InlineSuggestion +import android.view.inputmethod.InlineSuggestionsResponse import android.widget.FrameLayout import android.widget.ViewAnimator +import android.widget.inline.InlineContentView +import androidx.annotation.RequiresApi import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import org.fcitx.fcitx5.android.R import org.fcitx.fcitx5.android.core.CapabilityFlag import org.fcitx.fcitx5.android.core.CapabilityFlags @@ -19,9 +24,8 @@ import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.data.prefs.ManagedPreference import org.fcitx.fcitx5.android.data.theme.ThemeManager import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.State.* -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.BooleanKey.ClipboardEmpty -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.TransitionEvent.* -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.BooleanKey.* +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.BooleanKey.CandidateEmpty +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.BooleanKey.PreeditEmpty import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.* import org.fcitx.fcitx5.android.input.broadcast.InputBroadcastReceiver import org.fcitx.fcitx5.android.input.candidates.HorizontalCandidateComponent @@ -44,12 +48,16 @@ import org.fcitx.fcitx5.android.utils.AppUtil import org.mechdancer.dependency.DynamicScope import org.mechdancer.dependency.manager.must import splitties.bitflags.hasFlag +import splitties.dimensions.dp import splitties.views.backgroundColor import splitties.views.dsl.core.add import splitties.views.dsl.core.lParams import splitties.views.dsl.core.matchParent import splitties.views.imageResource import timber.log.Timber +import java.util.concurrent.Executor +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine class KawaiiBarComponent : UniqueViewComponent(), InputBroadcastReceiver { @@ -65,20 +73,27 @@ class KawaiiBarComponent : UniqueViewComponent( private val clipboardSuggestion = AppPrefs.getInstance().clipboard.clipboardSuggestion private val clipboardItemTimeout = AppPrefs.getInstance().clipboard.clipboardItemTimeout private val expandedCandidateStyle by AppPrefs.getInstance().keyboard.expandedCandidateStyle - private val expandToolbarByDefault = AppPrefs.getInstance().keyboard.expandToolbarByDefault + private val expandToolbarByDefault by AppPrefs.getInstance().keyboard.expandToolbarByDefault private val toolbarNumRowOnPassword by AppPrefs.getInstance().keyboard.toolbarNumRowOnPassword private var clipboardTimeoutJob: Job? = null + private var isClipboardEmpty: Boolean = true + private var isInlineSuggestionEmpty: Boolean = true + private var isCapabilityFlagsPassword: Boolean = false + private var isKeyboardLayoutNumber: Boolean = false + private val onClipboardUpdateListener = ClipboardManager.OnClipboardUpdateListener { if (!clipboardSuggestion.getValue()) return@OnClipboardUpdateListener service.lifecycleScope.launch { if (it.text.isEmpty()) { - idleUiStateMachine.push(ClipboardUpdated, ClipboardEmpty to true) + isClipboardEmpty = true + evalIdleUiState() } else { idleUi.setClipboardItemText(it.text.take(42)) - idleUiStateMachine.push(ClipboardUpdated, ClipboardEmpty to false) + isClipboardEmpty = false + evalIdleUiState() launchClipboardTimeoutJob() } } @@ -87,7 +102,8 @@ class KawaiiBarComponent : UniqueViewComponent( private val onClipboardSuggestionUpdateListener = ManagedPreference.OnChangeListener { _, it -> if (!it) { - idleUiStateMachine.push(ClipboardUpdated, ClipboardEmpty to true) + isClipboardEmpty = true + evalIdleUiState() clipboardTimeoutJob?.cancel() clipboardTimeoutJob = null } @@ -95,24 +111,15 @@ class KawaiiBarComponent : UniqueViewComponent( private val onClipboardTimeoutUpdateListener = ManagedPreference.OnChangeListener { _, _ -> - when (idleUiStateMachine.currentState) { - IdleUiStateMachine.State.Clipboard, - IdleUiStateMachine.State.ToolbarWithClip -> { + when (idleUi.currentState) { + KawaiiBarUi.Idle.State.Clipboard -> { // renew timeout when clipboard suggestion is present launchClipboardTimeoutJob() } - else -> {} } } - private val onExpandToolbarByDefaultUpdateListener = - ManagedPreference.OnChangeListener { _, it -> - idleUiStateMachine = IdleUiStateMachine.new(it) { - idleUi.switchUiByState(it) - } - } - private val popupActionListener by lazy { popup.listener } @@ -124,15 +131,44 @@ class KawaiiBarComponent : UniqueViewComponent( if (timeout < 0L) return clipboardTimeoutJob = service.lifecycleScope.launch { delay(timeout) - idleUiStateMachine.push(Timeout) + isClipboardEmpty = true + evalIdleUiState() clipboardTimeoutJob = null } } + private fun evalIdleUiState() { + val newState = when { + !isClipboardEmpty -> KawaiiBarUi.Idle.State.Clipboard + !isInlineSuggestionEmpty -> + KawaiiBarUi.Idle.State.InlineSuggestion + isCapabilityFlagsPassword && !isKeyboardLayoutNumber -> + KawaiiBarUi.Idle.State.NumberRow + else -> KawaiiBarUi.Idle.State.Empty + } + if (newState == idleUi.currentState) + return + else if (idleUi.currentState != KawaiiBarUi.Idle.State.Empty + && newState == KawaiiBarUi.Idle.State.Empty + ) { + if (expandToolbarByDefault || idleUi.isToolbarExpanded) { + idleUi.updateState(newState) + idleUi.expandToolbar() + } else + idleUi.updateState(newState) + } else idleUi.updateState(newState) + } + private val idleUi: KawaiiBarUi.Idle by lazy { - KawaiiBarUi.Idle(context, theme) { idleUiStateMachine.currentState }.apply { + KawaiiBarUi.Idle( + context, + theme, + popupActionListener, + popup, + commonKeyActionListener + ).apply { menuButton.setOnClickListener { - idleUiStateMachine.push(MenuButtonClicked) + idleUi.toggleToolbar() // reset timeout timer (if present) when user switch layout if (clipboardTimeoutJob != null) { launchClipboardTimeoutJob() @@ -159,7 +195,8 @@ class KawaiiBarComponent : UniqueViewComponent( } clipboardTimeoutJob?.cancel() clipboardTimeoutJob = null - idleUiStateMachine.push(Pasted) + isClipboardEmpty = true + evalIdleUiState() } clipboardSuggestionItem.setOnLongClickListener { ClipboardManager.lastEntry?.let { @@ -170,7 +207,6 @@ class KawaiiBarComponent : UniqueViewComponent( hideKeyboardButton.setOnClickListener { service.requestHideSelf(0) } - switchUiByState(idleUiStateMachine.currentState) } } @@ -180,8 +216,6 @@ class KawaiiBarComponent : UniqueViewComponent( private val titleUi by lazy { KawaiiBarUi.Title(context, theme) } - private val numberRowUi by lazy { KawaiiBarUi.NumberRowUi(context, theme) } - val barStateMachine = KawaiiBarStateMachine.new { switchUiByState(it) } @@ -204,9 +238,6 @@ class KawaiiBarComponent : UniqueViewComponent( } } - private var idleUiStateMachine = IdleUiStateMachine.new(expandToolbarByDefault.getValue()) { - idleUi.switchUiByState(it) - } // set expand candidate button to create expand candidate private fun setExpandButtonToAttach() { @@ -248,14 +279,6 @@ class KawaiiBarComponent : UniqueViewComponent( titleUi.setTitle("") titleUi.removeExtension() } - if (new == numberRowUi.root) { - numberRowUi.root.keyActionListener = commonKeyActionListener.listener - numberRowUi.root.popupActionListener = popupActionListener - } else { - numberRowUi.root.keyActionListener = null - numberRowUi.root.popupActionListener = null - popup.dismissAll() - } view.displayedChild = index } @@ -267,7 +290,6 @@ class KawaiiBarComponent : UniqueViewComponent( add(idleUi.root, lParams(matchParent, matchParent)) add(candidateUi.root, lParams(matchParent, matchParent)) add(titleUi.root, lParams(matchParent, matchParent)) - add(numberRowUi.root, lParams(matchParent, matchParent)) } } @@ -282,19 +304,18 @@ class KawaiiBarComponent : UniqueViewComponent( ClipboardManager.addOnUpdateListener(onClipboardUpdateListener) clipboardSuggestion.registerOnChangeListener(onClipboardSuggestionUpdateListener) clipboardItemTimeout.registerOnChangeListener(onClipboardTimeoutUpdateListener) - expandToolbarByDefault.registerOnChangeListener(onExpandToolbarByDefaultUpdateListener) } override fun onStartInput(info: EditorInfo, capFlags: CapabilityFlags) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { idleUi.privateMode(info.imeOptions.hasFlag(EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING)) } - idleUiStateMachine.push(KawaiiBarShown) - barStateMachine.setBooleanState( - CapFlagsPassword, - toolbarNumRowOnPassword && capFlags.has(CapabilityFlag.Password) - ) - barStateMachine.push(CapFlagsUpdated) + isCapabilityFlagsPassword = toolbarNumRowOnPassword && capFlags.has(CapabilityFlag.Password) + isInlineSuggestionEmpty = true + idleUi.inlineSuggestionsBar.clear() + evalIdleUiState() + if (idleUi.currentState == KawaiiBarUi.Idle.State.Empty && expandToolbarByDefault) + idleUi.expandToolbar() } override fun onPreeditEmptyStateUpdate(empty: Boolean) { @@ -325,4 +346,69 @@ class KawaiiBarComponent : UniqueViewComponent( barStateMachine.push(WindowDetached) } + private val suggestionSize by lazy { + Size(ViewGroup.LayoutParams.WRAP_CONTENT, context.dp(HEIGHT)) + } + + private val directExecutor by lazy { + Executor { it.run() } + } + + @SuppressLint("NotifyDataSetChanged") + @RequiresApi(Build.VERSION_CODES.R) + fun handleInlineSuggestions(response: InlineSuggestionsResponse): Boolean { + if (response.inlineSuggestions.isEmpty()) { + isInlineSuggestionEmpty = true + return true + } + var pinned: InlineSuggestion? = null + val scrollable = mutableListOf() + var extraPinnedCount = 0 + response.inlineSuggestions.forEach { + if (it.info.isPinned) { + if (pinned == null) { + pinned = it + } else { + scrollable.add(extraPinnedCount++, it) + } + } else { + scrollable.add(it) + } + } + service.lifecycleScope.launch { + idleUi.inlineSuggestionsBar.setPinnedView( + pinned?.let { inflateInlineContentView(it) } + ) + } + service.lifecycleScope.launch { + val views = scrollable.map { s -> + service.lifecycleScope.async { + inflateInlineContentView(s) + } + }.awaitAll() + idleUi.inlineSuggestionsBar.setScrollableViews(views) + } + isInlineSuggestionEmpty = false + evalIdleUiState() + return true + } + + @RequiresApi(Build.VERSION_CODES.R) + private suspend fun inflateInlineContentView(suggestion: InlineSuggestion): InlineContentView { + return suspendCoroutine { c -> + suggestion.inflate(context, suggestionSize, directExecutor) { v -> + c.resume(v) + } + } + } + + companion object { + const val HEIGHT = 40 + } + + fun onKeyboardLayoutSwitched(isNumber: Boolean) { + isKeyboardLayoutNumber = isNumber + evalIdleUiState() + } + } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarStateMachine.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarStateMachine.kt index ef30a9a34..c35d26a26 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarStateMachine.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarStateMachine.kt @@ -1,23 +1,19 @@ package org.fcitx.fcitx5.android.input.bar import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.BooleanKey.CandidateEmpty -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.BooleanKey.CapFlagsPassword import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.BooleanKey.PreeditEmpty -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.State.Candidate -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.State.Idle -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.State.NumberRow -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.State.Title +import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.State.* import org.fcitx.fcitx5.android.utils.BuildTransitionEvent import org.fcitx.fcitx5.android.utils.EventStateMachine import org.fcitx.fcitx5.android.utils.TransitionBuildBlock object KawaiiBarStateMachine { enum class State { - Idle, Candidate, Title, NumberRow + Idle, Candidate, Title } enum class BooleanKey : EventStateMachine.BooleanStateKey { - PreeditEmpty, CandidateEmpty, CapFlagsPassword + PreeditEmpty, CandidateEmpty } enum class TransitionEvent(val builder: TransitionBuildBlock) : @@ -37,24 +33,12 @@ object KawaiiBarStateMachine { ExtendedWindowAttached({ from(Idle) transitTo Title from(Candidate) transitTo Title - from(NumberRow) transitTo Title - }), - CapFlagsUpdated({ - from(Idle) transitTo NumberRow on (CapFlagsPassword to true) - from(NumberRow) transitTo Idle on (CapFlagsPassword to false) }), WindowDetached({ // candidate state has higher priority so here it goes first from(Title) transitTo Candidate on (CandidateEmpty to false) - from(Title) transitTo Idle on (CapFlagsPassword to false) - from(Title) transitTo NumberRow on (CapFlagsPassword to true) - }), - KeyboardSwitchedOutNumber({ - from(Idle) transitTo NumberRow on (CapFlagsPassword to true) + from(Title) transitTo Idle }), - KeyboardSwitchedToNumber({ - from(NumberRow) transitTo Idle - }) } fun new(block: (State) -> Unit) = diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarUi.kt index d9c7f8ec2..bf4740821 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarUi.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarUi.kt @@ -1,29 +1,34 @@ package org.fcitx.fcitx5.android.input.bar -import android.annotation.SuppressLint import android.content.Context import android.graphics.PorterDuff import android.graphics.PorterDuffColorFilter import android.graphics.Typeface +import android.os.Build import android.text.TextUtils import android.view.View import android.view.animation.AlphaAnimation import android.view.animation.AnimationSet -import android.view.animation.ScaleAnimation import android.view.animation.TranslateAnimation +import android.widget.HorizontalScrollView import android.widget.ViewAnimator +import android.widget.inline.InlineContentView import androidx.annotation.DrawableRes +import androidx.annotation.RequiresApi import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import com.google.android.flexbox.FlexWrap +import com.google.android.flexbox.FlexboxLayout +import com.google.android.flexbox.JustifyContent +import kotlinx.coroutines.* import org.fcitx.fcitx5.android.R import org.fcitx.fcitx5.android.core.KeySym import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.data.theme.Theme -import org.fcitx.fcitx5.android.input.bar.IdleUiStateMachine.State.* -import org.fcitx.fcitx5.android.input.keyboard.BaseKeyboard -import org.fcitx.fcitx5.android.input.keyboard.CustomGestureView -import org.fcitx.fcitx5.android.input.keyboard.KeyAction -import org.fcitx.fcitx5.android.input.keyboard.KeyDef +import org.fcitx.fcitx5.android.input.keyboard.* +import org.fcitx.fcitx5.android.input.popup.PopupActionListener +import org.fcitx.fcitx5.android.input.popup.PopupComponent import org.fcitx.fcitx5.android.utils.rippleDrawable import splitties.dimensions.dp import splitties.views.dsl.constraintlayout.* @@ -67,24 +72,32 @@ sealed class KawaiiBarUi(override val ctx: Context, protected val theme: Theme) class Idle( ctx: Context, theme: Theme, - private val getCurrentState: () -> IdleUiStateMachine.State, + private val popupActionListener: PopupActionListener, + private val popup: PopupComponent, + private val commonKeyActionListener: CommonKeyActionListener ) : KawaiiBarUi(ctx, theme) { - private val IdleUiStateMachine.State.menuButtonRotation + enum class State { + Clipboard, Empty, NumberRow, InlineSuggestion + } + + var currentState = State.Empty + private set + private val menuButtonRotation get() = - if (inPrivate) 0f - else when (this) { - Empty -> -90f - Clipboard -> -90f - Toolbar -> 90f - ToolbarWithClip -> 90f - ClipboardTimedOut -> -90f + when { + inPrivate -> 0f + isToolbarExpanded -> 90f + else -> -90f } private var inPrivate = false + var isToolbarExpanded = false + private set + val menuButton = toolButton(R.drawable.ic_baseline_expand_more_24) { - rotation = getCurrentState().menuButtonRotation + rotation = menuButtonRotation } val undoButton = toolButton(R.drawable.ic_baseline_undo_24) @@ -140,7 +153,6 @@ sealed class KawaiiBarUi(override val ctx: Context, protected val theme: Theme) val clipboardSuggestionItem = object : CustomGestureView(ctx) { init { - visibility = View.GONE isHapticFeedbackEnabled = false background = rippleDrawable(theme.keyPressHighlightColor) add(clipboardSuggestionLayout, lParams(wrapContent, matchParent)) @@ -149,17 +161,93 @@ sealed class KawaiiBarUi(override val ctx: Context, protected val theme: Theme) private val clipboardBar = constraintLayout { add(clipboardSuggestionItem, lParams(wrapContent, matchConstraints) { - topOfParent() - startOfParent() - endOfParent() - bottomOfParent() + centerInParent() verticalMargin = dp(4) }) } + private val emptyBar = constraintLayout() + + private val numberRowBar = object : BaseKeyboard( + ctx, + theme, + listOf(listOf("1", "2", "3", "4", "5", "6", "7", "8", "9", "0").map { digit -> + KeyDef( + KeyDef.Appearance.Text( + displayText = digit, + textSize = 21f, + border = KeyDef.Appearance.Border.Off, + margin = false + ), + setOf( + KeyDef.Behavior.Press(KeyAction.SymAction(KeySym(digit.codePointAt(0)))) + ), + arrayOf(KeyDef.Popup.Preview(digit)) + ) + }) + ) {} + + class InlineSuggestionsUi(override val ctx: Context) : Ui { + + private val scrollView = ctx.view(::HorizontalScrollView) { + isFillViewport = true + scrollBarSize = dp(1) + } + + private val pinnedView = frameLayout() + + override val root = constraintLayout { + add(scrollView, lParams(matchConstraints, matchParent) { + startOfParent() + before(pinnedView) + centerVertically() + }) + add(pinnedView, lParams(wrapContent, matchParent) { + endOfParent() + centerVertically() + }) + } + + fun clear() { + scrollView.removeAllViews() + pinnedView.removeAllViews() + } + + @RequiresApi(Build.VERSION_CODES.R) + fun setPinnedView(view: InlineContentView?) { + pinnedView.removeAllViews() + if (view != null) { + pinnedView.addView(view) + } + } + + @RequiresApi(Build.VERSION_CODES.R) + fun setScrollableViews(views: List) { + val flexbox = view(::FlexboxLayout) { + flexWrap = FlexWrap.NOWRAP + justifyContent = JustifyContent.CENTER + views.forEach { + addView(it) + it.updateLayoutParams { + flexShrink = 0f + } + } + } + scrollView.apply { + scrollTo(0, 0) + removeAllViews() + add(flexbox, lParams(wrapContent, matchParent)) + } + } + } + + val inlineSuggestionsBar = InlineSuggestionsUi(ctx) + private val animator = ViewAnimator(ctx).apply { + add(emptyBar, lParams(matchParent, matchParent)) add(clipboardBar, lParams(matchParent, matchParent)) add(buttonsBar, lParams(matchParent, matchParent)) + add(inlineSuggestionsBar.root, lParams(matchParent, matchParent)) if (disableAnimation) { inAnimation = null @@ -168,13 +256,14 @@ sealed class KawaiiBarUi(override val ctx: Context, protected val theme: Theme) inAnimation = AnimationSet(true).apply { duration = 200L addAnimation(AlphaAnimation(0f, 1f)) - addAnimation(ScaleAnimation(0f, 1f, 0f, 1f, 0f, dp(20f))) + // TODO: rework InlineContentView animation +// addAnimation(ScaleAnimation(0f, 1f, 0f, 1f, 0f, dp(20f))) addAnimation(TranslateAnimation(dp(-100f), 0f, 0f, 0f)) } outAnimation = AnimationSet(true).apply { duration = 200L addAnimation(AlphaAnimation(1f, 0f)) - addAnimation(ScaleAnimation(1f, 0f, 1f, 0f, 0f, dp(20f))) +// addAnimation(ScaleAnimation(1f, 0f, 1f, 0f, 0f, dp(20f))) addAnimation(TranslateAnimation(0f, dp(-100f), 0f, 0f)) } } @@ -182,11 +271,13 @@ sealed class KawaiiBarUi(override val ctx: Context, protected val theme: Theme) override val root = constraintLayout { addButton(menuButton) { startOfParent() } - add(animator, lParams(matchConstraints, dp(40)) { + add(animator, lParams(matchConstraints, matchParent) { after(menuButton) before(hideKeyboardButton) }) addButton(hideKeyboardButton) { endOfParent() } + numberRowBar.visibility = View.GONE + add(numberRowBar, lParams(matchParent, matchParent)) } fun privateMode(activate: Boolean = true) { @@ -203,7 +294,7 @@ sealed class KawaiiBarUi(override val ctx: Context, protected val theme: Theme) } private fun updateMenuButtonRotation(instant: Boolean = false) { - val targetRotation = getCurrentState().menuButtonRotation + val targetRotation = menuButtonRotation menuButton.apply { if (targetRotation == rotation) return if (instant || disableAnimation) { @@ -214,52 +305,63 @@ sealed class KawaiiBarUi(override val ctx: Context, protected val theme: Theme) } } - private fun transitionToClipboardBar() { - animator.displayedChild = 0 + fun expandToolbar() { + Timber.d("Expand idle ui toolbar at $currentState") + if (animator.displayedChild != 2) + animator.displayedChild = 2 + isToolbarExpanded = true + updateMenuButtonRotation() } - private fun transitionToButtonsBar() { - animator.displayedChild = 1 + fun collapseToolbar() { + Timber.d("Collapse idle ui toolbar at $currentState") + switchUiByState(currentState) + isToolbarExpanded = false + updateMenuButtonRotation() } - fun switchUiByState(state: IdleUiStateMachine.State) { - Timber.d("Switch idle ui to $state") - when (state) { - Clipboard -> { - transitionToClipboardBar() - enableClipboardItem() - } - - Toolbar -> { - transitionToButtonsBar() - disableClipboardItem() - } - - Empty -> { - // empty and clipboard share the same view - transitionToClipboardBar() - disableClipboardItem() - setClipboardItemText("") - } + fun toggleToolbar() { + if (isToolbarExpanded) + collapseToolbar() + else + expandToolbar() + } - ToolbarWithClip -> { - transitionToButtonsBar() - } + fun updateState(state: State) { + currentState = state + switchUiByState(state) + } - ClipboardTimedOut -> { - transitionToClipboardBar() - } + private fun switchUiByState(state: State) { + Timber.d("Switch idle ui to $state") + when (state) { + State.Clipboard -> animator.displayedChild = 1 + State.Empty -> animator.displayedChild = 0 + State.NumberRow -> {} + State.InlineSuggestion -> animator.displayedChild = 3 + } + if (state != State.Empty) { + isToolbarExpanded = false + } + if (state == State.NumberRow) { + menuButton.visibility = View.GONE + hideKeyboardButton.visibility = View.GONE + animator.visibility = View.GONE + numberRowBar.visibility = View.VISIBLE + numberRowBar.keyActionListener = commonKeyActionListener.listener + numberRowBar.popupActionListener = popupActionListener + } else { + menuButton.visibility = View.VISIBLE + hideKeyboardButton.visibility = View.VISIBLE + animator.visibility = View.VISIBLE + numberRowBar.visibility = View.GONE + numberRowBar.keyActionListener = null + numberRowBar.popupActionListener = null + popup.dismissAll() } updateMenuButtonRotation() } - private fun enableClipboardItem() { - clipboardSuggestionItem.visibility = View.VISIBLE - } - - private fun disableClipboardItem() { - clipboardSuggestionItem.visibility = View.GONE - } fun setClipboardItemText(text: String) { clipboardText.text = text @@ -329,26 +431,4 @@ sealed class KawaiiBarUi(override val ctx: Context, protected val theme: Theme) } } - class NumberRowUi(ctx: Context, theme: Theme) : KawaiiBarUi(ctx, theme) { - @SuppressLint("ViewConstructor") - class Keyboard(ctx: Context, theme: Theme) : BaseKeyboard( - ctx, - theme, - listOf(listOf("1", "2", "3", "4", "5", "6", "7", "8", "9", "0").map(::NumKey)) - ) { - class NumKey(digit: String) : KeyDef( - Appearance.Text( - displayText = digit, - textSize = 21f, - border = Appearance.Border.Off, - margin = false - ), - setOf(Behavior.Press(KeyAction.SymAction(KeySym(digit.codePointAt(0))))), - arrayOf(Popup.Preview(digit)) - ) - } - - override val root = Keyboard(ctx, theme) - - } } \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/adapter/BaseCandidateViewAdapter.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/adapter/BaseCandidateViewAdapter.kt index 2f5f1b49f..ba7f13799 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/adapter/BaseCandidateViewAdapter.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/adapter/BaseCandidateViewAdapter.kt @@ -9,7 +9,7 @@ import org.fcitx.fcitx5.android.input.candidates.CandidateItemUi abstract class BaseCandidateViewAdapter : RecyclerView.Adapter() { - inner class ViewHolder(val ui: CandidateItemUi) : RecyclerView.ViewHolder(ui.root) { + class ViewHolder(val ui: CandidateItemUi) : RecyclerView.ViewHolder(ui.root) { var idx = -1 } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardAdapter.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardAdapter.kt index 2d526514e..9161d8fc3 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardAdapter.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardAdapter.kt @@ -56,7 +56,12 @@ abstract class ClipboardAdapter : break } else { // append one line exactly - appendLine(str.substring(start, min(min(lineBreak, start + chars), totalLength))) + appendLine( + str.substring( + start, + min(min(lineBreak, start + chars), totalLength) + ) + ) } } } @@ -64,8 +69,7 @@ abstract class ClipboardAdapter : private var popupMenu: PopupMenu? = null - inner class ViewHolder(val entryUi: ClipboardEntryUi) : - RecyclerView.ViewHolder(entryUi.root) + class ViewHolder(val entryUi: ClipboardEntryUi) : RecyclerView.ViewHolder(entryUi.root) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = ViewHolder(ClipboardEntryUi(parent.context, theme)) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardEntryDiffCallback.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardEntryDiffCallback.kt deleted file mode 100644 index 6b05be4aa..000000000 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardEntryDiffCallback.kt +++ /dev/null @@ -1,19 +0,0 @@ -package org.fcitx.fcitx5.android.input.clipboard - -import androidx.recyclerview.widget.DiffUtil -import org.fcitx.fcitx5.android.data.clipboard.db.ClipboardEntry - -class ClipboardEntryDiffCallback( - val old: List, - val new: List -) : DiffUtil.Callback() { - override fun getOldListSize(): Int = old.size - - override fun getNewListSize(): Int = new.size - - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = - old[oldItemPosition].id == new[newItemPosition].id - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = - old[oldItemPosition] == new[newItemPosition] -} \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt index c1647588b..ad4001ddd 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt @@ -12,8 +12,6 @@ import org.fcitx.fcitx5.android.core.CapabilityFlags import org.fcitx.fcitx5.android.core.InputMethodEntry import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.input.bar.KawaiiBarComponent -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.KeyboardSwitchedOutNumber -import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.KeyboardSwitchedToNumber import org.fcitx.fcitx5.android.input.broadcast.InputBroadcastReceiver import org.fcitx.fcitx5.android.input.broadcast.ReturnKeyDrawableComponent import org.fcitx.fcitx5.android.input.dependency.fcitx @@ -178,9 +176,6 @@ class KeyboardWindow : InputWindow.SimpleInputWindow(), Essentia // 1) the keyboard window was newly attached // 2) currently keyboard window is attached and switchLayout was used private fun notifyBarLayoutChanged() { - if (currentKeyboardName == NumberKeyboard.Name) - bar.barStateMachine.push(KeyboardSwitchedToNumber) - else - bar.barStateMachine.push(KeyboardSwitchedOutNumber) + bar.onKeyboardLayoutSwitched(currentKeyboardName == NumberKeyboard.Name) } } \ No newline at end of file diff --git a/app/src/main/res/drawable/bkg_inline_suggestion_dark.xml b/app/src/main/res/drawable/bkg_inline_suggestion_dark.xml new file mode 100644 index 000000000..e50ffdc22 --- /dev/null +++ b/app/src/main/res/drawable/bkg_inline_suggestion_dark.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/bkg_inline_suggestion_light.xml b/app/src/main/res/drawable/bkg_inline_suggestion_light.xml new file mode 100644 index 000000000..dbbcbd0c1 --- /dev/null +++ b/app/src/main/res/drawable/bkg_inline_suggestion_light.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 27de9da20..f6bb3c700 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -207,4 +207,5 @@ Never fill width Fill width on demand Always fill width + Inline suggestions \ No newline at end of file diff --git a/app/src/main/res/xml-v30/input_method.xml b/app/src/main/res/xml-v30/input_method.xml new file mode 100644 index 000000000..d84c68696 --- /dev/null +++ b/app/src/main/res/xml-v30/input_method.xml @@ -0,0 +1,12 @@ + + + + diff --git a/flake.lock b/flake.lock index 1d57a29ab..6da7a08f6 100644 --- a/flake.lock +++ b/flake.lock @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1677342105, - "narHash": "sha256-kv1fpkfCJGb0M+LZaCHFUuIS9kRIwyVgupHu86Y28nc=", + "lastModified": 1677932085, + "narHash": "sha256-+AB4dYllWig8iO6vAiGGYl0NEgmMgGHpy9gzWJ3322g=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "b1f87ca164a9684404c8829b851c3586c4d9f089", + "rev": "3c5319ad3aa51551182ac82ea17ab1c6b0f0df89", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 35bd19740..19a3317ff 100644 --- a/flake.nix +++ b/flake.nix @@ -25,7 +25,7 @@ gettext python39 icu - androidStudioPackages.beta + androidStudioPackages.stable ]; ANDROID_SDK_ROOT = "${androidComposition.androidsdk}/libexec/android-sdk"; From af23219928f71ec2fc7bd2befdecf86cc833d40d Mon Sep 17 00:00:00 2001 From: Rocka Date: Thu, 9 Mar 2023 21:26:04 +0800 Subject: [PATCH 054/624] Wrap InlineContentView in SurfaceView --- .../android/input/bar/KawaiiBarComponent.kt | 4 +- .../fcitx5/android/input/bar/KawaiiBarUi.kt | 79 ++++++++++++++++--- 2 files changed, 73 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt index 83babff4c..697b3f693 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt @@ -312,7 +312,9 @@ class KawaiiBarComponent : UniqueViewComponent( } isCapabilityFlagsPassword = toolbarNumRowOnPassword && capFlags.has(CapabilityFlag.Password) isInlineSuggestionEmpty = true - idleUi.inlineSuggestionsBar.clear() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + idleUi.inlineSuggestionsBar.clear() + } evalIdleUiState() if (idleUi.currentState == KawaiiBarUi.Idle.State.Empty && expandToolbarByDefault) idleUi.expandToolbar() diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarUi.kt index bf4740821..bf6580b0b 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarUi.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarUi.kt @@ -6,10 +6,13 @@ import android.graphics.PorterDuffColorFilter import android.graphics.Typeface import android.os.Build import android.text.TextUtils +import android.view.SurfaceControl +import android.view.SurfaceView import android.view.View import android.view.animation.AlphaAnimation import android.view.animation.AnimationSet import android.view.animation.TranslateAnimation +import android.widget.FrameLayout import android.widget.HorizontalScrollView import android.widget.ViewAnimator import android.widget.inline.InlineContentView @@ -194,43 +197,101 @@ sealed class KawaiiBarUi(override val ctx: Context, protected val theme: Theme) scrollBarSize = dp(1) } + private val scrollSurfaceView = ctx.view(::SurfaceView) { + setZOrderOnTop(true) + } + + private val scrollableContentViews = mutableListOf() + private val pinnedView = frameLayout() + private val pinnedSurfaceView = ctx.view(::SurfaceView) { + setZOrderOnTop(true) + } + + private var pinnedContentView: InlineContentView? = null + override val root = constraintLayout { add(scrollView, lParams(matchConstraints, matchParent) { startOfParent() before(pinnedView) centerVertically() }) + add(scrollSurfaceView, lParams(matchConstraints, matchParent) { + centerOn(scrollView) + }) add(pinnedView, lParams(wrapContent, matchParent) { endOfParent() centerVertically() }) + add(pinnedSurfaceView, lParams(matchConstraints, matchParent) { + centerOn(pinnedView) + }) } + @RequiresApi(Build.VERSION_CODES.R) + private fun clearScrollView() { + scrollView.scrollTo(0, 0) + scrollableContentViews.forEach { v -> + scrollView.removeView(v) + v.surfaceControl?.let { sc -> + SurfaceControl.Transaction().reparent(sc, null).apply() + } + } + scrollableContentViews.clear() + } + + @RequiresApi(Build.VERSION_CODES.R) + private fun clearPinnedView() { + pinnedContentView?.let { v -> + pinnedView.removeView(v) + v.surfaceControl?.let { sc -> + SurfaceControl.Transaction().reparent(sc, null).apply() + } + pinnedContentView = null + } + } + + @RequiresApi(Build.VERSION_CODES.R) fun clear() { - scrollView.removeAllViews() - pinnedView.removeAllViews() + clearScrollView() + clearPinnedView() } @RequiresApi(Build.VERSION_CODES.R) fun setPinnedView(view: InlineContentView?) { - pinnedView.removeAllViews() - if (view != null) { - pinnedView.addView(view) + clearPinnedView() + if (view == null) return + pinnedView.addView(view) + view.updateLayoutParams { + val hMargin = ctx.dp(10) + leftMargin = hMargin + rightMargin = hMargin } } @RequiresApi(Build.VERSION_CODES.R) fun setScrollableViews(views: List) { + clearScrollView() val flexbox = view(::FlexboxLayout) { flexWrap = FlexWrap.NOWRAP justifyContent = JustifyContent.CENTER - views.forEach { - addView(it) - it.updateLayoutParams { - flexShrink = 0f + } + val parentSurfaceControl = scrollSurfaceView.surfaceControl + views.forEach { + scrollableContentViews.add(it) + it.setSurfaceControlCallback(object : InlineContentView.SurfaceControlCallback { + override fun onCreated(surfaceControl: SurfaceControl) { + SurfaceControl.Transaction() + .reparent(surfaceControl, parentSurfaceControl) + .apply() } + + override fun onDestroyed(surfaceControl: SurfaceControl) {} + }) + flexbox.addView(it) + it.updateLayoutParams { + flexShrink = 0f } } scrollView.apply { From 20206247fc7b4bb99a668d2666e3ab4de22a2198 Mon Sep 17 00:00:00 2001 From: Rocka Date: Sat, 11 Mar 2023 02:36:34 +0800 Subject: [PATCH 055/624] Refactor KawaiiBarUi and Idle state Co-authored-by: Potato Hatsue <1793913507@qq.com> --- .../android/input/bar/KawaiiBarComponent.kt | 155 +++--- .../fcitx5/android/input/bar/KawaiiBarUi.kt | 495 ------------------ .../android/input/bar/ui/CandidateUi.kt | 32 ++ .../fcitx5/android/input/bar/ui/IdleUi.kt | 185 +++++++ .../fcitx5/android/input/bar/ui/TitleUi.kt | 80 +++ .../android/input/bar/{ => ui}/ToolButton.kt | 27 +- .../android/input/bar/ui/idle/ButtonsBarUi.kt | 37 ++ .../bar/ui/idle/ClipboardSuggestionUi.kt | 55 ++ .../input/bar/ui/idle/InlineSuggestionsUi.kt | 114 ++++ .../android/input/bar/ui/idle/NumberRow.kt | 31 ++ .../android/input/clipboard/ClipboardUi.kt | 2 +- .../android/input/editing/TextEditingUi.kt | 2 +- .../android/input/status/StatusAreaWindow.kt | 2 +- 13 files changed, 622 insertions(+), 595 deletions(-) delete mode 100644 app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarUi.kt create mode 100644 app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/CandidateUi.kt create mode 100644 app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/IdleUi.kt create mode 100644 app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/TitleUi.kt rename app/src/main/java/org/fcitx/fcitx5/android/input/bar/{ => ui}/ToolButton.kt (69%) create mode 100644 app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/ButtonsBarUi.kt create mode 100644 app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/ClipboardSuggestionUi.kt create mode 100644 app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/InlineSuggestionsUi.kt create mode 100644 app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/NumberRow.kt diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt index 697b3f693..409ac7e11 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt @@ -1,6 +1,5 @@ package org.fcitx.fcitx5.android.input.bar -import android.annotation.SuppressLint import android.graphics.Color import android.os.Build import android.util.Size @@ -27,6 +26,9 @@ import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.State.* import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.BooleanKey.CandidateEmpty import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.BooleanKey.PreeditEmpty import org.fcitx.fcitx5.android.input.bar.KawaiiBarStateMachine.TransitionEvent.* +import org.fcitx.fcitx5.android.input.bar.ui.CandidateUi +import org.fcitx.fcitx5.android.input.bar.ui.IdleUi +import org.fcitx.fcitx5.android.input.bar.ui.TitleUi import org.fcitx.fcitx5.android.input.broadcast.InputBroadcastReceiver import org.fcitx.fcitx5.android.input.candidates.HorizontalCandidateComponent import org.fcitx.fcitx5.android.input.candidates.expanded.ExpandedCandidateStyle @@ -54,7 +56,6 @@ import splitties.views.dsl.core.add import splitties.views.dsl.core.lParams import splitties.views.dsl.core.matchParent import splitties.views.imageResource -import timber.log.Timber import java.util.concurrent.Executor import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @@ -64,8 +65,8 @@ class KawaiiBarComponent : UniqueViewComponent( private val context by manager.context() private val theme by manager.theme() - private val windowManager: InputWindowManager by manager.must() private val service by manager.inputMethodService() + private val windowManager: InputWindowManager by manager.must() private val horizontalCandidate: HorizontalCandidateComponent by manager.must() private val commonKeyActionListener: CommonKeyActionListener by manager.must() private val popup: PopupComponent by manager.must() @@ -78,21 +79,22 @@ class KawaiiBarComponent : UniqueViewComponent( private var clipboardTimeoutJob: Job? = null - private var isClipboardEmpty: Boolean = true + private var isClipboardFresh: Boolean = true private var isInlineSuggestionEmpty: Boolean = true private var isCapabilityFlagsPassword: Boolean = false private var isKeyboardLayoutNumber: Boolean = false + private var isToolbarManuallyExpanded: Boolean = false private val onClipboardUpdateListener = ClipboardManager.OnClipboardUpdateListener { if (!clipboardSuggestion.getValue()) return@OnClipboardUpdateListener service.lifecycleScope.launch { if (it.text.isEmpty()) { - isClipboardEmpty = true + isClipboardFresh = true evalIdleUiState() } else { - idleUi.setClipboardItemText(it.text.take(42)) - isClipboardEmpty = false + idleUi.clipboardUi.text.text = it.text.take(42) + isClipboardFresh = false evalIdleUiState() launchClipboardTimeoutJob() } @@ -102,7 +104,7 @@ class KawaiiBarComponent : UniqueViewComponent( private val onClipboardSuggestionUpdateListener = ManagedPreference.OnChangeListener { _, it -> if (!it) { - isClipboardEmpty = true + isClipboardFresh = true evalIdleUiState() clipboardTimeoutJob?.cancel() clipboardTimeoutJob = null @@ -112,7 +114,7 @@ class KawaiiBarComponent : UniqueViewComponent( private val onClipboardTimeoutUpdateListener = ManagedPreference.OnChangeListener { _, _ -> when (idleUi.currentState) { - KawaiiBarUi.Idle.State.Clipboard -> { + IdleUi.State.Clipboard -> { // renew timeout when clipboard suggestion is present launchClipboardTimeoutJob() } @@ -120,10 +122,6 @@ class KawaiiBarComponent : UniqueViewComponent( } } - private val popupActionListener by lazy { - popup.listener - } - private fun launchClipboardTimeoutJob() { clipboardTimeoutJob?.cancel() val timeout = clipboardItemTimeout.getValue() * 1000L @@ -131,92 +129,87 @@ class KawaiiBarComponent : UniqueViewComponent( if (timeout < 0L) return clipboardTimeoutJob = service.lifecycleScope.launch { delay(timeout) - isClipboardEmpty = true - evalIdleUiState() + isClipboardFresh = true clipboardTimeoutJob = null } } - private fun evalIdleUiState() { + private fun evalIdleUiState(fromUser: Boolean = false) { val newState = when { - !isClipboardEmpty -> KawaiiBarUi.Idle.State.Clipboard - !isInlineSuggestionEmpty -> - KawaiiBarUi.Idle.State.InlineSuggestion - isCapabilityFlagsPassword && !isKeyboardLayoutNumber -> - KawaiiBarUi.Idle.State.NumberRow - else -> KawaiiBarUi.Idle.State.Empty + !isClipboardFresh -> IdleUi.State.Clipboard + !isInlineSuggestionEmpty -> IdleUi.State.InlineSuggestion + isCapabilityFlagsPassword && !isKeyboardLayoutNumber -> IdleUi.State.NumberRow + expandToolbarByDefault || isToolbarManuallyExpanded -> IdleUi.State.Toolbar + else -> IdleUi.State.Empty } - if (newState == idleUi.currentState) - return - else if (idleUi.currentState != KawaiiBarUi.Idle.State.Empty - && newState == KawaiiBarUi.Idle.State.Empty - ) { - if (expandToolbarByDefault || idleUi.isToolbarExpanded) { - idleUi.updateState(newState) - idleUi.expandToolbar() - } else - idleUi.updateState(newState) - } else idleUi.updateState(newState) + if (newState == idleUi.currentState) return + idleUi.updateState(newState, fromUser) } - private val idleUi: KawaiiBarUi.Idle by lazy { - KawaiiBarUi.Idle( - context, - theme, - popupActionListener, - popup, - commonKeyActionListener - ).apply { + private val idleUi: IdleUi by lazy { + IdleUi(context, theme, popup, commonKeyActionListener).apply { menuButton.setOnClickListener { - idleUi.toggleToolbar() + if (idleUi.currentState == IdleUi.State.Toolbar) { + isToolbarManuallyExpanded = false + evalIdleUiState(fromUser = true) + } else { + isToolbarManuallyExpanded = true + idleUi.updateState(IdleUi.State.Toolbar, fromUser = true) + } // reset timeout timer (if present) when user switch layout if (clipboardTimeoutJob != null) { launchClipboardTimeoutJob() } } - undoButton.setOnClickListener { - service.sendCombinationKeyEvents(KeyEvent.KEYCODE_Z, ctrl = true) - } - redoButton.setOnClickListener { - service.sendCombinationKeyEvents(KeyEvent.KEYCODE_Z, ctrl = true, shift = true) - } - cursorMoveButton.setOnClickListener { - windowManager.attachWindow(TextEditingWindow()) - } - clipboardButton.setOnClickListener { - windowManager.attachWindow(ClipboardWindow()) - } - moreButton.setOnClickListener { - windowManager.attachWindow(StatusAreaWindow()) + hideKeyboardButton.setOnClickListener { + service.requestHideSelf(0) } - clipboardSuggestionItem.setOnClickListener { - ClipboardManager.lastEntry?.let { - service.commitText(it.text) + buttonsUi.apply { + undoButton.setOnClickListener { + service.sendCombinationKeyEvents(KeyEvent.KEYCODE_Z, ctrl = true) } - clipboardTimeoutJob?.cancel() - clipboardTimeoutJob = null - isClipboardEmpty = true - evalIdleUiState() - } - clipboardSuggestionItem.setOnLongClickListener { - ClipboardManager.lastEntry?.let { - AppUtil.launchClipboardEdit(context, it.id, true) + redoButton.setOnClickListener { + service.sendCombinationKeyEvents(KeyEvent.KEYCODE_Z, ctrl = true, shift = true) + } + cursorMoveButton.setOnClickListener { + windowManager.attachWindow(TextEditingWindow()) + } + clipboardButton.setOnClickListener { + windowManager.attachWindow(ClipboardWindow()) + } + moreButton.setOnClickListener { + windowManager.attachWindow(StatusAreaWindow()) } - true } - hideKeyboardButton.setOnClickListener { - service.requestHideSelf(0) + clipboardUi.suggestionView.apply { + setOnClickListener { + ClipboardManager.lastEntry?.let { + service.commitText(it.text) + } + clipboardTimeoutJob?.cancel() + clipboardTimeoutJob = null + isClipboardFresh = true + evalIdleUiState() + } + setOnLongClickListener { + ClipboardManager.lastEntry?.let { + AppUtil.launchClipboardEdit(context, it.id, true) + } + true + } } } } private val candidateUi by lazy { - KawaiiBarUi.Candidate(context, theme, horizontalCandidate.view) + CandidateUi(context, theme, horizontalCandidate.view) } - private val titleUi by lazy { KawaiiBarUi.Title(context, theme) } + private val titleUi by lazy { + TitleUi(context, theme) + } - val barStateMachine = KawaiiBarStateMachine.new { + private val barStateMachine = KawaiiBarStateMachine.new { switchUiByState(it) } @@ -226,19 +219,16 @@ class KawaiiBarComponent : UniqueViewComponent( setExpandButtonToAttach() setExpandButtonEnabled(true) } - ClickToDetachWindow -> { setExpandButtonToDetach() setExpandButtonEnabled(true) } - Hidden -> { setExpandButtonEnabled(false) } } } - // set expand candidate button to create expand candidate private fun setExpandButtonToAttach() { candidateUi.expandButton.setOnClickListener { @@ -262,17 +252,12 @@ class KawaiiBarComponent : UniqueViewComponent( // should be used with setExpandButtonToAttach or setExpandButtonToDetach private fun setExpandButtonEnabled(enabled: Boolean) { - if (enabled) - candidateUi.expandButton.visibility = View.VISIBLE - else - candidateUi.expandButton.visibility = View.INVISIBLE + candidateUi.expandButton.visibility = if (enabled) View.VISIBLE else View.INVISIBLE } private fun switchUiByState(state: KawaiiBarStateMachine.State) { val index = state.ordinal - if (view.displayedChild == index) - return - Timber.d("Switch bar to $state") + if (view.displayedChild == index) return val new = view.getChildAt(index) if (new != titleUi.root) { titleUi.setReturnButtonOnClickListener { } @@ -316,8 +301,6 @@ class KawaiiBarComponent : UniqueViewComponent( idleUi.inlineSuggestionsBar.clear() } evalIdleUiState() - if (idleUi.currentState == KawaiiBarUi.Idle.State.Empty && expandToolbarByDefault) - idleUi.expandToolbar() } override fun onPreeditEmptyStateUpdate(empty: Boolean) { @@ -338,7 +321,6 @@ class KawaiiBarComponent : UniqueViewComponent( } barStateMachine.push(ExtendedWindowAttached) } - is InputWindow.SimpleInputWindow<*> -> { } } @@ -356,7 +338,6 @@ class KawaiiBarComponent : UniqueViewComponent( Executor { it.run() } } - @SuppressLint("NotifyDataSetChanged") @RequiresApi(Build.VERSION_CODES.R) fun handleInlineSuggestions(response: InlineSuggestionsResponse): Boolean { if (response.inlineSuggestions.isEmpty()) { diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarUi.kt deleted file mode 100644 index bf6580b0b..000000000 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarUi.kt +++ /dev/null @@ -1,495 +0,0 @@ -package org.fcitx.fcitx5.android.input.bar - -import android.content.Context -import android.graphics.PorterDuff -import android.graphics.PorterDuffColorFilter -import android.graphics.Typeface -import android.os.Build -import android.text.TextUtils -import android.view.SurfaceControl -import android.view.SurfaceView -import android.view.View -import android.view.animation.AlphaAnimation -import android.view.animation.AnimationSet -import android.view.animation.TranslateAnimation -import android.widget.FrameLayout -import android.widget.HorizontalScrollView -import android.widget.ViewAnimator -import android.widget.inline.InlineContentView -import androidx.annotation.DrawableRes -import androidx.annotation.RequiresApi -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.view.isVisible -import androidx.core.view.updateLayoutParams -import com.google.android.flexbox.FlexWrap -import com.google.android.flexbox.FlexboxLayout -import com.google.android.flexbox.JustifyContent -import kotlinx.coroutines.* -import org.fcitx.fcitx5.android.R -import org.fcitx.fcitx5.android.core.KeySym -import org.fcitx.fcitx5.android.data.prefs.AppPrefs -import org.fcitx.fcitx5.android.data.theme.Theme -import org.fcitx.fcitx5.android.input.keyboard.* -import org.fcitx.fcitx5.android.input.popup.PopupActionListener -import org.fcitx.fcitx5.android.input.popup.PopupComponent -import org.fcitx.fcitx5.android.utils.rippleDrawable -import splitties.dimensions.dp -import splitties.views.dsl.constraintlayout.* -import splitties.views.dsl.core.* -import splitties.views.gravityCenter -import splitties.views.gravityVerticalCenter -import splitties.views.imageResource -import splitties.views.padding -import timber.log.Timber - -sealed class KawaiiBarUi(override val ctx: Context, protected val theme: Theme) : Ui { - - companion object { - val disableAnimation by AppPrefs.getInstance().advanced.disableAnimation - } - - protected fun toolButton(@DrawableRes icon: Int, initView: ToolButton.() -> Unit = {}) = - ToolButton(ctx, icon, theme).apply(initView) - - class Candidate(ctx: Context, theme: Theme, private val horizontalView: View) : - KawaiiBarUi(ctx, theme) { - - val expandButton = toolButton(R.drawable.ic_baseline_expand_more_24) { - id = R.id.expand_candidate_btn - visibility = View.INVISIBLE - } - - override val root = ctx.constraintLayout { - add(expandButton, lParams(dp(40)) { - centerVertically() - endOfParent() - }) - add(horizontalView, lParams { - centerVertically() - startOfParent() - before(expandButton) - }) - } - } - - class Idle( - ctx: Context, - theme: Theme, - private val popupActionListener: PopupActionListener, - private val popup: PopupComponent, - private val commonKeyActionListener: CommonKeyActionListener - ) : KawaiiBarUi(ctx, theme) { - - enum class State { - Clipboard, Empty, NumberRow, InlineSuggestion - } - - var currentState = State.Empty - private set - private val menuButtonRotation - get() = - when { - inPrivate -> 0f - isToolbarExpanded -> 90f - else -> -90f - } - - private var inPrivate = false - - var isToolbarExpanded = false - private set - - val menuButton = toolButton(R.drawable.ic_baseline_expand_more_24) { - rotation = menuButtonRotation - } - - val undoButton = toolButton(R.drawable.ic_baseline_undo_24) - - val redoButton = toolButton(R.drawable.ic_baseline_redo_24) - - val cursorMoveButton = toolButton(R.drawable.ic_cursor_move) - - val clipboardButton = toolButton(R.drawable.ic_clipboard) - - val moreButton = toolButton(R.drawable.ic_baseline_more_horiz_24) - - val hideKeyboardButton = toolButton(R.drawable.ic_baseline_arrow_drop_down_24) - - private fun ConstraintLayout.addButton( - v: View, - initParams: ConstraintLayout.LayoutParams.() -> Unit = {} - ) { - add(v, ConstraintLayout.LayoutParams(dp(40), dp(40)).apply { - centerVertically() - initParams(this) - }) - } - - private val buttonsBar = constraintLayout { - addButton(undoButton) { startOfParent(); before(redoButton) } - addButton(redoButton) { after(undoButton); before(cursorMoveButton) } - addButton(cursorMoveButton) { after(redoButton); before(clipboardButton) } - addButton(clipboardButton) { after(cursorMoveButton); before(moreButton) } - addButton(moreButton) { after(clipboardButton); endOfParent() } - } - - private val clipboardIcon = imageView { - imageResource = R.drawable.ic_clipboard - colorFilter = PorterDuffColorFilter(theme.altKeyTextColor, PorterDuff.Mode.SRC_IN) - } - - private val clipboardText = textView { - isSingleLine = true - maxWidth = dp(120) - ellipsize = TextUtils.TruncateAt.END - setTextColor(theme.altKeyTextColor) - } - - private val clipboardSuggestionLayout = horizontalLayout { - gravity = gravityCenter - padding = dp(4) - add(clipboardIcon, lParams(dp(20), dp(20))) - add(clipboardText, lParams { - leftMargin = dp(4) - }) - } - - val clipboardSuggestionItem = object : CustomGestureView(ctx) { - init { - isHapticFeedbackEnabled = false - background = rippleDrawable(theme.keyPressHighlightColor) - add(clipboardSuggestionLayout, lParams(wrapContent, matchParent)) - } - } - - private val clipboardBar = constraintLayout { - add(clipboardSuggestionItem, lParams(wrapContent, matchConstraints) { - centerInParent() - verticalMargin = dp(4) - }) - } - - private val emptyBar = constraintLayout() - - private val numberRowBar = object : BaseKeyboard( - ctx, - theme, - listOf(listOf("1", "2", "3", "4", "5", "6", "7", "8", "9", "0").map { digit -> - KeyDef( - KeyDef.Appearance.Text( - displayText = digit, - textSize = 21f, - border = KeyDef.Appearance.Border.Off, - margin = false - ), - setOf( - KeyDef.Behavior.Press(KeyAction.SymAction(KeySym(digit.codePointAt(0)))) - ), - arrayOf(KeyDef.Popup.Preview(digit)) - ) - }) - ) {} - - class InlineSuggestionsUi(override val ctx: Context) : Ui { - - private val scrollView = ctx.view(::HorizontalScrollView) { - isFillViewport = true - scrollBarSize = dp(1) - } - - private val scrollSurfaceView = ctx.view(::SurfaceView) { - setZOrderOnTop(true) - } - - private val scrollableContentViews = mutableListOf() - - private val pinnedView = frameLayout() - - private val pinnedSurfaceView = ctx.view(::SurfaceView) { - setZOrderOnTop(true) - } - - private var pinnedContentView: InlineContentView? = null - - override val root = constraintLayout { - add(scrollView, lParams(matchConstraints, matchParent) { - startOfParent() - before(pinnedView) - centerVertically() - }) - add(scrollSurfaceView, lParams(matchConstraints, matchParent) { - centerOn(scrollView) - }) - add(pinnedView, lParams(wrapContent, matchParent) { - endOfParent() - centerVertically() - }) - add(pinnedSurfaceView, lParams(matchConstraints, matchParent) { - centerOn(pinnedView) - }) - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun clearScrollView() { - scrollView.scrollTo(0, 0) - scrollableContentViews.forEach { v -> - scrollView.removeView(v) - v.surfaceControl?.let { sc -> - SurfaceControl.Transaction().reparent(sc, null).apply() - } - } - scrollableContentViews.clear() - } - - @RequiresApi(Build.VERSION_CODES.R) - private fun clearPinnedView() { - pinnedContentView?.let { v -> - pinnedView.removeView(v) - v.surfaceControl?.let { sc -> - SurfaceControl.Transaction().reparent(sc, null).apply() - } - pinnedContentView = null - } - } - - @RequiresApi(Build.VERSION_CODES.R) - fun clear() { - clearScrollView() - clearPinnedView() - } - - @RequiresApi(Build.VERSION_CODES.R) - fun setPinnedView(view: InlineContentView?) { - clearPinnedView() - if (view == null) return - pinnedView.addView(view) - view.updateLayoutParams { - val hMargin = ctx.dp(10) - leftMargin = hMargin - rightMargin = hMargin - } - } - - @RequiresApi(Build.VERSION_CODES.R) - fun setScrollableViews(views: List) { - clearScrollView() - val flexbox = view(::FlexboxLayout) { - flexWrap = FlexWrap.NOWRAP - justifyContent = JustifyContent.CENTER - } - val parentSurfaceControl = scrollSurfaceView.surfaceControl - views.forEach { - scrollableContentViews.add(it) - it.setSurfaceControlCallback(object : InlineContentView.SurfaceControlCallback { - override fun onCreated(surfaceControl: SurfaceControl) { - SurfaceControl.Transaction() - .reparent(surfaceControl, parentSurfaceControl) - .apply() - } - - override fun onDestroyed(surfaceControl: SurfaceControl) {} - }) - flexbox.addView(it) - it.updateLayoutParams { - flexShrink = 0f - } - } - scrollView.apply { - scrollTo(0, 0) - removeAllViews() - add(flexbox, lParams(wrapContent, matchParent)) - } - } - } - - val inlineSuggestionsBar = InlineSuggestionsUi(ctx) - - private val animator = ViewAnimator(ctx).apply { - add(emptyBar, lParams(matchParent, matchParent)) - add(clipboardBar, lParams(matchParent, matchParent)) - add(buttonsBar, lParams(matchParent, matchParent)) - add(inlineSuggestionsBar.root, lParams(matchParent, matchParent)) - - if (disableAnimation) { - inAnimation = null - outAnimation = null - } else { - inAnimation = AnimationSet(true).apply { - duration = 200L - addAnimation(AlphaAnimation(0f, 1f)) - // TODO: rework InlineContentView animation -// addAnimation(ScaleAnimation(0f, 1f, 0f, 1f, 0f, dp(20f))) - addAnimation(TranslateAnimation(dp(-100f), 0f, 0f, 0f)) - } - outAnimation = AnimationSet(true).apply { - duration = 200L - addAnimation(AlphaAnimation(1f, 0f)) -// addAnimation(ScaleAnimation(1f, 0f, 1f, 0f, 0f, dp(20f))) - addAnimation(TranslateAnimation(0f, dp(-100f), 0f, 0f)) - } - } - } - - override val root = constraintLayout { - addButton(menuButton) { startOfParent() } - add(animator, lParams(matchConstraints, matchParent) { - after(menuButton) - before(hideKeyboardButton) - }) - addButton(hideKeyboardButton) { endOfParent() } - numberRowBar.visibility = View.GONE - add(numberRowBar, lParams(matchParent, matchParent)) - } - - fun privateMode(activate: Boolean = true) { - if (activate == inPrivate) return - inPrivate = activate - updateMenuButtonIcon() - updateMenuButtonRotation(instant = true) - } - - private fun updateMenuButtonIcon() { - menuButton.image.imageResource = - if (inPrivate) R.drawable.ic_view_private - else R.drawable.ic_baseline_expand_more_24 - } - - private fun updateMenuButtonRotation(instant: Boolean = false) { - val targetRotation = menuButtonRotation - menuButton.apply { - if (targetRotation == rotation) return - if (instant || disableAnimation) { - rotation = targetRotation - } else { - animate().setDuration(200L).rotation(targetRotation) - } - } - } - - fun expandToolbar() { - Timber.d("Expand idle ui toolbar at $currentState") - if (animator.displayedChild != 2) - animator.displayedChild = 2 - isToolbarExpanded = true - updateMenuButtonRotation() - } - - fun collapseToolbar() { - Timber.d("Collapse idle ui toolbar at $currentState") - switchUiByState(currentState) - isToolbarExpanded = false - updateMenuButtonRotation() - } - - fun toggleToolbar() { - if (isToolbarExpanded) - collapseToolbar() - else - expandToolbar() - } - - fun updateState(state: State) { - currentState = state - switchUiByState(state) - } - - private fun switchUiByState(state: State) { - Timber.d("Switch idle ui to $state") - when (state) { - State.Clipboard -> animator.displayedChild = 1 - State.Empty -> animator.displayedChild = 0 - State.NumberRow -> {} - State.InlineSuggestion -> animator.displayedChild = 3 - } - if (state != State.Empty) { - isToolbarExpanded = false - } - if (state == State.NumberRow) { - menuButton.visibility = View.GONE - hideKeyboardButton.visibility = View.GONE - animator.visibility = View.GONE - numberRowBar.visibility = View.VISIBLE - numberRowBar.keyActionListener = commonKeyActionListener.listener - numberRowBar.popupActionListener = popupActionListener - } else { - menuButton.visibility = View.VISIBLE - hideKeyboardButton.visibility = View.VISIBLE - animator.visibility = View.VISIBLE - numberRowBar.visibility = View.GONE - numberRowBar.keyActionListener = null - numberRowBar.popupActionListener = null - popup.dismissAll() - } - updateMenuButtonRotation() - } - - - fun setClipboardItemText(text: String) { - clipboardText.text = text - } - } - - class Title(ctx: Context, theme: Theme) : KawaiiBarUi(ctx, theme) { - - private val backButton = toolButton(R.drawable.ic_baseline_arrow_back_24) - - private val titleText = textView { - typeface = Typeface.defaultFromStyle(Typeface.BOLD) - setTextColor(theme.altKeyTextColor) - gravity = gravityVerticalCenter - textSize = 16f - } - - private var extension: View? = null - - override val root = constraintLayout { - add(backButton, lParams(dp(40), dp(40)) { - topOfParent() - startOfParent() - bottomOfParent() - }) - add(titleText, lParams(wrapContent, dp(40)) { - topOfParent() - after(backButton, dp(8)) - bottomOfParent() - }) - } - - fun setReturnButtonOnClickListener(block: () -> Unit) { - backButton.setOnClickListener { - block() - } - } - - fun setTitle(title: String) { - titleText.text = title - } - - fun addExtension(view: View, showTitle: Boolean) { - if (extension != null) { - throw IllegalStateException("TitleBar extension is already present") - } - backButton.isVisible = showTitle - titleText.isVisible = showTitle - extension = view - root.run { - add(view, lParams(matchConstraints, dp(40)) { - centerVertically() - if (showTitle) { - endOfParent(dp(5)) - } else { - centerHorizontally() - } - }) - } - } - - fun removeExtension() { - if (extension == null) - return - root.removeView(extension) - extension = null - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/CandidateUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/CandidateUi.kt new file mode 100644 index 000000000..82b1614a1 --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/CandidateUi.kt @@ -0,0 +1,32 @@ +package org.fcitx.fcitx5.android.input.bar.ui + +import android.content.Context +import android.view.View +import org.fcitx.fcitx5.android.R +import org.fcitx.fcitx5.android.data.theme.Theme +import splitties.dimensions.dp +import splitties.views.dsl.constraintlayout.* +import splitties.views.dsl.core.Ui +import splitties.views.dsl.core.add +import splitties.views.dsl.core.view + +class CandidateUi(override val ctx: Context, theme: Theme, private val horizontalView: View) : Ui { + + val expandButton = view(::ToolButton, R.id.expand_candidate_btn) { + setIcon(R.drawable.ic_baseline_expand_more_24, theme.altKeyTextColor) + setPressHighlightColor(theme.keyPressHighlightColor) + visibility = View.INVISIBLE + } + + override val root = ctx.constraintLayout { + add(expandButton, lParams(dp(40)) { + centerVertically() + endOfParent() + }) + add(horizontalView, lParams { + centerVertically() + startOfParent() + before(expandButton) + }) + } +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/IdleUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/IdleUi.kt new file mode 100644 index 000000000..d4188b1b0 --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/IdleUi.kt @@ -0,0 +1,185 @@ +package org.fcitx.fcitx5.android.input.bar.ui + +import android.content.Context +import android.view.View +import android.view.animation.AlphaAnimation +import android.view.animation.AnimationSet +import android.view.animation.TranslateAnimation +import android.widget.Space +import android.widget.ViewAnimator +import org.fcitx.fcitx5.android.R +import org.fcitx.fcitx5.android.data.prefs.AppPrefs +import org.fcitx.fcitx5.android.data.theme.Theme +import org.fcitx.fcitx5.android.input.bar.KawaiiBarComponent +import org.fcitx.fcitx5.android.input.bar.ui.idle.ButtonsBarUi +import org.fcitx.fcitx5.android.input.bar.ui.idle.ClipboardSuggestionUi +import org.fcitx.fcitx5.android.input.bar.ui.idle.InlineSuggestionsUi +import org.fcitx.fcitx5.android.input.bar.ui.idle.NumberRow +import org.fcitx.fcitx5.android.input.keyboard.CommonKeyActionListener +import org.fcitx.fcitx5.android.input.popup.PopupComponent +import splitties.dimensions.dp +import splitties.views.dsl.constraintlayout.* +import splitties.views.dsl.core.Ui +import splitties.views.dsl.core.add +import splitties.views.dsl.core.lParams +import splitties.views.dsl.core.matchParent +import splitties.views.imageResource +import timber.log.Timber + +class IdleUi( + override val ctx: Context, + private val theme: Theme, + private val popup: PopupComponent, + private val commonKeyActionListener: CommonKeyActionListener +) : Ui { + + enum class State { + Empty, Toolbar, Clipboard, NumberRow, InlineSuggestion + } + + var currentState = State.Empty + private set + + private val disableAnimation by AppPrefs.getInstance().advanced.disableAnimation + + private var inPrivate = false + + private val menuButtonRotation + get() = when { + inPrivate -> 0f + currentState == State.Toolbar -> 90f + else -> -90f + } + + val menuButton = ToolButton(ctx, R.drawable.ic_baseline_expand_more_24, theme).apply { + rotation = menuButtonRotation + } + + val hideKeyboardButton = ToolButton(ctx, R.drawable.ic_baseline_arrow_drop_down_24, theme) + + val emptyBar = Space(ctx) + + val buttonsUi = ButtonsBarUi(ctx, theme) + + val clipboardUi = ClipboardSuggestionUi(ctx, theme) + + val numberRow = NumberRow(ctx, theme).apply { + visibility = View.GONE + } + + val inlineSuggestionsBar = InlineSuggestionsUi(ctx) + + private val animator = ViewAnimator(ctx).apply { + add(emptyBar, lParams(matchParent, matchParent)) + add(buttonsUi.root, lParams(matchParent, matchParent)) + add(clipboardUi.root, lParams(matchParent, matchParent)) + add(inlineSuggestionsBar.root, lParams(matchParent, matchParent)) + } + + private val inAnimation by lazy { + AnimationSet(true).apply { + duration = 200L + addAnimation(AlphaAnimation(0f, 1f)) + addAnimation(TranslateAnimation(ctx.dp(-100f), 0f, 0f, 0f)) + } + } + + private val outAnimation by lazy { + AnimationSet(true).apply { + duration = 200L + addAnimation(AlphaAnimation(1f, 0f)) + addAnimation(TranslateAnimation(0f, ctx.dp(-100f), 0f, 0f)) + } + } + + override val root = constraintLayout { + val size = dp(KawaiiBarComponent.HEIGHT) + add(menuButton, lParams(size, size) { + startOfParent() + centerVertically() + }) + add(hideKeyboardButton, lParams(size, size) { + endOfParent() + centerVertically() + }) + add(animator, lParams(matchConstraints, matchParent) { + after(menuButton) + before(hideKeyboardButton) + centerVertically() + }) + add(numberRow, lParams(matchParent, matchParent)) + } + + fun privateMode(activate: Boolean = true) { + if (activate == inPrivate) return + inPrivate = activate + updateMenuButtonIcon() + updateMenuButtonRotation(instant = true) + } + + private fun updateMenuButtonIcon() { + menuButton.image.imageResource = + if (inPrivate) R.drawable.ic_view_private + else R.drawable.ic_baseline_expand_more_24 + } + + private fun updateMenuButtonRotation(instant: Boolean = false) { + val targetRotation = menuButtonRotation + menuButton.apply { + if (targetRotation == rotation) return + animate().cancel() + if (!instant && !disableAnimation) { + animate().setDuration(200L).rotation(targetRotation) + } else { + rotation = targetRotation + } + } + } + + private fun clearAnimation() { + animator.inAnimation = null + animator.outAnimation = null + } + + private fun setAnimation() { + animator.inAnimation = inAnimation + animator.outAnimation = outAnimation + } + + fun updateState(state: State, fromUser: Boolean = false) { + Timber.d("Switch idle ui to $state") + if ( + disableAnimation || + (state == State.InlineSuggestion || currentState == State.InlineSuggestion) && !fromUser + ) { + clearAnimation() + } else { + setAnimation() + } + when (state) { + State.Empty -> animator.displayedChild = 0 + State.Toolbar -> animator.displayedChild = 1 + State.Clipboard -> animator.displayedChild = 2 + State.NumberRow -> {} + State.InlineSuggestion -> animator.displayedChild = 3 + } + if (state == State.NumberRow) { + menuButton.visibility = View.GONE + hideKeyboardButton.visibility = View.GONE + animator.visibility = View.GONE + numberRow.visibility = View.VISIBLE + numberRow.keyActionListener = commonKeyActionListener.listener + numberRow.popupActionListener = popup.listener + } else { + menuButton.visibility = View.VISIBLE + hideKeyboardButton.visibility = View.VISIBLE + animator.visibility = View.VISIBLE + numberRow.visibility = View.GONE + numberRow.keyActionListener = null + numberRow.popupActionListener = null + popup.dismissAll() + } + currentState = state + updateMenuButtonRotation(instant = !fromUser) + } +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/TitleUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/TitleUi.kt new file mode 100644 index 000000000..340ce68af --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/TitleUi.kt @@ -0,0 +1,80 @@ +package org.fcitx.fcitx5.android.input.bar.ui + +import android.content.Context +import android.graphics.Typeface +import android.view.View +import androidx.core.view.isVisible +import org.fcitx.fcitx5.android.R +import org.fcitx.fcitx5.android.data.theme.Theme +import splitties.dimensions.dp +import splitties.views.dsl.constraintlayout.* +import splitties.views.dsl.core.Ui +import splitties.views.dsl.core.add +import splitties.views.dsl.core.textView +import splitties.views.dsl.core.wrapContent +import splitties.views.gravityVerticalCenter + +class TitleUi(override val ctx: Context, theme: Theme) : Ui { + + private val backButton = ToolButton(ctx, R.drawable.ic_baseline_arrow_back_24, theme).apply { + id = R.id.expand_candidate_btn + } + + private val titleText = textView { + typeface = Typeface.defaultFromStyle(Typeface.BOLD) + setTextColor(theme.altKeyTextColor) + gravity = gravityVerticalCenter + textSize = 16f + } + + private var extension: View? = null + + override val root = constraintLayout { + add(backButton, lParams(dp(40), dp(40)) { + topOfParent() + startOfParent() + bottomOfParent() + }) + add(titleText, lParams(wrapContent, dp(40)) { + topOfParent() + after(backButton, dp(8)) + bottomOfParent() + }) + } + + fun setReturnButtonOnClickListener(block: () -> Unit) { + backButton.setOnClickListener { + block() + } + } + + fun setTitle(title: String) { + titleText.text = title + } + + fun addExtension(view: View, showTitle: Boolean) { + if (extension != null) { + throw IllegalStateException("TitleBar extension is already present") + } + backButton.isVisible = showTitle + titleText.isVisible = showTitle + extension = view + root.run { + add(view, lParams(matchConstraints, dp(40)) { + centerVertically() + if (showTitle) { + endOfParent(dp(5)) + } else { + centerHorizontally() + } + }) + } + } + + fun removeExtension() { + extension?.let { + root.removeView(it) + extension = null + } + } +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ToolButton.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/ToolButton.kt similarity index 69% rename from app/src/main/java/org/fcitx/fcitx5/android/input/bar/ToolButton.kt rename to app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/ToolButton.kt index 08981de1a..5007f3344 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ToolButton.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/ToolButton.kt @@ -1,10 +1,10 @@ -package org.fcitx.fcitx5.android.input.bar +package org.fcitx.fcitx5.android.input.bar.ui -import android.annotation.SuppressLint import android.content.Context import android.graphics.PorterDuff import android.graphics.PorterDuffColorFilter import android.widget.ImageView +import androidx.annotation.ColorInt import androidx.annotation.DrawableRes import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.data.theme.Theme @@ -21,31 +21,38 @@ import splitties.views.gravityCenter import splitties.views.imageDrawable import splitties.views.padding -@SuppressLint("ViewConstructor") -class ToolButton(context: Context, @DrawableRes icon: Int, val theme: Theme) : - CustomGestureView(context) { +class ToolButton(context: Context) : CustomGestureView(context) { companion object { val disableAnimation by AppPrefs.getInstance().advanced.disableAnimation } + constructor(context: Context, @DrawableRes icon: Int, theme: Theme) : this(context) { + setIcon(icon, theme.altKeyTextColor) + setPressHighlightColor(theme.keyPressHighlightColor) + } + val image = imageView { isClickable = false isFocusable = false - imageDrawable = context.drawable(icon) padding = dp(10) scaleType = ImageView.ScaleType.CENTER_INSIDE - colorFilter = PorterDuffColorFilter(theme.altKeyTextColor, PorterDuff.Mode.SRC_IN) } init { - val color = theme.keyPressHighlightColor + add(image, lParams(wrapContent, wrapContent, gravityCenter)) + } + + fun setIcon(@DrawableRes icon: Int, @ColorInt color: Int) { + image.imageDrawable = drawable(icon) + image.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) + } + + fun setPressHighlightColor(@ColorInt color: Int) { background = if (disableAnimation) { circlePressHighlightDrawable(color) } else { borderlessRippleDrawable(color, dp(20)) } - - add(image, lParams(wrapContent, wrapContent, gravityCenter)) } } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/ButtonsBarUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/ButtonsBarUi.kt new file mode 100644 index 000000000..99d9bf88b --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/ButtonsBarUi.kt @@ -0,0 +1,37 @@ +package org.fcitx.fcitx5.android.input.bar.ui.idle + +import android.content.Context +import androidx.annotation.DrawableRes +import com.google.android.flexbox.AlignItems +import com.google.android.flexbox.FlexboxLayout +import com.google.android.flexbox.JustifyContent +import org.fcitx.fcitx5.android.R +import org.fcitx.fcitx5.android.data.theme.Theme +import org.fcitx.fcitx5.android.input.bar.ui.ToolButton +import splitties.dimensions.dp +import splitties.views.dsl.core.Ui +import splitties.views.dsl.core.view + +class ButtonsBarUi(override val ctx: Context, private val theme: Theme) : Ui { + + override val root = view(::FlexboxLayout) { + alignItems = AlignItems.CENTER + justifyContent = JustifyContent.SPACE_AROUND + } + + private fun toolButton(@DrawableRes icon: Int) = ToolButton(ctx, icon, theme).also { + val size = ctx.dp(40) + root.addView(it, FlexboxLayout.LayoutParams(size, size)) + } + + val undoButton = toolButton(R.drawable.ic_baseline_undo_24) + + val redoButton = toolButton(R.drawable.ic_baseline_redo_24) + + val cursorMoveButton = toolButton(R.drawable.ic_cursor_move) + + val clipboardButton = toolButton(R.drawable.ic_clipboard) + + val moreButton = toolButton(R.drawable.ic_baseline_more_horiz_24) + +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/ClipboardSuggestionUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/ClipboardSuggestionUi.kt new file mode 100644 index 000000000..8d98d6da2 --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/ClipboardSuggestionUi.kt @@ -0,0 +1,55 @@ +package org.fcitx.fcitx5.android.input.bar.ui.idle + +import android.content.Context +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.text.TextUtils +import org.fcitx.fcitx5.android.R +import org.fcitx.fcitx5.android.data.theme.Theme +import org.fcitx.fcitx5.android.input.keyboard.CustomGestureView +import org.fcitx.fcitx5.android.utils.rippleDrawable +import splitties.dimensions.dp +import splitties.views.dsl.constraintlayout.* +import splitties.views.dsl.core.* +import splitties.views.imageResource + +class ClipboardSuggestionUi(override val ctx: Context, private val theme: Theme) : Ui { + + private val icon = imageView { + imageResource = R.drawable.ic_clipboard + colorFilter = PorterDuffColorFilter(theme.altKeyTextColor, PorterDuff.Mode.SRC_IN) + } + + val text = textView { + isSingleLine = true + maxWidth = dp(120) + ellipsize = TextUtils.TruncateAt.END + setTextColor(theme.altKeyTextColor) + } + + private val layout = constraintLayout { + val spacing = dp(4) + add(icon, lParams(dp(20), dp(20)) { + startOfParent(spacing) + before(text) + centerVertically() + }) + add(text, lParams(wrapContent, wrapContent) { + after(icon, spacing) + endOfParent(spacing) + centerVertically() + }) + } + + val suggestionView = CustomGestureView(ctx).apply { + add(layout, lParams(wrapContent, matchParent)) + background = rippleDrawable(theme.keyPressHighlightColor) + } + + override val root = constraintLayout { + add(suggestionView, lParams(wrapContent, matchConstraints) { + centerInParent() + verticalMargin = dp(4) + }) + } +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/InlineSuggestionsUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/InlineSuggestionsUi.kt new file mode 100644 index 000000000..6a1abd863 --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/InlineSuggestionsUi.kt @@ -0,0 +1,114 @@ +package org.fcitx.fcitx5.android.input.bar.ui.idle + +import android.content.Context +import android.os.Build +import android.view.SurfaceControl +import android.view.SurfaceView +import android.widget.HorizontalScrollView +import android.widget.inline.InlineContentView +import androidx.annotation.RequiresApi +import androidx.core.view.updateLayoutParams +import com.google.android.flexbox.FlexWrap +import com.google.android.flexbox.FlexboxLayout +import com.google.android.flexbox.JustifyContent +import splitties.dimensions.dp +import splitties.views.dsl.constraintlayout.* +import splitties.views.dsl.core.* +import splitties.views.horizontalPadding + +class InlineSuggestionsUi(override val ctx: Context) : Ui { + + private val scrollView = ctx.view(::HorizontalScrollView) { + isFillViewport = true + scrollBarSize = dp(1) + } + + private val scrollSurfaceView = ctx.view(::SurfaceView) { + setZOrderOnTop(true) + } + + private val scrollableContentViews = mutableListOf() + + private val pinnedView = frameLayout { + horizontalPadding = dp(10) + } + + private var pinnedContentView: InlineContentView? = null + + override val root = constraintLayout { + add(scrollView, lParams(matchConstraints, matchParent) { + startOfParent() + before(pinnedView) + centerVertically() + }) + add(scrollSurfaceView, lParams(matchConstraints, matchParent) { + centerOn(scrollView) + }) + add(pinnedView, lParams(wrapContent, matchParent) { + endOfParent() + centerVertically() + }) + } + + @RequiresApi(Build.VERSION_CODES.R) + private fun clearScrollView() { + scrollView.scrollTo(0, 0) + scrollView.removeAllViews() + scrollableContentViews.forEach { v -> + v.surfaceControl?.let { sc -> + SurfaceControl.Transaction().reparent(sc, null).apply() + } + } + scrollableContentViews.clear() + } + + @RequiresApi(Build.VERSION_CODES.R) + private fun clearPinnedView() { + pinnedView.removeAllViews() + pinnedContentView = null + } + + @RequiresApi(Build.VERSION_CODES.R) + fun clear() { + clearScrollView() + clearPinnedView() + } + + @RequiresApi(Build.VERSION_CODES.R) + fun setPinnedView(view: InlineContentView?) { + pinnedView.removeAllViews() + pinnedContentView = view?.also { + pinnedView.addView(it) + } + } + + @RequiresApi(Build.VERSION_CODES.R) + fun setScrollableViews(views: List) { + val flexbox = view(::FlexboxLayout) { + flexWrap = FlexWrap.NOWRAP + justifyContent = JustifyContent.CENTER + } + val parentSurfaceControl = scrollSurfaceView.surfaceControl + views.forEach { + scrollableContentViews.add(it) + it.setSurfaceControlCallback(object : InlineContentView.SurfaceControlCallback { + override fun onCreated(surfaceControl: SurfaceControl) { + SurfaceControl.Transaction() + .reparent(surfaceControl, parentSurfaceControl) + .apply() + } + + override fun onDestroyed(surfaceControl: SurfaceControl) {} + }) + flexbox.addView(it) + it.updateLayoutParams { + flexShrink = 0f + } + } + scrollView.apply { + scrollTo(0, 0) + removeAllViews() + add(flexbox, lParams(wrapContent, matchParent)) + } + } +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/NumberRow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/NumberRow.kt new file mode 100644 index 000000000..95e9b3dc3 --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/NumberRow.kt @@ -0,0 +1,31 @@ +package org.fcitx.fcitx5.android.input.bar.ui.idle + +import android.annotation.SuppressLint +import android.content.Context +import org.fcitx.fcitx5.android.core.KeySym +import org.fcitx.fcitx5.android.data.theme.Theme +import org.fcitx.fcitx5.android.input.keyboard.BaseKeyboard +import org.fcitx.fcitx5.android.input.keyboard.KeyAction +import org.fcitx.fcitx5.android.input.keyboard.KeyDef + +@SuppressLint("ViewConstructor") +class NumberRow(ctx: Context, theme: Theme) : BaseKeyboard(ctx, theme, Layout) { + companion object { + val Layout = listOf( + listOf("1", "2", "3", "4", "5", "6", "7", "8", "9", "0").map { digit -> + KeyDef( + KeyDef.Appearance.Text( + displayText = digit, + textSize = 21f, + border = KeyDef.Appearance.Border.Off, + margin = false + ), + setOf( + KeyDef.Behavior.Press(KeyAction.SymAction(KeySym(digit.codePointAt(0)))) + ), + arrayOf(KeyDef.Popup.Preview(digit)) + ) + } + ) + } +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardUi.kt index 6264f88d8..099f2ce67 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardUi.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardUi.kt @@ -9,7 +9,7 @@ import org.fcitx.fcitx5.android.R import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.data.theme.Theme import org.fcitx.fcitx5.android.data.theme.ThemeManager -import org.fcitx.fcitx5.android.input.bar.ToolButton +import org.fcitx.fcitx5.android.input.bar.ui.ToolButton import splitties.dimensions.dp import splitties.views.backgroundColor import splitties.views.dsl.core.* diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/editing/TextEditingUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/editing/TextEditingUi.kt index a807a7d95..a5feb9c69 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/editing/TextEditingUi.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/editing/TextEditingUi.kt @@ -11,7 +11,7 @@ import androidx.annotation.StringRes import org.fcitx.fcitx5.android.R import org.fcitx.fcitx5.android.data.theme.Theme import org.fcitx.fcitx5.android.data.theme.ThemeManager -import org.fcitx.fcitx5.android.input.bar.ToolButton +import org.fcitx.fcitx5.android.input.bar.ui.ToolButton import org.fcitx.fcitx5.android.input.keyboard.CustomGestureView import org.fcitx.fcitx5.android.utils.borderDrawable import org.fcitx.fcitx5.android.utils.pressHighlightDrawable diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/status/StatusAreaWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/status/StatusAreaWindow.kt index 951b3e63f..1d83b1d4a 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/status/StatusAreaWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/status/StatusAreaWindow.kt @@ -12,7 +12,7 @@ import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.data.theme.Theme import org.fcitx.fcitx5.android.data.theme.ThemeManager import org.fcitx.fcitx5.android.input.FcitxInputMethodService -import org.fcitx.fcitx5.android.input.bar.ToolButton +import org.fcitx.fcitx5.android.input.bar.ui.ToolButton import org.fcitx.fcitx5.android.input.broadcast.InputBroadcastReceiver import org.fcitx.fcitx5.android.input.dependency.fcitx import org.fcitx.fcitx5.android.input.dependency.inputMethodService From 20775c9efa806d302156ab1f55d3d77a5dd0ba15 Mon Sep 17 00:00:00 2001 From: rocka Date: Tue, 14 Mar 2023 00:10:19 +0800 Subject: [PATCH 056/624] Clipboard "Delete all" confirmation and "Swipe to delete" undo (#210) --- app/build.gradle.kts | 1 + .../3.json | 67 +++++++++++++++++ .../data/clipboard/ClipboardManager.kt | 26 +++++-- .../android/data/clipboard/db/ClipboardDao.kt | 36 +++++---- .../data/clipboard/db/ClipboardDatabase.kt | 5 +- .../data/clipboard/db/ClipboardEntry.kt | 4 +- .../input/clipboard/ClipboardAdapter.kt | 14 ++-- .../android/input/clipboard/ClipboardUi.kt | 20 +++-- .../input/clipboard/ClipboardWindow.kt | 75 +++++++++++++++---- app/src/main/res/values-night/themes.xml | 2 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/values/themes.xml | 2 + 12 files changed, 197 insertions(+), 57 deletions(-) create mode 100644 app/schemas/org.fcitx.fcitx5.android.data.clipboard.db.ClipboardDatabase/3.json diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0af81514e..84b7faca1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -305,6 +305,7 @@ tasks.register("cleanCxxIntermediates") { dependencies { implementation("androidx.autofill:autofill:1.1.0") implementation("org.ini4j:ini4j:0.5.4") + implementation("com.google.android.material:material:1.8.0") ksp(project(":codegen")) coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.2") implementation("io.arrow-kt:arrow-core:1.1.5") diff --git a/app/schemas/org.fcitx.fcitx5.android.data.clipboard.db.ClipboardDatabase/3.json b/app/schemas/org.fcitx.fcitx5.android.data.clipboard.db.ClipboardDatabase/3.json new file mode 100644 index 000000000..4808fb978 --- /dev/null +++ b/app/schemas/org.fcitx.fcitx5.android.data.clipboard.db.ClipboardDatabase/3.json @@ -0,0 +1,67 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "3046f390f998a6787882456a529ac924", + "entities": [ + { + "tableName": "clipboard", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT NOT NULL, `pinned` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL DEFAULT -1, `type` TEXT NOT NULL DEFAULT 'text/plain', `deleted` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'text/plain'" + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3046f390f998a6787882456a529ac924')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/ClipboardManager.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/ClipboardManager.kt index cbb6f1063..8cde51ddf 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/ClipboardManager.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/ClipboardManager.kt @@ -93,16 +93,28 @@ object ClipboardManager : ClipboardManager.OnPrimaryClipChangedListener, } suspend fun delete(id: Int) { - clbDao.delete(id) + clbDao.markAsDeleted(id) updateItemCount() } - suspend fun deleteAll(skipPinned: Boolean = true) { - if (skipPinned) - clbDao.deleteAllUnpinned() - else - clbDao.deleteAll() + suspend fun deleteAll(skipPinned: Boolean = true): IntArray { + val ids = if (skipPinned) { + clbDao.findUnpinnedIds() + } else { + clbDao.findAllIds() + } + clbDao.markAsDeleted(*ids) updateItemCount() + return ids + } + + suspend fun undoDelete(vararg ids: Int) { + clbDao.undoDelete(*ids) + updateItemCount() + } + + suspend fun realDelete() { + clbDao.realDelete() } suspend fun nukeTable() { @@ -143,7 +155,7 @@ object ClipboardManager : ClipboardManager.OnPrimaryClipChangedListener, .sortedBy { it.id } .getOrNull(unpinned.size - limit) // delete all unpinned before that, or delete all when limit <= 0 - clbDao.deleteUnpinnedIdLessThan(last?.timestamp ?: System.currentTimeMillis()) + clbDao.markUnpinnedAsDeletedEarlierThan(last?.timestamp ?: System.currentTimeMillis()) } } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardDao.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardDao.kt index 098047d14..b7b1b0c90 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardDao.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardDao.kt @@ -19,36 +19,42 @@ interface ClipboardDao { @Query("UPDATE ${ClipboardEntry.TABLE_NAME} SET timestamp=:timestamp WHERE id=:id") suspend fun updateTime(id: Int, timestamp: Long) - @Query("SELECT COUNT(*) FROM ${ClipboardEntry.TABLE_NAME}") + @Query("SELECT COUNT(*) FROM ${ClipboardEntry.TABLE_NAME} WHERE deleted=0") suspend fun itemCount(): Int - @Query("SELECT * FROM ${ClipboardEntry.TABLE_NAME} WHERE id=:id LIMIT 1") + @Query("SELECT * FROM ${ClipboardEntry.TABLE_NAME} WHERE id=:id AND deleted=0 LIMIT 1") suspend fun get(id: Int): ClipboardEntry? - @Query("SELECT * FROM ${ClipboardEntry.TABLE_NAME} WHERE rowId=:rowId LIMIT 1") + @Query("SELECT * FROM ${ClipboardEntry.TABLE_NAME} WHERE rowId=:rowId AND deleted=0 LIMIT 1") suspend fun get(rowId: Long): ClipboardEntry? - @Query("SELECT EXISTS(SELECT 1 FROM ${ClipboardEntry.TABLE_NAME} WHERE pinned=0)") + @Query("SELECT EXISTS(SELECT 1 FROM ${ClipboardEntry.TABLE_NAME} WHERE pinned=0 AND deleted=0)") suspend fun haveUnpinned(): Boolean - @Query("SELECT * FROM ${ClipboardEntry.TABLE_NAME} WHERE pinned=0") + @Query("SELECT * FROM ${ClipboardEntry.TABLE_NAME} WHERE pinned=0 AND deleted=0") suspend fun getAllUnpinned(): List - @Query("SELECT * FROM ${ClipboardEntry.TABLE_NAME} ORDER BY pinned DESC, timestamp DESC") + @Query("SELECT * FROM ${ClipboardEntry.TABLE_NAME} WHERE deleted=0 ORDER BY pinned DESC, timestamp DESC") fun allEntries(): PagingSource - @Query("SELECT * FROM ${ClipboardEntry.TABLE_NAME} WHERE text=:text LIMIT 1") + @Query("SELECT * FROM ${ClipboardEntry.TABLE_NAME} WHERE text=:text AND deleted=0 LIMIT 1") suspend fun find(text: String): ClipboardEntry? - @Query("DELETE FROM ${ClipboardEntry.TABLE_NAME} WHERE timestamp<:timestamp AND NOT pinned") - suspend fun deleteUnpinnedIdLessThan(timestamp: Long) + @Query("SELECT id FROM ${ClipboardEntry.TABLE_NAME} WHERE deleted=0") + suspend fun findAllIds(): IntArray - @Query("DELETE FROM ${ClipboardEntry.TABLE_NAME} WHERE id=:id") - suspend fun delete(id: Int) + @Query("SELECT id FROM ${ClipboardEntry.TABLE_NAME} WHERE pinned=0 AND deleted=0") + suspend fun findUnpinnedIds(): IntArray - @Query("DELETE FROM ${ClipboardEntry.TABLE_NAME} WHERE NOT pinned") - suspend fun deleteAllUnpinned() + @Query("UPDATE ${ClipboardEntry.TABLE_NAME} SET deleted=1 WHERE id in (:ids)") + suspend fun markAsDeleted(vararg ids: Int) - @Query("DELETE FROM ${ClipboardEntry.TABLE_NAME}") - suspend fun deleteAll() + @Query("UPDATE ${ClipboardEntry.TABLE_NAME} SET DELETED=1 WHERE timestamp<:timestamp AND pinned=0 AND deleted=0") + suspend fun markUnpinnedAsDeletedEarlierThan(timestamp: Long) + + @Query("UPDATE ${ClipboardEntry.TABLE_NAME} SET deleted=0 WHERE id in (:ids) AND deleted=1") + suspend fun undoDelete(vararg ids: Int) + + @Query("DELETE FROM ${ClipboardEntry.TABLE_NAME} WHERE deleted=1") + suspend fun realDelete() } \ No newline at end of file diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardDatabase.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardDatabase.kt index 241adc4e7..85d7f9d65 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardDatabase.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardDatabase.kt @@ -6,9 +6,10 @@ import androidx.room.RoomDatabase @Database( entities = [ClipboardEntry::class], - version = 2, + version = 3, autoMigrations = [ - AutoMigration(from = 1, to = 2) + AutoMigration(from = 1, to = 2), + AutoMigration(from = 2, to = 3) ] ) abstract class ClipboardDatabase : RoomDatabase() { diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardEntry.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardEntry.kt index 3a09dcbe3..0a6f62671 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardEntry.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardEntry.kt @@ -15,7 +15,9 @@ data class ClipboardEntry( @ColumnInfo(defaultValue = "-1") val timestamp: Long = System.currentTimeMillis(), @ColumnInfo(defaultValue = ClipDescription.MIMETYPE_TEXT_PLAIN) - val type: String = ClipDescription.MIMETYPE_TEXT_PLAIN + val type: String = ClipDescription.MIMETYPE_TEXT_PLAIN, + @ColumnInfo(defaultValue = "0") + val deleted: Boolean = false, ) { companion object { const val TABLE_NAME = "clipboard" diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardAdapter.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardAdapter.kt index 9161d8fc3..6ea100902 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardAdapter.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardAdapter.kt @@ -27,7 +27,7 @@ abstract class ClipboardAdapter : oldItem: ClipboardEntry, newItem: ClipboardEntry ): Boolean { - return true + return oldItem.id == newItem.id } override fun areContentsTheSame( @@ -45,23 +45,19 @@ abstract class ClipboardAdapter : * @param chars max chars per output line */ fun excerptText(str: String, lines: Int = 4, chars: Int = 128) = buildString { - val totalLength = str.length + val length = str.length var lineBreak = -1 for (i in 1..lines) { val start = lineBreak + 1 // skip previous '\n' + val excerptEnd = min(start + chars, length) lineBreak = str.indexOf('\n', start) if (lineBreak < 0) { // no line breaks remaining, substring to end of text - append(str.substring(start, min(start + chars, totalLength))) + append(str.substring(start, excerptEnd)) break } else { // append one line exactly - appendLine( - str.substring( - start, - min(min(lineBreak, start + chars), totalLength) - ) - ) + appendLine(str.substring(start, min(excerptEnd, lineBreak))) } } } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardUi.kt index 099f2ce67..f43160f54 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardUi.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardUi.kt @@ -12,6 +12,8 @@ import org.fcitx.fcitx5.android.data.theme.ThemeManager import org.fcitx.fcitx5.android.input.bar.ui.ToolButton import splitties.dimensions.dp import splitties.views.backgroundColor +import splitties.views.dsl.coordinatorlayout.coordinatorLayout +import splitties.views.dsl.coordinatorlayout.defaultLParams import splitties.views.dsl.core.* import splitties.views.dsl.recyclerview.recyclerView import timber.log.Timber @@ -26,16 +28,20 @@ class ClipboardUi(override val ctx: Context, private val theme: Theme) : Ui { val emptyUi = ClipboardInstructionUi.Empty(ctx, theme) + val viewAnimator = view(::ViewAnimator) { + add(recyclerView, lParams(matchParent, matchParent)) + add(emptyUi.root, lParams(matchParent, matchParent)) + add(enableUi.root, lParams(matchParent, matchParent)) + } + private val keyBorder by ThemeManager.prefs.keyBorder private val disableAnimation by AppPrefs.getInstance().advanced.disableAnimation - override val root = view(::ViewAnimator) { + override val root = coordinatorLayout { if (!keyBorder) { backgroundColor = theme.barColor } - add(recyclerView, lParams(matchParent, matchParent)) - add(emptyUi.root, lParams(matchParent, matchParent)) - add(enableUi.root, lParams(matchParent, matchParent)) + add(viewAnimator, defaultLParams(matchParent, matchParent)) } val deleteAllButton = ToolButton(ctx, R.drawable.ic_baseline_delete_sweep_24, theme) @@ -54,15 +60,15 @@ class ClipboardUi(override val ctx: Context, private val theme: Theme) : Ui { TransitionManager.beginDelayedTransition(root, Fade().apply { duration = 100L }) when (state) { ClipboardStateMachine.State.Normal -> { - root.displayedChild = 0 + viewAnimator.displayedChild = 0 setDeleteButtonShown(true) } ClipboardStateMachine.State.AddMore -> { - root.displayedChild = 1 + viewAnimator.displayedChild = 1 setDeleteButtonShown(false) } ClipboardStateMachine.State.EnableListening -> { - root.displayedChild = 2 + viewAnimator.displayedChild = 2 setDeleteButtonShown(false) } } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardWindow.kt index ca78dc9bd..7a7868a7c 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/clipboard/ClipboardWindow.kt @@ -1,16 +1,22 @@ package org.fcitx.fcitx5.android.input.clipboard +import android.annotation.SuppressLint import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout import android.widget.PopupMenu import androidx.core.text.bold import androidx.core.text.buildSpannedString import androidx.core.text.color +import androidx.core.view.updateLayoutParams import androidx.lifecycle.lifecycleScope import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.StaggeredGridLayoutManager +import com.google.android.material.snackbar.Snackbar +import com.google.android.material.snackbar.SnackbarContentLayout import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -31,7 +37,9 @@ import org.fcitx.fcitx5.android.input.dependency.theme import org.fcitx.fcitx5.android.input.wm.InputWindow import org.fcitx.fcitx5.android.utils.AppUtil import org.fcitx.fcitx5.android.utils.EventStateMachine +import splitties.dimensions.dp import splitties.resources.styledColor +import splitties.views.dsl.core.withTheme import kotlin.properties.Delegates class ClipboardWindow : InputWindow.ExtendedInputWindow() { @@ -39,6 +47,10 @@ class ClipboardWindow : InputWindow.ExtendedInputWindow() { private val service: FcitxInputMethodService by manager.inputMethodService() private val theme by manager.theme() + private val snackbarCtx by lazy { + context.withTheme(R.style.InputViewSnackbarTheme) + } + private lateinit var stateMachine: EventStateMachine private var isClipboardDbEmpty by Delegates.observable(ClipboardManager.itemCount == 0) { _, _, new -> @@ -56,16 +68,10 @@ class ClipboardWindow : InputWindow.ExtendedInputWindow() { private val clipboardEnabledPref = AppPrefs.getInstance().clipboard.clipboardListening private val clipboardEntriesPager by lazy { - Pager(PagingConfig(pageSize = 10)) { ClipboardManager.allEntries() } + Pager(PagingConfig(pageSize = 16)) { ClipboardManager.allEntries() } } private var adapterSubmitJob: Job? = null - private fun deleteAllEntries(skipPinned: Boolean) { - service.lifecycleScope.launch { - ClipboardManager.deleteAll(skipPinned) - } - } - private val adapter: ClipboardAdapter by lazy { object : ClipboardAdapter() { override val theme = this@ClipboardWindow.theme @@ -82,7 +88,10 @@ class ClipboardWindow : InputWindow.ExtendedInputWindow() { } override fun onDelete(id: Int) { - service.lifecycleScope.launch { ClipboardManager.delete(id) } + service.lifecycleScope.launch { + ClipboardManager.delete(id) + showUndoSnackbar(id) + } } override fun onPaste(entry: ClipboardEntry) { @@ -117,6 +126,7 @@ class ClipboardWindow : InputWindow.ExtendedInputWindow() { val entry = adapter.getEntryAt(viewHolder.bindingAdapterPosition) ?: return service.lifecycleScope.launch { ClipboardManager.delete(entry.id) + showUndoSnackbar(entry.id) } } }).attachToRecyclerView(recyclerView) @@ -125,11 +135,7 @@ class ClipboardWindow : InputWindow.ExtendedInputWindow() { } deleteAllButton.setOnClickListener { service.lifecycleScope.launch { - if (ClipboardManager.haveUnpinned()) { - deleteAllEntries(skipPinned = true) - } else { - promptDeleteAllPinned() - } + promptDeleteAll(ClipboardManager.haveUnpinned()) } } } @@ -139,14 +145,14 @@ class ClipboardWindow : InputWindow.ExtendedInputWindow() { private var promptMenu: PopupMenu? = null - private fun promptDeleteAllPinned() { + private fun promptDeleteAll(skipPinned: Boolean) { promptMenu?.dismiss() promptMenu = PopupMenu(context, ui.deleteAllButton).apply { menu.apply { add(buildSpannedString { bold { color(context.styledColor(android.R.attr.colorAccent)) { - append(context.getString(R.string.delete_all_pinned_items)) + append(context.getString(if (skipPinned) R.string.delete_all_except_pinned else R.string.delete_all_pinned_items)) } } }).apply { @@ -157,7 +163,10 @@ class ClipboardWindow : InputWindow.ExtendedInputWindow() { } add(android.R.string.ok).apply { setOnMenuItemClickListener { - deleteAllEntries(skipPinned = false) + service.lifecycleScope.launch { + val ids = ClipboardManager.deleteAll(skipPinned) + showUndoSnackbar(*ids) + } true } } @@ -169,6 +178,40 @@ class ClipboardWindow : InputWindow.ExtendedInputWindow() { } } + @SuppressLint("RestrictedApi") + private fun showUndoSnackbar(vararg id: Int) { + val str = context.resources.getString(R.string.num_items_deleted, id.size) + Snackbar.make(snackbarCtx, ui.root, str, Snackbar.LENGTH_LONG) + .setBackgroundTint(theme.keyBackgroundColor) + .setTextColor(theme.keyTextColor) + .setActionTextColor(theme.genericActiveBackgroundColor) + .setAction(R.string.undo) { + service.lifecycleScope.launch { + ClipboardManager.undoDelete(*id) + } + } + .addCallback(object : Snackbar.Callback() { + override fun onDismissed(transientBottomBar: Snackbar, event: Int) { + service.lifecycleScope.launch { + ClipboardManager.realDelete() + } + } + }).apply { + val hMargin = snackbarCtx.dp(24) + val vMargin = snackbarCtx.dp(16) + view.updateLayoutParams { + leftMargin = hMargin + rightMargin = hMargin + bottomMargin = vMargin + } + ((view as FrameLayout).getChildAt(0) as SnackbarContentLayout).apply { + messageView.letterSpacing = 0f + actionView.letterSpacing = 0f + } + show() + } + } + override fun onAttached() { val initialState = when { !clipboardEnabledPref.getValue() -> EnableListening diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index f36b62f1e..604374127 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -16,4 +16,6 @@ + + + + - - - -