Skip to content

Commit

Permalink
Change Settings Screen to a Popup Dialog (#36)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
alonalbert authored May 6, 2024
1 parent dae4a28 commit 93e65c8
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 46 deletions.
69 changes: 38 additions & 31 deletions src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/KotlinExplorer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 })
}
}
}
Expand All @@ -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)
Expand All @@ -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
)
}
}

Expand Down Expand Up @@ -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
)
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Menu.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
5 changes: 5 additions & 0 deletions src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Paths.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
71 changes: 59 additions & 12 deletions src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Settings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,19 @@
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
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
Expand All @@ -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<String>,
kotlinHome: MutableState<String>,
onSaveClick: () -> Unit
) {
this.androidHome = androidHome.value
this.kotlinHome = kotlinHome.value
this.reloadToolPathsFromSettings()
onSaveClick()
}

@Composable
Expand All @@ -81,7 +129,6 @@ private fun StringSetting(title: String, state: MutableState<String>, isValid: (
trailingIcon = { if (isValid()) ValidIcon() else ErrorIcon() }
)
}
Spacer(Modifier.height(8.dp))
}

@Composable
Expand Down
2 changes: 1 addition & 1 deletion src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/State.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> = readSettings(file)

Expand Down
75 changes: 75 additions & 0 deletions src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/SwingPanel.kt
Original file line number Diff line number Diff line change
@@ -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 <T : Component> 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
}

0 comments on commit 93e65c8

Please sign in to comment.