Skip to content

Commit

Permalink
Support select till the end of the file / till the start of the file …
Browse files Browse the repository at this point in the history
…keyboard actions on Windows (#989)

This PR is addressing following task

https://youtrack.jetbrains.com/issue/COMPOSE-784/Enable-selecting-till-the-end-of-the-file-till-the-beginning-of-the-file-in-Windows

(As of now, we do not support the standard way of selecting from current
position till the end of the text / till the beginning of the text (that
is - Shift / Ctrl / End and Shift / Ctrl / Home respectively) - but in
MacOS we actually do. This goal of this task is to introduce such
support)

Main changes are following:
* In KeyMapping.skikoMain.kt additional mapping is introduced and used
instead `defaultKeyMapping` on. desktop (which can be done later on,
it's just that we usually - this is at least my understanding - avoid
intrusive changes to commonMain as the very first step if possible)
* SelectionTest rewritten so there'll be less clutter whenever we are
adding new one
  • Loading branch information
Schahen committed Jan 12, 2024
1 parent 66fe4ba commit 8092be4
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 98 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ private val _platformDefaultKeyMapping: KeyMapping =
internal fun createPlatformDefaultKeyMapping(platform: DesktopPlatform): KeyMapping {
return when (platform) {
DesktopPlatform.MacOS -> createMacosDefaultKeyMapping()
else -> defaultKeyMapping
else -> defaultSkikoKeyMapping
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyMapping
import androidx.compose.foundation.text.createPlatformDefaultKeyMapping
import androidx.compose.foundation.text.overriddenDefaultKeyMapping
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.key.Key
Expand All @@ -37,6 +37,7 @@ import org.junit.After
import org.junit.Rule
import org.junit.Test


class SelectionTests {

@get:Rule
Expand All @@ -51,148 +52,240 @@ class SelectionTests {
overriddenDefaultKeyMapping = value
}

@OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
@Test
fun `select using Shift_End and Shift_Home combinations with DesktopPlatform-Windows`() = runBlocking {
setPlatformDefaultKeyMapping(createPlatformDefaultKeyMapping(DesktopPlatform.Windows))
val state = mutableStateOf(TextFieldValue("line 1\nline 2\nline 3\nline 4\nline 5"))
suspend fun SemanticsNodeInteraction.waitAndCheck(check: () -> Unit): SemanticsNodeInteraction {
rule.awaitIdle()
check()
return this
}

@OptIn(ExperimentalTestApi::class)
private fun DesktopPlatform.textFieldSemanticInteraction(initialValue: String = "", semanticNodeContext: suspend SemanticsNodeInteraction.(state: MutableState<TextFieldValue>) -> SemanticsNodeInteraction) =
runBlocking {
setPlatformDefaultKeyMapping(createPlatformDefaultKeyMapping(this@textFieldSemanticInteraction))
val state = mutableStateOf(TextFieldValue(initialValue))

rule.setContent {
BasicTextField(
value = state.value,
onValueChange = { state.value = it },
modifier = Modifier.testTag("textField")
)
}
rule.awaitIdle()
val textField = rule.onNodeWithTag("textField")
textField.performMouseInput {
click(Offset(0f, 0f))
}

rule.awaitIdle()
textField.assertIsFocused()

rule.setContent {
BasicTextField(
value = state.value,
onValueChange = { state.value = it },
modifier = Modifier.testTag("textField")
)
Truth.assertThat(state.value.selection).isEqualTo(TextRange(0, 0))

semanticNodeContext.invoke(textField, state)
}
rule.awaitIdle()
rule.onNodeWithTag("textField").performMouseInput {
click(Offset(0f, 0f))


@OptIn(ExperimentalTestApi::class)
private fun DesktopPlatform.selectLineStart(keyboardInteraction: KeyInjectionScope.() -> Unit) {
textFieldSemanticInteraction("line 1\nline 2\nline 3\nline 4\nline 5") { state ->
performKeyInput {
pressKey(Key.DirectionRight)
pressKey(Key.DirectionDown)
}
.waitAndCheck {
Truth.assertThat(state.value.selection).isEqualTo(TextRange(8, 8))
}
.performKeyInput(keyboardInteraction)
.waitAndCheck {
Truth.assertThat(state.value.selection).isEqualTo(TextRange(8, 7))
}
}
rule.awaitIdle()
rule.onNodeWithTag("textField").assertIsFocused()
Truth.assertThat(state.value.selection).isEqualTo(TextRange(0, 0))
}

rule.onNodeWithTag("textField").performKeyInput {
pressKey(Key.DirectionRight)
@OptIn(ExperimentalTestApi::class)
private fun DesktopPlatform.selectTextStart(keyboardInteraction: KeyInjectionScope.() -> Unit) {
textFieldSemanticInteraction("line 1\nline 2\nline 3\nline 4\nline 5") { state ->
performKeyInput {
pressKey(Key.DirectionRight)
pressKey(Key.DirectionDown)
}.waitAndCheck {
Truth.assertThat(state.value.selection).isEqualTo(TextRange(8, 8))
}
performKeyInput(keyboardInteraction)
.waitAndCheck { Truth.assertThat(state.value.selection).isEqualTo(TextRange(8, 0)) }
}
rule.awaitIdle()
Truth.assertThat(state.value.selection).isEqualTo(TextRange(1, 1))
}

@OptIn(ExperimentalTestApi::class)
private fun DesktopPlatform.selectTextEnd(keyboardInteraction: KeyInjectionScope.() -> Unit) {
textFieldSemanticInteraction("line 1\nline 2\nline 3\nline 4\nline 5") { state ->
performKeyInput {
pressKey(Key.DirectionRight)
pressKey(Key.DirectionDown)
}
.waitAndCheck {
Truth.assertThat(state.value.selection).isEqualTo(TextRange(8, 8))
}
.performKeyInput(keyboardInteraction)
.waitAndCheck {
Truth.assertThat(state.value.selection).isEqualTo(TextRange(8, 34))
}
}
}
@OptIn(ExperimentalTestApi::class)
private fun DesktopPlatform.selectLineEnd(keyboardInteraction: KeyInjectionScope.() -> Unit) {
textFieldSemanticInteraction("line 1\nline 2\nline 3\nline 4\nline 5") { state ->
performKeyInput {
pressKey(Key.DirectionRight)
pressKey(Key.DirectionDown)
}.waitAndCheck {
Truth.assertThat(state.value.selection).isEqualTo(TextRange(8, 8))
}
.performKeyInput(keyboardInteraction)
.waitAndCheck {
Truth.assertThat(state.value.selection).isEqualTo(TextRange(8, 13))
}
}
}

rule.onNodeWithTag("textField").performKeyInput {
@Test
fun `Select till line start with DesktopPlatform-Windows`() = runBlocking {
DesktopPlatform.Windows.selectLineStart {
keyDown(Key.ShiftLeft)
pressKey(Key.MoveEnd)
pressKey(Key.MoveHome)
keyUp(Key.ShiftLeft)
}
rule.awaitIdle()
Truth.assertThat(state.value.selection).isEqualTo(TextRange(1, 6))
}

rule.onNodeWithTag("textField").performKeyInput {
@Test
fun `Select till text start with DesktopPlatform-Windows`() = runBlocking {
DesktopPlatform.Windows.selectTextStart {
keyDown(Key.CtrlLeft)
keyDown(Key.ShiftLeft)
pressKey(Key.MoveHome)
keyUp(Key.ShiftLeft)
keyUp(Key.CtrlLeft)
}
rule.awaitIdle()
Truth.assertThat(state.value.selection).isEqualTo(TextRange(1, 0))
}

@OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
@Test
fun `select using Shift_End and Shift_Home combinations with DesktopPlatform-MacOs`() = runBlocking {
setPlatformDefaultKeyMapping(createPlatformDefaultKeyMapping(DesktopPlatform.MacOS))
val state = mutableStateOf(TextFieldValue("line 1\nline 2\nline 3\nline 4\nline 5"))

rule.setContent {
BasicTextField(
value = state.value,
onValueChange = { state.value = it },
modifier = Modifier.testTag("textField")
)
}
rule.awaitIdle()
rule.onNodeWithTag("textField").performMouseInput {
click(Offset(0f, 0f))
fun `Select till line end with DesktopPlatform-Windows`() = runBlocking {
DesktopPlatform.Windows.selectLineEnd {
keyDown(Key.ShiftLeft)
pressKey(Key.MoveEnd)
keyUp(Key.ShiftLeft)
}
rule.awaitIdle()
rule.onNodeWithTag("textField").assertIsFocused()
Truth.assertThat(state.value.selection).isEqualTo(TextRange(0, 0))
}

rule.onNodeWithTag("textField").performKeyInput {
pressKey(Key.DirectionRight)
@Test
fun `Select till text end with DesktopPlatform-Windows`() = runBlocking {
DesktopPlatform.Windows.selectTextEnd {
keyDown(Key.CtrlLeft)
keyDown(Key.ShiftLeft)
pressKey(Key.MoveEnd)
keyUp(Key.ShiftLeft)
keyUp(Key.CtrlLeft)
}
rule.awaitIdle()
Truth.assertThat(state.value.selection).isEqualTo(TextRange(1, 1))
}

rule.onNodeWithTag("textField").performKeyInput {

@Test
fun `Select till line start with DesktopPlatform-MacOs`() = runBlocking {
DesktopPlatform.MacOS.selectLineStart() {
keyDown(Key.ShiftLeft)
pressKey(Key.MoveEnd)
keyDown(Key.MetaLeft)
pressKey(Key.DirectionLeft)
keyUp(Key.ShiftLeft)
keyUp(Key.MetaLeft)
}
rule.awaitIdle()
Truth.assertThat(state.value.selection).isEqualTo(TextRange(1, 34))
}

rule.onNodeWithTag("textField").performKeyInput {
@Test
fun `Select till text start with DesktopPlatform-MacOs`() = runBlocking {
DesktopPlatform.MacOS.selectTextStart {
keyDown(Key.ShiftLeft)
pressKey(Key.MoveHome)
pressKey(Key.Home)
keyUp(Key.ShiftLeft)
}
rule.awaitIdle()
Truth.assertThat(state.value.selection).isEqualTo(TextRange(1, 0))
}

@OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
@Test
fun `Ctrl + Backspace on an empty line with DesktopPlatform-Windows`() = runBlocking {
setPlatformDefaultKeyMapping(createPlatformDefaultKeyMapping(DesktopPlatform.Windows))
val state = mutableStateOf(TextFieldValue(""))
fun `Select till line end with DesktopPlatform-Macos`() = runBlocking {
DesktopPlatform.MacOS.selectLineEnd {
keyDown(Key.ShiftLeft)
keyDown(Key.MetaLeft)
pressKey(Key.DirectionRight)
keyUp(Key.ShiftLeft)
keyUp(Key.MetaLeft)
}
}

rule.setContent {
BasicTextField(
value = state.value,
onValueChange = { state.value = it },
modifier = Modifier.testTag("textField")
)
@Test
fun `Select till text end with DesktopPlatform-Macos`() = runBlocking {
DesktopPlatform.MacOS.selectTextEnd {
keyDown(Key.ShiftLeft)
pressKey(Key.MoveEnd)
keyUp(Key.ShiftLeft)
}
rule.awaitIdle()
rule.onNodeWithTag("textField").performMouseInput {
click(Offset(0f, 0f))
}

@OptIn(ExperimentalTestApi::class)
private fun DesktopPlatform.deleteAllFromKeyBoard(
initialText: String, deleteAllInteraction: KeyInjectionScope.() -> Unit
) {
textFieldSemanticInteraction(initialText) { state ->
performKeyInput(deleteAllInteraction).waitAndCheck { Truth.assertThat(state.value.text).isEqualTo("") }
}
rule.awaitIdle()
rule.onNodeWithTag("textField").assertIsFocused()
Truth.assertThat(state.value.selection).isEqualTo(TextRange(0, 0))
}

rule.onNodeWithTag("textField").performKeyInput {

@Test
fun `Delete backwards on an empty line with DesktopPlatform-Windows`() {
DesktopPlatform.Windows.deleteAllFromKeyBoard("") {
keyDown(Key.CtrlLeft)
keyDown(Key.Backspace)
}
rule.awaitIdle()
}

@OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
@Test
fun `Ctrl + Backspace on an empty line with DesktopPlatform-Macos`() = runBlocking {
setPlatformDefaultKeyMapping(createPlatformDefaultKeyMapping(DesktopPlatform.MacOS))
val state = mutableStateOf(TextFieldValue(""))
fun `Delete backwards on an empty line with DesktopPlatform-Macos`() {
DesktopPlatform.MacOS.deleteAllFromKeyBoard("") {
keyDown(Key.MetaLeft)
keyDown(Key.Delete)
}
}

rule.setContent {
BasicTextField(
value = state.value,
onValueChange = { state.value = it },
modifier = Modifier.testTag("textField")
)
@OptIn(ExperimentalTestApi::class)
private fun DesktopPlatform.selectAllTest(selectAllInteraction: KeyInjectionScope.() -> Unit) {
textFieldSemanticInteraction("Select this text") { state ->
performKeyInput(selectAllInteraction)
.waitAndCheck {
Truth.assertThat(state.value.selection).isEqualTo(TextRange(0, 16))
}
.performKeyInput { keyDown(Key.Delete) }
.waitAndCheck {
Truth.assertThat(state.value.selection).isEqualTo(TextRange(0, 0))
Truth.assertThat(state.value.text).isEqualTo("")
}
}
rule.awaitIdle()
rule.onNodeWithTag("textField").performMouseInput {
click(Offset(0f, 0f))
}

@Test
fun `Select all with DesktopPlatform-Windows`() = runBlocking {
DesktopPlatform.Windows.selectAllTest {
keyDown(Key.CtrlLeft)
pressKey(Key.A)
keyUp(Key.CtrlLeft)
}
rule.awaitIdle()
rule.onNodeWithTag("textField").assertIsFocused()
Truth.assertThat(state.value.selection).isEqualTo(TextRange(0, 0))
}

rule.onNodeWithTag("textField").performKeyInput {
keyDown(Key.AltLeft)
keyDown(Key.Backspace)
@Test
fun `Select all with DesktopPlatform-Macos`() = runBlocking {
DesktopPlatform.MacOS.selectAllTest {
keyDown(Key.MetaLeft)
pressKey(Key.A)
keyUp(Key.MetaLeft)
}
rule.awaitIdle()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ internal expect val MappedKeys.D: Key
internal expect val MappedKeys.K: Key
internal expect val MappedKeys.O: Key

internal object defaultSkikoKeyMapping : KeyMapping {
override fun map(event: KeyEvent): KeyCommand? {
return when {
event.isCtrlPressed && event.isShiftPressed -> {
when (event.key) {
MappedKeys.MoveHome -> KeyCommand.SELECT_HOME
MappedKeys.MoveEnd -> KeyCommand.SELECT_END
else -> null
}
}
else -> null
} ?: defaultKeyMapping.map(event)
}
}

internal fun createMacosDefaultKeyMapping(): KeyMapping {
val common = commonKeyMapping(KeyEvent::isMetaPressed)
return object : KeyMapping {
Expand Down

0 comments on commit 8092be4

Please sign in to comment.