diff --git a/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt b/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt
index 818e8feb4..4927cac63 100644
--- a/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt
+++ b/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt
@@ -104,6 +104,10 @@ class KeyHandler(
handleModeChangeKey()
true
}
+ KeyboardBase.KEYCODE_FLOAT_TOGGLE -> {
+ ime.toggleFloatingMode()
+ true
+ }
KeyboardBase.KEYCODE_SPACE -> handleSpaceKeyPress(previousWasLastKeySpace)
in KeyboardBase.NAVIGATION_KEYS -> {
handleNavigationKey(code)
diff --git a/app/src/keyboards/java/be/scri/helpers/KeyboardLanguageMappingConstants.kt b/app/src/keyboards/java/be/scri/helpers/KeyboardLanguageMappingConstants.kt
index 28984886f..9ae3997d8 100644
--- a/app/src/keyboards/java/be/scri/helpers/KeyboardLanguageMappingConstants.kt
+++ b/app/src/keyboards/java/be/scri/helpers/KeyboardLanguageMappingConstants.kt
@@ -49,4 +49,16 @@ object KeyboardLanguageMappingConstants {
"RU" to RUInterfaceVariables.PLURAL_KEY_LBL,
"SV" to SVInterfaceVariables.PLURAL_KEY_LBL,
)
+
+ val floatPlaceholder =
+ mapOf(
+ "EN" to "Float",
+ "ES" to "Flotante",
+ "DE" to "Schweben",
+ "IT" to "Fluttuante",
+ "FR" to "Flottant",
+ "PT" to "Flutuante",
+ "RU" to "Плавающая",
+ "SV" to "Flytande",
+ )
}
diff --git a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
index 92e049590..cb518e5e0 100644
--- a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
+++ b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
@@ -56,6 +56,10 @@ class KeyboardUIManager(
fun onCloseClicked()
+ fun onFloatClicked()
+
+ fun isFloatingModeActive(): Boolean
+
fun onEmojiSelected(emoji: String)
fun onSuggestionClicked(suggestion: String)
@@ -74,6 +78,8 @@ class KeyboardUIManager(
fun isNumericKeyboardActive(): Boolean
+ fun getKeyboardWidth(): Int
+
fun onClipboardSuggestionClicked()
}
@@ -82,6 +88,8 @@ class KeyboardUIManager(
// UI Elements
var pluralBtn: Button? = binding.pluralBtn
+ var floatBtn: Button? = null
+ var separatorFloat: View? = null
var emojiBtnPhone1: Button? = binding.emojiBtnPhone1
var emojiSpacePhone: View? = binding.emojiSpacePhone
var emojiBtnPhone2: Button? = binding.emojiBtnPhone2
@@ -220,6 +228,7 @@ class KeyboardUIManager(
listOf(binding.translateBtn, binding.conjugateBtn, binding.pluralBtn).forEachIndexed { index, button ->
button.visibility = View.VISIBLE
button.background = null
+ button.foreground = null
button.setTextColor(textColor)
button.text = HintUtils.getBaseAutoSuggestions(language).getOrNull(index)
button.isAllCaps = false
@@ -289,11 +298,31 @@ class KeyboardUIManager(
button.backgroundTintList = ContextCompat.getColorStateList(context, R.color.theme_scribe_blue)
button.setTextColor(buttonTextColor)
button.textSize = GeneralKeyboardIME.SUGGESTION_SIZE
+ button.isAllCaps = false
}
- binding.translateBtn.text = translatePlaceholder[langAlias] ?: "Translate"
- binding.conjugateBtn.text = conjugatePlaceholder[langAlias] ?: "Conjugate"
- binding.pluralBtn.text = pluralPlaceholder[langAlias] ?: "Plural"
+ val isFloating = listener.isFloatingModeActive()
+ if (isFloating) {
+ binding.translateBtn.text = ""
+ binding.conjugateBtn.text = ""
+ binding.pluralBtn.text = ""
+
+ binding.translateBtn.foreground = ContextCompat.getDrawable(context, R.drawable.ic_translate_command)
+ binding.conjugateBtn.foreground = ContextCompat.getDrawable(context, R.drawable.ic_conjugate_command)
+ binding.pluralBtn.foreground = ContextCompat.getDrawable(context, R.drawable.ic_plural_command)
+
+ binding.translateBtn.foregroundGravity = android.view.Gravity.CENTER
+ binding.conjugateBtn.foregroundGravity = android.view.Gravity.CENTER
+ binding.pluralBtn.foregroundGravity = android.view.Gravity.CENTER
+ } else {
+ binding.translateBtn.text = translatePlaceholder[langAlias] ?: "Translate"
+ binding.conjugateBtn.text = conjugatePlaceholder[langAlias] ?: "Conjugate"
+ binding.pluralBtn.text = pluralPlaceholder[langAlias] ?: "Plural"
+
+ binding.translateBtn.foreground = null
+ binding.conjugateBtn.foreground = null
+ binding.pluralBtn.foreground = null
+ }
val separatorColor = (if (isUserDarkMode) GeneralKeyboardIME.DARK_THEME else GeneralKeyboardIME.LIGHT_THEME).toColorInt()
binding.separator2.setBackgroundColor(separatorColor)
@@ -610,7 +639,8 @@ class KeyboardUIManager(
*/
fun initializeKeyboard(xmlId: Int) {
val enterKeyType = listener.getCurrentEnterKeyType()
- keyboard = KeyboardBase(context, xmlId, enterKeyType)
+ val width = listener.getKeyboardWidth()
+ keyboard = KeyboardBase(context, xmlId, enterKeyType, width)
keyboardView.setKeyboard(keyboard!!)
keyboardView.mOnKeyboardActionListener = listener.onKeyboardActionListener()
keyboardView.requestLayout()
@@ -678,6 +708,10 @@ class KeyboardUIManager(
binding.separator4.visibility = View.GONE
binding.separator5.visibility = View.GONE
binding.separator6.visibility = View.GONE
+
+ binding.translateBtn.foreground = null
+ binding.conjugateBtn.foreground = null
+ pluralBtn?.foreground = null
}
/**
diff --git a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
index 4497353af..ea8e774fa 100644
--- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
+++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
@@ -8,6 +8,8 @@ import android.content.Context
import android.content.Intent
import android.database.sqlite.SQLiteException
import android.graphics.Color
+import android.graphics.Rect
+import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.LayerDrawable
import android.graphics.drawable.RippleDrawable
@@ -20,7 +22,10 @@ import android.text.InputType.TYPE_CLASS_PHONE
import android.text.InputType.TYPE_MASK_CLASS
import android.util.Log
import android.view.KeyEvent
+import android.view.MotionEvent
import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.EditorInfo.IME_ACTION_NONE
import android.view.inputmethod.EditorInfo.IME_FLAG_NO_ENTER_ACTION
@@ -35,6 +40,7 @@ import androidx.core.graphics.toColorInt
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
import be.scri.R
import be.scri.activities.MainActivity
import be.scri.databinding.InputMethodViewBinding
@@ -246,7 +252,7 @@ abstract class GeneralKeyboardIME(
keyboardView = uiManager.keyboardView
// Initial keyboard setup.
- keyboard = KeyboardBase(this, getKeyboardLayoutXML(), enterKeyType)
+ keyboard = KeyboardBase(this, getKeyboardLayoutXML(), enterKeyType, getKeyboardWidth())
keyboardView?.apply {
setVibrate = getIsVibrateEnabled(applicationContext, language)
@@ -259,6 +265,12 @@ abstract class GeneralKeyboardIME(
currentState = ScribeState.IDLE
saveConjugateModeType("none")
+ viewBinding.root.post {
+ disableParentClipping(viewBinding.root)
+ }
+ initFloatingMode()
+ setupFloatingDragListener()
+
refreshUI()
return viewBinding.root
@@ -286,21 +298,53 @@ abstract class GeneralKeyboardIME(
*/
override fun onComputeInsets(outInsets: Insets) {
super.onComputeInsets(outInsets)
- // Access root view via UI manager if initialized.
if (this::uiManager.isInitialized) {
val inputView = uiManager.binding.root
if (inputView.visibility == View.VISIBLE && inputView.height > 0) {
val location = IntArray(2)
inputView.getLocationInWindow(location)
- outInsets.visibleTopInsets = location[1]
- outInsets.contentTopInsets = location[1]
- outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_VISIBLE
+
+ if (isFloatingMode) {
+ // In floating mode, report zero insets so Android doesn't
+ // push app content up or render IME chrome (∨ / 🌐 buttons)
+ // below the card. The touchable region is restricted to the
+ // card bounds so taps outside reach the underlying app.
+ outInsets.visibleTopInsets = inputView.height
+ outInsets.contentTopInsets = inputView.height
+ outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_REGION
+
+ val card = binding.keyboardCard
+ val density = resources.displayMetrics.density
+ if (card.width > 0 && card.height > 0) {
+ val centerX = card.left + card.width / 2f + card.translationX
+ val centerY = card.top + card.height / 2f + card.translationY
+ val visualW = card.width * card.scaleX
+ val visualH = card.height * card.scaleY
+ val left = (centerX - visualW / 2f).toInt()
+ val top = (centerY - visualH / 2f).toInt()
+ val right = (centerX + visualW / 2f).toInt()
+ val bottom = (centerY + visualH / 2f).toInt()
+
+ val rect = Rect(left, top, right, bottom)
+ // Expand touchable region slightly to allow resizing handles to be clickable
+ val margin = (25 * density).toInt()
+ rect.inset(-margin, -margin)
+ outInsets.touchableRegion.set(rect)
+ } else {
+ outInsets.touchableRegion.setEmpty()
+ }
+ } else {
+ outInsets.visibleTopInsets = location[1]
+ outInsets.contentTopInsets = location[1]
+ outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_VISIBLE
+ }
}
}
}
override fun onWindowShown() {
super.onWindowShown()
+ applyFloatingModeState()
applyNavBarColor()
keyboardView?.setPreview = isShowPopupOnKeypressEnabled(applicationContext, language)
keyboardView?.setVibrate = getIsVibrateEnabled(applicationContext, language)
@@ -333,7 +377,7 @@ abstract class GeneralKeyboardIME(
loadLanguageData()
- keyboard = KeyboardBase(this, keyboardXml, enterKeyType)
+ keyboard = KeyboardBase(this, keyboardXml, enterKeyType, getKeyboardWidth())
keyboardView?.setKeyboard(keyboard!!)
if (this::uiManager.isInitialized && keyboardXml == R.xml.keys_symbols) {
@@ -485,7 +529,7 @@ abstract class GeneralKeyboardIME(
override fun onActionUp() {
if (switchToLetters) {
keyboardMode = keyboardLetters
- keyboard = KeyboardBase(this, getKeyboardLayoutXML(), enterKeyType)
+ keyboard = KeyboardBase(this, getKeyboardLayoutXML(), enterKeyType, getKeyboardWidth())
val editorInfo = currentInputEditorInfo
if (editorInfo != null && editorInfo.inputType != InputType.TYPE_NULL && keyboard?.mShiftState != SHIFT_ON_PERMANENT) {
if (currentInputConnection.getCursorCapsMode(editorInfo.inputType) != 0) {
@@ -607,36 +651,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)
+ }
}
}
}
@@ -753,6 +827,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)
@@ -1240,7 +1320,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)
@@ -1268,7 +1348,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) {
@@ -1775,6 +1855,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)
@@ -1973,6 +2054,513 @@ abstract class GeneralKeyboardIME(
fun disableAutoSuggest() = uiManager.disableAutoSuggest(language)
+ // MARK: Floating Keyboard Integration
+
+ override fun getKeyboardWidth(): Int =
+ if (isFloatingMode) {
+ val density = resources.displayMetrics.density
+ val screenWidth = resources.displayMetrics.widthPixels
+ val floatWidth = (320f * density).toInt()
+ Math.min(floatWidth, (screenWidth * 0.85f).toInt())
+ } else {
+ resources.displayMetrics.widthPixels
+ }
+
+ private fun recreateKeyboard() {
+ if (!this::uiManager.isInitialized) return
+ val xmlId = getCurrentKeyboardLayoutXML()
+ val currentShiftState = keyboard?.mShiftState ?: SHIFT_OFF
+ keyboard = KeyboardBase(this, xmlId, enterKeyType, getKeyboardWidth())
+ keyboard?.setShifted(currentShiftState)
+ keyboardView?.setKeyboard(keyboard!!)
+
+ if (xmlId == R.xml.keys_symbols) {
+ uiManager.setupCurrencySymbol(language)
+ }
+ keyboardView?.invalidateAllKeys()
+ }
+
+ var isFloatingMode: Boolean = false
+ private set
+
+ private var isUpdatePending = false
+ private var lastAppliedFloatingMode: Boolean? = null
+
+ fun initFloatingMode() {
+ isFloatingMode = PreferencesHelper.getIsFloatingModeEnabled(this, language)
+ lastAppliedFloatingMode = null
+ applyFloatingModeState()
+ }
+
+ fun toggleFloatingMode() {
+ isFloatingMode = !isFloatingMode
+ PreferencesHelper.setIsFloatingModeEnabled(this, language, isFloatingMode)
+ // Reset the cached mode so applyFloatingModeState always treats this as a change
+ lastAppliedFloatingMode = null
+ applyFloatingModeState()
+ window?.window?.decorView?.requestLayout()
+ }
+
+ 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
+ }
+ }
+ }
+
override fun onClipboardSuggestionClicked() {
latestClipText?.let { text ->
currentInputConnection?.commitText(text, 1)
@@ -2052,3 +2640,12 @@ abstract class GeneralKeyboardIME(
binding.clipboardItemsList.visibility = if (items.isEmpty()) android.view.View.GONE else android.view.View.VISIBLE
}
}
+
+private fun Float.coerceInSafe(
+ bound1: Float,
+ bound2: Float,
+): Float {
+ val minVal = if (bound1 < bound2) bound1 else bound2
+ val maxVal = if (bound1 > bound2) bound1 else bound2
+ return this.coerceIn(minVal, maxVal)
+}
diff --git a/app/src/main/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/java/be/scri/helpers/KeyboardBase.kt b/app/src/main/java/be/scri/helpers/KeyboardBase.kt
index 70aa1a9ad..54665c8a2 100644
--- a/app/src/main/java/be/scri/helpers/KeyboardBase.kt
+++ b/app/src/main/java/be/scri/helpers/KeyboardBase.kt
@@ -37,6 +37,8 @@ class KeyboardBase {
val keyboardLetters: Int
fun isSearchBar(): Boolean
+
+ fun isFloatingModeActive(): Boolean
}
/** Horizontal gap default for all rows */
@@ -81,6 +83,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
@@ -412,8 +415,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
@@ -591,6 +595,21 @@ class KeyboardBase {
key.gap = 0
}
+ 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()
+ }
+ }
+ }
+
mKeys!!.add(key)
if (key.code == KEYCODE_ENTER) {
val enterResourceId =
@@ -628,6 +647,29 @@ class KeyboardBase {
if (x > mMinWidth) {
mMinWidth = x
}
+ if (key.code == KEYCODE_MODE_CHANGE) {
+ val floatKey = Key(currentRow!!)
+ floatKey.code = KEYCODE_FLOAT_TOGGLE
+ floatKey.width = (mDisplayWidth * 0.1).toInt()
+ floatKey.gap = (mDisplayWidth * 0.005).toInt()
+ floatKey.x = x + floatKey.gap
+ floatKey.y = y
+ floatKey.height = currentRow.defaultHeight
+
+ val isFloating = provider?.isFloatingModeActive() == true
+ val floatResourceId =
+ if (isFloating) {
+ R.drawable.ic_keyboard_dismiss
+ } else {
+ R.drawable.ic_float_keyboard
+ }
+ floatKey.icon = context.resources.getDrawable(floatResourceId, context.theme)
+ floatKey.icon?.setBounds(0, 0, floatKey.icon!!.intrinsicWidth, floatKey.icon!!.intrinsicHeight)
+
+ mKeys!!.add(floatKey)
+ currentRow.mKeys.add(floatKey)
+ x += floatKey.gap + floatKey.width
+ }
} else if (inRow) {
inRow = false
y += currentRow!!.defaultHeight
diff --git a/app/src/main/java/be/scri/helpers/PreferencesHelper.kt b/app/src/main/java/be/scri/helpers/PreferencesHelper.kt
index b449b320f..2c138ba12 100644
--- a/app/src/main/java/be/scri/helpers/PreferencesHelper.kt
+++ b/app/src/main/java/be/scri/helpers/PreferencesHelper.kt
@@ -572,4 +572,79 @@ 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/java/be/scri/views/KeyboardView.kt b/app/src/main/java/be/scri/views/KeyboardView.kt
index be9755eb9..babb111e6 100644
--- a/app/src/main/java/be/scri/views/KeyboardView.kt
+++ b/app/src/main/java/be/scri/views/KeyboardView.kt
@@ -750,6 +750,17 @@ class KeyboardView
}
}
+ override fun onSizeChanged(
+ w: Int,
+ h: Int,
+ oldw: Int,
+ oldh: Int,
+ ) {
+ super.onSizeChanged(w, h, oldw, oldh)
+ mKeyboardChanged = true
+ invalidateAllKeys()
+ }
+
/**
* Compute the average distance between adjacent keys (horizontally and vertically)
* and square it to get the proximity threshold.
@@ -1142,7 +1153,8 @@ class KeyboardView
code == KEYCODE_DELETE ||
code == KEYCODE_SHIFT ||
code == KEYCODE_TAB ||
- code == KeyboardBase.KEYCODE_CLIPBOARD
+ code == KeyboardBase.KEYCODE_CLIPBOARD ||
+ code == KeyboardBase.KEYCODE_FLOAT_TOGGLE
if (isIconOnlyKey) {
key.icon!!.applyColorFilter(mTextColor)
}
diff --git a/app/src/main/res/drawable/floating_keyboard_background.xml b/app/src/main/res/drawable/floating_keyboard_background.xml
new file mode 100644
index 000000000..4b34336d7
--- /dev/null
+++ b/app/src/main/res/drawable/floating_keyboard_background.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_conjugate_command.xml b/app/src/main/res/drawable/ic_conjugate_command.xml
new file mode 100644
index 000000000..e23f6748c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_conjugate_command.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_drag_handle.xml b/app/src/main/res/drawable/ic_drag_handle.xml
new file mode 100644
index 000000000..9f5cdf1c9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_drag_handle.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_float_keyboard.xml b/app/src/main/res/drawable/ic_float_keyboard.xml
new file mode 100644
index 000000000..308a85fc9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_float_keyboard.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_keyboard_dismiss.xml b/app/src/main/res/drawable/ic_keyboard_dismiss.xml
new file mode 100644
index 000000000..f371a2722
--- /dev/null
+++ b/app/src/main/res/drawable/ic_keyboard_dismiss.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_plural_command.xml b/app/src/main/res/drawable/ic_plural_command.xml
new file mode 100644
index 000000000..450ca615a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_plural_command.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_resize_corner.xml b/app/src/main/res/drawable/ic_resize_corner.xml
new file mode 100644
index 000000000..868810a6d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_resize_corner.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_translate_command.xml b/app/src/main/res/drawable/ic_translate_command.xml
new file mode 100644
index 000000000..5120996c6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_translate_command.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/input_method_view.xml b/app/src/main/res/layout/input_method_view.xml
index 7a821b972..fe4a6de87 100644
--- a/app/src/main/res/layout/input_method_view.xml
+++ b/app/src/main/res/layout/input_method_view.xml
@@ -12,7 +12,20 @@ GeneralKeyboardIME.kt
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/keyboard_holder"
android:layout_width="match_parent"
- android:layout_height="wrap_content">
+ android:layout_height="wrap_content"
+ android:clipChildren="false"
+ android:clipToPadding="false">
+
+
+ app:layout_constraintTop_toTopOf="parent">
+
+
@@ -227,12 +249,12 @@ GeneralKeyboardIME.kt
android:id="@+id/plural_btn"
android:layout_width="0dp"
android:layout_height="@dimen/command_button_height"
- android:layout_marginEnd="@dimen/tiny_margin"
android:background="@drawable/cmd_key_background_rounded"
android:contentDescription="@string/command_bar"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
+ android:layout_marginEnd="@dimen/tiny_margin"
app:layout_constraintStart_toEndOf="@+id/separator_3"
app:layout_constraintTop_toTopOf="parent" />
@@ -690,7 +712,7 @@ GeneralKeyboardIME.kt
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 c299acc6d..416d1378c 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -33,12 +33,10 @@
name="conjugate_keyboard_color">@color/light_keyboard_bg_color
- @color/dark_scribe_color
- @color/light_scribe_color
- #808080
+ @color/dark_scribe_color
+ #FF555555
+ @color/light_scribe_color
+ #808080#FFFFFFFF
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
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 38134b505..e1b42d065 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