Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
970b5eb82a | ||
|
|
a2ceed4521 | ||
|
|
6d7825e129 | ||
|
|
10c1a82995 | ||
|
|
267a39e870 | ||
|
|
f6fcbbcc34 | ||
|
|
f98b3cec4b | ||
|
|
e5a942be9f |
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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) -->
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" }
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -24,5 +24,7 @@ enum class KeyboardMode {
|
||||
NUMERIC,
|
||||
NUMERIC_ADVANCED,
|
||||
PHONE,
|
||||
PHONE2
|
||||
PHONE2,
|
||||
SMARTBAR_CLIPBOARD_CURSOR_ROW,
|
||||
SMARTBAR_NUMBER_ROW
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
6
app/src/main/res/drawable/shape_rect_rounded_2.xml
Normal file
6
app/src/main/res/drawable/shape_rect_rounded_2.xml
Normal 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>
|
||||
49
app/src/main/res/layout/crash_dialog.xml
Normal file
49
app/src/main/res/layout/crash_dialog.xml
Normal 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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -55,4 +55,6 @@
|
||||
<item name="android:navigationBarColor">@color/navigationBarColor</item>
|
||||
</style>
|
||||
|
||||
<style name="CrashDialogTheme" parent="Theme.AppCompat.DayNight"/>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
5
fastlane/metadata/android/en-US/changelogs/16.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/16.txt
Normal 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
|
||||
5
fastlane/metadata/android/it/changelogs/16.txt
Normal file
5
fastlane/metadata/android/it/changelogs/16.txt
Normal 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
|
||||
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user