From d96edaf904f1b20c9f8e2ed9d33c970e90262d30 Mon Sep 17 00:00:00 2001 From: astroboii47 Date: Mon, 7 Sep 2026 16:52:41 +1000 Subject: [PATCH] Add unified single-row status bar --- .../palsoftware/pastiera/SettingsManager.kt | 4 +- .../pastiera/StatusBarButtonsScreen.kt | 38 +++++++-------- .../KeyboardVisibilityController.kt | 2 + .../inputmethod/StatusBarController.kt | 20 +++++--- .../inputmethod/ui/VariationBarView.kt | 30 ++++++++++++ app/src/main/res/values/strings.xml | 5 +- ...ettingsManagerStatusBarPresentationTest.kt | 47 +++++++++++++++++++ 7 files changed, 118 insertions(+), 28 deletions(-) create mode 100644 app/src/test/java/it/palsoftware/pastiera/SettingsManagerStatusBarPresentationTest.kt diff --git a/app/src/main/java/it/palsoftware/pastiera/SettingsManager.kt b/app/src/main/java/it/palsoftware/pastiera/SettingsManager.kt index 3990c78e0..6a0dbd452 100644 --- a/app/src/main/java/it/palsoftware/pastiera/SettingsManager.kt +++ b/app/src/main/java/it/palsoftware/pastiera/SettingsManager.kt @@ -163,7 +163,7 @@ object SettingsManager { private const val KEY_SHIFT_BACKSPACE_DELETE = "shift_backspace_delete" // Shift + Backspace performs forward delete private const val KEY_ALT_BACKSPACE_DELETE = "alt_backspace_delete" // Alt + Backspace performs forward delete private const val KEY_BACKSPACE_AT_START_DELETE = "backspace_at_start_delete" // Backspace at line start performs forward delete - private const val KEY_PASTIERINA_MODE_OVERRIDE = "pastierina_mode_override" // pastierina | full_status_bar + private const val KEY_PASTIERINA_MODE_OVERRIDE = "pastierina_mode_override" // pastierina | unified | full_status_bar private const val KEY_PASTIERINA_MODE_ACTIVE = "pastierina_mode_active" // Current effective state private const val KEY_SOFTWARE_KEYBOARD_MODE = "software_keyboard_mode" // auto | force_hardware | force_virtual const val KEY_SOFTWARE_KEYBOARD_MODE_RUNTIME_OVERRIDE = "software_keyboard_mode_runtime_override" @@ -437,6 +437,7 @@ object SettingsManager { enum class StatusBarPresentationMode(val storageValue: String) { PASTIERINA("pastierina"), + UNIFIED("unified"), FULL_STATUS_BAR("full_status_bar") } @@ -560,6 +561,7 @@ object SettingsManager { return when (value) { StatusBarPresentationMode.PASTIERINA.storageValue, "force_minimal" -> StatusBarPresentationMode.PASTIERINA + StatusBarPresentationMode.UNIFIED.storageValue -> StatusBarPresentationMode.UNIFIED else -> StatusBarPresentationMode.FULL_STATUS_BAR } } diff --git a/app/src/main/java/it/palsoftware/pastiera/StatusBarButtonsScreen.kt b/app/src/main/java/it/palsoftware/pastiera/StatusBarButtonsScreen.kt index 99f1d03e1..789adb233 100644 --- a/app/src/main/java/it/palsoftware/pastiera/StatusBarButtonsScreen.kt +++ b/app/src/main/java/it/palsoftware/pastiera/StatusBarButtonsScreen.kt @@ -28,7 +28,7 @@ import androidx.compose.foundation.layout.windowInsetsPadding import androidx.activity.compose.BackHandler import it.palsoftware.pastiera.R -private enum class StatusBarEditorMode { Extended, Pastierina } +private enum class StatusBarEditorMode { Extended, Unified, Pastierina } @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -61,13 +61,10 @@ fun StatusBarButtonsScreen( } var editorMode by remember { mutableStateOf( - if ( - SettingsManager.getStatusBarPresentationMode(context) == - SettingsManager.StatusBarPresentationMode.PASTIERINA - ) { - StatusBarEditorMode.Pastierina - } else { - StatusBarEditorMode.Extended + when (SettingsManager.getStatusBarPresentationMode(context)) { + SettingsManager.StatusBarPresentationMode.FULL_STATUS_BAR -> StatusBarEditorMode.Extended + SettingsManager.StatusBarPresentationMode.UNIFIED -> StatusBarEditorMode.Unified + SettingsManager.StatusBarPresentationMode.PASTIERINA -> StatusBarEditorMode.Pastierina } ) } @@ -230,10 +227,13 @@ fun StatusBarButtonsScreen( editorMode = mode SettingsManager.setStatusBarPresentationMode( context, - if (mode == StatusBarEditorMode.Pastierina) { - SettingsManager.StatusBarPresentationMode.PASTIERINA - } else { - SettingsManager.StatusBarPresentationMode.FULL_STATUS_BAR + when (mode) { + StatusBarEditorMode.Extended -> + SettingsManager.StatusBarPresentationMode.FULL_STATUS_BAR + StatusBarEditorMode.Unified -> + SettingsManager.StatusBarPresentationMode.UNIFIED + StatusBarEditorMode.Pastierina -> + SettingsManager.StatusBarPresentationMode.PASTIERINA } ) }, @@ -241,10 +241,10 @@ fun StatusBarButtonsScreen( ) { Text( text = stringResource( - if (mode == StatusBarEditorMode.Extended) { - R.string.extended_status_bar_title - } else { - R.string.pastierina_status_bar_buttons_title + when (mode) { + StatusBarEditorMode.Extended -> R.string.extended_status_bar_option + StatusBarEditorMode.Unified -> R.string.unified_status_bar_option + StatusBarEditorMode.Pastierina -> R.string.pastierina_status_bar_option } ), maxLines = 1 @@ -254,11 +254,11 @@ fun StatusBarButtonsScreen( } StatusBarLayoutPreview( - leftSlots = if (editorMode == StatusBarEditorMode.Extended) leftSlots else pastierinaLeftSlots, - rightSlots = if (editorMode == StatusBarEditorMode.Extended) rightSlots else pastierinaRightSlots, + leftSlots = if (editorMode != StatusBarEditorMode.Pastierina) leftSlots else pastierinaLeftSlots, + rightSlots = if (editorMode != StatusBarEditorMode.Pastierina) rightSlots else pastierinaRightSlots, centerText = if (editorMode == StatusBarEditorMode.Extended && variationsVisible) { "· · ·" - } else if (editorMode == StatusBarEditorMode.Pastierina) { + } else if (editorMode != StatusBarEditorMode.Extended) { stringResource(R.string.pastierina_preview_suggestions) } else null ) diff --git a/app/src/main/java/it/palsoftware/pastiera/inputmethod/KeyboardVisibilityController.kt b/app/src/main/java/it/palsoftware/pastiera/inputmethod/KeyboardVisibilityController.kt index 1f85a83a8..3806973b3 100644 --- a/app/src/main/java/it/palsoftware/pastiera/inputmethod/KeyboardVisibilityController.kt +++ b/app/src/main/java/it/palsoftware/pastiera/inputmethod/KeyboardVisibilityController.kt @@ -259,6 +259,8 @@ class KeyboardVisibilityController( statusBarPresentationMode = when (statusBarPresentationMode) { SettingsManager.StatusBarPresentationMode.PASTIERINA -> SettingsManager.StatusBarPresentationMode.FULL_STATUS_BAR + SettingsManager.StatusBarPresentationMode.UNIFIED -> + SettingsManager.StatusBarPresentationMode.PASTIERINA SettingsManager.StatusBarPresentationMode.FULL_STATUS_BAR -> SettingsManager.StatusBarPresentationMode.PASTIERINA } diff --git a/app/src/main/java/it/palsoftware/pastiera/inputmethod/StatusBarController.kt b/app/src/main/java/it/palsoftware/pastiera/inputmethod/StatusBarController.kt index 754361a8f..7c125b452 100644 --- a/app/src/main/java/it/palsoftware/pastiera/inputmethod/StatusBarController.kt +++ b/app/src/main/java/it/palsoftware/pastiera/inputmethod/StatusBarController.kt @@ -3035,10 +3035,16 @@ class StatusBarController( val activeTheme = activeThemeSettings(isFullSoftwareKeyboardMode) val activeColors = activeTheme.toKeyboardThemeColors() val softwareThemeSettings = if (isFullSoftwareKeyboardMode) activeTheme else softwareTheme() + val unifiedStatusBar = + SettingsManager.getStatusBarPresentationMode(context) == + SettingsManager.StatusBarPresentationMode.UNIFIED && + !isFullSoftwareKeyboardMode variationBarView?.onVariationSelectedListener = onVariationSelectedListener variationBarView?.onCursorMovedListener = onCursorMovedListener + variationBarView?.onSuggestionCommitted = onSuggestionCommitted + variationBarView?.onHideSuggestion = onHideSuggestion variationBarView?.updateInputConnection(inputConnection) - variationBarView?.forceVariationAreaVisible = isFullSoftwareKeyboardMode + variationBarView?.forceVariationAreaVisible = isFullSoftwareKeyboardMode || unifiedStatusBar variationBarView?.setSymModeActive((snapshot.symPage > 0 && !isSoftwareKeyboardOverlayPage) || snapshot.clipboardOverlay) variationBarView?.updateLanguageButtonText() updateClipboardCount(snapshot.clipboardCount) @@ -3103,7 +3109,8 @@ class StatusBarController( if (showLedStrip) { ledStatusView.update(snapshot) } - val showSecondRow = !pastierinaModeActive + val expansionActive = expansionSuggestions.isNotEmpty() + val showSecondRow = !pastierinaModeActive && !(unifiedStatusBar && expansionActive) val variationsBar = if (showSecondRow) variationBarView else null val variationsWrapperView = if (showSecondRow) variationsWrapper else null if (!showSecondRow) { @@ -3112,8 +3119,7 @@ class StatusBarController( val experimentalEnabled = SettingsManager.isExperimentalSuggestionsEnabled(context) val suggestionsEnabledSetting = SettingsManager.getSuggestionsEnabled(context) // Keep the suggestion/status row stable in both full-status-bar and Pastierina mode. - val expansionActive = expansionSuggestions.isNotEmpty() - val showFullBar = expansionActive || ( + val showFullBar = expansionActive || (!unifiedStatusBar && suggestionsEnabledSetting && (experimentalEnabled || isFullSoftwareKeyboardMode) && (isFullSoftwareKeyboardMode || !snapshot.shouldDisableSuggestions) && @@ -3260,7 +3266,7 @@ class StatusBarController( isEnabled = true isClickable = true } - val snapshotForVariations = if (snapshot.suggestions.isNotEmpty()) { + val snapshotForVariations = if (!unifiedStatusBar && snapshot.suggestions.isNotEmpty()) { snapshot.copy(suggestions = emptyList(), addWordCandidate = null) } else snapshot variationsBar?.showVariations(snapshotForVariations, inputConnection) @@ -3308,7 +3314,7 @@ class StatusBarController( isEnabled = true isClickable = true } - val snapshotForVariations = if (snapshot.suggestions.isNotEmpty()) { + val snapshotForVariations = if (!unifiedStatusBar && snapshot.suggestions.isNotEmpty()) { snapshot.copy(suggestions = emptyList(), addWordCandidate = null) } else snapshot variationsBar?.showVariations(snapshotForVariations, inputConnection) @@ -3325,7 +3331,7 @@ class StatusBarController( isEnabled = true isClickable = true } - val snapshotForVariations = if (snapshot.suggestions.isNotEmpty()) { + val snapshotForVariations = if (!unifiedStatusBar && snapshot.suggestions.isNotEmpty()) { snapshot.copy(suggestions = emptyList(), addWordCandidate = null) } else snapshot variationsBar?.showVariations(snapshotForVariations, inputConnection) diff --git a/app/src/main/java/it/palsoftware/pastiera/inputmethod/ui/VariationBarView.kt b/app/src/main/java/it/palsoftware/pastiera/inputmethod/ui/VariationBarView.kt index 6812a710e..6d8532585 100644 --- a/app/src/main/java/it/palsoftware/pastiera/inputmethod/ui/VariationBarView.kt +++ b/app/src/main/java/it/palsoftware/pastiera/inputmethod/ui/VariationBarView.kt @@ -36,6 +36,7 @@ import it.palsoftware.pastiera.inputmethod.TextSelectionHelper import it.palsoftware.pastiera.inputmethod.NotificationHelper import it.palsoftware.pastiera.inputmethod.VariationButtonHandler import it.palsoftware.pastiera.inputmethod.SpeechRecognitionActivity +import it.palsoftware.pastiera.inputmethod.suggestions.SuggestionButtonHandler import it.palsoftware.pastiera.data.variation.VariationRepository import android.graphics.Paint import android.text.TextUtils @@ -73,6 +74,8 @@ class VariationBarView( var onSpeechRecognitionRequested: (() -> Unit)? = null var onAddUserWord: ((String) -> Unit)? = null var onAddUserWordSubstitutionRequested: ((String) -> Unit)? = null + var onSuggestionCommitted: (() -> Unit)? = null + var onHideSuggestion: ((String) -> Unit)? = null var onLanguageSwitchRequested: (() -> Unit)? = null var onClipboardRequested: (() -> Unit)? = null var onEmojiPickerRequested: (() -> Unit)? = null @@ -407,9 +410,11 @@ class VariationBarView( // Legacy variations: always honor them when present, independent of suggestions. val hasDynamicVariations = canShowVariations && snapshot.variations.isNotEmpty() val hasSuggestions = canShowSuggestions && snapshot.suggestions.isNotEmpty() + val hasAddWordCandidate = canShowSuggestions && !snapshot.addWordCandidate.isNullOrBlank() val useDynamicVariations = statusBarVariationsEnabled && !staticModeEnabled && hasDynamicVariations val allowStaticFallback = statusBarVariationsEnabled && (forceVariationAreaVisible || staticModeEnabled || snapshot.shouldDisableVariations) + var isSuggestionContent = false val effectiveVariations: List val isStaticContent: Boolean @@ -422,6 +427,12 @@ class VariationBarView( statusBarVariationsEnabled && hasSuggestions -> { effectiveVariations = snapshot.suggestions isStaticContent = false + isSuggestionContent = true + } + statusBarVariationsEnabled && hasAddWordCandidate -> { + effectiveVariations = listOfNotNull(snapshot.addWordCandidate) + isStaticContent = false + isSuggestionContent = true } allowStaticFallback -> { val variations = if (snapshot.isEmailField) { @@ -555,6 +566,7 @@ class VariationBarView( val variationSlotsForSizing = when { !reservesVariationArea -> 0 isStaticContent -> rawDisplayedVariations.size + isSuggestionContent -> rawDisplayedVariations.size.coerceIn(1, 3) resizeDynamicVariationsToContent -> rawDisplayedVariations.size.coerceAtMost(dynamicSlotCount) else -> dynamicSlotCount } @@ -708,6 +720,8 @@ class VariationBarView( variationButtonHeight, maxButtonWidth, isStaticContent, + isSuggestionContent, + snapshot.shouldDisableAutoCapitalize, isAddCandidate, isLast, spacingBetweenButtons @@ -1098,6 +1112,8 @@ class VariationBarView( buttonHeight: Int, maxButtonWidth: Int, isStatic: Boolean, + isSuggestionContent: Boolean, + shouldDisableAutoCapitalize: Boolean, isAddCandidate: Boolean, isLast: Boolean, spacingBetweenButtons: Int @@ -1177,6 +1193,14 @@ class VariationBarView( context, onVariationSelectedListener ) + } else if (isSuggestionContent) { + SuggestionButtonHandler.createSuggestionClickListener( + variation, + inputConnection, + onVariationSelectedListener, + shouldDisableAutoCapitalize = shouldDisableAutoCapitalize, + onSuggestionCommitted = onSuggestionCommitted + ) } else { VariationButtonHandler.createVariationClickListener( variation, @@ -1195,6 +1219,12 @@ class VariationBarView( onAddUserWordSubstitutionRequested?.invoke(variation) true } + } else if (isSuggestionContent) { + setOnLongClickListener { + performHapticFeedback(android.view.HapticFeedbackConstants.LONG_PRESS) + onHideSuggestion?.invoke(variation) + true + } } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 68ad50ecb..c10a34ef1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1550,9 +1550,12 @@ Each key can be configured as Keycode, Action, Native Ctrl, or None. Use overrid Status Bar - Configure the Status Bar and Pastierina Mode. + Configure the status bar style and buttons. Status bar style Extended Status Bar + Extended + Unified + Pastierina Extended features Buttons Suggestions diff --git a/app/src/test/java/it/palsoftware/pastiera/SettingsManagerStatusBarPresentationTest.kt b/app/src/test/java/it/palsoftware/pastiera/SettingsManagerStatusBarPresentationTest.kt new file mode 100644 index 000000000..4e53fef77 --- /dev/null +++ b/app/src/test/java/it/palsoftware/pastiera/SettingsManagerStatusBarPresentationTest.kt @@ -0,0 +1,47 @@ +package it.palsoftware.pastiera + +import android.content.Context +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [33]) +class SettingsManagerStatusBarPresentationTest { + private val context: Context + get() = RuntimeEnvironment.getApplication() + + @After + fun tearDown() { + SettingsManager.getPreferences(context).edit().clear().commit() + } + + @Test + fun unifiedPresentationModeRoundTrips() { + SettingsManager.setStatusBarPresentationMode( + context, + SettingsManager.StatusBarPresentationMode.UNIFIED + ) + + assertEquals( + SettingsManager.StatusBarPresentationMode.UNIFIED, + SettingsManager.getStatusBarPresentationMode(context) + ) + } + + @Test + fun unknownPresentationModeFallsBackToExtended() { + SettingsManager.getPreferences(context).edit() + .putString("pastierina_mode_override", "unknown") + .commit() + + assertEquals( + SettingsManager.StatusBarPresentationMode.FULL_STATUS_BAR, + SettingsManager.getStatusBarPresentationMode(context) + ) + } +}