Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
970b5eb82a | ||
|
|
a2ceed4521 | ||
|
|
6d7825e129 | ||
|
|
10c1a82995 | ||
|
|
267a39e870 | ||
|
|
f6fcbbcc34 | ||
|
|
f98b3cec4b | ||
|
|
e5a942be9f | ||
|
|
edc63aa680 | ||
|
|
23def145b2 | ||
|
|
3f7bd4f65d | ||
|
|
7b91d4f9d3 | ||
|
|
175369f7d7 | ||
|
|
79c5acc007 | ||
|
|
94d470dd96 | ||
|
|
ee9d61ad1e | ||
|
|
a3c7b538d0 |
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 14
|
||||
versionName "0.2.2"
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,876 @@
|
||||
/*
|
||||
* 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.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
|
||||
|
||||
// Constants for detectLastUnicodeCharacterLengthBeforeCursor method
|
||||
private const val LIGHT_SKIN_TONE = 0x1F3FB
|
||||
private const val MEDIUM_LIGHT_SKIN_TONE = 0x1F3FC
|
||||
private const val MEDIUM_SKIN_TONE = 0x1F3FD
|
||||
private const val MEDIUM_DARK_SKIN_TONE = 0x1F3FE
|
||||
private const val DARK_SKIN_TONE = 0x1F3FF
|
||||
private const val RED_HAIR = 0x1F9B0
|
||||
private const val CURLY_HAIR = 0x1F9B1
|
||||
private const val WHITE_HAIR = 0x1F9B2
|
||||
private const val BALD = 0x1F9B3
|
||||
private const val ZERO_WIDTH_JOINER = 0x200D
|
||||
private const val VARIATION_SELECTOR = 0xFE0F
|
||||
|
||||
// Array which holds all variations for easier checking (convenience only)
|
||||
private val emojiVariationArray: Array<Int> = arrayOf(
|
||||
LIGHT_SKIN_TONE,
|
||||
MEDIUM_LIGHT_SKIN_TONE,
|
||||
MEDIUM_SKIN_TONE,
|
||||
MEDIUM_DARK_SKIN_TONE,
|
||||
DARK_SKIN_TONE,
|
||||
RED_HAIR,
|
||||
CURLY_HAIR,
|
||||
WHITE_HAIR,
|
||||
BALD
|
||||
)
|
||||
|
||||
/**
|
||||
* Class which holds information relevant to an editor instance like the input [cachedText], [selection],
|
||||
* [inputAttributes], [imeOptions], etc. This class is thought to be an improved [EditorInfo]
|
||||
* 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
|
||||
return InputAttributes.CapsMode.fromFlags(
|
||||
ic.getCursorCapsMode(inputAttributes.capsMode.toFlags())
|
||||
)
|
||||
}
|
||||
var currentWord: Region = Region(this)
|
||||
private set
|
||||
var imeOptions: ImeOptions = ImeOptions.fromImeOptionsInt(EditorInfo.IME_NULL)
|
||||
private set
|
||||
var inputAttributes: InputAttributes = InputAttributes.fromInputTypeInt(InputType.TYPE_NULL)
|
||||
private set
|
||||
var isComposingEnabled: Boolean = false
|
||||
set(v) {
|
||||
field = v
|
||||
reevaluateCurrentWord()
|
||||
if (v && !isRawInputEditor) {
|
||||
markComposingRegion(currentWord)
|
||||
} else {
|
||||
markComposingRegion(null)
|
||||
}
|
||||
}
|
||||
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
|
||||
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 {
|
||||
return EditorInstance(null)
|
||||
}
|
||||
|
||||
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
|
||||
/*selection = Selection(this).apply {
|
||||
start = editorInfo.initialSelStart
|
||||
end = editorInfo.initialSelEnd
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
updateEditorState()
|
||||
reevaluateCurrentWord()
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler which reacts to selection updates coming from the target app's editor.
|
||||
*/
|
||||
fun onUpdateSelection(
|
||||
oldSelStart: Int, oldSelEnd: Int,
|
||||
newSelStart: Int, newSelEnd: Int
|
||||
) {
|
||||
updateEditorState()
|
||||
isNewSelectionInBoundsOfOld =
|
||||
newSelStart >= (oldSelStart - 1) &&
|
||||
newSelStart <= (oldSelStart + 1) &&
|
||||
newSelEnd >= (oldSelEnd - 1) &&
|
||||
newSelEnd <= (oldSelEnd + 1)
|
||||
selection.apply {
|
||||
start = newSelStart
|
||||
end = newSelEnd
|
||||
}
|
||||
reevaluateCurrentWord()
|
||||
if (selection.isCursorMode && isComposingEnabled && !isRawInputEditor) {
|
||||
markComposingRegion(currentWord)
|
||||
} else {
|
||||
markComposingRegion(null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes the given [text] in the current composing region. Does nothing if the current
|
||||
* composing region is of zero length or null.
|
||||
*
|
||||
* @param text The text to complete in this editor's composing region.
|
||||
*
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun commitCompletion(text: String): Boolean {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits the given [text] to this editor instance and adjusts both the cursor position and
|
||||
* composing region, if any.
|
||||
*
|
||||
* This method overwrites any selected text and replaces it with given [text]. If there is no
|
||||
* text selected (selection is in cursor mode), then this method will insert the [text] after
|
||||
* the cursor, then set the cursor position to the first character after the inserted text.
|
||||
*
|
||||
* @param text The text to commit.
|
||||
*
|
||||
* @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
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a backward delete on this editor's text. If a text selection is active, all
|
||||
* characters inside this selection will be removed, else only the left-most character from
|
||||
* the cursor's position.
|
||||
*
|
||||
* @return True on success, false if an error occurred or the input connection is invalid.
|
||||
*/
|
||||
fun deleteBackwards(): Boolean {
|
||||
val ic = ims?.currentInputConnection ?: return false
|
||||
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)
|
||||
}
|
||||
updateEditorState()
|
||||
reevaluateCurrentWord()
|
||||
if (isComposingEnabled) {
|
||||
markComposingRegion(currentWord)
|
||||
}
|
||||
ic.endBatchEdit()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes [n] words before the current cursor's position.
|
||||
* NOTE: this implementation does currently only delete currentWord. This is due to change in
|
||||
* future versions.
|
||||
*
|
||||
* @param n The number of words to delete before the cursor. Must be greater than 0 or this
|
||||
* method will fail.
|
||||
*
|
||||
* @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
|
||||
return if (n < 1 || isRawInputEditor) {
|
||||
false
|
||||
} else {
|
||||
ic.beginBatchEdit()
|
||||
markComposingRegion(null)
|
||||
if (currentWord.isValid) {
|
||||
ic.setSelection(currentWord.start, currentWord.end)
|
||||
ic.commitText("", 1)
|
||||
}
|
||||
reevaluateCurrentWord()
|
||||
ic.endBatchEdit()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets [n] characters after the cursor's current position. The resulting string may be any
|
||||
* length ranging from 0 to n.
|
||||
*
|
||||
* @param n The number of characters to get after the cursor. Must be greater than 0 or this
|
||||
* method will fail.
|
||||
*
|
||||
* @return [n] or less characters after the cursor.
|
||||
*/
|
||||
fun getTextAfterCursor(n: Int): String {
|
||||
if (!selection.isValid || n < 1 || isRawInputEditor) {
|
||||
return ""
|
||||
}
|
||||
val from = selection.end.coerceIn(0, cachedText.length)
|
||||
val to = (selection.end + n).coerceIn(0, cachedText.length)
|
||||
return cachedText.substring(from, to)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets [n] characters before the cursor's current position. The resulting string may be any
|
||||
* length ranging from 0 to n.
|
||||
*
|
||||
* @param n The number of characters to get before the cursor. Must be greater than 0 or this
|
||||
* method will fail.
|
||||
*
|
||||
* @return [n] or less characters after the cursor.
|
||||
*/
|
||||
fun getTextBeforeCursor(n: Int): String {
|
||||
if (!selection.isValid || n < 1 || isRawInputEditor) {
|
||||
return ""
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a given [action] on the current input editor.
|
||||
*
|
||||
* @param action The action to be performed on this editor instance.
|
||||
*
|
||||
* @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
|
||||
return ic.performEditorAction(action.toInt())
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a given [keyCode] as a [KeyEvent.ACTION_DOWN].
|
||||
*
|
||||
* @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!!
|
||||
*
|
||||
* @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
|
||||
return ic.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, keyCode))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a given [keyCode] as a [KeyEvent.ACTION_DOWN] with ALT pressed.
|
||||
*
|
||||
* @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!!
|
||||
*
|
||||
* @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
|
||||
return ic.sendKeyEvent(
|
||||
KeyEvent(
|
||||
0,
|
||||
1,
|
||||
KeyEvent.ACTION_DOWN, keyCode,
|
||||
0,
|
||||
KeyEvent.META_ALT_LEFT_ON
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the selection region of this instance and notifies the input connection.
|
||||
*
|
||||
* @param from The start index of the selection in characters (inclusive).
|
||||
* @param to The end index of the selection in characters (exclusive).
|
||||
*
|
||||
* @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
|
||||
return if (isRawInputEditor) {
|
||||
selection.apply {
|
||||
start = -1
|
||||
end = -1
|
||||
}
|
||||
false
|
||||
} else {
|
||||
selection.apply {
|
||||
start = from
|
||||
end = to
|
||||
}
|
||||
ic.setSelection(from, to)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the length of the character before the cursor, as many Unicode characters nowadays
|
||||
* are longer than 1 Java char and thus the length has to be calculated in order to avoid
|
||||
* deleting only half of an emoji...
|
||||
* Is used primarily in [deleteBackwards].
|
||||
*
|
||||
* @return The length of the last Unicode character, in Java characters or 0 if the current
|
||||
* selection is invalid.
|
||||
*/
|
||||
private fun detectLastUnicodeCharacterLengthBeforeCursor(): Int {
|
||||
if (!selection.isValid) {
|
||||
return 0
|
||||
}
|
||||
var charIndex = 0
|
||||
var charLength = 0
|
||||
var charShouldGlue = false
|
||||
val textToSearch = cachedText.substring(0, selection.start.coerceAtMost(cachedText.length))
|
||||
var i = 0
|
||||
while (i < textToSearch.length) {
|
||||
val cp = textToSearch.codePointAt(i)
|
||||
val cpLength = Character.charCount(cp)
|
||||
when {
|
||||
charShouldGlue || cp == VARIATION_SELECTOR || emojiVariationArray.contains(cp) -> {
|
||||
charLength += cpLength
|
||||
charShouldGlue = false
|
||||
}
|
||||
cp == ZERO_WIDTH_JOINER -> {
|
||||
charLength += cpLength
|
||||
charShouldGlue = true
|
||||
}
|
||||
else -> {
|
||||
charIndex = i
|
||||
charLength = 0
|
||||
charShouldGlue = false
|
||||
}
|
||||
}
|
||||
i += cpLength
|
||||
}
|
||||
return textToSearch.length - charIndex
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a given [region] as composing and notifies the input connection.
|
||||
*
|
||||
* @param region The region which should be marked as composing.
|
||||
*
|
||||
* @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
|
||||
return when (region) {
|
||||
null -> ic.finishComposingText()
|
||||
else -> if (region.isValid) {
|
||||
ic.setComposingRegion(region.start, region.end)
|
||||
} else {
|
||||
ic.finishComposingText()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the current word in this editor instance based on the current cursor position and
|
||||
* given delimiter [regex].
|
||||
*
|
||||
* @param regex The delimiter regex which should be used to split up the content text and find
|
||||
* words. May differ from locale to locale.
|
||||
*
|
||||
* @return True on success, false if no current word could be found.
|
||||
*/
|
||||
private fun reevaluateCurrentWord(regex: Regex): Boolean {
|
||||
var foundValidWord = false
|
||||
if (selection.isValid && selection.isCursorMode) {
|
||||
val words = cachedText.split("((?<=$regex)|(?=$regex))".toRegex())
|
||||
var pos = 0
|
||||
for (word in words) {
|
||||
if (selection.start >= pos && selection.start <= pos + word.length &&
|
||||
word.isNotEmpty() && !word.matches(regex)) {
|
||||
currentWord.apply {
|
||||
start = pos
|
||||
end = pos + word.length
|
||||
}
|
||||
foundValidWord = true
|
||||
break
|
||||
} else {
|
||||
pos += word.length
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!foundValidWord) {
|
||||
currentWord.apply {
|
||||
start = -1
|
||||
end = -1
|
||||
}
|
||||
}
|
||||
return foundValidWord
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the current word with the correct delimiter regex for current subtype.
|
||||
* TODO: currently only supports en-US
|
||||
*/
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class which holds the same information as an [EditorInfo.imeOptions] int but more accessible and
|
||||
* readable.
|
||||
*/
|
||||
class ImeOptions private constructor(imeOptions: Int) {
|
||||
val action: Action = Action.fromInt(imeOptions)
|
||||
val flagForceAscii: Boolean = imeOptions and EditorInfo.IME_FLAG_FORCE_ASCII > 0
|
||||
val flagNavigateNext: Boolean = imeOptions and EditorInfo.IME_FLAG_NAVIGATE_NEXT > 0
|
||||
val flagNavigatePrevious: Boolean = imeOptions and EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS > 0
|
||||
val flagNoAccessoryAction: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_ACCESSORY_ACTION > 0
|
||||
val flagNoEnterAction: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_ENTER_ACTION > 0
|
||||
val flagNoExtractUi: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_EXTRACT_UI > 0
|
||||
val flagNoFullscreen: Boolean = imeOptions and EditorInfo.IME_FLAG_NO_FULLSCREEN > 0
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
enum class Action {
|
||||
DONE,
|
||||
GO,
|
||||
NEXT,
|
||||
NONE,
|
||||
PREVIOUS,
|
||||
SEARCH,
|
||||
SEND,
|
||||
UNSPECIFIED;
|
||||
|
||||
companion object {
|
||||
fun fromInt(raw: Int): Action {
|
||||
return when (raw and EditorInfo.IME_MASK_ACTION) {
|
||||
EditorInfo.IME_ACTION_DONE -> DONE
|
||||
EditorInfo.IME_ACTION_GO -> GO
|
||||
EditorInfo.IME_ACTION_NEXT -> NEXT
|
||||
EditorInfo.IME_ACTION_NONE -> NONE
|
||||
EditorInfo.IME_ACTION_PREVIOUS -> PREVIOUS
|
||||
EditorInfo.IME_ACTION_SEARCH -> SEARCH
|
||||
EditorInfo.IME_ACTION_SEND -> SEND
|
||||
EditorInfo.IME_ACTION_UNSPECIFIED -> UNSPECIFIED
|
||||
else -> NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toInt(): Int {
|
||||
return when (this) {
|
||||
DONE -> EditorInfo.IME_ACTION_DONE
|
||||
GO -> EditorInfo.IME_ACTION_GO
|
||||
NEXT -> EditorInfo.IME_ACTION_NEXT
|
||||
NONE -> EditorInfo.IME_ACTION_NONE
|
||||
PREVIOUS -> EditorInfo.IME_ACTION_PREVIOUS
|
||||
SEARCH -> EditorInfo.IME_ACTION_SEARCH
|
||||
SEND -> EditorInfo.IME_ACTION_SEND
|
||||
UNSPECIFIED-> EditorInfo.IME_ACTION_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class which holds the same information as an [EditorInfo.inputType] int but more accessible and
|
||||
* readable.
|
||||
*/
|
||||
class InputAttributes private constructor(inputType: Int) {
|
||||
val type: Type
|
||||
val variation: Variation
|
||||
val capsMode: CapsMode
|
||||
var flagNumberDecimal: Boolean = false
|
||||
private set
|
||||
var flagNumberSigned: Boolean = false
|
||||
private set
|
||||
var flagTextAutoComplete: Boolean = false
|
||||
private set
|
||||
var flagTextAutoCorrect: Boolean = false
|
||||
private set
|
||||
var flagTextImeMultiLine: Boolean = false
|
||||
private set
|
||||
var flagTextMultiLine: Boolean = false
|
||||
private set
|
||||
var flagTextNoSuggestions: Boolean = false
|
||||
private set
|
||||
|
||||
init {
|
||||
when (inputType and InputType.TYPE_MASK_CLASS) {
|
||||
InputType.TYPE_CLASS_DATETIME -> {
|
||||
type = Type.DATETIME
|
||||
variation = when (inputType and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_DATETIME_VARIATION_DATE -> Variation.DATE
|
||||
InputType.TYPE_DATETIME_VARIATION_NORMAL -> Variation.NORMAL
|
||||
InputType.TYPE_DATETIME_VARIATION_TIME -> Variation.TIME
|
||||
else -> Variation.NORMAL
|
||||
}
|
||||
capsMode = CapsMode.NONE
|
||||
}
|
||||
InputType.TYPE_CLASS_NUMBER -> {
|
||||
type = Type.NUMBER
|
||||
variation = when (inputType and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_NUMBER_VARIATION_NORMAL -> Variation.NORMAL
|
||||
InputType.TYPE_NUMBER_VARIATION_PASSWORD -> Variation.PASSWORD
|
||||
else -> Variation.NORMAL
|
||||
}
|
||||
capsMode = CapsMode.NONE
|
||||
flagNumberDecimal = inputType and InputType.TYPE_NUMBER_FLAG_DECIMAL > 0
|
||||
flagNumberSigned = inputType and InputType.TYPE_NUMBER_FLAG_SIGNED > 0
|
||||
}
|
||||
InputType.TYPE_CLASS_PHONE -> {
|
||||
type = Type.PHONE
|
||||
variation = Variation.NORMAL
|
||||
capsMode = CapsMode.NONE
|
||||
}
|
||||
InputType.TYPE_CLASS_TEXT -> {
|
||||
type = Type.TEXT
|
||||
variation = when (inputType and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS -> Variation.EMAIL_ADDRESS
|
||||
InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT -> Variation.EMAIL_SUBJECT
|
||||
InputType.TYPE_TEXT_VARIATION_FILTER -> Variation.FILTER
|
||||
InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE -> Variation.LONG_MESSAGE
|
||||
InputType.TYPE_TEXT_VARIATION_NORMAL -> Variation.NORMAL
|
||||
InputType.TYPE_TEXT_VARIATION_PASSWORD -> Variation.PASSWORD
|
||||
InputType.TYPE_TEXT_VARIATION_PERSON_NAME -> Variation.PERSON_NAME
|
||||
InputType.TYPE_TEXT_VARIATION_PHONETIC -> Variation.PHONETIC
|
||||
InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS -> Variation.POSTAL_ADDRESS
|
||||
InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE -> Variation.SHORT_MESSAGE
|
||||
InputType.TYPE_TEXT_VARIATION_URI -> Variation.URI
|
||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD -> Variation.VISIBLE_PASSWORD
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT -> Variation.WEB_EDIT_TEXT
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS -> Variation.WEB_EMAIL_ADDRESS
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD -> Variation.WEB_PASSWORD
|
||||
else -> Variation.NORMAL
|
||||
}
|
||||
capsMode = CapsMode.fromFlags(inputType)
|
||||
flagTextAutoComplete = inputType and InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE > 0
|
||||
flagTextAutoCorrect = inputType and InputType.TYPE_TEXT_FLAG_AUTO_CORRECT > 0
|
||||
flagTextImeMultiLine = inputType and InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE > 0
|
||||
flagTextMultiLine = inputType and InputType.TYPE_TEXT_FLAG_MULTI_LINE > 0
|
||||
flagTextNoSuggestions = inputType and InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS > 0
|
||||
}
|
||||
else -> {
|
||||
type = Type.TEXT
|
||||
variation = Variation.NORMAL
|
||||
capsMode = CapsMode.NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromInputTypeInt(inputType: Int): InputAttributes {
|
||||
return InputAttributes(inputType)
|
||||
}
|
||||
}
|
||||
|
||||
enum class Type {
|
||||
DATETIME,
|
||||
NUMBER,
|
||||
PHONE,
|
||||
TEXT;
|
||||
}
|
||||
|
||||
enum class Variation {
|
||||
DATE,
|
||||
EMAIL_ADDRESS,
|
||||
EMAIL_SUBJECT,
|
||||
FILTER,
|
||||
LONG_MESSAGE,
|
||||
NORMAL,
|
||||
PASSWORD,
|
||||
PERSON_NAME,
|
||||
PHONETIC,
|
||||
POSTAL_ADDRESS,
|
||||
SHORT_MESSAGE,
|
||||
TIME,
|
||||
URI,
|
||||
VISIBLE_PASSWORD,
|
||||
WEB_EDIT_TEXT,
|
||||
WEB_EMAIL_ADDRESS,
|
||||
WEB_PASSWORD;
|
||||
}
|
||||
|
||||
enum class CapsMode {
|
||||
ALL,
|
||||
NONE,
|
||||
SENTENCES,
|
||||
WORDS;
|
||||
|
||||
companion object {
|
||||
fun fromFlags(flags: Int): CapsMode {
|
||||
return when {
|
||||
flags and InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS > 0 -> ALL
|
||||
flags and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES > 0 -> SENTENCES
|
||||
flags and InputType.TYPE_TEXT_FLAG_CAP_WORDS > 0 -> WORDS
|
||||
else -> NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toFlags(): Int {
|
||||
return when (this) {
|
||||
ALL -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
|
||||
SENTENCES -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
||||
WORDS -> InputType.TYPE_TEXT_FLAG_CAP_WORDS
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class which marks a region of the [text] in [editorInstance].
|
||||
*/
|
||||
open class Region(private val editorInstance: EditorInstance) {
|
||||
var start: Int = -1
|
||||
var end: Int = -1
|
||||
val isValid: Boolean
|
||||
get() = start >= 0 && end >= 0 && length >= 0
|
||||
val length: Int
|
||||
get() = end - start
|
||||
val text: String
|
||||
get() {
|
||||
val eiText = editorInstance.cachedText
|
||||
return if (!isValid || start >= eiText.length) {
|
||||
""
|
||||
} else {
|
||||
val end = if (end >= eiText.length) { eiText.length } else { end }
|
||||
editorInstance.cachedText.substring(start, end)
|
||||
}
|
||||
}
|
||||
|
||||
override operator fun equals(other: Any?): Boolean {
|
||||
return if (other is Region) {
|
||||
start == other.start && end == other.end
|
||||
} else {
|
||||
super.equals(other)
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = start
|
||||
result = 31 * result + end
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class which holds selection attributes and returns the correct text for set selection based on
|
||||
* the text in [editorInstance].
|
||||
*/
|
||||
class Selection(private val editorInstance: EditorInstance) : Region(editorInstance) {
|
||||
val isCursorMode: Boolean
|
||||
get() = length == 0 && isValid
|
||||
val isSelectionMode: Boolean
|
||||
get() = length != 0 && isValid
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,6 @@ import android.util.Log
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.CursorAnchorInfo
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputConnection
|
||||
import android.widget.ImageButton
|
||||
@@ -44,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
|
||||
@@ -55,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
|
||||
|
||||
@@ -64,13 +64,15 @@ 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
|
||||
private var vibrator: Vibrator? = null
|
||||
private val osHandler = Handler()
|
||||
|
||||
var activeEditorInstance: EditorInstance = EditorInstance.default()
|
||||
|
||||
lateinit var subtypeManager: SubtypeManager
|
||||
lateinit var activeSubtype: Subtype
|
||||
private var currentThemeIsNight: Boolean = false
|
||||
@@ -88,6 +90,7 @@ class FlorisBoard : InputMethodService() {
|
||||
|
||||
companion object {
|
||||
private const val IME_ID: String = "dev.patrickgold.florisboard/.ime.core.FlorisBoard"
|
||||
private val TAG: String? = FlorisBoard::class.simpleName
|
||||
|
||||
fun checkIfImeIsEnabled(context: Context): Boolean {
|
||||
val activeImeIds = Settings.Secure.getString(
|
||||
@@ -144,10 +147,11 @@ class FlorisBoard : InputMethodService() {
|
||||
.build()
|
||||
)
|
||||
}
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onCreate()")
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onCreate()")
|
||||
|
||||
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()
|
||||
@@ -163,24 +167,24 @@ class FlorisBoard : InputMethodService() {
|
||||
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
|
||||
|
||||
super.onCreate()
|
||||
eventListeners.toList().forEach { it.onCreate() }
|
||||
eventListeners.toList().forEach { it.get()?.onCreate() }
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
override fun onCreateInputView(): View? {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onCreateInputView()")
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onCreateInputView()")
|
||||
|
||||
baseContext.setTheme(currentThemeResId)
|
||||
|
||||
inputWindowView = layoutInflater.inflate(R.layout.florisboard, null) as InputWindowView
|
||||
|
||||
eventListeners.toList().forEach { it.onCreateInputView() }
|
||||
eventListeners.toList().forEach { it.get()?.onCreateInputView() }
|
||||
|
||||
return inputWindowView
|
||||
}
|
||||
|
||||
fun registerInputView(inputView: InputView) {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "registerInputView(inputView)")
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "registerInputView($inputView)")
|
||||
|
||||
this.inputView = inputView
|
||||
initializeOneHandedEnvironment()
|
||||
@@ -188,36 +192,59 @@ 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(this::class.simpleName, "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()
|
||||
}
|
||||
|
||||
override fun onStartInput(attribute: EditorInfo?, restarting: Boolean) {
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onStartInput($attribute, $restarting)")
|
||||
|
||||
super.onStartInput(attribute, restarting)
|
||||
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_IMMEDIATE)
|
||||
}
|
||||
|
||||
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
|
||||
currentInputConnection?.requestCursorUpdates(InputConnection.CURSOR_UPDATE_MONITOR)
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onStartInputView($info, $restarting)")
|
||||
Log.i(TAG, "onStartInputView: " + info?.debugSummarize())
|
||||
|
||||
super.onStartInputView(info, restarting)
|
||||
eventListeners.toList().forEach { it.onStartInputView(info, restarting) }
|
||||
activeEditorInstance = EditorInstance.from(info, this)
|
||||
eventListeners.toList().forEach {
|
||||
it.get()?.onStartInputView(activeEditorInstance, restarting)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinishInputView(finishingInput: Boolean) {
|
||||
currentInputConnection?.requestCursorUpdates(0)
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onFinishInputView($finishingInput)")
|
||||
|
||||
if (finishingInput) {
|
||||
activeEditorInstance = EditorInstance.default()
|
||||
}
|
||||
|
||||
super.onFinishInputView(finishingInput)
|
||||
eventListeners.toList().forEach { it.onFinishInputView(finishingInput) }
|
||||
eventListeners.toList().forEach { it.get()?.onFinishInputView(finishingInput) }
|
||||
}
|
||||
|
||||
override fun onFinishInput() {
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onFinishInput()")
|
||||
|
||||
super.onFinishInput()
|
||||
currentInputConnection?.requestCursorUpdates(0)
|
||||
}
|
||||
|
||||
override fun onWindowShown() {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onWindowShown()")
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onWindowShown()")
|
||||
|
||||
prefs.sync()
|
||||
updateTheme()
|
||||
@@ -227,17 +254,18 @@ 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(this::class.simpleName, "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) {
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onConfigurationChanged($newConfig)")
|
||||
if (isInputViewShown) {
|
||||
updateOneHandedPanelVisibility()
|
||||
}
|
||||
@@ -245,37 +273,23 @@ class FlorisBoard : InputMethodService() {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
}
|
||||
|
||||
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
|
||||
super.onUpdateCursorAnchorInfo(cursorAnchorInfo)
|
||||
eventListeners.toList().forEach { it.onUpdateCursorAnchorInfo(cursorAnchorInfo) }
|
||||
}
|
||||
|
||||
override fun onUpdateSelection(
|
||||
oldSelStart: Int,
|
||||
oldSelEnd: Int,
|
||||
newSelStart: Int,
|
||||
newSelEnd: Int,
|
||||
candidatesStart: Int,
|
||||
candidatesEnd: Int
|
||||
oldSelStart: Int, oldSelEnd: Int,
|
||||
newSelStart: Int, newSelEnd: Int,
|
||||
candidatesStart: Int, candidatesEnd: Int
|
||||
) {
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onUpdateSelection($oldSelStart, $oldSelEnd, $newSelStart, $newSelEnd, $candidatesStart, $candidatesEnd)")
|
||||
|
||||
super.onUpdateSelection(
|
||||
oldSelStart,
|
||||
oldSelEnd,
|
||||
newSelStart,
|
||||
newSelEnd,
|
||||
candidatesStart,
|
||||
candidatesEnd
|
||||
oldSelStart, oldSelEnd,
|
||||
newSelStart, newSelEnd,
|
||||
candidatesStart, candidatesEnd
|
||||
)
|
||||
eventListeners.toList().forEach {
|
||||
it.onUpdateSelection(
|
||||
oldSelStart,
|
||||
oldSelEnd,
|
||||
newSelStart,
|
||||
newSelEnd,
|
||||
candidatesStart,
|
||||
candidatesEnd
|
||||
)
|
||||
}
|
||||
activeEditorInstance.onUpdateSelection(
|
||||
oldSelStart, oldSelEnd,
|
||||
newSelStart, newSelEnd
|
||||
)
|
||||
eventListeners.toList().forEach { it.get()?.onUpdateSelection() }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -311,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?) {
|
||||
@@ -523,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 {
|
||||
@@ -550,23 +573,16 @@ class FlorisBoard : InputMethodService() {
|
||||
fun onRegisterInputView(inputView: InputView) {}
|
||||
fun onDestroy() {}
|
||||
|
||||
fun onStartInputView(info: EditorInfo?, restarting: Boolean) {}
|
||||
fun onStartInputView(instance: EditorInstance, restarting: Boolean) {}
|
||||
fun onFinishInputView(finishingInput: Boolean) {}
|
||||
|
||||
fun onWindowShown() {}
|
||||
fun onWindowHidden() {}
|
||||
|
||||
fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {}
|
||||
fun onUpdateSelection(
|
||||
oldSelStart: Int,
|
||||
oldSelEnd: Int,
|
||||
newSelStart: Int,
|
||||
newSelEnd: Int,
|
||||
candidatesStart: Int,
|
||||
candidatesEnd: Int
|
||||
) {}
|
||||
fun onUpdateSelection() {}
|
||||
|
||||
fun onApplyThemeAttributes() {}
|
||||
fun onPrimaryClipChanged() {}
|
||||
fun onSubtypeChanged(newSubtype: Subtype) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -180,17 +180,20 @@ class PrefHelper(
|
||||
*/
|
||||
class Correction(private val prefHelper: PrefHelper) {
|
||||
companion object {
|
||||
const val AUTO_CAPITALIZATION = "correction__auto_capitalization"
|
||||
const val DOUBLE_SPACE_PERIOD = "correction__double_space_period"
|
||||
const val AUTO_CAPITALIZATION = "correction__auto_capitalization"
|
||||
const val DOUBLE_SPACE_PERIOD = "correction__double_space_period"
|
||||
const val REMEMBER_CAPS_LOCK_STATE = "correction__remember_caps_lock_state"
|
||||
}
|
||||
|
||||
var autoCapitalization: Boolean = false
|
||||
get() = prefHelper.getPref(AUTO_CAPITALIZATION, true)
|
||||
private set
|
||||
|
||||
var doubleSpacePeriod: Boolean = false
|
||||
get() = prefHelper.getPref(DOUBLE_SPACE_PERIOD, true)
|
||||
private set
|
||||
var autoCapitalization: Boolean
|
||||
get() = prefHelper.getPref(AUTO_CAPITALIZATION, true)
|
||||
set(v) = prefHelper.setPref(AUTO_CAPITALIZATION, v)
|
||||
var doubleSpacePeriod: Boolean
|
||||
get() = prefHelper.getPref(DOUBLE_SPACE_PERIOD, true)
|
||||
set(v) = prefHelper.setPref(DOUBLE_SPACE_PERIOD, v)
|
||||
var rememberCapsLockState: Boolean
|
||||
get() = prefHelper.getPref(REMEMBER_CAPS_LOCK_STATE, false)
|
||||
set(v) = prefHelper.setPref(REMEMBER_CAPS_LOCK_STATE, v)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -370,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
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.widget.*
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
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.InputView
|
||||
import dev.patrickgold.florisboard.ime.media.emoji.EmojiKeyData
|
||||
@@ -50,6 +51,8 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
FlorisBoard.EventListener {
|
||||
|
||||
private val florisboard = FlorisBoard.getInstance()
|
||||
private val activeEditorInstance: EditorInstance
|
||||
get() = florisboard.activeEditorInstance
|
||||
|
||||
private var activeTab: Tab? = null
|
||||
private var mediaViewFlipper: ViewFlipper? = null
|
||||
@@ -199,18 +202,14 @@ class MediaInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* Sends a given [emojiKeyData] to the current input editor.
|
||||
*/
|
||||
fun sendEmojiKeyPress(emojiKeyData: EmojiKeyData) {
|
||||
val ic = florisboard.currentInputConnection
|
||||
ic?.finishComposingText()
|
||||
ic?.commitText(emojiKeyData.getCodePointsAsString(), 1)
|
||||
activeEditorInstance.commitText(emojiKeyData.getCodePointsAsString())
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a given [emoticonKeyData] to the current input editor.
|
||||
*/
|
||||
fun sendEmoticonKeyPress(emoticonKeyData: EmoticonKeyData) {
|
||||
val ic = florisboard.currentInputConnection
|
||||
ic?.finishComposingText()
|
||||
ic?.commitText(emoticonKeyData.icon, 1)
|
||||
activeEditorInstance.commitText(emoticonKeyData.icon)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,10 +16,8 @@
|
||||
|
||||
package dev.patrickgold.florisboard.ime.text
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.text.InputType
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.inputmethod.*
|
||||
@@ -27,9 +25,7 @@ import android.widget.LinearLayout
|
||||
import android.widget.ViewFlipper
|
||||
import dev.patrickgold.florisboard.BuildConfig
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
import dev.patrickgold.florisboard.ime.core.InputView
|
||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||
import dev.patrickgold.florisboard.ime.core.*
|
||||
import dev.patrickgold.florisboard.ime.text.editing.EditingKeyboardView
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
|
||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||
@@ -59,6 +55,8 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
FlorisBoard.EventListener {
|
||||
|
||||
private val florisboard = FlorisBoard.getInstance()
|
||||
private val activeEditorInstance: EditorInstance
|
||||
get() = florisboard.activeEditorInstance
|
||||
|
||||
private var activeKeyboardMode: KeyboardMode? = null
|
||||
private val keyboardViews = EnumMap<KeyboardMode, KeyboardView>(KeyboardMode::class.java)
|
||||
@@ -68,36 +66,24 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
var textViewGroup: LinearLayout? = null
|
||||
|
||||
var keyVariation: KeyVariation = KeyVariation.NORMAL
|
||||
private val layoutManager = LayoutManager(florisboard)
|
||||
lateinit var smartbarManager: SmartbarManager
|
||||
val layoutManager = LayoutManager(florisboard)
|
||||
private lateinit var smartbarManager: SmartbarManager
|
||||
|
||||
// Caps/Space related properties
|
||||
var caps: Boolean = false
|
||||
private set
|
||||
var capsLock: Boolean = false
|
||||
private set
|
||||
private var cursorCapsMode: CapsMode = CapsMode.NONE
|
||||
private var editorCapsMode: CapsMode = CapsMode.NONE
|
||||
private var hasCapsRecentlyChanged: Boolean = false
|
||||
private var hasSpaceRecentlyPressed: Boolean = false
|
||||
|
||||
// Composing text related properties
|
||||
private var composingText: String? = null
|
||||
private var composingTextStart: Int? = null
|
||||
private var cursorPos: Int = 0
|
||||
private var isComposingEnabled: Boolean = false
|
||||
var isManualSelectionMode: Boolean = false
|
||||
private var isManualSelectionModeLeft: Boolean = false
|
||||
private var isManualSelectionModeRight: Boolean = false
|
||||
val isTextSelected: Boolean
|
||||
get() = selectionEnd - selectionStart != 0
|
||||
private var lastCursorAnchorInfo: CursorAnchorInfo? = null
|
||||
private var selectionStart: Int = 0
|
||||
private val selectionStartMin: Int = 0
|
||||
private var selectionEnd: Int = 0
|
||||
private var selectionEndMax: Int = 0
|
||||
|
||||
companion object {
|
||||
private val TAG: String? = TextInputManager::class.simpleName
|
||||
private var instance: TextInputManager? = null
|
||||
|
||||
@Synchronized
|
||||
@@ -118,7 +104,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* background).
|
||||
*/
|
||||
override fun onCreate() {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onCreate()")
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onCreate()")
|
||||
|
||||
var subtypes = florisboard.subtypeManager.subtypes
|
||||
if (subtypes.isEmpty()) {
|
||||
@@ -145,7 +131,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* Sets up the newly registered input view.
|
||||
*/
|
||||
override fun onRegisterInputView(inputView: InputView) {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onRegisterInputView(inputView)")
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onRegisterInputView(inputView)")
|
||||
|
||||
launch(Dispatchers.Default) {
|
||||
textViewGroup = inputView.findViewById(R.id.text_input)
|
||||
@@ -158,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)
|
||||
}
|
||||
}
|
||||
@@ -169,7 +155,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* Cancels all coroutines and cleans up.
|
||||
*/
|
||||
override fun onDestroy() {
|
||||
if (BuildConfig.DEBUG) Log.i(this::class.simpleName, "onDestroy()")
|
||||
if (BuildConfig.DEBUG) Log.i(TAG, "onDestroy()")
|
||||
|
||||
cancel()
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
@@ -179,58 +165,60 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the [activeKeyboardMode], [keyVariation] and [isComposingEnabled] property values
|
||||
* when starting to interact with a input editor. Also resets the composing texts and sets the
|
||||
* initial caps mode accordingly.
|
||||
* Evaluates the [activeKeyboardMode], [keyVariation] and [EditorInstance.isComposingEnabled]
|
||||
* property values when starting to interact with a input editor. Also resets the composing
|
||||
* texts and sets the initial caps mode accordingly.
|
||||
*/
|
||||
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
|
||||
val keyboardMode = when (info) {
|
||||
null -> KeyboardMode.CHARACTERS
|
||||
else -> when (info.inputType and InputType.TYPE_MASK_CLASS) {
|
||||
InputType.TYPE_CLASS_NUMBER -> {
|
||||
keyVariation = KeyVariation.NORMAL
|
||||
KeyboardMode.NUMERIC
|
||||
}
|
||||
InputType.TYPE_CLASS_PHONE -> {
|
||||
keyVariation = KeyVariation.NORMAL
|
||||
KeyboardMode.PHONE
|
||||
}
|
||||
InputType.TYPE_CLASS_TEXT -> {
|
||||
keyVariation = when (info.inputType and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS -> {
|
||||
KeyVariation.EMAIL_ADDRESS
|
||||
}
|
||||
InputType.TYPE_TEXT_VARIATION_PASSWORD,
|
||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD -> {
|
||||
KeyVariation.PASSWORD
|
||||
}
|
||||
InputType.TYPE_TEXT_VARIATION_URI -> {
|
||||
KeyVariation.URI
|
||||
}
|
||||
else -> {
|
||||
KeyVariation.NORMAL
|
||||
}
|
||||
override fun onStartInputView(instance: EditorInstance, restarting: Boolean) {
|
||||
val keyboardMode = when (instance.inputAttributes.type) {
|
||||
InputAttributes.Type.NUMBER -> {
|
||||
keyVariation = KeyVariation.NORMAL
|
||||
KeyboardMode.NUMERIC
|
||||
}
|
||||
InputAttributes.Type.PHONE -> {
|
||||
keyVariation = KeyVariation.NORMAL
|
||||
KeyboardMode.PHONE
|
||||
}
|
||||
InputAttributes.Type.TEXT -> {
|
||||
keyVariation = when (instance.inputAttributes.variation) {
|
||||
InputAttributes.Variation.EMAIL_ADDRESS,
|
||||
InputAttributes.Variation.WEB_EMAIL_ADDRESS -> {
|
||||
KeyVariation.EMAIL_ADDRESS
|
||||
}
|
||||
InputAttributes.Variation.PASSWORD,
|
||||
InputAttributes.Variation.VISIBLE_PASSWORD,
|
||||
InputAttributes.Variation.WEB_PASSWORD -> {
|
||||
KeyVariation.PASSWORD
|
||||
}
|
||||
InputAttributes.Variation.URI -> {
|
||||
KeyVariation.URI
|
||||
}
|
||||
else -> {
|
||||
KeyVariation.NORMAL
|
||||
}
|
||||
KeyboardMode.CHARACTERS
|
||||
}
|
||||
else -> {
|
||||
keyVariation = KeyVariation.NORMAL
|
||||
KeyboardMode.CHARACTERS
|
||||
}
|
||||
KeyboardMode.CHARACTERS
|
||||
}
|
||||
else -> {
|
||||
keyVariation = KeyVariation.NORMAL
|
||||
KeyboardMode.CHARACTERS
|
||||
}
|
||||
}
|
||||
isComposingEnabled = when (keyboardMode) {
|
||||
instance.isComposingEnabled = when (keyboardMode) {
|
||||
KeyboardMode.NUMERIC,
|
||||
KeyboardMode.PHONE,
|
||||
KeyboardMode.PHONE2 -> false
|
||||
else -> keyVariation != KeyVariation.PASSWORD && florisboard.prefs.suggestion.enabled
|
||||
else -> keyVariation != KeyVariation.PASSWORD &&
|
||||
florisboard.prefs.suggestion.enabled// &&
|
||||
//!instance.inputAttributes.flagTextAutoComplete &&
|
||||
//!instance.inputAttributes.flagTextNoSuggestions
|
||||
}
|
||||
if (!florisboard.prefs.correction.rememberCapsLockState) {
|
||||
capsLock = false
|
||||
}
|
||||
updateCapsState()
|
||||
resetComposingText()
|
||||
setActiveKeyboardMode(keyboardMode)
|
||||
smartbarManager.onStartInputView(keyboardMode, isComposingEnabled)
|
||||
smartbarManager.onStartInputView(keyboardMode)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -284,147 +272,28 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* Main logic point for processing cursor updates as well as parsing the current composing word
|
||||
* and passing this info on to the [SmartbarManager] to turn it into candidate suggestions.
|
||||
*/
|
||||
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
|
||||
cursorAnchorInfo ?: return
|
||||
lastCursorAnchorInfo = cursorAnchorInfo
|
||||
|
||||
val ic = florisboard.currentInputConnection
|
||||
|
||||
val isNewSelectionInBoundsOfOld =
|
||||
cursorAnchorInfo.selectionStart >= (selectionStart - 1) &&
|
||||
cursorAnchorInfo.selectionStart <= (selectionStart + 1) &&
|
||||
cursorAnchorInfo.selectionEnd >= (selectionEnd - 1) &&
|
||||
cursorAnchorInfo.selectionEnd <= (selectionEnd + 1)
|
||||
selectionStart = cursorAnchorInfo.selectionStart
|
||||
selectionEnd = cursorAnchorInfo.selectionEnd
|
||||
val inputText =
|
||||
(ic?.getExtractedText(ExtractedTextRequest(), 0)?.text ?: "").toString()
|
||||
selectionEndMax = inputText.length
|
||||
// TODO: separate composing text from delete swipe word detection
|
||||
//if (isComposingEnabled) {
|
||||
if (!isTextSelected) {
|
||||
val newCursorPos = cursorAnchorInfo.selectionStart
|
||||
val prevComposingText = (cursorAnchorInfo.composingText ?: "").toString()
|
||||
setComposingTextBasedOnInput(inputText, newCursorPos)
|
||||
if ((newCursorPos == cursorPos) && (composingText == prevComposingText)) {
|
||||
// Ignore this, as nothing has changed
|
||||
} else {
|
||||
cursorPos = newCursorPos
|
||||
if (composingText != null && composingTextStart != null) {
|
||||
ic?.setComposingRegion(
|
||||
composingTextStart!!,
|
||||
composingTextStart!! + composingText!!.length
|
||||
)
|
||||
} else {
|
||||
resetComposingText()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resetComposingText()
|
||||
}
|
||||
smartbarManager.generateCandidatesFromComposing(composingText)
|
||||
//}
|
||||
if (!isNewSelectionInBoundsOfOld) {
|
||||
override fun onUpdateSelection() {
|
||||
if (!activeEditorInstance.isNewSelectionInBoundsOfOld) {
|
||||
isManualSelectionMode = false
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = false
|
||||
}
|
||||
updateCapsState()
|
||||
smartbarManager.onUpdateCursorAnchorInfo(cursorAnchorInfo)
|
||||
smartbarManager.onUpdateSelection()
|
||||
}
|
||||
|
||||
override fun onPrimaryClipChanged() {
|
||||
smartbarManager.onPrimaryClipChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the [composingText] and [composingTextStart] properties. Does NOT sync with
|
||||
* [SmartbarManager]!
|
||||
*
|
||||
* @param notifyInputConnection If the current input connection should be notified.
|
||||
*/
|
||||
private fun resetComposingText(notifyInputConnection: Boolean = true) {
|
||||
if (notifyInputConnection) {
|
||||
val ic = florisboard.currentInputConnection
|
||||
ic?.finishComposingText()
|
||||
}
|
||||
composingText = null
|
||||
composingTextStart = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to parse the [composingText] from a given [inputCursorPos] within [inputText].
|
||||
* Sets both [composingText] and [composingTextStart] to null if it fails, else to its
|
||||
* parsed values.
|
||||
*
|
||||
* @param inputText The input text to search in.
|
||||
* @param inputCursorPos The position where to search in [inputText].
|
||||
*/
|
||||
private fun setComposingTextBasedOnInput(inputText: String, inputCursorPos: Int) {
|
||||
val words = inputText.split("[^\\p{L}]".toRegex())
|
||||
var pos = 0
|
||||
resetComposingText(false)
|
||||
for (word in words) {
|
||||
if (inputCursorPos >= pos && inputCursorPos <= pos + word.length && word.isNotEmpty()) {
|
||||
composingText = word
|
||||
composingTextStart = pos
|
||||
break
|
||||
} else {
|
||||
pos += word.length + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should primarily pe used by [SmartbarManager.candidateViewOnClickListener] to commit
|
||||
* a candidate if a user has pressed on it.
|
||||
*/
|
||||
fun commitCandidate(candidateText: String) {
|
||||
val ic = florisboard.currentInputConnection
|
||||
ic?.setComposingText(candidateText, 1)
|
||||
ic?.finishComposingText()
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the [CapsMode] out of the given [flags].
|
||||
*
|
||||
* @param flags The input flags.
|
||||
* @return A [CapsMode] value.
|
||||
*/
|
||||
private fun parseCapsModeFromFlags(flags: Int): CapsMode {
|
||||
return when {
|
||||
flags and InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS > 0 -> {
|
||||
CapsMode.ALL
|
||||
}
|
||||
flags and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES > 0 -> {
|
||||
CapsMode.SENTENCES
|
||||
}
|
||||
flags and InputType.TYPE_TEXT_FLAG_CAP_WORDS > 0 -> {
|
||||
CapsMode.WORDS
|
||||
}
|
||||
else -> {
|
||||
CapsMode.NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the current cursor caps mode from the current input connection.
|
||||
*
|
||||
* @return The [CapsMode] according to the returned flags by the current input connection.
|
||||
*/
|
||||
private fun fetchCurrentCursorCapsMode(): CapsMode {
|
||||
val ic = florisboard.currentInputConnection
|
||||
val info = florisboard.currentInputEditorInfo
|
||||
val capsFlags = ic?.getCursorCapsMode(info.inputType) ?: 0
|
||||
return parseCapsModeFromFlags(capsFlags)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current caps state according to the [cursorCapsMode], while respecting
|
||||
* [capsLock] property and the correction.autoCapitalization preference.
|
||||
* Updates the current caps state according to the [EditorInstance.cursorCapsMode], while
|
||||
* respecting [capsLock] property and the correction.autoCapitalization preference.
|
||||
*/
|
||||
private fun updateCapsState() {
|
||||
cursorCapsMode = fetchCurrentCursorCapsMode()
|
||||
editorCapsMode = parseCapsModeFromFlags(florisboard.currentInputEditorInfo.inputType)
|
||||
if (!capsLock) {
|
||||
caps = florisboard.prefs.correction.autoCapitalization && cursorCapsMode != CapsMode.NONE
|
||||
caps = florisboard.prefs.correction.autoCapitalization &&
|
||||
activeEditorInstance.cursorCapsMode != InputAttributes.CapsMode.NONE
|
||||
keyboardViews[activeKeyboardMode]?.invalidateAllKeys()
|
||||
}
|
||||
}
|
||||
@@ -445,90 +314,43 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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!!
|
||||
*/
|
||||
private fun sendSystemKeyEvent(ic: InputConnection?, keyCode: Int) {
|
||||
ic?.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, keyCode))
|
||||
}
|
||||
|
||||
/**
|
||||
* 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!!
|
||||
*/
|
||||
private fun sendSystemKeyEventAlt(ic: InputConnection?, keyCode: Int) {
|
||||
ic?.sendKeyEvent(
|
||||
KeyEvent(
|
||||
0,
|
||||
1,
|
||||
KeyEvent.ACTION_DOWN, keyCode,
|
||||
0,
|
||||
KeyEvent.META_ALT_LEFT_ON
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.DELETE] event.
|
||||
*/
|
||||
private fun handleDelete() {
|
||||
val ic = florisboard.currentInputConnection
|
||||
ic?.beginBatchEdit()
|
||||
resetComposingText()
|
||||
isManualSelectionMode = false
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = false
|
||||
sendSystemKeyEvent(ic, KeyEvent.KEYCODE_DEL)
|
||||
ic?.endBatchEdit()
|
||||
activeEditorInstance.deleteBackwards()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.DELETE_WORD] event.
|
||||
*/
|
||||
private fun handleDeleteWord() {
|
||||
val ic = florisboard.currentInputConnection
|
||||
ic?.beginBatchEdit()
|
||||
isManualSelectionMode = false
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = false
|
||||
ic?.setComposingText("", 1)
|
||||
ic?.finishComposingText()
|
||||
if (ic?.getTextBeforeCursor(1, 0)?.length ?: 0 > 0) {
|
||||
ic?.deleteSurroundingText(1, 0)
|
||||
}
|
||||
composingText = null
|
||||
composingTextStart = null
|
||||
ic?.endBatchEdit()
|
||||
activeEditorInstance.deleteWordsBeforeCursor(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.ENTER] event.
|
||||
*/
|
||||
private fun handleEnter() {
|
||||
val ic = florisboard.currentInputConnection
|
||||
resetComposingText()
|
||||
val action = florisboard.currentInputEditorInfo?.imeOptions ?: 0
|
||||
val actionMasked = action and EditorInfo.IME_MASK_ACTION
|
||||
if (action and EditorInfo.IME_FLAG_NO_ENTER_ACTION > 0) {
|
||||
sendSystemKeyEvent(ic, KeyEvent.KEYCODE_ENTER)
|
||||
if (activeEditorInstance.imeOptions.flagNoEnterAction) {
|
||||
activeEditorInstance.performEnter()
|
||||
} else {
|
||||
when (actionMasked) {
|
||||
EditorInfo.IME_ACTION_DONE,
|
||||
EditorInfo.IME_ACTION_GO,
|
||||
EditorInfo.IME_ACTION_NEXT,
|
||||
EditorInfo.IME_ACTION_PREVIOUS,
|
||||
EditorInfo.IME_ACTION_SEARCH,
|
||||
EditorInfo.IME_ACTION_SEND -> {
|
||||
ic?.performEditorAction(actionMasked)
|
||||
when (activeEditorInstance.imeOptions.action) {
|
||||
ImeOptions.Action.DONE,
|
||||
ImeOptions.Action.GO,
|
||||
ImeOptions.Action.NEXT,
|
||||
ImeOptions.Action.PREVIOUS,
|
||||
ImeOptions.Action.SEARCH,
|
||||
ImeOptions.Action.SEND -> {
|
||||
activeEditorInstance.performEnterAction(activeEditorInstance.imeOptions.action)
|
||||
}
|
||||
else -> sendSystemKeyEvent(ic, KeyEvent.KEYCODE_ENTER)
|
||||
else -> activeEditorInstance.performEnter()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -558,14 +380,13 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* enabled by the user.
|
||||
*/
|
||||
private fun handleSpace() {
|
||||
val ic = florisboard.currentInputConnection
|
||||
if (florisboard.prefs.correction.doubleSpacePeriod) {
|
||||
if (hasSpaceRecentlyPressed) {
|
||||
osHandler.removeCallbacksAndMessages(null)
|
||||
val text = ic?.getTextBeforeCursor(2, 0) ?: ""
|
||||
val text = activeEditorInstance.getTextBeforeCursor(2)
|
||||
if (text.length == 2 && !text.matches("""[.!?‽\s][\s]""".toRegex())) {
|
||||
ic?.deleteSurroundingText(1, 0)
|
||||
ic?.commitText(".", 1)
|
||||
activeEditorInstance.deleteBackwards()
|
||||
activeEditorInstance.commitText(".")
|
||||
}
|
||||
hasSpaceRecentlyPressed = false
|
||||
} else {
|
||||
@@ -575,107 +396,107 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
ic?.commitText(KeyCode.SPACE.toChar().toString(), 1)
|
||||
activeEditorInstance.commitText(KeyCode.SPACE.toChar().toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles [KeyCode] arrow and move events, behaves differently depending on text selection.
|
||||
*/
|
||||
private fun handleArrow(code: Int) {
|
||||
val ic = florisboard.currentInputConnection
|
||||
resetComposingText()
|
||||
if (isTextSelected && isManualSelectionMode) {
|
||||
private fun handleArrow(code: Int) = activeEditorInstance.apply {
|
||||
val selectionStartMin = 0
|
||||
val selectionEndMax = cachedText.length
|
||||
if (selection.isSelectionMode && isManualSelectionMode) {
|
||||
// Text is selected and it is manual selection -> Expand selection depending on started
|
||||
// direction.
|
||||
when (code) {
|
||||
KeyCode.ARROW_DOWN -> {}
|
||||
KeyCode.ARROW_LEFT -> {
|
||||
if (isManualSelectionModeLeft) {
|
||||
ic?.setSelection(
|
||||
(selectionStart - 1).coerceAtLeast(selectionStartMin),
|
||||
selectionEnd
|
||||
setSelection(
|
||||
(selection.start - 1).coerceAtLeast(selectionStartMin),
|
||||
selection.end
|
||||
)
|
||||
} else {
|
||||
ic?.setSelection(selectionStart, selectionEnd - 1)
|
||||
setSelection(selection.start, selection.end - 1)
|
||||
}
|
||||
}
|
||||
KeyCode.ARROW_RIGHT -> {
|
||||
if (isManualSelectionModeRight) {
|
||||
ic?.setSelection(
|
||||
selectionStart,
|
||||
(selectionEnd + 1).coerceAtMost(selectionEndMax)
|
||||
setSelection(
|
||||
selection.start,
|
||||
(selection.end + 1).coerceAtMost(selectionEndMax)
|
||||
)
|
||||
} else {
|
||||
ic?.setSelection(selectionStart + 1, selectionEnd)
|
||||
setSelection(selection.start + 1, selection.end)
|
||||
}
|
||||
}
|
||||
KeyCode.ARROW_UP -> {}
|
||||
KeyCode.MOVE_HOME -> {
|
||||
if (isManualSelectionModeLeft) {
|
||||
ic?.setSelection(selectionStartMin, selectionEnd)
|
||||
setSelection(selectionStartMin, selection.end)
|
||||
} else {
|
||||
ic?.setSelection(selectionStartMin, selectionStart)
|
||||
setSelection(selectionStartMin, selection.start)
|
||||
}
|
||||
}
|
||||
KeyCode.MOVE_END -> {
|
||||
if (isManualSelectionModeRight) {
|
||||
ic?.setSelection(selectionStart, selectionEndMax)
|
||||
setSelection(selection.start, selectionEndMax)
|
||||
} else {
|
||||
ic?.setSelection(selectionEnd, selectionEndMax)
|
||||
setSelection(selection.end, selectionEndMax)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isTextSelected && !isManualSelectionMode) {
|
||||
} else if (selection.isSelectionMode && !isManualSelectionMode) {
|
||||
// Text is selected but no manual selection mode -> arrows behave as if selection was
|
||||
// started in manual left mode
|
||||
when (code) {
|
||||
KeyCode.ARROW_DOWN -> {}
|
||||
KeyCode.ARROW_LEFT -> {
|
||||
ic?.setSelection(selectionStart, selectionEnd - 1)
|
||||
setSelection(selection.start, selection.end - 1)
|
||||
}
|
||||
KeyCode.ARROW_RIGHT -> {
|
||||
ic?.setSelection(
|
||||
selectionStart,
|
||||
(selectionEnd + 1).coerceAtMost(selectionEndMax)
|
||||
setSelection(
|
||||
selection.start,
|
||||
(selection.end + 1).coerceAtMost(selectionEndMax)
|
||||
)
|
||||
}
|
||||
KeyCode.ARROW_UP -> {}
|
||||
KeyCode.MOVE_HOME -> {
|
||||
ic?.setSelection(selectionStartMin, selectionStart)
|
||||
setSelection(selectionStartMin, selection.start)
|
||||
}
|
||||
KeyCode.MOVE_END -> {
|
||||
ic?.setSelection(selectionStart, selectionEndMax)
|
||||
setSelection(selection.start, selectionEndMax)
|
||||
}
|
||||
}
|
||||
} else if (!isTextSelected && isManualSelectionMode) {
|
||||
} else if (!selection.isSelectionMode && isManualSelectionMode) {
|
||||
// No text is selected but manual selection mode is active, user wants to start a new
|
||||
// selection. Must set manual selection direction.
|
||||
when (code) {
|
||||
KeyCode.ARROW_DOWN -> {}
|
||||
KeyCode.ARROW_LEFT -> {
|
||||
ic?.setSelection(
|
||||
(selectionStart - 1).coerceAtLeast(selectionStartMin),
|
||||
selectionStart
|
||||
setSelection(
|
||||
(selection.start - 1).coerceAtLeast(selectionStartMin),
|
||||
selection.start
|
||||
)
|
||||
isManualSelectionModeLeft = true
|
||||
isManualSelectionModeRight = false
|
||||
}
|
||||
KeyCode.ARROW_RIGHT -> {
|
||||
ic?.setSelection(
|
||||
selectionEnd,
|
||||
(selectionEnd + 1).coerceAtMost(selectionEndMax)
|
||||
setSelection(
|
||||
selection.end,
|
||||
(selection.end + 1).coerceAtMost(selectionEndMax)
|
||||
)
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = true
|
||||
}
|
||||
KeyCode.ARROW_UP -> {}
|
||||
KeyCode.MOVE_HOME -> {
|
||||
ic?.setSelection(selectionStartMin, selectionStart)
|
||||
setSelection(selectionStartMin, selection.start)
|
||||
isManualSelectionModeLeft = true
|
||||
isManualSelectionModeRight = false
|
||||
}
|
||||
KeyCode.MOVE_END -> {
|
||||
ic?.setSelection(selectionEnd, selectionEndMax)
|
||||
setSelection(selection.end, selectionEndMax)
|
||||
isManualSelectionModeLeft = false
|
||||
isManualSelectionModeRight = true
|
||||
}
|
||||
@@ -683,87 +504,39 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
} else {
|
||||
// No selection and no manual selection mode -> move cursor around
|
||||
when (code) {
|
||||
KeyCode.ARROW_DOWN -> sendSystemKeyEvent(ic, KeyEvent.KEYCODE_DPAD_DOWN)
|
||||
KeyCode.ARROW_LEFT -> sendSystemKeyEvent(ic, KeyEvent.KEYCODE_DPAD_LEFT)
|
||||
KeyCode.ARROW_RIGHT -> sendSystemKeyEvent(ic, KeyEvent.KEYCODE_DPAD_RIGHT)
|
||||
KeyCode.ARROW_UP -> sendSystemKeyEvent(ic, KeyEvent.KEYCODE_DPAD_UP)
|
||||
KeyCode.MOVE_HOME -> sendSystemKeyEventAlt(ic, KeyEvent.KEYCODE_DPAD_UP)
|
||||
KeyCode.MOVE_END -> sendSystemKeyEventAlt(ic, KeyEvent.KEYCODE_DPAD_DOWN)
|
||||
KeyCode.ARROW_DOWN -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN)
|
||||
KeyCode.ARROW_LEFT -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT)
|
||||
KeyCode.ARROW_RIGHT -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT)
|
||||
KeyCode.ARROW_UP -> activeEditorInstance.sendSystemKeyEvent(KeyEvent.KEYCODE_DPAD_UP)
|
||||
KeyCode.MOVE_HOME -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_UP)
|
||||
KeyCode.MOVE_END -> activeEditorInstance.sendSystemKeyEventAlt(KeyEvent.KEYCODE_DPAD_DOWN)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.CLIPBOARD_CUT] event.
|
||||
* TODO: handle other data than text too, e.g. Uri, Intent, ...
|
||||
*/
|
||||
private fun handleClipboardCut() {
|
||||
val ic = florisboard.currentInputConnection
|
||||
val selectedText = ic?.getSelectedText(0)
|
||||
if (selectedText != null) {
|
||||
florisboard.clipboardManager
|
||||
?.setPrimaryClip(ClipData.newPlainText(selectedText, selectedText))
|
||||
}
|
||||
resetComposingText()
|
||||
ic?.commitText("", 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.CLIPBOARD_COPY] event.
|
||||
* TODO: handle other data than text too, e.g. Uri, Intent, ...
|
||||
*/
|
||||
private fun handleClipboardCopy() {
|
||||
val ic = florisboard.currentInputConnection
|
||||
val selectedText = ic?.getSelectedText(0)
|
||||
if (selectedText != null) {
|
||||
florisboard.clipboardManager
|
||||
?.setPrimaryClip(ClipData.newPlainText(selectedText, selectedText))
|
||||
}
|
||||
resetComposingText()
|
||||
ic?.setSelection(selectionEnd, selectionEnd)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.CLIPBOARD_PASTE] event.
|
||||
* TODO: handle other data than text too, e.g. Uri, Intent, ...
|
||||
*/
|
||||
private fun handleClipboardPaste() {
|
||||
val ic = florisboard.currentInputConnection
|
||||
val item = florisboard.clipboardManager?.primaryClip?.getItemAt(0)
|
||||
val pasteText = item?.text
|
||||
if (pasteText != null) {
|
||||
resetComposingText()
|
||||
ic?.commitText(pasteText, 1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.CLIPBOARD_SELECT] event.
|
||||
*/
|
||||
private fun handleClipboardSelect() {
|
||||
val ic = florisboard.currentInputConnection
|
||||
resetComposingText()
|
||||
if (isTextSelected) {
|
||||
private fun handleClipboardSelect() = activeEditorInstance.apply {
|
||||
if (selection.isSelectionMode) {
|
||||
if (isManualSelectionMode && isManualSelectionModeLeft) {
|
||||
ic?.setSelection(selectionStart, selectionStart)
|
||||
setSelection(selection.start, selection.start)
|
||||
} else {
|
||||
ic?.setSelection(selectionEnd, selectionEnd)
|
||||
setSelection(selection.end, selection.end)
|
||||
}
|
||||
isManualSelectionMode = false
|
||||
} else {
|
||||
isManualSelectionMode = !isManualSelectionMode
|
||||
// Must recall to update UI properly
|
||||
florisboard.onUpdateCursorAnchorInfo(lastCursorAnchorInfo)
|
||||
// Must call to update UI properly
|
||||
editingKeyboardView?.onUpdateSelection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a [KeyCode.CLIPBOARD_SELECT_ALL] event.
|
||||
*/
|
||||
private fun handleClipboardSelectAll() {
|
||||
val ic = florisboard.currentInputConnection
|
||||
resetComposingText()
|
||||
ic?.setSelection(selectionStartMin, selectionEndMax)
|
||||
activeEditorInstance.setSelection(0, activeEditorInstance.cachedText.length)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -774,8 +547,6 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
* @param keyData The [KeyData] object which should be sent.
|
||||
*/
|
||||
fun sendKeyPress(keyData: KeyData) {
|
||||
val ic = florisboard.currentInputConnection
|
||||
|
||||
when (keyData.code) {
|
||||
KeyCode.ARROW_DOWN,
|
||||
KeyCode.ARROW_LEFT,
|
||||
@@ -783,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()
|
||||
@@ -809,8 +589,6 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
KeyCode.VIEW_SYMBOLS -> setActiveKeyboardMode(KeyboardMode.SYMBOLS)
|
||||
KeyCode.VIEW_SYMBOLS2 -> setActiveKeyboardMode(KeyboardMode.SYMBOLS2)
|
||||
else -> {
|
||||
ic?.beginBatchEdit()
|
||||
resetComposingText()
|
||||
when (activeKeyboardMode) {
|
||||
KeyboardMode.NUMERIC,
|
||||
KeyboardMode.NUMERIC_ADVANCED,
|
||||
@@ -819,13 +597,13 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
KeyType.CHARACTER,
|
||||
KeyType.NUMERIC -> {
|
||||
val text = keyData.code.toChar().toString()
|
||||
ic?.commitText(text, 1)
|
||||
activeEditorInstance.commitText(text)
|
||||
}
|
||||
else -> when (keyData.code) {
|
||||
KeyCode.PHONE_PAUSE,
|
||||
KeyCode.PHONE_WAIT -> {
|
||||
val text = keyData.code.toChar().toString()
|
||||
ic?.commitText(text, 1)
|
||||
activeEditorInstance.commitText(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -837,7 +615,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
true -> keyData.label.toUpperCase(Locale.getDefault())
|
||||
false -> keyData.label.toLowerCase(Locale.getDefault())
|
||||
}
|
||||
ic?.commitText(tld, 1)
|
||||
activeEditorInstance.commitText(tld)
|
||||
}
|
||||
else -> {
|
||||
var text = keyData.code.toChar().toString()
|
||||
@@ -845,26 +623,19 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(),
|
||||
true -> text.toUpperCase(Locale.getDefault())
|
||||
false -> text.toLowerCase(Locale.getDefault())
|
||||
}
|
||||
ic?.commitText(text, 1)
|
||||
activeEditorInstance.commitText(text)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Log.e(
|
||||
this::class.simpleName,
|
||||
"sendKeyPress(keyData): Received unknown key: $keyData"
|
||||
)
|
||||
Log.e(TAG,"sendKeyPress(keyData): Received unknown key: $keyData")
|
||||
}
|
||||
}
|
||||
}
|
||||
ic?.endBatchEdit()
|
||||
smartbarManager.resetClipboardSuggestion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class CapsMode {
|
||||
ALL,
|
||||
NONE,
|
||||
SENTENCES,
|
||||
WORDS;
|
||||
if (keyData.code != KeyCode.SHIFT && !capsLock) {
|
||||
updateCapsState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.inputmethod.CursorAnchorInfo
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.ime.core.FlorisBoard
|
||||
@@ -60,8 +59,8 @@ class EditingKeyboardView : ConstraintLayout, FlorisBoard.EventListener {
|
||||
pasteKey = findViewById(R.id.clipboard_paste)
|
||||
}
|
||||
|
||||
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
|
||||
val isSelectionActive = florisboard?.textInputManager?.isTextSelected ?: false
|
||||
override fun onUpdateSelection() {
|
||||
val isSelectionActive = florisboard?.activeEditorInstance?.selection?.isSelectionMode ?: false
|
||||
val isSelectionMode = florisboard?.textInputManager?.isManualSelectionMode ?: false
|
||||
arrowUpKey?.isEnabled = !(isSelectionActive || isSelectionMode)
|
||||
arrowDownKey?.isEnabled = !(isSelectionActive || isSelectionMode)
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package dev.patrickgold.florisboard.ime.text.key
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.*
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Handler
|
||||
@@ -25,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
|
||||
@@ -33,12 +31,14 @@ 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
|
||||
import dev.patrickgold.florisboard.ime.text.keyboard.KeyboardView
|
||||
import dev.patrickgold.florisboard.util.setBackgroundTintColor2
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* View class for managing the rendering and the events of a single keyboard key.
|
||||
@@ -64,6 +64,8 @@ class KeyView(
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
private var shouldBlockNextKeyCode: Boolean = false
|
||||
|
||||
private var desiredWidth: Int = 0
|
||||
private var desiredHeight: Int = 0
|
||||
private var drawable: Drawable? = null
|
||||
private var drawableColor: Int = 0
|
||||
private var drawablePadding: Int = 0
|
||||
@@ -87,6 +89,7 @@ class KeyView(
|
||||
textSize = resources.getDimension(R.dimen.key_textHintSize)
|
||||
typeface = Typeface.DEFAULT
|
||||
}
|
||||
private val tempRect: Rect = Rect()
|
||||
|
||||
var florisboard: FlorisBoard? = null
|
||||
private val swipeGestureDetector = SwipeGesture.Detector(context, this)
|
||||
@@ -201,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)
|
||||
@@ -326,7 +329,7 @@ class KeyView(
|
||||
* by Devunwired
|
||||
*/
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val desiredWidth = when (keyboardView.computedLayout?.mode) {
|
||||
desiredWidth = when (keyboardView.computedLayout?.mode) {
|
||||
KeyboardMode.NUMERIC,
|
||||
KeyboardMode.PHONE,
|
||||
KeyboardMode.PHONE2 -> (keyboardView.desiredKeyWidth * 2.68f).toInt()
|
||||
@@ -345,7 +348,7 @@ class KeyView(
|
||||
else -> keyboardView.desiredKeyWidth
|
||||
}
|
||||
}
|
||||
val desiredHeight = keyboardView.desiredKeyHeight
|
||||
desiredHeight = keyboardView.desiredKeyHeight
|
||||
|
||||
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
|
||||
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
|
||||
@@ -400,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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -464,6 +502,7 @@ class KeyView(
|
||||
* TextInputManager.
|
||||
*/
|
||||
fun updateVisibility() {
|
||||
updateEnabledState()
|
||||
when (data.code) {
|
||||
KeyCode.SWITCH_TO_TEXT_CONTEXT,
|
||||
KeyCode.SWITCH_TO_MEDIA_CONTEXT -> {
|
||||
@@ -497,6 +536,45 @@ class KeyView(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically sets the text size of [boxPaint] for given [text] so it fits within the given
|
||||
* bounds.
|
||||
*
|
||||
* Implementation based on this SO answer by Michael Scheper, but has been modified to
|
||||
* incorporate the height as well: https://stackoverflow.com/a/21895626/6801193
|
||||
*
|
||||
* @param boxPaint The [Paint] object which the text size should be applied to.
|
||||
* @param boxWidth The max width for the surrounding box of [text].
|
||||
* @param boxHeight The max height for the surrounding box of [text].
|
||||
* @param text The text for which the size should be calculated.
|
||||
*/
|
||||
private fun setTextSizeFor(boxPaint: Paint, boxWidth: Float, boxHeight: Float, text: String) {
|
||||
var textSize = 64.0f
|
||||
// Must loop twice as there can be bot with and height which are too big, which requires
|
||||
// 2 iterations to adjust
|
||||
for (n in 0..1) {
|
||||
boxPaint.textSize = textSize
|
||||
boxPaint.getTextBounds(text, 0, text.length, tempRect)
|
||||
val diffWidth = tempRect.width() - boxWidth
|
||||
val diffHeight = tempRect.height() - boxHeight
|
||||
val factor = if (diffWidth < 0 && diffHeight < 0) {
|
||||
// Text box is smaller as given box, text size must be increased
|
||||
if (abs(diffWidth) < abs(diffHeight)) {
|
||||
boxWidth / tempRect.width()
|
||||
} else {
|
||||
boxHeight / tempRect.height()
|
||||
}
|
||||
} else if (diffWidth > diffHeight) {
|
||||
// Text box is larger on minimum one side than given box, text size must be decreased
|
||||
boxWidth / tempRect.width()
|
||||
} else {
|
||||
boxHeight / tempRect.height()
|
||||
}
|
||||
textSize *= factor
|
||||
}
|
||||
boxPaint.textSize = textSize
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the key label / drawable.
|
||||
*/
|
||||
@@ -522,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)
|
||||
}
|
||||
}
|
||||
@@ -613,12 +715,12 @@ class KeyView(
|
||||
}
|
||||
}
|
||||
|
||||
val isPortrait =
|
||||
resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
|
||||
|
||||
// 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) {
|
||||
@@ -641,14 +743,36 @@ class KeyView(
|
||||
// Draw label
|
||||
val label = label
|
||||
if (label != null) {
|
||||
if (data.code == KeyCode.VIEW_NUMERIC || data.code == KeyCode.VIEW_NUMERIC_ADVANCED
|
||||
|| data.code == KeyCode.SPACE) {
|
||||
labelPaint.textSize = resources.getDimension(R.dimen.key_numeric_textSize)
|
||||
} else {
|
||||
labelPaint.textSize = resources.getDimension(R.dimen.key_textSize)
|
||||
}
|
||||
if (prefs.keyboard.oneHandedMode != "off" && isPortrait) {
|
||||
labelPaint.textSize *= 0.9f
|
||||
when (data.code) {
|
||||
KeyCode.VIEW_NUMERIC, KeyCode.VIEW_NUMERIC_ADVANCED -> {
|
||||
labelPaint.textSize = resources.getDimension(R.dimen.key_numeric_textSize)
|
||||
}
|
||||
else -> when {
|
||||
data.type == KeyType.CHARACTER && data.code != KeyCode.SPACE -> {
|
||||
setTextSizeFor(
|
||||
labelPaint,
|
||||
desiredWidth - (2.6f * drawablePadding),
|
||||
desiredHeight - (3.6f * drawablePadding),
|
||||
// Note: taking a "X" here because it is one of the biggest letters and
|
||||
// the keys must have the same base character for calculation, else
|
||||
// they will all look different and weird...
|
||||
"X"
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
setTextSizeFor(
|
||||
labelPaint,
|
||||
measuredWidth - (2.6f * drawablePadding),
|
||||
measuredHeight - (3.6f * drawablePadding),
|
||||
when (data.code) {
|
||||
KeyCode.VIEW_CHARACTERS, KeyCode.VIEW_SYMBOLS, KeyCode.VIEW_SYMBOLS2 -> {
|
||||
resources.getString(R.string.key__view_symbols)
|
||||
}
|
||||
else -> label
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
labelPaint.color = prefs.theme.keyFgColor
|
||||
labelPaint.alpha = if (keyboardView.computedLayout?.mode == KeyboardMode.CHARACTERS &&
|
||||
@@ -668,10 +792,15 @@ class KeyView(
|
||||
// Draw hinted label
|
||||
val hintedLabel = hintedLabel
|
||||
if (hintedLabel != null) {
|
||||
hintedLabelPaint.textSize = resources.getDimension(R.dimen.key_textHintSize)
|
||||
if (prefs.keyboard.oneHandedMode != "off" && isPortrait) {
|
||||
hintedLabelPaint.textSize *= 0.9f
|
||||
}
|
||||
setTextSizeFor(
|
||||
hintedLabelPaint,
|
||||
desiredWidth * 1.0f / 6.0f,
|
||||
desiredHeight * 1.0f / 6.0f,
|
||||
// Note: taking a "X" here because it is one of the biggest letters and
|
||||
// the keys must have the same base character for calculation, else
|
||||
// they will all look different and weird...
|
||||
"X"
|
||||
)
|
||||
hintedLabelPaint.color = prefs.theme.keyFgColor
|
||||
hintedLabelPaint.alpha = 120
|
||||
val centerX = measuredWidth * 5.0f / 6.0f
|
||||
|
||||
@@ -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,64 +2,55 @@ 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
|
||||
val text = view.text.toString()
|
||||
if (text.isNotEmpty()) {
|
||||
textInputManager.commitCandidate(text)
|
||||
florisboard.activeEditorInstance.commitCompletion(text)
|
||||
}
|
||||
}
|
||||
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,33 +156,21 @@ class SmartbarManager private constructor() : FlorisBoard.EventListener {
|
||||
isQuickActionsVisible = false
|
||||
}
|
||||
}
|
||||
updateSmartbarUI()
|
||||
}
|
||||
|
||||
fun onFinishInputView() {
|
||||
//spellCheckerSession?.close()
|
||||
}
|
||||
|
||||
override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo?) {
|
||||
val isSelectionActive = florisboard.textInputManager.isTextSelected
|
||||
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()
|
||||
override fun onUpdateSelection() {
|
||||
updateSmartbarUI()
|
||||
}
|
||||
|
||||
fun deleteCandidateFromDictionary(candidate: String) {
|
||||
//
|
||||
}
|
||||
|
||||
fun resetCandidates() {
|
||||
//
|
||||
}
|
||||
|
||||
fun generateCandidatesFromComposing(composingText: String?) {
|
||||
fun generateCandidatesFromComposing(composingText: String) {
|
||||
val smartbarView = smartbarView ?: return
|
||||
|
||||
if (composingText == null) {
|
||||
if (composingText == "") {
|
||||
smartbarView.candidateViewList[0].text = "candidate"
|
||||
smartbarView.candidateViewList[1].text = "suggestions"
|
||||
smartbarView.candidateViewList[2].text = "nyi"
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* 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.util
|
||||
|
||||
import android.text.InputType
|
||||
import android.text.TextUtils
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
fun EditorInfo.debugSummarize(): String {
|
||||
var summary = this::class.qualifiedName + "\r\n"
|
||||
summary += "imeOptions: " + this.imeOptions.debugSummarize(EditorInfo::class) + "\r\n"
|
||||
summary += "initialCapsMode: " + this.initialCapsMode.debugSummarize(TextUtils::class) + "\r\n"
|
||||
summary += "initialSelStart: " + this.initialSelStart + "\r\n"
|
||||
summary += "initialSelEnd: " + this.initialSelEnd + "\r\n"
|
||||
summary += "inputType: " + this.inputType.debugSummarize(InputType::class) + "\r\n"
|
||||
summary += "packageName: " + this.packageName
|
||||
return summary
|
||||
}
|
||||
|
||||
fun <T: Any> Int.debugSummarize(type: KClass<T>): String {
|
||||
var summary = ""
|
||||
when (type) {
|
||||
EditorInfo::class -> {
|
||||
when (this) {
|
||||
EditorInfo.IME_NULL -> {
|
||||
summary += "IME_NULL"
|
||||
}
|
||||
else -> {
|
||||
val tAction = when (this and EditorInfo.IME_MASK_ACTION) {
|
||||
EditorInfo.IME_ACTION_DONE -> "IME_ACTION_DONE"
|
||||
EditorInfo.IME_ACTION_GO -> "IME_ACTION_GO"
|
||||
EditorInfo.IME_ACTION_NEXT -> "IME_ACTION_NEXT"
|
||||
EditorInfo.IME_ACTION_NONE -> "IME_ACTION_NONE"
|
||||
EditorInfo.IME_ACTION_PREVIOUS -> "IME_ACTION_PREVIOUS"
|
||||
EditorInfo.IME_ACTION_SEARCH -> "IME_ACTION_SEARCH"
|
||||
EditorInfo.IME_ACTION_SEND -> "IME_ACTION_SEND"
|
||||
EditorInfo.IME_ACTION_UNSPECIFIED -> "IME_ACTION_UNSPECIFIED"
|
||||
else -> String.format("0x%08x", this and EditorInfo.IME_MASK_ACTION)
|
||||
}
|
||||
var tFlags = ""
|
||||
if (this and EditorInfo.IME_FLAG_FORCE_ASCII > 0) {
|
||||
tFlags += "IME_FLAG_FORCE_ASCII|"
|
||||
}
|
||||
if (this and EditorInfo.IME_FLAG_NAVIGATE_NEXT > 0) {
|
||||
tFlags += "IME_FLAG_NAVIGATE_NEXT|"
|
||||
}
|
||||
if (this and EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS > 0) {
|
||||
tFlags += "IME_FLAG_NAVIGATE_PREVIOUS|"
|
||||
}
|
||||
if (this and EditorInfo.IME_FLAG_NO_ACCESSORY_ACTION > 0) {
|
||||
tFlags += "IME_FLAG_NO_ACCESSORY_ACTION|"
|
||||
}
|
||||
if (this and EditorInfo.IME_FLAG_NO_ENTER_ACTION > 0) {
|
||||
tFlags += "IME_FLAG_NO_ENTER_ACTION|"
|
||||
}
|
||||
if (this and EditorInfo.IME_FLAG_NO_EXTRACT_UI > 0) {
|
||||
tFlags += "IME_FLAG_NO_EXTRACT_UI|"
|
||||
}
|
||||
if (this and EditorInfo.IME_FLAG_NO_FULLSCREEN > 0) {
|
||||
tFlags += "IME_FLAG_NO_FULLSCREEN|"
|
||||
}
|
||||
if (this and EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING > 0) {
|
||||
tFlags += "IME_FLAG_NO_PERSONALIZED_LEARNING|"
|
||||
}
|
||||
if (tFlags.isEmpty()) {
|
||||
tFlags = "(none)"
|
||||
}
|
||||
if (tFlags.endsWith("|")) {
|
||||
tFlags = tFlags.substring(0, tFlags.length - 1)
|
||||
}
|
||||
summary += "action=$tAction flags=$tFlags"
|
||||
}
|
||||
}
|
||||
}
|
||||
InputType::class -> {
|
||||
when (this) {
|
||||
InputType.TYPE_NULL -> {
|
||||
summary += "TYPE_NULL"
|
||||
}
|
||||
else -> {
|
||||
val tClass: String
|
||||
val tVariation: String
|
||||
var tFlags = ""
|
||||
when (this and InputType.TYPE_MASK_CLASS) {
|
||||
InputType.TYPE_CLASS_DATETIME -> {
|
||||
tClass = "TYPE_CLASS_DATETIME"
|
||||
tVariation = when (this and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_DATETIME_VARIATION_DATE -> "TYPE_DATETIME_VARIATION_DATE"
|
||||
InputType.TYPE_DATETIME_VARIATION_NORMAL -> "TYPE_DATETIME_VARIATION_NORMAL"
|
||||
InputType.TYPE_DATETIME_VARIATION_TIME -> "TYPE_DATETIME_VARIATION_TIME"
|
||||
else -> String.format("0x%08x", this and InputType.TYPE_MASK_VARIATION)
|
||||
}
|
||||
}
|
||||
InputType.TYPE_CLASS_NUMBER -> {
|
||||
tClass = "TYPE_CLASS_NUMBER"
|
||||
tVariation = when (this and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_NUMBER_VARIATION_NORMAL -> "TYPE_NUMBER_VARIATION_NORMAL"
|
||||
InputType.TYPE_NUMBER_VARIATION_PASSWORD -> "TYPE_NUMBER_VARIATION_PASSWORD"
|
||||
else -> String.format("0x%08x", this and InputType.TYPE_MASK_VARIATION)
|
||||
}
|
||||
if (this and InputType.TYPE_NUMBER_FLAG_DECIMAL > 0) {
|
||||
tFlags += "TYPE_NUMBER_FLAG_DECIMAL|"
|
||||
}
|
||||
if (this and InputType.TYPE_NUMBER_FLAG_SIGNED > 0) {
|
||||
tFlags += "TYPE_NUMBER_FLAG_SIGNED|"
|
||||
}
|
||||
}
|
||||
InputType.TYPE_CLASS_PHONE -> {
|
||||
tClass = "TYPE_CLASS_PHONE"
|
||||
tVariation = String.format("0x%08x", this and InputType.TYPE_MASK_VARIATION)
|
||||
}
|
||||
InputType.TYPE_CLASS_TEXT -> {
|
||||
tClass = "TYPE_CLASS_TEXT"
|
||||
tVariation = when (this and InputType.TYPE_MASK_VARIATION) {
|
||||
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS -> "TYPE_TEXT_VARIATION_EMAIL_ADDRESS"
|
||||
InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT -> "TYPE_TEXT_VARIATION_EMAIL_SUBJECT"
|
||||
InputType.TYPE_TEXT_VARIATION_FILTER -> "TYPE_TEXT_VARIATION_FILTER"
|
||||
InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE -> "TYPE_TEXT_VARIATION_LONG_MESSAGE"
|
||||
InputType.TYPE_TEXT_VARIATION_NORMAL -> "TYPE_TEXT_VARIATION_NORMAL"
|
||||
InputType.TYPE_TEXT_VARIATION_PASSWORD -> "TYPE_TEXT_VARIATION_PASSWORD"
|
||||
InputType.TYPE_TEXT_VARIATION_PERSON_NAME -> "TYPE_TEXT_VARIATION_PERSON_NAME"
|
||||
InputType.TYPE_TEXT_VARIATION_PHONETIC -> "TYPE_TEXT_VARIATION_PHONETIC"
|
||||
InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS -> "TYPE_TEXT_VARIATION_POSTAL_ADDRESS"
|
||||
InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE -> "TYPE_TEXT_VARIATION_SHORT_MESSAGE"
|
||||
InputType.TYPE_TEXT_VARIATION_URI -> "TYPE_TEXT_VARIATION_URI"
|
||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD -> "TYPE_TEXT_VARIATION_VISIBLE_PASSWORD"
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT -> "TYPE_TEXT_VARIATION_WEB_EDIT_TEXT"
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS -> "TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS"
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD -> "TYPE_TEXT_VARIATION_WEB_PASSWORD"
|
||||
else -> String.format("0x%08x", this and InputType.TYPE_MASK_VARIATION)
|
||||
}
|
||||
if (this and InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE > 0) {
|
||||
tFlags += "TYPE_TEXT_FLAG_AUTO_COMPLETE|"
|
||||
}
|
||||
if (this and InputType.TYPE_TEXT_FLAG_AUTO_CORRECT > 0) {
|
||||
tFlags += "TYPE_TEXT_FLAG_AUTO_CORRECT|"
|
||||
}
|
||||
if (this and InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS > 0) {
|
||||
tFlags += "TYPE_TEXT_FLAG_CAP_CHARACTERS|"
|
||||
}
|
||||
if (this and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES > 0) {
|
||||
tFlags += "TYPE_TEXT_FLAG_CAP_SENTENCES|"
|
||||
}
|
||||
if (this and InputType.TYPE_TEXT_FLAG_CAP_WORDS > 0) {
|
||||
tFlags += "TYPE_TEXT_FLAG_CAP_WORDS|"
|
||||
}
|
||||
if (this and InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE > 0) {
|
||||
tFlags += "TYPE_TEXT_FLAG_IME_MULTI_LINE|"
|
||||
}
|
||||
if (this and InputType.TYPE_TEXT_FLAG_MULTI_LINE > 0) {
|
||||
tFlags += "TYPE_TEXT_FLAG_MULTI_LINE|"
|
||||
}
|
||||
if (this and InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS > 0) {
|
||||
tFlags += "TYPE_TEXT_FLAG_NO_SUGGESTIONS|"
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
tClass = String.format("0x%08x", this and InputType.TYPE_MASK_CLASS)
|
||||
tVariation = String.format("0x%08x", this and InputType.TYPE_MASK_VARIATION)
|
||||
}
|
||||
}
|
||||
if (tFlags.isEmpty()) {
|
||||
tFlags = "(none)"
|
||||
}
|
||||
if (tFlags.endsWith("|")) {
|
||||
tFlags = tFlags.substring(0, tFlags.length - 1)
|
||||
}
|
||||
summary += "class=$tClass variation=$tVariation flags=$tFlags"
|
||||
}
|
||||
}
|
||||
}
|
||||
TextUtils::class -> {
|
||||
var tFlags = ""
|
||||
if (this and TextUtils.CAP_MODE_CHARACTERS > 0) {
|
||||
tFlags += "CAP_MODE_CHARACTERS|"
|
||||
}
|
||||
if (this and TextUtils.CAP_MODE_SENTENCES > 0) {
|
||||
tFlags += "CAP_MODE_SENTENCES|"
|
||||
}
|
||||
if (this and TextUtils.CAP_MODE_WORDS > 0) {
|
||||
tFlags += "CAP_MODE_WORDS|"
|
||||
}
|
||||
if (this and TextUtils.SAFE_STRING_FLAG_FIRST_LINE > 0) {
|
||||
tFlags += "SAFE_STRING_FLAG_FIRST_LINE|"
|
||||
}
|
||||
if (this and TextUtils.SAFE_STRING_FLAG_SINGLE_LINE > 0) {
|
||||
tFlags += "SAFE_STRING_FLAG_SINGLE_LINE|"
|
||||
}
|
||||
if (this and TextUtils.SAFE_STRING_FLAG_TRIM > 0) {
|
||||
tFlags += "SAFE_STRING_FLAG_TRIM|"
|
||||
}
|
||||
if (tFlags.isEmpty()) {
|
||||
tFlags = "(none)"
|
||||
}
|
||||
if (tFlags.endsWith("|")) {
|
||||
tFlags = tFlags.substring(0, tFlags.length - 1)
|
||||
}
|
||||
summary += "flags=$tFlags"
|
||||
}
|
||||
}
|
||||
return summary
|
||||
}
|
||||
@@ -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,11 +136,15 @@
|
||||
<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>
|
||||
<string name="pref__correction__auto_capitalization__label">Auto-capitalization</string>
|
||||
<string name="pref__correction__auto_capitalization__summary">Capitalize words based on the current input context</string>
|
||||
<string name="pref__correction__remember_caps_lock_state__label">Remember caps lock state</string>
|
||||
<string name="pref__correction__remember_caps_lock_state__summary">Caps lock will stay on when moving to another text field</string>
|
||||
<string name="pref__correction__double_space_period__label">Double-space period</string>
|
||||
<string name="pref__correction__double_space_period__summary">Tapping twice on spacebar inserts a period followed by a space</string>
|
||||
|
||||
@@ -222,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
|
||||
@@ -44,6 +52,13 @@
|
||||
app:title="@string/pref__correction__auto_capitalization__label"
|
||||
app:summary="@string/pref__correction__auto_capitalization__summary"/>
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
app:key="correction__remember_caps_lock_state"
|
||||
app:iconSpaceReserved="false"
|
||||
app:title="@string/pref__correction__remember_caps_lock_state__label"
|
||||
app:summary="@string/pref__correction__remember_caps_lock_state__summary"/>
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
app:key="correction__double_space_period"
|
||||
|
||||
@@ -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/15.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/15.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
- Rework core to better implement interface between FlorisBoard and other apps
|
||||
- Shift state should now update after a key press (#35)
|
||||
- Send key should now send the desired action or a newline character (#33)
|
||||
- Adjusting keyboard height also affects font size of keys (#32)
|
||||
- Add option to remember / forget caps lock state throughout different input fields (#30)
|
||||
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/15.txt
Normal file
5
fastlane/metadata/android/it/changelogs/15.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
- Riorganizzazione del core per implementare al meglio l'interfaccia tra FlorisBoard e le altre applicazioni
|
||||
- Lo stato del turno dovrebbe ora aggiornarsi dopo la pressione di un tasto (#35)
|
||||
- Il tasto Invio dovrebbe ora inviare l'azione desiderata o un carattere di nuova linea (#33)
|
||||
- La regolazione dell'altezza della tastiera influisce anche sulla dimensione dei caratteri dei tasti (#32)
|
||||
- Aggiungere l'opzione per ricordare / dimenticare lo stato di blocco dei maiuscoli in diversi campi di input (#30)
|
||||
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