Compare commits

..

8 Commits

Author SHA1 Message Date
Patrick Goldinger
970b5eb82a Release v0.2.4 2020-11-22 21:46:36 +01:00
Patrick Goldinger
a2ceed4521 Improve Smartbar layout / Add clipboard content suggestions (#38)
- This commit adds clipboard content suggestions. These suggestions do
  only show if suggestions in general are turned on.
- The suggestions show for both text and images in the clipboard, but
  do currently only work for text.
- Clipboard/Cursor row is now a proper KeyboardView, which gets rid of
  the hardcoded keys for the arrows / clipboard commands.
- Fix errors in doc strings.
- Fix other logic errors in TextInputManager and EditorInstance.
2020-11-22 21:25:35 +01:00
Patrick Goldinger
6d7825e129 Add crash handler and error detail form
- This crash handler catches nearly all uncaught errors and notifies
  the user about it. If an uncaught error occurs in the FlorisBoard
  service initialization, the handler detects this and switches to
  another installed keyboard.
- The error detail form contains the captured stacktrace and adds
  a copy to clipboard functionality as well as a button to the
  GitHub issue tracker.
2020-11-19 23:59:23 +01:00
Patrick Goldinger
10c1a82995 Rework core to fix potential crashes when entering text 2020-11-17 18:34:57 +01:00
Patrick Goldinger
267a39e870 Add basic clipboard text suggestion to Smartbar (#38) 2020-11-16 23:52:51 +01:00
Patrick Goldinger
f6fcbbcc34 Update project dependencies and build.gradle 2020-11-16 18:32:55 +01:00
Patrick Goldinger
f98b3cec4b Improve layout and behavior of number row in Smartbar (#31)
- Number row is now a proper keyboard instead of a LinearLayout with
  hardcoded keys.
- Number row takes whole Smartbar width by hiding the Smartbar arrow
  (improves size per number key, which allows it to be more easily
  touchable).
2020-11-15 23:43:16 +01:00
Patrick Goldinger
e5a942be9f Add support for raw text editors (e.g. terminals, ...)
- FlorisBoard is now able to perform input on raw input editors (editors
  which either have an incomplete, faulty or purposely simple
  implementation).
- Especially targeted at terminal apps, as these apps do not manage the
  state of the input but only forward it.
2020-11-13 20:56:41 +01:00
38 changed files with 1292 additions and 499 deletions

View File

@@ -27,8 +27,8 @@ assignees: ''
3. Scroll down to '....'
4. See error
<!--
<!-- (remove this line if you paste a log)
```
If applicable, paste the captured debug log here.
```
-->
(remove this line if you paste a log) -->

View File

@@ -77,4 +77,5 @@ is and how to solve it.
### Capturing ADB debug logs
[[ TODO: create tutorial ]]
Logs are captured by FlorisBoard's crash handler, which gives you the
ability to copy it to the clipboard and paste it in GitHub.

View File

@@ -10,8 +10,8 @@ android {
applicationId "dev.patrickgold.florisboard"
minSdkVersion 23
targetSdkVersion 29
versionCode 15
versionName "0.2.3"
versionCode 16
versionName "0.2.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -33,18 +33,18 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0'
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.12'
testImplementation 'androidx.test:core:1.2.0'
testImplementation 'androidx.test:core:1.3.0'
testImplementation 'org.mockito:mockito-core:1.10.19'
testImplementation 'org.mockito:mockito-inline:2.13.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation 'com.google.android:flexbox:2.0.1'
implementation "com.squareup.moshi:moshi-kotlin:1.9.2"
implementation 'com.google.android.material:material:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7"
implementation 'com.jaredrummler:colorpicker:1.1.0'

View File

@@ -21,6 +21,7 @@
<uses-permission android:name="android.permission.VIBRATE"/>
<application
android:name=".ime.core.FlorisApplication"
android:allowBackup="false"
android:extractNativeLibs="false"
android:icon="@mipmap/ic_launcher"
@@ -90,6 +91,13 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/SettingsTheme"/>
<!-- Crash Dialog Activity -->
<activity
android:name="dev.patrickgold.florisboard.crashutility.CrashDialogActivity"
android:icon="@mipmap/ic_launcher"
android:label="@string/crash_dialog__title"
android:theme="@style/CrashDialogTheme"/>
</application>
</manifest>

View File

@@ -0,0 +1,15 @@
{
"type": "extension",
"name": "clipboard_cursor_row",
"direction": "ltr",
"arrangement": [
[
{ "code": -135, "label": "clipboard_select_all", "type": "enter_editing" },
{ "code": -130, "label": "clipboard_copy", "type": "enter_editing" },
{ "code": -20, "label": "arrow_left", "type": "navigation" },
{ "code": -21, "label": "arrow_right", "type": "navigation" },
{ "code": -131, "label": "clipboard_cut", "type": "enter_editing" },
{ "code": -132, "label": "clipboard_paste", "type": "enter_editing" }
]
]
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (C) 2020 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.crashutility
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.databinding.CrashDialogBinding
class CrashDialogActivity : AppCompatActivity() {
private lateinit var binding: CrashDialogBinding
private var stacktrace: String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = CrashDialogBinding.inflate(layoutInflater)
setContentView(binding.root)
stacktrace = CrashUtility.getUnhandledStacktrace(this)
binding.stacktrace.text = stacktrace
binding.copyToClipboard.setOnClickListener {
val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE)
if (clipboardManager != null && clipboardManager is ClipboardManager) {
clipboardManager.setPrimaryClip(ClipData.newPlainText(stacktrace, stacktrace))
}
}
binding.openBugReportForm.setOnClickListener {
val browserIntent = Intent(
Intent.ACTION_VIEW,
Uri.parse(resources.getString(R.string.florisboard__issue_tracker_new_issue_url))
)
startActivity(browserIntent)
}
binding.close.setOnClickListener {
finish()
}
}
}

View File

@@ -0,0 +1,412 @@
/*
* Copyright (C) 2020 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.crashutility
import android.annotation.SuppressLint
import android.app.*
import android.app.Application.ActivityLifecycleCallbacks
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Process
import android.util.Log
import android.view.inputmethod.InputMethodManager
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import java.io.File
import java.io.PrintWriter
import java.io.StringWriter
import java.io.Writer
import java.lang.ref.WeakReference
import kotlin.system.exitProcess
/**
* Abstract class which holds several static methods used for handling unexpected errors.
*
* Parts of this class (especially the install() function and the uncaughtException() handler) have
* been inspired by the great CustomActivityOnCrash library:
* https://github.com/Ereza/CustomActivityOnCrash (licensed under Apache 2.0)
* https://github.com/Ereza/CustomActivityOnCrash/blob/master/library/src/main/java/cat/ereza/customactivityoncrash/CustomActivityOnCrash.java
*/
abstract class CrashUtility private constructor() {
companion object {
private const val SHARED_PREFS_FILE = "crash_utility"
private const val SHARED_PREFS_LAST_CRASH_TIMESTAMP = "last_crash_timestamp"
private const val NOTIFICATION_CHANNEL_ID = "dev.patrickgold.florisboard.crashutility"
private const val NOTIFICATION_ID = 0xFBAD0100
private const val UNHANDLED_STACKTRACE_FILE_EXT = "stacktrace"
private const val TAG = "CrashUtility"
private var lastActivityCreated: WeakReference<Activity?> = WeakReference(null)
/**
* Installs the CrashUtility crash handler for the given package [context]. Also registers
* a notification channel for devices with Android 8.0+.
*
* @param context The current package context. If null is supplied, this function does
* nothing.
* @return True if the installation was successful, false otherwise.
*/
fun install(context: Context?): Boolean {
if (context == null) {
Log.e(
TAG,
"install($context): Can't install crash handler with a null Context object, doing nothing!"
)
return false
}
val oldHandler = Thread.getDefaultUncaughtExceptionHandler()
if (oldHandler is UncaughtExceptionHandler) {
Log.i(TAG, "install($context): Crash handler is already installed, doing nothing!")
} else {
val application = context.applicationContext
if (application != null && application is Application) {
try {
Thread.setDefaultUncaughtExceptionHandler(
UncaughtExceptionHandler(
WeakReference(application),
WeakReference(oldHandler),
application.filesDir.absolutePath
)
)
Log.i(
TAG,
"install($context): Successfully installed crash handler for this application!"
)
} catch (e: SecurityException) {
Log.e(
TAG,
"install($context): Failed to install crash handler, probably due to missing runtime permission 'setDefaultUncaughtExceptionHandler':\n$e"
)
return false
} catch (e: Exception) {
Log.e(
TAG,
"install($context): Failed to install crash handler due to an unspecified error:\n$e"
)
return false
}
application.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
override fun onActivityCreated(
activity: Activity,
savedInstanceState: Bundle?
) {
if (activity !is CrashDialogActivity) {
lastActivityCreated = WeakReference(activity)
}
}
override fun onActivityStarted(activity: Activity) {}
override fun onActivityResumed(activity: Activity) {}
override fun onActivityPaused(activity: Activity) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivitySaveInstanceState(
activity: Activity,
outState: Bundle
) {}
override fun onActivityDestroyed(activity: Activity) {}
})
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE)
if (notificationManager != null && notificationManager is NotificationManager) {
val notificationChannel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
context.resources.getString(R.string.crash_notification_channel__title),
NotificationManager.IMPORTANCE_HIGH
)
notificationManager.createNotificationChannel(notificationChannel)
}
Log.i(
TAG,
"install($context): Successfully created crash handler notification channel!"
)
} catch (e: Exception) {
Log.e(
TAG,
"install($context): Failed to create crash handler notification channel due to an unspecified error:\n$e"
)
}
}
} else {
Log.e(
TAG,
"install($context): Can't install crash handler with a null Application object, doing nothing!"
)
return false
}
}
return true
}
/**
* Reads and returns all unhandled stacktrace files.
*
* @param context The current package context. If null is supplied, this function returns
* an empty string.
* @return All unhandled stacktrace files or an empty string.
*/
fun getUnhandledStacktrace(context: Context?): String {
context ?: return ""
val retString: StringBuilder = StringBuilder()
val ustDir = getUstDir(context)
if (ustDir.isDirectory) {
(ustDir.listFiles { pathname ->
pathname.name.endsWith(".$UNHANDLED_STACKTRACE_FILE_EXT")
})?.forEach { file ->
Log.i(TAG, "Reading unhandled stacktrace: ${file.name}")
retString.append("~~~ ${file.name} ~~~\n\n")
retString.append(readFile(file))
retString.append("\n\n")
file.delete()
}
}
return retString.toString()
}
fun hasUnhandledStacktraceFiles(context: Context): Boolean {
val ustDir = getUstDir(context)
return if (ustDir.isDirectory) {
(ustDir.listFiles { pathname ->
pathname.name.endsWith(".$UNHANDLED_STACKTRACE_FILE_EXT")
})?.isNotEmpty() ?: false
} else {
false
}
}
/**
* Gets the last crash timestamp from the shared preferences.
*
* @param context The current package context. If null is supplied, this function returns
* the default value for the timestamp (0).
* @return The last time crash timestamp or 0.
*/
private fun getLastCrashTimestamp(context: Context?): Long {
context ?: return 0
return context.getSharedPreferences(SHARED_PREFS_FILE, Context.MODE_PRIVATE)
.getLong(SHARED_PREFS_LAST_CRASH_TIMESTAMP, 0)
}
/**
* Sets the last crash timestamp in the shared preferences.
*
* @param context The current package context. If null is supplied, this function does
* nothing.
* @param value The timestamp of the current crash.
*/
@SuppressLint("ApplySharedPref")
private fun setLastCrashTimestamp(context: Context?, value: Long) {
context ?: return
// Note: must use commit() instead of apply(), as the value must be immediately written
// to be possibly instantly read again.
context.getSharedPreferences(SHARED_PREFS_FILE, Context.MODE_PRIVATE)
.edit()
.putLong(SHARED_PREFS_LAST_CRASH_TIMESTAMP, value)
.commit()
}
/**
* Gets a reference to the current unhandled stacktrace directory.
*
* @param context The current package context.
* @return The File object for the directory.
*/
private fun getUstDir(context: Context): File {
val path = context.filesDir.absolutePath
return File(path)
}
/**
* Gets a reference to the stacktrace file for given [timestamp].
*
* @param context The current package context.
* @param timestamp The timestamp of the stacktrace file to get.
* @return The File object for the stacktrace file.
*/
private fun getUstFile(context: Context, timestamp: Long): File {
val path = context.filesDir.absolutePath
return File("$path/$timestamp.$UNHANDLED_STACKTRACE_FILE_EXT")
}
/**
* Push a notification which opens [CrashDialogActivity] with given parameters.
*
* @param context The current package context. If null is supplied, this function does
* nothing.
* @param id The ID of the notification.
* @param title The title of the notification.
* @param body The body of the notification.
*/
private fun pushNotification(context: Context?, id: Int, title: String, body: String) {
context ?: return
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE)
if (notificationManager != null && notificationManager is NotificationManager) {
val notificationBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Notification.Builder(context.applicationContext, NOTIFICATION_CHANNEL_ID)
} else {
@Suppress("DEPRECATION")
Notification.Builder(context.applicationContext).apply {
setPriority(Notification.PRIORITY_MAX)
}
}
val crashDialogIntent = Intent(context, CrashDialogActivity::class.java)
val notification = notificationBuilder.run {
setContentTitle(title)
style = Notification.BigTextStyle().bigText(body)
setContentText(body)
setSmallIcon(android.R.drawable.stat_notify_error)
setContentIntent(PendingIntent.getActivity(context, 0, crashDialogIntent, 0)).setAutoCancel(
true
)
build()
}
notificationManager.notify(id, notification)
}
}
/**
* Push a notification configured for a single crash.
*
* @param context The current package context. If null is supplied, this function does
* nothing.
*/
private fun pushCrashOnceNotification(context: Context?) {
context ?: return
pushNotification(
context,
NOTIFICATION_ID.toInt(),
context.resources.getString(R.string.crash_once_notification__title),
context.resources.getString(R.string.crash_once_notification__body)
)
}
/**
* Push a notification configured for multiple crashes.
*
* @param context The current package context. If null is supplied, this function does
* nothing.
*/
private fun pushCrashMultipleNotification(context: Context?) {
context ?: return
pushNotification(
context,
NOTIFICATION_ID.toInt(),
context.resources.getString(R.string.crash_multiple_notification__title),
context.resources.getString(R.string.crash_multiple_notification__body)
)
}
/**
* Reads a given [file] and returns its content.
*
* @param file The file object.
* @return The contents of the file or an empty string, if the file does not exist.
*/
private fun readFile(file: File): String {
val retText = StringBuilder()
if (file.exists()) {
file.forEachLine { retText.append(it) }
}
return retText.toString()
}
/**
* Writes given [text] to given [file]. If the file already exists, its current content
* will be overwritten.
*
* @param file The file object.
* @param text The text to write to the file.
* @return The contents of the file or an empty string, if the file does not exist.
*/
private fun writeToFile(file: File, text: String) {
try {
file.writeText(text)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
/**
* Custom UncaughtExceptionHandler, which writes the captured stacktrace of the crash to the
* internal storage, pushes a crash notification and kills the current process.
*/
class UncaughtExceptionHandler(
private val application: WeakReference<Application>,
private val oldHandler: WeakReference<Thread.UncaughtExceptionHandler?>,
private val path: String
) : Thread.UncaughtExceptionHandler {
override fun uncaughtException(thread: Thread?, throwable: Throwable?) {
Log.e(TAG, "Detected application crash, executing custom crash handler.")
thread ?: return
throwable ?: return
val timestamp = System.currentTimeMillis()
val result: Writer = StringWriter()
val printWriter = PrintWriter(result)
throwable.printStackTrace(printWriter)
val stacktrace: String = result.toString()
printWriter.close()
val ustFile = File("$path/$timestamp.$UNHANDLED_STACKTRACE_FILE_EXT")
writeToFile(ustFile, stacktrace)
val application = application.get()
if (application != null) {
val lastTimestamp = getLastCrashTimestamp(application)
if (lastTimestamp > 0) {
val lastFile = getUstFile(application, lastTimestamp)
val lastStacktrace = readFile(lastFile)
if (lastStacktrace == stacktrace) {
// Delete last stacktrace if it matches previous unhandled one
lastFile.delete()
}
}
setLastCrashTimestamp(application, timestamp)
if (timestamp - lastTimestamp < 5000) {
pushCrashMultipleNotification(application)
val florisboard = FlorisBoard.getInstanceOrNull()
if (florisboard != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
florisboard.switchToPreviousInputMethod()
} else {
val imm = application.getSystemService(Context.INPUT_METHOD_SERVICE)
if (imm != null && imm is InputMethodManager) {
@Suppress("DEPRECATION")
imm.switchToNextInputMethod(
florisboard.window?.window?.attributes?.token,
false
)
}
}
}
} else {
pushCrashOnceNotification(application)
}
}
val lastActivity = lastActivityCreated.get()
if (lastActivity != null) {
//oldHandler.get()?.uncaughtException(thread, throwable)
lastActivity.finish()
lastActivityCreated.clear()
}
Process.killProcess(Process.myPid())
exitProcess(10)
}
}
}

