Compose activity: When selection is nonempty and a "special character" button is pressed, decorate all selected word starts (#1523)

* ComposeActivity: When selection is nonempty and a "special character" button is pressed, decorate all selected word starts
Closes #1417

* ComposeActivity: Tests for word break prepend logic
This commit is contained in:
Levi Bard 2020-01-13 15:21:17 +01:00 committed by Konrad Pozniak
parent 6a2c9bdbe4
commit c9a28b47c1
2 changed files with 189 additions and 2 deletions

View file

@ -416,12 +416,53 @@ class ComposeActivity : BaseActivity(),
composeEditField.setSelection(start + text.length) composeEditField.setSelection(start + text.length)
} }
fun prependSelectedWordsWith(text: CharSequence) {
// If you select "backward" in an editable, you get SelectionStart > SelectionEnd
val start = composeEditField.selectionStart.coerceAtMost(composeEditField.selectionEnd)
val end = composeEditField.selectionStart.coerceAtLeast(composeEditField.selectionEnd)
val editorText = composeEditField.text
if (start == end) {
// No selection, just insert text at caret
editorText.insert(start, text)
// Set the cursor after the inserted text
composeEditField.setSelection(start + text.length)
} else {
var wasWord: Boolean
var isWord = end < editorText.length && !Character.isWhitespace(editorText[end])
var newEnd = end
// Iterate the selection backward so we don't have to juggle indices on insertion
var index = end - 1
while (index >= start - 1 && index >= 0) {
wasWord = isWord
isWord = !Character.isWhitespace(editorText[index])
if (wasWord && !isWord) {
// We've reached the beginning of a word, perform insert
editorText.insert(index + 1, text)
newEnd += text.length
}
--index
}
if (start == 0 && isWord) {
// Special case when the selection includes the start of the text
editorText.insert(0, text)
newEnd += text.length
}
// Keep the same text (including insertions) selected
composeEditField.setSelection(start, newEnd)
}
}
private fun atButtonClicked() { private fun atButtonClicked() {
replaceTextAtCaret("@") prependSelectedWordsWith("@")
} }
private fun hashButtonClicked() { private fun hashButtonClicked() {
replaceTextAtCaret("#") prependSelectedWordsWith("#")
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {

View file

@ -198,6 +198,152 @@ class ComposeActivityTest {
assertEquals(activity.calculateTextLength(), additionalContent.length + (ComposeActivity.MAXIMUM_URL_LENGTH * 2)) assertEquals(activity.calculateTextLength(), additionalContent.length + (ComposeActivity.MAXIMUM_URL_LENGTH * 2))
} }
@Test
fun whenSelectionIsEmpty_specialTextIsInsertedAtCaret() {
val editor = activity.findViewById<EditText>(R.id.composeEditField)
val insertText = "#"
editor.setText("Some text")
for (caretIndex in listOf(9, 1, 0)) {
editor.setSelection(caretIndex)
activity.prependSelectedWordsWith(insertText)
// Text should be inserted at caret
assertEquals("Unexpected value at ${caretIndex}", insertText, editor.text.substring(caretIndex, caretIndex + insertText.length))
// Caret should be placed after inserted text
assertEquals(caretIndex + insertText.length, editor.selectionStart)
assertEquals(caretIndex + insertText.length, editor.selectionEnd)
}
}
@Test
fun whenSelectionDoesNotIncludeWordBreak_noSpecialTextIsInserted() {
val editor = activity.findViewById<EditText>(R.id.composeEditField)
val insertText = "#"
val originalText = "Some text"
val selectionStart = 1
val selectionEnd = 4
editor.setText(originalText)
editor.setSelection(selectionStart, selectionEnd) // "ome"
activity.prependSelectedWordsWith(insertText)
// Text and selection should be unmodified
assertEquals(originalText, editor.text.toString())
assertEquals(selectionStart, editor.selectionStart)
assertEquals(selectionEnd, editor.selectionEnd)
}
@Test
fun whenSelectionIncludesWordBreaks_startsOfAllWordsArePrepended() {
val editor = activity.findViewById<EditText>(R.id.composeEditField)
val insertText = "#"
val originalText = "one two three four"
val selectionStart = 2
val originalSelectionEnd = 15
val modifiedSelectionEnd = 18
editor.setText(originalText)
editor.setSelection(selectionStart, originalSelectionEnd) // "e two three f"
activity.prependSelectedWordsWith(insertText)
// text should be inserted at word starts inside selection
assertEquals("one #two #three #four", editor.text.toString())
// selection should be expanded accordingly
assertEquals(selectionStart, editor.selectionStart)
assertEquals(modifiedSelectionEnd, editor.selectionEnd)
}
@Test
fun whenSelectionIncludesEnd_textIsNotAppended() {
val editor = activity.findViewById<EditText>(R.id.composeEditField)
val insertText = "#"
val originalText = "Some text"
val selectionStart = 7
val selectionEnd = 9
editor.setText(originalText)
editor.setSelection(selectionStart, selectionEnd) // "xt"
activity.prependSelectedWordsWith(insertText)
// Text and selection should be unmodified
assertEquals(originalText, editor.text.toString())
assertEquals(selectionStart, editor.selectionStart)
assertEquals(selectionEnd, editor.selectionEnd)
}
@Test
fun whenSelectionIncludesStartAndStartIsAWord_textIsPrepended() {
val editor = activity.findViewById<EditText>(R.id.composeEditField)
val insertText = "#"
val originalText = "Some text"
val selectionStart = 0
val selectionEnd = 3
editor.setText(originalText)
editor.setSelection(selectionStart, selectionEnd) // "Som"
activity.prependSelectedWordsWith(insertText)
// Text should be inserted at beginning
assert(editor.text.startsWith(insertText))
// selection should be expanded accordingly
assertEquals(selectionStart, editor.selectionStart)
assertEquals(selectionEnd + insertText.length, editor.selectionEnd)
}
@Test
fun whenSelectionIncludesStartAndStartIsNotAWord_textIsNotPrepended() {
val editor = activity.findViewById<EditText>(R.id.composeEditField)
val insertText = "#"
val originalText = " Some text"
val selectionStart = 0
val selectionEnd = 1
editor.setText(originalText)
editor.setSelection(selectionStart, selectionEnd) // " "
activity.prependSelectedWordsWith(insertText)
// Text and selection should be unmodified
assertEquals(originalText, editor.text.toString())
assertEquals(selectionStart, editor.selectionStart)
assertEquals(selectionEnd, editor.selectionEnd)
}
@Test
fun whenSelectionBeginsAtWordStart_textIsPrepended() {
val editor = activity.findViewById<EditText>(R.id.composeEditField)
val insertText = "#"
val originalText = "Some text"
val selectionStart = 5
val selectionEnd = 9
editor.setText(originalText)
editor.setSelection(selectionStart, selectionEnd) // "text"
activity.prependSelectedWordsWith(insertText)
// Text is prepended
assertEquals("Some #text", editor.text.toString())
// Selection is expanded accordingly
assertEquals(selectionStart, editor.selectionStart)
assertEquals(selectionEnd + insertText.length, editor.selectionEnd)
}
@Test
fun whenSelectionEndsAtWordStart_textIsAppended() {
val editor = activity.findViewById<EditText>(R.id.composeEditField)
val insertText = "#"
val originalText = "Some text"
val selectionStart = 1
val selectionEnd = 5
editor.setText(originalText)
editor.setSelection(selectionStart, selectionEnd) // "ome "
activity.prependSelectedWordsWith(insertText)
// Text is prepended
assertEquals("Some #text", editor.text.toString())
// Selection is expanded accordingly
assertEquals(selectionStart, editor.selectionStart)
assertEquals(selectionEnd + insertText.length, editor.selectionEnd)
}
private fun clickUp() { private fun clickUp() {
val menuItem = RoboMenuItem(android.R.id.home) val menuItem = RoboMenuItem(android.R.id.home)
activity.onOptionsItemSelected(menuItem) activity.onOptionsItemSelected(menuItem)