Skip to content

Instantly share code, notes, and snippets.

@eygraber
Last active April 20, 2021 23:42
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eygraber/d35cb777f4ce5d6c936774268ac859a1 to your computer and use it in GitHub Desktop.
Save eygraber/d35cb777f4ce5d6c936774268ac859a1 to your computer and use it in GitHub Desktop.
An initial attempt at providing some basic text editing shortcuts for Compose
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeysSet
import androidx.compose.ui.input.key.ShortcutsBuilderScope
import androidx.compose.ui.input.key.plus
import androidx.compose.ui.text.TextRange
import java.awt.Toolkit
import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.StringSelection
import java.awt.event.KeyEvent
import java.util.Stack
fun ShortcutsBuilderScope.undoAndRedoShortcut(
value: String,
history: Stack<String>,
redo: Stack<String>,
undoAction: KeysSet = Key.CtrlLeft + Key.Z,
redoAction: KeysSet = Key.CtrlLeft + Key.Y,
onValueChanged: (String) -> Unit
) {
history.push(value)
on(undoAction) {
redo.push(history.pop())
history.peek()?.let(onValueChanged)
}
on(redoAction) {
redo.peek()?.let {
redo.pop()
onValueChanged(it)
}
}
}
fun ShortcutsBuilderScope.textEditingShortcuts(
value: String,
selection: TextRange,
selectForward: KeysSet = Key(KeyEvent.VK_RIGHT) + Key.ShiftLeft,
selectBackward: KeysSet = Key(KeyEvent.VK_LEFT) + Key.ShiftLeft,
navigateToStart: KeysSet = Key(KeyEvent.VK_LEFT) + Key.CtrlLeft,
navigateToEnd: KeysSet = Key(KeyEvent.VK_RIGHT) + Key.CtrlLeft,
selectToStart: KeysSet = Key(KeyEvent.VK_LEFT) + Key.CtrlLeft + Key.ShiftLeft,
selectToEnd: KeysSet = Key(KeyEvent.VK_RIGHT) + Key.CtrlLeft + Key.ShiftLeft,
navigateCamelCaseForward: KeysSet = Key(KeyEvent.VK_RIGHT) + Key.AltLeft,
selectCamelCaseForward: KeysSet = Key(KeyEvent.VK_RIGHT) + Key.AltLeft + Key.ShiftLeft,
navigateCamelCaseBackward: KeysSet = Key(KeyEvent.VK_LEFT) + Key.AltLeft,
selectCamelCaseBackward: KeysSet = Key(KeyEvent.VK_LEFT) + Key.AltLeft + Key.ShiftLeft,
onValueChanged: (String) -> Unit,
onSelectionChanged: (TextRange) -> Unit
) {
if(!selection.collapsed) {
on(Key(KeyEvent.VK_LEFT)) {
onSelectionChanged(TextRange(selection.min))
}
on(Key(KeyEvent.VK_RIGHT)) {
onSelectionChanged(TextRange(selection.max))
}
}
on(Key.Delete) {
onValueChanged(
StringBuilder(value).apply {
when {
selection.collapsed -> deleteCharAt(selection.max.coerceAtMost(value.lastIndex))
else -> deleteRange(selection.min, selection.max.coerceAtMost(value.length))
}
}.toString()
)
onSelectionChanged(TextRange(selection.min))
}
on(Key.CtrlLeft + Key.A) {
onSelectionChanged(TextRange(0, value.length + 1))
}
fun copy() {
if(!selection.collapsed) {
value.substring(selection.min, selection.max.coerceAtMost(value.length)).copyToClipboard()
}
}
on(Key.CtrlLeft + Key.C) {
copy()
}
on(Key.CtrlLeft + Key.Insert) {
copy()
}
fun cut() {
if(!selection.collapsed) {
value.substring(selection.min, selection.max.coerceAtMost(value.length)).copyToClipboard()
onValueChanged(
StringBuilder(value).apply {
deleteRange(selection.min, selection.max.coerceAtMost(value.length))
}.toString()
)
onSelectionChanged(TextRange(selection.min))
}
}
on(Key.CtrlLeft + Key.X) {
cut()
}
on(Key.ShiftLeft + Key.Delete) {
cut()
}
fun paste() {
getClipboardString()?.let { clipboardString ->
onValueChanged(
StringBuilder(value).apply {
when {
selection.collapsed -> when(selection.max) {
value.length -> append(clipboardString)
else -> insert(selection.max, clipboardString)
}
else -> {
replace(selection.min, selection.max, clipboardString)
}
}
onSelectionChanged(TextRange(selection.min + clipboardString.length))
}.toString()
)
}
}
on(Key.CtrlLeft + Key.V) {
paste()
}
on(Key.ShiftLeft + Key.Insert) {
paste()
}
on(Key(KeyEvent.VK_HOME)) {
onSelectionChanged(TextRange.Zero)
}
on(Key(KeyEvent.VK_END)) {
onSelectionChanged(TextRange(value.length + 1))
}
on(Key(KeyEvent.VK_HOME) + Key.ShiftLeft) {
onSelectionChanged(TextRange(0, selection.max))
}
on(Key(KeyEvent.VK_END) + Key.ShiftLeft) {
onSelectionChanged(TextRange(selection.min, value.length + 1))
}
on(navigateToStart) {
onSelectionChanged(TextRange.Zero)
}
on(navigateToEnd) {
onSelectionChanged(TextRange(value.length + 1))
}
on(selectToStart) {
onSelectionChanged(TextRange(0, selection.max))
}
on(selectToEnd) {
onSelectionChanged(TextRange(selection.min, value.length + 1))
}
on(selectForward) {
onSelectionChanged(TextRange(selection.min, (selection.max + 1).coerceAtMost(value.length + 1)))
}
on(selectBackward) {
onSelectionChanged(TextRange((selection.min - 1).coerceAtLeast(0), selection.max))
}
fun navigateCamelCaseForward(selectAllTextInRange: Boolean) {
val startingIndex = selection.max.let { startingIndex ->
when {
startingIndex > value.lastIndex -> value.lastIndex
value[startingIndex].isUpperCase() -> startingIndex + 1
else -> startingIndex
}
}.takeIf { it >= 0 } ?: return
var foundIndex = -1
for(index in startingIndex until value.length) {
if(value[index].isUpperCase() || !value[index].isLetterOrDigit()) {
foundIndex = index
break
}
}
if(foundIndex == startingIndex && foundIndex < value.lastIndex) {
foundIndex++
}
onSelectionChanged(
when(foundIndex) {
-1 -> when {
selectAllTextInRange -> TextRange(selection.min, value.length + 1)
else -> TextRange(value.length + 1)
}
else -> when {
selectAllTextInRange -> TextRange(selection.min, foundIndex)
else -> TextRange(foundIndex)
}
}
)
}
on(navigateCamelCaseForward) {
navigateCamelCaseForward(selectAllTextInRange = false)
}
on(selectCamelCaseForward) {
navigateCamelCaseForward(selectAllTextInRange = true)
}
fun navigateCamelCaseBackward(selectAllTextInRange: Boolean) {
val startingIndex = selection.min.let { startingIndex ->
when {
startingIndex > value.lastIndex -> value.lastIndex
else -> startingIndex - 1
}
}.takeIf { it >= 0 } ?: return
var foundIndex = -1
for(index in startingIndex downTo 0) {
if(value[index].isUpperCase()) {
foundIndex = when(index) {
startingIndex -> index - 1
else -> index
}
break
}
else if(!value[index].isLetterOrDigit()) {
foundIndex = when(index) {
startingIndex -> index
else -> index + 1
}
break
}
}
onSelectionChanged(
when(foundIndex) {
-1 -> when {
selectAllTextInRange -> TextRange(0, selection.max)
else -> TextRange.Zero
}
else -> when {
selectAllTextInRange -> TextRange(foundIndex, selection.max)
else -> TextRange(foundIndex)
}
}
)
}
on(navigateCamelCaseBackward) {
navigateCamelCaseBackward(selectAllTextInRange = false)
}
on(selectCamelCaseBackward) {
navigateCamelCaseBackward(selectAllTextInRange = true)
}
}
private fun getClipboardString() = runCatching {
Toolkit.getDefaultToolkit().systemClipboard.getData(DataFlavor.stringFlavor) as? String
}.getOrNull()
private fun String.copyToClipboard() = try {
Toolkit.getDefaultToolkit()
.systemClipboard
.setContents(
StringSelection(this),
null
)
}
catch(_: Throwable) {
}
@Nambers
Copy link

Nambers commented Apr 20, 2021

HI, How can I get the textrange?
I try to get the textRange(the selection) in SelectionContainer component
However, it is internal. So, how can i get it?
ps: I am try to make copy(ctrl + c) enable in text component(I wrapper a SelectionContainer component out it).
The version is 0.4

Thanks a lot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment