From 7a2ca28b50276da5e0620ac63886518920038dcc Mon Sep 17 00:00:00 2001
From: Prince Yadav <66916296+prince-0408@users.noreply.github.com>
Date: Fri, 19 Jun 2026 04:33:23 +0530
Subject: [PATCH 01/10] feat: Add floating keyboard functionality to
Scribe-Android (#261)
---
.../KeyboardLanguageMappingConstants.kt | 12 +
.../be/scri/helpers/ui/KeyboardUIManager.kt | 31 +-
.../be/scri/services/GeneralKeyboardIME.kt | 643 +++++++++++++++++-
.../main/java/be/scri/helpers/KeyboardBase.kt | 3 +-
.../java/be/scri/helpers/PreferencesHelper.kt | 47 ++
.../drawable/floating_keyboard_background.xml | 9 +
app/src/main/res/drawable/ic_drag_handle.xml | 10 +
.../main/res/drawable/ic_keyboard_dismiss.xml | 9 +
.../main/res/drawable/ic_resize_corner.xml | 12 +
app/src/main/res/layout/input_method_view.xml | 145 +++-
app/src/main/res/values-night/colors.xml | 1 +
app/src/main/res/values/colors.xml | 1 +
app/src/main/res/values/strings.xml | 1 +
13 files changed, 885 insertions(+), 39 deletions(-)
create mode 100644 app/src/main/res/drawable/floating_keyboard_background.xml
create mode 100644 app/src/main/res/drawable/ic_drag_handle.xml
create mode 100644 app/src/main/res/drawable/ic_keyboard_dismiss.xml
create mode 100644 app/src/main/res/drawable/ic_resize_corner.xml
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 102bf5986..61eba85bc 100644
--- a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
+++ b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
@@ -23,6 +23,7 @@ import be.scri.R.color.white
import be.scri.databinding.InputMethodViewBinding
import be.scri.helpers.KeyboardBase
import be.scri.helpers.KeyboardLanguageMappingConstants.conjugatePlaceholder
+import be.scri.helpers.KeyboardLanguageMappingConstants.floatPlaceholder
import be.scri.helpers.KeyboardLanguageMappingConstants.pluralPlaceholder
import be.scri.helpers.KeyboardLanguageMappingConstants.translatePlaceholder
import be.scri.helpers.LanguageMappingConstants.getLanguageAlias
@@ -56,6 +57,10 @@ class KeyboardUIManager(
fun onCloseClicked()
+ fun onFloatClicked()
+
+ fun isFloatingModeActive(): Boolean
+
fun onEmojiSelected(emoji: String)
fun onSuggestionClicked(suggestion: String)
@@ -73,6 +78,8 @@ class KeyboardUIManager(
fun processLinguisticSuggestions(word: String)
fun isNumericKeyboardActive(): Boolean
+
+ fun getKeyboardWidth(): Int
}
var keyboardView: KeyboardView = binding.keyboardView
@@ -80,6 +87,8 @@ class KeyboardUIManager(
// UI Elements
var pluralBtn: Button? = binding.pluralBtn
+ var floatBtn: Button? = binding.floatBtn
+ var separatorFloat: View? = binding.separatorFloat
var emojiBtnPhone1: Button? = binding.emojiBtnPhone1
var emojiSpacePhone: View? = binding.emojiSpacePhone
var emojiBtnPhone2: Button? = binding.emojiBtnPhone2
@@ -112,6 +121,7 @@ class KeyboardUIManager(
binding.translateBtn.setOnClickListener { listener.onTranslateClicked() }
binding.conjugateBtn.setOnClickListener { listener.onConjugateClicked() }
binding.pluralBtn.setOnClickListener { listener.onPluralClicked() }
+ binding.floatBtn.setOnClickListener { listener.onFloatClicked() }
binding.scribeKeyClose.setOnClickListener { listener.onCloseClicked() }
@@ -227,6 +237,8 @@ class KeyboardUIManager(
}
binding.separator1.visibility = View.GONE
+ binding.floatBtn.visibility = View.GONE
+ binding.separatorFloat.visibility = View.GONE
binding.ivInfo.visibility = View.GONE
binding.conjugateGridContainer.visibility = View.GONE
binding.keyboardView.visibility = View.VISIBLE
@@ -273,25 +285,34 @@ class KeyboardUIManager(
val buttonTextColor = if (isUserDarkMode) Color.WHITE else Color.BLACK
- listOf(binding.translateBtn, binding.conjugateBtn, binding.pluralBtn).forEach { button ->
+ listOf(binding.translateBtn, binding.conjugateBtn, binding.pluralBtn, binding.floatBtn).forEach { button ->
button.visibility = View.VISIBLE
button.background = ContextCompat.getDrawable(context, R.drawable.button_background_rounded)
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"
+ binding.floatBtn.text = floatPlaceholder[langAlias] ?: "Float"
+
+ // Show the float button as "active" (lighter tint) when floating mode is already on
+ if (listener.isFloatingModeActive()) {
+ binding.floatBtn.backgroundTintList = ContextCompat.getColorStateList(context, R.color.light_key_color)
+ binding.floatBtn.setTextColor(ContextCompat.getColor(context, R.color.theme_scribe_blue))
+ }
val separatorColor = (if (isUserDarkMode) GeneralKeyboardIME.DARK_THEME else GeneralKeyboardIME.LIGHT_THEME).toColorInt()
binding.separator2.setBackgroundColor(separatorColor)
binding.separator3.setBackgroundColor(separatorColor)
+ binding.separatorFloat.setBackgroundColor(separatorColor)
val spaceInDp = 4
val spaceInPx = (spaceInDp * context.resources.displayMetrics.density).toInt()
- listOf(binding.separator2, binding.separator3).forEach { separator ->
+ listOf(binding.separator2, binding.separator3, binding.separatorFloat).forEach { separator ->
separator.setBackgroundColor(Color.TRANSPARENT)
val params = separator.layoutParams
params.width = spaceInPx
@@ -301,6 +322,7 @@ class KeyboardUIManager(
binding.separator1.visibility = View.GONE
binding.separator2.visibility = View.VISIBLE
binding.separator3.visibility = View.VISIBLE
+ binding.separatorFloat.visibility = View.VISIBLE
binding.separator4.visibility = View.GONE
binding.separator5.visibility = View.GONE
binding.separator6.visibility = View.GONE
@@ -600,7 +622,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()
@@ -660,6 +683,8 @@ class KeyboardUIManager(
*/
private fun setupDefaultButtonVisibility() {
pluralBtn?.visibility = View.VISIBLE
+ floatBtn?.visibility = View.GONE
+ separatorFloat?.visibility = View.GONE
emojiBtnPhone1?.visibility = View.GONE
emojiBtnPhone2?.visibility = View.GONE
emojiBtnTablet1?.visibility = View.GONE
diff --git a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
index 57c7e1ca8..d4dc3441f 100644
--- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
+++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
@@ -8,10 +8,15 @@ import android.content.Context
import android.content.Intent
import android.database.sqlite.SQLiteException
import android.graphics.Color
+import android.graphics.Rect
+import android.graphics.Region
+import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.LayerDrawable
import android.graphics.drawable.RippleDrawable
import android.inputmethodservice.InputMethodService
+import android.view.Choreographer
+import android.view.WindowManager
import android.os.Build
import android.text.InputType
import android.text.InputType.TYPE_CLASS_DATETIME
@@ -20,7 +25,9 @@ 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.inputmethod.EditorInfo
import android.view.inputmethod.EditorInfo.IME_ACTION_NONE
import android.view.inputmethod.EditorInfo.IME_FLAG_NO_ENTER_ACTION
@@ -35,6 +42,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
@@ -232,7 +240,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)
@@ -245,6 +253,12 @@ abstract class GeneralKeyboardIME(
currentState = ScribeState.IDLE
saveConjugateModeType("none")
+ viewBinding.root.post {
+ disableParentClipping(viewBinding.root)
+ }
+ initFloatingMode()
+ setupFloatingDragListener()
+
refreshUI()
return viewBinding.root
@@ -265,6 +279,8 @@ abstract class GeneralKeyboardIME(
*/
override fun onEvaluateFullscreenMode(): Boolean = false
+
+
/**
* Compute the insets for the keyboard view. This is essential for API 36+
* where the system needs to know the exact size of the keyboard to properly
@@ -272,21 +288,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 = 0
+ outInsets.contentTopInsets = 0
+ 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)
@@ -319,7 +367,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) {
@@ -586,36 +634,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)
+ }
}
}
}
@@ -732,6 +810,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)
@@ -1936,4 +2020,505 @@ abstract class GeneralKeyboardIME(
* Disables all auto-suggestions and resets the suggestion buttons to their default, inactive state.
*/
fun disableAutoSuggest() = uiManager.disableAutoSuggest(language)
+
+ // MARK: Floating Keyboard Integration
+
+ override fun getKeyboardWidth(): Int {
+ return 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()
+ }
+
+ 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 scaleFactor = PreferencesHelper.getFloatingScale(this, language)
+ card.scaleX = scaleFactor
+ card.scaleY = scaleFactor
+
+ // Setup resize corner handlers
+ binding.resizeHandleTopLeft.setOnTouchListener(resizeTouchListener)
+ binding.resizeHandleTopRight.setOnTouchListener(resizeTouchListener)
+ binding.resizeHandleBottomLeft.setOnTouchListener(resizeTouchListener)
+ binding.resizeHandleBottomRight.setOnTouchListener(resizeTouchListener)
+
+ // Hide initially
+ 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 currentScale = PreferencesHelper.getFloatingScale(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 * currentScale) / 2f
+ val minTranslationX = -maxTranslationX
+
+ val minTranslationY = 0f
+ val maxTranslationY = screenHeight.toFloat() - cardHeight * currentScale
+
+ val targetX = storedX.coerceInSafe(minTranslationX, maxTranslationX)
+ val targetY = storedY.coerceInSafe(minTranslationY, maxTranslationY)
+
+ updateFloatingViewsPosition(targetX, targetY, currentScale)
+
+ 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
+
+ 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, scale: 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 = scale
+ card.scaleY = scale
+
+ val transX = targetX
+ val transY = (screenHeight - cardHeight * scale) / 2f - targetY
+
+ card.translationX = transX
+ card.translationY = transY
+
+ val scaleOffset = scale - 1.0f
+ val halfW = cardWidth / 2f
+ val halfH = cardHeight / 2f
+
+ binding.resizeHandleTopLeft.translationX = transX - halfW * scaleOffset
+ binding.resizeHandleTopLeft.translationY = transY - halfH * scaleOffset
+
+ binding.resizeHandleTopRight.translationX = transX + halfW * scaleOffset
+ binding.resizeHandleTopRight.translationY = transY - halfH * scaleOffset
+
+ binding.resizeHandleBottomLeft.translationX = transX - halfW * scaleOffset
+ binding.resizeHandleBottomLeft.translationY = transY + halfH * scaleOffset
+
+ binding.resizeHandleBottomRight.translationX = transX + halfW * scaleOffset
+ binding.resizeHandleBottomRight.translationY = transY + halfH * scaleOffset
+ }
+
+ 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 initialScale = 1.0f
+ 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(scale: Float) {
+ val card = binding.keyboardCard
+ val screenHeight = resources.displayMetrics.heightPixels.toFloat()
+ val cardHeight = card.height.toFloat()
+
+ // Recover current logical Y from live translationY (inverse of updateFloatingViewsPosition formula)
+ // transY = (screenHeight - cardHeight * scale) / 2f - targetY
+ // => targetY = (screenHeight - cardHeight * scale) / 2f - transY
+ // Use the previous scale to get the consistent Y value before scale changes
+ val liveX = card.translationX
+ val liveTransY = card.translationY
+ val prevScale = card.scaleX
+ val liveY = (screenHeight - cardHeight * prevScale) / 2f - liveTransY
+
+ updateFloatingViewsPosition(liveX, liveY, scale)
+ }
+
+ private val resizeTouchListener = View.OnTouchListener { _, event ->
+ if (!isFloatingMode) return@OnTouchListener false
+
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ isResizing = true
+ showCorners()
+
+ initialTouchX = event.rawX
+ initialTouchY = event.rawY
+ initialScale = PreferencesHelper.getFloatingScale(this, language)
+
+ val card = binding.keyboardCard
+ 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 currentDistance = Math.hypot(
+ (event.rawX - keyboardCenterX).toDouble(),
+ (event.rawY - keyboardCenterY).toDouble()
+ ).toFloat()
+
+ if (initialDistance > 0) {
+ var targetScale = initialScale * (currentDistance / initialDistance)
+ targetScale = targetScale.coerceIn(0.7f, 1.3f)
+ applyScaleAndPosition(targetScale)
+ }
+ true
+ }
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
+ isResizing = false
+ startHideCornersTimer()
+
+ val finalScale = binding.keyboardCard.scaleX
+ PreferencesHelper.setFloatingScale(this, language, finalScale)
+
+ // Save live position so applyFloatingModeState restores it correctly
+ val card = binding.keyboardCard
+ val screenHeight = resources.displayMetrics.heightPixels.toFloat()
+ val cardHeight = card.height.toFloat()
+ val liveY = (screenHeight - cardHeight * finalScale) / 2f - card.translationY
+ PreferencesHelper.setFloatingX(this, language, card.translationX)
+ PreferencesHelper.setFloatingY(this, language, liveY)
+
+ applyFloatingModeState()
+ 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 displayMetrics = resources.displayMetrics
+ val screenWidth = displayMetrics.widthPixels
+ val screenHeight = displayMetrics.heightPixels
+ val cardWidth = binding.keyboardCard.width.toFloat()
+ val cardHeight = binding.keyboardCard.height.toFloat()
+ val scaleFactor = PreferencesHelper.getFloatingScale(this@GeneralKeyboardIME, language)
+
+ maxTranslationX = (screenWidth - cardWidth * scaleFactor) / 2f
+ minTranslationX = -maxTranslationX
+
+ minTranslationY = 0f
+ maxTranslationY = screenHeight.toFloat() - cardHeight * scaleFactor
+
+ 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 scaleFactor = PreferencesHelper.getFloatingScale(this@GeneralKeyboardIME, language)
+ updateFloatingViewsPosition(targetX, targetY, scaleFactor)
+
+ 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 scaleFactor = PreferencesHelper.getFloatingScale(this@GeneralKeyboardIME, language)
+ updateFloatingViewsPosition(finalTargetX, finalTargetY, scaleFactor)
+
+ PreferencesHelper.setFloatingX(this@GeneralKeyboardIME, language, finalTargetX)
+ PreferencesHelper.setFloatingY(this@GeneralKeyboardIME, language, finalTargetY)
+
+ binding.root.requestLayout()
+ startHideCornersTimer()
+ true
+ }
+ else -> false
+ }
+ }
+ }
+}
+
+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 4af94453d..d10e1a2c3 100644
--- a/app/src/main/java/be/scri/helpers/KeyboardBase.kt
+++ b/app/src/main/java/be/scri/helpers/KeyboardBase.kt
@@ -411,8 +411,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
diff --git a/app/src/main/java/be/scri/helpers/PreferencesHelper.kt b/app/src/main/java/be/scri/helpers/PreferencesHelper.kt
index 1427bb8a0..8e7932648 100644
--- a/app/src/main/java/be/scri/helpers/PreferencesHelper.kt
+++ b/app/src/main/java/be/scri/helpers/PreferencesHelper.kt
@@ -632,4 +632,51 @@ 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"
+
+ 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)
+ }
}
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_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_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_resize_corner.xml b/app/src/main/res/drawable/ic_resize_corner.xml
new file mode 100644
index 000000000..0b339a5ac
--- /dev/null
+++ b/app/src/main/res/drawable/ic_resize_corner.xml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/app/src/main/res/layout/input_method_view.xml b/app/src/main/res/layout/input_method_view.xml
index f79cf8133..ea38161af 100644
--- a/app/src/main/res/layout/input_method_view.xml
+++ b/app/src/main/res/layout/input_method_view.xml
@@ -11,7 +11,20 @@
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">
+
+
@@ -25,7 +38,7 @@
app:layout_constraintBottom_toTopOf="@id/keyboard_view"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toBottomOf="parent">
+ app:layout_constraintTop_toTopOf="parent">
+
+
@@ -225,15 +247,42 @@
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"
+ app:layout_constraintEnd_toStartOf="@+id/separator_float"
app:layout_constraintStart_toEndOf="@+id/separator_3"
app:layout_constraintTop_toTopOf="parent" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
index 56a9fcab3..f4f15d8c0 100644
--- a/app/src/main/res/values-night/colors.xml
+++ b/app/src/main/res/values-night/colors.xml
@@ -23,6 +23,7 @@
@color/md_grey_800_dark@color/you_button_background_color@color/dark_scribe_blue
+ #FFB0B0B0@color/dark_keyboard_bg_color@color/dark_key_color@color/dark_keyboard_bg_color
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index e8065be55..13555101c 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -24,6 +24,7 @@
@color/dark_scribe_color
+ #FF555555@color/light_scribe_color#808080
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8431bc8dc..3dd17a727 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -13,4 +13,5 @@
EnterDeleteChange keyboard type
+ Drag keyboard
From 6becf1b6c9afeed98fff1fce7e38bb51086a409a Mon Sep 17 00:00:00 2001
From: Prince Yadav <66916296+prince-0408@users.noreply.github.com>
Date: Fri, 19 Jun 2026 05:36:03 +0530
Subject: [PATCH 02/10] style: fix ktlint issues in GeneralKeyboardIME and
PreferencesHelper
---
.../be/scri/services/GeneralKeyboardIME.kt | 235 +++++++++---------
.../java/be/scri/helpers/PreferencesHelper.kt | 44 +++-
2 files changed, 159 insertions(+), 120 deletions(-)
diff --git a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
index d4dc3441f..ba975a39c 100644
--- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
+++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
@@ -9,14 +9,11 @@ import android.content.Intent
import android.database.sqlite.SQLiteException
import android.graphics.Color
import android.graphics.Rect
-import android.graphics.Region
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.LayerDrawable
import android.graphics.drawable.RippleDrawable
import android.inputmethodservice.InputMethodService
-import android.view.Choreographer
-import android.view.WindowManager
import android.os.Build
import android.text.InputType
import android.text.InputType.TYPE_CLASS_DATETIME
@@ -28,6 +25,7 @@ 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
@@ -279,8 +277,6 @@ abstract class GeneralKeyboardIME(
*/
override fun onEvaluateFullscreenMode(): Boolean = false
-
-
/**
* Compute the insets for the keyboard view. This is essential for API 36+
* where the system needs to know the exact size of the keyboard to properly
@@ -293,7 +289,7 @@ abstract class GeneralKeyboardIME(
if (inputView.visibility == View.VISIBLE && inputView.height > 0) {
val location = IntArray(2)
inputView.getLocationInWindow(location)
-
+
if (isFloatingMode) {
// In floating mode, report zero insets so Android doesn't
// push app content up or render IME chrome (∨ / 🌐 buttons)
@@ -302,7 +298,7 @@ abstract class GeneralKeyboardIME(
outInsets.visibleTopInsets = 0
outInsets.contentTopInsets = 0
outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_REGION
-
+
val card = binding.keyboardCard
val density = resources.displayMetrics.density
if (card.width > 0 && card.height > 0) {
@@ -314,7 +310,7 @@ abstract class GeneralKeyboardIME(
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()
@@ -662,7 +658,7 @@ abstract class GeneralKeyboardIME(
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
- or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
+ or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
)
} else {
insetsController.show(WindowInsetsCompat.Type.navigationBars())
@@ -2023,8 +2019,8 @@ abstract class GeneralKeyboardIME(
// MARK: Floating Keyboard Integration
- override fun getKeyboardWidth(): Int {
- return if (isFloatingMode) {
+ override fun getKeyboardWidth(): Int =
+ if (isFloatingMode) {
val density = resources.displayMetrics.density
val screenWidth = resources.displayMetrics.widthPixels
val floatWidth = (320f * density).toInt()
@@ -2032,7 +2028,6 @@ abstract class GeneralKeyboardIME(
} else {
resources.displayMetrics.widthPixels
}
- }
private fun recreateKeyboard() {
if (!this::uiManager.isInitialized) return
@@ -2050,7 +2045,7 @@ abstract class GeneralKeyboardIME(
var isFloatingMode: Boolean = false
private set
-
+
private var isUpdatePending = false
private var lastAppliedFloatingMode: Boolean? = null
@@ -2100,7 +2095,7 @@ abstract class GeneralKeyboardIME(
parentViewGroup.layoutParams = pParams
}
}
-
+
if (isFloatingMode) {
setBackDisposition(BACK_DISPOSITION_ADJUST_NOTHING)
win?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
@@ -2139,12 +2134,13 @@ abstract class GeneralKeyboardIME(
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())
- }
+ 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
@@ -2184,7 +2180,7 @@ abstract class GeneralKeyboardIME(
val targetY = storedY.coerceInSafe(minTranslationY, maxTranslationY)
updateFloatingViewsPosition(targetX, targetY, currentScale)
-
+
val attr = win?.attributes
if (attr != null) {
attr.gravity = android.view.Gravity.TOP or android.view.Gravity.START
@@ -2261,36 +2257,40 @@ abstract class GeneralKeyboardIME(
applyNavBarColor()
}
- private fun updateFloatingViewsPosition(targetX: Float, targetY: Float, scale: Float) {
+ private fun updateFloatingViewsPosition(
+ targetX: Float,
+ targetY: Float,
+ scale: 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 = scale
card.scaleY = scale
-
+
val transX = targetX
val transY = (screenHeight - cardHeight * scale) / 2f - targetY
-
+
card.translationX = transX
card.translationY = transY
-
+
val scaleOffset = scale - 1.0f
val halfW = cardWidth / 2f
val halfH = cardHeight / 2f
-
+
binding.resizeHandleTopLeft.translationX = transX - halfW * scaleOffset
binding.resizeHandleTopLeft.translationY = transY - halfH * scaleOffset
-
+
binding.resizeHandleTopRight.translationX = transX + halfW * scaleOffset
binding.resizeHandleTopRight.translationY = transY - halfH * scaleOffset
-
+
binding.resizeHandleBottomLeft.translationX = transX - halfW * scaleOffset
binding.resizeHandleBottomLeft.translationY = transY + halfH * scaleOffset
-
+
binding.resizeHandleBottomRight.translationX = transX + halfW * scaleOffset
binding.resizeHandleBottomRight.translationY = transY + halfH * scaleOffset
}
@@ -2314,9 +2314,10 @@ abstract class GeneralKeyboardIME(
private var maxTranslationY = 0f
private val cornerHideHandler = android.os.Handler(android.os.Looper.getMainLooper())
- private val hideCornersRunnable = Runnable {
- animateHideCorners()
- }
+ private val hideCornersRunnable =
+ Runnable {
+ animateHideCorners()
+ }
private var initialTouchX = 0f
private var initialTouchY = 0f
@@ -2328,14 +2329,15 @@ abstract class GeneralKeyboardIME(
private fun showCorners() {
cornerHideHandler.removeCallbacks(hideCornersRunnable)
-
- val corners = listOf(
- binding.resizeHandleTopLeft,
- binding.resizeHandleTopRight,
- binding.resizeHandleBottomLeft,
- binding.resizeHandleBottomRight
- )
-
+
+ val corners =
+ listOf(
+ binding.resizeHandleTopLeft,
+ binding.resizeHandleTopRight,
+ binding.resizeHandleBottomLeft,
+ binding.resizeHandleBottomRight,
+ )
+
for (corner in corners) {
corner.animate().cancel()
corner.alpha = 1f
@@ -2349,21 +2351,22 @@ abstract class GeneralKeyboardIME(
}
private fun animateHideCorners() {
- val corners = listOf(
- binding.resizeHandleTopLeft,
- binding.resizeHandleTopRight,
- binding.resizeHandleBottomLeft,
- binding.resizeHandleBottomRight
- )
-
+ val corners =
+ listOf(
+ binding.resizeHandleTopLeft,
+ binding.resizeHandleTopRight,
+ binding.resizeHandleBottomLeft,
+ binding.resizeHandleBottomRight,
+ )
+
for (corner in corners) {
- corner.animate()
+ corner
+ .animate()
.alpha(0f)
.setDuration(300)
.withEndAction {
corner.visibility = View.GONE
- }
- .start()
+ }.start()
}
}
@@ -2384,68 +2387,73 @@ abstract class GeneralKeyboardIME(
updateFloatingViewsPosition(liveX, liveY, scale)
}
- private val resizeTouchListener = View.OnTouchListener { _, event ->
- if (!isFloatingMode) return@OnTouchListener false
+ private val resizeTouchListener =
+ View.OnTouchListener { _, event ->
+ if (!isFloatingMode) return@OnTouchListener false
+
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ isResizing = true
+ showCorners()
- when (event.action) {
- MotionEvent.ACTION_DOWN -> {
- isResizing = true
- showCorners()
-
- initialTouchX = event.rawX
- initialTouchY = event.rawY
- initialScale = PreferencesHelper.getFloatingScale(this, language)
+ initialTouchX = event.rawX
+ initialTouchY = event.rawY
+ initialScale = PreferencesHelper.getFloatingScale(this, language)
- val card = binding.keyboardCard
- 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 currentDistance = Math.hypot(
- (event.rawX - keyboardCenterX).toDouble(),
- (event.rawY - keyboardCenterY).toDouble()
- ).toFloat()
-
- if (initialDistance > 0) {
- var targetScale = initialScale * (currentDistance / initialDistance)
- targetScale = targetScale.coerceIn(0.7f, 1.3f)
- applyScaleAndPosition(targetScale)
+ val card = binding.keyboardCard
+ 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
}
- true
- }
- MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
- isResizing = false
- startHideCornersTimer()
-
- val finalScale = binding.keyboardCard.scaleX
- PreferencesHelper.setFloatingScale(this, language, finalScale)
-
- // Save live position so applyFloatingModeState restores it correctly
- val card = binding.keyboardCard
- val screenHeight = resources.displayMetrics.heightPixels.toFloat()
- val cardHeight = card.height.toFloat()
- val liveY = (screenHeight - cardHeight * finalScale) / 2f - card.translationY
- PreferencesHelper.setFloatingX(this, language, card.translationX)
- PreferencesHelper.setFloatingY(this, language, liveY)
+ MotionEvent.ACTION_MOVE -> {
+ if (!isResizing) return@OnTouchListener false
+
+ val currentDistance =
+ Math
+ .hypot(
+ (event.rawX - keyboardCenterX).toDouble(),
+ (event.rawY - keyboardCenterY).toDouble(),
+ ).toFloat()
+
+ if (initialDistance > 0) {
+ var targetScale = initialScale * (currentDistance / initialDistance)
+ targetScale = targetScale.coerceIn(0.7f, 1.3f)
+ applyScaleAndPosition(targetScale)
+ }
+ true
+ }
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
+ isResizing = false
+ startHideCornersTimer()
+
+ val finalScale = binding.keyboardCard.scaleX
+ PreferencesHelper.setFloatingScale(this, language, finalScale)
+
+ // Save live position so applyFloatingModeState restores it correctly
+ val card = binding.keyboardCard
+ val screenHeight = resources.displayMetrics.heightPixels.toFloat()
+ val cardHeight = card.height.toFloat()
+ val liveY = (screenHeight - cardHeight * finalScale) / 2f - card.translationY
+ PreferencesHelper.setFloatingX(this, language, card.translationX)
+ PreferencesHelper.setFloatingY(this, language, liveY)
- applyFloatingModeState()
- true
+ applyFloatingModeState()
+ true
+ }
+ else -> false
}
- else -> false
}
- }
@android.annotation.SuppressLint("ClickableViewAccessibility")
fun setupFloatingDragListener() {
@@ -2474,14 +2482,14 @@ abstract class GeneralKeyboardIME(
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
@@ -2506,7 +2514,7 @@ abstract class GeneralKeyboardIME(
PreferencesHelper.setFloatingX(this@GeneralKeyboardIME, language, finalTargetX)
PreferencesHelper.setFloatingY(this@GeneralKeyboardIME, language, finalTargetY)
-
+
binding.root.requestLayout()
startHideCornersTimer()
true
@@ -2517,7 +2525,10 @@ abstract class GeneralKeyboardIME(
}
}
-private fun Float.coerceInSafe(bound1: Float, bound2: Float): Float {
+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/PreferencesHelper.kt b/app/src/main/java/be/scri/helpers/PreferencesHelper.kt
index 8e7932648..b5bde4204 100644
--- a/app/src/main/java/be/scri/helpers/PreferencesHelper.kt
+++ b/app/src/main/java/be/scri/helpers/PreferencesHelper.kt
@@ -640,42 +640,70 @@ object PreferencesHelper {
private const val FLOATING_Y = "floating_y"
private const val FLOATING_SCALE = "floating_scale"
- fun setIsFloatingModeEnabled(context: Context, language: String, enabled: Boolean) {
+ 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 {
+ 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) {
+ 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 {
+ 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) {
+ 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 {
+ 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) {
+ 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 {
+ 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)
}
From e10e7243d409215118aadb8f8d6140f96b9f4b1d Mon Sep 17 00:00:00 2001
From: Prince Yadav <66916296+prince-0408@users.noreply.github.com>
Date: Fri, 19 Jun 2026 05:48:05 +0530
Subject: [PATCH 03/10] style: refine float button color and resize corner
handles position
---
.../keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt | 6 ------
app/src/main/res/drawable/ic_resize_corner.xml | 4 +++-
2 files changed, 3 insertions(+), 7 deletions(-)
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 61eba85bc..d3d901f5d 100644
--- a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
+++ b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
@@ -299,12 +299,6 @@ class KeyboardUIManager(
binding.pluralBtn.text = pluralPlaceholder[langAlias] ?: "Plural"
binding.floatBtn.text = floatPlaceholder[langAlias] ?: "Float"
- // Show the float button as "active" (lighter tint) when floating mode is already on
- if (listener.isFloatingModeActive()) {
- binding.floatBtn.backgroundTintList = ContextCompat.getColorStateList(context, R.color.light_key_color)
- binding.floatBtn.setTextColor(ContextCompat.getColor(context, R.color.theme_scribe_blue))
- }
-
val separatorColor = (if (isUserDarkMode) GeneralKeyboardIME.DARK_THEME else GeneralKeyboardIME.LIGHT_THEME).toColorInt()
binding.separator2.setBackgroundColor(separatorColor)
binding.separator3.setBackgroundColor(separatorColor)
diff --git a/app/src/main/res/drawable/ic_resize_corner.xml b/app/src/main/res/drawable/ic_resize_corner.xml
index 0b339a5ac..868810a6d 100644
--- a/app/src/main/res/drawable/ic_resize_corner.xml
+++ b/app/src/main/res/drawable/ic_resize_corner.xml
@@ -4,9 +4,11 @@
android:viewportWidth="24"
android:viewportHeight="24">
+
+
From 8888f673236df4d4f3c237adfa93d5b51ae4c068 Mon Sep 17 00:00:00 2001
From: Prince Yadav <66916296+prince-0408@users.noreply.github.com>
Date: Fri, 19 Jun 2026 06:25:19 +0530
Subject: [PATCH 04/10] fix: pass getKeyboardWidth() to all KeyboardBase
constructor calls to prevent keyboard cutting off on right in floating mode
---
.../keyboards/java/be/scri/services/GeneralKeyboardIME.kt | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
index ba975a39c..524c714ef 100644
--- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
+++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
@@ -509,7 +509,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) {
@@ -1294,7 +1294,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)
@@ -1322,7 +1322,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) {
From 43dcc7ecceda2fa57df0cae97e6305ca62a80230 Mon Sep 17 00:00:00 2001
From: Prince Yadav <66916296+prince-0408@users.noreply.github.com>
Date: Wed, 24 Jun 2026 04:22:03 +0530
Subject: [PATCH 05/10] style(keyboard): rework floating keyboard design and
behavior as per review feedback
---
.../java/be/scri/helpers/KeyHandler.kt | 4 ++
.../be/scri/helpers/ui/KeyboardUIManager.kt | 46 +++++++++++++------
.../be/scri/services/GeneralKeyboardIME.kt | 1 +
.../main/java/be/scri/helpers/KeyboardBase.kt | 41 +++++++++++++++++
.../main/java/be/scri/views/KeyboardView.kt | 6 +++
.../res/drawable/ic_conjugate_command.xml | 9 ++++
.../main/res/drawable/ic_float_keyboard.xml | 9 ++++
.../main/res/drawable/ic_plural_command.xml | 14 ++++++
.../res/drawable/ic_translate_command.xml | 9 ++++
app/src/main/res/layout/input_method_view.xml | 31 +------------
10 files changed, 126 insertions(+), 44 deletions(-)
create mode 100644 app/src/main/res/drawable/ic_conjugate_command.xml
create mode 100644 app/src/main/res/drawable/ic_float_keyboard.xml
create mode 100644 app/src/main/res/drawable/ic_plural_command.xml
create mode 100644 app/src/main/res/drawable/ic_translate_command.xml
diff --git a/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt b/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt
index 790630911..ae8cb8a76 100644
--- a/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt
+++ b/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt
@@ -100,6 +100,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/ui/KeyboardUIManager.kt b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
index d3d901f5d..30c7a1efc 100644
--- a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
+++ b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
@@ -87,8 +87,8 @@ class KeyboardUIManager(
// UI Elements
var pluralBtn: Button? = binding.pluralBtn
- var floatBtn: Button? = binding.floatBtn
- var separatorFloat: View? = binding.separatorFloat
+ var floatBtn: Button? = null
+ var separatorFloat: View? = null
var emojiBtnPhone1: Button? = binding.emojiBtnPhone1
var emojiSpacePhone: View? = binding.emojiSpacePhone
var emojiBtnPhone2: Button? = binding.emojiBtnPhone2
@@ -121,7 +121,6 @@ class KeyboardUIManager(
binding.translateBtn.setOnClickListener { listener.onTranslateClicked() }
binding.conjugateBtn.setOnClickListener { listener.onConjugateClicked() }
binding.pluralBtn.setOnClickListener { listener.onPluralClicked() }
- binding.floatBtn.setOnClickListener { listener.onFloatClicked() }
binding.scribeKeyClose.setOnClickListener { listener.onCloseClicked() }
@@ -220,6 +219,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
@@ -237,8 +237,6 @@ class KeyboardUIManager(
}
binding.separator1.visibility = View.GONE
- binding.floatBtn.visibility = View.GONE
- binding.separatorFloat.visibility = View.GONE
binding.ivInfo.visibility = View.GONE
binding.conjugateGridContainer.visibility = View.GONE
binding.keyboardView.visibility = View.VISIBLE
@@ -285,7 +283,7 @@ class KeyboardUIManager(
val buttonTextColor = if (isUserDarkMode) Color.WHITE else Color.BLACK
- listOf(binding.translateBtn, binding.conjugateBtn, binding.pluralBtn, binding.floatBtn).forEach { button ->
+ listOf(binding.translateBtn, binding.conjugateBtn, binding.pluralBtn).forEach { button ->
button.visibility = View.VISIBLE
button.background = ContextCompat.getDrawable(context, R.drawable.button_background_rounded)
button.backgroundTintList = ContextCompat.getColorStateList(context, R.color.theme_scribe_blue)
@@ -294,19 +292,36 @@ class KeyboardUIManager(
button.isAllCaps = false
}
- binding.translateBtn.text = translatePlaceholder[langAlias] ?: "Translate"
- binding.conjugateBtn.text = conjugatePlaceholder[langAlias] ?: "Conjugate"
- binding.pluralBtn.text = pluralPlaceholder[langAlias] ?: "Plural"
- binding.floatBtn.text = floatPlaceholder[langAlias] ?: "Float"
+ 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)
binding.separator3.setBackgroundColor(separatorColor)
- binding.separatorFloat.setBackgroundColor(separatorColor)
val spaceInDp = 4
val spaceInPx = (spaceInDp * context.resources.displayMetrics.density).toInt()
- listOf(binding.separator2, binding.separator3, binding.separatorFloat).forEach { separator ->
+ listOf(binding.separator2, binding.separator3).forEach { separator ->
separator.setBackgroundColor(Color.TRANSPARENT)
val params = separator.layoutParams
params.width = spaceInPx
@@ -316,7 +331,6 @@ class KeyboardUIManager(
binding.separator1.visibility = View.GONE
binding.separator2.visibility = View.VISIBLE
binding.separator3.visibility = View.VISIBLE
- binding.separatorFloat.visibility = View.VISIBLE
binding.separator4.visibility = View.GONE
binding.separator5.visibility = View.GONE
binding.separator6.visibility = View.GONE
@@ -677,8 +691,6 @@ class KeyboardUIManager(
*/
private fun setupDefaultButtonVisibility() {
pluralBtn?.visibility = View.VISIBLE
- floatBtn?.visibility = View.GONE
- separatorFloat?.visibility = View.GONE
emojiBtnPhone1?.visibility = View.GONE
emojiBtnPhone2?.visibility = View.GONE
emojiBtnTablet1?.visibility = View.GONE
@@ -687,6 +699,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 524c714ef..33b2124b7 100644
--- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
+++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
@@ -1816,6 +1816,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)
diff --git a/app/src/main/java/be/scri/helpers/KeyboardBase.kt b/app/src/main/java/be/scri/helpers/KeyboardBase.kt
index d10e1a2c3..ee2f57019 100644
--- a/app/src/main/java/be/scri/helpers/KeyboardBase.kt
+++ b/app/src/main/java/be/scri/helpers/KeyboardBase.kt
@@ -37,6 +37,7 @@ class KeyboardBase {
val keyboardLetters: Int
fun isSearchBar(): Boolean
+ fun isFloatingModeActive(): Boolean
}
/** Horizontal gap default for all rows */
@@ -81,6 +82,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
@@ -591,6 +593,23 @@ class KeyboardBase {
key.gap = 0
}
+ if (!isSearchBar) {
+ if (key.code == KEYCODE_MODE_CHANGE) {
+ key.width = (mDisplayWidth * 0.115).toInt()
+ } else if (currentRow.mKeys.any { it.code == KEYCODE_FLOAT_TOGGLE }) {
+ if (key.code == ','.code) {
+ key.width = (mDisplayWidth * 0.075).toInt()
+ } else if (key.label == "_") {
+ key.width = (mDisplayWidth * 0.075).toInt()
+ } else if (key.code == KEYCODE_SPACE) {
+ val isLettersLayout = currentRow.mKeys.none { it.label == "_" }
+ if (isLettersLayout) {
+ key.width = (mDisplayWidth * 0.475).toInt()
+ }
+ }
+ }
+ }
+
mKeys!!.add(key)
if (key.code == KEYCODE_ENTER) {
val enterResourceId =
@@ -628,6 +647,28 @@ class KeyboardBase {
if (x > mMinWidth) {
mMinWidth = x
}
+ if (key.code == KEYCODE_MODE_CHANGE && !isSearchBar) {
+ val floatKey = Key(currentRow!!)
+ floatKey.code = KEYCODE_FLOAT_TOGGLE
+ floatKey.width = (mDisplayWidth * 0.08).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
diff --git a/app/src/main/java/be/scri/views/KeyboardView.kt b/app/src/main/java/be/scri/views/KeyboardView.kt
index fd9372cf5..1b5cf329b 100644
--- a/app/src/main/java/be/scri/views/KeyboardView.kt
+++ b/app/src/main/java/be/scri/views/KeyboardView.kt
@@ -746,6 +746,12 @@ 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.
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_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_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_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 ea38161af..9f8bb26ce 100644
--- a/app/src/main/res/layout/input_method_view.xml
+++ b/app/src/main/res/layout/input_method_view.xml
@@ -251,36 +251,9 @@
android:contentDescription="@string/command_bar"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toStartOf="@+id/separator_float"
- app:layout_constraintStart_toEndOf="@+id/separator_3"
- app:layout_constraintTop_toTopOf="parent" />
-
-
-
-
-
-
From 77c2767a572e0e8de98607c3aa4600b792bd5f48 Mon Sep 17 00:00:00 2001
From: Prince Yadav <66916296+prince-0408@users.noreply.github.com>
Date: Wed, 24 Jun 2026 04:53:13 +0530
Subject: [PATCH 06/10] style: fix ktlint formatting and style issues in
KeyboardView, KeyboardBase, and KeyboardUIManager
---
.../java/be/scri/helpers/ui/KeyboardUIManager.kt | 1 -
.../main/java/be/scri/helpers/KeyboardBase.kt | 16 +++++++++-------
app/src/main/java/be/scri/views/KeyboardView.kt | 7 ++++++-
3 files changed, 15 insertions(+), 9 deletions(-)
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 30c7a1efc..dd54cf087 100644
--- a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
+++ b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
@@ -23,7 +23,6 @@ import be.scri.R.color.white
import be.scri.databinding.InputMethodViewBinding
import be.scri.helpers.KeyboardBase
import be.scri.helpers.KeyboardLanguageMappingConstants.conjugatePlaceholder
-import be.scri.helpers.KeyboardLanguageMappingConstants.floatPlaceholder
import be.scri.helpers.KeyboardLanguageMappingConstants.pluralPlaceholder
import be.scri.helpers.KeyboardLanguageMappingConstants.translatePlaceholder
import be.scri.helpers.LanguageMappingConstants.getLanguageAlias
diff --git a/app/src/main/java/be/scri/helpers/KeyboardBase.kt b/app/src/main/java/be/scri/helpers/KeyboardBase.kt
index ee2f57019..e18aa8eec 100644
--- a/app/src/main/java/be/scri/helpers/KeyboardBase.kt
+++ b/app/src/main/java/be/scri/helpers/KeyboardBase.kt
@@ -37,6 +37,7 @@ class KeyboardBase {
val keyboardLetters: Int
fun isSearchBar(): Boolean
+
fun isFloatingModeActive(): Boolean
}
@@ -655,16 +656,17 @@ class KeyboardBase {
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
- }
+ 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
diff --git a/app/src/main/java/be/scri/views/KeyboardView.kt b/app/src/main/java/be/scri/views/KeyboardView.kt
index 1b5cf329b..16e223b96 100644
--- a/app/src/main/java/be/scri/views/KeyboardView.kt
+++ b/app/src/main/java/be/scri/views/KeyboardView.kt
@@ -746,7 +746,12 @@ class KeyboardView
}
}
- override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ override fun onSizeChanged(
+ w: Int,
+ h: Int,
+ oldw: Int,
+ oldh: Int,
+ ) {
super.onSizeChanged(w, h, oldw, oldh)
mKeyboardChanged = true
invalidateAllKeys()
From 98d0a3f9758fe5543af98078cde681d5fab191a5 Mon Sep 17 00:00:00 2001
From: Prince Yadav <66916296+prince-0408@users.noreply.github.com>
Date: Mon, 6 Jul 2026 02:04:57 +0530
Subject: [PATCH 07/10] style: adjust float and comma key widths, reduce
spacebar size as per feedback; fix syntax error
---
.../keyboards/java/be/scri/services/GeneralKeyboardIME.kt | 1 +
app/src/main/java/be/scri/helpers/KeyboardBase.kt | 8 ++++----
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
index 59541ac0a..fa2965209 100644
--- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
+++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
@@ -2648,3 +2648,4 @@ private fun Float.coerceInSafe(
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 1f4b5d55a..101ca294f 100644
--- a/app/src/main/java/be/scri/helpers/KeyboardBase.kt
+++ b/app/src/main/java/be/scri/helpers/KeyboardBase.kt
@@ -600,13 +600,13 @@ class KeyboardBase {
key.width = (mDisplayWidth * 0.115).toInt()
} else if (currentRow.mKeys.any { it.code == KEYCODE_FLOAT_TOGGLE }) {
if (key.code == ','.code) {
- key.width = (mDisplayWidth * 0.075).toInt()
+ key.width = (mDisplayWidth * 0.1).toInt()
} else if (key.label == "_") {
- key.width = (mDisplayWidth * 0.075).toInt()
+ 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.475).toInt()
+ key.width = (mDisplayWidth * 0.43).toInt()
}
}
}
@@ -652,7 +652,7 @@ class KeyboardBase {
if (key.code == KEYCODE_MODE_CHANGE && !isSearchBar) {
val floatKey = Key(currentRow!!)
floatKey.code = KEYCODE_FLOAT_TOGGLE
- floatKey.width = (mDisplayWidth * 0.08).toInt()
+ floatKey.width = (mDisplayWidth * 0.1).toInt()
floatKey.gap = (mDisplayWidth * 0.005).toInt()
floatKey.x = x + floatKey.gap
floatKey.y = y
From b9e946f1c41c60b8dba448edd4f9c3bf5f0f4e4a Mon Sep 17 00:00:00 2001
From: Prince Yadav <66916296+prince-0408@users.noreply.github.com>
Date: Mon, 6 Jul 2026 04:13:54 +0530
Subject: [PATCH 08/10] fix: apply text color filter to float toggle icon;
correctly report floating insets to prevent hiding app input
---
app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt | 4 ++--
app/src/main/java/be/scri/views/KeyboardView.kt | 3 ++-
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
index fa2965209..ea8e774fa 100644
--- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
+++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
@@ -309,8 +309,8 @@ abstract class GeneralKeyboardIME(
// 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 = 0
- outInsets.contentTopInsets = 0
+ outInsets.visibleTopInsets = inputView.height
+ outInsets.contentTopInsets = inputView.height
outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_REGION
val card = binding.keyboardCard
diff --git a/app/src/main/java/be/scri/views/KeyboardView.kt b/app/src/main/java/be/scri/views/KeyboardView.kt
index f6dd5ad0c..babb111e6 100644
--- a/app/src/main/java/be/scri/views/KeyboardView.kt
+++ b/app/src/main/java/be/scri/views/KeyboardView.kt
@@ -1153,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)
}
From 849302e0e3f5757454ba2e83ab6a16427652f53b Mon Sep 17 00:00:00 2001
From: Prince Yadav <66916296+prince-0408@users.noreply.github.com>
Date: Mon, 6 Jul 2026 04:20:01 +0530
Subject: [PATCH 09/10] chore: update i18n submodule and regenerate strings
---
app/src/main/assets/i18n | 2 +-
app/src/main/res/values/string.xml | 266 +++++++----------------------
2 files changed, 63 insertions(+), 205 deletions(-)
diff --git a/app/src/main/assets/i18n b/app/src/main/assets/i18n
index 821fedcd1..12cffbc21 160000
--- a/app/src/main/assets/i18n
+++ b/app/src/main/assets/i18n
@@ -1 +1 @@
-Subproject commit 821fedcd16882ea8e8aa71f30a1ce9ca8d31dbef
+Subproject commit 12cffbc2188b1c3c9bc9ed482ebb4fcaea2d9ef9
diff --git a/app/src/main/res/values/string.xml b/app/src/main/res/values/string.xml
index 27400ebf9..fd948c0a3 100644
--- a/app/src/main/res/values/string.xml
+++ b/app/src/main/res/values/string.xml
@@ -30,8 +30,7 @@
RussianSpanishSwedish
- Here\'s where you can learn more about Scribe and
- its community.
+ Here\'s where you can learn more about Scribe and its community.See the code on GitHubFollow us on MastodonChat with the team on Matrix
@@ -41,20 +40,9 @@
Visit the Scribe websiteWikimedia and ScribeHow we work together
- Scribe would not be possible without
- countless contributions by Wikimedia contributors to the many projects that they support.
- Scribe makes use of data from the Wikidata Lexicographical data community, as well as data
- from Wikipedia for each language that Scribe supports.
- Wikidata is a collaboratively edited
- multilingual knowledge graph hosted by the Wikimedia Foundation. It provides freely
- available data that anyone can use under a Creative Commons Public Domain license (CC0).
- Scribe uses language data from Wikidata to provide users with verb conjugations, noun-form
- annotations, noun plurals, and many other features.
- Wikipedia is a multilingual free online
- encyclopedia written and maintained by a community of volunteers through open collaboration
- and a wiki-based editing system. Scribe uses data from Wikipedia to produce autosuggestions
- by deriving the most common words in a language as well as the most common words that follow
- them.
+ Scribe would not be possible without countless contributions by Wikimedia contributors to the many projects that they support. Scribe makes use of data from the Wikidata Lexicographical data community, as well as data from Wikipedia for each language that Scribe supports.
+ Wikidata is a collaboratively edited multilingual knowledge graph hosted by the Wikimedia Foundation. It provides freely available data that anyone can use under a Creative Commons Public Domain license (CC0). Scribe uses language data from Wikidata to provide users with verb conjugations, noun-form annotations, noun plurals, and many other features.
+ Wikipedia is a multilingual free online encyclopedia written and maintained by a community of volunteers through open collaboration and a wiki-based editing system. Scribe uses data from Wikipedia to produce autosuggestions by deriving the most common words in a language as well as the most common words that follow them.Report a bugRate Scribe ConjugateRate Scribe
@@ -63,98 +51,18 @@
Feedback and supportVersionKeeping you safe
- Please note that the English version of
- this policy takes precedence over all other versions.\n\nThe Scribe developers (SCRIBE)
- built the iOS application "Scribe - Language Keyboards" (SERVICE) as an open-source
- application. This SERVICE is provided by SCRIBE at no cost and is intended for use as
- is.\n\nThis privacy policy (POLICY) is used to inform the reader of the policies for the
- access, tracking, collection, retention, use, and disclosure of personal information (USER
- INFORMATION) and usage data (USER DATA) for all individuals who make use of this SERVICE
- (USERS).\n\nUSER INFORMATION is specifically defined as any information related to the USERS
- themselves or the devices they use to access the SERVICE.\n\nUSER DATA is specifically
- defined as any text that is typed or actions that are done by the USERS while using the
- SERVICE.\n\n1. Policy Statement\n\nThis SERVICE does not access, track, collect, retain,
- use, or disclose any USER INFORMATION or USER DATA.\n\n2. Do Not Track\n\nUSERS contacting
- SCRIBE to ask that their USER INFORMATION and USER DATA not be tracked will be provided with
- a copy of this POLICY as well as a link to all source codes as proof that they are not being
- tracked.\n\n3. Third-Party Data\n\nThis SERVICE makes use of third-party data. All data used
- in the creation of this SERVICE comes from sources that allow its full use in the manner
- done so by the SERVICE. Specifically, the data for this SERVICE comes from Wikidata,
- Wikipedia and Unicode. Wikidata states that, "All structured data in the main, property and
- lexeme namespaces is made available under the Creative Commons CC0 License; text in other
- namespaces is made available under the Creative Commons Attribution-Share Alike License."
- The policy detailing Wikidata data usage can be found at
- https://www.wikidata.org/wiki/Wikidata:Licensing. Wikipedia states that text data, the type
- of data used by the SERVICE, "… can be used under the terms of the Creative Commons
- Attribution Share-Alike license". The policy detailing Wikipedia data usage can be found at
- https://en.wikipedia.org/wiki/Wikipedia:Reusing_Wikipedia_content. Unicode provides
- permission, "… free of charge, to any person obtaining a copy of the Unicode data files and
- any associated documentation (the "Data Files") or Unicode software and any associated
- documentation (the "Software") to deal in the Data Files or Software without restriction…"
- The policy detailing Unicode data usage can be found at
- https://www.unicode.org/license.txt.\n\n4. Third-Party Source Code\n\nThis SERVICE was based
- on third-party code. All source code used in the creation of this SERVICE comes from sources
- that allow its full use in the manner done so by the SERVICE. Specifically, the basis of
- this project was the project Custom Keyboard by Ethan Sarif-Kattan. Custom Keyboard was
- released under an MIT license, with this license being available at
- https://github.com/EthanSK/CustomKeyboard/blob/master/LICENSE.\n\n5. Third-Party
- Services\n\nThis SERVICE makes use of third-party services to manipulate some of the
- third-party data. Specifically, data has been translated using models from Hugging Face
- transformers. This service is covered by an Apache License 2.0, which states that it is
- available for commercial use, modification, distribution, patent use, and private use. The
- license for the aforementioned service can be found at
- https://github.com/huggingface/transformers/blob/master/LICENSE.\n\n6. Third-Party
- Links\n\nThis SERVICE contains links to external websites. If USERS click on a third-party
- link, they will be directed to a website. Note that these external websites are not operated
- by this SERVICE. Therefore, USERS are strongly advised to review the privacy policy of these
- websites. This SERVICE has no control over and assumes no responsibility for the content,
- privacy policies, or practices of any third-party sites or services.\n\n7. Third-Party
- Images\n\nThis SERVICE contains images that are copyrighted by third-parties. Specifically,
- this app includes a copy of the logos of GitHub, Inc and Wikidata, trademarked by Wikimedia
- Foundation, Inc. The terms by which the GitHub logo can be used are found on
- https://github.com/logos, and the terms for the Wikidata logo are found on the following
- Wikimedia page: https://foundation.wikimedia.org/wiki/Policy:Trademark_policy. This SERVICE
- uses the copyrighted images in a way that matches these criteria, with the only deviation
- being a rotation of the GitHub logo that is common in the open-source community to indicate
- that there is a link to the GitHub website.\n\n8. Content Notice\n\nThis SERVICE allows
- USERS to access linguistic content (CONTENT). Some of this CONTENT could be deemed
- inappropriate for children and legal minors. Accessing CONTENT using the SERVICE is done in
- a way that the information is unavailable unless explicitly known. Specifically, USERS "can"
- translate words, conjugate verbs, and access other grammatical features of CONTENT that may
- be sexual, violent, or otherwise inappropriate in nature. USERS "cannot" translate words,
- conjugate verbs, and access other grammatical features of CONTENT that may be sexual,
- violent, or otherwise inappropriate in nature if they do not already know about the nature
- of this CONTENT. SCRIBE takes no responsibility for the access of such CONTENT.\n\n9.
- Changes\n\nThis POLICY is subject to change. Updates to this POLICY will replace all prior
- instances, and if deemed material will further be clearly stated in the next applicable
- update to the SERVICE. SCRIBE encourages USERS to periodically review this POLICY for the
- latest information on our privacy practices and to familiarize themselves with any
- changes.\n\n10. Contact\n\nIf you have any questions, concerns, or suggestions about this
- POLICY, do not hesitate to visit https://github.com/scribe-org or contact SCRIBE at
- security[at]scri.be. The person responsible for such inquiries is Andrew Tavis
- McAllister.\n\n11. Effective Date\n\nThis POLICY is effective as of the 24th of May, 2022.
+ Please note that the English version of this policy takes precedence over all other versions.\n\nThe Scribe developers (SCRIBE) built the iOS application "Scribe - Language Keyboards" (SERVICE) as an open-source application. This SERVICE is provided by SCRIBE at no cost and is intended for use as is.\n\nThis privacy policy (POLICY) is used to inform the reader of the policies for the access, tracking, collection, retention, use, and disclosure of personal information (USER INFORMATION) and usage data (USER DATA) for all individuals who make use of this SERVICE (USERS).\n\nUSER INFORMATION is specifically defined as any information related to the USERS themselves or the devices they use to access the SERVICE.\n\nUSER DATA is specifically defined as any text that is typed or actions that are done by the USERS while using the SERVICE.\n\n1. Policy Statement\n\nThis SERVICE does not access, track, collect, retain, use, or disclose any USER INFORMATION or USER DATA.\n\n2. Do Not Track\n\nUSERS contacting SCRIBE to ask that their USER INFORMATION and USER DATA not be tracked will be provided with a copy of this POLICY as well as a link to all source codes as proof that they are not being tracked.\n\n3. Third-Party Data\n\nThis SERVICE makes use of third-party data. All data used in the creation of this SERVICE comes from sources that allow its full use in the manner done so by the SERVICE. Specifically, the data for this SERVICE comes from Wikidata, Wikipedia and Unicode. Wikidata states that, "All structured data in the main, property and lexeme namespaces is made available under the Creative Commons CC0 License; text in other namespaces is made available under the Creative Commons Attribution-Share Alike License." The policy detailing Wikidata data usage can be found at https://www.wikidata.org/wiki/Wikidata:Licensing. Wikipedia states that text data, the type of data used by the SERVICE, "… can be used under the terms of the Creative Commons Attribution Share-Alike license". The policy detailing Wikipedia data usage can be found at https://en.wikipedia.org/wiki/Wikipedia:Reusing_Wikipedia_content. Unicode provides permission, "… free of charge, to any person obtaining a copy of the Unicode data files and any associated documentation (the "Data Files") or Unicode software and any associated documentation (the "Software") to deal in the Data Files or Software without restriction…" The policy detailing Unicode data usage can be found at https://www.unicode.org/license.txt.\n\n4. Third-Party Source Code\n\nThis SERVICE was based on third-party code. All source code used in the creation of this SERVICE comes from sources that allow its full use in the manner done so by the SERVICE. Specifically, the basis of this project was the project Custom Keyboard by Ethan Sarif-Kattan. Custom Keyboard was released under an MIT license, with this license being available at https://github.com/EthanSK/CustomKeyboard/blob/master/LICENSE.\n\n5. Third-Party Services\n\nThis SERVICE makes use of third-party services to manipulate some of the third-party data. Specifically, data has been translated using models from Hugging Face transformers. This service is covered by an Apache License 2.0, which states that it is available for commercial use, modification, distribution, patent use, and private use. The license for the aforementioned service can be found at https://github.com/huggingface/transformers/blob/master/LICENSE.\n\n6. Third-Party Links\n\nThis SERVICE contains links to external websites. If USERS click on a third-party link, they will be directed to a website. Note that these external websites are not operated by this SERVICE. Therefore, USERS are strongly advised to review the privacy policy of these websites. This SERVICE has no control over and assumes no responsibility for the content, privacy policies, or practices of any third-party sites or services.\n\n7. Third-Party Images\n\nThis SERVICE contains images that are copyrighted by third-parties. Specifically, this app includes a copy of the logos of GitHub, Inc and Wikidata, trademarked by Wikimedia Foundation, Inc. The terms by which the GitHub logo can be used are found on https://github.com/logos, and the terms for the Wikidata logo are found on the following Wikimedia page: https://foundation.wikimedia.org/wiki/Policy:Trademark_policy. This SERVICE uses the copyrighted images in a way that matches these criteria, with the only deviation being a rotation of the GitHub logo that is common in the open-source community to indicate that there is a link to the GitHub website.\n\n8. Content Notice\n\nThis SERVICE allows USERS to access linguistic content (CONTENT). Some of this CONTENT could be deemed inappropriate for children and legal minors. Accessing CONTENT using the SERVICE is done in a way that the information is unavailable unless explicitly known. Specifically, USERS "can" translate words, conjugate verbs, and access other grammatical features of CONTENT that may be sexual, violent, or otherwise inappropriate in nature. USERS "cannot" translate words, conjugate verbs, and access other grammatical features of CONTENT that may be sexual, violent, or otherwise inappropriate in nature if they do not already know about the nature of this CONTENT. SCRIBE takes no responsibility for the access of such CONTENT.\n\n9. Changes\n\nThis POLICY is subject to change. Updates to this POLICY will replace all prior instances, and if deemed material will further be clearly stated in the next applicable update to the SERVICE. SCRIBE encourages USERS to periodically review this POLICY for the latest information on our privacy practices and to familiarize themselves with any changes.\n\n10. Contact\n\nIf you have any questions, concerns, or suggestions about this POLICY, do not hesitate to visit https://github.com/scribe-org or contact SCRIBE at security[at]scri.be. The person responsible for such inquiries is Andrew Tavis McAllister.\n\n11. Effective Date\n\nThis POLICY is effective as of the 24th of May, 2022.Third-party licensesWhose code we used
- Custom Keyboard\n• Author:
- EthanSK\n• License: MIT\n• Link:
- https://github.com/EthanSK/CustomKeyboard/blob/master/LICENSE
- Simple Keyboard\n• Author:
- Simple Mobile Tools\n• License: GPL-3.0\n• Link:
- https://github.com/SimpleMobileTools/Simple-Keyboard/blob/main/LICENSE
- The Scribe developers (SCRIBE) built the
- iOS application "Scribe - Language Keyboards" (SERVICE) using third party code. All source
- code used in the creation of this SERVICE comes from sources that allow its full use in the
- manner done so by the SERVICE. This section lists the source code on which the SERVICE was
- based as well as the coinciding licenses of each.\n\nThe following is a list of all used
- source code, the main author or authors of the code, the license under which it was released
- at time of usage, and a link to the license.
+ Custom Keyboard\n• Author: EthanSK\n• License: MIT\n• Link: https://github.com/EthanSK/CustomKeyboard/blob/master/LICENSE
+ Simple Keyboard\n• Author: Simple Mobile Tools\n• License: GPL-3.0\n• Link: https://github.com/SimpleMobileTools/Simple-Keyboard/blob/main/LICENSE
+ The Scribe developers (SCRIBE) built the iOS application "Scribe - Language Keyboards" (SERVICE) using third party code. All source code used in the creation of this SERVICE comes from sources that allow its full use in the manner done so by the SERVICE. This section lists the source code on which the SERVICE was based as well as the coinciding licenses of each.\n\nThe following is a list of all used source code, the main author or authors of the code, the license under which it was released at time of usage, and a link to the license.AboutClipboard is emptyCopied to clipboardCopy to clipboardClipboard
- Download verb data with the menu below to
- start conjugating verbs.
+ Download verb data with the menu below to start conjugating verbs.Select tenseChoose a conjugation belowRecently conjugated
@@ -170,47 +78,32 @@
Network error: {error}Server error: {code}Download timed out
- Add new data to Scribe
- Conjugate.
+ Add new data to Scribe Conjugate.Download verb data
- Download data to
- start conjugating!
+ Download data to start conjugating!Verb data
- Add new data to Scribe
- keyboards.
+ Add new data to Scribe keyboards.Download keyboard dataLanguage data
- {language} conjugate
- data is already up to date
- {language} conjugate
- data downloaded successfully!
+ {language} conjugate data is already up to date
+ {language} conjugate data downloaded successfully!All languages
- {language} data is
- already up to date
- {language} data
- downloaded successfully!
+ {language} data is already up to date
+ {language} data downloaded successfully!Downloading
- Already up to
- date!
+ Already up to date!Select data to downloadUp to dateUpdateUpdate all
- You currently do not have any
- Scribe keyboard installed. Please click the Install keyboards button below to install a
- Scribe keyboard and then come back to download the needed data.
- Change
- language
- The data
- you will download will allow you to translate from {source_language} to {target_language}.
- Do you want to change the language you\'ll translate from?
- Use
- {source_language}
+ You currently do not have any Scribe keyboard installed. Please click the Install keyboards button below to install a Scribe keyboard and then come back to download the needed data.
+ Change language
+ The data you will download will allow you to translate from {source_language} to {target_language}. Do you want to change the language you\'ll translate from?
+ Use {source_language}Update dataCheck for new dataRegularly update data
- Follow the directions below to install
- Scribe keyboards on your device.
+ Follow the directions below to install Scribe keyboards on your device.Open keyboard settingsKeyboardsOpen Scribe settings
@@ -221,9 +114,7 @@
Keyboard installationInstallationTutorial chapters
- This quick tutorial will show you how to
- use Scribe to support writing in your second language.\nMake sure you select the desired
- Scribe keyboard by pressing 🌐 when typing.
+ This quick tutorial will show you how to use Scribe to support writing in your second language.\nMake sure you select the desired Scribe keyboard by pressing 🌐 when typing.Finish tutorialNextNot quite! Try writing {expected_word}.
@@ -233,101 +124,68 @@
Verb conjugationWord translationInstall keyboards
- Settings for the app\'s language and
- interface are found here.
- Annotate
- suggest/complete
- Underline
- suggestions and completions to show their genders as you type.
+ Settings for the app\'s language and interface are found here.
+ Annotate suggest/complete
+ Underline suggestions and completions to show their genders as you type.Autosuggest emojis
- Turn on
- emoji suggestions and completions for more expressive typing.
- Default emoji skin
- tone
- Skin tone to
- be used
- Set a
- default skin tone for emoji autosuggestions and completions.
- Word for word
- deletion on long press
- Delete
- text word by word when the delete key is pressed and held.
+ Turn on emoji suggestions and completions for more expressive typing.
+ Default emoji skin tone
+ Skin tone to be used
+ Set a default skin tone for emoji autosuggestions and completions.
+ Word for word deletion on long press
+ Delete text word by word when the delete key is pressed and held.Double space periods
- Automatically
- insert a period when the space key is pressed twice.
- Hold for alternate
- characters
- Select
- alternate characters by holding keys and dragging to the desired character.
+ Automatically insert a period when the space key is pressed twice.
+ Hold for alternate characters
+ Select alternate characters by holding keys and dragging to the desired character.Show popup on keypress
- Display a
- popup of keys as they\'re pressed.
- Delete punctuation
- spacing
- Remove
- excess spaces before punctuation marks.
+ Display a popup of keys as they\'re pressed.
+ Delete punctuation spacing
+ Remove excess spaces before punctuation marks.FunctionalityPlay sound on key press
- Have the device play a
- sound when keys are pressed.
+ Have the device play a sound when keys are pressed.Vibrate on key press
- Have the device vibrate
- when keys are pressed.
+ Have the device vibrate when keys are pressed.
+ Clipboard key on keyboard
+ Include a key to access the clipboard on the main keyboard.Default currency symbol
- Symbol for the 123
- keys
- Select which
- currency symbol appears on the number keys.
+ Symbol for the 123 keys
+ Select which currency symbol appears on the number keys.Default keyboardLayout to use
- Select a keyboard
- layout that suits your typing preference and language needs.
- Disable accent
- characters
- Remove
- accented letter keys on the primary keyboard layout.
+ Select a keyboard layout that suits your typing preference and language needs.
+ Disable accent characters
+ Remove accented letter keys on the primary keyboard layout.
+ Floating key on keyboard
+ Include a key to float the keyboard UI on the main keyboard.Period and comma on ABC
- Include period and
- comma keys on the main keyboard for convenient typing.
+ Include period and comma keys on the main keyboard for convenient typing.LayoutSelect installed keyboard
- You\'ve
- changed your source translation language. Would you like to download new data so that you
- can translate from {source_language}?
- Keep
- {source_language}
+ You\'ve changed your source translation language. Would you like to download new data so that you can translate from {source_language}?
+ Keep {source_language}Select language
- What the source
- language is
+ What the source language isTranslation language
- Change the
- language to translate from.
+ Change the language to translate from.Translation source language
- Settings for the app and installed
- language keyboards are found here.
+ Settings for the app and installed language keyboards are found here.Dark mode
- Change the application display
- to dark mode.
+ Change the application display to dark mode.App languageSelect language for app texts
- You only
- have one language installed on your device. Please install more languages in Settings and
- then you can select different localizations of Scribe.
- Only one
- device language
- Change which language the Scribe
- app is in.
+ You only have one language installed on your device. Please install more languages in Settings and then you can select different localizations of Scribe.
+ Only one device language
+ Change which language the Scribe app is in.High color contrast
- Increase color contrast
- for improved accessibility and a clearer viewing experience.
+ Increase color contrast for improved accessibility and a clearer viewing experience.Increase app text size
- Increase the size of menu
- texts for better readability.
+ Increase the size of menu texts for better readability.App settingsCancelOK
- You are selecting a theme that goes
- against your system default. Are you sure you want to proceed?
+ You are selecting a theme that goes against your system default. Are you sure you want to proceed?Theme overrideSettingsTranslation
From 6ae29ad1a220bde1ff24140a486e807c25ce2828 Mon Sep 17 00:00:00 2001
From: Prince Yadav <66916296+prince-0408@users.noreply.github.com>
Date: Mon, 6 Jul 2026 05:00:16 +0530
Subject: [PATCH 10/10] feat: show floating keyboard toggle button in search
bars
---
.../main/java/be/scri/helpers/KeyboardBase.kt | 26 +++++++++----------
1 file changed, 12 insertions(+), 14 deletions(-)
diff --git a/app/src/main/java/be/scri/helpers/KeyboardBase.kt b/app/src/main/java/be/scri/helpers/KeyboardBase.kt
index 101ca294f..54665c8a2 100644
--- a/app/src/main/java/be/scri/helpers/KeyboardBase.kt
+++ b/app/src/main/java/be/scri/helpers/KeyboardBase.kt
@@ -595,19 +595,17 @@ class KeyboardBase {
key.gap = 0
}
- if (!isSearchBar) {
- if (key.code == KEYCODE_MODE_CHANGE) {
- key.width = (mDisplayWidth * 0.115).toInt()
- } else if (currentRow.mKeys.any { it.code == KEYCODE_FLOAT_TOGGLE }) {
- if (key.code == ','.code) {
- 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()
- }
+ if (key.code == KEYCODE_MODE_CHANGE) {
+ key.width = (mDisplayWidth * 0.115).toInt()
+ } else if (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()
}
}
}
@@ -649,7 +647,7 @@ class KeyboardBase {
if (x > mMinWidth) {
mMinWidth = x
}
- if (key.code == KEYCODE_MODE_CHANGE && !isSearchBar) {
+ if (key.code == KEYCODE_MODE_CHANGE) {
val floatKey = Key(currentRow!!)
floatKey.code = KEYCODE_FLOAT_TOGGLE
floatKey.width = (mDisplayWidth * 0.1).toInt()