Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
Expand All @@ -52,6 +51,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.Modifier
Expand Down Expand Up @@ -866,38 +866,38 @@ class ScrollbarTest {
rule.setContent {
// Set up a text field that is exactly 10 lines tall, with text that has 20 lines,
// Scrollbar thumb should be 50.dp -- half the scrollbar height
Row{
Box(
modifier = Modifier.width(100.dp)
){
var text by remember {
mutableStateOf(
TextFieldValue(
buildString {
repeat(19) { // 20 lines including the last empty one
append("A\n")
}
Box(
modifier = Modifier.width(100.dp)
) {
var text by remember {
mutableStateOf(
TextFieldValue(
buildString {
repeat(19) { // 20 lines including the last empty one
append("A\n")
}
)
}
)
}
BasicTextField(
value = text,
onValueChange = {
text = it
},
scrollState = scrollState,
maxLines = 10, // Make sure not to give the text field any pixel height
modifier = Modifier
.testTag("textfield"),
)
}
BasicTextField(
value = text,
onValueChange = {
text = it
},
scrollState = scrollState,
maxLines = 10, // Make sure not to give the text field any pixel height
modifier = Modifier
.fillMaxWidth()
.testTag("textfield"),
)

VerticalScrollbar(
adapter = rememberScrollbarAdapter(scrollState),
modifier = Modifier
.width(10.dp)
.height(100.dp)
.align(Alignment.CenterEnd)
.testTag("scrollbar")
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,23 @@ import androidx.compose.foundation.interaction.FocusInteraction
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.RecomposeScope
import androidx.compose.runtime.currentRecomposeScope
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.input.key.Key
Expand All @@ -39,10 +45,14 @@ import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertIsFocused
import androidx.compose.ui.test.assertIsNotFocused
import androidx.compose.ui.test.click
import androidx.compose.ui.test.isFocused
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performKeyInput
import androidx.compose.ui.test.performMouseInput
import androidx.compose.ui.test.pressKey
import androidx.compose.ui.test.requestFocus
import androidx.compose.ui.test.runComposeUiTest
Expand Down Expand Up @@ -260,4 +270,28 @@ class ClickableFocusTest {
override fun hashCode() = super.hashCode()
override fun equals(other: Any?) = super.equals(other)
}

@Test
fun mouseClickOutsideClearsFocus() = runComposeUiTest {
val focusRequester = FocusRequester()
setContent {
Column(Modifier.size(300.dp, 400.dp)) {
BasicTextField(
state = rememberTextFieldState(),
modifier = Modifier
.testTag("textField")
.focusRequester(focusRequester)
)
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
Box(Modifier.testTag("box").fillMaxWidth().weight(1f))
}
}

onNodeWithTag("textField").assertIsFocused()
onNodeWithTag("box").performMouseInput { click() }
onNodeWithTag("textField").assertIsNotFocused()
onNode(isFocused()).assertDoesNotExist()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,14 @@ class ComposeDialog : JDialog {
get() = composePanel.rootForTestListener
set(value) { composePanel.rootForTestListener = value }

/**
* Controls whether mouse-down on an unfocusable element clears focus.
*/
@ExperimentalComposeUiApi
var isClearFocusOnMouseDownEnabled: Boolean
get() = composePanel.isClearFocusOnMouseDownEnabled
set(value) { composePanel.isClearFocusOnMouseDownEnabled = value }

private val undecoratedWindowResizer = UndecoratedWindowResizer(this)

override fun add(component: Component) = composePanel.add(component)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ package androidx.compose.ui.awt

import androidx.compose.runtime.Composable
import androidx.compose.ui.ComposeFeatureFlags
import androidx.compose.ui.ComposeUiFlags
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.LayerType
import androidx.compose.ui.awt.RenderSettings.SkiaSurface
import androidx.compose.ui.awt.RenderSettings.SwingGraphics
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.isClearFocusOnMouseDownEnabled
import androidx.compose.ui.scene.ComposeContainer
import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.window.WindowExceptionHandler
Expand Down Expand Up @@ -120,6 +122,16 @@ class ComposePanel @ExperimentalComposeUiApi constructor(
private var _composeContainer: ComposeContainer? = null
private var _composeContent: (@Composable () -> Unit)? = null

/**
* Controls whether mouse-down on an unfocusable element clears focus.
*/
@ExperimentalComposeUiApi
var isClearFocusOnMouseDownEnabled: Boolean = ComposeUiFlags.isClearFocusOnMouseDownEnabled
set(value) {
field = value
_composeContainer?.isClearFocusOnMouseDownEnabled = value
}

/**
* Determines whether the Compose state in [ComposePanel] should be disposed
* when panel is detached from Swing hierarchy (when [removeNotify] is called).
Expand Down Expand Up @@ -279,6 +291,8 @@ class ComposePanel @ExperimentalComposeUiApi constructor(

override fun focusLost(e: FocusEvent) = Unit
})

isClearFocusOnMouseDownEnabled = this@ComposePanel.isClearFocusOnMouseDownEnabled
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ class ComposeWindow @ExperimentalComposeUiApi constructor(
val semanticsOwners: Collection<SemanticsOwner>
get() = composePanel.semanticsOwners

/**
* Controls whether mouse-down on an unfocusable element clears focus.
*/
@ExperimentalComposeUiApi
var isClearFocusOnMouseDownEnabled: Boolean by composePanel::isClearFocusOnMouseDownEnabled

init {
contentPane.add(composePanel)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ internal class ComposeWindowPanel(
val renderApi by composeContainer::renderApi
val semanticsOwners by composeContainer::semanticsOwners

var isClearFocusOnMouseDownEnabled: Boolean by composeContainer::isClearFocusOnMouseDownEnabled

var isWindowTransparent: Boolean = false
set(value) {
if (field != value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@ internal class ComposeContainer(
private var isMinimized = false
private var isFocused = false

var isClearFocusOnMouseDownEnabled by mediator::isClearFocusOnMouseDownEnabled

init {
architectureComponentsOwner.enableSavedStateHandles()
setWindow(window)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalContext
import androidx.compose.runtime.mutableStateSetOf
import androidx.compose.ui.ComposeFeatureFlags
import androidx.compose.ui.ComposeUiFlags
import androidx.compose.ui.awt.AwtEventListener
import androidx.compose.ui.awt.AwtEventListeners
import androidx.compose.ui.awt.DebouncingEdtExecutor
Expand All @@ -42,6 +43,7 @@ import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
import androidx.compose.ui.input.pointer.PointerType
import androidx.compose.ui.isClearFocusOnMouseDownEnabled
import androidx.compose.ui.navigationevent.BackNavigationEventInput
import androidx.compose.ui.platform.AwtDragAndDropManager
import androidx.compose.ui.platform.DefaultInputModeManager
Expand Down Expand Up @@ -367,6 +369,8 @@ internal class ComposeSceneMediator(

private val composeInvalidationExecutor = DebouncingEdtExecutor()

var isClearFocusOnMouseDownEnabled: Boolean = ComposeUiFlags.isClearFocusOnMouseDownEnabled

init {
// Transparency is used during redrawer creation that triggered by [addNotify], so
// it must be set to correct value before adding to the hierarchy to handle cases
Expand Down Expand Up @@ -828,6 +832,8 @@ internal class ComposeSceneMediator(
get() = this@ComposeSceneMediator.rootForTestListener
override val semanticsOwnerListener
get() = this@ComposeSceneMediator.semanticsOwnerListener
override val isClearFocusOnMouseDownEnabled: Boolean
get() = this@ComposeSceneMediator.isClearFocusOnMouseDownEnabled
}

private inner class DesktopPlatformComponent : PlatformComponent {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
Expand All @@ -52,9 +53,12 @@ import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.onPointerEvent
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.sendCharTypedEvents
import androidx.compose.ui.sendKeyEvent
import androidx.compose.ui.sendMouseEvent
import androidx.compose.ui.sendMousePress
import androidx.compose.ui.sendMouseRelease
import androidx.compose.ui.sendMouseWheelEvent
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize
Expand Down Expand Up @@ -809,4 +813,56 @@ class ComposePanelTest {
}
}
}

@Test
fun testComposePanelClearFocusOnMouseDownEnabled() =
testComposePanelClearFocusOnMouseDownEnabledFlag(true)

@Test
fun testComposePanelClearFocusOnMouseDownDisabled() =
testComposePanelClearFocusOnMouseDownEnabledFlag(false)

fun testComposePanelClearFocusOnMouseDownEnabledFlag(enabled: Boolean) = runApplicationTest {
val focusRequester = FocusRequester()
var textFieldIsFocused = false

val window = JFrame()
try {
window.contentPane.add(ComposePanel().apply {
isClearFocusOnMouseDownEnabled = enabled
setContent {
Column(Modifier.size(300.dp, 400.dp)) {
BasicTextField(
state = rememberTextFieldState(),
modifier = Modifier
.testTag("textField")
.fillMaxWidth()
.height(100.dp)
.focusRequester(focusRequester)
.onFocusChanged {
textFieldIsFocused = it.isFocused
}
)
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
Box(Modifier.testTag("box").fillMaxWidth().weight(1f))
}
}
})
window.size = Dimension(300, 400)
window.isVisible = true

awaitIdle()

assertThat(textFieldIsFocused).isTrue()
window.sendMousePress(x = 100, y = 300)
window.sendMouseRelease(x = 100, y = 300)
awaitIdle()

assertThat(textFieldIsFocused).isEqualTo(!enabled)
} finally {
window.dispose()
}
}
}
Loading