mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
fix: TokenAutoComplete as lib
This commit is contained in:
@@ -112,7 +112,7 @@ dependencies {
|
||||
implementation "com.google.android.material:material:$android_material_version"
|
||||
// Token auto complete
|
||||
// From sources until https://github.com/splitwise/TokenAutoComplete/pull/422 fixed
|
||||
// implementation "com.splitwise:tokenautocomplete:4.0.0-beta04"
|
||||
implementation "com.splitwise:tokenautocomplete:4.0.0-beta05"
|
||||
// Database
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
package com.tokenautocomplete
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.text.SpannableString
|
||||
import android.text.Spanned
|
||||
import android.text.TextUtils
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Tokenizer with configurable array of characters to tokenize on.
|
||||
*
|
||||
* Created on 2/3/15.
|
||||
* @author mgod
|
||||
*/
|
||||
@Parcelize
|
||||
@SuppressLint("ParcelCreator")
|
||||
open class CharacterTokenizer(private val splitChar: List<Char>, private val tokenTerminator: String) : Tokenizer {
|
||||
override fun containsTokenTerminator(charSequence: CharSequence): Boolean {
|
||||
for (element in charSequence) {
|
||||
if (splitChar.contains(element)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun findTokenRanges(charSequence: CharSequence, start: Int, end: Int): List<Range> {
|
||||
val result = ArrayList<Range>()
|
||||
if (start == end) {
|
||||
//Can't have a 0 length token
|
||||
return result
|
||||
}
|
||||
var tokenStart = start
|
||||
for (cursor in start until end) {
|
||||
val character = charSequence[cursor]
|
||||
|
||||
//Avoid including leading whitespace, tokenStart will match the cursor as long as we're at the start
|
||||
if (tokenStart == cursor && Character.isWhitespace(character)) {
|
||||
tokenStart = cursor + 1
|
||||
}
|
||||
|
||||
//Either this is a split character, or we contain some content and are at the end of input
|
||||
if (splitChar.contains(character) || cursor == end - 1) {
|
||||
val hasTokenContent = //There is token content befor the current character
|
||||
cursor > tokenStart || //If the current single character is valid token content, not a split char or whitespace
|
||||
cursor == tokenStart && !splitChar.contains(character)
|
||||
if (hasTokenContent) {
|
||||
//There is some token content
|
||||
//Add one to range end as the end of the ranges is not inclusive
|
||||
result.add(Range(tokenStart, cursor + 1))
|
||||
}
|
||||
tokenStart = cursor + 1
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun wrapTokenValue(unwrappedTokenValue: CharSequence): CharSequence {
|
||||
val wrappedText: CharSequence = unwrappedTokenValue.toString() + tokenTerminator
|
||||
return if (unwrappedTokenValue is Spanned) {
|
||||
val sp = SpannableString(wrappedText)
|
||||
TextUtils.copySpansFrom(
|
||||
unwrappedTokenValue, 0, unwrappedTokenValue.length,
|
||||
Any::class.java, sp, 0
|
||||
)
|
||||
sp
|
||||
} else {
|
||||
wrappedText
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package com.tokenautocomplete
|
||||
|
||||
import android.text.Layout
|
||||
import android.text.TextPaint
|
||||
import android.text.style.CharacterStyle
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Span that displays +[count]
|
||||
*
|
||||
* Created on 2/3/15.
|
||||
* @author mgod
|
||||
*/
|
||||
class CountSpan : CharacterStyle() {
|
||||
var countText = ""
|
||||
private set
|
||||
|
||||
override fun updateDrawState(textPaint: TextPaint) {
|
||||
//Do nothing, we are using this span as a location marker
|
||||
}
|
||||
|
||||
fun setCount(c: Int) {
|
||||
countText = if (c > 0) {
|
||||
String.format(Locale.getDefault(), " +%d", c)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
fun getCountTextWidthForPaint(paint: TextPaint?): Float {
|
||||
return Layout.getDesiredWidth(countText, 0, countText.length, paint)
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package com.tokenautocomplete
|
||||
|
||||
import android.text.TextPaint
|
||||
import android.text.style.MetricAffectingSpan
|
||||
|
||||
/**
|
||||
* Invisible MetricAffectingSpan that will trigger a redraw when it is being added to or removed from an Editable.
|
||||
*
|
||||
* @see TokenCompleteTextView.redrawTokens
|
||||
*/
|
||||
internal class DummySpan private constructor() : MetricAffectingSpan() {
|
||||
override fun updateMeasureState(textPaint: TextPaint) {}
|
||||
override fun updateDrawState(tp: TextPaint) {}
|
||||
|
||||
companion object {
|
||||
val INSTANCE = DummySpan()
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package com.tokenautocomplete
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Filter
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Simplified custom filtered ArrayAdapter
|
||||
* override keepObject with your test for filtering
|
||||
*
|
||||
*
|
||||
* Based on gist [
|
||||
* FilteredArrayAdapter](https://gist.github.com/tobiasschuerg/3554252/raw/30634bf9341311ac6ad6739ef094222fc5f07fa8/FilteredArrayAdapter.java) by Tobias Schürg
|
||||
*
|
||||
*
|
||||
* Created on 9/17/13.
|
||||
* @author mgod
|
||||
*/
|
||||
abstract class FilteredArrayAdapter<T>
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param context The current context.
|
||||
* @param resource The resource ID for a layout file containing a layout to use when
|
||||
* instantiating views.
|
||||
* @param textViewResourceId The id of the TextView within the layout resource to be populated
|
||||
* @param objects The objects to represent in the ListView.
|
||||
*/(
|
||||
context: Context,
|
||||
resource: Int,
|
||||
textViewResourceId: Int,
|
||||
objects: List<T>
|
||||
) : ArrayAdapter<T>(
|
||||
context, resource, textViewResourceId, ArrayList(objects)
|
||||
) {
|
||||
private val originalObjects: List<T> = objects
|
||||
private var filter: Filter? = null
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param context The current context.
|
||||
* @param resource The resource ID for a layout file containing a TextView to use when
|
||||
* instantiating views.
|
||||
* @param objects The objects to represent in the ListView.
|
||||
*/
|
||||
constructor(context: Context, resource: Int, objects: Array<T>) : this(
|
||||
context,
|
||||
resource,
|
||||
0,
|
||||
objects
|
||||
)
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param context The current context.
|
||||
* @param resource The resource ID for a layout file containing a layout to use when
|
||||
* instantiating views.
|
||||
* @param textViewResourceId The id of the TextView within the layout resource to be populated
|
||||
* @param objects The objects to represent in the ListView.
|
||||
*/
|
||||
constructor(
|
||||
context: Context,
|
||||
resource: Int,
|
||||
textViewResourceId: Int,
|
||||
objects: Array<T>
|
||||
) : this(context, resource, textViewResourceId, ArrayList<T>(listOf(*objects)))
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param context The current context.
|
||||
* @param resource The resource ID for a layout file containing a TextView to use when
|
||||
* instantiating views.
|
||||
* @param objects The objects to represent in the ListView.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
constructor(context: Context, resource: Int, objects: List<T>) : this(
|
||||
context,
|
||||
resource,
|
||||
0,
|
||||
objects
|
||||
)
|
||||
|
||||
override fun getFilter(): Filter {
|
||||
if (filter == null) filter = AppFilter()
|
||||
return filter!!
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter method used by the adapter. Return true if the object should remain in the list
|
||||
*
|
||||
* @param obj object we are checking for inclusion in the adapter
|
||||
* @param mask current text in the edit text we are completing against
|
||||
* @return true if we should keep the item in the adapter
|
||||
*/
|
||||
protected abstract fun keepObject(obj: T, mask: String?): Boolean
|
||||
|
||||
/**
|
||||
* Class for filtering Adapter, relies on keepObject in FilteredArrayAdapter
|
||||
*
|
||||
* based on gist by Tobias Schürg
|
||||
* in turn inspired by inspired by Alxandr
|
||||
* (http://stackoverflow.com/a/2726348/570168)
|
||||
*/
|
||||
private inner class AppFilter : Filter() {
|
||||
override fun performFiltering(chars: CharSequence?): FilterResults {
|
||||
val sourceObjects = ArrayList(originalObjects)
|
||||
val result = FilterResults()
|
||||
if (chars != null && chars.isNotEmpty()) {
|
||||
val mask = chars.toString()
|
||||
val keptObjects = ArrayList<T>()
|
||||
for (sourceObject in sourceObjects) {
|
||||
if (keepObject(sourceObject, mask)) keptObjects.add(sourceObject)
|
||||
}
|
||||
result.count = keptObjects.size
|
||||
result.values = keptObjects
|
||||
} else {
|
||||
// add all objects
|
||||
result.values = sourceObjects
|
||||
result.count = sourceObjects.size
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun publishResults(constraint: CharSequence?, results: FilterResults) {
|
||||
clear()
|
||||
if (results.count > 0) {
|
||||
@Suppress("unchecked_cast")
|
||||
this@FilteredArrayAdapter.addAll(results.values as Collection<T>)
|
||||
notifyDataSetChanged()
|
||||
} else {
|
||||
notifyDataSetInvalidated()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package com.tokenautocomplete
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.text.style.TextAppearanceSpan
|
||||
|
||||
/**
|
||||
* Subclass of TextAppearanceSpan just to work with how Spans get detected
|
||||
*
|
||||
* Created on 2/3/15.
|
||||
* @author mgod
|
||||
*/
|
||||
internal class HintSpan(
|
||||
family: String?,
|
||||
style: Int,
|
||||
size: Int,
|
||||
color: ColorStateList?,
|
||||
linkColor: ColorStateList?
|
||||
) : TextAppearanceSpan(family, style, size, color, linkColor)
|
||||
@@ -1,41 +0,0 @@
|
||||
package com.tokenautocomplete
|
||||
|
||||
import java.util.*
|
||||
|
||||
class Range(start: Int, end: Int) {
|
||||
@JvmField
|
||||
val start: Int
|
||||
@JvmField
|
||||
val end: Int
|
||||
fun length(): Int {
|
||||
return end - start
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (null == other || other !is Range) {
|
||||
return false
|
||||
}
|
||||
return other.start == start && other.end == end
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return String.format(Locale.US, "[%d..%d]", start, end)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = start
|
||||
result = 31 * result + end
|
||||
return result
|
||||
}
|
||||
|
||||
init {
|
||||
require(start <= end) {
|
||||
String.format(
|
||||
Locale.ENGLISH,
|
||||
"Start (%d) cannot be greater than end (%d)", start, end
|
||||
)
|
||||
}
|
||||
this.start = start
|
||||
this.end = end
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package com.tokenautocomplete
|
||||
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.TextPaint
|
||||
import android.text.TextUtils
|
||||
import com.tokenautocomplete.TokenCompleteTextView.TokenImageSpan
|
||||
|
||||
internal object SpanUtils {
|
||||
@JvmStatic
|
||||
fun ellipsizeWithSpans(
|
||||
prefix: CharSequence?, countSpan: CountSpan?,
|
||||
tokenCount: Int, paint: TextPaint,
|
||||
originalText: CharSequence, maxWidth: Float
|
||||
): Spanned? {
|
||||
var countWidth = 0f
|
||||
if (countSpan != null) {
|
||||
//Assume the largest possible number of items for measurement
|
||||
countSpan.setCount(tokenCount)
|
||||
countWidth = countSpan.getCountTextWidthForPaint(paint)
|
||||
}
|
||||
val ellipsizeCallback = EllipsizeCallback()
|
||||
val tempEllipsized = TextUtils.ellipsize(
|
||||
originalText, paint, maxWidth - countWidth,
|
||||
TextUtils.TruncateAt.END, false, ellipsizeCallback
|
||||
)
|
||||
val ellipsized = SpannableStringBuilder(tempEllipsized)
|
||||
if (tempEllipsized is Spanned) {
|
||||
TextUtils.copySpansFrom(
|
||||
tempEllipsized,
|
||||
0,
|
||||
tempEllipsized.length,
|
||||
Any::class.java,
|
||||
ellipsized,
|
||||
0
|
||||
)
|
||||
}
|
||||
if (prefix != null && prefix.length > ellipsizeCallback.start) {
|
||||
//We ellipsized part of the prefix, so put it back
|
||||
ellipsized.replace(0, ellipsizeCallback.start, prefix)
|
||||
ellipsizeCallback.end = ellipsizeCallback.end + prefix.length - ellipsizeCallback.start
|
||||
ellipsizeCallback.start = prefix.length
|
||||
}
|
||||
if (ellipsizeCallback.start != ellipsizeCallback.end) {
|
||||
if (countSpan != null) {
|
||||
val visibleCount =
|
||||
ellipsized.getSpans(0, ellipsized.length, TokenImageSpan::class.java).size
|
||||
countSpan.setCount(tokenCount - visibleCount)
|
||||
ellipsized.replace(ellipsizeCallback.start, ellipsized.length, countSpan.countText)
|
||||
ellipsized.setSpan(
|
||||
countSpan, ellipsizeCallback.start, ellipsized.length,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
return ellipsized
|
||||
}
|
||||
//No ellipses necessary
|
||||
return null
|
||||
}
|
||||
|
||||
private class EllipsizeCallback : TextUtils.EllipsizeCallback {
|
||||
var start = 0
|
||||
var end = 0
|
||||
override fun ellipsized(ellipsedStart: Int, ellipsedEnd: Int) {
|
||||
start = ellipsedStart
|
||||
end = ellipsedEnd
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package com.tokenautocomplete
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.util.*
|
||||
|
||||
@Parcelize
|
||||
@SuppressLint("ParcelCreator")
|
||||
open class TagTokenizer constructor(private val tagPrefixes: List<Char>) : Tokenizer {
|
||||
|
||||
internal constructor() : this(listOf<Char>('@', '#'))
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
protected fun isTokenTerminator(character: Char): Boolean {
|
||||
//Allow letters, numbers and underscores
|
||||
return !Character.isLetterOrDigit(character) && character != '_'
|
||||
}
|
||||
|
||||
override fun containsTokenTerminator(charSequence: CharSequence): Boolean {
|
||||
for (element in charSequence) {
|
||||
if (isTokenTerminator(element)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun findTokenRanges(charSequence: CharSequence, start: Int, end: Int): List<Range> {
|
||||
val result = ArrayList<Range>()
|
||||
if (start == end) {
|
||||
//Can't have a 0 length token
|
||||
return result
|
||||
}
|
||||
var tokenStart = Int.MAX_VALUE
|
||||
for (cursor in start until end) {
|
||||
val character = charSequence[cursor]
|
||||
|
||||
//Either this is a terminator, or we contain some content and are at the end of input
|
||||
if (isTokenTerminator(character)) {
|
||||
//Is there some token content? Might just be two terminators in a row
|
||||
if (cursor - 1 > tokenStart) {
|
||||
result.add(Range(tokenStart, cursor))
|
||||
}
|
||||
|
||||
//mark that we don't have a candidate token start any more
|
||||
tokenStart = Int.MAX_VALUE
|
||||
}
|
||||
|
||||
//Set tokenStart when we hit a tag prefix
|
||||
if (tagPrefixes.contains(character)) {
|
||||
tokenStart = cursor
|
||||
}
|
||||
}
|
||||
if (end > tokenStart) {
|
||||
//There was unterminated text after a start of token
|
||||
result.add(Range(tokenStart, end))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun wrapTokenValue(unwrappedTokenValue: CharSequence): CharSequence {
|
||||
return unwrappedTokenValue
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,34 +0,0 @@
|
||||
package com.tokenautocomplete
|
||||
|
||||
import android.os.Parcelable
|
||||
|
||||
interface Tokenizer : Parcelable {
|
||||
/**
|
||||
* Find all ranges that can be tokenized. This system should detect possible tokens
|
||||
* both with and without having had wrapTokenValue called on the token string representation
|
||||
*
|
||||
* @param charSequence the string to search in
|
||||
* @param start where the tokenizer should start looking for tokens
|
||||
* @param end where the tokenizer should stop looking for tokens
|
||||
* @return all ranges of characters that are valid tokens
|
||||
*/
|
||||
fun findTokenRanges(charSequence: CharSequence, start: Int, end: Int): List<Range>
|
||||
|
||||
/**
|
||||
* Return a complete string representation of the token. Often used to add commas after email
|
||||
* addresses when creating tokens
|
||||
*
|
||||
* This value must NOT include any leading or trailing whitespace
|
||||
*
|
||||
* @param unwrappedTokenValue the value to wrap
|
||||
* @return the token value with any expected delimiter characters
|
||||
*/
|
||||
fun wrapTokenValue(unwrappedTokenValue: CharSequence): CharSequence
|
||||
|
||||
/**
|
||||
* Return true if there is a character in the charSequence that should trigger token detection
|
||||
* @param charSequence source text to look at
|
||||
* @return true if charSequence contains a value that should end a token
|
||||
*/
|
||||
fun containsTokenTerminator(charSequence: CharSequence): Boolean
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package com.tokenautocomplete
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Paint.FontMetricsInt
|
||||
import android.text.style.ReplacementSpan
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.IntRange
|
||||
|
||||
/**
|
||||
* Span that holds a view it draws when rendering
|
||||
*
|
||||
* Created on 2/3/15.
|
||||
* @author mgod
|
||||
*/
|
||||
open class ViewSpan(var view: View, private val layout: Layout) : ReplacementSpan() {
|
||||
private var cachedMaxWidth = -1
|
||||
private fun prepView() {
|
||||
if (layout.maxViewSpanWidth != cachedMaxWidth || view.isLayoutRequested) {
|
||||
cachedMaxWidth = layout.maxViewSpanWidth
|
||||
var spec = View.MeasureSpec.AT_MOST
|
||||
if (cachedMaxWidth == 0) {
|
||||
//If the width is 0, allow the view to choose it's own content size
|
||||
spec = View.MeasureSpec.UNSPECIFIED
|
||||
}
|
||||
val widthSpec = View.MeasureSpec.makeMeasureSpec(cachedMaxWidth, spec)
|
||||
val heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
||||
view.measure(widthSpec, heightSpec)
|
||||
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
|
||||
}
|
||||
}
|
||||
|
||||
override fun draw(
|
||||
canvas: Canvas, text: CharSequence, @IntRange(from = 0) start: Int,
|
||||
@IntRange(from = 0) end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint
|
||||
) {
|
||||
prepView()
|
||||
canvas.save()
|
||||
canvas.translate(x, top.toFloat())
|
||||
view.draw(canvas)
|
||||
canvas.restore()
|
||||
}
|
||||
|
||||
override fun getSize(
|
||||
paint: Paint, charSequence: CharSequence, @IntRange(from = 0) start: Int,
|
||||
@IntRange(from = 0) end: Int, fontMetricsInt: FontMetricsInt?
|
||||
): Int {
|
||||
prepView()
|
||||
if (fontMetricsInt != null) {
|
||||
//We need to make sure the layout allots enough space for the view
|
||||
val height = view.measuredHeight
|
||||
var adjustedBaseline = view.baseline
|
||||
//-1 means the view doesn't support baseline alignment, so align bottom to font baseline
|
||||
if (adjustedBaseline == -1) {
|
||||
adjustedBaseline = height
|
||||
}
|
||||
fontMetricsInt.top = -adjustedBaseline
|
||||
fontMetricsInt.ascent = fontMetricsInt.top
|
||||
fontMetricsInt.bottom = height - adjustedBaseline
|
||||
fontMetricsInt.descent = fontMetricsInt.bottom
|
||||
}
|
||||
return view.right
|
||||
}
|
||||
|
||||
interface Layout {
|
||||
val maxViewSpanWidth: Int
|
||||
}
|
||||
|
||||
init {
|
||||
view.layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user