Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support select till the end of the file / till the start of the file keyboard actions on Windows #989

Merged
merged 3 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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()
igordmn marked this conversation as resolved.
Show resolved Hide resolved
else -> defaultKeyMapping
else -> defaultSkikoKeyMapping
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we just modify defaultKeyMapping in common?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MatkovIvan I'd ve glad to I just thought that this is less recommended because we need to merge back and to coordinate any changes - no matter how small - with google team.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the code is platform specific, better to keep it in our source set only.

If the code isn't platform specific - better to change it in commonMain and upstream.

Key mapping usually is platform specific, even if we have some default. We can't tell if it is needed for Android. defaultKeyMapping was added only because we didn't want to investigate platform behaviours.

}
}

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