diff --git a/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt b/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt index 818e8feb4..4927cac63 100644 --- a/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt +++ b/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt @@ -104,6 +104,10 @@ class KeyHandler( handleModeChangeKey() true } + KeyboardBase.KEYCODE_FLOAT_TOGGLE -> { + ime.toggleFloatingMode() + true + } KeyboardBase.KEYCODE_SPACE -> handleSpaceKeyPress(previousWasLastKeySpace) in KeyboardBase.NAVIGATION_KEYS -> { handleNavigationKey(code) diff --git a/app/src/keyboards/java/be/scri/helpers/KeyboardLanguageMappingConstants.kt b/app/src/keyboards/java/be/scri/helpers/KeyboardLanguageMappingConstants.kt index 28984886f..9ae3997d8 100644 --- a/app/src/keyboards/java/be/scri/helpers/KeyboardLanguageMappingConstants.kt +++ b/app/src/keyboards/java/be/scri/helpers/KeyboardLanguageMappingConstants.kt @@ -49,4 +49,16 @@ object KeyboardLanguageMappingConstants { "RU" to RUInterfaceVariables.PLURAL_KEY_LBL, "SV" to SVInterfaceVariables.PLURAL_KEY_LBL, ) + + val floatPlaceholder = + mapOf( + "EN" to "Float", + "ES" to "Flotante", + "DE" to "Schweben", + "IT" to "Fluttuante", + "FR" to "Flottant", + "PT" to "Flutuante", + "RU" to "Плавающая", + "SV" to "Flytande", + ) } diff --git a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt index 92e049590..cb518e5e0 100644 --- a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt +++ b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt @@ -56,6 +56,10 @@ class KeyboardUIManager( fun onCloseClicked() + fun onFloatClicked() + + fun isFloatingModeActive(): Boolean + fun onEmojiSelected(emoji: String) fun onSuggestionClicked(suggestion: String) @@ -74,6 +78,8 @@ class KeyboardUIManager( fun isNumericKeyboardActive(): Boolean + fun getKeyboardWidth(): Int + fun onClipboardSuggestionClicked() } @@ -82,6 +88,8 @@ class KeyboardUIManager( // UI Elements var pluralBtn: Button? = binding.pluralBtn + var floatBtn: Button? = null + var separatorFloat: View? = null var emojiBtnPhone1: Button? = binding.emojiBtnPhone1 var emojiSpacePhone: View? = binding.emojiSpacePhone var emojiBtnPhone2: Button? = binding.emojiBtnPhone2 @@ -220,6 +228,7 @@ class KeyboardUIManager( listOf(binding.translateBtn, binding.conjugateBtn, binding.pluralBtn).forEachIndexed { index, button -> button.visibility = View.VISIBLE button.background = null + button.foreground = null button.setTextColor(textColor) button.text = HintUtils.getBaseAutoSuggestions(language).getOrNull(index) button.isAllCaps = false @@ -289,11 +298,31 @@ class KeyboardUIManager( button.backgroundTintList = ContextCompat.getColorStateList(context, R.color.theme_scribe_blue) button.setTextColor(buttonTextColor) button.textSize = GeneralKeyboardIME.SUGGESTION_SIZE + button.isAllCaps = false } - binding.translateBtn.text = translatePlaceholder[langAlias] ?: "Translate" - binding.conjugateBtn.text = conjugatePlaceholder[langAlias] ?: "Conjugate" - binding.pluralBtn.text = pluralPlaceholder[langAlias] ?: "Plural" + val isFloating = listener.isFloatingModeActive() + if (isFloating) { + binding.translateBtn.text = "" + binding.conjugateBtn.text = "" + binding.pluralBtn.text = "" + + binding.translateBtn.foreground = ContextCompat.getDrawable(context, R.drawable.ic_translate_command) + binding.conjugateBtn.foreground = ContextCompat.getDrawable(context, R.drawable.ic_conjugate_command) + binding.pluralBtn.foreground = ContextCompat.getDrawable(context, R.drawable.ic_plural_command) + + binding.translateBtn.foregroundGravity = android.view.Gravity.CENTER + binding.conjugateBtn.foregroundGravity = android.view.Gravity.CENTER + binding.pluralBtn.foregroundGravity = android.view.Gravity.CENTER + } else { + binding.translateBtn.text = translatePlaceholder[langAlias] ?: "Translate" + binding.conjugateBtn.text = conjugatePlaceholder[langAlias] ?: "Conjugate" + binding.pluralBtn.text = pluralPlaceholder[langAlias] ?: "Plural" + + binding.translateBtn.foreground = null + binding.conjugateBtn.foreground = null + binding.pluralBtn.foreground = null + } val separatorColor = (if (isUserDarkMode) GeneralKeyboardIME.DARK_THEME else GeneralKeyboardIME.LIGHT_THEME).toColorInt() binding.separator2.setBackgroundColor(separatorColor) @@ -610,7 +639,8 @@ class KeyboardUIManager( */ fun initializeKeyboard(xmlId: Int) { val enterKeyType = listener.getCurrentEnterKeyType() - keyboard = KeyboardBase(context, xmlId, enterKeyType) + val width = listener.getKeyboardWidth() + keyboard = KeyboardBase(context, xmlId, enterKeyType, width) keyboardView.setKeyboard(keyboard!!) keyboardView.mOnKeyboardActionListener = listener.onKeyboardActionListener() keyboardView.requestLayout() @@ -678,6 +708,10 @@ class KeyboardUIManager( binding.separator4.visibility = View.GONE binding.separator5.visibility = View.GONE binding.separator6.visibility = View.GONE + + binding.translateBtn.foreground = null + binding.conjugateBtn.foreground = null + pluralBtn?.foreground = null } /** diff --git a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt index 4957ac9fb..9b82a6dca 100644 --- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt +++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt @@ -8,6 +8,8 @@ import android.content.Context import android.content.Intent import android.database.sqlite.SQLiteException import android.graphics.Color +import android.graphics.Rect +import android.graphics.drawable.ColorDrawable import android.graphics.drawable.GradientDrawable import android.graphics.drawable.LayerDrawable import android.graphics.drawable.RippleDrawable @@ -20,7 +22,10 @@ import android.text.InputType.TYPE_CLASS_PHONE import android.text.InputType.TYPE_MASK_CLASS import android.util.Log import android.view.KeyEvent +import android.view.MotionEvent import android.view.View +import android.view.ViewGroup +import android.view.WindowManager import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo.IME_ACTION_NONE import android.view.inputmethod.EditorInfo.IME_FLAG_NO_ENTER_ACTION @@ -35,6 +40,7 @@ import androidx.core.graphics.toColorInt import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat import be.scri.R import be.scri.activities.MainActivity import be.scri.databinding.InputMethodViewBinding @@ -246,7 +252,7 @@ abstract class GeneralKeyboardIME( keyboardView = uiManager.keyboardView // Initial keyboard setup. - keyboard = KeyboardBase(this, getKeyboardLayoutXML(), enterKeyType) + keyboard = KeyboardBase(this, getKeyboardLayoutXML(), enterKeyType, getKeyboardWidth()) keyboardView?.apply { setVibrate = getIsVibrateEnabled(applicationContext, language) @@ -259,6 +265,12 @@ abstract class GeneralKeyboardIME( currentState = ScribeState.IDLE saveConjugateModeType("none") + viewBinding.root.post { + disableParentClipping(viewBinding.root) + } + initFloatingMode() + setupFloatingDragListener() + refreshUI() return viewBinding.root @@ -286,21 +298,53 @@ abstract class GeneralKeyboardIME( */ override fun onComputeInsets(outInsets: Insets) { super.onComputeInsets(outInsets) - // Access root view via UI manager if initialized. if (this::uiManager.isInitialized) { val inputView = uiManager.binding.root if (inputView.visibility == View.VISIBLE && inputView.height > 0) { val location = IntArray(2) inputView.getLocationInWindow(location) - outInsets.visibleTopInsets = location[1] - outInsets.contentTopInsets = location[1] - outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_VISIBLE + + if (isFloatingMode) { + // In floating mode, report zero insets so Android doesn't + // push app content up or render IME chrome (∨ / 🌐 buttons) + // below the card. The touchable region is restricted to the + // card bounds so taps outside reach the underlying app. + outInsets.visibleTopInsets = inputView.height + outInsets.contentTopInsets = inputView.height + outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_REGION + + val card = binding.keyboardCard + val density = resources.displayMetrics.density + if (card.width > 0 && card.height > 0) { + val centerX = card.left + card.width / 2f + card.translationX + val centerY = card.top + card.height / 2f + card.translationY + val visualW = card.width * card.scaleX + val visualH = card.height * card.scaleY + val left = (centerX - visualW / 2f).toInt() + val top = (centerY - visualH / 2f).toInt() + val right = (centerX + visualW / 2f).toInt() + val bottom = (centerY + visualH / 2f).toInt() + + val rect = Rect(left, top, right, bottom) + // Expand touchable region slightly to allow resizing handles to be clickable + val margin = (25 * density).toInt() + rect.inset(-margin, -margin) + outInsets.touchableRegion.set(rect) + } else { + outInsets.touchableRegion.setEmpty() + } + } else { + outInsets.visibleTopInsets = location[1] + outInsets.contentTopInsets = location[1] + outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_VISIBLE + } } } } override fun onWindowShown() { super.onWindowShown() + applyFloatingModeState() applyNavBarColor() keyboardView?.setPreview = isShowPopupOnKeypressEnabled(applicationContext, language) keyboardView?.setVibrate = getIsVibrateEnabled(applicationContext, language) @@ -333,7 +377,7 @@ abstract class GeneralKeyboardIME( loadLanguageData() - keyboard = KeyboardBase(this, keyboardXml, enterKeyType) + keyboard = KeyboardBase(this, keyboardXml, enterKeyType, getKeyboardWidth()) keyboardView?.setKeyboard(keyboard!!) if (this::uiManager.isInitialized && keyboardXml == R.xml.keys_symbols) { @@ -485,7 +529,7 @@ abstract class GeneralKeyboardIME( override fun onActionUp() { if (switchToLetters) { keyboardMode = keyboardLetters - keyboard = KeyboardBase(this, getKeyboardLayoutXML(), enterKeyType) + keyboard = KeyboardBase(this, getKeyboardLayoutXML(), enterKeyType, getKeyboardWidth()) val editorInfo = currentInputEditorInfo if (editorInfo != null && editorInfo.inputType != InputType.TYPE_NULL && keyboard?.mShiftState != SHIFT_ON_PERMANENT) { if (currentInputConnection.getCursorCapsMode(editorInfo.inputType) != 0) { @@ -578,6 +622,8 @@ abstract class GeneralKeyboardIME( override fun isClipboardKeyEnabled(): Boolean = PreferencesHelper.getIsClipboardKeyEnabled(this, language) + override fun isFloatingKeyEnabled(): Boolean = PreferencesHelper.getIsFloatingKeyEnabled(this, language) + private fun loadLanguageData() { val languageAlias = getLanguageAlias(language) dataContract = dbManagers.getLanguageContract(languageAlias) @@ -609,36 +655,66 @@ abstract class GeneralKeyboardIME( private fun applyNavBarColor() { val window = window?.window ?: return - val isDarkMode = getIsDarkModeOrNot(applicationContext) - val colorRes = if (isDarkMode) R.color.dark_keyboard_bg_color else R.color.light_keyboard_bg_color - val color = ContextCompat.getColor(this, colorRes) + window.decorView.post { + val isDarkMode = getIsDarkModeOrNot(applicationContext) + val colorRes = if (isDarkMode) R.color.dark_keyboard_bg_color else R.color.light_keyboard_bg_color + val color = ContextCompat.getColor(this, colorRes) - if (Build.VERSION.SDK_INT >= 35) { WindowCompat.setDecorFitsSystemWindows(window, false) - } else { - window.navigationBarColor = Color.TRANSPARENT - } + if (Build.VERSION.SDK_INT < 35) { + window.navigationBarColor = Color.TRANSPARENT + } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - window.isNavigationBarContrastEnforced = false - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + window.isNavigationBarContrastEnforced = false + } - window.decorView.setBackgroundColor(color) - val insetsController = WindowCompat.getInsetsController(window, window.decorView) - insetsController.isAppearanceLightNavigationBars = isLightColor(color) + if (isFloatingMode) { + window.decorView.setBackgroundColor(Color.TRANSPARENT) + } else { + window.decorView.setBackgroundColor(color) + } + val insetsController = WindowCompat.getInsetsController(window, window.decorView) + insetsController.isAppearanceLightNavigationBars = isLightColor(color) + + if (isFloatingMode) { + insetsController.hide(WindowInsetsCompat.Type.navigationBars()) + insetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + @Suppress("DEPRECATION") + window.decorView.systemUiVisibility = ( + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + ) + } else { + insetsController.show(WindowInsetsCompat.Type.navigationBars()) + @Suppress("DEPRECATION") + window.decorView.systemUiVisibility = 0 + } - if (this::uiManager.isInitialized) { - uiManager.binding.root.setBackgroundColor(color) + if (this::uiManager.isInitialized) { + if (isFloatingMode) { + uiManager.binding.root.setBackgroundColor(Color.TRANSPARENT) + // Keep drag bar and pill color in sync with dark/light mode changes + val kbBgColor = ContextCompat.getColor(this, if (isDarkMode) R.color.dark_keyboard_bg_color else R.color.light_keyboard_bg_color) + uiManager.binding.floatingDragBar.setBackgroundColor(kbBgColor) + // Pill: dark mode → 30% white, light mode → 25% black + val pillColor = if (isDarkMode) 0x4DFFFFFF.toInt() else 0x40000000.toInt() + uiManager.binding.floatingDragHandle.setColorFilter(pillColor) + } else { + uiManager.binding.root.setBackgroundColor(color) + } - ViewCompat.setOnApplyWindowInsetsListener(uiManager.binding.root) { view, insets -> - val insetTypes = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() - val navBarHeight = insets.getInsets(insetTypes).bottom - view.setPadding(0, 0, 0, navBarHeight) - insets - } + ViewCompat.setOnApplyWindowInsetsListener(uiManager.binding.root) { view, insets -> + val insetTypes = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() + val navBarHeight = insets.getInsets(insetTypes).bottom + val paddingBottom = if (isFloatingMode) 0 else navBarHeight + view.setPadding(0, 0, 0, paddingBottom) + insets + } - uiManager.binding.root.post { - ViewCompat.requestApplyInsets(uiManager.binding.root) + uiManager.binding.root.post { + ViewCompat.requestApplyInsets(uiManager.binding.root) + } } } } @@ -755,6 +831,12 @@ abstract class GeneralKeyboardIME( moveToIdleState() } + override fun onFloatClicked() { + toggleFloatingMode() + } + + override fun isFloatingModeActive(): Boolean = isFloatingMode + override fun onEmojiSelected(emoji: String) { if (emoji.isNotEmpty()) { insertEmoji(emoji, currentInputConnection, emojiKeywords, emojiMaxKeywordLength) @@ -1242,7 +1324,7 @@ abstract class GeneralKeyboardIME( this.keyboardMode = keyboardSymbols getPrimarySymbolKeyboardLayoutXML() } - keyboard = KeyboardBase(this, keyboardXml, enterKeyType) + keyboard = KeyboardBase(this, keyboardXml, enterKeyType, getKeyboardWidth()) keyboardView!!.setKeyboard(keyboard!!) if (keyboardXml == R.xml.keys_symbols) { handleModeChange(keyboardMode, keyboardView, this) @@ -1270,7 +1352,7 @@ abstract class GeneralKeyboardIME( this.keyboardMode = keyboardLetters getKeyboardLayoutXML() } - keyboard = KeyboardBase(context, keyboardXml, enterKeyType) + keyboard = KeyboardBase(context, keyboardXml, enterKeyType, getKeyboardWidth()) if (this.keyboardMode == keyboardLetters) { val wasShifted = keyboard?.mShiftState == SHIFT_ON_ONE_CHAR || keyboard?.mShiftState == SHIFT_ON_PERMANENT if (wasShifted) { @@ -1777,6 +1859,7 @@ abstract class GeneralKeyboardIME( button.textSize = SUGGESTION_SIZE button.setOnClickListener(null) button.background = null + button.foreground = null button.setTextColor(textColor) button.setOnClickListener { currentInputConnection?.commitText("$text ", 1) @@ -1975,6 +2058,610 @@ abstract class GeneralKeyboardIME( fun disableAutoSuggest() = uiManager.disableAutoSuggest(language) + // MARK: Floating Keyboard Integration + + override fun getKeyboardWidth(): Int = + if (isFloatingMode) { + val density = resources.displayMetrics.density + val screenWidth = resources.displayMetrics.widthPixels + val floatWidth = (320f * density).toInt() + Math.min(floatWidth, (screenWidth * 0.85f).toInt()) + } else { + resources.displayMetrics.widthPixels + } + + private fun recreateKeyboard() { + if (!this::uiManager.isInitialized) return + val xmlId = getCurrentKeyboardLayoutXML() + val currentShiftState = keyboard?.mShiftState ?: SHIFT_OFF + keyboard = KeyboardBase(this, xmlId, enterKeyType, getKeyboardWidth()) + keyboard?.setShifted(currentShiftState) + keyboardView?.setKeyboard(keyboard!!) + + if (xmlId == R.xml.keys_symbols) { + uiManager.setupCurrencySymbol(language) + } + keyboardView?.invalidateAllKeys() + } + + var isFloatingMode: Boolean = false + private set + + private var isUpdatePending = false + private var lastAppliedFloatingMode: Boolean? = null + + fun initFloatingMode() { + isFloatingMode = PreferencesHelper.getIsFloatingModeEnabled(this, language) + lastAppliedFloatingMode = null + applyFloatingModeState() + } + + fun toggleFloatingMode() { + isFloatingMode = !isFloatingMode + PreferencesHelper.setIsFloatingModeEnabled(this, language, isFloatingMode) + // Reset the cached mode so applyFloatingModeState always treats this as a change + lastAppliedFloatingMode = null + applyFloatingModeState() + window?.window?.decorView?.requestLayout() + } + + fun disableFloatingMode() { + if (isFloatingMode) { + isFloatingMode = false + PreferencesHelper.setIsFloatingModeEnabled(this, language, isFloatingMode) + lastAppliedFloatingMode = null + applyFloatingModeState() + window?.window?.decorView?.requestLayout() + } + } + + private fun applyFloatingModeState() { + if (!this::uiManager.isInitialized) return + val card = binding.keyboardCard + val dragBar = binding.floatingDragBar + val density = resources.displayMetrics.density + val root = binding.root + val win = window?.window + + // Only recreate the keyboard when the floating mode actually changes. + // Calling applyFloatingModeState from onWindowShown should not rebuild + // the keyboard every time a text field is focused. + val modeChanged = lastAppliedFloatingMode != isFloatingMode + lastAppliedFloatingMode = isFloatingMode + + val rootWidth = ViewGroup.LayoutParams.MATCH_PARENT + val rootHeight = if (isFloatingMode) ViewGroup.LayoutParams.MATCH_PARENT else ViewGroup.LayoutParams.WRAP_CONTENT + val rootParams = root.layoutParams ?: ViewGroup.LayoutParams(rootWidth, rootHeight) + rootParams.width = rootWidth + rootParams.height = rootHeight + root.layoutParams = rootParams + root.minimumHeight = 0 + + val parentViewGroup = root.parent as? ViewGroup + if (parentViewGroup != null) { + val pParams = parentViewGroup.layoutParams + if (pParams != null) { + pParams.width = rootWidth + pParams.height = rootHeight + parentViewGroup.layoutParams = pParams + } + } + + if (isFloatingMode) { + setBackDisposition(BACK_DISPOSITION_ADJUST_NOTHING) + win?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + win?.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) + } else { + setBackDisposition(BACK_DISPOSITION_DEFAULT) + win?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + win?.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) + } + + if (isFloatingMode) { + val params = card.layoutParams + if (params != null) { + params.width = getKeyboardWidth() + card.layoutParams = params + } + + val scaleFactorX = PreferencesHelper.getFloatingScaleX(this, language) + val scaleFactorY = PreferencesHelper.getFloatingScaleY(this, language) + card.scaleX = scaleFactorX + card.scaleY = scaleFactorY + card.alpha = 1.0f + + // Setup resize corner handlers + binding.resizeHandleTopLeft.setOnTouchListener(resizeTouchListener) + binding.resizeHandleTopRight.setOnTouchListener(resizeTouchListener) + binding.resizeHandleBottomLeft.setOnTouchListener(resizeTouchListener) + binding.resizeHandleBottomRight.setOnTouchListener(resizeTouchListener) + + // Hide initially on mode change + if (modeChanged) { + binding.resizeHandleTopLeft.visibility = View.GONE + binding.resizeHandleTopRight.visibility = View.GONE + binding.resizeHandleBottomLeft.visibility = View.GONE + binding.resizeHandleBottomRight.visibility = View.GONE + } + + val isDarkMode = getIsDarkModeOrNot(this) + val kbBgColorRes = if (isDarkMode) R.color.dark_keyboard_bg_color else R.color.light_keyboard_bg_color + val kbBgColor = ContextCompat.getColor(this, kbBgColorRes) + + // Build a floating card background that matches the keyboard's actual theme color + val floatingBg = + GradientDrawable().apply { + shape = GradientDrawable.RECTANGLE + cornerRadius = 16f * density + setColor(kbBgColor) + setStroke((1f * density).toInt(), 0x40888888.toInt()) + } + card.background = floatingBg + card.elevation = 8f * density + card.clipToOutline = true + + // Drag bar must match the keyboard background — not the default (always-light) color + binding.floatingDragBar.setBackgroundColor(kbBgColor) + // Pill color: visible on both dark and light keyboard backgrounds + val pillColor = if (isDarkMode) 0x4DFFFFFF.toInt() else 0x40000000.toInt() + binding.floatingDragHandle.setColorFilter(pillColor) + + dragBar.visibility = View.VISIBLE + + if (modeChanged) recreateKeyboard() + + card.post { + disableParentClipping(root) + var storedX = PreferencesHelper.getFloatingX(this, language) + var storedY = PreferencesHelper.getFloatingY(this, language) + val currentScaleX = PreferencesHelper.getFloatingScaleX(this, language) + val currentScaleY = PreferencesHelper.getFloatingScaleY(this, language) + + // Default starting position: 100dp from the bottom of the screen + if (storedY == 0f) storedY = 100f * density + + val screenWidth = resources.displayMetrics.widthPixels + val screenHeight = resources.displayMetrics.heightPixels + val cardWidth = card.width.toFloat() + val cardHeight = card.height.toFloat() + + if (cardWidth > 0f && cardHeight > 0f) { + val maxTranslationX = (screenWidth - cardWidth * currentScaleX) / 2f + val minTranslationX = -maxTranslationX + + val minTranslationY = 0f + val maxTranslationY = screenHeight.toFloat() - cardHeight * currentScaleY + + val targetX = storedX.coerceInSafe(minTranslationX, maxTranslationX) + val targetY = storedY.coerceInSafe(minTranslationY, maxTranslationY) + + updateFloatingViewsPosition(targetX, targetY, currentScaleX, currentScaleY) + + val attr = win?.attributes + if (attr != null) { + attr.gravity = android.view.Gravity.TOP or android.view.Gravity.START + attr.x = 0 + attr.y = 0 + attr.width = ViewGroup.LayoutParams.MATCH_PARENT + attr.height = ViewGroup.LayoutParams.MATCH_PARENT + win.attributes = attr + } + root.requestLayout() + } + } + } else { + val params = card.layoutParams + if (params != null) { + params.width = ViewGroup.LayoutParams.MATCH_PARENT + card.layoutParams = params + } + + card.scaleX = 1.0f + card.scaleY = 1.0f + card.alpha = 1.0f + + val isDarkMode = getIsDarkModeOrNot(this) + val kbBgColorRes = if (isDarkMode) R.color.dark_keyboard_bg_color else R.color.light_keyboard_bg_color + card.background = ColorDrawable(ContextCompat.getColor(this, kbBgColorRes)) + card.elevation = 0f + card.clipToOutline = false + + dragBar.visibility = View.GONE + + if (modeChanged) recreateKeyboard() + + // Reset translations immediately so the card doesn't sit at a stale + // floating position while the window re-layouts to WRAP_CONTENT. + card.translationX = 0f + card.translationY = 0f + + binding.resizeHandleTopLeft.translationX = 0f + binding.resizeHandleTopLeft.translationY = 0f + binding.resizeHandleTopRight.translationX = 0f + binding.resizeHandleTopRight.translationY = 0f + binding.resizeHandleBottomLeft.translationX = 0f + binding.resizeHandleBottomLeft.translationY = 0f + binding.resizeHandleBottomRight.translationX = 0f + binding.resizeHandleBottomRight.translationY = 0f + + // Hide resize handles + binding.resizeHandleTopLeft.visibility = View.GONE + binding.resizeHandleTopRight.visibility = View.GONE + binding.resizeHandleBottomLeft.visibility = View.GONE + binding.resizeHandleBottomRight.visibility = View.GONE + + // Apply window attributes first, then force a layout pass to ensure + // the command options bar is fully visible after returning to docked mode. + val attr = win?.attributes + if (attr != null) { + attr.gravity = android.view.Gravity.BOTTOM + attr.x = 0 + attr.y = 0 + attr.width = ViewGroup.LayoutParams.MATCH_PARENT + attr.height = ViewGroup.LayoutParams.WRAP_CONTENT + win.attributes = attr + } + + // Post a second pass to guarantee translations are zero after the + // window has finished resizing — FLAG_LAYOUT_NO_LIMITS removal is + // async and can cause a stale layout frame where the bar is clipped. + card.post { + card.translationX = 0f + card.translationY = 0f + root.requestLayout() + } + } + applyNavBarColor() + } + + private fun updateFloatingViewsPosition( + targetX: Float, + targetY: Float, + scaleX: Float, + scaleY: Float, + ) { + val card = binding.keyboardCard + val screenHeight = resources.displayMetrics.heightPixels.toFloat() + + val cardWidth = card.width.toFloat() + val cardHeight = card.height.toFloat() + if (cardWidth == 0f || cardHeight == 0f) return + + card.scaleX = scaleX + card.scaleY = scaleY + + val transX = targetX + val transY = (screenHeight - cardHeight * scaleY) / 2f - targetY + + card.translationX = transX + card.translationY = transY + + val scaleOffsetX = scaleX - 1.0f + val scaleOffsetY = scaleY - 1.0f + val halfW = cardWidth / 2f + val halfH = cardHeight / 2f + + binding.resizeHandleTopLeft.translationX = transX - halfW * scaleOffsetX + binding.resizeHandleTopLeft.translationY = transY - halfH * scaleOffsetY + + binding.resizeHandleTopRight.translationX = transX + halfW * scaleOffsetX + binding.resizeHandleTopRight.translationY = transY - halfH * scaleOffsetY + + binding.resizeHandleBottomLeft.translationX = transX - halfW * scaleOffsetX + binding.resizeHandleBottomLeft.translationY = transY + halfH * scaleOffsetY + + binding.resizeHandleBottomRight.translationX = transX + halfW * scaleOffsetX + binding.resizeHandleBottomRight.translationY = transY + halfH * scaleOffsetY + } + + private fun disableParentClipping(view: View) { + var p = view.parent + while (p is ViewGroup) { + p.clipChildren = false + p.clipToPadding = false + p = p.parent + } + } + + private var initialX = 0f + private var initialY = 0f + private var initialTranslationX = 0f + private var initialTranslationY = 0f + private var maxTranslationX = 0f + private var minTranslationX = 0f + private var minTranslationY = 0f + private var maxTranslationY = 0f + + private val cornerHideHandler = android.os.Handler(android.os.Looper.getMainLooper()) + private val hideCornersRunnable = + Runnable { + animateHideCorners() + } + + private var initialTouchX = 0f + private var initialTouchY = 0f + private var initialScaleX = 1.0f + private var initialScaleY = 1.0f + private var dragFactorX = 1f + private var dragFactorY = 1f + private var keyboardCenterX = 0f + private var keyboardCenterY = 0f + private var initialDistance = 0f + private var isResizing = false + + private fun showCorners() { + cornerHideHandler.removeCallbacks(hideCornersRunnable) + + val corners = + listOf( + binding.resizeHandleTopLeft, + binding.resizeHandleTopRight, + binding.resizeHandleBottomLeft, + binding.resizeHandleBottomRight, + ) + + for (corner in corners) { + corner.animate().cancel() + corner.alpha = 1f + corner.visibility = View.VISIBLE + } + } + + private fun startHideCornersTimer() { + cornerHideHandler.removeCallbacks(hideCornersRunnable) + cornerHideHandler.postDelayed(hideCornersRunnable, 3000) + } + + private fun animateHideCorners() { + val corners = + listOf( + binding.resizeHandleTopLeft, + binding.resizeHandleTopRight, + binding.resizeHandleBottomLeft, + binding.resizeHandleBottomRight, + ) + + for (corner in corners) { + corner + .animate() + .alpha(0f) + .setDuration(300) + .withEndAction { + corner.visibility = View.GONE + }.start() + } + } + + private fun applyScaleAndPosition( + scaleX: Float, + scaleY: Float, + ) { + val card = binding.keyboardCard + val screenHeight = resources.displayMetrics.heightPixels.toFloat() + val cardHeight = card.height.toFloat() + + val liveX = card.translationX + val liveTransY = card.translationY + val prevScaleY = card.scaleY + val liveY = (screenHeight - cardHeight * prevScaleY) / 2f - liveTransY + + updateFloatingViewsPosition(liveX, liveY, scaleX, scaleY) + } + + private val resizeTouchListener = + View.OnTouchListener { view, event -> + if (!isFloatingMode) return@OnTouchListener false + + when (event.action) { + MotionEvent.ACTION_DOWN -> { + isResizing = true + showCorners() + + initialTouchX = event.rawX + initialTouchY = event.rawY + initialScaleX = PreferencesHelper.getFloatingScaleX(this, language) + initialScaleY = PreferencesHelper.getFloatingScaleY(this, language) + + val viewId = view.id + dragFactorX = + when (viewId) { + R.id.resize_handle_top_left -> -1f + R.id.resize_handle_bottom_left -> -1f + R.id.resize_handle_top_right -> 1f + R.id.resize_handle_bottom_right -> 1f + else -> 1f + } + dragFactorY = + when (viewId) { + R.id.resize_handle_top_left -> -1f + R.id.resize_handle_top_right -> -1f + R.id.resize_handle_bottom_left -> 1f + R.id.resize_handle_bottom_right -> 1f + else -> 1f + } + + val card = binding.keyboardCard + card.animate().cancel() + card.alpha = 0.7f + + val density = resources.displayMetrics.density + val activeColor = ContextCompat.getColor(this@GeneralKeyboardIME, R.color.theme_scribe_blue) + (card.background as? GradientDrawable)?.setStroke((2.5f * density).toInt(), activeColor) + + val location = IntArray(2) + card.getLocationOnScreen(location) + + keyboardCenterX = location[0] + card.width / 2f + keyboardCenterY = location[1] + card.height / 2f + + initialDistance = + Math + .hypot( + (event.rawX - keyboardCenterX).toDouble(), + (event.rawY - keyboardCenterY).toDouble(), + ).toFloat() + + true + } + MotionEvent.ACTION_MOVE -> { + if (!isResizing) return@OnTouchListener false + + val dx = event.rawX - initialTouchX + val dy = event.rawY - initialTouchY + + val card = binding.keyboardCard + val cardWidth = card.width.toFloat() + val cardHeight = card.height.toFloat() + + if (cardWidth > 0 && cardHeight > 0) { + var targetScaleX = initialScaleX + (dragFactorX * dx) / cardWidth + var targetScaleY = initialScaleY + (dragFactorY * dy) / cardHeight + + targetScaleX = targetScaleX.coerceIn(0.6f, 1.5f) + targetScaleY = targetScaleY.coerceIn(0.6f, 1.5f) + + applyScaleAndPosition(targetScaleX, targetScaleY) + } + true + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + isResizing = false + startHideCornersTimer() + + val card = binding.keyboardCard + val finalScaleX = card.scaleX + val finalScaleY = card.scaleY + PreferencesHelper.setFloatingScaleX(this, language, finalScaleX) + PreferencesHelper.setFloatingScaleY(this, language, finalScaleY) + + // Save live position so applyFloatingModeState restores it correctly + val screenHeight = resources.displayMetrics.heightPixels.toFloat() + val cardHeight = card.height.toFloat() + val liveY = (screenHeight - cardHeight * finalScaleY) / 2f - card.translationY + PreferencesHelper.setFloatingX(this, language, card.translationX) + PreferencesHelper.setFloatingY(this, language, liveY) + + applyFloatingModeState() + card + .animate() + .alpha(1.0f) + .setDuration(200) + .start() + true + } + else -> false + } + } + + @android.annotation.SuppressLint("ClickableViewAccessibility") + fun setupFloatingDragListener() { + if (!this::uiManager.isInitialized) return + + binding.floatingDragHandle.setOnTouchListener { _, event -> + if (!isFloatingMode) return@setOnTouchListener false + + when (event.action) { + MotionEvent.ACTION_DOWN -> { + initialX = event.rawX + initialY = event.rawY + + val card = binding.keyboardCard + card.animate().cancel() + card.alpha = 0.7f + + val density = resources.displayMetrics.density + val activeColor = ContextCompat.getColor(this@GeneralKeyboardIME, R.color.theme_scribe_blue) + (card.background as? GradientDrawable)?.setStroke((2.5f * density).toInt(), activeColor) + + val displayMetrics = resources.displayMetrics + val screenWidth = displayMetrics.widthPixels + val screenHeight = displayMetrics.heightPixels + val cardWidth = card.width.toFloat() + val cardHeight = card.height.toFloat() + val scaleFactorX = PreferencesHelper.getFloatingScaleX(this@GeneralKeyboardIME, language) + val scaleFactorY = PreferencesHelper.getFloatingScaleY(this@GeneralKeyboardIME, language) + + maxTranslationX = (screenWidth - cardWidth * scaleFactorX) / 2f + minTranslationX = -maxTranslationX + + minTranslationY = 0f + maxTranslationY = screenHeight.toFloat() - cardHeight * scaleFactorY + + initialTranslationX = PreferencesHelper.getFloatingX(this@GeneralKeyboardIME, language).coerceInSafe(minTranslationX, maxTranslationX) + initialTranslationY = PreferencesHelper.getFloatingY(this@GeneralKeyboardIME, language).coerceInSafe(minTranslationY, maxTranslationY) + + showCorners() + true + } + MotionEvent.ACTION_MOVE -> { + val deltaX = event.rawX - initialX + val deltaY = event.rawY - initialY + + var targetX = initialTranslationX + deltaX + var targetY = initialTranslationY - deltaY + + targetX = targetX.coerceInSafe(minTranslationX, maxTranslationX) + targetY = targetY.coerceInSafe(minTranslationY, maxTranslationY) + + val scaleFactorX = PreferencesHelper.getFloatingScaleX(this@GeneralKeyboardIME, language) + val scaleFactorY = PreferencesHelper.getFloatingScaleY(this@GeneralKeyboardIME, language) + updateFloatingViewsPosition(targetX, targetY, scaleFactorX, scaleFactorY) + + val card = binding.keyboardCard + val density = resources.displayMetrics.density + val isNearBottom = targetY < 60f * density + if (isNearBottom) { + // Highlight to indicate ready to dock + val dockColor = ContextCompat.getColor(this@GeneralKeyboardIME, R.color.theme_scribe_blue) + (card.background as? GradientDrawable)?.setStroke((4.0f * density).toInt(), dockColor) + card.alpha = 0.85f + } else { + // Normal dragging active outline + val activeColor = ContextCompat.getColor(this@GeneralKeyboardIME, R.color.theme_scribe_blue) + (card.background as? GradientDrawable)?.setStroke((2.5f * density).toInt(), activeColor) + card.alpha = 0.7f + } + + true + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + val deltaX = event.rawX - initialX + val deltaY = event.rawY - initialY + var finalTargetX = initialTranslationX + deltaX + var finalTargetY = initialTranslationY - deltaY + finalTargetX = finalTargetX.coerceInSafe(minTranslationX, maxTranslationX) + finalTargetY = finalTargetY.coerceInSafe(minTranslationY, maxTranslationY) + + val scaleFactorX = PreferencesHelper.getFloatingScaleX(this@GeneralKeyboardIME, language) + val scaleFactorY = PreferencesHelper.getFloatingScaleY(this@GeneralKeyboardIME, language) + val density = resources.displayMetrics.density + val isNearBottom = finalTargetY < 60f * density + + val card = binding.keyboardCard + + if (isNearBottom) { + disableFloatingMode() + } else { + updateFloatingViewsPosition(finalTargetX, finalTargetY, scaleFactorX, scaleFactorY) + PreferencesHelper.setFloatingX(this@GeneralKeyboardIME, language, finalTargetX) + PreferencesHelper.setFloatingY(this@GeneralKeyboardIME, language, finalTargetY) + applyFloatingModeState() + } + + card + .animate() + .alpha(1.0f) + .setDuration(200) + .start() + binding.root.requestLayout() + startHideCornersTimer() + true + } + else -> false + } + } + } + override fun onClipboardSuggestionClicked() { latestClipText?.let { text -> currentInputConnection?.commitText(text, 1) @@ -2054,3 +2741,12 @@ abstract class GeneralKeyboardIME( binding.clipboardItemsList.visibility = if (items.isEmpty()) android.view.View.GONE else android.view.View.VISIBLE } } + +private fun Float.coerceInSafe( + bound1: Float, + bound2: Float, +): Float { + val minVal = if (bound1 < bound2) bound1 else bound2 + val maxVal = if (bound1 > bound2) bound1 else bound2 + return this.coerceIn(minVal, maxVal) +} diff --git a/app/src/main/java/be/scri/helpers/KeyboardBase.kt b/app/src/main/java/be/scri/helpers/KeyboardBase.kt index d003b436a..daffde41e 100644 --- a/app/src/main/java/be/scri/helpers/KeyboardBase.kt +++ b/app/src/main/java/be/scri/helpers/KeyboardBase.kt @@ -38,7 +38,11 @@ class KeyboardBase { fun isSearchBar(): Boolean + fun isFloatingModeActive(): Boolean + fun isClipboardKeyEnabled(): Boolean + + fun isFloatingKeyEnabled(): Boolean } /** Horizontal gap default for all rows */ @@ -83,6 +87,7 @@ class KeyboardBase { private const val WIDTH_DIVIDER = 10 const val KEYCODE_SHIFT = -1 const val KEYCODE_MODE_CHANGE = -2 + const val KEYCODE_FLOAT_TOGGLE = -10 const val KEYCODE_ENTER = -4 const val KEYCODE_DELETE = -5 const val KEYCODE_SPACE = 32 @@ -414,8 +419,9 @@ class KeyboardBase { context: Context, @XmlRes xmlLayoutResId: Int, enterKeyType: Int, + customWidth: Int? = null, ) { - mDisplayWidth = context.resources.displayMetrics.widthPixels + mDisplayWidth = customWidth ?: context.resources.displayMetrics.widthPixels mDefaultHorizontalGap = 0 mDefaultWidth = mDisplayWidth / WIDTH_DIVIDER mDefaultHeight = mDefaultWidth @@ -593,6 +599,23 @@ class KeyboardBase { key.gap = 0 } + if (key.code == KEYCODE_MODE_CHANGE) { + if (provider?.isFloatingKeyEnabled() == true) { + key.width = (mDisplayWidth * 0.115).toInt() + } + } else if (provider?.isFloatingKeyEnabled() == true && currentRow.mKeys.any { it.code == KEYCODE_FLOAT_TOGGLE }) { + if (key.code == ','.code && !hideComma) { + key.width = (mDisplayWidth * 0.1).toInt() + } else if (key.label == "_") { + key.width = (mDisplayWidth * 0.1).toInt() + } else if (key.code == KEYCODE_SPACE) { + val isLettersLayout = currentRow.mKeys.none { it.label == "_" } + if (isLettersLayout) { + key.width = (mDisplayWidth * 0.43).toInt() + } + } + } + mKeys!!.add(key) if (key.code == KEYCODE_ENTER) { val enterResourceId = @@ -630,6 +653,29 @@ class KeyboardBase { if (x > mMinWidth) { mMinWidth = x } + if (key.code == KEYCODE_MODE_CHANGE && provider?.isFloatingKeyEnabled() == true) { + val floatKey = Key(currentRow!!) + floatKey.code = KEYCODE_FLOAT_TOGGLE + floatKey.width = (mDisplayWidth * 0.1).toInt() + floatKey.gap = (mDisplayWidth * 0.005).toInt() + floatKey.x = x + floatKey.gap + floatKey.y = y + floatKey.height = currentRow.defaultHeight + + val isFloating = provider?.isFloatingModeActive() == true + val floatResourceId = + if (isFloating) { + R.drawable.ic_keyboard_dismiss + } else { + R.drawable.ic_float_keyboard + } + floatKey.icon = context.resources.getDrawable(floatResourceId, context.theme) + floatKey.icon?.setBounds(0, 0, floatKey.icon!!.intrinsicWidth, floatKey.icon!!.intrinsicHeight) + + mKeys!!.add(floatKey) + currentRow.mKeys.add(floatKey) + x += floatKey.gap + floatKey.width + } } else if (inRow) { inRow = false y += currentRow!!.defaultHeight @@ -694,6 +740,53 @@ class KeyboardBase { } } + // Realign/shrink keys in any row that contains a float toggle key to fit the display width. + for (currentRow in mRows) { + if (currentRow != null) { + val hasFloatToggle = currentRow.mKeys.any { it?.code == KEYCODE_FLOAT_TOGGLE } + if (hasFloatToggle) { + var totalRowWidth = 0 + for (key in currentRow.mKeys) { + if (key != null) { + totalRowWidth += key.width + key.gap + } + } + + val excess = totalRowWidth - mDisplayWidth + if (excess > 0) { + val spaceKey = currentRow.mKeys.find { it?.code == KEYCODE_SPACE } + if (spaceKey != null && spaceKey.width > excess) { + spaceKey.width -= excess + } else { + val normalKeys = + currentRow.mKeys.filterNotNull().filter { + it.code != KEYCODE_FLOAT_TOGGLE && + it.code != KEYCODE_MODE_CHANGE && + it.code != KEYCODE_ENTER && + it.code != KEYCODE_DELETE + } + if (normalKeys.isNotEmpty()) { + val reductionPerKey = excess / normalKeys.size + for (key in normalKeys) { + key.width -= reductionPerKey + } + val remainder = excess % normalKeys.size + normalKeys.first().width -= remainder + } + } + + var currentX = 0 + for (key in currentRow.mKeys) { + if (key != null) { + key.x = currentX + key.gap + currentX += key.width + key.gap + } + } + } + } + } + } + mHeight = y } diff --git a/app/src/main/java/be/scri/helpers/PreferencesHelper.kt b/app/src/main/java/be/scri/helpers/PreferencesHelper.kt index 88284f498..1f3d0283d 100644 --- a/app/src/main/java/be/scri/helpers/PreferencesHelper.kt +++ b/app/src/main/java/be/scri/helpers/PreferencesHelper.kt @@ -27,6 +27,7 @@ object PreferencesHelper { private const val HOLD_FOR_ALT_KEYS = "hold_for_alt_keys" private const val INCREASE_TEXT_SIZE = "increase_text_size" private const val CLIPBOARD_KEY_ON_KEYBOARD = "clipboard_key_on_keyboard" + private const val FLOATING_KEY_ON_KEYBOARD = "floating_key_on_keyboard" /** * Sets the translation source language for a given language. @@ -560,6 +561,31 @@ object PreferencesHelper { return sharedPref.getBoolean(getLanguageSpecificPreferenceKey(CLIPBOARD_KEY_ON_KEYBOARD, language), true) } + /** + * Sets the preference for showing or hiding the floating key on the keyboard. + */ + fun setFloatingKeyPreference( + context: Context, + language: String, + enabled: Boolean, + ) { + val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE) + sharedPref.edit { + putBoolean(getLanguageSpecificPreferenceKey(FLOATING_KEY_ON_KEYBOARD, language), enabled) + } + } + + /** + * Retrieves whether the floating key is enabled on the keyboard for a given language. + */ + fun getIsFloatingKeyEnabled( + context: Context, + language: String, + ): Boolean { + val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE) + return sharedPref.getBoolean(getLanguageSpecificPreferenceKey(FLOATING_KEY_ON_KEYBOARD, language), false) + } + /** * Resets the application hints, marking them as not shown in the shared preferences. * @@ -598,4 +624,117 @@ object PreferencesHelper { val sharedPref = context.getSharedPreferences("app_preferences", Context.MODE_PRIVATE) return sharedPref.getBoolean(INCREASE_TEXT_SIZE, false) } + + // MARK: Floating Keyboard Preferences + + private const val FLOATING_MODE_ENABLED = "floating_mode_enabled" + private const val FLOATING_X = "floating_x" + private const val FLOATING_Y = "floating_y" + private const val FLOATING_SCALE = "floating_scale" + private const val FLOATING_SCALE_X = "floating_scale_x" + private const val FLOATING_SCALE_Y = "floating_scale_y" + + fun setIsFloatingModeEnabled( + context: Context, + language: String, + enabled: Boolean, + ) { + val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE) + sharedPref.edit { putBoolean(getLanguageSpecificPreferenceKey(FLOATING_MODE_ENABLED, language), enabled) } + } + + fun getIsFloatingModeEnabled( + context: Context, + language: String, + ): Boolean { + val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE) + return sharedPref.getBoolean(getLanguageSpecificPreferenceKey(FLOATING_MODE_ENABLED, language), false) + } + + fun setFloatingX( + context: Context, + language: String, + x: Float, + ) { + val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE) + sharedPref.edit { putFloat(getLanguageSpecificPreferenceKey(FLOATING_X, language), x) } + } + + fun getFloatingX( + context: Context, + language: String, + ): Float { + val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE) + return sharedPref.getFloat(getLanguageSpecificPreferenceKey(FLOATING_X, language), 0f) + } + + fun setFloatingY( + context: Context, + language: String, + y: Float, + ) { + val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE) + sharedPref.edit { putFloat(getLanguageSpecificPreferenceKey(FLOATING_Y, language), y) } + } + + fun getFloatingY( + context: Context, + language: String, + ): Float { + val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE) + return sharedPref.getFloat(getLanguageSpecificPreferenceKey(FLOATING_Y, language), 0f) + } + + fun setFloatingScale( + context: Context, + language: String, + scale: Float, + ) { + val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE) + sharedPref.edit { putFloat(getLanguageSpecificPreferenceKey(FLOATING_SCALE, language), scale) } + } + + fun getFloatingScale( + context: Context, + language: String, + ): Float { + val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE) + return sharedPref.getFloat(getLanguageSpecificPreferenceKey(FLOATING_SCALE, language), 1.0f) + } + + fun setFloatingScaleX( + context: Context, + language: String, + scaleX: Float, + ) { + val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE) + sharedPref.edit { putFloat(getLanguageSpecificPreferenceKey(FLOATING_SCALE_X, language), scaleX) } + } + + fun getFloatingScaleX( + context: Context, + language: String, + ): Float { + val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE) + val defaultScale = getFloatingScale(context, language) + return sharedPref.getFloat(getLanguageSpecificPreferenceKey(FLOATING_SCALE_X, language), defaultScale) + } + + fun setFloatingScaleY( + context: Context, + language: String, + scaleY: Float, + ) { + val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE) + sharedPref.edit { putFloat(getLanguageSpecificPreferenceKey(FLOATING_SCALE_Y, language), scaleY) } + } + + fun getFloatingScaleY( + context: Context, + language: String, + ): Float { + val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE) + val defaultScale = getFloatingScale(context, language) + return sharedPref.getFloat(getLanguageSpecificPreferenceKey(FLOATING_SCALE_Y, language), defaultScale) + } } diff --git a/app/src/main/java/be/scri/ui/screens/LanguageSettingsScreen.kt b/app/src/main/java/be/scri/ui/screens/LanguageSettingsScreen.kt index 887dcfe7e..9c73d2065 100644 --- a/app/src/main/java/be/scri/ui/screens/LanguageSettingsScreen.kt +++ b/app/src/main/java/be/scri/ui/screens/LanguageSettingsScreen.kt @@ -116,6 +116,13 @@ fun LanguageSettingsScreen( ) } + val floatingKeyOnKeyboardState = + remember { + mutableStateOf( + PreferencesHelper.getIsFloatingKeyEnabled(context, language), + ) + } + val wordByWordDeletionState = remember { mutableStateOf( @@ -170,6 +177,15 @@ fun LanguageSettingsScreen( isEnabled, ) }, + toggleFloatingKeyOnKeyboard = floatingKeyOnKeyboardState.value, + onToggleFloatingKeyOnKeyboard = { isEnabled -> + floatingKeyOnKeyboardState.value = isEnabled + PreferencesHelper.setFloatingKeyPreference( + context, + language, + isEnabled, + ) + }, onCurrencySelect = onCurrencySelect, ), ) @@ -360,6 +376,7 @@ private fun getFunctionalityListData(settings: FunctionalitySettings): List Unit, toggleClipboardKeyOnKeyboard: Boolean, onToggleClipboardKeyOnKeyboard: (Boolean) -> Unit, + toggleFloatingKeyOnKeyboard: Boolean, + onToggleFloatingKeyOnKeyboard: (Boolean) -> Unit, onCurrencySelect: () -> Unit, ): List { val list: MutableList = mutableListOf() @@ -401,6 +420,14 @@ private fun getLayoutListData( onToggle = onToggleClipboardKeyOnKeyboard, ), ) + list.add( + ScribeItem.SwitchItem( + title = R.string.i18n_app_settings_keyboard_layout_floating_on_keyboard, + desc = R.string.i18n_app_settings_keyboard_layout_floating_on_keyboard_description, + state = toggleFloatingKeyOnKeyboard, + onToggle = onToggleFloatingKeyOnKeyboard, + ), + ) list.add( ScribeItem.ClickableItem( title = R.string.i18n_app_settings_keyboard_layout_default_currency, diff --git a/app/src/main/java/be/scri/views/KeyboardView.kt b/app/src/main/java/be/scri/views/KeyboardView.kt index be9755eb9..babb111e6 100644 --- a/app/src/main/java/be/scri/views/KeyboardView.kt +++ b/app/src/main/java/be/scri/views/KeyboardView.kt @@ -750,6 +750,17 @@ class KeyboardView } } + override fun onSizeChanged( + w: Int, + h: Int, + oldw: Int, + oldh: Int, + ) { + super.onSizeChanged(w, h, oldw, oldh) + mKeyboardChanged = true + invalidateAllKeys() + } + /** * Compute the average distance between adjacent keys (horizontally and vertically) * and square it to get the proximity threshold. @@ -1142,7 +1153,8 @@ class KeyboardView code == KEYCODE_DELETE || code == KEYCODE_SHIFT || code == KEYCODE_TAB || - code == KeyboardBase.KEYCODE_CLIPBOARD + code == KeyboardBase.KEYCODE_CLIPBOARD || + code == KeyboardBase.KEYCODE_FLOAT_TOGGLE if (isIconOnlyKey) { key.icon!!.applyColorFilter(mTextColor) } diff --git a/app/src/main/res/drawable/floating_keyboard_background.xml b/app/src/main/res/drawable/floating_keyboard_background.xml new file mode 100644 index 000000000..4b34336d7 --- /dev/null +++ b/app/src/main/res/drawable/floating_keyboard_background.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_conjugate_command.xml b/app/src/main/res/drawable/ic_conjugate_command.xml new file mode 100644 index 000000000..e23f6748c --- /dev/null +++ b/app/src/main/res/drawable/ic_conjugate_command.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_drag_handle.xml b/app/src/main/res/drawable/ic_drag_handle.xml new file mode 100644 index 000000000..9f5cdf1c9 --- /dev/null +++ b/app/src/main/res/drawable/ic_drag_handle.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_float_keyboard.xml b/app/src/main/res/drawable/ic_float_keyboard.xml new file mode 100644 index 000000000..308a85fc9 --- /dev/null +++ b/app/src/main/res/drawable/ic_float_keyboard.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_keyboard_dismiss.xml b/app/src/main/res/drawable/ic_keyboard_dismiss.xml new file mode 100644 index 000000000..f371a2722 --- /dev/null +++ b/app/src/main/res/drawable/ic_keyboard_dismiss.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_plural_command.xml b/app/src/main/res/drawable/ic_plural_command.xml new file mode 100644 index 000000000..450ca615a --- /dev/null +++ b/app/src/main/res/drawable/ic_plural_command.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_resize_corner.xml b/app/src/main/res/drawable/ic_resize_corner.xml new file mode 100644 index 000000000..868810a6d --- /dev/null +++ b/app/src/main/res/drawable/ic_resize_corner.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_translate_command.xml b/app/src/main/res/drawable/ic_translate_command.xml new file mode 100644 index 000000000..5120996c6 --- /dev/null +++ b/app/src/main/res/drawable/ic_translate_command.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/input_method_view.xml b/app/src/main/res/layout/input_method_view.xml index 7a821b972..fe4a6de87 100644 --- a/app/src/main/res/layout/input_method_view.xml +++ b/app/src/main/res/layout/input_method_view.xml @@ -12,7 +12,20 @@ GeneralKeyboardIME.kt xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/keyboard_holder" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:clipChildren="false" + android:clipToPadding="false"> + + + app:layout_constraintTop_toTopOf="parent"> + + @@ -227,12 +249,12 @@ GeneralKeyboardIME.kt android:id="@+id/plural_btn" android:layout_width="0dp" android:layout_height="@dimen/command_button_height" - android:layout_marginEnd="@dimen/tiny_margin" android:background="@drawable/cmd_key_background_rounded" android:contentDescription="@string/command_bar" android:visibility="visible" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" + android:layout_marginEnd="@dimen/tiny_margin" app:layout_constraintStart_toEndOf="@+id/separator_3" app:layout_constraintTop_toTopOf="parent" /> @@ -690,7 +712,7 @@ GeneralKeyboardIME.kt