Merge "Add an API to check if the window is in focus" into androidx-master-dev
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt
index 49a4317..378ab68 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt
@@ -76,6 +76,7 @@
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.Uptime
import androidx.compose.ui.unit.milliseconds
+import androidx.compose.ui.platform.WindowManager
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.test.runBlockingTest
@@ -405,6 +406,8 @@
get() = TODO("Not yet implemented")
override val focusManager: FocusManager
get() = TODO("Not yet implemented")
+ override val windowManager: WindowManager
+ get() = TODO("Not yet implemented")
override val fontLoader: Font.ResourceLoader
get() = TODO("Not yet implemented")
override val layoutDirection: LayoutDirection
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index fb53e39..1288834 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -2027,6 +2027,7 @@
method public androidx.compose.ui.text.input.TextInputService getTextInputService();
method public androidx.compose.ui.platform.TextToolbar getTextToolbar();
method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
+ method public androidx.compose.ui.platform.WindowManager getWindowManager();
method public void measureAndLayout();
method public void onAttach(androidx.compose.ui.node.LayoutNode node);
method public void onDetach(androidx.compose.ui.node.LayoutNode node);
@@ -2052,6 +2053,7 @@
property public abstract androidx.compose.ui.text.input.TextInputService textInputService;
property public abstract androidx.compose.ui.platform.TextToolbar textToolbar;
property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
+ property public abstract androidx.compose.ui.platform.WindowManager windowManager;
field public static final androidx.compose.ui.node.Owner.Companion Companion;
}
@@ -2122,6 +2124,7 @@
method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.TextToolbar> getAmbientTextToolbar();
method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.UriHandler> getAmbientUriHandler();
method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ViewConfiguration> getAmbientViewConfiguration();
+ method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.WindowManager> getAmbientWindowManager();
method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.animation.core.AnimationClockObservable>! getAnimationClockAmbient();
method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.autofill.Autofill>! getAutofillAmbient();
method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.autofill.AutofillTree>! getAutofillTreeAmbient();
@@ -2314,6 +2317,15 @@
property public abstract float touchSlop;
}
+ @androidx.compose.runtime.Stable public interface WindowManager {
+ method public boolean isWindowFocused();
+ property public abstract boolean isWindowFocused;
+ }
+
+ public final class WindowManagerKt {
+ method @androidx.compose.runtime.Composable public static void WindowFocusObserver(kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onWindowFocusChanged);
+ }
+
public final class WrapperKt {
method public static androidx.compose.runtime.Composition setContent(androidx.activity.ComponentActivity, optional androidx.compose.runtime.CompositionReference parent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method public static androidx.compose.runtime.Composition setContent(android.view.ViewGroup, optional androidx.compose.runtime.CompositionReference parent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index fb53e39..1288834 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -2027,6 +2027,7 @@
method public androidx.compose.ui.text.input.TextInputService getTextInputService();
method public androidx.compose.ui.platform.TextToolbar getTextToolbar();
method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
+ method public androidx.compose.ui.platform.WindowManager getWindowManager();
method public void measureAndLayout();
method public void onAttach(androidx.compose.ui.node.LayoutNode node);
method public void onDetach(androidx.compose.ui.node.LayoutNode node);
@@ -2052,6 +2053,7 @@
property public abstract androidx.compose.ui.text.input.TextInputService textInputService;
property public abstract androidx.compose.ui.platform.TextToolbar textToolbar;
property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
+ property public abstract androidx.compose.ui.platform.WindowManager windowManager;
field public static final androidx.compose.ui.node.Owner.Companion Companion;
}
@@ -2122,6 +2124,7 @@
method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.TextToolbar> getAmbientTextToolbar();
method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.UriHandler> getAmbientUriHandler();
method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ViewConfiguration> getAmbientViewConfiguration();
+ method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.WindowManager> getAmbientWindowManager();
method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.animation.core.AnimationClockObservable>! getAnimationClockAmbient();
method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.autofill.Autofill>! getAutofillAmbient();
method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.autofill.AutofillTree>! getAutofillTreeAmbient();
@@ -2314,6 +2317,15 @@
property public abstract float touchSlop;
}
+ @androidx.compose.runtime.Stable public interface WindowManager {
+ method public boolean isWindowFocused();
+ property public abstract boolean isWindowFocused;
+ }
+
+ public final class WindowManagerKt {
+ method @androidx.compose.runtime.Composable public static void WindowFocusObserver(kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onWindowFocusChanged);
+ }
+
public final class WrapperKt {
method public static androidx.compose.runtime.Composition setContent(androidx.activity.ComponentActivity, optional androidx.compose.runtime.CompositionReference parent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method public static androidx.compose.runtime.Composition setContent(android.view.ViewGroup, optional androidx.compose.runtime.CompositionReference parent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 5f72ebc..4ecf54f 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -2089,6 +2089,7 @@
method public androidx.compose.ui.text.input.TextInputService getTextInputService();
method public androidx.compose.ui.platform.TextToolbar getTextToolbar();
method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
+ method public androidx.compose.ui.platform.WindowManager getWindowManager();
method public void measureAndLayout();
method public void onAttach(androidx.compose.ui.node.LayoutNode node);
method public void onDetach(androidx.compose.ui.node.LayoutNode node);
@@ -2114,6 +2115,7 @@
property public abstract androidx.compose.ui.text.input.TextInputService textInputService;
property public abstract androidx.compose.ui.platform.TextToolbar textToolbar;
property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
+ property public abstract androidx.compose.ui.platform.WindowManager windowManager;
field public static final androidx.compose.ui.node.Owner.Companion Companion;
}
@@ -2184,6 +2186,7 @@
method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.TextToolbar> getAmbientTextToolbar();
method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.UriHandler> getAmbientUriHandler();
method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ViewConfiguration> getAmbientViewConfiguration();
+ method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.WindowManager> getAmbientWindowManager();
method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.animation.core.AnimationClockObservable>! getAnimationClockAmbient();
method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.autofill.Autofill>! getAutofillAmbient();
method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.autofill.AutofillTree>! getAutofillTreeAmbient();
@@ -2376,6 +2379,15 @@
property public abstract float touchSlop;
}
+ @androidx.compose.runtime.Stable public interface WindowManager {
+ method public boolean isWindowFocused();
+ property public abstract boolean isWindowFocused;
+ }
+
+ public final class WindowManagerKt {
+ method @androidx.compose.runtime.Composable public static void WindowFocusObserver(kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onWindowFocusChanged);
+ }
+
public final class WrapperKt {
method public static androidx.compose.runtime.Composition setContent(androidx.activity.ComponentActivity, optional androidx.compose.runtime.CompositionReference parent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method public static androidx.compose.runtime.Composition setContent(android.view.ViewGroup, optional androidx.compose.runtime.CompositionReference parent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
index e38c20a..021cf64 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
@@ -43,6 +43,8 @@
import androidx.compose.ui.demos.viewinterop.ViewInteropDemo
import androidx.compose.integration.demos.common.ComposableDemo
import androidx.compose.integration.demos.common.DemoCategory
+import androidx.compose.ui.demos.focus.FocusInDialog
+import androidx.compose.ui.demos.focus.FocusInPopup
private val GestureDemos = DemoCategory(
"Gestures",
@@ -93,6 +95,8 @@
"Focus",
listOf(
ComposableDemo("Focusable Siblings") { FocusableDemo() },
+ ComposableDemo("Focus Within Dialog") { FocusInDialog() },
+ ComposableDemo("Focus Within Popup") { FocusInPopup() },
ComposableDemo("Reuse Focus Requester") { ReuseFocusRequester() }
)
)
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusInDialog.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusInDialog.kt
new file mode 100644
index 0000000..2769825
--- /dev/null
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusInDialog.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.demos.focus
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.material.Button
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color.Companion.LightGray
+import androidx.compose.ui.graphics.Color.Companion.White
+import androidx.compose.ui.platform.AmbientWindowManager
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+
+@Composable
+fun FocusInDialog() {
+ var showDialog by remember { mutableStateOf(false) }
+ var mainText by remember { mutableStateOf(TextFieldValue("Enter Value")) }
+ var dialogText by remember { mutableStateOf(TextFieldValue("Enter Value")) }
+ val windowManager = AmbientWindowManager.current
+
+ Column(Modifier.background(if (windowManager.isWindowFocused) White else LightGray)) {
+ Text("Click the button to show the dialog. Click outside the dialog to dismiss it.")
+ Spacer(Modifier.height(10.dp))
+ Button(onClick = { showDialog = true }) {
+ Text("Show Dialog")
+ }
+
+ Spacer(Modifier.height(50.dp))
+
+ Text("Click this text field to bring the main app in focus.")
+ TextField(value = mainText, onValueChange = { mainText = it })
+ FocusStatus()
+
+ if (showDialog) {
+ Dialog(onDismissRequest = { showDialog = false }) {
+ Column(Modifier.background(White)) {
+ Text("Click this text field to bring the dialog in focus")
+ TextField(value = dialogText, onValueChange = { dialogText = it })
+ FocusStatus()
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun FocusStatus() {
+ val windowManager = AmbientWindowManager.current
+ Text("Status: Window ${if (windowManager.isWindowFocused) "is" else "is not"} focused.")
+}
\ No newline at end of file
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusInPopup.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusInPopup.kt
new file mode 100644
index 0000000..e040b3c
--- /dev/null
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusInPopup.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.demos.focus
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.material.Button
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
+import androidx.compose.runtime.Composable
+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.Modifier
+import androidx.compose.ui.graphics.Color.Companion.LightGray
+import androidx.compose.ui.graphics.Color.Companion.White
+import androidx.compose.ui.platform.AmbientWindowManager
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Popup
+
+@Composable
+fun FocusInPopup() {
+ var showPopup by remember { mutableStateOf(false) }
+ var mainText by remember { mutableStateOf(TextFieldValue("Enter Value")) }
+ var popupText by remember { mutableStateOf(TextFieldValue("Enter Value")) }
+ val windowManager = AmbientWindowManager.current
+
+ Column(Modifier.background(if (windowManager.isWindowFocused) White else LightGray)) {
+ Text("Click the button to show the popup. Click outside the popup to dismiss it.")
+ Spacer(Modifier.height(10.dp))
+ Button(onClick = { showPopup = true }) {
+ Text("Show Popup")
+ }
+
+ Spacer(Modifier.height(50.dp))
+
+ Text("Click this text field to bring the main app in focus.")
+ TextField(value = mainText, onValueChange = { mainText = it })
+ FocusStatus()
+
+ if (showPopup) {
+ Popup(
+ alignment = Alignment.Center,
+ isFocusable = true,
+ onDismissRequest = { showPopup = false }
+ ) {
+ Column(Modifier.background(White)) {
+ Text("Click this text field to bring the popup in focus")
+ TextField(value = popupText, onValueChange = { popupText = it })
+ FocusStatus()
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun FocusStatus() {
+ val windowManager = AmbientWindowManager.current
+ Text("Status: Window ${if (windowManager.isWindowFocused) "is" else "is not"} focused.")
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
index 1ac3201..05c2735 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
@@ -50,6 +50,7 @@
import androidx.compose.ui.unit.Uptime
import androidx.compose.ui.unit.milliseconds
import androidx.compose.ui.unit.minus
+import androidx.compose.ui.platform.WindowManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
@@ -3154,6 +3155,8 @@
get() = TODO("Not yet implemented")
override val focusManager: FocusManager
get() = TODO("Not yet implemented")
+ override val windowManager: WindowManager
+ get() = TODO("Not yet implemented")
override val fontLoader: Font.ResourceLoader
get() = TODO("Not yet implemented")
override val layoutDirection: LayoutDirection
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowManagerAmbientTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowManagerAmbientTest.kt
new file mode 100644
index 0000000..b086688
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowManagerAmbientTest.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.platform
+
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.focus.ExperimentalFocus
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.Popup
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit.SECONDS
+
+@MediumTest
+@OptIn(ExperimentalFocus::class)
+@RunWith(AndroidJUnit4::class)
+class WindowManagerAmbientTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Ignore("Flaky Test b/173088588")
+ @Test
+ fun windowIsFocused_onLaunch() {
+ // Arrange.
+ lateinit var windowManager: WindowManager
+ val windowFocusGain = CountDownLatch(1)
+ rule.setContent {
+ BasicText("Main Window")
+ windowManager = AmbientWindowManager.current
+ WindowFocusObserver { if (it) windowFocusGain.countDown() }
+ }
+
+ // Act.
+ rule.waitForIdle()
+
+ // Assert.
+ windowFocusGain.await(5, SECONDS)
+ assertThat( windowManager.isWindowFocused ).isTrue()
+ }
+
+ @Test
+ fun mainWindowIsNotFocused_whenPopupIsVisible() {
+ // Arrange.
+ lateinit var mainWindowManager: WindowManager
+ lateinit var popupWindowManager: WindowManager
+ val mainWindowFocusLoss = CountDownLatch(1)
+ val popupFocusGain = CountDownLatch(1)
+ val showPopup = mutableStateOf(false)
+ rule.setContent {
+ BasicText("Main Window")
+ mainWindowManager = AmbientWindowManager.current
+ WindowFocusObserver { if (!it) mainWindowFocusLoss.countDown() }
+ if (showPopup.value) {
+ Popup(isFocusable = true, onDismissRequest = { showPopup.value = false }) {
+ BasicText("Popup Window")
+ popupWindowManager = AmbientWindowManager.current
+ WindowFocusObserver { if (it) popupFocusGain.countDown() }
+ }
+ }
+ }
+
+ // Act.
+ rule.runOnIdle { showPopup.value = true }
+
+ // Assert.
+ rule.waitForIdle()
+ assertThat(mainWindowFocusLoss.await(5, SECONDS)).isTrue()
+ assertThat(popupFocusGain.await(5, SECONDS)).isTrue()
+ assertThat( mainWindowManager.isWindowFocused ).isFalse()
+ assertThat( popupWindowManager.isWindowFocused ).isTrue()
+ }
+
+ @Test
+ fun windowIsFocused_whenPopupIsDismissed() {
+ // Arrange.
+ lateinit var mainWindowManager: WindowManager
+ var mainWindowFocusGain = CountDownLatch(1)
+ val popupFocusGain = CountDownLatch(1)
+ val showPopup = mutableStateOf(false)
+ rule.setContent {
+ BasicText(text = "Main Window")
+ mainWindowManager = AmbientWindowManager.current
+ WindowFocusObserver { if (it) mainWindowFocusGain.countDown() }
+ if (showPopup.value) {
+ Popup(isFocusable = true, onDismissRequest = { showPopup.value = false }) {
+ BasicText(text = "Popup Window")
+ WindowFocusObserver { if (it) popupFocusGain.countDown() }
+ }
+ }
+ }
+ rule.runOnIdle { showPopup.value = true }
+ rule.waitForIdle()
+ assertThat(popupFocusGain.await(5, SECONDS)).isTrue()
+ mainWindowFocusGain = CountDownLatch(1)
+
+ // Act.
+ rule.runOnIdle { showPopup.value = false }
+
+ // Assert.
+ rule.waitForIdle()
+ assertThat(mainWindowFocusGain.await(5, SECONDS)).isTrue()
+ assertThat(mainWindowManager.isWindowFocused).isTrue()
+ }
+
+ @Test
+ fun mainWindowIsNotFocused_whenDialogIsVisible() {
+ // Arrange.
+ lateinit var mainWindowManager: WindowManager
+ lateinit var dialogWindowManager: WindowManager
+ val mainWindowFocusLoss = CountDownLatch(1)
+ val dialogFocusGain = CountDownLatch(1)
+ val showDialog = mutableStateOf(false)
+ rule.setContent {
+ BasicText("Main Window")
+ mainWindowManager = AmbientWindowManager.current
+ WindowFocusObserver { if (!it) mainWindowFocusLoss.countDown() }
+ if (showDialog.value) {
+ Dialog(onDismissRequest = { showDialog.value = false }) {
+ BasicText("Popup Window")
+ dialogWindowManager = AmbientWindowManager.current
+ WindowFocusObserver { if (it) dialogFocusGain.countDown() }
+ }
+ }
+ }
+
+ // Act.
+ rule.runOnIdle { showDialog.value = true }
+
+ // Assert.
+ rule.waitForIdle()
+ assertThat(mainWindowFocusLoss.await(5, SECONDS)).isTrue()
+ assertThat(dialogFocusGain.await(5, SECONDS)).isTrue()
+ assertThat(mainWindowManager.isWindowFocused).isFalse()
+ assertThat(dialogWindowManager.isWindowFocused).isTrue()
+ }
+
+ @Test
+ fun windowIsFocused_whenDialogIsDismissed() {
+ // Arrange.
+ lateinit var mainWindowManager: WindowManager
+ var mainWindowFocusGain = CountDownLatch(1)
+ val dialogFocusGain = CountDownLatch(1)
+ val showDialog = mutableStateOf(false)
+ rule.setContent {
+ BasicText(text = "Main Window")
+ mainWindowManager = AmbientWindowManager.current
+ WindowFocusObserver { if (it) mainWindowFocusGain.countDown() }
+ if (showDialog.value) {
+ Dialog(onDismissRequest = { showDialog.value = false }) {
+ BasicText(text = "Popup Window")
+ WindowFocusObserver { if (it) dialogFocusGain.countDown() }
+ }
+ }
+ }
+ rule.runOnIdle { showDialog.value = true }
+ rule.waitForIdle()
+ assertThat(dialogFocusGain.await(5, SECONDS)).isTrue()
+ mainWindowFocusGain = CountDownLatch(1)
+
+ // Act.
+ rule.runOnIdle { showDialog.value = false }
+
+ // Assert.
+ rule.waitForIdle()
+ assertThat(mainWindowFocusGain.await(5, SECONDS)).isTrue()
+ assertThat(mainWindowManager.isWindowFocused).isTrue()
+ }
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt
index 6805cc6..3466b37 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt
@@ -114,6 +114,10 @@
override val focusManager: FocusManager
get() = _focusManager
+ private val _windowManager: WindowManagerImpl = WindowManagerImpl()
+ override val windowManager: WindowManager
+ get() = _windowManager
+
private val keyInputModifier = KeyInputModifier(null, null)
private val canvasHolder = CanvasHolder()
@@ -294,6 +298,11 @@
}
}
+ override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
+ _windowManager.isWindowFocused = hasWindowFocus
+ super.onWindowFocusChanged(hasWindowFocus)
+ }
+
override fun sendKeyEvent(keyEvent: KeyEvent): Boolean {
return keyInputModifier.processKeyInput(keyEvent)
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
index 5f9067a..f5107fe 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
@@ -32,6 +32,7 @@
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.platform.WindowManager
/**
* Owner implements the connection to the underlying view system. On Android, this connects
@@ -86,6 +87,11 @@
@ExperimentalFocus
val focusManager: FocusManager
+ /**
+ * Provide information about the window that hosts this [Owner].
+ */
+ val windowManager: WindowManager
+
val fontLoader: Font.ResourceLoader
val layoutDirection: LayoutDirection
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Ambients.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Ambients.kt
index 277b3de..bfbb003 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Ambients.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Ambients.kt
@@ -287,6 +287,11 @@
*/
val AmbientViewConfiguration = staticAmbientOf<ViewConfiguration>()
+/**
+ * The ambient that provides information about the window that hosts the current [Owner].
+ */
+val AmbientWindowManager = staticAmbientOf<WindowManager>()
+
@OptIn(ExperimentalFocus::class)
@Composable
internal fun ProvideCommonAmbients(
@@ -309,6 +314,7 @@
AmbientTextToolbar provides owner.textToolbar,
AmbientUriHandler provides uriHandler,
AmbientViewConfiguration provides owner.viewConfiguration,
+ AmbientWindowManager provides owner.windowManager,
content = content
)
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/WindowManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/WindowManager.kt
new file mode 100644
index 0000000..0c20424
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/WindowManager.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.platform
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ExperimentalComposeApi
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.snapshots.snapshotFlow
+import kotlinx.coroutines.InternalCoroutinesApi
+import kotlinx.coroutines.flow.collect
+
+/**
+ * Provides information about the Window that is hosting this compose hierarchy.
+ */
+@Stable
+interface WindowManager {
+ /**
+ * Indicates whether the window hosting this compose hierarchy is in focus.
+ *
+ * When there are multiple windows visible, either in a multi-window environment or if a
+ * popup or dialog is visible, this property can be used to determine if the current window
+ * is in focus.
+ */
+ val isWindowFocused: Boolean
+}
+
+/**
+ * Provides a callback that is called whenever the window gains or loses focus.
+ */
+@OptIn(
+ ExperimentalComposeApi::class,
+ InternalCoroutinesApi::class
+)
+@Composable
+fun WindowFocusObserver(onWindowFocusChanged: (isWindowFocused: Boolean) -> Unit) {
+ val windowManager = AmbientWindowManager.current
+ val callback = rememberUpdatedState(onWindowFocusChanged)
+ LaunchedEffect(windowManager) {
+ snapshotFlow { windowManager.isWindowFocused }.collect { callback.value(it) }
+ }
+}
+
+internal class WindowManagerImpl : WindowManager {
+ private val _isWindowFocused = mutableStateOf(false)
+ override var isWindowFocused: Boolean
+ set(value) { _isWindowFocused.value = value }
+ get() = _isWindowFocused.value
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt
index 61d96f6..c499c3c 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt
@@ -87,6 +87,11 @@
override val focusManager: FocusManager
get() = _focusManager
+ // TODO: set/clear _windowManager.isWindowFocused when the window gains/loses focus.
+ private val _windowManager: WindowManagerImpl = WindowManagerImpl()
+ override val windowManager: WindowManager
+ get() = _windowManager
+
private val keyInputModifier = KeyInputModifier(null, null)
override val root = LayoutNode().also {
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index 2f30912..9f7b77ab 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -50,6 +50,7 @@
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.platform.WindowManager
import androidx.compose.ui.zIndex
import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.anyOrNull
@@ -1708,6 +1709,8 @@
get() = TODO("Not yet implemented")
override val focusManager: FocusManager
get() = TODO("Not yet implemented")
+ override val windowManager: WindowManager
+ get() = TODO("Not yet implemented")
override val fontLoader: Font.ResourceLoader
get() = TODO("Not yet implemented")
override val layoutDirection: LayoutDirection