Skip to content

Commit ea460fa

Browse files
committed
feat(desktop): persistent window state
1 parent f66389d commit ea460fa

File tree

18 files changed

+432
-39
lines changed

18 files changed

+432
-39
lines changed

application/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ kotlin {
5454
implementation(catalog.voyager.navigator)
5555
implementation(catalog.voyager.transitions)
5656

57+
// lifecycle
58+
implementation(catalog.androidx.multplatform.lifecycle.runtime.compose)
59+
5760
// compose
5861
implementation(compose.runtime)
5962
implementation(compose.foundation)
@@ -62,6 +65,7 @@ kotlin {
6265
implementation(compose.materialIconsExtended)
6366
implementation(compose.components.resources)
6467

68+
// koin
6569
implementation(libs.koin.core)
6670
implementation(libs.koin.compose)
6771
}

application/src/desktopMain/kotlin/com/neoutils/neoregex/DesktopApp.kt

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import androidx.compose.material3.Icon
3030
import androidx.compose.material3.MaterialTheme.colorScheme
3131
import androidx.compose.material3.Text
3232
import androidx.compose.runtime.Composable
33+
import androidx.compose.runtime.LaunchedEffect
3334
import androidx.compose.runtime.collectAsState
3435
import androidx.compose.runtime.getValue
3536
import androidx.compose.ui.Alignment
@@ -39,12 +40,14 @@ import androidx.compose.ui.draw.clip
3940
import androidx.compose.ui.platform.LocalUriHandler
4041
import androidx.compose.ui.window.ApplicationScope
4142
import androidx.compose.ui.window.FrameWindowScope
42-
import androidx.compose.ui.window.WindowPosition
43-
import androidx.compose.ui.window.rememberWindowState
43+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
4444
import com.neoutils.neoregex.core.common.util.ColorTheme
4545
import com.neoutils.neoregex.core.common.util.rememberColorTheme
4646
import com.neoutils.neoregex.core.datasource.PreferencesDataSource
47+
import com.neoutils.neoregex.core.datasource.WindowStateDataSource
48+
import com.neoutils.neoregex.core.datasource.extension.observe
4749
import com.neoutils.neoregex.core.datasource.model.Preferences
50+
import com.neoutils.neoregex.core.datasource.remember.rememberWindowState
4851
import com.neoutils.neoregex.core.designsystem.theme.NeoTheme
4952
import com.neoutils.neoregex.core.designsystem.theme.NeoTheme.dimensions
5053
import com.neoutils.neoregex.core.resources.*
@@ -60,6 +63,9 @@ fun ApplicationScope.DesktopApp() = WithKoin {
6063
val preferencesDataSource = koinInject<PreferencesDataSource>()
6164
val preferences by preferencesDataSource.flow.collectAsState()
6265

66+
val windowStateDataSource = koinInject<WindowStateDataSource>()
67+
val windowState by windowStateDataSource.flow.collectAsState()
68+
6369
NeoTheme(
6470
colorTheme = when (preferences.colorTheme) {
6571
Preferences.ColorTheme.SYSTEM -> rememberColorTheme()
@@ -69,10 +75,11 @@ fun ApplicationScope.DesktopApp() = WithKoin {
6975
) {
7076
NeoWindow(
7177
header = { HeaderImpl() },
72-
windowState = rememberWindowState(
73-
position = WindowPosition.Aligned(Alignment.Center)
74-
)
78+
windowState = rememberWindowState(windowState)
7579
) {
80+
81+
windowStateDataSource.observe(window)
82+
7683
App()
7784
}
7885
}
@@ -82,7 +89,7 @@ fun ApplicationScope.DesktopApp() = WithKoin {
8289
private fun FrameWindowScope.HeaderImpl() = NeoHeader { padding ->
8390

8491
val preferencesDataSource = koinInject<PreferencesDataSource>()
85-
val preferences by preferencesDataSource.flow.collectAsState()
92+
val preferences by preferencesDataSource.flow.collectAsStateWithLifecycle()
8693

8794
Text(
8895
text = stringResource(Res.string.app_name),

build-logic/src/main/kotlin/com.neoutils.neoregex.core.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ kotlin {
6363
implementation(compose.materialIconsExtended)
6464
implementation(compose.components.resources)
6565

66+
// koin
6667
implementation(catalog.koin.core)
6768
implementation(catalog.koin.compose)
6869
}

core/datasource/src/commonMain/kotlin/com/neoutils/neoregex/core/datasource/di/PreferencesModule.kt renamed to core/datasource/src/androidMain/kotlin/com/neoutils/neoregex/core/datasource/di/DataSourceModule.android.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@
1919
package com.neoutils.neoregex.core.datasource.di
2020

2121
import com.neoutils.neoregex.core.datasource.PreferencesDataSource
22-
import com.neoutils.neoregex.core.datasource.settings.MultiplatformSettings
22+
import com.neoutils.neoregex.core.datasource.settings.PreferencesSettings
2323
import org.koin.dsl.bind
2424
import org.koin.dsl.module
2525

26-
val preferencesModule = module {
27-
single { MultiplatformSettings() } bind PreferencesDataSource::class
26+
actual val dataSourceModule = module {
27+
single { PreferencesSettings() } bind PreferencesDataSource::class
2828
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* NeoRegex.
3+
*
4+
* Copyright (C) 2024 Irineu A. Silva.
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
package com.neoutils.neoregex.core.datasource.di
20+
21+
import org.koin.core.module.Module
22+
23+
expect val dataSourceModule: Module

core/datasource/src/commonMain/kotlin/com/neoutils/neoregex/core/datasource/model/Preferences.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ import kotlinx.serialization.Serializable
2222

2323
@Serializable
2424
data class Preferences(
25-
val performanceLabelAlign: Alignment = Alignment.BOTTOM_END,
26-
val colorTheme: ColorTheme = ColorTheme.SYSTEM
25+
val performanceLabelAlign: Alignment,
26+
val colorTheme: ColorTheme
2727
) {
2828
@Serializable
2929
enum class Alignment {
@@ -37,5 +37,12 @@ data class Preferences(
3737
LIGHT,
3838
DARK
3939
}
40+
41+
companion object {
42+
val Default = Preferences(
43+
performanceLabelAlign = Alignment.BOTTOM_END,
44+
colorTheme = ColorTheme.SYSTEM
45+
)
46+
}
4047
}
4148

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,20 @@ import kotlinx.coroutines.flow.asStateFlow
2929
import kotlinx.serialization.ExperimentalSerializationApi
3030

3131
@OptIn(ExperimentalSerializationApi::class, ExperimentalSettingsApi::class)
32-
internal class MultiplatformSettings(
32+
internal class PreferencesSettings(
3333
private val settings: Settings = Settings()
3434
) : PreferencesDataSource {
3535

36-
private val _preferences = MutableStateFlow(
37-
settings.decodeValue(Preferences.serializer(), KEY, Preferences())
36+
private val _flow = MutableStateFlow(
37+
settings.decodeValue(
38+
serializer = Preferences.serializer(),
39+
defaultValue = Preferences.Default,
40+
key = KEY,
41+
)
3842
)
3943

40-
override val flow = _preferences.asStateFlow()
41-
override val current: Preferences get() = flow.value
44+
override val flow = _flow.asStateFlow()
45+
override val current get() = flow.value
4246

4347
override fun update(
4448
block: (Preferences) -> Preferences
@@ -47,7 +51,7 @@ internal class MultiplatformSettings(
4751
val preferences = block(flow.value)
4852

4953
settings.encodeValue(Preferences.serializer(), KEY, preferences)
50-
_preferences.value = preferences
54+
_flow.value = preferences
5155

5256
return preferences
5357
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* NeoRegex.
3+
*
4+
* Copyright (C) 2024 Irineu A. Silva.
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
package com.neoutils.neoregex.core.datasource
20+
21+
import com.neoutils.neoregex.core.datasource.model.WindowStateData
22+
import kotlinx.coroutines.flow.StateFlow
23+
24+
interface WindowStateDataSource {
25+
val flow: StateFlow<WindowStateData>
26+
val current: WindowStateData
27+
28+
fun update(block: (WindowStateData) -> WindowStateData): WindowStateData
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* NeoRegex.
3+
*
4+
* Copyright (C) 2024 Irineu A. Silva.
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
package com.neoutils.neoregex.core.datasource.di
20+
21+
import com.neoutils.neoregex.core.datasource.PreferencesDataSource
22+
import com.neoutils.neoregex.core.datasource.WindowStateDataSource
23+
import com.neoutils.neoregex.core.datasource.settings.PreferencesSettings
24+
import com.neoutils.neoregex.core.datasource.settings.WindowStateSettings
25+
import org.koin.dsl.bind
26+
import org.koin.dsl.module
27+
28+
actual val dataSourceModule = module {
29+
single { PreferencesSettings() } bind PreferencesDataSource::class
30+
single { WindowStateSettings() } bind WindowStateDataSource::class
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* NeoRegex.
3+
*
4+
* Copyright (C) 2024 Irineu A. Silva.
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
package com.neoutils.neoregex.core.datasource.extension
20+
21+
import androidx.compose.runtime.Composable
22+
import androidx.compose.runtime.DisposableEffect
23+
import androidx.compose.ui.awt.ComposeWindow
24+
import androidx.compose.ui.window.WindowPlacement
25+
import com.neoutils.neoregex.core.datasource.WindowStateDataSource
26+
import com.neoutils.neoregex.core.datasource.model.WindowStateData
27+
import java.awt.event.ComponentAdapter
28+
import java.awt.event.ComponentEvent
29+
import java.awt.event.WindowStateListener
30+
31+
@Composable
32+
fun WindowStateDataSource.observe(window: ComposeWindow) {
33+
34+
DisposableEffect(window) {
35+
val componentListener = object : ComponentAdapter() {
36+
override fun componentMoved(e: ComponentEvent) {
37+
if (window.placement == WindowPlacement.Floating) {
38+
update {
39+
it.copy(
40+
position = WindowStateData.Position(
41+
x = window.x,
42+
y = window.y
43+
)
44+
)
45+
}
46+
}
47+
}
48+
49+
override fun componentResized(e: ComponentEvent) {
50+
if (window.placement == WindowPlacement.Floating) {
51+
update {
52+
it.copy(
53+
size = WindowStateData.Size(
54+
width = window.width,
55+
height = window.height
56+
)
57+
)
58+
}
59+
}
60+
}
61+
}
62+
63+
val windowListener = WindowStateListener {
64+
update {
65+
it.copy(
66+
placement = when (window.placement) {
67+
WindowPlacement.Floating -> WindowStateData.Placement.FLOATING
68+
WindowPlacement.Maximized -> WindowStateData.Placement.MAXIMIZED
69+
WindowPlacement.Fullscreen -> WindowStateData.Placement.FULLSCREEN
70+
}
71+
)
72+
}
73+
}
74+
75+
window.addComponentListener(componentListener)
76+
window.addWindowStateListener(windowListener)
77+
78+
onDispose {
79+
window.removeComponentListener(componentListener)
80+
window.removeWindowStateListener(windowListener)
81+
}
82+
}
83+
}

0 commit comments

Comments
 (0)