Last active
April 20, 2021 23:42
-
-
Save eygraber/d35cb777f4ce5d6c936774268ac859a1 to your computer and use it in GitHub Desktop.
An initial attempt at providing some basic text editing shortcuts for Compose
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.