View File

@@ -16,15 +16,20 @@
package dev.patrickgold.florisboard.ime.core
import android.content.ClipData
import android.content.ClipDescription
import android.content.ClipboardManager
import android.content.Context
import android.inputmethodservice.InputMethodService
import android.net.Uri
import android.os.Build
import android.text.InputType
import android.view.KeyEvent
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.ExtractedTextRequest
import android.view.inputmethod.InputContentInfo
import androidx.annotation.RequiresApi
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import java.lang.StringBuilder
// Constants for detectLastUnicodeCharacterLengthBeforeCursor method
private const val LIGHT_SKIN_TONE = 0x1F3FB
@@ -58,6 +63,7 @@ private val emojiVariationArray: Array<Int> = arrayOf(
* object which also holds the state of the currently focused input editor.
*/
class EditorInstance private constructor(private val ims: InputMethodService?) {
var contentMimeTypes: Array<out String?>? = null
val cursorCapsMode: InputAttributes.CapsMode
get() {
val ic = ims?.currentInputConnection ?: return InputAttributes.CapsMode.NONE
@@ -74,8 +80,8 @@ class EditorInstance private constructor(private val ims: InputMethodService?) {
var isComposingEnabled: Boolean = false
set(v) {
field = v
reevaluate()
if (v) {
reevaluateCurrentWord()
if (v && !isRawInputEditor) {
markComposingRegion(currentWord)
} else {
markComposingRegion(null)
@@ -83,13 +89,22 @@ class EditorInstance private constructor(private val ims: InputMethodService?) {
}
var isNewSelectionInBoundsOfOld: Boolean = false
private set
var isRawInputEditor: Boolean = true
private set
var packageName: String = "undefined"
private set
var selection: Selection = Selection(this)
private set
val cachedText: String
get() = cachedTextInternal.toString()
private var cachedTextInternal: StringBuilder = StringBuilder("")
var cachedText: String = ""
private var clipboardManager: ClipboardManager? = null
init {
val tmpClipboardManager = ims?.getSystemService(Context.CLIPBOARD_SERVICE)
if (tmpClipboardManager != null && tmpClipboardManager is ClipboardManager) {
clipboardManager = tmpClipboardManager
}
}
companion object {
fun default(): EditorInstance {
@@ -99,6 +114,9 @@ class EditorInstance private constructor(private val ims: InputMethodService?) {
fun from(editorInfo: EditorInfo?, ims: InputMethodService?): EditorInstance {
return if (editorInfo == null) { default() } else {
EditorInstance(ims).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
contentMimeTypes = editorInfo.contentMimeTypes
}
imeOptions = ImeOptions.fromImeOptionsInt(editorInfo.imeOptions)
inputAttributes = InputAttributes.fromInputTypeInt(editorInfo.inputType)
packageName = editorInfo.packageName
@@ -112,8 +130,8 @@ class EditorInstance private constructor(private val ims: InputMethodService?) {
}
init {
fetchExtractedTextFromInputConnection()
reevaluate()
updateEditorState()
reevaluateCurrentWord()
}
/**
@@ -123,7 +141,7 @@ class EditorInstance private constructor(private val ims: InputMethodService?) {
oldSelStart: Int, oldSelEnd: Int,
newSelStart: Int, newSelEnd: Int
) {
fetchExtractedTextFromInputConnection()
updateEditorState()
isNewSelectionInBoundsOfOld =
newSelStart >= (oldSelStart - 1) &&
newSelStart <= (oldSelStart + 1) &&
@@ -133,8 +151,8 @@ class EditorInstance private constructor(private val ims: InputMethodService?) {
start = newSelStart
end = newSelEnd
}
reevaluate()
if (selection.isCursorMode && isComposingEnabled) {
reevaluateCurrentWord()
if (selection.isCursorMode && isComposingEnabled && !isRawInputEditor) {
markComposingRegion(currentWord)
} else {
markComposingRegion(null)
@@ -147,10 +165,54 @@ class EditorInstance private constructor(private val ims: InputMethodService?) {
*
* @param text The text to complete in this editor's composing region.
*
* @returns True on success, false if an error occurred or the input connection is invalid.
* @return True on success, false if an error occurred or the input connection is invalid.
*/
fun commitCompletion(text: String): Boolean {
return false
val ic = ims?.currentInputConnection ?: return false
return if (isRawInputEditor) {
false
} else {
ic.beginBatchEdit()
ic.setComposingText(text, 1)
markComposingRegion(null)
updateEditorState()
reevaluateCurrentWord()
ic.endBatchEdit()
true
}
}
/**
* Commits the given [content] to this editor instance and adjusts both the cursor position and
* composing region, if any.
*
* @param content The content to commit.
*
* @return True on success, false if an error occurred or the input connection is invalid.
*/
fun commitContent(content: Uri, description: ClipDescription): Boolean {
val ic = ims?.currentInputConnection ?: return false
val contentMimeTypes = contentMimeTypes
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1 || contentMimeTypes == null || contentMimeTypes.isEmpty()) {
commitText(content.toString())
} else {
var mimeTypesDoMatch = false
for (contentMimeType in contentMimeTypes) {
if (description.hasMimeType(contentMimeType)) {
mimeTypesDoMatch = true
break
}
}
if (mimeTypesDoMatch) {
ic.beginBatchEdit()
markComposingRegion(null)
val ret = ic.commitContent(InputContentInfo(content, description), 0, null)
ic.endBatchEdit()
ret
} else {
commitText(content.toString())
}
}
}
/**
@@ -163,29 +225,24 @@ class EditorInstance private constructor(private val ims: InputMethodService?) {
*
* @param text The text to commit.
*
* @returns True on success, false if an error occurred or the input connection is invalid.
* @return True on success, false if an error occurred or the input connection is invalid.
*/
fun commitText(text: String): Boolean {
val ic = ims?.currentInputConnection ?: return false
ic.beginBatchEdit()
markComposingRegion(null)
if (selection.isCursorMode) {
cachedTextInternal.insert(selection.start, text)
} else if (selection.isSelectionMode) {
cachedTextInternal.replace(selection.start, selection.end, text)
return if (isRawInputEditor) {
ic.commitText(text, 1)
} else {
ic.beginBatchEdit()
markComposingRegion(null)
ic.commitText(text, 1)
updateEditorState()
reevaluateCurrentWord()
if (isComposingEnabled) {
markComposingRegion(currentWord)
}
ic.endBatchEdit()
true
}
selection.apply {
start += text.length
end = start
}
reevaluate()
ic.commitText(text, 1)
if (isComposingEnabled) {
markComposingRegion(currentWord)
}
ic.setSelection(selection.start, selection.end)
ic.endBatchEdit()
return true
}
/**
@@ -193,34 +250,29 @@ class EditorInstance private constructor(private val ims: InputMethodService?) {
* characters inside this selection will be removed, else only the left-most character from
* the cursor's position.
*
* @returns True on success, false if an error occurred or the input connection is invalid.
* @return True on success, false if an error occurred or the input connection is invalid.
*/
fun deleteBackwards(): Boolean {
val ic = ims?.currentInputConnection ?: return false
ic.beginBatchEdit()
markComposingRegion(null)
if (selection.isCursorMode && selection.start > 0) {
val length = detectLastUnicodeCharacterLengthBeforeCursor()
cachedTextInternal.replace(selection.start - length, selection.start, "")
selection.apply {
start -= length
end = start
if (isRawInputEditor) {
return sendSystemKeyEvent(KeyEvent.KEYCODE_DEL)
} else {
ic.beginBatchEdit()
markComposingRegion(null)
if (selection.isCursorMode && selection.start > 0) {
val length = detectLastUnicodeCharacterLengthBeforeCursor()
ic.deleteSurroundingText(length, 0)
} else if (selection.isSelectionMode) {
ic.commitText("", 1)
}
ic.deleteSurroundingText(length, 0)
} else if (selection.isSelectionMode) {
cachedTextInternal.replace(selection.start, selection.end, "")
selection.apply {
end = start
updateEditorState()
reevaluateCurrentWord()
if (isComposingEnabled) {
markComposingRegion(currentWord)
}
ic.commitText("", 1)
ic.endBatchEdit()
return true
}
reevaluate()
if (isComposingEnabled) {
markComposingRegion(currentWord)
}
ic.setSelection(selection.start, selection.end)
ic.endBatchEdit()
return true
}
/**
@@ -231,28 +283,23 @@ class EditorInstance private constructor(private val ims: InputMethodService?) {
* @param n The number of words to delete before the cursor. Must be greater than 0 or this
* method will fail.
*
* @returns True on success, false if an error occurred or the input connection is invalid.
* @return True on success, false if an error occurred or the input connection is invalid.
*/
fun deleteWordsBeforeCursor(n: Int): Boolean {
val ic = ims?.currentInputConnection ?: return false
if (n < 1) {
return false
}
ic.beginBatchEdit()
markComposingRegion(null)
if (currentWord.isValid) {
cachedTextInternal.replace(currentWord.start, currentWord.end, "")
selection.apply {
start = currentWord.start
end = start
return if (n < 1 || isRawInputEditor) {
false
} else {
ic.beginBatchEdit()
markComposingRegion(null)
if (currentWord.isValid) {
ic.setSelection(currentWord.start, currentWord.end)
ic.commitText("", 1)
}
ic.setSelection(currentWord.start, currentWord.end)
ic.commitText("", 1)
reevaluateCurrentWord()
ic.endBatchEdit()
true
}
reevaluate()
ic.setSelection(selection.start, selection.end)
ic.endBatchEdit()
return true
}
/**
@@ -262,15 +309,15 @@ class EditorInstance private constructor(private val ims: InputMethodService?) {
* @param n The number of characters to get after the cursor. Must be greater than 0 or this
* method will fail.
*
* @returns [n] or less characters after the cursor.
* @return [n] or less characters after the cursor.
*/
fun getTextAfterCursor(n: Int): String {
if (!selection.isValid || n < 1) {
if (!selection.isValid || n < 1 || isRawInputEditor) {
return ""
}
val from = selection.end
val to = (selection.end + n).coerceAtMost(cachedTextInternal.length)
return cachedTextInternal.substring(from, to)
val from = selection.end.coerceIn(0, cachedText.length)
val to = (selection.end + n).coerceIn(0, cachedText.length)
return cachedText.substring(from, to)
}
/**
@@ -280,15 +327,84 @@ class EditorInstance private constructor(private val ims: InputMethodService?) {
* @param n The number of characters to get before the cursor. Must be greater than 0 or this
* method will fail.
*
* @returns [n] or less characters after the cursor.
* @return [n] or less characters after the cursor.
*/
fun getTextBeforeCursor(n: Int): String {
if (!selection.isValid || n < 1) {
if (!selection.isValid || n < 1 || isRawInputEditor) {
return ""
}
val from = (selection.start - n).coerceAtLeast(0)
val to = selection.start
return cachedTextInternal.substring(from, to)
val from = (selection.start - n).coerceIn(0, cachedText.length)
val to = selection.start.coerceIn(0, cachedText.length)
return cachedText.substring(from, to)
}
/**
* Performs a cut command on this editor instance and adjusts both the cursor position and
* composing region, if any.
*
* @return True on success, false if an error occurred or the input connection is invalid.
*/
fun performClipboardCut(): Boolean {
return if (selection.isSelectionMode) {
val clipData: ClipData = ClipData.newPlainText(selection.text, selection.text)
clipboardManager?.setPrimaryClip(clipData)
deleteBackwards()
true
} else {
false
}
}
/**
* Performs a copy command on this editor instance and adjusts both the cursor position and
* composing region, if any.
*
* @return True on success, false if an error occurred or the input connection is invalid.
*/
fun performClipboardCopy(): Boolean {
return if (selection.isSelectionMode) {
val clipData: ClipData = ClipData.newPlainText(selection.text, selection.text)
clipboardManager?.setPrimaryClip(clipData)
setSelection(selection.end, selection.end)
true
} else {
false
}
}
/**
* Performs a paste command on this editor instance and adjusts both the cursor position and
* composing region, if any.
*
* @return True on success, false if an error occurred or the input connection is invalid.
*/
fun performClipboardPaste(): Boolean {
val clipData: ClipData? = clipboardManager?.primaryClip
val item: ClipData.Item? = clipData?.getItemAt(0)
return when {
item?.text != null -> {
commitText(item.text.toString())
}
item?.uri != null -> {
commitContent(item.uri, clipData.description)
}
else -> {
false
}
}
}
/**
* Performs an enter key press on the current input editor.
*
* @return True on success, false if an error occurred or the input connection is invalid.
*/
fun performEnter(): Boolean {
return if (isRawInputEditor) {
sendSystemKeyEvent(KeyEvent.KEYCODE_ENTER)
} else {
commitText("\n")
}
}
/**
@@ -296,7 +412,7 @@ class EditorInstance private constructor(private val ims: InputMethodService?) {
*
* @param action The action to be performed on this editor instance.
*
* @returns True on success, false if an error occurred or the input connection is invalid.
* @return True on success, false if an error occurred or the input connection is invalid.
*/
fun performEnterAction(action: ImeOptions.Action): Boolean {
val ic = ims?.currentInputConnection ?: return false
@@ -306,11 +422,10 @@ class EditorInstance private constructor(private val ims: InputMethodService?) {
/**
* Sends a given [keyCode] as a [KeyEvent.ACTION_DOWN].
*
* @param ic The input connection on which this operation should be performed.
* @param keyCode The key code to send, use a key code defined in Android's [KeyEvent], not in
* [KeyCode] or this call may send a weird character, as this key codes do not match!!
*
* @returns True on success, false if an error occurred or the input connection is invalid.
* @return True on success, false if an error occurred or the input connection is invalid.
*/
fun sendSystemKeyEvent(keyCode: Int): Boolean {
val ic = ims?.currentInputConnection ?: return false
@@ -320,11 +435,10 @@ class EditorInstance private constructor(private val ims: InputMethodService?) {
/**
* Sends a given [keyCode] as a [KeyEvent.ACTION_DOWN] with ALT pressed.
*
* @param ic The input connection on which this operation should be performed.
* @param keyCode The key code to send, use a key code defined in Android's [KeyEvent], not in
* [KeyCode] or this call may send a weird character, as this key codes do not match!!
*
* @returns True on success, false if an error occurred or the input connection is invalid.
* @return True on success, false if an error occurred or the input connection is invalid.
*/
fun sendSystemKeyEventAlt(keyCode: Int): Boolean {
val ic = ims?.currentInputConnection ?: return false
@@ -345,15 +459,23 @@ class EditorInstance private constructor(private val ims: InputMethodService?) {
* @param from The start index of the selection in characters (inclusive).
* @param to The end index of the selection in characters (exclusive).
*
* @returns True on success, false if an error occurred or the input connection is invalid.
* @return True on success, false if an error occurred or the input connection is invalid.
*/
fun setSelection(from: Int, to: Int): Boolean {
val ic = ims?.currentInputConnection ?: return false
selection.apply {
start = from
end = to
return if (isRawInputEditor) {
selection.apply {
start = -1
end = -1
}
false
} else {
selection.apply {
start = from
end = to
}
ic.setSelection(from, to)
}
return ic.setSelection(from, to)
}
/**
@@ -362,7 +484,7 @@ class EditorInstance private constructor(private val ims: InputMethodService?) {
* deleting only half of an emoji...
* Is used primarily in [deleteBackwards].
*
* @returns The length of the last Unicode character, in Java characters or 0 if the current
* @return The length of the last Unicode character, in Java characters or 0 if the current
* selection is invalid.
*/
private fun detectLastUnicodeCharacterLengthBeforeCursor(): Int {
@@ -372,7 +494,7 @@ class EditorInstance private constructor(private val ims: InputMethodService?) {
var charIndex = 0
var charLength = 0
var charShouldGlue = false
val textToSearch = cachedTextInternal.substring(0, selection.start.coerceAtMost(cachedTextInternal.length))
val textToSearch = cachedText.substring(0, selection.start.coerceAtMost(cachedText.length))
var i = 0
while (i < textToSearch.length) {
val cp = textToSearch.codePointAt(i)
@@ -397,31 +519,12 @@ class EditorInstance private constructor(private val ims: InputMethodService?) {
return textToSearch.length - charIndex
}
/**
* Gets the current text from the app's editor view.
*
* @returns The target editor's content string.
*/
private fun fetchExtractedTextFromInputConnection() {
val ic = ims?.currentInputConnection ?: return
val et = ic.getExtractedText(
ExtractedTextRequest(), 0
) ?: return
val text = et.text ?: ""
cachedTextInternal.setLength(0)
cachedTextInternal.append(text)
selection.apply {
start = et.selectionStart.coerceAtMost(cachedTextInternal.length)
end = et.selectionEnd.coerceAtMost(cachedTextInternal.length)
}
}
/**
* Marks a given [region] as composing and notifies the input connection.
*
* @param region The region which should be marked as composing.
*
* @returns True on success, false if an error occurred or the input connection is invalid.
* @return True on success, false if an error occurred or the input connection is invalid.
*/
private fun markComposingRegion(region: Region?): Boolean {
val ic = ims?.currentInputConnection ?: return false
@@ -442,7 +545,7 @@ class EditorInstance private constructor(private val ims: InputMethodService?) {
* @param regex The delimiter regex which should be used to split up the content text and find
* words. May differ from locale to locale.
*
* @returns True on success, false if no current word could be found.
* @return True on success, false if no current word could be found.
*/
private fun reevaluateCurrentWord(regex: Regex): Boolean {
var foundValidWord = false
@@ -473,12 +576,42 @@ class EditorInstance private constructor(private val ims: InputMethodService?) {
}
/**
* Triggers all reevaluation processes.
* Evaluates the current word with the correct delimiter regex for current subtype.
* TODO: currently only supports en-US
*/
private fun reevaluate() {
private fun reevaluateCurrentWord() {
val regex = "[^\\p{L}]".toRegex()
reevaluateCurrentWord(regex)
}
/**
* Gets the current text from the app's editor view.
*
* @return The target editor's content string.
*/
private fun updateEditorState() {
val ic = ims?.currentInputConnection
val et = ic?.getExtractedText(
ExtractedTextRequest(), 0
)
val text = et?.text
if (ic == null || et == null || text == null) {
isRawInputEditor = true
cachedText = ""
selection.apply {
start = -1
end = -1
}
} else {
isRawInputEditor = false
cachedText = text.toString()
selection.apply {
start = et.selectionStart.coerceAtMost(cachedText.length)
end = et.selectionEnd.coerceAtMost(cachedText.length)
}
}
reevaluateCurrentWord()
}
}
/**
@@ -498,6 +631,10 @@ class ImeOptions private constructor(imeOptions: Int) {
val flagNoPersonalizedLearning: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING > 0
companion object {
fun default(): ImeOptions {
return fromImeOptionsInt(EditorInfo.IME_NULL)
}
fun fromImeOptionsInt(imeOptions: Int): ImeOptions {
return ImeOptions(imeOptions)
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright (C) 2020 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.core
import android.app.Application
import dev.patrickgold.florisboard.crashutility.CrashUtility
class FlorisApplication : Application() {
override fun onCreate() {
super.onCreate()
CrashUtility.install(this)
}
}

View File

@@ -43,6 +43,7 @@ import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyData
import dev.patrickgold.florisboard.settings.SettingsMainActivity
import dev.patrickgold.florisboard.util.*
import java.lang.ref.WeakReference
/**
* Variable which holds the current [FlorisBoard] instance. To get this instance from another
@@ -54,7 +55,7 @@ private var florisboardInstance: FlorisBoard? = null
* Core class responsible to link together both the text and media input managers as well as
* managing the one-handed UI.
*/
class FlorisBoard : InputMethodService() {
class FlorisBoard : InputMethodService(), ClipboardManager.OnPrimaryClipChangedListener {
lateinit var prefs: PrefHelper
private set
@@ -63,7 +64,7 @@ class FlorisBoard : InputMethodService() {
var inputView: InputView? = null
private set
private var inputWindowView: InputWindowView? = null
private var eventListeners: MutableList<EventListener> = mutableListOf()
private var eventListeners: MutableList<WeakReference<EventListener>> = mutableListOf()
private var audioManager: AudioManager? = null
var clipboardManager: ClipboardManager? = null
@@ -150,6 +151,7 @@ class FlorisBoard : InputMethodService() {
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboardManager?.addPrimaryClipChangedListener(this)
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
prefs = PrefHelper.getDefaultInstance(this)
prefs.initDefaultPreferences()
@@ -165,7 +167,7 @@ class FlorisBoard : InputMethodService() {
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
super.onCreate()
eventListeners.toList().forEach { it.onCreate() }
eventListeners.toList().forEach { it.get()?.onCreate() }
}
@SuppressLint("InflateParams")
@@ -176,7 +178,7 @@ class FlorisBoard : InputMethodService() {
inputWindowView = layoutInflater.inflate(R.layout.florisboard, null) as InputWindowView
eventListeners.toList().forEach { it.onCreateInputView() }
eventListeners.toList().forEach { it.get()?.onCreateInputView() }
return inputWindowView
}
@@ -190,16 +192,17 @@ class FlorisBoard : InputMethodService() {
updateSoftInputWindowLayoutParameters()
updateOneHandedPanelVisibility()
eventListeners.toList().forEach { it.onRegisterInputView(inputView) }
eventListeners.toList().forEach { it.get()?.onRegisterInputView(inputView) }
}
override fun onDestroy() {
if (BuildConfig.DEBUG) Log.i(TAG, "onDestroy()")
clipboardManager?.removePrimaryClipChangedListener(this)
osHandler.removeCallbacksAndMessages(null)
florisboardInstance = null
eventListeners.toList().forEach { it.onDestroy() }
eventListeners.toList().forEach { it.get()?.onDestroy() }
eventListeners.clear()
super.onDestroy()
}
@@ -218,7 +221,7 @@ class FlorisBoard : InputMethodService() {
super.onStartInputView(info, restarting)
activeEditorInstance = EditorInstance.from(info, this)
eventListeners.toList().forEach {
it.onStartInputView(activeEditorInstance, restarting)
it.get()?.onStartInputView(activeEditorInstance, restarting)
}
}
@@ -230,7 +233,7 @@ class FlorisBoard : InputMethodService() {
}
super.onFinishInputView(finishingInput)
eventListeners.toList().forEach { it.onFinishInputView(finishingInput) }
eventListeners.toList().forEach { it.get()?.onFinishInputView(finishingInput) }
}
override fun onFinishInput() {
@@ -251,14 +254,14 @@ class FlorisBoard : InputMethodService() {
setActiveInput(R.id.text_input)
super.onWindowShown()
eventListeners.toList().forEach { it.onWindowShown() }
eventListeners.toList().forEach { it.get()?.onWindowShown() }
}
override fun onWindowHidden() {
if (BuildConfig.DEBUG) Log.i(TAG, "onWindowHidden()")
super.onWindowHidden()
eventListeners.toList().forEach { it.onWindowHidden() }
eventListeners.toList().forEach { it.get()?.onWindowHidden() }
}
override fun onConfigurationChanged(newConfig: Configuration) {
@@ -286,7 +289,7 @@ class FlorisBoard : InputMethodService() {
oldSelStart, oldSelEnd,
newSelStart, newSelEnd
)
eventListeners.toList().forEach { it.onUpdateSelection() }
eventListeners.toList().forEach { it.get()?.onUpdateSelection() }
}
/**
@@ -322,7 +325,7 @@ class FlorisBoard : InputMethodService() {
?.imageTintList = ColorStateList.valueOf(prefs.theme.oneHandedButtonFgColor)
inputView?.findViewById<ImageButton>(R.id.one_handed_ctrl_close_end)
?.imageTintList = ColorStateList.valueOf(prefs.theme.oneHandedButtonFgColor)
eventListeners.toList().forEach { it.onApplyThemeAttributes() }
eventListeners.toList().forEach { it.get()?.onApplyThemeAttributes() }
}
override fun onComputeInsets(outInsets: Insets?) {
@@ -534,25 +537,34 @@ class FlorisBoard : InputMethodService() {
}, 0)
}
override fun onPrimaryClipChanged() {
eventListeners.toList().forEach { it.get()?.onPrimaryClipChanged() }
}
/**
* Adds a given [listener] to the list which will receive FlorisBoard events.
*
* @param listener The listener object which receives the events.
* @returns True if the listener has been added successfully, false otherwise.
* @return True if the listener has been added successfully, false otherwise.
*/
fun addEventListener(listener: EventListener): Boolean {
return eventListeners.add(listener)
return eventListeners.add(WeakReference(listener))
}
/**
* Removes a given [listener] from the list which will receive FlorisBoard events.
*
* @param listener The same listener object which was used in [addEventListener].
* @returns True if the listener has been removed successfully, false otherwise. A false return
* @return True if the listener has been removed successfully, false otherwise. A false return
* value may also indicate that the [listener] was not added previously.
*/
fun removeEventListener(listener: EventListener): Boolean {
return eventListeners.remove(listener)
eventListeners.toList().forEach {
if (it.get() == listener) {
return eventListeners.remove(it)
}
}
return false
}
interface EventListener {
@@ -570,6 +582,7 @@ class FlorisBoard : InputMethodService() {
fun onUpdateSelection() {}
fun onApplyThemeAttributes() {}
fun onPrimaryClipChanged() {}
fun onSubtypeChanged(newSubtype: Subtype) {}
}

View File

@@ -373,20 +373,24 @@ class PrefHelper(
*/
class Suggestion(private val prefHelper: PrefHelper) {
companion object {
const val ENABLED = "suggestion__enabled"
const val SHOW_INSTEAD = "suggestion__show_instead"
const val USE_PREV_WORDS = "suggestion__use_prev_words"
const val ENABLED = "suggestion__enabled"
const val SHOW_INSTEAD = "suggestion__show_instead"
const val SUGGEST_CLIPBOARD_CONTENT = "suggestion__suggest_clipboard_content"
const val USE_PREV_WORDS = "suggestion__use_prev_words"
}
var enabled: Boolean = false
get() = prefHelper.getPref(ENABLED, true)
private set
var showInstead: String = ""
get() = prefHelper.getPref(SHOW_INSTEAD, "number_row")
private set
var usePrevWords: Boolean = false
get() = prefHelper.getPref(USE_PREV_WORDS, true)
private set
var enabled: Boolean
get() = prefHelper.getPref(ENABLED, true)
set(v) = prefHelper.setPref(ENABLED, v)
var showInstead: String
get() = prefHelper.getPref(SHOW_INSTEAD, "number_row")
set(v) = prefHelper.setPref(SHOW_INSTEAD, v)
var suggestClipboardContent: Boolean
get() = prefHelper.getPref(SUGGEST_CLIPBOARD_CONTENT, false)
set(v) = prefHelper.setPref(SUGGEST_CLIPBOARD_CONTENT, v)
var usePrevWords: Boolean
get() = prefHelper.getPref(USE_PREV_WORDS, true)
set(v) = prefHelper.setPref(USE_PREV_WORDS, v)
}
/**

View File

@@ -71,7 +71,7 @@ class SubtypeManager(
* Loads the [FlorisBoard.ImeConfig] from ime/config.json.
*
* @param path The path to to IME config file.
* @returns The [FlorisBoard.ImeConfig] or a default config.
* @return The [FlorisBoard.ImeConfig] or a default config.
*/
private fun loadImeConfig(path: String): FlorisBoard.ImeConfig {
val rawJsonData: String = try {
@@ -93,7 +93,7 @@ class SubtypeManager(
* Adds a given [subtypeToAdd] to the subtype list, if it does not exist.
*
* @param subtypeToAdd The subtype which should be added.
* @returns True if the subtype was added, false otherwise. A return value of false indicates
* @return True if the subtype was added, false otherwise. A return value of false indicates
* that the subtype already exists.
*/
private fun addSubtype(subtypeToAdd: Subtype): Boolean {
@@ -112,7 +112,7 @@ class SubtypeManager(
*
* @param locale The locale of the subtype to be added.
* @param layoutName The layout name of the subtype to be added.
* @returns True if the subtype was added, false otherwise. A return value of false indicates
* @return True if the subtype was added, false otherwise. A return value of false indicates
* that the subtype already exists.
*/
fun addSubtype(locale: Locale, layoutName: String): Boolean {
@@ -129,7 +129,7 @@ class SubtypeManager(
* Gets the active subtype and returns it. If the activeSubtypeId points to a non-existent
* subtype, this method tries to determine a new active subtype.
*
* @returns The active subtype or null, if the subtype list is empty or no new active subtype
* @return The active subtype or null, if the subtype list is empty or no new active subtype
* could be determined.
*/
fun getActiveSubtype(): Subtype? {
@@ -152,7 +152,7 @@ class SubtypeManager(
* Gets a subtype by the given [id].
*
* @param id The id of the subtype you want to get.
* @returns The subtype or null, if no matching subtype could be found.
* @return The subtype or null, if no matching subtype could be found.
*/
fun getSubtypeById(id: Int): Subtype? {
for (subtype in subtypes) {
@@ -167,7 +167,7 @@ class SubtypeManager(
* Gets the default system subtype for a given [locale].
*
* @param locale The locale of the default system subtype to get.
* @returns The default system locale or null, if no matching default system subtype could be
* @return The default system locale or null, if no matching default system subtype could be
* found.
*/
fun getDefaultSubtypeForLocale(locale: Locale): DefaultSubtype? {
@@ -220,7 +220,7 @@ class SubtypeManager(
/**
* Switch to the previous subtype in the subtype list if possible.
*
* @returns The new active subtype or null if the determination process failed.
* @return The new active subtype or null if the determination process failed.
*/
fun switchToPrevSubtype(): Subtype? {
val subtypeList = subtypes
@@ -248,7 +248,7 @@ class SubtypeManager(
/**
* Switch to the next subtype in the subtype list if possible.
*
* @returns The new active subtype or null if the determination process failed.
* @return The new active subtype or null if the determination process failed.
*/
fun switchToNextSubtype(): Subtype? {
val subtypeList = subtypes

View File

@@ -102,7 +102,7 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
isWrapBefore: Boolean = false
): KeyPopupExtendedSingleView? {
val textView = KeyPopupExtendedSingleView(keyView.context, k, isInitActive)
val lp = FlexboxLayout.LayoutParams(keyPopupWidth, keyView.measuredHeight)
val lp = FlexboxLayout.LayoutParams(keyPopupWidth, (keyPopupHeight * 0.4f).toInt())
lp.isWrapBefore = isWrapBefore
textView.layoutParams = lp
textView.gravity = Gravity.CENTER
@@ -171,12 +171,22 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
if (keyboardView is KeyboardView) {
when (keyboardView.resources.configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> {
keyPopupWidth = (keyboardView.desiredKeyWidth * 0.6f).toInt()
keyPopupHeight = (keyboardView.desiredKeyHeight * 3.0f).toInt()
if (keyboardView.isSmartbarKeyboardView) {
keyPopupWidth = (keyView.measuredWidth * 0.6f).toInt()
keyPopupHeight = (keyboardView.desiredKeyHeight * 3.0f * 1.2f).toInt()
} else {
keyPopupWidth = (keyboardView.desiredKeyWidth * 0.6f).toInt()
keyPopupHeight = (keyboardView.desiredKeyHeight * 3.0f).toInt()
}
}
else -> {
keyPopupWidth = (keyboardView.desiredKeyWidth * 1.1f).toInt()
keyPopupHeight = (keyboardView.desiredKeyHeight * 2.5f).toInt()
if (keyboardView.isSmartbarKeyboardView) {
keyPopupWidth = (keyView.measuredWidth * 1.1f).toInt()
keyPopupHeight = (keyboardView.desiredKeyHeight * 2.5f * 1.2f).toInt()
} else {
keyPopupWidth = (keyboardView.desiredKeyWidth * 1.1f).toInt()
keyPopupHeight = (keyboardView.desiredKeyHeight * 2.5f).toInt()
}
}
}
} else if (keyboardView is EmojiKeyboardView) {
@@ -344,9 +354,9 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
// Calculate layout params
val extWidth = row0count * keyPopupWidth
val extHeight = when {
row1count > 0 -> keyView.measuredHeight * 2
else -> keyView.measuredHeight
}
row1count > 0 -> keyPopupHeight * 0.4f * 2.0f
else -> keyPopupHeight * 0.4f
}.toInt()
popupViewExt.justifyContent = if (anchorLeft) {
JustifyContent.FLEX_START
} else {
@@ -366,7 +376,7 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
else -> 0
}
val y = -keyPopupHeight - when {
row1count > 0 -> keyView.measuredHeight
row1count > 0 -> (keyPopupHeight * 0.4f).toInt()
else -> 0
}
@@ -480,8 +490,12 @@ class KeyPopupManager<T_KBD: View, T_KV: View>(private val keyboardView: T_KBD)
return if (keyView is KeyView) {
val activeExtIndex = activeExtIndex
if (activeExtIndex != null) {
val singleView = popupViewExt[activeExtIndex] as KeyPopupExtendedSingleView
keyView.dataPopupWithHint.getOrNull(singleView.adjustedIndex) ?: keyView.data
val singleView = popupViewExt[activeExtIndex]
if (singleView is KeyPopupExtendedSingleView) {
keyView.dataPopupWithHint.getOrNull(singleView.adjustedIndex) ?: keyView.data
} else {
keyView.data
}
} else {
keyView.data
}

View File

@@ -16,7 +16,6 @@
package dev.patrickgold.florisboard.ime.text
import android.content.ClipData
import android.content.Context
import android.os.Handler
import android.util.Log
@@ -67,7 +66,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
var textViewGroup: LinearLayout? = null
var keyVariation: KeyVariation = KeyVariation.NORMAL
private val layoutManager = LayoutManager(florisboard)
val layoutManager = LayoutManager(florisboard)
private lateinit var smartbarManager: SmartbarManager
// Caps/Space related properties
@@ -145,7 +144,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
setActiveKeyboardMode(activeKeyboardMode)
}
for (mode in KeyboardMode.values()) {
if (mode != activeKeyboardMode) {
if (mode != activeKeyboardMode && mode != KeyboardMode.SMARTBAR_NUMBER_ROW) {
addKeyboardView(mode)
}
}
@@ -210,16 +209,16 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
KeyboardMode.PHONE,
KeyboardMode.PHONE2 -> false
else -> keyVariation != KeyVariation.PASSWORD &&
florisboard.prefs.suggestion.enabled &&
florisboard.prefs.suggestion.enabled// &&
//!instance.inputAttributes.flagTextAutoComplete &&
!instance.inputAttributes.flagTextNoSuggestions
//!instance.inputAttributes.flagTextNoSuggestions
}
if (!florisboard.prefs.correction.rememberCapsLockState) {
capsLock = false
}
updateCapsState()
setActiveKeyboardMode(keyboardMode)
smartbarManager.onStartInputView(keyboardMode, instance.isComposingEnabled)
smartbarManager.onStartInputView(keyboardMode)
}
/**
@@ -274,9 +273,6 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
* and passing this info on to the [SmartbarManager] to turn it into candidate suggestions.
*/
override fun onUpdateSelection() {
if (activeEditorInstance.selection.isCursorMode) {
smartbarManager.generateCandidatesFromComposing(activeEditorInstance.currentWord.text)
}
if (!activeEditorInstance.isNewSelectionInBoundsOfOld) {
isManualSelectionMode = false
isManualSelectionModeLeft = false
@@ -286,6 +282,10 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
smartbarManager.onUpdateSelection()
}
override fun onPrimaryClipChanged() {
smartbarManager.onPrimaryClipChanged()
}
/**
* Updates the current caps state according to the [EditorInstance.cursorCapsMode], while
* respecting [capsLock] property and the correction.autoCapitalization preference.
@@ -339,7 +339,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
*/
private fun handleEnter() {
if (activeEditorInstance.imeOptions.flagNoEnterAction) {
activeEditorInstance.commitText("\n")
activeEditorInstance.performEnter()
} else {
when (activeEditorInstance.imeOptions.action) {
ImeOptions.Action.DONE,
@@ -350,7 +350,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
ImeOptions.Action.SEND -> {
activeEditorInstance.performEnterAction(activeEditorInstance.imeOptions.action)
}
else -> activeEditorInstance.commitText("\n")
else -> activeEditorInstance.performEnter()
}
}
}
@@ -514,40 +514,6 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
}
}
/**
* Handles a [KeyCode.CLIPBOARD_CUT] event.
* TODO: handle other data than text too, e.g. Uri, Intent, ...
*/
private fun handleClipboardCut() {
val selectedText = activeEditorInstance.selection.text
florisboard.clipboardManager
?.setPrimaryClip(ClipData.newPlainText(selectedText, selectedText))
activeEditorInstance.commitText("")
}
/**
* Handles a [KeyCode.CLIPBOARD_COPY] event.
* TODO: handle other data than text too, e.g. Uri, Intent, ...
*/
private fun handleClipboardCopy() {
val selectedText = activeEditorInstance.selection.text
florisboard.clipboardManager
?.setPrimaryClip(ClipData.newPlainText(selectedText, selectedText))
activeEditorInstance.apply { setSelection(selection.end, selection.end) }
}
/**
* Handles a [KeyCode.CLIPBOARD_PASTE] event.
* TODO: handle other data than text too, e.g. Uri, Intent, ...
*/
private fun handleClipboardPaste() {
val item = florisboard.clipboardManager?.primaryClip?.getItemAt(0)
val pasteText = item?.text
if (pasteText != null) {
activeEditorInstance.commitText(pasteText.toString())
}
}
/**
* Handles a [KeyCode.CLIPBOARD_SELECT] event.
*/
@@ -588,13 +554,22 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
KeyCode.ARROW_UP,
KeyCode.MOVE_HOME,
KeyCode.MOVE_END -> handleArrow(keyData.code)
KeyCode.CLIPBOARD_CUT -> handleClipboardCut()
KeyCode.CLIPBOARD_COPY -> handleClipboardCopy()
KeyCode.CLIPBOARD_PASTE -> handleClipboardPaste()
KeyCode.CLIPBOARD_CUT -> activeEditorInstance.performClipboardCut()
KeyCode.CLIPBOARD_COPY -> activeEditorInstance.performClipboardCopy()
KeyCode.CLIPBOARD_PASTE -> {
activeEditorInstance.performClipboardPaste()
smartbarManager.resetClipboardSuggestion()
}
KeyCode.CLIPBOARD_SELECT -> handleClipboardSelect()
KeyCode.CLIPBOARD_SELECT_ALL -> handleClipboardSelectAll()
KeyCode.DELETE -> handleDelete()
KeyCode.ENTER -> handleEnter()
KeyCode.DELETE -> {
handleDelete()
smartbarManager.resetClipboardSuggestion()
}
KeyCode.ENTER -> {
handleEnter()
smartbarManager.resetClipboardSuggestion()
}
KeyCode.LANGUAGE_SWITCH -> florisboard.switchToNextSubtype()
KeyCode.SETTINGS -> florisboard.launchSettings()
KeyCode.SHIFT -> handleShift()
@@ -656,7 +631,11 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
}
}
}
smartbarManager.resetClipboardSuggestion()
}
}
if (keyData.code != KeyCode.SHIFT && !capsLock) {
updateCapsState()
}
}
}

View File

@@ -24,7 +24,6 @@ import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.ViewOutlineProvider
import android.view.inputmethod.EditorInfo
import androidx.core.content.ContextCompat.getDrawable
import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
@@ -32,6 +31,7 @@ import androidx.core.view.children
import com.google.android.flexbox.FlexboxLayout
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.ImeOptions
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.text.gestures.SwipeGesture
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
@@ -204,7 +204,7 @@ class KeyView(
* go look at which child the pointer is actually above.
*/
fun onFlorisTouchEvent(event: MotionEvent?): Boolean {
event ?: return false
if (event == null || !isEnabled) return false
if (swipeGestureDetector.onTouchEvent(event)) {
isKeyPressed = false
osHandler?.removeCallbacksAndMessages(null)
@@ -403,34 +403,69 @@ class KeyView(
outlineProvider = KeyViewOutline(w, h)
}
/**
* Updates the enabled state of a key depending on the [data] and its parameters.
*/
private fun updateEnabledState() {
isEnabled = when (data.code) {
KeyCode.CLIPBOARD_COPY,
KeyCode.CLIPBOARD_CUT -> {
florisboard?.activeEditorInstance?.selection?.isSelectionMode == true &&
florisboard?.activeEditorInstance?.isRawInputEditor == false
}
KeyCode.CLIPBOARD_PASTE -> florisboard?.clipboardManager?.hasPrimaryClip() == true
KeyCode.CLIPBOARD_SELECT_ALL -> {
florisboard?.activeEditorInstance?.isRawInputEditor == false
}
else -> true
}
if (!isEnabled) {
isKeyPressed = false
}
}
/**
* Updates the background depending on [isKeyPressed] and [data].
*/
private fun updateKeyPressedBackground() {
when (data.code) {
KeyCode.ENTER -> {
when {
keyboardView.isSmartbarKeyboardView -> {
elevation = 0.0f
setBackgroundTintColor2(
this, when {
isKeyPressed -> prefs.theme.keyEnterBgColorPressed
else -> prefs.theme.keyEnterBgColor
}
)
}
KeyCode.SHIFT -> {
setBackgroundTintColor2(
this, when {
isKeyPressed -> prefs.theme.keyShiftBgColorPressed
else -> prefs.theme.keyShiftBgColor
isKeyPressed && isEnabled -> prefs.theme.smartbarButtonBgColor
else -> prefs.theme.smartbarBgColor
}
)
}
else -> {
setBackgroundTintColor2(
this, when {
isKeyPressed -> prefs.theme.keyBgColorPressed
else -> prefs.theme.keyBgColor
elevation = 4.0f
when (data.code) {
KeyCode.ENTER -> {
setBackgroundTintColor2(
this, when {
isKeyPressed && isEnabled -> prefs.theme.keyEnterBgColorPressed
else -> prefs.theme.keyEnterBgColor
}
)
}
)
KeyCode.SHIFT -> {
setBackgroundTintColor2(
this, when {
isKeyPressed && isEnabled -> prefs.theme.keyShiftBgColorPressed
else -> prefs.theme.keyShiftBgColor
}
)
}
else -> {
setBackgroundTintColor2(
this, when {
isKeyPressed && isEnabled -> prefs.theme.keyBgColorPressed
else -> prefs.theme.keyBgColor
}
)
}
}
}
}
}
@@ -467,6 +502,7 @@ class KeyView(
* TextInputManager.
*/
fun updateVisibility() {
updateEnabledState()
when (data.code) {
KeyCode.SWITCH_TO_TEXT_CONTEXT,
KeyCode.SWITCH_TO_MEDIA_CONTEXT -> {
@@ -564,24 +600,48 @@ class KeyView(
} else {
when (data.code) {
KeyCode.ARROW_LEFT -> {
drawable = getDrawable(context, R.drawable.ic_keyboard_arrow_left)
drawableColor = prefs.theme.keyFgColor
}
KeyCode.ARROW_RIGHT -> {
drawable = getDrawable(context, R.drawable.ic_keyboard_arrow_right)
drawableColor = prefs.theme.keyFgColor
}
KeyCode.CLIPBOARD_COPY -> {
drawable = getDrawable(context, R.drawable.ic_content_copy)
drawableColor = prefs.theme.keyFgColor
}
KeyCode.CLIPBOARD_CUT -> {
drawable = getDrawable(context, R.drawable.ic_content_cut)
drawableColor = prefs.theme.keyFgColor
}
KeyCode.CLIPBOARD_PASTE -> {
drawable = getDrawable(context, R.drawable.ic_content_paste)
drawableColor = prefs.theme.keyFgColor
}
KeyCode.CLIPBOARD_SELECT_ALL -> {
drawable = getDrawable(context, R.drawable.ic_select_all)
drawableColor = prefs.theme.keyFgColor
}
KeyCode.DELETE -> {
drawable = getDrawable(context, R.drawable.ic_backspace)
drawableColor = prefs.theme.keyFgColor
}
KeyCode.ENTER -> {
val action = florisboard?.currentInputEditorInfo?.imeOptions ?: 0
drawable = getDrawable(context, when (action and EditorInfo.IME_MASK_ACTION) {
EditorInfo.IME_ACTION_DONE -> R.drawable.ic_done
EditorInfo.IME_ACTION_GO -> R.drawable.ic_arrow_right_alt
EditorInfo.IME_ACTION_NEXT -> R.drawable.ic_arrow_right_alt
EditorInfo.IME_ACTION_NONE -> R.drawable.ic_keyboard_return
EditorInfo.IME_ACTION_PREVIOUS -> R.drawable.ic_arrow_right_alt
EditorInfo.IME_ACTION_SEARCH -> R.drawable.ic_search
EditorInfo.IME_ACTION_SEND -> R.drawable.ic_send
else -> R.drawable.ic_arrow_right_alt
val imeOptions = florisboard?.activeEditorInstance?.imeOptions ?: ImeOptions.default()
drawable = getDrawable(context, when (imeOptions.action) {
ImeOptions.Action.DONE -> R.drawable.ic_done
ImeOptions.Action.GO -> R.drawable.ic_arrow_right_alt
ImeOptions.Action.NEXT -> R.drawable.ic_arrow_right_alt
ImeOptions.Action.NONE -> R.drawable.ic_keyboard_return
ImeOptions.Action.PREVIOUS -> R.drawable.ic_arrow_right_alt
ImeOptions.Action.SEARCH -> R.drawable.ic_search
ImeOptions.Action.SEND -> R.drawable.ic_send
ImeOptions.Action.UNSPECIFIED -> R.drawable.ic_keyboard_return
})
drawableColor = prefs.theme.keyEnterFgColor
if (action and EditorInfo.IME_FLAG_NO_ENTER_ACTION > 0) {
if (imeOptions.flagNoEnterAction) {
drawable = getDrawable(context, R.drawable.ic_keyboard_return)
}
}
@@ -658,6 +718,9 @@ class KeyView(
// Draw drawable
val drawable = drawable
if (drawable != null) {
if (keyboardView.isSmartbarKeyboardView && !isEnabled) {
drawableColor = prefs.theme.smartbarFgColorAlt
}
var marginV = 0
var marginH = 0
if (measuredWidth > measuredHeight) {

View File

@@ -24,5 +24,7 @@ enum class KeyboardMode {
NUMERIC,
NUMERIC_ADVANCED,
PHONE,
PHONE2
PHONE2,
SMARTBAR_CLIPBOARD_CURSOR_ROW,
SMARTBAR_NUMBER_ROW
}

View File

@@ -60,6 +60,7 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
var florisboard: FlorisBoard? = FlorisBoard.getInstanceOrNull()
private var initialKeyCode: Int = 0
var isPreviewMode: Boolean = false
var isSmartbarKeyboardView: Boolean = false
var popupManager = KeyPopupManager<KeyboardView, KeyView>(this)
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
private val swipeGestureDetector = SwipeGesture.Detector(context, this)
@@ -132,7 +133,7 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
return false
}
val eventFloris = MotionEvent.obtainNoHistory(event)
if (swipeGestureDetector.onTouchEvent(event)) {
if (!isSmartbarKeyboardView && swipeGestureDetector.onTouchEvent(event)) {
sendFlorisTouchEvent(eventFloris, MotionEvent.ACTION_CANCEL)
activeKeyView = null
activePointerId = null
@@ -287,20 +288,25 @@ class KeyboardView : LinearLayout, FlorisBoard.EventListener, SwipeGesture.Liste
* The desired key heights/widths are being calculated here.
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val keyMarginH = resources.getDimension((R.dimen.key_marginH)).toInt()
desiredKeyWidth = (widthSize / 10) - (2 * keyMarginH)
val keyMarginV = resources.getDimension((R.dimen.key_marginV)).toInt()
val keyHeightFactor = when (isPreviewMode) {
true -> 0.90f
else -> 1.00f
}
val desiredHeight = keyHeightFactor * (florisboard?.inputView?.desiredTextKeyboardViewHeight ?: resources.getDimension(R.dimen.textKeyboardView_baseHeight).toInt())
desiredKeyHeight = (desiredHeight / 4 - 2 * keyMarginV).roundToInt()
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(desiredHeight.roundToInt(), MeasureSpec.EXACTLY))
val desiredWidth = MeasureSpec.getSize(widthMeasureSpec).toFloat()
desiredKeyWidth = if (isSmartbarKeyboardView) {
(desiredWidth / 6.0f - 2.0f * keyMarginH).roundToInt()
} else {
(desiredWidth / 10.0f - 2.0f * keyMarginH).roundToInt()
}
val desiredHeight = MeasureSpec.getSize(heightMeasureSpec) * if (isPreviewMode) { 0.90f } else { 1.00f }
desiredKeyHeight = when {
isSmartbarKeyboardView -> desiredHeight - 1.5f * keyMarginV
else -> desiredHeight / 4.0f - 2.0f * keyMarginV
}.roundToInt()
super.onMeasure(
MeasureSpec.makeMeasureSpec(desiredWidth.roundToInt(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(desiredHeight.roundToInt(), MeasureSpec.EXACTLY)
)
}
override fun onApplyThemeAttributes() {

View File

@@ -39,7 +39,7 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
/**
* Loads the layout for the specified type and name.
*
* @returns the [LayoutData] or null.
* @return the [LayoutData] or null.
*/
private fun loadLayout(ltn: LTN?) = loadLayout(ltn?.first, ltn?.second)
private fun loadLayout(type: LayoutType?, name: String?): LayoutData? {
@@ -106,7 +106,7 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
* @param main The main layout type and name.
* @param modifier The modifier (mod) layout type and name.
* @param extension The extension layout type and name.
* @returns a [ComputedLayoutData] object, regardless of the specified LTNs or errors.
* @return a [ComputedLayoutData] object, regardless of the specified LTNs or errors.
*/
private suspend fun mergeLayoutsAsync(
keyboardMode: KeyboardMode,
@@ -267,6 +267,12 @@ class LayoutManager(private val context: Context) : CoroutineScope by MainScope(
main = LTN(LayoutType.SYMBOLS2, "western_default")
modifier = LTN(LayoutType.SYMBOLS2_MOD, "default")
}
KeyboardMode.SMARTBAR_CLIPBOARD_CURSOR_ROW -> {
extension = LTN(LayoutType.EXTENSION, "clipboard_cursor_row")
}
KeyboardMode.SMARTBAR_NUMBER_ROW -> {
extension = LTN(LayoutType.EXTENSION, "number_row")
}
}
return mergeLayoutsAsync(keyboardMode, subtype, main, modifier, extension)

View File

@@ -2,32 +2,40 @@ package dev.patrickgold.florisboard.ime.text.smartbar
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.CursorAnchorInfo
import android.widget.Button
import android.widget.ImageButton
import android.widget.LinearLayout
import androidx.core.view.children
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.EditorInstance
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.text.TextInputManager
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyData
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardMode
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
// TODO: Implement suggestion creation functionality
// TODO: Cleanup and reorganize SmartbarManager
class SmartbarManager private constructor() : FlorisBoard.EventListener {
class SmartbarManager private constructor() : CoroutineScope by MainScope(),
FlorisBoard.EventListener {
private val florisboard: FlorisBoard = FlorisBoard.getInstance()
private var isComposingEnabled: Boolean = false
private val activeEditorInstance: EditorInstance
get() = florisboard.activeEditorInstance
private val prefs: PrefHelper
get() = florisboard.prefs
private val textInputManager: TextInputManager = TextInputManager.getInstance()
var smartbarView: SmartbarView? = null
private set
private var shouldSuggestClipboardContents: Boolean = false
private var smartbarView: SmartbarView? = null
var isQuickActionsVisible: Boolean = false
set(value) { field = value; updateActiveContainerVisibility() }
private val candidateViewOnClickListener = View.OnClickListener { v ->
val view = v as Button
@@ -39,27 +47,10 @@ class SmartbarManager private constructor() : FlorisBoard.EventListener {
private val candidateViewOnLongClickListener = View.OnLongClickListener { v ->
true
}
private val keyButtonOnClickListener = View.OnClickListener { v ->
val keyData = when (v.id) {
R.id.number_row_0 -> KeyData(48, "0")
R.id.number_row_1 -> KeyData(49, "1")
R.id.number_row_2 -> KeyData(50, "2")
R.id.number_row_3 -> KeyData(51, "3")
R.id.number_row_4 -> KeyData(52, "4")
R.id.number_row_5 -> KeyData(53, "5")
R.id.number_row_6 -> KeyData(54, "6")
R.id.number_row_7 -> KeyData(55, "7")
R.id.number_row_8 -> KeyData(56, "8")
R.id.number_row_9 -> KeyData(57, "9")
R.id.cc_select_all -> KeyData(KeyCode.CLIPBOARD_SELECT_ALL)
R.id.cc_copy -> KeyData(KeyCode.CLIPBOARD_COPY)
R.id.cc_arrow_left -> KeyData(KeyCode.ARROW_LEFT)
R.id.cc_arrow_right -> KeyData(KeyCode.ARROW_RIGHT)
R.id.cc_cut -> KeyData(KeyCode.CLIPBOARD_CUT)
R.id.cc_paste -> KeyData(KeyCode.CLIPBOARD_PASTE)
else -> KeyData(0)
}
florisboard.textInputManager.sendKeyPress(keyData)
private val clipboardSuggestionViewOnClickListener = View.OnClickListener {
activeEditorInstance.performClipboardPaste()
shouldSuggestClipboardContents = false
updateActiveContainerVisibility()
}
private val quickActionOnClickListener = View.OnClickListener { v ->
when (v.id) {
@@ -82,9 +73,11 @@ class SmartbarManager private constructor() : FlorisBoard.EventListener {
else -> return@OnClickListener
}
isQuickActionsVisible = false
updateSmartbarUI()
}
private val quickActionToggleOnClickListener = View.OnClickListener {
isQuickActionsVisible = !isQuickActionsVisible
updateSmartbarUI()
}
companion object {
@@ -111,16 +104,24 @@ class SmartbarManager private constructor() : FlorisBoard.EventListener {
quickAction.setOnClickListener(quickActionOnClickListener)
}
}
val numberRow = smartbarView.findViewById<LinearLayout>(R.id.number_row)
for (numberRowButton in numberRow.children) {
if (numberRowButton is Button) {
numberRowButton.setOnClickListener(keyButtonOnClickListener)
launch(Dispatchers.Default) {
val numberRow = smartbarView.findViewById<KeyboardView>(R.id.smartbar_variant_number_row)
numberRow.isSmartbarKeyboardView = true
val layout = textInputManager.layoutManager.fetchComputedLayoutAsync(KeyboardMode.SMARTBAR_NUMBER_ROW, Subtype.DEFAULT).await()
launch(Dispatchers.Main) {
numberRow.computedLayout = layout
numberRow.updateVisibility()
}
}
val clipboardCursorRow = smartbarView.findViewById<ViewGroup>(R.id.clipboard_cursor_row)
for (clipboardCursorRowButton in clipboardCursorRow.children) {
if (clipboardCursorRowButton is ImageButton) {
clipboardCursorRowButton.setOnClickListener(keyButtonOnClickListener)
val clipboardSuggestion = smartbarView.findViewById<Button>(R.id.clipboard_suggestion)
clipboardSuggestion.setOnClickListener(clipboardSuggestionViewOnClickListener)
launch(Dispatchers.Default) {
val ccRow = smartbarView.findViewById<KeyboardView>(R.id.clipboard_cursor_row)
ccRow.isSmartbarKeyboardView = true
val layout = textInputManager.layoutManager.fetchComputedLayoutAsync(KeyboardMode.SMARTBAR_CLIPBOARD_CURSOR_ROW, Subtype.DEFAULT).await()
launch(Dispatchers.Main) {
ccRow.computedLayout = layout
ccRow.updateVisibility()
}
}
val backButton = smartbarView.findViewById<View>(R.id.back_button)
@@ -130,11 +131,12 @@ class SmartbarManager private constructor() : FlorisBoard.EventListener {
candidateView.setOnLongClickListener(candidateViewOnLongClickListener)
}
smartbarView.setActiveVariant(R.id.smartbar_variant_default)
updateSmartbarUI()
}
override fun onWindowShown() {
isQuickActionsVisible = false
updateActiveContainerVisibility()
}
// TODO: clean up resources here
@@ -144,8 +146,7 @@ class SmartbarManager private constructor() : FlorisBoard.EventListener {
instance = null
}
fun onStartInputView(keyboardMode: KeyboardMode, isComposingEnabled: Boolean) {
this.isComposingEnabled = isComposingEnabled
fun onStartInputView(keyboardMode: KeyboardMode) {
when (keyboardMode) {
KeyboardMode.NUMERIC, KeyboardMode.PHONE, KeyboardMode.PHONE2 -> {
smartbarView?.setActiveVariant(null)
@@ -155,6 +156,7 @@ class SmartbarManager private constructor() : FlorisBoard.EventListener {
isQuickActionsVisible = false
}
}
updateSmartbarUI()
}
fun onFinishInputView() {
@@ -162,20 +164,7 @@ class SmartbarManager private constructor() : FlorisBoard.EventListener {
}
override fun onUpdateSelection() {
val isSelectionActive = florisboard.activeEditorInstance.selection.isSelectionMode
smartbarView?.findViewById<View>(R.id.cc_cut)?.isEnabled = isSelectionActive
smartbarView?.findViewById<View>(R.id.cc_copy)?.isEnabled = isSelectionActive
smartbarView?.findViewById<View>(R.id.cc_paste)?.isEnabled =
florisboard.clipboardManager?.hasPrimaryClip() ?: false
smartbarView?.invalidate()
}
fun deleteCandidateFromDictionary(candidate: String) {
//
}
fun resetCandidates() {
//
updateSmartbarUI()
}
fun generateCandidatesFromComposing(composingText: String) {
@@ -190,43 +179,87 @@ class SmartbarManager private constructor() : FlorisBoard.EventListener {
smartbarView.candidateViewList[1].text = composingText + "test"
smartbarView.candidateViewList[2].text = ""
}
//spellCheckerSession?.getSentenceSuggestions(arrayOf(TextInfo(composing)), 3)
//android.util.Log.i("SPELL", "GEN")
/*val dic: Uri = UserDictionary.Words.CONTENT_URI
val resolver: ContentResolver = florisboard.contentResolver
val cursor: Cursor = resolver.query(dic, null, null, null, null) ?: return
var count = 0
while (cursor.moveToNext()) {
val word = cursor.getString(cursor.getColumnIndex(UserDictionary.Words.WORD))
candidateViewList[count].text = word
if (count++ > 2) {
break
}
}
cursor.close()*/
}
fun writeCandidate(candidate: String) {
//
override fun onPrimaryClipChanged() {
if (prefs.suggestion.enabled && prefs.suggestion.suggestClipboardContent) {
shouldSuggestClipboardContents = true
updateActiveContainerVisibility()
}
}
fun resetClipboardSuggestion() {
if (prefs.suggestion.enabled && prefs.suggestion.suggestClipboardContent) {
shouldSuggestClipboardContents = false
updateActiveContainerVisibility()
}
}
private fun updateSmartbarUI() {
val ei = activeEditorInstance
if (ei.selection.isCursorMode && ei.isComposingEnabled) {
generateCandidatesFromComposing(ei.currentWord.text)
}
updateActiveContainerVisibility()
val ccRow = smartbarView?.findViewById<KeyboardView>(R.id.clipboard_cursor_row)
ccRow?.updateVisibility()
}
private fun updateActiveContainerVisibility() {
val smartbarView = smartbarView ?: return
if (isQuickActionsVisible) {
smartbarView.setActiveVariant(R.id.smartbar_variant_default)
smartbarView.setActiveContainer(R.id.quick_actions)
smartbarView.findViewById<View>(R.id.quick_action_toggle)?.rotation = -180.0f
} else {
if (isComposingEnabled) {
smartbarView.setActiveContainer(R.id.candidates)
} else if (textInputManager.getActiveKeyboardMode() == KeyboardMode.CHARACTERS) {
smartbarView.setActiveContainer(when (florisboard.prefs.suggestion.showInstead) {
"number_row" -> R.id.number_row
"clipboard_cursor_tools" -> R.id.clipboard_cursor_row
else -> null
})
} else {
smartbarView.setActiveContainer(null)
when {
textInputManager.getActiveKeyboardMode() == KeyboardMode.EDITING -> {
smartbarView.setActiveVariant(R.id.smartbar_variant_back_only)
smartbarView.setActiveContainer(null)
}
activeEditorInstance.isComposingEnabled -> {
smartbarView.setActiveVariant(R.id.smartbar_variant_default)
val containerId = if (shouldSuggestClipboardContents && florisboard.clipboardManager?.hasPrimaryClip() == true) {
val clipboardSuggestion = smartbarView.findViewById<Button>(R.id.clipboard_suggestion)
val item = florisboard.clipboardManager?.primaryClip?.getItemAt(0)
when {
item?.text != null -> {
clipboardSuggestion?.text = item.text
}
item?.uri != null -> {
clipboardSuggestion?.text = "(Image) " + item.uri.toString()
}
else -> {
clipboardSuggestion?.text = item?.text ?: "(Error while retrieving clipboard data)"
}
}
R.id.clipboard_suggestion_row
} else {
R.id.candidates
}
smartbarView.setActiveContainer(containerId)
}
textInputManager.getActiveKeyboardMode() == KeyboardMode.CHARACTERS -> {
when (prefs.suggestion.showInstead) {
"number_row" -> {
smartbarView.setActiveVariant(R.id.smartbar_variant_number_row)
smartbarView.setActiveContainer(null)
}
"clipboard_cursor_tools" -> {
smartbarView.setActiveVariant(R.id.smartbar_variant_default)
smartbarView.setActiveContainer(R.id.clipboard_cursor_row)
}
else -> {
smartbarView.setActiveVariant(null)
smartbarView.setActiveContainer(null)
}
}
}
else -> {
smartbarView.setActiveVariant(null)
smartbarView.setActiveContainer(null)
}
}
smartbarView.findViewById<View>(R.id.quick_action_toggle)?.rotation = 0.0f
}

View File

@@ -23,7 +23,6 @@ import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageButton
import android.widget.LinearLayout
import androidx.annotation.IdRes
import androidx.core.view.children
@@ -31,8 +30,8 @@ import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.FlorisBoard
import dev.patrickgold.florisboard.ime.core.PrefHelper
import dev.patrickgold.florisboard.util.setImageTintColor2
import kotlinx.android.synthetic.main.florisboard.view.*
import dev.patrickgold.florisboard.util.setBackgroundTintColor2
import dev.patrickgold.florisboard.util.setDrawableTintColor2
/**
* View class which keeps the references to important children and informs [SmartbarManager] that
@@ -61,10 +60,11 @@ class SmartbarView : LinearLayout {
variants.add(findViewById(R.id.smartbar_variant_default))
variants.add(findViewById(R.id.smartbar_variant_back_only))
variants.add(findViewById(R.id.smartbar_variant_number_row))
containers.add(findViewById(R.id.candidates))
containers.add(findViewById(R.id.clipboard_suggestion_row))
containers.add(findViewById(R.id.clipboard_cursor_row))
containers.add(findViewById(R.id.number_row))
containers.add(findViewById(R.id.quick_actions))
candidateViewList.add(findViewById(R.id.candidate0))
@@ -131,25 +131,13 @@ class SmartbarView : LinearLayout {
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
setBackgroundColor(prefs.theme.smartbarBgColor)
for (container in containers) {
for (container in containers + variants) {
when (container.id) {
R.id.number_row -> {
for (button in container.children) {
if (button is Button) {
button.setTextColor(prefs.theme.smartbarFgColor)
}
}
}
R.id.clipboard_cursor_row -> {
for (button in container.children) {
if (button is ImageButton) {
if (button.isEnabled) {
setImageTintColor2(button, prefs.theme.smartbarFgColor)
} else {
setImageTintColor2(button, prefs.theme.smartbarFgColorAlt)
}
}
}
R.id.clipboard_suggestion_row -> {
val clipboardSuggestion = findViewById<Button>(R.id.clipboard_suggestion)
setBackgroundTintColor2(clipboardSuggestion, prefs.theme.smartbarButtonBgColor)
setDrawableTintColor2(clipboardSuggestion, prefs.theme.smartbarButtonFgColor)
clipboardSuggestion.setTextColor(prefs.theme.smartbarButtonFgColor)
}
R.id.candidates -> {
for (view in container.children) {

View File

@@ -88,7 +88,7 @@ data class Theme(
* @param context A reference to the current [Context]. Used to request
* asset file.
* @param path The path to the json theme file in the asset folder.
* @returns A parsed [Theme] or null. A null value may indicate that
* @return A parsed [Theme] or null. A null value may indicate that
* the file does not exist or that an error during the reading
* of the file occurred.
*/
@@ -105,7 +105,7 @@ data class Theme(
* Loads a theme from the given [rawData].
*
* @param rawData The raw json theme file as a string.
* @returns A parsed [Theme] or null. A null value may indicate that an error
* @return A parsed [Theme] or null. A null value may indicate that an error
* during the reading of the [rawData] occurred.
*/
fun fromJsonString(rawData: String): Theme? {
@@ -259,7 +259,7 @@ data class ThemeMetaOnly(
* @param context A reference to the current [Context]. Used to request
* asset file.
* @param path The path to the json theme file in the asset folder.
* @returns [ThemeMetaOnly] or null. A null value may indicate that
* @return [ThemeMetaOnly] or null. A null value may indicate that
* the file does not exist or that an error during the reading
* of the file occurred.
*/
@@ -282,7 +282,7 @@ data class ThemeMetaOnly(
* @param context A reference to the current [Context]. Used to request
* asset file.
* @param path The path to the dir in the asset folder.
* @returns [ThemeMetaOnly] or null. A null value may indicate that
* @return [ThemeMetaOnly] or null. A null value may indicate that
* the file does not exist or that an error during the reading
* of the file occurred.
*/

View File

@@ -146,7 +146,7 @@ class DialogSeekBarPreference : Preference {
* handle. (Android's SeekBar step is fixed at 1 and min at 0)
*
* @param actual The actual value.
* @returns the internal value which is used to allow different min and step values.
* @return the internal value which is used to allow different min and step values.
*/
private fun actualValueToSeekBarProgress(actual: Int): Int {
return (actual - min) / step
@@ -156,7 +156,7 @@ class DialogSeekBarPreference : Preference {
* Converts the Android SeekBar value to the actual value.
*
* @param progress The progress value of the SeekBar.
* @returns the actual value which is ready to use.
* @return the actual value which is ready to use.
*/
private fun seekBarProgressToActualValue(progress: Int): Int {
return (progress * step) + min

View File

@@ -33,6 +33,7 @@ import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
import dev.patrickgold.florisboard.ime.text.layout.LayoutManager
import dev.patrickgold.florisboard.settings.SettingsMainActivity
import kotlinx.coroutines.*
import kotlin.math.roundToInt
class ThemeFragment : SettingsMainActivity.SettingsFragment(), CoroutineScope by MainScope(),
SharedPreferences.OnSharedPreferenceChangeListener {
@@ -54,7 +55,7 @@ class ThemeFragment : SettingsMainActivity.SettingsFragment(), CoroutineScope by
keyboardView = KeyboardView(themeContext)
keyboardView.layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
resources.getDimension(R.dimen.textKeyboardView_baseHeight).roundToInt()
).apply {
val m = resources.getDimension(R.dimen.keyboard_preview_margin).toInt()
setMargins(m, m, m, m)

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false" android:color="?android:colorButtonNormal"/>
<item android:color="#FFFFFF"/>
</selector>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:left="8dp"
android:right="8dp"
android:drawable="@drawable/ic_content_paste"/>
</layer-list>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/black" />
<corners android:radius="@dimen/smartbar_radius" />
</shape>

View File

@@ -0,0 +1,49 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="8dp"
android:theme="@style/CrashDialogTheme">
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="@string/crash_dialog__description"/>
<Button
android:id="@+id/copy_to_clipboard"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="@string/crash_dialog__copy_to_clipboard"/>
<Button
android:id="@+id/open_bug_report_form"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="@string/crash_dialog__open_bug_report_form"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<TextView
android:id="@+id/stacktrace"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"/>
</ScrollView>
<Button
android:id="@+id/close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="@string/crash_dialog__close"/>
</LinearLayout>

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<dev.patrickgold.florisboard.ime.text.smartbar.SmartbarView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/smartbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -43,6 +42,18 @@
</LinearLayout>
<LinearLayout
android:id="@+id/clipboard_suggestion_row"
style="@style/SmartbarContainer"
android:visibility="gone">
<Button
android:id="@+id/clipboard_suggestion"
android:drawableStart="@drawable/ic_content_paste_with_padding"
style="@style/SmartbarQuickAction.ClipboardSuggestion"/>
</LinearLayout>
<LinearLayout
android:id="@+id/quick_actions"
style="@style/SmartbarContainer"
@@ -76,109 +87,10 @@
</LinearLayout>
<!-- TODO: integrate a KeyboardView instead of hardcoding these buttons -->
<LinearLayout
android:id="@+id/number_row"
style="@style/SmartbarContainer"
android:visibility="gone"
tools:ignore="HardcodedText">
<Button
android:id="@+id/number_row_1"
style="@style/SmartbarCandidate"
android:text="1"/>
<Button
android:id="@+id/number_row_2"
style="@style/SmartbarCandidate"
android:text="2"/>
<Button
android:id="@+id/number_row_3"
style="@style/SmartbarCandidate"
android:text="3"/>
<Button
android:id="@+id/number_row_4"
style="@style/SmartbarCandidate"
android:text="4"/>
<Button
android:id="@+id/number_row_5"
style="@style/SmartbarCandidate"
android:text="5"/>
<Button
android:id="@+id/number_row_6"
style="@style/SmartbarCandidate"
android:text="6"/>
<Button
android:id="@+id/number_row_7"
style="@style/SmartbarCandidate"
android:text="7"/>
<Button
android:id="@+id/number_row_8"
style="@style/SmartbarCandidate"
android:text="8"/>
<Button
android:id="@+id/number_row_9"
style="@style/SmartbarCandidate"
android:text="9"/>
<Button
android:id="@+id/number_row_0"
style="@style/SmartbarCandidate"
android:text="0"/>
</LinearLayout>
<!-- TODO: integrate a KeyboardView instead of hardcoding these buttons -->
<LinearLayout
<dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
android:id="@+id/clipboard_cursor_row"
style="@style/SmartbarContainer"
android:visibility="gone"
tools:ignore="HardcodedText">
<ImageButton
android:id="@+id/cc_select_all"
style="@style/SmartbarCandidate"
android:src="@drawable/ic_select_all"
android:tint="@drawable/button_key_enable_color_selector"/>
<ImageButton
android:id="@+id/cc_copy"
style="@style/SmartbarCandidate"
android:src="@drawable/ic_content_copy"
android:tint="@drawable/button_key_enable_color_selector"/>
<ImageButton
android:id="@+id/cc_arrow_left"
style="@style/SmartbarCandidate"
android:src="@drawable/ic_keyboard_arrow_left"
android:tint="@drawable/button_key_enable_color_selector"/>
<ImageButton
android:id="@+id/cc_arrow_right"
style="@style/SmartbarCandidate"
android:src="@drawable/ic_keyboard_arrow_right"
android:tint="@drawable/button_key_enable_color_selector"/>
<ImageButton
android:id="@+id/cc_cut"
style="@style/SmartbarCandidate"
android:src="@drawable/ic_content_cut"
android:tint="@drawable/button_key_enable_color_selector"/>
<ImageButton
android:id="@+id/cc_paste"
style="@style/SmartbarCandidate"
android:src="@drawable/ic_content_paste"
android:tint="@drawable/button_key_enable_color_selector"/>
</LinearLayout>
android:visibility="gone"/>
<!-- Placeholder on the right which reserves the space for a second button -->
<dev.patrickgold.florisboard.ime.text.smartbar.SmartbarQuickActionButton
@@ -190,6 +102,12 @@
</LinearLayout>
<dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
android:id="@+id/smartbar_variant_number_row"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"/>
<LinearLayout
android:id="@+id/smartbar_variant_back_only"
android:layout_width="match_parent"

View File

@@ -33,6 +33,7 @@
<dimen name="one_handed_button_height">@dimen/one_handed_width</dimen>
<dimen name="smartbar_height">40dp</dimen>
<dimen name="smartbar_radius">20dp</dimen>
<dimen name="smartbar_button_margin">4dp</dimen>
<dimen name="smartbar_button_padding">6dp</dimen>

View File

@@ -136,6 +136,8 @@
<string name="pref__suggestion__show_instead__label">What to show instead of suggestions</string>
<string name="pref__suggestion__show_instead__number_row">Number row</string>
<string name="pref__suggestion__show_instead__clipboard_cursor_tools">Clipboard cursor tools</string>
<string name="pref__suggestion__suggest_clipboard_content__label">Clipboard content suggestions</string>
<string name="pref__suggestion__suggest_clipboard_content__summary">Suggest clipboard content to paste if previously copied</string>
<string name="pref__suggestion__use_pref_words__label">[NYI] Next-word suggestions</string>
<string name="pref__suggestion__use_pref_words__summary">Use previous words for generating suggestions</string>
<string name="pref__correction__title">Corrections</string>
@@ -224,4 +226,16 @@
<string name="setup__make_default__text_after_switch">Successfully switched the default keyboard to FlorisBoard!</string>
<string name="setup__finish__title">Setup finished!</string>
<!-- Crash Dialog strings -->
<string name="crash_dialog__title">FlorisBoard error report</string>
<string name="crash_dialog__description">Sorry for the inconvenience, but FlorisBoard has crashed due to an unexpected error.\n\nIf you wish to report this error, click on "Copy to clipboard", then on the "Open bug report" button. Fill out the bug report and paste the log. This helps in making FlorisBoard better and more stable for everyone. Thank you!</string>
<string name="crash_dialog__copy_to_clipboard">Copy to clipboard</string>
<string name="crash_dialog__open_bug_report_form">Open bug report form (github.com)</string>
<string name="crash_dialog__close">Close</string>
<string name="crash_notification_channel__title">FlorisBoard error reports</string>
<string name="crash_once_notification__title">FlorisBoard has stopped working…</string>
<string name="crash_once_notification__body">Tap to view error details</string>
<string name="crash_multiple_notification__title">FlorisBoard seems to stop working repeatedly…</string>
<string name="crash_multiple_notification__body">Falling back to previous keyboard to stop infinite crash loop. Tap to view error details</string>
</resources>

View File

@@ -2,6 +2,8 @@
<string name="app_name" translatable="false">FlorisBoard</string>
<string name="florisboard__repo_url" translatable="false">https://github.com/florisboard/florisboard</string>
<string name="florisboard__issue_tracker_url" translatable="false">https://github.com/florisboard/florisboard/issues</string>
<string name="florisboard__issue_tracker_new_issue_url" translatable="false">https://github.com/florisboard/florisboard/issues/new</string>
<string name="florisboard__privacy_policy_url" translatable="false">https://gist.github.com/patrickgold/a18f1e47468d72f0868afc69d6faaf0b</string>
<string name="key__view_characters" translatable="false">ABC</string>

View File

@@ -58,6 +58,18 @@
<item name="android:tint">#000000</item>
</style>
<style name="SmartbarQuickAction.ClipboardSuggestion">
<item name="android:layout_width">200dp</item>
<item name="android:layout_height">match_parent</item>
<item name="android:layout_weight">0</item>
<item name="android:background">@drawable/shape_rect_rounded_2</item>
<item name="android:singleLine">true</item>
<item name="android:ellipsize">marquee</item>
<item name="android:fadingEdge">horizontal</item>
<item name="android:textAllCaps">false</item>
<item name="android:textStyle">normal</item>
</style>
<style name="SmartbarQuickAction.Toggle">
<item name="android:layout_weight">0</item>
<item name="android:autoMirrored">true</item>

View File

@@ -55,4 +55,6 @@
<item name="android:navigationBarColor">@color/navigationBarColor</item>
</style>
<style name="CrashDialogTheme" parent="Theme.AppCompat.DayNight"/>
</resources>

View File

@@ -8,12 +8,28 @@
<SwitchPreferenceCompat
android:defaultValue="false"
app:enabled="false"
app:enabled="true"
app:key="suggestion__enabled"
app:iconSpaceReserved="false"
app:title="@string/pref__suggestion__enabled__label"
app:summary="@string/pref__suggestion__enabled__summary"/>
<SwitchPreferenceCompat
android:defaultValue="true"
app:dependency="suggestion__enabled"
app:key="suggestion__use_prev_words"
app:iconSpaceReserved="false"
app:title="@string/pref__suggestion__use_pref_words__label"
app:summary="@string/pref__suggestion__use_pref_words__summary"/>
<SwitchPreferenceCompat
android:defaultValue="true"
app:dependency="suggestion__enabled"
app:key="suggestion__suggest_clipboard_content"
app:iconSpaceReserved="false"
app:title="@string/pref__suggestion__suggest_clipboard_content__label"
app:summary="@string/pref__suggestion__suggest_clipboard_content__summary"/>
<ListPreference
android:defaultValue="clipboard_cursor_tools"
app:entries="@array/pref__suggestion__show_instead__entries"
@@ -23,14 +39,6 @@
app:title="@string/pref__suggestion__show_instead__label"
app:useSimpleSummaryProvider="true"/>
<SwitchPreferenceCompat
android:defaultValue="true"
app:dependency="suggestion__enabled"
app:key="suggestion__use_prev_words"
app:iconSpaceReserved="false"
app:title="@string/pref__suggestion__use_pref_words__label"
app:summary="@string/pref__suggestion__use_pref_words__summary"/>
</PreferenceCategory>
<PreferenceCategory

View File

@@ -8,7 +8,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.2'
classpath 'com.android.tools.build:gradle:4.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong

View File

@@ -0,0 +1,5 @@
- Add clipboard content suggestions (#38)
- Add support for raw input editors (like terminal apps, etc.)
- Add crash handler and error form
- Improve layout of Smartbar and number row (#31)
- Rework core to fix potential crashes when entering text

View File

@@ -0,0 +1,5 @@
- Aggiungere suggerimenti per il contenuto degli appunti (#38)
- Aggiungere il supporto per gli editor di input grezzi (come le app dei terminali, ecc.)
- Aggiungere il gestore di crash e il modulo di errore
- Migliorare il layout della Smartbar e della fila di numeri (#31)
- Core di rilavorazione per correggere potenziali crash durante l'inserimento del testo

View File

@@ -1,6 +1,6 @@
#Fri May 29 19:10:09 CEST 2020
#Mon Nov 16 12:10:10 CET 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip