From cb48f67a50c3d2910b11c5e3c27780fa54aa2fa6 Mon Sep 17 00:00:00 2001 From: Alon Albert Date: Mon, 6 May 2024 15:06:54 -0700 Subject: [PATCH] Change Settings Screen to a Popup Dialog SwingPanel doesn't work with Dialogs so I added DialogSupportingSwingPanel which works around the problem by grabbing a screenshot of its component and rendering an Image when the dialog is visible. Some more changes were made to work better with a popup: * Wrap the settings with a Material Card * Add a Cancel button * Move buttons to the bottom right * Update the icons on the fly * Disable the Save button if paths are invalid * Allow dismissing the dialog with an invalid state but disable the Compile menu item. This should only be possible on first run. --- .../kotlin/explorer/KotlinExplorer.kt | 69 +++++++++-------- .../dev/romainguy/kotlin/explorer/Menu.kt | 4 +- .../dev/romainguy/kotlin/explorer/Paths.kt | 5 ++ .../dev/romainguy/kotlin/explorer/Settings.kt | 71 +++++++++++++++--- .../dev/romainguy/kotlin/explorer/State.kt | 2 +- .../romainguy/kotlin/explorer/SwingPanel.kt | 75 +++++++++++++++++++ 6 files changed, 180 insertions(+), 46 deletions(-) create mode 100644 src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/SwingPanel.kt diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/KotlinExplorer.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/KotlinExplorer.kt index ccafa315..df9770b1 100644 --- a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/KotlinExplorer.kt +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/KotlinExplorer.kt @@ -25,7 +25,6 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier -import androidx.compose.ui.awt.SwingPanel import androidx.compose.ui.input.key.Key.Companion.D import androidx.compose.ui.input.key.Key.Companion.F import androidx.compose.ui.input.key.Key.Companion.G @@ -128,9 +127,9 @@ private fun FrameWindowScope.KotlinExplorer( val findDialog = remember { FindDialog(window, searchListener).apply { searchContext.searchWrap = true } } var showSettings by remember { mutableStateOf(!explorerState.toolPaths.isValid) } - val sourcePanel: @Composable () -> Unit = { SourcePanel(sourceTextArea, explorerState) } - val dexPanel: @Composable () -> Unit = { TextPanel("DEX", dexTextArea, explorerState) } - val oatPanel: @Composable () -> Unit = { TextPanel("OAT", oatTextArea, explorerState) } + val sourcePanel: @Composable () -> Unit = { SourcePanel(sourceTextArea, explorerState, showSettings) } + val dexPanel: @Composable () -> Unit = { TextPanel("DEX", dexTextArea, explorerState, showSettings) } + val oatPanel: @Composable () -> Unit = { TextPanel("OAT", oatTextArea, explorerState, showSettings) } var panels by remember { mutableStateOf(explorerState.getPanels(sourcePanel, dexPanel, oatPanel)) } val updatePresentationMode: (Boolean) -> Unit = { @@ -158,27 +157,27 @@ private fun FrameWindowScope.KotlinExplorer( ) if (showSettings) { - Settings( - explorerState, - onSaveClick = { showSettings = !explorerState.toolPaths.isValid } + Settings(explorerState, onDismissRequest = { showSettings = false }) + } + + Column(modifier = Modifier.background(JewelTheme.globalColors.paneBackground)) { + MultiSplitter(modifier = Modifier.weight(1.0f), panels) + StatusBar(status, progress) + } +} + +@Composable +private fun StatusBar(status: String, progress: Float) { + Row(verticalAlignment = CenterVertically) { + val width = 160.dp + Text( + modifier = Modifier + .widthIn(min = width, max = width) + .padding(8.dp), + text = status ) - } else { - Column( - modifier = Modifier.background(JewelTheme.globalColors.paneBackground) - ) { - MultiSplitter(modifier = Modifier.weight(1.0f), panels) - Row(verticalAlignment = CenterVertically) { - val width = 160.dp - Text( - modifier = Modifier - .widthIn(min = width, max = width) - .padding(8.dp), - text = status - ) - if (progress < 1) { - LinearProgressIndicator({ progress }) - } - } + if (progress < 1) { + LinearProgressIndicator({ progress }) } } } @@ -200,10 +199,10 @@ private fun ExplorerState.getPanels( } @Composable -private fun SourcePanel(sourceTextArea: RSyntaxTextArea, explorerState: ExplorerState) { +private fun SourcePanel(sourceTextArea: RSyntaxTextArea, explorerState: ExplorerState, showSettings: Boolean) { Column { Title("Source") - SwingPanel( + DialogSupportingSwingPanel( modifier = Modifier.fillMaxSize(), factory = { RTextScrollPane(sourceTextArea) @@ -213,19 +212,22 @@ private fun SourcePanel(sourceTextArea: RSyntaxTextArea, explorerState: Explorer sourceTextArea.text = explorerState.sourceCode } sourceTextArea.updateStyle(explorerState) - } + }, + isDialogVisible = showSettings, ) } } @Composable -private fun TextPanel(title: String, textArea: RSyntaxTextArea, explorerState: ExplorerState) { +private fun TextPanel(title: String, textArea: RSyntaxTextArea, explorerState: ExplorerState, showSettings: Boolean) { Column { Title(title) - SwingPanel( + DialogSupportingSwingPanel( modifier = Modifier.fillMaxSize(), factory = { RTextScrollPane(textArea) }, - update = { textArea.updateStyle(explorerState) }) + update = { textArea.updateStyle(explorerState) }, + isDialogVisible = showSettings + ) } } @@ -317,7 +319,12 @@ private fun FrameWindowScope.MainMenu( } Menu("Compilation") { MenuCheckboxItem("Optimize with R8", CtrlShift(O), explorerState::optimize) - MenuItem("Compile & Disassemble", CtrlShift(D), onClick = compileAndDisassemble) + MenuItem( + "Compile & Disassemble", + CtrlShift(D), + onClick = compileAndDisassemble, + enabled = explorerState.toolPaths.isValid + ) } } } diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Menu.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Menu.kt index d7892385..5f351a4b 100644 --- a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Menu.kt +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Menu.kt @@ -46,6 +46,6 @@ fun MenuScope.MenuCheckboxItem( } @Composable -fun MenuScope.MenuItem(text: String, shortcut: Shortcut, onClick: () -> Unit) { - Item(text, shortcut = shortcut.asKeyShortcut(), onClick = onClick) +fun MenuScope.MenuItem(text: String, shortcut: Shortcut, onClick: () -> Unit, enabled: Boolean = true) { + Item(text, enabled = enabled, shortcut = shortcut.asKeyShortcut(), onClick = onClick) } diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Paths.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Paths.kt index 51c8284a..913ff542 100644 --- a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Paths.kt +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Paths.kt @@ -24,6 +24,11 @@ import kotlin.io.path.extension import kotlin.jvm.optionals.getOrElse class ToolPaths(settingsDirectory: Path, androidHome: Path, kotlinHome: Path) { + constructor(settingsDirectory: Path, androidHome: String, kotlinHome: String) : this( + settingsDirectory, + Path.of(androidHome), + Path.of(kotlinHome) + ) val tempDirectory = Files.createTempDirectory("kotlin-explorer")!! val platform: Path val d8: Path diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Settings.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Settings.kt index 7d67f51d..82a04ede 100644 --- a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Settings.kt +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Settings.kt @@ -19,6 +19,8 @@ package dev.romainguy.kotlin.explorer import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf @@ -26,7 +28,10 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog import org.jetbrains.jewel.ui.component.DefaultButton import org.jetbrains.jewel.ui.component.Icon import org.jetbrains.jewel.ui.component.Text @@ -35,32 +40,75 @@ import org.jetbrains.jewel.ui.component.TextField @Composable fun Settings( explorerState: ExplorerState, - onSaveClick: () -> Unit + onDismissRequest: () -> Unit ) { - val androidHome = remember { mutableStateOf(explorerState.androidHome) } - val kotlinHome = remember { mutableStateOf(explorerState.kotlinHome) } + Dialog(onDismissRequest = onDismissRequest) { + SettingsContent(explorerState, onDismissRequest) + } +} + +@Composable +private fun SettingsContent( + state: ExplorerState, + onDismissRequest: () -> Unit +) { + val androidHome = remember { mutableStateOf(state.androidHome) } + val kotlinHome = remember { mutableStateOf(state.kotlinHome) } - Box(modifier = Modifier.fillMaxSize()) { - Column(modifier = Modifier.align(Alignment.Center)) { - StringSetting("Android home directory: ", androidHome) { explorerState.toolPaths.isAndroidHomeValid } - StringSetting("Kotlin home directory: ", kotlinHome) { explorerState.toolPaths.isKotlinHomeValid } + Card(shape = RoundedCornerShape(8.dp)) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(16.dp)) { + Title() - DefaultButton({ explorerState.saveState(androidHome, kotlinHome, onSaveClick) }) { - Text("Save") + val toolPaths = ToolPaths(state.directory, androidHome.value, kotlinHome.value) + StringSetting("Android home directory: ", androidHome) { toolPaths.isAndroidHomeValid } + StringSetting("Kotlin home directory: ", kotlinHome) { toolPaths.isKotlinHomeValid } + + Spacer(modifier = Modifier.height(32.dp)) + val onSaveClick = { + state.saveState(androidHome, kotlinHome) + onDismissRequest() } + Buttons(saveEnabled = toolPaths.isValid, onSaveClick, onDismissRequest) + } + } +} + +@Composable +private fun Title() { + Text( + "Settings", + fontSize = 24.sp, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp) + ) +} + +@Composable +private fun ColumnScope.Buttons( + saveEnabled: Boolean, + onSaveClick: () -> Unit, + onCancelClick: () -> Unit +) { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.align(Alignment.End)) { + DefaultButton(onClick = onCancelClick) { + Text("Cancel") + } + DefaultButton(enabled = saveEnabled, onClick = onSaveClick) { + Text("Save") } } + } private fun ExplorerState.saveState( androidHome: MutableState, kotlinHome: MutableState, - onSaveClick: () -> Unit ) { this.androidHome = androidHome.value this.kotlinHome = kotlinHome.value this.reloadToolPathsFromSettings() - onSaveClick() } @Composable @@ -81,7 +129,6 @@ private fun StringSetting(title: String, state: MutableState, isValid: ( trailingIcon = { if (isValid()) ValidIcon() else ErrorIcon() } ) } - Spacer(Modifier.height(8.dp)) } @Composable diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/State.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/State.kt index 7b979fb5..3dacc2bf 100644 --- a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/State.kt +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/State.kt @@ -40,7 +40,7 @@ private const val Placement = "WINDOW_PLACEMENT" @Stable class ExplorerState { - private val directory = settingsPath() + val directory: Path = settingsPath() private val file: Path = directory.resolve("settings") private val entries: MutableMap = readSettings(file) diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/SwingPanel.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/SwingPanel.kt new file mode 100644 index 00000000..31693b6e --- /dev/null +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/SwingPanel.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2023 Romain Guy + * + * 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. + */ + +@file:Suppress("FunctionName") + +package dev.romainguy.kotlin.explorer + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.NoOpUpdate +import androidx.compose.ui.awt.SwingPanel +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toComposeImageBitmap +import java.awt.Component +import java.awt.GraphicsEnvironment +import java.awt.image.BufferedImage + + +/** + * A [SwingPanel] that supports a Dialog rendered over it + * + * When [isDialogVisible] is true, the panel captures a screenshot its [Component] and renders an [Image] instead of + * the actual [SwingPanel]. The [SwingPanel] is still rendered in order to keep it attached to the component hierarchy. + */ +@Composable +fun DialogSupportingSwingPanel( + background: Color = Color.White, + factory: () -> T, + modifier: Modifier = Modifier, + update: (T) -> Unit = NoOpUpdate, + isDialogVisible: Boolean, +) { + val component = remember { factory() } + + Column(modifier = modifier) { + val fillMaxSize = Modifier.fillMaxSize() + if (isDialogVisible) { + val bitmap = remember { component.getScreenShot()?.toComposeImageBitmap() } + if (bitmap != null) { + Image(bitmap = bitmap, contentDescription = "", modifier = fillMaxSize) + } else { + Box(modifier = fillMaxSize) + } + } + SwingPanel(background, { component }, fillMaxSize, update) + } +} + +fun Component.getScreenShot(): BufferedImage? { + if (width == 0 || height == 0) { + return null + } + val config = GraphicsEnvironment.getLocalGraphicsEnvironment().defaultScreenDevice.defaultConfiguration + val image = config.createCompatibleImage(width, height) + paint(image.graphics) + return image +}