diff --git a/briar-desktop/src/automatedScreenshots/kotlin/org/briarproject/briar/desktop/ScreenshotTest.kt b/briar-desktop/src/automatedScreenshots/kotlin/org/briarproject/briar/desktop/ScreenshotTest.kt index 2deeab1cd8be2a92dfae507872081dfeed8ea7b8..5871785d32033032013fd64d9b5d3ad5d346242f 100644 --- a/briar-desktop/src/automatedScreenshots/kotlin/org/briarproject/briar/desktop/ScreenshotTest.kt +++ b/briar-desktop/src/automatedScreenshots/kotlin/org/briarproject/briar/desktop/ScreenshotTest.kt @@ -1,6 +1,6 @@ /* * Briar Desktop - * Copyright (C) 2021-2022 The Briar Project + * Copyright (C) 2021-2023 The Briar Project * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -42,9 +42,11 @@ import org.briarproject.bramble.api.plugin.LanTcpConstants import org.briarproject.briar.BriarCoreEagerSingletons import org.briarproject.briar.desktop.TestUtils.getDataDir import org.briarproject.briar.desktop.contact.ContactListViewModel -import org.briarproject.briar.desktop.ui.LocalWindowFocusState -import org.briarproject.briar.desktop.ui.LocalWindowScope -import org.briarproject.briar.desktop.ui.WindowFocusState +import org.briarproject.briar.desktop.forums.ForumViewModel +import org.briarproject.briar.desktop.navigation.SidebarViewModel +import org.briarproject.briar.desktop.ui.LocalWindowInfo +import org.briarproject.briar.desktop.ui.UiMode +import org.briarproject.briar.desktop.ui.WindowInfo import org.jetbrains.annotations.NonNls import org.jetbrains.skia.Image import org.junit.Test @@ -53,7 +55,7 @@ import java.io.FileOutputStream @OptIn(ExperimentalTestApi::class) class ScreenshotTest { - private fun DesktopComposeUiTest.start(): BriarDesktopTestApp { + private fun DesktopComposeUiTest.start(windowWidth: Int): BriarDesktopTestApp { // TODO: unify with interactive tests val dataDir = getDataDir() val app = @@ -70,16 +72,19 @@ class ScreenshotTest { BrambleCoreEagerSingletons.Helper.injectEagerSingletons(app) BriarCoreEagerSingletons.Helper.injectEagerSingletons(app) - val windowScope = object : FrameWindowScope { - // would be needed for launching dialogs like the image picker - override val window: ComposeWindow get() = TODO() + val windowInfo = object : WindowInfo { + override val windowScope = object : FrameWindowScope { + override val window: ComposeWindow + // would be needed for launching dialogs like the image picker + get() = error("no ComposeWindow available in automatedScreenshots") + } + override val windowWidth: Int = windowWidth + override var focused: Boolean = true } - val windowFocusState = WindowFocusState().apply { focused = true } setContent { CompositionLocalProvider( - LocalWindowScope provides windowScope, - LocalWindowFocusState provides windowFocusState, + LocalWindowInfo provides windowInfo, ) { app.getBriarUi().content() } @@ -105,7 +110,7 @@ class ScreenshotTest { @Test fun makeScreenshot() = runDesktopComposeUiTest(700, 700) { runBlocking { - val app = start() + val app = start(windowWidth = 700) // close expiration banner and move mouse outside of window (to avoid hover effect) // TODO: *sometimes* leads to Compose crash (somehow connected to Tooltip?) @@ -130,22 +135,38 @@ class ScreenshotTest { "Faythe" ) - val viewModel = getViewModelProvider().get(ContactListViewModel::class) + val sidebarViewModel = getViewModelProvider().get(SidebarViewModel::class) + val contactListViewModel = getViewModelProvider().get(ContactListViewModel::class) + val forumViewModel = getViewModelProvider().get(ForumViewModel::class) // give IO executor some time to add contacts and messages delay(10_000) - waitUntil(60_000) { viewModel.combinedContactList.value.size > 2 } + waitUntil(60_000) { contactListViewModel.combinedContactList.value.size >= 2 } // select Bob in list of contacts and wait for the chat history to load - viewModel.selectContact(viewModel.combinedContactList.value[1]) + contactListViewModel.selectContact(contactListViewModel.combinedContactList.value[1]) + awaitIdle() + + // contact list screenshot + captureToImage().saveAsScreenshot("contact-list") + + sidebarViewModel.setUiMode(UiMode.FORUMS) + waitUntil(60_000) { forumViewModel.forumList.value.size >= 2 } + + // select the second forum in the list and wait for the forum posts to load + forumViewModel.selectGroup(forumViewModel.forumList.value[1]) awaitIdle() - captureToImage().save("contact-list.png") + // forum list screenshot + captureToImage().saveAsScreenshot("forum-list") } } } } +private fun Image.saveAsScreenshot(name: String) = + save("../utils/screenshots/$name.png") + private fun Image.save(file: String) { encodeToData()?.bytes?.let { bytes -> FileOutputStream(file).use { out -> diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationInput.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationInput.kt index 6c11e914aafe51f517ae0c29db65720adb910a59..3fe54c14d46c0e09c0d26a63f7799d2e3b232455 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationInput.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationInput.kt @@ -1,6 +1,6 @@ /* * Briar Desktop - * Copyright (C) 2021-2022 The Briar Project + * Copyright (C) 2021-2023 The Briar Project * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -53,7 +53,7 @@ import androidx.compose.ui.unit.dp import org.briarproject.briar.desktop.theme.sendButton import org.briarproject.briar.desktop.ui.ColoredIconButton import org.briarproject.briar.desktop.ui.HorizontalDivider -import org.briarproject.briar.desktop.ui.LocalWindowScope +import org.briarproject.briar.desktop.ui.getWindowScope import org.briarproject.briar.desktop.utils.ImagePicker.pickImageUsingDialog import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n import org.briarproject.briar.desktop.utils.PreviewUtils.preview @@ -112,7 +112,7 @@ fun ConversationInput( unfocusedIndicatorColor = MaterialTheme.colors.background ), leadingIcon = { - val windowScope = LocalWindowScope.current!! + val windowScope = getWindowScope() ColoredIconButton( icon = if (image == null) Icons.Filled.Add else Icons.Filled.Close, contentDescription = if (image == null) i18n("access.attachment_add") else i18n("access.attachment_remove"), diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadItemView.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadItemView.kt index 64cc1ec225e0b2ee8ca12e0b909308266c98cd94..6b143789d309dc413dbb1eaebd9d258274a21244 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadItemView.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadItemView.kt @@ -58,10 +58,10 @@ import org.briarproject.briar.desktop.theme.Blue500 import org.briarproject.briar.desktop.theme.divider import org.briarproject.briar.desktop.ui.Constants.COLUMN_WIDTH import org.briarproject.briar.desktop.ui.HorizontalDivider -import org.briarproject.briar.desktop.ui.LocalWindowScope import org.briarproject.briar.desktop.ui.Tooltip import org.briarproject.briar.desktop.ui.TrustIndicatorShort import org.briarproject.briar.desktop.ui.VerticalDivider +import org.briarproject.briar.desktop.ui.getWindowWidth import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n import org.briarproject.briar.desktop.utils.PreviewUtils.preview import org.briarproject.briar.desktop.utils.TimeUtils.getFormattedTimestamp @@ -221,7 +221,7 @@ fun NestingLevelView( } @Composable -fun getMaxNestingLevel(): Int = when (LocalWindowScope.current!!.window.width.dp) { +fun getMaxNestingLevel(): Int = when (getWindowWidth()) { in (0.dp..COLUMN_WIDTH * 2) -> 5 in (COLUMN_WIDTH * 2..COLUMN_WIDTH * 3) -> 10 in (COLUMN_WIDTH * 2..COLUMN_WIDTH * 4) -> 25 diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt index 9824f142a92feedcaa3bc8910976018501774fc5..f8d2af51b3634d04b186ed861c15d5da8900914c 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt @@ -28,7 +28,6 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier @@ -37,7 +36,6 @@ import androidx.compose.ui.graphics.toAwtImage import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.painterResource -import androidx.compose.ui.window.FrameWindowScope import androidx.compose.ui.window.Window import org.briarproject.bramble.api.event.EventBus import org.briarproject.bramble.api.event.EventListener @@ -88,7 +86,6 @@ interface BriarUi { fun content() } -val LocalWindowScope = staticCompositionLocalOf<FrameWindowScope?> { null } val LocalViewModelProvider = staticCompositionLocalOf<ViewModelProvider?> { null } val LocalAvatarManager = staticCompositionLocalOf<AvatarManager?> { null } val LocalConfiguration = staticCompositionLocalOf<Configuration?> { null } @@ -122,12 +119,12 @@ constructor( @Composable override fun start(onClose: () -> Unit) { - val focusState = remember { WindowFocusState() } - Window( title = Strings.APP_NAME, onCloseRequest = onClose, ) { + val windowInfo = rememberWindowInfo(this) + // changing the icon in the Composable itself automatically brings the window to front // see https://github.com/JetBrains/compose-jb/issues/1861 // therefore the icon is set here on the AWT Window @@ -144,19 +141,19 @@ constructor( val focusListener = object : WindowFocusListener { override fun windowGainedFocus(e: WindowEvent?) { - focusState.focused = true + windowInfo.focused = true window.iconImage = iconNormal } override fun windowLostFocus(e: WindowEvent?) { - focusState.focused = false + windowInfo.focused = false // reset notification cool-down lastNotificationPrivateMessage = 0 lastNotificationForum = 0 } } val messageCounterListener: MessageCounterListener = { (type, total, groups, inc) -> - if (inc && total > 0 && !focusState.focused) { + if (inc && total > 0 && !windowInfo.focused) { val callback: NotificationProvider.() -> Unit = when (type) { PrivateMessage -> { { notifyPrivateMessages(total, groups) } @@ -200,8 +197,7 @@ constructor( } CompositionLocalProvider( - LocalWindowScope provides this, - LocalWindowFocusState provides focusState, + LocalWindowInfo provides windowInfo, ) { // invalidate whole application window in case the theme, language or UI scale // setting is changed diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/WindowFocusState.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/WindowFocusState.kt deleted file mode 100644 index b16c3694adbae82f0d490f580f4caba3f61503aa..0000000000000000000000000000000000000000 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/WindowFocusState.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Briar Desktop - * Copyright (C) 2021-2022 The Briar Project - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package org.briarproject.briar.desktop.ui - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.compose.runtime.staticCompositionLocalOf - -class WindowFocusState { - var focused by mutableStateOf(false) -} - -val LocalWindowFocusState = staticCompositionLocalOf<WindowFocusState?> { null } - -@Composable -fun isWindowFocused() = checkNotNull(LocalWindowFocusState.current) { - "No WindowFocusState was provided via LocalWindowFocusState" // NON-NLS -}.focused diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/WindowInfo.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/WindowInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..e0ed05f2e8255b9d6a4d1938905ab5dfaadb7a44 --- /dev/null +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/WindowInfo.kt @@ -0,0 +1,61 @@ +/* + * Briar Desktop + * Copyright (C) 2021-2023 The Briar Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package org.briarproject.briar.desktop.ui + +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.runtime.staticCompositionLocalOf +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.window.FrameWindowScope + +interface WindowInfo { + val windowScope: FrameWindowScope + val windowWidth: Int + var focused: Boolean +} + +@Composable +fun rememberWindowInfo(windowScope: FrameWindowScope): WindowInfo = + remember { WindowInfoImpl(windowScope) } + +private class WindowInfoImpl( + override val windowScope: FrameWindowScope, + override val windowWidth: Int = windowScope.window.width, +) : WindowInfo { + override var focused by mutableStateOf(false) +} + +val LocalWindowInfo = staticCompositionLocalOf<WindowInfo?> { null } + +@Composable +private fun getWindowInfo() = checkNotNull(LocalWindowInfo.current) { + "No WindowInfo was provided via LocalWindowFocusState" // NON-NLS +} + +@Composable +fun isWindowFocused() = getWindowInfo().focused + +@Composable +fun getWindowScope() = getWindowInfo().windowScope + +@Composable +fun getWindowWidth() = with(LocalDensity.current) { getWindowInfo().windowWidth.toDp() } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/PreviewUtils.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/PreviewUtils.kt index 60c4ffed5d9e9ebc9c5f27bd5fc3b890504ae123..bc1a00fc8c4b04ede810ed9ad5cf0481c30dfc84 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/PreviewUtils.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/PreviewUtils.kt @@ -54,9 +54,8 @@ import androidx.compose.ui.window.singleWindowApplication import org.briarproject.bramble.api.UniqueId import org.briarproject.briar.desktop.settings.UnencryptedSettings import org.briarproject.briar.desktop.theme.BriarTheme -import org.briarproject.briar.desktop.ui.LocalWindowFocusState -import org.briarproject.briar.desktop.ui.LocalWindowScope -import org.briarproject.briar.desktop.ui.WindowFocusState +import org.briarproject.briar.desktop.ui.LocalWindowInfo +import org.briarproject.briar.desktop.ui.rememberWindowInfo import org.briarproject.briar.desktop.utils.UiUtils.DensityDimension import org.briarproject.briar.desktop.viewmodel.SingleStateEvent import org.jetbrains.annotations.NonNls @@ -217,10 +216,9 @@ object PreviewUtils { val settingsDensity: Float? = prefs.get("previewsUiScale", null)?.toFloat() singleWindowApplication(title = "Interactive Preview") { - val focusState = remember { WindowFocusState() } + val windowInfo = rememberWindowInfo(this) CompositionLocalProvider( - LocalWindowScope provides this, - LocalWindowFocusState provides focusState + LocalWindowInfo provides windowInfo ) { Column { val density = settingsDensity ?: LocalDensity.current.density