From d0de4aa0e2706cedf0009890c81fff189bbb5478 Mon Sep 17 00:00:00 2001 From: KazumaProject <59742125+KazumaProject@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:17:00 -0400 Subject: [PATCH 1/7] refactor empty suggestion adapter --- .../CandidateStripPresentationPolicy.kt | 9 +- .../ime_service/IMEService.kt | 159 ++++++++- .../ShortcutToolbarPresentationPolicy.kt | 21 +- .../ime_service/adapters/SuggestionAdapter.kt | 303 +++++++++++++----- ...CandidateHeightLandscapeSettingFragment.kt | 6 +- .../CandidateViewHeightSettingFragment.kt | 6 +- app/src/main/res/drawable/more_horiz_24px.xml | 10 + .../suggestion_clipboard_preview_item.xml | 56 ++++ .../layout/suggestion_quick_actions_item.xml | 146 +++++++++ .../layout/suggestion_shortcut_entry_item.xml | 18 ++ app/src/main/res/values-ja/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + .../CandidateStripPresentationPolicyTest.kt | 73 ++++- .../ShortcutToolbarPresentationPolicyTest.kt | 80 ++++- .../SuggestionAdapterDisplayItemTest.kt | 177 ++++++++++ ...SuggestionAdapterShortcutEntryClickTest.kt | 53 +++ 16 files changed, 996 insertions(+), 123 deletions(-) create mode 100644 app/src/main/res/drawable/more_horiz_24px.xml create mode 100644 app/src/main/res/layout/suggestion_clipboard_preview_item.xml create mode 100644 app/src/main/res/layout/suggestion_quick_actions_item.xml create mode 100644 app/src/main/res/layout/suggestion_shortcut_entry_item.xml create mode 100644 app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapterDisplayItemTest.kt create mode 100644 app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapterShortcutEntryClickTest.kt diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/CandidateStripPresentationPolicy.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/CandidateStripPresentationPolicy.kt index dee13587..0488fd2a 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/CandidateStripPresentationPolicy.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/CandidateStripPresentationPolicy.kt @@ -21,7 +21,8 @@ data class CandidateStripPresentation( val resetCandidateTabSelection: Boolean, val showIndependentShortcutToolbar: Boolean, val reserveIndependentShortcutToolbarSpace: Boolean, - val showIntegratedShortcut: Boolean + val showIntegratedShortcutItems: Boolean, + val showIntegratedShortcutEntry: Boolean ) object CandidateStripPresentationPolicy { @@ -49,8 +50,10 @@ object CandidateStripPresentationPolicy { shortcutPresentation.showIndependentToolbar && !hideShortcutForCandidates, reserveIndependentShortcutToolbarSpace = shortcutPresentation.showIndependentToolbar && hideShortcutForCandidates, - showIntegratedShortcut = - shortcutPresentation.showIntegratedShortcuts && !hideShortcutForCandidates + showIntegratedShortcutItems = + shortcutPresentation.showIntegratedShortcutItems && !hideShortcutForCandidates, + showIntegratedShortcutEntry = + shortcutPresentation.showIntegratedShortcutEntry && !hideShortcutForCandidates ) } } diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/IMEService.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/IMEService.kt index f0edff91..eefd6f21 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/IMEService.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/IMEService.kt @@ -10,6 +10,7 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Color import android.graphics.Matrix +import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable import android.hardware.input.InputManager @@ -497,6 +498,10 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, private var zenzEngine: ZenzEngine? = null private var shortcutAdapter: ShortcutAdapter? = null + private var shortcutEntryPopupWindow: PopupWindow? = null + private var shortcutEntryPopupAdapter: ShortcutAdapter? = null + private var shortcutEntryPopupAnchorView: View? = null + private var shortcutEntryPopupDetachListener: View.OnAttachStateChangeListener? = null private var romajiConverter: RomajiKanaConverter? = null @@ -649,6 +654,7 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, private fun setSuggestionAdapterSuggestionsOnMain(candidates: List) { runOnMainThread { measureDebugSection("IMEService.setSuggestionAdapterSuggestionsOnMain") { + dismissShortcutEntryPopup() suggestionAdapter?.suggestions = candidates } } @@ -660,6 +666,7 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, ) { runOnMainThread { measureDebugSection("IMEService.setSuggestionAdaptersOnMain") { + dismissShortcutEntryPopup() suggestionAdapter?.suggestions = candidates suggestionAdapterFull?.suggestions = fullCandidates } @@ -673,6 +680,7 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, ) = measureDebugStage("IMEService.updateSuggestionAdaptersOnMain") { withContext(Dispatchers.Main.immediate) { if (!shouldApplyCandidateResult(insertString)) return@withContext + dismissShortcutEntryPopup() suggestionAdapter?.suggestions = candidates suggestionAdapterFull?.suggestions = fullCandidates } @@ -3285,6 +3293,7 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, override fun onStartInputView(editorInfo: EditorInfo?, restarting: Boolean) { super.onStartInputView(editorInfo, restarting) isInputViewActive = true + dismissShortcutEntryPopup() shortcutInputBehaviorOverride = null keyboardSelectionPopupWindow?.dismiss() addUserDictionaryPopup?.dismiss() @@ -3654,6 +3663,7 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, releaseKeyboardBackgroundVideoPlayer() releaseFloatingKeyboardBackgroundVideoPlayer() stopVoiceInput() + dismissShortcutEntryPopup() floatingCandidateWindow?.dismiss() floatingDockWindow?.dismiss() floatingModeSwitchWindow?.dismiss() @@ -3673,6 +3683,7 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, Timber.d("onUpdate onDestroy") stopAllOngoingKeyLongPresses() disableKeyboardLayoutEditMode(updateSurface = false) + dismissShortcutEntryPopup() isInputViewActive = false releaseSuminagashiInkEffects() releaseKeyboardBackgroundVideoPlayer() @@ -4038,6 +4049,7 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) + dismissShortcutEntryPopup() when (newConfig.orientation) { Configuration.ORIENTATION_PORTRAIT -> { finishComposingText() @@ -11910,6 +11922,7 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, */ private fun updateClipboardPreview() { Timber.d("SuggestionAdapter Clipboard: updateClipboardPreview") + dismissShortcutEntryPopup() suggestionAdapter?.apply { when (val item = clipboardUtil.getPrimaryClipContent()) { is ClipboardItem.Image -> { @@ -16797,6 +16810,9 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, adapter.setOnShortcutItemClickListener { type -> handleShortcutAction(type, mainView) } + adapter.setOnShortcutEntryClickListener { anchorView -> + showShortcutEntryPopup(anchorView, mainView) + } } suggestionAdapterFull?.let { adapter -> adapter.setOnItemClickListener { candidate, position -> @@ -16989,6 +17005,8 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, override fun getSpanSize(position: Int): Int { return when (adapter?.getItemViewType(position)) { SuggestionAdapter.VIEW_TYPE_EMPTY, + SuggestionAdapter.VIEW_TYPE_CLIPBOARD_PREVIEW, + SuggestionAdapter.VIEW_TYPE_SHORTCUT_ENTRY, SuggestionAdapter.VIEW_TYPE_CUSTOM_LAYOUT_PICKER, SuggestionAdapter.VIEW_TYPE_SHORTCUT -> spanCount else -> 1 @@ -17590,17 +17608,25 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, return applicationContext.dpToPx(toolbarHeightDp) } - private fun applyShortcutToolbarSize(mainView: MainLayoutBinding) { - val toolbarHeightDp = shortcutToolbarHeightDp.coerceIn( + private fun shortcutToolbarIconSizePx(toolbarHeightDp: Int = shortcutToolbarHeightDp): Int { + val normalizedToolbarHeightDp = toolbarHeightDp.coerceIn( AppPreference.SHORTCUT_TOOLBAR_HEIGHT_MIN_DP, AppPreference.SHORTCUT_TOOLBAR_HEIGHT_MAX_DP ) val iconSizeDp = appPreference.resolveShortcutToolbarIconSizeDp( - toolbarHeightDp = toolbarHeightDp, + toolbarHeightDp = normalizedToolbarHeightDp, iconSizeDp = shortcutToolbarIconSizeDp ) + return applicationContext.dpToPx(iconSizeDp) + } + + private fun applyShortcutToolbarSize(mainView: MainLayoutBinding) { + val toolbarHeightDp = shortcutToolbarHeightDp.coerceIn( + AppPreference.SHORTCUT_TOOLBAR_HEIGHT_MIN_DP, + AppPreference.SHORTCUT_TOOLBAR_HEIGHT_MAX_DP + ) val toolbarHeightPx = applicationContext.dpToPx(toolbarHeightDp) - val iconSizePx = applicationContext.dpToPx(iconSizeDp) + val iconSizePx = shortcutToolbarIconSizePx(toolbarHeightDp) mainView.shortcutToolbarRecyclerview.layoutParams = mainView.shortcutToolbarRecyclerview.layoutParams.apply { height = toolbarHeightPx @@ -17662,7 +17688,9 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, candidatesShown = candidatesShown, resetCandidateTabSelection = resetCandidateTabSelection ) - suggestionAdapter?.setIntegratedShortcutVisibility(presentation.showIntegratedShortcut) + dismissShortcutEntryPopup() + suggestionAdapter?.setIntegratedShortcutItemsVisibility(presentation.showIntegratedShortcutItems) + suggestionAdapter?.setIntegratedShortcutEntryVisibility(presentation.showIntegratedShortcutEntry) applyCandidateTabSuggestionOffset(mainView, presentation.showCandidateTab) mainView.candidateTabLayout.isVisible = presentation.showCandidateTab if (presentation.resetCandidateTabSelection) { @@ -17678,6 +17706,127 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, } } + private fun showShortcutEntryPopup(anchorView: View, mainView: MainLayoutBinding) { + dismissShortcutEntryPopup() + val shortcutItems = shortcutAdapter?.currentList.orEmpty() + if (shortcutItems.isEmpty()) return + if (!canShowPopupWindow(anchorView)) return + + val toolbarHeightPx = shortcutToolbarHeightPx() + val popupVerticalPaddingPx = applicationContext.dpToPx(4) + val popupHorizontalPaddingPx = applicationContext.dpToPx(8) + val popupWidth = mainView.root.width + .takeIf { it > 0 } + ?: resources.displayMetrics.widthPixels + + val popupAdapter = ShortcutAdapter().apply { + setShortcutToolbarSize( + toolbarHeightPx = toolbarHeightPx, + iconSizePx = shortcutToolbarIconSizePx() + ) + setActiveShortcutTypes( + resolveShortcutActiveTypes( + keyboardLayoutEditActive = keyboardLayoutEditState.value is KeyboardLayoutEditState.Enabled, + keyboardFloatingActive = isKeyboardFloatingMode == true, + inputBehavior = currentInputBehavior, + ) + ) + if (keyboardThemeMode == "custom") { + setIconColor(customThemeShortcutIconColor ?: Color.BLACK) + } + onItemClicked = { type -> + dismissShortcutEntryPopup() + handleShortcutAction(type, mainView) + } + submitList(shortcutItems) + } + val recyclerView = RecyclerView(this).apply { + layoutManager = + LinearLayoutManager(this@IMEService, LinearLayoutManager.HORIZONTAL, false) + adapter = popupAdapter + itemAnimator = null + isFocusable = false + overScrollMode = View.OVER_SCROLL_NEVER + } + val contentView = FrameLayout(this).apply { + setPadding( + popupHorizontalPaddingPx, + popupVerticalPaddingPx, + popupHorizontalPaddingPx, + popupVerticalPaddingPx + ) + background = GradientDrawable().apply { + cornerRadius = resources.displayMetrics.density * 12f + setColor( + customThemeSpecialKeyColor ?: ContextCompat.getColor( + this@IMEService, + com.kazumaproject.core.R.color.keyboard_bg + ) + ) + } + addView( + recyclerView, + FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + toolbarHeightPx + ) + ) + } + val popupHeight = toolbarHeightPx + popupVerticalPaddingPx * 2 + val popupWindow = PopupWindow( + contentView, + popupWidth, + popupHeight, + false + ).apply { + isOutsideTouchable = true + isClippingEnabled = true + inputMethodMode = PopupWindow.INPUT_METHOD_NOT_NEEDED + setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + } + val detachListener = object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) = Unit + + override fun onViewDetachedFromWindow(v: View) { + dismissShortcutEntryPopup() + } + } + shortcutEntryPopupWindow = popupWindow + shortcutEntryPopupAdapter = popupAdapter + shortcutEntryPopupAnchorView = anchorView + shortcutEntryPopupDetachListener = detachListener + anchorView.addOnAttachStateChangeListener(detachListener) + popupWindow.setOnDismissListener { + clearShortcutEntryPopupReferences(popupWindow) + } + runCatching { + popupWindow.showAsDropDown(anchorView, 0, 0) + }.onFailure { throwable -> + Timber.w(throwable, "Shortcut entry popup show failed") + dismissShortcutEntryPopup() + } + } + + private fun dismissShortcutEntryPopup() { + val popupWindow = shortcutEntryPopupWindow + clearShortcutEntryPopupReferences(popupWindow) + if (popupWindow?.isShowing == true) { + popupWindow.dismiss() + } + } + + private fun clearShortcutEntryPopupReferences(popupWindow: PopupWindow?) { + if (popupWindow != null && shortcutEntryPopupWindow !== popupWindow) return + shortcutEntryPopupDetachListener?.let { listener -> + shortcutEntryPopupAnchorView?.removeOnAttachStateChangeListener(listener) + } + shortcutEntryPopupAdapter?.onItemClicked = null + shortcutEntryPopupWindow = null + shortcutEntryPopupAdapter = null + shortcutEntryPopupAnchorView = null + shortcutEntryPopupDetachListener = null + } + private fun handleShortcutAction(type: ShortcutType, mainView: MainLayoutBinding) { when (type) { ShortcutType.SETTINGS -> { diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/ShortcutToolbarPresentationPolicy.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/ShortcutToolbarPresentationPolicy.kt index 95e268fc..9daeed9c 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/ShortcutToolbarPresentationPolicy.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/ShortcutToolbarPresentationPolicy.kt @@ -14,7 +14,8 @@ data class ShortcutToolbarPresentationState( data class ShortcutToolbarPresentation( val showIndependentToolbar: Boolean, - val showIntegratedShortcuts: Boolean + val showIntegratedShortcutItems: Boolean, + val showIntegratedShortcutEntry: Boolean ) object ShortcutToolbarPresentationPolicy { @@ -23,25 +24,35 @@ object ShortcutToolbarPresentationPolicy { if (!state.shortcutToolbarVisible || state.symbolKeyboardShown) { return ShortcutToolbarPresentation( showIndependentToolbar = false, - showIntegratedShortcuts = false + showIntegratedShortcutItems = false, + showIntegratedShortcutEntry = false ) } if (!state.integratedInSuggestion) { return ShortcutToolbarPresentation( showIndependentToolbar = true, - showIntegratedShortcuts = false + showIntegratedShortcutItems = false, + showIntegratedShortcutEntry = false ) } - val showIntegratedShortcuts = + val showIntegratedShortcutItems = state.inputStringEmpty && state.tailEmpty && !state.clipboardPreviewShown && !state.selectedTextGemmaActionsShown && state.suggestionsEmpty && !state.customLayoutPickerShown + val isCentralSpecialContentShown = + state.clipboardPreviewShown || state.selectedTextGemmaActionsShown + val showIntegratedShortcutEntry = + state.inputStringEmpty && + state.tailEmpty && + isCentralSpecialContentShown && + !state.customLayoutPickerShown return ShortcutToolbarPresentation( showIndependentToolbar = false, - showIntegratedShortcuts = showIntegratedShortcuts + showIntegratedShortcutItems = showIntegratedShortcutItems, + showIntegratedShortcutEntry = showIntegratedShortcutEntry ) } } diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapter.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapter.kt index bba1fffc..efa29405 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapter.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapter.kt @@ -111,6 +111,8 @@ class SuggestionAdapter : RecyclerView.Adapter() { const val VIEW_TYPE_CUSTOM_LAYOUT_PICKER = 2 const val VIEW_TYPE_GEMMA_ACTION = 3 const val VIEW_TYPE_SHORTCUT = 4 + const val VIEW_TYPE_CLIPBOARD_PREVIEW = 5 + const val VIEW_TYPE_SHORTCUT_ENTRY = 6 private val diffThreadIndex = AtomicInteger(0) private val diffExecutor: Executor = Executors.newFixedThreadPool(2) { runnable -> @@ -124,6 +126,16 @@ class SuggestionAdapter : RecyclerView.Adapter() { UNDO, REDO, RECONVERT, PASTE } + internal enum class SuggestionDisplayItemKind { + CandidateItem, + GemmaActionItem, + QuickActionsItem, + ClipboardPreviewItem, + ShortcutEntryItem, + ShortcutItem, + CustomLayoutItem + } + private sealed class SuggestionDisplayItem { data class CandidateItem( val candidate: Candidate, @@ -135,10 +147,16 @@ class SuggestionAdapter : RecyclerView.Adapter() { val candidateIndex: Int, ) : SuggestionDisplayItem() - data class HelperActionsItem( - val state: HelperActionsState, + data class QuickActionsItem( + val state: QuickActionsState, ) : SuggestionDisplayItem() + data class ClipboardPreviewItem( + val state: ClipboardPreviewState, + ) : SuggestionDisplayItem() + + object ShortcutEntryItem : SuggestionDisplayItem() + data class ShortcutItem( val shortcutType: ShortcutType, ) : SuggestionDisplayItem() @@ -149,29 +167,32 @@ class SuggestionAdapter : RecyclerView.Adapter() { ) : SuggestionDisplayItem() } - private data class HelperActionsState( + private data class QuickActionsState( val undoEnabled: Boolean, val redoEnabled: Boolean, val reconvertEnabled: Boolean, - val pasteEnabled: Boolean, - val clipboardDescriptionShown: Boolean, - val clipboardText: String, - val clipboardBitmap: Bitmap?, val undoText: String, val redoText: String, val incognitoIconDrawable: android.graphics.drawable.Drawable?, ) { - val hasClipboardPreview: Boolean - get() = pasteEnabled && (clipboardBitmap != null || clipboardText.isNotBlank()) - val hasVisibleAction: Boolean get() = undoEnabled || redoEnabled || reconvertEnabled || - pasteEnabled || incognitoIconDrawable != null } + private data class ClipboardPreviewState( + val pasteEnabled: Boolean, + val clipboardDescriptionShown: Boolean, + val clipboardText: String, + val clipboardBitmap: Bitmap?, + val hasLeadingShortcutEntry: Boolean + ) { + val hasClipboardPreview: Boolean + get() = pasteEnabled && (clipboardBitmap != null || clipboardText.isNotBlank()) + } + // Listeners for clicks private var onItemClickListener: ((Candidate, Int) -> Unit)? = null private var onItemLongClickListener: ((Candidate, Int) -> Unit)? = null @@ -179,6 +200,7 @@ class SuggestionAdapter : RecyclerView.Adapter() { private var onItemHelperIconLongClickListener: ((HelperIcon) -> Unit)? = null private var onCustomLayoutItemClickListener: ((Int) -> Unit)? = null private var onShortcutItemClickListener: ((ShortcutType) -> Unit)? = null + private var onShortcutEntryClickListener: ((View) -> Unit)? = null private var onShowSoftKeyboardClick: (() -> Unit)? = null private val adapterScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) @@ -203,7 +225,8 @@ class SuggestionAdapter : RecyclerView.Adapter() { private var showCustomTab: Boolean = true private var shortcutItems: List = emptyList() - private var showIntegratedShortcuts: Boolean = false + private var showIntegratedShortcutItems: Boolean = false + private var showIntegratedShortcutEntry: Boolean = false private var shortcutIconColor: Int? = null private var activeShortcutTypes: Set = emptySet() @@ -243,6 +266,10 @@ class SuggestionAdapter : RecyclerView.Adapter() { this.onShortcutItemClickListener = listener } + fun setOnShortcutEntryClickListener(listener: (View) -> Unit) { + this.onShortcutEntryClickListener = listener + } + fun setOnPhysicalKeyboardListener(listener: () -> Unit) { this.onShowSoftKeyboardClick = listener } @@ -255,6 +282,7 @@ class SuggestionAdapter : RecyclerView.Adapter() { onItemHelperIconLongClickListener = null onCustomLayoutItemClickListener = null onShortcutItemClickListener = null + onShortcutEntryClickListener = null onShowSoftKeyboardClick = null onListUpdated = null incognitoIconDrawable = null @@ -324,7 +352,7 @@ class SuggestionAdapter : RecyclerView.Adapter() { } fun isShowingClipboardPreviewForEmptyState(): Boolean { - return currentHelperActionsState().hasClipboardPreview + return currentClipboardPreviewState(hasLeadingShortcutEntry = false).hasClipboardPreview } fun isShowingCustomLayoutPicker(): Boolean { @@ -337,16 +365,22 @@ class SuggestionAdapter : RecyclerView.Adapter() { rebuildDisplayItems() } - fun setIntegratedShortcutVisibility(visible: Boolean) { - if (showIntegratedShortcuts == visible) return - showIntegratedShortcuts = visible + fun setIntegratedShortcutItemsVisibility(visible: Boolean) { + if (showIntegratedShortcutItems == visible) return + showIntegratedShortcutItems = visible + rebuildDisplayItems() + } + + fun setIntegratedShortcutEntryVisibility(visible: Boolean) { + if (showIntegratedShortcutEntry == visible) return + showIntegratedShortcutEntry = visible rebuildDisplayItems() } fun setShortcutIconColor(color: Int) { if (shortcutIconColor == color) return shortcutIconColor = color - if (showIntegratedShortcuts && suggestions.isEmpty()) { + if (showIntegratedShortcutItems || showIntegratedShortcutEntry) { notifyItemRangeChanged(0, itemCount) } } @@ -417,8 +451,14 @@ class SuggestionAdapter : RecyclerView.Adapter() { oldItem.candidate.string == newItem.candidate.string && oldItem.candidate.type == newItem.candidate.type - oldItem is SuggestionDisplayItem.HelperActionsItem && - newItem is SuggestionDisplayItem.HelperActionsItem -> true + oldItem is SuggestionDisplayItem.QuickActionsItem && + newItem is SuggestionDisplayItem.QuickActionsItem -> true + + oldItem is SuggestionDisplayItem.ClipboardPreviewItem && + newItem is SuggestionDisplayItem.ClipboardPreviewItem -> true + + oldItem is SuggestionDisplayItem.ShortcutEntryItem && + newItem is SuggestionDisplayItem.ShortcutEntryItem -> true oldItem is SuggestionDisplayItem.ShortcutItem && newItem is SuggestionDisplayItem.ShortcutItem -> @@ -508,11 +548,16 @@ class SuggestionAdapter : RecyclerView.Adapter() { private fun buildDisplayItems(): List { if (candidateSuggestions.isNotEmpty()) { - return candidateSuggestions.mapIndexed { index, candidate -> - if (candidate.isSelectedTextGemmaActionCandidate()) { - SuggestionDisplayItem.GemmaActionItem(candidate, index) - } else { - SuggestionDisplayItem.CandidateItem(candidate, index) + return buildList { + if (isShowingSelectedTextGemmaActions() && shouldShowIntegratedShortcutEntry()) { + add(SuggestionDisplayItem.ShortcutEntryItem) + } + candidateSuggestions.forEachIndexed { index, candidate -> + if (candidate.isSelectedTextGemmaActionCandidate()) { + add(SuggestionDisplayItem.GemmaActionItem(candidate, index)) + } else { + add(SuggestionDisplayItem.CandidateItem(candidate, index)) + } } } } @@ -524,11 +569,23 @@ class SuggestionAdapter : RecyclerView.Adapter() { } return buildList { - val helperState = currentHelperActionsState() - if (helperState.hasVisibleAction) { - add(SuggestionDisplayItem.HelperActionsItem(helperState)) + val showShortcutEntry = shouldShowIntegratedShortcutEntry() + val clipboardPreviewState = currentClipboardPreviewState( + hasLeadingShortcutEntry = showShortcutEntry + ) + if (clipboardPreviewState.hasClipboardPreview) { + if (showShortcutEntry) { + add(SuggestionDisplayItem.ShortcutEntryItem) + } + add(SuggestionDisplayItem.ClipboardPreviewItem(clipboardPreviewState)) + return@buildList + } + + val quickActionsState = currentQuickActionsState() + if (quickActionsState.hasVisibleAction) { + add(SuggestionDisplayItem.QuickActionsItem(quickActionsState)) } - if (shouldShowIntegratedShortcuts()) { + if (shouldShowIntegratedShortcutItems()) { shortcutItems.forEach { shortcutType -> add(SuggestionDisplayItem.ShortcutItem(shortcutType)) } @@ -536,20 +593,49 @@ class SuggestionAdapter : RecyclerView.Adapter() { } } - private fun currentHelperActionsState(): HelperActionsState = - HelperActionsState( + internal fun buildDisplayItemKindsForTesting(): List { + return buildDisplayItems().map { it.kind() } + } + + private fun currentQuickActionsState(): QuickActionsState = + QuickActionsState( undoEnabled = isUndoEnabled, redoEnabled = isRedoEnabled, reconvertEnabled = isReconvertEnabled, + undoText = undoText, + redoText = redoText, + incognitoIconDrawable = incognitoIconDrawable, + ) + + private fun currentClipboardPreviewState( + hasLeadingShortcutEntry: Boolean + ): ClipboardPreviewState = + ClipboardPreviewState( pasteEnabled = isPasteEnabled, clipboardDescriptionShown = isClipboardDescriptionShow, clipboardText = clipboardText, clipboardBitmap = clipboardBitmap, - undoText = undoText, - redoText = redoText, - incognitoIconDrawable = incognitoIconDrawable, + hasLeadingShortcutEntry = hasLeadingShortcutEntry, ) + private fun SuggestionDisplayItem.kind(): SuggestionDisplayItemKind = + when (this) { + is SuggestionDisplayItem.CandidateItem -> + SuggestionDisplayItemKind.CandidateItem + is SuggestionDisplayItem.GemmaActionItem -> + SuggestionDisplayItemKind.GemmaActionItem + is SuggestionDisplayItem.QuickActionsItem -> + SuggestionDisplayItemKind.QuickActionsItem + is SuggestionDisplayItem.ClipboardPreviewItem -> + SuggestionDisplayItemKind.ClipboardPreviewItem + SuggestionDisplayItem.ShortcutEntryItem -> + SuggestionDisplayItemKind.ShortcutEntryItem + is SuggestionDisplayItem.ShortcutItem -> + SuggestionDisplayItemKind.ShortcutItem + is SuggestionDisplayItem.CustomLayoutItem -> + SuggestionDisplayItemKind.CustomLayoutItem + } + inner class SuggestionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val text: MaterialTextView = itemView.findViewById(R.id.suggestion_item_text_view) val yomiText: MaterialTextView = itemView.findViewById(R.id.suggestion_item_yomi_text_view) @@ -561,7 +647,7 @@ class SuggestionAdapter : RecyclerView.Adapter() { val actionText: MaterialTextView = itemView.findViewById(R.id.suggestion_gemma_action_text) } - inner class EmptyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + inner class QuickActionsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val undoIconParent: ConstraintLayout? = itemView.findViewById(R.id.undo_icon_parent) val undoImageView: ImageView? = itemView.findViewById(R.id.imageView) val undoIcon: MaterialTextView? = itemView.findViewById(R.id.undo_icon) @@ -571,13 +657,16 @@ class SuggestionAdapter : RecyclerView.Adapter() { val reconvertIconParent: ConstraintLayout? = itemView.findViewById(R.id.reconvert_icon_parent) val reconvertIcon: MaterialTextView? = itemView.findViewById(R.id.reconvert_icon) val reconvertImageView: ImageView? = itemView.findViewById(R.id.reconvert_image_view) + val incognitoIcon: AppCompatImageButton? = itemView.findViewById(R.id.incognito_icon) + } + + inner class ClipboardPreviewViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val pasteIconParent: ConstraintLayout? = itemView.findViewById(R.id.paste_icon_patent) val pasteIcon: ImageView? = itemView.findViewById(R.id.paste_icon) val clipboardPreviewText: MaterialTextView? = itemView.findViewById(R.id.clipboard_text_preview) val clipboardPreviewTextDescription: MaterialTextView? = itemView.findViewById(R.id.clipboard_preview_text_description) - val incognitoIcon: AppCompatImageButton? = itemView.findViewById(R.id.incognito_icon) } inner class CustomLayoutViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { @@ -588,11 +677,17 @@ class SuggestionAdapter : RecyclerView.Adapter() { val imageView: ImageView = itemView.findViewById(R.id.item_image) } + inner class ShortcutEntryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val imageView: ImageView = itemView.findViewById(R.id.shortcut_entry_image) + } + override fun getItemViewType(position: Int): Int { return when (displayItems[position]) { is SuggestionDisplayItem.CandidateItem -> VIEW_TYPE_SUGGESTION is SuggestionDisplayItem.GemmaActionItem -> VIEW_TYPE_GEMMA_ACTION - is SuggestionDisplayItem.HelperActionsItem -> VIEW_TYPE_EMPTY + is SuggestionDisplayItem.QuickActionsItem -> VIEW_TYPE_EMPTY + is SuggestionDisplayItem.ClipboardPreviewItem -> VIEW_TYPE_CLIPBOARD_PREVIEW + SuggestionDisplayItem.ShortcutEntryItem -> VIEW_TYPE_SHORTCUT_ENTRY is SuggestionDisplayItem.ShortcutItem -> VIEW_TYPE_SHORTCUT is SuggestionDisplayItem.CustomLayoutItem -> VIEW_TYPE_CUSTOM_LAYOUT_PICKER } @@ -607,8 +702,14 @@ class SuggestionAdapter : RecyclerView.Adapter() { return when (viewType) { VIEW_TYPE_EMPTY -> { val emptyView = LayoutInflater.from(parent.context) - .inflate(R.layout.suggestion_empty_layout, parent, false) - EmptyViewHolder(emptyView) + .inflate(R.layout.suggestion_quick_actions_item, parent, false) + QuickActionsViewHolder(emptyView) + } + + VIEW_TYPE_CLIPBOARD_PREVIEW -> { + val clipboardPreviewView = LayoutInflater.from(parent.context) + .inflate(R.layout.suggestion_clipboard_preview_item, parent, false) + ClipboardPreviewViewHolder(clipboardPreviewView) } VIEW_TYPE_CUSTOM_LAYOUT_PICKER -> { @@ -641,16 +742,26 @@ class SuggestionAdapter : RecyclerView.Adapter() { ShortcutViewHolder(itemView) } + VIEW_TYPE_SHORTCUT_ENTRY -> { + val itemView = LayoutInflater.from(parent.context) + .inflate(R.layout.suggestion_shortcut_entry_item, parent, false) + ShortcutEntryViewHolder(itemView) + } + else -> throw IllegalArgumentException("Unknown view type: $viewType") } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val item = displayItems.getOrNull(position) ?: return - when (holder.itemViewType) { - VIEW_TYPE_EMPTY -> onBindEmptyViewHolder( - holder as EmptyViewHolder, - (item as SuggestionDisplayItem.HelperActionsItem).state, + when (getItemViewType(position)) { + VIEW_TYPE_EMPTY -> onBindQuickActionsViewHolder( + holder as QuickActionsViewHolder, + (item as SuggestionDisplayItem.QuickActionsItem).state, + ) + VIEW_TYPE_CLIPBOARD_PREVIEW -> onBindClipboardPreviewViewHolder( + holder as ClipboardPreviewViewHolder, + (item as SuggestionDisplayItem.ClipboardPreviewItem).state, ) VIEW_TYPE_SUGGESTION -> onBindSuggestionViewHolder( holder as SuggestionViewHolder, @@ -666,6 +777,10 @@ class SuggestionAdapter : RecyclerView.Adapter() { item as SuggestionDisplayItem.ShortcutItem, ) + VIEW_TYPE_SHORTCUT_ENTRY -> onBindShortcutEntryViewHolder( + holder as ShortcutEntryViewHolder, + ) + VIEW_TYPE_CUSTOM_LAYOUT_PICKER -> onBindCustomLayoutViewHolder( holder as CustomLayoutViewHolder, item as SuggestionDisplayItem.CustomLayoutItem, @@ -673,9 +788,8 @@ class SuggestionAdapter : RecyclerView.Adapter() { } } - private fun onBindEmptyViewHolder(holder: EmptyViewHolder, state: HelperActionsState) { + private fun onBindQuickActionsViewHolder(holder: QuickActionsViewHolder, state: QuickActionsState) { val isDynamicColorEnable = DynamicColors.isDynamicColorAvailable() - Timber.d("SuggestionAdapter onBindEmptyViewHolder: ${state.clipboardText} ${state.pasteEnabled}") holder.apply { incognitoIcon?.apply { if (state.incognitoIconDrawable != null) { @@ -702,25 +816,6 @@ class SuggestionAdapter : RecyclerView.Adapter() { isVisible = state.reconvertEnabled isFocusable = false } - pasteIconParent?.apply { - isEnabled = state.pasteEnabled - visibility = if (state.pasteEnabled) View.VISIBLE else View.GONE - isFocusable = false - } - - pasteIcon?.apply { - if (state.clipboardBitmap != null) { - setImageBitmap(state.clipboardBitmap) - clearColorFilter() - scaleType = ImageView.ScaleType.CENTER_CROP - } else { - setImageResource(com.kazumaproject.core.R.drawable.content_paste_24px) - scaleType = ImageView.ScaleType.CENTER_INSIDE - } - } - - clipboardPreviewText?.text = - if (state.clipboardBitmap == null) state.clipboardText else "" applyEmptyHelperButtonStyle( parent = undoIconParent, @@ -740,13 +835,6 @@ class SuggestionAdapter : RecyclerView.Adapter() { icon = reconvertImageView, isDynamicColorEnable = isDynamicColorEnable, ) - applyEmptyHelperButtonStyle( - parent = pasteIconParent, - text = clipboardPreviewText, - icon = if (state.clipboardBitmap == null) pasteIcon else null, - isDynamicColorEnable = isDynamicColorEnable, - ) - applyEmptyHelperTextColor(clipboardPreviewTextDescription) undoIconParent?.apply { isVisible = state.undoEnabled @@ -779,9 +867,49 @@ class SuggestionAdapter : RecyclerView.Adapter() { false } } + } + } - clipboardPreviewTextDescription?.isVisible = false + private fun onBindClipboardPreviewViewHolder( + holder: ClipboardPreviewViewHolder, + state: ClipboardPreviewState + ) { + val isDynamicColorEnable = DynamicColors.isDynamicColorAvailable() + Timber.d("SuggestionAdapter onBindClipboardPreviewViewHolder: ${state.clipboardText} ${state.pasteEnabled}") + holder.itemView.translationX = if (state.hasLeadingShortcutEntry) { + -holder.itemView.resources.displayMetrics.density * 28f + } else { + 0f + } + holder.apply { + pasteIconParent?.apply { + isEnabled = state.pasteEnabled + visibility = if (state.pasteEnabled) View.VISIBLE else View.GONE + isFocusable = false + } + pasteIcon?.apply { + if (state.clipboardBitmap != null) { + setImageBitmap(state.clipboardBitmap) + clearColorFilter() + scaleType = ImageView.ScaleType.CENTER_CROP + } else { + setImageResource(com.kazumaproject.core.R.drawable.content_paste_24px) + scaleType = ImageView.ScaleType.CENTER_INSIDE + } + } + + clipboardPreviewText?.text = + if (state.clipboardBitmap == null) state.clipboardText else "" + + applyEmptyHelperButtonStyle( + parent = pasteIconParent, + text = clipboardPreviewText, + icon = if (state.clipboardBitmap == null) pasteIcon else null, + isDynamicColorEnable = isDynamicColorEnable, + ) + applyEmptyHelperTextColor(clipboardPreviewTextDescription) + clipboardPreviewTextDescription?.isVisible = false pasteIconParent?.apply { setOnClickListener { onItemHelperIconClickListener?.invoke(HelperIcon.PASTE) @@ -841,13 +969,25 @@ class SuggestionAdapter : RecyclerView.Adapter() { } } - private fun shouldShowIntegratedShortcuts(): Boolean { - return showIntegratedShortcuts && + private fun shouldShowIntegratedShortcutItems(): Boolean { + return showIntegratedShortcutItems && shortcutItems.isNotEmpty() && + candidateSuggestions.isEmpty() && !isShowingCustomLayoutPicker() && !isShowingClipboardPreviewForEmptyState() } + private fun shouldShowIntegratedShortcutEntry(): Boolean { + return showIntegratedShortcutEntry && + shortcutItems.isNotEmpty() && + !isShowingCustomLayoutPicker() + } + + private fun isShowingSelectedTextGemmaActions(): Boolean { + return candidateSuggestions.isNotEmpty() && + candidateSuggestions.all { it.isSelectedTextGemmaActionCandidate() } + } + private fun onBindShortcutViewHolder( holder: ShortcutViewHolder, item: SuggestionDisplayItem.ShortcutItem, @@ -872,6 +1012,23 @@ class SuggestionAdapter : RecyclerView.Adapter() { } } + private fun onBindShortcutEntryViewHolder( + holder: ShortcutEntryViewHolder, + ) { + holder.imageView.apply { + setImageResource(R.drawable.more_horiz_24px) + contentDescription = context.getString(R.string.shortcut_entry_content_description) + shortcutIconColor?.let { color -> + setColorFilter(color, PorterDuff.Mode.SRC_IN) + } ?: clearColorFilter() + } + holder.itemView.contentDescription = + holder.itemView.context.getString(R.string.shortcut_entry_content_description) + holder.itemView.setOnClickListener { + onShortcutEntryClickListener?.invoke(holder.itemView) + } + } + private fun ShortcutType.resolveShortcutIconResId(): Int { return if (this in activeShortcutTypes) { activeIconResId ?: iconResId diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/candidate_view_height_landscape_setting/CandidateHeightLandscapeSettingFragment.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/candidate_view_height_landscape_setting/CandidateHeightLandscapeSettingFragment.kt index ca327a5e..b91fbe77 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/candidate_view_height_landscape_setting/CandidateHeightLandscapeSettingFragment.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/candidate_view_height_landscape_setting/CandidateHeightLandscapeSettingFragment.kt @@ -862,14 +862,14 @@ class CandidateHeightLandscapeSettingFragment : Fragment() { } } suggestionAdapter.setShortcutItems(previewShortcutItems()) - suggestionAdapter.setIntegratedShortcutVisibility(presentation.showIntegratedShortcut) + suggestionAdapter.setIntegratedShortcutVisibility(presentation.showIntegratedShortcutItems) } private data class CandidateHeightPreviewPresentation( val showCandidateTab: Boolean, val showIndependentShortcutToolbar: Boolean, val reserveIndependentShortcutToolbarSpace: Boolean, - val showIntegratedShortcut: Boolean, + val showIntegratedShortcutItems: Boolean, val candidateTabOffsetPx: Int, val independentShortcutToolbarHeightPx: Int ) @@ -913,7 +913,7 @@ class CandidateHeightLandscapeSettingFragment : Fragment() { showIndependentShortcutToolbar = presentation.showIndependentShortcutToolbar, reserveIndependentShortcutToolbarSpace = presentation.reserveIndependentShortcutToolbarSpace, - showIntegratedShortcut = presentation.showIntegratedShortcut, + showIntegratedShortcutItems = presentation.showIntegratedShortcutItems, candidateTabOffsetPx = if (presentation.showCandidateTab) 36.dpToPx() else 0, independentShortcutToolbarHeightPx = independentHeightPx ) diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/candidate_view_height_setting/CandidateViewHeightSettingFragment.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/candidate_view_height_setting/CandidateViewHeightSettingFragment.kt index 8ea4fc58..16f62d4e 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/candidate_view_height_setting/CandidateViewHeightSettingFragment.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/candidate_view_height_setting/CandidateViewHeightSettingFragment.kt @@ -854,14 +854,14 @@ class CandidateViewHeightSettingFragment : Fragment() { } } suggestionAdapter.setShortcutItems(previewShortcutItems()) - suggestionAdapter.setIntegratedShortcutVisibility(presentation.showIntegratedShortcut) + suggestionAdapter.setIntegratedShortcutVisibility(presentation.showIntegratedShortcutItems) } private data class CandidateHeightPreviewPresentation( val showCandidateTab: Boolean, val showIndependentShortcutToolbar: Boolean, val reserveIndependentShortcutToolbarSpace: Boolean, - val showIntegratedShortcut: Boolean, + val showIntegratedShortcutItems: Boolean, val candidateTabOffsetPx: Int, val independentShortcutToolbarHeightPx: Int ) @@ -905,7 +905,7 @@ class CandidateViewHeightSettingFragment : Fragment() { showIndependentShortcutToolbar = presentation.showIndependentShortcutToolbar, reserveIndependentShortcutToolbarSpace = presentation.reserveIndependentShortcutToolbarSpace, - showIntegratedShortcut = presentation.showIntegratedShortcut, + showIntegratedShortcutItems = presentation.showIntegratedShortcutItems, candidateTabOffsetPx = if (presentation.showCandidateTab) 36.dpToPx() else 0, independentShortcutToolbarHeightPx = independentHeightPx ) diff --git a/app/src/main/res/drawable/more_horiz_24px.xml b/app/src/main/res/drawable/more_horiz_24px.xml new file mode 100644 index 00000000..70279656 --- /dev/null +++ b/app/src/main/res/drawable/more_horiz_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/suggestion_clipboard_preview_item.xml b/app/src/main/res/layout/suggestion_clipboard_preview_item.xml new file mode 100644 index 00000000..fde7579a --- /dev/null +++ b/app/src/main/res/layout/suggestion_clipboard_preview_item.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/suggestion_quick_actions_item.xml b/app/src/main/res/layout/suggestion_quick_actions_item.xml new file mode 100644 index 00000000..a991df18 --- /dev/null +++ b/app/src/main/res/layout/suggestion_quick_actions_item.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/suggestion_shortcut_entry_item.xml b/app/src/main/res/layout/suggestion_shortcut_entry_item.xml new file mode 100644 index 00000000..767e76ae --- /dev/null +++ b/app/src/main/res/layout/suggestion_shortcut_entry_item.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index e55f79fd..b160967e 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -563,6 +563,7 @@ 変換候補ビューの上部に「予測」「変換」「英数・カナ」の切り替えタブを表示します。 ショートカットツールバー 絵文字キーボードへの切り替えなどの便利なショートカットアイコンを含むツールバーを表示します + ショートカットを表示 ショートカットを候補欄に統合 入力中でないとき、クリップボードプレビューやAI選択アクションを表示していない場合に、ショートカットアイコンを候補欄に表示します。 ローマ字/英語切り替えキー diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 121e5dc3..2bec65cc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -567,6 +567,7 @@ Display tabs at the top of the candidate view to switch between "Prediction", "Conversion", and "Alphanumeric/Kana". Shortcut Toolbar Show a toolbar with useful shortcuts like switching Emoji keyboard + Show shortcuts Integrate shortcuts into candidate bar When there is no composing text and no clipboard preview or selected-text AI actions are shown, display shortcut icons inside the candidate bar. Romaji/English Switch Key diff --git a/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/CandidateStripPresentationPolicyTest.kt b/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/CandidateStripPresentationPolicyTest.kt index 502dcb39..21108d33 100644 --- a/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/CandidateStripPresentationPolicyTest.kt +++ b/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/CandidateStripPresentationPolicyTest.kt @@ -14,7 +14,8 @@ class CandidateStripPresentationPolicyTest { assertFalse(presentation.showIndependentShortcutToolbar) assertFalse(presentation.reserveIndependentShortcutToolbarSpace) - assertFalse(presentation.showIntegratedShortcut) + assertFalse(presentation.showIntegratedShortcutItems) + assertFalse(presentation.showIntegratedShortcutEntry) } @Test @@ -25,15 +26,17 @@ class CandidateStripPresentationPolicyTest { assertTrue(presentation.showIndependentShortcutToolbar) assertFalse(presentation.reserveIndependentShortcutToolbarSpace) - assertFalse(presentation.showIntegratedShortcut) + assertFalse(presentation.showIntegratedShortcutItems) + assertFalse(presentation.showIntegratedShortcutEntry) } @Test - fun shortcutToolbarVisibleAndIntegrationOnWithEmptyInputShowsIntegratedShortcut() { + fun integratedOnShowsShortcutItemsForNormalEmptyState() { val presentation = CandidateStripPresentationPolicy.resolve(baseState()) assertFalse(presentation.showIndependentShortcutToolbar) - assertTrue(presentation.showIntegratedShortcut) + assertTrue(presentation.showIntegratedShortcutItems) + assertFalse(presentation.showIntegratedShortcutEntry) } @Test @@ -42,7 +45,8 @@ class CandidateStripPresentationPolicyTest { baseState(inputStringEmpty = false) ) - assertFalse(presentation.showIntegratedShortcut) + assertFalse(presentation.showIntegratedShortcutItems) + assertFalse(presentation.showIntegratedShortcutEntry) } @Test @@ -51,40 +55,75 @@ class CandidateStripPresentationPolicyTest { baseState(tailEmpty = false) ) - assertFalse(presentation.showIntegratedShortcut) + assertFalse(presentation.showIntegratedShortcutItems) + assertFalse(presentation.showIntegratedShortcutEntry) } @Test - fun clipboardPreviewDisablesIntegratedShortcut() { + fun integratedOnClipboardPreviewShowsShortcutEntryOnly() { val presentation = CandidateStripPresentationPolicy.resolve( baseState(clipboardPreviewShown = true) ) assertFalse(presentation.showIndependentShortcutToolbar) assertFalse(presentation.reserveIndependentShortcutToolbarSpace) - assertFalse(presentation.showIntegratedShortcut) + assertFalse(presentation.showIntegratedShortcutItems) + assertTrue(presentation.showIntegratedShortcutEntry) } @Test - fun selectedTextGemmaActionsDisableIntegratedShortcut() { + fun integratedOnGemmaActionsShowShortcutEntryOnly() { val presentation = CandidateStripPresentationPolicy.resolve( - baseState(selectedTextGemmaActionsShown = true) + baseState(selectedTextGemmaActionsShown = true, suggestionsEmpty = false) ) assertFalse(presentation.showIndependentShortcutToolbar) assertFalse(presentation.reserveIndependentShortcutToolbarSpace) - assertFalse(presentation.showIntegratedShortcut) + assertFalse(presentation.showIntegratedShortcutItems) + assertTrue(presentation.showIntegratedShortcutEntry) + } + + @Test + fun integratedOffClipboardPreviewDoesNotShowShortcutEntry() { + val presentation = CandidateStripPresentationPolicy.resolve( + baseState( + shortcutToolbarIntegratedInSuggestion = false, + clipboardPreviewShown = true + ) + ) + + assertTrue(presentation.showIndependentShortcutToolbar) + assertFalse(presentation.reserveIndependentShortcutToolbarSpace) + assertFalse(presentation.showIntegratedShortcutItems) + assertFalse(presentation.showIntegratedShortcutEntry) + } + + @Test + fun integratedOffGemmaActionsDoNotShowShortcutEntry() { + val presentation = CandidateStripPresentationPolicy.resolve( + baseState( + shortcutToolbarIntegratedInSuggestion = false, + selectedTextGemmaActionsShown = true, + suggestionsEmpty = false + ) + ) + + assertTrue(presentation.showIndependentShortcutToolbar) + assertFalse(presentation.reserveIndependentShortcutToolbarSpace) + assertFalse(presentation.showIntegratedShortcutItems) + assertFalse(presentation.showIntegratedShortcutEntry) } @Test fun customLayoutPickerDisablesIntegratedShortcut() { val presentation = CandidateStripPresentationPolicy.resolve( - baseState(customLayoutPickerShown = true) + baseState(customLayoutPickerShown = true, clipboardPreviewShown = true) ) assertFalse(presentation.showIndependentShortcutToolbar) assertFalse(presentation.reserveIndependentShortcutToolbarSpace) - assertFalse(presentation.showIntegratedShortcut) + assertFalse(presentation.showIntegratedShortcutItems) + assertFalse(presentation.showIntegratedShortcutEntry) } @Test @@ -95,7 +134,8 @@ class CandidateStripPresentationPolicyTest { assertFalse(presentation.showIndependentShortcutToolbar) assertFalse(presentation.reserveIndependentShortcutToolbarSpace) - assertFalse(presentation.showIntegratedShortcut) + assertFalse(presentation.showIntegratedShortcutItems) + assertFalse(presentation.showIntegratedShortcutEntry) } @Test @@ -104,7 +144,8 @@ class CandidateStripPresentationPolicyTest { baseState(suggestionsEmpty = false) ) - assertFalse(presentation.showIntegratedShortcut) + assertFalse(presentation.showIntegratedShortcutItems) + assertFalse(presentation.showIntegratedShortcutEntry) } @Test @@ -159,6 +200,8 @@ class CandidateStripPresentationPolicyTest { assertFalse(presentation.showIndependentShortcutToolbar) assertTrue(presentation.reserveIndependentShortcutToolbarSpace) + assertFalse(presentation.showIntegratedShortcutItems) + assertFalse(presentation.showIntegratedShortcutEntry) } private fun baseState( diff --git a/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/ShortcutToolbarPresentationPolicyTest.kt b/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/ShortcutToolbarPresentationPolicyTest.kt index 55ea8145..b8ee28c2 100644 --- a/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/ShortcutToolbarPresentationPolicyTest.kt +++ b/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/ShortcutToolbarPresentationPolicyTest.kt @@ -13,7 +13,8 @@ class ShortcutToolbarPresentationPolicyTest { ) assertFalse(presentation.showIndependentToolbar) - assertFalse(presentation.showIntegratedShortcuts) + assertFalse(presentation.showIntegratedShortcutItems) + assertFalse(presentation.showIntegratedShortcutEntry) } @Test @@ -23,15 +24,17 @@ class ShortcutToolbarPresentationPolicyTest { ) assertTrue(presentation.showIndependentToolbar) - assertFalse(presentation.showIntegratedShortcuts) + assertFalse(presentation.showIntegratedShortcutItems) + assertFalse(presentation.showIntegratedShortcutEntry) } @Test - fun shortcutToolbarVisibleAndIntegrationOnWithEmptyInputShowsIntegratedShortcuts() { + fun integratedOnShowsShortcutItemsForNormalEmptyState() { val presentation = ShortcutToolbarPresentationPolicy.resolve(baseState()) assertFalse(presentation.showIndependentToolbar) - assertTrue(presentation.showIntegratedShortcuts) + assertTrue(presentation.showIntegratedShortcutItems) + assertFalse(presentation.showIntegratedShortcutEntry) } @Test @@ -41,7 +44,8 @@ class ShortcutToolbarPresentationPolicyTest { ) assertFalse(presentation.showIndependentToolbar) - assertFalse(presentation.showIntegratedShortcuts) + assertFalse(presentation.showIntegratedShortcutItems) + assertFalse(presentation.showIntegratedShortcutEntry) } @Test @@ -51,27 +55,56 @@ class ShortcutToolbarPresentationPolicyTest { ) assertFalse(presentation.showIndependentToolbar) - assertFalse(presentation.showIntegratedShortcuts) + assertFalse(presentation.showIntegratedShortcutItems) + assertFalse(presentation.showIntegratedShortcutEntry) } @Test - fun clipboardPreviewDisablesIntegratedShortcuts() { + fun integratedOnClipboardPreviewShowsShortcutEntryOnly() { val presentation = ShortcutToolbarPresentationPolicy.resolve( baseState(clipboardPreviewShown = true) ) assertFalse(presentation.showIndependentToolbar) - assertFalse(presentation.showIntegratedShortcuts) + assertFalse(presentation.showIntegratedShortcutItems) + assertTrue(presentation.showIntegratedShortcutEntry) } @Test - fun selectedTextGemmaActionsDisableIntegratedShortcuts() { + fun integratedOnGemmaActionsShowShortcutEntryOnly() { val presentation = ShortcutToolbarPresentationPolicy.resolve( - baseState(selectedTextGemmaActionsShown = true) + baseState(selectedTextGemmaActionsShown = true, suggestionsEmpty = false) ) assertFalse(presentation.showIndependentToolbar) - assertFalse(presentation.showIntegratedShortcuts) + assertFalse(presentation.showIntegratedShortcutItems) + assertTrue(presentation.showIntegratedShortcutEntry) + } + + @Test + fun integratedOffClipboardPreviewDoesNotShowShortcutEntry() { + val presentation = ShortcutToolbarPresentationPolicy.resolve( + baseState(integratedInSuggestion = false, clipboardPreviewShown = true) + ) + + assertTrue(presentation.showIndependentToolbar) + assertFalse(presentation.showIntegratedShortcutItems) + assertFalse(presentation.showIntegratedShortcutEntry) + } + + @Test + fun integratedOffGemmaActionsDoNotShowShortcutEntry() { + val presentation = ShortcutToolbarPresentationPolicy.resolve( + baseState( + integratedInSuggestion = false, + selectedTextGemmaActionsShown = true, + suggestionsEmpty = false + ) + ) + + assertTrue(presentation.showIndependentToolbar) + assertFalse(presentation.showIntegratedShortcutItems) + assertFalse(presentation.showIntegratedShortcutEntry) } @Test @@ -81,17 +114,30 @@ class ShortcutToolbarPresentationPolicyTest { ) assertFalse(presentation.showIndependentToolbar) - assertFalse(presentation.showIntegratedShortcuts) + assertFalse(presentation.showIntegratedShortcutItems) + assertFalse(presentation.showIntegratedShortcutEntry) } @Test fun customLayoutPickerDisablesIntegratedShortcuts() { val presentation = ShortcutToolbarPresentationPolicy.resolve( - baseState(customLayoutPickerShown = true) + baseState(customLayoutPickerShown = true, clipboardPreviewShown = true) + ) + + assertFalse(presentation.showIndependentToolbar) + assertFalse(presentation.showIntegratedShortcutItems) + assertFalse(presentation.showIntegratedShortcutEntry) + } + + @Test + fun symbolKeyboardDoesNotShowShortcutEntry() { + val presentation = ShortcutToolbarPresentationPolicy.resolve( + baseState(symbolKeyboardShown = true, clipboardPreviewShown = true) ) assertFalse(presentation.showIndependentToolbar) - assertFalse(presentation.showIntegratedShortcuts) + assertFalse(presentation.showIntegratedShortcutItems) + assertFalse(presentation.showIntegratedShortcutEntry) } private fun baseState( @@ -102,7 +148,8 @@ class ShortcutToolbarPresentationPolicyTest { clipboardPreviewShown: Boolean = false, selectedTextGemmaActionsShown: Boolean = false, suggestionsEmpty: Boolean = true, - customLayoutPickerShown: Boolean = false + customLayoutPickerShown: Boolean = false, + symbolKeyboardShown: Boolean = false ): ShortcutToolbarPresentationState { return ShortcutToolbarPresentationState( shortcutToolbarVisible = shortcutToolbarVisible, @@ -112,7 +159,8 @@ class ShortcutToolbarPresentationPolicyTest { clipboardPreviewShown = clipboardPreviewShown, selectedTextGemmaActionsShown = selectedTextGemmaActionsShown, suggestionsEmpty = suggestionsEmpty, - customLayoutPickerShown = customLayoutPickerShown + customLayoutPickerShown = customLayoutPickerShown, + symbolKeyboardShown = symbolKeyboardShown ) } } diff --git a/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapterDisplayItemTest.kt b/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapterDisplayItemTest.kt new file mode 100644 index 00000000..53b3f5ac --- /dev/null +++ b/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapterDisplayItemTest.kt @@ -0,0 +1,177 @@ +package com.kazumaproject.markdownhelperkeyboard.ime_service.adapters + +import com.kazumaproject.core.domain.state.TenKeyQWERTYMode +import com.kazumaproject.markdownhelperkeyboard.converter.candidate.Candidate +import com.kazumaproject.markdownhelperkeyboard.custom_keyboard.data.CustomKeyboardLayout +import com.kazumaproject.markdownhelperkeyboard.gemma.GemmaTranslationManager +import com.kazumaproject.markdownhelperkeyboard.short_cut.ShortcutType +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [35]) +class SuggestionAdapterDisplayItemTest { + + @Test + fun integratedOffShowsQuickActionsLeftAligned() { + val adapter = SuggestionAdapter() + adapter.setUndoEnabled(true) + adapter.setShortcutItems(shortcuts()) + adapter.setIntegratedShortcutItemsVisibility(false) + + assertEquals( + listOf(SuggestionAdapter.SuggestionDisplayItemKind.QuickActionsItem), + adapter.buildDisplayItemKindsForTesting() + ) + adapter.release() + } + + @Test + fun integratedOnShowsQuickActionsAndShortcutItemsInSameLane() { + val adapter = SuggestionAdapter() + adapter.setUndoEnabled(true) + adapter.setShortcutItems(shortcuts()) + adapter.setIntegratedShortcutItemsVisibility(true) + + assertEquals( + listOf( + SuggestionAdapter.SuggestionDisplayItemKind.QuickActionsItem, + SuggestionAdapter.SuggestionDisplayItemKind.ShortcutItem, + SuggestionAdapter.SuggestionDisplayItemKind.ShortcutItem, + ), + adapter.buildDisplayItemKindsForTesting() + ) + adapter.release() + } + + @Test + fun integratedOffClipboardPreviewDoesNotShowShortcutEntry() { + val adapter = SuggestionAdapter() + adapter.setShortcutItems(shortcuts()) + adapter.setIntegratedShortcutEntryVisibility(false) + adapter.setClipboardPreview("clip") + + assertEquals( + listOf(SuggestionAdapter.SuggestionDisplayItemKind.ClipboardPreviewItem), + adapter.buildDisplayItemKindsForTesting() + ) + adapter.release() + } + + @Test + fun integratedOnClipboardPreviewShowsShortcutEntry() { + val adapter = SuggestionAdapter() + adapter.setShortcutItems(shortcuts()) + adapter.setIntegratedShortcutEntryVisibility(true) + adapter.setClipboardPreview("clip") + + assertEquals( + listOf( + SuggestionAdapter.SuggestionDisplayItemKind.ShortcutEntryItem, + SuggestionAdapter.SuggestionDisplayItemKind.ClipboardPreviewItem, + ), + adapter.buildDisplayItemKindsForTesting() + ) + adapter.release() + } + + @Test + fun integratedOffGemmaActionsDoNotShowShortcutEntry() { + val adapter = SuggestionAdapter() + adapter.setShortcutItems(shortcuts()) + adapter.setIntegratedShortcutEntryVisibility(false) + adapter.suggestions = gemmaActions() + + assertEquals( + listOf( + SuggestionAdapter.SuggestionDisplayItemKind.GemmaActionItem, + SuggestionAdapter.SuggestionDisplayItemKind.GemmaActionItem, + ), + adapter.buildDisplayItemKindsForTesting() + ) + adapter.release() + } + + @Test + fun integratedOnGemmaActionsShowShortcutEntry() { + val adapter = SuggestionAdapter() + adapter.setShortcutItems(shortcuts()) + adapter.setIntegratedShortcutEntryVisibility(true) + adapter.suggestions = gemmaActions() + + assertEquals( + listOf( + SuggestionAdapter.SuggestionDisplayItemKind.ShortcutEntryItem, + SuggestionAdapter.SuggestionDisplayItemKind.GemmaActionItem, + SuggestionAdapter.SuggestionDisplayItemKind.GemmaActionItem, + ), + adapter.buildDisplayItemKindsForTesting() + ) + adapter.release() + } + + @Test + fun normalCandidatesDoNotShowShortcutEntry() { + val adapter = SuggestionAdapter() + adapter.setShortcutItems(shortcuts()) + adapter.setIntegratedShortcutEntryVisibility(true) + adapter.suggestions = listOf(candidate("候補1"), candidate("候補2")) + + assertEquals( + listOf( + SuggestionAdapter.SuggestionDisplayItemKind.CandidateItem, + SuggestionAdapter.SuggestionDisplayItemKind.CandidateItem, + ), + adapter.buildDisplayItemKindsForTesting() + ) + adapter.release() + } + + @Test + fun customLayoutPickerDoesNotShowShortcutEntry() { + val adapter = SuggestionAdapter() + adapter.setShortcutItems(shortcuts()) + adapter.setIntegratedShortcutEntryVisibility(true) + adapter.updateState( + TenKeyQWERTYMode.Custom, + listOf(CustomKeyboardLayout(name = "Custom", columnCount = 5, rowCount = 4)) + ) + + assertEquals( + listOf(SuggestionAdapter.SuggestionDisplayItemKind.CustomLayoutItem), + adapter.buildDisplayItemKindsForTesting() + ) + adapter.release() + } + + private fun shortcuts(): List = + listOf(ShortcutType.SETTINGS, ShortcutType.EMOJI) + + private fun gemmaActions(): List = + listOf( + candidate( + string = "Translate", + type = GemmaTranslationManager.SELECTION_TRANSLATE_ACTION_CANDIDATE_TYPE.toByte() + ), + candidate( + string = "Prompt", + type = GemmaTranslationManager.SELECTION_PROMPT_ACTION_CANDIDATE_TYPE.toByte() + ) + ) + + private fun candidate( + string: String, + type: Byte = 1.toByte() + ): Candidate { + return Candidate( + string = string, + type = type, + length = string.length.toUByte(), + score = 0, + yomi = string + ) + } +} diff --git a/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapterShortcutEntryClickTest.kt b/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapterShortcutEntryClickTest.kt new file mode 100644 index 00000000..826359e3 --- /dev/null +++ b/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapterShortcutEntryClickTest.kt @@ -0,0 +1,53 @@ +package com.kazumaproject.markdownhelperkeyboard.ime_service.adapters + +import android.content.Context +import android.os.Looper +import android.widget.FrameLayout +import androidx.test.core.app.ApplicationProvider +import com.kazumaproject.markdownhelperkeyboard.short_cut.ShortcutType +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.Shadows.shadowOf + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [35]) +class SuggestionAdapterShortcutEntryClickTest { + + @Test + fun shortcutEntryClickNotifiesListener() { + val adapter = SuggestionAdapter() + adapter.setShortcutItems(listOf(ShortcutType.SETTINGS)) + adapter.setIntegratedShortcutEntryVisibility(true) + adapter.setClipboardPreview("clip") + drainMainUntilItemCount(adapter, expectedItemCount = 2) + + var clicked = false + adapter.setOnShortcutEntryClickListener { + clicked = true + } + + val context = ApplicationProvider.getApplicationContext() + val parent = FrameLayout(context) + val holder = adapter.onCreateViewHolder( + parent, + SuggestionAdapter.VIEW_TYPE_SHORTCUT_ENTRY + ) + adapter.onBindViewHolder(holder, 0) + + holder.itemView.performClick() + + assertTrue(clicked) + adapter.release() + } + + private fun drainMainUntilItemCount(adapter: SuggestionAdapter, expectedItemCount: Int) { + repeat(20) { + shadowOf(Looper.getMainLooper()).idle() + if (adapter.itemCount >= expectedItemCount) return + Thread.sleep(10) + } + } +} From dfddafebed3667bf66846488f69208390bc01d86 Mon Sep 17 00:00:00 2001 From: KazumaProject <59742125+KazumaProject@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:48:35 -0400 Subject: [PATCH 2/7] fix two fingers key inout --- .../custom_keyboard/view/FlickKeyboardView.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/custom_keyboard/src/main/java/com/kazumaproject/custom_keyboard/view/FlickKeyboardView.kt b/custom_keyboard/src/main/java/com/kazumaproject/custom_keyboard/view/FlickKeyboardView.kt index d83bdf53..a1bf8084 100644 --- a/custom_keyboard/src/main/java/com/kazumaproject/custom_keyboard/view/FlickKeyboardView.kt +++ b/custom_keyboard/src/main/java/com/kazumaproject/custom_keyboard/view/FlickKeyboardView.kt @@ -2320,20 +2320,22 @@ class FlickKeyboardView @JvmOverloads constructor( val x = event.getX(existingPointerIndex) val y = event.getY(existingPointerIndex) - val cancelEvent = MotionEvent.obtain( + // A second finger starts a new key gesture; the first finger should be + // committed at its current position, not canceled. + val upEvent = MotionEvent.obtain( downTime, event.eventTime, - MotionEvent.ACTION_CANCEL, + MotionEvent.ACTION_UP, x, y, event.metaState ) - cancelEvent.offsetLocation( + upEvent.offsetLocation( -target.left.toFloat(), -target.top.toFloat() ) - target.dispatchTouchEvent(cancelEvent) - cancelEvent.recycle() + target.dispatchTouchEvent(upEvent) + upEvent.recycle() } } From 4e9e3970b7f895968c198a16a78f9c3ef91eb975 Mon Sep 17 00:00:00 2001 From: KazumaProject <59742125+KazumaProject@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:49:10 -0400 Subject: [PATCH 3/7] update versions --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index dc0dfe93..4c3b5219 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,8 +31,8 @@ android { applicationId "com.kazumaproject.markdownhelperkeyboard" minSdk 24 targetSdk 36 - versionCode 779 - versionName "1.7.86" + versionCode 780 + versionName "1.7.87" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } From b2c593e07051ece64135df3b3e6b4f0fc83e9218 Mon Sep 17 00:00:00 2001 From: KazumaProject <59742125+KazumaProject@users.noreply.github.com> Date: Tue, 16 Jun 2026 19:14:03 -0400 Subject: [PATCH 4/7] fix switch toolbar icons to clipboard preview --- .../ime_service/IMEService.kt | 147 ++---------------- .../ime_service/adapters/SuggestionAdapter.kt | 56 ++++++- .../SuggestionAdapterDisplayItemTest.kt | 59 +++++++ 3 files changed, 124 insertions(+), 138 deletions(-) diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/IMEService.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/IMEService.kt index eefd6f21..98ad3379 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/IMEService.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/IMEService.kt @@ -10,7 +10,6 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Color import android.graphics.Matrix -import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable import android.hardware.input.InputManager @@ -498,10 +497,6 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, private var zenzEngine: ZenzEngine? = null private var shortcutAdapter: ShortcutAdapter? = null - private var shortcutEntryPopupWindow: PopupWindow? = null - private var shortcutEntryPopupAdapter: ShortcutAdapter? = null - private var shortcutEntryPopupAnchorView: View? = null - private var shortcutEntryPopupDetachListener: View.OnAttachStateChangeListener? = null private var romajiConverter: RomajiKanaConverter? = null @@ -654,7 +649,7 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, private fun setSuggestionAdapterSuggestionsOnMain(candidates: List) { runOnMainThread { measureDebugSection("IMEService.setSuggestionAdapterSuggestionsOnMain") { - dismissShortcutEntryPopup() + collapseShortcutEntryExpansion() suggestionAdapter?.suggestions = candidates } } @@ -666,7 +661,7 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, ) { runOnMainThread { measureDebugSection("IMEService.setSuggestionAdaptersOnMain") { - dismissShortcutEntryPopup() + collapseShortcutEntryExpansion() suggestionAdapter?.suggestions = candidates suggestionAdapterFull?.suggestions = fullCandidates } @@ -680,7 +675,7 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, ) = measureDebugStage("IMEService.updateSuggestionAdaptersOnMain") { withContext(Dispatchers.Main.immediate) { if (!shouldApplyCandidateResult(insertString)) return@withContext - dismissShortcutEntryPopup() + collapseShortcutEntryExpansion() suggestionAdapter?.suggestions = candidates suggestionAdapterFull?.suggestions = fullCandidates } @@ -3293,7 +3288,7 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, override fun onStartInputView(editorInfo: EditorInfo?, restarting: Boolean) { super.onStartInputView(editorInfo, restarting) isInputViewActive = true - dismissShortcutEntryPopup() + collapseShortcutEntryExpansion() shortcutInputBehaviorOverride = null keyboardSelectionPopupWindow?.dismiss() addUserDictionaryPopup?.dismiss() @@ -3663,7 +3658,7 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, releaseKeyboardBackgroundVideoPlayer() releaseFloatingKeyboardBackgroundVideoPlayer() stopVoiceInput() - dismissShortcutEntryPopup() + collapseShortcutEntryExpansion() floatingCandidateWindow?.dismiss() floatingDockWindow?.dismiss() floatingModeSwitchWindow?.dismiss() @@ -3683,7 +3678,7 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, Timber.d("onUpdate onDestroy") stopAllOngoingKeyLongPresses() disableKeyboardLayoutEditMode(updateSurface = false) - dismissShortcutEntryPopup() + collapseShortcutEntryExpansion() isInputViewActive = false releaseSuminagashiInkEffects() releaseKeyboardBackgroundVideoPlayer() @@ -4049,7 +4044,7 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) - dismissShortcutEntryPopup() + collapseShortcutEntryExpansion() when (newConfig.orientation) { Configuration.ORIENTATION_PORTRAIT -> { finishComposingText() @@ -11922,7 +11917,7 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, */ private fun updateClipboardPreview() { Timber.d("SuggestionAdapter Clipboard: updateClipboardPreview") - dismissShortcutEntryPopup() + collapseShortcutEntryExpansion() suggestionAdapter?.apply { when (val item = clipboardUtil.getPrimaryClipContent()) { is ClipboardItem.Image -> { @@ -16810,8 +16805,8 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, adapter.setOnShortcutItemClickListener { type -> handleShortcutAction(type, mainView) } - adapter.setOnShortcutEntryClickListener { anchorView -> - showShortcutEntryPopup(anchorView, mainView) + adapter.setOnShortcutEntryClickListener { + adapter.toggleIntegratedShortcutEntryExpansion() } } suggestionAdapterFull?.let { adapter -> @@ -17688,7 +17683,6 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, candidatesShown = candidatesShown, resetCandidateTabSelection = resetCandidateTabSelection ) - dismissShortcutEntryPopup() suggestionAdapter?.setIntegratedShortcutItemsVisibility(presentation.showIntegratedShortcutItems) suggestionAdapter?.setIntegratedShortcutEntryVisibility(presentation.showIntegratedShortcutEntry) applyCandidateTabSuggestionOffset(mainView, presentation.showCandidateTab) @@ -17706,125 +17700,8 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, } } - private fun showShortcutEntryPopup(anchorView: View, mainView: MainLayoutBinding) { - dismissShortcutEntryPopup() - val shortcutItems = shortcutAdapter?.currentList.orEmpty() - if (shortcutItems.isEmpty()) return - if (!canShowPopupWindow(anchorView)) return - - val toolbarHeightPx = shortcutToolbarHeightPx() - val popupVerticalPaddingPx = applicationContext.dpToPx(4) - val popupHorizontalPaddingPx = applicationContext.dpToPx(8) - val popupWidth = mainView.root.width - .takeIf { it > 0 } - ?: resources.displayMetrics.widthPixels - - val popupAdapter = ShortcutAdapter().apply { - setShortcutToolbarSize( - toolbarHeightPx = toolbarHeightPx, - iconSizePx = shortcutToolbarIconSizePx() - ) - setActiveShortcutTypes( - resolveShortcutActiveTypes( - keyboardLayoutEditActive = keyboardLayoutEditState.value is KeyboardLayoutEditState.Enabled, - keyboardFloatingActive = isKeyboardFloatingMode == true, - inputBehavior = currentInputBehavior, - ) - ) - if (keyboardThemeMode == "custom") { - setIconColor(customThemeShortcutIconColor ?: Color.BLACK) - } - onItemClicked = { type -> - dismissShortcutEntryPopup() - handleShortcutAction(type, mainView) - } - submitList(shortcutItems) - } - val recyclerView = RecyclerView(this).apply { - layoutManager = - LinearLayoutManager(this@IMEService, LinearLayoutManager.HORIZONTAL, false) - adapter = popupAdapter - itemAnimator = null - isFocusable = false - overScrollMode = View.OVER_SCROLL_NEVER - } - val contentView = FrameLayout(this).apply { - setPadding( - popupHorizontalPaddingPx, - popupVerticalPaddingPx, - popupHorizontalPaddingPx, - popupVerticalPaddingPx - ) - background = GradientDrawable().apply { - cornerRadius = resources.displayMetrics.density * 12f - setColor( - customThemeSpecialKeyColor ?: ContextCompat.getColor( - this@IMEService, - com.kazumaproject.core.R.color.keyboard_bg - ) - ) - } - addView( - recyclerView, - FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - toolbarHeightPx - ) - ) - } - val popupHeight = toolbarHeightPx + popupVerticalPaddingPx * 2 - val popupWindow = PopupWindow( - contentView, - popupWidth, - popupHeight, - false - ).apply { - isOutsideTouchable = true - isClippingEnabled = true - inputMethodMode = PopupWindow.INPUT_METHOD_NOT_NEEDED - setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - } - val detachListener = object : View.OnAttachStateChangeListener { - override fun onViewAttachedToWindow(v: View) = Unit - - override fun onViewDetachedFromWindow(v: View) { - dismissShortcutEntryPopup() - } - } - shortcutEntryPopupWindow = popupWindow - shortcutEntryPopupAdapter = popupAdapter - shortcutEntryPopupAnchorView = anchorView - shortcutEntryPopupDetachListener = detachListener - anchorView.addOnAttachStateChangeListener(detachListener) - popupWindow.setOnDismissListener { - clearShortcutEntryPopupReferences(popupWindow) - } - runCatching { - popupWindow.showAsDropDown(anchorView, 0, 0) - }.onFailure { throwable -> - Timber.w(throwable, "Shortcut entry popup show failed") - dismissShortcutEntryPopup() - } - } - - private fun dismissShortcutEntryPopup() { - val popupWindow = shortcutEntryPopupWindow - clearShortcutEntryPopupReferences(popupWindow) - if (popupWindow?.isShowing == true) { - popupWindow.dismiss() - } - } - - private fun clearShortcutEntryPopupReferences(popupWindow: PopupWindow?) { - if (popupWindow != null && shortcutEntryPopupWindow !== popupWindow) return - shortcutEntryPopupDetachListener?.let { listener -> - shortcutEntryPopupAnchorView?.removeOnAttachStateChangeListener(listener) - } - shortcutEntryPopupAdapter?.onItemClicked = null - shortcutEntryPopupWindow = null - shortcutEntryPopupAdapter = null - shortcutEntryPopupAnchorView = null - shortcutEntryPopupDetachListener = null + private fun collapseShortcutEntryExpansion() { + suggestionAdapter?.setIntegratedShortcutEntryExpanded(false) } private fun handleShortcutAction(type: ShortcutType, mainView: MainLayoutBinding) { diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapter.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapter.kt index efa29405..f0ea8d1d 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapter.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapter.kt @@ -227,6 +227,7 @@ class SuggestionAdapter : RecyclerView.Adapter() { private var shortcutItems: List = emptyList() private var showIntegratedShortcutItems: Boolean = false private var showIntegratedShortcutEntry: Boolean = false + private var integratedShortcutEntryExpanded: Boolean = false private var shortcutIconColor: Int? = null private var activeShortcutTypes: Set = emptySet() @@ -360,23 +361,50 @@ class SuggestionAdapter : RecyclerView.Adapter() { } fun setShortcutItems(items: List) { - if (shortcutItems == items) return + val shouldCollapseExpandedEntry = items.isEmpty() && integratedShortcutEntryExpanded + if (shortcutItems == items && !shouldCollapseExpandedEntry) return shortcutItems = items + if (items.isEmpty()) { + integratedShortcutEntryExpanded = false + } rebuildDisplayItems() } fun setIntegratedShortcutItemsVisibility(visible: Boolean) { - if (showIntegratedShortcutItems == visible) return + val shouldCollapseExpandedEntry = visible && integratedShortcutEntryExpanded + if (showIntegratedShortcutItems == visible && !shouldCollapseExpandedEntry) return showIntegratedShortcutItems = visible + if (visible) { + integratedShortcutEntryExpanded = false + } rebuildDisplayItems() } fun setIntegratedShortcutEntryVisibility(visible: Boolean) { - if (showIntegratedShortcutEntry == visible) return + val shouldCollapseExpandedEntry = !visible && integratedShortcutEntryExpanded + if (showIntegratedShortcutEntry == visible && !shouldCollapseExpandedEntry) return showIntegratedShortcutEntry = visible + if (!visible) { + integratedShortcutEntryExpanded = false + } rebuildDisplayItems() } + fun setIntegratedShortcutEntryExpanded(expanded: Boolean) { + val normalizedExpanded = + expanded && + showIntegratedShortcutEntry && + shortcutItems.isNotEmpty() && + hasSwitchableShortcutEntryContent() + if (integratedShortcutEntryExpanded == normalizedExpanded) return + integratedShortcutEntryExpanded = normalizedExpanded + rebuildDisplayItems() + } + + fun toggleIntegratedShortcutEntryExpansion() { + setIntegratedShortcutEntryExpanded(!integratedShortcutEntryExpanded) + } + fun setShortcutIconColor(color: Int) { if (shortcutIconColor == color) return shortcutIconColor = color @@ -547,6 +575,10 @@ class SuggestionAdapter : RecyclerView.Adapter() { } private fun buildDisplayItems(): List { + if (shouldShowExpandedShortcutEntryItems()) { + return buildExpandedShortcutEntryItems() + } + if (candidateSuggestions.isNotEmpty()) { return buildList { if (isShowingSelectedTextGemmaActions() && shouldShowIntegratedShortcutEntry()) { @@ -593,6 +625,14 @@ class SuggestionAdapter : RecyclerView.Adapter() { } } + private fun buildExpandedShortcutEntryItems(): List = + buildList { + add(SuggestionDisplayItem.ShortcutEntryItem) + shortcutItems.forEach { shortcutType -> + add(SuggestionDisplayItem.ShortcutItem(shortcutType)) + } + } + internal fun buildDisplayItemKindsForTesting(): List { return buildDisplayItems().map { it.kind() } } @@ -983,6 +1023,16 @@ class SuggestionAdapter : RecyclerView.Adapter() { !isShowingCustomLayoutPicker() } + private fun shouldShowExpandedShortcutEntryItems(): Boolean { + if (!integratedShortcutEntryExpanded || !shouldShowIntegratedShortcutEntry()) return false + return hasSwitchableShortcutEntryContent() + } + + private fun hasSwitchableShortcutEntryContent(): Boolean { + return isShowingSelectedTextGemmaActions() || + currentClipboardPreviewState(hasLeadingShortcutEntry = false).hasClipboardPreview + } + private fun isShowingSelectedTextGemmaActions(): Boolean { return candidateSuggestions.isNotEmpty() && candidateSuggestions.all { it.isSelectedTextGemmaActionCandidate() } diff --git a/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapterDisplayItemTest.kt b/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapterDisplayItemTest.kt index 53b3f5ac..60881f06 100644 --- a/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapterDisplayItemTest.kt +++ b/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapterDisplayItemTest.kt @@ -78,6 +78,35 @@ class SuggestionAdapterDisplayItemTest { adapter.release() } + @Test + fun expandedShortcutEntryReplacesClipboardPreviewWithShortcutItems() { + val adapter = SuggestionAdapter() + adapter.setShortcutItems(shortcuts()) + adapter.setIntegratedShortcutEntryVisibility(true) + adapter.setClipboardPreview("clip") + adapter.setIntegratedShortcutEntryExpanded(true) + + assertEquals( + listOf( + SuggestionAdapter.SuggestionDisplayItemKind.ShortcutEntryItem, + SuggestionAdapter.SuggestionDisplayItemKind.ShortcutItem, + SuggestionAdapter.SuggestionDisplayItemKind.ShortcutItem, + ), + adapter.buildDisplayItemKindsForTesting() + ) + + adapter.setIntegratedShortcutEntryExpanded(false) + + assertEquals( + listOf( + SuggestionAdapter.SuggestionDisplayItemKind.ShortcutEntryItem, + SuggestionAdapter.SuggestionDisplayItemKind.ClipboardPreviewItem, + ), + adapter.buildDisplayItemKindsForTesting() + ) + adapter.release() + } + @Test fun integratedOffGemmaActionsDoNotShowShortcutEntry() { val adapter = SuggestionAdapter() @@ -113,6 +142,36 @@ class SuggestionAdapterDisplayItemTest { adapter.release() } + @Test + fun expandedShortcutEntryReplacesGemmaActionsWithShortcutItems() { + val adapter = SuggestionAdapter() + adapter.setShortcutItems(shortcuts()) + adapter.setIntegratedShortcutEntryVisibility(true) + adapter.suggestions = gemmaActions() + adapter.setIntegratedShortcutEntryExpanded(true) + + assertEquals( + listOf( + SuggestionAdapter.SuggestionDisplayItemKind.ShortcutEntryItem, + SuggestionAdapter.SuggestionDisplayItemKind.ShortcutItem, + SuggestionAdapter.SuggestionDisplayItemKind.ShortcutItem, + ), + adapter.buildDisplayItemKindsForTesting() + ) + + adapter.setIntegratedShortcutEntryExpanded(false) + + assertEquals( + listOf( + SuggestionAdapter.SuggestionDisplayItemKind.ShortcutEntryItem, + SuggestionAdapter.SuggestionDisplayItemKind.GemmaActionItem, + SuggestionAdapter.SuggestionDisplayItemKind.GemmaActionItem, + ), + adapter.buildDisplayItemKindsForTesting() + ) + adapter.release() + } + @Test fun normalCandidatesDoNotShowShortcutEntry() { val adapter = SuggestionAdapter() From ed43c422a4148c769f85e648d183ec1fcdfd866c Mon Sep 17 00:00:00 2001 From: KazumaProject <59742125+KazumaProject@users.noreply.github.com> Date: Tue, 16 Jun 2026 19:35:50 -0400 Subject: [PATCH 5/7] settings for secret mode --- .../ime_service/IMEService.kt | 100 ++++++++++++------ .../ime_service/ImePreferencesSnapshot.kt | 6 ++ .../setting_activity/AppPreference.kt | 22 ++++ .../ui/setting/SettingDestination.kt | 2 + app/src/main/res/values-ja/strings.xml | 6 ++ app/src/main/res/values/strings.xml | 6 ++ app/src/main/res/xml/pref_dictionary.xml | 16 +++ .../IncognitoModeSettingsEntryPointTest.kt | 93 ++++++++++++++++ 8 files changed, 217 insertions(+), 34 deletions(-) create mode 100644 app/src/test/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/setting/IncognitoModeSettingsEntryPointTest.kt diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/IMEService.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/IMEService.kt index 98ad3379..3aaee58a 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/IMEService.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/IMEService.kt @@ -1038,6 +1038,8 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, private val _ngPattern = MutableStateFlow("".toRegex()) private val ngPattern: StateFlow = _ngPattern private var isPrivateMode = false + private var incognitoModeDetectionPreference: Boolean = true + private var showLearnedCandidatesInIncognitoPreference: Boolean = true private val _keyboardFloatingMode = MutableStateFlow(false) private val keyboardFloatingMode = _keyboardFloatingMode.asStateFlow() @@ -1663,6 +1665,9 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, isOmissionSearchEnable = preferences.isOmissionSearchEnable delayTime = preferences.delayTime isLearnDictionaryMode = preferences.isLearnDictionaryMode + incognitoModeDetectionPreference = preferences.incognitoModeDetectionPreference + showLearnedCandidatesInIncognitoPreference = + preferences.showLearnedCandidatesInIncognitoPreference isUserDictionaryEnable = preferences.isUserDictionaryEnable isUserTemplateEnable = preferences.isUserTemplateEnable hankakuPreference = preferences.hankakuPreference @@ -1964,6 +1969,26 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, } } + private fun updateIncognitoModeState(editorInfo: EditorInfo?) { + val detected = incognitoModeDetectionPreference && + editorInfo != null && + (editorInfo.imeOptions and EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING) != 0 + isPrivateMode = detected + suggestionAdapter?.setIncognitoIcon( + if (detected) { + ContextCompat.getDrawable(this, com.kazumaproject.core.R.drawable.incognito) + } else { + null + } + ) + } + + private fun learnedRepositoryForSuggestion(): LearnRepository? { + if (isLearnDictionaryMode != true) return null + if (isPrivateMode && !showLearnedCandidatesInIncognitoPreference) return null + return learnRepository + } + private fun applyDictionaryOverrideRevisionIfNeeded() { val currentRevision = dictionaryOverrideStore.currentRevision if (currentRevision == lastAppliedDictionaryOverrideRevision) return @@ -3627,17 +3652,7 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, } setMainSuggestionColumn(mainView) } - editorInfo?.let { info -> - if ((info.imeOptions and EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING) != 0) { - isPrivateMode = true - suggestionAdapter?.setIncognitoIcon( - ContextCompat.getDrawable(this, com.kazumaproject.core.R.drawable.incognito) - ) - } else { - isPrivateMode = false - suggestionAdapter?.setIncognitoIcon(null) - } - } + updateIncognitoModeState(editorInfo) if (hasPhysicalKeyboard) { floatingDockView.setText("あ") ensurePhysicalKeyboardPopupWindows() @@ -3722,6 +3737,8 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, isOmissionSearchEnable = null delayTime = null isLearnDictionaryMode = null + incognitoModeDetectionPreference = true + showLearnedCandidatesInIncognitoPreference = true isUserDictionaryEnable = null isUserTemplateEnable = null hankakuPreference = null @@ -19498,22 +19515,24 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, } private fun handlePartialOrExcessLength( - insertString: String, candidate: Candidate + insertString: String, candidate: Candidate, currentInputMode: InputMode ) { val candidateLength = candidate.length.toInt() val candidateString = candidate.string if (insertString.length > candidateLength) { stringInTail.set(insertString.substring(candidateLength)) - ioScope.launch { - learnRepository.upsertLearnedData( - LearnEntity( - input = insertString.substring(0, candidateLength), - out = candidateString, - score = candidate.score.toShort(), - leftId = candidate.leftId, - rightId = candidate.rightId + if (currentInputMode == InputMode.ModeJapanese && isLearnDictionaryMode == true && !isPrivateMode) { + ioScope.launch { + learnRepository.upsertLearnedData( + LearnEntity( + input = insertString.substring(0, candidateLength), + out = candidateString, + score = candidate.score.toShort(), + leftId = candidate.leftId, + rightId = candidate.rightId + ) ) - ) + } } } commitAndClearInput(candidateString) @@ -19574,7 +19593,9 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, ) } else { handlePartialOrExcessLength( - insertString = insertString, candidate = candidate + insertString = insertString, + candidate = candidate, + currentInputMode = currentInputMode ) } } @@ -19665,6 +19686,8 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, _tenKeyQWERTYMode.update { TenKeyQWERTYMode.Default } clearZenzLiveSlot("resetAllFlags") setSuggestionAdapterSuggestionsOnMain(emptyList()) + isPrivateMode = false + suggestionAdapter?.setIncognitoIcon(null) stringInTail.set("") suggestionClickNum = 0 currentCustomKeyboardPosition = 0 @@ -20607,12 +20630,15 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, emptyList() } + val suggestionLearnRepository = learnedRepositoryForSuggestion() val resultFromLearnDictionary = - if (enablePredictionSearchLearnDictionaryPreference == true) { + if (enablePredictionSearchLearnDictionaryPreference == true && + suggestionLearnRepository != null + ) { withContext(Dispatchers.IO) { val prefixMatchNumber = (learnPredictionPreference ?: 2) - 1 if (insertString.length <= prefixMatchNumber) return@withContext emptyList() - learnRepository.predictiveSearchByInput( + suggestionLearnRepository.predictiveSearchByInput( prefix = insertString, limit = 4 ).map { Candidate( @@ -20649,7 +20675,7 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, mozcUTNeologd = mozcUTNeologd, mozcUTWeb = mozcUTWeb, userDictionaryRepository = userDictionaryRepository, - learnRepository = if (isLearnDictionaryMode == true) learnRepository else null, + learnRepository = suggestionLearnRepository, isOmissionSearchEnable = isOmissionSearchEnable ?: false, enableTypoCorrectionJapaneseFlick = enableTypoCorrectionJapaneseFlick, enableTypoCorrectionQwertyEnglish = enableTypoCorrectionQwertyEnglish, @@ -20668,7 +20694,7 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, mozcUTNeologd = mozcUTNeologd, mozcUTWeb = mozcUTWeb, userDictionaryRepository = userDictionaryRepository, - learnRepository = if (isLearnDictionaryMode == true) learnRepository else null, + learnRepository = suggestionLearnRepository, isOmissionSearchEnable = isOmissionSearchEnable ?: false, enableTypoCorrectionJapaneseFlick = enableTypoCorrectionJapaneseFlick, enableTypoCorrectionQwertyEnglish = enableTypoCorrectionQwertyEnglish, @@ -20763,13 +20789,16 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, emptyList() } + val suggestionLearnRepository = learnedRepositoryForSuggestion() val resultFromLearnDictionary = - if (enablePredictionSearchLearnDictionaryPreference == true) { + if (enablePredictionSearchLearnDictionaryPreference == true && + suggestionLearnRepository != null + ) { measureDebugStage("IMEService.getSuggestionList.learnDictionary") { withContext(Dispatchers.IO) { val prefixMatchNumber = (learnPredictionPreference ?: 2) - 1 if (insertString.length <= prefixMatchNumber) return@withContext emptyList() - learnRepository.predictiveSearchByInput( + suggestionLearnRepository.predictiveSearchByInput( prefix = insertString, limit = 4 ).map { Candidate( @@ -20810,7 +20839,7 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, mozcUTNeologd = mozcUTNeologd, mozcUTWeb = mozcUTWeb, userDictionaryRepository = userDictionaryRepository, - learnRepository = if (isLearnDictionaryMode == true) learnRepository else null, + learnRepository = suggestionLearnRepository, isOmissionSearchEnable = isOmissionSearchEnable ?: false, enableTypoCorrectionJapaneseFlick = enableTypoCorrectionJapaneseFlick, enableTypoCorrectionQwertyEnglish = enableTypoCorrectionQwertyEnglish, @@ -20832,7 +20861,7 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, mozcUTNeologd = mozcUTNeologd, mozcUTWeb = mozcUTWeb, userDictionaryRepository = userDictionaryRepository, - learnRepository = if (isLearnDictionaryMode == true) learnRepository else null, + learnRepository = suggestionLearnRepository, isOmissionSearchEnable = isOmissionSearchEnable ?: false, enableTypoCorrectionJapaneseFlick = enableTypoCorrectionJapaneseFlick, enableTypoCorrectionQwertyEnglish = enableTypoCorrectionQwertyEnglish, @@ -20952,12 +20981,15 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, emptyList() } + val suggestionLearnRepository = learnedRepositoryForSuggestion() val resultFromLearnDictionary = - if (enablePredictionSearchLearnDictionaryPreference == true) { + if (enablePredictionSearchLearnDictionaryPreference == true && + suggestionLearnRepository != null + ) { withContext(Dispatchers.IO) { val prefixMatchNumber = (learnPredictionPreference ?: 2) - 1 if (insertString.length <= prefixMatchNumber) return@withContext emptyList() - learnRepository.predictiveSearchByInput( + suggestionLearnRepository.predictiveSearchByInput( prefix = insertString, limit = 4 ).map { Candidate( @@ -20986,7 +21018,7 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, mozcUTNeologd = mozcUTNeologd, mozcUTWeb = mozcUTWeb, userDictionaryRepository = userDictionaryRepository, - learnRepository = if (isLearnDictionaryMode == true) learnRepository else null, + learnRepository = suggestionLearnRepository, typoCorrectionOffsetScore = enableTypoCorrectionJapaneseFlickKeyboardOffsetScorePreference ?: 3000, omissionSearchOffsetScore = omissionSearchOffsetScorePreference ?: 1900 @@ -21002,7 +21034,7 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, mozcUTNeologd = mozcUTNeologd, mozcUTWeb = mozcUTWeb, userDictionaryRepository = userDictionaryRepository, - learnRepository = if (isLearnDictionaryMode == true) learnRepository else null, + learnRepository = suggestionLearnRepository, typoCorrectionOffsetScore = enableTypoCorrectionJapaneseFlickKeyboardOffsetScorePreference ?: 3000, omissionSearchOffsetScore = omissionSearchOffsetScorePreference ?: 1900 diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/ImePreferencesSnapshot.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/ImePreferencesSnapshot.kt index 9f3f9ce8..f17bef87 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/ImePreferencesSnapshot.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/ImePreferencesSnapshot.kt @@ -21,6 +21,8 @@ data class ImePreferencesSnapshot( val isOmissionSearchEnable: Boolean, val delayTime: Int, val isLearnDictionaryMode: Boolean, + val incognitoModeDetectionPreference: Boolean, + val showLearnedCandidatesInIncognitoPreference: Boolean, val isUserDictionaryEnable: Boolean, val isUserTemplateEnable: Boolean, val hankakuPreference: Boolean, @@ -265,6 +267,10 @@ data class ImePreferencesSnapshot( isOmissionSearchEnable = appPreference.omission_search_preference ?: false, delayTime = appPreference.time_same_pronounce_typing_preference ?: 1000, isLearnDictionaryMode = appPreference.learn_dictionary_preference ?: true, + incognitoModeDetectionPreference = + appPreference.incognito_mode_detection_preference, + showLearnedCandidatesInIncognitoPreference = + appPreference.show_learned_candidates_in_incognito_preference, isUserDictionaryEnable = appPreference.user_dictionary_preference ?: true, isUserTemplateEnable = appPreference.user_template_preference ?: true, hankakuPreference = appPreference.space_hankaku_preference ?: false, diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/AppPreference.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/AppPreference.kt index 22c34817..54ae1b8a 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/AppPreference.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/AppPreference.kt @@ -62,6 +62,10 @@ object AppPreference { private val KEY_SOUND_VOLUME_PERCENT_PREFERENCE = Pair("key_sound_volume_percent_preference", 0) private val LEARN_DICTIONARY_PREFERENCE = Pair("learn_dictionary_preference", true) + private val INCOGNITO_MODE_DETECTION_PREFERENCE = + Pair("incognito_mode_detection_preference", true) + private val SHOW_LEARNED_CANDIDATES_IN_INCOGNITO_PREFERENCE = + Pair("show_learned_candidates_in_incognito_preference", true) private val USER_DICTIONARY_PREFERENCE = Pair("user_dictionary_preference", true) private val USER_DICTIONARY_PREFIX_PREFERENCE = Pair("user_dictionary_prefix_match_number", 2) private val USER_TEMPLATE_PREFERENCE = Pair("user_template_preference", true) @@ -1364,6 +1368,24 @@ object AppPreference { it.putBoolean(LEARN_DICTIONARY_PREFERENCE.first, value ?: true) } + var incognito_mode_detection_preference: Boolean + get() = preferences.getBoolean( + INCOGNITO_MODE_DETECTION_PREFERENCE.first, + INCOGNITO_MODE_DETECTION_PREFERENCE.second + ) + set(value) = preferences.edit { + it.putBoolean(INCOGNITO_MODE_DETECTION_PREFERENCE.first, value) + } + + var show_learned_candidates_in_incognito_preference: Boolean + get() = preferences.getBoolean( + SHOW_LEARNED_CANDIDATES_IN_INCOGNITO_PREFERENCE.first, + SHOW_LEARNED_CANDIDATES_IN_INCOGNITO_PREFERENCE.second + ) + set(value) = preferences.edit { + it.putBoolean(SHOW_LEARNED_CANDIDATES_IN_INCOGNITO_PREFERENCE.first, value) + } + var user_dictionary_preference: Boolean? get() = preferences.getBoolean( USER_DICTIONARY_PREFERENCE.first, USER_DICTIONARY_PREFERENCE.second diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/setting/SettingDestination.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/setting/SettingDestination.kt index 89266d60..fe98d542 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/setting/SettingDestination.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/setting/SettingDestination.kt @@ -448,6 +448,8 @@ object SettingDestinations { "candidate_order_override_preference", "ng_word_preference", "learn_dictionary_preference", + "incognito_mode_detection_preference", + "show_learned_candidates_in_incognito_preference", "user_dictionary_preference", "user_template_preference", "enable_ai_conversion_zenz_preference", diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index b160967e..d03aa65c 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -266,6 +266,12 @@ 学習辞書 学習辞書を有効化\n学習した変換候補を表示します 学習辞書を無効化\n学習した変換候補を表示しません + シークレットモードの検知 + 入力先が個人用学習の無効化を要求したとき、アイコンを表示し、新しい学習を保存しません。 + 入力先からの個人用学習無効化要求を無視します。 + シークレットモード中の学習済み候補 + シークレットモードを検知している間も、保存済みの学習候補を使用します。 + シークレットモードを検知している間は、保存済みの学習候補を使用しません。 ユーザー辞書の予測変換を有効化\n登録した単語を予測変換として変換候補を表示します ユーザー辞書の予測変換を無効化 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2bec65cc..23855cdd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -270,6 +270,12 @@ Learning Dictionary Enable learning dictionary\nShows learned conversion candidates Disable learning dictionary\nDoes not show learned conversion candidates + Detect incognito mode + When an app requests no personalized learning, show the incognito icon and do not save new learning data. + Ignore no-personalized-learning requests from the input field. + Show learned candidates in incognito mode + Use already learned candidates even when incognito mode is detected. + Do not use already learned candidates while incognito mode is detected. Enable predictive conversion for user dictionary\nShows registered words as predictive candidates Disable predictive conversion for user dictionary diff --git a/app/src/main/res/xml/pref_dictionary.xml b/app/src/main/res/xml/pref_dictionary.xml index 3bae9e6a..948d2070 100644 --- a/app/src/main/res/xml/pref_dictionary.xml +++ b/app/src/main/res/xml/pref_dictionary.xml @@ -73,6 +73,22 @@ android:summaryOn="@string/learn_dictionary_summary_on" android:title="@string/learn_dictionary_title" /> + + + + + assertTrue("Missing $key from new settings search", key in newHomeKeys) + assertTrue("Missing $key from legacy settings search", key in legacyKeys) + } + } + + @Test + fun incognitoSettingsCanBeAddedFromFrequentSettingsScreen() { + val candidateKeys = SettingDestinations.frequentCandidates(context) + .map { it.key } + .toSet() + + requiredIncognitoSettingKeys.forEach { key -> + assertTrue("Missing $key from frequent setting candidates", key in candidateKeys) + } + } + + private companion object { + val requiredIncognitoSettingKeys = setOf( + "incognito_mode_detection_preference", + "show_learned_candidates_in_incognito_preference", + ) + } +} From 6a7ebaa5eea4ef31b09bd0e5a0dfc6b4793dce79 Mon Sep 17 00:00:00 2001 From: KazumaProject <59742125+KazumaProject@users.noreply.github.com> Date: Tue, 16 Jun 2026 19:43:47 -0400 Subject: [PATCH 6/7] fix settings --- .../ui/setting/SettingSearchIndex.kt | 2 ++ app/src/main/res/xml/pref_common_legacy.xml | 16 +++++++++++ app/src/main/res/xml/pref_dictionary.xml | 16 ----------- .../IncognitoModeSettingsEntryPointTest.kt | 28 +++++++++++++++++++ 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/setting/SettingSearchIndex.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/setting/SettingSearchIndex.kt index 83a79b14..c039aa81 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/setting/SettingSearchIndex.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/setting/SettingSearchIndex.kt @@ -67,6 +67,7 @@ object SettingSearchIndex { R.id.advancedPreferenceFragment, R.id.zenzPreferenceFragment, R.id.gemmaPreferenceFragment, + R.id.legacyCommonPreferenceFragment, ) fun searchable(context: Context): List { @@ -277,6 +278,7 @@ object SettingSearchIndex { } private fun sources(): List = buildList { + add(PreferenceXmlSource(R.xml.pref_common_legacy, R.id.legacyCommonPreferenceFragment, SettingCategory.ADVANCED)) add(PreferenceXmlSource(R.xml.pref_keyboard_display, R.id.keyboardDisplayPreferenceFragment, SettingCategory.KEYBOARD_DISPLAY)) add(PreferenceXmlSource(R.xml.pref_input_method, R.id.inputMethodPreferenceFragment, SettingCategory.INPUT_METHOD)) add(PreferenceXmlSource(R.xml.pref_candidate_conversion, R.id.candidateConversionPreferenceFragment, SettingCategory.CANDIDATE_CONVERSION)) diff --git a/app/src/main/res/xml/pref_common_legacy.xml b/app/src/main/res/xml/pref_common_legacy.xml index 11cd5f1f..9b4b07c1 100644 --- a/app/src/main/res/xml/pref_common_legacy.xml +++ b/app/src/main/res/xml/pref_common_legacy.xml @@ -355,6 +355,22 @@ android:summaryOn="@string/hide_candidate_password_summary_on" android:title="@string/hide_candidate_password_title" /> + + + + - - - - + assertTrue("Missing $key from common settings XML", key in commonKeys) + assertFalse("Unexpected $key in dictionary settings XML", key in dictionaryKeys) + } + } + @Test fun incognitoSettingsAreSearchableInNewAndLegacySettings() { val newHomeKeys = SettingSearchIndex.searchable(context, SettingSearchScope.NEW_HOME) @@ -90,4 +103,19 @@ class IncognitoModeSettingsEntryPointTest { "show_learned_candidates_in_incognito_preference", ) } + + private fun preferenceKeys(xmlRes: Int): Set { + val androidNamespace = "http://schemas.android.com/apk/res/android" + val parser = context.resources.getXml(xmlRes) + return parser.use { + buildSet { + while (parser.eventType != XmlPullParser.END_DOCUMENT) { + if (parser.eventType == XmlPullParser.START_TAG) { + parser.getAttributeValue(androidNamespace, "key")?.let(::add) + } + parser.next() + } + } + } + } } From a0d6f8715080893037c5bf3089bdb52f6c2f40cd Mon Sep 17 00:00:00 2001 From: KazumaProject <59742125+KazumaProject@users.noreply.github.com> Date: Tue, 16 Jun 2026 20:12:16 -0400 Subject: [PATCH 7/7] fix icon in left side --- .../ime_service/IMEService.kt | 29 +++++++++ .../ime_service/adapters/SuggestionAdapter.kt | 59 +++++++++++++++++++ .../SuggestionAdapterDisplayItemTest.kt | 59 +++++++++++++++++++ .../SuggestionAdapterListUpdateTest.kt | 39 +++++++++++- 4 files changed, 185 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/IMEService.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/IMEService.kt index 3aaee58a..651aa508 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/IMEService.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/IMEService.kt @@ -1452,6 +1452,15 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, } } } + onStartAnchoredContentCommitted = { + if (Looper.myLooper() == Looper.getMainLooper()) { + anchorActiveSuggestionStripStartForLeadingContent() + } else { + mainHandler.post { + anchorActiveSuggestionStripStartForLeadingContent() + } + } + } } suggestionAdapterFull = SuggestionAdapter() shortcutAdapter = ShortcutAdapter() @@ -3653,6 +3662,7 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, setMainSuggestionColumn(mainView) } updateIncognitoModeState(editorInfo) + anchorActiveSuggestionStripStartIfLeadingContentExpected() if (hasPhysicalKeyboard) { floatingDockView.setText("あ") ensurePhysicalKeyboardPopupWindows() @@ -16960,6 +16970,25 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, } } + private fun anchorActiveSuggestionStripStartForLeadingContent() { + assertMainThread("anchorActiveSuggestionStripStartForLeadingContent") + measureDebugSection("IMEService.anchorActiveSuggestionStripStartForLeadingContent") { + if (isKeyboardFloatingMode == true) { + floatingKeyboardBinding?.suggestionRecyclerView?.scrollToPosition(0) + return@measureDebugSection + } + val binding = mainLayoutBinding ?: return@measureDebugSection + setMainSuggestionColumn(binding) + binding.suggestionRecyclerView.scrollToPosition(0) + } + } + + private fun anchorActiveSuggestionStripStartIfLeadingContentExpected() { + assertMainThread("anchorActiveSuggestionStripStartIfLeadingContentExpected") + if (suggestionAdapter?.isStartAnchoredContentExpected() != true) return + anchorActiveSuggestionStripStartForLeadingContent() + } + private fun setMainSuggestionColumn( mainView: MainLayoutBinding ) { diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapter.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapter.kt index f0ea8d1d..46ec263e 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapter.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapter.kt @@ -136,6 +136,24 @@ class SuggestionAdapter : RecyclerView.Adapter() { CustomLayoutItem } + internal enum class StartAnchorRole { + QuickActions, + ShortcutItems, + ShortcutEntry + } + + internal data class QuickActionsVisibilitySignature( + val incognitoVisible: Boolean, + val undoVisible: Boolean, + val redoVisible: Boolean, + val reconvertVisible: Boolean + ) + + internal data class StartAnchorSignature( + val role: StartAnchorRole, + val quickActions: QuickActionsVisibilitySignature? = null + ) + private sealed class SuggestionDisplayItem { data class CandidateItem( val candidate: Candidate, @@ -205,6 +223,7 @@ class SuggestionAdapter : RecyclerView.Adapter() { private val adapterScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) var onListUpdated: (() -> Unit)? = null + var onStartAnchoredContentCommitted: (() -> Unit)? = null // Holds the preview content for the empty state. private var clipboardText: String = "" @@ -242,6 +261,7 @@ class SuggestionAdapter : RecyclerView.Adapter() { private var candidateEmptyDrawableTextColor: Int? = null private var released: Boolean = false private var displayGeneration: Int = 0 + private var committedStartAnchorSignature: StartAnchorSignature? = null fun setOnItemClickListener(onItemClick: (Candidate, Int) -> Unit) { this.onItemClickListener = onItemClick @@ -286,6 +306,7 @@ class SuggestionAdapter : RecyclerView.Adapter() { onShortcutEntryClickListener = null onShowSoftKeyboardClick = null onListUpdated = null + onStartAnchoredContentCommitted = null incognitoIconDrawable = null adapterScope.cancel() } @@ -566,10 +587,20 @@ class SuggestionAdapter : RecyclerView.Adapter() { val newItems = buildDisplayItems() if (displayItems == newItems) return@measureDebugSection + val newStartAnchorSignature = startAnchorSignatureFor(newItems) val generation = ++displayGeneration differ.submitList(newItems) { if (released || generation != displayGeneration) return@submitList + val previousStartAnchorSignature = committedStartAnchorSignature + committedStartAnchorSignature = newStartAnchorSignature onCommitted?.invoke() + if (released || generation != displayGeneration) return@submitList + if ( + newStartAnchorSignature != null && + previousStartAnchorSignature != newStartAnchorSignature + ) { + onStartAnchoredContentCommitted?.invoke() + } } } } @@ -637,6 +668,14 @@ class SuggestionAdapter : RecyclerView.Adapter() { return buildDisplayItems().map { it.kind() } } + internal fun buildStartAnchorSignatureForTesting(): StartAnchorSignature? { + return startAnchorSignatureFor(buildDisplayItems()) + } + + internal fun isStartAnchoredContentExpected(): Boolean { + return startAnchorSignatureFor(buildDisplayItems()) != null + } + private fun currentQuickActionsState(): QuickActionsState = QuickActionsState( undoEnabled = isUndoEnabled, @@ -676,6 +715,26 @@ class SuggestionAdapter : RecyclerView.Adapter() { SuggestionDisplayItemKind.CustomLayoutItem } + private fun startAnchorSignatureFor(items: List): StartAnchorSignature? { + return when (val first = items.firstOrNull()) { + is SuggestionDisplayItem.QuickActionsItem -> + StartAnchorSignature( + role = StartAnchorRole.QuickActions, + quickActions = QuickActionsVisibilitySignature( + incognitoVisible = first.state.incognitoIconDrawable != null, + undoVisible = first.state.undoEnabled, + redoVisible = first.state.redoEnabled, + reconvertVisible = first.state.reconvertEnabled + ) + ) + SuggestionDisplayItem.ShortcutEntryItem -> + StartAnchorSignature(role = StartAnchorRole.ShortcutEntry) + is SuggestionDisplayItem.ShortcutItem -> + StartAnchorSignature(role = StartAnchorRole.ShortcutItems) + else -> null + } + } + inner class SuggestionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val text: MaterialTextView = itemView.findViewById(R.id.suggestion_item_text_view) val yomiText: MaterialTextView = itemView.findViewById(R.id.suggestion_item_yomi_text_view) diff --git a/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapterDisplayItemTest.kt b/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapterDisplayItemTest.kt index 60881f06..84263ba8 100644 --- a/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapterDisplayItemTest.kt +++ b/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapterDisplayItemTest.kt @@ -1,11 +1,15 @@ package com.kazumaproject.markdownhelperkeyboard.ime_service.adapters +import android.graphics.Color +import android.graphics.drawable.ColorDrawable import com.kazumaproject.core.domain.state.TenKeyQWERTYMode import com.kazumaproject.markdownhelperkeyboard.converter.candidate.Candidate import com.kazumaproject.markdownhelperkeyboard.custom_keyboard.data.CustomKeyboardLayout import com.kazumaproject.markdownhelperkeyboard.gemma.GemmaTranslationManager import com.kazumaproject.markdownhelperkeyboard.short_cut.ShortcutType import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @@ -47,6 +51,60 @@ class SuggestionAdapterDisplayItemTest { adapter.release() } + @Test + fun integratedShortcutItemsUseStartAnchorSignature() { + val adapter = SuggestionAdapter() + adapter.setShortcutItems(shortcuts()) + adapter.setIntegratedShortcutItemsVisibility(true) + + assertEquals( + SuggestionAdapter.StartAnchorSignature( + role = SuggestionAdapter.StartAnchorRole.ShortcutItems + ), + adapter.buildStartAnchorSignatureForTesting() + ) + assertTrue(adapter.isStartAnchoredContentExpected()) + adapter.release() + } + + @Test + fun incognitoQuickActionChangesStartAnchorSignature() { + val adapter = SuggestionAdapter() + adapter.setUndoEnabled(true) + adapter.setShortcutItems(shortcuts()) + adapter.setIntegratedShortcutItemsVisibility(true) + + assertEquals( + SuggestionAdapter.StartAnchorSignature( + role = SuggestionAdapter.StartAnchorRole.QuickActions, + quickActions = SuggestionAdapter.QuickActionsVisibilitySignature( + incognitoVisible = false, + undoVisible = true, + redoVisible = false, + reconvertVisible = false + ) + ), + adapter.buildStartAnchorSignatureForTesting() + ) + + adapter.setIncognitoIcon(ColorDrawable(Color.BLACK)) + + assertEquals( + SuggestionAdapter.StartAnchorSignature( + role = SuggestionAdapter.StartAnchorRole.QuickActions, + quickActions = SuggestionAdapter.QuickActionsVisibilitySignature( + incognitoVisible = true, + undoVisible = true, + redoVisible = false, + reconvertVisible = false + ) + ), + adapter.buildStartAnchorSignatureForTesting() + ) + assertTrue(adapter.isStartAnchoredContentExpected()) + adapter.release() + } + @Test fun integratedOffClipboardPreviewDoesNotShowShortcutEntry() { val adapter = SuggestionAdapter() @@ -186,6 +244,7 @@ class SuggestionAdapterDisplayItemTest { ), adapter.buildDisplayItemKindsForTesting() ) + assertFalse(adapter.isStartAnchoredContentExpected()) adapter.release() } diff --git a/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapterListUpdateTest.kt b/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapterListUpdateTest.kt index 4d29a05b..7d3111bc 100644 --- a/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapterListUpdateTest.kt +++ b/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapterListUpdateTest.kt @@ -1,7 +1,10 @@ package com.kazumaproject.markdownhelperkeyboard.ime_service.adapters +import android.graphics.Color +import android.graphics.drawable.ColorDrawable import android.os.Looper import com.kazumaproject.markdownhelperkeyboard.converter.candidate.Candidate +import com.kazumaproject.markdownhelperkeyboard.short_cut.ShortcutType import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test @@ -40,9 +43,43 @@ class SuggestionAdapterListUpdateTest { adapter.release() } + @Test + fun incognitoQuickActionRequestsStartAnchorWithoutSuggestionListUpdate() { + val adapter = SuggestionAdapter() + adapter.setShortcutItems(listOf(ShortcutType.SETTINGS, ShortcutType.EMOJI)) + adapter.setIntegratedShortcutItemsVisibility(true) + drainMainUntil { adapter.itemCount == 2 } + + val startAnchorCount = AtomicInteger(0) + val listUpdateCount = AtomicInteger(0) + val firstAnchor = CountDownLatch(1) + adapter.onStartAnchoredContentCommitted = { + startAnchorCount.incrementAndGet() + firstAnchor.countDown() + } + adapter.onListUpdated = { + listUpdateCount.incrementAndGet() + } + + adapter.setIncognitoIcon(ColorDrawable(Color.BLACK)) + + drainMainUntil(firstAnchor) + assertTrue( + "incognito leading action should request a start anchor", + firstAnchor.await(0, TimeUnit.MILLISECONDS) + ) + assertEquals(1, startAnchorCount.get()) + assertEquals(0, listUpdateCount.get()) + adapter.release() + } + private fun drainMainUntil(latch: CountDownLatch) { + drainMainUntil { latch.count == 0L } + } + + private fun drainMainUntil(condition: () -> Boolean) { val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2) - while (latch.count > 0 && System.nanoTime() < deadline) { + while (!condition() && System.nanoTime() < deadline) { shadowOf(Looper.getMainLooper()).idle() Thread.sleep(10) }